@codexstar/bug-hunter 3.0.0 → 3.0.5
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 +149 -83
- package/README.md +150 -15
- package/SKILL.md +94 -27
- package/agents/openai.yaml +4 -0
- package/bin/bug-hunter +9 -3
- package/docs/images/2026-03-12-fix-plan-rollout.png +0 -0
- package/docs/images/2026-03-12-hero-bug-hunter-overview.png +0 -0
- package/docs/images/2026-03-12-machine-readable-artifacts.png +0 -0
- package/docs/images/2026-03-12-pr-review-flow.png +0 -0
- package/docs/images/2026-03-12-security-pack.png +0 -0
- package/docs/images/adversarial-debate.png +0 -0
- package/docs/images/doc-verify-fix-plan.png +0 -0
- package/docs/images/hero.png +0 -0
- package/docs/images/pipeline-overview.png +0 -0
- package/docs/images/security-finding-card.png +0 -0
- package/docs/plans/2026-03-11-structured-output-migration-plan.md +288 -0
- package/docs/plans/2026-03-12-audit-bug-fixes-surgical-plan.md +193 -0
- package/docs/plans/2026-03-12-enterprise-security-pack-e2e-plan.md +59 -0
- package/docs/plans/2026-03-12-local-security-skills-integration-plan.md +39 -0
- package/docs/plans/2026-03-12-pr-review-strategic-fix-flow.md +78 -0
- package/evals/evals.json +366 -102
- package/modes/extended.md +2 -2
- package/modes/fix-loop.md +30 -30
- package/modes/fix-pipeline.md +32 -6
- package/modes/large-codebase.md +14 -15
- package/modes/local-sequential.md +44 -20
- package/modes/loop.md +56 -56
- package/modes/parallel.md +3 -3
- package/modes/scaled.md +2 -2
- package/modes/single-file.md +3 -3
- package/modes/small.md +11 -11
- package/package.json +10 -1
- package/prompts/fixer.md +37 -23
- package/prompts/hunter.md +39 -20
- package/prompts/referee.md +34 -20
- package/prompts/skeptic.md +25 -22
- package/schemas/coverage.schema.json +67 -0
- package/schemas/examples/findings.invalid.json +13 -0
- package/schemas/examples/findings.valid.json +17 -0
- package/schemas/findings.schema.json +76 -0
- package/schemas/fix-plan.schema.json +94 -0
- package/schemas/fix-report.schema.json +105 -0
- package/schemas/fix-strategy.schema.json +99 -0
- package/schemas/recon.schema.json +31 -0
- package/schemas/referee.schema.json +46 -0
- package/schemas/shared.schema.json +51 -0
- package/schemas/skeptic.schema.json +21 -0
- package/scripts/bug-hunter-state.cjs +35 -12
- package/scripts/code-index.cjs +11 -4
- package/scripts/fix-lock.cjs +95 -25
- package/scripts/payload-guard.cjs +24 -10
- package/scripts/pr-scope.cjs +181 -0
- package/scripts/render-report.cjs +346 -0
- package/scripts/run-bug-hunter.cjs +667 -32
- package/scripts/schema-runtime.cjs +273 -0
- package/scripts/schema-validate.cjs +40 -0
- package/scripts/tests/bug-hunter-state.test.cjs +68 -3
- package/scripts/tests/code-index.test.cjs +15 -0
- package/scripts/tests/fix-lock.test.cjs +60 -2
- package/scripts/tests/fixtures/flaky-worker.cjs +6 -1
- package/scripts/tests/fixtures/low-confidence-worker.cjs +8 -2
- package/scripts/tests/fixtures/success-worker.cjs +6 -1
- package/scripts/tests/payload-guard.test.cjs +154 -2
- package/scripts/tests/pr-scope.test.cjs +212 -0
- package/scripts/tests/render-report.test.cjs +180 -0
- package/scripts/tests/run-bug-hunter.test.cjs +686 -2
- package/scripts/tests/security-skills-integration.test.cjs +29 -0
- package/scripts/tests/skills-packaging.test.cjs +30 -0
- package/scripts/tests/worktree-harvest.test.cjs +66 -0
- package/scripts/worktree-harvest.cjs +62 -9
- package/skills/README.md +19 -0
- package/skills/commit-security-scan/SKILL.md +63 -0
- package/skills/security-review/SKILL.md +57 -0
- package/skills/threat-model-generation/SKILL.md +47 -0
- package/skills/vulnerability-validation/SKILL.md +59 -0
- package/templates/subagent-wrapper.md +12 -3
- package/modes/_dispatch.md +0 -121
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
const childProcess = require('child_process');
|
|
4
4
|
const fs = require('fs');
|
|
5
5
|
const path = require('path');
|
|
6
|
+
const { validateArtifactFile, validateArtifactValue } = require('./schema-runtime.cjs');
|
|
6
7
|
|
|
7
8
|
const BACKEND_PRIORITY = ['spawn_agent', 'subagent', 'teams', 'local-sequential'];
|
|
8
9
|
const DEFAULT_TIMEOUT_MS = 120000;
|
|
@@ -17,7 +18,8 @@ const DEFAULT_EXPANSION_CAP = 40;
|
|
|
17
18
|
function usage() {
|
|
18
19
|
console.error('Usage:');
|
|
19
20
|
console.error(' run-bug-hunter.cjs preflight [--skill-dir <path>] [--available-backends <csv>] [--backend <name>]');
|
|
20
|
-
console.error(' run-bug-hunter.cjs run --files-json <path> [--mode <name>] [--skill-dir <path>] [--state <path>] [--chunk-size <n>] [--worker-cmd <template>] [--timeout-ms <n>] [--max-retries <n>] [--backoff-ms <n>] [--available-backends <csv>] [--backend <name>] [--fail-fast <true|false>] [--use-index <true|false>] [--index-path <path>] [--delta-mode <true|false>] [--changed-files-json <path>] [--delta-hops <n>] [--expand-on-low-confidence <true|false>] [--confidence-threshold <n>] [--canary-size <n>] [--expansion-cap <n>]');
|
|
21
|
+
console.error(' run-bug-hunter.cjs run --files-json <path> [--mode <name>] [--skill-dir <path>] [--state <path>] [--chunk-size <n>] [--worker-cmd <template>] [--timeout-ms <n>] [--max-retries <n>] [--backoff-ms <n>] [--available-backends <csv>] [--backend <name>] [--fail-fast <true|false>] [--use-index <true|false>] [--index-path <path>] [--delta-mode <true|false>] [--changed-files-json <path>] [--delta-hops <n>] [--expand-on-low-confidence <true|false>] [--confidence-threshold <n>] [--canary-size <n>] [--expansion-cap <n>] [--strategy-path <path>] [--strategy-markdown-path <path>]');
|
|
22
|
+
console.error(' run-bug-hunter.cjs phase --artifact <name> --output-path <path> --worker-cmd <template> [--phase-name <name>] [--skill-dir <path>] [--journal-path <path>] [--render-cmd <template>] [--render-output-path <path>] [--timeout-ms <n>] [--render-timeout-ms <n>] [--max-retries <n>] [--backoff-ms <n>]');
|
|
21
23
|
console.error(' run-bug-hunter.cjs plan --files-json <path> [--mode <name>] [--skill-dir <path>] [--chunk-size <n>] [--plan-path <path>]');
|
|
22
24
|
}
|
|
23
25
|
|
|
@@ -114,10 +116,23 @@ function requiredScripts(skillDir) {
|
|
|
114
116
|
return [
|
|
115
117
|
path.join(skillDir, 'scripts', 'bug-hunter-state.cjs'),
|
|
116
118
|
path.join(skillDir, 'scripts', 'payload-guard.cjs'),
|
|
119
|
+
path.join(skillDir, 'scripts', 'schema-validate.cjs'),
|
|
120
|
+
path.join(skillDir, 'scripts', 'schema-runtime.cjs'),
|
|
121
|
+
path.join(skillDir, 'scripts', 'render-report.cjs'),
|
|
117
122
|
path.join(skillDir, 'scripts', 'fix-lock.cjs'),
|
|
118
123
|
path.join(skillDir, 'scripts', 'doc-lookup.cjs'),
|
|
119
124
|
path.join(skillDir, 'scripts', 'context7-api.cjs'),
|
|
120
|
-
path.join(skillDir, 'scripts', 'delta-mode.cjs')
|
|
125
|
+
path.join(skillDir, 'scripts', 'delta-mode.cjs'),
|
|
126
|
+
path.join(skillDir, 'scripts', 'pr-scope.cjs'),
|
|
127
|
+
path.join(skillDir, 'schemas', 'findings.schema.json'),
|
|
128
|
+
path.join(skillDir, 'schemas', 'skeptic.schema.json'),
|
|
129
|
+
path.join(skillDir, 'schemas', 'referee.schema.json'),
|
|
130
|
+
path.join(skillDir, 'schemas', 'coverage.schema.json'),
|
|
131
|
+
path.join(skillDir, 'schemas', 'fix-report.schema.json'),
|
|
132
|
+
path.join(skillDir, 'schemas', 'fix-plan.schema.json'),
|
|
133
|
+
path.join(skillDir, 'schemas', 'fix-strategy.schema.json'),
|
|
134
|
+
path.join(skillDir, 'schemas', 'recon.schema.json'),
|
|
135
|
+
path.join(skillDir, 'schemas', 'shared.schema.json')
|
|
121
136
|
];
|
|
122
137
|
}
|
|
123
138
|
|
|
@@ -149,18 +164,38 @@ function runJsonScript(scriptPath, args) {
|
|
|
149
164
|
return JSON.parse(output);
|
|
150
165
|
}
|
|
151
166
|
|
|
167
|
+
function runTextScript(scriptPath, args) {
|
|
168
|
+
const result = childProcess.spawnSync('node', [scriptPath, ...args], {
|
|
169
|
+
encoding: 'utf8'
|
|
170
|
+
});
|
|
171
|
+
if (result.status !== 0) {
|
|
172
|
+
const stderr = (result.stderr || '').trim();
|
|
173
|
+
const stdout = (result.stdout || '').trim();
|
|
174
|
+
throw new Error(stderr || stdout || `Script failed: ${scriptPath}`);
|
|
175
|
+
}
|
|
176
|
+
return result.stdout || '';
|
|
177
|
+
}
|
|
178
|
+
|
|
152
179
|
function appendJournal(logPath, event) {
|
|
153
180
|
ensureDir(path.dirname(logPath));
|
|
154
181
|
const line = JSON.stringify({ at: nowIso(), ...event });
|
|
155
182
|
fs.appendFileSync(logPath, `${line}\n`, 'utf8');
|
|
156
183
|
}
|
|
157
184
|
|
|
185
|
+
function shellQuote(value) {
|
|
186
|
+
const stringValue = String(value);
|
|
187
|
+
if (stringValue.length === 0) {
|
|
188
|
+
return "''";
|
|
189
|
+
}
|
|
190
|
+
return `'${stringValue.replace(/'/g, `'\\''`)}'`;
|
|
191
|
+
}
|
|
192
|
+
|
|
158
193
|
function fillTemplate(template, variables) {
|
|
159
194
|
return template.replace(/\{([a-zA-Z0-9_]+)\}/g, (match, key) => {
|
|
160
195
|
if (!(key in variables)) {
|
|
161
|
-
|
|
196
|
+
throw new Error(`Unknown template placeholder: ${key}`);
|
|
162
197
|
}
|
|
163
|
-
return
|
|
198
|
+
return shellQuote(variables[key]);
|
|
164
199
|
});
|
|
165
200
|
}
|
|
166
201
|
|
|
@@ -213,7 +248,9 @@ async function runWithRetry({
|
|
|
213
248
|
backoffMs,
|
|
214
249
|
journalPath,
|
|
215
250
|
phase,
|
|
216
|
-
chunkId
|
|
251
|
+
chunkId,
|
|
252
|
+
beforeAttempt,
|
|
253
|
+
postAttempt
|
|
217
254
|
}) {
|
|
218
255
|
const attempts = maxRetries + 1;
|
|
219
256
|
let lastResult = null;
|
|
@@ -227,20 +264,45 @@ async function runWithRetry({
|
|
|
227
264
|
attempts,
|
|
228
265
|
timeoutMs
|
|
229
266
|
});
|
|
267
|
+
if (typeof beforeAttempt === 'function') {
|
|
268
|
+
await beforeAttempt({ attempt });
|
|
269
|
+
}
|
|
230
270
|
const result = await runCommandOnce({ command, timeoutMs });
|
|
231
|
-
|
|
271
|
+
let finalResult = result;
|
|
272
|
+
|
|
273
|
+
if (finalResult.ok && typeof postAttempt === 'function') {
|
|
274
|
+
const postAttemptResult = await postAttempt({ attempt });
|
|
275
|
+
if (!postAttemptResult.ok) {
|
|
276
|
+
const validationMessage = String(postAttemptResult.errorMessage || 'post-attempt validation failed');
|
|
277
|
+
appendJournal(journalPath, {
|
|
278
|
+
event: 'attempt-post-check-failed',
|
|
279
|
+
phase,
|
|
280
|
+
chunkId,
|
|
281
|
+
attempt,
|
|
282
|
+
errorMessage: validationMessage.slice(0, 500)
|
|
283
|
+
});
|
|
284
|
+
finalResult = {
|
|
285
|
+
...finalResult,
|
|
286
|
+
ok: false,
|
|
287
|
+
stderr: validationMessage
|
|
288
|
+
};
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
|
|
232
292
|
appendJournal(journalPath, {
|
|
233
293
|
event: 'attempt-end',
|
|
234
294
|
phase,
|
|
235
295
|
chunkId,
|
|
236
296
|
attempt,
|
|
237
|
-
ok:
|
|
238
|
-
code:
|
|
239
|
-
timeoutHit:
|
|
240
|
-
stderr:
|
|
297
|
+
ok: finalResult.ok,
|
|
298
|
+
code: finalResult.code,
|
|
299
|
+
timeoutHit: finalResult.timeoutHit,
|
|
300
|
+
stderr: finalResult.stderr.slice(0, 500)
|
|
241
301
|
});
|
|
242
|
-
|
|
243
|
-
|
|
302
|
+
|
|
303
|
+
lastResult = finalResult;
|
|
304
|
+
if (finalResult.ok) {
|
|
305
|
+
return { ok: true, result: finalResult, attemptsUsed: attempt };
|
|
244
306
|
}
|
|
245
307
|
if (attempt < attempts) {
|
|
246
308
|
const delayMs = backoffMs * 2 ** (attempt - 1);
|
|
@@ -378,8 +440,8 @@ function buildConsistencyReport({ bugLedger, confidenceThreshold }) {
|
|
|
378
440
|
}
|
|
379
441
|
|
|
380
442
|
const lowConfidence = bugLedger.filter((entry) => {
|
|
381
|
-
const
|
|
382
|
-
return
|
|
443
|
+
const confidenceScore = entry.confidenceScore;
|
|
444
|
+
return confidenceScore === null || confidenceScore === undefined || Number(confidenceScore) < confidenceThreshold;
|
|
383
445
|
}).length;
|
|
384
446
|
|
|
385
447
|
return {
|
|
@@ -391,30 +453,69 @@ function buildConsistencyReport({ bugLedger, confidenceThreshold }) {
|
|
|
391
453
|
};
|
|
392
454
|
}
|
|
393
455
|
|
|
394
|
-
function
|
|
395
|
-
const
|
|
396
|
-
|
|
397
|
-
|
|
456
|
+
function buildConflictSets(consistency) {
|
|
457
|
+
const conflicts = toArray(consistency && consistency.conflicts);
|
|
458
|
+
const bugIds = new Set();
|
|
459
|
+
const locations = new Set();
|
|
460
|
+
|
|
461
|
+
for (const conflict of conflicts) {
|
|
462
|
+
if (conflict && conflict.type === 'bug-id-reused' && conflict.bugId) {
|
|
463
|
+
bugIds.add(String(conflict.bugId));
|
|
464
|
+
}
|
|
465
|
+
if (conflict && conflict.type === 'location-claim-conflict' && conflict.location) {
|
|
466
|
+
locations.add(String(conflict.location));
|
|
467
|
+
}
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
return { bugIds, locations };
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
function applyConflictClassification(entry, classification, conflictSets) {
|
|
474
|
+
const bugId = String(entry.bugId || '').trim();
|
|
475
|
+
const location = `${entry.file || ''}|${entry.lines || ''}`;
|
|
476
|
+
const hasConflict = conflictSets.bugIds.has(bugId) || conflictSets.locations.has(location);
|
|
477
|
+
if (!hasConflict) {
|
|
478
|
+
return classification;
|
|
479
|
+
}
|
|
480
|
+
return {
|
|
481
|
+
strategy: 'manual-review',
|
|
482
|
+
executionStage: 'manual-review',
|
|
483
|
+
autofixEligible: false,
|
|
484
|
+
reason: 'Consistency conflict requires manual review before any fix is attempted.'
|
|
485
|
+
};
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
function buildFixPlan({ bugLedger, confidenceThreshold, canarySize, consistency }) {
|
|
489
|
+
const conflictSets = buildConflictSets(consistency);
|
|
490
|
+
const classifiedEntries = bugLedger.map((entry) => {
|
|
491
|
+
const confidenceRaw = entry.confidenceScore;
|
|
492
|
+
const confidenceScore = Number.isFinite(Number(confidenceRaw)) ? Number(confidenceRaw) : null;
|
|
493
|
+
const classification = applyConflictClassification(
|
|
494
|
+
entry,
|
|
495
|
+
classifyStrategy({ ...entry, confidenceScore }, confidenceThreshold),
|
|
496
|
+
conflictSets
|
|
497
|
+
);
|
|
398
498
|
return {
|
|
399
499
|
...entry,
|
|
400
|
-
|
|
500
|
+
confidenceScore,
|
|
501
|
+
...classification
|
|
401
502
|
};
|
|
402
503
|
});
|
|
403
|
-
const eligible =
|
|
404
|
-
.filter((entry) => entry.
|
|
504
|
+
const eligible = classifiedEntries
|
|
505
|
+
.filter((entry) => entry.autofixEligible === true)
|
|
405
506
|
.sort((left, right) => {
|
|
406
507
|
const severityDiff = severityRank(right.severity) - severityRank(left.severity);
|
|
407
508
|
if (severityDiff !== 0) {
|
|
408
509
|
return severityDiff;
|
|
409
510
|
}
|
|
410
|
-
const confidenceDiff = (right.
|
|
511
|
+
const confidenceDiff = (right.confidenceScore || 0) - (left.confidenceScore || 0);
|
|
411
512
|
if (confidenceDiff !== 0) {
|
|
412
513
|
return confidenceDiff;
|
|
413
514
|
}
|
|
414
515
|
return String(left.key).localeCompare(String(right.key));
|
|
415
516
|
});
|
|
416
|
-
const manualReview =
|
|
417
|
-
.filter((entry) => entry.
|
|
517
|
+
const manualReview = classifiedEntries
|
|
518
|
+
.filter((entry) => entry.autofixEligible !== true);
|
|
418
519
|
const canary = eligible.slice(0, canarySize);
|
|
419
520
|
const rollout = eligible.slice(canarySize);
|
|
420
521
|
|
|
@@ -423,7 +524,7 @@ function buildFixPlan({ bugLedger, confidenceThreshold, canarySize }) {
|
|
|
423
524
|
confidenceThreshold,
|
|
424
525
|
canarySize,
|
|
425
526
|
totals: {
|
|
426
|
-
findings:
|
|
527
|
+
findings: classifiedEntries.length,
|
|
427
528
|
eligible: eligible.length,
|
|
428
529
|
canary: canary.length,
|
|
429
530
|
rollout: rollout.length,
|
|
@@ -435,6 +536,431 @@ function buildFixPlan({ bugLedger, confidenceThreshold, canarySize }) {
|
|
|
435
536
|
};
|
|
436
537
|
}
|
|
437
538
|
|
|
539
|
+
function classifyStrategy(entry, confidenceThreshold) {
|
|
540
|
+
const confidenceScore = Number.isFinite(Number(entry.confidenceScore)) ? Number(entry.confidenceScore) : null;
|
|
541
|
+
const claim = String(entry.claim || '').toLowerCase();
|
|
542
|
+
const crossReferences = toArray(entry.crossReferences);
|
|
543
|
+
const architecturalSignals = ['architecture', 'migration', 'schema', 'contract', 'signature', 'protocol'];
|
|
544
|
+
const refactorSignals = ['refactor', 'transaction', 'concurrency', 'race', 'lock ordering'];
|
|
545
|
+
|
|
546
|
+
if (confidenceScore === null || confidenceScore < confidenceThreshold) {
|
|
547
|
+
return {
|
|
548
|
+
strategy: 'manual-review',
|
|
549
|
+
executionStage: 'manual-review',
|
|
550
|
+
autofixEligible: false,
|
|
551
|
+
reason: 'Confidence is below the autofix threshold.'
|
|
552
|
+
};
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
if (architecturalSignals.some((signal) => claim.includes(signal)) || crossReferences.length >= 3) {
|
|
556
|
+
return {
|
|
557
|
+
strategy: 'architectural-remediation',
|
|
558
|
+
executionStage: 'report-only',
|
|
559
|
+
autofixEligible: false,
|
|
560
|
+
reason: 'Claim spans broader contracts or architecture boundaries.'
|
|
561
|
+
};
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
if (refactorSignals.some((signal) => claim.includes(signal)) || severityRank(entry.severity) >= 2 && crossReferences.length >= 2) {
|
|
565
|
+
return {
|
|
566
|
+
strategy: 'larger-refactor',
|
|
567
|
+
executionStage: 'manual-review',
|
|
568
|
+
autofixEligible: false,
|
|
569
|
+
reason: 'Fix likely needs coordinated multi-file changes beyond a surgical patch.'
|
|
570
|
+
};
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
return {
|
|
574
|
+
strategy: 'safe-autofix',
|
|
575
|
+
executionStage: severityRank(entry.severity) >= 2 ? 'canary' : 'rollout',
|
|
576
|
+
autofixEligible: true,
|
|
577
|
+
reason: 'Finding is localized enough for a guarded surgical fix.'
|
|
578
|
+
};
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
function recommendedActionForStrategy(strategy) {
|
|
582
|
+
if (strategy === 'architectural-remediation') {
|
|
583
|
+
return 'Do not auto-edit. Capture a remediation design and schedule a broader change.';
|
|
584
|
+
}
|
|
585
|
+
if (strategy === 'larger-refactor') {
|
|
586
|
+
return 'Pause before patching. Review interfaces, callers, and rollback scope with a human.';
|
|
587
|
+
}
|
|
588
|
+
if (strategy === 'manual-review') {
|
|
589
|
+
return 'Keep this in the report and require human approval before any edits.';
|
|
590
|
+
}
|
|
591
|
+
return 'Proceed through the guarded fix pipeline with canary verification and rollback safety.';
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
function buildFixStrategy({ bugLedger, confidenceThreshold, consistency }) {
|
|
595
|
+
const conflictSets = buildConflictSets(consistency);
|
|
596
|
+
const normalized = bugLedger.map((entry) => {
|
|
597
|
+
const confidenceScore = Number.isFinite(Number(entry.confidenceScore)) ? Number(entry.confidenceScore) : null;
|
|
598
|
+
const classification = applyConflictClassification(
|
|
599
|
+
entry,
|
|
600
|
+
classifyStrategy({ ...entry, confidenceScore }, confidenceThreshold),
|
|
601
|
+
conflictSets
|
|
602
|
+
);
|
|
603
|
+
const filePath = String(entry.file || '').trim() || 'unknown-file';
|
|
604
|
+
const clusterDir = path.dirname(filePath);
|
|
605
|
+
const clusterSeed = `${classification.strategy}|${classification.executionStage}|${clusterDir}`;
|
|
606
|
+
return {
|
|
607
|
+
...entry,
|
|
608
|
+
confidenceScore,
|
|
609
|
+
file: filePath,
|
|
610
|
+
clusterDir,
|
|
611
|
+
clusterSeed,
|
|
612
|
+
...classification
|
|
613
|
+
};
|
|
614
|
+
});
|
|
615
|
+
|
|
616
|
+
const byCluster = new Map();
|
|
617
|
+
for (const entry of normalized) {
|
|
618
|
+
if (!byCluster.has(entry.clusterSeed)) {
|
|
619
|
+
byCluster.set(entry.clusterSeed, []);
|
|
620
|
+
}
|
|
621
|
+
byCluster.get(entry.clusterSeed).push(entry);
|
|
622
|
+
}
|
|
623
|
+
|
|
624
|
+
const clusters = [...byCluster.entries()].map(([clusterSeed, entries], index) => {
|
|
625
|
+
const strategy = entries[0].strategy;
|
|
626
|
+
const executionStage = entries[0].executionStage;
|
|
627
|
+
const files = [...new Set(entries.map((entry) => entry.file))].sort();
|
|
628
|
+
const bugIds = [...new Set(entries.map((entry) => String(entry.bugId || entry.key || '').trim()).filter(Boolean))];
|
|
629
|
+
const maxSeverity = entries
|
|
630
|
+
.map((entry) => entry.severity)
|
|
631
|
+
.sort((left, right) => severityRank(right) - severityRank(left))[0] || 'LOW';
|
|
632
|
+
const reasons = [...new Set(entries.map((entry) => entry.reason).filter(Boolean))];
|
|
633
|
+
const firstDir = entries[0].clusterDir || path.dirname(files[0] || 'unknown-file');
|
|
634
|
+
return {
|
|
635
|
+
clusterId: `cluster-${index + 1}`,
|
|
636
|
+
strategy,
|
|
637
|
+
executionStage,
|
|
638
|
+
autofixEligible: entries.every((entry) => entry.autofixEligible),
|
|
639
|
+
bugIds,
|
|
640
|
+
files,
|
|
641
|
+
maxSeverity,
|
|
642
|
+
summary: `${bugIds.length} bug(s) in ${firstDir || '.'} classified as ${strategy}.`,
|
|
643
|
+
recommendedAction: recommendedActionForStrategy(strategy),
|
|
644
|
+
reasons
|
|
645
|
+
};
|
|
646
|
+
}).sort((left, right) => {
|
|
647
|
+
const stageRank = {
|
|
648
|
+
canary: 0,
|
|
649
|
+
rollout: 1,
|
|
650
|
+
'manual-review': 2,
|
|
651
|
+
'report-only': 3
|
|
652
|
+
};
|
|
653
|
+
const stageDiff = stageRank[left.executionStage] - stageRank[right.executionStage];
|
|
654
|
+
if (stageDiff !== 0) {
|
|
655
|
+
return stageDiff;
|
|
656
|
+
}
|
|
657
|
+
return severityRank(right.maxSeverity) - severityRank(left.maxSeverity);
|
|
658
|
+
});
|
|
659
|
+
|
|
660
|
+
const summary = {
|
|
661
|
+
confirmed: normalized.length,
|
|
662
|
+
safeAutofix: normalized.filter((entry) => entry.strategy === 'safe-autofix').length,
|
|
663
|
+
manualReview: normalized.filter((entry) => entry.strategy === 'manual-review').length,
|
|
664
|
+
largerRefactor: normalized.filter((entry) => entry.strategy === 'larger-refactor').length,
|
|
665
|
+
architecturalRemediation: normalized.filter((entry) => entry.strategy === 'architectural-remediation').length,
|
|
666
|
+
canaryCandidates: normalized.filter((entry) => entry.executionStage === 'canary').length,
|
|
667
|
+
rolloutCandidates: normalized.filter((entry) => entry.executionStage === 'rollout').length
|
|
668
|
+
};
|
|
669
|
+
|
|
670
|
+
return {
|
|
671
|
+
version: '3.1.0',
|
|
672
|
+
generatedAt: nowIso(),
|
|
673
|
+
confidenceThreshold,
|
|
674
|
+
summary,
|
|
675
|
+
clusters
|
|
676
|
+
};
|
|
677
|
+
}
|
|
678
|
+
|
|
679
|
+
function toCoverageStatus(chunkStatus) {
|
|
680
|
+
if (chunkStatus === 'done') {
|
|
681
|
+
return 'done';
|
|
682
|
+
}
|
|
683
|
+
if (chunkStatus === 'in_progress') {
|
|
684
|
+
return 'in_progress';
|
|
685
|
+
}
|
|
686
|
+
if (chunkStatus === 'failed') {
|
|
687
|
+
return 'failed';
|
|
688
|
+
}
|
|
689
|
+
return 'pending';
|
|
690
|
+
}
|
|
691
|
+
|
|
692
|
+
function buildCoverageArtifact({ state, fixPlan }) {
|
|
693
|
+
const fileEntries = toArray(state.chunks).flatMap((chunk) => {
|
|
694
|
+
return toArray(chunk.files).map((filePath) => {
|
|
695
|
+
return {
|
|
696
|
+
path: String(filePath),
|
|
697
|
+
status: toCoverageStatus(chunk.status)
|
|
698
|
+
};
|
|
699
|
+
});
|
|
700
|
+
});
|
|
701
|
+
|
|
702
|
+
const bugs = toArray(state.bugLedger).map((entry) => {
|
|
703
|
+
return {
|
|
704
|
+
bugId: String(entry.bugId || '').trim() || String(entry.key || '').trim(),
|
|
705
|
+
severity: String(entry.severity || 'Low'),
|
|
706
|
+
file: String(entry.file || '').trim(),
|
|
707
|
+
claim: String(entry.claim || '').trim()
|
|
708
|
+
};
|
|
709
|
+
});
|
|
710
|
+
|
|
711
|
+
const fixStatusByBugId = new Map();
|
|
712
|
+
for (const entry of toArray(fixPlan && fixPlan.canary)) {
|
|
713
|
+
fixStatusByBugId.set(String(entry.bugId || '').trim(), 'CANARY');
|
|
714
|
+
}
|
|
715
|
+
for (const entry of toArray(fixPlan && fixPlan.rollout)) {
|
|
716
|
+
fixStatusByBugId.set(String(entry.bugId || '').trim(), 'ROLLOUT');
|
|
717
|
+
}
|
|
718
|
+
for (const entry of toArray(fixPlan && fixPlan.manualReview)) {
|
|
719
|
+
fixStatusByBugId.set(String(entry.bugId || '').trim(), 'MANUAL_REVIEW');
|
|
720
|
+
}
|
|
721
|
+
|
|
722
|
+
const fixes = [...fixStatusByBugId.entries()]
|
|
723
|
+
.filter(([bugId]) => Boolean(bugId))
|
|
724
|
+
.map(([bugId, status]) => {
|
|
725
|
+
return {
|
|
726
|
+
bugId,
|
|
727
|
+
status
|
|
728
|
+
};
|
|
729
|
+
});
|
|
730
|
+
|
|
731
|
+
const hasOpenChunks = toArray(state.chunks).some((chunk) => chunk.status !== 'done');
|
|
732
|
+
|
|
733
|
+
return {
|
|
734
|
+
schemaVersion: 1,
|
|
735
|
+
iteration: 1,
|
|
736
|
+
status: hasOpenChunks ? 'IN_PROGRESS' : 'COMPLETE',
|
|
737
|
+
files: fileEntries,
|
|
738
|
+
bugs,
|
|
739
|
+
fixes
|
|
740
|
+
};
|
|
741
|
+
}
|
|
742
|
+
|
|
743
|
+
function renderCoverageMarkdown(coverage) {
|
|
744
|
+
const lines = [
|
|
745
|
+
'# Bug Hunter Coverage',
|
|
746
|
+
'',
|
|
747
|
+
`- Status: ${coverage.status}`,
|
|
748
|
+
`- Iteration: ${coverage.iteration}`,
|
|
749
|
+
`- Files: ${coverage.files.length}`,
|
|
750
|
+
`- Bugs: ${coverage.bugs.length}`,
|
|
751
|
+
`- Fix entries: ${coverage.fixes.length}`,
|
|
752
|
+
'',
|
|
753
|
+
'## Files'
|
|
754
|
+
];
|
|
755
|
+
|
|
756
|
+
if (coverage.files.length === 0) {
|
|
757
|
+
lines.push('- None');
|
|
758
|
+
} else {
|
|
759
|
+
for (const entry of coverage.files) {
|
|
760
|
+
lines.push(`- ${entry.status} | ${entry.path}`);
|
|
761
|
+
}
|
|
762
|
+
}
|
|
763
|
+
|
|
764
|
+
lines.push('', '## Bugs');
|
|
765
|
+
if (coverage.bugs.length === 0) {
|
|
766
|
+
lines.push('- None');
|
|
767
|
+
} else {
|
|
768
|
+
for (const bug of coverage.bugs) {
|
|
769
|
+
lines.push(`- ${bug.bugId} | ${bug.severity} | ${bug.file} | ${bug.claim}`);
|
|
770
|
+
}
|
|
771
|
+
}
|
|
772
|
+
|
|
773
|
+
lines.push('', '## Fixes');
|
|
774
|
+
if (coverage.fixes.length === 0) {
|
|
775
|
+
lines.push('- None');
|
|
776
|
+
} else {
|
|
777
|
+
for (const fix of coverage.fixes) {
|
|
778
|
+
lines.push(`- ${fix.bugId} | ${fix.status}`);
|
|
779
|
+
}
|
|
780
|
+
}
|
|
781
|
+
|
|
782
|
+
return `${lines.join('\n')}\n`;
|
|
783
|
+
}
|
|
784
|
+
|
|
785
|
+
function validateFindingsArtifact(findingsJsonPath) {
|
|
786
|
+
if (!fs.existsSync(findingsJsonPath)) {
|
|
787
|
+
return {
|
|
788
|
+
ok: false,
|
|
789
|
+
errors: [`Missing findings artifact: ${findingsJsonPath}`]
|
|
790
|
+
};
|
|
791
|
+
}
|
|
792
|
+
return validateArtifactFile({
|
|
793
|
+
artifactName: 'findings',
|
|
794
|
+
filePath: findingsJsonPath
|
|
795
|
+
});
|
|
796
|
+
}
|
|
797
|
+
|
|
798
|
+
function validateNamedArtifact({ artifactName, filePath }) {
|
|
799
|
+
if (!fs.existsSync(filePath)) {
|
|
800
|
+
return {
|
|
801
|
+
ok: false,
|
|
802
|
+
errors: [`Missing ${artifactName} artifact: ${filePath}`]
|
|
803
|
+
};
|
|
804
|
+
}
|
|
805
|
+
return validateArtifactFile({
|
|
806
|
+
artifactName,
|
|
807
|
+
filePath
|
|
808
|
+
});
|
|
809
|
+
}
|
|
810
|
+
|
|
811
|
+
function removeFileIfExists(filePath) {
|
|
812
|
+
if (!filePath) {
|
|
813
|
+
return;
|
|
814
|
+
}
|
|
815
|
+
if (fs.existsSync(filePath)) {
|
|
816
|
+
fs.unlinkSync(filePath);
|
|
817
|
+
}
|
|
818
|
+
}
|
|
819
|
+
|
|
820
|
+
async function runPhase(options) {
|
|
821
|
+
const artifact = String(options.artifact || '').trim();
|
|
822
|
+
if (!artifact) {
|
|
823
|
+
throw new Error('--artifact is required for phase command');
|
|
824
|
+
}
|
|
825
|
+
if (!options['output-path']) {
|
|
826
|
+
throw new Error('--output-path is required for phase command');
|
|
827
|
+
}
|
|
828
|
+
if (!options['worker-cmd']) {
|
|
829
|
+
throw new Error('--worker-cmd is required for phase command');
|
|
830
|
+
}
|
|
831
|
+
|
|
832
|
+
const skillDir = resolveSkillDir(options);
|
|
833
|
+
const preflightResult = preflight(options);
|
|
834
|
+
if (!preflightResult.ok) {
|
|
835
|
+
throw new Error(`Missing helper scripts: ${preflightResult.missing.join(', ')}`);
|
|
836
|
+
}
|
|
837
|
+
|
|
838
|
+
const phaseName = options['phase-name'] || artifact;
|
|
839
|
+
const outputPath = path.resolve(options['output-path']);
|
|
840
|
+
const renderOutputPath = options['render-output-path']
|
|
841
|
+
? path.resolve(options['render-output-path'])
|
|
842
|
+
: null;
|
|
843
|
+
const workerCmdTemplate = options['worker-cmd'];
|
|
844
|
+
const renderCmdTemplate = options['render-cmd'] || null;
|
|
845
|
+
const timeoutMs = toPositiveInt(options['timeout-ms'], DEFAULT_TIMEOUT_MS);
|
|
846
|
+
const renderTimeoutMs = toPositiveInt(options['render-timeout-ms'], timeoutMs);
|
|
847
|
+
const maxRetries = toPositiveInt(options['max-retries'], DEFAULT_MAX_RETRIES);
|
|
848
|
+
const backoffMs = toPositiveInt(options['backoff-ms'], DEFAULT_BACKOFF_MS);
|
|
849
|
+
const journalPath = path.resolve(
|
|
850
|
+
options['journal-path'] || path.join(path.dirname(outputPath), `${phaseName}.log`)
|
|
851
|
+
);
|
|
852
|
+
const templateVariables = {
|
|
853
|
+
artifact,
|
|
854
|
+
outputPath,
|
|
855
|
+
outputFilePath: outputPath,
|
|
856
|
+
renderOutputPath: renderOutputPath || '',
|
|
857
|
+
journalPath,
|
|
858
|
+
phaseName,
|
|
859
|
+
skillDir
|
|
860
|
+
};
|
|
861
|
+
|
|
862
|
+
ensureDir(path.dirname(outputPath));
|
|
863
|
+
if (renderOutputPath) {
|
|
864
|
+
ensureDir(path.dirname(renderOutputPath));
|
|
865
|
+
}
|
|
866
|
+
removeFileIfExists(outputPath);
|
|
867
|
+
removeFileIfExists(renderOutputPath);
|
|
868
|
+
|
|
869
|
+
appendJournal(journalPath, {
|
|
870
|
+
event: 'phase-start',
|
|
871
|
+
artifact,
|
|
872
|
+
phase: phaseName,
|
|
873
|
+
outputPath,
|
|
874
|
+
renderOutputPath
|
|
875
|
+
});
|
|
876
|
+
|
|
877
|
+
const workerCommand = fillTemplate(workerCmdTemplate, templateVariables);
|
|
878
|
+
const runResult = await runWithRetry({
|
|
879
|
+
command: workerCommand,
|
|
880
|
+
timeoutMs,
|
|
881
|
+
maxRetries,
|
|
882
|
+
backoffMs,
|
|
883
|
+
journalPath,
|
|
884
|
+
phase: phaseName,
|
|
885
|
+
chunkId: artifact,
|
|
886
|
+
beforeAttempt: async () => {
|
|
887
|
+
removeFileIfExists(outputPath);
|
|
888
|
+
removeFileIfExists(renderOutputPath);
|
|
889
|
+
},
|
|
890
|
+
postAttempt: async () => {
|
|
891
|
+
const validation = validateNamedArtifact({
|
|
892
|
+
artifactName: artifact,
|
|
893
|
+
filePath: outputPath
|
|
894
|
+
});
|
|
895
|
+
if (validation.ok) {
|
|
896
|
+
return { ok: true };
|
|
897
|
+
}
|
|
898
|
+
return {
|
|
899
|
+
ok: false,
|
|
900
|
+
errorMessage: validation.errors.join('; ')
|
|
901
|
+
};
|
|
902
|
+
}
|
|
903
|
+
});
|
|
904
|
+
|
|
905
|
+
if (!runResult.ok) {
|
|
906
|
+
const errorMessage = (runResult.result && runResult.result.stderr) || `${phaseName} failed`;
|
|
907
|
+
appendJournal(journalPath, {
|
|
908
|
+
event: 'phase-failed',
|
|
909
|
+
artifact,
|
|
910
|
+
phase: phaseName,
|
|
911
|
+
errorMessage: errorMessage.slice(0, 500)
|
|
912
|
+
});
|
|
913
|
+
throw new Error(errorMessage);
|
|
914
|
+
}
|
|
915
|
+
|
|
916
|
+
if (renderCmdTemplate) {
|
|
917
|
+
const renderCommand = fillTemplate(renderCmdTemplate, templateVariables);
|
|
918
|
+
appendJournal(journalPath, {
|
|
919
|
+
event: 'phase-render-start',
|
|
920
|
+
artifact,
|
|
921
|
+
phase: phaseName,
|
|
922
|
+
renderOutputPath
|
|
923
|
+
});
|
|
924
|
+
const renderResult = await runCommandOnce({
|
|
925
|
+
command: renderCommand,
|
|
926
|
+
timeoutMs: renderTimeoutMs
|
|
927
|
+
});
|
|
928
|
+
if (!renderResult.ok) {
|
|
929
|
+
const renderError = renderResult.stderr || renderResult.stdout || `${phaseName} render failed`;
|
|
930
|
+
appendJournal(journalPath, {
|
|
931
|
+
event: 'phase-render-failed',
|
|
932
|
+
artifact,
|
|
933
|
+
phase: phaseName,
|
|
934
|
+
errorMessage: renderError.slice(0, 500)
|
|
935
|
+
});
|
|
936
|
+
throw new Error(renderError);
|
|
937
|
+
}
|
|
938
|
+
appendJournal(journalPath, {
|
|
939
|
+
event: 'phase-render-end',
|
|
940
|
+
artifact,
|
|
941
|
+
phase: phaseName,
|
|
942
|
+
renderOutputPath
|
|
943
|
+
});
|
|
944
|
+
}
|
|
945
|
+
|
|
946
|
+
appendJournal(journalPath, {
|
|
947
|
+
event: 'phase-end',
|
|
948
|
+
artifact,
|
|
949
|
+
phase: phaseName,
|
|
950
|
+
attemptsUsed: runResult.attemptsUsed
|
|
951
|
+
});
|
|
952
|
+
|
|
953
|
+
return {
|
|
954
|
+
ok: true,
|
|
955
|
+
artifact,
|
|
956
|
+
phase: phaseName,
|
|
957
|
+
outputPath,
|
|
958
|
+
renderOutputPath,
|
|
959
|
+
journalPath,
|
|
960
|
+
attemptsUsed: runResult.attemptsUsed
|
|
961
|
+
};
|
|
962
|
+
}
|
|
963
|
+
|
|
438
964
|
function loadIndex(indexPath) {
|
|
439
965
|
if (!indexPath || !fs.existsSync(indexPath)) {
|
|
440
966
|
return null;
|
|
@@ -513,7 +1039,21 @@ async function processPendingChunks({
|
|
|
513
1039
|
backoffMs,
|
|
514
1040
|
journalPath,
|
|
515
1041
|
phase: 'chunk-worker',
|
|
516
|
-
chunkId: chunk.id
|
|
1042
|
+
chunkId: chunk.id,
|
|
1043
|
+
beforeAttempt: async () => {
|
|
1044
|
+
removeFileIfExists(findingsJsonPath);
|
|
1045
|
+
removeFileIfExists(factsJsonPath);
|
|
1046
|
+
},
|
|
1047
|
+
postAttempt: async () => {
|
|
1048
|
+
const findingsValidation = validateFindingsArtifact(findingsJsonPath);
|
|
1049
|
+
if (findingsValidation.ok) {
|
|
1050
|
+
return { ok: true };
|
|
1051
|
+
}
|
|
1052
|
+
return {
|
|
1053
|
+
ok: false,
|
|
1054
|
+
errorMessage: findingsValidation.errors.join('; ')
|
|
1055
|
+
};
|
|
1056
|
+
}
|
|
517
1057
|
});
|
|
518
1058
|
|
|
519
1059
|
if (!runResult.ok) {
|
|
@@ -531,10 +1071,8 @@ async function processPendingChunks({
|
|
|
531
1071
|
}
|
|
532
1072
|
|
|
533
1073
|
let findings = [];
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
findings = readJson(findingsJsonPath);
|
|
537
|
-
}
|
|
1074
|
+
runJsonScript(stateScript, ['record-findings', statePath, findingsJsonPath, 'orchestrator']);
|
|
1075
|
+
findings = readJson(findingsJsonPath);
|
|
538
1076
|
|
|
539
1077
|
if (fs.existsSync(factsJsonPath)) {
|
|
540
1078
|
runJsonScript(stateScript, ['record-fact-card', statePath, chunk.id, factsJsonPath]);
|
|
@@ -662,6 +1200,10 @@ async function runPipeline(options) {
|
|
|
662
1200
|
const chunksDir = path.resolve(path.dirname(statePath), 'chunks');
|
|
663
1201
|
const consistencyReportPath = path.resolve(options['consistency-report'] || path.join(path.dirname(statePath), 'consistency.json'));
|
|
664
1202
|
const fixPlanPath = path.resolve(options['fix-plan-path'] || path.join(path.dirname(statePath), 'fix-plan.json'));
|
|
1203
|
+
const strategyPath = path.resolve(options['strategy-path'] || path.join(path.dirname(statePath), 'fix-strategy.json'));
|
|
1204
|
+
const strategyMarkdownPath = path.resolve(options['strategy-markdown-path'] || path.join(path.dirname(statePath), 'fix-strategy.md'));
|
|
1205
|
+
const coveragePath = path.resolve(options['coverage-path'] || path.join(path.dirname(statePath), 'coverage.json'));
|
|
1206
|
+
const coverageMarkdownPath = path.resolve(options['coverage-markdown-path'] || path.join(path.dirname(statePath), 'coverage.md'));
|
|
665
1207
|
const factsPath = path.resolve(options['facts-path'] || path.join(path.dirname(statePath), 'bug-hunter-facts.json'));
|
|
666
1208
|
ensureDir(chunksDir);
|
|
667
1209
|
|
|
@@ -709,7 +1251,7 @@ async function runPipeline(options) {
|
|
|
709
1251
|
const state = readJson(statePath);
|
|
710
1252
|
const lowConfidenceFiles = normalizeFiles(state.bugLedger
|
|
711
1253
|
.filter((entry) => {
|
|
712
|
-
return entry.
|
|
1254
|
+
return entry.confidenceScore === null || entry.confidenceScore === undefined || Number(entry.confidenceScore) < confidenceThreshold;
|
|
713
1255
|
})
|
|
714
1256
|
.map((entry) => entry.file));
|
|
715
1257
|
if (lowConfidenceFiles.length > 0 && scope.indexPath) {
|
|
@@ -773,14 +1315,96 @@ async function runPipeline(options) {
|
|
|
773
1315
|
writeJson(consistencyReportPath, consistency);
|
|
774
1316
|
runJsonScript(stateScript, ['set-consistency', statePath, consistencyReportPath]);
|
|
775
1317
|
|
|
1318
|
+
const hasOpenOrFailedChunks = (status.summary.chunkStatus.pending || 0) > 0
|
|
1319
|
+
|| (status.summary.chunkStatus.inProgress || 0) > 0
|
|
1320
|
+
|| (status.summary.chunkStatus.failed || 0) > 0;
|
|
1321
|
+
|
|
1322
|
+
if (hasOpenOrFailedChunks) {
|
|
1323
|
+
appendJournal(journalPath, {
|
|
1324
|
+
event: 'fix-planning-skipped',
|
|
1325
|
+
reason: 'incomplete-or-failed-chunks',
|
|
1326
|
+
chunkStatus: status.summary.chunkStatus
|
|
1327
|
+
});
|
|
1328
|
+
|
|
1329
|
+
return {
|
|
1330
|
+
ok: true,
|
|
1331
|
+
backend,
|
|
1332
|
+
journalPath,
|
|
1333
|
+
statePath,
|
|
1334
|
+
indexPath: scope.indexPath,
|
|
1335
|
+
deltaMode: scope.deltaMode,
|
|
1336
|
+
deltaSummary: scope.deltaResult ? {
|
|
1337
|
+
selectedCount: (scope.deltaResult.selected || []).length,
|
|
1338
|
+
expansionCandidatesCount: (scope.deltaResult.expansionCandidates || []).length
|
|
1339
|
+
} : null,
|
|
1340
|
+
consistencyReportPath,
|
|
1341
|
+
strategyPath: null,
|
|
1342
|
+
strategyMarkdownPath: null,
|
|
1343
|
+
fixPlanPath: null,
|
|
1344
|
+
coveragePath: null,
|
|
1345
|
+
coverageMarkdownPath: null,
|
|
1346
|
+
factsPath,
|
|
1347
|
+
status: status.summary,
|
|
1348
|
+
consistency: {
|
|
1349
|
+
conflicts: consistency.conflicts.length,
|
|
1350
|
+
lowConfidenceFindings: consistency.lowConfidenceFindings
|
|
1351
|
+
},
|
|
1352
|
+
fixStrategy: null,
|
|
1353
|
+
fixPlan: null
|
|
1354
|
+
};
|
|
1355
|
+
}
|
|
1356
|
+
|
|
1357
|
+
const fixStrategy = buildFixStrategy({
|
|
1358
|
+
bugLedger: toArray(finalState.bugLedger),
|
|
1359
|
+
confidenceThreshold,
|
|
1360
|
+
consistency
|
|
1361
|
+
});
|
|
1362
|
+
const fixStrategyValidation = validateArtifactValue({
|
|
1363
|
+
artifactName: 'fix-strategy',
|
|
1364
|
+
value: fixStrategy
|
|
1365
|
+
});
|
|
1366
|
+
if (!fixStrategyValidation.ok) {
|
|
1367
|
+
throw new Error(`Generated invalid fix strategy artifact: ${fixStrategyValidation.errors.join('; ')}`);
|
|
1368
|
+
}
|
|
1369
|
+
writeJson(strategyPath, fixStrategy);
|
|
1370
|
+
ensureDir(path.dirname(strategyMarkdownPath));
|
|
1371
|
+
fs.writeFileSync(
|
|
1372
|
+
strategyMarkdownPath,
|
|
1373
|
+
runTextScript(path.join(skillDir, 'scripts', 'render-report.cjs'), ['fix-strategy', strategyPath]),
|
|
1374
|
+
'utf8'
|
|
1375
|
+
);
|
|
1376
|
+
|
|
776
1377
|
const fixPlan = buildFixPlan({
|
|
777
1378
|
bugLedger: toArray(finalState.bugLedger),
|
|
778
1379
|
confidenceThreshold,
|
|
779
|
-
canarySize
|
|
1380
|
+
canarySize,
|
|
1381
|
+
consistency
|
|
780
1382
|
});
|
|
1383
|
+
const fixPlanValidation = validateArtifactValue({
|
|
1384
|
+
artifactName: 'fix-plan',
|
|
1385
|
+
value: fixPlan
|
|
1386
|
+
});
|
|
1387
|
+
if (!fixPlanValidation.ok) {
|
|
1388
|
+
throw new Error(`Generated invalid fix plan artifact: ${fixPlanValidation.errors.join('; ')}`);
|
|
1389
|
+
}
|
|
781
1390
|
writeJson(fixPlanPath, fixPlan);
|
|
782
1391
|
runJsonScript(stateScript, ['set-fix-plan', statePath, fixPlanPath]);
|
|
783
1392
|
|
|
1393
|
+
const coverage = buildCoverageArtifact({
|
|
1394
|
+
state: finalState,
|
|
1395
|
+
fixPlan
|
|
1396
|
+
});
|
|
1397
|
+
const coverageValidation = validateArtifactValue({
|
|
1398
|
+
artifactName: 'coverage',
|
|
1399
|
+
value: coverage
|
|
1400
|
+
});
|
|
1401
|
+
if (!coverageValidation.ok) {
|
|
1402
|
+
throw new Error(`Generated invalid coverage artifact: ${coverageValidation.errors.join('; ')}`);
|
|
1403
|
+
}
|
|
1404
|
+
writeJson(coveragePath, coverage);
|
|
1405
|
+
ensureDir(path.dirname(coverageMarkdownPath));
|
|
1406
|
+
fs.writeFileSync(coverageMarkdownPath, renderCoverageMarkdown(coverage), 'utf8');
|
|
1407
|
+
|
|
784
1408
|
writeJson(factsPath, finalState.factCards || {});
|
|
785
1409
|
|
|
786
1410
|
appendJournal(journalPath, {
|
|
@@ -802,13 +1426,18 @@ async function runPipeline(options) {
|
|
|
802
1426
|
expansionCandidatesCount: (scope.deltaResult.expansionCandidates || []).length
|
|
803
1427
|
} : null,
|
|
804
1428
|
consistencyReportPath,
|
|
1429
|
+
strategyPath,
|
|
1430
|
+
strategyMarkdownPath,
|
|
805
1431
|
fixPlanPath,
|
|
1432
|
+
coveragePath,
|
|
1433
|
+
coverageMarkdownPath,
|
|
806
1434
|
factsPath,
|
|
807
1435
|
status: status.summary,
|
|
808
1436
|
consistency: {
|
|
809
1437
|
conflicts: consistency.conflicts.length,
|
|
810
1438
|
lowConfidenceFindings: consistency.lowConfidenceFindings
|
|
811
1439
|
},
|
|
1440
|
+
fixStrategy: fixStrategy.summary,
|
|
812
1441
|
fixPlan: fixPlan.totals
|
|
813
1442
|
};
|
|
814
1443
|
}
|
|
@@ -835,6 +1464,12 @@ async function main() {
|
|
|
835
1464
|
return;
|
|
836
1465
|
}
|
|
837
1466
|
|
|
1467
|
+
if (command === 'phase') {
|
|
1468
|
+
const result = await runPhase(options);
|
|
1469
|
+
console.log(JSON.stringify(result, null, 2));
|
|
1470
|
+
return;
|
|
1471
|
+
}
|
|
1472
|
+
|
|
838
1473
|
if (command === 'plan') {
|
|
839
1474
|
if (!options['files-json']) {
|
|
840
1475
|
throw new Error('--files-json is required for plan command');
|