fix(core): the id_field no-declared-twin diagnostic carries the spec's published FIELD_KEY_GUIDANCE reason (objectui#7650) - #11175
Conversation
…s published FIELD_KEY_GUIDANCE reason (objectui#7650) The leave arm's diagnostic for a stored `id_field` now appends `FIELD_KEY_GUIDANCE.id_field.why`, read unconditionally off the installed `@objectstack/spec/data` on the dev-only emitting path. No local copy of the prose and no optional read with a fallback (both refused on the card): a spec without the row throws in dev instead of warning with less, and production never reads it. The fold is unchanged: `id_field` is never folded, no `idField` read is added, and a typo gets no suggestion. Claude-Session: https://claude.ai/code/session_012UwY3ahMixEFkfTUxMVkYm Co-authored-by: Claude <noreply@anthropic.com>
✅ Console Performance Budget
The eager closure is every chunk the entry reaches through static imports — what the browser fetches and parses before the app renders. The entry chunk on its own is a small fraction of it. 📦 Bundle Size Report
Size Limits
|
…ees the value (objectui#7650) Round 2 text correction, words only. The no-declared-twin / occupied / ambiguous line closed on "so this value reaches no reader", which is false where a retired spelling is still read on purpose (resolveActionParam reads id_field and title_format, kept by objectui#7435; resolveGroupByLabels reads id_field). It now says a consumer that reads only the spellings FieldSchema declares will not see the value. The module docblock names those kept readers; the pending 8938 changeset gets one appended, dated correction note (no existing line edited); this PR's own changeset gets one sentence. No read, no fold and no id_field reason logic changes. Claude-Session: https://claude.ai/code/session_012UwY3ahMixEFkfTUxMVkYm Co-authored-by: Claude <noreply@anthropic.com>
✅ Console Performance Budget
The eager closure is every chunk the entry reaches through static imports — what the browser fetches and parses before the app renders. The entry chunk on its own is a small fraction of it. 📦 Bundle Size Report
Size Limits
|
Contract reviewServed-tier: Director seat's at-tier review (objectstack#12708, ① Derived judgments
② Semver levelClause-②: no — as declared: a dev-mode diagnostic's text, read from the published guidance; no accept set widens or narrows. ③ Boundary flags
Implemented-by: VERDICT: PASS State: |
Fixes #7650
Clause-②: no — a dev-mode diagnostic's text now comes from the spec's published guidance; no accepted document, published type or served value changes. The PR still waits as a draft for the director seat's review.
The one open slice of objectui#7650,⚠️ This stays a DRAFT for the director seat's contract review, per the maintainer's instruction of 2026-09-29 quoted in the dispatch. The dev does not mark it ready and does not enable auto-merge.
id_field, as claimed in comment 5904884226.Implemented in session
https://claude.ai/code/session_012UwY3ahMixEFkfTUxMVkYm(PM seatdomain:spec @ objectui, dispatched dev).What changes
In
packages/core/src/utils/reference-keys.ts,warnUnfoldableRetiredKeynow appendsFIELD_KEY_GUIDANCE.id_field.whyto the dev-mode line when the refusal isno-declared-twinand the authored key is exactlyid_field.FIELD_KEY_GUIDANCEis imported from@objectstack/spec/data, next to theFieldSchemaimport the module already had.FIELD_KEY_GUIDANCE.id_field.why, with no?.and no??. It sits after theisDev()and memo checks, so production never evaluates it. On a spec without the row it throws in dev, before the memo is taken, instead of warning with less.canonicalizeRetiredFieldKeysis not touched,id_fieldis never folded, and noidFieldread is added.lintAuthoredRecordKeysis not called and no near match is offered. The appended text is the contract's own sentence about the key. A typo (sortible) gets neither a suggestion nor theid_fieldreason.whyrow matches only the exact authored spelling, with no alias probe. The diagnostic follows the same rule.id_field: never folded, and the diagnostic carries the spec's own reason. Theno-declared-twinbullet in thewarnUnfoldableRetiredKeydocblock gets one matching sentence..changeset/7650-id-field-published-guidance.mdbumps@object-ui/coreas apatch. There is no floor raise (see premise 2).Premises, re-measured on
origin/main0ffc423b1before writingpackages/core/node_modules/@objectstack/spec/package.jsonreads17.5.0.FIELD_KEY_GUIDANCEis exported from the@objectstack/spec/dataentry and typed as a readonly record of{ to?: string; why?: string }. Itsid_fieldrow has one key,why: awhyand noto. Lit control in the same read: thestartingNumberrow is present with awhy. Dark control: in the npm-packed17.4.0tarball,dist/data/index.mjshas 0 hits forid_fieldand 2 forstartingNumber. npm lists no version between 17.4.0 and 17.5.0.@object-ui/corealready declares"@objectstack/spec": "^17.5.0"(raised by PR objectui#11086). No raise was needed, so the lockfile is unchanged.check:spec-floors, run after building core, reports no finding for@object-ui/coreand 0floor-too-lowfindings overall. The gate's exit 1 comes from 18no-artifactfindings, all for packages not built in this worktree. That is a missing prerequisite, so those 18 are NOT MEASURED; it is not a red.normalizeSchemaReferenceKeys(source module,node --experimental-strip-types). Theid_fieldline went straight from "...and this path deliberately does not guess a near match." to "The key and its value are LEFT on the def exactly as served...", with no spec reason. After the change, the same line has "@objectstack/spec'sFIELD_KEY_GUIDANCEgives the reason: " plus the installed row'swhybetween those two sentences. ThesortibleandstartingNumberlines from the same probe are unchanged.ExpressionProvider.tsxandRecordFormPage.tsxmentionFIELD_KEY_GUIDANCEonly in comments. Nothing in the tree imports it:git grepfor an import of the symbol finds 0 hits, against a lit control of 192 files that import from@objectstack/spec/data. So the import follows this module's ownFieldSchemaimport from the same subpath.Tests
The new describe
the id_field diagnostic carries the PUBLISHED spec reason (objectui#7650)is inreference-keys.retiredDialect-7650.test.ts. Every read of the row happens at test time and is unconditional. The test holds no copy of the prose.the installed row is a retirement — a why, and no tothe no-declared-twin line for id_field carries that why, and the key is still not foldedthe reason is read off the spec module when the line is built — not from a copy: substitutes the row throughvi.doMockwithimportOriginala spec WITHOUT the row fails loudly in dev instead of warning with less, and production never reads it: removes the row throughvi.doMock; the production leg is the controla TYPO still gets no suggestion — and not the id_field reason either(The backticks around the key names inside those test names are dropped here.) Two sentences in the same file were corrected because this change made them false: the header bullet that said the row "is in NO published version", and the title of the
id_fieldnegative pin, which said "the slice is blocked on a spec release". That pin is now titleddoes NOT fold id_field — no declared successor.Ablations: each from committed HEAD
21306be7b, with the direction predicted before the runEach leg ran through
ablation-replace.mjs. The anchor must hit exactly once, and the tool checks the on-disk anchor and replacement counts and the blob change. Each leg ran under atraprestore on EXIT INT TERM and was restored by state: the blob is back to the HEAD blob8347e30f9e83, andgit diff HEADis empty. No build was needed: the tests import../reference-keysby relative path (source), and the mocked module is the unchanged spec dist.${detail}${published}becomes${detail}?.why ?? 'LOCAL FALLBACK SENTENCE'no-declared-twinkeyGates at HEAD
21306be7b(exit codes captured before any pipe)pnpm --filter @object-ui/core type-check, afterpnpm --filter "@object-ui/core^..." build: exit 0. The test file is part of thetsconfig.test.jsonprogram (--listFiles: 1 hit).pnpm exec vitest run --maxWorkers=2 packages/core/:Test Files 190 passed (190),Tests 3737 passed | 27 skipped (3764), VERDICT command-exit 0.id_field:Test Files 21 passed (21),Tests 192 passed (192).pnpm --filter @object-ui/core lint: exit 0 with 0 errors (the 568 warnings were already there). The two touched files have 0 errors and 0 warnings.pnpm --filter @object-ui/core build: exit 0, and the dist completeness check passes.check-governed-queue-guard.mjs --teston the three paths: NOT GOVERNED.check:spec-floors: see premise 2. Clean for core; NOT MEASURED for the 18 unbuilt packages.pnpm lint, thepnpm testshards and the full type-check. The lockfile gates do not apply, because the lockfile is unchanged.Acceptance notes
id_field(and fortitle_format), the diagnostic's shared closing clause is not true today. It reads: "the consumers read only the spelling@objectstack/spec'sFieldSchemadeclares, so this value reaches no reader". There are production reads of both snake spellings. Inpackages/plugin-charts/src/ObjectChart.tsx, the expressionfieldDef.id_field || 'id'reads it, and its own census comment traces that def togetObjectSchema. Inpackages/app-shell/src/utils/resolveActionParams.ts,idField: field.id_fieldandtitleFormat: field.title_formatread them. The clause came with objectui#8938 and this PR does not change it, but it now sits on the same line as the spec's reason. The diagnostic text has not been released yet: the changeset8938-retired-dialect-measured-surface.mdis still pending, and@object-ui/core@17.6.0on npm contains none of that text. So the clause would ship as-is in the next release. Fixing it means deciding who the readers are, or retiring those two reads, which the spec's reason argues for. That is a judgement for the reviewing seat, so it is reported to the seat and not filed by this dev.FIELD_KEY_GUIDANCErows that carry awhy(for examplestartingNumber) also land in theno-declared-twinarm, and they get no reason. They are outside this slice under both the claim's file surface and the no-optional-read rule. Generalising would need its own ruling on what the line says when a row is absent.维护者速读(草稿)
id_field时,开发模式下的那条诊断现在会带上 spec 官方写的理由(FIELD_KEY_GUIDANCE.id_field.why,直接读已安装的 spec,本仓不存副本)。id_field仍然不会被折叠成别的键;生产环境行为、服务出去的数据、公开类型都不变。^17.5.0已经保证有这一行。回滚就是撤这个 PR。id_field不成立)交席位判断。Round 2: the closing clause no longer claims that no reader sees the value
In response to the seat's ACCEPT comment 5905375580 on objectui#7650. Head
a71744d4f. Only words change. No read, no fold and noid_fieldreason logic changes, and the one code line that changes is the message string.resolveActionParam(@object-ui/app-shell) readsidField: field.id_fieldandtitleFormat: field.title_format. objectui#7435 kept those reads, and its pinresolveActionParams.declaredLookupLegs-7435.test.tscalls them "the snake spellings that must keep working".resolveGroupByLabels(@object-ui/plugin-charts) readsfieldDef.id_field || 'id'.@objectstack/spec'sFieldSchemadeclares will not see this value."warnUnfoldableRetiredKeydocblock, "a key that does not fold reaches none of them" and "a value nothing reads" now apply only to the consumers of the declared spellings. A new paragraph names the kept readers by symbol (resolveActionParam,resolveGroupByLabels). It says the list is not complete (deriveColumnsin@object-ui/plugin-formalso readsdisplay_field) and that nothing re-derives a complete list. Two other sentences in the same file made the same claim and are narrowed the same way: the module docblock's "the consumers … read none of it", and thecanonical-occupiedbullet's "the retired one is inert". Thecanonical-occupiedcomment in the 7650 pin file ("reaches no consumer") is narrowed the same way too.8938-retired-dialect-measured-surface.md. One dated correction note is appended at the end, under the maintainer's ruling 「Allow the appended note (Recommended)」.git diffof that file shows 11 additions and 0 deletions. Its first 39 lines (2491 bytes, frontmatter included) are byte-identical to the base. The note also corrects that file's "the retired value is inert", which makes the same claim for the occupied case.check-changeset-overwritelists the file as a modified pre-existing changeset (exit 0). The declaration is unchanged:@object-ui/core: minorbefore and after.Pins. Two new tests in the same describe:
the id_field line never claims NO reader sees the value — kept snake reads do: the line does not contain "reaches no reader". As a lit control on the same line, it still carries the narrower claim.CONTROL — the scoped claim is kept where it is true, beside the fact it states: an occupieddisplayFieldthat keeps the producer's value, plus a typo. Both lines carry "will not see this value".(The backticks in the first test name are dropped here.)
Ablation, from committed HEAD
a71744d4f: the old two-line clause was restored throughablation-replace.mjs. The anchor went from 1 hit to 0 and the blob from5cc832c5b346toe0eaee263d61. "reaches no reader" counts 1 in the mutated file and 0 at HEAD. The prediction was 2 red, the two new pins. The run gaveTests 2 failed | 77 passed (79), and the two failures were exactly those pins. The file was restored and checked by state: the blob is back to5cc832c5b346andgit diff HEADis empty. A trap on EXIT INT TERM guarded the restore.Gates at
a71744d4f(exit codes captured before any pipe):pnpm --filter "@object-ui/core^..." buildthenpnpm --filter @object-ui/core type-check: 0.pnpm --filter @object-ui/core lint: 0, with 0 errors. The 568 warnings were already there, and the two touched source files have 0 errors and 0 warnings.Test Files 4 passed (4),Tests 79 passed (79).packages/core/:Test Files 190 passed (190),Tests 3739 passed | 27 skipped (3766). The 21 outside files:Test Files 21 passed (21),Tests 192 passed (192).check-governed-queue-guard.mjs --teston the four paths: NOT GOVERNED.Noted, not changed. A test title in
reference-keys.measuredSurface-8938.test.ts,speaks up when the canonical key is OCCUPIED — the retired value is inert, and that was silent, makes the same "inert" claim. That file is outside the claim's file surface, so it is left for the seat.The
Clause-②: nodeclaration above still holds, because only text changes.Generated by Claude Code