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
@@ -0,0 +1,196 @@
1
+ import { installToCache, resolveAnalyzerDep } from "@audit-tools/shared";
2
+ import { buildDispositionMap } from "../extractors/disposition.js";
3
+ import { buildPathLookup } from "../extractors/graph.js";
4
+ import { mergeAnalyzerEdges } from "../extractors/analyzers/merge.js";
5
+ import { ANALYZER_REGISTRY } from "../extractors/analyzers/registry.js";
6
+ import { applyEdgeReasoning, } from "./edgeReasoning.js";
7
+ const BUCKET_BY_KIND = {
8
+ "ts-import": "imports",
9
+ "ts-reexport": "imports",
10
+ "ts-call": "calls",
11
+ "ts-extends": "references",
12
+ "ts-implements": "references",
13
+ // Python (tree-sitter) imports merge into the imports bucket alongside the
14
+ // regex floor's python-* edges.
15
+ "py-import": "imports",
16
+ "py-from-import": "imports",
17
+ // HTML/CSS (tree-sitter) resource references live with the floor's
18
+ // html-resource-link / reference edges.
19
+ "html-resource": "references",
20
+ "css-import": "references",
21
+ "css-url": "references",
22
+ };
23
+ function bucketForKind(kind) {
24
+ const bucket = kind ? BUCKET_BY_KIND[kind] : undefined;
25
+ return bucket ?? "references";
26
+ }
27
+ function settingFor(analyzers, id) {
28
+ return analyzers?.[id] ?? "auto";
29
+ }
30
+ function routeSignature(route) {
31
+ return `${route.method ?? ""}\0${route.path}\0${route.handler}`;
32
+ }
33
+ function mergeRoutes(floor, analyzer) {
34
+ const deduped = new Map();
35
+ for (const route of [...floor, ...analyzer]) {
36
+ deduped.set(routeSignature(route), route);
37
+ }
38
+ return [...deduped.values()].sort((a, b) => a.path.localeCompare(b.path) ||
39
+ a.handler.localeCompare(b.handler) ||
40
+ (a.method ?? "").localeCompare(b.method ?? ""));
41
+ }
42
+ /**
43
+ * Resolve a dependency for actual execution (may install for ephemeral/permanent).
44
+ * `auto`/`repo` with an absent dependency falls back to the regex floor.
45
+ */
46
+ function resolveForRun(analyzer, root, setting, cacheRoot) {
47
+ if (!analyzer.dependency) {
48
+ return { resolution: "repo" };
49
+ }
50
+ const options = cacheRoot ? { cacheRoot } : {};
51
+ const resolved = resolveAnalyzerDep(analyzer.dependency, root, options);
52
+ if (resolved.via === "repo" || resolved.via === "cache") {
53
+ return { resolution: resolved.via, path: resolved.path };
54
+ }
55
+ if (setting === "ephemeral" || setting === "permanent") {
56
+ const install = installToCache(analyzer.dependency, options);
57
+ if (install.ok && install.path) {
58
+ return { resolution: "installed", path: install.path };
59
+ }
60
+ return {
61
+ resolution: "absent",
62
+ note: `Install of '${analyzer.dependency}' failed: ${install.error ?? "unknown error"}.`,
63
+ };
64
+ }
65
+ return {
66
+ resolution: "absent",
67
+ note: `Dependency '${analyzer.dependency}' not resolvable; kept regex floor.`,
68
+ };
69
+ }
70
+ /**
71
+ * Resolve the optional graph-enrichment obligation. Layers language-analyzer
72
+ * edges onto the deterministic regex floor in `graph_bundle.json`
73
+ * (higher-confidence-kind-wins) and records provenance in
74
+ * `analyzer_capability.json`. With no root, or when every analyzer skips / is
75
+ * absent / not-applicable, the graph bundle is left byte-identical to the floor
76
+ * and only the marker is written.
77
+ */
78
+ export async function runGraphEnrichmentExecutor(bundle, options = {}) {
79
+ const floor = bundle.graph_bundle;
80
+ if (!floor) {
81
+ throw new Error("Cannot run graph enrichment without graph_bundle");
82
+ }
83
+ const registry = options.registry ?? ANALYZER_REGISTRY;
84
+ const root = options.root;
85
+ const disposition = bundle.file_disposition;
86
+ const dispositionMap = buildDispositionMap(disposition);
87
+ const pathLookup = bundle.repo_manifest
88
+ ? buildPathLookup(bundle.repo_manifest, dispositionMap)
89
+ : new Map();
90
+ const includedFiles = [...new Set(pathLookup.values())].sort((a, b) => a.localeCompare(b));
91
+ const entries = [];
92
+ const bucketEdges = { imports: [], calls: [], references: [] };
93
+ const routeEdges = [];
94
+ const analyzersUsed = [];
95
+ for (const analyzer of registry) {
96
+ const setting = settingFor(options.analyzers, analyzer.id);
97
+ const supportedFiles = includedFiles.filter((file) => analyzer.supports(file));
98
+ if (supportedFiles.length === 0) {
99
+ entries.push({ id: analyzer.id, resolution: "not_applicable", setting, edges_added: 0, routes_added: 0 });
100
+ continue;
101
+ }
102
+ if (setting === "skip") {
103
+ entries.push({ id: analyzer.id, resolution: "skip", setting, edges_added: 0, routes_added: 0, note: "Analyzer disabled via session config." });
104
+ continue;
105
+ }
106
+ if (!root || !bundle.repo_manifest) {
107
+ entries.push({ id: analyzer.id, resolution: "absent", setting, edges_added: 0, routes_added: 0, note: "No repository root available for analysis." });
108
+ continue;
109
+ }
110
+ const run = resolveForRun(analyzer, root, setting, options.cacheRoot);
111
+ if (run.resolution === "absent") {
112
+ entries.push({ id: analyzer.id, resolution: "absent", setting, edges_added: 0, routes_added: 0, note: run.note });
113
+ continue;
114
+ }
115
+ let edges = [];
116
+ let routes = [];
117
+ try {
118
+ const output = await analyzer.analyze(supportedFiles, {
119
+ root,
120
+ repoManifest: bundle.repo_manifest,
121
+ disposition,
122
+ includedFiles,
123
+ pathLookup,
124
+ dependencyPath: run.path,
125
+ });
126
+ edges = output.edges ?? [];
127
+ routes = output.routes ?? [];
128
+ }
129
+ catch (error) {
130
+ entries.push({
131
+ id: analyzer.id,
132
+ resolution: run.resolution,
133
+ setting,
134
+ edges_added: 0,
135
+ routes_added: 0,
136
+ note: `Analyzer failed: ${error instanceof Error ? error.message : String(error)}.`,
137
+ });
138
+ continue;
139
+ }
140
+ for (const edge of edges) {
141
+ bucketEdges[bucketForKind(edge.kind)].push(edge);
142
+ }
143
+ routeEdges.push(...routes);
144
+ if (edges.length + routes.length > 0) {
145
+ analyzersUsed.push(analyzer.id);
146
+ }
147
+ entries.push({ id: analyzer.id, resolution: run.resolution, setting, edges_added: edges.length, routes_added: routes.length });
148
+ }
149
+ const applied = analyzersUsed.length > 0;
150
+ const record = {
151
+ status: applied ? "applied" : "omitted",
152
+ analyzers: entries,
153
+ };
154
+ // The graph this obligation produces: the enriched bundle when analyzers
155
+ // contributed, otherwise the regex floor. Phase 4B may then rewrite the
156
+ // reasons of low-confidence edges on whichever graph stands — the floor's
157
+ // heuristic edges exist regardless of analyzers.
158
+ const graphBundle = applied
159
+ ? {
160
+ ...floor,
161
+ graphs: {
162
+ ...floor.graphs,
163
+ imports: mergeAnalyzerEdges(floor.graphs.imports ?? [], bucketEdges.imports),
164
+ calls: mergeAnalyzerEdges(floor.graphs.calls ?? [], bucketEdges.calls),
165
+ references: mergeAnalyzerEdges(floor.graphs.references ?? [], bucketEdges.references),
166
+ ...(routeEdges.length > 0
167
+ ? { routes: mergeRoutes(floor.graphs.routes ?? [], routeEdges) }
168
+ : {}),
169
+ },
170
+ analyzers_used: [...new Set(analyzersUsed)].sort(),
171
+ }
172
+ : floor;
173
+ let reasoned = { rewritten: 0, candidates: 0 };
174
+ if (options.llmEdgeReasoning === true && options.edgeReasoning) {
175
+ reasoned = applyEdgeReasoning(graphBundle, options.edgeReasoning);
176
+ }
177
+ const graphChanged = applied || reasoned.rewritten > 0;
178
+ const reasonSuffix = reasoned.rewritten > 0
179
+ ? ` Edge reasoning rewrote ${reasoned.rewritten} reason(s).`
180
+ : "";
181
+ if (!graphChanged) {
182
+ return {
183
+ updated: { ...bundle, analyzer_capability: record },
184
+ artifacts_written: ["analyzer_capability.json"],
185
+ progress_summary: "Graph enrichment omitted; deterministic regex graph retained.",
186
+ };
187
+ }
188
+ const totalEdges = entries.reduce((sum, entry) => sum + entry.edges_added, 0);
189
+ return {
190
+ updated: { ...bundle, graph_bundle: graphBundle, analyzer_capability: record },
191
+ artifacts_written: ["graph_bundle.json", "analyzer_capability.json"],
192
+ progress_summary: applied
193
+ ? `Graph enrichment applied ${totalEdges} analyzer edge(s) from ${analyzersUsed.join(", ")}.${reasonSuffix}`
194
+ : `Graph enrichment omitted analyzers; edge reasoning rewrote ${reasoned.rewritten} reason(s).`,
195
+ };
196
+ }
@@ -2,6 +2,8 @@ import type { ArtifactBundle } from "../io/artifacts.js";
2
2
  import type { AuditResult } from "../types.js";
