@toolbaux/guardian 0.1.23 → 0.2.1

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/README.md CHANGED
@@ -41,21 +41,24 @@ Developer writes code
41
41
  ↓ (save)
42
42
  VSCode extension (5s debounce)
43
43
 
44
- guardian extract → .specs/
45
- guardian generate --ai-context → .specs/
46
- guardian context → CLAUDE.md (between markers)
44
+ guardian extract → .specs/ + guardian.db (BM25 search index)
45
+ guardian generate --ai-context → reads from guardian.db, writes CLAUDE.md
47
46
  Status bar: "✓ Guardian: stable · 35 ep · 8 pg"
48
47
  ↓ (git commit)
49
48
  Pre-commit hook: extract + context → auto-staged
50
49
 
51
- Claude Code / Cursor reads CLAUDE.md fresh architecture context
50
+ Claude Code reads CLAUDE.md at session start
51
+ Claude Code calls MCP tools (guardian_search, guardian_grep, guardian_glob…)
52
+ ↓ fresh, indexed context on every query
52
53
  ```
53
54
 
54
55
  After `guardian init`, your project gets:
55
- - `.specs/` directory with architecture snapshots
56
+ - `.specs/` directory with architecture snapshots + `guardian.db` (SQLite search index)
56
57
  - `CLAUDE.md` with auto-injected context (refreshed on every save and commit)
57
58
  - Pre-commit hook that keeps context fresh automatically
58
- - `guardian.config.json` for project settings (roots auto-detected at runtime)
59
+ - `.mcp.json` wiring Claude Code and Cursor to Guardian's MCP server
60
+ - `guardian.config.json` with a unique `project_id` and auto-detected roots
61
+ - MCP-first hook: Claude Code is nudged to call `guardian_search` before reading source files
59
62
 
60
63
  ## Claude Code / Cursor Integration
61
64
 
@@ -85,16 +88,18 @@ The block between markers is replaced on every save (VSCode extension) and every
85
88
 
86
89
  Guardian includes an MCP server that Claude Code and Cursor connect to automatically. The VSCode extension sets this up on first activation — no manual config needed.
87
90
 
88
- **6 compact tools available to AI:**
91
+ **8 compact tools available to AI:**
89
92
 
90
93
  | Tool | Tokens | Purpose |
91
94
  |------|--------|---------|
92
95
  | `guardian_orient` | ~100 | Project summary at session start |
93
96
  | `guardian_context` | ~50-80 | File or endpoint dependencies before editing |
94
97
  | `guardian_impact` | ~30 | What breaks if you change a file |
95
- | `guardian_search` | ~70 | Find endpoints, models, modules by keyword |
98
+ | `guardian_search` | ~70 | Find endpoints, models, modules, and functions by keyword |
96
99
  | `guardian_model` | ~90 | Full field details (only when needed) |
97
100
  | `guardian_metrics` | ~50 | Session usage stats |
101
+ | `guardian_grep` | ~40 | Semantic grep — search symbols and literals across the codebase |
102
+ | `guardian_glob` | ~30 | Semantic file discovery — find files by pattern with module context |
98
103
 
99
104
  All responses are compact JSON — no pretty-printing, no verbose keys. Repeated calls are cached (30s TTL). Usage metrics tracked per session.
100
105
 
@@ -113,6 +118,19 @@ All responses are compact JSON — no pretty-printing, no verbose keys. Repeated
113
118
 
114
119
  > **Note:** After `.mcp.json` is created or modified, you must **restart your Claude Code / Cursor session** (or reload the VSCode window) for the MCP server to connect. MCP config is only read at session start.
115
120
 
121
+ ### MCP-First Hook
122
+
123
+ `guardian init` also installs a Claude Code hook that encourages AI tools to call Guardian before reading source files directly. The hook is session-scoped — once any `guardian_*` tool is called, file reads are unblocked for the rest of the session. No repeated interruptions.
124
+
125
+ The block message tells Claude exactly what to call:
126
+ ```
127
+ Call one of these first:
128
+ guardian_search("your query") — find files/symbols/endpoints by keyword
129
+ guardian_grep("pattern") — semantic grep (replaces Grep tool)
130
+ guardian_glob("src/auth/**") — semantic file discovery (replaces Glob tool)
131
+ guardian_orient() — get codebase overview
132
+ ```
133
+
116
134
  ## VSCode Extension
117
135
 
118
136
  Install from [VS Code Marketplace](https://marketplace.visualstudio.com/items?itemName=toolbaux.toolbaux-guardian):
@@ -137,14 +155,21 @@ Cmd+Shift+P → "Extensions: Install from VSIX"
137
155
  ## Key Commands
138
156
 
139
157
  ```bash
140
- # One-time setup — creates config, .specs/, pre-commit hook, CLAUDE.md
158
+ # One-time setup — config, .specs/, guardian.db, pre-commit hook, .mcp.json, CLAUDE.md
141
159
  guardian init
142
160
 
143
- # Extract architecture (run after major changes, or let the hook do it)
161
+ # Extract architecture + build search index (guardian.db built automatically)
144
162
  guardian extract
145
163
 
146
- # Search your codebase by concept
164
+ # Extract without DB (CI environments that don't need search)
165
+ guardian extract --backend file
166
+
167
+ # Search your codebase by concept (uses guardian.db when available)
147
168
  guardian search --query "session"
169
+ guardian search --query "auth" --types functions,endpoints
170
+
171
+ # Inject fresh context into CLAUDE.md
172
+ guardian context --output CLAUDE.md
148
173
 
149
174
  # Compute architectural drift
150
175
  guardian drift
