agent-file-tools 0.56.0

Agent File Tools — tree-sitter powered code analysis for AI agents
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
# Incremental derived-view materialization

## Interface and durability

`apply_manifest_diff(path, base, next, callgraph_blob_database)` updates a **private copy** of the base generation's derived database. The publication owner supplies that copy and owns generation paths, copying/reflinks, fsync, pointer CAS and reader handles. This function does not replace a published file. One immediate SQLite transaction contains graph rows, dependency state and the manifest fingerprint; a missing blob or another error rolls it back. The previous generation's file remains unchanged.

The cold writer remains available through its old callgraph-store re-export. The materializer shares the existing graph schema, rather than duplicating it. Edges are owned by their `ref_id` through `refs.caller_file`; node IDs encode path, scoped name and AST ordinal, not SQLite rowids. Changed target ordinals therefore require incoming reference/edge relinking.

Derived metadata version **4** includes `view_manifest_fingerprint` and `view_materialization_version`. A mismatched fingerprint is refused. An older materialization version, or a legacy clone with neither diff metadata key, takes the cold path, preventing use of a database without binding dependencies. `view_bindings` is file-owned, and the existing `file_dependencies(file_path, dep_file)` table stores reverse-queryable dependency rows. Other legacy side tables are not populated by either view materialization path.

## Selection and remaining corpus work

Cold joins retain each bound reference's dependency candidates, resolved targets and source-file probes, including missing canonical paths. Binding caches use vector positions, not AST ordinals: real structural references can share an ordinal. Incremental joins start with changed paths and take the transitive reverse-dependency closure. Thus an unchanged barrel referring to a not-yet-existing module invalidates its unchanged importers when that module appears.

Binding work and reference work are separate. Unchanged callers reuse bindings unless membership probes changed. Candidate dependents replay the resolver-index queries consumed by their previous resolution (exports, aliases, nodes, modules and reexports); only changed answers require re-resolution. An unrelated appended export therefore does not invalidate a caller of an unchanged symbol. Queried files become dependencies even when they were not final targets. Rust crate-wide inline-module/parent lookups also depend on a module-index domain rechecked on manifest changes. Stable reference/edge tuples are not rewritten.

The resolver reads `package.json`, `tsconfig.json`, `pnpm-workspace.yaml` and `Cargo.toml` through its manifest facts. Changes to those names, explicitly marked resolution inputs, synthetic entries or symlink/gitlink identity force full resolution. Existing directory probes are excluded from persisted caller dependencies: workspace-discovery memo hits omit those incidental probes, and configuration changes invalidate discovery globally. File probes and misses are retained. Rust's absent declared-module candidates need recording **before** `FactPaths`' canonicalization can reject them.

The join restores compact persisted per-file symbol surfaces and rebuilds only changed or membership-invalidated entries. Manifest facts and lazily loaded immutable payloads are memoized for the duration of that join, with dependency probes recorded on hits as well as misses. Unchanged callers decode only after their consumed surface queries change. Row emission loads only changed owners and the callers/targets needed by actual emitted references. Restoring compact lookup maps and loading binding caches still scales with the manifest; expensive source/AST decode, binding and resolution do not. There is no separate semantic-plane materialization in this function. Historical version 3 measurements below predate persisted file surfaces.

## Measurements (Darwin, debug builds)

Input was copied from opencode view `0f3900af641f5248` and immutable callgraph blob database `aa69d52ef2dcad4d`; no probe writes to live storage. Consecutive retained manifests **14 → 15** contain **17 changed entries**, not 300: 16 removals and one addition; total membership 7,060 → 7,045. This is the real pair available in the supplied artifact. A separate controlled fixture covers 300 changes.

All bytes below are literal byte counts. Physical/logical counters use Darwin `RUSAGE_INFO_V4`; CPU uses `getrusage`; WAL is measured while a keeper connection remains open. Both branches start from copies of the same freshly materialized base, use WAL with the existing writer defaults, and include writer connection close/checkpoint effects. Copying and base preparation are outside the measured interval. Row parity includes **every derived table**, including metadata and dependency caches.

### Real 17-path pair

| implementation | physical bytes | logical bytes | WAL bytes | wall seconds | CPU seconds |
| --- | ---: | ---: | ---: | ---: | ---: |
| Full rewrite before dependency caching | 541,216,768 | 1,027,746,902 | 271,133,112 | 289.908 | 278.424 |
| Write-bounded, still full resolution | 19,386,368 | 11,748,000 | 4,379,592 | 297.580 | 279.096 |
| Version 2 full rewrite, including dependency cache | 674,893,824 | 1,160,887,532 | 336,517,512 | 220.138 | 214.764 |
| Version 2 incremental binding/resolution | **28,332,032** | **16,664,736** | **6,752,712** | **90.804** | **89.920** |
| Version 3 full rewrite, including surface cache | 848,138,240 | 1,350,133,088 | 424,739,072 | 199.779 | 188.604 |
| Version 3 surface-pruned incremental | **34,279,424** | **25,155,920** | **9,595,512** | **49.467** | **48.519** |

Version 2 incremental selection: **529 unchanged dependents**, 530 current files resolved, 78,989 references resolved, versus cold 4,921 files / 299,487 references. Graph writes: 949 owned deletions, 43 owned insertions, 28 relink deletions, 15 relink insertions. Dependency-cache writes: 784 deletions and 415 insertions. Physical writes are 27.02 MiB, rather than hundreds of MiB. The cold CPU numbers vary across debug runs on a shared machine; the final paired measurement is the like-for-like comparison.

Version 3 reduces that to **29 unchanged dependents**, 30 current files and **7,546 references** resolved. Its physical write delta is 32.69 MiB and CPU is 48.519 seconds, versus 188.604 seconds for the paired full rewrite. Surface-cache rows add storage and modest write overhead compared with version 2 while eliminating most dependent reference work.

The real parity gate initially caught 20 excess dependency rows caused by incidental workspace-directory probes. Excluding existing directories while retaining file probes/misses fixed that discrepancy. The surface-cache gate subsequently exposed a real `tool.ts` reexport/export-alias ordinal collision: keying cached bindings by ordinal merged distinct dependencies. Unique vector-position cache keys fix it, while emitted reference rows preserve the original cold writer's first-reference lookup. The final version 3 run passes every-table parity.

### Controlled 300 changed paths out of 5,056 TypeScript files

Each file has a local caller and callee; the first 300 files receive a leading newline. This is not a claim about 300 real drill changes.

| operation | physical bytes | logical bytes | WAL bytes | wall seconds | CPU seconds |
| --- | ---: | ---: | ---: | ---: | ---: |
| Full rewrite | 30,253,056 | 21,394,768 | 10,089,912 | 4.308 | 3.392 |
| Incremental | 18,010,112 | 21,485,816 | 8,132,912 | 1.919 | 1.675 |

Incremental resolves exactly 300 files / 300 references, with zero unchanged dependents. It writes 3,000 graph rows plus 1,200 dependency rows, versus 50,560 graph rows plus 20,224 dependency rows cold. SQLite page/index locality and checkpoint effects mean this small fixture's logical byte counter does not improve despite exact row bounds. All-table parity passes.

## Reproduction and guards

Place copied `base.json`, `next.json` and `callgraph.sqlite` in an offline input directory, then run:

```sh
AFT_VIEW_DIFF_INPUT="$PWD/target/view-diff-input" cargo test -p agent-file-tools --lib views::materialization::tests::bench_real_manifest_diff -- --ignored --exact --nocapture
cargo test -p agent-file-tools --lib views::materialization::tests::bench_controlled_300_path_diff -- --ignored --exact --nocapture
cargo test -p agent-file-tools --lib views::materialization::tests
```

The real probe prints and retains its measured database directory under the offline input so parity failures can be inspected without rerunning the expensive cold build. The comparison reports only the first mismatching table and a bounded row sample.

The small edit/add/remove fixture writes exactly **13 graph rows + 6 dependency/surface-cache rows**. The original full rewrite performs **23 graph writes**. Mutation controls demonstrate that skipping incoming relinks fails edge parity, forcing a full rewrite fails exact work counts, skipping transitive dependents fails the new-reexport edge, and omitting missing canonical probes fails the Rust module-addition fixture. Each mutation is restored before any commit.

## Release branch drill after integration

Two both-arm runs used this worktree's optimized `aft` binary, copied warm non-view caches, and a fresh view directory so the warm-up built the matching materialization schema. The opencode checkout and baseline were restored by the drill; the live daemon and live view storage were not subjects. Generated reports in the investigation directory were copied to `target/` and restored rather than committed over the existing investigation.

### First run: dependency closure without consumer-surface pruning

| switch | publication ms | views CPU s | legacy CPU s | views correct ms | legacy correct ms |
| --- | ---: | ---: | ---: | ---: | ---: |
| HEAD → a085bf62a459 | 67,657 | 72.18 | 30.23 | 67,930 | 27,354 |
| a085bf62a459 → HEAD | 63,562 | 68.32 | 28.01 | 63,562 | 23,646 |
| HEAD → upstream/v2-timeouts | 61,571 | 66.47 | 28.85 | 61,571 | 24,329 |
| upstream/v2-timeouts → HEAD | 63,312 | 68.82 | 30.98 | 63,640 | 25,963 |

All correctness probes converged. The drill incorrectly reported 15 puts from the absolute manifest membership delta because it did not parse the new root-owned publication phase profile. Actual profile counters were zero. The attribution fix now prefers those profiles; its unit test goes red if they are ignored.

### Final run: consumer surfaces, unique binding positions, lazy row emission

Measured code: `d81a33a4` (optimized build), observed **2026-09-12T20:04:02Z**. Warm-up: views 76,294 ms; legacy 5,506 ms. This is the actual captured table; the script's static narrative and historical SHA label are not reused as attribution.

