@fro.bot/systematic 3.13.4 → 3.13.6

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
@@ -1988,7 +1988,10 @@ var MAX_FINDINGS = 32;
1988
1988
  var MAX_PERSONAS = 64;
1989
1989
  var REVIEW_ARTIFACT_CUSTOM_MESSAGES = [
1990
1990
  "severity count must match rejected finding count",
1991
- "filtered findings require a validation reason"
1991
+ "filtered findings require a validation reason",
1992
+ "risk-critical dispatches require a non-empty selection surface",
1993
+ "satisfied risk coverage requires a citing input finding ID",
1994
+ "unsatisfied risk coverage must not cite an input finding ID"
1992
1995
  ];
1993
1996
  var boundedText = (maxLength) => exports_external.string().min(1).max(maxLength).regex(/\S/);
1994
1997
  var DispatchOutcomeSchema = exports_external.enum([
@@ -2012,6 +2015,14 @@ var BranchSchema = exports_external.string().max(MAX_BRANCH_LENGTH);
2012
2015
  var HeadShaSchema = exports_external.string().regex(/^[0-9a-f]{40}$/);
2013
2016
  var CompletedAtSchema = exports_external.iso.datetime({ offset: false });
2014
2017
  var ReasonSchema = boundedText(MAX_REASON_LENGTH);
2018
+ var RISK_CRITICAL_PERSONAS = [
2019
+ "security",
2020
+ "data-migrations",
2021
+ "api-contract",
2022
+ "reliability",
2023
+ "performance"
2024
+ ];
2025
+ var RiskCriticalPersonaSchema = exports_external.enum(RISK_CRITICAL_PERSONAS);
2015
2026
  var FindingTitleSchema = boundedText(256);
2016
2027
  var SeveritySchema = exports_external.enum(["P0", "P1", "P2", "P3", "unknown"]);
2017
2028
  var FindingSeveritySchema = SeveritySchema.exclude(["unknown"]);
@@ -2098,8 +2109,18 @@ var DispatchSchema = exports_external.object({
2098
2109
  persona: ReviewerSchema,
2099
2110
  dispatch_outcome: DispatchOutcomeSchema,
2100
2111
  input_finding_count: exports_external.number().int().nonnegative().max(MAX_FINDINGS),
2101
- rejection_reason: ReasonSchema.optional()
2102
- }).strict();
2112
+ rejection_reason: ReasonSchema.optional(),
2113
+ selection_surface: exports_external.array(RepoRelativePathSchema).max(MAX_FINDINGS).optional(),
2114
+ selection_reason: ReasonSchema.optional()
2115
+ }).strict().superRefine((dispatch, ctx) => {
2116
+ if (RISK_CRITICAL_PERSONAS.includes(dispatch.persona) && (!dispatch.selection_surface || dispatch.selection_surface.length === 0)) {
2117
+ ctx.addIssue({
2118
+ code: "custom",
2119
+ path: ["selection_surface"],
2120
+ message: REVIEW_ARTIFACT_CUSTOM_MESSAGES[2]
2121
+ });
2122
+ }
2123
+ });
2103
2124
  var DispositionCountsSchema = exports_external.object({
2104
2125
  surviving: exports_external.number().int().nonnegative().max(MAX_FINDINGS * MAX_PERSONAS),
2105
2126
  merged: exports_external.number().int().nonnegative().max(MAX_FINDINGS * MAX_PERSONAS),
@@ -2116,6 +2137,31 @@ var CoverageSchema = exports_external.object({
2116
2137
  validator_failures: exports_external.array(ReasonSchema).max(MAX_PERSONAS),
2117
2138
  intent_uncertainty: exports_external.array(ReasonSchema).max(MAX_PERSONAS)
2118
2139
  }).strict();
2140
+ var DeclinedMergeSchema = exports_external.object({
2141
+ file: RepoRelativePathSchema,
2142
+ input_finding_ids: exports_external.array(boundedText(MAX_INPUT_ID_LENGTH)).min(2).max(MAX_FINDINGS),
2143
+ reason: ReasonSchema
2144
+ }).strict();
2145
+ var RiskCoverageSchema = exports_external.object({
2146
+ persona: RiskCriticalPersonaSchema,
2147
+ satisfied: exports_external.boolean(),
2148
+ input_finding_id: boundedText(MAX_INPUT_ID_LENGTH).optional()
2149
+ }).strict().superRefine((coverage, ctx) => {
2150
+ if (coverage.satisfied && coverage.input_finding_id === undefined) {
2151
+ ctx.addIssue({
2152
+ code: "custom",
2153
+ path: ["input_finding_id"],
2154
+ message: REVIEW_ARTIFACT_CUSTOM_MESSAGES[3]
2155
+ });
2156
+ }
2157
+ if (!coverage.satisfied && coverage.input_finding_id !== undefined) {
2158
+ ctx.addIssue({
2159
+ code: "custom",
2160
+ path: ["input_finding_id"],
2161
+ message: REVIEW_ARTIFACT_CUSTOM_MESSAGES[4]
2162
+ });
2163
+ }
2164
+ });
2119
2165
  var ReviewArtifactSchema = exports_external.object({
2120
2166
  schema_version: exports_external.literal(1),
2121
2167
  run_id: boundedText(MAX_RUN_ID_LENGTH),
@@ -2134,6 +2180,8 @@ var ReviewArtifactSchema = exports_external.object({
2134
2180
  dispatches: exports_external.array(DispatchSchema).max(MAX_PERSONAS),
2135
2181
  input_findings: exports_external.array(InputFindingSchema).max(MAX_FINDINGS * MAX_PERSONAS),
2136
2182
  findings: exports_external.array(SynthesizedFindingSchema).max(MAX_FINDINGS),
2183
+ declined_merges: exports_external.array(DeclinedMergeSchema).max(MAX_FINDINGS).optional(),
2184
+ risk_coverage: exports_external.array(RiskCoverageSchema).max(MAX_PERSONAS).optional(),
2137
2185
  disposition_counts: DispositionCountsSchema,
2138
2186
  applied_fixes: exports_external.array(ReasonSchema).max(MAX_FINDINGS),
2139
2187
  residual_actionable_work: exports_external.array(ReasonSchema).max(MAX_FINDINGS),
@@ -1,5 +1,5 @@
1
1
  import { z } from 'zod';
2
- export declare const REVIEW_ARTIFACT_CUSTOM_MESSAGES: readonly ['severity count must match rejected finding count', 'filtered findings require a validation reason'];
2
+ export declare const REVIEW_ARTIFACT_CUSTOM_MESSAGES: readonly ['severity count must match rejected finding count', 'filtered findings require a validation reason', 'risk-critical dispatches require a non-empty selection surface', 'satisfied risk coverage requires a citing input finding ID', 'unsatisfied risk coverage must not cite an input finding ID'];
3
3
  export declare const DispatchOutcomeSchema: z.ZodEnum<{
4
4
  empty: "empty";
5
5
  findings: "findings";
@@ -134,6 +134,8 @@ export declare const ReviewArtifactSchema: z.ZodObject<{
134
134
  }>;
135
135
  input_finding_count: z.ZodNumber;
136
136
  rejection_reason: z.ZodOptional<z.ZodString>;
137
+ selection_surface: z.ZodOptional<z.ZodArray<z.ZodString>>;
138
+ selection_reason: z.ZodOptional<z.ZodString>;
137
139
  }, z.core.$strict>>;
138
140
  input_findings: z.ZodArray<z.ZodDiscriminatedUnion<[z.ZodObject<{
139
141
  record_type: z.ZodLiteral<"admitted">;
@@ -218,6 +220,22 @@ export declare const ReviewArtifactSchema: z.ZodObject<{
218
220
  agreement_credit: string[];
219
221
  };
220
222
  }>>>;
223
+ declined_merges: z.ZodOptional<z.ZodArray<z.ZodObject<{
224
+ file: z.ZodString;
225
+ input_finding_ids: z.ZodArray<z.ZodString>;
226
+ reason: z.ZodString;
227
+ }, z.core.$strict>>>;
228
+ risk_coverage: z.ZodOptional<z.ZodArray<z.ZodObject<{
229
+ persona: z.ZodEnum<{
230
+ "api-contract": "api-contract";
231
+ "data-migrations": "data-migrations";
232
+ performance: "performance";
233
+ reliability: "reliability";
234
+ security: "security";
235
+ }>;
236
+ satisfied: z.ZodBoolean;
237
+ input_finding_id: z.ZodOptional<z.ZodString>;
238
+ }, z.core.$strict>>>;
221
239
  disposition_counts: z.ZodObject<{
222
240
  surviving: z.ZodNumber;
223
241
  merged: z.ZodNumber;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fro.bot/systematic",
3
- "version": "3.13.4",
3
+ "version": "3.13.6",
4
4
  "description": "Compound-engineering loops for OpenCode, Pi, and Claude Code",
5
5
  "type": "module",
6
6
  "homepage": "https://fro.bot/systematic",
@@ -360,6 +360,10 @@ Review team:
360
360
 
361
361
  This is progress reporting, not a blocking confirmation.
362
362
 
363
+ Record each conditional persona's selection reason and triggering file paths
364
+ on its dispatch record; see the [synthesis artifact contract](./references/synthesis-artifact-contract.md)
365
+ for the field semantics.
366
+
363
367
  ### Stage 3b: Discover project standards paths
364
368
 
365
369
  Before spawning sub-agents, find the file paths (not contents) of all relevant standards files for the `project-standards` persona. Use the native file-search/glob tool to locate:
@@ -468,8 +472,10 @@ Before applying the confidence gate, keep the parent-owned ledger through every
468
472
  - **Dispatch outcome:** Record the parent-owned dispatch outcomes and ledger dispositions according to the [synthesis artifact contract](./references/synthesis-artifact-contract.md); keep dispatch outcomes separate from finding dispositions.
469
473
  - **Rejection policy: degrade, do not fail the whole review.** Continue merging conforming returns when a persona or finding is rejected; record the rejection and apply the risk-aware verdict according to the [synthesis artifact contract](./references/synthesis-artifact-contract.md). If every persona fails or times out, use the existing degraded-review behavior.
470
474
  2. **Confidence gate.** Suppress findings below 0.60 confidence. Exception: P0 findings at 0.50+ confidence survive the gate -- critical-but-uncertain issues must not be silently dropped. Record the suppressed finding's original confidence and an explicit reason in the input ledger. A retained P0 at 0.50+ is recorded as `surviving` unless it later participates in a deduplication merge. This matches the persona instructions and the schema's confidence thresholds.
471
- 3. **Deduplicate.** Compute fingerprint: `normalize(file) + line_bucket(line, +/-3) + normalize(title)`. When fingerprints match, merge: keep highest severity, keep highest confidence, preserve the exact fingerprint, and retain the input IDs that produced the merged entry. A singleton that passes the gate is `surviving`; each input in a multi-input merge is provisionally `merged`.
472
- 4. **Cross-reviewer agreement.** When 2+ independent reviewers flag the same issue (same fingerprint), boost the merged confidence by 0.10 (capped at 1.0). Cross-reviewer agreement is strong signal -- independent reviewers converging on the same issue is more reliable than any single reviewer's confidence. Preserve the distinction in the merged finding's artifact provenance according to the [synthesis artifact contract](./references/synthesis-artifact-contract.md).
475
+ 3. **Deduplicate.** Group all gated findings by `normalize(file)`. A file with two or more findings from different personas forms a candidate group. Do not use line number to form groups. Sort findings within each group by line. Adjudicate each candidate group: merge findings judged to describe the same underlying defect; keep genuinely different defects separate. Adjacency creates a candidate, not a conclusion -- findings on the same line that describe different defects must not merge. For each resulting finding, derive the fingerprint from its file and line as `normalize(file) + "|" + line`. Keep highest severity, keep highest confidence, and retain the input IDs that produced the merged entry. Record each declined merge in the artifact's optional `declined_merges` field with the normalized file, input finding IDs considered but not merged, and a brief reason. A singleton that passes the gate is `surviving`; each input in a multi-input merge is provisionally `merged`.
476
+
477
+ Worked example: at `src/lib/model-availability.ts:139`, reliability's `Config hook awaits providers API without a timeout` and adversarial's `Config startup can hang forever behind a stalled /config` describe the same underlying defect in different words, so they merge.
478
+ 4. **Cross-reviewer agreement.** When an adjudicated merge contains findings from 2+ independent reviewers, boost the merged confidence by 0.10 (capped at 1.0). Cross-reviewer agreement is strong signal -- independent reviewers converging on the same issue is more reliable than any single reviewer's confidence. Preserve the distinction in the merged finding's artifact provenance according to the [synthesis artifact contract](./references/synthesis-artifact-contract.md).
473
479
  5. **Separate pre-existing.** Pull out findings with `pre_existing: true` into a separate list.
474
480
  6. **Resolve disagreements.** When reviewers flag the same code region but disagree on severity, autofix_class, or owner, annotate the Reviewer column with the disagreement (e.g., "security (P0), correctness (P1) -- kept P0"). This transparency helps the user understand why a finding was routed the way it was.
475
481
  7. **Normalize routing.** For each merged finding, set the final `autofix_class`, `owner`, and `requires_verification`. If reviewers disagree, keep the most conservative route. Synthesis may narrow a finding from `safe_auto` to `gated_auto` or `manual`, but must not widen it without new evidence.
@@ -518,8 +524,8 @@ Assemble the final report using **pipe-delimited markdown tables for findings**
518
524
  8. **Learnings & Past Solutions.** Surface learnings-researcher results: if past solutions are relevant, flag them as "Known Pattern" with links to docs/solutions/ files.
519
525
  9. **Agent-Native Gaps.** Surface agent-native-reviewer results. Omit section if no gaps found.
520
526
  10. **Deployment Notes.** If deployment-verification-agent ran, surface the key Go/No-Go items: blocking pre-deploy checks, the most important verification queries, rollback caveats, and monitoring focus areas. Keep the checklist actionable rather than dropping it into Coverage.
521
- 11. **Coverage.** Suppressed count, residual risks, testing gaps, failed/timed-out reviewers, validator failures, and any intent uncertainty carried by non-interactive modes.
522
- 12. **Verdict.** Ready to merge / Ready with fixes / Not ready. Fix order if applicable. When an `explicit` plan has unaddressed requirements, the verdict must reflect it — a PR that's code-clean but missing planned requirements is "Not ready" unless the omission is intentional. When an `inferred` plan has unaddressed requirements, note it in the verdict reasoning but do not block on it alone. Apply the risk-aware degraded verdict rule from the [synthesis artifact contract](./references/synthesis-artifact-contract.md).
527
+ 11. **Coverage.** Suppressed count, residual risks, testing gaps, failed/timed-out reviewers, validator failures, risk-coverage entries with citing input finding IDs and exit conditions for blocked entries, and any intent uncertainty carried by non-interactive modes.
528
+ 12. **Verdict.** Ready to merge / Ready with fixes / Not ready. Fix order if applicable. When an `explicit` plan has unaddressed requirements, the verdict must reflect it — a PR that's code-clean but missing planned requirements is "Not ready" unless the omission is intentional. When an `inferred` plan has unaddressed requirements, note it in the verdict reasoning but do not block on it alone. Apply the risk-aware degraded verdict rule from the [synthesis artifact contract](./references/synthesis-artifact-contract.md), including the recorded exit condition for a blocked risk-critical verdict.
523
529
 
524
530
  Do not include time estimates.
525
531
 
@@ -596,7 +602,7 @@ Review complete
596
602
 
597
603
  **Detail enrichment (headless only):** The headless envelope includes `Why:`, `Evidence:`, and `Suggested fix:` lines. After merge (Stage 5), use the validated full persona returns retained in parent memory for only the findings that survived dedup and confidence gating.
598
604
  - **Field tiers:** `Why:` and `Evidence:` are detail-tier and are already present in the validated inline return. `Suggested fix:` is also available directly from that return and survives merge as optional fix context.
599
- - **In-memory matching:** For each surviving finding, look up its detail-tier fields in the validated returns of the contributing reviewers. Match on `file + line_bucket(line, +/-3)` (the same tolerance used in Stage 5 dedup). When multiple entries fall within the line bucket, apply `normalize(title)` to the merged finding's title and each candidate entry's title as a tie-breaker.
605
+ - **In-memory matching:** For each surviving finding, look up its detail-tier fields in the validated returns of the contributing reviewers. Use the merged finding's `input_finding_ids` (`<reviewer>#<1-based finding index>`) to identify the contributing return and source finding. When an input ID cannot be resolved or multiple candidates remain, match on normalized `file`, then use line and `normalize(title)` only as tie-breakers.
600
606
  - **Reviewer order:** Try contributing reviewers in the order they appear in the merged finding's reviewer list; use the first validated match.
601
607
  - **No-match fallback:** If no validated in-memory return contains a match, omit the `Why:` and `Evidence:` lines for that finding and note the gap in Coverage. This should indicate a synthesis/matching gap, not a failed artifact-file write. Never re-read per-agent files to recover detail.
602
608
 
@@ -137,7 +137,7 @@ This fails because: no pipe-delimited tables, no severity-grouped `###` headers,
137
137
  - **Learnings & Past Solutions section** -- results from learnings-researcher, with links to docs/solutions/ files
138
138
  - **Agent-Native Gaps section** -- results from agent-native-reviewer. Omit if no gaps found.
139
139
  - **Deployment Notes section** -- key checklist items from deployment-verification-agent. Omit if the agent did not run.
140
- - **Coverage section** -- suppressed count with original confidences, residual risks, testing gaps, failed reviewers, and disposition reconciliation
140
+ - **Coverage section** -- suppressed count with original confidences, residual risks, testing gaps, failed reviewers, disposition reconciliation, and risk-coverage entries with their citing input finding IDs and blocked-entry exit conditions
141
141
  - **Summary uses blockquotes** for verdict, reasoning, and fix order
142
142
  - **Horizontal rule** (`---`) separates findings from verdict
143
143
  - **`###` headers** for each section -- never plain text headers
@@ -69,6 +69,31 @@
69
69
  "minLength": 1,
70
70
  "maxLength": 2048,
71
71
  "pattern": "\\S"
72
+ },
73
+ "selection_surface": {
74
+ "maxItems": 32,
75
+ "type": "array",
76
+ "items": {
77
+ "type": "string",
78
+ "minLength": 1,
79
+ "maxLength": 256,
80
+ "allOf": [
81
+ {
82
+ "type": "string",
83
+ "pattern": "\\S"
84
+ },
85
+ {
86
+ "type": "string",
87
+ "pattern": "^(?!\\/)(?![A-Za-z]:[\\\\/])(?!\\\\).+"
88
+ }
89
+ ]
90
+ }
91
+ },
92
+ "selection_reason": {
93
+ "type": "string",
94
+ "minLength": 1,
95
+ "maxLength": 2048,
96
+ "pattern": "\\S"
72
97
  }
73
98
  },
74
99
  "required": ["persona", "dispatch_outcome", "input_finding_count"],
@@ -374,6 +399,79 @@
374
399
  "additionalProperties": false
375
400
  }
376
401
  },
402
+ "declined_merges": {
403
+ "maxItems": 32,
404
+ "type": "array",
405
+ "items": {
406
+ "type": "object",
407
+ "properties": {
408
+ "file": {
409
+ "type": "string",
410
+ "minLength": 1,
411
+ "maxLength": 256,
412
+ "allOf": [
413
+ {
414
+ "type": "string",
415
+ "pattern": "\\S"
416
+ },
417
+ {
418
+ "type": "string",
419
+ "pattern": "^(?!\\/)(?![A-Za-z]:[\\\\/])(?!\\\\).+"
420
+ }
421
+ ]
422
+ },
423
+ "input_finding_ids": {
424
+ "minItems": 2,
425
+ "maxItems": 32,
426
+ "type": "array",
427
+ "items": {
428
+ "type": "string",
429
+ "minLength": 1,
430
+ "maxLength": 128,
431
+ "pattern": "\\S"
432
+ }
433
+ },
434
+ "reason": {
435
+ "type": "string",
436
+ "minLength": 1,
437
+ "maxLength": 2048,
438
+ "pattern": "\\S"
439
+ }
440
+ },
441
+ "required": ["file", "input_finding_ids", "reason"],
442
+ "additionalProperties": false
443
+ }
444
+ },
445
+ "risk_coverage": {
446
+ "maxItems": 64,
447
+ "type": "array",
448
+ "items": {
449
+ "type": "object",
450
+ "properties": {
451
+ "persona": {
452
+ "type": "string",
453
+ "enum": [
454
+ "security",
455
+ "data-migrations",
456
+ "api-contract",
457
+ "reliability",
458
+ "performance"
459
+ ]
460
+ },
461
+ "satisfied": {
462
+ "type": "boolean"
463
+ },
464
+ "input_finding_id": {
465
+ "type": "string",
466
+ "minLength": 1,
467
+ "maxLength": 128,
468
+ "pattern": "\\S"
469
+ }
470
+ },
471
+ "required": ["persona", "satisfied"],
472
+ "additionalProperties": false
473
+ }
474
+ },
377
475
  "disposition_counts": {
378
476
  "type": "object",
379
477
  "properties": {
@@ -78,12 +78,19 @@ The artifact must preserve these distinctions:
78
78
  "title": "<merged finding>",
79
79
  "input_finding_ids": ["correctness#2", "testing#1"],
80
80
  "provenance": {
81
- "fingerprint": "<normalize(file) + line_bucket(line, +/-3) + normalize(title)>",
81
+ "fingerprint": "<normalize(file) + \"|\" + line>",
82
82
  "submitters": ["correctness", "testing"],
83
83
  "agreement_credit": []
84
84
  }
85
85
  }
86
86
  ],
87
+ "declined_merges": [
88
+ {
89
+ "file": "src/example.ts",
90
+ "input_finding_ids": ["correctness#3", "testing#2"],
91
+ "reason": "The findings concern separate validation paths."
92
+ }
93
+ ],
87
94
  "disposition_counts": {
88
95
  "surviving": 0,
89
96
  "merged": 2,
@@ -98,7 +105,10 @@ The artifact must preserve these distinctions:
98
105
  records what a persona returned: `findings`, `empty`, `malformed`, or
99
106
  `never_returned`. A rejection reason is the exact safe validation reason,
100
107
  naming persona and field without echoing the offending value. Dispatch
101
- outcome is separate from finding disposition.
108
+ outcome is separate from finding disposition. Conditional selections record
109
+ their triggering changed-file paths in `selection_surface` and the announced
110
+ selection explanation in `selection_reason`; always-on personas may omit
111
+ both fields.
102
112
  - `input_findings` is the authoritative parent-owned ledger. Before the
103
113
  confidence gate, every admitted finding receives an `input_id` of
104
114
  `<reviewer>#<1-based finding index>`. Every admitted input has exactly one
@@ -119,12 +129,24 @@ The artifact must preserve these distinctions:
119
129
  finding has zero ledger entries, not a fabricated finding. Never include the
120
130
  offending value in a rejection reason.
121
131
  - Synthesized and filtered findings retain their original fields plus
122
- `input_finding_ids` and provenance. Provenance contains the exact dedup
123
- fingerprint `normalize(file) + line_bucket(line, +/-3) + normalize(title)`,
124
- `submitters`, and `agreement_credit` arrays.
125
- - `submitters` contains only personas with an input finding in the merged
126
- fingerprint group. `agreement_credit` contains only personas credited by the
127
- cross-reviewer agreement boost without an input finding in that group. A
132
+ `input_finding_ids` and provenance. Provenance contains the fingerprint
133
+ `normalize(file) + "|" + line`, derived from the merged finding's file and
134
+ line, plus `submitters` and `agreement_credit` arrays.
135
+ - `declined_merges` is optional. For each candidate group that is not merged,
136
+ record the normalized file, the input finding IDs considered and not merged,
137
+ and a brief reason. Candidate groups contain two or more findings from
138
+ different personas on the same normalized file; sort their findings by line.
139
+ Adjacency creates a candidate, not a conclusion: genuinely different
140
+ defects on the same line remain separate.
141
+ - A candidate group may resolve partially. When some findings in a group merge
142
+ and others stay separate, record one entry per declined separation, listing
143
+ the input finding IDs on both sides of it. A group of three where two merge
144
+ and one stays separate records a single entry naming all three IDs, with a
145
+ reason describing why the third is a different defect. Record the separation,
146
+ not the merge; a fully merged group produces no entry.
147
+ - `submitters` contains only personas with an input finding in the adjudicated
148
+ merge. `agreement_credit` contains only personas credited by the
149
+ cross-reviewer agreement boost without an input finding in that merge. A
128
150
  persona returning zero findings never appears in `submitters`; do not infer
129
151
  submission from the report's Reviewer column.
130
152
  - A `filtered` finding remains available for human review with the validator's
@@ -227,18 +249,20 @@ The risk-critical surfaces are `security`, `data-migrations`, `api-contract`,
227
249
  specifically for the matching diff shape in Stage 3. If one of those selected
228
250
  personas has `dispatch_outcome: "malformed"` or
229
251
  `dispatch_outcome: "never_returned"`, the review verdict must not be clean:
230
- it is blocking unless another persona covered the same surface and returned
231
- validated evidence for it. For this rule, validated evidence means at least
252
+ it is blocking unless another persona covered the lost surface with validated
253
+ evidence. For this rule, a validated finding from another persona covers a
254
+ lost risk-critical surface if and only if the finding's `file` appears in the
255
+ lost persona's recorded `selection_surface`; validated evidence means at least
232
256
  one finding from that other persona's return passed complete schema and
233
- environment-value validation and is relevant to the same surface. A coverage
234
- note alone cannot satisfy this rule; the verdict must reflect the missing
235
- risk-critical evidence.
257
+ environment-value validation. A coverage note alone cannot satisfy this rule;
258
+ the verdict must reflect the missing risk-critical evidence.
236
259
 
237
260
  Finding-level rejection is keyed by the severities in
238
261
  `rejected_severities`. A selected risk-critical persona whose rejected
239
262
  findings include any `P0`, `P1`, or `unknown` severity is treated exactly as a
240
263
  rejected persona for this verdict rule: blocking unless another persona
241
- covered the same surface with validated evidence. A selected risk-critical
264
+ covered the lost surface with a validated finding whose `file` appears in the
265
+ lost persona's recorded `selection_surface`. A selected risk-critical
242
266
  persona whose rejected findings are only `P2` or `P3` does not block on that
243
267
  basis alone; record it in the Coverage section instead. Unknown severity is
244
268
  treated as blocking as deliberate fail-closed behavior because the parent
@@ -246,3 +270,12 @@ could not determine what was lost. Admitted findings and verdict blocking
246
270
  are independent: surviving findings from the same return continue through
247
271
  synthesis normally. Partial return is not partial coverage when the lost
248
272
  part was critical.
273
+
274
+ Record each lost risk-critical persona in the optional `risk_coverage` array
275
+ with `satisfied: true` and `input_finding_id` set to the citing input finding
276
+ ID when coverage is satisfied, or `satisfied: false` without an
277
+ `input_finding_id` otherwise. A blocked verdict must name its exit condition
278
+ in the report: re-run the lost persona, or supply a validated finding from
279
+ another persona whose `file` appears in the lost persona's recorded
280
+ `selection_surface`. Derive that condition from the dispatch and risk-coverage
281
+ records, not from a coverage note.