@boyingliu01/xp-gate 0.35.16 → 0.35.17

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 (33) hide show
  1. package/hooks/pre-commit +7 -1
  2. package/lib/mattpocock-license-verdict.json +1 -1
  3. package/mock-policy/AGENTS.md +2 -2
  4. package/mutation/AGENTS.md +2 -2
  5. package/mutation/runners/mutmut-runner.ts +165 -70
  6. package/package.json +1 -1
  7. package/plugins/claude-code/.claude-plugin/plugin.json +1 -1
  8. package/plugins/claude-code/skills/delphi-review/AGENTS.md +2 -2
  9. package/plugins/claude-code/skills/sprint-flow/AGENTS.md +2 -2
  10. package/plugins/claude-code/skills/test-specification-alignment/AGENTS.md +2 -2
  11. package/plugins/dsh/package.json +1 -1
  12. package/plugins/dsh/skills/delphi-review/AGENTS.md +2 -2
  13. package/plugins/dsh/skills/sprint-flow/AGENTS.md +2 -2
  14. package/plugins/dsh/skills/test-specification-alignment/AGENTS.md +2 -2
  15. package/plugins/opencode/package.json +1 -1
  16. package/plugins/opencode/skills/delphi-review/AGENTS.md +2 -2
  17. package/plugins/opencode/skills/sprint-flow/AGENTS.md +2 -2
  18. package/plugins/opencode/skills/test-specification-alignment/AGENTS.md +2 -2
  19. package/plugins/qoder/plugin.json +1 -1
  20. package/plugins/qoder/skills/delphi-review/AGENTS.md +2 -2
  21. package/plugins/qoder/skills/sprint-flow/AGENTS.md +2 -2
  22. package/plugins/qoder/skills/test-specification-alignment/AGENTS.md +2 -2
  23. package/principles/AGENTS.md +2 -2
  24. package/principles/adapters/import-clause.ts +57 -0
  25. package/principles/adapters/member-scan.ts +73 -0
  26. package/principles/adapters/position-analysis.ts +43 -0
  27. package/principles/adapters/quote-state.ts +80 -0
  28. package/principles/adapters/typescript.ts +57 -116
  29. package/principles/rules/clean-code/god-class.ts +37 -14
  30. package/principles/rules/solid/dip.ts +15 -2
  31. package/skills/delphi-review/AGENTS.md +2 -2
  32. package/skills/sprint-flow/AGENTS.md +2 -2
  33. package/skills/test-specification-alignment/AGENTS.md +2 -2
package/hooks/pre-commit CHANGED
@@ -2659,7 +2659,13 @@ else
2659
2659
  # Exclusions come from filter_project_paths (#649). The hand-maintained list
2660
2660
  # this replaced omitted vendor/target/coverage/out, so a staged vendor/x.ts
2661
2661
  # was reported as new source with no test.
2662
- NEW_TS_FILES=$(git diff --cached --name-only --diff-filter=A | filter_project_paths | grep -E '\.(ts|tsx)$' | grep -v '\.d\.ts$' | grep -v '__tests__' | grep -v '\.test\.' | grep -v '\.spec\.' | grep -v '__snapshots__' | grep -Ev '(^|/)vitest\.config\.' | grep -Ev '(^|/)vite\.config\.' | grep -Ev '(^|/)jest\.config\.' | grep -Ev '(^|/)tsconfig\.' | grep -Ev '(^|/)eslint\.' | grep -Ev '(^|/)prettier\.' | grep -Ev '(^|/)tailwind\.' || true)
2662
+ #
2663
+ # Byte-identical mirrors under src/npm-package/ are excluded the same way
2664
+ # lint (#494) and Boy Scout (#649) already exclude them. A mirror cannot
2665
+ # carry its own test: the next resync overwrites the directory, so the
2666
+ # requirement is structurally unsatisfiable and blocked every commit that
2667
+ # added a file to a mirrored module. The canonical source keeps its test.
2668
+ NEW_TS_FILES=$(git diff --cached --name-only --diff-filter=A | filter_project_paths | grep -Ev "$MIRROR_PATH_FILTER" | grep -E '\.(ts|tsx)$' | grep -v '\.d\.ts$' | grep -v '__tests__' | grep -v '\.test\.' | grep -v '\.spec\.' | grep -v '__snapshots__' | grep -Ev '(^|/)vitest\.config\.' | grep -Ev '(^|/)vite\.config\.' | grep -Ev '(^|/)jest\.config\.' | grep -Ev '(^|/)tsconfig\.' | grep -Ev '(^|/)eslint\.' | grep -Ev '(^|/)prettier\.' | grep -Ev '(^|/)tailwind\.' || true)
2663
2669
 
2664
2670
  # Strip PROJECT_SUBDIR prefix from new-TS-file paths ONLY when the hook
2665
2671
  # actually cd'd into the subdirectory (IN_PROJECT_SUBDIR=1; single-language
@@ -2,6 +2,6 @@
2
2
  "verdict": "PASS",
3
3
  "license_type": "MIT",
4
4
  "sha256": "0e7ac423bf2c6e223b7c5b156f8cf72da49d748e56a1641402c31f22ad07dbb5",
5
- "checked_at": "2026-10-31T05:05:10.442Z",
5
+ "checked_at": "2026-10-31T07:40:57.523Z",
6
6
  "source": "network"
7
7
  }
@@ -1,9 +1,9 @@
1
1
  # SRC/MOCK-POLICY KNOWLEDGE BASE
2
2
 
3
3
  **Generated:** 2026-10-31
4
- **Commit:** 3aa988e7
4
+ **Commit:** 26a741de
5
5
  **Branch:** main
6
- **Version:** 0.35.16.0
6
+ **Version:** 0.35.17.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-10-31
4
- **Commit:** 3aa988e7
4
+ **Commit:** 26a741de
5
5
  **Branch:** main
6
- **Version:** 0.35.16.0
6
+ **Version:** 0.35.17.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.
@@ -1,5 +1,5 @@
1
1
  // @no-test-required: mutmut wrapper — tested via gate-m integration + pre-push Gate M
2
- import { spawn, execSync } from 'child_process';
2
+ import { spawn, execSync, type ChildProcess } from 'child_process';
3
3
  import { writeFileSync, unlinkSync, readFileSync, existsSync } from 'fs';
4
4
  import { join, dirname } from 'path';
