@toolbaux/guardian 0.1.4 → 0.1.6
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/dist/commands/context.js +10 -1
- package/dist/commands/generate.js +1 -1
- package/dist/config.js +7 -0
- package/dist/extract/context-block.js +32 -5
- package/dist/extract/index.js +106 -2
- package/dist/project-discovery.js +7 -1
- package/package.json +3 -2
package/dist/commands/context.js
CHANGED
|
@@ -12,11 +12,20 @@ export async function runContext(options) {
|
|
|
12
12
|
loadArchitectureDiff(inputDir),
|
|
13
13
|
loadHeatmap(inputDir)
|
|
14
14
|
]);
|
|
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 */ }
|
|
15
23
|
const content = renderContextBlock(architecture, ux, {
|
|
16
24
|
focusQuery: options.focus,
|
|
17
25
|
maxLines: normalizeMaxLines(options.maxLines),
|
|
18
26
|
diff,
|
|
19
|
-
heatmap
|
|
27
|
+
heatmap,
|
|
28
|
+
structuralIntelligence: si
|
|
20
29
|
});
|
|
21
30
|
if (!options.output) {
|
|
22
31
|
console.log(content);
|
|
@@ -144,7 +144,7 @@ function renderAiContextMarkdown(architecture, ux, options) {
|
|
|
144
144
|
}
|
|
145
145
|
lines.push(renderContextBlock(architecture, ux, {
|
|
146
146
|
focusQuery: options?.focusQuery,
|
|
147
|
-
maxLines: options?.maxLines ??
|
|
147
|
+
maxLines: options?.maxLines ?? 200,
|
|
148
148
|
structuralIntelligence: options?.structuralIntelligence
|
|
149
149
|
}));
|
|
150
150
|
lines.push("");
|
package/dist/config.js
CHANGED
|
@@ -7,6 +7,7 @@ const DEFAULT_CONFIG = {
|
|
|
7
7
|
root: "",
|
|
8
8
|
backendRoot: "",
|
|
9
9
|
frontendRoot: "",
|
|
10
|
+
roots: [],
|
|
10
11
|
discovery: {
|
|
11
12
|
enabled: true
|
|
12
13
|
},
|
|
@@ -137,6 +138,11 @@ function normalizeConfig(input, configDir) {
|
|
|
137
138
|
project.root = resolveMaybe(project.root) ?? "";
|
|
138
139
|
project.backendRoot = resolveMaybe(project.backendRoot) ?? "";
|
|
139
140
|
project.frontendRoot = resolveMaybe(project.frontendRoot) ?? "";
|
|
141
|
+
if (Array.isArray(project.roots) && configDir) {
|
|
142
|
+
project.roots = project.roots
|
|
143
|
+
.filter((r) => typeof r === "string" && r.trim().length > 0)
|
|
144
|
+
.map(r => path.resolve(configDir, r));
|
|
145
|
+
}
|
|
140
146
|
normalized.project = project;
|
|
141
147
|
}
|
|
142
148
|
if (input.python) {
|
|
@@ -271,6 +277,7 @@ function mergeConfig(base, override) {
|
|
|
271
277
|
root: override.project?.root ?? base.project?.root ?? "",
|
|
272
278
|
backendRoot: override.project?.backendRoot ?? base.project?.backendRoot ?? "",
|
|
273
279
|
frontendRoot: override.project?.frontendRoot ?? base.project?.frontendRoot ?? "",
|
|
280
|
+
roots: mergeArrays(base.project?.roots, override.project?.roots),
|
|
274
281
|
discovery: {
|
|
275
282
|
enabled: override.project?.discovery?.enabled ??
|
|
276
283
|
base.project?.discovery?.enabled ??
|
|
@@ -8,7 +8,36 @@ export function renderContextBlock(architecture, ux, options) {
|
|
|
8
8
|
lines.push("");
|
|
9
9
|
lines.push(`**Backend:** ${ormModels.length} ORM models + ${schemaModels} schemas · ${architecture.endpoints.length} endpoints · ${architecture.modules.length} modules`);
|
|
10
10
|
lines.push(`**Frontend:** ${ux.components.length} components · ${ux.pages.length} pages`);
|
|
11
|
+
// Show all roots if multi-root project
|
|
12
|
+
const roots = architecture.project.roots;
|
|
13
|
+
if (roots && roots.length > 2) {
|
|
14
|
+
lines.push(`**Roots:** ${roots.join(", ")}`);
|
|
15
|
+
}
|
|
11
16
|
lines.push("");
|
|
17
|
+
// Module map with key exports — the most useful section for AI context
|
|
18
|
+
const modulesWithExports = architecture.modules.filter(m => m.exports.length > 0 || m.files.length > 0);
|
|
19
|
+
if (modulesWithExports.length > 0) {
|
|
20
|
+
lines.push("### Module Map");
|
|
21
|
+
for (const mod of modulesWithExports) {
|
|
22
|
+
const allSymbols = mod.exports.flatMap(e => e.symbols).filter(Boolean);
|
|
23
|
+
const topSymbols = allSymbols.slice(0, 6);
|
|
24
|
+
const symbolStr = topSymbols.length > 0
|
|
25
|
+
? ` — exports: ${topSymbols.join(", ")}${allSymbols.length > 6 ? ` (+${allSymbols.length - 6} more)` : ""}`
|
|
26
|
+
: ` — ${mod.files.length} files`;
|
|
27
|
+
const epCount = mod.endpoints.length > 0 ? ` · ${mod.endpoints.length} endpoints` : "";
|
|
28
|
+
lines.push(`- **${mod.id}** (${mod.layer})${epCount}${symbolStr}`);
|
|
29
|
+
}
|
|
30
|
+
lines.push("");
|
|
31
|
+
}
|
|
32
|
+
// Cross-module dependencies
|
|
33
|
+
const crossEdges = architecture.dependencies.module_graph.filter(e => e.from !== e.to);
|
|
34
|
+
if (crossEdges.length > 0) {
|
|
35
|
+
lines.push("### Module Dependencies");
|
|
36
|
+
for (const edge of crossEdges.slice(0, 10)) {
|
|
37
|
+
lines.push(`- ${edge.from} → ${edge.to}`);
|
|
38
|
+
}
|
|
39
|
+
lines.push("");
|
|
40
|
+
}
|
|
12
41
|
const couplingFiles = pickTopCouplingFiles(architecture, options?.heatmap, 5);
|
|
13
42
|
if (couplingFiles.length > 0) {
|
|
14
43
|
lines.push("### High-Coupling Files");
|
|
@@ -103,11 +132,9 @@ export function renderContextBlock(architecture, ux, options) {
|
|
|
103
132
|
}
|
|
104
133
|
}
|
|
105
134
|
lines.push("<!-- /guardian:context -->");
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
}
|
|
110
|
-
return [...lines.slice(0, maxLines - 2), "- context truncated for line budget", "<!-- /guardian:context -->"].join("\n");
|
|
135
|
+
// No global truncation — each section self-limits via .slice().
|
|
136
|
+
// Every section is guaranteed to appear with at least a few entries.
|
|
137
|
+
return lines.join("\n");
|
|
111
138
|
}
|
|
112
139
|
function tokenize(value) {
|
|
113
140
|
return value
|
package/dist/extract/index.js
CHANGED
|
@@ -28,8 +28,20 @@ export async function buildSnapshots(options) {
|
|
|
28
28
|
const reportedFrontendRoot = formatOutputPath(resolvedFrontendRoot);
|
|
29
29
|
const reportedWorkspaceRoot = formatOutputPath(workspaceRoot);
|
|
30
30
|
const config = resolvedProject.config;
|
|
31
|
-
|
|
32
|
-
const
|
|
31
|
+
// Analyze all roots — run both analyzers on each root, then merge
|
|
32
|
+
const allRoots = resolvedProject.roots;
|
|
33
|
+
const backendResults = [];
|
|
34
|
+
const frontendResults = [];
|
|
35
|
+
for (const root of allRoots) {
|
|
36
|
+
const [be, fe] = await Promise.all([
|
|
37
|
+
analyzeBackend(root, config, workspaceRoot),
|
|
38
|
+
analyzeFrontend(root, config)
|
|
39
|
+
]);
|
|
40
|
+
backendResults.push(be);
|
|
41
|
+
frontendResults.push(fe);
|
|
42
|
+
}
|
|
43
|
+
const backend = mergeBackendAnalyses(backendResults, allRoots, workspaceRoot);
|
|
44
|
+
const frontend = mergeFrontendAnalyses(frontendResults, allRoots, workspaceRoot);
|
|
33
45
|
const projectRoot = workspaceRoot;
|
|
34
46
|
const runtime = await analyzeRuntime(workspaceRoot, config);
|
|
35
47
|
const projectName = deriveProjectName(resolvedBackendRoot);
|
|
@@ -82,6 +94,7 @@ export async function buildSnapshots(options) {
|
|
|
82
94
|
workspace_root: reportedWorkspaceRoot,
|
|
83
95
|
backend_root: reportedBackendRoot,
|
|
84
96
|
frontend_root: reportedFrontendRoot,
|
|
97
|
+
roots: resolvedProject.roots.map(formatOutputPath),
|
|
85
98
|
resolution_source: resolvedProject.resolutionSource,
|
|
86
99
|
entrypoints: backend.entrypoints
|
|
87
100
|
},
|
|
@@ -305,6 +318,97 @@ async function buildFunctionTestCoverage(params) {
|
|
|
305
318
|
};
|
|
306
319
|
});
|
|
307
320
|
}
|
|
321
|
+
function mergeBackendAnalyses(results, roots, workspaceRoot) {
|
|
322
|
+
if (results.length === 1)
|
|
323
|
+
return results[0];
|
|
324
|
+
// Prefix module IDs with root-relative path so they're globally unique
|
|
325
|
+
for (let i = 0; i < results.length; i++) {
|
|
326
|
+
const rootLabel = path.relative(workspaceRoot, roots[i]).replace(/\\/g, "/");
|
|
327
|
+
const idMap = new Map();
|
|
328
|
+
for (const mod of results[i].modules) {
|
|
329
|
+
const newId = `${rootLabel}/${mod.id}`;
|
|
330
|
+
idMap.set(mod.id, newId);
|
|
331
|
+
mod.id = newId;
|
|
332
|
+
mod.path = `${rootLabel}/${mod.path}`;
|
|
333
|
+
}
|
|
334
|
+
// Remap references in graph edges, endpoints, etc.
|
|
335
|
+
for (const edge of results[i].moduleGraph) {
|
|
336
|
+
edge.from = idMap.get(edge.from) ?? edge.from;
|
|
337
|
+
edge.to = idMap.get(edge.to) ?? edge.to;
|
|
338
|
+
}
|
|
339
|
+
for (const ep of results[i].endpoints) {
|
|
340
|
+
if (ep.module && idMap.has(ep.module)) {
|
|
341
|
+
ep.module = idMap.get(ep.module);
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
for (const cycle of results[i].circularDependencies) {
|
|
345
|
+
for (let j = 0; j < cycle.length; j++) {
|
|
346
|
+
cycle[j] = idMap.get(cycle[j]) ?? cycle[j];
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
results[i].orphanModules = results[i].orphanModules.map(m => idMap.get(m) ?? m);
|
|
350
|
+
const newUsage = {};
|
|
351
|
+
for (const [key, value] of Object.entries(results[i].moduleUsage)) {
|
|
352
|
+
newUsage[idMap.get(key) ?? key] = value;
|
|
353
|
+
}
|
|
354
|
+
results[i].moduleUsage = newUsage;
|
|
355
|
+
}
|
|
356
|
+
const moduleUsage = {};
|
|
357
|
+
for (const r of results) {
|
|
358
|
+
for (const [key, value] of Object.entries(r.moduleUsage)) {
|
|
359
|
+
moduleUsage[key] = (moduleUsage[key] ?? 0) + value;
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
// Merge testCoverage: combine arrays across all roots
|
|
363
|
+
const mergedCoverage = { ...results[0].testCoverage };
|
|
364
|
+
mergedCoverage.untested_source_files = [...mergedCoverage.untested_source_files];
|
|
365
|
+
mergedCoverage.test_files_missing_source = [...mergedCoverage.test_files_missing_source];
|
|
366
|
+
mergedCoverage.coverage_map = [...mergedCoverage.coverage_map];
|
|
367
|
+
for (let i = 1; i < results.length; i++) {
|
|
368
|
+
const tc = results[i].testCoverage;
|
|
369
|
+
mergedCoverage.untested_source_files.push(...tc.untested_source_files);
|
|
370
|
+
mergedCoverage.test_files_missing_source.push(...tc.test_files_missing_source);
|
|
371
|
+
mergedCoverage.coverage_map.push(...tc.coverage_map);
|
|
372
|
+
}
|
|
373
|
+
return {
|
|
374
|
+
modules: results.flatMap(r => r.modules),
|
|
375
|
+
moduleGraph: results.flatMap(r => r.moduleGraph),
|
|
376
|
+
fileGraph: results.flatMap(r => r.fileGraph),
|
|
377
|
+
endpoints: results.flatMap(r => r.endpoints),
|
|
378
|
+
dataModels: results.flatMap(r => r.dataModels),
|
|
379
|
+
enums: results.flatMap(r => r.enums),
|
|
380
|
+
constants: results.flatMap(r => r.constants),
|
|
381
|
+
endpointModelUsage: results.flatMap(r => r.endpointModelUsage),
|
|
382
|
+
tasks: results.flatMap(r => r.tasks),
|
|
383
|
+
circularDependencies: results.flatMap(r => r.circularDependencies),
|
|
384
|
+
orphanModules: results.flatMap(r => r.orphanModules),
|
|
385
|
+
orphanFiles: results.flatMap(r => r.orphanFiles),
|
|
386
|
+
moduleUsage,
|
|
387
|
+
unusedExports: results.flatMap(r => r.unusedExports),
|
|
388
|
+
unusedEndpoints: results.flatMap(r => r.unusedEndpoints),
|
|
389
|
+
entrypoints: results.flatMap(r => r.entrypoints),
|
|
390
|
+
duplicateFunctions: results.flatMap(r => r.duplicateFunctions),
|
|
391
|
+
similarFunctions: results.flatMap(r => r.similarFunctions),
|
|
392
|
+
testCoverage: mergedCoverage,
|
|
393
|
+
tests: results.flatMap(r => r.tests)
|
|
394
|
+
};
|
|
395
|
+
}
|
|
396
|
+
function mergeFrontendAnalyses(results, _roots, _workspaceRoot) {
|
|
397
|
+
if (results.length === 1)
|
|
398
|
+
return results[0];
|
|
399
|
+
return {
|
|
400
|
+
files: results.flatMap(r => r.files),
|
|
401
|
+
pages: results.flatMap(r => r.pages),
|
|
402
|
+
apiCalls: results.flatMap(r => r.apiCalls),
|
|
403
|
+
uxPages: results.flatMap(r => r.uxPages),
|
|
404
|
+
components: results.flatMap(r => r.components),
|
|
405
|
+
componentGraph: results.flatMap(r => r.componentGraph),
|
|
406
|
+
fileGraph: results.flatMap(r => r.fileGraph),
|
|
407
|
+
orphanFiles: results.flatMap(r => r.orphanFiles),
|
|
408
|
+
unusedExports: results.flatMap(r => r.unusedExports),
|
|
409
|
+
tests: results.flatMap(r => r.tests)
|
|
410
|
+
};
|
|
411
|
+
}
|
|
308
412
|
function findCommonRoot(paths) {
|
|
309
413
|
if (paths.length === 0) {
|
|
310
414
|
return process.cwd();
|
|
@@ -53,16 +53,22 @@ export async function resolveProjectPaths(options) {
|
|
|
53
53
|
(await chooseBackendRoot(workspaceRoot));
|
|
54
54
|
const frontendRoot = explicitFrontend ??
|
|
55
55
|
(await chooseFrontendRoot(workspaceRoot));
|
|
56
|
+
// Build unified roots list: backendRoot + frontendRoot + any extra from config
|
|
57
|
+
const configRoots = (config.project?.roots ?? []).map(r => path.resolve(r));
|
|
58
|
+
const allRoots = [...new Set([backendRoot, frontendRoot, ...configRoots].filter(Boolean))];
|
|
56
59
|
return {
|
|
57
60
|
workspaceRoot,
|
|
58
61
|
backendRoot,
|
|
59
62
|
frontendRoot,
|
|
63
|
+
roots: allRoots,
|
|
60
64
|
resolutionSource,
|
|
61
65
|
config
|
|
62
66
|
};
|
|
63
67
|
}
|
|
64
68
|
export function logResolvedProjectPaths(resolved) {
|
|
65
|
-
|
|
69
|
+
const extra = resolved.roots.filter(r => r !== resolved.backendRoot && r !== resolved.frontendRoot);
|
|
70
|
+
const extraMsg = extra.length > 0 ? ` +${extra.length} roots` : "";
|
|
71
|
+
console.log(`Guardian roots (${resolved.resolutionSource}): workspace=${resolved.workspaceRoot} backend=${resolved.backendRoot} frontend=${resolved.frontendRoot}${extraMsg}`);
|
|
66
72
|
}
|
|
67
73
|
async function chooseBackendRoot(workspaceRoot) {
|
|
68
74
|
const candidates = await discoverBackendCandidates(workspaceRoot);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@toolbaux/guardian",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.6",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Architectural intelligence for codebases. Verify that AI-generated code matches your architectural intent.",
|
|
6
6
|
"keywords": [
|
|
@@ -41,7 +41,8 @@
|
|
|
41
41
|
},
|
|
42
42
|
"scripts": {
|
|
43
43
|
"build": "tsc -p tsconfig.json",
|
|
44
|
-
"prepublishOnly": "npm run build",
|
|
44
|
+
"prepublishOnly": "npm run test:critical && npm run build",
|
|
45
|
+
"test:critical": "vitest run tests/critical/",
|
|
45
46
|
"dev": "tsx src/cli.ts",
|
|
46
47
|
"start": "node dist/cli.js",
|
|
47
48
|
"typecheck": "tsc -p tsconfig.json --noEmit",
|