diff --git a/.beads/beads.jsonl b/.beads/beads.jsonl index 21d876c6..99dc4837 100644 --- a/.beads/beads.jsonl +++ b/.beads/beads.jsonl @@ -206,6 +206,7 @@ {"schema_version":1,"id":"mache-5caf12","title":"content_template omitempty is a behavioral change","description":"content_template JSON tag changed to omitempty. Previously always present in output; now omitted when empty. May affect downstream tooling checking key existence. Document in release notes.","status":"done","priority":3,"issue_type":"task","owner":"dev-agent","created_at":"2026-07-23T04:49:13+00:00","updated_at":"2026-07-23T04:49:13+00:00","created_by":"","dependency_count":0,"dependent_count":0,"comment_count":0,"dependencies":[],"comments":[],"repo":"mache","scope":"","external_ref":null,"branch":null,"pr_url":null,"acceptance_criteria":"","files":["api/schema.go"],"test_files":[],"derived_from":[]} {"schema_version":1,"id":"mache-5d1o","title":"find_smells: methods on receivers missed by ingest, causing false-positive dead_code","description":"Dogfooding mache against itself surfaced this. cleanPath() is called 5+ times in internal/nfsmount/graphfs.go but appears in dead_code findings because none of its callers — methods on *GraphFS — show up in node_refs. Investigated:\n\n- examples/go-schema.json HAS a 'methods' selector for method_declaration nodes\n- mache build uses FCA inference by default (no --schema flag accepted by build subcommand)\n- FCA inference produces only 'functions/' and '_project_files/' roots\n- method_declaration AST nodes are silently dropped at ingest\n- Result: any function only-called-by-methods looks dead\n\nTwo related fixes:\n1. mache build should accept --schema (or re-use root --schema) so users can opt into the example go-schema instead of relying on inference\n2. FCA inference for Go should produce a 'methods' construct dir (or fold methods into 'functions') so receiver-method callers appear in node_refs\n\n(2) is the larger fix; (1) unblocks dogfooding and gets mache to be correctly self-analyzed. Both useful.\n\nVerified: cleanPath has 0 entries in node_refs even though grep finds 5 callsites. After methods get projected, those callsites would create node_refs entries and dead_code would no longer flag cleanPath.","status":"done","priority":2,"issue_type":"bug","owner":null,"created_at":"2026-07-23T04:49:13+00:00","updated_at":"2026-07-23T04:49:13+00:00","created_by":"jamestexas","dependency_count":0,"dependent_count":0,"comment_count":0,"dependencies":[],"comments":[],"repo":"mache","scope":"","external_ref":null,"branch":null,"pr_url":null,"acceptance_criteria":"","files":[],"test_files":[],"derived_from":[]} {"schema_version":1,"id":"mache-5df546","title":"Clean up ~50 stale branches","description":"mache has ~50 branches from prior work, most already merged. Should prune merged branches to reduce clutter.","status":"done","priority":3,"issue_type":"task","owner":"dev-agent","created_at":"2026-07-23T04:49:13+00:00","updated_at":"2026-07-23T04:49:13+00:00","created_by":"","dependency_count":0,"dependent_count":0,"comment_count":0,"dependencies":[],"comments":[],"repo":"mache","scope":"","external_ref":null,"branch":null,"pr_url":null,"acceptance_criteria":"","files":["README.md"],"test_files":[],"derived_from":[]} +{"schema_version":1,"id":"mache-5e925d","title":"Make cost-quality baseline PR merge-ready","description":"PR #601 rescues the historical cost-quality benchmark but is draft with failing CI and two unresolved correctness reviews: parser failures are recorded as successful zero-usage runs, and scoring rejects legitimate responses that mention error early. Repair those behaviors, add focused Python tests, resolve the smell gate without grandfathering avoidable code debt, update the PR scope, and verify with python3 -m unittest discover -s benchmarks/cost-quality plus task ci.","status":"open","priority":1,"issue_type":"bug","owner":"scoping-agent","created_at":"2026-08-11T20:01:55+00:00","updated_at":"2026-08-11T20:01:55+00:00","created_by":"jamestexas","dependency_count":0,"dependent_count":0,"comment_count":3,"dependencies":[],"comments":[{"id":"699","issue_id":"mache-5e925d","text":"PR #601 review fixes implemented test-first in isolated worktree. Added regressions for pretty-printed/malformed Claude JSON and legitimate answers beginning with Error; invalid successful stdout now becomes an explicit error row. Refactored benchmark orchestration to clear duplicate_definitions and fan_out_skew without expanding the smell baseline. Focused unittest suite and task smells pass using the fixed LLO 0.18.1 binary. Awaiting #618 merge before rebasing, full CI, thread resolution, and merge.","author":"rsry-mcp","created_at":"2026-08-11T20:11:02Z"},{"id":"700","issue_id":"mache-5e925d","text":"Full task ci now passes in the normal pinned-Leyline environment, including race tests, smell gate, install:verify, docs/lint, and server.json drift. The first attempted full run intentionally exported the fixed LLO override globally and demonstrated why that is invalid for the complete suite: binary-resolution tests expect to control MACHE_LEYLINE_BINARY themselves. Fixed-LLO validation remains isolated to the focused producer/consumer seam tests. Worktree is clean and one commit ahead; rebase/push awaits authorization-gated #618 merge.","author":"rsry-mcp","created_at":"2026-08-11T20:21:52Z"},{"id":"702","issue_id":"mache-5e925d","text":"After PR #618 merged, merged origin/main into the isolated #601 branch. All five focused Python regressions pass, and a fresh full task ci passes on the caller-fix baseline, including formatter/vet/lint, smell ratchet, race suite, Leyline consumer smoke tests, install:verify, and server.json drift. Final branch sync awaits the already-green v0.18.2 pin PR #619 auto-merge.","author":"rsry-mcp","created_at":"2026-08-11T21:34:37Z"}],"repo":"mache","scope":"","external_ref":null,"branch":null,"pr_url":null,"acceptance_criteria":"PR #601 is current with main, both review findings have regression tests and fixes, the smell gate and full task ci pass, all review threads are resolved, and the PR is ready and merged.","files":["benchmarks/cost-quality/bench.py"],"test_files":["benchmarks/cost-quality/test_bench.py"],"derived_from":[]} {"schema_version":1,"id":"mache-5v8","title":"[cmd/build.go] Replace fmt.Println/Printf with structured logger","description":"Lines 35, 81, 85: Uses fmt.Println/Printf for progress tracking without log levels. Priority: Low.","status":"done","priority":2,"issue_type":"chore","owner":null,"created_at":"2026-07-23T04:49:13+00:00","updated_at":"2026-07-23T04:49:13+00:00","created_by":"jamestexas","dependency_count":0,"dependent_count":0,"comment_count":0,"dependencies":[],"comments":[],"repo":"mache","scope":"","external_ref":null,"branch":null,"pr_url":null,"acceptance_criteria":"","files":[],"test_files":[],"derived_from":[]} {"schema_version":1,"id":"mache-602a70","title":"SB-01: parameterize TestE2E_AllMCPTools over (MemoryStore, SQLiteGraph)","description":"Parent: [[mache-6682ec]] (ADR-0017, M1).\n\nReplace the single `buildMaybeMultiGraph` call in `cmd/all_tools_e2e_test.go` with a table-driven loop over backend constructors. Emit a `backend` column in `tool-profile.json`. Pin the schema-projected fixture so both backends consume identical input.","status":"done","priority":1,"issue_type":"feature","owner":"scoping-agent","created_at":"2026-07-23T04:49:13+00:00","updated_at":"2026-07-23T04:49:13+00:00","created_by":"jamestexas","dependency_count":0,"dependent_count":5,"comment_count":1,"dependencies":[],"comments":[{"id":"59","issue_id":"mache-602a70","text":"DONE. Refactor landed: `TestE2E_AllMCPTools` is now table-driven over `[]backendBuilder{memory, sqlite}` with each backend run as a t.Run subtest. The full 16-tool inventory runs against both, profile rows carry `backend` column, manifest aggregates across backends.\n\nSurfaced real backend deltas on first run — filed as separate bead. Specifically:\n- `find_smells` memory SKIPS (no SQL tables) vs sqlite OK\n- `find_callees`, `get_impact`, `read_file`, `get_architecture` return 2-9× smaller bodies on sqlite\n\nBoth backends PASS the existing acceptance bar (zero transport errors, at least one ok). Test runtime 15.2s (was 5.1s single-backend).\n\nImplementation:\n- `backendBuilder` struct + `allE2EBackends()` registry\n- `buildMemoryBackend` (existing `buildMaybeMultiGraph` path)\n- `buildSQLiteBackend` (new — uses `SQLiteWriter` + `OpenSQLiteGraph`)\n- Extracted `runToolMatrix` (so SB-02..SB-06 can reuse the invocation table)\n- Extracted `assertHarnessHealth` (per-subtest acceptance scoping)\n- Added `Backend` field to `toolProfile`","author":"rsry-mcp","created_at":"2026-07-23T04:49:13Z"}],"repo":"mache","scope":"","external_ref":null,"branch":null,"pr_url":null,"acceptance_criteria":"","files":["cmd/all_tools_e2e_test.go"],"test_files":["cmd/all_tools_e2e_test.go"],"derived_from":[]} {"schema_version":1,"id":"mache-603671","title":"SB-02: backend matrix runner for WritableGraph + CompositeGraph","description":"Parent: [[mache-6682ec]] (ADR-0017, M1). Depends on SB-01.\n\nExtend SB-01 to also cover `WritableGraph` and `CompositeGraph`. Document inapplicable cells with explicit `requireIsError` assertions naming the reason.","status":"open","priority":1,"issue_type":"feature","owner":"scoping-agent","created_at":"2026-07-23T04:49:13+00:00","updated_at":"2026-07-23T04:49:13+00:00","created_by":"jamestexas","dependency_count":0,"dependent_count":2,"comment_count":0,"dependencies":["mache-602a70"],"comments":[],"repo":"mache","scope":"","external_ref":null,"branch":null,"pr_url":null,"acceptance_criteria":"","files":["internal/graph/","cmd/all_tools_matrix_test.go"],"test_files":["cmd/all_tools_matrix_test.go"],"derived_from":[]} @@ -243,6 +244,7 @@ {"schema_version":1,"id":"mache-66f08b","title":"Landing page: rosary design language at mache.rosary.bot/","description":"The hosted mache landing page is a plain text string. Needs the rosary design language (void bg, JetBrains Mono, bead accents, stage markers).\n\nShould show:\n- What mache is (structural code intelligence)\n- How to connect: claude mcp add --transport http mache \"https://mache.rosary.bot/mcp?repo=\"\n- Available tools (get_overview, find_definition, search, get_diagram, etc.)\n- Fix http:// → https:// in the connect URL (uses r.Host but hardcodes http scheme)\n\nReference: auth.rosary.bot landing page (signet), site/DESIGN.md in rig repo.\n\nAlso fix: X-Forwarded-Proto detection for scheme, or use relative URL.","status":"done","priority":1,"issue_type":"task","owner":"dev-agent","created_at":"2026-07-23T04:49:13+00:00","updated_at":"2026-07-23T04:49:13+00:00","created_by":"","dependency_count":0,"dependent_count":0,"comment_count":1,"dependencies":[],"comments":[{"id":"134","issue_id":"mache-66f08b","text":"Closing — landing page HTML now lives in rig repo (fe concern). mache-bdd36f added the static file serving from /app/static/mache-landing.html which rig injects.","author":"rsry-mcp","created_at":"2026-07-23T04:49:13Z"}],"repo":"mache","scope":"","external_ref":null,"branch":null,"pr_url":null,"acceptance_criteria":"","files":["cmd/serve.go"],"test_files":[],"derived_from":[]} {"schema_version":1,"id":"mache-68980e","title":"find_smells dead_code rule: 57s on 353-file corpus (mache-on-mache via SQLiteGraph backend)","description":"Surfaced 2026-05-19 by SB-06 ([[mache-606947]]) — the mache-on-mache matrix test.\n\nRunning `find_smells --rule dead_code --limit 20` against mache's own source (353 Go files, ~30K LOC) on the SQLiteGraph backend takes **57.5 seconds**. On the synthetic 4-file fixture the same rule completes in 1ms. The work doesn't scale linearly with corpus size — it scales catastrophically.\n\nProfile snippet:\n```\nsqlite find_smells 57545ms 87176 B alloc 612 allocs ok (body=1072 B)\n```\n\nCompare:\n- toy 4-file: 1 ms\n- mache 353-file: 57 545 ms (≈57 000× the latency for 88× the file count)\n\nAllocations are tiny (87 KB) — this isn't an allocation problem, it's a query-cost problem. Likely candidates:\n1. `dead_code` rule does a full table scan per definition (O(n²))\n2. The rule lacks an index hint or `EXPLAIN QUERY PLAN` reveals a full sequential scan\n3. The dispatch loop runs once per node when it could run once over the whole graph\n\nThis must be fixed before SB-08's bench gate ships — a 57s benchmark on a 30K-LOC fixture is unworkable for nightly CI.\n\nAcceptance:\n- `find_smells dead_code` against mache-on-mache (sqlite backend) completes in <5s\n- `cmd/serve_find_smells_test.go` adds a benchmark with the mache repo as fixture (or a synthetic ~300-file corpus, when SB-05 lands)\n- `EXPLAIN QUERY PLAN` output documented in the fix commit so future regressions catch the index loss","status":"done","priority":2,"issue_type":"bug","owner":"scoping-agent","created_at":"2026-07-23T04:49:13+00:00","updated_at":"2026-07-23T04:49:13+00:00","created_by":"jamestexas","dependency_count":0,"dependent_count":0,"comment_count":1,"dependencies":[],"comments":[{"id":"280","issue_id":"mache-68980e","text":"Fixed via PR #404 — https://github.com/agentic-research/mache/pull/404\n\nRoot cause: SQLite planner cannot use indexes when JOIN ON-clause has top-level OR. Fix: 3 UNION arms, each single-predicate (target_node_id arm, token arm, receiver-method arm). Each arm is index-eligible. UNION preserves DISTINCT.\n\nMeasured on mache-on-mache (403 source files, sqlite backend): **57.5s → 123.9ms (~460× speedup)**. Findings identical (4 in both, no semantic change).\n\nTests added:\n- TestFindSmells_DeadCode_CorrectnessParity_FixtureUnchanged — pins exact finding set on a fixture exercising all 3 alive-check arms + skip-list dimensions\n- TestFindSmells_DeadCode_PerfGate_MacheOnMache (MACHE_E2E_SELF=1) — asserts <5s on mache-on-mache; runs at ~124ms\n\nClosing when PR #404 merges.","author":"rsry-mcp","created_at":"2026-07-23T04:49:13Z"}],"repo":"mache","scope":"","external_ref":null,"branch":null,"pr_url":null,"acceptance_criteria":"","files":["cmd/serve_find_smells.go","cmd/find_smells_cli.go","internal/graph/sqlite_graph.go","internal/graph/nodes_table_reader.go"],"test_files":["cmd/serve_find_smells_test.go"],"derived_from":[]} {"schema_version":1,"id":"mache-69162a","title":"Typed relation kinds on node_refs — service boundaries (HTTP/gRPC/GraphQL/MCP tool) with fidelity typing","description":"Gleaned from https://github.com/entireio/entire-graph while surveying prior art. The gleanable artifact is the relation VOCABULARY, not their implementation.\n\nTODAY: node_refs is (token, node_id) PRIMARY KEY - every relation is the same untyped relation. Direction and kind are both implicit.\n\nTHEIRS: 30 named relation types, grouped - structural (DEFINES/CONTAINS/IMPORTS), calls (CALLS/CONSTRUCTS/ASYNC_CALLS), types (EXTENDS/IMPLEMENTS/OVERRIDES/USES_TYPE/PARAM_TYPE/RETURNS_TYPE), fields (READS_FIELD/WRITES_FIELD/ACCESSES), service boundaries (HANDLES_ROUTE/HANDLES_GRPC/HANDLES_GRAPHQL/HANDLES_TRPC/HTTP_CALLS/EMITS/LISTENS_ON/HANDLES_TOOL), analysis (CONFIGURES/SIMILAR_TO/TESTS/RESOURCE_DEPENDS_ON/DATA_FLOWS/FILE_CHANGES_WITH).\n\nWHY MACHE'S VERSION WOULD BE BETTER, NOT DERIVATIVE:\n1. They state plainly that 'CALLS, HANDLES_ROUTE, and HANDLES_TOOL relations are heuristic' and 'Dependent counts are heuristic, not compiler / type-checker accurate'. A route binding carrying an ADR-0013 fidelity level (mention subset-of binding subset-of reachability) is strictly more useful than an untyped heuristic edge - a consumer can decide whether to gate on it.\n2. FCA (internal/lattice) can INFER boundary concepts from the data rather than hardcoding eight relation names. Their taxonomy is hand-authored and fixed; mache's could be derived. That is a differentiated path, not a catch-up.\n3. mache already projects both sides of several joins - terraform-schema and go-schema exist side by side, and mcp-registry-schema / mcp-schema model the MCP surface.\n\nSTART WITH HANDLES_TOOL: which function serves which MCP tool, joinable against mcp-registry-schema. Highest ecosystem value (rosary dispatches agents against mache's tool surface) and the smallest surface to get right.\n\nSCOPE NOTE: this is NOT a call to chase their language count. mache is a schema-driven projection engine - examples/ holds 20 schemas spanning CVE data (nvd, kev, trivy-ghsa), Notion, MCP registries, LLM conversation logs and audit records alongside source languages. entire-graph is exclusively a code-symbol graph. Language count is the right yardstick for their product, not this one.","status":"open","priority":2,"issue_type":"design","owner":"architect-agent","created_at":"2026-07-23T05:06:08+00:00","updated_at":"2026-07-23T05:06:08+00:00","created_by":"jamestexas","dependency_count":0,"dependent_count":0,"comment_count":2,"dependencies":[],"comments":[{"id":"446","issue_id":"mache-69162a","text":"RECON: read the entire-graph source (MIT, cloned at depth 1). How their relations actually work — this strongly favours doing it in mache.\n\nTHEIR RELATIONS ARE REGEX, NOT AST.\n- 599 regexp.MustCompile in internal/sem alone (parser.go 186, provider.go 175, types.go 99, then per-language files).\n- ZERO .scm files in the whole repo — they use no tree-sitter queries at all.\n- tree-sitter (smacker/go-tree-sitter, CGO) is used in parser.go for parsing/symbol extraction; the RELATION layer is text matching on top.\n- internal/sem/provider.go is 16,040 lines in one file.\n\nThe Go route detector, verbatim (provider.go:12422):\n handleFuncRe := regexp.MustCompile(`\\\\b(?:[A-Za-z_][A-Za-z0-9_]*\\\\.)?HandleFunc\\\\s*\\\\(\\\\s*([^,\\\\n]+)\\\\s*,\\\\s*(` + goHandlerExpr + `)\\\\s*\\\\)`)\n\nThat is a regex for the literal text 'HandleFunc(' with a comma-split argument capture. It cannot see through a variable holding the pattern, a mux wrapper, a method value, or a route table — and it explains why their own README says 'CALLS, HANDLES_ROUTE, and HANDLES_TOOL relations are heuristic'.\n\nWHY THIS MATTERS FOR MACHE: doing these relations over _ast (call_expression with resolved arguments) is not catching up, it is strictly more precise on the same relation names. Plus mache can attach an ADR-0013 fidelity level, which a text match cannot support at all. The vocabulary is worth borrowing; the technique is the thing mache deliberately removed (regex ratchet, internal/lint/regexp_ratchet_test.go).\n\nPractical read: the 30 relation names are a naming scheme to adopt, and the implementation is a SQL/selector exercise against tables mache already has — not a parser project.","author":"rsry-cli","created_at":"2026-07-23T05:09:22Z"},{"id":"452","issue_id":"mache-69162a","text":"Codex recon 2026-07-23: the strongest evidence for mache differentiation is in entire-graph internal/sem/provider.go. It declares HANDLES_ROUTE/HANDLES_TOOL/etc. as heuristic relation types, and the route extraction path is largely regex/context scanning: routeLiteralRe, routingCallRe, Go HandleFunc/router regexes, Python decorator regexes, Laravel/Rails/NestJS/Spring/C# route regexes, etc. That supports the bead premise: adopt typed relation vocabulary, but make mache edges carry fidelity/provenance instead of flattening them into one heuristic relation class.","author":"rsry-cli","created_at":"2026-07-23T05:20:00Z"}],"repo":"mache","scope":"","external_ref":null,"branch":null,"pr_url":null,"acceptance_criteria":"node_refs carries a relation kind; at least one service-boundary kind (HANDLES_TOOL or HANDLES_ROUTE) is populated and queryable, with its fidelity level recorded per ADR-0013 rather than asserted flat.","files":["internal/ingest/sqlite_writer.go","internal/graph/"],"test_files":[],"derived_from":[]} +{"schema_version":1,"id":"mache-697a0f","title":"Land open dependency update PR queue","description":"Review, update onto current main, validate, and merge Mache PRs #613 (docker/login-action 4.6.0), #614 (modernc.org/sqlite 1.56.0), and #615 (ohler55/ojg 1.28.3). Distinguish dependency regressions from stale/transient CI; PR #615 historical failure was a sum.golang.org HTTP 500 during Task installation, while all code test/lint/integration jobs passed. Close after each PR has current green checks and is merged. Verification: task ci && task install:verify","status":"open","priority":2,"issue_type":"chore","owner":"dev-agent","created_at":"2026-08-11T20:13:49+00:00","updated_at":"2026-08-11T20:13:49+00:00","created_by":"jamestexas","dependency_count":0,"dependent_count":0,"comment_count":0,"dependencies":[],"comments":[],"repo":"mache","scope":"","external_ref":null,"branch":null,"pr_url":null,"acceptance_criteria":"PRs #613, #614, and #615 are updated onto current main, reviewed, pass current required GitHub checks, and merge successfully; local verification passes with task ci and task install:verify.","files":[".github/workflows/release.yml","go.mod","go.sum"],"test_files":["Taskfile.yml"],"derived_from":[]} {"schema_version":1,"id":"mache-6ac5d6","title":"Two internal/leyline tests fail only under full-suite parallelism — they pass isolated and package-alone, so the flake looks like an unrelated diff's fault","description":"Observed repeatedly 2026-07-30 on macOS arm64 while landing unrelated work. Filed because the failure mode actively misattributes blame: the tests pass every way a reviewer would naturally check them, and fail only in the configuration that gates a push.\n\n## The two\n\n TestQueryBinaryVersion_HonorsProbeTimeout (internal/leyline/provenance_test.go:92)\n TestDiscoverOrStart_DaemonExitsDuringStartup (internal/leyline)\n\n## Evidence, and why it is confusing\n\n isolated (-run , no -race) PASS\n package alone, -tags boltdb -race PASS (7.9s)\n full `task check` (go test ./...) FAIL (observed 3x)\n\nAn external reviewer independently ran `TestQueryBinaryVersion_HonorsProbeTimeout` SIX times on a clean origin/main worktree — 3 isolated, 2 full-package, 1 full-package with -race — and got 6/6 PASS. They concluded my claim that it fails on clean main was \"unverified\". Both observations are correct: it passes package-alone and fails under `./...`. The distinguishing variable is CROSS-PACKAGE parallelism (go test runs packages concurrently up to GOMAXPROCS, and internal/leyline competes with the heavy cmd/ package plus everything else).\n\nThe reviewer also recorded run-to-run variance of 176.9s vs 8.9s for the same package, which is consistent with load sensitivity rather than a logic bug.\n\n## Mechanism (for TestQueryBinaryVersion_HonorsProbeTimeout)\n\nThe test writes a shell script that does `sleep 1` then echoes a version, and asserts:\n - probeTimeout = 50ms -> queryBinaryVersion returns \"\" (deadline fires)\n - probeTimeout = 5s -> returns the version\n\nThe second assertion is what fails, and the test takes ~5.05s — i.e. it consumed the ENTIRE 5s budget rather than the ~1s the script needs. A standalone Go program doing exactly this completes in 1.14s. So under load, a `sleep 1` subprocess plus process spawn is exceeding a 5-second deadline, which is a real scheduling-starvation signal, not a wrong assertion.\n\nNote this package spawns many leyline daemons and does HTTP downloads against local test servers in parallel, so it is a heavy neighbour to itself.\n\n## Why it matters beyond annoyance\n\nI hit this three times tonight while pushing unrelated changes. Each time the pre-push `task ci` failed on a test my diff did not touch, and the honest options were (a) investigate a red herring or (b) push with --no-verify. I chose (b) once, with reasoning recorded on the PR, which is exactly the erosion a flaky required gate produces: the gate stops being evidence and becomes a toll.\n\nIt also cost a reviewer real effort — they spent six runs disproving a claim that was true under a condition they had no reason to try.\n\n## Not yet determined\n\n- Whether it reproduces on Linux/CI. CI is green on main, so either the runners are less contended, or the flake is darwin-specific. Worth checking whether CI runs the suite with the same parallelism.\n- Whether raising probeTimeout is the right fix or papers over a real starvation problem. A test asserting a DEADLINE is inherently timing-coupled; the falsifiable half (50ms must fire) is sound, but the generous half may want a condition rather than a wall-clock budget, or `-p 1` / `t.Parallel()` discipline for this package.\n\nSuggest not simply bumping the timeout without first establishing why 5s is insufficient for a 1s sleep — that ratio is the actual finding.\n","status":"open","priority":2,"issue_type":"bug","owner":"scoping-agent","created_at":"2026-07-30T23:17:11+00:00","updated_at":"2026-07-30T23:17:11+00:00","created_by":"jamestexas","dependency_count":0,"dependent_count":0,"comment_count":1,"dependencies":[],"comments":[{"id":"588","issue_id":"mache-6ac5d6","text":"ROOT CAUSE FOUND — and it is NOT mache, NOT parallelism, and NOT a timeout that needs widening. This bead's original diagnosis (cross-package contention) is superseded.\n\n## Measured\n\nmacOS XProtect is scanning every newly-created executable SYNCHRONOUSLY on first exec:\n\n first exec of a new file 16.7s / 19.1s / 27.5s (varies)\n second exec, SAME file 0.007s\n new path, IDENTICAL content 54.6s\n\n`time` reports `0.00s user 0.00s system 0% cpu` — pure wall-clock wait, no CPU. Cached per-file after first exec; identical content at a new path pays again, so the cache is keyed on the file, not its bytes.\n\nReproduced outside any test, outside the repo, in both /tmp and $HOME — so it is system-wide, not a tmpdir or Go-toolchain artifact.\n\nCulprit processes running: XProtect.app, xprotectd, XProtectPluginService, XprotectService, XProtectBridgeService, and XProtectUpdateService. The presence of the update service suggests it was mid-definition-update, which is when this gets pathological.\n\n## What this explains\n\n- TestQueryBinaryVersion_HonorsProbeTimeout: no probe budget survives a 20s scan. Measured `queryBinaryVersion` on a plain `echo` script hanging to the deadline at exactly 2.001s, 3/3, IN ISOLATION with no load — which already falsified the contention theory before the cause was known.\n- TestDiscoverOrStart_DaemonExitsDuringStartup: spawns a fresh daemon binary.\n- Why an external reviewer got 6/6 PASS on the same machine: they ran when XProtect was idle, or hit warm inodes. Both observations were always correct; neither was about parallelism.\n- Every `go build` / `task build` output on this machine pays 20-50s on FIRST run. This is a whole-dev-loop tax, not a test problem.\n\n## Consequence for the fix\n\nI was mid-way through redesigning the test to split the two assertions so neither had to win a wall-clock race. That redesign is better test design and worth keeping on its own merits — but IT WOULD NOT HAVE FIXED THIS. Even a no-sleep `echo` script pays the scan. Shipping it as \"the fix\" would have been a plausible-looking change against the wrong cause, and the flake would have returned.\n\nDo NOT widen probeTimeout further. provenance.go's comment already records one widening (2s, mache-3a0da5) and the test had been widened to 5s; both lose to a 20-55s scan. Widening again encodes a machine pathology into a production constant.\n\n## Open question worth answering before acting\n\nIs a 2s production probeTimeout actually adequate? On a machine in this state, `queryBinaryVersion` against a freshly-downloaded leyline returns \"\" — provenance silently degrades. That is by design (\"best-effort, informational, never load-bearing\"), so it may be fine. But `EnsureCachedBinary` downloads a binary and then something runs it; if the first-exec scan is unbounded, more than provenance is affected. Worth measuring the real download-then-exec path rather than assuming.\n\n## For the operator, not the codebase\n\nNothing here is a mache defect. If this persists it is worth checking whether XProtect settles after its update completes, since a 20-55s penalty on every new binary makes the whole Go dev loop painful independent of this repo.","author":"rsry-mcp","created_at":"2026-07-30T23:56:46Z"}],"repo":"mache","scope":"","external_ref":null,"branch":null,"pr_url":null,"acceptance_criteria":"`task check` passes 5 consecutive times on macOS arm64 without either test failing, OR the two tests are made load-independent (condition-based rather than wall-clock-budget-based) and that change is shown to fail when the behaviour it guards is broken. Any timeout increase must be accompanied by a recorded explanation of why the previous budget was insufficient for a 1-second sleep, since that ratio is the finding rather than the symptom.","files":["internal/leyline/provenance_test.go","internal/leyline/socket_test.go","Taskfile.yml"],"test_files":[],"derived_from":[]} {"schema_version":1,"id":"mache-6b6da6","title":"End-to-end MCP tool harness with profiling + flamegraphs","description":"Surfaced post-T8 closeout (2026-05-09). With 16 MCP tools, 9 find_smells rules, 2 graph backends (MemoryStore + SQLiteGraph), and now a capnp event-log readthrough path, we have no single test that exercises the whole tool surface against a realistic workload AND captures performance profile data.\n\nToday's coverage:\n- Per-package unit tests (cmd/, internal/lsp/, internal/graph/, etc.) — pin individual contracts\n- Falsifiability A/B integration tests — gated, exercise the canonical-view layer with real LLO data\n- Per-rule SQL fixtures — isolated correctness, no breadth\n\nWhat's missing:\n- Single test invocation that exercises ALL 16 MCP tools against a realistic .db\n- Tool-level latency / allocation / CPU profile per tool, captured deterministically\n- Flamegraphs for hot paths (currently we only know hot paths via dogfood / production observability, both shallow)\n- Regression detection on perf changes — today a 10× slowdown in find_callers wouldn't fail any test\n- Cross-tool resource accounting (which tools share which graph operations under the hood?)\n\nThe user's framing: \"we need a proper e2e harness here that exercises all tools and profiles them. flamegraphs might be useful.\"\n\n## Concrete deliverables\n\n### Phase 1 — E2E exerciser\n\n`tests/e2e/all_tools_test.go` (new file or new directory):\n\nA single test that:\n1. Builds a realistic fixture .db once (mache itself indexed via leyline parse + lsp; checked into testdata/ or built in TestMain)\n2. Stands up `mache serve --http :PORT` (or in-process MCP server)\n3. Calls every registered MCP tool at least once with a meaningful argument:\n - `list_directory \"\"` → top-level\n - `read_file` on a known leaf\n - `find_callers` on a known token\n - `find_callees` on a known construct\n - `find_definition` for a token\n - `search` for a literal\n - `get_overview`\n - `get_communities`\n - `get_impact` for a node\n - `get_architecture`\n - `get_diagram`\n - `find_smells` for each registered rule\n - `get_type_info` (LLO-paired only; skip when not available)\n - `get_diagnostics` (same gate)\n - `semantic_search` (gated on daemon)\n - `write_file` (writable mount only)\n4. Records per-tool latency + memory delta to a structured artifact\n5. Optional: gated by env var, dumps pprof.cpu / pprof.heap profiles per tool\n\nPass criterion: every tool returns a non-error response (or a documented expected-skip), and the harness produces a per-tool perf manifest.\n\n### Phase 2 — Profiling hooks\n\nEach tool wrapped in a profiling decorator (test-only):\n\n```go\ntype toolProfile struct {\n Name string\n LatencyNS int64\n AllocBytes int64\n AllocCount int64\n CPUSamples string // base64 pprof\n HeapSamples string // base64 pprof\n}\n```\n\n`go test -tags profile ./tests/e2e/ -run TestE2E_AllTools -profile-out=tools.json`\n\nThe profile output feeds Phase 3.\n\n### Phase 3 — Flamegraph generation\n\nUse `go tool pprof` + `github.com/google/pprof/profile` to emit per-tool flamegraphs as SVG. Land a `task profile-tools` that:\n1. Runs Phase 1+2\n2. For each tool with significant CPU samples, writes `pprof/.cpu.svg` flamegraph\n3. Writes `pprof/INDEX.md` table linking each tool to its flamegraph + key metrics\n4. Optional: regression detection by comparing against a checked-in baseline manifest\n\nReference: brendangregg's flamegraph workflow + go-pprof's built-in -svg output. Both well-trodden.\n\n### Phase 4 — CI integration (optional, downstream)\n\nA workflow that runs Phase 3 on PRs and uploads the flamegraphs as artifacts. Could power \"performance regression\" labeling.\n\n## Acceptance\n\n- `task profile-tools` produces a complete per-tool perf manifest + flamegraphs\n- Every MCP tool exercised at least once with a non-trivial argument\n- Profile artifacts are reproducible (same .db, same arguments, similar profile shapes across runs)\n- Documented in docs/ARCHITECTURE.md or a new docs/PROFILING.md\n- README mentions the profile harness as part of \"Status\" or \"Docs\"\n\n## Why now\n\nT8 closeout puts mache in a state where the structural debt is paid down — every binding-fidelity ref flows through one capnp event log, every consumer reads canonical views, schema evolution is byte-stable. That's the right time to build the perf instrumentation layer because (a) the data path is finally stable, (b) future capnp fields and rules will compose into the same harness, (c) regressions in this layer matter more now that the surface is stabilizing.\n\nThe harness also doubles as living documentation: a test that calls every tool serves as the executable spec for \"what does mache do, and how fast?\"\n\n## Out of scope (file follow-ups if needed)\n\n- **Multi-language hetero corpus** — already filed as mache-d332b5 (cross-runtime fixtures). Different concern (correctness across languages, not perf across tools). The two harnesses can share fixture DBs but their assertions and gates differ.\n- **Production telemetry / OpenTelemetry hooks** — separate concern; this bead is for build-time profiling.\n- **Stress / load tests** — N concurrent clients, large input fixtures. Worth doing eventually but goes after the basic per-tool flamegraph harness.\n\n## Refs\n\nSurfaced at T8 closeout (2026-05-09). Pairs informally with mache-d332b5 (hetero corpus); together they exercise correctness × perf across the matrix.","status":"done","priority":2,"issue_type":"feature","owner":"scoping-agent","created_at":"2026-07-23T04:49:13+00:00","updated_at":"2026-07-23T04:49:13+00:00","created_by":"jamestexas","dependency_count":0,"dependent_count":0,"comment_count":3,"dependencies":[],"comments":[{"id":"327","issue_id":"mache-6b6da6","text":"**Phase 1 in flight** — PR #360 (https://github.com/agentic-research/mache/pull/360).\n\nShipped:\n- `TestE2E_AllMCPTools` exercises 16 read-surface MCP tools\n- 4-package Go fixture (auth/billing/util/main with cross-package calls)\n- Per-tool latency + alloc-delta capture via `runtime.MemStats`\n- ok / skipped / tool_error classification (skipped is documented behavior, not failure)\n- Stdout summary table + JSON manifest at `$E2E_PROFILE_OUT`\n- `task profile-tools` with manifest pinned to `{{.ROOT_DIR}}/.e2e/`\n\nFirst-run baseline (in-process MemoryStore, no leyline):\n- 13/16 ok, 3/16 skipped (LSP/daemon dependencies)\n- Runtime ~0.6s, per-tool latency <1ms, alloc 4-62 KB\n\nPhases 2-4 stay under this bead:\n- Phase 2: pprof CPU/heap per tool → `.e2e/.cpu.pprof`\n- Phase 3: SVG flamegraphs via `go tool pprof -svg` + INDEX.md\n- Phase 4: regression detection vs checked-in baseline\n\nOnce #360 merges, will queue phase 2 immediately — that's where the user's \"flamegraphs would be useful\" lands.","author":"rsry-mcp","created_at":"2026-07-23T04:49:13Z"},{"id":"328","issue_id":"mache-6b6da6","text":"**Phase 2 in flight** — PR #361 (https://github.com/agentic-research/mache/pull/361).\n\nPhase 1 (#360) merged at 16:13Z. Phase 2 extends the harness:\n- `runtime/pprof` CPU + heap capture per tool, gated on `E2E_CAPTURE_PPROF=1`\n- 32 profile files per run (16 tools × CPU + heap)\n- `task profile-tools-pprof` variant\n- Default 500 iterations to cross the 100Hz sampler threshold (tools are <1ms each)\n\nFirst-run signal:\n- Heap profiles immediately actionable: 4.6MB inuse on `get_architecture` with attribution to `runtime.mallocgc`, `strings.Fields`, `jsonschema/v6.newSchema`, `hclsyntax.init.0`, `fmt.Sprintf`\n- CPU profiles record samples (~14% duty cycle) but background `runtime.kevent` from the file watcher dominates — phase 3 will apply `-focus=` to filter\n\nPhase 3 next once #361 lands: SVG flamegraph generation via `go tool pprof -svg` + INDEX.md linking each tool to its profiles + flamegraphs.","author":"rsry-mcp","created_at":"2026-07-23T04:49:13Z"},{"id":"329","issue_id":"mache-6b6da6","text":"Closing — phases 1-3 all shipped 2026-05-09:\n\n- Phase 1 (harness): PR #360 (2026-05-09 16:13)\n- Phase 2 (pprof CPU + heap): PR #361 (2026-05-09 17:02)\n- Phase 3 followup (DefsMap/RefsMap memoize): PR #362 (2026-05-09 17:23)\n\nCodebase: `cmd/all_tools_e2e_test.go` exists and is exercised by `task profile-tools`. `docs/ARCHITECTURE.md` records \"(phases 1-3 shipped)\" in the e2e harness section.\n\nIf new instrumentation work is needed, file a fresh bead (e.g. mache-be8090 already covers rich-fixture mode); this umbrella has fulfilled its scope.","author":"rsry-mcp","created_at":"2026-07-23T04:49:13Z"}],"repo":"mache","scope":"","external_ref":null,"branch":null,"pr_url":null,"acceptance_criteria":"","files":["tests/e2e/","Taskfile.yml","docs/PROFILING.md","docs/ARCHITECTURE.md"],"test_files":["tests/e2e/all_tools_test.go","tests/e2e/profile_test.go","tests/e2e/testdata/"],"derived_from":[]} {"schema_version":1,"id":"mache-6bb5e7","title":"Auto-detect and project HCL, YAML, shell, HTML, CSS via tree-sitter","description":"Mache only projects TypeScript for the rig repo. HCL (tofu/*.tf), YAML (fly.toml, cloudflared.yml, Taskfile.yml), shell (entrypoint.sh, scripts/), HTML (site/index.html), and CSS (site/styles.css) are visible as raw files in _project_files but have no language-aware projection.\\n\\nTree-sitter grammars exist for all of these: tree-sitter-hcl, tree-sitter-yaml, tree-sitter-bash, tree-sitter-html, tree-sitter-css. Ley-line already has tree-sitter projection support (rs/crates/ts/) — mache should be auto-detecting the language and loading the appropriate grammar.\\n\\nExpected behavior: mache projects terraform resources, YAML keys, shell functions, HTML elements, CSS selectors as navigable definitions — not just TypeScript.","status":"done","priority":1,"issue_type":"bug","owner":"dev-agent","created_at":"2026-07-23T04:49:13+00:00","updated_at":"2026-07-23T04:49:13+00:00","created_by":"","dependency_count":0,"dependent_count":0,"comment_count":1,"dependencies":[],"comments":[{"id":"141","issue_id":"mache-6bb5e7","text":"Fixed: added preset schemas for all 18 compiled-in tree-sitter languages. Every language now has a curated schema (functions, classes, imports, etc.) instead of falling through to FCA inference. Also improved: Rust (added use imports), Terraform (added terraform{} and moved{} blocks). Wired sourceCodePresets for all 18 languages.","author":"rsry-mcp","created_at":"2026-07-23T04:49:13Z"}],"repo":"mache","scope":"","external_ref":"AGE-606","branch":null,"pr_url":null,"acceptance_criteria":"","files":["cmd/mache/main.go"],"test_files":[],"derived_from":[]} @@ -316,6 +318,7 @@ {"schema_version":1,"id":"mache-9108a1","title":"Standalone find_callers/find_callees underperform on Rust vs Go — tree-sitter extraction tier is Go-tuned; ley-line _ast + LSP enrichment is the uniform path","description":"## Real-world observation (2026-06-25)\n\nRunning mache on a Rust codebase (lectio, ~/github/jamestexas/lectio), the user reports:\n\n- **Weak:** `find_callers` / `find_callees` \"not useful in Rust vs Go\" — suspected to be our extraction impl, not a fundamental limit.\n- **Shines:** Go codebases; AND **any** codebase *after* ley-line parse runs — LSP enrichment then makes `find_callers` accurate and lights up `get_type_info` / `get_diagnostics`.\n\n## The seam (same family as mache-5bb181, the kind-filter bug)\n\nmache has two backend tiers, and quality is uneven across them:\n\n1. **Standalone / heuristic tier** — tree-sitter SitterWalker (CGO) extracts calls/refs via per-language queries. The Go call-extraction queries (`engine_languages.go`, `sitter_walker.go`, `ast_walker_calls.go`) are mature; Rust's qualified-call / method / trait-dispatch resolution is weaker, so caller/callee edges are sparse or wrong on Rust. This tier is effectively **Go-tuned**.\n2. **Ley-line / semantic tier** — once ley-line emits `_ast` (kinds) + `_lsp_refs`/`_lsp_defs`/`_lsp_hover`/`_lsp` tables, `find_callers` supplements with compiler-grade LSP refs (`serve_lsp.go`) and type/diagnostics tools work. This tier is **uniform across languages** because it's the compiler's view, not tree-sitter heuristics.\n\nThe kind-filter bug (mache-5bb181) was one symptom of this split (path-segment vs `_ast.node_kind`). The Rust find_callers weakness is another symptom: the heuristic tier is language-uneven; the semantic tier makes mache good everywhere. The CGO-removal campaign's thesis — make ley-line `_ast`+`_lsp` the universal load-bearing path and retire the Go-tuned SitterWalker — is precisely the resolution.\n\n## What this research bead should establish\n\n1. **Quantify the gap**: on a fixed Rust corpus (lectio), measure find_callers/callees edge recall (a) standalone tree-sitter vs (b) ley-line `_ast`+`_lsp`. Use a hand-labeled set of known call edges as ground truth (match-truth-not-model discipline).\n2. **Locate the Rust-specific extraction holes** in the tree-sitter call queries (method calls `recv.method()`, associated functions `Type::assoc()`, trait-method dispatch, macro-call edges) — are they missing from the Rust query set, or mis-resolved?\n3. **Decide the disposition**: fix the Rust tree-sitter queries to close the standalone gap, OR formally gate find_callers/callees quality on ley-line availability and document standalone-Rust as best-effort (consistent with the CGO-removal endgame). The latter is likely correct — don't pour effort into a tier being retired.\n\n## Acceptance\n\nA short findings doc with the measured standalone-vs-leyline edge-recall gap on Rust, an enumeration of the specific Rust call-edge classes the tree-sitter tier misses, and a recommendation (fix-queries vs gate-on-leyline) with rationale tied to the CGO-removal campaign.\n\n## Cross-refs\n\n- mache-5bb181 (kind filter: path-segment vs `_ast` — same two-tier seam)\n- mache-8fab5d (get_architecture noise — ranks raw refs incl. external tokens; another facet of \"heuristic tier surfaces non-defs\")\n- ADR-0012 (CGO-removal migration — make `_ast`/`_lsp` universal)\n- LSP enrichment path: `cmd/serve_lsp.go`, `_lsp*` tables produced by ley-line-open's lsp crate","status":"open","priority":2,"issue_type":"research","owner":"architect-agent","created_at":"2026-07-23T04:49:13+00:00","updated_at":"2026-07-23T04:49:13+00:00","created_by":null,"dependency_count":0,"dependent_count":0,"comment_count":4,"dependencies":[],"comments":[{"id":"242","issue_id":"mache-9108a1","text":"## Measurement scoping pass (2026-06-25) — instruments + gotchas found\n\nBuilt the leyline `.db` for lectio (`leyline parse .` → `~/github/jamestexas/lectio/output.db`) and started the standalone-vs-leyline comparison. Three blockers/findings that the real measurement run must account for:\n\n### 1. `mache serve ` is NOT a pure-tree-sitter baseline\nServing the source dir auto-starts the leyline daemon (backend=auto prefers leyline when on PATH). On this machine the daemon socket failed to appear within 5s (`leyline daemon started but socket .../default.sock did not appear within 5s`) → it fell back to in-process tree-sitter, which DID return Rust callers for `run` (tree-sitter node paths). So: (a) tree-sitter does extract Rust refs; (b) the daemon-startup flakiness is the same lifecycle fragility as the \"/mcp conn refused\" thread (mache-823d91) and will confound any measurement that relies on the live daemon. The LSP tier needs either the daemon working OR a pre-baked `_lsp` db.\n\n### 2. `mache build --backend tree-sitter` emits 0 refs (instrument gap)\n`mache build --backend tree-sitter ~/github/jamestexas/lectio out.db` finished in 67ms with node_refs=0, node_defs=0 — the auto-FCA inference path does not apply a code-aware projection to a source dir, so no construct/ref extraction happens. To get a clean tree-sitter baseline `.db` for SQL-level recall comparison, the build must be driven with a code schema (the serve path's projection), not FCA. This is a prerequisite to the measurement and possibly a `mache build` bug worth its own note.\n\n### 3. lectio output.db (leyline `parse` tier) inventory\n`_ast`=224,915 rows, `node_refs`=20,569, `node_defs`=1,711. No `_lsp*` tables — `leyline parse` produces the `_ast` tier only; the LSP tier requires a separate `leyline lsp` enrichment pass (the `lsp` subcommand exists: \"Spawn a language server, collect symbols + diagnostics, and write a .db\").\n\n### Resume plan (clean run, own session)\n1. Pre-bake all three tiers as static `.db`s so the measurement is daemon-independent: (a) tree-sitter baseline via a code-schema-driven build, (b) `output.db` (already have), (c) `leyline lsp` enrichment → `_lsp_refs`.\n2. Establish ground-truth: hand-label ~15-20 known call edges in lectio (free fns + impl methods + trait dispatch + macro calls) to score recall against, not just raw counts.\n3. Score find_callers recall per tier on the ground-truth set; report the gap + the specific Rust edge classes tree-sitter misses (the bead's core deliverable).\n\nRaw counts directionally: leyline `_ast` captured 20,569 ref edges on 94 Rust files; need the tree-sitter baseline count to quantify the gap.","author":"rsry-mcp","created_at":"2026-07-23T04:49:13Z"},{"id":"243","issue_id":"mache-9108a1","text":"## Benchmark framing (per user): this IS a mache differential benchmark — PostHog shadow-replay method\n\nThe right frame is PostHog's SQL-parser validation (the blog behind mache-7qf0): instead of hand-labeling ground truth, run `find_callers` for the same symbols across extraction tiers and measure where the weaker tier diverges from the compiler-grade reference (LSP). The differential IS the test. Tiers: (1) mache in-process tree-sitter, (2) leyline `parse` grammar-refs (`_ast`/`node_refs`), (3) leyline `lsp` compiler-refs (`_lsp_refs`) = reference.\n\n## LSP tier is BLOCKED on workspace indexing (de-risked via pilot)\n\n`leyline lsp --server rust-analyzer --input --merge-db output.db` ran in ~1s but returned **0 defs/0 hovers/0 refs** (\"32 symbols matched to AST, enrichment: 0 refs\"); only a `_lsp` (diagnostics) table was written, no `_lsp_refs`/`_lsp_defs`. Cause: rust-analyzer resolves references only with the whole-workspace project model (cargo metadata + crate-graph index); per-file invocation collects document symbols but cannot resolve cross-file refs. So the LSP reference tier needs either (a) `leyline lsp` to drive whole-project ref collection, or (b) a harness that boots rust-analyzer against the workspace root and waits for indexing. This is a real setup task (and possibly a leyline-side limitation/bead) — not a quick win. (Note: `rust-analyzer --version` reports `0.0.0` here, worth sanity-checking the binary.)\n\n## tree-sitter baseline tier also blocked\n`mache build --backend tree-sitter out.db` → 0 node_refs/0 node_defs (auto-FCA path applies a data schema, not the serve path's code projection). Need a code-schema-driven build to get a clean in-process baseline `.db`.\n\n## Directional result available NOW — grammar tier (output.db) name-presence coverage on Rust\n\nFor fn/method defs, fraction whose token appears as a ref (a NAME-match upper bound, not target-resolved):\n\n| kind | defs | with ≥1 ref-token | coverage |\n|------|------|-------------------|----------|\n| free function | 1049 | 355 | **33.8%** |\n| method | 316 | 235 | **74.4%** |\n\nTotals: 1365 fn/method defs, 1206 distinct ref tokens, 20,569 ref edges. CAVEAT: `node_refs` matches by bare token (name), so \"with caller\" over-counts via name collisions (any `foo()` matches any `foo` def) AND can't see calls the grammar missed — it's an UPPER bound on recall. The true recall gap (and the free-fn-vs-method Rust weakness the user observed) requires the LSP tier to resolve actual call targets, which is exactly the blocked piece. Even so, the 33.8% free-fn vs 74.4% method split is a real signal that grammar-tier free-function call resolution is the weak spot.\n\n## Resume requirements (dedicated session)\n1. Unblock LSP tier: drive rust-analyzer with the workspace project model (verify the rust-analyzer binary first). File a leyline bead if `leyline lsp` can't do whole-project ref collection.\n2. Unblock tree-sitter baseline: code-schema-driven `mache build`.\n3. Then: differential `find_callers` per def symbol across all 3 tiers, LSP as reference, report per-Rust-edge-class recall (free fn / impl method / trait dispatch / macro).","author":"rsry-mcp","created_at":"2026-07-23T04:49:13Z"},{"id":"244","issue_id":"mache-9108a1","text":"## Grammar-tier baseline result (lectio output.db, static SQL — no daemon)\n\nTwo clean measurements from the leyline `parse` tier on lectio (94 Rust files, 20,569 ref edges, 1,452 distinct def tokens):\n\n### 1. find_callers name-coverage by construct kind (caller present at all)\n| kind | defs | with ≥1 ref-token | coverage (upper bound) |\n|------|------|------|------|\n| free function | 1049 | 355 | 33.8% |\n| method | 316 | 235 | 74.4% |\n\n### 2. Name-collision / disambiguation failure rate (the upper-bound confound, quantified)\n- 74 / 1452 distinct def tokens (**5.1%**) are ambiguous (same name, >1 definition).\n- **13.8% of all 20,569 ref edges (2,833) point at an ambiguous multi-def token** — for ~1 in 7 call edges the grammar tier's name-match find_callers CANNOT tell which definition is the real target.\n- Worst offenders are textbook Rust: `new` (23 defs — `Type::new()` constructors), `tests` (48), `sweep` (19), `resolve` (7), `label`/`source_id`/`uri_prefixes` (18 each).\n\n### Interpretation\nThe grammar tier resolves callers by bare token name, so (a) it over-counts (any `foo()` matches any `foo` def — the 33.8/74.4% are upper bounds), and (b) for 13.8% of edges it's genuinely ambiguous. This is precisely the gap compiler-grade LSP resolution closes: `_lsp_refs` resolve the actual target def, eliminating the name-collision ambiguity. **This number (13.8% ambiguous edges) is the concrete size of the prize for the LSP tier** — and the differential measurement, once the LSP tier is unblocked, should show the LSP tier correctly splitting those 2,833 edges across the right targets.\n\n### Tree-sitter baseline tier — re-confirmed blocked\nForcing mache's in-process tree-sitter (to compare implementation vs leyline parse) is defeated by a `leyline` binary in `~/.mache/bin/` that mache falls back to even with `~/.local/bin` removed from PATH; the forced-serve probe broke (BrokenPipe). Needs either temporarily relocating that fallback binary or a working `mache build --backend tree-sitter` (which currently emits 0 refs via the FCA path). Both remain dedicated-setup items.","author":"rsry-mcp","created_at":"2026-07-23T04:49:13Z"},{"id":"245","issue_id":"mache-9108a1","text":"## LSP-tier blocker moved one layer deeper (daemon path, not the one-shot CLI)\n\nProbed the daemon-based LSP enrichment (get_type_info with file=, the path the one-shot `leyline lsp` CLI is NOT). Findings, in order of discovery:\n\n1. First it failed with \"leyline daemon ... did not appear within 5s\" — but that was the stale-`~/.mache`-scratch startup bug (now isolated, see mache-823d91), NOT rust-analyzer.\n2. With fresh `~/.mache` scratch the daemon binds in <1s, and enrichment proceeds — then fails with: **`LSP auto-enrichment failed: tool lsp: unknown op: tool`**.\n\nSo the daemon-LSP path is now blocked at the PROTOCOL layer: mache sends an LSP-enrichment op the leyline 0.5.1 daemon doesn't recognize (\"unknown op: tool\"). We still never reached rust-analyzer — so the workspace-indexing question remains unanswered.\n\nThis smells like mache↔daemon protocol/version skew (the exact class mache-8kif's version-assert-on-connect targets): mache 0.9.0's LSP-enrichment op vs what leyline 0.5.1's daemon implements. Next step to unblock the bench LSP tier: determine whether (a) the op name/shape mache sends matches leyline 0.5.1's daemon op set (mache-side or leyline-side fix), or (b) a newer leyline implements it. Until resolved, the headline differential (grammar 13.8% ambiguous edges → LSP resolves them) can't run via the daemon path.\n\nNet: the bench LSP tier is gated by, in sequence: daemon-startup reliability (823d91, mostly mache-side) → the `unknown op: tool` protocol mismatch (mache↔leyline) → finally rust-analyzer workspace indexing (untested, leyline-side). Each is a real gate; the user's \"once daemon reliability lands, bench works\" is true for the FIRST gate but two more remain.","author":"rsry-mcp","created_at":"2026-07-23T04:49:13Z"}],"repo":"mache","scope":"","external_ref":null,"branch":null,"pr_url":null,"acceptance_criteria":"","files":["internal/ingest/sitter_walker.go","internal/ingest/ast_walker_calls.go","internal/ingest/engine_languages.go","cmd/serve_handler_find_callers.go","cmd/serve_lsp.go"],"test_files":[],"derived_from":[]} {"schema_version":1,"id":"mache-912a77","title":"Embed trigger fires before lazy graph materialized — zero content pushed to ley-line","description":"E2E test showed: mache serve starts → embed trigger goroutine fires during sync.Once → but the graph walk produces zero nodes because the lazy graph hasn't been materialized yet (first tool call triggers materialization).\n\nThe embed_content → semantic_search pipeline itself works (manually tested with 3 test nodes, correct ranking). But the automatic trigger on serve startup races with graph initialization.\n\nFix: move the embed trigger to fire AFTER the first tool call materializes the graph, or after a deferred init timer (e.g., 5s after serve starts). The trigger should not fire during sync.Once — it should fire when the graph is ready.\n\nFound during rig session e2e test of leyline serve --embed + mache serve against mache's own source.\n\nRelates to: ley-line embed_content/semantic_search ops (proven working), kiln integration (leyline serve --embed --vec-db).","status":"done","priority":1,"issue_type":"bug","owner":"dev-agent","created_at":"2026-07-23T04:49:13+00:00","updated_at":"2026-07-23T04:49:13+00:00","created_by":"","dependency_count":0,"dependent_count":0,"comment_count":1,"dependencies":[],"comments":[{"id":"138","issue_id":"mache-912a77","text":"Already fixed. lazyGraph.get() uses embedOnce.Do to trigger embedding only AFTER init() completes successfully. The graph is fully materialized before TriggerEmbedding runs. This was wired in the serve_registry.go refactor (PR #120+).","author":"rsry-mcp","created_at":"2026-07-23T04:49:13Z"}],"repo":"mache","scope":"","external_ref":"AGE-605","branch":null,"pr_url":null,"acceptance_criteria":"","files":["internal/graph/lazy.go","internal/embed/trigger.go"],"test_files":[],"derived_from":[]} {"schema_version":1,"id":"mache-91956b","title":"lazyGraph silently dropped the dbPathProvider capability — serve-mode find_callers lost lsp_refs and MCP find_smells read fewer refs than the CLI over the same .db","description":"## Mechanism\n\nHandlers reach optional graph capabilities by type assertion — `g.(refsQuerier)`, `g.(dbPathProvider)`, and eight more. In serve mode `g` is ALWAYS a `*lazyGraph`, never the `SQLiteGraph`/`WritableGraph` underneath it. `lazyGraph` forwards each capability explicitly, and it did not forward `DBPath()`.\n\nA missed forwarder is not a compile error and not a test failure. The assertion returns `ok=false` and the caller takes its \"backend doesn't support this\" branch. The feature just disappears for everyone reaching mache through `mache serve` — which is every MCP client.\n\n## Two consumers were affected\n\n1. `queryLSPRefs` (`cmd/serve_lsp.go:611`) — the capnp source needs `dp.DBPath()`. With the assertion failing it fell through to `queryLSPRefsLegacy`, the `_lsp_refs` SQL path that mache-6bd4d8 retired as the consumer-side contract. On any .db built after LLO T8.2 — which emits the sibling `.bindings.capnp` log instead — `find_callers` lost its `lsp_refs` supplement entirely.\n\n2. `ensureSmellQueryContext` (`cmd/smell_findings.go:46`) — skipped `LoadCapnpBindings`, so the `_capnp_binding_refs` TEMP table stayed empty and the `v_refs` UNION arm over it contributed nothing. **MCP `find_smells` therefore saw a strictly smaller ref set than the `find-smells` CLI**, whose `dbQuerier` does implement `DBPath`. Same rules, same .db, different answers — with the CLI being the one the smell baseline is generated from.\n\n## Why the tests did not catch it\n\nEvery existing `queryLSPRefs` test passes a querier that already implements `dbPathProvider` (`FixtureDB`, `sqlDBQuerier`, `dbQuerier`). The capnp path was well covered; the serve-mode path that reaches it had no coverage at all. The regression test added here calls `queryLSPRefs` with the `*lazyGraph` a handler actually holds.\n\n## Fix + guard\n\n`lazyGraph.DBPath()` delegates to the inner graph, mirroring `QueryRefs`/`RefsMap`. `\"\"` stays the documented no-source sentinel — both `readLSPRefsFromCapnp` and `LoadCapnpBindings` no-op on it.\n\nMore important than the one-line fix: compile-time assertions now pin the entire capability set (`var _ refsQuerier = (*lazyGraph)(nil)` and nine more, plus `graph.Graph`). Adding an opt-in interface without a lazyGraph forwarder is now a build failure instead of a silent runtime degradation. That is the actual defect class; DBPath was one instance of it.","status":"open","priority":1,"issue_type":"bug","owner":"scoping-agent","created_at":"2026-08-06T02:18:27+00:00","updated_at":"2026-08-06T02:18:27+00:00","created_by":"jamestexas","dependency_count":0,"dependent_count":0,"comment_count":0,"dependencies":[],"comments":[],"repo":"mache","scope":"","external_ref":null,"branch":null,"pr_url":null,"acceptance_criteria":"`go test ./cmd/ -run 'TestLazyGraph_|TestQueryLSPRefs_ThroughLazyGraph'` passes, including a test that reads a sibling .bindings.capnp log through a *lazyGraph whose fixture has no _lsp_refs table (so the result can only have come from capnp). `go build ./...` fails if any opt-in interface loses its lazyGraph forwarder.","files":["cmd/serve_registry.go"],"test_files":["cmd/serve_registry_dbpath_test.go"],"derived_from":[]} +{"schema_version":1,"id":"mache-91beb0","title":"Pin and validate ley-line-open v0.18.2","description":"Advance Mache from the v0.18.0 LLO binary/schema-client pin to the official v0.18.2 release that contains Terraform address-ref extraction. Source all platform SHA-256 digests from the published GitHub release, keep the no-CGO architecture, and validate the installed Mache path populates node_refs.token and supports callers/impact/dataflow for Terraform address references.","status":"closed","priority":1,"issue_type":"task","owner":"dev-agent","created_at":"2026-08-11T20:31:18+00:00","updated_at":"2026-08-11T21:41:00+00:00","created_by":"jamestexas","dependency_count":0,"dependent_count":0,"comment_count":2,"dependencies":[],"comments":[{"id":"701","issue_id":"mache-91beb0","text":"Pinned work is on isolated branch chore/llo-v0.18.2-pin. Confirmed official release v0.18.2 at tag commit 75e3af6aecf3287eb7adc0610399ff923256ee7e: binary 0.18.2, schema handshake/client 0.18.1, wire major 1, compat floor 0.6.0, IR merkle-ast-v2. Mache's live release download/hash/version gate passes for the published darwin-arm64 asset. TDD regression first failed against cached v0.18.0 with an empty node_refs table, then passed against SHA-verified cached v0.18.2 with exactly env:bucket_name, env:region, and mod:./modules/logging and non-empty find_callers results for every token. Upstream release asset verification confirmed every asset checksum, but its nested Go-module postflight incorrectly requested schema @v0.18.2; filed ley-line-open-b52c9d. Mache correctly pairs binary v0.18.2 with published Apache schema module v0.18.1.","author":"rsry-mcp","created_at":"2026-08-11T21:11:11Z"},{"id":"703","issue_id":"mache-91beb0","text":"Completed in PR #619, squash merge 7ad18dca8edad22da2909e6e62bb8ad7d498c5c3. Every hosted gate passed, including macOS/Linux tests, full-repository integrations, cache round trips, install verification, smell/lint/docs gates, and CodeQL. Mache main now pins official LLO v0.18.2 while retaining schema client v0.18.1, and the Terraform regression proves populated address-ref tokens and non-empty callers.","author":"rsry-mcp","created_at":"2026-08-11T21:40:51Z"}],"repo":"mache","scope":"","external_ref":null,"branch":null,"pr_url":null,"acceptance_criteria":"Mache pins official ley-line-open v0.18.2 consistently across binary version, all release asset SHA-256 digests, schema client dependency, fixture schema version, generated server metadata, and pin tests; task leyline:verify-release passes against published artifacts; task install provisions the v0.18.2 binary; an installed Mache build over a Terraform fixture produces non-empty node_refs.token values and correct find_callers/get_impact/dataflow results; full task ci passes; changes are committed, pushed, and merged.","files":["internal/leyline/socket.go","internal/leyline/binary_pin.go","internal/leyline/binary_pin_test.go","internal/fixturedb/schema_leyline.go","go.mod","go.sum","server.json"],"test_files":[],"derived_from":[]} {"schema_version":1,"id":"mache-92cdbc","title":"Extract leyline pin into a CGO-free leaf package; dedupe the two regex-over-source test helpers","description":"Two test helpers (internal/ingest/leyline_ast_test_helper_test.go, internal/lattice/infer_astdb_parity_test.go) recover the pinned leyline version by running regexp over internal/leyline/socket.go SOURCE TEXT — a regex-is-a-smell (see feedback_regex_is_a_smell). mache-e34aec exported leyline.BinaryVersion, which tempts a direct import, but there is a real -tags leyline CGO trap:\n\ninternal/leyline/client.go is //go:build leyline + import \"C\" + #cgo LDFLAGS: -lleyline_fs. Under -tags leyline, importing package leyline (for ANY symbol, incl. the pure-Go BinaryVersion const in socket.go) pulls client.go and requires the Rust staticlib libleyline_fs.a. Under default tags it's excluded and safe.\n\n- ingest: already imports leyline (address_refs_test.go) + cmd/* import it non-test, so the regex protects nothing there; a direct import is safe.\n- lattice: imports leyline NOWHERE today. A direct import would newly require libleyline_fs.a for 'go test -tags leyline ./internal/lattice/' — a real regression.\n\nClean fix: move the pin const to a leaf package with only pure-Go files (no build-tagged siblings), e.g. internal/leyline/pin { const Version = \"v0.8.0\" }; socket.go aliases it (const leylineBinaryVersion = pin.Version). A pure-Go leaf is CGO-free under ALL tags, so every consumer (socket.go, server-json-gen, both test helpers) imports it without staticlib risk. Then dedupe the two near-identical resolvePinnedLeyline* helpers into one shared helper. Bonus: lets server-json-gen import the leaf instead of package leyline, decoupling its -tags leyline build too.\n\nDeferred from mache-e34aec (kept the regex to avoid the lattice regression under a rushed one-liner).","status":"done","priority":3,"issue_type":"task","owner":"dev-agent","created_at":"2026-07-23T04:49:13+00:00","updated_at":"2026-07-23T04:49:13+00:00","created_by":"jamestexas","dependency_count":0,"dependent_count":0,"comment_count":3,"dependencies":[],"comments":[{"id":"411","issue_id":"mache-92cdbc","text":"SIMPLIFIED by mache-bd7686 (client.go deletion). The whole point of the leaf-package workaround was to avoid pulling client.go's CGO when importing package leyline under -tags leyline. With client.go DELETED, 'go build -tags leyline ./internal/leyline/' builds with no libleyline_fs — importing package leyline is CGO-free under ALL tags. So the regex test helpers (internal/ingest, internal/lattice) can now just import leyline.BinaryVersion DIRECTLY, no leaf package needed. Rescope this bead to: 'replace the two regexp-over-socket.go helpers with a direct leyline.BinaryVersion import + dedupe into one shared helper.'","author":"rsry-cli","created_at":"2026-07-23T04:49:13Z"},{"id":"412","issue_id":"mache-92cdbc","text":"DONE (2026-07-22): both regexp resolvers removed. internal/ingest/leyline_ast_test_helper_test.go + internal/lattice/infer_astdb_parity_test.go now call leyline.ResolveBinary(false) directly (the production PATH→~/.mache/bin→verified-pin resolver) — no regex, no hand-rolled candidate loop, no socket.go source-scraping. Both regexes gone (const-read AND --version-parse). Enabled by the client.go deletion (mache-bd7686) making package leyline CGO-free under all tags. PLUS added a self-contained guard: internal/lint/regexp_ratchet_test.go — a structural (go/ast) ratchet that freezes the 14 existing regexp users and FAILS on any new one; runs via task test → task ci → pre-push, no manual cmd. Follow-ups filed: reduce the 4 production regexps; minimal-surface-area-dep detector; curated .golangci.yml.","author":"rsry-cli","created_at":"2026-07-23T04:49:13Z"},{"id":"413","issue_id":"mache-92cdbc","text":"Auto-closed by rsry close-merged --local: PR #542 merged","author":"rosary","created_at":"2026-07-23T04:49:13Z"}],"repo":"mache","scope":"","external_ref":null,"branch":null,"pr_url":null,"acceptance_criteria":"The leyline version pin lives in a leaf package with no build-tagged/CGO files (e.g. internal/leyline/pin, const Version). socket.go, server-json-gen, and both test helpers reference it. The 'const leylineBinaryVersion' regexp.MustCompile is gone from both test files. One shared test helper replaces the two near-identical resolvePinnedLeyline* funcs. 'go test -tags leyline ./internal/lattice/' still builds without libleyline_fs.a.","files":["internal/leyline/socket.go","tools/server-json-gen/main.go"],"test_files":["internal/ingest/leyline_ast_test_helper_test.go","internal/lattice/infer_astdb_parity_test.go"],"derived_from":[]} {"schema_version":1,"id":"mache-949de6","title":"Delete drift_doc_outdated_count placeholder smell rule (14->13); it structurally can't fire","description":"drift_doc_outdated_count is a v1 placeholder whose Query returns zero rows (WHERE 1=0). Its own description names '17 MCP tools should match a SQL ground-truth query' as the motivating example — the exact drift mache-e34aec found and fixed with a Go test — yet the rule never could fire.\n\nDeeper: even fully implemented, a smell rule runs SQL over the PROJECTED GRAPH, so for cross-artifact/runtime claims (tool counts, version pins) it can only match the MODEL (source text) not the TRUTH (live registry/buildinfo) — the match-truth-not-model trap (project_smell_enforcement). Those invariants belong in Go tests (feedback_prefer_go_tests_over_shell_and_smell_rules), which mache-e34aec demonstrated (TestREADMEToolMatrixMatchesRegistry pins to a live tools/list).\n\nA gate that can't fire is worse than none: it reads as covered. Delete it. This drops find_smells from 14 -> 13 rules, a user-visible count hand-stated in README/ROADMAP (and my own mache-e34aec README table says '14 rules') — so it's a small standalone PR, not a silent edit. Note the related follow-up mache-d07b38 (generate such counts from ground truth).","status":"open","priority":2,"issue_type":"task","owner":"dev-agent","created_at":"2026-07-23T04:49:13+00:00","updated_at":"2026-07-27T19:26:12+00:00","created_by":"jamestexas","dependency_count":0,"dependent_count":0,"comment_count":1,"dependencies":[],"comments":[{"id":"517","issue_id":"mache-949de6","text":"Contradiction resolved in this bead's favour — mache-faacf3 (implement the same rule) closed as superseded, with the reasoning recorded there.\n\nTwo changes here:\n- **P3 → P2.** This is small, standalone, and currently causes a user-visible doc inconsistency: `cmd/rules/` ships 14 rule JSONs and the README states 14, but one of them structurally cannot fire, so the honest count is 13.\n- **Assigned to `0018-doc-drift-executable-specs/drift-rules`.** It had no thread at all while its counterpart sat inside that thread — which is precisely why the contradiction survived: the two halves could never appear side by side in any listing.\n\nVerified against the tree today: `cmd/rules/drift_doc_outdated_count.json` is still present (14 rule files), so no part of this has been actioned. The AC on this bead is accurate for the current externalized-JSON rule layout (post mache-b0b979).","author":"rsry-mcp","created_at":"2026-07-27T19:26:17Z"}],"repo":"mache","scope":"","external_ref":null,"branch":null,"pr_url":null,"acceptance_criteria":"cmd/rules/drift_doc_outdated_count.json removed; smell rule count is 13 everywhere it's hand-stated (README, ROADMAP, any CHANGELOG/ARCHITECTURE mention); cmd/smell_rules_embed_test.go and the rule-listing tests updated; task smells + task test green. If the count is worth guarding, add a Go test asserting len(registered gate rules) matches the doc — but per feedback_prefer_go_tests_over_shell_and_smell_rules, do NOT resurrect it as a placeholder.","files":["cmd/rules/drift_doc_outdated_count.json","cmd/smell_rules_embed_test.go","README.md","docs/ROADMAP.md"],"test_files":["cmd/serve_find_smells_test.go"],"derived_from":[]} {"schema_version":1,"id":"mache-95e161","title":"Unify the find_smells MCP and find-smells CLI response envelopes, with a byte-for-byte parity test standing over both","description":"## Why parity is load-bearing\n\nThe committed smell baseline (`docs/smell-baseline.json`) is generated by the **CLI**. The ratchet only means anything to an agent calling the **MCP tool** if both see the same findings over the same .db. They had drifted in two independent ways, each invisible from inside its own path.\n\n## Divergence 1 — a capability the MCP path could not reach\n\n`lazyGraph` did not forward `DBPath()`, so `ensureSmellQueryContext` skipped `LoadCapnpBindings` and the `_capnp_binding_refs` arm of `v_refs` stayed empty in serve mode. The CLI's `dbQuerier` implements `DBPath`, so it read the sibling `.bindings.capnp` log and the MCP tool did not. Fixed in mache-91956b.\n\n## Divergence 2 — two hand-built envelopes\n\nBoth paths constructed `{rule, total, findings}` inline. Only the CLI normalized a nil finding set to `[]`; the MCP handler emitted `\"findings\": null`. A clean run — the common case — therefore serialized differently depending on who asked.\n\nUnified into `newSmellResponse` in `cmd/smell_findings.go`, called by both. The nil -> `[]` normalization is contract, not cosmetics: `null` forces every JSON consumer to special-case the clean-gate case, and diverges from `--format=sarif`, which always emits arrays.\n\n## The proof\n\n`TestFindSmells_MCPAndCLI_ByteForByteParity` runs the same rule through both paths over the same fixture .db and asserts byte equality (trimming only the CLI Encoder's trailing newline, which is stream framing rather than payload). Three cases: a rule with findings, a clean run, and — the discriminating one — a fixture carrying a sibling `.bindings.capnp` record that retires the finding, reachable only via `DBPath -> LoadCapnpBindings -> v_refs`.\n\n**Falsified, not assumed.** With `lazyGraph.DBPath` neutered to return the no-source sentinel, the test fails exactly as the production bug did:\n\n- CLI: `\"total\": 0` — the binding retires `pkg/Dead`\n- MCP: `\"total\": 1` — still reports `pkg/Dead`\n\nRestored, all three cases pass.\n\n## Residual, deliberately not unified\n\nThe CLI has surface the MCP tool does not: `--format=md|ci|sarif`, multi-rule glob runs, baseline read/write, and `--fail-on` gating. Those are CLI-only concerns and should stay that way; the parity contract covers the JSON payload for a single rule, which is the shared thing.","status":"open","priority":1,"issue_type":"task","owner":"dev-agent","created_at":"2026-08-06T02:23:08+00:00","updated_at":"2026-08-06T02:23:08+00:00","created_by":"jamestexas","dependency_count":0,"dependent_count":0,"comment_count":0,"dependencies":[],"comments":[],"repo":"mache","scope":"","external_ref":null,"branch":null,"pr_url":null,"acceptance_criteria":"`go test ./cmd/ -run TestFindSmells_MCPAndCLI_ByteForByteParity` passes, and fails when lazyGraph.DBPath is neutered (verified by doing exactly that). Both paths construct their envelope via newSmellResponse — no second inline `{rule,total,findings}` struct remains in cmd/.","files":["cmd/smell_findings.go","cmd/serve_find_smells.go","cmd/find_smells_cli.go"],"test_files":["cmd/find_smells_parity_test.go"],"derived_from":[]} @@ -423,7 +426,7 @@ {"schema_version":1,"id":"mache-c3248b","title":"Audit public repos for canonical bead JSONL format","description":"Check public ecosystem repos for the same canonical bead JSONL shape fixed in mache: tracked .beads/beads.jsonl, local binary DB ignored, beads-jsonl merge driver wiring where applicable, and no active bd CLI guidance. Include ~/github/jamestexas/agents and exclude crumb per user request.","status":"open","priority":2,"issue_type":"task","owner":"dev-agent","created_at":"2026-07-23T14:31:23+00:00","updated_at":"2026-07-23T14:31:23+00:00","created_by":"jamestexas","dependency_count":0,"dependent_count":0,"comment_count":7,"dependencies":[],"comments":[{"id":"458","issue_id":"mache-c3248b","text":"Audit result 2026-07-23: public GitHub remotes checked, crumb excluded, agents included. OK JSONL canonical: ley-line-open, mache, rosary, canonical-hours, cloister (except beads.db ignore gap), notme (missing tracked .gitattributes reference), agents (missing tracked .gitattributes reference + legacy bd docs), plus no-bead-store public repos claude-code/codex/turso/zen. Missing tracked .beads/beads.jsonl but exportable: assay 45, kiln 27, signet 68. Empty stores missing JSONL: go-cms 0, go-platform-signers 0. x-ray has a store/schema failure: rsry export fails around comments.edit_reason / no such table issues after Dolt migration attempt. Older repos also retain tracked bd README/config/hooks and AGENTS bd guidance. Do not touch crumb; canonical-hours noted only.","author":"rsry-cli","created_at":"2026-07-23T14:35:03Z"},{"id":"459","issue_id":"mache-c3248b","text":"Starting fix pass: target public repos with bead stores. Will avoid crumb; no-store third-party repos left alone; x-ray tracked separately as store/schema repair. Need preserve pre-existing dirty worktrees.","author":"rsry-cli","created_at":"2026-07-23T14:37:04Z"},{"id":"460","issue_id":"mache-c3248b","text":"User clarified: fixes must land remotely too, and audit must verify bead refs/remotes/hooks are not pointing at entire/entire-graph. Checking git config, hooks, .beads, .gitattributes, AGENTS, and remote refs before making cross-repo changes.","author":"rsry-cli","created_at":"2026-07-23T14:37:36Z"},{"id":"461","issue_id":"mache-c3248b","text":"Fix pass result 2026-07-23: remote PRs opened and merged for agents#6, assay#2, cloister#186, go-cms#19, go-platform-signers#6, notme#48, signet#138. Each branch came from clean origin/main temp worktrees; setup scan excluding bead history found no entire/entire-graph refs in git config/hooks/.beads setup/.gitattributes/AGENTS. JSONL records after fix: agents 5, assay 45, cloister 108, go-cms 0, go-platform-signers 0, notme 115, signet 68. Kiln could not be pushed: GitHub reports repo archived/read-only. x-ray remains blocked by bead store/schema export failure (comments.edit_reason / issues table). canonical-hours and rosary noted but not touched.","author":"rsry-cli","created_at":"2026-07-23T14:51:51Z"},{"id":"462","issue_id":"mache-c3248b","text":"Remaining non-code blocker: kiln has a valid local export candidate (27 bead records) but GitHub rejects push with 'repository was archived so it is read-only'. To land kiln remotely, unarchive the repo or intentionally leave it frozen.","author":"rsry-cli","created_at":"2026-07-23T14:52:14Z"},{"id":"463","issue_id":"mache-c3248b","text":"User asked to true up canonical-hours too. Clarification: prior audit left it alone because JSONL + .gitattributes were already present; remaining drift is stale bd scaffold/guidance. Starting clean-temp-worktree fix.","author":"rsry-cli","created_at":"2026-07-23T15:23:35Z"},{"id":"464","issue_id":"mache-c3248b","text":"Canonical-hours true-up complete: PR agentic-research/canonical-hours#26 merged after check + smells passed. It already had .beads/beads.jsonl (54 records) and .gitattributes merge=beads-jsonl; fix removed remaining tracked bd scaffold/guidance and normalized .beads/.gitignore. Setup scan found no entire/entire-graph refs.","author":"rsry-cli","created_at":"2026-07-23T15:27:35Z"}],"repo":"mache","scope":"","external_ref":null,"branch":null,"pr_url":null,"acceptance_criteria":"Report each audited public repo as OK/missing/drift, with concrete file-level evidence and follow-up beads for fixes.","files":[".beads","AGENTS.md",".gitattributes",".gitignore"],"test_files":[],"derived_from":[]} {"schema_version":1,"id":"mache-c35b45","title":"mache serve is a hard error post-CGO-removal — add temp-dir staging shim like mount --infer","description":"Found during PR-B (mache-37ae8b) test alignment (commit 1f83171). After in-process tree-sitter removal, `buildServeGraph` on a single SOURCE file (.go) is a HARD ERROR: the auto-leyline serve path (cmd/serve.go:457) is IsDir-gated, `leyline parse` rejects non-directory paths, and ingestSourceFile fails loudly without an ASTWalker. `mount --infer` already handles single files by staging into a temp dir (cmd/mount_inference.go), but serve has no equivalent shim — so `mache serve foo.go` (serve_registry.go:522 passes an empty schema for non-dir sources) dies end-to-end. Either add the staging shim to buildServeGraph or make the error explicitly guide the user to the dir form. Documented degradation, not a regression in projected output — but a UX cliff. TestBuildServeGraph_NilInvalidatorForNonDir currently pins the hard-error behavior.","status":"open","priority":2,"issue_type":"task","owner":"dev-agent","created_at":"2026-07-23T04:49:13+00:00","updated_at":"2026-07-23T04:49:13+00:00","created_by":"jamestexas","dependency_count":0,"dependent_count":0,"comment_count":0,"dependencies":[],"comments":[],"repo":"mache","scope":"","external_ref":null,"branch":null,"pr_url":null,"acceptance_criteria":"`mache serve ` either succeeds by staging the file into a temp dir before `leyline parse` (mirroring cmd/mount_inference.go's single-file handling) OR fails with a clear, actionable message pointing the user at the directory form. A cmd test asserts the chosen behavior. `go test ./cmd/` green.","files":["cmd/serve.go","cmd/serve_registry.go"],"test_files":["cmd/sheaf_wire_test.go"],"derived_from":[]} {"schema_version":1,"id":"mache-c380d2","title":"_ast smell dogfood blocked: leyline _ast baseline isn't reproducible across darwin/linux (CI mismatch)","description":"The _ast smell dogfood (mache-46af85 part 2) was split out of PR #516 (preserved on branch wip/mache-46af85-dogfood-ast-rules) because a docs/smell-baseline-ast.json generated locally on darwin does NOT match the linux CI run: the CI smells:ast step reported many NEW long_function findings (socket.go:210, tools/mcp-fetch/main.go:54, tools/notion-fetch/*, tools/server-json-gen/*, tools/coverage-gate/*, tools/sheaf-subscribe-probe/main.go:28, several test files) that the darwin baseline lacked.\n\nThe lane already builds from TRACKED files only (git ls-files → cpio) for parity within a platform, and uses the pinned leyline. Two candidate causes to determine:\n 1. Cross-platform leyline variance (darwin-arm64 vs linux-amd64 v0.7.0 producing different _ast / long_function line-spans or function sets) — if real, a committed baseline can't be authored on darwin. Depends on/relates to ley-line-open determinism.\n 2. Local baseline was generated from an incomplete tracked-file set (some tracked tools/*-probe were physically moved out during the session, so cpio skipped them even though git ls-files listed them → those files absent from the baseline; CI's clean checkout has them → NEW).\n\nResolution paths: (a) verify (2) by regenerating with a fully-present tree and diffing; (b) if (1) is real, generate the baseline IN CI (a `task smells:ast:baseline` job that commits the artifact) or normalize the metric to be platform-stable; (c) confirm leyline _ast output is byte-stable across platforms (file an LLO bead if not). Also note: mache's community-detection quotient test (internal/ingest/quotient_test.go:704) flaked on the same linux run — a separate determinism flake to watch.\n\nClose condition: `task smells:ast` passes in linux CI against a committed baseline, and the baseline is reproducible (0 NEW) from a clean checkout on the platform CI uses.","status":"done","priority":2,"issue_type":"bug","owner":"scoping-agent","created_at":"2026-07-23T04:49:13+00:00","updated_at":"2026-07-23T04:49:13+00:00","created_by":null,"dependency_count":0,"dependent_count":0,"comment_count":2,"dependencies":[],"comments":[{"id":"196","issue_id":"mache-c380d2","text":"ROOT CAUSE FOUND — it's cause (2), a cpio portability bug, NOT cross-platform leyline variance:\n\n`git ls-files -z | cpio -pd0 \"$SRC\"` copies 0 files on macOS (BSD cpio doesn't handle -0/null-delimited stdin the way GNU cpio on Linux/CI does). So the baseline was generated from an EMPTY tree (0 findings baselined); CI's GNU cpio copied all tracked files → every finding was NEW. I misread the earlier \"tracked tool present? NO (bad)\" + \"0 parsed\" as working.\n\nFIX confirmed: replace cpio with `git archive HEAD | tar -x -C \"$SRC\"` — fully portable (git+tar everywhere, no flag ambiguity). Local test: copies 770 tracked files, leyline parses 696, long_function = 103 INCLUDING the 5 CI-flagged files (socket.go, mcp-fetch, notion-fetch, server-json-gen, coverage-gate). git archive HEAD uses the COMMITTED tree = exactly CI's checkout → exact parity. Minor nuance: local runs then gate HEAD, not uncommitted working-tree changes (acceptable for a ratchet).\n\nResidual risk: git archive equalizes the FILE SET; if leyline ALSO has darwin↔linux output variance (masked until now by the empty baseline), the regenerated darwin baseline could still differ from linux CI — handle by generating the baseline in CI if that surfaces. But the primary blocker (cpio) is resolved.\n\nNext: on top of LLO v0.7.1 (#517), swap cpio→git archive in smells:ast:run, regenerate a REAL baseline, verify smells:ast 0 NEW, re-open the dogfood PR.","author":"rsry-mcp","created_at":"2026-07-23T04:49:13Z"},{"id":"197","issue_id":"mache-c380d2","text":"Auto-closed by rsry close-merged --local: PR #518 merged","author":"rosary","created_at":"2026-07-23T04:49:13Z"}],"repo":"mache","scope":"","external_ref":null,"branch":null,"pr_url":null,"acceptance_criteria":"smells:ast passes in CI against a committed docs/smell-baseline-ast.json, and regenerating from a clean checkout on the CI platform yields 0 NEW (the baseline is reproducible). Root cause (cross-platform variance vs incomplete-set) identified and addressed.","files":["Taskfile.yml","docs/smell-baseline-ast.json","cmd/rules/long_function.json"],"test_files":[],"derived_from":[]} -{"schema_version":1,"id":"mache-c436a6","title":"Consume LLO's source_blobs CDC chunk-diff as a change-detection signal to narrow live-reparse invalidation","description":"## Context\n\nTwo prior findings converge here:\n\n1. `mache-6c9e1d` — a source-backed `mache serve` does a one-shot `leyline parse` into a frozen `.db`; live edits aren't reparsed and the sheaf cascade doesn't engage without the managed daemon path.\n2. `mache-238673` — node_hash memoization / sheaf incremental recompute is \"the real payoff\" of an incremental architecture, separate from CDC's storage-dedup framing.\n3. `mache-abf404` (closed 2026-08-05) — CDC's `source_blobs` target (once ley-line-open-baa57f landed) is cost-neutral (+0.35% db size, re-measured on v0.15.1) but shows zero dedup benefit on a single-repo snapshot, because mache's own read pattern (construct-granular, ~731 B/read) can't benefit from chunk-level paging regardless.\n\nSo CDC's storage-dedup value proposition is a dead end for mache. But a falsification experiment run in the same session found the OTHER property CDC claims — boundary stability — genuinely holds and IS useful:\n\n## The falsified finding (2026-08-05, real edit to a real file)\n\nSingle-line edit to `cmd/serve_registry.go` (41,682 → 41,765 bytes, +83 bytes at line 500):\n- File chunks into exactly 2 chunks (source_blobs target).\n- Chunk 0 (bytes [0, 22888)→[0, 22971)): hash changed — contains the edit.\n- Chunk 1 (bytes [22888, 41682)→[22971, 41765)): hash **byte-identical** across the edit, despite its absolute byte offset shifting +83. Content-defined chunking correctly re-finds the same boundary in the unchanged tail.\n\nThis is a real, working change-detection primitive: \"which byte range(s) of this file actually differ from the last-known chunk manifest\" — for free, without diffing file content directly.\n\n## The idea (NOT this bead's implementation — scoping only)\n\nmache already tracks node byte-provenance in the same coordinate system (`internal/graph.SourceOrigin`, used today for write-back's `ShiftOrigins`). If a daemon-managed CDC index is kept live (source_blobs target, incrementally rechunked on file change — `leyline_cdc::rechunk_with_stats`, ADR-0033 §1.2), the changed chunk(s)' byte spans could be intersected against mache's own node byte-origins to determine which specific AST nodes actually need re-invalidation on an edit — narrowing mache's current \"any fsnotify event → reparse/invalidate the whole file\" behavior to \"reparse/invalidate only the nodes overlapping the changed byte range.\"\n\nExpected payoff scales with file size: negligible for small files (most Go source, ~1-3 chunks each) but real for large files (generated code, big data/config files) where an edit near the end currently forces whole-file reprocessing.\n\n## Explicitly NOT in scope for this bead\n\n- Implementation. Per 2026-08-05 direction: \"LLO needs to be in a good place first.\"\n- Blocked on `ley-line-open-c3d746` (daemon `--cdc` has no way to select the `source-blobs` target today — only the standalone `leyline cdc enable` CLI does).\n- Requires daemon-side incremental rechunk to actually be exercised live (not just one-shot activation) — needs verification that `rechunk_with_stats`'s bounded-rescan-window claim (ley-line-open-f8ebe7, closed) holds under mache's actual watch-and-reparse cadence, not just LLO's own unit tests.\n\n## Acceptance criteria (once unblocked and picked up)\n\nA daemon-managed `mache serve` with CDC enabled (source-blobs target) on a large fixture file: editing one region produces a chunk-manifest diff naming only the chunks overlapping the edit; those chunk byte-spans, intersected against mache's `SourceOrigin` node table, name a node set that a human/independent check agrees is exactly \"the nodes touched by the edit\" (no false negatives — a changed node must appear; false positives are acceptable but should be measured). `go test ./internal/leyline/... ./internal/graph/...` covers the correlation logic.","status":"open","priority":3,"issue_type":"feature","owner":"scoping-agent","created_at":"2026-08-05T17:54:30+00:00","updated_at":"2026-08-05T17:54:30+00:00","created_by":null,"dependency_count":0,"dependent_count":0,"comment_count":0,"dependencies":["ley-line-open-c3d746"],"comments":[],"repo":"mache","scope":"","external_ref":null,"branch":null,"pr_url":null,"acceptance_criteria":"","files":["internal/leyline/socket.go","internal/graph/graph.go","cmd/serve.go"],"test_files":[],"derived_from":[]} +{"schema_version":1,"id":"mache-c436a6","title":"Consume LLO's source_blobs CDC chunk-diff as a change-detection signal to narrow live-reparse invalidation","description":"## Context\n\nTwo prior findings converge here:\n\n1. `mache-6c9e1d` — a source-backed `mache serve` does a one-shot `leyline parse` into a frozen `.db`; live edits aren't reparsed and the sheaf cascade doesn't engage without the managed daemon path.\n2. `mache-238673` — node_hash memoization / sheaf incremental recompute is \"the real payoff\" of an incremental architecture, separate from CDC's storage-dedup framing.\n3. `mache-abf404` (closed 2026-08-05) — CDC's `source_blobs` target (once ley-line-open-baa57f landed) is cost-neutral (+0.35% db size, re-measured on v0.15.1) but shows zero dedup benefit on a single-repo snapshot, because mache's own read pattern (construct-granular, ~731 B/read) can't benefit from chunk-level paging regardless.\n\nSo CDC's storage-dedup value proposition is a dead end for mache. But a falsification experiment run in the same session found the OTHER property CDC claims — boundary stability — genuinely holds and IS useful:\n\n## The falsified finding (2026-08-05, real edit to a real file)\n\nSingle-line edit to `cmd/serve_registry.go` (41,682 → 41,765 bytes, +83 bytes at line 500):\n- File chunks into exactly 2 chunks (source_blobs target).\n- Chunk 0 (bytes [0, 22888)→[0, 22971)): hash changed — contains the edit.\n- Chunk 1 (bytes [22888, 41682)→[22971, 41765)): hash **byte-identical** across the edit, despite its absolute byte offset shifting +83. Content-defined chunking correctly re-finds the same boundary in the unchanged tail.\n\nThis is a real, working change-detection primitive: \"which byte range(s) of this file actually differ from the last-known chunk manifest\" — for free, without diffing file content directly.\n\n## The idea (NOT this bead's implementation — scoping only)\n\nmache already tracks node byte-provenance in the same coordinate system (`internal/graph.SourceOrigin`, used today for write-back's `ShiftOrigins`). If a daemon-managed CDC index is kept live (source_blobs target, incrementally rechunked on file change — `leyline_cdc::rechunk_with_stats`, ADR-0033 §1.2), the changed chunk(s)' byte spans could be intersected against mache's own node byte-origins to determine which specific AST nodes actually need re-invalidation on an edit — narrowing mache's current \"any fsnotify event → reparse/invalidate the whole file\" behavior to \"reparse/invalidate only the nodes overlapping the changed byte range.\"\n\nExpected payoff scales with file size: negligible for small files (most Go source, ~1-3 chunks each) but real for large files (generated code, big data/config files) where an edit near the end currently forces whole-file reprocessing.\n\n## Explicitly NOT in scope for this bead\n\n- Implementation. Per 2026-08-05 direction: \"LLO needs to be in a good place first.\"\n- Blocked on `ley-line-open-c3d746` (daemon `--cdc` has no way to select the `source-blobs` target today — only the standalone `leyline cdc enable` CLI does).\n- Requires daemon-side incremental rechunk to actually be exercised live (not just one-shot activation) — needs verification that `rechunk_with_stats`'s bounded-rescan-window claim (ley-line-open-f8ebe7, closed) holds under mache's actual watch-and-reparse cadence, not just LLO's own unit tests.\n\n## Acceptance criteria (once unblocked and picked up)\n\nA daemon-managed `mache serve` with CDC enabled (source-blobs target) on a large fixture file: editing one region produces a chunk-manifest diff naming only the chunks overlapping the edit; those chunk byte-spans, intersected against mache's `SourceOrigin` node table, name a node set that a human/independent check agrees is exactly \"the nodes touched by the edit\" (no false negatives — a changed node must appear; false positives are acceptable but should be measured). `go test ./internal/leyline/... ./internal/graph/...` covers the correlation logic.","status":"open","priority":3,"issue_type":"feature","owner":"scoping-agent","created_at":"2026-08-05T17:54:30+00:00","updated_at":"2026-08-11T20:12:27+00:00","created_by":null,"dependency_count":0,"dependent_count":0,"comment_count":0,"dependencies":["ley-line-open-c3d746"],"comments":[],"repo":"mache","scope":"","external_ref":null,"branch":null,"pr_url":null,"acceptance_criteria":"A daemon-managed mache serve with CDC enabled for source_blobs demonstrates on a large fixture that a one-region edit produces a chunk-manifest diff limited to overlapping changed chunks; intersecting those byte spans with SourceOrigin identifies every changed node with no false negatives and measured false positives. Verify correlation logic with: go test ./internal/leyline/... ./internal/graph/...","files":["internal/leyline/socket.go","internal/graph/graph.go","cmd/serve.go"],"test_files":[],"derived_from":[]} {"schema_version":1,"id":"mache-c49155","title":"Semantic relevance over smell findings (XTR/witchcraft) — conversation-conditioned 'which findings matter now'","description":"PARALLEL track to mache-ae54d8 (deterministic find_smells digest), NOT a later phase (user 2026-06-23: \"not later tho, feels like a parallel task\").\n\nTwo primitives were conflated and are now separated:\n- TIERING (continuity memory-tree \"shape without weight\", L0/L1/L2) = the deterministic find_smells digest. Ranks by each rule's exact metric (complexity/dup-count/line-count). mache-ae54d8.\n- RANKING (XTR-Warp / T5 semantic search = dropbox/witchcraft, the SAME engine lectio runs: \"hybrid XTR-WARP + BM25\") = THIS bead. Surfaces \"which of N findings matters to what I'm working on RIGHT NOW\" — a similarity question, not a metric question.\n\nThis is the selective-recall frontier (wiki ~/remotes/art/wiki/docs/problems/selective-recall.md) wearing a find_smells hat: the live conversation/working-context as the query vector, ranking findings by relevance rather than raw severity. pickbrain (witchcraft's example CLI: \"semantic search over AI coding sessions\") is the reference shape.\n\nScope sketch (design-first): embed findings (or the constructs they point at) into the XTR/lectio substrate; given a working-context query, rank findings by sim; surface the relevant subset. Open question: embed the finding text, the construct source, or the rule+location? Falsification: on a held-out probe (a session where a specific smell became relevant), does XTR-ranked surfacing beat metric-ranked + BM25? (mirror selective-recall.md's ablation: A static / B convo-sim / C HDC.)\n\nRefs: dropbox/witchcraft (XTR-Warp Rust), lectio (same engine), continuity (tiering sibling), wiki selective-recall.md + agent-memory-systems.md. Depends on nothing in mache-ae54d8 — runs in parallel.","status":"open","priority":2,"issue_type":"design","owner":"architect-agent","created_at":"2026-07-23T04:49:13+00:00","updated_at":"2026-07-23T04:49:13+00:00","created_by":"jamestexas","dependency_count":0,"dependent_count":0,"comment_count":0,"dependencies":[],"comments":[],"repo":"mache","scope":"","external_ref":null,"branch":null,"pr_url":null,"acceptance_criteria":"","files":[],"test_files":[],"derived_from":[]} {"schema_version":1,"id":"mache-c725e9","title":"Ingestion silently drops constructs on in-record name collision (14.6% loss on Rust)","description":"`collectNodes` (internal/ingest/engine_walk.go:83-172) — the streaming path taken by `mache build` via engine_sqlite.go:41 -> processRecord -> collectNodes — performs NO dedup. It renders `name` from the schema template, computes `id := toNodeID(currentPath)`, and appends to `result.nodes`. Two matches within the SAME record that render the same name produce the same node ID, and the last one written to the store wins. The earlier constructs are lost with no error, no warning, and no diagnostic.\n\nThe store-based dedup at engine_walk.go:222-229 does NOT cover this. It lives in the OTHER path (`walkNode`), and even there the suffix is derived solely from `dedupSuffix(sourceFile)`, so it disambiguates ACROSS files but never WITHIN one — N>2 same-named constructs in a single file collapse to the same suffixed ID.\n\nREPRODUCTION (minimal, deterministic):\n\n // /tmp/rsfix/lib.rs\n pub struct A { pub v: u32 }\n pub struct B { pub v: u32 }\n pub struct C { pub v: u32 }\n impl A { pub fn new() -> Self { A { v: 1 } } }\n impl B { pub fn new() -> Self { B { v: 2 } } }\n impl C { pub fn new() -> Self { C { v: 3 } } }\n\n mache build --schema examples/rust-schema.json /tmp/rsfix /tmp/rsfix.db\n sqlite3 /tmp/rsfix.db \"select id, record from nodes where name='source' and id like 'functions/%';\"\n\n -> functions/new/source | pub fn new() -> Self { C { v: 3 } }\n\nThree distinct methods, ONE node, holding C's body. A::new and B::new are gone. No `.from_` suffix was applied at all.\n\nSCALE (measured against ley-line-open rs/, 228 files):\n ground truth `fn new(` in source ......... 63\n captured as functions/new ................ 1\n ground truth all fn items ................ 4210\n captured functions/*/source .............. 3597\n -> 613 constructs lost (14.6%), silently\n\nGo is unaffected in practice because its schema nests by package AND names methods `{{.receiver}}.{{.name}}`, so collisions cannot arise. Any language whose schema uses a flat, bare-name namespace is exposed. The Rust schema is the concrete case (see the sibling schema bead).\n\nWHY P0: this is silent data loss in the core projection path. mache's primary claim is that it presents a faithful structural view of a codebase; a 14.6% silent shortfall makes find_definition / get_overview / callers quietly incomplete, and nothing in the pipeline surfaces it. A caller cannot distinguish \"not present\" from \"dropped\".\n\nFIX (two parts, this bead is the first):\n1. `collectNodes` must never emit a duplicate ID. Track IDs seen within the record and disambiguate deterministically (source order is stable for a given file, so a counter is reproducible). Losing data must be impossible by construction.\n2. Emit a WARN naming the schema node and the colliding name, so the schema author learns their name template is not unique rather than having the engine paper over it. The suffix is a safety net, not the intended outcome — the schema fix is the real one.\n\nExplicitly NOT acceptable: keeping last-writer-wins, or deduping only across files.\n\nDiscovered while running the F4 per-language generality benchmark (mache-544659) against the ley-line-open Rust workspace.","status":"done","priority":0,"issue_type":"bug","owner":"scoping-agent","created_at":"2026-07-27T22:27:38+00:00","updated_at":"2026-07-28T02:28:17+00:00","created_by":null,"dependency_count":0,"dependent_count":1,"comment_count":5,"dependencies":[],"comments":[{"id":"540","issue_id":"mache-c725e9","text":"ROOT CAUSE CONFIRMED — and it corrects two claims in the original description. Do not act on the description's attribution; use this.\n\nWRONG (original): \"`collectNodes` performs NO dedup ... the streaming path taken by `mache build` via engine_sqlite.go:41\".\nRIGHT: `mache build` on source takes `processNode` (engine_walk.go:179), NOT `collectNodes`. Instrumenting both paths and rebuilding the repro emitted `[PATH-processNode] id=functions/new files=1` three times and ZERO `collectNodes` hits. A first fix attempt placed in `collectNodes` changed nothing, which is what exposed the misattribution. `collectNodes` may well have the same latent gap, but it is not the path that produces this bug and is not where the fix belongs.\n\nALSO WRONG: I speculated the survivor was nondeterministic because workers are parallel. It is deterministic — 5 consecutive rebuilds all yielded C's body. Within one record, matches are walked in source order and the last one wins. Do not write a test asserting nondeterminism.\n\nACTUAL MECHANISM — the dedup guard at engine_walk.go:222-229 is DEAD CODE on this path:\n\n if len(schema.Files) > 0 && sourceFile != \"\" {\n if existing, err := store.GetNode(id); err == nil && len(existing.Children) > 0 {\n suffix := dedupSuffix(sourceFile) // never reached\n\nProbing the guard's own condition on the 3-collision repro:\n\n [PROBE] id=functions/new ABSENT err=node not found <- match 1\n [PROBE] id=functions/new EXISTS children=0 <- match 2\n [PROBE] id=functions/new EXISTS children=0 <- match 3\n\nThe node EXISTS by match 2, but reports zero children, so `len(existing.Children) > 0` is false and the suffix branch is never taken. That is why no `.from_lib_rs` node appears anywhere in the output — the store ends with a single `functions/new` holding C's body, with A's and B's silently discarded. Full store dump confirms: only `functions/new/source` exists under `functions/`.\n\nWhy children reads 0 at check time is the remaining sub-question. processNode builds `node` with `Children: existingChildren` re-fetched from the store, then attaches files via `bufferingTarget.AddFileChildren`, which appends to `parent.Children` in memory and republishes the parent via `AddNode`. The file nodes themselves are buffered for a later `ReplaceFileNodes` atomic swap (engine_refs.go:31-69). Something in that publish/swap sequence means the parent's Children are not observable to the next match's `GetNode`. Confirm this before fixing — it determines whether the guard is repairable in place.\n\nFIX GUIDANCE (revised)\nDo NOT simply repair the children check. It is order-dependent and couples collision detection to the file-buffering lifecycle — the exact coupling that made it silently dead. Detect the collision on the thing that actually collides: the FILE node ID (`functions/new/source`). That is unambiguous, needs no knowledge of publish order, and is checkable in one place.\n\nAlso note the guard is unreachable for a second, independent reason: even when it does fire, `dedupSuffix(sourceFile)` is a function of the source file ALONE, so N>2 same-named constructs in ONE file all render the identical suffixed ID and collide again. Both defects must be fixed together, or a 3-way collision still loses one.\n\nPreferred end state: the engine makes loss impossible AND reports the collision at WARN naming the schema node and the name, so the schema author learns their template is not unique (Rust methods lack a receiver qualifier — mache-c777ef, which is the real fix). The suffix is a safety net, not the intended outcome.\n\nREPRO IS EXACT AND FAST (~3ms). /tmp/rsfix/lib.rs with three `impl {A,B,C} { pub fn new() }` -> one node. Keep it as the regression fixture.\n\nVerified on branch docs/mache-0ca7e6-required-gates with a locally built ./mache; working tree left clean (all instrumentation reverted).","author":"rsry-mcp","created_at":"2026-07-27T22:32:28Z"},{"id":"541","issue_id":"mache-c725e9","text":"FALSIFICATION PASS — four tests run against the finding. It survives, the mechanism is confirmed to 99.8%, but ONE framing claim was overstated and ONE scope claim was wrong in the direction that makes this worse. Corrected numbers below supersede both the description and the previous comment.\n\nT4 — DOES COLLISION ACTUALLY EXPLAIN THE GAP? (the decisive test)\nIf bare-name collision is the mechanism, then captured should equal the number of DISTINCT bare names, not the number of functions.\n\n adjusted ground truth (fns with a body) .. 4193\n distinct bare names ..................... 3605 <- predicted capture\n mache actually captured ................. 3597\n residual unexplained .................... 8 (0.2%)\n\nCollision explains 99.8% of the loss. The mechanism is confirmed, not inferred. The 8 residual are worth a look but are not the story.\n\nT3 — GROUND TRUTH WAS INFLATED. 35 of the matches I counted were bodyless trait method DECLARATIONS (`fn foo(&self);`), which mache is CORRECT to omit — they have no body to project. Corrected loss: 588 constructs (14.0%), not 613 (14.6%). Use 588.\n\nT1 — \"SILENTLY DROPPED\" WAS TOO STRONG FOR RUST. For methods inside `impl` blocks the bytes are still in the db, inside the coarse `implementations//source` blob:\n\n implementations/A/source | impl A { pub fn new() -> Self { A { v: 1 } } }\n implementations/B/source | impl B { pub fn new() -> Self { B { v: 2 } } }\n\nSo for Rust impl methods this is ADDRESSABILITY loss, not byte loss: `functions/` is missing 588 entries and find_definition / get_overview / callers are correspondingly incomplete, but the source is recoverable from a coarser node. Retrieval correctness bug, not corruption. Downgrade the language in the description accordingly.\n\nT2 — \"GO IS UNAFFECTED IN PRACTICE\" IS FALSE, AND GO IS THE WORSE CASE.\nThe description claims Go is safe because it nests by package and qualifies methods. That is wrong for FREE functions, and the guard's own documented purpose is to handle exactly this (\"This handles cases like multiple init() functions across Go files\") — it does not work:\n\n 3 files, each `func init()`, one package -> ONE node, holding c's body\n\nCritically, Go has NO coarser parent to fall back to — there is no impl-block equivalent holding the whole region. The other two bodies are nowhere in the database. For Go this is TRUE, IRRECOVERABLE BYTE LOSS.\n\nDemonstrated on mache's OWN source (self-hosting):\n\n `func init()` in cmd/*.go .............. 10 (build, cache, init, infer, config,\n mount, pack, find_smells_cli, schemas, serve)\n captured as cmd/functions/init ........ 1 (serve.go's)\n cmd/mount.go's init() body in the db .. ABSENT\n\nQuery proving absence: `select id from nodes where record like '%controlPath%' and record like '%Leyline control block%'` returns zero rows, while `grep -l 'Leyline control block' cmd/*.go` returns cmd/mount.go. mache cannot see 9 of its own init() functions, including the one registering the --schema/--data/--control flags.\n\nWhole-corpus Go exposure is small in RATE but not zero: ~52 of 6683 funcs+methods (0.8%) by name-collision arithmetic, concentrated in init(). Low rate, high severity — irrecoverable, and it hits the flag-registration functions that define the CLI surface.\n\nNET EFFECT ON PRIORITY: stays P0. The Rust headline softens to addressability loss; the Go case hardens to irrecoverable byte loss in the project's own graph. The fix does not change — detect the collision on the FILE node ID, disambiguate deterministically, WARN naming the schema node — but the regression test must now cover BOTH: a Rust impl-method collision AND a Go multi-file init() collision, asserting all bodies are present and distinct.\n\nAlso update mache-c777ef: it currently asserts Go is unaffected. It is not.","author":"rsry-mcp","created_at":"2026-07-27T22:36:19Z"},{"id":"543","issue_id":"mache-c725e9","text":"NUMBERS SUPERSEDED — see mache-c777ef for the authoritative measurement.\n\nEvery figure on this bead (613, then 588) came from a line-based regex over source text. That was the wrong instrument for auditing a tree-sitter pipeline: it over-counted by ~1.4% (macro bodies, block comments) and could not see qualification.\n\nRe-measured against LLO's own `node_defs` index on a matched pair (leyline parse -> mache build, same 245-file corpus):\n\n DISTINCT QUALIFIED tokens (addressable) .. 4330\n DISTINCT BARE names ..................... 3597 <- prediction\n mache actually captured ................. 3597 <- actual\n residual ................................ 0\n\nThe hypothesis \"mache captures exactly the set of distinct bare names\" is confirmed to ZERO residual by the parser's own definition index. The 8 unexplained residual noted earlier was an artifact of the regex and does not exist.\n\nCORRECTED LOSS: 733 constructs, 16.9% (not 588 / 14.0%).\n\nThe Go findings on this bead are unaffected — they were measured by direct query against mache's own projection, not by regex, and stand as written: 10 `func init()` in cmd/, 1 captured, cmd/mount.go's body absent from the graph entirely.","author":"rsry-mcp","created_at":"2026-07-27T22:51:28Z"},{"id":"544","issue_id":"mache-c725e9","text":"ROOT CAUSE OF THE DEAD GUARD — FOUND. This closes the open sub-question from the previous comment (\"why children reads 0 at check time\"). It is not a race and not an ordering problem.\n\n`SQLiteWriter.GetNode` (internal/ingest/sqlite_writer.go:582-637) NEVER POPULATES Children:\n\n SELECT kind, mtime, record, context, props FROM nodes WHERE id = ?\n ...\n n := &graph.Node{\n ID: id,\n Mode: mode,\n ModTime: time.Unix(0, mtimeNano),\n Context: context,\n }\n n.Properties = graph.DecodeProps(props)\n return n, nil // <-- Children is never set. Always nil.\n\nIts sibling confirms the intent: `func (w *SQLiteWriter) ListChildren(id string) ([]string, error) { return nil, nil // Not used during ingest }`. Children are simply not a readable property of this store during ingest.\n\n`mache build` writes through SQLiteWriter (wrapped in bufferingTarget — confirmed at runtime: `storeType=*ingest.bufferingTarget`). Therefore `len(existing.Children) > 0` at engine_walk.go:223 is ALWAYS FALSE on this path, and the dedup branch is structurally unreachable. Not dead by accident of ordering — dead because it interrogates a field the store never fills.\n\nRuntime trace proving the write is not the problem (3x `impl {A,B,C} { fn new() }` in one file):\n\n [G] guard id=functions/new ABSENT\n [F] AddFileChildren node=functions/new files=1 nowChildren=1 <- node HAS 1 child\n [G] guard id=functions/new children=0 <- refetch says 0\n [F] AddFileChildren node=functions/new files=1 nowChildren=1\n [G] guard id=functions/new children=0\n\nAddFileChildren correctly sets Children=1 on the node it holds; the refetch through GetNode drops it. The write is fine; the READ is lossy.\n\nSECOND CASUALTY — the same nil Children silently disables a DIFFERENT feature. engine_walk.go:236-241:\n\n // Create/Update Node — preserve existing children when merging\n // multiple files into the same node (e.g. multiple .go files in one package).\n var existingChildren []string\n if existing, err := store.GetNode(id); err == nil {\n existingChildren = existing.Children // always nil on this path\n }\n\nThat preservation is also inert. It survives in practice only because the parent-child wiring at engine_walk.go:326-338 keeps its own IN-MEMORY set (`e.childSeen`) rather than trusting the store. That is the pattern the dedup guard should have followed.\n\nSO THERE ARE NOW THREE INDEPENDENT DEFECTS, all of which must be fixed together:\n 1. The guard reads Children from a store that never populates it -> never fires. (this comment)\n 2. Even when it fires, `dedupSuffix(sourceFile)` is keyed on the FILE alone, so N>2 same-named constructs in one file all render the identical suffixed ID and collide again.\n 3. The name being disambiguated is the wrong identity in the first place — bare, unqualified (mache-c777ef).\n\nFIX DIRECTION (revised, and now concrete)\nDo NOT teach SQLiteWriter.GetNode to return Children just to feed this guard — that adds a per-node query to the ingest hot path to answer a question the engine can answer in memory for free. Follow the `e.childSeen` precedent: keep an in-memory set of claimed construct IDs for the current build and check that. It is O(1), needs no store round-trip, and cannot be defeated by a store that does not track children.\n\nBetter still, make the collision impossible rather than detected: key construct nodes on LLO's `node_id`, which is unique per definition (4137 distinct node_ids == 4137 function_item nodes, exact) instead of a rendered bare name. See mache-c777ef for that measurement and the caveat that `node_defs.token` alone is NOT sufficient (Go's init() tokens are bare; identity needs node_id).\n\nWorking tree left clean; all instrumentation reverted.","author":"rsry-mcp","created_at":"2026-07-27T22:55:27Z"},{"id":"554","issue_id":"mache-c725e9","text":"FIXED — merged to main as 723fd73 (PR #570).\n\nRESULT, against ground truth rather than an estimate:\n\n _ast function_item nodes (ground truth) .... 4137\n reaching the projection BEFORE ............. 3597\n AFTER ...................................... 4137 exact\n\n540 constructs recovered, zero residual. On mache's own source, cmd/ went from\n1 captured init() of 10 to all of them, and cmd/mount.go's init() — the one\nregistering --schema/--data/--control, previously absent from mache's graph\nentirely — now resolves at cmd/functions/init.from_mount_go.\n\nMECHANISM (confirmed, not inferred): the dedup guard at engine_walk.go:222 gated\non `store.GetNode(id).Children`, and SQLiteWriter.GetNode never populates\nChildren — it selects kind/mtime/record/context/props and stops, and its\nListChildren is `return nil, nil // Not used during ingest`. The condition was\npermanently false on the build path, so the branch was dead code. MemoryStore\nreturns the live node pointer, so the same guard DID fire there.\n\nA second, independent defect: even when it fired, dedupSuffix() keys on the\nsource file alone, so N>2 constructs sharing a name within one file all rendered\nthe identical suffixed ID and collided again.\n\nFIX: claimed IDs tracked in the engine rather than read back from the store,\nfollowing the existing e.childSeen precedent — collision detection must not\ndepend on which backend is being written to. Disambiguation prefers the\nmeaningful .from_ shape and falls back to a counter only for same-file\ncollisions, which the old code could not express. 832 collisions now log at WARN\non the Rust corpus instead of vanishing.\n\nSmell gate passed with the EXISTING baseline — 540 new nodes introduced no new\ngated smells, no regeneration needed.\n\nSHIPPED ALONGSIDE (same PR, second commit): the GetNodePopulatesChildren pin on\nthe Graph conformance suite — the test that would have caught this. See\nmache-e3d9bb; that bead stays OPEN, because this PR routes around the store\ncontract divergence rather than repairing it.\n\nSTILL PROVISIONAL, BY DESIGN: the numeric suffix is positional, so inserting a\nconstruct above renumbers those below. It stops the data loss; it does not settle\naddressing. mache-c777ef (receiver/module-qualified names from node_defs) is the\nreal fix, and ley-line v0.11 may deliver the upstream half via the name-keyed\nwalk (ley-line-open-23377a) — if it does, these suffixes become a rarely-fired\nfallback rather than the primary path, which is the intended outcome.","author":"rsry-mcp","created_at":"2026-07-28T02:28:13Z"}],"repo":"mache","scope":"","external_ref":null,"branch":null,"pr_url":null,"acceptance_criteria":"`go test ./internal/ingest/ -run TestCollectNodes` passes with a new test that ingests a single record containing three same-named constructs and asserts all three are present as distinct nodes with their own distinct content. Regression assertion: rebuilding ley-line-open rs/ yields >= 4100 functions/*/source nodes (vs 3597 today) against 4210 ground-truth fn items.","files":["internal/ingest/engine_walk.go"],"test_files":["internal/ingest/engine_walk_test.go"],"derived_from":[]} {"schema_version":1,"id":"mache-c7421e","title":"Rule-polarity generalization: extend smell-rules engine to host must-hold rules (cloister done-rules) as a second polarity","description":"## Framing (user, 2026-05-21)\n\n> \"Polarity belongs in mache. That's why I mention tool repo. Like mache should compose enough, right? Cloister then uses it.\"\n\nmache already owns the rule-engine surface (smell-rules with SQL evaluator, severity tiers per ADR-0018, `MACHE_SMELL_RULES_DIR` external loader, find-smells CLI + GH Action). Cloister has `done-rules/*.json` driven by `scripts/done-runner.mjs` — a shell-evaluator with overlapping but inverted semantics. The shape is unified by adding a polarity flag to mache's rule schema and a shell evaluator alongside the SQL one. **Cloister consumes mache as a tool (per ADR-0026 / cloister-cf7a3b); cloister stops maintaining its own engine.**\n\n## The polarity, concretely\n\n| | smell-rule (mache, today) | done-rule (cloister, today) |\n|---|---|---|\n| Assertion | findings = drift; \"must NOT be present\" | exit 0 = pass; \"must hold\" |\n| Evaluator | SQL template over `_ast`/`nodes`/`node_refs`/`node_defs` | shell command |\n| Loader | `cmd/smell_rules.go` + `cmd/smell_rules_external.go` | `scripts/done-runner.mjs` (cloister) |\n| Severity | `off` / `warn` / `error` (ADR-0018) | `block` / `warn` |\n| Gate | `--fail-on` selects severities | `block` short-circuits the runner |\n| Fields | `{ ID, Languages, Description, Requires, ScopeColumn, Query, DefaultMinMetric, Severity, Tags }` | `{ id, description, severity, run }` |\n\nBoth are \"rule whose findings cross a gate.\" Adding `polarity: must-hold \\| must-not-hold` and a pluggable evaluator collapses them to one shape mache hosts.\n\n## Proposed unified schema (sketch — to refine via ADR)\n\n```jsonc\n{\n \"id\": \"lint-passes\",\n \"description\": \"task lint exits 0\",\n \"polarity\": \"must-hold\", // NEW — defaults to \"must-not-hold\" for back-compat with smell-rules\n \"evaluator\": { // NEW — was implicit (SQL only)\n \"kind\": \"shell\", // alternatives: \"sql\", later \"wasi-component\" once cloister-c2a7c3 ships\n \"run\": \"task lint\"\n },\n \"severity\": \"error\", // unified to ADR-0018 enum; map cloister \"block\" → \"error\"\n \"tags\": [\"ci\", \"fast-gate\"],\n \"requires\": []\n}\n```\n\nSmell-rules today carry SQL via top-level `Query` + `ScopeColumn` + `Requires` — move those into `evaluator.kind: \"sql\"` for the new shape, keep the existing top-level fields as a back-compat reader.\n\n## What mache ships\n\n1. **ADR** — pick the unified schema; align severity vocab (kill the `block` alias or formalize it as a per-call gate alias for `error`).\n2. **Evaluator interface** in `cmd/` — Go interface with `SQL` + `Shell` impls; `find_smells` becomes evaluator-agnostic.\n3. **Back-compat** — existing `smellRegistry` entries + external `MACHE_SMELL_RULES_DIR` rules continue to load. New `polarity` field defaults to `must-not-hold` if absent.\n4. **CLI** — `mache find-smells` already exists (mache-hupl). Probably rename to `mache check` or `mache rules` since it's no longer smell-specific, with `--polarity=both|must-hold|must-not-hold` filtering. (Naming decision is part of the ADR.)\n5. **MCP tool surface** — `find_smells` either renames or gains a sibling `check_rules`. Either way it returns findings tagged by polarity so consumers (cloister done-runner) can interpret correctly.\n\n## Why this benefits \"tools that compose\"\n\nCloister's `done-runner.mjs` becomes a 30-line wrapper that:\n- Loads cloister/done-rules/*.json (and any other repo's rules dir cloister has been told about by `cluster.toml`)\n- Calls `mache check --polarity=must-hold` (or the MCP tool) with that rules-dir as input\n- Reports findings + exit code\n\ncloister no longer maintains an engine. mache becomes The Rule Engine for the ecosystem. The tools-repo template (cloister-c1b77e) ships `rules/*.json` (collapsed name once this lands) that mache picks up regardless of which tool authored them.\n\n## Related work\n\n- `mache-6z2e` (Sibling 'machelint' tool: rules engine over mache + LSP diagnostics) — broader generalization. This bead is narrower (just polarity + shell evaluator); machelint adds the LSP-diagnostic dimension. Decide at ADR time whether this work ships INSIDE mache or initializes machelint.\n- `mache-3f8w` (extract find_smells to ext/machelint when product split is right) — decides physical home. Polarity work should land before the physical split, so the extraction happens with the unified shape already in place.\n- `mache-hupl` (find-smells CLI, closed) — CLI surface this bead extends.\n- `mache-0yy4` (find-smells PR commenter GH Action, closed) — advisory pattern that's compatible; just gains a \"Required checks\" group for `must-hold` rules that fail.\n- `cloister-cf7a3b` (ADR-0026 tool composition implementation arc) — the resolver that lets cloister consume mache.\n- `cloister-c1b77e` (tools-repo template, just filed) — the template ships `rules/*.json` in the unified shape this bead defines.\n- (Closed) `cloister-c1123d` — previously held this work in the wrong repo; closed with redirect.\n\n## Future evaluator: wasi-component\n\nOnce cloister-c2a7c3 (Ring Seam reference impl crate) ships, a rule's `evaluator.kind` can be `\"wasi-component\"` — the rule itself is a sandboxed unit reading findings from the host via the Ring Seam. This is the elegant endgame; not in scope for v1, but worth not foreclosing in the schema.\n\n## Files (asserted scope)\n\n- `docs/adr/` — ADR for the unification\n- `cmd/smell_rules.go` — adds polarity + evaluator interface\n- `cmd/smell_rules_external.go` — back-compat reader\n- `cmd/serve_find_smells.go` — handler dispatch by evaluator\n- `cmd/find_smells_cli.go` — CLI flags\n- (Maybe) `cmd/run_shell_rule.go` (new) — shell evaluator\n- `examples/smell-rules/` — add a must-hold example","status":"open","priority":2,"issue_type":"design","owner":"architect-agent","created_at":"2026-07-23T04:49:13+00:00","updated_at":"2026-07-23T04:49:13+00:00","created_by":null,"dependency_count":0,"dependent_count":0,"comment_count":0,"dependencies":[],"comments":[],"repo":"mache","scope":"","external_ref":null,"branch":null,"pr_url":null,"acceptance_criteria":"","files":["docs/adr/","cmd/smell_rules.go","cmd/smell_rules_external.go","cmd/serve_find_smells.go","cmd/find_smells_cli.go","examples/smell-rules/"],"test_files":[],"derived_from":[]}