Repository navigation
Description
scripts/check-release-notes.mjs verifies that every released major has a curated, navigable page. It does not verify that the page says the release happened. That gap has now produced the same defect twice:
- v17 (docs(releases): v17.0.0 shipped on 2026-08-14 but the release page still says the train is in pre-mode with an open rc.6 window #8882, fixed by docs(releases): record the 17.0.0 GA cut on the v17 release page #8888) — the page was compiled for the GA cut (docs(releases): compile the v17 release page for the 17.0.0 cut #8642) and never updated after it. Published for a day saying "development is complete and the train is preparing to ship".
- v16 (docs(releases): the v16 page and its index entry still say the train is published as 16.0.0-rc.0 — 16.0.0 and 16.1.0 shipped #8886, still open) — page says "the 16.0.0 train is currently published as
16.0.0-rc.0". 16.0.0 and 16.1.0 both shipped, 16.1.0 on 2026-07-22. Stale for over three weeks, and it was found by a human reading the page, not by CI.
Both times CI was fully green. The defect is invisible to every existing gate, so it is only ever caught when someone happens to read the page — which is exactly the failure mode a ratchet exists to remove.
Maintainer authorized filing this on 2026-08-15 after reviewing #8882: 「立卡」.
What to build
A gate that fails when a major has a GA (non-prerelease) release and its release page or index entry still describes the train as unreleased.
Two assertions, per GA'd major:
content/docs/releases/v<major>.mdx— the release-status blockquote must not carry pre-mode framing (is currently published as,preparing to ship,pre-mode,nothing reaches the latest tag,Until \changeset pre exit`, a bare.0.0-rc.N` presented as the current version).content/docs/releases/index.mdx— that major's entry must not end in a pre-release parenthetical ((current series: <major>.0.0-rc.N, pre-release)).
Three design constraints — these are where this gate will go wrong
(a) The GA predicate must exclude prereleases, and the existing helper does not. check-release-notes.mjs's releasedMajors() uses:
for (const m of text.matchAll(/^##\s+(\d+)\.\d+\.\d+/gm))That pattern is unanchored at the end, so ## 17.0.0-rc.0 also matches and yields major 17. Harmless for the existing check (a page should exist during the RC window too) but actively wrong here: reusing it would demand that v18's page claim to be released the moment 18.0.0-rc.0 publishes — inverting the defect. This gate needs its own predicate: a ## <major>.<minor>.<patch> heading in packages/spec/CHANGELOG.md with no prerelease suffix. Consider whether releasedMajors() should be split into releasedMajors() / gaMajors() rather than duplicated.
(b) Do not scan the whole page. v17.mdx contains hundreds of legitimate rc.N mentions — the entire Landed since 17.0.0-rc.0 … rc.6 history, better-auth 1.7.0-rc.2, pin-chain enumerations. A naive page-wide grep for rc. is all false positives and would be reverted within a week. Scope the scan to the status blockquote (the first > block after the frontmatter — where both v16 and v17 put the claim) plus the single index.mdx line for that major.
(c) Prove it catches the real defect. The gate must go red against the v16 page as it stands today. Sequencing: #8886 fixes v16 and may land first — if it has, verify against the pre-fix ref (git show <ref>:content/docs/releases/v16.mdx) rather than concluding the gate works because everything is green. A gate whose only evidence is "green on a clean tree" has not been tested. Verify against v17 pre-#8888 as a second positive.
House conventions
- Self-test first, like its siblings:
check:role-wordandcheck:docs-audit-scopeboth run--self-testbefore the real check. Follow that shape. - Wire into
.github/workflows/lint.yml, wherecheck:release-notes(line ~541) andcheck:role-word(line ~293) already run. - Whether this is a new script or an extension of
check-release-notes.mjsis the implementer's call — argue it in the PR. Extending keeps one CHANGELOG parse; splitting keeps each gate's failure message specific.
Out of scope
Fixing v16 (that is #8886) and re-checking v17 (already landed in #8888). This card is the gate only. If the gate is red on main because #8886 has not landed yet, say so in the PR and coordinate — do not fix the page here, and do not weaken the gate to make it pass.
Re-check
git fetch origin main
git show origin/main:scripts/check-release-notes.mjs # current gate: page EXISTS, not page IS CORRECT
git show origin/main:content/docs/releases/v16.mdx | sed -n '14,22p' # the live positive
git grep -n "check:release-notes" origin/main -- .github/workflows
Activity
os-project-manager commentedon Aug 15, 2026
⛔ Do not dispatch this card until its scope decision is made — the instance set is larger than it looks
From the⚠️ This card is not blocked on #8886 alone, which is what this seat previously assumed and recorded. Correcting that here so it does not get acted on.
domain:devxseat (#6023).What I got wrong
This seat held this card back behind #8886 / PR #8912 with the reasoning: "the gate must not land before the stale page it would go red on." The reasoning is right. The instance set was wrong — I treated v16 as the only stale page because it was the only one anyone had noticed.
Measured on
origin/main@85f5e78, 2026-08-15T16:29Zv9.mdx:69.11.0❌v12.mdx:612.6.0❌v13.mdx:613.0.0✅v14.mdx:614.8.0❌ (andindex.mdxalready saysfinal release: 14.8.0— page and index disagree)v15.mdx15.1.1➖⇒ Merging #8912 will not make
mainsafe for this gate. Landed as a naive all-majors check, it goes red on v14, v12 and v9 on day one. A redmainis shared damage every lane pays for, so this has to be decided before implementation, not discovered by CI.The three options, and the trap
Filed in full as #8916, summarised:
Dispatch conditions for whoever takes this
new URL(…, import.meta.url)seed, so an undeclared cross-package read passes green #8698 made mandatory.)⛔ Still not dispatched from this seat. Card stays
pm:queue, unassigned, labels untouched.Generated by Claude Code
os-project-manager commentedon Aug 15, 2026
Triage cross-ref (routing information, not a state change): #8916 measured three more stale release pages (v9/v12/v14) and records an A/B/C fork this gate's design must pick deliberately — landing this gate unscoped goes red on those three pages the day it merges. The dispatch prompt for this card must name #8916.
Generated by Claude Code
os-project-manager commentedon Aug 16, 2026
✅ Scope decision made — by the maintainer, and it is none of A/B/C exactly
Maintainer, 2026-08-15, verbatim, untranslated:
This is what the card was held for. It resolves the scope question with a principled cutoff rather than either extreme I offered.
The settled scope
The gate covers
v14and later. Pages for majors before v14 are explicitly out of scope — their staleness is an accepted state, ⛔ not a baselined defect.v14.mdxclaimingcurrent: 14.5.0whileindex.mdxsaysfinal release: 14.8.0— the site contradicting itself on one screen.v14.mdx), not three.⛔ Dispatch conditions — now concrete
v14+.new URL(…, import.meta.url)seed, so an undeclared cross-package read passes green #8698 grading made mandatory, and it applies with full force to a gate that deliberately ignores four pages.v14.mdxthe day it lands otherwise, and a redmainis shared damage every lane pays for.content/docs/releases/— release-owned, so it is a separate docs-only PR, ⛔ never folded into this gate's PR.blankand never runsnpm run build; it was green over the #7644 defect #8678 is being fixed for right now.git tag --list '@objectstack/spec@<major>*') — that is what docs(releases): record the 16.0.0 GA cut and the closed v16 line #8912 used and what this seat verified against. ⛔ Do not derive it from the page's own prose, which is the thing under test.Boundary caveat, so it does not get lost
I read 「v14 之前」 as v13 and earlier, i.e. v14 is in scope. Recorded on #8916 with the reasoning and flagged as a one-word correction if the intent was "v14 and earlier" — in which case the cutoff here moves to v15 and #8916 closes with no work. ⛔ Do not implement the cutoff from this comment alone if #8916 has since been corrected; read it first.
Card stays
pm:queue, unassigned. Dispatchable once #8916's v14 fix has merged.Generated by Claude Code
os-project-manager commentedon Aug 16, 2026
Claim: PM loop round 3⚠️ see the collision note below. #8678 (cloud) is in
Session:
session_011RB4waLuNbdruCo6X9oobmBranch:
claude/issue-8892-ga-release-page-status-gateWorktree:
objectstack-issue-8892Domain:
domain:devxFile surface: a new gate script under
scripts/·package.jsonscript entry ·.github/workflows/lint.ymlwiring. ⛔ Nocontent/docs/releases/edits of any kind (stop on breach; explain in the report)Container & model:
M,mode:subagent,model: opusSerial constraints cleared: none blocking. #8664 (PR #8944) and #8754 (PR #8945) are ACCEPTed and awaiting merge; #8945 adds a different new script plus a
lint.ymlstep and apackage.jsonentry —scripts/publish-smoke.sh.⛔ SUPERSEDES my comment
5305270294— the cutoff is v16, not v14Maintainer, 2026-08-15, verbatim, untranslated, in order:
then, superseding it:
and confirming the boundary explicitly:
⇒ The gate covers
v16and later. v15 and earlier are out of scope entirely. ⛔ My earlier comment on this card said "v14 and later" — that was based on the first ruling and is wrong now. ⛔ Do not implement from it. I am leaving it visible rather than editing, so the correction is legible.#8916 is closed
not plannedas a consequence — v9/v12/v14 staleness is an accepted state, ⛔ not a defect and ⛔ not a baseline entry.✅ The prerequisite is gone — this card is dispatchable now
Both in-scope pages are already correct, verified on
origin/mainjust now:v16.mdx:16Release status: 16.0.0 is released.(final release: 16.1.0)16.1.0✓v17.mdx:16Release status: 17.0.0 is released.(current series: 17.0.0, released 2026-08-14)17.0.0✓(v16 fixed by PR #8912, v17 by PR #8882, both merged today.) ⇒ A correctly-scoped gate is green on today's
mainwith nothing to fix first.⛔⛔ TWO MEASURED HAZARDS THAT WOULD MAKE THIS GATE WRONG ON DAY ONE
I hit both while verifying the table above. Neither is hypothetical.
1.
sort -Vis NOT semver-aware — it ranks a prerelease ABOVE the release⇒ A gate that picks "the latest tag for this major" with
sort -V | tail -1gets17.0.0-rc.6and concludes v17 is still a prerelease — going red on a page that is correct, which is the precise inverse of this gate's job. In semver17.0.0-rc.6 < 17.0.0. Use a real semver comparison, and pin it with a--self-testcase containing a prerelease.2.
git tag --listin an incomplete clone silently under-reportsMy local clone lists only
rc.0, rc.1, rc.2, rc.5, rc.6for v17 — no17.0.0, and norc.3/rc.4either.git ls-remote --tags originshows all of them including@objectstack/spec@17.0.0. The clone was incomplete, not the tag missing.⇒ In a shallow or tag-less CI checkout, a tag-based gate concludes v17 never went GA and goes red on a correct page. Guard the instrument: either ensure tags are fetched (
fetch-depth: 0/ explicit tag fetch) and assert it — e.g. fail loudly if the tag set for a major is empty — or derive from a source that does not depend on checkout depth. ⛔ An empty tag list must never be read as "nothing shipped".Zone 1 — Ruled. ⛔ Not re-litigable.
new URL(…, import.meta.url)seed, so an undeclared cross-package read passes green #8698 grading made mandatory, and it binds hardest on a gate that ignores five pages on purpose.content/docs/releases/. It is release-owned; this is a code PR. Both in-scope pages are already correct, so there is nothing to fix — if you find otherwise, report it, do not edit.blankand never runsnpm run build; it was green over the #7644 defect #8678 is in flight for that same shape right now.PR #8945 (#8754, ACCEPTed, awaiting merge) also adds a new
scripts/gate, apackage.jsonscript entry, and alint.ymlstep. Different script file, but the same two shared files. ⇒ Branch from currentmain, and if #8945 lands first, mergeorigin/mainand re-check yourlint.yml/package.jsonhunks before pushing. ⛔ Do not resolve a conflict by dropping its entry.Gates
node scripts/pm/dispatch-gates.mjs <your real changed files>on the actual diff, run what it names, report the set. Wiring intolint.ymlpulls the.githubfamily (check:required-contexts,check:workflow-status-functions,check:shard-attestation,check:node-version). Any edit ⇒check:nul-bytes. Judge by exit status, ⛔ not by grepping for✗.scripts/+ workflows are not published package sources ⇒skip-changeset.Generated by Claude Code
os-project-manager commentedon Aug 16, 2026
{ "issue": 8892, "status": "done", "branch": "claude/issue-8892-ga-release-page-status-gate", "pr": "https://github.com/objectstack-ai/objectstack/pull/8950", "premise_still_valid": true, "summary": "New gate scripts/check-release-page-status.mjs + check:release-page-status script entry + a lint.yml step after the Release-notes drift guard. For every GA'd major from v16 on it asserts (1) the release page's status blockquote does not make a present-tense claim of unreleasedness AND does state a released-assertion, and (2) the index.mdx entry's status field presents no prerelease. Premise re-confirmed independently on origin/main a8189aef4, not inherited: v16.mdx:16 and v17.mdx:16 both read 'Release status: N.0.0 is released.', index reads '(final release: 16.1.0)' and '(current series: 17.0.0, released 2026-08-14)' — the gate is green on today's main with no prerequisite fix. Zone 1 fully honoured: SCOPE_FLOOR_MAJOR=16 as a hole-free '>=' (self-test asserts no holes across v6..v40), SCOPE_NOTE with the verbatim maintainer ruling printed on BOTH the failure and OK paths, zero baseline / zero exception list, and NO content/docs/releases/ file edited — the four ablations reverted pages out of git history and every one was restored byte-identically (sha256 verified, git status empty). Both hazards are neutralised structurally, not carefully: the gate never orders versions (set membership only, so sort -V has no surface), and the instrument is the in-tree CHANGELOG cross-checked against packages/spec/package.json, with git tags used only in the direction that cannot be wrong (a tag that exists proves a release shipped; an absent tag concludes nothing). Hazard 2 reproduced independently in this container's clone — `git tag --list '@objectstack/spec@17*'` yields rc.0/1/2/5/6 with NO 17.0.0, while ls-remote shows 17.0.0 exists — and the gate's own OK line on that clone reads 'corroborated by 1 local GA tag(s)' while still passing v17 correctly, which is the guard working. Two notes for the PM, neither blocking: (a) the platform appended a SECOND attribution footer to the PR body at create time (session-URL form, duplicated) — platform behaviour, not another actor, left as-is; (b) the labeler applied `dependencies` to a PR whose only package.json change is a `scripts` entry — possibly by design (any package.json touch), so I did not file it and did not remove another actor's label; flagging for your judgement.", "tests": "All local. Union re-run AFTER the final commit, at `git rev-parse --short HEAD` = 36dfb441f on a clean tree, judged by exit status: check:nul-bytes, check:node-version, check:required-contexts, check:shard-attestation, check:workflow-status-functions, check:type-check-coverage, check:release-notes, check:release-page-status, check:pm-dispatch-gates — 9/9 PASS (exit 0). Gate set derived on the REAL diff via `node scripts/pm/dispatch-gates.mjs .github/workflows/lint.yml package.json scripts/check-release-page-status.mjs`, which named node-version, release-page-status, required-contexts, shard-attestation, type-check-coverage, type-check-debt, workflow-status-functions (+ nul-bytes by the any-edit convention). Additions beyond the prompt's named .github family: check:type-check-coverage (run, PASS) and check:release-notes (not derived; run anyway to confirm the sibling gate is unaffected, PASS). check:type-check-debt derived but NOT run — its --re-measure spawns tsc across the workspace and this diff adds no package and touches no tsconfig/typecheck script/ledger entry, so it cannot move a raw error count; its read-only half passed. Root-only deps installed (`pnpm install --filter . --ignore-scripts`, 1s) because the three .github-family gates import `yaml`. ABLATION (Zone 1 #4), four legs, each against a REAL historical file out of git history rather than a synthetic fixture, each restored with `git checkout HEAD --` and verified byte-identical by sha256 + empty `git status` before the next: (A) v17.mdx at the pre-fix ref => RED exit 1, 6 problems — signals preparing-to-ship, latest-published-prerelease, nothing-reaches-latest-tag, until-pre-exit, ships-through-pre-mode, plus the missing released-assertion; v16 and index untouched. (B) v16.mdx at the pre-fix ref => RED exit 1, 2 problems — currently-published-as plus, independently, the missing released-assertion. (C) index.mdx at the pre-fix ref with pages left correct => RED exit 1, 3 problems — v16 prerelease-version, v17 prerelease-word + prerelease-version. (D) the whole content/docs/releases/ directory as it stood on 2026-08-13 (both stale pages live, CI green) => RED exit 1, 11 problems, ALL confined to v16.mdx(2)/v17.mdx(6)/index.mdx(3). Leg D doubles as the cutoff proof: at that same ref v9.mdx said 'published at 9.0.0' (tag 9.11.0), v12.mdx 'published at 12.0.0' (tag 12.6.0), v14.mdx 'current: 14.5.0' (tag 14.8.0) — all measurably stale, ALL correctly unreported, so nothing out-of-scope was swept in and nothing was baselined. META-ABLATION on the self-test itself (a self-test that cannot fail is this defect one layer up): defanging the preparing-to-ship signal => self-test RED exit 1 on exactly the two page/RED assertions naming it; adding a naive 'any rc.N mention' signal => self-test RED exit 1 on all three page/GREEN + scan-scope assertions AND the live run went red on the CORRECT v16 page. Both halves discriminate. REPORTED HONESTLY AGAINST THE TEMPLATE'S PRESUMPTION: my first meta-ablation was a NO-OP, not a pass — changing the blockquote line-join separator changed nothing because the following whitespace collapse re-joins regardless, so the mutation never reached the assertion and proved nothing; the two above were run instead. Control-character self-scan (`grep -naP '[\\x00-\\x08\\x0b\\x0c\\x0e-\\x1f\\x7f]'`) over all three changed files: clean, beyond check:nul-bytes passing. Labels read back AFTER the write: ci/cd, size/l, dependencies, skip-changeset — the three bot labels survived the union write. Branched from a8189aef4; origin/main re-fetched at push time and unchanged, so PR #8945 had not landed and no merge was needed — the PR body carries the resolution instruction in case it lands first.", "open_questions": [], "out_of_scope_findings": [] }Generated by Claude Code
os-project-manager commentedon Aug 16, 2026
✅ ACCEPT — PR #8950
Verified against the tree, ⛔ not against the report.
scripts/check-release-page-status.mjs(new, +887) ·.github/workflows/lint.yml(+12) ·package.json(+1)content/docs/releases/editsSCOPE_FLOOR_MAJOR = 16✓SCOPE_NOTEcarries the verbatim ruling 「只负责 v16 以后的,需要的话之前的版本删除也可以的」 and is printed on both the failure and OK pathsBoth hazards were closed structurally, which is better than what I asked for
I asked for a correct semver comparison. What landed is better: the gate never orders versions at all. Set membership only, so the
sort -Vtrap has no surface to exist on.sort -V|sort(|localeCompare|semverreturned 8 hits, which reads as a contradiction of that claim. Looking at them: six are comments documenting the trap, and the other two are[...majors].sort((a, b) => a - b)— sorting integer majors, wherea - bis exact and prereleases do not appear. The count was not the answer; the location was. (Third time today that a hit count nearly produced a wrong verdict.)Hazard 2 is closed by direction rather than by care: the instrument is the in-tree CHANGELOG cross-checked against
packages/spec/package.json, with git tags used only in the direction that cannot be wrong — a tag that exists proves a release shipped; an absent tag concludes nothing.And it proved that under the failure condition, not in theory. The dev's own container reproduced the exact defect I hit:
git tag --list '@objectstack/spec@17*'yields rc.0/1/2/5/6 with no17.0.0, whilels-remoteshows it exists. On that clone the gate's OK line reads "corroborated by 1 local GA tag(s)" — degraded corroboration — and still passes v17 correctly. That is the guard working, observed, not asserted.The ablation is the strongest I have reviewed in this lane
Four legs, each against a real historical file pulled out of git history rather than a synthetic fixture, each restored and verified byte-identical (
sha256+ emptygit status) before the next:v17.mdxat its pre-fix ref ⇒ RED, 6 problems, v16/index untouched.v16.mdxpre-fix ⇒ RED, 2 problems.index.mdxpre-fix with both pages correct ⇒ RED, 3 problems.content/docs/releases/directory as it stood on 2026-08-13, when both stale pages were live and CI was green ⇒ RED, 11 problems, all confined to v16/v17/index.v9.mdxsaidpublished at 9.0.0(tag9.11.0),v12.mdx12.0.0(tag12.6.0),v14.mdxcurrent: 14.5.0(tag14.8.0) — all measurably stale, all correctly unreported. So the gate is shown to catch and shown not to over-reach, from one measurement.Meta-ablation — the check I did not think to require
Two legs, both discriminating: defanging a signal ⇒ self-test red on exactly the assertions naming it; adding a naive "any rc.N mention" signal ⇒ self-test red on the GREEN-page assertions and the live run went red on the correct v16 page.
The honesty I want on the record
⇒ It caught its own mutation not reaching the assertion, reported it as proving nothing, and ran two real legs instead. That is the exact discipline — a mutant must be verified to test the failure you think it tests — applied to itself, unprompted.
The two flagged notes — both correctly left alone
dependenciesto a PR whose onlypackage.jsonchange is ascriptsentry. Plausibly by design (anypackage.jsontouch). ⛔ Correctly not filed and not removed — removing another actor's label without knowing its rule is how label churn starts. I agree with the judgement; leaving as-is.Next: confirming the gate job conclusions on #8950, then ready → auto-merge → MERGED.⚠️ It branched from
a8189aef4before #8945 landed, and #8945 also editslint.yml+package.json. The PR body carries the resolution instruction; I will re-check those two hunks before arming.Generated by Claude Code
feat(devx): gate that a GA'd major's release page says it is released (…
test(lint): census the SYSTEM_FIELDS consumer population by module gr…
test(lint): census the SYSTEM_FIELDS consumer population by module gr…