Skip to content

Commit 03008c7

Browse files
docs(cli): #18769's pending changeset declares the os validate --strict narrowing an at-tier review exhibited (#18867)
Fixes #18823 Clause-②: no `.changeset/18677-validate-per-package-authoring-pass.md` — PR #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 #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 #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-#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 #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 (#5471) because an all-empty set makes `changesets/action` return green while publishing nothing (#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 #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 #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 — #18126 (the same repair, BREAKING banner + ADR-0087 disposition onto a pending entry) and #17851 — both carried `skip-changeset`. Measured, not recalled: the foreign-changeset refusal landed in #18146 at `0ffb4963e5` **2026-09-14T06:56:58Z** and #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 #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 #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 (#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>
1 parent 2d892dd commit 03008c7

1 file changed

Lines changed: 16 additions & 1 deletion

File tree

‎.changeset/18677-validate-per-package-authoring-pass.md‎

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,21 @@ After: both report 4, the same set, in the same order.
1717

1818
**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).
1919

20-
**Severity mapping is `os build`'s, unchanged.** A per-package `error` refuses (exit 1); an advisory joins `warnings`. So `os validate` is narrowed only to the bar the command that *ships* already holds: every input it can now refuse is one `os build` already refuses, which means **nothing that builds today stops validating**. No newly-refused input could be exhibited on any fixture — across the repo's own two-package example and three constructed variants the observable change is advisory-only, because `packageBodyAsStack` hands each package the artifact's whole `packages[]` as resolution context and the reference-integrity suite resolves object names through it. Graded `minor` rather than `patch` for the new observable step line, the new advisories and the newly reachable non-zero exit; ⛔ **not** declared breaking, because the narrowing could not be exhibited and is bounded by an existing gate.
20+
**Severity mapping is `os build`'s, unchanged.** A per-package `error` refuses (exit 1); an advisory joins `warnings`. So `os validate` is narrowed only to the bar the command that *ships* already holds: every input it can now refuse is one `os build` already refuses.
21+
22+
**BREAKING** — `os validate --strict` can now fail a project it passed before. Measured on a two-package fixture whose union fold is clean and whose per-package run is not (`core` owns `pp_account`; a sibling package owns the view that displays `pp_account.industry`), driving the CLI from source:
23+
24+
| `os validate` on that fixture | before | after |
25+
|---|---|---|
26+
| `--json` | warnings 0, exit 0 | warnings 1, exit 0 |
27+
| `--json --strict` | exit 0 | **exit 1** |
28+
29+
The one warning is `field-no-consumers` at `package 'com.example.ppflip.core' — object "pp_account" · field "industry"`, which `os build` already reports on the same fixture: nothing is refused here that `os build` does not already refuse, and the default (non-strict) face is unchanged in that measurement. A run that must keep its old verdict drops `--strict`; a project that wants to keep the flag fixes what the per-package pass reports, which is what `os build` has been reporting all along.
30+
31+
Why the union fold does not see it: `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 therefore live to the union run and inert to the per-package run, and that is the shape that reaches `--strict`.
32+
33+
Graded `minor` rather than `patch` for the new observable step line, the new advisories and the newly reachable non-zero exit; the launch window refuses `major`, so the breaking-ness is carried by the banner above and the ADR-0087 disposition below.
2134

2235
Unchanged and out of scope: the ADR-0130 D4 union fold (#17069, fixed — `authoringRuleUnionStack` is in both commands), `--json` rendering (#11727), and disagreements *within* the per-package pass's verdicts (#18204). `os lint` still runs the union pass alone; its `artifactPackages` / `packageBodyAsStack` imports serve its own intra-package duplicate-name advisory, not the shared table.
36+
37+
<!-- adr-0087: not-required (no-migration-prescription) Nothing an author writes changes: no spec key, export, config field or payload key is removed, renamed or added. What moved is which stacks one CLI command's existing rule table is run over, so `objectstack migrate meta` has nothing to rewrite and the ledger has nothing to record. -->

0 commit comments

Comments
 (0)