Skip to content

Commit e072c97

Browse files
claude[bot]os-zhuangclaude
authored
feat(devx): gate any Plugin implementation whose teardown the kernel never calls (#10774)
* feat(devx): gate Plugin implementations that spell teardown stop()/shutdown()/close()/dispose() with no destroy() Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DdCnBGcHeufjrq7drTD3wt * ci(lint): run the plugin teardown-shape gate, and classify it in the ratchet-remedy control Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DdCnBGcHeufjrq7drTD3wt --------- Co-authored-by: os-zhuang <jack@objectstack.ai> Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
1 parent 9c0215f commit e072c97

3 files changed

Lines changed: 817 additions & 0 deletions

File tree

.github/workflows/lint.yml

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1715,6 +1715,47 @@ jobs:
17151715
- name: Paired kernel-hook pin gate
17161716
run: pnpm check:kernel-hook-pairs
17171717

1718+
# Plugin teardown-shape gate (#10619, from #9371 / #10371). `Plugin`
1719+
# declares exactly one teardown hook — `destroy?()` — and both kernels
1720+
# call only that one. A plugin whose teardown is spelled `stop()` is
1721+
# therefore never torn down: `await kernel.shutdown()` RESOLVES with its
1722+
# timers still armed. #9371 measured 48 further delivery reads/writes in
1723+
# the 80 ms after a resolved shutdown, and the bill arrived as merge-queue
1724+
# evictions of two green PRs — because an `unref`'d timer lets a host
1725+
# process exit while a test process stays alive through teardown.
1726+
# WHY A GATE AND NOT THE REPAIRS: the trap is an ASYMMETRY. `start?()` IS
1727+
# on the interface and does fire, so a `start`/`stop` pair reads symmetric
1728+
# to a reviewer while only one half is ever called — which is how this
1729+
# survived review in two packages and was found only by the evictions. Fix
1730+
# the known instances and the next one arrives spelled `shutdown()` or
1731+
# `close()`; a gate makes the class unreachable. Ruled FOR by the
1732+
# `domain:devx` PM on the #9371 thread.
1733+
# The roster is `stop` / `shutdown` / `close` / `dispose`, and what was
1734+
# deliberately left OFF it (`disconnect`, `release`, `end`, `cleanup` …)
1735+
# is argued in the script header and pinned as green cases by the
1736+
# self-test — a roster chosen silently is the thing that goes stale.
1737+
# The delegating alias (`stop()` calling `destroy()`, the #9371 repair
1738+
# shape landed by PR #10375) stays green, and the self-test's positive
1739+
# control is the REAL pre-#10375 revision of that same file rather than a
1740+
# synthesised class.
1741+
# The 11 instances that predate the gate are baselined shrink-only, each
1742+
# pointing at #10371 which owns their repair; the list refuses new members
1743+
# outright, so nothing can join it to get CI green.
1744+
# Invoked as `node` rather than through a `pnpm check:*` alias, same as
1745+
# the other `node scripts/…` steps in this lane: that alias is a line in
1746+
# root package.json, inside the @changesets/cli v3 fence (#9465).
1747+
# Runs its own --self-test first, and that leg is not decoration: this
1748+
# gate computes its own population, so a scan that stopped matching would
1749+
# print a confident green over a tree it never read (#4690's family). The
1750+
# self-test's refusal cases are each PAIRED with a readable tree that
1751+
# still returns a verdict, so "refuses unconditionally" cannot satisfy it.
1752+
# Static AST over ~4,400 sources plus one `git show` and two spawns;
1753+
# 3.4s + 2.5s measured locally, no build needed, so it belongs in this job.
1754+
- name: Plugin teardown-shape gate
1755+
run: |
1756+
node scripts/check-plugin-teardown-shape.mjs --self-test
1757+
node scripts/check-plugin-teardown-shape.mjs
1758+
17181759
# Resume-authority declaration gate (#5561, from #3823). The #3801 resume
17191760
# gate keys on the SUSPENDED NODE, so it covers a pausing node type exactly
17201761
# when that type's author remembered to declare `resumeAuthority`. #3823 is

0 commit comments

Comments
 (0)