Skip to content

fix(cli): os validate runs the per-package author-time rule pass os build already ran - #18769

Merged
os-support-ai merged 3 commits into
mainfrom
claude/issue-18677-validate-per-package-authoring-pass
Sep 17, 2026
Merged

os-support-ai merged 3 commits into
mainfrom
claude/issue-18677-validate-per-package-authoring-pass

Conversation

@os-support-ai

Copy link
Copy Markdown
Collaborator

Fixes #18677

Clause-②: no — no key is added to any published payload, and no newly-refused input could be exhibited (measurement below). Verified through readClause2Line from scripts/pm/check-clause2-carriers.mjs, ⛔ not an ad-hoc regex.

os build runs the artifact's authoring rules twice: once over the union-folded stack, then a second runAuthoringRules('build', …) pass over each artifactPackages(…) entry with packageBodyAsStack(…) as resolution context, de-duplicated against the union run. os validate ran the union pass and stopped — importing neither seam. By compile.ts' own description the survivors of that second pass are "exactly the set the union could not see", so that whole set was findings os build reported and os validate structurally could not. False-clean, on the worse door: the fast pre-flight is what an author runs before shipping.

The measurement

examples/app-multi-package — the repo's only two-package fixture — at origin/main 09e16a5, both commands exiting 0:

os build --json os validate --json
before warnings 4 (3 union + 1 per-package survivor) warnings 3
after warnings 4 warnings 4 — same set, same order

os build's own output is unchanged: text face byte-identical modulo timings and artifact size, --json payload identical.

Red/green, both legs, from the committed state. With validate.ts alone restored to its pre-change blob (pinned commit 09e16a574, ⛔ never a moving ref) and artifact-packages.ts left carrying the shared pass, so the reverted change is exactly the one the card is about:

runPerPackageAuthoringRules occurrences: committed=3  mutated=0
mutated blob bafa54b07f… != HEAD blob 340cec6253…        == proof the mutation reached disk

unit  seam   1 failed | 5 passed  -> "both `os build` and `os validate` CALL the shared pass"
integ parity 1 failed | 3 passed  -> "these per-package findings ride `os build` and `os validate`
                                      cannot see them — the #18677 false-clean set:
                                      expected [ Array(1) ] to deeply equal []
                                      + "package 'com.example.ppparity.core' — object \"pp_account\" · field \"industry\""

Restored with git checkout HEAD -- … (⛔ not a bare git checkout --, which reads the poisoned index); git diff HEAD empty and the blob hash back to 340cec6253…. After: 28 passed in the unit run (the two pins + vitest-tiers-partition) and 4 passed in the integration run.

The card's declared-unmeasured limb, answered

"Does any authoring rule fire ONLY under packageBodyAsStack resolution today?" — measured against the registry, ⛔ not guessed from rule names, with the instrument's positive control lit:

reading result
per-package survivors on examples/app-multi-package 1
of those, genuinely new (a finding the union could not see) 0
of those, ECHOES (same rule + same message as a union finding, differing only in positional path) 1
⭐ positive control — the same instrument on a stack with a top-level-only consumer 1 genuinely new ⇒ the zero is measured, not a blind instrument
per-package findings at error severity, any fixture tried 0

⇒ a measured "none today" for the gating tier, and the mechanism is packageBodyAsStack's own design, not luck: it hands each package the artifact's whole packages[] as resolution context, and the reference-integrity suite resolves object names through it (artifactProvidedObjectNames, packages/lint/src/validate-object-references.ts). Two constructed attempts at a per-package-only error — a view on a sibling's object, an app whose own navigation names a sibling's object — resolved identically in both runs and produced nothing. defineStack refuses the first shape outright at definition time.

⛔ NOT MEASURED: whether any of the 30 gating rules is reachable per-package-only. Two candidate shapes were measured and both were negative; the remaining rules were not swept one by one.

Why this is not a second copy of the loop

runPerPackageAuthoringRules lives beside artifactPackages / packageBodyAsStack in utils/artifact-packages.ts, whose header already forbids a second copy of that shape by name. What would have drifted between two hand-written loops is not the package reading but the verdict — the de-duplication key, the severity split, the where prefix. The severity mapping is os build's, unchanged: a per-package error refuses, an advisory joins warnings.

Tiers — both pins gate the merge queue

  • test/validate-per-package-authoring-seam.test.ts → UNIT (tierSignals = none). Source ratchet + the pass's door-independence, with a non-vacuity case.
  • test/validate-per-package-authoring-parity.test.ts → INTEGRATION (childProcess, helperCliOrTsx). Deliberately carries no .e2e segment, so by the orthogonal nightly cut it stays QUEUE-tier — the combination vitest-tiers.ts names as deliberate. Both were confirmed present in testFilesOnDisk() with OS_TEST_TIERS unset, and test/vitest-tiers-partition.test.ts passes.

⚠️ test/build-json-advisory-parity.e2e.test.ts — whose standing assertion "nothing rides in build's warnings that validate does not also report" stayed green through this entire defect, its fixtures declaring no packages[] — is NIGHTLY tier by name, so the queue run skips it. Run explicitly under OS_TEST_TIERS=nightly: 8 passed, unaffected.

Acceptance notes — out of scope, noted, not fixed here

  1. The de-duplication key is leaky, and the file's own sentence overstates what survives. findingKey includes the positional path, and a collection index inside one package's body is not the index the flattened top level gives the same item — so what survives is "the set the union could not see" plus every finding whose two coordinates differ. On examples/app-multi-package the single survivor is an echo of the union's own crm_account.industry finding at the package-local index. ⛔ Not fixed here: changing the key changes what os build reports, a different decision from making the two doors agree. To file. Dedupe words: findingKey positional path · per-package echo · de-duplication key collection index.
  2. os build counts per-package advisories on its text face that it never prints. printAuthoringAdvisories(ruleAdvisories) runs before the per-package loop appends to that list, so the closing line reads ⚠ 4 author-time warning(s) — see above above a list of 3. Measured on examples/app-multi-package; --json carries all 4, so this is the text face only and is not os build --json also drops the capability-provider and package-docs warnings that os validate --json carries #11727. After this change os validate prints all 4 — the direction is now build-side. To file. Dedupe words: printAuthoringAdvisories before per-package loop · see above count mismatch · build text face advisory omitted.
  3. os lint has the same structural gap and it is not fixed here. The card's table reads compile.ts vs validate.ts; measured, the asymmetry was 2 of 3 doors. lint.ts imports artifactPackages and packageBodyAsStack — but for its own intra-package duplicate-name advisory (os lint's naming/namespace-prefix reports a legitimate cross-package name reuse as an intra-package duplicate, contradicting the ADR-0048 §3.4 sentence in its own message #17821), not the shared table. Its runAuthoringRules('lint', …) is union-only, exactly as validate.ts' was. ⛔ A count is not a reading: opening the hits is what separates the two. Out of the dispatch fence, which named os validate. To file. Dedupe words: os lint per-package authoring pass · lint.ts union-only runAuthoringRules · third door #18677.

origin/main merged at 4c837a58aa; gates re-run on the merged tree.


Generated by Claude Code