| switch | on publication_ms | on puts | on embeds | on cpu_s | on rss_delta_mb | on correct_ms | off publication_ms | off puts | off embeds | off cpu_s | off rss_delta_mb | off correct_ms |
|---|---:|---:|---:|---:|---:|---:|---:|---:|---:|---:|---:|---:|
| HEAD→a085bf62a459 | 70103 | 0 | 0 | 77.64 | 1935.953 | 70646 | — | — | 3 | 78.1 | 2689.516 | 65825 |
| a085bf62a459→HEAD | 70415 | 0 | 0 | 75.7 | 25.078 | 71031 | — | — | 4 | 68.71 | 101.141 | 56743 |
| HEAD→refs/remotes/upstream/v2-timeouts | 66092 | 0 | 0 | 71.48 | -45.141 | 66092 | — | — | 3 | 68.03 | -331.625 | 55434 |
| refs/remotes/upstream/v2-timeouts→HEAD | 64636 | 0 | 0 | 70.63 | -87.438 | 65024 | — | — | 4 | 60.58 | -447.469 | 52275 |

Every correctness probe passed, every views publication put/embedded zero blobs/batches, and the script reported no defects. **The stronger shipping criterion is still unmet: views does not beat legacy on every case.** It is slower to correctness on all four transitions, and uses more CPU on three. Do not treat a zero-defect script exit as a performance pass. The substantially different legacy CPU times between runs also rule out presenting these wall/CPU observations as a controlled cross-run speedup.

Profile attribution for the final run:

| switch | unchanged consumers re-resolved | current files resolved | references resolved | derived phase ms | CAS ms |
| --- | ---: | ---: | ---: | ---: | ---: |
| HEAD → A | 296 | 556 | 93,230 | 37,632 | 13 |
| A → HEAD | 296 | 569 | 95,222 | 35,499 | 17 |
| HEAD → B | 323 | 583 | 96,254 | 34,951 | 13 |
| B → HEAD | 323 | 596 | 98,078 | 33,174 | 13 |

Compared with the first run's 1,335 / 1,146 unchanged dependents and 188,185 / 174,750 forward references, selection is materially smaller. However, complete symbol-index reconstruction, emitting tens of thousands of changed-owner rows, and persisted surface-query cache size remain material costs. The final derived database is approximately 424–428 MB versus 335–339 MB before surface recording. This delivery establishes parity, bounded writes and narrower resolution; it does not establish that enabling views is ready to ship.

Retained local evidence: `target/branch-drill-surface.json`, `target/branch-drill-surface.stderr.log`, and `target/view-diff-input/.tmpuNg1g2/{base,cold,incremental}.sqlite`. These are offline artifacts, not live stores.

### Offline measurement of the real 300-Git-path transition

The final drill produced a better input pair than the older retained artifact: its generations 1 → 2 correspond to HEAD → A, **300 changed Git paths and 276 changed manifest entries**. Those immutable manifests and the closed blob database were copied inside this worktree and measured separately with the same benchmark. This is additional offline measurement, not another drill run. The initial read-only Python backup opener returned `SQLITE_CANTOPEN`; a main-file clone of the closed, WAL-free callgraph blob database succeeded instead.

| operation | physical bytes | logical bytes | WAL bytes | wall seconds | CPU seconds |
| --- | ---: | ---: | ---: | ---: | ---: |
| Full rewrite | 846,159,872 | 1,340,615,544 | 422,897,432 | 263.089 | 208.216 |
| Incremental | 379,342,848 | 487,851,246 | 143,586,152 | 291.479 | 167.437 |

Every-table parity passes on this real transition. Incremental work is 110,596 graph-row operations plus 36,923 dependency/surface-cache operations, versus 821,633 plus 344,787 cold; 296 unchanged consumers / 556 total files / 93,230 references are resolved. **The requested tens-of-MiB write target is not met on this larger real transition:** incremental physical writes are 361.77 MiB, though below 806.96 MiB cold. Wall time also did not improve in this offline sample. The successful 17-entry measurement must not be substituted for this larger case. Combined with the final drill, this is an explicit remaining acceptance gap, not a shipping recommendation.

Retained databases: `target/view-diff-real-300-input/.tmpoSckwe/{base,cold,incremental}.sqlite`.

## Release profiling prerequisite run (2026-09-12, base e2589c10)

The previous worker's offline artifacts were unavailable in the new worktree. A fresh release binary (`cargo build --release -p agent-file-tools --bin aft`, passed) regenerated the input with `scripts/views-branch-drill.sh --mode both --binary "$PWD/target/release/aft" --storage "$PWD/target/branch-drill-baseline"`. No optimization was applied. The script restored opencode to `5716f8ba60e79ec60ec485b6e5291c0b0bc1f252` with a clean checkout. Generated reports were retained under `target/branch-drill-baseline.{json,md}` rather than changing the historical investigation reports.

Generations 1 → 2 are **7,060 → 7,045 entries and 276 changed entries** across 300 Git paths. The membership-count difference of 15 is not the changed-entry count. Fingerprints:

- Base: `c3d6ca11eca18e25d2e58e52629921215cdea1d4f23ce9278941c1a13faa95d1`
- Next: `a632b1603f62b77a16c041435b34764810ec2919461ae14fba9c2a70b49fdace`

The pair is copied to `target/view-diff-real-300-input/{base,next}.json`, beside the supplied offline `callgraph.sqlite`. Equality comparison of entries keyed by `rel_path`, including plane keys and metadata, confirms 276 changes. These fingerprints must not be classified as a 17-entry pair merely from membership counts.

### Unmodified release drill baseline

Observed `2026-09-12T22:16:10Z`. Views warm-up: 561,819 ms; legacy warm-up: 2,511 ms. Fresh storage did not reproduce the earlier zero-embedding forward legs. The generated report's static narrative claims zero embeddings, but the actual table below does not; only the table is evidence for this run.

| switch | views publication ms | views puts | views embeds | views CPU s | views correct ms | legacy embeds | legacy CPU s | legacy correct ms |
| --- | ---: | ---: | ---: | ---: | ---: | ---: | ---: | ---: |
| HEAD → A | 39093 | 0 | 3 | 104.41 | 39093 | 3 | 30.89 | 27679 |
| A → HEAD | 35377 | 0 | 0 | 56.23 | 35376 | 4 | 27.93 | 23747 |
| HEAD → B | 38490 | 0 | 1 | 61.68 | 38794 | 3 | 29.73 | 25009 |
| B → HEAD | 35554 | 0 | 0 | 56.77 | 35857 | 4 | 30.25 | 25513 |

Views remains slower to correctness by 11,414 / 11,629 / 13,785 / 10,344 ms. This is an unmodified baseline, not a before/after optimization comparison or a shipping pass.

### Blocked release benchmark

`cargo test --release -p agent-file-tools --lib views::materialization::tests::bench_real_manifest_diff --no-run` fails before producing a benchmark executable: 17 E0425 errors in `gh_shim.rs` tests reference `DEV_MANIFEST_KEY_ID` / `DEV_MANIFEST_PUBLIC_KEY`, whose definitions are gated by `#[cfg(debug_assertions)]` at lines 2180–2183. The production release binary builds, but the release library test target does not. That file is outside the materialization/selected-join fence, and changing security-key compilation or enabling debug assertions was not used as a measurement workaround. Fix the release-test cfg mismatch separately, then run the release benchmark on the retained pair with sampling before selecting an optimization. No bucket attribution, optimized measurements, work-count mutation proofs, or parity/warnings acceptance is claimed by this prerequisite run.

## Release profile and persistent file surfaces (resumed)

The release-test prerequisite was supplied as `57be7181d` and cherry-picked without modifying its test-cfg changes. The original drill table above is retained verbatim. After the task's original `target/` artifacts were reclaimed, a second unmodified both-arm drill regenerated the same two fingerprints and **276** changed entries; its reports are `target/branch-drill-recovered.{json,md}`. The offline input now uses a SQLite backup of that isolated drill's blob database, not live storage.

### Release attribution before the optimization

`cargo test --release -p agent-file-tools --lib views::materialization::tests::bench_real_manifest_diff --no-run` passed. The resulting test executable was run with the real input while `/usr/bin/sample <pid> 180 10 -file target/real-release-before.sample.txt` sampled its lifetime (the benchmark exited after 90.25 s). Every-table parity passed. This sample includes base preparation, cold replacement, incremental replacement and snapshot comparison: its aggregated stack counts must not be labeled incremental-only CPU. The stacks show manifest blob reads through SQLite `pread`, decoding/binding in the selected join, resolver work, SQLite B-tree writes, and checkpoint I/O. A subsequent same-input run with `AFT_VIEW_PROFILE=1` supplies **wall-time phase boundaries**, not inferred CPU times, below. SQLite index maintenance is included with the corresponding writes; commit includes synchronization and checkpoint work and is not a pure fsync counter.

| incremental phase | before ms | persistent surfaces ms |
| --- | ---: | ---: |
| Binding-cache load and dependency selection | 811.948 | 616.234 |
| SQLite owned deletions, including indexes | 1688.395 | 1043.251 |
| Changed-owner blob decode and node/file inserts | 798.042 | 565.182 |
| Eager whole-manifest blob fetch | 3339.224 | 0.000 |
| Blob decode, binding and file symbol-index construction/restoration | 4756.353 | 895.065 |
| Global index setup and consumer-surface replay | 60.083 | 52.130 |
| Deferred decode/bind of consumers that failed surface replay | included above | 443.226 |
| Reference resolution and surface/dependency recording | 2016.768 | 2111.027 |
| Dependency union | 60.367 | 74.248 |
| Entire selected join, including allocations/drop overhead | 10289.146 | 3600.256 |
| SQLite binding-cache writes | 192.922 | 232.496 |
| Ref/edge emission, relink comparisons, lazy blob decode and indexes | 2163.370 | 2079.154 |
| Metadata and transaction commit | 630.292 | 565.245 |
| Entire materialization, including connection-close overhead | **16646** | **8766** |

Nested join rows are subdivisions, not additional time to add to the entire selected join. Decode and symbol reconstruction are grouped where they share the same loop; these numbers do not pretend to distinguish their individual CPU costs. The largest actionable combined bucket was reading/decoding/binding all manifest payloads to reconstruct file indexes (~8.1 s), not the global `ProjectIndex::from_parts` setup (~60 ms including surface replay).

