@jphutchins/code-review 0.1.0-alpha.47 → 0.1.0-alpha.48

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/dist/index.js CHANGED
@@ -33,6 +33,7 @@ var LineNumber = t.refinement(
33
33
  var Confidence = t.refinement(t.number, (n) => n >= 0 && n <= 1, "Confidence");
34
34
  var Likelihood = t.refinement(t.number, (n) => n >= 0 && n <= 1, "Likelihood");
35
35
  var FiniteNumber = t.refinement(t.number, (n) => Number.isFinite(n), "FiniteNumber");
36
+ var NonNegativePrice = t.refinement(t.number, (n) => n >= 0, "NonNegativePrice");
36
37
  var SCHEMA_VERSION_RE = /^(0|[1-9]\d*)\.(\d+)\.(\d+)(?:-[0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*)?(?:\+[0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*)?$/;
37
38
  var SchemaVersion = t.refinement(
38
39
  t.string,
@@ -170,6 +171,15 @@ var ConvergenceStrict = t.refinement(
170
171
  "ConvergenceStrict"
171
172
  );
172
173
  var ConvergenceCodec = t.exact(ConvergenceStrict);
174
+ var LineDelta = t.refinement(
175
+ t.number,
176
+ (n) => Number.isInteger(n) && n >= 0,
177
+ "LineDelta"
178
+ );
179
+ var ChangeLinesCodec = t.exact(t.type({ added: LineDelta, removed: LineDelta }));
180
+ var ChangeSizeCodec = t.exact(
181
+ t.partial({ code: ChangeLinesCodec, tests: ChangeLinesCodec, docs: ChangeLinesCodec })
182
+ );
173
183
  var FindingsCodec = t.exact(
174
184
  t.intersection([
175
185
  t.type({
@@ -183,10 +193,20 @@ var FindingsCodec = t.exact(
183
193
  // Pipeline-stamped only (issue #150); see ScopeMetastasisCodec.
184
194
  scope_metastasis: ScopeMetastasisCodec,
185
195
  // Pipeline-stamped only (issue #174); see ConvergenceCodec.
186
- convergence: ConvergenceCodec
196
+ convergence: ConvergenceCodec,
197
+ // Agent-written best-effort (issue #182); see ChangeSizeCodec.
198
+ change_size: ChangeSizeCodec
187
199
  })
188
200
  ])
189
201
  );
202
+ var PIPELINE_STAMPED_FIELDS = /* @__PURE__ */ new Set([
203
+ "convergence",
204
+ "scope_metastasis"
205
+ ]);
206
+ var RECOVERABLE_OPTIONAL_FIELDS = /* @__PURE__ */ new Set([
207
+ ...PIPELINE_STAMPED_FIELDS,
208
+ "change_size"
209
+ ]);
190
210
  var TriageCodec = t.type({
191
211
  safe: t.boolean,
192
212
  reasons: t.string
@@ -219,6 +239,11 @@ var ResultEnvelopeCodec = t.intersection([
219
239
  vendor_cost_usd: t.union([t.number, t.null]),
220
240
  route: t.string,
221
241
  effort: t.string,
242
+ // The ISO-8601 UTC instant the run completed (stamped by `adapt` when it built this envelope, issue
243
+ // #170). Cost recomputation prices a time-slotted model at THIS instant, so the same envelope prices
244
+ // to the same slot deterministically wherever it is re-rendered — not at each command's wall clock.
245
+ // Absent on a pre-#170 envelope (cost then falls back to the caller's instant).
246
+ generated_at: t.string,
222
247
  // The run produced a notice rather than a completed review (security-gate block, agent kill, no
223
248
  // recoverable findings). An empty `findings` array alone can't say this — a genuine clean review
224
249
  // is also empty — so the render suppresses "clean review" and the sticky precedence guard refuses
@@ -226,12 +251,44 @@ var ResultEnvelopeCodec = t.intersection([
226
251
  incomplete: t.boolean
227
252
  })
228
253
  ]);
229
- var ModelPricesCodec = t.type({
230
- in: t.number,
231
- out: t.number,
232
- cache_read: t.number,
233
- cache_write: t.number
254
+ var FlatModelPricesShape = t.type({
255
+ in: NonNegativePrice,
256
+ out: NonNegativePrice,
257
+ cache_read: NonNegativePrice,
258
+ cache_write: NonNegativePrice
234
259
  });
260
+ var FLAT_PRICE_KEYS = new Set(Object.keys(FlatModelPricesShape.props));
261
+ var FlatModelPricesStrict = t.refinement(
262
+ FlatModelPricesShape,
263
+ (p) => Object.keys(p).every((k) => FLAT_PRICE_KEYS.has(k)),
264
+ "FlatModelPricesStrict"
265
+ );
266
+ var FlatModelPricesCodec = t.exact(FlatModelPricesStrict);
267
+ var UTC_HHMM_RE = /^([01][0-9]|2[0-3]):[0-5][0-9]$/;
268
+ var UtcHHMM = t.refinement(t.string, (s) => UTC_HHMM_RE.test(s), "UtcHHMM");
269
+ var PriceSlotShape = t.intersection([
270
+ t.type({ utc_from: UtcHHMM, utc_to: UtcHHMM }),
271
+ FlatModelPricesShape
272
+ ]);
273
+ var PRICE_SLOT_KEYS = /* @__PURE__ */ new Set(["utc_from", "utc_to", ...Object.keys(FlatModelPricesShape.props)]);
274
+ var PriceSlotStrict = t.refinement(
275
+ PriceSlotShape,
276
+ (s) => Object.keys(s).every((k) => PRICE_SLOT_KEYS.has(k)),
277
+ "PriceSlotStrict"
278
+ );
279
+ var PriceSlotCodec = t.exact(PriceSlotStrict);
280
+ var NonEmptyPriceSlots = t.refinement(
281
+ t.array(PriceSlotCodec),
282
+ (a) => a.length >= 1,
283
+ "NonEmptyPriceSlots"
284
+ );
285
+ var SlottedModelPricesStrict = t.refinement(
286
+ t.type({ slots: NonEmptyPriceSlots }),
287
+ (s) => Object.keys(s).every((k) => k === "slots"),
288
+ "SlottedModelPricesStrict"
289
+ );
290
+ var SlottedModelPricesCodec = t.exact(SlottedModelPricesStrict);
291
+ var ModelPricesCodec = t.union([FlatModelPricesCodec, SlottedModelPricesCodec]);
235
292
  var PriceMapCodec = t.type({
236
293
  _updated: t.string,
237
294
  _unit: t.string,
@@ -265,35 +322,64 @@ var defaultWarn = (message) => {
265
322
  process.stderr.write(`${message}
266
323
  `);
267
324
  };
268
- var computeModelCost = (entry, prices, warn) => {
269
- const p = prices.models[entry.model];
270
- const cacheRead = entry.cache_read_tokens ?? 0;
271
- const cacheWrite = entry.cache_write_tokens ?? 0;
272
- if (!p) {
325
+ var parseInstant = (iso) => {
326
+ if (iso === void 0) return void 0;
327
+ if (iso.includes("T") && !/(Z|[+-]\d{2}:?\d{2})$/.test(iso)) return void 0;
328
+ const d = new Date(iso);
329
+ return Number.isNaN(d.getTime()) ? void 0 : d;
330
+ };
331
+ var utcMinuteOfDay = (at) => at.getUTCHours() * 60 + at.getUTCMinutes();
332
+ var hhmmToMinutes = (hhmm) => {
333
+ const [h, m] = hhmm.split(":");
334
+ return Number(h) * 60 + Number(m);
335
+ };
336
+ var hhmmOf = (minute) => `${String(Math.floor(minute / 60)).padStart(2, "0")}:${String(minute % 60).padStart(2, "0")}`;
337
+ var slotCovers = (slot, minute) => {
338
+ const from = hhmmToMinutes(slot.utc_from);
339
+ const to = hhmmToMinutes(slot.utc_to);
340
+ return from < to ? minute >= from && minute < to : minute >= from || minute < to;
341
+ };
342
+ var resolveFlatPrices = (model, p, pricedAt, warn) => {
343
+ if (!("slots" in p)) return p;
344
+ if (pricedAt === void 0) {
273
345
  warn(
274
- `code-review cost: unknown model "${entry.model}" \u2014 no entry in price map; cost for this model set to $0`
346
+ `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`
275
347
  );
276
- return {
277
- model: entry.model,
278
- inputTokens: entry.input_tokens,
279
- outputTokens: entry.output_tokens,
280
- cacheReadTokens: cacheRead,
281
- cacheWriteTokens: cacheWrite,
282
- costUSD: 0
283
- };
348
+ return null;
284
349
  }
285
- const costUSD = (entry.input_tokens * p.in + entry.output_tokens * p.out + cacheRead * p.cache_read + cacheWrite * p.cache_write) / 1e6;
286
- return {
350
+ const minute = utcMinuteOfDay(pricedAt);
351
+ const covering = p.slots.filter((s) => slotCovers(s, minute));
352
+ if (covering.length === 1) return covering[0] ?? null;
353
+ warn(
354
+ `code-review cost: model "${model}" \u2014 ${String(covering.length)} price slots cover ${hhmmOf(minute)} UTC (expected exactly 1); a model's slots must partition the 24h day with no gap or overlap; cost for this model set to $0`
355
+ );
356
+ return null;
357
+ };
358
+ var computeModelCost = (entry, prices, pricedAt, warn) => {
359
+ const p = prices.models[entry.model];
360
+ const cacheRead = entry.cache_read_tokens ?? 0;
361
+ const cacheWrite = entry.cache_write_tokens ?? 0;
362
+ const zero = {
287
363
  model: entry.model,
288
364
  inputTokens: entry.input_tokens,
289
365
  outputTokens: entry.output_tokens,
290
366
  cacheReadTokens: cacheRead,
291
367
  cacheWriteTokens: cacheWrite,
292
- costUSD
368
+ costUSD: 0
293
369
  };
370
+ if (!p) {
371
+ warn(
372
+ `code-review cost: unknown model "${entry.model}" \u2014 no entry in price map; cost for this model set to $0`
373
+ );
374
+ return zero;
375
+ }
376
+ const rate = resolveFlatPrices(entry.model, p, pricedAt, warn);
377
+ if (rate === null) return zero;
378
+ const costUSD = (entry.input_tokens * rate.in + entry.output_tokens * rate.out + cacheRead * rate.cache_read + cacheWrite * rate.cache_write) / 1e6;
379
+ return { ...zero, costUSD };
294
380
  };
295
- var computeCost = (models, prices, warn = defaultWarn) => {
296
- const lines = models.map((entry) => computeModelCost(entry, prices, warn));
381
+ var computeCost = (models, prices, pricedAt, warn = defaultWarn) => {
382
+ const lines = models.map((entry) => computeModelCost(entry, prices, pricedAt, warn));
297
383
  return {
298
384
  lines,
299
385
  totalInputTokens: lines.reduce((s, l) => s + l.inputTokens, 0),
@@ -660,6 +746,15 @@ var priorBelowFloorNits = (priorDoc, floor = DEFAULT_NIT_VISIBILITY_FLOOR) => {
660
746
  };
661
747
  var convergenceBadge = (c) => c.converged ? `**Convergence** \u{1F3C1} ${formatScore(c.score)} \u2264 ${String(c.threshold)} \u2014 converged` : `**Convergence** \u{1F504} ${formatScore(c.score)} > ${String(c.threshold)} \u2014 iterating`;
662
748
  var convergenceSummary = (doc, threshold = DEFAULT_CONVERGENCE_THRESHOLD) => convergenceBadge(convergenceSignal(doc, threshold));
749
+ var changeSizeSummary = (changeSize) => {
750
+ if (changeSize === void 0) return "";
751
+ const cell = (label, d) => d === void 0 ? null : `+${String(d.added)} / \u2212${String(d.removed)} ${label}`;
752
+ return [
753
+ cell("code", changeSize.code),
754
+ cell("tests", changeSize.tests),
755
+ cell("docs", changeSize.docs)
756
+ ].filter((c) => c !== null).join(" \xB7 ");
757
+ };
663
758
  var nextRoundNumber = (priorTraj, priorConvRounds) => {
664
759
  const last = (rounds) => rounds.length > 0 ? rounds[rounds.length - 1]?.round ?? rounds.length : 0;
665
760
  return Math.max(last(priorTraj), last(priorConvRounds)) + 1;
@@ -1133,7 +1228,8 @@ var render = (input) => {
1133
1228
  const usageAvailable = input.envelope !== null;
1134
1229
  const hasUsage = input.envelope !== null && input.envelope.models.length > 0;
1135
1230
  const incomplete = (input.incomplete ?? input.envelope?.incomplete ?? false) || isIncompleteFindings(input.findings);
1136
- const costReport = input.envelope ? computeCost(input.envelope.models, input.prices) : null;
1231
+ const pricedAt = parseInstant(input.envelope?.generated_at) ?? input.pricedAt;
1232
+ const costReport = input.envelope ? computeCost(input.envelope.models, input.prices, pricedAt) : null;
1137
1233
  const pricesProvided = input.pricesProvided ?? true;
1138
1234
  const route = input.route ?? input.envelope?.route ?? null;
1139
1235
  const effort = input.effort ?? input.envelope?.effort ?? null;
@@ -1156,6 +1252,10 @@ var render = (input) => {
1156
1252
  effort,
1157
1253
  modelNames,
1158
1254
  testReport: input.testReport ?? null,
1255
+ // The agent's best-effort role split (issue #182) and the pipeline's deterministic cloc table; both
1256
+ // chrome, gated to a completed review by the template's !incomplete block.
1257
+ changeSummary: changeSizeSummary(input.findings.change_size),
1258
+ clocDiff: input.clocDiff !== void 0 ? escapeFence(input.clocDiff) : null,
1159
1259
  reviewedSha: input.reviewedSha ?? "0000000000000000000000000000000000000000",
1160
1260
  postedAt: input.postedAt ?? "",
1161
1261
  severityCounts,
@@ -1778,11 +1878,24 @@ var triageTable = [
1778
1878
  ];
1779
1879
  var pricesTable = [
1780
1880
  {
1881
+ // Retained non-latest so `validate`/`print-schema --schema-version 0.1` still resolves (the
1882
+ // keep-old-minors convention the findings table follows). The price map carries no version signal,
1883
+ // and the schema is a single unversioned file, so both entries point at it; the codec's oneOf
1884
+ // accepts a flat 0.1-era map unchanged.
1781
1885
  minor: "0.1",
1782
1886
  defaultVersion: "0.1.0",
1783
1887
  schemaFile: "prices.schema.json",
1784
1888
  codec: PriceMapCodec,
1785
1889
  normalize: identity,
1890
+ latest: false
1891
+ },
1892
+ {
1893
+ // v0.2.0 (issue #170): a model's value gained the time-slotted alternative (flat | { slots }).
1894
+ minor: "0.2",
1895
+ defaultVersion: "0.2.0",
1896
+ schemaFile: "prices.schema.json",
1897
+ codec: PriceMapCodec,
1898
+ normalize: identity,
1786
1899
  latest: true
1787
1900
  }
1788
1901
  ];
@@ -1827,7 +1940,8 @@ var resolveFindings = (raw) => {
1827
1940
  return decoded._tag === "Left" ? { kind: "invalid-shape", errors: formatErrors(decoded.left) } : { kind: "ok", version, value: entry.normalize(decoded.right) };
1828
1941
  };
1829
1942
  var resolveSingleVersion = (kind, raw) => {
1830
- const entry = tableFor(kind)[0];
1943
+ const table = tableFor(kind);
1944
+ const entry = table.find((e) => e.latest) ?? table[0];
1831
1945
  if (!entry) throw new Error(`Registry invariant violated \u2014 no entry for "${kind}"`);
1832
1946
  const decoded = entry.codec.decode(raw);
1833
1947
  return decoded._tag === "Left" ? { kind: "invalid-shape", errors: formatErrors(decoded.left) } : { kind: "ok", version: entry.defaultVersion, value: entry.normalize(decoded.right) };
@@ -2056,7 +2170,6 @@ var checkLongSuggestions = (comments) => {
2056
2170
  });
2057
2171
  return { comments: adjusted, longFiles };
2058
2172
  };
2059
- var PIPELINE_STAMPED_FIELDS = /* @__PURE__ */ new Set(["convergence", "scope_metastasis"]);
2060
2173
  var decodeFindings = (doc) => {
2061
2174
  const resolution = resolve("findings", doc);
2062
2175
  switch (resolution.kind) {
@@ -2077,14 +2190,14 @@ var loadFindings = (path) => {
2077
2190
  return { kind: "corrupt" };
2078
2191
  }
2079
2192
  const first = decodeFindings(raw);
2080
- if (first.kind === "invalid-shape" && typeof raw === "object" && raw !== null && !Array.isArray(raw) && Object.keys(raw).some((k) => PIPELINE_STAMPED_FIELDS.has(k))) {
2193
+ if (first.kind === "invalid-shape" && typeof raw === "object" && raw !== null && !Array.isArray(raw) && Object.keys(raw).some((k) => RECOVERABLE_OPTIONAL_FIELDS.has(k))) {
2081
2194
  const stripped = Object.fromEntries(
2082
- Object.entries(raw).filter(([key2]) => !PIPELINE_STAMPED_FIELDS.has(key2))
2195
+ Object.entries(raw).filter(([key2]) => !RECOVERABLE_OPTIONAL_FIELDS.has(key2))
2083
2196
  );
2084
2197
  const retry = decodeFindings(stripped);
2085
2198
  if (retry.kind === "ok") {
2086
2199
  process.stderr.write(
2087
- "Warning: the review draft carried an invalid pipeline-stamped field (convergence/scope_metastasis) \u2014 stripped it and used the rest; the pipeline re-stamps convergence\n"
2200
+ "Warning: the review draft carried an invalid best-effort field (convergence/scope_metastasis/change_size) \u2014 stripped it and used the rest; the pipeline re-stamps convergence\n"
2088
2201
  );
2089
2202
  return retry;
2090
2203
  }
@@ -2134,6 +2247,19 @@ var loadTestReport = (path) => {
2134
2247
  }
2135
2248
  return decoded.right;
2136
2249
  };
2250
+ var loadClocDiff = (path) => {
2251
+ let raw;
2252
+ try {
2253
+ raw = readFileSync(path, "utf-8");
2254
+ } catch (err) {
2255
+ process.stderr.write(
2256
+ `Warning: could not read cloc diff at ${path}: ${errMsg(err)} \u2014 omitting the cloc collapsible
2257
+ `
2258
+ );
2259
+ return void 0;
2260
+ }
2261
+ return raw.trim() === "" ? void 0 : raw;
2262
+ };
2137
2263
  var parseHtmlUrl = (raw) => {
2138
2264
  const parsed = tryParseJson(raw);
2139
2265
  const htmlUrl = parsed.ok ? asRecord(parsed.value)?.["html_url"] : void 0;
@@ -2511,6 +2637,7 @@ ${dropNote}` : ""}`,
2511
2637
  const answeredDropNote = answeredReRaiseNote(verbatimReRaised, droppedCount) + (verbatimReRaised.length > 0 && findings.findings.length === 0 ? "\n> _The stop signal reflects the kept findings \u2014 this round carries none._" : "");
2512
2638
  const envelope = loadEnvelope(input.envelopePath);
2513
2639
  const testReport = input.testReportPath ? loadTestReport(input.testReportPath) : void 0;
2640
+ const clocDiff = input.clocDiffPath ? loadClocDiff(input.clocDiffPath) : void 0;
2514
2641
  const effectiveRoute = input.route ?? envelope?.route;
2515
2642
  if (envelope === null) {
2516
2643
  const envelopelessIncomplete = isIncompleteFindings(findings);
@@ -2548,6 +2675,7 @@ ${dropNote}` : ""}`,
2548
2675
  roundCount: priorRoundCount,
2549
2676
  convergenceRound: false,
2550
2677
  testReport,
2678
+ clocDiff,
2551
2679
  inlineDisposition: { kind: "no-envelope" },
2552
2680
  runUrl: input.runUrl,
2553
2681
  jsonUrl: input.jsonUrl,
@@ -2626,6 +2754,7 @@ ${dropNote}` : ""}`,
2626
2754
  reviewedSha: input.headSha,
2627
2755
  effort: input.effort,
2628
2756
  testReport,
2757
+ clocDiff,
2629
2758
  severityCounts: currentCounts,
2630
2759
  sameRootNotes,
2631
2760
  answeredNotes: reRaisedNotes,
@@ -2639,7 +2768,8 @@ ${dropNote}` : ""}`,
2639
2768
  runUrl: input.runUrl,
2640
2769
  jsonUrl: input.jsonUrl,
2641
2770
  findingsPointer: findingsMarker,
2642
- postedAt: input.postedAt
2771
+ postedAt: input.postedAt,
2772
+ pricedAt: input.pricedAt
2643
2773
  };
2644
2774
  const longFilesNote = longFiles.length > 0 ? `
2645
2775
 
@@ -3084,6 +3214,7 @@ var renderOutputs = (result) => {
3084
3214
  conclusion=${result.conclusion}
3085
3215
  diff_size=${String(result.diffSize)}
3086
3216
  stacked=${String(result.stacked)}
3217
+ base_sha=${result.baseSha}
3087
3218
  `;
3088
3219
  }
3089
3220
  };
@@ -3353,7 +3484,8 @@ var gather = async (input, ghApi = runGhApi, gitRun = runGit) => {
3353
3484
  pr: prNumber,
3354
3485
  conclusion: input.conclusion,
3355
3486
  diffSize: Buffer.byteLength(prDiff, "utf8"),
3356
- stacked
3487
+ stacked,
3488
+ baseSha: meta.base_sha
3357
3489
  };
3358
3490
  };
3359
3491
 
@@ -3586,10 +3718,16 @@ var nativeTelemetry = (native, meta) => resolveTelemetry(
3586
3718
  );
3587
3719
  var absentTelemetry = (meta) => resolveTelemetry({ models: [], turns: 0, durationMs: 0, vendorCostUsd: null }, meta);
3588
3720
  var buildEnvelope = (telemetry, native, agentFilePath, agentFileFallbackPath, seedUnrevised) => {
3721
+ const generated_at = (/* @__PURE__ */ new Date()).toISOString();
3589
3722
  const outcome = findingsOutcome(native, agentFilePath, agentFileFallbackPath);
3590
3723
  switch (outcome.kind) {
3591
3724
  case "ok":
3592
- return { schema_version: outcome.version, findings: outcome.findings, ...telemetry };
3725
+ return {
3726
+ schema_version: outcome.version,
3727
+ findings: outcome.findings,
3728
+ generated_at,
3729
+ ...telemetry
3730
+ };
3593
3731
  case "telemetry-only": {
3594
3732
  const reason = seedUnrevised ? "the review agent did not write a review (its draft is still the pre-seeded sentinel)" : outcome.reason;
3595
3733
  return {
@@ -3598,6 +3736,7 @@ var buildEnvelope = (telemetry, native, agentFilePath, agentFileFallbackPath, se
3598
3736
 
3599
3737
  ${reason}`),
3600
3738
  incomplete: true,
3739
+ generated_at,
3601
3740
  ...telemetry
3602
3741
  };
3603
3742
  }
@@ -3802,6 +3941,7 @@ var resolvePrices = (pricesArg) => {
3802
3941
  return { kind: "absent", path: bundledPath("schema", "prices.example.json") };
3803
3942
  };
3804
3943
  var TEST_REPORT_DESCRIPTION = 'Path to a JSON test summary: {"passed": number, "failed": number, "total": number, "failures"?: [{"name": string, "message"?: string}]}';
3944
+ var CLOC_DIFF_DESCRIPTION = "Path to a raw `cloc --git --diff <base> <head>` table, rendered verbatim in the sticky's cloc collapsible. Best-effort: an absent, unreadable, or empty file omits the collapsible.";
3805
3945
  var CONVERGENCE_THRESHOLD_DESCRIPTION = "Advisory convergence tolerance: the per-finding convergence score (each finding's severity floor + confidence-and-likelihood-weighted headroom; ceilings critical 4 \xB7 major 2 \xB7 minor 1 \xB7 nit 0) at or below which the sticky reads as converged. The floor values and the systemic-likelihood rule are documented in the README and the findings schema (default: 1)";
3806
3946
  var NIT_VISIBILITY_FLOOR_DESCRIPTION = "Nit visibility floor: nits whose confidence \xD7 likelihood falls below this are hidden from humans (no inline comment; a collapsed aside in the sticky) but kept in the machine blob as adjudicated. In [0, 1] (default: 0.25)";
3807
3947
  var renderCmd = defineCommand({
@@ -3844,6 +3984,10 @@ var renderCmd = defineCommand({
3844
3984
  type: "string",
3845
3985
  description: TEST_REPORT_DESCRIPTION
3846
3986
  },
3987
+ "cloc-diff": {
3988
+ type: "string",
3989
+ description: CLOC_DIFF_DESCRIPTION
3990
+ },
3847
3991
  "convergence-threshold": {
3848
3992
  type: "string",
3849
3993
  description: CONVERGENCE_THRESHOLD_DESCRIPTION
@@ -3861,6 +4005,7 @@ var renderCmd = defineCommand({
3861
4005
  const prices = decode(PriceMapCodec.decode(readJSON(priceResolution.path)), "prices");
3862
4006
  const template = readFileSync(templatePath, "utf-8");
3863
4007
  const testReport = args["test-report"] ? decode(TestSummaryCodec.decode(readJSON(args["test-report"])), "test report") : void 0;
4008
+ const clocDiff = args["cloc-diff"] ? loadClocDiff(args["cloc-diff"]) : void 0;
3864
4009
  const route = args.route || envelope.route || null;
3865
4010
  const isRound = isConvergenceRound(route, envelope.incomplete === true || isIncompleteFindings(findings)) && isReviewVerdict(findings.verdict);
3866
4011
  const threshold = parseConvergenceThreshold(args["convergence-threshold"]);
@@ -3878,10 +4023,12 @@ var renderCmd = defineCommand({
3878
4023
  route: args.route,
3879
4024
  effort: args.effort,
3880
4025
  testReport,
4026
+ clocDiff,
3881
4027
  convergenceThreshold: threshold,
3882
4028
  nitVisibilityFloor: parseNitVisibilityFloor(args["nit-visibility-floor"]),
3883
4029
  convergenceRound: isRound,
3884
- postedAt: formatUtc(/* @__PURE__ */ new Date())
4030
+ postedAt: formatUtc(/* @__PURE__ */ new Date()),
4031
+ pricedAt: /* @__PURE__ */ new Date()
3885
4032
  });
3886
4033
  process.stdout.write(output2);
3887
4034
  }
@@ -3946,7 +4093,11 @@ var costCmd = defineCommand({
3946
4093
  run: async ({ args }) => {
3947
4094
  const envelope = decode(ResultEnvelopeCodec.decode(readJSON(args.envelope)), "envelope");
3948
4095
  const prices = decode(PriceMapCodec.decode(readJSON(args.prices)), "prices");
3949
- const report = computeCost(envelope.models, prices);
4096
+ const report = computeCost(
4097
+ envelope.models,
4098
+ prices,
4099
+ parseInstant(envelope.generated_at) ?? /* @__PURE__ */ new Date()
4100
+ );
3950
4101
  process.stdout.write(JSON.stringify(report, null, 2));
3951
4102
  }
3952
4103
  });
@@ -3977,7 +4128,11 @@ var checkCostCmd = defineCommand({
3977
4128
  const usage = sumTranscriptUsage(tree.entries);
3978
4129
  const priceResolution = resolvePrices(args.prices);
3979
4130
  const prices = decode(PriceMapCodec.decode(readJSON(priceResolution.path)), "prices");
3980
- const report = computeCost(usage.models, prices);
4131
+ const report = computeCost(
4132
+ usage.models,
4133
+ prices,
4134
+ usage.lastTsMs !== null ? new Date(usage.lastTsMs) : /* @__PURE__ */ new Date()
4135
+ );
3981
4136
  process.stdout.write(
3982
4137
  `${JSON.stringify(
3983
4138
  {
@@ -4121,7 +4276,17 @@ var budgetHookCmd = defineCommand({
4121
4276
  const tree = transcriptPath ? readTranscriptTree(resolve$1(transcriptPath)) : void 0;
4122
4277
  const usage = tree ? sumTranscriptUsage(tree.entries) : void 0;
4123
4278
  const prices = args.prices ? tryReadPrices(args.prices) : null;
4124
- const spentUsd = prices !== null && usage ? computeCost(usage.models, prices).totalCostUSD : null;
4279
+ const spentUsd = prices !== null && usage ? (
4280
+ // Price at the transcript's last activity instant, not the wall clock (issue #170 review
4281
+ // r2). Silent warn: this budget-steering cost is recomputed on EVERY tool event, so a
4282
+ // misconfigured slot map would otherwise flood stderr; the final post's cost render warns.
4283
+ computeCost(
4284
+ usage.models,
4285
+ prices,
4286
+ usage.lastTsMs !== null ? new Date(usage.lastTsMs) : /* @__PURE__ */ new Date(),
4287
+ () => void 0
4288
+ ).totalCostUSD
4289
+ ) : null;
4125
4290
  const wallMs = args.wall ? parseWallMs(args.wall) : null;
4126
4291
  const output2 = evaluateBudgetHook(input, {
4127
4292
  spentUsd,
@@ -4322,7 +4487,6 @@ ${printableSchema(schemaPath)}
4322
4487
  }
4323
4488
  });
4324
4489
  var isSurfaceStampedDoc = (doc) => typeof doc === "object" && doc !== null && !Array.isArray(doc) && doc["schema_version"] === SURFACE_SCHEMA_VERSION;
4325
- var PIPELINE_STAMPED_FIELDS2 = /* @__PURE__ */ new Set(["scope_metastasis", "convergence"]);
4326
4490
  var withoutScopeMetastasis = (doc) => typeof doc === "object" && doc !== null && !Array.isArray(doc) ? Object.fromEntries(Object.entries(doc).filter(([key2]) => key2 !== "scope_metastasis")) : doc;
4327
4491
  var seedDraftCmd = defineCommand({
4328
4492
  meta: {
@@ -4467,12 +4631,12 @@ var seedDraftCmd = defineCommand({
4467
4631
  const schemaPath = args.schema ? resolve$1(args.schema) : schemaPathFor(kind, args["schema-version"]);
4468
4632
  const barePrior = typeof priorFindings === "object" && !Array.isArray(priorFindings) ? Object.fromEntries(
4469
4633
  Object.entries(priorFindings).filter(
4470
- ([key2]) => !PIPELINE_STAMPED_FIELDS2.has(key2)
4634
+ ([key2]) => !RECOVERABLE_OPTIONAL_FIELDS.has(key2)
4471
4635
  )
4472
4636
  ) : priorFindings;
4473
4637
  const accepts = (doc) => validateAgainstSchema(doc, schemaPath).valid && resolve("findings", doc).kind === "ok";
4474
4638
  const seedDoc = accepts(priorFindings) ? priorFindings : accepts(barePrior) ? (process.stderr.write(
4475
- `Note: the in-force schema rejects the carried scope_metastasis entry \u2014 seeding the prior without it (issue #150 review r2)
4639
+ `Note: the in-force schema rejects a carried recoverable field (scope_metastasis/convergence/change_size) \u2014 seeding the prior without it (issue #150 review r2 / #182 review r2)
4476
4640
  `
4477
4641
  ), barePrior) : null;
4478
4642
  if (seedDoc === null) return false;
@@ -4961,6 +5125,10 @@ var postCmd = defineCommand({
4961
5125
  type: "string",
4962
5126
  description: TEST_REPORT_DESCRIPTION
4963
5127
  },
5128
+ "cloc-diff": {
5129
+ type: "string",
5130
+ description: CLOC_DIFF_DESCRIPTION
5131
+ },
4964
5132
  "run-url": {
4965
5133
  type: "string",
4966
5134
  description: "Workflow run URL (transcript/traces), rendered as a link in the LLM Disclosure aside"
@@ -4994,11 +5162,13 @@ var postCmd = defineCommand({
4994
5162
  headBranch: args["head-branch"],
4995
5163
  effort: args.effort,
4996
5164
  testReportPath: args["test-report"],
5165
+ clocDiffPath: args["cloc-diff"],
4997
5166
  runUrl: args["run-url"],
4998
5167
  jsonUrl: args["json-url"],
4999
5168
  convergenceThreshold: parseConvergenceThreshold(args["convergence-threshold"]),
5000
5169
  nitVisibilityFloor: parseNitVisibilityFloor(args["nit-visibility-floor"]),
5001
- postedAt: formatUtc(/* @__PURE__ */ new Date())
5170
+ postedAt: formatUtc(/* @__PURE__ */ new Date()),
5171
+ pricedAt: /* @__PURE__ */ new Date()
5002
5172
  });
5003
5173
  }
5004
5174
  });