Skip to content

Indexed directory tree: cap lifted, per-file receipts, files:all gate - #3

Merged
conikeec merged 12 commits into
mainfrom
feat/tree-scale
Jul 13, 2026
Merged

Indexed directory tree: cap lifted, per-file receipts, files:all gate#3
conikeec merged 12 commits into
mainfrom
feat/tree-scale

Conversation

@conikeec

Copy link
Copy Markdown
Contributor

What this does

Replaces the per-file-token folder model with an indexed directory tree, lifting the ~200-file mint cap to thousands of files in one call, and makes coverage a per-file receipt.

Backend (waggle-tree + mcp)

  • waggle-tree — new pure crate: fixed-size union-composable Bloom, trigram inverted index, Merkle DirIndex, ranking primitives.
  • Mint — two-phase indexed mint: one content-addressed node per folder (trigram + Bloom), snapshot-pinned. 1,527 files in ~1.6s (old cap: 200).
  • Read — a folder token returns its table of contents; read --file <name> serves one file by name.
  • Search — one call spans the whole lineage: Bloom-pruned, trigram-narrowed, regex-confirmed, ranked; each match names its file path + owning node token.
  • Coverage — per-file (files: read/total, complete, unread NAMED), via an additive Event.entry ordinal (a position into the signed dir index — I-1-safe like the region-touch field) folded by a commutative/idempotent EntryTouchFold.

files:all gate, rewired onto trees

mint --tree --require files:all now attaches the contract to the root node and coverage emits met (false→true as the last file is read) — the flat tree's gate, now proven per file. Verified live on the reasoning corpus (0/11 → 11/11).

Benchmark + docs

  • Rewrote the zero-LLM conformance suite to the indexed contract (53 checks, green on the real corpora); fixed a pre-existing test-isolation flake.
  • Corrected the stale flat-tree model across the ops catalog (→ CLI help, MCP tool descriptions, COMMANDS.md), the site, the guides, and the design logs (superseded-by banners, history preserved). Paper stays historical + gains an Outlook note.

Breaking / release

  • Breaking CLI: the dead --from fan-out cursor is removed from read.
  • New CLI arg: read --file <name>.
  • Version bumped 0.4.0 → 0.5.0.

Merging deploys the site (pages.yml) and recompiles/publishes the paper PDF (paper.yml). Tagging v0.5.0 cuts binaries + the Homebrew tap via release.yml.

🤖 Generated with Claude Code

conikeec added 12 commits July 13, 2026 12:29
New sans-I/O crate — the deterministic foundation for tree-scale minting and
search. Three structures, each a pure function of bytes so an index blob is
content-addressable and byte-stable (I-2), and so the wasm edge can build/query
them too:

  bloom.rs    fixed-size (256 B), union-composable Bloom over trigrams — the prune
              gate that makes deep-tree search sublinear. Parent = OR of children,
              so nesting is free. Pinned seed + k, no false negatives, saturates
              gracefully. Inlinable in a manifest → a prune is a pure manifest read.
  trigram.rs  case-folded 3-byte shingles + an inverted TrigramIndex (posting-list
              intersection). "grep N files" -> "look up a few short lists and
              confirm survivors." Hex-keyed wire form (folded bytes need not be
              UTF-8).
  dirindex.rs the Merkle directory node: files pinned by sha256, subdirs addressed
              by subtree token, with recursive files/bytes totals so a node knows
              its weight without a walk.
  search.rs   the pure decisions: prune (Bloom gate), candidates, and rank
              (matches desc, then depth, then path — deterministic).

Traversal, the filesystem walk, and the blob store stay in waggle-mcp; this crate
holds only what must be deterministic and unit-testable in isolation. 22 tests,
clippy clean, largest file 299 lines.
Additive, signed-core field `tree: Option<TreeNode>` (skip-if-none, so non-tree
manifests keep their exact bytes — same pattern as `extraction`). A TreeNode carries
the node's content-addressed directory index, an optional trigram-index pointer, an
inlined hex Bloom summary of the subtree, and recursive files/bytes totals.

