Skip to content

feat(spec): declare id_field a retirement with no successor on FieldSchema - #17060

Merged
os-bill merged 2 commits into
mainfrom
claude/issue-16632-id-field-canonical-target
Sep 9, 2026
Merged

feat(spec): declare id_field a retirement with no successor on FieldSchema#17060
os-bill merged 2 commits into
mainfrom
claude/issue-16632-id-field-canonical-target

Conversation

@os-bill

@os-bill os-bill commented Sep 9, 2026

Copy link
Copy Markdown
Collaborator

Fixes #16632

Clause-②: no

The declaration above supersedes the Clause-②: yes recorded in the claim comment (5595423887). The measured direction is why, which changes no schema; an independent contract-review-tier review of this diff ruled clause ② no and the seat executed it. See the ## Clause-② section below and the provenance comment on #16632.

The direction call first, because it is the first deliverable and it is a factual finding, not a preference.

Direction: id_field has NO canonical successor on FieldSchema — a why entry

The cheaper branch is also the measured one, so here is the evidence rather than the verdict alone. Four independent readings on this tree (022f6d23), all re-derived, none inherited from the card:

  1. No member to fold onto. FieldSchema (packages/spec/src/data/field.zod.ts:849) declares the whole lookup-picker group — reference, referenceVia, displayField, descriptionField, lookupColumns, lookupPageSize, lookupFilters, dependsOn, allowCreate — and nothing that names which field holds a lookup's stored value.
  2. No runtime reads one. Repo-wide, packages/objectql, packages/runtime and packages/metadata-protocol contain zero readers of a per-field id key. The one idField identifier in packages/runtime/src/action-execution.ts:696 is a local bound from action.recordIdField — an Action key, a different surface.
  3. The two places a reference IS stored by something other than an id are declared elsewhere, centrally. APPROVER_VALUE_BINDINGS (packages/spec/src/automation/approval.zod.ts:167) types each approver as { source: 'record'; object: string; valueField: 'id' | 'name' } and pins position: { object: 'sys_position', valueField: 'name' }. That is precisely the example objectui's own LookupFieldMetadata.idField docblock cites (an approval position approver stores sys_position.name, objectstack 审批节点「处理人 Value」应改为记录 lookup(现查 metadata 端点→只能手填);附 approver value 语义核实 + queue 未实现 #3508) — so the capability exists, but the platform decides it per approver type, not the author per field. The other channel is a seed dataset's externalId (packages/metadata-protocol/src/seed-loader.ts:320, :561), through which lookup/master_detail references already resolve.
  4. It is already stated in the published line. content/docs/releases/v17.mdx:493 distinguishes the eight resolved-side picker keys exactly this way: six "are FieldSchema keys, resolved at runtime from the referenced field's own metadata", while "idField and titleFormat were never authorable on either side — the picker resolves record identity itself".

So id_field is the why branch, and per the acceptance criterion the entry is a prescription, not a bare "retired": it names why (identity is not an authored per-field choice) and what to reach for instead (displayField for the label, a dataset externalId for a portable natural key).

retiredKey() was considered and rejected: its own contract is "Declare a key that has been REMOVED from the spec" (shared/retired-key.ts). id_field was never in the spec, which is the indexed / immutable / filterable / startingNumber shape — a why row.

The same-named GridColumn key is a DIFFERENT schema

⛔ Do not unify them. git grep -c idField packages/spec/src/data/field.zod.ts still returns 2, and neither hit is a FieldSchema member:

line what it is
:837 idField on InlineGridColumnSchema (declared :804) — the strict inlineColumns mirror of objectui's GridColumn. Live, and it stays live.
:789 a file path in a comment, packages/fields/src/widgets/GridField.tsx

The :789 hit is not merely incidental: GridField literally contains the substring idField ("GridField".includes("idField") === true). A word-boundary count is 1, not 2:

git grep -c  'idField'    -- packages/spec/src/data/field.zod.ts   ->  2
git grep -cP '\bidField\b' -- packages/spec/src/data/field.zod.ts   ->  1

displayField is simultaneously a member of both schemas with different meanings, which makes "find the target by name" worse still. The third new assertion pins both halves: InlineGridColumnSchema still accepts idField, and FieldSchema's shape still does not carry it.

The entry is keyed id_field in snake_case, and the spelling is load-bearing

Triage warned that pasting the card's snake spelling into a camelCase table would file a row the lint never hits. Measured, the answer is the opposite of the fear, and for a reason worth writing down: the two channels this table feeds do not agree on the key face.

  • A to becomes a strictObject alias, and aliases are indexed by aliasProbekey.toLowerCase().replace(/[_\-\s]/g, '') (shared/alias-probe.ts). Its contract says so: "Map keys are matched case-insensitively with _ / - / space separators removed." One camelCase row covers every separator spelling; the rollup comment in field.zod.ts:866 says exactly this.
  • A why becomes strict guidance, and that channel is guidance[key] with no probe (strictUnknownKeyError, shared/suggestions.zod.ts:445). Its contract: "Matched case-sensitively (exact authored spelling)." This file's own comparator does the same (guidance[key] in lintAuthoredRecordKeys).

