audit-tools 0.50.20 → 0.51.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 (158) hide show
  1. package/dist/audit/cli/auditStep.d.ts +1 -1
  2. package/dist/audit/cli/charterExtractionPrompt.d.ts.map +1 -1
  3. package/dist/audit/cli/charterExtractionPrompt.js +14 -1
  4. package/dist/audit/cli/charterExtractionPrompt.js.map +1 -1
  5. package/dist/audit/cli/conceptualDispatch.d.ts.map +1 -1
  6. package/dist/audit/cli/conceptualDispatch.js +30 -5
  7. package/dist/audit/cli/conceptualDispatch.js.map +1 -1
  8. package/dist/audit/cli/dispatch/hostHandoff.d.ts.map +1 -1
  9. package/dist/audit/cli/dispatch/hostHandoff.js +10 -0
  10. package/dist/audit/cli/dispatch/hostHandoff.js.map +1 -1
  11. package/dist/audit/cli/fanoutLanes.d.ts +7 -0
  12. package/dist/audit/cli/fanoutLanes.d.ts.map +1 -1
  13. package/dist/audit/cli/fanoutLanes.js +9 -1
  14. package/dist/audit/cli/fanoutLanes.js.map +1 -1
  15. package/dist/audit/cli/laneSubmissions.d.ts +38 -27
  16. package/dist/audit/cli/laneSubmissions.d.ts.map +1 -1
  17. package/dist/audit/cli/laneSubmissions.js +129 -2
  18. package/dist/audit/cli/laneSubmissions.js.map +1 -1
  19. package/dist/audit/cli/nextStepCommand.d.ts.map +1 -1
  20. package/dist/audit/cli/nextStepCommand.js +44 -10
  21. package/dist/audit/cli/nextStepCommand.js.map +1 -1
  22. package/dist/audit/cli/nextStepHelpers.d.ts.map +1 -1
  23. package/dist/audit/cli/nextStepHelpers.js +34 -4
  24. package/dist/audit/cli/nextStepHelpers.js.map +1 -1
  25. package/dist/audit/cli/sampleRunCommand.d.ts +2 -1
  26. package/dist/audit/cli/sampleRunCommand.d.ts.map +1 -1
  27. package/dist/audit/contracts/workerSchemas.d.ts +4 -4
  28. package/dist/audit/contracts/workerSchemas.d.ts.map +1 -1
  29. package/dist/audit/contracts/workerSchemas.js +12 -6
  30. package/dist/audit/contracts/workerSchemas.js.map +1 -1
  31. package/dist/audit/contracts/wrapperResponse.d.ts +7 -7
  32. package/dist/audit/contracts/wrapperResponse.d.ts.map +1 -1
  33. package/dist/audit/contracts/wrapperResponse.js +9 -1
  34. package/dist/audit/contracts/wrapperResponse.js.map +1 -1
  35. package/dist/audit/extractors/commentDecomposition.d.ts +28 -0
  36. package/dist/audit/extractors/commentDecomposition.d.ts.map +1 -1
  37. package/dist/audit/extractors/commentDecomposition.js +117 -0
  38. package/dist/audit/extractors/commentDecomposition.js.map +1 -1
  39. package/dist/audit/orchestrator/charterDeltaExecutor.d.ts.map +1 -1
  40. package/dist/audit/orchestrator/charterDeltaExecutor.js +14 -0
  41. package/dist/audit/orchestrator/charterDeltaExecutor.js.map +1 -1
  42. package/dist/audit/orchestrator/charterExtractionExecutor.d.ts +16 -6
  43. package/dist/audit/orchestrator/charterExtractionExecutor.d.ts.map +1 -1
  44. package/dist/audit/orchestrator/charterExtractionExecutor.js +127 -9
  45. package/dist/audit/orchestrator/charterExtractionExecutor.js.map +1 -1
  46. package/dist/audit/orchestrator/charterPacketArchive.d.ts +51 -0
  47. package/dist/audit/orchestrator/charterPacketArchive.d.ts.map +1 -0
  48. package/dist/audit/orchestrator/charterPacketArchive.js +173 -0
  49. package/dist/audit/orchestrator/charterPacketArchive.js.map +1 -0
  50. package/dist/audit/orchestrator/charterPackets.d.ts +31 -13
  51. package/dist/audit/orchestrator/charterPackets.d.ts.map +1 -1
  52. package/dist/audit/orchestrator/charterPackets.js +433 -79
  53. package/dist/audit/orchestrator/charterPackets.js.map +1 -1
  54. package/dist/audit/orchestrator/designReviewPrompt.d.ts +20 -4
  55. package/dist/audit/orchestrator/designReviewPrompt.d.ts.map +1 -1
  56. package/dist/audit/orchestrator/designReviewPrompt.js +89 -12
  57. package/dist/audit/orchestrator/designReviewPrompt.js.map +1 -1
  58. package/dist/audit/orchestrator/executorRunners.d.ts.map +1 -1
  59. package/dist/audit/orchestrator/executorRunners.js +17 -1
  60. package/dist/audit/orchestrator/executorRunners.js.map +1 -1
  61. package/dist/audit/orchestrator/graphEnrichmentExecutor.d.ts.map +1 -1
  62. package/dist/audit/orchestrator/graphEnrichmentExecutor.js +8 -1
  63. package/dist/audit/orchestrator/graphEnrichmentExecutor.js.map +1 -1
  64. package/dist/audit/orchestrator/state.d.ts.map +1 -1
  65. package/dist/audit/orchestrator/state.js +21 -14
  66. package/dist/audit/orchestrator/state.js.map +1 -1
  67. package/dist/audit/orchestrator/synthesisExecutors.d.ts +8 -0
  68. package/dist/audit/orchestrator/synthesisExecutors.d.ts.map +1 -1
  69. package/dist/audit/orchestrator/synthesisExecutors.js +35 -1
  70. package/dist/audit/orchestrator/synthesisExecutors.js.map +1 -1
  71. package/dist/audit/reporting/synthesis.d.ts +43 -2
  72. package/dist/audit/reporting/synthesis.d.ts.map +1 -1
  73. package/dist/audit/reporting/synthesis.js +448 -15
  74. package/dist/audit/reporting/synthesis.js.map +1 -1
  75. package/dist/audit/supervisor/operatorHandoff.d.ts.map +1 -1
  76. package/dist/audit/supervisor/operatorHandoff.js +14 -6
  77. package/dist/audit/supervisor/operatorHandoff.js.map +1 -1
  78. package/dist/audit/systemic/secondOrderAdversaryPrompt.d.ts +2 -1
  79. package/dist/audit/systemic/secondOrderAdversaryPrompt.d.ts.map +1 -1
  80. package/dist/audit/systemic/secondOrderAdversaryPrompt.js +13 -3
  81. package/dist/audit/systemic/secondOrderAdversaryPrompt.js.map +1 -1
  82. package/dist/audit/types/analyzerCapability.d.ts +35 -11
  83. package/dist/audit/types/analyzerCapability.d.ts.map +1 -1
  84. package/dist/audit/types/analyzerCapability.js +83 -5
  85. package/dist/audit/types/analyzerCapability.js.map +1 -1
  86. package/dist/audit/types/auditState.d.ts +8 -8
  87. package/dist/audit/types/charterRegister.d.ts +33 -3
  88. package/dist/audit/types/charterRegister.d.ts.map +1 -1
  89. package/dist/audit/types/charterRegister.js +11 -1
  90. package/dist/audit/types/charterRegister.js.map +1 -1
  91. package/dist/audit/types/conceptualAdjudication.d.ts +383 -4
  92. package/dist/audit/types/conceptualAdjudication.d.ts.map +1 -1
  93. package/dist/audit/types/conceptualAdjudication.js +109 -5
  94. package/dist/audit/types/conceptualAdjudication.js.map +1 -1
  95. package/dist/audit/types/designAssessment.d.ts +1 -1
  96. package/dist/audit/types/designAssessment.d.ts.map +1 -1
  97. package/dist/audit/types.d.ts +7 -2
  98. package/dist/audit/types.d.ts.map +1 -1
  99. package/dist/remediate/intent/intentOrdering.d.ts.map +1 -1
  100. package/dist/remediate/intent/intentOrdering.js +29 -1
  101. package/dist/remediate/intent/intentOrdering.js.map +1 -1
  102. package/dist/remediate/phases/grounding.d.ts +12 -4
  103. package/dist/remediate/phases/grounding.d.ts.map +1 -1
  104. package/dist/remediate/phases/grounding.js +23 -60
  105. package/dist/remediate/phases/grounding.js.map +1 -1
  106. package/dist/remediate/state/types.d.ts +5 -0
  107. package/dist/remediate/state/types.d.ts.map +1 -1
  108. package/dist/shared/analyzers/types.d.ts +17 -6
  109. package/dist/shared/analyzers/types.d.ts.map +1 -1
  110. package/dist/shared/analyzers/types.js +6 -15
  111. package/dist/shared/analyzers/types.js.map +1 -1
  112. package/dist/shared/decompose/systemicChallenge.d.ts +5 -0
  113. package/dist/shared/decompose/systemicChallenge.d.ts.map +1 -1
  114. package/dist/shared/engine/obligationEngine.d.ts +23 -6
  115. package/dist/shared/engine/obligationEngine.d.ts.map +1 -1
  116. package/dist/shared/engine/obligationEngine.js +38 -3
  117. package/dist/shared/engine/obligationEngine.js.map +1 -1
  118. package/dist/shared/findings/dedupe.d.ts +10 -0
  119. package/dist/shared/findings/dedupe.d.ts.map +1 -1
  120. package/dist/shared/findings/dedupe.js +15 -0
  121. package/dist/shared/findings/dedupe.js.map +1 -1
  122. package/dist/shared/index.d.ts +14 -6
  123. package/dist/shared/index.d.ts.map +1 -1
  124. package/dist/shared/index.js +12 -5
  125. package/dist/shared/index.js.map +1 -1
  126. package/dist/shared/measurement/measuredOutcome.d.ts +61 -0
  127. package/dist/shared/measurement/measuredOutcome.d.ts.map +1 -0
  128. package/dist/shared/measurement/measuredOutcome.js +109 -0
  129. package/dist/shared/measurement/measuredOutcome.js.map +1 -0
  130. package/dist/shared/reporting/findingDisplay.d.ts +14 -2
  131. package/dist/shared/reporting/findingDisplay.d.ts.map +1 -1
  132. package/dist/shared/reporting/findingDisplay.js +29 -2
  133. package/dist/shared/reporting/findingDisplay.js.map +1 -1
  134. package/dist/shared/reporting/lensCoverage.d.ts +70 -0
  135. package/dist/shared/reporting/lensCoverage.d.ts.map +1 -0
  136. package/dist/shared/reporting/lensCoverage.js +108 -0
  137. package/dist/shared/reporting/lensCoverage.js.map +1 -0
  138. package/dist/shared/submission/submissionLedger.d.ts +24 -1
  139. package/dist/shared/submission/submissionLedger.d.ts.map +1 -1
  140. package/dist/shared/submission/submissionLedger.js +61 -0
  141. package/dist/shared/submission/submissionLedger.js.map +1 -1
  142. package/dist/shared/types/charterPacket.d.ts +155 -0
  143. package/dist/shared/types/charterPacket.d.ts.map +1 -0
  144. package/dist/shared/types/charterPacket.js +9 -0
  145. package/dist/shared/types/charterPacket.js.map +1 -0
  146. package/dist/shared/types/finding.d.ts +275 -40
  147. package/dist/shared/types/finding.d.ts.map +1 -1
  148. package/dist/shared/types/finding.js +87 -0
  149. package/dist/shared/types/finding.js.map +1 -1
  150. package/dist/shared/validation/citationGrounding.d.ts +115 -0
  151. package/dist/shared/validation/citationGrounding.d.ts.map +1 -0
  152. package/dist/shared/validation/citationGrounding.js +347 -0
  153. package/dist/shared/validation/citationGrounding.js.map +1 -0
  154. package/dist/shared/validation/findingsReport.d.ts.map +1 -1
  155. package/dist/shared/validation/findingsReport.js +62 -0
  156. package/dist/shared/validation/findingsReport.js.map +1 -1
  157. package/package.json +1 -1
  158. package/schemas/audit-code-v1alpha1.schema.json +2 -1