The bloom stays a hex string here rather than a typed `waggle-tree::Bloom` so
waggle-core keeps its dependency-free leaf position; waggle-mcp parses it. MintSpec
gains a `.tree(TreeNode)` builder.
…is A+B)

mint --tree now builds a Merkle hierarchy of directory NODES instead of a flat list
of per-file tokens. Two phases (tree_mint.rs):

  build_node (bottom-up): pin every file's bytes eagerly (content-addressed, so a
    deleted file still reads — C-1 holds), build each directory's DirIndex, trigram
    index, and Bloom summary (union of children's + local files), pre-generate the
    node token. Enforces a byte budget (max-bytes, default 256 MB) so nobody pins a
    giant media folder by accident.
  mint_nodes (top-down): mint one signed manifest per directory, parent before
    child so the parent exists when a child links to it.

A file is an index entry, not a token — so the old 200-FILE cap becomes a bound on
directory nodes (few). A 1,527-file corpus mints in one call. A per-file token is
minted only lazily, when a consumer needs a standalone reference (follow-up).

mint routes tree builds early (no orphan root token). The old flat mint_tree /
mint_children / collect_files / DENIED_DIRS are deleted. read/search/coverage still
read the old shape and are rewired next.

tree_mint.rs 406 lines; clippy clean.
…ll falls

read, search, and coverage now operate on the Merkle/trigram/Bloom index instead
of per-file child tokens (tree_read.rs):

  read (projection)  the directory's table of contents from its DirIndex — local
                     files (name/size/type) and subdirs (name/token/totals). No
                     descent; totals come from the index.
  read --file <name> one file's bytes, fetched from the content-addressed blob by
                     its hash, and stamped as a real read.
  search             ONE call spans the whole lineage: prune each subtree on its
                     Bloom (literal patterns), narrow to candidate files with the
                     node's trigram index, confirm with a real regex match, and
                     RANK (matches desc, then depth, then path). Each match carries
                     its path + owning token to drill in. Fixes "search each
                     subtree token separately".
  coverage           node-granular over the tree: file-bearing nodes read / total,
                     with the true file count from the root index. Pure-container
                     nodes are excluded (nothing of their own to read).

The old tree.rs (read_tree/lens/tree_files) and search_tree are deleted. Four
integration tests rewritten to the indexed model; the gap-fix deny-list test too.

Verified on ~/tulving/rote/docs (1,527 files, deep hierarchy): mints in one call
in 1.6s (old cap was 200); a search for "fingerprint" spans the tree and ranks
identity-binding.md (x418) first. Correct against a disk grep.

Known, documented in the design log: the 256-byte Bloom saturates for very large
subtrees, so pruning bites mainly at small/leaf nodes (correct — no false
negatives — just less pruning at extreme density); and files:all per-file coverage
awaits a served-set the payload-free log can't hold in an 8-bit mask. Both are
follow-ups, not correctness gaps.

All workspace tests pass; clippy clean; every file <= 750 lines.
…payload

Coverage on an indexed tree was node-granular ("the folder was touched"). Too
coarse for the product's own promise — "see exactly what your AI read" should mean
WHICH FILES. This makes it per-file without breaking I-1.

The unlock: I-1 forbids bytes in the log, not positions into a signed manifest.
The log already carries two such integers, both I-1-safe by their own docstrings —
`variant` (which manifest variant served) and `regions` (which contract line-ranges
a read touched). A tree node's DirIndex is a sorted, signed, immutable file list,
so "file #k of this node was read" is the same species of object: meaningless
without the signed manifest, therefore leaks nothing. The only blocker was the
8-bit width of `regions` — a width limit, not an I-1 limit.

  core   Event.entry: Option<u32> — the ordinal, in the owning node's signed
         DirIndex file order, of the one file a read served. Additive and
         serde(default, skip): old wire frames keep parsing, ordinary non-tree
         traffic never carries it, and records persist as JSON payload so there is
         NO schema migration. EntryTouchFold unions the touched ordinals per token;
         union is commutative + idempotent, the same algebra that gives
         RegionTouchFold its replay tolerance, so R-1/R-3/C-8 come for free.

  mcp    read --file stamps the file's ordinal; search stamps each confirmed
         match's ordinal (carried on waggle_tree::Hit.entry, a stable position in
         DirIndex.files()). Tree coverage replays each file-bearing node (the same
         scan_token + replay path contract_coverage uses), counts
         |touched ∩ [0, local_files)|, and sums across the lineage → files read /
         total at true per-file granularity, with per-node unread counts and the
         first missing file NAMED.

