@boyingliu01/xp-gate 0.19.2 → 0.20.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/adapters/gate-4.sh +83 -18
- package/adapters/gate-8.sh +49 -5
- package/adapters/iac.sh +1 -1
- package/adapters/typescript.sh +15 -60
- package/bin/xp-gate.js +15 -16
- package/gate-10.sh +35 -2
- package/gate-4.sh +110 -21
- package/gate-8.sh +49 -5
- package/hooks/gate-10.sh +35 -2
- package/hooks/gate-4.sh +110 -21
- package/hooks/gate-8.sh +49 -5
- package/hooks/lib/jscpd-run.sh +63 -0
- package/hooks/lib/sprint-gate-report.sh +58 -0
- package/hooks/lib/test-failure.sh +152 -0
- package/hooks/lib/typecheck.sh +141 -0
- package/hooks/lib/validate-code-walkthrough.cjs +43 -5
- package/hooks/post-merge +100 -0
- package/hooks/pre-commit +401 -155
- package/hooks/pre-push +75 -19
- package/lib/__tests__/bootstrap.test.js +23 -41
- package/lib/__tests__/check-version.test.js +8 -1
- package/lib/__tests__/detect-deps.test.js +16 -0
- package/lib/__tests__/doctor-drift-round2.test.js +212 -0
- package/lib/__tests__/doctor-executed-surface.test.js +209 -0
- package/lib/__tests__/doctor-hook-drift.test.js +329 -0
- package/lib/__tests__/doctor-machine-mutation.test.js +116 -0
- package/lib/__tests__/doctor-module-drift.test.js +206 -0
- package/lib/__tests__/doctor-module-resolution-parity.test.js +314 -0
- package/lib/__tests__/doctor-path-equivalence.test.js +59 -0
- package/lib/__tests__/doctor-repair-boundary.test.js +52 -0
- package/lib/__tests__/doctor-tui-skill-freshness.test.js +105 -0
- package/lib/__tests__/doctor.test.js +155 -0
- package/lib/__tests__/gate-runner.test.js +19 -3
- package/lib/__tests__/helpers/lib-sandbox.cjs +55 -0
- package/lib/__tests__/init.test.js +44 -17
- package/lib/__tests__/install-skill-full-dir.test.js +151 -0
- package/lib/__tests__/install-skill.test.js +51 -132
- package/lib/__tests__/language-tools-detect.test.js +158 -0
- package/lib/__tests__/phase-transition.test.js +15 -1
- package/lib/__tests__/qoder-delphi-agents.test.js +285 -0
- package/lib/__tests__/shared-paths.test.js +20 -0
- package/lib/__tests__/shared-utils.test.js +69 -0
- package/lib/__tests__/skill-flags.test.js +48 -0
- package/lib/__tests__/ui-detector.test.ts +6 -2
- package/lib/__tests__/uninstall.test.js +20 -0
- package/lib/__tests__/update-hooks.test.js +12 -0
- package/lib/__tests__/update-skill.test.js +7 -4
- package/lib/detect-deps.js +3 -20
- package/lib/doctor-tui.js +22 -5
- package/lib/doctor.js +1178 -31
- package/lib/init.js +184 -33
- package/lib/install-cmd.js +7 -4
- package/lib/install-skill.js +38 -95
- package/lib/language-tools.js +83 -4
- package/lib/mattpocock-license-verdict.json +1 -1
- package/lib/shared-paths.js +5 -2
- package/lib/shared-utils.js +47 -3
- package/lib/uninstall.js +12 -0
- package/lib/update-hooks.js +4 -4
- package/lib/update-skill.js +6 -7
- package/mock-policy/AGENTS.md +4 -4
- package/mutation/AGENTS.md +4 -4
- package/mutation/__tests__/gate-m.test.ts +66 -14
- package/mutation/__tests__/mutmut-runner.test.ts +91 -42
- package/mutation/__tests__/test-filter-parity.test.ts +37 -0
- package/mutation/gate-m.ts +21 -8
- package/package.json +1 -1
- package/plugins/claude-code/.claude-plugin/plugin.json +2 -2
- package/plugins/claude-code/skills/clipboard-vision/__tests__/clipboard-vision-distribution.test.cjs +0 -1
- package/plugins/claude-code/skills/delphi-review/AGENTS.md +4 -4
- package/plugins/claude-code/skills/delphi-review/SKILL.md +98 -14
- package/plugins/claude-code/skills/delphi-review/references/code-walkthrough.md +36 -3
- package/plugins/claude-code/skills/delphi-review/references/requirements.md +6 -6
- package/plugins/claude-code/skills/sprint-flow/AGENTS.md +4 -4
- package/plugins/claude-code/skills/sprint-flow/references/phase-2-design.md +3 -3
- package/plugins/claude-code/skills/test-specification-alignment/AGENTS.md +4 -4
- package/plugins/dsh/README.md +169 -4
- package/plugins/dsh/dsh-overlay.patch.yml +67 -0
- package/plugins/dsh/package.json +1 -1
- package/plugins/dsh/scripts/prepack.cjs +24 -19
- package/plugins/dsh/setup-dsh.ps1 +160 -0
- package/plugins/dsh/skills/clipboard-vision/__tests__/clipboard-vision-distribution.test.cjs +0 -1
- package/plugins/dsh/skills/delphi-review/AGENTS.md +4 -4
- package/plugins/dsh/skills/delphi-review/SKILL.md +98 -14
- package/plugins/dsh/skills/delphi-review/references/code-walkthrough.md +36 -3
- package/plugins/dsh/skills/delphi-review/references/requirements.md +6 -6
- package/plugins/dsh/skills/sprint-flow/AGENTS.md +4 -4
- package/plugins/dsh/skills/sprint-flow/references/phase-2-design.md +3 -3
- package/plugins/dsh/skills/test-specification-alignment/AGENTS.md +4 -4
- package/plugins/dsh/src/command.test.ts +66 -2
- package/plugins/dsh/src/command.ts +36 -14
- package/plugins/dsh/src/contract.test.ts +31 -1
- package/plugins/dsh/src/delphi-config.test.ts +190 -0
- package/plugins/dsh/src/delphi-config.ts +223 -0
- package/plugins/dsh/src/delphi-run.test.ts +181 -0
- package/plugins/dsh/src/delphi-run.ts +129 -0
- package/plugins/dsh/src/delphi.test.ts +222 -0
- package/plugins/dsh/src/delphi.ts +247 -0
- package/plugins/dsh/src/dialect.test.ts +59 -0
- package/plugins/dsh/src/dialect.ts +40 -0
- package/plugins/dsh/src/index.ts +70 -4
- package/plugins/dsh/src/skills.test.ts +26 -19
- package/plugins/opencode/package.json +1 -1
- package/plugins/opencode/skills/clipboard-vision/__tests__/clipboard-vision-distribution.test.cjs +0 -1
- package/plugins/opencode/skills/delphi-review/AGENTS.md +4 -4
- package/plugins/opencode/skills/delphi-review/SKILL.md +98 -14
- package/plugins/opencode/skills/delphi-review/references/code-walkthrough.md +36 -3
- package/plugins/opencode/skills/delphi-review/references/requirements.md +6 -6
- package/plugins/opencode/skills/sprint-flow/AGENTS.md +4 -4
- package/plugins/opencode/skills/sprint-flow/references/phase-2-design.md +3 -3
- package/plugins/opencode/skills/test-specification-alignment/AGENTS.md +4 -4
- package/plugins/qoder/agents/delphi-architecture.md +1 -1
- package/plugins/qoder/agents/delphi-feasibility.md +1 -1
- package/plugins/qoder/agents/delphi-technical.md +1 -1
- package/plugins/qoder/plugin.json +2 -2
- package/plugins/qoder/skills/admin-template-guidelines/SKILL.md +6 -797
- package/plugins/qoder/skills/admin-template-guidelines/references/rule-1.md +91 -0
- package/plugins/qoder/skills/admin-template-guidelines/references/rule-2.md +155 -0
- package/plugins/qoder/skills/admin-template-guidelines/references/rule-3.md +70 -0
- package/plugins/qoder/skills/admin-template-guidelines/references/rule-4.md +154 -0
- package/plugins/qoder/skills/admin-template-guidelines/references/rule-5.md +126 -0
- package/plugins/qoder/skills/admin-template-guidelines/references/rule-6.md +180 -0
- package/plugins/qoder/skills/batch-grill-me/SKILL.md +15 -0
- package/plugins/qoder/skills/clipboard-vision/__tests__/clipboard-vision-distribution.test.cjs +0 -1
- package/plugins/qoder/skills/clipboard-vision/describe-clipboard.ps1 +115 -0
- package/plugins/qoder/skills/clipboard-vision/describe-clipboard.sh +161 -0
- package/plugins/qoder/skills/delphi-review/.delphi-config.json.example +61 -28
- package/plugins/qoder/skills/delphi-review/AGENTS.md +4 -4
- package/plugins/qoder/skills/delphi-review/INSTALL.md +97 -98
- package/plugins/qoder/skills/delphi-review/SKILL.md +170 -63
- package/plugins/qoder/skills/delphi-review/hooks.md +19 -0
- package/plugins/qoder/skills/delphi-review/opencode.json.delphi.example +6 -10
- package/plugins/qoder/skills/delphi-review/references/code-walkthrough.md +36 -3
- package/plugins/qoder/skills/delphi-review/references/orchestrator-dispatch.md +64 -0
- package/plugins/qoder/skills/delphi-review/references/requirements.md +402 -0
- package/plugins/qoder/skills/delphi-review/references/round-templates.md +134 -0
- package/plugins/qoder/skills/delphi-review/scripts/verify-consensus.sh +43 -0
- package/plugins/qoder/skills/delphi-review/tools.md +29 -0
- package/plugins/qoder/skills/domain-modeling/ADR-FORMAT.md +64 -0
- package/plugins/qoder/skills/domain-modeling/CONTEXT-FORMAT.md +60 -0
- package/plugins/qoder/skills/domain-modeling/SKILL.md +74 -0
- package/plugins/qoder/skills/grill-with-docs/SKILL.md +7 -0
- package/plugins/qoder/skills/grilling/SKILL.md +12 -0
- package/plugins/qoder/skills/ralph-loop/SKILL.md +87 -428
- package/plugins/qoder/skills/ralph-loop/references/components.md +113 -0
- package/plugins/qoder/skills/ralph-loop/references/workflow-details.md +99 -0
- package/plugins/qoder/skills/sprint-flow/AGENTS.md +5 -5
- package/plugins/qoder/skills/sprint-flow/SKILL.md +255 -454
- package/plugins/qoder/skills/sprint-flow/__tests__/sprint-flow.test.cjs +457 -0
- package/plugins/qoder/skills/sprint-flow/hooks.md +18 -0
- package/plugins/qoder/skills/sprint-flow/references/components/memory.md +1 -1
- package/plugins/qoder/skills/sprint-flow/references/components/middleware.md +1 -1
- package/plugins/qoder/skills/sprint-flow/references/components/skill-invocations.md +21 -20
- package/plugins/qoder/skills/sprint-flow/references/components/tool-descriptions.md +22 -23
- package/plugins/qoder/skills/sprint-flow/references/force-levels.md +0 -1
- package/plugins/qoder/skills/sprint-flow/references/orchestration-rules.md +34 -27
- package/plugins/qoder/skills/sprint-flow/references/phase-1-prep.md +1 -1
- package/plugins/qoder/skills/sprint-flow/references/phase-2-design.md +160 -123
- package/plugins/qoder/skills/sprint-flow/references/phase-3-build.md +203 -2
- package/plugins/qoder/skills/sprint-flow/references/phase-4-verify.md +107 -34
- package/plugins/qoder/skills/sprint-flow/references/phase-5-ship.md +194 -12
- package/plugins/qoder/skills/sprint-flow/references/phase-6-close.md +139 -7
- package/plugins/qoder/skills/sprint-flow/references/phase-overview.md +486 -0
- package/plugins/qoder/skills/sprint-flow/scripts/verify-sprint-state.sh +38 -0
- package/plugins/qoder/skills/sprint-flow/tools.md +29 -0
- package/plugins/qoder/skills/test-specification-alignment/AGENTS.md +4 -4
- package/plugins/qoder/skills/test-specification-alignment/SKILL.md +104 -601
- package/plugins/qoder/skills/to-issues/SKILL.md +1 -1
- package/principles/AGENTS.md +9 -8
- package/principles/__tests__/adapter-metrics.test.ts +292 -0
- package/principles/__tests__/analyzer.test.ts +29 -6
- package/principles/__tests__/baseline-prune-timeout.test.ts +71 -0
- package/principles/__tests__/baseline-prune.test.ts +338 -0
- package/principles/__tests__/boy-scout-init-baseline.test.ts +167 -0
- package/principles/__tests__/boy-scout-integration.test.ts +97 -6
- package/principles/__tests__/boy-scout.test.ts +253 -7
- package/principles/__tests__/config-enforcement.test.ts +469 -0
- package/principles/__tests__/config-git-timeout.test.ts +66 -0
- package/principles/__tests__/direct-execution.test.ts +187 -0
- package/principles/__tests__/index.test.ts +16 -3
- package/principles/__tests__/warnings-baseline-budgets.test.ts +74 -0
- package/principles/adapters/typescript.ts +613 -13
- package/principles/analyzer.ts +22 -0
- package/principles/baseline-prune.ts +217 -0
- package/principles/boy-scout.ts +481 -51
- package/principles/config.ts +362 -54
- package/principles/direct-execution.ts +60 -0
- package/principles/index.ts +40 -17
- package/principles/rules/__tests__/clean-code/many-exports.test.ts +16 -10
- package/principles/rules/clean-code/code-duplication.ts +8 -8
- package/principles/rules/clean-code/deep-nesting.ts +8 -8
- package/principles/rules/clean-code/god-class.ts +8 -8
- package/principles/rules/clean-code/large-file.ts +15 -11
- package/principles/rules/clean-code/long-function.ts +8 -8
- package/principles/rules/clean-code/magic-numbers.ts +10 -7
- package/principles/rules/clean-code/many-exports.ts +7 -7
- package/principles/rules/clean-code/missing-error-handling.ts +5 -5
- package/principles/rules/clean-code/too-many-params.ts +8 -8
- package/principles/rules/clean-code/unused-imports.ts +5 -5
- package/principles/rules/solid/dip.ts +18 -8
- package/principles/rules/solid/isp.ts +9 -8
- package/principles/rules/solid/lsp.ts +6 -6
- package/principles/rules/solid/ocp.ts +6 -5
- package/principles/rules/solid/srp.ts +7 -8
- package/principles/types.ts +15 -0
- package/scripts/__tests__/sync-package-content.test.js +28 -0
- package/scripts/delphi-external-review.cjs +193 -10
- package/scripts/sync-package-content.js +7 -4
- package/skills/clipboard-vision/__tests__/clipboard-vision-distribution.test.cjs +0 -1
- package/skills/delphi-review/AGENTS.md +4 -4
- package/skills/delphi-review/SKILL.md +98 -14
- package/skills/delphi-review/references/code-walkthrough.md +36 -3
- package/skills/delphi-review/references/requirements.md +6 -6
- package/skills/sprint-flow/AGENTS.md +4 -4
- package/skills/sprint-flow/references/phase-2-design.md +3 -3
- package/skills/test-specification-alignment/AGENTS.md +4 -4
- package/lib/__tests__/download-skill.test.js +0 -281
- package/lib/download-skill.js +0 -96
- package/plugins/qoder/skills/ralph-loop/references/components/memory.md +0 -55
- package/plugins/qoder/skills/ralph-loop/references/components/middleware.md +0 -54
- package/plugins/qoder/skills/ralph-loop/references/components/skill-invocations.md +0 -39
- package/plugins/qoder/skills/ralph-loop/references/components/system-prompt.md +0 -24
- package/plugins/qoder/skills/ralph-loop/references/components/tool-descriptions.md +0 -32
|
@@ -242,7 +242,7 @@ Phase 1: PLAN → autoplan → delphi-review → specification.yaml
|
|
|
242
242
|
→ **/to-issues** → slices-manifest.json + Issue 列表
|
|
243
243
|
Phase 2: BUILD → 按 execution_order 逐个切片执行
|
|
244
244
|
→ 每个切片 = 一个 REQ(ralph-loop 模式)
|
|
245
|
-
→
|
|
245
|
+
→ 或通过原生 Task 并行派发(无依赖的 AFK 切片)
|
|
246
246
|
Phase 3: REVIEW → **整体 review**(全部切片完成后统一 code-walkthrough)
|
|
247
247
|
→ browse / QA 验证端到端行为
|
|
248
248
|
Phase 4: USER ACC → 人工验收(全部切片集成后的完整功能)
|
package/principles/AGENTS.md
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
# PRINCIPLES CHECKER MODULE
|
|
2
2
|
|
|
3
|
-
**Generated:** 2026-
|
|
4
|
-
**Commit:**
|
|
5
|
-
**Branch:**
|
|
6
|
-
**Version:** 0.
|
|
3
|
+
**Generated:** 2026-10-07
|
|
4
|
+
**Commit:** ff55f35b
|
|
5
|
+
**Branch:** fix/batch-defects-20261007
|
|
6
|
+
**Version:** 0.20.0.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.
|
|
@@ -52,7 +52,8 @@ src/principles/
|
|
|
52
52
|
| Output | reporter.ts | Emits Console, JSON, or SARIF 2.1.0 |
|
|
53
53
|
| Thresholds | config.ts + .principlesrc | Defaults + project overrides |
|
|
54
54
|
| Boy Scout enforcement | boy-scout.ts | classifyFiles → calculateDelta → enforceBoyScoutRule |
|
|
55
|
-
| Baseline lifecycle |
|
|
55
|
+
| Baseline lifecycle | boy-scout.ts | loadBaseline, saveBaseline, initBaseline |
|
|
56
|
+
| Baseline storage | baseline.ts | BaselineStorage (size/timeout/batch guards) + filterBaselineWarnings |
|
|
56
57
|
| Adapter for new language | adapters/base.ts + add `<lang>.ts` | Subclass base; export via index |
|
|
57
58
|
| C / C++ extraction | adapters/cpp.ts | Regex-based; handles both `.cpp` and `.c` |
|
|
58
59
|
| Tests | __tests__/ | Adapter, rule, and core tests |
|
|
@@ -67,9 +68,9 @@ src/principles/
|
|
|
67
68
|
| classifyFiles | Function | boy-scout.ts | new / modified / untouched bucket assignment |
|
|
68
69
|
| calculateDelta | Function | boy-scout.ts | Diff vs `.warnings-baseline.json` |
|
|
69
70
|
| enforceBoyScoutRule | Function | boy-scout.ts | Block when modified-file warnings increase |
|
|
70
|
-
| loadBaseline | Function |
|
|
71
|
-
| saveBaseline | Function |
|
|
72
|
-
| initBaseline | Function |
|
|
71
|
+
| loadBaseline | Function | boy-scout.ts | Read `.warnings-baseline.json` |
|
|
72
|
+
| saveBaseline | Function | boy-scout.ts | Write updated baseline after successful commit |
|
|
73
|
+
| initBaseline | Function | boy-scout.ts | First-time auto-snapshot (≤5 warnings ⇒ must clear to zero) |
|
|
73
74
|
|
|
74
75
|
## CONVENTIONS
|
|
75
76
|
- **Rule ID format**: `clean-code.long-function`, `solid.srp`. Stable, dot-namespaced.
|
|
@@ -0,0 +1,292 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @test REQ-DSH-015
|
|
3
|
+
* @intent 验证 TypeScriptAdapter 为各规则提供其所需度量字段:fix #446 之前,
|
|
4
|
+
* createCodeMatch() 只返回 {name,type,line,code},而规则读取
|
|
5
|
+
* length / paramCount / nestingDepth / methodCount / hasTryCatch,
|
|
6
|
+
* 导致 14 条规则里 12 条在 TypeScript 上结构性无法触发(Gate 4 形同虚设)
|
|
7
|
+
* @covers AC-DSH-015-01
|
|
8
|
+
*
|
|
9
|
+
* Mock-first per src/principles/AGENTS.md: inline fixture, no separate files.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import { describe, it, expect, vi, type Mock } from 'vitest';
|
|
13
|
+
import { TypeScriptAdapter } from '../adapters/typescript';
|
|
14
|
+
|
|
15
|
+
vi.mock('fs', () => ({
|
|
16
|
+
readFileSync: vi.fn(),
|
|
17
|
+
existsSync: vi.fn(() => true),
|
|
18
|
+
}));
|
|
19
|
+
|
|
20
|
+
import { readFileSync } from 'fs';
|
|
21
|
+
|
|
22
|
+
const LONG_BODY = Array.from({ length: 60 }, (_, i) => ` const v${i} = ${i};`).join('\n');
|
|
23
|
+
|
|
24
|
+
const SAMPLE = `
|
|
25
|
+
import { helper } from './helper';
|
|
26
|
+
|
|
27
|
+
export function eightParams(a: number, b: number, c: number, d: number, e: number, f: number, g: number, h: number) {
|
|
28
|
+
return a + b + c + d + e + f + g + h;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function longOne() {
|
|
32
|
+
${LONG_BODY}
|
|
33
|
+
return 0;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function deeplyNested(a: number) {
|
|
37
|
+
if (a > 0) {
|
|
38
|
+
if (a > 1) {
|
|
39
|
+
if (a > 2) {
|
|
40
|
+
if (a > 3) {
|
|
41
|
+
if (a > 4) {
|
|
42
|
+
return 'deep';
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
return 'shallow';
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export function rawIo() {
|
|
52
|
+
return readFileSync('/tmp/x', 'utf-8');
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export function guardedIo() {
|
|
56
|
+
try {
|
|
57
|
+
return readFileSync('/tmp/x', 'utf-8');
|
|
58
|
+
} catch (e) {
|
|
59
|
+
return '';
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export const arrowFn = (x: number, y: number) => x + y;
|
|
64
|
+
|
|
65
|
+
export class MegaService {
|
|
66
|
+
aMethod() { return 1; }
|
|
67
|
+
m1() { return 1; } m2() { return 1; } m3() { return 1; } m4() { return 1; }
|
|
68
|
+
m5() { return 1; } m6() { return 1; } m7() { return 1; } m8() { return 1; }
|
|
69
|
+
m9() { return 1; } m10() { return 1; } m11() { return 1; } m12() { return 1; }
|
|
70
|
+
m13() { return 1; } m14() { return 1; } m15() { return 1; }
|
|
71
|
+
}
|
|
72
|
+
`;
|
|
73
|
+
|
|
74
|
+
interface FnInfo {
|
|
75
|
+
name: string;
|
|
76
|
+
startLine?: number;
|
|
77
|
+
line?: number;
|
|
78
|
+
length?: number;
|
|
79
|
+
paramCount?: number;
|
|
80
|
+
nestingDepth?: number;
|
|
81
|
+
hasTryCatch?: boolean;
|
|
82
|
+
ioOperations?: string[];
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
interface ClsInfo {
|
|
86
|
+
name: string;
|
|
87
|
+
methodCount?: number;
|
|
88
|
+
line?: number;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function adapterFor(content = SAMPLE): TypeScriptAdapter {
|
|
92
|
+
(readFileSync as Mock).mockReturnValue(content);
|
|
93
|
+
return new TypeScriptAdapter('sample.ts');
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
describe('TypeScriptAdapter metric extraction (#446)', () => {
|
|
97
|
+
describe('extractFunctions', () => {
|
|
98
|
+
it('reports paramCount for every function', () => {
|
|
99
|
+
const fns = adapterFor().extractFunctions() as FnInfo[];
|
|
100
|
+
const eight = fns.find(f => f.name === 'eightParams');
|
|
101
|
+
expect(eight).toBeDefined();
|
|
102
|
+
expect(eight?.paramCount).toBe(8);
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
it('reports length (body span) for every function', () => {
|
|
106
|
+
const fns = adapterFor().extractFunctions() as FnInfo[];
|
|
107
|
+
const long = fns.find(f => f.name === 'longOne');
|
|
108
|
+
expect(long).toBeDefined();
|
|
109
|
+
// > 50 lines is the long-function threshold; assert it is measured at all.
|
|
110
|
+
expect(long?.length).toBeGreaterThan(50);
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
it('reports nestingDepth for every function', () => {
|
|
114
|
+
const fns = adapterFor().extractFunctions() as FnInfo[];
|
|
115
|
+
const deep = fns.find(f => f.name === 'deeplyNested');
|
|
116
|
+
expect(deep).toBeDefined();
|
|
117
|
+
expect(deep?.nestingDepth).toBeGreaterThanOrEqual(5);
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
it('reports hasTryCatch for unguarded and guarded IO', () => {
|
|
121
|
+
const fns = adapterFor().extractFunctions() as FnInfo[];
|
|
122
|
+
const io = fns.find(f => f.name === 'rawIo');
|
|
123
|
+
expect(io).toBeDefined();
|
|
124
|
+
expect(io?.hasTryCatch).toBe(false);
|
|
125
|
+
|
|
126
|
+
const guarded = fns.find(f => f.name === 'guardedIo');
|
|
127
|
+
expect(guarded).toBeDefined();
|
|
128
|
+
expect(guarded?.hasTryCatch).toBe(true);
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
it('reports ioOperations for IO-performing functions', () => {
|
|
132
|
+
const fns = adapterFor().extractFunctions() as FnInfo[];
|
|
133
|
+
const io = fns.find(f => f.name === 'rawIo');
|
|
134
|
+
expect(io?.ioOperations?.length ?? 0).toBeGreaterThan(0);
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
it('still reports name / line for every function', () => {
|
|
138
|
+
const fns = adapterFor().extractFunctions() as FnInfo[];
|
|
139
|
+
expect(fns.length).toBeGreaterThan(0);
|
|
140
|
+
for (const f of fns) {
|
|
141
|
+
expect(f.name).toBeTruthy();
|
|
142
|
+
expect(f.line).toBeGreaterThan(0);
|
|
143
|
+
}
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
it('finds arrow functions and class methods, not only `function` declarations', () => {
|
|
147
|
+
const fns = adapterFor().extractFunctions() as FnInfo[];
|
|
148
|
+
const names = fns.map(f => f.name);
|
|
149
|
+
// Arrow consts and class methods are the dominant modern TS forms; the old
|
|
150
|
+
// regex only matched the literal `function` keyword.
|
|
151
|
+
expect(names).toContain('arrowFn');
|
|
152
|
+
expect(names).toContain('aMethod');
|
|
153
|
+
});
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
describe('extractClasses', () => {
|
|
157
|
+
it('reports methodCount per class', () => {
|
|
158
|
+
const classes = adapterFor().extractClasses() as ClsInfo[];
|
|
159
|
+
const mega = classes.find(c => c.name === 'MegaService');
|
|
160
|
+
expect(mega).toBeDefined();
|
|
161
|
+
expect(mega?.methodCount).toBeGreaterThanOrEqual(15);
|
|
162
|
+
});
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
describe('imports', () => {
|
|
166
|
+
interface ImportInfo {
|
|
167
|
+
name?: string;
|
|
168
|
+
line?: number;
|
|
169
|
+
used?: boolean;
|
|
170
|
+
type?: string;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
it('returns objects, not bare strings (the rule reads .name/.used)', () => {
|
|
174
|
+
const imports = (adapterFor() as unknown as { imports: ImportInfo[] }).imports;
|
|
175
|
+
expect(Array.isArray(imports)).toBe(true);
|
|
176
|
+
expect(imports.length).toBeGreaterThan(0);
|
|
177
|
+
for (const imp of imports) {
|
|
178
|
+
expect(typeof imp).toBe('object');
|
|
179
|
+
// Never leak `undefined` as a name -- it renders straight into the message.
|
|
180
|
+
expect(imp.name).toBeTruthy();
|
|
181
|
+
expect(imp.name).not.toBe('undefined');
|
|
182
|
+
expect(imp.line).toBeGreaterThan(0);
|
|
183
|
+
}
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
it('marks a genuinely unused import as unused', () => {
|
|
187
|
+
const imports = (adapterFor() as unknown as { imports: ImportInfo[] }).imports;
|
|
188
|
+
const helper = imports.find(i => i.name === 'helper');
|
|
189
|
+
expect(helper).toBeDefined();
|
|
190
|
+
// `helper` is imported but never referenced in SAMPLE.
|
|
191
|
+
expect(helper?.used).toBe(false);
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
it('marks a referenced import as used', () => {
|
|
195
|
+
const content = `
|
|
196
|
+
import { readFileSync } from 'fs';
|
|
197
|
+
export function go() { return readFileSync('/tmp/x', 'utf-8'); }
|
|
198
|
+
`;
|
|
199
|
+
const imports = (adapterFor(content) as unknown as { imports: ImportInfo[] }).imports;
|
|
200
|
+
const rfs = imports.find(i => i.name === 'readFileSync');
|
|
201
|
+
expect(rfs).toBeDefined();
|
|
202
|
+
expect(rfs?.used).toBe(true);
|
|
203
|
+
});
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
describe('extract (magic numbers, #446)', () => {
|
|
207
|
+
it('returns {value,line} objects and ignores comments/strings', () => {
|
|
208
|
+
const content = `
|
|
209
|
+
// 999 should not count (comment)
|
|
210
|
+
const s = "1234 should not count (string)";
|
|
211
|
+
export function f() { return 12345; }
|
|
212
|
+
`;
|
|
213
|
+
const nums = (adapterFor(content) as unknown as {
|
|
214
|
+
extract: () => Array<{ value: number; line: number }>;
|
|
215
|
+
}).extract();
|
|
216
|
+
const values = nums.map(n => n.value);
|
|
217
|
+
expect(values).toContain(12345);
|
|
218
|
+
expect(values).not.toContain(999);
|
|
219
|
+
expect(values).not.toContain(1234);
|
|
220
|
+
for (const n of nums) expect(n.line).toBeGreaterThan(0);
|
|
221
|
+
});
|
|
222
|
+
|
|
223
|
+
/**
|
|
224
|
+
* @test REQ-DSH-018
|
|
225
|
+
* @intent 验证「命名常量的初始化值」不被当作 magic number:规则的修复建议
|
|
226
|
+
* 就是「改用命名常量」,若常量本身仍被标记,该违规永远无法消除
|
|
227
|
+
* @covers AC-DSH-018-01
|
|
228
|
+
*/
|
|
229
|
+
it('skips the initializer of a named constant but still reports bare literals', () => {
|
|
230
|
+
// Fixtures are bare fragments: no `export`, so this file does not itself
|
|
231
|
+
// trip the many-exports rule that the adapter under test is meant to flag.
|
|
232
|
+
const content = `
|
|
233
|
+
const RENAME_PARTS = 3;
|
|
234
|
+
const BUDGET: number = 5;
|
|
235
|
+
function f(): number {
|
|
236
|
+
return 137;
|
|
237
|
+
}
|
|
238
|
+
`;
|
|
239
|
+
const nums = (adapterFor(content) as unknown as {
|
|
240
|
+
extract: () => Array<{ value: number; line: number }>;
|
|
241
|
+
}).extract();
|
|
242
|
+
const values = nums.map(n => n.value);
|
|
243
|
+
|
|
244
|
+
// The rule asks for named constants, so the constants must not be flagged.
|
|
245
|
+
expect(values).not.toContain(3);
|
|
246
|
+
expect(values).not.toContain(5);
|
|
247
|
+
// A bare literal in a body is the actual target and must still be reported.
|
|
248
|
+
expect(values).toContain(137);
|
|
249
|
+
});
|
|
250
|
+
|
|
251
|
+
it('still reports a number assigned to a non-constant binding', () => {
|
|
252
|
+
// Only `const`/`readonly` declarations are exempt; `let x = 99` is not.
|
|
253
|
+
const content = `
|
|
254
|
+
function f(): number {
|
|
255
|
+
let x = 99;
|
|
256
|
+
return x;
|
|
257
|
+
}
|
|
258
|
+
`;
|
|
259
|
+
const nums = (adapterFor(content) as unknown as {
|
|
260
|
+
extract: () => Array<{ value: number; line: number }>;
|
|
261
|
+
}).extract();
|
|
262
|
+
expect(nums.map(n => n.value)).toContain(99);
|
|
263
|
+
});
|
|
264
|
+
});
|
|
265
|
+
|
|
266
|
+
describe('extractInterfaces (ISP, #446)', () => {
|
|
267
|
+
it('reports interface names and methodCount', () => {
|
|
268
|
+
const content = `
|
|
269
|
+
export interface Worker {
|
|
270
|
+
run(): void;
|
|
271
|
+
stop(): void;
|
|
272
|
+
}
|
|
273
|
+
`;
|
|
274
|
+
const ifaces = (adapterFor(content) as unknown as {
|
|
275
|
+
extractInterfaces: () => Array<{ name: string; methodCount: number }>;
|
|
276
|
+
}).extractInterfaces();
|
|
277
|
+
const worker = ifaces.find(i => i.name === 'Worker');
|
|
278
|
+
expect(worker).toBeDefined();
|
|
279
|
+
expect(worker?.methodCount).toBe(2);
|
|
280
|
+
});
|
|
281
|
+
});
|
|
282
|
+
|
|
283
|
+
describe('no control-flow false positives', () => {
|
|
284
|
+
it('does not report `if` / `for` / `while` as functions', () => {
|
|
285
|
+
const fns = adapterFor().extractFunctions() as FnInfo[];
|
|
286
|
+
const names = fns.map(f => f.name);
|
|
287
|
+
for (const kw of ['if', 'for', 'while', 'switch', 'catch', 'return']) {
|
|
288
|
+
expect(names).not.toContain(kw);
|
|
289
|
+
}
|
|
290
|
+
});
|
|
291
|
+
});
|
|
292
|
+
});
|
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
|
|
7
7
|
import { describe, it, expect, beforeEach, vi } from 'vitest';
|
|
8
8
|
import { analyze } from '../analyzer';
|
|
9
|
+
import { getDefaultConfig, resetActiveConfig, setActiveConfig } from '../config';
|
|
9
10
|
import { Rule, Adapter, Severity } from '../types';
|
|
10
11
|
|
|
11
12
|
describe('analyzer.ts - Rule Orchestration Engine', () => {
|
|
@@ -114,6 +115,26 @@ describe('analyzer.ts - Rule Orchestration Engine', () => {
|
|
|
114
115
|
expect(disabledRule.check).not.toHaveBeenCalled();
|
|
115
116
|
});
|
|
116
117
|
|
|
118
|
+
it('a rule disabled by config must not report files as checked (#457)', async () => {
|
|
119
|
+
// `filesChecked` used to be incremented before the `enabled` check, so a rule
|
|
120
|
+
// that never ran still claimed it had examined the file -- inflating per-rule
|
|
121
|
+
// coverage statistics for work the rule did not do.
|
|
122
|
+
const disabled = getDefaultConfig();
|
|
123
|
+
disabled.rules['clean-code']['long-function'].enabled = false;
|
|
124
|
+
try {
|
|
125
|
+
setActiveConfig(disabled);
|
|
126
|
+
const result = await analyze(['test.ts'], [mockRule], mockAdapter, {});
|
|
127
|
+
expect(mockRule.check).not.toHaveBeenCalled();
|
|
128
|
+
expect(result.ruleResults['clean-code.long-function'].filesChecked).toBe(0);
|
|
129
|
+
} finally {
|
|
130
|
+
// Restore the process-global config so later tests are unaffected.
|
|
131
|
+
// `resetActiveConfig()` rather than re-installing the captured config:
|
|
132
|
+
// replacing an already-installed config is exactly what the #457
|
|
133
|
+
// guardrail reports, and this suite never installs a non-default one.
|
|
134
|
+
resetActiveConfig();
|
|
135
|
+
}
|
|
136
|
+
});
|
|
137
|
+
|
|
117
138
|
it('should return empty violations if all rules pass', async () => {
|
|
118
139
|
const passingRule: Rule = {
|
|
119
140
|
id: 'clean-code.passing',
|
|
@@ -228,6 +249,13 @@ it('should skip files that do not match adapter language', async () => {
|
|
|
228
249
|
});
|
|
229
250
|
|
|
230
251
|
describe('runRuleOnFile (via analyze) — verifies context-object parameter bundling', () => {
|
|
252
|
+
// Extracted from the test body: inlining this callback pushed the test past the
|
|
253
|
+
// 4-level nesting limit for no readability gain.
|
|
254
|
+
const ruleACheck = vi.fn().mockImplementation((file: string) => {
|
|
255
|
+
if (file !== 'a.ts') return [];
|
|
256
|
+
return [{ file: 'a.ts', line: 1, ruleId: 'test.rule-a', message: 'violation in a', severity: 'warning' as Severity }];
|
|
257
|
+
});
|
|
258
|
+
|
|
231
259
|
it('should correctly aggregate violations per file and per rule', async () => {
|
|
232
260
|
const files = ['a.ts', 'b.ts'];
|
|
233
261
|
const ruleA = {
|
|
@@ -235,12 +263,7 @@ it('should skip files that do not match adapter language', async () => {
|
|
|
235
263
|
name: 'Rule A',
|
|
236
264
|
threshold: 10,
|
|
237
265
|
severity: 'warning' as Severity,
|
|
238
|
-
check:
|
|
239
|
-
if (file === 'a.ts') {
|
|
240
|
-
return [{ file: 'a.ts', line: 1, ruleId: 'test.rule-a', message: 'violation in a', severity: 'warning' as Severity }];
|
|
241
|
-
}
|
|
242
|
-
return [];
|
|
243
|
-
})
|
|
266
|
+
check: ruleACheck
|
|
244
267
|
} as Rule;
|
|
245
268
|
const ruleB = {
|
|
246
269
|
id: 'test.rule-b',
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @test REQ-452 REQ-3
|
|
3
|
+
* @intent The baseline pruner runs inside a git hook, so its worst failure mode is
|
|
4
|
+
* not a wrong answer but a commit that never returns. Every git call it
|
|
5
|
+
* makes must carry a timeout, and the timeout must be short enough that a
|
|
6
|
+
* wedged git degrades to "keep the entry" rather than hanging the hook.
|
|
7
|
+
* @covers AC-452-14
|
|
8
|
+
*
|
|
9
|
+
* Mocked rather than stubbed with a fake binary: `execFileSync` resolves PATH
|
|
10
|
+
* entries without consulting PATHEXT unless `shell` is set, so a `git.cmd` shim on
|
|
11
|
+
* PATH is silently ignored in favour of the real binary -- a test built that way
|
|
12
|
+
* passes while proving nothing. Asserting the options handed to child_process
|
|
13
|
+
* tests the guard itself instead of Node's PATH resolution.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
|
17
|
+
|
|
18
|
+
const execFileSyncMock = vi.fn();
|
|
19
|
+
|
|
20
|
+
vi.mock('child_process', () => ({
|
|
21
|
+
execFileSync: (...args: unknown[]) => execFileSyncMock(...args),
|
|
22
|
+
}));
|
|
23
|
+
vi.mock('fs', async () => {
|
|
24
|
+
const actual = await vi.importActual<typeof import('fs')>('fs');
|
|
25
|
+
return { ...actual, existsSync: vi.fn(() => false) };
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
import { pruneBaselineEntries } from '../baseline-prune';
|
|
29
|
+
|
|
30
|
+
/** Generous for a healthy `git ls-files`, short enough not to wedge a hook. */
|
|
31
|
+
const MAX_ACCEPTABLE_GIT_TIMEOUT_MS = 60_000;
|
|
32
|
+
|
|
33
|
+
describe('baseline pruning git invocation (#452)', () => {
|
|
34
|
+
beforeEach(() => {
|
|
35
|
+
execFileSyncMock.mockReset();
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
it('AC-452-14: bounds every git call with a timeout', () => {
|
|
39
|
+
execFileSyncMock.mockReturnValue('tracked.ts\n');
|
|
40
|
+
|
|
41
|
+
pruneBaselineEntries(JSON.stringify({ 'gone.ts': { 'clean-code.large-file': 1 } }), '/tmp/project');
|
|
42
|
+
|
|
43
|
+
expect(execFileSyncMock).toHaveBeenCalled();
|
|
44
|
+
for (const call of execFileSyncMock.mock.calls) {
|
|
45
|
+
const options = call[2] as { timeout?: number };
|
|
46
|
+
expect(typeof options.timeout).toBe('number');
|
|
47
|
+
expect(options.timeout).toBeGreaterThan(0);
|
|
48
|
+
expect(options.timeout).toBeLessThanOrEqual(MAX_ACCEPTABLE_GIT_TIMEOUT_MS);
|
|
49
|
+
}
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
it('AC-452-14: a git timeout keeps entries instead of failing the hook', () => {
|
|
53
|
+
// execFileSync reports a killed process as an error with no stdout, which is
|
|
54
|
+
// how a wedged git surfaces here.
|
|
55
|
+
execFileSyncMock.mockImplementation(() => {
|
|
56
|
+
const error = new Error('spawnSync git ETIMEDOUT') as Error & { signal?: string };
|
|
57
|
+
error.signal = 'SIGTERM';
|
|
58
|
+
throw error;
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
const result = pruneBaselineEntries(
|
|
62
|
+
JSON.stringify({ 'gone.ts': { 'clean-code.large-file': 1 } }),
|
|
63
|
+
'/tmp/project',
|
|
64
|
+
);
|
|
65
|
+
|
|
66
|
+
// Fail closed: an unreadable index must never justify deleting an entry.
|
|
67
|
+
expect(result.removed).toEqual([]);
|
|
68
|
+
expect(result.kept).toEqual(['gone.ts']);
|
|
69
|
+
expect(result.error).toBeDefined();
|
|
70
|
+
});
|
|
71
|
+
});
|