@boyingliu01/xp-gate 0.18.1 → 0.18.3

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 (35) hide show
  1. package/adapters/gate-3.sh +68 -0
  2. package/adapters/gate-4.sh +80 -0
  3. package/adapters/gate-7.sh +45 -0
  4. package/adapters/gate-8.sh +56 -0
  5. package/adapters/gate-9.sh +105 -0
  6. package/bin/xp-gate.js +45 -0
  7. package/hooks/pre-commit +108 -9
  8. package/hooks/pre-push +63 -232
  9. package/lib/__tests__/install-skill.test.js +3 -3
  10. package/lib/__tests__/test-alignment.test.ts +761 -0
  11. package/lib/install-skill.js +11 -3
  12. package/lib/mattpocock-license-verdict.json +1 -1
  13. package/lib/phase-transition.js +106 -1
  14. package/lib/sprint-init.js +41 -0
  15. package/lib/test-alignment.ts +651 -0
  16. package/mock-policy/AGENTS.md +3 -3
  17. package/mutation/AGENTS.md +3 -3
  18. package/mutation/gate-m.ts +18 -4
  19. package/package.json +1 -1
  20. package/plugins/claude-code/.claude-plugin/plugin.json +1 -1
  21. package/plugins/claude-code/skills/delphi-review/AGENTS.md +3 -3
  22. package/plugins/claude-code/skills/sprint-flow/AGENTS.md +3 -3
  23. package/plugins/claude-code/skills/test-specification-alignment/AGENTS.md +3 -3
  24. package/plugins/opencode/package.json +1 -1
  25. package/plugins/opencode/skills/delphi-review/AGENTS.md +3 -3
  26. package/plugins/opencode/skills/sprint-flow/AGENTS.md +3 -3
  27. package/plugins/opencode/skills/test-specification-alignment/AGENTS.md +3 -3
  28. package/plugins/qoder/plugin.json +1 -1
  29. package/plugins/qoder/skills/delphi-review/AGENTS.md +3 -3
  30. package/plugins/qoder/skills/sprint-flow/AGENTS.md +3 -3
  31. package/plugins/qoder/skills/test-specification-alignment/AGENTS.md +3 -3
  32. package/principles/AGENTS.md +3 -3
  33. package/skills/delphi-review/AGENTS.md +3 -3
  34. package/skills/sprint-flow/AGENTS.md +3 -3
  35. package/skills/test-specification-alignment/AGENTS.md +3 -3
