Skip to content

docs(formulas): drop the M9.1 / M9.2 phase language from the docs page and the pending changesets - #17851

Merged
claude[bot] merged 1 commit into
mainfrom
claude/issue-17323-drop-m9-phase-language-docs-half
Sep 12, 2026
Merged

claude[bot] merged 1 commit into
mainfrom
claude/issue-17323-drop-m9-phase-language-docs-half

Conversation

@claude

@claude claude Bot commented Sep 12, 2026

Copy link
Copy Markdown
Contributor

The M9.1 / M9.2 expression phases — including the promise that ast becomes required in build output — were asserted across the tree and defined in no ADR. Decision batch #123 item 1 (director seat, 2026-09-12) ruled option B, maintainer verbatim 「同意」: the phase assertions are removed, and ast stays an accepted optional persisted form with no promise of becoming required. This PR is the docs half of that ruling — the published docs page and the two still-unconsumed changesets now state the contract in the present tense: source is the canonical persisted form, ast is an optional opaque structured value. Per the ruling's item 4 (「Files under packages/spec and packages/formula are the spec seat's: the domain:skills taker edits docs and coordinates the spec half as a sibling PR by that seat, ⛔ not a cross-lane rider」), nothing under packages/ is touched here; the spec half is a sibling PR by that seat, tracked as #17849. No ADR is written — B means there is no phase to define.

Part of #17323

Acceptance notes

Premises, re-taken at this branch's base 758ac409

  1. HOLDS. git grep -l 'M9\.[12]' origin/main returns exactly seven files — .changeset/blank-node-condition-refused-at-registration.md (1 hit), .changeset/flow-edge-condition-evaluated-slot.md (3 hits), content/docs/data-modeling/formulas.mdx (1 hit), packages/formula/src/cel-engine.ts, packages/formula/src/normalize.ts, packages/spec/CHANGELOG.md, packages/spec/src/shared/expression.zod.ts. The last four are the spec seat's and are untouched here; after this PR the repo-wide count is those four and nothing else.
  2. HOLDS, with a timing note. Both changesets are present and unconsumed under .changeset/ at this base. ⚠️ The standing Version Packages PR chore: version packages #17076 (changeset-release/main, bot-authored) lists both of them as removed — it is the consuming side. So this correction has to land before that PR does; once a release consumes these files, the ruling's item 3 routes the same correction to a CHANGELOG.md erratum instead, which rides the spec half.
  3. HOLDS. formulas.mdx :27 (deleted in M9), :111 (M9.9b) and :583 (M9.5) are other milestone references and are byte-identical — they do not appear in the diff, and git grep -c 'M9' -- content/docs/data-modeling/formulas.mdx still reports 3 (now at :27 / :112 / :584, shifted by the one line this PR adds).
  4. HOLDS. All 22 open PRs' file lists were re-scanned; none names the three files, with the single qualification in premise 2 — chore: version packages #17076 lists the two changesets as consumed, which is not a competing edit.

The sentences, before → after

A · content/docs/data-modeling/formulas.mdx :86-87

- artifact always contains the full envelope (and, after M9.2, the AST).
+ artifact always contains the full envelope: `source` is the canonical persisted
+ form, and `ast` is an optional opaque structured value.

B1 · .changeset/flow-edge-condition-evaluated-slot.md :30 — inside the blockquote that reproduces the refusal sentence:

- > engine evaluates `source` (the canonical persisted form of phase M9.1) and
+ > engine evaluates `source` (the canonical persisted form) and

B2 · .changeset/flow-edge-condition-evaluated-slot.md :44-48 — the phase-2 promise itself:

-   the persistence contract (`source` OR `ast`), whose docblock declares that
-   `ast` becomes required in build output at phase M9.2. When AST-only
-   evaluation lands, `EvaluatedExpressionSchema` is the one place to relax, and
-   every evaluated slot follows.
+   the persistence contract (`source` OR `ast`), where `ast` is accepted as an
+   optional opaque structured value and carries no promise of becoming required.
+   If AST-only evaluation is ever chartered, `EvaluatedExpressionSchema` is the
+   one place to relax, and every evaluated slot follows.

B3 · .changeset/flow-edge-condition-evaluated-slot.md :73

- carried only `ast` has no evaluable form under M9.1: author its `source`. A
+ carried only `ast` has no evaluable form: author its `source`. A

C · .changeset/blank-node-condition-refused-at-registration.md :33 — the eighth site, which neither the card nor the ruling named. What it said: the same verbatim blockquote of the refusal sentence that B1 carries, with the identical (the canonical persisted form of phase M9.1) parenthetical; it is the only M9 reference in that file, and it gets the identical treatment:

- > engine evaluates `source` (the canonical persisted form of phase M9.1) and
+ > engine evaluates `source` (the canonical persisted form) and

