Skip to content

Commit 0eefcb0

Browse files
paddymulclaude
andcommitted
docs: staged plan — catalog coexistence, perf lifecycle, reactive recalc
Sequences the reactive-catalog work into three gated stages: - Stage 1: close the tallyman/xorq catalog coexistence cluster (#48, #52; #49 deprioritized for single-user/--no-sync), rebuild-from-scratch script doubling as the stage-2 corpus generator, end-to-end demo-walk invariant. - Stage 2: reframed around the expression lifecycle (build/load/execute counts as the explanatory variable, cache key strategy second-order); consolidate perf instrumentation behind configurable log levels; two-tier page-load harness (Playwright ground truth + python proxy, calibrated); full/truncated parking corpora. The harness is the instrument approaches are compared with. - Stage 3: reactive design gated on stage-2 numbers; tagged-read spine landable early. Open: stage-1 bookkeeping-file location (recommend sibling tallyman_state repo). Relates to #48, #52; builds on the reactive-catalog ADR. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent 412e50e commit 0eefcb0

1 file changed

Lines changed: 258 additions & 0 deletions

File tree

Lines changed: 258 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,258 @@
1+
# Plan: three-stage path to reactive catalog recalc
2+
3+
Status: **proposed (2026-06-14).** Synthesises the decisions from the planning
4+
conversation around the reactive-catalog ADR (`plans/adr-reactive-catalog-recalc.md`).
5+
Splits the work into three sequenced stages. No implementation here.
6+
7+
The throughline: don't build the reactive layer on machinery that doesn't yet
8+
hold its own invariants, and don't redesign the caching until it's measured. So
9+
stage 1 makes the existing catalog machinery correct and tested, stage 2 maps and
10+
measures the caching, and stage 3 builds reactivity on top — using stage 2's
11+
numbers to settle the ADR's open design questions.
12+
13+
Operating constraints (stated 2026-06-14): single user, one active parking
14+
catalog, everything `--no-sync`. All perf work runs on this Mac alone; no
15+
distributed or CI perf runs. These narrow the scope in places noted below.
16+
17+
---
18+
19+
## Stage 1 — catalog ↔ xorq coexistence, correct and tested
20+
21+
The existing machinery has a cluster of bugs where tallyman and the xorq catalog
22+
disagree about what is durable. Stage 1 closes that cluster and proves it closed,
23+
then produces a clean parking catalog for stage 2 to measure.
24+
25+
### The bug cluster
26+
27+
- **#48** — tallyman commits `aliases.json` / `alias_history.json` into the xorq
28+
catalog repo → `assert_consistency` fails → every `xorq catalog add` after the
29+
first silently no-ops. 17/18 recipes never reached git.
30+
- **#52**`reset_to` reconciles the untracked tallyman artifacts (entry dirs,
31+
result/compute cache) against the pointer lists via the bullpen, but the
32+
git-tracked xorq `.zip` recipes are reconciled separately by `git reset --hard`,
33+
with no cross-view consistency check. A back→forward reset restores an entry the
34+
durable catalog cannot reproduce and reports success. Compounded by #48 today,
35+
but independent: any failed/interrupted `add` (which `checkpoint_catalog` already
36+
anticipates, `catalog_state.py:388-390`) triggers it after #48 is fixed.
37+
- **#49** — authored state (charts, post_processing, …) stored as extra
38+
`catalog.yaml` keys is dropped when xorq rebuilds `catalog.yaml` from
39+
entries+aliases on a merge. **Deprioritised out of stage 1's critical path:**
40+
latent only under multi-user `pull`/sync, which does not happen single-user /
41+
`--no-sync`. Stays filed; not fixed this stage.
42+
43+
### Work
44+
45+
1. **#48 fix.** Move `aliases.json` / `alias_history.json` (and any other non-xorq
46+
file the checkpoint commits — audit the full set written under
47+
`catalog_dir`, not just the two named) out of the xorq catalog repo.
48+
- **Open decision — where they go.** *Recommendation: a sibling
49+
`<project>/artifacts/tallyman_state/` git repo, committed in lockstep with
50+
the xorq repo by the same `checkpoint_catalog` (matching step tags), and
51+
reset by the same `reset_to`.* This keeps the xorq repo holding only what
52+
`assert_consistency` expects, while preserving the free `git reset --hard`
53+
rollback of alias state — so `test_reset_rolls_back_alias_state` stays green
54+
via git rather than bespoke reconciliation. The alternative (files outside
55+
any repo + explicit pointer reconciliation in `reset_to`) is simpler to
56+
write but reintroduces the manual-reconciliation surface #52 is about. Needs
57+
ratification before coding.
58+
- The deferred `test_reset_rolls_back_alias_state` guard (PR #50) ships *with*
59+
this fix — moving the files changes reset's rollback behaviour, so the guard
60+
must travel with the change per the TDD rule.
61+
62+
2. **#52 fix.** Add an explicit cross-view reconciliation + assertion at the end of
63+
`reset_to`: compare the tracked `entries/*.zip` set against `entry_hashes`,
64+
re-register (or fail loudly on) any pointer whose recipe is missing, and assert
65+
`Catalog.from_kwargs(init=False)` opens. Turns the silent divergence observable.
66+
67+
3. **#48 secondary — surface the silent swallow.** `add_entry` is best-effort and
68+
discards its result (`xorq_catalog.py:63-68`, `build.py`); expose a
69+
`BuildResult.catalog_registered`-style signal, log failures at ERROR, and emit a
70+
one-time companion warning. PR #50's Group D pins this.
71+
72+
4. **Reproducible parking-catalog rebuild script.** No migration/repair of the
73+
existing broken catalog — rebuild from scratch through the *fixed* machinery.
74+
One script drives the parking sources through `catalog_create`/`catalog_revise`
75+
to produce the canonical catalog, and emits both corpus variants for stage 2
76+
(full ≈10M+ rows, truncated ≈1M). This replaces "repair" on the task list and
77+
doubles as the stage-2 corpus generator. Depends on #48 + #52 being fixed first
78+
(otherwise the rebuild is born broken again).
79+
80+
### Tests / acceptance
81+
82+
- PR #50's failing integration tests (Groups A/B/D) go green.
83+
- One end-to-end demo-walk invariant test: genesis → multiple creates → revise →
84+
reset back → forward → back, asserting at every step that the catalog opens,
85+
every recipe is durable as a tracked `.zip`, alias state is correct, and (where
86+
in scope) authored state survives.
87+
- TDD split per house rule: failing tests (PR #50) seen red on CI first, then the
88+
fixes; the alias-rollback guard ships with the fix commit.
89+
90+
---
91+
92+
## Stage 2 — model the expression lifecycle, then measure it
93+
94+
Premise (corrected 2026-06-14): app performance is dictated by **how many
95+
expressions have to be built, loaded, and re-executed, and when** — not by the
96+
cache *key* strategy (Snapshot vs ModificationTime), which experimentation so far
97+
shows is second-order. So stage 2 is reframed around the expression lifecycle, not
98+
the cache design.
99+
100+
The xorq question is pragmatic, not a correctness audit. Ignore "are you using it
101+
right" — the only question that matters is the ADR's guiding principle: where xorq
102+
can do what we need *and is fast enough*, lean on it and write no code; where it
103+
can't, or is too slow for interactive use, build our own. Each capability gets a
104+
native-or-build verdict backed by a number, not an opinion.
105+
106+
### Deliverable 0 — consolidate perf instrumentation, make noise configurable
107+
108+
Across tallyman and buckaroo, perf/cache-stat logging has been added and then
109+
turned back off in churn (buckaroo `19abae89` "log cache stats", `72bcf2ad`
110+
bk-flash cross-stream tracing + "console cleanup", and similar on/off cycles).
111+
Stop the churn: **leave the instrumentation in permanently and gate its noisiness
112+
the normal way** — log levels and namespaces, not commenting lines in and out.
113+
114+
The plumbing already exists and is partly used correctly: `TALLYMAN_LOG_LEVEL`
115+
(`src/tallyman_mcp/server.py:1383`), namespaced loggers (`tallyman.companion`,
116+
`tallyman.buckaroo`, `tallyman_mcp`), and instrumentation already level-gated —
117+
the warmup timing in `app.py:411-444` (`log.debug` per entry, `log.info`
118+
summary), `execute_seconds` in `build.py:335-341`. The work:
119+
120+
- Recover the toggled-off perf/cache-stat logs and reinstate them permanently at
121+
the right level (per-event detail at DEBUG, per-action summaries at INFO).
122+
- Route perf logging through a dedicated child namespace (e.g. `tallyman.perf`,
123+
and the buckaroo equivalent) so it is independently dialable without raising
124+
the level of everything else.
125+
- For the buckaroo JS side, gate the bk-flash / cross-stream tracing behind a
126+
debug flag rather than deleting it in a "console cleanup."
127+
128+
This is a prerequisite for the rest of stage 2: the lifecycle counts in
129+
deliverable 1 and the harness in deliverable 2 read these logs as the in-app
130+
source of build/load/execute events. (buckaroo changes are in scope — it's the
131+
companion's own viewer — and follow buckaroo's build/test conventions.)
132+
133+
### Deliverable 1 — the expression-lifecycle cost model (write before any perf test)
134+
135+
For each user-facing action — open the app, view an entry, revise a source, open a
136+
diff, reset — answer: how many expressions get **built** (graph constructed /
137+
tokenized), how many get **loaded** (from a build dir, the `/load_expr` path), and
138+
how many get **re-executed** (`.execute()`), and which of those were avoidable.
139+
Build/load/execute counts are the explanatory variable; wall-clock is the symptom.
140+
141+
- **Build cost** dominates with graph depth and count — long xorq graphs are
142+
expensive to construct independent of execution, which is the whole reason
143+
`from_catalog` truncates at a parquet read (ADR). So "how many entries, how deep
144+
each graph" is a first-order lever.
145+
- **Load cost** is distinct from build — #34 (the 10s `/load_expr` timeout that
146+
abandons cold loads) is a load-path problem, not a cache-key problem.
147+
- **Re-execution timing** is where caching actually shows up, but the lever is
148+
*when* re-execution is triggered, not how the key is computed — #21 (caches warm
149+
lazily on view, never at build) means re-execution lands at view time and the
150+
user waits.
151+
152+
The cache layers matter only insofar as they change those counts (a hit skips a
153+
re-execution; a shallow graph skips a build). The catalogue of layers already
154+
exists — the `docs/caching-architecture` branch ("map every cache in the
155+
tallyman/xorq/buckaroo stack", `xorq` `SnapshotStrategy`, tallyman `result_cache`,
156+
the `.cas` store + `content_hash`, buckaroo's stat/session caches). Reuse it as
157+
the levers section; the deliverable here is the lifecycle model on top, not a
158+
re-derived cache-key taxonomy.
159+
160+
Existing backlog items are findings against this model: #21 (lazy warm), #22
161+
(checkpoint capture cost per mutating request), #30 (measured cost rubric vs
162+
structural `cache_worthy`), #35 (leftover result dirs). The hypotheses to settle
163+
with numbers: does the parquet-truncation boundary actually cut build cost at
164+
depth (ADR open question, untimed); how many expressions are needlessly rebuilt or
165+
reloaded per app open; does revising a source re-execute dependents at all today
166+
(ADR says no — fast but stale).
167+
168+
### Deliverable 2 — performance integration tests
169+
170+
Three measurements, each on both corpus variants (full / truncated) and across a
171+
cold/warm cache axis. Local-only on this Mac, marker-gated off CI (same convention
172+
as `cache_lab`).
173+
174+
Every measurement records the **build / load / execute counts** alongside
175+
wall-clock — per deliverable 1, the counts are the explanatory variable and the
176+
time is the symptom. A regression in wall-clock should be attributable to a
177+
changed count (one more expression rebuilt, one more cold load), not left as an
178+
unexplained slowdown.
179+
180+
- **Execution time**`time expr.execute()`. Trivial; cold and warm.
181+
- **Page load including buckaroo — two tiers.**
182+
- *Tier A (Playwright, ground truth).* Spin up the catalog + buckaroo server,
183+
drive `/load` (`mode: 'buckaroo'`), measure to **data-grid-visible**
184+
(`.df-viewer .ag-cell` first-visible) — that is "loaded." Follow buckaroo's
185+
server-mode examples: `packages/buckaroo-js-core/pw-tests/server-buckaroo-summary.spec.ts`,
186+
`playwright.config.server.ts`, `scripts/test_playwright_server.sh`. Produces
187+
the baseline number.
188+
- *Tier B (python-only proxy).* Call the same backend the buckaroo `/load`
189+
(`mode=buckaroo`) handler dispatches to — load the xorq expr from the entry's
190+
`xorq_build/` dir + compute summary stats — no server, no browser. Fast and
191+
deterministic; this is the test that runs routinely.
192+
- *Calibration (one-time, required).* Run both tiers on the same entry, record
193+
the backend/total ratio, and document what Tier B cannot see (browser render,
194+
network, first-paint JS). For the big parking datasets the backend dominates,
195+
so the proxy is faithful — but the ratio must be written down so the proxy
196+
can't silently drift from reality.
197+
- **Diff performance** — exercise the `catalog_diff` stat path (#45 measured 27s
198+
synchronous on first open for 18.4M vs 1.0M). Define the revision pairs diffed;
199+
cold and warm.
200+
201+
**Truncation design.** Truncate at the raw-source read (`head(1M)` on the source
202+
parquet) so the DAG shape and build/tokenize cost are identical to the full
203+
corpus and only execution cost drops. Both variants emitted by the stage-1
204+
rebuild script; regenerable, not checked in.
205+
206+
**Gating.** Report-only first (no baselines yet), matching the existing perf-report
207+
suite. Promote a few to regression gates once baselined — #45's diff time is the
208+
obvious first gate.
209+
210+
**Why this is the point.** Once the harness exists, competing approaches stop
211+
being argued and start being measured. Every native-or-build verdict in this
212+
stage, and every design choice in stage 3, is decided by running both options
213+
through the same harness and reading the build/load/execute counts and wall-clock
214+
off the parking corpus. The harness is the comparison instrument, not just a
215+
regression guard.
216+
217+
---
218+
219+
## Stage 3 — reactive recalc & staleness
220+
221+
Design captured in `plans/adr-reactive-catalog-recalc.md`. Two of its open
222+
questions are answered by stage 2's measurements, so stage 3 design is gated on
223+
stage 2:
224+
225+
- deep-chain build cost (ADR open question, explicitly untimed) → stage 2's
226+
build-cost-at-depth measurement.
227+
- is `cas` affordable as the default for recalc-enabled projects (disk cost) →
228+
stage 2's disk/cold-warm numbers.
229+
230+
**Landable early (low-risk, ADR-verified, net-neutral).** These can go in during
231+
the stage-1/2 window:
232+
233+
- The tagged-read spine: replace the anonymous `deferred_read_parquet` in
234+
`io.py` with `deferred_read_parquet(<entry>/result.parquet).hashing_tag(
235+
CatalogTag.SOURCE, entry_name=…)` (ADR: built in 0.019s, stayed shallow).
236+
- Drop the regex `result.parquet`-path matching in `lineage.py` for
237+
`walk_nodes(HashingTag)`.
238+
239+
**Still open (settle after stage 2):** read-intent recording (alias-follow vs
240+
hash-pin at build time), trigger model (button vs scan-on-load vs
241+
auto-cascade — leaning button + scan), cascade/transaction semantics, archive
242+
overlay (derived vs stored).
243+
244+
---
245+
246+
## Cross-stage dependencies
247+
248+
```
249+
#48 fix ─┐
250+
#52 fix ─┴─► rebuild script ─► clean parking catalog ─► stage 2 corpus
251+
252+
cache-architecture map ───────────────────────────────► perf tests ─► numbers
253+
254+
┌──────────────┘
255+
tagged-read spine (early) ──────────────────────────────►│
256+
257+
stage 3 reactive design
258+
```

0 commit comments

Comments
 (0)