spec(ui): a navigation entry may omit its label and inherit its target's current label at render time - #19089
Conversation
…at render time `BaseNavItemSchema.label` is `.optional()`; its describe carries the semantic — absent means the entry inherits, at render time, the CURRENT label of what it opens (the view's label when it names a labelled view, else the object's / dashboard's label); a present label renders verbatim and is never overwritten. No new member, no stored inherited flag. Claude-Session: https://claude.ai/code/session_01JbZnqu8bt6YqfJsr9vaFb3 Co-authored-by: Claude <noreply@anthropic.com>
…rections - `content/docs/references/**` and `api-surface-declarations/ui.txt` regenerated from the relaxed schema (40 nav-item label rows flip to optional; the App / Area / Dropdown label rows stay required). - `app-nav-label-optional-inherited.pin.test.ts` pins both directions: absent parses on the union and on all eight spread branches and materialises no key, present is returned byte-for-byte, unknown keys are still refused, and the three sibling label sites stay required. - minor changeset for `@objectstack/spec`. Claude-Session: https://claude.ai/code/session_01JbZnqu8bt6YqfJsr9vaFb3 Co-authored-by: Claude <noreply@anthropic.com>
📓 Docs Drift Check1 anchor(s) derived from 1 changed package(s); no hand-written page names any of them. What this run could not see
Coarse fallback — 136 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): Which tree this was computed onThis run read A worktree cut from an older # while this PR is open — GitHub drops the merge commit once it closes
git fetch origin 4d1de1d62df3b2db24bdd76a2e355b8475474ae7 && git checkout 4d1de1d62df3b2db24bdd76a2e355b8475474ae7
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 5aa3a46703136b7dcd12ba879d60a6cb01ba486e b89f48bb58426821d4054cd3d1142c4f46a79457 && git checkout -B drift-repro 5aa3a46703136b7dcd12ba879d60a6cb01ba486e && git merge --no-ff b89f48bb58426821d4054cd3d1142c4f46a79457
node scripts/docs-audit/affected-docs.mjs --json 5aa3a46703136b7dcd12ba879d60a6cb01ba486e |
|
⭐ 本席( 所判 head: 档位证据复核子代理自身 transcript: 喂给复核席的只有卡面、在先裁决、PR 本体与 ⭐ 一件本席特意提前做、而它独立复现了的事本席在派发前已把卡面验收词里的 本席接下来要做的(记录点名或隐含归本席的)
通道申报
Contract reviewServed-tier: Judged in a detached worktree at that head with its own ① Derived judgments
② Semver levelChangeset
③ Boundary flagsNothing blocks landing on contract grounds. Stated rather than flagged:
Zeros and their radii. (a) 0 stale "label required" statements about nav items in published prose: instrument = VERDICT: PASS Implemented-by: Generated by Claude Code |
Provenance —— 契约复核已归档,两个载体同笔清除本席 2026-09-18T17:48Z 清标。按章程「凡清标同笔留 provenance 评论,引记录 id 与所判 head」:
清除动作(⏱️ 2026-09-18T17:47Z 读回): 入队前自查: ⛔ 记录隐含归本席、本席照办的一条记录 ①-5 点出本 diff 动了 ⇒ 条款②闸已清,接下来只剩常规落地。 Generated by Claude Code |
Fixes #19049
Clause-②: yes
BaseNavItemSchema.labelinpackages/spec/src/ui/app.zod.tsbecomes.optional(), and the semantic is declared on the key itself: absent means the entry inherits, at render time, the CURRENT label of what it opens — the view's label when it names a view and that view is labelled, else the object's / dashboard's label. A present label renders verbatim and is never overwritten. No new member, no storedinheritedflag, nothing materialised for the absent case.This executes the maintainer's cloud#2021 ruling (「2021 可以接受有些修改刷新才生效」) as letter A on objectui#9868. The spec moves first because the console reads its navigation contract from here: until now an unnamed entry was not representable, so the promise "an unnamed entry shows its target's name" had nowhere to be declared. The rationale the relaxation had to survive — 「every real destination must have identity and text」 — still holds and is recorded in the
describe: identity is the target, text is inherited at render.The spread surface, measured on this branch
BaseNavItemSchemais declared atpackages/spec/src/ui/app.zod.ts:295and...BaseNavItemSchema.shapeis spread into 8 branch declarations, so the one-line relaxation reaches all eight at once — the card's intent, not a side effect. Measured at this branch's base176b03582e(grep -n '\.\.\.BaseNavItemSchema\.shape'); a ninthgrephit at:268is prose inside a docblock and is not a spread point:ObjectNavItemSchemaDashboardNavItemSchemaPageNavItemSchemaUrlNavItemSchemaReportNavItemSchemaActionNavItemSchemaComponentNavItemSchemaGroupNavItemSchemaThe ninth nav branch,
SeparatorNavItemSchema(:630), spreads nothing and has never carried alabel— its own guidance table already prescribesgroupfor a titled divider.What deliberately did NOT move
The same file carries 4
label: I18nLabelSchemadeclarations. Only the first is this ruling's:BaseNavItemSchema—Display proper labelNavigationAreaSchema—Area display labelAppContextSelectorSchema—Dropdown labelAppSchema—App display labelEach of the three names a container the author is creating rather than a target it could inherit from, so there is nothing for an absent label to resolve against. They are asserted in the pin test, and the generated reference page shows the split independently: of the 45
labelrows incontent/docs/references/ui/app.mdx, 45 were required before and exactly 5 are required after — the App row plus the Area and Dropdown rows, each rendered in two sections.Also untouched:
packages/spec/src/ai/solution-blueprint.zod.ts(its optionallabelbelongs to the AI blueprint proposal schema and is not a precedent here), the.objectui-shapin (53ded82bf7a494f54e344e19099dbf00854b8694, read on this branch and unchanged), and every file outside this repository.Evidence
Both acceptance directions, and the bright control. The same probe was run against the unmodified source and again after the edit, through
NavigationItemSchema:{ id, type: 'object', objectName }(no label)success=false, one issue at pathlabelsuccess=true,data = { id: 'nav_users', type: 'object', objectName: 'sys_user' }label: 'Users'success=truesuccess=true,data.label === 'Users'label: { en, zh-CN }success=truesuccess=true, the locale map returned unchangedThe bright control is that first BEFORE cell: the label-less packet really was refused on this branch's base, so the widening is measured here rather than quoted from the card. One correction to the card's acceptance wording: the refusal's issue
codeisinvalid_union, notinvalid_type—I18nLabelSchemais az.union([...]), so a missing required union-valued key reportsinvalid_unionat pathlabel. The refusal and its path are exactly as the card describes; only the code name differs.Dark control. A packet carrying a bogus key (
labl) is refused before AND after, withunrecognized_keysand the branch's own "did you mean" suggestion intact — the relaxation is a widening of one key, not an opening of the shape. On the sibling sites,NavigationAreaSchema,AppContextSelectorSchemaandAppSchemaeach still refuse a label-less packet after the change, and each parses the same packet once a label is added (so the leg is a verdict aboutlabel, not about the rest of the packet).Branch propagation. All eight spread branches parse a label-less, otherwise-valid payload on their own schema after the change and refused one before; two different branches (
dashboard,url) are additionally exercised through the union, plusgroupwith its union-levelchildren.Regeneration.
check:generatedproved exactly two artifacts stale and both were regenerated with the named generators (gen:api-surface-declarations,gen:docs);authorable-surface/andjson-schema.manifest/are untouched, because the key set does not move — only its requiredness. Emitted JSON Schema forui/ObjectNavItem, read from the generator with the pre-edit source on disk and again with the edit (the source was restored fromHEADafterwards and the restored blob hash compared againstHEAD's):required: ["id","label","type","objectName"],label.description: "Display proper label"required: ["id","type","objectName"], andlabel.descriptioncarries the inheritance semantic in fullThe reference page moved with it, e.g. under
ActionNavItem:| **label** | ... | ✅ | Display proper label || **label** | ... | optional | Display proper label. Optional: absent means the entry inherits the CURRENT label of what it opens at render time ... |Suites and gates.
@objectstack/spec— 493 test files, 14500 tests, all passing;typecheckgreen (tsc --noEmit,check:scripts-typecheck,check:test-typecheck). The derived gate roster for this diff (scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack) is 106 families: 104 ran green, and 2 recorded exit 3 — the code those gates use for "nothing was measured" — because they read the built output of the whole monorepo, which this worktree does not have:check:dual-build-cjs-loads(87 packages with nodist) andcheck:type-check-debt(30 unbuilt workspace dependencies of the ledgered packages). Neither is a finding and neither is a pass; CI builds the closure and runs both. Repo-wide lint is green as a whole run, not a narrowing:eslint . --no-inline-config --format jsonselected 6875 files and reported 0 errors / 0 warnings atb89f48bb(the final commit).Acceptance notes
BaseNavItemSchemasays the shape is "spread into the nine branches below" and that its keys are "ALREADY gated at all nine doors". The spread count is 8:separatoris the ninth nav branch and spreads nothing, which the same docblock's sibling comment onnavItemSurfacestates correctly. Pre-existing prose, untouched here, and noted rather than filed — no behaviour reads it.GroupNavItemSchemaparsed on its own accepts an item with nochildren;childrenis required only on the union member thatNavigationItemSchemabuilds from it. Unchanged by this PR and pinned as observed, not as a defect.What this PR does not do
Downstream work stays where the card sequenced it: objectui#9868 relaxes its own
packages/typesvalidator, teaches the nav renderer to read an absent label from its target, and stops writinglabel || pageNamefor an unnamed entry; then cloud#2021 stops materialising an inherited label inapply_blueprint. Neither is touched here, and objectui#9868 remains open.Generated by Claude Code