auditor-lambda 0.12.0 → 0.12.1

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.
@@ -3,6 +3,7 @@ import { existsSync } from "node:fs";
3
3
  import { readFile, writeFile, mkdir } from "node:fs/promises";
4
4
  import { getRootDir } from "./args.js";
5
5
  import { normalizeExistingFindingsReport, renderAuditReportMarkdown, } from "../reporting/synthesis.js";
6
+ import { AGENT_FEEDBACK_FILENAME, parseReflectionsNdjson, readOptionalTextFile, } from "@audit-tools/shared";
6
7
  const AUDIT_TOOLS_DIR = ".audit-tools";
7
8
  const FINDINGS_FILENAME = "audit-findings.json";
8
9
  const REPORT_FILENAME = "audit-report.md";
@@ -33,7 +34,13 @@ export async function cmdResynthesize(argv) {
33
34
  return;
34
35
  }
35
36
  const normalized = normalizeExistingFindingsReport(report);
36
- const markdown = renderAuditReportMarkdown(normalized);
37
+ // Best-effort: if the working artifacts dir (and its worker-appended
38
+ // feedback) still exists — e.g. an interrupted run being compiled by hand —
39
+ // carry the Process Feedback section into the re-rendered report.
40
+ const feedbackText = await readOptionalTextFile(join(auditToolsDir, "audit", AGENT_FEEDBACK_FILENAME));
41
+ const markdown = renderAuditReportMarkdown(normalized, {
42
+ reflections: feedbackText ? parseReflectionsNdjson(feedbackText) : undefined,
43
+ });
37
44
  await mkdir(auditToolsDir, { recursive: true });
38
45
  const outputFindingsPath = join(auditToolsDir, FINDINGS_FILENAME);
39
46
  const outputReportPath = join(auditToolsDir, REPORT_FILENAME);
@@ -13,6 +13,7 @@ import type { AnalyzerCapabilityRecord } from "../types/analyzerCapability.js";
13
13
  import type { AuditScopeManifest } from "../types/auditScope.js";
14
14
  import type { ToolingManifest } from "../types/toolingManifest.js";
15
15
  import type { ActiveDispatchState } from "../types/activeDispatch.js";