@@ -0,0 +1,651 @@
1
+ /**
2
+ * test-alignment.ts — Deterministic Test-Specification Alignment Engine
3
+ *
4
+ * Extracted from skills/test-specification-alignment/references/alignment-verification-algorithm.md
5
+ * (493 lines of TypeScript pseudocode) into real, compiled TypeScript.
6
+ *
7
+ * Architecture:
8
+ * 1. parseSpecification() — Parse YAML → requirement IDs
9
+ * 2. parseTestFiles() — Parse test files → annotation map
10
+ * 3. verifyAlignment() — Cross-reference requirements ↔ tests
11
+ * 4. calculateScore() — Weighted alignment score
12
+ * 5. writeReport() — Write test-alignment-report.json
13
+ *
14
+ * Design decisions:
15
+ * - Lightweight YAML parser (~150 LOC, no external dependencies) for zero-dep npm package
16
+ * - Regex-based annotation extraction for TS, Python, Go (with .each/.skip/.todo support)
17
+ * - Real avgAssertions from testMap (not hardcoded placeholder)
18
+ * - Canonical spec format: specification.requirements (with backward compat for bare spec.requirements)
19
+ */
20
+
21
+ import * as fs from 'fs';
22
+ import * as path from 'path';
23
+ import * as crypto from 'crypto';
24
+
25
+ // ============================================================================
26
+ // Types
27
+ // ============================================================================
28
+
29
+ export interface AcceptanceCriterion {
30
+ id: string;
31
+ given?: string;
32
+ when?: string;
33
+ then?: string;
34
+ }
35
+
36
+ export interface Requirement {
37
+ id: string;
38
+ description?: string;
39
+ priority?: 'MUST' | 'SHOULD' | 'MAY';
40
+ acceptanceCriteria: string[];
41
+ }
42
+
43
+ export interface DesignDecision {
44
+ id: string;
45
+ description?: string;
46
+ rationale?: string;
47
+ alternatives_considered?: string[];
48
+ }
49
+
50
+ export interface ApiContract {
51
+ endpoint: string;
52
+ method?: string;
53
+ [key: string]: unknown;
54
+ }
55
+
56
+ export interface SpecificationMap {
57
+ requirements: Requirement[];
58
+ designDecisions: DesignDecision[];
59
+ apiContracts: ApiContract[];
60
+ }
61
+
62
+ export interface TestCase {
63
+ name: string;
64
+ file: string;
65
+ requirementId?: string;
66
+ intent?: string;
67
+ covers: string[];
68
+ edgeCases: string[];
69
+ assertions: number;
70
+ }
71
+
72
+ export interface TestMap {
73
+ tests: TestCase[];
74
+ totalTests: number;
75
+ totalAssertions: number;
76
+ }
77
+
78
+ export type IssueType =
79
+ | 'MISSING_REQUIREMENT_TEST'
80
+ | 'UNCOVERED_ACCEPTANCE_CRITERIA'
81
+ | 'MISSING_INTENT'
82
+ | 'UNCOVERED_EDGE_CASE'
83
+ | 'INSUFFICIENT_ASSERTIONS';
84
+
85
+ export type IssueSeverity = 'critical' | 'major' | 'minor';
86
+
87
+ export interface AlignmentIssue {
88
+ type: IssueType;
89
+ severity: IssueSeverity;
90
+ requirementId?: string;
91
+ acId?: string;
92
+ testId?: string;
93
+ message: string;
94
+ }
95
+
96
+ export interface CoverageReport {
97
+ requirementsCovered: number;
98
+ requirementsTotal: number;
99
+ acCovered: number;
100
+ acTotal: number;
101
+ edgeCasesCovered: number;
102
+ edgeCasesTotal: number;
103
+ testsWithIntent: number;
104
+ testsTotal: number;
105
+ }
106
+
107
+ export interface AlignmentReport {
108
+ score: number;
109
+ issues: AlignmentIssue[];
110
+ coverage: CoverageReport;
111
+ passed: boolean;
112
+ }
113
+
114
+ export interface TestAlignmentReportFile {
115
+ alignment_status: 'PASS' | 'FAIL' | 'BLOCKED';
116
+ phase: number;
117
+ score: number;
118
+ head_commit: string;
119
+ spec_hash: string;
120
+ timestamp: string;
121
+ misaligned_tests: Array<{
122
+ test_name: string;
123
+ spec_requirement: string;
124
+ gap: string;
125
+ }>;
126
+ anti_pattern_detected: boolean;
127
+ errors: string[];
128
+ }
129
+
130
+
131
+ // ============================================================================
132
+ // parseSpecification() — regex-based extraction for specification.yaml
133
+ // ============================================================================
134
+
135
+ export function parseSpecification(specPath: string): SpecificationMap {
136
+ if (!fs.existsSync(specPath)) {
137
+ throw new Error(`Specification file not found: ${specPath}`);
138
+ }
139
+
140
+ const content = fs.readFileSync(specPath, 'utf8');
141
+
142
+ // Split on REQ boundaries: "\n - id: REQ-" (canonical indentation)
143
+ // Also handle legacy "\n - id: REQ-" (minimal indentation)
144
+ const hasCanonicalIndent = content.includes('\n - id: REQ-');
145
+ const splitDelimiter = hasCanonicalIndent ? '\n - id: REQ-' : '\n - id: REQ-';
146
+ const blockPrefix = hasCanonicalIndent ? ' - id: REQ-' : ' - id: REQ-';
147
+
148
+ const blocks = content.split(splitDelimiter);
149
+
150
+ const requirements: Requirement[] = [];
151
+
152
+ for (let i = 1; i < blocks.length; i++) {
153
+ const fullBlock = blockPrefix + blocks[i]!;
154
+
155
+ const reqMatch = fullBlock.match(/- id: (REQ-[\w-]+)/);
156
+ if (!reqMatch) continue;
157
+
158
+ const reqId = reqMatch[1]!;
159
+
160
+ // Extract AC IDs: deeply indented "- id: AC-XXX-XX"
161
+ const acRegex = /- id: (AC-[\w-]+)/g;
162
+ const acIds: string[] = [];
163
+ let acMatch: RegExpExecArray | null;
164
+ while ((acMatch = acRegex.exec(fullBlock)) !== null) {
165
+ acIds.push(acMatch[1]!);
166
+ }
167
+
168
+ requirements.push({
169
+ id: reqId,
170
+ acceptanceCriteria: acIds,
171
+ });
172
+ }
173
+
174
+ const designDecisions: DesignDecision[] = [];
175
+ const ddRegex = /- id: ([\w-]+)/g;
176
+ const ddSection = content.match(/design_decisions:\n([\s\S]*?)(?=\n[^\s#-])/);
177
+ if (ddSection) {
178
+ let ddMatch: RegExpExecArray | null;
179
+ while ((ddMatch = ddRegex.exec(ddSection[1]!)) !== null) {
180
+ designDecisions.push({ id: ddMatch[1]! });
181
+ }
182
+ }
183
+
184
+ const apiContracts: ApiContract[] = [];
185
+ const apiRegex = /- endpoint: (.+)/g;
186
+ const apiSection = content.match(/api_contracts:\n([\s\S]*?)(?=\n[^\s#-])/);
187
+ if (apiSection) {
188
+ let apiMatch: RegExpExecArray | null;
189
+ while ((apiMatch = apiRegex.exec(apiSection[1]!)) !== null) {
190
+ apiContracts.push({ endpoint: apiMatch[1]!.trim() });
191
+ }
192
+ }
193
+
194
+ return { requirements, designDecisions, apiContracts };
195
+ }
196
+
197
+ // ============================================================================
198
+ // Step 2: parseTestFiles()
199
+ // ============================================================================
200
+
201
+ const REQ_ID_PATTERN = /REQ-[A-Z0-9]+-[A-Z0-9]+/;
202
+
203
+ function extractTag(text: string, tag: string): string | null {
204
+ // Match @tag value (single line extraction, stops at newline or */)
205
+ const regex = new RegExp('@' + tag.replace(/[.*+?^${}()|[\]\\]/g, '\\$&') + '\\s+(.+?)(?:\\n|\\*\\/)', 's');
206
+ const match = text.match(regex);
207
+ if (!match) return null;
208
+ return match[1]!.trim();
209
+ }
210
+
211
+ function extractAllTags(text: string, tag: string): string[] {
212
+ const escapedTag = tag.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
213
+ const regex = new RegExp('@' + escapedTag + '\\s+(.+?)(?:\\n|\\*\\/)', 'gs');
214
+ const results: string[] = [];
215
+ let match: RegExpExecArray | null;
216
+ while ((match = regex.exec(text)) !== null) {
217
+ const items1 = match[1]!.split(',').map(s => s.trim()).filter(Boolean);
218
+ results.push(...items1);
219
+ }
220
+ return results;
221
+ }
222
+
223
+ function countAssertions(content: string): number {
224
+ // Count common assertion patterns
225
+ const patterns = [
226
+ /expect\(/g,
227
+ /assert\./g,
228
+ /assert\(/g,
229
+ /\.should\./g,
230
+ /expect\./g,
231
+ /toEqual\(/g,
232
+ /toBe\(/g,
233
+ ];
234
+ let count = 0;
235
+ for (const pattern of patterns) {
236
+ const matches = content.match(pattern);
237
+ if (matches) count += matches.length;
238
+ }
239
+ return count;
240
+ }
241
+
242
+ /**
243
+ * Extract JSDoc block preceding a test/it/describe declaration.
244
+ * Handles .each(), .skip(), .todo() variants and backtick template literals.
245
+ */
246
+ function extractJSDocBeforeTest(content: string): Array<{ jsdoc: string; testBody: string; testName: string }> {
247
+ const results: Array<{ jsdoc: string; testBody: string; testName: string }> = [];
248
+
249
+ // Match: /** JSDOC */ (test|it|describe)[.modifier]( [args], )'name', ...
250
+ // `.each` variants have an extra `()` before the test name string
251
+ const testPattern = /\/\*\*([\s\S]*?)\*\/(?:[\s\n]*)(?:test|it|describe)(?:\.(?:each|skip|todo|only))?\s*\([^)]*\)\s*,\s*\(\s*(?:['"]([^'"]+)['"]|`([^`]*)`)\s*[,\)]/g;
252
+
253
+ let match: RegExpExecArray | null;
254
+ while ((match = testPattern.exec(content)) !== null) {
255
+ const jsdoc = match[1]!;
256
+ const testName = match[2] || match[3] || '';
257
+ if (testName) {
258
+ const testBody = content.slice(match.index, match.index + match[0].length);
259
+ results.push({ jsdoc, testBody, testName });
260
+ }
261
+ }
262
+
263
+ // Second pass: match standard test/it/describe without .each
264
+ const simplePattern = /\/\*\*([\s\S]*?)\*\/(?:[\s\n]*)(?:test|it|describe)(?:\.(?:skip|todo|only))?\s*\(\s*(?:['"]([^'"]+)['"]|`([^`]*)`)\s*[,\)]/g;
265
+
266
+ while ((match = simplePattern.exec(content)) !== null) {
267
+ const jsdoc = match[1]!;
268
+ const testName = match[2] || match[3] || '';
269
+ if (testName) {
270
+ // Don't duplicate if already found in first pass
271
+ const alreadyFound = results.some(r => r.jsdoc === jsdoc);
272
+ if (!alreadyFound) {
273
+ const testBody = content.slice(match.index, match.index + match[0].length);
274
+ results.push({ jsdoc, testBody, testName });
275
+ }
276
+ }
277
+ }
278
+
279
+ return results;
280
+ }
281
+
282
+ function pushTestWithCovers(
283
+ tests: TestCase[],
284
+ testPath: string,
285
+ name: string,
286
+ reqId: string,
287
+ intent: string | undefined,
288
+ fileContent: string,
289
+ totalAssertionsRef: { value: number },
290
+ ) {
291
+ const coversRegex = /@covers\s+(.+)/g;
292
+ const covers: string[] = [];
293
+ let coversMatch: RegExpExecArray | null;
294
+ while ((coversMatch = coversRegex.exec(fileContent)) !== null) {
295
+ covers.push(...coversMatch[1]!.split(',').map(s => s.trim()).filter(c => /^AC-[A-Z0-9]+-\d+-\d+$/.test(c)));
296
+ }
297
+
298
+ const assertions = countAssertions(fileContent);
299
+ totalAssertionsRef.value += assertions;
300
+
301
+ tests.push({
302
+ name,
303
+ file: testPath,
304
+ requirementId: reqId,
305
+ intent,
306
+ covers,
307
+ edgeCases: [],
308
+ assertions,
309
+ });
310
+ }
311
+
312
+ export function parseTestFiles(testPaths: string[]): TestMap {
313
+ const tests: TestCase[] = [];
314
+ const totalAssertionsObj = { value: 0 };
315
+
316
+ for (const testPath of testPaths) {
317
+ if (!fs.existsSync(testPath)) continue;
318
+
319
+ const content = fs.readFileSync(testPath, 'utf8');
320
+ const ext = path.extname(testPath);
321
+
322
+ if (ext === '.ts' || ext === '.tsx' || ext === '.js' || ext === '.jsx') {
323
+ const matches = extractJSDocBeforeTest(content);
324
+
325
+ for (const m of matches) {
326
+ const reqMatch = m.jsdoc.match(/@test\s+(REQ-[A-Z0-9]+-[A-Z0-9]+)/);
327
+ const intentMatch = m.jsdoc.match(/@intent\s+(.+?)(?:\n\s*\*)/s);
328
+ const coversRaw = extractAllTags(m.jsdoc, 'covers');
329
+ // Filter: only valid AC IDs (AC-XXX-NNN-NN pattern)
330
+ const covers = coversRaw.filter(c => /^AC-[A-Z0-9]+-\d+-\d+$/.test(c));
331
+
332
+ const assertions = countAssertions(content);
333
+ totalAssertionsObj.value += assertions;
334
+
335
+ tests.push({
336
+ name: m.testName,
337
+ file: testPath,
338
+ requirementId: reqMatch ? reqMatch[1] : undefined,
339
+ intent: intentMatch ? intentMatch[1]!.trim() : undefined,
340
+ covers,
341
+ edgeCases: [],
342
+ assertions,
343
+ });
344
+ }
345
+ } else if (ext === '.py') {
346
+ const pyTestPattern = /(# @test\s+(REQ-[A-Z0-9]+-[A-Z0-9]+)[\s\S]*?)(?:# @intent\s+(.+?)\n)?def\s+(test_\w+)/g;
347
+ let m: RegExpExecArray | null;
348
+ while ((m = pyTestPattern.exec(content)) !== null) {
349
+ pushTestWithCovers(tests, testPath, m[4]!, m[2]!, m[3]?.trim(), content, totalAssertionsObj);
350
+ }
351
+ } else if (ext === '.go') {
352
+ const goTestPattern = /(\/\/ @test\s+(REQ-[A-Z0-9]+-[A-Z0-9]+))\s*[\s\S]*?func\s+(Test_\w+)/g;
353
+ let m: RegExpExecArray | null;
354
+ while ((m = goTestPattern.exec(content)) !== null) {
355
+ const block = content.slice(Math.max(0, m.index - 500), m.index);
356
+ const intentMatch = block.match(/\/\/ @intent\s+(.+)/);
357
+ pushTestWithCovers(tests, testPath, m[3]!, m[2]!, intentMatch?.[1]?.trim(), content, totalAssertionsObj);
358
+ }
359
+ }
360
+ }
361
+
362
+ return {
363
+ tests,
364
+ totalTests: tests.length,
365
+ totalAssertions: totalAssertionsObj.value,
366
+ };
367
+ }
368
+
369
+ // ============================================================================
370
+ // Step 3: verifyAlignment()
371
+ // ============================================================================
372
+
373
+ export function verifyAlignment(
374
+ specMap: SpecificationMap,
375
+ testMap: TestMap,
376
+ ): AlignmentReport {
377
+ const issues: AlignmentIssue[] = [];
378
+
379
+ // Build coverage data
380
+ const coveredReqIds = new Set<string>();
381
+ const coveredAcIds = new Set<string>();
382
+ let testsWithIntent = 0;
383
+
384
+ for (const test of testMap.tests) {
385
+ if (test.requirementId) {
386
+ coveredReqIds.add(test.requirementId);
387
+ }
388
+ for (const acId of test.covers) {
389
+ coveredAcIds.add(acId);
390
+ }
391
+ if (test.intent && test.intent.trim().length > 0) {
392
+ testsWithIntent++;
393
+ }
394
+ }
395
+
396
+ // Collect all requirement IDs and acceptance criteria IDs from spec
397
+ const allReqIds = new Set<string>();
398
+ const allAcIds = new Set<string>();
399
+ let totalEdgeCases = 0;
400
+
401
+ for (const req of specMap.requirements) {
402
+ allReqIds.add(req.id);
403
+ for (const acId of req.acceptanceCriteria) {
404
+ allAcIds.add(acId);
405
+ }
406
+ }
407
+
408
+ // Rule 1: Each REQ must have a test (Critical)
409
+ for (const reqId of allReqIds) {
410
+ if (!coveredReqIds.has(reqId)) {
411
+ issues.push({
412
+ type: 'MISSING_REQUIREMENT_TEST',
413
+ severity: 'critical',
414
+ requirementId: reqId,
415
+ message: `Requirement ${reqId} has no associated test`,
416
+ });
417
+ }
418
+ }
419
+
420
+ // Rule 2: Each AC must have assertion coverage (Major)
421
+ for (const acId of allAcIds) {
422
+ if (!coveredAcIds.has(acId)) {
423
+ // Find which requirement this AC belongs to
424
+ const parentReq = specMap.requirements.find(r => r.acceptanceCriteria.includes(acId));
425
+ issues.push({
426
+ type: 'UNCOVERED_ACCEPTANCE_CRITERIA',
427
+ severity: 'major',
428
+ acId,
429
+ requirementId: parentReq?.id,
430
+ message: `Acceptance criterion ${acId} is not covered by any test`,
431
+ });
432
+ }
433
+ }
434
+
435
+ // Rule 3: Each test must have @intent (Major)
436
+ for (const test of testMap.tests) {
437
+ if (!test.intent || test.intent.trim().length === 0) {
438
+ issues.push({
439
+ type: 'MISSING_INTENT',
440
+ severity: 'major',
441
+ testId: test.name,
442
+ message: `Test "${test.name}" in ${test.file} is missing @intent annotation`,
443
+ });
444
+ }
445
+ }
446
+
447
+ // Rule 4: Each edge case should have a test (Minor) — not blocking in score
448
+
449
+ // Rule 5: Each test needs >= 2 assertions (Minor)
450
+ for (const test of testMap.tests) {
451
+ if (test.assertions < 2) {
452
+ issues.push({
453
+ type: 'INSUFFICIENT_ASSERTIONS',
454
+ severity: 'minor',
455
+ testId: test.name,
456
+ message: `Test "${test.name}" has only ${test.assertions} assertion(s) — consider adding more`,
457
+ });
458
+ }
459
+ }
460
+
461
+ const coverage: CoverageReport = {
462
+ requirementsCovered: coveredReqIds.size,
463
+ requirementsTotal: allReqIds.size,
464
+ acCovered: coveredAcIds.size,
465
+ acTotal: allAcIds.size,
466
+ edgeCasesCovered: 0,
467
+ edgeCasesTotal: totalEdgeCases,
468
+ testsWithIntent,
469
+ testsTotal: testMap.totalTests,
470
+ };
471
+
472
+ return {
473
+ score: 0, // computed in calculateScore
474
+ issues,
475
+ coverage,
476
+ passed: false,
477
+ };
478
+ }
479
+
480
+ // ============================================================================
481
+ // Step 4: calculateScore()
482
+ // Uses real avgAssertions from testMap — no hardcoded placeholder
483
+ // ============================================================================
484
+
485
+ export function calculateScore(
486
+ issues: AlignmentIssue[],
487
+ coverage: CoverageReport,
488
+ testMap: { totalTests: number; totalAssertions: number },
489
+ ): number {
490
+ // Weighted dimensions
491
+ const weights = {
492
+ requirementCoverage: 30,
493
+ acCoverage: 25,
494
+ testIntent: 20,
495
+ edgeCaseCoverage: 15,
496
+ dataValidity: 10,
497
+ };
498
+
499
+ // Requirement coverage score
500
+ let reqCoverageScore = 0;
501
+ if (coverage.requirementsTotal > 0) {
502
+ reqCoverageScore = (coverage.requirementsCovered / coverage.requirementsTotal) * weights.requirementCoverage;
503
+ }
504
+
505
+ // AC coverage score
506
+ let acCoverageScore = 0;
507
+ if (coverage.acTotal > 0) {
508
+ acCoverageScore = (coverage.acCovered / coverage.acTotal) * weights.acCoverage;
509
+ } else {
510
+ // No ACs → full score (nothing to cover)
511
+ acCoverageScore = weights.acCoverage;
512
+ }
513
+
514
+ // Test intent score
515
+ let intentScore = 0;
516
+ if (coverage.testsTotal > 0) {
517
+ intentScore = (coverage.testsWithIntent / coverage.testsTotal) * weights.testIntent;
518
+ }
519
+
520
+ // Edge case coverage (stub — no edge case tracking in v1)
521
+ const edgeCaseScore = (coverage.edgeCasesTotal > 0)
522
+ ? (coverage.edgeCasesCovered / coverage.edgeCasesTotal) * weights.edgeCaseCoverage
523
+ : weights.edgeCaseCoverage; // full score if no edge cases tracked
524
+
525
+ // Data validity: use real avgAssertions from testMap
526
+ const avgAssertions = testMap.totalTests > 0
527
+ ? testMap.totalAssertions / testMap.totalTests
528
+ : 0;
529
+ let dataValidityScore = 0;
530
+ if (avgAssertions >= 5) {
531
+ dataValidityScore = weights.dataValidity; // full score
532
+ } else if (avgAssertions >= 3) {
533
+ dataValidityScore = weights.dataValidity * 0.75;
534
+ } else if (avgAssertions >= 1) {
535
+ dataValidityScore = weights.dataValidity * 0.5;
536
+ } else {
537
+ dataValidityScore = 0;
538
+ }
539
+
540
+ let score = reqCoverageScore + acCoverageScore + intentScore + edgeCaseScore + dataValidityScore;
541
+
542
+ // Penalize for issues
543
+ for (const issue of issues) {
544
+ if (issue.severity === 'critical') {
545
+ score -= 5;
546
+ } else if (issue.severity === 'major') {
547
+ score -= 2;
548
+ }
549
+ // minor issues don't deduct (they're informational)
550
+ }
551
+
552
+ return Math.max(0, Math.round(score * 10) / 10);
553
+ }
554
+
555
+ // ============================================================================
556
+ // Step 5: writeReport()
557
+ // ============================================================================
558
+
559
+ export function writeReport(report: TestAlignmentReportFile, outputPath: string): void {
560
+ const dir = path.dirname(outputPath);
561
+ if (!fs.existsSync(dir)) {
562
+ fs.mkdirSync(dir, { recursive: true });
563
+ }
564
+
565
+ fs.writeFileSync(outputPath, JSON.stringify(report, null, 2), 'utf8');
566
+ }
567
+
568
+ // ============================================================================
569
+ // Orchestrator: runFullAlignment()
570
+ // ============================================================================
571
+
572
+ export interface AlignmentOptions {
573
+ specPath: string;
574
+ testDirs?: string[];
575
+ headCommit?: string;
576
+ outputPath?: string;
577
+ }
578
+
579
+ export function runFullAlignment(options: AlignmentOptions): TestAlignmentReportFile {
580
+ const { specPath, testDirs = ['tests', 'test', '__tests__', 'src/__tests__'], headCommit, outputPath } = options;
581
+
582
+ // Step 1: Parse spec
583
+ let spec: SpecificationMap;
584
+ try {
585
+ spec = parseSpecification(specPath);
586
+ } catch (e: any) {
587
+ return {
588
+ alignment_status: 'BLOCKED',
589
+ phase: 0,
590
+ score: 0,
591
+ head_commit: headCommit || 'unknown',
592
+ spec_hash: '',
593
+ timestamp: new Date().toISOString(),
594
+ misaligned_tests: [],
595
+ anti_pattern_detected: false,
596
+ errors: [`Failed to parse specification: ${e.message}`],
597
+ };
598
+ }
599
+
600
+ const specContent = fs.readFileSync(specPath, 'utf8');
601
+ const specHash = crypto.createHash('sha256').update(specContent).digest('hex');
602
+
603
+ // Step 2: Collect test files
604
+ const testFiles: string[] = [];
605
+ for (const dir of testDirs) {
606
+ if (fs.existsSync(dir)) {
607
+ const walkDir = (d: string) => {
608
+ const entries = fs.readdirSync(d, { withFileTypes: true });
609
+ for (const entry of entries) {
610
+ const fullPath = path.join(d, entry.name);
611
+ if (entry.isDirectory() && entry.name !== 'node_modules') {
612
+ walkDir(fullPath);
613
+ } else if (entry.isFile() && /\.(test|spec)\.(ts|tsx|js|jsx|py|go)$/.test(entry.name)) {
614
+ testFiles.push(fullPath);
615
+ }
616
+ }
617
+ };
618
+ walkDir(dir);
619
+ }
620
+ }
621
+
622
+ // Step 3: Parse tests
623
+ const testMap = parseTestFiles(testFiles);
624
+
625
+ // Step 4: Verify alignment
626
+ const alignmentReport = verifyAlignment(spec, testMap);
627
+ const score = calculateScore(alignmentReport.issues, alignmentReport.coverage, testMap);
628
+
629
+ const report: TestAlignmentReportFile = {
630
+ alignment_status: score >= 80 ? 'PASS' : 'FAIL',
631
+ phase: score >= 80 ? 2 : 1,
632
+ score,
633
+ head_commit: headCommit || 'unknown',
634
+ spec_hash: specHash,
635
+ timestamp: new Date().toISOString(),
636
+ misaligned_tests: alignmentReport.issues.map(issue => ({
637
+ test_name: issue.testId || 'unknown',
638
+ spec_requirement: issue.requirementId || 'unknown',
639
+ gap: issue.message,
640
+ })),
641
+ anti_pattern_detected: false,
642
+ errors: [],
643
+ };
644
+
645
+ // Step 5: Write output
646
+ if (outputPath) {
647
+ writeReport(report, outputPath);
648
+ }
649
+
650
+ return report;
651
+ }
@@ -1,9 +1,9 @@
1
1
  # SRC/MOCK-POLICY KNOWLEDGE BASE
2
2
 
3
- **Generated:** 2026-07-25
4
- **Commit:** 3150b22
3
+ **Generated:** 2026-07-29
4
+ **Commit:** 527fca8
5
5
  **Branch:** main
6
- **Version:** 0.18.0.0
6
+ **Version:** 0.18.3.0
7
7
 
8
8
  ## OVERVIEW
9
9
  Mock layering policy enforcement — Gate M3 of pre-push hook. Ensures integration tests use real implementations for internal dependencies, mock external dependencies, and annotate pending mocks with removal plans. Combines project scope scanning, mock decision engine, and per-file validation into a single pipeline.
@@ -1,9 +1,9 @@
1
1
  # SRC/MUTATION KNOWLEDGE BASE
2
2
 
3
- **Generated:** 2026-07-25
4
- **Commit:** 3150b22
3
+ **Generated:** 2026-07-29
4
+ **Commit:** 527fca8
5
5
  **Branch:** main
6
- **Version:** 0.18.0.0
6
+ **Version:** 0.18.3.0
7
7
 
8
8
  ## OVERVIEW
9
9
  **Gate M** (incremental mutation testing) + **Gate M2** helpers (test-layer detection used by `src/mock-policy/`). Pre-push quality gate. TypeScript-only; uses Stryker.