Nothing else in the three files is touched: 3 files, 9 insertions, 8 deletions, and the changeset frontmatter (package and bump level) of both changesets is unchanged.

D · Verification

reading result
git grep -l 'M9\.[12]' -- content .changeset 0 files (exit 1, no match)
control — git grep -c 'M9' -- content/docs/data-modeling/formulas.mdx still 3 (the :27 / :112 / :584 milestones)
control — the pattern still matches elsewhere, so it is not a dead pattern: git grep -c 'M9\.[12]' 4 files, all the spec seat's
dark control — git grep -c 'M91zzz' -- content .changeset 0 (exit 1), so the pattern does not match everything
grep -naP for raw control bytes over the three files clean (exit 1)

Coordination with the spec half, #17849

The blockquote edited in B1 and C is a verbatim quote of an exported constant — EVALUATED_EXPRESSION_SOURCE_REQUIRED in packages/spec/src/shared/expression.zod.ts, which still carries of phase M9.1 on main. This PR drops exactly that phase clause and keeps (the canonical persisted form), because "source is the canonical persisted form" is the present-tense contract the ruling's item 1 names. For the quote to stay a quote, the spec half must land that same wording in the constant. Flagged rather than worked around: packages/spec is not this lane's, per item 4.

Changeset: skip-changeset, measured

Two existing changesets are edited and none is added, so the changeset count this PR adds is 0 and the label is the applicable rule (AGENTS.md Post-Task Checklist step 3: the label is for a diff that publishes nothing from any released package). Measured rather than assumed:

  • no published package's files[] ships content/** or .changeset/** — 0 matching entries across every non-private workspace manifest;
  • the added strings (carries no promise of becoming required, is an optional opaque structured value) appear in 0 published paths;
  • positive control — canonical persisted form does hit a published path (packages/spec/CHANGELOG.md, packages/spec/src/shared/expression.zod.ts), so the measurement is not vacuous.

The publication this correction affects is already declared by the two changesets it edits (@objectstack/spec minor, @objectstack/service-automation minor); a third changeset would declare a release of its own for a prose correction that rides theirs. The CHANGELOG.md erratum for the already-published M9.2 text is the ruling's item 2 and belongs to the spec half.

Gates

Derived in this worktree with node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack (no paths — the tool took the change set from git itself at 9e12d4a62 vs merge base 758ac4097): 51 commands. Every one was run; reconciliation with --ran reports 51 derived, 51 run, 0 NOT-MEASURED, 0 UNRUN, all exit 0.

Four of them first exited 3 / 1 as PREREQUISITE NOT MET — nothing measured, unbuilt workspace packages, not findings: check:doc-formula-expressions and check:doc-security-posture (@objectstack/formula, @objectstack/lint unbuilt), check:docs-transcript-drift (same), and check:skill-examples (packages/client-react/dist held no .d.ts). After pnpm exec turbo run build --filter='@objectstack/lint...' --filter='@objectstack/formula' --filter='@objectstack/client-react...' --concurrency=2, all four are exit 0. Verdict lines, as each gate printed them:

✓ check-adr-0087-registration: this PR adds no declared-breaking changeset (2 non-breaking changeset(s) seen).
✓ This diff introduces no `major` bump.
✓ No empty-frontmatter changeset introduced by this diff (2 declaring changeset(s) added).
✓ doc authoring guard: 401 files clean — no bare metadata literals.
check-nul-bytes: OK (scanned 8476 text file(s) -- 8476 tracked, 0 untracked-not-ignored; skipped 7 binary; no raw ASCII control bytes).
✓ check:doc-formula-expressions: 22 record-scoped formula example(s) across 438 files / 1374 TS blocks judged clean by @objectstack/formula.
✓ check-docs-transcript-drift: 4 declared transcript value(s) across 402 page(s) under content/docs/ equal what the registry derives today, and no undeclared block quotes one.

node scripts/check-changeset-fixed.mjs was run as well and is exit 0 — the derivation flags its roster as living under .changeset, one of this PR's paths, so its silence is evidence in neither direction until run. It reads .changeset/config.json and the workspace manifests, neither of which this PR touches.

Repo-wide scans (pnpm lint and the rest) are CI's, as are the two path-scheduled CI jobs and the 11 declared-wide families the derivation names as unplaceable locally.

Noted, not filed

  • scripts/check-adr-0087-registration.mjs's pass line labels every skipped changeset "non-breaking" (${result.skipped.length} non-breaking changeset(s) seen), so the two changesets here — both declared breaking at base and skipped as inherited stock by the specified M breaking at head, already breaking at base row — are reported as non-breaking. The verdict is correct and the row is a pinned self-test case; only the summary wording under-describes. A pass-line wording nit, not a defect class, not a contract violation and not an authoring trap, so no card. Taker: none — no queued PR touches that script.

