Skip to content

fix: refuse emit after the collection seal has greened (§11.1, F6)#17

Merged
alexattd merged 1 commit into
mainfrom
ship/18f428
Jul 4, 2026
Merged

fix: refuse emit after the collection seal has greened (§11.1, F6)#17
alexattd merged 1 commit into
mainfrom
ship/18f428

Conversation

@alexattd

@alexattd alexattd commented Jul 4, 2026

Copy link
Copy Markdown
Contributor

Summary

Workflow wf_18f42839277d59336785e6cf / run run_fcdb7452fb74a0536bed6f1a.

  • Engine.emit() (src/engine.ts) never checked the seal's acceptance, so a
    run holding a still-open lease could keep emitting members into a
    collection whose seal was already green — i.e. a "complete" set (§11.1)
    that silently grows. Downstream self-heals via the forward cascade, but any
    consumer that already acted on the sealed set acted on stale data, and
    repeated late emits churn rebuilds for nothing.
  • emit() now checks isGreen(seal) right after the existing born-rejected
    CAS check (same refusal precedence, unchanged) and, if the seal is green,
    returns a new structural outcome 'sealed-rejected' — no counters bumped,
    no artifacts touched, lease stays open so the run can still close.
    EmitResult's outcome union gains 'sealed-rejected' as an additive
    sibling to 'emitted' | 'born-rejected' | 'schema-rejected'.
  • No CLI change needed: the emit case in cli.ts already treats any
    outcome other than 'emitted' as a non-zero-exit refusal via a catch-all
    (if (emitRes.outcome !== 'emitted') { ... return 1; }), so
    'sealed-rejected' surfaces correctly for free.
  • No model.ts change needed: the checker's eligibleFirings only
    offers a collection-producer firing when its seal is a debt
    (isDebt(seal) && ...), and isDebt/isGreen are disjoint by
    construction — the checker can never generate an emit-after-green-seal
    transition in the first place, so checker and runtime already agree.
  • docs/design.md §11.1 gets one line documenting the refusal.

Test plan

  • New engine test: late emit on the same open lease after seal
    returns 'sealed-rejected', creates no new members, leaves the seal's
    version/counters untouched, and the run still close()s cleanly.
  • New CLI test: same scenario via the CLI, asserting non-zero exit and
    outcome === 'sealed-rejected' on stderr, mirroring the existing
    emit: schema-rejected CLI test.
  • Existing emit tests (CAS staleness born-reject, atomic schema refusal,
    accretion order) unchanged and still pass.
  • npm run check (typecheck + build + full test suite, 611 tests) green.

🤖 Generated with Claude Code

Engine.emit() never checked the seal's acceptance, so a run holding a
still-open lease could keep emitting members into a collection whose
seal was already green — i.e. a "complete" set that silently grows.
Downstream self-heals via the forward cascade, but any consumer that
already acted on the sealed set acted on stale data, and repeated late
emits churn rebuilds for nothing.

emit() now checks isGreen(seal) right after the born-rejected CAS check
(same precedence as before) and, if green, returns a new structural
outcome 'sealed-rejected' with no counters bumped and no artifacts
touched — the lease stays open so the run can still close. EmitResult's
outcome union gains 'sealed-rejected' as an additive sibling to
'emitted' | 'born-rejected' | 'schema-rejected'.

No CLI change needed: cli.ts's emit handler already treats any outcome
other than 'emitted' as a non-zero-exit refusal via a catch-all, so
'sealed-rejected' surfaces correctly for free.

No model.ts change needed either: the checker's eligibleFirings only
offers a collection producer firing when its seal is a debt, and
isDebt/isGreen are disjoint by construction — so the checker can never
generate an emit-after-green-seal transition in the first place.

Tests: engine-level late-emit-after-seal (sealed-rejected, no new
members, seal untouched, run still closes) and a CLI-level late-emit
non-zero-exit test mirroring the existing schema-rejected CLI test.
design.md §11.1 gets one line documenting the refusal.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@alexattd

alexattd commented Jul 4, 2026

Copy link
Copy Markdown
Contributor Author

Reviewer verdict: APPROVED (risk: low)

Reviewed adversarially against plan wf_18f42839277d59336785e6cf.

  • Refusal ordering (born-rejected → sealed-rejected → schema check) matches plan exactly — verified in src/engine.ts.
  • New branch mutates nothing (no counters bumped, no artifacts touched) and correctly skips settle().
  • Verified in source, not just claimed: CLI catch-all (cli.ts:641, if (emitRes.outcome !== 'emitted')) and model.ts isDebt/isGreen disjointness (model.ts:151-156, DEBT_STATES membership vs. exact 'green') — both hold, so no CLI or model.ts change was needed, as the PR states.
  • Tests assert no counter/version drift, no new member artifacts, seal still green, lease stays open, run still closes cleanly.
  • docs/design.md §11.1 update matches plan wording.
  • CI all green (CLA, check, node 22, node 24).
  • No unrelated drive-bys.

(Self-approval via gh pr review --approve is blocked by GitHub since I authored/reviewed as the same account — posting this as the recorded approving review per workflow policy.)

@alexattd alexattd merged commit cd963cf into main Jul 4, 2026
4 checks passed
@alexattd alexattd deleted the ship/18f428 branch July 4, 2026 18:17
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants