@toolbaux/guardian 0.1.22 → 0.2.0

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.
Files changed (37) hide show
  1. package/README.md +6 -4
  2. package/dist/adapters/runner.js +72 -3
  3. package/dist/adapters/typescript-adapter.js +24 -10
  4. package/dist/benchmarking/metrics/context-coverage.js +82 -0
  5. package/dist/benchmarking/metrics/drift-score.js +104 -0
  6. package/dist/benchmarking/metrics/search-recall.js +207 -0
  7. package/dist/benchmarking/metrics/token-efficiency.js +79 -0
  8. package/dist/benchmarking/report.js +131 -0
  9. package/dist/benchmarking/runner.js +175 -0
  10. package/dist/benchmarking/types.js +13 -0
  11. package/dist/cli.js +53 -10
  12. package/dist/commands/benchmark.js +62 -0
  13. package/dist/commands/context.js +87 -29
  14. package/dist/commands/discrepancy.js +1 -1
  15. package/dist/commands/doc-generate.js +1 -1
  16. package/dist/commands/doc-html.js +1 -1
  17. package/dist/commands/extract.js +4 -1
  18. package/dist/commands/feature-context.js +1 -1
  19. package/dist/commands/generate.js +83 -10
  20. package/dist/commands/init.js +89 -56
  21. package/dist/commands/intel.js +70 -1
  22. package/dist/commands/mcp-serve.js +155 -316
  23. package/dist/commands/search.js +642 -14
  24. package/dist/config.js +1 -0
  25. package/dist/db/embeddings.js +113 -0
  26. package/dist/db/file-specs-store.js +174 -0
  27. package/dist/db/fts-builder.js +390 -0
  28. package/dist/db/index.js +55 -0
  29. package/dist/db/specs-store.js +13 -0
  30. package/dist/db/sqlite-specs-store.js +934 -0
  31. package/dist/extract/codebase-intel.js +31 -2
  32. package/dist/extract/compress.js +70 -3
  33. package/dist/extract/context-block.js +11 -2
  34. package/dist/extract/function-intel.js +5 -2
  35. package/dist/extract/index.js +1 -23
  36. package/dist/extract/writer.js +6 -0
  37. package/package.json +4 -1
@@ -5,21 +5,90 @@ import { loadArchitectureDiff, loadHeatmap } from "../extract/compress.js";
5
5
  import { renderContextBlock } from "../extract/context-block.js";
6
6
  import { resolveMachineInputDir } from "../output-layout.js";
7
7
  import { DEFAULT_SPECS_DIR } from "../config.js";
8
+ import { SqliteSpecsStore, DB_FILENAME } from "../db/sqlite-specs-store.js";
9
+ /** Open a SqliteSpecsStore if guardian.db exists, return null otherwise. */
10
+ async function tryOpenStore(specsDir) {
11
+ const dbPath = path.join(specsDir, DB_FILENAME);
12
+ try {
13
+ await fs.stat(dbPath);
14
+ const store = new SqliteSpecsStore(specsDir);
15
+ await store.init();
16
+ return store;
17
+ }
18
+ catch {
19
+ return null;
20
+ }
21
+ }
22
+ /** Reconstruct the SI report shape renderContextBlock needs from module_metrics rows. */
23
+ function siFromMetrics(rows) {
24
+ return rows.map(r => ({
25
+ feature: r.module,
26
+ structure: { nodes: r.nodes, edges: r.edges },
27
+ metrics: { depth: 0, fanout_avg: 0, fanout_max: 0, density: 0, has_cycles: false },
28
+ scores: { depth_score: 0, fanout_score: 0, density_score: 0, cycle_score: 0, query_score: 0 },
29
+ confidence: { value: r.confidence, level: r.confidence_level },
30
+ ambiguity: { level: "LOW" },
31
+ classification: {
32
+ depth_level: r.depth_level,
33
+ propagation: r.propagation,
34
+ compressible: r.compressible,
35
+ },
36
+ recommendation: {
37
+ primary: { pattern: r.pattern, confidence: r.confidence },
38
+ fallback: { pattern: "", condition: "" },
39
+ avoid: [],
40
+ },
41
+ guardrails: { enforce_if_confidence_above: 0.7 },
42
+ override: { allowed: true, requires_reason: true },
43
+ }));
44
+ }
8
45
  export async function runContext(options) {
9
46
  const inputDir = await resolveMachineInputDir(options.input || DEFAULT_SPECS_DIR);
10
- const { architecture, ux } = await loadSnapshots(inputDir);
47
+ // inputDir resolves to .specs/machine/; DB lives one level up at .specs/guardian.db
48
+ const specsDir = path.dirname(inputDir);
49
+ const store = await tryOpenStore(specsDir);
50
+ let architecture;
51
+ let ux;
52
+ let si;
53
+ try {
54
+ // ── Load snapshots: DB first, file fallback ─────────────────────────────
55
+ if (store) {
56
+ const archEntry = await store.readSpec("architecture.snapshot");
57
+ const uxEntry = await store.readSpec("ux.snapshot");
58
+ if (archEntry && uxEntry) {
59
+ architecture = yaml.load(archEntry.content);
60
+ ux = yaml.load(uxEntry.content);
61
+ }
62
+ else {
63
+ ({ architecture, ux } = await loadSnapshotsFromFiles(inputDir));
64
+ }
65
+ }
66
+ else {
67
+ ({ architecture, ux } = await loadSnapshotsFromFiles(inputDir));
68
+ }
69
+ // ── Load SI reports: module_metrics table first, file fallback ──────────
70
+ if (store) {
71
+ const rows = store.readModuleMetrics();
72
+ if (rows.length > 0) {
73
+ si = siFromMetrics(rows);
74
+ }
75
+ }
76
+ if (!si) {
77
+ try {
78
+ const siRaw = await fs.readFile(path.join(inputDir, "structural-intelligence.json"), "utf8");
79
+ si = JSON.parse(siRaw);
80
+ }
81
+ catch { /* not available */ }
82
+ }
83
+ }
84
+ finally {
85
+ if (store)
86
+ await store.close();
87
+ }
11
88
  const [diff, heatmap] = await Promise.all([
12
89
  loadArchitectureDiff(inputDir),
13
90
  loadHeatmap(inputDir)
14
91
  ]);
15
- // Load structural intelligence if available
16
- let si;
17
- try {
18
- const siPath = path.join(inputDir, "structural-intelligence.json");
19
- const siRaw = await fs.readFile(siPath, "utf8");
20
- si = JSON.parse(siRaw);
21
- }
22
- catch { /* not available */ }
23
92
  const content = renderContextBlock(architecture, ux, {
24
93
  focusQuery: options.focus,
25
94
  maxLines: normalizeMaxLines(options.maxLines),
@@ -38,16 +107,18 @@ export async function runContext(options) {
38
107
  await fs.writeFile(outputPath, next, "utf8");
39
108
  console.log(`Wrote ${outputPath}`);
40
109
  }
41
- async function loadSnapshots(inputDir) {
110
+ async function loadSnapshotsFromFiles(inputDir) {
42
111
  const architecturePath = path.join(inputDir, "architecture.snapshot.yaml");
43
112
  const uxPath = path.join(inputDir, "ux.snapshot.yaml");
44
- let architectureRaw;
45
- let uxRaw;
46
113
  try {
47
- [architectureRaw, uxRaw] = await Promise.all([
114
+ const [architectureRaw, uxRaw] = await Promise.all([
48
115
  fs.readFile(architecturePath, "utf8"),
49
116
  fs.readFile(uxPath, "utf8")
50
117
  ]);
118
+ return {
119
+ architecture: yaml.load(architectureRaw),
120
+ ux: yaml.load(uxRaw)
121
+ };
51
122
  }
52
123
  catch (error) {
53
124
  if (error.code === "ENOENT") {
@@ -55,10 +126,6 @@ async function loadSnapshots(inputDir) {
55
126
  }
56
127
  throw error;
57
128
  }
58
- return {
59
- architecture: yaml.load(architectureRaw),
60
- ux: yaml.load(uxRaw)
61
- };
62
129
  }
63
130
  async function readIfExists(filePath) {
64
131
  try {
@@ -75,37 +142,28 @@ function stripExistingSpecGuardBlocks(content) {
75
142
  .replace(/<!-- guardian:auto-context -->[\s\S]*?<!-- \/guardian:auto-context -->/g, "<!-- guardian:auto-context -->\n<!-- /guardian:auto-context -->")
76
143
  .replace(/\n{3,}/g, "\n\n");
77
144
  }
78
- /**
79
- * Inject context into a file that has <!-- guardian:auto-context --> markers.
80
- * Replaces content between the markers instead of appending.
81
- */
82
145
  function injectIntoAutoContext(existing, contextBlock) {
83
146
  const marker = "<!-- guardian:auto-context -->";
84
147
  const endMarker = "<!-- /guardian:auto-context -->";
85
148
  if (!existing.includes(marker)) {
86
- // No auto-context markers — fall back to append behavior
87
149
  const cleaned = stripExistingSpecGuardBlocks(existing).trim();
88
150
  return cleaned.length > 0 ? `${cleaned}\n\n${contextBlock}\n` : `${contextBlock}\n`;
89
151
  }
90
- // Replace content between markers
91
152
  const startIdx = existing.indexOf(marker);
92
153
  const endIdx = existing.indexOf(endMarker);
93
- if (startIdx === -1 || endIdx === -1) {
154
+ if (startIdx === -1 || endIdx === -1)
94
155
  return existing;
95
- }
96
156
  const before = existing.slice(0, startIdx + marker.length);
97
157
  const after = existing.slice(endIdx);
98
158
  return `${before}\n${contextBlock}\n${after}`;
99
159
  }
100
160
  function normalizeMaxLines(value) {
101
- if (typeof value === "number" && Number.isFinite(value)) {
161
+ if (typeof value === "number" && Number.isFinite(value))
102
162
  return value;
103
- }
104
163
  if (typeof value === "string" && value.trim().length > 0) {
105
164
  const parsed = Number.parseInt(value, 10);
106
- if (Number.isFinite(parsed) && parsed > 0) {
165
+ if (Number.isFinite(parsed) && parsed > 0)
107
166
  return parsed;
108
- }
109
167
  }
110
168
  return undefined;
111
169
  }
@@ -22,7 +22,7 @@ export async function runDiscrepancy(options) {
22
22
  // Load codebase intelligence
23
23
  const intelPath = path.join(layout.machineDir, "codebase-intelligence.json");
24
24
  const intel = await loadCodebaseIntelligence(intelPath).catch(() => {
25
- throw new Error(`Could not load codebase-intelligence.json from ${intelPath}. Run \`guardian intel --specs ${options.specs}\` first.`);
25
+ throw new Error(`Could not load codebase-intelligence.json from ${intelPath}. Run \`guardian extract --output ${options.specs}\` first.`);
26
26
  });
27
27
  const baselinePath = path.join(layout.machineDir, "product-document.baseline.json");
28
28
  const featureSpecsDir = options.featureSpecs ? path.resolve(options.featureSpecs) : null;
@@ -46,7 +46,7 @@ export async function runDocGenerate(options) {
46
46
  process.stdout.write("Loading codebase intelligence... ");
47
47
  const intel = await loadCodebaseIntelligence(intelPath).catch(() => {
48
48
  console.log("failed");
49
- throw new Error(`Could not load ${intelPath}. Run \`guardian intel --specs ${options.specs}\` first.`);
49
+ throw new Error(`Could not load ${intelPath}. Run \`guardian extract --output ${options.specs}\` first.`);
50
50
  });
51
51
  console.log(`${intel.meta.counts.endpoints} endpoints, ${intel.meta.counts.models} models, ` +
52
52
  `${intel.meta.counts.enums} enums, ${intel.meta.counts.tasks} tasks`);
@@ -23,7 +23,7 @@ export async function runDocHtml(options) {
23
23
  process.stdout.write("Loading codebase intelligence... ");
24
24
  const intel = await loadCodebaseIntelligence(intelPath).catch(() => {
25
25
  console.log("failed");
26
- throw new Error(`Could not load ${intelPath}. Run \`guardian intel --specs ${options.specs}\` first.`);
26
+ throw new Error(`Could not load ${intelPath}. Run \`guardian extract --output ${options.specs}\` first.`);
27
27
  });
28
28
  console.log(`${intel.meta.counts.endpoints} endpoints, ${intel.meta.counts.models} models`);
29
29
  // ── Feature arcs (optional) ───────────────────────────────────────────────
@@ -5,13 +5,16 @@ import { runIntel } from "./intel.js";
5
5
  import { runGenerate } from "./generate.js";
6
6
  import { runContext } from "./context.js";
7
7
  export async function runExtract(options) {
8
+ // Default to sqlite so every extract builds guardian.db automatically.
9
+ // Pass --backend file to opt out (e.g. CI environments that don't need search).
10
+ const backend = options.backend ?? "sqlite";
8
11
  const { architecturePath, uxPath } = await extractProject(options);
9
12
  console.log(`Wrote ${architecturePath}`);
10
13
  console.log(`Wrote ${uxPath}`);
11
14
  // Auto-build codebase intelligence after every extract
12
15
  const specsDir = path.resolve(options.output);
13
16
  try {
14
- await runIntel({ specs: specsDir });
17
+ await runIntel({ specs: specsDir, backend });
15
18
  }
16
19
  catch {
17
20
  // Non-fatal — intel build failure should not break extract
@@ -26,7 +26,7 @@ export async function runFeatureContext(options) {
26
26
  // Load codebase intelligence
27
27
  const intelPath = path.join(layout.machineDir, "codebase-intelligence.json");
28
28
  const intel = await loadCodebaseIntelligence(intelPath).catch(() => {
29
- throw new Error(`Could not load codebase-intelligence.json from ${intelPath}. Run \`guardian intel --specs ${options.specs}\` first.`);
29
+ throw new Error(`Could not load codebase-intelligence.json from ${intelPath}. Run \`guardian extract --output ${options.specs}\` first.`);
30
30
  });
31
31
  // Build filtered context
32
32
  const context = buildFeatureContext(spec, intel);
@@ -1,26 +1,99 @@
1
1
  import fs from "node:fs/promises";
2
2
  import path from "node:path";
3
+ import yaml from "js-yaml";
3
4
  import { buildSnapshots } from "../extract/index.js";
4
5
  import { renderContextBlock } from "../extract/context-block.js";
5
6
  import { getOutputLayout } from "../output-layout.js";
6
7
  import { DEFAULT_SPECS_DIR } from "../config.js";
7
8
  import { analyzeDepth } from "../extract/analyzers/depth.js";
9
+ import { SqliteSpecsStore, DB_FILENAME } from "../db/sqlite-specs-store.js";
8
10
  export async function runGenerate(options) {
9
11
  if (!options.aiContext) {
10
12
  throw new Error("`guardian generate` currently supports `--ai-context` only.");
11
13
  }
12
14
  const outputRoot = path.resolve(options.output ?? DEFAULT_SPECS_DIR);
13
15
  const layout = getOutputLayout(outputRoot);
14
- const { architecture, ux } = await buildSnapshots({
15
- projectRoot: options.projectRoot,
16
- backendRoot: options.backendRoot,
17
- frontendRoot: options.frontendRoot,
18
- output: outputRoot,
19
- includeFileGraph: true,
20
- configPath: options.configPath
21
- });
22
- // Load persisted Structural Intelligence reports emitted by `guardian extract`
23
- const siReports = await loadStructuralIntelligenceReports(layout.machineDir);
16
+ // ── Load snapshots: DB first, full re-extraction as fallback ──────────────
17
+ // When guardian.db exists (built by extract), load snapshots from the specs
18
+ // table instead of re-analysing the whole codebase. This is ~10× faster.
19
+ let architecture;
20
+ let ux;
21
+ let siReports;
22
+ const dbPath = path.join(outputRoot, DB_FILENAME);
23
+ let store = null;
24
+ try {
25
+ await fs.stat(dbPath);
26
+ store = new SqliteSpecsStore(outputRoot);
27
+ await store.init();
28
+ }
29
+ catch {
30
+ store = null;
31
+ }
32
+ try {
33
+ if (store) {
34
+ const archEntry = await store.readSpec("architecture.snapshot");
35
+ const uxEntry = await store.readSpec("ux.snapshot");
36
+ if (archEntry && uxEntry) {
37
+ console.log("[guardian] Loading snapshots from guardian.db");
38
+ architecture = yaml.load(archEntry.content);
39
+ ux = yaml.load(uxEntry.content);
40
+ }
41
+ else {
42
+ console.log("[guardian] Snapshots not in DB — extracting from codebase");
43
+ ({ architecture, ux } = await buildSnapshots({
44
+ projectRoot: options.projectRoot,
45
+ backendRoot: options.backendRoot,
46
+ frontendRoot: options.frontendRoot,
47
+ output: outputRoot,
48
+ includeFileGraph: true,
49
+ configPath: options.configPath
50
+ }));
51
+ }
52
+ // SI from module_metrics, fall back to file
53
+ const metricRows = store.readModuleMetrics();
54
+ if (metricRows.length > 0) {
55
+ siReports = metricRows.map(r => ({
56
+ feature: r.module,
57
+ structure: { nodes: r.nodes, edges: r.edges },
58
+ metrics: { depth: 0, fanout_avg: 0, fanout_max: 0, density: 0, has_cycles: false },
59
+ scores: { depth_score: 0, fanout_score: 0, density_score: 0, cycle_score: 0, query_score: 0 },
60
+ confidence: { value: r.confidence, level: r.confidence_level },
61
+ ambiguity: { level: "LOW" },
62
+ classification: {
63
+ depth_level: r.depth_level,
64
+ propagation: r.propagation,
65
+ compressible: r.compressible,
66
+ },
67
+ recommendation: {
68
+ primary: { pattern: r.pattern, confidence: r.confidence },
69
+ fallback: { pattern: "", condition: "" },
70
+ avoid: [],
71
+ },
72
+ guardrails: { enforce_if_confidence_above: 0.7 },
73
+ override: { allowed: true, requires_reason: true },
74
+ }));
75
+ }
76
+ else {
77
+ siReports = await loadStructuralIntelligenceReports(layout.machineDir);
78
+ }
79
+ }
80
+ else {
81
+ console.log("[guardian] No guardian.db found — extracting from codebase");
82
+ ({ architecture, ux } = await buildSnapshots({
83
+ projectRoot: options.projectRoot,
84
+ backendRoot: options.backendRoot,
85
+ frontendRoot: options.frontendRoot,
86
+ output: outputRoot,
87
+ includeFileGraph: true,
88
+ configPath: options.configPath
89
+ }));
90
+ siReports = await loadStructuralIntelligenceReports(layout.machineDir);
91
+ }
92
+ }
93
+ finally {
94
+ if (store)
95
+ await store.close();
96
+ }
24
97
  // If a --focus query is provided, prepend a real-time SI report for that query
25
98
  if (options.focus) {
26
99
  try {
@@ -13,32 +13,62 @@
13
13
  */
14
14
  import fs from "node:fs/promises";
15
15
  import path from "node:path";
16
+ import { randomUUID } from "node:crypto";
16
17
  import { DEFAULT_SPECS_DIR } from "../config.js";
17
18
  const DEFAULT_CONFIG = {
18
19
  docs: {
19
20
  mode: "full",
20
21
  },
21
22
  };
23
+ /**
24
+ * Hook script written to .claude/hooks/mcp-first.sh
25
+ *
26
+ * Blocks Read/Glob/Grep until a guardian MCP tool has been called in the
27
+ * current session. Session state lives in /tmp/guardian-used-<SESSION_ID>
28
+ * and is set by the PostToolUse hook (guardian-used.sh) below.
29
+ */
22
30
  const CLAUDE_CODE_HOOK_SCRIPT = `#!/bin/bash
23
- # Guardian MCP-first hook — ensures AI tools use Guardian MCP before reading source files.
24
- # Installed by: guardian init
31
+ # Guardian MCP-first hook — blocks Read/Glob/Grep until guardian tools are used.
32
+ # Installed by: guardian init (v3 — session-scoped flag, no time drift)
25
33
 
26
34
  INPUT=$(cat)
27
- TOOL_NAME=$(echo "$INPUT" | jq -r '.tool_name // empty')
35
+ SESSION_ID=$(echo "$INPUT" | jq -r '.session_id // "default"')
36
+ FLAG="/tmp/guardian-used-\${SESSION_ID}"
28
37
 
29
- cat >&2 <<BLOCK
30
- BLOCKED: Use Guardian MCP tools before reading source files.
38
+ # If guardian was called in this session, allow all file operations.
39
+ if [ -f "$FLAG" ]; then
40
+ exit 0
41
+ fi
42
+
43
+ cat >&2 <<'BLOCK'
44
+ BLOCKED: Use Guardian MCP tools before exploring source files.
31
45
 
32
- Use these MCP tools first:
33
- - guardian_orientget codebase overview
34
- - guardian_search find features by keyword
35
- - guardian_context deep dive into a specific area
46
+ Call one of these first:
47
+ guardian_search("your query")find files/symbols/endpoints by keyword
48
+ guardian_grep("pattern") semantic grep (replaces Grep tool)
49
+ guardian_glob("src/auth/**") semantic file discovery (replaces Glob tool)
50
+ guardian_orient() — get codebase overview
36
51
 
37
- Then you can read individual files as needed.
52
+ File reads are unblocked automatically for the rest of this session.
38
53
  BLOCK
39
54
 
40
55
  exit 2
41
56
  `;
57
+ /**
58
+ * Hook script written to .claude/hooks/guardian-used.sh
59
+ *
60
+ * Called by PostToolUse after any guardian_* tool. Sets the session flag
61
+ * that mcp-first.sh checks, unblocking subsequent Read/Glob/Grep calls.
62
+ */
63
+ const GUARDIAN_USED_SCRIPT = `#!/bin/bash
64
+ # Guardian PostToolUse hook — marks guardian as used for this session.
65
+ # Installed by: guardian init
66
+
67
+ INPUT=$(cat)
68
+ SESSION_ID=$(echo "$INPUT" | jq -r '.session_id // "default"')
69
+ touch "/tmp/guardian-used-\${SESSION_ID}"
70
+ exit 0
71
+ `;
42
72
  const HOOK_SCRIPT = `#!/bin/sh
43
73
  # guardian pre-commit hook — keeps architecture context fresh
44
74
  # Installed by: guardian init
@@ -80,6 +110,22 @@ export async function runInit(options) {
80
110
  else {
81
111
  console.log(" · guardian.config.json already exists");
82
112
  }
113
+ // 2b. Ensure project_id is present in guardian.config.json
114
+ try {
115
+ const raw = await fs.readFile(configPath, "utf8");
116
+ const cfg = JSON.parse(raw);
117
+ if (!cfg.project_id) {
118
+ cfg.project_id = randomUUID();
119
+ await fs.writeFile(configPath, JSON.stringify(cfg, null, 2) + "\n", "utf8");
120
+ console.log(" ✓ Added project_id to guardian.config.json");
121
+ }
122
+ else {
123
+ console.log(" · guardian.config.json already has project_id");
124
+ }
125
+ }
126
+ catch {
127
+ // Non-fatal — config may not be valid JSON yet
128
+ }
83
129
  // 3. Install pre-commit hook
84
130
  if (!options.skipHook) {
85
131
  const gitDir = path.join(root, ".git");
@@ -162,6 +208,7 @@ export async function runInit(options) {
162
208
  frontendRoot: options.frontendRoot,
163
209
  output: specsDir,
164
210
  includeFileGraph: true,
211
+ backend: options.backend,
165
212
  });
166
213
  const { runGenerate } = await import("./generate.js");
167
214
  await runGenerate({
@@ -212,18 +259,13 @@ async function setupClaudeCodeHooks(root, specsDir) {
212
259
  try {
213
260
  mcpConfig = JSON.parse(await fs.readFile(mcpJsonPath, "utf8"));
214
261
  }
215
- catch {
216
- // Corrupted — overwrite
217
- }
262
+ catch { /* corrupted — overwrite */ }
218
263
  }
219
264
  if (!mcpConfig.mcpServers)
220
265
  mcpConfig.mcpServers = {};
221
266
  const servers = mcpConfig.mcpServers;
222
267
  if (!servers.guardian) {
223
- servers.guardian = {
224
- command: "guardian",
225
- args: ["mcp-serve", "--specs", specsDir],
226
- };
268
+ servers.guardian = { command: "guardian", args: ["mcp-serve", "--specs", specsDir] };
227
269
  await fs.writeFile(mcpJsonPath, JSON.stringify(mcpConfig, null, 2) + "\n", "utf8");
228
270
  console.log(" ✓ Created .mcp.json (MCP server config)");
229
271
  }
@@ -237,58 +279,49 @@ async function setupClaudeCodeHooks(root, specsDir) {
237
279
  const claudeDir = path.join(root, ".claude");
238
280
  const hooksDir = path.join(claudeDir, "hooks");
239
281
  const settingsPath = path.join(claudeDir, "settings.json");
240
- const hookScriptPath = path.join(hooksDir, "mcp-first.sh");
282
+ const mcpFirstPath = path.join(hooksDir, "mcp-first.sh");
283
+ const guardianUsedPath = path.join(hooksDir, "guardian-used.sh");
241
284
  await fs.mkdir(hooksDir, { recursive: true });
242
- // Write the hook script
243
- if (!(await fileExists(hookScriptPath))) {
244
- await fs.writeFile(hookScriptPath, CLAUDE_CODE_HOOK_SCRIPT, "utf8");
245
- await fs.chmod(hookScriptPath, 0o755);
246
- console.log(" ✓ Created Claude Code MCP-first hook (.claude/hooks/mcp-first.sh)");
247
- }
248
- else {
249
- console.log(" · Claude Code hook already exists");
250
- }
285
+ // Always overwrite hook scripts so they stay in sync with this version of guardian.
286
+ await fs.writeFile(mcpFirstPath, CLAUDE_CODE_HOOK_SCRIPT, "utf8");
287
+ await fs.chmod(mcpFirstPath, 0o755);
288
+ console.log(" ✓ Wrote .claude/hooks/mcp-first.sh (PreToolUse — blocks until guardian called)");
289
+ await fs.writeFile(guardianUsedPath, GUARDIAN_USED_SCRIPT, "utf8");
290
+ await fs.chmod(guardianUsedPath, 0o755);
291
+ console.log(" ✓ Wrote .claude/hooks/guardian-used.sh (PostToolUse — sets session flag)");
251
292
  // Write or merge .claude/settings.json
252
293
  let settings = {};
253
294
  if (await fileExists(settingsPath)) {
254
295
  try {
255
296
  settings = JSON.parse(await fs.readFile(settingsPath, "utf8"));
256
297
  }
257
- catch {
258
- // Corrupted file — overwrite
259
- }
298
+ catch { /* corrupted — overwrite */ }
260
299
  }
261
- // Add MCP server config
300
+ // MCP server registration
262
301
  if (!settings.mcpServers)
263
302
  settings.mcpServers = {};
264
- const mcpServers = settings.mcpServers;
265
- if (!mcpServers.guardian) {
266
- mcpServers.guardian = {
267
- command: "guardian",
268
- args: ["mcp-serve", "--specs", specsDir],
269
- };
270
- }
271
- // Add PreToolUse hook
272
- const hookEntry = {
273
- matcher: "Read|Glob|Grep",
274
- hooks: [
303
+ settings.mcpServers.guardian = {
304
+ command: "guardian",
305
+ args: ["mcp-serve", "--specs", specsDir],
306
+ };
307
+ // Hooks always overwrite to keep in sync with installed scripts.
308
+ settings.hooks = {
309
+ // PreToolUse: block Read/Glob/Grep until a guardian tool has been called.
310
+ // The script itself handles the session-flag check — no "if" filter needed here.
311
+ PreToolUse: [
312
+ {
313
+ matcher: "Read|Glob|Grep",
314
+ hooks: [{ type: "command", command: ".claude/hooks/mcp-first.sh" }],
315
+ },
316
+ ],
317
+ // PostToolUse: set the session flag after any guardian MCP tool call.
318
+ PostToolUse: [
275
319
  {
276
- type: "command",
277
- if: "Read(//*/src/*)|Glob(*src*)|Grep(*src*)",
278
- command: '"$CLAUDE_PROJECT_DIR"/.claude/hooks/mcp-first.sh',
320
+ matcher: "mcp__guardian__guardian_search|mcp__guardian__guardian_orient|mcp__guardian__guardian_context|mcp__guardian__guardian_impact|mcp__guardian__guardian_grep|mcp__guardian__guardian_glob",
321
+ hooks: [{ type: "command", command: ".claude/hooks/guardian-used.sh" }],
279
322
  },
280
323
  ],
281
324
  };
282
- if (!settings.hooks)
283
- settings.hooks = {};
284
- const hooks = settings.hooks;
285
- if (!hooks.PreToolUse) {
286
- hooks.PreToolUse = [hookEntry];
287
- console.log(" ✓ Configured Claude Code PreToolUse hook in .claude/settings.json");
288
- }
289
- else {
290
- console.log(" · Claude Code PreToolUse hook already configured");
291
- }
292
325
  await fs.writeFile(settingsPath, JSON.stringify(settings, null, 2) + "\n", "utf8");
293
- console.log(" ✓ Updated .claude/settings.json (MCP server + hooks)");
326
+ console.log(" ✓ Updated .claude/settings.json (MCP server + PreToolUse + PostToolUse hooks)");
294
327
  }
@@ -2,16 +2,85 @@
2
2
  * `guardian intel` — build codebase-intelligence.json from existing snapshots.
3
3
  *
4
4
  * Reads: specs-out/machine/architecture.snapshot.yaml + ux.snapshot.yaml
5
- * Writes: specs-out/machine/codebase-intelligence.json
5
+ * Writes: specs-out/machine/codebase-intelligence.json (file backend, default)
6
+ * specs-out/guardian.db (sqlite backend)
6
7
  *
7
8
  * Also auto-runs at the end of `guardian extract`.
8
9
  */
9
10
  import path from "node:path";
10
11
  import { writeCodebaseIntelligence } from "../extract/codebase-intel.js";
12
+ import { writeCodebaseIntelligenceViaStore } from "../extract/codebase-intel.js";
11
13
  import { getOutputLayout } from "../output-layout.js";
14
+ import { SqliteSpecsStore } from "../db/sqlite-specs-store.js";
15
+ import { populateFTSIndex } from "../db/fts-builder.js";
16
+ import { embedFunctions } from "../db/embeddings.js";
12
17
  export async function runIntel(options) {
13
18
  const specsDir = path.resolve(options.specs);
14
19
  const layout = getOutputLayout(specsDir);
20
+ if (options.backend === "sqlite") {
21
+ // ── SQLite path ──
22
+ // extract always writes snapshots as files, so we read those then write
23
+ // intel + FTS into guardian.db. This avoids requiring --backend on extract.
24
+ const store = new SqliteSpecsStore(layout.rootDir);
25
+ await store.init();
26
+ try {
27
+ // Read snapshots from the existing file-based layout
28
+ const machineDir = layout.machineDir;
29
+ const [archRaw, uxRaw] = await Promise.all([
30
+ (await import("node:fs/promises")).readFile((await import("node:path")).join(machineDir, "architecture.snapshot.yaml"), "utf8"),
31
+ (await import("node:fs/promises")).readFile((await import("node:path")).join(machineDir, "ux.snapshot.yaml"), "utf8"),
32
+ ]);
33
+ // Populate snapshots into the store so writeCodebaseIntelligenceViaStore can read them
34
+ await store.writeSpec("architecture.snapshot", archRaw, "yaml");
35
+ await store.writeSpec("ux.snapshot", uxRaw, "yaml");
36
+ // Build intel and write to DB
37
+ await writeCodebaseIntelligenceViaStore(store);
38
+ // Build FTS5 index — enrich with all extract output for best recall
39
+ const intelEntry = await store.readSpec("codebase-intelligence");
40
+ if (intelEntry) {
41
+ const intel = JSON.parse(intelEntry.content);
42
+ const archEntry = await store.readSpec("architecture.snapshot");
43
+ const arch = archEntry ? (await import("js-yaml")).load(archEntry.content) : undefined;
44
+ // Also load function-intelligence if present in the machine dir
45
+ let funcIntel;
46
+ try {
47
+ const fnRaw = await (await import("node:fs/promises")).readFile((await import("node:path")).join(machineDir, "function-intelligence.json"), "utf8");
48
+ funcIntel = JSON.parse(fnRaw);
49
+ }
50
+ catch { /* not generated yet — skip */ }
51
+ populateFTSIndex(store, intel, arch, funcIntel);
52
+ console.log(`Built FTS5 search index (${Object.keys(intel.api_registry ?? {}).length} endpoints indexed)`);
53
+ // Populate module_metrics from structural-intelligence.json (if present).
54
+ try {
55
+ const siRaw = await (await import("node:fs/promises")).readFile((await import("node:path")).join(machineDir, "structural-intelligence.json"), "utf8");
56
+ const siReports = JSON.parse(siRaw);
57
+ if (Array.isArray(siReports) && siReports.length > 0) {
58
+ store.rebuildModuleMetrics(siReports);
59
+ console.log(`Indexed ${siReports.length} module metrics`);
60
+ }
61
+ }
62
+ catch { /* structural-intelligence.json not generated yet — skip */ }
63
+ // Embed functions for semantic (vector) search.
64
+ // Uses local on-device model by default (no API key needed).
65
+ // If OPENAI_API_KEY is set, uses OpenAI text-embedding-3-small (better quality).
66
+ if (funcIntel?.functions?.length) {
67
+ console.log(`[guardian embed] embedding ${funcIntel.functions.length} functions…`);
68
+ try {
69
+ await embedFunctions(store, funcIntel.functions, process.env.OPENAI_API_KEY);
70
+ }
71
+ catch (err) {
72
+ console.warn(`[guardian embed] skipped: ${err.message}`);
73
+ }
74
+ }
75
+ }
76
+ console.log(`Wrote guardian.db → ${layout.rootDir}`);
77
+ }
78
+ finally {
79
+ await store.close();
80
+ }
81
+ return;
82
+ }
83
+ // ── File path (default): original behavior, unchanged ──
15
84
  const outputPath = options.output
16
85
  ? path.resolve(options.output)
17
86
  : path.join(layout.machineDir, "codebase-intelligence.json");