Deliberately out of scope: soa.rs (the 7-column fixed-width funnel/region
accelerator) gains no entry column — per-file coverage rides the replay path, not
the columnar one, and a u32 ordinal would muddy that structure's narrow-width
invariant for no gain.

Test rewritten: a node with two files, one read, reports "1/3" and names the file
nobody opened; a search that serves every file closes it to "3/3". Fold unit test
mirrors the region test (union commutative + duplicate-immune).

No token-per-file, so the 200-file cap stays lifted. All workspace tests pass;
clippy clean; every file <= 750 lines. Design log: waggle-tree-scale/DECISIONS.md.
…nto the CLI

The Tier-3 conformance suite (zero-LLM scripted-consumer detector) encoded the
OLD flat-tree contract — per-file child tokens, --section fan-out with a --from
cursor, a files:all gate with a met verdict. On the indexed tree it crashed
(read's `files` is now a count, not a list). Rewritten to the indexed contract it
actually ships:

  projection    read <root> is a table of contents — total_files (true tree size),
                local files, and subdirs each with a token to descend. Asserted on
                reasoning (11), folder (12), bigtree (180, nested).
  per-file      read <root> --file <name> serves one file and stamps exactly it:
                opening 1 of 11 reports files "1/11", names the other 10 unread,
                and clears its own name. This is the headline the indexing work
                stands on — a receipt of WHICH file, not "the folder was touched".
  search        one call spans the lineage, every match names its file (path +
                token); an absent literal visits ZERO nodes (Bloom prune bites);
                a zero-match search moves coverage not at all.
  the close     a perfect consumer closes per-file coverage cheaply: one search
                serves the bulk, coverage names the stragglers, read --file opens
                each — reasoning closes in 2 calls.
  budget        the bounded responses (TOC, search) fit max-bytes.

53 checks, all green on the real corpora against the indexed binary.

Wiring gap this surfaced: read --file existed in the MCP handler but was never
exposed on the CLI — there was no command-line way to read one file of a tree, so
the per-file close path was unreachable. Added `read --file <name>` to the CLI and
the matching ops-catalog ArgSpec (parity holds).

Known follow-up (flagged, not silently changed): the READ/coverage catalog
DESCRIPTIONS still narrate the old fan-out / files:all-gate model, which the
indexed tree no longer implements — that is model-facing prose and a wording call.

Per-file coverage remains the shipped receipt; the paper's flat-tree gate/fan-out
numbers stay pinned to the pre-index backend (decision 2). All workspace tests
pass; clippy clean; every file <= 750 lines.
…sured backend

Limitations gains a scope note on scale (decision: option 2). The evaluated
system mints one token per file — per-file coverage for free, but bounded past a
few hundred files. Subsequent work replaces the per-file forest with an indexed
directory tree (content-addressed nodes, per-node trigram + Bloom): thousands of
files in one mint, one search pruned and ranked across the lineage, coverage still
per file but now WITHOUT a token per file — each read records the file's position
in its node's signed index, a position not a payload, admissible under the same
payload-free-log invariant (sec:receipts) as the region-touch field.

Crucially honest: this is validated by the conformance suite (deterministic),
NOT by re-running the answer-quality sweep, which is reported on the per-file
system it was collected on. Whether indexing moves those numbers is future
measurement, not a claim drawn from these. No results numbers change.

Adds the Bloom (1970) citation. Compiles clean (tectonic) — no undefined refs or
citations.
…oven per file

mint --tree --require files:all silently ignored the contract: you asked for a
gate and got none. Now the root node carries the files:all contract (tree_mint
parses it via the shared contract_args path; a folder has no single text, so a
section: requirement is rejected — only files:all is meaningful over a tree), and
tree coverage emits `met`/`requires` alongside the per-file `files: read/total`.
`met` flips false -> true as the last file is read. A plain --tree carries no
contract and reports `complete` as a fact, never a verdict.

This restores the gate the flat tree had — and per-file coverage makes it
STRONGER: met is proven file by file, not node by node. The paper's gate results
are reproducible on the shipping backend again.

Verified live on corpus2/reasoning_0 (11 files, files:all): met=false at 0/11,
met=true at 11/11 after one search + one read --file.

Also fixes a pre-existing test flake: two extraction tests shared the temp dir
name `waggle-extract-{pid}`, so in parallel one's cleanup wiped the other's blobs
(binary_target_...pdf_story failed intermittently). Given distinct html/pdf names,
the suite is green multi-threaded. All workspace tests pass; clippy clean.
…/tree, drop --from

The operations catalog (the source of truth for CLI help, MCP tool descriptions,
and COMMANDS.md) described the OLD flat model: mint --tree "mints every file as a
child", read "FANS OUT a lens across every file" with an examined/total_files
truncation and a --from cursor, search groups matches "each with that file's own
token". None of that is how the indexed tree works.

Corrected to the shipping model:
  --tree      builds an indexed directory tree — one content-addressed node per
              folder (trigram + Bloom), thousands of files in one mint.
  read        a folder token returns its table of contents (files by name, subdirs
              by token); read --file <name> serves one file.
  search      spans the whole tree in ONE call — pruned, narrowed, ranked; each
              match names its file path + owning node token.
  coverage    a per-file receipt (files: read/total, complete, unread NAMED);
              --require files:all adds the met verdict.

Removed the --from arg (CLI + catalog): the fan-out it paged no longer exists, so
it was a dead, misleading flag. --require's files:all clause is unchanged — it is
correct again now that the gate is wired. COMMANDS.md regenerated; parity green.
…sign logs

The catalog fix (prior commit) corrected the generated surfaces; this corrects the
hand-written ones to the indexed tree + per-file model.

Site (docs/pages/index.html): the tutorial's step 3 listed a token per file and
step 4 fanned a --section lens "across every file at once". Now step 3 shows the
table of contents (files by name) and points at `read --file`; step 4 is the
one-call tree `search`, pruned and ranked. Steps 2 and 5 (--require files:all,
coverage with met) are unchanged — correct now that the gate is wired.

Guides:
  04-lifecycle-and-query  read --section fan-out -> read --file + one-call search;
                          coverage key read -> files.
  07-surgical-content     per-child funnels -> per-file receipts; run level ->
                          per-file files: read/total + complete + met.
  11-tmux-switchboard     per-file child tokens -> indexed tree; resolve INDEX of
                          file-tokens -> read table of contents + read --file.

Design logs (historical decision records): added "superseded by waggle-tree-scale"
banners to 18 §9 and 22 §4 rather than erasing the fan-out history — the failures
they name were real; only the mechanics changed. Fixed 18 §9.1 (per-file token ->
file by name) and 20-symbol-lens's "≤ 200 files" -> indexed, thousands.

Core: TreeNode doc-comment claimed a per-file token is minted lazily — no such
minting exists; files are addressed by name. Corrected.

The paper stays historical (measured pre-index) and needs no change. No code
behavior changed here; workspace builds, fmt clean.
Indexed directory tree (cap lifted to thousands of files), per-file coverage
receipts, files:all gate rewired onto trees, and the CLI read --file affordance.
Breaking CLI change: the dead --from fan-out cursor is removed.
On Windows, a directory node's dir_url was built with dir.display() (backslash
separators), and the parent derived each subdir's name by splitting that URL on
'/', which returned the whole path instead of the leaf. The projection's subdir
names were wrong, so a consumer could not find a subdirectory by name.

Carry the real base name in NodeData (from entry.file_name(), correct on every
platform) and drop the URL-parsing dir_name helper. Fixes the Windows-only
content_access::coverage_is_per_file_over_a_tree failure; the other platforms were
unaffected because '/' is their native separator.
@conikeec
conikeec merged commit a577ce6 into main Jul 13, 2026
13 checks passed
@conikeec
conikeec deleted the feat/tree-scale branch July 13, 2026 23:17
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