@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.
Files changed (78) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +366 -0
  3. package/dist/adapters/csharp-adapter.js +149 -0
  4. package/dist/adapters/go-adapter.js +96 -0
  5. package/dist/adapters/index.js +16 -0
  6. package/dist/adapters/java-adapter.js +122 -0
  7. package/dist/adapters/python-adapter.js +183 -0
  8. package/dist/adapters/runner.js +69 -0
  9. package/dist/adapters/types.js +1 -0
  10. package/dist/adapters/typescript-adapter.js +179 -0
  11. package/dist/benchmarking/framework.js +91 -0
  12. package/dist/cli.js +343 -0
  13. package/dist/commands/analyze-depth.js +43 -0
  14. package/dist/commands/api-spec-extractor.js +52 -0
  15. package/dist/commands/breaking-change-analyzer.js +334 -0
  16. package/dist/commands/config-compliance.js +219 -0
  17. package/dist/commands/constraints.js +221 -0
  18. package/dist/commands/context.js +101 -0
  19. package/dist/commands/data-flow-tracer.js +291 -0
  20. package/dist/commands/dependency-impact-analyzer.js +27 -0
  21. package/dist/commands/diff.js +146 -0
  22. package/dist/commands/discrepancy.js +71 -0
  23. package/dist/commands/doc-generate.js +163 -0
  24. package/dist/commands/doc-html.js +120 -0
  25. package/dist/commands/drift.js +88 -0
  26. package/dist/commands/extract.js +16 -0
  27. package/dist/commands/feature-context.js +116 -0
  28. package/dist/commands/generate.js +339 -0
  29. package/dist/commands/guard.js +182 -0
  30. package/dist/commands/init.js +209 -0
  31. package/dist/commands/intel.js +20 -0
  32. package/dist/commands/license-dependency-auditor.js +33 -0
  33. package/dist/commands/performance-hotspot-profiler.js +42 -0
  34. package/dist/commands/search.js +314 -0
  35. package/dist/commands/security-boundary-auditor.js +359 -0
  36. package/dist/commands/simulate.js +294 -0
  37. package/dist/commands/summary.js +27 -0
  38. package/dist/commands/test-coverage-mapper.js +264 -0
  39. package/dist/commands/verify-drift.js +62 -0
  40. package/dist/config.js +441 -0
  41. package/dist/extract/ai-context-hints.js +107 -0
  42. package/dist/extract/analyzers/backend.js +1704 -0
  43. package/dist/extract/analyzers/depth.js +264 -0
  44. package/dist/extract/analyzers/frontend.js +2221 -0
  45. package/dist/extract/api-usage-tracker.js +19 -0
  46. package/dist/extract/cache.js +53 -0
  47. package/dist/extract/codebase-intel.js +190 -0
  48. package/dist/extract/compress.js +452 -0
  49. package/dist/extract/context-block.js +356 -0
  50. package/dist/extract/contracts.js +183 -0
  51. package/dist/extract/discrepancies.js +233 -0
  52. package/dist/extract/docs-loader.js +110 -0
  53. package/dist/extract/docs.js +2379 -0
  54. package/dist/extract/drift.js +1578 -0
  55. package/dist/extract/duplicates.js +435 -0
  56. package/dist/extract/feature-arcs.js +138 -0
  57. package/dist/extract/graph.js +76 -0
  58. package/dist/extract/html-doc.js +1409 -0
  59. package/dist/extract/ignore.js +45 -0
  60. package/dist/extract/index.js +455 -0
  61. package/dist/extract/llm-client.js +159 -0
  62. package/dist/extract/pattern-registry.js +141 -0
  63. package/dist/extract/product-doc.js +497 -0
  64. package/dist/extract/python.js +1202 -0
  65. package/dist/extract/runtime.js +193 -0
  66. package/dist/extract/schema-evolution-validator.js +35 -0
  67. package/dist/extract/test-gap-analyzer.js +20 -0
  68. package/dist/extract/tests.js +74 -0
  69. package/dist/extract/types.js +1 -0
  70. package/dist/extract/validate-backend.js +30 -0
  71. package/dist/extract/writer.js +11 -0
  72. package/dist/output-layout.js +37 -0
  73. package/dist/project-discovery.js +309 -0
  74. package/dist/schema/architecture.js +350 -0
  75. package/dist/schema/feature-spec.js +89 -0
  76. package/dist/schema/index.js +8 -0
  77. package/dist/schema/ux.js +46 -0
  78. package/package.json +75 -0