…s build` already ran

`os build` runs the artifact's authoring rules twice: once over the
union-folded stack, then a second `runAuthoringRules('build', …)` pass over
each `artifactPackages(…)` entry with `packageBodyAsStack(…)` as resolution
context, de-duplicated against the union run. `os validate` ran the union pass
and stopped — importing neither seam. By `compile.ts`' own description the
survivors of that second pass are "exactly the set the union could not see", so
that whole set was findings `os build` reported and `os validate` structurally
could not. False-clean, and on the worse door: the fast pre-flight is what an
author runs before shipping.

Measured on origin/main 09e16a5 over examples/app-multi-package, both
commands exiting 0 — `os build --json` warnings: 4 (3 union + 1 per-package
survivor), `os validate --json` warnings: 3. After: both 4, same set, same
order.

The loop is now ONE seam, not two copies. `runPerPackageAuthoringRules` lives
beside `artifactPackages` / `packageBodyAsStack` in `utils/artifact-packages.ts`,
whose header already forbids a second copy of that shape by name: what would
have drifted between two hand-written loops is not the package reading but the
verdict — the de-duplication key, the severity split, the `where` prefix.
`os build`'s observable output is unchanged (text face byte-identical modulo
timings; `--json` payload identical).

Severity mapping is `os build`'s, unchanged: a per-package `error` refuses, an
advisory joins `warnings`. So `os validate` is narrowed only to the bar the
command that ships already holds, and nothing that builds today stops
validating.

Two pins, one per tier. `test/validate-per-package-authoring-seam.test.ts`
(unit) ratchets that both doors call the shared pass, that neither names
`packageBodyAsStack` itself, and that the pass cannot answer differently per
door — with a non-vacuity case, because every single-package fixture in this
suite is blind to the defect. That blindness is measured, not asserted:
`test/build-json-advisory-parity.e2e.test.ts` already claims "nothing rides in
build's `warnings` that validate does not also report" and stayed green through
this defect, its fixtures declaring no `packages[]` at all.
`test/validate-per-package-authoring-parity.test.ts` (integration) is the
behavioural half over a two-package project, with a single-package control.

Claude-Session: https://claude.ai/code/session_01DvvamiacK328idtBYJBxV3
Co-authored-by: Claude <noreply@anthropic.com>
@github-actions github-actions Bot added size/l documentation Improvements or additions to documentation tests tooling labels Sep 17, 2026
@github-actions

github-actions Bot commented Sep 17, 2026 •

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 1 package(s): @objectstack/cli, touching 5 documentable anchor(s).

17 hand-written doc(s) name something this change touched — list omitted above 15 rows. Re-derive on the tree named below: node scripts/docs-audit/affected-docs.mjs --json 062f5cdd7116a56d687e87dec71d789ef5c593b6.

⛔ 3 release-owned page(s) also affected — read-only, see AGENTS.md Documentation Guardrails.

What this run could not see
  • 1 anchor(s) matched too much of the corpus to be a work list: os validate (command, 49 pages)
  • 2 name(s) were too generic to anchor anything (single lowercase words)
  • the SDK route bridge reached 60 of 215 client-bound route-ledger rows — the other 155 have no registrar path: tail to select them, so pages documenting THEIR client methods cannot appear above, on this or any run. Of those 155: 0 are remediable by widening that discovery convention (an in-repo file declares the path; the convention did not scan it); 55 are structural — on a ledger where NOT ONE row is declared in-repo, so no discovery change reaches them at any price; 100 are undecided (no in-repo declaration, on a ledger that has other in-repo registrars — absence and an unreadable spelling are not distinguishable here). The rows themselves: node scripts/docs-audit/affected-docs.mjs --bridge-coverage
  • a page that states a rule by its inputs shares no identifier with the emitter that implements the rule, so an emitter-only diff cannot list it — not on this run and not on any run. Measured on fix(driver-sql): emit varchar(maxLength) for a text field a declared index keys on #11430: content/docs/protocol/objectql/types.mdx documents the text-family column mapping by the ObjectQL type names it maps FROM (text / textarea / html) while the diff changed createColumn; it went unlisted, and it was the page that diff falsified, in four places. No shared token exists to detect this on, so a rule your change carries has to be re-read by hand in the pages that restate it.

Coarse fallback — 24 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): node scripts/docs-audit/affected-docs.mjs --json 062f5cdd7116a56d687e87dec71d789ef5c593b6 → packageMentionDocs.

Which tree this was computed on

This run read content/docs from 75b93a3124346c7df60764918003eea3fff9f911 — the merge of head e62c44e48bbde25eb01109e3f1383c44df880796 into base 062f5cdd7116a56d687e87dec71d789ef5c593b6, which is what actions/checkout gives a pull_request run. Not the PR head.

A worktree cut from an older main holds a different content/docs, so re-deriving there can legitimately return a different list — that is a different tree, not a wrong row. To answer on the same tree:

# while this PR is open — GitHub drops the merge commit once it closes
git fetch origin 75b93a3124346c7df60764918003eea3fff9f911 && git checkout 75b93a3124346c7df60764918003eea3fff9f911
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 062f5cdd7116a56d687e87dec71d789ef5c593b6 e62c44e48bbde25eb01109e3f1383c44df880796 && git checkout -B drift-repro 062f5cdd7116a56d687e87dec71d789ef5c593b6 && git merge --no-ff e62c44e48bbde25eb01109e3f1383c44df880796

node scripts/docs-audit/affected-docs.mjs --json 062f5cdd7116a56d687e87dec71d789ef5c593b6

⚠️ That checkout carried uncommitted changes, so the commit above does not fully identify what was read.

Advisory only, and a precision-first one (#9192): a page is listed because it names a
symbol, wire route or SDK method this diff touched — not because it mentions a changed
package. Each row says which anchor put it there, so a wrong row is reportable rather than
merely annoying. To re-verify, run the docs-accuracy-audit workflow scoped to these files:
node scripts/docs-audit/affected-docs.mjs 062f5cdd7116a56d687e87dec71d789ef5c593b6 → pass the list as
args.docs, on the commit named under Which tree this was computed on.

Copy link
Copy Markdown
Collaborator Author

Docs rider — NOT FALSIFIED, and the one page that states the invariant was RIGHT while the code was wrong · 2026-09-17T20:04Z

⚠️ The drift bot's own note says the main anchor is unusable as a work list: os validate (command, **49 pages**) was dropped as overbroad. So a page merely naming os validate was never the predicate here.

The predicate, stated BEFORE reading

A page is falsified by this diff only if it states one of:

  • (a) os validate does NOT run the per-package authoring-rule pass, or that only os build does;
  • (b) it enumerates what os validate runs as a CLOSED list that excludes that pass;
  • (c) it asserts a difference between os validate and os build in which rules, or which stack tiers, they judge — in the direction this diff removes;
  • (d) it reproduces os validate's step lines as a closed transcript that would now be missing → Running author-time rules per package (N)...;
  • (e) it states a finding or warning COUNT for os validate on a multi-package project.

The population — the corpus, ⛔ not the bot's 17 rows

Re-derived on the tree the bot named (7f86ed685af3…, the merge of head 4c837a58aa into base 74327d3f054d), in a detached worktree, ⛔ not on my own worktree cut from an older main: 20 docs, of which 3 are release-owned. The predicate was then swept over the whole hand-written corpus — 181 pages under content/docs outside references/ and releases/, plus the release-owned three read separately — because a statement-shaped predicate must not inherit a symbol-shaped population.

The readings

predicate pages matching verdict
(a) / (b) / (c) — any statement about the per-package authoring pass, in either direction 0 corpus-wide nothing to falsify
(d) — declared transcript=os-validate blocks 4 (deployment/cli.mdx, deployment/validating-metadata.mdx, getting-started/build-with-claude-code.mdx, ui/react-pages.mdx) all single-package runs — the new step line is emitted only when packages[] is non-empty, so all four are byte-unchanged. node scripts/docs-audit/check-docs-transcript-drift.mjs → exit 0, "4 declared transcript value(s) across 404 page(s) … equal what the registry derives today"
(e) — a validate finding count on a multi-package project 0 —

⭐ The one page that carries the invariant states it CORRECTLY — the code was the falsified half

content/docs/deployment/validating-metadata.mdx:518, in "The one gate, four doors":

Some rows are deliberately not universal across the three commands, and each is
one-directional (none lets a stack through a gate another command enforces)

That sentence was false on origin/main and my diff makes it true: the per-package pass is precisely a gate os build enforces that os validate let a stack through. ⇒ the page needs no edit, and the direction is worth recording — the documentation held the invariant the implementation had drifted from, which is the opposite of the usual drift finding.

Release-owned pages — read, ⛔ not edited, nothing to route

content/docs/releases/{v12,v13,v14}.mdx. Only one carries a relevant sentence — v12.mdx:224, "Re-run os compile / os validate — the new … lints may flag pre-existing authoring mistakes" — which treats the two commands as interchangeable for authoring findings. This diff makes that more true, not less. v13 and v14 carry no statement the predicate reaches. ⇒ nothing falsified, so there is no docs-only PR and no issue to route.

Noted, ⛔ not done here

The per-package pass is undocumented for both doors: the "one gate, four doors" table enumerates rule FAMILIES (all of which run in both passes), never the two stack TIERS the pass adds. Documenting it is a real improvement, but it is an ADR-0130 D4 statement about os build as much as os validate, so writing it for one door only would restate the asymmetry this PR removes. Out of the dispatch fence; reported rather than ridden.


Generated by Claude Code

…e pass was recorded in

`test/validate-build-gate-parity.test.ts` holds a CLOSED roster: every bare
identifier either command calls must land in exactly one of its three ledgers.
Wiring `runPerPackageAuthoringRules` into both doors left it unclassified, so
the roster reddened — correctly, and on the one shard that runs
`packages/cli` (`Test Core (4/6)`, step "Run this shard's tests").

⛔ Not a refusal: nothing in the suite was newly rejected by `os validate`, so
the `Clause-②: no` declaration is untouched by this red. The two failures were
"every call site in compile.ts and validate.ts is classified" (1 unclassified
name) and "no ledger entry is stale" (3 names no command uses any more).

What the ledgers now say:

  * `runPerPackageAuthoringRules` joins SHARED_NON_REGISTRY_GATES. It cannot
    become a registry rule: a registry rule is handed ONE stack, and this pass
    is the thing that DECIDES which stack — the artifact sliced per package
    (ADR-0130 D4/D5). Its row buys a real assertion for free, because
    `it.each(SHARED_NON_REGISTRY_GATES)` asserts both commands run it.
  * The #18491 entry "Input to the compile-only per-package rule walk — a real
    parity gap, reported not closed" is DELETED, not reworded. That entry was
    right and is now spent; a ledger row that outlives its finding is how a
    closed gap reads as an open one.
  * `artifactPackages` survives on its own reason — both commands read it to
    COUNT the packages for the step line. `packageBodyAsStack` left the command
    files with the loop, as did `findingKey` and the `new Set(…)` de-duplication.

packages/cli unit tier: 213 files / 3041 tests, all passing.

Claude-Session: https://claude.ai/code/session_01DvvamiacK328idtBYJBxV3
Co-authored-by: Claude <noreply@anthropic.com>
@os-support-ai
os-support-ai marked this pull request as ready for review September 17, 2026 20:43
@os-support-ai
os-support-ai added this pull request to the merge queue Sep 17, 2026
Merged via the queue into main with commit 095c7f6 Sep 17, 2026
36 checks passed
@os-support-ai
os-support-ai deleted the claude/issue-18677-validate-per-package-authoring-pass branch September 17, 2026 21:18
os-support-ai pushed a commit that referenced this pull request Sep 18, 2026
…ict` narrowing an at-tier review exhibited

