auditor-lambda 0.3.38 → 0.3.40
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/dist/cli.js +292 -37
- package/dist/extractors/designAssessment.d.ts +11 -0
- package/dist/extractors/designAssessment.js +254 -0
- package/dist/io/artifacts.d.ts +3 -0
- package/dist/io/artifacts.js +1 -0
- package/dist/orchestrator/advance.js +7 -1
- package/dist/orchestrator/dependencyMap.js +5 -0
- package/dist/orchestrator/designReviewPrompt.d.ts +2 -0
- package/dist/orchestrator/designReviewPrompt.js +151 -0
- package/dist/orchestrator/executors.js +10 -0
- package/dist/orchestrator/internalExecutors.d.ts +2 -0
- package/dist/orchestrator/internalExecutors.js +43 -0
- package/dist/orchestrator/nextStep.js +2 -0
- package/dist/orchestrator/state.js +2 -0
- package/dist/reporting/mergeFindings.d.ts +2 -1
- package/dist/reporting/mergeFindings.js +13 -1
- package/dist/reporting/synthesis.d.ts +2 -0
- package/dist/reporting/synthesis.js +1 -1
- package/dist/types/designAssessment.d.ts +7 -0
- package/dist/types/designAssessment.js +1 -0
- package/dist/validation/auditResults.d.ts +1 -0
- package/dist/validation/auditResults.js +30 -12
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { mkdir, readFile, readdir, rename, rm, writeFile } from "node:fs/promises";
|
|
1
|
+
import { mkdir, readFile, readdir, rename, rm, unlink, writeFile } from "node:fs/promises";
|
|
2
2
|
import { createReadStream, existsSync } from "node:fs";
|
|
3
3
|
import { Buffer } from "node:buffer";
|
|
4
4
|
import { createHash } from "node:crypto";
|
|
@@ -22,6 +22,7 @@ import { buildAuditReportModel, renderAuditReportMarkdown, } from "./reporting/s
|
|
|
22
22
|
import { deriveAuditState } from "./orchestrator/state.js";
|
|
23
23
|
import { advanceAudit } from "./orchestrator/advance.js";
|
|
24
24
|
import { decideNextStep } from "./orchestrator/nextStep.js";
|
|
25
|
+
import { renderDesignReviewPrompt } from "./orchestrator/designReviewPrompt.js";
|
|
25
26
|
import { createFreshSessionProvider, resolveFreshSessionProviderName, } from "./providers/index.js";
|
|
26
27
|
import { appendRunLedgerEntry, loadRunLedger } from "./supervisor/runLedger.js";
|
|
27
28
|
import { buildAuditCodeHandoff, writeAuditCodeHandoffArtifacts, } from "./supervisor/operatorHandoff.js";
|
|
@@ -1038,6 +1039,32 @@ async function runDeterministicForNextStep(params) {
|
|
|
1038
1039
|
: join(params.artifactsDir, "audit-report.md"),
|
|
1039
1040
|
};
|
|
1040
1041
|
}
|
|
1042
|
+
if (decision.selected_executor === "design_review") {
|
|
1043
|
+
const findingsPath = join(params.artifactsDir, "incoming", "design-review-findings.json");
|
|
1044
|
+
let reviewFindings;
|
|
1045
|
+
try {
|
|
1046
|
+
reviewFindings = await readJsonFile(findingsPath);
|
|
1047
|
+
}
|
|
1048
|
+
catch (error) {
|
|
1049
|
+
if (!isFileMissingError(error))
|
|
1050
|
+
throw error;
|
|
1051
|
+
}
|
|
1052
|
+
if (reviewFindings && Array.isArray(reviewFindings)) {
|
|
1053
|
+
const existing = bundle.design_assessment;
|
|
1054
|
+
if (existing) {
|
|
1055
|
+
existing.review_findings = reviewFindings;
|
|
1056
|
+
existing.reviewed = true;
|
|
1057
|
+
await writeJsonFile(join(params.artifactsDir, "design_assessment.json"), existing);
|
|
1058
|
+
await unlink(findingsPath).catch(() => { });
|
|
1059
|
+
continue;
|
|
1060
|
+
}
|
|
1061
|
+
}
|
|
1062
|
+
return {
|
|
1063
|
+
kind: "design_review",
|
|
1064
|
+
state,
|
|
1065
|
+
bundle,
|
|
1066
|
+
};
|
|
1067
|
+
}
|
|
1041
1068
|
if (decision.selected_executor === "agent") {
|
|
1042
1069
|
return {
|
|
1043
1070
|
kind: "semantic_review",
|
|
@@ -1177,6 +1204,38 @@ async function cmdNextStep(argv) {
|
|
|
1177
1204
|
console.log(JSON.stringify(step, null, 2));
|
|
1178
1205
|
return;
|
|
1179
1206
|
}
|
|
1207
|
+
if (result.kind === "design_review") {
|
|
1208
|
+
const designReviewResultsPath = join(artifactsDir, "incoming", "design-review-findings.json");
|
|
1209
|
+
await mkdir(join(artifactsDir, "incoming"), { recursive: true });
|
|
1210
|
+
const continueCommand = nextStepCommand(root, artifactsDir);
|
|
1211
|
+
const prompt = renderDesignReviewPrompt(result.bundle);
|
|
1212
|
+
const fullPrompt = [
|
|
1213
|
+
prompt,
|
|
1214
|
+
"## Results path",
|
|
1215
|
+
"",
|
|
1216
|
+
`Write the JSON array of findings to:`,
|
|
1217
|
+
"",
|
|
1218
|
+
` ${designReviewResultsPath}`,
|
|
1219
|
+
"",
|
|
1220
|
+
`Then run: ${continueCommand}`,
|
|
1221
|
+
"",
|
|
1222
|
+
].join("\n");
|
|
1223
|
+
const step = await writeCurrentStep({
|
|
1224
|
+
artifactsDir,
|
|
1225
|
+
stepKind: "design_review",
|
|
1226
|
+
status: "ready",
|
|
1227
|
+
runId: null,
|
|
1228
|
+
allowedCommands: [continueCommand],
|
|
1229
|
+
stopCondition: "Write design review findings to the results path, then run next-step.",
|
|
1230
|
+
repoRoot: root,
|
|
1231
|
+
artifactPaths: {
|
|
1232
|
+
design_review_results: designReviewResultsPath,
|
|
1233
|
+
},
|
|
1234
|
+
prompt: fullPrompt,
|
|
1235
|
+
});
|
|
1236
|
+
console.log(JSON.stringify(step, null, 2));
|
|
1237
|
+
return;
|
|
1238
|
+
}
|
|
1180
1239
|
if (!hostCanDispatch) {
|
|
1181
1240
|
const singleTaskPromptPath = join(artifactsDir, "dispatch", "current-single-task-prompt.md");
|
|
1182
1241
|
const workerCommand = renderCommand(result.activeReviewRun.worker_command);
|
|
@@ -2083,6 +2142,7 @@ async function cmdWorkerRun(argv) {
|
|
|
2083
2142
|
}
|
|
2084
2143
|
}
|
|
2085
2144
|
const DISPATCH_RESULT_MAP_FILENAME = "dispatch-result-map.json";
|
|
2145
|
+
const ACTIVE_DISPATCH_FILENAME = "active-dispatch.json";
|
|
2086
2146
|
function dispatchResultMapPath(runDir) {
|
|
2087
2147
|
return join(runDir, DISPATCH_RESULT_MAP_FILENAME);
|
|
2088
2148
|
}
|
|
@@ -2378,6 +2438,10 @@ async function prepareDispatchArtifacts(params) {
|
|
|
2378
2438
|
.map(([key, value]) => `input.${key}: ${value}`)
|
|
2379
2439
|
: [];
|
|
2380
2440
|
const isLensVerification = task.tags?.includes("lens_verification") ?? false;
|
|
2441
|
+
const coverageTemplate = task.file_paths.map((path) => ({
|
|
2442
|
+
path,
|
|
2443
|
+
total_lines: task.file_line_counts?.[path] ?? lineIndex[path] ?? 0,
|
|
2444
|
+
}));
|
|
2381
2445
|
return [
|
|
2382
2446
|
`### ${task.task_id}`,
|
|
2383
2447
|
`unit_id: ${task.unit_id}`,
|
|
@@ -2398,6 +2462,11 @@ async function prepareDispatchArtifacts(params) {
|
|
|
2398
2462
|
]
|
|
2399
2463
|
: []),
|
|
2400
2464
|
"",
|
|
2465
|
+
"file_coverage (copy exactly into your AuditResult for this task):",
|
|
2466
|
+
"```json",
|
|
2467
|
+
JSON.stringify(coverageTemplate),
|
|
2468
|
+
"```",
|
|
2469
|
+
"",
|
|
2401
2470
|
];
|
|
2402
2471
|
});
|
|
2403
2472
|
const submitCommand = `"${process.execPath}" "${join(packageRoot, "audit-code.mjs")}" submit-packet ` +
|
|
@@ -2442,7 +2511,7 @@ async function prepareDispatchArtifacts(params) {
|
|
|
2442
2511
|
" unit_id copy from the task metadata",
|
|
2443
2512
|
" pass_id copy from the task metadata",
|
|
2444
2513
|
" lens copy from the task metadata",
|
|
2445
|
-
" file_coverage [{path, total_lines}] -
|
|
2514
|
+
" file_coverage [{path, total_lines}] - copy the exact template from each task section above",
|
|
2446
2515
|
" findings [] or array of finding objects",
|
|
2447
2516
|
"",
|
|
2448
2517
|
"Lens verification tasks:",
|
|
@@ -2555,6 +2624,14 @@ async function prepareDispatchArtifacts(params) {
|
|
|
2555
2624
|
if (warningsPath) {
|
|
2556
2625
|
await writeJsonFile(warningsPath, warnings);
|
|
2557
2626
|
}
|
|
2627
|
+
const activeDispatch = {
|
|
2628
|
+
run_id: runId,
|
|
2629
|
+
created_at: new Date().toISOString(),
|
|
2630
|
+
packet_count: plan.length,
|
|
2631
|
+
task_count: orderedTasks.length,
|
|
2632
|
+
status: "active",
|
|
2633
|
+
};
|
|
2634
|
+
await writeJsonFile(join(artifactsDir, ACTIVE_DISPATCH_FILENAME), activeDispatch);
|
|
2558
2635
|
return {
|
|
2559
2636
|
run_id: runId,
|
|
2560
2637
|
dispatch_plan_path: dispatchPlanPath,
|
|
@@ -2602,12 +2679,23 @@ async function cmdSubmitPacket(argv) {
|
|
|
2602
2679
|
if (!resultMap) {
|
|
2603
2680
|
throw new Error(`No ${DISPATCH_RESULT_MAP_FILENAME} found for run ${runId}; run prepare-dispatch first.`);
|
|
2604
2681
|
}
|
|
2605
|
-
|
|
2682
|
+
let packetEntries = resultMap.entries.filter((entry) => entry.packet_id === packetId);
|
|
2683
|
+
let resolvedPacketId = packetId;
|
|
2606
2684
|
if (packetEntries.length === 0) {
|
|
2607
|
-
|
|
2685
|
+
const trimmed = packetId.trim();
|
|
2686
|
+
packetEntries = resultMap.entries.filter((entry) => entry.packet_id.trim().toLowerCase() === trimmed.toLowerCase());
|
|
2687
|
+
if (packetEntries.length > 0) {
|
|
2688
|
+
resolvedPacketId = packetEntries[0].packet_id;
|
|
2689
|
+
process.stderr.write(`[submit-packet] Resolved packet_id '${packetId}' → '${resolvedPacketId}' (case/whitespace normalization)\n`);
|
|
2690
|
+
}
|
|
2691
|
+
}
|
|
2692
|
+
if (packetEntries.length === 0) {
|
|
2693
|
+
const knownIds = [...new Set(resultMap.entries.map((e) => e.packet_id))];
|
|
2694
|
+
throw new Error(`Unknown packet_id '${packetId}' for run ${runId}.\n` +
|
|
2695
|
+
`Valid packet IDs: ${knownIds.join(", ")}`);
|
|
2608
2696
|
}
|
|
2609
2697
|
if (entriesByTaskId(packetEntries).size !== packetEntries.length) {
|
|
2610
|
-
throw new Error(`Dispatch result map has duplicate task entries for packet '${
|
|
2698
|
+
throw new Error(`Dispatch result map has duplicate task entries for packet '${resolvedPacketId}'.`);
|
|
2611
2699
|
}
|
|
2612
2700
|
const allTasks = await readJsonFile(tasksPath);
|
|
2613
2701
|
const taskById = new Map(allTasks.map((task) => [task.task_id, task]));
|
|
@@ -2658,7 +2746,7 @@ async function cmdSubmitPacket(argv) {
|
|
|
2658
2746
|
}
|
|
2659
2747
|
seen.add(taskId);
|
|
2660
2748
|
if (!expectedTaskIds.has(taskId)) {
|
|
2661
|
-
resultErrors.push(`Result at index ${index} uses task_id '${taskId}', which is not assigned to packet '${
|
|
2749
|
+
resultErrors.push(`Result at index ${index} uses task_id '${taskId}', which is not assigned to packet '${resolvedPacketId}'.`);
|
|
2662
2750
|
}
|
|
2663
2751
|
}
|
|
2664
2752
|
for (const task of tasks) {
|
|
@@ -2668,7 +2756,44 @@ async function cmdSubmitPacket(argv) {
|
|
|
2668
2756
|
}
|
|
2669
2757
|
}
|
|
2670
2758
|
if (resultErrors.length > 0) {
|
|
2671
|
-
throw new Error(`submit-packet rejected ${
|
|
2759
|
+
throw new Error(`submit-packet rejected ${resolvedPacketId}:\n${resultErrors.join("\n")}`);
|
|
2760
|
+
}
|
|
2761
|
+
// Check for duplicate findings against already-submitted results in this run
|
|
2762
|
+
const existingFindingKeys = new Set();
|
|
2763
|
+
const otherEntries = resultMap.entries.filter((e) => e.packet_id !== resolvedPacketId);
|
|
2764
|
+
for (const other of otherEntries) {
|
|
2765
|
+
try {
|
|
2766
|
+
const existing = JSON.parse(await readFile(other.result_path, "utf8"));
|
|
2767
|
+
if (existing?.findings) {
|
|
2768
|
+
for (const f of existing.findings) {
|
|
2769
|
+
const key = [
|
|
2770
|
+
(f.lens ?? "").trim().toLowerCase(),
|
|
2771
|
+
(f.category ?? "").trim().toLowerCase(),
|
|
2772
|
+
(f.title ?? "").trim().toLowerCase(),
|
|
2773
|
+
f.affected_files?.[0]?.path ?? "",
|
|
2774
|
+
].join("|");
|
|
2775
|
+
existingFindingKeys.add(key);
|
|
2776
|
+
}
|
|
2777
|
+
}
|
|
2778
|
+
}
|
|
2779
|
+
catch { /* file doesn't exist yet or invalid — skip */ }
|
|
2780
|
+
}
|
|
2781
|
+
let dupCount = 0;
|
|
2782
|
+
for (const result of payload) {
|
|
2783
|
+
for (const f of result.findings ?? []) {
|
|
2784
|
+
const key = [
|
|
2785
|
+
(f.lens ?? "").trim().toLowerCase(),
|
|
2786
|
+
(f.category ?? "").trim().toLowerCase(),
|
|
2787
|
+
(f.title ?? "").trim().toLowerCase(),
|
|
2788
|
+
f.affected_files?.[0]?.path ?? "",
|
|
2789
|
+
].join("|");
|
|
2790
|
+
if (existingFindingKeys.has(key)) {
|
|
2791
|
+
dupCount++;
|
|
2792
|
+
}
|
|
2793
|
+
}
|
|
2794
|
+
}
|
|
2795
|
+
if (dupCount > 0) {
|
|
2796
|
+
process.stderr.write(`[submit-packet] Warning: ${dupCount} finding(s) appear to duplicate findings from other packets in this run.\n`);
|
|
2672
2797
|
}
|
|
2673
2798
|
const entryByTaskId = entriesByTaskId(packetEntries);
|
|
2674
2799
|
for (const result of payload) {
|
|
@@ -2681,9 +2806,10 @@ async function cmdSubmitPacket(argv) {
|
|
|
2681
2806
|
const findingCount = payload.reduce((sum, result) => sum + result.findings.length, 0);
|
|
2682
2807
|
console.log(JSON.stringify({
|
|
2683
2808
|
run_id: runId,
|
|
2684
|
-
packet_id:
|
|
2809
|
+
packet_id: resolvedPacketId,
|
|
2685
2810
|
accepted_count: payload.length,
|
|
2686
2811
|
finding_count: findingCount,
|
|
2812
|
+
...(dupCount > 0 ? { duplicate_warning_count: dupCount } : {}),
|
|
2687
2813
|
}, null, 2));
|
|
2688
2814
|
}
|
|
2689
2815
|
async function cmdMergeAndIngest(argv) {
|
|
@@ -2722,11 +2848,24 @@ async function cmdMergeAndIngest(argv) {
|
|
|
2722
2848
|
const failing = [];
|
|
2723
2849
|
const seenTaskIds = new Set();
|
|
2724
2850
|
let spuriousFileCount = 0;
|
|
2851
|
+
const fallbackByTaskId = new Map();
|
|
2725
2852
|
for (const filename of files) {
|
|
2726
2853
|
const filePath = resolve(join(taskResultsDir, filename));
|
|
2727
2854
|
if (!expectedPaths.has(filePath)) {
|
|
2728
2855
|
spuriousFileCount++;
|
|
2729
|
-
|
|
2856
|
+
try {
|
|
2857
|
+
const raw = await readFile(filePath, "utf8");
|
|
2858
|
+
const parsed = JSON.parse(raw);
|
|
2859
|
+
if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
|
|
2860
|
+
const tid = typeof parsed.task_id === "string"
|
|
2861
|
+
? String(parsed.task_id) : undefined;
|
|
2862
|
+
if (tid && !fallbackByTaskId.has(tid)) {
|
|
2863
|
+
fallbackByTaskId.set(tid, parsed);
|
|
2864
|
+
}
|
|
2865
|
+
}
|
|
2866
|
+
}
|
|
2867
|
+
catch { /* not parseable — skip */ }
|
|
2868
|
+
process.stderr.write(`[merge-and-ingest] Warning: unexpected file in task-results/: ${filename}\n`);
|
|
2730
2869
|
}
|
|
2731
2870
|
}
|
|
2732
2871
|
for (const task of allTasks) {
|
|
@@ -2745,15 +2884,23 @@ async function cmdMergeAndIngest(argv) {
|
|
|
2745
2884
|
}
|
|
2746
2885
|
catch (e) {
|
|
2747
2886
|
if (isFileMissingError(e)) {
|
|
2748
|
-
|
|
2749
|
-
|
|
2750
|
-
|
|
2751
|
-
|
|
2887
|
+
const fallback = fallbackByTaskId.get(task.task_id);
|
|
2888
|
+
if (fallback) {
|
|
2889
|
+
process.stderr.write(`[merge-and-ingest] Recovered result for '${task.task_id}' from unexpected file (matched by task_id)\n`);
|
|
2890
|
+
obj = fallback;
|
|
2891
|
+
}
|
|
2892
|
+
else {
|
|
2893
|
+
failing.push({
|
|
2894
|
+
task_id: task.task_id,
|
|
2895
|
+
errors: ["Missing audit result for assigned task."],
|
|
2896
|
+
});
|
|
2897
|
+
continue;
|
|
2898
|
+
}
|
|
2752
2899
|
}
|
|
2753
2900
|
else {
|
|
2754
2901
|
failing.push({ task_id: task.task_id, errors: [`Invalid JSON: ${e.message}`] });
|
|
2902
|
+
continue;
|
|
2755
2903
|
}
|
|
2756
|
-
continue;
|
|
2757
2904
|
}
|
|
2758
2905
|
const record = obj && typeof obj === "object" && !Array.isArray(obj)
|
|
2759
2906
|
? obj
|
|
@@ -2784,45 +2931,87 @@ async function cmdMergeAndIngest(argv) {
|
|
|
2784
2931
|
}
|
|
2785
2932
|
}
|
|
2786
2933
|
await writeJsonFile(auditResultsPath, passing);
|
|
2934
|
+
const failedTasksPath = join(runDir, "failed-tasks.json");
|
|
2787
2935
|
if (failing.length > 0) {
|
|
2788
|
-
const failedTasksPath = join(runDir, "failed-tasks.json");
|
|
2789
2936
|
await writeJsonFile(failedTasksPath, failing);
|
|
2790
|
-
|
|
2937
|
+
}
|
|
2938
|
+
if (passing.length === 0 && failing.length > 0) {
|
|
2939
|
+
throw new Error(`All ${failing.length} assigned task result(s) were missing or invalid; blocked before ingestion. See ${failedTasksPath}`);
|
|
2791
2940
|
}
|
|
2792
2941
|
const findingCount = passing.reduce((sum, result) => sum + result.findings.length, 0);
|
|
2793
|
-
|
|
2794
|
-
|
|
2795
|
-
|
|
2796
|
-
|
|
2797
|
-
|
|
2798
|
-
|
|
2799
|
-
|
|
2800
|
-
|
|
2942
|
+
let result = null;
|
|
2943
|
+
if (passing.length > 0) {
|
|
2944
|
+
result = await runAuditStep({
|
|
2945
|
+
root: workerTask.repo_root,
|
|
2946
|
+
artifactsDir,
|
|
2947
|
+
preferredExecutor: "result_ingestion_executor",
|
|
2948
|
+
auditResultsPath,
|
|
2949
|
+
});
|
|
2950
|
+
const updatedPendingTasks = await addFileLineCountHints(workerTask.repo_root, buildPendingAuditTasks(result.updated_bundle));
|
|
2951
|
+
await writeJsonFile(tasksPath, updatedPendingTasks);
|
|
2952
|
+
}
|
|
2953
|
+
const activeDispatchPath = join(artifactsDir, ACTIVE_DISPATCH_FILENAME);
|
|
2954
|
+
try {
|
|
2955
|
+
const dispatch = await readJsonFile(activeDispatchPath);
|
|
2956
|
+
if (dispatch.run_id === runId) {
|
|
2957
|
+
dispatch.status = failing.length > 0 ? "active" : "merged";
|
|
2958
|
+
await writeJsonFile(activeDispatchPath, dispatch);
|
|
2959
|
+
}
|
|
2960
|
+
}
|
|
2961
|
+
catch { /* no active dispatch file — skip */ }
|
|
2962
|
+
let retryDispatchPath = null;
|
|
2963
|
+
if (failing.length > 0) {
|
|
2964
|
+
const failedTaskIds = new Set(failing.map((f) => f.task_id));
|
|
2965
|
+
const failedPacketIds = [
|
|
2966
|
+
...new Set(resultMap.entries
|
|
2967
|
+
.filter((e) => failedTaskIds.has(e.task_id))
|
|
2968
|
+
.map((e) => e.packet_id)),
|
|
2969
|
+
];
|
|
2970
|
+
const retryDispatch = {
|
|
2971
|
+
run_id: runId,
|
|
2972
|
+
retry_packet_ids: failedPacketIds,
|
|
2973
|
+
failed_task_count: failing.length,
|
|
2974
|
+
accepted_task_count: passing.length,
|
|
2975
|
+
};
|
|
2976
|
+
retryDispatchPath = join(runDir, "retry-dispatch.json");
|
|
2977
|
+
await writeJsonFile(retryDispatchPath, retryDispatch);
|
|
2978
|
+
process.stderr.write(`[merge-and-ingest] ${passing.length} accepted, ${failing.length} failed. ` +
|
|
2979
|
+
`Retry packets: ${failedPacketIds.join(", ")}\n`);
|
|
2980
|
+
}
|
|
2981
|
+
const status = failing.length > 0
|
|
2982
|
+
? "partial"
|
|
2983
|
+
: (result?.progress_made ? "completed" : "no_progress");
|
|
2801
2984
|
const workerResult = buildWorkerResult({
|
|
2802
2985
|
runId,
|
|
2803
2986
|
obligationId: workerTask.obligation_id,
|
|
2804
|
-
status: result
|
|
2805
|
-
progressMade: result
|
|
2806
|
-
selectedExecutor: result
|
|
2807
|
-
artifactsWritten: result
|
|
2808
|
-
summary: result.
|
|
2809
|
-
nextLikelyStep: result
|
|
2987
|
+
status: failing.length > 0 ? "no_progress" : (result?.progress_made ? "completed" : "no_progress"),
|
|
2988
|
+
progressMade: result?.progress_made ?? false,
|
|
2989
|
+
selectedExecutor: result?.selected_executor ?? null,
|
|
2990
|
+
artifactsWritten: result?.artifacts_written ?? [],
|
|
2991
|
+
summary: result?.progress_summary ?? `${failing.length} task(s) failed`,
|
|
2992
|
+
nextLikelyStep: result?.next_likely_step ?? null,
|
|
2810
2993
|
errors: [],
|
|
2811
2994
|
});
|
|
2812
2995
|
await writeJsonFile(workerTask.result_path, workerResult);
|
|
2813
2996
|
console.log(JSON.stringify({
|
|
2814
2997
|
run_id: runId,
|
|
2815
|
-
status
|
|
2998
|
+
status,
|
|
2816
2999
|
accepted_count: passing.length,
|
|
2817
|
-
rejected_count:
|
|
3000
|
+
rejected_count: failing.length,
|
|
2818
3001
|
spurious_file_count: spuriousFileCount,
|
|
2819
3002
|
finding_count: findingCount,
|
|
2820
3003
|
audit_results_path: auditResultsPath,
|
|
2821
|
-
|
|
2822
|
-
|
|
2823
|
-
|
|
2824
|
-
|
|
3004
|
+
...(retryDispatchPath ? { retry_dispatch_path: retryDispatchPath } : {}),
|
|
3005
|
+
...(result ? {
|
|
3006
|
+
selected_executor: workerResult.selected_executor,
|
|
3007
|
+
progress_made: workerResult.progress_made,
|
|
3008
|
+
progress_summary: workerResult.summary,
|
|
3009
|
+
next_likely_step: workerResult.next_likely_step,
|
|
3010
|
+
} : {}),
|
|
2825
3011
|
}, null, 2));
|
|
3012
|
+
if (failing.length > 0) {
|
|
3013
|
+
process.exitCode = 2;
|
|
3014
|
+
}
|
|
2826
3015
|
}
|
|
2827
3016
|
async function cmdValidateResult(argv) {
|
|
2828
3017
|
const rawRunId = getFlag(argv, "--run-id");
|
|
@@ -3298,6 +3487,69 @@ async function cmdQuota(argv) {
|
|
|
3298
3487
|
quota_state_path: getQuotaStatePath(),
|
|
3299
3488
|
}, null, 2));
|
|
3300
3489
|
}
|
|
3490
|
+
async function cmdDispatchStatus(argv) {
|
|
3491
|
+
const artifactsDir = getArtifactsDir(argv);
|
|
3492
|
+
const activeDispatchPath = join(artifactsDir, ACTIVE_DISPATCH_FILENAME);
|
|
3493
|
+
let activeDispatch = null;
|
|
3494
|
+
try {
|
|
3495
|
+
activeDispatch = await readJsonFile(activeDispatchPath);
|
|
3496
|
+
}
|
|
3497
|
+
catch (e) {
|
|
3498
|
+
if (!isFileMissingError(e))
|
|
3499
|
+
throw e;
|
|
3500
|
+
}
|
|
3501
|
+
if (!activeDispatch) {
|
|
3502
|
+
console.log(JSON.stringify({ status: "no_active_dispatch" }, null, 2));
|
|
3503
|
+
return;
|
|
3504
|
+
}
|
|
3505
|
+
const runDir = join(artifactsDir, "runs", activeDispatch.run_id);
|
|
3506
|
+
const resultMap = await loadDispatchResultMap(runDir);
|
|
3507
|
+
if (!resultMap) {
|
|
3508
|
+
console.log(JSON.stringify({
|
|
3509
|
+
status: "missing_result_map",
|
|
3510
|
+
run_id: activeDispatch.run_id,
|
|
3511
|
+
}, null, 2));
|
|
3512
|
+
return;
|
|
3513
|
+
}
|
|
3514
|
+
const packetIds = [...new Set(resultMap.entries.map((e) => e.packet_id))];
|
|
3515
|
+
const packetStatus = [];
|
|
3516
|
+
for (const pid of packetIds) {
|
|
3517
|
+
if (pid === "__prior_dispatch__")
|
|
3518
|
+
continue;
|
|
3519
|
+
const entries = resultMap.entries.filter((e) => e.packet_id === pid);
|
|
3520
|
+
let completed = 0;
|
|
3521
|
+
const missing = [];
|
|
3522
|
+
for (const entry of entries) {
|
|
3523
|
+
try {
|
|
3524
|
+
await readFile(entry.result_path, "utf8");
|
|
3525
|
+
completed++;
|
|
3526
|
+
}
|
|
3527
|
+
catch {
|
|
3528
|
+
missing.push(entry.task_id);
|
|
3529
|
+
}
|
|
3530
|
+
}
|
|
3531
|
+
packetStatus.push({
|
|
3532
|
+
packet_id: pid,
|
|
3533
|
+
task_count: entries.length,
|
|
3534
|
+
completed_count: completed,
|
|
3535
|
+
missing_task_ids: missing,
|
|
3536
|
+
});
|
|
3537
|
+
}
|
|
3538
|
+
const totalTasks = packetStatus.reduce((s, p) => s + p.task_count, 0);
|
|
3539
|
+
const completedTasks = packetStatus.reduce((s, p) => s + p.completed_count, 0);
|
|
3540
|
+
const completedPackets = packetStatus.filter((p) => p.missing_task_ids.length === 0).length;
|
|
3541
|
+
console.log(JSON.stringify({
|
|
3542
|
+
run_id: activeDispatch.run_id,
|
|
3543
|
+
dispatch_status: activeDispatch.status,
|
|
3544
|
+
created_at: activeDispatch.created_at,
|
|
3545
|
+
total_packets: packetStatus.length,
|
|
3546
|
+
completed_packets: completedPackets,
|
|
3547
|
+
total_tasks: totalTasks,
|
|
3548
|
+
completed_tasks: completedTasks,
|
|
3549
|
+
missing_tasks: totalTasks - completedTasks,
|
|
3550
|
+
packets: packetStatus,
|
|
3551
|
+
}, null, 2));
|
|
3552
|
+
}
|
|
3301
3553
|
async function main(argv) {
|
|
3302
3554
|
const command = argv[2] ?? "sample-run";
|
|
3303
3555
|
switch (command) {
|
|
@@ -3370,9 +3622,12 @@ async function main(argv) {
|
|
|
3370
3622
|
case "status":
|
|
3371
3623
|
await cmdStatus(argv);
|
|
3372
3624
|
return;
|
|
3625
|
+
case "dispatch-status":
|
|
3626
|
+
await cmdDispatchStatus(argv);
|
|
3627
|
+
return;
|
|
3373
3628
|
default:
|
|
3374
3629
|
console.error(`Unknown command: ${command}`);
|
|
3375
|
-
console.error("Available commands: sample-run, advance-audit, next-step, run-to-completion, worker-run, import-external-analyzer, intake, plan, ingest-results, explain-task, update-runtime-validation, validate, validate-results, requeue, synthesize, cleanup, mcp, prepare-dispatch, merge-and-ingest, submit-packet, validate-result, quota, status");
|
|
3630
|
+
console.error("Available commands: sample-run, advance-audit, next-step, run-to-completion, worker-run, import-external-analyzer, intake, plan, ingest-results, explain-task, update-runtime-validation, validate, validate-results, requeue, synthesize, cleanup, mcp, prepare-dispatch, merge-and-ingest, submit-packet, validate-result, quota, status, dispatch-status");
|
|
3376
3631
|
process.exitCode = 1;
|
|
3377
3632
|
}
|
|
3378
3633
|
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { UnitManifest } from "../types.js";
|
|
2
|
+
import type { DesignAssessment } from "../types/designAssessment.js";
|
|
3
|
+
import type { GraphBundle } from "../types/graph.js";
|
|
4
|
+
import type { CriticalFlowManifest } from "../types/flows.js";
|
|
5
|
+
import type { RiskRegister } from "../types/risk.js";
|
|
6
|
+
export declare function buildDesignAssessment(params: {
|
|
7
|
+
unitManifest: UnitManifest;
|
|
8
|
+
graphBundle: GraphBundle;
|
|
9
|
+
criticalFlows: CriticalFlowManifest;
|
|
10
|
+
riskRegister: RiskRegister;
|
|
11
|
+
}): DesignAssessment;
|
|
@@ -0,0 +1,254 @@
|
|
|
1
|
+
let nextFindingId = 1;
|
|
2
|
+
function findingId() {
|
|
3
|
+
return `DA-${String(nextFindingId++).padStart(3, "0")}`;
|
|
4
|
+
}
|
|
5
|
+
function allEdges(graphBundle) {
|
|
6
|
+
const edges = [];
|
|
7
|
+
for (const [key, value] of Object.entries(graphBundle.graphs)) {
|
|
8
|
+
if (key === "routes" || !Array.isArray(value))
|
|
9
|
+
continue;
|
|
10
|
+
for (const edge of value) {
|
|
11
|
+
if (edge && typeof edge.from === "string" && typeof edge.to === "string") {
|
|
12
|
+
edges.push(edge);
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
return edges;
|
|
17
|
+
}
|
|
18
|
+
function detectCycles(edges) {
|
|
19
|
+
const adjacency = new Map();
|
|
20
|
+
for (const edge of edges) {
|
|
21
|
+
if (!adjacency.has(edge.from))
|
|
22
|
+
adjacency.set(edge.from, new Set());
|
|
23
|
+
adjacency.get(edge.from).add(edge.to);
|
|
24
|
+
}
|
|
25
|
+
const cycles = [];
|
|
26
|
+
const visited = new Set();
|
|
27
|
+
const stack = new Set();
|
|
28
|
+
function dfs(node, path) {
|
|
29
|
+
if (stack.has(node)) {
|
|
30
|
+
const cycleStart = path.indexOf(node);
|
|
31
|
+
if (cycleStart >= 0) {
|
|
32
|
+
cycles.push(path.slice(cycleStart));
|
|
33
|
+
}
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
36
|
+
if (visited.has(node))
|
|
37
|
+
return;
|
|
38
|
+
visited.add(node);
|
|
39
|
+
stack.add(node);
|
|
40
|
+
path.push(node);
|
|
41
|
+
for (const neighbor of adjacency.get(node) ?? []) {
|
|
42
|
+
dfs(neighbor, path);
|
|
43
|
+
}
|
|
44
|
+
path.pop();
|
|
45
|
+
stack.delete(node);
|
|
46
|
+
}
|
|
47
|
+
for (const node of adjacency.keys()) {
|
|
48
|
+
dfs(node, []);
|
|
49
|
+
}
|
|
50
|
+
return cycles;
|
|
51
|
+
}
|
|
52
|
+
function deduplicateCycles(cycles) {
|
|
53
|
+
const seen = new Set();
|
|
54
|
+
const unique = [];
|
|
55
|
+
for (const cycle of cycles) {
|
|
56
|
+
const normalized = [...cycle].sort().join("\0");
|
|
57
|
+
if (!seen.has(normalized)) {
|
|
58
|
+
seen.add(normalized);
|
|
59
|
+
unique.push(cycle);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
return unique;
|
|
63
|
+
}
|
|
64
|
+
function detectCycleFindings(graphBundle) {
|
|
65
|
+
const edges = allEdges(graphBundle);
|
|
66
|
+
const cycles = deduplicateCycles(detectCycles(edges));
|
|
67
|
+
if (cycles.length === 0)
|
|
68
|
+
return [];
|
|
69
|
+
return cycles.slice(0, 10).map((cycle) => ({
|
|
70
|
+
id: findingId(),
|
|
71
|
+
title: `Dependency cycle: ${cycle.length} modules`,
|
|
72
|
+
category: "dependency_cycle",
|
|
73
|
+
severity: cycle.length > 4 ? "high" : "medium",
|
|
74
|
+
confidence: "high",
|
|
75
|
+
lens: "architecture",
|
|
76
|
+
summary: `Circular dependency among ${cycle.join(" → ")} → ${cycle[0]}. Cycles increase coupling, complicate testing, and can cause initialization-order bugs.`,
|
|
77
|
+
affected_files: cycle.map((path) => ({ path })),
|
|
78
|
+
systemic: true,
|
|
79
|
+
}));
|
|
80
|
+
}
|
|
81
|
+
function detectHubModules(graphBundle) {
|
|
82
|
+
const edges = allEdges(graphBundle);
|
|
83
|
+
const fanIn = new Map();
|
|
84
|
+
const fanOut = new Map();
|
|
85
|
+
for (const edge of edges) {
|
|
86
|
+
fanOut.set(edge.from, (fanOut.get(edge.from) ?? 0) + 1);
|
|
87
|
+
fanIn.set(edge.to, (fanIn.get(edge.to) ?? 0) + 1);
|
|
88
|
+
}
|
|
89
|
+
const allNodes = new Set([...fanIn.keys(), ...fanOut.keys()]);
|
|
90
|
+
const hubThreshold = Math.max(8, Math.ceil(allNodes.size * 0.15));
|
|
91
|
+
const findings = [];
|
|
92
|
+
for (const node of allNodes) {
|
|
93
|
+
const inCount = fanIn.get(node) ?? 0;
|
|
94
|
+
const outCount = fanOut.get(node) ?? 0;
|
|
95
|
+
if (inCount >= hubThreshold && outCount >= hubThreshold) {
|
|
96
|
+
findings.push({
|
|
97
|
+
id: findingId(),
|
|
98
|
+
title: `Hub module: ${node}`,
|
|
99
|
+
category: "hub_module",
|
|
100
|
+
severity: "medium",
|
|
101
|
+
confidence: "high",
|
|
102
|
+
lens: "architecture",
|
|
103
|
+
summary: `${node} has ${inCount} incoming and ${outCount} outgoing dependencies. Hub modules become change bottlenecks and make the dependency graph fragile.`,
|
|
104
|
+
affected_files: [{ path: node }],
|
|
105
|
+
systemic: true,
|
|
106
|
+
});
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
return findings;
|
|
110
|
+
}
|
|
111
|
+
function detectOrphanUnits(unitManifest, graphBundle) {
|
|
112
|
+
const edges = allEdges(graphBundle);
|
|
113
|
+
const connected = new Set();
|
|
114
|
+
for (const edge of edges) {
|
|
115
|
+
connected.add(edge.from);
|
|
116
|
+
connected.add(edge.to);
|
|
117
|
+
}
|
|
118
|
+
if (connected.size === 0)
|
|
119
|
+
return [];
|
|
120
|
+
const orphans = [];
|
|
121
|
+
for (const unit of unitManifest.units) {
|
|
122
|
+
const hasConnection = unit.files.some((file) => connected.has(file));
|
|
123
|
+
if (!hasConnection && unit.files.length > 0) {
|
|
124
|
+
orphans.push(unit.unit_id);
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
if (orphans.length === 0)
|
|
128
|
+
return [];
|
|
129
|
+
if (orphans.length > unitManifest.units.length * 0.5)
|
|
130
|
+
return [];
|
|
131
|
+
return [{
|
|
132
|
+
id: findingId(),
|
|
133
|
+
title: `${orphans.length} orphan unit(s) with no graph connections`,
|
|
134
|
+
category: "orphan_units",
|
|
135
|
+
severity: "low",
|
|
136
|
+
confidence: "medium",
|
|
137
|
+
lens: "architecture",
|
|
138
|
+
summary: `Units [${orphans.join(", ")}] have no import, call, or reference edges in the dependency graph. They may be dead code, or the graph extraction missed their connections.`,
|
|
139
|
+
affected_files: orphans.map((id) => {
|
|
140
|
+
const unit = unitManifest.units.find((u) => u.unit_id === id);
|
|
141
|
+
return { path: unit?.files[0] ?? id };
|
|
142
|
+
}),
|
|
143
|
+
systemic: true,
|
|
144
|
+
}];
|
|
145
|
+
}
|
|
146
|
+
function detectRiskConcentration(riskRegister, unitManifest) {
|
|
147
|
+
if (riskRegister.items.length < 4)
|
|
148
|
+
return [];
|
|
149
|
+
const sorted = [...riskRegister.items].sort((a, b) => b.risk_score - a.risk_score);
|
|
150
|
+
const topQuartileSize = Math.max(1, Math.ceil(sorted.length * 0.25));
|
|
151
|
+
const topQuartile = sorted.slice(0, topQuartileSize);
|
|
152
|
+
const totalRisk = sorted.reduce((sum, item) => sum + item.risk_score, 0);
|
|
153
|
+
const topRisk = topQuartile.reduce((sum, item) => sum + item.risk_score, 0);
|
|
154
|
+
if (totalRisk === 0)
|
|
155
|
+
return [];
|
|
156
|
+
const concentration = topRisk / totalRisk;
|
|
157
|
+
if (concentration < 0.6)
|
|
158
|
+
return [];
|
|
159
|
+
return [{
|
|
160
|
+
id: findingId(),
|
|
161
|
+
title: "Risk concentrated in top quartile of units",
|
|
162
|
+
category: "risk_concentration",
|
|
163
|
+
severity: concentration > 0.8 ? "high" : "medium",
|
|
164
|
+
confidence: "high",
|
|
165
|
+
lens: "architecture",
|
|
166
|
+
summary: `${Math.round(concentration * 100)}% of total risk score is concentrated in the top ${topQuartileSize} of ${sorted.length} units: ${topQuartile.map((i) => i.unit_id).join(", ")}. Consider decomposing high-risk units or adding isolation boundaries.`,
|
|
167
|
+
affected_files: topQuartile.flatMap((item) => {
|
|
168
|
+
const unit = unitManifest.units.find((u) => u.unit_id === item.unit_id);
|
|
169
|
+
return (unit?.files ?? [item.unit_id]).map((path) => ({ path }));
|
|
170
|
+
}),
|
|
171
|
+
systemic: true,
|
|
172
|
+
}];
|
|
173
|
+
}
|
|
174
|
+
function detectUnitSprawl(unitManifest) {
|
|
175
|
+
if (unitManifest.units.length < 3)
|
|
176
|
+
return [];
|
|
177
|
+
const fileCounts = unitManifest.units.map((u) => u.files.length);
|
|
178
|
+
const totalFiles = fileCounts.reduce((a, b) => a + b, 0);
|
|
179
|
+
const maxFiles = Math.max(...fileCounts);
|
|
180
|
+
const findings = [];
|
|
181
|
+
const dominantUnit = unitManifest.units.find((u) => u.files.length === maxFiles);
|
|
182
|
+
if (dominantUnit && maxFiles > totalFiles * 0.5 && totalFiles > 10) {
|
|
183
|
+
findings.push({
|
|
184
|
+
id: findingId(),
|
|
185
|
+
title: `Dominant unit: ${dominantUnit.unit_id}`,
|
|
186
|
+
category: "monolith_unit",
|
|
187
|
+
severity: "medium",
|
|
188
|
+
confidence: "medium",
|
|
189
|
+
lens: "architecture",
|
|
190
|
+
summary: `Unit ${dominantUnit.unit_id} contains ${maxFiles} of ${totalFiles} files (${Math.round((maxFiles / totalFiles) * 100)}%). A single unit this large suggests insufficient decomposition.`,
|
|
191
|
+
affected_files: dominantUnit.files.slice(0, 10).map((path) => ({ path })),
|
|
192
|
+
systemic: true,
|
|
193
|
+
});
|
|
194
|
+
}
|
|
195
|
+
if (unitManifest.units.length > 50) {
|
|
196
|
+
const smallUnits = unitManifest.units.filter((u) => u.files.length === 1);
|
|
197
|
+
if (smallUnits.length > unitManifest.units.length * 0.6) {
|
|
198
|
+
findings.push({
|
|
199
|
+
id: findingId(),
|
|
200
|
+
title: "Excessive single-file units",
|
|
201
|
+
category: "unit_fragmentation",
|
|
202
|
+
severity: "low",
|
|
203
|
+
confidence: "medium",
|
|
204
|
+
lens: "architecture",
|
|
205
|
+
summary: `${smallUnits.length} of ${unitManifest.units.length} units contain only a single file. This fragmentation may indicate that the unit grouping is too granular to reflect meaningful architectural boundaries.`,
|
|
206
|
+
affected_files: smallUnits.slice(0, 5).map((u) => ({ path: u.files[0] })),
|
|
207
|
+
systemic: true,
|
|
208
|
+
});
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
return findings;
|
|
212
|
+
}
|
|
213
|
+
function detectFlowGaps(criticalFlows, graphBundle) {
|
|
214
|
+
const edges = allEdges(graphBundle);
|
|
215
|
+
const connected = new Set();
|
|
216
|
+
for (const edge of edges) {
|
|
217
|
+
connected.add(edge.from);
|
|
218
|
+
connected.add(edge.to);
|
|
219
|
+
}
|
|
220
|
+
const findings = [];
|
|
221
|
+
for (const flow of criticalFlows.flows) {
|
|
222
|
+
const disconnected = flow.paths.filter((path) => !connected.has(path));
|
|
223
|
+
if (disconnected.length > 0 &&
|
|
224
|
+
disconnected.length > flow.paths.length * 0.5) {
|
|
225
|
+
findings.push({
|
|
226
|
+
id: findingId(),
|
|
227
|
+
title: `Critical flow "${flow.name}" has weak graph coverage`,
|
|
228
|
+
category: "flow_gap",
|
|
229
|
+
severity: "medium",
|
|
230
|
+
confidence: "low",
|
|
231
|
+
lens: "architecture",
|
|
232
|
+
summary: `${disconnected.length} of ${flow.paths.length} files in flow "${flow.name}" have no dependency graph edges. The flow's structural integrity cannot be verified through static analysis alone.`,
|
|
233
|
+
affected_files: disconnected.map((path) => ({ path })),
|
|
234
|
+
systemic: true,
|
|
235
|
+
});
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
return findings;
|
|
239
|
+
}
|
|
240
|
+
export function buildDesignAssessment(params) {
|
|
241
|
+
nextFindingId = 1;
|
|
242
|
+
const findings = [
|
|
243
|
+
...detectCycleFindings(params.graphBundle),
|
|
244
|
+
...detectHubModules(params.graphBundle),
|
|
245
|
+
...detectOrphanUnits(params.unitManifest, params.graphBundle),
|
|
246
|
+
...detectRiskConcentration(params.riskRegister, params.unitManifest),
|
|
247
|
+
...detectUnitSprawl(params.unitManifest),
|
|
248
|
+
...detectFlowGaps(params.criticalFlows, params.graphBundle),
|
|
249
|
+
];
|
|
250
|
+
return {
|
|
251
|
+
generated_at: new Date().toISOString(),
|
|
252
|
+
findings,
|
|
253
|
+
};
|
|
254
|
+
}
|
package/dist/io/artifacts.d.ts
CHANGED
|
@@ -11,6 +11,7 @@ import type { RiskRegister } from "../types/risk.js";
|
|
|
11
11
|
import type { AuditPlanMetrics, ReviewPacket } from "../types/reviewPlanning.js";
|
|
12
12
|
import type { RuntimeValidationReport, RuntimeValidationTaskManifest } from "../types/runtimeValidation.js";
|
|
13
13
|
import type { SurfaceManifest } from "../types/surfaces.js";
|
|
14
|
+
import type { DesignAssessment } from "../types/designAssessment.js";
|
|
14
15
|
import type { ToolingManifest } from "../types/toolingManifest.js";
|
|
15
16
|
type ArtifactPayloadMap = {
|
|
16
17
|
repo_manifest: RepoManifest;
|
|
@@ -22,6 +23,7 @@ type ArtifactPayloadMap = {
|
|
|
22
23
|
critical_flows: CriticalFlowManifest;
|
|
23
24
|
flow_coverage: FlowCoverageManifest;
|
|
24
25
|
risk_register: RiskRegister;
|
|
26
|
+
design_assessment: DesignAssessment;
|
|
25
27
|
coverage_matrix: CoverageMatrix;
|
|
26
28
|
runtime_validation_tasks: RuntimeValidationTaskManifest;
|
|
27
29
|
runtime_validation_report: RuntimeValidationReport;
|
|
@@ -60,6 +62,7 @@ export declare const ARTIFACT_DEFINITIONS: {
|
|
|
60
62
|
readonly critical_flows: ArtifactDefinition<"critical_flows">;
|
|
61
63
|
readonly flow_coverage: ArtifactDefinition<"flow_coverage">;
|
|
62
64
|
readonly risk_register: ArtifactDefinition<"risk_register">;
|
|
65
|
+
readonly design_assessment: ArtifactDefinition<"design_assessment">;
|
|
63
66
|
readonly coverage_matrix: ArtifactDefinition<"coverage_matrix">;
|
|
64
67
|
readonly runtime_validation_tasks: ArtifactDefinition<"runtime_validation_tasks">;
|
|
65
68
|
readonly runtime_validation_report: ArtifactDefinition<"runtime_validation_report">;
|
package/dist/io/artifacts.js
CHANGED
|
@@ -36,6 +36,7 @@ export const ARTIFACT_DEFINITIONS = {
|
|
|
36
36
|
critical_flows: jsonArtifact("critical_flows.json", "analysis"),
|
|
37
37
|
flow_coverage: jsonArtifact("flow_coverage.json", "analysis"),
|
|
38
38
|
risk_register: jsonArtifact("risk_register.json", "analysis"),
|
|
39
|
+
design_assessment: jsonArtifact("design_assessment.json", "analysis"),
|
|
39
40
|
coverage_matrix: jsonArtifact("coverage_matrix.json", "execution"),
|
|
40
41
|
runtime_validation_tasks: jsonArtifact("runtime_validation_tasks.json", "execution"),
|
|
41
42
|
runtime_validation_report: jsonArtifact("runtime_validation_report.json", "execution"),
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { decideNextStep } from "./nextStep.js";
|
|
2
2
|
import { deriveAuditState } from "./state.js";
|
|
3
3
|
import { computeArtifactMetadata } from "./artifactMetadata.js";
|
|
4
|
-
import { runIntakeExecutor, runStructureExecutor, runPlanningExecutor, runResultIngestionExecutor, runRuntimeValidationExecutor, runRuntimeValidationUpdateExecutor, runSynthesisExecutor, runExternalAnalyzerImportExecutor, } from "./internalExecutors.js";
|
|
4
|
+
import { runIntakeExecutor, runStructureExecutor, runPlanningExecutor, runResultIngestionExecutor, runRuntimeValidationExecutor, runRuntimeValidationUpdateExecutor, runSynthesisExecutor, runDesignAssessmentExecutor, runDesignReviewAutoComplete, runExternalAnalyzerImportExecutor, } from "./internalExecutors.js";
|
|
5
5
|
import { runAutoFixExecutor } from "./autoFixExecutor.js";
|
|
6
6
|
import { runSyntaxResolutionExecutor } from "./syntaxResolutionExecutor.js";
|
|
7
7
|
function cloneState(state) {
|
|
@@ -53,6 +53,12 @@ export async function advanceAudit(bundle, options = {}) {
|
|
|
53
53
|
case "structure_executor":
|
|
54
54
|
run = await runStructureExecutor(bundle, options.root);
|
|
55
55
|
break;
|
|
56
|
+
case "design_assessment_executor":
|
|
57
|
+
run = runDesignAssessmentExecutor(bundle);
|
|
58
|
+
break;
|
|
59
|
+
case "design_review":
|
|
60
|
+
run = runDesignReviewAutoComplete(bundle);
|
|
61
|
+
break;
|
|
56
62
|
case "planning_executor":
|
|
57
63
|
if (!options.root)
|
|
58
64
|
throw new Error("advanceAudit planning_executor requires root");
|
|
@@ -37,6 +37,7 @@ export const ARTIFACT_DEPENDENCY_MAP = {
|
|
|
37
37
|
],
|
|
38
38
|
"unit_manifest.json": [
|
|
39
39
|
"risk_register.json",
|
|
40
|
+
"design_assessment.json",
|
|
40
41
|
"coverage_matrix.json",
|
|
41
42
|
"audit_tasks.json",
|
|
42
43
|
"audit_plan_metrics.json",
|
|
@@ -54,6 +55,7 @@ export const ARTIFACT_DEPENDENCY_MAP = {
|
|
|
54
55
|
"critical_flows.json": [
|
|
55
56
|
"flow_coverage.json",
|
|
56
57
|
"risk_register.json",
|
|
58
|
+
"design_assessment.json",
|
|
57
59
|
"audit_tasks.json",
|
|
58
60
|
"audit_plan_metrics.json",
|
|
59
61
|
"review_packets.json",
|
|
@@ -62,6 +64,9 @@ export const ARTIFACT_DEPENDENCY_MAP = {
|
|
|
62
64
|
"runtime_validation_report.json",
|
|
63
65
|
"audit-report.md",
|
|
64
66
|
],
|
|
67
|
+
"design_assessment.json": [
|
|
68
|
+
"audit-report.md",
|
|
69
|
+
],
|
|
65
70
|
"external_analyzer_results.json": [
|
|
66
71
|
"coverage_matrix.json",
|
|
67
72
|
"flow_coverage.json",
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
function summarizeUnits(bundle) {
|
|
2
|
+
const units = bundle.unit_manifest?.units ?? [];
|
|
3
|
+
if (units.length === 0)
|
|
4
|
+
return "No units identified.";
|
|
5
|
+
const lines = units.map((unit) => {
|
|
6
|
+
const lenses = unit.required_lenses.join(", ") || "none";
|
|
7
|
+
return `- ${unit.unit_id} (${unit.files.length} files, lenses: ${lenses})`;
|
|
8
|
+
});
|
|
9
|
+
return [
|
|
10
|
+
`${units.length} units:`,
|
|
11
|
+
...lines.slice(0, 40),
|
|
12
|
+
...(units.length > 40 ? [` ... and ${units.length - 40} more`] : []),
|
|
13
|
+
].join("\n");
|
|
14
|
+
}
|
|
15
|
+
function summarizeGraph(bundle) {
|
|
16
|
+
const graphs = bundle.graph_bundle?.graphs;
|
|
17
|
+
if (!graphs)
|
|
18
|
+
return "No dependency graph available.";
|
|
19
|
+
const counts = [];
|
|
20
|
+
for (const [kind, edges] of Object.entries(graphs)) {
|
|
21
|
+
if (Array.isArray(edges) && edges.length > 0) {
|
|
22
|
+
counts.push(`${kind}: ${edges.length} edges`);
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
if (counts.length === 0)
|
|
26
|
+
return "Dependency graph is empty.";
|
|
27
|
+
return `Dependency graph: ${counts.join(", ")}.`;
|
|
28
|
+
}
|
|
29
|
+
function summarizeFlows(bundle) {
|
|
30
|
+
const flows = bundle.critical_flows?.flows ?? [];
|
|
31
|
+
if (flows.length === 0)
|
|
32
|
+
return "No critical flows identified.";
|
|
33
|
+
const lines = flows.map((flow) => `- ${flow.name}: ${flow.paths.length} files, concerns: ${flow.concerns.join(", ") || "none"}`);
|
|
34
|
+
return [`${flows.length} critical flows:`, ...lines].join("\n");
|
|
35
|
+
}
|
|
36
|
+
function summarizeRisk(bundle) {
|
|
37
|
+
const items = bundle.risk_register?.items ?? [];
|
|
38
|
+
if (items.length === 0)
|
|
39
|
+
return "No risk items.";
|
|
40
|
+
const sorted = [...items].sort((a, b) => b.risk_score - a.risk_score);
|
|
41
|
+
const top = sorted.slice(0, 10);
|
|
42
|
+
const lines = top.map((item) => `- ${item.unit_id}: score ${item.risk_score}, signals: ${item.signals.join(", ") || "none"}`);
|
|
43
|
+
return [
|
|
44
|
+
`${items.length} risk items (top ${top.length} by score):`,
|
|
45
|
+
...lines,
|
|
46
|
+
].join("\n");
|
|
47
|
+
}
|
|
48
|
+
function summarizeSurfaces(bundle) {
|
|
49
|
+
const surfaces = bundle.surface_manifest?.surfaces ?? [];
|
|
50
|
+
if (surfaces.length === 0)
|
|
51
|
+
return "No externally reachable surfaces identified.";
|
|
52
|
+
const lines = surfaces.map((surface) => `- ${surface.id} (${surface.kind}): ${surface.entrypoint}${surface.methods?.length ? ` [${surface.methods.join(", ")}]` : ""}`);
|
|
53
|
+
return [`${surfaces.length} surfaces:`, ...lines].join("\n");
|
|
54
|
+
}
|
|
55
|
+
function summarizeFiles(bundle) {
|
|
56
|
+
const files = bundle.repo_manifest?.files ?? [];
|
|
57
|
+
if (files.length === 0)
|
|
58
|
+
return "No files in manifest.";
|
|
59
|
+
const byLanguage = new Map();
|
|
60
|
+
for (const file of files) {
|
|
61
|
+
const lang = file.language || "unknown";
|
|
62
|
+
byLanguage.set(lang, (byLanguage.get(lang) ?? 0) + 1);
|
|
63
|
+
}
|
|
64
|
+
const langSummary = [...byLanguage.entries()]
|
|
65
|
+
.sort((a, b) => b[1] - a[1])
|
|
66
|
+
.map(([lang, count]) => `${lang}: ${count}`)
|
|
67
|
+
.join(", ");
|
|
68
|
+
return `${files.length} files (${langSummary}).`;
|
|
69
|
+
}
|
|
70
|
+
function formatDeterministicFindings(findings) {
|
|
71
|
+
if (findings.length === 0)
|
|
72
|
+
return "No structural issues detected by deterministic analysis.";
|
|
73
|
+
const lines = findings.map((finding) => `- [${finding.severity}] ${finding.title}: ${finding.summary}`);
|
|
74
|
+
return [
|
|
75
|
+
`${findings.length} structural findings from deterministic analysis:`,
|
|
76
|
+
...lines,
|
|
77
|
+
].join("\n");
|
|
78
|
+
}
|
|
79
|
+
export function renderDesignReviewPrompt(bundle) {
|
|
80
|
+
const deterministicFindings = bundle.design_assessment?.findings ?? [];
|
|
81
|
+
return [
|
|
82
|
+
"# Project design review",
|
|
83
|
+
"",
|
|
84
|
+
"You are reviewing the overall design of this project. The deterministic audit pipeline has already analyzed the codebase structure. Your job is to provide qualitative, big-picture design observations that static analysis cannot produce.",
|
|
85
|
+
"",
|
|
86
|
+
"## Project context",
|
|
87
|
+
"",
|
|
88
|
+
`Repository: ${bundle.repo_manifest?.repository?.name ?? "unknown"}`,
|
|
89
|
+
"",
|
|
90
|
+
"### File inventory",
|
|
91
|
+
"",
|
|
92
|
+
summarizeFiles(bundle),
|
|
93
|
+
"",
|
|
94
|
+
"### Unit structure",
|
|
95
|
+
"",
|
|
96
|
+
summarizeUnits(bundle),
|
|
97
|
+
"",
|
|
98
|
+
"### Dependency graph",
|
|
99
|
+
"",
|
|
100
|
+
summarizeGraph(bundle),
|
|
101
|
+
"",
|
|
102
|
+
"### Externally reachable surfaces",
|
|
103
|
+
"",
|
|
104
|
+
summarizeSurfaces(bundle),
|
|
105
|
+
"",
|
|
106
|
+
"### Critical flows",
|
|
107
|
+
"",
|
|
108
|
+
summarizeFlows(bundle),
|
|
109
|
+
"",
|
|
110
|
+
"### Risk profile",
|
|
111
|
+
"",
|
|
112
|
+
summarizeRisk(bundle),
|
|
113
|
+
"",
|
|
114
|
+
"### Deterministic structural findings",
|
|
115
|
+
"",
|
|
116
|
+
formatDeterministicFindings(deterministicFindings),
|
|
117
|
+
"",
|
|
118
|
+
"## What to assess",
|
|
119
|
+
"",
|
|
120
|
+
"Read the project source to understand what it does and how it works, then produce findings about:",
|
|
121
|
+
"",
|
|
122
|
+
"- **Tool and library opportunities**: third-party tools, libraries, or frameworks that would improve the project. Concrete suggestions with rationale, not generic advice.",
|
|
123
|
+
"- **Architecture pattern improvements**: structural changes that would improve extensibility, testability, or maintainability. Consider whether the current abstractions match the problem domain.",
|
|
124
|
+
"- **Design simplification**: areas where the design is over-engineered or where simpler alternatives would work. Conversely, areas that are under-designed for their importance.",
|
|
125
|
+
"- **Integration and generalization**: opportunities to make the project more portable, composable, or protocol-aligned (e.g., MCP, standard APIs, plugin architectures).",
|
|
126
|
+
"- **Missing capabilities**: gaps in the design that would become pain points as the project evolves.",
|
|
127
|
+
"",
|
|
128
|
+
"## Output format",
|
|
129
|
+
"",
|
|
130
|
+
"Produce a JSON array of findings. Each finding must conform to:",
|
|
131
|
+
"",
|
|
132
|
+
"```json",
|
|
133
|
+
"{",
|
|
134
|
+
' "id": "DR-001",',
|
|
135
|
+
' "title": "short descriptive title",',
|
|
136
|
+
' "category": "one of: tool_opportunity, architecture_pattern, design_simplification, integration, missing_capability",',
|
|
137
|
+
' "severity": "one of: critical, high, medium, low, info",',
|
|
138
|
+
' "confidence": "one of: high, medium, low",',
|
|
139
|
+
' "lens": "architecture",',
|
|
140
|
+
' "summary": "detailed explanation of the observation and the recommended change",',
|
|
141
|
+
' "affected_files": [{"path": "relevant/file.ts"}],',
|
|
142
|
+
' "systemic": true',
|
|
143
|
+
"}",
|
|
144
|
+
"```",
|
|
145
|
+
"",
|
|
146
|
+
"Write the JSON array to the design review results path provided below. Use finding IDs starting with DR-001.",
|
|
147
|
+
"",
|
|
148
|
+
"Focus on substantive, actionable observations. Prefer fewer high-quality findings over many surface-level ones.",
|
|
149
|
+
"",
|
|
150
|
+
].join("\n");
|
|
151
|
+
}
|
|
@@ -9,6 +9,16 @@ export const EXECUTOR_REGISTRY = [
|
|
|
9
9
|
obligation_ids: ["structure_artifacts"],
|
|
10
10
|
description: "Build structure artifacts such as units, surfaces, graphs, flows, and risk.",
|
|
11
11
|
},
|
|
12
|
+
{
|
|
13
|
+
id: "design_assessment_executor",
|
|
14
|
+
obligation_ids: ["design_assessment_current"],
|
|
15
|
+
description: "Run deterministic structural analysis to assess overall project design.",
|
|
16
|
+
},
|
|
17
|
+
{
|
|
18
|
+
id: "design_review",
|
|
19
|
+
obligation_ids: ["design_review_completed"],
|
|
20
|
+
description: "Pause the pipeline and delegate a holistic project design review to the active LLM agent.",
|
|
21
|
+
},
|
|
12
22
|
{
|
|
13
23
|
id: "planning_executor",
|
|
14
24
|
obligation_ids: ["planning_artifacts"],
|
|
@@ -13,6 +13,8 @@ export declare function resolveRuntimeValidationSpawnCommand(command: string[],
|
|
|
13
13
|
};
|
|
14
14
|
export declare function runIntakeExecutor(bundle: ArtifactBundle, root: string): Promise<ExecutorRunResult>;
|
|
15
15
|
export declare function runStructureExecutor(bundle: ArtifactBundle, root?: string): Promise<ExecutorRunResult>;
|
|
16
|
+
export declare function runDesignAssessmentExecutor(bundle: ArtifactBundle): ExecutorRunResult;
|
|
17
|
+
export declare function runDesignReviewAutoComplete(bundle: ArtifactBundle): ExecutorRunResult;
|
|
16
18
|
export declare function runPlanningExecutor(bundle: ArtifactBundle, root: string, lineIndex?: Record<string, number>): Promise<ExecutorRunResult>;
|
|
17
19
|
export declare function runResultIngestionExecutor(bundle: ArtifactBundle, results: AuditResult[]): ExecutorRunResult;
|
|
18
20
|
export declare function runRuntimeValidationExecutor(bundle: ArtifactBundle, root: string): Promise<ExecutorRunResult>;
|
|
@@ -15,6 +15,7 @@ import { buildUnitManifest } from "./unitBuilder.js";
|
|
|
15
15
|
import { buildRepoManifestFromFs } from "../extractors/fsIntake.js";
|
|
16
16
|
import { loadIgnoreFile } from "../extractors/ignore.js";
|
|
17
17
|
import { ingestAuditResults, updateAuditTaskStatuses, } from "./resultIngestion.js";
|
|
18
|
+
import { buildDesignAssessment } from "../extractors/designAssessment.js";
|
|
18
19
|
import { buildSelectiveDeepeningTasks } from "./selectiveDeepening.js";
|
|
19
20
|
import { updateRuntimeValidationReport } from "./runtimeValidationUpdate.js";
|
|
20
21
|
import { autoCompleteTrivialCoverage } from "./trivialAudit.js";
|
|
@@ -178,6 +179,47 @@ export async function runStructureExecutor(bundle, root) {
|
|
|
178
179
|
: ""),
|
|
179
180
|
};
|
|
180
181
|
}
|
|
182
|
+
export function runDesignAssessmentExecutor(bundle) {
|
|
183
|
+
if (!bundle.unit_manifest ||
|
|
184
|
+
!bundle.graph_bundle ||
|
|
185
|
+
!bundle.critical_flows ||
|
|
186
|
+
!bundle.risk_register) {
|
|
187
|
+
throw new Error("Cannot run design assessment executor without structure artifacts");
|
|
188
|
+
}
|
|
189
|
+
const designAssessment = buildDesignAssessment({
|
|
190
|
+
unitManifest: bundle.unit_manifest,
|
|
191
|
+
graphBundle: bundle.graph_bundle,
|
|
192
|
+
criticalFlows: bundle.critical_flows,
|
|
193
|
+
riskRegister: bundle.risk_register,
|
|
194
|
+
});
|
|
195
|
+
return {
|
|
196
|
+
updated: {
|
|
197
|
+
...bundle,
|
|
198
|
+
design_assessment: designAssessment,
|
|
199
|
+
},
|
|
200
|
+
artifacts_written: ["design_assessment.json"],
|
|
201
|
+
progress_summary: `Design assessment complete: ${designAssessment.findings.length} structural finding(s).`,
|
|
202
|
+
};
|
|
203
|
+
}
|
|
204
|
+
export function runDesignReviewAutoComplete(bundle) {
|
|
205
|
+
const existing = bundle.design_assessment;
|
|
206
|
+
if (!existing) {
|
|
207
|
+
throw new Error("Cannot auto-complete design review without design_assessment artifact");
|
|
208
|
+
}
|
|
209
|
+
const updated = {
|
|
210
|
+
...existing,
|
|
211
|
+
reviewed: true,
|
|
212
|
+
review_findings: existing.review_findings ?? [],
|
|
213
|
+
};
|
|
214
|
+
return {
|
|
215
|
+
updated: {
|
|
216
|
+
...bundle,
|
|
217
|
+
design_assessment: updated,
|
|
218
|
+
},
|
|
219
|
+
artifacts_written: ["design_assessment.json"],
|
|
220
|
+
progress_summary: "Design review auto-completed (host-agent review available via next-step).",
|
|
221
|
+
};
|
|
222
|
+
}
|
|
181
223
|
export async function runPlanningExecutor(bundle, root, lineIndex = {}) {
|
|
182
224
|
if (!bundle.repo_manifest) {
|
|
183
225
|
throw new Error("Cannot run planning executor without repo_manifest");
|
|
@@ -409,6 +451,7 @@ export function runSynthesisExecutor(bundle, results) {
|
|
|
409
451
|
coverageMatrix: bundle.coverage_matrix,
|
|
410
452
|
runtimeValidationReport: bundle.runtime_validation_report,
|
|
411
453
|
externalAnalyzerResults: bundle.external_analyzer_results,
|
|
454
|
+
designAssessment: bundle.design_assessment,
|
|
412
455
|
});
|
|
413
456
|
return {
|
|
414
457
|
updated: {
|
|
@@ -29,6 +29,8 @@ export function deriveAuditState(bundle) {
|
|
|
29
29
|
"critical_flows.json",
|
|
30
30
|
"risk_register.json",
|
|
31
31
|
], structureReady)));
|
|
32
|
+
obligations.push(obligation("design_assessment_current", staleOrSatisfied(staleArtifacts, ["design_assessment.json"], has(bundle.design_assessment))));
|
|
33
|
+
obligations.push(obligation("design_review_completed", bundle.design_assessment?.reviewed ? "satisfied" : "missing"));
|
|
32
34
|
const planningReady = has(bundle.coverage_matrix) &&
|
|
33
35
|
has(bundle.flow_coverage) &&
|
|
34
36
|
has(bundle.runtime_validation_tasks) &&
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { AuditResult, Finding } from "../types.js";
|
|
2
|
+
import type { DesignAssessment } from "../types/designAssessment.js";
|
|
2
3
|
import type { ExternalAnalyzerResults } from "../types/externalAnalyzer.js";
|
|
3
4
|
import type { RuntimeValidationReport } from "../types/runtimeValidation.js";
|
|
4
|
-
export declare function mergeFindings(results: AuditResult[], runtimeReport?: RuntimeValidationReport, externalAnalyzerResults?: ExternalAnalyzerResults): Finding[];
|
|
5
|
+
export declare function mergeFindings(results: AuditResult[], runtimeReport?: RuntimeValidationReport, externalAnalyzerResults?: ExternalAnalyzerResults, designAssessment?: DesignAssessment): Finding[];
|
|
@@ -236,8 +236,20 @@ function relevantExternalEvidence(finding, results) {
|
|
|
236
236
|
.filter((item) => findingPaths.has(item.path))
|
|
237
237
|
.map((item) => `external:${results.tool}:${item.path}:${item.summary}`);
|
|
238
238
|
}
|
|
239
|
-
export function mergeFindings(results, runtimeReport, externalAnalyzerResults) {
|
|
239
|
+
export function mergeFindings(results, runtimeReport, externalAnalyzerResults, designAssessment) {
|
|
240
240
|
const merged = new Map();
|
|
241
|
+
const allDesignFindings = [
|
|
242
|
+
...(designAssessment?.findings ?? []),
|
|
243
|
+
...(designAssessment?.review_findings ?? []),
|
|
244
|
+
];
|
|
245
|
+
for (const finding of allDesignFindings) {
|
|
246
|
+
const key = findingKey(finding);
|
|
247
|
+
merged.set(key, {
|
|
248
|
+
...finding,
|
|
249
|
+
affected_files: [...finding.affected_files],
|
|
250
|
+
evidence: [...(finding.evidence ?? [])],
|
|
251
|
+
});
|
|
252
|
+
}
|
|
241
253
|
for (const result of results) {
|
|
242
254
|
for (const finding of result.findings) {
|
|
243
255
|
const key = findingKey(finding);
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { AuditResult, CoverageMatrix, Finding, UnitManifest } from "../types.js";
|
|
2
|
+
import type { DesignAssessment } from "../types/designAssessment.js";
|
|
2
3
|
import type { ExternalAnalyzerResults } from "../types/externalAnalyzer.js";
|
|
3
4
|
import type { CriticalFlowManifest } from "../types/flows.js";
|
|
4
5
|
import type { GraphBundle } from "../types/graph.js";
|
|
@@ -25,5 +26,6 @@ export declare function buildAuditReportModel(params: {
|
|
|
25
26
|
coverageMatrix?: CoverageMatrix;
|
|
26
27
|
runtimeValidationReport?: RuntimeValidationReport;
|
|
27
28
|
externalAnalyzerResults?: ExternalAnalyzerResults;
|
|
29
|
+
designAssessment?: DesignAssessment;
|
|
28
30
|
}): AuditReportModel;
|
|
29
31
|
export declare function renderAuditReportMarkdown(model: AuditReportModel): string;
|
|
@@ -32,7 +32,7 @@ function formatSeverityList(summary) {
|
|
|
32
32
|
return parts.length > 0 ? parts.join(", ") : "none";
|
|
33
33
|
}
|
|
34
34
|
export function buildAuditReportModel(params) {
|
|
35
|
-
const findings = mergeFindings(params.results, params.runtimeValidationReport, params.externalAnalyzerResults);
|
|
35
|
+
const findings = mergeFindings(params.results, params.runtimeValidationReport, params.externalAnalyzerResults, params.designAssessment);
|
|
36
36
|
const workBlocks = buildWorkBlocks({
|
|
37
37
|
findings,
|
|
38
38
|
unitManifest: params.unitManifest,
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { AuditTask } from "../types.js";
|
|
2
2
|
import { type ValidationIssue } from "./basic.js";
|
|
3
3
|
export type IssueSeverity = "error" | "warning";
|
|
4
|
+
export declare function normalizeCoveragePath(path: string): string;
|
|
4
5
|
export interface AuditResultIssue extends ValidationIssue {
|
|
5
6
|
result_index: number;
|
|
6
7
|
task_id: string;
|
|
@@ -1,4 +1,7 @@
|
|
|
1
1
|
import { describeValue, formatValidationIssues, isRecord, } from "./basic.js";
|
|
2
|
+
export function normalizeCoveragePath(path) {
|
|
3
|
+
return path.replace(/\\/g, "/").replace(/^\.\//, "");
|
|
4
|
+
}
|
|
2
5
|
const REQUIRED_FINDING_FIELDS = [
|
|
3
6
|
"id",
|
|
4
7
|
"title",
|
|
@@ -423,6 +426,18 @@ export function validateAuditResults(results, tasks, options = {}) {
|
|
|
423
426
|
tasks.map((item) => item.task_id).join(", "),
|
|
424
427
|
});
|
|
425
428
|
}
|
|
429
|
+
const taskNormMap = new Map();
|
|
430
|
+
if (task) {
|
|
431
|
+
for (const fp of task.file_paths) {
|
|
432
|
+
taskNormMap.set(normalizeCoveragePath(fp), fp);
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
const normLineIndex = new Map();
|
|
436
|
+
if (options.lineIndex) {
|
|
437
|
+
for (const [k, v] of Object.entries(options.lineIndex)) {
|
|
438
|
+
normLineIndex.set(normalizeCoveragePath(k), v);
|
|
439
|
+
}
|
|
440
|
+
}
|
|
426
441
|
const fileCoverage = result.file_coverage;
|
|
427
442
|
const normalizedFileCoverage = [];
|
|
428
443
|
const declaredAssignedCoveragePaths = new Set();
|
|
@@ -447,6 +462,10 @@ export function validateAuditResults(results, tasks, options = {}) {
|
|
|
447
462
|
});
|
|
448
463
|
continue;
|
|
449
464
|
}
|
|
465
|
+
const entryNorm = isNonEmptyString(entry.path)
|
|
466
|
+
? normalizeCoveragePath(entry.path)
|
|
467
|
+
: "";
|
|
468
|
+
const canonicalPath = taskNormMap.get(entryNorm);
|
|
450
469
|
if (!isNonEmptyString(entry.path)) {
|
|
451
470
|
pushIssue(issues, {
|
|
452
471
|
result_index: i,
|
|
@@ -455,7 +474,7 @@ export function validateAuditResults(results, tasks, options = {}) {
|
|
|
455
474
|
message: "file_coverage entry has an empty path.",
|
|
456
475
|
});
|
|
457
476
|
}
|
|
458
|
-
else if (task && !
|
|
477
|
+
else if (task && !canonicalPath) {
|
|
459
478
|
pushIssue(issues, {
|
|
460
479
|
result_index: i,
|
|
461
480
|
task_id: taskId,
|
|
@@ -463,7 +482,7 @@ export function validateAuditResults(results, tasks, options = {}) {
|
|
|
463
482
|
message: `file_coverage path '${entry.path}' is not listed in the task file_paths.`,
|
|
464
483
|
});
|
|
465
484
|
}
|
|
466
|
-
else if (seenCoveragePaths.has(
|
|
485
|
+
else if (seenCoveragePaths.has(entryNorm)) {
|
|
467
486
|
pushIssue(issues, {
|
|
468
487
|
result_index: i,
|
|
469
488
|
task_id: taskId,
|
|
@@ -472,11 +491,10 @@ export function validateAuditResults(results, tasks, options = {}) {
|
|
|
472
491
|
});
|
|
473
492
|
}
|
|
474
493
|
else {
|
|
475
|
-
seenCoveragePaths.add(
|
|
494
|
+
seenCoveragePaths.add(entryNorm);
|
|
476
495
|
}
|
|
477
|
-
if (
|
|
478
|
-
(
|
|
479
|
-
declaredAssignedCoveragePaths.add(entry.path);
|
|
496
|
+
if (entryNorm.length > 0 && (!task || canonicalPath)) {
|
|
497
|
+
declaredAssignedCoveragePaths.add(canonicalPath ?? entryNorm);
|
|
480
498
|
}
|
|
481
499
|
if (!Number.isInteger(entry.total_lines)) {
|
|
482
500
|
pushIssue(issues, {
|
|
@@ -495,8 +513,8 @@ export function validateAuditResults(results, tasks, options = {}) {
|
|
|
495
513
|
message: "file_coverage total_lines must be zero or greater.",
|
|
496
514
|
});
|
|
497
515
|
}
|
|
498
|
-
const expectedLineCount =
|
|
499
|
-
?
|
|
516
|
+
const expectedLineCount = entryNorm.length > 0
|
|
517
|
+
? normLineIndex.get(entryNorm)
|
|
500
518
|
: undefined;
|
|
501
519
|
if (Number.isInteger(entry.total_lines) &&
|
|
502
520
|
typeof expectedLineCount === "number" &&
|
|
@@ -509,19 +527,19 @@ export function validateAuditResults(results, tasks, options = {}) {
|
|
|
509
527
|
`(expected ${expectedLineCount}, got ${entry.total_lines}).`,
|
|
510
528
|
});
|
|
511
529
|
}
|
|
512
|
-
if (
|
|
530
|
+
if (entryNorm.length > 0 &&
|
|
513
531
|
Number.isInteger(entry.total_lines) &&
|
|
514
532
|
Number(entry.total_lines) >= 0 &&
|
|
515
|
-
(!task ||
|
|
533
|
+
(!task || canonicalPath)) {
|
|
516
534
|
normalizedFileCoverage.push({
|
|
517
|
-
path:
|
|
535
|
+
path: canonicalPath ?? entryNorm,
|
|
518
536
|
total_lines: Number(entry.total_lines),
|
|
519
537
|
});
|
|
520
538
|
}
|
|
521
539
|
}
|
|
522
540
|
if (task) {
|
|
523
541
|
for (const path of task.file_paths) {
|
|
524
|
-
if (!seenCoveragePaths.has(path)) {
|
|
542
|
+
if (!seenCoveragePaths.has(normalizeCoveragePath(path))) {
|
|
525
543
|
pushIssue(issues, {
|
|
526
544
|
result_index: i,
|
|
527
545
|
task_id: taskId,
|