You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
Commit 8b48903
Browse filesBrowse the repository at this point in the historyBrowse files
feat(spec): ship a per-release section in spec-changes.json, verified against both tarballs (#18889)
Fixes#17080
Clause-②: yes (widening)
Ruled **A** (issue comment 5643392133, decision batch #119 item 2,
maintainer 「同意」 2026-09-12):
minors ship real, machine-readable change data, with a correctness gate.
All four execution
clauses land here.
## The defect, re-measured from the published tarballs
`npm pack @objectstack/spec@17.3.0 @objectstack/spec@17.4.0`, then the
repo's own export-surface
row convention (`ENTRY: NAME (KIND)` — entry point, export name, export
kind — the spelling
`build-spec-changes.ts` flattens to):
| | 17.3.0 | 17.4.0 |
|---|---|---|
| export-surface rows | 5259 | 5433 |
| **true delta** | — | **225 added, 51 removed** |
| `spec-changes.json` → `aggregate.added` / `.removed` | 0 / 0 | **0 /
0** |
| `protocolVersion` | 17.0.0 | 17.0.0 |
| `perMajor[16→17]` | converted 58, migrated 77 | converted 57, migrated
77 |
| `[REMOVED]` prescriptions in `json-schema/**` | 156 | 221 |
Every figure the card reports reproduced. The shipped manifest answered
`0 / 0` over a release
that moved 276 exports, and a consumer reading semver sees a minor.
## What lands
**1 · A per-release section, shipped in the tarball.**
`spec-changes.json` gains `release` —
`fromVersion` → `toVersion` at package-version resolution, with `added`
/ `removed` (the exports
that arrived and left, each named) and `converted` / `migrated` (the
ADR-0087 D2/D3 entries first
registered in that release). Generated at **publish time only**, from
the previously published
tarball: the committed copy stays the deterministic registry projection
and `check:spec-changes`
is green against it, which is how the determinism concern is answered
rather than traded away.
**2 · The correctness gate, as acceptance.** Before anything reaches
npm, the release lane packs
the artifact it is about to publish and recomputes the delta from **the
two tarballs**, with its
own reader — `scripts/check-release-spec-changes.mjs` deliberately does
not import the generator's
flattening, because an instrument that shares the code it audits reports
agreement with itself.
A mismatch fails the release, naming the disagreeing exports and the
**direction** of each
disagreement (claimed-but-not-real, real-but-unclaimed, per array) plus
the command that
regenerates the section. A held release arrives with its own diagnosis.
**3 · `os validate --json` reads the same data.** New key
`specReleaseChanges` — `fromVersion`,
`toVersion`, the four counts and the file it read. It is a **sibling**
of `protocolVersionGap`,
not a widening of it: that key means "the platform on disk is outside
the range you declared",
and a consumer gating CI on it must not start failing because an
ordinary minor shipped exports.
One key, one question. (Note `specVersionGap` is spelled
`protocolVersionGap` in this tree — it
was renamed by #14261, which is still an unreleased changeset; the
ruling's clause 3 names the
old spelling.)
**4 · Published payload change.** `Clause-②: yes (widening)`,
contract-review carrier, changeset
carrying the migration-free declaration, and
`content/docs/upgrading.mdx` gains *What the
installed artifact tells you, without a second worktree*.
## Absence is not zero
The section is **omitted, loudly**, when the previous tarball ships no
export snapshot or no
manifest — an empty `release` is indistinguishable from "this release
changed nothing", which is
the misreading the whole section exists to end. The gate derives the
same condition from the same
artifacts, accepts that absence, and **refuses** a section that is
present when it could not have
been computed. `specReleaseChanges` is `null` in exactly those cases.
## Verification
Real artifacts, not fixtures — `npm pack` of published 17.3.0, `pnpm
pack` of this tree:
```
generate --previous-package (the unpacked published 17.3.0) -> pack -> gate
✓ release 17.3.0 → 17.4.0 verified against both tarballs: 386 added, 302 removed, 0 converted, 0 migrated.
```
⚠️ **Corrected by the `domain:spec` seat** — this line read 「374 added」
when the PR opened. `origin/main` has since moved the export surface,
and **386** is the number measured at the current head `1737a24b510`
from a real `npm pack` of the published 17.3.0, independently confirmed
by the at-tier review at the previous head. `os-dev.md:56` reserves this
body to the PR-open write, so the round named the number and the seat
writes it.
**Ablation, on the real artifact.** One export dropped from
`release.added` in the packed
manifest (mutation proven on disk: sha256 `bc21927e…` → `3bbff365…`),
restored under
`trap … EXIT INT TERM`, hash verified equal after:
```
exit 1
✗ the per-release section of spec-changes.json disagrees with the two tarballs (ADR-0087 D4).
A wrong change file is worse than none — a consumer gates its upgrade on this data.
release.added: 1 export(s) the two tarballs show as added and the section OMITS:
./ai: BUILD_PROGRESS_FRAME_TYPE (const)
Regenerate with: pnpm --filter @objectstack/spec exec tsx scripts/build-spec-changes.ts
--previous-package PREV_PACKAGE_DIR [the real line spells the placeholder in angle brackets]
```
Gates and tests, exit codes captured by redirect:
- `pnpm check:release-spec-changes` — 15 batteries (roster + floor +
verdict handshake), exit 0.
The real run needs two published tarballs, so the self-test is what a PR
can run; it drives the
same `verifyRelease()` the release lane calls.
- `pnpm --filter @objectstack/spec check:generated` — **all 16 artifacts
up to date** after
`gen:api-surface` + `gen:export-origins` +
`gen:api-surface-declarations` (7 new declarations on
the root entry, 0 removed).
⚠️ **Corrected by the `domain:spec` seat: this read 「all 15」 when the PR
opened.** The count moved
because #18971 (`d8b12fca97c`) registered
`check:api-surface-declarations` as a new generated
artifact family. The gate now prints 「Checking 16 generated artifacts」.
⛔ The 15 was not wrong
when written — the base moved under it. `os-dev.md:56` reserves this
body to the PR-open write, so
the round named the number and the seat writes it.
- `pnpm --filter @objectstack/spec exec vitest run --project local
src/migrations/migrations.test.ts`
— 137 passed.
- `pnpm --filter @objectstack/cli exec vitest run --project unit
src/utils/spec-release-changes.test.ts`
— 6 passed.
- `pnpm --filter @objectstack/spec typecheck` — exit 0. `pnpm --filter
@objectstack/cli typecheck`
— exit 0 (after building the runtime closure the CLI's test project
reads; its first run reported
only `TS2307 Cannot find module` for packages this worktree had not
built).
- `pnpm check:entry-guard` — exit 0 (265 files, 205 exporters inert on
import). This one caught a
real defect in the new gate: it exports `verifyRelease` and dispatched
at the top level, so
importing it would have run it. Fixed with `isEntrypoint`.
- **Gate families**, derived by `scripts/pm/dispatch-gates.mjs` from the
diff and reconciled with
`--ran`: **153 derived, 147 run** (146 exit 0), **4 NOT MEASURED**
(`check:i18n`,
`check:i18n-coverage`, `check:i18n-walk-parity`,
`check:dual-build-cjs-loads` — each exits 3,
PREREQUISITE NOT MET, wanting a full repo build), **2 unrun**
(`check:pm-dispatch-gates`, whose
own header forbids running it in an agent container's foreground;
`check:type-check-debt`,
repo-wide `tsc`, killed by this runner's timeout). `check:spec-changes`
and
`check:skill-examples` deserve a word each: the first is **green**,
which is the determinism
half of clause 1; the second exits 1 saying *"packages/client-react/dist
holds no .d.ts — the
package is not built"*, so it is NOT MEASURED, not a finding. None of
the six touches this
diff's subject, and CI builds fresh.
## Acceptance notes
- **A withdrawn conversion is not reported by `release.converted`.**
Between 17.3.0 and 17.4.0
`perMajor[16→17].converted` went 58 → 57:
`field-required-notnull-explicit` left the published
chain. The registry records that as a deliberate withdrawal (`⛔
WITHDRAWN — there is
deliberately NO field-required-notnull-explicit`), so this is documented
behaviour, not drift.
The section reports entries a release **added**, which is the four
arrays ADR-0087 D4 names;
an id that disappeared is visible only by comparing two published
manifests, and the code says
so rather than implying otherwise.
- **The tombstone trap the card names is not closed by this change, and
this change does not
claim to close it.** Of the 65 new `[REMOVED]` prescriptions between
17.3.0 and 17.4.0, exactly
one names 17.4.0 as the retiring release. `release.removed` answers
"which exports left in this
release" exactly; it says nothing about which *prescriptions* were
written in it, because the
prescription text carries no retirement version in data. A consumer
still cannot tell
「retired in X」 from 「prescription written in Y」 from the tombstones
alone.
- The card's *"every conversion entry carries `toMajor: 17`"* holds
inside `perMajor[16→17]`; the
aggregate carries `toMajor` values 11, 13, 14, 15 and 17. The substance
— no resolution finer
than a major — reproduced.
---
_Generated by [Claude
Code](https://claude.ai/code/session_01LvwGppdonww4zGLWZo5rho)_
---
_Generated by [Claude Code](https://claude.ai/code)_
---------
Co-authored-by: Claude <noreply@anthropic.com>
# HUMAN LANE — the ONLY job in this repository that publishes.
@@ -1222,6 +1230,35 @@ jobs:
1222
1230
1223
1231
echo "::warning::hotcrm@${HOTCRM_REF} is incompatible with the about-to-publish @objectstack/spec. ADVISORY ONLY — the publish continues. HOTCRM_REF is pre-v17; ship a migrated hotcrm release, bump it, and set BLOCKING=1 in .github/workflows/release.yml to re-arm this gate."
0 commit comments