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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/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.6",
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",
@@ -65,7 +65,8 @@ 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` | **current** | Adds optional `reasoning` finding field. |
69
70
 
70
71
  ### Price-map schema
71
72
 
@@ -47,7 +47,7 @@
47
47
  "type": "string",
48
48
  "enum": ["RIGHT", "LEFT"],
49
49
  "default": "RIGHT",
50
- "description": "RIGHT for added/changed lines, LEFT for removed lines; matches the GitHub pulls reviews API."
50
+ "description": "As in a unified-diff view: RIGHT for added/changed lines, LEFT for removed lines."
51
51
  },
52
52
  "severity": {
53
53
  "type": "string",
@@ -73,13 +73,21 @@
73
73
  },
74
74
  "suggestion": {
75
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."
76
+ "description": "Proposed replacement text for start_line..end_line: null = no mechanical fix; \"\" (empty string) = the range should be deleted; a non-empty string = the exact, verbatim replacement text, so it must carry correct indentation."
77
77
  },
78
78
  "confidence": {
79
79
  "type": "number",
80
80
  "minimum": 0,
81
81
  "maximum": 1,
82
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
+ "reasoning": {
85
+ "type": "string",
86
+ "description": "Optional rationale/evidence for why the finding holds, so a human or downstream agent can judge its soundness — distinct from `body`, which explains the finding to the reader."
87
+ },
88
+ "patch": {
89
+ "type": "string",
90
+ "description": "Optional single-hunk unified diff of a mechanical fix, against this file's post-change (RIGHT-side) content. A machine-verifiable alternative to `suggestion`: because it can be validated exactly against the file, a consumer can derive a precise replacement from it or reject it if it no longer applies — preferred over a hand-authored `suggestion`, which is prone to wrong indentation or an over-wide range."
83
91
  }
84
92
  }
85
93
  }
@@ -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,52 @@
1
1
  <!-- code-review -->
2
2
  <!-- reviewed-sha: <%= it.reviewedSha %> -->
3
+ <% if (it.embeddedFindings) { -%>
4
+ <!-- code-review:findings-json;base64 <%~ it.embeddedFindings %> -->
5
+ <% } else if (it.jsonUrl) { -%>
6
+ <!-- code-review:findings-json <%~ it.jsonUrl %> -->
7
+ <% } -%>
3
8
 
4
9
  ### <%= it.verdictBadge(it.findings.verdict) %>
5
10
 
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(' · ') %>
11
+ <% 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)); meta.push('**models:** ' + it.modelNames); meta.push('**cost:** ' + it.formatCost(it.costReport.totalCostUSD)) } else { meta.push('_usage unavailable_') } %><%~ meta.join(' · ') %>
7
12
 
8
13
  <%~ it.findings.summary %>
9
14
 
10
15
  ---
11
16
 
12
- <% if (it.totalCount > 0) { %>
13
- #### 📊 Findings summary <%= it.totalCount %> finding<%= it.totalCount === 1 ? '' : 's' %> on <%= it.fileCount %> file<%= it.fileCount === 1 ? '' : 's' %>
14
-
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
- <% }) %>
17
+ <% const c = it.severityCounts %><% const findingsTotal = c.critical + c.major + c.minor + c.nit %>
18
+ <% if (findingsTotal > 0) { %>
19
+ <% 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) } %>
20
+ **Findings:** <%~ parts.join(' · ') %>
21
+ <% } else { %>
22
+ _No findings — clean review._
23
+ <% } %>
21
24
 
25
+ <% if (it.inlineDisposition && it.inlineDisposition.kind === 'posted') { %>
26
+ → <%= 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<% } %>.
27
+ <% } else if (it.inlineDisposition && it.inlineDisposition.kind === 'none-in-diff') { %>
28
+ → No inline comments — all findings are outside the diff (listed below).
29
+ <% } else if (it.inlineDisposition && it.inlineDisposition.kind === 'suppressed-existing-review') { %>
30
+ → Inline review suppressed — `<%= it.inlineDisposition.sha.slice(0, 7) %>` was already reviewed.
22
31
  <% } %>
23
- <% if (it.nitFindings.length > 0) { %>
24
- <details>
25
- <summary>⚪ <%= it.nitFindings.length %> nit<%= it.nitFindings.length === 1 ? '' : 's' %> (folded)</summary>
26
32
 
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
- <% }) %>
33
+ <% if (it.strays.length > 0) { %>
34
+ #### ⚠️ Findings outside the diff
32
35
 
33
- </details>
34
- <% } %>
36
+ These findings are on lines not in the diff, so they can't be anchored as inline comments:
35
37
 
36
- <% if (it.suggestionCount > 0) { %>
37
- <%= it.suggestionCount %> finding<%= it.suggestionCount === 1 ? '' : 's' %> included suggestionssee inline comments for details.
38
- <% } %>
39
- <% } else { %>
40
- _No findings — clean review._
38
+ <% it.strays.forEach(function(f) { -%>
39
+ - <%= it.severityEmoji(f.severity) %> `<%= f.path %>:<%= f.start_line %><% if (f.start_line !== f.end_line) { %>–<%= f.end_line %><% } %>` <%= f.title %><% if (typeof f.confidence === "number") { %> · confidence <%= f.confidence %><% } %>
40
+ <% if (f.reasoning) { -%>
41
+ <details><summary>Reasoning</summary>
42
+
43
+ <% f.reasoning.split("\n").forEach(function(rl) { -%>
44
+ <%= rl %>
45
+ <% }) -%>
46
+
47
+ </details>
48
+ <% } -%>
49
+ <% }) %>
41
50
  <% } %>
42
51
 
43
52
  <% if (it.testReport) { %>
@@ -47,7 +56,7 @@ _No findings — clean review._
47
56
  <summary>📊 Test results — <%= it.testReport.passed %> passed, <%= it.testReport.failed %> failed</summary>
48
57
 
49
58
  <% if (it.testReport.failed > 0 && it.testReport.failures) { %>
50
- <% it.testReport.failures.forEach(function(f) { %>
59
+ <% it.testReport.failures.forEach(function(f) { -%>
51
60
  - **<%= f.name %>**<% if (f.message) { %> — <%~ f.message %><% } %>
52
61
  <% }) %>
53
62
  <% } else { %>
@@ -59,24 +68,20 @@ All <%= it.testReport.total %> tests passed.
59
68
 
60
69
  ---
61
70
 
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 { %>
71
+ > [!WARNING]
72
+ > **LLM Disclosure** — this review was produced by <%= it.modelNames || "unknown model" %>.
73
+ >
74
+ <% if (it.usageAvailable && it.costReport.lines.length > 0) { -%>
75
+ > | Model | Input | Output | Cache read | Cache write | Cost |
76
+ > |---|--:|--:|--:|--:|--:|
77
+ <% it.costReport.lines.forEach(function(l) { -%>
78
+ > | <%= l.model %> | <%= it.formatTokens(l.inputTokens) %> | <%= it.formatTokens(l.outputTokens) %> | <%= it.formatTokens(l.cacheReadTokens) %> | <%= it.formatTokens(l.cacheWriteTokens) %> | <%= it.formatCost(l.costUSD) %> |
79
+ <% }) -%>
80
+ > | **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) %>** |
81
+ <% } else if (it.usageAvailable) { -%>
82
+ > No per-model usage was recorded for this run.
83
+ <% } else { -%>
76
84
  > **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.
85
+ <% } -%>
86
+ >
87
+ > _Generated by [code-review](https://github.com/JPHutchins/code-review)<% if (it.runUrl) { %> · [view the run & traces](<%~ it.runUrl %>)<% } %>._
@@ -1,9 +1,25 @@
1
+ <% if (it.jsonUrl) { -%>
2
+ <!-- code-review:findings-json <%~ it.jsonUrl %> -->
3
+ <% } -%>
4
+ <%= it.severityEmoji(it.severity) %> **<%= it.severity %>** — <%= it.title %>
5
+
1
6
  <%~ it.body %>
7
+ <% if (it.suggestion !== null && it.suggestion !== undefined) { -%>
2
8
 
3
- <% if (it.suggestion !== null && it.suggestion !== undefined) { %>
4
9
  ```suggestion
5
10
  <%~ it.suggestion %>
6
11
  ```
7
- <% } %>
12
+ <% } -%>
8
13
 
9
- <sub>🤖 AI-generated — advisory only</sub>
14
+ > [!TIP]
15
+ > Generated by <%~ it.modelsText %><% if (typeof it.confidence === "number") { %> at <%= it.confidence %> confidence<% } %>.
16
+ <% if (it.reasoning) { -%>
17
+ > <details>
18
+ > <summary>Reasoning</summary>
19
+ >
20
+ <% it.reasoning.split("\n").forEach(function(rl) { -%>
21
+ > <%= rl %>
22
+ <% }) -%>
23
+ >
24
+ > </details>
25
+ <% } -%>