`.changeset/18677-validate-per-package-authoring-pass.md` declined its
`**BREAKING**` banner on the strength of a negative — "not declared breaking,
because the narrowing could not be exhibited" — and the negative is false. The
fixture that exhibits it did not exist when that entry was written: on the
two-package `CONFIG_FLIP` config shipped in
`packages/cli/test/lint-per-package-authoring-parity.test.ts`,
`os validate --json --strict` reads exit 0 / 0 warnings with `validate.ts`
restored to its pre-#18769 blob `bafa54b07f` and exit 1 / 1 warning at head.

The entry is still pending and unreleased (`@objectstack/cli` 17.4.0 on npm
equals the manifest version, and the CHANGELOG carries none of this text), so
this is a forward edit to an unpublished file, not a rewrite of landed history.
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;
it also narrows "nothing that builds today stops validating", which was the
sentence an upgrader would have been misled by.

⛔ Out of scope, deliberately: the landed clause-② declaration on #18677 and
what a `yes (narrowing)` that shipped declared `no` owes beyond this file. That
is the maintainer's, and this commit does not answer it.

Claude-Session: https://claude.ai/code/session_01DvvamiacK328idtBYJBxV3
Co-authored-by: Claude <noreply@anthropic.com>
os-support-ai pushed a commit that referenced this pull request Sep 18, 2026
…os validate --json` pins

`validate.ts` has SEVEN `await emitJson(` exits; both nightly-only pins
asserted SIX, so `main`'s nightly tier has been red since the seventh exit
landed with #18769 — with no run able to say so, because these files are
nightly-tier by NAME and a `packages/cli/**` diff never triggers that
workflow.

Both CONTRACTS the two files exist for still hold: every one of the seven
exits carries `warnings` AND `conversions`. Only the COUNT was stale.

So the integer is not re-pinned to 7 — that re-arms the identical trap for
exit eight, and only a cron would ever read it. It is derived instead: the
extractor must produce one payload literal per `emitJson` call site the file
actually has, whatever today's number is. That is strictly stronger than an
integer — it also catches a call site the extractor CANNOT see (one spelled
without `await`), which a hard-coded count reads as green.

The one integer left is demoted from an equality to a floor (six, the
population the #12047 / #12125 rulings were made over). It rots only in the
direction that has to be reviewed anyway — exits being removed — and it
floors the derived pair away from the single vacuum they share, an `emitJson`
renamed out of existence taking both sides to zero.

Nothing is skipped, disabled or deleted: the count assertion, the
success/failure partition and the contract negative are all still there.
`validate.ts` is untouched.

Measured on this branch, `packages/cli`:
  OS_TEST_TIERS unset   266 files collected, ZERO of them these two
  OS_TEST_TIERS=nightly  68 files collected, both of them present

Claude-Session: https://claude.ai/code/session_01DvvamiacK328idtBYJBxV3
Co-authored-by: Claude <noreply@anthropic.com>
akarma-synetal pushed a commit to akarma-synetal/framework that referenced this pull request Sep 28, 2026
…her two doors already ran (objectstack-ai#18813)

Fixes objectstack-ai#18778

Clause-②: yes (narrowing)

`os lint --strict` now exits 1 on a project it exited 0 for. A
newly-refused input is **exhibited** below, not reasoned about. ⚠️
objectstack-ai#18677's `Clause-②: no` rested on "no newly-refused input could be
exhibited" measured on `os validate`'s door; ⛔ it does not transfer, and
on this door it is false. Verified through `readClause2Line` from
`scripts/pm/check-clause2-carriers.mjs`, ⛔ not an ad-hoc regex.

`os build` has run the author-time rule table **twice** since objectstack-ai#16611 —
once over the union-folded stack, then once per `artifactPackages(…)`
entry with `packageBodyAsStack(…)` as resolution context, de-duplicated
against the union run. objectstack-ai#18769 gave `os validate` the second half. `os
lint` ran the union fold and stopped, so by `compile.ts`' own
description the survivors of that pass — *"exactly the set the union
could not see"* — were findings `os build` reported and `os lint`
**structurally 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.ts` **does** import `artifactPackages` and `packageBodyAsStack` —
at `packages/cli/src/commands/lint.ts:18`, used at `:542` and `:546`.
Opening the hits is what settles it: they feed `os lint`'s **own**
intra-package duplicate-name advisory (objectstack-ai#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 lint` run it? — settled from the repo's own statements, ⛔
not assumed

The 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:**

1. `packages/lint/src/authoring-rules.ts:38-44` — *"Any rule that can
emit `error` runs 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"*.
2. `packages/cli/src/commands/lint.ts:652-656` — the union fold landed
**here**, for this exact direction: *"the whole table reported nothing
and `os lint` returned no finding of any severity for a project `os
build` refuses"*. 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.
3. `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"*.
4. `packages/cli/test/validate-build-gate-parity.test.ts` already holds
the INPUT equal across all three doors (`all three authoring commands
hand the rule table the union-folded stack`), and its objectstack-ai#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_COMMANDS` docblock read
*"`lint.ts` … emits no artifact and **runs no artifact-level gate**, so
it is not part of the parity question."* Measured at `7572329069`:
`lint.ts:13` imports and `lint.ts:881` **calls** `collectAndLintDocs` —
a name in that same file's `SHARED_NON_REGISTRY_GATES` roster, 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 lint` is exempt from the
artifact-level gates" and this card had to settle exactly that. What
excludes `lint.ts` from `PARITY_COMMANDS` is the ARTIFACT (that file's
question is `os validate` as `os build`'s read-only superset), not the
gates. ⛔ `PARITY_COMMANDS` is 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_GATES` and its `it.each('both commands run %s')`
did **not** move. They are keyed to `PARITY_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): `core` owns
`pp_account`, the sibling `orders` package owns the view that displays
`pp_account.industry`. Judged as one flattened union the field has a
consumer and nothing is raised; judged per package, `core` declares a
field nothing in `core` reads. ⇒ **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/main` `7572329069`:

| | `os build --json` | `os lint --json` | `os lint --json --strict` |
|---|---|---|---|
| **before** | warnings **1** (per-package only) | total **0**, exit
**0** | **exit 0**, `failing: 0` |
| **after** | warnings **1**, unchanged | total **1**, exit **0** |
**exit 1**, `failing: 1` |

On the sibling two-package fixture from objectstack-ai#18769 (`CONFIG_MULTI`, where
the survivor is the positional-key ECHO): `os build` 3 warnings · `os
validate` 3 · `os lint` **2 before, 3 after** — the three doors now
report one set.

**CONTROL** — a single-package project (no `packages[]`): `packageCount`
0, the pass is skipped, and `os lint` reports 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 at `warning`. That half is
**NOT MEASURED**, and the `--strict` pin 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 an `error` fails the run, a `warning` fails it only
under `--strict`. A per-package `error` is one `os build` ALREADY
refuses, so this narrows `os lint` to the bar the command that ships
holds and never past it.

## Red/green, both legs, from the committed state

`packages/cli/src/commands/lint.ts` alone restored to its `7572329069`
blob (`git show 7572329:…`), everything else at HEAD. Marker count on
disk `2 → 0` **verified before running**; restored with `git checkout
HEAD -- <path>`, `git diff HEAD` empty and `git hash-object` back to
`5b2eb1af023348865d06a4ab7355708af801db43` after each leg.

| pin | ablated | at HEAD |
|---|---|---|
| `test/lint-per-package-authoring-seam.test.ts` (unit) | **2 failed**,
4 passed | **6 passed** |
| `test/lint-per-package-authoring-parity.test.ts` (integration) | **2
failed**, 3 passed | **5 passed** |

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, the `packageBodyAsStack` count ratchet) —
they are not measuring `lint.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 1` on `failing`.

## Tier, read from the config's own answer both directions

`vitest list --filesOnly` per project, ⛔ not the predicate applied by
hand:

| file | `--project unit` | `--project integration` |
|---|---|---|
| `lint-per-package-authoring-seam.test.ts` | **1** | 0 |
| `lint-per-package-authoring-parity.test.ts` | 0 | **1** |
| `validate-per-package-authoring-seam.test.ts` (control) | **1** | 0 |
| `validate-per-package-authoring-parity.test.ts` (control) | 0 | **1**
|

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 except `validate-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 integration`
over 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, including
`check:test-typecheck`. Both new test files are really in that program:
`tsc -p tsconfig.test.json --listFiles` names them (2 of 2), so the
claim is measured, not assumed.
- Gate families derived from the merge base by
`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 `--ran` with an exit code recorded per
family. The three are `check:dual-build-cjs-loads`, `check:i18n`,
`check:i18n-coverage`, each **exit 3 = PREREQUISITE NOT MET** (packages
with no `dist/` in this worktree) — ⛔ read as NOT MEASURED, never as a
pass. `check:i18n-walk-parity` also refused at first for the same reason
and went green after `pnpm --filter @objectstack/cli build`.
- `eslint --no-inline-config` narrowed 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 because `eslint.config.mjs:326-329` states this repo *"never enables
type-aware linting (no `parserOptions.project`, no typed
`@typescript-eslint` rules) for ANY file"* — so this diff cannot move
the verdict on any file it does not touch. The repo-wide `pnpm lint` run
is CI's.

## Changeset

`minor` on `@objectstack/cli` with a `**BREAKING**` banner and the
ADR-0087 disposition `not-required (no-migration-prescription)` —
`check:adr-0087-registration` reads the arm and prints it back:
`[BREAKING+clause-②-narrowing]`. ⛔ Not `skip-changeset`: `packages/cli`
is released and both edited sources ship in its `files[]`. `major` is
out of the launch window, which `check:changeset-no-major` confirms
green.

## Acceptance notes

Noted, not filed — nothing here is a reproducible defect, a
declared-contract violation, or a metadata trap:

1. ⚠️ **objectstack-ai#18769's own `Clause-②: no` is falsified by this card's fixture,
and the falsification is MEASURED.** `os validate --strict` exits 1 when
`warnings > 0` (`validate.ts:715`). On `CONFIG_FLIP`, with `validate.ts`
alone restored to `095c7f60ae^` 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 objectstack-ai#18769's fixture (an
ECHO of a union finding) structurally cannot be one. ⛔ Not corrected
here — objectstack-ai#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`.
2. The `findings` member added to `runPerPackageAuthoringRules` exists
because `os lint` has no severity split to re-join; re-joining `errors`
then `advisories` at 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.
3. `packages/cli/vitest-tiers.ts` treats `new 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: **objectstack-ai#18779** (the positional
de-duplication key — changing it changes what `os build` reports) and
**objectstack-ai#18780** (`os build`'s text face counting advisories it never prints).

---
_Generated by [Claude
Code](https://claude.ai/code/session_01DvvamiacK328idtBYJBxV3)_


---
_Generated by [Claude Code](https://claude.ai/code)_

---------

Co-authored-by: Claude <noreply@anthropic.com>
akarma-synetal pushed a commit to akarma-synetal/framework that referenced this pull request Sep 28, 2026
… summary line counts (objectstack-ai#18857)

Fixes objectstack-ai#18780

Clause-②: no

`os build`'s text face counted per-package author-time advisories it
never printed: `⚠ 4 author-time warning(s) — see above` standing over a
list of 3. The list grows to the count.

⚠️ **This is a RESUMED delivery.** Two commits were already on this
branch from a run whose container was killed before it opened a PR. They
carried **no** verification — no test reading, no ablation, no changeset
measurement, no gate sweep survived. They were re-read adversarially and
re-measured from scratch; three defects in them are corrected in this PR
and are listed below.

## The card's reading, re-derived rather than relayed

The card recorded the 4-vs-3 count as the objectstack-ai#18677 deliverer's, explicitly
not re-driven by the filing seat. It reproduces exactly. Measured on
`examples/app-multi-package`, `compile.ts` at this branch's merge-base
`ad1f94e8ec`, CLI run from source, `NO_COLOR=1`:

```
os build         exit 0   3 rendered advisory entries   summary: "4 author-time warning(s) — see above"
os build --json  exit 0   warnings: 4, of which 1 carries a `package 'ID' — ` prefix
os validate      exit 0   4 rendered advisory entries   (no "see above" sentence on that face at all)
```

With `compile.ts` at this branch's HEAD, same fixture: `os build`
renders **4**, summary reads **4**, `--json` still carries **4**. The
mutation was proven on disk by blob hash before each reading and the
restore proven by an empty `git diff HEAD` — not by an editing command's
exit code.

⭐ One correction to the card's framing, offered rather than assumed: `os
validate` renders its four through a *different* printer — one line per
advisory, no closing `rule: ID at PATH` line — and it has no summary
sentence to keep honest. So "the two doors disagree on the rendered
list" is right, but only `os build` can carry this defect at all. `os
lint` cannot either: `see above` appears in exactly one place in
`packages/cli/src`, and it is `compile.ts`.

## Which side moves, and what the chosen side costs — measured

The card deliberately did not rule on which side moves. **The list
moves, not the count**, and the cost of that is measured rather than
argued:

| face | before | after | delta |
|:--|:--|:--|:--|
| single-package stack (no `packages[]`) | 2038 bytes | 2038 bytes | the
two clocks only — `Load time: Nms`, `Build complete (Nms)` |
| union-level author-time FAILURE | 3590 bytes | 3590 bytes | `Load
time: Nms` only |
| multi-package stack | 3 entries under a count of 4 | 4 entries under a
count of 4 | the block renders below the `Running author-time rules per
package (N)...` step line, and gains the per-package entry |

So the only rendered byte this moves is the one the card exists to move.
objectstack-ai#18769 held `os build`'s text output byte-identical on purpose; that
hold is honoured everywhere except the defect itself.

Shrinking the count instead would have made the text face report 3 while
its own `--json` and `os validate` both report 4 — the false-clean
direction objectstack-ai#11529 named one list over, and it would have needed a second
binding to count a rendering rather than a set.

## The pin, and the two controls

`packages/cli/test/build-text-face-advisory-count.test.ts` asserts an
**equality read from one run**, not a number: the integer in the summary
line, the count of entries rendered above it, and the length of
`--json`'s `warnings`. A fixture that raises a different number of
advisories keeps passing; a face that counts a set it did not print
cannot.

Both directions were run, each from a committed tree, each with the
mutation proven on disk and the restore proven clean:

- **fails before** — `compile.ts` reverted to its pre-fix blob, pin
unchanged: `Tests 2 failed | 3 passed`, on `summary said 3; rendered
list: … expected 2 to be 3` and on `this per-package finding rides
--json and the text face never prints it`.
- **passes after** — at HEAD: `Tests 5 passed`.
- **the lit control can fail, and fails on the condition it names** —
strip `packages[]` out of the fixture and the equality assertions all
stay GREEN while `the fixture reaches the per-package pass and raises a
survivor there` goes RED: `1 failed | 4 passed`. That is exactly the
vacuous pass the control exists to refuse.

## What was rewritten in the pre-existing diff

1. **A raw ESC byte in the pin.** `stripAnsi` carried a literal 0x1B
inside its regex literal — the class `check:nul-bytes` rejects,
invisible in every reader. Rewritten as an escape; byte-identical at
runtime.
2. **A red that predated this branch.** The new once-guard is a call
site in `compile.ts`, and `validate-build-gate-parity.test.ts` has held
a CLOSED roster since objectstack-ai#18491 — every bare-identifier call site must land
in exactly one ledger. It was in none, so that file failed twice (`every
call site … is classified`, and the parity gap derived from the same
set). The roster and the assertion are both present at this branch's
merge-base and the name is absent there, so the red was carried by the
two commits this branch started from, not introduced by merging `main`.
Classified as `NOT_A_GATE` under the presentation reason, with the
argument written next to it: the guard decides WHEN
`printAuthoringAdvisories` is called and nothing else, and `validate.ts`
has nothing to wire because it has no "see above" sentence.
3. **A changeset sentence stricter than its own measurement.** It
claimed the single-package captures "differ only in the `Build complete
(Nms)` timer". Re-measured: they differ in **two** clocks — `Load time:
Nms` as well. Both are clocks, so the claim survives; the sentence now
says what was actually measured.

## What was verified and kept

- **Every exit between step 3b and the summary flushes.** Enumerated:
the union-failure text branch, the per-package-failure text branch, the
continuing path, and the catch-all. The `--json` branches return early
through the guard, and `isExitSignal` re-throws ahead of the catch's
flush so no face double-prints.
- **`^ {4}rule: ` really does tell an advisory from an error.**
`printAuthoringAdvisories` closes each entry with a four-space line;
`printAuthoringRuleErrors` and `printDocIssueErrors` both indent theirs
by six.
- **The `--json` leg's `where`-typeof filter isolates `ruleAdvisories`
exactly.** None of the other five members of `warningsSoFar()` carries a
`where` key — `DocIssue`, `NavContributionGroupDiagnostic`,
`PermissionSetNameCollisionDiagnostic`, the capability-provider pairs
and the plain-string undeclared-key list were each read. So the third
assertion is sound in general, not only on this fixture.

## Changeset level, measured on the built `dist`

⛔ Not inferred from `files[]` and not from the file's look:

- positive control — `author-time warning(s) — see above` is present in
**1** published file, `packages/cli/dist/commands/compile.js`;
- negative control — a token nothing in the tree carries is present in
**0**;
- `dist/commands/compile.js` sha256 `4999075…` with `compile.ts` at its
pre-fix blob, `f1bbb59…` with the fix — **published bytes move**;
- restoring and rebuilding a third time reproduces `f1bbb59…` exactly,
which is what makes the middle reading a measurement rather than build
noise.

`@objectstack/cli` is public and versioned and ships `dist`, so a
changeset is owed. `patch`: a bug fix in a released package, `Clause-②:
no` — no schema key, no closed-set member, no published export, no
registry entry, and no payload key, exit code or `--json` byte moves.
`check-widening-tells --declaration no` over this diff reports no file
on any declared surface (3 NOT MEASURED, 0 tells).

## Verification run on this branch

`origin/main` is merged in (not rebased); both pre-existing commits are
still ancestors, verified by `git merge-base --is-ancestor` exit 0 on
each — a positive reading, which is self-proving in any checkout.

- `pnpm --filter @objectstack/cli exec vitest run --project unit` —
**214 files, 3048 tests, 0 failed**
- `pnpm --filter @objectstack/cli exec vitest run --project integration`
— **51 files, 427 tests, 0 failed**
- the six nightly-tier `os build` pins, run under
`OS_TEST_TIERS=nightly` because the queue population excludes them by
name — `build-json-advisory-parity`, `build-json-undeclared-key-parity`,
`build-json-failure-warnings`, `build-multi-package-artifact`,
`compile-artifact-packages`, `build-docs-step-count`: **6 files, 41
tests, 0 failed**
- `pnpm --filter @objectstack/cli typecheck` — exit 0. Note
`tsconfig.json` includes `src` only, so the test layer is judged by the
`check:test-typecheck` half of that script, which reports the layer
compiling under `tsconfig.test.json`.
- `pnpm --filter '@objectstack/cli^...' build` — exit 0 (the dependency
closure).
- the gate families `scripts/pm/dispatch-gates.mjs --commands --repo
objectstack-ai/objectstack` derives from this branch's own change set,
each exit code captured before any pipe.

⚠️ Two earlier readings were contention artefacts and are recorded as
such rather than as failures: four `scaffold-emission-typechecks` cases
reddened in a run that was competing with a gate sweep and was then
killed at a timeout, and both pass in the clean full run above.

## Acceptance notes

- `check:type-check-debt` was killed by the OOM killer (exit 137 inside
its full-repo build, gate exit 3) during a contended sweep. Its own
failure text says that is **not** a pass and **not** a finding — nothing
was measured. It is re-run alone in the final sweep and the reading is
in the report.
- Noted, not filed: `printDocIssueErrors` and `printAuthoringRuleErrors`
render identical six-space `rule:` lines, so a text-face assertion that
keys on that indent alone cannot tell a doc error from a rule error.
Nothing in this PR depends on it, no open PR is heading for that file,
and there is no carrier — recorded here rather than as a card.

---
_Generated by [Claude
Code](https://claude.ai/code/session_01DvvamiacK328idtBYJBxV3)_

---------

Co-authored-by: Claude <noreply@anthropic.com>
akarma-synetal pushed a commit to akarma-synetal/framework that referenced this pull request Sep 28, 2026
…, 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>
akarma-synetal pushed a commit to akarma-synetal/framework that referenced this pull request Sep 28, 2026
…os validate --json` pins (objectstack-ai#18880)

Fixes objectstack-ai#18848

Clause-②: no — the diff is two files under `packages/cli/test/`, no
governed surface (`docs/adr/**`, `.claude/**`, `skills/**`, `AGENTS.md`,
`CLAUDE.md`). Declared from the diff, and it agrees with the claim
comment's declaration from the card.

`packages/cli/src/commands/validate.ts` is **untouched** — read only,
never written. The seventh exit is correct and stays.

## What was actually wrong

`validate.ts` has **7** `await emitJson(` exits; two nightly-only pins
asserted **6**.

| reading | value |
|:--|:--|
| `await emitJson(` exits in `validate.ts` at `b0b5f31cc6` | **7**
(`:286 :364 :434 :517 :554 :661 :762`) |
| control at `095c7f60ae^` (pre-objectstack-ai#18769) | **6** |
| what both pins asserted | `toHaveLength(6)` |
| exits carrying `warnings:` | **7 of 7** |
| exits carrying `conversions:` | **7 of 7** |

⇒ both CONTRACTS these two files exist for still hold. Only the COUNT
was stale. Re-measured independently here and the dispatching seat's
figures reproduce exactly.

The seventh exit came with objectstack-ai#18769 and is legitimate. Its payload carries
both keys.

## The choice: DERIVED, not `7`

The claim asked for the option taken and the cost of the rejected one.

**Rejected — `toHaveLength(7)`.** It is one character of work and it is
wrong for a measurable reason. It re-arms the identical trap for exit
eight, and the trap is not theoretical: it just cost a night of red
`main` and was about to cost a duplicate p1 card. Its cost is not only
that it rots, but **where** it rots — see the tier reading below. And it
is strictly weaker: ablation C is a `validate.ts` with eight call sites
where a pin of `7` reads **green** while the file has an exit no
assertion in the family covers.

**Taken — read the count off the source.** The extractor must produce
one payload literal per `emitJson` call site the file actually has:

```
callSites = every `emitJson` call in SRC, matched with a word boundary,
            optional whitespace, then the opening paren
expect(literals).toHaveLength(callSites.length)
```

Both sides read raw source, which keeps them symmetric: a commented-out
`await emitJson(` is counted by the extractor and by the pattern alike.
The one asymmetric case — prose naming the call with its paren but no
`await` — reddens, and that is the accepted price for not importing a
comment masker into these two files.

Nothing is skipped, disabled, quarantined or deleted. The count
assertion is still there, the success/failure partition is still there,
the contract negative is still there:

- `toHaveLength(6)` → `toBeGreaterThanOrEqual(6)`. The **one integer
left**, demoted from an equality to a floor. Six is the population the
objectstack-ai#12047 / objectstack-ai#12125 rulings were made over, not a count of today; it rots
only in the direction that has to be reviewed anyway (exits being
*removed*), and it floors the derived pair away from the single vacuum
they share — an `emitJson` renamed out of existence takes both sides to
zero and every assertion here with them. **I write no `7` anywhere in
this diff.**
- `valid: false` × 5 → `literals.length - 1`, beside `valid: true` × 1.
Derived, and the two together now also assert the PARTITION: an exit
carrying neither literal reddens, which the two frozen integers never
checked.

## The tier, measured in BOTH directions

Not inferred from the workflow YAML — collected, on this branch, in
`packages/cli`:

| `OS_TEST_TIERS` | files vitest collects | these two pins among them |
|:--|--:|:--|
| unset (pull request · merge queue · local default) | 266 | **0** |
| `nightly` | 68 | **2** — both, in the `integration` project |

⇒ the pins are not "a red nobody looks at". In the queue population they
are **not collected at all**. No pull request and no merge-queue run
could have reddened on this, which is why objectstack-ai#18769's own CI was green and
correct. That is a measurement for objectstack-ai#18520, not a new card — it is in the
report for the seat.

## Red → green, in the nightly tier itself

Not a substitute: the tier the pins actually live in, reached locally
with `OS_TEST_TIERS=nightly`.

**Before** (at `b0b5f31cc6`, pre-fix):

```
FAIL |integration| test/validate-json-failure-conversions.e2e.test.ts
  AssertionError: the `emitJson` exit count moved — a new exit must carry
  `conversions` too: expected [ …(7) ] to have a length of 6 but got 7
FAIL |integration| test/validate-json-failure-warnings.e2e.test.ts
  AssertionError: … expected [ …(7) ] to have a length of 6 but got 7

Test Files  2 failed (2)      Tests  2 failed | 5 passed | 16 skipped (23)
```

This is also the **first runner-level reproduction** of this card: the
filing seat stated it had no install and relayed the PR run's result
instead.

**After:**

```
Test Files  4 passed (4)      Tests  14 passed | 36 skipped (50)
```

Four files, because the run also carries the two `build-json-failure-*`
siblings — see acceptance notes.

## Ablation — four legs, mutation proven on disk, restore proven
byte-exact

Run from the **committed** fix. The mutation is confined to my own two
test files (the `SRC` seed), driven by env var; `validate.ts` is never
written. `trap … EXIT INT TERM`, absolute paths, and the restore is
verified by `git diff HEAD` empty **and** `git hash-object` equal to the
HEAD blob hash (non-empty, both files).

| leg | mutation | expected | observed |
|:--|:--|:--|:--|
| **A** | an 8th exit, `await`, carrying both keys — an *honest
addition* | green | **green** — 2 files, 7 passed |
| **B** | an 8th exit missing `warnings:` | warnings red, conversions
green | **exactly that** — `expected [ Array(1) ] to deeply equal []` |
| **C** | an 8th call site spelled **without** `await` | derived
equality red | **both red** — ``an `emitJson(` call site the payload
extractor could not read: expected [ …(7) ] to have a length of 8 but
got 7`` |
| **D** | source truncated to 5 exits | floor red | **both red** —
`expected 5 to be greater than or equal to 6` |

Leg **A** is the fix doing its job: a hard-coded `7` is red here. Leg
**C** is the capability the rejected option does not have at all — the
extractor reports 7, so `toHaveLength(7)` passes while the file carries
an exit no assertion covers. Leg **B** proves deriving the count
weakened nothing: the contract still bites over the *added* exit, and
the two files isolate from each other. Leg **D** proves the remaining
integer is live.

A first ablation attempt exited 127 on a wrong vitest path — the
mutation landed but nothing ran, so those readings were void and
discarded rather than reported. The table above is the re-run.

## Changeset

`skip-changeset`. Measured, not assumed — nothing published moves:

- `packages/cli` `files[]` is `["dist", "README.md", "CHANGELOG.md"]`;
`tsconfig.build.json` has `include: ["src"]`, so `test/` is outside the
compiled tree entirely.
- `npm pack --dry-run --json`: **0** published paths under `test/` or
`src/`. Instrument lit by the positive control `README.md`, which is
present.
- ⚠️ The `dist/` positive control was **not lit** in the worktree where
that manifest was taken (no build yet at that point). The `include:
["src"]` boundary is what carries the negative, and the closure build
reading is in the report.

## Acceptance notes — noted, not filed

- **The same rotting shape sits on two more pins, green today.**
`test/build-json-failure-warnings.e2e.test.ts` and
`test/build-json-failure-conversions.e2e.test.ts` pin `toHaveLength(11)`
/ `10` / `1` over `compile.ts`. Measured: `compile.ts` has 11 literals,
10 `success: false`, 1 `success: true` — **green right now**, and one
honest exit away from repeating this exact p1, again only at night. ⛔
Deliberately **not** touched here: `compile.ts` is a objectstack-ai#18779 carrier and
is fenced for this card, and a p1 with a cron clock is not the place to
double the review surface. Offered to objectstack-ai#18520 as a measurement rather
than as a new card.
- **`validate.ts:661` carries a comment that the seventh exit made
false** — it says the ordering site is read by "every one of the six
exits" and that "a seventh exit cannot be added with a different member
order". A doc nit inside a read-only file; carrier: the next PR that
touches `validate.ts`.

## An instruction conflict, named rather than quietly resolved

Triage comment `5723031597` pinned the dispatch order: land the two-line
literal repair first, and take the "does an integer pin belong here at
all" question as a second, separate stroke. The claim comment
`5724808732` instead hands the choice to the deliverer and says ⛔ not to
reflexively hard-code `7`.

I took the claim's instruction, because the thing triage was protecting
against is not in play: the derived form is the *same edit in the same
assertion slot*, measured and proven in this run, and it delays the red
`main` by nothing. The design question triage deferred — whether these
files belong in the core tier, or the tier's path filter should name
`validate.ts` — is untouched here and remains the cli lane's call.

## Verification

`OS_TEST_TIERS` measured in both directions · red→green in the nightly
tier · four ablation legs · gate families derived with
`scripts/pm/dispatch-gates.mjs` and reconciled with `--ran`. Full
readings, including anything that came back NOT MEASURED, are in the
`os-dev-report` comment on objectstack-ai#18848.

---
_Generated by [Claude
Code](https://claude.ai/code/session_01DvvamiacK328idtBYJBxV3)_

Co-authored-by: Claude <noreply@anthropic.com>
akarma-synetal pushed a commit to akarma-synetal/framework that referenced this pull request Sep 28, 2026
…llection index, so an echo no longer survives it (objectstack-ai#18878)

Fixes objectstack-ai#18779

## The card's central reading, reproduced first

Measured on `origin/main` `a43b9d0654` over the repo's own two-package
fixture
`examples/app-multi-package`, `os build --json` exiting 0:

```
warnings: 4
  [0] field-no-consumers  object "crm_order" · field "account"    objects[0].fields.account
  [1] field-no-consumers  object "crm_order" · field "amount"     objects[0].fields.amount
  [2] field-no-consumers  object "crm_account" · field "industry" objects[1].fields.industry
  [3] field-no-consumers  package 'com.example.multi.core' —
                          object "crm_account" · field "industry" objects[0].fields.industry
```

`[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 `findingKey` had 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 build` printing `4 author-time warning(s)` for 3
distinct
ones, 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 lint` report one fewer line on such a project, which is an
observable change
and is why this carries a changeset.

Triage settled the blocker: the "`os build` output stays byte-identical"
constraint was objectstack-ai#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):

| | before | after |
|---|---|---|
| `os build --json` | warnings 4, exit 0 | warnings 3, exit 0 |
| `os validate --json` | warnings 4, exit 0 | warnings 3, exit 0 |
| `os lint --json` | total 4, failing 0, exit 0 | total 3, failing 0,
exit 0 |
| `os lint --json --strict` | total 4, failing 4, exit 1 | total 3,
failing 3, exit 1 |

The 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 build` already exits 1 on a union error *before* this pass
runs,
and `os lint --strict` fails on `errors + warnings`, a count that could
only
reach 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 "fewer
findings reported ⇒ `--strict` refuses less". Measured, `--strict` does
**not**
refuse less: `failing` drops 4 to 3 and the verdict stays `exit 1`,
because the
dropped 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: 5723810598` on the
card.

## The falsified sentence — all carriers, re-derived

`git grep "set the union could not see"` on `a43b9d0654` finds more than
the
four the dispatch named. Source **and** tests, all corrected here:

| file | treatment |
|---|---|
| `packages/cli/src/commands/compile.ts` | claim corrected, quote kept |
| `packages/cli/src/commands/lint.ts` | claim corrected, quote kept |
| `packages/cli/src/commands/validate.ts` | claim corrected, quote kept
|
| `packages/cli/src/utils/artifact-packages.ts` | claim corrected at the
definition |
| `packages/cli/test/lint-per-package-authoring-parity.test.ts` | claim
corrected |
| `packages/cli/test/lint-per-package-authoring-seam.test.ts` | claim
corrected |
| `packages/cli/test/validate-per-package-authoring-parity.test.ts` |
claim corrected |
| `packages/cli/test/validate-per-package-authoring-seam.test.ts` |
claim corrected |

Each 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`** (objectstack-ai#18677). Its
non-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`** (objectstack-ai#18780) — found by CI,
not
locally, and the local gap was mine: `packages/cli`'s `test` script is a
bare
`vitest run` with **no `--project` filter**, so CI runs both projects,
while I
had run `--project unit` plus 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:

```
AssertionError: expected 0 to be greater than 0
  test/build-text-face-advisory-count.test.ts:239
  > the fixture reaches the per-package pass and raises a survivor there
```

`bc_account.industry` had no consumer anywhere, so the union raised it
too and
the "survivor" was that finding re-reported at the package-local index.
`orders`
now owns the view that displays it. ⛔ `toBeGreaterThan(0)` is untouched
— it is
what stops objectstack-ai#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: 4`
reading in objectstack-ai#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 `where` still share a key, exactly as they already did
whenever
their 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.ts` holds something
narrower
than "every rule names its entity in `where`": it fails any rule that
puts a
**bare config path** in `where`. Measured instead over every example
stack in
this repo that parses today (`app-multi-package`'s built artifact,
`app-crm`,
`app-showcase`, `app-todo`): 45 registry rules, 103 findings, **103
distinct
neutralised keys, 0 collisions**, on `a43b9d0654`.

## Verification

- **Pin red before / green after.**
`test/per-package-dedup-positional-echo.test.ts`,
key reverted to base in place: `2 failed | 4 passed` (the ECHO and
REAL_UNION
cases). Key restored: `6 passed`. On-disk mutation proved by blob hash
both
ways; the test imports the mutated module through a relative source
specifier,
  so no build sits between mutation and assertion.
- **The control can fail.** An ablation widening the rewrite from the
top-level
index to *every* index turns exactly one case red — the NESTED control.
The
first draft of that control was built on a `field-no-consumers` twin and
stayed **green** under the same ablation, so the fixture now carries a
bare
`unique: true` index to give the control a finding whose path really has
a
  nested index. A control that cannot fail is decoration.
- **Tier measured in both directions**, from the predicate rather than
the
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.
- **Changeset decided by measuring the built `dist`**, with controls
both ways:
the rewritten key is present in
`packages/cli/dist/utils/artifact-packages.js`
and `files[]` ships `dist`; positive control
`runPerPackageAuthoringRules`
present; 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 test` run **WHOLE** — no `--project`,
no
file list, exactly what CI runs: **267 files / 3485 tests passed, exit
0**.
  The same command at `15cc0db8d4` reproduced CI's red first:
`3 failed | 264 passed (267)`, of which the one real assertion was
objectstack-ai#18780's
lit 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-loads` first returned exit 3 — its own text says
"This is
NOT a pass: nothing was measured", 8 packages had no `dist` in this
worktree. I
built them and re-ran it: exit 0.

## Acceptance notes

- **Two pending changesets still assert the falsified sentence** and are
**not**
touched here: `.changeset/18677-validate-per-package-authoring-pass.md`
and
`.changeset/18778-lint-per-package-authoring-pass.md`.
`check:empty-changeset`
refuses 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 objectstack-ai#18677's changeset says "After: both report
4",
  which this change makes read 3.
- Derivation ran against a tree behind `origin/main` (the derivation's
own
staleness warning named `scripts/gen-sdui-manifest-node.mjs` among
others);
  CI judges the merge.
- Noted, not filed: `bin/run-dev.js` needs `TSX_TSCONFIG_PATH` pinned
when
invoked 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](https://claude.ai/code)_

---------

Co-authored-by: Claude <noreply@anthropic.com>
akarma-synetal pushed a commit to akarma-synetal/framework that referenced this pull request Sep 28, 2026
…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>
akarma-synetal pushed a commit to akarma-synetal/framework that referenced this pull request Sep 28, 2026
…objectstack-ai#18677 / objectstack-ai#18778 pending changesets (objectstack-ai#19531)

Fixes objectstack-ai#19245

Clause-②: no

Two **pending, unreleased** changesets each asserted the sentence
`packages/cli`'s own source now explicitly forbids restating, and each
attributed it to that source. A changeset body ships verbatim into
`packages/cli/CHANGELOG.md`, so this correction costs a diff today and a
published falsehood after the release that consumes them. **Prose only**
— two `.changeset/*.md` files, no code path changes, no export, no key,
no accept set moved.

**The two shas this diff is actually between:** merge base
`eec56c37dfc89086658c1659bb2da869dfa08d4f` (the tip of `origin/main` at
branch time *and* at every measurement below) → head
`2babd1876786eb135509df70ab4fc2ee0113a7f3`. Verified with `git
merge-base origin/main HEAD`, not assumed from the branch point.

## 1. The settled bound, read at the head this branch points at

⛔ No fourth phrasing was invented. Both replacement sentences are the
tree's own, read from the two docblocks the card names:

`packages/cli/src/commands/compile.ts:465-470`

> `findingKey` now neutralises the top-level collection index, so what
survives is the set of per-package findings no union finding already
carried under the same rule, `where`, message and non-top-level
position. ⛔ Do not re-inflate that to "exactly the set the union could
not see" — `utils/artifact-packages.ts` states the bound and why it is
narrower than that sentence.

`packages/cli/src/utils/artifact-packages.ts:237-244`

> What reaches the lists below is therefore the set of per-package
findings whose `rule`, `where`, `message` and NON-top-level position no
union finding already carried. ⚠️ That is the whole claim, and it is
deliberately narrower than "exactly the set the union could not see" — ⛔
do not restate it as that sentence. Two entries rendering the same
`where` still collapse (see `findingKey`) …

Both halves are carried into both entries: the **bound**, and the
**reason it is narrower** (two entries rendering the same `where` still
collapse). An amended note that stated the bound and dropped the caveat
would be the same defect one notch smaller, so neither entry drops it.

⚠️ Deliberately, **neither replacement reproduces the retired sentence
verbatim**, not even as the "narrower than" contrast the source uses.
The source can quote it because the source is the thing that prohibits
it; a release note quoting it would put the sentence back into
`CHANGELOG.md`, which is the entire cost this card exists to avoid — and
it would leave the population sweep in §3 reading 3 again.

## 2. What each entry now says

| file | before | after |
|:--|:--|:--|
| `.changeset/18677-validate-per-package-authoring-pass.md:7` | "By
`compile.ts`' own description the survivors of that second pass are
«exactly the set the union could not see», so that whole set was
findings `os build` reported and `os validate` **structurally could
not**." | "By `compile.ts`' own description the survivors of that second
pass are the per-package findings no union finding already carried under
the same rule, `where`, message and non-top-level position —
deliberately narrower than everything the union run missed, because two
entries rendering the same `where` still collapse. That whole set was
findings `os build` reported and `os validate` **structurally could
not**." |
| `.changeset/18778-lint-per-package-authoring-pass.md:10-13` | "every
finding that pass produces — «exactly the set the union could not see»,
in the build command's own words — was reported by the command that
ships and invisible on the fastest of the three doors." | "every finding
that pass produces — in the build command's own words, the per-package
findings no union finding already carried under the same rule, `where`,
message and non-top-level position — was reported by the command that
ships and invisible on the fastest of the three doors. That bound is
deliberately narrower than everything the union run missed: two entries
rendering the same `where` still collapse." |

Both attributions are now **accurate**: `compile.ts` does say the
replacement, in those words. The downstream conclusion each entry draws
is untouched and still holds — a survivor is by construction something
the union run did not report, so it remains a finding `os build`
reported and the other door structurally could not. Only the **size**
claimed for that set moves, which is exactly the correction objectstack-ai#18779
landed in the code and did not reach these two notes.

⭐ **One correction, not two.** The two entries needed the same
substantive change; only the sentence surgery differed, because one file
is unwrapped prose and the other is hard-wrapped at 80 columns. Each
file's own wrapping convention is preserved.

⭐ **The `**BREAKING**` section PR objectstack-ai#18867 just added to `18677-…md` is
not touched.** The assertion sits at `:7`; that banner and its table
occupy `:22-29`. `git diff` shows one changed line in that file.

## 3. The sweep over the whole `.changeset/` population, with lit
controls

⚠️ **The first instrument was wrong and is reported rather than quietly
replaced.** Its normaliser collapsed `\s+` only. That is enough for
markdown, but inside a block comment the sentence's line wrap carries a
`*` continuation marker, so a bare `\s+` bridge does **not** join
`"exactly the` to `set the union could not see"`. On the tree scan in §4
it silently undercounted **13 → 11**. The corrected normaliser strips
each line's comment-continuation prefix (`*`, `//`, `#`) *before*
collapsing whitespace. Every number below is from the corrected
instrument; both are kept in the report.

Whole population, whitespace-normalised, case-insensitive, **every hit
opened**:

| run | population | lit control | needle |
|:--|:--|:--|:--|
| `.changeset/` @ `origin/main` `eec56c37df` (before) | **554** files |
superset "the union could not see" → **3** occurrences, INSTRUMENT LIT |
**3** |
| `.changeset/` @ head `2babd18767` (after) | **554** files | superset →
**1**, INSTRUMENT LIT | **1** |

The three before, each opened, ⛔ not counted:

1. `18677-…md:7` — assertion, attributed to `compile.ts` → **corrected
here**
2. `18778-…md:11` — assertion, attributed to the build command →
**corrected here**
3. `18779-…md:52` — *"Also corrected: the sentence «…», **which was
false** for as long as the key was positional"* → **quoted correction,
untouched**

The one after is row 3. ✅ **The only surviving hit in the whole
population is `18779`'s quoted correction**, which is what the claim
comment asked to be shown rather than trusted.

⚠️ A note on the population figure: the card recorded **482** changesets
at `847e5773a`. At `eec56c37df` it is **554**. The tree moved; the count
is re-measured here, ⛔ not inherited. The needle count is unchanged at
3, which is the number that matters.

⚠️ A second lit control was run on the before-sweep and is recorded
because a zero is not a reading until a control hits: the distinctive
string `position-insensitive, not collision-proof` returned exactly
**1** file (`18779-…md`), confirming the loop was reading real bytes at
real paths — the failure mode the card records (a loop that
double-prefixed `.changeset/` and returned a wholly convincing `0`).

## 4. `.changeset/18779-…md` is untouched, and this PR makes its
completeness claim true

`18779-…md:55-56` claims the sentence *"is now stated at the bound the
pass can actually hold, **in every file that carried it**"*. That claim
was **false on `origin/main`**, falsified by the two rows above. It is
true at this head. Measured over the **whole tree**, not just
`.changeset/` — 9128 tracked text files at head, corrected normaliser,
lit control 15 occurrences of the superset in 10 files:

**13 occurrences in 10 files, every one opened and classified. Zero are
assertions.**

| site | class |
|:--|:--|
| `.changeset/18779-per-package-dedup-positional-key.md` | quoted
correction |
| `packages/cli/src/commands/compile.ts` ×2 | 1 quoted correction ("used
to end … and that was FALSE"), 1 **prohibition** ("⛔ Do not re-inflate
that to") |
| `packages/cli/src/utils/artifact-packages.ts` ×3 | 1 "narrower than"
contrast (`:101`), 1 historical account that names **objectstack-ai#18677 and objectstack-ai#18778
by number** as the two that quoted it, 1 **prohibition** ("⛔ do not
restate it as that sentence") |
| `packages/cli/src/commands/lint.ts` | quoted correction |
| `packages/cli/src/commands/validate.ts` | quoted correction |
|
`packages/cli/test/{lint,validate}-per-package-authoring-{parity,seam}.test.ts`
×4 | quoted corrections |
| `packages/cli/test/per-package-dedup-positional-echo.test.ts` | quoted
correction |

⇒ Nothing in the tree still **asserts** the sentence. Editing
`18779-…md` would be a no-op that spends a third file on the serial. ⛔
Left alone, exactly as ordered.

## 5. ⭐ The written confirmation `Check Changeset` route 0 asks for —
and why that check stays red

`node scripts/check-empty-changeset.mjs --base origin/main` exits **1**
here, and that is **by design**, ⛔ not a defect and ⛔ not a finding
about the gate. The route-0 discriminator
`.github/workflows/pr-automation.yml` prescribes was run rather than
reasoned about:

```
$ git merge-base origin/main HEAD
eec56c3
$ git diff --name-status eec56c3 HEAD -- '.changeset/*.md'
M	.changeset/18677-validate-per-package-authoring-pass.md
M	.changeset/18778-lint-per-package-authoring-pass.md
```

Every row is `M`, none is `A` ⇒ the **DELIBERATE CORRECTION** class. The
workflow's own instruction for it, verbatim:

> -> do NOT apply 'skip-changeset'. Write the confirmation on the PR --
name the note and what changed under it, and get it confirmed there in
writing -- and LEAVE THIS CHECK RED.
> … Ruled on objectstack-ai#18375 (ruling D, maintainer 2026-09-18): the
'skip-changeset' label is never applied to a PR that edits an existing
changeset.

**So, saying it, as the gate asks:**

- **The notes:**
`.changeset/18677-validate-per-package-authoring-pass.md` (PR objectstack-ai#18769's,
amended by PR objectstack-ai#18867) and
`.changeset/18778-lint-per-package-authoring-pass.md` (PR objectstack-ai#18778's).
Both pending, both unreleased.
- **What changed under them:** ⛔ nothing in this PR's code — this PR
contains none. What changed under them earlier is **objectstack-ai#18779**, which
neutralised the top-level collection index in `findingKey` and in the
same stroke retired the sentence both notes quote. Both notes were
written before that landed and were never revisited; the source they
cite was, and now prohibits the sentence by name.
- **What is asked:** confirmation that these two pending release notes
may be corrected in place. ⛔ Restoring either from the base would
republish a sentence the shipping code contradicts. This PR stays
**draft** until that confirmation.

⚠️ **This PR carries no label, and the red is expected on CI.** Step 11
of `changeset-check` ("Require a changeset (or the skip-changeset
label)") fails first — this PR adds no changeset of its own — which
short-circuits the job, so the foreign-changeset refusal above never
executes on CI at all. `Check Changeset` is not a required context, so
its red blocks no merge and an approver merges over it. ⭐ Measured on
the adjacent precedent rather than recalled: PR objectstack-ai#18867's head
`1056c00195` shows `Check Changeset` = **failure** on both runs, its
final label set is `documentation, size/s, tooling` with **no
`skip-changeset`**, and it merged. The route-0 block and ruling D are
present in `pr-automation.yml` at objectstack-ai#18867's own merge commit
`03008c7e1a`, so that ruling is not newer than the precedent.

## 6. Verification

All **19** commands derived for this surface, re-derived at this head —
`node scripts/pm/dispatch-gates.mjs --commands --repo
objectstack-ai/objectstack`, which reported `--repo … checked against
this checkout's 'origin' remote — it holds`, change set `2 path(s) vs
merge base eec56c3`, and emitted **exactly the 19** the dispatch
named. Exit codes captured by redirecting first (⛔ never through a
pipe).

| # | command | exit |
|:--|:--|:--|
| 1 | `node scripts/check-adr-0087-registration.mjs --base origin/main`
| **0** |
| 2 | `node scripts/check-adr-0087-registration.mjs --self-test` | **0**
|
| 3 | `node scripts/check-changeset-no-major.mjs --base origin/main` |
**0** |
| 4 | `node scripts/check-changeset-no-major.mjs --self-test` | **0** |
| 5 | `node scripts/check-closing-keyword-parity.mjs` | **0** |
| 6 | `node scripts/check-closing-keyword-parity.mjs --self-test` |
**0** |
| 7 | `node scripts/check-comment-mask-corpus.mjs` | **0** |
| 8 | `node scripts/check-empty-changeset.mjs --base origin/main` | **1
— by design, §5** |
| 9 | `node scripts/check-empty-changeset.mjs --self-test` | **0** |
| 10 | `node scripts/pm/release-rehearsal-clone.mjs --self-test` | **0**
|
| 11 | `pnpm check:changeset-gate-self-tests` | **0** |
| 12 | `pnpm check:driver-memory-census` | **0** |
| 13 | `pnpm check:gitlink-declared` | **0** |
| 14 | `pnpm check:nul-bytes` | **0** |
| 15 | `pnpm check:objectui-changeset` | **0** |
| 16 | `pnpm check:pm-changeset-deadline-census` | **0** |
| 17 | `pnpm check:published-files` | **0** |
| 18 | `pnpm check:refd-timer-probe` | **0** |
| 19 | `pnpm check:watch-hint-literal` | **0** |

Gate 8's refusal names both files and the DELIBERATE CORRECTION class;
§5 is its remedy. Gate 1 reads `.changeset/18677-…md [BREAKING]
not-required (no-migration-prescription)` — the disposition PR objectstack-ai#18867
added survives this edit intact.

Beyond the 19, run because the derivation refuses to call their silence
a clearance:

| what | result |
|:--|:--|
| `node scripts/check-changeset-fixed.mjs` — flagged ⛔ by the
derivation, "roster under `.changeset`, which one of your paths is in" |
**exit 0** — config `fixed` group in sync with 70 public packages |
| `pnpm check:pm-governed-prose` | **exit 0** — and it names the 6
governed surfaces (`docs/adr/**` · `.claude/**` · `skills/**` ·
`AGENTS.md` · `CLAUDE.md` · `docs/NORTH-STAR.md`). `.changeset/**` is
not among them, so no governed-surface obligations attach to this diff |
| `pnpm lint` — whole repo, `eslint . --no-inline-config`, ⛔ **not
narrowed**, so no narrowing argument is owed | **exit 0** at
`2babd18767` |
| control characters — `grep -naP '[\x00-\x08\x0b\x0c\x0e-\x1f\x7f]'`
over both changed files | no hits (grep exit 1) |

**No package test or typecheck is owed and none is claimed.** The diff
touches no package source, no `exports`, no spec contract, no built
artefact and no test. `turbo`'s affected set is empty of packages for a
`.changeset/*.md` edit; that is stated as "not owed", ⛔ not as "green".

**No changeset is owed for this PR.** It declares no release of its own
— route 0 above is the discriminator, and it also forbids the
`skip-changeset` label that would otherwise declare that in writing.

**No label is written by this PR**, as ordered, and the order is
independently correct: ruling D forbids `skip-changeset` on exactly this
class.

## 7. Where this PR's reading differs from the dispatch order

⭐ Reported rather than silently accommodated, per the order's own
instruction.

- **`Clause-②: no` is declared above exactly as the seat declared it**,
and this PR's reading of the finished diff **agrees**: prose in two
unreleased notes, no export, no key, no member, no registration, no
accept set moved in either direction.
- ⚠️ **The order's label reasoning is right and its prediction is
wrong.** "Write no labels" is correct — ruling D forbids
`skip-changeset` here by name. But "⇒ your round needs no label to go
green" does not hold: PR objectstack-ai#18867, the precedent cited for it, went
**red** on `Check Changeset` and was merged over, as measured in §5.
This round will be red too, and ⛔ that red is not "a real finding about
the gate" — it is the gate's documented behaviour for this class, and §5
is the prose-and-person remedy it prescribes.
- **Nothing else in the order needed refusing.** The settled bound fits
both sentences; `18677`'s `**BREAKING**` section is untouched; the two
entries needed one correction, not two.

## Boundaries — what this PR deliberately does not do

- ⛔ It does not touch
`.changeset/18779-per-package-dedup-positional-key.md`. §4 shows why
that would be a no-op.
- ⛔ It does not touch `packages/cli/src/**`, any test,
`content/docs/releases/`, or any `packages/*/CHANGELOG.md`.
- ⛔ It does not add or change a `Clause-②:` line inside either changeset
**body**. `18778-…md` carries `Clause-②: yes (narrowing)` at `:36`; that
is a landed declaration about its own release and ⛔ not this card's to
re-grade.
- ⛔ It writes no label, and it did not route around that anywhere.

## Acceptance notes

- **Noted, not filed:** `.changeset/18677-…md:9-16` pins a measurement
to `origin/main 09e16a5` — `os build --json warnings: 4` against `os
validate --json warnings: 3` — and objectstack-ai#18779 has since moved that fixture's
build count from 4 to 3. It is ⛔ not a defect: the reading is explicitly
bound to a named sha, it was true there, and `18779-…md:19-24` publishes
the 4→3 move in the same release, so a CHANGELOG reader gets both.
Rewriting a correctly-dated historical measurement would be the larger
error. **Carrier:** none — no PR is in flight on that file and none is
predicted; recorded here because the "somebody will touch this anyway"
fallback ⛔ does not hold for `.changeset/*`.
- **Noted, not filed:** the first sweep instrument used for §4
undercounted the tree scan 13 → 11 by collapsing `\s+` without first
stripping block-comment continuation markers. That is a fact about a
throwaway script in this session, ⛔ not about any tracked file — no
gate, helper or committed tool has the defect. It is written down
because the card's own history records the opposite failure of the same
instrument class (a convincing `0`), and the pair is the argument for
the lit control. **Carrier:** none; nothing in the repo carries this
code.

---
_Generated by [Claude
Code](https://claude.ai/code/session_01QCdUBjM47SxioST9z5Zwdf)_

Co-authored-by: Claude <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation size/l tests tooling

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[finding] os build runs a per-package authoring-rule walk os validate does not — the residue #17069 left, in the same false-clean direction

2 participants