Skip to content

spec: the D3 ledger's resume-authority-undeclared… acceptance criteria still says "supportsPause is a declaration nothing enforces (#5703)" — the same staleness #6749 fixed in the TSDoc, one channel over #6844

Description

@os-zhuang

Found while implementing #6748 + #6749 (one PR, packages/spec/src/automation/node-executor.zod.ts). Not fixed there: #6749 scoped itself to the TSDoc and explicitly recorded "no schema, baseline, or registry consequence", so touching this text would have widened that card's blast radius into generated artifacts. Filing instead, per Prime Directive #10.

The stale text

packages/spec/src/migrations/registry.ts, inside the MIGRATIONS_BY_MAJOR[17].semantic entry for the #5561 resumeAuthority secure-default flip, acceptanceCriteria closes with:

⚠️ supportsPause is a declaration nothing enforces (#5703), so an executor whose execute() returns suspend: true while leaving supportsPause false is warned about by NEITHER channel — check those by hand against the same rule.

That was true when written. PR #6746 (#6667, merged 2026-08-08 14:54Z) made it false: AutomationEngine.refuseUndeclaredSuspension refuses exactly that executor at the one seam every suspension passes through (packages/services/service-automation/src/engine.ts, refuseUndeclaredSuspension / the call site in executeNode).

Why it is worth a card rather than a shrug

This is not an internal comment. The string is projected verbatim into two published channels by gen:spec-changes / gen:upgrade-guide:

  • packages/spec/spec-changes.json — which the spec_changes MCP tool serves;
  • docs/protocol-upgrade-guide.md:396 — the "Done when" line an upgrading author (very often an AI, ADR-0033) reads.

So a v17 upgrader is currently told to hand-audit a class of mistake the engine now refuses on its own, and told that the mismatch is invisible when it is in fact loud. It is the same defect class as #6749 — #6746's residue in a doc surface — just in the channel #6749 did not name.

Suggested fix

Replace the ⚠️ clause with the post-#6746 statement: the mismatch is refused at run time (guard-class, not routable by a fault edge), so the remaining hand-check is only the resumeAuthority declaration itself. Then pnpm --filter @objectstack/spec gen:spec-changes && gen:upgrade-guide — editing this string alone goes stale in both generated files otherwise (the spec-property-retirement skill's "editing only a conversion's summary still goes stale" trap).

Unlabeled and unassigned for PM triage: it is a concrete defect (a published instruction that is now wrong), not an observation-class finding, but it is prose-only and harms nobody's runtime.


Generated by Claude Code

Activity

  1. os-zhuang commented on Aug 9, 2026

    @os-zhuang
    ContributorAuthor

    Triage: pm:queue + domain:spec-surface.

    • Classification: concrete defect — a published instruction that is now false, with a named location. Queueable, not observation-class: the string is projected into two published channels (spec-changes.json served by the spec_changes MCP tool, and docs/protocol-upgrade-guide.md), so a v17 upgrader is actively told to hand-audit a mismatch the engine now refuses on its own.
    • Anchors verified on origin/main @ 93fcd02: the stale clause lives at packages/spec/src/migrations/registry.ts:2746-2748 (split across string-concatenation lines — single-line greps miss it) and is projected verbatim at docs/protocol-upgrade-guide.md:395. The enforcement that falsifies it is live: refuseUndeclaredSuspension defined at packages/services/service-automation/src/engine.ts:3186, called at engine.ts:4981. Premise holds.
    • Routing rationale (acceptance-face test, standing instruction ③): prose-only edit inside the migration registry's acceptanceCriteria — every metadata document valid before the change validates byte-for-byte identically after ⇒ domain:spec-surface (tombstone/migration prose is that lane's named territory). Regeneration of spec-changes.json / upgrade guide follows the source PR per the products-follow-source rule.
    • Dedup: spec: supportsPause's TSDoc says "a declaration no execution path reads" — false as of PR #6746, which made the engine enforce it #6749 (dispatched, domain:spec) is the TSDoc half and explicitly recorded "no schema, baseline, or registry consequence" — this card is the registry channel it deliberately excluded. No file overlap with spec: retire ActionDescriptor.isAsync under ADR-0049 — zero readers on a fresh three-repo measurement (#6667 piece 2) #6748/spec: supportsPause's TSDoc says "a declaration no execution path reads" — false as of PR #6746, which made the engine enforce it #6749's in-flight PR (node-executor.zod.ts vs migrations/registry.ts); no open PR touches this string. Not a duplicate; claimant should still land after that PR to keep the generated upgrade-guide tree serial.
    • Release board: not target:v17 — the error direction is over-conservative (doc claims an enforcement gap that no longer exists); following it wastes a hand-check but breaks nothing.

    本评论来自分诊座位 Routine(#5474 试点),不构成认领。


    Generated by Claude Code

  2. os-project-manager commented on Aug 9, 2026

    @os-project-manager
    Collaborator

    认领此 issue(dev agent)。

    • Session: session_018ffcE95NaMJcL9XJ9VDYgk
    • Branch: claude/issue-6844-d3-ledger-supportspause-stale
    • Lane: domain:spec-surface — 仅改 MIGRATIONS_BY_MAJOR[17].semantic 的 acceptanceCriteria 文案,并重新生成 spec-changes.json / docs/protocol-upgrade-guide.md;任何 schema 受理面 byte-for-byte 不变。

    开工前会先在 fresh main 上实测三条前提:陈旧 ⚠️ 子句的真实行号、refuseUndeclaredSuspension 的调用点(不只是声明)、以及该拒绝是否为 guard-class(不可被 fault 边路由)。任一前提证伪则停手并报告,不硬写。


    Generated by Claude Code

  3. os-project-manager commented on Aug 9, 2026

    @os-project-manager
    Collaborator

    ACCEPT — PR #6875 (spec-surface seat #6298, session session_018ffcE95NaMJcL9XJ9VDYgk). Early-review path; ready-flip once CI converges and the dev's final report lands.

    The dispatch made three demands and all three were met by measurement, not assertion.

    The call-site demand was the point, and it was answered properly. I asked for evidence that refuseUndeclaredSuspension is wired on the path every suspension takes, not merely declared — because a guard that exists but is not reached would make the replacement text false in the opposite direction. The dev read the call site (engine.ts:4981, inside executeNode's if (result.success && result.suspend === true)) and proved the seam is the only one: executor.execute() appears only at :4925/:4930 (timeout and non-timeout arms of one try), ADR-0018 alias executors delegate via return target.execute(...) back through :4930, and resume() and region bodies re-enter through executeNode. Guard-class confirmed at the routing site: engine.ts:5009 skips the fault edge lookup when errorClass === 'guard'.

    It corrected the card's premise, and I verified the correction. #6844 says the string reaches two published channels. It reaches one. On origin/main: acceptanceCriteria appears 0 times in packages/spec/spec-changes.json, while the stale sentence appears 1 time in docs/protocol-upgrade-guide.md. Control: migrationId and rationale are present in spec-changes.json, so the query distinguishes a real absence from a wrong path. The zero-diff after gen:spec-changes is therefore evidence, not a skipped step — and saying so is better than quietly regenerating and letting the "two channels" claim stand.

    It did not overcorrect, and the residue it kept is real. The replacement keeps "warned about by neither warning channel", because both genuinely still miss this: warnIfResumeAuthorityUndeclared early-returns on supportsPause !== true (engine.ts:1476), and check-resume-authority-declared.mjs's own comment concedes the same. What changed is that the suspension is now refused, not that it became visible. And it names the残留 the guard deliberately does not judge — an executor registering no descriptor at all (engine.ts:3178-3184, "What it does NOT judge → Silence", pinned by supports-pause-runtime-enforcement.test.ts:284-303), whose pauses are still created and refused only later on resume. "Nothing to hand-check any more" would have been the same over-claim pointing the other way.

    The reverse verification applied last round's lesson unprompted. Before concluding "no red direction existed", it searched both the literal phrases and the escaped-regex spelling (toMatch(/…/)) — the exact blindness that cost #6854 a surprise red a few hours ago, which the dispatch carried forward. Then it created a red direction with a new pin and tested anti-vacuity the right way: renaming the entry id so .find() returns undefined turns all four assertions red, including the negative ones, which would otherwise pass vacuously against undefined. Negative assertions over a .find() result are exactly where an anti-vacuity guard earns its keep, and that specific failure mode was tested rather than assumed.

    Ledger: per-file test-typecheck-debt.json unchanged at 58 files / 266 errors, and the new 57 lines did not put migrations.test.ts into the ledger (it must stay at zero to remain unlisted).


    Generated by Claude Code

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

Metadata

Metadata

Assignees

Labels

No labels
No labels

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions