@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/cli.js
ADDED
|
@@ -0,0 +1,343 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { Command } from "commander";
|
|
3
|
+
import { runExtract } from "./commands/extract.js";
|
|
4
|
+
import { runDrift } from "./commands/drift.js";
|
|
5
|
+
import { runConstraints } from "./commands/constraints.js";
|
|
6
|
+
import { runSimulate } from "./commands/simulate.js";
|
|
7
|
+
import { runGuard } from "./commands/guard.js";
|
|
8
|
+
import { runDiff } from "./commands/diff.js";
|
|
9
|
+
import { runSummary } from "./commands/summary.js";
|
|
10
|
+
import { runSearch } from "./commands/search.js";
|
|
11
|
+
import { runContext } from "./commands/context.js";
|
|
12
|
+
import { runGenerate } from "./commands/generate.js";
|
|
13
|
+
import { runVerifyDrift } from "./commands/verify-drift.js";
|
|
14
|
+
import { runAnalyzeDepth } from "./commands/analyze-depth.js";
|
|
15
|
+
import { runIntel } from "./commands/intel.js";
|
|
16
|
+
import { runFeatureContext } from "./commands/feature-context.js";
|
|
17
|
+
import { runDocGenerate } from "./commands/doc-generate.js";
|
|
18
|
+
import { runDiscrepancy } from "./commands/discrepancy.js";
|
|
19
|
+
import { runDocHtml } from "./commands/doc-html.js";
|
|
20
|
+
import { runInit } from "./commands/init.js";
|
|
21
|
+
const program = new Command();
|
|
22
|
+
program
|
|
23
|
+
.name("guardian")
|
|
24
|
+
.description("Guardian — Architectural intelligence for codebases (by Toolbaux)")
|
|
25
|
+
.version("0.1.0");
|
|
26
|
+
program
|
|
27
|
+
.command("generate")
|
|
28
|
+
.description("Generate compact AI-ready architecture context")
|
|
29
|
+
.argument("[projectRoot]", "Repo or project root", process.cwd())
|
|
30
|
+
.option("--backend-root <path>", "Path to backend root")
|
|
31
|
+
.option("--frontend-root <path>", "Path to frontend root")
|
|
32
|
+
.option("--config <path>", "Path to specguard.config.json")
|
|
33
|
+
.option("--output <path>", "Output directory", "specs-out")
|
|
34
|
+
.option("--focus <text>", "Focus the generated AI context on a feature area")
|
|
35
|
+
.option("--max-lines <count>", "Maximum lines for the generated context")
|
|
36
|
+
.option("--ai-context", "Generate architecture-context.md for AI tools", false)
|
|
37
|
+
.action(async (projectRoot, options) => {
|
|
38
|
+
await runGenerate({
|
|
39
|
+
projectRoot,
|
|
40
|
+
backendRoot: options.backendRoot,
|
|
41
|
+
frontendRoot: options.frontendRoot,
|
|
42
|
+
configPath: options.config,
|
|
43
|
+
output: options.output,
|
|
44
|
+
focus: options.focus,
|
|
45
|
+
maxLines: options.maxLines,
|
|
46
|
+
aiContext: options.aiContext ?? false
|
|
47
|
+
});
|
|
48
|
+
});
|
|
49
|
+
program
|
|
50
|
+
.command("extract")
|
|
51
|
+
.description("Generate architecture and UX snapshots")
|
|
52
|
+
.argument("[projectRoot]", "Repo or project root", process.cwd())
|
|
53
|
+
.option("--backend-root <path>", "Path to backend root")
|
|
54
|
+
.option("--frontend-root <path>", "Path to frontend root")
|
|
55
|
+
.option("--output <path>", "Output directory", "specs-out")
|
|
56
|
+
.option("--include-file-graph", "Include file-level dependency graph", false)
|
|
57
|
+
.option("--config <path>", "Path to specguard.config.json")
|
|
58
|
+
.option("--docs-mode <mode>", "Docs mode (lean|full)")
|
|
59
|
+
.action(async (projectRoot, options) => {
|
|
60
|
+
await runExtract({
|
|
61
|
+
projectRoot,
|
|
62
|
+
backendRoot: options.backendRoot,
|
|
63
|
+
frontendRoot: options.frontendRoot,
|
|
64
|
+
output: options.output ?? "specs-out",
|
|
65
|
+
includeFileGraph: options.includeFileGraph ?? false,
|
|
66
|
+
configPath: options.config,
|
|
67
|
+
docsMode: options.docsMode
|
|
68
|
+
});
|
|
69
|
+
});
|
|
70
|
+
program
|
|
71
|
+
.command("diff")
|
|
72
|
+
.description("Generate changelog diff between a baseline snapshot and current snapshot")
|
|
73
|
+
.requiredOption("--baseline <path>", "Path to baseline architecture.snapshot.yaml")
|
|
74
|
+
.requiredOption("--current <path>", "Path to current architecture.snapshot.yaml")
|
|
75
|
+
.option("--output <path>", "Output diff path", "specs-out/machine/docs/diff.md")
|
|
76
|
+
.action(async (options) => {
|
|
77
|
+
await runDiff({
|
|
78
|
+
baselinePath: options.baseline,
|
|
79
|
+
currentPath: options.current,
|
|
80
|
+
output: options.output ?? "specs-out/docs/diff.md"
|
|
81
|
+
});
|
|
82
|
+
});
|
|
83
|
+
program
|
|
84
|
+
.command("drift")
|
|
85
|
+
.description("Compute architectural drift metrics")
|
|
86
|
+
.argument("[projectRoot]", "Repo or project root", process.cwd())
|
|
87
|
+
.option("--backend-root <path>", "Path to backend root")
|
|
88
|
+
.option("--frontend-root <path>", "Path to frontend root")
|
|
89
|
+
.option("--output <path>", "Output report path", "specs-out/machine/drift.report.json")
|
|
90
|
+
.option("--baseline [path]", "Write baseline drift file")
|
|
91
|
+
.option("--history [path]", "Append drift history entry")
|
|
92
|
+
.option("--config <path>", "Path to specguard.config.json")
|
|
93
|
+
.action(async (projectRoot, options) => {
|
|
94
|
+
await runDrift({
|
|
95
|
+
projectRoot,
|
|
96
|
+
backendRoot: options.backendRoot,
|
|
97
|
+
frontendRoot: options.frontendRoot,
|
|
98
|
+
output: options.output ?? "specs-out/drift.report.json",
|
|
99
|
+
configPath: options.config,
|
|
100
|
+
baseline: options.baseline,
|
|
101
|
+
history: options.history
|
|
102
|
+
});
|
|
103
|
+
});
|
|
104
|
+
program
|
|
105
|
+
.command("verify-drift")
|
|
106
|
+
.description("Verify architectural drift against a baseline and strict thresholds for CI/CD")
|
|
107
|
+
.argument("[projectRoot]", "Repo or project root", process.cwd())
|
|
108
|
+
.option("--backend-root <path>", "Path to backend root")
|
|
109
|
+
.option("--frontend-root <path>", "Path to frontend root")
|
|
110
|
+
.option("--config <path>", "Path to specguard.config.json")
|
|
111
|
+
.option("--baseline <path>", "Path to baseline payload")
|
|
112
|
+
.option("--strict-threshold <val>", "Maximum allowed delta shift (default 0.15)")
|
|
113
|
+
.action(async (projectRoot, options) => {
|
|
114
|
+
await runVerifyDrift({
|
|
115
|
+
projectRoot,
|
|
116
|
+
backendRoot: options.backendRoot,
|
|
117
|
+
frontendRoot: options.frontendRoot,
|
|
118
|
+
configPath: options.config,
|
|
119
|
+
baseline: options.baseline,
|
|
120
|
+
strictThreshold: options.strictThreshold
|
|
121
|
+
});
|
|
122
|
+
});
|
|
123
|
+
program
|
|
124
|
+
.command("constraints")
|
|
125
|
+
.description("Generate LLM constraint summary")
|
|
126
|
+
.argument("[projectRoot]", "Repo or project root", process.cwd())
|
|
127
|
+
.option("--backend-root <path>", "Path to backend root")
|
|
128
|
+
.option("--frontend-root <path>", "Path to frontend root")
|
|
129
|
+
.option("--output <path>", "Output constraints path", "specs-out/machine/constraints.json")
|
|
130
|
+
.option("--config <path>", "Path to specguard.config.json")
|
|
131
|
+
.action(async (projectRoot, options) => {
|
|
132
|
+
await runConstraints({
|
|
133
|
+
projectRoot,
|
|
134
|
+
backendRoot: options.backendRoot,
|
|
135
|
+
frontendRoot: options.frontendRoot,
|
|
136
|
+
output: options.output ?? "specs-out/constraints.json",
|
|
137
|
+
configPath: options.config
|
|
138
|
+
});
|
|
139
|
+
});
|
|
140
|
+
program
|
|
141
|
+
.command("simulate")
|
|
142
|
+
.description("Simulate drift for a candidate workspace")
|
|
143
|
+
.argument("[projectRoot]", "Repo or project root", process.cwd())
|
|
144
|
+
.option("--backend-root <path>", "Path to backend root")
|
|
145
|
+
.option("--frontend-root <path>", "Path to frontend root")
|
|
146
|
+
.option("--output <path>", "Output simulation report", "specs-out/machine/drift.simulation.json")
|
|
147
|
+
.option("--baseline <path>", "Baseline drift/baseline file")
|
|
148
|
+
.option("--baseline-summary <path>", "Baseline architecture summary path")
|
|
149
|
+
.option("--patch <path>", "Patch file to apply for simulation")
|
|
150
|
+
.option("--mode <mode>", "Simulation mode (soft|hard)")
|
|
151
|
+
.option("--config <path>", "Path to specguard.config.json")
|
|
152
|
+
.action(async (projectRoot, options) => {
|
|
153
|
+
await runSimulate({
|
|
154
|
+
projectRoot,
|
|
155
|
+
backendRoot: options.backendRoot,
|
|
156
|
+
frontendRoot: options.frontendRoot,
|
|
157
|
+
output: options.output ?? "specs-out/drift.simulation.json",
|
|
158
|
+
baseline: options.baseline,
|
|
159
|
+
baselineSummary: options.baselineSummary,
|
|
160
|
+
configPath: options.config,
|
|
161
|
+
patch: options.patch,
|
|
162
|
+
mode: options.mode
|
|
163
|
+
});
|
|
164
|
+
});
|
|
165
|
+
program
|
|
166
|
+
.command("guard")
|
|
167
|
+
.description("Generate LLM prompt, optional patch, and simulate drift")
|
|
168
|
+
.argument("[projectRoot]", "Repo or project root", process.cwd())
|
|
169
|
+
.option("--backend-root <path>", "Path to backend root")
|
|
170
|
+
.option("--frontend-root <path>", "Path to frontend root")
|
|
171
|
+
.requiredOption("--task <text>", "Task description for the LLM")
|
|
172
|
+
.option("--prompt-out <path>", "Prompt output path", "specs-out/machine/guard.prompt.txt")
|
|
173
|
+
.option("--patch-out <path>", "Patch output path", "specs-out/machine/guard.patch")
|
|
174
|
+
.option("--simulation-out <path>", "Simulation report output", "specs-out/machine/drift.simulation.json")
|
|
175
|
+
.option("--mode <mode>", "Simulation mode (soft|hard)")
|
|
176
|
+
.option("--llm-command <cmd>", "Override LLM command from config")
|
|
177
|
+
.option("--print-context", "Print an IDE-ready context block instead of calling an LLM", false)
|
|
178
|
+
.option("--config <path>", "Path to specguard.config.json")
|
|
179
|
+
.action(async (projectRoot, options) => {
|
|
180
|
+
await runGuard({
|
|
181
|
+
projectRoot,
|
|
182
|
+
backendRoot: options.backendRoot,
|
|
183
|
+
frontendRoot: options.frontendRoot,
|
|
184
|
+
task: options.task,
|
|
185
|
+
promptOutput: options.promptOut,
|
|
186
|
+
patchOutput: options.patchOut,
|
|
187
|
+
simulationOutput: options.simulationOut,
|
|
188
|
+
mode: options.mode,
|
|
189
|
+
llmCommand: options.llmCommand,
|
|
190
|
+
printContext: options.printContext ?? false,
|
|
191
|
+
configPath: options.config
|
|
192
|
+
});
|
|
193
|
+
});
|
|
194
|
+
program
|
|
195
|
+
.command("summary")
|
|
196
|
+
.description("Generate a plain-language project summary from existing snapshots")
|
|
197
|
+
.option("--input <path>", "Snapshot output directory", "specs-out")
|
|
198
|
+
.option("--output <path>", "Summary output path")
|
|
199
|
+
.action(async (options) => {
|
|
200
|
+
await runSummary({
|
|
201
|
+
input: options.input ?? "specs-out",
|
|
202
|
+
output: options.output
|
|
203
|
+
});
|
|
204
|
+
});
|
|
205
|
+
program
|
|
206
|
+
.command("search")
|
|
207
|
+
.description("Search existing snapshots for models, endpoints, components, modules, and tasks")
|
|
208
|
+
.option("--input <path>", "Snapshot output directory", "specs-out")
|
|
209
|
+
.requiredOption("--query <text>", "Search query")
|
|
210
|
+
.option("--output <path>", "Write search results to a file")
|
|
211
|
+
.option("--types <items>", "Comma-separated filters: models,endpoints,components,modules,tasks")
|
|
212
|
+
.action(async (options) => {
|
|
213
|
+
await runSearch({
|
|
214
|
+
input: options.input ?? "specs-out",
|
|
215
|
+
query: options.query,
|
|
216
|
+
output: options.output,
|
|
217
|
+
types: options.types ? [options.types] : undefined
|
|
218
|
+
});
|
|
219
|
+
});
|
|
220
|
+
program
|
|
221
|
+
.command("context")
|
|
222
|
+
.description("Render an AI-ready context block from existing snapshots")
|
|
223
|
+
.option("--input <path>", "Snapshot output directory", "specs-out")
|
|
224
|
+
.option("--output <path>", "Append the context block to a file")
|
|
225
|
+
.option("--focus <text>", "Focus the context on a feature area")
|
|
226
|
+
.option("--max-lines <count>", "Maximum number of lines to include")
|
|
227
|
+
.action(async (options) => {
|
|
228
|
+
await runContext({
|
|
229
|
+
input: options.input ?? "specs-out",
|
|
230
|
+
output: options.output,
|
|
231
|
+
focus: options.focus,
|
|
232
|
+
maxLines: options.maxLines
|
|
233
|
+
});
|
|
234
|
+
});
|
|
235
|
+
program
|
|
236
|
+
.command("analyze-depth")
|
|
237
|
+
.description("Compute the Structural Intelligence profile for a feature or query")
|
|
238
|
+
.argument("[projectRoot]", "Repo or project root", process.cwd())
|
|
239
|
+
.requiredOption("--query <text>", "Feature or area to analyze (e.g. 'stripe', 'auth')")
|
|
240
|
+
.option("--backend-root <path>", "Path to backend root")
|
|
241
|
+
.option("--frontend-root <path>", "Path to frontend root")
|
|
242
|
+
.option("--config <path>", "Path to specguard.config.json")
|
|
243
|
+
.option("--output <path>", "Write report to a file instead of stdout")
|
|
244
|
+
.option("--format <fmt>", "Output format: yaml or json (default: yaml)")
|
|
245
|
+
.option("--ci", "Exit with code 1 when HIGH complexity is detected with strong confidence", false)
|
|
246
|
+
.action(async (projectRoot, options) => {
|
|
247
|
+
await runAnalyzeDepth({
|
|
248
|
+
projectRoot,
|
|
249
|
+
backendRoot: options.backendRoot,
|
|
250
|
+
frontendRoot: options.frontendRoot,
|
|
251
|
+
configPath: options.config,
|
|
252
|
+
output: options.output,
|
|
253
|
+
format: options.format ?? "yaml",
|
|
254
|
+
ci: options.ci ?? false,
|
|
255
|
+
query: options.query
|
|
256
|
+
});
|
|
257
|
+
});
|
|
258
|
+
program
|
|
259
|
+
.command("intel")
|
|
260
|
+
.description("Build codebase-intelligence.json from existing snapshots")
|
|
261
|
+
.option("--specs <dir>", "Snapshot output directory", "specs-out")
|
|
262
|
+
.option("--output <path>", "Output path for codebase-intelligence.json")
|
|
263
|
+
.action(async (options) => {
|
|
264
|
+
await runIntel({
|
|
265
|
+
specs: options.specs,
|
|
266
|
+
output: options.output
|
|
267
|
+
});
|
|
268
|
+
});
|
|
269
|
+
program
|
|
270
|
+
.command("feature-context")
|
|
271
|
+
.description("Generate a filtered context packet for implementing a single feature")
|
|
272
|
+
.requiredOption("--spec <file>", "Path to feature spec YAML")
|
|
273
|
+
.option("--specs <dir>", "Snapshot output directory", "specs-out")
|
|
274
|
+
.option("--output <path>", "Output path for feature context JSON")
|
|
275
|
+
.action(async (options) => {
|
|
276
|
+
await runFeatureContext({
|
|
277
|
+
spec: options.spec,
|
|
278
|
+
specs: options.specs,
|
|
279
|
+
output: options.output
|
|
280
|
+
});
|
|
281
|
+
});
|
|
282
|
+
program
|
|
283
|
+
.command("doc-generate")
|
|
284
|
+
.description("Generate a human-readable product document from codebase intelligence")
|
|
285
|
+
.option("--specs <dir>", "Snapshot output directory", "specs-out")
|
|
286
|
+
.option("--feature-specs <dir>", "Directory of feature spec YAML files")
|
|
287
|
+
.option("--output <path>", "Output path for product-document.md")
|
|
288
|
+
.option("--update-baseline", "Freeze current state as new baseline for discrepancy tracking", false)
|
|
289
|
+
.action(async (options) => {
|
|
290
|
+
await runDocGenerate({
|
|
291
|
+
specs: options.specs,
|
|
292
|
+
featureSpecs: options.featureSpecs,
|
|
293
|
+
output: options.output,
|
|
294
|
+
updateBaseline: options.updateBaseline ?? false
|
|
295
|
+
});
|
|
296
|
+
});
|
|
297
|
+
program
|
|
298
|
+
.command("discrepancy")
|
|
299
|
+
.description("Diff current codebase intelligence against a committed baseline")
|
|
300
|
+
.option("--specs <dir>", "Snapshot output directory", "specs-out")
|
|
301
|
+
.option("--feature-specs <dir>", "Directory of feature spec YAML files")
|
|
302
|
+
.option("--output <path>", "Output path (used when --format is json or md)")
|
|
303
|
+
.option("--format <fmt>", "Output format: json, md, or both (default: both)", "both")
|
|
304
|
+
.action(async (options) => {
|
|
305
|
+
await runDiscrepancy({
|
|
306
|
+
specs: options.specs,
|
|
307
|
+
featureSpecs: options.featureSpecs,
|
|
308
|
+
output: options.output,
|
|
309
|
+
format: options.format ?? "both"
|
|
310
|
+
});
|
|
311
|
+
});
|
|
312
|
+
program
|
|
313
|
+
.command("doc-html")
|
|
314
|
+
.description("Generate a self-contained Javadoc-style HTML viewer from codebase intelligence")
|
|
315
|
+
.option("--specs <dir>", "Snapshot output directory", "specs-out")
|
|
316
|
+
.option("--output <path>", "Output path for index.html")
|
|
317
|
+
.action(async (options) => {
|
|
318
|
+
await runDocHtml({
|
|
319
|
+
specs: options.specs ?? "specs-out",
|
|
320
|
+
output: options.output,
|
|
321
|
+
});
|
|
322
|
+
});
|
|
323
|
+
program
|
|
324
|
+
.command("init")
|
|
325
|
+
.description("Initialize specguard for a project (config, .specs dir, pre-commit hook, CLAUDE.md)")
|
|
326
|
+
.argument("[projectRoot]", "Repo or project root", process.cwd())
|
|
327
|
+
.option("--backend-root <path>", "Path to backend root")
|
|
328
|
+
.option("--frontend-root <path>", "Path to frontend root")
|
|
329
|
+
.option("--output <path>", "Output directory", ".specs")
|
|
330
|
+
.option("--skip-hook", "Skip pre-commit hook installation", false)
|
|
331
|
+
.action(async (projectRoot, options) => {
|
|
332
|
+
await runInit({
|
|
333
|
+
projectRoot,
|
|
334
|
+
backendRoot: options.backendRoot,
|
|
335
|
+
frontendRoot: options.frontendRoot,
|
|
336
|
+
output: options.output,
|
|
337
|
+
skipHook: options.skipHook ?? false,
|
|
338
|
+
});
|
|
339
|
+
});
|
|
340
|
+
program.parseAsync().catch((error) => {
|
|
341
|
+
console.error(error);
|
|
342
|
+
process.exitCode = 1;
|
|
343
|
+
});
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
import fs from "node:fs/promises";
|
|
3
|
+
import yaml from "js-yaml";
|
|
4
|
+
import { buildSnapshots } from "../extract/index.js";
|
|
5
|
+
import { analyzeDepth } from "../extract/analyzers/depth.js";
|
|
6
|
+
export async function runAnalyzeDepth(options) {
|
|
7
|
+
const { architecture } = await buildSnapshots({
|
|
8
|
+
projectRoot: options.projectRoot,
|
|
9
|
+
backendRoot: options.backendRoot,
|
|
10
|
+
frontendRoot: options.frontendRoot,
|
|
11
|
+
output: options.output ?? "specs-out",
|
|
12
|
+
includeFileGraph: true,
|
|
13
|
+
configPath: options.configPath
|
|
14
|
+
});
|
|
15
|
+
const report = analyzeDepth({
|
|
16
|
+
query: options.query,
|
|
17
|
+
modules: architecture.modules,
|
|
18
|
+
moduleGraph: architecture.dependencies.module_graph,
|
|
19
|
+
fileGraph: architecture.dependencies.file_graph,
|
|
20
|
+
circularDependencies: architecture.analysis.circular_dependencies
|
|
21
|
+
});
|
|
22
|
+
const formatted = options.format === "json"
|
|
23
|
+
? JSON.stringify(report, null, 2)
|
|
24
|
+
: (yaml.dump(report, { lineWidth: 100 }));
|
|
25
|
+
if (options.output) {
|
|
26
|
+
const target = path.resolve(options.output);
|
|
27
|
+
await fs.mkdir(path.dirname(target), { recursive: true });
|
|
28
|
+
await fs.writeFile(target, formatted, "utf8");
|
|
29
|
+
console.log(`Wrote ${target}`);
|
|
30
|
+
}
|
|
31
|
+
else {
|
|
32
|
+
console.log(formatted);
|
|
33
|
+
}
|
|
34
|
+
// CI enforcement: exit 1 when confident and non-compressible
|
|
35
|
+
if (options.ci &&
|
|
36
|
+
report.classification.compressible === "NON_COMPRESSIBLE" &&
|
|
37
|
+
report.confidence.value >= report.guardrails.enforce_if_confidence_above) {
|
|
38
|
+
console.error(`\n[SpecGuard] CI FAIL: "${options.query}" classified as HIGH complexity (confidence ${report.confidence.value.toFixed(2)}).\n` +
|
|
39
|
+
`Recommended pattern: ${report.recommendation.primary.pattern}\n` +
|
|
40
|
+
`Avoid: ${report.recommendation.avoid.join(", ")}`);
|
|
41
|
+
process.exit(1);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import fs from 'fs';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
async function readDirRecursive(dir) {
|
|
4
|
+
const entries = await fs.promises.readdir(dir, { withFileTypes: true });
|
|
5
|
+
const files = [];
|
|
6
|
+
for (const e of entries) {
|
|
7
|
+
const res = path.join(dir, e.name);
|
|
8
|
+
if (e.isDirectory())
|
|
9
|
+
files.push(...await readDirRecursive(res));
|
|
10
|
+
else
|
|
11
|
+
files.push(res);
|
|
12
|
+
}
|
|
13
|
+
return files;
|
|
14
|
+
}
|
|
15
|
+
export async function run() {
|
|
16
|
+
const root = process.cwd();
|
|
17
|
+
const srcDir = path.join(root, 'src');
|
|
18
|
+
let routes = [];
|
|
19
|
+
try {
|
|
20
|
+
const files = await readDirRecursive(srcDir);
|
|
21
|
+
const jsTs = files.filter(f => f.endsWith('.ts') || f.endsWith('.js'));
|
|
22
|
+
for (const f of jsTs) {
|
|
23
|
+
const text = await fs.promises.readFile(f, 'utf8');
|
|
24
|
+
const matches = Array.from(text.matchAll(/(?:get|post|put|delete|patch)\s*\(['"`]([^'"`]+)/ig));
|
|
25
|
+
if (matches.length) {
|
|
26
|
+
routes.push({ file: path.relative(root, f), samplePaths: matches.map(m => m[1]) });
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
catch (err) {
|
|
31
|
+
// no-op: project may not have src; return empty
|
|
32
|
+
}
|
|
33
|
+
const openapi = {
|
|
34
|
+
openapi: '3.0.0',
|
|
35
|
+
info: { title: 'Inferred API', version: '0.0.0' },
|
|
36
|
+
paths: {}
|
|
37
|
+
};
|
|
38
|
+
for (const r of routes) {
|
|
39
|
+
for (const p of r.samplePaths) {
|
|
40
|
+
openapi.paths[p] = { get: { description: `Discovered in ${r.file}` } };
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
const out = path.join(process.cwd(), 'out', 'inferred-openapi.json');
|
|
44
|
+
await fs.promises.mkdir(path.dirname(out), { recursive: true });
|
|
45
|
+
await fs.promises.writeFile(out, JSON.stringify(openapi, null, 2), 'utf8');
|
|
46
|
+
console.log('Wrote inferred OpenAPI to', out);
|
|
47
|
+
return openapi;
|
|
48
|
+
}
|
|
49
|
+
const _isMain = (typeof require !== 'undefined' && (require.main === module)) || (process.argv[1] && process.argv[1].endsWith('api-spec-extractor.ts'));
|
|
50
|
+
if (_isMain) {
|
|
51
|
+
run().catch(err => { console.error(err); process.exit(1); });
|
|
52
|
+
}
|