@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
package/principles/config.ts
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { existsSync } from 'fs';
|
|
3
|
-
import { join } from 'path';
|
|
1
|
+
import { execFileSync } from 'child_process';
|
|
2
|
+
import { existsSync, readFileSync } from 'fs';
|
|
3
|
+
import { join, resolve } from 'path';
|
|
4
4
|
|
|
5
|
-
interface RuleConfig {
|
|
5
|
+
export interface RuleConfig {
|
|
6
6
|
enabled: boolean;
|
|
7
7
|
threshold?: number;
|
|
8
8
|
exclude?: (string | number)[];
|
|
@@ -43,36 +43,89 @@ interface PrinciplesConfig {
|
|
|
43
43
|
};
|
|
44
44
|
}
|
|
45
45
|
|
|
46
|
+
/** Config keys that carry a numeric threshold, per group. */
|
|
47
|
+
const NUMERIC_KEYS = ['threshold', 'methodThreshold'] as const;
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Ceiling on any single git call made while resolving config. This runs inside a
|
|
51
|
+
* pre-commit hook, so an unbounded wait is not a slow commit but a hung one.
|
|
52
|
+
*/
|
|
53
|
+
const GIT_TIMEOUT_MS = 15_000;
|
|
54
|
+
|
|
55
|
+
/** Config keys that must remain arrays. */
|
|
56
|
+
const ARRAY_KEYS = ['exclude'] as const;
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* `severity` is not a free-form string: `summary.errorCount` and Gate 4's report
|
|
60
|
+
* grep both match the exact lowercase literal `error`, so anything else -- "Error",
|
|
61
|
+
* "fatal" -- runs the rule, reports the violation, and blocks nothing. Validated
|
|
62
|
+
* against the vocabulary rather than treated as an opaque string.
|
|
63
|
+
*/
|
|
64
|
+
const SEVERITIES = ['error', 'warning', 'info'] as const;
|
|
65
|
+
|
|
66
|
+
/** Config keys whose value must be one of a fixed set, with that set. */
|
|
67
|
+
const ENUM_KEYS = { severity: SEVERITIES } as const;
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Built-in defaults, hoisted out of `getDefaultConfig()`.
|
|
71
|
+
*
|
|
72
|
+
* Kept as module constants so the factory stays a single return statement and
|
|
73
|
+
* below the deep-nesting threshold (clean-code.deep-nesting), and so the two
|
|
74
|
+
* rule groups are readable side by side.
|
|
75
|
+
*/
|
|
76
|
+
const DEFAULT_CLEAN_CODE_RULES = {
|
|
77
|
+
'long-function': { enabled: true, threshold: 50, severity: 'warning' },
|
|
78
|
+
'large-file': { enabled: true, threshold: 1150, severity: 'warning' },
|
|
79
|
+
'god-class': { enabled: true, threshold: 15, severity: 'warning' },
|
|
80
|
+
'deep-nesting': { enabled: true, threshold: 4, severity: 'warning' },
|
|
81
|
+
'too-many-params': { enabled: true, threshold: 7, severity: 'info' },
|
|
82
|
+
'magic-numbers': {
|
|
83
|
+
enabled: true,
|
|
84
|
+
exclude: [0, 1, -1, 2, 10, 100, 1000, 60, 24, 7, 30, 365, 256, 1024],
|
|
85
|
+
severity: 'info'
|
|
86
|
+
},
|
|
87
|
+
'missing-error-handling': { enabled: true, severity: 'warning' },
|
|
88
|
+
'unused-imports': { enabled: true, severity: 'info' },
|
|
89
|
+
'code-duplication': { enabled: true, threshold: 15, severity: 'warning' },
|
|
90
|
+
'many-exports': { enabled: true, threshold: 10, severity: 'warning' }
|
|
91
|
+
} satisfies PrinciplesConfig['rules']['clean-code'];
|
|
92
|
+
|
|
93
|
+
const DEFAULT_SOLID_RULES = {
|
|
94
|
+
srp: { enabled: true, methodThreshold: 15, severity: 'warning' },
|
|
95
|
+
ocp: { enabled: true, severity: 'info' },
|
|
96
|
+
lsp: { enabled: true, severity: 'info' },
|
|
97
|
+
isp: { enabled: true, methodThreshold: 10, severity: 'info' },
|
|
98
|
+
dip: {
|
|
99
|
+
enabled: true,
|
|
100
|
+
exclude: ['Date', 'Map', 'Set', 'Error', 'Array', 'Object', 'Promise'],
|
|
101
|
+
severity: 'warning'
|
|
102
|
+
}
|
|
103
|
+
} satisfies PrinciplesConfig['rules']['solid'];
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Deep-clone a rule group so callers cannot mutate the module constants.
|
|
107
|
+
*
|
|
108
|
+
* Without this, `getDefaultConfig()` would hand out references to
|
|
109
|
+
* `DEFAULT_CLEAN_CODE_RULES` / `DEFAULT_SOLID_RULES` and a caller that tweaks
|
|
110
|
+
* one rule (e.g. `config.rules['clean-code']['large-file'].threshold = 10`)
|
|
111
|
+
* would permanently corrupt the defaults for every later caller in the process.
|
|
112
|
+
*/
|
|
113
|
+
function cloneRules<T extends Record<string, RuleConfig>>(rules: T): T {
|
|
114
|
+
const copy: Record<string, RuleConfig> = {};
|
|
115
|
+
for (const [ruleId, rule] of Object.entries(rules)) {
|
|
116
|
+
copy[ruleId] = {
|
|
117
|
+
...rule,
|
|
118
|
+
...(rule.exclude ? { exclude: [...rule.exclude] } : {})
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
return copy as T;
|
|
122
|
+
}
|
|
123
|
+
|
|
46
124
|
export function getDefaultConfig(): PrinciplesConfig {
|
|
47
125
|
return {
|
|
48
126
|
rules: {
|
|
49
|
-
'clean-code':
|
|
50
|
-
|
|
51
|
-
'large-file': { enabled: true, threshold: 1150, severity: 'warning' },
|
|
52
|
-
'god-class': { enabled: true, threshold: 15, severity: 'warning' },
|
|
53
|
-
'deep-nesting': { enabled: true, threshold: 4, severity: 'warning' },
|
|
54
|
-
'too-many-params': { enabled: true, threshold: 7, severity: 'info' },
|
|
55
|
-
'magic-numbers': {
|
|
56
|
-
enabled: true,
|
|
57
|
-
exclude: [0, 1, -1, 2, 10, 100, 1000, 60, 24, 7, 30, 365, 256, 1024],
|
|
58
|
-
severity: 'info'
|
|
59
|
-
},
|
|
60
|
-
'missing-error-handling': { enabled: true, severity: 'warning' },
|
|
61
|
-
'unused-imports': { enabled: true, severity: 'info' },
|
|
62
|
-
'code-duplication': { enabled: true, threshold: 15, severity: 'warning' },
|
|
63
|
-
'many-exports': { enabled: true, threshold: 10, severity: 'warning' }
|
|
64
|
-
},
|
|
65
|
-
'solid': {
|
|
66
|
-
'srp': { enabled: true, methodThreshold: 15, severity: 'warning' },
|
|
67
|
-
'ocp': { enabled: true, severity: 'info' },
|
|
68
|
-
'lsp': { enabled: true, severity: 'info' },
|
|
69
|
-
'isp': { enabled: true, methodThreshold: 10, severity: 'info' },
|
|
70
|
-
'dip': {
|
|
71
|
-
enabled: true,
|
|
72
|
-
exclude: ['Date', 'Map', 'Set', 'Error', 'Array', 'Object', 'Promise'],
|
|
73
|
-
severity: 'warning'
|
|
74
|
-
}
|
|
75
|
-
}
|
|
127
|
+
'clean-code': cloneRules(DEFAULT_CLEAN_CODE_RULES),
|
|
128
|
+
'solid': cloneRules(DEFAULT_SOLID_RULES)
|
|
76
129
|
},
|
|
77
130
|
output: {
|
|
78
131
|
format: 'console',
|
|
@@ -86,33 +139,288 @@ export function getDefaultConfig(): PrinciplesConfig {
|
|
|
86
139
|
};
|
|
87
140
|
}
|
|
88
141
|
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
142
|
+
/**
|
|
143
|
+
* Load `.principlesrc`, falling back to built-in defaults.
|
|
144
|
+
*
|
|
145
|
+
* Resolution order when `configPath` is omitted:
|
|
146
|
+
* 1. `<git toplevel>/.principlesrc` -- so Gate 4 behaves the same regardless
|
|
147
|
+
* of the process's cwd.
|
|
148
|
+
* 2. `<cwd>/.principlesrc` -- when git is unavailable (non-repo, broken
|
|
149
|
+
* shallow checkout, sandboxed CI).
|
|
150
|
+
*
|
|
151
|
+
* A missing, unreadable, or malformed file never throws: the built-in defaults
|
|
152
|
+
* are returned and a warning is printed. Invalid *values* (a string where a
|
|
153
|
+
* number belongs, etc.) keep the default for that key rather than poisoning the
|
|
154
|
+
* rule with a value it cannot compare against.
|
|
155
|
+
*
|
|
156
|
+
* One exception, for a path the CALLER named: if `configPath` is passed and that
|
|
157
|
+
* file does not exist, loading fails. "The project has no `.principlesrc`" is a
|
|
158
|
+
* normal state, but "the project declared one and it cannot be found" is a
|
|
159
|
+
* misconfiguration, and answering it with built-in thresholds re-creates exactly
|
|
160
|
+
* the fail-open gate this module exists to close (#457, walkthrough FC-08).
|
|
161
|
+
*/
|
|
162
|
+
export async function loadConfig(configPath?: string): Promise<PrinciplesConfig> {
|
|
163
|
+
const defaults = getDefaultConfig();
|
|
164
|
+
const resolvedPath = configPath ? resolve(configPath) : resolveDefaultConfigPath();
|
|
92
165
|
|
|
93
|
-
if (!existsSync(
|
|
94
|
-
|
|
166
|
+
if (!resolvedPath || !existsSync(resolvedPath)) {
|
|
167
|
+
if (configPath) {
|
|
168
|
+
throw new Error(`config file not found: ${resolvedPath}`);
|
|
169
|
+
}
|
|
170
|
+
return defaults;
|
|
95
171
|
}
|
|
96
172
|
|
|
173
|
+
let parsed: unknown;
|
|
97
174
|
try {
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
return
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
175
|
+
parsed = JSON.parse(readFileSync(resolvedPath, 'utf-8'));
|
|
176
|
+
} catch (error) {
|
|
177
|
+
warn(`could not parse ${resolvedPath}: ${describeError(error)} — using built-in defaults`);
|
|
178
|
+
return defaults;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
if (!isPlainObject(parsed)) {
|
|
182
|
+
warn(`${resolvedPath} must contain a JSON object — using built-in defaults`);
|
|
183
|
+
return defaults;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
return mergeConfig(defaults, parsed as Partial<PrinciplesConfig>);
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/** Resolve the default config path: git toplevel first, then cwd. */
|
|
190
|
+
function resolveDefaultConfigPath(): string | null {
|
|
191
|
+
const toplevel = gitToplevel();
|
|
192
|
+
if (toplevel) {
|
|
193
|
+
const candidate = join(toplevel, '.principlesrc');
|
|
194
|
+
if (existsSync(candidate)) return candidate;
|
|
195
|
+
}
|
|
196
|
+
return join(process.cwd(), '.principlesrc');
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/**
|
|
200
|
+
* `git rev-parse --show-toplevel`, or null when git is unavailable.
|
|
201
|
+
*
|
|
202
|
+
* Deliberately swallows failure: this runs in consumer projects, non-repo
|
|
203
|
+
* directories, and CI sandboxes where git may be absent or the checkout shallow.
|
|
204
|
+
* Callers fall back to cwd.
|
|
205
|
+
*/
|
|
206
|
+
function gitToplevel(): string | null {
|
|
207
|
+
try {
|
|
208
|
+
const out = execFileSync('git', ['rev-parse', '--show-toplevel'], {
|
|
209
|
+
encoding: 'utf-8',
|
|
210
|
+
stdio: ['ignore', 'pipe', 'ignore'],
|
|
211
|
+
// Bounded like every other git call that runs inside a hook. Without it a
|
|
212
|
+
// wedged git (locked index, stalled credential prompt, network-backed fs) hangs
|
|
213
|
+
// the commit -- and this path is taken by every Gate 4 run, since gate-4.sh
|
|
214
|
+
// passes no --config. Raised by the Delphi walkthrough (MAJ-01), which noticed
|
|
215
|
+
// the timeout added to baseline-prune.ts had not been applied here too.
|
|
216
|
+
timeout: GIT_TIMEOUT_MS,
|
|
217
|
+
}).trim();
|
|
218
|
+
return out.length > 0 ? out : null;
|
|
115
219
|
} catch {
|
|
116
|
-
return
|
|
220
|
+
return null;
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
/** Deep-merge a user config over the defaults, validating each value's shape. */
|
|
225
|
+
function mergeConfig(
|
|
226
|
+
defaults: PrinciplesConfig,
|
|
227
|
+
user: Partial<PrinciplesConfig>
|
|
228
|
+
): PrinciplesConfig {
|
|
229
|
+
return {
|
|
230
|
+
...defaults,
|
|
231
|
+
...user,
|
|
232
|
+
// `rules` needs the group-aware merge below. The other sections are plain
|
|
233
|
+
// option bags, and spreading `...user` alone would let a partial block wipe
|
|
234
|
+
// its sibling defaults -- naming one key under `output` silently discarded
|
|
235
|
+
// `show-score` and `colorize`. Same fail-open shape #457 exists to remove,
|
|
236
|
+
// just outside `rules`; found by the Delphi walkthrough.
|
|
237
|
+
output: mergeSection(defaults.output, user.output),
|
|
238
|
+
performance: mergeSection(defaults.performance, user.performance),
|
|
239
|
+
rules: {
|
|
240
|
+
'clean-code': mergeGroup(
|
|
241
|
+
defaults.rules['clean-code'],
|
|
242
|
+
user.rules?.['clean-code']
|
|
243
|
+
),
|
|
244
|
+
'solid': mergeGroup(defaults.rules['solid'], user.rules?.['solid'])
|
|
245
|
+
}
|
|
246
|
+
};
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
/** Merge one flat options section, keeping defaults for keys the user omitted. */
|
|
250
|
+
function mergeSection<T extends object>(
|
|
251
|
+
defaults: T | undefined,
|
|
252
|
+
user: Partial<T> | undefined
|
|
253
|
+
): T | undefined {
|
|
254
|
+
if (!isPlainObject(defaults)) return user as T | undefined;
|
|
255
|
+
if (!isPlainObject(user)) return defaults;
|
|
256
|
+
return { ...defaults, ...user };
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
/**
|
|
260
|
+
* Merge one rule group.
|
|
261
|
+
*
|
|
262
|
+
* Crucially this starts from the defaults for *every* rule in the group, so
|
|
263
|
+
* mentioning only `clean-code` in the file cannot drop the `solid` group.
|
|
264
|
+
* The generic preserves the concrete key set of the group being merged.
|
|
265
|
+
*/
|
|
266
|
+
function mergeGroup<T extends Record<string, RuleConfig>>(
|
|
267
|
+
defaults: T,
|
|
268
|
+
user: Record<string, Partial<RuleConfig>> | undefined
|
|
269
|
+
): T {
|
|
270
|
+
const result = { ...defaults };
|
|
271
|
+
if (!isPlainObject(user)) return result;
|
|
272
|
+
|
|
273
|
+
for (const ruleId of Object.keys(defaults)) {
|
|
274
|
+
const override = user[ruleId];
|
|
275
|
+
if (!isPlainObject(override)) continue;
|
|
276
|
+
// Safe: `ruleId` comes from Object.keys(defaults), so it is a key of T.
|
|
277
|
+
(result as Record<string, RuleConfig>)[ruleId] = mergeRule(
|
|
278
|
+
defaults[ruleId],
|
|
279
|
+
override as Partial<RuleConfig>,
|
|
280
|
+
ruleId
|
|
281
|
+
);
|
|
282
|
+
}
|
|
283
|
+
return result;
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
/** Merge one rule, keeping the default for any key whose override is mistyped. */
|
|
287
|
+
function mergeRule(
|
|
288
|
+
base: RuleConfig,
|
|
289
|
+
override: Partial<RuleConfig>,
|
|
290
|
+
ruleId: string
|
|
291
|
+
): RuleConfig {
|
|
292
|
+
const merged: RuleConfig = { ...base };
|
|
293
|
+
|
|
294
|
+
if (typeof override.enabled === 'boolean') {
|
|
295
|
+
merged.enabled = override.enabled;
|
|
296
|
+
} else if (override.enabled !== undefined) {
|
|
297
|
+
warn(`rules.${ruleId}.enabled must be a boolean — keeping default`);
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
for (const key of NUMERIC_KEYS) {
|
|
301
|
+
const value = override[key];
|
|
302
|
+
if (value === undefined) continue;
|
|
303
|
+
if (typeof value === 'number' && Number.isFinite(value)) {
|
|
304
|
+
merged[key] = value;
|
|
305
|
+
} else {
|
|
306
|
+
warn(`rules.${ruleId}.${key} must be a finite number — keeping default`);
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
for (const key of ARRAY_KEYS) {
|
|
311
|
+
const value = override[key];
|
|
312
|
+
if (value === undefined) continue;
|
|
313
|
+
// Arrays REPLACE rather than concatenate, matching how .principlesrc is
|
|
314
|
+
// written today (an exclude list is the project's complete list).
|
|
315
|
+
if (Array.isArray(value)) {
|
|
316
|
+
merged[key] = [...value] as RuleConfig[typeof key];
|
|
317
|
+
} else {
|
|
318
|
+
warn(`rules.${ruleId}.${key} must be an array — keeping default`);
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
for (const key of Object.keys(ENUM_KEYS) as (keyof typeof ENUM_KEYS)[]) {
|
|
323
|
+
const allowed: readonly string[] = ENUM_KEYS[key];
|
|
324
|
+
const value = override[key];
|
|
325
|
+
if (value === undefined) continue;
|
|
326
|
+
if (typeof value === 'string' && allowed.includes(value)) {
|
|
327
|
+
merged[key] = value;
|
|
328
|
+
} else {
|
|
329
|
+
// Loud on purpose: silently keeping the default would let a project believe
|
|
330
|
+
// it raised a rule to blocking when it did not.
|
|
331
|
+
warn(`rules.${ruleId}.${key} must be one of ${allowed.join(', ')} — keeping default`);
|
|
332
|
+
}
|
|
117
333
|
}
|
|
334
|
+
|
|
335
|
+
return merged;
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
function isPlainObject(value: unknown): value is Record<string, unknown> {
|
|
339
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
function describeError(error: unknown): string {
|
|
343
|
+
return error instanceof Error ? error.message : String(error);
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
/** Emit a diagnostic without failing the run (Gate 4 must stay usable). */
|
|
347
|
+
function warn(message: string): void {
|
|
348
|
+
process.stderr.write(`⚠️ [principles] ${message}\n`);
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
// ---------------------------------------------------------------------------
|
|
352
|
+
// Active configuration
|
|
353
|
+
// ---------------------------------------------------------------------------
|
|
354
|
+
//
|
|
355
|
+
// Rules read their thresholds lazily through `getActiveConfig()` instead of
|
|
356
|
+
// snapshotting `getDefaultConfig()` at module load. That snapshot was #457: a
|
|
357
|
+
// project's `.principlesrc` was parsed but never consulted, so Gate 4 silently
|
|
358
|
+
// enforced the built-in defaults.
|
|
359
|
+
//
|
|
360
|
+
// CONCURRENCY: this is process-global mutable state. A single process must not
|
|
361
|
+
// evaluate two different projects' configs at the same time. Callers that need
|
|
362
|
+
// isolation should run them in separate processes. Tests MUST call
|
|
363
|
+
// `resetActiveConfig()` between cases to avoid leaking state -- and doing so is
|
|
364
|
+
// no longer only a convention: `setActiveConfig` reports a replacement of an
|
|
365
|
+
// already-installed config, so the leak fails visibly instead of silently.
|
|
366
|
+
// ---------------------------------------------------------------------------
|
|
367
|
+
|
|
368
|
+
let activeConfig: PrinciplesConfig = getDefaultConfig();
|
|
369
|
+
|
|
370
|
+
// Set by setActiveConfig, cleared by resetActiveConfig. The concurrency contract
|
|
371
|
+
// above was documentation only; this makes an undocumented second set visible
|
|
372
|
+
// instead of letting the last writer silently win (walkthrough FC-02).
|
|
373
|
+
let activeConfigInstalled = false;
|
|
374
|
+
|
|
375
|
+
/** The configuration rules currently enforce. */
|
|
376
|
+
export function getActiveConfig(): PrinciplesConfig {
|
|
377
|
+
return activeConfig;
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
/**
|
|
381
|
+
* The effective configuration of one rule, read from what is being enforced NOW.
|
|
382
|
+
*
|
|
383
|
+
* `Rule.threshold` and `Rule.severity` are deprecated snapshots taken when the
|
|
384
|
+
* rule module was constructed; a project's `.principlesrc` overrides them at
|
|
385
|
+
* run time (#457). Consumers that report or compare thresholds must read them
|
|
386
|
+
* here, or they will describe a number `check()` never compared against
|
|
387
|
+
* (walkthrough MAJ-03 / FC-05).
|
|
388
|
+
*
|
|
389
|
+
* Fails on an unknown id rather than returning defaults: answering "there is no
|
|
390
|
+
* such rule" with a plausible-looking threshold is the same silent-defaults
|
|
391
|
+
* defect this module exists to remove.
|
|
392
|
+
*/
|
|
393
|
+
export function getEffectiveConfigFor(ruleId: string): RuleConfig {
|
|
394
|
+
const separator = ruleId.indexOf('.');
|
|
395
|
+
if (separator <= 0 || separator === ruleId.length - 1) {
|
|
396
|
+
throw new Error(`rule id must be in group.name form, got "${ruleId}"`);
|
|
397
|
+
}
|
|
398
|
+
const group = ruleId.slice(0, separator);
|
|
399
|
+
const name = ruleId.slice(separator + 1);
|
|
400
|
+
const groups = activeConfig.rules as Record<string, Record<string, RuleConfig> | undefined>;
|
|
401
|
+
const settings = groups[group]?.[name];
|
|
402
|
+
if (!settings) {
|
|
403
|
+
throw new Error(`no rule config for "${ruleId}" in the active configuration`);
|
|
404
|
+
}
|
|
405
|
+
return settings;
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
/** Install the configuration that rules should enforce. */
|
|
409
|
+
export function setActiveConfig(config: PrinciplesConfig): void {
|
|
410
|
+
if (activeConfigInstalled) {
|
|
411
|
+
warn('active config replaced without resetActiveConfig(); one process should analyse one project with one config');
|
|
412
|
+
}
|
|
413
|
+
activeConfig = config;
|
|
414
|
+
activeConfigInstalled = true;
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
/**
|
|
418
|
+
* Restore the built-in defaults.
|
|
419
|
+
*
|
|
420
|
+
* Exists for test isolation: without it, one test's `setActiveConfig` leaks into
|
|
421
|
+
* the next and produces order-dependent failures.
|
|
422
|
+
*/
|
|
423
|
+
export function resetActiveConfig(): void {
|
|
424
|
+
activeConfig = getDefaultConfig();
|
|
425
|
+
activeConfigInstalled = false;
|
|
118
426
|
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { realpathSync } from 'node:fs';
|
|
2
|
+
import { resolve } from 'node:path';
|
|
3
|
+
import { pathToFileURL } from 'node:url';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Decide whether the calling module is the process entry point.
|
|
7
|
+
*
|
|
8
|
+
* Why this exists: the previous check in `boy-scout.ts` was
|
|
9
|
+
* `process.argv[1]?.includes('boy-scout')`, which is wrong in both directions.
|
|
10
|
+
* It fires for any unrelated path that happens to contain the substring (e.g.
|
|
11
|
+
* `/tmp/boy-scout-notes/run.ts`), and it fails to fire when the real script is
|
|
12
|
+
* invoked through a path that does not repeat the filename -- which is exactly
|
|
13
|
+
* what happens under CI, where argv[1] is the test runner's path. The CLI then
|
|
14
|
+
* silently does nothing and prints nothing (#453).
|
|
15
|
+
*
|
|
16
|
+
* IMPORTANT -- pass `metaUrl` explicitly. `import.meta.url` is evaluated where
|
|
17
|
+
* it is *written*, so a default of `import.meta.url` inside this shared module
|
|
18
|
+
* captures THIS file's URL, not the caller's, and every guard would return false.
|
|
19
|
+
* Callers must use `isDirectExecution(process.argv[1], import.meta.url)`.
|
|
20
|
+
*
|
|
21
|
+
* Both sides are realpath'd before comparison. Node resolves ESM module URLs
|
|
22
|
+
* through symlinks, while `argv[1]` keeps whatever path the caller typed -- so a
|
|
23
|
+
* bin shim, `npm link`, or a symlinked checkout compares unequal without this and
|
|
24
|
+
* the CLI silently no-ops, which is the same failure class as #453.
|
|
25
|
+
*/
|
|
26
|
+
export function isDirectExecution(
|
|
27
|
+
argv1: string | undefined,
|
|
28
|
+
metaUrl: string | undefined,
|
|
29
|
+
): boolean {
|
|
30
|
+
// CJS entry points are already unambiguous, and `require.main === module` is
|
|
31
|
+
// exact. This tests the CALLER's module only when the caller's own file runs
|
|
32
|
+
// this branch; under ESM the whole expression is inert because `typeof require`
|
|
33
|
+
// is 'undefined' (that is why it is guarded by `typeof`, not `require` -- a bare
|
|
34
|
+
// `require.main` would throw ReferenceError in ESM).
|
|
35
|
+
if (typeof require !== 'undefined' && require.main === module) {
|
|
36
|
+
return true;
|
|
37
|
+
}
|
|
38
|
+
if (!metaUrl || !argv1) {
|
|
39
|
+
return false;
|
|
40
|
+
}
|
|
41
|
+
try {
|
|
42
|
+
return canonical(pathToFileURL(resolve(argv1)).href) === canonical(metaUrl);
|
|
43
|
+
} catch {
|
|
44
|
+
// An unparsable argv[1] is not an entry point; never throw from a guard.
|
|
45
|
+
return false;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Resolve a file URL to its real path so symlinked and non-symlinked spellings of
|
|
51
|
+
* the same file compare equal. Falls back to the input when the path does not
|
|
52
|
+
* exist (a guard must not throw) -- an unresolvable URL simply will not match.
|
|
53
|
+
*/
|
|
54
|
+
function canonical(fileUrl: string): string {
|
|
55
|
+
try {
|
|
56
|
+
return pathToFileURL(realpathSync(new URL(fileUrl))).href;
|
|
57
|
+
} catch {
|
|
58
|
+
return fileUrl;
|
|
59
|
+
}
|
|
60
|
+
}
|
package/principles/index.ts
CHANGED
|
@@ -1,13 +1,16 @@
|
|
|
1
1
|
import { analyze, getAdapterForFile } from './analyzer';
|
|
2
2
|
import { formatConsole, formatJSON, formatSARIF } from './reporter';
|
|
3
|
-
import { loadConfig } from './config';
|
|
3
|
+
import { loadConfig, setActiveConfig } from './config';
|
|
4
4
|
import { getAllPrincipleRules } from './rules';
|
|
5
|
+
import { isDirectExecution } from './direct-execution.js';
|
|
5
6
|
|
|
6
7
|
interface CLIOptions {
|
|
7
8
|
files: string[];
|
|
8
9
|
format: 'console' | 'json' | 'sarif';
|
|
9
10
|
changedOnly: boolean;
|
|
10
11
|
showScore: boolean;
|
|
12
|
+
/** Explicit `.principlesrc` path; falls back to git-toplevel/cwd lookup. */
|
|
13
|
+
configPath?: string;
|
|
11
14
|
}
|
|
12
15
|
|
|
13
16
|
const VALID_FORMATS: readonly string[] = ['json', 'console', 'sarif'] as const;
|
|
@@ -36,6 +39,12 @@ export function parseArgs(args: string[]): CLIOptions {
|
|
|
36
39
|
case '--show-score':
|
|
37
40
|
options.showScore = true;
|
|
38
41
|
break;
|
|
42
|
+
// Previously absent, so `--config .principlesrc` was silently filed into
|
|
43
|
+
// `files` and the path was analysed as if it were source (#457).
|
|
44
|
+
case '--config':
|
|
45
|
+
const cfg = args[++i];
|
|
46
|
+
if (cfg) options.configPath = cfg;
|
|
47
|
+
break;
|
|
39
48
|
default:
|
|
40
49
|
if (!args[i].startsWith('--')) options.files.push(args[i]);
|
|
41
50
|
}
|
|
@@ -54,11 +63,19 @@ export async function main(args: string[]): Promise<number> {
|
|
|
54
63
|
const options = parseArgs(args);
|
|
55
64
|
|
|
56
65
|
if (options.files.length === 0) {
|
|
57
|
-
console.error('Usage: principles-checker --files <file1> <file2> ... [--format console|json|sarif] [--changed-only]');
|
|
58
|
-
|
|
66
|
+
console.error('Usage: principles-checker --files <file1> <file2> ... [--format console|json|sarif] [--changed-only] [--config <path>]');
|
|
67
|
+
// 2, not 1. Under this module's contract exit 1 asserts "I examined files and
|
|
68
|
+
// found ERROR-severity violations". A gate that sees 1 with an empty report
|
|
69
|
+
// reads it as a clean pass, so returning 1 here would let a miswired hook
|
|
70
|
+
// announce PASSED having checked nothing at all.
|
|
71
|
+
return 2;
|
|
59
72
|
}
|
|
60
73
|
|
|
61
|
-
|
|
74
|
+
// Install the loaded config so rules read the PROJECT's thresholds. The
|
|
75
|
+
// previous `await loadConfig();` discarded the result, so `.principlesrc` was
|
|
76
|
+
// parsed and thrown away while the built-in defaults were enforced (#457).
|
|
77
|
+
setActiveConfig(await loadConfig(options.configPath));
|
|
78
|
+
|
|
62
79
|
const rules = getAllRules();
|
|
63
80
|
const result = await analyze(options.files, rules, getAdapterForFile);
|
|
64
81
|
|
|
@@ -69,24 +86,23 @@ export async function main(args: string[]): Promise<number> {
|
|
|
69
86
|
};
|
|
70
87
|
console.log(formatters[options.format](result));
|
|
71
88
|
|
|
72
|
-
|
|
89
|
+
// Exit 1 only for ERROR-severity violations. Returning 1 for any violation
|
|
90
|
+
// made info/warning noise (e.g. magic-numbers `info`) look like a tool
|
|
91
|
+
// failure: Gate 4 treats a non-zero exit as "checker execution failed" and
|
|
92
|
+
// downgrades itself to SKIPPED, silently disabling the gate.
|
|
93
|
+
return result.summary.errorCount > 0 ? 1 : 0;
|
|
73
94
|
}
|
|
74
95
|
|
|
75
96
|
// Support both CJS (require.main === module) and ESM (import.meta.url) runtimes.
|
|
76
97
|
// npx tsx loads files via import() making require.main unreliable.
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
const fileUrl = `file://${process.argv[1]}`;
|
|
83
|
-
return import.meta.url === fileUrl || import.meta.url.endsWith(`/${process.argv[1]}`);
|
|
84
|
-
}
|
|
85
|
-
return false;
|
|
86
|
-
}
|
|
98
|
+
//
|
|
99
|
+
// The implementation lives in ./direct-execution.ts so the identical guard can be
|
|
100
|
+
// shared with boy-scout.ts without either module exceeding the 10-export limit.
|
|
101
|
+
// Re-exported here because it is part of this module's public contract (#444).
|
|
102
|
+
export { isDirectExecution } from './direct-execution.js';
|
|
87
103
|
|
|
88
104
|
const args = process.argv.slice(2);
|
|
89
|
-
if (isDirectExecution()) {
|
|
105
|
+
if (isDirectExecution(process.argv[1], import.meta.url)) {
|
|
90
106
|
main(args)
|
|
91
107
|
.then(exitCode => {
|
|
92
108
|
if (exitCode !== 0) {
|
|
@@ -95,6 +111,13 @@ if (isDirectExecution()) {
|
|
|
95
111
|
})
|
|
96
112
|
.catch(err => {
|
|
97
113
|
console.error('Analysis failed:', err.message);
|
|
98
|
-
|
|
114
|
+
// Exit 2, not 1. Gate 4 decides by testing the exit status, and exit 1 now
|
|
115
|
+
// means "the checker ran and found ERROR-severity violations". Reusing 1 for
|
|
116
|
+
// a crash would make a genuine finding indistinguishable from a broken tool,
|
|
117
|
+
// and the gate's `else` branch SKIPs -- releasing exactly the most serious
|
|
118
|
+
// violations. 2 is this repo's existing convention for a runtime error:
|
|
119
|
+
// src/gates/gate-8.ts and gate-9.ts both SKIP on exit >= 2 and treat 1 as a
|
|
120
|
+
// real finding.
|
|
121
|
+
process.exit(2);
|
|
99
122
|
});
|
|
100
123
|
}
|
|
@@ -1,17 +1,23 @@
|
|
|
1
1
|
import { describe, it, expect, vi } from 'vitest';
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
3
|
+
// The rule now reads its settings from `getActiveConfig()` at check time
|
|
4
|
+
// (Issue #457: a module-load snapshot ignored `.principlesrc`), so the mock must
|
|
5
|
+
// expose that accessor. The same fixture is returned by both entry points.
|
|
6
|
+
const mockConfig = {
|
|
7
|
+
rules: {
|
|
8
|
+
'clean-code': {
|
|
9
|
+
'many-exports': {
|
|
10
|
+
enabled: true,
|
|
11
|
+
// threshold intentionally omitted -> exercises ?? 10 fallback branch
|
|
12
|
+
severity: 'warning',
|
|
12
13
|
},
|
|
13
14
|
},
|
|
14
|
-
}
|
|
15
|
+
},
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
vi.mock('../../../config', () => ({
|
|
19
|
+
getDefaultConfig: () => mockConfig,
|
|
20
|
+
getActiveConfig: () => mockConfig,
|
|
15
21
|
}));
|
|
16
22
|
|
|
17
23
|
import { manyExportsRule } from '../../clean-code/many-exports';
|
|
@@ -1,27 +1,27 @@
|
|
|
1
1
|
import { Rule, Violation, Severity } from '../../types';
|
|
2
|
-
import {
|
|
3
|
-
|
|
4
|
-
const config = getDefaultConfig();
|
|
2
|
+
import { getActiveConfig } from '../../config';
|
|
5
3
|
|
|
6
4
|
export const codeDuplicationRule: Rule = {
|
|
7
5
|
id: 'clean-code.code-duplication',
|
|
8
6
|
name: 'Code Duplication Rule',
|
|
9
|
-
threshold:
|
|
10
|
-
severity:
|
|
7
|
+
threshold: 15,
|
|
8
|
+
severity: 'warning',
|
|
11
9
|
check: (file: string, adapter: unknown): Violation[] => {
|
|
12
10
|
const violations: Violation[] = [];
|
|
11
|
+
// Read the ACTIVE config per invocation; a module-load snapshot was #457.
|
|
12
|
+
const settings = getActiveConfig().rules['clean-code']['code-duplication'];
|
|
13
13
|
|
|
14
14
|
try {
|
|
15
15
|
const typedAdapter = adapter as { duplicationPercentage?: number };
|
|
16
16
|
const duplicationPercentage = typedAdapter.duplicationPercentage;
|
|
17
17
|
|
|
18
|
-
if (duplicationPercentage && duplicationPercentage > (
|
|
18
|
+
if (duplicationPercentage && duplicationPercentage > (settings.threshold ?? 0)) {
|
|
19
19
|
violations.push({
|
|
20
20
|
file,
|
|
21
21
|
line: 1,
|
|
22
22
|
ruleId: 'clean-code.code-duplication',
|
|
23
|
-
message: `Code duplication detected: ${duplicationPercentage}% (threshold: ${
|
|
24
|
-
severity:
|
|
23
|
+
message: `Code duplication detected: ${duplicationPercentage}% (threshold: ${settings.threshold}%). Consider refactoring duplicated code.`,
|
|
24
|
+
severity: (settings.severity as Severity) ?? 'warning'
|
|
25
25
|
});
|
|
26
26
|
}
|
|
27
27
|
} catch { }
|