Repository navigation
Commit 6dfa3ea
test(cli): derive the build/validate gate roster from what the commands call, not from how a gate is spelled (#18675)
Fixes #18491
`packages/cli/test/validate-build-gate-parity.test.ts` holds one
invariant: 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 call sites by spelling (the two prefixes `lint` and `validate`
followed by a
capital), so it saw **2 of the 47** bare-identifier call sites
`compile.ts` has, and **2 of
39** in `validate.ts`. A gate named anything else was invisible, and the
failure direction
is 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
| Premise | Verdict | Evidence |
|:--|:--|:--|
| `findNavGroupDiagnostics` is really wired into both commands |
**holds** | `compile.ts:51` import, `compile.ts:496` call;
`validate.ts:46` import, `validate.ts:401` call |
| `find*` is the only invisible family | **FALSIFIED** |
`checkProtocolVersionGap` — `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/spec` installed in the APP's `node_modules`, which is the
filesystem, not the stack |
| the roster held the gates it lists | holds, but for a narrower reason
than it reads | the by-name `it.each` rows 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 fail |
Both lists, measured on `objectstack-ai/objectstack` at `62d830e54`:
what the two commands
actually call is 47 and 39 names; what the old detector could see is
`lintUnknownStackKeys` and `lintUnknownAuthoringKeys`. 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` — gates `os build` may 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. `findNavGroupDiagnostics` and `checkProtocolVersionGap` join the
roster **because
the 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 a
parenthesis 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 at
the 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 read` refuses
each shape that would
hide 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 parse
skipped (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
`formatUnknownAuthoringKey`
has today.
## Evidence: two ablations, on the committed tree, each restored by blob
hash
**Positive control (the card's).** Delete the `findNavGroupDiagnostics`
roster row; on-disk
occurrences 1 to 0, verified before the run:
```
LEG A1 new-detector exit=1 (expect 1)
AssertionError: compile.ts / validate.ts call 1 name(s) this file has not classified: findNavGroupDiagnostics.
Tests 1 failed | 18 passed (19)
```
So the row is demanded by the detector, not hand-added.
**The defect direction, two legs on one mutation.** A `find*`-named gate
wired into
`compile.ts` only (`findArtifactSomethingNew`, on-disk occurrences 0 to
1, verified), with
the detector from the base commit `99fcb4ac1` restored beside the new
one:
```
LEG A2 OLD-detector exit=0 (expect 0 — the blind spot)
Test Files 1 passed (1)
Tests 13 passed (13)
LEG A2 NEW-detector exit=1 (expect 1)
AssertionError: compile.ts / validate.ts call 1 name(s) this file has not classified: findArtifactSomethingNew.
AssertionError: os build runs 1 gate(s) that os validate does not: findArtifactSomethingNew.
Tests 2 failed | 18 passed (20)
```
Restore verified by blob hash, not by exit code: test `0d676ed9ef…` and
compile
`32b23f5fc5…` both match HEAD, `git diff HEAD` empty. The ablation
script carried a
`trap ... EXIT INT TERM` restoring with `git checkout HEAD -- path`
against an absolute
repo 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 failed
`packages/cli is not built` (PREREQUISITE NOT MET, not a red); after
`pnpm --filter @objectstack/cli build` both pass —
`published-subpath-console.pin` and `published-subpath-hook-body.pin`,
29 tests. The `integration` tier 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).
- Derived gate families — `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 are
`check:type-check-debt` and `check:dual-build-cjs-loads`, both exit
**3** = PREREQUISITE NOT MET: each needs a repo-wide build, and
`check: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 lint` repo-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` / `StandardErrorCode` untouched**, **no accept-set
change** (no schema
widened or narrowed; the roster is a test ledger, not an accept set).
`skip-changeset`
measured rather than assumed: `@objectstack/cli` ships `files:
["dist","README.md","CHANGELOG.md"]`,
`test/` is not among them, a symbol unique to this diff (`NOT_A_GATE`)
has **0** hits across
`dist` and `bin` after a build, and the positive control
(`findNavGroupDiagnostics`, which
does ship) has **4**. Nothing published moves.
## Acceptance notes
**To file (class (a)/(b)) — `os build` runs a per-package rule walk `os
validate` does not.**
`compile.ts` runs a SECOND `runAuthoringRules('build', ...)` pass over
each entry of
`artifactPackages(...)` with `packageBodyAsStack(...)` resolution
context, de-duplicated
against the union run; `validate.ts` has one
`runAuthoringRules('validate', ...)` call and no
package walk. `compile.ts`'s own comment says what survives that
de-duplication is "exactly
the set the union could not see" — so `os build` reports findings `os
validate` structurally
cannot, 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_GATE` ledger next to the two names, so it is
visible rather than
silent. Dedupe words: `per-package authoring rules` · `artifactPackages`
·
`packageBodyAsStack` · `runAuthoringRules per package` · `os validate
package walk`.
**noted, not filed** — `os lint` hand-wires gates of its own
(`checkHookBodyLowering`,
`computeI18nCoverage`, `scoreMetadata`, `runMetadataEval`) and is
outside the parity
question 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-closure` went red
mid-verification naming `driver-mongodb`, `plugin-reports` and
`service-messaging` as
missing `.d.ts`. Cause was the OOM-killed repo-wide build inside
`check:type-check-debt` on
this 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](https://claude.ai/code/session_01DvvamiacK328idtBYJBxV3)_
---------
Co-authored-by: Claude <noreply@anthropic.com>1 parent 551139b commit 6dfa3ea
1 file changed
Lines changed: 522 additions & 20 deletions
0 commit comments