tlc-claude-code 1.6.4 → 1.8.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 (105) hide show
  1. package/dashboard/dist/components/ContainerSecurityPane.d.ts +45 -0
  2. package/dashboard/dist/components/ContainerSecurityPane.js +44 -0
  3. package/dashboard/dist/components/ContainerSecurityPane.test.d.ts +1 -0
  4. package/dashboard/dist/components/ContainerSecurityPane.test.js +153 -0
  5. package/package.json +1 -1
  6. package/server/lib/access-control.test.js +1 -1
  7. package/server/lib/agents-cancel-command.test.js +1 -1
  8. package/server/lib/agents-get-command.test.js +1 -1
  9. package/server/lib/agents-list-command.test.js +1 -1
  10. package/server/lib/agents-logs-command.test.js +1 -1
  11. package/server/lib/agents-retry-command.test.js +1 -1
  12. package/server/lib/budget-limits.test.js +2 -2
  13. package/server/lib/code-gate/bypass-logger.js +129 -0
  14. package/server/lib/code-gate/bypass-logger.test.js +142 -0
  15. package/server/lib/code-gate/first-commit-audit.js +138 -0
  16. package/server/lib/code-gate/first-commit-audit.test.js +203 -0
  17. package/server/lib/code-gate/gate-command.js +114 -0
  18. package/server/lib/code-gate/gate-command.test.js +111 -0
  19. package/server/lib/code-gate/gate-config.js +163 -0
  20. package/server/lib/code-gate/gate-config.test.js +181 -0
  21. package/server/lib/code-gate/gate-engine.js +193 -0
  22. package/server/lib/code-gate/gate-engine.test.js +258 -0
  23. package/server/lib/code-gate/gate-reporter.js +123 -0
  24. package/server/lib/code-gate/gate-reporter.test.js +159 -0
  25. package/server/lib/code-gate/hooks-generator.js +149 -0
  26. package/server/lib/code-gate/hooks-generator.test.js +142 -0
  27. package/server/lib/code-gate/llm-reviewer.js +176 -0
  28. package/server/lib/code-gate/llm-reviewer.test.js +161 -0
  29. package/server/lib/code-gate/multi-model-reviewer.js +172 -0
  30. package/server/lib/code-gate/multi-model-reviewer.test.js +217 -0
  31. package/server/lib/code-gate/push-gate.js +133 -0
  32. package/server/lib/code-gate/push-gate.test.js +190 -0
  33. package/server/lib/code-gate/rules/architecture-rules.js +228 -0
  34. package/server/lib/code-gate/rules/architecture-rules.test.js +155 -0
  35. package/server/lib/code-gate/rules/client-rules.js +120 -0
  36. package/server/lib/code-gate/rules/client-rules.test.js +121 -0
  37. package/server/lib/code-gate/rules/config-rules.js +140 -0
  38. package/server/lib/code-gate/rules/config-rules.test.js +103 -0
  39. package/server/lib/code-gate/rules/database-rules.js +158 -0
  40. package/server/lib/code-gate/rules/database-rules.test.js +119 -0
  41. package/server/lib/code-gate/rules/docker-rules.js +201 -0
  42. package/server/lib/code-gate/rules/docker-rules.test.js +104 -0
  43. package/server/lib/code-gate/rules/quality-rules.js +304 -0
  44. package/server/lib/code-gate/rules/quality-rules.test.js +199 -0
  45. package/server/lib/code-gate/rules/security-rules.js +228 -0
  46. package/server/lib/code-gate/rules/security-rules.test.js +131 -0
  47. package/server/lib/code-gate/rules/structure-rules.js +155 -0
  48. package/server/lib/code-gate/rules/structure-rules.test.js +107 -0
  49. package/server/lib/code-gate/rules/test-rules.js +93 -0
  50. package/server/lib/code-gate/rules/test-rules.test.js +97 -0
  51. package/server/lib/code-gate/typescript-gate.js +128 -0
  52. package/server/lib/code-gate/typescript-gate.test.js +131 -0
  53. package/server/lib/code-generator.test.js +1 -1
  54. package/server/lib/cost-command.test.js +1 -1
  55. package/server/lib/cost-optimizer.test.js +1 -1
  56. package/server/lib/cost-projections.test.js +1 -1
  57. package/server/lib/cost-reports.test.js +1 -1
  58. package/server/lib/cost-tracker.test.js +1 -1
  59. package/server/lib/crypto-patterns.test.js +1 -1
  60. package/server/lib/design-command.test.js +1 -1
  61. package/server/lib/design-parser.test.js +1 -1
  62. package/server/lib/gemini-vision.test.js +1 -1
  63. package/server/lib/infra/infra-generator.js +331 -0
  64. package/server/lib/infra/infra-generator.test.js +146 -0
  65. package/server/lib/input-validator.test.js +1 -1
  66. package/server/lib/litellm-client.test.js +1 -1
  67. package/server/lib/litellm-command.test.js +1 -1
  68. package/server/lib/litellm-config.test.js +1 -1
  69. package/server/lib/llm/adapters/api-adapter.js +95 -0
  70. package/server/lib/llm/adapters/api-adapter.test.js +81 -0
  71. package/server/lib/llm/adapters/codex-adapter.js +85 -0
  72. package/server/lib/llm/adapters/codex-adapter.test.js +54 -0
  73. package/server/lib/llm/adapters/gemini-adapter.js +100 -0
  74. package/server/lib/llm/adapters/gemini-adapter.test.js +54 -0
  75. package/server/lib/llm/index.js +109 -0
  76. package/server/lib/llm/index.test.js +147 -0
  77. package/server/lib/llm/provider-executor.js +168 -0
  78. package/server/lib/llm/provider-executor.test.js +244 -0
  79. package/server/lib/llm/provider-registry.js +104 -0
  80. package/server/lib/llm/provider-registry.test.js +157 -0
  81. package/server/lib/llm/review-service.js +222 -0
  82. package/server/lib/llm/review-service.test.js +220 -0
  83. package/server/lib/model-pricing.test.js +1 -1
  84. package/server/lib/models-command.test.js +1 -1
  85. package/server/lib/optimize-command.test.js +1 -1
  86. package/server/lib/orchestration-integration.test.js +1 -1
  87. package/server/lib/output-encoder.test.js +1 -1
  88. package/server/lib/quality-evaluator.test.js +1 -1
  89. package/server/lib/quality-gate-command.test.js +1 -1
  90. package/server/lib/quality-gate-scorer.test.js +1 -1
  91. package/server/lib/quality-history.test.js +1 -1
  92. package/server/lib/quality-presets.test.js +1 -1
  93. package/server/lib/quality-retry.test.js +1 -1
  94. package/server/lib/quality-thresholds.test.js +1 -1
  95. package/server/lib/secure-auth.test.js +1 -1
  96. package/server/lib/secure-code-command.test.js +1 -1
  97. package/server/lib/secure-errors.test.js +1 -1
  98. package/server/lib/security/auth-security.test.js +4 -3
  99. package/server/lib/shame/shame-registry.js +224 -0
  100. package/server/lib/shame/shame-registry.test.js +202 -0
  101. package/server/lib/standards/cleanup-dry-run.js +254 -0
  102. package/server/lib/standards/cleanup-dry-run.test.js +220 -0
  103. package/server/lib/vision-command.test.js +1 -1
  104. package/server/lib/visual-command.test.js +1 -1
  105. package/server/lib/visual-testing.test.js +1 -1
