Repository navigation
fix(cli): the per-package de-duplication key ignores the top-level collection index, so an echo no longer survives it - #18878
Conversation
…llection index, so an echo no longer survives it (#18779) Claude-Session: https://claude.ai/code/session_01DvvamiacK328idtBYJBxV3 Co-authored-by: Claude <noreply@anthropic.com>
…ries a nested index, and the key's bound is measured rather than quoted (#18779) Claude-Session: https://claude.ai/code/session_01DvvamiacK328idtBYJBxV3 Co-authored-by: Claude <noreply@anthropic.com>
…ple corpus, not sized by quoting a neighbouring pin (#18779) Claude-Session: https://claude.ai/code/session_01DvvamiacK328idtBYJBxV3 Co-authored-by: Claude <noreply@anthropic.com>
…tion (#18779) Claude-Session: https://claude.ai/code/session_01DvvamiacK328idtBYJBxV3 Co-authored-by: Claude <noreply@anthropic.com>
…enuinely cannot see, not an echo (#18779) The per-package survivor this pin relied on was an echo of a union finding. Once the de-duplication key stopped comparing the top-level index it was correctly filtered, the fixture's per-package set went empty and the non-vacuity case went red. The fixture now uses the falsifier shape its lint sibling already used, and the non-vacuity case asserts the survivor's pedigree rather than only its count. Claude-Session: https://claude.ai/code/session_01DvvamiacK328idtBYJBxV3 Co-authored-by: Claude <noreply@anthropic.com>
📓 Docs Drift CheckThis PR changes 1 package(s): 32 hand-written doc(s) name something this change touched — list omitted above 15 rows. Re-derive on the tree named below: ⛔ 8 release-owned page(s) also affected — read-only, see AGENTS.md Documentation Guardrails. 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 1664c593ba7ce4cc55ff6bdb9755600cf4521776 && git checkout 1664c593ba7ce4cc55ff6bdb9755600cf4521776
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 600b1e2ab129008e9d43e7101be35d22befe2142 6a0a4df1b4e9e2f83578c2278f5985fc0053e74c && git checkout -B drift-repro 600b1e2ab129008e9d43e7101be35d22befe2142 && git merge --no-ff 6a0a4df1b4e9e2f83578c2278f5985fc0053e74c
node scripts/docs-audit/affected-docs.mjs --json 600b1e2ab129008e9d43e7101be35d22befe2142
|
The failing assertion, recovered locally — CI could not hand it over
What CI did publish, as annotations: Re-run here, whole, with no What it is. That is a precondition test, not a count pin: it asserts #18780's fixture really reaches the per-package pass and leaves a survivor, so the equality assertions below it are not vacuous. On that fixture the only per-package survivor was the echo — so once this PR's key stops letting echoes through, the count is 0 and the precondition fails. ⇒ Same shape as the Why a green local run and a red CI coexisted. The failing test is One reading in that run is NOT this PR's, recorded so nobody chases it. ⇒ One real failure, one file, one fixture to repair. The fix is in progress; this comment exists so the assertion is on the record, since the job log that carries it cannot be fetched. Generated by Claude Code |
… echo (#18779) The multi-package fixture gave bc_account.industry no consumer anywhere, so the union run raised it too and the per-package 'survivor' was that same finding re-reported at the package-local index. Once the de-duplication key stopped comparing the top-level collection index the survivor count went to 0 and the precondition failed — correctly. `orders` now owns the view that displays the field, the same falsifier shape the two parity pins carry, and the precondition additionally asserts the survivor's pedigree so the control cannot be re-lit by a duplicate. Claude-Session: https://claude.ai/code/session_01DvvamiacK328idtBYJBxV3 Co-authored-by: Claude <noreply@anthropic.com>
…s the source states it `content/docs/deployment/validating-metadata.mdx` said of the per-package walk that "what it reports is exactly the set the union could not see". That sentence was removed from eight code carriers by #18878 because it is false, and the two notes that replaced it forbid restating it: `packages/cli/src/commands/compile.ts` carries "Do not re-inflate that to" it, and `packages/cli/src/utils/artifact-packages.ts` records that the claim the pass is entitled to make "is narrower than" it. The page was the last place in the tree still asserting it as a claim. The page now states the bound in the source's own settled words — the set of per-package findings no union finding already carried under the same rule, `where`, message and non-top-level position — and carries the narrowness note the source wrote down so the next reader does not re-inflate it: the key is position-insensitive, not collision-proof. Claude-Session: https://claude.ai/code/session_01QCdUBjM47SxioST9z5Zwdf Co-authored-by: Claude <noreply@anthropic.com>
…s own source, and derive the pins that bound a spelling (objectstack-ai#18924) Fixes objectstack-ai#18520 Clause-②: no The nightly-tier test files under `packages/cli/test/` that read this package's own source TEXT now read it through the shared mask (`scripts/js-comment-mask.mjs`), and the pins that bound a byte-exact SPELLING now bind a property derived from the source instead. Every file was judged on its own; the table below gives the rung and the reason per file. ## The population, re-derived — no number inherited **Predicate, stated:** a file under `packages/cli/test/` whose NAME puts it in a nightly tier (`scripts/nightly-tiers.mjs` — `*.e2e.test.*` / `*.live.test.*`, and nothing else selects a tier), which READS this package's own source text at test time, directly or through a helper under `test/helpers/` it imports. Evaluated over each file's local import closure, on MASKED text, so a read one module away still counts and a path named only in prose does not. | predicate | count | measured at | |:--|--:|:--| | card's: `test/*.e2e.test.ts` containing `readFileSync` | **26** | reproduces exactly | | card's: of those, importing the shared mask | **3** | reproduces exactly — `serve-port-readback`, `published-entry-stderr-nonblocking`, `run-dev-stderr-nonblocking` | | claim's: `grep -rl readFileSync test/*.e2e.test.ts \| xargs grep -l 'src/'` | **20** | reproduces exactly | | **mine** (above) — nightly-tier readers of this package's own source | **15** | 12 raw, 3 already masked | ⭐ **20 and 15 are not a contradiction — they are two predicates, and the delta is readable both ways.** Eight files are in the claim's 20 and not in mine, three are in mine and not in its 20: - **prose only** — `lint-eval-generator-load-envelope` matches `src/` in a docblock sentence and reads `/definitely/not/here.json`. This is the false positive the dispatch predicted. - **a temp fixture's `src/`, not ours** — `build-docs-step-count`, `build-multi-package-artifact`, `generate-skill`, `serve-publishes-bound-port` create `src/docs` or `src/skills` inside a scratch project and read `dist/objectstack.json` back. - **another package's source** — `scaffold-emission-policy` reads `packages/create-objectstack`'s template `package.json`. JSON carries no comments, so no rung applies. - **own source, but `bin/` not `src/`** — `published-entry-stderr-nonblocking` (already masked) and `run-dev-unbuilt-workspace` (raw). Both read a hand-written published entry; the second is in this PR, the first was already correct. - **missed by the claim's grep, found by mine** — `config-miss-stdout-purity` reads `src/commands` through `test/helpers/config-miss-family.ts` and never spells `src/` itself; `invocation-loudness` and `serve-host-fallback-base` DO spell `../src/...` but never read it as text — they `symlink`/`execFile` it, so they are readers of a module, not of prose. ## Rung per file | file | reads | rung | what changed, and why that rung | |:--|:--|:--|:--| | `build-json-failure-conversions.e2e.test.ts` | `src/commands/compile.ts` | **3 only** | Masked. Its `indexOf` ORDER block (`declAt` / `tryAt` / `loadAt` / `normalizeAt`) is the objectstack-ai#17633 shape verbatim — raw positions over a file whose docblocks name `await loadConfig(`. ⛔ The six frozen integers are card objectstack-ai#18894's and are untouched here. | | `build-json-failure-warnings.e2e.test.ts` | `src/commands/compile.ts` | **3 only** | Masked, same fence — integers untouched. | | `cloud-login-json-ndjson.e2e.test.ts` | `src/commands/cloud/login.ts` | **3 + 2** | Masked; and the pin that subtracted ONE byte-exact line from the `emitJson(` hits now partitions the call sites against `emitRecord`'s own brace-matched body: outside must be empty, inside must not be. | | `login-json-ndjson.e2e.test.ts` | `src/commands/login.ts` | **3 + 2** | Same conversion, same reason. | | `login-json-noninteractive.e2e.test.ts` | `src/commands/login.ts` | **3** | Masked only. Its `rl.question(` filter already binds a SHAPE (does the line carry the abort signal), not a spelling — rung 2 has nothing to convert. | | `json-stdout-purity.e2e.test.ts` | `src/commands/**` | **3 + 2** | Masked; and `toHaveLength(10)` demoted to a floor. The line above it already binds the SET against the map a sibling nightly file drives, so the integer was a second frozen copy of one fact. It stays as a floor because it is the only guard on the vacuum both sides share. | | `config-miss-stdout-purity.e2e.test.ts` | `src/commands/**` via helper | **3** | ⭐ No edit in this file — its reader lives in `test/helpers/config-miss-family.ts`, which this PR masks once for both consumers. | | `test/helpers/config-miss-family.ts` | `src/commands/**` | **3** | Masked. Both discovery halves are regexes over command source and both are satisfiable by prose: a docblock naming `json: Flags.boolean(` beside a `utils/config.js` import invents a direct member; a commented-out `export default class X extends Y` invents an alias. | | `diff-usage-error-stream.e2e.test.ts` | `src/commands/**` | **3** | Masked. This scan decides by LINE POSITION — writers above the first `flags.json` read — which is the print-ORDER failure that opened this card. Its `toBeGreaterThan(10)` population control was already a floor. | | `run-dev-unbuilt-workspace.e2e.test.ts` | `bin/run-dev.js` | **3** | Masked. `exec` takes the FIRST match, so a docblock recording the old `STDERR_DRAIN_STALL_MS` would be read as the shim's bound. Its sibling over the other published entry already masks; this makes the pair consistent. | | `serve-app-anchored-optional-import.e2e.test.ts` | `src/commands/serve.ts` | **1 + 2 + 3** | See below — five byte-exact statement pins, rewritten. | | `validate-json-failure-conversions.e2e.test.ts` | `src/commands/validate.ts` | **3** | Masked, and the paragraph that recorded the opposite decision is corrected rather than left false. | | `validate-json-failure-warnings.e2e.test.ts` | `src/commands/validate.ts` | **3** | Same. | | `serve-port-readback` · `published-entry-stderr-nonblocking` · `run-dev-stderr-nonblocking` | own source | — | Already masked. Untouched. | ### Rung 1 — where it applied, and where it did NOT Rung 1 permits DELETING a nightly assertion when a per-PR sibling already binds the same thing. I searched for a sibling for every byte-exact subject in this population and **opened** the one I found: - ✅ **Applied once.** `serve-app-anchored-optional-import` bound `function importFromHost(specifier: string, hostRoot: string = servedAppRootOrCwd())` — an argument LIST, the objectstack-ai#17725 shape exactly. `src/commands/serve-cluster-host-resolution.test.ts` is queue-tier (its name carries no `.e2e`, so `nightly-tiers.mjs` leaves it in the per-PR run) and binds that function's EXISTENCE, its module scope and its uniqueness. So this PR does not re-pin any of that here; what it keeps, because the sibling does not bind it, is the DEFAULT — read off the paren-matched parameter list. - ⛔ **Did not apply anywhere else, and this is a measurement.** `anchorServedApp` and `servedAppRootOrCwd` appear in no other test in the package. The login emitter contract (`emitRecord`) appears in no queue-tier test at all. For `warningsSoFar`, the queue-tier `test/truncation-remainder-notices.test.ts` binds `'warnings: warningsSoFar(),'` in `compile.ts` — the CALL SITE, not the declaration and not the spread ORDER these files pin, and for `validate.ts` it binds different payload keys entirely. Not the same thing, so nothing was deleted on its account. ### `serve-app-anchored-optional-import` in detail Five `toContain`/`toMatch` pins over whole statements of `serve.ts`, in a file only the nightly tier collects. Each now binds what it was written for: | was | is | |:--|:--| | `toContain('const { configPath: absolutePath, configExists } = anchorServedApp(args.config!);')` | exactly ONE `anchorServedApp(` call site (the declaration excluded by the same `function` lookbehind the per-PR sibling uses), and its ARGUMENT is `args.config!`. The destructured local names are deliberately no longer bound. | | `not.toMatch(/const absolutePath = path\.resolve\(process\.cwd\(\), args\.config!\)/)` | the same negative as a SHAPE: `path.resolve(process.cwd(), args.config` with whitespace tolerated. A negative pin on one exact spelling passes for every respelling of the defect. | | `toContain('function importFromHost(specifier: string, hostRoot: string = servedAppRootOrCwd())')` | the `hostRoot` parameter's DEFAULT, read off the paren-matched parameter list. A third parameter or a renamed `specifier` no longer reddens it. | | `toContain('const hostRoot = servedAppRootOrCwd();')` and `toContain('const root = hostRoot ?? servedAppRootOrCwd();')` | a partition over every `const`/`let` host-root binding: at least two exist, and none may be bound without resolving through `servedAppRootOrCwd()`. | | `toMatch(/^function servedAppRootOrCwd\(\): string \{$/m)` | exactly one MODULE-SCOPE `function servedAppRootOrCwd(`, never indented, never a `const`/`let`/`var`. The return-type annotation and the brace are not the defect. | ## What the mask changed TODAY: nothing — and that is the measurement Every converted reader was evaluated raw and masked over the same tree, and every verdict is identical. Masking is therefore behaviour-preserving now and protective later — it did not launder a stale pin green: ``` SAME json-stdout-purity/discoverFamily (the same command ids) SAME config-miss-family/discover (the same 10 ids) SAME diff-usage/offenders = [] SAME diff-usage/withJson count = 28 SAME emitJson call sites — login.ts = 1 cloud/login.ts = 1 SAME rl.question without signal — login.ts = [] SAME payloadLiterals — validate.ts = 7 compile.ts = 11 SAME order indexes — validate.ts / compile.ts (declAt/tryAt/loadAt/normalizeAt) SAME serve.ts anchorServedApp / servedAppRootOrCwd sites SAME run-dev STDERR_DRAIN_STALL_MS = "15_000" ``` ⛔ `scripts/check-comment-mask-adoption.mjs` is green before and after and its ledger is unchanged at 14 rows — because none of these twelve files ever carried a private stripper. They carried NO masking at all, which is the blind spot the card names and the gate documents. This PR adds no private stripper; every file imports the shared module. ## Ablations Each is a script with `trap` restore on `EXIT INT TERM`, absolute paths, an on-disk occurrence count proving the mutation landed, exit codes captured BEFORE any pipe, and a restore proven by `git hash-object` against the HEAD blob plus a clean `git status` for both paths.⚠️ The first attempt of all three read exit 1 everywhere in 17 seconds. That was not a result: without `OS_TEST_TIERS=nightly` the package collects none of these files, and it says so loudly. Recorded here because a run that measured nothing is the failure mode this card is about. The numbers below are from the re-run with the switch set. **ABL-1 — the objectstack-ai#17633 shape, reproduced and then shown fixed.** A COMMENT is injected into `src/commands/info.ts` between `async run(` and its first `flags.json` read, naming `printHeader(`. Behaviour is untouched; only prose moved. | leg | pin | exit | reading | |:--|:--|--:|:--| | masked (this PR) | `diff-usage-error-stream` | **0** | prose is invisible | | raw (pre-conversion, restored from the merge base) | same file | **1** | `AssertionError: expected [ 'info.ts' ] to deeply equal []` | ⭐ That failure line IS the card's first row: a docblock reporting an order change that never happened. **ABL-2 — `serve.ts`, both directions.** | leg | mutation | pin | exit | reading | |:--|:--|:--|--:|:--| | 1 | `const hostRoot = servedAppRootOrCwd();` becomes `process.cwd()` | converted | **1** | `a host root is bound without resolving through servedAppRootOrCwd(): expected [ Array(1) ] to deeply equal []` — the new partition is not vacuous | | 2 | a THIRD parameter added to `importFromHost`, same arguments, same behaviour | converted | **0** | the argument LIST is no longer the contract | | 2 | same mutation | pre-conversion | **1** | `expected '…' to contain 'function importFromHost(specifier: st…'` — objectstack-ai#17725 verbatim, on a pin no pull request can see | **ABL-3 — the login emitter partition.** | leg | mutation | pin | exit | reading | |:--|:--|:--|--:|:--| | 1 | a real second `await emitJson(` outside `emitRecord` | converted | **1** | `every --json write in login.ts must go through emitRecord(): expected [ Array(1) ] to deeply equal []` | | 2 | a COMMENT quoting `emitJson(payload, 0)` | converted | **0** | prose is invisible | | 2 | same comment | pre-conversion | **1** | same assertion — a comment breaking a pin whose code never moved, the card's second row | All three restored cleanly: `git hash-object` matches the HEAD blob for every mutated path and `git status --porcelain` is empty for both paths in each script. ## Verification -⚠️ **Tier.** `OS_TEST_TIERS` unset collects none of these files. Every red/green below is under `OS_TEST_TIERS=nightly`. Independently confirmed by `pnpm check:tier-file-adoption`: 68 nightly-tier files on disk, owned by one package. - **`packages/cli` built** (`pnpm --filter '@objectstack/cli...' build`) so the pins that refuse at load on an absent `dist/index.js` actually run. - **Both whole-package runs, no `--project` filter — what CI runs:** ``` pnpm --filter @objectstack/cli test Test Files 267 passed (267) Tests 3485 passed (3485) exit 0 OS_TEST_TIERS=nightly pnpm --filter @objectstack/cli test Test Files 68 passed (68) Tests 695 passed (695) exit 0 ``` The 12 converted files were also run on their own under `OS_TEST_TIERS=nightly` before the whole runs: 12 files / 336 tests / exit 0. - **Gate families** derived with `node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack` and reconciled with `--ran`: 47 derived, 46 run green, **1 NOT MEASURED** — `pnpm check:dual-build-cjs-loads` exits 3 (PREREQUISITE NOT MET: it reads built output for packages outside this closure). ⛔ Recorded as NOT MEASURED, not as a pass and not as a red. - **`pnpm lint`** over the whole repo, unnarrowed. `node --stack-size=4000 eslint . --no-inline-config` over the whole repository, **exit 0**. This is the unnarrowed run, so no narrowing has to be justified. ## `skip-changeset`, by measurement with controls both ways This PR's own diff (`git diff --name-only origin/main...HEAD`) is 12 files, all under `packages/cli/test/`. `packages/cli`'s `files[]` is `["dist","README.md","CHANGELOG.md"]` and `tsconfig.build.json` has `include: ["src"]`, so `test/` reaches neither. Measured against the built tree rather than argued: - **negative** — `bodySpan`, `splitParams`, `paramsOf` (the three symbols this PR introduces): 0 files in `dist`, 0 in `README.md`. - **positive control** — `anchorServedApp` (2 files), `servedAppRootOrCwd` (2), `emitRecord` (3) in `dist`, so the grep over the published tree finds things and the zero above is a reading, not a dead search. - ⛔ No `scripts/**` path is touched, so the published surface did not have to be re-derived. ## Fences - ⛔ The six frozen integers in `build-json-failure-{warnings,conversions}` are untouched; card objectstack-ai#18894 owns that axis. Only rung 3 was applied in those two files. - PR objectstack-ai#18878 (card objectstack-ai#18779) was re-measured at the start: zero intersection with this population, and it has since merged as `031e5fbfa3`. Its `compile.ts` / `validate.ts` edits moved no count these pins read (compile.ts still 11 payloads, validate.ts still 7), re-measured after the merge. This PR touches none of its six test files. - ⛔ No test is skipped, quarantined or weakened; no assertion was loosened to make a run pass. - ⛔ `content/docs/releases/` and every `packages/*/CHANGELOG.md` untouched. - ⛔ No command was refused by the permission classifier. ## Acceptance notes - `src/commands/serve-cluster-host-resolution.test.ts` carries a private `stripComments` at line 151. It is a DECLARED row in `check-comment-mask-adoption.mjs`'s shrink-only ledger, measured there as agreeing with the shared mask byte for byte over 212 files, so it is recorded debt rather than a finding, and converting it is that ledger's per-row work. Noted, not filed. - `validate-json-failure-{warnings,conversions}` still pin a multi-line spread ORDER in `validate.ts` byte-exactly (`...ruleAdvisories, ...docWarnings, ...`), and the two `build-json-failure-*` files pin the same shape over `compile.ts`. That is a rung-1/rung-2 question on an axis this PR did not open, and for the `build-*` pair it sits in the file the fence closes. Noted, not filed — the carrier is card objectstack-ai#18894, which is already open over those two files. - `invocation-loudness.e2e.test.ts` and `serve-host-fallback-base.e2e.test.ts` reach `../src/...` and are NOT source-text readers (they symlink and execute it). No rung applies; recorded so the next re-derivation does not re-open them. Noted, not filed. --- _Generated by [Claude Code](https://claude.ai/code/session_01DvvamiacK328idtBYJBxV3)_ Co-authored-by: Claude <noreply@anthropic.com>
…the source states it (objectstack-ai#19241) Fixes objectstack-ai#18893 `content/docs/deployment/validating-metadata.mdx` was the last place in the tree still asserting, as a claim, the sentence the CLI source explicitly forbids restating — of the per-package walk: *"what it reports is exactly the set the union could not see"*. PR objectstack-ai#18878 (card objectstack-ai#18779) removed that sentence from eight code carriers because it is false, and the two notes that replaced it are this page's acceptance baseline: - `packages/cli/src/utils/artifact-packages.ts` — the `findingKey` docblock records that the claim the pass is entitled to make *"is narrower than"* that sentence, and that the key is *"position-insensitive, ⛔ not collision-proof"*. - `packages/cli/src/commands/compile.ts` — *"⛔ Do not re-inflate that to"* it, beside the settled statement of what does survive. ## What changed One sentence, one file. The page now states the bound in the source's own settled words — the set of per-package findings no union finding already carried under the same rule, `where`, message and non-top-level position — says why the leading collection index is neutralised (a package body re-bases its collections from 0, so one finding would otherwise get two keys), and carries the narrowness note the source wrote down so the next reader does not re-inflate it. The surrounding paragraph's teaching is untouched. ⛔ No source file was changed. The source is the authority here; the page is what was wrong. ## Measurement Whitespace-normalised, because the target sentence **wraps across two lines** and a line-oriented `grep -F` returns `0` on it — a zero triage and two seats each paid for once on this very card: | needle | base `e233db9` | after | |:--|--:|--:| | `exactly the set the union could not see` | 1 | **0** | | lit control `one gate, four doors` | 2 | 2 | | dark control `zzzNotARealToken` | 0 | 0 | The lit control still fires after the edit, so that `0` is a reading and ⛔ not an instrument artefact. The naive line-oriented `grep -F` reads `0` both before and after — recorded here so nobody re-derives a clearance from it. ## Gates 39 families derived from the **actual diff** (`node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack`, change set taken by the script itself from the merge base), every one run, every one exit `0`, reconciled `39 derived / 39 run / 0 UNRUN`. Plus the full `pnpm lint` union, which `dispatch-gates` does not name. Four of the 39 first exited `3`/`1` carrying PREREQUISITE-NOT-MET text — *"Nothing was measured: this gate exited before running a single check"* — because workspace packages were unbuilt. They were re-run to a real verdict after building `@objectstack/spec`, `@objectstack/formula`, `@objectstack/lint` and `@objectstack/client-react`. ⛔ Those refusals are recorded as not-measured-then-measured, never as a failed measurement. ## Changeset `skip-changeset`, **derived rather than assumed**: the one changed path lives under no package directory except the private monorepo root (`@objectstack/spec-monorepo`, `private: true`), so no published package's tarball can contain it whatever decides its contents; and no published package names `content/docs` in its `files[]`. ⛔ No label was written — this dispatch forbids label writes, so the label is the seat's to apply. ## Acceptance notes - **Only one carrier on this page.** The card asked for a grep rather than an assumption, since PR objectstack-ai#18872 introduced the whole section in one landing: probed whitespace-normalised for `de-duplicat`, `union could not`, `could not see`, `only what`, `echo` and `duplicate` across the page — the corrected sentence was the single restatement. No second one exists. - **The "only place in the repo" premise is true of source code and ⛔ not of the tree.** Re-derived at the branch base, whitespace-normalised: 13 non-doc occurrences across 13 files. Eleven are the settled shapes — one bound declaration, one prohibition, and nine quoted corrections in `compile.ts`, `lint.ts`, `validate.ts` and five CLI pin tests. **Two are still assertions**: `.changeset/18677-validate-per-package-authoring-pass.md` states *"By `compile.ts`' own description the survivors of that second pass are …"* and `.changeset/18778-lint-per-package-authoring-pass.md` states *"every finding that pass produces — … — in the build command's own words"*. Both attribute the sentence to source text that no longer says it, both are unreleased, and a changeset body ships verbatim into `CHANGELOG.md` as the text an upgrading agent greps. ⛔ Not fixed here — out of this card's declared one-file surface, and `.changeset/18677-…` is the claimed surface of in-flight card objectstack-ai#18823 (PR objectstack-ai#18867) for a different defect. Reported for filing. Clause-②: no --- _Generated by [Claude Code](https://claude.ai/code/session_01QCdUBjM47SxioST9z5Zwdf)_ Co-authored-by: Claude <noreply@anthropic.com>
…xit code (objectstack-ai#19369) Fixes objectstack-ai#18897 Clause-②: no ## The sweep, and what it found The card's discriminant: **a control is satisfied by an impostor whenever some mechanism other than the one under test can supply the asserted value in that fixture.** The method is ablation — delete the mechanism a control claims to pin, re-run, and read the colour. A control that stays green was never pinning it. 21 `packages/cli` fixtures were enumerated by the property (*a control that asserts a survivor or a resolution exists*), not by the three files the card body tabulates, and every one of them was ablated. Baseline first: 21 files / 225 tests, all green at the dispatch base `13d52947d8`. **One control family was impostor-satisfied, and its impostor is a THIRD mechanism — neither the echo nor the id tail.** `test/union-fold-command-parity.test.ts` claimed to pin `authoringRuleUnionStack`, the fold that makes the author-time rule table's INPUT non-empty on an option-B (`packages[]`-only) stack. It could not. Since objectstack-ai#18677 (`os validate`) and objectstack-ai#18778 (`os lint`) all three doors ALSO run `runPerPackageAuthoringRules`, which judges each package body as its own stack — and on this fixture that pass raises the SAME rule at the SAME path and refuses with the same exit code. | ablation | `union-fold-command-parity.test.ts` | |:--|:--| | `authoringRuleUnionStack` never folds | **6/6 GREEN** | | `runPerPackageAuthoringRules` yields no findings | **6/6 GREEN** | | BOTH of the above | **3 RED** (the three refusal cases) | Either mechanism alone kept every case green. Only deleting both turned the refusal cases red. The control was correct when it was written (objectstack-ai#17069) and became impostor-satisfied when a sibling pass was added to the same doors a year of cards later — which is the generalisation this PR contributes back to the card. ## The repair The two mechanisms are told apart by the finding's **pedigree**. `runPerPackageAuthoringRules` prefixes the `where` of every finding it raises with `package 'ID' — `; the union run renders `where` bare. Measured on that fixture, with only the fold's presence moving: ```text fold intact • object "ob_order" · field "ghost": … fold ablated • package 'com.example.ob' — object "ob_order" · field "ghost": … ``` So each refusal case now asserts that prefix is ABSENT, and a new **positive pedigree control** asserts the same suite can make it PRESENT — on a fixture whose only finding really is per-package-only (`core` declares `ob_account.industry`, `orders` owns the view that displays it: the falsifier shape PR objectstack-ai#18878 landed). Without that pair the negative assertion would be satisfied by a prefix this suite never produces at all. Verified in both directions, from the committed state: | run | result | |:--|:--| | repaired file at HEAD | 9/9 GREEN | | union fold ablated | **3 RED** — the three refusal cases (was 6/6 green before this change) | | per-package pass ablated | **3 RED** — the three pedigree controls; refusal cases correctly stay green | ### One half of the prescribed remedy is not available here, and the header says so The card's remedy shape is *a real falsifier AND a pedigree assertion*. The falsifier half cannot be built for this rule class, and that was measured rather than assumed: the per-package pass is a SUPERSET of the union run for reference rules (`utils/artifact-packages.ts`: *"the per-package run is STRICTER"*), because each body is judged with the artifact's own `packages[]` handed in as resolution context. A name no package provides therefore dangles in BOTH views, under every arrangement of packages. Two candidate union-only fixtures were built and probed; neither produced a finding the per-package pass could not also raise. The pedigree is the whole discriminant here, and the positive control is what makes it falsifiable. ## The full ablation table Every fixture is reported, including the ones that needed no change — a sweep that lists only what it changed cannot be checked for coverage. | fixture | mechanism deleted | red? | verdict | |:--|:--|:--|:--| | `test/union-fold-command-parity.test.ts` | `authoringRuleUnionStack` fold | **NO — 6/6 green** | **IMPOSTOR (third family)** — repaired here | | `test/union-fold-command-parity.test.ts` | per-package pass findings | **NO — 6/6 green** | same control, other mechanism | | `test/union-fold-command-parity.test.ts` | both of the above | YES — 3 | confirms neither alone is load-bearing | | `test/validate-per-package-authoring-parity.test.ts` | per-package pass findings | YES — 1/4 | correct (only the lit control; the parity pins go vacuous, as the card predicts) | | `test/validate-per-package-authoring-parity.test.ts` | the fixture's falsifier view | YES — 1/4 | correct; `expected 0 to be greater than 0` | | `test/build-text-face-advisory-count.test.ts` | per-package pass findings | YES — 3/5 | correct | | `test/build-text-face-advisory-count.test.ts` | the fixture's falsifier view | YES — 1/5 | correct | | `test/lint-per-package-authoring-parity.test.ts` | per-package pass findings | YES — 2/5 | correct (documented already-correct; verified, not inherited) | | `test/lint-per-package-authoring-parity.test.ts` | the fixture's falsifier view | YES — 1/5 | correct | | `test/lint-per-package-authoring-seam.test.ts` | per-package pass findings | YES — 2/6 | correct | | `test/lint-per-package-authoring-seam.test.ts` | the fixture's falsifier view | YES — 1/6 | correct | | `test/validate-per-package-authoring-seam.test.ts` | per-package pass findings | YES — 2/6 | correct for its narrower claim (it de-duplicates against an EMPTY union by construction, and its header says so) | | `test/per-package-dedup-positional-echo.test.ts` | per-package pass findings | YES — 5/6 | correct by construction | | `src/utils/collect-docs.package-docs.test.ts` | `docsPackageRefs` id-TAIL branch | YES — 12/32 | correct | | `src/utils/collect-docs.package-docs.test.ts` | `docsPackageRefs` FULL-ID branch | YES — 2/32 | correct — and the 30 staying green is the objectstack-ai#18962 reading reproduced | | `src/utils/artifact-packages.test.ts` | `packageBodyAsStack` resolution context | YES — 2/4 | correct; it already carries its own contextless CONTROL leg | | `test/lint-handwritten-checks-package-fold.test.ts` | `authoringRuleUnionStack` fold | YES — 5/7 | correct | | `src/utils/format.metadata-stats-package-fold.test.ts` | `authoringRuleUnionStack` fold | YES — 3/8 | correct (the top-level-only and empty-stack rows rightly stay green) | | `src/utils/stack-collections.test.ts` | `authoringRuleUnionStack` fold | YES — 3/16 | correct | | `src/utils/stack-collections.test.ts` | `resolveStackCollection` packages leg | YES — 2/16 | correct | | `test/info-detail-package-fold.test.ts` | `resolveStackCollection` packages leg | YES — 7/7 | correct | | `src/utils/nav-contribution-groups.test.ts` | `artifactPackages` reports no packages | YES — 6/9 | correct | | `src/utils/nav-contribution-groups.package-id.test.ts` | `artifactPackages` reports no packages | YES — 4/4 | correct | | `src/utils/permission-set-name-collisions.test.ts` | `artifactPackages` reports no packages | YES — 5/10 | correct | | `test/build-multi-package-artifact.e2e.test.ts` | `packageBodyAsStack` resolution context | YES — 1/7 | correct | | `test/build-multi-package-artifact.e2e.test.ts` | compile drops `packages` from the artifact | YES — 2/7 | correct | | `test/build-package-docs-attachment.e2e.test.ts` | `docsPackageRefs` id-TAIL branch | YES — 2/3 | correct | | `test/build-package-docs-attachment.e2e.test.ts` | `docsPackageRefs` FULL-ID branch | **NO — 3/3 green** | correct, NOT an impostor: its fixture directory is an id tail by construction and its header says so. It never claims the full-id spelling. Recorded as a coverage boundary, not a defect | | `test/compile-artifact-packages.e2e.test.ts` | `artifactPackages` reports no packages | **NO — 4/4 green** | correct, NOT an impostor: it reads the written artifact, which is not produced through `artifactPackages` | | `test/compile-artifact-packages.e2e.test.ts` | compile drops `packages` from the artifact | YES — 1/4 | correct; this IS its mechanism | | `test/validate-build-gate-parity.test.ts` | `authoringRuleUnionStack` fold body | **NO — green** | correct, NOT an impostor: it is a source-text WIRING pin with its own positive control (the export must exist) and fabricated negative controls. Ablating the body leaves the wiring, which is what it asserts | | `test/init.test.ts` | not ablated | n/a | EXCLUDED, with evidence: its `packages:` occurrences are the pnpm-workspace key, and no control in it asserts a per-package survivor or resolution. It appears in the card's keyword tally as a false positive | Every ablation was performed through `scripts/ablation-replace.mjs`, so each mutation carries its own on-disk proof (anchor count moved, blob hash changed) and each restore is proven by blob equality against HEAD plus an empty `git diff HEAD`. No ablation is left on disk. ## Fences observed - `packages/cli/src/utils/collect-docs.ts` is **not** edited by this PR — objectstack-ai#19248 holds that file. Two ablation legs mutated it transiently inside this worktree only, each restored with a proven blob match; the diff touches one file, `packages/cli/test/union-fold-command-parity.test.ts`. - The de-duplication key (objectstack-ai#18893) and the docs scan depth (objectstack-ai#18965) are out of scope. The key was reverted to its positional form for ONE measurement — the historical-impostor reconstruction described in the report comment — and restored. ## Verification At `e2b7d13d0f`: - `pnpm --filter @objectstack/cli exec vitest run --project unit` — 220 files / 3114 tests, all pass - `pnpm --filter @objectstack/cli exec vitest run --project integration` — 51 files / 430 tests, all pass, run in two halves for the foreground cap - `pnpm --filter @objectstack/cli typecheck` — pass - `node scripts/pm/dispatch-gates.mjs --commands` derived 46 gate families; all 46 run, all exit 0, reconciled with `--ran` carrying each exit code: *"46 derived families accounted for — 46 run, 0 NOT-MEASURED (a DERIVED zero)"* - `pnpm lint` (repo-wide, `eslint . --no-inline-config`) — exit 0, run in full rather than narrowed - `pnpm --filter '@objectstack/cli^...' build` and the full `turbo run build` over the package farm — both exit 0 `check:dual-build-cjs-loads` and `check:type-check-debt` first answered `PREREQUISITE NOT MET` (exit 3, which both scripts state is neither a pass nor a finding) because only the CLI closure had been built. Both were re-run after the prescribed full build and both exit 0. ## Acceptance notes Noted, not filed — observations from the sweep that are not reproducible defects, contract violations or authoring traps: - `test/build-package-docs-attachment.e2e.test.ts` cannot distinguish `docsPackageRefs`' full-id branch from its id-tail branch. That is deliberate in its fixture and stated in its own comments, so it is a coverage boundary rather than a defect. Next toucher: whoever changes `docsPackageRefs`' spelling set — the unit pins next door already cover both branches, and they go red. - `test/validate-per-package-authoring-seam.test.ts` and the non-vacuity case of `test/lint-per-package-authoring-seam.test.ts` de-duplicate against an EMPTY union by construction, so their survivors are not asserted to be union-invisible. Both headers say so explicitly; the stronger claim lives in the parity files. Next toucher: none currently in flight. - The card's keyword tally lists `test/init.test.ts` as an unexamined multi-package fixture. It is not one; the match is the pnpm-workspace `packages:` key. --- _Generated by [Claude Code](https://claude.ai/code/session_01QCdUBjM47SxioST9z5Zwdf)_ Co-authored-by: Claude <noreply@anthropic.com>
Fixes #18779
The card's central reading, reproduced first
Measured on
origin/maina43b9d0654over the repo's own two-package fixtureexamples/app-multi-package,os build --jsonexiting 0:[3]is[2], re-reported at the package-local index. Same rule, same entity,same message; the only difference is the top-level collection index, which is
the one coordinate
findingKeyhad no business comparing. 1 survivor, 1 echo,0 genuinely new — the card's reading holds, and the pass's strongest available
value statement was carried entirely by a duplicate.
Which option I took, and what the rejected one would have cost
I fixed the key, and the comments with it.
Fixing only the comments was the cheaper option and it was rejected on a
measurement, not a preference: the de-duplication exists so that "the author
cannot tell a real per-package finding from an echo" would stop being true, and
on the one fixture anyone can check, it was still true. Correcting the prose
would have left
os buildprinting4 author-time warning(s)for 3 distinctones, and left the repo with an accurate comment describing a filter that does
not filter. The cost of the option I took is that
os build/os validate/os lintreport one fewer line on such a project, which is an observable changeand is why this carries a changeset.
Triage settled the blocker: the "
os buildoutput stays byte-identical"constraint was #18769's PR contract, not a product contract, and its scope
ended with that PR.
Measured: exactly what stops being reported
Same fixture, all three doors, the key reverted and restored in place (on-disk
blob hash asserted both ways, tree verified clean after):
os build --jsonos validate --jsonos lint --jsonos lint --json --strictThe one line that stops being reported is the echo above. No input's verdict
moves, and that is structural rather than a property of this fixture: every
finding the de-duplication drops has, by construction, a finding with the same
key already in the reported set — the seed is the union run's findings, which
every door reports, and it grows only with per-package findings that themselves
survived.
os buildalready exits 1 on a union error before this pass runs,and
os lint --strictfails onerrors + warnings, a count that could onlyreach zero if the twin went unreported too.
Clause-②
Clause-②: no
Derived from the measured diff, not inherited. The dispatching claim left this
blank deliberately and expected
yes (widening)on the reasoning "fewerfindings reported ⇒
--strictrefuses less". Measured,--strictdoes notrefuse less:
failingdrops 4 to 3 and the verdict staysexit 1, because thedropped line's twin is still counted. No accept set moves in either direction,
and the diff adds no schema key, closed-set member, published export or registry
entry. Declaration carrier:
Clause-②-correction: 5723810598on the card.The falsified sentence — all carriers, re-derived
git grep "set the union could not see"ona43b9d0654finds more than thefour the dispatch named. Source and tests, all corrected here:
packages/cli/src/commands/compile.tspackages/cli/src/commands/lint.tspackages/cli/src/commands/validate.tspackages/cli/src/utils/artifact-packages.tspackages/cli/test/lint-per-package-authoring-parity.test.tspackages/cli/test/lint-per-package-authoring-seam.test.tspackages/cli/test/validate-per-package-authoring-parity.test.tspackages/cli/test/validate-per-package-authoring-seam.test.tsEach keeps the sentence as a quotation being corrected rather than deleting
it, so the next reader meets the correction where they would have met the claim.
TWO pins were being held up by the echo
Both are the same shape and both were repaired the same way — by giving the
fixture a survivor the union genuinely cannot see, never by relaxing an
assertion.
1.
validate-per-package-authoring-parity.test.ts(#18677). Itsnon-vacuity case went red: the planted fixture's only per-package survivor was
the echo, so filtering it left the parity cases comparing two empty sets.
2.
build-text-face-advisory-count.test.ts(#18780) — found by CI, notlocally, and the local gap was mine:
packages/cli'stestscript is a barevitest runwith no--projectfilter, so CI runs both projects, while Ihad run
--project unitplus only the two integration files this diff touches.This file is in the integration project and was never collected. Its lit
control asserts the fixture reaches the per-package pass and leaves a survivor:
bc_account.industryhad no consumer anywhere, so the union raised it too andthe "survivor" was that finding re-reported at the package-local index.
ordersnow owns the view that displays it. ⛔
toBeGreaterThan(0)is untouched — it iswhat stops #18780's equality pins from going vacuous.
Both preconditions now additionally assert the survivor's pedigree (the
field is named only behind the per-package prefix, and no union finding names
it), so neither control can be silently re-lit by a duplicate. The
warnings: 4reading in #18780's header is kept as the record of the defect it measured, with
a note that the same fixture reports 3 since this change.
What the key does not buy, measured rather than quoted
The key becomes position-insensitive, not collision-proof: two entries that
render the same
wherestill share a key, exactly as they already did whenevertheir indices happened to match.
That residue is measured, not sized by citing a neighbouring pin — which is the
move this card exists to correct.
packages/lint/src/data-model-rule-where-slot.test.tsholds something narrowerthan "every rule names its entity in
where": it fails any rule that puts abare config path in
where. Measured instead over every example stack inthis repo that parses today (
app-multi-package's built artifact,app-crm,app-showcase,app-todo): 45 registry rules, 103 findings, 103 distinctneutralised keys, 0 collisions, on
a43b9d0654.Verification
test/per-package-dedup-positional-echo.test.ts,key reverted to base in place:
2 failed | 4 passed(the ECHO and REAL_UNIONcases). Key restored:
6 passed. On-disk mutation proved by blob hash bothways; the test imports the mutated module through a relative source specifier,
so no build sits between mutation and assertion.
index to every index turns exactly one case red — the NESTED control. The
first draft of that control was built on a
field-no-consumerstwin andstayed green under the same ablation, so the fixture now carries a bare
unique: trueindex to give the control a finding whose path really has anested index. A control that cannot fail is decoration.
filename: the new pin fires no integration signal and is absent from the
derived integration population — UNIT tier, asserted by the file's own last
case.
dist, with controls both ways:the rewritten key is present in
packages/cli/dist/utils/artifact-packages.jsand
files[]shipsdist; positive controlrunPerPackageAuthoringRulespresent; negative control (a test-only symbol) 0 hits. Published ⇒ changeset,
graded
patch.dispatch-gates.mjs: 61 derived families, 61 run, 0 NOT-MEASURED, 0 UNRUN(exit codes recorded, none is 3).
pnpm lint(eslint . --no-inline-config, whole repo, not narrowed):exit 0 at
6a0a4df1b4.pnpm --filter @objectstack/cli testrun WHOLE — no--project, nofile list, exactly what CI runs: 267 files / 3485 tests passed, exit 0.
The same command at
15cc0db8d4reproduced CI's red first:3 failed | 264 passed (267), of which the one real assertion was [finding]os build's text face COUNTS per-package advisories it never prints — the closing line reads "4 author-time warning(s) — see above" above a list of 3 #18780'slit control (the other two were this worktree lacking
packages/cli/dist,which that pin refuses by name; cleared by building the package).
pnpm --filter @objectstack/cli typecheck: exit 0.check:dual-build-cjs-loadsfirst returned exit 3 — its own text says "This isNOT a pass: nothing was measured", 8 packages had no
distin this worktree. Ibuilt them and re-ran it: exit 0.
Acceptance notes
touched here:
.changeset/18677-validate-per-package-authoring-pass.mdand.changeset/18778-lint-per-package-authoring-pass.md.check:empty-changesetrefuses a PR that modifies a changeset present on the merge base, and names
this exact situation as its DELIBERATE CORRECTION class, whose remedy is "do
NOT restore it; get it confirmed on the PR". That confirmation is the
reviewing seat's to give, so the call is surfaced here rather than taken. The
correction itself is published in this PR's own changeset, which lands in the
same release. Note also that [finding]
os buildruns a per-package authoring-rule walkos validatedoes not — the residue #17069 left, in the same false-clean direction #18677's changeset says "After: both report 4",which this change makes read 3.
origin/main(the derivation's ownstaleness warning named
scripts/gen-sdui-manifest-node.mjsamong others);CI judges the merge.
bin/run-dev.jsneedsTSX_TSCONFIG_PATHpinned wheninvoked from an example directory, and says so itself in a good refusal — a
working command, not a defect.
Authored by Claude Code in session
session_01DvvamiacK328idtBYJBxV3(durable attribution kept in prose: the body-edit channel appends its own footer block, so a footer sent here would be stored twice).Generated by Claude Code