@jphutchins/code-review 0.1.0-alpha.4 → 0.1.0-alpha.41

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.4",
3
+ "version": "0.1.0-alpha.41",
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",
@@ -48,6 +48,7 @@
48
48
  "eslint-config-prettier": "^10.1.8",
49
49
  "prettier": "^3.9.4",
50
50
  "tsup": "^8.3.5",
51
+ "tsx": "^4.23.0",
51
52
  "typescript": "^5.7.3",
52
53
  "typescript-eslint": "^8.62.1",
53
54
  "vitest": "^3.0.4"
@@ -65,7 +65,31 @@ ignored); a version outside the supported set degrades to a §5.5 sticky notice.
65
65
  | Version | Status | Notes |
66
66
  |---|---|---|
67
67
  | `v0.1.0` | superseded | Initial schema. Matches the proven camas reference implementation. |
68
- | `v0.2.0` | **current** | Adds required `schema_version`; optional `code`/`code_url` finding fields; normative `suggestion` `""`/`null` semantics; abstract vendor-neutral envelope (see SPEC §6.1). |
68
+ | `v0.2.0` | superseded | Adds required `schema_version`; optional `code`/`code_url` finding fields; normative `suggestion` `""`/`null` semantics; abstract vendor-neutral envelope (see SPEC §6.1). |
69
+ | `v0.3.0` | superseded | Adds optional `reasoning` finding field. |
70
+ | `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
+ | `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
+ | `v0.6.0` | **current** | 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, so stickies embedded by earlier CLIs still seed a re-review. |
73
+
74
+ ### Surfaced findings document
75
+
76
+ The commenter does not embed the agent's raw findings document in review comments — it embeds a
77
+ **surfaced** copy (`<!-- code-review:findings-json -->`): the same fields, stamped with a surface
78
+ version and the pipeline-computed `convergence` (`{score, threshold, converged}` — a literal
79
+ boolean, so a decoding agent cannot re-derive the weights) and `round` (the count of completed
80
+ full-review rounds) of the last completed full-review round. The agent never writes these fields
81
+ (it cannot know the score — the weights and threshold are commenter-side), so the findings schema
82
+ above describes only the agent-written document; the surfaced document has its own version axis:
83
+
84
+ | Version | Status | Notes |
85
+ |---|---|---|
86
+ | `v0.7.0` | superseded | The surfaced document carries `convergence` + `round` — the deterministic stop signal an iterating author-agent decodes instead of the prose (issue #141). Both are omitted until at least one full-review round has completed, and both survive the in-progress banner (carried forward verbatim with the marker). `stripSurfaceFields` drops them when a surfaced blob feeds back into the agent channel (the re-review seed), restoring the draft version. This surface axis is deliberately **distinct** from the draft axis (now `v0.6.0` after issue #134) so a surfaced doc is never mistaken for an agent-written draft. |
87
+ | `v0.8.0` | **current** | Adds the agent-facing `scope_metastasis` entry (issue #150): per-code consecutive-round recurrence counts plus a decision prompt, computed from the same rounds history the prose metastasis note renders. Unlike `convergence`/`round` it is NOT stripped by `stripSurfaceFields` — the re-review seed must deliver the recurrence data to the next-round agent, so the agent can respond to the scope decision instead of letting the end state emerge piecemeal. To tolerate a seed-echoing draft, the flat draft schema (still `v0.6.0`) additionally accepts an optional `scope_metastasis` property — an in-place additive change, deliberately NOT a draft version bump: a `0.7.0` draft would collide with the surfaced axis's version gate (the axes must stay distinct so `stripSurfaceFields`/`parseSurfaceSignal` can tell a surfaced doc from a draft). |
88
+
89
+ The surfaced axis is independent of the draft-version registry: `v0.8.0` is the surface shape's
90
+ marker contract, while the agent-written document above remains at `v0.6.0`. `stripSurfaceFields`
91
+ and `parseSurfaceSignal` are version-gated on the surface axis, so a future draft bump can never be
92
+ mistaken for a surfaced document.
69
93
 
70
94
  ### Price-map schema
71
95
 
@@ -2,7 +2,7 @@
2
2
  "$schema": "https://json-schema.org/draft/2020-12/schema",
3
3
  "$id": "https://raw.githubusercontent.com/JPHutchins/code-review/main/schema/findings.schema.json",
4
4
  "title": "Code Review — findings",
5
- "description": "Structured output of an agentic PR 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`); a deterministic commenter renders it into a sticky summary comment and inline PR review comments. Agent- and provider-agnostic: any model driven by a compatible CLI adapter may produce it. Kept free of $ref/$defs so the same file works for both JSON-Schema validators and CLI structured-output enforcement.",
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
6
  "type": "object",
7
7
  "additionalProperties": false,
8
8
  "required": ["schema_version", "summary", "verdict", "findings"],
@@ -10,24 +10,129 @@
10
10
  "schema_version": {
11
11
  "type": "string",
12
12
  "pattern": "^(0|[1-9]\\d*)\\.(\\d+)\\.(\\d+)(?:-[0-9A-Za-z-]+(?:\\.[0-9A-Za-z-]+)*)?(?:\\+[0-9A-Za-z-]+(?:\\.[0-9A-Za-z-]+)*)?$",
13
- "description": "Semver version of this findings schema the object conforms to (e.g. \"0.1.0\"). Lets a commenter detect a version mismatch rather than silently dropping fields."
13
+ "description": "Semver version of this findings schema the object conforms to (e.g. \"0.1.0\")."
14
14
  },
15
15
  "summary": {
16
16
  "type": "string",
17
- "description": "Markdown walkthrough of the change and the overall assessment; rendered as the body of the sticky summary comment."
17
+ "description": "1-4 sentence markdown justification of the overall verdict — 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."
18
18
  },
19
19
  "verdict": {
20
20
  "type": "string",
21
- "enum": ["approve", "comment", "changes"],
22
- "description": "Overall stance. Advisory only: a conforming commenter posts the GitHub review as COMMENT and never REQUEST_CHANGES, surfacing this value as a badge."
21
+ "enum": ["approve", "comment", "changes", "error"],
22
+ "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 always with `findings: []`), so a consumer switching on `verdict` gets a case it does not recognise rather than a false clean pass."
23
+ },
24
+ "systemic_problems": {
25
+ "type": "array",
26
+ "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.",
27
+ "items": {
28
+ "type": "object",
29
+ "additionalProperties": false,
30
+ "required": ["title", "description", "severity", "reasoning", "confidence"],
31
+ "properties": {
32
+ "title": {
33
+ "type": "string",
34
+ "description": "One-line summary of the systemic problem."
35
+ },
36
+ "description": {
37
+ "type": "string",
38
+ "description": "1-3 sentence markdown explanation of the pattern and why it matters — how it ties the related findings together. Details belong in the individual findings' `reasoning`."
39
+ },
40
+ "severity": {
41
+ "type": "string",
42
+ "enum": ["critical", "major", "minor", "nit"],
43
+ "description": "Overall severity of the systemic problem — the severity of the pattern itself, not of any single finding. It does not count toward the findings severity histogram."
44
+ },
45
+ "reasoning": {
46
+ "type": "string",
47
+ "description": "Rationale/evidence for why the systemic problem holds, so a human or downstream agent can judge its soundness — distinct from `description`, which explains the pattern to the reader. Mirrors the finding field of the same name."
48
+ },
49
+ "confidence": {
50
+ "type": "number",
51
+ "minimum": 0,
52
+ "maximum": 1,
53
+ "description": "0..1 confidence in the systemic problem itself. Mirrors the finding field of the same name."
54
+ },
55
+ "code": {
56
+ "type": "string",
57
+ "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."
58
+ },
59
+ "code_url": {
60
+ "type": "string",
61
+ "format": "uri",
62
+ "description": "URL documenting the rule named by code."
63
+ },
64
+ "finding_codes": {
65
+ "type": "array",
66
+ "items": {
67
+ "type": "string"
68
+ },
69
+ "description": "The `code` values of the findings this systemic problem ties together, when those findings carry codes."
70
+ },
71
+ "paths": {
72
+ "type": "array",
73
+ "items": {
74
+ "type": "string"
75
+ },
76
+ "description": "Repo-relative file paths the systemic problem spans."
77
+ }
78
+ }
79
+ }
80
+ },
81
+ "scope_metastasis": {
82
+ "type": "object",
83
+ "additionalProperties": false,
84
+ "description": "Pipeline-stamped 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 computes it from the carried round history and stamps it into the surfaced document. When it is present in the prior review context of a re-review, 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.",
85
+ "required": ["decision_prompt", "recurring"],
86
+ "properties": {
87
+ "decision_prompt": {
88
+ "type": "string",
89
+ "description": "The scope decision to make in the summary of the next review — a decision, not a directive."
90
+ },
91
+ "recurring": {
92
+ "type": "array",
93
+ "description": "The mechanisms flagged: one entry per code whose findings recurred in `consecutive_rounds` or more consecutive completed full-review rounds.",
94
+ "items": {
95
+ "type": "object",
96
+ "additionalProperties": false,
97
+ "required": ["code", "consecutive_rounds", "start_round"],
98
+ "properties": {
99
+ "code": {
100
+ "type": "string",
101
+ "description": "The finding code of the recurring mechanism."
102
+ },
103
+ "consecutive_rounds": {
104
+ "type": "integer",
105
+ "minimum": 1,
106
+ "maximum": 9007199254740991,
107
+ "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."
108
+ },
109
+ "start_round": {
110
+ "type": "integer",
111
+ "minimum": 1,
112
+ "maximum": 9007199254740991,
113
+ "description": "The 1-indexed round where the consecutive streak began."
114
+ }
115
+ }
116
+ }
117
+ }
118
+ }
23
119
  },
24
120
  "findings": {
25
121
  "type": "array",
26
- "description": "Zero or more specific findings. Each becomes one inline review comment when its line is inside the diff; otherwise it is demoted into the summary.",
122
+ "description": "Zero or more specific findings.",
27
123
  "items": {
28
124
  "type": "object",
29
125
  "additionalProperties": false,
30
- "required": ["path", "start_line", "end_line", "severity", "title", "body"],
126
+ "required": [
127
+ "path",
128
+ "start_line",
129
+ "end_line",
130
+ "severity",
131
+ "title",
132
+ "description",
133
+ "reasoning",
134
+ "confidence"
135
+ ],
31
136
  "properties": {
32
137
  "path": {
33
138
  "type": "string",
@@ -47,7 +152,7 @@
47
152
  "type": "string",
48
153
  "enum": ["RIGHT", "LEFT"],
49
154
  "default": "RIGHT",
50
- "description": "RIGHT for added/changed lines, LEFT for removed lines; matches the GitHub pulls reviews API."
155
+ "description": "As in a unified-diff view: RIGHT for added/changed lines, LEFT for removed lines."
51
156
  },
52
157
  "severity": {
53
158
  "type": "string",
@@ -67,19 +172,27 @@
67
172
  "type": "string",
68
173
  "description": "One-line summary of the finding."
69
174
  },
70
- "body": {
175
+ "description": {
71
176
  "type": "string",
72
- "description": "Markdown explanation; rendered as the inline comment body."
177
+ "description": "1-3 sentence markdown explanation of what is wrong. Details belong in `reasoning`."
73
178
  },
74
- "suggestion": {
75
- "type": ["string", "null"],
76
- "description": "null = no mechanical fix (render no suggestion block); \"\" (empty string) = delete start_line..end_line (render an empty suggestion block); non-empty = exact replacement text for start_line..end_line (render a GitHub suggestion block). A suggestion spanning more than GitHub's single-block line limit is demoted to the summary."
179
+ "recommendation": {
180
+ "type": "string",
181
+ "description": "1-2 sentence markdown prose describing what to do about the finding 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."
77
182
  },
78
183
  "confidence": {
79
184
  "type": "number",
80
185
  "minimum": 0,
81
186
  "maximum": 1,
82
- "description": "Optional 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."
187
+ "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."
188
+ },
189
+ "reasoning": {
190
+ "type": "string",
191
+ "description": "Rationale/evidence for why the finding holds, so a human or downstream agent can judge its soundness — distinct from `description`, which explains the finding to the reader."
192
+ },
193
+ "patch": {
194
+ "type": "string",
195
+ "description": "Optional single-hunk unified diff of one mechanical fix to this file, expressed against its current (post-change) content — 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 — never invent one; put prose guidance in `recommendation` instead."
83
196
  }
84
197
  }
85
198
  }
@@ -0,0 +1,88 @@
1
+ {
2
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
3
+ "$id": "https://raw.githubusercontent.com/JPHutchins/code-review/schema-v0.2.0/schema/findings.schema.json",
4
+ "title": "Code Review — findings",
5
+ "description": "Structured output of an agentic PR 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`); a deterministic commenter renders it into a sticky summary comment and inline PR review comments. Agent- and provider-agnostic: any model driven by a compatible CLI adapter may produce it. Kept free of $ref/$defs so the same file works for both JSON-Schema validators and CLI structured-output enforcement.",
6
+ "type": "object",
7
+ "additionalProperties": false,
8
+ "required": ["schema_version", "summary", "verdict", "findings"],
9
+ "properties": {
10
+ "schema_version": {
11
+ "type": "string",
12
+ "pattern": "^(0|[1-9]\\d*)\\.(\\d+)\\.(\\d+)(?:-[0-9A-Za-z-]+(?:\\.[0-9A-Za-z-]+)*)?(?:\\+[0-9A-Za-z-]+(?:\\.[0-9A-Za-z-]+)*)?$",
13
+ "description": "Semver version of this findings schema the object conforms to (e.g. \"0.1.0\"). Lets a commenter detect a version mismatch rather than silently dropping fields."
14
+ },
15
+ "summary": {
16
+ "type": "string",
17
+ "description": "Markdown walkthrough of the change and the overall assessment; rendered as the body of the sticky summary comment."
18
+ },
19
+ "verdict": {
20
+ "type": "string",
21
+ "enum": ["approve", "comment", "changes"],
22
+ "description": "Overall stance. Advisory only: a conforming commenter posts the GitHub review as COMMENT and never REQUEST_CHANGES, surfacing this value as a badge."
23
+ },
24
+ "findings": {
25
+ "type": "array",
26
+ "description": "Zero or more specific findings. Each becomes one inline review comment when its line is inside the diff; otherwise it is demoted into the summary.",
27
+ "items": {
28
+ "type": "object",
29
+ "additionalProperties": false,
30
+ "required": ["path", "start_line", "end_line", "severity", "title", "body"],
31
+ "properties": {
32
+ "path": {
33
+ "type": "string",
34
+ "description": "Repo-relative file path the finding anchors to."
35
+ },
36
+ "start_line": {
37
+ "type": "integer",
38
+ "minimum": 1,
39
+ "description": "1-indexed first line of the anchored range (post-change state when side is RIGHT)."
40
+ },
41
+ "end_line": {
42
+ "type": "integer",
43
+ "minimum": 1,
44
+ "description": "1-indexed last line of the range; MUST be >= start_line. A suggestion replaces exactly start_line..end_line."
45
+ },
46
+ "side": {
47
+ "type": "string",
48
+ "enum": ["RIGHT", "LEFT"],
49
+ "default": "RIGHT",
50
+ "description": "RIGHT for added/changed lines, LEFT for removed lines; matches the GitHub pulls reviews API."
51
+ },
52
+ "severity": {
53
+ "type": "string",
54
+ "enum": ["critical", "major", "minor", "nit"],
55
+ "description": "Used for grouping and for folding nits into a collapsed section."
56
+ },
57
+ "code": {
58
+ "type": "string",
59
+ "description": "Stable rule identifier (e.g. \"null-check-missing\") for rule-based filtering, suppression, and cross-run dedup."
60
+ },
61
+ "code_url": {
62
+ "type": "string",
63
+ "format": "uri",
64
+ "description": "URL documenting the rule named by code."
65
+ },
66
+ "title": {
67
+ "type": "string",
68
+ "description": "One-line summary of the finding."
69
+ },
70
+ "body": {
71
+ "type": "string",
72
+ "description": "Markdown explanation; rendered as the inline comment body."
73
+ },
74
+ "suggestion": {
75
+ "type": ["string", "null"],
76
+ "description": "null = no mechanical fix (render no suggestion block); \"\" (empty string) = delete start_line..end_line (render an empty suggestion block); non-empty = exact replacement text for start_line..end_line (render a GitHub suggestion block). A suggestion spanning more than GitHub's single-block line limit is demoted to the summary."
77
+ },
78
+ "confidence": {
79
+ "type": "number",
80
+ "minimum": 0,
81
+ "maximum": 1,
82
+ "description": "Optional 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."
83
+ }
84
+ }
85
+ }
86
+ }
87
+ }
88
+ }
@@ -1,43 +1,122 @@
1
1
  <!-- code-review -->
2
2
  <!-- reviewed-sha: <%= it.reviewedSha %> -->
3
+ <% if (!it.incomplete && it.route) { -%>
4
+ <!-- reviewed-route: <%= it.route %> -->
5
+ <% } -%>
6
+ <% if (!it.incomplete) { -%>
7
+ <!-- review-complete -->
8
+ <% } -%>
9
+ <% if (it.findingsPointer) { -%>
10
+ <%~ it.findingsPointer %>
11
+ <% } -%>
12
+ <% if (it.roundsMarker) { -%>
13
+ <%~ it.roundsMarker %>
14
+ <% } -%>
3
15
 
4
16
  ### <%= it.verdictBadge(it.findings.verdict) %>
5
17
 
6
- <% const meta = [] %><% if (it.route) { meta.push('**Route:** ' + it.route) } %><% if (it.effort) { meta.push('**effort:** ' + it.effort) } %><% if (it.usageAvailable) { meta.push('**turns:** ' + it.envelope.turns); meta.push('**wall:** ' + it.formatDuration(it.envelope.duration_ms)) } else { meta.push('_usage unavailable_') } %><%~ meta.join(' · ') %>
18
+ <% const meta = [] %><% if (it.postedAt) { meta.push('**Reviewed** `' + it.reviewedSha.slice(0, 7) + '` at ' + it.postedAt) } %><% if (it.route) { meta.push('**route:** ' + it.route) } %><% if (it.effort) { meta.push('**effort:** ' + it.effort) } %><% if (it.hasUsage) { meta.push('**turns:** ' + it.envelope.turns); meta.push('**wall:** ' + it.formatDuration(it.envelope.duration_ms)); meta.push('**models:** ' + it.modelNames); meta.push('**cost:** ' + it.formatCost(it.costReport.totalCostUSD)) } else { meta.push('_usage unavailable_') } %><sub><%~ meta.join(' · ') %></sub>
19
+ <% if (!it.incomplete) { -%>
20
+ <% if (it.roundsSummary) { -%>
21
+ <sub><%~ it.roundsSummary %></sub>
22
+ <% } -%>
23
+ <% if (it.convergenceSummary) { -%>
24
+ <sub><%~ it.convergenceSummary %></sub>
25
+ <% } -%>
26
+ <% if (it.metastasisNote) { -%>
27
+ <%~ it.metastasisNote %>
28
+ <% } -%>
29
+ <% if (it.answeredReRaiseNote) { -%>
30
+ <%~ it.answeredReRaiseNote %>
31
+ <% } -%>
32
+ <% } -%>
7
33
 
8
34
  <%~ it.findings.summary %>
9
35
 
10
- ---
36
+ <% if (!it.incomplete && it.systemic.length > 0) { -%>
11
37
 
12
- <% if (it.totalCount > 0) { %>
13
- #### 📊 Findings summary — <%= it.totalCount %> finding<%= it.totalCount === 1 ? '' : 's' %> on <%= it.fileCount %> file<%= it.fileCount === 1 ? '' : 's' %>
38
+ ### 🔗 Systemic problems
14
39
 
15
- <% if (it.visibleFindings.length > 0) { %>
16
- | Severity | File | Line | Summary |
17
- |---|---|---|---|
18
- <% it.visibleFindings.forEach(function(f) { %>
19
- | <%= it.severityEmoji(f.severity) %> <%= f.severity %> | `<%= f.path %>` | <%= f.start_line %><% if (f.start_line !== f.end_line) { %>–<%= f.end_line %><% } %> | <%= f.title %> |
20
- <% }) %>
40
+ <% it.systemic.forEach(function(s) { -%>
41
+ #### <%= it.severityEmoji(s.severity) %> (<%= s.severity %>) <%= s.title %> · confidence <%= it.formatConfidence(s.confidence) %>
42
+ <% 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(' · ') %>_<% } %>
21
43
 
22
- <% } %>
23
- <% if (it.nitFindings.length > 0) { %>
24
- <details>
25
- <summary>⚪ <%= it.nitFindings.length %> nit<%= it.nitFindings.length === 1 ? '' : 's' %> (folded)</summary>
44
+ <%~ s.description %>
26
45
 
27
- | File | Line | Summary |
28
- |---|---|---|
29
- <% it.nitFindings.forEach(function(f) { %>
30
- | `<%= f.path %>` | <%= f.start_line %><% if (f.start_line !== f.end_line) { %>–<%= f.end_line %><% } %> | <%= f.title %> |
31
- <% }) %>
46
+ > [!TIP]
47
+ > <details><summary>Reasoning</summary>
48
+ >
49
+ <% s.reasoning.split("\n").forEach(function(rl) { -%>
50
+ > <%= rl %>
51
+ <% }) -%>
52
+ >
53
+ > </details>
54
+ <% }) -%>
55
+ <% } -%>
32
56
 
33
- </details>
57
+ ---
58
+
59
+ <% const c = it.severityCounts %><% const findingsTotal = c.critical + c.major + c.minor + c.nit %>
60
+ <% if (findingsTotal > 0) { %>
61
+ <% const parts = [] %><% if (c.critical) { parts.push(it.severityEmoji('critical') + ' ' + c.critical) } %><% if (c.major) { parts.push(it.severityEmoji('major') + ' ' + c.major) } %><% if (c.minor) { parts.push(it.severityEmoji('minor') + ' ' + c.minor) } %><% if (c.nit) { parts.push(it.severityEmoji('nit') + ' ' + c.nit) } %>
62
+ **Findings:** <%~ parts.join(' · ') %>
63
+ <% } else if (!it.incomplete && it.systemic.length === 0 && !it.answeredReRaiseNote) { %>
64
+ _No findings — clean review._
34
65
  <% } %>
35
66
 
36
- <% if (it.suggestionCount > 0) { %>
37
- <%= it.suggestionCount %> finding<%= it.suggestionCount === 1 ? '' : 's' %> included suggestions — see inline comments for details.
67
+ <% if (it.inlineDisposition && it.inlineDisposition.kind === 'posted') { %>
68
+ <%= it.inlineDisposition.count %> comment<%= it.inlineDisposition.count === 1 ? '' : 's' %> posted inline on `<%= it.inlineDisposition.sha.slice(0, 7) %>` <% if (it.reviewUrl) { %>[see the review](<%~ it.reviewUrl %>)<% } else { %>see the review<% } %>.
69
+ <% } else if (it.inlineDisposition && it.inlineDisposition.kind === 'none-in-diff') { %>
70
+ → No inline comments — all findings are outside the diff (listed below).
71
+ <% } else if (it.inlineDisposition && it.inlineDisposition.kind === 'inline-unavailable') { %>
72
+ → ⚠️ Inline comments couldn't be posted for this review (GitHub rejected a comment's position) — every finding is listed below instead.
38
73
  <% } %>
39
- <% } else { %>
40
- _No findings clean review._
74
+
75
+ <% if (it.strays.length > 0) { %>
76
+ ### <% if (it.inlineDisposition && it.inlineDisposition.kind === 'no-envelope') { %>Findings (no inline review — result envelope lost)<% } else { %><%= (it.unanchoredCount && it.unanchoredCount > 0) ? 'Findings' : 'Findings outside the diff' %><% } %>
77
+ <% if (it.inlineDisposition && it.inlineDisposition.kind === 'no-envelope') { %>
78
+ _Every finding is listed below — the inline review could not be built because the result envelope was missing._
79
+ <% } else if (it.unanchoredCount && it.unanchoredCount > 0) { %>
80
+ _<%= it.unanchoredCount %> finding<%= it.unanchoredCount === 1 ? '' : 's' %> below couldn't be posted as inline comment<%= it.unanchoredCount === 1 ? '' : 's' %> (GitHub rejected the position)<% if (it.strays.length > it.unanchoredCount) { %>; the rest are outside the diff<% } %>._
81
+ <% } %>
82
+
83
+ <% it.strays.forEach(function(f) { -%>
84
+ #### <%= it.severityEmoji(f.severity) %> (<%= f.severity %>) `<%= f.path %>:<%= f.start_line %><% if (f.start_line !== f.end_line) { %>–<%= f.end_line %><% } %>` — <%= f.title %> · confidence <%= it.formatConfidence(f.confidence) %>
85
+
86
+ <%~ f.description %>
87
+ <% if (f.code && it.sameRootNotes && Object.prototype.hasOwnProperty.call(it.sameRootNotes, f.code)) { -%>
88
+
89
+ > ⚠️ <%~ it.sameRootNotes[f.code] %>
90
+ <% } -%>
91
+ <% if (f.answeredNote) { -%>
92
+
93
+ > ↩️ <%~ f.answeredNote %>
94
+ <% } -%>
95
+ <% if (f.recommendation) { -%>
96
+
97
+ **Recommended fix:** <%~ f.recommendation %>
98
+ <% } -%>
99
+ <% if (f.patchProjection.kind === "suggestion") { -%>
100
+
101
+ ```suggestion
102
+ <%~ f.patchProjection.text %>
103
+ ```
104
+ <% } else if (f.patchProjection.kind === "patch") { -%>
105
+
106
+ ```patch
107
+ <%~ f.patchProjection.raw %>
108
+ ```
109
+ <% } -%>
110
+
111
+ > [!TIP]
112
+ > <details><summary>Reasoning (<%= it.formatConfidence(f.confidence) %> confidence)</summary>
113
+ >
114
+ <% f.reasoning.split("\n").forEach(function(rl) { -%>
115
+ > <%= rl %>
116
+ <% }) -%>
117
+ >
118
+ > </details>
119
+ <% }) %>
41
120
  <% } %>
42
121
 
43
122
  <% if (it.testReport) { %>
@@ -47,7 +126,7 @@ _No findings — clean review._
47
126
  <summary>📊 Test results — <%= it.testReport.passed %> passed, <%= it.testReport.failed %> failed</summary>
48
127
 
49
128
  <% if (it.testReport.failed > 0 && it.testReport.failures) { %>
50
- <% it.testReport.failures.forEach(function(f) { %>
129
+ <% it.testReport.failures.forEach(function(f) { -%>
51
130
  - **<%= f.name %>**<% if (f.message) { %> — <%~ f.message %><% } %>
52
131
  <% }) %>
53
132
  <% } else { %>
@@ -59,24 +138,24 @@ All <%= it.testReport.total %> tests passed.
59
138
 
60
139
  ---
61
140
 
62
- <% if (it.usageAvailable && it.costReport.lines.length > 0) { %>
63
- <sub>
64
-
65
- | Model | Input | Output | Cache read | Cache write | Cost |
66
- |---|--:|--:|--:|--:|--:|
67
- <% it.costReport.lines.forEach(function(l) { %>
68
- | <%= l.model %> | <%= it.formatTokens(l.inputTokens) %> | <%= it.formatTokens(l.outputTokens) %> | <%= it.formatTokens(l.cacheReadTokens) %> | <%= it.formatTokens(l.cacheWriteTokens) %> | <%= it.formatCost(l.costUSD) %> |
69
- <% }) %>
70
- | **Total** | **<%= it.formatTokens(it.costReport.totalInputTokens) %>** | **<%= it.formatTokens(it.costReport.totalOutputTokens) %>** | **<%= it.formatTokens(it.costReport.totalCacheReadTokens) %>** | **<%= it.formatTokens(it.costReport.totalCacheWriteTokens) %>** | **<%= it.formatCost(it.costReport.totalCostUSD) %>** |
71
-
72
- </sub>
73
- <% } else if (it.usageAvailable) { %>
74
- <sub>No per-model usage was recorded for this run.</sub>
75
- <% } else { %>
141
+ > [!WARNING]
142
+ > **LLM Disclosure** — this review was produced by <%= it.modelNames || "unknown model" %>.
143
+ >
144
+ <% if (it.usageAvailable && it.costReport.lines.length > 0) { -%>
145
+ > | Model | Input | Output | Cache read | Cache write | Cost |
146
+ > |---|--:|--:|--:|--:|--:|
147
+ <% it.costReport.lines.forEach(function(l) { -%>
148
+ > | <%= l.model %> | <%= it.formatTokens(l.inputTokens) %> | <%= it.formatTokens(l.outputTokens) %> | <%= it.formatTokens(l.cacheReadTokens) %> | <%= it.formatTokens(l.cacheWriteTokens) %> | <%= it.formatCost(l.costUSD) %> |
149
+ <% }) -%>
150
+ > | **Total** | **<%= it.formatTokens(it.costReport.totalInputTokens) %>** | **<%= it.formatTokens(it.costReport.totalOutputTokens) %>** | **<%= it.formatTokens(it.costReport.totalCacheReadTokens) %>** | **<%= it.formatTokens(it.costReport.totalCacheWriteTokens) %>** | **<%= it.formatCost(it.costReport.totalCostUSD) %>** |
151
+ <% } else if (it.usageAvailable) { -%>
152
+ > No per-model usage was recorded for this run.
153
+ <% } else { -%>
76
154
  > **Usage/cost unavailable** — the result envelope for this run was missing or malformed.
77
- <% } %>
78
-
79
- > [!NOTE]
80
- > **LLM Disclosure** — this review was produced by <%= it.modelNames || "unknown model" %> running
81
- > headless in an ephemeral, egress-locked CI runner with no write access to the repository. It is
82
- > advisory and does not block merge.
155
+ <% } -%>
156
+ <% if (it.usageAvailable && !it.pricesProvided) { -%>
157
+ >
158
+ > _Cost shown as **N/A**[No `.github/prices.json`](https://github.com/JPHutchins/code-review/blob/main/schema/prices.example.json) price map to recompute from._
159
+ <% } -%>
160
+ >
161
+ > _Generated by [code-review](https://github.com/JPHutchins/code-review)<% if (it.runUrl) { %> · [view the run & traces](<%~ it.runUrl %>)<% } %>._
@@ -1,9 +1,40 @@
1
- <%~ it.body %>
1
+ <% if (it.findingsPointer) { -%>
2
+ <%~ it.findingsPointer %>
3
+ <% } -%>
4
+ <%= it.severityEmoji(it.severity) %> <%= it.severity.charAt(0).toUpperCase() + it.severity.slice(1) %>: **<%= it.title %>** · <%= it.formatConfidence(it.confidence) %> confidence
5
+
6
+ <%~ it.description %>
7
+ <% if (it.sameRootNote) { -%>
8
+
9
+ > ⚠️ <%~ it.sameRootNote %>
10
+ <% } -%>
11
+ <% if (it.answeredNote) { -%>
12
+
13
+ > ↩️ <%~ it.answeredNote %>
14
+ <% } -%>
15
+ <% if (it.recommendation) { -%>
16
+
17
+ **Recommended fix:** <%~ it.recommendation %>
18
+ <% } -%>
19
+ <% if (it.patchProjection.kind === "suggestion") { -%>
2
20
 
3
- <% if (it.suggestion !== null && it.suggestion !== undefined) { %>
4
21
  ```suggestion
5
- <%~ it.suggestion %>
22
+ <%~ it.patchProjection.text %>
23
+ ```
24
+ <% } else if (it.patchProjection.kind === "patch") { -%>
25
+
26
+ ```patch
27
+ <%~ it.patchProjection.raw %>
6
28
  ```
7
- <% } %>
29
+ <% } -%>
8
30
 
9
- <sub>🤖 AI-generated — advisory only</sub>
31
+ > [!TIP]
32
+ > Generated by <%~ it.modelsText %>.
33
+ > <details>
34
+ > <summary>Reasoning</summary>
35
+ >
36
+ <% it.reasoning.split("\n").forEach(function(rl) { -%>
37
+ > <%= rl %>
38
+ <% }) -%>
39
+ >
40
+ > </details>