### Mechanism and work proof

Materialization version **4** persists a deterministic compact per-file resolver surface in `view_bindings`, alongside the existing generation-owned binding dependencies. It stores symbol/export/module/reexport lookup data but no source, AST or call sites. Unchanged entries restore their surface without opening their blob; changed entries or membership-invalidated bindings rebuild it. Existing configuration invalidation still forces a cold join. Consumer surface replay runs before decoding unchanged callers: only consumers actually selected for re-resolution decode/bind their call sites. No process-global cache, checkout read, or publication/orchestration change is involved. The existing `JoinResult` implementation is unchanged.

On the real pair only **260 current changed parse entries rebuild surfaces**, versus 4,921 cold. Only **556 caller blobs decode**, corresponding to the 260 changed files and 296 consumers whose results may change. Reference work remains 93,230; this optimization does not claim per-binding resolver deduplication. Removed files and non-parse entries explain the difference between 260 rebuilt surfaces and 276 changed manifest entries.

`persistent_surfaces_rebuild_only_changed_entries_without_reading_pruned_callers` reopens persisted bindings, counts actual immutable-blob reads, and checks cold parity. Disabling surface reuse with a restored `NON-VACUITY BREAK` makes that test alone fail (`rebuilt_surface_entries`: 2 rather than 1); the existing unrelated-export parity test remains green under the same mutation. The established graph/dependency row counts remain unchanged; its expected stats only gained the two new work counters. Debug/release materialization suites, callgraph-store suites, selected-join parity, and host plus Windows GNU library checks with `RUSTFLAGS='-D warnings'` pass.

### Same-input release offline measurements

| implementation | physical bytes | logical bytes | WAL bytes | wall seconds | CPU seconds |
| --- | ---: | ---: | ---: | ---: | ---: |
| Baseline cold, sampled run | 849338368 | 1333890296 | 422963352 | 31.613 | 28.604 |
| Baseline incremental, sampled run | 378933248 | 479782414 | 143635592 | 21.626 | 18.913 |
| Baseline cold, phase-timed run | 849338368 | 1335094520 | 422963352 | 30.184 | 27.142 |
| Baseline incremental, phase-timed run | 378933248 | 475145742 | 143635592 | 16.646 | 14.466 |
| Persistent surfaces cold | 880517120 | 1363766296 | 436979592 | 23.916 | 21.375 |
| Persistent surfaces incremental | 383717376 | 484611958 | 145806832 | **8.766** | **6.951** |

All three runs pass every-table parity against their own cold materialization. Shared-machine and page-cache variation affects timings (including cold, whose work count is unchanged), so the work counters are the mechanism evidence. The optimized offline derived phase is 1.234 s below the 10 s target; the physical-write target remains unmet and slightly regresses from 361.38 to 365.94 MiB.

### WAL attribution, not a speculative index removal

The benchmark maps **every WAL frame** through the final database's `dbstat` page ownership, preserving repeated page writes. This is final-owner attribution: pages reused mid-transaction can have had another owner. Missing `dbstat` fails the benchmark rather than silently printing an empty map. The largest baseline incremental object is **view_bindings: 46,090,440 bytes**, rising to 48,261,680 with persisted surfaces; `refs` is 18,366,960. The five refs secondary indexes together are 30,731,080 bytes: caller-file 6,044,040; caller-node/kind 8,610,800; kind/caller-file 6,888,640; short-name 5,479,600; target-file 3,708,000. Ref primary-key index: 6,600,240. Edge secondary indexes total 11,568,960. No individual secondary index dominates: removing a shared graph index is not justified by this profile. Binding payload churn and collective ref/edge row/index churn remain the physical-write problem; a separate normalized binding/surface persistence layout is a better next write-amplification experiment than dropping one shared query index.

Raw logs: `target/real-release-before.log`, `target/real-release-before-profile.log`, `target/real-release-surface.log`, and `target/surface-mutation.log`. The measured databases remain under `target/view-diff-real-300-input/`.

### Final release drill after persistent surfaces

Measured implementation: `f143185c`; observed `2026-09-13T01:12:24Z`. Command: `scripts/views-branch-drill.sh --mode both --binary "$PWD/target/release/aft" --storage "$PWD/target/branch-drill-surface"`, after a fresh release binary build. Views warm-up: 956,554 ms; legacy warm-up: 5,418 ms. The designated checkout was restored clean to `5716f8ba60e79ec60ec485b6e5291c0b0bc1f252`. Reports were copied to `target/branch-drill-surface.{json,md}` and historical investigation files restored.

| switch | on publication_ms | on puts | on embeds | on cpu_s | on rss_delta_mb | on correct_ms | off publication_ms | off puts | off embeds | off cpu_s | off rss_delta_mb | off correct_ms |
|---|---:|---:|---:|---:|---:|---:|---:|---:|---:|---:|---:|---:|
| HEAD→a085bf62a459 | 53980 | 0 | 3 | 137.86 | 754.453 | 53980 | — | — | 3 | 65.72 | 2686.703 | 59809 |
| a085bf62a459→HEAD | 48569 | 0 | 0 | 69.9 | -130.719 | 48875 | — | — | 4 | 46.63 | -543.312 | 44762 |
| HEAD→refs/remotes/upstream/v2-timeouts | 43846 | 0 | 1 | 70.0 | -443.75 | 44216 | — | — | 3 | 48.14 | 2.391 | 44020 |
| refs/remotes/upstream/v2-timeouts→HEAD | 46048 | 0 | 0 | 72.51 | -416.438 | 46047 | — | — | 4 | 44.67 | 41.078 | 38002 |

| switch | derived ms | over 10,000 ms target | final publication event total ms | views correct_ms minus legacy correct_ms |
| --- | ---: | ---: | ---: | ---: |
| HEAD → A | 23539 | 13539 | 26703 | -5829 |
| A → HEAD | 20245 | 10245 | 23696 | +4113 |
| HEAD → B | 18042 | 8042 | 21061 | +196 |
| B → HEAD | 19521 | 9521 | 22779 | +8045 |

**Shipping criterion remains unmet.** Only HEAD → A beats legacy to correctness; views CPU is higher on every row. The offline 8.766 s result must not be substituted for the drill's 18–24 s derived phase. Legacy times also increased substantially versus the retained before table, so cross-run wall-time changes are not a controlled speedup claim. Within the final run, there is another ~23–27 s between switch initiation and the final publication event's own measured duration, including the earlier pending publication and semantic readiness work; that is outside this materialization-only change. Final published-event puts are zero, but earlier pending forward events put 269 / 13 blobs, and embeddings remain 3 / 1 on forward legs. The generated script's static zero-embedding narrative is not evidence.

The next measured offline CPU/wall buckets are resolver/surface recording (~2.11 s) and ref/edge emission (~2.08 s). Resolution still runs once per reference. A cache keyed only by `(dependent, import binding)` would be unsound: namespace member accesses differ by `full_ref`/`short_name`, value refs apply callable-target checks, and Rust resolution consumes additional raw-reference context. A narrower JS/TS target lookup memo could preserve those inputs and replay both surface queries and dependency probes, but has not been implemented or claimed here. Independently, the dependency-basis set is reconstructed per reference in the existing selected loop; hoisting that immutable per-caller set is a lower-risk next experiment. These follow-ups and normalized binding storage need their own work guards and same-input measurements. No unmeasured second optimization was included to make this run appear to pass.

## In-situ publication attribution and release drill (2026-09-13)

The publication profile now reports the materializer's existing phase timers on the root-attributed `index_event kind=view_publication` line. It also distinguishes the `apply_manifest_diff` call from generation clone and publication-closure durability. The old `materialize_ms` field incorrectly repeated the whole `derived_ms` phase; it now measures only the materialization call. `derived_ms` remains the cancellation/health phase boundary and therefore includes clone, materialization, trigram creation and closure durability.

A fresh-storage, pre-attribution-fix both-arm drill (`target/branch-drill-prefixed.{json,md}` and `.stderr.log`) had one pending and one published event per switch. Every pending event had `derived_ms=0`; only the published event materialized. Thus there are two publication attempts, but **not two materializations** for one switch. The pending attempt assembled HEAD and filled missing immutable blobs while semantic data was unavailable; the semantic-ready attempt adopted those blobs and performed the sole derived build.

The final release drill reused that isolated storage so its warm-up measured the same already-built caches rather than repeating the 15-minute cold semantic build. Command: `scripts/views-branch-drill.sh --mode both --binary "$PWD/target/release/aft" --storage "$PWD/target/branch-drill-prefixed"`. Reports are retained as `target/branch-drill-final.{json,md}` and `.stderr.log`; the historical investigation reports were restored. The designated opencode checkout was restored clean at `5716f8ba60e79ec60ec485b6e5291c0b0bc1f252`.

### Offline and daemon materialization phases

All daemon rows below are the one published event for that switch. Times are milliseconds. Nested join rows are still subdivisions of `selected join`, not additional time. Small differences between the displayed phase sum and materialization call are connection setup/close, fingerprinting and integer truncation.

| operation | load/select | delete rows | owned decode/insert | selected join | binding writes | ref/edge emission | commit | materialization call | closure durability | derived phase |
| --- | ---: | ---: | ---: | ---: | ---: | ---: | ---: | ---: | ---: | ---: |
| Offline persistent surfaces | 616 | 1,043 | 565 | 3,600 | 232 | 2,079 | 565 | **8,766** | not measured | not measured |
| Daemon HEAD → A | 1,054 | 820 | 496 | 3,463 | 224 | 1,820 | 442 | **8,482** | 2,502 | 10,986 |
| Daemon A → HEAD | 1,220 | 754 | 628 | 3,858 | 193 | 2,229 | 498 | **9,453** | 3,791 | 13,245 |
| Daemon HEAD → B | 1,245 | 805 | 518 | 3,654 | 221 | 2,218 | 552 | **9,296** | 3,377 | 12,673 |
| Daemon B → HEAD | 1,111 | 715 | 576 | 3,987 | 201 | 2,349 | 560 | **9,577** | 3,622 | 13,200 |

