@jphutchins/code-review 0.1.0-alpha.60 → 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 +89 -16
- 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);
|
|
@@ -4989,7 +5039,8 @@ var transcriptFallbackFrom = (path) => {
|
|
|
4989
5039
|
return { models: usage.models, turns: usage.turns, durationMs: usage.durationMs };
|
|
4990
5040
|
};
|
|
4991
5041
|
var bundledPath = (...segments) => resolve$1(import.meta.dirname, "..", ...segments);
|
|
4992
|
-
var
|
|
5042
|
+
var readPackageVersion = () => JSON.parse(readFileSync(bundledPath("package.json"), "utf-8")).version;
|
|
5043
|
+
var packageVersion = readPackageVersion();
|
|
4993
5044
|
var resolveTemplatePath = (templateArg) => templateArg ? resolve$1(templateArg) : bundledPath("templates", "comment.eta");
|
|
4994
5045
|
var resolveInlineTemplatePath = (templateArg) => templateArg ? resolve$1(templateArg) : bundledPath("templates", "inline.eta");
|
|
4995
5046
|
var resolvePrices = (pricesArg) => {
|
|
@@ -5062,6 +5113,7 @@ var renderCmd = defineCommand({
|
|
|
5062
5113
|
const templatePath = resolveTemplatePath(args.template);
|
|
5063
5114
|
const priceResolution = resolvePrices(args.prices);
|
|
5064
5115
|
const prices = decode(PriceMapCodec.decode(readJSON(priceResolution.path)), "prices");
|
|
5116
|
+
warnStalePrices(priceResolution.kind === "provided" ? prices : null);
|
|
5065
5117
|
const template = readFileSync(templatePath, "utf-8");
|
|
5066
5118
|
const testReport = args["test-report"] ? decode(TestSummaryCodec.decode(readJSON(args["test-report"])), "test report") : void 0;
|
|
5067
5119
|
const clocDiff = args["cloc-diff"] ? loadClocDiff(args["cloc-diff"]) : void 0;
|
|
@@ -5155,6 +5207,7 @@ var costCmd = defineCommand({
|
|
|
5155
5207
|
run: async ({ args }) => {
|
|
5156
5208
|
const envelope = decode(ResultEnvelopeCodec.decode(readJSON(args.envelope)), "envelope");
|
|
5157
5209
|
const prices = decode(PriceMapCodec.decode(readJSON(args.prices)), "prices");
|
|
5210
|
+
warnStalePrices(prices);
|
|
5158
5211
|
const report = computeCost(
|
|
5159
5212
|
envelope.models,
|
|
5160
5213
|
prices,
|
|
@@ -5176,7 +5229,7 @@ var checkCostCmd = defineCommand({
|
|
|
5176
5229
|
},
|
|
5177
5230
|
prices: {
|
|
5178
5231
|
type: "string",
|
|
5179
|
-
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)"
|
|
5180
5233
|
}
|
|
5181
5234
|
},
|
|
5182
5235
|
run: async ({ args }) => {
|
|
@@ -5190,6 +5243,7 @@ var checkCostCmd = defineCommand({
|
|
|
5190
5243
|
const usage = sumTranscriptUsage(tree.entries);
|
|
5191
5244
|
const priceResolution = resolvePrices(args.prices);
|
|
5192
5245
|
const prices = decode(PriceMapCodec.decode(readJSON(priceResolution.path)), "prices");
|
|
5246
|
+
warnStalePrices(priceResolution.kind === "provided" ? prices : null);
|
|
5193
5247
|
const report = computeCost(
|
|
5194
5248
|
usage.models,
|
|
5195
5249
|
prices,
|
|
@@ -5338,7 +5392,7 @@ var budgetHookCmd = defineCommand({
|
|
|
5338
5392
|
const tree = transcriptPath ? readTranscriptTree(resolve$1(transcriptPath)) : void 0;
|
|
5339
5393
|
const usage = tree ? sumTranscriptUsage(tree.entries) : void 0;
|
|
5340
5394
|
const prices = args.prices ? tryReadPrices(args.prices) : null;
|
|
5341
|
-
const
|
|
5395
|
+
const costReport = prices !== null && usage ? (
|
|
5342
5396
|
// Price at the transcript's last activity instant, not the wall clock (issue #170 review
|
|
5343
5397
|
// r2). Silent warn: this budget-steering cost is recomputed on EVERY tool event, so a
|
|
5344
5398
|
// misconfigured slot map would otherwise flood stderr; the final post's cost render warns.
|
|
@@ -5347,8 +5401,27 @@ var budgetHookCmd = defineCommand({
|
|
|
5347
5401
|
prices,
|
|
5348
5402
|
usage.lastTsMs !== null ? new Date(usage.lastTsMs) : /* @__PURE__ */ new Date(),
|
|
5349
5403
|
() => void 0
|
|
5350
|
-
)
|
|
5404
|
+
)
|
|
5351
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;
|
|
5352
5425
|
const wallMs = args.wall ? parseWallMs(args.wall) : null;
|
|
5353
5426
|
const output2 = evaluateBudgetHook(input, {
|
|
5354
5427
|
spentUsd,
|
|
@@ -6721,6 +6794,6 @@ if (!process.env["VITEST"]) {
|
|
|
6721
6794
|
await runMain(main);
|
|
6722
6795
|
}
|
|
6723
6796
|
|
|
6724
|
-
export { main, printableSchema, snapshotIfValid };
|
|
6797
|
+
export { main, printableSchema, readPackageVersion, snapshotIfValid };
|
|
6725
6798
|
//# sourceMappingURL=index.js.map
|
|
6726
6799
|
//# sourceMappingURL=index.js.map
|