@llm-dev-ops/agentics-cli 2.1.5 → 2.3.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 (76) hide show
  1. package/dist/pipeline/auto-chain.d.ts +73 -0
  2. package/dist/pipeline/auto-chain.d.ts.map +1 -1
  3. package/dist/pipeline/auto-chain.js +525 -38
  4. package/dist/pipeline/auto-chain.js.map +1 -1
  5. package/dist/pipeline/phase2/phases/prompt-generator.d.ts.map +1 -1
  6. package/dist/pipeline/phase2/phases/prompt-generator.js +53 -6
  7. package/dist/pipeline/phase2/phases/prompt-generator.js.map +1 -1
  8. package/dist/pipeline/phase2/schemas.d.ts +10 -10
  9. package/dist/pipeline/phase4/phases/http-server-generator.d.ts +12 -0
  10. package/dist/pipeline/phase4/phases/http-server-generator.d.ts.map +1 -1
  11. package/dist/pipeline/phase4/phases/http-server-generator.js +92 -25
  12. package/dist/pipeline/phase4/phases/http-server-generator.js.map +1 -1
  13. package/dist/pipeline/phase5-build/phase5-build-coordinator.d.ts.map +1 -1
  14. package/dist/pipeline/phase5-build/phase5-build-coordinator.js +44 -0
  15. package/dist/pipeline/phase5-build/phase5-build-coordinator.js.map +1 -1
  16. package/dist/pipeline/phase5-build/phases/post-generation-validator.d.ts +75 -0
  17. package/dist/pipeline/phase5-build/phases/post-generation-validator.d.ts.map +1 -0
  18. package/dist/pipeline/phase5-build/phases/post-generation-validator.js +728 -0
  19. package/dist/pipeline/phase5-build/phases/post-generation-validator.js.map +1 -0
  20. package/dist/pipeline/phase5-build/types.d.ts +1 -1
  21. package/dist/pipeline/phase5-build/types.d.ts.map +1 -1
  22. package/dist/pipeline/types.d.ts +84 -0
  23. package/dist/pipeline/types.d.ts.map +1 -1
  24. package/dist/pipeline/types.js +43 -1
  25. package/dist/pipeline/types.js.map +1 -1
  26. package/dist/synthesis/consensus-svg.d.ts +19 -0
  27. package/dist/synthesis/consensus-svg.d.ts.map +1 -0
  28. package/dist/synthesis/consensus-svg.js +95 -0
  29. package/dist/synthesis/consensus-svg.js.map +1 -0
  30. package/dist/synthesis/consensus-tiers.d.ts +99 -0
  31. package/dist/synthesis/consensus-tiers.d.ts.map +1 -0
  32. package/dist/synthesis/consensus-tiers.js +285 -0
  33. package/dist/synthesis/consensus-tiers.js.map +1 -0
  34. package/dist/synthesis/domain-labor-classifier.d.ts +101 -0
  35. package/dist/synthesis/domain-labor-classifier.d.ts.map +1 -0
  36. package/dist/synthesis/domain-labor-classifier.js +312 -0
  37. package/dist/synthesis/domain-labor-classifier.js.map +1 -0
  38. package/dist/synthesis/domain-unit-registry.d.ts +59 -0
  39. package/dist/synthesis/domain-unit-registry.d.ts.map +1 -0
  40. package/dist/synthesis/domain-unit-registry.js +294 -0
  41. package/dist/synthesis/domain-unit-registry.js.map +1 -0
  42. package/dist/synthesis/financial-claim-extractor.d.ts +52 -0
  43. package/dist/synthesis/financial-claim-extractor.d.ts.map +1 -0
  44. package/dist/synthesis/financial-claim-extractor.js +351 -0
  45. package/dist/synthesis/financial-claim-extractor.js.map +1 -0
  46. package/dist/synthesis/financial-consistency-rules.d.ts +66 -0
  47. package/dist/synthesis/financial-consistency-rules.d.ts.map +1 -0
  48. package/dist/synthesis/financial-consistency-rules.js +432 -0
  49. package/dist/synthesis/financial-consistency-rules.js.map +1 -0
  50. package/dist/synthesis/financial-consistency-runner.d.ts +73 -0
  51. package/dist/synthesis/financial-consistency-runner.d.ts.map +1 -0
  52. package/dist/synthesis/financial-consistency-runner.js +131 -0
  53. package/dist/synthesis/financial-consistency-runner.js.map +1 -0
  54. package/dist/synthesis/forbidden-spin-phrases.d.ts +32 -0
  55. package/dist/synthesis/forbidden-spin-phrases.d.ts.map +1 -0
  56. package/dist/synthesis/forbidden-spin-phrases.js +84 -0
  57. package/dist/synthesis/forbidden-spin-phrases.js.map +1 -0
  58. package/dist/synthesis/phase-gate-thresholds.d.ts +30 -0
  59. package/dist/synthesis/phase-gate-thresholds.d.ts.map +1 -0
  60. package/dist/synthesis/phase-gate-thresholds.js +34 -0
  61. package/dist/synthesis/phase-gate-thresholds.js.map +1 -0
  62. package/dist/synthesis/prompts/index.d.ts.map +1 -1
  63. package/dist/synthesis/prompts/index.js +22 -0
  64. package/dist/synthesis/prompts/index.js.map +1 -1
  65. package/dist/synthesis/simulation-artifact-generator.d.ts.map +1 -1
  66. package/dist/synthesis/simulation-artifact-generator.js +89 -1
  67. package/dist/synthesis/simulation-artifact-generator.js.map +1 -1
  68. package/dist/synthesis/simulation-renderers.d.ts +105 -2
  69. package/dist/synthesis/simulation-renderers.d.ts.map +1 -1
  70. package/dist/synthesis/simulation-renderers.js +1056 -92
  71. package/dist/synthesis/simulation-renderers.js.map +1 -1
  72. package/dist/synthesis/unit-economics-loader.d.ts +71 -0
  73. package/dist/synthesis/unit-economics-loader.d.ts.map +1 -0
  74. package/dist/synthesis/unit-economics-loader.js +200 -0
  75. package/dist/synthesis/unit-economics-loader.js.map +1 -0
  76. package/package.json +1 -1
