feat(#89): reactive recalc (stage 2/3) — cone walk, replay, alias re-point + surfaces - #128
Merged
Conversation
Cone-walk recompute (tallyman_xorq.recalc) and its MCP/companion surfaces, test-first. Red until the implementation lands: test_recalc imports the absent recalc module, test_recalc_surfaces imports the not-yet-added catalog_recalc / catalog_scan_staleness tools and /api/recalc, /api/staleness routes. Covers: dry-run preview builds nothing; a source-drifted root rebuilds and the cone re-points; a followed-alias advance cascades to its follower; pinned children hold; nothing-stale is a no-op; a build failure stops the walk with the prefix committed; a cycle is reported not hung; the surfaces are read-only/one- revision as designed. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…aces Stage 2/3 of the reactive consumer. Stage 1 (staleness detection, PR #126) flags an entry as stale relative to its recorded inputs but never acts; this is the half that acts. recalc.py: recalc(project, roots, *, dry_run) walks the roots' topologically ordered descendant cone and replays each entry's recipe through build_and_persist. A followed parent re-resolves to the advanced alias head (get_alias), so re-pointing a parent's alias before its children replay propagates the change down the cone; an unchanged input replays to the same content_hash and build_and_persist early-returns, so the walk is idempotent and a hash-pinned child is a no-op. Cascade-failure policy is stop-and-report: a build error halts the walk, leaves the rebuilt prefix committed, and is flagged in the report rather than raised. One checkpoint per walk (only when something changed), so a recalc is a single revision reset_to undoes atomically. Surfaces (thin; logic stays in recalc.py / staleness.py): - MCP: catalog_scan_staleness (read-only) and catalog_recalc (dry_run defaults true). Both in _NO_CHECKPOINT — recalc self-checkpoints, arg-aware. - Companion: GET /{project}/api/staleness (scan-on-load) and POST /{project}/api/recalc (preview + commit), the recalc path exempt from the checkpoint middleware so the op is exactly one revision. plans/recalc-mechanism.md documents the mechanism, the idempotence argument, the cheap-chain source-leak nuance, and the decision-gate defaults (stop-and-report; scan-on-load + explicit recalc; off-mode=unknown; pin stays). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
paddymul
added a commit
that referenced
this pull request
Jun 22, 2026
…lc SSE shape, guard manifest-less roots (#128) Three review findings on PR #128: 1. /internal/notify now handles kind="recalc" like project_reset — invalidate the result/compare LRUs and reload buckaroo sessions. An MCP-driven recalc (the primary path) arrives via _notify, so without this the companion kept serving stale viewer state that only the in-process /api/recalc route cleaned up (#80's communication-gap class). 2. Both recalc emitters publish through a single _recalc_sse_event(remap, step) helper, so the SSE event is the same {kind, remap, step} shape regardless of surface. The MCP _notify now forwards report.checkpoint_step too; the notify handler republishes normalized instead of the raw payload.model_dump() that buried remap under extra and dropped step. 3. recalc()'s root guard checks the manifest, not the directory. verdicts (and the cone) are keyed by manifest-bearing entries, so a dir-without-manifest root (a crashed mid-build leftover) used to enter the cone and KeyError the walk at verdicts[hash]; it is now dropped and the run reports clean. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
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.
The second landable increment of the reactive consumer (epic #89): the half
that acts. Stage 1 (PR #126) flags an entry as stale relative to its recorded
inputs but never recomputes; this walks the dependency cone and rebuilds it. The
hard prerequisites it rested on are all merged — #115 (cold-read content
faithfulness), #124 (#80/#96 LRU invalidation on reset), #121 (#88 determinism
attribution).
Design + mechanism:
plans/recalc-mechanism.md(new), againstplans/reactive-staleness-recalc.md(Stage 2/3) and the ADR (#51).What lands
tallyman_xorq/recalc.py—recalc(project, roots, *, dry_run)walks theroots' topologically ordered
descendant_coneand replays each entry's recipethrough
build_and_persist(the primitivecatalog_revisealready uses). Afollowed parent re-resolves to the advanced alias head (
io.from_catalog→get_alias), so re-pointing a parent's alias before its children replaypropagates the change down the cone. The walk is idempotent: an unchanged input
replays to the same
content_hashandbuild_and_persistearly-returns, so anuntouched node — and a hash-pinned (
follow=False) child, whose literal-hashrecipe re-resolves to the same parent — is a no-op with no alias move.
catalog_scan_staleness(read-only) andcatalog_recalc(
dry_rundefaults true;roots=Nonedefaults to the directly-stale set).GET /{project}/api/staleness(the scan-on-loadsurface) and
POST /{project}/api/recalc(preview + commit), which invalidatesthe companion caches, reloads buckaroo sessions, and publishes a
recalcevent.Cascade failure + transaction
Stop-and-report (the plan's default): a build error halts the walk, leaves the
already-rebuilt prefix committed, and is flagged in the report rather than raised.
One checkpoint per walk (only when something changed), so a recalc is a single
revision
reset_toundoes atomically. Checkpointing is arg-aware —recalcself-checkpoints and both surfaces are exempt from their generic per-op checkpoint
— so a dry run takes zero revisions and a real run exactly one (neither
double-commits nor misses).
TDD
Red→green. First commit is the failing tests (the
recalcmodule and the newtools/routes are absent, so collection errors); the fix commit follows. Full
suite green locally: 566 passed.
Decision defaults exercised (confirm)
auto-cascade-on-revise.
unknown(Stage 1).Note: the cheap-chain source leak
A cheap child inlines its parent's recipe, so the parent's source is recorded in
the child's own
manifest.sources. A source edit therefore makes such a childdirectly stale, not merely transitively. Expected (the #74 reconstruction
model); recalc rebuilds it either way. The pure transitive case shows up across an
expensive, snapshot-baked intermediate. Tests cover both.
Not in this PR
The React badge/affordance rendering (the backend
/api/stalenessdata + entryannotation are ready for it), and deletion-side semantics (archive / delete cone /
multi-parent unbind — ADR Q3/Q4).
🤖 Generated with Claude Code