So a camelCase why row would never be reached by id_field, the spelling objectui's resolveActionParams reads off an object-schema field def and the one this card exists for. A NOTE in the table records this so nobody "normalises" the row to match its neighbours.

The reading that proves the entry is REACHED

An assertion that cannot fail and an assertion that passed look identical from the outside, so this PR ships the reading, not just a green tick.

The channel is the parse, not the lint. FieldSchema is a strictObject that pulls this table in via fieldKeyGuidanceAsStrictOptions() (field.zod.ts:762, :854, :888), and the authoring-key walker is silent on a strict surface by its own posture rule (kernel/metadata-authoring-lint.ts: strict → silent, "the parse is loud on its own"). Triage suggested proving reachability by making the lint fire on metadata carrying the key — on this tree that would have proved nothing, because the lint never fires here.

Before (measured on de1a611, both spellings, bare refusal — no rename, no prescription):

Unrecognized key(s) on this field: `id_field`. Until this shape was closed these were
dropped silently — the field was still created, minus whatever the key was meant to
constrain, protect or compute.

After, from FieldSchema.safeParse({ name: 'account_id', type: 'lookup', reference: 'crm_account', id_field: 'name' }):

Unrecognized key(s) on this field: `id_field`.
  • `id_field` was never a FieldSchema key: a lookup stores the referenced record's id, and
    which field holds that value is not an authored per-field choice — the picker resolves
    record identity itself. Use `displayField` to change which field is SHOWN as a
    candidate's label, and a seed dataset's `externalId` to author references by a portable
    natural key. The live `idField` is the one on an `inlineColumns` entry — the GridColumn
    mirror in `field.zod.ts`, which is a DIFFERENT schema.
  Until this shape was closed these were dropped silently — …

Two ablations, both restored byte-exact from HEAD (git diff HEAD empty, blob hash back to the HEAD blob), each with its on-disk mutation proven by anchor counts before and after — the editor's exit code was not taken as evidence. These resolve through src (a relative import inside the same package, not a dependency's exports), so no dist leg applies.

ablation on-disk proof result
re-key the row id_fieldidField snake 1 → 0, camel 0 → 1; blob d9d2b4dba40ee0 RED, 1 failed / 12 passed
sever the delivery (...fieldKeyGuidanceAsStrictOptions().guidance{}), row left in the table anchor 1 → 0, marker 0 → 1; blob a05e28ec8e3ecd; table still holds the row (1) RED, 1 failed / 12 passed

The second is the decisive one: the row is present and the parse message reverts exactly to the pre-change bare refusal, so the assertion is live rather than vacuously satisfied by the table lookup.

One honest note: the first attempt at the second ablation was a no-opperl -0pi -e aborted on the replacement text and exited without writing. The anchor counts caught it (after: old=1 marker=0, blob unchanged), so its exit 0 was discarded as NOT MEASURED rather than read as green; the table above is the redone run.

Clause-② — the measured answer flips it, and that is the triage seat's call, not mine

The claim comment (5595423887) records Clause-②: yes, so needs:contract-review is hung with this PR as declared. But the direction landed on why, and triage's own rule (5578761558) is explicit: "若答案是 why(无后继,只在 guidance 表里加一条)⇒ ⛔ 没有 schema 变化,Clause-② no,本枚标应当摘掉". No published schema gains or renames a member herecheck:api-surface, check:export-origins and check:authorable-surface all pass unmoved, confirming it. ⛔ I have not removed the label: triage asked to be told and said it would retire it. Flagged, not decided.

Scope

title_format is deliberately untouched. Comment 5590745899 measured it as a second key with no declared target and said of it: "That is a maintainer ruling, not mine; recording the measurement so the ruling is taken on eight keys rather than four." Widening this card would re-block objectui#7650's id_field slice, which is the one thing it exists to unblock. Reported upward, not filed and not fixed here.

For the landing seat

Once this merges, objectui#7650 needs a reply so its id_field slice can release its Blocked-by. The declared fact its ingestion choke point reads is FIELD_KEY_GUIDANCE.id_field.whyFIELD_KEY_GUIDANCE is a published export of @objectstack/spec/data (api-surface/data.json:260), the subpath objectui already consumes — so the choke point drops the key with that sentence as its loud diagnostic instead of folding it onto a member that does not exist. ⛔ Nothing was pushed to the objectui repo from here.

验收备注