@@ -229,19 +254,20 @@ npm install && npm run build && npm link
229
254
 
230
255
  ```bash
231
256
  guardian init # config, .specs dir, pre-commit hook, CLAUDE.md
232
- guardian extract # full architecture + UX snapshots + docs
233
- guardian extract --backend sqlite # same + builds guardian.db with FTS index
257
+ guardian extract # full architecture + UX snapshots + guardian.db (default: sqlite)
258
+ guardian extract --backend file # file-only mode, skips guardian.db
234
259
  guardian generate --ai-context # compact ~3K token AI context only
235
260
  ```
236
261
 
237
262
  ### Search & Context
238
263
 
239
264
  ```bash
240
- guardian search --query "session" # search models, endpoints, components
241
- guardian search --query "auth" --types models,endpoints
242
- guardian context --focus "auth" # focused AI context block
243
- guardian context --output CLAUDE.md # inject between auto-context markers
244
- guardian summary # executive summary
265
+ guardian search --query "session" # search models, endpoints, components, functions
266
+ guardian search --query "auth" --types models,endpoints # filter by type
267
+ guardian search --query "validate token" --types functions # function-level search (uses guardian.db)
268
+ guardian context --focus "auth" # focused AI context block
269
+ guardian context --output CLAUDE.md # inject between auto-context markers
270
+ guardian summary # executive summary
245
271
  ```
246
272
 
247
273
  ### Architectural Metrics
@@ -283,11 +309,16 @@ guardian feature-context --spec feature-specs/billing.yaml
283
309
 
284
310
  ```json
285
311
  {
312
+ "project_id": "auto-generated-uuid",
286
313
  "project": {
287
314
  "description": "Short product description for generated docs",
288
315
  "backendRoot": "./backend",
289
316
  "frontendRoot": "./frontend"
290
317
  },
318
+ "ignore": {
319
+ "directories": ["bench-repos", "fixtures", "vendor"],
320
+ "paths": ["src/generated"]
321
+ },
291
322
  "frontend": {
292
323
  "routeDirs": ["app"],
293
324
  "aliases": { "@": "./frontend" }
@@ -309,6 +340,8 @@ guardian feature-context --spec feature-specs/billing.yaml
309
340
  }
310
341
  ```
311
342
 
343
+ > **Tip:** Use `ignore.directories` to exclude directories that Guardian indexes but aren't part of your project (e.g. benchmark repos, vendor directories, generated code). Guardian scans all source files under the project root by design — configure ignores to keep the search index clean.
344
+
312
345
  </details>
313
346
 
314
347
  <details>
@@ -316,13 +349,15 @@ guardian feature-context --spec feature-specs/billing.yaml
316
349
 
317
350
  ```
318
351
  .specs/
352
+ ├── guardian.db ← SQLite search index (BM25 + function call graph)
319
353
  ├── machine/
320
354
  │ ├── architecture-context.md ← AI context (~3K tokens)
321
355
  │ ├── architecture.snapshot.yaml ← full architecture snapshot
322
356
  │ ├── ux.snapshot.yaml ← frontend components + pages
323
357
  │ ├── codebase-intelligence.json ← unified registry
324
- │ ├── drift.report.json drift metrics
325
- │ ├── constraints.json duplicates, cycles
358
+ │ ├── function-intelligence.json function call graph + literal index
359
+ │ ├── structural-intelligence.json depth/complexity per module
360
+ │ ├── drift.heatmap.json ← file-level change frequency
326
361
  │ └── docs/ ← generated markdown docs
327
362
  ├── human/
328
363
  │ ├── product-document.md ← LLM-powered product doc
package/dist/cli.js CHANGED
@@ -62,7 +62,7 @@ program
62
62
  .option("--no-file-graph", "Exclude file-level dependency graph")
63
63
  .option("--config <path>", "Path to guardian.config.json")
64
64
  .option("--docs-mode <mode>", "Docs mode (lean|full)")
65
- .option("--backend <backend>", "Storage backend: 'file' (default) or 'sqlite' (also builds guardian.db + FTS index)")
65
+ .option("--backend <backend>", "Storage backend: 'sqlite' (default, builds guardian.db + FTS index) or 'file'")
66
66
  .action(async (projectRoot, options) => {
67
67
  await runExtract({
68
68
  projectRoot,
@@ -349,16 +349,12 @@ program
349
349
  .command("init")
350
350
  .description("Initialize guardian for a project (config, .specs dir, pre-commit hook, CLAUDE.md)")
351
351
  .argument("[projectRoot]", "Repo or project root", process.cwd())
352
- .option("--backend-root <path>", "Path to backend root")
353
- .option("--frontend-root <path>", "Path to frontend root")
354
352
  .option("--output <path>", "Output directory", DEFAULT_SPECS_DIR)
355
353
  .option("--skip-hook", "Skip pre-commit hook installation", false)
356
- .option("--backend <backend>", "Storage backend: 'file' (default) or 'sqlite' (builds guardian.db + FTS index)")
354
+ .option("--backend <backend>", "Storage backend: 'sqlite' (default) or 'file'")
357
355
  .action(async (projectRoot, options) => {
358
356
  await runInit({
359
357
  projectRoot,
360
- backendRoot: options.backendRoot,
361
- frontendRoot: options.frontendRoot,
362
358
  output: options.output,
363
359
  skipHook: options.skipHook ?? false,
364
360
  backend: options.backend,
@@ -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
  }
@@ -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, backend: options.backend });
17
+ await runIntel({ specs: specsDir, backend });
15
18
  }
16
19
  catch {
17
20
  // Non-fatal — intel build failure should not break extract
@@ -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 {