@jphutchins/code-review 0.1.0-alpha.61 → 0.1.0-alpha.62
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 +2 -1
- package/dist/index.js +86 -14
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/schema/prices.example.json +1 -1
- package/templates/comment.eta +13 -3
package/README.md
CHANGED
|
@@ -67,7 +67,8 @@ in [templates/](templates/). See [docs/adapters.md](docs/adapters.md) for the ad
|
|
|
67
67
|
window). Both are required: with no endpoint configured the workflow fails loudly rather than
|
|
68
68
|
letting the CLI pick where your key gets sent.
|
|
69
69
|
3. Commit `.github/prices.json` (fork [schema/prices.example.json](schema/prices.example.json) and
|
|
70
|
-
fill in your provider's per-token rates
|
|
70
|
+
fill in your provider's per-token rates, and set `_updated` to the day you verified them — the
|
|
71
|
+
staleness warn keys on it) — without it the cost footer renders **N/A**
|
|
71
72
|
([SPEC §4.4](SPEC.md#44-required-controls-conformance)).
|
|
72
73
|
4. Merge to your default branch first — `workflow_run` only fires from the default branch, so the
|
|
73
74
|
introducing PR won't review itself — then open a test PR.
|
package/dist/index.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { defineCommand, runMain } from 'citty';
|
|
3
|
-
import { readFileSync, writeFileSync, copyFileSync, statSync, readdirSync, appendFileSync } from 'fs';
|
|
3
|
+
import { readFileSync, writeFileSync, unlinkSync, copyFileSync, statSync, readdirSync, appendFileSync } from 'fs';
|
|
4
4
|
import { randomBytes, createHash } from 'crypto';
|
|
5
5
|
import { resolve as resolve$1, join, dirname, basename, extname, sep } from 'path';
|
|
6
6
|
import { Eta } from 'eta';
|
|
@@ -484,6 +484,9 @@ var incompleteFindings = (summary) => ({
|
|
|
484
484
|
});
|
|
485
485
|
var isIncompleteFindings = (findings) => findings.verdict === "error" && findings.findings.length === 0;
|
|
486
486
|
|
|
487
|
+
// src/released.ts
|
|
488
|
+
var RELEASED = "2026-09-27";
|
|
489
|
+
|
|
487
490
|
// src/cost.ts
|
|
488
491
|
var defaultWarn = (message) => {
|
|
489
492
|
process.stderr.write(`${message}
|
|
@@ -515,7 +518,7 @@ var resolveFlatPrices = (model, p, pricedAt, warn) => {
|
|
|
515
518
|
if (!("slots" in p)) return p;
|
|
516
519
|
if (pricedAt === void 0) {
|
|
517
520
|
warn(
|
|
518
|
-
`code-review cost: model "${model}" has time-slotted prices but no run instant was supplied to select a slot; cost for this model
|
|
521
|
+
`code-review cost: model "${annotationSafe(model)}" has time-slotted prices but no run instant was supplied to select a slot; cost for this model renders as N/A`
|
|
519
522
|
);
|
|
520
523
|
return null;
|
|
521
524
|
}
|
|
@@ -526,12 +529,12 @@ var resolveFlatPrices = (model, p, pricedAt, warn) => {
|
|
|
526
529
|
const covering = slots.filter((s) => slotCovers(s, minute));
|
|
527
530
|
if (covering.length === 1) return covering[0] ?? null;
|
|
528
531
|
warn(
|
|
529
|
-
`code-review cost: model "${model}" \u2014 ${String(covering.length)} price slots in \`${useWeekend ? "weekend_slots" : "slots"}\` cover ${hhmmOf(minute)} UTC (expected exactly 1); that array must partition the 24h day with no gap or overlap; cost for this model
|
|
532
|
+
`code-review cost: model "${annotationSafe(model)}" \u2014 ${String(covering.length)} price slots in \`${useWeekend ? "weekend_slots" : "slots"}\` cover ${hhmmOf(minute)} UTC (expected exactly 1); that array must partition the 24h day with no gap or overlap; cost for this model renders as N/A`
|
|
530
533
|
);
|
|
531
534
|
return null;
|
|
532
535
|
};
|
|
533
536
|
var computeModelCost = (entry, prices, pricedAt, warn) => {
|
|
534
|
-
const p = prices.models[entry.model];
|
|
537
|
+
const p = Object.hasOwn(prices.models, entry.model) ? prices.models[entry.model] : void 0;
|
|
535
538
|
const cacheRead = entry.cache_read_tokens ?? 0;
|
|
536
539
|
const cacheWrite = entry.cache_write_tokens ?? 0;
|
|
537
540
|
const zero = {
|
|
@@ -540,16 +543,19 @@ var computeModelCost = (entry, prices, pricedAt, warn) => {
|
|
|
540
543
|
outputTokens: entry.output_tokens,
|
|
541
544
|
cacheReadTokens: cacheRead,
|
|
542
545
|
cacheWriteTokens: cacheWrite,
|
|
543
|
-
costUSD: 0
|
|
546
|
+
costUSD: 0,
|
|
547
|
+
known: true
|
|
544
548
|
};
|
|
545
549
|
if (!p) {
|
|
546
550
|
warn(
|
|
547
|
-
|
|
551
|
+
// annotationSafe: a model id is an unvalidated t.string — a CR/LF in it must not emit a
|
|
552
|
+
// second line the Actions runner parses as a workflow command.
|
|
553
|
+
`code-review cost: unknown model "${annotationSafe(entry.model)}" \u2014 no entry in price map; cost for this model renders as N/A`
|
|
548
554
|
);
|
|
549
|
-
return zero;
|
|
555
|
+
return { ...zero, known: false };
|
|
550
556
|
}
|
|
551
557
|
const rate = resolveFlatPrices(entry.model, p, pricedAt, warn);
|
|
552
|
-
if (rate === null) return zero;
|
|
558
|
+
if (rate === null) return { ...zero, known: false };
|
|
553
559
|
const costUSD = (entry.input_tokens * rate.in + entry.output_tokens * rate.out + cacheRead * rate.cache_read + cacheWrite * rate.cache_write) / 1e6;
|
|
554
560
|
return { ...zero, costUSD };
|
|
555
561
|
};
|
|
@@ -561,9 +567,35 @@ var computeCost = (models, prices, pricedAt, warn = defaultWarn) => {
|
|
|
561
567
|
totalOutputTokens: lines.reduce((s, l) => s + l.outputTokens, 0),
|
|
562
568
|
totalCacheReadTokens: lines.reduce((s, l) => s + l.cacheReadTokens, 0),
|
|
563
569
|
totalCacheWriteTokens: lines.reduce((s, l) => s + l.cacheWriteTokens, 0),
|
|
564
|
-
totalCostUSD: lines.reduce((s, l) => s + l.costUSD, 0)
|
|
570
|
+
totalCostUSD: lines.reduce((s, l) => s + l.costUSD, 0),
|
|
571
|
+
allKnown: lines.every((l) => l.known)
|
|
565
572
|
};
|
|
566
573
|
};
|
|
574
|
+
var parseIsoDate = (stamp) => {
|
|
575
|
+
if (!/^\d{4}-\d{2}-\d{2}$/.test(stamp)) return void 0;
|
|
576
|
+
const parsed = /* @__PURE__ */ new Date(`${stamp}T00:00:00Z`);
|
|
577
|
+
return !Number.isNaN(parsed.getTime()) && parsed.toISOString().slice(0, 10) === stamp ? stamp : void 0;
|
|
578
|
+
};
|
|
579
|
+
var costAxisAnnouncement = (report, markerExists) => {
|
|
580
|
+
if (report === null) return "none";
|
|
581
|
+
if (!report.allKnown) return markerExists ? "none" : "announce";
|
|
582
|
+
return markerExists ? "clear" : "none";
|
|
583
|
+
};
|
|
584
|
+
var warnStalePrices = (prices, warn = defaultWarn) => {
|
|
585
|
+
if (prices === null) return;
|
|
586
|
+
const updated = parseIsoDate(prices._updated);
|
|
587
|
+
if (updated === void 0) return;
|
|
588
|
+
if (updated > (/* @__PURE__ */ new Date()).toISOString().slice(0, 10)) {
|
|
589
|
+
warn(
|
|
590
|
+
`::warning:: code-review cost: the price map's _updated (${prices._updated}) is in the future \u2014 a typo'd year silences the staleness check permanently; re-verify and re-stamp`
|
|
591
|
+
);
|
|
592
|
+
return;
|
|
593
|
+
}
|
|
594
|
+
if (updated >= RELEASED) return;
|
|
595
|
+
warn(
|
|
596
|
+
`::warning:: code-review cost: the price map was last verified ${prices._updated}, before this CLI's release (${RELEASED}) \u2014 the map may miss pricing this CLI ships; re-verify against the provider's pricing page`
|
|
597
|
+
);
|
|
598
|
+
};
|
|
567
599
|
|
|
568
600
|
// src/patch.ts
|
|
569
601
|
var HUNK_HEADER_RE = /^@@ -(\d+)(?:,\d+)? \+\d+(?:,\d+)? @@/;
|
|
@@ -1611,6 +1643,7 @@ var render = (input) => {
|
|
|
1611
1643
|
const incomplete = (input.incomplete ?? input.envelope?.incomplete ?? false) || isIncompleteFindings(input.findings);
|
|
1612
1644
|
const pricedAt = parseInstant(input.envelope?.generated_at) ?? input.pricedAt;
|
|
1613
1645
|
const costReport = input.envelope ? computeCost(input.envelope.models, input.prices, pricedAt) : null;
|
|
1646
|
+
const costProvenance = costReport === null || costReport.lines.length === 0 ? "unknown" : costReport.allKnown ? "complete" : costReport.lines.some((line) => line.known) ? "partial" : "unknown";
|
|
1614
1647
|
const pricesProvided = input.pricesProvided ?? true;
|
|
1615
1648
|
const route = input.route ?? input.envelope?.route ?? null;
|
|
1616
1649
|
const effort = input.effort ?? input.envelope?.effort ?? null;
|
|
@@ -1721,6 +1754,13 @@ var render = (input) => {
|
|
|
1721
1754
|
incomplete,
|
|
1722
1755
|
costReport,
|
|
1723
1756
|
pricesProvided,
|
|
1757
|
+
costProvenance,
|
|
1758
|
+
// The house sanitizer for untrusted template fields — collapses line breaks AND escapes
|
|
1759
|
+
// backticks, so a hand-edited stamp cannot break the code span or the blockquote.
|
|
1760
|
+
// A nonconforming stamp must not be presented as validated: the template shows a note
|
|
1761
|
+
// instead of the smoothed value when the staleness axis cannot parse it.
|
|
1762
|
+
stampValid: parseIsoDate(input.prices._updated) !== void 0,
|
|
1763
|
+
pricesUpdatedAt: escapeCodeBackticks(input.prices._updated.replace(/\r/g, " ")).trim(),
|
|
1724
1764
|
route,
|
|
1725
1765
|
effort,
|
|
1726
1766
|
modelNames,
|
|
@@ -1764,8 +1804,16 @@ var render = (input) => {
|
|
|
1764
1804
|
answeredReRaiseNote: input.answeredReRaiseNote ?? "",
|
|
1765
1805
|
reviewUrl: input.reviewUrl ?? null,
|
|
1766
1806
|
formatTokens: (n) => Number.isFinite(n) && n >= 0 ? n.toLocaleString("en-US") : "\u2014",
|
|
1767
|
-
// N/A (never a false $0.00) when no real price map was provided — real tokens, no rates to
|
|
1768
|
-
|
|
1807
|
+
// N/A (never a false $0.00) when no real price map was provided — real tokens, no rates to
|
|
1808
|
+
// price them — and N/A for a model that missed the map entirely (issue #221): a misconfigured
|
|
1809
|
+
// row must not look like a free review. Provenance is REQUIRED (no permissive default): a
|
|
1810
|
+
// user-swapped template that drops the argument fails loudly, not silently fail-open.
|
|
1811
|
+
formatCost: (n, provenance) => {
|
|
1812
|
+
if (!pricesProvided || provenance !== "complete" && provenance !== "partial") return "N/A";
|
|
1813
|
+
if (provenance === "partial" && !(n >= 5e-3)) return "N/A";
|
|
1814
|
+
const figure = Number.isFinite(n) ? n > 0 && n.toFixed(2) === "0.00" ? "<$0.01" : `$${n.toFixed(2)}` : "\u2014";
|
|
1815
|
+
return provenance === "partial" ? `\u2265 ${figure}` : figure;
|
|
1816
|
+
},
|
|
1769
1817
|
formatDuration: (ms) => {
|
|
1770
1818
|
if (!Number.isFinite(ms) || ms < 0) return "\u2014";
|
|
1771
1819
|
const s = Math.round(ms / 1e3);
|
|
@@ -2129,6 +2177,7 @@ var priorContextPath = (draftPath) => sidecarPath(draftPath, ".prior");
|
|
|
2129
2177
|
var priorAnswersPath = (draftPath) => sidecarPath(draftPath, ".prior-answers");
|
|
2130
2178
|
var priorSuppressedPath = (draftPath) => sidecarPath(draftPath, ".prior-suppressed");
|
|
2131
2179
|
var lastValidPath = (draftPath) => sidecarPath(draftPath, ".last-valid");
|
|
2180
|
+
var costAxisDisengagedPath = (draftPath) => sidecarPath(draftPath, ".cost-axis-disengaged");
|
|
2132
2181
|
var spawnFloorMessage = (draftPath) => `Write your own first-pass findings to ${draftPath} before spawning subagents \u2014 a review must never depend on subagents alone, and the pre-seeded $DRAFT is a non-review sentinel: it does not count until you have replaced it with your own review. Write ${draftPath} from what you have read so far (preliminary findings are fine), run \`code-review validate ${draftPath} --explain\` until it passes, then fan out; your subagents run in the background, so keep refining the draft as their reports arrive.`;
|
|
2133
2182
|
var forceBackgroundSpawn = (toolInput) => ({
|
|
2134
2183
|
hookSpecificOutput: {
|
|
@@ -3400,6 +3449,7 @@ ${dropNote}` : ""}`,
|
|
|
3400
3449
|
if (decodedPrices._tag === "Left") {
|
|
3401
3450
|
throw new Error(`Price map at ${input.pricesPath} does not match the expected shape`);
|
|
3402
3451
|
}
|
|
3452
|
+
warnStalePrices(input.pricesProvided ? decodedPrices.right : null);
|
|
3403
3453
|
const template = readFileSync(input.templatePath, "utf-8");
|
|
3404
3454
|
const inlineRequested = input.inline === true;
|
|
3405
3455
|
const envelope = loadEnvelope(input.envelopePath);
|
|
@@ -5063,6 +5113,7 @@ var renderCmd = defineCommand({
|
|
|
5063
5113
|
const templatePath = resolveTemplatePath(args.template);
|
|
5064
5114
|
const priceResolution = resolvePrices(args.prices);
|
|
5065
5115
|
const prices = decode(PriceMapCodec.decode(readJSON(priceResolution.path)), "prices");
|
|
5116
|
+
warnStalePrices(priceResolution.kind === "provided" ? prices : null);
|
|
5066
5117
|
const template = readFileSync(templatePath, "utf-8");
|
|
5067
5118
|
const testReport = args["test-report"] ? decode(TestSummaryCodec.decode(readJSON(args["test-report"])), "test report") : void 0;
|
|
5068
5119
|
const clocDiff = args["cloc-diff"] ? loadClocDiff(args["cloc-diff"]) : void 0;
|
|
@@ -5156,6 +5207,7 @@ var costCmd = defineCommand({
|
|
|
5156
5207
|
run: async ({ args }) => {
|
|
5157
5208
|
const envelope = decode(ResultEnvelopeCodec.decode(readJSON(args.envelope)), "envelope");
|
|
5158
5209
|
const prices = decode(PriceMapCodec.decode(readJSON(args.prices)), "prices");
|
|
5210
|
+
warnStalePrices(prices);
|
|
5159
5211
|
const report = computeCost(
|
|
5160
5212
|
envelope.models,
|
|
5161
5213
|
prices,
|
|
@@ -5177,7 +5229,7 @@ var checkCostCmd = defineCommand({
|
|
|
5177
5229
|
},
|
|
5178
5230
|
prices: {
|
|
5179
5231
|
type: "string",
|
|
5180
|
-
description: "Path to price map JSON (default: bundled schema/prices.example.json \u2014 token totals stay real, cost reads as
|
|
5232
|
+
description: "Path to price map JSON (default: bundled schema/prices.example.json \u2014 token totals stay real, cost reads as N/A)"
|
|
5181
5233
|
}
|
|
5182
5234
|
},
|
|
5183
5235
|
run: async ({ args }) => {
|
|
@@ -5191,6 +5243,7 @@ var checkCostCmd = defineCommand({
|
|
|
5191
5243
|
const usage = sumTranscriptUsage(tree.entries);
|
|
5192
5244
|
const priceResolution = resolvePrices(args.prices);
|
|
5193
5245
|
const prices = decode(PriceMapCodec.decode(readJSON(priceResolution.path)), "prices");
|
|
5246
|
+
warnStalePrices(priceResolution.kind === "provided" ? prices : null);
|
|
5194
5247
|
const report = computeCost(
|
|
5195
5248
|
usage.models,
|
|
5196
5249
|
prices,
|
|
@@ -5339,7 +5392,7 @@ var budgetHookCmd = defineCommand({
|
|
|
5339
5392
|
const tree = transcriptPath ? readTranscriptTree(resolve$1(transcriptPath)) : void 0;
|
|
5340
5393
|
const usage = tree ? sumTranscriptUsage(tree.entries) : void 0;
|
|
5341
5394
|
const prices = args.prices ? tryReadPrices(args.prices) : null;
|
|
5342
|
-
const
|
|
5395
|
+
const costReport = prices !== null && usage ? (
|
|
5343
5396
|
// Price at the transcript's last activity instant, not the wall clock (issue #170 review
|
|
5344
5397
|
// r2). Silent warn: this budget-steering cost is recomputed on EVERY tool event, so a
|
|
5345
5398
|
// misconfigured slot map would otherwise flood stderr; the final post's cost render warns.
|
|
@@ -5348,8 +5401,27 @@ var budgetHookCmd = defineCommand({
|
|
|
5348
5401
|
prices,
|
|
5349
5402
|
usage.lastTsMs !== null ? new Date(usage.lastTsMs) : /* @__PURE__ */ new Date(),
|
|
5350
5403
|
() => void 0
|
|
5351
|
-
)
|
|
5404
|
+
)
|
|
5352
5405
|
) : null;
|
|
5406
|
+
if (!isSubagentHookInput(input)) {
|
|
5407
|
+
const axisPath = costAxisDisengagedPath(draftPath);
|
|
5408
|
+
const action = costAxisAnnouncement(costReport, readFileOrNull(axisPath) !== null);
|
|
5409
|
+
if (action === "announce") {
|
|
5410
|
+
process.stderr.write(
|
|
5411
|
+
"code-review budget-hook: the price map does not cover every model in the transcript, or its slots leave the run instant uncovered \u2014 the cost axis is disengaged for this run; add the missing models (or fix the slot partition) and re-verify\n"
|
|
5412
|
+
);
|
|
5413
|
+
try {
|
|
5414
|
+
writeFileSync(axisPath, "");
|
|
5415
|
+
} catch {
|
|
5416
|
+
}
|
|
5417
|
+
} else if (action === "clear") {
|
|
5418
|
+
try {
|
|
5419
|
+
unlinkSync(axisPath);
|
|
5420
|
+
} catch {
|
|
5421
|
+
}
|
|
5422
|
+
}
|
|
5423
|
+
}
|
|
5424
|
+
const spentUsd = costReport !== null && costReport.allKnown ? costReport.totalCostUSD : null;
|
|
5353
5425
|
const wallMs = args.wall ? parseWallMs(args.wall) : null;
|
|
5354
5426
|
const output2 = evaluateBudgetHook(input, {
|
|
5355
5427
|
spentUsd,
|