Skip to content

spec(changes): generate the per-major spec-changes section and the upgrade guide at publish; the pull request generates both in memory and renders the diff (#22449 B′, condition 1) - #22533

Merged
objectstack-fleet[bot] merged 9 commits into
mainfrom
claude/issue-22482-spec-changes-at-publish
Oct 9, 2026
Merged

objectstack-fleet[bot] merged 9 commits into
mainfrom
claude/issue-22482-spec-changes-at-publish

Conversation

@objectstack-fleet

Copy link
Copy Markdown
Contributor

Fixes #22482
Clause-②: no

The per-major section of spec-changes.json and the protocol upgrade guide are now generated from the ADR-0087 registries at publish, verified there, and shipped in the @objectstack/spec tarball and on the GitHub Release, by the lane the release section already uses (scripts/release-spec-changes.sh). check:spec-changes and check:upgrade-guide stop comparing a committed copy: they generate both in memory and go red when generation fails. Every pull request renders the generated diff against its base. This is ruling 6078203801 on #22449 (B′), its condition (1), and the pin in triage 6081379232.

The committed copies and their merge=os-regen routes stay; deleting them is #22485, which remains open. The guide's public address and the pointer stub are #22483. ADR-0087 D4 is #22484.

What changed

One generation entry point per projection. build-spec-changes.ts and build-upgrade-guide.ts keep their build() functions and now share one command-line contract, packages/spec/scripts/lib/projection-cli.ts:

Mode Does
no flag writes the committed copy, as before (kept until #22485)
--check generates in memory and writes nothing. ⛔ No committed copy is read. Exit 1 when generation fails, naming the projection and the cause
--out FILE writes the generated bytes to FILE. The publish lane and the diff renderer use it

The publish lane, the pull-request check and the pull-request diff all run these same two scripts, so they cannot disagree.

At publish (scripts/release-spec-changes.sh):

  • --prepare (release.yml) now also writes the guide into the package: build-upgrade-guide.ts --out packages/spec/protocol-upgrade-guide.md. It already rewrote all of spec-changes.json from the registries, per-major records included. That part is unchanged.
  • --generate (new, for cut-rc.yml) writes both registry projections and no per-release section. That is what an rc tarball has always carried. It reads nothing from npm.
  • --verify packs the artifact and runs check-release-spec-changes.mjs with a new --registry DIR: a fresh generation of both projections. The packed copies must equal it. For the manifest that covers everything except the publish-time fields: the release section and the aggregate's one-release export diff. For the guide it is byte equality. After --prepare the existing per-release checks run unchanged. After --generate the lane declares --no-release-section.
  • --attach uploads both files in one gh release upload call.
  • packages/spec/package.json files[] lists protocol-upgrade-guide.md. scripts/check-published-files.mjs registers it, as its REGISTERED invariant requires.
  • cut-rc.yml runs --generate and --verify before its publish. Before this change it packed whatever copy the tree held.

At pull request (lint.yml, Type Check · source gates, a required lane with no paths filter):

  • The two check steps now generate in memory.
  • A new step runs packages/spec/scripts/render-projection-diff.ts --base HEAD^1. On a pull request, HEAD^1 is the base tip the merge ref sits on. The step generates both sides with the same --out generators: this checkout's, and the base's own generators in a git archive of the base. It renders the diff into the job summary and a ::notice annotation. A per-record headline names the registry ids each record gained or lost. It writes spec-projections.diff, and the next step uploads that file as the spec-projections-diff artifact, only when the diff is non-empty. A side that cannot be generated is red. A diff, however large, is never red.

The PM's hypotheses, measured first

H1, the pre-pack hook: confirmed, and the guide half was missing. Dry run on 9411faa1ba (the base), RELEASE_VERSION=17.7.0, previous 17.6.0 read from npm. --prepare exit 0, --verify exit 0.

  • The packed tarball held CHANGELOG.md LICENSE README.md api-surface liveness llms.txt package.json prompts spec-changes.json src and no guide: docs/ ships in no package.
  • The packed perMajor equalled the committed one, and so did the aggregate's registry half. --prepare already regenerates the whole manifest, per-major records included, at the point the ruling asks for.
  • In release.yml both --prepare and --verify run before Publish to npm + push version tags (pnpm run release = build + build-console + changeset publish). --attach runs after Create GitHub Releases. The spec build (gen:schema, gen:openapi, tsup) writes neither projection.

Same dry run on this branch, --prepare exit 0 and then --verify exit 0:

  • The tarball now also carries protocol-upgrade-guide.md.
  • --verify prints ✓ registry projections verified against a fresh generation: spec-changes.json (2 per-major record(s), 121 converted / 406 migrated) and protocol-upgrade-guide.md (1590904 bytes) match the artifact.
  • Then ✓ release 17.6.0 → 17.7.0 verified against both tarballs: 83 added, 28 removed, 64 converted, 329 migrated. aggregate export diff 17.6.0 → 17.7.0 verified: 83 added, 28 removed. That line is byte-identical to the base run's.

H2, every reader of the committed copy, at 9411faa1ba. For each: what it reads after this PR, before #22485 deletes the copies.

Reader Reads after this PR
check:spec-changes, check:upgrade-guide nothing: generation in memory
packages/spec/scripts/check-generated.ts (GATED rows for both) runs the two checks by name. They no longer go stale-red, so --fix never regenerates the copies. The rows' artifact text still names the copies (wording for #22485)
scripts/regen-artifacts.mjs + .githooks/pre-commit (merge=os-regen) a merge deferral of either path is discharged by the two checks, with no regeneration. The copy keeps whatever the merge produced
scripts/pm/os-regen-merge.sh and the pm-dispatch hot-file row (.claude/skills/pm-dispatch/SKILL.md:296) its regeneration step (check:generated --fix) no longer regenerates the two copies
scripts/check-adr-0087-registration.mjs:2184 still reads the committed spec-changes.json at HEAD, as its parser-rot witness (projection ⊆ source). It now reads an aging copy. That copy stays a subset while registration is insertion-only, but a withdrawn id would false-red it. #22485 must give it a generated witness before deleting the copy
scripts/check-future-spec-major.mjs:351, :363 exemption rows with covers counts over both tracked files. The frozen copies keep their counts
scripts/check-doc-authoring.mjs:1597 walks the frozen docs/protocol-upgrade-guide.md (#22483 turns it into a stub)
release.yml version-pr (run_gate … check:spec-changes / check:upgrade-guide) generation only. The step comment is corrected
release.yml publish job and Releases backfill never read it: --prepare overwrites it in the tree. Now also writes the guide
cut-rc.yml packed it as-is before this PR. Now --generate plus --verify
packages/spec/package.json files[] ships whatever is at packages/spec/spec-changes.json at pack time, which is now always the lane's generation
.claude/skills/spec-property-retirement/SKILL.md:336 tells authors that a prose-only conversion edit needs gen:spec-changes + gen:upgrade-guide. That is no longer needed. Governed .claude/**, not touched here
comment-only: pr-automation.yml:905, scripts/pm/dispatch-gates.mjs:5667, and the retirement pins' EXCLUDED_PREFIXES they do not read the content

Cloud was moved by cloud#2750. It runs gen:spec-changes in its pinned checkout, and this PR leaves that script's default output path as it was.

H3, the window before #22485: the lanes that ship generate. One lane did not, and now does.

  • release.yml publish and backfill overwrite the committed bytes. Measured: a drifted tree copy, with websocket-durations-unit-in-key dropped from perMajor[17 → 18], was rewritten by --prepare, and --verify exited 0.
  • cut-rc.yml packed the committed bytes with no generation step. In the window that would publish a stale perMajor. After spec(changes): delete the committed spec-changes per-major projection and the upgrade guide copy, with their two merge=os-regen routes, once generation at publish has landed (#22449 B′) #22485 it would publish no spec-changes.json at all, because npm pack silently skips a missing files[] entry.
  • The lane is dormant today: .changeset/pre.json has tag next, and cut-rc refuses anything but rc. The minimal guard is --generate + --verify before its publish. Measured on the same drifted tree: --generate overwrote it, then --verify exit 0 with ✓ no per-release section is owed. The packed manifest keys are the same as an rc tarball's today: no release.
  • The guard's red was also measured. A stale copy packed after generation gives --verify exit 1, naming perMajor[17 → 18].migrated: 1 id(s) the registries project and the artifact OMITS: websocket-durations-unit-in-key. A tarball without the guide gives exit 1 ships no protocol-upgrade-guide.md. --verify with neither --prepare nor --generate gives exit 1 Nothing was verified.

H4, rendering on the pull request: the job summary, a notice annotation and an artifact, with no write path. Measured in .github/workflows:

  • $GITHUB_STEP_SUMMARY is used in 13 workflows.
  • actions/upload-artifact@v7 is used in ci.yml (9 steps), cut-rc, merged-branch-reaper, showcase-smoke, test-nightly-tiers and coverage-nightly.
  • PR comments go through issues.createComment under pull-requests: write in docs-drift-check.yml (advisory), merge-queue-triage.yml and cross-repo-issue-closer.yml.
  • The required typecheck-source-gates lane holds contents: read only.

So the diff renders where that lane can write without a token: its summary, an annotation, and an artifact. No new write path is added.

Pins

  • packages/spec/scripts/projection-cli.test.ts (11 tests):
    • --check neither reads nor writes a stale committed copy, and is green with none;
    • a generation failure is red in --check and in both write modes, and writes nothing;
    • --out writes only there;
    • usage errors build nothing;
    • the two real generators route through the runner and are byte-deterministic across runs, which --verify depends on.
  • packages/spec/scripts/render-projection-diff.test.ts (9 tests):
    • an entry-only change renders its id in the headline, the diff and the artifact;
    • unchanged renders "no change" with no artifact;
    • a head or base side that fails to generate is red;
    • a base that predates --out is read from the path it writes, and the head never falls back;
    • plus the id delta, the fence and truncation.
  • scripts/check-release-spec-changes.mjs --self-test: batteries 23 → 31 (P1–P8, the registry half). R1–R17 are untouched and green.
  • scripts/release-verify-npm.mjs --self-test battery 13 floor 11 → 13 (113 → 115 cases). The backfill generates the guide in the version commit's tree and attaches the publish job's bytes.
  • release section unchanged: R1–R17 are unchanged, and the H1 verdict line for the section is byte-identical before and after.

One-shot proofs, not kept as tests:

  • An entry-only change needs no regeneration commit. I added a step-18 semantic entry file plus gen:migration-registry (the registry's generated regions) and regenerated no projection.
    • check:spec-changes exit 0 (407 migrated) and check:upgrade-guide exit 0.
    • Control: the base's build-spec-changes.ts --check on the same tree exits 1, spec-changes.json is stale.
    • render-projection-diff.ts --base HEAD exits 0: spec-changes.json +14 −0 (perMajor 17 → 18: +1 migrated (zz-demo-entry-only); aggregate 16 → 18: +1 migrated (zz-demo-entry-only)) · protocol-upgrade-guide.md +3 −0.
    • Restored, git diff HEAD empty.
  • A registry that fails to generate turns the check red. The same entry with a non-string replacement:
    • check:spec-changes exits 1: ✗ spec-changes.json could not be generated from the ADR-0087 registries. Nothing was written., then the ZodError.
    • The renderer exits 1: NOT rendered — head spec-changes.json: build-spec-changes.ts exited 1.
    • Restored.
  • Ablation of battery 13's new case. I dropped the guide from the gh release upload line through scripts/ablation-replace.mjs: anchor 1 → 0, blob d9cde2bfbe94 → 516d5f9dc619. The self-test exited 1 on exactly that case. Restored, blob equals HEAD.

Verification

All readings at 1245a3058c (this branch's head) unless a line says otherwise.

  • Derived gate union. node scripts/pm/dispatch-gates.mjs --commands printed 128 commands. All 128 ran here, each with its exit code recorded, and all exited 0. --ran reports: 128 derived, 128 run, 0 NOT-MEASURED, 0 UNRUN.
    • An earlier pass at 58d075abdc recorded exit 3 (PREREQUISITE NOT MET, no build) on the four gates that read built dist/: dts-closure, dual-build-cjs-loads, lean-entry-closure and sourcemap-no-sources-content.
    • The spec repo-project tests then built the workspace's dist/ in this worktree. The final pass measured all four green.
    • This covers every command the dispatch named, plus what the re-derivation added: check-declaration-mirrors (both), release-verify-npm --self-test, check:engine-double-contract, check:objectql-double-limit, check:query-options-erasure, check:ratchet-remedy-authority, check:release-spec-changes and check:where-matcher.
  • Spec suites, under scripts/pm/os-verify-lock.sh.
    • pnpm --filter @objectstack/spec exec vitest run --project local --maxWorkers=2: VERDICT command-exit 0, 632 files passed, 1 skipped, 18847 tests passed. Taken at 58d075abdc; 1245a3058c adds only a three-line comment.
    • pnpm --filter @objectstack/spec exec vitest run --project repo --maxWorkers=2: VERDICT command-exit 0, 54 files, 915 tests passed.
    • pnpm --filter @objectstack/spec typecheck: VERDICT command-exit 0. The scripts program lists all four new script files.
  • The two new test files: 20 of 20 tests pass.
  • ESLint, narrowed and measured:
    • Population: the 9 changed JS/TS files, all under eslint.config.mjs's **/*.{ts,…} object.
    • --no-inline-config --format json reports 9 files: 0 errors, 0 warnings, 0 ignored notices.
    • The config enables no type-aware linting (no parserOptions.project), so this diff cannot move a verdict on a file it does not touch.
    • The repo-wide pnpm lint is CI's.
  • Main. The branch is 6 commits behind origin/main d303b3e7af. One of those commits (ee8751d41e) regenerated the two committed copies, which this diff does not touch. No commit touches a file in this diff, and git merge-tree is clean. CI runs on the merge ref.

File surface

Two extensions beyond the claim's list, both forced:

  • packages/spec/package.json files[]: the same file as the in-surface check scripts. The ruling ships the guide "in the package", and H1 asks the tarball to carry it.
  • scripts/check-published-files.mjs EXTRA_ENTRIES: one entry, which its REGISTERED invariant requires for any new files[] entry.

Also touched, reading them as the lane's own parts:

  • scripts/check-release-spec-changes.mjs: the gate --verify runs.
  • scripts/release-verify-npm.mjs: battery 13 executes the lane's release.yml steps.
  • cut-rc.yml: a release workflow step that calls the script.

Acceptance notes


Generated by Claude Code

claude added 9 commits October 9, 2026 18:48
…heck and write anywhere with --out

check:spec-changes and check:upgrade-guide stop comparing a committed copy:
they generate both projections from the registries, write nothing, and exit 1
naming the projection when generation fails. --out <file> writes the generated
bytes elsewhere, for the publish lane and the pull-request diff renderer.

Claude-Session: https://claude.ai/code/session_01VZqqwTj2wsihZEbfT6yyYN
Co-authored-by: Claude <noreply@anthropic.com>
--check reads no committed copy, a generation failure is red in every mode and
writes nothing, --out writes only where it is told, and the two real
generators are deterministic across runs.

Claude-Session: https://claude.ai/code/session_01VZqqwTj2wsihZEbfT6yyYN
Co-authored-by: Claude <noreply@anthropic.com>
…for the pull request

render-projection-diff.ts generates spec-changes.json and the upgrade guide on
both sides with the same --out generators the publish lane runs (the base in a
git archive of the base commit, borrowing this checkout's node_modules), and
renders the diff into a job summary, a notice annotation and a diff file for
the workflow to upload. A side that cannot be generated is red.

Claude-Session: https://claude.ai/code/session_01VZqqwTj2wsihZEbfT6yyYN
Co-authored-by: Claude <noreply@anthropic.com>
…at publish

--prepare and the new rc-lane --generate write spec-changes.json and the
upgrade guide into packages/spec from the registries; files[] ships the guide.
--verify packs the artifact and refuses it when either packed projection
differs from a fresh generation (check-release-spec-changes --registry), on
top of the unchanged per-release section check. --attach uploads both files.

Claude-Session: https://claude.ai/code/session_01VZqqwTj2wsihZEbfT6yyYN
Co-authored-by: Claude <noreply@anthropic.com>
…ackfill D4 steps

The stub pnpm answers the guide generator, the stub gh copies every uploaded
file by tag and basename, and two new cases pin that the backfill generates
the guide in the version commit's tree and attaches the publish job's bytes.

Claude-Session: https://claude.ai/code/session_01VZqqwTj2wsihZEbfT6yyYN
Co-authored-by: Claude <noreply@anthropic.com>
… the rc lane generates and verifies both projections

lint.yml's source-gates lane keeps the two in-memory checks, then renders the
generated diff against HEAD^1 into the job summary and a notice annotation and
uploads it as an artifact. cut-rc.yml runs release-spec-changes.sh --generate
and --verify before its publish, so it no longer packs the tree's copy.

Claude-Session: https://claude.ai/code/session_01VZqqwTj2wsihZEbfT6yyYN
Co-authored-by: Claude <noreply@anthropic.com>
…jections are generated at publish

Claude-Session: https://claude.ai/code/session_01VZqqwTj2wsihZEbfT6yyYN
Co-authored-by: Claude <noreply@anthropic.com>
@github-actions github-actions Bot added ci/cd dependencies Pull requests that update a dependency file documentation Improvements or additions to documentation tests tooling labels Oct 9, 2026
@github-actions

github-actions Bot commented Oct 9, 2026

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

⚠️ 1 changed file(s) yielded no anchor (packages/spec/package.json), so the pages documenting them are NOT COVERED by this run — this is not a clean bill of health for those files. Nothing else in this diff resolved to a documentable surface (no symbol, route or SDK anchor derived from 1 changed package(s)).

What this run could not see
  • 1 changed file(s) yielded no anchor (packages/spec/package.json) — pages documenting those are invisible to this run
  • a page that states a rule by its inputs shares no identifier with the emitter that implements the rule, so an emitter-only diff cannot list it — not on this run and not on any run. Measured on fix(driver-sql): emit varchar(maxLength) for a text field a declared index keys on #11430: content/docs/protocol/objectql/types.mdx documents the text-family column mapping by the ObjectQL type names it maps FROM (text / textarea / html) while the diff changed createColumn; it went unlisted, and it was the page that diff falsified, in four places. No shared token exists to detect this on, so a rule your change carries has to be re-read by hand in the pages that restate it.
  • a key NAME is not a key, so the hand re-read the line above prescribes can land on the wrong schema. The same spelling is authorable on one governed type and a [REMOVED] tombstone on another for each of active, aria, joins, objects, template, tools and version (censused on [finding] tools is a key on BOTH AgentSchema (tombstoned, dead) and SkillSchema (live, cloud-attested), so a name-based search attributes skill examples to the agent key — it produced a false stop-the-line alarm on PR #19059 #19093 over the liveness ledger's governed types, top-level keys); nothing in a search result distinguishes the two, so a grep hit on a LIVE example reads as evidence about the DEAD key. Measured on fix(spec): the agent.tools liveness row says dead — it claimed live on a key the schema tombstoned #19059: content/docs/ai/agents.mdx was reported as contradicting the agent.tools tombstone over its tools: example at :161, which is inside the defineSkill({ block opened at :155 — the page was already correct. Settle ownership by PARSING the value against both schemas, never by the name: that literal PASSES SkillSchema, and as an AgentSchema it FAILS at tools with the tombstone prescription. ⛔ These names are not the whole class — a key retired through a .strict() guidance map leaves no tombstone in the walked shape and none of them here (tool.category, live as AIToolDefinition.category).

Coarse fallback — 139 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): node scripts/docs-audit/affected-docs.mjs --json d303b3e7af64ba39a13e060da277485ace7ecaf4 → packageMentionDocs.

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

Standing down on a failure that is not this PR's. domain:spec seat 1 (#6017) · session session_01VZqqwTj2wsihZEbfT6yyYN · 2026-10-09T21:31Z.

@github-actions

github-actions Bot commented Oct 9, 2026

Copy link
Copy Markdown
Contributor

⛔ merge queue 构建失败 — 先分诊,再决定要不要重排

队列构建 37992689182 红了。队列跑的是全量套件(PR 侧 CI 只跑 affected 子集),
所以失败的测试可能在本 PR 没碰过的包里 —— 那不是重排能修的。每次盲目重排都会让排在后面的所有 PR 重建一轮。

分类:failure —— 按下面的日志分诊。

失败的 job(日志抽取,best effort):

↳ 失败原因 是判读的关键:超时(Test timed out in … / Hook timed out in …)多半是负载/时序,不是本 PR 的回归;
断言(AssertionError: …)才指向真实的行为改变。两者的 FAIL 行长得一模一样,只有这一行能区分。

⚠️ 断言这一侧有一类例外,判据是断言在测什么,不是它是不是 AssertionError。 断言的对象是产品行为(一个值、一个形状、一次拒收)⇒ 照上面读:真实的行为改变,去查,⛔ 不要重排掉;
断言的对象是这次实验自身的有效性前提(跑完的耗时、负载下的先后、任何只在时间预算内才成立的条件)⇒ 它跟超时是同一类,同样对负载敏感,重排一次是合法的判别手段。
识别是机械的:断言的消息或它比较的值本身点名了一段时长、一个时间戳、一个耗时计数。实测过的一对 —— AssertionError: SecurityPlugin.init() ran: expected false to be true 测的是产品行为(真回归);
AssertionError: this run took over a second, so second-precision stamps could have differed too: expected 1006 to be less than 1000 测的是实验前提:它守护的那条不变式当时是绿的,同一个 head 原样重排一次即成功。
穿着 AssertionError 外衣的时间测量,仍然是时间测量。(⛔ 这只改「怎么读一次红」,不改「哪些测试可以重排」——后者由别处管。)

跨 PR 相同签名(24h,按失败测试文件聚合):

  • ⚠️ 本次没有可用的聚合签名(日志里没有能解析出测试文件名的 FAIL 行)—— 这不是「没有同签名的其他 PR」,是这一轮没测到。跨 PR 聚合本次不可用,请手工比对其他 PR 的同类评论。
  • ⚠️ 24h 评论账本没读完(超过 5 页仍未读到窗口尽头),所以上面的「不同 PR 数」是下界,不是全量。

历史信号:

  • 本 PR 过去 24h 无队列失败记录(首次)。
  • 过去 24h 队列共有 13 个失败构建(不含本次)。

分诊清单:

  1. 失败测试在本 PR 改动的包里 → 真回归,修 PR。
  2. 失败测试与本 PR 无关 → 看上面的「跨 PR 相同签名」;已有汇总 issue ⇒ flaky/环境问题实锤,去那张 issue 上谈,修好前重排只会再烧一轮全队列。
  3. 两者都不是 → 可能与同组 PR 语义冲突;等前面的 PR 落地或失败出队后再重排一次即可,不要连续重排。

Generated by Claude Code · merge-queue-triage workflow (#4859)

@objectstack-fleet
objectstack-fleet Bot added this pull request to the merge queue Oct 9, 2026
Merged via the queue into main with commit 5b12503 Oct 9, 2026
38 checks passed
@objectstack-fleet
objectstack-fleet Bot deleted the claude/issue-22482-spec-changes-at-publish branch October 9, 2026 23:09
This was referenced Oct 9, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

ci/cd dependencies Pull requests that update a dependency file documentation Improvements or additions to documentation size/xl tests tooling

Projects

None yet

2 participants