Generated by Claude Code

…e and the pending changesets

The M9.1 / M9.2 expression phases — including the promise that `ast` becomes
required in build output — were asserted across the tree and defined in no ADR.
Decision batch #123 item 1 ruled option B: the phase assertions are removed and
`ast` stays an accepted optional persisted form with no promise of becoming
required. This is the docs half: the published page and the two unconsumed
changesets now state the contract in the present tense — `source` is the
canonical persisted form, `ast` is an optional opaque structured value.

Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MCLBsUgfykL74aU716rzVK
@claude claude Bot added the skip-changeset PR has no user-facing published change; bypasses the changeset gate label Sep 12, 2026
@github-actions github-actions Bot added documentation Improvements or additions to documentation tooling labels Sep 12, 2026
@claude
claude Bot marked this pull request as ready for review September 12, 2026 12:44
@claude
claude Bot added this pull request to the merge queue Sep 12, 2026
Merged via the queue into main with commit 2cc4884 Sep 12, 2026
38 checks passed
@claude
claude Bot deleted the claude/issue-17323-drop-m9-phase-language-docs-half branch September 12, 2026 13:17
akarma-synetal pushed a commit to akarma-synetal/framework that referenced this pull request Sep 17, 2026
…e — drop the M9.1 / M9.2 phase language (objectstack-ai#18557)

Part of objectstack-ai#17849

Clause-②: no

The spec-seat half of objectstack-ai#17323, ruled **B** by the director seat (comment
`5644710461`, decision batch objectstack-ai#123 item 1, 2026-09-12; maintainer
verbatim 「同意」). The M9.1 / M9.2 phase language is dropped from the
citation sites in `packages/spec` and `packages/formula`, and the
contract each of them describes is restated in the present tense. ⛔ No
accept-set change: no key is added, renamed, re-typed or retired, and
every envelope that parsed before parses byte-identically.