| operation | join decode/index | join surface replay | join deferred caller decode | join resolve/record | join dependency union |
| --- | ---: | ---: | ---: | ---: | ---: |
| Offline persistent surfaces | 895 | 52 | 443 | 2,111 | 74 |
| Daemon HEAD → A | 994 | 58 | 415 | 1,911 | 59 |
| Daemon A → HEAD | 1,125 | 55 | 687 | 1,913 | 56 |
| Daemon HEAD → B | 1,053 | 54 | 499 | 1,965 | 60 |
| Daemon B → HEAD | 1,098 | 51 | 721 | 2,034 | 61 |

The materialization itself is 8.482–9.577 s in situ, bracketing the 8.766 s offline result. No phase exhibits the former 2–3× inflation, and clonefile is 0 ms in every row. The apparent 18–24 s discrepancy came from treating the publication's broader derived phase as the materialization timer, plus run-to-run contention: the fresh-storage profiling run measured 12.819–14.539 s derived, while the final run measured 10.986–13.245 s. In the final run, 2.502–3.791 s is required publication-closure checkpoint/fsync and is absent from the offline benchmark. These numbers do not support clone warming, lower-priority deferral, or publication cancellation as a materialization optimization.

### Publication count and final acceptance

| switch | publication events | pending derived ms | published materializations | views CPU s | legacy CPU s | views correct ms | legacy correct ms | views minus legacy correct ms |
| --- | ---: | ---: | ---: | ---: | ---: | ---: | ---: | ---: |
| HEAD → A | 2 | 0 | **1** | 33.03 | 40.17 | 32,792 | 34,211 | -1,419 |
| A → HEAD | 2 | 0 | **1** | 58.26 | 32.62 | 38,008 | 27,466 | +10,542 |
| HEAD → B | 2 | 0 | **1** | 54.40 | 35.85 | 35,291 | 29,947 | +5,344 |
| B → HEAD | 2 | 0 | **1** | 58.63 | 32.58 | 38,225 | 27,160 | +11,065 |

Every correctness probe passed and the drill reported no defects. **The shipping criterion remains unmet:** views must beat legacy to correctness on all four rows with CPU not above legacy. Views wins correctness and CPU only on HEAD → A. It is 5.344–11.065 s slower on the other three rows and uses 18.55–26.05 more CPU-seconds there.

The next wall bucket is outside the final publication event: correctness minus that event's own total is 19.551 / 22.117 / 20.414 / 22.418 s. That interval includes watcher application, semantic readiness, the pending assembly and other concurrent maintenance. Within the sole materialization, selected join (3.463–3.987 s) and ref/edge emission (1.820–2.349 s) remain the largest buckets. Optimizing either requires a separate guarded change; the present measurements do not justify changing publication orchestration or durability semantics.

## Plane-ready publication investigation (2026-09-13)

The retained baseline log is `~/.cache/aft-views-soak/0f3900af641f5248/branch-drill-views-on.stderr.log`; a copy was regenerated while testing the release binary. For the baseline forward HEAD → A switch its timestamped timeline was:

| UTC | elapsed from watcher | event / attribution |
| --- | ---: | --- |
| 03:22:05 | 0 s | watcher applied the 300-path batch and invalidated the resident indexes |
| 03:22:05–03:22:08 | 0–3 s | concurrent tier-2 refreshes ran; no legacy callgraph refresh completion was logged |
| 03:22:05–03:22:20 | 0–15 s | semantic collection waited/scheduled and then collected 2,690 chunks from 260 files (`sched=6302ms`, collection 73 ms) |
| 03:22:20–03:22:23 | 15–18 s | the semantic embedder completed 3 batches for 260 files |
| 03:22:22–03:22:25 | 17–20 s | the first view attempt assembled HEAD, found 260 semantic keys pending, and ended `outcome=pending`, `derived_ms=0`, `total_ms=3053` |
| 03:22:25–03:22:52 | 20–47 s | semantic readiness retriggered the view; the final publication spent 26,556 ms total, including 20,494 ms materialization and 3,834 ms closure durability |
| 03:22:53 | 48 s | the first `callers(activeInfo)` probe succeeded |

This confirms Fact A: the manifest was withheld solely because semantic keys were absent, even though its callgraph blobs were available. Publication now treats semantic misses as plane-local pending state: it publishes the callgraph-bearing manifest immediately, retains those paths for the semantic fill, and the fill advances the manifest fingerprint without rewriting callgraph rows. The guarded acceptance test holds an embedding request at the fake server and resolves the checkout's new callgraph symbol through the view while that request remains held.

Fact B was **false in the same baseline log**. The views-on switch contained two `index_event kind=view_publication` lines (one pending and one published), zero `refreshed callgraph store ... for N watcher path(s)` lines, and its perf tick reported `callgraph_invalidations=0`. `CallGraphRead` selected `reader_kind=view` for the current pinned generation, but the hypothesized duplicate legacy incremental refresh did not occur. No legacy-refresh suppression is shipped from this investigation.

### Release drill after the plane split

The required command was attempted with both throwaway seeded storage and the known warm storage:

`scripts/views-branch-drill.sh --mode both --binary "$PWD/target/release/aft" --storage <isolated-storage>`

Both attempts exercised all four views-on transitions, restored the designated checkout to `5716f8ba60e79ec60ec485b6e5291c0b0bc1f252`, and then hit the command timeout before producing the combined JSON/Markdown table. Consequently process CPU deltas and a valid same-run legacy comparison are unavailable; the partial wall observations below are diagnostic only, not an acceptance claim. The warm store had inherited mismatched historical manifests, forcing full-resolution materialization (17.0–23.8 s) rather than the 8.5–9.6 s incremental path measured above.

| switch | views correct ms (log timestamps) | views CPU s | prior legacy correct ms | prior legacy CPU s |
| --- | ---: | ---: | ---: | ---: |
| HEAD → A | ~49,000 | unavailable | 34,211 | 40.17 |
| A → HEAD | ~53,000 | unavailable | 27,466 | 32.62 |
| HEAD → B | ~43,000 | unavailable | 29,947 | 35.85 |
| B → HEAD | ~42,000 | unavailable | 27,160 | 32.58 |

The owner's rule remains: **views must beat legacy to correctness on every row, with views CPU not above legacy**. This incomplete drill does not establish that rule. Its wall observations miss legacy by approximately 14.8 / 25.5 / 13.1 / 14.8 s, and CPU cannot be adjudicated. The next benchmark bucket is obtaining a clean, compatible warm manifest set (or allowing its one-time rebuild to finish outside the timed run), then rerunning the unchanged both-arm drill so the intended incremental callgraph-plane publication—not full-resolution recovery—is measured.

## Policy costs landed: own 1 s publication window, deferred checkpoint (2026-09-13, f0750e15)

Both-arm drill on fresh storage per arm, release binary at `f0750e15`, `scripts/views-branch-drill.sh --mode both --binary target/release/aft --storage /tmp/aft-views-drill-77/on --baseline-storage /tmp/aft-views-drill-77/off --output-dir /tmp/aft-views-drill-77/out` (detached, ~1 h; views-on warm-up 533 s, views-off 492 s, both cold semantic builds). No defects, every correctness probe passed, zero blob puts and zero embeds on both return legs. Raw record: `/tmp/aft-views-drill-77/out/branch-drill.{json,md}` and the two per-arm stderr logs.

| switch | views publication ms | views CPU s | views correct ms | legacy CPU s | legacy correct ms | views − legacy correct |
| --- | ---: | ---: | ---: | ---: | ---: | ---: |
| HEAD → A | 27,876 | 95.3 | 32,436 | 80.4 | 34,254 | −1,818 |
| A → HEAD | 27,228 | 72.6 | 31,434 | 28.9 | 23,997 | +7,437 |
| HEAD → B | 25,222 | 54.6 | 29,252 | 31.0 | 25,932 | +3,320 |
| B → HEAD | 25,225 | 54.7 | 29,327 | 30.5 | 25,708 | +3,619 |

**Shipping criterion still unmet** (views beats legacy to correctness on all four rows with CPU not above legacy): views wins one row and loses three by 3.3–7.4 s; views CPU is above legacy on every row.

The published events' own attribution, callgraph-plane publication per switch (the semantic fill that follows each is 2.8–3.2 s total with a 26–31 ms materialization and a 2.5–3.0 s closure):

| switch | manifest | derived | materialization call | load/select | delete rows | selected join | ref/edge emission | commit | closure |
| --- | ---: | ---: | ---: | ---: | ---: | ---: | ---: | ---: | ---: |
| HEAD → A | 1,971 | 22,350 | 19,213 | 615 | 1,121 | 10,781 | 5,187 | 97 | 3,126 |
| A → HEAD | 2,163 | 22,136 | 19,577 | 753 | 917 | 10,249 | 5,850 | 121 | 2,548 |
| HEAD → B | 1,616 | 20,951 | 18,330 | 800 | 823 | 10,200 | 5,202 | 77 | 2,611 |
| B → HEAD | 1,612 | 20,953 | 18,475 | 877 | 1,335 | 9,834 | 5,089 | 85 | 2,469 |

Two findings this run adds, both unexplained and both the next work:

1. **The incremental materialization measures 18.3–19.6 s here against 8.5–9.6 s in the previous in-situ run and 8.8 s offline, on the same transition and the same binary lineage.** Selected join 10 s vs 3.5 s, ref/edge emission 5.2 s vs 2 s — a uniform ~2.5–3× on the two SQLite-heavy phases, and the cold seed materialization (18.5 s) costs the same as the incremental. The only product changes between the two runs are this delivery's (`journal_mode=WAL` + `synchronous=FULL` + `wal_autocheckpoint=0` on the materialization connection, the keeper connection held open through the build, the deferred checkpoint) and fresh storage per arm. Bisect on the same input before touching the join.
2. **The closure costs 2.5–3.1 s per publication with zero blob puts** (the semantic-fill publications, materialization 26 ms, closure 2.5–3.0 s), so it is a fixed cost: PASSIVE checkpoints and fsyncs of the two shared blob stores (the callgraph store is ~1.7 GB) plus alias/trigram fsyncs, paid twice per switch. It is no longer the derived checkpoint (that is deferred; `derived_clone_ms` is 4–11 ms). Proportional-to-change durability is the fix shape: a store with no puts since its last durable point needs neither checkpoint nor fsync.

