Repository navigation
test(rest): opt the OS_REST_LOG suite down, with every fault-log observer declaring its own level and a pairing gate - #18090
Conversation
Decision batch #128 item 4 ruled option B on this card: the packages/rest suite is opted down, every test file that observes the fault log declares its own level, and a pairing gate makes an undeclared observer a finding. This commit is item 1 alone, committed on purpose so the red population it produces can be measured against a real base before item 2 lands. The shipped default in packages/rest/src/log.ts is untouched and still gate-pinned at 'info'. Claude-Session: https://claude.ai/code/session_01TbSMtGzMrtPwh925wDEZd5 Co-authored-by: Claude <noreply@anthropic.com>
Ruling item 2. The 29 test files in packages/rest that observe the fault log -- a console.error/warn spy, or an import of the shim itself -- now declare 'info', the SHIPPED default, in their own setup instead of inheriting the suite's level. Measured on the committed opt-down, same command, same 191 files: OS_REST_LOG 'info' -> 191 passed, 2,095 indented `at ` frames OS_REST_LOG 'silent' -> 15 files / 28 tests RED, 0 frames + these declarations -> 29/29 observer files green again Claude-Session: https://claude.ai/code/session_01TbSMtGzMrtPwh925wDEZd5 Co-authored-by: Claude <noreply@anthropic.com>
…ding Ruling item 3, the half that keeps item 2 true. A test file in the package that owns the OS_REST_LOG seam is an OBSERVER when it spies on console.error/warn, installs a mock by assignment, or imports the shim; an observer must declare OS_REST_LOG in code, at a level loud enough that both shim sites still emit. Seam location and the level vocabulary are imported from check-rest-log-declared.mjs rather than re-implemented, so a renamed level moves both gates at once. Zero observers in the owning package is a MEASUREMENT FAILURE, not a clean bill: this detector recognises the spellings it knows, and a gate that finds nothing passes everything. 18-case self-test, both directions per rule. Wired additively into the root package.json and lint.yml. Claude-Session: https://claude.ai/code/session_01TbSMtGzMrtPwh925wDEZd5 Co-authored-by: Claude <noreply@anthropic.com>
check:pm-dispatch-gates named the new gate as an unlisted repo-root sweeper: its source walks the workspace to locate the seam but carried no path literal, so no card could be told it reads their surface. It now declares ROOT_DIR_WATCH_HINTS in the sibling's idiom, and five live-tree self-test cases hold the declaration against the walk it actually performs plus an anti-vacuity floor on the observer population. Self-test battery 18 -> 23 cases. Claude-Session: https://claude.ai/code/session_01TbSMtGzMrtPwh925wDEZd5 Co-authored-by: Claude <noreply@anthropic.com>
📓 Docs Drift Check1 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 — 14 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 0233f381b0c5240a98467fc2af98e978ec3c8ad7 && git checkout 0233f381b0c5240a98467fc2af98e978ec3c8ad7
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin b3d6918b86abcc31f82e963d2fabc37308bc4bb7 6d699ac6fbc49e6135ee89f11c793675fc2e99c1 && git checkout -B drift-repro b3d6918b86abcc31f82e963d2fabc37308bc4bb7 && git merge --no-ff 6d699ac6fbc49e6135ee89f11c793675fc2e99c1
node scripts/docs-audit/affected-docs.mjs --json b3d6918b86abcc31f82e963d2fabc37308bc4bb7 |
…claring packages (objectstack-ai#18121) Fixes objectstack-ai#17978 One shared vitest filter preflight, serving all eight packages that declare vitest `projects`. Direction A was ruled by the `domain:cli` execution seat under 验证策略 (`5656963807`) and confirmed by triage (`5657101795`), which also ruled the ownership question: > **引用或点名一个包不会把卡拉进它的车道,只有编辑它才会。** ## What changed - **New private workspace package `packages/qa/vitest-filter-preflight`** — a `domain:cli`-owned home, ⛔ **not** root `scripts/` and ⛔ not inside `packages/spec`. It holds the ONE transcription of vitest's `TestProject.filterFiles`; `packages/qa/refd-timer-testkit` is the shape precedent (private, no build, `exports` straight at `src`). - **`packages/cli` migrates onto it** and its local copy is deleted. Triage calls the move "a migration inside the owning lane". Measured behaviour-equivalent (below). - **Call sites in all eight configs.** `packages/spec`, `packages/objectql` and `packages/core` are **call sites only** — one file each, `vitest.config.ts`, 32 insertions and 0 deletions, no new export and nothing under `src/`. - **Two findings this card carried are folded in**: the notice's package name is now a parameter (it was hardcoded `@objectstack/cli`), and the no-`--project` typo case is covered and pinned. ## Three measurements that shaped the design **1. The precedent's shape does not transfer, re-confirmed at `a26a114d7`.** `Populations` takes concrete paths, so a glob cannot be a member. 82 package roots under `packages/`, **8** declaring `projects` — the same eight, no drift. `packages/cli` is 1 of 8 with two exact-path projects, and only as a by-product of a tier walk it already had (objectstack-ai#13504 / objectstack-ai#14554). The other 7 each pair one explicit list `L` with a glob complement of `L`, so the missing component is identical across all seven. `exactAndGlobPopulations` is that component. **2. The glob project's population is a deliberate SUPERSET** (the ruled shape, and it holds): a plain recursive walk of the `*.{test,spec}.?(c|m)[jt]s?(x)` family minus `node_modules`/`dist`, minus `L`. Matching nothing in a superset implies matching nothing in the real set, so a false accusation is structurally impossible and drift can only under-report. ⛔ Not vitest's own glob engine. Cost of the walk, measured: 0.3 ms (`core`) to 5.3 ms (`spec`, 507 test files) per config load. **3. ⛔ Consumers import the module by RELATIVE PATH, never by its bare package name — and that is a measurement, not a preference.** Vite bundles a config's relative imports through esbuild, which transpiles TypeScript; it externalises bare specifiers and leaves Node to load the resolved path, which here is a `.ts` file. The bare form works on this box and prints no warning — because Node 22.22 strips types by default. Re-run with `NODE_OPTIONS=--no-experimental-strip-types`: ```text failed to load config from .../packages/core/vitest.probe-bare.config.ts TypeError [ERR_UNKNOWN_FILE_EXTENSION]: Unknown file extension ".ts" for .../src/index.ts ``` This repo declares `engines.node: ">=22.0.0"`, so on a supported Node the bare form turns a silent-drop defect into a total harness outage for that package. The two escapes are worse: building to `dist` would make eight harnesses' **config load** depend on build state, and `pnpm --filter PKG exec vitest run FILE` — the invocation this card is about — runs no build; authoring it as `.mjs` would drop the types, and `CliParseResultOptions` is typed structurally precisely so a vitest upgrade renaming an option is a type error here instead of a silent decline. ## Per-package before/after — the preflight now speaks where it was silent `before` = `origin/main`'s config, run verbatim via `--config vitest.before.config.ts` beside the real one so `__dirname` still resolves to the package root. Every run exits **0** in both legs; the defect is silence, not failure. | package | glob side: dropped path named | exact side: dropped path named | no-`--project` typo named | healthy control | |:--|:--|:--|:--|:--| | `packages/types` | 0 → 4 | 0 → 4 | 0 → 2 | byte-identical | | `packages/rest` | 0 → 4 | 0 → 4 | 0 → 2 | byte-identical¹ | | `packages/runtime` | 0 → 4 | 0 → 4 | 0 → 2 | byte-identical | | `packages/spec` | 0 → 4 | 0 → 4 | 0 → 2 | byte-identical | | `packages/objectql` | 0 → 4 | 0 → 4 | 0 → 2 | byte-identical | | `packages/core` | 0 → 4 | 0 → 4 | 0 → 2 | byte-identical | | `packages/qa/dogfood` | 0 → 4 | 0 → 4 | 0 → 2 | byte-identical¹ | ¹ the only diff line is the `Duration` decimal. "`FILTER SELECTED NOTHING` present" reads **0 before, 2 after** in all three shapes in all seven (twice: once at config load, once from the `exit` listener). ⭐ This extends the card's reproduction from the 4 packages the first dispatch probed to **7 of 7**, including `spec`, `objectql` and `core`, which had never been probed — still **zero negative probes**, vitest **4.1.11**. `packages/qa/dogfood` also confirms the shape outside the `REPO_TESTS` idiom: its exact list is the inline `SHARED_SHOWCASE` and its glob project is `isolated`. **`packages/cli` — migration equivalence, not before/after.** Its `before` already carried a preflight, so the reading asked whether the move changed anything a reader sees. Same notice count (2), same dropped-path count (4), same `pnpm --filter @objectstack/cli` in the notice (4), healthy control byte-identical. The lost-run output differs in exactly **two words, twice**: `tier` → `project`. Deliberate — "tier" is this package's private vocabulary while "project" is vitest's own and is correct for all eight. A healthy narrowed run still contributes **zero bytes**: `renderLostFilterNotice` returns the empty string and no writer is called and no `exit` listener is registered. ## Tests, and the ablation that proves they can fail `pnpm --filter @objectstack/vitest-filter-preflight test` — **2 files, 73 tests, pass**. `typecheck` green, and `tsc --listFiles` puts **4 of 4** of the package's files in the program (`src/index.ts`, both tests, `vitest.config.ts`) — nothing hidden. `test/config-wiring-sweep.test.ts` is the anti-phantom sweep, and its population is **derived, not listed**: it walks `packages/` for package-root vitest configs, masks their comments, and requires every one that declares `projects` to invoke the preflight — so a ninth package is caught on the PR that adds it. The count is asserted as a floor, so a newcomer fails on its own wiring rather than on a number. **Ablation** (fix committed first; mutation proven on disk by occurrence count; `trap`-restored with absolute paths; restore proven by blob hash, not by an exit code): | leg | on-disk proof | sweep | |:--|:--|:--| | delete the `runFilterPreflight` call from `packages/types/vitest.config.ts` | `runFilterPreflight({` count 1 → 0; blob `a5d708a1` → `ef829c5c` | **exit 1**, `1 failed \| 40 passed`, fails on that package's row | | point `packageName` at another package | own-name count 1 → 0, wrong-name 1; blob → `aba7fc56` | **exit 1**, same row | | restore | — | blob back to `a5d708a1` (= `HEAD`), `git diff HEAD` empty | Predicted direction was "turns red" and that is what both legs did. The second leg is the pin for the carried finding: a shared notice bound to one package sends every other package's reader to a command that runs the wrong suite. ## Gates `node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack` against the actual change set, every command run, exit codes recorded to disk, then reconciled: ```text Run reconciliation — 91 derived, 91 run, 0 NOT-MEASURED, 0 UNRUN. ✓ dispatch-gates --ran: 91 derived famil(ies) accounted for — 91 run, 0 NOT-MEASURED (a DERIVED zero — all 91 recorded an exit code and none of them is 3). ``` Plus this lane's standing full union, which the derivation does not name: **`pnpm lint` exit 0** at `cd75179f35` (`eslint . --no-inline-config`, the whole repo — not a narrowing). Two gates needed work rather than just passing: - **`check:cross-package-test-inputs` went red and is satisfied, not routed around.** The sweep reads outside its package by construction. It now declares its radius in `scripts/cross-package-test-inputs.mjs` — three globs under `packages/` plus three `scripts/` files, with `heldBy` witnesses for the two globs built from a loop variable that the detector can see but cannot name — and `turbo.json` carries the matching `@objectstack/vitest-filter-preflight#test` inputs with `!**/node_modules/**`. ⭐ Side effect worth having: the suite's turbo hash now really covers every package's config, so a config change re-runs the sweep. - **`check:type-check-debt` first exited 3 (OOM at `--max-old-space-size=4096` on a shared box) — recorded as NOT MEASURED, not as a pass**, then re-run at 8192 and green: 77/81 packages type-checked, 55 raw errors, none above its recorded number, `surplus: none`. `check:type-check-coverage` green with the new package counted (81 packages, +1 vs the record). `check-engine-split-ratio --days 90` also exited 2 first — the gate refusing to measure on a shallow clone — and is green after `git fetch --shallow-since`, at 97.8%. Neither of those two numbers is about this diff; both are recorded so the zero above is real. No repo-level sweeper was added, so `check:pm-dispatch-gates` does not fire: the anti-phantom sweep is a vitest test inside the cli-owned home, which is also what keeps a gate-class file out of root `scripts/`. ## `skip-changeset` — measured, not assumed Nothing published moves. The new package is `private: true`. For each of the seven touched published packages, every `files[]` path was grepped for the subject symbols (`runFilterPreflight`, `exactAndGlobPopulations`, `matchesVitestFilter`, `testFilesUnder`): **zero hits in all seven**, with a positive control proving the grep works (`ObjectLogger` in `packages/core/dist`, `defineStack` in `packages/spec/dist`). `packages/cli`'s `files[]` is `["dist","README.md","CHANGELOG.md"]` and the moved module lived at the package root, outside all three. `packages/spec`'s `files[]` includes `src/**/*.zod.ts`; this diff touches no `.zod.ts`. `pnpm-lock.yaml` moved by **12 insertions and 0 deletions** — exactly one new `importers:` block for the new package, no version change to anything installed — and `pnpm install --frozen-lockfile` exits 0, which is the proof it is tool-written rather than hand-edited. ## Acceptance notes - **The seven new call sites are in no tsc program.** Only `packages/cli` type-checks its own `vitest.config.ts` (via `tsconfig.test.json`); the other seven declare `include: ["src/**/*"]` and their `tsconfig.test.json` siblings declare `["src*"]`, so no package-root config there is compiled by anything. Pre-existing — `packages/cli/tsconfig.test.json`'s header records the same state for cli before objectstack-ai#14554 graduated it — and not widened by this change, but it does mean a `@ts-expect-error` in any of those seven files would be a phantom check. Covered here by measurement instead: the wiring sweep asserts the call shape textually, and every one of the seven configs was loaded and run in both legs above. Noted, not filed — no declared contract requires a config to be type-checked, and the carrier is the `check:type-check-coverage` ratchet's own graduation work. - The wiring sweep is scoped to `packages/`. A package-root config outside that tree growing `projects` would not be swept — an under-report, the direction this card resolves uncertainty in. Scoping is what keeps the declared radius to one already-open root instead of opening `examples/` and `apps/` roots in `ci.yml`'s `crosspkg` filter. ## Fences Module home is `domain:cli`-owned, so the stop-clause did not trigger: nothing landed in root `scripts/` or inside `packages/spec`. No gate weakened. `packages/rest`'s `OS_REST_LOG: 'silent'` from `a26a114d7` (objectstack-ai#18090) is untouched in all three blocks. No file on objectstack-ai#17630's surface was touched — this diff edits no test file in `packages/qa/dogfood`. No governed surface is in the PR diff. --- _Generated by [Claude Code](https://claude.ai/code/session_01TbSMtGzMrtPwh925wDEZd5)_ --------- Co-authored-by: Claude <noreply@anthropic.com>
Fixes #17865
Decision batch #128 item 4 ruled option B. All four items of that ruling land here, and items 1 and 3 are in one PR by the ruling's own instruction: 「It lands in the same PR as 1; ⛔ 1 does not merge without 3.」
5651668684is the authority. Its measurements are good and were re-taken here rather than inherited.What landed
packages/rest/vitest.config.tsopts the suite down —OS_REST_LOG: 'silent', in all three blocks. A root-levelenvis inert for a project run, so both inline projects carry it too.'silent'is the quiet level and not'error':logError's own rank iserror, so at'error'the shim still speaks and 100% of the frame population survives.The 29 fault-log observers declare their own level —
'info', the SHIPPED default, so each file asserts against what a real caller gets:scripts/check-rest-log-spy-declared.mjsmakes an undeclared observer a finding by name — 23-case self-test, both directions per rule, wired additively (onepackage.jsonscript, onelint.ymlstep). Seam location and the level vocabulary are imported fromcheck-rest-log-declared.mjsrather than re-implemented, so a renamed level moves both gates at once.The shipped default is untouched.
REST_LOG_DEFAULT_LEVELis still'info'inpackages/rest/src/log.ts, still pinned bycheck:rest-log-declared(green here). Nothing in this PR changes production behaviour.The population, re-measured
The claim comment flagged one instrument bound:
git grep -l "console.error"returns 4, against the card's "~15 files / 28 assertions". Both readings are correct and they measure different things — 26 of the 29 observer files spell the spyvi.spyOn(console, 'error'), which contains no literalconsole.errorsubstring at all. 4 was a floor of a different measurement, exactly as the claim said.packages/restconsole.error/warn, install a mock by assignment, or import the shim)spyOn(console, 'error')spyOn(console, 'warn')'silent'with no per-file declarationrest-log-declared-level-seam.test.tsrest-expected-error-logging.test.tsaloneexpect(unhandledLogs()).toHaveLength(0)+ 1 siblingThe control that makes 15/28 a reading rather than a probe that could only answer one way: the same 191 files, the same command, at
'info'— 191 passed, 3,196 tests, 0 failures. So the reds are attributable to the level and not to the tree. And the 15 red files are a strict subset of the 29 observers, which is what says the observer detector is not under-counting: no file outside the detected population goes red.Volume
Same command (
pnpm testinpackages/rest), same 191 files, three states:atframes'info'(shipped default, i.e.maintoday)'silent', no per-file declarations'silent'+ the 29 declarations (this PR, at6d699ac6f)The card's 2,095 / 36.7% reproduced exactly.
The residual 76 frames are honest and attributable: every one of them belongs to two declaring files —
analytics-filter-refusal-envelope.test.ts(23 own-file frames, identical to its baseline count) andrest-endpoint-surfaces-served-only.test.ts(3) — which log through a window their spy does not cover. That residue is also the leak control: if a per-filevi.stubEnvleaked across files sharing a worker process, non-declaring files would still emit. They emit zero —rest-share-user-message-bypass-exits.test.ts145 to 0,execctx-consumer-census.test.ts112 to 0.Why item 2 is load-bearing — a four-leg ablation
The 15 red files announce themselves. The other half does not, and that is the half the card measured and refused as option C. Ablation on
rest-expected-error-logging.test.ts, mutating the production predicateisExpectedRouteErrortoreturn falseso that every expected 4xx logs[REST] Unhandled errorloudly:L3 against L4 is the whole point: with the declaration gone, those assertions return the same verdict whether the code logs nothing or logs everything. They are then true of a shim that never spoke. Each leg proved its mutation reached the disk by counting the exact text it touched (a bare
git diff --statwould go green on any other edit in the same round), and both files were restored fromHEADand verified byte-identical bygit hash-objectagainst the HEAD blob — not by agit diff HEADthat a checkout-from-ref can leave clean.Gates
node scripts/pm/dispatch-gates.mjs --commandswas derived against the actual 33-path change set, all 98 commands were run with exit codes recorded to disk before anything read them, and the run was reconciled with--ran.pnpm lint— the FULL union, not a narrowing — exit 0 at the final head6d699ac6f, 88s. No narrowing was claimed because none was needed.pnpm --filter @objectstack/rest typecheckexit 0;check:test-typecheckreports0 file(s) / 0 error(s)intest-typecheck-debt.json, so the 29 edited test files moved no TEST_DEBT number.PREREQUISITE NOT MET, which is NOT MEASURED and not a finding:check:dual-build-cjs-loadsandcheck:type-check-debt --re-measureboth refuse without a builtdist/for the whole workspace (43 and 6 packages named respectively). That prerequisite is CI'sBuild Core/ lint-job build; declared here rather than absorbed. The half ofcheck:type-check-debtmy diff can actually move is thepackages/resttest layer, and that is measured green above.One gate reported a defect in this change, and it was fixed rather than weakened.
check:pm-dispatch-gates's own self-test namedscripts/check-rest-log-spy-declared.mjsas an unlisted repo-root sweeper: the gate walks the workspace to locate the seam but carried no path literal, so no card could ever be told it reads their surface. Repaired the honest way — it now declaresROOT_DIR_WATCH_HINTSin the sibling's idiom, with five live-tree self-test cases holding the declaration against the walk it actually performs plus an anti-vacuity floor on the observer population (battery 18 to 23).check:pm-dispatch-gates,check:declared-population-liveandcheck:watch-hint-literalare all green after it.The new gate found nothing beyond the ruling's population —
29 of 193 test file(s) … every one of them declares its own OS_REST_LOG level. No overflow to report.Changeset:
skip-changeset, measuredpackages/rest'sfiles[]is["dist","README.md","CHANGELOG.md"]. Built the package and grepped those paths with a positive control:REST_LOG_DEFAULT_LEVEL(POSITIVE CONTROL — must be present)unhandledLogs(only in a changed test file)stubEnv(the declaration this PR adds)check-rest-log-spy(the new gate)*test*file indist/The remaining changed paths are the repo-root
package.json(private),.github/workflows/lint.ymlandscripts/— none published by any package. Control hits, subject symbols zero ⇒ no published artefact moves ⇒skip-changeset.Acceptance notes
Observations from the surface, noted and deliberately not filed — neither is a reproducible defect, a contract violation, or a metadata-authoring trap:
noted, not filed:the brace-matching and env-block readers are now in a third copy acrosscheck-registry-log-declared.mjs,check-rest-log-declared.mjsand this gate. The sibling's header already records the extraction as the right follow-up and states why it did not do it (moving that gate's self-test battery floor). This PR imported what mattered (seam location, vocabulary) instead of copying it, so the duplication did not grow in the load-bearing direction. Carrier if it is ever done: whichever PR next editscheck-registry-log-declared.mjs's reader.noted, not filed:two declaring files still emit 76 stack frames because their spy does not cover the window in which the fault is logged. Tightening each spy's window would take the suite to 0, but it edits assertions the ruling did not name, on files that are not defective — the frames are correct output at a correctly declared level. Carrier: a future volume card on this suite, if one is filed. No such card exists today.Related: #15484's execution list reads ruling comment
5651668684as the authority for its opt-down sentence. That card remains open and is out of scope here.Generated by Claude Code