3
3
  import type { RuntimeValidationReport } from "../types/runtimeValidation.js";
4
4
  import type { ExternalAnalyzerResults } from "../types/externalAnalyzer.js";
5
+ import type { AuditScopeManifest } from "../types/auditScope.js";
6
+ import type { SynthesisNarrative } from "@audit-tools/shared";
5
7
  export interface ExecutorRunResult {
6
8
  updated: ArtifactBundle;
7
9
  artifacts_written: string[];
@@ -15,11 +17,18 @@ export declare function runIntakeExecutor(bundle: ArtifactBundle, root: string):
15
17
  export declare function runStructureExecutor(bundle: ArtifactBundle, root?: string): Promise<ExecutorRunResult>;
16
18
  export declare function runDesignAssessmentExecutor(bundle: ArtifactBundle): ExecutorRunResult;
17
19
  export declare function runDesignReviewAutoComplete(bundle: ArtifactBundle): ExecutorRunResult;
18
- export declare function runPlanningExecutor(bundle: ArtifactBundle, root: string, lineIndex?: Record<string, number>): Promise<ExecutorRunResult>;
20
+ export declare function runPlanningExecutor(bundle: ArtifactBundle, root: string, lineIndex?: Record<string, number>, sizeIndex?: Record<string, number>, scope?: AuditScopeManifest): Promise<ExecutorRunResult>;
19
21
  export declare function runResultIngestionExecutor(bundle: ArtifactBundle, results: AuditResult[]): ExecutorRunResult;
20
22
  export declare function runRuntimeValidationExecutor(bundle: ArtifactBundle, root: string, options?: {
21
23
  opentoken?: boolean;
22
24
  }): Promise<ExecutorRunResult>;
23
25
  export declare function runRuntimeValidationUpdateExecutor(bundle: ArtifactBundle, updates: RuntimeValidationReport): ExecutorRunResult;
24
26
  export declare function runSynthesisExecutor(bundle: ArtifactBundle, results?: AuditResult[]): ExecutorRunResult;
27
+ /**
28
+ * Resolve the optional synthesis-narrative obligation. When a host/provider
29
+ * narrative is supplied it is merged into the canonical findings report and the
30
+ * human report is re-rendered with themes/executive-summary/top-risks; without
31
+ * one the narrative is recorded as omitted and the deterministic report stands.
32
+ */
33
+ export declare function runSynthesisNarrativeExecutor(bundle: ArtifactBundle, narrative?: SynthesisNarrative): ExecutorRunResult;
25
34
  export declare function runExternalAnalyzerImportExecutor(bundle: ArtifactBundle, externalResults: ExternalAnalyzerResults): ExecutorRunResult;
@@ -5,12 +5,13 @@ import { buildCriticalFlowManifest } from "../extractors/flows.js";
5
5
  import { buildRiskRegister } from "../extractors/risk.js";
6
6
  import { buildSurfaceManifest } from "../extractors/surfaces.js";
7
7
  import { initializeCoverageFromPlan } from "./planning.js";
8
+ import { applyScopeToCoverage, fullAuditScope } from "./scope.js";
8
9
  import { buildFlowCoverage } from "./flowCoverage.js";
9
10
  import { buildRequeuePayload } from "./requeueCommand.js";
10
11
  import { buildRuntimeValidationTasks, discoverRuntimeValidationCommand, mergeRuntimeValidationReport, } from "./runtimeValidation.js";
11
- import { buildAuditReportModel, renderAuditReportMarkdown, } from "../reporting/synthesis.js";
12
+ import { applyNarrative, buildAuditFindingsReport, buildAuditReportModel, renderAuditReportMarkdown, } from "../reporting/synthesis.js";
12
13
  import { buildChunkedAuditTasks, } from "./taskBuilder.js";
13
- import { buildAuditPlanMetrics, buildReviewPackets, } from "./reviewPackets.js";
14
+ import { buildAuditPlanMetrics, buildReviewPackets, sizeIndexFromManifest, } from "./reviewPackets.js";
14
15
  import { buildUnitManifest } from "./unitBuilder.js";
15
16
  import { buildRepoManifestFromFs } from "../extractors/fsIntake.js";
16
17
  import { loadIgnoreFile } from "../extractors/ignore.js";
@@ -27,6 +28,7 @@ function appendSelectiveDeepeningTasks(params) {
27
28
  return { bundle: params.bundle, taskCount: 0, artifacts: [] };
28
29
  }
29
30
  const lineIndex = lineIndexFromTasks(params.bundle.audit_tasks);
31
+ const sizeIndex = sizeIndexFromManifest(params.bundle.repo_manifest);
30
32
  const selectiveDeepeningTasks = buildSelectiveDeepeningTasks({
31
33
  existingTasks: params.bundle.audit_tasks,
32
34
  results: params.results,
@@ -46,10 +48,12 @@ function appendSelectiveDeepeningTasks(params) {
46
48
  audit_plan_metrics: buildAuditPlanMetrics(auditTasks, {
47
49
  graphBundle: params.bundle.graph_bundle,
48
50
  lineIndex,
51
+ sizeIndex,
49
52
  }),
50
53
  review_packets: buildReviewPackets(auditTasks, {
51
54
  graphBundle: params.bundle.graph_bundle,
52
55
  lineIndex,
56
+ sizeIndex,
53
57
  }),
54
58
  },
55
59
  taskCount: selectiveDeepeningTasks.length,
@@ -238,10 +242,11 @@ export function runDesignReviewAutoComplete(bundle) {
238
242
  progress_summary: "Design review auto-completed (host-agent review available via next-step).",
239
243
  };
240
244
  }
241
- export async function runPlanningExecutor(bundle, root, lineIndex = {}) {
245
+ export async function runPlanningExecutor(bundle, root, lineIndex = {}, sizeIndex, scope) {
242
246
  if (!bundle.repo_manifest) {
243
247
  throw new Error("Cannot run planning executor without repo_manifest");
244
248
  }
249
+ const resolvedSizeIndex = sizeIndex ?? sizeIndexFromManifest(bundle.repo_manifest);
245
250
  if (!bundle.file_disposition ||
246
251
  !bundle.unit_manifest ||
247
252
  !bundle.surface_manifest ||
@@ -249,9 +254,13 @@ export async function runPlanningExecutor(bundle, root, lineIndex = {}) {
249
254
  !bundle.risk_register) {
250
255
  throw new Error("Cannot run planning executor without current structure artifacts");
251
256
  }
257
+ const resolvedScope = scope ?? fullAuditScope();
252
258
  const externalAnalyzerResults = bundle.external_analyzer_results;
253
259
  const coverage = initializeCoverageFromPlan(bundle.repo_manifest, bundle.unit_manifest, bundle.file_disposition, externalAnalyzerResults);
254
260
  const skippedTrivialPaths = autoCompleteTrivialCoverage(coverage, lineIndex, externalAnalyzerResults);
261
+ // Delta scope: only seed + expanded files stay pending; the rest inherit prior
262
+ // completion or are excluded from this run. Full scope is a no-op.
263
+ applyScopeToCoverage(coverage, resolvedScope, bundle.coverage_matrix);
255
264
  const flowCoverage = buildFlowCoverage(bundle.critical_flows, coverage);
256
265
  const runtimeCommand = await discoverRuntimeValidationCommand(root);
257
266
  const runtimeValidationTasks = buildRuntimeValidationTasks({
@@ -274,15 +283,21 @@ export async function runPlanningExecutor(bundle, root, lineIndex = {}) {
274
283
  const reviewPackets = buildReviewPackets(taggedAuditTasks, {
275
284
  graphBundle: bundle.graph_bundle,
276
285
  lineIndex,
286
+ sizeIndex: resolvedSizeIndex,
277
287
  });
278
288
  const auditPlanMetrics = buildAuditPlanMetrics(taggedAuditTasks, {
279
289
  graphBundle: bundle.graph_bundle,
280
290
  lineIndex,
291
+ sizeIndex: resolvedSizeIndex,
281
292
  });
282
293
  const requeuePayload = buildRequeuePayload(coverage, bundle.critical_flows, flowCoverage, externalAnalyzerResults);
294
+ const scopeSummary = resolvedScope.mode === "delta"
295
+ ? ` Delta scope since ${resolvedScope.since}: ${resolvedScope.seed_files.length} changed file(s) + ${resolvedScope.expanded_files.length} graph neighbour(s) queued; a full audit is advised before release.`
296
+ : "";
283
297
  return {
284
298
  updated: {
285
299
  ...bundle,
300
+ scope: resolvedScope,
286
301
  coverage_matrix: coverage,
287
302
  flow_coverage: flowCoverage,
288
303
  runtime_validation_tasks: runtimeValidationTasks,
@@ -294,6 +309,7 @@ export async function runPlanningExecutor(bundle, root, lineIndex = {}) {
294
309
  audit_report: undefined,
295
310
  },
296
311
  artifacts_written: [
312
+ "scope.json",
297
313
  "coverage_matrix.json",
298
314
  "flow_coverage.json",
299
315
  "runtime_validation_tasks.json",
@@ -304,6 +320,7 @@ export async function runPlanningExecutor(bundle, root, lineIndex = {}) {
304
320
  "requeue_tasks.json",
305
321
  ],
306
322
  progress_summary: `Built planning artifacts; generated ${taggedAuditTasks.length} review tasks in ${reviewPackets.length} packet(s) and ${requeuePayload.task_count} requeue tasks.` +
323
+ scopeSummary +
307
324
  (skippedTrivialPaths.length > 0
308
325
  ? ` Skipped ${skippedTrivialPaths.length} trivial path${skippedTrivialPaths.length === 1 ? "" : "s"} from semantic review.`
309
326
  : "") +
@@ -459,10 +476,9 @@ export function runRuntimeValidationUpdateExecutor(bundle, updates) {
459
476
  : ""),
460
477
  };
461
478
  }
462
- export function runSynthesisExecutor(bundle, results) {
463
- const finalResults = results ?? bundle.audit_results ?? [];
464
- const model = buildAuditReportModel({
465
- results: finalResults,
479
+ function buildBaseFindingsReport(bundle, results) {
480
+ return buildAuditFindingsReport(buildAuditReportModel({
481
+ results,
466
482
  unitManifest: bundle.unit_manifest,
467
483
  graphBundle: bundle.graph_bundle,
468
484
  criticalFlows: bundle.critical_flows,
@@ -470,15 +486,77 @@ export function runSynthesisExecutor(bundle, results) {
470
486
  runtimeValidationReport: bundle.runtime_validation_report,
471
487
  externalAnalyzerResults: bundle.external_analyzer_results,
472
488
  designAssessment: bundle.design_assessment,
473
- });
489
+ }));
490
+ }
491
+ export function runSynthesisExecutor(bundle, results) {
492
+ const finalResults = results ?? bundle.audit_results ?? [];
493
+ // Emit the canonical machine contract and render the human report from it.
494
+ // No narrative yet — that is layered by the synthesis-narrative obligation.
495
+ const findings = buildBaseFindingsReport(bundle, finalResults);
474
496
  return {
475
497
  updated: {
476
498
  ...bundle,
477
499
  audit_results: finalResults,
478
- audit_report: renderAuditReportMarkdown(model),
500
+ audit_findings: findings,
501
+ audit_report: renderAuditReportMarkdown(findings, { scope: bundle.scope }),
502
+ },
503
+ artifacts_written: ["audit-findings.json", "audit-report.md"],
504
+ progress_summary: `Rendered deterministic audit report and canonical findings for ${finalResults.length} audit result entries.`,
505
+ };
506
+ }
507
+ /**
508
+ * Resolve the optional synthesis-narrative obligation. When a host/provider
509
+ * narrative is supplied it is merged into the canonical findings report and the
510
+ * human report is re-rendered with themes/executive-summary/top-risks; without
511
+ * one the narrative is recorded as omitted and the deterministic report stands.
512
+ */
513
+ export function runSynthesisNarrativeExecutor(bundle, narrative) {
514
+ const baseReport = bundle.audit_findings ??
515
+ buildBaseFindingsReport(bundle, bundle.audit_results ?? []);
516
+ const needsBaseWrite = !bundle.audit_findings;
517
+ const hasNarrative = Boolean(narrative &&
518
+ ((narrative.themes?.length ?? 0) > 0 ||
519
+ (narrative.executive_summary?.trim().length ?? 0) > 0 ||
520
+ (narrative.top_risks?.length ?? 0) > 0));
521
+ if (!hasNarrative) {
522
+ const record = {
523
+ status: "omitted",
524
+ theme_count: 0,
525
+ executive_summary_present: false,
526
+ top_risk_count: 0,
527
+ };
528
+ return {
529
+ updated: {
530
+ ...bundle,
531
+ audit_findings: baseReport,
532
+ synthesis_narrative: record,
533
+ },
534
+ artifacts_written: needsBaseWrite
535
+ ? ["audit-findings.json", "synthesis-narrative.json"]
536
+ : ["synthesis-narrative.json"],
537
+ progress_summary: "Synthesis narrative omitted; deterministic findings report retained.",
538
+ };
539
+ }
540
+ const enriched = applyNarrative(baseReport, narrative);
541
+ const record = {
542
+ status: "applied",
543
+ theme_count: enriched.themes?.length ?? 0,
544
+ executive_summary_present: (enriched.executive_summary?.trim().length ?? 0) > 0,
545
+ top_risk_count: enriched.top_risks?.length ?? 0,
546
+ };
547
+ return {
548
+ updated: {
549
+ ...bundle,
550
+ audit_findings: enriched,
551
+ audit_report: renderAuditReportMarkdown(enriched, { scope: bundle.scope }),
552
+ synthesis_narrative: record,
479
553
  },
480
- artifacts_written: ["audit-report.md"],
481
- progress_summary: `Rendered deterministic audit report for ${finalResults.length} audit result entries.`,
554
+ artifacts_written: [
555
+ "audit-findings.json",
556
+ "audit-report.md",
557
+ "synthesis-narrative.json",
558
+ ],
559
+ progress_summary: `Synthesis narrative applied: ${record.theme_count} theme(s), ${record.top_risk_count} top risk(s).`,
482
560
  };
483
561
  }
484
562
  export function runExternalAnalyzerImportExecutor(bundle, externalResults) {
@@ -1,32 +1,13 @@
1
1
  import { existsSync } from "node:fs";
2
2
  import { spawnSync } from "node:child_process";
3
3
  import { delimiter, extname, isAbsolute, join } from "node:path";
4
- function isWindowsBatchCommand(path) {
5
- return process.platform === "win32" && /\.(cmd|bat)$/i.test(path);
6
- }
7
- function quoteForCmd(arg) {
8
- if (arg.length === 0)
9
- return '""';
10
- if (!/[\s"]/u.test(arg))
11
- return arg;
12
- return `"${arg.replace(/"/g, '""')}"`;
13
- }
4
+ import { resolveExecArgv } from "@audit-tools/shared";
14
5
  function toSpawnTuple(candidate) {
15
- if (!isWindowsBatchCommand(candidate.command)) {
16
- return {
17
- command: candidate.command,
18
- args: candidate.args,
19
- };
20
- }
21
- return {
22
- command: process.env.ComSpec ?? "cmd.exe",
23
- args: [
24
- "/d",
25
- "/s",
26
- "/c",
27
- [candidate.command, ...candidate.args].map(quoteForCmd).join(" "),
28
- ],
29
- };
6
+ // Shared resolver applies the single Windows `.cmd`/`.bat` wrapping impl.
7
+ // The candidate command is already PATH-resolved (absolute path or
8
+ // process.execPath), so package-manager shim mapping is a no-op here.
9
+ const resolved = resolveExecArgv([candidate.command, ...candidate.args]);
10
+ return { command: resolved[0], args: resolved.slice(1) };
30
11
  }
31
12
  function resolveFromPath(command) {
32
13
  if (command.trim().length === 0) {
@@ -6,6 +6,7 @@ const PRIORITY = [
6
6
  "auto_fixes_applied",
7
7
  "syntax_resolved",
8
8
  "structure_artifacts",
9
+ "graph_enrichment_current",
9
10
  "design_assessment_current",
10
11
  "design_review_completed",
11
12
  "planning_artifacts",
@@ -13,6 +14,7 @@ const PRIORITY = [
13
14
  "audit_results_ingested",
14
15
  "runtime_validation_current",
15
16
  "synthesis_current",
17
+ "synthesis_narrative_current",
16
18
  ];
17
19
  export function findObligation(obligations) {
18
20
  for (const id of PRIORITY) {
@@ -1,20 +1,53 @@
1
1
  import type { AuditTask } from "../types.js";
2
2
  import type { AuditPlanMetrics, ReviewPacket } from "../types/reviewPlanning.js";
3
- import type { GraphBundle } from "@audit-tools/shared";
3
+ import type { GraphBundle, GraphEdge } from "@audit-tools/shared";
4
4
  export declare const ESTIMATED_TOKENS_PER_LINE = 4;
5
5
  export declare const ESTIMATED_PACKET_PROMPT_TOKENS = 900;
6
- export declare function estimateTaskGroupTokens(tasks: AuditTask[]): number;
6
+ /**
7
+ * Build a path → size_bytes index from a repo manifest. Byte counts are
8
+ * recorded during intake, so this never reads files. Review packet token
9
+ * estimates are derived from these bytes (Phase 2) instead of counted lines.
10
+ */
11
+ export declare function sizeIndexFromManifest(repoManifest?: {
12
+ files: ReadonlyArray<{
13
+ path: string;
14
+ size_bytes: number;
15
+ }>;
16
+ }): Record<string, number>;
17
+ export declare function estimateTaskGroupTokens(tasks: AuditTask[], sizeIndex?: Record<string, number>, lineIndex?: Record<string, number>): number;
18
+ /**
19
+ * Fan-in / fan-out degree above which a node is treated as a hub. Exported so
20
+ * the Phase 3 delta-scope expansion skips the same hubs that packet planning
21
+ * skips, preventing scope blow-up through highly-connected modules.
22
+ */
23
+ export declare const HIGH_FAN_DEGREE_THRESHOLD = 12;
7
24
  export interface BuildReviewPacketOptions {
8
25
  graphBundle?: GraphBundle;
9
26
  lineIndex?: Record<string, number>;
27
+ /** Path → size_bytes (from the repo manifest); drives byte-based token sizing. */
28
+ sizeIndex?: Record<string, number>;
10
29
  maxTasksPerPacket?: number;
11
- targetPacketLines?: number;
30
+ /**
31
+ * Soft per-packet content-token budget. Defaults to
32
+ * DEFAULT_TARGET_PACKET_TOKENS. A packet is split when its estimated content
33
+ * tokens would exceed this budget.
34
+ */
35
+ targetPacketTokens?: number;
12
36
  /**
13
37
  * Available context budget in tokens (context_tokens − reserved_output_tokens).
14
- * When provided, targetPacketLines is capped to fit within this budget.
38
+ * When provided, targetPacketTokens is capped to fit within this budget so a
39
+ * packet's estimated tokens never exceed it.
15
40
  */
16
41
  maxContextTokens?: number;
17
42
  }
43
+ export declare function normalizeGraphPath(path: string): string;
44
+ export declare function collectGraphEdges(graphBundle?: GraphBundle): GraphEdge[];
45
+ export declare function graphEdgeConfidence(edge: GraphEdge): number;
46
+ export interface GraphDegreeIndex {
47
+ fanIn: Map<string, number>;
48
+ fanOut: Map<string, number>;
49
+ }
50
+ export declare function buildGraphDegreeIndex(edges: GraphEdge[]): GraphDegreeIndex;
18
51
  export declare function buildReviewPackets(tasks: AuditTask[], options?: BuildReviewPacketOptions): ReviewPacket[];
19
52
  export declare function orderTasksForPacketReview(tasks: AuditTask[], options?: BuildReviewPacketOptions): AuditTask[];
20
53
  export declare function buildAuditPlanMetrics(tasks: AuditTask[], options?: BuildReviewPacketOptions & {