Repository navigation
fix(cli): nav-contribution-groups imports the artifact package-id owner instead of carrying a second copy - #18710
Conversation
Claude-Session: https://claude.ai/code/session_01DvvamiacK328idtBYJBxV3 Co-authored-by: Claude <noreply@anthropic.com>
…r file Claude-Session: https://claude.ai/code/session_01DvvamiacK328idtBYJBxV3 Co-authored-by: Claude <noreply@anthropic.com>
📓 Docs Drift Check3 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 — 24 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 70e9810ec17a2f7fbefad249066f3bb345a0eeec && git checkout 70e9810ec17a2f7fbefad249066f3bb345a0eeec
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 72dd95fa5a87df8990986490b4638506dd8b2d3a 6a886b8a4c6271639400cf4d4400e0ba97ab9d1b && git checkout -B drift-repro 72dd95fa5a87df8990986490b4638506dd8b2d3a && git merge --no-ff 6a886b8a4c6271639400cf4d4400e0ba97ab9d1b
node scripts/docs-audit/affected-docs.mjs --json 72dd95fa5a87df8990986490b4638506dd8b2d3a |
Fixes #18490
What changed
packages/cli/src/utils/nav-contribution-groups.tsno longer carries its own copy of the artifact package-id rule.artifactPackagesOfis deleted;artifactPackages— the owner, inpackages/cli/src/utils/artifact-packages.ts— is imported and used asfindNavGroupDiagnostics' default package walk. This is the same movepermission-set-name-collisions.tsalready made, for the reason that module states in its own header.CompiledPackagestays: it is a structural parameter type naming the two facts this module reads, not a second spelling of the id rule.The half the card left open, measured before anything was edited
The card is explicit that importing the owner is a decision, not a merge: the owner answers
''for a package whose id keys are empty, the deleted copy answered the positional spellingpackages[0]. The dispatch asked which is right for the nav path, and required it measured rather than reasoned.M1 — the divergent input is REACHABLE.
ManifestSchemarequiresidandnameas strings and constrains neither to be non-empty, so{ manifest: { id: '', name: '', … } }parses green through the verynormalizeStackInput+ObjectStackDefinitionSchemachain both commands run. It narrows the card's framing: both keys have to be empty. Withidabsent the parse refuses (packages.0.manifest.id: expected string, received undefined); withnameabsent, likewise. Withid: 'ok'andname: ''the two rules agreed already.M2 — behaviour does change, and only there. On the reachable input the two rules produce different diagnostics; on every other input, byte-identical output (control legs:
idok +nameempty, and both ok).id: '',name: ''packageId: 'packages[0]'packageId: ''id: 'com.example…',name: ''com.example…com.example…(identical)id: 'com.example…',name: 'Orders'com.example…com.example…(identical)M3 — the runtime is the judge, and it answers
''.nav-contribution-groups.ts' own header says the id it carries is "the string the runtime registers a contribution under, so a command names a package the same way the fold does". Asked of a realObjectQL:registerAppderivesmanifest.id || manifest.name, which has no positional fallback at all, so the fold registers that package under''and prints:The deleted copy made
os buildprintPackage "packages[0]"for that same artifact. ⇒ the owner's''is not merely different, it is the one that matches the runtime, andpackages[0]is a name the runtime cannot produce. The STOP-AND-REPORT condition in the dispatch is therefore not triggered — the measurement came out in favour of the direction triage settled, and it could have come out the other way: hadregisterAppcarried a positional fallback, or dropped a contribution whose id is empty, the copy would have been the runtime-matching side.M2b — the id is carried and printed, never keyed on. Two packages that both resolve to
''still produce two findings under both rules; nothing on this path uses the id as a map key, a dedupe key, a route segment or a sort key. Downstream it is spread into thewarningsarray of both commands' JSON payloads, unkeyed.M4/M5 — the one robustness delta, and why it cannot be reached. The owner does not re-check entry shape (its header declares that precondition). A
nullelement ofpackages[]throws under the owner where the copy returned a positional id. Every malformed element —null, a string, a non-objectmanifest, a missing one — is refused byArtifactPackageSchemabefore either command'sfindNavGroupDiagnostics(result.data)sees it; measured, all five refused. The remaining shapes (manifesta string,manifestabsent) produce identical output under both rules anyway.Tests
packages/cli/src/utils/nav-contribution-groups.package-id.test.ts— new, four pins:id-and-nameartifact parses (the floor: if a spec change starts refusing it, this reds first and says the pins under it now measure nothing);findNavGroupDiagnostics, runtime side from a realObjectQL, neither rule re-spelled, asserting the twopackageIdstrings and the two messages are equal;packages[0]);Why a separate file.
new ObjectQL(is a KERNEL signal inpackages/cli/vitest-tiers.ts, so a file carrying it is integration tier by derivation. Measured: adding the pin tonav-contribution-groups.test.tsmoved that whole file — and its nine existing #14553 pins — out of the unit tier (unit 212 → 211, integration 47 → 48). Splitting confines the tier change to the cases that actually boot a registry. The unit file now differs only by the import repoint and two comments, andunitTestFiles()places it back inunit.Ablation — the fix reverted to its pre-change blob, the new pins re-run, then restored:
The one pin that stays green under ablation is the reachability floor, which reads the owner directly — the in-run control that the harness is not simply broken.
Verification, at
6a886b8a4pnpm lint— repo-wide,eslint . --no-inline-config, full population, no narrowingpnpm --filter @objectstack/cli typecheck(incl.check:test-typecheck)pnpm --filter @objectstack/cli exec vitest run --project unitpnpm --filter @objectstack/cli exec vitest run --project integrationdispatch-gates.mjs --commands --repo objectstack-ai/objectstack, every family run, reconciled with--rancarrying each exit codeThe integration tier was run locally because this diff adds an integration-tier file; it touches no existing one, and no spawn entry.
An earlier reconciliation at an intermediate commit recorded
exit 3forcheck:dual-build-cjs-loadsandcheck:i18n-coverage(PREREQUISITE NOT MET — unbuiltdist). Both were re-run at the final commit and returned real verdicts (104 published require entry point(s) … load;13 config(s), 621 baselined untranslated string(s), none new).Changeset
Clause-②: no
patchfor@objectstack/cli. The declaration above is measured, not assumed. Published bytes move:files[]is["dist", …]andartifactPackagesOfappears indist/utils/nav-contribution-groups.jsand its.d.ts(positive controlfindNavGroupDiagnosticsalso hits). ⇒skip-changesetdoes not apply. The diff adds no key, arm, export or registration; it removes one, and that removal reaches no consumer — the package'sexportsmap publishes.,./console,./hook-bodyand./package.json, and none of the three entry.d.tsfiles names the symbol.In-flight fence — verified rather than trusted
PR #18675 (card #18491) is the only in-flight claim whose roster names
findNavGroupDiagnostics, which is defined in the file edited here. Read at its head: its diff is one file,packages/cli/test/validate-build-gate-parity.test.ts, and its closed-ledger scan extracts bare-identifier call sites incompile.tsandvalidate.ts. This change touches neither command, and moves that function's name, signature (two parameters, same names, same types — only the default argument's expression changes) and export not at all. ⇒ the seat's benign judgement holds. The other claim, card #18402, facespackages/rest/src/and is disjoint.Acceptance notes
nav_contribution_group_missingpackage id, such that this change makes that text wrong. Swept by symbol (artifactPackagesOf,artifactPackages,nav_contribution_group_missing) and by input shape (packages[0],packages[i], the bracketed-index spelling,Package "") overcontent/docs/,skills/,docs/adr/. Controls both ways: positivenavigationContributionshit 6 files, negative nonsense token hit 0. One hand-written hit,content/docs/ui/setup-app.mdx, says the diagnostic names "the contributing package" generically and shows no id spelling — still true, and marginally more true, since its "the same finding" claim about the two doors was what the divergence quietly falsified. The other two hits are undercontent/docs/references/, auto-generated, and reference the error code rather than the id rule.collectNavGroupInputs' single-package branch (nopackages[]) derives its ownpackageIdfrom the top-level manifest —manifest.idthenmanifest.name, both string-guarded, with no positional fallback. It is not the artifact package-id rule and cannot be: with nopackages[]there is no index. Noted, not filed; no PR or seat is routed to that expression by this change.navigationContributions[].groupthat names no group in the target app is silently RELOCATED to the top level — refuse, warn, or leave to the consumer? #14553 is pinned inpackages/objectql/src/registry-nav-contribution-group-semantics.test.ts, and nothing there covers the empty-id identity. Noted, not filed — out of the declared face (packages/cli/src/utils/), and the new pin reads the real runtime, so the fact is guarded from one side. Would-be carrier: the next card touching that objectql suite.Generated by Claude Code