Critical path after this run: ~1 s window + ~2 s assembly + 18–19 s materialization + ~2.6 s closure ≈ 25 s publication, correctness at ~29–32 s; legacy at 24–26 s. With the materialization back at ~9 s and the fixed closure removed, the views path is ~13 s against legacy's ~24 s on this switch.

## Proportional publication closure follow-up (2026-09-13)

### Controlled policy bisect

The retained fresh-storage HEAD/A manifests and callgraph store from `/tmp/aft-views-drill-77/on` were copied before use. The retained pair has the same `a632b160...` and `c3d6ca11...` fingerprints as the drill transition and 285 manifest-entry differences; this is larger than the drill's 276-entry source transition because the generated manifests include additional plane-state changes. `bench_real_manifest_diff` ran in release mode from a newly materialized base for each arm. Times are milliseconds except wall and CPU; WAL is the final byte count while the measurement keeper remains open.

| arm | wall s | CPU s | WAL bytes | load/select | delete | owned decode/insert | selected join | binding writes | ref/edge emission | commit |
| --- | ---: | ---: | ---: | ---: | ---: | ---: | ---: | ---: | ---: | ---: |
| policy as shipped | 18.184 | 16.151 | 148,736,152 | 570 | 958 | 533 | 10,543 | 223 | 5,083 | 99 |
| default `wal_autocheckpoint` | 16.762 | 14.567 | 148,736,152 | 532 | 989 | 507 | 9,287 | 174 | 4,651 | 459 |
| `synchronous=NORMAL` | 16.160 | 14.193 | 148,736,152 | 520 | 885 | 501 | 9,268 | 175 | 4,584 | 63 |
| no keeper during build | 16.658 | 14.622 | 0 after close | 531 | 868 | 464 | 9,320 | 182 | 4,705 | 101 |

The no-keeper zero does not mean zero generated frames: closing the last connection checkpointed and removed the WAL. The cold control remained 19.105-21.882 s in all four arms. No single policy arm restored the earlier 8.5-9.6 s result, so changing the materializer's durability policy would not close the measured gap.

The retained logs identify the actual difference. The fast run's A to HEAD publication selected an incremental closure (`full_resolution=false`, 296 dependent files, 569 resolved files, 95,222 resolved references, and 273 rebuilt surfaces). The fresh run over the same Git direction performed required full resolution (`full_resolution=true`, 4,654 dependent files, 4,934 resolved files, 300,219 resolved references, and 4,934 rebuilt surfaces). Its manifests change both `package.json` and `packages/session-ui/package.json`, which are resolution inputs and intentionally force full resolution. A later run on the old warm store also became full-resolution and took 21.788 s, so neither retained surfaces nor page cache explains the fast number. A copied persisted base likewise took 16.373 s with full resolution.

The old warm generation that seeded the fast A to HEAD row was swept, so its exact stale manifest entry cannot be recovered. The surviving work counters are sufficient to bound the finding: the 8-9 s and 18-19 s rows did not execute the same materialization work even though they followed the same Git checkout transition. The former is not a valid policy baseline, and no materialization-policy change is made here. Configuration-input invalidation remains correctness-required.

### Dirty-since-durable blob stores

Dirty state is tracked per shared blob-database path for the life of the process. First sight is conservatively dirty, every inserted payload marks the store dirty before releasing the put/publication barrier, and a successful checkpoint plus fsync clears it. Publication and puts share a barrier through closure probing, so a manifest cannot observe an in-flight put after deciding that its store is clean. A clean store's durable contents are already its current contents, leaving the pointer CAS argument unchanged.

The mechanism red was `publication_cas_test::clean_blob_stores_skip_checkpoint_and_fsync_until_a_put_marks_one_dirty`: before the fix its clean publication observed `(3 checkpoints, 1 fsync event)` instead of `(0, 0)`. The observer now reports each blob-store checkpoint and fsync individually. The clean publication observes exactly zero of each, while one inserted semantic payload causes exactly one checkpoint and one fsync; an idempotent reused put leaves the already-durable store clean. Alias checkpoint/fsync, trigram fsync, and derived main/WAL fsync remain in the closure and are attributed separately.

### Final fresh-storage branch drill

The final binary was built after the inserted-versus-reused refinement and run detached without shortening the drill:

`scripts/views-branch-drill.sh --mode both --binary target/release/aft --storage /tmp/aft-views-drill-77-proportional-final/on --baseline-storage /tmp/aft-views-drill-77-proportional-final/off --output-dir /tmp/aft-views-drill-77-proportional-final/out`

Views-on warm-up was 603,619 ms and legacy warm-up was 690,649 ms. The report contains no defects, and every row reached the correct callgraph result.

| switch | views publication ms | views CPU s | views correct ms | legacy CPU s | legacy correct ms | views - legacy correct |
| --- | ---: | ---: | ---: | ---: | ---: | ---: |
| HEAD to A | 34,017 | 99.12 | 34,016 | 84.07 | 34,942 | -926 |
| A to HEAD | 33,748 | 79.57 | 34,061 | 32.24 | 26,674 | +7,387 |
| HEAD to B | 30,795 | 53.48 | 31,083 | 32.20 | 26,838 | +4,245 |
| B to HEAD | 28,301 | 53.49 | 28,606 | 32.57 | 26,710 | +1,896 |

The owner's rule remains: **views must beat legacy to correctness on every row, with views CPU not above legacy**. It is not met. Views wins correctness only on HEAD to A and exceeds legacy CPU by 15.05 / 47.33 / 21.28 / 20.92 seconds. The required full-resolution materializations remain the largest bucket at 17,816-21,846 ms per callgraph-plane publication.

The four semantic-fill publications perform 30-54 ms of materialization and retain closure times of 2,922 / 2,673 / 2,509 / 2,503 ms. The dirty-store red proves that clean blob stores issue no checkpoint or fsync calls, so this residual is not evidence that the skip failed. The final manifest requires 8,891 blob-membership probes, and `SqliteClosure::contains_blob` currently opens a SQLite connection for every key. On copied final artifacts, reproducing that pattern took 6,789 ms; retaining two read connections reduced it to 1,755 ms. For comparison, fsyncing copied derived WAL/main, trigram, and alias artifacts measured 0.190 / 0.023 / 0.478 ms after they were already durable, while the drill's detached derived checkpoints completed in 8-25 ms. Those copied-file fsync numbers are lower bounds, but they rule out multi-second file size alone.

The next closure bucket is therefore the manifest-wide per-key connection/query pattern, followed by separately timed derived, trigram, and alias syncs. The next overall acceptance bucket remains correctness-required full resolution when package resolution inputs change; optimizing it belongs in resolver-selection work, not this policy/closure change.

## Closure connection and batching follow-up (2026-09-13)

This follow-up changes the closure probe only. Resolution-input invalidation is
unchanged: configuration transitions still require full resolution. There is no
new six-case fact-invalidation parity matrix, fact-invalidation work red, or
`full_resolution=false` real-pair claim in this follow-up.

### Resolver reads that still need fact attribution

The manifest path uses `module_dependencies` at
`callgraph_store/mod.rs:14095-14118`, not a second resolution algorithm. The
following locations describe the reads in this revision (paths relative to
`crates/aft/src`):

| Read | Existing location | Important constraint for recording |
| --- | --- | --- |
| TS alias targets and base directory | `callgraph.rs:2395-2431` | Reads `compilerOptions.paths` and `baseUrl` from the nearest tsconfig; does not follow `extends`. |
| Ancestor package identity | `callgraph.rs:2481-2498`, `2805-2814` | Reads `name`; a scan for one name must not subscribe to every other package's entry points. |
| Workspace-root discovery | `callgraph.rs:2500-2524`, `2643-2666` | Reads package workspaces (array or `packages` object) and pnpm package patterns. |
| Name-to-package-directory lookup | `callgraph.rs:2563-2608`, `2610-2641` | Per-pass and process-wide memo returns bypass member traversal, so recording must also cover those returns. |
| pnpm workspace patterns | `callgraph.rs:2668-2701` | Uses a line-oriented parser for `packages:` entries, not a general YAML parser. |
| Package entry point | `callgraph.rs:2816-2844`, `2846-2930` | Reads `exports`, then `module`/`main` for root imports; `types` is not consumed here. |
| Rust manifest crate names | `callgraph_store/mod.rs:10444-10485` | Uses line-oriented `name` extraction and `[lib]` state, not TOML semantic parsing; replacing it with a TOML projection would change which facts are represented. |
| Rust crate map | `callgraph_store/mod.rs:10402-10442` | Walks project directories excluding target/node_modules/.git; does not restrict this map to workspace members. |
| Rust source candidates | `callgraph_store/mod.rs:14120-14154`, `callgraph.rs:210-234`, `join.rs:666-711` | Candidate misses and declared module paths remain ordinary file dependencies. |

The older disk-oriented Rust resolver also reads package name, lib name/path,
and workspace members at `callgraph.rs:2196-2223` and `2346-2365`. These are not
interchangeable with the manifest crate-name parser. Neither resolver's decisions
were changed here. The `ProjectFacts::config_bytes` seam returns opaque bytes;
field consultations and memo-hit replay still need recording at the actual
reads before the conservative full-resolution bound can be removed safely.

### Closure mechanism and offline comparison

`SqliteClosure` now lazily retains a `TrackedConnection` per blob plane, scoped
to the closure object. Its batch override sorts and deduplicates keys per plane
and queries at most 500 placeholders per statement. Missing keys still fail the
closure, with the first missing key reported in manifest order. Other
`PublicationClosure` implementations retain their single-key behavior through a
default method. The normal `contains_blob` path shares the same retained handles.

