auditor-lambda 0.2.12 → 0.2.14
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 +17 -9
- package/dist/extractors/disposition.js +8 -1
- package/dist/extractors/flows.js +10 -2
- package/dist/extractors/pathPatterns.d.ts +1 -0
- package/dist/extractors/pathPatterns.js +16 -1
- package/dist/io/artifacts.d.ts +3 -0
- package/dist/io/artifacts.js +3 -0
- package/dist/io/runArtifacts.js +11 -3
- package/dist/io/toolingManifest.d.ts +2 -0
- package/dist/io/toolingManifest.js +75 -0
- package/dist/orchestrator/advance.js +6 -2
- package/dist/orchestrator/artifactMetadata.d.ts +1 -1
- package/dist/orchestrator/artifactMetadata.js +15 -2
- package/dist/orchestrator/dependencyMap.js +3 -0
- package/dist/orchestrator/internalExecutors.js +16 -0
- package/dist/orchestrator/localCommands.js +16 -2
- package/dist/orchestrator/staleness.js +48 -1
- package/dist/prompts/renderWorkerPrompt.js +3 -2
- package/dist/types/toolingManifest.d.ts +7 -0
- package/dist/types/toolingManifest.js +1 -0
- package/dist/types.d.ts +1 -0
- package/dist/validation/artifacts.js +3 -0
- package/dist/validation/auditResults.js +3 -3
- package/package.json +1 -1
- package/schemas/audit_result.schema.json +1 -1
- package/schemas/audit_task.schema.json +7 -0
- package/skills/audit-code/SKILL.md +9 -0
- package/skills/audit-code/audit-code.prompt.md +1 -1
package/dist/cli.js
CHANGED
|
@@ -287,6 +287,13 @@ function buildPendingAuditTasks(bundle) {
|
|
|
287
287
|
const completedTaskIds = new Set((bundle.audit_results ?? []).map((result) => result.task_id));
|
|
288
288
|
return (bundle.audit_tasks ?? []).filter((task) => task.status !== "complete" && !completedTaskIds.has(task.task_id));
|
|
289
289
|
}
|
|
290
|
+
async function addFileLineCountHints(root, tasks) {
|
|
291
|
+
const lineIndex = await buildLineIndexForPaths(root, tasks.flatMap((task) => task.file_paths));
|
|
292
|
+
return tasks.map((task) => ({
|
|
293
|
+
...task,
|
|
294
|
+
file_line_counts: Object.fromEntries(task.file_paths.map((path) => [path, lineIndex[path] ?? 0])),
|
|
295
|
+
}));
|
|
296
|
+
}
|
|
290
297
|
function formatAuditResultValidationError(issues) {
|
|
291
298
|
return (`audit-results validation failed with ${issues.length} error(s):\n` +
|
|
292
299
|
formatAuditResultIssues(issues));
|
|
@@ -400,7 +407,7 @@ async function ingestBatchAuditResults(options) {
|
|
|
400
407
|
}
|
|
401
408
|
const bundle = lastStep?.updated_bundle ??
|
|
402
409
|
(await loadArtifactBundle(options.artifactsDir));
|
|
403
|
-
const state =
|
|
410
|
+
const state = deriveAuditState(bundle);
|
|
404
411
|
const decision = decideNextStep(bundle);
|
|
405
412
|
return {
|
|
406
413
|
batchFiles,
|
|
@@ -720,7 +727,7 @@ async function cmdRunToCompletion(argv) {
|
|
|
720
727
|
if (preferredExecutor === "agent" && provider.name === LOCAL_SUBPROCESS_PROVIDER_NAME) {
|
|
721
728
|
const blocker = buildManualReviewBlocker(provider.name);
|
|
722
729
|
const blockedState = buildBlockedAuditState({
|
|
723
|
-
state:
|
|
730
|
+
state: decision.state,
|
|
724
731
|
obligationId,
|
|
725
732
|
executor: preferredExecutor,
|
|
726
733
|
blocker,
|
|
@@ -731,7 +738,7 @@ async function cmdRunToCompletion(argv) {
|
|
|
731
738
|
});
|
|
732
739
|
const blockRunId = buildRunId(obligationId, runCount + 1);
|
|
733
740
|
const blockPaths = getRunPaths(artifactsDir, blockRunId);
|
|
734
|
-
const blockPendingTasks = buildPendingAuditTasks(bundle).slice(0, agentBatchSize);
|
|
741
|
+
const blockPendingTasks = await addFileLineCountHints(root, buildPendingAuditTasks(bundle).slice(0, agentBatchSize));
|
|
735
742
|
const blockPendingTasksPath = join(blockPaths.runDir, "pending-audit-tasks.json");
|
|
736
743
|
const blockAuditResultsPath = join(blockPaths.runDir, "audit-results.json");
|
|
737
744
|
const blockTask = {
|
|
@@ -784,7 +791,7 @@ async function cmdRunToCompletion(argv) {
|
|
|
784
791
|
return;
|
|
785
792
|
}
|
|
786
793
|
if (!preferredExecutor) {
|
|
787
|
-
const state =
|
|
794
|
+
const state = decision.state;
|
|
788
795
|
await clearDispatchFiles(artifactsDir);
|
|
789
796
|
await emitEnvelope({
|
|
790
797
|
root,
|
|
@@ -814,7 +821,8 @@ async function cmdRunToCompletion(argv) {
|
|
|
814
821
|
const allPendingTasks = buildPendingAuditTasks(bundle);
|
|
815
822
|
const taskGroups = chunkArray(allPendingTasks.slice(0, parallelWorkers * agentBatchSize), agentBatchSize);
|
|
816
823
|
const workerSlots = [];
|
|
817
|
-
for (const
|
|
824
|
+
for (const rawGroup of taskGroups) {
|
|
825
|
+
const group = await addFileLineCountHints(root, rawGroup);
|
|
818
826
|
runCount += 1;
|
|
819
827
|
const slotRunId = buildRunId(obligationId, runCount);
|
|
820
828
|
const slotPaths = getRunPaths(artifactsDir, slotRunId);
|
|
@@ -1131,7 +1139,7 @@ async function cmdRunToCompletion(argv) {
|
|
|
1131
1139
|
continue;
|
|
1132
1140
|
}
|
|
1133
1141
|
const pendingAuditTasks = preferredExecutor === "agent"
|
|
1134
|
-
? buildPendingAuditTasks(bundle).slice(0, agentBatchSize)
|
|
1142
|
+
? await addFileLineCountHints(root, buildPendingAuditTasks(bundle).slice(0, agentBatchSize))
|
|
1135
1143
|
: undefined;
|
|
1136
1144
|
const pendingAuditTasksPath = preferredExecutor === "agent"
|
|
1137
1145
|
? join(paths.runDir, "pending-audit-tasks.json")
|
|
@@ -1251,12 +1259,12 @@ async function cmdRunToCompletion(argv) {
|
|
|
1251
1259
|
const shouldBlock = workerResult.status === "failed" || workerResult.status === "blocked";
|
|
1252
1260
|
const state = shouldBlock
|
|
1253
1261
|
? buildBlockedAuditState({
|
|
1254
|
-
state:
|
|
1262
|
+
state: deriveAuditState(bundleAfter),
|
|
1255
1263
|
obligationId: workerResult.obligation_id,
|
|
1256
1264
|
executor: workerResult.selected_executor,
|
|
1257
1265
|
blocker: buildWorkerFailureBlocker(workerResult),
|
|
1258
1266
|
})
|
|
1259
|
-
:
|
|
1267
|
+
: deriveAuditState(bundleAfter);
|
|
1260
1268
|
if (shouldBlock) {
|
|
1261
1269
|
await writeCoreArtifacts(artifactsDir, {
|
|
1262
1270
|
...bundleAfter,
|
|
@@ -1285,7 +1293,7 @@ async function cmdRunToCompletion(argv) {
|
|
|
1285
1293
|
}
|
|
1286
1294
|
const bundle = await loadArtifactBundle(artifactsDir);
|
|
1287
1295
|
const decision = decideNextStep(bundle);
|
|
1288
|
-
const state =
|
|
1296
|
+
const state = decision.state;
|
|
1289
1297
|
if (state.status === "complete") {
|
|
1290
1298
|
await clearDispatchFiles(artifactsDir);
|
|
1291
1299
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { isNodeModulesOrGit, isBuildOutput, isVendorPath, isBinaryArtifact, isLicensePath, isLockfilePath, isLogPath, isDocPath, isGeneratedInstallArtifactPath, normalizeExtractorPath, } from "./pathPatterns.js";
|
|
1
|
+
import { isNodeModulesOrGit, isBuildOutput, isVendorPath, isBinaryArtifact, isLicensePath, isLockfilePath, isLogPath, isDocPath, isAuditArtifactPath, isGeneratedInstallArtifactPath, normalizeExtractorPath, } from "./pathPatterns.js";
|
|
2
2
|
function inferDisposition(path) {
|
|
3
3
|
const normalized = normalizeExtractorPath(path);
|
|
4
4
|
if (isNodeModulesOrGit(normalized)) {
|
|
@@ -26,6 +26,13 @@ function inferDisposition(path) {
|
|
|
26
26
|
if (isLockfilePath(normalized)) {
|
|
27
27
|
return { path, status: "generated", reason: "Lockfile excluded from code audit scope." };
|
|
28
28
|
}
|
|
29
|
+
if (isAuditArtifactPath(normalized)) {
|
|
30
|
+
return {
|
|
31
|
+
path,
|
|
32
|
+
status: "generated",
|
|
33
|
+
reason: "Generated audit artifact.",
|
|
34
|
+
};
|
|
35
|
+
}
|
|
29
36
|
if (isDocPath(normalized)) {
|
|
30
37
|
return { path, status: "doc_only", reason: "Documentation artifact." };
|
|
31
38
|
}
|
package/dist/extractors/flows.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { isAuditExcludedStatus } from "./disposition.js";
|
|
2
|
-
import { EXTRACTOR_HEURISTIC_NOTE, isAsyncTaskPath, isBillingPath, isIdentityPath, isSecuritySensitivePath, isDataLayerPath, isConcurrencyPath, isInterfacePath, isDeploymentConfigPath, normalizeExtractorPath, } from "./pathPatterns.js";
|
|
2
|
+
import { EXTRACTOR_HEURISTIC_NOTE, isAsyncTaskPath, isBillingPath, isIdentityPath, isSecuritySensitivePath, isTestPath, isDataLayerPath, isConcurrencyPath, isInterfacePath, isDeploymentConfigPath, normalizeExtractorPath, } from "./pathPatterns.js";
|
|
3
3
|
function inferConcerns(paths) {
|
|
4
4
|
const concerns = new Set();
|
|
5
5
|
for (const path of paths) {
|
|
@@ -15,6 +15,12 @@ function inferConcerns(paths) {
|
|
|
15
15
|
}
|
|
16
16
|
return concerns.size > 0 ? [...concerns] : ["correctness"];
|
|
17
17
|
}
|
|
18
|
+
function isSchemaContractPath(normalized) {
|
|
19
|
+
return normalized.endsWith(".schema.json");
|
|
20
|
+
}
|
|
21
|
+
function isSupportArtifactPath(normalized) {
|
|
22
|
+
return isTestPath(normalized) || normalized.startsWith("examples/");
|
|
23
|
+
}
|
|
18
24
|
function relatedPaths(entry, availablePaths) {
|
|
19
25
|
const normalized = normalizeExtractorPath(entry);
|
|
20
26
|
const linked = new Set([entry]);
|
|
@@ -82,7 +88,9 @@ export function buildCriticalFlowManifest(repoManifest, surfaceManifest, disposi
|
|
|
82
88
|
}
|
|
83
89
|
for (const path of availablePaths) {
|
|
84
90
|
const normalized = normalizeExtractorPath(path);
|
|
85
|
-
if (isDataLayerPath(normalized)
|
|
91
|
+
if (isDataLayerPath(normalized) &&
|
|
92
|
+
!isSchemaContractPath(normalized) &&
|
|
93
|
+
!isSupportArtifactPath(normalized)) {
|
|
86
94
|
flows.push({
|
|
87
95
|
id: `flow:data:${path.replace(/[^a-zA-Z0-9:_-]/g, "-")}`,
|
|
88
96
|
name: `data evolution flow for ${path}`,
|
|
@@ -14,6 +14,7 @@ export declare function isLicensePath(normalized: string): boolean;
|
|
|
14
14
|
export declare function isLockfilePath(normalized: string): boolean;
|
|
15
15
|
export declare function isDocPath(normalized: string): boolean;
|
|
16
16
|
export declare function isGeneratedInstallArtifactPath(normalized: string): boolean;
|
|
17
|
+
export declare function isAuditArtifactPath(normalized: string): boolean;
|
|
17
18
|
export declare function isTestPath(normalized: string): boolean;
|
|
18
19
|
export declare function isInterfacePath(normalized: string): boolean;
|
|
19
20
|
export declare function isDataLayerPath(normalized: string): boolean;
|
|
@@ -75,6 +75,13 @@ function baseName(normalized) {
|
|
|
75
75
|
const segments = splitSegments(normalized);
|
|
76
76
|
return segments.at(-1) ?? normalized;
|
|
77
77
|
}
|
|
78
|
+
function pathTokens(normalized) {
|
|
79
|
+
return normalized.split(/[^a-z0-9]+/).filter(Boolean);
|
|
80
|
+
}
|
|
81
|
+
function hasToken(normalized, values) {
|
|
82
|
+
const tokens = new Set(pathTokens(normalized));
|
|
83
|
+
return values.some((value) => tokens.has(value));
|
|
84
|
+
}
|
|
78
85
|
export function isNodeModulesOrGit(normalized) {
|
|
79
86
|
return hasSegment(normalized, "node_modules") || hasSegment(normalized, ".git");
|
|
80
87
|
}
|
|
@@ -103,6 +110,9 @@ export function isDocPath(normalized) {
|
|
|
103
110
|
export function isGeneratedInstallArtifactPath(normalized) {
|
|
104
111
|
return normalized.startsWith(".audit-code/install/");
|
|
105
112
|
}
|
|
113
|
+
export function isAuditArtifactPath(normalized) {
|
|
114
|
+
return splitSegments(normalized).some((segment) => segment.startsWith(".audit-artifacts"));
|
|
115
|
+
}
|
|
106
116
|
export function isTestPath(normalized) {
|
|
107
117
|
return includesAny(normalized, TEST_KEYWORDS);
|
|
108
118
|
}
|
|
@@ -110,7 +120,12 @@ export function isInterfacePath(normalized) {
|
|
|
110
120
|
return includesAny(normalized, INTERFACE_KEYWORDS) || hasSegment(normalized, "api");
|
|
111
121
|
}
|
|
112
122
|
export function isDataLayerPath(normalized) {
|
|
113
|
-
return
|
|
123
|
+
return (hasToken(normalized, DATA_LAYER_KEYWORDS) ||
|
|
124
|
+
hasSegment(normalized, "models") ||
|
|
125
|
+
hasSegment(normalized, "schemas") ||
|
|
126
|
+
hasSegment(normalized, "migrations") ||
|
|
127
|
+
hasSegment(normalized, "seeds") ||
|
|
128
|
+
hasSegment(normalized, "db"));
|
|
114
129
|
}
|
|
115
130
|
export function isSecuritySensitivePath(normalized) {
|
|
116
131
|
return includesAny(normalized, SECURITY_KEYWORDS);
|
package/dist/io/artifacts.d.ts
CHANGED
|
@@ -9,6 +9,7 @@ import type { GraphBundle } from "../types/graph.js";
|
|
|
9
9
|
import type { RiskRegister } from "../types/risk.js";
|
|
10
10
|
import type { RuntimeValidationReport, RuntimeValidationTaskManifest } from "../types/runtimeValidation.js";
|
|
11
11
|
import type { SurfaceManifest } from "../types/surfaces.js";
|
|
12
|
+
import type { ToolingManifest } from "../types/toolingManifest.js";
|
|
12
13
|
type ArtifactPayloadMap = {
|
|
13
14
|
repo_manifest: RepoManifest;
|
|
14
15
|
file_disposition: FileDisposition;
|
|
@@ -29,6 +30,7 @@ type ArtifactPayloadMap = {
|
|
|
29
30
|
audit_report: string;
|
|
30
31
|
audit_state: AuditState;
|
|
31
32
|
artifact_metadata: ArtifactMetadataManifest;
|
|
33
|
+
tooling_manifest: ToolingManifest;
|
|
32
34
|
};
|
|
33
35
|
/**
|
|
34
36
|
* Audit artifacts accumulate phase-by-phase as the orchestrator advances.
|
|
@@ -63,6 +65,7 @@ export declare const ARTIFACT_DEFINITIONS: {
|
|
|
63
65
|
readonly audit_report: ArtifactDefinition<"audit_report">;
|
|
64
66
|
readonly audit_state: ArtifactDefinition<"audit_state">;
|
|
65
67
|
readonly artifact_metadata: ArtifactDefinition<"artifact_metadata">;
|
|
68
|
+
readonly tooling_manifest: ArtifactDefinition<"tooling_manifest">;
|
|
66
69
|
};
|
|
67
70
|
export declare const ARTIFACT_FILE_TO_BUNDLE_KEY: Record<string, ArtifactBundleKey>;
|
|
68
71
|
export declare function getArtifactValue(bundle: ArtifactBundle, artifactName: string): unknown;
|
package/dist/io/artifacts.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { cp, rm, unlink } from "node:fs/promises";
|
|
2
2
|
import { join } from "node:path";
|
|
3
3
|
import { isFileMissingError, readOptionalJsonFile, readOptionalNdjsonFile, readOptionalTextFile, writeJsonFile, writeNdjsonFile, writeTextFile, } from "./json.js";
|
|
4
|
+
import { buildToolingManifest } from "./toolingManifest.js";
|
|
4
5
|
function jsonArtifact(fileName, phase) {
|
|
5
6
|
return {
|
|
6
7
|
fileName,
|
|
@@ -45,6 +46,7 @@ export const ARTIFACT_DEFINITIONS = {
|
|
|
45
46
|
audit_report: textArtifact("audit-report.md", "reporting"),
|
|
46
47
|
audit_state: jsonArtifact("audit_state.json", "supervisor"),
|
|
47
48
|
artifact_metadata: jsonArtifact("artifact_metadata.json", "supervisor"),
|
|
49
|
+
tooling_manifest: jsonArtifact("tooling_manifest.json", "supervisor"),
|
|
48
50
|
};
|
|
49
51
|
const ARTIFACT_ENTRIES = Object.entries(ARTIFACT_DEFINITIONS);
|
|
50
52
|
export const ARTIFACT_FILE_TO_BUNDLE_KEY = Object.fromEntries(ARTIFACT_ENTRIES.map(([key, definition]) => [definition.fileName, key]));
|
|
@@ -62,6 +64,7 @@ export async function loadArtifactBundle(root) {
|
|
|
62
64
|
bundleRecord[key] = value;
|
|
63
65
|
}
|
|
64
66
|
}
|
|
67
|
+
bundle.tooling_manifest = await buildToolingManifest();
|
|
65
68
|
return bundle;
|
|
66
69
|
}
|
|
67
70
|
export async function writeCoreArtifacts(root, bundle) {
|
package/dist/io/runArtifacts.js
CHANGED
|
@@ -5,10 +5,12 @@ import { writeJsonFile } from "./json.js";
|
|
|
5
5
|
const moduleDir = dirname(fileURLToPath(import.meta.url));
|
|
6
6
|
const packageRoot = resolve(moduleDir, "..", "..");
|
|
7
7
|
const auditResultSchemaPath = join(packageRoot, "schemas", "audit_result.schema.json");
|
|
8
|
+
const findingSchemaPath = join(packageRoot, "schemas", "finding.schema.json");
|
|
8
9
|
const CURRENT_TASK_FILENAME = "current-task.json";
|
|
9
10
|
const CURRENT_PROMPT_FILENAME = "current-prompt.md";
|
|
10
11
|
const CURRENT_TASKS_FILENAME = "current-tasks.json";
|
|
11
12
|
const CURRENT_SCHEMA_FILENAME = "audit-result.schema.json";
|
|
13
|
+
const CURRENT_FINDING_SCHEMA_FILENAME = "finding.schema.json";
|
|
12
14
|
function pad(value, size = 2) {
|
|
13
15
|
return String(value).padStart(size, "0");
|
|
14
16
|
}
|
|
@@ -53,6 +55,11 @@ export async function ensureSupervisorDirs(artifactsDir) {
|
|
|
53
55
|
await mkdir(join(artifactsDir, "dispatch"), { recursive: true });
|
|
54
56
|
await mkdir(join(artifactsDir, "runs"), { recursive: true });
|
|
55
57
|
}
|
|
58
|
+
async function writeDispatchSchemaFiles(artifactsDir) {
|
|
59
|
+
const dispatchDir = join(artifactsDir, "dispatch");
|
|
60
|
+
await writeFile(join(dispatchDir, CURRENT_SCHEMA_FILENAME), await readFile(auditResultSchemaPath, "utf8"), "utf8");
|
|
61
|
+
await writeFile(join(dispatchDir, CURRENT_FINDING_SCHEMA_FILENAME), await readFile(findingSchemaPath, "utf8"), "utf8");
|
|
62
|
+
}
|
|
56
63
|
export async function writeWorkerTaskFiles(task, prompt, paths, artifactsDir, currentTasks, options = {}) {
|
|
57
64
|
await mkdir(paths.runDir, { recursive: true });
|
|
58
65
|
await writeJsonFile(paths.taskPath, task);
|
|
@@ -62,13 +69,13 @@ export async function writeWorkerTaskFiles(task, prompt, paths, artifactsDir, cu
|
|
|
62
69
|
status: "dispatched",
|
|
63
70
|
});
|
|
64
71
|
if (options.updateDispatch === false) {
|
|
65
|
-
await
|
|
72
|
+
await writeDispatchSchemaFiles(artifactsDir);
|
|
66
73
|
return;
|
|
67
74
|
}
|
|
68
75
|
await writeJsonFile(join(artifactsDir, "dispatch", CURRENT_TASK_FILENAME), task);
|
|
69
76
|
await writeFile(join(artifactsDir, "dispatch", CURRENT_PROMPT_FILENAME), prompt, "utf8");
|
|
70
77
|
await writeJsonFile(join(artifactsDir, "dispatch", CURRENT_TASKS_FILENAME), currentTasks ?? []);
|
|
71
|
-
await
|
|
78
|
+
await writeDispatchSchemaFiles(artifactsDir);
|
|
72
79
|
}
|
|
73
80
|
export async function writeDispatchBatchFiles(artifactsDir, runs, currentTasks) {
|
|
74
81
|
const summary = {
|
|
@@ -104,7 +111,7 @@ export async function writeDispatchBatchFiles(artifactsDir, runs, currentTasks)
|
|
|
104
111
|
await writeJsonFile(join(artifactsDir, "dispatch", CURRENT_TASK_FILENAME), summary);
|
|
105
112
|
await writeFile(join(artifactsDir, "dispatch", CURRENT_PROMPT_FILENAME), promptLines.join("\n"), "utf8");
|
|
106
113
|
await writeJsonFile(join(artifactsDir, "dispatch", CURRENT_TASKS_FILENAME), currentTasks);
|
|
107
|
-
await
|
|
114
|
+
await writeDispatchSchemaFiles(artifactsDir);
|
|
108
115
|
}
|
|
109
116
|
export async function clearDispatchFiles(artifactsDir) {
|
|
110
117
|
const targets = [
|
|
@@ -112,6 +119,7 @@ export async function clearDispatchFiles(artifactsDir) {
|
|
|
112
119
|
CURRENT_PROMPT_FILENAME,
|
|
113
120
|
CURRENT_TASKS_FILENAME,
|
|
114
121
|
CURRENT_SCHEMA_FILENAME,
|
|
122
|
+
CURRENT_FINDING_SCHEMA_FILENAME,
|
|
115
123
|
];
|
|
116
124
|
for (const name of targets) {
|
|
117
125
|
await rm(join(artifactsDir, "dispatch", name), { force: true });
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { readdir, readFile, stat } from "node:fs/promises";
|
|
3
|
+
import { dirname, join, relative, resolve } from "node:path";
|
|
4
|
+
import { fileURLToPath } from "node:url";
|
|
5
|
+
const PACKAGE_ROOT = resolve(dirname(fileURLToPath(import.meta.url)), "..", "..");
|
|
6
|
+
const TOOLING_INPUTS = [
|
|
7
|
+
"audit-code.mjs",
|
|
8
|
+
"audit-code-wrapper-lib.mjs",
|
|
9
|
+
"package.json",
|
|
10
|
+
"dist",
|
|
11
|
+
"schemas",
|
|
12
|
+
"skills/audit-code",
|
|
13
|
+
];
|
|
14
|
+
async function pathExists(path) {
|
|
15
|
+
try {
|
|
16
|
+
await stat(path);
|
|
17
|
+
return true;
|
|
18
|
+
}
|
|
19
|
+
catch {
|
|
20
|
+
return false;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
async function collectFiles(path) {
|
|
24
|
+
const info = await stat(path);
|
|
25
|
+
if (info.isFile()) {
|
|
26
|
+
return [path];
|
|
27
|
+
}
|
|
28
|
+
if (!info.isDirectory()) {
|
|
29
|
+
return [];
|
|
30
|
+
}
|
|
31
|
+
const entries = await readdir(path, { withFileTypes: true });
|
|
32
|
+
const files = [];
|
|
33
|
+
for (const entry of entries.sort((a, b) => a.name.localeCompare(b.name))) {
|
|
34
|
+
files.push(...(await collectFiles(join(path, entry.name))));
|
|
35
|
+
}
|
|
36
|
+
return files;
|
|
37
|
+
}
|
|
38
|
+
async function readPackageVersion() {
|
|
39
|
+
const packageJsonPath = join(PACKAGE_ROOT, "package.json");
|
|
40
|
+
if (!(await pathExists(packageJsonPath))) {
|
|
41
|
+
return null;
|
|
42
|
+
}
|
|
43
|
+
try {
|
|
44
|
+
const packageJson = JSON.parse(await readFile(packageJsonPath, "utf8"));
|
|
45
|
+
return typeof packageJson.version === "string" ? packageJson.version : null;
|
|
46
|
+
}
|
|
47
|
+
catch {
|
|
48
|
+
return null;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
export async function buildToolingManifest() {
|
|
52
|
+
const hash = createHash("sha256");
|
|
53
|
+
const existingInputs = [];
|
|
54
|
+
for (const input of TOOLING_INPUTS) {
|
|
55
|
+
const absolute = join(PACKAGE_ROOT, input);
|
|
56
|
+
if (!(await pathExists(absolute))) {
|
|
57
|
+
continue;
|
|
58
|
+
}
|
|
59
|
+
existingInputs.push(input);
|
|
60
|
+
const files = await collectFiles(absolute);
|
|
61
|
+
for (const file of files.sort((a, b) => a.localeCompare(b))) {
|
|
62
|
+
hash.update(relative(PACKAGE_ROOT, file).replace(/\\/g, "/"));
|
|
63
|
+
hash.update("\n");
|
|
64
|
+
hash.update(await readFile(file));
|
|
65
|
+
hash.update("\n");
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
return {
|
|
69
|
+
generated_at: new Date().toISOString(),
|
|
70
|
+
package_root: PACKAGE_ROOT,
|
|
71
|
+
package_version: await readPackageVersion(),
|
|
72
|
+
implementation_hash: hash.digest("hex"),
|
|
73
|
+
inputs: existingInputs,
|
|
74
|
+
};
|
|
75
|
+
}
|
|
@@ -108,8 +108,12 @@ export async function advanceAudit(bundle, options = {}) {
|
|
|
108
108
|
catch (error) {
|
|
109
109
|
throw formatExecutorFailure(selectedExecutor, selectedObligation, error);
|
|
110
110
|
}
|
|
111
|
-
const metadata = computeArtifactMetadata(run.updated, bundle.artifact_metadata);
|
|
112
|
-
const metadataBundle = {
|
|
111
|
+
const metadata = computeArtifactMetadata(run.updated, bundle.artifact_metadata, [...run.artifacts_written, "tooling_manifest.json"]);
|
|
112
|
+
const metadataBundle = {
|
|
113
|
+
...run.updated,
|
|
114
|
+
tooling_manifest: bundle.tooling_manifest,
|
|
115
|
+
artifact_metadata: metadata,
|
|
116
|
+
};
|
|
113
117
|
const updatedState = deriveAuditState(metadataBundle);
|
|
114
118
|
updatedState.last_executor = selectedExecutor;
|
|
115
119
|
updatedState.last_obligation = selectedObligation ?? undefined;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
1
|
import type { ArtifactMetadataManifest } from "../types/artifactMetadata.js";
|
|
2
2
|
import type { ArtifactBundle } from "../io/artifacts.js";
|
|
3
3
|
export declare function present(bundle: ArtifactBundle, artifactName: string): boolean;
|
|
4
|
-
export declare function computeArtifactMetadata(bundle: ArtifactBundle, previous?: ArtifactMetadataManifest): ArtifactMetadataManifest;
|
|
4
|
+
export declare function computeArtifactMetadata(bundle: ArtifactBundle, previous?: ArtifactMetadataManifest, updatedArtifacts?: Iterable<string>): ArtifactMetadataManifest;
|
|
@@ -28,6 +28,14 @@ function normalizeForMetadataHash(artifactName, value) {
|
|
|
28
28
|
const { generated_at: _generatedAt, ...rest } = record;
|
|
29
29
|
return rest;
|
|
30
30
|
}
|
|
31
|
+
if (artifactName === "tooling_manifest.json" &&
|
|
32
|
+
value &&
|
|
33
|
+
typeof value === "object" &&
|
|
34
|
+
!Array.isArray(value)) {
|
|
35
|
+
const record = value;
|
|
36
|
+
const { generated_at: _generatedAt, ...rest } = record;
|
|
37
|
+
return rest;
|
|
38
|
+
}
|
|
31
39
|
return value;
|
|
32
40
|
}
|
|
33
41
|
function buildReverseDependencyMap() {
|
|
@@ -72,8 +80,9 @@ export function present(bundle, artifactName) {
|
|
|
72
80
|
const value = getArtifactValue(bundle, artifactName);
|
|
73
81
|
return value !== undefined && value !== null;
|
|
74
82
|
}
|
|
75
|
-
export function computeArtifactMetadata(bundle, previous) {
|
|
83
|
+
export function computeArtifactMetadata(bundle, previous, updatedArtifacts = []) {
|
|
76
84
|
const artifacts = {};
|
|
85
|
+
const updated = new Set(updatedArtifacts);
|
|
77
86
|
const presentArtifacts = Object.keys(REVERSE_DEPENDENCY_MAP).filter((artifactName) => artifactName !== "artifact_metadata.json" &&
|
|
78
87
|
present(bundle, artifactName));
|
|
79
88
|
const orderedArtifacts = computeDependencyFirstOrder(presentArtifacts);
|
|
@@ -83,8 +92,12 @@ export function computeArtifactMetadata(bundle, previous) {
|
|
|
83
92
|
const value = getArtifactValue(bundle, artifactName);
|
|
84
93
|
if (value === undefined || value === null)
|
|
85
94
|
continue;
|
|
86
|
-
const contentHash = hashValue(normalizeForMetadataHash(artifactName, value));
|
|
87
95
|
const previousEntry = previous?.artifacts[artifactName];
|
|
96
|
+
if (previousEntry && !updated.has(artifactName)) {
|
|
97
|
+
artifacts[artifactName] = previousEntry;
|
|
98
|
+
continue;
|
|
99
|
+
}
|
|
100
|
+
const contentHash = hashValue(normalizeForMetadataHash(artifactName, value));
|
|
88
101
|
const dependencyRevisions = Object.fromEntries((REVERSE_DEPENDENCY_MAP[artifactName] ?? [])
|
|
89
102
|
.filter((dependencyName) => dependencyName !== "artifact_metadata.json")
|
|
90
103
|
.sort()
|
|
@@ -174,6 +174,18 @@ export function runResultIngestionExecutor(bundle, results) {
|
|
|
174
174
|
const flowCoverage = bundle.critical_flows
|
|
175
175
|
? buildFlowCoverage(bundle.critical_flows, updatedCoverageMatrix)
|
|
176
176
|
: bundle.flow_coverage;
|
|
177
|
+
const runtimeCommand = bundle.runtime_validation_tasks?.tasks.find((task) => task.command && task.command.length > 0)?.command;
|
|
178
|
+
const runtimeValidationTasks = bundle.unit_manifest && flowCoverage
|
|
179
|
+
? buildRuntimeValidationTasks({
|
|
180
|
+
unitManifest: bundle.unit_manifest,
|
|
181
|
+
criticalFlows: bundle.critical_flows,
|
|
182
|
+
flowCoverage,
|
|
183
|
+
command: runtimeCommand,
|
|
184
|
+
})
|
|
185
|
+
: bundle.runtime_validation_tasks;
|
|
186
|
+
const runtimeValidationReport = runtimeValidationTasks
|
|
187
|
+
? mergeRuntimeValidationReport(runtimeValidationTasks, bundle.runtime_validation_report)
|
|
188
|
+
: bundle.runtime_validation_report;
|
|
177
189
|
const requeuePayload = buildRequeuePayload(updatedCoverageMatrix, bundle.critical_flows, flowCoverage, bundle.external_analyzer_results);
|
|
178
190
|
const mergedResults = [...(bundle.audit_results ?? []), ...results];
|
|
179
191
|
const updatedAuditTasks = updateAuditTaskStatuses(bundle.audit_tasks, mergedResults);
|
|
@@ -182,6 +194,8 @@ export function runResultIngestionExecutor(bundle, results) {
|
|
|
182
194
|
...bundle,
|
|
183
195
|
coverage_matrix: updatedCoverageMatrix,
|
|
184
196
|
flow_coverage: flowCoverage,
|
|
197
|
+
runtime_validation_tasks: runtimeValidationTasks,
|
|
198
|
+
runtime_validation_report: runtimeValidationReport,
|
|
185
199
|
audit_results: mergedResults,
|
|
186
200
|
audit_tasks: updatedAuditTasks,
|
|
187
201
|
requeue_tasks: requeuePayload.tasks,
|
|
@@ -190,6 +204,8 @@ export function runResultIngestionExecutor(bundle, results) {
|
|
|
190
204
|
artifacts_written: [
|
|
191
205
|
"coverage_matrix.json",
|
|
192
206
|
"flow_coverage.json",
|
|
207
|
+
...(runtimeValidationTasks ? ["runtime_validation_tasks.json"] : []),
|
|
208
|
+
...(runtimeValidationReport ? ["runtime_validation_report.json"] : []),
|
|
193
209
|
"audit_results.jsonl",
|
|
194
210
|
"audit_tasks.json",
|
|
195
211
|
"requeue_tasks.json",
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { existsSync } from "node:fs";
|
|
2
2
|
import { spawnSync } from "node:child_process";
|
|
3
|
-
import { delimiter, isAbsolute, join } from "node:path";
|
|
3
|
+
import { delimiter, extname, isAbsolute, join } from "node:path";
|
|
4
4
|
function isWindowsBatchCommand(path) {
|
|
5
5
|
return process.platform === "win32" && /\.(cmd|bat)$/i.test(path);
|
|
6
6
|
}
|
|
@@ -45,10 +45,24 @@ function resolveFromPath(command) {
|
|
|
45
45
|
const extensions = process.platform === "win32"
|
|
46
46
|
? (process.env.PATHEXT ?? ".COM;.EXE;.BAT;.CMD")
|
|
47
47
|
.split(";")
|
|
48
|
-
.map((ext) => ext.toLowerCase())
|
|
48
|
+
.map((ext) => ext.trim().toLowerCase())
|
|
49
|
+
.filter((ext) => ext.length > 0)
|
|
50
|
+
.map((ext) => (ext.startsWith(".") ? ext : `.${ext}`))
|
|
49
51
|
: [""];
|
|
50
52
|
for (const dir of pathEntries) {
|
|
51
53
|
const directPath = join(dir, command);
|
|
54
|
+
if (process.platform === "win32" && extname(command).length === 0) {
|
|
55
|
+
for (const ext of extensions) {
|
|
56
|
+
const candidatePath = join(dir, `${command}${ext}`);
|
|
57
|
+
if (existsSync(candidatePath)) {
|
|
58
|
+
return candidatePath;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
if (existsSync(directPath)) {
|
|
62
|
+
return directPath;
|
|
63
|
+
}
|
|
64
|
+
continue;
|
|
65
|
+
}
|
|
52
66
|
if (existsSync(directPath)) {
|
|
53
67
|
return directPath;
|
|
54
68
|
}
|
|
@@ -23,6 +23,14 @@ function normalizeForMetadataHash(artifactName, value) {
|
|
|
23
23
|
const { generated_at: _generatedAt, ...rest } = record;
|
|
24
24
|
return rest;
|
|
25
25
|
}
|
|
26
|
+
if (artifactName === "tooling_manifest.json" &&
|
|
27
|
+
value &&
|
|
28
|
+
typeof value === "object" &&
|
|
29
|
+
!Array.isArray(value)) {
|
|
30
|
+
const record = value;
|
|
31
|
+
const { generated_at: _generatedAt, ...rest } = record;
|
|
32
|
+
return rest;
|
|
33
|
+
}
|
|
26
34
|
return value;
|
|
27
35
|
}
|
|
28
36
|
function computeContentHash(artifactName, bundle) {
|
|
@@ -33,6 +41,18 @@ function computeContentHash(artifactName, bundle) {
|
|
|
33
41
|
.update(stableStringify(normalizeForMetadataHash(artifactName, value)))
|
|
34
42
|
.digest("hex");
|
|
35
43
|
}
|
|
44
|
+
function buildReverseDependencyMap() {
|
|
45
|
+
const reverse = {};
|
|
46
|
+
for (const [upstream, downstreamList] of Object.entries(ARTIFACT_DEPENDENCY_MAP)) {
|
|
47
|
+
reverse[upstream] ??= [];
|
|
48
|
+
for (const downstream of downstreamList) {
|
|
49
|
+
reverse[downstream] ??= [];
|
|
50
|
+
reverse[downstream].push(upstream);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
return reverse;
|
|
54
|
+
}
|
|
55
|
+
const REVERSE_DEPENDENCY_MAP = buildReverseDependencyMap();
|
|
36
56
|
export function computeStaleArtifacts(bundle) {
|
|
37
57
|
const stale = new Set();
|
|
38
58
|
const metadata = bundle.artifact_metadata;
|
|
@@ -40,6 +60,15 @@ export function computeStaleArtifacts(bundle) {
|
|
|
40
60
|
for (const [artifactName, entry] of Object.entries(metadata.artifacts)) {
|
|
41
61
|
if (!present(bundle, artifactName))
|
|
42
62
|
continue;
|
|
63
|
+
const expectedDependencies = [...(REVERSE_DEPENDENCY_MAP[artifactName] ?? [])]
|
|
64
|
+
.filter((dependencyName) => dependencyName !== "artifact_metadata.json")
|
|
65
|
+
.sort();
|
|
66
|
+
const recordedDependencies = Object.keys(entry.dependency_revisions).sort();
|
|
67
|
+
if (stableStringify(expectedDependencies) !==
|
|
68
|
+
stableStringify(recordedDependencies)) {
|
|
69
|
+
stale.add(artifactName);
|
|
70
|
+
continue;
|
|
71
|
+
}
|
|
43
72
|
let isStale = false;
|
|
44
73
|
for (const [dependencyName, recordedRevision] of Object.entries(entry.dependency_revisions)) {
|
|
45
74
|
if (!present(bundle, dependencyName)) {
|
|
@@ -51,7 +80,7 @@ export function computeStaleArtifacts(bundle) {
|
|
|
51
80
|
}
|
|
52
81
|
const dependencyEntry = metadata.artifacts[dependencyName];
|
|
53
82
|
if (!dependencyEntry) {
|
|
54
|
-
if (recordedRevision > 0) {
|
|
83
|
+
if (present(bundle, dependencyName) || recordedRevision > 0) {
|
|
55
84
|
isStale = true;
|
|
56
85
|
break;
|
|
57
86
|
}
|
|
@@ -70,6 +99,9 @@ export function computeStaleArtifacts(bundle) {
|
|
|
70
99
|
}
|
|
71
100
|
}
|
|
72
101
|
for (const [upstream, downstreamList] of Object.entries(ARTIFACT_DEPENDENCY_MAP)) {
|
|
102
|
+
if (upstream === "tooling_manifest.json" && !present(bundle, upstream)) {
|
|
103
|
+
continue;
|
|
104
|
+
}
|
|
73
105
|
if (!present(bundle, upstream)) {
|
|
74
106
|
for (const downstream of downstreamList) {
|
|
75
107
|
const hasMetadataEntry = Boolean(metadata?.artifacts[downstream]);
|
|
@@ -79,5 +111,20 @@ export function computeStaleArtifacts(bundle) {
|
|
|
79
111
|
}
|
|
80
112
|
}
|
|
81
113
|
}
|
|
114
|
+
let changed = true;
|
|
115
|
+
while (changed) {
|
|
116
|
+
changed = false;
|
|
117
|
+
for (const [upstream, downstreamList] of Object.entries(ARTIFACT_DEPENDENCY_MAP)) {
|
|
118
|
+
if (!stale.has(upstream)) {
|
|
119
|
+
continue;
|
|
120
|
+
}
|
|
121
|
+
for (const downstream of downstreamList) {
|
|
122
|
+
if (present(bundle, downstream) && !stale.has(downstream)) {
|
|
123
|
+
stale.add(downstream);
|
|
124
|
+
changed = true;
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
}
|
|
82
129
|
return stale;
|
|
83
130
|
}
|
|
@@ -8,7 +8,7 @@ export function renderWorkerPrompt(task) {
|
|
|
8
8
|
const tasksPath = task.pending_audit_tasks_path ??
|
|
9
9
|
`${task.artifacts_dir}/audit_tasks.json`;
|
|
10
10
|
const lines = [
|
|
11
|
-
"You are executing one bounded audit
|
|
11
|
+
"You are executing one bounded audit run for audit-code.",
|
|
12
12
|
`Run ID: ${task.run_id}`,
|
|
13
13
|
`Repository root: ${task.repo_root}`,
|
|
14
14
|
"",
|
|
@@ -23,6 +23,7 @@ export function renderWorkerPrompt(task) {
|
|
|
23
23
|
" task_id, unit_id, pass_id, lens",
|
|
24
24
|
" file_coverage: [{path, total_lines}] for every assigned file you reviewed",
|
|
25
25
|
" findings: array (empty if nothing found)",
|
|
26
|
+
" If the task includes file_line_counts, use those values for file_coverage.total_lines.",
|
|
26
27
|
" total_lines must match the file's current total line count.",
|
|
27
28
|
" Each finding must include:",
|
|
28
29
|
" id, title, category, severity, confidence, lens, summary, affected_files,",
|
|
@@ -33,7 +34,7 @@ export function renderWorkerPrompt(task) {
|
|
|
33
34
|
task.timeout_ms
|
|
34
35
|
? ` Time budget for this task: ${task.timeout_ms} ms.`
|
|
35
36
|
: " Keep the task bounded to the assigned files only.",
|
|
36
|
-
`Reference
|
|
37
|
+
`Reference schemas: ${task.artifacts_dir}/dispatch/audit-result.schema.json and ${task.artifacts_dir}/dispatch/finding.schema.json`,
|
|
37
38
|
`Write the AuditResult[] JSON array to: ${task.audit_results_path}`,
|
|
38
39
|
];
|
|
39
40
|
if (usesDeferredWorkerCommand(task)) {
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
package/dist/types.d.ts
CHANGED
|
@@ -40,6 +40,9 @@ export function validateArtifactBundle(bundle) {
|
|
|
40
40
|
if (bundle.external_analyzer_results) {
|
|
41
41
|
issues.push(...requireKeys(bundle.external_analyzer_results, "external_analyzer_results", ["tool", "results"]));
|
|
42
42
|
}
|
|
43
|
+
if (bundle.tooling_manifest) {
|
|
44
|
+
issues.push(...requireKeys(bundle.tooling_manifest, "tooling_manifest", ["generated_at", "package_root", "implementation_hash", "inputs"]));
|
|
45
|
+
}
|
|
43
46
|
const repoManifestFiles = asArray(bundle.repo_manifest?.files);
|
|
44
47
|
const fileDispositionEntries = asArray(bundle.file_disposition?.files);
|
|
45
48
|
const unitManifestUnits = asArray(bundle.unit_manifest?.units);
|
|
@@ -306,12 +306,12 @@ export function validateAuditResults(results, tasks, options = {}) {
|
|
|
306
306
|
});
|
|
307
307
|
}
|
|
308
308
|
if (Number.isInteger(entry.total_lines) &&
|
|
309
|
-
Number(entry.total_lines)
|
|
309
|
+
Number(entry.total_lines) < 0) {
|
|
310
310
|
pushIssue(issues, {
|
|
311
311
|
result_index: i,
|
|
312
312
|
task_id: taskId,
|
|
313
313
|
field: `file_coverage[${j}].total_lines`,
|
|
314
|
-
message: "file_coverage total_lines must be
|
|
314
|
+
message: "file_coverage total_lines must be zero or greater.",
|
|
315
315
|
});
|
|
316
316
|
}
|
|
317
317
|
const expectedLineCount = typeof entry.path === "string"
|
|
@@ -330,7 +330,7 @@ export function validateAuditResults(results, tasks, options = {}) {
|
|
|
330
330
|
}
|
|
331
331
|
if (isNonEmptyString(entry.path) &&
|
|
332
332
|
Number.isInteger(entry.total_lines) &&
|
|
333
|
-
Number(entry.total_lines)
|
|
333
|
+
Number(entry.total_lines) >= 0) {
|
|
334
334
|
normalizedFileCoverage.push({
|
|
335
335
|
path: entry.path,
|
|
336
336
|
total_lines: Number(entry.total_lines),
|
package/package.json
CHANGED
|
@@ -46,6 +46,15 @@ audit-code
|
|
|
46
46
|
|
|
47
47
|
from the target repository root.
|
|
48
48
|
|
|
49
|
+
When developing inside the `auditor-lambda` repository itself, prefer:
|
|
50
|
+
|
|
51
|
+
```bash
|
|
52
|
+
node audit-code.mjs
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
That keeps the run pinned to the local wrapper and local `dist/` output instead
|
|
56
|
+
of whichever global `audit-code` binary happens to be on `PATH`.
|
|
57
|
+
|
|
49
58
|
Debug one-step mode:
|
|
50
59
|
|
|
51
60
|
```bash
|
|
@@ -18,7 +18,7 @@ To move the state machine forward, execute the backend framework using your term
|
|
|
18
18
|
audit-code
|
|
19
19
|
```
|
|
20
20
|
|
|
21
|
-
_(If the wrapper is only available as a package dependency in the current repository, `npx audit-code` is equivalent. If developing
|
|
21
|
+
_(If the wrapper is only available as a package dependency in the current repository, `npx audit-code` is equivalent. If you are developing inside the `auditor-lambda` repository itself, prefer `node audit-code.mjs` so the run uses the local wrapper and local `dist/` output instead of a potentially stale global install.)_
|
|
22
22
|
|
|
23
23
|
## Step 2: Handle Blockages (The "Thinking" Phase)
|
|
24
24
|
|