critical-gate 2.0.1 → 2.1.1
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/README.md +163 -0
- package/dist/cli.d.ts +8 -2
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +484 -22
- package/dist/cli.js.map +1 -1
- package/dist/config/critical-gate-config.d.ts +41 -0
- package/dist/config/critical-gate-config.d.ts.map +1 -1
- package/dist/config/critical-gate-config.js +193 -4
- package/dist/config/critical-gate-config.js.map +1 -1
- package/dist/config/index.d.ts +2 -1
- package/dist/config/index.d.ts.map +1 -1
- package/dist/config/index.js +2 -1
- package/dist/config/index.js.map +1 -1
- package/dist/config/learning-policy.d.ts +9 -0
- package/dist/config/learning-policy.d.ts.map +1 -0
- package/dist/config/learning-policy.js +34 -0
- package/dist/config/learning-policy.js.map +1 -0
- package/dist/detectors/api-surface-detector.d.ts.map +1 -1
- package/dist/detectors/api-surface-detector.js +123 -5
- package/dist/detectors/api-surface-detector.js.map +1 -1
- package/dist/detectors/confidence-calibration.d.ts +11 -0
- package/dist/detectors/confidence-calibration.d.ts.map +1 -0
- package/dist/detectors/confidence-calibration.js +53 -0
- package/dist/detectors/confidence-calibration.js.map +1 -0
- package/dist/detectors/existing-solution-detector.js +6 -0
- package/dist/detectors/existing-solution-detector.js.map +1 -1
- package/dist/detectors/expected-companions-detector.d.ts.map +1 -1
- package/dist/detectors/expected-companions-detector.js +208 -10
- package/dist/detectors/expected-companions-detector.js.map +1 -1
- package/dist/detectors/index.d.ts +1 -0
- package/dist/detectors/index.d.ts.map +1 -1
- package/dist/detectors/index.js +1 -0
- package/dist/detectors/index.js.map +1 -1
- package/dist/detectors/intent-verification-detector.d.ts.map +1 -1
- package/dist/detectors/intent-verification-detector.js +64 -1
- package/dist/detectors/intent-verification-detector.js.map +1 -1
- package/dist/detectors/reason-chain.d.ts +4 -0
- package/dist/detectors/reason-chain.d.ts.map +1 -0
- package/dist/detectors/reason-chain.js +102 -0
- package/dist/detectors/reason-chain.js.map +1 -0
- package/dist/detectors/repair-contract.d.ts +4 -0
- package/dist/detectors/repair-contract.d.ts.map +1 -0
- package/dist/detectors/repair-contract.js +45 -0
- package/dist/detectors/repair-contract.js.map +1 -0
- package/dist/detectors/repository-intelligence-detector.d.ts.map +1 -1
- package/dist/detectors/repository-intelligence-detector.js +17 -4
- package/dist/detectors/repository-intelligence-detector.js.map +1 -1
- package/dist/detectors/rewrite-detector.d.ts.map +1 -1
- package/dist/detectors/rewrite-detector.js +5 -2
- package/dist/detectors/rewrite-detector.js.map +1 -1
- package/dist/detectors/runner.d.ts +1 -0
- package/dist/detectors/runner.d.ts.map +1 -1
- package/dist/detectors/runner.js +50 -3
- package/dist/detectors/runner.js.map +1 -1
- package/dist/detectors/scope-detector.d.ts.map +1 -1
- package/dist/detectors/scope-detector.js +1 -8
- package/dist/detectors/scope-detector.js.map +1 -1
- package/dist/detectors/secret-path-detector.js +1 -1
- package/dist/detectors/secret-path-detector.js.map +1 -1
- package/dist/detectors/test-weakening-detector.d.ts.map +1 -1
- package/dist/detectors/test-weakening-detector.js +188 -39
- package/dist/detectors/test-weakening-detector.js.map +1 -1
- package/dist/detectors/types.d.ts +2 -0
- package/dist/detectors/types.d.ts.map +1 -1
- package/dist/diff/git-diff-reader.d.ts +1 -0
- package/dist/diff/git-diff-reader.d.ts.map +1 -1
- package/dist/diff/git-diff-reader.js +6 -3
- package/dist/diff/git-diff-reader.js.map +1 -1
- package/dist/diff/path-classifier.d.ts.map +1 -1
- package/dist/diff/path-classifier.js +9 -1
- package/dist/diff/path-classifier.js.map +1 -1
- package/dist/frameworks/detect.d.ts +11 -0
- package/dist/frameworks/detect.d.ts.map +1 -0
- package/dist/frameworks/detect.js +29 -0
- package/dist/frameworks/detect.js.map +1 -0
- package/dist/frameworks/index.d.ts +5 -0
- package/dist/frameworks/index.d.ts.map +1 -0
- package/dist/frameworks/index.js +4 -0
- package/dist/frameworks/index.js.map +1 -0
- package/dist/frameworks/packs.d.ts +3 -0
- package/dist/frameworks/packs.d.ts.map +1 -0
- package/dist/frameworks/packs.js +146 -0
- package/dist/frameworks/packs.js.map +1 -0
- package/dist/frameworks/path-patterns.d.ts +3 -0
- package/dist/frameworks/path-patterns.d.ts.map +1 -0
- package/dist/frameworks/path-patterns.js +16 -0
- package/dist/frameworks/path-patterns.js.map +1 -0
- package/dist/frameworks/types.d.ts +15 -0
- package/dist/frameworks/types.d.ts.map +1 -0
- package/dist/frameworks/types.js +2 -0
- package/dist/frameworks/types.js.map +1 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -0
- package/dist/index.js.map +1 -1
- package/dist/intent/diff-coherence-score.d.ts +13 -0
- package/dist/intent/diff-coherence-score.d.ts.map +1 -0
- package/dist/intent/diff-coherence-score.js +125 -0
- package/dist/intent/diff-coherence-score.js.map +1 -0
- package/dist/intent/index.d.ts +2 -0
- package/dist/intent/index.d.ts.map +1 -1
- package/dist/intent/index.js +2 -0
- package/dist/intent/index.js.map +1 -1
- package/dist/intent/intent-core.d.ts.map +1 -1
- package/dist/intent/intent-core.js +17 -1
- package/dist/intent/intent-core.js.map +1 -1
- package/dist/intent/intent-model.d.ts.map +1 -1
- package/dist/intent/intent-model.js +39 -2
- package/dist/intent/intent-model.js.map +1 -1
- package/dist/intent/task-intent-quality.d.ts +4 -0
- package/dist/intent/task-intent-quality.d.ts.map +1 -0
- package/dist/intent/task-intent-quality.js +98 -0
- package/dist/intent/task-intent-quality.js.map +1 -0
- package/dist/knowledge/history-index.d.ts.map +1 -1
- package/dist/knowledge/history-index.js +10 -3
- package/dist/knowledge/history-index.js.map +1 -1
- package/dist/knowledge/index.d.ts +2 -1
- package/dist/knowledge/index.d.ts.map +1 -1
- package/dist/knowledge/index.js +1 -0
- package/dist/knowledge/index.js.map +1 -1
- package/dist/knowledge/normal-change-model.d.ts +8 -0
- package/dist/knowledge/normal-change-model.d.ts.map +1 -0
- package/dist/knowledge/normal-change-model.js +121 -0
- package/dist/knowledge/normal-change-model.js.map +1 -0
- package/dist/knowledge/solution-index.d.ts.map +1 -1
- package/dist/knowledge/solution-index.js +43 -3
- package/dist/knowledge/solution-index.js.map +1 -1
- package/dist/knowledge/types.d.ts +11 -0
- package/dist/knowledge/types.d.ts.map +1 -1
- package/dist/reporters/index.d.ts +4 -1
- package/dist/reporters/index.d.ts.map +1 -1
- package/dist/reporters/index.js +6 -0
- package/dist/reporters/index.js.map +1 -1
- package/dist/reporters/markdown-reporter.d.ts.map +1 -1
- package/dist/reporters/markdown-reporter.js +67 -0
- package/dist/reporters/markdown-reporter.js.map +1 -1
- package/dist/reporters/pr-comment-reporter.d.ts +3 -0
- package/dist/reporters/pr-comment-reporter.d.ts.map +1 -0
- package/dist/reporters/pr-comment-reporter.js +105 -0
- package/dist/reporters/pr-comment-reporter.js.map +1 -0
- package/dist/reporters/repair-contract.d.ts +3 -0
- package/dist/reporters/repair-contract.d.ts.map +1 -0
- package/dist/reporters/repair-contract.js +25 -0
- package/dist/reporters/repair-contract.js.map +1 -0
- package/dist/reporters/repair-reporter.d.ts.map +1 -1
- package/dist/reporters/repair-reporter.js +19 -1
- package/dist/reporters/repair-reporter.js.map +1 -1
- package/dist/reporters/reviewer-checklist.d.ts +10 -0
- package/dist/reporters/reviewer-checklist.d.ts.map +1 -0
- package/dist/reporters/reviewer-checklist.js +112 -0
- package/dist/reporters/reviewer-checklist.js.map +1 -0
- package/dist/repository/api-snapshot.d.ts +41 -0
- package/dist/repository/api-snapshot.d.ts.map +1 -0
- package/dist/repository/api-snapshot.js +219 -0
- package/dist/repository/api-snapshot.js.map +1 -0
- package/dist/repository/index.d.ts +2 -0
- package/dist/repository/index.d.ts.map +1 -1
- package/dist/repository/index.js +2 -0
- package/dist/repository/index.js.map +1 -1
- package/dist/repository/monorepo.d.ts +7 -0
- package/dist/repository/monorepo.d.ts.map +1 -0
- package/dist/repository/monorepo.js +189 -0
- package/dist/repository/monorepo.js.map +1 -0
- package/dist/schema/gate-result.schema.d.ts +272 -0
- package/dist/schema/gate-result.schema.d.ts.map +1 -1
- package/dist/schema/gate-result.schema.js +148 -1
- package/dist/schema/gate-result.schema.js.map +1 -1
- package/dist/schema/index.d.ts +1 -1
- package/dist/schema/index.d.ts.map +1 -1
- package/dist/schema/index.js.map +1 -1
- package/dist/schema/types.d.ts +63 -0
- package/dist/schema/types.d.ts.map +1 -1
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import { writeFileSync } from "node:fs";
|
|
3
|
-
import { resolve } from "node:path";
|
|
2
|
+
import { chmodSync, existsSync, mkdirSync, readFileSync, realpathSync, writeFileSync } from "node:fs";
|
|
3
|
+
import { join, resolve } from "node:path";
|
|
4
4
|
import { fileURLToPath } from "node:url";
|
|
5
|
-
import { GATE_RESULT_SCHEMA_VERSION, loadCriticalGateConfig, readGitDiff, renderReport, runDetectors, summarizeFindings, summarizeIntentVerification } from "./index.js";
|
|
6
|
-
export const CLI_VERSION = "2.
|
|
5
|
+
import { CRITICAL_GATE_CONFIG_FILE, GATE_RESULT_SCHEMA_VERSION, applyLearningPolicy, analyzeTaskIntentQuality, buildApiSurfaceSnapshot, createDefaultPolicyConfig, detectFrameworkPacks, detectMonorepoContext, getApiSnapshotOutputDirectory, getApiSnapshotOutputPath, getConfiguredExpectedSupportFiles, getConfiguredFailOn, getPolicyBlockingDetectors, getPolicyObservationDetectors, loadApiSurfaceSnapshot, loadCriticalGateConfig, readGitDiff, renderReport, runDetectors, summarizeApiSurfaceSnapshot, summarizeFindings, summarizeIntentVerification, updateCriticalGateConfig } from "./index.js";
|
|
6
|
+
export const CLI_VERSION = "2.1.1";
|
|
7
7
|
export const ExitCode = {
|
|
8
8
|
Pass: 0,
|
|
9
9
|
FindingsFailed: 1,
|
|
@@ -13,9 +13,15 @@ export const ExitCode = {
|
|
|
13
13
|
const defaultIo = {
|
|
14
14
|
stdout: (message) => console.log(message),
|
|
15
15
|
stderr: (message) => console.error(message),
|
|
16
|
-
writeFile: (path, content) =>
|
|
16
|
+
writeFile: (path, content) => {
|
|
17
|
+
mkdirSync(getApiSnapshotOutputDirectory(path), { recursive: true });
|
|
18
|
+
writeFileSync(path, content, "utf8");
|
|
19
|
+
},
|
|
17
20
|
now: () => new Date(),
|
|
18
|
-
|
|
21
|
+
exists: (path) => existsSync(path),
|
|
22
|
+
readFile: (path) => readFileSync(path, "utf8"),
|
|
23
|
+
chmodFile: (path, mode) => chmodSync(path, mode),
|
|
24
|
+
readDiff: (baseRef, options) => readGitDiff({ baseRef, staged: options?.staged })
|
|
19
25
|
};
|
|
20
26
|
export function main(argv = process.argv.slice(2), io = defaultIo) {
|
|
21
27
|
try {
|
|
@@ -43,17 +49,32 @@ function runCli(argv, io) {
|
|
|
43
49
|
return ExitCode.UsageError;
|
|
44
50
|
}
|
|
45
51
|
if (args.includes("--help") || args.includes("-h")) {
|
|
46
|
-
io.stdout(command
|
|
52
|
+
io.stdout(getCommandHelpText(command));
|
|
47
53
|
return ExitCode.Pass;
|
|
48
54
|
}
|
|
55
|
+
if (command === "accept") {
|
|
56
|
+
return runAcceptCommand(args, io);
|
|
57
|
+
}
|
|
58
|
+
if (command === "teach") {
|
|
59
|
+
return runTeachCommand(args, io);
|
|
60
|
+
}
|
|
61
|
+
if (command === "snapshot-api") {
|
|
62
|
+
return runSnapshotApiCommand(args, io);
|
|
63
|
+
}
|
|
64
|
+
if (command === "install-hooks") {
|
|
65
|
+
return runInstallHooksCommand(args, io);
|
|
66
|
+
}
|
|
67
|
+
if (command === "init-policy") {
|
|
68
|
+
return runInitPolicyCommand(args, io);
|
|
69
|
+
}
|
|
49
70
|
const parsed = parseCheckArgs(args, command);
|
|
50
71
|
if (!parsed.ok) {
|
|
51
72
|
io.stderr(parsed.error);
|
|
52
73
|
io.stderr("Run critical-gate check --help for usage.");
|
|
53
74
|
return ExitCode.UsageError;
|
|
54
75
|
}
|
|
55
|
-
const diff = io.readDiff(parsed.options.base);
|
|
56
|
-
const result = createGateResult(parsed.options, io.now(), diff);
|
|
76
|
+
const diff = io.readDiff(parsed.options.base, { staged: parsed.options.staged });
|
|
77
|
+
const result = createGateResult(parsed.options, io.now(), diff, io);
|
|
57
78
|
const rendered = command === "hook"
|
|
58
79
|
? renderReport(result.summary.decision === "pass" ? { ...result, findings: [] } : result, "repair")
|
|
59
80
|
: renderReport(result, parsed.options.format);
|
|
@@ -68,7 +89,8 @@ function runCli(argv, io) {
|
|
|
68
89
|
function parseCheckArgs(args, command) {
|
|
69
90
|
const options = {
|
|
70
91
|
format: "markdown",
|
|
71
|
-
strict: false
|
|
92
|
+
strict: false,
|
|
93
|
+
staged: false
|
|
72
94
|
};
|
|
73
95
|
for (let index = 0; index < args.length; index += 1) {
|
|
74
96
|
const arg = args[index];
|
|
@@ -76,7 +98,15 @@ function parseCheckArgs(args, command) {
|
|
|
76
98
|
options.strict = true;
|
|
77
99
|
continue;
|
|
78
100
|
}
|
|
79
|
-
if (arg === "--
|
|
101
|
+
if (arg === "--staged") {
|
|
102
|
+
options.staged = true;
|
|
103
|
+
continue;
|
|
104
|
+
}
|
|
105
|
+
if (arg === "--task" ||
|
|
106
|
+
arg === "--base" ||
|
|
107
|
+
arg === "--format" ||
|
|
108
|
+
arg === "--output" ||
|
|
109
|
+
arg === "--fail-on") {
|
|
80
110
|
const value = args[index + 1];
|
|
81
111
|
if (value === undefined || value.startsWith("--")) {
|
|
82
112
|
return { ok: false, error: `Missing value for ${arg}.` };
|
|
@@ -92,11 +122,20 @@ function parseCheckArgs(args, command) {
|
|
|
92
122
|
if (!isReportFormat(value)) {
|
|
93
123
|
return {
|
|
94
124
|
ok: false,
|
|
95
|
-
error: "Invalid --format value. Expected json, markdown, sarif, or
|
|
125
|
+
error: "Invalid --format value. Expected json, markdown, sarif, repair, or pr-comment."
|
|
96
126
|
};
|
|
97
127
|
}
|
|
98
128
|
options.format = value;
|
|
99
129
|
}
|
|
130
|
+
else if (arg === "--fail-on") {
|
|
131
|
+
if (!isFailOnSeverity(value)) {
|
|
132
|
+
return {
|
|
133
|
+
ok: false,
|
|
134
|
+
error: "Invalid --fail-on value. Expected blocker, high, or medium."
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
options.failOn = value;
|
|
138
|
+
}
|
|
100
139
|
else if (arg === "--output") {
|
|
101
140
|
options.output = value;
|
|
102
141
|
}
|
|
@@ -122,12 +161,17 @@ function parseCheckArgs(args, command) {
|
|
|
122
161
|
base: options.base,
|
|
123
162
|
format: options.format ?? "markdown",
|
|
124
163
|
strict: options.strict ?? false,
|
|
164
|
+
staged: options.staged ?? false,
|
|
165
|
+
failOn: options.failOn,
|
|
125
166
|
output: options.output
|
|
126
167
|
}
|
|
127
168
|
};
|
|
128
169
|
}
|
|
129
|
-
function createGateResult(options, generatedAt, diffResult) {
|
|
130
|
-
const configResult = loadCriticalGateConfig(diffResult.root
|
|
170
|
+
function createGateResult(options, generatedAt, diffResult, io = {}) {
|
|
171
|
+
const configResult = loadCriticalGateConfig(diffResult.root, {
|
|
172
|
+
exists: io.exists,
|
|
173
|
+
readFile: io.readFile
|
|
174
|
+
});
|
|
131
175
|
const task = {
|
|
132
176
|
source: "cli",
|
|
133
177
|
text: options.task
|
|
@@ -137,9 +181,19 @@ function createGateResult(options, generatedAt, diffResult) {
|
|
|
137
181
|
headRef: diffResult.headRef,
|
|
138
182
|
files: diffResult.files
|
|
139
183
|
};
|
|
184
|
+
const frameworkPacks = detectFrameworkPacks({
|
|
185
|
+
files: diff.files,
|
|
186
|
+
packageJson: readOptionalPackageJson(diffResult.root, io),
|
|
187
|
+
config: configResult.config
|
|
188
|
+
});
|
|
189
|
+
const monorepo = detectMonorepoContext(diffResult.root, diff.files, io);
|
|
190
|
+
const apiSnapshot = loadApiSurfaceSnapshot(diffResult.root, io);
|
|
140
191
|
const context = {
|
|
141
192
|
root: diffResult.root,
|
|
142
193
|
packageManager: "pnpm",
|
|
194
|
+
monorepo,
|
|
195
|
+
apiSnapshot: summarizeApiSurfaceSnapshot(apiSnapshot),
|
|
196
|
+
frameworkPacks: frameworkPacks.map((pack) => pack.id),
|
|
143
197
|
repositoryProfile: diffResult.repositoryProfile,
|
|
144
198
|
utilityIndex: diffResult.utilityIndex,
|
|
145
199
|
git: {
|
|
@@ -149,9 +203,15 @@ function createGateResult(options, generatedAt, diffResult) {
|
|
|
149
203
|
};
|
|
150
204
|
const detectorContext = {
|
|
151
205
|
...context,
|
|
206
|
+
apiSurfaceSnapshot: apiSnapshot,
|
|
152
207
|
knowledge: diffResult.knowledge
|
|
153
208
|
};
|
|
154
|
-
const
|
|
209
|
+
const detectorFindings = runDetectors(task, diff, detectorContext);
|
|
210
|
+
const learningResult = applyLearningPolicy(detectorFindings, diff.files, {
|
|
211
|
+
...configResult.config.learning,
|
|
212
|
+
expectedSupportFiles: getConfiguredExpectedSupportFiles(configResult.config)
|
|
213
|
+
});
|
|
214
|
+
const findings = learningResult.findings;
|
|
155
215
|
const loadedHistoryIndex = diffResult.knowledge?.getLoadedHistoryIndex?.();
|
|
156
216
|
const loadedSolutionIndex = diffResult.knowledge?.getLoadedSolutionIndex?.();
|
|
157
217
|
context.repositoryProfile ??= loadedHistoryIndex?.profile;
|
|
@@ -163,28 +223,214 @@ function createGateResult(options, generatedAt, diffResult) {
|
|
|
163
223
|
diff,
|
|
164
224
|
context,
|
|
165
225
|
findings,
|
|
166
|
-
summary: summarizeFindings(findings, task, diff,
|
|
226
|
+
summary: summarizeFindings(findings, task, diff, {
|
|
227
|
+
observationDetectors: getPolicyObservationDetectors(configResult.config),
|
|
228
|
+
blockingDetectors: getPolicyBlockingDetectors(configResult.config),
|
|
229
|
+
failOn: options.failOn ?? getConfiguredFailOn(configResult.config)
|
|
230
|
+
}),
|
|
167
231
|
intentVerification: summarizeIntentVerification(task, diff.files),
|
|
232
|
+
intentQuality: analyzeTaskIntentQuality(task),
|
|
168
233
|
metadata: {
|
|
169
234
|
cliVersion: CLI_VERSION,
|
|
170
235
|
strict: options.strict,
|
|
236
|
+
staged: options.staged,
|
|
237
|
+
failOn: options.failOn ?? getConfiguredFailOn(configResult.config) ?? "high",
|
|
171
238
|
rolloutPolicy: configResult.config.rollout,
|
|
239
|
+
policy: configResult.config.policy,
|
|
240
|
+
frameworkPacks: frameworkPacks.map((pack) => pack.id),
|
|
241
|
+
learning: {
|
|
242
|
+
acceptedFindingsApplied: learningResult.appliedAcceptedFindings,
|
|
243
|
+
expectedSupportRulesApplied: learningResult.appliedExpectedSupportRules
|
|
244
|
+
},
|
|
172
245
|
configWarnings: configResult.warnings
|
|
173
246
|
}
|
|
174
247
|
};
|
|
175
248
|
}
|
|
249
|
+
function runSnapshotApiCommand(args, io) {
|
|
250
|
+
const parsed = parseSnapshotApiArgs(args);
|
|
251
|
+
if (!parsed.ok) {
|
|
252
|
+
io.stderr(parsed.error);
|
|
253
|
+
io.stderr("Run critical-gate snapshot-api --help for usage.");
|
|
254
|
+
return ExitCode.UsageError;
|
|
255
|
+
}
|
|
256
|
+
const root = io.readDiff().root;
|
|
257
|
+
const snapshot = buildApiSurfaceSnapshot({
|
|
258
|
+
root,
|
|
259
|
+
generatedAt: io.now(),
|
|
260
|
+
entrypoints: parsed.options.entrypoints,
|
|
261
|
+
reader: io
|
|
262
|
+
});
|
|
263
|
+
const outputPath = getApiSnapshotOutputPath(root, parsed.options.output);
|
|
264
|
+
io.writeFile(outputPath, `${JSON.stringify(snapshot, null, 2)}\n`);
|
|
265
|
+
io.stdout(`Wrote public API snapshot to ${outputPath} (${snapshot.exports.length} exports across ${snapshot.entrypoints.length} entrypoints).`);
|
|
266
|
+
return ExitCode.Pass;
|
|
267
|
+
}
|
|
268
|
+
function runInstallHooksCommand(args, io) {
|
|
269
|
+
const parsed = parseInstallHooksArgs(args);
|
|
270
|
+
if (!parsed.ok) {
|
|
271
|
+
io.stderr(parsed.error);
|
|
272
|
+
io.stderr("Run critical-gate install-hooks --help for usage.");
|
|
273
|
+
return ExitCode.UsageError;
|
|
274
|
+
}
|
|
275
|
+
const root = io.readDiff().root;
|
|
276
|
+
const hooks = parsed.options.hook === "all" ? ["pre-commit", "pre-push"] : [parsed.options.hook];
|
|
277
|
+
const installed = [];
|
|
278
|
+
for (const hook of hooks) {
|
|
279
|
+
const path = join(root, ".git", "hooks", hook);
|
|
280
|
+
if (io.exists?.(path) === true && parsed.options.force !== true) {
|
|
281
|
+
io.stderr(`Refusing to overwrite existing ${hook} hook at ${path}. Re-run with --force.`);
|
|
282
|
+
return ExitCode.UsageError;
|
|
283
|
+
}
|
|
284
|
+
io.writeFile(path, renderGitHookScript(hook, parsed.options.cli));
|
|
285
|
+
io.chmodFile?.(path, 0o755);
|
|
286
|
+
installed.push(path);
|
|
287
|
+
}
|
|
288
|
+
io.stdout(`Installed Critical Gate hook(s): ${installed.join(", ")}`);
|
|
289
|
+
return ExitCode.Pass;
|
|
290
|
+
}
|
|
291
|
+
function runInitPolicyCommand(args, io) {
|
|
292
|
+
const parsed = parseInitPolicyArgs(args);
|
|
293
|
+
if (!parsed.ok) {
|
|
294
|
+
io.stderr(parsed.error);
|
|
295
|
+
io.stderr("Run critical-gate init-policy --help for usage.");
|
|
296
|
+
return ExitCode.UsageError;
|
|
297
|
+
}
|
|
298
|
+
const root = io.readDiff().root;
|
|
299
|
+
const path = join(root, CRITICAL_GATE_CONFIG_FILE);
|
|
300
|
+
if (io.exists?.(path) === true && parsed.options.force !== true) {
|
|
301
|
+
io.stderr(`Refusing to overwrite existing ${CRITICAL_GATE_CONFIG_FILE}. Re-run with --force.`);
|
|
302
|
+
return ExitCode.UsageError;
|
|
303
|
+
}
|
|
304
|
+
io.writeFile(path, `${JSON.stringify(createDefaultPolicyConfig(io.now()), null, 2)}\n`);
|
|
305
|
+
io.stdout(`Wrote reviewable Critical Gate policy to ${path}.`);
|
|
306
|
+
return ExitCode.Pass;
|
|
307
|
+
}
|
|
308
|
+
function readOptionalPackageJson(root, io) {
|
|
309
|
+
const path = join(root, "package.json");
|
|
310
|
+
if (io.exists?.(path) !== true) {
|
|
311
|
+
return undefined;
|
|
312
|
+
}
|
|
313
|
+
try {
|
|
314
|
+
return JSON.parse(io.readFile?.(path) ?? "");
|
|
315
|
+
}
|
|
316
|
+
catch {
|
|
317
|
+
return undefined;
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
function runAcceptCommand(args, io) {
|
|
321
|
+
const parsed = parseFlagArgs(args, ["--finding", "--reason"]);
|
|
322
|
+
if (!parsed.ok) {
|
|
323
|
+
io.stderr(parsed.error);
|
|
324
|
+
io.stderr("Run critical-gate accept --help for usage.");
|
|
325
|
+
return ExitCode.UsageError;
|
|
326
|
+
}
|
|
327
|
+
const finding = parsed.values["--finding"];
|
|
328
|
+
const reason = parsed.values["--reason"];
|
|
329
|
+
if (finding === undefined || reason === undefined) {
|
|
330
|
+
io.stderr("Missing required --finding or --reason value.");
|
|
331
|
+
io.stderr("Run critical-gate accept --help for usage.");
|
|
332
|
+
return ExitCode.UsageError;
|
|
333
|
+
}
|
|
334
|
+
const root = io.readDiff().root;
|
|
335
|
+
const updated = updateCriticalGateConfig(root, (config) => ({
|
|
336
|
+
...config,
|
|
337
|
+
learning: {
|
|
338
|
+
...config.learning,
|
|
339
|
+
acceptedFindings: upsertById(config.learning?.acceptedFindings ?? [], {
|
|
340
|
+
id: finding,
|
|
341
|
+
reason,
|
|
342
|
+
createdAt: io.now().toISOString()
|
|
343
|
+
})
|
|
344
|
+
}
|
|
345
|
+
}), {
|
|
346
|
+
exists: io.exists,
|
|
347
|
+
readFile: io.readFile,
|
|
348
|
+
writeFile: io.writeFile
|
|
349
|
+
});
|
|
350
|
+
io.stdout(`Accepted finding ${finding} in ${CRITICAL_GATE_CONFIG_FILE} (${updated.learning?.acceptedFindings?.length ?? 0} accepted finding rules).`);
|
|
351
|
+
return ExitCode.Pass;
|
|
352
|
+
}
|
|
353
|
+
function runTeachCommand(args, io) {
|
|
354
|
+
const parsed = parseFlagArgs(args, ["--id", "--when-changed", "--allow", "--reason"]);
|
|
355
|
+
if (!parsed.ok) {
|
|
356
|
+
io.stderr(parsed.error);
|
|
357
|
+
io.stderr("Run critical-gate teach --help for usage.");
|
|
358
|
+
return ExitCode.UsageError;
|
|
359
|
+
}
|
|
360
|
+
const id = parsed.values["--id"];
|
|
361
|
+
const whenChanged = parsed.values["--when-changed"];
|
|
362
|
+
const allow = parsed.values["--allow"]
|
|
363
|
+
?.split(",")
|
|
364
|
+
.map((entry) => entry.trim())
|
|
365
|
+
.filter(Boolean);
|
|
366
|
+
const reason = parsed.values["--reason"];
|
|
367
|
+
if (id === undefined ||
|
|
368
|
+
whenChanged === undefined ||
|
|
369
|
+
allow === undefined ||
|
|
370
|
+
reason === undefined) {
|
|
371
|
+
io.stderr("Missing required --id, --when-changed, --allow, or --reason value.");
|
|
372
|
+
io.stderr("Run critical-gate teach --help for usage.");
|
|
373
|
+
return ExitCode.UsageError;
|
|
374
|
+
}
|
|
375
|
+
const root = io.readDiff().root;
|
|
376
|
+
const updated = updateCriticalGateConfig(root, (config) => ({
|
|
377
|
+
...config,
|
|
378
|
+
learning: {
|
|
379
|
+
...config.learning,
|
|
380
|
+
expectedSupportFiles: upsertById(config.learning?.expectedSupportFiles ?? [], {
|
|
381
|
+
id,
|
|
382
|
+
whenChanged,
|
|
383
|
+
allow,
|
|
384
|
+
reason,
|
|
385
|
+
createdAt: io.now().toISOString()
|
|
386
|
+
})
|
|
387
|
+
}
|
|
388
|
+
}), {
|
|
389
|
+
exists: io.exists,
|
|
390
|
+
readFile: io.readFile,
|
|
391
|
+
writeFile: io.writeFile
|
|
392
|
+
});
|
|
393
|
+
io.stdout(`Taught expected support rule ${id} in ${CRITICAL_GATE_CONFIG_FILE} (${updated.learning?.expectedSupportFiles?.length ?? 0} support rules).`);
|
|
394
|
+
return ExitCode.Pass;
|
|
395
|
+
}
|
|
176
396
|
function getHelpText() {
|
|
177
397
|
return [
|
|
178
398
|
"critical-gate",
|
|
179
399
|
"",
|
|
180
400
|
"Usage:",
|
|
181
|
-
" critical-gate check --task <text> [--base <ref>] [--format json|markdown|sarif|repair] [--strict] [--output <path>]",
|
|
401
|
+
" critical-gate check --task <text> [--base <ref>] [--format json|markdown|sarif|repair|pr-comment] [--strict] [--output <path>]",
|
|
182
402
|
" critical-gate hook [--task <text>] [--base <ref>] [--output <path>]",
|
|
403
|
+
" critical-gate accept --finding <id> --reason <text>",
|
|
404
|
+
" critical-gate teach --id <id> --when-changed <glob> --allow <glob[,glob]> --reason <text>",
|
|
405
|
+
" critical-gate snapshot-api [--entrypoint <path>] [--output <path>]",
|
|
406
|
+
" critical-gate install-hooks [--hook pre-commit|pre-push|all] [--cli <command>] [--force]",
|
|
407
|
+
" critical-gate init-policy [--force]",
|
|
183
408
|
" critical-gate --version",
|
|
184
409
|
" critical-gate --help",
|
|
185
410
|
""
|
|
186
411
|
].join("\n");
|
|
187
412
|
}
|
|
413
|
+
function getCommandHelpText(command) {
|
|
414
|
+
if (command === "hook") {
|
|
415
|
+
return getHookHelpText();
|
|
416
|
+
}
|
|
417
|
+
if (command === "accept") {
|
|
418
|
+
return getAcceptHelpText();
|
|
419
|
+
}
|
|
420
|
+
if (command === "teach") {
|
|
421
|
+
return getTeachHelpText();
|
|
422
|
+
}
|
|
423
|
+
if (command === "snapshot-api") {
|
|
424
|
+
return getSnapshotApiHelpText();
|
|
425
|
+
}
|
|
426
|
+
if (command === "install-hooks") {
|
|
427
|
+
return getInstallHooksHelpText();
|
|
428
|
+
}
|
|
429
|
+
if (command === "init-policy") {
|
|
430
|
+
return getInitPolicyHelpText();
|
|
431
|
+
}
|
|
432
|
+
return getCheckHelpText();
|
|
433
|
+
}
|
|
188
434
|
function getCheckHelpText() {
|
|
189
435
|
return [
|
|
190
436
|
"critical-gate check",
|
|
@@ -194,7 +440,9 @@ function getCheckHelpText() {
|
|
|
194
440
|
"",
|
|
195
441
|
"Options:",
|
|
196
442
|
" --base <ref> Git baseline reference",
|
|
197
|
-
" --format <format> json, markdown, sarif, or
|
|
443
|
+
" --format <format> json, markdown, sarif, repair, or pr-comment",
|
|
444
|
+
" --fail-on <level> blocker, high, or medium; defaults to high",
|
|
445
|
+
" --staged Analyze staged changes with git diff --cached",
|
|
198
446
|
" --strict Fail on strict-mode findings once detectors exist",
|
|
199
447
|
" --output <path> Write report to a file instead of stdout",
|
|
200
448
|
""
|
|
@@ -211,14 +459,228 @@ function getHookHelpText() {
|
|
|
211
459
|
""
|
|
212
460
|
].join("\n");
|
|
213
461
|
}
|
|
462
|
+
function getAcceptHelpText() {
|
|
463
|
+
return [
|
|
464
|
+
"critical-gate accept",
|
|
465
|
+
"",
|
|
466
|
+
"Records an explicit accepted finding in .critical-gate.json.",
|
|
467
|
+
"",
|
|
468
|
+
"Required:",
|
|
469
|
+
" --finding <id> Exact finding id to accept",
|
|
470
|
+
" --reason <text> Reviewable reason for accepting the finding",
|
|
471
|
+
""
|
|
472
|
+
].join("\n");
|
|
473
|
+
}
|
|
474
|
+
function getTeachHelpText() {
|
|
475
|
+
return [
|
|
476
|
+
"critical-gate teach",
|
|
477
|
+
"",
|
|
478
|
+
"Records a repository-specific expected support-file rule in .critical-gate.json.",
|
|
479
|
+
"",
|
|
480
|
+
"Required:",
|
|
481
|
+
" --id <id> Stable rule id",
|
|
482
|
+
" --when-changed <glob> Changed file glob that activates the rule",
|
|
483
|
+
" --allow <glob[,glob]> Support-file glob or comma-separated globs to allow",
|
|
484
|
+
" --reason <text> Reviewable reason for the rule",
|
|
485
|
+
""
|
|
486
|
+
].join("\n");
|
|
487
|
+
}
|
|
488
|
+
function getSnapshotApiHelpText() {
|
|
489
|
+
return [
|
|
490
|
+
"critical-gate snapshot-api",
|
|
491
|
+
"",
|
|
492
|
+
"Generates a reviewable public API surface snapshot.",
|
|
493
|
+
"",
|
|
494
|
+
"Options:",
|
|
495
|
+
" --entrypoint <path> Public entrypoint to snapshot; may be passed more than once",
|
|
496
|
+
" --output <path> Snapshot path; defaults to .critical-gate/api-surface.json",
|
|
497
|
+
""
|
|
498
|
+
].join("\n");
|
|
499
|
+
}
|
|
500
|
+
function getInstallHooksHelpText() {
|
|
501
|
+
return [
|
|
502
|
+
"critical-gate install-hooks",
|
|
503
|
+
"",
|
|
504
|
+
"Installs reviewable local git hooks.",
|
|
505
|
+
"",
|
|
506
|
+
"Options:",
|
|
507
|
+
" --hook <hook> pre-commit, pre-push, or all; defaults to all",
|
|
508
|
+
" --cli <command> Critical Gate command/path used by the hook; defaults to critical-gate",
|
|
509
|
+
" --force Overwrite an existing generated or custom hook",
|
|
510
|
+
"",
|
|
511
|
+
"Defaults:",
|
|
512
|
+
" pre-commit runs staged changes with --fail-on blocker.",
|
|
513
|
+
" pre-push runs branch changes against ${CRITICAL_GATE_BASE:-origin/main} with --fail-on high.",
|
|
514
|
+
""
|
|
515
|
+
].join("\n");
|
|
516
|
+
}
|
|
517
|
+
function getInitPolicyHelpText() {
|
|
518
|
+
return [
|
|
519
|
+
"critical-gate init-policy",
|
|
520
|
+
"",
|
|
521
|
+
`Writes a starter ${CRITICAL_GATE_CONFIG_FILE} policy file.`,
|
|
522
|
+
"",
|
|
523
|
+
"Options:",
|
|
524
|
+
" --force Overwrite an existing policy file",
|
|
525
|
+
""
|
|
526
|
+
].join("\n");
|
|
527
|
+
}
|
|
214
528
|
function isCommandName(value) {
|
|
215
|
-
return value === "check" ||
|
|
529
|
+
return (value === "check" ||
|
|
530
|
+
value === "hook" ||
|
|
531
|
+
value === "accept" ||
|
|
532
|
+
value === "teach" ||
|
|
533
|
+
value === "snapshot-api" ||
|
|
534
|
+
value === "install-hooks" ||
|
|
535
|
+
value === "init-policy");
|
|
216
536
|
}
|
|
217
537
|
function isReportFormat(value) {
|
|
218
|
-
return value === "json" ||
|
|
538
|
+
return (value === "json" ||
|
|
539
|
+
value === "markdown" ||
|
|
540
|
+
value === "sarif" ||
|
|
541
|
+
value === "repair" ||
|
|
542
|
+
value === "pr-comment");
|
|
543
|
+
}
|
|
544
|
+
function isFailOnSeverity(value) {
|
|
545
|
+
return value === "blocker" || value === "high" || value === "medium";
|
|
546
|
+
}
|
|
547
|
+
function parseFlagArgs(args, allowedFlags) {
|
|
548
|
+
const values = {};
|
|
549
|
+
for (let index = 0; index < args.length; index += 1) {
|
|
550
|
+
const arg = args[index];
|
|
551
|
+
if (!allowedFlags.includes(arg)) {
|
|
552
|
+
return { ok: false, error: `Unknown option: ${arg}.` };
|
|
553
|
+
}
|
|
554
|
+
const value = args[index + 1];
|
|
555
|
+
if (value === undefined || value.startsWith("--")) {
|
|
556
|
+
return { ok: false, error: `Missing value for ${arg}.` };
|
|
557
|
+
}
|
|
558
|
+
values[arg] = value;
|
|
559
|
+
index += 1;
|
|
560
|
+
}
|
|
561
|
+
return { ok: true, values };
|
|
562
|
+
}
|
|
563
|
+
function parseSnapshotApiArgs(args) {
|
|
564
|
+
const options = {
|
|
565
|
+
entrypoints: []
|
|
566
|
+
};
|
|
567
|
+
for (let index = 0; index < args.length; index += 1) {
|
|
568
|
+
const arg = args[index];
|
|
569
|
+
if (arg !== "--entrypoint" && arg !== "--output") {
|
|
570
|
+
return { ok: false, error: `Unknown option: ${arg}.` };
|
|
571
|
+
}
|
|
572
|
+
const value = args[index + 1];
|
|
573
|
+
if (value === undefined || value.startsWith("--")) {
|
|
574
|
+
return { ok: false, error: `Missing value for ${arg}.` };
|
|
575
|
+
}
|
|
576
|
+
if (arg === "--entrypoint") {
|
|
577
|
+
options.entrypoints.push(value);
|
|
578
|
+
}
|
|
579
|
+
else {
|
|
580
|
+
options.output = value;
|
|
581
|
+
}
|
|
582
|
+
index += 1;
|
|
583
|
+
}
|
|
584
|
+
return {
|
|
585
|
+
ok: true,
|
|
586
|
+
options: {
|
|
587
|
+
output: options.output,
|
|
588
|
+
entrypoints: options.entrypoints.length > 0 ? options.entrypoints : undefined
|
|
589
|
+
}
|
|
590
|
+
};
|
|
591
|
+
}
|
|
592
|
+
function parseInstallHooksArgs(args) {
|
|
593
|
+
const options = {
|
|
594
|
+
hook: "all",
|
|
595
|
+
cli: "critical-gate",
|
|
596
|
+
force: false
|
|
597
|
+
};
|
|
598
|
+
for (let index = 0; index < args.length; index += 1) {
|
|
599
|
+
const arg = args[index];
|
|
600
|
+
if (arg === "--force") {
|
|
601
|
+
options.force = true;
|
|
602
|
+
continue;
|
|
603
|
+
}
|
|
604
|
+
if (arg !== "--hook" && arg !== "--cli") {
|
|
605
|
+
return { ok: false, error: `Unknown option: ${arg}.` };
|
|
606
|
+
}
|
|
607
|
+
const value = args[index + 1];
|
|
608
|
+
if (value === undefined || value.startsWith("--")) {
|
|
609
|
+
return { ok: false, error: `Missing value for ${arg}.` };
|
|
610
|
+
}
|
|
611
|
+
if (arg === "--hook") {
|
|
612
|
+
if (value !== "pre-commit" && value !== "pre-push" && value !== "all") {
|
|
613
|
+
return { ok: false, error: "Invalid --hook value. Expected pre-commit, pre-push, or all." };
|
|
614
|
+
}
|
|
615
|
+
options.hook = value;
|
|
616
|
+
}
|
|
617
|
+
else {
|
|
618
|
+
options.cli = value;
|
|
619
|
+
}
|
|
620
|
+
index += 1;
|
|
621
|
+
}
|
|
622
|
+
return { ok: true, options };
|
|
623
|
+
}
|
|
624
|
+
function parseInitPolicyArgs(args) {
|
|
625
|
+
const options = {
|
|
626
|
+
force: false
|
|
627
|
+
};
|
|
628
|
+
for (const arg of args) {
|
|
629
|
+
if (arg !== "--force") {
|
|
630
|
+
return { ok: false, error: `Unknown option: ${arg}.` };
|
|
631
|
+
}
|
|
632
|
+
options.force = true;
|
|
633
|
+
}
|
|
634
|
+
return { ok: true, options };
|
|
635
|
+
}
|
|
636
|
+
function renderGitHookScript(hook, cliCommand) {
|
|
637
|
+
const quotedCli = shellQuote(cliCommand);
|
|
638
|
+
if (hook === "pre-commit") {
|
|
639
|
+
return [
|
|
640
|
+
"#!/bin/sh",
|
|
641
|
+
"# Critical Gate pre-commit hook.",
|
|
642
|
+
"# Generated by `critical-gate install-hooks`; review before trusting.",
|
|
643
|
+
"set -eu",
|
|
644
|
+
'TASK="${CRITICAL_GATE_TASK:-Pre-commit staged change}"',
|
|
645
|
+
`${quotedCli} check --staged --task "$TASK" --format repair --fail-on blocker`,
|
|
646
|
+
""
|
|
647
|
+
].join("\n");
|
|
648
|
+
}
|
|
649
|
+
return [
|
|
650
|
+
"#!/bin/sh",
|
|
651
|
+
"# Critical Gate pre-push hook.",
|
|
652
|
+
"# Generated by `critical-gate install-hooks`; review before trusting.",
|
|
653
|
+
"set -eu",
|
|
654
|
+
'TASK="${CRITICAL_GATE_TASK:-Pre-push branch change}"',
|
|
655
|
+
'BASE="${CRITICAL_GATE_BASE:-origin/main}"',
|
|
656
|
+
`${quotedCli} check --base "$BASE" --task "$TASK" --format repair --fail-on high`,
|
|
657
|
+
""
|
|
658
|
+
].join("\n");
|
|
659
|
+
}
|
|
660
|
+
function shellQuote(value) {
|
|
661
|
+
if (/^[A-Za-z0-9_./:-]+$/.test(value)) {
|
|
662
|
+
return value;
|
|
663
|
+
}
|
|
664
|
+
return `'${value.replaceAll("'", "'\\''")}'`;
|
|
665
|
+
}
|
|
666
|
+
function upsertById(entries, next) {
|
|
667
|
+
return [...entries.filter((entry) => entry.id !== next.id), next];
|
|
668
|
+
}
|
|
669
|
+
export function isCliEntrypoint(importMetaUrl, argvPath = process.argv[1]) {
|
|
670
|
+
return (argvPath !== undefined &&
|
|
671
|
+
normalizeExecutablePath(fileURLToPath(importMetaUrl)) === normalizeExecutablePath(argvPath));
|
|
672
|
+
}
|
|
673
|
+
function normalizeExecutablePath(path) {
|
|
674
|
+
let normalized = resolve(path);
|
|
675
|
+
try {
|
|
676
|
+
normalized = realpathSync.native(normalized);
|
|
677
|
+
}
|
|
678
|
+
catch {
|
|
679
|
+
// Fall back to the resolved path when the target is virtual or has already been removed.
|
|
680
|
+
}
|
|
681
|
+
return process.platform === "win32" ? normalized.toLowerCase() : normalized;
|
|
219
682
|
}
|
|
220
|
-
|
|
221
|
-
if (isDirectRun) {
|
|
683
|
+
if (isCliEntrypoint(import.meta.url)) {
|
|
222
684
|
process.exitCode = main();
|
|
223
685
|
}
|
|
224
686
|
//# sourceMappingURL=cli.js.map
|