Skip to content

Commit 030500f

Browse files
HumanBean17claude
andauthored
docs(plans): execution plan for init/increment perf (PR-P1..PR-P3) (#339)
* docs(plans): execution plan for init/increment perf (PR-P1..PR-P3) Adds plans/active/PLAN-INIT-INCREMENT-PERF.md and the companion plans/AGENT-PROMPTS-INIT-INCREMENT-PERF.md implementing the approved proposal propose/active/INIT-INCREMENT-PERF-PROPOSE.md. Three PRs: - PR-P1: bulk in-memory-pyarrow COPY FROM for the full rebuild path; equivalence harness is the merge gate. - PR-P2: same primitive for the incremental path (Route-MERGE dedup retained). - PR-P3: lifespan-cached LayeredIgnore (ContextKey) + is_ignored _mega memo. No production code. Stacks behind proposal PR #338. Co-Authored-By: Claude <noreply@anthropic.com> * docs(plans): apply review feedback to init/increment perf plan 5-lens subagent review of the plan found the PR-P1/P2 boundary was architecturally wrong: the graph write helpers are SHARED between the full and incremental paths, so a "full-path-only" split is impossible. - Verified call graph: _write_edges/_write_routes_and_exposes/_write_nodes_impl/ _write_meta are each called by BOTH paths; _write_clients_producers_and_calls is incremental-only (global pass5/6). - Re-split by write-FUNCTION: PR-P1 = _bulk_copy + _write_edges (the ~250s prize, accelerates both paths); PR-P2 = _write_nodes_impl + _write_routes_and_exposes + _write_clients_producers_and_calls; PR-P3 = ignore cache (independent). - GraphMeta (_write_meta) left on MERGE (shared, one row) — reverses Open Q1. - Fixed all binding sentinel greps: PR-P1 zeros the edge _CREATE_* only; PR-P2 zeros node/route/client constants + _MERGE_SYMBOL only after both routes functions convert; PR-P3 sentinel narrowed to LayeredIgnore(project_root).is_ignored (the bare-constructor grep wrongly matched once-per-run sites :177/:569, which are correctly left alone). - Load-order §1f corrected (UnresolvedCallSite before UNRESOLVED_AT; Route/Client/Producer before their edges). Test files qualified (test_brownfield_routes / test_mcp_v2_compose / test_vectors_progress / test_path_filtering). PR-P2 tests placed in TestIncrementalOrchestrator. Baseline flagged as equivalence anchor, not production invariant. PR-P1 DoD lists the four test names. Co-Authored-By: Claude <noreply@anthropic.com> --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent 0396492 commit 030500f

2 files changed

Lines changed: 779 additions & 0 deletions

File tree

Lines changed: 373 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,373 @@
1+
# Agent task prompts — Faster init/increment (PR-P1 → PR-P3)
2+
3+
Status: **active**. One self-contained prompt per PR. Copy the prompt verbatim
4+
into the agent, attach the files in its `@-files` block, and let it execute.
5+
6+
> **Revised after the PR #339 subagent review.** PRs are split by **write-function**,
7+
> not by path — the graph write helpers are shared between the full and incremental
8+
> paths, so converting one accelerates both. Sentinel greps were corrected (the
9+
> PR-P3 "must return zero" grep previously over-matched once-per-run sites).
10+
11+
**Workflow per PR:**
12+
13+
1. Create the branch named in the prompt off the stated base.
14+
2. Read the cited plan section in full **before** writing code.
15+
3. Implement step-by-step; run the listed tests after each step.
16+
4. Run the sentinel greps — every "must return zero" line must be empty, every
17+
"must be non-zero" line must hit.
18+
5. Paste the manual-evidence output into the PR description.
19+
6. Open a PR with the exact title in the Definition of Done.
20+
21+
**Universal rules for every prompt:**
22+
23+
- Use only `.venv/bin/python` and `.venv/bin/pip` (never system python/pip).
24+
- `server.py` is stdio — never write to stdout from anything reachable by a tool handler.
25+
- Do not add a cocoindex dependency outside `java_index_flow_lancedb.py`.
26+
- The plan is the source of truth — if this prompt and the plan disagree, the plan wins.
27+
- Do not touch any file outside the prompt's `@-files` + the test files it names. If you think an adjacent file must change, **stop and ask**.
28+
- Do not loosen any existing test assertion to make it pass.
29+
- Breaking changes are allowed; no compatibility shims.
30+
31+
---
32+
33+
## PR-P1 — Bulk `COPY FROM` for `_write_edges` (shared; the ~250s prize)
34+
35+
**Branch:** `perf/bulk-graph-writes-p1` off `master`.
36+
**Base:** `master`.
37+
**Plan section:** `plans/active/PLAN-INIT-INCREMENT-PERF.md` § PR-P1 (read this first).
38+
**Estimated diff size:** medium (one module + tests + a fixture).
39+
40+
**Attach (`@-files`):**
41+
42+
- `@build_ast_graph.py` (focus: `_write_edges:3244`, `_node_row:2994`, `_SCHEMA_*:2812-2940`, `_callee_declaring_role_at_write:1647`, `seen_calls:3282`, `seen_ucs:3317`, `_populate_declares_rows:3189`)
43+
- `@propose/active/INIT-INCREMENT-PERF-PROPOSE.md` (design; on PR #338's branch — if absent, the staging invariants are inlined in the plan §PR-P1)
44+
- `@tests/test_ast_graph_build.py` (regression net + where new tests go)
45+
- `@tests/test_incremental_graph.py` (`_write_edges` is shared — incremental regression is binding)
46+
- `@tests/_builders.py` (graph-build helpers)
47+
48+
**Prompt:**
49+
50+
````
51+
You are implementing PR-P1 from `plans/active/PLAN-INIT-INCREMENT-PERF.md`.
52+
53+
Read the **PR-P1** section in full. The plan wins if this prompt disagrees.
54+
55+
KEY FACT: `_write_edges` (build_ast_graph.py:3244) is a SHARED helper called by
56+
BOTH the full path (write_ladybug:3926) and the incremental path (:805). So
57+
converting it accelerates both — there is no "full-only" edge conversion. PR-P1
58+
converts ONLY `_write_edges` (+ adds the `_bulk_copy` primitive). Nodes, routes,
59+
clients/producers, and GraphMeta are PR-P2.
60+
61+
## Scope
62+
63+
1. **Step-1 spike (first commit):** confirm the exact REL `COPY FROM` column
64+
naming + `pa.Table.from_pylist` typing with a throwaway 2-Symbol + 1-CALLS
65+
toy. Record the working incantation in the `_bulk_copy` docstring.
66+
2. Add `_bulk_copy(conn, table_name, columns, rows)` + the `_REL_*_COLUMNS` /
67+
column constants (REL tables list FROM/TO first; match `_SCHEMA_*` order).
68+
3. Convert `_write_edges` to per-edge-type row staging (apply the SAME
69+
`seen_calls`/`seen_ucs` dedup and SAME `_callee_declaring_role_at_write`
70+
lookup at staging, before appending), then `_bulk_copy` each REL table.
71+
Bulk-load UnresolvedCallSite NODE rows before UNRESOLVED_AT edges (Symbol
72+
nodes are already loaded by `_write_nodes`).
73+
4. Delete the dead module constants `_CREATE_EXT/IMPL/INJ/DECL/OVERRIDES/CALL`
74+
and the local `_CREATE_UNRESOLVED`/`_CREATE_UNRESOLVED_AT` (defined inside
75+
`_write_edges` at :3307/:3313 — removed with the rewrite).
76+
5. Generate + commit `tests/fixtures/graph_baseline_bank_chat.json` from the
77+
last per-row `_write_edges` build before removal.
78+
6. Add the four named tests.
79+
80+
## Out of scope (do NOT touch)
81+
82+
- Node writes (`_write_nodes`, `_write_nodes_impl`, `_write_nodes_merge`,
83+
`_CREATE_SYMBOL`, `_MERGE_SYMBOL`) — PR-P2.
84+
- Routes/clients/producers/calls (`_write_routes_and_exposes`,
85+
`_write_clients_producers_and_calls`, and their `_CREATE_*` constants
86+
including `_CREATE_CLIENT`/`_CREATE_PRODUCER`/`_CREATE_ROUTE` etc.) — PR-P2.
87+
- `_write_meta` / GraphMeta — leave the MERGE alone (PR-P2 also leaves it).
88+
- `java_index_flow_lancedb.py`, `path_filtering.py`, `server.py`.
89+
- Any schema/ontology/re-index change. CSV or Parquet-file staging.
90+
91+
If you find yourself wanting to touch any of the above, **stop and ask**.
92+
93+
## Deliverables
94+
95+
1. `_bulk_copy` helper + column-order constants.
96+
2. `_write_edges` stages per-type rows and bulk-loads (CALLS dedup + callee_declaring_role at staging; UnresolvedCallSite before UNRESOLVED_AT).
97+
3. Dead `_CREATE_EXT/IMPL/INJ/DECL/OVERRIDES/CALL` + locals removed.
98+
4. `tests/fixtures/graph_baseline_bank_chat.json` committed.
99+
5. Four new tests in `tests/test_ast_graph_build.py`.
100+
101+
## Tests
102+
103+
Run, all must pass:
104+
```
105+
.venv/bin/python -m pytest tests/test_ast_graph_build.py tests/test_incremental_graph.py -v
106+
.venv/bin/python -m pytest tests/test_bank_chat_brownfield_integration.py tests/test_call_edges_e2e.py -q
107+
.venv/bin/ruff check .
108+
```
109+
110+
Sentinel greps — **must return zero**:
111+
```
112+
grep -nE "_CREATE_(EXT|IMPL|INJ|DECL|OVERRIDES|CALL|UNRESOLVED|UNRESOLVED_AT)\b" build_ast_graph.py
113+
```
114+
115+
Sentinel greps — **must be non-zero** (guards against over-deletion; these belong to PR-P2 / are retained):
116+
```
117+
grep -n "_MERGE_SYMBOL\b" build_ast_graph.py # node upsert, kept until PR-P2
118+
grep -n "_CREATE_CLIENT\b" build_ast_graph.py # routes/clients, PR-P2
119+
grep -n "MERGE (r:Route" build_ast_graph.py # pass5/6 dedup, kept
120+
grep -n "COPY .*FROM \$rows" build_ast_graph.py # bulk path present
121+
```
122+
123+
## Manual evidence (paste in PR description)
124+
125+
```bash
126+
rm -rf /tmp/p1 && .venv/bin/python build_ast_graph.py \
127+
--source-root tests/bank-chat-system \
128+
--ladybug-path /tmp/p1/code_graph.lbug --verbose
129+
.venv/bin/java-codebase-rag meta --source-root tests/bank-chat-system --index-dir /tmp/p1
130+
```
131+
Expected: meta `counts_json` + node/edge counts identical to a pre-PR per-row
132+
build (paste both). Note the graph-write phase timing from `JCIRAG_PROGRESS`
133+
lines vs the pre-PR baseline.
134+
135+
## Definition of Done
136+
137+
- [ ] Step-1 spike result recorded in `_bulk_copy` docstring.
138+
- [ ] `_write_edges` stages per-type rows (CALLS dedup + callee_declaring_role at staging); UnresolvedCallSite bulk-loaded before UNRESOLVED_AT.
139+
- [ ] `_CREATE_EXT/IMPL/INJ/DECL/OVERRIDES/CALL` + local `_CREATE_UNRESOLVED/_UNRESOLVED_AT` deleted.
140+
- [ ] `test_bulk_write_edges_match_per_row_baseline`, `test_bulk_write_is_deterministic_double_build`, `test_bulk_write_preserves_calls_dedup_and_callee_declaring_role`, `test_bulk_write_empty_rel_table_is_noop` pass.
141+
- [ ] Full `test_ast_graph_build.py` + `test_incremental_graph.py` pass unchanged.
142+
- [ ] Sentinel greps: zero where required, non-zero where required.
143+
- [ ] `.venv/bin/ruff check .` clean; benchmark in PR description.
144+
- [ ] PR title: `perf(graph): bulk COPY FROM for _write_edges (PR-P1)`.
145+
````
146+
147+
---
148+
149+
## PR-P2 — Bulk write for nodes + routes/clients/producers/calls
150+
151+
**Branch:** `perf/bulk-graph-writes-p2` off PR-P1's branch (or `master` if PR-P1 merged).
152+
**Base:** PR-P1 merged.
153+
**Plan section:** `plans/active/PLAN-INIT-INCREMENT-PERF.md` § PR-P2 (read this first).
154+
**Estimated diff size:** medium.
155+
156+
**Attach (`@-files`):**
157+
158+
- `@build_ast_graph.py` (`_write_nodes_impl:3029`, `_write_nodes:3096`, `_write_nodes_merge:817`, `_CREATE_SYMBOL`, `_MERGE_SYMBOL`, `_write_routes_and_exposes:3338`, `_write_clients_producers_and_calls:3810`, the Route `MERGE (r:Route` at `:3819`, `_write_meta:3421`, `_bulk_copy` + `_REL_*_COLUMNS` from PR-P1)
159+
- `@tests/test_incremental_graph.py` (regression; add to `TestIncrementalOrchestrator`)
160+
161+
**Prompt:**
162+
163+
````
164+
You are implementing PR-P2 from `plans/active/PLAN-INIT-INCREMENT-PERF.md`.
165+
Read the **PR-P2** section in full. It reuses PR-P1's `_bulk_copy` +
166+
`_REL_*_COLUMNS`. The plan wins.
167+
168+
## Scope
169+
170+
1. Convert `_write_nodes_impl` (:3029, the shared workhorse called by
171+
`_write_nodes` full + `_write_nodes_merge` incremental) to stage Symbol rows
172+
then `_bulk_copy(conn, "Symbol", NODE_COLUMNS, rows)`. Delete `_CREATE_SYMBOL`
173+
and `_MERGE_SYMBOL` (both dead once the workhorse is bulk). Do the existing
174+
`resolve_role_and_capabilities` + `type_role_by_node_id` population before
175+
staging, unchanged.
176+
2. Convert `_write_routes_and_exposes` (:3338, shared) to per-table staging +
177+
`_bulk_copy` for Route/EXPOSES/Client/Producer/DECLARES_CLIENT/DECLARES_PRODUCER/
178+
HTTP_CALLS/ASYNC_CALLS (keep the existing `_file_by_node_id`/`_file_by_client_id`/
179+
`_file_by_producer_id` source_file resolution). Bulk-load Route/Client/Producer
180+
NODES before the EXPOSES/DECLARES_*/HTTP_CALLS/ASYNC_CALLS edges. Delete
181+
`_CREATE_ROUTE`/`_CREATE_EXPOSES`.
182+
3. Convert `_write_clients_producers_and_calls` (:3810, incremental-only global
183+
pass5/6) Client/Producer/edge writes to per-type staging + `_bulk_copy` (keep
184+
the `member_by_id`/`client_by_id`/`producer_by_id` resolution). **Retain the
185+
`MERGE (r:Route {id:$id}) …` dedup (:3819-3828) verbatim** + add a one-line
186+
comment it is intentionally kept. Now delete the 6 shared constants —
187+
`_CREATE_CLIENT`/`_CREATE_PRODUCER`/`_CREATE_DECLARES_CLIENT`/
188+
`_CREATE_DECLARES_PRODUCER`/`_CREATE_HTTP_CALL`/`_CREATE_ASYNC_CALL` — which
189+
are dead only after BOTH routes/exposes and clients_producers functions convert.
190+
4. Leave `_write_meta` (:3421) and its `MERGE (m:GraphMeta …)` UNTOUCHED.
191+
5. Add the two named tests as methods of `TestIncrementalOrchestrator` in
192+
`tests/test_incremental_graph.py`.
193+
194+
## Out of scope (do NOT touch)
195+
196+
- `_write_edges` (done in PR-P1).
197+
- `_write_meta` / GraphMeta MERGE — leave it.
198+
- `_delete_file_scope`, `incremental_rebuild` algorithm, dependent-expansion,
199+
crash-marker logic.
200+
- Anything outside `build_ast_graph.py` + `tests/test_incremental_graph.py`.
201+
202+
If you find yourself wanting to touch any of the above, **stop and ask**.
203+
204+
## Deliverables
205+
206+
1. `_write_nodes_impl` bulk; `_CREATE_SYMBOL` + `_MERGE_SYMBOL` deleted.
207+
2. `_write_routes_and_exposes` bulk; `_CREATE_ROUTE`/`_CREATE_EXPOSES` deleted.
208+
3. `_write_clients_producers_and_calls` Client/Producer/edges bulk; `MERGE (r:Route)` retained + commented; 6 shared `_CREATE_*` deleted.
209+
4. `_write_meta` untouched.
210+
5. Two new tests in `TestIncrementalOrchestrator`.
211+
212+
## Tests
213+
214+
Run, all must pass:
215+
```
216+
.venv/bin/python -m pytest tests/test_incremental_graph.py tests/test_ast_graph_build.py -v
217+
.venv/bin/ruff check .
218+
```
219+
220+
Sentinel greps — **must return zero**:
221+
```
222+
grep -nE "_CREATE_(SYMBOL|ROUTE|EXPOSES|CLIENT|PRODUCER|DECLARES_CLIENT|DECLARES_PRODUCER|HTTP_CALL|ASYNC_CALL)\b" build_ast_graph.py
223+
grep -nE "_MERGE_SYMBOL\b" build_ast_graph.py
224+
```
225+
226+
Sentinel greps — **must be non-zero** (Route dedup + GraphMeta MERGE retained; bulk present):
227+
```
228+
grep -n "MERGE (r:Route" build_ast_graph.py
229+
grep -n "MERGE (m:GraphMeta" build_ast_graph.py
230+
grep -n "COPY .*FROM \$rows" build_ast_graph.py
231+
```
232+
233+
## Manual evidence (paste in PR description)
234+
235+
Single-file change equivalence:
236+
```bash
237+
# set up an index, touch one file, increment (bulk), then full-rebuild the same
238+
# state (bulk) and diff graphs (node count, per-type edge counts, GraphMeta).
239+
```
240+
Expected: incremental(bulk) == full-rebuild(bulk) for that state. Paste side-by-side counts.
241+
242+
## Definition of Done
243+
244+
- [ ] `_write_nodes_impl` bulk; `_CREATE_SYMBOL` + `_MERGE_SYMBOL` deleted.
245+
- [ ] `_write_routes_and_exposes` bulk (Route/Client/Producer before edges); `_CREATE_ROUTE`/`_CREATE_EXPOSES` deleted.
246+
- [ ] `_write_clients_producers_and_calls` Client/Producer/edges bulk; `MERGE (r:Route)` retained + commented; 6 shared `_CREATE_*` deleted.
247+
- [ ] `_write_meta` untouched.
248+
- [ ] `test_incremental_bulk_write_equivalent_to_full_rebuild`, `test_incremental_route_merge_dedup_preserved` (both in `TestIncrementalOrchestrator`) pass; full `test_incremental_graph.py` + `test_ast_graph_build.py` green.
249+
- [ ] Sentinel greps pass.
250+
- [ ] `.venv/bin/ruff check .` clean.
251+
- [ ] PR title: `perf(graph): bulk COPY FROM for nodes, routes, clients/producers (PR-P2)`.
252+
````
253+
254+
---
255+
256+
## PR-P3 — Cached `LayeredIgnore` + `is_ignored` memo
257+
258+
**Branch:** `perf/cached-ignore-p3` off `master`.
259+
**Base:** `master` (independent of PR-P1/P2).
260+
**Plan section:** `plans/active/PLAN-INIT-INCREMENT-PERF.md` § PR-P3 (read this first).
261+
**Estimated diff size:** small.
262+
263+
**Attach (`@-files`):**
264+
265+
- `@java_index_flow_lancedb.py` (`ContextKey` defs `:60-72`, `coco_lifespan` provide sites `:287-306`, `process_java_file:345`/`process_sql_file:417`/`process_yaml_file:465`)
266+
- `@path_filtering.py` (`LayeredIgnore`, `_mega:334`, `_mega_build_for_rel:193`, `is_ignored:345`, `diagnose_dict:377`)
267+
- `@tests/test_path_filtering.py` (where the two memo unit tests go)
268+
- `@tests/test_lancedb_e2e.py` (HEAVY once-per-flow test)
269+
270+
**Prompt:**
271+
272+
````
273+
You are implementing PR-P3 from `plans/active/PLAN-INIT-INCREMENT-PERF.md`.
274+
275+
Read the **PR-P3** section in full. Independent of PR-P1/P2. The plan wins.
276+
277+
KEY FACT: `LayeredIgnore(project_root)` appears at FIVE sites in
278+
java_index_flow_lancedb.py — :177, :351, :423, :471, :569. PR-P3 converts ONLY
279+
the three `process_*_file` sites (:351/:423/:471). The other two (:177 in
280+
`_approximate_vectors_total`, :569 in the app_main pre-walk) call
281+
`cocoindex_excluded_patterns()` ONCE PER RUN — leave them alone.
282+
283+
## Scope
284+
285+
1. Define `IGNORE = coco.ContextKey[LayeredIgnore]("java_lance_layered_ignore")`
286+
alongside `PROJECT_ROOT`/`EMBEDDER`/`LANCE_DB` (:60-72), reusing the SAME
287+
`_ck_params` (`detect_change` vs `tracked`) detection block.
288+
2. In `coco_lifespan` (:287-306), add `builder.provide(IGNORE, LayeredIgnore(root))`
289+
— built ONCE per flow run.
290+
3. In `process_java_file`/`process_sql_file`/`process_yaml_file`: add
291+
`ignore = coco.use_context(IGNORE)` and replace
292+
`LayeredIgnore(project_root).is_ignored((project_root / file.file_path.path).resolve())`
293+
with `ignore.is_ignored((project_root / file.file_path.path).resolve())`.
294+
Keep `project_root`. DO NOT touch :177 or :569.
295+
4. In `path_filtering.py` `LayeredIgnore`: add `self._mega_cache` in `__init__`
296+
and memoize `_mega(rel)` keyed by `Path(rel_project).parent.as_posix()`
297+
(`_mega_build_for_rel` reads only `dir_parts`, so this is correct).
298+
5. Add the three named tests in the right files.
299+
300+
## Out of scope (do NOT touch)
301+
302+
- `build_ast_graph.py` and the graph write path (PR-P1/P2).
303+
- The ignore *decision* logic (`_mega_build_for_rel`, `_winning_row`, negation
304+
scanning) — only memoize.
305+
- Sites :177 and :569.
306+
- Any schema/ontology/re-index change. Loosening any existing test.
307+
308+
If you find yourself wanting to touch any of the above, **stop and ask**.
309+
310+
## Deliverables
311+
312+
1. `IGNORE` ContextKey (version-detected) + provided once in `coco_lifespan`.
313+
2. The three `process_*_file` consume it; :177/:569 untouched.
314+
3. `_mega` memoized by directory in `LayeredIgnore`.
315+
4. Three tests: two in `tests/test_path_filtering.py`, one (HEAVY) in `tests/test_lancedb_e2e.py`.
316+
317+
## Tests
318+
319+
Run, all must pass:
320+
```
321+
.venv/bin/python -m pytest tests/test_path_filtering.py tests/test_lancedb_e2e.py -q
322+
.venv/bin/python -m pytest tests -q -k "ignore or path_filter or vectors_progress"
323+
.venv/bin/ruff check .
324+
```
325+
Heavy (only if you can run cocoindex e2e locally):
326+
```
327+
JAVA_CODEBASE_RAG_RUN_HEAVY=1 .venv/bin/python -m pytest tests/test_lancedb_e2e.py -q
328+
```
329+
330+
Sentinel greps — **must return zero** (matches ONLY the 3 process sites; :177/:569 use the bare constructor + `cocoindex_excluded_patterns`, not `.is_ignored`):
331+
```
332+
grep -nE "LayeredIgnore\(project_root\)\.is_ignored" java_index_flow_lancedb.py
333+
```
334+
335+
Sentinel greps — **must be non-zero**:
336+
```
337+
grep -n "coco.use_context(IGNORE)" java_index_flow_lancedb.py # 3 sites
338+
grep -n "_mega_cache" path_filtering.py # memo present
339+
```
340+
341+
## Manual evidence (paste in PR description)
342+
343+
With `JAVA_CODEBASE_RAG_RUN_HEAVY=1`, run the flow over a small corpus and log
344+
`id(ignore)` per file (temporary instrumentation) — confirm a single object id
345+
across all files. Then micro-benchmark `is_ignored` over N files: same-directory
346+
files hit the `_mega` cache.
347+
348+
## Definition of Done
349+
350+
- [ ] `IGNORE` ContextKey (version-detected) + provided once in `coco_lifespan`.
351+
- [ ] The three `process_*_file` consume it; :177/:569 untouched.
352+
- [ ] `_mega` memoized by directory; `is_ignored`/`diagnose_dict` results unchanged.
353+
- [ ] `test_is_ignored_mega_caches_by_directory`, `test_layered_ignore_memo_preserves_decisions` (in `tests/test_path_filtering.py`), `test_layered_ignore_provided_once_per_flow` (in `tests/test_lancedb_e2e.py`, HEAVY) pass.
354+
- [ ] Existing ignore + vectors-progress tests pass unchanged.
355+
- [ ] Sentinel greps pass.
356+
- [ ] `.venv/bin/ruff check .` clean.
357+
- [ ] PR title: `perf(vectors): lifespan-cached LayeredIgnore + is_ignored memo (PR-P3)`.
358+
````
359+
360+
---
361+
362+
## Notes for the orchestrator
363+
364+
- **Landing order:** PR-P1 → PR-P2 (P2 needs `_bulk_copy` + `_REL_*_COLUMNS`).
365+
PR-P3 is independent (can go first, last, or between).
366+
- **Shared-helper awareness:** `_write_edges`, `_write_routes_and_exposes`,
367+
`_write_nodes_impl`, `_write_meta` are each called by BOTH paths. Converting
368+
one accelerates both — so `test_incremental_graph.py` is a binding gate for
369+
PR-P1 and PR-P2, not just PR-P2.
370+
- **Review between PRs** (`superpowers:requesting-code-review`): the equivalence
371+
harness gates P1/P2; the memo-parity test gates P3.
372+
- **Sentinel greps are binding:** a non-empty "must return zero" grep = scope
373+
leak; an empty "must be non-zero" grep = over-deletion. Either blocks merge.

0 commit comments

Comments
 (0)