Commit e956924
Fixes #20390
Clause-②: yes
Implements ruling `5865890672` (batch #235 item 1, letter **A**,
maintainer 「同意 A」; maintainer record `5865873150`, route `5866178043`):
every retired entry in the ADR-0087 conversion registry carries a
REQUIRED `retiredAfter`, and the artifact forward-conversion window
decides per entry. It is one vertical PR across `packages/spec`,
`packages/metadata-core` and the artifact door in `packages/metadata`.
## Spec half
- **`MetadataConversion` is a live-or-retired union**
(`packages/spec/src/conversions/types.ts`). An entry with
`retiredFromLoadPath: true` must also carry `retiredAfter`, typed as a
stable `x.y.z` template-literal string; a live entry carries neither.
tsc refuses an unstamped retirement (the reverse verification is below).
The type moves from an interface to a type alias, so `gen:api-surface`
and `gen:export-origins` each rewrite one row: `MetadataConversion
(interface)` becomes `MetadataConversion (type)`.
- **Backfill, from the published tarballs.** Each published entry's
value is the stable release just before the first tarball that carries
it retired. Each entry in no published tarball carries the current
`package.json` label, `17.4.0`.
- **Census test.** `src/conversions/retired-after.census.json` holds raw
facts per stable release since the registry first shipped (14.8.0
through 17.4.0): the tarball integrity and the ids its `ALL_CONVERSIONS`
marks retired. `src/conversions/retired-after.census.test.ts` pins every
entry's value against it, offline, in the `local` tier. It pins that
every entry absent from the last published tarball carries the label,
and that no value is malformed or above the label.
`scripts/build-retired-after-census.ts` re-derives the census from
registry.npmjs.org. It checks each tarball's integrity, imports each
release's `dist/index.mjs`, and writes the census, or compares it with
`--check`.
## metadata-core half
`applyArtifactForwardConversions` replays entry E when the artifact's
floor is below the runtime label OR at or below `E.retiredAfter`.
`DEFAULT_FLIPS_NOT_REPLAYED_HERE` is still read first. Its membership is
unchanged; `flow-decision-mode-inclusive-explicit` came in with the
merge of #20344. When the floor is at or above the label, only the
entries the floor predates are replayed. The rest reach the strict parse
and their tombstones through the existing `excludeConversionIds` seam,
computed per entry from the registry. There is no second table.
`ArtifactForwardConversionVerdict` gains `'converted-retired-after'` for
that case. `ArtifactForwardConversionResult` gains `replayedRetirements`
(element type `ArtifactReplayedRetirement`): under that verdict, each
retirement this runtime enforces past the artifact's floor, with its
`retiredAfter`; it is empty for every other verdict. The module
docblock's two policy sentences still hold: "a key retired at version V
stays a loud refusal for anything authored at ≥ V" (the
floor-at-or-above-label bullet), and "Not a second conversion table".
## The door's consumer arm (`packages/metadata/src/plugin.ts`)
The verdict has one in-tree consumer that branches on it, and the new
arm is added there.
- **Which verdicts open the window** is now one total table,
`FORWARD_WINDOW_OPENED` (a readonly `Record` keyed by every
`ArtifactForwardConversionVerdict` member, valued `boolean`), with
`'converted-retired-after'` on the open side.
`_warnUnboundFormPredicateRoots` (the #12915 scope-C notice) returns on
`!FORWARD_WINDOW_OPENED[result.verdict]`. That makes its docblock
sentence true again: the notice is "read off that pass's own verdict
rather than recomputed, so the two can never disagree", and it no longer
depends on the label. On `main` today, a 17.4.0-built artifact with a
bare-root form predicate is announced now, not once the label reaches
17.5.0.
- **Why the table, not the inverted guard.** The two forms the order
offered have opposite defaults for a verdict that does not exist yet.
Adding the arm to the old hand-written guard defaults a future verdict
to "closed", which is how this defect arose. Inverting the guard (return
only on `'authored-current'` / `'runtime-version-unknown'`) defaults it
to "open", and it would also admit `'not-an-object'`. A total `Record`
over the verdict union has no default: a new member is a compile error
until someone places it. This is the "add the arm" route, spelled so
that tsc forces the next decision. Reverse-verified below.
- **The warn lines under the new verdict** no longer say the artifact
"predates this runtime's spec" beside a runtime version equal to its
floor. The conversion summary names the retirement this runtime enforces
past the artifact's floor, with the release that last accepted the shape
(from `replayedRetirements`). It then says the artifact converts again
on every boot until it is rebuilt with tooling from a release that ships
the retirement. The #12915 notice opens with the same verdict-aware
clause. Every other verdict keeps its existing wording.
- `plugin-unbound-form-predicate-roots.test.ts`'s "current surface"
silence pin had derived that surface as a caret range on the installed
label. That spelling is itself the label-dependence this change removes:
on `main` it names an artifact built BY the last release. It now derives
the first `x.y.z` past both the label and every `retiredAfter`.
## The four pins
| Pin | Where | Asserts |
|:--|:--|:--|
| (1) a 17.4.0-CLI-built artifact with dashboard charts and page
`assignedProfiles` boots on `main` and logs the notices |
`packages/metadata/src/plugin-artifact-forward-conversion-retired-after.test.ts`,
on a REAL fixture: `dist/objectstack.json` built verbatim by the
published `@objectstack/cli` 17.4.0 | the dashboard and page register
with `chartConfig.type`/`xAxis`/`yAxis` and `assignedProfiles` converted
away; one warn line each for
`dashboard-widget-chart-config-structure-removed` (3 sites) and
`page-assigned-profiles-removed` (1 site) |
| (2) newly authored sources using the retired keys are still refused
loudly | same file | `defineStack` refuses with `code:
'STACK_SCHEMA_INVALID'`, `status: 422`, and one issue per retired site
(4 paths) |
| (3) floor exactly 17.5.0 on a 17.5.0-labelled runtime is refused, not
converted |
`packages/metadata-core/src/artifact-forward-conversion.test.ts` |
verdict `authored-current`, zero notices, and the strict parse refuses
the same 4 paths |
| (4) unreleased `main` (label 17.4.0), artifact at the last release
(`^17.4.0`) | same file | verdict `converted-retired-after`, notices by
id and path, and the strict parse passes |
Beside pin (1), **the #12915 pin**
(`plugin-artifact-forward-conversion-retired-after.test.ts`, "announces
a bare-root form predicate once"): the `^17.4.0` fixture with one
bare-root form predicate (`stage == "won"`) on the 17.4.0 runtime logs
the unbound-root line exactly once, including across a second ingestion.
It is red under the old guard and green now (below).
Three companions sit beside the pins. After the release (label 17.5.0)
the same artifact converts through the label half, with
`replayedRetirements` empty. A 17.2.0 retirement still meets its
tombstone inside the open per-entry window.
`flow-decision-mode-inclusive-explicit` stays refused inside its own
per-entry window. Pin (4) also asserts `replayedRetirements`: both
retirements at `17.4.0`, and never the default flip.
## Census (re-derived on this tree, npm `latest` = `17.4.0`, label =
`17.4.0`)
94 retired entries: **73 published** and **21 unpublished**. The ruling
counted 91 retired with 18 unpublished at `df3ba164`. Three unpublished
entries landed since then: `action-aria-removed`,
`connector-resilience-keys-removed` (#20350) and
`flow-decision-mode-inclusive-explicit` (#20344, merged into this
branch).
| first published retirement | entries | `retiredAfter` |
|:--|--:|:--|
| 15.1.0 | 5 | 15.0.0 |
| 17.0.0 | 45 | 16.1.0 |
| 17.1.0 | 5 | 17.0.0 |
| 17.2.0 | 2 | 17.1.0 |
| 17.3.0 | 8 | 17.2.0 |
| 17.4.0 | 8 | 17.3.0 |
| none (unpublished) | 21 | 17.4.0 |
The ruling's census bucket of 50 entries "first retired in 17.0.0" is 45
+ 5. The engine seat's census started at the 17.0.0 tarball. Those 5
entries (`object-compactLayout-to-highlightFields`,
`stack-roles-to-positions`, `owd-legacy-read-aliases`,
`sharing-recipient-role-to-position`,
`book-audience-profile-to-permission-set`) are already retired in the
15.1.0, 15.1.1, 16.0.0 and 16.1.0 tarballs, so the ruling's own
principle gives them `15.0.0`.
## Verification (final HEAD `2c537b7e`)
- Derived gates: `node scripts/pm/dispatch-gates.mjs --commands --repo
objectstack-ai/objectstack` gave 90 commands at `2c537b7e` (16 files,
+1667/−72), and all 90 exit 0 on that head. The `--ran` reconciliation
(each line carrying its exit code) reads: "90 derived, 90 run, 0
NOT-MEASURED, 0 UNRUN". The full package closure was rebuilt first
(turbo 71/71).
- `@objectstack/spec` `test` (`--project local`): Test Files 565 passed
(565), Tests 16645 passed, 1 todo. `test:repo` (`--project repo`, run in
two halves of 18 files each to fit the foreground cap): 18 files / 460
tests and 18 files / 195 tests, together Test Files 36 passed (36),
Tests 655 passed.
- `@objectstack/metadata-core` `test`: Test Files 16 passed (16), Tests
295 passed (295). `typecheck` exit 0.
- `@objectstack/metadata` `test`: Test Files 55 passed (55), Tests 826
passed (826). `typecheck` exit 0.
- eslint `--no-inline-config --format json` on the 10 changed source
files: 10 files linted, 0 errors, 0 warnings. `eslint.config.mjs` never
enables type-aware linting, so this diff cannot move the verdict on any
untouched file.
- Main was merged three times, all through
`scripts/pm/os-regen-merge.sh`. None of this round's incoming commits
touch `packages/spec/src/conversions`, `packages/metadata-core` or
`packages/metadata`, and none adds a retired entry: every one of the 94
carries `retiredAfter`.
## Ablation and reverse verification (from committed state, through
`scripts/ablation-replace.mjs`)
- **Guard ablation (this round).** In `plugin.ts`, `if
(!FORWARD_WINDOW_OPENED[result.verdict]) return;` was put back to the
old guard, `if (result.verdict !== 'converted-forward' && result.verdict
!== 'converted-undeclared') return;`, with a marker comment. On-disk
count: marker 1, new guard 0. Across the three door suites (21 tests),
exactly one went red, the #12915 pin ("announces a bare-root form
predicate once"). The rest stayed green, including the updated
current-surface silence pin. Restore: blob `8f43972c` equals HEAD, `git
diff HEAD` is empty, `git status --porcelain` has 0 lines, and all 21
tests pass again. The suites import `plugin.ts` from source, so no build
sits between the mutation and the run.
- **tsc forces the next verdict decision.** With the
`'converted-retired-after': true` row removed from
`FORWARD_WINDOW_OPENED`, `tsc --noEmit` in `packages/metadata` exits 2
with `error TS2741: Property '"converted-retired-after"' is missing`.
Restored to the HEAD blob.
- **Window ablation (round 0, at `87da6b88`).** The per-entry branch was
replaced with the old label-only verdict, and `metadata-core` was
rebuilt, with the marker present in 2 built files. Pin (4), pin (1) boot
and pin (1) notices went red, along with both per-entry companions. Pins
(2) and (3) stayed green. The restore was proven (blob equals HEAD, 0
porcelain lines, and the marker absent from the rebuilt dist).
- **tsc refuses an unstamped retirement.** With `retiredAfter` removed
from `page-assigned-profiles-removed`, spec `tsc --noEmit` exits 2 with
exactly one `error TS2322`.
- **The census test fails when it should.** A published entry stamped
low reds the PUBLISHED test, and an unpublished entry stamped low reds
the UNPUBLISHED test. `build-retired-after-census.ts --check` passes
against npm (11 releases), and exits 1 on a tampered census.
## Deviations from the ruling text, and why
1. **The rule for unpublished entries has one tolerance.** While the
label is AHEAD of the census's last release, an unpublished entry may
carry any version from that release up to the label. Taken literally
("carries the current label"), the rule turns the Version Packages PR
red. That PR bumps the label to 17.5.0 before 17.5.0 is published, while
the 17.5.0 entries correctly carry 17.4.0. The tolerance closes again
once the census records the new tarball. The seat confirmed this reading
(`5869635456`). The refresh is now a written step of the GA release
flow: `docs/releases-maintenance.md`, under "Cutting a GA release — the
Version Packages PR flow", says to run
`scripts/build-retired-after-census.ts` after a stable
`@objectstack/spec` publish and commit the refreshed census. The seat
answered the refresh question with A; no workflow and no gate are added.
2. **The network half is a script, not a repo-tier test** (accepted by
the seat, `5869635456`). `vitest.repo-tests.json` is held equal to the
set of tests that read outside the package
(`check:cross-package-test-inputs`), so a network-only test cannot be
listed there. Reading the tarballs means downloading every stable
release since 14.8.0 (about 11 tarballs, over 250 MB), so no per-run
suite does it. So CI pins the committed census offline, and
`scripts/build-retired-after-census.ts` re-derives it. The script
refuses loudly when offline; it never skips. It is not a `package.json`
script and not wired into CI, so no gate is added.
3. **Stable releases only.** The census and the rule skip `-rc`
versions: a caret floor never names a prerelease, and the door compares
`x.y.z` triples.
4. **Counts.** See the Census section: 73 published, 21 unpublished, and
a 15.1.0 bucket the ruling's counts did not have.
## Acceptance notes
- `packages/metadata` now carries a `patch` changeset entry for the door
change. It changes no public API; the #12915 notice and the conversion
summary wording follow the per-entry window.
- `field-required-notnull-explicit` appears retired in the 17.0.0
through 17.3.0 tarballs and is gone from 17.4.0 and `main`, withdrawn by
#16693. The census test ignores ids not on `main`.
- The seat files two follow-ups at landing, as governed surfaces outside
this PR (per `5869635456`): ADR-0087's #12772 addendum sentence that a
floor at or above the runtime "replays nothing", and the retirement kit
in `.claude/skills/spec-property-retirement/SKILL.md`.
- Once this lands, any open PR that adds a retired entry fails typecheck
until it stamps `retiredAfter`. That is the designed loud direction.
- `main` narrowed `manifest.id` (underscores refused, #17534). So a
17.4.0-built artifact whose id has an underscore is refused whatever
this window does. The pin fixture uses a reverse-domain id for that
reason.
---
_Generated by [Claude
Code](https://claude.ai/code/session_01ARcDurZ5j34RdqsGgc4jgH)_
---------
Co-authored-by: Claude <noreply@anthropic.com>
1 parent dc0ab6a commit e956924
16 files changed
Lines changed: 1668 additions & 72 deletions
File tree
- .changeset
- docs
- packages
- metadata-core/src
- metadata/src
- __fixtures__
- spec
- api-surface
- export-origins
- scripts
- src/conversions
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
| 17 | + | |
| 18 | + | |
| 19 | + | |
| 20 | + | |
| 21 | + | |
| 22 | + | |
| 23 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
447 | 447 | | |
448 | 448 | | |
449 | 449 | | |
| 450 | + | |
| 451 | + | |
450 | 452 | | |
451 | 453 | | |
452 | 454 | | |
| |||
Lines changed: 171 additions & 2 deletions
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
98 | 98 | | |
99 | 99 | | |
100 | 100 | | |
101 | | - | |
102 | | - | |
| 101 | + | |
| 102 | + | |
| 103 | + | |
| 104 | + | |
| 105 | + | |
| 106 | + | |
103 | 107 | | |
104 | 108 | | |
105 | 109 | | |
| |||
401 | 405 | | |
402 | 406 | | |
403 | 407 | | |
| 408 | + | |
| 409 | + | |
| 410 | + | |
| 411 | + | |
| 412 | + | |
| 413 | + | |
| 414 | + | |
| 415 | + | |
| 416 | + | |
| 417 | + | |
| 418 | + | |
| 419 | + | |
| 420 | + | |
| 421 | + | |
| 422 | + | |
| 423 | + | |
| 424 | + | |
| 425 | + | |
| 426 | + | |
| 427 | + | |
| 428 | + | |
| 429 | + | |
| 430 | + | |
| 431 | + | |
| 432 | + | |
| 433 | + | |
| 434 | + | |
| 435 | + | |
| 436 | + | |
| 437 | + | |
| 438 | + | |
| 439 | + | |
| 440 | + | |
| 441 | + | |
| 442 | + | |
| 443 | + | |
| 444 | + | |
| 445 | + | |
| 446 | + | |
| 447 | + | |
| 448 | + | |
| 449 | + | |
| 450 | + | |
| 451 | + | |
| 452 | + | |
| 453 | + | |
| 454 | + | |
| 455 | + | |
| 456 | + | |
| 457 | + | |
| 458 | + | |
| 459 | + | |
| 460 | + | |
| 461 | + | |
| 462 | + | |
| 463 | + | |
| 464 | + | |
| 465 | + | |
| 466 | + | |
| 467 | + | |
| 468 | + | |
| 469 | + | |
| 470 | + | |
| 471 | + | |
| 472 | + | |
| 473 | + | |
| 474 | + | |
| 475 | + | |
| 476 | + | |
| 477 | + | |
| 478 | + | |
| 479 | + | |
| 480 | + | |
| 481 | + | |
| 482 | + | |
| 483 | + | |
| 484 | + | |
| 485 | + | |
| 486 | + | |
| 487 | + | |
| 488 | + | |
| 489 | + | |
| 490 | + | |
| 491 | + | |
| 492 | + | |
| 493 | + | |
| 494 | + | |
| 495 | + | |
| 496 | + | |
| 497 | + | |
| 498 | + | |
| 499 | + | |
| 500 | + | |
| 501 | + | |
| 502 | + | |
| 503 | + | |
| 504 | + | |
| 505 | + | |
| 506 | + | |
| 507 | + | |
| 508 | + | |
| 509 | + | |
| 510 | + | |
| 511 | + | |
| 512 | + | |
| 513 | + | |
| 514 | + | |
| 515 | + | |
| 516 | + | |
| 517 | + | |
| 518 | + | |
| 519 | + | |
| 520 | + | |
| 521 | + | |
| 522 | + | |
| 523 | + | |
| 524 | + | |
| 525 | + | |
| 526 | + | |
| 527 | + | |
| 528 | + | |
| 529 | + | |
| 530 | + | |
| 531 | + | |
| 532 | + | |
| 533 | + | |
| 534 | + | |
| 535 | + | |
| 536 | + | |
| 537 | + | |
| 538 | + | |
| 539 | + | |
| 540 | + | |
| 541 | + | |
| 542 | + | |
| 543 | + | |
| 544 | + | |
| 545 | + | |
| 546 | + | |
| 547 | + | |
| 548 | + | |
| 549 | + | |
| 550 | + | |
| 551 | + | |
| 552 | + | |
| 553 | + | |
| 554 | + | |
| 555 | + | |
| 556 | + | |
404 | 557 | | |
405 | 558 | | |
406 | 559 | | |
| |||
476 | 629 | | |
477 | 630 | | |
478 | 631 | | |
| 632 | + | |
| 633 | + | |
| 634 | + | |
| 635 | + | |
| 636 | + | |
| 637 | + | |
| 638 | + | |
| 639 | + | |
| 640 | + | |
| 641 | + | |
| 642 | + | |
| 643 | + | |
| 644 | + | |
| 645 | + | |
| 646 | + | |
| 647 | + | |
479 | 648 | | |
480 | 649 | | |
481 | 650 | | |
| |||
0 commit comments