Triage's five criteria (5578761558), each answered above:

  1. Direction decided before any code, with the conclusion and its evidence in the PR — the first section. It is why, on four measurements, and the reasoning against the to branch is stated rather than assumed.
  2. The row is written in the key face the table actually consults, with a reading proving it is really reached — the spelling section and the reachability section. The proof is a parse reading plus two ablations, not "the tests are green".
  3. The why row gives an executable alternative in the style of the index rowdisplayField for the label, a dataset externalId for a portable natural key, not a bare "retired".
  4. Not misled by field.zod.ts:837; the two same-named keys are named as different schemas — its own section, plus a pinning assertion, plus the GridField-contains-idField substring trap that makes the naive count read 2.
  5. Reply on objectui#7650 after landing — noted for the landing seat above; nothing pushed there from this seat.

Noted, not filed (out of scope, no card opened):

  • title_format — routed upward per the scope fence, not filed.
  • The camelCase twin idField authored on a FieldSchema gets a bare refusal with no rename suggestion today (measured: the edit-distance fallback offers nothing for it), so there is no confident-wrong advice to correct and no defect here. Left alone deliberately — the guidance channel's exact match means covering it would be a second row, which is beyond this card.
  • auditTrail, dataQuality, encryptionConfig and the other camelCase why rows have the same exact-match property: a snake_case authoring of any of them reaches no prescription. Observation only; no card, and no evidence any of those spellings circulates.

Verification

  • pnpm --filter @objectstack/spec build && … test && … typecheck467 test files, 13103 tests passed, typecheck clean, on the merged head 022f6d23 (VERDICT command-exit 0 from os-verify-lock.sh).
  • 76 of 76 derived gate families run (node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack, reconciled with --ran: "76 derived famil(ies) accounted for — 76 run, 0 NOT-MEASURED"), re-derived after the merge and identical. 73 green; 3 return exit 3 PREREQUISITE NOT MET (check:dual-build-cjs-loads, check:lean-entry-closure, check:type-check-debt) because they read built output of packages this worktree has not built — that is nothing measured, neither a pass nor a finding, and it is left to CI's full build. check:doc-formula-expressions returned the same exit 3, was given its named targeted prerequisite build, and then passed.
  • pnpm exec eslint . --no-inline-config — the full repo union, not a narrowing: 6398 files, 0 errors, 0 warnings, at 022f6d23.
  • pnpm check:nul-bytes green, plus a direct control-character sweep of the changed files.

origin/main merged once (022f6d23) before opening.

🤖 Generated with Claude Code

https://claude.ai/code/session_01MkQhmuuJAVDjmeWNixwDDH


Generated by Claude Code


Generated by Claude Code

os-bill and others added 2 commits September 9, 2026 03:54
…dSchema

`FIELD_KEY_GUIDANCE` gains an `id_field` entry so objectui's ingestion choke
point has the declared spec-side fact it needs to canonicalise the retired
spelling (objectui#7650 ruling A). A lookup stores the referenced record's id
and the picker resolves record identity itself, so there is no `FieldSchema`
member to fold onto; the prescription names `displayField` and a seed dataset's
`externalId` instead.

The entry is keyed in snake_case on purpose: `to` rows become `strictObject`
aliases matched through `aliasProbe` (case and separators folded), while `why`
rows become strict guidance matched exactly and case-sensitively, so a
camelCase row would never be reached. Three new assertions read the parse
channel that actually answers an authored field key, which is the reachability
the existing table tests never asserted.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MkQhmuuJAVDjmeWNixwDDH
@github-actions github-actions Bot added size/s documentation Improvements or additions to documentation protocol:data tests tooling labels Sep 9, 2026
@github-actions

github-actions Bot commented Sep 9, 2026

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

⚠️ 1 changed file(s) yielded no anchor (packages/spec/src/data/authoring-key-lint.ts), so the pages documenting them are NOT COVERED by this run — this is not a clean bill of health for those files. Nothing else in this diff resolved to a documentable surface (no symbol, route or SDK anchor derived from 1 changed package(s)).

What this run could not see
  • 1 changed file(s) yielded no anchor (packages/spec/src/data/authoring-key-lint.ts) — pages documenting those are invisible to this run
  • a page that states a rule by its inputs shares no identifier with the emitter that implements the rule, so an emitter-only diff cannot list it — not on this run and not on any run. Measured on fix(driver-sql): emit varchar(maxLength) for a text field a declared index keys on #11430: content/docs/protocol/objectql/types.mdx documents the text-family column mapping by the ObjectQL type names it maps FROM (text / textarea / html) while the diff changed createColumn; it went unlisted, and it was the page that diff falsified, in four places. No shared token exists to detect this on, so a rule your change carries has to be re-read by hand in the pages that restate it.

Coarse fallback — 131 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): node scripts/docs-audit/affected-docs.mjs --json e4fd55d9b0897bfc5031e0cfa2654b08514d69depackageMentionDocs.

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/s tests tooling

Projects

None yet

Development

Successfully merging this pull request may close these issues.

FieldSchema: declare the target for the retired id_field spelling so objectui's ingestion choke point can canonicalise it (objectui#7650 ruling A)

2 participants