@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 +55 -20
- package/dist/cli.js +2 -6
- package/dist/commands/context.js +87 -29
- package/dist/commands/extract.js +4 -1
- package/dist/commands/generate.js +83 -10
- package/dist/commands/init.js +88 -60
- package/dist/commands/intel.js +23 -0
- package/dist/commands/mcp-serve.js +112 -0
- package/dist/commands/search.js +43 -3
- package/dist/config.js +1 -0
- package/dist/db/embeddings.js +113 -0
- package/dist/db/fts-builder.js +108 -0
- package/dist/db/sqlite-specs-store.js +496 -3
- package/package.json +2 -1
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 → .
|
|
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
|
|
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
|
-
-
|
|
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
|
-
**
|
|
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 —
|
|
158
|
+
# One-time setup — config, .specs/, guardian.db, pre-commit hook, .mcp.json, CLAUDE.md
|
|
141
159
|
guardian init
|
|
142
160
|
|
|
143
|
-
# Extract architecture
|
|
161
|
+
# Extract architecture + build search index (guardian.db built automatically)
|
|
144
162
|
guardian extract
|
|
145
163
|
|
|
146
|
-
#
|
|
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 +
|
|
233
|
-
guardian extract --backend
|
|
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"
|
|
241
|
-
guardian search --query "auth" --types models,endpoints
|
|
242
|
-
guardian
|
|
243
|
-
guardian context --
|
|
244
|
-
guardian
|
|
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
|
-
│ ├──
|
|
325
|
-
│ ├──
|
|
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: '
|
|
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: '
|
|
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,
|
package/dist/commands/context.js
CHANGED
|
@@ -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
|
-
|
|
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
|
|
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
|
}
|
package/dist/commands/extract.js
CHANGED
|
@@ -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
|
|
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
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
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 {
|