@jphutchins/code-review 0.1.0-alpha.65 → 0.1.0-alpha.67

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.65",
3
+ "version": "0.1.0-alpha.67",
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",
@@ -75,7 +75,7 @@ ignored); a version outside the supported set degrades to a §5.5 sticky notice.
75
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. |
76
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
77
  | `v0.10.0` | superseded | 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). |
78
- | `v0.11.0` | **current** (draft) | Adds an optional `rebuttal` to every finding and systemic problem: on a re-raise, the reviewer's answer to the response recorded against that id (the review dialogue, #247). A re-raise that carries a non-blank `rebuttal` is never a verbatim re-raise. A rebuttal answers one round's response, so the re-review seed strips rebuttals from the prior review it carries forward, and a re-raise must supply its own. The legacy codec drops a `rebuttal` on a pre-0.10 document, which the frozen `v0.9` file rejects: the one deliberate codec-only tolerance, since only a hybrid document ever carries one. The systemic `id` description now states that a re-raise of the same class reuses the id. Backwards-compatible: a `0.10` document is a valid `0.11` document, so both minors resolve through the same live file and codec, the way `0.4`–`0.9` share the frozen legacy file. Draft until the next release, so the dialogue's later phases may still add to it. |
78
+ | `v0.11.0` | **current** (draft) | Adds an optional `rebuttal` to every finding and systemic problem: on a re-raise, the reviewer's answer to the response recorded against that id (the review dialogue, #247). A re-raise that carries a non-blank `rebuttal` is never a verbatim re-raise. A rebuttal answers one round's response, so the re-review seed strips rebuttals from the prior review it carries forward, and a re-raise must supply its own. The legacy codec drops a `rebuttal` on a pre-0.10 document, which the frozen `v0.9` file rejects: the one deliberate codec-only tolerance, since only a hybrid document ever carries one. The systemic `id` description now states that a re-raise of the same class reuses the id. Adds an optional top-level `unfinished`: the checks a review planned but did not complete, rendered beside the convergence badge. Backwards-compatible: a `0.10` document is a valid `0.11` document, so both minors resolve through the same live file and codec, the way `0.4`–`0.9` share the frozen legacy file. Draft until the next release, so the dialogue's later phases may still add to it. |
79
79
 
80
80
  ### Surface channel (stop signal)
81
81
 
@@ -176,6 +176,11 @@
176
176
  }
177
177
  }
178
178
  },
179
+ "unfinished": {
180
+ "type": "array",
181
+ "description": "Checks this review planned but did not complete, one entry each.",
182
+ "items": { "type": "string", "pattern": "\\S" }
183
+ },
179
184
  "change_size": {
180
185
  "type": "object",
181
186
  "additionalProperties": false,
@@ -47,22 +47,88 @@
47
47
  <% } -%>
48
48
 
49
49
  <%~ it.findings.summary %>