@@ -1,8 +1,12 @@
1
- import { AUDIT_FINDINGS_CONTRACT_VERSION as SHARED_AUDIT_FINDINGS_CONTRACT_VERSION, AUDITOR_REPORT_MARKER, renderProcessFeedbackSection, renderFindingBlockLines, compareCodeUnits, countBy, } from "audit-tools/shared";
1
+ import { degradedAnalyzerEntries, } from "../types/analyzerCapability.js";
2
+ import { AUDIT_FINDINGS_CONTRACT_VERSION as SHARED_AUDIT_FINDINGS_CONTRACT_VERSION, AUDITOR_REPORT_MARKER, renderProcessFeedbackSection, renderFindingBlockLines, compareCodeUnits, countBy, deriveLensCoverage, isIngestEvent, reprojectLensCoverage, } from "audit-tools/shared";
3
+ import { resolveIntentLensSelection } from "../orchestrator/lensSelection.js";
2
4
  import { buildWorkBlockPartition } from "./workBlocks.js";
3
5
  import { mergeFindings } from "./mergeFindings.js";
4
6
  import { selectCurrentResults } from "../orchestrator/ledger.js";
5
7
  import { assignStableFindingIds } from "./findingIdentity.js";
8
+ const CONCEPTUAL_ATTRIBUTION_IDS = Symbol("conceptual-attribution-ids");
9
+ const CONCEPTUAL_ID_MARKER_PREFIX = "\u0000audit-tools:conceptual-final-id:";
6
10
  /**
7
11
  * Contract version stamped onto the canonical `audit-findings.json`.
8
12
  * Single-sourced from `audit-tools/shared` so the auditor's output and the
@@ -10,6 +14,47 @@ import { assignStableFindingIds } from "./findingIdentity.js";
10
14
  * `seam-artifact-ipc-envelope` test).
11
15
  */