`bench_closure_probe_strategies` reads the actual copied manifest and checks
membership of every one of its 8,891 plane keys, rather than substituting a
store-wide row count. The fixture is the final generation from
`/tmp/aft-views-drill-77-proportional-final/on`; the semantic database was copied
with SQLite backup and the checkpointed callgraph database with an APFS clone.
The final debug benchmark (after the drill and release build completed) reported:

| strategy | elapsed ms |
| --- | ---: |
| open and query once per key | 8,577 |
| two retained handles, cached point statements | 2,135 |
| retained handles, sorted batches (production probe) | 1,931 |

The batch strategy was retained. These are offline debug timings, not a release
or in-situ speedup claim. Run with absolute paths in `AFT_CLOSURE_MANIFEST`,
`AFT_CLOSURE_SEMANTIC`, `AFT_CLOSURE_CALLGRAPH`, and `AFT_CLOSURE_TRIGRAM`:

`cargo test -p agent-file-tools --lib bench_closure_probe_strategies -- --ignored --nocapture`

### Mutation evidence

All mutations were staged from the live implementation before mutation, produced
a nonempty unstaged diff, and were restored to an empty unstaged diff before
subsequent work. No mutation was committed.

The initial retained-point-query implementation, with connection retention
neutralized, produced:

```text
test closure_probe_opens_at_most_one_connection_per_plane ... FAILED
assertion `left == right` failed
  left: 64
 right: 2
test closure_probe_rejects_absent_key_after_successful_probes ... ok
test malformed_key_does_not_open_a_plane ... ok
```

The final batch fixture uses 600 distinct keys per plane, crossing the batch
boundary. Clearing the retained connection map at each batch produced:

```text
test closure_probe_opens_at_most_one_connection_per_plane ... FAILED
assertion `left == right` failed
  left: 4
 right: 2
test closure_probe_rejects_absent_key_after_successful_probes ... ok
test malformed_key_does_not_open_a_plane ... ok
```

Neutralizing the final batch membership rejection produced:

```text
test closure_probe_rejects_absent_key_after_successful_probes ... FAILED
assertion failed: matches!(probe_publication_closure(&missing,
  &ClosureRequirements::default(), &closure),
  Err(ViewError::MissingBlob { plane: ArtifactPlane::Semantic,
  key: actual }) if actual == key)
test closure_probe_opens_at_most_one_connection_per_plane ... ok
test malformed_key_does_not_open_a_plane ... ok
```

These tests live under `views::assembly::closure_connection_tests`. Cumulative
opens and live connections are measured at the `TrackedConnection` lifecycle
seam using a test-only per-thread mirror; they are not inferred from timing.

### Retained-point-query drill, not final-batch acceptance

The unshortened both-arm fresh-storage drill ran against commit `64ef79d4`
(the retained-point-query implementation, **before batching**):

`scripts/views-branch-drill.sh --mode both --binary target/release/aft --storage /tmp/aft-views-drill-closure-291/on --baseline-storage /tmp/aft-views-drill-closure-291/off --output-dir /tmp/aft-views-drill-closure-291/out`

It ran through nohup as a background task. Both arms completed, with no reported
correctness defects. Views warm-up was 896,925 ms; legacy warm-up was 613,930 ms.
Local builds and offline probing overlapped this run, so the following raw
measurements are **not a controlled final performance acceptance run**.

| switch | views publication ms | views CPU s | views correct ms | legacy CPU s | legacy correct ms | views minus legacy correct ms |
| --- | ---: | ---: | ---: | ---: | ---: | ---: |
| HEAD to A | 57,565 | 114.61 | 57,565 | 85.71 | 36,110 | +21,455 |
| A to HEAD | 45,532 | 76.90 | 46,073 | 44.09 | 41,444 | +4,629 |
| HEAD to B | 44,954 | 79.59 | 45,346 | 34.45 | 29,615 | +15,731 |
| B to HEAD | 32,854 | 35.88 | 32,854 | 35.70 | 31,498 | +1,356 |

Semantic-fill closure_ms was 3,123 / 4,420 / 2,977 / 2,471, versus the earlier
per-key-open drill's 2,922 / 2,673 / 2,509 / 2,503. No in-situ closure speedup can
be claimed from these differently loaded runs. A fresh drill of the final
batched binary is still required.

The owner's rule remains: **views beats legacy to correctness on every row,
with CPU not above legacy**. The raw retained-point-query run misses correctness
by the amounts above and CPU by 28.90 / 32.81 / 45.14 / 0.18 seconds. The next
major bucket remains fact-sensitive resolution-input invalidation; materializer
resolution policy is deliberately unchanged by this closure optimization.

The final batch code passes the views and durable publication/restart tests,
`tool_call_parity_test`, and host plus x86_64-pc-windows-gnu `cargo check` with
`RUSTFLAGS='-D warnings'`. The callgraph_store suite (including join) passed on
the retained-handle unit; no resolver code changed in the batch unit. Both
retained-handle and final batch release binaries were built successfully.

## Consulted resolution facts (2026-09-13)

Materialization version **5** replaces the config-content full-resolution rule
with per-field projections and recorded consultations. Package inputs project
`name`, normalized workspace patterns, `exports`, `module`, and `main` (not
`types`); tsconfig projects `compilerOptions.paths` and `baseUrl` and does not
follow `extends`. The pnpm projection calls the resolver's extracted line parser.
Rust's manifest reader and projection share the first-non-lib/last-lib name
extractor. Its facts are `manifest.name` and `manifest.lib.name`; the disk TOML
reader's package/lib names, lib path and workspace members have separate `disk.*`
identities. Workspace membership does not constrain the manifest Rust crate map.

Bindings persist consulted `(input path, field)` identities. Changed input blobs
are decoded and projected once per side of the transition, never once per ref.
Changed fact consumers rebind, then reference-binding equality and the existing
consumer-specific resolver-index queries prune unchanged reference resolution.
Config add/remove also rechecks a compact membership domain, so previously unseen
workspace packages and nearer tsconfigs cannot escape invalidation. Ordinary
source-file candidates, missing paths and Rust module-index dependencies remain.
Raw `ProjectFacts::config_bytes` calls outside the instrumented readers mark the
caller unattributed, including a raw read after a known read of the same input.
Unknown projections or an unattributed caller on an input transition use the full
fallback and log `views materialization: full resolution
(reason=unattributed_reads count=N)`. The returned stats count unattributed
callers. Old dependency versions take the cold path once.

View workspace memo answers now belong to the immutable join. The old shared
absolute `/` workspace cache could otherwise return manifest A's answer during a
cold join of manifest B. Legacy disk globals and resolution decisions are
unchanged. Both per-pass and workspace-cache returns replay consultation traces;
legacy default hooks check no recording state and allocate no relative path or
field strings. The callgraph suites and tool-call envelope parity pass.

### Every-table matrix and work counts

Each case compares every SQLite table with a newly materialized cold next view,
including binding payloads, dependencies and metadata. The JS fixtures include
both a second importer that hits the workspace memo and an independent named
package importer (not only relative/local calls).

| transition | unchanged callers resolved | total callers resolved | refs resolved | surfaces rebuilt | caller blobs decoded | full resolution | unattributed |
| --- | ---: | ---: | ---: | ---: | ---: | --- | ---: |
| package version only | 0 | 0 | 0 | 0 | 0 | false | 0 |
| workspace member admitted by patterns | 2 | 2 | 4 | 3 | 3 | false | 0 |
| one package exports changed | 2 | 2 | 4 | 2 | 2 | false | 0 |
| tsconfig alias targets changed | 2 | 2 | 4 | 3 | 3 | false | 0 |
| Cargo workspace member and crate added | 1 | 2 | 1 | 2 | 2 | false | 0 |
| Rust mod file moved to directory form | 1 | 2 | 2 | 2 | 2 | false | 0 |

Broad workspace/alias facts can require rebinding an independent named importer,
but its unchanged binding and surface answers avoid reference resolution. The
Cargo member fixture adds the actual crate manifest/source as well as the root
member entry; changing only `[workspace].members` does not affect this manifest
resolver. A separate member-name transition verifies that the memo-hit caller
loses the old edge when its package name disappears. A subprocess cold-B oracle
checks the shared-root memo-scope correction without inheriting any A cache.

### Restored mutation controls

All controls were staged from the live implementation, had a nonempty unstaged
`git diff --stat` during mutation, and restored to an empty unstaged diff before
continuing. No `NON-VACUITY BREAK` is retained in production.

- Whole-file hashing: `version_only_package_edit_resolves_no_unchanged_callers`
  **FAILED**, rebuilt surfaces `left: 3, right: 0`. Reference-resolution pruning
  still avoided refs, so the test also asserts zero caller decode/binding work.
  The earlier pure projection control
  `package_version_and_types_are_not_resolution_facts` also failed when `name`
  was replaced with the whole-file digest.
- Dropped exports hook: `exports_change_resolves_only_package_importers`
  **FAILED**, `table edges: missing=2 extra=2` (one.ts instead of two.ts).
  `version_only_package_edit_resolves_no_unchanged_callers` remained green.
- Dropped package memo-return hooks:
  `workspace_member_name_change_invalidates_memo_hit_callers` **FAILED**,
  `table edges: missing=0 extra=1`, the extra edge owned by **b.ts**.
  The version-only control remained green. The direct recording test
  `memo_hit_callers_retain_member_name_consultations` also reddened for b.ts,
  with `disk_facts_disable_recording_hooks` still green.
- Removed view memo scoping:
  `cold_workspace_after_prior_manifest_matches_fresh_process` **FAILED**,
  `table edges: missing=2 extra=2` (old packages/pkg instead of packages/new).
  `disk_facts_disable_recording_hooks` stayed green.
- Removed compact config-probe domain:
  `workspace_discovery_uses_one_config_membership_dependency` **FAILED**,
  `a.ts stored directory-wide config probes as individual dependency rows`.
  `opaque_config_read_is_unattributed_even_after_a_known_field_read` stayed green.

### Real offline 300-Git-path pair

