@jphutchins/code-review 0.1.0-alpha.53 → 0.1.0-alpha.55

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jphutchins/code-review",
3
- "version": "0.1.0-alpha.53",
3
+ "version": "0.1.0-alpha.55",
4
4
  "description": "Deterministic commenter for agentic PR review — gather, render, inline, post, adapt, extract, cost, validate, print-schema",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -54,7 +54,10 @@ newer version ships. This is distinct from the git-tag `$id` mechanism above:
54
54
  - When a version stops being latest, it is **frozen** to `schema/v<major.minor>/<kind>.schema.json`
55
55
  with its `$id` pinned to that version's schema-release tag
56
56
  (`https://raw.githubusercontent.com/JPHutchins/code-review/schema-v<version>/schema/<kind>.schema.json`).
57
- A frozen copy is never edited again.
57
+ A frozen copy is never edited again — with ONE deliberate exception: the frozen `v0.9` copy also
58
+ accepts the tolerant-in `id` spellings (`id`, `finding_ids`, `ids`) its legacy codec accepts, so
59
+ the ajv gate and the codec gate agree on the hybrid doc a mid-transition reviewer produces (new
60
+ field, old version stamp).
58
61
 
59
62
  The CLI's registry (`src/registry.ts`) maps each supported `major.minor` to its bundled file, codec,
60
63
  and upcast normalizer. Dispatch is by the `major.minor` of the document's `schema_version` (patch is
@@ -70,7 +73,8 @@ ignored); a version outside the supported set degrades to a §5.5 sticky notice.
70
73
  | `v0.4.0` | superseded | Breaking: renames finding `body` → `description`; makes `reasoning` and `confidence` **required**; adds optional `recommendation` (prose fix); removes the free-text `suggestion` field (a `patch`, now `string \| null`, is the sole mechanical fix, projected into a suggestion by the commenter). |
71
74
  | `v0.5.0` | superseded | Widens the `verdict` enum with a pipeline-reserved `error` value: a run that produced no verdict about the diff (operational failure or security refusal) now carries `verdict: "error"` with `findings: []`, so its machine-readable blob is no longer byte-identical to a clean pass. Backwards-compatible (a `0.4` document is a valid `0.5` document); the CLI keeps resolving `0.4` via an identity upcast, so a sticky embedded by a `0.4` CLI still seeds a re-review. |
72
75
  | `v0.6.0` | superseded | Adds optional `systemic_problems` — an array of cross-cutting observations that tie findings together and are hard to express with a line range, each item with required `title`/`description`/`severity`/`reasoning`/`confidence` and optional `code`/`code_url`/`finding_codes`/`paths` (no line anchors). Refocuses `summary` on justifying the overall verdict rather than restating findings. Backwards-compatible (a `0.5` document is a valid `0.6` document); the CLI keeps resolving `0.4`/`0.5` via identity upcasts. |
