diff --git a/.claude/hooks/session-start.sh b/.claude/hooks/session-start.sh index ba351dbd2..afbf9d0ac 100755 --- a/.claude/hooks/session-start.sh +++ b/.claude/hooks/session-start.sh @@ -64,15 +64,25 @@ cd "$CLAUDE_PROJECT_DIR" # merges, which surfaces as fake typecheck/test regressions (2026-07-19 audit). # Stamp the lockfile hash after a successful install and reinstall whenever the # lockfile no longer matches the stamp. -LOCK_STAMP="node_modules/.session-start-lock-hash" +# Keep this marker inside node_modules/.cache. npm's postinstall records a +# trusted file inventory of node_modules (scripts/check-installed-lock-parity.mjs +# --write-stamp), and this hook writes its own marker *after* that runs — so a +# marker written directly into node_modules/ leaves the tree one file ahead of +# the stamp and fails check:installed-lock-parity, which is the first real step +# of verify:pr-local. `.cache` is in that check's VOLATILE_DIRECTORIES set, so a +# marker there is ignored by the inventory while still being wiped by npm ci +# along with the rest of node_modules, preserving the staleness semantics below. +LOCK_STAMP="node_modules/.cache/session-start-lock-hash" lock_hash="$(sha256sum package-lock.json | cut -d' ' -f1)" if [ ! -d node_modules ]; then npm ci --no-audit --no-fund + mkdir -p "$(dirname "$LOCK_STAMP")" echo "$lock_hash" > "$LOCK_STAMP" echo "[session-start] Dependencies installed" elif [ ! -f "$LOCK_STAMP" ] || [ "$(cat "$LOCK_STAMP")" != "$lock_hash" ]; then echo "[session-start] node_modules is stale for the current lockfile, reinstalling" npm ci --no-audit --no-fund + mkdir -p "$(dirname "$LOCK_STAMP")" echo "$lock_hash" > "$LOCK_STAMP" echo "[session-start] Dependencies reinstalled" else diff --git a/docs/database-drift-detection.md b/docs/database-drift-detection.md index 878c572c2..b957463a8 100644 --- a/docs/database-drift-detection.md +++ b/docs/database-drift-detection.md @@ -52,6 +52,32 @@ storage bucket rows + storage.objects policies. carries the _identical_ name-stripped index definition under a legacy name (the machine-checked version of `search_schema_health()`'s `index_aliases`). +### Known coverage limits + +What the check does **not** see — established offline on 2026-08-12 while +answering `/issues` `#248` (the 20260705180000 search-health indexes that were +missing on live despite an applied history): + +- **Invalid indexes read as healthy.** `schema_drift_snapshot()` + (`20260706200000`) builds its index rows from `pg_index` via + `pg_get_indexdef` + an md5 `def_hash`, and never reads `indisvalid` / + `indisready`. An index left behind by a failed `CREATE INDEX CONCURRENTLY` + still has a definition, so it compares byte-identical and drift stays green + while the planner refuses to use it. `20260804110240` checks both flags at + apply time, so the guard exists for that one migration but not for the + ongoing probe. +- **Nothing gates on it.** `check:drift` runs only from + `.github/workflows/live-drift.yml` — `workflow_dispatch` plus a weekly Sunday + 18:30 UTC cron — and blocks no PR or release. Coverage of the plain + missing-index class is genuine (indexes compare by name on `table` + + `def_hash`, and `supabase/drift-allowlist.json` is empty, so a missing index + fails the run), but nothing forces a run between the drift appearing and + runtime `search_schema_health()` noticing. + +Both are decisions rather than defects: adding validity to the snapshot RPC is +a migration, and raising the cadence spends provider budget. Recorded so the +gap is chosen, not assumed away. + ### Workflow - Change `supabase/schema.sql` → run `npm run drift:manifest` (Docker) in the