12
16
  export const AUDIT_FINDINGS_CONTRACT_VERSION = SHARED_AUDIT_FINDINGS_CONTRACT_VERSION;
17
+ function markConceptualFindingIds(designAssessment) {
18
+ const conceptualFindings = designAssessment?.conceptual_findings ?? [];
19
+ if (conceptualFindings.length === 0) {
20
+ return { designAssessment, findingIdByMarker: new Map() };
21
+ }
22
+ const findingIdByMarker = new Map();
23
+ const markedFindings = conceptualFindings.map((finding, index) => {
24
+ const marker = `${CONCEPTUAL_ID_MARKER_PREFIX}${index}:${finding.id}\u0000`;
25
+ findingIdByMarker.set(marker, finding.id);
26
+ return {
27
+ ...finding,
28
+ evidence: [...(finding.evidence ?? []), marker],
29
+ };
30
+ });
31
+ return {
32
+ designAssessment: { ...designAssessment, conceptual_findings: markedFindings },
33
+ findingIdByMarker,
34
+ };
35
+ }
36
+ function extractConceptualAttributionIds(mergedFindings, findingIdByMarker) {
37
+ if (findingIdByMarker.size === 0)
38
+ return mergedFindings.map(() => []);
39
+ let recoveredMarkerCount = 0;
40
+ const localIdsByFinding = mergedFindings.map((finding) => {
41
+ const localIds = [];
42
+ const evidence = (finding.evidence ?? []).filter((entry) => {
43
+ const localId = findingIdByMarker.get(entry);
44
+ if (localId === undefined)
45
+ return true;
46
+ localIds.push(localId);
47
+ recoveredMarkerCount += 1;
48
+ return false;
49
+ });
50
+ finding.evidence = evidence;
51
+ return localIds;
52
+ });
53
+ if (recoveredMarkerCount !== findingIdByMarker.size) {
54
+ throw new Error("Conceptual attribution identity markers were not conserved through finding merge.");
55
+ }
56
+ return localIdsByFinding;
57
+ }
13
58
  function severityBreakdown(findings) {
14
59
  return countBy(findings, (finding) => finding.severity);
15
60
  }
