@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.
- package/hooks/pre-commit +7 -1
- package/lib/mattpocock-license-verdict.json +1 -1
- package/mock-policy/AGENTS.md +2 -2
- package/mutation/AGENTS.md +2 -2
- package/mutation/runners/mutmut-runner.ts +165 -70
- package/package.json +1 -1
- package/plugins/claude-code/.claude-plugin/plugin.json +1 -1
- package/plugins/claude-code/skills/delphi-review/AGENTS.md +2 -2
- package/plugins/claude-code/skills/sprint-flow/AGENTS.md +2 -2
- package/plugins/claude-code/skills/test-specification-alignment/AGENTS.md +2 -2
- package/plugins/dsh/package.json +1 -1
- package/plugins/dsh/skills/delphi-review/AGENTS.md +2 -2
- package/plugins/dsh/skills/sprint-flow/AGENTS.md +2 -2
- package/plugins/dsh/skills/test-specification-alignment/AGENTS.md +2 -2
- package/plugins/opencode/package.json +1 -1
- package/plugins/opencode/skills/delphi-review/AGENTS.md +2 -2
- package/plugins/opencode/skills/sprint-flow/AGENTS.md +2 -2
- package/plugins/opencode/skills/test-specification-alignment/AGENTS.md +2 -2
- package/plugins/qoder/plugin.json +1 -1
- package/plugins/qoder/skills/delphi-review/AGENTS.md +2 -2
- package/plugins/qoder/skills/sprint-flow/AGENTS.md +2 -2
- package/plugins/qoder/skills/test-specification-alignment/AGENTS.md +2 -2
- package/principles/AGENTS.md +2 -2
- package/principles/adapters/import-clause.ts +57 -0
- package/principles/adapters/member-scan.ts +73 -0
- package/principles/adapters/position-analysis.ts +43 -0
- package/principles/adapters/quote-state.ts +80 -0
- package/principles/adapters/typescript.ts +57 -116
- package/principles/rules/clean-code/god-class.ts +37 -14
- package/principles/rules/solid/dip.ts +15 -2
- package/skills/delphi-review/AGENTS.md +2 -2
- package/skills/sprint-flow/AGENTS.md +2 -2
- 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
|
-
|
|
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
|
package/mock-policy/AGENTS.md
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
# SRC/MOCK-POLICY KNOWLEDGE BASE
|
|
2
2
|
|
|
3
3
|
**Generated:** 2026-10-31
|
|
4
|
-
**Commit:**
|
|
4
|
+
**Commit:** 26a741de
|
|
5
5
|
**Branch:** main
|
|
6
|
-
**Version:** 0.35.
|
|
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.
|
package/mutation/AGENTS.md
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
# SRC/MUTATION KNOWLEDGE BASE
|
|
2
2
|
|
|
3
3
|
**Generated:** 2026-10-31
|
|
4
|
-
**Commit:**
|
|
4
|
+
**Commit:** 26a741de
|
|
5
5
|
**Branch:** main
|
|
6
|
-
**Version:** 0.35.
|
|
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
|
|
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
|
-
|
|
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 =
|
|
177
|
-
|
|
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
|
|
192
|
-
stderr +=
|
|
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
|
-
|
|
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
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
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": "xp-gate",
|
|
3
|
-
"version": "0.35.
|
|
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:**
|
|
4
|
+
**Commit:** 26a741de
|
|
5
5
|
**Branch:** main
|
|
6
|
-
**Version:** 0.35.
|
|
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:**
|
|
4
|
+
**Commit:** 26a741de
|
|
5
5
|
**Branch:** main
|
|
6
|
-
**Version:** 0.35.
|
|
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:**
|
|
4
|
+
**Commit:** 26a741de
|
|
5
5
|
**Branch:** main
|
|
6
|
-
**Version:** 0.35.
|
|
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.
|
package/plugins/dsh/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@boyingliu01/dsh-plugin-xp-gate",
|
|
3
|
-
"version": "0.35.
|
|
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:**
|
|
4
|
+
**Commit:** 26a741de
|
|
5
5
|
**Branch:** main
|
|
6
|
-
**Version:** 0.35.
|
|
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:**
|
|
4
|
+
**Commit:** 26a741de
|
|
5
5
|
**Branch:** main
|
|
6
|
-
**Version:** 0.35.
|
|
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:**
|
|
4
|
+
**Commit:** 26a741de
|
|
5
5
|
**Branch:** main
|
|
6
|
-
**Version:** 0.35.
|
|
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
|
# SKILLS/DELPHI-REVIEW KNOWLEDGE BASE
|
|
2
2
|
|
|
3
3
|
**Generated:** 2026-10-31
|
|
4
|
-
**Commit:**
|
|
4
|
+
**Commit:** 26a741de
|
|
5
5
|
**Branch:** main
|
|
6
|
-
**Version:** 0.35.
|
|
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:**
|
|
4
|
+
**Commit:** 26a741de
|
|
5
5
|
**Branch:** main
|
|
6
|
-
**Version:** 0.35.
|
|
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:**
|
|
4
|
+
**Commit:** 26a741de
|
|
5
5
|
**Branch:** main
|
|
6
|
-
**Version:** 0.35.
|
|
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.
|
|
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:**
|
|
4
|
+
**Commit:** 26a741de
|
|
5
5
|
**Branch:** main
|
|
6
|
-
**Version:** 0.35.
|
|
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:**
|
|
4
|
+
**Commit:** 26a741de
|
|
5
5
|
**Branch:** main
|
|
6
|
-
**Version:** 0.35.
|
|
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:**
|
|
4
|
+
**Commit:** 26a741de
|
|
5
5
|
**Branch:** main
|
|
6
|
-
**Version:** 0.35.
|
|
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.
|
package/principles/AGENTS.md
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
# PRINCIPLES CHECKER MODULE
|
|
2
2
|
|
|
3
3
|
**Generated:** 2026-10-31
|
|
4
|
-
**Commit:**
|
|
4
|
+
**Commit:** 26a741de
|
|
5
5
|
**Branch:** main
|
|
6
|
-
**Version:** 0.35.
|
|
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
|
|
736
|
-
|
|
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
|
-
|
|
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 (!
|
|
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 (!
|
|
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 (!
|
|
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
|
-
//
|
|
1008
|
-
//
|
|
1009
|
-
//
|
|
1010
|
-
|
|
1011
|
-
const
|
|
1012
|
-
|
|
1013
|
-
|
|
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
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
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
|
-
|
|
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:**
|
|
4
|
+
**Commit:** 26a741de
|
|
5
5
|
**Branch:** main
|
|
6
|
-
**Version:** 0.35.
|
|
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:**
|
|
4
|
+
**Commit:** 26a741de
|
|
5
5
|
**Branch:** main
|
|
6
|
-
**Version:** 0.35.
|
|
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:**
|
|
4
|
+
**Commit:** 26a741de
|
|
5
5
|
**Branch:** main
|
|
6
|
-
**Version:** 0.35.
|
|
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.
|