The docs half (PR objectstack-ai#17851, `Part of objectstack-ai#17323`) merged on
2026-09-12T13:17:02Z, so `content/docs/data-modeling/formulas.mdx` and
the two pending changesets already carry the new wording. This PR is the
other half the ruling's item 4 assigns to this seat — `packages/formula`
included, which the domain table calls `domain:engine` and the ruling
puts here verbatim.

## What the text says now

- `source` is the canonical persisted form — it is what the engine
evaluates.
- `ast` is accepted beside it as an optional opaque structured value
(each engine validates its own shape), and it carries no promise of
becoming required.
- A slot whose value the engine RUNS requires `source`;
`EvaluatedExpressionSchema` is where that is spelled out.

| file | `M9\.[12]` hits at `79a046f` | after |
|:--|--:|--:|
| `packages/spec/src/shared/expression.zod.ts` | 7 | 0 |
| `packages/formula/src/cel-engine.ts` | 1 | 0 |
| `packages/formula/src/normalize.ts` | 1 | 0 |

Lit control for the same probe: `ExpressionSchema` in
`expression.zod.ts` reads 19, so the zero is a reading and not a broken
path.

`cel-engine.ts`'s AST-only arm also carried an `M9.7` promise ("revisit
AST-only execution in M9.7 when we cut the spec persistence over")
**inside the same sentence** as its `M9.1` hit. It goes with the
sentence rather than being left as a phase promise standing beside a
de-phased one; it is named here rather than smuggled.

## The verbatim-quote constraint, and the proof it holds

Coordination note `5645945565`: the two pending changesets quote the
exported constant `EVALUATED_EXPRESSION_SOURCE_REQUIRED` VERBATIM inside
a blockquote, and the docs half already edited them to read 「(the
canonical persisted form)」. The constant loses exactly the four words
`of phase M9.1` and nothing else, so the quote stays a quote. Proved
mechanically rather than by eye — the constant's runtime value was
reassembled from the source and compared with each changeset's
blockquote, joined:

```
.changeset/flow-edge-condition-evaluated-slot.md              ==> BYTE-IDENTICAL to the constant
.changeset/blank-node-condition-refused-at-registration.md    ==> BYTE-IDENTICAL to the constant
```

No consumer parses that sentence: every call site imports the constant
by name, and the message assertions in `packages/lint`,
`packages/services/service-automation` and `packages/spec` read
substrings that never contained the phase id — the non-blank-source
clause and the cannot-evaluate-ast clause.

## ⚠️ Declared deviation — the `packages/spec/CHANGELOG.md` erratum is
NOT in this PR

The card's work items and the ruling's item 2 also name an erratum line
under the already-published `## 17.4.0` entry that promised M9.2 (3 hits
at `:1096`, `:1103`, `:1119`). It is **deliberately not carried here**,
because AGENTS.md's Documentation Guardrails row for
`packages/*/CHANGELOG.md` is categorical about the packaging:

> ❌ Never edit in a code PR … Factual error in a released entry →
**amend that entry in a dedicated docs-only PR**, ⛔ never an erratum in
a later entry and never a rider on code changes

This PR is a code PR: it moves a published string and owes a `patch`
changeset. The ruling names 「the objectstack-ai#17026 shape」 for the erratum, and that
shape — read off the card objectstack-ai#17026 and PR objectstack-ai#17896 that executed it — **is**
a dedicated docs-only PR with `skip-changeset`, whose executing seat
quoted this same AGENTS.md row verbatim as its authorization chain. So
the ruling and the guardrail agree; only the card's file-surface
bundling puts them in one PR.

⇒ The erratum is owed as a **separate docs-only PR**, and objectstack-ai#17849 stays
open for it — which is why this body reads `Part of objectstack-ai#17849` and not the
closing form the dispatch asked for. The residual after this PR,
measured on this branch:

```
$ git grep -lE 'M9\.[12]' -- packages
packages/spec/CHANGELOG.md          # 3 hits — the erratum's target, owed as a docs-only PR
```

Nothing under `content/` remains; the docs half cleared it.

## Verification

Everything below is a real local run, exit codes landed to disk before
reading (⛔ never through a pipe).

| run | verdict |
|:--|:--|
| `pnpm --filter @objectstack/spec --filter @objectstack/formula build`
| `VERDICT command-exit 0` (shared verify lock) |
| `pnpm --filter @objectstack/spec check:generated` | exit 0 — ✓ all 15
generated artifacts up to date (nothing to regenerate: no `.describe()`
moved) |
| `pnpm --filter @objectstack/spec --filter @objectstack/formula
typecheck` | exit 0 |
| `pnpm --filter @objectstack/spec --filter @objectstack/formula test` |
exit 0 — spec 483 files / 13773 tests, formula 29 files / 856 tests |
| `pnpm lint` (`eslint . --no-inline-config`, whole repo) | exit 0 |
| derived gate families (`dispatch-gates.mjs --repo
objectstack-ai/objectstack --commands`) | 78 derived · 76 run green · 2
NOT MEASURED |

`--ran` reconciliation with exit codes recorded per family: `78 derived
famil(ies) accounted for — 76 run, 2 NOT-MEASURED (2 DERIVED from a
recorded exit 3)`. The two are `check:dual-build-cjs-loads` and
`check:lean-entry-closure`, both exit **3 = PREREQUISITE NOT MET** (they
read the `dist/` of ~80 packages this tree never built) — ⛔ neither a
pass nor a failure; CI's `Build Core` is where they run.

Two families needed a prerequisite before they could answer, and both
are green once given it: `check:doc-formula-expressions` (exit 3 until
`@objectstack/lint` was built, then exit 0) and
`check-plugin-teardown-shape --self-test` (exit 1 on this shallow clone
until its pinned fixture commit was fetched, then exit 0 / 47 cases).

## Acceptance notes

⚠️ **One gate reds on this tree and it is NOT this diff** — `pnpm
check:cross-package-test-inputs` (exit 1), flagging `@objectstack/cli
descends a directory tree from packages/spec/dist/`, rooted in
`packages/cli/test/init-created-files-summary.e2e.test.ts`. Ablated to
the cause on a **pristine `origin/main` worktree with zero diff**:

```
node scripts/check-cross-package-test-inputs.mjs          -> exit 0  (OK: 29 package(s) … 13 walked root(s) judged)
mkdir -p packages/spec/dist  &&  (same command)           -> exit 1  (the identical failure text)
rm -rf packages/spec/dist    &&  (same command)           -> exit 0  (restored; tree clean)
```

⇒ the verdict turns on whether the gitignored `packages/spec/dist`
exists, not on any source change — and building `packages/spec` is
exactly what AGENTS.md requires before the spec artifact gates run. CI
cannot see it: `Lint & Repo Gates` installs and never builds, so the
gate is green there. Filed as a finding for the dispatching seat rather
than fixed here — `packages/cli` and
`scripts/check-cross-package-test-inputs.mjs` are both outside this
card's file surface.

Noted, not filed: nothing else. The three source files were read whole
and carry no other stale phase promise; the remaining `M9.x` mentions in
the tree (`M9.9b` in `default-value-shape.ts`, `action.zod.ts`,
`objectql/engine.ts`; `M9.5` / `M9.7` in `objectql` and the docs page)
are ROADMAP section references outside the ruling's seven citation
sites, and the ruling names only the M9.1 / M9.2 phase promise.

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

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>
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

Development

Successfully merging this pull request may close these issues.

1 participant