Skip to content

Incremental indexing: reuse committed generations and bound write amplification across worktrees - #806

Open
zzet wants to merge 185 commits into
mainfrom
fix/incremental-index-write-amplification
Open

zzet wants to merge 185 commits into
mainfrom
fix/incremental-index-write-amplification

Conversation

@zzet

@zzet zzet commented Sep 14, 2026

Copy link
Copy Markdown
Owner

Summary

Indexing a repository with more than one checkout stops rewriting storage it does not need to change.

  • A committed snapshot of a repository is published only when something can read it, and once published it is not rewritten in place.
  • A dependent worktree pins the base it was built against, so advancing the primary branch costs it no rebuild and no catalog or payload write.
  • Safely separable resolution context is withdrawn from output. Contract-bearing context files remain explicit output when withdrawal could discard a canonical contract or its ownership edge.
  • A caller can demand a freshness guarantee — refuse a substituted answer, wait for a filesystem state — and the tool list did not grow to carry it.
  • Historically measured on a 1,500-file fixture, before the main merge and the later publication and aggregate-count fixes, against main 56a1c29d: a main advance with ten dependents writes 0.28× the bytes, untrack/retrack 0.51×, a cold index 1.04×; one phase regresses at 1.28× and is reported, not rounded away.
  • The store takes one additive migration (schema v25) and one full re-index per tracked checkout on the first start after the upgrade.
  • No acceptance gate is closed. The evidence below says what each one is still missing.

Problem

On the measured baseline main 56a1c29d, everything a sibling worktree reads sits on the primary's mutable generation zero. Four consequences the measurements name directly:

  1. A committed base was published whether or not anyone could read it — for every configured repository as soon as its generation-0 index completed, and again on every HEAD move, with a reader set of size zero in the measured shape (checkouts: 1, checkout_routes: 0).
  2. Advancing the primary re-did the dependents' work. The baseline arm spends a median 5,147,295,504 logical write bytes on one main-advance phase with ten dependents, and 1,464,290,236 on one dependent untrack/retrack.
  3. Read-only context was persisted as output. A file admitted only so a changed file could resolve got a durable payload row, FTS entries, an inventory row and a clone/vector record.
  4. Observation treated bookkeeping as content. Staging state, an mtime touch and a same-tree amend could each imply new source content, and a file delta expanded into a much wider closure: one 17-file resolver pass admitted 5,088 outgoing and 32,682 incoming pending references across 18 repositories, of which two resolved.

Underneath that, a payload write paid per-row B-tree maintenance across 19 secondary indexes, and a read-only tool call wrote a durable ~37 KB sidecar transaction.

What changes

Committed generations are published on demand and are immutable in effect. Sealing a generation is separate from making it active, and publication waits for a consumer: a no dependent checkout skip plus an on-demand schedule from the first reader's claim replace the unconditional one. Adoption is fenced on owner, incarnation, lifecycle, the previous active pointer, the desired tree and the complete build identity; head and active pointer move together or not at all. Generation zero is never relabelled.

A dependent pins the base it was built against. A committed-base advance costs a dependent whose own tree did not move zero commit-layer builds, zero working-tree builds and zero catalog or payload writes. Old coherent routes stay available with truthful freshness while a new one composes, and a new base is never spliced into an old delta.

A matching tree is copied, not re-parsed, inside a generation-scoped bulk write window whose bracket closes on every exit path. The earlier candidate measured 0.35× the WAL bytes of the re-parse route on a 1,500-file-sized payload, before counting avoided parsing; that figure predates the main merge and subsequent production correction.

Safely separable context is withdrawn from persisted output. The in-memory route removes context from payload, FTS, inventory and ancillary output before publication. Contract-bearing context paths are conservatively retained as explicit output because canonical contracts can still have surviving owners after file eviction. The hard check against payload on a withdrawn path remains intact, and ordinary context still withdraws. This can retain extra unchanged file payload; its write cost has not been remeasured. An earlier 120-file/6-changed audit measured WAL −32.1 %, db −17.4 %, free-list 73,728 → 0 before this correction; where the in-memory route cannot be taken the build falls back to write-then-withdraw and reports it.

A committed build reads the tree it advertises, and every rider tells the truth. Side channels can no longer reach dirty working-copy bytes on an immutable view; text search and file bytes come from the selected snapshot or are refused. A degraded capability, a drifted route, a labelled base fallback and a refused answer each say so in their own words.

Freshness is a contract the caller can state, and the tool list did not grow. require_fresh, an absolute RFC3339 wait_deadline and require_exact are accepted. fresh:true says which route answered, never that a wait returned; require_exact refuses every non-fresh outcome rather than a list of known reasons, so a new reason cannot bypass it; fallbacks stay read-only. A wait ends on a bound the request itself set — a 60-second deadline used to end at ~5 s, about 12× early, whenever a git status was slow. The three published profile bodies got smaller while absorbing it: 4,605 → 4,360 bytes against an unchanged 4,608-byte ceiling, 3,677 → 3,552, 7,518 → 7,356.

Storage stays shared and the migration is additive. One logical store, not a database per worktree or generation; schema v25 adds a generation axis to the analysis cache, legacy rows are copied at generation 0, and the newer-schema refusal still works. Maintenance moved into a serialized, pre-emptible lane, and the savings ledger commits once per window instead of once per read-only call — the historical measurement retained a sidecar database of 2,516,768 bytes against 4,695,192 (0.54×), before the later main merge and production correction.

Upgrade cost

  • One-time invalidation, once. The widened configuration fingerprint and the newly non-empty dependency revision each re-key every stored checkout generation, so every layer rebuilds once on first contact with this binary. Both are bundled into one deploy: plan for one full re-index per tracked checkout on the first start after it.
  • One additive migration to schema v25 — a generation column on the analysis-generation table, the active-generation table re-keyed on (generation, slot). Legacy rows are copied at generation 0, generation zero is not relabelled, and a future-schema store is still refused before a writer opens.
  • No down-migration. The context ownership mode is a vocabulary extension in a plain TEXT column with no version gate, so a binary predating this branch reads such a row as a claim with no payload and serves that path empty until the generation is rebuilt. Do not roll back across a store that has published a context-mode generation.
  • Knobs. The operator's re-resolve bound (index.affected_by_reresolve_max) now reaches the committed-base arm at all, and the closure cap there is sized by the change rather than raised. No new required configuration; the matrices and the measurement are opt-in, behind variables with no default.

Validation

Portability and maintenance repairs

Committed head: 5b18ea3d.

