Repository navigation
Commit da9c2bf
feat(adr-0087): the seventh disposition category, runtime-export-retired — retired exports and members, proven by the compiler at both revs (#22940)
Fixes #22918
Clause-②: no
## What this does
ADR-0087's closed disposition vocabulary gains a **seventh category,
`runtime-export-retired`**, for a declared-breaking changeset that
retires published runtime exports (and members of an exported type) and
carries the FROM → TO line each retired name owes. Built on the
maintainer's ruling `6116982351` on #22918 (Q1 → B narrowed · Q2 → A)
and the unrevised clauses of ruling A `6116351873` on #22915.
The marker, written here without its comment delimiters (this body
cannot carry them): `adr-0087: not-required (runtime-export-retired
PACKAGE#EXPORT, PACKAGE#TYPE.MEMBER) WHY`. The two operand kinds mix in
one marker; the package is named, never a file.
Three predicates, re-verified on every run and pinned by name
(`RUNTIME_EXPORT_PREDICATES`):
1. **`retired-from-entry`** — an export is reachable from the package's
published entry at the merge base and absent at HEAD; for a member, its
type is reachable at the base, the member is present on it there
(`getPropertiesOfType`) and absent at HEAD while the type is still
exported. Read by the TypeScript compiler: a Program over the package's
**source** entry at each rev, served from that rev's blobs, and
`getExportsOfModule` on the entry's symbol (so `export *` resolves). The
entry is mapped through the `exports` map (`resolveTypesEntry`, imported
read-only from the readme-exports gate) and tsup's `dist/index.d.ts` →
`src/index.ts`; anything else is **refused loudly, never guessed**. An
`export *` the reader cannot follow refuses an absence claim at HEAD.
2. **`no-metadata-surface`** — at the merge base, no operand is declared
in a `*.zod.ts`, under `packages/spec/src/contracts/` or in an object
definition, none is a Zod projection, and no metadata surface references
it in code (the `runtime-interface-only` primitives; its step-4
reference scan is extracted unchanged as `metadataSurfaceReference` and
now shared).
3. **`from-operands-named`** — every FROM operand is a named operand
spelled as in the marker (`TYPE.MEMBER` for a member), and every named
operand has a FROM side. A call `name(...)` / `new Name(...)` reads as
that name; no constructor argument or object literal is parsed, so `new
EmailServicePlugin({ seedTemplates: false })` names `EmailServicePlugin`
and is refused as out of range (the ruling's own example). The category
is **not** exempt from the `no-migration-prescription` refusal: it sets
aside exactly the FROM lines it read and runs the same detector over
what is left.
`typescript` is loaded **lazily** (`loadExportReader`, through
`import-prerequisite.mjs`), only when a changeset names the category:
the entry point scans, and if a scan reports `needsExportReader` it
loads the compiler and scans again. The header line "Zero third-party
IMPORTS" is retracted, as ruled.
Files: `scripts/check-adr-0087-registration.mjs` and
`docs/adr/0087-metadata-protocol-upgrade-contract.md` (the vocabulary
block, and a new addendum beside the sixth category: D9, both operand
kinds, the predicates, the mapping refusal, the accepted deviation, the
worked example, what was not taken).
## Two readings taken where the ruling's words leave a choice (please
confirm on review)
- **"exactly the named operands" is read as set equality, both
directions.** A FROM operand outside the named set is refused (the ruled
negative case), and so is a named operand with no FROM side (self-test
`REX-R7`). This also mechanises the house rule that a removed export's
changeset carries its FROM → TO line. If only the first direction was
meant, the second check is one `for` loop to delete.
- **tsup's `.d.mts` / `.d.cts` twin maps like `dist/index.d.ts`.**
Re-measured at `018b077fb`: `resolveTypesEntry` lands
`@objectstack/spec`'s `.` on `dist/index.d.mts` (its `import`
condition). Mapping only the literal `.d.ts` makes 5 of 70 published
packages unmappable; with the twin it is the 4 the ruling cites
(`@objectstack/client-react`: no `src/index.ts`; `@objectstack/console`,
`@objectstack/skills`, `create-objectstack`: no types entry).
## Evidence (all at HEAD `f06cee413`, which includes `origin/main`
`e4ad6776d`)
- **The worked example, end to end through the shipping CLI** — two
ref-less probe commits on `7de0265fc`, its changeset re-marked under the
new category, judged with `--base 9e9e6f9`:
- lines 25–28 only (the nine exports): **exit 0**, `verified:
@objectstack/plugin-email#createSysEmailTemplateLoader (export,
packages/plugins/plugin-email/src/template-loader.ts), …` (the entry
point took the load-and-rescan path);
- lines 25–30 (the full block): **exit 1**, `the FROM operand new
EmailServicePlugin({ seedTemplates: false }) names EmailServicePlugin,
which the marker does not name -- out of range`.
- **Self-test**: `node scripts/check-adr-0087-registration.mjs
--self-test` → `548 assertions` (463 before; +3 from the vocabulary
growing, +82 new in four batteries: the category, `REX-N` predicates by
name, `REX-U` units, `REX-L` lazy load). The ruled cases: accept the
export half (`REX-G1`); refuse a still-present export (`REX-R1`), a
`*.zod.ts` symbol (`REX-R2`), a FROM operand outside the named set
(`REX-R3a`, `REX-R3b` = lines 29–30 verbatim), a non-canonical member
spelling (`REX-R4a`, `REX-R4b`), an unmappable package (`REX-R5a`,
`REX-R5b`); the six existing categories' batteries all green, and
`REX-C` pins that the worked example's own `no-migration-prescription`
verdict is unchanged.
- **Ablation** (committed first, mutated through
`scripts/ablation-replace.mjs`, trap-guarded): predicate 1's
still-present check off → `REX-R1` red; predicate 3's out-of-range check
off → `REX-R3a`, `REX-R3b`, `REX-R4a` red; the inherited-detector
residual off → `REX-R8a` red. Each restore proven: blob `322b6f872` =
HEAD and an empty `git diff HEAD`.
- **Six existing categories unchanged on stock**: `--list` before (at
`018b077fb`) and after is byte-identical (md5 `862b7498…`): 131
declared-breaking, 131 with a disposition, 83 on
`no-migration-prescription`. `--base origin/main` exit 0 before and
after.
- **Cost on every other PR**: a bare import of the gate loads no
`typescript` (`REX-L1`, positive control `REX-L2`). Interleaved wall
time on an empty diff, 6 runs each on a shared box: median 3848 ms (base
gate) vs 3795 ms (this gate).
- **Gates**: `node scripts/pm/dispatch-gates.mjs --ran` → `43 derived
famil(ies) accounted for — 43 run, 0 NOT-MEASURED (a DERIVED zero — all
43 recorded an exit code and none of them is 3)`.
`check:doc-formula-expressions` first answered exit 3 (PREREQUISITE NOT
MET: `@objectstack/formula` / `@objectstack/lint` unbuilt) and was
re-run green after building them. `check:pm-dispatch-gates`: `1828 cases
pass` (fast tier).
- **Lint, narrowed**: the diff's only lintable file is the gate (the ADR
is `.md`, matched by no config object's `files`); `eslint
--no-inline-config --format json` reports 1 file, 0 errors, 0 warnings.
The config resolved for it has no `parserOptions.project` /
`projectService`, so type-aware linting is off and this diff cannot move
any untouched file's verdict.
- Size: +1377 / −56 over 2 files (precedent PR #13596 +956 / −10; the
director estimated about 1.3× of ruling A's size).
## Acceptance notes
- The gate header's "What keeps `not-required` honest" section still
says "four of the five dispositions" — stale since the sixth category,
not touched here (prose drift, nothing reads it).
- The first dev's noted self-description gap is closed in this diff: the
detector's residual-blind-spot paragraph now names the arrowless `FROM …
TO` line (6 of 417 stock changesets, 4 declared-breaking, none read by
the detector), stating it stays out of the detector's reach and is read
by this category.
- The worked example
(`.changeset/15205-email-templates-resolve-registry.md`) is not
re-graded, as ruled.
## 维护者速读(草稿)
**改了什么**:ADR-0087 的「破坏性变更怎么处置迁移账本」词表新增第七类 `runtime-export-retired`。一个
changeset 删掉了已发布包的公开函数、常量、类型,或某个导出类型上的配置项时,作者在标记里点名这些名字;门禁用 TypeScript
编译器在合并基点和当前版本各读一次这个包的公开入口,核对「以前确实公开导出、现在确实没了」,确认它们都不是元数据,并确认 changeset
里的 FROM → TO 行恰好只写这些被点名的名字。只改了门禁脚本和 ADR 两个文件。
**为什么改**:这类变更以前只能挂在兜底类 `no-migration-prescription` 上,而且是靠检测器读不到「没有箭头的
FROM … TO 行」才过关(样例 15205),正是 D7 要淘汰的「靠漏检过关」。你 10-12 的裁决(Q1 B 收窄、Q2
A)要求给它一个能复跑、能正面证明的类别,并覆盖导出类型上的成员。
**风险与代价(含回滚)**:门禁多了一个 `typescript` 依赖,但只在 changeset 点名这一类时才加载,其他 PR
的门禁耗时不变(实测中位数 3.85 秒对 3.80 秒)。不碰任何运行时代码,也不重评已有 changeset:存量 131 份破坏性
changeset 的审计输出前后逐字节相同。回滚就是 revert 本
PR。有两处按字面读的地方请你确认:「恰好」按双向执行(被点名的每个名字都必须有自己的 FROM 行);tsup 的 `.d.mts`
入口也算可映射,否则 `@objectstack/spec` 会被拒,与裁决引用的「70 个里 4 个映射不上」对不上。
**席位意见**:
**你要做的**:读 ADR 新附录 D9 和上面两处读法;同意就在本 PR 上给 APPROVED review,席位随后落地。
---
_Generated by [Claude
Code](https://claude.ai/code/session_012yK1ddAdAqfzwigRHZq3r2)_
---------
Co-authored-by: Claude <noreply@anthropic.com>1 parent 51b43a8 commit da9c2bf
2 files changed
Lines changed: 1377 additions & 56 deletions
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
730 | 730 | | |
731 | 731 | | |
732 | 732 | | |
| 733 | + | |
| 734 | + | |
733 | 735 | | |
734 | 736 | | |
735 | 737 | | |
| |||
739 | 741 | | |
740 | 742 | | |
741 | 743 | | |
742 | | - | |
743 | | - | |
| 744 | + | |
| 745 | + | |
| 746 | + | |
744 | 747 | | |
745 | 748 | | |
746 | 749 | | |
| |||
970 | 973 | | |
971 | 974 | | |
972 | 975 | | |
| 976 | + | |
| 977 | + | |
| 978 | + | |
| 979 | + | |
| 980 | + | |
| 981 | + | |
| 982 | + | |
| 983 | + | |
| 984 | + | |
| 985 | + | |
| 986 | + | |
| 987 | + | |
| 988 | + | |
| 989 | + | |
| 990 | + | |
| 991 | + | |
| 992 | + | |
| 993 | + | |
| 994 | + | |
| 995 | + | |
| 996 | + | |
| 997 | + | |
| 998 | + | |
| 999 | + | |
| 1000 | + | |
| 1001 | + | |
| 1002 | + | |
| 1003 | + | |
| 1004 | + | |
| 1005 | + | |
| 1006 | + | |
| 1007 | + | |
| 1008 | + | |
| 1009 | + | |
| 1010 | + | |
| 1011 | + | |
| 1012 | + | |
| 1013 | + | |
| 1014 | + | |
| 1015 | + | |
| 1016 | + | |
| 1017 | + | |
| 1018 | + | |
| 1019 | + | |
| 1020 | + | |
| 1021 | + | |
| 1022 | + | |
| 1023 | + | |
| 1024 | + | |
| 1025 | + | |
| 1026 | + | |
| 1027 | + | |
| 1028 | + | |
| 1029 | + | |
| 1030 | + | |
| 1031 | + | |
| 1032 | + | |
| 1033 | + | |
| 1034 | + | |
| 1035 | + | |
| 1036 | + | |
| 1037 | + | |
| 1038 | + | |
| 1039 | + | |
| 1040 | + | |
| 1041 | + | |
| 1042 | + | |
| 1043 | + | |
| 1044 | + | |
| 1045 | + | |
| 1046 | + | |
| 1047 | + | |
| 1048 | + | |
| 1049 | + | |
| 1050 | + | |
| 1051 | + | |
| 1052 | + | |
| 1053 | + | |
| 1054 | + | |
| 1055 | + | |
| 1056 | + | |
| 1057 | + | |
| 1058 | + | |
| 1059 | + | |
| 1060 | + | |
| 1061 | + | |
| 1062 | + | |
| 1063 | + | |
| 1064 | + | |
| 1065 | + | |
| 1066 | + | |
| 1067 | + | |
| 1068 | + | |
| 1069 | + | |
| 1070 | + | |
| 1071 | + | |
| 1072 | + | |
| 1073 | + | |
| 1074 | + | |
| 1075 | + | |
| 1076 | + | |
| 1077 | + | |
| 1078 | + | |
| 1079 | + | |
| 1080 | + | |
| 1081 | + | |
| 1082 | + | |
| 1083 | + | |
| 1084 | + | |
| 1085 | + | |
| 1086 | + | |
| 1087 | + | |
| 1088 | + | |
| 1089 | + | |
| 1090 | + | |
| 1091 | + | |
| 1092 | + | |
| 1093 | + | |
| 1094 | + | |
| 1095 | + | |
| 1096 | + | |
| 1097 | + | |
| 1098 | + | |
| 1099 | + | |
| 1100 | + | |
| 1101 | + | |
| 1102 | + | |
| 1103 | + | |
| 1104 | + | |
| 1105 | + | |
| 1106 | + | |
| 1107 | + | |
| 1108 | + | |
| 1109 | + | |
| 1110 | + | |
| 1111 | + | |
| 1112 | + | |
973 | 1113 | | |
974 | 1114 | | |
975 | 1115 | | |
| |||
0 commit comments