@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
|
@@ -171,8 +171,10 @@ function buildEndpointPatternMap(architecture) {
|
|
|
171
171
|
}
|
|
172
172
|
return result;
|
|
173
173
|
}
|
|
174
|
+
// ── File-based IO (original implementation — unchanged) ────────────────────
|
|
174
175
|
/**
|
|
175
|
-
* Load snapshots and
|
|
176
|
+
* Load snapshots and write codebase-intelligence.json to disk.
|
|
177
|
+
* This is the original file-based implementation, kept intact.
|
|
176
178
|
*/
|
|
177
179
|
export async function writeCodebaseIntelligence(specsDir, outputPath) {
|
|
178
180
|
const machineDir = await resolveMachineInputDir(specsDir);
|
|
@@ -187,9 +189,36 @@ export async function writeCodebaseIntelligence(specsDir, outputPath) {
|
|
|
187
189
|
await fs.writeFile(outputPath, JSON.stringify(intel, null, 2), "utf8");
|
|
188
190
|
}
|
|
189
191
|
/**
|
|
190
|
-
* Load an existing codebase-intelligence.json from
|
|
192
|
+
* Load an existing codebase-intelligence.json from a file path.
|
|
193
|
+
* Original file-based implementation, kept intact.
|
|
191
194
|
*/
|
|
192
195
|
export async function loadCodebaseIntelligence(intelPath) {
|
|
193
196
|
const raw = await fs.readFile(intelPath, "utf8");
|
|
194
197
|
return JSON.parse(raw);
|
|
195
198
|
}
|
|
199
|
+
// ── Store-based IO (new — works with both FileSpecsStore and SqliteSpecsStore) ─
|
|
200
|
+
/**
|
|
201
|
+
* Build CodebaseIntelligence and write it via a SpecsStore.
|
|
202
|
+
* Use this when operating on a guardian.db or when you already have a store open.
|
|
203
|
+
*/
|
|
204
|
+
export async function writeCodebaseIntelligenceViaStore(store) {
|
|
205
|
+
const archEntry = await store.readSpec("architecture.snapshot");
|
|
206
|
+
const uxEntry = await store.readSpec("ux.snapshot");
|
|
207
|
+
if (!archEntry || !uxEntry) {
|
|
208
|
+
throw new Error("architecture.snapshot or ux.snapshot not found in store. Run `guardian extract` first.");
|
|
209
|
+
}
|
|
210
|
+
const architecture = yaml.load(archEntry.content);
|
|
211
|
+
const ux = yaml.load(uxEntry.content);
|
|
212
|
+
const intel = buildCodebaseIntelligence(architecture, ux);
|
|
213
|
+
await store.writeSpec("codebase-intelligence", JSON.stringify(intel, null, 2), "json");
|
|
214
|
+
}
|
|
215
|
+
/**
|
|
216
|
+
* Load CodebaseIntelligence from a SpecsStore.
|
|
217
|
+
* Returns null if not yet built.
|
|
218
|
+
*/
|
|
219
|
+
export async function loadCodebaseIntelligenceViaStore(store) {
|
|
220
|
+
const entry = await store.readSpec("codebase-intelligence");
|
|
221
|
+
if (!entry)
|
|
222
|
+
return null;
|
|
223
|
+
return JSON.parse(entry.content);
|
|
224
|
+
}
|
package/dist/extract/compress.js
CHANGED
|
@@ -319,6 +319,7 @@ function buildHeatmapFromGraph(level, nodes, edges, nodeLayers) {
|
|
|
319
319
|
}
|
|
320
320
|
}
|
|
321
321
|
const cycleNodes = findCycleNodes(nodes, adjacency, reverse);
|
|
322
|
+
const pageRank = computePageRank(nodes, adjacency, reverse);
|
|
322
323
|
const degreeValues = nodes.map((node) => (outbound.get(node) ?? 0) + (inbound.get(node) ?? 0));
|
|
323
324
|
const maxDegree = Math.max(1, ...degreeValues);
|
|
324
325
|
const maxCrossRatio = Math.max(1, ...nodes.map((node) => {
|
|
@@ -332,15 +333,22 @@ function buildHeatmapFromGraph(level, nodes, edges, nodeLayers) {
|
|
|
332
333
|
const out = outbound.get(node) ?? 0;
|
|
333
334
|
const crossRatio = out === 0 ? 0 : crossOut / out;
|
|
334
335
|
const cycleFlag = cycleNodes.has(node) ? 1 : 0;
|
|
335
|
-
const
|
|
336
|
-
|
|
337
|
-
|
|
336
|
+
const pr = pageRank.get(node) ?? 0;
|
|
337
|
+
// PageRank (40%) — importance by what depends on this node
|
|
338
|
+
// Degree (30%) — raw connectivity (fallback signal)
|
|
339
|
+
// Cross-layer (20%) — architectural violation risk
|
|
340
|
+
// Cycle (10%) — circular dependency penalty
|
|
341
|
+
const score = 0.4 * pr +
|
|
342
|
+
0.3 * (degree / maxDegree) +
|
|
343
|
+
0.2 * (crossRatio / maxCrossRatio) +
|
|
344
|
+
0.1 * cycleFlag;
|
|
338
345
|
return {
|
|
339
346
|
id: node,
|
|
340
347
|
layer: nodeLayers.get(node) ?? "unknown",
|
|
341
348
|
score: round(score, 4),
|
|
342
349
|
components: {
|
|
343
350
|
degree,
|
|
351
|
+
pagerank: round(pr, 4),
|
|
344
352
|
cross_layer_ratio: round(crossRatio, 4),
|
|
345
353
|
cycle: cycleFlag
|
|
346
354
|
}
|
|
@@ -368,6 +376,65 @@ function resolveDomainForModule(moduleId, domainMap) {
|
|
|
368
376
|
}
|
|
369
377
|
return null;
|
|
370
378
|
}
|
|
379
|
+
/**
|
|
380
|
+
* Iterative PageRank over a directed graph.
|
|
381
|
+
* Returns a map of node → normalized score in [0, 1].
|
|
382
|
+
*
|
|
383
|
+
* Semantics: a node is important if many important nodes import/depend on it.
|
|
384
|
+
* Damping factor α=0.85 (web-standard). Converges in ~20 iterations for
|
|
385
|
+
* codebases with <10K files.
|
|
386
|
+
*
|
|
387
|
+
* Edge direction follows dependency arrows (A imports B → edge A→B).
|
|
388
|
+
* Rank flows *backward*: B gains rank because A depends on it, meaning
|
|
389
|
+
* files that many other files rely on get high scores — exactly what we
|
|
390
|
+
* want to surface in AI context.
|
|
391
|
+
*/
|
|
392
|
+
function computePageRank(nodes, adjacency, // forward edges (importer → imported)
|
|
393
|
+
reverse // backward edges (imported → importers)
|
|
394
|
+
) {
|
|
395
|
+
const N = nodes.length;
|
|
396
|
+
if (N === 0)
|
|
397
|
+
return new Map();
|
|
398
|
+
const DAMPING = 0.85;
|
|
399
|
+
const ITERATIONS = 30;
|
|
400
|
+
const BASE = (1 - DAMPING) / N;
|
|
401
|
+
// Initialize uniform rank
|
|
402
|
+
const rank = new Map();
|
|
403
|
+
for (const node of nodes)
|
|
404
|
+
rank.set(node, 1 / N);
|
|
405
|
+
// Precompute out-degrees (how many nodes each node imports)
|
|
406
|
+
const outDeg = new Map();
|
|
407
|
+
for (const node of nodes)
|
|
408
|
+
outDeg.set(node, (adjacency.get(node) ?? []).length);
|
|
409
|
+
// Dangling nodes (no outgoing edges) distribute rank uniformly
|
|
410
|
+
for (let iter = 0; iter < ITERATIONS; iter++) {
|
|
411
|
+
const next = new Map();
|
|
412
|
+
// Dangling mass: sum of ranks of sink nodes spread across all nodes
|
|
413
|
+
let danglingMass = 0;
|
|
414
|
+
for (const node of nodes) {
|
|
415
|
+
if ((outDeg.get(node) ?? 0) === 0) {
|
|
416
|
+
danglingMass += (rank.get(node) ?? 0);
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
const danglingContrib = DAMPING * danglingMass / N;
|
|
420
|
+
for (const node of nodes) {
|
|
421
|
+
let incoming = 0;
|
|
422
|
+
for (const importer of (reverse.get(node) ?? [])) {
|
|
423
|
+
const d = outDeg.get(importer) ?? 1;
|
|
424
|
+
incoming += (rank.get(importer) ?? 0) / d;
|
|
425
|
+
}
|
|
426
|
+
next.set(node, BASE + danglingContrib + DAMPING * incoming);
|
|
427
|
+
}
|
|
428
|
+
for (const node of nodes)
|
|
429
|
+
rank.set(node, next.get(node) ?? 0);
|
|
430
|
+
}
|
|
431
|
+
// Normalize to [0, 1] relative to max
|
|
432
|
+
const max = Math.max(1e-10, ...Array.from(rank.values()));
|
|
433
|
+
const normalized = new Map();
|
|
434
|
+
for (const [node, r] of rank.entries())
|
|
435
|
+
normalized.set(node, r / max);
|
|
436
|
+
return normalized;
|
|
437
|
+
}
|
|
371
438
|
function findCycleNodes(nodes, adjacency, reverse) {
|
|
372
439
|
const visited = new Set();
|
|
373
440
|
const order = [];
|
|
@@ -29,8 +29,17 @@ export function renderContextBlock(architecture, ux, options) {
|
|
|
29
29
|
}
|
|
30
30
|
lines.push("");
|
|
31
31
|
}
|
|
32
|
-
// Cross-module dependencies
|
|
33
|
-
const
|
|
32
|
+
// Cross-module dependencies (deduplicated)
|
|
33
|
+
const seenEdges = new Set();
|
|
34
|
+
const crossEdges = architecture.dependencies.module_graph.filter(e => {
|
|
35
|
+
if (e.from === e.to)
|
|
36
|
+
return false;
|
|
37
|
+
const key = `${e.from}→${e.to}`;
|
|
38
|
+
if (seenEdges.has(key))
|
|
39
|
+
return false;
|
|
40
|
+
seenEdges.add(key);
|
|
41
|
+
return true;
|
|
42
|
+
});
|
|
34
43
|
if (crossEdges.length > 0) {
|
|
35
44
|
lines.push("### Module Dependencies");
|
|
36
45
|
for (const edge of crossEdges.slice(0, 10)) {
|
|
@@ -160,8 +160,10 @@ async function listSourceFiles(dir, config, results = []) {
|
|
|
160
160
|
* Scan one or more project roots, run adapters on every source file, and
|
|
161
161
|
* return the aggregated FunctionIntelligence index.
|
|
162
162
|
*/
|
|
163
|
-
export async function buildFunctionIntelligenceFromRoots(roots, config) {
|
|
163
|
+
export async function buildFunctionIntelligenceFromRoots(roots, config, projectRoot) {
|
|
164
164
|
const allFunctions = [];
|
|
165
|
+
// Relativize against project root if provided; otherwise fall back to the scan root
|
|
166
|
+
const baseDir = projectRoot ?? roots[0];
|
|
165
167
|
for (const root of roots) {
|
|
166
168
|
const files = await listSourceFiles(root, config);
|
|
167
169
|
await Promise.all(files.map(async (filePath) => {
|
|
@@ -177,7 +179,8 @@ export async function buildFunctionIntelligenceFromRoots(roots, config) {
|
|
|
177
179
|
}
|
|
178
180
|
try {
|
|
179
181
|
const result = runAdapter(adapter, filePath, source);
|
|
180
|
-
|
|
182
|
+
const relPath = path.relative(baseDir, filePath);
|
|
183
|
+
allFunctions.push(...result.functions.map(fn => ({ ...fn, file: relPath })));
|
|
181
184
|
}
|
|
182
185
|
catch {
|
|
183
186
|
// Skip files that fail to parse (malformed source, encoding issues)
|
package/dist/extract/index.js
CHANGED
|
@@ -191,8 +191,7 @@ export async function extractProject(options) {
|
|
|
191
191
|
// Generate Function Intelligence — call graph, literal index across all languages.
|
|
192
192
|
// Runs as an additive second pass; never modifies the architecture snapshot.
|
|
193
193
|
try {
|
|
194
|
-
const
|
|
195
|
-
const funcIntel = await buildFunctionIntelligenceFromRoots(allRoots, config);
|
|
194
|
+
const funcIntel = await buildFunctionIntelligenceFromRoots([projectRoot], config, projectRoot);
|
|
196
195
|
await writeFunctionIntelligence(layout.machineDir, funcIntel);
|
|
197
196
|
}
|
|
198
197
|
catch (err) {
|
|
@@ -421,27 +420,6 @@ function mergeFrontendAnalyses(results, _roots, _workspaceRoot) {
|
|
|
421
420
|
tests: results.flatMap(r => r.tests)
|
|
422
421
|
};
|
|
423
422
|
}
|
|
424
|
-
function findCommonRoot(paths) {
|
|
425
|
-
if (paths.length === 0) {
|
|
426
|
-
return process.cwd();
|
|
427
|
-
}
|
|
428
|
-
const splitPaths = paths.map((entry) => path.resolve(entry).split(path.sep));
|
|
429
|
-
const minLength = Math.min(...splitPaths.map((parts) => parts.length));
|
|
430
|
-
const shared = [];
|
|
431
|
-
for (let i = 0; i < minLength; i += 1) {
|
|
432
|
-
const segment = splitPaths[0][i];
|
|
433
|
-
if (splitPaths.every((parts) => parts[i] === segment)) {
|
|
434
|
-
shared.push(segment);
|
|
435
|
-
}
|
|
436
|
-
else {
|
|
437
|
-
break;
|
|
438
|
-
}
|
|
439
|
-
}
|
|
440
|
-
if (shared.length === 0) {
|
|
441
|
-
return path.parse(paths[0]).root;
|
|
442
|
-
}
|
|
443
|
-
return shared.join(path.sep);
|
|
444
|
-
}
|
|
445
423
|
async function loadPreviousSnapshots(machineDir, rootDir) {
|
|
446
424
|
const result = {};
|
|
447
425
|
const candidates = [
|
package/dist/extract/writer.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import fs from "node:fs/promises";
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
import yaml from "js-yaml";
|
|
4
|
+
// ── File-based (original, unchanged) ─────────────────────────────────────────
|
|
4
5
|
export async function writeSnapshots(outputDir, architecture, ux) {
|
|
5
6
|
await fs.mkdir(outputDir, { recursive: true });
|
|
6
7
|
const architecturePath = path.join(outputDir, "architecture.snapshot.yaml");
|
|
@@ -9,3 +10,8 @@ export async function writeSnapshots(outputDir, architecture, ux) {
|
|
|
9
10
|
await fs.writeFile(uxPath, yaml.dump(ux, { noRefs: true, lineWidth: 120 }));
|
|
10
11
|
return { architecturePath, uxPath };
|
|
11
12
|
}
|
|
13
|
+
// ── Store-based (new — works with FileSpecsStore or SqliteSpecsStore) ─────────
|
|
14
|
+
export async function writeSnapshotsViaStore(store, architecture, ux) {
|
|
15
|
+
await store.writeSpec("architecture.snapshot", yaml.dump(architecture, { noRefs: true, lineWidth: 120 }), "yaml");
|
|
16
|
+
await store.writeSpec("ux.snapshot", yaml.dump(ux, { noRefs: true, lineWidth: 120 }), "yaml");
|
|
17
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@toolbaux/guardian",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Architectural intelligence for codebases. Verify that AI-generated code matches your architectural intent.",
|
|
6
6
|
"keywords": [
|
|
@@ -53,6 +53,8 @@
|
|
|
53
53
|
"benchmark:llm": "tsx scripts/benchmark-llm-context/index.ts"
|
|
54
54
|
},
|
|
55
55
|
"dependencies": {
|
|
56
|
+
"@xenova/transformers": "^2.17.2",
|
|
57
|
+
"better-sqlite3": "^12.8.0",
|
|
56
58
|
"commander": "^12.1.0",
|
|
57
59
|
"dotenv": "^17.3.1",
|
|
58
60
|
"js-yaml": "^4.1.0",
|
|
@@ -67,6 +69,7 @@
|
|
|
67
69
|
"zod": "^3.23.8"
|
|
68
70
|
},
|
|
69
71
|
"devDependencies": {
|
|
72
|
+
"@types/better-sqlite3": "^7.6.13",
|
|
70
73
|
"@types/js-yaml": "^4.0.9",
|
|
71
74
|
"@types/node": "^20.11.30",
|
|
72
75
|
"@vitest/coverage-v8": "^4.1.0",
|