@boyingliu01/xp-gate 0.31.3 → 0.32.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/adapters/java.sh +256 -4
- package/adapters/powershell.sh +45 -6
- package/bin/xp-gate.js +8 -2
- package/build-integrity/__tests__/gate-10.test.ts +88 -1
- package/build-integrity/gate-10.ts +51 -16
- package/dsh.bundle.patch.yml +15 -0
- package/gate-13-compiler-strictness.sh +1031 -0
- package/gate-3.sh +48 -13
- package/hooks/gate-13-compiler-strictness.sh +1031 -0
- package/hooks/gate-3.sh +48 -13
- package/hooks/lib/coverage-total.sh +18 -2
- package/hooks/lib/java-test-selection.sh +251 -0
- package/hooks/lib/jscpd-run.sh +9 -0
- package/hooks/lib/validate-code-walkthrough.cjs +186 -29
- package/hooks/pre-commit +269 -13
- package/lib/__tests__/detect-deps-dsh-profile.test.js +106 -0
- package/lib/__tests__/doctor-tui-skill-freshness.test.js +140 -0
- package/lib/__tests__/gate-runner.test.js +29 -2
- package/lib/__tests__/issue-586-principles-cmdline.test.js +35 -0
- package/lib/__tests__/issue-618-gate-not-run.test.js +100 -0
- package/lib/__tests__/shared-utils.test.js +21 -0
- package/lib/check.js +6 -2
- package/lib/detect-deps.js +42 -10
- package/lib/doctor-tui.js +111 -21
- package/lib/gate-runner.js +111 -37
- package/lib/mattpocock-license-verdict.json +1 -1
- package/lib/principles.js +48 -6
- package/lib/shared-utils.js +8 -0
- package/mock-policy/AGENTS.md +3 -3
- package/mock-policy/gate-m3.ts +57 -23
- package/mutation/AGENTS.md +3 -3
- package/mutation/__tests__/pitest-gradle-availability.test.ts +74 -0
- package/mutation/__tests__/test-filter-parity.test.ts +5 -2
- package/mutation/gate-m.ts +30 -24
- package/mutation/runners/mutmut-runner.ts +80 -30
- package/mutation/runners/pitest-runner.ts +60 -42
- package/package.json +14 -2
- package/plugins/claude-code/.claude-plugin/plugin.json +1 -1
- package/plugins/claude-code/skills/delphi-review/AGENTS.md +3 -3
- package/plugins/claude-code/skills/sprint-flow/AGENTS.md +3 -3
- package/plugins/claude-code/skills/test-specification-alignment/AGENTS.md +3 -3
- package/plugins/dsh/lib/command.d.ts +38 -0
- package/plugins/dsh/lib/command.d.ts.map +1 -0
- package/plugins/dsh/lib/command.js +87 -0
- package/plugins/dsh/lib/credentials.d.ts +39 -0
- package/plugins/dsh/lib/credentials.d.ts.map +1 -0
- package/plugins/dsh/lib/credentials.js +57 -0
- package/plugins/dsh/lib/delphi-config.d.ts +129 -0
- package/plugins/dsh/lib/delphi-config.d.ts.map +1 -0
- package/plugins/dsh/lib/delphi-config.js +369 -0
- package/plugins/dsh/lib/delphi-run.d.ts +24 -0
- package/plugins/dsh/lib/delphi-run.d.ts.map +1 -0
- package/plugins/dsh/lib/delphi-run.js +113 -0
- package/plugins/dsh/lib/delphi.d.ts +94 -0
- package/plugins/dsh/lib/delphi.d.ts.map +1 -0
- package/plugins/dsh/lib/delphi.js +207 -0
- package/plugins/dsh/lib/dialect.d.ts +24 -0
- package/plugins/dsh/lib/dialect.d.ts.map +1 -0
- package/plugins/dsh/lib/dialect.js +36 -0
- package/plugins/dsh/lib/evidence.d.ts +70 -0
- package/plugins/dsh/lib/evidence.d.ts.map +1 -0
- package/plugins/dsh/lib/evidence.js +92 -0
- package/plugins/dsh/lib/gate-runner.d.ts +49 -0
- package/plugins/dsh/lib/gate-runner.d.ts.map +1 -0
- package/plugins/dsh/lib/gate-runner.js +95 -0
- package/plugins/dsh/lib/git-context.d.ts +26 -0
- package/plugins/dsh/lib/git-context.d.ts.map +1 -0
- package/plugins/dsh/lib/git-context.js +30 -0
- package/plugins/dsh/lib/hooks.d.ts +42 -0
- package/plugins/dsh/lib/hooks.d.ts.map +1 -0
- package/plugins/dsh/lib/hooks.js +113 -0
- package/plugins/dsh/lib/index.d.ts +20 -0
- package/plugins/dsh/lib/index.d.ts.map +1 -0
- package/plugins/dsh/lib/index.js +430 -0
- package/plugins/dsh/lib/skill-provider.d.ts +15 -0
- package/plugins/dsh/lib/skill-provider.d.ts.map +1 -0
- package/plugins/dsh/lib/skill-provider.js +205 -0
- package/plugins/dsh/lib/sprint.d.ts +60 -0
- package/plugins/dsh/lib/sprint.d.ts.map +1 -0
- package/plugins/dsh/lib/sprint.js +92 -0
- package/plugins/dsh/package.json +14 -8
- package/plugins/dsh/scripts/verify-skill-provider.mjs +113 -0
- package/plugins/dsh/skills/delphi-review/AGENTS.md +3 -3
- package/plugins/dsh/skills/sprint-flow/AGENTS.md +3 -3
- package/plugins/dsh/skills/test-specification-alignment/AGENTS.md +3 -3
- package/plugins/dsh/src/command-single-form.test.ts +67 -0
- package/plugins/dsh/src/command.test.ts +21 -17
- package/plugins/dsh/src/command.ts +19 -21
- package/plugins/dsh/src/compat.test.ts +200 -0
- package/plugins/dsh/src/contract.test.ts +7 -1
- package/plugins/dsh/src/credentials.test.ts +93 -0
- package/plugins/dsh/src/credentials.ts +71 -0
- package/plugins/dsh/src/delphi-cancellation.test.ts +71 -0
- package/plugins/dsh/src/delphi-config-apikey-override.test.ts +94 -0
- package/plugins/dsh/src/delphi-config-credential-refs.test.ts +75 -0
- package/plugins/dsh/src/delphi-config.ts +146 -53
- package/plugins/dsh/src/delphi-run.ts +27 -4
- package/plugins/dsh/src/delphi.ts +133 -65
- package/plugins/dsh/src/evidence.test.ts +74 -0
- package/plugins/dsh/src/evidence.ts +141 -0
- package/plugins/dsh/src/gate-runner.test.ts +14 -6
- package/plugins/dsh/src/gate-runner.ts +68 -5
- package/plugins/dsh/src/git-context.test.ts +39 -0
- package/plugins/dsh/src/git-context.ts +38 -0
- package/plugins/dsh/src/hooks.test.ts +87 -0
- package/plugins/dsh/src/hooks.ts +126 -0
- package/plugins/dsh/src/index.test.ts +63 -6
- package/plugins/dsh/src/index.ts +340 -13
- package/plugins/dsh/src/skill-provider.test.ts +194 -0
- package/plugins/dsh/src/skill-provider.ts +231 -0
- package/plugins/dsh/src/sprint.test.ts +64 -0
- package/plugins/dsh/src/sprint.ts +122 -0
- package/plugins/dsh/src/tri-state.test.ts +123 -0
- package/plugins/opencode/index.ts +192 -58
- package/plugins/opencode/package.json +1 -1
- package/plugins/opencode/skills/delphi-review/AGENTS.md +3 -3
- package/plugins/opencode/skills/sprint-flow/AGENTS.md +3 -3
- package/plugins/opencode/skills/test-specification-alignment/AGENTS.md +3 -3
- package/plugins/qoder/plugin.json +1 -1
- package/plugins/qoder/skills/delphi-review/AGENTS.md +3 -3
- package/plugins/qoder/skills/sprint-flow/AGENTS.md +3 -3
- package/plugins/qoder/skills/test-specification-alignment/AGENTS.md +3 -3
- package/principles/AGENTS.md +3 -3
- package/principles/__tests__/analyze-read-failure.test.ts +70 -0
- package/principles/__tests__/baseline-entry-total-warnings.test.ts +76 -0
- package/principles/__tests__/baseline-prune.test.ts +61 -0
- package/principles/__tests__/cli-files-separators.test.ts +69 -0
- package/principles/__tests__/config-merge-warnings.test.ts +148 -0
- package/principles/adapters/__tests__/cpp-code-block.test.ts +123 -0
- package/principles/adapters/__tests__/cpp-scanner-equivalence.test.ts +67 -0
- package/principles/adapters/__tests__/typescript-block-comment-lines.test.ts +53 -0
- package/principles/adapters/__tests__/typescript-masking.test.ts +96 -0
- package/principles/adapters/cpp.ts +102 -50
- package/principles/adapters/java.ts +22 -3
- package/principles/adapters/typescript.ts +548 -178
- package/principles/analyzer.ts +36 -3
- package/principles/baseline-prune.ts +19 -7
- package/principles/baseline.ts +58 -70
- package/principles/config.ts +56 -35
- package/principles/index.ts +122 -40
- package/principles/rules/clean-code/deep-nesting.ts +36 -22
- package/principles/rules/clean-code/unused-imports.ts +30 -19
- package/principles/rules/solid/dip.ts +30 -20
- package/principles/rules/solid/lsp.ts +17 -2
- package/principles/rules/solid/ocp.ts +33 -23
- package/principles/rules/solid/srp.ts +65 -23
- package/scripts/delphi-external-review.cjs +25 -2
- package/scripts/sync-package-content.js +28 -1
- package/skills/delphi-review/AGENTS.md +3 -3
- package/skills/sprint-flow/AGENTS.md +3 -3
- package/skills/test-specification-alignment/AGENTS.md +3 -3
|
@@ -1,31 +1,42 @@
|
|
|
1
1
|
import { Rule, Violation, Severity } from '../../types';
|
|
2
2
|
import { getActiveConfig } from '../../config';
|
|
3
3
|
|
|
4
|
+
/** One import as the adapter reports it. */
|
|
5
|
+
interface ImportFacts {
|
|
6
|
+
name?: string;
|
|
7
|
+
line?: number;
|
|
8
|
+
used?: boolean;
|
|
9
|
+
type?: string;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* An import is unused when nothing references it. A `import type` is exempt: it
|
|
14
|
+
* is erased at compile time, so an unused type import is not dead code in the
|
|
15
|
+
* shipped output.
|
|
16
|
+
*/
|
|
17
|
+
function isUnusedValueImport(imp: ImportFacts): boolean {
|
|
18
|
+
return !imp.used && imp.type !== 'type';
|
|
19
|
+
}
|
|
20
|
+
|
|
4
21
|
export const unusedImportsRule: Rule = {
|
|
5
22
|
id: 'clean-code.unused-imports',
|
|
6
23
|
name: 'Unused Imports Rule',
|
|
7
24
|
threshold: 1,
|
|
8
25
|
severity: 'info',
|
|
9
26
|
check: (file: string, adapter: unknown): Violation[] => {
|
|
10
|
-
const violations: Violation[] = [];
|
|
11
27
|
// Read the ACTIVE config per invocation; a module-load snapshot was #457.
|
|
12
28
|
const settings = getActiveConfig().rules['clean-code']['unused-imports'];
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
const imports =
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
});
|
|
26
|
-
}
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
return violations;
|
|
29
|
+
const severity = (settings.severity as Severity) ?? 'info';
|
|
30
|
+
|
|
31
|
+
const imports = (adapter as { imports?: ImportFacts[] }).imports || [];
|
|
32
|
+
return imports
|
|
33
|
+
.filter(isUnusedValueImport)
|
|
34
|
+
.map((imp) => ({
|
|
35
|
+
file,
|
|
36
|
+
line: imp.line ?? 1,
|
|
37
|
+
ruleId: 'clean-code.unused-imports',
|
|
38
|
+
message: `Unused import "${imp.name}" - consider removing`,
|
|
39
|
+
severity,
|
|
40
|
+
}));
|
|
30
41
|
}
|
|
31
|
-
};
|
|
42
|
+
};
|
|
@@ -21,35 +21,45 @@ function shouldSkipInstantiation(className: string): boolean {
|
|
|
21
21
|
return false;
|
|
22
22
|
}
|
|
23
23
|
|
|
24
|
+
/** `new Foo(` -> `Foo`. */
|
|
25
|
+
function classNameFromNewMatch(match: string): string {
|
|
26
|
+
return match.replace(/new\s+/, '').replace(/\s*\(/, '');
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/** Violations for one class body: each `new X(` that is not exempt. */
|
|
30
|
+
function instantiationViolations(
|
|
31
|
+
code: string, line: number, file: string, severity: Severity,
|
|
32
|
+
): Violation[] {
|
|
33
|
+
const violations: Violation[] = [];
|
|
34
|
+
for (const match of code.match(/new\s+(\w+)\s*\(/g) || []) {
|
|
35
|
+
const className = classNameFromNewMatch(match);
|
|
36
|
+
if (shouldSkipInstantiation(className)) continue;
|
|
37
|
+
violations.push({
|
|
38
|
+
file,
|
|
39
|
+
line,
|
|
40
|
+
ruleId: 'solid.dip',
|
|
41
|
+
severity,
|
|
42
|
+
message: `Direct instantiation detected: new ${className}(). Prefer dependency injection for flexibility.`,
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
return violations;
|
|
46
|
+
}
|
|
47
|
+
|
|
24
48
|
export const dipRule: Rule = {
|
|
25
49
|
id: 'solid.dip',
|
|
26
50
|
name: 'Dependency Inversion Principle Rule',
|
|
27
51
|
threshold: 0,
|
|
28
52
|
severity: 'warning',
|
|
29
53
|
check: (file: string, adapter: import('../../types').Adapter): Violation[] => {
|
|
30
|
-
const violations: Violation[] = [];
|
|
31
54
|
const settings = getActiveConfig().rules['solid']['dip'];
|
|
32
55
|
const severity = (settings.severity as Severity) ?? 'warning';
|
|
33
56
|
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
const
|
|
37
|
-
if (!code) continue;
|
|
38
|
-
|
|
39
|
-
const newMatches = code.match(/new\s+(\w+)\s*\(/g) || [];
|
|
40
|
-
for (const match of newMatches) {
|
|
41
|
-
const className = match.replace(/new\s+/, '').replace(/\s*\(/, '');
|
|
42
|
-
if (shouldSkipInstantiation(className)) continue;
|
|
43
|
-
|
|
44
|
-
violations.push({
|
|
45
|
-
file,
|
|
46
|
-
line: cls.line || 0,
|
|
47
|
-
ruleId: 'solid.dip',
|
|
48
|
-
severity,
|
|
49
|
-
message: `Direct instantiation detected: new ${className}(). Prefer dependency injection for flexibility.`,
|
|
50
|
-
});
|
|
51
|
-
}
|
|
57
|
+
const violations: Violation[] = [];
|
|
58
|
+
for (const clsAny of adapter.extractClasses() || []) {
|
|
59
|
+
const cls = clsAny as { code?: string; line?: number };
|
|
60
|
+
if (!cls.code) continue;
|
|
61
|
+
violations.push(...instantiationViolations(cls.code, cls.line || 0, file, severity));
|
|
52
62
|
}
|
|
53
63
|
return violations;
|
|
54
64
|
}
|
|
55
|
-
};
|
|
65
|
+
};
|
|
@@ -170,13 +170,28 @@ function undetermined(): Violation[] {
|
|
|
170
170
|
* @param paramsText parameter list without surrounding parens
|
|
171
171
|
* @returns parameter fragments
|
|
172
172
|
*/
|
|
173
|
+
/** Delimiters that nest inside a parameter list (generics included). */
|
|
174
|
+
const PARAM_OPENERS = '<([{';
|
|
175
|
+
const PARAM_CLOSERS = '>)]}';
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* Depth delta contributed by one character.
|
|
179
|
+
*
|
|
180
|
+
* Kept separate from the splitter so the nesting rule is stated once instead of
|
|
181
|
+
* being folded into the loop's condition (#620).
|
|
182
|
+
*/
|
|
183
|
+
function paramDepthDelta(ch: string): number {
|
|
184
|
+
if (PARAM_OPENERS.includes(ch)) return 1;
|
|
185
|
+
if (PARAM_CLOSERS.includes(ch)) return -1;
|
|
186
|
+
return 0;
|
|
187
|
+
}
|
|
188
|
+
|
|
173
189
|
function splitParams(paramsText: string): string[] {
|
|
174
190
|
const parts: string[] = [];
|
|
175
191
|
let depth = 0;
|
|
176
192
|
let current = '';
|
|
177
193
|
for (const ch of paramsText) {
|
|
178
|
-
|
|
179
|
-
else if (ch === '>' || ch === ')' || ch === ']' || ch === '}') depth -= 1;
|
|
194
|
+
depth += paramDepthDelta(ch);
|
|
180
195
|
if (ch === ',' && depth <= 0) {
|
|
181
196
|
parts.push(current);
|
|
182
197
|
current = '';
|
|
@@ -1,39 +1,49 @@
|
|
|
1
1
|
import { Rule, Violation } from '../../types';
|
|
2
2
|
import { getActiveConfig } from '../../config';
|
|
3
3
|
|
|
4
|
+
/** The base class named in `extends X`, or null when the class extends nothing. */
|
|
5
|
+
function baseClassOf(code: string): string | null {
|
|
6
|
+
const match = code.match(/extends\s+(\w+)/);
|
|
7
|
+
return match ? match[1] : null;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* OCP is violated when a class, in the same body, both extends a base class and
|
|
12
|
+
* redeclares it — a local extension that had to reach back into the base.
|
|
13
|
+
*/
|
|
14
|
+
function ocpViolation(
|
|
15
|
+
code: string, name: string | undefined, line: number, file: string,
|
|
16
|
+
severity: 'error' | 'warning' | 'info',
|
|
17
|
+
): Violation | null {
|
|
18
|
+
const baseClass = baseClassOf(code);
|
|
19
|
+
if (baseClass === null) return null;
|
|
20
|
+
if (!code.includes(`class ${baseClass}`)) return null;
|
|
21
|
+
return {
|
|
22
|
+
file,
|
|
23
|
+
line,
|
|
24
|
+
ruleId: 'solid.ocp',
|
|
25
|
+
message: `Possible modification of base class "${baseClass}" while extending. Extension should not require modifying the base.`,
|
|
26
|
+
severity,
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
|
|
4
30
|
export const ocpRule: Rule = {
|
|
5
31
|
id: 'solid.ocp',
|
|
6
32
|
name: 'Open/Closed Principle Rule',
|
|
7
33
|
threshold: 0,
|
|
8
34
|
severity: 'info',
|
|
9
35
|
check: (file: string, adapter: import('../../types').Adapter): Violation[] => {
|
|
10
|
-
const violations: Violation[] = [];
|
|
11
36
|
// Read the ACTIVE config per invocation; a module-load snapshot was #457.
|
|
12
37
|
const settings = getActiveConfig().rules['solid']['ocp'];
|
|
13
|
-
const severity = (settings.severity as
|
|
14
|
-
|
|
15
|
-
const
|
|
16
|
-
|
|
17
|
-
for (const raw of classes) {
|
|
38
|
+
const severity = (settings.severity as 'error' | 'warning' | 'info') ?? 'info';
|
|
39
|
+
|
|
40
|
+
const violations: Violation[] = [];
|
|
41
|
+
for (const raw of adapter.extractClasses() || []) {
|
|
18
42
|
const cls = raw as { code?: string; line?: number; name?: string };
|
|
19
43
|
if (!cls.code) continue;
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
if (extendsMatch) {
|
|
23
|
-
const baseClass = extendsMatch[1];
|
|
24
|
-
|
|
25
|
-
if (cls.code.includes(`class ${baseClass}`)) {
|
|
26
|
-
violations.push({
|
|
27
|
-
file,
|
|
28
|
-
line: cls.line ?? 1,
|
|
29
|
-
ruleId: 'solid.ocp',
|
|
30
|
-
message: `Possible modification of base class "${baseClass}" while extending. Extension should not require modifying the base.`,
|
|
31
|
-
severity: severity
|
|
32
|
-
});
|
|
33
|
-
}
|
|
34
|
-
}
|
|
44
|
+
const found = ocpViolation(cls.code, cls.name, cls.line ?? 1, file, severity);
|
|
45
|
+
if (found) violations.push(found);
|
|
35
46
|
}
|
|
36
|
-
|
|
37
47
|
return violations;
|
|
38
48
|
}
|
|
39
|
-
};
|
|
49
|
+
};
|
|
@@ -1,41 +1,83 @@
|
|
|
1
1
|
import { Rule, Violation } from '../../types';
|
|
2
2
|
import { getActiveConfig } from '../../config';
|
|
3
3
|
|
|
4
|
+
/** The shape the adapter hands us for one class. */
|
|
5
|
+
interface RawClass {
|
|
6
|
+
code?: string;
|
|
7
|
+
line?: number;
|
|
8
|
+
name?: string;
|
|
9
|
+
methodCount?: number;
|
|
10
|
+
imports?: Record<string, unknown>;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/** A class, normalized for the checks below. */
|
|
14
|
+
interface ClassFacts {
|
|
15
|
+
name: string | undefined;
|
|
16
|
+
line: number;
|
|
17
|
+
methodCount: number;
|
|
18
|
+
importCategories: number;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/** Read the (optional) fields of a raw class once, with sane defaults. */
|
|
22
|
+
function readClass(raw: unknown): ClassFacts {
|
|
23
|
+
const cls = raw as RawClass;
|
|
24
|
+
return {
|
|
25
|
+
name: cls.name,
|
|
26
|
+
line: cls.line ?? 1,
|
|
27
|
+
methodCount: cls.methodCount || 0,
|
|
28
|
+
importCategories: cls.imports ? Object.keys(cls.imports).length : 0,
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** Too many methods in one class. */
|
|
33
|
+
function methodViolation(
|
|
34
|
+
facts: ClassFacts, file: string, maxMethods: number, severity: 'error' | 'warning' | 'info',
|
|
35
|
+
): Violation | null {
|
|
36
|
+
if (facts.methodCount <= maxMethods) return null;
|
|
37
|
+
return {
|
|
38
|
+
file,
|
|
39
|
+
line: facts.line,
|
|
40
|
+
ruleId: 'solid.srp',
|
|
41
|
+
severity,
|
|
42
|
+
message: `Class "${facts.name}" has too many methods: ${facts.methodCount} (maximum: ${maxMethods}). Consider splitting into focused classes.`,
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/** Imports reaching across too many domains. */
|
|
47
|
+
function importViolation(
|
|
48
|
+
facts: ClassFacts, file: string, severity: 'error' | 'warning' | 'info',
|
|
49
|
+
): Violation | null {
|
|
50
|
+
if (facts.importCategories <= 3) return null;
|
|
51
|
+
return {
|
|
52
|
+
file,
|
|
53
|
+
line: facts.line,
|
|
54
|
+
ruleId: 'solid.srp',
|
|
55
|
+
severity,
|
|
56
|
+
message: `Class "${facts.name}" imports from ${facts.importCategories} different domains. Each class should focus on one responsibility.`,
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
|
|
4
60
|
export const srpRule: Rule = {
|
|
5
61
|
id: 'solid.srp',
|
|
6
62
|
name: 'Single Responsibility Principle Rule',
|
|
7
63
|
threshold: 15,
|
|
8
64
|
severity: 'warning',
|
|
9
65
|
check: (file: string, adapter: import('../../types').Adapter): Violation[] => {
|
|
10
|
-
const violations: Violation[] = [];
|
|
11
66
|
// Read the ACTIVE config per invocation; a module-load snapshot was #457.
|
|
12
67
|
const settings = getActiveConfig().rules['solid']['srp'];
|
|
13
|
-
|
|
14
|
-
const classes = adapter.extractClasses() || [];
|
|
15
68
|
const maxMethods = settings.methodThreshold as number;
|
|
16
69
|
const sev = (settings.severity as 'error' | 'warning' | 'info') ?? 'warning';
|
|
17
70
|
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
const
|
|
21
|
-
const
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
violations.push(
|
|
26
|
-
file, line, ruleId: 'solid.srp', severity: sev,
|
|
27
|
-
message: `Class "${cls.name}" has too many methods: ${methodCount} (maximum: ${maxMethods}). Consider splitting into focused classes.`,
|
|
28
|
-
});
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
if (importCategories > 3) {
|
|
32
|
-
violations.push({
|
|
33
|
-
file, line, ruleId: 'solid.srp', severity: sev,
|
|
34
|
-
message: `Class "${cls.name}" imports from ${importCategories} different domains. Each class should focus on one responsibility.`,
|
|
35
|
-
});
|
|
71
|
+
const violations: Violation[] = [];
|
|
72
|
+
for (const raw of adapter.extractClasses() || []) {
|
|
73
|
+
const facts = readClass(raw);
|
|
74
|
+
for (const found of [
|
|
75
|
+
methodViolation(facts, file, maxMethods, sev),
|
|
76
|
+
importViolation(facts, file, sev),
|
|
77
|
+
]) {
|
|
78
|
+
if (found) violations.push(found);
|
|
36
79
|
}
|
|
37
80
|
}
|
|
38
|
-
|
|
39
81
|
return violations;
|
|
40
82
|
},
|
|
41
|
-
};
|
|
83
|
+
};
|
|
@@ -152,8 +152,8 @@ function readConfig(configPath, profileOverride) {
|
|
|
152
152
|
// pins ~/.config/xp-gate — overriding only this loader would make the two
|
|
153
153
|
// disagree. On Linux, $HOME/.config IS the XDG default, so behaviour there
|
|
154
154
|
// is identical; the env var only matters for non-default setups.
|
|
155
|
-
|
|
156
|
-
const globalPath =
|
|
155
|
+
// Extracted so all three resolution points share one definition (REQ-636).
|
|
156
|
+
const globalPath = globalConfigPath(process.env);
|
|
157
157
|
if (fs.existsSync(globalPath)) {
|
|
158
158
|
console.error(`[delphi-review] ${configPath} not found here — using global config ${globalPath}`);
|
|
159
159
|
configPath = globalPath;
|
|
@@ -958,6 +958,7 @@ if (require.main !== module) {
|
|
|
958
958
|
module.exports = {
|
|
959
959
|
parseArgs,
|
|
960
960
|
readConfig,
|
|
961
|
+
globalConfigPath,
|
|
961
962
|
validateDistinctModels,
|
|
962
963
|
extractJsonFromResponse,
|
|
963
964
|
buildSystemPrompt,
|
|
@@ -985,3 +986,25 @@ if (require.main !== module) {
|
|
|
985
986
|
process.exit(1);
|
|
986
987
|
});
|
|
987
988
|
}
|
|
989
|
+
|
|
990
|
+
/**
|
|
991
|
+
* Machine-wide `.delphi-config.json` path: `$XDG_CONFIG_HOME/xp-gate/`, or
|
|
992
|
+
* `~/.config/xp-gate/` when the variable is unset.
|
|
993
|
+
*
|
|
994
|
+
* Exported so the path-parity test can hold this loader, the Gate MW validator
|
|
995
|
+
* and the DSH plugin to one definition instead of three restatements.
|
|
996
|
+
*
|
|
997
|
+
* Deliberately declared at the END of the file. Function declarations hoist, so
|
|
998
|
+
* readConfig above can call it, but placing it here keeps every line before the
|
|
999
|
+
* old EOF byte-identical — which matters because `.ccn-baseline.json` anchors
|
|
1000
|
+
* anonymous functions by line number (`file::(anonymous)@657`). Adding these
|
|
1001
|
+
* lines near the top shifted that anchor to 676 and Gate 3 then read a
|
|
1002
|
+
* pre-existing CCN=24 function as a brand-new violation.
|
|
1003
|
+
*
|
|
1004
|
+
* @param {NodeJS.ProcessEnv} env
|
|
1005
|
+
* @returns {string}
|
|
1006
|
+
*/
|
|
1007
|
+
function globalConfigPath(env) {
|
|
1008
|
+
const configHome = env.XDG_CONFIG_HOME || path.join(os.homedir(), '.config');
|
|
1009
|
+
return path.join(configHome, 'xp-gate', '.delphi-config.json');
|
|
1010
|
+
}
|
|
@@ -41,7 +41,11 @@ function rmrf(target) {
|
|
|
41
41
|
// package — but `find -type f` in scripts/check-hook-mirror.sh saw it in the mirror and
|
|
42
42
|
// reported "hook mirror file set drifted", which blocks the push with no fix the operator
|
|
43
43
|
// can apply from their own code (#587/#588).
|
|
44
|
-
|
|
44
|
+
// `__tests__` is here for the same reason as `dist` and `coverage`: it is not
|
|
45
|
+
// distribution content. The package declares no `files` allowlist, so anything
|
|
46
|
+
// copied reaches users -- and plugins/opencode/__tests__/ would have shipped a
|
|
47
|
+
// vitest suite to consumers who have no vitest installed (#625/#626).
|
|
48
|
+
const SKIP_DIRS = new Set(['node_modules', '.git', 'dist', 'coverage', '.opencode', '__pycache__', '__tests__']);
|
|
45
49
|
const SKIP_FILES = new Set(['package-lock.json']);
|
|
46
50
|
const SKIP_FILE_SUFFIXES = ['.lock', '.js.map'];
|
|
47
51
|
|
|
@@ -121,9 +125,32 @@ function syncPlugins() {
|
|
|
121
125
|
console.error(`[sync] plugins/${name}`);
|
|
122
126
|
}
|
|
123
127
|
}
|
|
128
|
+
assertDshBundleEntry();
|
|
124
129
|
return copied;
|
|
125
130
|
}
|
|
126
131
|
|
|
132
|
+
/**
|
|
133
|
+
* Fail loudly if the dsh plugin's compiled entry is missing.
|
|
134
|
+
*
|
|
135
|
+
* The npm main package declares `exports["./dsh"]` -> plugins/dsh/lib/index.js
|
|
136
|
+
* so that `dsh plugin add @boyingliu01/xp-gate` can mount the tool directly.
|
|
137
|
+
* `lib/` is a gitignored tsc artifact, and this sync only COPIES what already
|
|
138
|
+
* exists — it does not build. Publishing without a prior build therefore
|
|
139
|
+
* produces a tarball whose exports target does not exist, which fails at plugin
|
|
140
|
+
* load time on the user's machine and is invisible to every unit test here.
|
|
141
|
+
* Checking it at pack time turns that into an immediate, named error.
|
|
142
|
+
*/
|
|
143
|
+
function assertDshBundleEntry() {
|
|
144
|
+
const entry = path.join(PKG_ROOT, 'plugins', 'dsh', 'lib', 'index.js');
|
|
145
|
+
if (fs.existsSync(entry)) return;
|
|
146
|
+
console.error(
|
|
147
|
+
'[sync] ERROR: plugins/dsh/lib/index.js is missing. The npm main package ' +
|
|
148
|
+
'exports "./dsh" from that path, so the published bundle would not load. ' +
|
|
149
|
+
'Build it first: (cd plugins/dsh && npm run build)',
|
|
150
|
+
);
|
|
151
|
+
process.exit(1);
|
|
152
|
+
}
|
|
153
|
+
|
|
127
154
|
function syncScripts() {
|
|
128
155
|
const srcRoot = path.join(REPO_ROOT, 'scripts');
|
|
129
156
|
const destRoot = path.join(PKG_ROOT, 'scripts');
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
# SKILLS/DELPHI-REVIEW KNOWLEDGE BASE
|
|
2
2
|
|
|
3
|
-
**Generated:** 2026-10-
|
|
4
|
-
**Commit:**
|
|
3
|
+
**Generated:** 2026-10-28
|
|
4
|
+
**Commit:** 0f58c300
|
|
5
5
|
**Branch:** main
|
|
6
|
-
**Version:** 0.
|
|
6
|
+
**Version:** 0.32.0.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
|
-
**Generated:** 2026-10-
|
|
4
|
-
**Commit:**
|
|
3
|
+
**Generated:** 2026-10-28
|
|
4
|
+
**Commit:** 0f58c300
|
|
5
5
|
**Branch:** main
|
|
6
|
-
**Version:** 0.
|
|
6
|
+
**Version:** 0.32.0.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
|
-
**Generated:** 2026-10-
|
|
4
|
-
**Commit:**
|
|
3
|
+
**Generated:** 2026-10-28
|
|
4
|
+
**Commit:** 0f58c300
|
|
5
5
|
**Branch:** main
|
|
6
|
-
**Version:** 0.
|
|
6
|
+
**Version:** 0.32.0.0
|
|
7
7
|
|
|
8
8
|
## OVERVIEW
|
|
9
9
|
Test-Specification Alignment Engine — two-stage validation ensuring tests accurately reflect requirements and design specs.
|