Offline source: an APFS clone of the retained, closed
`/tmp/aft-views-drill-77-proportional-final/on` artifacts into this worktree.
Generations **3 → 4**, A → HEAD, contain **7,045 → 7,060 entries and 285 changed
entries**, including the same two package.json inputs. Their manifest identity
suffixes are `a632b1603f62b77a16c041435b34764810ec2919461ae14fba9c2a70b49fdace`
and `c3d6ca11eca18e25d2e58e52629921215cdea1d4f23ce9278941c1a13faa95d1`.
This is the retained 300-Git-path transition with additional plane-state entries,
not the older 17-entry pair and not a synthetic 300-file fixture.

Command (release, private databases):

```sh
AFT_VIEW_DIFF_INPUT="$PWD/target/view-diff-real-300-input" AFT_VIEW_PROFILE=1 \
  cargo test --release -p agent-file-tools --lib \
  views::materialization::tests::bench_real_manifest_diff -- --ignored --exact --nocapture
```

| operation | wall s | CPU s | physical bytes | logical bytes | WAL bytes | resolved files | resolved refs | full resolution | unattributed |
| --- | ---: | ---: | ---: | ---: | ---: | ---: | ---: | --- | ---: |
| cold replacement | 20.428 | 18.255 | 460918784 | 940012652 | 455618472 | 4934 | 300219 | true | 0 |
| fact-sensitive incremental | **7.490** | **5.871** | 153341952 | 323547718 | 151199912 | **576** | **95228** | **false** | **0** |

Every-table parity passed. Incremental selected **296 unchanged dependents**,
rebuilt 299 surfaces and decoded 585 caller blobs. The 576/95,228 counts differ
from the historical 569/95,222 warm row because this retained pair has additional
manifest entries; the earlier count must not be substituted for this measurement.
The 7.490 s derived call is 10.84–12.09 s below the documented 18.33–19.58 s
full-resolution drill rows; those are historical, differently loaded in-situ
measurements, not a same-run policy comparison. The same-input cold control above
is the direct work/timing comparison.

| incremental phase | ms |
| --- | ---: |
| load bindings and select | 756 |
| delete rows | 919 |
| changed-owner decode/insert | 509 |
| selected join | 3100 |
| binding writes | 163 |
| ref/edge emission | 1812 |
| commit | 84 |

Within selected join: bind/index 978 ms, surface replay 48 ms, deferred caller
decode 361 ms, resolve/record 1627 ms, dependency union 60 ms. These are nested
wall boundaries, not CPU attributions. The next within-materializer bucket is
selected join (~3.1 s) and emission (~1.8 s); neither was optimized here.

The first recording implementation expanded config-presence probes into ordinary
rows: 2,670,644 config-path dependencies, 15.980 s incremental and 49.011 s cold.
The compact membership-domain guard above eliminated that avoidable storage
regression before acceptance. Raw logs remain in `target/fact-real-release.log`
and `target/fact-real-release-compact.log`; the final measured databases are
`target/view-diff-real-300-input/.tmpjQEAfp/{base,cold,incremental}.sqlite`.

Verification before the drill: views 46 passed / 3 ignored; callgraph-filtered
library suites (including store/join) 230 passed / 3 ignored; callgraph_store_test
37 passed / 1 ignored; tool_call_parity_test 12 passed; durable_restart 2 passed /
1 child ignored; publication_cas 3 passed. Host and x86_64-pc-windows-gnu library
checks with `RUSTFLAGS='-D warnings'` passed. The sidekick service was unavailable
(provider credential outage); the owner authorized direct inspection and comment
self-review instead. The earlier AFT inspect request timed out; cargo checks are
the authoritative diagnostics gate.

## Fresh-storage drill, cold work gated (2026-09-13)

The release binary was built from the `b7dd9b4f` product tree; the intervening harness commit changes only the fenced Python drill. The both-arm command used fresh `/tmp/aft-views-drill-cold-gated-297/{on,off}` storage and kept both standalone processes alive. Before launch, `uptime` reported a one-minute load average of **5.81**. The views-on cold-work gate cleared in **1,338,120 ms** and the legacy gate in **1,382,693 ms**. Neither process switched commits until both gates had completed the semantic seed and one full dead-code pass.

The previous misleading `semantic_index.status=ready` value reported queryability of an installed resident/view index, not semantic-worker idleness. When a `SemanticIndex` is already installed, the status producer labels that index from `idx.status_label()` and does not attach `semantic_build_progress`; those progress fields are attached only in the no-index cold-building branch. Because the views-on subject was a fresh isolated standalone process, it could not have borrowed a legacy index from another process. The cold gate consequently checks the progress fields and root-owned semantic build lifecycle rather than treating `ready` as proof that the post-configure seed stopped.

| switch | views correct ms | legacy correct ms | views CPU s | legacy CPU s | views contention | legacy contention | owner rule |
| --- | ---: | ---: | ---: | ---: | --- | --- | --- |
| HEAD → A | 43,109 | 55,581 | 81.10 | 101.59 | `build_progress=0; tier2_scheduled=1` | `build_progress=0; tier2_scheduled=1` | met |
| A → HEAD | 34,180 | 48,088 | 50.24 | 58.21 | `build_progress=0; tier2_phases=dead_code,unused_exports,duplicates,cycles,complexity` | `build_progress=0; tier2_phases=dead_code,unused_exports,duplicates,cycles,complexity` | met |
| HEAD → B | 28,253 | 26,469 | 42.18 | 30.36 | none | none | not met |
| B → HEAD | 31,708 | 25,057 | 33.86 | 29.91 | none | none | not met |

The contention interval is exactly switch initiation through the first correct search-and-callgraph observation. The completed pre-switch cold pass made each process idle at the gate; the first checkout then scheduled a new Tier-2 refresh in both arms, and its phases crossed the first two rows. No semantic or other index `build_progress` line crossed any measured window.

The owner's rule is: **"views beats legacy to correctness on every row with CPU not above legacy"**.

- **HEAD → A — met:** views was correct **12,472 ms sooner** and used **20.49 fewer CPU-seconds**.
- **A → HEAD — met:** views was correct **13,908 ms sooner** and used **7.97 fewer CPU-seconds**.
- **HEAD → B — not met:** views was correct **1,784 ms later** and used **11.82 more CPU-seconds**. With no reported contention, the named views bucket is publication work: the callgraph-plane publication spent **15,963 ms** in materialization, followed by a **2,636 ms** semantic-fill publication.
- **B → HEAD — not met:** views was correct **6,651 ms later** and used **3.95 more CPU-seconds**. With no reported contention, the named views bucket is again publication work: **19,880 ms** in materialization, followed by a **2,591 ms** semantic-fill publication.

The rule is therefore **not met overall**. All eight correctness probes converged, process PIDs remained stable, and the harness reported no correctness or publication defects. Raw JSON, Markdown, and both stderr logs remain under `/tmp/aft-views-drill-cold-gated-297/out`.

## Per-binding resolution (2026-09-13)

### Same-work baseline and reconstruction

No retained `target/view-diff-real-300-input` survived the target-directory sweep.
The input was regenerated with the unmodified `57830e306` product tree (includes
`c0c8e8022`), not a synthetic replacement. The owner cancelled the separately
armed idle baseline; this worktree exclusively launched the reconstruction after
an idle gate observed load **2.985**, empty `docker ps`, and no `branch_drill.py`.
The release executable was built before the run. Both arms used fresh storage:

```sh
cargo build --release -p agent-file-tools --bin aft
# First require: uptime 1-minute load < 3; docker ps empty;
# pgrep -f branch_drill.py finds no other drill.
git -C ~/Work/OSS/opencode rev-parse HEAD
nohup scripts/views-branch-drill.sh --mode both \
  --binary "$PWD/target/release/aft" \
  --storage "$PWD/target/branch-drill-baseline/on" \
  --baseline-storage "$PWD/target/branch-drill-baseline/off" \
  --output-dir "$PWD/target/branch-drill-baseline/out" \
  > target/branch-drill-baseline.log 2>&1 &
# Wait for completion before copying databases or touching the soak checkout.
git -C ~/Work/OSS/opencode rev-parse HEAD
mkdir -p target/view-diff-real-300-input target/legacy-next-project
cp target/branch-drill-baseline/on/views/0f3900af641f5248/manifest-1-*-c3d6ca11eca18e25d2e58e52629921215cdea1d4f23ce9278941c1a13faa95d1.json \
  target/view-diff-real-300-input/base.json
cp target/branch-drill-baseline/on/views/0f3900af641f5248/manifest-2-*-a632b1603f62b77a16c041435b34764810ec2919461ae14fba9c2a70b49fdace.json \
  target/view-diff-real-300-input/next.json
# Quiescent blob database, with no outstanding callgraph WAL after process exit.
cp target/branch-drill-baseline/on/blobs/aa69d52ef2dcad4d/callgraph.sqlite \
  target/view-diff-real-300-input/callgraph.sqlite
git -C ~/Work/OSS/opencode archive a085bf62a459 | tar -x -C target/legacy-next-project
git -C ~/Work/OSS/opencode diff --name-only \
  5716f8ba60e79ec60ec485b6e5291c0b0bc1f252 a085bf62a459 \
  > target/view-diff-real-300-input/changed-paths.txt
```

Original and restored opencode HEAD were both
`5716f8ba60e79ec60ec485b6e5291c0b0bc1f252`. The graph fingerprints match the
historical pair above. Full manifests have **7,060 → 7,045 entries, 285 changed
entries**, across **300 Git paths**. Do not silently substitute the historical
276-change count: semantic-plane membership varies during the publications.
This retained pair resolves **93,236 rows / 563 files**, not the previous
95,228 / 576 row. The before and after below use these exact same JSON files and
immutable blobs. The legacy input is an archive of the *same target Git commit*,
against a copy of the completed views-off arm's HEAD store.