@@ -0,0 +1,309 @@
1
+ import fs from "node:fs/promises";
2
+ import path from "node:path";
3
+ import { loadSpecGuardConfig } from "./config.js";
4
+ const IGNORE_DIRS = new Set([
5
+ ".git",
6
+ "node_modules",
7
+ ".next",
8
+ "dist",
9
+ "build",
10
+ "coverage",
11
+ "__pycache__",
12
+ ".venv",
13
+ "venv",
14
+ "specs-out",
15
+ ".pytest_cache",
16
+ ".mypy_cache",
17
+ ".turbo"
18
+ ]);
19
+ export async function resolveProjectPaths(options) {
20
+ const startingRoot = path.resolve(options.projectRoot ?? process.cwd());
21
+ const config = await loadSpecGuardConfig({
22
+ projectRoot: startingRoot,
23
+ backendRoot: options.backendRoot,
24
+ frontendRoot: options.frontendRoot,
25
+ configPath: options.configPath
26
+ });
27
+ const configWorkspaceRoot = config.project?.root
28
+ ? path.resolve(config.project.root)
29
+ : startingRoot;
30
+ const workspaceRoot = configWorkspaceRoot;
31
+ const detectionEnabled = config.project?.discovery?.enabled ?? true;
32
+ const explicitBackend = options.backendRoot
33
+ ? await normalizeBackendRoot(path.resolve(options.backendRoot))
34
+ : config.project?.backendRoot
35
+ ? await normalizeBackendRoot(path.resolve(config.project.backendRoot))
36
+ : null;
37
+ const explicitFrontend = options.frontendRoot
38
+ ? path.resolve(options.frontendRoot)
39
+ : config.project?.frontendRoot
40
+ ? path.resolve(config.project.frontendRoot)
41
+ : null;
42
+ let resolutionSource = "auto";
43
+ if (options.backendRoot || options.frontendRoot) {
44
+ resolutionSource = "cli";
45
+ }
46
+ else if (config.project?.backendRoot || config.project?.frontendRoot || config.project?.root) {
47
+ resolutionSource = "config";
48
+ }
49
+ if (!detectionEnabled && (!explicitBackend || !explicitFrontend)) {
50
+ throw new Error("Project autodiscovery is disabled in config, but backend/frontend roots were not fully provided.");
51
+ }
52
+ const backendRoot = explicitBackend ??
53
+ (await chooseBackendRoot(workspaceRoot));
54
+ const frontendRoot = explicitFrontend ??
55
+ (await chooseFrontendRoot(workspaceRoot));
56
+ return {
57
+ workspaceRoot,
58
+ backendRoot,
59
+ frontendRoot,
60
+ resolutionSource,
61
+ config
62
+ };
63
+ }
64
+ export function logResolvedProjectPaths(resolved) {
65
+ console.log(`SpecGuard roots (${resolved.resolutionSource}): workspace=${resolved.workspaceRoot} backend=${resolved.backendRoot} frontend=${resolved.frontendRoot}`);
66
+ }
67
+ async function chooseBackendRoot(workspaceRoot) {
68
+ const candidates = await discoverBackendCandidates(workspaceRoot);
69
+ if (candidates.length === 0) {
70
+ console.warn(`⚠️ Could not distinctly auto-detect a backend root. Defaulting to universal workspace root parsing: ${workspaceRoot}`);
71
+ return workspaceRoot;
72
+ }
73
+ if (candidates.length > 1 && candidates[0].score === candidates[1].score) {
74
+ console.warn(`⚠️ Backend autodetection is ambiguous between ${candidates[0].path} and ${candidates[1].path}. Defaulting to universal workspace root parsing.`);
75
+ return workspaceRoot;
76
+ }
77
+ return normalizeBackendRoot(candidates[0].path);
78
+ }
79
+ async function chooseFrontendRoot(workspaceRoot) {
80
+ const candidates = await discoverFrontendCandidates(workspaceRoot);
81
+ if (candidates.length === 0) {
82
+ console.warn(`⚠️ Could not distinctly auto-detect a frontend root. Defaulting to universal workspace root parsing: ${workspaceRoot}`);
83
+ return workspaceRoot;
84
+ }
85
+ if (candidates.length > 1 && candidates[0].score === candidates[1].score) {
86
+ console.warn(`⚠️ Frontend autodetection is ambiguous between ${candidates[0].path} and ${candidates[1].path}. Defaulting to universal workspace root parsing.`);
87
+ return workspaceRoot;
88
+ }
89
+ return path.resolve(candidates[0].path);
90
+ }
91
+ async function discoverBackendCandidates(workspaceRoot) {
92
+ const candidates = [];
93
+ const preferredDirs = ["backend", "services", "apps", "server", "api"];
94
+ for (const name of preferredDirs) {
95
+ const target = path.join(workspaceRoot, name);
96
+ const stats = await safeStat(target);
97
+ if (!stats?.isDirectory()) {
98
+ continue;
99
+ }
100
+ const candidate = await scoreBackendDirectory(target, workspaceRoot);
101
+ if (candidate) {
102
+ candidates.push(candidate);
103
+ }
104
+ }
105
+ const workspaceCandidate = await scoreBackendDirectory(workspaceRoot, workspaceRoot);
106
+ if (workspaceCandidate) {
107
+ candidates.push(workspaceCandidate);
108
+ }
109
+ const childDirs = await listChildDirs(workspaceRoot);
110
+ for (const dir of childDirs) {
111
+ const candidate = await scoreBackendDirectory(dir, workspaceRoot);
112
+ if (candidate) {
113
+ candidates.push(candidate);
114
+ }
115
+ }
116
+ return dedupeAndSortCandidates(candidates);
117
+ }
118
+ async function discoverFrontendCandidates(workspaceRoot) {
119
+ const candidates = [];
120
+ const preferredDirs = ["frontend", "web", "client", "app", "ui"];
121
+ for (const name of preferredDirs) {
122
+ const target = path.join(workspaceRoot, name);
123
+ const candidate = await scoreFrontendDirectory(target, workspaceRoot);
124
+ if (candidate) {
125
+ candidates.push(candidate);
126
+ }
127
+ }
128
+ const workspaceCandidate = await scoreFrontendDirectory(workspaceRoot, workspaceRoot);
129
+ if (workspaceCandidate) {
130
+ candidates.push(workspaceCandidate);
131
+ }
132
+ const childDirs = await listChildDirs(workspaceRoot);
133
+ for (const dir of childDirs) {
134
+ const candidate = await scoreFrontendDirectory(dir, workspaceRoot);
135
+ if (candidate) {
136
+ candidates.push(candidate);
137
+ }
138
+ }
139
+ return dedupeAndSortCandidates(candidates);
140
+ }
141
+ async function scoreBackendDirectory(dir, workspaceRoot) {
142
+ const stats = await safeStat(dir);
143
+ if (!stats?.isDirectory()) {
144
+ return null;
145
+ }
146
+ let score = 0;
147
+ const reasons = [];
148
+ const name = path.basename(dir).toLowerCase();
149
+ if (name === "backend" || name === "services" || name === "api") {
150
+ score += 4;
151
+ reasons.push(name);
152
+ }
153
+ const files = await listDirNames(dir);
154
+ if (files.has("pyproject.toml") || files.has("requirements.txt") || files.has("manage.py")) {
155
+ score += 5;
156
+ reasons.push("python-manifest");
157
+ }
158
+ if (files.has("package.json")) {
159
+ const pkg = await readTextIfExists(path.join(dir, "package.json"));
160
+ if (pkg && /(express|fastify|nest|koa)/i.test(pkg)) {
161
+ score += 4;
162
+ reasons.push("node-backend");
163
+ }
164
+ }
165
+ const serviceDirs = await listChildDirs(dir);
166
+ let serviceLikeCount = 0;
167
+ for (const serviceDir of serviceDirs) {
168
+ const serviceFiles = await listDirNames(serviceDir);
169
+ if (serviceFiles.has("pyproject.toml") ||
170
+ serviceFiles.has("requirements.txt") ||
171
+ serviceFiles.has("package.json")) {
172
+ serviceLikeCount += 1;
173
+ }
174
+ }
175
+ if (serviceLikeCount >= 2) {
176
+ score += 8 + serviceLikeCount;
177
+ reasons.push(`services:${serviceLikeCount}`);
178
+ }
179
+ const markerFiles = ["main.py", "app.py", "server.py", "index.js", "server.js"];
180
+ for (const marker of markerFiles) {
181
+ const markerPath = path.join(dir, marker);
182
+ const raw = await readTextIfExists(markerPath);
183
+ if (raw && /(FastAPI|APIRouter|Flask|Django|express\(|NestFactory)/.test(raw)) {
184
+ score += 4;
185
+ reasons.push(`marker:${marker}`);
186
+ break;
187
+ }
188
+ }
189
+ if (dir === workspaceRoot && score < 5) {
190
+ return null;
191
+ }
192
+ return score > 0 ? { path: dir, score, reasons } : null;
193
+ }
194
+ async function scoreFrontendDirectory(dir, workspaceRoot) {
195
+ const stats = await safeStat(dir);
196
+ if (!stats?.isDirectory()) {
197
+ return null;
198
+ }
199
+ let score = 0;
200
+ const reasons = [];
201
+ const name = path.basename(dir).toLowerCase();
202
+ if (["frontend", "web", "client", "ui"].includes(name)) {
203
+ score += 5;
204
+ reasons.push(name);
205
+ }
206
+ const files = await listDirNames(dir);
207
+ const hasPackage = files.has("package.json");
208
+ if (files.has("next.config.js") || files.has("next.config.mjs") || files.has("next.config.ts")) {
209
+ score += 8;
210
+ reasons.push("next");
211
+ }
212
+ if (files.has("vite.config.ts") || files.has("vite.config.js")) {
213
+ score += 7;
214
+ reasons.push("vite");
215
+ }
216
+ if (hasPackage) {
217
+ score += 3;
218
+ const pkg = await readTextIfExists(path.join(dir, "package.json"));
219
+ if (pkg && /(next|react|vue|svelte)/i.test(pkg)) {
220
+ score += 4;
221
+ reasons.push("frontend-package");
222
+ }
223
+ }
224
+ if (files.has("tsconfig.json")) {
225
+ score += 1;
226
+ }
227
+ for (const routeDir of ["app", "pages", path.join("src", "app"), path.join("src", "pages")]) {
228
+ const stat = await safeStat(path.join(dir, routeDir));
229
+ if (stat?.isDirectory()) {
230
+ score += 4;
231
+ reasons.push(`routes:${routeDir}`);
232
+ }
233
+ }
234
+ if (dir === workspaceRoot && score < 5) {
235
+ return null;
236
+ }
237
+ return score > 0 ? { path: dir, score, reasons } : null;
238
+ }
239
+ function dedupeAndSortCandidates(candidates) {
240
+ const best = new Map();
241
+ for (const candidate of candidates) {
242
+ const resolved = path.resolve(candidate.path);
243
+ const existing = best.get(resolved);
244
+ if (!existing || candidate.score > existing.score) {
245
+ best.set(resolved, { ...candidate, path: resolved });
246
+ }
247
+ }
248
+ return Array.from(best.values()).sort((a, b) => b.score - a.score || a.path.localeCompare(b.path));
249
+ }
250
+ function formatCandidates(candidates) {
251
+ return candidates
252
+ .slice(0, 4)
253
+ .map((candidate) => `${candidate.path} (score ${candidate.score})`)
254
+ .join(", ");
255
+ }
256
+ async function listChildDirs(root) {
257
+ const entries = await safeReadDir(root);
258
+ return entries
259
+ .filter((entry) => entry.isDirectory() && !IGNORE_DIRS.has(entry.name))
260
+ .map((entry) => path.join(root, entry.name));
261
+ }
262
+ async function listDirNames(dir) {
263
+ const entries = await safeReadDir(dir);
264
+ return new Set(entries.map((entry) => entry.name));
265
+ }
266
+ async function safeReadDir(dir) {
267
+ try {
268
+ return await fs.readdir(dir, { withFileTypes: true });
269
+ }
270
+ catch {
271
+ return [];
272
+ }
273
+ }
274
+ async function readTextIfExists(filePath) {
275
+ try {
276
+ return await fs.readFile(filePath, "utf8");
277
+ }
278
+ catch {
279
+ return null;
280
+ }
281
+ }
282
+ async function safeStat(filePath) {
283
+ try {
284
+ return await fs.stat(filePath);
285
+ }
286
+ catch {
287
+ return null;
288
+ }
289
+ }
290
+ async function normalizeBackendRoot(backendRoot) {
291
+ const resolved = path.resolve(backendRoot);
292
+ const base = path.basename(resolved).toLowerCase();
293
+ if (base === "backend" || base === "src" || base === "services") {
294
+ return resolved;
295
+ }
296
+ const backendCandidate = path.join(resolved, "backend");
297
+ const srcCandidate = path.join(resolved, "src");
298
+ const servicesCandidate = path.join(resolved, "services");
299
+ if ((await safeStat(backendCandidate))?.isDirectory()) {
300
+ return backendCandidate;
301
+ }
302
+ if ((await safeStat(servicesCandidate))?.isDirectory()) {
303
+ return servicesCandidate;
304
+ }
305
+ if ((await safeStat(srcCandidate))?.isDirectory()) {
306
+ return srcCandidate;
307
+ }
308
+ return resolved;
309
+ }
@@ -0,0 +1,350 @@
1
+ import { z } from "zod";
2
+ export const moduleSummarySchema = z.object({
3
+ id: z.string(),
4
+ path: z.string(),
5
+ type: z.enum(["backend", "frontend"]),
6
+ layer: z.enum(["core", "middle", "top", "isolated"]),
7
+ files: z.array(z.string()),
8
+ endpoints: z.array(z.string()),
9
+ imports: z.array(z.string()),
10
+ exports: z.array(z.object({
11
+ file: z.string(),
12
+ symbols: z.array(z.string()),
13
+ exports: z.array(z.object({
14
+ name: z.string(),
15
+ kind: z.enum(["default", "named"]),
16
+ alias: z.string().optional()
17
+ }))
18
+ }))
19
+ });
20
+ export const frontendPageSummarySchema = z.object({
21
+ path: z.string(),
22
+ component: z.string()
23
+ });
24
+ export const frontendApiCallSummarySchema = z.object({
25
+ method: z.string(),
26
+ path: z.string(),
27
+ source: z.string(),
28
+ request_fields: z.array(z.string()).optional()
29
+ });
30
+ export const backendEndpointSchema = z.object({
31
+ id: z.string(),
32
+ method: z.string(),
33
+ path: z.string(),
34
+ handler: z.string(),
35
+ file: z.string(),
36
+ module: z.string(),
37
+ request_schema: z.string().nullable().optional(),
38
+ response_schema: z.string().nullable().optional(),
39
+ service_calls: z.array(z.string()),
40
+ ai_operations: z.array(z.object({
41
+ provider: z.enum(["openai", "anthropic", "unknown"]),
42
+ operation: z.string(),
43
+ model: z.string().nullable().optional(),
44
+ max_tokens: z.number().nullable().optional(),
45
+ max_output_tokens: z.number().nullable().optional(),
46
+ token_budget: z.number().nullable().optional()
47
+ }))
48
+ });
49
+ export const dataModelSchema = z.object({
50
+ name: z.string(),
51
+ file: z.string(),
52
+ framework: z.enum(["sqlalchemy", "django", "pydantic"]),
53
+ fields: z.array(z.string()),
54
+ relationships: z.array(z.string()),
55
+ field_details: z
56
+ .array(z.object({
57
+ name: z.string(),
58
+ type: z.string().nullable().optional(),
59
+ nullable: z.boolean().nullable().optional(),
60
+ primary_key: z.boolean().nullable().optional(),
61
+ foreign_key: z.string().nullable().optional(),
62
+ enum: z.string().nullable().optional(),
63
+ default: z.string().nullable().optional()
64
+ }))
65
+ .optional()
66
+ });
67
+ export const enumSummarySchema = z.object({
68
+ name: z.string(),
69
+ file: z.string(),
70
+ values: z.array(z.string())
71
+ });
72
+ export const constantSummarySchema = z.object({
73
+ name: z.string(),
74
+ file: z.string(),
75
+ type: z.string(),
76
+ value: z.string()
77
+ });
78
+ export const endpointModelUsageSchema = z.object({
79
+ endpoint_id: z.string(),
80
+ endpoint: z.string(),
81
+ models: z.array(z.object({
82
+ name: z.string(),
83
+ access: z.enum(["read", "write", "read_write", "unknown"])
84
+ }))
85
+ });
86
+ export const testCoverageSummarySchema = z.object({
87
+ test_file: z.string(),
88
+ source_file: z.string().nullable(),
89
+ match_type: z.enum(["exact", "implicit", "none"])
90
+ });
91
+ export const testGapSummarySchema = z.object({
92
+ untested_source_files: z.array(z.string()),
93
+ test_files_missing_source: z.array(z.string()),
94
+ coverage_map: z.array(testCoverageSummarySchema)
95
+ });
96
+ export const endpointTestCoverageSchema = z.object({
97
+ endpoint: z.string(),
98
+ method: z.string(),
99
+ path: z.string(),
100
+ file: z.string(),
101
+ covered: z.boolean(),
102
+ coverage_type: z.enum(["file", "none"]),
103
+ test_files: z.array(z.string())
104
+ });
105
+ export const functionTestCoverageSchema = z.object({
106
+ function_id: z.string(),
107
+ file: z.string(),
108
+ covered: z.boolean(),
109
+ coverage_type: z.enum(["file", "none"]),
110
+ test_files: z.array(z.string())
111
+ });
112
+ export const backgroundTaskSchema = z.object({
113
+ name: z.string(),
114
+ file: z.string(),
115
+ kind: z.enum(["celery", "background"]),
116
+ queue: z.string().nullable().optional(),
117
+ schedule: z.string().nullable().optional()
118
+ });
119
+ export const runtimeServiceSchema = z.object({
120
+ name: z.string(),
121
+ source: z.string(),
122
+ image: z.string().optional(),
123
+ build: z.string().optional(),
124
+ ports: z.array(z.string()).optional(),
125
+ environment: z.array(z.string()).optional(),
126
+ depends_on: z.array(z.string()).optional()
127
+ });
128
+ export const systemManifestSchema = z.object({
129
+ file: z.string(),
130
+ kind: z.enum(["npm", "poetry", "pip", "go", "maven", "gradle", "makefile", "github-action", "doc", "unknown"]),
131
+ commands: z.array(z.string()).optional(),
132
+ dependencies: z.array(z.string()).optional(),
133
+ dev_dependencies: z.array(z.string()).optional(),
134
+ description: z.string().optional()
135
+ });
136
+ export const runtimeTopologySchema = z.object({
137
+ dockerfiles: z.array(z.string()),
138
+ services: z.array(runtimeServiceSchema),
139
+ manifests: z.array(systemManifestSchema),
140
+ shell_scripts: z.array(z.string())
141
+ });
142
+ export const moduleDependencySchema = z.object({
143
+ from: z.string(),
144
+ to: z.string(),
145
+ file: z.string()
146
+ });
147
+ export const fileDependencySchema = z.object({
148
+ from: z.string(),
149
+ to: z.string()
150
+ });
151
+ const capacityStatusSchema = z.enum(["ok", "warning", "critical", "unbudgeted"]);
152
+ const capacityLayerUsageSchema = z.object({
153
+ layer: z.string(),
154
+ nodes: z.number(),
155
+ edges: z.number(),
156
+ cross_layer_out: z.number(),
157
+ budget: z.number().optional(),
158
+ ratio: z.number().optional(),
159
+ remaining: z.number().optional(),
160
+ status: capacityStatusSchema
161
+ });
162
+ const driftCapacitySchema = z.object({
163
+ thresholds: z.object({
164
+ warning: z.number(),
165
+ critical: z.number()
166
+ }),
167
+ total: z
168
+ .object({
169
+ budget: z.number().optional(),
170
+ used: z.number(),
171
+ ratio: z.number().optional(),
172
+ remaining: z.number().optional(),
173
+ status: capacityStatusSchema
174
+ })
175
+ .optional(),
176
+ layers: z.array(capacityLayerUsageSchema),
177
+ status: capacityStatusSchema
178
+ });
179
+ const driftGrowthSchema = z.object({
180
+ edges_per_hour: z.number(),
181
+ edges_per_day: z.number(),
182
+ trend: z.enum(["increasing", "decreasing", "stable", "insufficient_data"]),
183
+ window: z.object({
184
+ from: z.string().optional(),
185
+ to: z.string().optional(),
186
+ hours: z.number().optional()
187
+ }),
188
+ status: z.enum(["ok", "critical", "insufficient_data"])
189
+ });
190
+ const driftScaleSchema = z.object({
191
+ level: z.enum(["function", "file", "module", "domain"]),
192
+ metrics: z.object({
193
+ entropy: z.number(),
194
+ cross_layer_ratio: z.number(),
195
+ cycle_density: z.number(),
196
+ modularity_gap: z.number()
197
+ }),
198
+ D_t: z.number(),
199
+ K_t: z.number(),
200
+ delta: z.number(),
201
+ status: z.enum(["stable", "critical", "drift"]),
202
+ capacity: driftCapacitySchema,
203
+ growth: driftGrowthSchema,
204
+ alerts: z.array(z.string()),
205
+ details: z.object({
206
+ nodes: z.number(),
207
+ edges: z.number(),
208
+ cycles: z.number(),
209
+ cross_layer_edges: z.number(),
210
+ layers: z.array(z.string()),
211
+ fingerprint: z.string(),
212
+ shape_fingerprint: z.string()
213
+ })
214
+ });
215
+ export const driftReportSchema = z.object({
216
+ version: z.literal("0.3"),
217
+ graph_level: z.enum(["function", "file", "module", "domain"]),
218
+ metrics: driftScaleSchema.shape.metrics,
219
+ D_t: z.number(),
220
+ K_t: z.number(),
221
+ delta: z.number(),
222
+ status: z.enum(["stable", "critical", "drift"]),
223
+ capacity: driftCapacitySchema,
224
+ growth: driftGrowthSchema,
225
+ alerts: z.array(z.string()),
226
+ details: driftScaleSchema.shape.details,
227
+ scales: z.array(driftScaleSchema)
228
+ });
229
+ export const testExtractionSummarySchema = z.object({
230
+ file: z.string(),
231
+ test_name: z.string(),
232
+ suite_name: z.string().nullable().optional()
233
+ });
234
+ export const structuralIntelligenceReportSchema = z.object({
235
+ feature: z.string(),
236
+ structure: z.object({ nodes: z.number(), edges: z.number() }),
237
+ metrics: z.object({
238
+ depth: z.number(),
239
+ fanout_avg: z.number(),
240
+ fanout_max: z.number(),
241
+ density: z.number(),
242
+ has_cycles: z.boolean()
243
+ }),
244
+ scores: z.object({
245
+ depth_score: z.number(),
246
+ fanout_score: z.number(),
247
+ density_score: z.number(),
248
+ cycle_score: z.number(),
249
+ query_score: z.number()
250
+ }),
251
+ confidence: z.object({ value: z.number(), level: z.enum(["WEAK", "MODERATE", "STRONG"]) }),
252
+ ambiguity: z.object({ level: z.enum(["LOW", "MEDIUM", "HIGH"]) }),
253
+ classification: z.object({
254
+ depth_level: z.enum(["LOW", "MEDIUM", "HIGH"]),
255
+ propagation: z.enum(["LOCAL", "MODERATE", "STRONG"]),
256
+ compressible: z.enum(["COMPRESSIBLE", "PARTIAL", "NON_COMPRESSIBLE"])
257
+ }),
258
+ recommendation: z.object({
259
+ primary: z.object({ pattern: z.string(), confidence: z.number() }),
260
+ fallback: z.object({ pattern: z.string(), condition: z.string() }),
261
+ avoid: z.array(z.string())
262
+ }),
263
+ guardrails: z.object({ enforce_if_confidence_above: z.number() }),
264
+ override: z.object({ allowed: z.literal(true), requires_reason: z.literal(true) })
265
+ });
266
+ export const architectureSnapshotSchema = z.object({
267
+ version: z.literal("1.0"),
268
+ metadata: z.object({
269
+ generated_at: z.string(),
270
+ duration_ms: z.number(),
271
+ target_backend: z.string().nullable().optional(),
272
+ target_frontend: z.string().nullable().optional()
273
+ }),
274
+ runtime: runtimeTopologySchema.optional(),
275
+ modules: z.array(moduleSummarySchema),
276
+ endpoints: z.array(backendEndpointSchema),
277
+ data_models: z.array(dataModelSchema),
278
+ enums: z.array(enumSummarySchema).optional(),
279
+ constants: z.array(constantSummarySchema).optional(),
280
+ endpoint_model_usage: z.array(endpointModelUsageSchema),
281
+ cross_stack_contracts: z.array(z.object({
282
+ endpoint_id: z.string(),
283
+ method: z.string(),
284
+ path: z.string(),
285
+ backend_request_schema: z.string().nullable(),
286
+ backend_response_schema: z.string().nullable(),
287
+ backend_request_fields: z.array(z.string()),
288
+ frontend_request_fields: z.array(z.string()),
289
+ frontend_callers: z.array(z.object({
290
+ component: z.string(),
291
+ file: z.string()
292
+ })),
293
+ status: z.enum(["ok", "mismatched", "unverified"]),
294
+ issues: z.array(z.string())
295
+ })),
296
+ tasks: z.array(backgroundTaskSchema),
297
+ data_flows: z.array(z.object({
298
+ page: z.string(),
299
+ endpoint_id: z.string(),
300
+ models: z.array(z.string())
301
+ })),
302
+ dependencies: z.object({
303
+ module_graph: z.array(moduleDependencySchema),
304
+ file_graph: z.array(fileDependencySchema)
305
+ }),
306
+ tests: z.array(testExtractionSummarySchema).optional(),
307
+ structural_intelligence: z.array(structuralIntelligenceReportSchema).optional(),
308
+ drift: driftReportSchema,
309
+ analysis: z.object({
310
+ circular_dependencies: z.array(z.array(z.string())),
311
+ orphan_modules: z.array(z.string()),
312
+ orphan_files: z.array(z.string()),
313
+ frontend_orphan_files: z.array(z.string()),
314
+ module_usage: z.record(z.number()),
315
+ unused_exports: z.array(z.object({
316
+ file: z.string(),
317
+ symbol: z.string()
318
+ })),
319
+ frontend_unused_exports: z.array(z.object({
320
+ file: z.string(),
321
+ symbol: z.string()
322
+ })),
323
+ unused_endpoints: z.array(z.string()),
324
+ frontend_unused_api_calls: z.array(z.string()),
325
+ duplicate_functions: z.array(z.object({
326
+ hash: z.string(),
327
+ size: z.number(),
328
+ functions: z.array(z.object({
329
+ id: z.string(),
330
+ name: z.string(),
331
+ file: z.string(),
332
+ language: z.enum(["ts", "js", "py"]),
333
+ size: z.number()
334
+ }))
335
+ })),
336
+ similar_functions: z.array(z.object({
337
+ similarity: z.number(),
338
+ basis: z.enum(["call_pattern", "ast_structure"]),
339
+ functions: z.array(z.object({
340
+ id: z.string(),
341
+ name: z.string(),
342
+ file: z.string(),
343
+ language: z.enum(["ts", "js", "py"])
344
+ }))
345
+ })),
346
+ test_coverage: testGapSummarySchema,
347
+ endpoint_test_coverage: z.array(endpointTestCoverageSchema),
348
+ function_test_coverage: z.array(functionTestCoverageSchema)
349
+ })
350
+ });