Repository navigation
fix(cli): os lint runs the per-package author-time rule pass the other two doors already ran - #18813
Conversation
Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DvvamiacK328idtBYJBxV3
…tion for #18778 Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DvvamiacK328idtBYJBxV3
📓 Docs Drift CheckThis PR changes 1 package(s): 22 hand-written doc(s) name something this change touched — list omitted above 15 rows. Re-derive on the tree named below: ⛔ 6 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 c440eaf2c667a682fb11462f3b55cadae228a5f2 && git checkout c440eaf2c667a682fb11462f3b55cadae228a5f2
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin ac720a986593f1983e802d49f81d77989277ea21 45b22427a91f66ad090ec6ba3d64b9642a63a6d0 && git checkout -B drift-repro ac720a986593f1983e802d49f81d77989277ea21 && git merge --no-ff 45b22427a91f66ad090ec6ba3d64b9642a63a6d0
node scripts/docs-audit/affected-docs.mjs --json ac720a986593f1983e802d49f81d77989277ea21
|
Contract reviewServed-tier: Isolated at-tier contract review of PR #18813 (card #18778), declared ① Derived judgmentsDeclared arm tested: narrowing. Each accept-set or public-surface change the diff implies, named and judged one by one:
Red/green from the committed state, mine: unit ② Semver levelChangeset ③ Boundary flags
CI on the head at the time of this record: 45 check runs, 37 Implemented-by: Independence, stated plainly: the implementation claim (5722028692) is VERDICT: PASS Generated by Claude Code |
…ojects` package (objectstack-ai#18853) Fixes objectstack-ai#18788 Clause-②: no⚠️ **This is a RESUMED delivery.** A previous run's container was killed after it pushed one commit and before it opened a PR, ran a single test, or measured anything. That commit was treated here as an untrusted proposal: every claim in it was re-measured, one false statement in it was corrected, and its incomplete half was rewritten. What was verified and what was rewritten is itemised at the foot of this body. --- ## 1. The premise, reproduced FIRST — with four lit controls ⛔ The card's whole subject is that a reading which cannot fail is indistinguishable from one that passed, so the premise was reproduced before anything was built on it, against the BASE tree (`ad1f94e8ec`, this branch's merge base) rather than against the branch's own fix. The pre-fix `packages/cli/vitest.config.ts` was read out of that commit verbatim (`git hash-object` of the copy `837c2d1319` equals the BASE blob) and run under a temporary name, so no tracked file was mutated; the probe files were untracked and `git status` was empty afterwards. Probe: a `beforeAll` sleeping 500ms. vitest `4.1.11`. | # | run | exit | evidence | |---|---|---|---| | R1 | `@objectstack/cli`, `--project unit`, `--hookTimeout=1` | **0** | `Test Files 1 passed` | | R2 | `@objectstack/cli`, no `--project`, `--hookTimeout=1` | **0** | `Test Files 1 passed` | | R3 | ⭐ **LIT** `@objectstack/plugin-dev` (no `projects`), `--hookTimeout=1` | **1** | `Error: Hook timed out in 1ms.` | | R4 | ⭐ **LIT** `@objectstack/cli`, `--testTimeout=1`, 500ms in the test BODY | **1** | `Error: Test timed out in 1ms.` | | R5 | ⭐ **LIT** `@objectstack/cli`, `beforeAll(fn, 1)`, no flags | **1** | `Error: Hook timed out in 1ms.` | | R6 | ⭐ **LIT** `@objectstack/cli`, root `test.hookTimeout: 1` in the config | **1** | `Error: Hook timed out in 1ms.` | ⇒ The two zeros are READINGS. R3 proves the flag works where no `projects` narrowing exists; R4 proves the CLI-to-project path is ALIVE in this very package under the same config and invocation shape, so the defect is ALLOWLIST MEMBERSHIP and nothing wider; R5 and R6 prove the VALUE reaches the hook by both other spellings, so `hookTimeout` is not unenforceable here — only the CLI spelling of it is inert. ### The mechanism, read off the installed runner `resolveProjects` in `vitest/dist/chunks/cli-api.CnMVyzaz.js` builds each project's test config as `test: { ...options.test, ...cliOverrides }`, where `cliOverrides` is a CLOSED allowlist of **twenty** CLI option names. `testTimeout` is on it. `hookTimeout` and `teardownTimeout` are not. Everything else reaches the ROOT config and stops there. --- ## 2. The three routes, and why route 3 ⛔ The card deliberately does not rule on which side moves, and neither did the dispatch. All three were costed against the measurements above. **Route 1 — stop relying on the flag, rewrite the witness.** Available and measured to work: R5 and R6 are exactly the two spellings that DO bite in a `projects` package. What it cannot do is stop the NEXT reader reaching for the flag — this repo's own prior art reaches for it, and a dispatching seat handed it over as a suggestion on the round that filed this card. A remedy that is a habit is not a remedy. ⭐ Route 1 is therefore not discarded: it is what the refusal text NAMES, which is the one way a convention gets enforced rather than recommended. **Route 2 — forward the value into the projects.** The cheapest spelling is not even per-project: every one of these configs declares `extends: true`, so writing the parsed value into the ROOT `test` block reaches all of them — R6 measures exactly that, by hand. It works. Rejected on three counts: - **it invents a precedence rule vitest does not have, and then hides the day vitest acquires one.** For an allowlisted name vitest spreads the CLI value last, so the CLI wins over the project's own block; for a name vitest never carries there is no rule at all, so a forward has to pick one, for eight packages, on this repo's authority. And the pick goes stale silently: the day a vitest bump adds `hookTimeout` to `cliOverrides`, vitest's own override and the forward are BOTH live and nothing reddens. The refusal has the opposite failure mode — its transcription pin equals-checks the installed array, so that same bump turns it RED. **This is measured, not argued:** ablation B below puts `hookTimeout` on the transcribed allowlist and the pin goes red on cue. - **it fixes exactly the names it enumerates.** The allowlist is twenty names and vitest's CLI surface is not. A forward built for three names leaves the rest handing out the same false greens — the identical defect, narrower, now behind something called a fix. Widening the forward to every CLI option is a second transcription of vitest's option table, which is the multiplication `packages/qa/vitest-filter-preflight` exists to stop. - **it is a lenient consumer-side accommodation for an upstream contract**, which Prime Directive objectstack-ai#12 refuses in the form it usually takes. **Route 3 — refuse the run, and name the spellings that do bite. CHOSEN.** Refusing costs nothing that was working: the flag measures nothing today, so no run loses a capability, and a run naming no such flag is byte-identical to the run it is today (pinned in both directions). What it buys is that the false clearance becomes impossible to READ — and because the notice carries route 1's two measured spellings, the reader leaves with the instrument they came for rather than a bare refusal. --- ## 3.⚠️ Why the fix lives in `packages/qa/vitest-filter-preflight`, not in `packages/cli` The dispatch order neither scoped this package in nor out, so here is the measurement that justifies it rather than a preference. The fix needs a transcription of a private vitest code path (the twenty-name allowlist). This repo has already ruled once, in objectstack-ai#17978, on where such a transcription goes and why there is exactly one: `matchesVitestFilter` — a transcription of `TestProject.filterFiles` — was pulled out of `packages/cli` into this package precisely because eight configs needed it and eight copies would drift from vitest and from each other, with every drift failing SILENTLY GREEN. That is the same failure direction as this card's defect. The concrete measurements that follow from putting it there: - **Cost to the consumer: ZERO new import lines.** All eight configs already import this module by relative path; the new function is re-exported from `src/index.ts`, so each config's wiring is one name in an existing brace list plus the call. - **The anti-phantom sweep already exists and already DERIVES its population.** `test/config-wiring-sweep.test.ts` walks `packages/` for configs declaring `projects` and requires the wiring. Putting the refusal anywhere else means either no sweep for it, or a second sweep. - **The alternative was costed.** Inlining the transcription in `packages/cli/vitest.config.ts` fixes one package and makes the other seven a copy-paste job — which is objectstack-ai#17978's finding, reintroduced. ⇒ The package is justified by the prior ruling it is the product of, not by convenience. ⛔ If the seat still wants it narrowed, the narrowing is mechanical (move the module into `packages/cli`, drop seven wirings) and this section is the argument to overrule. --- ## 4. ⭐ The population is EIGHT, and all eight are wired The card names `packages/cli`; triage measured the population at eight and told the fixer so in its own words (「修的时候**总体是 8**,⛔ 不是 1」). The defect is a property of declaring `projects`, not of `packages/cli`, and that was measured in a SECOND package rather than assumed: | run | exit | evidence | |---|---|---| | `@objectstack/types`, 500ms `beforeAll`, `--hookTimeout=1` | **0** | `Test Files 1 passed` | | ⭐ **LIT** `@objectstack/types`, `beforeAll(fn, 1)`, no flag | **1** | `Error: Hook timed out in 1ms.` | The interrupted commit wired `packages/cli` only. Wiring one and leaving seven would have shipped the shared module's own sentence — "eight packages in this repo declare vitest `projects`, every one of them has this defect" — as a documented, unfixed defect in seven places, with nothing to catch them, since a sweep that covered the new call would then have needed a maintained exemption list for the other seven: the exact artefact objectstack-ai#17978 removed. ⛔ The wiring is NOT the interesting half. A config can contain the call in code position and still refuse nowhere — a swallowed throw, a wrong argv source, a re-export resolving to a stub. So `config-wiring-sweep.test.ts` grew a BEHAVIOURAL leg and asks a real `vitest` child, per package, in both directions: ``` PACKAGE clean(exit/refusal) --hookTimeout=1(exit) names-own-pkg packages/cli exit=0 refusal=0 exit=1 refusal=1 ownname=2 packages/core exit=0 refusal=0 exit=1 refusal=1 ownname=2 packages/objectql exit=0 refusal=0 exit=1 refusal=1 ownname=2 packages/qa/dogfood exit=0 refusal=0 exit=1 refusal=1 ownname=2 packages/rest exit=0 refusal=0 exit=1 refusal=1 ownname=2 packages/runtime exit=0 refusal=0 exit=1 refusal=1 ownname=2 packages/spec exit=0 refusal=0 exit=1 refusal=1 ownname=2 packages/types exit=0 refusal=0 exit=1 refusal=1 ownname=2 ``` The refusal must name THAT subject's own manifest name — that is what makes the non-zero a reading, since a spawn failure, an unresolvable config or a missing build all exit non-zero too and none of them prints this text. ⛔ The clean-run leg is not optional: without it every other assertion in the file is satisfied by a config that refuses EVERYTHING, which would be a far worse defect than the one being fixed. `--filesOnly` globs test paths and never imports them, so neither leg depends on build state. 16 children, ~9s wall. --- ## 5. Ablations — these pins can fail, and it was watched Each leg: mutate → prove the mutation reached disk by an anchored count → run → restore → prove the restore by `git hash-object` equality against the HEAD blob. ⛔ No `dist` leg applies and that is a measurement, not an omission: `@objectstack/vitest-filter-preflight` is `private: true`, has **no** `build` script, and its `exports` points straight at `src`, which every consumer imports by relative path — there is no built artefact for a mutation to fail to reach. **Ablation A — delete the wiring from `packages/types/vitest.config.ts`.** On-disk proof: call sites before=1 after=0. Result: `Test Files 1 failed`, `Tests 2 failed | 63 passed` — exactly the two new legs for that subject, the source assertion and the behavioural refusal. Restored to blob `c2f03814c52b71f88fa1f8c90938a7db102e4e7e` (== HEAD). **Ablation B — put `hookTimeout` ON the transcribed allowlist.** On-disk proof: occurrences before=0 after=1. Result: `Test Files 2 failed | 1 passed` — the transcription pin red (`equals the cliOverrides array in the installed vitest`), the pure-function legs red, and **all eight** behavioural refusal legs red. ⭐ The discriminator legs (`--testTimeout` is not refused; the notice is the empty string when nothing is dropped) stayed GREEN, which is the right direction: the ablation removed a refusal, it did not make the module refuse everything. Restored to blob `6b078d5e61ee51113ec35d950caffe75ed09f55d` (== HEAD). **Restore leg:** `git diff HEAD --stat` empty, suite green again — `Test Files 3 passed (3)`, `Tests 111 passed (111)`. --- ## 6. Changeset: `skip-changeset`, and it rests on a measurement The dispatch asked this be decided by measuring, ⛔ never inferred from `files[]` or from a file's look. Two independent instruments, both with lit controls: **(a) The manifests.** `@objectstack/vitest-filter-preflight` is `private: true`, carries no `files[]` and has **no `build` script** — nothing in it is ever shipped. `@objectstack/dogfood` likewise. **(b) npm's OWN packing** (`npm pack --dry-run --json`), per published package, asked whether any path this PR changes is in the list npm would actually ship: | package | shipped files | `vitest.config*` | `test/*` | the card's test file | ⭐ POS CTRL (`package.json` + `dist/*`) | |---|---|---|---|---|---| | `cli` | 533 | 0 | 0 | 0 | **529** | | `core` | 18 | 0 | 0 | 0 | **15** | | `objectql` | 18 | 0 | 0 | 0 | **15** | | `rest` | 10 | 0 | 0 | 0 | **7** | | `runtime` | 10 | 0 | 0 | 0 | **7** | | `spec` | 2021 | 0 | 0 | 0 | **217** | | `types` | 16 | 0 | 0 | 0 | **13** | ⇒ Every one of this PR's thirteen paths is a vitest config, a test file, or a file in a private package. Zero of them ship, and the positive control is lit in every row, so the zeros are readings. **`skip-changeset` is the measured verdict**, and the label is applied on this PR. --- ## 7.⚠️ `objectstack-ai#18520` is NOT absorbed, and the TIER split did not move `packages/cli/vitest.config.ts` is touched, and the dispatch required this be stated explicitly. **The `unit` / `integration` TIER split is untouched** — no change to `vitest-tiers.ts`, to `integrationTestFiles` / `unitTestFiles`, to `OS_TEST_TIERS`, or to either project's `include`. The only edit to that file is the preflight call above the `defineConfig`. objectstack-ai#18520's other half — "nightly-only tiers can never be reddened by a PR" — is reported, not done. ⛔ Not widened into. The new `packages/cli` pin lands in the `unit` tier by the derivation, not by a name: it imports `node:child_process` to ask vitest a question, which is the same classification `vitest-tiers-partition.test.ts` already carries. --- ## 8. ⛔ Files the dispatch fenced off — untouched `packages/cli/src/commands/compile.ts` (objectstack-ai#18780 in flight), `packages/cli/src/utils/artifact-packages.ts` and `packages/cli/src/commands/lint.ts` (objectstack-ai#18813) appear in no hunk of this branch's own commits. They appear in `git diff BASE...HEAD` only because `origin/main` was MERGED in — never rebased, never force-pushed, never amended, per the dispatch's hard constraint. --- ## 9. Verification - `pnpm --filter @objectstack/vitest-filter-preflight test` → **`Test Files 3 passed (3)`, `Tests 111 passed (111)`** - `pnpm --filter @objectstack/cli exec vitest run --project unit test/hook-timeout-override-refusal.test.ts` → **`Test Files 1 passed (1)`, `Tests 4 passed (4)`** - `pnpm --filter @objectstack/vitest-filter-preflight typecheck` → clean - The derived gate family (`node scripts/pm/dispatch-gates.mjs --commands`, 62 commands over the measured 13-path change set) — results in the round report. `check:cross-package-test-inputs`, `check:test-source-alias`, `check:tier-file-adoption`, `check:cli-test-child-env`, `check:published-files`, `check:nul-bytes` and `check:comment-mask-corpus` are the ones this diff most directly implicates; all green. - `check:dual-build-cjs-loads` reports **`PREREQUISITE NOT MET`** in this worktree — it reads built output and names `studio`, `client-react` and the connectors, packages this diff never touches. ⛔ Recorded as NOT MEASURED, not as a pass and not as a failure. CI checks out and builds fresh. --- ## 10. What was VERIFIED from the pre-existing commit, and what was REWRITTEN **Verified and kept** — `src/project-cli-override-preflight.ts`'s mechanism (the twenty-name allowlist, transcribed verbatim and confirmed against the installed `cli-api` chunk, order included); its refusal-over-forwarding conclusion; `test/project-cli-override-preflight.test.ts` (the transcription pin, the discriminator and the silence contract — all now actually run, 111 green, and ablated); the `src/index.ts` re-export; the `packages/cli` wiring and its behavioural pin. **Corrected** — the header's account of vitest's merge order. The commit claimed a forwarded value would override a project's own timeout "in the opposite direction from the one vitest documents". Read off `resolveProjects`, the CLI value is spread LAST for an allowlisted name, so the CLI already wins; the true objection is that vitest has NO rule for a name it never carries, so a forward invents one and then goes silently stale. The route-2 rejection is rebuilt on the three counts that survive the correction. **Rewritten / added** — the population. The commit fixed one of eight packages and measured none of it. Seven configs wired, the sweep grown a per-subject behavioural leg in both directions, the defect measured in a second package, both ablations, the npm-pack changeset measurement, and the declared division of labour between the card-local `packages/cli` pin and the population sweep. 🤖 Generated with [Claude Code](https://claude.com/claude-code) https://claude.ai/code/session_01DvvamiacK328idtBYJBxV3 --- _Generated by [Claude Code](https://claude.ai/code/session_01DvvamiacK328idtBYJBxV3)_ --------- Co-authored-by: Claude <noreply@anthropic.com>
…, so the input each door hands them is written as its own axis (objectstack-ai#18872) Fixes objectstack-ai#18815 Clause-②: no Docs-only. One file, +69 / -0: `content/docs/deployment/validating-metadata.mdx`. ## The gap, restated at the scope it is actually at The "one gate, four doors" table has exactly two axes: its **rows** are rule **families**, its **columns** are the four doors. A check mark says that door runs that family. It cannot say what that door **hands** the family to judge — and that input is where the three CLI doors have drifted three separate times. So the two-pass shape they run (the union fold over the artifact's collections, then the same table once per `packages[]` entry) was documented nowhere, for any door, across the three landings that wired it. This PR adds a third axis to the page as its own `###` section, plus two short scoping paragraphs: one above the table naming what the table's two axes are, one under the `one-directional` parenthetical saying which scope that sentence is written at. ## The card's gating claim, re-derived rather than relayed The card rests on "all three doors now run the per-package pass". Verified on the branch base `fb2bccfc96` by reading the call sites, not by trusting the card: | door | call site | wired by | |---|---|---| | `os build` | `packages/cli/src/commands/compile.ts:482` | objectstack-ai#16611 | | `os validate` | `packages/cli/src/commands/validate.ts:421` | objectstack-ai#18677 (PR objectstack-ai#18769, merged 2026-09-17T21:18:28Z) | | `os lint` | `packages/cli/src/commands/lint.ts:722` | objectstack-ai#18778 (PR objectstack-ai#18813, merged 2026-09-17T23:32:18Z) | All three reach the one loop, `runPerPackageAuthoringRules` at `packages/cli/src/utils/artifact-packages.ts:189`. Both PRs are merged and are ancestors of this branch's base. **The successor condition holds.** Measured end to end as well, not only read: `validate-per-package-authoring-parity`, `lint-per-package-authoring-parity` and `union-fold-command-parity` (spawned, real binaries) — 3 files, 15 tests, exit 0. ## The seat's premise probe, re-run structurally The dispatch handed one grep (`per-package | union fold | union-folded | stack tier` => 0 hits, lit control `os build|os validate|os lint` => 25). Re-run as a structural read rather than a keyword miss, and widened to the whole docs tree: - on the page, every occurrence of "tier" is a **rule** tier — `pre-parse tier` (:519), `react tier` (:335), `tier findings` (:63), the ADR-0049 `tier programme` (:241). None is a stack shape. - across `content/docs/**`: zero hits for the per-package pass or the union fold in that sense (the 13 `per-package` hits are changelogs, doc-book grouping, capability prefixes, one-app-per-package ADRs), against a lit control of **64** files naming the three commands. Same probe over the published `skills/` catalog: zero, lit control 14. So the gap is real and is repo-wide, not just table-shaped. ## Falsification — the card's own word for the axis is already taken, and for a DIFFERENT axis The card and the dispatch both name the second axis the **stack TIER**. That phrase is already spent in this repo, on something else: - `packages/lint/src/authoring-rules.ts:1671` — *"The stack tiers a command has in hand when it runs the registry"*, documenting `AuthoringRuleRun`, whose members are `normalized` and `parsed`; - `:215` — *"Which tier of the stack a rule reads"*, for the same two; - `:213` — and `AuthoringRuleTier` is a third sense again, `'gating' | 'advisory'`. Those are not near-synonyms of the axis this card is about; they are **orthogonal to it**. `runPerPackageAuthoringRules` hands BOTH stack tiers the same per-package stack (`artifact-packages.ts:230-231`, `normalized: asStack, parsed: asStack`), and `lint.ts:653` says the mirror thing for the other pass — *"Both tiers are handed the UNION-FOLDED stack"*. A page that wrote "stack tier" for the union-vs-per-package axis would therefore have created a **new** collision, in the very module the table cites as its source, of exactly the class this card exists to close. ⇒ the page names the axis for what it is — the **input** each door hands the rule table — and names the two passes what the code already calls them: **the union fold** and **the per-package walk**. The card's *requirement* is met (family and input are kept apart, explicitly, at the one paragraph where they were conflated); its *wording* is deliberately not adopted. Flagging it rather than quietly diverging. ## Also written out, because it is a third thing again The fourth door is on **neither** pass. A runtime write is one item, so the publish gate evaluates differentially (context alone, then with the item grafted in, objectstack-ai#4463) and narrows its resolution context to the written item's **package closure** (objectstack-ai#9612) — which changes what a rule can *resolve*, never what it judges, and iterates no `packages[]`. "Runs per package" therefore names one thing at the three CLI doors and another at this one, and the `runtime publish` column says neither. Sourced from `packages/lint/src/runtime-gate.ts:208-259`, not from the card, which explicitly did not assert what that row should say. ## Evidence - **Derived gate families** — `node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack` derived **39**; all 39 run, exit codes recorded and reconciled: `--ran` => *"39 derived famil(ies) accounted for — 39 run, 0 NOT-MEASURED (a DERIVED zero — all 39 recorded an exit code and none of them is 3)"*. Five first reported `PREREQUISITE NOT MET` (exit 3 / exit 1, nothing measured) and were re-run to exit 0 after building `@objectstack/spec`, `@objectstack/formula`, `@objectstack/lint` and `@objectstack/client-react`: `check:doc-formula-expressions`, `check:doc-security-posture`, `spec check:docs`, `spec check:skill-examples`, `check:docs-transcript-drift`. - **`pnpm lint`** — the family `dispatch-gates.mjs` does not name. Run **whole**, not narrowed: `eslint . --no-inline-config`, **exit 0**, 82s, at `777cd9aeca`. - **MDX compiles** — `@mdx-js/mdx` 3.1.1 `compile()` on the edited page, exit 0. - **Anchors** — `pnpm check:doc-anchors` exit 0; the new heading adds `#what-each-door-hands-the-rule-table` and renames nothing, so the existing inbound links to `#the-one-gate-four-doors` (`cli.mdx:650`, `deployment/index.mdx:153`) are untouched. - **Control bytes** — `grep -naP` over the edited file: no hits; `pnpm check:nul-bytes` exit 0. - **Publishing surface, measured with controls both ways** — 83 manifests, 70 non-private. Manifests whose `files[]` mentions `content`: **0**. Positive control, `dist`: **70 of 70**. Manifests with no `files[]` at all (whole dir ships): **0**. `content/docs` is at the repo root, outside every package directory, and the one manifest that names a `content/docs` path at all does so in its `description` string (`plugin-webhooks`), not in `files[]`. ⇒ nothing published moves ⇒ `skip-changeset`, applied to this PR. ## Acceptance notes *Out of scope, noted and not filed:* - `content/docs/getting-started/examples.mdx` §"A project is a multi-package artifact" is the page that teaches `packages[]` to authors and would be the natural second home for a one-line pointer at the per-package pass. It does **not** enumerate the doors and contradicts nothing here, so triage's escalation condition ("a second page that cannot express the axis ⇒ p1 plus a structural card") is **not** met — measured, not assumed. Successor: none; noted for whoever next edits that section. - `content/docs/deployment/cli.mdx` carries the doors in two places (`:649`, `:668`, `:1485`) and defers to this page's matrix by link for the detail. Those three statements are true at both passes now, so nothing there is falsified by this PR and nothing was edited there. Successor: none. --- _Generated by [Claude Code](https://claude.ai/code/session_01DvvamiacK328idtBYJBxV3)_ Co-authored-by: Claude <noreply@anthropic.com>
…validate --strict` narrowing an at-tier review exhibited (objectstack-ai#18867) Fixes objectstack-ai#18823 Clause-②: no `.changeset/18677-validate-per-package-authoring-pass.md` — PR objectstack-ai#18769's still-pending, still-unreleased entry — declined its `**BREAKING**` banner on the strength of a negative, verbatim: *"⛔ **not** declared breaking, because the narrowing could not be exhibited and is bounded by an existing gate"*, alongside *"**No newly-refused input could be exhibited on any fixture**"*. The negative is false. This PR edits that one file: it adds the banner, the measured table, the mechanism that explains why the union fold cannot see the finding, and the ADR-0087 disposition the banner owes — and it narrows the sentence an upgrader would have been misled by. ⛔ A forward edit to an **unpublished** file. ⛔ Not a rewrite of landed history, and ⛔ not a ruling on objectstack-ai#18677's landed `Clause-②: no` — see "Boundaries" below. ## 1. The clock, re-confirmed at this branch's base `be7aeb8275` — ⛔ not inherited The card is `priority:p1` because the entry is unconsumed. Three readings, all taken here: | reading | value | |:--|:--| | the file on `origin/main` | present (`git ls-tree`) | | `npm view @objectstack/cli version` | **17.4.0** | | `packages/cli/package.json` version on `origin/main` | **17.4.0** — equal, so no release has bumped it | | the entry's own distinctive sentence in `packages/cli/CHANGELOG.md` | **0** hits | ⇒ unconsumed. Amending it now costs a diff; amending it after the release that consumes it costs a published version number that cannot be recalled. ## 2. ⭐ The reviewer's table, REPRODUCED here — ⛔ not copied The measurement is PR objectstack-ai#18813's isolated at-tier contract review (record `5722342660`, finding **F2**). The card's first instruction is to reproduce it rather than build on it. Driven in this worktree, on the `CONFIG_FLIP` two-package fixture planted verbatim from `packages/cli/test/lint-per-package-authoring-parity.test.ts` (lines 115-166, sha256 `d5fb835ac5…`), through `packages/cli/bin/run-dev.js` with tsx: | `os validate --json --strict` on `CONFIG_FLIP` | exit | warnings | |:--|:--|:--| | `packages/cli/src/commands/validate.ts` restored to its pre-objectstack-ai#18769 blob `bafa54b07f` | **0** | 0 | | at head (blob `340cec6253`) | **1** | 1 | **It reproduces.** The one warning is `field-no-consumers` at `package 'com.example.ppflip.core' — object "pp_account" · field "industry"`. Ablation hygiene, because a mutation that never reached disk reads exactly like a clean run: - the mutation was proven on disk before the CLI was driven — `git hash-object` on the file returned `bafa54b07f…`, equal to the target blob, and the marker count `runPerPackageAuthoringRules` moved **3 → 0** in that file; - restore is `git checkout HEAD -- packages/cli/src/commands/validate.ts` from a `trap … EXIT INT TERM` with an absolute path, verified by hash equality back to `340cec6253…` **and** by `git diff HEAD` being empty, not by an exit code; - `git status --porcelain` is empty after the run. ⛔ No landed code is modified by this PR — the ablation is a one-off measurement, and `validate.ts` is untouched in the diff. Extra reading this PR took that the record did not, and the changeset now states: the **default (non-strict)** face of `os validate --json` on the same fixture at head is **exit 0 with 1 warning**. So on this fixture only the `--strict` door moved. ## 3. What the file now says - `**BREAKING**` banner naming the door that moved: `os validate --strict` can now fail a project it passed before. - The before/after table above, plus the named finding and the fact that `os build` already reports it — so the narrowing is still bounded by the command that ships. - The remedy an upgrader owes: drop `--strict` to keep the old verdict, or fix what the per-package pass reports. - ⭐ The mechanism, which is what replaces the false negative: `packageBodyAsStack` hands each package the artifact's whole `packages[]` as **resolution context**, so a cross-package *reference* still resolves and the reference-integrity rules stay quiet — but a **reachability** rule asks what the *stack* reads, and per package the stack is that one package's own body. A field whose only consumer lives in a sibling package is live to the union run and inert to the per-package run. That is the shape the earlier fixtures could not produce, and it is why "could not be exhibited" was a statement about the fixtures rather than about the property. - An `adr-0087:` disposition marker, in the HTML-comment form the gate reads, claiming `not-required (no-migration-prescription)`: nothing an author writes changes, so `objectstack migrate meta` has nothing to rewrite. - The sentence "nothing that builds today stops validating" is removed. It was true of the default face and false of `--strict`, and it is the sentence the card names as the one that would mislead an upgrader. - Level is untouched at `minor`. ⛔ Nothing here asks for `major`; the launch window refuses it and the banner plus the disposition are what carry breaking-ness. ## 4. This PR's own changeset — MEASURED, with controls both ways The question "does a PR that only edits a changeset file publish anything?" was answered by running `changeset version` in a throwaway comparison worktree at this branch's base and reading `packages/cli/CHANGELOG.md`, which `packages/cli`'s `files[]` ships (`["dist","README.md","CHANGELOG.md"]`). Four legs: | leg | resulting `@objectstack/cli` version | `packages/cli/CHANGELOG.md` | |:--|:--|:--| | base, untouched | 17.5.0 | sha256 `d132f18a05…` | | **negative control** — base + an edit to a file no package ships (`scripts/check-adr-0087-registration.mjs`) | 17.5.0 | **byte-identical** to base | | **this PR** — base + the changeset amendment only | 17.5.0 | sha256 `10ccd96910…`, 20 diff lines, **all inside the entry objectstack-ai#18677 already schedules** | | **positive control** — base + a NEW changeset (`'@objectstack/cli': patch`) | 17.5.0 | one **new bullet** appears: a release entry of its own | ⇒ Two facts, and they point in different directions, so both are stated: 1. This PR **does** move bytes that ship. ⛔ It is not true that editing a changeset publishes nothing. 2. This PR **declares no release of its own** — no new entry, no version movement — which is exactly the wording the `changeset-check` job uses for the `skip-changeset` exemption, and it is what the positive control above makes visible rather than assumed. ⇒ **Route taken: `skip-changeset`.** Of the three routes the `Check Changeset` log itself names, route 3 (an empty-frontmatter changeset) is closed — newly added ones are rejected (objectstack-ai#5471) because an all-empty set makes `changesets/action` return green while publishing nothing (objectstack-ai#4898). Between the other two, the positive control above is what decides it: adding a real changeset would add **one new published CHANGELOG bullet** — a user-facing release note announcing a correction to the release note directly above it — while moving no version. This PR amends the prose of a bump that is **already declared**; it adds none. That is the exemption's own wording.⚠️ **The label is not on this PR yet.** `node scripts/pm/label-write.mjs --issue 18867 --repo objectstack-ai/objectstack --add skip-changeset` was refused by this session's local permission classifier (reason: `[CI Bypass]`) before any request was issued — ⛔ not by GitHub, and ⛔ no status code was reached. This dev did ⛔ not route around that refusal through a second channel. **The measurement is above and the route is declared; applying the label is left to the dispatching seat.** Until it is applied, the `Check Changeset` red below stands. ## 5.⚠️ The pending-note correction still needs a person's word — ⛔ and the red on this PR is NOT the gate that asks for it Run locally, `node scripts/check-empty-changeset.mjs --base origin/main` exits **1** here, naming this file and this class: > DELIBERATE CORRECTION -- your change may have made this PENDING release note false, and you rewrote it in the same stroke. Remedy: do NOT restore it -- say so on the PR and get it confirmed; restoring it from the base would put the false sentence back. and closing: > Correcting a pending release note is a decision about a release rather than a refactor -- say so on the PR, naming the note and what changed under it, and get it confirmed. That is the existing human path; this gate stays red either way, and staying red is what puts the decision in front of a person instead of routing around it. **So, saying it, as the gate asks:** - **The note:** `.changeset/18677-validate-per-package-authoring-pass.md`, PR objectstack-ai#18769's, pending and unreleased. - **What changed under it:** ⛔ nothing in the code. What changed is the **evidence**: a fixture that did not exist when that note was written (`CONFIG_FLIP`, shipped by PR objectstack-ai#18813) exhibits the newly-refused input the note declared unexhibitable. The note's runtime claims are otherwise untouched and undisputed. - **What is asked:** confirmation that this pending release note may be corrected in place. This PR stays **draft** until then.⚠️ **A correction to an earlier reading in this very PR, stated rather than quietly dropped.** This section first argued that `skip-changeset` must be withheld so the refusal above would stay red on CI and summon a person. **Measured on this PR's own failing run** (job 105457719184, step list read from the Actions API, ⛔ not inferred from the check name), that argument is false: | step | outcome | |:--|:--| | 11 · Require a changeset (or the skip-changeset label) | **failure** | | 12 · Reject an empty-frontmatter changeset added by this PR — where `check-empty-changeset --base` runs | **skipped** | | 13 · Require an ADR-0087 disposition on a declared-breaking changeset | **skipped** | | 14-15 · allow-major re-read, major guard | **skipped** | Step 11 short-circuits the job, so the foreign-changeset refusal **never executes on CI at all** — labelled or not. Withholding the label therefore hides nothing and reveals nothing; what it does instead is leave a *misleading* headline red ("This PR adds no changeset ... run `pnpm changeset`") on a PR that correctly adds none. ⇒ the refusal's "say so on the PR and get it confirmed" is a **prose-and-person** requirement, discharged by this section, ⛔ not by a CI red that does not happen.⚠️ **What the label costs, so nobody reads a green board as more than it is:** with `skip-changeset` on, the whole `changeset-check` job is exempt, so steps 12 and 13 do not run here either. Both were run locally at `1056c00195`: `check-empty-changeset --base origin/main` exit **1** (by design, the refusal quoted above) and `check-adr-0087-registration --base origin/main` exit **0**, reading `.changeset/18677-…md [BREAKING] not-required (no-migration-prescription)`. The live ADR-0087 check also runs over the whole pending stock at RC-cut time (`cut-rc.yml`, `--base $SNAPSHOT_SHA`), so the disposition is still checked before any release consumes this entry — just not on this PR.⚠️ For context, the two landed precedents for this act — objectstack-ai#18126 (the same repair, BREAKING banner + ADR-0087 disposition onto a pending entry) and objectstack-ai#17851 — both carried `skip-changeset`. Measured, not recalled: the foreign-changeset refusal landed in objectstack-ai#18146 at `0ffb4963e5` **2026-09-14T06:56:58Z** and objectstack-ai#18126 merged at `f3b41e87d4` **2026-09-14T04:04:11Z**; `git merge-base --is-ancestor 0ffb496 f3b41e8` exits **1**, with a control leg (`f3b41e87d4^` against `f3b41e87d4`) at exit **0** on a non-shallow checkout. So the refusal post-dates both by about three hours and ⛔ neither is precedent for it — which is exactly why the reading above was measured on this PR rather than borrowed from them. ## 6. Verification | what | result | |:--|:--| | `node scripts/check-adr-0087-registration.mjs --base origin/main` | **exit 0** — `.changeset/18677-…md [BREAKING] not-required (no-migration-prescription)` | | `node scripts/check-changeset-no-major.mjs --base origin/main` | exit 0 | | `node scripts/check-empty-changeset.mjs --base origin/main` | **exit 1 — by design, see §5; it does not run on this PR's CI, labelled or not** | | the other 15 commands from `scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack` (self-tests, `check:nul-bytes`, `check:published-files`, `check:objectui-changeset`, `check:pm-changeset-deadline-census`, …) | all **exit 0** | | `pnpm lint` — the whole repo, `eslint . --no-inline-config`, ⛔ not narrowed | **exit 0** at `1056c00195` | | control characters | `grep -naP` over the changed file for `[\x00-\x08\x0b\x0c\x0e-\x1f\x7f]`: no hits | `dispatch-gates.mjs` does not name the `pnpm lint` family; it was run anyway, unnarrowed, so no narrowing argument is owed. Its own provenance line reads `objectstack-ai/objectstack` at `1056c00195`, and `--repo` was asserted and held. No package test or typecheck is owed: the diff touches no package source, no `exports`, no spec contract and no built artefact. `packages/cli`'s dependency closure was built only to drive the CLI for §2. ## Boundaries — what this PR deliberately does not do - ⛔ **It does not touch `validate.ts` or any landed code.** The diff is one file. - ⛔ **It adds no `Clause-②:` line to the changeset body**, though the sibling `.changeset/18778-lint-per-package-authoring-pass.md` carries one. Writing `yes (narrowing)` into objectstack-ai#18677's note would be a retro-correction of a landed declaration, and the card marks that "Not asserted" and routes it above this seat. The banner and the ADR-0087 disposition are release-facing and are what the card prescribes; the governance declaration is not. - ⛔ **It does not rule on what a landed `yes (narrowing)` that shipped declared `no` owes beyond this file**, nor on whether objectstack-ai#18677's mandatory contract review is now owed. That is the maintainer's. - ⛔ It does not touch `content/docs/releases/`, any `packages/*/CHANGELOG.md`, `packages/cli/src/commands/compile.ts`, `packages/qa/vitest-filter-preflight/**` or `packages/cli/vitest.config.ts`. ## Acceptance notes - **Noted, not filed:** `.changeset/18778-lint-per-package-authoring-pass.md` carries its `Clause-②: yes (narrowing)` line inside the changeset body, i.e. in text that ships verbatim into the published CHANGELOG. Whether that governance token belongs in a user-facing release note is a question about changeset convention, not a defect: no gate reads it there, nothing is falsified by it, and it is out of this card's one-file surface. Carrier: the next PR to touch changeset conventions; no PR is in flight on it. - **Noted, not filed:** the ⭐ generalization this card turns on — *a property that could not be exhibited with the fixtures on hand is UNEXHIBITED, ⛔ not absent* — is currently recorded only in card prose (objectstack-ai#18823, and triage `5722459190` which asks for it on the discriminant list). It has no home in `AGENTS.md` or any gate. Placing it is a governed-surface edit and so is not this PR's to make. Carrier: the triage seat that asked for it. Authored by Claude Code in session `session_01DvvamiacK328idtBYJBxV3`; the branch is `claude/issue-18823-pending-changeset-breaking-banner`. (Attribution is stated here in prose on purpose: this body is edited through a channel measured to store only a bare footer, which carries no session id.) --- _Generated by [Claude Code](https://claude.ai/code)_ Co-authored-by: Claude <noreply@anthropic.com>
Fixes #18778
Clause-②: yes (narrowing)
os lint --strictnow exits 1 on a project it exited 0 for. A newly-refused input is exhibited below, not reasoned about.Clause-②: norested on "no newly-refused input could be exhibited" measured onos validate's door; ⛔ it does not transfer, and on this door it is false. Verified throughreadClause2Linefromscripts/pm/check-clause2-carriers.mjs, ⛔ not an ad-hoc regex.os buildhas run the author-time rule table twice since #16611 — once over the union-folded stack, then once perartifactPackages(…)entry withpackageBodyAsStack(…)as resolution context, de-duplicated against the union run. #18769 gaveos validatethe second half.os lintran the union fold and stopped, so bycompile.ts' own description the survivors of that pass — "exactly the set the union could not see" — were findingsos buildreported andos lintstructurally could not. Same false-clean direction, on the fastest of the three doors. All three now call the one shared pass.⭐ The trap, confirmed rather than relayed
lint.tsdoes importartifactPackagesandpackageBodyAsStack— atpackages/cli/src/commands/lint.ts:18, used at:542and:546. Opening the hits is what settles it: they feedos lint's own intra-package duplicate-name advisory (#17821), never the shared rule table. ⛔ A count is not a reading. This door is also the one place in the tree where "the loop is already written here, write the second one beside it" is the cheap move, so the seam pin ratchets the call count rather than its absence — the sibling doors'not.toMatch(/packageBodyAsStack\(/)assertion ⛔ cannot be transplanted here.Should
os lintrun it? — settled from the repo's own statements, ⛔ not assumedThe dispatch fenced this as a contract question with a STOP arm. It did not need the stop arm; four statements settle it, and the one statement that reads the other way is false on the tree.
For, and they are this door's own words:
packages/lint/src/authoring-rules.ts:38-44— "Any rule that can emiterrorruns on all three commands. A gate is only as strong as the weakest command an author or CI happens to run, so a gating rule with partial coverage is not a stricter check — it is a coin flip", and "the three commands are three doors in ONE wall".packages/cli/src/commands/lint.ts:652-656— the union fold landed here, for this exact direction: "the whole table reported nothing andos lintreturned no finding of any severity for a projectos buildrefuses". That is the same sentence as this card, one layer out; the fold fixed which COLLECTIONS the table sees, this fixes which STACKS it is run over.packages/cli/src/commands/lint.ts:604-611—os lint's pre-registry hand-wired subset was removed because "a pre-flight that disagrees with the gate in both directions is worse than no pre-flight".packages/cli/test/validate-build-gate-parity.test.tsalready holds the INPUT equal across all three doors (all three authoring commands hand the rule table the union-folded stack), and itsos lintnever surfaces ADR-0087 conversion notices — it normalizes with noonConversionNoticesink, the #3782 parity gapos buildwas in #12297 note states the governing rule for exactly this shape: "A guard that enumerates a subset of the class it describes reports green for the members it forgot. The list is the class now, not the card."Against — one statement, and⚠️ it was already FALSE when it was written:
validate-build-gate-parity.test.ts'PARITY_COMMANDSdocblock read "lint.ts… emits no artifact and runs no artifact-level gate, so it is not part of the parity question." Measured at7572329069:lint.ts:13imports andlint.ts:881callscollectAndLintDocs— a name in that same file'sSHARED_NON_REGISTRY_GATESroster, i.e. an artifact-level gate by the file's own classification. The clause is corrected in this PR rather than deleted, because it is the one sentence in this repository that could be read as "os lintis exempt from the artifact-level gates" and this card had to settle exactly that. What excludeslint.tsfromPARITY_COMMANDSis the ARTIFACT (that file's question isos validateasos build's read-only superset), not the gates. ⛔PARITY_COMMANDSis deliberately NOT widened — that would re-open every ledger classification against a third file in one stroke.⇒ the roster prediction in the dispatch resolved the other way:
SHARED_NON_REGISTRY_GATESand itsit.each('both commands run %s')did not move. They are keyed toPARITY_COMMANDS, which this PR leaves at two files, so nothing on that roster reddened. What moved is the docblock the roster is read through.The measurement
Fixture
CONFIG_FLIP(shipped as the pin's own fixture):coreownspp_account, the siblingorderspackage owns the view that displayspp_account.industry. Judged as one flattened union the field has a consumer and nothing is raised; judged per package,coredeclares a field nothing incorereads. ⇒ the union run is clean and the per-package run is not — the only shape that can tell "the doors agree" from "the doors agree because neither looked".At
origin/main7572329069:os build --jsonos lint --jsonos lint --json --strictfailing: 0failing: 1On the sibling two-package fixture from #18769 (
CONFIG_MULTI, where the survivor is the positional-key ECHO):os build3 warnings ·os validate3 ·os lint2 before, 3 after — the three doors now report one set.CONTROL — a single-package project (no
packages[]):packageCount0, the pass is skipped, andos lintreports zero per-package findings before and after. Without it "the doors agree" is satisfied by three commands that all looked at nothing, which is exactly how this gap survived two cards' worth of parity files.⛔ The DEFAULT face is not claimed to move. No
error-severity per-package-only finding was exhibited: two probe shapes were tried — a cross-package field consumer and a cross-package page reference (nav-target-unresolved) — and both land atwarning. That half is NOT MEASURED, and the--strictpin says so in its own comment rather than asserting an unmeasured default-face flip.The severity face is
os lint's own and unchanged: one mapping expression now serves both halves (info→suggestion, everything else verbatim), so anerrorfails the run, awarningfails it only under--strict. A per-packageerroris oneos buildALREADY refuses, so this narrowsos lintto the bar the command that ships holds and never past it.Red/green, both legs, from the committed state
packages/cli/src/commands/lint.tsalone restored to its7572329069blob (git show 7572329069:…), everything else at HEAD. Marker count on disk2 → 0verified before running; restored withgit checkout HEAD -- <path>,git diff HEADempty andgit hash-objectback to5b2eb1af023348865d06a4ab7355708af801db43after each leg.test/lint-per-package-authoring-seam.test.ts(unit)test/lint-per-package-authoring-parity.test.ts(integration)The 4 and 3 that pass in both legs are deliberately door-independent (the pass's own three-door equality, the
findings∪ split identity, the single-package control, thepackageBodyAsStackcount ratchet) — they are not measuringlint.ts, and a file where everything reddened would mean the pins were coupled to the fix rather than to the behaviour. The ablated integration failure is the narrowing itself:expected +0 to be 1onfailing.Tier, read from the config's own answer both directions
vitest list --filesOnlyper project, ⛔ not the predicate applied by hand:--project unit--project integrationlint-per-package-authoring-seam.test.tslint-per-package-authoring-parity.test.tsvalidate-per-package-authoring-seam.test.ts(control)validate-per-package-authoring-parity.test.ts(control)Populations non-vacuous: 214 unit files / 50 integration files. Neither new file constructs
new ObjectQL(, so neither carries the KERNEL signal, and no existing file changed tier (no existing test file's source was touched exceptvalidate-build-gate-parity.test.ts, comment-only, which stays unit).What ran
pnpm --filter '@objectstack/cli^...' build— dependency closure, exit 0.pnpm --filter @objectstack/cli exec vitest run --project unit— 214 files / 3047 tests, all pass.pnpm --filter @objectstack/cli exec vitest run --project integrationover the declared-narrowed set of 9 files this diff can reach (authoring-rule-command-parity,union-fold-command-parity,validate-per-package-authoring-parity,lint-per-package-authoring-parity,info-detail-package-fold,lint-eval-generator-refusal-separator,emit-json-pipe,adr-0048-app-split,nav-contribution-groups.package-id) — 9 files / 53 tests, all pass. The other 41 integration-tier files are DB/migration/secret/generate suites that never run the authoring rule table; ⇒ declared to CI.pnpm --filter @objectstack/cli typecheck— exit 0, includingcheck:test-typecheck. Both new test files are really in that program:tsc -p tsconfig.test.json --listFilesnames them (2 of 2), so the claim is measured, not assumed.scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack(⛔ not a hand-made path list): 61 derived, 58 run green, 3 NOT MEASURED, 0 UNRUN, reconciled through--ranwith an exit code recorded per family. The three arecheck:dual-build-cjs-loads,check:i18n,check:i18n-coverage, each exit 3 = PREREQUISITE NOT MET (packages with nodist/in this worktree) — ⛔ read as NOT MEASURED, never as a pass.check:i18n-walk-parityalso refused at first for the same reason and went green afterpnpm --filter @objectstack/cli build.eslint --no-inline-confignarrowed to the 5 changed source files: 0 errors, 0 warnings, file count 5 read from--format json, not guessed. The narrowing is a measurement because the population is read from eslint's own config (files: ['**/*.{ts,tsx,mts,cts,js,jsx,mjs,cjs}']plus per-directory overlays) and becauseeslint.config.mjs:326-329states this repo "never enables type-aware linting (noparserOptions.project, no typed@typescript-eslintrules) for ANY file" — so this diff cannot move the verdict on any file it does not touch. The repo-widepnpm lintrun is CI's.Changeset
minoron@objectstack/cliwith a**BREAKING**banner and the ADR-0087 dispositionnot-required (no-migration-prescription)—check:adr-0087-registrationreads the arm and prints it back:[BREAKING+clause-②-narrowing]. ⛔ Notskip-changeset:packages/cliis released and both edited sources ship in itsfiles[].majoris out of the launch window, whichcheck:changeset-no-majorconfirms green.Acceptance notes
Noted, not filed — nothing here is a reproducible defect, a declared-contract violation, or a metadata trap:
os validateruns the per-package author-time rule passos buildalready ran #18769's ownClause-②: nois falsified by this card's fixture, and the falsification is MEASURED.os validate --strictexits 1 whenwarnings > 0(validate.ts:715). OnCONFIG_FLIP, withvalidate.tsalone restored to095c7f60ae^it exits 0 (0 warnings); at HEAD it exits 1 (1 per-package warning). Restore verified by blob hash. ⇒ a newly-refused input could be exhibited on that door too; what was missing was a fixture whose union run is clean, and fix(cli):os validateruns the per-package author-time rule passos buildalready ran #18769's fixture (an ECHO of a union finding) structurally cannot be one. ⛔ Not corrected here — fix(cli):os validateruns the per-package author-time rule passos buildalready ran #18769 is merged, and rewriting a landed declaration is not this card's act. Carrier: the PM seat. Dedupe words:18769 clause-② narrowing falsified·validate --strict per-package warning exit·union-clean per-package fixture.findingsmember added torunPerPackageAuthoringRulesexists becauseos linthas no severity split to re-join; re-joiningerrorsthenadvisoriesat that door would put all errors before all advisories and silently re-order a list the union run above it produces in rule order. Stated in the member's own docblock so the next door does not have to re-derive it.packages/cli/vitest-tiers.tstreatsnew ObjectQL(as a KERNEL signal, and a per-package pin is exactly the kind of test that grows one later. Nothing to file; the tier table above is the reading that would catch it.⛔ Untouched, each with its own card: #18779 (the positional de-duplication key — changing it changes what
os buildreports) and #18780 (os build's text face counting advisories it never prints).Generated by Claude Code
Generated by Claude Code