@@ -24,6 +69,15 @@ function lensBreakdown(findings) {
24
69
  function groundingStatusBreakdown(findings) {
25
70
  return countBy(findings, (finding) => finding.grounding?.status);
26
71
  }
72
+ /**
73
+ * Per-status counts of the judge's defect-presence claim. Findings with no
74
+ * status (every deterministic finding — the conceptual pass is the only
75
+ * producer) are skipped by `countBy`, so an empty result means "no finding
76
+ * carried a claim" and the caller omits the field, exactly as for grounding.
77
+ */
78
+ function verificationStatusBreakdown(findings) {
79
+ return countBy(findings, (finding) => finding.verification_status);
80
+ }
27
81
  function runtimeStatusBreakdown(report, taskManifest) {
28
82
  const breakdown = countBy(report?.results ?? [], (result) => result.status);
29
83
  const resultTaskIds = new Set((report?.results ?? []).map((result) => result.task_id));
@@ -56,6 +110,7 @@ function formatCountList(summary) {
56
110
  return parts.length > 0 ? parts.join(", ") : "none";
57
111
  }
58
112
  export function buildAuditReportModel(params) {
113
+ const markedConceptual = markConceptualFindingIds(params.designAssessment);
59
114
  // Re-key the finalized findings with globally-unique, content-addressed ids
60
115
  // before anything addresses them by id. mergeFindings emits exactly one
61
116
  // finding per file-independent identity (exact normalized lens|category|
@@ -69,7 +124,20 @@ export function buildAuditReportModel(params) {
69
124
  // before merging, so a re-dispatched result's fresh findings replace the stale
70
125
  // base record they superseded — including findings the re-audit dropped, which
71
126
  // a finding-id upsert alone would leave behind.
72
- const allFindings = assignStableFindingIds(mergeFindings(selectCurrentResults(params.results), params.runtimeValidationReport, params.externalAnalyzerResults, params.designAssessment, params.structureDecomposition, params.charterRegister, params.systemicChallenge));
127
+ const mergedFindings = mergeFindings(selectCurrentResults(params.results), params.runtimeValidationReport, params.externalAnalyzerResults, markedConceptual.designAssessment, params.structureDecomposition, params.charterRegister, params.systemicChallenge);
128
+ const conceptualIdsByFinding = extractConceptualAttributionIds(mergedFindings, markedConceptual.findingIdByMarker);
129
+ const allFindings = assignStableFindingIds(mergedFindings);
130
+ const conceptualAttributionIds = new Map();
131
+ for (const [index, localIds] of conceptualIdsByFinding.entries()) {
132
+ const canonicalId = allFindings[index].id;
133
+ for (const localId of localIds) {
134
+ const existing = conceptualAttributionIds.get(localId);
135
+ if (existing !== undefined && existing !== canonicalId) {
136
+ throw new Error(`Conceptual finding id "${localId}" resolved to multiple canonical findings.`);
137
+ }
138
+ conceptualAttributionIds.set(localId, canonicalId);
139
+ }
140
+ }
73
141
  // B4: a tool-executable anchor that REFUTED a claim (status `refuted`, distinct
74
142
  // from `ungrounded`) is quarantined-EXCLUDED — kept out of the admitted findings
75
143
  // AND the work blocks so a disproven claim never merges as actionable fact. The
@@ -87,20 +155,48 @@ export function buildAuditReportModel(params) {
87
155
  sizeIndex: params.sizeIndex,
88
156
  });
89
157
  const coverage = coverageSummary(params.coverageMatrix);
158
+ // A lens is only `clean` — "asked, and there was nothing there" — when at
159
+ // least one LENS-OPEN channel came back. Per-file audit results are lens-gated
160
+ // by planning (`required_lenses`), and the two design-review passes carry the
161
+ // selection into their prompts and record their own completion. With none of
162
+ // them ingested, a selected lens with no findings was never exercised, and
163
+ // saying `clean` would be exactly the "absence of a finding reads as absence
164
+ // of a defect" claim this field exists to refuse.
165
+ const lensOpenChannelIngested = params.results.length > 0 ||
166
+ params.designAssessment?.contract_reviewed === true ||
167
+ params.designAssessment?.conceptual_reviewed === true;
168
+ const selectedLenses = resolveIntentLensSelection(params.intentCheckpoint?.lens_selection);
90
169
  // Count grounding over ALL findings (incl. quarantined-refuted) so the `refuted`
91
170
  // tally reflects findings dropped from the admitted set.
92
171
  const groundingBreakdown = groundingStatusBreakdown(allFindings);
172
+ const verificationBreakdown = verificationStatusBreakdown(allFindings);
93
173
  const model = {
94
174
  summary: {
95
175
  finding_count: findings.length,
96
176
  work_block_count: workBlocks.blocks.length,
97
177
  severity_breakdown: severityBreakdown(findings),
98
178
  lens_breakdown: lensBreakdown(findings),
179
+ // Presence is guaranteed HERE, at the one boundary that holds both the
180
+ // operator's selection and the produced findings. Absent — deliberately —
181
+ // when no selection resolved: "no limit" and "every lens" are different
182
+ // answers, and inventing a map would be a claim nobody made.
183
+ ...(selectedLenses === undefined
184
+ ? {}
185
+ : {
186
+ lens_coverage: deriveLensCoverage({
187
+ selectedLenses,
188
+ findings,
189
+ lensOpenChannelIngested,
190
+ }),
191
+ }),
99
192
  audited_file_count: coverage.audited_file_count,
100
193
  excluded_file_count: coverage.excluded_file_count,
101
194
  ...(Object.keys(groundingBreakdown).length > 0
102
195
  ? { grounding_status_breakdown: groundingBreakdown }
103
196
  : {}),
197
+ ...(Object.keys(verificationBreakdown).length > 0
198
+ ? { verification_status_breakdown: verificationBreakdown }
199
+ : {}),
104
200
  runtime_validation_status_breakdown: runtimeStatusBreakdown(params.runtimeValidationReport, params.runtimeValidationTaskManifest),
105
201
  },
106
202
  findings,
@@ -109,6 +205,9 @@ export function buildAuditReportModel(params) {
109
205
  work_block_seams: workBlocks.seams,
110
206
  ...(quarantinedRefuted.length > 0 ? { quarantined_findings: quarantinedRefuted } : {}),
111
207
  };
208
+ if (conceptualAttributionIds.size > 0) {
209
+ model[CONCEPTUAL_ATTRIBUTION_IDS] = conceptualAttributionIds;
210
+ }
112
211
  return model;
113
212
  }
114
213
  /**
@@ -128,6 +227,10 @@ export function buildAuditFindingsReport(model) {
128
227
  ? { quarantined_findings: model.quarantined_findings }
129
228
  : {}),
130
229
  };
230
+ const conceptualAttributionIds = model[CONCEPTUAL_ATTRIBUTION_IDS];
231
+ if (conceptualAttributionIds) {
232
+ report[CONCEPTUAL_ATTRIBUTION_IDS] = conceptualAttributionIds;
233
+ }
131
234
  return report;
132
235
  }
133
236
  /**
@@ -182,6 +285,96 @@ export function applyNarrative(report, narrative) {
182
285
  top_risks: narrative.top_risks,
183
286
  };
184
287
  }
288
+ /**
289
+ * Name the selected lenses this run did not exercise.
290
+ *
291
+ * Rendered whenever a selected lens produced no findings, with its outcome, so
292
+ * `clean` ("asked, nothing found") and `not_run` ("never asked") stay legible as
293
+ * the different statements they are. Nothing to say — every selected lens
294
+ * produced findings, or no selection was made — renders nothing.
295
+ */
296
+ function renderUnexercisedLensLine(coverage) {
297
+ const unexercised = (coverage ?? []).filter((entry) => entry.selected && entry.outcome !== "findings");
298
+ if (unexercised.length === 0)
299
+ return [];
300
+ const byOutcome = new Map();
301
+ for (const entry of unexercised) {
302
+ const bucket = byOutcome.get(entry.outcome);
303
+ if (bucket)
304
+ bucket.push(entry.lens);
305
+ else
306
+ byOutcome.set(entry.outcome, [entry.lens]);
307
+ }
308
+ const parts = [...byOutcome.entries()]
309
+ .sort(([left], [right]) => compareCodeUnits(left, right))
310
+ .map(([outcome, lenses]) => `${outcome === "not_run" ? "never exercised" : "exercised, no findings"}: ${lenses.join(", ")}`);
311
+ return [`- Lenses not exercised: ${parts.join("; ")}`];
312
+ }
313
+ /**
314
+ * The *Audit Limitations* body for a degraded graph-enrichment channel.
315
+ *
316
+ * Names each analyzer that was ASKED FOR and could not run, with its own note —
317
+ * the honest per-analyzer row that `analyzer_capability.json` has always carried
318
+ * and that, until now, nothing in the tree ever opened. The scalar roll-up is
319
+ * deliberately not the sentence: it cannot say "1932 edges added AND two
320
+ * analyzers degraded", so the entries are listed rather than summarized.
321
+ */
322
+ function renderAnalyzerDegradationLines(record) {
323
+ const degraded = degradedAnalyzerEntries(record);
324
+ if (degraded.length === 0)
325
+ return [];
326
+ return [
327
+ `${degraded.length} language analyzer(s) were requested and could not run, so parser-grade extraction was OFF for them and the weaker deterministic regex floor was used instead. Findings that depend on the dependency graph are correspondingly weaker for these languages:`,
328
+ "",
329
+ ...degraded.map((entry) => `- \`${entry.id}\` (requested \`${entry.setting}\`, resolution \`${entry.resolution}\`)` +
330
+ (entry.note ? ` — ${entry.note}` : "")),
331
+ "",
332
+ ];
333
+ }
334
+ /**
335
+ * What the charter lanes were actually GIVEN, per evidence class.
336
+ *
337
+ * This renders UNCONDITIONALLY, deliberately diverging from the drift section
338
+ * above where clean is the expected case and presence is the statement. Here the
339
+ * measured case was 0 of 72 source-comment blocks delivered while the register
340
+ * and the report said nothing at all — so a silent section would leave
341
+ * "complete" indistinguishable from "not measured", which is the exact
342
+ * false-green this block exists to close. A run with no charter layer says so.
343
+ */
344
+ function renderCharterCoverageSection(coverage, archive) {
345
+ // Its OWN top-level section, not a sub-head of "Process Feedback": coverage is
346
+ // a delivery measurement about the tool's inputs, not feedback about the host —
347
+ // and nesting it there would force that heading to render on every report,
348
+ // silently inverting the drift section's "presence is the statement" contract.
349
+ const lines = ["## Evidence Coverage", "", "### Charter evidence coverage", ""];
350
+ if (coverage.length === 0) {
351
+ lines.push("No charter evidence packets were measured in this run (the charter layer " +
352
+ "is opt-in at a `deep` ceiling). This is a stated absence, not a clean bill.", "");
353
+ }
354
+ for (const packet of coverage) {
355
+ lines.push(`- \`${packet.kind}\` channel:`);
356
+ for (const entry of packet.classes) {
357
+ const truncated = entry.truncated > 0 ? `, ${entry.truncated} truncated` : "";
358
+ lines.push(` - ${entry.evidence_class}: ${entry.delivered} of ${entry.named} delivered${truncated}` +
359
+ (entry.omitted.length > 0
360
+ ? ` — omitted: ${entry.omitted
361
+ .map((row) => `${row.path} (${row.reason})`)
362
+ .join(", ")}`
363
+ : ""));
364
+ }
365
+ }
366
+ const unarchived = archive.filter((row) => !row.archived);
367
+ if (unarchived.length > 0) {
368
+ lines.push("", "⚠ Packet retention FAILED for " +
369
+ unarchived
370
+ .map((row) => `\`${row.kind}\` (${row.reason ?? "unstated reason"})`)
371
+ .join(", ") +
372
+ ". The lane-asset copy was left in place rather than deleted, so the " +
373
+ "evidence still exists — but it is not in the run's archive.");
374
+ }
375
+ lines.push("");
376
+ return lines;
377
+ }
185
378
  /**
186
379
  * What happened to this run's submissions, when something went wrong.
187
380
  *
@@ -199,6 +392,71 @@ export function applyNarrative(report, narrative) {
199
392
  * ambiguity. Totals are sorted by content, never arrival: a derived summary may
200
393
  * sort; the ledger file it derives from may not.
201
394
  */
395
+ /**
396
+ * What this run actually DISPATCHED, and how much of it came back.
397
+ *
398
+ * The drift section below renders only when something was refused, so a run in
399
+ * which five lanes silently under-delivered — exit 0, nothing written, then
400
+ * re-dispatched until they succeeded — produced zero `rejected` events and read
401
+ * as clean. This is the other half: dispatched versus delivered, per round, so
402
+ * a lane that reported success and wrote nothing is a printed fact rather than
403
+ * something recoverable only from the host's transcript.
404
+ *
405
+ * A dispatch is DELIVERED when its submission has any terminal record other
406
+ * than a `lane_outcome` of `not_run`: an ingest event (the tool consumed it) or
407
+ * an observed outcome. No terminal row at all is `not_run` too — the absence IS
408
+ * the record, never an inference that something arrived.
409
+ */
410
+ function renderLaneDeliverySection(events) {
411
+ const dispatched = events.filter((event) => event.kind === "dispatched");
412
+ if (dispatched.length === 0)
413
+ return [];
414
+ const outcomeById = new Map();
415
+ for (const event of events) {
416
+ if (event.kind === "lane_outcome" && event.outcome !== undefined) {
417
+ outcomeById.set(event.submission_id, event.outcome);
418
+ }
419
+ else if (isIngestEvent(event.kind) && !outcomeById.has(event.submission_id)) {
420
+ // The tool ingested it, so it was delivered; the ledger does not record
421
+ // how much was in it, and this must not invent a count.
422
+ outcomeById.set(event.submission_id, "findings");
423
+ }
424
+ }
425
+ const byRound = new Map();
426
+ for (const event of dispatched) {
427
+ const round = event.round_id ?? "";
428
+ const bucket = byRound.get(round);
429
+ if (bucket)
430
+ bucket.push(event);
431
+ else
432
+ byRound.set(round, [event]);
433
+ }
434
+ const lines = [
435
+ "### Lane dispatch and delivery",
436
+ "",
437
+ `${dispatched.length} lane(s) were dispatched this run. A dispatched lane that wrote nothing at the bound path the tool declared is recorded \`not_run\` — the absence is the record, not an inference.`,
438
+ "",
439
+ ];
440
+ // Sorted by content, never arrival: a derived summary may sort; the ledger
441
+ // file it derives from may not.
442
+ for (const round of [...byRound.keys()].sort(compareCodeUnits)) {
443
+ const rows = byRound.get(round);
444
+ const outcomes = rows.map((event) => outcomeById.get(event.submission_id) ?? "not_run");
445
+ const delivered = outcomes.filter((outcome) => outcome !== "not_run").length;
446
+ const tally = new Map();
447
+ for (const outcome of outcomes) {
448
+ tally.set(outcome, (tally.get(outcome) ?? 0) + 1);
449
+ }
450
+ const breakdown = [...tally.entries()]
451
+ .sort(([left], [right]) => compareCodeUnits(left, right))
452
+ .map(([outcome, count]) => `${outcome} ${count}`)
453
+ .join(", ");
454
+ lines.push(`- ${round === "" ? "this run's gate lanes" : `round \`${round}\``}: ` +
455
+ `${delivered} of ${rows.length} delivered (${breakdown})`);
456
+ }
457
+ lines.push("");
458
+ return lines;
459
+ }
202
460
  function renderSubmissionDriftSection(events) {
203
461
  const refusedIds = new Set(events
204
462
  .filter((event) => event.kind === "rejected")
@@ -208,11 +466,15 @@ function renderSubmissionDriftSection(events) {
208
466
  .map((event) => event.submission_id));
209
467
  if (refusedIds.size === 0 && handRepairedIds.size === 0)
210
468
  return [];
211
- // Trailing state per submission: a refusal followed by an acceptance or a
212
- // hand recovery is RESOLVED; one that is still the last word is not.
469
+ // Trailing state per submission, over the INGEST events only: a refusal
470
+ // followed by an acceptance or a hand recovery is RESOLVED; one that is still
471
+ // the last word is not. It used to be "everything except `expected`", so a
472
+ // `dispatched` row appended when the still-pending lane was re-materialized
473
+ // would have become the trailing event and made this claim the refusal "was
474
+ // later accepted or re-landed by hand" when nothing had accepted it.
213
475
  const trailing = new Map();
214
476
  for (const event of events) {
215
- if (event.kind === "expected")
477
+ if (!isIngestEvent(event.kind))
216
478
  continue;
217
479
  trailing.set(event.submission_id, event.kind);
218
480
  }
@@ -255,9 +517,125 @@ function renderSubmissionDriftSection(events) {
255
517
  function pushFindingBlock(finding, lines) {
256
518
  lines.push(...renderFindingBlockLines(finding));
257
519
  }
258
- function renderConceptualAttributionSection(adjudication) {
520
+ export function canonicalizeConceptualAttributionIds(report, adjudication, idSource = report) {
521
+ const canonicalIds = new Set([
522
+ ...report.findings.map((finding) => finding.id),
523
+ ...(report.quarantined_findings ?? []).map((finding) => finding.id),
524
+ ]);
525
+ const idMap = idSource[CONCEPTUAL_ATTRIBUTION_IDS];
526
+ const resolve = (id) => {
527
+ if (canonicalIds.has(id))
528
+ return id;
529
+ const canonicalId = idMap?.get(id);
530
+ if (canonicalId !== undefined && canonicalIds.has(canonicalId)) {
531
+ return canonicalId;
532
+ }
533
+ throw new Error(`Conceptual attribution refused: final finding id "${id}" does not resolve ` +
534
+ "to a canonical finding in audit-findings.json.");
535
+ };
536
+ // Canonicalize the carried adjudication itself, not only the markdown text.
537
+ // The fold persists every carried core artifact, so subsequent narrative and
538
+ // resynthesis renders reload canonical targets rather than reviving the
539
+ // judge-local ids that existed before the synthesis boundary.
540
+ const sharesByCanonicalId = new Map();
541
+ for (const share of adjudication.final_finding_shares) {
542
+ const sourceFinalFindingId = share.final_finding_id;
543
+ const canonicalId = resolve(sourceFinalFindingId);
544
+ const entries = sharesByCanonicalId.get(canonicalId) ?? [];
545
+ entries.push({ sourceFinalFindingId, share });
546
+ sharesByCanonicalId.set(canonicalId, entries);
547
+ }
548
+ const resolvedFinalFindingShares = [...sharesByCanonicalId.entries()]
549
+ .sort(([left], [right]) => compareCodeUnits(left, right))
550
+ .map(([canonicalId, entries]) => {
551
+ if (entries.length === 1) {
552
+ return { ...entries[0].share, final_finding_id: canonicalId };
553
+ }
554
+ const orderedEntries = [...entries].sort((left, right) => compareCodeUnits(left.sourceFinalFindingId, right.sourceFinalFindingId));
555
+ const contributions = new Map();
556
+ for (const entry of orderedEntries) {
557
+ for (const contribution of entry.share.contributors) {
558
+ const contributorEntries = contributions.get(contribution.contributor_id) ?? [];
559
+ contributorEntries.push({
560
+ sourceFinalFindingId: entry.sourceFinalFindingId,
561
+ contribution,
562
+ });
563
+ contributions.set(contribution.contributor_id, contributorEntries);
564
+ }
565
+ }
566
+ const orderedContributions = [...contributions.entries()].sort(([left], [right]) => compareCodeUnits(left, right));
567
+ // Each judge-local final has its own 100% allocation. Once several local
568
+ // finals collapse to one canonical finding, the canonical allocation is
569
+ // their equal-weight mean (a missing contributor contributes 0%). Allocate
570
+ // power-of-two micro-percent units by largest remainder: division back to
571
+ // numbers is exact in binary, every result stays bounded, and the common
572
+ // denominator makes the sum exactly 100 rather than approximately 100.
573
+ // Source-local percentages/rationales remain verbatim in rationale below.
574
+ const unitsPerPercent = 2 ** 20;
575
+ const totalUnits = 100 * unitsPerPercent;
576
+ const allocationInputs = orderedContributions.map(([contributorId, contributorEntries]) => {
577
+ const sourcePercent = contributorEntries.reduce((sum, entry) => sum + entry.contribution.contribution_percent, 0);
578
+ return { contributorId, contributorEntries, sourcePercent };
579
+ });
580
+ const totalSourcePercent = allocationInputs.reduce((sum, item) => sum + item.sourcePercent, 0);
581
+ if (!Number.isFinite(totalSourcePercent) || totalSourcePercent <= 0) {
582
+ throw new Error("Conceptual attribution percentage allocation has no positive total.");
583
+ }
584
+ const allocations = allocationInputs.map(({ contributorId, contributorEntries, sourcePercent }) => {
585
+ const exactUnits = (sourcePercent / totalSourcePercent) * totalUnits;
586
+ const units = Math.floor(exactUnits);
587
+ return {
588
+ contributorId,
589
+ contributorEntries,
590
+ units,
591
+ remainder: exactUnits - units,
592
+ };
593
+ });
594
+ const remainingUnits = totalUnits - allocations.reduce((sum, item) => sum + item.units, 0);
595
+ if (remainingUnits < 0 || remainingUnits > allocations.length) {
596
+ throw new Error("Conceptual attribution percentage allocation exceeded bounded remainder.");
597
+ }
598
+ const remainderOrder = [...allocations].sort((left, right) => right.remainder - left.remainder ||
599
+ compareCodeUnits(left.contributorId, right.contributorId));
600
+ for (let index = 0; index < remainingUnits; index += 1) {
601
+ remainderOrder[index].units += 1;
602
+ }
603
+ const contributors = orderedContributions.map(([contributorId, contributorEntries]) => {
604
+ const allocation = allocations.find((item) => item.contributorId === contributorId);
605
+ return {
606
+ contributor_id: contributorId,
607
+ source_candidate_ids: [
608
+ ...new Set(contributorEntries.flatMap((entry) => entry.contribution.source_candidate_ids)),
609
+ ].sort(compareCodeUnits),
610
+ contribution_percent: allocation.units / unitsPerPercent,
611
+ rationale: contributorEntries
612
+ .map((entry) => `${entry.sourceFinalFindingId} ` +
613
+ `(${entry.contribution.contribution_percent}%): ` +
614
+ entry.contribution.rationale)
615
+ .join(" | "),
616
+ };
617
+ });
618
+ return {
619
+ final_finding_id: canonicalId,
620
+ contributors,
621
+ };
622
+ });
623
+ const resolvedCandidateDispositions = adjudication.candidate_dispositions.map((disposition) => ({
624
+ ...disposition,
625
+ target_final_finding_ids: [
626
+ ...new Set(disposition.target_final_finding_ids.map(resolve)),
627
+ ],
628
+ }));
629
+ // Commit both reference-bearing arrays only after every share and target has
630
+ // resolved. A late unknown disposition target cannot leave half-canonical
631
+ // attribution in the caller's carried bundle.
632
+ adjudication.final_finding_shares = resolvedFinalFindingShares;
633
+ adjudication.candidate_dispositions = resolvedCandidateDispositions;
634
+ }
635
+ function renderConceptualAttributionSection(report, adjudication) {
259
636
  if (!adjudication)
260
637
  return [];
638
+ canonicalizeConceptualAttributionIds(report, adjudication);
261
639
  const contributors = new Map(adjudication.contributors.map((contributor) => [
262
640
  contributor.contributor_id,
263
641
  contributor,
@@ -265,7 +643,15 @@ function renderConceptualAttributionSection(adjudication) {
265
643
  const lines = [
266
644
  "## Conceptual Review Attribution",
267
645
  "",
268
- `Round: \`${adjudication.round_id}\`. Contribution and modification percentages are judge-authored semantic estimates; tooling verified references, bounds, complete candidate coverage, and 100% contribution totals.`,
646
+ `Round: \`${adjudication.round_id}\`. Contribution and modification percentages are judge-authored semantic estimates; tooling verified references, bounds, complete candidate coverage, and 100% contribution totals. \`verification_status\` is likewise judge-authored — the tool enforces that a claim is present, consistent and published, never that it is true. Note that a conceptual finding's \`grounded\` verdict certifies component-path existence ONLY; the defect-presence claim is carried by \`verification_status\`, not by grounding.`,
647
+ "",
648
+ // The aggregate the live run had to be obtained by counting the artifact by
649
+ // hand. Both populations are CANDIDATE-scoped here and are not reconciled
650
+ // with the finding-scoped Summary counts: the post-adjudication grounding
651
+ // quarantine can drop a merged candidate's final finding, so they
652
+ // legitimately differ and each is labelled by its population.
653
+ `- Candidate dispositions: ${formatCountList(adjudication.candidate_disposition_breakdown)}`,
654
+ `- Candidate verification: ${formatCountList(adjudication.candidate_verification_status_breakdown)}`,
269
655
  "",
270
656
  ];
271
657
  for (const finalShare of adjudication.final_finding_shares) {
@@ -284,7 +670,7 @@ function renderConceptualAttributionSection(adjudication) {
284
670
  }
285
671
  lines.push("### Candidate dispositions", "");
286
672
  for (const disposition of adjudication.candidate_dispositions) {
287
- lines.push(`- \`${disposition.candidate_id}\` — **${disposition.disposition}**; modified ${disposition.modification_percent}%; targets ${disposition.target_final_finding_ids.join(", ") || "none"}. ${disposition.rationale}`);
673
+ lines.push(`- \`${disposition.candidate_id}\` — **${disposition.disposition}**; \`${disposition.verification_status}\`; modified ${disposition.modification_percent}%; targets ${disposition.target_final_finding_ids.join(", ") || "none"}. ${disposition.rationale}${disposition.verification_note ? ` _Verified:_ ${disposition.verification_note}` : ""}`);
288
674
  }
289
675
  lines.push("");
290
676
  return lines;
@@ -300,7 +686,12 @@ export function renderAuditReportMarkdown(report, options = {}) {
300
686
  }
301
687
  lines.push("## Summary", "", `- Findings: ${report.summary.finding_count}`, `- Work blocks: ${report.summary.work_block_count}`, `- Severity breakdown: ${formatSeverityList(report.summary.severity_breakdown)}`, ...(report.summary.lens_breakdown && Object.keys(report.summary.lens_breakdown).length > 0
302
688
  ? [`- Lens breakdown: ${formatCountList(report.summary.lens_breakdown)}`]
303
- : []), ...(report.summary.grounding_status_breakdown &&
689
+ : []),
690
+ // The line the breakdown structurally cannot carry: a selected lens with no
691
+ // findings has no key in a `countBy`, so it used to be invisible. Named
692
+ // here rather than in *Audit Limitations* (owner working assumption) — terse
693
+ // and always in front of the reader, beside the counts it qualifies.
694
+ ...renderUnexercisedLensLine(report.summary.lens_coverage), ...(report.summary.grounding_status_breakdown &&
304
695
  Object.keys(report.summary.grounding_status_breakdown).length > 0
305
696
  ? [
306
697
  `- Grounding (S7): ${formatCountList(report.summary.grounding_status_breakdown)}` +
@@ -315,6 +706,12 @@ export function renderAuditReportMarkdown(report, options = {}) {
315
706
  .filter(Boolean)
316
707
  .reduce((acc, note, i) => acc + (i === 0 ? " — " : "; ") + note, ""),
317
708
  ]
709
+ : []), ...(report.summary.verification_status_breakdown &&
710
+ Object.keys(report.summary.verification_status_breakdown).length > 0
711
+ ? [
712
+ `- Verification: ${formatCountList(report.summary.verification_status_breakdown)}` +
713
+ " — judge-authored claims about whether each defect is present at HEAD, not tool runs",
714
+ ]
318
715
  : []), `- Fully audited files: ${report.summary.audited_file_count}`, `- Excluded non-auditable files: ${report.summary.excluded_file_count}`, "");
319
716
  if (report.top_risks && report.top_risks.length > 0) {
320
717
  lines.push("## Top Risks", "");
@@ -401,8 +798,12 @@ export function renderAuditReportMarkdown(report, options = {}) {
401
798
  pushFindingBlock(finding, lines);
402
799
  }
403
800
  }
404
- const driftLines = renderSubmissionDriftSection(options.submission_ledger ?? []);
405
- lines.push(...renderConceptualAttributionSection(options.conceptual_adjudication));
801
+ const ledgerEvents = options.submission_ledger ?? [];
802
+ const driftLines = [
803
+ ...renderLaneDeliverySection(ledgerEvents),
804
+ ...renderSubmissionDriftSection(ledgerEvents),
805
+ ];
806
+ lines.push(...renderConceptualAttributionSection(report, options.conceptual_adjudication));
406
807
  const reflections = options.reflections ?? [];
407
808
  // Structural capability limitations are report limitations, not process
408
809
  // feedback. Keep the machine contract untouched: this only partitions the
@@ -410,15 +811,32 @@ export function renderAuditReportMarkdown(report, options = {}) {
410
811
  const capabilityLimitations = reflections.filter((reflection) => reflection.task_id === "audit-capability-preflight" &&
411
812
  (reflection.severity === "high" || reflection.severity === "critical"));
412
813
  const processFeedback = reflections.filter((reflection) => !capabilityLimitations.includes(reflection));
413
- const limitationLines = renderProcessFeedbackSection(capabilityLimitations);
414
- if (limitationLines.length > 0)
415
- limitationLines[0] = "## Audit Limitations";
814
+ // *Audit Limitations* states what this run could NOT do. It has two sources
815
+ // now, and the section is CREATED by either — it used to be the process-
816
+ // feedback heading renamed, so it existed only when a capability-preflight
817
+ // reflection happened to be present.
818
+ const analyzerLimitationLines = renderAnalyzerDegradationLines(options.analyzer_capability);
819
+ const reflectionLimitationLines = renderProcessFeedbackSection(capabilityLimitations);
820
+ if (reflectionLimitationLines.length > 0) {
821
+ reflectionLimitationLines[0] = "## Audit Limitations";
822
+ }
823
+ const limitationLines = analyzerLimitationLines.length > 0
824
+ ? [
825
+ "## Audit Limitations",
826
+ "",
827
+ ...analyzerLimitationLines,
828
+ // A reflection-sourced block that follows keeps its own body but must
829
+ // not re-open the heading it is now inside.
830
+ ...reflectionLimitationLines.slice(reflectionLimitationLines.length > 0 ? 2 : 0),
831
+ ]
832
+ : reflectionLimitationLines;
416
833
  const feedbackLines = renderProcessFeedbackSection(processFeedback);
834
+ const coverageLines = renderCharterCoverageSection(options.charter_evidence_coverage ?? [], options.charter_packet_archive ?? []);
417
835
  lines.push(...limitationLines, ...feedbackLines);
418
836
  if (driftLines.length > 0 && feedbackLines.length === 0) {
419
837
  lines.push("## Process Feedback", "");
420
838
  }
421
- lines.push(...driftLines);
839
+ lines.push(...driftLines, ...coverageLines);
422
840
  const excludedScope = options.intent_checkpoint?.excluded_scope ?? [];
423
841
  if (excludedScope.length > 0) {
424
842
  lines.push("## Excluded / Out-of-Scope", "");
@@ -453,6 +871,7 @@ export function renderAuditReportMarkdown(report, options = {}) {
453
871
  */
454
872
  export function normalizeExistingFindingsReport(report) {
455
873
  const groundingBreakdown = groundingStatusBreakdown(report.findings);
874
+ const verificationBreakdown = verificationStatusBreakdown(report.findings);
456
875
  return {
457
876
  ...report,
458
877
  contract_version: AUDIT_FINDINGS_CONTRACT_VERSION,
@@ -463,9 +882,23 @@ export function normalizeExistingFindingsReport(report) {
463
882
  work_block_count: report.work_blocks.length,
464
883
  severity_breakdown: severityBreakdown(report.findings),
465
884
  lens_breakdown: lensBreakdown(report.findings),
885
+ // The SELECTION is carried through — this function has no checkpoint and
886
+ // must not invent one — while the counts and outcomes are re-derived over
887
+ // the same findings `lens_breakdown` is re-counted from. Copying the map
888
+ // untouched beside a re-derived breakdown is how the two come to
889
+ // contradict each other, and the contradiction is refused downstream by
890
+ // `projectApprovedFindings`, which throws.
891
+ ...(report.summary.lens_coverage === undefined
892
+ ? {}
893
+ : {
894
+ lens_coverage: reprojectLensCoverage(report.summary.lens_coverage, report.findings),
895
+ }),
466
896
  ...(Object.keys(groundingBreakdown).length > 0
467
897
  ? { grounding_status_breakdown: groundingBreakdown }
468
898
  : {}),
899
+ ...(Object.keys(verificationBreakdown).length > 0
900
+ ? { verification_status_breakdown: verificationBreakdown }
901
+ : {}),
469
902
  },
470
903
  };
471
904
  }