@boyingliu01/xp-gate 0.19.3 → 0.20.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 (173) hide show
  1. package/adapters/gate-4.sh +83 -18
  2. package/adapters/gate-8.sh +49 -5
  3. package/adapters/iac.sh +1 -1
  4. package/adapters/typescript.sh +15 -60
  5. package/bin/xp-gate.js +15 -16
  6. package/gate-10.sh +35 -2
  7. package/gate-4.sh +110 -21
  8. package/gate-8.sh +49 -5
  9. package/hooks/gate-10.sh +35 -2
  10. package/hooks/gate-4.sh +110 -21
  11. package/hooks/gate-8.sh +49 -5
  12. package/hooks/lib/jscpd-run.sh +63 -0
  13. package/hooks/lib/sprint-gate-report.sh +58 -0
  14. package/hooks/lib/test-failure.sh +152 -0
  15. package/hooks/lib/typecheck.sh +141 -0
  16. package/hooks/lib/validate-code-walkthrough.cjs +43 -5
  17. package/hooks/post-merge +100 -0
  18. package/hooks/pre-commit +389 -162
  19. package/hooks/pre-push +75 -19
  20. package/lib/__tests__/bootstrap.test.js +23 -41
  21. package/lib/__tests__/check-version.test.js +8 -1
  22. package/lib/__tests__/detect-deps.test.js +16 -0
  23. package/lib/__tests__/doctor-drift-round2.test.js +212 -0
  24. package/lib/__tests__/doctor-executed-surface.test.js +209 -0
  25. package/lib/__tests__/doctor-hook-drift.test.js +329 -0
  26. package/lib/__tests__/doctor-machine-mutation.test.js +116 -0
  27. package/lib/__tests__/doctor-module-drift.test.js +206 -0
  28. package/lib/__tests__/doctor-module-resolution-parity.test.js +314 -0
  29. package/lib/__tests__/doctor-path-equivalence.test.js +59 -0
  30. package/lib/__tests__/doctor-repair-boundary.test.js +52 -0
  31. package/lib/__tests__/doctor-tui-skill-freshness.test.js +105 -0
  32. package/lib/__tests__/doctor.test.js +155 -0
  33. package/lib/__tests__/gate-runner.test.js +19 -3
  34. package/lib/__tests__/helpers/lib-sandbox.cjs +55 -0
  35. package/lib/__tests__/init.test.js +44 -17
  36. package/lib/__tests__/install-skill-full-dir.test.js +151 -0
  37. package/lib/__tests__/install-skill.test.js +51 -132
  38. package/lib/__tests__/language-tools-detect.test.js +158 -0
  39. package/lib/__tests__/phase-transition.test.js +15 -1
  40. package/lib/__tests__/qoder-delphi-agents.test.js +285 -0
  41. package/lib/__tests__/shared-paths.test.js +20 -0
  42. package/lib/__tests__/shared-utils.test.js +69 -0
  43. package/lib/__tests__/skill-flags.test.js +48 -0
  44. package/lib/__tests__/ui-detector.test.ts +6 -2
  45. package/lib/__tests__/uninstall.test.js +20 -0
  46. package/lib/__tests__/update-hooks.test.js +12 -0
  47. package/lib/__tests__/update-skill.test.js +7 -4
  48. package/lib/detect-deps.js +3 -20
  49. package/lib/doctor-tui.js +22 -5
  50. package/lib/doctor.js +1178 -31
  51. package/lib/init.js +184 -33
  52. package/lib/install-cmd.js +7 -4
  53. package/lib/install-skill.js +38 -95
  54. package/lib/language-tools.js +83 -4
  55. package/lib/mattpocock-license-verdict.json +1 -1
  56. package/lib/shared-paths.js +5 -2
  57. package/lib/shared-utils.js +47 -3
  58. package/lib/uninstall.js +12 -0
  59. package/lib/update-hooks.js +4 -4
  60. package/lib/update-skill.js +6 -7
  61. package/mock-policy/AGENTS.md +4 -4
  62. package/mutation/AGENTS.md +4 -4
  63. package/mutation/__tests__/gate-m.test.ts +66 -14
  64. package/mutation/__tests__/mutmut-runner.test.ts +91 -42
  65. package/mutation/__tests__/test-filter-parity.test.ts +37 -0
  66. package/mutation/gate-m.ts +21 -8
  67. package/package.json +1 -1
  68. package/plugins/claude-code/.claude-plugin/plugin.json +1 -1
  69. package/plugins/claude-code/skills/clipboard-vision/__tests__/clipboard-vision-distribution.test.cjs +0 -1
  70. package/plugins/claude-code/skills/delphi-review/AGENTS.md +4 -4
  71. package/plugins/claude-code/skills/delphi-review/SKILL.md +98 -14
  72. package/plugins/claude-code/skills/delphi-review/references/code-walkthrough.md +36 -3
  73. package/plugins/claude-code/skills/delphi-review/references/requirements.md +6 -6
  74. package/plugins/claude-code/skills/sprint-flow/AGENTS.md +4 -4
  75. package/plugins/claude-code/skills/sprint-flow/references/phase-2-design.md +3 -3
  76. package/plugins/claude-code/skills/test-specification-alignment/AGENTS.md +4 -4
  77. package/plugins/dsh/README.md +169 -4
  78. package/plugins/dsh/dsh-overlay.patch.yml +67 -0
  79. package/plugins/dsh/package.json +1 -1
  80. package/plugins/dsh/scripts/prepack.cjs +24 -19
  81. package/plugins/dsh/setup-dsh.ps1 +160 -0
  82. package/plugins/dsh/skills/clipboard-vision/__tests__/clipboard-vision-distribution.test.cjs +0 -1
  83. package/plugins/dsh/skills/delphi-review/AGENTS.md +4 -4
  84. package/plugins/dsh/skills/delphi-review/SKILL.md +98 -14
  85. package/plugins/dsh/skills/delphi-review/references/code-walkthrough.md +36 -3
  86. package/plugins/dsh/skills/delphi-review/references/requirements.md +6 -6
  87. package/plugins/dsh/skills/sprint-flow/AGENTS.md +4 -4
  88. package/plugins/dsh/skills/sprint-flow/references/phase-2-design.md +3 -3
  89. package/plugins/dsh/skills/test-specification-alignment/AGENTS.md +4 -4
  90. package/plugins/dsh/src/command.test.ts +66 -2
  91. package/plugins/dsh/src/command.ts +36 -14
  92. package/plugins/dsh/src/contract.test.ts +31 -1
  93. package/plugins/dsh/src/delphi-config.test.ts +190 -0
  94. package/plugins/dsh/src/delphi-config.ts +223 -0
  95. package/plugins/dsh/src/delphi-run.test.ts +181 -0
  96. package/plugins/dsh/src/delphi-run.ts +129 -0
  97. package/plugins/dsh/src/delphi.test.ts +222 -0
  98. package/plugins/dsh/src/delphi.ts +247 -0
  99. package/plugins/dsh/src/dialect.test.ts +59 -0
  100. package/plugins/dsh/src/dialect.ts +40 -0
  101. package/plugins/dsh/src/index.ts +70 -4
  102. package/plugins/dsh/src/skills.test.ts +26 -19
  103. package/plugins/opencode/package.json +1 -1
  104. package/plugins/opencode/skills/clipboard-vision/__tests__/clipboard-vision-distribution.test.cjs +0 -1
  105. package/plugins/opencode/skills/delphi-review/AGENTS.md +4 -4
  106. package/plugins/opencode/skills/delphi-review/SKILL.md +98 -14
  107. package/plugins/opencode/skills/delphi-review/references/code-walkthrough.md +36 -3
  108. package/plugins/opencode/skills/delphi-review/references/requirements.md +6 -6
  109. package/plugins/opencode/skills/sprint-flow/AGENTS.md +4 -4
  110. package/plugins/opencode/skills/sprint-flow/references/phase-2-design.md +3 -3
  111. package/plugins/opencode/skills/test-specification-alignment/AGENTS.md +4 -4
  112. package/plugins/qoder/agents/delphi-architecture.md +1 -1
  113. package/plugins/qoder/agents/delphi-feasibility.md +1 -1
  114. package/plugins/qoder/agents/delphi-technical.md +1 -1
  115. package/plugins/qoder/plugin.json +1 -1
  116. package/plugins/qoder/skills/clipboard-vision/__tests__/clipboard-vision-distribution.test.cjs +0 -1
  117. package/plugins/qoder/skills/delphi-review/AGENTS.md +4 -4
  118. package/plugins/qoder/skills/delphi-review/SKILL.md +98 -14
  119. package/plugins/qoder/skills/delphi-review/references/code-walkthrough.md +36 -3
  120. package/plugins/qoder/skills/delphi-review/references/requirements.md +6 -6
  121. package/plugins/qoder/skills/sprint-flow/AGENTS.md +4 -4
  122. package/plugins/qoder/skills/sprint-flow/references/phase-2-design.md +3 -3
  123. package/plugins/qoder/skills/test-specification-alignment/AGENTS.md +4 -4
  124. package/principles/AGENTS.md +9 -8
  125. package/principles/__tests__/adapter-metrics.test.ts +292 -0
  126. package/principles/__tests__/analyzer.test.ts +29 -6
  127. package/principles/__tests__/baseline-prune-timeout.test.ts +71 -0
  128. package/principles/__tests__/baseline-prune.test.ts +338 -0
  129. package/principles/__tests__/boy-scout-init-baseline.test.ts +167 -0
  130. package/principles/__tests__/boy-scout-integration.test.ts +97 -6
  131. package/principles/__tests__/boy-scout.test.ts +253 -7
  132. package/principles/__tests__/config-enforcement.test.ts +469 -0
  133. package/principles/__tests__/config-git-timeout.test.ts +66 -0
  134. package/principles/__tests__/direct-execution.test.ts +187 -0
  135. package/principles/__tests__/index.test.ts +16 -3
  136. package/principles/__tests__/warnings-baseline-budgets.test.ts +74 -0
  137. package/principles/adapters/typescript.ts +613 -13
  138. package/principles/analyzer.ts +22 -0
  139. package/principles/baseline-prune.ts +217 -0
  140. package/principles/boy-scout.ts +481 -51
  141. package/principles/config.ts +362 -54
  142. package/principles/direct-execution.ts +60 -0
  143. package/principles/index.ts +40 -17
  144. package/principles/rules/__tests__/clean-code/many-exports.test.ts +16 -10
  145. package/principles/rules/clean-code/code-duplication.ts +8 -8
  146. package/principles/rules/clean-code/deep-nesting.ts +8 -8
  147. package/principles/rules/clean-code/god-class.ts +8 -8
  148. package/principles/rules/clean-code/large-file.ts +15 -11
  149. package/principles/rules/clean-code/long-function.ts +8 -8
  150. package/principles/rules/clean-code/magic-numbers.ts +10 -7
  151. package/principles/rules/clean-code/many-exports.ts +7 -7
  152. package/principles/rules/clean-code/missing-error-handling.ts +5 -5
  153. package/principles/rules/clean-code/too-many-params.ts +8 -8
  154. package/principles/rules/clean-code/unused-imports.ts +5 -5
  155. package/principles/rules/solid/dip.ts +18 -8
  156. package/principles/rules/solid/isp.ts +9 -8
  157. package/principles/rules/solid/lsp.ts +6 -6
  158. package/principles/rules/solid/ocp.ts +6 -5
  159. package/principles/rules/solid/srp.ts +7 -8
  160. package/principles/types.ts +15 -0
  161. package/scripts/__tests__/sync-package-content.test.js +28 -0
  162. package/scripts/delphi-external-review.cjs +193 -10
  163. package/scripts/sync-package-content.js +7 -4
  164. package/skills/clipboard-vision/__tests__/clipboard-vision-distribution.test.cjs +0 -1
  165. package/skills/delphi-review/AGENTS.md +4 -4
  166. package/skills/delphi-review/SKILL.md +98 -14
  167. package/skills/delphi-review/references/code-walkthrough.md +36 -3
  168. package/skills/delphi-review/references/requirements.md +6 -6
  169. package/skills/sprint-flow/AGENTS.md +4 -4
  170. package/skills/sprint-flow/references/phase-2-design.md +3 -3
  171. package/skills/test-specification-alignment/AGENTS.md +4 -4
  172. package/lib/__tests__/download-skill.test.js +0 -281
  173. package/lib/download-skill.js +0 -96
