Repository navigation
fix!: enforce task counter, finalization, and parent invariants - #66
Merged
stromseng merged 2 commits intoSep 6, 2026
Merged
Conversation
stromseng
force-pushed
the
task-contracts/state-invariants
branch
from
September 6, 2026 16:35
a1f5dd5 to
e60e9b3
Compare
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.
Problem
Several mutation paths allow task state to disagree with its intended contract:
NaNand infinity, including arithmetic overflow from finite inputs.Solution
Enforce the rules at the store mutation boundary:
parentId: nullwhen that parent does not exist, so snapshot identity, inherited policies, and root render order agree.Document these policies on the public contracts and in the README. Metadata objects are not deep-frozen; the terminal-state rule applies to task API mutations.
Examples
Invalid counters
Starting from succeeded=2 and failed=1,
incrementSucceeded(NaN)preserves those counters. An update{ succeeded: Infinity, failed: 3 }preserves succeeded=2 and accepts failed=3. If two finite counters would sum to infinity, neither counter change is applied. Finite counts such as 5 processed against a total of 2 remain supported.Finalized handles
After
yield* handle.completeoryield* handle.fail, laterupdate, counter increments, metadata setters/updaters, and finalization calls do nothing. For retained tasks,getSnapshotandgetMetadatastill returnSomewith the final state. For transient tasks they returnNone, as established in the previous layer.Missing parents
A task created with a removed parent's ID becomes a root:
parentIdis null, render depth is zero, and it does not inherit the removed parent's transient or count-display settings. A valid existing parent retains the normal nesting behavior.Validation
bun test: 146 passed, 0 failed, 444 assertions.bun run check: typecheck, lint, formatting, and Knip passed.bun run formatapplied; staged diff whitespace check passed.New tests exercise NaN and both infinities, overflow of an individual counter and the combined sum, preserved finite overflow/negative clamping, missing and removed parents, self-parent prevention, and all handle mutations after both completion and failure. Existing tests retain coverage of finalization accounting, valid nesting, publication, and transient cleanup. Non-failing TSGO schema suggestions remain. No build or dev server was run.
Compatibility and review
This intentionally stops post-finalization updates, rejects non-finite counter state, and normalizes missing parent references. Negative/non-finite total handling remains unchanged. Review
modifyTaskfirst for the terminal-state guard, then numeric normalization and parent creation. This is the top layer of the task-contract stack.Stack
GitHub stack #67:
main→ #64 (handle metadata API) → #65 (optional reads) → #66 (state invariants). Each PR is based on the layer below it. Manage the chain withgh stack.