Repository navigation
fix(cli): os lint and os i18n check expect a contributed navigation item's label under its target app - #25171
Conversation
…et app collectExpectedEntries walked apps[].navigation only, so a key for an item another package contributes through manifest.navigationContributions was never expected and deleting its translation left os lint and os i18n check green, though translation-target-unknown accepts that key. Claude-Session: https://claude.ai/code/session_01AFrKwndmrCLtF5V9hNtrw3 Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AFrKwndmrCLtF5V9hNtrw3 Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AFrKwndmrCLtF5V9hNtrw3 Co-authored-by: Claude <noreply@anthropic.com>
…es the required app label Claude-Session: https://claude.ai/code/session_01AFrKwndmrCLtF5V9hNtrw3 Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AFrKwndmrCLtF5V9hNtrw3 Co-authored-by: Claude <noreply@anthropic.com>
…ntributed-nav-i18n-coverage Claude-Session: https://claude.ai/code/session_01AFrKwndmrCLtF5V9hNtrw3 Co-authored-by: Claude <noreply@anthropic.com>
📓 Docs Drift Check2 anchor(s) derived from 1 changed package(s); no hand-written page names any of them, so this run has nothing to list — not a clean bill of health. This check sees only pages that NAME a derived anchor: one that documents this change in prose, or enumerates it in an authoring dialect, names none and stays invisible to it on every run. What this run could not see
Coarse fallback — 30 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 527b17633daa5ef83479082b8618923ff8b25a3b && git checkout 527b17633daa5ef83479082b8618923ff8b25a3b
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin e3aa111ad979cf019dda166ca0c6cf5a06946631 4f263166d7b72eef7b3c753593542f1483b94064 && git checkout -B drift-repro e3aa111ad979cf019dda166ca0c6cf5a06946631 && git merge --no-ff 4f263166d7b72eef7b3c753593542f1483b94064
node scripts/docs-audit/affected-docs.mjs --json e3aa111ad979cf019dda166ca0c6cf5a06946631 |
席位核对 · ACCEPT (landing on green)
Generated by Claude Code |
Fixes #24993
Clause-②: no
Metadata-protocol: none
What changes
collectExpectedEntries(packages/cli/src/utils/i18n-extract.ts) is the one walk behindos lint'si18n/missing-*family,os i18n checkandos i18n extract. Its navigation arm readapps[].navigationonly. An item a package contributes into an app throughmanifest.navigationContributions(ADR-0029 D7) never enters that array: the runtime folds it in on every read (SchemaRegistry.applyNavContributions), andtranslateAppresolves its label fromapps.TARGET.navigation.ID.label.translation-target-unknownaccepts that key since #18203. So the key was accepted but never expected, and deleting its translation left both doors green.navigationContributions[]entry is walked by the samewalkNavigation, keyed under its targetapp. The seed is the item's ownlabel. A contributed group's children follow the rule an own group's children follow. An entry with no stringappis skipped.navigationContributionsOfreads the two carriers the target rule reads (contributedNavItemsByAppinpackages/lint/src/validate-translation-references.ts): the stack's ownmanifest.navigationContributions, andpackages[].manifest.navigationContributions(the ADR-0130 D4 entry body). Both are read unconditionally, as the rule reads them. On apreservecomposition whose last input is the contributor, both carriers hold the same items, and the walker's existingdedupeByPathkeeps one entry per key (first emission wins, so an id an app also declares keeps the app's own entry).i18n-coverage.tsis not touched: its surface phrase already reads "navigation items"..changeset/24993-contributed-nav-i18n-coverage.md:@objectstack/clipatch.The mechanism hypotheses, measured (BASE
6b44efa7b)navigationContributions[].items[]id under its target app'snavigationkeys. The target rule's resolution iscontributedNavItemsByApp, which is module-private to@objectstack/lint(its index exportsvalidateTranslationReferences, not the reader). Importing it would need a new public export from@objectstack/lint, outside this card's file surface. The other in-repo readers each need a new import line ini18n-extract.ts's import block, which the dispatch reserved for PR feat(spec,lint): a bulk action's confirmText is a template slot over count and its params, judged at validate, compile and the publish gate #24947:collectNavGroupInputs(cli) reads either/or carriers, not both;stackPackageBodies(@objectstack/core) covers only the package carrier. So the carrier read is written here, in about ten lines that mirror the rule's two carriers. It is held to the rule by a parity pin, described below.label(sourceValue/inline=Cases). The key sits under the app it is contributed TO, read from every package body of the composed artifact (the union stack both doors hand the walker since cli:os linttranslation coverage reads only a multi-package artifacts top level — everyi18n/missing-*` goes silent once an app ships as packages (missed reader of the ADR-0130 2026-09-22 addendum; family of #22189) #22238).grp_knowledge) and its child (nav_article) are both expected, by the samewalkNavigationthe own tree uses. Deleting the child's key is reported.examples/app-multi-packagecontributesnav_ordersintomulti_crm, but it declares noi18nblock and ships no bundle. Only the default locale is active, and its inline labels satisfy it. Itscheck:i18n-coveragecount stays at its baselined0. No example declares a translated locale and also contributes navigation, so there was nothing to translate and no baseline moved.Both doors, measured on the card's probe
The fixture has the card's shape: a service module contributes
nav_caseinto the app'sgrp_servicegroup, and adds a group of its own (grp_knowledgewith childnav_article). It is composed module-first withmanifest: 'preserve', so the contributions live only in the module's package body. The fixture declaresenandzh-CN, and every other key is translated. Each row deletes one zh-CN navigation key. Run with the source CLI (bin/run-dev.js) from a scratch directory, atd4e0e405a. The ablated runs used the same tree with the arm's loop emptied, as in the ablation below.os lint --strict --json, fixedos i18n check --json --strict, fixedfailing: 0nav_case(contributed)i18n/missing-navigationtranslations.zh-CN.apps.cnv_desk.navigation.nav_case.labelerror zh-CN apps.cnv_desk.navigation.nav_case.labelnav_article(child of a contributed group)…nav_article.label…nav_article.labelnav_accounts(control: the app's own)…nav_accounts.label…nav_accounts.labelThe ablated column reproduces the card's finding: the contributed deletions exit 0, and the own-item control exits 1.
Tests
New
packages/cli/test/i18n-contributed-nav-coverage.test.ts(unit tier: no spawn, no kernel). It has 13 cases over the realdefineStack+composeStacks(…, { manifest: 'preserve' })+authoringRuleUnionStackpath that both doors take:translation-target-unknown. A zh-CN bundle carrying exactly the navigation keys the walker expects draws notranslation-target-unknownfinding, on the composed stack and on a contribute-only single stack. Each case first asserts that the bundle really carries the contributed ids, so the pin cannot pass empty. The control: an id nothing declares or contributes is still refused.Ablation. The fix was committed first.
node scripts/ablation-replace.mjsreplaced the arm'sfor (const contribution of navigationContributionsOf(config)) {with a loop over[]. The tool reported the anchor going 1 → 0 and the blobc23f3ab341d6→055daea8a714. The test file and the subject resolve fromsrc/(relative import;run-dev.jsrunssrc/through tsx), so nodist/leg applies.c23f3ab341d6), andgit diff HEADis empty (the tool's own verdict).Local verification
Final head
4f263166d: this branch plus a merge ofmainat06900c9c2, which brought one objectui pin bump and touches no file of this diff. Every lock run below printed its own VERDICT line, which is quoted.pnpm --workspace-concurrency=2 --filter '@objectstack/cli^...' build→VERDICT command-exit 0.pnpm --filter @objectstack/cli typecheck(tsc --noEmitpluscheck:test-typecheck) →VERDICT command-exit 0, test-layer debt held at 3 files / 28 errors / 6 pinned signatures.tsc -p tsconfig.test.json --listFileslists the new test file once, so it is type-checked.pnpm --filter @objectstack/cli exec vitest run --project unit --maxWorkers=2at0c16d8246(before the merge) → Test Files 323 passed (323) · Tests 4769 passed (4769),VERDICT command-exit 0. The integration tier is left to CI: this diff touches no spawn entry and no integration-tier file.4f263166d. The new file, plus the three cli tests that read.objectui-sha: 4 files, 44 tests passed.node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstackderived 68 commands. All 68 were run at4f263166d, with each exit code captured before any pipe.--ran:68 derived famil(ies) accounted for — 66 run, 2 NOT-MEASURED, 0 unrun. Readings that bear on this change:check:i18n-coverage:OK (14 config(s), 621 baselined untranslated string(s), none new). The first attempt refused with exit 3,PREREQUISITE NOT MET(the showcase example'sconnector-slackwas unbuilt). The re-run came after the gate's own printed turbo build.check:i18n:OK (10 package(s) — all bundles in sync.check:i18n-walk-parity:11 declared group(s), 9 walked, 2 exempted.check:test-source-alias,check:cross-package-test-inputs,check:nul-bytes,check:issue-citationsandcheck:doc-authoring: green.check:type-check-debt. Reason: twiceVERDICT queue-timeout (exit 99). The shared verify lock was held more than 19 minutes by another session'spnpm --filter @objectstack/dogfood test(pid 28810, alive). CI's required jobs run it.check:dual-build-cjs-loads. Reason:PREREQUISITE NOT MET, because six packages outside this diff's closure have nodist/(studio,client-react,embedder-openai,knowledge-ragflow,organizations,service-cluster-redis). The build that would supply them got the same queue-timeout. CI's required jobs run it.!andClause-②has no(narrowing)arm.Acceptance notes
TranslationDataSchema, theapps.APPentry requireslabel(packages/spec/src/system/translation.zod.ts). Measured: a module pack carrying onlyapps.cnv_desk.navigation.nav_case.labelis refused bydefineStackwithtranslations.0.zh-CN.apps.cnv_desk.label: Required property 'label' is missing.That refusal is loud, not silent. The way around it is to restate the label of an app the module does not own, and that restated label merges with the owner's translation. Whether the module's copy then wins at runtime is not measured. This is a boundary note, not filed. The fixture keeps every navigation label in the app package's pack, the layout HotCRM's packs were measured in (lint:translation-target-unknownreadsapps[].navigationonly, so every locale key for a CONTRIBUTED navigation item (navigationContributions) is a false positive whose advice deletes a translation the runtime honours #18203). Carrier: none.id ?? nameand descendsitemsas well aschildren. The rule readsidand descendschildrenonly, plusareas[].navigation, whichtranslateAppdoes not translate. For own items this predates this PR and is unchanged. On spec-valid input (anidandchildren) the two agree, and the parity pin holds them there. Noted, not filed. Carrier: none.Generated by Claude Code