# ADR-0037: Materialized project index (store-owned, daemon-written)

- **Status:** accepted (operator-agreed direction 2026-07-10, dispatched 2026-07-15)
- **Context:** PROJECT-INDEX milestone; RCA `.claude/reports/2026-07-10-hertz-session/03-cli-project-index-latency-rca.md`

## Context

Project attribution for endpoints (which project a perch belongs to, its
display name) is DERIVED state computed today at read time: every
list-shaped command walks session history and every `p-*` context branch
with sequential git subprocesses — `O(perches × branches)` process spawns,
measured 29–35s on a real node. The same walk rode adapter hook paths and
broke message delivery by timing out the hook ceiling. Two prior mitigations
(subnet removal, adapter-side identity bypass) proved the root is this
synchronous derivation, not the mesh and not the adapter.

## Decision

Project attribution becomes a **persistent materialized index**:

1. **spt-store owns the versioned index format and the read path.** Readers
   (CLI list/picker/endpoint-info, adapters, hooks) join the index with the
   perch roster and return immediately — they never run git, never fall back
   to synchronous enrichment, and render last-known-good (or `-`) when stale.
   Daemon-offline reads consume the last persisted snapshot.
2. **The daemon is the sole, single-flight writer.** It loads the persisted
   index at startup, reports ready without warming, reconciles in the
   background with batched work (`O(P + B + F + C)`: one branch enumeration,
   at most one tree scan per changed branch, one derivation per distinct
   normalized cwd), replaces the index atomically, coalesces invalidations,
   and preserves last-known-good on any failure.
3. **Freshness is event-driven + fingerprinted.** No authoritative
   context-store commit path exists, so invalidation keys on branch-tip
   fingerprints with a debounced global refresh, per-endpoint refreshes on
   session/cwd/bind/rename/fork/purge events, and a low-frequency periodic
   reconcile. Ordinary commits do not change project identity.
   <!-- [doc->REQ-PROJECT-INDEX-INVALIDATION] -->
4. **Behavioral parity is part of the contract.** Precedence (session-cwd →
   origin-cwd → context-recency) and rendered IDs/names are unchanged;
   complexity counters are the deterministic CI gate; wall-clock latency is
   manual acceptance only.

## Consequences

- Reads become O(index) and daemon-offline-safe; the hook-deadline delivery
  class closes (with the W1 identity-only resolver riding this milestone).
- The daemon gains a derived-state maintenance duty and an observability
  surface (generation, pending refresh, last duration/error, counts,
  hits/misses/stale-reads) — index presence alone is not health.
- Staleness becomes a rendered state (`-`/last-known) instead of a stall:
  readers trade worst-case blocking for bounded-lag truthfulness.
- Rejected alternatives: memoizing the existing walk (still unbounded
  subprocess fanout on misses), moving the loop to a background thread
  (same work, hidden), a writer-maintained generation counter (nothing
  authoritative maintains it today).