16
+ import { type AgentReflection } from "@audit-tools/shared";
16
17
  type ArtifactPayloadMap = {
17
18
  repo_manifest: RepoManifest;
18
19
  file_disposition: FileDisposition;
@@ -52,9 +53,17 @@ type ArtifactPayloadMap = {
52
53
  * the artifacts root rather than as a standard pruned artifact, and carries the
53
54
  * in-flight dispatch phase plus any budget-deferred task ids the completion
54
55
  * obligation must exclude.
56
+ *
57
+ * `agent_reflections` is the parsed view of the worker-APPENDED
58
+ * `agent-feedback.jsonl` (opt-in meta-audit feedback). Workers own that file;
59
+ * the orchestrator only ever reads it, so it is deliberately NOT an
60
+ * ARTIFACT_DEFINITIONS entry — writeCoreArtifacts must never rewrite it (a
61
+ * round-trip would drop lines a worker appended after load, and prune would
62
+ * delete a file the orchestrator does not own).
55
63
  */
56
64
  export type ArtifactBundle = Partial<ArtifactPayloadMap> & {
57
65
  active_dispatch?: ActiveDispatchState;
66
+ agent_reflections?: AgentReflection[];
58
67
  };
59
68
  export type ArtifactBundleKey = keyof ArtifactPayloadMap;
60
69
  type ArtifactPhase = "intake" | "analysis" | "execution" | "reporting" | "supervisor";
@@ -1,6 +1,6 @@
1
1
  import { cp, rm, unlink } from "node:fs/promises";
2
2
  import { dirname, join } from "node:path";
3
- import { isFileMissingError, readOptionalJsonFile, readOptionalNdjsonFile, readOptionalTextFile, writeJsonFile, writeNdjsonFile, writeTextFile, } from "@audit-tools/shared";
3
+ import { AGENT_FEEDBACK_FILENAME, isFileMissingError, parseReflectionsNdjson, readOptionalJsonFile, readOptionalNdjsonFile, readOptionalTextFile, writeJsonFile, writeNdjsonFile, writeTextFile, } from "@audit-tools/shared";
4
4
  import { buildToolingManifest } from "./toolingManifest.js";
5
5
  // Canonical filename for the rendered findings report. Single source of truth
6
6
  // for path construction. The dependency table below still lists it as plain
@@ -64,6 +64,11 @@ export const ARTIFACT_DEFINITIONS = {
64
64
  const ARTIFACT_ENTRIES = Object.entries(ARTIFACT_DEFINITIONS);
65
65
  export const ARTIFACT_FILE_TO_BUNDLE_KEY = Object.fromEntries(ARTIFACT_ENTRIES.map(([key, definition]) => [definition.fileName, key]));
66
66
  export function getArtifactValue(bundle, artifactName) {
67
+ // Worker-appended feedback participates in the staleness DAG (its content
68
+ // hash re-stales audit-report.md) without being a writable registry entry.
69
+ if (artifactName === AGENT_FEEDBACK_FILENAME) {
70
+ return bundle.agent_reflections;
71
+ }
67
72
  const key = ARTIFACT_FILE_TO_BUNDLE_KEY[artifactName];
68
73
  return key ? bundle[key] : undefined;
69
74
  }
@@ -85,6 +90,14 @@ export async function loadArtifactBundle(root) {
85
90
  if (activeDispatch !== undefined) {
86
91
  bundle.active_dispatch = activeDispatch;
87
92
  }
93
+ // agent-feedback.jsonl is appended by workers (opt-in reflections), never
94
+ // written by the orchestrator. Parse leniently: malformed lines are skipped,
95
+ // a present-but-unusable file is just an empty list. Synthesis surfaces the
96
+ // parsed reflections as the report's "Process Feedback" section.
97
+ const feedbackText = await readOptionalTextFile(join(root, AGENT_FEEDBACK_FILENAME));
98
+ if (feedbackText !== undefined) {
99
+ bundle.agent_reflections = parseReflectionsNdjson(feedbackText);
100
+ }
88
101
  return bundle;
89
102
  }
90
103
  export async function writeCoreArtifacts(root, bundle, options = {}) {
@@ -12,7 +12,7 @@ import { runAutoFixExecutor } from "./autoFixExecutor.js";
12
12
  import { runSyntaxResolutionExecutor } from "./syntaxResolutionExecutor.js";
13
13
  import { runGraphEnrichmentExecutor } from "./graphEnrichmentExecutor.js";
14
14
  import { resolveAuditScope } from "./scope.js";
15
- import { RunLogger } from "@audit-tools/shared";
15
+ import { AGENT_FEEDBACK_FILENAME, RunLogger } from "@audit-tools/shared";
16
16
  /**
17
17
  * Narrow an optional root to a definite string for an executor that requires
18
18
  * it, throwing the canonical "advanceAudit <executor> requires root" error
@@ -234,10 +234,18 @@ export async function advanceAudit(bundle, options = {}) {
234
234
  artifact,
235
235
  });
236
236
  }
237
- const metadata = computeArtifactMetadata(run.updated, bundle.artifact_metadata, [...run.artifacts_written, "tooling_manifest.json"]);
237
+ // tooling_manifest.json and agent-feedback.jsonl are produced outside the
238
+ // executor loop (environment probe / worker appends), so no executor ever
239
+ // lists them in artifacts_written. Treat both as always-updated: their
240
+ // metadata entries are recomputed from live content each advance — unchanged
241
+ // content keeps its revision (no churn), changed content bumps it so
242
+ // dependents re-stale exactly once instead of perpetually mismatching a
243
+ // carried-forward stale hash.
244
+ const metadata = computeArtifactMetadata(run.updated, bundle.artifact_metadata, [...run.artifacts_written, "tooling_manifest.json", AGENT_FEEDBACK_FILENAME]);
238
245
  const metadataBundle = {
239
246
  ...run.updated,
240
247
  tooling_manifest: bundle.tooling_manifest,
248
+ agent_reflections: bundle.agent_reflections,
241
249
  artifact_metadata: metadata,
242
250
  };
243
251
  const updatedState = deriveAuditState(metadataBundle);
@@ -148,6 +148,16 @@ export const ARTIFACT_DEPENDENTS_MAP = {
148
148
  "runtime_validation_report.json": [
149
149
  "audit-report.md",
150
150
  ],
151
+ // Opt-in worker reflections (appended by workers, read-only to the
152
+ // orchestrator; parsed into bundle.agent_reflections). Only the markdown
153
+ // render depends on it — the "Process Feedback" section — NOT
154
+ // audit-findings.json, whose machine contract carries no reflections.
155
+ // advanceAudit hashes it fresh every advance (tooling_manifest pattern), so
156
+ // a reflection appended after synthesis re-stales the report exactly once
157
+ // and an unchanged file never churns finalization.
158
+ "agent-feedback.jsonl": [
159
+ "audit-report.md",
160
+ ],
151
161
  // The canonical machine contract is co-produced with audit-report.md by the
152
162
  // synthesis executor. The optional narrative pass tracks its revision: a fresh
153
163
  // (re-synthesized) audit-findings.json re-stales the narrative marker so the
@@ -34,7 +34,11 @@ export function runSynthesisExecutor(bundle, results) {
34
34
  updated: {
35
35
  ...bundle,
36
36
  audit_findings: findings,
37
- audit_report: renderAuditReportMarkdown(findings, { scope: bundle.scope, intent_checkpoint: bundle.intent_checkpoint }),
37
+ audit_report: renderAuditReportMarkdown(findings, {
38
+ scope: bundle.scope,
39
+ intent_checkpoint: bundle.intent_checkpoint,
40
+ reflections: bundle.agent_reflections,
41
+ }),
38
42
  },
39
43
  artifacts_written: ["audit-findings.json", "audit-report.md"],
40
44
  progress_summary: `Rendered deterministic audit report and canonical findings for ${finalResults.length} audit result entries.`,
@@ -84,7 +88,11 @@ export function runSynthesisNarrativeExecutor(bundle, narrative) {
84
88
  updated: {
85
89
  ...bundle,
86
90
  audit_findings: enriched,
87
- audit_report: renderAuditReportMarkdown(enriched, { scope: bundle.scope, intent_checkpoint: bundle.intent_checkpoint }),
91
+ audit_report: renderAuditReportMarkdown(enriched, {
92
+ scope: bundle.scope,
93
+ intent_checkpoint: bundle.intent_checkpoint,
94
+ reflections: bundle.agent_reflections,
95
+ }),
88
96
  synthesis_narrative: record,
89
97
  },
90
98
  artifacts_written: [
@@ -4,9 +4,9 @@ import type { IntentCheckpoint } from "@audit-tools/shared";
4
4
  import type { DesignAssessment } from "../types/designAssessment.js";
5
5
  import type { ExternalAnalyzerResults } from "../types/externalAnalyzer.js";
6
6
  import type { AuditFindingsReport, CriticalFlowManifest, Finding as SharedFinding, FindingTheme, GraphBundle, SynthesisNarrative } from "@audit-tools/shared";
7
+ import { type AgentReflection } from "@audit-tools/shared";
7
8
  import type { RuntimeValidationReport, RuntimeValidationTaskManifest } from "../types/runtimeValidation.js";
8
9
  import { type WorkBlock } from "./workBlocks.js";
9
- import { type AgentReflection } from "./agentReflections.js";
10
10
  /** Contract version stamped onto the canonical `audit-findings.json`. */
11
11
  export declare const AUDIT_FINDINGS_CONTRACT_VERSION = "audit-tools/audit-findings/v1";
12
12
  /**
@@ -75,8 +75,9 @@ export interface RenderAuditReportOptions {
75
75
  scope?: AuditScopeManifest;
76
76
  /**
77
77
  * Opt-in agent meta-audit reflections to surface in a "Process Feedback"
78
- * section. Omitted/empty renders nothing. The synthesis disk-load that
79
- * populates this from `agent-feedback.jsonl` is wired separately.
78
+ * section. Omitted/empty renders nothing. Populated from the parsed
79
+ * `agent-feedback.jsonl` (`bundle.agent_reflections`) by the synthesis
80
+ * executors.
80
81
  */
81
82
  reflections?: AgentReflection[];
82
83
  /**
@@ -1,8 +1,7 @@
1
- import { AUDITOR_REPORT_MARKER } from "@audit-tools/shared";
1
+ import { AUDITOR_REPORT_MARKER, renderProcessFeedbackSection, } from "@audit-tools/shared";
2
2
  import { buildWorkBlocks } from "./workBlocks.js";
3
3
  import { mergeFindings } from "./mergeFindings.js";
4
4
  import { assignStableFindingIds } from "./findingIdentity.js";
5
- import { renderProcessFeedbackSection, } from "./agentReflections.js";
6
5
  /** Contract version stamped onto the canonical `audit-findings.json`. */
7
6
  export const AUDIT_FINDINGS_CONTRACT_VERSION = "audit-tools/audit-findings/v1";
8
7
  function countBy(items, selectKey) {
@@ -82,7 +81,6 @@ export function buildAuditReportModel(params) {
82
81
  findings,
83
82
  work_blocks: workBlocks,
84
83
  };
85
- console.error(JSON.stringify({ tag: 'synthesis_complete', finding_count: findings.length, work_block_count: workBlocks.length, severity_breakdown: severityBreakdown(findings), audited_file_count: coverage.audited_file_count, excluded_file_count: coverage.excluded_file_count, budget_deferred_task_count: coverage.budget_deferred_task_count }));
86
84
  return model;
87
85
  }
88
86
  /**
@@ -97,7 +95,6 @@ export function buildAuditFindingsReport(model) {
97
95
  findings: model.findings,
98
96
  work_blocks: model.work_blocks,
99
97
  };
100
- console.error(JSON.stringify({ tag: 'audit_findings_report_built', contract_version: AUDIT_FINDINGS_CONTRACT_VERSION, finding_count: model.findings.length, work_block_count: model.work_blocks.length }));
101
98
  return report;
102
99
  }
103
100
  /**
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "auditor-lambda",
3
- "version": "0.12.0",
3
+ "version": "0.12.1",
4
4
  "private": false,
5
5
  "description": "Portable hybrid code-auditing framework for arbitrary repositories.",
6
6
  "type": "module",
@@ -1,38 +0,0 @@
1
- export type ReflectionClarity = "clear" | "mostly_clear" | "ambiguous" | "unclear";
2
- export type ReflectionSeverity = "info" | "low" | "medium" | "high";
3
- export interface AgentReflection {
4
- task_id: string;
5
- lens?: string;
6
- instruction_clarity: ReflectionClarity;
7
- ambiguities?: string[];
8
- tool_friction?: string[];
9
- suggestions?: string[];
10
- severity: ReflectionSeverity;
11
- }
12
- /**
13
- * Parse NDJSON reflection text, keeping only schema-valid objects. Blank lines,
14
- * non-JSON lines, and objects missing the required `task_id`/`instruction_clarity`/
15
- * `severity` (or with out-of-enum values) are skipped silently — the channel is
16
- * opt-in and best-effort, so a bad reflection must never break synthesis.
17
- */
18
- export declare function parseReflectionsNdjson(text: string): AgentReflection[];
19
- export interface ReflectionAggregate {
20
- total: number;
21
- clarity_breakdown: Record<ReflectionClarity, number>;
22
- severity_breakdown: Record<ReflectionSeverity, number>;
23
- /** Deduped notes, highest reported impact first (ties broken alphabetically). */
24
- friction: string[];
25
- ambiguities: string[];
26
- suggestions: string[];
27
- }
28
- /**
29
- * Tally clarity/severity and dedupe the free-text notes across reflections,
30
- * ranking each distinct note by the highest severity it was reported under so the
31
- * most impactful friction surfaces first.
32
- */
33
- export declare function aggregateReflections(reflections: AgentReflection[]): ReflectionAggregate;
34
- /**
35
- * Render the "## Process Feedback" section. Returns `[]` when there are no
36
- * reflections so the report omits the section entirely.
37
- */
38
- export declare function renderProcessFeedbackSection(reflections: AgentReflection[]): string[];
@@ -1,162 +0,0 @@
1
- // Agent meta-audit reflections: a canonical, opt-in feedback channel. Workers may
2
- // append one reflection per task (NDJSON) to `agent-feedback.jsonl` — schema
3
- // `schemas/agent_reflection.schema.json`. Synthesis aggregates them into a
4
- // "Process Feedback" report section so recurring operational friction is visible
5
- // without hand-reading the JSONL. The channel is best-effort: a malformed line is
6
- // skipped, never fatal, and never competes with the actual audit obligation.
7
- const CLARITY_VALUES = new Set([
8
- "clear",
9
- "mostly_clear",
10
- "ambiguous",
11
- "unclear",
12
- ]);
13
- const SEVERITY_VALUES = new Set([
14
- "info",
15
- "low",
16
- "medium",
17
- "high",
18
- ]);
19
- const SEVERITY_RANK = {
20
- high: 3,
21
- medium: 2,
22
- low: 1,
23
- info: 0,
24
- };
25
- function isStringArray(value) {
26
- return Array.isArray(value) && value.every((item) => typeof item === "string");
27
- }
28
- /**
29
- * Parse NDJSON reflection text, keeping only schema-valid objects. Blank lines,
30
- * non-JSON lines, and objects missing the required `task_id`/`instruction_clarity`/
31
- * `severity` (or with out-of-enum values) are skipped silently — the channel is
32
- * opt-in and best-effort, so a bad reflection must never break synthesis.
33
- */
34
- export function parseReflectionsNdjson(text) {
35
- const reflections = [];
36
- for (const rawLine of text.split(/\r?\n/)) {
37
- const line = rawLine.trim();
38
- if (line.length === 0)
39
- continue;
40
- let parsed;
41
- try {
42
- parsed = JSON.parse(line);
43
- }
44
- catch {
45
- continue;
46
- }
47
- if (!parsed || typeof parsed !== "object" || Array.isArray(parsed))
48
- continue;
49
- const record = parsed;
50
- if (typeof record.task_id !== "string" || record.task_id.length === 0) {
51
- continue;
52
- }
53
- if (typeof record.instruction_clarity !== "string" ||
54
- !CLARITY_VALUES.has(record.instruction_clarity)) {
55
- continue;
56
- }
57
- if (typeof record.severity !== "string" ||
58
- !SEVERITY_VALUES.has(record.severity)) {
59
- continue;
60
- }
61
- const reflection = {
62
- task_id: record.task_id,
63
- instruction_clarity: record.instruction_clarity,
64
- severity: record.severity,
65
- };
66
- if (typeof record.lens === "string")
67
- reflection.lens = record.lens;
68
- if (isStringArray(record.ambiguities))
69
- reflection.ambiguities = record.ambiguities;
70
- if (isStringArray(record.tool_friction))
71
- reflection.tool_friction = record.tool_friction;
72
- if (isStringArray(record.suggestions))
73
- reflection.suggestions = record.suggestions;
74
- reflections.push(reflection);
75
- }
76
- return reflections;
77
- }
78
- /**
79
- * Tally clarity/severity and dedupe the free-text notes across reflections,
80
- * ranking each distinct note by the highest severity it was reported under so the
81
- * most impactful friction surfaces first.
82
- */
83
- export function aggregateReflections(reflections) {
84
- const clarity_breakdown = {
85
- clear: 0,
86
- mostly_clear: 0,
87
- ambiguous: 0,
88
- unclear: 0,
89
- };
90
- const severity_breakdown = {
91
- info: 0,
92
- low: 0,
93
- medium: 0,
94
- high: 0,
95
- };
96
- const friction = new Map();
97
- const ambiguities = new Map();
98
- const suggestions = new Map();
99
- const collect = (target, items, severity) => {
100
- for (const item of items ?? []) {
101
- const key = item.trim();
102
- if (key.length === 0)
103
- continue;
104
- target.set(key, Math.max(target.get(key) ?? 0, SEVERITY_RANK[severity]));
105
- }
106
- };
107
- for (const reflection of reflections) {
108
- clarity_breakdown[reflection.instruction_clarity] += 1;
109
- severity_breakdown[reflection.severity] += 1;
110
- collect(friction, reflection.tool_friction, reflection.severity);
111
- collect(ambiguities, reflection.ambiguities, reflection.severity);
112
- collect(suggestions, reflection.suggestions, reflection.severity);
113
- }
114
- const rankedKeys = (target) => [...target.entries()]
115
- .sort((a, b) => b[1] - a[1] || a[0].localeCompare(b[0]))
116
- .map(([key]) => key);
117
- return {
118
- total: reflections.length,
119
- clarity_breakdown,
120
- severity_breakdown,
121
- friction: rankedKeys(friction),
122
- ambiguities: rankedKeys(ambiguities),
123
- suggestions: rankedKeys(suggestions),
124
- };
125
- }
126
- function formatCounts(counts) {
127
- const parts = Object.entries(counts)
128
- .filter(([, count]) => count > 0)
129
- .map(([key, count]) => `${key}: ${count}`);
130
- return parts.length > 0 ? parts.join(", ") : "none";
131
- }
132
- /**
133
- * Render the "## Process Feedback" section. Returns `[]` when there are no
134
- * reflections so the report omits the section entirely.
135
- */
136
- export function renderProcessFeedbackSection(reflections) {
137
- if (reflections.length === 0)
138
- return [];
139
- const aggregate = aggregateReflections(reflections);
140
- const lines = [
141
- "## Process Feedback",
142
- "",
143
- `Aggregated from ${aggregate.total} agent reflection(s) appended during the run ` +
144
- `(opt-in; schema: agent_reflection.schema.json).`,
145
- "",
146
- `- Instruction clarity: ${formatCounts(aggregate.clarity_breakdown)}`,
147
- `- Reported impact: ${formatCounts(aggregate.severity_breakdown)}`,
148
- "",
149
- ];
150
- const block = (title, items) => {
151
- if (items.length === 0)
152
- return;
153
- lines.push(`### ${title}`, "");
154
- for (const item of items)
155
- lines.push(`- ${item}`);
156
- lines.push("");
157
- };
158
- block("Tool & instruction friction", aggregate.friction);
159
- block("Ambiguities", aggregate.ambiguities);
160
- block("Suggestions", aggregate.suggestions);
161
- return lines;
162
- }