Skip to content

feat(adr-0087): the seventh disposition category, runtime-export-retired — retired exports and members, proven by the compiler at both revs - #22940

Merged
os-zhuang merged 4 commits into
mainfrom
claude/issue-22918-runtime-export-retired
Oct 12, 2026
Merged

os-zhuang merged 4 commits into
mainfrom
claude/issue-22918-runtime-export-retired

Conversation

@objectstack-fleet

Copy link
Copy Markdown
Contributor

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 9e9e6f97f:
    • 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 feat(scripts,adr): a published TYPE-surface narrowing gets an honest ADR-0087 disposition — the sixth category, paid for by a base-side type reading #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

…an becomes metadataSurfaceReference

The same reading, extracted unchanged so a second category can ask it of a
retired export at the merge base without a second spelling of the question.
Self-test: 463 assertions, unchanged.

Claude-Session: https://claude.ai/code/session_012yK1ddAdAqfzwigRHZq3r2
Co-authored-by: Claude <noreply@anthropic.com>
…rammar, the compiler export reader, the three predicates (WIP: ADR half and self-tests follow)

Claude-Session: https://claude.ai/code/session_012yK1ddAdAqfzwigRHZq3r2
Co-authored-by: Claude <noreply@anthropic.com>
…red -- the ADR addendum (D9) and the self-test batteries

Claude-Session: https://claude.ai/code/session_012yK1ddAdAqfzwigRHZq3r2
Co-authored-by: Claude <noreply@anthropic.com>
@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

维护者速读

改了什么

  • ADR-0087 的「破坏性变更怎么处置迁移账本」词表新增第七类 runtime-export-retired,只改门禁脚本和 ADR 两个文件。
  • 一份 changeset 删掉已发布包的公开导出,或某个导出类型上的成员(例如插件配置项)时,作者在标记里点名这些名字。
  • 门禁用 TypeScript 编译器在合并基点和当前版本各读一次包的公开入口,确认三件事:以前确实导出、现在确实没了;它们都不是元数据;changeset 里的 FROM → TO 行恰好只写这些被点名的名字。

为什么改

  • 这类变更以前只能挂在兜底类 no-migration-prescription 上,而且是靠检测器读不到「没有箭头的 FROM … TO 行」才过关(样例 15205)。这正是 ADR D7 要淘汰的「靠漏检过关」。
  • 你 10-12 的裁决(6116982351:Q1 选 B 收窄、Q2 选 A)要求给它一个能复跑、能正面证明的类别,并覆盖导出类型上的成员。

风险与代价(含回滚)

  • 不碰任何运行时代码,不重评已有 changeset。存量 131 份破坏性 changeset 的审计输出前后逐字节相同。
  • 门禁多一个 typescript 依赖,但只在 changeset 点名这一类时才加载,其他 PR 的门禁耗时不变(实测中位数 3.85 秒对 3.80 秒)。
  • 有 4 个发布包映射不到源码入口,点名这些包会被响亮拒收,不会猜。
  • 回滚就是 revert 本 PR。

席位意见

  • 本席把 ADR 新附录 D9 和门禁改动逐条对过裁决:两种写法、三条检查、拒收规则、读源码入口这一偏差、样例 25–28 行通过而 29–30 行越界,全部一致。
  • CI 在当前提交上全绿(24 项通过、11 项为预期跳过)。改动 1433 行,在 3000 行人工合并线以下。
  • 开发标出两处按字面执行的读法,本席认为都在裁决范围内:
    • 「恰好」按双向执行:每个被点名的名字都必须有自己的一行 FROM → TO 修法。这与「删除要带一行修法」的约定一致。
    • tsup 的 .d.mts / .d.cts 入口视同 .d.ts。否则 @objectstack/spec 会被拒,映射不上的包数也会和裁决引用的 4 个对不上。
  • 推荐批准。

你要做的


Generated by Claude Code

@os-zhuang
os-zhuang marked this pull request as ready for review October 12, 2026 04:33
@os-zhuang
os-zhuang added this pull request to the merge queue Oct 12, 2026
Merged via the queue into main with commit da9c2bf Oct 12, 2026
44 checks passed
@os-zhuang
os-zhuang deleted the claude/issue-22918-runtime-export-retired branch October 12, 2026 05:35
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation size/xl skip-changeset PR has no user-facing published change; bypasses the changeset gate

Projects

None yet

3 participants