@jphutchins/code-review 0.1.0-alpha.5 → 0.1.0-alpha.50

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.5",
3
+ "version": "0.1.0-alpha.50",
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,9 +48,11 @@
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
- "vitest": "^3.0.4"
54
+ "vitest": "^3.0.4",
55
+ "yaml": "^2.9.0"
54
56
  },
55
57
  "engines": {
56
58
  "node": ">=22"
@@ -65,7 +65,38 @@ 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` | superseded | Adds optional `systemic_problems` — an array of cross-cutting observations that tie findings together and are hard to express with a line range, each item with required `title`/`description`/`severity`/`reasoning`/`confidence` and optional `code`/`code_url`/`finding_codes`/`paths` (no line anchors). Refocuses `summary` on justifying the overall verdict rather than restating findings. Backwards-compatible (a `0.5` document is a valid `0.6` document); the CLI keeps resolving `0.4`/`0.5` via identity upcasts. |
73
+ | `v0.9.0` | **current** | Adds a REQUIRED `likelihood` (0..1) to every finding and systemic problem (issue #163): the probability the triggering input/state actually occurs — distinct from `confidence` (whether the defect is real) — folded into the convergence score as a second multiplier. **Breaking**: a pre-0.9 document lacks `likelihood` and no longer validates, so a stale sticky degrades to a sentinel-only seed (one cold re-review) rather than seeding. The draft axis skips `0.7`/`0.8`: those are the surface-signal axis's versions (below), so the draft jumps past them to keep the two version spaces distinct — a draft must never be mistaken for a legacy surfaced blob (issue #156). |
74
+
75
+ ### Surface channel (stop signal)
76
+
77
+ The commenter embeds the agent's **complete** findings document verbatim in review comments
78
+ (`<!-- code-review:findings-json -->`) — no surfaced copy, no added fields (issue #156). The
79
+ deterministic stop signal an iterating author-agent needs is the pipeline-stamped `convergence` field
80
+ INSIDE that document (issue #174): `{score, threshold, converged}` (a literal boolean, so a decoding
81
+ agent cannot re-derive the weights) plus the per-round `rounds` trajectory. It is the single source of
82
+ truth — the agent never writes it (it cannot know the score; the weights and threshold are
83
+ commenter-side), and the findings schema above describes the whole embedded document, `convergence`
84
+ included. When an oversized review falls to the link form (the blob is a URL, not base64), the same
85
+ stamped object rides a compact `<!-- code-review:convergence -->` marker beside the link (issue #185)
86
+ so the trajectory and stop signal survive; the embedded blob always wins on read.
87
+
88
+ Two older surface formats survive only as READ-ONLY migration inputs — a re-review may still parse a
89
+ sticky posted by an earlier release. Nothing writes either anymore:
90
+
91
+ | Version | Status | Notes |
92
+ |---|---|---|
93
+ | `v0.7.0` | legacy (read-only) | The pre-#156 surface axis: the commenter embedded a **surfaced** copy of the findings document carrying `convergence` + `round` inside it (issue #141). A sticky written by a `0.7.0` release still seeds — `stripSurfaceFields` peels the surfaced copy back to the agent's draft. |
94
+ | `v0.8.0` | legacy (read-only) | The version a compact `<!-- code-review:signal -->` marker declared (and a legacy `0.8.0` surfaced blob). Its writer is retired (issue #186); `parseSignalMarker` / `parseSurfaceSignal` still decode it so a pre-#185 sticky seeds. `0.8.0` also added the agent-facing `scope_metastasis` entry (issue #150): per-code consecutive-round recurrence counts plus a decision prompt. Post-#156 that entry is embedded in no document — the re-review seed re-derives it from the carried trajectory. The flat draft schema (now `v0.9.0`) accepts an optional `scope_metastasis` property so a seed-echoing draft validates — an in-place additive change, deliberately NOT a draft version bump: a draft sharing a `0.7.0`/`0.8.0` number would collide with the surface axis's version gate, so the draft axis skips past them to `0.9.0` (issue #163); the axes must stay distinct so `stripSurfaceFields`/`parseSurfaceSignal` can tell a legacy surfaced blob from a draft). |
95
+
96
+ The surface axis is independent of the draft-version registry: it numbers the legacy read-only signal
97
+ formats, while the agent-written document above is `v0.9.0`. `stripSurfaceFields` and
98
+ `parseSurfaceSignal` are version-gated on the surface axis, so a future draft bump can never be
99
+ mistaken for a legacy surfaced blob — and no fresh document is ever surfaced.
69
100
 
70
101
  ### Price-map schema
71
102
 
@@ -75,7 +106,8 @@ policy; the `main` `$id` tracks latest, tagged releases pin to the version. Its
75
106
 
76
107
  | Version | Status | Notes |
77
108
  |---|---|---|
78
- | `v0.1.0` | **current** | Initial price-map schema. Per-model `in`/`out`/`cache_read`/`cache_write` (USD per 1M tokens); `_updated` date; `_unit`. |
109
+ | `v0.1.0` | superseded | Initial price-map schema. Per-model `in`/`out`/`cache_read`/`cache_write` (USD per 1M tokens); `_updated` date; `_unit`. |
110
+ | `v0.2.0` | **current** | A model's value is now a `oneOf` (issue #170): the flat shape above, OR `{ "slots": [ { "utc_from", "utc_to", "in", "out", "cache_read", "cache_write" } ] }` — UTC time-of-day pricing (half-open `[utc_from, utc_to)` windows; `utc_to <= utc_from` wraps past midnight; slots must partition the 24h day). Additive/backward-compatible: every flat map keeps validating. |
79
111
 
80
112
  The `_updated` field inside a price-map instance tracks **price drift** (a data concern) and is
81
113
  distinct from the schema's semver version (a **contract** concern). Adding a new price field (e.g. a
@@ -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,219 @@
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", "likelihood"],
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
+ "likelihood": {
56
+ "type": "number",
57
+ "minimum": 0,
58
+ "maximum": 1,
59
+ "description": "Set to 1 for a systemic problem. A structural/cross-cutting observation has no single triggering input — it is definitionally always present — so the convergence score treats a systemic problem's likelihood as 1 and never discounts it. (Required for schema symmetry with a finding's likelihood; the written value is not read by the score.)"
60
+ },
61
+ "code": {
62
+ "type": "string",
63
+ "description": "Stable rule identifier (e.g. \"repeated-null-check\") for rule-based filtering, suppression, and cross-run dedup — mirrors the finding field of the same name."
64
+ },
65
+ "code_url": {
66
+ "type": "string",
67
+ "format": "uri",
68
+ "description": "URL documenting the rule named by code."
69
+ },
70
+ "finding_codes": {
71
+ "type": "array",
72
+ "items": {
73
+ "type": "string"
74
+ },
75
+ "description": "The `code` values of the findings this systemic problem ties together, when those findings carry codes."
76
+ },
77
+ "paths": {
78
+ "type": "array",
79
+ "items": {
80
+ "type": "string"
81
+ },
82
+ "description": "Repo-relative file paths the systemic problem spans."
83
+ }
84
+ }
85
+ }
86
+ },
87
+ "scope_metastasis": {
88
+ "type": "object",
89
+ "additionalProperties": false,
90
+ "description": "Advisory (issue #150): the mechanisms whose findings recurred in consecutive prior full-review rounds — per-code consecutive-round counts plus a decision prompt. The review agent does NOT write this field: the commenter re-derives it from the carried round history and delivers it in the prior review context of a re-review. When it is present, respond to the decision prompt — state in your summary whether you are committing to the expanding scope (planning the remaining facets of the recurring mechanism(s) as one unit) or narrowing the scope — so the decision is made explicitly rather than left to emerge piecemeal.",
91
+ "required": ["decision_prompt", "recurring"],
92
+ "properties": {
93
+ "decision_prompt": {
94
+ "type": "string",
95
+ "description": "The scope decision to make in the summary of the next review — a decision, not a directive."
96
+ },
97
+ "recurring": {
98
+ "type": "array",
99
+ "description": "The mechanisms flagged: one entry per code whose findings recurred in `consecutive_rounds` or more consecutive completed full-review rounds.",
100
+ "items": {
101
+ "type": "object",
102
+ "additionalProperties": false,
103
+ "required": ["code", "consecutive_rounds", "start_round"],
104
+ "properties": {
105
+ "code": {
106
+ "type": "string",
107
+ "description": "The finding code of the recurring mechanism."
108
+ },
109
+ "consecutive_rounds": {
110
+ "type": "integer",
111
+ "minimum": 1,
112
+ "maximum": 9007199254740991,
113
+ "description": "How many consecutive completed full-review rounds (ending at the last completed round) carried a finding with this code. Same-head CI retries are collapsed by the streak detector, so this is the de-duplicated streak, not the raw span: it may be less than `round - start_round + 1` when the history contains retries."
114
+ },
115
+ "start_round": {
116
+ "type": "integer",
117
+ "minimum": 1,
118
+ "maximum": 9007199254740991,
119
+ "description": "The 1-indexed round where the consecutive streak began."
120
+ }
121
+ }
122
+ }
123
+ }
124
+ }
125
+ },
126
+ "convergence": {
127
+ "type": "object",
128
+ "additionalProperties": false,
129
+ "description": "Advisory (issue #174): the current round's convergence score, the threshold it is judged against, whether it converged, and the per-round trajectory. The review agent does NOT write this field — the pipeline computes the score deterministically from the findings (floor(severity) + headroom × confidence × likelihood, summed) and stamps it, so this JSON document is the sole source of the convergence data a decoding agent reads. `score` and `threshold` are both carried so the number is interpretable on its own (converged when score <= threshold); prior rounds are carried verbatim, so changing the threshold never rewrites a past round's score.",
130
+ "required": ["score", "threshold", "converged"],
131
+ "properties": {
132
+ "score": {
133
+ "type": "number",
134
+ "description": "This round's convergence score — the sum of every finding's and systemic problem's contribution, rounded to 2 decimals. Lower is closer to convergence."
135
+ },
136
+ "threshold": {
137
+ "type": "number",
138
+ "description": "The convergence tolerance in force for this round: the round is converged when score <= threshold."
139
+ },
140
+ "converged": {
141
+ "type": "boolean",
142
+ "description": "Whether this round converged (score <= threshold). Advisory only — never alters the verdict."
143
+ },
144
+ "rounds": {
145
+ "type": "array",
146
+ "description": "The convergence trajectory: one entry per completed full-review round, oldest first, this round's entry last. Each score is a historical snapshot carried verbatim.",
147
+ "items": {
148
+ "type": "object",
149
+ "additionalProperties": false,
150
+ "required": ["round"],
151
+ "properties": {
152
+ "round": {
153
+ "type": "integer",
154
+ "minimum": 1,
155
+ "description": "The 1-indexed completed-round number this entry represents."
156
+ },
157
+ "score": {
158
+ "type": "number",
159
+ "description": "That round's convergence score, carried verbatim from when the round completed. Absent only for a round migrated from a pre-#174 sticky, which stored no score."
160
+ },
161
+ "codes": {
162
+ "type": "object",
163
+ "additionalProperties": { "type": "integer", "minimum": 0 },
164
+ "description": "That round's mechanism-frequency map: finding code -> number of findings carrying it. Feeds the same-mechanism and scope-metastasis recurrence signals."
165
+ },
166
+ "sha": {
167
+ "type": "string",
168
+ "description": "The reviewed head SHA (short form) for that round, so a same-head CI retry is recognised as the same iteration rather than new recurrence evidence."
169
+ }
170
+ }
171
+ }
172
+ }
173
+ }
174
+ },
175
+ "change_size": {
176
+ "type": "object",
177
+ "additionalProperties": false,
178
+ "description": "Best-effort (issue #182): the change's added/removed line counts bucketed by ROLE. Fill this with a LOW-effort path heuristic, no file introspection: a file under a tests directory or matching *.test.* / *_test.* is `tests`; a *.md file, anything under docs/, or a license/config doc is `docs`; everything else is `code`. Sum the diff's added and removed lines per bucket. Omit a role that did not change, or omit the whole field if you cannot estimate it. Chrome only — never affects the verdict or the convergence score. (The per-language cloc table shown beside it is supplied deterministically by the pipeline, not by you.)",
179
+ "properties": {
180
+ "code": {
181
+ "type": "object",
182
+ "additionalProperties": false,
183
+ "required": ["added", "removed"],
184
+ "properties": {
185
+ "added": { "type": "integer", "minimum": 0 },
186
+ "removed": { "type": "integer", "minimum": 0 }
187
+ }
188
+ },
189
+ "tests": {
190
+ "type": "object",
191
+ "additionalProperties": false,
192
+ "required": ["added", "removed"],
193
+ "properties": {
194
+ "added": { "type": "integer", "minimum": 0 },
195
+ "removed": { "type": "integer", "minimum": 0 }
196
+ }
197
+ },
198
+ "docs": {
199
+ "type": "object",
200
+ "additionalProperties": false,
201
+ "required": ["added", "removed"],
202
+ "properties": {
203
+ "added": { "type": "integer", "minimum": 0 },
204
+ "removed": { "type": "integer", "minimum": 0 }
205
+ }
206
+ }
207
+ }
23
208
  },
24
209
  "findings": {
25
210
  "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.",
211
+ "description": "Zero or more specific findings.",
27
212
  "items": {
28
213
  "type": "object",
29
214
  "additionalProperties": false,
30
- "required": ["path", "start_line", "end_line", "severity", "title", "body"],
215
+ "required": [
216
+ "path",
217
+ "start_line",
218
+ "end_line",
219
+ "severity",
220
+ "title",
221
+ "description",
222
+ "reasoning",
223
+ "confidence",
224
+ "likelihood"
225
+ ],
31
226
  "properties": {
32
227
  "path": {
33
228
  "type": "string",
@@ -47,7 +242,7 @@
47
242
  "type": "string",
48
243
  "enum": ["RIGHT", "LEFT"],
49
244
  "default": "RIGHT",
50
- "description": "RIGHT for added/changed lines, LEFT for removed lines; matches the GitHub pulls reviews API."
245
+ "description": "As in a unified-diff view: RIGHT for added/changed lines, LEFT for removed lines."
51
246
  },
52
247
  "severity": {
53
248
  "type": "string",
@@ -67,19 +262,33 @@
67
262
  "type": "string",
68
263
  "description": "One-line summary of the finding."
69
264
  },
70
- "body": {
265
+ "description": {
71
266
  "type": "string",
72
- "description": "Markdown explanation; rendered as the inline comment body."
267
+ "description": "1-3 sentence markdown explanation of what is wrong. Details belong in `reasoning`."
73
268
  },
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."
269
+ "recommendation": {
270
+ "type": "string",
271
+ "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
272
  },
78
273
  "confidence": {
79
274
  "type": "number",
80
275
  "minimum": 0,
81
276
  "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."
277
+ "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."
278
+ },
279
+ "likelihood": {
280
+ "type": "number",
281
+ "minimum": 0,
282
+ "maximum": 1,
283
+ "description": "0..1 probability the triggering input or state actually occurs in practice — from a normal user in routine use, OR from an adversary who chooses it when the finding is a security issue. Distinct from confidence (which rates whether the defect is real): a footgun that fires only on hostile-to-oneself input is high-confidence yet low-likelihood. Anchors: 0.9 = any normal input, or any input an attacker can choose (e.g. an injectable query parameter); 0.5 = a plausible-but-uncommon configuration; 0.1 = a genuinely obscure state no one seeks; 0.02 = requires deliberately hostile-to-oneself input (e.g. naming a struct field \"__slots__\"). Name the triggering input, then rate how routinely it occurs."
284
+ },
285
+ "reasoning": {
286
+ "type": "string",
287
+ "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."
288
+ },
289
+ "patch": {
290
+ "type": "string",
291
+ "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
292
  }
84
293
  }
85
294
  }
@@ -1,9 +1,15 @@
1
1
  {
2
- "_comment": "USD per 1,000,000 tokens. PRICES DRIFT — keep _updated current. cost = sum over models of (input*in + output*out + cache_read*cache_read + cache_write*cache_write) / 1e6. The CLI's vendor_cost_usd is vendor-priced and will be wrong for non-Anthropic backends; recompute from this map using the `models` array in the abstract result envelope (SPEC §6.1). Fill the zeros from your provider's pricing page.",
2
+ "_comment": "USD per 1,000,000 tokens. PRICES DRIFT — keep _updated current. cost = sum over models of (input*in + output*out + cache_read*cache_read + cache_write*cache_write) / 1e6. A model's value is EITHER a flat all-day price OR a { \"slots\": [...] } set of UTC time-of-day slots for peak/off-peak pricing (issue #170) — see schema/prices.schema.json for the rules: each slot is a half-open [utc_from, utc_to) UTC window, a slot whose utc_to <= utc_from wraps past midnight, and a model's slots must partition the 24h day with no gap or overlap. The CLI's vendor_cost_usd is vendor-priced and will be wrong for non-Anthropic backends; recompute from this map using the `models` array in the abstract result envelope (SPEC §6.1). Fill the zeros from your provider's pricing page.",
3
3
  "_updated": "2026-07-03",
4
4
  "_unit": "USD per 1M tokens",
5
5
  "models": {
6
6
  "deepseek-v4-pro": { "in": 0.0, "out": 0.0, "cache_read": 0.0, "cache_write": 0.0 },
7
- "deepseek-v4-flash": { "in": 0.0, "out": 0.0, "cache_read": 0.0, "cache_write": 0.0 }
7
+ "deepseek-v4-flash": { "in": 0.0, "out": 0.0, "cache_read": 0.0, "cache_write": 0.0 },
8
+ "example-time-slotted-model": {
9
+ "slots": [
10
+ { "utc_from": "10:00", "utc_to": "01:00", "in": 0.0, "out": 0.0, "cache_read": 0.0, "cache_write": 0.0 },
11
+ { "utc_from": "01:00", "utc_to": "10:00", "in": 0.0, "out": 0.0, "cache_read": 0.0, "cache_write": 0.0 }
12
+ ]
13
+ }
8
14
  }
9
15
  }
@@ -21,33 +21,86 @@
21
21
  },
22
22
  "models": {
23
23
  "type": "object",
24
- "description": "Map of model identifier → per-token prices. Keys are the model identifiers as they appear in the result envelope's `models[].model`.",
24
+ "description": "Map of model identifier → per-token prices. Keys are the model identifiers as they appear in the result envelope's `models[].model`. A model's value is EITHER a flat all-day price (unchanged) OR a set of UTC time-of-day slots (issue #170), for a provider with peak/off-peak rates.",
25
25
  "additionalProperties": {
26
- "type": "object",
27
- "additionalProperties": false,
28
- "required": ["in", "out", "cache_read", "cache_write"],
29
- "properties": {
30
- "in": {
31
- "type": "number",
32
- "minimum": 0,
33
- "description": "Price per 1M input tokens."
26
+ "oneOf": [
27
+ {
28
+ "type": "object",
29
+ "additionalProperties": false,
30
+ "required": ["in", "out", "cache_read", "cache_write"],
31
+ "properties": {
32
+ "in": {
33
+ "type": "number",
34
+ "minimum": 0,
35
+ "description": "Price per 1M input tokens (cache-miss)."
36
+ },
37
+ "out": {
38
+ "type": "number",
39
+ "minimum": 0,
40
+ "description": "Price per 1M output tokens."
41
+ },
42
+ "cache_read": {
43
+ "type": "number",
44
+ "minimum": 0,
45
+ "description": "Price per 1M cache-read tokens (prompt-cache hits)."
46
+ },
47
+ "cache_write": {
48
+ "type": "number",
49
+ "minimum": 0,
50
+ "description": "Price per 1M cache-write tokens (prompt-cache population)."
51
+ }
52
+ }
34
53
  },
35
- "out": {
36
- "type": "number",
37
- "minimum": 0,
38
- "description": "Price per 1M output tokens."
39
- },
40
- "cache_read": {
41
- "type": "number",
42
- "minimum": 0,
43
- "description": "Price per 1M cache-read tokens (prompt-cache hits)."
44
- },
45
- "cache_write": {
46
- "type": "number",
47
- "minimum": 0,
48
- "description": "Price per 1M cache-write tokens (prompt-cache population)."
54
+ {
55
+ "type": "object",
56
+ "additionalProperties": false,
57
+ "required": ["slots"],
58
+ "description": "UTC time-of-day pricing (issue #170). `slots` MUST partition the full 24h UTC day with no gap and no overlap: each is a half-open [utc_from, utc_to) window, and a slot whose utc_to <= utc_from wraps past midnight. Everything is UTC — there is no timezone or DST field. Cost recomputation selects the one slot covering the run's UTC instant; a run's time-of-day matching zero or more than one slot is a misconfiguration that warns loudly and prices that model at $0 (never silently wrong).",
59
+ "properties": {
60
+ "slots": {
61
+ "type": "array",
62
+ "minItems": 1,
63
+ "items": {
64
+ "type": "object",
65
+ "additionalProperties": false,
66
+ "required": ["utc_from", "utc_to", "in", "out", "cache_read", "cache_write"],
67
+ "properties": {
68
+ "utc_from": {
69
+ "type": "string",
70
+ "pattern": "^([01][0-9]|2[0-3]):[0-5][0-9]$",
71
+ "description": "Slot start, HH:MM in UTC (inclusive)."
72
+ },
73
+ "utc_to": {
74
+ "type": "string",
75
+ "pattern": "^([01][0-9]|2[0-3]):[0-5][0-9]$",
76
+ "description": "Slot end, HH:MM in UTC (exclusive); a value <= utc_from wraps past midnight."
77
+ },
78
+ "in": {
79
+ "type": "number",
80
+ "minimum": 0,
81
+ "description": "Price per 1M input tokens (cache-miss) in this slot."
82
+ },
83
+ "out": {
84
+ "type": "number",
85
+ "minimum": 0,
86
+ "description": "Price per 1M output tokens in this slot."
87
+ },
88
+ "cache_read": {
89
+ "type": "number",
90
+ "minimum": 0,
91
+ "description": "Price per 1M cache-read tokens (prompt-cache hits) in this slot."
92
+ },
93
+ "cache_write": {
94
+ "type": "number",
95
+ "minimum": 0,
96
+ "description": "Price per 1M cache-write tokens in this slot."
97
+ }
98
+ }
99
+ }
100
+ }
101
+ }
49
102
  }
50
- }
103
+ ]
51
104
  }
52
105
  }
53
106
  }
@@ -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
+ }