Repository navigation
test(cli): derive the build/validate gate roster from what the commands call, not from how a gate is spelled - #18675
Merged
os-support-ai merged 3 commits intoSep 17, 2026
Conversation
…ds call, not from how a gate is spelled `validate-build-gate-parity.test.ts` claims every artifact-level gate is wired into both `os build` and `os validate` or into neither — "There is no third option — that is the whole point of the file". Its drift detector extracted `lintFoo(` / `validateFoo(` call sites, so it saw 2 of the 45 names `compile.ts` calls and an entire naming family was invisible: a `find*`-named gate wired into one command only passed, which is the false-clean direction the file exists to stop. Two such gates were live on the tree, in both commands and in no roster: `findNavGroupDiagnostics` (since #14553) and `checkProtocolVersionGap` — the second falsifies "`find*` is the only invisible family" and is why widening the pattern would have moved the blind spot rather than closed it. The scan is inverted instead. It now extracts every bare-identifier call site from comment- and string-blanked source and requires each to land in exactly one of three ledgers: the shared-gate roster, the build-only roster, or a new `NOT_A_GATE` ledger that states why the name is not a gate. It reads no names, so no naming choice defeats it. The two gates above join the roster because the scan demands a row for them. Also closed, same file and same class: `calls()` matched raw source text, so a comment writing a gate name with a parenthesis satisfied the roster's "both commands run it" assertion for a command that had stopped calling it. What the scan still cannot see is asserted absent rather than written down: namespace imports, dynamic imports, `require()`, an import statement the parse skipped, and a value import that is handed on rather than called. Claude-Session: https://claude.ai/code/session_01DvvamiacK328idtBYJBxV3 Co-authored-by: Claude <noreply@anthropic.com>
…te-parity-detector-naming-family
The two counts the docblocks cite were taken before the extractor's spread-call and declaration-site fixes landed, so they read 45 where the scan now finds 47. Corrected, and each now names the repository and commit it was measured on, as a reading without its tree is not a reading. Claude-Session: https://claude.ai/code/session_01DvvamiacK328idtBYJBxV3 Co-authored-by: Claude <noreply@anthropic.com>
Contributor
📓 Docs Drift CheckNothing in this diff resolved to a documentable surface (no symbol, route or SDK anchor derived from 0 changed package(s)), so this run has no opinion about the docs. What this run could not see
Coarse fallback — 0 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): |
This was referenced Sep 17, 2026
os-support-ai
marked this pull request as ready for review
September 17, 2026 14:14
This was referenced Sep 17, 2026
os-support-ai
deleted the
claude/issue-18491-gate-parity-detector-naming-family
branch
September 17, 2026 14:38
This was referenced Sep 17, 2026
akarma-synetal
pushed a commit
to akarma-synetal/framework
that referenced
this pull request
Sep 28, 2026
…er instead of carrying a second copy (objectstack-ai#18710) Fixes objectstack-ai#18490 ## What changed `packages/cli/src/utils/nav-contribution-groups.ts` no longer carries its own copy of the artifact package-id rule. `artifactPackagesOf` is **deleted**; `artifactPackages` — the owner, in `packages/cli/src/utils/artifact-packages.ts` — is imported and used as `findNavGroupDiagnostics`' default package walk. This is the same move `permission-set-name-collisions.ts` already made, for the reason that module states in its own header. `CompiledPackage` stays: 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 spelling `packages[0]`. The dispatch asked which is right for the nav path, and required it measured rather than reasoned. **M1 — the divergent input is REACHABLE.** `ManifestSchema` requires `id` and `name` as strings and constrains neither to be non-empty, so `{ manifest: { id: '', name: '', … } }` parses green through the very `normalizeStackInput` + `ObjectStackDefinitionSchema` chain both commands run. It narrows the card's framing: **both** keys have to be empty. With `id` absent the parse refuses (`packages.0.manifest.id: expected string, received undefined`); with `name` absent, likewise. With `id: 'ok'` and `name: ''` 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: `id` ok + `name` empty, and both ok). | package identity | copy (deleted) | owner (imported) | |:--|:--|:--| | `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 real `ObjectQL`: `registerApp` derives `manifest.id || manifest.name`, which has **no positional fallback at all**, so the fold registers that package under `''` and prints: ``` [Registry] [nav_contribution_group_missing] Package "" contributes 1 navigation item [nav_orders] into group "sales_grp" of app "multi_crm", … ``` The deleted copy made `os build` print `Package "packages[0]"` for that same artifact. ⇒ **the owner's `''` is not merely different, it is the one that matches the runtime**, and `packages[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: had `registerApp` carried 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 the `warnings` array 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 `null` element of `packages[]` throws under the owner where the copy returned a positional id. Every malformed element — `null`, a string, a non-object `manifest`, a missing one — is refused by `ArtifactPackageSchema` before either command's `findNavGroupDiagnostics(result.data)` sees it; measured, all five refused. The remaining shapes (`manifest` a string, `manifest` absent) produce identical output under both rules anyway.⚠️ **Two different claims, kept different**: this PR shows no *current* shipped artifact with empty id keys — it does not look for one, and the card says the blast radius is unmeasured. What it shows is that the input is **accepted by the schema both commands run**, so the divergence was reachable rather than latent-by-construction. ## Tests `packages/cli/src/utils/nav-contribution-groups.package-id.test.ts` — new, four pins: 1. the empty-`id`-and-`name` artifact **parses** (the floor: if a spec change starts refusing it, this reds first and says the pins under it now measure nothing); 2. **the build names that package exactly as the runtime fold does** — build side from the shipped `findNavGroupDiagnostics`, runtime side from a real `ObjectQL`, neither rule re-spelled, asserting the two `packageId` strings and the two messages are equal; 3. and that shared name is **not** the deleted copy's positional spelling (stated separately, because pin 2 would also pass if *both* doors moved to `packages[0]`); 4. two empty-id packages still produce two findings. **Why a separate file.** `new ObjectQL(` is a KERNEL signal in `packages/cli/vitest-tiers.ts`, so a file carrying it is integration tier *by derivation*. Measured: adding the pin to `nav-contribution-groups.test.ts` moved that whole file — and its nine existing objectstack-ai#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, and `unitTestFiles()` places it back in `unit`. **Ablation** — the fix reverted to its pre-change blob, the new pins re-run, then restored: ``` pre-mutation : fe0c8f3 (== HEAD blob) post-mutation: d4b626b (== the BASE blob) artifactPackagesOf occurrences: 0 -> 2 'artifactPackages(parsed)' : 1 -> 0 ablated run: 3 failed | 1 passed (4) AssertionError: expected 'packages[0]' to be '' post-restore : fe0c8f3 == HEAD blob; git diff HEAD empty ``` 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 `6a886b8a4` | what | result | |:--|:--| | `pnpm lint` — **repo-wide**, `eslint . --no-inline-config`, full population, no narrowing | exit 0 | | `pnpm --filter @objectstack/cli typecheck` (incl. `check:test-typecheck`) | exit 0 | | `pnpm --filter @objectstack/cli exec vitest run --project unit` | 212 files, 3026 tests, exit 0 | | `pnpm --filter @objectstack/cli exec vitest run --project integration` | 48 files, 413 tests, exit 0 | | `dispatch-gates.mjs --commands --repo objectstack-ai/objectstack`, every family run, reconciled with `--ran` carrying each exit code | **60 derived, 60 run, 0 NOT-MEASURED, 0 UNRUN** | The 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 3` for `check:dual-build-cjs-loads` and `check:i18n-coverage` (PREREQUISITE NOT MET — unbuilt `dist`). 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 `patch` for `@objectstack/cli`. The declaration above is **measured, not assumed**. Published bytes move: `files[]` is `["dist", …]` and `artifactPackagesOf` appears in `dist/utils/nav-contribution-groups.js` and its `.d.ts` (positive control `findNavGroupDiagnostics` also hits). ⇒ `skip-changeset` does not apply. The diff adds no key, arm, export or registration; it removes one, and that removal reaches no consumer — the package's `exports` map publishes `.`, `./console`, `./hook-body` and `./package.json`, and none of the three entry `.d.ts` files names the symbol. ## In-flight fence — verified rather than trusted PR objectstack-ai#18675 (card objectstack-ai#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 in `compile.ts` and `validate.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 objectstack-ai#18402, faces `packages/rest/src/` and is disjoint. ## Acceptance notes - **Docs-drift sweep — NOT FALSIFIED.** Predicate stated before reading: *hand-written docs describe the artifact package-id fallback, or show a `nav_contribution_group_missing` package 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 ""`) over `content/docs/`, `skills/`, `docs/adr/`. Controls both ways: positive `navigationContributions` hit 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 under `content/docs/references/`, auto-generated, and reference the error code rather than the id rule. - `collectNavGroupInputs`' single-package branch (no `packages[]`) derives its own `packageId` from the top-level manifest — `manifest.id` then `manifest.name`, both string-guarded, with no positional fallback. It is not the artifact package-id rule and cannot be: with no `packages[]` there is no index. Noted, not filed; no PR or seat is routed to that expression by this change. - A cross-door pin now exists on the CLI side only. The runtime half of objectstack-ai#14553 is pinned in `packages/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](https://claude.ai/code)_ --------- Co-authored-by: Claude <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes #18491
packages/cli/test/validate-build-gate-parity.test.tsholds one invariant: everyartifact-level gate is wired into both
os buildandos validate, or into neither —"There is no third option — that is the whole point of the file." Its drift detector
extracted call sites by spelling (the two prefixes
lintandvalidatefollowed by acapital), so it saw 2 of the 47 bare-identifier call sites
compile.tshas, and 2 of39 in
validate.ts. A gate named anything else was invisible, and the failure directionis FALSE CLEAN: a gate wired into one command only passed, which is the file's entire
reason to exist.
Premises checked at the tree, not taken from the card
findNavGroupDiagnosticsis really wired into both commandscompile.ts:51import,compile.ts:496call;validate.ts:46import,validate.ts:401callfind*is the only invisible familycheckProtocolVersionGap—check*— is a second one, live on the tree:compile.ts:822,validate.ts:514, and in zero roster rows. It is a non-registry gate by this file's own definition: it resolves the@objectstack/specinstalled in the APP'snode_modules, which is the filesystem, not the stackit.eachrows were held; the drift scan that was supposed to catch unlisted ones compared two 2-element sets that are identical in both commands, so it could not failBoth lists, measured on
objectstack-ai/objectstackat62d830e54: what the two commandsactually call is 47 and 39 names; what the old detector could see is
lintUnknownStackKeysandlintUnknownAuthoringKeys. The gap is 45 and 37 names.The choice: invert the direction, do not widen the pattern
Widening the regex to cover
find*was the obvious move and the wrong stopping point —two families were invisible at once, which is the evidence that widening moves the blind
spot to whatever the repo names a gate next rather than closing it.
So the scan now asks the opposite question. It extracts every bare-identifier call site
from comment- and string-blanked source and requires each one to land in exactly one of
three ledgers:
SHARED_NON_REGISTRY_GATES— hand-wired artifact-level gates, asserted present in BOTH commands;BUILD_ONLY_GATES— gatesos buildmay legitimately run alone, each with a reason;NOT_A_GATE(new) — everything else, keyed by the reason it is not a gate.It reads no names, so no naming choice defeats it, and a name nobody classified fails its
own PR.
findNavGroupDiagnosticsandcheckProtocolVersionGapjoin the roster becausethe scan demands a row for them — not by hand.
Also closed, same file and same class
calls()matched raw source text, so a comment or a string writing a gate name with aparenthesis after it satisfied the roster's "both commands run it" assertion for a command
that had stopped calling it. It now reads the blanked source. Pinned by
a comment or a string literal is not a wiring. The inline-conversion-notice assertion atthe foot of the file deliberately keeps reading RAW source — an inline copy of that
sentence IS a string literal, and blanking strings there would make that assertion pass on
exactly the file it exists to reject.
What the scan still cannot see — asserted absent, not written down and hoped for
the command sources use no call shape this scan cannot readrefuses each shape that wouldhide a gate, so introducing one is a RED rather than a silent return to the blind spot:
namespace imports, dynamic
import(...),require(...), an import statement the parseskipped (the consumed count is asserted equal to the statement count: 22 of 22 and 18 of
18), and a value import handed on rather than called — the shape
formatUnknownAuthoringKeyhas today.
Evidence: two ablations, on the committed tree, each restored by blob hash
Positive control (the card's). Delete the
findNavGroupDiagnosticsroster row; on-diskoccurrences 1 to 0, verified before the run:
So the row is demanded by the detector, not hand-added.
The defect direction, two legs on one mutation. A
find*-named gate wired intocompile.tsonly (findArtifactSomethingNew, on-disk occurrences 0 to 1, verified), withthe detector from the base commit
99fcb4ac1restored beside the new one:Restore verified by blob hash, not by exit code: test
0d676ed9ef…and compile32b23f5fc5…both match HEAD,git diff HEADempty. The ablation script carried atrap ... EXIT INT TERMrestoring withgit checkout HEAD -- pathagainst an absoluterepo root.
Verification (all at
02f3bfadb)pnpm --filter @objectstack/cli exec vitest run test/validate-build-gate-parity.test.ts— 20 passed (was 13).pnpm --filter @objectstack/cli exec vitest run --project unit— 211 files / 3026 tests, 29 skipped. Two suites first failedpackages/cli is not built(PREREQUISITE NOT MET, not a red); afterpnpm --filter @objectstack/cli buildboth pass —published-subpath-console.pinandpublished-subpath-hook-body.pin, 29 tests. Theintegrationtier is declared to CI: no path in this diff touches it,bin/, or a spawn entry point.pnpm --filter @objectstack/cli typecheck— exit 0.pnpm --filter @objectstack/cli check:test-typecheck— OK, 3 files / 28 errors / 6 pinned signatures, unmoved.pnpm --filter '@objectstack/cli^...' build— exit 0 (the dependency closure).node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack, reconciled with--ran: 47 derived, 45 run green, 2 NOT MEASURED, 0 UNRUN. The two arecheck:type-check-debtandcheck:dual-build-cjs-loads, both exit 3 = PREREQUISITE NOT MET: each needs a repo-wide build, andcheck:type-check-debt's own internal build was OOM-killed (exit 137) on this shared container. Declared to CI; neither is a finding and neither is a pass.pnpm lintrepo-wide (eslint . --no-inline-config) — exit 0. Run whole, so no narrowing to justify.Clause-②: no
Three readings on the measured diff: no new exports (a test file; it exports nothing),
ERROR_CODE_LEDGER/StandardErrorCodeuntouched, no accept-set change (no schemawidened or narrowed; the roster is a test ledger, not an accept set).
skip-changesetmeasured rather than assumed:
@objectstack/clishipsfiles: ["dist","README.md","CHANGELOG.md"],test/is not among them, a symbol unique to this diff (NOT_A_GATE) has 0 hits acrossdistandbinafter a build, and the positive control (findNavGroupDiagnostics, whichdoes ship) has 4. Nothing published moves.
Acceptance notes
To file (class (a)/(b)) —
os buildruns a per-package rule walkos validatedoes not.compile.tsruns a SECONDrunAuthoringRules('build', ...)pass over each entry ofartifactPackages(...)withpackageBodyAsStack(...)resolution context, de-duplicatedagainst the union run;
validate.tshas onerunAuthoringRules('validate', ...)call and nopackage walk.
compile.ts's own comment says what survives that de-duplication is "exactlythe set the union could not see" — so
os buildreports findingsos validatestructurallycannot, in the false-clean direction, one layer past the gate roster this PR repairs. Not
wired up here: wiring a real gate into the other door is a decision, not a test fix. It is
recorded in the
NOT_A_GATEledger next to the two names, so it is visible rather thansilent. Dedupe words:
per-package authoring rules·artifactPackages·packageBodyAsStack·runAuthoringRules per package·os validate package walk.noted, not filed —
os linthand-wires gates of its own (checkHookBodyLowering,computeI18nCoverage,scoreMetadata,runMetadataEval) and is outside the parityquestion by design: it emits no artifact. The closed-ledger mechanism added here would
extend to it unchanged if that ever becomes a parity question. Carrier: the next PR that
touches this file.
noted, not filed — local build-state only, no carrier:
check:dts-closurewent redmid-verification naming
driver-mongodb,plugin-reportsandservice-messagingasmissing
.d.ts. Cause was the OOM-killed repo-wide build insidecheck:type-check-debtonthis shared container leaving three packages with JS and no declarations. Rebuilding the
three restored it (61 packages swept, 153/153 declaration files present). CI checks out
fresh, so there is nothing to carry.
Generated by Claude Code