Skip to content

fix!: enforce task counter, finalization, and parent invariants - #66

Merged
stromseng merged 2 commits into
task-contracts/optional-readsfrom
task-contracts/state-invariants
Sep 6, 2026
Merged

stromseng merged 2 commits into
task-contracts/optional-readsfrom
task-contracts/state-invariants

Conversation

@stromseng

@stromseng stromseng commented Sep 6, 2026 •

Copy link
Copy Markdown
Owner

Problem

Several mutation paths allow task state to disagree with its intended contract:

  • Totals reject non-finite values, but counters accepted NaN and infinity, including arithmetic overflow from finite inputs.
  • Finalization prevents another status transition, but later counter, field, and metadata updates still change retained completed/failed tasks.
  • A missing parent ID is preserved in the task snapshot even though tree insertion displays it as a root. A caller can even supply the next allocated ID and create a self-parent reference.

Solution

Enforce the rules at the store mutation boundary:

  • Ignore all mutations of finalized or removed tasks before invoking the transform. This also prevents metadata updater callbacks, progress samples, timestamps, and publication state from changing after finalization.
  • Preserve a counter's previous value when its proposed value is non-finite. Clamp finite negative values to zero, retaining the existing finite-overflow-beyond-total behavior.
  • If the succeeded-plus-failed sum would overflow to infinity, retain both previous counters. Valid non-counter fields in an update can still change.
  • Resolve the parent from current store state within task creation's atomic transition. Store parentId: null when 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.complete or yield* handle.fail, later update, counter increments, metadata setters/updaters, and finalization calls do nothing. For retained tasks, getSnapshot and getMetadata still return Some with the final state. For transient tasks they return None, as established in the previous layer.

Missing parents

A task created with a removed parent's ID becomes a root: parentId is 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 format applied; 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 modifyTask first 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 with gh stack.

@stromseng stromseng changed the title task contracts/state invariants fix!: enforce task counter, finalization, and parent invariants Sep 6, 2026
@stromseng
stromseng force-pushed the task-contracts/state-invariants branch from a1f5dd5 to e60e9b3 Compare September 6, 2026 16:35
@stromseng
stromseng merged commit b414bfe into main Sep 6, 2026
2 checks passed
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.

1 participant