CI run 35037425925 passed all 12 test jobs: Store, indexer, MCP and the remaining packages on Linux, macOS and Windows. Linux and macOS ran with race detection and coverage; Windows ran the normal suite. All seven supporting jobs also passed, including security and skill drift. The tested merge acc97a2a4a109289eda7cb889f95c7b586114d02 has the same tree as this committed head.

Platform Store package Indexer package MCP package
Linux, race and coverage 1797.419s 1879.257s 1428.461s
macOS, race and coverage 1248.822s 1672.223s 1238.806s
Windows, normal 255.122s 1785.105s 804.732s

These are package elapsed times, not whole-job times or controlled performance comparisons. Some unchanged smaller packages used Go's test cache.

Windows executed and passed all three repaired cases: TestIncrementalMultiFileBatchKeepsFailedFileAndCommitsSiblings, TestIncrementalReindex_FailedFileSurfacedAndRetried and TestWatcherStormBatchDeletionFailureIsolationAndOneResolve. The precondition diagnostic observed a real lock violation on content read while the file remained stale, admitted and stat-able through the default filesystem path. Their original failed-file reporting, sibling progress, retained retry watermark and recovery assertions passed. The ownership-source rebind test and all seven confined-reader cases also passed without skips.

Skip identities are unchanged from the preceding ccc02e22 CI on every partition. Windows recorded 13 indexer, one Store, 42 MCP and 105 remaining-package skip events, including subtests; the logs retain their individual names and reasons. These include platform-specific and opt-in checks. The original acceptance-gate statuses and all 31 declared limitations remain as recorded below; this unit-test matrix does not supply new paired-write measurements or rerun the opt-in end-to-end matrices.

The follow-up local checks executed four common indexer tests on Darwin/arm64 in both normal and race/coverage modes: four passes in each mode, with no failures or skips. Formatting, whole-module build, vet, all-package test compilation, two test-name inventories, and lint also passed. The subsequent Windows-only helper change leaves all 4,898 declared repository inputs to those checks unchanged. Twenty-six earlier Go results are reused only for unchanged declared inputs; the earlier full local indexer normal and seven race runs below remain attached to their recorded source.

The Windows read-denial helper separately passed formatting, Windows/amd64 vet, and standalone test-file cross-compilation with CGO disabled. These checks compiled the helper without executing a Windows binary or cross-building the full indexer package. The helper preserves the default filesystem path, holds an exclusive byte-range lock, checks that metadata access succeeds while a second-handle content read fails, and explicitly unlocks and closes during restoration. The original failed-file, sibling-progress, retry-watermark and recovery assertions remain intact.

The preceding CI at ccc02e22 passed 11 of 12 test jobs. It proved all seven Windows regular-file cases and the ownership-source rebind case. The three remaining Windows cases failed because their read-denial fixture still allowed a direct read before indexing; stale state, admission and metadata access were correct. The replacement lock fixture's runtime result is reported in the current CI record above. The exact reason the prior sharing-mode handle was ineffective on that runner remains unexplained.

Local validation recorded at 6e7b2db8

The historical local checks in this table ran against merge fc13d2a03b1fcb08b7fdd926bfe53f094bc1428c plus the repairs subsequently committed at 6e7b2db8. Their 27-path fingerprint is d57b208eccf742d5f8a8a9263b6a18dc185a828b896f03a720acdc17dccb3c4b. They used Go 1.27 on Darwin/arm64, CGO enabled, GOMAXPROCS=2, GOMEMLIMIT=2GiB, isolated home/Git/temporary directories, and serialized test processes. Per-run manifests bracket Go-declared source/dependency/embed inputs and selected fixture/configuration inputs; those inputs remained unchanged. These checks do not make system libraries, SDK headers, other runtime-discovered inputs or daemon background load hermetic.

Check Result
Build and static checks Whole-module build, vet, test compilation and lint pass. All 154 test packages compile; 40 packages have no tests. Formatting passes for the explicit change scope. Compile-only package events are not executed test counts.
Prescribed normal scope All 23 runs across 18 packages pass: 9,882 top-level passes, 23 skips, zero failures; including subtests, 16,319 pass events and 29 skips. The fresh MCP and indexer inventories each produce exactly one terminal result per test name, with no omissions or duplicates. Benchmarks are compiled but not executed by these commands.
Full indexer normal 2,088 top-level passes, two opt-in skips, zero failures; 874.419s package / 885.154s driver.
Full Store race and coverage 1,023 top-level passes, two skips, zero failures; 1,906 pass events including subtests; 1,205.894s package / 1,223.872s driver. The 290 historical added identities total 218.91s; including the new cancellation-entry regression, 291 total 219.04s, missing the 180s target by 39.04s. The maximum is 15.00s.
Indexer race and coverage All seven chunks pass: 2,088 top-level passes, two opt-in skips, zero failures; 3,091 pass events including subtests. Every one of the 2,090 fresh test names has exactly one terminal result. Summed package time 1,674.954s; summed driver time 1,748.573s. The 491 added tests total 684.58s, missing the 240s target by 444.58s. Maximum 19.17s, within the 20s target; no added test exceeds it.
Supplemental main-integration checks Full githooks normal/race pass 19 tests each. Full resolver race passes 1,056 top-level tests with two skips and no failures, 59.008s package / 78.307s driver. These runs are separate from the prescribed normal total.

The unlimited-error indexer compilation and both focused Linux audit regressions pass; their race-and-coverage run has two top-level/four total pass events, no skips or failures. All 37 audit-helper calls pass the actual fixture Store. The scoped independent reviews found no weakened workload or assertions. Graph-excluded tests require this actual compiler and test evidence; a graph zero-count is not treated as proof of absence. The new cancellation test covers an already-canceled context, not a timed post-join cancellation race.

The earlier local timings below used different source and, for the large race suites, no coverage instrumentation. They remain historical evidence and are not controlled speedup comparisons.

The previous CI run at 4ef554463b1d0099c9bfced0cdd2cdbd333937e4 failed all three platform test jobs. The repairs close notebook sidecars after shared-server cleanup, make command fixtures and compile-database paths portable, and add a confined Windows regular-file reader. Dedicated-write audits now join already queued maintenance before installing their external SQLite triggers and taking the baseline. Their trigger corpus and logical-write, WAL-size and WAL-timestamp assertions remain intact. Store conformance tests reuse private copies of a pristine schema while preserving the conformance corpus and caller-owned cleanup.

