Repository navigation
test(spec): under a prerelease package label the retired-after census refuses the release triple, and the retiredAfter TSDoc names the newest published stable release - #23130
Conversation
… refuses the release triple, and the retiredAfter TSDoc names the newest published stable release A prerelease label x.y.z-PRERELEASE precedes x.y.z, so every census comparison against the label reads it with semver precedence: the release-pending range is [last censused, x.y.z) under a prerelease label and stays [last censused, x.y.z] under a stable one. A prerelease label never takes the exact-label branch. The UNPUBLISHED rule is one helper with a simulated-label seam, pinned on both labels. Claude-Session: https://claude.ai/code/session_012uFegdfR6K6uqvLzt7XsdZ Co-authored-by: Claude <noreply@anthropic.com>
…erelease-retired-after
📓 Docs Drift Check1 anchor(s) derived from 1 changed package(s); no hand-written page names any of them, so this run has nothing to list — not a clean bill of health. This check sees only pages that NAME a derived anchor: one that documents this change in prose, or enumerates it in an authoring dialect, names none and stays invisible to it on every run. What this run could not see
Coarse fallback — 141 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): Which tree this was computed onThis run read A worktree cut from an older # while this PR is open — GitHub drops the merge commit once it closes
git fetch origin d4f411b218e7d1b70ba0f82ff5aa04287ea4c504 && git checkout d4f411b218e7d1b70ba0f82ff5aa04287ea4c504
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 287fd6e5d054b9b1efa80f08f9e5be609edb32c2 e10a433726204b9a93358021a4a8d68218b8f211 && git checkout -B drift-repro 287fd6e5d054b9b1efa80f08f9e5be609edb32c2 && git merge --no-ff e10a433726204b9a93358021a4a8d68218b8f211
node scripts/docs-audit/affected-docs.mjs --json 287fd6e5d054b9b1efa80f08f9e5be609edb32c2 |
Contract reviewServed-tier: PR #23130 for card #23042, read on 2026-10-13T03:01Z: the card body and all 4 comments (triage ① Derived judgmentsEvery accept-set and public-surface movement the diff implies, each judged:
② Semver level
③ Boundary flags
Deviations
Out-of-scope findings
My own sweep beyond the dev's ( Governed surfaces: none of the 3 paths is in the register (no Check-runs on the head at 2026-10-13T03:01Z: 39 total — 23 success (among them Implemented-by: VERDICT: PASS |
Fixes #23042
Clause-②: no (the published accept set does not change:
retiredAfterkeeps its${number}.${number}.${number}type; the census is a repository test, and the TSDoc states the rule the PUBLISHED rule already implies)Under a prerelease package label (
18.0.0-next.N, Changesets pre mode), a new retirement now has a stamp that tsc, the census and the PUBLISHED rule all accept: the newest published stable release, which sits below the label's release triple. The census reads the label with semver precedence (x.y.z-PREprecedesx.y.z), so18.0.0is refused now, during the prerelease. Before this change it passed until the census refresh after18.0.0ships. TheretiredAfterTSDoc states the rule for both labels.This follows triage's direction on the card (comment 6124786465) and card option B. The governed playbook half (
.claude/skills/spec-property-retirement/SKILL.md:204-205) is #23057's surface; #23057 remains open and this PR does not edit that file.What changed
packages/spec/src/conversions/retired-after.census.test.tsatOrBelowLabel(v, label). A stable labelx.y.zadmitsx.y.z, as before. A prerelease label admits only versions strictly belowx.y.z. All three comparisons against the label use it: the census well-formedness check, the "never above the package label" check, and the UNPUBLISHED release-pending range.unpublishedStampProblem(v, label, lastCensused). The label is an argument, so tests can simulate any label without depending onpackage.json. Under a prerelease label the range is[last censused, x.y.z). A prerelease label never takes the exact-label branch: the census never records a prerelease tarball, and that branch's stamp would be the release triple.18.0.0and18.0.1and admits17.4.0,17.7.0and17.7.1, under both18.0.0-next.0and18.0.0-next.12. It keeps the range open even when the census holds18.0.0.atOrBelowLabelon both labels.17.xmaintenance release published during the 18 prerelease without the retirement still accepted the old shape. Once it is censused, it raises the range's lower bound, and the entry is re-stamped to it. After GA, the PUBLISHED rule names that release as "just before"18.0.0, because the census sorts stable releases by semver.packages/spec/src/conversions/types.ts: theRetiredConversionState.retiredAfterTSDoc. The old sentence was "it is the package's version label at the moment the retirement lands", which has no value that type-checks under a prerelease label. It now says the newest stable release at the moment the retirement lands. Under a stable label that is the label itself. Under a prerelease label it is the newest published stable release, belowx.y.z. The maintenance-release case is stated too..changeset/23042-retired-after-prerelease-stamp.md:'@objectstack/spec': patch. The TSDoc ships: before this change,dist/types-*.d.tsand.d.mtscarried the old sentence (2 files). After the rebuild they carry the new text once each, and the old sentence 0 times. The census test ships nothing:files[]publishesdistandsrc/**/*.zod.ts, andretired-after.censushas 0 hits indist/**/*.js.Not touched:
packages/spec/src/conversions/registry.tsand the census JSON (no row needed).check:generatednames no stale artifact.Measurements
All runs on this branch. Single-file runs use
pnpm --filter @objectstack/spec exec vitest run --project local --maxWorkers=2 src/conversions/retired-after.census.test.ts. Every mutation went throughnode scripts/ablation-replace.mjs, and every restore was proven by the tool: blob equals HEAD andgit diff HEADis empty.21c86444e, before any edit. With the label set to18.0.0-next.0(blob9eba400eto73244d5e) anddeclared-index-unique-scopestamped18.0.0(blob96d69e4btobabee78a), the census gaveTests 9 passed (9). Base admits18.0.0.2e7bb5fb2, same two mutations:Tests 2 failed | 10 passed (12). Red as expected. The "never above the package label" check and the UNPUBLISHED check both namedeclared-index-unique-scope.Tests 12 passed (12). No retired entry stamps a value this narrowing refuses.atOrBelowLabelchanged from "cmp(v, release)less than 0" to "less than or equal to 0" (blob7607c543toc094d14e). Result:Tests 2 failed | 10 passed (12); the prerelease pin and the precedence pin go red. A first attempt passed a repo-relative test path, ran no test file ("FILTER SELECTED NOTHING"), and counts as void.!prerelease &&guard on the exact-label branch removed (blob7607c543to3b70eb08). Result:Tests 1 failed | 11 passed (12); the "census holds18.0.0" pin goes red.17.4.0(census caught up, so the exact-label branch): the 22-entry UNPUBLISHED failure lists are byte-identical.17.7.0with one stamp at17.8.0(the pending branch): the range messageoutside [17.4.0, 17.7.0] — refresh the census (...)is identical.expected false to be trueinstead ofexpected 1 to be less than or equal to 0). The author-facing part,ID: retiredAfter V > label 'L', is identical.main(fromALL_CONVERSIONS, at21c86444e, which includes PR feat(spec)!: retire an email template's category and isSystem — the grouping and the built-in flag nothing read (#22876) #22990): 117 retired entries out of 134. The highest stamp is17.7.0, on 5 entries; no stamp reaches18.0.0.packages/spec/package.jsonreads17.7.0, and.changeset/pre.jsonhasmode: pre,tag: next.Tests and gates (head
e10a43372, merge basebcebd8409)src/conversionsate10a43372:Test Files 11 passed (11),Tests 448 passed (448).pnpm --filter @objectstack/spec typecheckate10a43372: exit 0.check:test-typecheckis OK. The census test is in thetsconfig.test.jsonprogram (--listFilesOnly: 1 hit) and has 0 debt rows.localproject at2e7bb5fb2, before the second main merge:Test Files 659 passed (659),Tests 19990 passed | 1 todo. Declared narrowing: ate10a43372that run was cut by the 595 s foreground cap under lock contention. The merge brought in spec files from other PRs and changed none of this diff's files, so that run is left to CI.pnpm --filter @objectstack/spec build && check:generatedate10a43372:All 14 generated artifacts are up to date.node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstackderived 83 commands ate10a43372. All were run with exit codes captured before any pipe;--ranreports83 derived, 81 run, 1 NOT-MEASURED, 1 UNRUN.check:plugin-teardown-shape --self-test, after agit fetch --depth=1of its pinned fixture commit;check:lean-entry-closure, after an objectql closure build; andcheck:query-options-erasure, re-run alone with a longer cap.check:dual-build-cjs-loads. Reason: exit 3, PREREQUISITE NOT MET, it needs a whole-workspacedist. This diff changes no build input it reads.check:type-check-debt. Reason: whole-workspace build, CI owns it.--ranalso printed a stale-tree note:origin/mainmoved 7 commits past this head during the run (to287fd6e5d). The one derived-from file that changed isscripts/engine-double-contract.pinned.json. This diff adds no engine double.eslint --no-inline-config --format jsonreports files 2, errors 0, warnings 0. Both files are in the population (--print-configresolves 4 and 5 rules). No type-aware linting is configured (noparserOptions.projectorprojectService), so this diff cannot change the verdict on any untouched file. Repo-widepnpm lintis CI's.check:nul-bytesexits 0.Acceptance notes
Sweep of shipped sentences that state the stamp rule.
git grepcoveredpackages/spec/src,packages/metadata-core,packages/spec/scripts,scriptsandcontent/docs, for "version label", "package label", "label at the moment" andretiredAfter. Each hit is either fixed here or listed below as outside this PR's file surface.packages/spec/scripts/build-retired-after-census.ts, header section "When to run it". It describes the release-pending tolerance as[last censused release, package.json label]. Under a prerelease label the upper bound is now exclusive of the release triple. The section defers to the test's docblock, and the script is not published. Outside this card's file surface. Noted, not filed.packages/metadata-core/src/artifact-forward-conversion.tsmodule docblock (theM.0.0bullet) andartifact-forward-conversion.test.ts(the major-step block's docblock). Both say that during the prerelease "the package label still names the last 17.x release". That holds only until the first pre-mode Version Packages PR writes18.0.0-next.N. Their stamp half ("stamped at or below that label", meaning the last 17.x release) agrees with this rule. The test docblock's "stamped at the prerelease label" means17.7.0. The door's verdicts do not depend on the stamp for step-18 entries. Noted, not filed.packages/metadata-core/CHANGELOG.md:489, a released entry: "the package's own version label at that moment". It was accurate for the stable labels of its release and is release-owned. No action..claude/skills/spec-property-retirement/SKILL.md:204-205("packages/spec/package.json的当前版本标签"). This is skills(spec-property-retirement): under a prerelease package label, the retiredAfter checklist item stamps the newest published stable release — the governed half of #23042 #23057's surface; that card is blocked on this one.docs/adr/0087-metadata-protocol-upgrade-contract.md(the 2026-09-30 amendment) definesretiredAfteras "the last published@objectstack/specwhose authoring surface still accepted the old shape". That agrees with this rule, so no finding.The PR body is written once, at creation.
Generated by Claude Code