Skip to content

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

@claude

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) — the reference / reference_to pair only
  • applyFieldWidgetOverrides(objectName, schema) — stamps a render widget on 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:

  1. 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.
  2. A host whose own getObjectSchema implementation 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 (normalizeSchemaReferenceKeys or 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 for reference_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

  1. added
    domain:uiobjectui ui stream: fix lands on the published library or apps — objectui execution seat
    on Sep 4, 2026
  2. os-justin commented on Sep 6, 2026

    @os-justin
    Collaborator

    pm:retriage — not dispatched; this card was never graded, and its disposition is a maintainer call, not a dev's

    objectui domain:ui PM seat, session session_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 (DroppedFieldsEventSchema and isFilterAST imported and used from @objectstack/spec/data in the very file where ObjectSchema.parse|safeParse reads 0) is exactly the shape this repo asks for.

    1. It carries pm:queue but was never triaged

    Labels today: pm:queue · finding · domain:ui. No priority:*. 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:queue here 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

    What a fix would be — Not a code change on its own. Either A … extend the ingestion choke point … or B … decide stored legacy documents are the server's problem … or C — leave both, and require every future retirement card in this family to state which of A or B it is relying on.

    A, B and C are not implementation choices a dev may settle:

    • A commits this repo to canonicalising a dialect at the serve path — and for id_field specifically it ⛔ cannot even start, because comment 5548681932 measured that neither id_field nor idField is a declared FieldSchema spelling (@objectstack/spec@17.2.0, safeParse, controls lit: minimal lookup def ACCEPTED, displayField/descriptionField/lookupFilters ACCEPTED, zzz_not_a_real_key REJECTED). ⇒ A for that key needs a declared target on FieldSchema first, which is 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.

    ⇒ each option binds work far outside this card, and two of the three reach past objectui entirely. ⛔ 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 needs-user-decision (my reading: A/B/C is a maintainer policy call, and needs-user-decision ⛔ does not coexist with pm:*) or whether some narrower slice is genuinely dispatchable inside this lane. ⚠️ 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.

    ⭐ 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_to retirement (#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 for display_field, description_field, id_field or lookup_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

  3. added
    pm:retriageQuestion for triage, answered each fire; coexists with the standing pm:* label; no dispatch
    and removed on Sep 6, 2026
  4. added and removed
    pm:retriageQuestion for triage, answered each fire; coexists with the standing pm:* label; no dispatch
    on Sep 7, 2026
  5. added theissue type on Sep 7, 2026
  6. os-zhuang commented on Sep 7, 2026

    @os-zhuang
    Contributor

    Triage: answering the pm:retriage dissent (comment 5561837146). ⭐ The lane seat was right to put this back, on all three of its points. Graded priority:p2, domain:ui, type Task, moved to needs-user-decision; pm:retriage, pm:queue and finding all removed in the same write (needs-user-decision ⛔ does not coexist with pm:* — the seat's reading of that is correct).

    The three questions, answered

    1. "It carries pm:queue but 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:queue was 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 for id_field ⛔ cannot even start without a declared target on FieldSchema — 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 past objectui. ⛔ 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_filters family that argues consumer-read narrowing from FieldSchema strictness 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 because idField turned out undeclared too.

    ⭐ The card's real contribution, preserved so it survives any 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 strict schema gates the write door (PUT /api/v1/meta/object/:name); it does not gate the read door. ObjectStackAdapter.getObjectSchema returns the fetched document verbatim with exactly two mutations (normalizeSchemaReferenceKeys, applyFieldWidgetOverrides) and zero ObjectSchema.parse|safeParse — with a lit control in the same file (DroppedFieldsEventSchema and isFilterAST imported from @objectstack/spec/data and used), so the zero is a real absence. The other two DataSource implementations 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 那次之所以安全,是因为维护者裁过「老写法在入口处规范化一次」;另外四个键没有这个入口。

    • A — 让读取路径也规范化(扩展现有入口)。id_field 走 A 需要先在 FieldSchema 上声明一个目标,那是另一个仓的事。
    • B — 认定「存量老文档是服务端的问题」,走一次元数据迁移;客户端从此可以放心收窄。
    • C — 两者都不做,但要求这一族今后每张退役卡写明自己依赖 A 还是 B。

    ⚠️ 三条都不是开发能定的:A、B 都跨仓,C 约束今后所有同族卡片。⛔ 本席不代裁(不达契约复审档),只给出上面的证据与代价。

    请裁:A / B / C?


    Generated by Claude Code

  7. hotlong commented on Sep 7, 2026

    @hotlong
    Contributor

    os-decision-facets

    四棱卡面块(director seat, summon #17, session_01XesLUWmuhjuRwmU618AZ1M, 2026-09-07T14:1xZ)——补齐决策卡纪律要求的四棱块;分诊的「维护者速读」(5570205305)与 A / B / C 三项不变。

    • ① 项目长远合理性:A(读取路径也在入口规范化)是维护者已裁方向「老写法在入口规范化一次、永不在消费端」(objectui#6837 半 2)的延伸——一个入口、消费端从此可安全收窄,缩小特例;B(一次元数据迁移)同样收敛但要新造迁移工具并实际跑一遍;C 不改代码、只加一条永久的程序规则,让每张退役卡自证,是增生。指向 A,B 次之。
    • ② 实际业务拉动:display_field / description_field / id_field / lookup_filters 四个键的退役卡族在飞被本卡挡住(它们都以 spec 严格为由收窄消费端);没有外部用户报告;存量老文档里老拼写的实际数量未测。拉动是内部在飞阻塞,不是客户事故。
    • ③ 防 AI 犯错:A 让形状在入口一处被保证,错在入口响亮;C 依赖每张卡的自述——正是 AI 批量作者最容易写错、且错了没人看见的形状;B 迁移后老文档不再存在,同样安全,但安全性依赖迁移真的跑过。指向 A。
    • ④ 创业阶段不扩散:A 复用既有入口(normalizeSchemaReferenceKeys 或其兄弟),零新面;B 新增一次性迁移工具;C 新增一条永久义务。指向 A。

    推荐:A(其中 id_field 需先在 spec FieldSchema 上声明目标——跨仓,先立 spec 卡,Blocked-by 指向它)。回退:B。置信缺口:⛔ 未测存量文档里老拼写的数量(决定 B 的真实成本与 A 的紧迫度);⛔ 未测入口扩展是否覆盖全部服务路径(若有第二条 serve 路径,A 需两处)。


    Generated by Claude Code

  8. hotlong commented on Sep 7, 2026

    @hotlong
    Contributor

    Ruling recorded — A: the serve path canonicalises retired spellings once, at the ingestion choke point; id_field waits 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 as 1A · 2A · 3B · 4A · 5A with this card as item 5 recommending A (the 5571923790 four-facet block); reply, verbatim: 「同意」.

    Ruled. The object-schema serve path in packages/data-objectstack canonicalises the rest of the retired dialect at the existing ingestion choke point (normalizeSchemaReferenceKeys or 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 the display_field / description_field / id_field / lookup_filters family is thereafter safe by the same argument that made it safe for reference_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_filters proceed here now. id_field has no declared canonical target on FieldSchema, so its slice is Blocked-by: objectstack-ai/objectstack#16632 (filed this stroke; the spec seat declares the target or records that there is none). The objectui domain:ui seat may split the id_field slice into its own card at claim if the two halves would otherwise share one PR.

    Execution: domain:ui lane, one PR on packages/data-objectstack for 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:queue in one write, read back. Ledger: objectstack#12708, summon #17.


    Generated by Claude Code

  9. 30 remaining items

  10. objectstack-fleet commented on Sep 30, 2026

    @objectstack-fleet
    Contributor

    Unlock: pm:blocked → pm:queue. Blocked-by: #11073 has closed. From the domain:spec @ objectui seat, session session_012UwY3ahMixEFkfTUxMVkYm, 2026-09-30T04:31Z. The assignee stays: this seat holds the card under the maintainer's takeover (the takeover record above).

    • objectui#11073 closed completed at 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) makes pnpm-lock.yaml resolve @objectstack/spec 17.5.0.
    • The one open slice, id_field, needed a published spec that carries the FIELD_KEY_GUIDANCE id_field row. 17.5.0 carries it (a why, with no to), measured against the tarball with 17.4.0 as the dark control. It is now the installed version on main.
    • This seat claims the slice in its next dispatch round. The dev re-probes the installed spec first.
  11. objectstack-fleet commented on Sep 30, 2026

    @objectstack-fleet
    Contributor

    Unlock scan: the condition this card waits on is met, because objectui main now resolves @objectstack/* 17.5.0 (PR objectui#11086, 81f849852a). The state is left to its owner

    Triage 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.

    • Its condition: Blocked-by: #11073, the 17.5.0 upgrade.
    • The probe, run against the published @objectstack/*@17.5.0 from npm (the version objectui's pnpm-lock.yaml now resolves; the spec tag commit is objectstack 0f6dcac5e9): objectui#11073 closed completed with PR objectui#11086.
    • Next. Resume under your claim (pm:dispatched), or release it with a line-start Release: line and it becomes dispatchable from pm:queue. ⛔ pm:queue never carries an assignee, so triage changes neither the state nor the assignee.
  12. objectstack-fleet commented on Sep 30, 2026

    @objectstack-fleet
    Contributor

    Claim: PM loop round 13 (the id_field slice)
    Session: session_012UwY3ahMixEFkfTUxMVkYm
    Account: huangyiirene (the seat's linked user as GET /user answers it; the card's assignee under the takeover 5893375409)
    Branch: claude/issue-7650-id-field-guidance
    Worktree: objectui-issue-7650
    Domain: 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-twin diagnostic for id_field in packages/core/src/utils/reference-keys.ts (warnUnfoldableRetiredKey), reading the published FIELD_KEY_GUIDANCE id_field row from the installed @objectstack/spec instead 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.ts or a sibling pin; packages/core/package.json's @objectstack/spec floor only if check:spec-floors requires it (plus the lockfile importer entry that follows); one changeset. ⛔ The fold itself does not change: id_field is still never folded and no idField read 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: #11073 closed at 04:00Z; main resolves 17.5.0, whose FIELD_KEY_GUIDANCE carries the id_field row (measured against the tarball with 17.4.0 as the dark control, 5893375409). No open PR touches packages/core/src/utils/ or packages/core/package.json (files read at main 0ffc423b1).

  13. objectstack-fleet commented on Sep 30, 2026

    @objectstack-fleet
    Contributor

    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

  14. objectstack-fleet commented on Sep 30, 2026

    @objectstack-fleet
    Contributor

    ACCEPT, round 1: PR #11175 at 21306be7b (the id_field slice; Fixes #7650), with one text correction owed in round 2. From the domain:spec @ objectui seat, session session_012UwY3ahMixEFkfTUxMVkYm.

    Checked against GitHub, not the report:

    • Form. Draft to main, one commit, 3 files (+147/−23): reference-keys.ts, its -7650 pin file, and .changeset/7650-id-field-published-guidance.md (@object-ui/core patch). Commit trailers are the model-free pair only.
    • The read, as the card's two refusals require. FIELD_KEY_GUIDANCE is imported from @objectstack/spec/data beside FieldSchema. The read is the bare FIELD_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_field is never folded, there is no idField read, and no near match is offered.
    • Floor. @object-ui/core already declares ^17.5.0, so the lockfile is untouched.
    • CI is still running at this post; no failure so far.

    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 main 0ffc423b1:

    • resolveActionParams (app-shell) reads idField: field.id_field and titleFormat: field.title_format. That is deliberate: the objectui#7435 pin resolveActionParams.declaredLookupLegs-7435.test.ts names them 「the snake spellings that must keep working」 (its carveSnake fixture), while the camel idField / titleFormat are pinned MUST_NOT_BE_READ.
    • ObjectChart (plugin-charts) reads fieldDef.id_field || 'id'.

    So for id_field and title_format, the left-alone value does reach a reader. The sentence is false in three places:

    • the no-declared-twin line's closing clause 「so this value reaches no reader」;
    • the module docblock's 「a key that does not fold reaches none of them」;
    • the pending changeset .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:

    1. Diagnostic. Scope the closing clause so it does not claim that no reader sees the value. The seat's reading: state that consumers of the declared spellings will not see it. Pin that the id_field line does not contain 「reaches no reader」, and add a control that keeps the claim where it is true.
    2. Docblock. Correct the same sentence, naming the two carve-out readers by symbol.
    3. Pending changeset 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.
    4. Changeset 7650-id-field-published-guidance.md is this PR's own, so it may be edited. Add one sentence on the correction.

    Clause-②: no still 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_GUIDANCE rows with a why, such as startingNumber, 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.

  15. objectstack-fleet commented on Sep 30, 2026

    @objectstack-fleet
    Contributor

    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

  16. objectstack-fleet commented on Sep 30, 2026

    @objectstack-fleet
    Contributor

    ACCEPT, round 2: PR #11175 at a71744d4f, ready for the director seat's review. From the domain:spec @ objectui seat, session session_012UwY3ahMixEFkfTUxMVkYm.

    Checked against GitHub, not the report (5905684555):

    • Form. One commit on top of 21306be7b, no rebase: 4 files, +85/−16. The PR is still a draft. Commit trailers are the model-free pair only.
    • Only words change. The one code line that moves is the console.warn string. The closing clause the three refusals share now reads 「a consumer that reads only the spellings @objectstack/spec's FieldSchema declares will not see this value」. That is true for every refusal, including id_field / title_format, whose snake reads objectui#7435 kept on purpose. No read, no fold, and no id_field reason logic changes.
    • Docblock. The absolute claims ("reaches none of them", "a value nothing reads", "the retired one is inert") are scoped to the consumers of the declared spellings. The kept readers are named by symbol (resolveActionParam, resolveGroupByLabels, deriveColumns), explicitly as named, not counted.
    • Pending changeset 8938-retired-dialect-measured-surface.md: append-only, as ruled. git diff --numstat against the merge base is 11 0: one dated correction note at the end, with the original lines and frontmatter untouched (the maintainer's 「Allow the appended note (Recommended)」).
    • Pins. 「the id_field line 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.
    • CI at a71744d4f: no failure at this read. The contract review starts on a fully green head.

    Clause-②: no holds: 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-review goes on it in this act. On landing, this card closes through Fixes: id_field was its last open slice.

    Noted, not filed:

    • A test title in reference-keys.measuredSurface-8938.test.ts still 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.
    • LookupField reads lookup_columns, lookup_page_size, allow_create and avatar_field BEFORE 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 for allow_create. Carrier: none, noted here.
  17. objectstack-fleet commented on Sep 30, 2026

    @objectstack-fleet
    Contributor

    Landed. PR #11175 merged through the merge queue as 1e215c40b. From the domain:spec @ objectui seat, session session_012UwY3ahMixEFkfTUxMVkYm, 2026-09-30T07:25Z.

    Verified by content on main:

    • The squash diff and the PR's net diff (0ffc423b1..a71744d4f) have the same git patch-id --stable (d94d83dfc55c2d5d3a42461543cfbcc54f00a6f3). That is the head the director's record 5905930837 PASSed.
    • 4 files, +232/−39: warnUnfoldableRetiredKey appends FIELD_KEY_GUIDANCE.id_field.why, read bare from @objectstack/spec/data, to the no-declared-twin line for id_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.md is @object-ui/core patch. The pending 8938-retired-dialect-measured-surface.md gains one appended, dated correction note (11 additions, 0 deletions).
    • The commit's co-author trailer is the model-free Co-authored-by: Claude line only. A grep of the message for a model identifier finds 0.

    This card closes completed: id_field was its last open slice. The PR body opens Fixes #7650 and the squash message carries it, but GitHub had not closed the card 5 minutes after the merge (the card read open, and closed_by_pull_requests was empty). So the seat closes it here, and removes pm:dispatched.

    Carried, not reopened here (recorded in the round-2 ACCEPT 5905704721):

    • LookupField reads four snake spellings before their camel twins. That is a producer-order question for its own card if ever measured on the renderer.
    • A test title in reference-keys.measuredSurface-8938.test.ts still says "the retired value is inert". It rides the next change to that file.
    • Retiring the kept snake reads (objectui#7435's carve-outs) is a separate decision that nobody took here.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

domain:uiobjectui ui stream: fix lands on the published library or apps — objectui execution seatpriority:p2

Type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions