auditor-lambda 0.3.30 → 0.3.33

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.
Files changed (36) hide show
  1. package/README.md +2 -1
  2. package/audit-code-wrapper-lib.mjs +208 -198
  3. package/dist/cli.d.ts +5 -0
  4. package/dist/cli.js +65 -101
  5. package/dist/extractors/risk.js +6 -4
  6. package/dist/io/artifacts.d.ts +2 -0
  7. package/dist/io/artifacts.js +1 -0
  8. package/dist/io/toolingManifest.d.ts +1 -0
  9. package/dist/io/toolingManifest.js +1 -1
  10. package/dist/mcp/server.d.ts +71 -0
  11. package/dist/mcp/server.js +261 -222
  12. package/dist/orchestrator/artifactFreshness.d.ts +4 -0
  13. package/dist/orchestrator/artifactFreshness.js +45 -0
  14. package/dist/orchestrator/artifactMetadata.js +2 -51
  15. package/dist/orchestrator/dependencyMap.js +14 -0
  16. package/dist/orchestrator/internalExecutors.js +8 -0
  17. package/dist/orchestrator/staleness.js +2 -46
  18. package/dist/orchestrator/state.js +1 -1
  19. package/dist/orchestrator/syntaxResolutionExecutor.js +121 -13
  20. package/dist/orchestrator/unitBuilder.js +2 -1
  21. package/dist/providers/spawnLoggedCommand.js +71 -18
  22. package/dist/providers/types.d.ts +5 -0
  23. package/dist/quota/scheduler.js +10 -2
  24. package/dist/quota/state.js +6 -2
  25. package/dist/supervisor/operatorHandoff.js +1 -1
  26. package/dist/types/externalAnalyzer.d.ts +10 -0
  27. package/dist/types/sessionConfig.d.ts +1 -0
  28. package/dist/types/workerSession.js +1 -2
  29. package/dist/validation/artifacts.js +36 -0
  30. package/dist/validation/sessionConfig.js +4 -0
  31. package/package.json +1 -1
  32. package/schemas/audit_task.schema.json +2 -2
  33. package/schemas/risk_register.schema.json +1 -1
  34. package/schemas/unit_manifest.schema.json +2 -1
  35. package/scripts/postinstall.mjs +10 -41
  36. package/skills/audit-code/audit-code.prompt.md +5 -0
@@ -69,7 +69,7 @@ function failure(id, code, message, data) {
69
69
  },
70
70
  };
71
71
  }
72
- function parseContentLength(headerBlock) {
72
+ export function parseContentLength(headerBlock) {
73
73
  const headers = headerBlock.split("\r\n");
74
74
  const contentLengthHeader = headers.find((header) => header.toLowerCase().startsWith("content-length:"));
75
75
  if (!contentLengthHeader) {
@@ -186,95 +186,127 @@ function toolResult(value) {
186
186
  structuredContent: value && typeof value === "object" ? value : { value },
187
187
  };
188
188
  }
189
- async function readResource(uri, context) {
190
- switch (uri) {
191
- case "audit-code://artifacts/current": {
189
+ export const resourceRegistry = [
190
+ {
191
+ uri: "audit-code://artifacts/current",
192
+ name: "current_artifacts",
193
+ description: "Current artifact bundle as JSON.",
194
+ mimeType: "application/json",
195
+ async read(context) {
192
196
  const bundle = await loadArtifactBundle(context.artifactsDir);
193
- return {
194
- mimeType: "application/json",
195
- text: JSON.stringify(bundle),
196
- };
197
- }
198
- case "audit-code://handoff/current": {
197
+ return { mimeType: this.mimeType, text: JSON.stringify(bundle) };
198
+ },
199
+ },
200
+ {
201
+ uri: "audit-code://handoff/current",
202
+ name: "operator_handoff",
203
+ description: "Current operator handoff payload as JSON.",
204
+ mimeType: "application/json",
205
+ async read(context) {
199
206
  const status = (await getStatusPayload(context)).handoff;
200
- return {
201
- mimeType: "application/json",
202
- text: JSON.stringify(status),
203
- };
204
- }
205
- case "audit-code://install/guide": {
207
+ return { mimeType: this.mimeType, text: JSON.stringify(status) };
208
+ },
209
+ },
210
+ {
211
+ uri: "audit-code://install/guide",
212
+ name: "install_guide",
213
+ description: "Repo-local install guide for supported IDE hosts.",
214
+ mimeType: "text/markdown",
215
+ async read(context) {
206
216
  const path = join(context.root, ".audit-code", "install", "GETTING-STARTED.md");
207
217
  const guide = (await readOptionalTextFile(path)) ??
208
218
  "Run `audit-code install` from the repository root to generate the repo-local setup guide.";
209
- return {
210
- mimeType: "text/markdown",
211
- text: guide,
212
- };
213
- }
214
- case "audit-code://report/current": {
219
+ return { mimeType: this.mimeType, text: guide };
220
+ },
221
+ },
222
+ {
223
+ uri: "audit-code://report/current",
224
+ name: "audit_report",
225
+ description: "Current deterministic audit report if available.",
226
+ mimeType: "text/markdown",
227
+ async read(context) {
215
228
  const report = (await readOptionalTextFile(join(context.artifactsDir, "audit-report.md"))) ??
216
229
  (await readOptionalTextFile(join(context.root, "audit-report.md"))) ??
217
230
  "The audit report has not been rendered yet.";
218
- return {
219
- mimeType: "text/markdown",
220
- text: report,
221
- };
222
- }
223
- default:
224
- throw new Error(`Unknown resource URI: ${uri}`);
231
+ return { mimeType: this.mimeType, text: report };
232
+ },
233
+ },
234
+ ];
235
+ async function readResource(uri, context) {
236
+ const entry = resourceRegistry.find((r) => r.uri === uri);
237
+ if (!entry) {
238
+ throw new Error(`Unknown resource URI: ${uri}`);
225
239
  }
240
+ return entry.read(context);
226
241
  }
227
- function promptDefinitions() {
228
- return [
229
- {
230
- name: "audit-code",
231
- description: "Start or continue the autonomous audit loop through the auditor MCP tools.",
232
- arguments: [],
233
- },
234
- {
235
- name: "review-task",
236
- description: "Inspect one audit task with explain_task and the current artifacts before reviewing code.",
237
- arguments: [
238
- {
239
- name: "task_id",
240
- required: true,
241
- description: "Audit task id to inspect.",
242
- },
243
- ],
244
- },
245
- {
246
- name: "synthesize-report",
247
- description: "Read the current audit report resource and summarize the highest-signal findings.",
248
- arguments: [],
249
- },
250
- ];
242
+ function resourceListPayload() {
243
+ return resourceRegistry.map((entry) => ({
244
+ uri: entry.uri,
245
+ name: entry.name,
246
+ description: entry.description,
247
+ mimeType: entry.mimeType,
248
+ }));
251
249
  }
252
- function renderPrompt(name, args) {
253
- switch (name) {
254
- case "audit-code":
250
+ export const promptRegistry = [
251
+ {
252
+ name: "audit-code",
253
+ description: "Start or continue the autonomous audit loop through the next-step machine.",
254
+ arguments: [],
255
+ render() {
255
256
  return [
256
- "Use the auditor MCP tools as the primary interface to the backend wrapper.",
257
- "1. Call `start_audit`.",
258
- "2. If the audit is blocked, inspect `audit-code://handoff/current`.",
257
+ "Use `audit-code next-step` as the canonical interface to the backend wrapper.",
258
+ "1. Prefer running `audit-code next-step` directly from the repository root.",
259
+ "2. If this MCP adapter is your only available integration, call `start_audit` or `continue_audit`; both return the same one-step contract.",
260
+ "3. If the audit is blocked, inspect `audit-code://handoff/current`.",
259
261
  " Do not read `audit-code://artifacts/current` unless explicitly needed for a specific task; it is massive and consumes your context window.",
260
- "3. When the user provides additional evidence, call `import_results` or `import_runtime_updates`.",
261
- "4. Call `continue_audit` until the status is complete or explicitly blocked for operator input.",
262
+ "4. When the user provides additional evidence, call `import_results` or `import_runtime_updates`.",
262
263
  ].join("\n");
263
- case "review-task":
264
+ },
265
+ },
266
+ {
267
+ name: "review-task",
268
+ description: "Inspect one audit task with explain_task and the current artifacts before reviewing code.",
269
+ arguments: [
270
+ {
271
+ name: "task_id",
272
+ required: true,
273
+ description: "Audit task id to inspect.",
274
+ },
275
+ ],
276
+ render(args) {
264
277
  return [
265
278
  `Use \`explain_task\` for task \`${String(args?.task_id ?? "")}\` before you inspect code manually.`,
266
279
  "Do not read the full `audit-code://artifacts/current` bundle unless specifically needed, as it is massive.",
267
280
  ].join("\n");
268
- case "synthesize-report":
281
+ },
282
+ },
283
+ {
284
+ name: "synthesize-report",
285
+ description: "Read the current audit report resource and summarize the highest-signal findings.",
286
+ arguments: [],
287
+ render() {
269
288
  return [
270
289
  "Read `audit-code://report/current`.",
271
290
  "Summarize the final audit report as work blocks first, then highlight the most important risks and remediation priorities.",
272
291
  ].join("\n");
273
- default:
274
- throw new Error(`Unknown prompt: ${name}`);
292
+ },
293
+ },
294
+ ];
295
+ function promptDefinitions() {
296
+ return promptRegistry.map((entry) => ({
297
+ name: entry.name,
298
+ description: entry.description,
299
+ arguments: entry.arguments,
300
+ }));
301
+ }
302
+ function renderPrompt(name, args) {
303
+ const entry = promptRegistry.find((p) => p.name === name);
304
+ if (!entry) {
305
+ throw new Error(`Unknown prompt: ${name}`);
275
306
  }
307
+ return entry.render(args);
276
308
  }
277
- async function runContinueAudit(context, extraArgs = []) {
309
+ async function runContinueAudit(context, extraArgs = ["next-step"]) {
278
310
  const step = await parseCliJson(extraArgs, context);
279
311
  if (!step || typeof step !== "object" || Array.isArray(step))
280
312
  return step;
@@ -378,7 +410,7 @@ function toolDefinitions() {
378
410
  return [
379
411
  {
380
412
  name: "start_audit",
381
- description: "Start the audit wrapper and advance until completion or blocked operator handoff.",
413
+ description: "Compatibility adapter over audit-code next-step; returns one step contract.",
382
414
  inputSchema: {
383
415
  type: "object",
384
416
  properties: {
@@ -406,7 +438,7 @@ function toolDefinitions() {
406
438
  },
407
439
  {
408
440
  name: "continue_audit",
409
- description: "Continue the audit wrapper from the current artifacts directory.",
441
+ description: "Compatibility adapter over audit-code next-step from the current artifacts directory.",
410
442
  inputSchema: {
411
443
  type: "object",
412
444
  properties: {
@@ -510,7 +542,7 @@ function toolDefinitions() {
510
542
  },
511
543
  {
512
544
  name: "report_capability",
513
- description: "Report host subagent dispatch capability and advance to the next step. Call this instead of running audit-code next-step from the shell during a capability_check step.",
545
+ description: "Compatibility adapter that calls audit-code next-step with host subagent capability flags.",
514
546
  inputSchema: {
515
547
  type: "object",
516
548
  properties: {
@@ -542,6 +574,153 @@ function toolDefinitions() {
542
574
  },
543
575
  ];
544
576
  }
577
+ /**
578
+ * Extract zero or more complete Content-Length framed messages from a buffer.
579
+ * Returns an array of parsed body strings and the remaining unconsumed buffer.
580
+ * On framing errors, emits a framing error response via `emit` and resets the buffer.
581
+ */
582
+ export function extractFrames(buffer, emit) {
583
+ const bodies = [];
584
+ let current = buffer;
585
+ while (true) {
586
+ const separator = current.indexOf("\r\n\r\n");
587
+ if (separator < 0) {
588
+ break;
589
+ }
590
+ let contentLength;
591
+ try {
592
+ const headerBlock = current.slice(0, separator).toString("utf8");
593
+ contentLength = parseContentLength(headerBlock);
594
+ }
595
+ catch (error) {
596
+ current = Buffer.alloc(0);
597
+ emit(failure(null, -32700, `Invalid MCP framing: ${error instanceof Error ? error.message : String(error)}.`));
598
+ break;
599
+ }
600
+ const frameLength = separator + 4 + contentLength;
601
+ if (current.length < frameLength) {
602
+ break;
603
+ }
604
+ bodies.push(current.slice(separator + 4, frameLength).toString("utf8"));
605
+ current = current.slice(frameLength);
606
+ }
607
+ return { bodies, remaining: current };
608
+ }
609
+ /**
610
+ * Dispatch a single JSON-RPC request and return the response(s) to send,
611
+ * plus updated shutdown state.
612
+ */
613
+ export async function dispatchRequest(request, ctx) {
614
+ const responses = [];
615
+ let { shutdownRequested } = ctx;
616
+ if (!request.method) {
617
+ responses.push(failure(request.id ?? null, -32600, "Missing method."));
618
+ return { responses, shutdownRequested };
619
+ }
620
+ try {
621
+ switch (request.method) {
622
+ case "initialize": {
623
+ const requestedVersion = typeof request.params?.protocolVersion === "string"
624
+ ? request.params.protocolVersion
625
+ : PROTOCOL_VERSION;
626
+ const negotiatedVersion = requestedVersion <= PROTOCOL_VERSION
627
+ ? requestedVersion
628
+ : PROTOCOL_VERSION;
629
+ responses.push(success(request.id ?? null, {
630
+ protocolVersion: negotiatedVersion,
631
+ serverInfo: {
632
+ name: "audit-code",
633
+ version: ctx.version,
634
+ },
635
+ instructions: "Use audit-code next-step as the primary backend loop. These MCP tools are compatibility adapters that return the same one-step contract.",
636
+ capabilities: {
637
+ tools: { listChanged: false },
638
+ resources: { subscribe: false, listChanged: false },
639
+ prompts: { listChanged: false },
640
+ },
641
+ }));
642
+ break;
643
+ }
644
+ case "notifications/initialized":
645
+ break;
646
+ case "ping":
647
+ if (request.id !== undefined) {
648
+ responses.push(success(request.id, {}));
649
+ }
650
+ break;
651
+ case "tools/list":
652
+ responses.push(success(request.id ?? null, { tools: toolDefinitions() }));
653
+ break;
654
+ case "tools/call": {
655
+ const params = parseObject(request.params);
656
+ const toolName = params.name;
657
+ if (!hasValue(toolName)) {
658
+ throw new Error("tools/call requires a tool name.");
659
+ }
660
+ responses.push(success(request.id ?? null, await handleToolCall(toolName, parseObject(params.arguments), ctx.defaults)));
661
+ break;
662
+ }
663
+ case "resources/list":
664
+ responses.push(success(request.id ?? null, {
665
+ resources: resourceListPayload(),
666
+ }));
667
+ break;
668
+ case "resources/read": {
669
+ const params = parseObject(request.params);
670
+ if (!hasValue(params.uri)) {
671
+ throw new Error("resources/read requires uri.");
672
+ }
673
+ const resource = await readResource(params.uri, ctx.defaults);
674
+ responses.push(success(request.id ?? null, {
675
+ contents: [
676
+ {
677
+ uri: params.uri,
678
+ mimeType: resource.mimeType,
679
+ text: resource.text,
680
+ },
681
+ ],
682
+ }));
683
+ break;
684
+ }
685
+ case "prompts/list":
686
+ responses.push(success(request.id ?? null, {
687
+ prompts: promptDefinitions(),
688
+ }));
689
+ break;
690
+ case "prompts/get": {
691
+ const params = parseObject(request.params);
692
+ if (!hasValue(params.name)) {
693
+ throw new Error("prompts/get requires name.");
694
+ }
695
+ responses.push(success(request.id ?? null, {
696
+ description: promptDefinitions().find((prompt) => prompt.name === params.name)?.description,
697
+ messages: [
698
+ {
699
+ role: "user",
700
+ content: {
701
+ type: "text",
702
+ text: renderPrompt(params.name, parseObject(params.arguments)),
703
+ },
704
+ },
705
+ ],
706
+ }));
707
+ break;
708
+ }
709
+ case "shutdown":
710
+ shutdownRequested = true;
711
+ responses.push(success(request.id ?? null, {}));
712
+ break;
713
+ case "exit":
714
+ return { responses, shutdownRequested, exit: shutdownRequested ? 0 : 1 };
715
+ default:
716
+ responses.push(failure(request.id ?? null, -32601, `Unknown method: ${request.method}`));
717
+ }
718
+ }
719
+ catch (error) {
720
+ responses.push(failure(request.id ?? null, -32000, error instanceof Error ? error.message : String(error)));
721
+ }
722
+ return { responses, shutdownRequested };
723
+ }
545
724
  export async function runAuditCodeMcpServer(argv) {
546
725
  const defaults = parseServerOptions(argv);
547
726
  const version = await packageVersion();
@@ -549,29 +728,9 @@ export async function runAuditCodeMcpServer(argv) {
549
728
  let buffer = Buffer.alloc(0);
550
729
  process.stdin.on("data", async (chunk) => {
551
730
  buffer = Buffer.concat([buffer, chunk]);
552
- while (true) {
553
- const separator = buffer.indexOf("\r\n\r\n");
554
- if (separator < 0) {
555
- return;
556
- }
557
- let contentLength;
558
- try {
559
- const headerBlock = buffer.slice(0, separator).toString("utf8");
560
- contentLength = parseContentLength(headerBlock);
561
- }
562
- catch (error) {
563
- buffer = Buffer.alloc(0);
564
- writeMessage(failure(null, -32700, `Invalid MCP framing: ${error instanceof Error ? error.message : String(error)}.`));
565
- return;
566
- }
567
- const frameLength = separator + 4 + contentLength;
568
- if (buffer.length < frameLength) {
569
- return;
570
- }
571
- const body = buffer
572
- .slice(separator + 4, frameLength)
573
- .toString("utf8");
574
- buffer = buffer.slice(frameLength);
731
+ const { bodies, remaining } = extractFrames(buffer, writeMessage);
732
+ buffer = remaining;
733
+ for (const body of bodies) {
575
734
  let request;
576
735
  try {
577
736
  request = JSON.parse(body);
@@ -580,137 +739,17 @@ export async function runAuditCodeMcpServer(argv) {
580
739
  writeMessage(failure(null, -32700, "Invalid JSON-RPC payload.", error instanceof Error ? error.message : String(error)));
581
740
  continue;
582
741
  }
583
- if (!request.method) {
584
- writeMessage(failure(request.id ?? null, -32600, "Missing method."));
585
- continue;
586
- }
587
- try {
588
- switch (request.method) {
589
- case "initialize": {
590
- const requestedVersion = typeof request.params?.protocolVersion === "string"
591
- ? request.params.protocolVersion
592
- : PROTOCOL_VERSION;
593
- const negotiatedVersion = requestedVersion <= PROTOCOL_VERSION
594
- ? requestedVersion
595
- : PROTOCOL_VERSION;
596
- writeMessage(success(request.id ?? null, {
597
- protocolVersion: negotiatedVersion,
598
- serverInfo: {
599
- name: "audit-code",
600
- version,
601
- },
602
- instructions: "Use the audit-code MCP tools as the primary interface to the backend wrapper. Prefer start_audit, get_status, continue_audit, and the audit-code resources over ad hoc shell commands.",
603
- capabilities: {
604
- tools: { listChanged: false },
605
- resources: { subscribe: false, listChanged: false },
606
- prompts: { listChanged: false },
607
- },
608
- }));
609
- break;
610
- }
611
- case "notifications/initialized":
612
- break;
613
- case "ping":
614
- if (request.id !== undefined) {
615
- writeMessage(success(request.id, {}));
616
- }
617
- break;
618
- case "tools/list":
619
- writeMessage(success(request.id ?? null, { tools: toolDefinitions() }));
620
- break;
621
- case "tools/call": {
622
- const params = parseObject(request.params);
623
- const toolName = params.name;
624
- if (!hasValue(toolName)) {
625
- throw new Error("tools/call requires a tool name.");
626
- }
627
- writeMessage(success(request.id ?? null, await handleToolCall(toolName, parseObject(params.arguments), defaults)));
628
- break;
629
- }
630
- case "resources/list":
631
- writeMessage(success(request.id ?? null, {
632
- resources: [
633
- {
634
- uri: "audit-code://artifacts/current",
635
- name: "current_artifacts",
636
- description: "Current artifact bundle as JSON.",
637
- mimeType: "application/json",
638
- },
639
- {
640
- uri: "audit-code://handoff/current",
641
- name: "operator_handoff",
642
- description: "Current operator handoff payload as JSON.",
643
- mimeType: "application/json",
644
- },
645
- {
646
- uri: "audit-code://install/guide",
647
- name: "install_guide",
648
- description: "Repo-local install guide for supported IDE hosts.",
649
- mimeType: "text/markdown",
650
- },
651
- {
652
- uri: "audit-code://report/current",
653
- name: "audit_report",
654
- description: "Current deterministic audit report if available.",
655
- mimeType: "text/markdown",
656
- },
657
- ],
658
- }));
659
- break;
660
- case "resources/read": {
661
- const params = parseObject(request.params);
662
- if (!hasValue(params.uri)) {
663
- throw new Error("resources/read requires uri.");
664
- }
665
- const resource = await readResource(params.uri, defaults);
666
- writeMessage(success(request.id ?? null, {
667
- contents: [
668
- {
669
- uri: params.uri,
670
- mimeType: resource.mimeType,
671
- text: resource.text,
672
- },
673
- ],
674
- }));
675
- break;
676
- }
677
- case "prompts/list":
678
- writeMessage(success(request.id ?? null, {
679
- prompts: promptDefinitions(),
680
- }));
681
- break;
682
- case "prompts/get": {
683
- const params = parseObject(request.params);
684
- if (!hasValue(params.name)) {
685
- throw new Error("prompts/get requires name.");
686
- }
687
- writeMessage(success(request.id ?? null, {
688
- description: promptDefinitions().find((prompt) => prompt.name === params.name)?.description,
689
- messages: [
690
- {
691
- role: "user",
692
- content: {
693
- type: "text",
694
- text: renderPrompt(params.name, parseObject(params.arguments)),
695
- },
696
- },
697
- ],
698
- }));
699
- break;
700
- }
701
- case "shutdown":
702
- shutdownRequested = true;
703
- writeMessage(success(request.id ?? null, {}));
704
- break;
705
- case "exit":
706
- process.exit(shutdownRequested ? 0 : 1);
707
- break;
708
- default:
709
- writeMessage(failure(request.id ?? null, -32601, `Unknown method: ${request.method}`));
710
- }
742
+ const result = await dispatchRequest(request, {
743
+ version,
744
+ defaults,
745
+ shutdownRequested,
746
+ });
747
+ shutdownRequested = result.shutdownRequested;
748
+ for (const response of result.responses) {
749
+ writeMessage(response);
711
750
  }
712
- catch (error) {
713
- writeMessage(failure(request.id ?? null, -32000, error instanceof Error ? error.message : String(error)));
751
+ if (result.exit !== undefined) {
752
+ process.exit(result.exit);
714
753
  }
715
754
  }
716
755
  });
@@ -0,0 +1,4 @@
1
+ export declare function stableStringify(value: unknown): string;
2
+ export declare function normalizeForMetadataHash(artifactName: string, value: unknown): unknown;
3
+ export declare function hashArtifactValue(artifactName: string, value: unknown): string;
4
+ export declare function buildReverseDependencyMap(): Record<string, string[]>;
@@ -0,0 +1,45 @@
1
+ import { createHash } from "node:crypto";
2
+ import { ARTIFACT_DEPENDENCY_MAP } from "./dependencyMap.js";
3
+ export function stableStringify(value) {
4
+ if (value === undefined) {
5
+ return "null";
6
+ }
7
+ if (value === null || typeof value !== "object") {
8
+ return JSON.stringify(value);
9
+ }
10
+ if (Array.isArray(value)) {
11
+ return `[${value.map((item) => stableStringify(item ?? null)).join(",")}]`;
12
+ }
13
+ const entries = Object.entries(value)
14
+ .filter(([, item]) => item !== undefined)
15
+ .sort(([a], [b]) => a.localeCompare(b));
16
+ return `{${entries.map(([key, item]) => `${JSON.stringify(key)}:${stableStringify(item)}`).join(",")}}`;
17
+ }
18
+ export function normalizeForMetadataHash(artifactName, value) {
19
+ if ((artifactName === "repo_manifest.json" ||
20
+ artifactName === "tooling_manifest.json") &&
21
+ value &&
22
+ typeof value === "object" &&
23
+ !Array.isArray(value)) {
24
+ const record = value;
25
+ const { generated_at: _generatedAt, ...rest } = record;
26
+ return rest;
27
+ }
28
+ return value;
29
+ }
30
+ export function hashArtifactValue(artifactName, value) {
31
+ return createHash("sha256")
32
+ .update(stableStringify(normalizeForMetadataHash(artifactName, value)))
33
+ .digest("hex");
34
+ }
35
+ export function buildReverseDependencyMap() {
36
+ const reverse = {};
37
+ for (const [upstream, downstreamList] of Object.entries(ARTIFACT_DEPENDENCY_MAP)) {
38
+ reverse[upstream] ??= [];
39
+ for (const downstream of downstreamList) {
40
+ reverse[downstream] ??= [];
41
+ reverse[downstream].push(upstream);
42
+ }
43
+ }
44
+ return reverse;
45
+ }
@@ -1,54 +1,5 @@
1
- import { createHash } from "node:crypto";
2
- import { ARTIFACT_DEPENDENCY_MAP } from "./dependencyMap.js";
3
1
  import { getArtifactValue } from "../io/artifacts.js";
4
- function stableStringify(value) {
5
- if (value === undefined) {
6
- return "null";
7
- }
8
- if (value === null || typeof value !== "object") {
9
- return JSON.stringify(value);
10
- }
11
- if (Array.isArray(value)) {
12
- return `[${value.map((item) => stableStringify(item ?? null)).join(",")}]`;
13
- }
14
- const entries = Object.entries(value)
15
- .filter(([, item]) => item !== undefined)
16
- .sort(([a], [b]) => a.localeCompare(b));
17
- return `{${entries.map(([key, item]) => `${JSON.stringify(key)}:${stableStringify(item)}`).join(",")}}`;
18
- }
19
- function hashValue(value) {
20
- return createHash("sha256").update(stableStringify(value)).digest("hex");
21
- }
22
- function normalizeForMetadataHash(artifactName, value) {
23
- if (artifactName === "repo_manifest.json" &&
24
- value &&
25
- typeof value === "object" &&
26
- !Array.isArray(value)) {
27
- const record = value;
28
- const { generated_at: _generatedAt, ...rest } = record;
29
- return rest;
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
- }
39
- return value;
40
- }
41
- function buildReverseDependencyMap() {
42
- const reverse = {};
43
- for (const [upstream, downstreamList] of Object.entries(ARTIFACT_DEPENDENCY_MAP)) {
44
- reverse[upstream] ??= [];
45
- for (const downstream of downstreamList) {
46
- reverse[downstream] ??= [];
47
- reverse[downstream].push(upstream);
48
- }
49
- }
50
- return reverse;
51
- }
2
+ import { buildReverseDependencyMap, hashArtifactValue, stableStringify, } from "./artifactFreshness.js";
52
3
  const REVERSE_DEPENDENCY_MAP = buildReverseDependencyMap();
53
4
  function computeDependencyFirstOrder(artifactNames) {
54
5
  const target = new Set(artifactNames);
@@ -97,7 +48,7 @@ export function computeArtifactMetadata(bundle, previous, updatedArtifacts = [])
97
48
  artifacts[artifactName] = previousEntry;
98
49
  continue;
99
50
  }
100
- const contentHash = hashValue(normalizeForMetadataHash(artifactName, value));
51
+ const contentHash = hashArtifactValue(artifactName, value);
101
52
  const dependencyRevisions = Object.fromEntries((REVERSE_DEPENDENCY_MAP[artifactName] ?? [])
102
53
  .filter((dependencyName) => dependencyName !== "artifact_metadata.json")
103
54
  .sort()