Skip to content

refactor(data)!: a select-list entry is a field name — the nested-select object form is removed (#4196) - #4268

Merged
os-zhuang merged 3 commits into
mainfrom
claude/fieldnode-nested-select-form-tygnzg
Jul 31, 2026
Merged

refactor(data)!: a select-list entry is a field name — the nested-select object form is removed (#4196)#4268
os-zhuang merged 3 commits into
mainfrom
claude/fieldnode-nested-select-form-tygnzg

Conversation

@os-zhuang

@os-zhuang os-zhuang commented Jul 31, 2026

Copy link
Copy Markdown
Contributor

Closes #4196.

FieldNodeSchema declared two forms for one entry of QueryAST['fields']:

type FieldNode =
  | string                                                  // "name"
  | { field: string; fields?: FieldNode[]; alias?: string }; // nested select

The issue's recommended disposition was remove, and the build agreed — nothing refuted it.

Why the object form had to go

No producer, and no consumer that read .fields or .alias. Every reader on the path treats the list as string[]:

Reader What it did with an object entry
objectql/engine.ts — formula projection + both known-field filters dropped it (one already reached for String(f).split('.')[0], which is what defensiveness against an unhandled shape looks like)
driver-sql select(), driver-memory projectFields dropped it
driver-mongodb keyed the projection with the entry itself → asked for a column literally named "[object Object]". #4194 taught it to read .field because the newly-precise type made the old line a compile error — defensive handling of a shape nothing sends, not evidence of life
the REST ingress (assertProjectionFieldsExist) stringified it → 400 INVALID_FIELD: Unknown field '[object Object]', a rejection naming something the caller never wrote

So fields: [{ field: 'owner', fields: ['name'] }] was accepted by validation and then dropped or mangled depending on the driver — ADR-0078's silently-inert declaration, ADR-0049's enforce-or-remove. The capability it described is already served by expand, resolved through batch $in queries. Deleting the second spelling rather than lowering it into the first is Prime Directive #12: one capability, one contract.

I also checked the other end of the wire: objectstack-ai/objectui has its own unrelated internal FieldNode (a QueryASTNode with type: 'field'), and none of its 23 fields: [{ field: … sites is a select list — they are form sections, grid grouping, user filters and error envelopes. No producer there either.

FROM → TO

Was Now
fields: [{ field: 'owner', fields: ['name'] }] expand: { owner: { object: 'user', fields: ['name'] } }
fields: [{ field: 'owner' }] fields: ['owner']
…one related column only fields: ['owner.name'] (dotted path)
fields: [{ field: 'total', alias: 't' }] aggregations / windowFunctions — they carry the live alias

The one-line fix: a fields[] entry is a string.

The retirement kit

  • Schema. FieldNode is string; FieldNodeSchema is z.string() with an error map answering an object entry with the prescription above instead of zod's "expected string, received object". Only an object gets it — telling the author of fields: [42] that their entry "was removed" would misinform. z.input becomes string, so tsc fails at the authoring site first.
  • Ingress. assertProjectionFieldsExist gains the shape axis, judged before the object's field map: the entry is wrong about its shape, not about this object, and a registry-less host (which the name gate gives up on) would otherwise hand it to a driver that cannot read it.
  • ADR-0087 D3 semantic migration (query-field-node-object-form-retired) rather than a D2 conversion + os migrate meta step — deliberately. QueryAST is a request shape, never stored in stack metadata, so the chain has no source to rewrite; same treatment as EnhancedApiError.fieldErrors and BatchOptions.validateOnly on this step. The protocol-18 rationale is extended to say so.
  • Consumers. The defensive as string[] casts and String(f) coercions the drivers and engine had grown against a shape nothing sends go with it.
  • Tests. Two spec cases pinned the object form as parseable — they proved the DECLARATION, which is exactly how a declared-but-inert shape survives a green suite. Both are inverted, plus new pins for the prescription, for a non-object mistake keeping zod's own message, and for the expand replacement. Three more in the REST 列表:sort / select / expand 指向不存在的字段时被静默丢弃(filter 轴已收口,这三条轴还没有) #4226 list-path conformance suite, including that a dotted path still works.
  • Docs. query-syntax.mdx loses its "the engine drops non-string entries" row (there is nothing to drop now) and its FieldNode type block; data-engine.mdx and the generated references follow. Strictness ledger re-counted (query.zod.ts 10 → 9 z.object( sites).
  • Changeset. major for @objectstack/spec, carrying the FROM → TO table and the one-line fix.

No liveness-ledger entry is involved: FieldNode is not a governed metadata type, and the union's members were never in authorable-surface.json (the walker does not descend unions), so no baseline moved on that axis.

The main merge caught a jointly-wrong pair (second commit)

#4252 landed while this branch was open, pinning the INPUT half of every recursive schema — including two probes asserting the { field, fields, alias } entry is assignable (recursive-schema-input-assertions.ts). The two changes merge with no git conflict and are jointly wrong: exactly the shape AGENTS.md multi-agent discipline #10 describes, and the reason for the pull-main-and-re-run-before-merging rule.

FieldNode is no longer recursive, so FieldNodeSchema infers both halves itself rather than carrying a z.ZodType<Output, Input> annotation. Its probes stay anyway — the file's job is that the input side is checked, and a z.string() someone re-widens to z.ZodType<FieldNode> fails there exactly as a dropped type parameter would. The positive probe becomes a dotted path; the object form joins the negative side. wiredFieldNode needs no change (z.input is string, so 42 is still the expected error).

Verification

Out-of-scope finding — filed and already closed

check:generated failed its own ledger reconciliation when I hit it (check:strictness-ledger, added by #4232, classified in neither GATED nor NO_GENERATOR). Filed as #4267; #4252 had fixed it on main minutes earlier, so the issue is closed as already-done rather than carried here.

…ect object form is removed (#4196)

`FieldNodeSchema` declared two forms for one entry of `QueryAST['fields']`: a
string, and `{ field, fields?, alias? }` for a nested select on a relationship.
The object form was declared-but-inert. Nothing produced it, and nothing read
`.fields` or `.alias`:

- `objectql/engine.ts` — formula projection and both known-field filters treat
  the list as `string[]`; one already reached for `String(f).split('.')[0]`,
  which is what defensiveness against an unhandled shape looks like.
- `driver-sql` `select()`, `driver-memory` `projectFields` — same.
- `driver-mongodb` keyed its projection with the entry itself, so an object
  entry asked for a column literally named `"[object Object]"`. #4194 taught it
  to read `.field` because the newly-precise type made the old line a compile
  error — defensive handling of a shape nothing sends, not evidence of life.
- The REST ingress stringified each entry before comparing it to the field map,
  so the same entry came back as `400 INVALID_FIELD: Unknown field
  '[object Object]'` — a rejection naming something the caller never wrote.

So an author who wrote `fields: [{ field: 'owner', fields: ['name'] }]` got it
accepted by validation and then dropped or mangled, depending on the driver
(ADR-0078 silently-inert declaration; ADR-0049 enforce-or-remove). The capability
it described is already served by `expand`, which the engine resolves through
batch `$in` queries. Removing the second spelling rather than lowering it into
the first is Prime Directive #12: one capability, one contract.

`FieldNode` is now `string`; `FieldNodeSchema` is `z.string()` with an error map
that answers an object entry with the FROM → TO prescription instead of zod's
"expected string, received object" — and only an object, since telling the author
of `fields: [42]` that their entry "was removed" would misinform. `z.input`
becomes `string`, so `tsc` fails at the authoring site first.

`assertProjectionFieldsExist` gains the shape axis, judged BEFORE the object's
field map: the entry is wrong about its shape, not about this object, and a
registry-less host (which the name gate gives up on) would otherwise hand it to a
driver that cannot read it.

Registered as an ADR-0087 D3 semantic migration
(`query-field-node-object-form-retired`) rather than a D2 conversion: `QueryAST`
is a request shape, never stored in stack metadata, so the chain has no source to
rewrite. Same treatment as `EnhancedApiError.fieldErrors` and
`BatchOptions.validateOnly` on this step.

Two spec tests that pinned the object form as parseable are inverted — they
proved the DECLARATION, which is exactly how a declared-but-inert shape survives
a green suite. New pins cover the prescription, the non-object mistake keeping
zod's own message, and the `expand` replacement; the list path gets three more in
the #4226 conformance suite.

No runtime behaviour changes for anything that ever worked; the defensive
unwrapping the drivers had grown against a shape nothing sends goes with it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EskBioEkCCJpGz3SvPQDxx
@vercel

vercel Bot commented Jul 31, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

1 Skipped Deployment
Project Deployment Actions Updated (UTC)
objectstack Ignored Ignored Jul 31, 2026 2:12am

Request Review

@github-actions github-actions Bot added documentation Improvements or additions to documentation protocol:data tests tooling labels Jul 31, 2026
@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 6 package(s): @objectstack/metadata-protocol, @objectstack/objectql, @objectstack/driver-memory, @objectstack/driver-mongodb, @objectstack/driver-sql, @objectstack/spec.

111 hand-written doc(s) reference the affected code and may need an implementation-accuracy re-verification:

  • content/docs/ai/agents.mdx (via @objectstack/spec)
  • content/docs/ai/skills-reference.mdx (via @objectstack/spec)
  • content/docs/ai/skills.mdx (via @objectstack/spec)
  • content/docs/api/client-sdk.mdx (via @objectstack/spec)
  • content/docs/api/environment-routing.mdx (via @objectstack/spec)
  • content/docs/api/error-catalog.mdx (via @objectstack/spec)
  • content/docs/api/error-handling-client.mdx (via @objectstack/spec)
  • content/docs/api/error-handling-server.mdx (via @objectstack/spec)
  • content/docs/api/index.mdx (via @objectstack/spec)
  • content/docs/automation/approvals.mdx (via packages/spec)
  • content/docs/automation/flows.mdx (via @objectstack/spec)
  • content/docs/automation/hook-bodies.mdx (via packages/spec)
  • content/docs/automation/hooks.mdx (via @objectstack/spec)
  • content/docs/automation/index.mdx (via @objectstack/spec)
  • content/docs/automation/webhooks.mdx (via @objectstack/spec)
  • content/docs/automation/workflows.mdx (via @objectstack/spec)
  • content/docs/concepts/architecture.mdx (via @objectstack/spec)
  • content/docs/concepts/design-principles.mdx (via packages/spec)
  • content/docs/concepts/index.mdx (via @objectstack/spec)
  • content/docs/concepts/metadata-driven.mdx (via @objectstack/spec)
  • content/docs/concepts/metadata-lifecycle.mdx (via @objectstack/metadata-protocol, @objectstack/objectql, packages/spec)
  • content/docs/concepts/north-star.mdx (via packages/spec)
  • content/docs/data-modeling/analytics.mdx (via @objectstack/spec)
  • content/docs/data-modeling/drivers.mdx (via @objectstack/driver-memory, @objectstack/driver-mongodb, @objectstack/driver-sql, @objectstack/spec)
  • content/docs/data-modeling/external-datasources.mdx (via @objectstack/spec)
  • content/docs/data-modeling/field-types.mdx (via @objectstack/spec)
  • content/docs/data-modeling/fields.mdx (via @objectstack/spec)
  • content/docs/data-modeling/formulas.mdx (via packages/objectql, @objectstack/spec)
  • content/docs/data-modeling/index.mdx (via @objectstack/spec)
  • content/docs/data-modeling/objects.mdx (via @objectstack/spec)
  • content/docs/data-modeling/queries.mdx (via @objectstack/spec)
  • content/docs/data-modeling/schema-design.mdx (via @objectstack/spec)
  • content/docs/data-modeling/seed-data.mdx (via @objectstack/spec)
  • content/docs/data-modeling/validation-rules.mdx (via @objectstack/spec)
  • content/docs/data-modeling/validation.mdx (via @objectstack/spec)
  • content/docs/deployment/cli.mdx (via @objectstack/spec)
  • content/docs/deployment/migration-from-objectql.mdx (via @objectstack/objectql)
  • content/docs/deployment/troubleshooting.mdx (via @objectstack/spec)
  • content/docs/deployment/validating-metadata.mdx (via @objectstack/spec)
  • content/docs/deployment/vercel.mdx (via @objectstack/objectql, @objectstack/driver-memory)
  • content/docs/getting-started/build-with-claude-code.mdx (via @objectstack/spec)
  • content/docs/getting-started/common-patterns.mdx (via @objectstack/spec)
  • content/docs/getting-started/examples.mdx (via @objectstack/spec)
  • content/docs/getting-started/glossary.mdx (via @objectstack/driver-memory, @objectstack/driver-mongodb, @objectstack/driver-sql)
  • content/docs/getting-started/quick-reference.mdx (via @objectstack/spec)
  • content/docs/getting-started/quick-start.mdx (via @objectstack/spec)
  • content/docs/getting-started/your-first-project.mdx (via @objectstack/spec)
  • content/docs/kernel/cluster.mdx (via @objectstack/spec)
  • content/docs/kernel/contracts/auth-service.mdx (via packages/spec)
  • content/docs/kernel/contracts/cache-service.mdx (via packages/spec)
  • content/docs/kernel/contracts/data-engine.mdx (via @objectstack/spec)
  • content/docs/kernel/contracts/index.mdx (via @objectstack/spec)
  • content/docs/kernel/contracts/metadata-service.mdx (via packages/spec)
  • content/docs/kernel/contracts/storage-service.mdx (via packages/spec)
  • content/docs/kernel/index.mdx (via packages/spec)
  • content/docs/kernel/runtime-services/email-service.mdx (via packages/spec)
  • content/docs/kernel/runtime-services/index.mdx (via packages/spec)
  • content/docs/kernel/runtime-services/queue-service.mdx (via packages/spec)
  • content/docs/kernel/runtime-services/sharing-service.mdx (via packages/spec)
  • content/docs/kernel/runtime-services/sms-service.mdx (via packages/spec)
  • content/docs/kernel/runtime-services/storage-service.mdx (via packages/spec)
  • content/docs/kernel/services-checklist.mdx (via @objectstack/objectql, @objectstack/driver-memory, @objectstack/driver-mongodb, @objectstack/driver-sql, @objectstack/spec)
  • content/docs/kernel/services.mdx (via @objectstack/objectql, @objectstack/spec)
  • content/docs/permissions/authentication.mdx (via @objectstack/objectql, @objectstack/driver-memory)
  • content/docs/permissions/authorization.mdx (via @objectstack/spec)
  • content/docs/permissions/permission-sets.mdx (via @objectstack/spec)
  • content/docs/permissions/permissions-matrix.mdx (via @objectstack/spec)
  • content/docs/permissions/positions.mdx (via @objectstack/spec)
  • content/docs/permissions/rls.mdx (via @objectstack/spec)
  • content/docs/permissions/sharing-rules.mdx (via @objectstack/spec)
  • content/docs/plugins/adding-a-metadata-type.mdx (via @objectstack/spec)
  • content/docs/plugins/anatomy.mdx (via @objectstack/driver-sql)
  • content/docs/plugins/development.mdx (via @objectstack/spec)
  • content/docs/plugins/index.mdx (via @objectstack/objectql, @objectstack/driver-memory, @objectstack/spec)
  • content/docs/plugins/packages.mdx (via @objectstack/objectql, @objectstack/driver-memory, @objectstack/driver-mongodb, @objectstack/driver-sql, @objectstack/spec)
  • content/docs/protocol/backward-compatibility.mdx (via @objectstack/spec)
  • content/docs/protocol/diagram.mdx (via packages/spec)
  • content/docs/protocol/kernel/config-resolution.mdx (via @objectstack/spec)
  • content/docs/protocol/kernel/i18n-standard.mdx (via @objectstack/spec)
  • content/docs/protocol/kernel/index.mdx (via @objectstack/objectql, @objectstack/driver-sql, @objectstack/spec)
  • content/docs/protocol/kernel/lifecycle.mdx (via @objectstack/driver-sql, @objectstack/spec)
  • content/docs/protocol/kernel/plugin-spec.mdx (via @objectstack/spec)
  • content/docs/protocol/kernel/runtime-capabilities.mdx (via @objectstack/spec)
  • content/docs/protocol/knowledge.mdx (via @objectstack/spec)
  • content/docs/protocol/objectql/index.mdx (via @objectstack/spec)
  • content/docs/protocol/objectql/query-syntax.mdx (via packages/objectql, @objectstack/driver-memory, @objectstack/driver-mongodb, @objectstack/driver-sql, @objectstack/spec)
  • content/docs/protocol/objectql/schema.mdx (via @objectstack/spec)
  • content/docs/protocol/objectql/security.mdx (via packages/spec)
  • content/docs/protocol/objectql/state-machine.mdx (via @objectstack/objectql, @objectstack/spec)
  • content/docs/protocol/objectui/actions.mdx (via @objectstack/spec)
  • content/docs/protocol/objectui/concept.mdx (via @objectstack/spec)
  • content/docs/protocol/objectui/index.mdx (via @objectstack/spec)
  • content/docs/protocol/objectui/layout-dsl.mdx (via @objectstack/spec)
  • content/docs/protocol/objectui/record-alert.mdx (via @objectstack/spec)
  • content/docs/protocol/objectui/widget-contract.mdx (via @objectstack/spec)
  • content/docs/releases/implementation-status.mdx (via @objectstack/objectql, @objectstack/driver-memory, @objectstack/driver-mongodb, @objectstack/driver-sql, @objectstack/spec)
  • content/docs/releases/index.mdx (via @objectstack/spec)
  • content/docs/releases/v12.mdx (via @objectstack/spec)
  • content/docs/releases/v13.mdx (via @objectstack/spec)
  • content/docs/releases/v16.mdx (via @objectstack/spec)
  • content/docs/releases/v17.mdx (via @objectstack/spec)
  • content/docs/releases/v9.mdx (via @objectstack/objectql, @objectstack/spec)
  • content/docs/ui/actions.mdx (via @objectstack/spec)
  • content/docs/ui/create-vs-edit-form.mdx (via @objectstack/spec)
  • content/docs/ui/dashboards.mdx (via @objectstack/spec)
  • content/docs/ui/forms.mdx (via @objectstack/spec)
  • content/docs/ui/index.mdx (via @objectstack/spec)
  • content/docs/ui/public-data-collection.mdx (via @objectstack/spec)
  • content/docs/ui/setup-app.mdx (via @objectstack/spec)
  • content/docs/ui/translations.mdx (via @objectstack/spec)
  • content/docs/ui/views.mdx (via @objectstack/spec)

Advisory only. To re-verify, run the docs-accuracy-audit workflow scoped to these files:
node scripts/docs-audit/affected-docs.mjs origin/main → pass the list as args.docs.

claude added 2 commits July 31, 2026 01:56
…d FieldNode form

#4252 landed while this branch was open and pinned the INPUT half of every
recursive schema — including two probes that assert the `{ field, fields, alias }`
select entry is assignable. Both changes merge without a git conflict and are
jointly wrong: the exact shape AGENTS.md's multi-agent discipline #10 warns about.

`FieldNode` is no longer recursive, so `FieldNodeSchema` infers both halves
itself rather than carrying a `z.ZodType<Output, Input>` annotation. Its probes
stay anyway — this file's job is that the input side is CHECKED, and a
`z.string()` someone re-widens to `z.ZodType<FieldNode>` fails here exactly as a
dropped type parameter would. The positive probe becomes a dotted path (the
replacement the removal prescribes) and the object form joins the negative side.

`wiredFieldNode` needs no change: `z.input<typeof FieldNodeSchema>` is `string`,
so `42` is still the error the suppression expects.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EskBioEkCCJpGz3SvPQDxx
@os-zhuang
os-zhuang marked this pull request as ready for review July 31, 2026 02:12
@os-zhuang
os-zhuang merged commit 8675db6 into main Jul 31, 2026
18 checks passed
@os-zhuang
os-zhuang deleted the claude/fieldnode-nested-select-form-tygnzg branch July 31, 2026 02:27
os-zhuang pushed a commit that referenced this pull request Jul 31, 2026
…false (#3438)

The boot line that names an open value-shape gate (#4253) fired on every
deployment there is, and stated something untrue on any deployment that had
already settled the posture with an environment switch. Both are one failure:
an advisory that speaks where it does not apply is exactly how readers learn to
ignore it, and the line's whole value is that a warn-first deployment is told
once, credibly, which command ends warn mode.

Neither defect is reachable from the suite that shipped with the announcement.
`engine.test.ts` mocks `./registry`, and a mocked registry hands the engine
exactly the fields the test wrote — so these cases get a sibling file built on
the genuine `registerApp → registry` path, which is the only way a test sees
what a deployment sees.

`objectHasCoveredValueField` — the dormancy short-circuit meant to spare an
object with no covered field the flag query — tested raw type membership, while
the real registry INJECTS covered-type fields into every object it registers:
`organization_id` and `owner_id` (both `system`), `created_by` and `updated_by`
(both in `SKIP_FIELDS`), four `lookup`s. `validateRecord` skips every one of
them before it reaches the value-shape check, so the short-circuit answered
`true` for literally every object, never fired, and its WeakMap memoized a
constant. It now counts by the validator's own `isScannableValueShapeField` —
the predicate the scanner already imports. Three readings of "a covered field"
drifting by one clause is how a gate ends up governing fields nothing enforces,
which is the shape #4235 argued against.

The announcement also consulted no environment switch, though both postures it
reports on short-circuit ahead of the deployment flag it reads. Under
`OS_DATA_VALUE_SHAPE_STRICT_ENABLED` enforcement is already on for both
classes, so "checked but NOT enforced here" was false; under either opt-out the
operator chose leniency deliberately, so naming a migration that cannot change
what they get is noise. Each gate consults its own pair — `mediaPostureSetByEnv`
and `valueShapePostureSetByEnv`, siblings for the reason `mediaStrictEffective`
and `valueShapeStrictEffective` are siblings, since the opt-outs are per-class
while the opt-in opens both. Cheapest test first, so a kernel with nothing to
say still reaches no flag query.

Enforcement is unchanged: same gates, same flags, same default. The flag read
was already memoized per process, so what this corrects is the property
ADR-0104 states, not a user-visible cost.

Verified: objectql 1345 (7 new), cli 628, spec check:generated 8/8, objectql
build, check:release-notes and check:doc-authoring pass, ESLint clean. One
pre-existing failure on main is untouched by this branch and reproduces with
zero of its source changes: `query-expression-conformance.test.ts` >
"the removed nested-select object form is refused BY NAME", from #4268.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016zgA8CQMJeJbFEjnbib1Uv
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation protocol:data size/m tests tooling

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[P3] data: FieldNode's nested-select object form is declared but nothing produces or consumes it — enforce or remove

2 participants