Repository navigation
feat(spec,lint,cli): a page:tabs item label gains the key pages.PAGE.components.TABS.items.VALUE.label - #25005
Conversation
…n key A pack key pages.PAGE.components.TABS.items.VALUE.label is refused by the schema, never applied by translatePage, not judged by objectstack validate and not offered by the extractor. Tests first; the fix follows. Co-authored-by: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01D6F9axfoPzR7t8G1J7Ytzz
Co-authored-by: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01D6F9axfoPzR7t8G1J7Ytzz
…nents.TABS.items.VALUE.label The tabs component's own id, then the item's value (the stable ?tab= token). translatePage overlays the item label; walkAddressedPageComponents keeps a visitor's edit to a panel's own keys; objectstack validate judges the key; os i18n extract offers the slot and coverage demands it. Regenerated the api-surface, export-origins and strictness-ledger counts. Co-authored-by: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01D6F9axfoPzR7t8G1J7Ytzz
…ge-tabs-item-label-key
📓 Docs Drift CheckThis PR changes 3 package(s): ⛔ 1 release-owned page(s) name something this change touched. These are read-only:
What this run could not see
Coarse fallback — 150 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 0d27c0cd444658440ed5b67317ae6be68aefd048 && git checkout 0d27c0cd444658440ed5b67317ae6be68aefd048
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 62bc1293080968f0ec8c3d31dc1f9ed8da4077df f18a2f55dd050513dc8fe2dfdfab4217b5f48dff && git checkout -B drift-repro 62bc1293080968f0ec8c3d31dc1f9ed8da4077df && git merge --no-ff f18a2f55dd050513dc8fe2dfdfab4217b5f48dff
node scripts/docs-audit/affected-docs.mjs --json 62bc1293080968f0ec8c3d31dc1f9ed8da4077df
|
…ge-tabs-item-label-key
… the docs table names the tab-item key The alias put a `tabs` key beside `emptyState` in one object literal, which the list-view tabs retirement's tree-scoped absence pin reads as a list-view payload carrying `tabs` (translation.zod.ts:1191). The alias was not part of the card's ask, so it goes rather than widening the pin's exclusions. content/docs/ui/translations.mdx gains the pages.NAME.components.TABS.items.VALUE.label row. Co-authored-by: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01D6F9axfoPzR7t8G1J7Ytzz
Contract reviewServed-tier: Read against card #24968 (body, claim 6300704588, correction 6300738468, dev reports 6304394234 and 6307292111), the standing ruling on #6015 (comment 6292005854, rule 甲), the PR body and file list, and the three-dot diff Delta from the previously reviewed head ① Derived judgments
② Semver level
③ Boundary flags
Implemented-by: VERDICT: PASS Seat re-read of the check-runs at landing: all 49 runs on |
Closes #24968
Clause-②: yes (widening)
What changes
A
page:tabsitem'slabelhad no translation key.TranslationBundlepages.PAGE.componentsis keyed by component id. A tab item is not a component and carries no id. SpectranslatePagedescendeditems[].childrenbut never readitems[].label. HotCRM measured this onsales_home_page: three English tabs under a translated header, in zh-CN, ja-JP and es-ES.packages/spec/src/system/translation.zod.ts):pages.PAGE.components.TABS_ID.items.VALUE.labelis a declared key. The component entry gainsitems, a record keyed by the tab item'svalue. Each entry is closed (strictObject, surface "this page tab item translation").title,nameandtextare renames ontolabel. (Thetabsalias ontoitemswas dropped inf18a2f55dd.)packages/spec/src/system/i18n-resolver.ts):translatePageoverlays each item'slabelfrom the key. It resolves across the locale chain item by item, under the sameaddressedcollision arbitration as the component copy. Onlypage:tabscomponents are read. Two new exports are shared by the resolver, the extractor and the lint check, so the three cannot drift:PAGE_TABS_COMPONENT_TYPEandpageTabItemKey(item).walkAddressedPageComponents): the walk used to rebuild a panel list from the authored node, so a visitor's edit to a panel's own key (itslabel) was overwritten. A panel's own keys now come from the visitor's return when the visitor keeps the list's authored shape (same length, an object at every panel position). Panelchildrenare still descended from the authored document, so the walk still owns the descent. A visitor that changes the shape gets the authored list back. The docblocks state this, and a new walk test pins it.objectstack validate(packages/lint/src/validate-translation-references.ts): the check builds a per-page universe with spec'swalkAddressedPageComponents, the same walktranslatePageruns. Acomponents.ID.items.VALUEkey resolves over a declaredpage:tabsitemvalue. Otherwise it is refused astranslation-target-unknown, in the existing one-line verdict style:tab-2included): "...whichpage:tabs"home_tabs" on page "sales_home_page" gives no item as itsvalue, so the tab keeps its source-locale label.", followed by "Did you mean";page:accordion: "...whose panels have no stable key, so nothing resolves them.", with a fix line pointing at the inline locale map;record:related_list, not apage:tabs...";os i18n extract/ coverage (packages/cli/src/utils/i18n-extract.ts):emitPageComponentCopywrites a slot for every valued item of an addressedpage:tabs. A reader exists (translatePage), so the slot is demanded, which differs from feat(spec,lint): flows translation face gains approvals.NODE.label for an approval node's step label #24943. An inline locale map on the label is recorded as authored, through the samepushEntrypath as the component keys.Why this spelling (shape (a) over (b))
value, under the tabs component's id.PageTabsProps.items[]already declares the identifier the key needs.valueis the stable?tab=token that the renderer reads and restores, so it is unique within the strip. The InlineI18nLabelmaps are invisible toos i18n extractandcheck:i18n-coverage— should the extractor and the coverage gate see them? (objectstack#14412 ruling A follow-up) #14749 ruling (Q3 = C3, quoted in the extractor's header) recorded the direction "identity first:component.id/section.name/tabs item.value". Adding anidto tab items (shape (b)) would give one tab two identifiers. It would also makecomponents.IDaddress something that is not a component, and it would need a new authorable key on a published schema. Shape (a) adds no authorable page key and reuses the grammar the face already has, the same waydashboards.D.widgets.IDandflows.F.screens.NODEkey a child by the identifier its consumer already holds.valuehas no key. Its renderer token istab-INDEX. The InlineI18nLabelmaps are invisible toos i18n extractandcheck:i18n-coverage— should the extractor and the coverage gate see them? (objectstack#14412 ruling A follow-up) #14749 ruling forbids array-index keys, because reordering two tabs would swap their translations with every check still green. The lint verdict names the remedy: give the item avalue.translatePagewrites only where the bundle has an entry. An item the bundle does not key keeps its map, and a bundle entry wins where both exist. This is the rule the component copy already follows.page:accordionitems have no author-settable identity (the renderer derivespanel-INDEX, andvalueis refused on them with guidance), so their labels stay an inline-map surface.Tests
757aadb573adds the tests before the fix. At that commit, on a rebuilt spec dist, the spec suite had 9 failing cases (4 schema, 4 resolver, 1 walk), the lint suite had 7 (5 refusal cases plus the two one-line arm coverage checks), and the new CLI file did not load, because the closure was not built yet.BOUNDARYcase ini18n-resolver.test.ts("a tab panel label has no bundle key") said "If a key is added, this pin is the one to flip". It is flipped: its inline-map half is kept as a CONTROL, and the newtranslatePage — page:tabs item labelsblock pins the key.components.IDkeys; the tabs component's ownlabelkey composes with its item keys; the sameitemskey on apage:accordiontranslates nothing; an item without avalueis untouched even iftab-2is keyed; the locale-chain fallback works key by key; the component keys of the page validate clean beside an item key; a page the stack does not declare is not judged.c8a248af73, local baseorigin/main7adfaec2a1):pnpm --filter @objectstack/spec test: 703 files, 21377 passed, 1 todo.pnpm --filter @objectstack/lint test: 152 files, 7033 passed.pnpm --filter @objectstack/cli exec vitest run --project unit: 316 files, 4629 passed, 29 skipped. Two published-subpath pins first refused becausepackages/cliitself was not built. That is a prerequisite refusal, not a measurement. After building the CLI they passed, with the new file: 3 files, 37 passed. Theintegrationtier is declared to CI, since the diff touches no spawn entry.typecheckfor spec, lint and cli: exit 0 (each runstsc --noEmitandcheck:test-typecheck).pnpm --filter @objectstack/spec check:generated --fixregeneratedapi-surface/system.jsonandexport-origins/system.json(the two new exports) and the strictness-ledger counts (system.md). The reference doctranslation.mdxis unchanged, because itscomponentscell is truncated with an ellipsis beforeitems.scripts/ablation-replace.mjs: the anchor hit once, and the restore was proven byblob == HEADwith an emptygit diff HEAD. Each suite readssrc.translatePageTabItemscall removed: spec resolver suite4 failed | 363 passed.5 failed | 362 passed(the 4 above plus the walk pin).4 failed | 190 passed(the two value refusals and the two one-line arm-coverage checks).4 failed | 4 passed.dispatch-gates --commands, reconciled with--ranwith exit codes recorded): 94 derived, 93 run, all exit 0.check:i18n,check:i18n-coverageandcheck:i18n-walk-parityfirst refused with exit 3 (the CLI was not built). After the build they were re-run and exited 0:check:i18nreports 10 packages in sync,check:i18n-coveragereports 14 configs with none new, andcheck:i18n-walk-parityreports 11 declared groups, 9 walked and 2 exempted. NOT MEASURED:check:dual-build-cjs-loads, reason: PREREQUISITE NOT MET (exit 3), because packages this diff does not touch have nodistwithout a whole-repopnpm build. Declared to CI.check:nul-bytes: exit 0.--no-inline-config --format json, atc8a248af73): 8 files, 0 errors, 0 warnings. The config declares no type-aware linting (eslint.config.mjs: noparserOptions.project, no typed rules), so this diff cannot change the verdict on any untouched file.Review tier
packages/specis a published schema, so this PR owes aCONTRACT_REVIEW_TIERreview of record before it enters the queue (pm-dispatchreferences/contract-review.md). It touches no governed surface (.claude/**,docs/adr/**,AGENTS.md, workflows,skills/**), so no Tier S or Tier H landing rule applies.Liveness
packages/spec/liveness/translation.json'spagesrow isliveand not drilled, so its blanket verdict covers the new member. The reader istranslatePage, which the REST metadata read applies throughtranslateMetadataDocument. objectui'spage:tabsrenders a string label as is.Acceptance notes
objectstack validatestill judges no otherpages.PAGEmember. An unknown page name, an unknown component id carrying only copy keys, and an unknown copy target are not checked by this rule today. This PR judges only theitemsmember it adds, and skips pages the stack does not declare.page:accordionpanel labels have no bundle key (no stable identity). This is deliberate and recorded in the schema docblock.#24947(this seat's open PR) also touchesi18n-extract.ts, in a different function (bulk-confirm template slots). No textual overlap is expected.wip:commit (818b89e351) between the repro and the fix commit. This PR is squash-landed, so it does not reachmain. I did not rewrite the history.Generated by Claude Code