@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 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) — without it the cost footer renders **$0**
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 set to $0`
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 set to $0`
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
- `code-review cost: unknown model "${entry.model}" \u2014 no entry in price map; cost for this model set to $0`
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 price them.
1768
- formatCost: (n) => !pricesProvided ? "N/A" : Number.isFinite(n) ? n > 0 && n.toFixed(2) === "0.00" ? "<$0.01" : `$${n.toFixed(2)}` : "\u2014",
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 $0)"
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 spentUsd = prices !== null && usage ? (
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
- ).totalCostUSD
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,