@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.
- package/README.md +6 -4
- package/dist/adapters/runner.js +72 -3
- package/dist/adapters/typescript-adapter.js +24 -10
- package/dist/benchmarking/metrics/context-coverage.js +82 -0
- package/dist/benchmarking/metrics/drift-score.js +104 -0
- package/dist/benchmarking/metrics/search-recall.js +207 -0
- package/dist/benchmarking/metrics/token-efficiency.js +79 -0
- package/dist/benchmarking/report.js +131 -0
- package/dist/benchmarking/runner.js +175 -0
- package/dist/benchmarking/types.js +13 -0
- package/dist/cli.js +53 -10
- package/dist/commands/benchmark.js +62 -0
- package/dist/commands/context.js +87 -29
- package/dist/commands/discrepancy.js +1 -1
- package/dist/commands/doc-generate.js +1 -1
- package/dist/commands/doc-html.js +1 -1
- package/dist/commands/extract.js +4 -1
- package/dist/commands/feature-context.js +1 -1
- package/dist/commands/generate.js +83 -10
- package/dist/commands/init.js +89 -56
- package/dist/commands/intel.js +70 -1
- package/dist/commands/mcp-serve.js +155 -316
- package/dist/commands/search.js +642 -14
- package/dist/config.js +1 -0
- package/dist/db/embeddings.js +113 -0
- package/dist/db/file-specs-store.js +174 -0
- package/dist/db/fts-builder.js +390 -0
- package/dist/db/index.js +55 -0
- package/dist/db/specs-store.js +13 -0
- package/dist/db/sqlite-specs-store.js +934 -0
- package/dist/extract/codebase-intel.js +31 -2
- package/dist/extract/compress.js +70 -3
- package/dist/extract/context-block.js +11 -2
- package/dist/extract/function-intel.js +5 -2
- package/dist/extract/index.js +1 -23
- package/dist/extract/writer.js +6 -0
- package/package.json +4 -1
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
|
}
|
|
@@ -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
|
|
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
|
|
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
|
|
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) ───────────────────────────────────────────────
|
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 });
|
|
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
|
|
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
|
-
|
|
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 {
|
package/dist/commands/init.js
CHANGED
|
@@ -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 —
|
|
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
|
-
|
|
35
|
+
SESSION_ID=$(echo "$INPUT" | jq -r '.session_id // "default"')
|
|
36
|
+
FLAG="/tmp/guardian-used-\${SESSION_ID}"
|
|
28
37
|
|
|
29
|
-
|
|
30
|
-
|
|
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
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
//
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
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
|
-
//
|
|
300
|
+
// MCP server registration
|
|
262
301
|
if (!settings.mcpServers)
|
|
263
302
|
settings.mcpServers = {};
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
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
|
-
|
|
277
|
-
|
|
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
|
}
|
package/dist/commands/intel.js
CHANGED
|
@@ -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");
|