The existing benchmark is `crates/aft/tests/callgraph_refresh_bench.rs`, not a
`benches/` target. It now accepts a newline-delimited path list, including removed
files, and excludes paths without a parser (283 of 300 paths reach refresh).
In transition mode it **does not warm up or force hashes stale**. It re-roots
only the copied backend identity before opening; otherwise root repair
cold-builds the next revision before timing, falsely reporting a 0.303-second
"refresh". That contaminated exploratory run was discarded. The regression
`transition_copy_does_not_refresh_next_revision_during_open` verifies the old
content hash survives open and only the timed refresh changes it. Neutralizing
the re-root UPDATE with `WHERE 0 /* NON-VACUITY BREAK */` fails that test alone
(3 other tests pass); the scratch edit was restored before commit.

```sh
AFT_VIEW_DIFF_INPUT="$PWD/target/view-diff-real-300-input" AFT_VIEW_PROFILE=1 \
  cargo test --release -p agent-file-tools --lib \
  views::materialization::tests::bench_real_manifest_diff \
  -- --ignored --exact --nocapture
AFT_CALLGRAPH_REFRESH_STORE="$PWD/target/branch-drill-baseline/off/callgraph/aa69d52ef2dcad4d" \
AFT_CALLGRAPH_REFRESH_ROOT="$PWD/target/legacy-next-project" \
AFT_CALLGRAPH_REFRESH_PATHS="$PWD/target/view-diff-real-300-input/changed-paths.txt" \
  cargo test --release -p agent-file-tools --test callgraph_refresh_bench \
  bench_refresh_files_on_store_copy -- --ignored --exact --nocapture
# Repeat only the legacy command with AFT_CALLGRAPH_REFRESH_COUNT_ROWS=1
# for the separate private-copy audit pass. Never use its timing/WAL as baseline.
```

Both harnesses link the same release product implementation. Copying/preparation
is excluded from timing. Legacy audit triggers count actual INSERT/UPDATE/DELETE
operations separately from the uninstrumented timing pass; views uses its
existing row-operation counters. "Reference rows" does not count extra resolver
probes used by legacy's stored-extract equality checks.

| same transition, offline | wall s | CPU s | owned extraction/decode | unchanged callers resolved | reference rows | graph row operations | all counted row operations | WAL bytes |
| --- | ---: | ---: | --- | ---: | ---: | ---: | ---: | ---: |
| legacy incremental refresh | 44.928 | 40.484 | 269 changed extracts (245 rewritten, 24 graph-equal) | 527 (audit) | 123,570 inserted (audit) | 320,972 (audit) | 386,402 (audit) | 240,146,592 |
| views before | 7.403 | 5.768 | 286 surfaces rebuilt; 572 caller blobs decoded | 296 | 93,236 resolved | 110,834 | 148,196 | 148,546,632 |
| views after | **5.739** | **4.268** | **unchanged** | **296** | **93,236** | **110,834** | **148,196** | **148,546,632** |

Legacy selected 110,896 dependent ref IDs in the timing pass and 110,904 in the
separate audit pass (an eight-ID variation); audit inserts cover 766 callers.
Its phase table was parse 5,963 ms, dependency selection 21,050, dependent parse
11,261, index load 1,534, ref resolution 2,661, own-row deletes/inserts 733/335,
method dispatch 167, commit 684. This is not views doing the same work slower:
legacy re-extracts more importers, emits more refs, and spends substantially more
on dependent selection. The 44.928-second offline result is **not** a substitute
for the end-to-end drill's legacy correctness latency.

Raw logs: `target/per-binding-before-{views,legacy}.log`,
`target/per-binding-legacy-audit.log`, `target/per-binding-after-views.log`.
Baseline/optimized every-table databases:
`target/view-diff-real-300-input/{.tmpNGhmzS,.tmpBsO6La}/incremental.sqlite`.

### Binding and emission result

The target memo key contains caller path, reference kind, full and short names,
and the Rust visible-import prefix. It is not merely an import module path:
namespace members, local calls, unresolved names and value references have
separate resolution semantics. On the real incremental pair there are
**39,456 distinct target bindings for 93,236 rows** (2.36x, not an order of
magnitude). Cold resolution uses 140,510 bindings for 298,227 rows. A call site's
own dependency candidates are always retained; surface/config consultations are
unioned once into the same caller's persisted binding record. The dependency
basis is now built once per caller instead of constructing the same set on every
reference. The legacy resolver itself is unchanged.

Emission already used one immediate transaction and retained the secondary
indexes; it nevertheless prepared SELECT/DELETE/INSERT SQL for each reference.
Five statements now live across the fan-out, with no schema/index or transaction
change. The emission phase includes unchanged-ref checks and lazy caller/target
decode, not only inserted rows. Its throughput is **51,128 → 66,683 examined
reference rows/s**. Actual ref/edge insertion operations are 50,535 (54,172 total
inserts less 267 owned files and 3,370 owned nodes), giving **27,712 → 36,143
ref/edge insert operations/s** over that whole phase. No index-write reduction is
claimed; WAL bytes and every-table rows are unchanged.

| incremental phase | historical 7.490 s row, ms | same-pair before, ms | after, ms |
| --- | ---: | ---: | ---: |
| load bindings/select | 756 | 654 | 701 |
| delete rows | 919 | 1,047 | 965 |
| owner decode/insert | 509 | 481 | 457 |
| selected join | 3,100 | 3,018 | 1,845 |
| ↳ bind/index | 978 | 942 | 936 |
| ↳ surface replay | 48 | 48 | 47 |
| ↳ deferred caller decode | 361 | 366 | 384 |
| ↳ resolve/record (includes dependency basis) | 1,627 | 1,589 | **400** |
| ↳ dependency union | 60 | 53 | 55 |
| binding writes | 163 | 140 | 142 |
| ref/edge emission | 1,812 | 1,823 | **1,398** |
| commit | 84 | 92 | 93 |

Same-pair wall improves **22.5%**, CPU **26.0%**; cold replacement improves
20.862/18.361 wall/CPU seconds to 17.150/15.029. This measures an improvement,
not an irreducible floor: selection/load, owner work and emission still dominate.

Every value in every derived table was compared between the pre-change and
optimized real incremental databases, including metadata, dependency caches and
empty side tables. All match. Seven fact-transition snapshots (the requested six
plus workspace-member rename) are byte-equal before/after, using temporary test
instrumentation to save the existing sorted snapshot representation; no snapshot
knob was added to the product or tests. Files are under
`target/per-binding-fact-{before,after}`. The final materialization suite passes
32 tests / 2 ignored; the callgraph-store-filtered suite passes 135 / 3 ignored;
release library check and release binary build pass. AFT inspect timed out twice;
Cargo is the authoritative gate. Sidekick's cold reader was unavailable; comment
lint reported no unclear comments.

Scratch caller-key red:

```text
// NON-VACUITY BREAK: replace caller: raw.caller_file.clone() with String::new()
test views::materialization::tests::memoized_rust_bindings_keep_import_visibility ... ok
test views::materialization::tests::memoized_bindings_keep_callers_and_reference_kinds_distinct ... FAILED
assertion `left == right` failed (canonical reference rows differ)
test result: FAILED. 1 passed; 1 failed
```

The staged live tree had an empty unstaged diff, the mutation showed one
insertion/one deletion in `join.rs`, and restore returned the unstaged diff to
empty. The restored full materialization suite is green. No mutation remains.

### Before drill (unmodified product)

Reconstruction completed at `2026-09-13T19:30:52Z`; cold gates were 1,181,480 ms
(views) and 1,215,250 ms (legacy). The launch idle gate did not guarantee idle row
windows: reported host load ranged 6.41–7.82. All eight search/callgraph probes
converged and the shared checkout was restored. However, the harness reported
**return-leg manifest mismatches on both HEAD returns** and three embeddings on
the first return, already on unmodified main. These are retained as baseline
caveats, not hidden by a successful shell exit or by the graph-only parity above.

| switch | views correct ms | legacy correct ms | views CPU s | legacy CPU s | latency/CPU rule |
| --- | ---: | ---: | ---: | ---: | --- |
| HEAD → A | 19,524 | 62,256 | 51.10 | 123.93 | met |
| A → HEAD | 15,389 | 36,634 | 39.53 | 70.43 | met; manifest caveat |
| HEAD → B | 27,201 | 29,146 | 69.23 | 38.36 | **not met: CPU** |
| B → HEAD | 15,429 | 27,027 | 28.35 | 30.99 | met; manifest caveat |

### After drill — pending at owner handoff

**drill: not run - box shared (load 3.98, two foreign containers) at delivery**.
This refers to the optimized after-drill; the unmodified reconstruction/before
run above did complete. The final idle gate waited a full hour without passing.
Containers `8265fb983c3c` (`mc-investigate-890`) and `8ae3f6f2657e` belonged to
another seat and were left untouched. The owner explicitly directed delivery of
the verified offline changes rather than further waiting, and will run/append
the both-arm drill at the next idle window.

Consequently **the owner's shipping rule is not established for this change**:
"views beats legacy to correctness on every row with CPU not above legacy".
Offline views is strictly cheaper on this retained transition, but that does
not establish four-row end-to-end correctness latency or CPU. No optimized
drill table is fabricated or inferred from the offline timings.

From a clean checkout containing the optimizer, with no competing drill,
load < 3 and empty `docker ps`, use fresh storage (do not reuse either baseline
arm):

```sh
cargo build --release -p agent-file-tools --bin aft
git -C ~/Work/OSS/opencode rev-parse HEAD
nohup scripts/views-branch-drill.sh --mode both \
  --binary "$PWD/target/release/aft" \
  --storage "$PWD/target/branch-drill-per-binding/on" \
  --baseline-storage "$PWD/target/branch-drill-per-binding/off" \
  --output-dir "$PWD/target/branch-drill-per-binding/out" \
  > target/branch-drill-per-binding.log 2>&1 &
# Wait for completion, then verify the original HEAD is restored.
git -C ~/Work/OSS/opencode rev-parse HEAD
```

Append the four-row latency/CPU table and evaluate the rule separately for each
row, retaining host-load/phase contention and any publication defects. The
before report to compare is `target/branch-drill-baseline/out/branch-drill.md`;
its data table and caveats are also preserved above if target artifacts are swept.