CI selects every package exactly once across four partitions on each platform: Store, indexer, MCP and the remaining packages. Linux and macOS retain race detection and coverage; Windows runs the normal suite. The 45-minute package timeout and 60-minute job limit are unchanged. Workflow lint and selection dry runs pass; the current CI result above records execution on all three platforms.

The branch also integrates main fdf7fd02d3f0db59b5a7ca834423850f9da567ba in merge fc13d2a03b1fcb08b7fdd926bfe53f094bc1428c. The merge was conflict-free and preserved the pending repair-file bytes. Main's resolver change belongs to the Store package's compiled dependencies, so the pre-merge Store race-and-coverage pass is retained as scoped evidence and the merged source is validated separately.

The Windows reader's parent-directory acquisition can synchronously await a directory oplock; cancellation cannot interrupt that OS open. Confinement, final-reparse rejection, regular-file checks and handle cleanup remain enforced. The seven confined-reader cases and the ownership-source rebind case passed in Windows CI at ccc02e22; the latest CI record above covers the current revision. See limitation 31.

Historical local validation at 4ef55446

Local checks ran against the saved Go inputs subsequently committed at 4ef554463b1d0099c9bfced0cdd2cdbd333937e4. They used Go 1.27 on Darwin/arm64, GOMAXPROCS=2, GOMEMLIMIT=2GiB, and isolated home, Git and temporary directories. Before/after manifests record unchanged declared inputs. Their scope differs: indexer manifests cover known/changed Go and module inputs plus the unchanged oracle files; root runners additionally cover Go-declared dependencies and selected fixture/configuration files. They do not establish hermetic execution or control daemon background load.

Historical local validation at 4ef55446 Result
Whole-module build, vet and lint Passed; lint reported zero issues.
Test compilation 154 test packages passed compilation; 40 packages have no tests.
Formatting of changed Go files No formatting output for the 49 newly changed Go paths or the 18 paths in the earlier CI repairs.
Prescribed normal scope: 23 runs across 18 packages 9,875 top-level passes, zero failures, 23 skips; including subtests, 16,300 pass events and 29 skips. Repeated focused runs are excluded from these totals.
Full Store race suite 1,022 top-level passes, zero failures, two skips; 1,195.597s package time.
Full indexer race selection Seven disjoint chunks: 2,088 top-level passes, zero failures, two skips; 1,791.413s summed package time. All 2,090 compiled names have exactly one terminal result; no race reports.
Additional races Full graph and graphview; named command and MCP subsets; helper and contract-required subpackages. All passed within their declared scope.

The Store’s 290 added tests pass in 209.80s, missing the 180s target by 29.80s; the slowest takes 14.91s, below 20s. The indexer’s 491 added tests pass in 702.40s, missing the 240s target by 462.40s. Its only added test above 20s is the pinned-dependent case at 23.42s in chunk order. The isolated pinned-dependent race repetitions passed at 19.87s and 20.10s; the latter also exceeds 20s. These runtime targets are test-cost measurements, separate from product write-volume measurements.

The subsequent CI run at this source failed all three platform test jobs. Its local results remain historical evidence; they do not describe the repaired source or establish current platform readiness.

Validation manifests retain exact commands and named skips; the execution ledger records their scope and results. End-to-end matrices and paired write measurements were not rerun at this source. Historical E2E failures and all acceptance-gate boundaries below remain in force; these package results do not close them.

The current changes give private Git fixtures their own identity, make SQLite test URIs portable while preserving literal POSIX backslashes, preserve the Windows child environment, and use a portable free-space check. Test fixtures reuse private pristine stores and fresh Go-only registries only where the corpus is proven to contain Go exclusively. Reopen/isolation regressions protect against accidentally replacing populated databases. The checkout-removal test uses a separate terminal-error control and retains all four operations against a live lifecycle.

The publication correction preserves canonical contracts when withdrawing resolution context. Its regressions reproduce the previous failure and check ownership edges, masks, and unchanged lower layers. A second correction makes composed graph totals use the resolved base node’s file when accounting for replaced or removed identities. This prevents opaque IDs from being subtracted twice and relocated IDs from being missed. Its eight-case regression compares NodeCount, Stats and repository totals with expected enumeration, preserves the base graph, and rejects a direct AllNodes fallback from aggregation. These checks do not close an end-to-end or performance acceptance gate.

Historical validation provenance

Historical commit IDs below identify the source recorded when each binary was built. A later message-only history rewrite preserved every commit tree and merge-parent order; it did not rerun those binaries. For orientation, the recorded merge 0dd501bc is now a69e441a, and measured candidates 2fd5db82 and 271a9e9f correspond to 0caa20a6 and 19df574e. Historical numbers remain attached to their original recorded identities.

Merged with main

origin/main at a4b5c4df — 25 commits past the base 56a1c29d, 45 changed files — is merged into the branch as 0dd501bc, whose parents are 85d315c6 (the branch head before the merge) and a4b5c4df. Exactly two files conflicted, both on the same blame.EnrichGraph call each side had rewritten: the branch's store/root routing is kept verbatim and main's new repo-scoping argument is applied to the rerouted call, so neither side's change is dropped. Every other path in the merged tree is byte-identical to git's own clean-merge result. The merge was validated for correctness; its write cost was not remeasured. See limitation 28.

What ran at the merged source 0dd501bc Result
go build ./..., go vet ./..., and go test -count=1 -run '^$' ./... (every test package compiles) exit 0 — 153 packages ok, 40 without tests, 0 FAIL
Normal suites over 18 runs: cmd, internal/mcp (six first-letter chunks), internal/indexer (seven), internal/resolver, internal/daemon, graphview, store 15,146 pass / 0 fail / 29 skip; every skip a pre-existing platform or opt-in gate, each with its printed reason
Race subsets: cmd coordinator/checkout/dedicated/lease/handoff/status, internal/mcp ^Test[C-D], internal/indexer incremental/vector/warm/reconcile 955 pass / 0 fail, 0 DATA RACE
golangci-lint v2.13.1 (the CI pin), whole module 0 issues
gofmt -l ./cmd ./internal 99 files — every one byte-identical to a parent and none of them changed by the merge; an inherited repository-wide condition under go1.27.0's gofmt, recorded rather than fixed here
No-op family matrix, on a daemon built from the merged source (gortex-merged, sha256 c8b941c5…) 10/10 PASS, every case
Edit-taxonomy matrix, same daemon 4 PASS / 11 declared SKIP / 2 FAIL over 17 cases — the same table as before the merge, the same two reds, still reproducing byte-identically on a daemon built from main 56a1c29d

Static checks and package suites

What ran Source identity Result
Full normal + race suites (33 compiles, 46 runs) 9fc2e7ce 19442 pass / 0 fail / 41 skip, 0 DATA RACE, no new skip
golangci-lint v2.13.1 (the CI pin), whole module; gofmt -l over the diff; go vet ./...; go build ./... 9fc2e7ce 18 → 0 issues, all 18 branch-introduced, none suppressed; gofmt empty; vet and build exit 0
internal/graph/store_sqlite under -race 9fc2e7ce 0 races, 0 failed assertions, but -timeout 30m is not enough: it times out at 1800.6 s and is green at 90m in 2256.7 s
Naming and comment scrub suites (6 compiles, 6 runs) ea4aad93, commits through 55b5f5bf 3372 pass / 0 fail / 17 skip; build and vet exit 0; every test package compiles (153 ok, 40 without tests, 0 FAIL). No -race arm — renames and comments only

Skips are reported with their printed reasons. The historical rows retain their original platform and opt-in gates. Current platform-specific regressions are identified separately in the final validation record; a platform skip is not a runtime pass on that platform.

End-to-end matrices

Seven opt-in matrices drive a private daemon through the public CLI. Below is the re-run at 9fc2e7ce (daemon gortex-e2e sha256 28624cd5…), except the two rows that moved later, at fcc9ddd7 / 15b00d4f and again at the merged source 0dd501bc.

Matrix Outcome
View lifecycle PASS — 12 cases: 11 PASS, 1 OBSERVED (a same-tree branch switch re-keyed the routed pair, exact again in 68 ms)
Main advance, 10 dependents PASS — 12 cases: 9 PASS, 3 OBSERVED; 16 committed bases over 20 commits, dependents minting zero generations
Checkout lifecycle PASS — every row; drains gave 89 coherent answers, 89 clean refusals, 0 incoherent
Adversarial coherence PASS — every row; 48 advances → 1 root + 45 deltas, 49 live generations against a 70 ceiling
No-op family 10/10 PASS at 15b00d4f, with a dependent checkout and a live calibration (dirty half seq +0, catalog 0; commit half seq +1, catalog 3). At 9fc2e7ce nine of its ten cases failed on one shared instrument error, now gone
Edit taxonomy 4 PASS / 11 declared SKIP / 2 FAIL over 17 cases at 15b00d4f; the SKIPs are the declared semantic-metadata shortfall. Both FAILs reproduce byte-identically on a daemon built from main 56a1c29d, so they are pre-existing producer-side path-admission defects, not regressions
Resolution / provenance / manifests FAIL — at 9fc2e7ce, 20 rows: 13 pass, 2 not_exercised, 5 FAIL. Its own round scored 12 pass · 5 FAIL · 3 not_exercised against a main 56a1c29d arm of 9 pass · 8 FAIL · 3 not_exercised: five fail on both arms, three fail on the baseline and pass here

Do not read "end-to-end validated" from this: five of the seven matrices ran at 9fc2e7ce; the no-op family and the edit taxonomy re-ran at 15b00d4f, a descendant of it, and both ran a third time at the merged source 0dd501bc. The other five have not run since 9fc2e7ce. Both measurement verdicts ran against ancestor candidates (2fd5db82, 271a9e9f) and neither was re-run at any later source.

Paired measurement

Baseline main 56a1c29d against candidate 271a9e9f; three repetitions, medians with min/max; 1,500 generated files / 60 packages / 10 dependents / 20 commits / 10 edits / 60 s idles; judged against a budget file frozen from the baseline arm, never rewritten, digest-checked before judging.

Phase Baseline median (min/max) Candidate median (min/max) Ratio Verdict
P0_cold_index 949,066,148 (947,921,636/961,942,788) 989,877,036 (989,119,308/994,855,020) 1.04× recorded
P1_idle_cold 1,490,944 (1,323,008/1,556,480) 1,040,384 (765,952/1,044,480) 0.70× within budget
P2_small_edits 16,879,664 (16,781,360/17,084,464) 16,162,864 (16,035,888/16,355,376) 0.96× within budget
P3_touch_stage_unstage 364,544 (286,720/458,752) 335,792 (299,104/24,219,696) 0.92× recorded
P4_amend_same_tree 401,408 (303,104/446,464) 513,840 (438,016/595,232) 1.28× regression > 10 %, preserved
P5_main_advance 5,147,295,504 (4,958,552,632/5,152,481,816) 1,447,530,384 (1,420,576,656/1,496,648,000) 0.28× within budget (0.50× ceiling)
P6_dependent_edits 11,392,456 (11,113,944/13,308,200) 8,707,752 (4,775,480/8,790,232) 0.76× recorded
P7_dependent_untrack_retrack 1,464,290,236 (1,379,998,724/1,550,549,748) 752,579,724 (698,810,348/803,904,484) 0.51× recorded
P8_idle_warm 3,299,312 (3,166,256/14,796,120) 2,799,664 (2,147,408/4,228,088) 0.85× within budget

Series: logical writes, checkpoint-excluded for P2P4 and P8 (an upper bound on non-checkpoint work).

over budget: none · incomparable: none · regressions preserved: P4_amend_same_tree 1.28×. All nine phases completed in all three repetitions. Retained bytes after teardown 375,751,391 against the baseline's 434,887,204 (0.86×), under a frozen ceiling; store at run end 0.83×; at the end of the cold index 1.006×, with the generation table empty and one copy of the corpus in nodes (14,989 rows on both arms, against 29,978 before).

The one regression, plainly. Amending a commit without changing the tree writes 1.28× the baseline's bytes (112,432 median bytes) on a ~0.4 MB base, and no mechanism has been identified. The counters say what it is not: no publication (6 advances dispatched, 6 skipped, 0 published), no generation movement, not the amend alone.

What the headline is. The candidate's own counters put the main-advance saving in what a publish writes, not in publishes avoided: ~20 delta-shaped publishes, 20 dependent recompositions, zero reused base claims.

Declared limitations

All of them, as behaviour — what the branch does not deliver.

  1. Text search on a committed identity is not served completely — no generation-scoped text corpus exists (the trigram index has no serialization path), and a generation built before this change keeps its explicit "complete" row until rebuilt.

  2. No real compaction or reseed. Chain growth is bounded only by retention and the forced-full-root policy, as a measured number, not a proven bound: 48 advances gave superseded=1, retired=0, swept=0.

  3. Non-Git repositories have no immutable source image; correctness and lifetime closure are scoped to Git-backed ones. A raw roster member is witnessed, not frozen — read under a 2 s budget, a timeout refusing the cohort.

  4. Dirty-layer reuse does not survive a restart: a restart between two identical dirty states pays a full rebuild, and the dependent's base pin is in-process too.

  5. The primary's own working route stays on legacy generation 0 — its uncommitted edits remain the standing skew, stated at internal/indexer/checkout_coordinator.go:1759; publication is not activation.

  6. The forward closure is as wide as today: the forward-frontier delta gate (builder_closure.go:872) is not implemented, and the import-placement harness widens the relative arm the way the closure may err.

  7. gortex repos reads freshness at generation 0 by construction — the base view-generation constant is declared at internal/graph/store_sqlite/read_index_state.go:21 and bound into the WHERE view_gen = ? query at :77 — so it shows neither worktree nor derived generations; the door declares this in three places rather than changing it.

  8. Two identity changes each invalidate every cached generation once on first deploy — one deliberate deploy, as the upgrade cost states.

  9. Producer incompletenesses survive the context/output split: clone and similarity symmetry stays incomplete for a sparse generation, the reference-fact sidecar is not generation-inherited, and enrichment on the in-memory route sees the change set rather than the closure — untested, its hook being nil on every fixture in the package.

  10. In-memory context mode is an optimisation, not a guarantee: an oversized closure, a backend with no bulk path or a shadow slot not granted inside 2 s falls back to write-then-withdraw, where the orphan sidecars (file_mtimes, file_index_failures, clone_shingles, vectors, constant_values) remain. Both routes publish the same generation.

  11. A downgraded reader has nothing to refuse on: the context ownership mode is a plain TEXT vocabulary extension with no version gate, so an older binary serves such a path empty until it is rebuilt. The new-binary direction fails closed.

  12. A pin held by a dormant checkout cannot be asked back: its base is offered, refused and re-offered every sweep pass until the checkout is activated — bounded in payload, not in sweep work. The release path holds one slot; a second request overwrites rather than queues.

  13. A recomposition cannot reuse the dependent's working-tree payload — the survive-restart half is not implemented; what is bounded, and measured, is the build.

  14. The bulk window's second-copy payload ceiling ("≤ 260 MB, from 841 MB") is unverified: WAL bytes for the same payload are byte-identical across a 4000× cache_size range, the dense secondary indexes stay live, and synchronous is untouched.

  15. The change-bounded delta's re-measurement misses both targets — store delta 7.91 MB against ≤ 1 MB, a burst of 83.0 MB against ≤ 20 MB — attributed to its own withholding comparison, not to the cap. Its contribution to bounded costs is blocked.

  16. The 6,000-file scale axis has no post-fix counterpart: cold-idle 7.44× and amend 674× were measured once, before the fixes, and that arm did not finish.

  17. The same-tree amend regression survives at 1.28× (112,432 median bytes) with no identified mechanism, preserved rather than rounded away.

  18. The copy route carries residuals: generation zero's post-ready enrichment sidecars are copied though a re-parse never ran (a superset, never less), the four ownership masks unscoped, and a resolver-version bump between that pass and the reservation stays unprovable; containment is proved, not equality.

  19. Five product defects found by the resolution matrix reproduce on main 56a1c29d and have no owner: a withdrawn definition rebinding to an unrelated same-named module with origin=text_matched; a byte-identical restore not restoring the binding; a fresh-index oracle of the same tree binding correctly; a comment-only re-parse dropping provenance-bearing value_flow rows; a dynamically reached bind indistinguishable from a proof. With them, the user-level config.yaml index: block never reaching a repository's indexer (internal/config/manager.go:281-286).

  20. Two snapshot-correctness divergences found by the edit taxonomy are open and unowned: a withdrawn path's name is still served, and an excluded package's name is still served out of that file.

  21. Some diagnostics no live run has exercised: the view-lifecycle counters the quiescence verdict reads never appeared live, so it guards nothing it can see. The rendering half is closed (renderDaemonViews).

  22. X-Gortex-Cwd has no hop or loop guard. The class is pre-existing — the body cwd argument was always relayed verbatim — but the header widens the affected population; a mutually-claiming roster would cycle.

  23. The internal/persistence sidecar database has no generation axis — notes, memories, scopes and notebooks in a separate file, exactly what the storage criterion excludes. Neither planned nor rejected.

  24. Measurement honesty. Process-accounted writes are not NAND writes; a WAL size is not cumulative writes; the checkpoint counter is a lower bound; a 1,500-file replay is not sustained daemon behaviour. n = 3, darwin only, host load uncontrolled, janitor at 5 s rather than 1 h. One named phase improved by a measured factor on one fixture under stated accelerations; that is not "the problem is fixed".

  25. No acceptance gate is closed — the branch's evidence closes none of the ten, and bounded costs in particular is not claimable.

  26. The raw-repository owner surface ships with no production registrar: its seven exported entry points have zero non-test callers, so a raw roster member cannot exist in a running daemon and limitation 3 describes a path production never takes.

  27. Two mutable package globals exist only as test seamsindexer.installCppIncludeSearchPath (indexer.go:1518) and store_sqlite.maintenanceQuiesceTimeout (store_compact.go:118), each overwritten by one test file and unguarded against a parallel test.

  28. The paired measurement predates the merge of main. Both paired write-budget verdicts were taken on candidate binaries built before main was merged in; none of the paired write-volume ratios was produced by the merged or current source. main brought its own changes to the write paths — the durable vector-corpus restore on all warm reconcile routes (ee34a3f7), the hybrid vector channel surfaced in daemon status (8555824e), C# using-static incremental binding and its narrowed reuse-tag carry (13633a09, 023bf139), and the Pydantic validator/serializer entry points (4990233c). That delta was validated for correctness at the merged source (build, vet, the full test-package compile, 18 normal suite runs, 3 race subsets, both matrices, lint at 0 issues) and its write cost was not re-measured: not one phase of the paired protocol ran after the merge, so every paired write ratio and paired write-budget verdict above belongs to the pre-merge candidate and none is claimed for the merged or current source.

  29. Contract-bearing context paths are retained conservatively. A path carrying a canonical contract is explicit output even if no surviving owner ultimately requires every node on that path. This preserves coherent publication and can add unchanged payload. The paired write protocol has not run after this production change, so the historical ratios do not establish its write cost.

  30. Correct aggregate counts can require additional base-node lookups. The count correction resolves deduplicated layer candidate IDs before checking their actual base-file ownership. It adds no full-corpus enumeration, but can issue more bounded lookups than the former ID-prefix shortcut. That additional lookup cost has not been benchmarked.

  31. Windows parent-directory opens remain synchronous. On Windows, acquiring the confined parent directory uses synchronous os.Root operations. A directory oplock can delay that open, and context cancellation cannot interrupt it. The final metadata-file open rejects an incomplete oplock break, reparse entries, and nonregular descriptors. This change does not bound parent-directory open latency.

Open findings

  • Two producer-side path-admission divergences, pre-existing and unowned. The incremental admission path does not apply the ignore rules cold discovery applies, so a withdrawn path's name is still served; and a committed .gortex.yaml exclude: does not withdraw content the incremental path already holds. Both reproduce byte-identically against a daemon built from main 56a1c29d, on the same test binary, fixture and oracle, three samples a minute apart — so neither arm is convergence lag.
  • The resolution matrix's reds are pre-existing. Five cases fail on both arms (main 56a1c29d: 9 pass · 8 FAIL · 3 not_exercised; this branch: 12 pass · 5 FAIL · 3 not_exercised); three fail on the baseline and pass here. They are listed in limitation 19.
  • The same-tree amend regression has no mechanism — the counters rule out publication, generation movement and the amend itself.
  • The 6,000-file axis was never re-run after the fixes. Cold-idle 7.44× and amend 674× have no post-fix counterpart, and the 6,000-file main-advance arm failed its trailing isolation probe after all 20 commits and 230 exactness waits succeeded, all ten dependents composed and exact a moment later. Whether the dependent view leaked is unanswered, not answered in the negative, and the guard was not relaxed to finish the arm.
  • Not run: the second-copy payload ceiling, the daemon-level 60-second idle window, peak space for a reseed (there is none). Run and missed: the change-bounded delta's re-measurement.
  • Guards nothing has exercised: the typed closure limit never truncated, even at the smallest value the source honours; the quiescence verdict is vacuous, its counters never having appeared live; dirty reuse has no outcome label of its own, so the undo/redo case is not measurable; and the publication-and-route entry point is dead production code.
  • On a full volume the daemon does not retry the build it lost (the marker became reachable only after a restart), SIGINT is not honoured, and the storage-failure census stayed empty while the log named the failure.
  • Both measurement verdicts ran against ancestor candidate binaries and the paired measurement was never re-run: not at 9fc2e7ce, where the whole production delta is a lint/format commit and "no measured quantity can move" is an argument rather than a measurement; and not after the merge, where main's own changes to the write paths make even that argument unavailable. See limitations 28–30.
  • At the recorded merged source, the core tool-list byte budget was 20 bytes from its hard ceiling. The assertion is a strict < 97500; the recorded merged source measured 97480, against 97439 before the merge, the +41 bytes being main's new search_text limit description. That recorded assertion passed with only 20 bytes of margin; current package-suite results are reported in the validation section; the exact byte figures here remain historical. Neither side is at fault alone — it is a composition effect, and a compaction pass is worth doing before the next description lands. At that recorded source, the two narrower published surfaces measured 29,968 / 30,000 and 21,525 / 21,600 bytes.
  • gofmt -l ./cmd ./internal is not empty at the merged source — 99 files — and the merge caused none of it. Every one of the 99 is byte-identical to a parent (98 to the branch head before the merge, 1 to main, which is itself unformatted under go1.27.0's gofmt), and neither conflicted file is among them. It is an inherited repository-wide condition, almost certainly a toolchain formatting-rule change, recorded rather than fixed here; golangci-lint reports 0 issues over the same tree.
  • The historical merge-validation suites first ran on the merged worktree before its merge commit existed, so those initial result files record the pre-merge head with a dirty tree. Build, vet, full test-package compilation and the prescribed package suites were subsequently rerun and passed on the committed merge. The current validation section identifies the later source and input manifests separately.

How to reproduce

Use a checkout of this repository, Git, the Go 1.27 toolchain, and the C toolchain required for CGO builds. The shell examples use a POSIX shell. Each opt-in test starts its own daemon under a private root: it inherits your HOME but drops every GORTEX_*, XDG_* and GIT_* variable from the environment and replaces them with private XDG_CONFIG_HOME, XDG_DATA_HOME and XDG_CACHE_HOME directories, an explicit store path, its own Git fixture, and a private GIT_CONFIG_GLOBAL with GIT_CONFIG_NOSYSTEM=1. The daemon's socket, PID file and logs are derived from that private cache root, so the socket is private too; none of it addresses a running daemon, store or configuration.

The commands below cover selected package checks and the historical opt-in experiment. The execution ledger records local validation and its environmental boundaries; the CI record above supplies the completed current-head platform results.

export GOWORK=off GOTOOLCHAIN=local GOFLAGS="-mod=readonly -buildvcs=false"
export GOMAXPROCS=2 GOMEMLIMIT=2GiB

# 1. Build and static checks; the compile-only command executes no tests.
go build ./...
go vet ./...
go test -run '^$' ./...
golangci-lint run

# 2. Selected package suites. The ledger lists all 18 packages used locally.
go test -count=1 ./internal/graph/... ./internal/graphview/... ./internal/resolver/...

# 3. Full Store race with coverage. The recorded 6e7b2db8 Darwin run took 1205.894s;
#    291 added tests total 219.04s, above the 180s optimization target.
#    CI retains the 45-minute per-package timeout. This is not Windows proof.
go test -count=1 -race -timeout=45m -coverprofile=store-coverage.out ./internal/graph/store_sqlite/

# 4. Full indexer normal, then seven race-and-coverage selections.
#    Validate the fresh test-name inventory if adapting these selections.
go test -count=1 -timeout=45m ./internal/indexer/
chunk=0
for p in '^Test[A-B]' '^TestC' '^Test[D-H]' '^Test[I-M]' '^Test[N-R]' '^Test[S-T]' '^Test[U-Z]'; do
  chunk=$((chunk + 1))
  go test -count=1 -race -timeout=45m -coverprofile="indexer-${chunk}-coverage.out" -run "$p" ./internal/indexer/ || exit 1
done

# MCP uses six normal selections locally; all 3629 compiled names are covered.
for p in '^Test[A-C]' '^Test[D-H]' '^Test[I-M]' '^Test[N-R]' '^Test[S-T]' '^Test[U-Z]'; do
  go test -count=1 -timeout=45m -run "$p" ./internal/mcp/ || exit 1
done

The matrices and the measurement are opt-in: with the variables below unset they skip by name, so a plain go test ./... never runs them. Each takes a daemon binary you build yourself — BIN is any path you choose.

BIN=$(pwd)/bin/gortex-candidate
mkdir -p "$(dirname "$BIN")"
go build -o "$BIN" ./cmd/gortex/

# 5. All seven end-to-end matrices (one variable switches on every one of them).
GX_E2E_MATRIX_BINARY="$BIN" go test -count=1 -timeout 120m ./cmd/gortex/ -run \
 'TestE2EMatrix(NoopFamily|EditTaxonomy|ResolutionProvenanceManifests|4ViewLifecycle|5MainAdvanceWithTenDependents|6Lifecycle|7Adversarial)$'

# 6. The paired measurement: build the recorded baseline commit,
#    then run both arms into a directory you choose.
BASE=$(mktemp -d) && git archive 56a1c29d514d8f7d3b5feec455c7b57de358b1d3 | tar -x -C "$BASE"
(cd "$BASE" && go build -o "$BASE/gortex-baseline" ./cmd/gortex/)

OUT=$(mktemp -d "${TMPDIR:-/tmp}/gortex-measurement.XXXXXX")
GX_SUSTAINED_IO_TEST_BINARY="$BIN" \
GX_SUSTAINED_IO_BASELINE_BINARY="$BASE/gortex-baseline" \
GX_SUSTAINED_IO_ARTIFACT_DIR="$OUT" GX_SUSTAINED_IO_REPS=3 \
  go test -count=1 -timeout 8h ./cmd/gortex/ -run '^TestSustainedIOSustainedWriteAmplification$' -v

# 7. Freeze the budgets from the baseline arm, then judge — in this order: the
#    first invocation never reads the candidate, and the frozen file refuses
#    to be rewritten by the second.
GX_SUSTAINED_IO_PAIRED_ARTIFACT_DIR="$OUT" GX_SUSTAINED_IO_BUDGETS_ONLY=1 \
  go test -count=1 ./cmd/gortex/ -run '^TestSustainedIOPairedArmsVerdict$' -v
GX_SUSTAINED_IO_PAIRED_ARTIFACT_DIR="$OUT" \
  go test -count=1 ./cmd/gortex/ -run '^TestSustainedIOPairedArmsVerdict$' -v

Without GX_SUSTAINED_IO_BASELINE_BINARY only the candidate arm runs. The fixture defaults are the measured ones (1,500 files / 60 packages / seed 767 / 10 worktrees / 20 commits / 10 edits / 60 s idles / 1 s sampling), each overridable within bounds. The measured arms accelerate the janitor to 5 s from its 1 h default; a confirmatory arm at that default is recorded separately.

The execution ledger records current state and validation, the measurement record records the historical write-volume results, and the design document explains the design.

zzet added 30 commits September 10, 2026 15:09
Preserve source-group cardinality and resolver provenance in point and batch updates. Fall back to structural indexing for ambiguous duplicate targets and ignore foreign-file rows when matching owned output.

Validation: 37 focused tests, repeated race runs, and before/after presentation, structural, and ambiguous row-mutation benchmarks.
Do not advise restarting another daemon for a database newer than this binary. Close constructor resources when backend initialization fails, allowing a later safe retry to acquire the store lock.

Validation: full serverstack suite, new regression tests under race detection three times, vet and private future-schema refusal benchmark.
Separate tree/path/mode/blob identity from staging and timestamp observations. Rehash uncertain files and fence root/path replacement, recent writes, and clock discontinuities; retain only bounded digest metadata.

Validate with full unskipped gitstate normal/race tests, Linux and Windows compilation, and scoped lint. Benchmarks document the extra Git-status cost and exclude subprocess reads from sampler byte counts. Git-clean-filter admission remains a documented limitation.
Add an optional bounded regular-file capability with confined filesystem reads, bounded Git batch headers, and explicit non-content inventory. Preserve legacy ContentSource behavior and selected-layer ownership.

Validated with source tests and race tests, lint, Windows amd64/386 and Linux amd64 compile checks, 1595 indexer tests (2 opt-in skips), and corrected 100x3 reader/inventory benchmarks. Native mutation publication is fresh. Post-change graph analysis timed out even for a single 14-line staged file; those graph checks are unavailable, not passed. Committed with user approval on the completed isolated validation.

This is a prerequisite for safe manifest discovery, not completion of immutable-base publication or write-amplification repair.
Never compose a dedicated full root over mutable generation zero. Validate graph, checkout, repository and full-root identity for standalone and inherited dedicated ancestry. Preserve legacy commit composition and existing lease release behavior.

Validated on actual files: full graphview tests, full race suite, scoped lint, formatting, and diff checks. Regression cases cover mutable-root leakage, malformed roots, masks, ancestry and public-ref lease cleanup. Paired 100x3 benchmarks show about 11 microseconds of inherited-root validation overhead; no indexing-throughput claim.

All native edits published fresh. Staged native detect timed out after 59 seconds without symbol IDs; downstream native checks remain unavailable, not passed. User approved isolated tests/race/lint/benchmark fallback for unavailable native post-analysis. This is a prerequisite, not completion of immutable-base runtime or disk-I/O work.
Preserve the probe variable scope while clearing ineffassign. Validate 15 normal and 15 race executions, vet, and full store-package lint; record the isolated probe benchmark without attributing I/O variance to this declaration-only change.
Share the physical generation runner while preserving leader ownership and ordinary sparse behavior. Add guarded initial-snapshot construction with empty-commit and ready-reuse coverage; runtime activation remains deferred.

Validation: full indexer suite 1608 pass, 2 opt-in skips; final claimed tests 13 normal and 39 race executions; vet and lint clean. Native staged analysis unavailable while the exact view refreshes; isolated validation used under the approved post-analysis fallback.
Normalize store-emitted sealed payload refusals at the panic boundary while preserving legacy nonfatal precedence and unrelated panic behavior. Add regression coverage and document validation and benchmark evidence.

Validation: complete normal package coverage; split race coverage covers 780 passing and two skipped tests after the original run hit its 15-minute global timeout. Focused physical-source tests, vet, and lint passed. Native post-change graph analysis was unavailable; no native green result is claimed.
Serialize reference admission with retirement, qualify managed writers, and recheck physical flights and reader ownership before deleting payload. Preserve guarded terminal transitions and compatibility error priorities.

Add deterministic Store, reader, and physical-builder regressions, repair fixtures to use valid catalog identities, and document measured transaction cost plus remaining runtime and disk-I/O gates.
The staging-graph fence lists every non-test file permitted to build an
in-memory graph that no restart can recover. The dedicated claimed builder
legitimately needs one: an initial claimed snapshot composes against an empty
lower operand, and that operand never receives the indexed corpus. The
reserved positive SQLite generation stays the durable payload target and the
workers stream their output into it, so nothing recoverable is held in memory.

Record the caller with that reasoning so the fence keeps failing for any other
new entry.
…rded it

Twenty-one evidence-log rows all read 2026-09-10 because that date was stamped by default, but the work they describe was actually recorded between 2026-09-11 and 2026-09-15.
Each row now carries the date of the ledger commit that first wrote it, which also puts the date column back in order; the genuine 2026-09-10 rows and the later ones are untouched.
Two files needed a hand resolution, and both conflicts had the same cause.
Main gave blame.EnrichGraph a third argument that scopes the pass to one
repository, so a walk over one repo's root can no longer stamp another
repo's identically-pathed nodes. This branch had rerouted the same call to
read the store and root handed out by the enrichment output authority.
Each side rewrote the same call for a different reason, so neither rewrite
could simply win; both were kept.

cmd/gortex/daemon_controller.go: kept the branch's admission of an output
generation before the enricher runs, and gave blame.EnrichGraph the
target's repository prefix that main now requires.

internal/mcp/tools_enhancements.go: kept the branch's per-target output
admission together with its generation and superseded reporting in the
blame enrichment handler, and gave blame.EnrichGraph the loop's repository
prefix. Main's ownership data-state counting elsewhere in the same file
merged without conflict and is unchanged.

Everything else composed cleanly: main's hybrid vector channel in daemon
status, the durable vector corpus restore on warm reconcile routes, the C#
using-static call binding, and the dependabot module bumps.
Adds the merge evidence row (conflict resolutions, suites, race subsets, lint,
both end-to-end matrices on the merged daemon, verifier findings), declares that
the paired write measurement was not re-run after the merge, and points the
reproducible-evidence gate at the merged-source row. Also corrects an orphaned
commit reference and two stale file:line pointers.
The limitation cited the doc comment line; the constant itself is
declared nine lines later.
The private git fixtures set GIT_CONFIG_GLOBAL and GIT_CONFIG_NOSYSTEM so a
developer's configuration cannot change what they commit. That also hides every
configured user.name and user.email, leaving git to auto-detect an identity from
the passwd entry and the hostname. A developer machine has a hostname git
accepts, so the guess works and the fixture looks hermetic. A CI runner has an
unqualified hostname, git marks the guess bogus, and every commit the fixture
makes dies with "Author identity unknown" - 22 failures on the Linux leg and 18
on Windows, all of them through the dedicated-advance fixture's git helper.

Name the identity in the helper's environment instead. Putting it there rather
than in -c user.name= arguments covers every git subcommand that writes an
object, so a later merge or am in one of these fixtures cannot reintroduce the
failure by forgetting the flags.

Reproduced first by injecting user.useConfigOnly through GIT_CONFIG_COUNT, the
one config channel these helpers do not override: red with the same
"Author identity unknown" and the same file:line as CI, green afterwards.
(&url.URL{Scheme: "file", Path: path}).String() writes the "//" that introduces
an authority whenever a scheme and a path are both present. A POSIX path already
starts with "/", so the result is file:///tmp/... and is correct. A Windows path
does not, so the drive letter lands in the authority position and SQLite refuses
the DSN with "invalid uri authority: C:%5CUsers%5C...". That is ~25 Windows test
failures, nearly all of them through the two write-audit probes in the indexer
tests.

Move the rules to one place. internal/testdsn mirrors the production builder's
sqliteDSN: absolute path, separators folded to "/", a leading "/" prefixed when
the result lacks one so the drive letter sits in the path, and url.URL for the
escaping. Its own test asserts the exact DSN strings for Windows-style and POSIX
inputs, which is the only Windows evidence a POSIX host can produce, and is why
the rendering half folds backslashes on every platform instead of delegating to
filepath.ToSlash.

Every hand-rolled DSN in the branch's new tests now goes through it, including
the "file:" + ToSlash sites that survive a drive letter but escape nothing.
The isolated tracking child is this test binary re-executed with an environment
built from nothing but the isolation variables. That is not a loadable process
environment on Windows: the binary is CGo-linked, and the loader resolves its
own runtime DLL imports from PATH before main runs. With PATH rebuilt as git's
directory plus System32, the toolchain directories are unreachable and the child
dies at load time with STATUS_DLL_NOT_FOUND (0xc0000135), which the parent
reports as "isolated tracking child failed" with no child output at all.

Start from the parent's environment and drop only what the fixture supplies
itself - HOME, the temp and AppData names, and anything prefixed GORTEX_, XDG_
or GIT_ - then append the isolation variables as before; exec dedupes to the
last value per key, so the overrides still win. The parent's PATH goes behind
the directories the fixture names, so the resolved git stays first. SystemRoot,
COMSPEC, PATHEXT and the rest of a startable Windows environment now come
through on their own.

The child's own git helper inherits this environment rather than building one,
so it gets an explicit author and committer identity here too: the isolation
hides every configured user.name, and a runner's unqualified hostname turns
git's fallback guess into a fatal "Author identity unknown".

Isolation is unchanged and the child proves it: it still asserts HOME is unset,
the XDG and temp roots are the private ones, no external provider or telemetry
endpoint is inherited, the four Git guards hold exactly, and every platform
directory resolves inside the isolation root.
The lifecycle matrix probed free disk space with syscall.Statfs, which does
not exist on Windows. That single helper made the whole cmd/gortex test
package fail to build there, so none of its tests ran, while Ubuntu and macOS
built it fine.

Move the helper into two build-tagged files. The POSIX file keeps the statfs
implementation unchanged. The Windows file reports the space available to the
calling user via golang.org/x/sys/windows.GetDiskFreeSpaceEx, the same figure
internal/platform/disk_windows.go already uses; like statfs f_bavail it
excludes space the caller cannot actually write, so the filler loop still
stops when the volume is full for the user running it. golang.org/x/sys is
already a direct requirement, so this adds no dependency.

The disk-full row itself stays Darwin-only, since it builds its volume with
hdiutil, and it records a named skip elsewhere rather than a pass. Both call
sites already treated a failed probe as "cannot measure" - a recorded note
and the string "unknown" - and that is unchanged.
@zzet
zzet force-pushed the fix/incremental-index-write-amplification branch from 303cbfe to aff1d47 Compare September 15, 2026 06:40
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant