@jphutchins/code-review 0.1.0-alpha.50 → 0.1.0-alpha.52
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 +6 -4
- package/dist/index.js +136 -40
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
- package/schema/VERSIONING.md +28 -1
- package/schema/prices.example.json +29 -5
- package/schema/prices.schema.json +73 -6
- package/templates/comment.eta +15 -2
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@jphutchins/code-review",
|
|
3
|
-
"version": "0.1.0-alpha.
|
|
4
|
-
"description": "Deterministic commenter for agentic PR review
|
|
3
|
+
"version": "0.1.0-alpha.52",
|
|
4
|
+
"description": "Deterministic commenter for agentic PR review \u2014 gather, render, inline, post, adapt, extract, cost, validate, print-schema",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
7
7
|
"author": "JP Hutchins <jp@intercreate.io>",
|
package/schema/VERSIONING.md
CHANGED
|
@@ -107,13 +107,40 @@ policy; the `main` `$id` tracks latest, tagged releases pin to the version. Its
|
|
|
107
107
|
| Version | Status | Notes |
|
|
108
108
|
|---|---|---|
|
|
109
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` |
|
|
110
|
+
| `v0.2.0` | superseded | 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. |
|
|
111
|
+
| `v0.3.0` | **current** | Optional `weekend_slots` beside `slots` (issue #216): a second partition, same shape and same 24h-partition requirement, selected on Saturdays and Sundays in **Beijing** time (Friday 16:00 UTC → Sunday 16:00 UTC — a day-of-week rule, not a timezone axis). A model without it uses `slots` every day. Additive for readers that know the key; see the delivery rule below for readers that do not. |
|
|
111
112
|
|
|
112
113
|
The `_updated` field inside a price-map instance tracks **price drift** (a data concern) and is
|
|
113
114
|
distinct from the schema's semver version (a **contract** concern). Adding a new price field (e.g. a
|
|
114
115
|
future `cache_write_5m`) is a MINOR schema bump; updating a price value is only an `_updated`
|
|
115
116
|
change.
|
|
116
117
|
|
|
118
|
+
Every rate in a map states **current** pricing. Recomputing an old envelope reprices it at today's
|
|
119
|
+
rates, so a cost recomputed long after the run is not a record of what was billed — this applies to
|
|
120
|
+
the `weekend_slots` axis exactly as it applies to the numbers.
|
|
121
|
+
|
|
122
|
+
#### Adding a key to a shipped map: the delivery-order rule
|
|
123
|
+
|
|
124
|
+
A price map and the CLI that reads it travel by **different routes**. `.github/prices.json` is read
|
|
125
|
+
from the default-branch checkout and takes effect the moment it merges; the CLI is installed from
|
|
126
|
+
npm at the pinned `CODE_REVIEW_VERSION` and takes effect only when a release ships. The price codecs
|
|
127
|
+
are strict-keyed by design (`SlottedModelPricesStrict` requires every key to be `slots`), so a map
|
|
128
|
+
carrying a key the pinned CLI predates does not degrade — `PriceMapCodec` returns `Left` and `post`
|
|
129
|
+
throws, taking down every review between the merge and the release.
|
|
130
|
+
|
|
131
|
+
So a map key MUST NOT reach the default branch before the CLI that parses it:
|
|
132
|
+
|
|
133
|
+
1. Merge the codec, schema, and tests on their own — safe in any order, since a newer CLI reads
|
|
134
|
+
every older map.
|
|
135
|
+
2. Put the **data** change in the release commit itself, beside the `CODE_REVIEW_VERSION` bump, so
|
|
136
|
+
the map and the CLI that understands it land in the same commit and the same tag.
|
|
137
|
+
3. Only then roll consumers — pin and map together, one commit each (a consumer's map is copied
|
|
138
|
+
from the tag).
|
|
139
|
+
|
|
140
|
+
Verify before merging a map change: install the pinned version and decode the new map with it —
|
|
141
|
+
`npx @jphutchins/code-review@$CODE_REVIEW_VERSION cost <envelope> --prices <new-map>` prints a cost
|
|
142
|
+
if the pinned CLI accepts it and `prices does not match expected shape` if it does not.
|
|
143
|
+
|
|
117
144
|
## Compatibility with CLI structured-output enforcement
|
|
118
145
|
|
|
119
146
|
The schema is **inlined** — no `$ref`, `$defs`, or `$id` fragments. This is an intentional
|
|
@@ -1,14 +1,38 @@
|
|
|
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. 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
|
|
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), optionally with a \"weekend_slots\" partition for a provider that prices weekends differently (issue #216) — see schema/prices.schema.json, whose field descriptions are the spec: 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
|
-
"deepseek-v4-pro": {
|
|
7
|
-
|
|
6
|
+
"deepseek-v4-pro": {
|
|
7
|
+
"in": 0.0,
|
|
8
|
+
"out": 0.0,
|
|
9
|
+
"cache_read": 0.0,
|
|
10
|
+
"cache_write": 0.0
|
|
11
|
+
},
|
|
12
|
+
"deepseek-v4-flash": {
|
|
13
|
+
"in": 0.0,
|
|
14
|
+
"out": 0.0,
|
|
15
|
+
"cache_read": 0.0,
|
|
16
|
+
"cache_write": 0.0
|
|
17
|
+
},
|
|
8
18
|
"example-time-slotted-model": {
|
|
9
19
|
"slots": [
|
|
10
|
-
{
|
|
11
|
-
|
|
20
|
+
{
|
|
21
|
+
"utc_from": "10:00",
|
|
22
|
+
"utc_to": "01:00",
|
|
23
|
+
"in": 0.0,
|
|
24
|
+
"out": 0.0,
|
|
25
|
+
"cache_read": 0.0,
|
|
26
|
+
"cache_write": 0.0
|
|
27
|
+
},
|
|
28
|
+
{
|
|
29
|
+
"utc_from": "01:00",
|
|
30
|
+
"utc_to": "10:00",
|
|
31
|
+
"in": 0.0,
|
|
32
|
+
"out": 0.0,
|
|
33
|
+
"cache_read": 0.0,
|
|
34
|
+
"cache_write": 0.0
|
|
35
|
+
}
|
|
12
36
|
]
|
|
13
37
|
}
|
|
14
38
|
}
|
|
@@ -5,7 +5,11 @@
|
|
|
5
5
|
"description": "A date-stamped map of per-model token prices (USD per 1M tokens) the commenter uses to recompute canonical cost from the abstract result envelope's `models` array (SPEC §6.1). The CLI's `vendor_cost_usd` is vendor-priced and wrong for non-Anthropic backends; this map is the recomputation source. Kept free of $ref/$defs to match the findings schema's house style (inlining is not strictly required here — the price map is never passed to a model — but consistency with findings.schema.json is preferred).",
|
|
6
6
|
"type": "object",
|
|
7
7
|
"additionalProperties": false,
|
|
8
|
-
"required": [
|
|
8
|
+
"required": [
|
|
9
|
+
"_updated",
|
|
10
|
+
"_unit",
|
|
11
|
+
"models"
|
|
12
|
+
],
|
|
9
13
|
"properties": {
|
|
10
14
|
"_comment": {
|
|
11
15
|
"type": "string",
|
|
@@ -21,13 +25,18 @@
|
|
|
21
25
|
},
|
|
22
26
|
"models": {
|
|
23
27
|
"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`. 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.",
|
|
28
|
+
"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), optionally with a second `weekend_slots` partition (issue #216), for a provider with peak/off-peak rates.",
|
|
25
29
|
"additionalProperties": {
|
|
26
30
|
"oneOf": [
|
|
27
31
|
{
|
|
28
32
|
"type": "object",
|
|
29
33
|
"additionalProperties": false,
|
|
30
|
-
"required": [
|
|
34
|
+
"required": [
|
|
35
|
+
"in",
|
|
36
|
+
"out",
|
|
37
|
+
"cache_read",
|
|
38
|
+
"cache_write"
|
|
39
|
+
],
|
|
31
40
|
"properties": {
|
|
32
41
|
"in": {
|
|
33
42
|
"type": "number",
|
|
@@ -54,8 +63,10 @@
|
|
|
54
63
|
{
|
|
55
64
|
"type": "object",
|
|
56
65
|
"additionalProperties": false,
|
|
57
|
-
"required": [
|
|
58
|
-
|
|
66
|
+
"required": [
|
|
67
|
+
"slots"
|
|
68
|
+
],
|
|
69
|
+
"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. Slot times are always UTC and there is no timezone or DST field in the data. `weekend_slots` is an optional second partition (issue #216), selected on Saturdays and Sundays in BEIJING time — a day-of-week RULE, not a timezone axis: the Beijing weekend runs Friday 16:00 UTC to Sunday 16:00 UTC, so weekend-ness is NOT a property of the UTC date. It must partition the day exactly as `slots` does. Like every rate here it states CURRENT pricing, so recomputing an envelope from before a pricing change reprices it at today's rates rather than what was billed. A model with no `weekend_slots` uses `slots` every day. Cost recomputation selects the map first, then 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
70
|
"properties": {
|
|
60
71
|
"slots": {
|
|
61
72
|
"type": "array",
|
|
@@ -63,7 +74,14 @@
|
|
|
63
74
|
"items": {
|
|
64
75
|
"type": "object",
|
|
65
76
|
"additionalProperties": false,
|
|
66
|
-
"required": [
|
|
77
|
+
"required": [
|
|
78
|
+
"utc_from",
|
|
79
|
+
"utc_to",
|
|
80
|
+
"in",
|
|
81
|
+
"out",
|
|
82
|
+
"cache_read",
|
|
83
|
+
"cache_write"
|
|
84
|
+
],
|
|
67
85
|
"properties": {
|
|
68
86
|
"utc_from": {
|
|
69
87
|
"type": "string",
|
|
@@ -97,6 +115,55 @@
|
|
|
97
115
|
}
|
|
98
116
|
}
|
|
99
117
|
}
|
|
118
|
+
},
|
|
119
|
+
"weekend_slots": {
|
|
120
|
+
"type": "array",
|
|
121
|
+
"minItems": 1,
|
|
122
|
+
"items": {
|
|
123
|
+
"type": "object",
|
|
124
|
+
"additionalProperties": false,
|
|
125
|
+
"required": [
|
|
126
|
+
"utc_from",
|
|
127
|
+
"utc_to",
|
|
128
|
+
"in",
|
|
129
|
+
"out",
|
|
130
|
+
"cache_read",
|
|
131
|
+
"cache_write"
|
|
132
|
+
],
|
|
133
|
+
"properties": {
|
|
134
|
+
"utc_from": {
|
|
135
|
+
"type": "string",
|
|
136
|
+
"pattern": "^([01][0-9]|2[0-3]):[0-5][0-9]$",
|
|
137
|
+
"description": "Slot start, HH:MM in UTC (inclusive)."
|
|
138
|
+
},
|
|
139
|
+
"utc_to": {
|
|
140
|
+
"type": "string",
|
|
141
|
+
"pattern": "^([01][0-9]|2[0-3]):[0-5][0-9]$",
|
|
142
|
+
"description": "Slot end, HH:MM in UTC (exclusive); a value <= utc_from wraps past midnight."
|
|
143
|
+
},
|
|
144
|
+
"in": {
|
|
145
|
+
"type": "number",
|
|
146
|
+
"minimum": 0,
|
|
147
|
+
"description": "Price per 1M input tokens (cache-miss) in this slot."
|
|
148
|
+
},
|
|
149
|
+
"out": {
|
|
150
|
+
"type": "number",
|
|
151
|
+
"minimum": 0,
|
|
152
|
+
"description": "Price per 1M output tokens in this slot."
|
|
153
|
+
},
|
|
154
|
+
"cache_read": {
|
|
155
|
+
"type": "number",
|
|
156
|
+
"minimum": 0,
|
|
157
|
+
"description": "Price per 1M cache-read tokens (prompt-cache hits) in this slot."
|
|
158
|
+
},
|
|
159
|
+
"cache_write": {
|
|
160
|
+
"type": "number",
|
|
161
|
+
"minimum": 0,
|
|
162
|
+
"description": "Price per 1M cache-write tokens in this slot."
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
},
|
|
166
|
+
"description": "Optional: the slot partition used on Beijing Saturdays and Sundays. Same shape and same 24h-partition requirement as `slots`."
|
|
100
167
|
}
|
|
101
168
|
}
|
|
102
169
|
}
|
package/templates/comment.eta
CHANGED
|
@@ -31,6 +31,7 @@
|
|
|
31
31
|
```
|
|
32
32
|
|
|
33
33
|
</details>
|
|
34
|
+
|
|
34
35
|
<% } -%>
|
|
35
36
|
<% if (it.metastasisNote) { -%>
|
|
36
37
|
<%~ it.metastasisNote %>
|
|
@@ -66,11 +67,19 @@
|
|
|
66
67
|
---
|
|
67
68
|
|
|
68
69
|
<% const c = it.severityCounts %><% const findingsTotal = c.critical + c.major + c.minor + c.nit %>
|
|
70
|
+
<% if (it.unverifiedNoLogs && findingsTotal > 0) { %>
|
|
71
|
+
> [!WARNING]
|
|
72
|
+
> **Unverified — no failing-job logs were available.** This route reads the failing CI job's log to
|
|
73
|
+
> name the failure; none could be staged for this run, so every finding below was reasoned from the
|
|
74
|
+
> diff alone. Treat them as hypotheses until the log is checked.
|
|
75
|
+
|
|
76
|
+
<% } %>
|
|
77
|
+
|
|
69
78
|
<% if (findingsTotal > 0) { %>
|
|
70
79
|
<% 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) } %>
|
|
71
80
|
**Findings:** <%~ parts.join(' · ') %>
|
|
72
81
|
<% } else if (!it.incomplete && it.systemic.length === 0 && !it.answeredReRaiseNote) { %>
|
|
73
|
-
_No findings — clean review._
|
|
82
|
+
<% 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._<% } %>
|
|
74
83
|
<% } %>
|
|
75
84
|
|
|
76
85
|
<% if (it.inlineDisposition && it.inlineDisposition.kind === 'posted') { %>
|
|
@@ -79,12 +88,16 @@ _No findings — clean review._
|
|
|
79
88
|
→ No inline comments — all findings are outside the diff (listed below).
|
|
80
89
|
<% } else if (it.inlineDisposition && it.inlineDisposition.kind === 'inline-unavailable') { %>
|
|
81
90
|
→ ⚠️ Inline comments couldn't be posted for this review (GitHub rejected a comment's position) — every finding is listed below instead.
|
|
91
|
+
<% } else if (it.inlineDisposition && it.inlineDisposition.kind === 'whole-document') { %>
|
|
92
|
+
→ This run's <%= it.incomplete ? 'review record' : 'complete review' %><% if (it.reviewUrl) { %> — [see the review](<%~ it.reviewUrl %>)<% } %>.
|
|
82
93
|
<% } %>
|
|
83
94
|
|
|
84
95
|
<% if (it.strays.length > 0) { %>
|
|
85
|
-
### <% 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' %><% } %>
|
|
96
|
+
### <% 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' %><% } %>
|
|
86
97
|
<% if (it.inlineDisposition && it.inlineDisposition.kind === 'no-envelope') { %>
|
|
87
98
|
_Every finding is listed below — the inline review could not be built because the result envelope was missing._
|
|
99
|
+
<% } else if (it.inlineDisposition && it.inlineDisposition.kind === 'whole-document') { %>
|
|
100
|
+
_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)<% } %>._
|
|
88
101
|
<% } else if (it.unanchoredCount && it.unanchoredCount > 0) { %>
|
|
89
102
|
_<%= 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<% } %>._
|
|
90
103
|
<% } %>
|