@@ -1,6 +1,312 @@
1
1
  import { BaseAdapter } from './base';
2
2
  import { Adapter } from '../types';
3
3
 
4
+ /** IO-ish callees that the missing-error-handling rule cares about. */
5
+ const IO_CALLEES = [
6
+ 'readFile', 'readFileSync', 'writeFile', 'writeFileSync', 'appendFile', 'appendFileSync',
7
+ 'open', 'openSync', 'unlink', 'unlinkSync', 'mkdir', 'mkdirSync', 'readdir', 'readdirSync',
8
+ 'stat', 'statSync', 'access', 'accessSync', 'fetch', 'axios', 'createReadStream', 'createWriteStream',
9
+ ];
10
+
11
+ /**
12
+ * Child-process / network entry points. `exec` and friends are matched only as
13
+ * bare calls (no `obj.` receiver): `regex.exec(str)` is not IO, so requiring the
14
+ * call to start a statement avoids flagging `RegExp.prototype.exec` (#446).
15
+ */
16
+ const IO_GLOBAL_CALLEES = ['exec', 'execSync', 'spawn', 'spawnSync', 'request'];
17
+
18
+ /**
19
+ * Builders, not shared literals: a module-level regex with the `g` flag carries
20
+ * mutable `lastIndex` across calls, so any caller that forgets to reset it
21
+ * silently drops matches. Fresh instances keep every call independent.
22
+ */
23
+ function ioCalleeRe(): RegExp {
24
+ return new RegExp(`\\b(?:${IO_CALLEES.join('|')})\\s*\\(`, 'g');
25
+ }
26
+
27
+ function ioGlobalRe(): RegExp {
28
+ return new RegExp(`(?:^|[^.\\w$])(?:${IO_GLOBAL_CALLEES.join('|')})\\s*\\(`, 'gm');
29
+ }
30
+
31
+ /** Keywords that look like `name(` but are control flow, not callables. */
32
+ const CONTROL_KEYWORDS = new Set([
33
+ 'if', 'for', 'while', 'switch', 'catch', 'return', 'typeof', 'new', 'delete',
34
+ 'void', 'in', 'instanceof', 'do', 'else', 'try', 'finally', 'throw', 'await',
35
+ 'yield', 'function', 'class', 'super', 'this', 'import', 'export', 'default',
36
+ 'case', 'break', 'continue', 'const', 'let', 'var', 'as', 'satisfies', 'keyof',
37
+ ]);
38
+
39
+ /**
40
+ * Find the index of the `{` that opens a declaration body for the parameter
41
+ * list starting at `parenIndex`.
42
+ *
43
+ * Everything between the closing `)` and the body is scanned with a small depth
44
+ * tracker so that a `{` belonging to a type literal in the return annotation
45
+ * (`extract(): Array<{value: number}> {`) is not mistaken for the body. The
46
+ * body brace is the first `{` seen at depth 0.
47
+ */
48
+ function findBodyBrace(masked: string, parenIndex: number): number {
49
+ if (masked[parenIndex] !== '(') return -1;
50
+ const { end } = readParenGroup(masked, parenIndex);
51
+ let depth = 0;
52
+ for (let i = end; i < masked.length; i++) {
53
+ const c = masked[i];
54
+ if (c === '(') { const g = readParenGroup(masked, i); i = g.end - 1; continue; }
55
+ if (c === '<' || c === '[') { depth++; continue; }
56
+ if (c === '>' || c === ']') { if (depth > 0) depth--; continue; }
57
+ if (c === '{') {
58
+ if (depth === 0) return i;
59
+ depth++;
60
+ continue;
61
+ }
62
+ if (c === '}') { if (depth > 0) depth--; continue; }
63
+ if (c === ';' || c === '\n') {
64
+ // A line break before any `{` means this header has no braced body
65
+ // (e.g. an interface member or an overload signature).
66
+ if (depth === 0) return -1;
67
+ }
68
+ }
69
+ return -1;
70
+ }
71
+
72
+ /** Index just past the matching `}` for the block opened at `open`. */
73
+ function findBlockEnd(masked: string, open: number): number {
74
+ let depth = 0;
75
+ for (let i = open; i < masked.length; i++) {
76
+ const c = masked[i];
77
+ if (c === '{') depth++;
78
+ else if (c === '}') {
79
+ depth--;
80
+ if (depth === 0) return i + 1;
81
+ }
82
+ }
83
+ return masked.length;
84
+ }
85
+
86
+ /**
87
+ * Body brace for declarations that have no parameter list (`class`, `interface`).
88
+ * Scans forward from the end of the declaration header; the first `{` at generic
89
+ * depth 0 is the body. A `;` before any brace means there is no body.
90
+ */
91
+ function findDeclarationBrace(masked: string, from: number): number {
92
+ let depth = 0;
93
+ for (let i = from; i < masked.length; i++) {
94
+ const c = masked[i];
95
+ if (c === '<' || c === '[' || c === '(') {
96
+ if (c === '(') { const g = readParenGroup(masked, i); i = g.end - 1; continue; }
97
+ depth++;
98
+ continue;
99
+ }
100
+ if (c === '>' || c === ']') { if (depth > 0) depth--; continue; }
101
+ if (c === '{') {
102
+ if (depth === 0) return i;
103
+ depth++;
104
+ continue;
105
+ }
106
+ if (c === '}') { if (depth > 0) depth--; continue; }
107
+ if (c === ';' && depth === 0) return -1;
108
+ // Stop at a blank line / a new top-level keyword so we do not run into the
109
+ // next declaration when this one has no body.
110
+ if (c === '\n') {
111
+ const rest = masked.substring(i + 1).replace(/^[ \t]*/, '');
112
+ if (/^(?:export|import|interface|class|const|let|var|function|type|declare|@)/.test(rest) && depth === 0) {
113
+ return -1;
114
+ }
115
+ }
116
+ }
117
+ return -1;
118
+ }
119
+
120
+ function lineOf(src: string, index: number): number {
121
+ return src.substring(0, index).split('\n').length;
122
+ }
123
+
124
+ /** Count top-level, comma-separated parameters in a raw parameter list. */
125
+ function countParams(rawList: string): number {
126
+ const trimmed = rawList.trim();
127
+ if (trimmed === '') return 0;
128
+ let depth = 0;
129
+ let count = 1;
130
+ let inSingle = false;
131
+ let inDouble = false;
132
+ let inTemplate = false;
133
+ for (let i = 0; i < trimmed.length; i++) {
134
+ const c = trimmed[i];
135
+ if (inSingle) { if (c === '\\') i++; else if (c === "'") inSingle = false; continue; }
136
+ if (inDouble) { if (c === '\\') i++; else if (c === '"') inDouble = false; continue; }
137
+ if (inTemplate) { if (c === '\\') i++; else if (c === '`') inTemplate = false; continue; }
138
+ if (c === "'") { inSingle = true; continue; }
139
+ if (c === '"') { inDouble = true; continue; }
140
+ if (c === '`') { inTemplate = true; continue; }
141
+ if (c === '(' || c === '[' || c === '{' || c === '<') depth++;
142
+ else if (c === ')' || c === ']' || c === '}' || c === '>') depth--;
143
+ else if (c === ',' && depth === 0) count++;
144
+ }
145
+ return count;
146
+ }
147
+
148
+ /**
149
+ * Extract the balanced `( ... )` group that starts at `open`.
150
+ * Returns the inner text and the index just past the closing paren.
151
+ */
152
+ function readParenGroup(src: string, open: number): { inner: string; end: number } {
153
+ let depth = 0;
154
+ let inSingle = false;
155
+ let inDouble = false;
156
+ let inTemplate = false;
157
+ for (let i = open; i < src.length; i++) {
158
+ const c = src[i];
159
+ if (inSingle) { if (c === '\\') i++; else if (c === "'") inSingle = false; continue; }
160
+ if (inDouble) { if (c === '\\') i++; else if (c === '"') inDouble = false; continue; }
161
+ if (inTemplate) { if (c === '\\') i++; else if (c === '`') inTemplate = false; continue; }
162
+ if (c === "'") { inSingle = true; continue; }
163
+ if (c === '"') { inDouble = true; continue; }
164
+ if (c === '`') { inTemplate = true; continue; }
165
+ if (c === '(') depth++;
166
+ else if (c === ')') {
167
+ depth--;
168
+ if (depth === 0) return { inner: src.substring(open + 1, i), end: i + 1 };
169
+ }
170
+ }
171
+ return { inner: src.substring(open + 1), end: src.length };
172
+ }
173
+
174
+ /** Max brace nesting depth inside a function body (1 = body itself). */
175
+ function maxNesting(body: string): number {
176
+ let depth = 0;
177
+ let max = 0;
178
+ let inSingle = false;
179
+ let inDouble = false;
180
+ let inTemplate = false;
181
+ let inLineComment = false;
182
+ let inBlockComment = false;
183
+ for (let i = 0; i < body.length; i++) {
184
+ const c = body[i];
185
+ const next = body[i + 1];
186
+ if (inLineComment) { if (c === '\n') inLineComment = false; continue; }
187
+ if (inBlockComment) { if (c === '*' && next === '/') { inBlockComment = false; i++; } continue; }
188
+ if (inSingle) { if (c === '\\') i++; else if (c === "'") inSingle = false; continue; }
189
+ if (inDouble) { if (c === '\\') i++; else if (c === '"') inDouble = false; continue; }
190
+ if (inTemplate) { if (c === '\\') i++; else if (c === '`') inTemplate = false; continue; }
191
+ if (c === '/' && next === '/') { inLineComment = true; i++; continue; }
192
+ if (c === '/' && next === '*') { inBlockComment = true; i++; continue; }
193
+ if (c === "'") { inSingle = true; continue; }
194
+ if (c === '"') { inDouble = true; continue; }
195
+ if (c === '`') { inTemplate = true; continue; }
196
+ if (c === '{') { depth++; if (depth > max) max = depth; }
197
+ else if (c === '}') depth--;
198
+ }
199
+ return max;
200
+ }
201
+
202
+ /** Which IO callees appear in this body. */
203
+ function ioOperationsIn(body: string): string[] {
204
+ const found = new Set<string>();
205
+ // Fresh regex instances per call: no shared lastIndex to reset or leak.
206
+ for (const m of body.matchAll(ioCalleeRe())) {
207
+ found.add(m[0].replace(/\s*\($/, ''));
208
+ }
209
+ for (const m of body.matchAll(ioGlobalRe())) {
210
+ const name = /(\w+)\s*\($/.exec(m[0].trim());
211
+ if (name) found.add(name[1]);
212
+ }
213
+ return [...found];
214
+ }
215
+
216
+ /**
217
+ * Blank out comments and string/template literals, preserving offsets.
218
+ * A single left-to-right scan is used rather than chained `.replace()` calls:
219
+ * sequential regex replacement re-scans text that earlier passes produced and
220
+ * can pair a quote inside a regex literal with a quote in a later comment,
221
+ * swallowing hundreds of lines (observed as a 302-line phantom function).
222
+ */
223
+ function maskLiterals(src: string): string {
224
+ const out = src.split('');
225
+ let i = 0;
226
+ while (i < src.length) {
227
+ const c = src[i];
228
+ const next = src[i + 1];
229
+
230
+ if (c === '/' && next === '/') {
231
+ while (i < src.length && src[i] !== '\n') { out[i] = ' '; i++; }
232
+ continue;
233
+ }
234
+ if (c === '/' && next === '*') {
235
+ out[i] = ' '; out[i + 1] = ' '; i += 2;
236
+ while (i < src.length && !(src[i] === '*' && src[i + 1] === '/')) { out[i] = ' '; i++; }
237
+ if (i < src.length) { out[i] = ' '; out[i + 1] = ' '; i += 2; }
238
+ continue;
239
+ }
240
+
241
+ // Regex literals: `/.../flags`. Only treat `/` as a regex start when the
242
+ // previous significant character cannot end an expression.
243
+ if (c === '/') {
244
+ const before = src.substring(0, i).replace(/\s+$/, '');
245
+ const prev = before[before.length - 1];
246
+ const prevWord = /(?:^|[^\w$])(return|typeof|case|in|of|new|delete|void|instanceof)$/.test(before);
247
+ if (prev === undefined || /[=(,:[!&|?{};+\-*%^~<>]/.test(prev) || prevWord) {
248
+ let j = i + 1;
249
+ let inClass = false;
250
+ let closed = false;
251
+ while (j < src.length) {
252
+ const d = src[j];
253
+ if (d === '\\') { j += 2; continue; }
254
+ if (d === '\n') break;
255
+ if (d === '[') inClass = true;
256
+ else if (d === ']') inClass = false;
257
+ else if (d === '/' && !inClass) { closed = true; break; }
258
+ j++;
259
+ }
260
+ if (closed) {
261
+ for (let k = i; k <= j; k++) out[k] = ' ';
262
+ i = j + 1;
263
+ while (i < src.length && /[a-z]/i.test(src[i])) { out[i] = ' '; i++; }
264
+ continue;
265
+ }
266
+ }
267
+ }
268
+
269
+ if (c === "'" || c === '"' || c === '`') {
270
+ const quote = c;
271
+ out[i] = ' ';
272
+ i++;
273
+ while (i < src.length) {
274
+ const d = src[i];
275
+ if (d === '\\') { out[i] = ' '; if (i + 1 < src.length) out[i + 1] = ' '; i += 2; continue; }
276
+ if (d === quote) { out[i] = ' '; i++; break; }
277
+ if (d === '\n' && quote !== '`') break;
278
+ out[i] = ' ';
279
+ i++;
280
+ }
281
+ continue;
282
+ }
283
+
284
+ i++;
285
+ }
286
+ return out.join('');
287
+ }
288
+
289
+ /**
290
+ * The metrics the rule set actually reads (see src/principles/rules/**).
291
+ * Previously only {name,type,line,code} was returned, so every rule that reads
292
+ * `length` / `paramCount` / `nestingDepth` / `hasTryCatch` / `methodCount`
293
+ * silently never fired on TypeScript (#446).
294
+ */
295
+ interface FunctionMetrics {
296
+ name: string;
297
+ type: 'function' | 'method' | 'arrow';
298
+ line: number;
299
+ startLine: number;
300
+ endLine: number;
301
+ /** Body line span, matching the `length` field long-function reads. */
302
+ length: number;
303
+ paramCount: number;
304
+ nestingDepth: number;
305
+ hasTryCatch: boolean;
306
+ ioOperations: string[];
307
+ code: string;
308
+ }
309
+
4
310
  export class TypeScriptAdapter extends BaseAdapter implements Adapter {
5
311
  detectLanguage(): string {
6
312
  const ext = this.filePath.toLowerCase();
@@ -14,28 +320,322 @@ export class TypeScriptAdapter extends BaseAdapter implements Adapter {
14
320
  return this.createParseResult('typescript');
15
321
  }
16
322
 
323
+ private maskedCache?: string;
324
+
325
+ /**
326
+ * The file with comments and string/template/regex literals blanked out,
327
+ * computed ONCE per adapter instance. Every extraction pass used to call
328
+ * maskLiterals(this.fileContent) itself, and buildFunction() called it again
329
+ * for each function -- O(functions x fileLength) per file.
330
+ */
331
+ private get maskedSource(): string {
332
+ if (this.maskedCache === undefined) {
333
+ this.maskedCache = maskLiterals(this.fileContent);
334
+ }
335
+ return this.maskedCache;
336
+ }
337
+
338
+
339
+ /**
340
+ * Numeric literals for the magic-numbers rule, which reads
341
+ * `adapter.extract()` -> `[{value, line}]`. Numbers inside comments and
342
+ * string/template literals are skipped via maskLiterals().
343
+ *
344
+ * A number that IS the initializer of a named constant is skipped: the rule's
345
+ * own advice is "consider using a named constant instead", so reporting the
346
+ * constant it just asked for made the violation unfixable -- extracting the
347
+ * literal could never clear it.
348
+ */
349
+ extract(): Array<{ value: number; line: number }> {
350
+ const src = this.fileContent;
351
+ const results: Array<{ value: number; line: number }> = [];
352
+ const masked = this.maskedSource;
353
+
354
+ // `const NAME = 3` / `const NAME: T = 3` -- capture the value's offset so the
355
+ // literal can be matched positionally rather than by value.
356
+ const namedConstRe = /\b(?:const|readonly)\s+[A-Za-z_$][\w$]*\s*(?::[^=;]+)?=\s*(-?\d+(?:\.\d+)?)\s*;/g;
357
+ const namedConstOffsets = new Set<number>();
358
+ let c: RegExpExecArray | null;
359
+ while ((c = namedConstRe.exec(masked)) !== null) {
360
+ namedConstOffsets.add(c.index + c[0].lastIndexOf(c[1]));
361
+ }
362
+
363
+ const numRe = /(?<![\w$.])(-?\d+(?:\.\d+)?)(?![\w$])/g;
364
+ let m: RegExpExecArray | null;
365
+ while ((m = numRe.exec(masked)) !== null) {
366
+ if (namedConstOffsets.has(m.index)) continue;
367
+ results.push({ value: Number(m[1]), line: lineOf(src, m.index) });
368
+ }
369
+ return results;
370
+ }
371
+
372
+ /**
373
+ * Import specifiers for the unused-imports rule. The rule reads
374
+ * `adapter.imports` as `Array<{name, line, used, type?}>` -- returning bare
375
+ * strings made every entry look unused and rendered `undefined` as the name.
376
+ * `used` is computed by counting identifier occurrences outside the import
377
+ * statements themselves.
378
+ */
379
+ get imports(): Array<{ name: string; line: number; used: boolean; type?: string }> {
380
+ const src = this.fileContent;
381
+ const entries: Array<{ name: string; line: number; used: boolean; type?: string }> = [];
382
+ const importRe = /^[ \t]*import\s+(?:type\s+)?([\s\S]*?)\s+from\s+['"][^'"]+['"]/gm;
383
+ const importSpans: Array<[number, number]> = [];
384
+ let m: RegExpExecArray | null;
385
+ const idRe = /^[A-Za-z_$][\w$]*$/;
386
+
387
+ while ((m = importRe.exec(src)) !== null) {
388
+ const clause = m[1].trim();
389
+ const line = lineOf(src, m.index);
390
+ const isTypeOnly = /^import\s+type\b/.test(m[0]);
391
+ const push = (name: string, type?: string) => {
392
+ if (idRe.test(name)) entries.push({ name, line, used: false, ...(type ? { type } : {}) });
393
+ };
394
+
395
+ // default import
396
+ const def = /^(\w+)/.exec(clause);
397
+ if (def) push(def[1], isTypeOnly ? 'type' : undefined);
398
+
399
+ // named imports { a, b as c }
400
+ const braces = /\{([\s\S]*?)\}/.exec(clause);
401
+ if (braces) {
402
+ for (const raw of braces[1].split(',')) {
403
+ const part = raw.trim();
404
+ if (!part) continue;
405
+ const asMatch = /\bas\s+(\w+)\s*$/.exec(part);
406
+ const local = asMatch ? asMatch[1] : part.replace(/^type\s+/, '').trim();
407
+ const memberTypeOnly = /^type\s+/.test(part);
408
+ push(local, isTypeOnly || memberTypeOnly ? 'type' : undefined);
409
+ }
410
+ }
411
+
412
+ // namespace import * as ns
413
+ const ns = /\*\s+as\s+(\w+)/.exec(clause);
414
+ if (ns) push(ns[1], isTypeOnly ? 'type' : undefined);
415
+
416
+ importSpans.push([m.index, m.index + m[0].length]);
417
+ }
418
+
419
+ // Count identifier usage outside import statements.
420
+ let body = src;
421
+ for (const [start, end] of [...importSpans].reverse()) {
422
+ body = body.substring(0, start) + ' '.repeat(end - start) + body.substring(end);
423
+ }
424
+ for (const entry of entries) {
425
+ const useRe = new RegExp(`(?<![\\w$.])${entry.name.replace(/\$/g, '\\$')}(?![\\w$])`, 'g');
426
+ entry.used = useRe.test(body);
427
+ }
428
+
429
+ return entries;
430
+ }
431
+
432
+ /**
433
+ * Interfaces for the ISP rule, which reads `adapter.extractInterfaces()`.
434
+ */
435
+ extractInterfaces(): Array<{ name: string; line: number; methods: string[]; methodCount: number; code: string }> {
436
+ const src = this.fileContent;
437
+ const masked = this.maskedSource;
438
+ const results: Array<{ name: string; line: number; methods: string[]; methodCount: number; code: string }> = [];
439
+ const ifaceRe = /(?:export\s+)?(?:declare\s+)?interface\s+(\w+)/g;
440
+ let m: RegExpExecArray | null;
441
+ while ((m = ifaceRe.exec(masked)) !== null) {
442
+ const bodyOpen = findDeclarationBrace(masked, ifaceRe.lastIndex);
443
+ if (bodyOpen === -1) continue;
444
+ const bodyEnd = findBlockEnd(masked, bodyOpen);
445
+ const body = src.substring(bodyOpen, bodyEnd);
446
+ const methods = [...body.matchAll(/(?<![\w$.])(\w+)\s*\(/g)]
447
+ .map(x => x[1])
448
+ .filter(n => !CONTROL_KEYWORDS.has(n));
449
+ results.push({
450
+ name: m[1],
451
+ line: lineOf(src, m.index),
452
+ methods,
453
+ methodCount: methods.length,
454
+ code: body,
455
+ });
456
+ }
457
+ return results;
458
+ }
459
+
460
+ private buildFunction(
461
+ name: string,
462
+ type: 'function' | 'method' | 'arrow',
463
+ nameIndex: number,
464
+ parenIndex: number,
465
+ bodyOpen: number,
466
+ ): FunctionMetrics {
467
+ const src = this.fileContent;
468
+ // Bodies/positions come from the real source; brace matching uses the masked
469
+ // text so braces inside regex/string literals cannot desync the block scan.
470
+ const masked = this.maskedSource;
471
+ const { inner } = readParenGroup(masked, parenIndex);
472
+ const bodyEnd = findBlockEnd(masked, bodyOpen);
473
+ const body = src.substring(bodyOpen, bodyEnd);
474
+ const startLine = lineOf(src, nameIndex);
475
+ const endLine = lineOf(src, bodyEnd);
476
+
477
+ // Arrow functions may be `=> expr` with no braces; then the "body" is a
478
+ // single expression and `findBlockEnd` will not have found a `{`.
479
+ const hasBraces = masked[bodyOpen] === '{';
480
+
481
+ return {
482
+ name,
483
+ type,
484
+ line: startLine,
485
+ startLine,
486
+ endLine,
487
+ length: hasBraces ? Math.max(1, endLine - startLine + 1) : 1,
488
+ paramCount: countParams(inner),
489
+ nestingDepth: hasBraces ? maxNesting(maskLiterals(body)) : 0,
490
+ hasTryCatch: /\btry\s*\{/.test(masked.substring(bodyOpen, bodyEnd)),
491
+ ioOperations: ioOperationsIn(masked.substring(bodyOpen, bodyEnd)),
492
+ code: body,
493
+ };
494
+ }
495
+
17
496
  extractFunctions(): unknown[] {
18
- const functionMatches = [];
19
- const fnRegex = /(export\s+)?(async\s+)?function\s+(\w+)\s*\([^)]*\)\s*[:\w\s]*{/g;
20
- let match;
497
+ const src = this.fileContent;
498
+ // Match declarations against the masked source so identifiers that appear
499
+ // only in comments or strings are never mistaken for functions, while
500
+ // bodies/positions still come from the original text (same offsets).
501
+ const masked = this.maskedSource;
502
+ const found: FunctionMetrics[] = [];
503
+ const seen = new Set<string>();
504
+
505
+ // 1. `function name(...)` declarations (incl. export / async / generators).
506
+ const declRe = /(?:^|[^\w$.])(?:export\s+)?(?:default\s+)?(?:async\s+)?function\s*\*?\s*(\w+)\s*/g;
507
+ let m: RegExpExecArray | null;
508
+ while ((m = declRe.exec(masked)) !== null) {
509
+ const nameIndex = m.index + m[0].length - m[1].length;
510
+ const parenIndex = masked.indexOf('(', declRe.lastIndex - 1);
511
+ if (parenIndex === -1) continue;
512
+ const bodyOpen = findBodyBrace(masked, parenIndex);
513
+ if (bodyOpen === -1) continue;
514
+ const key = `${m[1]}@${nameIndex}`;
515
+ if (seen.has(key)) continue;
516
+ seen.add(key);
517
+ found.push(this.buildFunction(m[1], 'function', nameIndex, parenIndex, bodyOpen));
518
+ }
21
519
 
22
- while ((match = fnRegex.exec(this.fileContent)) !== null) {
23
- functionMatches.push(this.createCodeMatch(match[3], 'function', match.index));
520
+ // 2. Class methods: `name(...) {`. Not anchored to line start so minified
521
+ // single-line bodies count too. Control-flow keywords are excluded -- a
522
+ // bare `if (a > 0) {` matches the shape but is not a method.
523
+ const memberRe = /(?<![\w$.])(?:public\s+|private\s+|protected\s+|static\s+|async\s+|readonly\s+|get\s+|set\s+|\*\s*)*(\w+)\s*\(/g;
524
+ while ((m = memberRe.exec(masked)) !== null) {
525
+ if (CONTROL_KEYWORDS.has(m[1])) continue;
526
+ // The pattern ends with `\(`, so the match's last character IS the opening
527
+ // paren. Re-searching with indexOf() could drift to a later `(` inside the
528
+ // parameter list or a type annotation, which corrupted the reported span.
529
+ const parenIndex = m.index + m[0].length - 1;
530
+ const nameIndex = parenIndex - m[1].length;
531
+ // Only treat as a method when a body brace follows the parameter list.
532
+ const bodyOpen = findBodyBrace(masked, parenIndex);
533
+ if (bodyOpen === -1) continue;
534
+ const key = `${m[1]}@${nameIndex}`;
535
+ if (seen.has(key)) continue;
536
+ seen.add(key);
537
+ found.push(this.buildFunction(m[1], 'method', nameIndex, parenIndex, bodyOpen));
24
538
  }
25
539
 
26
- return functionMatches;
540
+ // 3. Arrow consts / assignments: `const name = (...) =>` / `name = x =>`.
541
+ const arrowRe = /(?:const|let|var)\s+(\w+)\s*(?::[^=]+)?=\s*(?:async\s*)?/g;
542
+ while ((m = arrowRe.exec(masked)) !== null) {
543
+ const rest = masked.substring(arrowRe.lastIndex);
544
+ const parenOffset = /^\s*\(/.test(rest) ? rest.indexOf('(') : -1;
545
+ let parenIndex: number;
546
+ if (parenOffset !== -1) {
547
+ parenIndex = arrowRe.lastIndex + parenOffset;
548
+ } else {
549
+ // Single-identifier param: `x => ...`
550
+ const simple = /^\s*(\w+)\s*=>/.exec(rest);
551
+ if (!simple) continue;
552
+ parenIndex = -1;
553
+ }
554
+ const afterParams = parenIndex === -1
555
+ ? arrowRe.lastIndex + (/^\s*(\w+)/.exec(rest) as RegExpExecArray)[0].length
556
+ : readParenGroup(src, parenIndex).end;
557
+ if (!/^\s*(?::[^=]+)?=>/.test(masked.substring(afterParams))) continue;
558
+
559
+ const nameIndex = m.index + m[0].length - m[1].length;
560
+ const braceAfter = masked.indexOf('{', afterParams);
561
+ const arrowBodyStart = braceAfter !== -1 && /^\s*(?::[^=]+)?=>\s*\{/.test(masked.substring(afterParams, braceAfter + 1))
562
+ ? braceAfter
563
+ : -1;
564
+
565
+ let length = 1;
566
+ let nestingDepth = 0;
567
+ let hasTryCatch = false;
568
+ let ioOperations: string[] = [];
569
+ let code = '';
570
+ if (arrowBodyStart !== -1) {
571
+ const bodyEnd = findBlockEnd(masked, arrowBodyStart);
572
+ code = src.substring(arrowBodyStart, bodyEnd);
573
+ const startLine = lineOf(src, nameIndex);
574
+ const endLine = lineOf(src, bodyEnd);
575
+ length = Math.max(1, endLine - startLine + 1);
576
+ nestingDepth = maxNesting(code);
577
+ hasTryCatch = /\btry\s*\{/.test(code);
578
+ ioOperations = ioOperationsIn(code);
579
+ }
580
+
581
+ const startLine = lineOf(src, nameIndex);
582
+ const key = `${m[1]}@${nameIndex}`;
583
+ if (seen.has(key)) continue;
584
+ seen.add(key);
585
+ found.push({
586
+ name: m[1],
587
+ type: 'arrow',
588
+ line: startLine,
589
+ startLine,
590
+ endLine: startLine,
591
+ length,
592
+ paramCount: parenIndex === -1 ? 1 : countParams(readParenGroup(src, parenIndex).inner),
593
+ nestingDepth,
594
+ hasTryCatch,
595
+ ioOperations,
596
+ code,
597
+ });
598
+ }
599
+
600
+ return found;
27
601
  }
28
602
 
29
603
  extractClasses(): unknown[] {
30
- const classMatches = [];
31
- const classRegex = /(export\s+)?class\s+(\w+)\s*(extends\s+[\w.]+)?\s*{/g;
32
- let match;
604
+ const src = this.fileContent;
605
+ const classRe = /(export\s+)?(?:default\s+)?(?:abstract\s+)?class\s+(\w+)/g;
606
+ const results: Array<Record<string, unknown>> = [];
607
+ const masked = this.maskedSource;
608
+ let m: RegExpExecArray | null;
609
+
610
+ while ((m = classRe.exec(masked)) !== null) {
611
+ const name = m[2];
612
+ const nameIndex = m.index + m[0].length - name.length;
613
+ const bodyOpen = findDeclarationBrace(masked, classRe.lastIndex);
614
+ if (bodyOpen === -1) continue;
615
+ const bodyEnd = findBlockEnd(masked, bodyOpen);
616
+ const body = src.substring(bodyOpen, bodyEnd);
33
617
 
34
- while ((match = classRegex.exec(this.fileContent)) !== null) {
35
- classMatches.push(this.createCodeMatch(match[2], 'class', match.index));
618
+ // Count method-ish members. Not anchored to line start: minified /
619
+ // single-line class bodies (`m1() {} m2() {}`) are valid TS and must count.
620
+ // `(?<![\w$.])` avoids matching call sites like `foo.bar(`.
621
+ const memberRe = /(?<![\w$.])(?:public\s+|private\s+|protected\s+|static\s+|async\s+|readonly\s+|get\s+|set\s+|\*\s*)*(\w+)\s*\(/g;
622
+ const methods = [...body.matchAll(memberRe)]
623
+ .map(x => x[1])
624
+ .filter(n => !['if', 'for', 'while', 'switch', 'catch', 'return', 'typeof', 'new'].includes(n));
625
+
626
+ results.push({
627
+ name,
628
+ type: 'class',
629
+ line: lineOf(src, nameIndex),
630
+ startLine: lineOf(src, nameIndex),
631
+ endLine: lineOf(src, bodyEnd),
632
+ methodCount: methods.length,
633
+ methods,
634
+ code: body,
635
+ });
36
636
  }
37
637
 
38
- return classMatches;
638
+ return results;
39
639
  }
40
640
 
41
641
  extractExports(): unknown[] {
@@ -63,4 +663,4 @@ export class TypeScriptAdapter extends BaseAdapter implements Adapter {
63
663
 
64
664
  return exportMatches;
65
665
  }
66
- }
666
+ }
@@ -1,4 +1,5 @@
1
1
  import { Rule, Violation, Adapter } from './types';
2
+ import { getActiveConfig } from './config';
2
3
  import { TypeScriptAdapter } from './adapters/typescript';
3
4
  import { PythonAdapter } from './adapters/python';
4
5
  import { GoAdapter } from './adapters/go';
@@ -113,6 +114,12 @@ function computeSummary(
113
114
 
114
115
  function runRuleOnFile(ctx: RunRuleContext): void {
115
116
  const { file, rule, adapter, violations, fileResult, ruleResult, errors } = ctx;
117
+ // Honour `enabled: false` from `.principlesrc`. Nothing read this flag before
118
+ // #457, so a project could not turn a rule off.
119
+ //
120
+ // Checked BEFORE `filesChecked++`: a rule that never ran must not report that it
121
+ // examined a file, or the per-rule stats claim coverage the rule did not perform.
122
+ if (!isRuleEnabled(rule.id)) return;
116
123
  ruleResult.filesChecked++;
117
124
  try {
118
125
  const ruleViolations = rule.check(file, adapter);
@@ -128,6 +135,21 @@ function runRuleOnFile(ctx: RunRuleContext): void {
128
135
  }
129
136
  }
130
137
 
138
+ /**
139
+ * Whether `ruleId` (e.g. `clean-code.large-file`) is enabled in the active
140
+ * config. Unknown or malformed entries default to enabled: a config typo must
141
+ * not silently disable a gate.
142
+ */
143
+ function isRuleEnabled(ruleId: string): boolean {
144
+ const separator = ruleId.indexOf('.');
145
+ if (separator <= 0) return true;
146
+ const group = ruleId.slice(0, separator);
147
+ const name = ruleId.slice(separator + 1);
148
+ const rules = getActiveConfig().rules as Record<string, Record<string, { enabled?: boolean }>>;
149
+ const settings = rules[group]?.[name];
150
+ return settings?.enabled !== false;
151
+ }
152
+
131
153
  interface RunRuleContext {
132
154
  file: string;
133
155
  rule: Rule;