Skip to content

fix(changeset): declare the installed-package response type break on plain-donkeys-repeat - #18126

Merged
claude[bot] merged 1 commit into
mainfrom
claude/issue-18057-changeset-breaking-declaration
Sep 14, 2026
Merged

claude[bot] merged 1 commit into
mainfrom
claude/issue-18057-changeset-breaking-declaration

Conversation

@claude

@claude claude Bot commented Sep 14, 2026

Copy link
Copy Markdown
Contributor

Fixes #18057

The defect

.changeset/plain-donkeys-repeat.md describes a compile-affecting change to two published response types in prose, and declared it in no metadata.

Measured on the file at the branch point ca788604:

predicate this file lit control, .changeset/15110-retired-element-node-refusal.md
BREAKING 0 1
adr-0087 0 1

The control runs the same predicate over the same directory, so the zero is a real negative rather than a dead grep. @objectstack/spec is at 17.4.0 and the changeset is still pending, so this corrects an unconsumed declaration rather than published history.

⛔ The runtime claim in that body is not disputed and is untouched: the change is genuinely additive at runtime. The defect is the declaration alone, and nothing outside .changeset/ is edited.

What this changes — one file, two additions, level untouched

  • a **BREAKING** banner naming the type-level break on the two published response types in both directions: reading a manifest field off ListInstalledPackagesResponseSchema / GetInstalledPackageResponseSchema can stop compiling, and assigning a malformed manifest to either can start compiling where the old annotation refused it. It also states that the compiler — not the ledger — is the channel that reaches every affected consumer;
  • the mandatory ADR-0087 disposition marker, in the not-required (no-migration-prescription) form.
 .changeset/plain-donkeys-repeat.md | 15 +++++++++++++++
 1 file changed, 15 insertions(+)

The level is decided mechanically, not chosen — and it stays minor

