@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 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);
@@ -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 packageVersion = JSON.parse(readFileSync(bundledPath("package.json"), "utf-8")).version;
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 $0)"
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 spentUsd = prices !== null && usage ? (
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
- ).totalCostUSD
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