# PROJECT-INDEX — milestone triage (JIT)

> Doyle triage 2026-07-15, dispatched at the v0.32.0 boundary (operator pick).
> Canonical RCA: `.claude/reports/2026-07-10-hertz-session/03-cli-project-index-latency-rca.md`
> (hertz, root pinned, architecture agreed with operator). Decision record:
> ADR-0037. Lane: doyle triages → todlando builds → doyle gates → hertz
> field-verify partner.

## Why (two independent motivators, one root)

1. **CLI responsiveness (the operator's daily pain):** `spt endpoint list`,
   the picker, and every list-shaped read spawn 100+ sequential git
   subprocesses per invocation — `cmd_endpoint_list` computes a latest
   project per perch via `latest_project_ref → project_refs_for →
   project_history_for` (cli.rs ~2954, picker/data.rs ~456-518). Measured
   29–35s at 13 perches × 7 `p-*` branches; `O(P × B + C)` process fanout.
2. **Message-delivery correctness (the promoter):** the same fanout rode the
   UserPromptSubmit hook path via `whoami`'s alias to the full endpoint list
   and TIMED OUT the ~30s hook ceiling — bodies black-holed (2026-07-15
   incident, closed adapter-side; core root UNFIXED). Any box with real
   perch/branch counts can reproduce. The adapter's harness-hosted fallback
   stays deadline-vulnerable until the core resolver ships (perri's F-040
   filing, claude-spt `docs/SPT-CORE-FINDINGS.md` @d775b38).

## Shape (agreed, RCA §Agreed architecture — not re-litigated at build)

Persistent **materialized project index**, one subsystem — not a memoization
patch, not a background thread around the existing loop (explicitly
rejected):

- **spt-store owns** the versioned index format + read path (daemon-offline
  reads = store reads of the last persisted snapshot).
- **The daemon is the SOLE single-flight writer**: load persisted index at
  start; ready WITHOUT warm; background reconcile; enumerate branches once;
  ≤1 tree scan per changed branch; one derivation per distinct normalized
  cwd; atomic replace; coalesced invalidations; last-known-good on failure.
- **Readers NEVER run git.** List/picker/endpoint-info join index × perch
  roster and return immediately; stale/missing renders last-known or `-`.
- **Invalidation is event-driven**: new session/cwd → that endpoint + shared
  cwd cache; context-store mutation → ONE debounced global membership
  refresh keyed on branch-tip fingerprints (no authoritative commit path
  exists today — do not invent a generation counter that nothing maintains);
  bind/rename/fork/purge → affected rows; low-frequency periodic reconcile.
  Ordinary commits do NOT alter project identity — cwd identity refreshes
  only on path/repo-identity/`.git/config` change.
  <!-- [doc->REQ-PROJECT-INDEX-INVALIDATION] -->
- **Behavioral parity is binding**: precedence session-cwd → origin-cwd →
  context-recency; rendered project IDs/display names must not change.

## Waves

### W1 — identity-only resolver + the index store (read side)
- **REQ-WHOAMI-IDENTITY-ONLY** (F-040, correctness-critical opener): a core
  identity-only resolution — session → endpoint|null — that touches NO
  list/registry/project/git/network path, plus `whoami` de-aliased from
  `cmd_endpoint_list`. This alone closes the hook-deadline delivery class.
- **REQ-PROJECT-INDEX-STORE**: the versioned format + read path + reader
  contract in spt-store.
- W1 ships a consumable surface on its own: hooks/adapters get the fast
  identity verb even before the writer exists.

### W2 — the daemon writer + invalidation
- **REQ-PROJECT-INDEX-WRITER**: single-flight background writer with the
  batched complexity contract `O(P + B + F + C)` + observability (generated
  time, source generation, pending refresh, last duration/error, counts,
  hits/misses/stale-reads/repairs — index presence alone is not health).
- **REQ-PROJECT-INDEX-INVALIDATION**: the event-driven invalidation set +
  debounce/coalesce semantics above.

### W3 — reader cutover + acceptance
- **REQ-PROJECT-INDEX-READER-CUTOVER**: list/picker/endpoint-info consume
  the index (no git in any reader); parity suite; complexity counters wired
  as the CI gate; fully-qualified direct run stays picker-free and the
  direct-run 25s session gate stays separately observable.
- Manual latency acceptance on the 13-perch/7-branch fixture (target:
  ~30s → sub-second) + hertz field-verify on HFENDULEAM.

## Gate rulings (pre-made, RCA §Regression tests — binding at gate)

1. **Complexity COUNTERS are the CI gate** (one branch enumeration, ≤1 tree
   scan per changed branch, one derivation per distinct cwd). **Strict
   wall-clock budgets are manual acceptance ONLY** — never a shared-runner
   CI assertion (flake class).
2. Cold start: daemon ready + CLI fast BEFORE background index completes.
   Warm start: persisted index immediately readable; unchanged generation
   performs no scan.
3. Degradation legs: git unavailable, branch malformed/locked, cwd deleted,
   index truncated, schema mismatch — all preserve fast reads +
   last-known-good.
4. Every new e2e that spawns a daemon tree enters the HEAVY nextest group AT
   BIRTH (FLAKE-LEDGER #15 lesson).

## Explicitly out of scope

- MESH-RECOVERY (sibling milestone candidate, separate contract).
- Any change to project-precedence semantics or rendered names.
- Adapter-side work (perri consumes the W1 verb; comms at W1 ship).
