auditor-lambda 0.3.6 → 0.3.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.js CHANGED
@@ -1504,6 +1504,9 @@ function buildDispatchModelHint(complexity) {
1504
1504
  if (complexity.tags.some((tag) => tag === "external_analyzer_signal" || tag.startsWith("external_tool:"))) {
1505
1505
  deepReasons.push("external_analyzer_signal");
1506
1506
  }
1507
+ if (complexity.tags.includes("lens_verification")) {
1508
+ deepReasons.push("lens_verification");
1509
+ }
1507
1510
  if (deepReasons.length > 0) {
1508
1511
  return { tier: "deep", reasons: deepReasons };
1509
1512
  }
@@ -1680,15 +1683,31 @@ async function cmdPrepareDispatch(argv) {
1680
1683
  : [];
1681
1684
  const taskSections = packetTasks.flatMap((task) => {
1682
1685
  const lensDef = lensDefs[task.lens];
1686
+ const inputLines = task.inputs
1687
+ ? Object.entries(task.inputs)
1688
+ .sort(([a], [b]) => a.localeCompare(b))
1689
+ .map(([key, value]) => `input.${key}: ${value}`)
1690
+ : [];
1691
+ const isLensVerification = task.tags?.includes("lens_verification") ?? false;
1683
1692
  return [
1684
1693
  `### ${task.task_id}`,
1685
1694
  `unit_id: ${task.unit_id}`,
1686
1695
  `pass_id: ${task.pass_id}`,
1687
1696
  `lens: ${task.lens}`,
1697
+ ...(task.tags?.length ? [`tags: ${task.tags.join(", ")}`] : []),
1698
+ ...inputLines,
1688
1699
  `rationale: ${task.rationale}`,
1689
1700
  "",
1690
1701
  `Lens guidance: ${lensDef?.description ?? task.lens}`,
1691
1702
  `Do NOT report: ${lensDef?.do_not_report ?? "N/A"}`,
1703
+ ...(isLensVerification
1704
+ ? [
1705
+ "",
1706
+ "Lens verification mode: review the prior result summary in the rationale and use only targeted source checks.",
1707
+ "Do not redo every packet and do not write direct findings for this task.",
1708
+ "Return findings: [] plus verification metadata. Include followup_tasks only for bounded, specific re-review packets.",
1709
+ ]
1710
+ : []),
1692
1711
  "",
1693
1712
  ];
1694
1713
  });
@@ -1735,6 +1754,13 @@ async function cmdPrepareDispatch(argv) {
1735
1754
  " file_coverage [{path, total_lines}] - one entry per assigned file; use the line counts listed above",
1736
1755
  " findings [] or array of finding objects",
1737
1756
  "",
1757
+ "Lens verification tasks:",
1758
+ " tasks tagged lens_verification must use findings: [] and include verification:",
1759
+ " {verified: boolean, needs_followup: boolean, concerns?: string[],",
1760
+ " coverage_concerns?: string[], confidence_concerns?: string[],",
1761
+ " followup_tasks?: AuditTask[]}.",
1762
+ " Follow-up AuditTask suggestions must stay bounded to files in this packet and use the same lens.",
1763
+ "",
1738
1764
  "Each finding object:",
1739
1765
  " id unique ID, e.g. \"COR-001\"",
1740
1766
  " title short title",
@@ -93,13 +93,13 @@ export async function writeDispatchBatchFiles(artifactsDir, runs, currentTasks)
93
93
  "",
94
94
  `This batch launched ${runs.length} deferred review run(s).`,
95
95
  "Each run keeps its own task.json, prompt.md, result.json, and status.json under .audit-artifacts/runs/<run_id>/.",
96
- "Use current-tasks.json for the combined task list, then inspect the per-run files below when you need exact prompts or result paths.",
96
+ "Use current-tasks.json for the combined task list. The per-run files below are operational references for launched workers; do not read per-run prompt or schema files unless debugging a failed dispatch.",
97
97
  "",
98
98
  "Runs:",
99
99
  ...runs.flatMap((run) => [
100
100
  `- ${run.run_id}`,
101
101
  ` task: ${run.task_path}`,
102
- ` prompt: ${run.prompt_path}`,
102
+ ` prompt (worker-owned; do not read during normal orchestration): ${run.prompt_path}`,
103
103
  ` result: ${run.result_path}`,
104
104
  ` status: ${run.status_path}`,
105
105
  ...(run.audit_results_path
@@ -32,6 +32,7 @@ function appendSelectiveDeepeningTasks(params) {
32
32
  lineIndex,
33
33
  runtimeValidationTasks: params.bundle.runtime_validation_tasks,
34
34
  runtimeValidationReport: params.runtimeValidationReport ?? params.bundle.runtime_validation_report,
35
+ externalAnalyzerResults: params.bundle.external_analyzer_results,
35
36
  });
36
37
  if (selectiveDeepeningTasks.length === 0) {
37
38
  return { bundle: params.bundle, taskCount: 0, artifacts: [] };
@@ -1,4 +1,5 @@
1
1
  import type { AuditResult, AuditTask } from "../types.js";
2
+ import type { ExternalAnalyzerResults } from "../types/externalAnalyzer.js";
2
3
  import type { RuntimeValidationReport, RuntimeValidationTaskManifest } from "../types/runtimeValidation.js";
3
4
  export interface BuildSelectiveDeepeningTaskOptions {
4
5
  existingTasks?: AuditTask[];
@@ -6,9 +7,12 @@ export interface BuildSelectiveDeepeningTaskOptions {
6
7
  lineIndex?: Record<string, number>;
7
8
  runtimeValidationTasks?: RuntimeValidationTaskManifest;
8
9
  runtimeValidationReport?: RuntimeValidationReport;
10
+ externalAnalyzerResults?: ExternalAnalyzerResults;
9
11
  maxTasks?: number;
10
12
  }
11
13
  export declare function buildSelectiveDeepeningTasks(options: BuildSelectiveDeepeningTaskOptions): AuditTask[];
12
14
  export declare const selectiveDeepeningTestUtils: {
13
15
  DEEPENING_TAG: string;
16
+ LENS_VERIFICATION_TAG: string;
17
+ LENS_VERIFICATION_FOLLOWUP_TAG: string;
14
18
  };
@@ -1,6 +1,16 @@
1
1
  import { createHash } from "node:crypto";
2
2
  const DEFAULT_MAX_DEEPENING_TASKS = 6;
3
3
  const DEEPENING_TAG = "selective_deepening";
4
+ const LENS_VERIFICATION_TAG = "lens_verification";
5
+ const LENS_VERIFICATION_FOLLOWUP_TAG = "lens_verification_followup";
6
+ const MAX_LENS_VERIFICATION_FILES = 12;
7
+ const MAX_LENS_VERIFICATION_RESULT_SUMMARIES = 12;
8
+ const MAX_VERIFICATION_FOLLOWUP_TASKS_PER_RESULT = 4;
9
+ const IMPORTANT_LENS_VERIFICATION_LENSES = new Set([
10
+ "security",
11
+ "data_integrity",
12
+ "reliability",
13
+ ]);
4
14
  const SEVERITY_RANK = {
5
15
  critical: 5,
6
16
  high: 4,
@@ -27,6 +37,9 @@ function priorityRank(priority) {
27
37
  function isDeepeningTask(task) {
28
38
  return task?.tags?.includes(DEEPENING_TAG) ?? false;
29
39
  }
40
+ function isLensVerificationTask(task) {
41
+ return task?.tags?.includes(LENS_VERIFICATION_TAG) ?? false;
42
+ }
30
43
  function sanitizeSegment(value) {
31
44
  const sanitized = value
32
45
  .replace(/[^a-zA-Z0-9_-]+/g, "-")
@@ -85,6 +98,29 @@ function lineCountFromSources(path, tasks, results, lineIndex) {
85
98
  }
86
99
  return lineIndex?.[path] ?? 0;
87
100
  }
101
+ function formatList(values, maxItems) {
102
+ const visible = values.slice(0, maxItems);
103
+ const suffix = values.length > maxItems ? `, ... (+${values.length - maxItems} more)` : "";
104
+ return `${visible.join(", ")}${suffix}`;
105
+ }
106
+ function priorityLabel(priority) {
107
+ return priority ?? "low";
108
+ }
109
+ function getExternalAnalyzerPaths(externalAnalyzerResults) {
110
+ return new Set((externalAnalyzerResults?.results ?? [])
111
+ .map((result) => result && typeof result.path === "string" && result.path.length > 0
112
+ ? result.path
113
+ : null)
114
+ .filter((path) => path !== null));
115
+ }
116
+ function isRecord(value) {
117
+ return value !== null && typeof value === "object" && !Array.isArray(value);
118
+ }
119
+ function normalizedSuggestedPriority(value, fallback = "medium") {
120
+ return value === "high" || value === "medium" || value === "low"
121
+ ? value
122
+ : fallback;
123
+ }
88
124
  function buildFindingFollowupTask(params) {
89
125
  const paths = pathsForFinding(params.finding, params.result, params.task);
90
126
  const triggerLabel = params.triggers.join("+");
@@ -261,6 +297,309 @@ function buildRuntimeValidationFollowupTask(params) {
261
297
  status: "pending",
262
298
  };
263
299
  }
300
+ function sourceTaskIds(sources) {
301
+ return uniqueSorted(sources.map((source) => source.result.task_id));
302
+ }
303
+ function resultFiles(source) {
304
+ return uniqueSorted(source.task?.file_paths && source.task.file_paths.length > 0
305
+ ? source.task.file_paths
306
+ : source.result.file_coverage.map((coverage) => coverage.path));
307
+ }
308
+ function lensVerificationTriggers(params) {
309
+ const filePaths = uniqueSorted(params.sources.flatMap(resultFiles));
310
+ const findingPaths = new Set(params.sources.flatMap((source) => source.result.findings.flatMap((finding) => finding.affected_files.map((file) => file.path))));
311
+ const externalPathsInScope = filePaths.filter((path) => params.externalAnalyzerPaths.has(path));
312
+ const unresolvedExternalPaths = externalPathsInScope.filter((path) => !findingPaths.has(path));
313
+ const cleanResults = params.sources.filter((source) => source.result.findings.length === 0 &&
314
+ source.result.requires_followup !== false);
315
+ const highRiskCleanResults = params.sources.filter((source) => isHighRiskCleanResult(source.result, source.task));
316
+ const totalLines = filePaths.reduce((sum, path) => {
317
+ const owner = params.sources.find((source) => resultFiles(source).includes(path));
318
+ return (sum +
319
+ (owner
320
+ ? lineCountForPath(path, owner.task, owner.result)
321
+ : 0));
322
+ }, 0);
323
+ const triggers = [];
324
+ if (params.sources.some((source) => source.task?.priority === "high")) {
325
+ triggers.push("high_priority_lens");
326
+ }
327
+ if (params.sources.some((source) => source.task?.tags?.some((tag) => tag === "critical_flow" || tag.startsWith("critical_flow:")))) {
328
+ triggers.push("critical_flow");
329
+ }
330
+ if (params.sources.some((source) => source.task?.tags?.some((tag) => tag === "external_analyzer_signal" ||
331
+ tag.startsWith("external_tool:"))) ||
332
+ externalPathsInScope.length > 0) {
333
+ triggers.push("external_analyzer_signal");
334
+ }
335
+ if (unresolvedExternalPaths.length > 0) {
336
+ triggers.push("unresolved_external_signal");
337
+ }
338
+ if (params.sources.length >= 3 ||
339
+ filePaths.length >= 4 ||
340
+ totalLines >= 2000) {
341
+ triggers.push("large_lens_surface");
342
+ }
343
+ if (cleanResults.length >= 2 && cleanResults.length >= params.sources.length / 2) {
344
+ triggers.push("many_no_finding_results");
345
+ }
346
+ if (highRiskCleanResults.length > 0) {
347
+ triggers.push("high_risk_clean_result");
348
+ }
349
+ if (params.sources.some((source) => source.task?.tags?.some((tag) => tag === "large_file"))) {
350
+ triggers.push("large_file_reviewed");
351
+ }
352
+ return uniqueSorted(triggers);
353
+ }
354
+ function hasPendingBaseTaskForLens(lens, tasks, completedResultIds) {
355
+ return tasks.some((task) => task.lens === lens &&
356
+ !isDeepeningTask(task) &&
357
+ !completedResultIds.has(task.task_id) &&
358
+ task.status !== "complete");
359
+ }
360
+ function shouldBuildLensVerificationTask(params) {
361
+ if (!IMPORTANT_LENS_VERIFICATION_LENSES.has(params.lens)) {
362
+ return false;
363
+ }
364
+ if (params.sources.length === 0 || params.triggers.length === 0) {
365
+ return false;
366
+ }
367
+ const explicitlyClosedCleanScope = params.sources.every((source) => source.result.findings.length === 0 &&
368
+ source.result.requires_followup === false);
369
+ if (explicitlyClosedCleanScope &&
370
+ !params.triggers.some((trigger) => ["external_analyzer_signal", "unresolved_external_signal"].includes(trigger))) {
371
+ return false;
372
+ }
373
+ if (hasPendingBaseTaskForLens(params.lens, params.existingTasks, params.completedResultIds)) {
374
+ return false;
375
+ }
376
+ const enoughSurface = params.sources.length >= 2 ||
377
+ params.triggers.some((trigger) => [
378
+ "critical_flow",
379
+ "external_analyzer_signal",
380
+ "unresolved_external_signal",
381
+ "large_lens_surface",
382
+ ].includes(trigger));
383
+ if (!enoughSurface) {
384
+ return false;
385
+ }
386
+ const sourceSignature = sourceTaskIds(params.sources);
387
+ const candidateId = taskIdFor("steward", [params.lens, ...sourceSignature]);
388
+ return !params.existingTasks.some((task) => task.task_id === candidateId);
389
+ }
390
+ function selectLensVerificationFiles(sources, externalAnalyzerPaths) {
391
+ const scores = new Map();
392
+ function add(path, score, lines) {
393
+ const current = scores.get(path) ?? { score: 0, lines };
394
+ current.score += score;
395
+ current.lines = Math.max(current.lines, lines);
396
+ scores.set(path, current);
397
+ }
398
+ for (const source of sources) {
399
+ const priorityScore = priorityRank(source.task?.priority);
400
+ const highRiskClean = isHighRiskCleanResult(source.result, source.task);
401
+ for (const path of resultFiles(source)) {
402
+ add(path, priorityScore, lineCountForPath(path, source.task, source.result));
403
+ if (source.task?.tags?.includes("critical_flow"))
404
+ add(path, 6, 0);
405
+ if (source.task?.tags?.includes("external_analyzer_signal"))
406
+ add(path, 6, 0);
407
+ if (source.task?.tags?.includes("large_file"))
408
+ add(path, 4, 0);
409
+ if (highRiskClean)
410
+ add(path, 5, 0);
411
+ }
412
+ for (const finding of source.result.findings) {
413
+ for (const file of finding.affected_files) {
414
+ add(file.path, SEVERITY_RANK[finding.severity], 0);
415
+ }
416
+ }
417
+ }
418
+ for (const path of externalAnalyzerPaths) {
419
+ if (scores.has(path)) {
420
+ add(path, 8, 0);
421
+ }
422
+ }
423
+ return [...scores.entries()]
424
+ .sort((a, b) => {
425
+ const scoreDelta = b[1].score - a[1].score;
426
+ if (scoreDelta !== 0)
427
+ return scoreDelta;
428
+ const lineDelta = b[1].lines - a[1].lines;
429
+ if (lineDelta !== 0)
430
+ return lineDelta;
431
+ return a[0].localeCompare(b[0]);
432
+ })
433
+ .slice(0, MAX_LENS_VERIFICATION_FILES)
434
+ .map(([path]) => path);
435
+ }
436
+ function summarizeLensVerificationSource(source) {
437
+ const findings = source.result.findings.length === 0
438
+ ? "findings=none"
439
+ : `findings=${source.result.findings
440
+ .slice(0, 3)
441
+ .map((finding) => `${finding.id} ${finding.severity}/${finding.confidence} ${finding.category}: ${finding.title}`)
442
+ .join("; ")}${source.result.findings.length > 3 ? "; ..." : ""}`;
443
+ const tags = source.task?.tags?.length
444
+ ? ` tags=${source.task.tags.join(",")}`
445
+ : "";
446
+ return (`- ${source.result.task_id} priority=${priorityLabel(source.task?.priority)}` +
447
+ `${tags} files=${formatList(resultFiles(source), 4)} ${findings}` +
448
+ (source.result.requires_followup === true ? " requires_followup=true" : ""));
449
+ }
450
+ function buildLensVerificationTask(params) {
451
+ const sourceIds = sourceTaskIds(params.sources);
452
+ const selectedPaths = selectLensVerificationFiles(params.sources, params.externalAnalyzerPaths);
453
+ const allPaths = uniqueSorted(params.sources.flatMap(resultFiles));
454
+ const omittedPathCount = Math.max(0, allPaths.length - selectedPaths.length);
455
+ const externalPathsInScope = allPaths.filter((path) => params.externalAnalyzerPaths.has(path));
456
+ const summaries = params.sources
457
+ .sort((a, b) => a.result.task_id.localeCompare(b.result.task_id))
458
+ .slice(0, MAX_LENS_VERIFICATION_RESULT_SUMMARIES)
459
+ .map(summarizeLensVerificationSource);
460
+ return {
461
+ task_id: taskIdFor("steward", [params.lens, ...sourceIds]),
462
+ unit_id: `lens-steward:${params.lens}`,
463
+ pass_id: `lens-steward:${params.lens}`,
464
+ lens: params.lens,
465
+ file_paths: selectedPaths,
466
+ file_line_counts: Object.fromEntries(selectedPaths.map((path) => [
467
+ path,
468
+ lineCountFromSources(path, params.sources.map((source) => source.task).filter((task) => task !== undefined), params.sources.map((source) => source.result), params.lineIndex),
469
+ ])),
470
+ inputs: {
471
+ source_task_ids: sourceIds.join(","),
472
+ trigger_summary: params.triggers.join(","),
473
+ },
474
+ rationale: `Lens steward verification for ${params.lens} after ${params.sources.length} completed base result(s) across ${allPaths.length} file(s). ` +
475
+ `Triggers: ${params.triggers.join(", ")}. ` +
476
+ "Review whether high-risk packets are suspiciously clean, severity/confidence levels are consistent, external analyzer signals were resolved rather than hand-waved, cross-packet issues are visible, no-finding conclusions are believable, and related-file findings contradict each other. " +
477
+ "Do not write direct findings from this verification task; return findings: [] plus verification metadata with bounded follow-up AuditTask suggestions when needed.\n" +
478
+ `Selected verification files: ${formatList(selectedPaths, 8)}${omittedPathCount > 0 ? `; omitted ${omittedPathCount} lower-priority file(s) from direct source checks` : ""}.\n` +
479
+ (externalPathsInScope.length > 0
480
+ ? `External analyzer paths in scope: ${formatList(externalPathsInScope, 8)}.\n`
481
+ : "") +
482
+ "Source result summary:\n" +
483
+ summaries.join("\n") +
484
+ (params.sources.length > MAX_LENS_VERIFICATION_RESULT_SUMMARIES
485
+ ? `\n- ... (+${params.sources.length - MAX_LENS_VERIFICATION_RESULT_SUMMARIES} more result summaries omitted)`
486
+ : ""),
487
+ priority: "high",
488
+ tags: [
489
+ DEEPENING_TAG,
490
+ LENS_VERIFICATION_TAG,
491
+ `lens:${params.lens}`,
492
+ ...params.triggers.map((trigger) => `trigger:${trigger}`),
493
+ ],
494
+ status: "pending",
495
+ };
496
+ }
497
+ function buildLensVerificationTasks(params) {
498
+ const taskById = new Map(params.existingTasks.map((task) => [task.task_id, task]));
499
+ const completedResultIds = new Set(params.results.map((result) => result.task_id));
500
+ const externalAnalyzerPaths = getExternalAnalyzerPaths(params.externalAnalyzerResults);
501
+ const tasks = [];
502
+ for (const lens of [...IMPORTANT_LENS_VERIFICATION_LENSES].sort((a, b) => a.localeCompare(b))) {
503
+ const sources = params.results
504
+ .map((result) => ({ result, task: taskById.get(result.task_id) }))
505
+ .filter((source) => source.result.lens === lens &&
506
+ !isDeepeningTask(source.task) &&
507
+ !isLensVerificationTask(source.task));
508
+ const triggers = lensVerificationTriggers({
509
+ lens,
510
+ sources,
511
+ externalAnalyzerPaths,
512
+ });
513
+ if (!shouldBuildLensVerificationTask({
514
+ lens,
515
+ sources,
516
+ triggers,
517
+ existingTasks: params.existingTasks,
518
+ completedResultIds,
519
+ })) {
520
+ continue;
521
+ }
522
+ tasks.push(buildLensVerificationTask({
523
+ lens,
524
+ sources,
525
+ triggers,
526
+ externalAnalyzerPaths,
527
+ lineIndex: params.lineIndex,
528
+ }));
529
+ }
530
+ return tasks;
531
+ }
532
+ function buildVerificationFollowupTasks(params) {
533
+ if (!params.result.verification?.needs_followup ||
534
+ !Array.isArray(params.result.verification.followup_tasks) ||
535
+ !isLensVerificationTask(params.task)) {
536
+ return [];
537
+ }
538
+ const coverageByPath = new Map(params.result.file_coverage.map((coverage) => [
539
+ coverage.path,
540
+ coverage.total_lines,
541
+ ]));
542
+ const concerns = [
543
+ ...(params.result.verification.concerns ?? []),
544
+ ...(params.result.verification.coverage_concerns ?? []),
545
+ ...(params.result.verification.confidence_concerns ?? []),
546
+ ];
547
+ const tasks = [];
548
+ for (let index = 0; index < params.result.verification.followup_tasks.length; index++) {
549
+ if (tasks.length >= MAX_VERIFICATION_FOLLOWUP_TASKS_PER_RESULT) {
550
+ break;
551
+ }
552
+ const suggestion = params.result.verification.followup_tasks[index];
553
+ if (!isRecord(suggestion)) {
554
+ continue;
555
+ }
556
+ if (suggestion.lens !== params.result.lens) {
557
+ continue;
558
+ }
559
+ const suggestedPaths = Array.isArray(suggestion.file_paths)
560
+ ? suggestion.file_paths.filter((path) => typeof path === "string" && coverageByPath.has(path))
561
+ : [];
562
+ const paths = uniqueSorted(suggestedPaths);
563
+ if (paths.length === 0) {
564
+ continue;
565
+ }
566
+ const suggestedRationale = typeof suggestion.rationale === "string" && suggestion.rationale.trim().length > 0
567
+ ? suggestion.rationale.trim()
568
+ : concerns[0] ?? "Lens steward requested bounded follow-up.";
569
+ const suggestedId = typeof suggestion.task_id === "string" && suggestion.task_id.trim().length > 0
570
+ ? suggestion.task_id
571
+ : `suggestion-${index + 1}`;
572
+ tasks.push({
573
+ task_id: taskIdFor("steward-followup", [
574
+ params.result.task_id,
575
+ String(index),
576
+ suggestedId,
577
+ ...paths,
578
+ suggestedRationale,
579
+ ]),
580
+ unit_id: typeof suggestion.unit_id === "string" && suggestion.unit_id.trim().length > 0
581
+ ? suggestion.unit_id
582
+ : params.result.unit_id,
583
+ pass_id: `deepening:${params.result.pass_id}`,
584
+ lens: params.result.lens,
585
+ file_paths: paths,
586
+ file_line_counts: Object.fromEntries(paths.map((path) => [
587
+ path,
588
+ coverageByPath.get(path) ?? params.lineIndex?.[path] ?? 0,
589
+ ])),
590
+ rationale: `Lens steward follow-up from ${params.result.task_id}. ${suggestedRationale}`,
591
+ priority: normalizedSuggestedPriority(suggestion.priority, "medium"),
592
+ tags: [
593
+ DEEPENING_TAG,
594
+ LENS_VERIFICATION_FOLLOWUP_TAG,
595
+ "trigger:lens_verification",
596
+ `source_task:${sanitizeSegment(params.result.task_id)}`,
597
+ ],
598
+ status: "pending",
599
+ });
600
+ }
601
+ return tasks;
602
+ }
264
603
  function findingContexts(results, taskById) {
265
604
  const contexts = [];
266
605
  for (const result of results) {
@@ -345,6 +684,16 @@ export function buildSelectiveDeepeningTasks(options) {
345
684
  lineIndex: options.lineIndex,
346
685
  }));
347
686
  }
687
+ for (const result of options.results) {
688
+ const task = taskById.get(result.task_id);
689
+ for (const followupTask of buildVerificationFollowupTasks({
690
+ result,
691
+ task,
692
+ lineIndex: options.lineIndex,
693
+ })) {
694
+ pushIfNew(followupTask);
695
+ }
696
+ }
348
697
  const runtimeTaskById = new Map((options.runtimeValidationTasks?.tasks ?? []).map((task) => [
349
698
  task.id,
350
699
  task,
@@ -369,6 +718,14 @@ export function buildSelectiveDeepeningTasks(options) {
369
718
  lineIndex: options.lineIndex,
370
719
  }));
371
720
  }
721
+ for (const task of buildLensVerificationTasks({
722
+ existingTasks,
723
+ results: options.results,
724
+ lineIndex: options.lineIndex,
725
+ externalAnalyzerResults: options.externalAnalyzerResults,
726
+ })) {
727
+ pushIfNew(task);
728
+ }
372
729
  const cleanResults = options.results
373
730
  .map((result) => ({ result, task: taskById.get(result.task_id) }))
374
731
  .filter(({ result, task }) => isHighRiskCleanResult(result, task))
@@ -389,4 +746,6 @@ export function buildSelectiveDeepeningTasks(options) {
389
746
  }
390
747
  export const selectiveDeepeningTestUtils = {
391
748
  DEEPENING_TAG,
749
+ LENS_VERIFICATION_TAG,
750
+ LENS_VERIFICATION_FOLLOWUP_TAG,
392
751
  };
@@ -7,13 +7,9 @@ export function renderWorkerPrompt(task) {
7
7
  if (task.preferred_executor === "agent" && task.audit_results_path) {
8
8
  const tasksPath = task.pending_audit_tasks_path ??
9
9
  `${task.artifacts_dir}/audit_tasks.json`;
10
- const resultsSchemaPath = `${task.artifacts_dir}/dispatch/audit-results.schema.json`;
11
- const singleResultSchemaPath = `${task.artifacts_dir}/dispatch/audit-result.schema.json`;
12
10
  const lines = [
13
11
  `Audit run: ${task.run_id}`,
14
12
  `Read: ${tasksPath}`,
15
- `Array schema: ${resultsSchemaPath}`,
16
- `Single-result schema: ${singleResultSchemaPath}`,
17
13
  "Scope: review only the tasks listed in the Read file. Do not add tasks,",
18
14
  "edit source files, remediate findings, run unrelated audits, or write result_path.",
19
15
  "For each listed task: read the assigned file_paths under the specified lens,",
@@ -25,6 +21,9 @@ export function renderWorkerPrompt(task) {
25
21
  "Each finding: id, title, category, severity, confidence, lens, summary,",
26
22
  " affected_files [{path, line_start, line_end, symbol}] (objects, not strings; min 1 entry),",
27
23
  " evidence [strings] (min 1 entry).",
24
+ "For tasks tagged lens_verification: do not write direct findings; use findings: []",
25
+ " and include verification {verified, needs_followup, concerns, coverage_concerns,",
26
+ " confidence_concerns, followup_tasks}.",
28
27
  "Constraint: line_end must not exceed total_lines for that file.",
29
28
  `Write only the JSON array of AuditResult objects to: ${task.audit_results_path}`,
30
29
  ];
package/dist/types.d.ts CHANGED
@@ -88,6 +88,14 @@ export interface Finding {
88
88
  systemic?: boolean;
89
89
  related_findings?: string[];
90
90
  }
91
+ export interface AuditVerification {
92
+ verified: boolean;
93
+ needs_followup: boolean;
94
+ concerns?: string[];
95
+ coverage_concerns?: string[];
96
+ confidence_concerns?: string[];
97
+ followup_tasks?: AuditTask[];
98
+ }
91
99
  export interface AuditResult {
92
100
  task_id: string;
93
101
  unit_id: string;
@@ -102,4 +110,5 @@ export interface AuditResult {
102
110
  notes?: string[];
103
111
  requires_followup?: boolean;
104
112
  followup_tasks?: string[];
113
+ verification?: AuditVerification;
105
114
  }
@@ -10,6 +10,8 @@ const REQUIRED_FINDING_FIELDS = [
10
10
  ];
11
11
  const VALID_SEVERITIES = new Set(["critical", "high", "medium", "low", "info"]);
12
12
  const VALID_CONFIDENCES = new Set(["high", "medium", "low"]);
13
+ const VALID_PRIORITIES = new Set(["high", "medium", "low"]);
14
+ const LENS_VERIFICATION_TAG = "lens_verification";
13
15
  const VALID_LENSES = new Set([
14
16
  "correctness",
15
17
  "architecture",
@@ -207,6 +209,161 @@ function validateFinding(finding, label, taskId, resultIndex) {
207
209
  }
208
210
  return issues;
209
211
  }
212
+ function validateOptionalStringArray(value, label, taskId, resultIndex, issues) {
213
+ if (value === undefined) {
214
+ return;
215
+ }
216
+ if (!Array.isArray(value)) {
217
+ pushIssue(issues, {
218
+ result_index: resultIndex,
219
+ task_id: taskId,
220
+ field: label,
221
+ message: `${label} must be an array of strings, got ${describeValue(value)}.`,
222
+ });
223
+ return;
224
+ }
225
+ for (let index = 0; index < value.length; index++) {
226
+ if (typeof value[index] !== "string") {
227
+ pushIssue(issues, {
228
+ result_index: resultIndex,
229
+ task_id: taskId,
230
+ field: `${label}[${index}]`,
231
+ message: `${label}[${index}] must be a string, got ${describeValue(value[index])}.`,
232
+ });
233
+ }
234
+ }
235
+ }
236
+ function validateVerificationFollowupTask(task, label, taskId, resultIndex, expectedLens, allowedPaths, issues) {
237
+ if (!isRecord(task)) {
238
+ pushIssue(issues, {
239
+ result_index: resultIndex,
240
+ task_id: taskId,
241
+ field: label,
242
+ message: `${label} must be an AuditTask object, got ${describeValue(task)}.`,
243
+ });
244
+ return;
245
+ }
246
+ for (const field of ["task_id", "unit_id", "pass_id", "lens", "rationale"]) {
247
+ validateRequiredStringField(task[field], `${label}.${field}`, taskId, resultIndex, issues);
248
+ }
249
+ if (typeof task.lens === "string" && !VALID_LENSES.has(task.lens)) {
250
+ pushIssue(issues, {
251
+ result_index: resultIndex,
252
+ task_id: taskId,
253
+ field: `${label}.lens`,
254
+ message: `Invalid lens '${task.lens}'. Must be one of: ${[...VALID_LENSES].join(", ")}.`,
255
+ });
256
+ }
257
+ if (typeof expectedLens === "string" &&
258
+ typeof task.lens === "string" &&
259
+ task.lens !== expectedLens) {
260
+ pushIssue(issues, {
261
+ result_index: resultIndex,
262
+ task_id: taskId,
263
+ field: `${label}.lens`,
264
+ message: `${label}.lens must match the lens verification task ` +
265
+ `(expected '${expectedLens}', got '${task.lens}').`,
266
+ });
267
+ }
268
+ if (task.priority !== undefined &&
269
+ (typeof task.priority !== "string" || !VALID_PRIORITIES.has(task.priority))) {
270
+ pushIssue(issues, {
271
+ result_index: resultIndex,
272
+ task_id: taskId,
273
+ field: `${label}.priority`,
274
+ message: `${label}.priority must be one of: ${[...VALID_PRIORITIES].join(", ")}.`,
275
+ });
276
+ }
277
+ if (!Array.isArray(task.file_paths) || task.file_paths.length === 0) {
278
+ pushIssue(issues, {
279
+ result_index: resultIndex,
280
+ task_id: taskId,
281
+ field: `${label}.file_paths`,
282
+ message: `${label}.file_paths must be a non-empty array.`,
283
+ });
284
+ }
285
+ else {
286
+ for (let index = 0; index < task.file_paths.length; index++) {
287
+ const path = task.file_paths[index];
288
+ if (!isNonEmptyString(path)) {
289
+ pushIssue(issues, {
290
+ result_index: resultIndex,
291
+ task_id: taskId,
292
+ field: `${label}.file_paths[${index}]`,
293
+ message: `${label}.file_paths[${index}] must be a non-empty string.`,
294
+ });
295
+ continue;
296
+ }
297
+ if (!allowedPaths.has(path)) {
298
+ pushIssue(issues, {
299
+ result_index: resultIndex,
300
+ task_id: taskId,
301
+ field: `${label}.file_paths[${index}]`,
302
+ message: `${label}.file_paths[${index}] references '${path}', which is outside the verification task's file_coverage.`,
303
+ });
304
+ }
305
+ }
306
+ }
307
+ validateOptionalStringArray(task.tags, `${label}.tags`, taskId, resultIndex, issues);
308
+ }
309
+ function validateVerification(value, result, task, coverage, taskId, resultIndex, issues) {
310
+ if (value === undefined) {
311
+ return;
312
+ }
313
+ if (!isRecord(value)) {
314
+ pushIssue(issues, {
315
+ result_index: resultIndex,
316
+ task_id: taskId,
317
+ field: "verification",
318
+ message: `verification must be an object, got ${describeValue(value)}.`,
319
+ });
320
+ return;
321
+ }
322
+ if (typeof value.verified !== "boolean") {
323
+ pushIssue(issues, {
324
+ result_index: resultIndex,
325
+ task_id: taskId,
326
+ field: "verification.verified",
327
+ message: `verification.verified must be a boolean, got ${describeValue(value.verified)}.`,
328
+ });
329
+ }
330
+ if (typeof value.needs_followup !== "boolean") {
331
+ pushIssue(issues, {
332
+ result_index: resultIndex,
333
+ task_id: taskId,
334
+ field: "verification.needs_followup",
335
+ message: `verification.needs_followup must be a boolean, got ${describeValue(value.needs_followup)}.`,
336
+ });
337
+ }
338
+ if (task && !task.tags?.includes(LENS_VERIFICATION_TAG)) {
339
+ pushIssue(issues, {
340
+ result_index: resultIndex,
341
+ task_id: taskId,
342
+ field: "verification",
343
+ message: "verification is intended only for tasks tagged lens_verification.",
344
+ severity: "warning",
345
+ });
346
+ }
347
+ validateOptionalStringArray(value.concerns, "verification.concerns", taskId, resultIndex, issues);
348
+ validateOptionalStringArray(value.coverage_concerns, "verification.coverage_concerns", taskId, resultIndex, issues);
349
+ validateOptionalStringArray(value.confidence_concerns, "verification.confidence_concerns", taskId, resultIndex, issues);
350
+ if (value.followup_tasks === undefined) {
351
+ return;
352
+ }
353
+ if (!Array.isArray(value.followup_tasks)) {
354
+ pushIssue(issues, {
355
+ result_index: resultIndex,
356
+ task_id: taskId,
357
+ field: "verification.followup_tasks",
358
+ message: `verification.followup_tasks must be an array, got ${describeValue(value.followup_tasks)}.`,
359
+ });
360
+ return;
361
+ }
362
+ const allowedPaths = new Set(coverage.map((entry) => entry.path));
363
+ for (let index = 0; index < value.followup_tasks.length; index++) {
364
+ validateVerificationFollowupTask(value.followup_tasks[index], `verification.followup_tasks[${index}]`, taskId, resultIndex, result.lens, allowedPaths, issues);
365
+ }
366
+ }
210
367
  function coversAffectedSpan(coverage, path, start, end) {
211
368
  return coverage.some((entry) => entry.path === path &&
212
369
  start > 0 &&
@@ -438,6 +595,7 @@ export function validateAuditResults(results, tasks, options = {}) {
438
595
  }
439
596
  }
440
597
  }
598
+ validateVerification(result.verification, result, task, normalizedFileCoverage, taskId, i, issues);
441
599
  }
442
600
  return issues;
443
601
  }
package/docs/contract.md CHANGED
@@ -31,6 +31,9 @@ Important rules:
31
31
  - each finding lens must match the assigned task lens.
32
32
  - `findings[].affected_files` must be objects, not strings.
33
33
  - `findings[].evidence` must be an array of plain strings.
34
+ - lens steward tasks are tagged `lens_verification`; they must emit
35
+ `findings: []` plus `verification` metadata. Suggested `verification.followup_tasks`
36
+ are treated as bounded follow-up requests, not direct findings.
34
37
 
35
38
  Use `audit-code validate-results --results <file>` before ingestion to validate
36
39
  results against the active task manifest.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "auditor-lambda",
3
- "version": "0.3.6",
3
+ "version": "0.3.7",
4
4
  "private": false,
5
5
  "description": "Portable hybrid code-auditing framework for arbitrary repositories.",
6
6
  "type": "module",
@@ -14,6 +14,9 @@
14
14
  "$defs": {
15
15
  "Finding": {
16
16
  "$ref": "finding.schema.json"
17
+ },
18
+ "AuditTask": {
19
+ "$ref": "audit_task.schema.json"
17
20
  }
18
21
  },
19
22
  "properties": {
@@ -50,6 +53,31 @@
50
53
  "followup_tasks": {
51
54
  "type": "array",
52
55
  "items": { "type": "string" }
56
+ },
57
+ "verification": {
58
+ "type": "object",
59
+ "required": ["verified", "needs_followup"],
60
+ "properties": {
61
+ "verified": { "type": "boolean" },
62
+ "needs_followup": { "type": "boolean" },
63
+ "concerns": {
64
+ "type": "array",
65
+ "items": { "type": "string" }
66
+ },
67
+ "coverage_concerns": {
68
+ "type": "array",
69
+ "items": { "type": "string" }
70
+ },
71
+ "confidence_concerns": {
72
+ "type": "array",
73
+ "items": { "type": "string" }
74
+ },
75
+ "followup_tasks": {
76
+ "type": "array",
77
+ "items": { "$ref": "#/$defs/AuditTask" }
78
+ }
79
+ },
80
+ "additionalProperties": false
53
81
  }
54
82
  },
55
83
  "additionalProperties": false