@@ -0,0 +1,155 @@
1
+ /**
2
+ * Structure Rules
3
+ *
4
+ * Detects flat folder anti-patterns, files in wrong locations,
5
+ * and missing test colocation.
6
+ *
7
+ * @module code-gate/rules/structure-rules
8
+ */
9
+
10
+ const path = require('path');
11
+
12
+ /** Folder names that indicate flat anti-patterns when at root level */
13
+ const FLAT_FOLDER_PATTERNS = ['services', 'controllers', 'interfaces', 'helpers', 'utils'];
14
+
15
+ /** Files allowed at project root */
16
+ const ALLOWED_ROOT_FILES = [
17
+ 'index.js', 'index.ts', 'index.mjs',
18
+ 'package.json', 'package-lock.json',
19
+ 'tsconfig.json', 'vitest.config.js', 'vitest.config.ts',
20
+ 'jest.config.js', 'jest.config.ts', '.eslintrc.js',
21
+ ];
22
+
23
+ /** Patterns for allowed root file naming */
24
+ const ALLOWED_ROOT_PATTERNS = [
25
+ /^\./, // dotfiles
26
+ /\.config\.[jt]s$/, // config files
27
+ /\.json$/, // JSON files
28
+ /\.ya?ml$/, // YAML files
29
+ /\.md$/, // Markdown
30
+ /\.lock$/, // Lock files
31
+ ];
32
+
33
+ /**
34
+ * Detect flat folder anti-patterns (services/, interfaces/, controllers/ at root).
35
+ *
36
+ * @param {string} filePath - File path relative to project root
37
+ * @param {string} content - File content (unused for structure checks)
38
+ * @returns {Array<{severity: string, rule: string, line: number, message: string, fix: string}>}
39
+ */
40
+ function checkFlatFolders(filePath, content) {
41
+ const parts = filePath.split(/[/\\]/);
42
+
43
+ // Check if the first or second directory segment is a flat anti-pattern
44
+ for (let i = 0; i < Math.min(parts.length - 1, 2); i++) {
45
+ const dir = parts[i];
46
+ if (FLAT_FOLDER_PATTERNS.includes(dir)) {
47
+ // Only flag if not inside a module (e.g. src/user/services is fine)
48
+ const isNested = i >= 2 || (i === 1 && parts[0] === 'src' && parts.length > 3);
49
+ if (!isNested) {
50
+ return [{
51
+ severity: 'warn',
52
+ rule: 'no-flat-folders',
53
+ line: 1,
54
+ message: `File in flat '${dir}/' folder — use module-scoped organization`,
55
+ fix: `Move to a feature module: src/{feature}/${dir}/${path.basename(filePath)}`,
56
+ }];
57
+ }
58
+ }
59
+ }
60
+
61
+ return [];
62
+ }
63
+
64
+ /**
65
+ * Detect loose source files at project root that should be in subdirectories.
66
+ *
67
+ * @param {string} filePath
68
+ * @param {string} content
69
+ * @returns {Array}
70
+ */
71
+ function checkLooseFiles(filePath, content) {
72
+ // Only check files at root (no directory separator)
73
+ if (filePath.includes('/') || filePath.includes('\\')) {
74
+ return [];
75
+ }
76
+
77
+ // Check against allowed files and patterns
78
+ if (ALLOWED_ROOT_FILES.includes(filePath)) {
79
+ return [];
80
+ }
81
+ for (const pattern of ALLOWED_ROOT_PATTERNS) {
82
+ if (pattern.test(filePath)) {
83
+ return [];
84
+ }
85
+ }
86
+
87
+ // Only flag JS/TS source files
88
+ if (!/\.[jt]sx?$/.test(filePath)) {
89
+ return [];
90
+ }
91
+
92
+ return [{
93
+ severity: 'warn',
94
+ rule: 'no-loose-files',
95
+ line: 1,
96
+ message: `Source file '${filePath}' at project root`,
97
+ fix: 'Move into a feature module directory: src/{feature}/',
98
+ }];
99
+ }
100
+
101
+ /**
102
+ * Check that source files have corresponding test files.
103
+ * Skips test files, config files, and non-JS files.
104
+ *
105
+ * @param {string} filePath
106
+ * @param {string} content
107
+ * @param {Object} [options]
108
+ * @param {string[]} [options.allFiles] - All file paths in the changeset
109
+ * @returns {Array}
110
+ */
111
+ function checkTestColocation(filePath, content, options = {}) {
112
+ // Skip non-JS files
113
+ if (!/\.[jt]sx?$/.test(filePath)) return [];
114
+
115
+ // Skip test files themselves
116
+ if (/\.(test|spec)\.[jt]sx?$/.test(filePath)) return [];
117
+
118
+ // Skip config files
119
+ if (/\.config\.[jt]sx?$/.test(filePath)) return [];
120
+
121
+ // Skip index files and type definitions
122
+ if (/^index\.[jt]sx?$/.test(path.basename(filePath))) return [];
123
+ if (/\.d\.ts$/.test(filePath)) return [];
124
+
125
+ const allFiles = options.allFiles || [];
126
+ const base = filePath.replace(/\.[jt]sx?$/, '');
127
+ const testPatterns = [
128
+ `${base}.test.js`,
129
+ `${base}.test.ts`,
130
+ `${base}.test.jsx`,
131
+ `${base}.test.tsx`,
132
+ `${base}.spec.js`,
133
+ `${base}.spec.ts`,
134
+ ];
135
+
136
+ const hasTest = testPatterns.some(tp => allFiles.includes(tp));
137
+
138
+ if (!hasTest) {
139
+ return [{
140
+ severity: 'block',
141
+ rule: 'require-test-file',
142
+ line: 1,
143
+ message: `No test file found for '${filePath}'`,
144
+ fix: `Create test file: ${base}.test.js`,
145
+ }];
146
+ }
147
+
148
+ return [];
149
+ }
150
+
151
+ module.exports = {
152
+ checkFlatFolders,
153
+ checkLooseFiles,
154
+ checkTestColocation,
155
+ };
@@ -0,0 +1,107 @@
1
+ /**
2
+ * Structure Rules Tests
3
+ *
4
+ * Detects flat folder anti-patterns, files in wrong locations,
5
+ * and missing test colocation.
6
+ */
7
+ import { describe, it, expect } from 'vitest';
8
+
9
+ const {
10
+ checkFlatFolders,
11
+ checkLooseFiles,
12
+ checkTestColocation,
13
+ } = require('./structure-rules.js');
14
+
15
+ describe('Structure Rules', () => {
16
+ describe('checkFlatFolders', () => {
17
+ it('detects services/ folder at root', () => {
18
+ const findings = checkFlatFolders('services/userService.js', 'code');
19
+ expect(findings).toHaveLength(1);
20
+ expect(findings[0].rule).toBe('no-flat-folders');
21
+ expect(findings[0].severity).toBe('warn');
22
+ });
23
+
24
+ it('detects controllers/ folder', () => {
25
+ const findings = checkFlatFolders('controllers/userCtrl.js', 'code');
26
+ expect(findings).toHaveLength(1);
27
+ });
28
+
29
+ it('detects interfaces/ at src root', () => {
30
+ const findings = checkFlatFolders('src/interfaces/IUser.ts', 'code');
31
+ expect(findings).toHaveLength(1);
32
+ });
33
+
34
+ it('passes module-scoped paths', () => {
35
+ const findings = checkFlatFolders('src/user/services/userService.js', 'code');
36
+ expect(findings).toHaveLength(0);
37
+ });
38
+
39
+ it('passes nested interfaces in module', () => {
40
+ const findings = checkFlatFolders('src/user/interfaces/IUser.ts', 'code');
41
+ expect(findings).toHaveLength(0);
42
+ });
43
+ });
44
+
45
+ describe('checkLooseFiles', () => {
46
+ it('detects .js files in project root', () => {
47
+ const findings = checkLooseFiles('random-script.js', 'code');
48
+ expect(findings).toHaveLength(1);
49
+ expect(findings[0].rule).toBe('no-loose-files');
50
+ expect(findings[0].severity).toBe('warn');
51
+ });
52
+
53
+ it('allows index.js at root', () => {
54
+ const findings = checkLooseFiles('index.js', 'code');
55
+ expect(findings).toHaveLength(0);
56
+ });
57
+
58
+ it('allows config files at root', () => {
59
+ const findings = checkLooseFiles('vitest.config.js', 'code');
60
+ expect(findings).toHaveLength(0);
61
+ });
62
+
63
+ it('allows package.json at root', () => {
64
+ const findings = checkLooseFiles('package.json', '{}');
65
+ expect(findings).toHaveLength(0);
66
+ });
67
+
68
+ it('passes files in subdirectories', () => {
69
+ const findings = checkLooseFiles('src/app.js', 'code');
70
+ expect(findings).toHaveLength(0);
71
+ });
72
+ });
73
+
74
+ describe('checkTestColocation', () => {
75
+ it('flags source file without test file', () => {
76
+ const allFiles = ['src/user.js'];
77
+ const findings = checkTestColocation('src/user.js', 'code', { allFiles });
78
+ expect(findings).toHaveLength(1);
79
+ expect(findings[0].rule).toBe('require-test-file');
80
+ expect(findings[0].severity).toBe('block');
81
+ });
82
+
83
+ it('passes when test file exists', () => {
84
+ const allFiles = ['src/user.js', 'src/user.test.js'];
85
+ const findings = checkTestColocation('src/user.js', 'code', { allFiles });
86
+ expect(findings).toHaveLength(0);
87
+ });
88
+
89
+ it('skips check for test files themselves', () => {
90
+ const allFiles = ['src/user.test.js'];
91
+ const findings = checkTestColocation('src/user.test.js', 'test code', { allFiles });
92
+ expect(findings).toHaveLength(0);
93
+ });
94
+
95
+ it('skips check for config files', () => {
96
+ const allFiles = ['vitest.config.js'];
97
+ const findings = checkTestColocation('vitest.config.js', 'config', { allFiles });
98
+ expect(findings).toHaveLength(0);
99
+ });
100
+
101
+ it('skips check for non-JS files', () => {
102
+ const allFiles = ['README.md'];
103
+ const findings = checkTestColocation('README.md', '# readme', { allFiles });
104
+ expect(findings).toHaveLength(0);
105
+ });
106
+ });
107
+ });
@@ -0,0 +1,93 @@
1
+ /**
2
+ * Test Rules
3
+ *
4
+ * Detects test files with no assertions, skipped tests,
5
+ * and other test quality issues.
6
+ *
7
+ * @module code-gate/rules/test-rules
8
+ */
9
+
10
+ /**
11
+ * Check if a file is a test file.
12
+ * @param {string} filePath
13
+ * @returns {boolean}
14
+ */
15
+ function isTestFile(filePath) {
16
+ return /\.(test|spec)\.[jt]sx?$/.test(filePath);
17
+ }
18
+
19
+ /**
20
+ * Check for test files that contain no assertions.
21
+ * Tests without expect(), assert, or should are likely empty stubs.
22
+ *
23
+ * @param {string} filePath
24
+ * @param {string} content
25
+ * @returns {Array<{severity: string, rule: string, line: number, message: string, fix: string}>}
26
+ */
27
+ function checkEmptyTests(filePath, content) {
28
+ if (!isTestFile(filePath)) return [];
29
+
30
+ const hasAssertion =
31
+ /\bexpect\s*\(/.test(content) ||
32
+ /\bassert\b/.test(content) ||
33
+ /\.should\b/.test(content);
34
+
35
+ if (!hasAssertion) {
36
+ return [{
37
+ severity: 'block',
38
+ rule: 'no-empty-tests',
39
+ line: 1,
40
+ message: 'Test file contains no assertions',
41
+ fix: 'Add expect() or assert calls to verify behavior',
42
+ }];
43
+ }
44
+
45
+ return [];
46
+ }
47
+
48
+ /**
49
+ * Check for skipped tests (.skip, xit, xdescribe).
50
+ * Skipped tests hide regressions.
51
+ *
52
+ * @param {string} filePath
53
+ * @param {string} content
54
+ * @returns {Array}
55
+ */
56
+ function checkSkippedTests(filePath, content) {
57
+ if (!isTestFile(filePath)) return [];
58
+ const findings = [];
59
+ const lines = content.split('\n');
60
+
61
+ for (let i = 0; i < lines.length; i++) {
62
+ const line = lines[i];
63
+
64
+ const skipPatterns = [
65
+ { pattern: /\bdescribe\.skip\s*\(/, label: 'describe.skip' },
66
+ { pattern: /\bit\.skip\s*\(/, label: 'it.skip' },
67
+ { pattern: /\btest\.skip\s*\(/, label: 'test.skip' },
68
+ { pattern: /\bxdescribe\s*\(/, label: 'xdescribe' },
69
+ { pattern: /\bxit\s*\(/, label: 'xit' },
70
+ { pattern: /\bxtest\s*\(/, label: 'xtest' },
71
+ ];
72
+
73
+ for (const { pattern, label } of skipPatterns) {
74
+ if (pattern.test(line)) {
75
+ findings.push({
76
+ severity: 'warn',
77
+ rule: 'no-skipped-tests',
78
+ line: i + 1,
79
+ message: `Skipped test found: ${label}`,
80
+ fix: 'Remove .skip or fix the test — skipped tests hide regressions',
81
+ });
82
+ break;
83
+ }
84
+ }
85
+ }
86
+
87
+ return findings;
88
+ }
89
+
90
+ module.exports = {
91
+ checkEmptyTests,
92
+ checkSkippedTests,
93
+ };
@@ -0,0 +1,97 @@
1
+ /**
2
+ * Test Rules Tests
3
+ *
4
+ * Detects test files with no assertions, skipped tests, and
5
+ * source files without corresponding test files.
6
+ */
7
+ import { describe, it, expect } from 'vitest';
8
+
9
+ const {
10
+ checkEmptyTests,
11
+ checkSkippedTests,
12
+ } = require('./test-rules.js');
13
+
14
+ describe('Test Rules', () => {
15
+ describe('checkEmptyTests', () => {
16
+ it('detects test file with no assertions', () => {
17
+ const code = `
18
+ describe('User', () => {
19
+ it('should work', () => {
20
+ const user = getUser();
21
+ });
22
+ });`;
23
+ const findings = checkEmptyTests('user.test.js', code);
24
+ expect(findings).toHaveLength(1);
25
+ expect(findings[0].rule).toBe('no-empty-tests');
26
+ expect(findings[0].severity).toBe('block');
27
+ });
28
+
29
+ it('passes test with expect assertion', () => {
30
+ const code = `
31
+ describe('User', () => {
32
+ it('should work', () => {
33
+ expect(getUser()).toBeDefined();
34
+ });
35
+ });`;
36
+ const findings = checkEmptyTests('user.test.js', code);
37
+ expect(findings).toHaveLength(0);
38
+ });
39
+
40
+ it('passes test with assert assertion', () => {
41
+ const code = `
42
+ describe('User', () => {
43
+ it('should work', () => {
44
+ assert.ok(getUser());
45
+ });
46
+ });`;
47
+ const findings = checkEmptyTests('user.test.js', code);
48
+ expect(findings).toHaveLength(0);
49
+ });
50
+
51
+ it('skips non-test files', () => {
52
+ const code = 'const x = 1;';
53
+ const findings = checkEmptyTests('app.js', code);
54
+ expect(findings).toHaveLength(0);
55
+ });
56
+ });
57
+
58
+ describe('checkSkippedTests', () => {
59
+ it('detects describe.skip', () => {
60
+ const code = 'describe.skip("User", () => {});';
61
+ const findings = checkSkippedTests('user.test.js', code);
62
+ expect(findings).toHaveLength(1);
63
+ expect(findings[0].rule).toBe('no-skipped-tests');
64
+ expect(findings[0].severity).toBe('warn');
65
+ });
66
+
67
+ it('detects it.skip', () => {
68
+ const code = 'it.skip("should work", () => {});';
69
+ const findings = checkSkippedTests('user.test.js', code);
70
+ expect(findings).toHaveLength(1);
71
+ });
72
+
73
+ it('detects xdescribe', () => {
74
+ const code = 'xdescribe("User", () => {});';
75
+ const findings = checkSkippedTests('user.test.js', code);
76
+ expect(findings).toHaveLength(1);
77
+ });
78
+
79
+ it('detects xit', () => {
80
+ const code = 'xit("should work", () => {});';
81
+ const findings = checkSkippedTests('user.test.js', code);
82
+ expect(findings).toHaveLength(1);
83
+ });
84
+
85
+ it('passes normal test', () => {
86
+ const code = 'it("should work", () => { expect(1).toBe(1); });';
87
+ const findings = checkSkippedTests('user.test.js', code);
88
+ expect(findings).toHaveLength(0);
89
+ });
90
+
91
+ it('skips non-test files', () => {
92
+ const code = 'describe.skip("something", () => {});';
93
+ const findings = checkSkippedTests('app.js', code);
94
+ expect(findings).toHaveLength(0);
95
+ });
96
+ });
97
+ });
@@ -0,0 +1,128 @@
1
+ /**
2
+ * TypeScript Compilation Gate
3
+ *
4
+ * Integrates `tsc --noEmit` as an optional check in the push gate.
5
+ * Catches 7 categories of bugs that compile silently with esbuild/Vite:
6
+ * - Invented enum values (Bug #4, #33)
7
+ * - Wrong field names (Bug #7)
8
+ * - Date type mismatches (Bug #8)
9
+ * - Undefined variables (Bug #31)
10
+ * - Accumulated type errors (Bug #32)
11
+ * - Missing required fields in spreads (Bug #34)
12
+ *
13
+ * @module code-gate/typescript-gate
14
+ */
15
+
16
+ const path = require('path');
17
+
18
+ /**
19
+ * Check if a project has TypeScript configured.
20
+ *
21
+ * @param {string} projectPath - Project root
22
+ * @param {Object} [options]
23
+ * @param {Object} [options.fs] - File system (injectable)
24
+ * @returns {boolean}
25
+ */
26
+ function detectTypeScript(projectPath, options = {}) {
27
+ const fs = options.fs || require('fs');
28
+ const tsconfigPath = path.join(projectPath, 'tsconfig.json');
29
+ return fs.existsSync(tsconfigPath);
30
+ }
31
+
32
+ /**
33
+ * Parse tsc --noEmit output into gate findings.
34
+ * TypeScript error format: file(line,col): error TSxxxx: message
35
+ *
36
+ * @param {string} output - Raw tsc output
37
+ * @returns {Array<{severity: string, rule: string, file: string, line: number, message: string, fix: string}>}
38
+ */
39
+ function parseTscOutput(output) {
40
+ if (!output || !output.trim()) return [];
41
+ const findings = [];
42
+ const lines = output.split('\n');
43
+
44
+ // TS error format: path/file.ts(line,col): error TSxxxx: message
45
+ const errorPattern = /^(.+?)\((\d+),\d+\):\s*error\s+(TS\d+):\s*(.+)$/;
46
+
47
+ for (const line of lines) {
48
+ const match = line.match(errorPattern);
49
+ if (match) {
50
+ const [, file, lineNum, code, message] = match;
51
+ findings.push({
52
+ severity: 'block',
53
+ rule: 'typescript-error',
54
+ file,
55
+ line: parseInt(lineNum, 10),
56
+ message: `${code}: ${message}`,
57
+ fix: 'Fix the TypeScript type error — tsc --noEmit must pass with zero errors',
58
+ });
59
+ }
60
+ }
61
+
62
+ return findings;
63
+ }
64
+
65
+ /**
66
+ * Run tsc --noEmit and return gate-compatible result.
67
+ *
68
+ * @param {string} projectPath - Project root
69
+ * @param {Object} [options]
70
+ * @param {Function} [options.exec] - Command executor (injectable)
71
+ * @returns {Promise<{passed: boolean, findings: Array, summary: Object, skipped?: boolean}>}
72
+ */
73
+ async function runTypeScriptGate(projectPath, options = {}) {
74
+ const exec = options.exec;
75
+
76
+ try {
77
+ const result = await exec('npx tsc --noEmit', { cwd: projectPath });
78
+ const output = result.stdout || '';
79
+ const findings = parseTscOutput(output);
80
+ const summary = { total: findings.length, block: findings.length, warn: 0, info: 0 };
81
+
82
+ return {
83
+ passed: findings.length === 0,
84
+ findings,
85
+ summary,
86
+ };
87
+ } catch (err) {
88
+ // If tsc fails with errors, the output is usually in stdout or stderr
89
+ if (err.stdout || err.stderr) {
90
+ const output = err.stdout || err.stderr || '';
91
+ const findings = parseTscOutput(output);
92
+ const summary = { total: findings.length, block: findings.length, warn: 0, info: 0 };
93
+ return {
94
+ passed: findings.length === 0,
95
+ findings,
96
+ summary,
97
+ };
98
+ }
99
+
100
+ // tsc not installed or other system error — skip gracefully
101
+ return {
102
+ passed: true,
103
+ findings: [],
104
+ summary: { total: 0, block: 0, warn: 0, info: 0 },
105
+ skipped: true,
106
+ };
107
+ }
108
+ }
109
+
110
+ /**
111
+ * Create a TypeScript gate with configurable options.
112
+ *
113
+ * @param {Object} [options]
114
+ * @param {boolean} [options.enabled=true]
115
+ * @returns {{ enabled: boolean }}
116
+ */
117
+ function createTypeScriptGate(options = {}) {
118
+ return {
119
+ enabled: options.enabled !== false,
120
+ };
121
+ }
122
+
123
+ module.exports = {
124
+ detectTypeScript,
125
+ parseTscOutput,
126
+ runTypeScriptGate,
127
+ createTypeScriptGate,
128
+ };