@neurcode-ai/cli 0.9.41 → 0.9.43
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 +9 -3
- package/dist/api-client.d.ts +14 -0
- package/dist/api-client.d.ts.map +1 -1
- package/dist/api-client.js +10 -0
- package/dist/api-client.js.map +1 -1
- package/dist/commands/bootstrap.d.ts.map +1 -1
- package/dist/commands/bootstrap.js +16 -89
- package/dist/commands/bootstrap.js.map +1 -1
- package/dist/commands/contract.d.ts.map +1 -1
- package/dist/commands/contract.js +179 -20
- package/dist/commands/contract.js.map +1 -1
- package/dist/commands/doctor.d.ts +13 -3
- package/dist/commands/doctor.d.ts.map +1 -1
- package/dist/commands/doctor.js +476 -95
- package/dist/commands/doctor.js.map +1 -1
- package/dist/commands/feedback.d.ts.map +1 -1
- package/dist/commands/feedback.js +68 -0
- package/dist/commands/feedback.js.map +1 -1
- package/dist/commands/remediate.d.ts +8 -0
- package/dist/commands/remediate.d.ts.map +1 -1
- package/dist/commands/remediate.js +485 -103
- package/dist/commands/remediate.js.map +1 -1
- package/dist/commands/verify.d.ts.map +1 -1
- package/dist/commands/verify.js +361 -6
- package/dist/commands/verify.js.map +1 -1
- package/dist/index.js +23 -3
- package/dist/index.js.map +1 -1
- package/dist/utils/cli-json.d.ts +54 -0
- package/dist/utils/cli-json.d.ts.map +1 -0
- package/dist/utils/cli-json.js +152 -0
- package/dist/utils/cli-json.js.map +1 -0
- package/dist/utils/policy-compiler.d.ts +3 -1
- package/dist/utils/policy-compiler.d.ts.map +1 -1
- package/dist/utils/policy-compiler.js +8 -0
- package/dist/utils/policy-compiler.js.map +1 -1
- package/dist/utils/runtime-guard.d.ts +3 -1
- package/dist/utils/runtime-guard.d.ts.map +1 -1
- package/dist/utils/runtime-guard.js +8 -0
- package/dist/utils/runtime-guard.js.map +1 -1
- package/package.json +1 -1
|
@@ -1,101 +1,18 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.remediateCommand = remediateCommand;
|
|
4
|
+
const crypto_1 = require("crypto");
|
|
4
5
|
const child_process_1 = require("child_process");
|
|
5
6
|
const fs_1 = require("fs");
|
|
7
|
+
const path_1 = require("path");
|
|
6
8
|
const project_root_1 = require("../utils/project-root");
|
|
9
|
+
const manual_approvals_1 = require("../utils/manual-approvals");
|
|
7
10
|
const core_1 = require("@neurcode-ai/core");
|
|
8
11
|
const analysis_1 = require("@neurcode-ai/analysis");
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
chalk = require('chalk');
|
|
12
|
-
}
|
|
13
|
-
catch {
|
|
14
|
-
chalk = {
|
|
15
|
-
green: (value) => value,
|
|
16
|
-
yellow: (value) => value,
|
|
17
|
-
red: (value) => value,
|
|
18
|
-
bold: (value) => value,
|
|
19
|
-
dim: (value) => value,
|
|
20
|
-
cyan: (value) => value,
|
|
21
|
-
};
|
|
22
|
-
}
|
|
12
|
+
const cli_json_1 = require("../utils/cli-json");
|
|
13
|
+
const chalk = (0, cli_json_1.loadChalk)();
|
|
23
14
|
function emitJson(payload) {
|
|
24
|
-
|
|
25
|
-
}
|
|
26
|
-
function stripAnsi(value) {
|
|
27
|
-
return value.replace(/\u001b\[[0-9;]*m/g, '');
|
|
28
|
-
}
|
|
29
|
-
function extractLastJsonObject(output) {
|
|
30
|
-
const clean = stripAnsi(output).trim();
|
|
31
|
-
const end = clean.lastIndexOf('}');
|
|
32
|
-
if (end === -1)
|
|
33
|
-
return null;
|
|
34
|
-
for (let start = end; start >= 0; start -= 1) {
|
|
35
|
-
if (clean[start] !== '{')
|
|
36
|
-
continue;
|
|
37
|
-
const candidate = clean.slice(start, end + 1);
|
|
38
|
-
try {
|
|
39
|
-
const parsed = JSON.parse(candidate);
|
|
40
|
-
if (parsed && typeof parsed === 'object' && !Array.isArray(parsed)) {
|
|
41
|
-
return parsed;
|
|
42
|
-
}
|
|
43
|
-
}
|
|
44
|
-
catch {
|
|
45
|
-
// Keep searching.
|
|
46
|
-
}
|
|
47
|
-
}
|
|
48
|
-
return null;
|
|
49
|
-
}
|
|
50
|
-
function asString(record, key) {
|
|
51
|
-
if (!record)
|
|
52
|
-
return null;
|
|
53
|
-
const value = record[key];
|
|
54
|
-
return typeof value === 'string' ? value : null;
|
|
55
|
-
}
|
|
56
|
-
function asNumber(record, key) {
|
|
57
|
-
if (!record)
|
|
58
|
-
return null;
|
|
59
|
-
const value = record[key];
|
|
60
|
-
return typeof value === 'number' && Number.isFinite(value) ? value : null;
|
|
61
|
-
}
|
|
62
|
-
function asViolationsCount(record) {
|
|
63
|
-
if (!record)
|
|
64
|
-
return 0;
|
|
65
|
-
const value = record.violations;
|
|
66
|
-
if (!Array.isArray(value))
|
|
67
|
-
return 0;
|
|
68
|
-
return value.length;
|
|
69
|
-
}
|
|
70
|
-
async function runCliJson(commandArgs) {
|
|
71
|
-
const args = commandArgs.includes('--json') ? [...commandArgs] : [...commandArgs, '--json'];
|
|
72
|
-
const stdoutChunks = [];
|
|
73
|
-
const stderrChunks = [];
|
|
74
|
-
const exitCode = await new Promise((resolvePromise, reject) => {
|
|
75
|
-
const child = (0, child_process_1.spawn)(process.execPath, [process.argv[1], ...args], {
|
|
76
|
-
cwd: process.cwd(),
|
|
77
|
-
env: {
|
|
78
|
-
...process.env,
|
|
79
|
-
CI: process.env.CI || 'true',
|
|
80
|
-
FORCE_COLOR: '0',
|
|
81
|
-
},
|
|
82
|
-
stdio: ['ignore', 'pipe', 'pipe'],
|
|
83
|
-
});
|
|
84
|
-
child.stdout.on('data', (chunk) => stdoutChunks.push(String(chunk)));
|
|
85
|
-
child.stderr.on('data', (chunk) => stderrChunks.push(String(chunk)));
|
|
86
|
-
child.on('error', (error) => reject(error));
|
|
87
|
-
child.on('close', (code) => resolvePromise(typeof code === 'number' ? code : 1));
|
|
88
|
-
});
|
|
89
|
-
const stdout = stdoutChunks.join('');
|
|
90
|
-
const stderr = stderrChunks.join('');
|
|
91
|
-
const payload = extractLastJsonObject(`${stdout}\n${stderr}`);
|
|
92
|
-
return {
|
|
93
|
-
exitCode,
|
|
94
|
-
stdout,
|
|
95
|
-
stderr,
|
|
96
|
-
payload,
|
|
97
|
-
command: args,
|
|
98
|
-
};
|
|
15
|
+
(0, cli_json_1.emitJson)(payload);
|
|
99
16
|
}
|
|
100
17
|
function resolveStrictArtifacts(options) {
|
|
101
18
|
if (options.strictArtifacts === true)
|
|
@@ -120,6 +37,319 @@ function resolveRequireRuntimeGuard(options) {
|
|
|
120
37
|
return false;
|
|
121
38
|
return process.env.NEURCODE_REMEDIATE_REQUIRE_RUNTIME_GUARD === '1';
|
|
122
39
|
}
|
|
40
|
+
function parsePositiveInt(raw) {
|
|
41
|
+
if (!raw)
|
|
42
|
+
return null;
|
|
43
|
+
const parsed = Number.parseInt(raw, 10);
|
|
44
|
+
if (!Number.isFinite(parsed) || parsed <= 0)
|
|
45
|
+
return null;
|
|
46
|
+
return parsed;
|
|
47
|
+
}
|
|
48
|
+
function resolveRequireApproval(options) {
|
|
49
|
+
if (options.requireApproval === true)
|
|
50
|
+
return true;
|
|
51
|
+
if (options.requireApproval === false)
|
|
52
|
+
return false;
|
|
53
|
+
return process.env.NEURCODE_REMEDIATE_REQUIRE_APPROVAL === '1';
|
|
54
|
+
}
|
|
55
|
+
function resolveMinApprovals(options) {
|
|
56
|
+
if (Number.isFinite(options.minApprovals)) {
|
|
57
|
+
return Math.max(1, Math.min(5, Math.floor(Number(options.minApprovals))));
|
|
58
|
+
}
|
|
59
|
+
const envValue = parsePositiveInt(process.env.NEURCODE_REMEDIATE_MIN_APPROVALS);
|
|
60
|
+
if (envValue != null) {
|
|
61
|
+
return Math.max(1, Math.min(5, envValue));
|
|
62
|
+
}
|
|
63
|
+
return 1;
|
|
64
|
+
}
|
|
65
|
+
function resolveRollbackOnRegression(options) {
|
|
66
|
+
if (options.rollbackOnRegression === true)
|
|
67
|
+
return true;
|
|
68
|
+
if (options.rollbackOnRegression === false)
|
|
69
|
+
return false;
|
|
70
|
+
if (process.env.NEURCODE_REMEDIATE_ROLLBACK_ON_REGRESSION === '0')
|
|
71
|
+
return false;
|
|
72
|
+
return true;
|
|
73
|
+
}
|
|
74
|
+
function resolveRequireRollbackSnapshot(options) {
|
|
75
|
+
if (options.requireRollbackSnapshot === true)
|
|
76
|
+
return true;
|
|
77
|
+
if (options.requireRollbackSnapshot === false)
|
|
78
|
+
return false;
|
|
79
|
+
if (process.env.NEURCODE_ENTERPRISE_MODE === '1')
|
|
80
|
+
return true;
|
|
81
|
+
return process.env.NEURCODE_REMEDIATE_REQUIRE_ROLLBACK_SNAPSHOT === '1';
|
|
82
|
+
}
|
|
83
|
+
function resolveSnapshotLimits(options) {
|
|
84
|
+
const maxFiles = Number.isFinite(options.snapshotMaxFiles)
|
|
85
|
+
? Math.max(100, Math.floor(Number(options.snapshotMaxFiles)))
|
|
86
|
+
: (parsePositiveInt(process.env.NEURCODE_REMEDIATE_SNAPSHOT_MAX_FILES) || 5000);
|
|
87
|
+
const maxBytes = Number.isFinite(options.snapshotMaxBytes)
|
|
88
|
+
? Math.max(5_000_000, Math.floor(Number(options.snapshotMaxBytes)))
|
|
89
|
+
: (parsePositiveInt(process.env.NEURCODE_REMEDIATE_SNAPSHOT_MAX_BYTES) || 128_000_000);
|
|
90
|
+
const maxFileBytes = Number.isFinite(options.snapshotMaxFileBytes)
|
|
91
|
+
? Math.max(100_000, Math.floor(Number(options.snapshotMaxFileBytes)))
|
|
92
|
+
: (parsePositiveInt(process.env.NEURCODE_REMEDIATE_SNAPSHOT_MAX_FILE_BYTES) || 8_000_000);
|
|
93
|
+
return {
|
|
94
|
+
maxFiles,
|
|
95
|
+
maxBytes,
|
|
96
|
+
maxFileBytes,
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
function resolveApprovalCommitSha(projectRoot, explicitCommit) {
|
|
100
|
+
if (explicitCommit && explicitCommit.trim()) {
|
|
101
|
+
return explicitCommit.trim().toLowerCase();
|
|
102
|
+
}
|
|
103
|
+
const run = (0, child_process_1.spawnSync)('git', ['rev-parse', 'HEAD'], {
|
|
104
|
+
cwd: projectRoot,
|
|
105
|
+
encoding: 'utf-8',
|
|
106
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
107
|
+
});
|
|
108
|
+
if (run.status !== 0) {
|
|
109
|
+
return null;
|
|
110
|
+
}
|
|
111
|
+
const sha = String(run.stdout || '').trim().toLowerCase();
|
|
112
|
+
return sha || null;
|
|
113
|
+
}
|
|
114
|
+
function resolveApprovalState(projectRoot, commitSha, minApprovals) {
|
|
115
|
+
if (!commitSha) {
|
|
116
|
+
return {
|
|
117
|
+
commitSha: null,
|
|
118
|
+
distinctApprovers: 0,
|
|
119
|
+
satisfied: false,
|
|
120
|
+
message: 'Unable to resolve HEAD commit for manual approval gating.',
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
const approvals = (0, manual_approvals_1.getManualApprovalsForCommit)(projectRoot, commitSha);
|
|
124
|
+
const distinctApprovers = (0, manual_approvals_1.countDistinctApprovers)(approvals);
|
|
125
|
+
const satisfied = distinctApprovers >= minApprovals;
|
|
126
|
+
const message = satisfied
|
|
127
|
+
? `Manual approvals satisfied (${distinctApprovers}/${minApprovals}) for commit ${commitSha}.`
|
|
128
|
+
: `Manual approvals required (${distinctApprovers}/${minApprovals}) for commit ${commitSha}.`;
|
|
129
|
+
return {
|
|
130
|
+
commitSha,
|
|
131
|
+
distinctApprovers,
|
|
132
|
+
satisfied,
|
|
133
|
+
message,
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
function parseNulSeparated(raw) {
|
|
137
|
+
return raw
|
|
138
|
+
.split('\u0000')
|
|
139
|
+
.map((entry) => entry.trim())
|
|
140
|
+
.filter(Boolean);
|
|
141
|
+
}
|
|
142
|
+
function runGitCapture(projectRoot, args) {
|
|
143
|
+
const run = (0, child_process_1.spawnSync)('git', args, {
|
|
144
|
+
cwd: projectRoot,
|
|
145
|
+
encoding: 'utf-8',
|
|
146
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
147
|
+
});
|
|
148
|
+
return {
|
|
149
|
+
ok: run.status === 0,
|
|
150
|
+
stdout: String(run.stdout || ''),
|
|
151
|
+
stderr: String(run.stderr || ''),
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
function listSnapshotPaths(projectRoot) {
|
|
155
|
+
const tracked = runGitCapture(projectRoot, ['ls-files', '-z']);
|
|
156
|
+
if (!tracked.ok) {
|
|
157
|
+
return {
|
|
158
|
+
paths: [],
|
|
159
|
+
error: `git ls-files failed: ${tracked.stderr.trim() || 'unknown error'}`,
|
|
160
|
+
};
|
|
161
|
+
}
|
|
162
|
+
const untracked = runGitCapture(projectRoot, ['ls-files', '--others', '--exclude-standard', '-z']);
|
|
163
|
+
if (!untracked.ok) {
|
|
164
|
+
return {
|
|
165
|
+
paths: [],
|
|
166
|
+
error: `git ls-files --others failed: ${untracked.stderr.trim() || 'unknown error'}`,
|
|
167
|
+
};
|
|
168
|
+
}
|
|
169
|
+
const unique = new Set();
|
|
170
|
+
for (const entry of [...parseNulSeparated(tracked.stdout), ...parseNulSeparated(untracked.stdout)]) {
|
|
171
|
+
const normalized = entry.replace(/\\/g, '/').replace(/^\.\//, '').trim();
|
|
172
|
+
if (!normalized || normalized.startsWith('.git/'))
|
|
173
|
+
continue;
|
|
174
|
+
if (normalized.includes('\u0000'))
|
|
175
|
+
continue;
|
|
176
|
+
unique.add(normalized);
|
|
177
|
+
}
|
|
178
|
+
return {
|
|
179
|
+
paths: [...unique].sort((a, b) => a.localeCompare(b)),
|
|
180
|
+
error: null,
|
|
181
|
+
};
|
|
182
|
+
}
|
|
183
|
+
function withinProject(projectRoot, relativePath) {
|
|
184
|
+
if (!relativePath)
|
|
185
|
+
return false;
|
|
186
|
+
if (relativePath.startsWith('/'))
|
|
187
|
+
return false;
|
|
188
|
+
if (relativePath.startsWith('../'))
|
|
189
|
+
return false;
|
|
190
|
+
const normalized = relativePath.replace(/\\/g, '/');
|
|
191
|
+
if (normalized.includes('/../') || normalized === '..')
|
|
192
|
+
return false;
|
|
193
|
+
const absolute = (0, path_1.resolve)(projectRoot, relativePath);
|
|
194
|
+
return absolute === projectRoot || absolute.startsWith(`${projectRoot}${path_1.sep}`);
|
|
195
|
+
}
|
|
196
|
+
function createRollbackSnapshot(projectRoot, limits) {
|
|
197
|
+
const listed = listSnapshotPaths(projectRoot);
|
|
198
|
+
if (listed.error) {
|
|
199
|
+
return {
|
|
200
|
+
snapshot: null,
|
|
201
|
+
message: listed.error,
|
|
202
|
+
};
|
|
203
|
+
}
|
|
204
|
+
if (listed.paths.length > limits.maxFiles) {
|
|
205
|
+
return {
|
|
206
|
+
snapshot: null,
|
|
207
|
+
message: `rollback snapshot skipped: ${listed.paths.length} files exceeds maxFiles=${limits.maxFiles}`,
|
|
208
|
+
};
|
|
209
|
+
}
|
|
210
|
+
const snapshotId = `snapshot-${Date.now()}`;
|
|
211
|
+
const rootDir = (0, path_1.join)(projectRoot, '.neurcode', 'remediate', 'snapshots', snapshotId);
|
|
212
|
+
const filesDir = (0, path_1.join)(rootDir, 'files');
|
|
213
|
+
(0, fs_1.mkdirSync)(filesDir, { recursive: true });
|
|
214
|
+
const files = [];
|
|
215
|
+
let totalBytes = 0;
|
|
216
|
+
for (const relativePath of listed.paths) {
|
|
217
|
+
if (!withinProject(projectRoot, relativePath)) {
|
|
218
|
+
(0, fs_1.rmSync)(rootDir, { recursive: true, force: true });
|
|
219
|
+
return {
|
|
220
|
+
snapshot: null,
|
|
221
|
+
message: `rollback snapshot skipped unsafe path: ${relativePath}`,
|
|
222
|
+
};
|
|
223
|
+
}
|
|
224
|
+
const absolutePath = (0, path_1.join)(projectRoot, relativePath);
|
|
225
|
+
let stats;
|
|
226
|
+
try {
|
|
227
|
+
stats = (0, fs_1.statSync)(absolutePath);
|
|
228
|
+
}
|
|
229
|
+
catch {
|
|
230
|
+
continue;
|
|
231
|
+
}
|
|
232
|
+
if (!stats.isFile())
|
|
233
|
+
continue;
|
|
234
|
+
const fileSize = Number(stats.size) || 0;
|
|
235
|
+
if (fileSize > limits.maxFileBytes) {
|
|
236
|
+
(0, fs_1.rmSync)(rootDir, { recursive: true, force: true });
|
|
237
|
+
return {
|
|
238
|
+
snapshot: null,
|
|
239
|
+
message: `rollback snapshot skipped: ${relativePath} size ${fileSize} exceeds maxFileBytes=${limits.maxFileBytes}`,
|
|
240
|
+
};
|
|
241
|
+
}
|
|
242
|
+
totalBytes += fileSize;
|
|
243
|
+
if (totalBytes > limits.maxBytes) {
|
|
244
|
+
(0, fs_1.rmSync)(rootDir, { recursive: true, force: true });
|
|
245
|
+
return {
|
|
246
|
+
snapshot: null,
|
|
247
|
+
message: `rollback snapshot skipped: total bytes ${totalBytes} exceeds maxBytes=${limits.maxBytes}`,
|
|
248
|
+
};
|
|
249
|
+
}
|
|
250
|
+
const content = (0, fs_1.readFileSync)(absolutePath);
|
|
251
|
+
const snapshotFilePath = (0, path_1.join)(filesDir, relativePath);
|
|
252
|
+
(0, fs_1.mkdirSync)((0, path_1.dirname)(snapshotFilePath), { recursive: true });
|
|
253
|
+
(0, fs_1.writeFileSync)(snapshotFilePath, content);
|
|
254
|
+
files.push({
|
|
255
|
+
path: relativePath,
|
|
256
|
+
sha256: (0, crypto_1.createHash)('sha256').update(content).digest('hex'),
|
|
257
|
+
size: fileSize,
|
|
258
|
+
});
|
|
259
|
+
}
|
|
260
|
+
const manifestPath = (0, path_1.join)(rootDir, 'manifest.json');
|
|
261
|
+
(0, fs_1.writeFileSync)(manifestPath, JSON.stringify({
|
|
262
|
+
snapshotId,
|
|
263
|
+
createdAt: new Date().toISOString(),
|
|
264
|
+
projectRoot,
|
|
265
|
+
totalBytes,
|
|
266
|
+
fileCount: files.length,
|
|
267
|
+
limits,
|
|
268
|
+
files,
|
|
269
|
+
}, null, 2) + '\n', 'utf-8');
|
|
270
|
+
return {
|
|
271
|
+
snapshot: {
|
|
272
|
+
snapshotId,
|
|
273
|
+
rootDir,
|
|
274
|
+
filesDir,
|
|
275
|
+
files,
|
|
276
|
+
totalBytes,
|
|
277
|
+
},
|
|
278
|
+
message: `rollback snapshot created (${files.length} files, ${totalBytes} bytes)`,
|
|
279
|
+
};
|
|
280
|
+
}
|
|
281
|
+
function restoreRollbackSnapshot(projectRoot, snapshot) {
|
|
282
|
+
const listed = listSnapshotPaths(projectRoot);
|
|
283
|
+
if (listed.error) {
|
|
284
|
+
return {
|
|
285
|
+
restored: false,
|
|
286
|
+
message: `rollback restore failed: ${listed.error}`,
|
|
287
|
+
};
|
|
288
|
+
}
|
|
289
|
+
const snapshotPaths = new Set(snapshot.files.map((entry) => entry.path));
|
|
290
|
+
const snapshotRootRelative = snapshot.rootDir
|
|
291
|
+
.replace(projectRoot, '')
|
|
292
|
+
.replace(/^[\\/]+/, '')
|
|
293
|
+
.replace(/\\/g, '/');
|
|
294
|
+
let removedCount = 0;
|
|
295
|
+
for (const currentPath of listed.paths) {
|
|
296
|
+
if (currentPath.startsWith('.git/'))
|
|
297
|
+
continue;
|
|
298
|
+
if (snapshotRootRelative
|
|
299
|
+
&& (currentPath === snapshotRootRelative || currentPath.startsWith(`${snapshotRootRelative}/`))) {
|
|
300
|
+
continue;
|
|
301
|
+
}
|
|
302
|
+
if (snapshotPaths.has(currentPath))
|
|
303
|
+
continue;
|
|
304
|
+
if (!withinProject(projectRoot, currentPath))
|
|
305
|
+
continue;
|
|
306
|
+
const absolutePath = (0, path_1.join)(projectRoot, currentPath);
|
|
307
|
+
try {
|
|
308
|
+
const stats = (0, fs_1.statSync)(absolutePath);
|
|
309
|
+
if (!stats.isFile())
|
|
310
|
+
continue;
|
|
311
|
+
(0, fs_1.rmSync)(absolutePath, { force: true });
|
|
312
|
+
removedCount += 1;
|
|
313
|
+
}
|
|
314
|
+
catch {
|
|
315
|
+
// Ignore best-effort cleanup failures.
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
let restoredCount = 0;
|
|
319
|
+
for (const entry of snapshot.files) {
|
|
320
|
+
if (!withinProject(projectRoot, entry.path)) {
|
|
321
|
+
return {
|
|
322
|
+
restored: false,
|
|
323
|
+
message: `rollback restore aborted due to unsafe snapshot path: ${entry.path}`,
|
|
324
|
+
};
|
|
325
|
+
}
|
|
326
|
+
const source = (0, path_1.join)(snapshot.filesDir, entry.path);
|
|
327
|
+
const destination = (0, path_1.join)(projectRoot, entry.path);
|
|
328
|
+
if (!(0, fs_1.existsSync)(source)) {
|
|
329
|
+
return {
|
|
330
|
+
restored: false,
|
|
331
|
+
message: `rollback restore failed: missing snapshot file ${entry.path}`,
|
|
332
|
+
};
|
|
333
|
+
}
|
|
334
|
+
(0, fs_1.mkdirSync)((0, path_1.dirname)(destination), { recursive: true });
|
|
335
|
+
(0, fs_1.copyFileSync)(source, destination);
|
|
336
|
+
restoredCount += 1;
|
|
337
|
+
}
|
|
338
|
+
return {
|
|
339
|
+
restored: true,
|
|
340
|
+
message: `rollback restored ${restoredCount} files and removed ${removedCount} generated files`,
|
|
341
|
+
};
|
|
342
|
+
}
|
|
343
|
+
function cleanupRollbackSnapshot(snapshot) {
|
|
344
|
+
if (!snapshot)
|
|
345
|
+
return;
|
|
346
|
+
try {
|
|
347
|
+
(0, fs_1.rmSync)(snapshot.rootDir, { recursive: true, force: true });
|
|
348
|
+
}
|
|
349
|
+
catch {
|
|
350
|
+
// Ignore cleanup errors.
|
|
351
|
+
}
|
|
352
|
+
}
|
|
123
353
|
function buildVerifyArgs(options, strictArtifacts, enforceChangeContract) {
|
|
124
354
|
const args = ['verify'];
|
|
125
355
|
if (options.planId)
|
|
@@ -166,10 +396,10 @@ function isVerifyPass(snapshot) {
|
|
|
166
396
|
function toVerifySnapshot(result) {
|
|
167
397
|
return {
|
|
168
398
|
exitCode: result.exitCode,
|
|
169
|
-
verdict: asString(result.payload, 'verdict'),
|
|
170
|
-
score: asNumber(result.payload, 'score'),
|
|
171
|
-
message: asString(result.payload, 'message'),
|
|
172
|
-
violations: asViolationsCount(result.payload),
|
|
399
|
+
verdict: (0, cli_json_1.asString)(result.payload, 'verdict'),
|
|
400
|
+
score: (0, cli_json_1.asNumber)(result.payload, 'score'),
|
|
401
|
+
message: (0, cli_json_1.asString)(result.payload, 'message'),
|
|
402
|
+
violations: (0, cli_json_1.asViolationsCount)(result.payload),
|
|
173
403
|
};
|
|
174
404
|
}
|
|
175
405
|
function hasImproved(before, after) {
|
|
@@ -268,7 +498,7 @@ function extractChangeJustificationFromLog(projectRoot) {
|
|
|
268
498
|
return null;
|
|
269
499
|
}
|
|
270
500
|
function shouldAttemptAiLogRepair(verifyRun) {
|
|
271
|
-
const payloadMessage = asString(verifyRun.payload, 'message') || '';
|
|
501
|
+
const payloadMessage = (0, cli_json_1.asString)(verifyRun.payload, 'message') || '';
|
|
272
502
|
const combined = `${payloadMessage}\n${verifyRun.stdout}\n${verifyRun.stderr}`.toLowerCase();
|
|
273
503
|
if (combined.includes('ai change-log integrity check failed')) {
|
|
274
504
|
return true;
|
|
@@ -343,12 +573,18 @@ async function remediateCommand(options = {}) {
|
|
|
343
573
|
const strictArtifacts = resolveStrictArtifacts(options);
|
|
344
574
|
const enforceChangeContract = resolveEnforceChangeContract(options, strictArtifacts);
|
|
345
575
|
const requireRuntimeGuard = resolveRequireRuntimeGuard(options);
|
|
576
|
+
const requireApproval = resolveRequireApproval(options);
|
|
577
|
+
const minApprovals = resolveMinApprovals(options);
|
|
578
|
+
const approvalCommit = resolveApprovalCommitSha(projectRoot, options.approvalCommit);
|
|
346
579
|
const autoRepairAiLog = resolveAutoRepairAiLog(options);
|
|
580
|
+
const rollbackOnRegression = resolveRollbackOnRegression(options);
|
|
581
|
+
const requireRollbackSnapshot = resolveRequireRollbackSnapshot(options);
|
|
582
|
+
const snapshotLimits = resolveSnapshotLimits(options);
|
|
347
583
|
const maxAttempts = Number.isFinite(options.maxFixAttempts) && Number(options.maxFixAttempts) >= 0
|
|
348
584
|
? Math.floor(Number(options.maxFixAttempts))
|
|
349
585
|
: 2;
|
|
350
586
|
try {
|
|
351
|
-
let baselineVerifyRun = await runCliJson(buildVerifyArgs(options, strictArtifacts, enforceChangeContract));
|
|
587
|
+
let baselineVerifyRun = await (0, cli_json_1.runCliJson)(buildVerifyArgs(options, strictArtifacts, enforceChangeContract));
|
|
352
588
|
let currentSnapshot = toVerifySnapshot(baselineVerifyRun);
|
|
353
589
|
let aiLogRepair = {
|
|
354
590
|
attempted: false,
|
|
@@ -359,7 +595,7 @@ async function remediateCommand(options = {}) {
|
|
|
359
595
|
if (autoRepairAiLog && !isVerifyPass(currentSnapshot) && shouldAttemptAiLogRepair(baselineVerifyRun)) {
|
|
360
596
|
aiLogRepair = attemptAiLogIntegrityRepair(projectRoot);
|
|
361
597
|
if (aiLogRepair.repaired) {
|
|
362
|
-
baselineVerifyRun = await runCliJson(buildVerifyArgs(options, strictArtifacts, enforceChangeContract));
|
|
598
|
+
baselineVerifyRun = await (0, cli_json_1.runCliJson)(buildVerifyArgs(options, strictArtifacts, enforceChangeContract));
|
|
363
599
|
currentSnapshot = toVerifySnapshot(baselineVerifyRun);
|
|
364
600
|
}
|
|
365
601
|
}
|
|
@@ -377,6 +613,18 @@ async function remediateCommand(options = {}) {
|
|
|
377
613
|
enforceChangeContract,
|
|
378
614
|
requireRuntimeGuard,
|
|
379
615
|
},
|
|
616
|
+
governance: {
|
|
617
|
+
requireApproval,
|
|
618
|
+
minApprovals,
|
|
619
|
+
approvalCommit,
|
|
620
|
+
rollbackOnRegression,
|
|
621
|
+
requireRollbackSnapshot,
|
|
622
|
+
snapshotLimits: {
|
|
623
|
+
maxFiles: snapshotLimits.maxFiles,
|
|
624
|
+
maxBytes: snapshotLimits.maxBytes,
|
|
625
|
+
maxFileBytes: snapshotLimits.maxFileBytes,
|
|
626
|
+
},
|
|
627
|
+
},
|
|
380
628
|
baseline: currentSnapshot,
|
|
381
629
|
attempts,
|
|
382
630
|
finalVerify: currentSnapshot,
|
|
@@ -411,6 +659,18 @@ async function remediateCommand(options = {}) {
|
|
|
411
659
|
enforceChangeContract,
|
|
412
660
|
requireRuntimeGuard,
|
|
413
661
|
},
|
|
662
|
+
governance: {
|
|
663
|
+
requireApproval,
|
|
664
|
+
minApprovals,
|
|
665
|
+
approvalCommit,
|
|
666
|
+
rollbackOnRegression,
|
|
667
|
+
requireRollbackSnapshot,
|
|
668
|
+
snapshotLimits: {
|
|
669
|
+
maxFiles: snapshotLimits.maxFiles,
|
|
670
|
+
maxBytes: snapshotLimits.maxBytes,
|
|
671
|
+
maxFileBytes: snapshotLimits.maxFileBytes,
|
|
672
|
+
},
|
|
673
|
+
},
|
|
414
674
|
baseline: currentSnapshot,
|
|
415
675
|
attempts,
|
|
416
676
|
finalVerify: currentSnapshot,
|
|
@@ -431,6 +691,14 @@ async function remediateCommand(options = {}) {
|
|
|
431
691
|
const attemptSummary = {
|
|
432
692
|
attempt,
|
|
433
693
|
before: currentSnapshot,
|
|
694
|
+
approval: {
|
|
695
|
+
required: requireApproval,
|
|
696
|
+
satisfied: !requireApproval,
|
|
697
|
+
commitSha: approvalCommit,
|
|
698
|
+
minimumApprovals: minApprovals,
|
|
699
|
+
distinctApprovers: 0,
|
|
700
|
+
message: requireApproval ? 'Approval check pending.' : 'Approval gate disabled.',
|
|
701
|
+
},
|
|
434
702
|
runtimeGuard: {
|
|
435
703
|
executed: false,
|
|
436
704
|
pass: null,
|
|
@@ -448,29 +716,69 @@ async function remediateCommand(options = {}) {
|
|
|
448
716
|
score: null,
|
|
449
717
|
violations: null,
|
|
450
718
|
},
|
|
719
|
+
rollback: {
|
|
720
|
+
snapshotCreated: false,
|
|
721
|
+
snapshotId: null,
|
|
722
|
+
restored: false,
|
|
723
|
+
postRollbackVerify: null,
|
|
724
|
+
message: rollbackOnRegression ? 'Rollback snapshot pending.' : 'Rollback disabled.',
|
|
725
|
+
},
|
|
451
726
|
stopReason: null,
|
|
452
727
|
};
|
|
728
|
+
if (requireApproval) {
|
|
729
|
+
const approvalState = resolveApprovalState(projectRoot, approvalCommit, minApprovals);
|
|
730
|
+
attemptSummary.approval.commitSha = approvalState.commitSha;
|
|
731
|
+
attemptSummary.approval.distinctApprovers = approvalState.distinctApprovers;
|
|
732
|
+
attemptSummary.approval.satisfied = approvalState.satisfied;
|
|
733
|
+
attemptSummary.approval.message = approvalState.message;
|
|
734
|
+
if (!approvalState.satisfied) {
|
|
735
|
+
attemptSummary.stopReason = 'approval_required';
|
|
736
|
+
attempts.push(attemptSummary);
|
|
737
|
+
stopReason = 'approval_required';
|
|
738
|
+
break;
|
|
739
|
+
}
|
|
740
|
+
}
|
|
741
|
+
let rollbackSnapshot = null;
|
|
742
|
+
if (rollbackOnRegression) {
|
|
743
|
+
const snapshotResult = createRollbackSnapshot(projectRoot, snapshotLimits);
|
|
744
|
+
if (snapshotResult.snapshot) {
|
|
745
|
+
rollbackSnapshot = snapshotResult.snapshot;
|
|
746
|
+
attemptSummary.rollback.snapshotCreated = true;
|
|
747
|
+
attemptSummary.rollback.snapshotId = rollbackSnapshot.snapshotId;
|
|
748
|
+
attemptSummary.rollback.message = snapshotResult.message;
|
|
749
|
+
}
|
|
750
|
+
else {
|
|
751
|
+
attemptSummary.rollback.message = snapshotResult.message;
|
|
752
|
+
if (requireRollbackSnapshot) {
|
|
753
|
+
attemptSummary.stopReason = 'rollback_snapshot_unavailable';
|
|
754
|
+
attempts.push(attemptSummary);
|
|
755
|
+
stopReason = 'rollback_snapshot_unavailable';
|
|
756
|
+
break;
|
|
757
|
+
}
|
|
758
|
+
}
|
|
759
|
+
}
|
|
453
760
|
if (requireRuntimeGuard) {
|
|
454
761
|
attemptSummary.runtimeGuard.executed = true;
|
|
455
|
-
const guardRun = await runCliJson(['guard', 'check', '--head']);
|
|
762
|
+
const guardRun = await (0, cli_json_1.runCliJson)(['guard', 'check', '--head']);
|
|
456
763
|
const guardPass = guardRun.exitCode === 0;
|
|
457
764
|
attemptSummary.runtimeGuard.pass = guardPass;
|
|
458
765
|
attemptSummary.runtimeGuard.message =
|
|
459
|
-
asString(guardRun.payload, 'message')
|
|
766
|
+
(0, cli_json_1.asString)(guardRun.payload, 'message')
|
|
460
767
|
|| (guardPass ? 'Runtime guard check passed.' : 'Runtime guard check failed.');
|
|
461
768
|
if (!guardPass) {
|
|
769
|
+
cleanupRollbackSnapshot(rollbackSnapshot);
|
|
462
770
|
attemptSummary.stopReason = 'runtime_guard_blocked';
|
|
463
771
|
attempts.push(attemptSummary);
|
|
464
772
|
stopReason = 'runtime_guard_blocked';
|
|
465
773
|
break;
|
|
466
774
|
}
|
|
467
775
|
}
|
|
468
|
-
const shipRun = await runCliJson(buildShipArgs(options));
|
|
776
|
+
const shipRun = await (0, cli_json_1.runCliJson)(buildShipArgs(options));
|
|
469
777
|
attemptSummary.ship.executed = true;
|
|
470
778
|
attemptSummary.ship.exitCode = shipRun.exitCode;
|
|
471
|
-
attemptSummary.ship.status = asString(shipRun.payload, 'status');
|
|
472
|
-
attemptSummary.ship.finalPlanId = asString(shipRun.payload, 'finalPlanId');
|
|
473
|
-
const afterVerifyRun = await runCliJson(buildVerifyArgs(options, strictArtifacts, enforceChangeContract));
|
|
779
|
+
attemptSummary.ship.status = (0, cli_json_1.asString)(shipRun.payload, 'status');
|
|
780
|
+
attemptSummary.ship.finalPlanId = (0, cli_json_1.asString)(shipRun.payload, 'finalPlanId');
|
|
781
|
+
const afterVerifyRun = await (0, cli_json_1.runCliJson)(buildVerifyArgs(options, strictArtifacts, enforceChangeContract));
|
|
474
782
|
const afterSnapshot = toVerifySnapshot(afterVerifyRun);
|
|
475
783
|
attemptSummary.after = afterSnapshot;
|
|
476
784
|
attemptSummary.delta = {
|
|
@@ -480,8 +788,38 @@ async function remediateCommand(options = {}) {
|
|
|
480
788
|
violations: afterSnapshot.violations - currentSnapshot.violations,
|
|
481
789
|
};
|
|
482
790
|
attemptSummary.improved = hasImproved(currentSnapshot, afterSnapshot);
|
|
791
|
+
if (attemptSummary.improved === false && rollbackOnRegression) {
|
|
792
|
+
if (rollbackSnapshot) {
|
|
793
|
+
const restoreResult = restoreRollbackSnapshot(projectRoot, rollbackSnapshot);
|
|
794
|
+
attemptSummary.rollback.restored = restoreResult.restored;
|
|
795
|
+
attemptSummary.rollback.message = restoreResult.message;
|
|
796
|
+
if (!restoreResult.restored && requireRollbackSnapshot) {
|
|
797
|
+
attemptSummary.stopReason = 'rollback_restore_failed';
|
|
798
|
+
attempts.push(attemptSummary);
|
|
799
|
+
stopReason = 'rollback_restore_failed';
|
|
800
|
+
cleanupRollbackSnapshot(rollbackSnapshot);
|
|
801
|
+
break;
|
|
802
|
+
}
|
|
803
|
+
if (restoreResult.restored) {
|
|
804
|
+
const rollbackVerifyRun = await (0, cli_json_1.runCliJson)(buildVerifyArgs(options, strictArtifacts, enforceChangeContract));
|
|
805
|
+
const rollbackSnapshotVerify = toVerifySnapshot(rollbackVerifyRun);
|
|
806
|
+
attemptSummary.rollback.postRollbackVerify = rollbackSnapshotVerify;
|
|
807
|
+
currentSnapshot = rollbackSnapshotVerify;
|
|
808
|
+
}
|
|
809
|
+
else {
|
|
810
|
+
currentSnapshot = afterSnapshot;
|
|
811
|
+
}
|
|
812
|
+
}
|
|
813
|
+
else {
|
|
814
|
+
attemptSummary.rollback.message = 'Rollback requested but snapshot was not available for this attempt.';
|
|
815
|
+
currentSnapshot = afterSnapshot;
|
|
816
|
+
}
|
|
817
|
+
}
|
|
818
|
+
else {
|
|
819
|
+
currentSnapshot = afterSnapshot;
|
|
820
|
+
}
|
|
821
|
+
cleanupRollbackSnapshot(rollbackSnapshot);
|
|
483
822
|
attempts.push(attemptSummary);
|
|
484
|
-
currentSnapshot = afterSnapshot;
|
|
485
823
|
if (isVerifyPass(afterSnapshot)) {
|
|
486
824
|
stopReason = 'verify_passed_after_remediation';
|
|
487
825
|
break;
|
|
@@ -507,6 +845,18 @@ async function remediateCommand(options = {}) {
|
|
|
507
845
|
enforceChangeContract,
|
|
508
846
|
requireRuntimeGuard,
|
|
509
847
|
},
|
|
848
|
+
governance: {
|
|
849
|
+
requireApproval,
|
|
850
|
+
minApprovals,
|
|
851
|
+
approvalCommit,
|
|
852
|
+
rollbackOnRegression,
|
|
853
|
+
requireRollbackSnapshot,
|
|
854
|
+
snapshotLimits: {
|
|
855
|
+
maxFiles: snapshotLimits.maxFiles,
|
|
856
|
+
maxBytes: snapshotLimits.maxBytes,
|
|
857
|
+
maxFileBytes: snapshotLimits.maxFileBytes,
|
|
858
|
+
},
|
|
859
|
+
},
|
|
510
860
|
baseline: toVerifySnapshot(baselineVerifyRun),
|
|
511
861
|
attempts,
|
|
512
862
|
finalVerify: currentSnapshot,
|
|
@@ -535,6 +885,9 @@ async function remediateCommand(options = {}) {
|
|
|
535
885
|
console.log(chalk.dim(`Strict mode: artifacts=${strictArtifacts ? 'on' : 'off'}, `
|
|
536
886
|
+ `change-contract=${enforceChangeContract ? 'on' : 'off'}, `
|
|
537
887
|
+ `runtime-guard=${requireRuntimeGuard ? 'required' : 'optional'}`));
|
|
888
|
+
console.log(chalk.dim(`Governance: approval=${requireApproval ? `required(${minApprovals})` : 'optional'}, `
|
|
889
|
+
+ `rollback=${rollbackOnRegression ? 'on' : 'off'}`
|
|
890
|
+
+ `${rollbackOnRegression ? ` [maxFiles=${snapshotLimits.maxFiles}, maxBytes=${snapshotLimits.maxBytes}]` : ''}`));
|
|
538
891
|
for (const attempt of output.attempts) {
|
|
539
892
|
const after = attempt.after;
|
|
540
893
|
const afterLabel = after
|
|
@@ -545,6 +898,23 @@ async function remediateCommand(options = {}) {
|
|
|
545
898
|
console.log(chalk.dim(` runtime guard: ${attempt.runtimeGuard.pass ? 'pass' : 'block'}`
|
|
546
899
|
+ `${attempt.runtimeGuard.message ? ` (${attempt.runtimeGuard.message})` : ''}`));
|
|
547
900
|
}
|
|
901
|
+
if (attempt.approval.required) {
|
|
902
|
+
console.log(chalk.dim(` approvals: ${attempt.approval.satisfied ? 'satisfied' : 'blocked'} `
|
|
903
|
+
+ `(${attempt.approval.distinctApprovers}/${attempt.approval.minimumApprovals})`
|
|
904
|
+
+ `${attempt.approval.commitSha ? ` on ${attempt.approval.commitSha}` : ''}`));
|
|
905
|
+
}
|
|
906
|
+
if (attempt.rollback.snapshotCreated || attempt.rollback.restored || attempt.rollback.message) {
|
|
907
|
+
const rollbackPrefix = attempt.rollback.restored ? 'restored' : (attempt.rollback.snapshotCreated ? 'captured' : 'skipped');
|
|
908
|
+
console.log(chalk.dim(` rollback: ${rollbackPrefix}`
|
|
909
|
+
+ `${attempt.rollback.snapshotId ? ` (${attempt.rollback.snapshotId})` : ''}`
|
|
910
|
+
+ `${attempt.rollback.message ? ` - ${attempt.rollback.message}` : ''}`));
|
|
911
|
+
}
|
|
912
|
+
if (attempt.rollback.postRollbackVerify) {
|
|
913
|
+
const rollbackVerify = attempt.rollback.postRollbackVerify;
|
|
914
|
+
console.log(chalk.dim(` rollback verify: ${rollbackVerify.verdict || 'UNKNOWN'}`
|
|
915
|
+
+ `${rollbackVerify.score != null ? ` (score ${rollbackVerify.score})` : ''}`
|
|
916
|
+
+ `, violations: ${rollbackVerify.violations}`));
|
|
917
|
+
}
|
|
548
918
|
if (attempt.improved === false) {
|
|
549
919
|
console.log(chalk.yellow(' no measurable governance improvement; stopping remediation loop'));
|
|
550
920
|
}
|
|
@@ -579,6 +949,18 @@ async function remediateCommand(options = {}) {
|
|
|
579
949
|
enforceChangeContract,
|
|
580
950
|
requireRuntimeGuard,
|
|
581
951
|
},
|
|
952
|
+
governance: {
|
|
953
|
+
requireApproval,
|
|
954
|
+
minApprovals,
|
|
955
|
+
approvalCommit,
|
|
956
|
+
rollbackOnRegression,
|
|
957
|
+
requireRollbackSnapshot,
|
|
958
|
+
snapshotLimits: {
|
|
959
|
+
maxFiles: snapshotLimits.maxFiles,
|
|
960
|
+
maxBytes: snapshotLimits.maxBytes,
|
|
961
|
+
maxFileBytes: snapshotLimits.maxFileBytes,
|
|
962
|
+
},
|
|
963
|
+
},
|
|
582
964
|
baseline: {
|
|
583
965
|
exitCode: 1,
|
|
584
966
|
verdict: null,
|