@tekyzinc/gsd-t 5.11.27 → 5.11.29

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -2,6 +2,84 @@
2
2
 
3
3
  All notable changes to GSD-T are documented here. Updated with each release.
4
4
 
5
+ ## [5.11.29] - 2026-08-11
6
+
7
+ ### Fixed — 20 of 27 projects had no usable code graph, and nothing said so
8
+
9
+ A binvoice session reached for the graph, found none, and read an 827-file
10
+ project by grep. `update-all` had reported that project as current for months,
11
+ because it was: every file GSD-T ships was in place. The graph is not a shipped
12
+ file — it is built state, created only when someone runs `gsd-t graph index` by
13
+ hand, and no propagation step creates it.
14
+
15
+ Checking for it exposed a second, larger failure. M99 moved the store from
16
+ `.gsd-t/graph.db` to `.gsd-t/graphDB/graph.db` — it changed where the code LOOKS
17
+ without moving what was already there. 18 projects still held a real, populated
18
+ index at the old path that every tool walked straight past.
19
+
20
+ never built 2 (binvoice, newman)
21
+ at the old path 18 — a full index, invisible to the tooling
22
+ working 7
23
+
24
+ **Absence is now repaired, not reported.** Per David's rule: a missing graph is
25
+ BUILT, a stale one is UPDATED, a misplaced one is MOVED, and grep is reserved
26
+ for content that cannot be indexed at all (`.md`, `.sql`, `.json`, config,
27
+ prose). The prior contract said HALT on an absent graph — correct 20 times here,
28
+ and it would have repaired nothing.
29
+
30
+ The rule now also governs plain conversational sessions. The binvoice failure
31
+ was not a wired command; it was ordinary work, and the contract only ever
32
+ covered `/gsd-t-*` commands.
33
+
34
+ - `bin/gsd-t.js`: `graphState()` tells the two faults apart; `migrateLegacyGraph()`
35
+ moves a pre-M99 store (the resolver's own migration, already written for M99
36
+ and never called on existing projects); `buildGraph()` indexes from scratch and
37
+ verifies a store actually landed rather than trusting the indexer's exit code.
38
+ A failed repair is reported per project and never counted as fixed.
39
+ - `.gsd-t/contracts/graph-consumer-wiring-contract.md`: FAIL-LOUD now repairs
40
+ before halting.
41
+ - `~/.claude/CLAUDE.md` + `templates/CLAUDE-global.md`: the rule reaches every
42
+ session, not only wired commands.
43
+ - `test/m112-graph-health-check.test.js`: 11 regressions.
44
+
45
+ Run once here: 18 stores moved, 2 graphs built (binvoice 639 files/56,822 edges,
46
+ newman 610 files/27,582 edges), all verified answering.
47
+
48
+ ## [5.11.28] - 2026-08-11
49
+
50
+ ### Fixed — the graph stored the alias edges, then could not find them (two bugs)
51
+
52
+ `who-imports src/lib/db.ts` returned 5 importers on hilo-figma-atos.
53
+ `grep -rl '@/lib/db' src` returned 793. Found by David, who halted the scan
54
+ rather than running it on a graph that was answering wrongly.
55
+
56
+ v5.11.26 taught the INDEXER to expand `@/lib/db` into `src/lib/db`, and it does
57
+ — the edges are in the database, correctly. It never met the QUERY side, which
58
+ is what turns `src/lib/db` into the real file id `src/lib/db.ts`. Two
59
+ independent bugs there, and fixing either alone still returns nothing:
60
+
61
+ 1. The resolver opened with `if (!dst.startsWith(".")) return dst` — an expanded
62
+ alias has no leading dot, so it was classed as an external package like
63
+ "react" and returned untouched. The extension was never appended.
64
+ 2. The set of known files was built from FUNCTION nodes, so a module exporting
65
+ only constants, types, or re-exports was not in it. Atos's `src/lib/db.ts`
66
+ exports a constant. The indexer already records every file it walked in the
67
+ `files` table — the authoritative list was stored all along.
68
+
69
+ - `bin/gsd-t-graph-query-cli.cjs`: resolve non-relative targets against the file
70
+ set (packages still pass through untouched, and an expanded alias is NOT
71
+ re-joined to the importer's directory); read the file set from the `files`
72
+ table, announcing the degradation on an older graph that lacks it.
73
+ - `test/m112-alias-query-resolution.test.js`: 7 regressions, all going through
74
+ the QUERY rather than the stored edge.
75
+
76
+ Every v5.11.26 test asserted the edge was STORED correctly. Not one asked
77
+ whether it could then be FOUND — the feature was tested at the write and called
78
+ done. These tests close that gap.
79
+
80
+ Requires `gsd-t graph index` only if the graph predates the `files` table;
81
+ otherwise the fix applies to existing indexes immediately.
82
+
5
83
  ## [5.11.27] - 2026-08-11
6
84
 
7
85
  ### Fixed — the scan's volume probe returned a stand-in, and the whole run was built on it
package/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # GSD-T: Contract-Driven Development for Claude Code
2
2
 
3
- **v5.11.27** - A methodology for reliable, parallelizable development using Claude Code with optional Agent Teams support.
3
+ **v5.11.29** - A methodology for reliable, parallelizable development using Claude Code with optional Agent Teams support.
4
4
 
5
5
  **Eliminates context rot** — task-level fresh dispatch (one subagent per task, ~10-20% context each) means compaction never triggers.
6
6
  **Compaction-proof debug loops** — `gsd-t headless --debug-loop` runs test-fix-retest cycles as separate `claude -p` sessions. A JSONL debug ledger persists all hypothesis/fix/learning history across fresh sessions. Anti-repetition preamble injection prevents retrying failed hypotheses. Escalation tiers (sonnet → opus → human) and a hard iteration ceiling enforced externally.
@@ -437,10 +437,68 @@ function loadSqliteStore(dbPath) {
437
437
  // TS/JS routinely omit the extension; the graph keys on real file ids.
438
438
  // Every indexed file appears as the `file` of its nodes (the indexer does not
439
439
  // emit dedicated FILE nodes), so collect the distinct file set from there.
440
- const fileIds = new Set(nodes.map((n) => norm(n.file)).filter(Boolean));
440
+ // [RULE] query-file-set-comes-from-files-table-not-inferred-from-functions
441
+ //
442
+ // hilo-figma-atos, 2026-08-11, the second half of the same miss. This set
443
+ // decides whether an extensionless import target can be matched to a real
444
+ // file. Built from node rows, it contains only files that declare a
445
+ // FUNCTION — so `src/lib/db.ts`, which exports a constant, was not in it,
446
+ // and no query could ever resolve an import of it. A file of constants,
447
+ // types, or re-exports is exactly the kind of shared module a whole codebase
448
+ // imports, and it was invisible.
449
+ //
450
+ // The indexer records every file it walked in the `files` table. That is the
451
+ // authoritative list; inferring one from functions was always an
452
+ // approximation of a fact already stored.
453
+ let fileIds;
454
+ const hasFilesTable = db
455
+ .prepare("SELECT name FROM sqlite_master WHERE type='table' AND name='files'")
456
+ .get();
457
+ if (hasFilesTable) {
458
+ fileIds = new Set(
459
+ db.prepare("SELECT file FROM files").all().map((r) => norm(r.file)).filter(Boolean)
460
+ );
461
+ } else {
462
+ // A graph built before the files table existed. Node-derived is what this
463
+ // has always done — announced, because it under-reports imports of any
464
+ // file that declares no function.
465
+ fileIds = new Set(nodes.map((n) => norm(n.file)).filter(Boolean));
466
+ process.stderr.write(
467
+ "[graph] this index predates the files table — imports of constant-only/type-only files may be missed; re-run `gsd-t graph index`\n"
468
+ );
469
+ }
441
470
  const EXTS = ["", ".ts", ".tsx", ".js", ".jsx", ".mjs", ".cjs", ".py", "/index.ts", "/index.tsx", "/index.js"];
471
+ // [RULE] query-resolves-expanded-alias-not-only-relative
472
+ //
473
+ // hilo-figma-atos, 2026-08-11: `who-imports src/lib/db.ts` returned 5 of 793
474
+ // real importers. The 788 missing ones all wrote `@/lib/db`.
475
+ //
476
+ // The two halves of the alias fix did not meet. The indexer (v5.11.26)
477
+ // expands `@/lib/db` to `src/lib/db` — correct, and stored. But this
478
+ // resolver, which is what appends the file extension, skipped it: the gate
479
+ // below returned early for anything not starting with ".", treating an
480
+ // expanded project path exactly like the package import `react`. So the edge
481
+ // sat in the database as `src/lib/db` while the query asked for
482
+ // `src/lib/db.ts`, and the two never met.
483
+ //
484
+ // A relative specifier is resolved against its source file's directory; an
485
+ // expanded alias is ALREADY project-relative and must not be — joining it to
486
+ // the importer's directory would produce `src/app/src/lib/db`. So the two
487
+ // take different routes to the same place: add the extension either way.
442
488
  const resolveDst = (srcFile, dst) => {
443
- if (typeof dst !== "string" || !dst.startsWith(".")) return dst; // package/external
489
+ if (typeof dst !== "string" || !dst) return dst;
490
+
491
+ if (!dst.startsWith(".")) {
492
+ // Not relative. Either a package ("react", "next/navigation") or an
493
+ // alias the indexer already expanded to a project path. The file set is
494
+ // what tells them apart — a package matches nothing in it.
495
+ const base = norm(dst);
496
+ for (const ext of EXTS) {
497
+ if (fileIds.has(base + ext)) return base + ext;
498
+ }
499
+ return dst; // genuinely external — keep it exactly as written
500
+ }
501
+
444
502
  const base = path.posix.normalize(path.posix.join(path.posix.dirname(norm(srcFile)), norm(dst)));
445
503
  // Prefer an actual indexed file id (try the bare path, then common extensions).
446
504
  for (const ext of EXTS) {
package/bin/gsd-t.js CHANGED
@@ -2917,10 +2917,129 @@ function createProjectChangelog(projectDir, projectName) {
2917
2917
  }
2918
2918
  }
2919
2919
 
2920
+ /**
2921
+ * Does this project have a built code graph, and does it have code worth one?
2922
+ *
2923
+ * [RULE] health-reports-missing-graph-never-assumes-built
2924
+ *
2925
+ * binvoice, 2026-08-11: a session reached for the graph, found nothing, and fell
2926
+ * back to grep — in an 827-file project. The tooling was installed and
2927
+ * propagated like everywhere else; the index had simply never been built.
2928
+ * Nothing builds it automatically, so a project has a graph only if someone once
2929
+ * ran `gsd-t graph index` there by hand.
2930
+ *
2931
+ * Nothing ever said so. `update-all` reported the project as current, because it
2932
+ * was — every file it ships was in place. The gap lived in state no propagation
2933
+ * step creates, and stayed invisible until a session hit it mid-task.
2934
+ *
2935
+ * Reported, never auto-built: indexing a large repo is slow, and doing it to 33
2936
+ * of them inside an update would be a surprise nobody asked for.
2937
+ */
2938
+ function graphState(projectDir) {
2939
+ // Where the store lives is the resolver's answer alone. If it cannot answer,
2940
+ // this check HALTS the caller rather than reporting a project as healthy — a
2941
+ // silent "graph fine" here is exactly the blindness being fixed.
2942
+ const resolver = require("./gsd-t-graph-store-resolver.cjs");
2943
+ const storePath = resolver.resolveStorePath(projectDir);
2944
+ if (storePath && fs.existsSync(storePath)) return { missing: false, files: 0 };
2945
+
2946
+ // A graph at the pre-M99 location (.gsd-t/graph.db) EXISTS — it is just where
2947
+ // the resolver no longer looks. Telling the user to build one would be wrong
2948
+ // twice over: the work is already done, and the real problem (a store the
2949
+ // tooling cannot find) would go unnamed.
2950
+ const legacyPath = resolver.resolveLegacyStorePath(projectDir);
2951
+ if (legacyPath && fs.existsSync(legacyPath)) return { missing: false, files: 0, legacy: true };
2952
+
2953
+ let n = 0;
2954
+ const SKIP = new Set(["node_modules", ".git", ".next", "dist", "build", ".venv", "venv", "out", "coverage"]);
2955
+ const EXT = new Set([".ts", ".tsx", ".js", ".jsx", ".mjs", ".cjs", ".py"]);
2956
+ const walk = (dir, depth) => {
2957
+ if (depth > 6) return;
2958
+ if (n > 400) return; // enough to answer "is this a real codebase?"
2959
+ let entries;
2960
+ try {
2961
+ entries = fs.readdirSync(dir, { withFileTypes: true });
2962
+ } catch (_e) {
2963
+ // An unreadable subdirectory makes the count a floor, not a lie: the
2964
+ // result is only ever compared against a low threshold, and a miscount
2965
+ // downward can only omit a project from a report, never invent one.
2966
+ return;
2967
+ }
2968
+ for (const e of entries) {
2969
+ if (e.isDirectory()) {
2970
+ if (SKIP.has(e.name)) continue;
2971
+ if (e.name.startsWith(".")) continue;
2972
+ walk(path.join(dir, e.name), depth + 1);
2973
+ continue;
2974
+ }
2975
+ if (EXT.has(path.extname(e.name))) n++;
2976
+ }
2977
+ };
2978
+ walk(projectDir, 0);
2979
+
2980
+ // A project with almost no source has nothing for a graph to map; calling that
2981
+ // "missing" would be noise on every docs-only or config-only repo.
2982
+ return { missing: n >= 25, files: n };
2983
+ }
2984
+
2985
+ /**
2986
+ * Move a graph that sits at the pre-M99 path into the one the tooling reads.
2987
+ *
2988
+ * The store already holds a full index — M99 changed where every tool LOOKS
2989
+ * without moving what was there, so the graph was present and unreachable. The
2990
+ * resolver's own migration does the move, checkpoints the write-ahead log, and
2991
+ * verifies the result is readable.
2992
+ */
2993
+ function migrateLegacyGraph(projectDir) {
2994
+ try {
2995
+ const r = require("./gsd-t-graph-store-resolver.cjs").migrateGraphStore(projectDir);
2996
+ if (r && r.migrated) return { ok: true };
2997
+ return { ok: false, err: (r && r.reason) || "migration reported no result" };
2998
+ } catch (e) {
2999
+ return { ok: false, err: e.message || String(e) };
3000
+ }
3001
+ }
3002
+
3003
+ /**
3004
+ * Build a project's code graph from scratch.
3005
+ *
3006
+ * Slow on a large repo, so the caller announces it before this runs. A failure
3007
+ * is returned, never swallowed: a project left without a graph must say so, so
3008
+ * the next session halts instead of quietly grepping.
3009
+ */
3010
+ function buildGraph(projectDir) {
3011
+ try {
3012
+ const idx = path.join(__dirname, "gsd-t-graph-index.cjs");
3013
+ const r = require("child_process").spawnSync(process.execPath, [idx], {
3014
+ cwd: projectDir,
3015
+ encoding: "utf8",
3016
+ timeout: 15 * 60 * 1000,
3017
+ stdio: ["ignore", "pipe", "pipe"],
3018
+ });
3019
+ if (r.error) return { ok: false, err: r.error.message };
3020
+ if (r.status !== 0) {
3021
+ const tail = String(r.stderr || r.stdout || "").trim().split("\n").slice(-2).join(" ");
3022
+ return { ok: false, err: `exit ${r.status}${tail ? ` — ${tail}` : ""}` };
3023
+ }
3024
+ // The indexer reporting success is not proof a store landed. Verify the file
3025
+ // the tooling will actually read.
3026
+ const storePath = require("./gsd-t-graph-store-resolver.cjs").resolveStorePath(projectDir);
3027
+ if (!fs.existsSync(storePath)) return { ok: false, err: "indexer reported success but no store was written" };
3028
+ return { ok: true };
3029
+ } catch (e) {
3030
+ return { ok: false, err: e.message || String(e) };
3031
+ }
3032
+ }
3033
+
2920
3034
  async function checkProjectHealth(projects) {
2921
3035
  heading("Project Health");
2922
3036
  const playwrightMissing = [];
2923
3037
  const swaggerMissing = [];
3038
+ const graphMissing = [];
3039
+ const graphLegacy = [];
3040
+ const graphMigrated = [];
3041
+ const graphBuilt = [];
3042
+ const graphRepairFailed = [];
2924
3043
  const playwrightAutoInstalled = [];
2925
3044
  const playwrightInstallFailed = [];
2926
3045
 
@@ -2929,6 +3048,42 @@ async function checkProjectHealth(projects) {
2929
3048
  const name = path.basename(projectDir);
2930
3049
  if (!hasPlaywright(projectDir)) playwrightMissing.push(name);
2931
3050
  if (hasApi(projectDir) && !hasSwagger(projectDir)) swaggerMissing.push(name);
3051
+ const g = graphState(projectDir);
3052
+ if (g.missing) graphMissing.push({ name, dir: projectDir, files: g.files });
3053
+ if (g.legacy) graphLegacy.push({ name, dir: projectDir });
3054
+ }
3055
+
3056
+ // [RULE] graph-missing-is-built-not-reported
3057
+ //
3058
+ // A missing graph is not a status to report — it is work to do. Every session
3059
+ // in a project without one silently falls back to grep, which reads a fraction
3060
+ // of what the graph knows and answers a different question. Reporting it and
3061
+ // moving on leaves that in place until a human notices, which for binvoice
3062
+ // (827 files) meant months.
3063
+ //
3064
+ // Two different faults, two different repairs:
3065
+ //
3066
+ // · at the old path — the store EXISTS and holds a full index. M99 changed
3067
+ // where the tooling looks without moving what was there, so 18 projects
3068
+ // were reading past a perfectly good graph. This is a file move plus a
3069
+ // write-ahead-log checkpoint, and takes no time at all.
3070
+ // · never built — there is nothing to move. Indexing is genuinely slow
3071
+ // on a large repo, so this is announced before it runs rather than
3072
+ // appearing as an unexplained pause.
3073
+ //
3074
+ // A repair that fails is REPORTED and the project is left as it was. It is not
3075
+ // retried differently and never half-applied: a graph that came up short would
3076
+ // answer confidently with partial data, which is worse than not having one.
3077
+ for (const { name, dir } of graphLegacy) {
3078
+ const r = migrateLegacyGraph(dir);
3079
+ if (r.ok) graphMigrated.push(name);
3080
+ else graphRepairFailed.push({ name, what: "move", err: r.err });
3081
+ }
3082
+ for (const { name, dir, files } of graphMissing) {
3083
+ log(` building code graph for ${name} (${files}+ source files) — first build, this takes a while`);
3084
+ const r = buildGraph(dir);
3085
+ if (r.ok) graphBuilt.push(name);
3086
+ else graphRepairFailed.push({ name, what: "build", err: r.err });
2932
3087
  }
2933
3088
 
2934
3089
  // M50 D1: auto-install Playwright for any UI project that's missing it.
@@ -2947,6 +3102,19 @@ async function checkProjectHealth(projects) {
2947
3102
  }
2948
3103
  }
2949
3104
 
3105
+ if (graphMigrated.length > 0) {
3106
+ success(`Code graph moved to where the tooling reads it: ${graphMigrated.join(", ")}`);
3107
+ }
3108
+ if (graphBuilt.length > 0) {
3109
+ success(`Code graph built: ${graphBuilt.join(", ")}`);
3110
+ }
3111
+ if (graphRepairFailed.length > 0) {
3112
+ for (const f of graphRepairFailed) {
3113
+ warn(` ${f.name} — graph ${f.what} FAILED: ${f.err}`);
3114
+ }
3115
+ info("Those projects have no usable graph. Fix before working in them — a session there cannot answer structural questions.");
3116
+ }
3117
+
2950
3118
  if (playwrightMissing.length === 0 && swaggerMissing.length === 0) {
2951
3119
  success("All projects have Playwright and Swagger configured");
2952
3120
  } else {
@@ -2978,6 +3146,11 @@ async function checkProjectHealth(projects) {
2978
3146
  return {
2979
3147
  playwrightMissing,
2980
3148
  swaggerMissing,
3149
+ graphMissing,
3150
+ graphLegacy,
3151
+ graphMigrated,
3152
+ graphBuilt,
3153
+ graphRepairFailed,
2981
3154
  playwrightAutoInstalled,
2982
3155
  playwrightInstallFailed,
2983
3156
  };
@@ -3155,6 +3328,7 @@ async function doUpdateAll() {
3155
3328
  playwrightMissing,
3156
3329
  swaggerMissing,
3157
3330
  playwrightAutoInstalled,
3331
+ graphMissing,
3158
3332
  } = await checkProjectHealth(projects);
3159
3333
  showUpdateAllSummary(
3160
3334
  projects.length,
@@ -3163,6 +3337,7 @@ async function doUpdateAll() {
3163
3337
  swaggerMissing,
3164
3338
  syncCount,
3165
3339
  playwrightAutoInstalled,
3340
+ graphMissing,
3166
3341
  );
3167
3342
  }
3168
3343
 
@@ -3693,6 +3868,7 @@ function showUpdateAllSummary(
3693
3868
  swaggerMissing,
3694
3869
  syncCount,
3695
3870
  playwrightAutoInstalled,
3871
+ graphMissing,
3696
3872
  ) {
3697
3873
  log("");
3698
3874
  heading("Update All Complete");
@@ -3706,6 +3882,9 @@ function showUpdateAllSummary(
3706
3882
  log(` Auto-installed Playwright in: ${playwrightAutoInstalled.length} project(s)`);
3707
3883
  }
3708
3884
  if (swaggerMissing.length > 0) log(` Missing Swagger: ${swaggerMissing.length}`);
3885
+ if (Array.isArray(graphMissing) && graphMissing.length > 0) {
3886
+ log(` No code graph: ${graphMissing.length}`);
3887
+ }
3709
3888
  if (syncCount > 0) log(` Global rules synced: ${syncCount}`);
3710
3889
  log("");
3711
3890
  }
@@ -5336,6 +5515,7 @@ function showHelp() {
5336
5515
  // ─── Exports (for testing) ───────────────────────────────────────────────────
5337
5516
 
5338
5517
  module.exports = {
5518
+ graphState,
5339
5519
  validateProjectName,
5340
5520
  applyTokens,
5341
5521
  normalizeEol,
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@tekyzinc/gsd-t",
3
- "version": "5.11.27",
4
- "description": "GSD-T: Contract-Driven Development for Claude Code 54 slash commands with headless-by-default workflow spawning, unattended supervisor relay with event stream, graph-powered code analysis, real-time agent dashboard, task telemetry, doc-ripple enforcement, backlog management, impact analysis, test sync, milestone archival, and PRD generation",
3
+ "version": "5.11.29",
4
+ "description": "GSD-T: Contract-Driven Development for Claude Code \u2014 54 slash commands with headless-by-default workflow spawning, unattended supervisor relay with event stream, graph-powered code analysis, real-time agent dashboard, task telemetry, doc-ripple enforcement, backlog management, impact analysis, test sync, milestone archival, and PRD generation",
5
5
  "author": "Tekyz, Inc.",
6
6
  "license": "MIT",
7
7
  "repository": {
@@ -310,6 +310,25 @@ When any GSD-T command creates or modifies an API endpoint:
310
310
 
311
311
  This applies during: `gsd-t-execute`, `gsd-t-quick`, `gsd-t-integrate`, `gsd-t-wave`, and any command that touches API code.
312
312
 
313
+ ## Code Graph — build it, never grep around it (MANDATORY)
314
+
315
+ **Read code structure through the graph. If the graph is missing, BUILD it. If it is out of date, UPDATE it. Grep only where the answer cannot be indexed.**
316
+
317
+ ```
318
+ NEED A STRUCTURAL ANSWER? (what imports this, who calls this, what breaks if I change it)
319
+ ├── Graph present and fresh? → query it
320
+ ├── Graph missing? → `gsd-t graph index` — BUILD IT, then query
321
+ ├── Graph stale? → re-index the touched set, then query
322
+ ├── Graph at the old path? → it EXISTS; move it (`gsd-t graph index`), then query
323
+ └── Build/repair FAILED? → HALT and say so. Never answer the structural question by grep.
324
+ ```
325
+
326
+ **Grep is correct ONLY where the content cannot be indexed** — `.md`, `.sql`, `.json`, `.sh`, config, prose, comments. For anything about code structure, grep is not a weaker answer, it is a **different and wrong one**: it matches text, the question is about relationships.
327
+
328
+ **This governs plain conversational work, not just `/gsd-t-*` commands.** The failure that produced this rule (binvoice, 2026-08-11) was an ordinary session: it reached for the graph, found none, grepped an 827-file project, and nothing objected. Checking the graph's existence before reasoning about code is the first move, not a fallback.
329
+
330
+ **Absence is a repairable condition, not a stop sign.** When this was checked across the machine, 20 of 27 registered projects had no usable graph — 2 never built, 18 holding a real index at a path the tooling stopped reading after the store moved. Every one of those sessions had been grepping. `gsd-t update-all` now repairs both automatically and reports any it could not.
331
+
313
332
  ## Prime Rule
314
333
  KEEP GOING. Only stop for:
315
334
  1. Unrecoverable errors after 2 fix attempts (delegate to `gsd-t headless --debug-loop` first — only stop if exit code 4)