5
5
  import type {
@@ -24,6 +24,87 @@ type MutmutRoute = 'native' | 'wsl' | 'unavailable' | 'unresolved';
24
24
  * - Results stored in SQLite; `mutmut results` outputs per-mutant status lines
25
25
  * - `mutmut run` stdout shows emoji progress: 🎉 N 🫥 N ⏰ N 🤔 N 🙁 N
26
26
  */
27
+ /**
28
+ * Wire a child process's stdout/stderr into one accumulating callback.
29
+ *
30
+ * Extracted from `runMutmut`: the two listeners were six lines of pure
31
+ * accumulation inside an already-long Promise body, and their nesting was part
32
+ * of what made that method exceed both the length and the nesting budget.
33
+ */
34
+ function collectOutput(
35
+ child: ChildProcess,
36
+ onChunk: (chunk: string, isStderr: boolean) => void,
37
+ ): void {
38
+ child.stdout?.on('data', (data: Buffer) => onChunk(data.toString(), false));
39
+ child.stderr?.on('data', (data: Buffer) => onChunk(data.toString(), true));
40
+ }
41
+
42
+ /** How long a SIGTERMed mutmut gets before it is killed outright. */
43
+ const ESCALATION_DELAY_MS = 5000;
44
+
45
+ /** Where the pre-run copy of pyproject.toml lives. */
46
+ function pyprojectBackupPath(pyprojectPath: string): string {
47
+ return `${pyprojectPath}.xp-gate-backup`;
48
+ }
49
+
50
+ /**
51
+ * Put pyproject.toml back the way it was, or remove the one we generated.
52
+ *
53
+ * Cleanup is best-effort by design: the mutation run's own result is what the
54
+ * caller acts on, and failing here would replace a usable outcome with an
55
+ * exception raised from a `finally`. Extracted from `run` because the nested
56
+ * try/if/if/else was what pushed that method to 5 nesting levels.
57
+ */
58
+ function restorePyproject(pyprojectPath: string, hadExisting: boolean): void {
59
+ const backupPath = pyprojectBackupPath(pyprojectPath);
60
+ try {
61
+ if (hadExisting && existsSync(backupPath)) {
62
+ writeFileSync(pyprojectPath, readFileSync(backupPath, 'utf-8'), { encoding: 'utf-8' });
63
+ unlinkSync(backupPath);
64
+ } else if (!hadExisting && existsSync(pyprojectPath)) {
65
+ unlinkSync(pyprojectPath);
66
+ }
67
+ } catch {
68
+ // Best effort cleanup
69
+ }
70
+ }
71
+
72
+ /** The tracker shape `settleTracker` returns; only the timeout path uses these. */
73
+ interface SettleTracker {
74
+ didTimeOut: () => boolean;
75
+ markTimedOut: () => void;
76
+ settle: (outcome: MutationRunOutcome) => void;
77
+ isSettled: () => boolean;
78
+ }
79
+
80
+ /**
81
+ * Arm the run timeout, escalating SIGTERM to SIGKILL once.
82
+ *
83
+ * Extracted from `runMutmut` for the same reason as `collectOutput`: the
84
+ * two-step kill sequence is twelve lines of policy that pushed the method past
85
+ * both budgets. The escalation timer id is handed back through `setEscalation`
86
+ * so the caller's `cancelTimers` closure can still reach it.
87
+ *
88
+ * @returns the primary timer id, to be cleared by the caller.
89
+ */
90
+ function armTimeout(
91
+ child: ChildProcess,
92
+ timeoutMs: number,
93
+ tracker: SettleTracker,
94
+ setEscalation: (id: NodeJS.Timeout) => void,
95
+ ): NodeJS.Timeout {
96
+ const escalationMs = ESCALATION_DELAY_MS;
97
+ return setTimeout(() => {
98
+ tracker.markTimedOut();
99
+ child.kill('SIGTERM');
100
+ if (tracker.isSettled()) return;
101
+ // SIGTERM may be ignored; escalate once.
102
+ setEscalation(setTimeout(() => {
103
+ if (!tracker.isSettled()) child.kill('SIGKILL');
104
+ }, escalationMs));
105
+ }, timeoutMs);
106
+ }
107
+
27
108
  export class MutmutRunner implements MutationRunner {
28
109
  readonly name = 'mutmut';
29
110
  readonly extensions = ['py'];
@@ -72,33 +153,35 @@ export class MutmutRunner implements MutationRunner {
72
153
  const sourceDirs = this.extractUniqueDirs(options.files);
73
154
 
74
155
  const pyprojectPath = join(options.cwd, 'pyproject.toml');
75
- const backupPath = join(options.cwd, 'pyproject.toml.xp-gate-backup');
76
- let hadExisting = false;
156
+ const hadExisting = this.backupPyproject(pyprojectPath);
77
157
 
78
158
  try {
79
- if (existsSync(pyprojectPath)) {
80
- hadExisting = true;
81
- const existing = readFileSync(pyprojectPath, 'utf-8');
82
- writeFileSync(backupPath, existing, { encoding: 'utf-8' });
83
- }
84
159
  this.writeMutmutConfig(pyprojectPath, sourceDirs);
85
-
86
160
  return await this.runMutmut(options);
87
161
  } finally {
88
- try {
89
- if (hadExisting && existsSync(backupPath)) {
90
- const backup = readFileSync(backupPath, 'utf-8');
91
- writeFileSync(pyprojectPath, backup, { encoding: 'utf-8' });
92
- unlinkSync(backupPath);
93
- } else if (!hadExisting && existsSync(pyprojectPath)) {
94
- unlinkSync(pyprojectPath);
95
- }
96
- } catch {
97
- // Best effort cleanup
98
- }
162
+ restorePyproject(pyprojectPath, hadExisting);
99
163
  }
100
164
  }
101
165
 
166
+ /**
167
+ * Copy an existing pyproject.toml aside so `run` can put it back.
168
+ * @returns whether a file was there to back up.
169
+ */
170
+ private backupPyproject(pyprojectPath: string): boolean {
171
+ if (!existsSync(pyprojectPath)) return false;
172
+ const backupPath = pyprojectBackupPath(pyprojectPath);
173
+ try {
174
+ writeFileSync(backupPath, readFileSync(pyprojectPath, 'utf-8'), { encoding: 'utf-8' });
175
+ } catch (err) {
176
+ // Failing here must be loud: without a backup, `restorePyproject` would
177
+ // silently leave this repo's pyproject.toml overwritten by a generated one.
178
+ throw new Error(
179
+ `Could not back up ${pyprojectPath} before writing a mutmut config: ${(err as Error).message}`,
180
+ );
181
+ }
182
+ return true;
183
+ }
184
+
102
185
  /**
103
186
  * Build the terminal-outcome handler for one `mutmut run`.
104
187
  *
@@ -173,39 +256,17 @@ export class MutmutRunner implements MutationRunner {
173
256
 
174
257
  const tracker = this.settleTracker(resolve, cancelTimers);
175
258
 
176
- const timeoutId = setTimeout(() => {
177
- tracker.markTimedOut();
178
- child.kill('SIGTERM');
179
- if (tracker.isSettled()) return;
180
- // SIGTERM may be ignored; escalate once, and let the retry live in a
181
- // variable so `cancelTimers` can reach it.
182
- escalationId = setTimeout(() => {
183
- if (!tracker.isSettled()) child.kill('SIGKILL');
184
- }, 5000);
185
- }, options.timeoutMs);
186
-
187
- child.stdout?.on('data', (data: Buffer) => {
188
- stdout += data.toString();
259
+ const timeoutId = armTimeout(child, options.timeoutMs, tracker, (id) => {
260
+ escalationId = id;
189
261
  });
190
262
 
191
- child.stderr?.on('data', (data: Buffer) => {
192
- stderr += data.toString();
263
+ collectOutput(child, (chunk, isErr) => {
264
+ if (isErr) stderr += chunk;
265
+ else stdout += chunk;
193
266
  });
194
267
 
195
268
  child.on('close', (code) => {
196
- // A null exit code means the process was killed ("timed out" when we did
197
- // it, "unknown" otherwise); there is no report to parse either way.
198
- if (code === null) {
199
- tracker.settle({ report: null, timedOut: tracker.didTimeOut() });
200
- return;
201
- }
202
-
203
- tracker.settle({
204
- // Parse results from mutmut run stdout (v3.x emoji progress)
205
- report: this.parseEmojiProgress(stdout),
206
- timedOut: tracker.didTimeOut(),
207
- error: code !== 0 ? stderr || stdout : undefined,
208
- });
269
+ tracker.settle(this.outcomeFor(code, stdout, stderr, tracker));
209
270
  });
210
271
 
211
272
  child.on('error', (err) => {
@@ -214,6 +275,29 @@ export class MutmutRunner implements MutationRunner {
214
275
  });
215
276
  }
216
277
 
278
+ /**
279
+ * Turn one `mutmut run` exit into the outcome the caller expects.
280
+ *
281
+ * Extracted from `runMutmut`'s close handler to bring that method back under
282
+ * the length and nesting budgets. A null exit code means the process was
283
+ * killed -- "timed out" when we did it, "unknown" otherwise -- and there is no
284
+ * report to parse either way.
285
+ */
286
+ private outcomeFor(
287
+ code: number | null,
288
+ stdout: string,
289
+ stderr: string,
290
+ tracker: SettleTracker,
291
+ ): MutationRunOutcome {
292
+ if (code === null) return { report: null, timedOut: tracker.didTimeOut() };
293
+ return {
294
+ // Parse results from mutmut run stdout (v3.x emoji progress)
295
+ report: this.parseEmojiProgress(stdout),
296
+ timedOut: tracker.didTimeOut(),
297
+ error: code !== 0 ? stderr || stdout : undefined,
298
+ };
299
+ }
300
+
217
301
  private toWslPath(path: string): string | null {
218
302
  const match = path.match(/^([A-Za-z]):[\\/](.*)$/);
219
303
  if (!match) return null;
@@ -300,28 +384,39 @@ export class MutmutRunner implements MutationRunner {
300
384
  }
301
385
 
302
386
  private writeMutmutConfig(pyprojectPath: string, sourceDirs: string[]): void {
303
- const sourcePaths = sourceDirs.map((d) => `"${d}"`).join(', ');
304
- const sourcePathsLine = `source_paths = [${sourcePaths}]`;
305
-
306
- let content = '';
307
- if (existsSync(pyprojectPath)) {
308
- content = readFileSync(pyprojectPath, 'utf-8');
309
- if (content.includes('[tool.mutmut]')) {
310
- const sourcePathsRegex = /source_paths\s*=\s*\[.*?\]/;
311
- if (sourcePathsRegex.test(content)) {
312
- content = content.replace(sourcePathsRegex, sourcePathsLine);
313
- } else {
314
- content = content.replace(
315
- '[tool.mutmut]',
316
- `[tool.mutmut]\n${sourcePathsLine}`,
317
- );
318
- }
319
- } else {
320
- content = content.trimEnd() + `\n\n[tool.mutmut]\n${sourcePathsLine}\n`;
321
- }
322
- } else {
323
- content = `[tool.mutmut]\n${sourcePathsLine}\n`;
387
+ const sourcePathsLine = `source_paths = [${sourceDirs.map((d) => `"${d}"`).join(', ')}]`;
388
+ try {
389
+ // Read here so the content builder stays pure: it then has no I/O to
390
+ // fail on, and the one place that can fail is the one that explains why.
391
+ const existing = existsSync(pyprojectPath) ? readFileSync(pyprojectPath, 'utf-8') : null;
392
+ writeFileSync(pyprojectPath, mutmutConfigContent(existing, sourcePathsLine), {
393
+ encoding: 'utf-8',
394
+ });
395
+ } catch (err) {
396
+ // A missing or half-written pyproject.toml would make mutmut scan the wrong
397
+ // tree, so surface the path instead of letting a bare fs error escape.
398
+ throw new Error(`Could not write mutmut config to ${pyprojectPath}: ${(err as Error).message}`);
324
399
  }
325
- writeFileSync(pyprojectPath, content, { encoding: 'utf-8' });
326
400
  }
327
401
  }
402
+
403
+ /**
404
+ * pyproject.toml content with `[tool.mutmut] source_paths` set as requested.
405
+ *
406
+ * Pure by design: the caller reads the existing file and passes it in, so this
407
+ * function has no I/O that could fail partway through. Three shapes have to be
408
+ * preserved -- an existing `source_paths` is rewritten in place, a bare
409
+ * `[tool.mutmut]` table gets the key added, and a file without the table gets
410
+ * one appended (a fresh file passes `null`).
411
+ *
412
+ * @param existing current file content, or null when the file does not exist.
413
+ */
414
+ function mutmutConfigContent(existing: string | null, sourcePathsLine: string): string {
415
+ if (existing === null) return `[tool.mutmut]\n${sourcePathsLine}\n`;
416
+ if (!existing.includes('[tool.mutmut]')) {
417
+ return existing.trimEnd() + `\n\n[tool.mutmut]\n${sourcePathsLine}\n`;
418
+ }
419
+ const sourcePathsRegex = /source_paths\s*=\s*\[.*?\]/;
420
+ if (sourcePathsRegex.test(existing)) return existing.replace(sourcePathsRegex, sourcePathsLine);
421
+ return existing.replace('[tool.mutmut]', `[tool.mutmut]\n${sourcePathsLine}`);
422
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@boyingliu01/xp-gate",
3
- "version": "0.35.16",
3
+ "version": "0.35.17",
4
4
  "description": "AI-driven development workflow: 6 quality gates + Delphi review + Sprint Flow",
5
5
  "bin": {
6
6
  "xp-gate": "bin/xp-gate.js"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "xp-gate",
3
- "version": "0.35.16",
3
+ "version": "0.35.17",
4
4
  "displayName": "XP-Gate",
5
5
  "description": "Extreme Programming quality gates + AI workflow skills for Claude Code. Includes 12 pre-commit gates (Gate 0-11), 8 pre-push gates, Sprint Flow (6 phases), and Delphi multi-expert review (>=90% consensus).",
6
6
  "author": {
@@ -1,9 +1,9 @@
1
1
  # SKILLS/DELPHI-REVIEW KNOWLEDGE BASE
2
2
 
3
3
  **Generated:** 2026-10-31
4
- **Commit:** 3aa988e7
4
+ **Commit:** 26a741de
5
5
  **Branch:** main
6
- **Version:** 0.35.16.0
6
+ **Version:** 0.35.17.0
7
7
 
8
8
  ## OVERVIEW
9
9
  Delphi Consensus Review — multi-round anonymous expert review (≥90% threshold, exactly 3 experts with distinct executable model IDs). Supports design, requirements, and code-walkthrough modes.
@@ -1,9 +1,9 @@
1
1
  # SKILLS/SPRINT-FLOW KNOWLEDGE BASE
2
2
 
3
3
  **Generated:** 2026-10-31
4
- **Commit:** 3aa988e7
4
+ **Commit:** 26a741de
5
5
  **Branch:** main
6
- **Version:** 0.35.16.0
6
+ **Version:** 0.35.17.0
7
7
 
8
8
  ## OVERVIEW
9
9
  **6-phase** development pipeline (v2.0 compact redesign, Issue #290): PREP → DESIGN → BUILD → VERIFY → SHIP → CLOSE. Phase 3/6 BUILD default build mode is **ralph-loop** (REQ-level iteration, 40-67% token savings vs parallel). HARD-GATE between DESIGN (2/6) and BUILD (3/6): design must pass Delphi review (≥90% consensus) before any coding.
@@ -1,9 +1,9 @@
1
1
  # SKILLS/TEST-SPECIFICATION-ALIGNMENT KNOWLEDGE BASE
2
2
 
3
3
  **Generated:** 2026-10-31
4
- **Commit:** 3aa988e7
4
+ **Commit:** 26a741de
5
5
  **Branch:** main
6
- **Version:** 0.35.16.0
6
+ **Version:** 0.35.17.0
7
7
 
8
8
  ## OVERVIEW
9
9
  Test-Specification Alignment Engine — two-stage validation ensuring tests accurately reflect requirements and design specs.
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@boyingliu01/dsh-plugin-xp-gate",
3
- "version": "0.35.16",
3
+ "version": "0.35.17",
4
4
  "description": "XP-Gate deterministic quality gates + AI workflow skills as a DeepSeek Harness (DSH) native plugin: gate-check / gate-principles / gate-arch tools + 12 SKILL.md skills",
5
5
  "type": "module",
6
6
  "main": "./lib/index.js",
@@ -1,9 +1,9 @@
1
1
  # SKILLS/DELPHI-REVIEW KNOWLEDGE BASE
2
2
 
3
3
  **Generated:** 2026-10-31
4
- **Commit:** 3aa988e7
4
+ **Commit:** 26a741de
5
5
  **Branch:** main
6
- **Version:** 0.35.16.0
6
+ **Version:** 0.35.17.0
7
7
 
8
8
  ## OVERVIEW
9
9
  Delphi Consensus Review — multi-round anonymous expert review (≥90% threshold, exactly 3 experts with distinct executable model IDs). Supports design, requirements, and code-walkthrough modes.
@@ -1,9 +1,9 @@
1
1
  # SKILLS/SPRINT-FLOW KNOWLEDGE BASE
2
2
 
3
3
  **Generated:** 2026-10-31
4
- **Commit:** 3aa988e7
4
+ **Commit:** 26a741de
5
5
  **Branch:** main
6
- **Version:** 0.35.16.0
6
+ **Version:** 0.35.17.0
7
7
 
8
8
  ## OVERVIEW
9
9
  **6-phase** development pipeline (v2.0 compact redesign, Issue #290): PREP → DESIGN → BUILD → VERIFY → SHIP → CLOSE. Phase 3/6 BUILD default build mode is **ralph-loop** (REQ-level iteration, 40-67% token savings vs parallel). HARD-GATE between DESIGN (2/6) and BUILD (3/6): design must pass Delphi review (≥90% consensus) before any coding.
@@ -1,9 +1,9 @@
1
1
  # SKILLS/TEST-SPECIFICATION-ALIGNMENT KNOWLEDGE BASE
2
2
 
3
3
  **Generated:** 2026-10-31
4
- **Commit:** 3aa988e7
4
+ **Commit:** 26a741de
5
5
  **Branch:** main
6
- **Version:** 0.35.16.0
6
+ **Version:** 0.35.17.0
7
7
 
8
8
  ## OVERVIEW
9
9
  Test-Specification Alignment Engine — two-stage validation ensuring tests accurately reflect requirements and design specs.
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@boyingliu01/opencode-plugin",
3
- "version": "0.35.16",
3
+ "version": "0.35.17",
4
4
  "type": "module",
5
5
  "main": "index.ts",
6
6
  "description": "XP-Gate quality gates + AI workflow skills for OpenCode",
@@ -1,9 +1,9 @@
1
1
  # SKILLS/DELPHI-REVIEW KNOWLEDGE BASE
2
2
 
3
3
  **Generated:** 2026-10-31
4
- **Commit:** 3aa988e7
4
+ **Commit:** 26a741de
5
5
  **Branch:** main
6
- **Version:** 0.35.16.0
6
+ **Version:** 0.35.17.0
7
7
 
8
8
  ## OVERVIEW
9
9
  Delphi Consensus Review — multi-round anonymous expert review (≥90% threshold, exactly 3 experts with distinct executable model IDs). Supports design, requirements, and code-walkthrough modes.
@@ -1,9 +1,9 @@
1
1
  # SKILLS/SPRINT-FLOW KNOWLEDGE BASE
2
2
 
3
3
  **Generated:** 2026-10-31
4
- **Commit:** 3aa988e7
4
+ **Commit:** 26a741de
5
5
  **Branch:** main
6
- **Version:** 0.35.16.0
6
+ **Version:** 0.35.17.0
7
7
 
8
8
  ## OVERVIEW
9
9
  **6-phase** development pipeline (v2.0 compact redesign, Issue #290): PREP → DESIGN → BUILD → VERIFY → SHIP → CLOSE. Phase 3/6 BUILD default build mode is **ralph-loop** (REQ-level iteration, 40-67% token savings vs parallel). HARD-GATE between DESIGN (2/6) and BUILD (3/6): design must pass Delphi review (≥90% consensus) before any coding.
@@ -1,9 +1,9 @@
1
1
  # SKILLS/TEST-SPECIFICATION-ALIGNMENT KNOWLEDGE BASE
2
2
 
3
3
  **Generated:** 2026-10-31
4
- **Commit:** 3aa988e7
4
+ **Commit:** 26a741de
5
5
  **Branch:** main
6
- **Version:** 0.35.16.0
6
+ **Version:** 0.35.17.0
7
7
 
8
8
  ## OVERVIEW
9
9
  Test-Specification Alignment Engine — two-stage validation ensuring tests accurately reflect requirements and design specs.
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "xp-gate",
3
- "version": "0.35.16",
3
+ "version": "0.35.17",
4
4
  "displayName": "XP-Gate",
5
5
  "description": "Extreme Programming quality gates + AI workflow skills for Qoder. Includes 12 pre-commit gates (Gate 0-11), 8 pre-push gates, Sprint Flow (6 phases), and Delphi multi-expert review (>=90% consensus).",
6
6
  "author": {
@@ -1,9 +1,9 @@
1
1
  # SKILLS/DELPHI-REVIEW KNOWLEDGE BASE
2
2
 
3
3
  **Generated:** 2026-10-31
4
- **Commit:** 3aa988e7
4
+ **Commit:** 26a741de
5
5
  **Branch:** main
6
- **Version:** 0.35.16.0
6
+ **Version:** 0.35.17.0
7
7
 
8
8
  ## OVERVIEW
9
9
  Delphi Consensus Review — multi-round anonymous expert review (≥90% threshold, exactly 3 experts with distinct executable model IDs). Supports design, requirements, and code-walkthrough modes.
@@ -1,9 +1,9 @@
1
1
  # SKILLS/SPRINT-FLOW KNOWLEDGE BASE
2
2
 
3
3
  **Generated:** 2026-10-31
4
- **Commit:** 3aa988e7
4
+ **Commit:** 26a741de
5
5
  **Branch:** main
6
- **Version:** 0.35.16.0
6
+ **Version:** 0.35.17.0
7
7
 
8
8
  ## OVERVIEW
9
9
  **6-phase** development pipeline (v2.0 compact redesign, Issue #290): PREP → DESIGN → BUILD → VERIFY → SHIP → CLOSE. Phase 3/6 BUILD default build mode is **ralph-loop** (REQ-level iteration, 40-67% token savings vs parallel). HARD-GATE between DESIGN (2/6) and BUILD (3/6): design must pass Delphi review (≥90% consensus) before any coding.
@@ -1,9 +1,9 @@
1
1
  # SKILLS/TEST-SPECIFICATION-ALIGNMENT KNOWLEDGE BASE
2
2
 
3
3
  **Generated:** 2026-10-31
4
- **Commit:** 3aa988e7
4
+ **Commit:** 26a741de
5
5
  **Branch:** main
6
- **Version:** 0.35.16.0
6
+ **Version:** 0.35.17.0
7
7
 
8
8
  ## OVERVIEW
9
9
  Test-Specification Alignment Engine — two-stage validation ensuring tests accurately reflect requirements and design specs.
@@ -1,9 +1,9 @@
1
1
  # PRINCIPLES CHECKER MODULE
2
2
 
3
3
  **Generated:** 2026-10-31
4
- **Commit:** 3aa988e7
4
+ **Commit:** 26a741de
5
5
  **Branch:** main
6
- **Version:** 0.35.16.0
6
+ **Version:** 0.35.17.0
7
7
 
8
8
  ## OVERVIEW
9
9
  Clean Code & SOLID principles checker — **Gate 4** of pre-commit. 14 rules × 9 language adapters, SARIF 2.1.0 output. Houses the **Boy Scout Rule** enforcement engine (Gate 6) and warning-baseline storage.
@@ -0,0 +1,57 @@
1
+ /**
2
+ * Import-clause parsing for the TypeScript adapter's `imports` getter.
3
+ *
4
+ * Extracted from `typescript.ts` so that one function does not have to carry
5
+ * both the parsing rules and the usage-scoring loop: together they pushed the
6
+ * getter past the 50-line budget and to 5 nesting levels. Splitting also lets
7
+ * the clause grammar be tested without reading a file.
8
+ */
9
+
10
+ /** One symbol bound by an import statement. */
11
+ export interface ImportBinding {
12
+ name: string;
13
+ type?: string;
14
+ }
15
+
16
+ /** A plain identifier -- these are the only names an import can bind. */
17
+ const IDENTIFIER = /^[A-Za-z_$][\w$]*$/;
18
+
19
+ /**
20
+ * Parse the clause between `import` and `from` into the symbols it binds.
21
+ *
22
+ * Covers the four shapes TypeScript allows: a default binding, a named brace
23
+ * list (with per-member `type` and `as` renames), and a namespace binding.
24
+ *
25
+ * @param clause text between `import` / `import type` and `from`
26
+ * @param isTypeOnly whether the whole statement was `import type ...`
27
+ */
28
+ export function parseImportClause(clause: string, isTypeOnly: boolean): ImportBinding[] {
29
+ const bindings: ImportBinding[] = [];
30
+ const marker = isTypeOnly ? 'type' : undefined;
31
+ const push = (name: string, type?: string): void => {
32
+ if (IDENTIFIER.test(name)) bindings.push({ name, ...(type ? { type } : {}) });
33
+ };
34
+
35
+ // default import
36
+ const def = /^(\w+)/.exec(clause);
37
+ if (def) push(def[1], marker);
38
+
39
+ // named imports { a, b as c }
40
+ const braces = /\{([\s\S]*?)\}/.exec(clause);
41
+ if (braces) {
42
+ for (const raw of braces[1].split(',')) {
43
+ const part = raw.trim();
44
+ if (!part) continue;
45
+ const asMatch = /\bas\s+(\w+)\s*$/.exec(part);
46
+ const local = asMatch ? asMatch[1] : part.replace(/^type\s+/, '').trim();
47
+ const memberTypeOnly = /^type\s+/.test(part);
48
+ push(local, isTypeOnly || memberTypeOnly ? 'type' : undefined);
49
+ }
50
+ }
51
+
52
+ // namespace import * as ns
53
+ const ns = /\*\s+as\s+(\w+)/.exec(clause);
54
+ if (ns) push(ns[1], marker);
55
+
56
+ return bindings;
57
+ }
@@ -0,0 +1,73 @@
1
+ /**
2
+ * Class-member scanning for the TypeScript adapter.
3
+ *
4
+ * Extracted from `typescript.ts` to keep that file inside its line budget, and
5
+ * because this is the piece that was wrong: counting members by looking at the
6
+ * character BEFORE a name reported every call site inside a method body as a
7
+ * member. `{ helper(1); }` and `{ async run() { ... }` differ in what follows the
8
+ * name, not in what precedes it -- so depth is what separates them.
9
+ */
10
+
11
+ /** Words that look like `name(` but are control flow, never members. */
12
+ const CONTROL_KEYWORDS = new Set([
13
+ 'if', 'for', 'while', 'switch', 'catch', 'return', 'typeof', 'new',
14
+ ]);
15
+
16
+ /** Class body members live one level in: the body string starts AT its `{`. */
17
+ const MEMBER_DEPTH = 1;
18
+
19
+ /**
20
+ * Names of the members declared directly in a class body.
21
+ *
22
+ * Depth-aware rather than prefix-aware: a member declaration sits at brace
23
+ * depth 1 of the body, and a call inside a method sits deeper. Nesting is
24
+ * tracked over the MASKED body, so braces inside strings and comments cannot
25
+ * shift the depth -- prose in a JSDoc line like
26
+ * "three separate events (close / error / timeout)" otherwise reads as
27
+ * `events(` and is counted as a method, as is `unknown` from `(): unknown {`.
28
+ *
29
+ * Arrow-function properties (`handler = () => {}`) carry no `name(` shape and
30
+ * are not counted; they were not counted before either.
31
+ */
32
+ export function countTopLevelMembers(body: string): string[] {
33
+ const names: string[] = [];
34
+ let depth = 0;
35
+ for (let i = 0; i < body.length; i += 1) {
36
+ const c = body[i];
37
+ if (c === '{') { depth += 1; continue; }
38
+ if (c === '}') { depth -= 1; continue; }
39
+ if (depth !== MEMBER_DEPTH || c !== '(') continue;
40
+ const name = memberNameBefore(body, i);
41
+ if (name && !CONTROL_KEYWORDS.has(name.value)) names.push(name.value);
42
+ }
43
+ return names;
44
+ }
45
+
46
+ /**
47
+ * The identifier immediately before the `(` at `parenIndex`, skipping modifiers
48
+ * (`public`, `static`, `async`, `readonly`, `get`, `set`, `*`).
49
+ *
50
+ * @returns the name and its offset, or null when no identifier precedes the paren.
51
+ */
52
+ function memberNameBefore(
53
+ body: string,
54
+ parenIndex: number,
55
+ ): { value: string; index: number } | null {
56
+ let end = parenIndex;
57
+ while (end > 0 && /\s/.test(body[end - 1])) end -= 1;
58
+ let start = end;
59
+ while (start > 0 && /[\w$]/.test(body[start - 1])) start -= 1;
60
+ if (start === end) return null;
61
+ return { value: body.substring(start, end), index: start };
62
+ }
63
+
64
+ /**
65
+ * Claim a `name@offset` key once, so overlapping extraction shapes cannot
66
+ * double-count the same declaration.
67
+ */
68
+ export function claimMember(seen: Set<string>, name: string, nameIndex: number): boolean {
69
+ const key = `${name}@${nameIndex}`;
70
+ if (seen.has(key)) return false;
71
+ seen.add(key);
72
+ return true;
73
+ }
@@ -0,0 +1,43 @@
1
+ /**
2
+ * Position analysis for TypeScript's regex-based extraction.
3
+ *
4
+ * The extraction regexes match `name(` for both a method DECLARATION and a CALL.
5
+ * This module answers which one it is, keeping `typescript.ts` inside its line
6
+ * budget and giving the rule a place to be tested directly.
7
+ */
8
+
9
+ /**
10
+ * Characters that can only precede a CALL, never a declaration.
11
+ *
12
+ * `.` covers member calls (`this.c()`, `obj.method()`), `(` a call used as an
13
+ * argument (`if (existsSync(p))`), `,` a call in a list (`foo(a, bar(b))`), and
14
+ * the operators cover a call in a condition or expression
15
+ * (`hadExisting && existsSync(p)`, `x = read(p)`, `!isReady(q)`). No member
16
+ * declaration begins with any of them.
17
+ *
18
+ * Deliberately NOT included: `{`, `;`, `}` and plain whitespace. Those precede
19
+ * BOTH shapes -- `{ helper(1); }` is a call, `{ run(): void {` is a declaration
20
+ * -- so using them makes the answer depend on formatting. Depth is what
21
+ * separates those two, which is why extractClasses counts by brace depth.
22
+ */
23
+ const CALL_PRECEDERS = new Set([
24
+ '.', '(', ',',
25
+ '&', '|', '=', '!', '?', ':', '+', '-', '*', '/', '<', '>', '[',
26
+ ]);
27
+
28
+ /**
29
+ * Is the identifier starting at `nameIndex` a call rather than a declaration?
30
+ *
31
+ * Without this check `if (existsSync(p))` was extracted as a method named
32
+ * `existsSync` -- the extraction regex's lookbehind permits `(` before the name
33
+ * -- which then borrowed the enclosing block's brace as a "body" and reported a
34
+ * phantom `missing-error-handling` warning that no edit could clear. It also
35
+ * inflated `extractClasses().methodCount` with the names of whatever the class
36
+ * body happened to call (66 reported for a class declaring 30).
37
+ */
38
+ export function isCallPosition(masked: string, nameIndex: number): boolean {
39
+ let i = nameIndex - 1;
40
+ while (i >= 0 && /\s/.test(masked[i])) i -= 1;
41
+ return i >= 0 && CALL_PRECEDERS.has(masked[i]);
42
+ }
43
+
@@ -0,0 +1,80 @@
1
+ /**
2
+ * Character-level quote and delimiter tracking for TypeScript source scanning.
3
+ *
4
+ * The param-list and brace scanners all need "is this delimiter inside a string
5
+ * or template?" before they can act on it. That logic is identical for every
6
+ * caller, so it lives here instead of being repeated -- and it keeps
7
+ * `typescript.ts` inside the 1050-line budget.
8
+ */
9
+
10
+ export interface QuoteState {
11
+ inSingle: boolean;
12
+ inDouble: boolean;
13
+ inTemplate: boolean;
14
+ }
15
+
16
+ export function newQuoteState(): QuoteState {
17
+ return { inSingle: false, inDouble: false, inTemplate: false };
18
+ }
19
+
20
+ export function inQuotedState(state: QuoteState): boolean {
21
+ return state.inSingle || state.inDouble || state.inTemplate;
22
+ }
23
+
24
+ /** The three quote kinds tracked by QuoteState, with the flag that marks each. */
25
+ const QUOTE_KINDS: ReadonlyArray<{ quote: string; flag: 'inSingle' | 'inDouble' | 'inTemplate' }> = [
26
+ { quote: "'", flag: 'inSingle' },
27
+ { quote: '"', flag: 'inDouble' },
28
+ { quote: '`', flag: 'inTemplate' },
29
+ ];
30
+
31
+ /**
32
+ * Advance one character of quote tracking.
33
+ *
34
+ * Extracted because `countParams` and `readParenGroup` carried the same
35
+ * six-branch block verbatim (#602): both must ignore delimiters that appear
36
+ * inside a string, and neither may treat an escape as a closing quote.
37
+ *
38
+ * Table-driven (#620): all three quote kinds behave identically -- an escape
39
+ * skips two characters, the matching quote closes the literal -- so they are one
40
+ * rule applied to a list rather than three copies of it.
41
+ *
42
+ * @returns the number of characters consumed (2 when the next char is escaped).
43
+ */
44
+ export function advanceQuoteState(src: string, i: number, state: QuoteState): number {
45
+ const c = src[i];
46
+
47
+ for (const kind of QUOTE_KINDS) {
48
+ if (!state[kind.flag]) continue;
49
+ if (c === '\\') return 2;
50
+ if (c === kind.quote) state[kind.flag] = false;
51
+ return 1;
52
+ }
53
+
54
+ // Not inside any literal: open the one this character starts, if any.
55
+ const opener = QUOTE_KINDS.find(kind => kind.quote === c);
56
+ if (opener) state[opener.flag] = true;
57
+ return 1;
58
+ }
59
+
60
+ /** Opening delimiters that raise the generic/bracket depth in a param list. */
61
+ const DEPTH_OPENERS = new Set(['(', '[', '{', '<']);
62
+ /** Closing delimiters that lower it. */
63
+ const DEPTH_CLOSERS = new Set([')', ']', '}', '>']);
64
+
65
+ /**
66
+ * Adjust `depth` for one delimiter.
67
+ * @returns true when the character was a delimiter (so it cannot also be a
68
+ * separator), false otherwise.
69
+ */
70
+ export function applyDelimiter(c: string, depth: { value: number }): boolean {
71
+ if (DEPTH_OPENERS.has(c)) { depth.value++; return true; }
72
+ if (DEPTH_CLOSERS.has(c)) { depth.value--; return true; }
73
+ return false;
74
+ }
75
+
76
+ /** True when the character opens a quote/template the scanners must skip. */
77
+ export function isQuote(c: string): boolean {
78
+ return c === "'" || c === '"' || c === '`';
79
+ }
80
+
@@ -1,5 +1,11 @@
1
1
  import { BaseAdapter } from './base';
2
2
  import { Adapter } from '../types';
3
+ import { isCallPosition } from './position-analysis';
4
+ import { claimMember, countTopLevelMembers } from './member-scan';
5
+ import { parseImportClause } from './import-clause';
6
+ import {
7
+ type QuoteState, newQuoteState, inQuotedState, advanceQuoteState, applyDelimiter, isQuote,
8
+ } from './quote-state';
3
9
 
4
10
  /** IO-ish callees that the missing-error-handling rule cares about. */
5
11
  const IO_CALLEES = [
@@ -181,77 +187,6 @@ function lineOf(src: string, index: number): number {
181
187
  }
182
188
 
183
189
  /** The three quoted states shared by the parameter/paren scanners. */
184
- interface QuoteState {
185
- inSingle: boolean;
186
- inDouble: boolean;
187
- inTemplate: boolean;
188
- }
189
-
190
- function newQuoteState(): QuoteState {
191
- return { inSingle: false, inDouble: false, inTemplate: false };
192
- }
193
-
194
- function inQuotedState(state: QuoteState): boolean {
195
- return state.inSingle || state.inDouble || state.inTemplate;
196
- }
197
-
198
- /** The three quote kinds tracked by QuoteState, with the flag that marks each. */
199
- const QUOTE_KINDS: ReadonlyArray<{ quote: string; flag: 'inSingle' | 'inDouble' | 'inTemplate' }> = [
200
- { quote: "'", flag: 'inSingle' },
201
- { quote: '"', flag: 'inDouble' },
202
- { quote: '`', flag: 'inTemplate' },
203
- ];
204
-
205
- /**
206
- * Advance one character of quote tracking.
207
- *
208
- * Extracted because `countParams` and `readParenGroup` carried the same
209
- * six-branch block verbatim (#602): both must ignore delimiters that appear
210
- * inside a string, and neither may treat an escape as a closing quote.
211
- *
212
- * Table-driven (#620): all three quote kinds behave identically -- an escape
213
- * skips two characters, the matching quote closes the literal -- so they are one
214
- * rule applied to a list rather than three copies of it.
215
- *
216
- * @returns the number of characters consumed (2 when the next char is escaped).
217
- */
218
- function advanceQuoteState(src: string, i: number, state: QuoteState): number {
219
- const c = src[i];
220
-
221
- for (const kind of QUOTE_KINDS) {
222
- if (!state[kind.flag]) continue;
223
- if (c === '\\') return 2;
224
- if (c === kind.quote) state[kind.flag] = false;
225
- return 1;
226
- }
227
-
228
- // Not inside any literal: open the one this character starts, if any.
229
- const opener = QUOTE_KINDS.find(kind => kind.quote === c);
230
- if (opener) state[opener.flag] = true;
231
- return 1;
232
- }
233
-
234
- /** Opening delimiters that raise the generic/bracket depth in a param list. */
235
- const DEPTH_OPENERS = new Set(['(', '[', '{', '<']);
236
- /** Closing delimiters that lower it. */
237
- const DEPTH_CLOSERS = new Set([')', ']', '}', '>']);
238
-
239
- /**
240
- * Adjust `depth` for one delimiter.
241
- * @returns true when the character was a delimiter (so it cannot also be a
242
- * separator), false otherwise.
243
- */
244
- function applyDelimiter(c: string, depth: { value: number }): boolean {
245
- if (DEPTH_OPENERS.has(c)) { depth.value++; return true; }
246
- if (DEPTH_CLOSERS.has(c)) { depth.value--; return true; }
247
- return false;
248
- }
249
-
250
- /** True when the character opens a quote/template the scanners must skip. */
251
- function isQuote(c: string): boolean {
252
- return c === "'" || c === '"' || c === '`';
253
- }
254
-
255
190
  /** Count top-level, comma-separated parameters in a raw parameter list. */
256
191
  function countParams(rawList: string): number {
257
192
  const trimmed = rawList.trim();
@@ -726,37 +661,13 @@ export class TypeScriptAdapter extends BaseAdapter implements Adapter {
726
661
  const importRe = /^[ \t]*import\s+(?:type\s+)?([\s\S]*?)\s+from\s+['"][^'"]+['"]/gm;
727
662
  const importSpans: Array<[number, number]> = [];
728
663
  let m: RegExpExecArray | null;
729
- const idRe = /^[A-Za-z_$][\w$]*$/;
730
664
 
731
665
  while ((m = importRe.exec(src)) !== null) {
732
- const clause = m[1].trim();
733
666
  const line = lineOf(src, m.index);
734
667
  const isTypeOnly = /^import\s+type\b/.test(m[0]);
735
- const push = (name: string, type?: string) => {
736
- if (idRe.test(name)) entries.push({ name, line, used: false, ...(type ? { type } : {}) });
737
- };
738
-
739
- // default import
740
- const def = /^(\w+)/.exec(clause);
741
- if (def) push(def[1], isTypeOnly ? 'type' : undefined);
742
-
743
- // named imports { a, b as c }
744
- const braces = /\{([\s\S]*?)\}/.exec(clause);
745
- if (braces) {
746
- for (const raw of braces[1].split(',')) {
747
- const part = raw.trim();
748
- if (!part) continue;
749
- const asMatch = /\bas\s+(\w+)\s*$/.exec(part);
750
- const local = asMatch ? asMatch[1] : part.replace(/^type\s+/, '').trim();
751
- const memberTypeOnly = /^type\s+/.test(part);
752
- push(local, isTypeOnly || memberTypeOnly ? 'type' : undefined);
753
- }
668
+ for (const binding of parseImportClause(m[1].trim(), isTypeOnly)) {
669
+ entries.push({ ...binding, line, used: false });
754
670
  }
755
-
756
- // namespace import * as ns
757
- const ns = /\*\s+as\s+(\w+)/.exec(clause);
758
- if (ns) push(ns[1], isTypeOnly ? 'type' : undefined);
759
-
760
671
  importSpans.push([m.index, m.index + m[0].length]);
761
672
  }
762
673
 
@@ -831,7 +742,9 @@ export class TypeScriptAdapter extends BaseAdapter implements Adapter {
831
742
  length: hasBraces ? Math.max(1, endLine - startLine + 1) : 1,
832
743
  paramCount: countParams(inner),
833
744
  nestingDepth: hasBraces ? maxNesting(maskLiterals(body)) : 0,
834
- hasTryCatch: /\btry\s*\{/.test(masked.substring(bodyOpen, bodyEnd)),
745
+ // The RAW body: `on('error', ...)` needs its string literal intact, and
746
+ // masking blanks string bodies. try/catch is a keyword and is unaffected.
747
+ hasTryCatch: hasErrorHandling(body),
835
748
  ioOperations: ioOperationsIn(masked.substring(bodyOpen, bodyEnd)),
836
749
  code: body,
837
750
  };
@@ -853,13 +766,6 @@ export class TypeScriptAdapter extends BaseAdapter implements Adapter {
853
766
  }
854
767
 
855
768
  /** Claim a `name@offset` key once, so overlapping shapes cannot double-count. */
856
- private claim(seen: Set<string>, name: string, nameIndex: number): boolean {
857
- const key = `${name}@${nameIndex}`;
858
- if (seen.has(key)) return false;
859
- seen.add(key);
860
- return true;
861
- }
862
-
863
769
  /** `function name(...)` declarations, including export / async / generators. */
864
770
  private collectFunctionDecls(seen: Set<string>, found: FunctionMetrics[]): void {
865
771
  const masked = this.maskedSource;
@@ -871,7 +777,7 @@ export class TypeScriptAdapter extends BaseAdapter implements Adapter {
871
777
  if (parenIndex === -1) continue;
872
778
  const bodyOpen = findBodyBrace(masked, parenIndex);
873
779
  if (bodyOpen === -1) continue;
874
- if (!this.claim(seen, m[1], nameIndex)) continue;
780
+ if (!claimMember(seen, m[1], nameIndex)) continue;
875
781
  found.push(this.buildFunction(m[1], 'function', nameIndex, parenIndex, bodyOpen));
876
782
  }
877
783
  }
@@ -892,10 +798,11 @@ export class TypeScriptAdapter extends BaseAdapter implements Adapter {
892
798
  // parameter list or a type annotation, which corrupted the reported span.
893
799
  const parenIndex = m.index + m[0].length - 1;
894
800
  const nameIndex = parenIndex - m[1].length;
801
+ if (isCallPosition(masked, nameIndex)) continue;
895
802
  // Only treat as a method when a body brace follows the parameter list.
896
803
  const bodyOpen = findBodyBrace(masked, parenIndex);
897
804
  if (bodyOpen === -1) continue;
898
- if (!this.claim(seen, m[1], nameIndex)) continue;
805
+ if (!claimMember(seen, m[1], nameIndex)) continue;
899
806
  found.push(this.buildFunction(m[1], 'method', nameIndex, parenIndex, bodyOpen));
900
807
  }
901
808
  }
@@ -909,7 +816,7 @@ export class TypeScriptAdapter extends BaseAdapter implements Adapter {
909
816
  const arrow = this.parseArrowAt(m, arrowRe.lastIndex);
910
817
  if (!arrow) continue;
911
818
  const nameIndex = m.index + m[0].length - m[1].length;
912
- if (!this.claim(seen, m[1], nameIndex)) continue;
819
+ if (!claimMember(seen, m[1], nameIndex)) continue;
913
820
  found.push(this.buildArrowMetrics(m[1], nameIndex, arrow));
914
821
  }
915
822
  }
@@ -1003,14 +910,19 @@ export class TypeScriptAdapter extends BaseAdapter implements Adapter {
1003
910
  if (bodyOpen === -1) continue;
1004
911
  const bodyEnd = findBlockEnd(masked, bodyOpen);
1005
912
  const body = src.substring(bodyOpen, bodyEnd);
1006
-
1007
- // Count method-ish members. Not anchored to line start: minified /
1008
- // single-line class bodies (`m1() {} m2() {}`) are valid TS and must count.
1009
- // `(?<![\w$.])` avoids matching call sites like `foo.bar(`.
1010
- const memberRe = /(?<![\w$.])(?:public\s+|private\s+|protected\s+|static\s+|async\s+|readonly\s+|get\s+|set\s+|\*\s*)*(\w+)\s*\(/g;
1011
- const methods = [...body.matchAll(memberRe)]
1012
- .map(x => x[1])
1013
- .filter(n => !['if', 'for', 'while', 'switch', 'catch', 'return', 'typeof', 'new'].includes(n));
913
+ // Members are counted on the MASKED body: the prose in a JSDoc line like
914
+ // "three separate events (close / error / timeout)" reads as `events(` and
915
+ // was counted as a method, as did `unknown` from a `(): unknown {` return
916
+ // type. Masking blanks comments and string bodies, so only real code has
917
+ // braces and parens left.
918
+ const maskedBody = masked.substring(bodyOpen, bodyEnd);
919
+
920
+ // Count members declared at the class body's TOP brace depth. A call
921
+ // inside a method body sits one level deeper, which is the only signal
922
+ // that separates `{ helper(1); }` from `{ async run() { ... }`.
923
+ // Earlier attempts compared the preceding character and reported 66
924
+ // methods for a class declaring 30 -- every call site counted once.
925
+ const methods = countTopLevelMembers(maskedBody);
1014
926
 
1015
927
  results.push({
1016
928
  name,
@@ -1065,3 +977,32 @@ export class TypeScriptAdapter extends BaseAdapter implements Adapter {
1065
977
  return exportMatches;
1066
978
  }
1067
979
  }
980
+
981
+
982
+ /**
983
+ * Does this function handle failures?
984
+ *
985
+ * `try`/`catch` is one way, and for a function that owns a child process it is
986
+ * not the idiomatic one: Node reports spawn failures through an `'error'`
987
+ * event, and a `try` around `spawn()` would never see them. Treating the event
988
+ * listener as unhandled reported `runMutmut` for having "I/O without error
989
+ * handling" while it was in fact the only handler for that failure mode --
990
+ * a warning no correct edit could clear, which is how a rule loses its meaning.
991
+ *
992
+ * `'error'` is matched as a QUOTED argument (`on('error', ...)`) so that a
993
+ * string that merely contains the word does not qualify.
994
+ */
995
+ function hasErrorHandling(body: string): boolean {
996
+ return TRY_BLOCK.test(body) || ERROR_LISTENER.test(body);
997
+ }
998
+
999
+ /**
1000
+ * The two handler shapes, as module-level constants.
1001
+ *
1002
+ * ccn-report locates a function body by counting brackets on its lines; a regex
1003
+ * literal INSIDE the body makes that extent ambiguous, and the row comes back
1004
+ * "CCN could not be span-verified" -- a Gate 3 block. Brackets and braces in the
1005
+ * patterns are written as character classes for the same reason.
1006
+ */
1007
+ const TRY_BLOCK = /\btry\s*[{]/;
1008
+ const ERROR_LISTENER = /\bon\s*[(]\s*['"]error['"]\s*,/;
@@ -15,25 +15,26 @@ export const godClassRule: Rule = {
15
15
  code?: string;
16
16
  line: number;
17
17
  name: string;
18
+ /** Method count when the adapter parsed the class structurally. */
19
+ methodCount?: number;
18
20
  }
19
-
21
+
20
22
  interface TypedAdapter {
21
23
  extractClasses?: () => ClassObject[] | undefined;
22
24
  }
23
-
25
+
24
26
  const typedAdapter = adapter as TypedAdapter;
25
27
  const classes = typedAdapter.extractClasses?.() || [];
26
-
28
+
27
29
  for (const cls of classes) {
28
- const methodMatches = cls.code?.match(/(get|set)\s+\w+\s*\(|\w+\s*\([^)]*\)\s*{/g) || [];
29
-
30
- const methodCount = methodMatches.filter((m: string) => {
31
- if (m.startsWith('get ') || m.startsWith('set ')) {
32
- return false;
33
- }
34
- return true;
35
- }).length;
36
-
30
+ // Prefer the adapter's structural count. The regex fallback below cannot
31
+ // see methods whose signature spans lines or carries a return type, so on
32
+ // `TypeScriptAdapter` (66 methods) it counted 0 while `solid.srp` read the
33
+ // adapter's own figure and reported it -- the same class, two different
34
+ // answers, and the wrong one was silent. A count the adapter already
35
+ // computed must not be recomputed by guessing at source text.
36
+ const methodCount = cls.methodCount ?? countMethodsByRegex(cls.code);
37
+
37
38
  if (methodCount > (settings.threshold ?? 0)) {
38
39
  violations.push({
39
40
  file,
@@ -46,7 +47,29 @@ export const godClassRule: Rule = {
46
47
  });
47
48
  }
48
49
  }
49
-
50
+
50
51
  return violations;
51
52
  }
52
- };
53
+ };
54
+
55
+ /**
56
+ * Source-text method matcher.
57
+ *
58
+ * Kept at module scope on purpose: ccn-report locates a function body by counting
59
+ * brackets on its lines, so a regex literal inside the body makes the extent
60
+ * ambiguous and the row comes back "CCN could not be span-verified".
61
+ */
62
+ const METHOD_PATTERN = /(?:get|set)\s+\w+\s*[(]|\w+\s*[(][^)]*[)]\s*[{]/g;
63
+
64
+ /** A matched accessor keyword pair rather than a real method. */
65
+ const ACCESSOR_PATTERN = /^(?:get|set)\s/;
66
+
67
+ /**
68
+ * Count methods by matching source text. Only used when the adapter does not
69
+ * report `methodCount` (Java, C++ and other regex-backed adapters), so the
70
+ * getter/setter exclusion stays meaningful.
71
+ */
72
+ function countMethodsByRegex(code: string | undefined): number {
73
+ const methodMatches = code?.match(METHOD_PATTERN) ?? [];
74
+ return methodMatches.filter((m: string) => !ACCESSOR_PATTERN.test(m)).length;
75
+ }
@@ -1,8 +1,14 @@
1
1
  import { Rule, Violation, Severity } from '../../types';
2
2
  import { getActiveConfig } from '../../config';
3
3
 
4
+ // Value objects, not collaborators. Every entry here is a type that ordinary
5
+ // code CONSTRUCTS from data it already has, and that has no injectable
6
+ // alternative: you cannot pass in a Date/Map/RegExp factory any more usefully
7
+ // than you can pass in the number 1. Constructing one is not the inversion being
8
+ // bypassed, so reporting it teaches the wrong lesson (#676).
4
9
  const DEFAULT_EXCLUDED_CLASSES = [
5
- 'Date', 'Map', 'Set', 'Error', 'Array', 'Object', 'Promise'
10
+ 'Date', 'Map', 'Set', 'Error', 'Array', 'Object', 'Promise',
11
+ 'RegExp', 'Number', 'String', 'Boolean', 'BigInt', 'Symbol', 'WeakMap', 'WeakSet',
6
12
  ];
7
13
 
8
14
  /**
@@ -10,9 +16,16 @@ const DEFAULT_EXCLUDED_CLASSES = [
10
16
  *
11
17
  * A module-load snapshot was #457: a project's `.principlesrc` override was
12
18
  * parsed but never consulted.
19
+ *
20
+ * The config entry EXTENDS the built-in value-object list rather than replacing
21
+ * it (#676). Replacing meant a project that added one name silently lost every
22
+ * default exemption -- `.principlesrc` here lists 7 entries, so `new RegExp()`
23
+ * was reported even though the default list would have excused it. A project
24
+ * cannot reasonably be expected to restate the built-ins it never knew about.
13
25
  */
14
26
  function excludedClasses(): (string | number)[] {
15
- return getActiveConfig().rules['solid']['dip'].exclude ?? DEFAULT_EXCLUDED_CLASSES;
27
+ const configured = getActiveConfig().rules['solid']['dip'].exclude ?? [];
28
+ return [...new Set([...DEFAULT_EXCLUDED_CLASSES, ...configured])];
16
29
  }
17
30
 
18
31
  function shouldSkipInstantiation(className: string): boolean {
@@ -1,9 +1,9 @@
1
1
  # SKILLS/DELPHI-REVIEW KNOWLEDGE BASE
2
2
 
3
3
  **Generated:** 2026-10-31
4
- **Commit:** 3aa988e7
4
+ **Commit:** 26a741de
5
5
  **Branch:** main
6
- **Version:** 0.35.16.0
6
+ **Version:** 0.35.17.0
7
7
 
8
8
  ## OVERVIEW
9
9
  Delphi Consensus Review — multi-round anonymous expert review (≥90% threshold, exactly 3 experts with distinct executable model IDs). Supports design, requirements, and code-walkthrough modes.
@@ -1,9 +1,9 @@
1
1
  # SKILLS/SPRINT-FLOW KNOWLEDGE BASE
2
2
 
3
3
  **Generated:** 2026-10-31
4
- **Commit:** 3aa988e7
4
+ **Commit:** 26a741de
5
5
  **Branch:** main
6
- **Version:** 0.35.16.0
6
+ **Version:** 0.35.17.0
7
7
 
8
8
  ## OVERVIEW
9
9
  **6-phase** development pipeline (v2.0 compact redesign, Issue #290): PREP → DESIGN → BUILD → VERIFY → SHIP → CLOSE. Phase 3/6 BUILD default build mode is **ralph-loop** (REQ-level iteration, 40-67% token savings vs parallel). HARD-GATE between DESIGN (2/6) and BUILD (3/6): design must pass Delphi review (≥90% consensus) before any coding.
@@ -1,9 +1,9 @@
1
1
  # SKILLS/TEST-SPECIFICATION-ALIGNMENT KNOWLEDGE BASE
2
2
 
3
3
  **Generated:** 2026-10-31
4
- **Commit:** 3aa988e7
4
+ **Commit:** 26a741de
5
5
  **Branch:** main
6
- **Version:** 0.35.16.0
6
+ **Version:** 0.35.17.0
7
7
 
8
8
  ## OVERVIEW
9
9
  Test-Specification Alignment Engine — two-stage validation ensuring tests accurately reflect requirements and design specs.