@codexstar/bug-hunter 3.0.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/CHANGELOG.md +151 -0
- package/LICENSE +21 -0
- package/README.md +665 -0
- package/SKILL.md +624 -0
- package/bin/bug-hunter +222 -0
- package/evals/evals.json +362 -0
- package/modes/_dispatch.md +121 -0
- package/modes/extended.md +94 -0
- package/modes/fix-loop.md +115 -0
- package/modes/fix-pipeline.md +384 -0
- package/modes/large-codebase.md +212 -0
- package/modes/local-sequential.md +143 -0
- package/modes/loop.md +125 -0
- package/modes/parallel.md +113 -0
- package/modes/scaled.md +76 -0
- package/modes/single-file.md +38 -0
- package/modes/small.md +86 -0
- package/package.json +56 -0
- package/prompts/doc-lookup.md +44 -0
- package/prompts/examples/hunter-examples.md +131 -0
- package/prompts/examples/skeptic-examples.md +87 -0
- package/prompts/fixer.md +103 -0
- package/prompts/hunter.md +146 -0
- package/prompts/recon.md +159 -0
- package/prompts/referee.md +122 -0
- package/prompts/skeptic.md +143 -0
- package/prompts/threat-model.md +122 -0
- package/scripts/bug-hunter-state.cjs +537 -0
- package/scripts/code-index.cjs +541 -0
- package/scripts/context7-api.cjs +133 -0
- package/scripts/delta-mode.cjs +219 -0
- package/scripts/dep-scan.cjs +343 -0
- package/scripts/doc-lookup.cjs +316 -0
- package/scripts/fix-lock.cjs +167 -0
- package/scripts/init-test-fixture.sh +19 -0
- package/scripts/payload-guard.cjs +197 -0
- package/scripts/run-bug-hunter.cjs +892 -0
- package/scripts/tests/bug-hunter-state.test.cjs +87 -0
- package/scripts/tests/code-index.test.cjs +57 -0
- package/scripts/tests/delta-mode.test.cjs +47 -0
- package/scripts/tests/fix-lock.test.cjs +36 -0
- package/scripts/tests/fixtures/flaky-worker.cjs +63 -0
- package/scripts/tests/fixtures/low-confidence-worker.cjs +73 -0
- package/scripts/tests/fixtures/success-worker.cjs +42 -0
- package/scripts/tests/payload-guard.test.cjs +41 -0
- package/scripts/tests/run-bug-hunter.test.cjs +403 -0
- package/scripts/tests/test-utils.cjs +59 -0
- package/scripts/tests/worktree-harvest.test.cjs +297 -0
- package/scripts/triage.cjs +528 -0
- package/scripts/worktree-harvest.cjs +516 -0
- package/templates/subagent-wrapper.md +109 -0
|
@@ -0,0 +1,537 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
const crypto = require('crypto');
|
|
4
|
+
const fs = require('fs');
|
|
5
|
+
const path = require('path');
|
|
6
|
+
|
|
7
|
+
const VALID_CHUNK_STATUS = new Set(['pending', 'in_progress', 'done', 'failed']);
|
|
8
|
+
const DEFAULT_CHUNK_SIZE = 30;
|
|
9
|
+
|
|
10
|
+
function nowIso() {
|
|
11
|
+
return new Date().toISOString();
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function ensureDir(filePath) {
|
|
15
|
+
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function readJson(filePath) {
|
|
19
|
+
return JSON.parse(fs.readFileSync(filePath, 'utf8'));
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function writeJson(filePath, value) {
|
|
23
|
+
ensureDir(filePath);
|
|
24
|
+
fs.writeFileSync(filePath, `${JSON.stringify(value, null, 2)}\n`, 'utf8');
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function splitChunks(files, chunkSize) {
|
|
28
|
+
const chunks = [];
|
|
29
|
+
let index = 0;
|
|
30
|
+
while (index < files.length) {
|
|
31
|
+
const filesSlice = files.slice(index, index + chunkSize);
|
|
32
|
+
chunks.push({
|
|
33
|
+
id: `chunk-${chunks.length + 1}`,
|
|
34
|
+
files: filesSlice,
|
|
35
|
+
status: 'pending',
|
|
36
|
+
retries: 0,
|
|
37
|
+
startedAt: null,
|
|
38
|
+
completedAt: null,
|
|
39
|
+
lastError: null
|
|
40
|
+
});
|
|
41
|
+
index += chunkSize;
|
|
42
|
+
}
|
|
43
|
+
return chunks;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function nextChunkNumber(chunks) {
|
|
47
|
+
const maxId = chunks.reduce((max, chunk) => {
|
|
48
|
+
const match = /^chunk-(\d+)$/.exec(String(chunk.id || ''));
|
|
49
|
+
if (!match) {
|
|
50
|
+
return max;
|
|
51
|
+
}
|
|
52
|
+
const value = Number.parseInt(match[1], 10);
|
|
53
|
+
if (!Number.isInteger(value)) {
|
|
54
|
+
return max;
|
|
55
|
+
}
|
|
56
|
+
return Math.max(max, value);
|
|
57
|
+
}, 0);
|
|
58
|
+
return maxId + 1;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function buildInitialState({ mode, chunkSize, files }) {
|
|
62
|
+
const normalizedFiles = [...new Set(files)].sort();
|
|
63
|
+
return {
|
|
64
|
+
schemaVersion: 2,
|
|
65
|
+
mode,
|
|
66
|
+
createdAt: nowIso(),
|
|
67
|
+
updatedAt: nowIso(),
|
|
68
|
+
chunkSize,
|
|
69
|
+
runtime: {
|
|
70
|
+
parallelDisabled: false
|
|
71
|
+
},
|
|
72
|
+
metrics: {
|
|
73
|
+
filesTotal: normalizedFiles.length,
|
|
74
|
+
filesScanned: 0,
|
|
75
|
+
chunksTotal: Math.ceil(normalizedFiles.length / chunkSize),
|
|
76
|
+
chunksDone: 0,
|
|
77
|
+
findingsTotal: 0,
|
|
78
|
+
findingsUnique: 0,
|
|
79
|
+
lowConfidenceFindings: 0
|
|
80
|
+
},
|
|
81
|
+
chunks: splitChunks(normalizedFiles, chunkSize),
|
|
82
|
+
bugLedger: [],
|
|
83
|
+
hashCache: {},
|
|
84
|
+
factCards: {},
|
|
85
|
+
consistency: {
|
|
86
|
+
checkedAt: null,
|
|
87
|
+
conflicts: []
|
|
88
|
+
},
|
|
89
|
+
fixPlan: null
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function readState(statePath) {
|
|
94
|
+
if (!fs.existsSync(statePath)) {
|
|
95
|
+
throw new Error(`State file does not exist: ${statePath}`);
|
|
96
|
+
}
|
|
97
|
+
return readJson(statePath);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function saveState(statePath, state) {
|
|
101
|
+
state.updatedAt = nowIso();
|
|
102
|
+
writeJson(statePath, state);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function hashFile(filePath) {
|
|
106
|
+
const data = fs.readFileSync(filePath);
|
|
107
|
+
return crypto.createHash('sha256').update(data).digest('hex');
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function summarize(state) {
|
|
111
|
+
const pending = state.chunks.filter((chunk) => chunk.status === 'pending').length;
|
|
112
|
+
const inProgress = state.chunks.filter((chunk) => chunk.status === 'in_progress').length;
|
|
113
|
+
const done = state.chunks.filter((chunk) => chunk.status === 'done').length;
|
|
114
|
+
const failed = state.chunks.filter((chunk) => chunk.status === 'failed').length;
|
|
115
|
+
return {
|
|
116
|
+
schemaVersion: state.schemaVersion,
|
|
117
|
+
mode: state.mode,
|
|
118
|
+
updatedAt: state.updatedAt,
|
|
119
|
+
runtime: state.runtime,
|
|
120
|
+
metrics: state.metrics,
|
|
121
|
+
chunkStatus: {
|
|
122
|
+
pending,
|
|
123
|
+
inProgress,
|
|
124
|
+
done,
|
|
125
|
+
failed
|
|
126
|
+
}
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function severityRank(severity) {
|
|
131
|
+
const normalized = String(severity || '').toLowerCase();
|
|
132
|
+
if (normalized === 'critical') {
|
|
133
|
+
return 3;
|
|
134
|
+
}
|
|
135
|
+
if (normalized === 'medium') {
|
|
136
|
+
return 2;
|
|
137
|
+
}
|
|
138
|
+
if (normalized === 'low') {
|
|
139
|
+
return 1;
|
|
140
|
+
}
|
|
141
|
+
return 0;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
function usage() {
|
|
145
|
+
console.error('Usage:');
|
|
146
|
+
console.error(' bug-hunter-state.cjs init <statePath> <mode> <filesJsonPath> [chunkSize]');
|
|
147
|
+
console.error(' bug-hunter-state.cjs status <statePath>');
|
|
148
|
+
console.error(' bug-hunter-state.cjs next-chunk <statePath>');
|
|
149
|
+
console.error(' bug-hunter-state.cjs mark-chunk <statePath> <chunkId> <pending|in_progress|done|failed> [error]');
|
|
150
|
+
console.error(' bug-hunter-state.cjs record-findings <statePath> <findingsJsonPath> [source]');
|
|
151
|
+
console.error(' bug-hunter-state.cjs hash-filter <statePath> <filesJsonPath>');
|
|
152
|
+
console.error(' bug-hunter-state.cjs hash-update <statePath> <filesJsonPath> [status]');
|
|
153
|
+
console.error(' bug-hunter-state.cjs append-files <statePath> <filesJsonPath>');
|
|
154
|
+
console.error(' bug-hunter-state.cjs record-fact-card <statePath> <chunkId> <factCardJsonPath>');
|
|
155
|
+
console.error(' bug-hunter-state.cjs set-consistency <statePath> <consistencyJsonPath>');
|
|
156
|
+
console.error(' bug-hunter-state.cjs set-fix-plan <statePath> <fixPlanJsonPath>');
|
|
157
|
+
console.error(' bug-hunter-state.cjs set-parallel-disabled <statePath> <true|false>');
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
function assertArray(value, label) {
|
|
161
|
+
if (!Array.isArray(value)) {
|
|
162
|
+
throw new Error(`${label} must be an array`);
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
function toConfidence(value) {
|
|
167
|
+
if (value === null || value === undefined || value === '') {
|
|
168
|
+
return null;
|
|
169
|
+
}
|
|
170
|
+
const parsed = Number(value);
|
|
171
|
+
if (!Number.isFinite(parsed)) {
|
|
172
|
+
return null;
|
|
173
|
+
}
|
|
174
|
+
return parsed;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
function main() {
|
|
178
|
+
const [command, ...args] = process.argv.slice(2);
|
|
179
|
+
|
|
180
|
+
if (!command) {
|
|
181
|
+
usage();
|
|
182
|
+
process.exit(1);
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
if (command === 'init') {
|
|
186
|
+
const [statePath, mode, filesJsonPath, chunkSizeRaw] = args;
|
|
187
|
+
if (!statePath || !mode || !filesJsonPath) {
|
|
188
|
+
usage();
|
|
189
|
+
process.exit(1);
|
|
190
|
+
}
|
|
191
|
+
const files = readJson(filesJsonPath);
|
|
192
|
+
assertArray(files, 'filesJson');
|
|
193
|
+
const chunkSizeParsed = Number.parseInt(chunkSizeRaw || '', 10);
|
|
194
|
+
const chunkSize = Number.isInteger(chunkSizeParsed) && chunkSizeParsed > 0
|
|
195
|
+
? chunkSizeParsed
|
|
196
|
+
: DEFAULT_CHUNK_SIZE;
|
|
197
|
+
const state = buildInitialState({ mode, chunkSize, files });
|
|
198
|
+
saveState(statePath, state);
|
|
199
|
+
console.log(JSON.stringify({
|
|
200
|
+
ok: true,
|
|
201
|
+
statePath,
|
|
202
|
+
summary: summarize(state)
|
|
203
|
+
}, null, 2));
|
|
204
|
+
return;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
if (command === 'status') {
|
|
208
|
+
const [statePath] = args;
|
|
209
|
+
const state = readState(statePath);
|
|
210
|
+
console.log(JSON.stringify({
|
|
211
|
+
ok: true,
|
|
212
|
+
statePath,
|
|
213
|
+
summary: summarize(state)
|
|
214
|
+
}, null, 2));
|
|
215
|
+
return;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
if (command === 'next-chunk') {
|
|
219
|
+
const [statePath] = args;
|
|
220
|
+
const state = readState(statePath);
|
|
221
|
+
const nextChunk = state.chunks.find((chunk) => chunk.status === 'pending');
|
|
222
|
+
if (!nextChunk) {
|
|
223
|
+
console.log(JSON.stringify({ ok: true, done: true }, null, 2));
|
|
224
|
+
return;
|
|
225
|
+
}
|
|
226
|
+
console.log(JSON.stringify({ ok: true, done: false, chunk: nextChunk }, null, 2));
|
|
227
|
+
return;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
if (command === 'mark-chunk') {
|
|
231
|
+
const [statePath, chunkId, status, errorMessage] = args;
|
|
232
|
+
if (!statePath || !chunkId || !status) {
|
|
233
|
+
usage();
|
|
234
|
+
process.exit(1);
|
|
235
|
+
}
|
|
236
|
+
if (!VALID_CHUNK_STATUS.has(status)) {
|
|
237
|
+
throw new Error(`Invalid chunk status: ${status}`);
|
|
238
|
+
}
|
|
239
|
+
const state = readState(statePath);
|
|
240
|
+
const chunk = state.chunks.find((entry) => entry.id === chunkId);
|
|
241
|
+
if (!chunk) {
|
|
242
|
+
throw new Error(`Unknown chunk id: ${chunkId}`);
|
|
243
|
+
}
|
|
244
|
+
chunk.status = status;
|
|
245
|
+
if (status === 'in_progress') {
|
|
246
|
+
chunk.startedAt = nowIso();
|
|
247
|
+
chunk.retries += 1;
|
|
248
|
+
chunk.lastError = null;
|
|
249
|
+
} else if (status === 'done') {
|
|
250
|
+
chunk.completedAt = nowIso();
|
|
251
|
+
chunk.lastError = null;
|
|
252
|
+
} else if (status === 'failed') {
|
|
253
|
+
chunk.lastError = errorMessage || 'unknown';
|
|
254
|
+
}
|
|
255
|
+
state.metrics.chunksDone = state.chunks.filter((entry) => entry.status === 'done').length;
|
|
256
|
+
state.metrics.filesScanned = state.chunks
|
|
257
|
+
.filter((entry) => entry.status === 'done')
|
|
258
|
+
.flatMap((entry) => entry.files)
|
|
259
|
+
.length;
|
|
260
|
+
saveState(statePath, state);
|
|
261
|
+
console.log(JSON.stringify({ ok: true, chunk }, null, 2));
|
|
262
|
+
return;
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
if (command === 'record-findings') {
|
|
266
|
+
const [statePath, findingsJsonPath, source = 'unknown'] = args;
|
|
267
|
+
if (!statePath || !findingsJsonPath) {
|
|
268
|
+
usage();
|
|
269
|
+
process.exit(1);
|
|
270
|
+
}
|
|
271
|
+
const state = readState(statePath);
|
|
272
|
+
const findings = readJson(findingsJsonPath);
|
|
273
|
+
assertArray(findings, 'findingsJson');
|
|
274
|
+
|
|
275
|
+
let inserted = 0;
|
|
276
|
+
let updated = 0;
|
|
277
|
+
for (const finding of findings) {
|
|
278
|
+
const file = String(finding.file || '').trim();
|
|
279
|
+
if (!file) {
|
|
280
|
+
continue;
|
|
281
|
+
}
|
|
282
|
+
const lines = String(finding.lines || '').trim();
|
|
283
|
+
const claim = String(finding.claim || '').trim();
|
|
284
|
+
const severity = String(finding.severity || 'Low');
|
|
285
|
+
const confidence = toConfidence(finding.confidence);
|
|
286
|
+
const bugId = String(finding.bugId || '').trim();
|
|
287
|
+
const key = `${file}|${lines}|${claim}`;
|
|
288
|
+
const existing = state.bugLedger.find((entry) => entry.key === key);
|
|
289
|
+
if (!existing) {
|
|
290
|
+
state.bugLedger.push({
|
|
291
|
+
key,
|
|
292
|
+
bugId,
|
|
293
|
+
severity,
|
|
294
|
+
file,
|
|
295
|
+
lines,
|
|
296
|
+
claim,
|
|
297
|
+
confidence,
|
|
298
|
+
status: 'open',
|
|
299
|
+
source,
|
|
300
|
+
updatedAt: nowIso()
|
|
301
|
+
});
|
|
302
|
+
inserted += 1;
|
|
303
|
+
continue;
|
|
304
|
+
}
|
|
305
|
+
const existingRank = severityRank(existing.severity);
|
|
306
|
+
const incomingRank = severityRank(severity);
|
|
307
|
+
if (incomingRank > existingRank) {
|
|
308
|
+
existing.severity = severity;
|
|
309
|
+
}
|
|
310
|
+
if (!existing.bugId && bugId) {
|
|
311
|
+
existing.bugId = bugId;
|
|
312
|
+
}
|
|
313
|
+
if (existing.confidence === null && confidence !== null) {
|
|
314
|
+
existing.confidence = confidence;
|
|
315
|
+
} else if (existing.confidence !== null && confidence !== null) {
|
|
316
|
+
existing.confidence = Math.max(existing.confidence, confidence);
|
|
317
|
+
}
|
|
318
|
+
existing.updatedAt = nowIso();
|
|
319
|
+
existing.source = source;
|
|
320
|
+
updated += 1;
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
state.metrics.findingsTotal += findings.length;
|
|
324
|
+
state.metrics.findingsUnique = state.bugLedger.length;
|
|
325
|
+
state.metrics.lowConfidenceFindings = state.bugLedger.filter((entry) => {
|
|
326
|
+
return entry.confidence === null || entry.confidence < 75;
|
|
327
|
+
}).length;
|
|
328
|
+
saveState(statePath, state);
|
|
329
|
+
console.log(JSON.stringify({
|
|
330
|
+
ok: true,
|
|
331
|
+
inserted,
|
|
332
|
+
updated,
|
|
333
|
+
metrics: state.metrics
|
|
334
|
+
}, null, 2));
|
|
335
|
+
return;
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
if (command === 'hash-filter') {
|
|
339
|
+
const [statePath, filesJsonPath] = args;
|
|
340
|
+
if (!statePath || !filesJsonPath) {
|
|
341
|
+
usage();
|
|
342
|
+
process.exit(1);
|
|
343
|
+
}
|
|
344
|
+
const state = readState(statePath);
|
|
345
|
+
const files = readJson(filesJsonPath);
|
|
346
|
+
assertArray(files, 'filesJson');
|
|
347
|
+
|
|
348
|
+
const scan = [];
|
|
349
|
+
const skip = [];
|
|
350
|
+
const missing = [];
|
|
351
|
+
|
|
352
|
+
for (const filePath of files) {
|
|
353
|
+
const normalized = String(filePath);
|
|
354
|
+
if (!fs.existsSync(normalized)) {
|
|
355
|
+
missing.push(normalized);
|
|
356
|
+
continue;
|
|
357
|
+
}
|
|
358
|
+
const currentHash = hashFile(normalized);
|
|
359
|
+
const previous = state.hashCache[normalized];
|
|
360
|
+
if (previous && previous.hash === currentHash) {
|
|
361
|
+
skip.push(normalized);
|
|
362
|
+
} else {
|
|
363
|
+
scan.push(normalized);
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
console.log(JSON.stringify({ ok: true, scan, skip, missing }, null, 2));
|
|
368
|
+
return;
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
if (command === 'hash-update') {
|
|
372
|
+
const [statePath, filesJsonPath, cacheStatus = 'scanned'] = args;
|
|
373
|
+
if (!statePath || !filesJsonPath) {
|
|
374
|
+
usage();
|
|
375
|
+
process.exit(1);
|
|
376
|
+
}
|
|
377
|
+
const state = readState(statePath);
|
|
378
|
+
const files = readJson(filesJsonPath);
|
|
379
|
+
assertArray(files, 'filesJson');
|
|
380
|
+
const updatedFiles = [];
|
|
381
|
+
const missing = [];
|
|
382
|
+
|
|
383
|
+
for (const filePath of files) {
|
|
384
|
+
const normalized = String(filePath);
|
|
385
|
+
if (!fs.existsSync(normalized)) {
|
|
386
|
+
missing.push(normalized);
|
|
387
|
+
continue;
|
|
388
|
+
}
|
|
389
|
+
state.hashCache[normalized] = {
|
|
390
|
+
hash: hashFile(normalized),
|
|
391
|
+
status: cacheStatus,
|
|
392
|
+
scannedAt: nowIso()
|
|
393
|
+
};
|
|
394
|
+
updatedFiles.push(normalized);
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
saveState(statePath, state);
|
|
398
|
+
console.log(JSON.stringify({
|
|
399
|
+
ok: true,
|
|
400
|
+
updated: updatedFiles.length,
|
|
401
|
+
missing,
|
|
402
|
+
updatedFiles
|
|
403
|
+
}, null, 2));
|
|
404
|
+
return;
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
if (command === 'append-files') {
|
|
408
|
+
const [statePath, filesJsonPath] = args;
|
|
409
|
+
if (!statePath || !filesJsonPath) {
|
|
410
|
+
usage();
|
|
411
|
+
process.exit(1);
|
|
412
|
+
}
|
|
413
|
+
const state = readState(statePath);
|
|
414
|
+
const files = readJson(filesJsonPath);
|
|
415
|
+
assertArray(files, 'filesJson');
|
|
416
|
+
const existing = new Set(state.chunks.flatMap((chunk) => chunk.files));
|
|
417
|
+
const toAppend = [...new Set(files.map((filePath) => String(filePath)))]
|
|
418
|
+
.filter((filePath) => !existing.has(filePath))
|
|
419
|
+
.sort();
|
|
420
|
+
if (toAppend.length === 0) {
|
|
421
|
+
console.log(JSON.stringify({ ok: true, appended: 0, chunksAdded: 0 }, null, 2));
|
|
422
|
+
return;
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
const chunkNumberStart = nextChunkNumber(state.chunks);
|
|
426
|
+
const newChunks = splitChunks(toAppend, state.chunkSize)
|
|
427
|
+
.map((chunk, index) => {
|
|
428
|
+
return {
|
|
429
|
+
...chunk,
|
|
430
|
+
id: `chunk-${chunkNumberStart + index}`
|
|
431
|
+
};
|
|
432
|
+
});
|
|
433
|
+
state.chunks.push(...newChunks);
|
|
434
|
+
state.metrics.filesTotal += toAppend.length;
|
|
435
|
+
state.metrics.chunksTotal = state.chunks.length;
|
|
436
|
+
saveState(statePath, state);
|
|
437
|
+
console.log(JSON.stringify({
|
|
438
|
+
ok: true,
|
|
439
|
+
appended: toAppend.length,
|
|
440
|
+
chunksAdded: newChunks.length,
|
|
441
|
+
summary: summarize(state)
|
|
442
|
+
}, null, 2));
|
|
443
|
+
return;
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
if (command === 'record-fact-card') {
|
|
447
|
+
const [statePath, chunkId, factCardJsonPath] = args;
|
|
448
|
+
if (!statePath || !chunkId || !factCardJsonPath) {
|
|
449
|
+
usage();
|
|
450
|
+
process.exit(1);
|
|
451
|
+
}
|
|
452
|
+
const state = readState(statePath);
|
|
453
|
+
const factCard = readJson(factCardJsonPath);
|
|
454
|
+
if (!state.factCards || typeof state.factCards !== 'object') {
|
|
455
|
+
state.factCards = {};
|
|
456
|
+
}
|
|
457
|
+
state.factCards[chunkId] = {
|
|
458
|
+
chunkId,
|
|
459
|
+
updatedAt: nowIso(),
|
|
460
|
+
apiContracts: Array.isArray(factCard.apiContracts) ? factCard.apiContracts : [],
|
|
461
|
+
authAssumptions: Array.isArray(factCard.authAssumptions) ? factCard.authAssumptions : [],
|
|
462
|
+
invariants: Array.isArray(factCard.invariants) ? factCard.invariants : []
|
|
463
|
+
};
|
|
464
|
+
saveState(statePath, state);
|
|
465
|
+
console.log(JSON.stringify({
|
|
466
|
+
ok: true,
|
|
467
|
+
chunkId,
|
|
468
|
+
factCards: Object.keys(state.factCards).length
|
|
469
|
+
}, null, 2));
|
|
470
|
+
return;
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
if (command === 'set-consistency') {
|
|
474
|
+
const [statePath, consistencyJsonPath] = args;
|
|
475
|
+
if (!statePath || !consistencyJsonPath) {
|
|
476
|
+
usage();
|
|
477
|
+
process.exit(1);
|
|
478
|
+
}
|
|
479
|
+
const state = readState(statePath);
|
|
480
|
+
const consistency = readJson(consistencyJsonPath);
|
|
481
|
+
state.consistency = consistency;
|
|
482
|
+
saveState(statePath, state);
|
|
483
|
+
console.log(JSON.stringify({
|
|
484
|
+
ok: true,
|
|
485
|
+
consistency
|
|
486
|
+
}, null, 2));
|
|
487
|
+
return;
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
if (command === 'set-fix-plan') {
|
|
491
|
+
const [statePath, fixPlanJsonPath] = args;
|
|
492
|
+
if (!statePath || !fixPlanJsonPath) {
|
|
493
|
+
usage();
|
|
494
|
+
process.exit(1);
|
|
495
|
+
}
|
|
496
|
+
const state = readState(statePath);
|
|
497
|
+
const fixPlan = readJson(fixPlanJsonPath);
|
|
498
|
+
state.fixPlan = fixPlan;
|
|
499
|
+
saveState(statePath, state);
|
|
500
|
+
console.log(JSON.stringify({
|
|
501
|
+
ok: true,
|
|
502
|
+
fixPlan
|
|
503
|
+
}, null, 2));
|
|
504
|
+
return;
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
if (command === 'set-parallel-disabled') {
|
|
508
|
+
const [statePath, boolValue] = args;
|
|
509
|
+
if (!statePath || !boolValue) {
|
|
510
|
+
usage();
|
|
511
|
+
process.exit(1);
|
|
512
|
+
}
|
|
513
|
+
const state = readState(statePath);
|
|
514
|
+
const normalized = String(boolValue).toLowerCase();
|
|
515
|
+
if (normalized !== 'true' && normalized !== 'false') {
|
|
516
|
+
throw new Error('set-parallel-disabled expects true or false');
|
|
517
|
+
}
|
|
518
|
+
state.runtime.parallelDisabled = normalized === 'true';
|
|
519
|
+
saveState(statePath, state);
|
|
520
|
+
console.log(JSON.stringify({
|
|
521
|
+
ok: true,
|
|
522
|
+
runtime: state.runtime
|
|
523
|
+
}, null, 2));
|
|
524
|
+
return;
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
usage();
|
|
528
|
+
process.exit(1);
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
try {
|
|
532
|
+
main();
|
|
533
|
+
} catch (error) {
|
|
534
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
535
|
+
console.error(message);
|
|
536
|
+
process.exit(1);
|
|
537
|
+
}
|