@@ -0,0 +1,728 @@
1
+ /**
2
+ * Phase 5 Build — Stage 7: Post-Generation Code Validator
3
+ *
4
+ * Implements ADR-PIPELINE-065 (which is the implementation spec for the
5
+ * Accepted-but-unimplemented ADR-PIPELINE-042 Post-Generation Code Validation).
6
+ *
7
+ * Unlike `implementation-quality-gate.ts` which is a PRE-generation prompt
8
+ * quality gate (it scans Phase 2 artifacts BEFORE code is generated), this
9
+ * validator scans the GENERATED project tree AFTER all other Phase 5 stages
10
+ * have run. It applies 10 rules that enforce the patterns documented in
11
+ * ADR-PIPELINE-046/047/049/051/059/064 and catches regressions in observability,
12
+ * typed API boundaries, audit trails, and test coverage.
13
+ *
14
+ * Non-blocking by default. When AGENTICS_STRICT_POSTGEN=true, any rule at
15
+ * severity 'error' fails the stage and — per coordinator configuration —
16
+ * can block the pipeline.
17
+ *
18
+ * Error codes:
19
+ * ECLI-P5-040 Post-generation validation failed (strict mode)
20
+ * ECLI-P5-041 Generated project directory not found
21
+ */
22
+ import * as fs from 'node:fs';
23
+ import * as path from 'node:path';
24
+ import { createSpan, endSpan, emitSpan } from '../../phase2/telemetry.js';
25
+ import { OWNED_SCAFFOLD_MODULES } from '../../auto-chain.js';
26
+ // ============================================================================
27
+ // File Walking & Helpers
28
+ // ============================================================================
29
+ const SCAN_EXTENSIONS = new Set(['.ts']);
30
+ const SKIP_DIRS = new Set(['node_modules', 'dist', 'build', '.git', 'coverage', '.next']);
31
+ /**
32
+ * Walk a directory recursively and return a map of relative paths → file contents.
33
+ * Safe: ignores unreadable files, caps file size at 1MB to avoid blowup.
34
+ */
35
+ function loadProjectFiles(rootDir) {
36
+ const files = new Map();
37
+ if (!fs.existsSync(rootDir))
38
+ return files;
39
+ const walk = (currentDir) => {
40
+ let entries;
41
+ try {
42
+ entries = fs.readdirSync(currentDir, { withFileTypes: true });
43
+ }
44
+ catch {
45
+ return;
46
+ }
47
+ for (const entry of entries) {
48
+ if (entry.isDirectory()) {
49
+ if (SKIP_DIRS.has(entry.name))
50
+ continue;
51
+ walk(path.join(currentDir, entry.name));
52
+ continue;
53
+ }
54
+ const ext = path.extname(entry.name);
55
+ if (!SCAN_EXTENSIONS.has(ext))
56
+ continue;
57
+ const fullPath = path.join(currentDir, entry.name);
58
+ try {
59
+ const stat = fs.statSync(fullPath);
60
+ if (stat.size > 1_000_000)
61
+ continue; // skip files > 1MB
62
+ const content = fs.readFileSync(fullPath, 'utf-8');
63
+ const relPath = path.relative(rootDir, fullPath);
64
+ files.set(relPath, content);
65
+ }
66
+ catch {
67
+ // skip unreadable
68
+ }
69
+ }
70
+ };
71
+ walk(rootDir);
72
+ return files;
73
+ }
74
+ /**
75
+ * Convert a character offset into a 1-based line number.
76
+ */
77
+ function lineAt(content, offset) {
78
+ let line = 1;
79
+ for (let i = 0; i < offset && i < content.length; i++) {
80
+ if (content.charCodeAt(i) === 10 /* \n */)
81
+ line++;
82
+ }
83
+ return line;
84
+ }
85
+ /**
86
+ * Path predicates used by multiple rules.
87
+ */
88
+ function isTestFile(relPath) {
89
+ const n = relPath.replace(/\\/g, '/').toLowerCase();
90
+ return n.endsWith('.test.ts') || n.endsWith('.spec.ts') || n.includes('/tests/') || n.includes('/__tests__/');
91
+ }
92
+ function isScriptOrDemo(relPath) {
93
+ const n = relPath.replace(/\\/g, '/').toLowerCase();
94
+ return n.includes('/scripts/') || n.includes('/demo') || n.endsWith('/demo.ts');
95
+ }
96
+ function isServerFile(relPath) {
97
+ const n = relPath.replace(/\\/g, '/').toLowerCase();
98
+ return n.includes('/server/') || n.includes('/routes/') || n.endsWith('/app.ts') || n.endsWith('/api.ts');
99
+ }
100
+ function isServiceFile(relPath) {
101
+ const n = relPath.replace(/\\/g, '/').toLowerCase();
102
+ return n.includes('/services/') || n.includes('/application/') || n.includes('/domain/services/');
103
+ }
104
+ function isPublicTypesFile(relPath) {
105
+ const n = relPath.replace(/\\/g, '/').toLowerCase();
106
+ return n.includes('/domain/types/') || n.includes('/contracts/') || n.endsWith('/types.ts');
107
+ }
108
+ /**
109
+ * Collect all regex matches with line numbers for a per-file scan.
110
+ */
111
+ function collectMatches(content, pattern, filePath, ruleId, severity, message) {
112
+ const findings = [];
113
+ const re = new RegExp(pattern.source, pattern.flags.includes('g') ? pattern.flags : pattern.flags + 'g');
114
+ let match;
115
+ while ((match = re.exec(content)) !== null) {
116
+ findings.push({
117
+ ruleId,
118
+ filePath,
119
+ line: lineAt(content, match.index),
120
+ message,
121
+ severity,
122
+ });
123
+ if (match.index === re.lastIndex)
124
+ re.lastIndex++; // guard against zero-width loops
125
+ }
126
+ return findings;
127
+ }
128
+ // ============================================================================
129
+ // Rule Definitions — 10 rules per ADR-PIPELINE-065
130
+ // ============================================================================
131
+ /** PGV-001: No `as string` / `as any` casts on route-handler inputs. */
132
+ const RULE_PGV_001 = {
133
+ id: 'PGV-001',
134
+ title: 'No `as string` / `as any` casts on route handler context or request',
135
+ severity: 'error',
136
+ adrReference: 'ADR-PIPELINE-064',
137
+ check: (files) => {
138
+ const findings = [];
139
+ // Hono: c.get('x') as string
140
+ const honoPattern = /c\.get\(\s*['"][\w.-]+['"]\s*\)\s*as\s+(?:string|any|unknown|Record<[^>]+>)/g;
141
+ // Express: (req as any).x OR req.headers['x-foo'] as string
142
+ const expressReqPattern = /\(\s*req\s+as\s+any\s*\)/g;
143
+ const expressHeaderPattern = /req\.headers\[['"][^'"]+['"]\]\s*as\s+(?:string|any)/g;
144
+ for (const [filePath, content] of files) {
145
+ if (isTestFile(filePath) || !isServerFile(filePath))
146
+ continue;
147
+ findings.push(...collectMatches(content, honoPattern, filePath, 'PGV-001', 'error', 'Cast `as string` on Hono `c.get()` — use typed `Hono<{ Variables: ContextVariables }>` generic (ADR-PIPELINE-064).'));
148
+ findings.push(...collectMatches(content, expressReqPattern, filePath, 'PGV-001', 'error', 'Cast `(req as any)` — declare `declare global { namespace Express { interface Request { ... } } }` (ADR-PIPELINE-064).'));
149
+ findings.push(...collectMatches(content, expressHeaderPattern, filePath, 'PGV-001', 'error', 'Cast `req.headers[\'x-foo\'] as string` — use middleware to parse headers into typed request properties (ADR-PIPELINE-064).'));
150
+ }
151
+ return findings;
152
+ },
153
+ };
154
+ /** PGV-002: `new Hono()` must be typed with a Variables generic. */
155
+ const RULE_PGV_002 = {
156
+ id: 'PGV-002',
157
+ title: '`new Hono()` must be parameterized with Variables generic',
158
+ severity: 'error',
159
+ adrReference: 'ADR-PIPELINE-064',
160
+ check: (files) => {
161
+ const findings = [];
162
+ // Match `new Hono(` NOT followed by `<`
163
+ const pattern = /new\s+Hono\s*\((?!\s*<)/g;
164
+ for (const [filePath, content] of files) {
165
+ if (isTestFile(filePath) || !isServerFile(filePath))
166
+ continue;
167
+ findings.push(...collectMatches(content, pattern, filePath, 'PGV-002', 'error', '`new Hono()` without type parameter — use `new Hono<{ Variables: ContextVariables }>()` so `c.get()` returns typed values (ADR-PIPELINE-064).'));
168
+ }
169
+ return findings;
170
+ },
171
+ };
172
+ /** PGV-003: No hardcoded magic numbers in service files. */
173
+ const RULE_PGV_003 = {
174
+ id: 'PGV-003',
175
+ title: 'No hardcoded magic numbers in service files',
176
+ severity: 'warn',
177
+ adrReference: 'ADR-PIPELINE-046',
178
+ check: (files) => {
179
+ const findings = [];
180
+ // Numbers with 4+ digits appearing as a standalone literal in expressions
181
+ // Skip: year constants (19xx/20xx), port numbers inside env fallbacks,
182
+ // Zod default() calls, and values inside string literals.
183
+ const pattern = /(?<![\w$.'"`])(\d{4,})(?![\w$'"`])/g;
184
+ for (const [filePath, content] of files) {
185
+ if (isTestFile(filePath) || !isServiceFile(filePath))
186
+ continue;
187
+ let match;
188
+ const re = new RegExp(pattern.source, pattern.flags);
189
+ while ((match = re.exec(content)) !== null) {
190
+ const num = parseInt(match[1], 10);
191
+ // Skip year literals
192
+ if (num >= 1900 && num <= 2100)
193
+ continue;
194
+ // Skip common non-magic numbers (bit widths, HTTP status, etc.)
195
+ if ([1024, 2048, 4096, 8192, 16384, 32768, 65536].includes(num))
196
+ continue;
197
+ if (num >= 100 && num <= 599)
198
+ continue; // HTTP status-like
199
+ // Skip if within a default() call
200
+ const windowStart = Math.max(0, match.index - 40);
201
+ const window = content.slice(windowStart, match.index + 10);
202
+ if (/\.default\s*\(\s*$/.test(window))
203
+ continue;
204
+ if (/z\.coerce\.(number|int)\(\)/.test(window))
205
+ continue;
206
+ // Skip if inside a string literal (crude heuristic: same line has matching quotes around the number)
207
+ const lineStart = content.lastIndexOf('\n', match.index) + 1;
208
+ const lineEnd = content.indexOf('\n', match.index);
209
+ const lineText = content.slice(lineStart, lineEnd === -1 ? content.length : lineEnd);
210
+ const numPosInLine = match.index - lineStart;
211
+ const before = lineText.slice(0, numPosInLine);
212
+ const quoteCount = (before.match(/(?<!\\)['"`]/g) ?? []).length;
213
+ if (quoteCount % 2 === 1)
214
+ continue; // inside a string
215
+ // Skip comments
216
+ if (/^\s*\/\//.test(lineText) || /^\s*\*/.test(lineText))
217
+ continue;
218
+ findings.push({
219
+ ruleId: 'PGV-003',
220
+ filePath,
221
+ line: lineAt(content, match.index),
222
+ severity: 'warn',
223
+ message: `Magic number ${num} in service file — externalize via config or typed constant (ADR-PIPELINE-046).`,
224
+ });
225
+ }
226
+ }
227
+ return findings;
228
+ },
229
+ };
230
+ /** PGV-004: State-mutating methods must write to the audit trail. */
231
+ const RULE_PGV_004 = {
232
+ id: 'PGV-004',
233
+ title: 'State-mutating service methods must call audit.append / audit.record',
234
+ severity: 'warn',
235
+ adrReference: 'ADR-PIPELINE-049',
236
+ check: (files) => {
237
+ const findings = [];
238
+ const mutationPattern = /\b(?:repo|repository)\.(save|update|delete|insert|upsert)\s*\(/;
239
+ const auditPattern = /\b(?:audit|auditService|auditLog)\.(?:append|record|log|write)\s*\(/;
240
+ for (const [filePath, content] of files) {
241
+ if (isTestFile(filePath) || !isServiceFile(filePath))
242
+ continue;
243
+ if (!mutationPattern.test(content))
244
+ continue;
245
+ if (auditPattern.test(content))
246
+ continue;
247
+ findings.push({
248
+ ruleId: 'PGV-004',
249
+ filePath,
250
+ line: 0,
251
+ severity: 'warn',
252
+ message: 'File contains repository write operations but no audit.append()/record() calls. State mutations must be auditable (ADR-PIPELINE-049).',
253
+ });
254
+ }
255
+ return findings;
256
+ },
257
+ };
258
+ /** PGV-005: No raw `console.*` calls in production code. */
259
+ const RULE_PGV_005 = {
260
+ id: 'PGV-005',
261
+ title: 'Use createLogger() instead of console.* in production code',
262
+ severity: 'warn',
263
+ adrReference: 'ADR-PIPELINE-051',
264
+ check: (files) => {
265
+ const findings = [];
266
+ const pattern = /console\.(log|error|warn|info|debug)\s*\(/g;
267
+ for (const [filePath, content] of files) {
268
+ if (isTestFile(filePath) || isScriptOrDemo(filePath))
269
+ continue;
270
+ findings.push(...collectMatches(content, pattern, filePath, 'PGV-005', 'warn', 'Direct `console.*` call — use `createLogger()` for structured logging (ADR-PIPELINE-051).'));
271
+ }
272
+ return findings;
273
+ },
274
+ };
275
+ /** PGV-006: Every route file has a sibling test file. */
276
+ const RULE_PGV_006 = {
277
+ id: 'PGV-006',
278
+ title: 'Route files must have a corresponding .test.ts',
279
+ severity: 'warn',
280
+ adrReference: 'ADR-PIPELINE-059',
281
+ check: (files) => {
282
+ const findings = [];
283
+ const allPaths = new Set(files.keys());
284
+ for (const filePath of files.keys()) {
285
+ const n = filePath.replace(/\\/g, '/').toLowerCase();
286
+ const isRoute = n.includes('/routes/') && n.endsWith('.ts') && !isTestFile(filePath);
287
+ if (!isRoute)
288
+ continue;
289
+ // Check sibling test file
290
+ const base = filePath.replace(/\.ts$/i, '');
291
+ const candidates = [`${base}.test.ts`, `${base}.spec.ts`];
292
+ const hasTest = candidates.some(c => allPaths.has(c));
293
+ if (hasTest)
294
+ continue;
295
+ // Also allow a test file in ../tests/ with the same base name
296
+ const baseName = path.basename(base);
297
+ const looseMatch = Array.from(allPaths).some(p => {
298
+ const pn = p.replace(/\\/g, '/').toLowerCase();
299
+ return isTestFile(p) && pn.includes(`/${baseName}.`);
300
+ });
301
+ if (looseMatch)
302
+ continue;
303
+ findings.push({
304
+ ruleId: 'PGV-006',
305
+ filePath,
306
+ line: 0,
307
+ severity: 'warn',
308
+ message: `Route file has no corresponding .test.ts (ADR-PIPELINE-059). Expected one of: ${candidates.join(', ')}`,
309
+ });
310
+ }
311
+ return findings;
312
+ },
313
+ };
314
+ /** PGV-007: Seed data validates via Zod at load. */
315
+ const RULE_PGV_007 = {
316
+ id: 'PGV-007',
317
+ title: 'Seed data must validate via Zod at load',
318
+ severity: 'warn',
319
+ adrReference: 'ADR-PIPELINE-046',
320
+ check: (files) => {
321
+ const findings = [];
322
+ for (const [filePath, content] of files) {
323
+ const n = filePath.replace(/\\/g, '/').toLowerCase();
324
+ if (!n.endsWith('/seed.ts') && !n.endsWith('/data/seed.ts'))
325
+ continue;
326
+ if (isTestFile(filePath))
327
+ continue;
328
+ if (/\.\s*(?:safeParse|parse)\s*\(/.test(content))
329
+ continue;
330
+ findings.push({
331
+ ruleId: 'PGV-007',
332
+ filePath,
333
+ line: 0,
334
+ severity: 'warn',
335
+ message: 'Seed data file does not call .parse()/.safeParse() — exported arrays should be validated at load (ADR-PIPELINE-046).',
336
+ });
337
+ }
338
+ return findings;
339
+ },
340
+ };
341
+ /** PGV-008: No `Record<string, unknown | any>` in public DTO types. */
342
+ const RULE_PGV_008 = {
343
+ id: 'PGV-008',
344
+ title: 'No Record<string, unknown|any> in public DTO types',
345
+ severity: 'warn',
346
+ adrReference: 'ADR-PIPELINE-039',
347
+ check: (files) => {
348
+ const findings = [];
349
+ const pattern = /Record\s*<\s*string\s*,\s*(?:unknown|any)\s*>/g;
350
+ for (const [filePath, content] of files) {
351
+ if (isTestFile(filePath) || !isPublicTypesFile(filePath))
352
+ continue;
353
+ findings.push(...collectMatches(content, pattern, filePath, 'PGV-008', 'warn', '`Record<string, unknown>` in public type — replace with concrete interface derived from the DDD model (ADR-PIPELINE-039).'));
354
+ }
355
+ return findings;
356
+ },
357
+ };
358
+ /** PGV-009: Circuit breaker has state transition tests. */
359
+ const RULE_PGV_009 = {
360
+ id: 'PGV-009',
361
+ title: 'Circuit breaker must have state transition tests',
362
+ severity: 'warn',
363
+ adrReference: 'ADR-PIPELINE-050',
364
+ check: (files) => {
365
+ const findings = [];
366
+ const cbFiles = [];
367
+ for (const filePath of files.keys()) {
368
+ const n = filePath.replace(/\\/g, '/').toLowerCase();
369
+ if ((n.endsWith('/circuit-breaker.ts') || n.endsWith('/circuitbreaker.ts')) && !isTestFile(filePath)) {
370
+ cbFiles.push(filePath);
371
+ }
372
+ }
373
+ if (cbFiles.length === 0)
374
+ return findings;
375
+ // Look for ANY test file that references all 3 state strings
376
+ const allContent = Array.from(files.entries()).filter(([p]) => isTestFile(p));
377
+ // Match state names with word boundaries. The rule only fires when a
378
+ // circuit-breaker.ts file exists, so false positives on generic "open"
379
+ // are unlikely — we also require the distinctive "half-open" compound.
380
+ const hasTransitionTests = allContent.some(([, c]) => /\bclosed\b/.test(c) && /\bopen\b/.test(c) && /\bhalf[-_]?open\b/.test(c));
381
+ if (hasTransitionTests)
382
+ return findings;
383
+ for (const filePath of cbFiles) {
384
+ findings.push({
385
+ ruleId: 'PGV-009',
386
+ filePath,
387
+ line: 0,
388
+ severity: 'warn',
389
+ message: 'Circuit breaker implementation has no state transition tests (closed → open → half-open → closed). Add tests referencing all three states (ADR-PIPELINE-050).',
390
+ });
391
+ }
392
+ return findings;
393
+ },
394
+ };
395
+ /** PGV-010: Repository interfaces have at least one implementation. */
396
+ const RULE_PGV_010 = {
397
+ id: 'PGV-010',
398
+ title: 'Repository interfaces must have at least one implementation',
399
+ severity: 'warn',
400
+ adrReference: 'ADR-PIPELINE-047',
401
+ check: (files) => {
402
+ const findings = [];
403
+ const interfacePattern = /export\s+interface\s+(\w*Repository)\b/g;
404
+ const interfaces = new Map();
405
+ for (const [filePath, content] of files) {
406
+ if (isTestFile(filePath))
407
+ continue;
408
+ let match;
409
+ const re = new RegExp(interfacePattern.source, interfacePattern.flags);
410
+ while ((match = re.exec(content)) !== null) {
411
+ interfaces.set(match[1], { filePath, line: lineAt(content, match.index) });
412
+ }
413
+ }
414
+ for (const [name, loc] of interfaces) {
415
+ // Search for `implements Name` or `class X implements ... Name`
416
+ const implRegex = new RegExp(`\\bimplements\\b[^{]*\\b${name}\\b`);
417
+ let found = false;
418
+ for (const [p, c] of files) {
419
+ if (isTestFile(p))
420
+ continue;
421
+ if (implRegex.test(c)) {
422
+ found = true;
423
+ break;
424
+ }
425
+ }
426
+ if (found)
427
+ continue;
428
+ findings.push({
429
+ ruleId: 'PGV-010',
430
+ filePath: loc.filePath,
431
+ line: loc.line,
432
+ severity: 'warn',
433
+ message: `Repository interface \`${name}\` has no class implementing it. Provide at least one concrete implementation (ADR-PIPELINE-047).`,
434
+ });
435
+ }
436
+ return findings;
437
+ },
438
+ };
439
+ /** PGV-011: No CommonJS `require(` calls in generated TypeScript (ADR-PIPELINE-068). */
440
+ const RULE_PGV_011 = {
441
+ id: 'PGV-011',
442
+ title: 'No CommonJS require() calls in generated TypeScript',
443
+ severity: 'error',
444
+ adrReference: 'ADR-PIPELINE-068',
445
+ check: (files) => {
446
+ const findings = [];
447
+ // Match `require('...')` / `require("...")` / `require(\`...\`)` —
448
+ // a CJS module load always passes a string literal as its first arg.
449
+ // Constraints:
450
+ // - Char before `require` must NOT be `.` (excludes obj.require)
451
+ // or word/$ (excludes requireSimulationLineage, _require, etc.)
452
+ // - First argument must be a string literal (excludes method
453
+ // definitions like `require(name: string)`)
454
+ // - `createRequire(import.meta.url)` is the one safe interop pattern
455
+ // and is allowed when the file imports from node:module.
456
+ const pattern = /(?:^|[^.\w$])(require)\s*\(\s*['"`]/g;
457
+ const createRequirePattern = /createRequire\s*\(/;
458
+ for (const [filePath, content] of files) {
459
+ // Skip tests, scripts, and generated demo files. Demo is covered by
460
+ // ADR-068's banner rule — not by the require() ban.
461
+ if (isTestFile(filePath) || isScriptOrDemo(filePath))
462
+ continue;
463
+ // Type-only declaration files have no runtime behavior.
464
+ if (filePath.endsWith('.d.ts'))
465
+ continue;
466
+ const hasCreateRequire = createRequirePattern.test(content);
467
+ const re = new RegExp(pattern.source, 'g');
468
+ let match;
469
+ while ((match = re.exec(content)) !== null) {
470
+ // `createRequire(import.meta.url)` produces a `require` binding
471
+ // that's legal in ESM — don't flag its call sites when the file
472
+ // contains the createRequire import.
473
+ if (hasCreateRequire)
474
+ continue;
475
+ // Skip matches inside comments or string literals (rough check).
476
+ const start = match.index + (match[0].length - 'require('.length);
477
+ const lineStart = content.lastIndexOf('\n', start) + 1;
478
+ const prefix = content.slice(lineStart, start);
479
+ if (/^\s*\/\//.test(prefix) || /^\s*\*/.test(prefix))
480
+ continue;
481
+ findings.push({
482
+ ruleId: 'PGV-011',
483
+ filePath,
484
+ line: lineAt(content, start),
485
+ severity: 'error',
486
+ message: 'CommonJS `require()` in generated ESM project — use `import` instead, or `createRequire(import.meta.url)` for CJS interop. Silent `require()` throws at runtime under "type":"module" and severs simulation lineage (ADR-PIPELINE-068).',
487
+ });
488
+ if (match.index === re.lastIndex)
489
+ re.lastIndex++;
490
+ }
491
+ }
492
+ return findings;
493
+ },
494
+ };
495
+ /** PGV-012: No generator-emitted file may redeclare a scaffold-owned export (ADR-PIPELINE-069). */
496
+ const RULE_PGV_012 = {
497
+ id: 'PGV-012',
498
+ title: 'Generator files must not redeclare scaffold-owned exports',
499
+ severity: 'error',
500
+ adrReference: 'ADR-PIPELINE-069',
501
+ check: (files) => {
502
+ const findings = [];
503
+ // Build (exportName -> ownedPath) lookup. Skip the scaffold-owned
504
+ // files themselves (matched by basename) since they're allowed to
505
+ // declare their own exports.
506
+ const exportToOwned = new Map();
507
+ const ownedBasenames = new Set();
508
+ for (const mod of OWNED_SCAFFOLD_MODULES) {
509
+ ownedBasenames.add(path.basename(mod.path));
510
+ for (const ex of mod.exports) {
511
+ if (!exportToOwned.has(ex))
512
+ exportToOwned.set(ex, mod.path);
513
+ }
514
+ }
515
+ for (const [filePath, content] of files) {
516
+ const basename = path.basename(filePath);
517
+ // Skip the scaffold-owned files themselves
518
+ if (ownedBasenames.has(basename))
519
+ continue;
520
+ // Skip tests, scripts, demos, and type-only declarations
521
+ if (isTestFile(filePath) || isScriptOrDemo(filePath))
522
+ continue;
523
+ if (filePath.endsWith('.d.ts'))
524
+ continue;
525
+ for (const [exportName, ownedPath] of exportToOwned) {
526
+ // Match `export class|function|const|let|interface|type|enum NAME`
527
+ const declRe = new RegExp(`\\bexport\\s+(?:class|function|const|let|interface|type|enum)\\s+${exportName}\\b`, 'g');
528
+ // Match `export { NAME }` (re-export named) — both `export { NAME }` and
529
+ // `export { NAME as ... }`. The wildcard inside braces tolerates
530
+ // extra exports on the same line.
531
+ const reExportRe = new RegExp(`\\bexport\\s*\\{[^}]*\\b${exportName}\\b[^}]*\\}`, 'g');
532
+ let m;
533
+ while ((m = declRe.exec(content)) !== null) {
534
+ findings.push({
535
+ ruleId: 'PGV-012',
536
+ filePath,
537
+ line: lineAt(content, m.index),
538
+ severity: 'error',
539
+ message: `Redeclaration of scaffold-owned export \`${exportName}\` (owned by ${ownedPath}). Import from the scaffold path instead of redefining (ADR-PIPELINE-069).`,
540
+ });
541
+ if (m.index === declRe.lastIndex)
542
+ declRe.lastIndex++;
543
+ }
544
+ let r;
545
+ while ((r = reExportRe.exec(content)) !== null) {
546
+ findings.push({
547
+ ruleId: 'PGV-012',
548
+ filePath,
549
+ line: lineAt(content, r.index),
550
+ severity: 'error',
551
+ message: `Re-export of scaffold-owned name \`${exportName}\` from a non-scaffold file (owned by ${ownedPath}). Import directly from the scaffold path (ADR-PIPELINE-069).`,
552
+ });
553
+ if (r.index === reExportRe.lastIndex)
554
+ reExportRe.lastIndex++;
555
+ }
556
+ }
557
+ }
558
+ return findings;
559
+ },
560
+ };
561
+ const ALL_RULES = [
562
+ RULE_PGV_001, RULE_PGV_002, RULE_PGV_003, RULE_PGV_004, RULE_PGV_005,
563
+ RULE_PGV_006, RULE_PGV_007, RULE_PGV_008, RULE_PGV_009, RULE_PGV_010,
564
+ RULE_PGV_011, RULE_PGV_012,
565
+ ];
566
+ // Exported for unit tests that want to run individual rules
567
+ export const RULES = ALL_RULES;
568
+ // ============================================================================
569
+ // Main Validator
570
+ // ============================================================================
571
+ /**
572
+ * Run all validation rules against the files in a project directory.
573
+ * Pure function — no I/O side effects beyond reading files.
574
+ */
575
+ export function validateGeneratedCode(projectDir) {
576
+ const files = loadProjectFiles(projectDir);
577
+ const findings = [];
578
+ for (const rule of ALL_RULES) {
579
+ try {
580
+ findings.push(...rule.check(files));
581
+ }
582
+ catch (err) {
583
+ const errMsg = err instanceof Error ? err.message : String(err);
584
+ findings.push({
585
+ ruleId: rule.id,
586
+ filePath: '<rule-error>',
587
+ line: 0,
588
+ severity: 'warn',
589
+ message: `Rule ${rule.id} threw during execution: ${errMsg}. Skipping.`,
590
+ });
591
+ }
592
+ }
593
+ const errorCount = findings.filter(f => f.severity === 'error').length;
594
+ const warnCount = findings.filter(f => f.severity === 'warn').length;
595
+ // Scoring: 100 - (10 * errors, capped at 70) - (2 * warns, capped at 20)
596
+ const errorPenalty = Math.min(70, errorCount * 10);
597
+ const warnPenalty = Math.min(20, warnCount * 2);
598
+ const score = Math.max(0, 100 - errorPenalty - warnPenalty);
599
+ const passed = score >= 70;
600
+ return {
601
+ passed,
602
+ score,
603
+ findings,
604
+ filesScanned: files.size,
605
+ rulesApplied: ALL_RULES.length,
606
+ errorCount,
607
+ warnCount,
608
+ projectDir,
609
+ };
610
+ }
611
+ // ============================================================================
612
+ // Stage Entry Point
613
+ // ============================================================================
614
+ /**
615
+ * Resolve the generated project directory. Prefers the deploy project,
616
+ * falls back to codegen project. Returns null if neither exists.
617
+ */
618
+ function resolveProjectDir(phase5Dir) {
619
+ const candidates = [
620
+ path.join(phase5Dir, 'deploy', 'project'),
621
+ path.join(phase5Dir, 'codegen', 'project'),
622
+ ];
623
+ for (const c of candidates) {
624
+ if (fs.existsSync(c))
625
+ return c;
626
+ }
627
+ return null;
628
+ }
629
+ /**
630
+ * Execute the post-generation code validator as a Phase 5 pipeline stage.
631
+ *
632
+ * Non-blocking by default. When AGENTICS_STRICT_POSTGEN=true, any error-level
633
+ * finding fails the stage. The coordinator decides whether stage failure
634
+ * blocks the pipeline.
635
+ */
636
+ export function executePostGenerationValidator(context) {
637
+ const startTime = Date.now();
638
+ const span = createSpan('post-generation-validation', 'validate-generated-code', {
639
+ traceId: context.traceId,
640
+ phase5Dir: context.phase5Dir,
641
+ });
642
+ const projectDir = resolveProjectDir(context.phase5Dir);
643
+ if (!projectDir) {
644
+ const elapsed = Date.now() - startTime;
645
+ const failSpan = endSpan(span, 'error', { reason: 'project-not-found' });
646
+ emitSpan(failSpan);
647
+ context.telemetrySpans.push(failSpan);
648
+ return {
649
+ stageOutput: {
650
+ stage: 'post-generation-validation',
651
+ status: 'failed',
652
+ timing: elapsed,
653
+ artifacts: [],
654
+ summary: 'ECLI-P5-041: No generated project directory found (tried deploy/project and codegen/project).',
655
+ data: {
656
+ passed: false,
657
+ score: 0,
658
+ findings: [],
659
+ filesScanned: 0,
660
+ rulesApplied: ALL_RULES.length,
661
+ errorCount: 0,
662
+ warnCount: 0,
663
+ projectDir: null,
664
+ },
665
+ },
666
+ };
667
+ }
668
+ const report = validateGeneratedCode(projectDir);
669
+ const elapsed = Date.now() - startTime;
670
+ const strict = process.env['AGENTICS_STRICT_POSTGEN'] === 'true';
671
+ const failOnStrict = strict && report.errorCount > 0;
672
+ // Persist a report artifact so reviewers can inspect findings without rerunning
673
+ const reportPath = path.join(context.phase5Dir, 'post-generation-report.json');
674
+ try {
675
+ fs.writeFileSync(reportPath, JSON.stringify(report, null, 2), { encoding: 'utf-8', mode: 0o600 });
676
+ }
677
+ catch {
678
+ // best-effort
679
+ }
680
+ // Log findings to stderr using the same pattern as implementation-quality-gate.ts
681
+ if (report.findings.length > 0) {
682
+ console.error(` [POSTGEN] Post-generation validation: ${report.errorCount} errors, ${report.warnCount} warnings (score: ${report.score}/100)`);
683
+ // Group findings by rule for readability
684
+ const byRule = new Map();
685
+ for (const f of report.findings) {
686
+ const list = byRule.get(f.ruleId) ?? [];
687
+ list.push(f);
688
+ byRule.set(f.ruleId, list);
689
+ }
690
+ for (const [ruleId, findings] of byRule) {
691
+ const first = findings[0];
692
+ const severityTag = first.severity === 'error' ? 'ERROR' : 'warn';
693
+ console.error(` [${severityTag}] ${ruleId} — ${findings.length} finding${findings.length > 1 ? 's' : ''}: ${first.message}`);
694
+ for (const f of findings.slice(0, 5)) {
695
+ console.error(` ${f.filePath}${f.line > 0 ? `:${f.line}` : ''}`);
696
+ }
697
+ if (findings.length > 5) {
698
+ console.error(` ... and ${findings.length - 5} more (see post-generation-report.json)`);
699
+ }
700
+ }
701
+ }
702
+ else {
703
+ console.error(` [POSTGEN] Post-generation validation: PASSED (score: ${report.score}/100, ${report.filesScanned} files, ${ALL_RULES.length} rules).`);
704
+ }
705
+ const finalSpan = endSpan(span, failOnStrict ? 'error' : 'ok', {
706
+ errorCount: report.errorCount,
707
+ warnCount: report.warnCount,
708
+ score: report.score,
709
+ strict,
710
+ });
711
+ emitSpan(finalSpan);
712
+ context.telemetrySpans.push(finalSpan);
713
+ const status = failOnStrict ? 'failed' : 'completed';
714
+ const summary = failOnStrict
715
+ ? `ECLI-P5-040: Post-generation validation FAILED in strict mode — ${report.errorCount} errors, score ${report.score}/100.`
716
+ : ` Post-generation validation: ${report.errorCount} errors, ${report.warnCount} warnings, score ${report.score}/100 (${report.filesScanned} files scanned, ${ALL_RULES.length} rules).`;
717
+ return {
718
+ stageOutput: {
719
+ stage: 'post-generation-validation',
720
+ status,
721
+ timing: elapsed,
722
+ artifacts: [reportPath],
723
+ summary,
724
+ data: report,
725
+ },
726
+ };
727
+ }
728
+ //# sourceMappingURL=post-generation-validator.js.map