Repository navigation
Description
Symptom
POST /api/v1/data/showcase_invoice/query with {"fields":["name","account.name"]} answers 200 returning every business field of each record, and no resolved account.name.
Expected: the projection narrows to the named columns (with the dotted path resolved), or — if a dotted path is not supported on this door — a 400 INVALID_FIELD.
Measured across three doors and two object pairs in the same run:
| request | observed | expected |
|---|---|---|
POST /data/showcase_invoice/query {"fields":["name","account.name"]} |
200, every field, no resolved account.name |
narrowed projection |
GET /data/showcase_invoice?$select=name,account.name |
200, every field |
narrowed projection |
POST /data/showcase_task/query {"fields":["title","project.name"]} |
200, every field |
narrowed projection |
control — POST /data/showcase_task/query {"fields":["title"]} |
200, correctly narrowed |
— |
| a nonexistent dotted column | 200 + every field (swallowed) |
400 INVALID_FIELD |
| a nonexistent plain column | 400 INVALID_FIELD |
— |
Two things are wrong at once: the request is answered with strictly more data than it asked for (a projection is how a caller keeps heavy or unwanted columns out of a response, and nothing in the body says it did not apply), and the two spellings of the same mistake — plain vs dotted unknown column — get opposite verdicts on one endpoint. The 400 INVALID_FIELD message the plain spelling produces names this exact failure mode.
Root cause
assertProjectionFieldsExist (packages/metadata-protocol/src/protocol.ts) validates only f.split('.')[0], so a dotted entry passes the ingress gate on its head segment and then defeats the projection downstream — the unknown projection name is dropped and the projection falls back to * (the engine's documented per-axis tolerance for internal callers), i.e. over-return instead of refusal.
Neighbour: #4226 (closed/completed) is the card that closed the select / sort / expand axes against silently-dropped unknown fields. Plain names behave per #4226 in this run; the dotted spelling is the leg its gate does not cover, so treat this as an uncovered leg of — or a regression against — #4226.
Reproduction
- Boot showcase on a fresh isolated file DB (
SqlDriver/ better-sqlite3); authenticate asadmin@objectos.ai. POST /api/v1/data/showcase_invoice/querybody{"fields":["name","account.name"]}→200, every business field, no resolvedaccount.name.- Same via
GET /api/v1/data/showcase_invoice?$select=name,account.name. - Second object pair:
POST /api/v1/data/showcase_task/querybody{"fields":["title","project.name"]}. - Controls:
{"fields":["title"]}narrows correctly; a nonexistent plain column answers400 INVALID_FIELD; a nonexistent dotted column answers200+ every field.
Activity
os-zhuang commentedon Aug 11, 2026
Claimed —
domain:metadataseat.session_01TXK7QuSmTF3DKWji91cmknclaude/issue-7532-dotted-projection-widens../objectstack-7532(dedicated)packages/metadata-protocol/src/protocol.ts—assertProjectionFieldsExist, plus new pins on both doors.PM ruling on the fork this card leaves open. The card accepts either resolving the dotted path or refusing it. The dispatch is to refuse with
400 INVALID_FIELD, and explicitly not to implement dotted-path resolution — that is a feature, whereas this card is about a door answering with strictly more data than was asked for. Refusing also makes the two spellings of one mistake agree, which is the card's second complaint. The dev is told to confirmexpandis the sanctioned mechanism for related-object data and point callers at it in the PR, and to stop and report if anything in-repo indicates dotted projections are meant to be supported.resolveQueryFieldsresolution; if either concludes it must change shape, they stop and report.Generated by Claude Code
os-zhuang commentedon Aug 11, 2026
ACCEPT — PR #7588, marked ready and armed into the merge queue. 27/27 green.
The ruling was followed on every point, including the two that were easy to skip.
POST /querybodyfieldsandGET ?$select=/?select=all fold intofieldsviaWIRE_QUERY_ALIAS_SLOTSbefore the gate, verified rather than assumed, with both call sites (findData,getData) traced. Had either door bypassed the gate, closing it alone would have left that door open — the answer was owed either way, and it came back measured.packages/driver-sqluntouched, and the driver ladder filed as finding: SqlDriver's #3821 recovery ladder widens an unresolvable projection to every field #7589 carrying the measurement and a drop-in reproduction harness — unassigned,findingonly, nodomain:*. Evidence stayed with whoever measured it, which is the whole point of asking the dev to file it rather than filing it myself.resolveQueryFieldsunchanged, so the serialization constraint with Data query: an unknown field insidewhere/$filteranswers 200/0 instead of400 INVALID_FIELD— the bare-key door disagrees (#4134's uncovered sibling) #7534 held on both sides. Data query: an unknown field insidewhere/$filteranswers 200/0 instead of400 INVALID_FIELD— the bare-key door disagrees (#4134's uncovered sibling) #7534's diff is+150 / -0and this one leaves the shared resolution alone; neither dev reshaped the helper while the other was in flight.The card's attribution was corrected, and that correction is worth keeping. The card placed the widening at the ingress gate. It is one layer lower: the protocol forwards
['title','project_id.name']to the driver verbatim, andSqlDriver's #3821 recovery ladder is what retriesselect('*')after sqlite answersno such column. Both halves were driven directly — protocol through a stub driver, driver through real better-sqlite3 — so the ruling's split between "what this card fixes" and "what gets its own issue" rests on measurement rather than on my say-so.Two things I'd single out in the verification:
query-expression-conformance.test.tsasserted a dotted path "is still accepted — the head segment is validated here, the tail resolved downstream". The tail is resolved nowhere, which is exactly what the measurement shows — so that test was protecting the defect. Flipping it and writing down why is correct; quietly deleting it would not have been.expect(Object.keys(record).sort()).toEqual([...])rather than "does not contain X" — and that detail is load-bearing here, because an over-return defect passes every absence check ever written. 8 predicted reds, exactly 8 on revert, no missed predictions.Ruling on the follow-up, recorded here as decided rather than pending: the in-repo surfaces that still prescribe a dotted
fieldspath describe behaviour no driver implements, and are now additionally contradicted by a400. Aligning them is a restore-invariant fix (declared = enforced), not an open product question — the direction was already settled by this card, since dotted-path resolution is a feature it explicitly did not authorise. It was correctly kept out of this PR: cross-package spec/docs surface that regenerates artifacts, with #7534 in flight in the same file family, is the collision the serialization constraint exists to prevent. Now filed as #7601, carrying the surfaces table with the normative one (packages/spec/src/data/query.zod.ts:484) called out first, the coherent split (the zod schema is a shape check and should keep parsing a dotted string; the prose and generated artifacts are the fix), and the measuredexpandsharp edge — the projection must retain the foreign-key column, or expansion has nothing to resolve.Verified independently: three files, no
docs/adr/**, nocontent/docs/releases/, changeset present,Fixes #7532as the first line, and the gate jobs' own conclusions (ESLintsuccess, TypeScript Type Checksuccess) rather than the aggregate. Thetscnote in the report — 63 errors, identical count at base, none inprotocol.ts— matches the gate's own reading.Generated by Claude Code
fix(metadata-protocol): refuse a dotted projection instead of widenin…
docs(spec): `fields` stops prescribing a dotted path no driver resolv…
docs: drop refused dotted-fields remedy from v17 release notes (objec…
fix(spec): os migrate meta guidance for the engine-* migration entrie…