⚠️ Dispatch flagged a live dispute here (minor vs major, issue #18064) and instructed that if the level I need is the one that card is deadlocked on, I must stop. It is not deadlocked. #18064 carries state closed (completed, 2026-09-14T00:36Z) with a director-seat ruling that supersedes one of the two rulings and names the other as the convention. Its authority is already on main, in docs/adr/0087-metadata-protocol-upgrade-contract.md, section Ratified: the pre-launch launch-window exemption, amended 2026-09-13:

  • Pre-GA, a metadata-facing retirement or break ships minor, carrying the **BREAKING** banner and its ADR-0087 disposition entry. The level is not the carrier of breaking-ness during the window — the banner and the disposition are, and they are mandatory precisely because the level says nothing.
  • An npm major is a planned act — taken when the window closes and the queued conversions activate together — ⛔ never a side effect of one retirement card, however breaking that card is.

So the level was never the missing half; the two mandatory carriers were. scripts/check-changeset-no-major.mjs independently refuses a major outright, which settles the same question from the enforcement side. This PR therefore touches no side of any maintainer decision.

Was an ADR-0087 registration owed? Measured, not assumed

Adding the BREAKING token is exactly what arms check-adr-0087-registration — before this change the gate had nothing to judge. Armed, it does not demand a registration:

✓ check-adr-0087-registration: 1 declared-breaking changeset(s), each carrying an ADR-0087 disposition.
    .changeset/plain-donkeys-repeat.md  [BREAKING]  not-required (no-migration-prescription)

⇒ ⭐ no semantic entry, and packages/spec/src/migrations/registry.ts is not touched. That keeps the smaller diff, and it keeps that file free for the card serialised behind this one.

Why this disposition and not the semantically closer one

The category that describes this class is type-surface-only — the gate's own header was written for "a published type-surface narrowing … a consumer's code can stop compiling". It is mechanically unavailable to a declaration-only correction, and that was measured rather than assumed. Claiming it on a throwaway commit returns:

`type-surface-only packages/spec/src/api/package-api.zod.ts#ListInstalledPackagesResponseParsed`
[predicate 4: narrowed-from-erased] is FALSE: at the merge base the exported
`type ListInstalledPackagesResponseParsed` alias was already CONCRETE
(a `z.infer` projection of `ListInstalledPackagesResponseSchema`), not `any` / `unknown` / unannotated.

The generic parameter in that last line is spelled out rather than quoted literally, because the angle-bracket form does not survive GitHub's body sanitizer; nothing else in the excerpt is altered.

Predicate 4 reads the named symbol at the merge base and at HEAD. A PR that corrects a declaration moves no source, so the two reads are identical by construction and the predicate can never hold. registered is refused for a different and stronger reason, stated in the gate's own header: writing a ledger id for a type-only move would put a prescription in the ledger that objectstack migrate meta, spec-changes.json and the upgrade guide cannot project.

no-migration-prescription is both the honest and the mechanically-verified answer: nothing is removed or renamed, ManifestSchema is unchanged, no authorable key and no stored row shape moves, so the ledger has nothing to carry. The gate re-verifies it against the body's own prose on every run, and the body carries no prescription.

Gates

Derived for the actual diff with node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack, run at the final head 25a539f5, each exit code captured before any pipe. 19 commands, all exit 0 — the 18 derived families plus check-changeset-fixed.mjs, which the derivation flagged because its roster lives under .changeset, the directory this diff is in.

Reconciled with --ran: 18 derived famil(ies) accounted for — 18 run, 0 NOT-MEASURED (a DERIVED zero — all 18 recorded an exit code and none of them is 3).

Named in dispatch, all green: check-adr-0087-registration, check-changeset-no-major, check-empty-changeset, each with its --self-test (370 / 265 / 118 assertions).

Not owed, and measured rather than skipped:

  • no build, no package tests. The diff touches no workspace package, so there is no dependency closure to build and no affected package to test.
  • pnpm lint. eslint's configured surface in eslint.config.mjs contains no markdown block at all — every files: entry targets {ts,tsx,mts,cts,js,jsx,mjs,cjs}. eslint --format json over the one changed file returns errorCount: 0 with File ignored because no matching configuration was supplied. Since no config block matches markdown and no config file moves, this diff cannot move the verdict on any untouched file.

Acceptance notes

Out of scope, noted and deliberately not filed:

  • The Check Changeset job counts changesets with --diff-filter=A, so a PR that corrects an existing changeset is credited with zero and reds without the skip-changeset label. That is the label's designed purpose rather than a defect, and this PR is the successor that carries it.
  • type-surface-only is structurally unreachable from a declaration-only correction PR, for the predicate-4 reason measured above. By design — the gate judges the claim the diff makes — and the catch-all accepted this case, so no consumer is harmed. Successor that would hit it: none identified beyond this PR.

Clause-②: no — this corrects a declaration; it widens no accept set and no public surface. Nothing outside .changeset/ moves, so the reading holds.

🤖 Generated with Claude Code

https://claude.ai/code/session_014DBGjJFyndTj766aReCL2g


Generated by Claude Code

`.changeset/plain-donkeys-repeat.md` describes a compile-affecting change to two
published response types in prose and declared it in no metadata: `BREAKING` and
`adr-0087` both counted 0 on the file while `@objectstack/spec` sat at 17.4.0,
still unreleased.

The level is unchanged and stays `minor`: ADR-0087's launch-window section, as
amended on 2026-09-13, states that pre-GA a metadata-facing break ships `minor`
carrying the BREAKING banner and its ADR-0087 disposition, and
`check-changeset-no-major.mjs` refuses a `major` outright. So the two mandatory
carriers are what was missing, and they are what this adds.

The disposition is `not-required (no-migration-prescription)`: nothing is removed
or renamed, `ManifestSchema` is unchanged, and no stored row shape moves, so
`objectstack migrate meta` has nothing to rewrite. The runtime claim in the body
is untouched — the change is genuinely additive at runtime.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014DBGjJFyndTj766aReCL2g
@claude claude Bot added the skip-changeset PR has no user-facing published change; bypasses the changeset gate label Sep 14, 2026
@github-actions github-actions Bot added documentation Improvements or additions to documentation tooling labels Sep 14, 2026
@claude
claude Bot marked this pull request as ready for review September 14, 2026 03:59
@claude
claude Bot enabled auto-merge September 14, 2026 03:59
@claude
claude Bot added this pull request to the merge queue Sep 14, 2026
Merged via the queue into main with commit f3b41e8 Sep 14, 2026
32 checks passed
@claude
claude Bot deleted the claude/issue-18057-changeset-breaking-declaration branch September 14, 2026 04:39
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>
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/s skip-changeset PR has no user-facing published change; bypasses the changeset gate tooling

Projects

None yet

1 participant