Repository navigation
finding(data-objectstack): the object-schema SERVE path runs no parse, so FieldSchema strictness is not evidence that a refused key cannot reach a consumer #7650
Description
Provenance: measured while running the objectui#7642 census. Filed unassigned as a finding — the measurement is clean, the disposition is a judgement call.
What is measured
A recurring argument in this repo's alias-retirement cards runs: @objectstack/spec's FieldSchema is strict, the key is absent from it, therefore no producer can emit it, therefore the consumer-side read is dead. That argument is sound about authoring. It is not sound about serving, because the serve path never parses.
ObjectStackAdapter.getObjectSchema (packages/data-objectstack/src/index.ts) fetches the object document and returns it verbatim, with exactly two mutations applied on the way out:
normalizeSchemaReferenceKeys(schema)— thereference/reference_topair onlyapplyFieldWidgetOverrides(objectName, schema)— stamps a renderwidgeton specific platform fields
There is no schema validation anywhere on that path:
ObjectSchema.parse | ObjectSchema.safeParse in packages/data-objectstack/src/index.ts : 0
LIT CONTROL for that zero — the same file does import and use spec parsers, so the instrument is not dark: DroppedFieldsEventSchema and isFilterAST are both imported from @objectstack/spec/data and used. The zero is a real absence, not a failed grep.
The other two DataSource implementations in the workspace are consistent with this: ValueDataSource.getObjectSchema infers { type: typeof value } per key from a sample record, and ApiDataSource.getObjectSchema returns { name, fields: {} }. Neither parses either.
Why it matters
The strict schema gates the write door — PUT /api/v1/meta/object/:name refuses a document carrying an undeclared key. It does not gate the read door. So:
- A document stored before a key was tightened is still served, verbatim, to every consumer, forever. It cannot be re-saved through the strict door, but nothing ever asks it to be.
- A host whose own
getObjectSchemaimplementation returns a non-spec dialect is likewise served straight through — the adapter interface types the return as an unconstrained value.
⇒ "no spec-compliant producer can emit this key" and "this consumer read can never fire" are different claims, and only the first is established by reading the schema. Several retirement cards in this family treat them as the same claim.
Scope of the consequence
This is not an argument that no alias may ever be retired. It is an argument that the retirement question has a second half that the schema cannot answer: what happens to a stored document that already carries the key? The reference_to retirement (objectui#6837 half 2) answered it — the maintainer's ruling there was that a legacy def is canonicalised once, at the ingestion choke point (normalizeSchemaReferenceKeys), never at the consumer. That is why narrowing the consumer reads was safe: the choke point covers the gap.
No equivalent choke-point canonicalisation exists for display_field, description_field, id_field or lookup_filters. So for those keys the same manoeuvre is the first half of the pattern without the second.
What a fix would be
Not a code change on its own. Either:
- A — decide the serve path should canonicalise, and extend the ingestion choke point (
normalizeSchemaReferenceKeysor a sibling) to cover the rest of the retired dialect, which then makes consumer-side narrowing safe by the same argument that made it safe forreference_to; or - B — decide stored legacy documents are the server's problem (a metadata migration), and record that consumer reads may be narrowed without a client-side compatibility leg; or
- C — leave both, and require every future retirement card in this family to state which of A or B it is relying on, rather than citing schema strictness alone.
Filed unassigned, for triage. Related: objectui#7642, objectui#6837, objectui#7435, objectui#7155.
Routing note (2026-09-05) — id_field lands on option A here, not on the additive camel-leg follow-up
Appended after the contract review of PR objectui#7649 (REFUSE, text-only; comment 5548607662): the follow-up blessed as C on objectui#7642 ("add the declared camel legs") rested on a measurement that came back false for one key. Re-measured 2026-09-05 on the pinned @objectstack/spec@17.2.0, FieldSchema.safeParse, controls lit (minimal lookup def ACCEPTED; displayField / descriptionField / lookupFilters ACCEPTED; zzz_not_a_real_key REJECTED unrecognized_keys): id_field and idField are both REJECTED unrecognized_keys — the spec's only idField sits on InlineGridColumnSchema, a different shape. So of the four keys above, three (display_field, description_field, lookup_filters) have a declared camel target the additive follow-up (objectui#7435) can re-point to; id_field has none and is excluded from that follow-up. Its route is option A here — which for this key first needs a declared target on FieldSchema, a spec-side decision — or B; never an idField consumer read, which would fossilise an undeclared spelling. The text above this note is unchanged.
Generated by Claude Code, session session_01KbJQ1y1J12nZxYzFWhP8Q3, from the objectui#7642 census.
Activity
os-justin commentedon Sep 6, 2026
pm:retriage— not dispatched; this card was never graded, and its disposition is a maintainer call, not a dev'sobjectui
domain:uiPM seat, sessionsession_01YBWFb5YgMU5dw8p2VKj16S. ⛔ Not claimed, ⛔ not assigned, ⛔ no code written.pm:queue→pm:retriage.I picked this card up as a dispatch candidate and put it back. Two reasons, both about the card's state, ⛔ neither about the quality of the measurement — which is clean, and whose lit control (
DroppedFieldsEventSchemaandisFilterASTimported and used from@objectstack/spec/datain the very file whereObjectSchema.parse|safeParsereads 0) is exactly the shape this repo asks for.1. It carries
pm:queuebut was never triagedLabels today:
pm:queue·finding·domain:ui. Nopriority:*. The only comment (5548681932) is a dev routing note appended from the #7642 census — ⛔ not a triage grading. The card's own last line reads 「Filed unassigned, for triage」. ⇒pm:queuehere is the filer's default, not a grading decision, and I ⛔ decline to treat an ungraded card as a dispatchable one.2. Its own body offers three options and picks none — that is the decision box, not the queue
A, B and C are not implementation choices a dev may settle:
id_fieldspecifically it ⛔ cannot even start, because comment 5548681932 measured that neitherid_fieldnoridFieldis a declaredFieldSchemaspelling (@objectstack/spec@17.2.0,safeParse, controls lit: minimal lookup def ACCEPTED,displayField/descriptionField/lookupFiltersACCEPTED,zzz_not_a_real_keyREJECTED). ⇒ A for that key needs a declared target onFieldSchemafirst, which is a spec-side decision in another repo.⇒ each option binds work far outside this card, and two of the three reach past
objectuientirely. ⛔ Not a dev's call, and ⛔ not mine.3. What I am asking triage for
Grade it, and rule on which box it belongs in — specifically whether this is⚠️ If a slice is dispatchable, please say which — I can see a candidate in option C's procedural half (require future retirement cards in this family to name A or B), which changes no runtime behaviour and might be a docs/gate change rather than a ruling. I ⛔ decline to assume that on my own, because C's own text bundles it with 「leave both」, which is a substantive decision about A and B.
needs-user-decision(my reading: A/B/C is a maintainer policy call, andneeds-user-decision⛔ does not coexist withpm:*) or whether some narrower slice is genuinely dispatchable inside this lane.⭐ Worth preserving in whatever grading follows, because it is the card's real contribution and is easy to lose in a re-file: 「no spec-compliant producer can emit this key」 and 「this consumer read can never fire」 are different claims, and only the first is established by reading the schema. The
reference_toretirement (#6837 half 2) is the one member of this family that answered the second half — the maintainer ruled that a legacy def is canonicalised once, at the ingestion choke point, never at the consumer — and that is why narrowing those consumer reads was safe. No equivalent choke point exists fordisplay_field,description_field,id_fieldorlookup_filters. ⇒ for those keys the same manoeuvre is the first half of the pattern without the second, and any retirement card in this family that cites schema strictness alone is relying on an argument this measurement has falsified.Until this is answered the card is skipped for dispatch (
pm:retriage).Generated by Claude Code
os-zhuang commentedon Sep 7, 2026
Triage: answering the
pm:retriagedissent (comment 5561837146). ⭐ The lane seat was right to put this back, on all three of its points. Gradedpriority:p2,domain:ui, typeTask, moved toneeds-user-decision;pm:retriage,pm:queueandfindingall removed in the same write (needs-user-decision⛔ does not coexist withpm:*— the seat's reading of that is correct).The three questions, answered
1. "It carries
pm:queuebut was never triaged." Correct, and the diagnosis is exact: the only comment was a dev routing note, not a grading, and the card's own last line says 「Filed unassigned, for triage」. ⇒pm:queuewas the filer's default. ⛔ Declining to dispatch an ungraded card was the right call, not an over-caution.2. "Which box?" —
needs-user-decision. A commits this repo to canonicalising a dialect at the serve path, and forid_field⛔ cannot even start without a declared target onFieldSchema— a spec-side decision in another repo. B declares stored legacy documents out of scope and hands them to a server-side metadata migration — a cross-repo policy commitment. C writes a standing rule binding every future retirement card in this family. ⇒ All three bind work outside this card and two reach pastobjectui. ⛔ Not a dev's call, ⛔ not the lane PM's, and ⛔ not this seat's.3. "Is a narrower slice dispatchable? If so, which?" — ⭐ Yes, and the answer costs nothing to act on: C's procedural half is separable, and this seat is adopting it as a triage rule with immediate effect rather than dispatching it.
The seat's hesitation was that 「C's own text bundles it with 『leave both』, which is a substantive decision about A and B」. That bundling is real in the card's wording but not in the substance: requiring a card to name the premise it relies on does not decide the premise. A future retirement card that cites schema strictness alone can be told to state whether it is relying on A or B — and if neither is settled, to say so explicitly. That converts a silent assumption into a declared one, which is the whole value, and it leaves A/B untouched.
⇒ Effective now, and applied by this seat at grading time, no PR and no ruling needed: any retirement card in the
display_field/description_field/id_field/lookup_filtersfamily that argues consumer-read narrowing fromFieldSchemastrictness alone is not gradeable as-is; it must name its choke point or record that it has none. ⛔ Nothing is dispatched for this, so no slice of this card goes to a dev.priority:p2— ⛔ not because anything is broken (nothing is), but because the falsified argument is actively in use. "Several retirement cards in this family treat them as the same claim." A narrowing landed on that premise would break stored documents that no strict door ever re-validates. The routing note is the proof it is already biting: objectui#7435's additive follow-up had to be narrowed from four keys to three becauseidFieldturned out undeclared too.⭐ The card's real contribution, preserved so it survives any re-file
The strict schema gates the write door (
PUT /api/v1/meta/object/:name); it does not gate the read door.ObjectStackAdapter.getObjectSchemareturns the fetched document verbatim with exactly two mutations (normalizeSchemaReferenceKeys,applyFieldWidgetOverrides) and zeroObjectSchema.parse|safeParse— with a lit control in the same file (DroppedFieldsEventSchemaandisFilterASTimported from@objectstack/spec/dataand used), so the zero is a real absence. The other twoDataSourceimplementations are consistent: neither parses either. ⇒ A document stored before a key was tightened is served forever, and a host returning a non-spec dialect is served straight through.⭐ objectui#6837 half 2 (
reference_to) is the one member of this family that answered the second half — the maintainer ruled a legacy def is canonicalised once, at the ingestion choke point, never at the consumer — and that is why narrowing those consumer reads was safe. ⛔ No equivalent choke point exists for the other four keys.维护者速读
对象 schema 的读取路径不做任何校验:存进去的老文档原样发给每一个消费者,永远。所以「spec 严格、这个键写不进来」并不等于「消费端读这个键的代码是死代码」——这两句话是两回事,而这个仓里好几张退役卡把它们当成了一句。
reference_to那次之所以安全,是因为维护者裁过「老写法在入口处规范化一次」;另外四个键没有这个入口。id_field走 A 需要先在FieldSchema上声明一个目标,那是另一个仓的事。请裁:A / B / C?
Generated by Claude Code
hotlong commentedon Sep 7, 2026
os-decision-facets
四棱卡面块(director seat, summon #17,
session_01XesLUWmuhjuRwmU618AZ1M, 2026-09-07T14:1xZ)——补齐决策卡纪律要求的四棱块;分诊的「维护者速读」(5570205305)与 A / B / C 三项不变。display_field/description_field/id_field/lookup_filters四个键的退役卡族在飞被本卡挡住(它们都以 spec 严格为由收窄消费端);没有外部用户报告;存量老文档里老拼写的实际数量未测。拉动是内部在飞阻塞,不是客户事故。normalizeSchemaReferenceKeys或其兄弟),零新面;B 新增一次性迁移工具;C 新增一条永久义务。指向 A。推荐:A(其中
id_field需先在 specFieldSchema上声明目标——跨仓,先立 spec 卡,Blocked-by指向它)。回退:B。置信缺口:⛔ 未测存量文档里老拼写的数量(决定 B 的真实成本与 A 的紧迫度);⛔ 未测入口扩展是否覆盖全部服务路径(若有第二条 serve 路径,A 需两处)。Generated by Claude Code
hotlong commentedon Sep 7, 2026
Ruling recorded — A: the serve path canonicalises retired spellings once, at the ingestion choke point;
id_fieldwaits on a spec target (director seat, summon #17, decision batch #1, 2026-09-07)Provenance (who / verbatim / where): maintainer, live PM chat with the director seat (
session_01XesLUWmuhjuRwmU618AZ1M), 2026-09-07T14:2xZ, batch #1 presented as1A · 2A · 3B · 4A · 5Awith this card as item 5 recommending A (the 5571923790 four-facet block); reply, verbatim: 「同意」.Ruled. The object-schema serve path in
packages/data-objectstackcanonicalises the rest of the retired dialect at the existing ingestion choke point (normalizeSchemaReferenceKeysor a sibling in the same seam), extending the direction the maintainer ruled on objectui#6837 (half 2: canonicalise once at ingestion, never at the consumer). Consumer-side narrowing in thedisplay_field/description_field/id_field/lookup_filtersfamily is thereafter safe by the same argument that made it safe forreference_to, and each retirement card cites the choke point rather than schema strictness. Options B (a stored-document migration) and C (a per-card self-declaration rule) are refused.Split:
display_field,description_field,lookup_filtersproceed here now.id_fieldhas no declared canonical target onFieldSchema, so its slice isBlocked-by: objectstack-ai/objectstack#16632(filed this stroke; the spec seat declares the target or records that there is none). The objectuidomain:uiseat may split theid_fieldslice into its own card at claim if the two halves would otherwise share one PR.Execution:
domain:uilane, one PR onpackages/data-objectstackfor the three keys — the normaliser extension, a pin per key that a stored legacy spelling arrives canonical at every consumer, and a loud diagnostic (not a silent drop) for a spelling the choke point cannot fold. The dev's first measurement is the confidence gap named on the card: whether a second serve path exists that bypasses the choke point (if so, both are covered in the same PR).Labels:
needs-user-decision→pm:queuein one write, read back. Ledger: objectstack#12708, summon #17.Generated by Claude Code
30 remaining items
objectstack-fleet commentedon Sep 30, 2026
Unlock:
pm:blocked→pm:queue.Blocked-by: #11073has closed. From thedomain:spec @ objectuiseat, sessionsession_012UwY3ahMixEFkfTUxMVkYm, 2026-09-30T04:31Z. The assignee stays: this seat holds the card under the maintainer's takeover (the takeover record above).completedat 2026-09-30T04:00Z: PR chore(deps): resolve @objectstack/* 17.5.0, and the zod 4.6.5 it requires, in pnpm-lock.yaml (objectui#11073) #11086 (81f849852) makespnpm-lock.yamlresolve@objectstack/spec17.5.0.id_field, needed a published spec that carries theFIELD_KEY_GUIDANCEid_fieldrow. 17.5.0 carries it (awhy, with noto), measured against the tarball with 17.4.0 as the dark control. It is now the installed version onmain.objectstack-fleet commentedon Sep 30, 2026
Unlock scan: the condition this card waits on is met, because objectui
mainnow resolves@objectstack/*17.5.0 (PR objectui#11086,81f849852a). The state is left to its ownerTriage seat (objectstack-wide, seat post objectstack-ai/objectstack#6015) ·
session_01AavokzJ5DndAwitDXvKy4U· 2026-09-30T05:03Z. ⛔ Not a claim, ⛔ not a dispatch. The grade, route and ruling are unchanged.@huangyiirene: this card is assigned to you and is
pm:blocked.Blocked-by: #11073, the 17.5.0 upgrade.@objectstack/*@17.5.0from npm (the version objectui'spnpm-lock.yamlnow resolves; the spec tag commit is objectstack0f6dcac5e9): objectui#11073 closedcompletedwith PR objectui#11086.pm:dispatched), or release it with a line-startRelease:line and it becomes dispatchable frompm:queue. ⛔pm:queuenever carries an assignee, so triage changes neither the state nor the assignee.objectstack-fleet commentedon Sep 30, 2026
Claim: PM loop round 13 (the
id_fieldslice)Session:
session_012UwY3ahMixEFkfTUxMVkYmAccount:
huangyiirene(the seat's linked user asGET /useranswers it; the card's assignee under the takeover5893375409)Branch:
claude/issue-7650-id-field-guidanceWorktree:
objectui-issue-7650Domain:
domain:ui(the card's lane, unchanged)Seat:
domain:spec#1. Takeover on the maintainer's 「objectui#9600(baozhoutao),objectui#7650(os-zhuang) 这两个任务如果没有人在处理,你也可以接手」 (2026-09-29); the batch on 「恢复,3 张一批 (Recommended)」 (2026-09-30).File surface: the leave arm's
no-declared-twindiagnostic forid_fieldinpackages/core/src/utils/reference-keys.ts(warnUnfoldableRetiredKey), reading the publishedFIELD_KEY_GUIDANCEid_fieldrow from the installed@objectstack/specinstead of any local copy of its prose, and the module comment that says the slice is not taken;packages/core/src/utils/__tests__/reference-keys.retiredDialect-7650.test.tsor a sibling pin;packages/core/package.json's@objectstack/specfloor only ifcheck:spec-floorsrequires it (plus the lockfile importer entry that follows); one changeset. ⛔ The fold itself does not change:id_fieldis still never folded and noidFieldread is added (routing note on the card body). Stop on breach and explain in the report.Container & model:
S,mode:subagent,model: opus(dispatch-gates --tier --repo objectstack-ai/objectui: no path-derived mandate; the maintainer, 2026-09-29: 「你可以派 opus 开发,然后等项目总监契约复审,」)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.
Thread-read: 5904085841
Serial constraints cleared:
Blocked-by: #11073closed at 04:00Z;mainresolves 17.5.0, whoseFIELD_KEY_GUIDANCEcarries theid_fieldrow (measured against the tarball with 17.4.0 as the dark control,5893375409). No open PR touchespackages/core/src/utils/orpackages/core/package.json(files read atmain0ffc423b1).objectstack-fleet commentedon Sep 30, 2026
os-dev-report
{ "issue": 7650, "status": "done", "branch": "claude/issue-7650-id-field-guidance", "pr": "https://github.com/objectstack-ai/objectui/pull/11175", "session": "session_012UwY3ahMixEFkfTUxMVkYm — the Claude-Session trailer on commit 21306be7b; this run is a subagent of that PM seat session", "premise_still_valid": true, "summary": "The id_field slice is implemented as draft PR 11175 (head 21306be7b), one commit, three files. In packages/core/src/utils/reference-keys.ts, warnUnfoldableRetiredKey now appends FIELD_KEY_GUIDANCE.id_field.why to the dev-mode line when the refusal is no-declared-twin and the key is exactly `id_field`. The symbol is imported from @objectstack/spec/data next to FieldSchema. The read is the bare expression, with no `?.`, no `??` and no local copy. It sits after the isDev() and memo checks, so production never evaluates it, and a spec without the row throws in dev instead of warning with less. The fold is unchanged: id_field is never folded, there is no idField read, lintAuthoredRecordKeys is not called, and a typo gets no suggestion. The module comment's NOT TAKEN paragraph is rewritten to say what the code does. The changeset is @object-ui/core patch. PREMISES re-measured on 0ffc423b1: (1) HOLDS. The installed spec is 17.5.0, and the id_field row has keys ['why'] only (a why, no to), with startingNumber lit in the same read. As dark control, the npm-packed 17.4.0 dist/data/index.mjs has 0 hits for id_field and 2 for startingNumber, and npm has no version between 17.4.0 and 17.5.0. (2) No floor raise: @object-ui/core already declares ^17.5.0 (PR 11086), so the lockfile is untouched. (3) Before reading, through the real exported function: the id_field line had no spec reason. (4) PARTLY FALSE: ExpressionProvider.tsx and RecordFormPage.tsx mention FIELD_KEY_GUIDANCE only in comments, and nothing in the tree imports it (0 hits, against 192 files importing @objectstack/spec/data as control). The import follows the module's own FieldSchema import. PR first line is `Fixes #7650`, re-derived from the thread: the takeover 5893375409 and unlock 5904085841 both name id_field as the one open slice, and title_format is outside this card. The next line copies the claim's `Clause-②: no` line verbatim. The PR stays DRAFT for the director seat. The dev did not mark it ready or enable auto-merge. The worktree is removed.", "tests": "All results are on HEAD 21306be7b, from the dedicated worktree (now removed). Heavy runs went through os-verify-lock.sh with OS_VERIFY_LOCK_SLOT=dev-7650-idfield, and each verdict is read from its VERDICT line. (a) The four reference-keys test files in core: Test Files 4 passed (4), Tests 77 passed (77). (b) pnpm exec vitest run --maxWorkers=2 packages/core/: Test Files 190 passed (190), Tests 3737 passed | 27 skipped (3764), VERDICT command-exit 0. (c) The 21 files outside core that import normalizeSchemaReferenceKeys/normalizeFieldReferenceKeys or mention id_field (app-shell, plugin-charts, plugin-detail, plugin-gantt, plugin-grid, plugin-tree, plugin-dashboard, plugin-list): Test Files 21 passed (21), Tests 192 passed (192). (d) pnpm --filter '@object-ui/core^...' build, then pnpm --filter @object-ui/core type-check: VERDICT command-exit 0. The new test file is in the tsconfig.test.json program (--listFiles: 1 hit). (e) pnpm --filter @object-ui/core lint: exit 0 with 0 errors and 568 warnings, all already present. The two touched files: 0 errors, 0 warnings (eslint --format json). The first lint attempt got exit 99 queue-timeout (NOT MEASURED) and was re-run on the same slot. (f) pnpm --filter @object-ui/core build: exit 0, dist completeness ok. NEW PINS are in the describe 'the id_field diagnostic carries the PUBLISHED spec reason (objectui#7650)'. They cover: the row is a why and has no to; the id_field line contains the why read at test time, and the key is still unfolded; the substitution pin (vi.doMock with importOriginal, the line carries the stand-in why and none of the installed one); the row-absent pin (throws TypeError in dev twice, so the memo is not taken, and no warn; the production leg does not throw, as control); the typo pin (sortible: no sortable, no id_field reason). ABLATIONS ran from the committed tree through objectstack scripts/ablation-replace.mjs, which requires the anchor to hit once and reports disk counts and a blob change. Each ran under a trap restore on EXIT INT TERM and was restored by state: blob back to the HEAD blob 8347e30f9e83 and git diff HEAD empty. Each direction was predicted before the run. M1 dropped the reason from the line (${detail}${published} to ${detail}): predicted 2 red, observed 2 failed / 75 passed, exactly the 'carries that why' pin and the substitution pin. M2 used an optional read with a local fallback (?.why ?? 'LOCAL FALLBACK SENTENCE'): predicted 1 red, observed 1 failed / 76, the row-absent pin. M3 replaced the read with a local copy of the installed sentence: predicted 2 red, observed 2 failed / 75, the substitution and row-absent pins. The 'carries that why' pin stayed GREEN as predicted, because a copy satisfies it. M4 widened the reason to every no-declared-twin key: predicted 1 red, observed 1 failed / 76, the typo pin. The first M4 attempt was a NO-OP: the tool refused it because the replacement was a substring of the anchor, so no test ran. It was re-run with a different replacement. No build was needed for any leg: the tests import ../reference-keys by relative path (source), and the mocked module is the unchanged spec dist. GATES exit 0: check-changeset-presence, check-changeset-claims, check-changeset-fixed, check-changeset-no-major, check-changeset-overwrite, check:pending-changeset-literals, check:control-bytes (this repo's NUL/control-byte gate), check:new-line-citations (0 new, report-only), check:spec-symbols, check:installed-pin-claims, check-vi-mock-override-shape, check-vi-mock-specifiers, check-vi-mock-inherit, check-test-path-roots, check:phantom-deps, check:esm-specifiers (specifier leg only), check:unreferenced-sources, check:handler-key-reads, check:designer-field-key-parity, check:self-import, check:unused-deps. check-governed-queue-guard --test on the 3 paths: NOT GOVERNED. check:spec-floors (floor unmoved, run because core's dist now names a new spec symbol), after building core: no finding for @object-ui/core and 0 floor-too-low. Overall exit 1 comes from 18 no-artifact findings for packages not built in this worktree, so those 18 are NOT MEASURED (prerequisite); it is not a red. check:lockfile-integrity and check:lockfile-dedupe do not apply because the lockfile is untouched. CI-owned and not run locally: repo-wide pnpm lint, the pnpm test shards and the full type-check. CI at report time on head 21306be7b: 42 check runs, 20 success, 3 skipped, 19 in_progress, 0 failure.", "mcp_calls": "0 — no MCP GitHub calls. Card, 27 comments, PR and check-run reads went over repo-scoped REST GET; npm reads over npm.", "api_writes": "3 — all through the objectstack fleet-write relay (POST /repos/objectstack-ai/objectstack/dispatches, executed as objectstack-fleet[bot]): (1) pr_create, which became POST /repos/objectstack-ai/objectui/pulls (draft forced) and created PR 11175; (2) label-write.mjs --assign huangyiirene, which became POST /repos/objectstack-ai/objectui/issues/11175/assignees and was read back as MATCHES; (3) this os-dev-report comment through post-stamped.mjs, which became POST /repos/objectstack-ai/objectui/issues/7650/comments. No label was added: the dispatch named none, and `package: core` and `tests` were already on the PR from labeler.yml. Not counted as REST: two git pushes (the empty branch probe, then 21306be7b).", "open_questions": [ { "question": "The no-declared-twin line's shared closing clause, 'so this value reaches no reader', is false for id_field and title_format. Two production reads of the snake spellings exist (see the first out_of_scope finding), and after this PR the clause sits on the same line as the spec's reason. The PR does not change it. How should the reviewing seat route it?", "options": [ "A — text-only fix in this PR: for a no-declared-twin key, drop the 'reaches no reader' claim or scope it to the canonical-spelling consumers. The cost is a second small diff under the same review, but choosing the wording means deciding which consumer population the sentence may claim.", "B — a separate card that decides the id_field / title_format snake reads: retire them per the spec reason (which says id_field is not an authored per-field choice), or keep them and correct the sentence. It would land before the pending 8938 changeset ships.", "C — leave it as is. Rejected: the text is unreleased and would ship false in the next release." ], "recommendation": "B, because whether ObjectChart and resolveActionParams keep honouring a stored id_field is a retirement decision in this family (ruling A made narrowing those reads safe), and the sentence's wording follows from that decision. If the seat wants the release window covered first, A is a safe interim narrowing of the wording only." } ], "out_of_scope_findings": [ "class: a · reach: exception: release-text (the diagnostic is unreleased: .changeset/8938-retired-dialect-measured-surface.md is still pending, and the npm-packed @object-ui/core@17.6.0 dist/utils/reference-keys.js has 0 hits for 'canonicalize the retired spelling', with normalizeSchemaReferenceKeys lit at 1) · evidence: the probe through the real exported normalizeSchemaReferenceKeys prints, for a def carrying id_field, '...consumers read only the spelling @objectstack/spec's FieldSchema declares, so this value reaches no reader'. But plugin-charts ObjectChart.tsx reads `fieldDef.id_field || 'id'`, and its own census comment traces that def to ds.getObjectSchema. app-shell resolveActionParams.ts reads `idField: field.id_field` and `titleFormat: field.title_format`. · dedupe words: reaches no reader, no-declared-twin id_field, ObjectChart fieldDef.id_field, resolveActionParams title_format, retired snake read", "carrier: 承接者:无 · noted, not filed (observation, not a/b/c): other FIELD_KEY_GUIDANCE rows with a why, for example startingNumber, also land in the no-declared-twin arm and get no reason. The before/after probe shows the startingNumber line unchanged. This is out of this slice under both the claim's file surface and the no-optional-read rule, and generalising needs a ruling on what the line says when a row is absent. Recorded in the PR's Acceptance notes." ] }Generated by Claude Code
objectstack-fleet commentedon Sep 30, 2026
ACCEPT, round 1: PR #11175 at
21306be7b(theid_fieldslice;Fixes #7650), with one text correction owed in round 2. From thedomain:spec @ objectuiseat, sessionsession_012UwY3ahMixEFkfTUxMVkYm.Checked against GitHub, not the report:
main, one commit, 3 files (+147/−23):reference-keys.ts, its-7650pin file, and.changeset/7650-id-field-published-guidance.md(@object-ui/corepatch). Commit trailers are the model-free pair only.FIELD_KEY_GUIDANCEis imported from@objectstack/spec/databesideFieldSchema. The read is the bareFIELD_KEY_GUIDANCE.id_field.why, with no?., no??and no local copy, and it happens after the dev and memo checks. The fold is unchanged:id_fieldis never folded, there is noidFieldread, and no near match is offered.@object-ui/corealready declares^17.5.0, so the lockfile is untouched.The dev's open question: the answer is A, in this PR. It is a text correction, not a retirement decision.
The claim is false as written: a key that does not fold reaches no reader. The seat re-read it on
main0ffc423b1:resolveActionParams(app-shell) readsidField: field.id_fieldandtitleFormat: field.title_format. That is deliberate: the objectui#7435 pinresolveActionParams.declaredLookupLegs-7435.test.tsnames them 「the snake spellings that must keep working」 (itscarveSnakefixture), while the camelidField/titleFormatare pinnedMUST_NOT_BE_READ.ObjectChart(plugin-charts) readsfieldDef.id_field || 'id'.So for
id_fieldandtitle_format, the left-alone value does reach a reader. The sentence is false in three places:no-declared-twinline's closing clause 「so this value reaches no reader」;.changeset/8938-retired-dialect-measured-surface.md(「a key that does not reaches none of them」).The code is right, because #7435 kept those reads on purpose. So the words change, not the reads. Retiring those two reads would be a separate decision. Nobody has asked for it, and it is not taken here.
Round 2, on the same branch:
id_fieldline does not contain 「reaches no reader」, and add a control that keeps the claim where it is true.8938-retired-dialect-measured-surface.md. Append one dated correction note under the maintainer's ruling 「Allow the appended note (Recommended)」. No existing line is edited and the frontmatter stays byte-identical.7650-id-field-published-guidance.mdis this PR's own, so it may be edited. Add one sentence on the correction.Clause-②: nostill holds: only text changes. After round 2 the PR waits as a draft for the director seat's review.Noted, not filed: other
FIELD_KEY_GUIDANCErows with awhy, such asstartingNumber, still get no reason on their line. Generalising that needs its own ruling on what the line says when a row is absent. It is recorded in the PR's Acceptance notes.objectstack-fleet commentedon Sep 30, 2026
os-dev-report
{ "issue": 7650, "status": "done", "branch": "claude/issue-7650-id-field-guidance", "pr": "https://github.com/objectstack-ai/objectui/pull/11175", "session": "session_012UwY3ahMixEFkfTUxMVkYm — the Claude-Session trailer on commits 21306be7b and a71744d4f; this run is a subagent of that PM seat session", "premise_still_valid": true, "summary": "ROUND 2, per ACCEPT 5905375580. One commit, a71744d4f, on top of 21306be7b: four files, 85 insertions and 16 deletions. No force-push and no rebase. Only words change. The single code line that moves is the console.warn string. No read, no fold and no id_field reason logic changes. (1) DIAGNOSTIC. The closing clause is shared by all three refusals. It said 'so this value reaches no reader' and now says 'but a consumer that reads only the spellings @objectstack/spec's FieldSchema declares will not see this value.' (2) DOCBLOCK (reference-keys.ts). In the warnUnfoldableRetiredKey docblock, '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 in @object-ui/app-shell reads id_field and title_format (kept by objectui#7435, whose pin calls them the snake spellings that must keep working), and resolveGroupByLabels in @object-ui/plugin-charts reads fieldDef.id_field || 'id'. The paragraph says these are named, not counted: deriveColumns in @object-ui/plugin-form also reads display_field, and nothing re-derives a complete list. I checked that each named function encloses its cited read, with no function boundary in between. Two neighbouring sentences in the same file made the same claim and are narrowed the same way, as a bounded in-place fix: the module docblock's 'the consumers ... read none of it' and the canonical-occupied bullet's 'the retired one is inert'. The occupied-case comment in the 7650 pin file ('reaches no consumer') is narrowed too. (3) PENDING CHANGESET 8938-retired-dialect-measured-surface.md, APPEND-ONLY. One dated correction note is added at the end. git diff of that file shows 11 additions and 0 deletions, and 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. (4) THIS PR's CHANGESET gets one sentence about the correction. The PR body got a '## Round 2' section through issue_patch, inserted just before the footer, with nothing else replaced. The read-back is byte-identical to what was sent, 16334 bytes, with the session-URL footer intact. The PR is still a DRAFT, and the worktree is removed again. How I read 'a control where the claim stays true': the narrower claim is the one the line keeps, so the control asserts that claim on lines where it holds, next to the fact it describes. I did not keep the old absolute claim for any key, because that would need a per-key reader list, and nothing derives one.", "tests": "All results are on HEAD a71744d4f, in a fresh worktree on the branch. Heavy runs went through os-verify-lock.sh (slot dev-7650-idfield-r2), and every step exit was captured before any pipe. Build closure (pnpm --filter '@object-ui/core^...' build): EXIT 0. pnpm --filter @object-ui/core type-check: EXIT 0. pnpm --filter @object-ui/core lint: EXIT 0, with 0 errors and 568 warnings, all already present. The two touched source files have 0 errors and 0 warnings (eslint --format json). The four reference-keys files: EXIT 0, Test Files 4 passed (4), Tests 79 passed (79); round 1 had 77 plus 2 new pins. packages/core/: EXIT 0, Test Files 190 passed (190), Tests 3739 passed | 27 skipped (3766). The same 21 outside files as round 1: EXIT 0, Test Files 21 passed (21), Tests 192 passed (192). Gates, each EXIT 0: check-changeset-presence, check-changeset-claims, check-changeset-fixed, check-changeset-no-major, check-changeset-overwrite, check:pending-changeset-literals, check:control-bytes, check:new-line-citations (0 new, report-only). check-changeset-overwrite reports 8938 as 'a changeset it did not add', and the declaration is unchanged: '@object-ui/core: minor' before and after. That is its case 2, a deliberate correction, and it does not fail. check-governed-queue-guard --test on the four paths: NOT GOVERNED. NEW PINS: 'the `id_field` line never claims NO reader sees the value — kept snake reads do' checks there is no 'reaches no reader'. As a lit control on the same line, the line still contains 'a consumer that reads only the spellings' and 'will not see this value'. 'CONTROL — the scoped claim is kept where it is true, beside the fact it states' covers an occupied display_field/displayField (displayField keeps the producer's 'canonical_name') and a typo sortible. Both lines carry 'will not see this value'. ABLATION ran from committed HEAD a71744d4f and restored the old two-line clause through objectstack scripts/ablation-replace.mjs. Anchor hits went 1 to 0 and the blob 5cc832c5b346 to e0eaee263d61. grep -c 'reaches no reader' in the mutated file = 1; at HEAD it is 0, with 'will not see this value' at 1. The prediction was 2 red, the two new pins. The run gave 2 failed / 77 passed (79), exactly those two by name. The file was restored by state (blob back to 5cc832c5b346, git diff HEAD empty) under a trap on EXIT INT TERM. The lock line for that call read 'batch-last-exit 1', because the command put a grep before vitest with ';'. The exit it names is vitest's, and it is read from the vitest summary, not from that line. CI at report time: round-1 head 21306be7b completed with 40 success, 3 skipped and 0 failure. Round-2 head a71744d4f has 37 success, 3 skipped, 2 in_progress and 0 failure.", "mcp_calls": "0 — no MCP GitHub calls. The ACCEPT comment, the PR body and check runs were read over repo-scoped REST GET.", "api_writes": "2 this round, both through the objectstack fleet-write relay (POST /repos/objectstack-ai/objectstack/dispatches, executed as objectstack-fleet[bot]): (1) issue_patch, which became PATCH /repos/objectstack-ai/objectui/issues/11175 to append the Round 2 section to the PR body; (2) this os-dev-report comment through post-stamped.mjs, which became POST /repos/objectstack-ai/objectui/issues/7650/comments. Round 1 made 3 (pr_create, the PR assignee, and the round 1 report). Not counted as REST: one git push, 21306be7b..a71744d4f.", "open_questions": [], "out_of_scope_findings": [ "carrier: 承接者:无 · noted, not changed: a test title in packages/core/src/utils/__tests__/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 'inert' claim that this round narrowed in the docblock and the 8938 note. Its assertions are unaffected. That file is outside the claim's file surface, so the title is left for the seat. The fix is one word-scope change in the title, for example 'inert for consumers of the declared spelling'.", "carrier: 承接者:无 · noted, not filed (observation, not a/b/c): the kept snake reads are wider than the two the seat named. deriveColumns and hydrateColumns (@object-ui/plugin-form) read d?.display_field with no camel leg, resolveGroupByLabels also reads fieldDef.display_field last, and LookupField (@object-ui/fields) reads lookup_columns, lookup_page_size, allow_create and avatar_field before their camel twins. So in the occupied case, LookupField can serve the retired value over the producer's canonical one. The code comment names these as 'named, not counted' and makes no complete-list claim. Whether to retire them is the separate decision the ACCEPT already set aside." ] }Generated by Claude Code
objectstack-fleet commentedon Sep 30, 2026
ACCEPT, round 2: PR #11175 at
a71744d4f, ready for the director seat's review. From thedomain:spec @ objectuiseat, sessionsession_012UwY3ahMixEFkfTUxMVkYm.Checked against GitHub, not the report (
5905684555):21306be7b, no rebase: 4 files, +85/−16. The PR is still a draft. Commit trailers are the model-free pair only.console.warnstring. The closing clause the three refusals share now reads 「a consumer that reads only the spellings@objectstack/spec'sFieldSchemadeclares will not see this value」. That is true for every refusal, includingid_field/title_format, whose snake reads objectui#7435 kept on purpose. No read, no fold, and noid_fieldreason logic changes.resolveActionParam,resolveGroupByLabels,deriveColumns), explicitly as named, not counted.8938-retired-dialect-measured-surface.md: append-only, as ruled.git diff --numstatagainst the merge base is11 0: one dated correction note at the end, with the original lines and frontmatter untouched (the maintainer's 「Allow the appended note (Recommended)」).id_fieldline never claims NO reader sees the value」 is paired with a control that keeps the scoped claim where it holds. The ablation restoring the old clause gave exactly those 2 red.a71744d4f: no failure at this read. The contract review starts on a fully green head.Clause-②: noholds: the edit is diagnostic text plus an appended changeset note, with no change to the accepted set. On the maintainer's 「你可以派 opus 开发,然后等项目总监契约复审,」 the PR waits as a draft for the director seat's review.needs:contract-reviewgoes on it in this act. On landing, this card closes throughFixes:id_fieldwas its last open slice.Noted, not filed:
reference-keys.measuredSurface-8938.test.tsstill says 「the retired value is inert」. Its assertions are unaffected, and the file is outside this claim's surface. It rides the next change to that file.LookupFieldreadslookup_columns,lookup_page_size,allow_createandavatar_fieldBEFORE their camel twins. In the occupied case it could serve the retired value over the producer's canonical one. This is read from source, not yet measured on the renderer, so no reach was measured and no card is filed. It is the kind of question objectui#8938's direction table (5700882992) recorded forallow_create. Carrier: none, noted here.objectstack-fleet commentedon Sep 30, 2026
Landed. PR #11175 merged through the merge queue as
1e215c40b. From thedomain:spec @ objectuiseat, sessionsession_012UwY3ahMixEFkfTUxMVkYm, 2026-09-30T07:25Z.Verified by content on
main:0ffc423b1..a71744d4f) have the samegit patch-id --stable(d94d83dfc55c2d5d3a42461543cfbcc54f00a6f3). That is the head the director's record5905930837PASSed.warnUnfoldableRetiredKeyappendsFIELD_KEY_GUIDANCE.id_field.why, read bare from@objectstack/spec/data, to theno-declared-twinline forid_field. The closing clause the three refusals share no longer claims that no reader sees the value. The pin file carries the new pins..changeset/7650-id-field-published-guidance.mdis@object-ui/corepatch. The pending8938-retired-dialect-measured-surface.mdgains one appended, dated correction note (11 additions, 0 deletions).Co-authored-by: Claudeline only. A grep of the message for a model identifier finds 0.This card closes
completed:id_fieldwas its last open slice. The PR body opensFixes #7650and the squash message carries it, but GitHub had not closed the card 5 minutes after the merge (the card readopen, andclosed_by_pull_requestswas empty). So the seat closes it here, and removespm:dispatched.Carried, not reopened here (recorded in the round-2 ACCEPT
5905704721):LookupFieldreads four snake spellings before their camel twins. That is a producer-order question for its own card if ever measured on the renderer.reference-keys.measuredSurface-8938.test.tsstill says "the retired value is inert". It rides the next change to that file.fix(core): the id_field no-declared-twin diagnostic carries the spec'…