fix: refuse emit after the collection seal has greened (§11.1, F6)#17
Merged
Conversation
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>
Contributor
Author
|
Reviewer verdict: APPROVED (risk: low) Reviewed adversarially against plan wf_18f42839277d59336785e6cf.
(Self-approval via |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Workflow
wf_18f42839277d59336785e6cf/ runrun_fcdb7452fb74a0536bed6f1a.Engine.emit()(src/engine.ts) never checked the seal's acceptance, so arun 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 checksisGreen(seal)right after the existing born-rejectedCAS 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 additivesibling to
'emitted' | 'born-rejected' | 'schema-rejected'.emitcase incli.tsalready treats anyoutcome 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.model.tschange needed: the checker'seligibleFiringsonlyoffers a collection-producer firing when its seal is a debt
(
isDebt(seal) && ...), andisDebt/isGreenare disjoint byconstruction — 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
emiton the same open lease aftersealreturns
'sealed-rejected', creates no new members, leaves the seal'sversion/counters untouched, and the run still
close()s cleanly.outcome === 'sealed-rejected'on stderr, mirroring the existingemit: schema-rejectedCLI test.accretion order) unchanged and still pass.
npm run check(typecheck + build + full test suite, 611 tests) green.🤖 Generated with Claude Code