@toolbaux/guardian 0.1.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/LICENSE +21 -0
- package/README.md +366 -0
- package/dist/adapters/csharp-adapter.js +149 -0
- package/dist/adapters/go-adapter.js +96 -0
- package/dist/adapters/index.js +16 -0
- package/dist/adapters/java-adapter.js +122 -0
- package/dist/adapters/python-adapter.js +183 -0
- package/dist/adapters/runner.js +69 -0
- package/dist/adapters/types.js +1 -0
- package/dist/adapters/typescript-adapter.js +179 -0
- package/dist/benchmarking/framework.js +91 -0
- package/dist/cli.js +343 -0
- package/dist/commands/analyze-depth.js +43 -0
- package/dist/commands/api-spec-extractor.js +52 -0
- package/dist/commands/breaking-change-analyzer.js +334 -0
- package/dist/commands/config-compliance.js +219 -0
- package/dist/commands/constraints.js +221 -0
- package/dist/commands/context.js +101 -0
- package/dist/commands/data-flow-tracer.js +291 -0
- package/dist/commands/dependency-impact-analyzer.js +27 -0
- package/dist/commands/diff.js +146 -0
- package/dist/commands/discrepancy.js +71 -0
- package/dist/commands/doc-generate.js +163 -0
- package/dist/commands/doc-html.js +120 -0
- package/dist/commands/drift.js +88 -0
- package/dist/commands/extract.js +16 -0
- package/dist/commands/feature-context.js +116 -0
- package/dist/commands/generate.js +339 -0
- package/dist/commands/guard.js +182 -0
- package/dist/commands/init.js +209 -0
- package/dist/commands/intel.js +20 -0
- package/dist/commands/license-dependency-auditor.js +33 -0
- package/dist/commands/performance-hotspot-profiler.js +42 -0
- package/dist/commands/search.js +314 -0
- package/dist/commands/security-boundary-auditor.js +359 -0
- package/dist/commands/simulate.js +294 -0
- package/dist/commands/summary.js +27 -0
- package/dist/commands/test-coverage-mapper.js +264 -0
- package/dist/commands/verify-drift.js +62 -0
- package/dist/config.js +441 -0
- package/dist/extract/ai-context-hints.js +107 -0
- package/dist/extract/analyzers/backend.js +1704 -0
- package/dist/extract/analyzers/depth.js +264 -0
- package/dist/extract/analyzers/frontend.js +2221 -0
- package/dist/extract/api-usage-tracker.js +19 -0
- package/dist/extract/cache.js +53 -0
- package/dist/extract/codebase-intel.js +190 -0
- package/dist/extract/compress.js +452 -0
- package/dist/extract/context-block.js +356 -0
- package/dist/extract/contracts.js +183 -0
- package/dist/extract/discrepancies.js +233 -0
- package/dist/extract/docs-loader.js +110 -0
- package/dist/extract/docs.js +2379 -0
- package/dist/extract/drift.js +1578 -0
- package/dist/extract/duplicates.js +435 -0
- package/dist/extract/feature-arcs.js +138 -0
- package/dist/extract/graph.js +76 -0
- package/dist/extract/html-doc.js +1409 -0
- package/dist/extract/ignore.js +45 -0
- package/dist/extract/index.js +455 -0
- package/dist/extract/llm-client.js +159 -0
- package/dist/extract/pattern-registry.js +141 -0
- package/dist/extract/product-doc.js +497 -0
- package/dist/extract/python.js +1202 -0
- package/dist/extract/runtime.js +193 -0
- package/dist/extract/schema-evolution-validator.js +35 -0
- package/dist/extract/test-gap-analyzer.js +20 -0
- package/dist/extract/tests.js +74 -0
- package/dist/extract/types.js +1 -0
- package/dist/extract/validate-backend.js +30 -0
- package/dist/extract/writer.js +11 -0
- package/dist/output-layout.js +37 -0
- package/dist/project-discovery.js +309 -0
- package/dist/schema/architecture.js +350 -0
- package/dist/schema/feature-spec.js +89 -0
- package/dist/schema/index.js +8 -0
- package/dist/schema/ux.js +46 -0
- package/package.json +75 -0
package/dist/config.js
ADDED
|
@@ -0,0 +1,441 @@
|
|
|
1
|
+
import fs from "node:fs/promises";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
const DEFAULT_CONFIG = {
|
|
4
|
+
project: {
|
|
5
|
+
root: "",
|
|
6
|
+
backendRoot: "",
|
|
7
|
+
frontendRoot: "",
|
|
8
|
+
discovery: {
|
|
9
|
+
enabled: true
|
|
10
|
+
},
|
|
11
|
+
description: "",
|
|
12
|
+
readmePath: ""
|
|
13
|
+
},
|
|
14
|
+
ignore: {
|
|
15
|
+
directories: [
|
|
16
|
+
".git",
|
|
17
|
+
"node_modules",
|
|
18
|
+
"dist",
|
|
19
|
+
"build",
|
|
20
|
+
".next",
|
|
21
|
+
".venv",
|
|
22
|
+
"venv",
|
|
23
|
+
"__pycache__",
|
|
24
|
+
".pytest_cache",
|
|
25
|
+
".mypy_cache",
|
|
26
|
+
"coverage",
|
|
27
|
+
"htmlcov",
|
|
28
|
+
"logs",
|
|
29
|
+
"log",
|
|
30
|
+
"tmp",
|
|
31
|
+
"cache",
|
|
32
|
+
"specs-out",
|
|
33
|
+
"ghost-out"
|
|
34
|
+
],
|
|
35
|
+
paths: []
|
|
36
|
+
},
|
|
37
|
+
python: {
|
|
38
|
+
absoluteImportRoots: []
|
|
39
|
+
},
|
|
40
|
+
frontend: {
|
|
41
|
+
routeDirs: ["src/routes", "src/pages", "routes", "pages"],
|
|
42
|
+
aliases: {},
|
|
43
|
+
tsconfigPath: ""
|
|
44
|
+
},
|
|
45
|
+
drift: {
|
|
46
|
+
graphLevel: "module",
|
|
47
|
+
scales: ["module", "file", "function"],
|
|
48
|
+
weights: {
|
|
49
|
+
entropy: 0.4,
|
|
50
|
+
crossLayer: 0.3,
|
|
51
|
+
cycles: 0.2,
|
|
52
|
+
modularity: 0.1
|
|
53
|
+
},
|
|
54
|
+
layers: {},
|
|
55
|
+
domains: {},
|
|
56
|
+
capacity: {
|
|
57
|
+
layers: {},
|
|
58
|
+
total: 0,
|
|
59
|
+
warningRatio: 0.85,
|
|
60
|
+
criticalRatio: 1.0
|
|
61
|
+
},
|
|
62
|
+
growth: {
|
|
63
|
+
maxEdgesPerHour: 0,
|
|
64
|
+
maxEdgesPerDay: 0,
|
|
65
|
+
maxEdgeGrowthRatio: 0
|
|
66
|
+
},
|
|
67
|
+
baselinePath: "",
|
|
68
|
+
historyPath: "",
|
|
69
|
+
criticalDelta: 0.25
|
|
70
|
+
},
|
|
71
|
+
guard: {
|
|
72
|
+
mode: "soft"
|
|
73
|
+
},
|
|
74
|
+
llm: {
|
|
75
|
+
command: "",
|
|
76
|
+
args: [],
|
|
77
|
+
timeoutMs: 120000,
|
|
78
|
+
promptTemplate: ""
|
|
79
|
+
},
|
|
80
|
+
docs: {
|
|
81
|
+
mode: "lean",
|
|
82
|
+
internalDir: "internal"
|
|
83
|
+
}
|
|
84
|
+
};
|
|
85
|
+
export async function loadSpecGuardConfig(options) {
|
|
86
|
+
const configPath = await resolveConfigPath(options);
|
|
87
|
+
if (!configPath) {
|
|
88
|
+
return DEFAULT_CONFIG;
|
|
89
|
+
}
|
|
90
|
+
let parsed = {};
|
|
91
|
+
try {
|
|
92
|
+
const raw = await fs.readFile(configPath, "utf8");
|
|
93
|
+
parsed = normalizeConfig(JSON.parse(raw), path.dirname(configPath));
|
|
94
|
+
}
|
|
95
|
+
catch (error) {
|
|
96
|
+
throw new Error(`Failed to read config at ${configPath}: ${String(error)}`);
|
|
97
|
+
}
|
|
98
|
+
return mergeConfig(DEFAULT_CONFIG, parsed);
|
|
99
|
+
}
|
|
100
|
+
function normalizeConfig(input, configDir) {
|
|
101
|
+
const normalized = { ...input };
|
|
102
|
+
if (input.project) {
|
|
103
|
+
const project = { ...input.project };
|
|
104
|
+
if (!project.backendRoot && typeof project.backend_root !== "undefined") {
|
|
105
|
+
project.backendRoot = project.backend_root;
|
|
106
|
+
}
|
|
107
|
+
delete project.backend_root;
|
|
108
|
+
if (!project.frontendRoot && typeof project.frontend_root !== "undefined") {
|
|
109
|
+
project.frontendRoot = project.frontend_root;
|
|
110
|
+
}
|
|
111
|
+
delete project.frontend_root;
|
|
112
|
+
if (project.discovery && typeof project.discovery === "object") {
|
|
113
|
+
const discovery = { ...project.discovery };
|
|
114
|
+
if (typeof discovery.enabled === "undefined" &&
|
|
115
|
+
typeof discovery.auto_detect !== "undefined") {
|
|
116
|
+
discovery.enabled = discovery.auto_detect;
|
|
117
|
+
}
|
|
118
|
+
delete discovery.auto_detect;
|
|
119
|
+
project.discovery = discovery;
|
|
120
|
+
}
|
|
121
|
+
const resolveMaybe = (value) => {
|
|
122
|
+
if (typeof value !== "string" || value.trim().length === 0) {
|
|
123
|
+
return undefined;
|
|
124
|
+
}
|
|
125
|
+
return configDir ? path.resolve(configDir, value) : value;
|
|
126
|
+
};
|
|
127
|
+
project.root = resolveMaybe(project.root) ?? "";
|
|
128
|
+
project.backendRoot = resolveMaybe(project.backendRoot) ?? "";
|
|
129
|
+
project.frontendRoot = resolveMaybe(project.frontendRoot) ?? "";
|
|
130
|
+
normalized.project = project;
|
|
131
|
+
}
|
|
132
|
+
if (input.python) {
|
|
133
|
+
const python = { ...input.python };
|
|
134
|
+
if (!python.absoluteImportRoots && typeof python.absolute_import_roots !== "undefined") {
|
|
135
|
+
python.absoluteImportRoots = python.absolute_import_roots;
|
|
136
|
+
}
|
|
137
|
+
delete python.absolute_import_roots;
|
|
138
|
+
normalized.python = python;
|
|
139
|
+
}
|
|
140
|
+
if (input.frontend) {
|
|
141
|
+
const frontend = { ...input.frontend };
|
|
142
|
+
if (!frontend.routeDirs && typeof frontend.route_dirs !== "undefined") {
|
|
143
|
+
frontend.routeDirs = frontend.route_dirs;
|
|
144
|
+
}
|
|
145
|
+
delete frontend.route_dirs;
|
|
146
|
+
if (!frontend.tsconfigPath && typeof frontend.tsconfig_path !== "undefined") {
|
|
147
|
+
frontend.tsconfigPath = frontend.tsconfig_path;
|
|
148
|
+
}
|
|
149
|
+
delete frontend.tsconfig_path;
|
|
150
|
+
normalized.frontend = frontend;
|
|
151
|
+
}
|
|
152
|
+
if (input.drift) {
|
|
153
|
+
const drift = { ...input.drift };
|
|
154
|
+
if (!drift.graphLevel && typeof drift.graph_level !== "undefined") {
|
|
155
|
+
drift.graphLevel = drift.graph_level;
|
|
156
|
+
}
|
|
157
|
+
delete drift.graph_level;
|
|
158
|
+
if (!drift.scales && typeof drift.scale_levels !== "undefined") {
|
|
159
|
+
drift.scales = drift.scale_levels;
|
|
160
|
+
}
|
|
161
|
+
delete drift.scale_levels;
|
|
162
|
+
if (!drift.baselinePath && typeof drift.baseline_path !== "undefined") {
|
|
163
|
+
drift.baselinePath = drift.baseline_path;
|
|
164
|
+
}
|
|
165
|
+
delete drift.baseline_path;
|
|
166
|
+
if (!drift.historyPath && typeof drift.history_path !== "undefined") {
|
|
167
|
+
drift.historyPath = drift.history_path;
|
|
168
|
+
}
|
|
169
|
+
delete drift.history_path;
|
|
170
|
+
if (!drift.domains && typeof drift.domain_map !== "undefined") {
|
|
171
|
+
drift.domains = drift.domain_map;
|
|
172
|
+
}
|
|
173
|
+
delete drift.domain_map;
|
|
174
|
+
if (drift.capacity && typeof drift.capacity === "object") {
|
|
175
|
+
const capacity = { ...drift.capacity };
|
|
176
|
+
if (!capacity.warningRatio && typeof capacity.warning_ratio !== "undefined") {
|
|
177
|
+
capacity.warningRatio = capacity.warning_ratio;
|
|
178
|
+
}
|
|
179
|
+
delete capacity.warning_ratio;
|
|
180
|
+
if (!capacity.criticalRatio && typeof capacity.critical_ratio !== "undefined") {
|
|
181
|
+
capacity.criticalRatio = capacity.critical_ratio;
|
|
182
|
+
}
|
|
183
|
+
delete capacity.critical_ratio;
|
|
184
|
+
if (!capacity.total && typeof capacity.total_edges !== "undefined") {
|
|
185
|
+
capacity.total = capacity.total_edges;
|
|
186
|
+
}
|
|
187
|
+
delete capacity.total_edges;
|
|
188
|
+
drift.capacity = capacity;
|
|
189
|
+
}
|
|
190
|
+
if (drift.growth && typeof drift.growth === "object") {
|
|
191
|
+
const growth = { ...drift.growth };
|
|
192
|
+
if (!growth.maxEdgesPerHour && typeof growth.max_edges_per_hour !== "undefined") {
|
|
193
|
+
growth.maxEdgesPerHour = growth.max_edges_per_hour;
|
|
194
|
+
}
|
|
195
|
+
delete growth.max_edges_per_hour;
|
|
196
|
+
if (!growth.maxEdgesPerDay && typeof growth.max_edges_per_day !== "undefined") {
|
|
197
|
+
growth.maxEdgesPerDay = growth.max_edges_per_day;
|
|
198
|
+
}
|
|
199
|
+
delete growth.max_edges_per_day;
|
|
200
|
+
if (!growth.maxEdgeGrowthRatio && typeof growth.max_edge_growth_ratio !== "undefined") {
|
|
201
|
+
growth.maxEdgeGrowthRatio = growth.max_edge_growth_ratio;
|
|
202
|
+
}
|
|
203
|
+
delete growth.max_edge_growth_ratio;
|
|
204
|
+
drift.growth = growth;
|
|
205
|
+
}
|
|
206
|
+
if (!drift.criticalDelta && typeof drift.critical_delta !== "undefined") {
|
|
207
|
+
drift.criticalDelta = drift.critical_delta;
|
|
208
|
+
}
|
|
209
|
+
delete drift.critical_delta;
|
|
210
|
+
if (drift.weights && typeof drift.weights === "object") {
|
|
211
|
+
const weights = { ...drift.weights };
|
|
212
|
+
if (!weights.crossLayer && typeof weights.cross_layer !== "undefined") {
|
|
213
|
+
weights.crossLayer = weights.cross_layer;
|
|
214
|
+
}
|
|
215
|
+
delete weights.cross_layer;
|
|
216
|
+
drift.weights = weights;
|
|
217
|
+
}
|
|
218
|
+
normalized.drift = drift;
|
|
219
|
+
}
|
|
220
|
+
if (input.guard) {
|
|
221
|
+
const guard = { ...input.guard };
|
|
222
|
+
if (!guard.mode && typeof guard.guard_mode !== "undefined") {
|
|
223
|
+
guard.mode = guard.guard_mode;
|
|
224
|
+
}
|
|
225
|
+
delete guard.guard_mode;
|
|
226
|
+
normalized.guard = guard;
|
|
227
|
+
}
|
|
228
|
+
if (input.llm) {
|
|
229
|
+
const llm = { ...input.llm };
|
|
230
|
+
if (!llm.timeoutMs && typeof llm.timeout_ms !== "undefined") {
|
|
231
|
+
llm.timeoutMs = llm.timeout_ms;
|
|
232
|
+
}
|
|
233
|
+
delete llm.timeout_ms;
|
|
234
|
+
if (!llm.promptTemplate && typeof llm.prompt_template !== "undefined") {
|
|
235
|
+
llm.promptTemplate = llm.prompt_template;
|
|
236
|
+
}
|
|
237
|
+
delete llm.prompt_template;
|
|
238
|
+
if (!llm.args && typeof llm.arguments !== "undefined") {
|
|
239
|
+
llm.args = llm.arguments;
|
|
240
|
+
}
|
|
241
|
+
delete llm.arguments;
|
|
242
|
+
normalized.llm = llm;
|
|
243
|
+
}
|
|
244
|
+
if (input.docs) {
|
|
245
|
+
const docs = { ...input.docs };
|
|
246
|
+
if (!docs.mode && typeof docs.docs_mode !== "undefined") {
|
|
247
|
+
docs.mode = docs.docs_mode;
|
|
248
|
+
}
|
|
249
|
+
delete docs.docs_mode;
|
|
250
|
+
if (!docs.internalDir && typeof docs.internal_dir !== "undefined") {
|
|
251
|
+
docs.internalDir = docs.internal_dir;
|
|
252
|
+
}
|
|
253
|
+
delete docs.internal_dir;
|
|
254
|
+
normalized.docs = docs;
|
|
255
|
+
}
|
|
256
|
+
return normalized;
|
|
257
|
+
}
|
|
258
|
+
function mergeConfig(base, override) {
|
|
259
|
+
return {
|
|
260
|
+
project: {
|
|
261
|
+
root: override.project?.root ?? base.project?.root ?? "",
|
|
262
|
+
backendRoot: override.project?.backendRoot ?? base.project?.backendRoot ?? "",
|
|
263
|
+
frontendRoot: override.project?.frontendRoot ?? base.project?.frontendRoot ?? "",
|
|
264
|
+
discovery: {
|
|
265
|
+
enabled: override.project?.discovery?.enabled ??
|
|
266
|
+
base.project?.discovery?.enabled ??
|
|
267
|
+
true
|
|
268
|
+
},
|
|
269
|
+
description: override.project?.description ?? base.project?.description ?? "",
|
|
270
|
+
readmePath: override.project?.readmePath ?? base.project?.readmePath ?? ""
|
|
271
|
+
},
|
|
272
|
+
ignore: {
|
|
273
|
+
directories: mergeArrays(base.ignore?.directories, override.ignore?.directories),
|
|
274
|
+
paths: mergeArrays(base.ignore?.paths, override.ignore?.paths)
|
|
275
|
+
},
|
|
276
|
+
python: {
|
|
277
|
+
absoluteImportRoots: mergeArrays(base.python?.absoluteImportRoots, override.python?.absoluteImportRoots)
|
|
278
|
+
},
|
|
279
|
+
frontend: {
|
|
280
|
+
routeDirs: mergeArrays(base.frontend?.routeDirs, override.frontend?.routeDirs),
|
|
281
|
+
aliases: {
|
|
282
|
+
...(base.frontend?.aliases ?? {}),
|
|
283
|
+
...(override.frontend?.aliases ?? {})
|
|
284
|
+
},
|
|
285
|
+
tsconfigPath: override.frontend?.tsconfigPath || base.frontend?.tsconfigPath || ""
|
|
286
|
+
},
|
|
287
|
+
drift: {
|
|
288
|
+
graphLevel: override.drift?.graphLevel || base.drift?.graphLevel || "module",
|
|
289
|
+
scales: mergeArrays(base.drift?.scales, override.drift?.scales),
|
|
290
|
+
weights: {
|
|
291
|
+
entropy: override.drift?.weights?.entropy ?? base.drift?.weights?.entropy ?? 0.4,
|
|
292
|
+
crossLayer: override.drift?.weights?.crossLayer ?? base.drift?.weights?.crossLayer ?? 0.3,
|
|
293
|
+
cycles: override.drift?.weights?.cycles ?? base.drift?.weights?.cycles ?? 0.2,
|
|
294
|
+
modularity: override.drift?.weights?.modularity ?? base.drift?.weights?.modularity ?? 0.1
|
|
295
|
+
},
|
|
296
|
+
layers: {
|
|
297
|
+
...(base.drift?.layers ?? {}),
|
|
298
|
+
...(override.drift?.layers ?? {})
|
|
299
|
+
},
|
|
300
|
+
domains: {
|
|
301
|
+
...(base.drift?.domains ?? {}),
|
|
302
|
+
...(override.drift?.domains ?? {})
|
|
303
|
+
},
|
|
304
|
+
capacity: {
|
|
305
|
+
layers: {
|
|
306
|
+
...(base.drift?.capacity?.layers ?? {}),
|
|
307
|
+
...(override.drift?.capacity?.layers ?? {})
|
|
308
|
+
},
|
|
309
|
+
total: override.drift?.capacity?.total ?? base.drift?.capacity?.total ?? 0,
|
|
310
|
+
warningRatio: override.drift?.capacity?.warningRatio ??
|
|
311
|
+
base.drift?.capacity?.warningRatio ??
|
|
312
|
+
0.85,
|
|
313
|
+
criticalRatio: override.drift?.capacity?.criticalRatio ??
|
|
314
|
+
base.drift?.capacity?.criticalRatio ??
|
|
315
|
+
1.0
|
|
316
|
+
},
|
|
317
|
+
growth: {
|
|
318
|
+
maxEdgesPerHour: override.drift?.growth?.maxEdgesPerHour ??
|
|
319
|
+
base.drift?.growth?.maxEdgesPerHour ??
|
|
320
|
+
0,
|
|
321
|
+
maxEdgesPerDay: override.drift?.growth?.maxEdgesPerDay ??
|
|
322
|
+
base.drift?.growth?.maxEdgesPerDay ??
|
|
323
|
+
0,
|
|
324
|
+
maxEdgeGrowthRatio: override.drift?.growth?.maxEdgeGrowthRatio ??
|
|
325
|
+
base.drift?.growth?.maxEdgeGrowthRatio ??
|
|
326
|
+
0
|
|
327
|
+
},
|
|
328
|
+
baselinePath: override.drift?.baselinePath || base.drift?.baselinePath || "",
|
|
329
|
+
historyPath: override.drift?.historyPath || base.drift?.historyPath || "",
|
|
330
|
+
criticalDelta: override.drift?.criticalDelta ?? base.drift?.criticalDelta ?? 0.25
|
|
331
|
+
},
|
|
332
|
+
guard: {
|
|
333
|
+
mode: override.guard?.mode || base.guard?.mode || "soft"
|
|
334
|
+
},
|
|
335
|
+
llm: {
|
|
336
|
+
command: override.llm?.command ?? base.llm?.command ?? "",
|
|
337
|
+
args: mergeArrays(base.llm?.args, override.llm?.args),
|
|
338
|
+
timeoutMs: override.llm?.timeoutMs ?? base.llm?.timeoutMs ?? 120000,
|
|
339
|
+
promptTemplate: override.llm?.promptTemplate ?? base.llm?.promptTemplate ?? ""
|
|
340
|
+
},
|
|
341
|
+
docs: {
|
|
342
|
+
mode: override.docs?.mode ?? base.docs?.mode ?? "lean",
|
|
343
|
+
internalDir: override.docs?.internalDir ?? base.docs?.internalDir ?? "internal"
|
|
344
|
+
}
|
|
345
|
+
};
|
|
346
|
+
}
|
|
347
|
+
function mergeArrays(base, override) {
|
|
348
|
+
const result = new Set();
|
|
349
|
+
for (const entry of base ?? []) {
|
|
350
|
+
result.add(entry);
|
|
351
|
+
}
|
|
352
|
+
for (const entry of override ?? []) {
|
|
353
|
+
result.add(entry);
|
|
354
|
+
}
|
|
355
|
+
return Array.from(result);
|
|
356
|
+
}
|
|
357
|
+
async function resolveConfigPath(options) {
|
|
358
|
+
if (options.configPath) {
|
|
359
|
+
const resolved = path.resolve(options.configPath);
|
|
360
|
+
const stat = await safeStat(resolved);
|
|
361
|
+
if (stat?.isDirectory()) {
|
|
362
|
+
for (const name of ["guardian.config.json", "specguard.config.json"]) {
|
|
363
|
+
const candidate = path.join(resolved, name);
|
|
364
|
+
if (await fileExists(candidate)) {
|
|
365
|
+
return candidate;
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
throw new Error(`guardian.config.json not found in ${resolved}`);
|
|
369
|
+
}
|
|
370
|
+
if (stat?.isFile()) {
|
|
371
|
+
return resolved;
|
|
372
|
+
}
|
|
373
|
+
throw new Error(`Config path not found: ${resolved}`);
|
|
374
|
+
}
|
|
375
|
+
const roots = uniquePaths([
|
|
376
|
+
options.projectRoot,
|
|
377
|
+
options.backendRoot,
|
|
378
|
+
options.frontendRoot
|
|
379
|
+
].filter((entry) => typeof entry === "string" && entry.length > 0));
|
|
380
|
+
const commonRoot = roots.length > 0 ? findCommonRoot(roots) : process.cwd();
|
|
381
|
+
// Check guardian.config.json first, fall back to specguard.config.json
|
|
382
|
+
const guardianCandidate = path.join(commonRoot, "guardian.config.json");
|
|
383
|
+
if (await fileExists(guardianCandidate)) {
|
|
384
|
+
return guardianCandidate;
|
|
385
|
+
}
|
|
386
|
+
const specguardCandidate = path.join(commonRoot, "specguard.config.json");
|
|
387
|
+
if (await fileExists(specguardCandidate)) {
|
|
388
|
+
return specguardCandidate;
|
|
389
|
+
}
|
|
390
|
+
return null;
|
|
391
|
+
}
|
|
392
|
+
function findCommonRoot(paths) {
|
|
393
|
+
if (paths.length === 0) {
|
|
394
|
+
return process.cwd();
|
|
395
|
+
}
|
|
396
|
+
const splitPaths = paths.map((p) => path.resolve(p).split(path.sep));
|
|
397
|
+
const minLength = Math.min(...splitPaths.map((parts) => parts.length));
|
|
398
|
+
const shared = [];
|
|
399
|
+
for (let i = 0; i < minLength; i += 1) {
|
|
400
|
+
const segment = splitPaths[0][i];
|
|
401
|
+
if (splitPaths.every((parts) => parts[i] === segment)) {
|
|
402
|
+
shared.push(segment);
|
|
403
|
+
}
|
|
404
|
+
else {
|
|
405
|
+
break;
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
if (shared.length === 0) {
|
|
409
|
+
return path.parse(paths[0]).root;
|
|
410
|
+
}
|
|
411
|
+
return shared.join(path.sep);
|
|
412
|
+
}
|
|
413
|
+
function uniquePaths(paths) {
|
|
414
|
+
const seen = new Set();
|
|
415
|
+
const result = [];
|
|
416
|
+
for (const entry of paths) {
|
|
417
|
+
const resolved = path.resolve(entry);
|
|
418
|
+
if (!seen.has(resolved)) {
|
|
419
|
+
seen.add(resolved);
|
|
420
|
+
result.push(resolved);
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
return result;
|
|
424
|
+
}
|
|
425
|
+
async function fileExists(filePath) {
|
|
426
|
+
try {
|
|
427
|
+
const stat = await fs.stat(filePath);
|
|
428
|
+
return stat.isFile();
|
|
429
|
+
}
|
|
430
|
+
catch {
|
|
431
|
+
return false;
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
async function safeStat(filePath) {
|
|
435
|
+
try {
|
|
436
|
+
return await fs.stat(filePath);
|
|
437
|
+
}
|
|
438
|
+
catch {
|
|
439
|
+
return null;
|
|
440
|
+
}
|
|
441
|
+
}
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Small utilities that treat architecture-context.md as the planning surface.
|
|
3
|
+
* Used to validate how far compact AI context can carry implementation work.
|
|
4
|
+
*/
|
|
5
|
+
const COUPLING_LINE = /^-\s+(.+?)\s+\(score\s+([\d.]+)\)\s*$/;
|
|
6
|
+
function tokenizeQuery(query) {
|
|
7
|
+
return query
|
|
8
|
+
.toLowerCase()
|
|
9
|
+
.split(/[^a-z0-9/_-]+/i)
|
|
10
|
+
.filter((t) => t.length > 0);
|
|
11
|
+
}
|
|
12
|
+
/** Strip leading "<package>/" so paths match the in-repo layout. */
|
|
13
|
+
export function toRepoRelativePath(absoluteFromContext, packageName) {
|
|
14
|
+
const prefix = `${packageName}/`;
|
|
15
|
+
return absoluteFromContext.startsWith(prefix)
|
|
16
|
+
? absoluteFromContext.slice(prefix.length)
|
|
17
|
+
: absoluteFromContext;
|
|
18
|
+
}
|
|
19
|
+
export function parseArchitectureContext(markdown) {
|
|
20
|
+
const projectMatch = markdown.match(/Project:\s+\*\*([^*]+)\*\*/);
|
|
21
|
+
const workspaceMatch = markdown.match(/Workspace:\s+`([^`]+)`/);
|
|
22
|
+
const backendMatch = markdown.match(/^Backend:\s+`([^`]+)`/m);
|
|
23
|
+
const frontendMatch = markdown.match(/^Frontend:\s+`([^`]+)`/m);
|
|
24
|
+
const backendSummaryMatch = markdown.match(/^\*\*Backend:\*\*\s*(.+)$/m);
|
|
25
|
+
const siMatch = markdown.match(/^-\s+scripts:\s*(.+)$/m);
|
|
26
|
+
const generatedMatch = markdown.match(/generated=(\d{4}-\d{2}-\d{2}T[^Z]+Z)/);
|
|
27
|
+
const highCoupling = [];
|
|
28
|
+
for (const line of markdown.split("\n")) {
|
|
29
|
+
const m = line.match(COUPLING_LINE);
|
|
30
|
+
if (m) {
|
|
31
|
+
highCoupling.push({ path: m[1].trim(), score: Number(m[2]) });
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
const behavioralTests = [];
|
|
35
|
+
const testRe = /`([^`]+\.test\.ts)`/g;
|
|
36
|
+
let tm;
|
|
37
|
+
while ((tm = testRe.exec(markdown)) !== null) {
|
|
38
|
+
behavioralTests.push(tm[1]);
|
|
39
|
+
}
|
|
40
|
+
let generatedAt = null;
|
|
41
|
+
if (generatedMatch) {
|
|
42
|
+
const d = new Date(generatedMatch[1]);
|
|
43
|
+
generatedAt = Number.isNaN(d.getTime()) ? null : d;
|
|
44
|
+
}
|
|
45
|
+
return {
|
|
46
|
+
projectName: projectMatch?.[1].trim() ?? null,
|
|
47
|
+
workspace: workspaceMatch?.[1] ?? null,
|
|
48
|
+
backendRoot: backendMatch?.[1] ?? null,
|
|
49
|
+
frontendRoot: frontendMatch?.[1] ?? null,
|
|
50
|
+
backendSummaryLine: backendSummaryMatch?.[1].trim() ?? null,
|
|
51
|
+
structuralIntelligenceLine: siMatch?.[1].trim() ?? null,
|
|
52
|
+
highCoupling,
|
|
53
|
+
behavioralTests,
|
|
54
|
+
generatedAt
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* Rank high-coupling files by overlap between query tokens and path segments.
|
|
59
|
+
*/
|
|
60
|
+
export function suggestImplementationFiles(parsed, query, limit = 6) {
|
|
61
|
+
const pkg = parsed.projectName ?? "specguard";
|
|
62
|
+
const terms = tokenizeQuery(query);
|
|
63
|
+
const scored = parsed.highCoupling.map((c) => {
|
|
64
|
+
const rel = toRepoRelativePath(c.path, pkg);
|
|
65
|
+
const hay = `${c.path} ${rel}`.toLowerCase();
|
|
66
|
+
const overlap = terms.filter((t) => hay.includes(t)).length;
|
|
67
|
+
const rank = overlap * 100 + c.score * 10;
|
|
68
|
+
return { rel, rank };
|
|
69
|
+
});
|
|
70
|
+
scored.sort((a, b) => b.rank - a.rank);
|
|
71
|
+
const out = [];
|
|
72
|
+
const seen = new Set();
|
|
73
|
+
for (const { rel } of scored) {
|
|
74
|
+
if (seen.has(rel))
|
|
75
|
+
continue;
|
|
76
|
+
seen.add(rel);
|
|
77
|
+
out.push(rel);
|
|
78
|
+
if (out.length >= limit)
|
|
79
|
+
break;
|
|
80
|
+
}
|
|
81
|
+
return out;
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* Point at behavioral tests whose paths mention query tokens.
|
|
85
|
+
*/
|
|
86
|
+
export function suggestBehavioralTests(parsed, query, limit = 6) {
|
|
87
|
+
const pkg = parsed.projectName ?? "specguard";
|
|
88
|
+
const terms = tokenizeQuery(query);
|
|
89
|
+
const hits = [];
|
|
90
|
+
for (const raw of parsed.behavioralTests) {
|
|
91
|
+
const rel = toRepoRelativePath(raw, pkg);
|
|
92
|
+
const hay = `${raw} ${rel}`.toLowerCase();
|
|
93
|
+
if (terms.some((t) => hay.includes(t))) {
|
|
94
|
+
hits.push(rel);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
return hits.slice(0, limit);
|
|
98
|
+
}
|
|
99
|
+
/** Very rough tokenizer proxy (benchmark-style char/4 heuristic). */
|
|
100
|
+
export function estimateTokensApprox(text) {
|
|
101
|
+
return Math.ceil(text.length / 4);
|
|
102
|
+
}
|
|
103
|
+
export function isContextStale(generatedAt, maxAgeMs, nowMs = Date.now()) {
|
|
104
|
+
if (!generatedAt)
|
|
105
|
+
return true;
|
|
106
|
+
return nowMs - generatedAt.getTime() > maxAgeMs;
|
|
107
|
+
}
|