@boyingliu01/xp-gate 0.10.1 → 0.10.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.
@@ -0,0 +1,641 @@
1
+ import { execFile } from 'child_process';
2
+ import { promisify } from 'util';
3
+ import * as fs from 'fs/promises';
4
+ import * as fsNative from 'fs';
5
+ import path from 'path';
6
+ import { fileURLToPath } from 'url';
7
+ import type {
8
+ CheckResult,
9
+ Gate10Options,
10
+ Gate10Result,
11
+ Gate10Status,
12
+ ImportCheckResult,
13
+ ImportViolation,
14
+ } from './types';
15
+
16
+ const execFileAsync = promisify(execFile);
17
+
18
+ /**
19
+ * Gate 10: Build Integrity Check — TypeScript compilation verification.
20
+ * Runs `tsc --noEmit --incremental` to verify the project compiles without errors.
21
+ *
22
+ * @param projectRoot - Absolute path to the project root
23
+ * @param timeoutMs - Timeout in milliseconds
24
+ * @returns CheckResult with status 'pass', 'fail', or 'skip'
25
+ */
26
+ export async function runTscCheck(
27
+ projectRoot: string,
28
+ timeoutMs: number
29
+ ): Promise<CheckResult> {
30
+ const startTime = Date.now();
31
+
32
+ // Check if tsconfig.json exists
33
+ const tsconfigPath = path.join(projectRoot, 'tsconfig.json');
34
+ try {
35
+ await fs.access(tsconfigPath);
36
+ } catch {
37
+ return {
38
+ status: 'skip',
39
+ message: 'No tsconfig.json found in project root',
40
+ durationMs: Date.now() - startTime,
41
+ };
42
+ }
43
+
44
+ // Check if tsc is available (run from projectRoot where node_modules should exist)
45
+ try {
46
+ await execFileAsync('npx', ['tsc', '--version'], {
47
+ cwd: projectRoot,
48
+ timeout: 5000,
49
+ env: { ...process.env, PATH: process.env.PATH },
50
+ });
51
+ } catch {
52
+ return {
53
+ status: 'skip',
54
+ message: 'tsc is not available on PATH',
55
+ durationMs: Date.now() - startTime,
56
+ };
57
+ }
58
+
59
+ // Run tsc --noEmit --incremental
60
+ try {
61
+ const { stdout, stderr } = await execFileAsync(
62
+ 'npx',
63
+ ['tsc', '--noEmit', '--incremental'],
64
+ {
65
+ cwd: projectRoot,
66
+ timeout: timeoutMs,
67
+ maxBuffer: 10 * 1024 * 1024, // 10MB
68
+ }
69
+ );
70
+
71
+ const output = stdout + stderr;
72
+
73
+ return {
74
+ status: 'pass',
75
+ message: output.trim() || 'TypeScript compilation successful',
76
+ durationMs: Date.now() - startTime,
77
+ };
78
+ } catch (error: unknown) {
79
+ const err = error as {
80
+ code?: number;
81
+ signal?: string;
82
+ stdout?: string;
83
+ stderr?: string;
84
+ killed?: boolean;
85
+ message?: string;
86
+ };
87
+
88
+ // Timeout → SKIP
89
+ if (err.killed || err.signal === 'SIGTERM' || err.code === null) {
90
+ return {
91
+ status: 'skip',
92
+ message: `tsc timed out after ${timeoutMs}ms`,
93
+ durationMs: Date.now() - startTime,
94
+ };
95
+ }
96
+
97
+ // Non-zero exit → FAIL
98
+ const output = (err.stdout || '') + (err.stderr || '');
99
+ const truncatedOutput = output.slice(0, 2000);
100
+
101
+ // Count errors in output
102
+ const errorMatches = output.match(/error TS\d+:/g);
103
+ const errorCount = errorMatches ? errorMatches.length : 0;
104
+
105
+ const message = errorCount > 0
106
+ ? `TypeScript compilation failed with ${errorCount} error(s):\n${truncatedOutput}`
107
+ : `TypeScript compilation failed:\n${truncatedOutput}`;
108
+
109
+ return {
110
+ status: 'fail',
111
+ message,
112
+ durationMs: Date.now() - startTime,
113
+ };
114
+ }
115
+ }
116
+
117
+ /**
118
+ * Extracts relative import paths from TypeScript/JavaScript source code.
119
+ * Skips bare npm package imports and node: protocol imports.
120
+ *
121
+ * Supported forms:
122
+ * import { x } from './foo'
123
+ * import x from './foo'
124
+ * import * as x from './foo'
125
+ * import './foo'
126
+ * const x = require('./foo')
127
+ * import('./foo')
128
+ * export { x } from './foo'
129
+ * export * from './foo'
130
+ */
131
+ export function extractImports(content: string): Array<{ path: string; line: number }> {
132
+ const results: Array<{ path: string; line: number }> = [];
133
+ const lines = content.split('\n');
134
+
135
+ const staticImportExportRe =
136
+ /(?:import|export)\s+(?:(?:[^'"]*?\s+from\s+)?['"]([^'"]+)['"]|['"]([^'"]+)['"])/g;
137
+ const requireRe = /require\(\s*['"]([^'"]+)['"]\s*\)/g;
138
+ const dynamicImportRe = /(?<!\w)import\(\s*['"]([^'"]+)['"]\s*\)/g;
139
+
140
+ for (let i = 0; i < lines.length; i++) {
141
+ const line = lines[i];
142
+ const lineNum = i + 1;
143
+
144
+ // Skip comment lines (// or leading * in JSDoc blocks)
145
+ const trimmed = line.trim();
146
+ if (trimmed.startsWith('//') || trimmed.startsWith('*') || trimmed.startsWith('/*')) {
147
+ continue;
148
+ }
149
+
150
+ let match: RegExpExecArray | null;
151
+ const staticRe = new RegExp(staticImportExportRe.source, 'g');
152
+ while ((match = staticRe.exec(line)) !== null) {
153
+ const importPath = match[1] ?? match[2];
154
+ if (importPath && isRelativeImport(importPath)) {
155
+ results.push({ path: importPath, line: lineNum });
156
+ }
157
+ }
158
+
159
+ const reqRe = new RegExp(requireRe.source, 'g');
160
+ while ((match = reqRe.exec(line)) !== null) {
161
+ const importPath = match[1];
162
+ if (importPath && isRelativeImport(importPath)) {
163
+ results.push({ path: importPath, line: lineNum });
164
+ }
165
+ }
166
+
167
+ const dynRe = new RegExp(dynamicImportRe.source, 'g');
168
+ while ((match = dynRe.exec(line)) !== null) {
169
+ const importPath = match[1];
170
+ if (importPath && isRelativeImport(importPath)) {
171
+ results.push({ path: importPath, line: lineNum });
172
+ }
173
+ }
174
+ }
175
+
176
+ return results;
177
+ }
178
+
179
+ function isRelativeImport(importPath: string): boolean {
180
+ return importPath.startsWith('./') || importPath.startsWith('../') || importPath === '.' || importPath === '..';
181
+ }
182
+
183
+ /**
184
+ * Resolves a relative import path to an absolute file path on disk.
185
+ * Returns null for bare npm package imports, node: protocol imports,
186
+ * or when the target file cannot be found.
187
+ *
188
+ * Checks extensions in order: .ts, .tsx, .js, .jsx, .mjs, .cjs, /index.ts, /index.js
189
+ * Also handles explicit .js extension resolving to .ts (TypeScript convention).
190
+ */
191
+ export function resolveImportPath(importPath: string, fromFile: string): string | null {
192
+ if (!isRelativeImport(importPath)) {
193
+ return null;
194
+ }
195
+
196
+ const fromDir = path.dirname(fromFile);
197
+ const resolved = path.resolve(fromDir, importPath);
198
+
199
+ const extensions = ['.ts', '.tsx', '.js', '.jsx', '.mjs', '.cjs'];
200
+ const indexExtensions = ['/index.ts', '/index.js'];
201
+
202
+ const ext = path.extname(importPath);
203
+ if (ext) {
204
+ try {
205
+ fsNative.accessSync(resolved);
206
+ return resolved;
207
+ } catch {
208
+ if (ext === '.js') {
209
+ const tsPath = resolved.slice(0, -3) + '.ts';
210
+ try {
211
+ fsNative.accessSync(tsPath);
212
+ return tsPath;
213
+ } catch {
214
+ // fall through
215
+ }
216
+ const tsxPath = resolved.slice(0, -3) + '.tsx';
217
+ try {
218
+ fsNative.accessSync(tsxPath);
219
+ return tsxPath;
220
+ } catch {
221
+ // fall through
222
+ }
223
+ }
224
+ return null;
225
+ }
226
+ }
227
+
228
+ for (const tryExt of extensions) {
229
+ const candidate = resolved + tryExt;
230
+ try {
231
+ fsNative.accessSync(candidate);
232
+ return candidate;
233
+ } catch {
234
+ // continue
235
+ }
236
+ }
237
+
238
+ for (const indexExt of indexExtensions) {
239
+ const candidate = resolved + indexExt;
240
+ try {
241
+ fsNative.accessSync(candidate);
242
+ return candidate;
243
+ } catch {
244
+ // continue
245
+ }
246
+ }
247
+
248
+ return null;
249
+ }
250
+
251
+ /**
252
+ * Runs the import check on a list of changed files.
253
+ * Verifies that all relative imports in the changed files resolve to files
254
+ * within the project boundary.
255
+ *
256
+ * FAIL conditions:
257
+ * - A relative import resolves to a path outside projectRoot
258
+ * - A relative import resolves to a file that doesn't exist on disk
259
+ */
260
+ export async function runImportCheck(
261
+ changedFiles: string[],
262
+ projectRoot: string,
263
+ _timeoutMs: number
264
+ ): Promise<ImportCheckResult> {
265
+ const startTime = Date.now();
266
+ const violations: ImportViolation[] = [];
267
+
268
+ const fsSync = fsNative;
269
+
270
+ for (const file of changedFiles) {
271
+ // Skip test files — they contain intentionally broken imports for unit testing.
272
+ // The published npm package does not include __tests__ directories.
273
+ if (file.includes('__tests__')) continue;
274
+
275
+ try {
276
+ fsSync.accessSync(file);
277
+ } catch {
278
+ continue;
279
+ }
280
+
281
+ let content: string;
282
+ try {
283
+ content = await fs.readFile(file, 'utf-8');
284
+ } catch {
285
+ continue;
286
+ }
287
+
288
+ const imports = extractImports(content);
289
+
290
+ for (const imp of imports) {
291
+ const rawResolved = path.resolve(path.dirname(file), imp.path);
292
+ const normalizedRoot = path.resolve(projectRoot);
293
+
294
+ const escapesBoundary =
295
+ !rawResolved.startsWith(normalizedRoot + path.sep) && rawResolved !== normalizedRoot;
296
+
297
+ if (escapesBoundary) {
298
+ violations.push({
299
+ file,
300
+ line: imp.line,
301
+ importPath: imp.path,
302
+ resolvedPath: rawResolved,
303
+ reason: `Import path escapes package boundary (resolves outside project root)`,
304
+ });
305
+ continue;
306
+ }
307
+
308
+ const resolvedPath = resolveImportPath(imp.path, file);
309
+
310
+ if (resolvedPath === null) {
311
+ violations.push({
312
+ file,
313
+ line: imp.line,
314
+ importPath: imp.path,
315
+ resolvedPath: rawResolved,
316
+ reason: `Import target does not exist on disk`,
317
+ });
318
+ }
319
+ }
320
+ }
321
+
322
+ const durationMs = Date.now() - startTime;
323
+
324
+ if (violations.length > 0) {
325
+ return {
326
+ status: 'fail',
327
+ message: `Found ${violations.length} import violation(s): imports resolve outside project boundary or target missing`,
328
+ durationMs,
329
+ violations,
330
+ };
331
+ }
332
+
333
+ return {
334
+ status: 'pass',
335
+ message: `All relative imports in ${changedFiles.length} file(s) resolve within project boundary`,
336
+ durationMs,
337
+ violations: [],
338
+ };
339
+ }
340
+
341
+ export async function runPackCheck(
342
+ projectRoot: string,
343
+ timeoutMs: number
344
+ ): Promise<CheckResult> {
345
+ const startTime = Date.now();
346
+
347
+ const pkgJsonPath = path.join(projectRoot, 'package.json');
348
+ try {
349
+ await fs.access(pkgJsonPath);
350
+ } catch {
351
+ return {
352
+ status: 'skip',
353
+ message: 'No package.json found in project root',
354
+ durationMs: Date.now() - startTime,
355
+ };
356
+ }
357
+
358
+ const pkgJsonRaw = await fs.readFile(pkgJsonPath, 'utf-8');
359
+ const pkgJson = JSON.parse(pkgJsonRaw) as { files?: unknown };
360
+ if (!pkgJson.files || !Array.isArray(pkgJson.files)) {
361
+ return {
362
+ status: 'skip',
363
+ message: 'package.json has no files field — skipping pack check',
364
+ durationMs: Date.now() - startTime,
365
+ };
366
+ }
367
+
368
+ try {
369
+ const { stdout } = await execFileAsync('npm', ['pack', '--dry-run', '--json'], {
370
+ cwd: projectRoot,
371
+ timeout: timeoutMs,
372
+ maxBuffer: 10 * 1024 * 1024,
373
+ });
374
+
375
+ const packages = JSON.parse(stdout) as Array<{ files?: Array<{ path: string }> }>;
376
+ if (!packages || packages.length === 0) {
377
+ return {
378
+ status: 'fail',
379
+ message: 'npm pack produced no package output',
380
+ durationMs: Date.now() - startTime,
381
+ };
382
+ }
383
+
384
+ const files = packages[0].files || [];
385
+ if (files.length === 0) {
386
+ return {
387
+ status: 'fail',
388
+ message: 'npm pack produced empty file list',
389
+ durationMs: Date.now() - startTime,
390
+ };
391
+ }
392
+
393
+ // Check if only package.json is included (no actual content files)
394
+ const hasContentFiles = files.some((f) => f.path !== 'package.json');
395
+ if (!hasContentFiles) {
396
+ return {
397
+ status: 'fail',
398
+ message: 'npm pack produced no files — only package.json included',
399
+ durationMs: Date.now() - startTime,
400
+ };
401
+ }
402
+
403
+ return {
404
+ status: 'pass',
405
+ message: `npm pack: ${files.length} file(s) included`,
406
+ durationMs: Date.now() - startTime,
407
+ };
408
+ } catch (error: unknown) {
409
+ const err = error as {
410
+ killed?: boolean;
411
+ signal?: string;
412
+ code?: number | null;
413
+ stderr?: string;
414
+ stdout?: string;
415
+ message?: string;
416
+ };
417
+
418
+ if (err.killed || err.signal === 'SIGTERM') {
419
+ return {
420
+ status: 'skip',
421
+ message: `npm pack timeout after ${timeoutMs}ms`,
422
+ durationMs: Date.now() - startTime,
423
+ };
424
+ }
425
+
426
+ const detail = (err.stderr || err.stdout || err.message || '').slice(0, 2000);
427
+ return {
428
+ status: 'fail',
429
+ message: `npm pack failed: ${detail}`,
430
+ durationMs: Date.now() - startTime,
431
+ };
432
+ }
433
+ }
434
+
435
+ // ─── Gate 10 Orchestrator ──────────────────────────────────────────────────────
436
+
437
+ /**
438
+ * Runs all three Gate 10 checks (tsc, pack, imports) in parallel and combines
439
+ * the results into a single Gate10Result.
440
+ *
441
+ * Exit logic:
442
+ * - BLOCK (exitCode 1, status 'block') when ANY check returns status 'fail'.
443
+ * - PASS (exitCode 0, status 'pass') when all checks return 'pass' or 'skip'
444
+ * AND at least one check returned 'pass'.
445
+ * - SKIP (exitCode 0, status 'skip') when all checks return 'skip' (or no
446
+ * applicable checks found).
447
+ */
448
+ export async function runGate10(options: Gate10Options): Promise<Gate10Result> {
449
+ const { changedFiles, projectRoot, timeoutMs } = options;
450
+
451
+ const [tscResult, packResult, importResult] = await Promise.all([
452
+ runTscCheck(projectRoot, timeoutMs),
453
+ runPackCheck(projectRoot, timeoutMs),
454
+ runImportCheck(changedFiles, projectRoot, timeoutMs),
455
+ ]);
456
+
457
+ const errors: string[] = [];
458
+ const warnings: string[] = [];
459
+
460
+ if (tscResult.status === 'fail') {
461
+ errors.push(`tsc: ${tscResult.message}`);
462
+ }
463
+ if (packResult.status === 'fail') {
464
+ errors.push(`pack: ${packResult.message}`);
465
+ }
466
+ if (importResult.status === 'fail') {
467
+ errors.push(`imports: ${importResult.message}`);
468
+ }
469
+
470
+ const anyFail =
471
+ tscResult.status === 'fail' ||
472
+ packResult.status === 'fail' ||
473
+ importResult.status === 'fail';
474
+
475
+ const allSkip =
476
+ tscResult.status === 'skip' &&
477
+ packResult.status === 'skip' &&
478
+ importResult.status === 'skip';
479
+
480
+ const allSkipOrPass =
481
+ (tscResult.status === 'skip' || tscResult.status === 'pass') &&
482
+ (packResult.status === 'skip' || packResult.status === 'pass') &&
483
+ (importResult.status === 'skip' || importResult.status === 'pass');
484
+
485
+ const anyPass =
486
+ tscResult.status === 'pass' ||
487
+ packResult.status === 'pass' ||
488
+ importResult.status === 'pass';
489
+
490
+ let status: Gate10Status;
491
+ let exitCode: number;
492
+
493
+ if (anyFail) {
494
+ status = 'block';
495
+ exitCode = 1;
496
+ } else if (allSkip || (allSkipOrPass && !anyPass)) {
497
+ status = 'skip';
498
+ exitCode = 0;
499
+ } else {
500
+ status = 'pass';
501
+ exitCode = 0;
502
+ }
503
+
504
+ return {
505
+ exitCode,
506
+ status,
507
+ checks: {
508
+ tsc: tscResult,
509
+ pack: packResult,
510
+ imports: importResult,
511
+ },
512
+ warnings,
513
+ errors,
514
+ };
515
+ }
516
+
517
+ // ─── CLI Entry Point ───────────────────────────────────────────────────────────
518
+
519
+ /**
520
+ * Parses CLI arguments and runs Gate 10.
521
+ *
522
+ * Usage:
523
+ * npx tsx src/build-integrity/gate-10.ts --changed-files "file1.ts,file2.ts"
524
+ *
525
+ * Options:
526
+ * --changed-files <files> Comma-separated list of changed file paths
527
+ * --project-root <path> Project root (defaults to cwd)
528
+ * --timeout <ms> Timeout per check in ms (defaults to 60000)
529
+ * --help Show usage information
530
+ *
531
+ * @returns exit code: 0 = pass/skip, 1 = block
532
+ */
533
+ export async function main(args: string[]): Promise<number> {
534
+ let changedFilesStr = '';
535
+ let projectRoot = process.cwd();
536
+ let timeoutMs = 60000;
537
+
538
+ for (let i = 0; i < args.length; i++) {
539
+ const arg = args[i];
540
+ if (arg === '--help' || arg === '-h') {
541
+ console.log(`Gate 10: Build Integrity Check
542
+
543
+ Usage:
544
+ npx tsx src/build-integrity/gate-10.ts --changed-files <files> [options]
545
+
546
+ Options:
547
+ --changed-files <files> Comma-separated list of changed file paths
548
+ --project-root <path> Project root (defaults to cwd)
549
+ --timeout <ms> Timeout per check in ms (defaults to 60000)
550
+ --help, -h Show this help message
551
+
552
+ Exit codes:
553
+ 0 Pass or skip (all checks passed or were skipped)
554
+ 1 Block (one or more checks failed)`);
555
+ return 0;
556
+ } else if (arg === '--changed-files') {
557
+ changedFilesStr = args[++i] || '';
558
+ } else if (arg === '--project-root') {
559
+ projectRoot = args[++i] || process.cwd();
560
+ } else if (arg === '--timeout') {
561
+ timeoutMs = parseInt(args[++i] || '60000', 10);
562
+ }
563
+ }
564
+
565
+ const changedFiles = changedFilesStr
566
+ ? changedFilesStr.split(',').map((f) => f.trim()).filter(Boolean)
567
+ : [];
568
+
569
+ const result = await runGate10({
570
+ changedFiles,
571
+ projectRoot,
572
+ timeoutMs,
573
+ });
574
+
575
+ console.log('');
576
+ console.log('Gate 10: Build Integrity Check');
577
+ console.log('─'.repeat(50));
578
+ console.log(
579
+ ` tsc: ${formatStatus(result.checks.tsc.status)} ${result.checks.tsc.message.split('\n')[0]} (${result.checks.tsc.durationMs}ms)`
580
+ );
581
+ console.log(
582
+ ` pack: ${formatStatus(result.checks.pack.status)} ${result.checks.pack.message.split('\n')[0]} (${result.checks.pack.durationMs}ms)`
583
+ );
584
+ console.log(
585
+ ` imports: ${formatStatus(result.checks.imports.status)} ${result.checks.imports.message.split('\n')[0]} (${result.checks.imports.durationMs}ms)`
586
+ );
587
+ console.log('─'.repeat(50));
588
+ console.log(` Overall: ${formatStatus(result.status)}`);
589
+
590
+ if (result.errors.length > 0) {
591
+ console.log('');
592
+ console.log(' Errors:');
593
+ for (const err of result.errors) {
594
+ console.log(` - ${err.split('\n')[0]}`);
595
+ }
596
+ }
597
+
598
+ if (result.warnings.length > 0) {
599
+ console.log('');
600
+ console.log(' Warnings:');
601
+ for (const warn of result.warnings) {
602
+ console.log(` - ${warn}`);
603
+ }
604
+ }
605
+
606
+ console.log('');
607
+ return result.exitCode;
608
+ }
609
+
610
+ function formatStatus(status: string): string {
611
+ switch (status) {
612
+ case 'pass':
613
+ return 'PASS';
614
+ case 'fail':
615
+ return 'FAIL';
616
+ case 'skip':
617
+ return 'SKIP';
618
+ case 'block':
619
+ return 'BLOCK';
620
+ default:
621
+ return status.toUpperCase();
622
+ }
623
+ }
624
+
625
+ const isDirectRun = (() => {
626
+ try {
627
+ const scriptPath = process.argv[1];
628
+ if (!scriptPath) return false;
629
+ const normalizedScript = path.resolve(scriptPath);
630
+ const normalizedThis = path.resolve(fileURLToPath(import.meta.url));
631
+ return normalizedScript === normalizedThis;
632
+ } catch {
633
+ return false;
634
+ }
635
+ })();
636
+
637
+ if (isDirectRun) {
638
+ main(process.argv.slice(2)).then((exitCode) => {
639
+ process.exit(exitCode);
640
+ });
641
+ }
@@ -0,0 +1,51 @@
1
+ // Gate 10: Build/Compile Integrity Check — Type Definitions
2
+
3
+ /** Parameters for running the Gate 10 build integrity check. */
4
+ export interface Gate10Options {
5
+ changedFiles: string[];
6
+ projectRoot: string;
7
+ timeoutMs: number;
8
+ }
9
+
10
+ /** Overall gate status. */
11
+ export type Gate10Status = 'pass' | 'block' | 'skip';
12
+
13
+ /** Result of a single check (tsc, pack, or imports). */
14
+ export interface CheckResult {
15
+ status: 'pass' | 'fail' | 'skip';
16
+ message: string;
17
+ durationMs: number;
18
+ }
19
+
20
+ /** A broken import found by the import resolver. */
21
+ export interface ImportViolation {
22
+ /** Absolute path to the file containing the broken import. */
23
+ file: string;
24
+ /** 1-based line number of the import statement. */
25
+ line: number;
26
+ /** The raw import path string (e.g. `../../src/baz`). */
27
+ importPath: string;
28
+ /** What the import resolved to on disk. */
29
+ resolvedPath: string;
30
+ /** Human-readable explanation (e.g. "path escapes package boundary"). */
31
+ reason: string;
32
+ }
33
+
34
+ /** Result of the import resolver check (extends CheckResult with violations). */
35
+ export interface ImportCheckResult extends CheckResult {
36
+ violations: ImportViolation[];
37
+ }
38
+
39
+ /** Complete result returned by Gate 10. */
40
+ export interface Gate10Result {
41
+ /** Exit code: 0 = allow, 1 = block. */
42
+ exitCode: number;
43
+ status: Gate10Status;
44
+ checks: {
45
+ tsc: CheckResult;
46
+ pack: CheckResult;
47
+ imports: ImportCheckResult;
48
+ };
49
+ warnings: string[];
50
+ errors: string[];
51
+ }
@@ -1,9 +1,9 @@
1
1
  # SRC/MOCK-POLICY KNOWLEDGE BASE
2
2
 
3
3
  **Generated:** 2026-06-22
4
- **Commit:** 448ac7e
4
+ **Commit:** 1cc434d
5
5
  **Branch:** main
6
- **Version:** 0.10.1.0
6
+ **Version:** 0.10.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
3
  **Generated:** 2026-06-22
4
- **Commit:** 448ac7e
4
+ **Commit:** 1cc434d
5
5
  **Branch:** main
6
- **Version:** 0.10.1.0
6
+ **Version:** 0.10.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.