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 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 = lastStep?.audit_state ?? deriveAuditState(bundle);
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: bundle.audit_state ?? decision.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 = bundle.audit_state ?? decision.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 group of taskGroups) {
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: bundleAfter.audit_state ?? deriveAuditState(bundleAfter),
1262
+ state: deriveAuditState(bundleAfter),
1255
1263
  obligationId: workerResult.obligation_id,
1256
1264
  executor: workerResult.selected_executor,
1257
1265
  blocker: buildWorkerFailureBlocker(workerResult),
1258
1266
  })
1259
- : bundleAfter.audit_state ?? deriveAuditState(bundleAfter);
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 = bundle.audit_state ?? decision.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
  }
@@ -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 includesAny(normalized, DATA_LAYER_KEYWORDS) || hasSegment(normalized, "db");
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);
@@ -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;
@@ -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) {
@@ -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 writeFile(join(artifactsDir, "dispatch", CURRENT_SCHEMA_FILENAME), await readFile(auditResultSchemaPath, "utf8"), "utf8");
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 writeFile(join(artifactsDir, "dispatch", CURRENT_SCHEMA_FILENAME), await readFile(auditResultSchemaPath, "utf8"), "utf8");
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 writeFile(join(artifactsDir, "dispatch", CURRENT_SCHEMA_FILENAME), await readFile(auditResultSchemaPath, "utf8"), "utf8");
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,2 @@
1
+ import type { ToolingManifest } from "../types/toolingManifest.js";
2
+ export declare function buildToolingManifest(): Promise<ToolingManifest>;
@@ -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 = { ...run.updated, artifact_metadata: metadata };
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()
@@ -1,4 +1,7 @@
1
1
  export const ARTIFACT_DEPENDENCY_MAP = {
2
+ "tooling_manifest.json": [
3
+ "repo_manifest.json",
4
+ ],
2
5
  "repo_manifest.json": [
3
6
  "file_disposition.json",
4
7
  "unit_manifest.json",
@@ -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 task for audit-code.",
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 schema: ${task.artifacts_dir}/dispatch/audit-result.schema.json`,
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,7 @@
1
+ export interface ToolingManifest {
2
+ generated_at: string;
3
+ package_root: string;
4
+ package_version: string | null;
5
+ implementation_hash: string;
6
+ inputs: string[];
7
+ }
@@ -0,0 +1 @@
1
+ export {};
package/dist/types.d.ts CHANGED
@@ -53,6 +53,7 @@ export interface AuditTask {
53
53
  pass_id: string;
54
54
  lens: Lens;
55
55
  file_paths: string[];
56
+ file_line_counts?: Record<string, number>;
56
57
  line_ranges?: Array<{
57
58
  path: string;
58
59
  start: number;
@@ -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) <= 0) {
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 greater than zero.",
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) > 0) {
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "auditor-lambda",
3
- "version": "0.2.12",
3
+ "version": "0.2.14",
4
4
  "private": false,
5
5
  "description": "Portable hybrid code-auditing framework for arbitrary repositories.",
6
6
  "type": "module",
@@ -33,7 +33,7 @@
33
33
  "required": ["path", "total_lines"],
34
34
  "properties": {
35
35
  "path": { "type": "string" },
36
- "total_lines": { "type": "integer", "minimum": 1 }
36
+ "total_lines": { "type": "integer", "minimum": 0 }
37
37
  },
38
38
  "additionalProperties": false
39
39
  }
@@ -24,6 +24,13 @@
24
24
  "minItems": 1,
25
25
  "items": { "type": "string" }
26
26
  },
27
+ "file_line_counts": {
28
+ "type": "object",
29
+ "additionalProperties": {
30
+ "type": "integer",
31
+ "minimum": 0
32
+ }
33
+ },
27
34
  "line_ranges": {
28
35
  "type": "array",
29
36
  "items": {
@@ -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 locally against this repository, run `node audit-code.mjs`.)_
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