auditor-lambda 0.3.41 → 0.6.0

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 (78) hide show
  1. package/dist/cli/dispatch.js +5 -1
  2. package/dist/cli/prompts.d.ts +19 -0
  3. package/dist/cli/prompts.js +95 -0
  4. package/dist/cli/steps.d.ts +1 -1
  5. package/dist/cli.js +398 -78
  6. package/dist/extractors/analyzers/css.d.ts +2 -0
  7. package/dist/extractors/analyzers/css.js +101 -0
  8. package/dist/extractors/analyzers/html.d.ts +2 -0
  9. package/dist/extractors/analyzers/html.js +92 -0
  10. package/dist/extractors/analyzers/merge.d.ts +14 -0
  11. package/dist/extractors/analyzers/merge.js +85 -0
  12. package/dist/extractors/analyzers/python.d.ts +2 -0
  13. package/dist/extractors/analyzers/python.js +104 -0
  14. package/dist/extractors/analyzers/registry.d.ts +33 -0
  15. package/dist/extractors/analyzers/registry.js +100 -0
  16. package/dist/extractors/analyzers/resourceUrl.d.ts +7 -0
  17. package/dist/extractors/analyzers/resourceUrl.js +25 -0
  18. package/dist/extractors/analyzers/sql.d.ts +2 -0
  19. package/dist/extractors/analyzers/sql.js +19 -0
  20. package/dist/extractors/analyzers/treeSitter.d.ts +34 -0
  21. package/dist/extractors/analyzers/treeSitter.js +111 -0
  22. package/dist/extractors/analyzers/types.d.ts +53 -0
  23. package/dist/extractors/analyzers/types.js +1 -0
  24. package/dist/extractors/analyzers/typescript.d.ts +2 -0
  25. package/dist/extractors/analyzers/typescript.js +257 -0
  26. package/dist/extractors/disposition.js +8 -1
  27. package/dist/extractors/graph.d.ts +1 -0
  28. package/dist/extractors/graph.js +167 -1
  29. package/dist/extractors/graphPythonImports.d.ts +15 -0
  30. package/dist/extractors/graphPythonImports.js +36 -0
  31. package/dist/extractors/pathPatterns.d.ts +6 -0
  32. package/dist/extractors/pathPatterns.js +8 -0
  33. package/dist/io/artifacts.d.ts +13 -1
  34. package/dist/io/artifacts.js +19 -3
  35. package/dist/mcp/server.js +3 -3
  36. package/dist/orchestrator/advance.d.ts +20 -0
  37. package/dist/orchestrator/advance.js +61 -2
  38. package/dist/orchestrator/dependencyMap.js +27 -0
  39. package/dist/orchestrator/edgeReasoning.d.ts +39 -0
  40. package/dist/orchestrator/edgeReasoning.js +125 -0
  41. package/dist/orchestrator/executors.js +11 -1
  42. package/dist/orchestrator/graphEnrichmentExecutor.d.ts +29 -0
  43. package/dist/orchestrator/graphEnrichmentExecutor.js +196 -0
  44. package/dist/orchestrator/internalExecutors.d.ts +10 -1
  45. package/dist/orchestrator/internalExecutors.js +89 -11
  46. package/dist/orchestrator/localCommands.js +6 -25
  47. package/dist/orchestrator/nextStep.js +2 -0
  48. package/dist/orchestrator/reviewPackets.d.ts +37 -4
  49. package/dist/orchestrator/reviewPackets.js +93 -46
  50. package/dist/orchestrator/runtimeValidation.js +4 -31
  51. package/dist/orchestrator/scope.d.ts +62 -0
  52. package/dist/orchestrator/scope.js +227 -0
  53. package/dist/orchestrator/state.js +2 -0
  54. package/dist/reporting/synthesis.d.ts +37 -2
  55. package/dist/reporting/synthesis.js +95 -16
  56. package/dist/reporting/synthesisNarrativePrompt.d.ts +7 -0
  57. package/dist/reporting/synthesisNarrativePrompt.js +60 -0
  58. package/dist/reporting/workBlocks.d.ts +2 -10
  59. package/dist/supervisor/operatorHandoff.d.ts +1 -1
  60. package/dist/supervisor/operatorHandoff.js +26 -16
  61. package/dist/supervisor/sessionConfig.d.ts +8 -1
  62. package/dist/supervisor/sessionConfig.js +22 -1
  63. package/dist/types/analyzerCapability.d.ts +16 -0
  64. package/dist/types/analyzerCapability.js +1 -0
  65. package/dist/types/auditScope.d.ts +43 -0
  66. package/dist/types/auditScope.js +14 -0
  67. package/dist/types/synthesisNarrative.d.ts +7 -0
  68. package/dist/types/synthesisNarrative.js +5 -0
  69. package/dist/types.d.ts +2 -19
  70. package/dist/validation/artifacts.js +9 -0
  71. package/dist/validation/sessionConfig.js +24 -1
  72. package/docs/contracts.md +10 -3
  73. package/package.json +4 -2
  74. package/schemas/analyzer_capability.schema.json +47 -0
  75. package/schemas/audit_findings.schema.json +141 -0
  76. package/schemas/finding.schema.json +2 -1
  77. package/schemas/graph_bundle.schema.json +5 -0
  78. package/schemas/scope.schema.json +46 -0
@@ -279,6 +279,42 @@ function addPythonImportEdge(edges, fromPath, target, kind, specifier) {
279
279
  reason: `Resolved Python import specifier '${specifier}'.`,
280
280
  }));
281
281
  }
282
+ /**
283
+ * Resolve a single `import <spec>` module specifier to a repo file, or
284
+ * undefined. Shared with the tree-sitter Python analyzer so AST-extracted
285
+ * imports resolve to exactly the same targets as the regex floor.
286
+ */
287
+ export function resolvePythonImportTarget(fromPath, specifier, pathLookup) {
288
+ if (!isPythonAbsoluteModuleSpecifier(specifier)) {
289
+ return undefined;
290
+ }
291
+ return resolvePythonModuleSpecifier(fromPath, specifier, pathLookup);
292
+ }
293
+ /**
294
+ * Resolve a `from <module> import <names>` statement to repo files. Mirrors the
295
+ * floor: prefer submodule files (`module.name`), else the module itself. Shared
296
+ * with the tree-sitter Python analyzer.
297
+ */
298
+ export function resolvePythonFromImportTargets(fromPath, moduleSpecifier, importedNames, pathLookup) {
299
+ if (!isPythonModuleSpecifier(moduleSpecifier)) {
300
+ return [];
301
+ }
302
+ const submoduleTargets = importedNames
303
+ .filter((name) => name !== "*" && isPythonIdentifier(name))
304
+ .map((name) => appendPythonImportedSpecifier(moduleSpecifier, name))
305
+ .map((specifier) => ({
306
+ specifier,
307
+ target: resolvePythonModuleSpecifier(fromPath, specifier, pathLookup),
308
+ }))
309
+ .filter((item) => item.target !== undefined && item.target !== fromPath);
310
+ if (submoduleTargets.length > 0) {
311
+ return submoduleTargets;
312
+ }
313
+ const moduleTarget = resolvePythonModuleSpecifier(fromPath, moduleSpecifier, pathLookup);
314
+ return moduleTarget && moduleTarget !== fromPath
315
+ ? [{ specifier: moduleSpecifier, target: moduleTarget }]
316
+ : [];
317
+ }
282
318
  export function extractPythonImportEdges(fromPath, content, pathLookup) {
283
319
  if (!isPythonSourcePath(fromPath)) {
284
320
  return [];
@@ -8,6 +8,12 @@ export declare const EXTRACTOR_HEURISTIC_NOTE = "Heuristic path classification n
8
8
  export declare function normalizeExtractorPath(path: string): string;
9
9
  export declare function pathTokens(normalized: string): string[];
10
10
  export declare function isNodeModulesOrGit(normalized: string): boolean;
11
+ /**
12
+ * `.tmp/` holds transient scratch and bundled tool copies (e.g. a vendored
13
+ * `.tmp/opentoken`). These are not the audited project's source — excluding
14
+ * them keeps the self-audit from auditing its own bundled dependencies.
15
+ */
16
+ export declare function isTmpPath(normalized: string): boolean;
11
17
  export declare function isBuildOutput(normalized: string): boolean;
12
18
  export declare function isVendorPath(normalized: string): boolean;
13
19
  export declare function isBinaryArtifact(normalized: string): boolean;
@@ -156,6 +156,14 @@ function hasToken(normalized, values) {
156
156
  export function isNodeModulesOrGit(normalized) {
157
157
  return hasSegment(normalized, "node_modules") || hasSegment(normalized, ".git");
158
158
  }
159
+ /**
160
+ * `.tmp/` holds transient scratch and bundled tool copies (e.g. a vendored
161
+ * `.tmp/opentoken`). These are not the audited project's source — excluding
162
+ * them keeps the self-audit from auditing its own bundled dependencies.
163
+ */
164
+ export function isTmpPath(normalized) {
165
+ return hasSegment(normalized, ".tmp");
166
+ }
159
167
  export function isBuildOutput(normalized) {
160
168
  return hasSegment(normalized, "dist") || hasSegment(normalized, "build");
161
169
  }
@@ -2,12 +2,15 @@ import { cp, rm } from "node:fs/promises";
2
2
  import type { AuditResult, AuditTask, CoverageMatrix, RepoManifest, UnitManifest } from "../types.js";
3
3
  import type { AuditState } from "../types/auditState.js";
4
4
  import type { ArtifactMetadataManifest } from "../types/artifactMetadata.js";
5
- import type { FileDisposition, CriticalFlowManifest, GraphBundle, RiskRegister, SurfaceManifest } from "@audit-tools/shared";
5
+ import type { AuditFindingsReport, FileDisposition, CriticalFlowManifest, GraphBundle, RiskRegister, SurfaceManifest } from "@audit-tools/shared";
6
+ import type { SynthesisNarrativeRecord } from "../types/synthesisNarrative.js";
6
7
  import type { ExternalAnalyzerResults } from "../types/externalAnalyzer.js";
7
8
  import type { FlowCoverageManifest } from "../types/flowCoverage.js";
8
9
  import type { AuditPlanMetrics, ReviewPacket } from "../types/reviewPlanning.js";
9
10
  import type { RuntimeValidationReport, RuntimeValidationTaskManifest } from "../types/runtimeValidation.js";
10
11
  import type { DesignAssessment } from "../types/designAssessment.js";
12
+ import type { AnalyzerCapabilityRecord } from "../types/analyzerCapability.js";
13
+ import type { AuditScopeManifest } from "../types/auditScope.js";
11
14
  import type { ToolingManifest } from "../types/toolingManifest.js";
12
15
  type ArtifactPayloadMap = {
13
16
  repo_manifest: RepoManifest;
@@ -20,6 +23,8 @@ type ArtifactPayloadMap = {
20
23
  flow_coverage: FlowCoverageManifest;
21
24
  risk_register: RiskRegister;
22
25
  design_assessment: DesignAssessment;
26
+ analyzer_capability: AnalyzerCapabilityRecord;
27
+ scope: AuditScopeManifest;
23
28
  coverage_matrix: CoverageMatrix;
24
29
  runtime_validation_tasks: RuntimeValidationTaskManifest;
25
30
  runtime_validation_report: RuntimeValidationReport;
@@ -31,6 +36,8 @@ type ArtifactPayloadMap = {
31
36
  review_packets: ReviewPacket[];
32
37
  requeue_tasks: AuditTask[];
33
38
  audit_report: string;
39
+ audit_findings: AuditFindingsReport;
40
+ synthesis_narrative: SynthesisNarrativeRecord;
34
41
  audit_state: AuditState;
35
42
  artifact_metadata: ArtifactMetadataManifest;
36
43
  tooling_manifest: ToolingManifest;
@@ -48,6 +55,7 @@ interface ArtifactDefinition<K extends ArtifactBundleKey = ArtifactBundleKey> {
48
55
  read: (path: string) => Promise<ArtifactPayloadMap[K] | undefined>;
49
56
  write: (path: string, value: ArtifactPayloadMap[K]) => Promise<void>;
50
57
  }
58
+ export declare const AUDIT_REPORT_FILENAME = "audit-report.md";
51
59
  export declare const ARTIFACT_DEFINITIONS: {
52
60
  readonly repo_manifest: ArtifactDefinition<"repo_manifest">;
53
61
  readonly file_disposition: ArtifactDefinition<"file_disposition">;
@@ -59,6 +67,8 @@ export declare const ARTIFACT_DEFINITIONS: {
59
67
  readonly flow_coverage: ArtifactDefinition<"flow_coverage">;
60
68
  readonly risk_register: ArtifactDefinition<"risk_register">;
61
69
  readonly design_assessment: ArtifactDefinition<"design_assessment">;
70
+ readonly analyzer_capability: ArtifactDefinition<"analyzer_capability">;
71
+ readonly scope: ArtifactDefinition<"scope">;
62
72
  readonly coverage_matrix: ArtifactDefinition<"coverage_matrix">;
63
73
  readonly runtime_validation_tasks: ArtifactDefinition<"runtime_validation_tasks">;
64
74
  readonly runtime_validation_report: ArtifactDefinition<"runtime_validation_report">;
@@ -70,6 +80,8 @@ export declare const ARTIFACT_DEFINITIONS: {
70
80
  readonly review_packets: ArtifactDefinition<"review_packets">;
71
81
  readonly requeue_tasks: ArtifactDefinition<"requeue_tasks">;
72
82
  readonly audit_report: ArtifactDefinition<"audit_report">;
83
+ readonly audit_findings: ArtifactDefinition<"audit_findings">;
84
+ readonly synthesis_narrative: ArtifactDefinition<"synthesis_narrative">;
73
85
  readonly audit_state: ArtifactDefinition<"audit_state">;
74
86
  readonly artifact_metadata: ArtifactDefinition<"artifact_metadata">;
75
87
  readonly tooling_manifest: ArtifactDefinition<"tooling_manifest">;
@@ -2,6 +2,10 @@ 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 "@audit-tools/shared";
4
4
  import { buildToolingManifest } from "./toolingManifest.js";
5
+ // Canonical filename for the rendered findings report. Single source of truth
6
+ // for path construction. The dependency table below still lists it as plain
7
+ // data alongside its sibling artifact-name literals.
8
+ export const AUDIT_REPORT_FILENAME = "audit-report.md";
5
9
  function jsonArtifact(fileName, phase) {
6
10
  return {
7
11
  fileName,
@@ -37,6 +41,8 @@ export const ARTIFACT_DEFINITIONS = {
37
41
  flow_coverage: jsonArtifact("flow_coverage.json", "analysis"),
38
42
  risk_register: jsonArtifact("risk_register.json", "analysis"),
39
43
  design_assessment: jsonArtifact("design_assessment.json", "analysis"),
44
+ analyzer_capability: jsonArtifact("analyzer_capability.json", "analysis"),
45
+ scope: jsonArtifact("scope.json", "execution"),
40
46
  coverage_matrix: jsonArtifact("coverage_matrix.json", "execution"),
41
47
  runtime_validation_tasks: jsonArtifact("runtime_validation_tasks.json", "execution"),
42
48
  runtime_validation_report: jsonArtifact("runtime_validation_report.json", "execution"),
@@ -47,7 +53,9 @@ export const ARTIFACT_DEFINITIONS = {
47
53
  audit_plan_metrics: jsonArtifact("audit_plan_metrics.json", "execution"),
48
54
  review_packets: jsonArtifact("review_packets.json", "execution"),
49
55
  requeue_tasks: jsonArtifact("requeue_tasks.json", "execution"),
50
- audit_report: textArtifact("audit-report.md", "reporting"),
56
+ audit_report: textArtifact(AUDIT_REPORT_FILENAME, "reporting"),
57
+ audit_findings: jsonArtifact("audit-findings.json", "reporting"),
58
+ synthesis_narrative: jsonArtifact("synthesis-narrative.json", "reporting"),
51
59
  audit_state: jsonArtifact("audit_state.json", "supervisor"),
52
60
  artifact_metadata: jsonArtifact("artifact_metadata.json", "supervisor"),
53
61
  tooling_manifest: jsonArtifact("tooling_manifest.json", "supervisor"),
@@ -99,8 +107,8 @@ export async function cleanupIntermediateArtifacts(root) {
99
107
  return deleted;
100
108
  }
101
109
  export async function promoteFinalAuditReport(params, options = {}) {
102
- const source = join(params.artifactsDir, "audit-report.md");
103
- const destination = join(params.repoRoot, "audit-report.md");
110
+ const source = join(params.artifactsDir, AUDIT_REPORT_FILENAME);
111
+ const destination = join(params.repoRoot, AUDIT_REPORT_FILENAME);
104
112
  const copy = options.copy ?? cp;
105
113
  const remove = options.remove ?? rm;
106
114
  const warn = options.warn ?? ((message) => process.stderr.write(`${message}\n`));
@@ -113,6 +121,14 @@ export async function promoteFinalAuditReport(params, options = {}) {
113
121
  warn(warning);
114
122
  return { promoted: false, cleaned: false, warning };
115
123
  }
124
+ // Promote the canonical machine contract alongside the human report. Missing
125
+ // (e.g. legacy bundle) or unreadable: best-effort, never blocks completion.
126
+ try {
127
+ await copy(join(params.artifactsDir, "audit-findings.json"), join(params.repoRoot, "audit-findings.json"), { force: true });
128
+ }
129
+ catch {
130
+ // audit-findings.json is optional output; absence must not fail promotion.
131
+ }
116
132
  try {
117
133
  await remove(params.artifactsDir, { recursive: true, force: true });
118
134
  return { promoted: true, cleaned: true };
@@ -2,7 +2,7 @@ import { readFile } from "node:fs/promises";
2
2
  import { spawn } from "node:child_process";
3
3
  import { dirname, join, resolve } from "node:path";
4
4
  import { fileURLToPath } from "node:url";
5
- import { loadArtifactBundle } from "../io/artifacts.js";
5
+ import { loadArtifactBundle, AUDIT_REPORT_FILENAME } from "../io/artifacts.js";
6
6
  import { readOptionalTextFile } from "@audit-tools/shared";
7
7
  import { deriveAuditState } from "../orchestrator/state.js";
8
8
  import { decideNextStep } from "../orchestrator/nextStep.js";
@@ -225,8 +225,8 @@ export const resourceRegistry = [
225
225
  description: "Current deterministic audit report if available.",
226
226
  mimeType: "text/markdown",
227
227
  async read(context) {
228
- const report = (await readOptionalTextFile(join(context.artifactsDir, "audit-report.md"))) ??
229
- (await readOptionalTextFile(join(context.root, "audit-report.md"))) ??
228
+ const report = (await readOptionalTextFile(join(context.artifactsDir, AUDIT_REPORT_FILENAME))) ??
229
+ (await readOptionalTextFile(join(context.root, AUDIT_REPORT_FILENAME))) ??
230
230
  "The audit report has not been rendered yet.";
231
231
  return { mimeType: this.mimeType, text: report };
232
232
  },
@@ -3,14 +3,34 @@ import type { AuditState } from "../types/auditState.js";
3
3
  import type { AuditResult } from "../types.js";
4
4
  import type { RuntimeValidationReport } from "../types/runtimeValidation.js";
5
5
  import type { ExternalAnalyzerResults } from "../types/externalAnalyzer.js";
6
+ import { RunLogger } from "@audit-tools/shared";
7
+ import type { AnalyzerSetting, SynthesisNarrative } from "@audit-tools/shared";
8
+ import type { EdgeReasoningResults } from "./edgeReasoning.js";
6
9
  export interface AdvanceAuditOptions {
7
10
  root?: string;
8
11
  lineIndex?: Record<string, number>;
12
+ /** Path → size_bytes (from the repo manifest); drives byte-based packet token sizing. */
13
+ sizeIndex?: Record<string, number>;
9
14
  auditResults?: AuditResult[];
10
15
  runtimeValidationUpdates?: RuntimeValidationReport;
11
16
  externalAnalyzerResults?: ExternalAnalyzerResults;
17
+ /** Host/provider-supplied synthesis narrative; merged by synthesis_narrative_executor. */
18
+ narrativeResults?: SynthesisNarrative;
19
+ /** Per-analyzer resolution policy for the optional graph-enrichment pass. */
20
+ analyzers?: Record<string, AnalyzerSetting>;
21
+ /** Phase 4B gate (session-config `graph.llm_edge_reasoning`); default off. */
22
+ graphLlmEdgeReasoning?: boolean;
23
+ /** Phase 4B host-supplied reason rewrites for low-confidence graph edges. */
24
+ edgeReasoningResults?: EdgeReasoningResults;
25
+ /**
26
+ * Git ref for Phase 3 delta mode (the `--since` flag). When set and resolvable
27
+ * against a git repo, planning scopes coverage to the changed files and their
28
+ * graph neighbours; otherwise the run is a full audit.
29
+ */
30
+ since?: string;
12
31
  preferredExecutor?: string;
13
32
  opentoken?: boolean;
33
+ runLogger?: RunLogger;
14
34
  }
15
35
  export interface AdvanceAuditResult {
16
36
  audit_state: AuditState;
@@ -1,9 +1,12 @@
1
1
  import { decideNextStep, findObligation } from "./nextStep.js";
2
2
  import { deriveAuditState } from "./state.js";
3
3
  import { computeArtifactMetadata } from "./artifactMetadata.js";
4
- import { runIntakeExecutor, runStructureExecutor, runPlanningExecutor, runResultIngestionExecutor, runRuntimeValidationExecutor, runRuntimeValidationUpdateExecutor, runSynthesisExecutor, runDesignAssessmentExecutor, runDesignReviewAutoComplete, runExternalAnalyzerImportExecutor, } from "./internalExecutors.js";
4
+ import { runIntakeExecutor, runStructureExecutor, runPlanningExecutor, runResultIngestionExecutor, runRuntimeValidationExecutor, runRuntimeValidationUpdateExecutor, runSynthesisExecutor, runSynthesisNarrativeExecutor, runDesignAssessmentExecutor, runDesignReviewAutoComplete, runExternalAnalyzerImportExecutor, } from "./internalExecutors.js";
5
5
  import { runAutoFixExecutor } from "./autoFixExecutor.js";
6
6
  import { runSyntaxResolutionExecutor } from "./syntaxResolutionExecutor.js";
7
+ import { runGraphEnrichmentExecutor } from "./graphEnrichmentExecutor.js";
8
+ import { resolveAuditScope } from "./scope.js";
9
+ import { RunLogger } from "@audit-tools/shared";
7
10
  function cloneState(state) {
8
11
  return {
9
12
  ...state,
@@ -18,12 +21,19 @@ function formatExecutorFailure(selectedExecutor, selectedObligation, error) {
18
21
  });
19
22
  }
20
23
  export async function advanceAudit(bundle, options = {}) {
24
+ const log = options.runLogger ?? RunLogger.disabled();
21
25
  const decision = decideNextStep(bundle);
22
26
  const forcedExecutor = options.preferredExecutor ?? null;
23
27
  const selectedExecutor = forcedExecutor ?? decision.selected_executor;
24
28
  const selectedObligation = forcedExecutor
25
29
  ? `forced:${forcedExecutor}`
26
30
  : decision.selected_obligation;
31
+ log.event({
32
+ phase: "advance",
33
+ kind: "obligation",
34
+ obligation: selectedObligation ?? undefined,
35
+ note: decision.reason,
36
+ });
27
37
  if (!selectedExecutor) {
28
38
  const state = cloneState(decision.state);
29
39
  state.last_executor = bundle.audit_state?.last_executor ?? state.last_executor;
@@ -43,6 +53,14 @@ export async function advanceAudit(bundle, options = {}) {
43
53
  };
44
54
  }
45
55
  let run;
56
+ let plannedScope;
57
+ const executorStartedAt = Date.now();
58
+ log.event({
59
+ phase: "advance",
60
+ kind: "executor_start",
61
+ obligation: selectedObligation ?? undefined,
62
+ note: selectedExecutor,
63
+ });
46
64
  try {
47
65
  switch (selectedExecutor) {
48
66
  case "intake_executor":
@@ -53,6 +71,14 @@ export async function advanceAudit(bundle, options = {}) {
53
71
  case "structure_executor":
54
72
  run = await runStructureExecutor(bundle, options.root);
55
73
  break;
74
+ case "graph_enrichment_executor":
75
+ run = await runGraphEnrichmentExecutor(bundle, {
76
+ root: options.root,
77
+ analyzers: options.analyzers,
78
+ llmEdgeReasoning: options.graphLlmEdgeReasoning,
79
+ edgeReasoning: options.edgeReasoningResults,
80
+ });
81
+ break;
56
82
  case "design_assessment_executor":
57
83
  run = runDesignAssessmentExecutor(bundle);
58
84
  break;
@@ -62,7 +88,12 @@ export async function advanceAudit(bundle, options = {}) {
62
88
  case "planning_executor":
63
89
  if (!options.root)
64
90
  throw new Error("advanceAudit planning_executor requires root");
65
- run = await runPlanningExecutor(bundle, options.root, options.lineIndex ?? {});
91
+ plannedScope = resolveAuditScope({
92
+ root: options.root,
93
+ since: options.since,
94
+ bundle,
95
+ });
96
+ run = await runPlanningExecutor(bundle, options.root, options.lineIndex ?? {}, options.sizeIndex, plannedScope);
66
97
  break;
67
98
  case "result_ingestion_executor":
68
99
  run = runResultIngestionExecutor(bundle, options.auditResults ?? bundle.audit_results ?? []);
@@ -77,6 +108,9 @@ export async function advanceAudit(bundle, options = {}) {
77
108
  case "synthesis_executor":
78
109
  run = runSynthesisExecutor(bundle, options.auditResults);
79
110
  break;
111
+ case "synthesis_narrative_executor":
112
+ run = runSynthesisNarrativeExecutor(bundle, options.narrativeResults);
113
+ break;
80
114
  case "runtime_validation_update_executor":
81
115
  if (!options.runtimeValidationUpdates)
82
116
  throw new Error("advanceAudit runtime_validation_update_executor requires runtimeValidationUpdates");
@@ -117,6 +151,31 @@ export async function advanceAudit(bundle, options = {}) {
117
151
  catch (error) {
118
152
  throw formatExecutorFailure(selectedExecutor, selectedObligation, error);
119
153
  }
154
+ log.event({
155
+ phase: "advance",
156
+ kind: "executor_end",
157
+ obligation: selectedObligation ?? undefined,
158
+ note: selectedExecutor,
159
+ duration_ms: Date.now() - executorStartedAt,
160
+ });
161
+ if (plannedScope) {
162
+ log.event({
163
+ phase: "advance",
164
+ kind: "scope",
165
+ obligation: selectedObligation ?? undefined,
166
+ note: plannedScope.mode === "delta"
167
+ ? `delta since ${plannedScope.since}: ${plannedScope.seed_files.length} changed + ${plannedScope.expanded_files.length} neighbors; full audit advised before release`
168
+ : "full audit scope",
169
+ });
170
+ }
171
+ for (const artifact of run.artifacts_written) {
172
+ log.event({
173
+ phase: "advance",
174
+ kind: "artifact_write",
175
+ obligation: selectedObligation ?? undefined,
176
+ artifact,
177
+ });
178
+ }
120
179
  const metadata = computeArtifactMetadata(run.updated, bundle.artifact_metadata, [...run.artifacts_written, "tooling_manifest.json"]);
121
180
  const metadataBundle = {
122
181
  ...run.updated,
@@ -19,6 +19,15 @@ export const ARTIFACT_DEPENDENCY_MAP = {
19
19
  "runtime_validation_report.json",
20
20
  "audit-report.md",
21
21
  ],
22
+ // The optional graph-enrichment pass layers analyzer edges onto graph_bundle
23
+ // and records provenance in analyzer_capability.json. A re-built (structure)
24
+ // graph re-stales the marker so enrichment re-runs. No cycle: the enrichment
25
+ // executor writes graph_bundle AND the marker in one advanceAudit call, and
26
+ // computeArtifactMetadata is dependency-first, so the marker records the
27
+ // post-enrichment graph_bundle revision (mirrors audit-findings → narrative).
28
+ "graph_bundle.json": [
29
+ "analyzer_capability.json",
30
+ ],
22
31
  "file_disposition.json": [
23
32
  "unit_manifest.json",
24
33
  "surface_manifest.json",
@@ -91,6 +100,17 @@ export const ARTIFACT_DEPENDENCY_MAP = {
91
100
  "runtime_validation_report.json",
92
101
  "audit-report.md",
93
102
  ],
103
+ // Phase 3 delta scope. scope.json is produced by the planning executor (full
104
+ // or delta) and gates coverage: in delta mode it decides which coverage
105
+ // entries are (re)queued vs. inherited-complete/excluded. A changed scope
106
+ // (different `--since`/seed set → new content hash) re-stales coverage so the
107
+ // plan rebuilds. No cycle: planning writes scope.json AND coverage_matrix.json
108
+ // in one advanceAudit call, and computeArtifactMetadata is dependency-first,
109
+ // so coverage records the post-write scope revision (mirrors graph_bundle →
110
+ // analyzer_capability and audit-findings → narrative).
111
+ "scope.json": [
112
+ "coverage_matrix.json",
113
+ ],
94
114
  "coverage_matrix.json": [
95
115
  "flow_coverage.json",
96
116
  "audit_plan_metrics.json",
@@ -115,4 +135,11 @@ export const ARTIFACT_DEPENDENCY_MAP = {
115
135
  "runtime_validation_report.json": [
116
136
  "audit-report.md",
117
137
  ],
138
+ // The canonical machine contract is co-produced with audit-report.md by the
139
+ // synthesis executor. The optional narrative pass tracks its revision: a fresh
140
+ // (re-synthesized) audit-findings.json re-stales the narrative marker so the
141
+ // themes/executive-summary/top-risks regenerate. See spec/dependency-map.md.
142
+ "audit-findings.json": [
143
+ "synthesis-narrative.json",
144
+ ],
118
145
  };
@@ -0,0 +1,39 @@
1
+ import type { GraphBundle, GraphEdge } from "@audit-tools/shared";
2
+ export declare const DEFAULT_EDGE_CONFIDENCE_FLOOR = 0.65;
3
+ /** Bound the candidate set so one pathological repo cannot balloon the call. */
4
+ export declare const MAX_REASONED_EDGES = 200;
5
+ /** One host-supplied reason rewrite, matched to an edge by (from, to, kind). */
6
+ export interface EdgeReasonRewrite {
7
+ from: string;
8
+ to: string;
9
+ /** Optional; when omitted the rewrite matches any candidate with from+to. */
10
+ kind?: string;
11
+ reason: string;
12
+ }
13
+ export interface EdgeReasoningResults {
14
+ rewrites: EdgeReasonRewrite[];
15
+ }
16
+ export interface EdgeReasoningOptions {
17
+ /** Edges strictly below this confidence are candidates (default 0.65). */
18
+ confidenceFloor?: number;
19
+ }
20
+ export interface EdgeReasoningSummary {
21
+ rewritten: number;
22
+ candidates: number;
23
+ }
24
+ /**
25
+ * Collect the low-confidence edges (the actual edge objects, so the caller can
26
+ * mutate `reason` in place) in a deterministic order. Routes are excluded — they
27
+ * carry no `reason`/`confidence`.
28
+ */
29
+ export declare function collectLowConfidenceEdges(bundle: GraphBundle, floor?: number): GraphEdge[];
30
+ /** Stable content hash of the candidate edge set, for host-side call caching. */
31
+ export declare function edgeReasoningContentHash(candidates: GraphEdge[]): string;
32
+ /** The single bounded prompt a host runs to produce {@link EdgeReasoningResults}. */
33
+ export declare function buildEdgeReasoningPrompt(candidates: GraphEdge[]): string;
34
+ /**
35
+ * Apply host-supplied reason rewrites to `bundle` (mutated in place). Only edges
36
+ * below the confidence floor are eligible; a rewrite that matches no eligible
37
+ * edge is ignored. Returns a summary; the edge set itself is invariant.
38
+ */
39
+ export declare function applyEdgeReasoning(bundle: GraphBundle, results: EdgeReasoningResults | undefined, options?: EdgeReasoningOptions): EdgeReasoningSummary;
@@ -0,0 +1,125 @@
1
+ import { createHash } from "node:crypto";
2
+ /**
3
+ * Phase 4B — optional, bounded edge-reasoning pass.
4
+ *
5
+ * A deterministic transform that may only rewrite the human-readable `reason`
6
+ * of existing low-confidence graph edges. It never adds, removes, re-targets, or
7
+ * re-weights an edge: the `(from, to, kind, confidence, direction)` identity of
8
+ * every edge is preserved exactly. Rewrites are host/provider-supplied (the same
9
+ * conversation-first pattern as the Phase 6 synthesis narrative) — no in-process
10
+ * LLM call. No rewrites (or the config flag off) → no-op, leaving the
11
+ * deterministic graph byte-identical. This is part of the
12
+ * `graph_enrichment_current` obligation.
13
+ *
14
+ * The pass is bounded to edges below a confidence floor (default 0.65) because
15
+ * those are exactly the heuristic edges whose terse machine reason benefits from
16
+ * a clearer explanation; high-confidence compiler/import edges are left alone.
17
+ * {@link buildEdgeReasoningPrompt} and {@link edgeReasoningContentHash} let a
18
+ * host produce and cache that single rewriting call by edge-set content hash.
19
+ */
20
+ const EDGE_REASONING_VERSION = 1;
21
+ export const DEFAULT_EDGE_CONFIDENCE_FLOOR = 0.65;
22
+ /** Bound the candidate set so one pathological repo cannot balloon the call. */
23
+ export const MAX_REASONED_EDGES = 200;
24
+ function confidenceOf(edge) {
25
+ return typeof edge.confidence === "number" && Number.isFinite(edge.confidence)
26
+ ? edge.confidence
27
+ : 0;
28
+ }
29
+ function edgeSignature(edge) {
30
+ return `${edge.from}\0${edge.to}\0${edge.kind ?? ""}`;
31
+ }
32
+ /**
33
+ * Collect the low-confidence edges (the actual edge objects, so the caller can
34
+ * mutate `reason` in place) in a deterministic order. Routes are excluded — they
35
+ * carry no `reason`/`confidence`.
36
+ */
37
+ export function collectLowConfidenceEdges(bundle, floor = DEFAULT_EDGE_CONFIDENCE_FLOOR) {
38
+ const candidates = [];
39
+ for (const bucket of [
40
+ bundle.graphs.imports,
41
+ bundle.graphs.calls,
42
+ bundle.graphs.references,
43
+ ]) {
44
+ for (const edge of bucket ?? []) {
45
+ if (confidenceOf(edge) < floor) {
46
+ candidates.push(edge);
47
+ }
48
+ }
49
+ }
50
+ return candidates
51
+ .sort((a, b) => edgeSignature(a).localeCompare(edgeSignature(b)))
52
+ .slice(0, MAX_REASONED_EDGES);
53
+ }
54
+ /** Stable content hash of the candidate edge set, for host-side call caching. */
55
+ export function edgeReasoningContentHash(candidates) {
56
+ const basis = JSON.stringify({
57
+ version: EDGE_REASONING_VERSION,
58
+ edges: candidates.map((edge) => ({
59
+ from: edge.from,
60
+ to: edge.to,
61
+ kind: edge.kind ?? "",
62
+ confidence: confidenceOf(edge),
63
+ reason: edge.reason ?? "",
64
+ })),
65
+ });
66
+ return createHash("sha256").update(basis).digest("hex");
67
+ }
68
+ /** The single bounded prompt a host runs to produce {@link EdgeReasoningResults}. */
69
+ export function buildEdgeReasoningPrompt(candidates) {
70
+ const lines = candidates.map((edge) => `- from: ${edge.from} | to: ${edge.to} | kind: ${edge.kind ?? "?"} | confidence: ${confidenceOf(edge).toFixed(2)} | current: ${edge.reason ?? "(none)"}`);
71
+ return [
72
+ "You are improving the human-readable 'reason' for low-confidence edges in a code dependency graph.",
73
+ "Each edge links a source file to a target file by a relationship 'kind'.",
74
+ "For each edge you can improve, write one clear, specific sentence explaining why that relationship plausibly holds.",
75
+ "Do NOT invent new edges, drop edges, or change which files are linked — only rewrite the reason text.",
76
+ "Omit any edge whose reason you cannot improve.",
77
+ "",
78
+ "Edges:",
79
+ ...lines,
80
+ "",
81
+ 'Respond with JSON only: {"rewrites":[{"from":"...","to":"...","kind":"...","reason":"..."}]}',
82
+ ].join("\n");
83
+ }
84
+ /**
85
+ * Apply host-supplied reason rewrites to `bundle` (mutated in place). Only edges
86
+ * below the confidence floor are eligible; a rewrite that matches no eligible
87
+ * edge is ignored. Returns a summary; the edge set itself is invariant.
88
+ */
89
+ export function applyEdgeReasoning(bundle, results, options = {}) {
90
+ const floor = options.confidenceFloor ?? DEFAULT_EDGE_CONFIDENCE_FLOOR;
91
+ const candidates = collectLowConfidenceEdges(bundle, floor);
92
+ if (!results || !Array.isArray(results.rewrites) || candidates.length === 0) {
93
+ return { rewritten: 0, candidates: candidates.length };
94
+ }
95
+ const bySignature = new Map();
96
+ const byEndpoints = new Map();
97
+ for (const edge of candidates) {
98
+ bySignature.set(edgeSignature(edge), edge);
99
+ const endpoints = `${edge.from}\0${edge.to}`;
100
+ if (!byEndpoints.has(endpoints)) {
101
+ byEndpoints.set(endpoints, edge);
102
+ }
103
+ }
104
+ let rewritten = 0;
105
+ const seen = new Set();
106
+ for (const rewrite of results.rewrites) {
107
+ if (!rewrite ||
108
+ typeof rewrite.from !== "string" ||
109
+ typeof rewrite.to !== "string" ||
110
+ typeof rewrite.reason !== "string" ||
111
+ rewrite.reason.trim().length === 0) {
112
+ continue;
113
+ }
114
+ const edge = rewrite.kind !== undefined
115
+ ? bySignature.get(`${rewrite.from}\0${rewrite.to}\0${rewrite.kind}`)
116
+ : byEndpoints.get(`${rewrite.from}\0${rewrite.to}`);
117
+ if (!edge || seen.has(edge)) {
118
+ continue;
119
+ }
120
+ edge.reason = rewrite.reason.trim();
121
+ seen.add(edge);
122
+ rewritten += 1;
123
+ }
124
+ return { rewritten, candidates: candidates.length };
125
+ }
@@ -9,6 +9,11 @@ export const EXECUTOR_REGISTRY = [
9
9
  obligation_ids: ["structure_artifacts"],
10
10
  description: "Build structure artifacts such as units, surfaces, graphs, flows, and risk.",
11
11
  },
12
+ {
13
+ id: "graph_enrichment_executor",
14
+ obligation_ids: ["graph_enrichment_current"],
15
+ description: "Layer optional language-analyzer edges onto the deterministic graph (regex floor preserved); record analyzer provenance.",
16
+ },
12
17
  {
13
18
  id: "design_assessment_executor",
14
19
  obligation_ids: ["design_assessment_current"],
@@ -42,7 +47,12 @@ export const EXECUTOR_REGISTRY = [
42
47
  {
43
48
  id: "synthesis_executor",
44
49
  obligation_ids: ["synthesis_current"],
45
- description: "Render the final deterministic Markdown audit report.",
50
+ description: "Emit the canonical audit-findings.json and render the deterministic Markdown audit report.",
51
+ },
52
+ {
53
+ id: "synthesis_narrative_executor",
54
+ obligation_ids: ["synthesis_narrative_current"],
55
+ description: "Resolve the optional synthesis narrative (themes, executive summary, top risks); omit deterministically without a provider.",
46
56
  },
47
57
  {
48
58
  id: "external_analyzer_import_executor",
@@ -0,0 +1,29 @@
1
+ import type { ArtifactBundle } from "../io/artifacts.js";
2
+ import type { ExecutorRunResult } from "./internalExecutors.js";
3
+ import type { AnalyzerSetting } from "@audit-tools/shared";
4
+ import type { LanguageAnalyzer } from "../extractors/analyzers/types.js";
5
+ import { type EdgeReasoningResults } from "./edgeReasoning.js";
6
+ export interface GraphEnrichmentOptions {
7
+ root?: string;
8
+ analyzers?: Record<string, AnalyzerSetting>;
9
+ /** Injectable for tests; defaults to the global registry. */
10
+ registry?: LanguageAnalyzer[];
11
+ /** Injectable analyzer-cache root; defaults to ~/.audit-tools/analyzer-cache. */
12
+ cacheRoot?: string;
13
+ /**
14
+ * Phase 4B: gate for the optional edge-reasoning pass (mirrors
15
+ * session-config `graph.llm_edge_reasoning`; default off).
16
+ */
17
+ llmEdgeReasoning?: boolean;
18
+ /** Phase 4B: host-supplied reason rewrites for low-confidence edges. */
19
+ edgeReasoning?: EdgeReasoningResults;
20
+ }
21
+ /**
22
+ * Resolve the optional graph-enrichment obligation. Layers language-analyzer
23
+ * edges onto the deterministic regex floor in `graph_bundle.json`
24
+ * (higher-confidence-kind-wins) and records provenance in
25
+ * `analyzer_capability.json`. With no root, or when every analyzer skips / is
26
+ * absent / not-applicable, the graph bundle is left byte-identical to the floor
27
+ * and only the marker is written.
28
+ */
29
+ export declare function runGraphEnrichmentExecutor(bundle: ArtifactBundle, options?: GraphEnrichmentOptions): Promise<ExecutorRunResult>;