50
- <% const c = it.severityCounts %><% const findingsTotal = c.critical + c.major + c.minor + c.nit %>
51
- <% if (it.teachesResponses && (findingsTotal > 0 || it.systemic.length > 0)) { -%>
50
+ <% if (it.unfinished.length > 0) { -%>
52
51
 
53
- <sub><%~ it.responseTeaching %></sub>
52
+ > [!WARNING]
53
+ > **Unfinished checks** — this round did not complete every check it planned, so its convergence covers less than a full review:
54
+ <% it.unfinished.forEach(function(check) { -%>
55
+ > - <%~ check %>
56
+ <% }) -%>
57
+ <% } -%>
58
+ <% const c = it.severityCounts %><% const findingsTotal = c.critical + c.major + c.minor + c.nit -%>
59
+ <% if (it.strays.length > 0 || (!it.incomplete && (findingsTotal > 0 || it.systemic.length > 0 || it.contested.length > 0))) { -%>
60
+
61
+ ## <% if (it.strays.length === 0) { %>Findings<% } else if (it.inlineDisposition && (it.inlineDisposition.kind === 'disabled' || it.inlineDisposition.kind === 'whole-document')) { %>Findings<% } else 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' %><% } %>
62
+ <% if (it.strays.length > 0) { %>
63
+ <% if (it.inlineDisposition && it.inlineDisposition.kind === 'no-envelope') { %>
64
+ <% if (it.shedCount === 0) { %>
65
+ _Every finding is listed below — the inline review could not be built because the result envelope was missing._
66
+ <% } -%>
67
+ <% } else if (it.inlineDisposition && it.inlineDisposition.kind === 'whole-document') { %>
68
+ _Every finding from this run<% if (it.inlineDisposition.inlineCount > 0) { %>, including the <%= it.inlineDisposition.inlineCount %> posted as inline comment<%= it.inlineDisposition.inlineCount === 1 ? '' : 's' %> on the diff<% } %><% if (it.inlineDisposition.rejectedCount > 0) { %>; <%= it.inlineDisposition.rejectedCount %> could not be anchored (GitHub rejected the position)<% } %>._
69
+ <% } else if (it.unanchoredCount && it.unanchoredCount > 0) { %>
70
+ _<%= 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<% } %>._
71
+ <% } %>
72
+ <% } %>
73
+ <% if (it.teachesResponses && (findingsTotal > 0 || it.systemic.length > 0 || it.contested.length > 0)) { -%>
74
+
75
+ <details><summary>How to answer these findings</summary>
76
+
77
+ <%~ it.responseTeaching %>
78
+ </details>
79
+ <% } -%>
80
+ <% if (!it.incomplete && it.contested.length > 0) { -%>
81
+
82
+ ### ⚖️ Contested
83
+
84
+ _The reviewer rebutted a maintainer's answer, and a maintainer answered again. Each stays in the score at a fixed weight, whether or not it is raised again, until a maintainer rules._
85
+ <% if (it.teachesResponses) { -%>
86
+
87
+ <%~ it.rulingTeaching %>
88
+ <% } -%>
89
+
90
+ <% it.contested.forEach(function(k) { -%>
91
+ #### <%= it.severityEmoji(k.severity) %> (<%= k.severity %>) <%= k.title %>
92
+ <sub><% if (k.reRaise && k.reRaise.location) { %>`<%~ k.reRaise.location %>` · <% } %>id `<%~ k.id %>` · [the maintainer's latest answer](<%~ k.answerUrl %>)</sub>
93
+ <% if (k.reRaise) { -%>
94
+
95
+ <%~ k.reRaise.description %>
96
+ <% if (k.reRaise.rebuttal) { -%>
97
+
98
+ **Rebuttal:** <%~ k.reRaise.rebuttal %>
54
99
  <% } -%>
55
100
 
101
+ > <details><summary>Reasoning</summary>
102
+ >
103
+ <% k.reRaise.reasoning.split("\n").forEach(function(rl) { -%>
104
+ > <%= rl %>
105
+ <% }) -%>
106
+ >
107
+ > </details>
108
+ <% } -%>
109
+ <% if (k.discussionHtml) { -%>
110
+
111
+ <details><summary>💬 Discussion</summary>
112
+
113
+ <%~ k.discussionHtml %>
114
+ </details>
115
+ <% } -%>
116
+ <% }) -%>
117
+ <% } -%>
56
118
  <% if (!it.incomplete && it.systemic.length > 0) { -%>
57
119
 
58
- ## 🔗 Systemic problems
120
+ ### 🔗 Systemic problems
59
121
 
60
122
  <% it.systemic.forEach(function(s) { -%>
61
- ### <%= it.severityEmoji(s.severity) %> (<%= s.severity %>) <%= s.title %>
123
+ #### <%= it.severityEmoji(s.severity) %> (<%= s.severity %>) <%= s.title %>
62
124
  <sub>confidence <%= it.formatConfidence(s.confidence) %><% if (s.id) { %> · id <% if (s.code_url) { %>[`<%~ s.id %>`](<%~ s.code_url %>)<% } else { %>`<%~ s.id %>`<% } %><% } %></sub>
63
125
  <% 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(' · ') %>_<% } %>
64
126
 
65
127
  <%~ s.description %>
128
+ <% if (s.answeredNote) { -%>
129
+
130
+ > ↩️ <%~ s.answeredNote %>
131
+ <% } -%>
66
132
  <% if (it.hasRebuttal(s)) { -%>
67
133
 
68
134
  **Rebuttal:** <%~ s.rebuttal %>
@@ -84,6 +150,7 @@
84
150
  > </details>
85
151
  <% }) -%>
86
152
  <% } -%>
153
+ <% } -%>
87
154
 
88
155
  ---
89
156
 
@@ -99,7 +166,10 @@
99
166
  <% if (findingsTotal > 0) { %>
100
167
  <% 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) } %>
101
168
  **Findings:** <%~ parts.join(' · ') %>
102
- <% } else if (!it.incomplete && it.systemic.length === 0 && !it.answeredReRaiseNote) { %>
169
+ <% } else if (!it.incomplete && (it.systemic.length > 0 || it.contested.length > 0)) { %>
170
+ <% const held = [] %><% if (it.contested.length > 0) { held.push(it.contested.length + ' contested item' + (it.contested.length === 1 ? '' : 's')) } %><% if (it.systemic.length > 0) { held.push(it.systemic.length + ' systemic problem' + (it.systemic.length === 1 ? '' : 's')) } %>
171
+ _No findings beyond the <%= held.join(' and ') %> above._
172
+ <% } else if (!it.incomplete && !it.answeredReRaiseNote) { %>
103
173
  <% if (it.unverifiedNoLogs) { %>_No findings — but no failing-job logs were staged, so this pass could not read the CI failure. "No findings" is not evidence of none._<% } else { %>_No findings — clean review._<% } %>
104
174
  <% } %>
105
175
 
@@ -118,16 +188,6 @@
118
188
  <% } %>
119
189
 
120
190
  <% if (it.strays.length > 0) { %>
121
- ## <% if (it.inlineDisposition && (it.inlineDisposition.kind === 'disabled' || it.inlineDisposition.kind === 'whole-document')) { %>Findings<% } else 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' %><% } %>
122
- <% if (it.inlineDisposition && it.inlineDisposition.kind === 'no-envelope') { %>
123
- <% if (it.shedCount === 0) { %>
124
- _Every finding is listed below — the inline review could not be built because the result envelope was missing._
125
- <% } -%>
126
- <% } else if (it.inlineDisposition && it.inlineDisposition.kind === 'whole-document') { %>
127
- _Every finding from this run<% if (it.inlineDisposition.inlineCount > 0) { %>, including the <%= it.inlineDisposition.inlineCount %> posted as inline comment<%= it.inlineDisposition.inlineCount === 1 ? '' : 's' %> on the diff<% } %><% if (it.inlineDisposition.rejectedCount > 0) { %>; <%= it.inlineDisposition.rejectedCount %> could not be anchored (GitHub rejected the position)<% } %>._
128
- <% } else if (it.unanchoredCount && it.unanchoredCount > 0) { %>
129
- _<%= 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<% } %>._
130
- <% } %>
131
191
 
132
192
  <% it.strays.forEach(function(f) { -%>
133
193
  ### <%= it.severityEmoji(f.severity) %> (<%= f.severity %>) `<%= f.path %>:<%= f.rangeLabel %><% if (f.side) { %> (<%= f.side %>)<% } %>` — <%= f.title %>