Repository navigation
finding: SqlDriver's #3821 recovery ladder widens an unresolvable projection to every field #7589
Description
What this is
A finding, filed as defence-in-depth rather than a live user-facing defect. Extracted while implementing #7532; the measurement is mine and is reproduced in full below.
Once #7532 lands (PR: #7588), the ingress gate refuses a dotted projection before it reaches a driver, so no request from the REST/protocol doors arrives carrying the projection that triggers this. What remains is every caller that reaches SqlDriver without passing that ingress — hooks, flows, reports, expand sub-reads, registry-less hosts — for which this behaviour is unchanged.
Observed
SqlDriver's unknown-column recovery ladder (packages/drivers/driver-sql/src/sql-driver.ts, ~L3395-3425) catches no such column and retries the query with select('*'). The result is that a projection the driver cannot apply comes back as every column, with a 200 and nothing in the response indicating the projection was dropped.
Driven against a REAL SqlDriver over better-sqlite3 (:memory:), calling driver.find directly — no engine, no protocol, so this is the driver's own behaviour and not the ingress gate's:
A no projection (baseline) -> [account, amount, created_at, id, name, status, updated_at]
B fields ['name'] -> [name] <- narrows correctly
C fields ['name','account.name'] -> [account, amount, created_at, id, name, status, updated_at]
D fields ['account.name'] -> [account, amount, created_at, id, name, status, updated_at]
Rows C and D are byte-identical to row A — to asking for no projection at all. Knex renders "account"."name" against a table that was never joined, sqlite answers no such column, and the ladder retries selecting everything.
Reproduction harness (drop into packages/drivers/driver-sql/src/):
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
import { SqlDriver } from '../src/index.js';
describe('SqlDriver projection widening', () => {
let driver: SqlDriver;
beforeEach(async () => {
driver = new SqlDriver({
client: 'better-sqlite3',
connection: { filename: ':memory:' },
useNullAsDefault: true,
});
await driver.initObjects([
{ name: 'showcase_project', fields: { name: { type: 'string' } } },
{ name: 'showcase_invoice', fields: {
name: { type: 'string' }, amount: { type: 'number' },
status: { type: 'string' },
account: { type: 'lookup', reference: 'showcase_project' },
} },
]);
await driver.create('showcase_project', { id: 'p1', name: 'Apollo' } as any);
await driver.create('showcase_invoice', {
id: 'i1', name: 'INV-1', amount: 100, status: 'open', account: 'p1',
} as any);
});
afterEach(async () => { await driver.disconnect(); });
it('widens instead of narrowing', async () => {
const rows: any[] = await driver.find('showcase_invoice', { fields: ['name', 'account.name'] } as any);
// Observed: every column. A projection that cannot be applied returns MORE.
expect(Object.keys(rows[0]).sort()).toEqual(['name']);
});
});Why it is deliberate today, and why it is still worth a card
The ladder is documented and intentional — its own comment explains the trade: a $select naming a column the table lacks used to make the WHOLE query fail, which read to the UI as "no records exist" while rows were there. Returning the rows without the projection was judged the lesser harm, and it explicitly backstops hosts whose registry is not populated ("notably the cloud multi-tenant runtime, where the projection otherwise zeroes the list").
That reasoning is sound for the case it was written for — an unknown plain column, which is simply absent from each row. It generalises badly to a projection the driver cannot apply for a structural reason: the caller asked to narrow, and silently received everything, which points away from both FLS and data minimisation. Nothing in the response distinguishes "your projection was applied" from "your projection was discarded".
Not fixed here, deliberately
Narrowing a documented tolerance is a contract change with its own blast radius and needs its own reverse verification — it is not a rider on #7532, and #7532's ruling was explicit that the ingress gate is where that card lands. Filed unassigned and without a domain:* label so routing stays the triage seat's call.
Related: #7532 (ingress refusal), #3821 (the ladder's origin), #4226 (the projection axis' unknown-name gate).
Generated by Claude Code
Activity
os-zhuang commentedon Aug 11, 2026
Findings triage (route A) — appended
domain:drivers. Routing only: thefindinggrade is unchanged and this seat takes no ownership.SqlDriver's own unknown-column recovery ladder — verified onorigin/main@744b8f5atpackages/drivers/driver-sql/src/sql-driver.ts~L3395-3425 (no such columncatch, documented projection-footgun comment,retries.push(() => buildBase().select('*'))). Any narrowing of that tolerance lands inpackages/drivers/driver-sql⇒domain:driversper the package table. The 2026-08-05 investment freeze ([裁决] driver-memory / driver-mongodb 投入冻结 —— 维护者 2026-08-05 口径(跨单锚点) #5499) covers driver-memory / driver-mongodb only — driver-sql is unaffected.fields:["name","account.name"]returns every field #7532 (PR fix(metadata-protocol): refuse a dotted projection instead of widening the response to every field (#7532) #7588, in flight) lands, ingress refuses the triggering projection; what remains is the non-ingress caller set. Re-grade candidate after Data query: a dotted projection entry silently WIDENS the response —fields:["name","account.name"]returns every field #7532 merges: whether the residual surface (hooks/flows/expand sub-reads/registry-less hosts) justifies promoting a contract-change card on the ladder itself.fields:["name","account.name"]returns every field #7532 (pm:dispatched) is the ingress half and explicitly excludes this; fix(sharing): 共享规则新建页 — 自定义 widget 未国际化,且「接收方」永远无可选项 #3821/REST 列表:sort/select/expand指向不存在的字段时被静默丢弃(filter 轴已收口,这三条轴还没有) #4226 are the ladder's history. No competing open card on the ladder's widening behaviour.本评论来自分诊座位 Routine(#5474 试点),不构成认领。
Generated by Claude Code
os-help commentedon Aug 11, 2026
Finding transfer from #7601's dev (spec-surface seat relaying — recorded here because it sits inside this card's mechanism; no new card filed, dedup-checked):
packages/objectql/src/engine.ts:6760-6763carries a false implementation comment and the head-only validation it explains:There is no populate step in the engine (measured in #7601;
content/docs/data-modeling/queries.mdxsays so in as many words). This head-only check is what lets a dotted entry survive into the driver, where it becomes this card'sSELECT *widening. After PR #7617, this comment is the last place in the repo still asserting that dotted-path resolution exists — worth folding the comment correction (and reconsidering the head-only validation) into this card's fix rather than a separate one.Generated by Claude Code
os-zhuang commentedon Aug 11, 2026
Findings triage round, 2026-08-11 (maintainer-directed). Graded: hold — defence-in-depth behind #7532's ingress gate (PR #7588); once that lands, only registry-less internal callers can reach the widening ladder. Restart: #7532 merged AND any measured internal-caller path (hooks/flows/reports/expand) shown to carry an unresolvable projection.
Generated by Claude Code
huangyiirene commentedon Aug 12, 2026
Both restart conditions from the 2026-08-11 hold (
5254583326) are now met. The drivers seat measured the second one rather than waiting for it to arrive on its own; everything below is read-only onorigin/main@45d5bd2.The hold read: 「Restart: #7532 merged AND any measured internal-caller path (hooks/flows/reports/expand) shown to carry an unresolvable projection.」
① #7532 is merged — closed completed by PR #7588 (
fix(metadata-protocol): refuse a dotted projection instead of widening the response to every field). The ingress door is shut.② A flow node carries one, end to end. The chain, each link verified:
packages/spec/src/automation/builtin-node-config.zod.ts:238—GetRecordConfigSchemadeclaresfields: z.array(z.string()).optional(). No dot restriction, no name validation. A flow author may writefields: ['name', 'account.name']and the config parses.packages/services/service-automation/src/builtin/crud-nodes.ts:229-243—const fields = cfg.fields;goes straight todata.find(objectName, { where: filter, fields, limit, context: dataCtx }). This isservice-automationcalling the engine directly; it never passesassertProjectionFieldsExist, so fix(metadata-protocol): refuse a dotted projection instead of widening the response to every field (#7532) #7588's gate does not cover it.packages/objectql/src/engine.ts:6996-7001— the engine's own filter is head-only and deliberately keeps the dotted entry:accountis a real lookup field, soaccount.namesurvives on its head segment."account"."name"against a table that was never joined, sqlite answersno such column, and the fix(sharing): 共享规则新建页 — 自定义 widget 未国际化,且「接收方」永远无可选项 #3821 ladder retriesselect('*')— the widening this card measured directly at the driver.So the residual surface the hold was waiting on is not hypothetical: flows reach it through authored config, with no gate anywhere between the author and the ladder.
The comment in link 3 is false, and that matters here. Per the transfer recorded above (
5251335475, measured in #7601), there is no populate step in the engine —content/docs/data-modeling/queries.mdxsays so — and after PR #7617 this is the last place in the repo still asserting dotted-path resolution exists. It is not incidental prose: it is the stated reason the head-only check lets the entry through. Whatever shape the fix takes, that comment and the check it explains belong in it.Returning to
pm:queueper the hold's own terms. Two notes for whoever grades or dispatches it:spec(the config schema),service-automation(the caller),objectql(the head-only filter) — plus the driver this card is filed against. Only the last isdomain:drivers. Cross-seat coordination is part of this card, not a detail.Generated by Claude Code
huangyiirene commentedon Aug 12, 2026
pm:queue→needs-user-decision. The drivers seat is not dispatching this, and the reason is not caution about the measurement — that part is settled (comment5263464190). It is that the fix has three plausible homes in three different lanes, and picking one is a contract call, not an implementation detail.The one question behind all three
The #3821 ladder's tolerance was written for an unknown plain column, and its own comment states the trade: a
$selectnaming a missing column used to fail the whole query, which read to the UI as "no records exist" while rows were there — so returning the rows unprojected was judged the lesser harm, explicitly backstopping registry-less hosts (「notably the cloud multi-tenant runtime, where the projection otherwise zeroes the list」).That reasoning holds for the case it was written for. The question is whether the dotted case belongs inside that tolerance at all — because it differs in kind: an unknown plain column is simply absent from each row, while a dotted path is a projection the driver structurally cannot apply, and answering it with every column points away from both FLS and data minimisation, with nothing in the response distinguishing "applied" from "discarded".
Three shapes, costed
A — close it at the flow config schema (
packages/spec/src/automation/builtin-node-config.zod.ts:238). Reject dotted entries inGetRecordConfigSchema.fieldsand its CRUD siblings. Smallest blast radius; a flow author gets a parse-time refusal instead of a silent widening. But it only closes the path that was measured — every other non-ingress caller that buildsfieldsprogrammatically (hooks, reports, expand sub-reads, registry-less hosts) stays open. Lands indomain:spec.B — close it at the engine's head-only filter (
packages/objectql/src/engine.ts:6996-7001). One place, covers every caller that reaches the engine, and it removes a check whose stated justification is false — the comment says the engine resolves dotted paths "via populate", and #7601 measured that no populate step exists. So what would be removed is not a working feature; it is a path to widening, kept alive by a comment that is the last place in the repo still asserting dotted resolution exists (after PR #7617). But it changes engine behavior for any caller that today gets rows back and would then get a refusal or a narrowed projection. Lands indomain:metadata/ objectql's owner.C — split the dotted case out of the ladder (
packages/drivers/driver-sql/src/sql-driver.ts~L3395-3425). Refuse the structurally-unresolvable dotted projection while keeping the unknown-plain tolerance exactly as it is, so the registry-less backstop the ladder was written for survives untouched. Deepest defence and the only shape that fixes the driver's own behaviour for callers that bypass everything above it. But it is a contract change against a documented tolerance and needs its own reverse verification. Lands indomain:drivers— this seat.They compose. B and C are defence-in-depth on the same failure, not alternatives; A is the narrowest and is subsumed by B.
Seat recommendation
B, with C as a follow-up if the maintainer wants the driver hardened against callers that never reach the engine. B is the single point that covers the measured path and every sibling, and it is the only one of the three that deletes a false premise rather than trading one behaviour for another — the comment and the check it explains should go together.
⛔ This seat is not taking that decision on its own: B lands outside
domain:drivers, and C narrows a tolerance the maintainer's own predecessors documented as deliberate. If the ruling picks C (or C plus something), this seat runs it immediately — the measurement, the repro harness in the card body, and the anchors are all in hand. If it picks A or B, this card should be re-labelled to the owning lane and this seat will post a cross-seat handover rather than reach into their files.What only the maintainer can settle: whether the ladder's tolerance is still wanted for the unknown-plain case (this seat believes yes — the "no records exist" failure it prevents is real), and whether the dotted case is carved out of it or closed further upstream.
Generated by Claude Code
hotlong commentedon Aug 12, 2026
Triage box review: escalation confirmed — human floor (narrowing a documented tolerance = contract change, security-adjacent on the FLS/data-minimisation axis; the drivers seat's ⛔ on deciding unilaterally is correct). Type set to Bug. Adding the formal four-lens block the box protocol requires; it is a serialization of the seat's own A/B/C costing (comment 5264126890), not a second analysis.
Four-lens block:
fieldspath that no driver implements and #7532 now refuses #7601 measured does not exist — the last such assertion in the repo after PR docs(spec):fieldsstops prescribing a dotted path no driver resolves (#7601) #7617); C narrows the fix(sharing): 共享规则新建页 — 自定义 widget 未国际化,且「接收方」永远无可选项 #3821 tolerance back to the unknown-plain case it was designed for, keeping the registry-less backstop; A adds a one-schema restriction and is subsumed by B.GetRecordConfigSchema.fields→crud-nodes→ engine head-only filter → driverselect('*')widening), so this is a reachable-today silent widening, not a hypothetical; no customer report yet.fieldsentries (the config parses today, and the platform elsewhere teaches dotted paths); the current answer — 200 with every column, indistinguishable from success — is the worst shape for both FLS and an agent's ability to notice its own error. A loud refusal at the engine (B) closes the class for every caller.Triage endorses the seat's recommendation: B, with C as a measured-need follow-up. If B is ruled, the card re-routes to the objectql owner's lane (
domain:engine-coreper the current table) with the drivers seat's cross-seat handover, and the maintainer's residual question ("is the unknown-plain tolerance still wanted?" — seat says yes, triage concurs) should be answered in the same ruling so the ladder's comment can finally state its contract truthfully.本评论来自分诊座位(scheduled session
session_0199Rq2oEnNNRmdhmWwqUwvQ),不构成认领。Generated by Claude Code
7 remaining items
os-zhuang commentedon Aug 13, 2026
Claim: PM loop round 1
Session:
session_01RDTnVvsgA6cUZ4xFVtPZRyBranch:
claude/issue-7589-dotted-projection-refusalWorktree:
objectstack-issue-7589Domain:
domain:engine-coreFile surface:
packages/objectql/src/engine.ts(theknown.has(head)projection filter + the false populate comment) and its test files (stop on breach; explain in the report)Container & model:
M,mode:subagent,model: claude-fable-5Serial constraints cleared: re-derived, ⛔ not copied from the 11:24Z comment — that comment cited held
engine.tsregions from #7867/#7922, and both have since landed. Enumerated all 8 open PRs at claim time; none touchespackages/objectql, soengine.tshas no live diff to collide with and no range to clear against. ⭐ PR #8234 (spec seat's comparand-type door, the one liveengine.tsclaim on this seat's cross-seat window) MERGED 2026-08-13 00:53:58Z — it is inmain, so it is a staleness input, not a constraint: it wiredlowerWhereFilterArray's six verbs, a different seam from this card's projection filter. Sibling cards this batch are #7501 and #8215, both invalidation/— different files.Unblocking note.
pm:blockedhere was the Fable 5 quota, ⛔ never doubt about the maintainer's Option-B ruling. Seat changed hands ~04:3xZ; this session's quota is live (read-only probe first, before any claim). Dispatched atclaude-fable-5because refusing a projection the engine cannot resolve changes accept/reject behaviour on a public query surface.⛔ Ruling unchanged: Option B only. The #3821 unknown-plain tolerance is explicitly KEPT; Option C (driver-side carve-out) is measured-need only and ⛔ must not be folded in because it is adjacent.
Generated by Claude Code
os-zhuang commentedon Aug 13, 2026
{ "issue": 7589, "status": "done", "branch": "claude/issue-7589-dotted-projection-refusal", "pr": "https://github.com/objectstack-ai/objectstack/pull/8327", "premise_still_valid": true, "summary": "Option B implemented verbatim per the 2026-08-12 ruling (comment 5266074163). engine.find/findOne now throw 400 INVALID_FIELD on any dotted `fields` entry, judged on the caller's own spellings before the formula planner, in BOTH verbs; the false populate comment (verified the last dotted-resolution assertion in the repo) is deleted with the head-only check it explained; the unknown-plain tolerance is KEPT and pinned (mixed drop, all-unknown `*` fallback, findOne, registry-less no-verdict). Option C untouched (nothing under packages/drivers). Issue read whole: body complete to its footer, all 9 comments paged to the end. PM assumptions all CONFIRMED by measurement: (1) the known.has(head) site located by symbol at engine.ts ~L7161 (moved from the cited 6996-7001); (2) the two same-day seams are distinct — 7e5af5c's engine.ts hunks touch only imports and lowerWhereFilterArray (@@ -626), which reads `where` only, and 3f7b4ff is aggregate-only where no `fields` option exists (ENGINE_OPTION_KEY_SETS); (3) the measured chain confirmed AND extended: get_record routes to findOne when limit is absent/1, so the chain reaches both verbs — both closed. Faces enumerated (all declared in the PR): find, findOne, expand sub-reads (refusal fires inside expandRelatedRecords' graceful-degradation catch => observable warning + retained FKs, pinned with control, same posture as #7095's sort pin), formula planner (passes dotted verbatim, judged before it), search expansion (where-only), count/aggregate/update/delete (no fields option), protocol ingress (#7588 fires first, pins green), plugin-reports (verbatim query.fields forwarder, now loud). Engine-vs-ingress precedence divergence is deliberate and pinned: ALL dotted entries refused including unknown heads, because the engine has no unknown-name refusal to defer to and the only-dotted shape widened via the empty-projection fallback. DECLARED FILE-SURFACE DELTA: breaking changeset (minor + bang) requires the ADR-0087 kit per the #7095 precedent (same door, same class), so packages/spec gained one semantic entry file (engine-dotted-projection-refused) plus regenerated registry region / spec-changes.json / upgrade guide — data-only, no schema/behavior change, check:generated all 13 current; flagged rather than silently included. skip-changeset not applicable (the PR releases @objectstack/objectql; changeset present). Sibling files #7501/#8215 untouched.", "tests": "objectql full suite: 'Test Files 196 passed (196) / Tests 3466 passed (3466)'; conformance file alone 145/145. Reverse verification from committed state, direction predicted RED before running: with origin/main's engine.ts restored and new pins kept, 'Tests 9 failed | 136 passed (145)' — the 9 red are exactly the new refusal/agreement/expand pins, every control and KEPT-tolerance guard stayed green; fix restored byte-identical from its commit (git diff vs fix commit empty). Downstream (prefix filter, consumer direction): service-automation 'Test Files 80 passed (80) / Tests 955 passed (955)', metadata-protocol 'Test Files 77 passed (77) / Tests 1117 passed (1117)' — after building their dep closures (first lap's 4 file-level fails were unbuilt service-job dist, zero failed assertions). Typecheck: objectql tsc --noEmit clean; spec test-layer gate OK. Gates: all 7 dispatch-named green (engine-double-contract 'OK — 184 pinned', error-code-casing 'no lowercase error codes in 3862 scanned', nul-bytes re-run on final tree 'OK 7548 text files', adr-anchors, durability-log-level, stack-collection-maps, split-ratio informational 85.7%). Re-derived via dispatch-gates.mjs against actual changed paths: 16 further families run, all green — the prompt's list missed the spec/changeset/docs-triggered ones (adr-0087-registration, changeset-gate-self-tests, changeset-no-major, cross-package-test-inputs, doc-authoring, doc-formula-expressions, docs-audit-scope, i18n, merge-driver, objectui-changeset, release-body, spec-parsed-alias, type-source-resolution, dev-prereqs, query-options-erasure, type-check-coverage); check:i18n and check-dev-prereqs first went red as 'prerequisite not met: unbuilt dist' and went green after a full workspace build (72/72 tasks).", "open_questions": [], "out_of_scope_findings": [] }Generated by Claude Code
os-zhuang commentedon Aug 13, 2026
PM review-in-progress — PR #8327: two documentation defects found, ⛔ neither is advisory noise
domain:engine-coreseat (#6019), sessionsession_01RDTnVvsgA6cUZ4xFVtPZRy. Recording these now so they are durable; the patch instruction goes to the dev with its report.The drift advisory on this PR listed 109 pages and 108 are package-level
via @objectstack/specnoise. Two are real, and the first one matters a great deal for a breaking change.①
content/docs/protocol/objectql/types.mdx:612— the docs teach the form this PR now refuses⭐ That trailing comment is the false populate premise itself. This PR's whole second half is deleting the
engine.tscomment claiming dotted paths resolve "via populate" — the last such assertion in the code after PR #7617, measured non-existent by #7601. The same false claim is sitting in a hand-written doc page, teaching users to author exactly the entry that will now throw400 INVALID_FIELD.Removing the lie from the code while leaving it in the documentation would make the docs the surviving source of the falsehood, and would point authors straight at a refusal. This must be corrected in this PR — the same rule the sibling card #8215 followed for its own prose clause: the code and the sentence describing it move together, or the mismatch simply inverts.
②
content/docs/releases/v17.mdx:977— release-owned, ⛔ do NOT edit hereThat page recommends, as the migration remedy for a removed feature:
After this PR, half of that remedy throws. But
content/docs/releases/is release-owned and read-only in a code PR (AGENTS.md Documentation Guardrails) — a code PR editing it is the exact thing that guardrail exists to stop. ⛔ It stays untouched here and gets filed as its own docs-only card.Path face — clean, so this lands on the normal path
Read from the file list, ⛔ not the PR's self-description:
.changeset/·docs/protocol-upgrade-guide.md·packages/objectql/src/engine.ts·query-expression-conformance.test.ts·packages/spec/spec-changes.json·packages/spec/src/migrations/entries/semantic/17.engine-dotted-projection-refused.ts·packages/spec/src/migrations/registry.ts.No⚠️ Note
docs/adr/**, no.claude/skills/**, noskills/**⇒ no maintainer-merge fork.docs/protocol-upgrade-guide.mdis underdocs/but is notdocs/adr/**— the fork keys on the ADR directory, and reading it as a hit would be wrong in the opposite direction.What I credit already
The dispatch demanded every compile face reaching the filter be enumerated rather than silently skipped, and the PR enumerates eight, including the three that could have been quietly dropped: expand sub-reads (where the refusal lands inside a pre-existing graceful-degradation
catch, so the outcome is a warning plus retained FK ids — declared, not hidden), the formula planner, andplugin-reportsforwarding a saved report'squery.fieldsverbatim. The ruling's two carve-outs are pinned as guards so a future tighten cannot ride in on this card's precedent, and Option C is absent as ruled.Generated by Claude Code
docs(objectql): teach expand, not a dotted projection, for reading a …
os-zhuang commentedon Aug 13, 2026
PM review — ACCEPT. PR #8327 marked ready and enqueued
domain:engine-coreseat (#6019), sessionsession_01RDTnVvsgA6cUZ4xFVtPZRy. Reviewed against GitHub — file list, diff, andorigin/main— ⛔ not against the report's narrative.Gate reading: all 26 check runs read individually
25
success, 1skipped(Console Pin Gate, conditional). ⛔ No aggregate, ⛔ noget_status.filtercompletedsuccess, so the Test/Dogfood shards were genuinely scheduled.Test Core (3/3)andTypeScript Type Checkwere the last two outstanding and each waited out to its own conclusion — the patch commit reset the whole farm, and the previous head's green was not carried forward.Path face — clean, and the near-miss is worth naming
.changeset/·docs/protocol-upgrade-guide.md·packages/objectql/src/engine.ts·query-expression-conformance.test.ts·packages/spec/spec-changes.json·packages/spec/src/migrations/{entries/semantic/17.engine-dotted-projection-refused.ts,registry.ts}·content/docs/protocol/objectql/types.mdx.docs/protocol-upgrade-guide.mdsits underdocs/but is notdocs/adr/**. The maintainer-merge fork keys on the ADR directory, and reading adocs/prefix as a hit would be wrong in the opposite direction — a PR held for a human that never needed holding. No.claude/skills/**, noskills/**. Normal landing path.⭐ The correction the dev handed back, which I am recording rather than smoothing
The maintainer's ruling states the false populate comment was "the last place in the repo" asserting dotted-path resolution exists. That was true of code, and not true of the repository. Two hand-written documentation pages still carried the claim, and one of them taught it as a worked example with the comment
// Expands account— the premise itself, still teaching users to author the entry this PR makes throw.⛔ I did not find that by trusting the drift advisory's verdict; the advisory listed 109 pages and 108 were
via @objectstack/specnoise. It came from reading the one page whose subject actually matched. The dev then swept the rest ofcontent/docsand confirmed it was the only occurrence — verifying my one-line reading instead of inheriting it — and caught a second defect in the same example: its headaccountdid not even match the page's own declared fieldaccount_id.The release-owned half (
content/docs/releases/v17.mdx:977, which prescribes a dottedfieldspath as a migration remedy) was correctly left untouched and filed as #8329, unassigned and ungraded for triage. ⛔ A code PR editingcontent/docs/releases/is the exact thing that guardrail exists to stop.Assumptions: two confirmed by measurement, one confirmed and extended
:6996to~:7161— located by symbol, which is the whole reason the address in the brief was labelled stale.7e5af5c's actualengine.tshunks (imports +lowerWhereFilterArray, which readswhereonly) and3f7b4ff(aggregate-only, nofieldsoption perENGINE_OPTION_KEY_SETS). Distinct, measured.get_recordroutes tofindOnewhenlimitis absent or 1, so the measured chain reaches both verbs. Both are closed. I briefed one.What earns the ACCEPT on substance
Eight compile faces enumerated and declared — including the three that could have been quietly skipped: expand sub-reads (where the refusal lands inside a pre-existing graceful-degradation
catch, so the outcome is a warning plus retained FK ids — declared, pinned, with a control, not hidden), the formula planner, andplugin-reportsforwarding a saved report'squery.fieldsverbatim. The ruling's two carve-outs are pinned as guards so a future tighten cannot ride in on this card's precedent. Option C is absent, as ruled. Reverse verification ran from committed state with the direction declared in advance: 9 red, 136 green, the 9 being exactly the new pins, restored byte-identical.The engine-vs-ingress precedence divergence (all dotted entries refused, including unknown heads) is a real semantics call the dev made — declared and pinned with its reasoning rather than absorbed silently, and consistent with the ruling: the engine has no unknown-name refusal to defer to, and an only-dotted projection widened through the empty-projection fallback.
Declared-breaking, with the ADR-0087 kit registered per the #7095 precedent. Cross-seat declaration posted to
domain:spec(#6017) with the separable-commit offer; veto window runs to merge.Landing confirmation follows on two readings — the merge event and the content on
origin/main.Generated by Claude Code
os-zhuang commentedon Aug 13, 2026
✅ LANDED —
origin/main@1b2eb1bdomain:engine-coreseat (#6019), sessionsession_01RDTnVvsgA6cUZ4xFVtPZRy.Confirmed on two readings, ⛔ neither a report nor an auto-merge receipt:
merged.origin/maincontent — the refusal is live inengine.ts(INVALID_FIELD, 3 hits), and the dotted teaching example is gone fromcontent/docs/protocol/objectql/types.mdx(0 hits foraccount.company_name).Grepping
origin/mainforvia populatereturned 1 hit where the ruling required zero — so I read it before calling it a defect. It is the new docblock describing the removal in past tense:That is the historical record kept correctly, ⛔ not a surviving assertion. A grep cannot tell a claim from a description of a claim — recording the near-miss because the reflex it would have triggered (a rework round for a correctly-written comment) is worse than the check it came from. The positive control (
populatein any sense: 13 hits) is what made it obvious the naive count meant nothing.What landed
engine.find/engine.findOnenow refuse a dottedfieldsentry with400 INVALID_FIELDinstead of answering200with every column — a response byte-identical to no projection at all, which pointed away from both FLS and data minimisation while the caller believed it had narrowed. The measured chain was a flowget_recordnode's authoredfields, reaching both verbs; a saved report'squery.fieldsreached it the same way.The ruling's carve-outs are intact and pinned as guards: the #3821 unknown-plain tolerance still tolerates, registry-less hosts still get no verdict, and Option C stayed out.
⭐ And the ruling's own framing got corrected in passing. It called the false populate comment "the last place in the repo" asserting dotted resolution. That was true of code. Two documentation pages still carried it — one fixed in this PR, one release-owned and now filed as #8329. The card that deletes a false premise from the code is exactly the card that should find out where else the premise lives.
Declared-breaking, ADR-0087 kit registered. Cross-seat declaration to
domain:spec(#6017) ran its window to merge with no objection.Generated by Claude Code
chore(spec): regenerate the ADR-0087 projections after merging main (#…
docs(spec): `fields` stops prescribing a dotted path no driver resolv…
fix(objectql)!: engine.find/findOne refuse a dotted projection instea…
docs: drop refused dotted-fields remedy from v17 release notes (objec…
refactor(plugin-audit)!: retire `restore` from the sys_audit_log acti…
fix(spec): os migrate meta guidance for the engine-* migration entrie…