@toolbaux/guardian 0.1.22 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (37) hide show
  1. package/README.md +6 -4
  2. package/dist/adapters/runner.js +72 -3
  3. package/dist/adapters/typescript-adapter.js +24 -10
  4. package/dist/benchmarking/metrics/context-coverage.js +82 -0
  5. package/dist/benchmarking/metrics/drift-score.js +104 -0
  6. package/dist/benchmarking/metrics/search-recall.js +207 -0
  7. package/dist/benchmarking/metrics/token-efficiency.js +79 -0
  8. package/dist/benchmarking/report.js +131 -0
  9. package/dist/benchmarking/runner.js +175 -0
  10. package/dist/benchmarking/types.js +13 -0
  11. package/dist/cli.js +53 -10
  12. package/dist/commands/benchmark.js +62 -0
  13. package/dist/commands/context.js +87 -29
  14. package/dist/commands/discrepancy.js +1 -1
  15. package/dist/commands/doc-generate.js +1 -1
  16. package/dist/commands/doc-html.js +1 -1
  17. package/dist/commands/extract.js +4 -1
  18. package/dist/commands/feature-context.js +1 -1
  19. package/dist/commands/generate.js +83 -10
  20. package/dist/commands/init.js +89 -56
  21. package/dist/commands/intel.js +70 -1
  22. package/dist/commands/mcp-serve.js +155 -316
  23. package/dist/commands/search.js +642 -14
  24. package/dist/config.js +1 -0
  25. package/dist/db/embeddings.js +113 -0
  26. package/dist/db/file-specs-store.js +174 -0
  27. package/dist/db/fts-builder.js +390 -0
  28. package/dist/db/index.js +55 -0
  29. package/dist/db/specs-store.js +13 -0
  30. package/dist/db/sqlite-specs-store.js +934 -0
  31. package/dist/extract/codebase-intel.js +31 -2
  32. package/dist/extract/compress.js +70 -3
  33. package/dist/extract/context-block.js +11 -2
  34. package/dist/extract/function-intel.js +5 -2
  35. package/dist/extract/index.js +1 -23
  36. package/dist/extract/writer.js +6 -0
  37. package/package.json +4 -1
@@ -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 build CodebaseIntelligence, then write to disk.
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 disk.
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
+ }
@@ -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 score = 0.5 * (degree / maxDegree) +
336
- 0.3 * (crossRatio / maxCrossRatio) +
337
- 0.2 * cycleFlag;
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 crossEdges = architecture.dependencies.module_graph.filter(e => e.from !== e.to);
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
- allFunctions.push(...result.functions);
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)
@@ -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 allRoots = (architecture.project.roots ?? [projectRoot]).map((r) => path.isAbsolute(r) ? r : path.join(projectRoot, r));
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 = [
@@ -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.1.22",
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",