@jphutchins/code-review 0.1.0-alpha.4 → 0.1.0-alpha.40
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/README.md +80 -7
- package/dist/index.js +3591 -411
- package/dist/index.js.map +1 -1
- package/package.json +2 -1
- package/schema/VERSIONING.md +24 -1
- package/schema/findings.schema.json +88 -14
- package/schema/v0.2/findings.schema.json +88 -0
- package/templates/comment.eta +115 -45
- package/templates/inline.eta +32 -5
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@jphutchins/code-review",
|
|
3
|
-
"version": "0.1.0-alpha.
|
|
3
|
+
"version": "0.1.0-alpha.40",
|
|
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"
|
package/schema/VERSIONING.md
CHANGED
|
@@ -65,7 +65,30 @@ 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` |
|
|
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` | **current** | 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
|
+
|
|
88
|
+
The surfaced axis is independent of the draft-version registry: `v0.7.0` is the surface shape's
|
|
89
|
+
marker contract, while the agent-written document above remains at `v0.6.0`. `stripSurfaceFields`
|
|
90
|
+
and `parseSurfaceSignal` are version-gated on the surface axis, so a future draft bump can never be
|
|
91
|
+
mistaken for a surfaced document.
|
|
69
92
|
|
|
70
93
|
### Price-map schema
|
|
71
94
|
|
|
@@ -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
|
|
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,90 @@
|
|
|
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\").
|
|
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": "
|
|
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.
|
|
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
|
+
}
|
|
23
80
|
},
|
|
24
81
|
"findings": {
|
|
25
82
|
"type": "array",
|
|
26
|
-
"description": "Zero or more specific findings.
|
|
83
|
+
"description": "Zero or more specific findings.",
|
|
27
84
|
"items": {
|
|
28
85
|
"type": "object",
|
|
29
86
|
"additionalProperties": false,
|
|
30
|
-
"required": [
|
|
87
|
+
"required": [
|
|
88
|
+
"path",
|
|
89
|
+
"start_line",
|
|
90
|
+
"end_line",
|
|
91
|
+
"severity",
|
|
92
|
+
"title",
|
|
93
|
+
"description",
|
|
94
|
+
"reasoning",
|
|
95
|
+
"confidence"
|
|
96
|
+
],
|
|
31
97
|
"properties": {
|
|
32
98
|
"path": {
|
|
33
99
|
"type": "string",
|
|
@@ -47,7 +113,7 @@
|
|
|
47
113
|
"type": "string",
|
|
48
114
|
"enum": ["RIGHT", "LEFT"],
|
|
49
115
|
"default": "RIGHT",
|
|
50
|
-
"description": "RIGHT for added/changed lines, LEFT for removed lines
|
|
116
|
+
"description": "As in a unified-diff view: RIGHT for added/changed lines, LEFT for removed lines."
|
|
51
117
|
},
|
|
52
118
|
"severity": {
|
|
53
119
|
"type": "string",
|
|
@@ -67,19 +133,27 @@
|
|
|
67
133
|
"type": "string",
|
|
68
134
|
"description": "One-line summary of the finding."
|
|
69
135
|
},
|
|
70
|
-
"
|
|
136
|
+
"description": {
|
|
71
137
|
"type": "string",
|
|
72
|
-
"description": "
|
|
138
|
+
"description": "1-3 sentence markdown explanation of what is wrong. Details belong in `reasoning`."
|
|
73
139
|
},
|
|
74
|
-
"
|
|
75
|
-
"type":
|
|
76
|
-
"description": "
|
|
140
|
+
"recommendation": {
|
|
141
|
+
"type": "string",
|
|
142
|
+
"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
143
|
},
|
|
78
144
|
"confidence": {
|
|
79
145
|
"type": "number",
|
|
80
146
|
"minimum": 0,
|
|
81
147
|
"maximum": 1,
|
|
82
|
-
"description": "
|
|
148
|
+
"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."
|
|
149
|
+
},
|
|
150
|
+
"reasoning": {
|
|
151
|
+
"type": "string",
|
|
152
|
+
"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."
|
|
153
|
+
},
|
|
154
|
+
"patch": {
|
|
155
|
+
"type": "string",
|
|
156
|
+
"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
157
|
}
|
|
84
158
|
}
|
|
85
159
|
}
|
|
@@ -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
|
+
}
|
package/templates/comment.eta
CHANGED
|
@@ -1,43 +1,113 @@
|
|
|
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('**
|
|
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
|
+
<% } -%>
|
|
7
30
|
|
|
8
31
|
<%~ it.findings.summary %>
|
|
9
32
|
|
|
10
|
-
|
|
33
|
+
<% if (!it.incomplete && it.systemic.length > 0) { -%>
|
|
11
34
|
|
|
12
|
-
|
|
13
|
-
#### 📊 Findings summary — <%= it.totalCount %> finding<%= it.totalCount === 1 ? '' : 's' %> on <%= it.fileCount %> file<%= it.fileCount === 1 ? '' : 's' %>
|
|
35
|
+
### 🔗 Systemic problems
|
|
14
36
|
|
|
15
|
-
<%
|
|
16
|
-
|
|
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
|
-
<% }) %>
|
|
37
|
+
<% it.systemic.forEach(function(s) { -%>
|
|
38
|
+
#### <%= it.severityEmoji(s.severity) %> (<%= s.severity %>) <%= s.title %> · confidence <%= it.formatConfidence(s.confidence) %>
|
|
39
|
+
<% 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
40
|
|
|
22
|
-
|
|
23
|
-
<% if (it.nitFindings.length > 0) { %>
|
|
24
|
-
<details>
|
|
25
|
-
<summary>⚪ <%= it.nitFindings.length %> nit<%= it.nitFindings.length === 1 ? '' : 's' %> (folded)</summary>
|
|
41
|
+
<%~ s.description %>
|
|
26
42
|
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
43
|
+
> [!TIP]
|
|
44
|
+
> <details><summary>Reasoning</summary>
|
|
45
|
+
>
|
|
46
|
+
<% s.reasoning.split("\n").forEach(function(rl) { -%>
|
|
47
|
+
> <%= rl %>
|
|
48
|
+
<% }) -%>
|
|
49
|
+
>
|
|
50
|
+
> </details>
|
|
51
|
+
<% }) -%>
|
|
52
|
+
<% } -%>
|
|
32
53
|
|
|
33
|
-
|
|
54
|
+
---
|
|
55
|
+
|
|
56
|
+
<% const c = it.severityCounts %><% const findingsTotal = c.critical + c.major + c.minor + c.nit %>
|
|
57
|
+
<% if (findingsTotal > 0) { %>
|
|
58
|
+
<% 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) } %>
|
|
59
|
+
**Findings:** <%~ parts.join(' · ') %>
|
|
60
|
+
<% } else if (!it.incomplete && it.systemic.length === 0) { %>
|
|
61
|
+
_No findings — clean review._
|
|
34
62
|
<% } %>
|
|
35
63
|
|
|
36
|
-
<% if (it.
|
|
37
|
-
<%= it.
|
|
64
|
+
<% if (it.inlineDisposition && it.inlineDisposition.kind === 'posted') { %>
|
|
65
|
+
→ <%= 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<% } %>.
|
|
66
|
+
<% } else if (it.inlineDisposition && it.inlineDisposition.kind === 'none-in-diff') { %>
|
|
67
|
+
→ No inline comments — all findings are outside the diff (listed below).
|
|
68
|
+
<% } else if (it.inlineDisposition && it.inlineDisposition.kind === 'inline-unavailable') { %>
|
|
69
|
+
→ ⚠️ Inline comments couldn't be posted for this review (GitHub rejected a comment's position) — every finding is listed below instead.
|
|
38
70
|
<% } %>
|
|
39
|
-
|
|
40
|
-
|
|
71
|
+
|
|
72
|
+
<% if (it.strays.length > 0) { %>
|
|
73
|
+
### <%= (it.unanchoredCount && it.unanchoredCount > 0) ? 'Findings' : 'Findings outside the diff' %>
|
|
74
|
+
<% if (it.unanchoredCount && it.unanchoredCount > 0) { %>
|
|
75
|
+
_<%= 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<% } %>._
|
|
76
|
+
<% } %>
|
|
77
|
+
|
|
78
|
+
<% it.strays.forEach(function(f) { -%>
|
|
79
|
+
#### <%= 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) %>
|
|
80
|
+
|
|
81
|
+
<%~ f.description %>
|
|
82
|
+
<% if (f.code && it.sameRootNotes && Object.prototype.hasOwnProperty.call(it.sameRootNotes, f.code)) { -%>
|
|
83
|
+
|
|
84
|
+
> ⚠️ <%~ it.sameRootNotes[f.code] %>
|
|
85
|
+
<% } -%>
|
|
86
|
+
<% if (f.recommendation) { -%>
|
|
87
|
+
|
|
88
|
+
**Recommended fix:** <%~ f.recommendation %>
|
|
89
|
+
<% } -%>
|
|
90
|
+
<% if (f.patchProjection.kind === "suggestion") { -%>
|
|
91
|
+
|
|
92
|
+
```suggestion
|
|
93
|
+
<%~ f.patchProjection.text %>
|
|
94
|
+
```
|
|
95
|
+
<% } else if (f.patchProjection.kind === "patch") { -%>
|
|
96
|
+
|
|
97
|
+
```patch
|
|
98
|
+
<%~ f.patchProjection.raw %>
|
|
99
|
+
```
|
|
100
|
+
<% } -%>
|
|
101
|
+
|
|
102
|
+
> [!TIP]
|
|
103
|
+
> <details><summary>Reasoning (<%= it.formatConfidence(f.confidence) %> confidence)</summary>
|
|
104
|
+
>
|
|
105
|
+
<% f.reasoning.split("\n").forEach(function(rl) { -%>
|
|
106
|
+
> <%= rl %>
|
|
107
|
+
<% }) -%>
|
|
108
|
+
>
|
|
109
|
+
> </details>
|
|
110
|
+
<% }) %>
|
|
41
111
|
<% } %>
|
|
42
112
|
|
|
43
113
|
<% if (it.testReport) { %>
|
|
@@ -47,7 +117,7 @@ _No findings — clean review._
|
|
|
47
117
|
<summary>📊 Test results — <%= it.testReport.passed %> passed, <%= it.testReport.failed %> failed</summary>
|
|
48
118
|
|
|
49
119
|
<% if (it.testReport.failed > 0 && it.testReport.failures) { %>
|
|
50
|
-
<% it.testReport.failures.forEach(function(f) {
|
|
120
|
+
<% it.testReport.failures.forEach(function(f) { -%>
|
|
51
121
|
- **<%= f.name %>**<% if (f.message) { %> — <%~ f.message %><% } %>
|
|
52
122
|
<% }) %>
|
|
53
123
|
<% } else { %>
|
|
@@ -59,24 +129,24 @@ All <%= it.testReport.total %> tests passed.
|
|
|
59
129
|
|
|
60
130
|
---
|
|
61
131
|
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
<% } else { %>
|
|
132
|
+
> [!WARNING]
|
|
133
|
+
> **LLM Disclosure** — this review was produced by <%= it.modelNames || "unknown model" %>.
|
|
134
|
+
>
|
|
135
|
+
<% if (it.usageAvailable && it.costReport.lines.length > 0) { -%>
|
|
136
|
+
> | Model | Input | Output | Cache read | Cache write | Cost |
|
|
137
|
+
> |---|--:|--:|--:|--:|--:|
|
|
138
|
+
<% it.costReport.lines.forEach(function(l) { -%>
|
|
139
|
+
> | <%= l.model %> | <%= it.formatTokens(l.inputTokens) %> | <%= it.formatTokens(l.outputTokens) %> | <%= it.formatTokens(l.cacheReadTokens) %> | <%= it.formatTokens(l.cacheWriteTokens) %> | <%= it.formatCost(l.costUSD) %> |
|
|
140
|
+
<% }) -%>
|
|
141
|
+
> | **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) %>** |
|
|
142
|
+
<% } else if (it.usageAvailable) { -%>
|
|
143
|
+
> No per-model usage was recorded for this run.
|
|
144
|
+
<% } else { -%>
|
|
76
145
|
> **Usage/cost unavailable** — the result envelope for this run was missing or malformed.
|
|
77
|
-
<% }
|
|
78
|
-
|
|
79
|
-
>
|
|
80
|
-
>
|
|
81
|
-
|
|
82
|
-
>
|
|
146
|
+
<% } -%>
|
|
147
|
+
<% if (it.usageAvailable && !it.pricesProvided) { -%>
|
|
148
|
+
>
|
|
149
|
+
> _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._
|
|
150
|
+
<% } -%>
|
|
151
|
+
>
|
|
152
|
+
> _Generated by [code-review](https://github.com/JPHutchins/code-review)<% if (it.runUrl) { %> · [view the run & traces](<%~ it.runUrl %>)<% } %>._
|
package/templates/inline.eta
CHANGED
|
@@ -1,9 +1,36 @@
|
|
|
1
|
-
|
|
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.recommendation) { -%>
|
|
12
|
+
|
|
13
|
+
**Recommended fix:** <%~ it.recommendation %>
|
|
14
|
+
<% } -%>
|
|
15
|
+
<% if (it.patchProjection.kind === "suggestion") { -%>
|
|
2
16
|
|
|
3
|
-
<% if (it.suggestion !== null && it.suggestion !== undefined) { %>
|
|
4
17
|
```suggestion
|
|
5
|
-
<%~ it.
|
|
18
|
+
<%~ it.patchProjection.text %>
|
|
19
|
+
```
|
|
20
|
+
<% } else if (it.patchProjection.kind === "patch") { -%>
|
|
21
|
+
|
|
22
|
+
```patch
|
|
23
|
+
<%~ it.patchProjection.raw %>
|
|
6
24
|
```
|
|
7
|
-
<% }
|
|
25
|
+
<% } -%>
|
|
8
26
|
|
|
9
|
-
|
|
27
|
+
> [!TIP]
|
|
28
|
+
> Generated by <%~ it.modelsText %>.
|
|
29
|
+
> <details>
|
|
30
|
+
> <summary>Reasoning</summary>
|
|
31
|
+
>
|
|
32
|
+
<% it.reasoning.split("\n").forEach(function(rl) { -%>
|
|
33
|
+
> <%= rl %>
|
|
34
|
+
<% }) -%>
|
|
35
|
+
>
|
|
36
|
+
> </details>
|