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 as admin@objectos.ai. POST /api/v1/data/showcase_invoice/query body {"fields":["name","account.name"]} → 200, every business field, no resolved account.name.- Same via
GET /api/v1/data/showcase_invoice?$select=name,account.name. - Second object pair:
POST /api/v1/data/showcase_task/query body {"fields":["title","project.name"]}. - Controls:
{"fields":["title"]} narrows correctly; a nonexistent plain column answers 400 INVALID_FIELD; a nonexistent dotted column answers 200 + every field.
Source
Extracted from the QA run #7463 (framework a86db17).
Symptom
POST /api/v1/data/showcase_invoice/querywith{"fields":["name","account.name"]}answers200returning every business field of each record, and no resolvedaccount.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:
POST /data/showcase_invoice/query {"fields":["name","account.name"]}200, every field, no resolvedaccount.nameGET /data/showcase_invoice?$select=name,account.name200, every fieldPOST /data/showcase_task/query {"fields":["title","project.name"]}200, every fieldPOST /data/showcase_task/query {"fields":["title"]}200, correctly narrowed200+ every field (swallowed)400 INVALID_FIELD400 INVALID_FIELDTwo 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_FIELDmessage the plain spelling produces names this exact failure mode.Root cause
assertProjectionFieldsExist(packages/metadata-protocol/src/protocol.ts) validates onlyf.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/expandaxes 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
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.GET /api/v1/data/showcase_invoice?$select=name,account.name.POST /api/v1/data/showcase_task/querybody{"fields":["title","project.name"]}.{"fields":["title"]}narrows correctly; a nonexistent plain column answers400 INVALID_FIELD; a nonexistent dotted column answers200+ every field.Source
Extracted from the QA run #7463 (framework a86db17).