73
- | `v0.9.0` | **current** | Adds a REQUIRED `likelihood` (0..1) to every finding and systemic problem (issue #163): the probability the triggering input/state actually occurs — distinct from `confidence` (whether the defect is real) — folded into the convergence score as a second multiplier. **Breaking**: a pre-0.9 document lacks `likelihood` and no longer validates, so a stale sticky degrades to a sentinel-only seed (one cold re-review) rather than seeding. The draft axis skips `0.7`/`0.8`: those are the surface-signal axis's versions (below), so the draft jumps past them to keep the two version spaces distinct — a draft must never be mistaken for a legacy surfaced blob (issue #156). |
76
+ | `v0.9.0` | superseded (frozen at `schema/v0.9/`) | Adds a REQUIRED `likelihood` (0..1) to every finding and systemic problem (issue #163): the probability the triggering input/state actually occurs — distinct from `confidence` (whether the defect is real) — folded into the convergence score as a second multiplier. **Breaking**: a pre-0.9 document lacks `likelihood` and no longer validates, so a stale sticky degrades to a sentinel-only seed (one cold re-review) rather than seeding. The draft axis skips `0.7`/`0.8`: those are the surface-signal axis's versions (below), so the draft jumps past them to keep the two version spaces distinct — a draft must never be mistaken for a legacy surfaced blob (issue #156). |
77
+ | `v0.10.0` | **current** | Renames the mechanism identifier `code` → `id` and makes it **required** on every finding (issue #246), so every finding is always trackable across rounds and discussions. Systemic problems rename `code`/`finding_codes` → `id`/`finding_ids`; convergence rounds rename `codes` → `ids`; `scope_metastasis.recurring` renames `code` → `id`. `change_size.code` (the role bucket) and `code_url` keep their names. **Breaking**: a pre-0.10 document lacks `id` — the registry upcasts every pre-0.10 minor through one tolerant legacy codec (`code` → `id`, or a synthesized content-derived id — sha256 of path+title — when a finding carried none), so a legacy sticky still seeds a re-review with its ids intact, and the pre-0.10 shape stays frozen at `schema/v0.9/` (above). |
74
78
 
75
79
  ### Surface channel (stop signal)
76
80
 
@@ -58,21 +58,21 @@
58
58
  "maximum": 1,
59
59
  "description": "Set to 1 for a systemic problem. A structural/cross-cutting observation has no single triggering input — it is definitionally always present — so the convergence score treats a systemic problem's likelihood as 1 and never discounts it. (Required for schema symmetry with a finding's likelihood; the written value is not read by the score.)"
60
60
  },
61
- "code": {
61
+ "id": {
62
62
  "type": "string",
63
- "description": "Stable rule identifier (e.g. \"repeated-null-check\") for rule-based filtering, suppression, and cross-run dedup — mirrors the finding field of the same name."
63
+ "description": "Stable mechanism identifier (e.g. \"repeated-null-check\") for rule-based filtering, suppression, and cross-run dedup — mirrors the finding field of the same name."
64
64
  },
65
65
  "code_url": {
66
66
  "type": "string",
67
67
  "format": "uri",
68
- "description": "URL documenting the rule named by code."
68
+ "description": "URL documenting the rule named by id."
69
69
  },
70
- "finding_codes": {
70
+ "finding_ids": {
71
71
  "type": "array",
72
72
  "items": {
73
73
  "type": "string"
74
74
  },
75
- "description": "The `code` values of the findings this systemic problem ties together, when those findings carry codes."
75
+ "description": "The `id` values of the findings this systemic problem ties together."
76
76
  },
77
77
  "paths": {
78
78
  "type": "array",
@@ -87,7 +87,7 @@
87
87
  "scope_metastasis": {
88
88
  "type": "object",
89
89
  "additionalProperties": false,
90
- "description": "Advisory (issue #150): the mechanisms whose findings recurred in consecutive prior full-review rounds — per-code consecutive-round counts plus a decision prompt. The review agent does NOT write this field: the commenter re-derives it from the carried round history and delivers it in the prior review context of a re-review. When it is present, respond to the decision prompt — state in your summary whether you are committing to the expanding scope (planning the remaining facets of the recurring mechanism(s) as one unit) or narrowing the scope — so the decision is made explicitly rather than left to emerge piecemeal.",
90
+ "description": "Advisory (issue #150): the mechanisms whose findings recurred in consecutive prior full-review rounds — per-id consecutive-round counts plus a decision prompt. The review agent does NOT write this field: the commenter re-derives it from the carried round history and delivers it in the prior review context of a re-review. When it is present, respond to the decision prompt — state in your summary whether you are committing to the expanding scope (planning the remaining facets of the recurring mechanism(s) as one unit) or narrowing the scope — so the decision is made explicitly rather than left to emerge piecemeal.",
91
91
  "required": ["decision_prompt", "recurring"],
92
92
  "properties": {
93
93
  "decision_prompt": {
@@ -96,21 +96,21 @@
96
96
  },
97
97
  "recurring": {
98
98
  "type": "array",
99
- "description": "The mechanisms flagged: one entry per code whose findings recurred in `consecutive_rounds` or more consecutive completed full-review rounds.",
99
+ "description": "The mechanisms flagged: one entry per id whose findings recurred in `consecutive_rounds` or more consecutive completed full-review rounds.",
100
100
  "items": {
101
101
  "type": "object",
102
102
  "additionalProperties": false,
103
- "required": ["code", "consecutive_rounds", "start_round"],
103
+ "required": ["id", "consecutive_rounds", "start_round"],
104
104
  "properties": {
105
- "code": {
105
+ "id": {
106
106
  "type": "string",
107
- "description": "The finding code of the recurring mechanism."
107
+ "description": "The finding id of the recurring mechanism."
108
108
  },
109
109
  "consecutive_rounds": {
110
110
  "type": "integer",
111
111
  "minimum": 1,
112
112
  "maximum": 9007199254740991,
113
- "description": "How many consecutive completed full-review rounds (ending at the last completed round) carried a finding with this code. Same-head CI retries are collapsed by the streak detector, so this is the de-duplicated streak, not the raw span: it may be less than `round - start_round + 1` when the history contains retries."
113
+ "description": "How many consecutive completed full-review rounds (ending at the last completed round) carried a finding with this id. Same-head CI retries are collapsed by the streak detector, so this is the de-duplicated streak, not the raw span: it may be less than `round - start_round + 1` when the history contains retries."
114
114
  },
115
115
  "start_round": {
116
116
  "type": "integer",
@@ -158,10 +158,10 @@
158
158
  "type": "number",
159
159
  "description": "That round's convergence score, carried verbatim from when the round completed. Absent only for a round migrated from a pre-#174 sticky, which stored no score."
160
160
  },
161
- "codes": {
161
+ "ids": {
162
162
  "type": "object",
163
163
  "additionalProperties": { "type": "integer", "minimum": 0 },
164
- "description": "That round's mechanism-frequency map: finding code -> number of findings carrying it. Feeds the same-mechanism and scope-metastasis recurrence signals."
164
+ "description": "That round's mechanism-frequency map: finding id -> number of findings carrying it. Feeds the same-mechanism and scope-metastasis recurrence signals."
165
165
  },
166
166
  "sha": {
167
167
  "type": "string",
@@ -213,6 +213,7 @@
213
213
  "type": "object",
214
214
  "additionalProperties": false,
215
215
  "required": [
216
+ "id",
216
217
  "path",
217
218
  "start_line",
218
219
  "end_line",
@@ -249,14 +250,14 @@
249
250
  "enum": ["critical", "major", "minor", "nit"],
250
251
  "description": "Used for grouping and for folding nits into a collapsed section."
251
252
  },
252
- "code": {
253
+ "id": {
253
254
  "type": "string",
254
- "description": "Stable rule identifier (e.g. \"null-check-missing\") for rule-based filtering, suppression, and cross-run dedup."
255
+ "description": "Stable mechanism identifier (e.g. \"null-check-missing\") REQUIRED so every finding is trackable across rounds and discussions. Two findings share an id exactly when they are the same mechanism; a re-raise reuses the id."
255
256
  },
256
257
  "code_url": {
257
258
  "type": "string",
258
259
  "format": "uri",
259
- "description": "URL documenting the rule named by code."
260
+ "description": "URL documenting the rule named by id."
260
261
  },
261
262
  "title": {
262
263
  "type": "string",
@@ -0,0 +1,418 @@
1
+ {
2
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
3
+ "$id": "https://raw.githubusercontent.com/JPHutchins/code-review/schema-v0.9.0/schema/findings.schema.json",
4
+ "title": "Code Review \u2014 findings",
5
+ "description": "Structured output of an agentic PR code review. The review agent emits this object (e.g. via a coding-agent CLI's structured-output mode, such as Claude Code's `--json-schema`, whose result lands in `.structured_output`, or a file).",
6
+ "type": "object",
7
+ "additionalProperties": false,
8
+ "required": [
9
+ "schema_version",
10
+ "summary",
11
+ "verdict",
12
+ "findings"
13
+ ],
14
+ "properties": {
15
+ "schema_version": {
16
+ "type": "string",
17
+ "pattern": "^(0|[1-9]\\d*)\\.(\\d+)\\.(\\d+)(?:-[0-9A-Za-z-]+(?:\\.[0-9A-Za-z-]+)*)?(?:\\+[0-9A-Za-z-]+(?:\\.[0-9A-Za-z-]+)*)?$",
18
+ "description": "Semver version of this findings schema the object conforms to (e.g. \"0.1.0\")."
19
+ },
20
+ "summary": {
21
+ "type": "string",
22
+ "description": "1-4 sentence markdown justification of the overall verdict \u2014 the shape of the review and why the verdict follows from it. Do not restate individual findings (those live in `findings`) or cross-cutting observations (those live in `systemic_problems`); the summary explains the verdict, the arrays carry the evidence."
23
+ },
24
+ "verdict": {
25
+ "type": "string",
26
+ "enum": [
27
+ "approve",
28
+ "comment",
29
+ "changes",
30
+ "error"
31
+ ],
32
+ "description": "Overall stance. The review agent emits one of `approve`/`comment`/`changes`. `error` is reserved for the pipeline: it marks a run that produced no verdict about the diff (an operational failure, a security refusal, or an empty diff with nothing to review \u2014 always with `findings: []`), so a consumer switching on `verdict` gets a case it does not recognise rather than a false clean pass."
33
+ },
34
+ "systemic_problems": {
35
+ "type": "array",
36
+ "description": "Zero or more cross-cutting observations that tie several findings together or describe a pattern spanning the change, and are hard to express with a single line range. Omit the field when there are none; the summary must not restate them.",
37
+ "items": {
38
+ "type": "object",
39
+ "additionalProperties": false,
40
+ "required": [
41
+ "title",
42
+ "description",
43
+ "severity",
44
+ "reasoning",
45
+ "confidence",
46
+ "likelihood"
47
+ ],
48
+ "properties": {
49
+ "title": {
50
+ "type": "string",
51
+ "description": "One-line summary of the systemic problem."
52
+ },
53
+ "description": {
54
+ "type": "string",
55
+ "description": "1-3 sentence markdown explanation of the pattern and why it matters \u2014 how it ties the related findings together. Details belong in the individual findings' `reasoning`."
56
+ },
57
+ "severity": {
58
+ "type": "string",
59
+ "enum": [
60
+ "critical",
61
+ "major",
62
+ "minor",
63
+ "nit"
64
+ ],
65
+ "description": "Overall severity of the systemic problem \u2014 the severity of the pattern itself, not of any single finding. It does not count toward the findings severity histogram."
66
+ },
67
+ "reasoning": {
68
+ "type": "string",
69
+ "description": "Rationale/evidence for why the systemic problem holds, so a human or downstream agent can judge its soundness \u2014 distinct from `description`, which explains the pattern to the reader. Mirrors the finding field of the same name."
70
+ },
71
+ "confidence": {
72
+ "type": "number",
73
+ "minimum": 0,
74
+ "maximum": 1,
75
+ "description": "0..1 confidence in the systemic problem itself. Mirrors the finding field of the same name."
76
+ },
77
+ "likelihood": {
78
+ "type": "number",
79
+ "minimum": 0,
80
+ "maximum": 1,
81
+ "description": "Set to 1 for a systemic problem. A structural/cross-cutting observation has no single triggering input \u2014 it is definitionally always present \u2014 so the convergence score treats a systemic problem's likelihood as 1 and never discounts it. (Required for schema symmetry with a finding's likelihood; the written value is not read by the score.)"
82
+ },
83
+ "code": {
84
+ "type": "string",
85
+ "description": "Stable rule identifier (e.g. \"repeated-null-check\") for rule-based filtering, suppression, and cross-run dedup \u2014 mirrors the finding field of the same name."
86
+ },
87
+ "code_url": {
88
+ "type": "string",
89
+ "format": "uri",
90
+ "description": "URL documenting the rule named by code."
91
+ },
92
+ "finding_codes": {
93
+ "type": "array",
94
+ "items": {
95
+ "type": "string"
96
+ },
97
+ "description": "The `code` values of the findings this systemic problem ties together, when those findings carry codes."
98
+ },
99
+ "paths": {
100
+ "type": "array",
101
+ "items": {
102
+ "type": "string"
103
+ },
104
+ "description": "Repo-relative file paths the systemic problem spans."
105
+ },
106
+ "id": {
107
+ "type": "string",
108
+ "description": "Stable mechanism identifier \u2014 see the findings-level `id` note; the legacy reader maps it from `code`."
109
+ },
110
+ "finding_ids": {
111
+ "type": "array",
112
+ "items": {
113
+ "type": "string"
114
+ },
115
+ "description": "The tolerant-in spelling of `finding_codes` (0.10 renamed it)."
116
+ }
117
+ }
118
+ }
119
+ },
120
+ "scope_metastasis": {
121
+ "type": "object",
122
+ "additionalProperties": false,
123
+ "description": "Advisory (issue #150): the mechanisms whose findings recurred in consecutive prior full-review rounds \u2014 per-code consecutive-round counts plus a decision prompt. The review agent does NOT write this field: the commenter re-derives it from the carried round history and delivers it in the prior review context of a re-review. When it is present, respond to the decision prompt \u2014 state in your summary whether you are committing to the expanding scope (planning the remaining facets of the recurring mechanism(s) as one unit) or narrowing the scope \u2014 so the decision is made explicitly rather than left to emerge piecemeal.",
124
+ "required": [
125
+ "decision_prompt",
126
+ "recurring"
127
+ ],
128
+ "properties": {
129
+ "decision_prompt": {
130
+ "type": "string",
131
+ "description": "The scope decision to make in the summary of the next review \u2014 a decision, not a directive."
132
+ },
133
+ "recurring": {
134
+ "type": "array",
135
+ "description": "The mechanisms flagged: one entry per code whose findings recurred in `consecutive_rounds` or more consecutive completed full-review rounds.",
136
+ "items": {
137
+ "type": "object",
138
+ "additionalProperties": false,
139
+ "required": [
140
+ "consecutive_rounds",
141
+ "start_round"
142
+ ],
143
+ "properties": {
144
+ "code": {
145
+ "type": "string",
146
+ "description": "The finding code of the recurring mechanism."
147
+ },
148
+ "consecutive_rounds": {
149
+ "type": "integer",
150
+ "minimum": 1,
151
+ "maximum": 9007199254740991,
152
+ "description": "How many consecutive completed full-review rounds (ending at the last completed round) carried a finding with this code. Same-head CI retries are collapsed by the streak detector, so this is the de-duplicated streak, not the raw span: it may be less than `round - start_round + 1` when the history contains retries."
153
+ },
154
+ "start_round": {
155
+ "type": "integer",
156
+ "minimum": 1,
157
+ "maximum": 9007199254740991,
158
+ "description": "The 1-indexed round where the consecutive streak began."
159
+ },
160
+ "id": {
161
+ "type": "string",
162
+ "description": "The tolerant-in spelling of `code` (0.10 renamed it)."
163
+ }
164
+ },
165
+ "anyOf": [
166
+ {
167
+ "required": [
168
+ "code"
169
+ ],
170
+ "properties": {
171
+ "code": {
172
+ "type": "string"
173
+ }
174
+ }
175
+ },
176
+ {
177
+ "required": [
178
+ "id"
179
+ ],
180
+ "properties": {
181
+ "id": {
182
+ "type": "string"
183
+ }
184
+ }
185
+ }
186
+ ]
187
+ }
188
+ }
189
+ }
190
+ },
191
+ "convergence": {
192
+ "type": "object",
193
+ "additionalProperties": false,
194
+ "description": "Advisory (issue #174): the current round's convergence score, the threshold it is judged against, whether it converged, and the per-round trajectory. The review agent does NOT write this field \u2014 the pipeline computes the score deterministically from the findings (floor(severity) + headroom \u00d7 confidence \u00d7 likelihood, summed) and stamps it, so this JSON document is the sole source of the convergence data a decoding agent reads. `score` and `threshold` are both carried so the number is interpretable on its own (converged when score <= threshold); prior rounds are carried verbatim, so changing the threshold never rewrites a past round's score.",
195
+ "required": [
196
+ "score",
197
+ "threshold",
198
+ "converged"
199
+ ],
200
+ "properties": {
201
+ "score": {
202
+ "type": "number",
203
+ "description": "This round's convergence score \u2014 the sum of every finding's and systemic problem's contribution, rounded to 2 decimals. Lower is closer to convergence."
204
+ },
205
+ "threshold": {
206
+ "type": "number",
207
+ "description": "The convergence tolerance in force for this round: the round is converged when score <= threshold."
208
+ },
209
+ "converged": {
210
+ "type": "boolean",
211
+ "description": "Whether this round converged (score <= threshold). Advisory only \u2014 never alters the verdict."
212
+ },
213
+ "rounds": {
214
+ "type": "array",
215
+ "description": "The convergence trajectory: one entry per completed full-review round, oldest first, this round's entry last. Each score is a historical snapshot carried verbatim.",
216
+ "items": {
217
+ "type": "object",
218
+ "additionalProperties": false,
219
+ "required": [
220
+ "round"
221
+ ],
222
+ "properties": {
223
+ "round": {
224
+ "type": "integer",
225
+ "minimum": 1,
226
+ "description": "The 1-indexed completed-round number this entry represents."
227
+ },
228
+ "score": {
229
+ "type": "number",
230
+ "description": "That round's convergence score, carried verbatim from when the round completed. Absent only for a round migrated from a pre-#174 sticky, which stored no score."
231
+ },
232
+ "codes": {
233
+ "type": "object",
234
+ "additionalProperties": {
235
+ "type": "integer",
236
+ "minimum": 0
237
+ },
238
+ "description": "That round's mechanism-frequency map: finding code -> number of findings carrying it. Feeds the same-mechanism and scope-metastasis recurrence signals."
239
+ },
240
+ "sha": {
241
+ "type": "string",
242
+ "description": "The reviewed head SHA (short form) for that round, so a same-head CI retry is recognised as the same iteration rather than new recurrence evidence."
243
+ },
244
+ "ids": {
245
+ "type": "object",
246
+ "additionalProperties": {
247
+ "type": "integer",
248
+ "minimum": 0
249
+ },
250
+ "description": "The tolerant-in spelling of `codes` (0.10 renamed the round's mechanism map to `ids`)."
251
+ }
252
+ }
253
+ }
254
+ }
255
+ }
256
+ },
257
+ "change_size": {
258
+ "type": "object",
259
+ "additionalProperties": false,
260
+ "description": "Best-effort (issue #182): the change's added/removed line counts bucketed by ROLE. Fill this with a LOW-effort path heuristic, no file introspection: a file under a tests directory or matching *.test.* / *_test.* is `tests`; a *.md file, anything under docs/, or a license/config doc is `docs`; everything else is `code`. Sum the diff's added and removed lines per bucket. Omit a role that did not change, or omit the whole field if you cannot estimate it. Chrome only \u2014 never affects the verdict or the convergence score. (The per-language cloc table shown beside it is supplied deterministically by the pipeline, not by you.)",
261
+ "properties": {
262
+ "code": {
263
+ "type": "object",
264
+ "additionalProperties": false,
265
+ "required": [
266
+ "added",
267
+ "removed"
268
+ ],
269
+ "properties": {
270
+ "added": {
271
+ "type": "integer",
272
+ "minimum": 0
273
+ },
274
+ "removed": {
275
+ "type": "integer",
276
+ "minimum": 0
277
+ }
278
+ }
279
+ },
280
+ "tests": {
281
+ "type": "object",
282
+ "additionalProperties": false,
283
+ "required": [
284
+ "added",
285
+ "removed"
286
+ ],
287
+ "properties": {
288
+ "added": {
289
+ "type": "integer",
290
+ "minimum": 0
291
+ },
292
+ "removed": {
293
+ "type": "integer",
294
+ "minimum": 0
295
+ }
296
+ }
297
+ },
298
+ "docs": {
299
+ "type": "object",
300
+ "additionalProperties": false,
301
+ "required": [
302
+ "added",
303
+ "removed"
304
+ ],
305
+ "properties": {
306
+ "added": {
307
+ "type": "integer",
308
+ "minimum": 0
309
+ },
310
+ "removed": {
311
+ "type": "integer",
312
+ "minimum": 0
313
+ }
314
+ }
315
+ }
316
+ }
317
+ },
318
+ "findings": {
319
+ "type": "array",
320
+ "description": "Zero or more specific findings.",
321
+ "items": {
322
+ "type": "object",
323
+ "additionalProperties": false,
324
+ "required": [
325
+ "path",
326
+ "start_line",
327
+ "end_line",
328
+ "severity",
329
+ "title",
330
+ "description",
331
+ "reasoning",
332
+ "confidence",
333
+ "likelihood"
334
+ ],
335
+ "properties": {
336
+ "path": {
337
+ "type": "string",
338
+ "description": "Repo-relative file path the finding anchors to."
339
+ },
340
+ "start_line": {
341
+ "type": "integer",
342
+ "minimum": 1,
343
+ "description": "1-indexed first line of the anchored range (post-change state when side is RIGHT)."
344
+ },
345
+ "end_line": {
346
+ "type": "integer",
347
+ "minimum": 1,
348
+ "description": "1-indexed last line of the range; MUST be >= start_line. A suggestion replaces exactly start_line..end_line."
349
+ },
350
+ "side": {
351
+ "type": "string",
352
+ "enum": [
353
+ "RIGHT",
354
+ "LEFT"
355
+ ],
356
+ "default": "RIGHT",
357
+ "description": "As in a unified-diff view: RIGHT for added/changed lines, LEFT for removed lines."
358
+ },
359
+ "severity": {
360
+ "type": "string",
361
+ "enum": [
362
+ "critical",
363
+ "major",
364
+ "minor",
365
+ "nit"
366
+ ],
367
+ "description": "Used for grouping and for folding nits into a collapsed section."
368
+ },
369
+ "code": {
370
+ "type": "string",
371
+ "description": "Stable rule identifier (e.g. \"null-check-missing\") for rule-based filtering, suppression, and cross-run dedup."
372
+ },
373
+ "code_url": {
374
+ "type": "string",
375
+ "format": "uri",
376
+ "description": "URL documenting the rule named by code."
377
+ },
378
+ "title": {
379
+ "type": "string",
380
+ "description": "One-line summary of the finding."
381
+ },
382
+ "description": {
383
+ "type": "string",
384
+ "description": "1-3 sentence markdown explanation of what is wrong. Details belong in `reasoning`."
385
+ },
386
+ "recommendation": {
387
+ "type": "string",
388
+ "description": "1-2 sentence markdown prose describing what to do about the finding \u2014 distinct from `patch`, which is a machine-applicable unified diff. Use this when the fix is best explained rather than mechanically applied, or to accompany a `patch` with rationale."
389
+ },
390
+ "confidence": {
391
+ "type": "number",
392
+ "minimum": 0,
393
+ "maximum": 1,
394
+ "description": "0..1 confidence; a commenter MAY suppress findings below a configurable threshold to control noise, but MUST NOT suppress a critical-severity finding on confidence alone."
395
+ },
396
+ "likelihood": {
397
+ "type": "number",
398
+ "minimum": 0,
399
+ "maximum": 1,
400
+ "description": "0..1 probability the triggering input or state actually occurs in practice \u2014 from a normal user in routine use, OR from an adversary who chooses it when the finding is a security issue. Distinct from confidence (which rates whether the defect is real): a footgun that fires only on hostile-to-oneself input is high-confidence yet low-likelihood. Anchors: 0.9 = any normal input, or any input an attacker can choose (e.g. an injectable query parameter); 0.5 = a plausible-but-uncommon configuration; 0.1 = a genuinely obscure state no one seeks; 0.02 = requires deliberately hostile-to-oneself input (e.g. naming a struct field \"__slots__\"). Name the triggering input, then rate how routinely it occurs."
401
+ },
402
+ "reasoning": {
403
+ "type": "string",
404
+ "description": "Rationale/evidence for why the finding holds, so a human or downstream agent can judge its soundness \u2014 distinct from `description`, which explains the finding to the reader."
405
+ },
406
+ "patch": {
407
+ "type": "string",
408
+ "description": "Optional single-hunk unified diff of one mechanical fix to this file, expressed against its current (post-change) content \u2014 e.g. the `git diff` of just that edit after you apply it. It MUST be exactly one hunk and one contiguous change, and its context and removed lines MUST match the file verbatim so it applies cleanly. Omit this field when there is no clean mechanical fix \u2014 never invent one; put prose guidance in `recommendation` instead."
409
+ },
410
+ "id": {
411
+ "type": "string",
412
+ "description": "Stable mechanism identifier \u2014 0.10 renamed `code` to `id` and made it required. The frozen legacy schema accepts the tolerant-in `id` spelling too, so the ajv gate agrees with the legacy codec on the hybrid doc a mid-transition reviewer produces (new field, old version stamp)."
413
+ }
414
+ }
415
+ }
416
+ }
417
+ }
418
+ }
@@ -48,8 +48,8 @@
48
48
  ### 🔗 Systemic problems
49
49
 
50
50
  <% it.systemic.forEach(function(s) { -%>
51
- #### <%= it.severityEmoji(s.severity) %> (<%= s.severity %>) <%= s.title %><% if (s.code) { %> (<% if (s.code_url) { %>[`<%= s.code %>`](<%~ s.code_url %>)<% } else { %>`<%= s.code %>`<% } %>)<% } %> · confidence <%= it.formatConfidence(s.confidence) %>
52
- <% const meta = [] %><% if (s.paths && s.paths.length > 0) { meta.push('Affects: ' + s.paths.map(function(p) { return '`' + p + '`' }).join(', ')) } %><% if (s.finding_codes && s.finding_codes.length > 0) { meta.push('Ties together: ' + s.finding_codes.map(function(c) { return '`' + c + '`' }).join(', ')) } %><% if (meta.length > 0) { %>_<%~ meta.join(' · ') %>_<% } %>
51
+ #### <%= it.severityEmoji(s.severity) %> (<%= s.severity %>) <%= s.title %><% if (s.id) { %> (<% if (s.code_url) { %>[`<%= s.id %>`](<%~ s.code_url %>)<% } else { %>`<%= s.id %>`<% } %>)<% } %> · confidence <%= it.formatConfidence(s.confidence) %>
52
+ <% const meta = [] %><% if (s.paths && s.paths.length > 0) { meta.push('Affects: ' + s.paths.map(function(p) { return '`' + p + '`' }).join(', ')) } %><% if (s.finding_ids && s.finding_ids.length > 0) { meta.push('Ties together: ' + s.finding_ids.map(function(c) { return '`' + c + '`' }).join(', ')) } %><% if (meta.length > 0) { %>_<%~ meta.join(' · ') %>_<% } %>
53
53
 
54
54
  <%~ s.description %>
55
55
 
@@ -103,12 +103,16 @@ _<%= it.unanchoredCount %> finding<%= it.unanchoredCount === 1 ? '' : 's' %> bel
103
103
  <% } %>
104
104
 
105
105
  <% it.strays.forEach(function(f) { -%>
106
- #### <%= it.severityEmoji(f.severity) %> (<%= f.severity %>) `<%= f.path %>:<%= f.start_line %><% if (f.start_line !== f.end_line) { %>–<%= f.end_line %><% } %><% if (f.side) { %> (<%= f.side %>)<% } %>` — <%= f.title %><% if (f.code) { %> (<% if (f.code_url) { %>[`<%= f.code %>`](<%~ f.code_url %>)<% } else { %>`<%= f.code %>`<% } %>)<% } %> · confidence <%= it.formatConfidence(f.confidence) %> · likelihood <%= it.formatConfidence(f.likelihood) %>
106
+ #### <%= it.severityEmoji(f.severity) %> (<%= f.severity %>) `<%= f.path %>:<%= f.rangeLabel %><% if (f.side) { %> (<%= f.side %>)<% } %>` — <%= f.title %><% if (f.id) { %> (<% if (f.code_url) { %>[`<%= f.id %>`](<%~ f.code_url %>)<% } else { %>`<%= f.id %>`<% } %>)<% } %> · confidence <%= it.formatConfidence(f.confidence) %> · likelihood <%= it.formatConfidence(f.likelihood) %>
107
+ <% if (f.permalink) { -%>
108
+
109
+ <%~ f.permalink %><% if (f.permalinkAnchored) { %> · _agent-reported location_<% } %>
110
+ <% } -%>
107
111
 
108
112
  <%~ f.description %>
109
- <% if (f.code && it.sameRootNotes && Object.prototype.hasOwnProperty.call(it.sameRootNotes, (f.codeKey !== undefined ? f.codeKey : f.code))) { -%>
113
+ <% if (it.sameRootNotes && Object.prototype.hasOwnProperty.call(it.sameRootNotes, f.idKey)) { -%>
110
114
 
111
- > ⚠️ <%~ it.sameRootNotes[(f.codeKey !== undefined ? f.codeKey : f.code)] %>
115
+ > ⚠️ <%~ it.sameRootNotes[f.idKey] %>
112
116
  <% } -%>
113
117
  <% if (f.answeredNote) { -%>
114
118
 
@@ -146,7 +150,7 @@ _<%= it.unanchoredCount %> finding<%= it.unanchoredCount === 1 ? '' : 's' %> bel
146
150
  > <details><summary>🔇 <%= it.suppressedNits.length %> nit<%= it.suppressedNits.length === 1 ? '' : 's' %> below the visibility floor (confidence × likelihood < <%= it.nitVisibilityFloor %>) — hidden</summary>
147
151
  >
148
152
  <% it.suppressedNits.forEach(function(n) { -%>
149
- > - `<%= n.path %>:<%= n.startLine %>` — <%= n.title %><% if (n.code) { %> (<% if (n.codeUrl) { %>[`<%= n.code %>`](<%~ n.codeUrl %>)<% } else { %>`<%= n.code %>`<% } %>)<% } %> _(m = <%= n.m %>)_
153
+ > - `<%= n.path %>:<%= n.startLine %>` — <%= n.title %><% if (n.id) { %> (<% if (n.codeUrl) { %>[`<%= n.id %>`](<%~ n.codeUrl %>)<% } else { %>`<%= n.id %>`<% } %>)<% } %> _(m = <%= n.m %>)_
150
154
  > <%~ '<!-- code-review:suppressed-nit' %>
151
155
  > <%~ 'severity: ' + n.severity %>
152
156
  > <%~ 'location: ' + n.path + ':' + n.startLine + '-' + n.endLine + (n.side ? ' (' + n.side + ')' : '') %>