Commit 03008c7
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
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
17 | 17 | | |
18 | 18 | | |
19 | 19 | | |
20 | | - | |
| 20 | + | |
| 21 | + | |
| 22 | + | |
| 23 | + | |
| 24 | + | |
| 25 | + | |
| 26 | + | |
| 27 | + | |
| 28 | + | |
| 29 | + | |
| 30 | + | |
| 31 | + | |
| 32 | + | |
| 33 | + | |
21 | 34 | | |
22 | 35 | | |
| 36 | + | |
| 37 | + | |
0 commit comments