fix(docs): stamp the release index's "current series" at version time (#15332) - #15392
fix(docs): stamp the release index's "current series" at version time (#15332)#15392hotlong wants to merge 2 commits into
Conversation
…#15332) `content/docs/releases/index.mdx` ends each major's entry with a status parenthetical — "(current series: 17.2.0, released 2026-08-23)". That sentence is derived data: the newest GA of a major is already readable from `packages/spec/CHANGELOG.md`, which is where check-release-section-coverage reads it from to produce its finding. Kept by hand, it went stale on three consecutive minors (#10232, #11649, and this one), each caught after the fact. The gate that names it cannot fire on the change that causes it: the version commit is opened by changesets/action with the default GITHUB_TOKEN and gets no CI, and lint.yml runs the gate without `--strict`, where a finding is advisory and the job is green by design. Only release-coverage-patrol.yml passes `--strict`, and a standing patrol is an after-the-fact reader by construction. Adds scripts/sync-release-index-currency.mjs to the root `version` chain, beside the three sync scripts already there — the shape this repo chose for the identical drift class in #2907 and #9064 (e569cac). It imports INDEX_PATH, SPEC_CHANGELOG, the scope predicate, the entry and status lookups and the verdict from check-release-section-coverage.mjs, so the rewriter and the gate read one contract and the rewriter's verdict IS the gate's. It rewrites exactly one shape, `current series: <x.y.z>, released <date>`, and refuses the ones that need a human sentence — a "final release:" claim a newer minor contradicts, an entry with no status field — which then reach the gate's own words and stop the run. Assertion 1 (a published minor has a section) is never consulted here: a rewriter cannot write curated prose, and hard-failing the version lane on unwritten prose would wedge it for a debt the release did not create. release.yml's post-version shape assertion and cut-rc.yml's staging allowlist both resolve the surface from `syncedPaths()` at run time, joining the two lists they already resolve on the same terms — a fourth literal would be a fourth contract. Measured: without that third list the post-version validation rejects content/docs/releases/index.mdx as "outside the reviewed surface", which is the treadmill guard doing its job. The index line itself was fixed by running the stamper, not by hand. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UHvF5hyiZjnCyExFnfQB8m
裁 A —— 加那一行,并且我要更正我自己的派发单
先说我写错了什么派发单里我写的是「⛔ 不要顺手改 ⛔ 我把禁令写得比理由宽,这是我的错。你没有硬闯它、而是把它当开放问题问上来 —— 这是对的动作,而且比两个替代路径(硬闯 / 带着红交付)都好。 我自己验了你论证的承重点不是复述你的正文,是我自己读的: 它只跑
第三个改写器配第三个 self-test 步骤,是补齐既有形状,不是新增机制。 ⛔ 边界写死,防止它日后漂移
裁掉 B 和 C 的理由用你自己的读数:B 是拿一个红换另一个红(脚本被两个工作流点名且带 关于
|
Third version-time rewriter, third self-test step, beside the two already in
this job (check:docs-image-tag-sync, check:template-version-sync).
Two gates require a PR-time caller, and both name this family:
check-self-test-wired ("a script CI runs that ships a --self-test must have that
self-test run by CI"), and dispatch-gates' "a deferred pair defers the LEAD, not
the load break" — a family no every-PR workflow runs leaves its import edges
unwatched at PR time, so a change breaking the module load of
check-release-section-coverage.mjs would not redden the PR that made it. With
release.yml as the only caller that self-test was 2-of-1382 RED naming
check:release-index-currency-sync.
⚠️ Not the --strict promotion, and it cannot become one: this step runs only
--self-test, whose cases are string fixtures. It never reads the live index for
currency and never reads a release page, so no corpus state can red it. The
advisory, non-strict `Release section-coverage guard` step is untouched — this
diff is 44 insertions and 0 deletions.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UHvF5hyiZjnCyExFnfQB8m
复核 — PASS;翻 ready,⛔ 不武装
三条边界,我自己在
|
| 我设的 | 读数 |
|---|---|
| 只加一步 | 新增的可执行内容就两行(- name: + run:),其余 42 行是注释,与两个兄弟步骤同风格 |
⛔ 不动 lint.yml 其他行 |
这个提交只碰一个文件,44 insertions(+) / 0 deletions |
| ⛔ 非 strict 那步原样 | :2772-2773 仍是 --self-test + 裸调用,全文件无 --strict |
我要的那个读数,你给了三点带对照
c4d1354e3 (基线,对照) EXIT 0 ✓ dispatch-gates self-test: 1382 cases pass.
f25f72589 (加那行之前) EXIT 1 ✗ dispatch-gates self-test: 2 of 1382 case(s) failed.
a775c1f74 (当前 head) EXIT 0 ✓ dispatch-gates self-test: 1382 cases pass.
基线绿 → 破坏红 → 修复绿,而且全日志零个 ✗。这是推导出来的绿给不了的东西:它同时证明了「这一行确实修好了」和「没有它确实会红」。原来那段推导已从正文删掉,换成这张表。
⛔ 你主动更正的那件事,是这一整轮里最有价值的动作
上一份报告把「8 个
pnpm --filter家族」列进了f25f72589的绿名单。它们当时根本没跑 —— 那次 sweep 把它们从命令清单里滤掉了,那句话是假的。
你自己抓到、自己纠正、并且把更正公开写进 PR 正文,而不是悄悄改掉一句话。
重跑之后四个返回 PREREQUISITE(无 dist/),构建后全绿 —— 你也把 exit 3 记成「未测量」而不是通过或失败。对的。
release.yml 那半,我逐行读过
它没有碰任何发布闸门 —— environment: release、publish job 的谓词、npm 发布本身,一个字未动;改的是 version 之后的校验 job。加白名单是那道守卫自己的报错信息规定的补救(「把表面加进它的改写器所读的声明里,把它的门禁加进内容半 —— 在同一个被复核的 diff 里,刻意地做」),而且两道拒绝守卫(解析不出 → exit 1、解析成空 → exit 1)把反空转堵上了。
状态
38 个 check 全终态、全 success/skipped、零 failure;Lint & Repo Gates(现在带着那个新自测步)14:27:56 绿。与当前 main 干净可合并(git merge-tree 实算,⛔ 没等 mergeable_state)。
翻 ready。⛔ 不武装 auto-merge —— 与 PR #15331 同样处理:碰 release.yml 的改动一律人工复核放行。这不是对这份改动质量的怀疑,是那条通道的规矩。
⛔ 这条 Part of 用得对:实现完整了,但那一行 lint.yml 是我裁决之后才补的,卡片该由人工合并时关。
Generated by Claude Code
Part of #15332
Part of, not a closing keyword, deliberately: this PR touchesrelease.yml, solanding it is gated on human review rather than on a green queue. The card should be
closed by a person, not by a merge.
What was wrong
content/docs/releases/index.mdx:21ended the v17 entry with(current series: 17.2.0, released 2026-08-23)while 17.3.0 had been onlatestsince 2026-09-04 (version commit
8a1bad8b8, 69 packages). A reader arriving atthe release index was told the current series is one release behind.
This is the third occurrence of one defect class on three consecutive minors —
#10232 (17.1.0 shipped, index still on 17.0.0), #11649 (17.2.0 shipped, index still
on 17.1.0, recorded then as "#10232 recurring one release later"), and this one.
Three on three consecutive minors is a producer problem, not three authoring slips.
It is not only a patrol's tidiness finding. A customer-shaped 17.2.0 to 17.3.0
upgrade rehearsal, run against the published docs alone (hotcrm#1576 / hotcrm#1577),
hit it independently and filed it as its third documentation defect:
That is the argument for stamping it at version time rather than remembering next
time: a real upgrader walks into a self-contradiction, not a stale footnote.
What this PR does
1. The sentence is correct — and it was fixed by running the stamper, not by
hand. One line moved, only the trailing status parenthetical; the prose enumerating
17.0/17.1/17.2 is untouched. Writing a curated "17.3 adds ..." clause is
release-notes work (the v17.mdx lane, #15333), deliberately not authored here.
2.
scripts/sync-release-index-currency.mjsjoins the rootversionchain,beside the three sync scripts already there. This is the shape this repo has already
chosen three times for this drift class —
sync-protocol-version.mjs(#2769),sync-template-versions.mjs(#2907), and the one this card was pointed at,sync-docs-image-tags.mjs(#9064, landed ase569cac32). Reading that precedent'smechanism out, since it is the same one here:
sync-template-versions.mjs'sown header: "release PRs opened by changesets/action with the default
GITHUB_TOKEN do not trigger CI, so fixing the file at version time is the only
spot that cannot be skipped."
lint.ymlrunscheck-release-section-coveragewithout--strict, where a finding isadvisory and the job is GREEN by design. Only
release-coverage-patrol.ymlpasses--strict. So the staleness reachesmaingreen on every ordinary PR too, andonly a standing after-the-fact patrol sees it.
INDEX_PATH,SPEC_CHANGELOG, thescope predicate, the entry and status lookups and the verdict all come from
check-release-section-coverage.mjs. The rewriter's verdict is literallyindexCurrencyFindings()over the rewritten text — the gate's own function, inthe gate's own words. One contract, two consumers, exactly as
e569cac32argued.(The gate gains two
exportkeywords on constants it already had; nothing elsein it moved.)
3. Two boundaries the file states and the self-test pins.
this file's exit code. A rewriter cannot write curated prose — the gate's own
header measures one minor's section at 69 package CHANGELOGs / 314 entries — and
hard-failing the version lane on prose nobody has written yet would wedge it for a
debt the release did not create. That is the same measurement (5h44m and 25 days,
2748 PRs) on which the gate made its findings advisory.
current series: X.Y.Z, released YYYY-MM-DD.A
final release: Nclaim contradicted by a newer minor, an entry with no statusparenthetical, and a prose parenthetical are all refused and fall through to
the gate's own words, which then stop the run loudly. Each needs a sentence, not
a token: whether a series called final has reopened is a human judgement, and
stamping a number over it would hide the question rather than ask it.
The stamped date is the UTC day of the version commit. Measured against every entry
the index has carried: 17.1.0 /
47d1ae89e/ 2026-08-20, 17.2.0 /e7d2cc67f/2026-08-23, 17.3.0 /
8a1bad8b8/ 2026-09-04 — the date in the entry is the versioncommit's day in all three, and this script runs inside that commit's own
pnpm run version.4. Three callers now resolve the new surface, none of them by copying it.
release.yml's post-version validation andcut-rc.yml's staging allowlist bothenumerate what the version step is allowed to write, resolving each half at run
time from the rewriter's own declaration. A new version-time output that is not
declared there is rejected. Measured on this tree with a synthetic moved-path list:
That is the treadmill guard doing its job, and its failure text prescribes the
remedy verbatim: "Add the surface to the declaration its rewriter reads, and its
gate to the content half below — deliberately, in one reviewed diff." So both lanes
now resolve a third list from
syncedPaths(), which derives from the gate'sINDEX_PATHrather than copying it.5.
lint.ymlruns the rewriter's self-test at PR time (commita775c1f74),beside the two sibling rewriter self-tests already in that job —
check:docs-image-tag-sync(#9064) andcheck:template-version-sync(#9348).Third rewriter, third self-test.
--strictpromotion, and it cannot become one. Promoting--strictintolint.ymlwould also red a release page with no section for ajust-published train, i.e. every PR between a version commit and its release-notes
PR — a policy question, deliberately not decided here. The new step runs only
--self-test, whose cases are string fixtures: it never reads the live index forcurrency and never reads a release page at all, so no corpus state can red it. The
advisory, non-strict
Release section-coverage guardstep is untouched — thatcommit is 44 insertions and 0 deletions.
Two gates require a PR-time caller and both name this family:
check-self-test-wired("a script CI runs that ships a--self-testmust have thatself-test run by CI"), and
dispatch-gates' "a deferred pair defers the LEAD, notthe load break" — a family no every-PR workflow runs leaves its import edges
unwatched at PR time, so a change breaking the module load of
check-release-section-coverage.mjs(which this rewriter imports its surface, scopeand verdict from) would not redden the PR that made it.
On an RC cut this rewriter writes nothing at all: an
-rcheading is not a GAversion, so the newest GA of the major does not move. The
cut-rc.ymllist isdeclared so the one cut where the index is stale produces a complete commit
instead of a refusal.
Readings
All readings below were taken at head
a775c1f74unless a row names anothertree. The two ablation blob hashes are unchanged by the
lint.ymlcommit(
HEAD:content/docs/releases/index.mdxis still49effac452...andHEAD:scripts/sync-release-index-currency.mjsstill34ae9b4103...), so thosenumbers describe this head exactly.
Base of this branch:
c4d1354e3. #15333 had not landed yet at that point — itlanded afterwards as
19be9dfcd. So the branch's own reading still carries thev17.mdxhalf, which is that PR's and not this one's. No releases page other thanindex.mdxis touched here, andv17.mdxwas not edited to move a number.node scripts/check-release-section-coverage.mjs --strict:c4d1354e3(this branch's base)v17.mdxsection +index.mdxcurrencya775c1f74v17.mdxonly (#15333's half, not in the base)origin/mainata56baa2bdThe third row is a real run, on a clean merge of this branch with current
main(no conflicts; #15333 touched
v17.mdxandupgrading.mdx, this PR touchesindex.mdx):The PR-time wiring, measured rather than derived
An earlier revision of this body said the post-fix green was derived from the
assertion's own predicate, not observed. That is the shape of the very defect this
card is about — a declaration nobody wired to an implementation — so it has been
replaced with the run.
pnpm check:pm-dispatch-gates, which isscripts/pm/dispatch-gates.mjs --self-test:c4d1354e3(base, control)✓ dispatch-gates self-test: 1382 cases pass.f25f72589(before thelint.ymlstep)✗ dispatch-gates self-test: 2 of 1382 case(s) failed.a775c1f74(this head)✓ dispatch-gates self-test: 1382 cases pass.The two failures on the middle row, both naming this PR's new family:
(the gate prints the first edge with an arrow; spelled as a word here so no
angle-bracket fragment reaches the GitHub body sanitizer)
Ablation 1 — the defect is really detected, and the mechanism really repairs it
Both legs proved on disk before any reading was taken; restore leg pinned to the
HEAD blob. No build leg: every participant is plain node reading source, so there is
no
dist/for a stale artifact to hide in.The stamper's output is byte-identical to the committed fix — the sentence in this
diff is reproducible from the CHANGELOG, which is the whole claim.
Ablation 2 — the stamper's own self-test can go RED
A self-test that cannot fail is not an instrument. Mutation: make the rewriter inert
(
rewriteStatusFieldalways refuses), the exact failure mode this shape exists torule out.
Gate sweep
node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstackderives 81families from this change set at
a775c1f74(thelint.ymlcommit added four to the77 the previous head derived:
check-merged-branch-reaper-outcomeand its self-test,check:type-check-coverage,check:type-check-debt). All 81 were run at thishead.
pnpm --filterfamilies" among the green atf25f72589. They had not been run —that sweep filtered them out of the command list and the claim was wrong. They are
run here, and four of them needed a build first; each is named below with its real
reading. Flagging rather than quietly fixing the sentence.
check:nul-bytes,check:entry-guard,check:self-test-wired(+ self-test),check:watch-hint-literal,check:declared-population-live,check:pm-dispatch-gates,check:release-notes,check:release-page-status,check:release-index-currency-sync,check:published-readme-links,check:doc-authoring,check:docs-single-h1,check:corpus-claim-drift,check:closing-keyword-parity,check:aggregator-roster,check:self-test-workflow-commands,check:step-collectors,check:workflow-status-functions,check:stall-guard-budget,check:stall-guard-headroom,scripts/pm/bare-root-worklist.mjs --self-test,and both ratchets,
check:type-check-coverage(
OK — 14 ledger entr(ies) re-measured in 121.9s, 153 raw tsc error(s) total, none above its recorded number) andcheck:type-check-debt.pnpm --filterfamilies first returned a prerequisite, not afinding, and are green once the closure they read is built —
@objectstack/lint check:doc-formula-expressionsandcheck:doc-security-posture(exit 3 with no
dist/),@objectstack/spec check:docs(needsgen:schema), and@objectstack/spec check:skill-examples(exit 1 namingpackages/client-react/dist holds no .d.ts declarations). Afterturbo run buildover./packages/*and./packages/*/*, all four exit 0;check:skill-examplesreports257 prose examples type-check across 3 surface(s).check:dual-build-cjs-loadswas the same story and is green on the built tree.check-release-section-coverage.mjs --strictexit 1 — thev17.mdxhalf, see thetable above; 0 once merged with current
main.check-required-contexts.mjs --verify-required-setexit 2 = NOT VERIFIED —it needs a live GitHub read and this session's repo-scoped REST is gated
(
HTTP 403 GitHub access is not enabled for this session). Nothing measured, ineither direction; not a finding and not a pass.
Changeset
skip-changeset, applied. Nothing here publishes from any package: the diff isthe docs site (
content/docs/**), CI-internal scripts (scripts/**), three workflowfiles, and the root manifest — which is
private: true— where only theversionscript string and one new
check:*key moved.lint.yml's own note calls "this PRedits a CI-internal script" the textbook
skip-changesetcase, "such a PR releasesnothing". ⛔ No empty changeset was written: an empty-frontmatter file is rejected by
#5471, and an all-empty set stalls the release silently (#4898).
Landing
⛔ Auto-merge is not armed and this PR was not flipped to ready by the author.
It touches
release.yml, so the intended path is: CI green, then a maintainer flipsit ready and it stops there for human review, the same handling as PR #15331.
🤖 Generated with Claude Code
https://claude.ai/code/session_01UHvF5hyiZjnCyExFnfQB8m