Skip to content

feat(cache): implement ADR-007, ADR-008 and ADR-009 (tallyman-owned materialization, row order, digest stability) - #189

Merged
paddymul merged 132 commits into
mainfrom
feat/adr-007-009-cache-redesign
Sep 28, 2026
Merged

paddymul merged 132 commits into
mainfrom
feat/adr-007-009-cache-redesign

Conversation

@paddymul

@paddymul paddymul commented Sep 21, 2026 •

Copy link
Copy Markdown
Contributor

Implements the cache redesign that #184 proposes: ADR-007 (tallyman owns result materialization), ADR-008 (every file carries __row_order, every page sorts by it) and ADR-009 (digest stability).

Stacked on #184. The first four commits are that PR's ADRs, so merge it first and this diff shrinks to the implementation. Draft, like #184.

Status — 2026-09-22: the fix PRs stacked on this branch

This branch is the direction I am taking, and it is not ready for main yet. Reviewing it turned up 25 follow-up issues (#183–#211). Rather than reopen this PR later, each fix lands as its own PR into this branch, TDD'd red then green on CI — 605a677 changed the workflow so pull requests into feat/** run it.

Open on top of this branch, all four green and none merged:

PR Closes What it changes
#213 #193 A failed build keeps the snapshot already on disk for its hash. materialize(..., publish=False) stages the file under a temp name; the build publishes it after write_manifest, so a failure between the two no longer deletes a good file.
#214 #194, #195, #196 A pin survives a reset and a dismissed error banner. A retired entry dir stays in the bullpen and a live dir wins when the same hash is re-added; an unfaithful heal is recorded in the manifest (unfaithful_heal_digest) instead of being inferred from errors.jsonl; a corrupt line in errors.jsonl is skipped instead of hiding the log. The Cache page labels a snapshot a reset retired.
#215 #197, #198, #211 An ordered copy keeps its source's column types, because pyarrow writes it instead of polars. The first CSV copy uses the reader options the caller passed, a polars panic becomes a BuildError, and the .digest sidecar is written through a temp file so a reader never sees an empty one.
#216 — The docs describe the system as built: ADR-007, ADR-008 and ADR-009 marked implemented, ADR-010 marked rejected, and docs/architecture.md rewritten as the overview doc. Docs and plans only.

Merge order: #213, #214 and #215 first, in any order, then #216 rebased — it currently describes those bugs as open in about twenty places.

Still open against this branch

What changed

Terms, once: an entry is one catalog computation. A worthy entry is one tallyman writes a parquet file for, called its snapshot, because it does expensive work (an aggregate, join, sort, window function, UDF, union). A cheap entry is a row-preserving plan over one file and keeps no file of its own. An ordered copy is the parquet copy of a source that carries __row_order. A clone is the copy of a source under data/.cas/, named by the digest of its bytes.

  • No xorq cache node is in any build (ADR-007 D1). A recipe that calls .cache() is a build error. rewrite_cache_dirs, classify_build, snapshot_key, entry_graph_expr and the source-read injection are deleted, and a test keeps XORQ_CACHE_DIR empty through a build, a chained child, a view, a delete and a reopen.
  • One writer, materialize. It runs the entry's frozen build on a single-partition connection, writes the snapshot in a pinned layout (zstd, 1,048,576-row groups, a parquet page index, __row_order last) to a temp name and replaces the file atomically, under the project's write lock. A create runs the query twice; a recipe that is not reproducible is recorded and its file is pinned (ADR-007 D4, ADR-009 D1, D3, D6).
  • ensure_materialized makes every file an entry's plan reads exist before anything runs: a snapshot by recursing on the hash in its name, an ordered copy from its clone with the reader options in the manifest, a clone from the live source while the bytes still match. What it writes is checked against the recorded digest; a mismatch is loud and pinned (ADR-007 D5, D13).
  • Chaining a worthy parent is a bare read of its snapshot, so a child's hash follows its parent's and a filter over an aggregate is cheap (ADR-007 D3).
  • __row_order is an int64 0..N-1 last column on every snapshot and ordered copy. /api/data pages by ORDER BY __row_order. A cheap entry that drops the column fails to build with the fix shown; assigning to it is an error; every order_by gets the natural order as a tie-break and a non-final one is kept; three-way joins get an instruction; the diff drops the column from both sides (ADR-008).
  • Cheap or worthy is one allow-list test, decided when the entry is built and recorded in the manifest. expr.yaml is no longer parsed for it (ADR-008 D4, closes Cache classifier parses xorq build YAML with regex — silent misclassification if the format drifts #12).
  • Every source enters through an ordered copy keyed by the source's content digest and reader options, under compute_cache/ordered_sources/. Editing a CSV and re-running a recipe now forks the hash (closes tallyman_read_csv bypasses source identity — content-blind intermediate key, in-place re-materialization, no manifest.sources #168). A raw xo.deferred_read_parquet is a build error (ADR-008 D2, D7, D12).
  • result_digest is arrow-sha256:, a digest of the file's Arrow content read back, so it does not move with the codec, row-group size or pyarrow version. An unfaithful heal names an engine change when the recorded versions differ (ADR-009 D2, D4).
  • Buckaroo is handed files that exist. A worthy entry's grid gets a view build of its snapshot, a cheap entry its own build. Session ids are entry-<project>-<hash> and tallyman keeps no record of them; a klass reload posts one request per entry; an unfaithful heal forces a reload; every /load_expr names row_order_column (ADR-007 D6, ADR-008 D8).
  • Reset leaves compute_cache/ alone and moves clones into the bullpen instead of deleting them, which fixes an entry that failed after a reset back and forward. The project lock is re-entrant per thread (ADR-007 D11, D14).
  • Cache page: lists every snapshot file (a file whose entry is gone is an orphan row), marks a snapshot pinned, and refuses to delete a pinned one with the reason.

A snapshot's name is now the entry's content hash, which is the same on every machine, so this should also fix #77 (an empty grid on a clone of the project at another path). I did not test it on a second machine.

docs/system-contract.md and the descriptive docs are rewritten to match. Each of ADR-004 to ADR-009 has an updated status line, and ADR-007, ADR-008 and ADR-009 have an "Implementation notes" section that records where the code differs from the text. The differences a reviewer should look at: the ordered-copy key uses the source digest and not the clone's path, the reader options live in a new manifest.ordered_copies, a three-way join is refused by an explicit check because ibis no longer raises through the new canonical sort, and hoisting a non-final order_by uses only the keys the author wrote.

How it was built

  1. f9bb94b: 180 failing tests for the three ADRs in one commit. CI ran it: ruff passed and the fast suite failed with 180 failed and 688 passed, which matches the local run.
  2. The implementation, in the commits after it. CI on 283c483: ruff, the fast suite (861 passed), the integration suite (7) and the perf report all pass.
  3. Updates to the existing tests. About 115 changed for the two deliberate build rules: a cheap recipe now has to keep __row_order, and a raw parquet read is refused. Tests of retired mechanisms (xorq cache nodes, the session map, compute-cache pruning) were deleted or rewritten against the new one; the commit message lists each deleted test and the decision that retired it.

Checked locally before pushing: the fast suite (860 passed), the integration suite against a real Buckaroo (7), the on-demand cache_lab suite (11), a scripted create, revise, cascade, live diff, promote and reset back and forward against a real Buckaroo, and the Cache page in Chromium.

Not in this PR

After merge

Every worthy entry's hash, snapshot path and digest change, so the corpus has to be rebuilt once (ADR-007 D9). ~/.cache/xorq/result_cache and the older ~/.cache/xorq/parquet/ can then be deleted by hand.

🤖 Generated with Claude Code

paddymul and others added 19 commits September 20, 2026 10:29
Brings the three draft ADRs from the 2026-09-18 cache audit, and the
spikes they cite, onto one branch so they can be reviewed together. They
were drafted as separate PRs (#180, #181, #182) and revised there during
the 2026-09-19/20 design session; this commit is their state at the end
of that session, unchanged.

- ADR-007: tallyman owns result materialization (no xorq cache nodes in
  builds); Buckaroo is a displayer; every diff is built as an entry.
- ADR-008: every file carries a visible __row_order column and every page
  request sorts by it.
- ADR-009: materialization runs single-partition, and result_digest is a
  digest of the snapshot's content.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
… ADRs

- ADR-009 D6 (new): create runs the query twice and compares digests, so
  a non-reproducible recipe is known from birth and its file is pinned;
  a cheap entry gets the same check and is materialized if it fails.
  Adds a Testing section. The entry's schema is read from the file.
- ADR-007 D11 (new): one write at a time per project, by taking the
  existing project file lock around every write; replaces the per-entry
  lock. Two tallyman servers on one project is unsupported (#183).
- ADR-007 D12 (new): files are deleted only by an explicit user action;
  the startup warm-up stops rewriting deleted files; no disk budget yet.
- ADR-007 D6: tallyman stops remembering Buckaroo sessions and re-posts
  every time, with a session id derived from project, hash and view kind.
- ADR-007 D9: the agreed order of work. Nothing starts before review.
- ADR-007 open question 1: whether two kinds of entry survive. Unanswered.
- Testing sections for ADR-007 and ADR-008, marking which tests are red
  on main today.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
An adversarial review of the three draft ADRs, and Paddy's decisions on
its findings (2026-09-20). Still Proposed; nothing is implemented.

- ADR-007 D10 (every diff is built as an entry) moved out to #188, so the
  set stays about the core structure of the cache. D10 is kept as a stub
  so that references to D11 and D12 stay valid. The live diff is recorded
  as the one known exception to the governing rule.
- ADR-007 D5: the verify sweep is not a caller of ensure_materialized; it
  reads and never writes. The ordered-copy gap is recorded as accepted.
- ADR-007 D6: the session id is derived from project and content hash
  (closes #172); the exact condition under which Buckaroo skips a repeat
  /load_expr; deleting a snapshot ends no session; an unfaithful heal
  posts force_reload, since evict_session worked by forgetting a record
  that D6 removes.
- ADR-007 D11: three limits of the project lock (per-thread re-entrancy,
  recalc granularity left open, reads not covered, #118). #186 tracks the
  waiting it causes.
- ADR-007 D12: a file is written only because something is about to read
  it. The warm-up sentence now states the precise condition.
- ADR-007 D2: memoizing the read stops tables piling up, not footer reads.
- ADR-008 D2, D7: CSVs do not already work this way. The intermediate is
  keyed by path and overwritten in place (#168), so D7 cannot land before
  that fix. CSVs go through source identity and the ordered copy is built
  from the content-addressed clone.
- ADR-008: "repeatable pages" asserts the exact rows; a new test that an
  edited CSV forks the hash; memory at depth recorded under Consequences
  as a performance matter, deferred.
- ADR-009 D1, D3: single-partition execution does not make an ungrouped
  float total a function of the rows alone; it depends on the parent
  file's row-group layout (#187). Row-group size and batch_size are pinned
  and the manifest records a snapshot format version.
- ADR-009 D6: the cheap-entry half moved to #185; the limits of running
  twice are stated, and the #88 lint is mentioned.
- ADR-009: four stale cross-references to ADR-008 fixed.
- All three Testing sections: normal TDD. Every test goes in the
  failing-tests commit; a test of a missing function fails on import.

New evidence scripts, each runnable from a clean temp dir:
scripts/spike_csv_source_identity.py (ADR-008 D2, D7),
scripts/spike_float_layout_digest.py (ADR-009 D1, D3),
scripts/spike_deep_page_memory.py (ADR-008 Consequences).

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Keep two kinds of entry (ADR-007 open question 1). ADR-007 gains D13 (a file is cache only if ensure_materialized can re-create it) and D14 (a reset leaves compute_cache alone), closes the ordered-copy gap in D5, and adds the klass hot-reload fix to D6. ADR-008 rewrites D4 as a three-part test for cheap, corrects D6's three-way join claim, and adds D10 (natural order on every order_by), D11 (hoist a non-final sort) and D12 (a raw parquet read is a build error). ADR-009 says how a loaded build gets onto the single-partition connection and extends the format version to ordered copies.

Adds seven evidence scripts under scripts/. Items not yet confirmed by Paddy are marked as such in the ADR text.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
One commit of failing tests for the whole cache redesign, so every test is
seen red on CI before the change lands (ADR-007 D9, step 1):

- ADR-007: builds carry no cache nodes, snapshot path from the content hash,
  chaining as a bare read of the parent's snapshot, one writer
  (materialize), ensure_materialized, re-creation of each class of file,
  the Buckaroo hand-off (derived session ids, view build, forced reload,
  klass reload), one write at a time per project, reset leaves
  compute_cache alone, the xorq-cache sentinel.
- ADR-008: __row_order on every file, the cheap/worthy allow-list, build
  errors for a dropped or assigned column, joins, sort grafting and
  hoisting, repeatable pages, CSV roots and #168, raw parquet reads.
- ADR-009: the content digest, the pinned snapshot format, the
  single-partition connection, create runs the query twice, engine versions.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…mpute_cache alone

ADR-007 D11, D13, D14 and the manifest fields ADR-008 and ADR-009 record:

- project_lock is public and re-entrant per thread, so a promote can build and
  then checkpoint, and a build can materialize, while another thread or process
  still waits.
- Manifest loses snapshot_key and gains reproducible, nonreproducible_columns,
  snapshot_format, engine_versions and ordered_copies.
- reset_to no longer records, prunes or restores compute_cache/ (no
  compute_cache.jsonl). Source clones no surviving entry refers to move to the
  bullpen instead of being deleted, and a reset forward copies them back.
- ensure_cas_path clones to a unique temp name, so two builders cloning one
  source do not share one.
- paths: entry_view_build_dir for the view build a worthy entry's grid is
  handed; buckaroo_sessions_path is gone (tallyman keeps no session record).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
ADR-007 D1 to D5 and D7, ADR-008 D2 to D4, D6, D7 and D10 to D12, ADR-009
D1 to D4 and D6.

- No build holds a xorq cache node. A recipe that calls .cache() is a build
  error; rewrite_cache_dirs, classify_build, snapshot_key, entry_graph_expr and
  the source-read injection are gone.
- materialize is the one writer of snapshots: a single-partition connection, a
  pinned layout (zstd, 1,048,576-row groups, page index), a last __row_order
  column, atomic replace under the project lock. A create runs the query twice;
  a recipe that is not reproducible is recorded and its file is pinned.
- ensure_materialized makes every file an entry reads exist before anything
  runs: snapshots by recursion, ordered copies of sources from their clone with
  the reader options in the manifest, clones from the live source while the
  bytes match.
- Every source enters through an ordered copy under compute_cache/, keyed by
  content digest and reader options; tallyman_read_csv no longer sorts and its
  column is __row_order. A raw parquet read is a build error.
- Cheap or worthy is one allow-list test decided at build and recorded in the
  manifest. A cheap entry must keep __row_order, assignment to it is an error,
  every order_by gets the natural order as tie-break and a non-final order_by is
  hoisted, and a three-way join in one recipe gets an instruction.
- result_digest is an arrow-sha256 content digest of the file read back. A
  heal mismatch is attributed to an engine change when the recorded versions
  differ, and stays loud and pinned.
- Chaining a worthy parent is a bare read of its snapshot, so a child's identity
  follows its parent's.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
… pin what cannot be re-made

ADR-007 D6 and D12, ADR-008 D5 and D8, ADR-009 D6.

- load_session runs ensure_materialized first, then posts /load_expr with a
  session id derived from the project and the hash. A worthy entry is handed a
  view build of its snapshot, a cheap entry its own build. Tallyman keeps no
  session record; a klass reload posts /reload_expr per entry and treats a 404
  as not open; an unfaithful heal forces a reload of the open grid.
- Every /load_expr names __row_order as the row-order column.
- /api/data pages by ORDER BY __row_order.
- The startup warm-up loads cheap entries' builds and writes nothing.
- The Cache page lists every snapshot file (a file whose entry is gone is an
  orphan row), marks a snapshot pinned when it cannot be re-created faithfully,
  and refuses to delete it with a reason.
- A diff drops __row_order from both sides. The MCP tool descriptions explain
  the column and the build errors around it.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
The API now marks a snapshot pinned (with a reason) when it cannot be
re-created faithfully, and lists a file whose entry is gone as an orphan. The
page shows both, disables delete for a pinned file, and shows the reason when
the server refuses.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Rewrites the sections of docs/system-contract.md that ADR-007, ADR-008 and
ADR-009 change: xorq's cache nodes become background, the content hash covers
ordered copies and parent snapshots, the manifest table, worthiness as one
recorded verdict, row order, materialization and ensure_materialized, the
content digest, the write and read paths, chaining, the Buckaroo hand-off,
reset, verification (with the engine-change row), and a sixth invariant.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
#90's guard: a half-built or pruned entry passes the build-dir check and has no
manifest, and its page must still be served rather than 500ing. cache_worthy
now falls back to whether a snapshot is on disk when the manifest is gone, since
a snapshot can only have been written for a worthy entry.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Most existing tests failed on the redesign for one of two reasons the ADRs
chose on purpose:

- ADR-008 D3: a cheap recipe whose select list drops __row_order is now a
  build error, so those recipes keep the column, and exact column and row-key
  expectations now end in __row_order (about 67 tests).
- ADR-008 D12: a raw xo.deferred_read_parquet is a build error, so recipes
  read the file with read_project_file (about 48 tests).

test_connect_shim_matches_xorq_api_connect no longer compares xorq's snapshot
keys, which are gone; it checks the shim gives the same backend kind and
profile and that a materialization through it writes the same data.
compute_cache.jsonl leaves the expected tracked set (ADR-007 D14).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
architecture, caching, expression-lifecycle, mcp-server, reactive-recalc, the
README and the explanation doc no longer describe xorq cache nodes, the session
map, compute_cache.jsonl or original_row_order as current. They cover worthy and
cheap entries, snapshots and the content digest, ordered copies,
ensure_materialized, row order, the view build and derived session ids, the
project lock, and what a reset does to compute_cache and the source clones.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
ADR-007, ADR-008 and ADR-009 are implemented in #189: their status lines say so
and an Implementation notes section in each records where the code differs from
the text (the ordered-copy key and directory, the manifest's ordered_copies, the
explicit three-way join check, hoisting only the author's keys, nested types in
the digest). ADR-004, ADR-005 and ADR-006 note what the redesign amends or
supersedes. The datafusion scan-split threshold is 10,485,760 bytes, not 1 MiB
(ADR-008 D9). scripts/spike_sort_grafting.py uses classify_expr now that
_is_worthy_expr is gone.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Deleted, because the mechanism they protected is gone:

- xorq cache nodes in builds, and the lockstep between classify_build and
  _is_worthy_expr (ADR-007 D1, ADR-008 D4): test_scalar_udf_is_worthy_in_lockstep,
  test_worthiness_disagreement_falls_back_to_recompute,
  test_should_cache_read_treats_json_like_csv_not_exempt,
  test_self_heal_warning_degrades_to_execution_when_classify_build_raises.
- xorq's shared temp file and a peer process winning its rename (ADR-007 D4,
  D11): test_cross_process_heal_reads_landed_snapshot_else_reraises.
- Buckaroo's session record and file (ADR-007 D6): the session file round trip,
  location and persist tests, test_startup_prunes_entries_for_missing_builds.
- compute_cache pruning on reset (ADR-007 D14): the three prune tests. The new
  rules are in test_reset_keeps_compute_cache.py.
- Buckaroo replaying a build cold (ADR-006 D4, ADR-007 D3 and D5):
  test_chained_child_build_heals_parent_without_preheal.

Rewritten against the new mechanism, keeping what they protected: self-heal of
a deleted snapshot and its verification against the recorded content digest,
lineage-faithful reads, cold compute_cache reads, the loud unfaithful heal, the
result-plan memo after a reset, CAS reads after an edit or a deleted source,
Buckaroo unit-level open, restart and concurrency behaviour, and the klass
reload (one request per entry, a 404 means not open). The other tests changed
only for the two deliberate build rules. The 10 MiB scan-split threshold
replaces 1 MiB in the digest probe test (ADR-008 D9).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…not record it

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…ir the page-load profiler

recreate_ordered_copy looked for the copy's record only in the manifest of
the entry being opened. An entry that reached a cheap entry's graph by a path
that records nothing (a promoted diff, cached_result_expr in a recipe) could
not survive the loss of compute_cache/. The record now comes from the owner's
manifest, else from any entry in the catalog: the build that first reads a
source records its copy, and entries are never deleted.

scripts/profile_pageload.py imported the removed result_cache.classify_build,
found entries by the retired result.parquet, read the old aliases.json, and
paged over the bare build without making its files exist. It now reads the
worthiness verdict from the manifest and shares entry discovery, alias
lookup and a new _paged_build helper with the Tier-B harness.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@paddymul

Copy link
Copy Markdown
Contributor Author

Review pass (/code-review high, main...this branch)

Four findings, plus one more turned up while fixing the fourth.

  1. An ordered copy couldn't be made again for an entry whose manifest doesn't record it (ordered_copy.py). recreate_ordered_copy looked for the record only in the manifest of the entry being opened. Anything that reached a cheap entry's graph without _note_parent_records failed with SourceUnavailable once compute_cache/ was gone: a promoted diff, or cached_result_expr called from a recipe. Fixed: the record now comes from the owner's manifest, else from any entry in the catalog. The build that first reads a source records its copy, and entries are never deleted. Test in d7466bb (seen failing on CI), fix in 8bbd36b.

  2. Two async def handlers (api_promote_diff, put_code) build on the event loop while holding the blocking project lock, which freezes the companion while they wait. Not fixed here; a blocked build leaves the workflow stuck either way. The UI side (a busy indicator, which first needs the loop to stay free) is a build on the companion's event loop freezes the whole UI while it waits for the project lock — no way to show a busy indicator #190.

  3. A CSV outside data/ is recorded by absolute path, and staleness can't resolve it (reports unknown source: on every scan, never flags an edit). Not fixed here; filed as staleness never resolves a CSV recorded outside data/ — every scan reports it unknown, and an edit is never flagged #191.

  4. scripts/profile_pageload.py imported the removed result_cache.classify_build. Fixed in 8bbd36b. The profiler had also drifted from the Tier-B harness in three ways, fixed in the same commit and checked on a corpus built with this branch:

    • it found entries by the retired result.parquet and so saw none;
    • it read the old aliases.json;
    • it paged over the bare build without making its files exist.

    It now reads the worthiness verdict from the manifest, and shares entry discovery, alias lookup and a new _paged_build helper with tests/test_perf_integration.py.

The review also checked these and found them fine: the build hash doesn't depend on snapshot mtime/inode, reductions inside mutate/filter count as worthy, the content digest doesn't depend on how rows are batched, and project_lock can be re-taken within a thread.

Same test as #192, which shows it red against the profiler before 8bbd36b.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
paddymul and others added 3 commits September 26, 2026 14:48
…ow-order-and-membership

Resolves the conflict in src/tallyman_xorq/diff.py with #242: full_diff drops the row-order columns
first (an expression change, no execution) and runs buckaroo's three diff helpers under
execution_lock().

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…t-hardening

Resolves the conflict in src/tallyman_mcp/server.py with #241: keeps this branch's
_record_import_failure and takes #241's _entry_url, which resolves the companion's URL at call
time and gives None when no server holds the data dir.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…implemented

Brings in #242, #244, #245 and #241. Resolves docs/installing.md: keeps this branch's environment
table (with TALLYMAN_COMPANION_URL now read from the data dir's server.lock) and its troubleshooting
list, with #241's two bullets in place of the old port-7860 one, and without the stray tags this
branch had already removed.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
paddymul and others added 8 commits September 26, 2026 15:06
…e execution lock, one server per data dir, the manifest refusal and zoned CSV times

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
docs: describe the system as built with ADR-007, ADR-008 and ADR-009
The guard counted every name, attribute and parameter under src/ as defined,
so a message naming a tool that does not exist passed whenever a variable was
spelled the same way. Its scan moves into _unknown_catalog_names so a test can
run it over a small tree.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…failed build

The steps after catalog_import_source's import shared one try, so carrying the
entry's config forward failing skipped the new_entry notification and the
recalc, and the source's dependents stayed on the old version's rows. The
failure was recorded as a build_error with no hash, so the Log showed a failed
build for an import that was committed.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ot a failed build

The notebook append, the config carry-forward and the recalc each run in their
own try, so one failing no longer skips the new_entry notification or the
recalc. What failed is recorded once, as an after_import_error event rather
than build_error, with a message that says the version was imported and with
the new entry's hash, so a dependent left stale is tied back to it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…as defined

A variable, parameter or attribute spelled like a tool is not something a
message can send an agent to.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…and-membership

fix(diff): a diff leaves out __row_order and buckets rows by side markers (#200, #13)
fix(import): a failed import is recorded, names the user's file, and a column xorq cannot read is left out and named (#224, #225, #227, #234, #239)
…SIGSEGV

Opening a worthy entry's grid, a diff's compare grid and the re-hash after an unfaithful heal each call xorq's
build_expr in the companion process. The companion never installs the git-state guard, because catalog writes, which
install it, run in the MCP process. So xorq forks a bare `git rev-parse HEAD`, and on macOS, once PROJ is loaded, that
child can die with SIGSEGV (the #267 crash). The grid open then fails with "Tallyman could not prepare this entry:
CalledProcessError ... died with <Signals.SIGSEGV: 11>" until the companion restarts.

The tests put a git that kills itself with SIGSEGV on PATH and xorq's unguarded get_git_state in place, create the
companion app, and run each of the three builds.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
paddymul and others added 2 commits September 28, 2026 12:41
… created

The companion now calls install_git_state_guard() in create_app, so its own builds (a grid's view build, a diff's
compare build, the re-hash after an unfaithful heal) spawn git without forking, and a git that dies degrades to
placeholder provenance instead of failing the build. Before, only catalog writes installed the guard, and those
usually run in the MCP process.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
LICENSE is the text of https://www.gnu.org/licenses/agpl-3.0.txt, unchanged. pyproject.toml declares
`license = "AGPL-3.0-only"` with `license-files = ["LICENSE"]`, so the wheel carries License-Expression and the
license file. Both private JS packages declare the same SPDX id, and the README says where the license is.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
fix(companion): the companion installs the git-state guard, so a grid open cannot die on a forked git
chore: license tallyman under the GNU AGPL, version 3 only
@paddymul
paddymul marked this pull request as ready for review September 28, 2026 17:05
@paddymul
paddymul merged commit cff4fe4 into main Sep 28, 2026
3 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment