Skip to content

fix(metadata,runtime,plugins): five more Plugin implementations tear down from destroy(), the hook the kernel calls - #10987

Merged
os-warren merged 8 commits into
mainfrom
claude/issue-10772-teardown-five-more
Aug 22, 2026
Merged

fix(metadata,runtime,plugins): five more Plugin implementations tear down from destroy(), the hook the kernel calls#10987
os-warren merged 8 commits into
mainfrom
claude/issue-10772-teardown-five-more

Conversation

@os-warren

@os-warrenos-warren commented Aug 22, 2026

Copy link
Copy Markdown
Collaborator

Fixes#10772

Five Plugin implementations released their teardown from a hook the kernel never
calls. Each now tears down from destroy() — the kernel's only teardown hook — and
keeps its old spelling as a delegating alias, because that name is public API of an
exported class.

Plugin (@objectstack/core's types.ts) declares init(), start?(ctx) and
destroy?(). ObjectKernel.performShutdown() (kernel.ts:776) and
LiteKernel.destroy() (lite-kernel.ts:171) walk the plugins in reverse calling
plugin.destroy(), and nothing anywhere calls stop(), dispose(), close() or
shutdown() on a plugin. So what each of these five released was still held after
await kernel.shutdown() had resolved.

packageclasswas spelledwhat outlived a resolved shutdown
@objectstack/metadataMetadataPluginstop (arrow property)artifact watcher, manager.dispose(), repository handle
@objectstack/runtimeAppPluginstop (arrow property)the app:unregistered catalog event, never emitted
@objectstack/runtimeExternalValidationPluginstop (arrow property)every armed drift-check setInterval
@objectstack/plugin-emailEmailServicePlugindisposetwo metadata subscriptions, the SMTP transport, an engine binding
@objectstack/plugin-webhooksWebhookOutboxPlugindisposethe auto-enqueuer and two engine hooks

Three of the five spell the alias as an arrow property, which is exactly why a
method-only reading of these classes missed them.

Premise re-derived on this branch, not transcribed

  • The five really were unrepaired on origin/main @ 81845c65a. Each file's only
    teardown-shaped member there is the alias, with no destroy():
    plugin.ts:549 stop =, app-plugin.ts:1422 stop =,
    external-validation-plugin.ts:182 stop =, email-plugin.ts:1255 async dispose(),
    webhook-outbox-plugin.ts:172 async dispose().
  • ExternalValidationPlugin owns exactly one armed-timer site, setInterval( at
    external-validation-plugin.ts:294, arming one timer per opted-in datasource. The
    file's other setInterval occurrences are a type annotation and two prose mentions —
    a grep hit count is not a fact count. Enumerated over every non-test
    implements Plugin source under packages/, exactly two call setInterval at all:
    this one and plugin-reports/src/reports-plugin.ts (repaired under plugin-reports: the timers started by ReportsPlugin are released from stop(), which the kernel never calls #10371). The card's
    claim holds as measured — with one refinement the card does not make: that census is
    over each class's OWN text. WebhookOutboxPlugin owns a setIntervaltransitively,
    through the AutoEnqueuer it constructs, so a text scan of the plugin class cannot see
    it. Its pin test asserts the live timer count directly for exactly that reason.
  • The dispose() callers census, with a positive control on the same corpus.
    Positive control: 58 .destroy() call sites under packages/ non-test sources, so the
    expression finds what exists. Against that, EmailServicePlugin.dispose() had exactly
    one pre-existing caller in the whole repo (email-plugin.template-runtime-write.test.ts:375)
    and WebhookOutboxPlugin.dispose() had zero — its teardown had never run in any
    process at all.

The baseline is deleted, not retagged

scripts/check-plugin-teardown-shape.mjs is shrink-only by its own words (:225
⛔ SHRINK-ONLY, :136 "a stale entry is itself a failure"). Repairing a plugin means
deleting its KNOWN_TEARDOWN_UNREACHED entry — a retag would not have satisfied the
gate. Proven by re-inserting the five entries on the repaired tree:

❌ check:plugin-teardown-shape -- 5 stale KNOWN_TEARDOWN_UNREACHED entry/entries:
packages/metadata/src/plugin.ts MetadataPlugin.stop() -- no longer unreached
… (all five) …
Good news, and the list must say so: delete each line above.

The array is now []. The gate's own verdict line no longer cites #10371 as the live
repair owner — that card is closed — and instead reports baseline fully burned down
when the ratchet is empty. The gate's self-test fixture at :675
({ file: 'a/src/p.ts', … }) is untouched: it is a fixture, not an entry, and the real
entry count is the one taken with the packages/ prefix.

Both directions are pinned, and each direction was proved by ablation

Predictions were written down before each mutation; every restore is byte-identical by
git hash-object.

A1 — stale baseline (re-insert the five entries): predicted exit 1 naming all five as
stale. Observed exactly that (quoted above). Restore 5e93066ba…5e93066ba….

A2 — revert the load-bearing member (external-validation-plugin.ts back to its
origin/main bytes, destroy gone): predicted a red gate naming the class, and a red pin
test on the timer count. Observed:

❌ packages/runtime/src/external-validation-plugin.ts:159 ExternalValidationPlugin.stop() -- no destroy()
× leaves no armed interval once shutdown() has resolved AssertionError: expected 1 to be +0
× issues no further drift reads once shutdown() has resolved AssertionError: expected 7 to be 2
× the kernel reaches destroy() during shutdown TypeError: plugin.destroy is not a function
Tests 4 failed | 2 passed (6)

Restore 9c370df26…9c370df26….

No rebuild sits between the edit and the run, and that is the proof of what resolves.
The pin test imports the plugin as ./external-validation-plugin.js, a relative
specifier vitest resolves into packages/runtime/src/. No build command was run between
the mutation and the observation, and git hash-object packages/runtime/dist/index.js was
6c9145953… both before and after — identical bytes while the verdict flipped from green
to four failures. A suite reading dist/ could not have moved.

A3 — the other direction (delete the retained stop alias, keep destroy()):
predicted the gate stays green and only the alias legs go red. Observed: gate exit 0,
0 known-unreached; TypeError: plugin.stop is not a function on both alias legs,
Tests 3 failed | 3 passed (6). The gate alone cannot see an alias deletion — the alias
assertions are what pin it. Restore 9c370df26…9c370df26….

A4 — the new vitest config is load-bearing, not decoration.plugin-webhooks had no
vitest config; its pin test is the package's first value import of @objectstack/core.
Removing the config reds check-test-source-alias: "NEW unaliased artifact import(s)…
@objectstack/core… Alias them in the package's vitest.config.* — widening the registry
entry is not the fix." The config aliases exactly that one specifier, anchored
(/^@objectstack\/core$/) so the published @objectstack/core/logger subpath is left to
exports. No registry entry was widened. Restore 1851931a9…1851931a9….

Tests

Each member gets its own commit and its own behavioural assertions: the resource is
released after a real await kernel.shutdown(); the kernel demonstrably reaches
destroy(); and the retained alias still tears down for a direct caller — including the
detached const { stop } = plugin shape the arrow properties supported, the
stop(ctx) argument the pre-repair signature required, and the synchronous void return
ExternalValidationPlugin.stop() had. Every pre-shutdown leg is a positive control, so no
assertion can pass vacuously on a plugin that armed nothing.

Local verification — all of it on 019b06738, clean tree

The gate union was derived on the FINAL commit with node scripts/pm/dispatch-gates.mjs
and no path arguments (change set derived from git — 14 path(s) vs merge base 81845c65a,
committed 14, working tree 0, untracked 0). Every exit code below was captured before any
pipe. Each line quotes the gate's own verdict, never a bare $?.

Base was brought up to origin/main @ 81845c65a (merge 019b06738) before measuring —
the branch had been cut 67 commits back, and a baseline measured on a stale base is what
nearly turned main red on this same file once already.

Suites (script names echoed in the output, so none of these is a zero-match pnpm --filter
that exits 0 having run nothing):

packageresult
@objectstack/runtimeTest Files 181 passed (181) · Tests 2692 passed (2692)
@objectstack/metadataTest Files 32 passed (32) · Tests 615 passed (615)
@objectstack/plugin-emailTest Files 27 passed (27) · Tests 425 passed (425)
@objectstack/plugin-webhooksTest Files 10 passed (10) · Tests 124 passed (124)

typecheck green for @objectstack/runtime, @objectstack/plugin-email,
@objectstack/plugin-webhooks. Declared narrowing:@objectstack/metadata has no
typecheck script at all — asking for one would have been a silent zero-match exit 0 — so
its type surface is covered here by its tsup build, its 615-test suite, and the DEBT
ledger re-measure below, which type-checks the package as a whole.

Gates, each with its own verdict line:

  • check:plugin-teardown-shape (run directly as node scripts/…, since pnpm check:plugin-teardown-shape is not a wired script and would exit 254) — exit 0:
    ✓ 61 Plugin implementation(s) across 4424 source(s) … (0 known-unreached, ⛔ SHRINK-ONLY, baseline fully burned down). Self-test exit 0: 47 cases pass.
  • check:engine-double-contract — exit 0: OK — 377 pinned, 133 in the DEBT ledger, 2 exempt.
  • check:test-source-alias — exit 0: OK — 72 packages with tests scanned; 61 registered …
  • check:type-check-debt (--re-measure, the ratchet half, and the one that answers for
    the two new test files landing in ledgered packages — @objectstack/metadata in DEBT,
    @objectstack/runtime in TEST_DEBT) — exit 0: OK — 33 ledger entr(ies) re-measured in 323.9s, 1908 raw tsc error(s) total, none above its recorded number. It first refused
    outright (--re-measure cannot run: 1 workspace dependenc(ies) … create-objectstack),
    which is NOT MEASURED rather than a pass; the closure was completed and it was re-run.
    ⛔ Its --lower surplus note is deliberately not acted on here — that is not this card.
  • check:adr-0087-registration — exit 0: this PR adds no declared-breaking changeset (1 non-breaking changeset(s) seen). The changeset is committed, which is what makes this
    verdict carry information — this gate reads changesets from git, not the working tree.
  • check-changeset-no-major exit 0 · check-empty-changeset exit 0 (1 declaring changeset(s) added) · check:changeset-gate-self-tests exit 0 · check:objectui-changeset exit 0.
  • check:nul-bytes — exit 0: 6336 text file(s) … no raw ASCII control bytes.
  • check:route-envelope exit 0 · check:dispatcher-error-vocabulary exit 0 ·
    check:error-code-casing exit 0 (the [finding] Every PM dispatch list is short by the same ~5 changeset-triggered gate families — they are path-derivable, but the changeset does not exist yet when the list is derived #10309 family, run explicitly).
  • check:cross-package-test-inputs exit 0 · check:durability-log-level exit 0 ·
    check:entry-guard exit 0 · check:parse-guard exit 0 · check:slot-lookup exit 0 ·
    check:stack-collection-maps exit 0 · check:type-source-resolution exit 0 ·
    check:query-options-erasure exit 0 · check:where-matcher exit 0 · check:i18n exit 0 ·
    check:type-check-coverage exit 0 · check-ci-filter-parity exit 0 ·
    check-affected-docs exit 0.

The derivation named node scripts/check-plugin-teardown-shape.mjs by path and
check:engine-double-contract under its convention-triggered set (adds or edits a test
file
); both were run explicitly regardless, as the #10309 class requires.

scripts/check-single-claim-paths.mjs was not run: without PR_NUMBER it reports
NOT WIRED and with one it needs a token — both are wiring failures, not verdicts.

Not in this PR

Generated by Claude Code

… not stop()
[#10772] `MetadataPlugin.start()` attaches a real `FileSystemRepository`
(armed chokidar watcher + reconciliation sweep), hands it to the
`NodeMetadataManager`, and may attach an artifact file watcher on top. The
teardown that closed all three was spelled `stop = async (ctx) => …`.
`Plugin` (`@objectstack/core`'s `types.ts`) declares `init()`, `start?(ctx)`
and `destroy?()` — and no `stop()` — and `ObjectKernel.performShutdown()` /
`LiteKernel.destroy()` walk the plugins in reverse calling `plugin.destroy()`.
Nothing in the repo calls `stop()` on a plugin, so every one of those handles
was still held after `await kernel.shutdown()` had RESOLVED.
The alias is an arrow PROPERTY, which is why a method-only reading of the
class missed it and #10371's enumeration came out short by five.
Repair is the PR-10375 shape: the body moves into `destroy()`, and `stop()`
stays as a delegating alias — public API of an exported class, and an embedder
may have learned to call it directly precisely BECAUSE the kernel never did.
It stays an arrow property so a detached `const { stop } = plugin` call keeps
working, and its parameter becomes optional and ignored: `destroy()` takes no
context, so teardown logs through the context captured in `init()`.
Baseline: the `MetadataPlugin` line is deleted from `KNOWN_TEARDOWN_UNREACHED`
in `scripts/check-plugin-teardown-shape.mjs`, on that gate's own stale-entry
verdict rather than by line number.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PnJHU45vPJj5UQrxe946Bx
…op()
[#10772] `AppPlugin` emits `app:registered` on the kernel bus at start so the
control plane's `AppCatalogService` can mirror the app into `sys_app`, and it
emitted the matching `app:unregistered` from a teardown spelled
`stop = async (ctx) => …`.
`Plugin` (`@objectstack/core`'s `types.ts`) declares `init()`, `start?(ctx)`
and `destroy?()` — and no `stop()` — and `ObjectKernel.performShutdown()` /
`LiteKernel.destroy()` walk the plugins in reverse calling `plugin.destroy()`.
Nothing in the repo calls `stop()` on a plugin, so `app:unregistered` was
never emitted on a real shutdown and the catalog row outlived the kernel that
registered it.
The alias is an arrow PROPERTY, which is why a method-only reading of the
class missed it.
Repair is the PR-10375 shape: the body moves into `destroy()` and `stop()`
stays as a delegating arrow property, so both a direct call and a detached
`const { stop } = plugin` keep working. `destroy()` takes no context, so
`init()` now captures the one it needs — assigned as init's first statement,
ahead of the empty-env early return, so teardown is armed on every path init
takes.
Baseline: the `AppPlugin` line is deleted from `KNOWN_TEARDOWN_UNREACHED` in
`scripts/check-plugin-teardown-shape.mjs`, on that gate's own stale-entry
verdict rather than by line number.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PnJHU45vPJj5UQrxe946Bx
…oy(), not stop()
[#10772] This is the load-bearing member of the family. The plugin arms one
`setInterval` per opted-in datasource from `scheduleDriftChecks()` at
`kernel:ready` (ADR-0015 §5.2), and the `clearInterval` sweep over them was
spelled `stop = (): void => …`.
`Plugin` (`@objectstack/core`'s `types.ts`) declares `init()`, `start?(ctx)`
and `destroy?()` — and no `stop()` — and `ObjectKernel.performShutdown()` /
`LiteKernel.destroy()` walk the plugins in reverse calling `plugin.destroy()`.
The only caller `stop()` had anywhere in the tree was this class's own
`scheduleDriftChecks()` re-arming itself, so on kernel shutdown those
intervals were never cleared: the #9371 mechanism verbatim, in one of only two
`Plugin` implementations in this tree that own `setInterval` at all
(`ReportsServicePlugin` is the other, repaired under #10371). The plugin is
mounted on the real serve path (`kernel.use(createExternalValidationPlugin())`
in `packages/cli/src/commands/serve.ts`), so the leak is not confined to
tests; the timers are `unref`'d, which is why it stayed silent in a
long-lived host and lands in a vitest worker instead.
Repair is the PR-10375 shape: the body moves into `destroy()` and `stop()`
stays as a delegating alias. It stays a SYNCHRONOUS arrow property returning
`void` — widening a public alias to `Promise<void>` would change what an
embedder's non-awaiting call site does. `scheduleDriftChecks()` now re-arms
through `destroy()`, the canonical hook, leaving the alias purely for
embedders.
Baseline: the `ExternalValidationPlugin` line is deleted from
`KNOWN_TEARDOWN_UNREACHED` in `scripts/check-plugin-teardown-shape.mjs`, on
that gate's own stale-entry verdict rather than by line number.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PnJHU45vPJj5UQrxe946Bx
…dispose()
[#10772] The plugin arms a live `email_template` bridge at `kernel:ready`: a
metadata subscription, a protocol mutation listener, a provenance hook bound
to the data engine, and — when SMTP is configured — an open transport. The
teardown that released all of them was spelled `dispose()`.
`Plugin` (`@objectstack/core`'s `types.ts`) declares `init()`, `start?(ctx)`
and `destroy?()` — and no `dispose()` — and `ObjectKernel.performShutdown()` /
`LiteKernel.destroy()` walk the plugins in reverse calling `plugin.destroy()`.
Measured repo-wide on this revision: `dispose()` had exactly ONE caller
anywhere, a test in this package. The kernel was never one of them, so after
`await kernel.shutdown()` had resolved the bridge was still armed and still
writing `sys_email_template` rows through an engine the suite had moved on
from.
This member and `WebhookOutboxPlugin` are the `dispose()` half of the family
— the seventh spelling the #10619 gate's roster was widened for before any
instance of it was known, already present when the roster was measured. A
census looking only for `stop()` misses both.
Repair is the PR-10375 shape: the body moves into `destroy()` and `dispose()`
stays as a delegating alias with the same signature and return type — public
API of an exported class, and it has a live direct caller in this package.
Baseline: the `EmailServicePlugin` line is deleted from
`KNOWN_TEARDOWN_UNREACHED` in `scripts/check-plugin-teardown-shape.mjs`, on
that gate's own stale-entry verdict rather than by line number.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PnJHU45vPJj5UQrxe946Bx
…not dispose()
[#10772] At `kernel:ready` the plugin binds two hooks onto the data engine and
starts an `AutoEnqueuer`, which holds two realtime subscriptions, a
crypto-provider listener and a `setInterval` refresh timer. The teardown that
released all of it was spelled `dispose()`.
`Plugin` (`@objectstack/core`'s `types.ts`) declares `init()`, `start?(ctx)`
and `destroy?()` — and no `dispose()` — and `ObjectKernel.performShutdown()` /
`LiteKernel.destroy()` walk the plugins in reverse calling `plugin.destroy()`.
Measured repo-wide on this revision: `dispose()` had ZERO callers anywhere —
not the kernel, not a test, not an example — so this teardown had never run in
any process at all.
Repair is the PR-10375 shape: the body moves into `destroy()` and `dispose()`
stays as a delegating alias with the same signature and return type — public
API of an exported class, kept precisely because the zero-caller census is the
argument for deleting it and the wrong one to act on.
`vitest.config.ts` is new here and required rather than incidental: the pin
needs a REAL kernel, which is this package's first VALUE import of
`@objectstack/core` (the plugin's own `import type` is erased before
resolution). `pnpm check:test-source-alias` reds on an unaliased value import
resolving to `core/dist` and names this remedy — alias to source, do not widen
the shrink-only `KNOWN_UNALIASED_TEST_IMPORTS`. The entry is an ANCHORED regex
so it cannot swallow the published `@objectstack/core/logger` subpath, and
`test` is left unset so the package keeps the vitest defaults its bare
`vitest run` was relying on. Full suite after: 124/124.
Baseline: the last entry — `WebhookOutboxPlugin` — is deleted from
`KNOWN_TEARDOWN_UNREACHED`, on that gate's own stale-entry verdict rather than
by line number, leaving the ratchet EMPTY. The header, the do-not-widen
failure text and the verdict line are updated to match: the verdict now reads
its repair card from the surviving entries' own `repair` data instead of
hard-coding `#10371`, which is closed, and says "baseline fully burned down"
when there are none.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PnJHU45vPJj5UQrxe946Bx
Patch, not minor: nothing is removed, no signature narrows, and the `Plugin`
interface is untouched. Every old spelling is retained as a delegating alias
with an identical or WIDENED signature (`stop(ctx)` -> `stop(_ctx?)`), so no
`ADR-0087` conversion-layer marker is owed. The one behavioural difference for
direct callers — the two `stop(ctx)` aliases now log/emit through the context
captured in `init()` rather than the argument — is named in the changeset body
rather than left for a reader to discover.
Committed BEFORE the gate union is run on purpose: `check-adr-0087-registration`
reads changeset content from GIT, not the working tree, so running it against an
uncommitted changeset returns a green that carries no information.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PnJHU45vPJj5UQrxe946Bx
…o new fakes
Both findings came from the gate's own verdict, not from a guess:
x PINNED [delete]/[update]: plugin-shutdown-stops-auto-enqueuer.test.ts
declares 1 engine double whose delete()/update() does not route through
assertEngineDeleteDispatch/assertEngineUpdateDispatch (line 67).
x RETAINED [update]: plugin-shutdown-detaches-template-bridge.test.ts pins 1
engine double that the pinned ledger does not record.
The webhook double is fixed by DELETING the two verbs rather than pinning
them. Nothing on the path under test writes through `update()` or `delete()`,
so they were a double looser than `ObjectQL` serving no call — which is the
shape this gate exists to refuse. Removing them takes the double out of the
population instead of adding a row to a shrink-only ledger. ⛔ The baseline in
`scripts/engine-double-contract.baseline.json` is untouched: that path is
maintainer-only and the gate says so.
The email double already routes through `assertEngineUpdateDispatch` (it is the
same double `email-plugin.template-runtime-write.test.ts` uses); only the
RETAINED ledger had to learn about it, via the prescribed
`node scripts/check-engine-double-contract.mjs --write`. That regeneration
reported "1 added or grown, 0 lost" — new pinned coverage, no pin weakened.
After: `check-engine-double-contract: OK — 372 pinned, 133 in the DEBT ledger,
2 exempt.` plugin-webhooks suite 124/124.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PnJHU45vPJj5UQrxe946Bx
@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 4 package(s): @objectstack/metadata, @objectstack/plugin-email, @objectstack/plugin-webhooks, @objectstack/runtime, touching 7 documentable anchor(s).

13 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:

  • content/docs/api/client-sdk.mdx(via MetadataPlugin (symbol))
  • content/docs/api/index.mdx(via MetadataPlugin (symbol))
  • content/docs/automation/flows.mdx(via MetadataPlugin (symbol))
  • content/docs/kernel/services-checklist.mdx(via AppPlugin (symbol), MetadataPlugin (symbol))
  • content/docs/kernel/services.mdx(via EmailServicePlugin (symbol))
  • content/docs/permissions/authentication.mdx(via AppPlugin (symbol))
  • content/docs/permissions/capabilities.mdx(via AppPlugin (symbol))
  • content/docs/plugins/index.mdx(via AppPlugin (symbol))
  • content/docs/plugins/packages.mdx(via AppPlugin (symbol))
  • content/docs/protocol/kernel/index.mdx(via AppPlugin (symbol))
  • content/docs/protocol/kernel/lifecycle.mdx(via AppPlugin (symbol))
  • content/docs/protocol/kernel/metadata-service.mdx(via MetadataPlugin (symbol))
  • content/docs/protocol/kernel/plugin-spec.mdx(via AppPlugin (symbol))

3 release-owned page(s) also name something this change touched. These are read-only:

  • content/docs/releases/implementation-status.mdx(via MetadataPlugin (symbol))
  • content/docs/releases/v15.mdx(via AppPlugin (symbol))
  • content/docs/releases/v17.mdx(via AppPlugin (symbol))

content/docs/releases/ is RELEASE-OWNED (AGENTS.md "Documentation Guardrails"): release
notes are written centrally at release time, and a code PR that edits them is the exact PR
that guardrail exists to stop. They are still audited — read-only. If one of them is actually
wrong, file an issue or open a dedicated docs-only PR; do not edit it here.

What this run could not see
  • 1 changed file(s) yielded no anchor (packages/plugins/plugin-webhooks/vitest.config.ts) — pages documenting those are invisible to this run
  • 3 name(s) were too generic to anchor anything (single lowercase words)
  • the SDK route bridge reached 45 of 221 client-bound route-ledger rows — the other 176 have no registrar path: tail to select them, so pages documenting THEIR client methods cannot appear above, on this or any run: node scripts/docs-audit/affected-docs.mjs --bridge-coverage

Coarse fallback — 32 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): node scripts/docs-audit/affected-docs.mjs --json 368e7a06f4f68b0eccc3c287ff561f8d38bd2566packageMentionDocs.

Which tree this was computed on

This run read content/docs from 49e91121dbcf7e565f44ae791103a914bd852a98 — the merge of head 019b0673810545199df4d8875ba8670c7b11745e into base 368e7a06f4f68b0eccc3c287ff561f8d38bd2566, which is what actions/checkout gives a pull_request run. Not the PR head.

A worktree cut from an older main holds a different content/docs, so re-deriving there can legitimately return a different list — that is a different tree, not a wrong row. To answer on the same tree:

# while this PR is open — GitHub drops the merge commit once it closes
git fetch origin 49e91121dbcf7e565f44ae791103a914bd852a98 && git checkout 49e91121dbcf7e565f44ae791103a914bd852a98
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 368e7a06f4f68b0eccc3c287ff561f8d38bd2566 019b0673810545199df4d8875ba8670c7b11745e && git checkout -B drift-repro 368e7a06f4f68b0eccc3c287ff561f8d38bd2566 && git merge --no-ff 019b0673810545199df4d8875ba8670c7b11745e
node scripts/docs-audit/affected-docs.mjs --json 368e7a06f4f68b0eccc3c287ff561f8d38bd2566

⚠️ That checkout carried uncommitted changes, so the commit above does not fully identify what was read.

Advisory only, and a precision-first one (#9192): a page is listed because it names a
symbol, wire route or SDK method this diff touched — not because it mentions a changed
package. Each row says which anchor put it there, so a wrong row is reportable rather than
merely annoying. To re-verify, run the docs-accuracy-audit workflow scoped to these files:
node scripts/docs-audit/affected-docs.mjs 368e7a06f4f68b0eccc3c287ff561f8d38bd2566 → pass the list as
args.docs, on the commit named under Which tree this was computed on.

@github-actionsgithub-actionsBot added documentation Improvements or additions to documentation tests tooling labels Aug 22, 2026
@os-warren
os-warren marked this pull request as ready for review August 22, 2026 02:41
@os-warren
os-warren enabled auto-merge August 22, 2026 02:41
@os-warrenClaude

Copy link
Copy Markdown
CollaboratorAuthor

PM review — flipped ready, auto-merge armed. Verified by content, not from the report.

claimhow it was checkedresult
the baseline is deleted, not retaggedread KNOWN_TEARDOWN_UNREACHED on the branch= []packages/-prefixed entries: 0
the self-test fixture survivesgrep "a/src/p.ts"present at :675, untouched; it is the only remaining repair: '#10371' hit — a bare count returns 1, not 0, and that 1 is correct
base was brought current before measuringgit merge-base --is-ancestor origin/main HEADyes — 81845c65a is an ancestor. The branch had been cut 67 commits back
pushedlocal vs remote revboth 019b06738, working tree clean
lane red linesgit diff --name-only origin/main...HEADno content/docs/releases/**, no packages/spec
card trackingPR bodyFixes #10772 — correct; this card's scope is fully discharged

The base-currency check is the one that mattered. #10766 nearly turned main red on this exact file because its base predated the #10619 gate and its baseline entries went stale on landing. This branch was 67 commits behind when picked up; the merge to 81845c65a happened before any measurement, so every number in the body is from a current tree.

Two things I want on the record because they are the kind of thing that usually goes unwritten:

A3 is the ablation most reviews would not have asked for. Deleting the retained stop alias while keeping destroy() left the gate green at 0 known-unreached — so the gate alone cannot see an alias deletion, and the alias assertions are the only thing pinning it. That is a limit of the instrument, found and published rather than glossed.

The setInterval census was reported as a fact count, not a grep count — 3 textual hits in external-validation-plugin.ts, exactly 1 armed-timer site; the rest are a type annotation and two prose mentions. This lane got that distinction wrong four times today. It was right here.

Also carried correctly: WebhookOutboxPlugin owns its setIntervaltransitively via the AutoEnqueuer it constructs, so a text scan of the class cannot see it — which is why its pin asserts the live timer count directly rather than trusting the census.

⛔ Not merged by me and not merged red: auto-merge fires only on green. If CI comes back red, this PR is back in the drive-to-green loop, not waiting on review.


Generated by Claude Code

@os-warren
os-warren added this pull request to the merge queueAug 22, 2026
Merged via the queue into main with commit 047ac86Aug 22, 2026
35 checks passed
@os-warren
os-warren deleted the claude/issue-10772-teardown-five-more branch August 22, 2026 02:59
@github-actions

Copy link
Copy Markdown
Contributor

⛔ merge queue 构建失败 — 先分诊,再决定要不要重排

队列构建 32547121662 红了。队列跑的是全量套件(PR 侧 CI 只跑 affected 子集),
所以失败的测试可能在本 PR 没碰过的包里 —— 那不是重排能修的。每次盲目重排都会让排在后面的所有 PR 重建一轮。

失败的 job(日志抽取,best effort):

  • Console Pin Gate — 失败步骤: Build the Console SPA at the pinned objectui SHA

    ✗ Build failed in 5.57s
    

↳ 失败原因 是判读的关键:超时Test timed out in … / Hook timed out in …)多半是负载/时序,不是本 PR 的回归;
断言AssertionError: …)才指向真实的行为改变。两者的 FAIL 行长得一模一样,只有这一行能区分。

跨 PR 相同签名(24h,按失败测试文件聚合):

  • ⚠️本次没有可用的聚合签名(日志里没有能解析出测试文件名的 FAIL 行)—— 这不是「没有同签名的其他 PR」,是这一轮没测到。跨 PR 聚合本次不可用,请手工比对其他 PR 的同类评论。
  • ⚠️ 24h 评论账本没读完(超过 5 页仍未读到窗口尽头),所以上面的「不同 PR 数」是下界,不是全量。

历史信号:

  • 本 PR 过去 24h 无队列失败记录(首次)。
  • 过去 24h 队列共有 67 个失败构建(不含本次)。

分诊清单:

  1. 失败测试在本 PR 改动的包里 → 真回归,修 PR。
  2. 失败测试与本 PR 无关 → 看上面的「跨 PR 相同签名」;已有汇总 issue ⇒ flaky/环境问题实锤,去那张 issue 上谈,修好前重排只会再烧一轮全队列。
  3. 两者都不是 → 可能与同组 PR 语义冲突;等前面的 PR 落地或失败出队后再重排一次即可,不要连续重排。

Generated by Claude Code · merge-queue-triage workflow (#4859)

Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentationImprovements or additions to documentationsize/xlteststooling

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Five more Plugin implementations release their teardown from a hook the kernel never calls — the #10371 enumeration is short by five

2 participants

@os-warren@claude