@codedology/code-forge 0.7.2 → 0.7.4

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.
@@ -30,6 +30,13 @@
30
30
  * block's review budget (§4.10, B19): the forecast over the block's changed owned files (tier from
31
31
  * the path floors, before S1), one `review.budget` row per block; a failure there never fails the
32
32
  * review.
33
+ * B55: every round gets the packet's signed schema-miss history (`ledgerSchemaHistory`: rows of
34
+ * this run for this file and content hash whose MAC verifies with the run key), so a packet's
35
+ * second `schema` miss — this round's first try after the earlier ticket's — is tried once on
36
+ * `review.second_levels.<level>` when one is configured.
37
+ * B56: every round gets ONE memoised reader of the block's OTHER changed files' diffs
38
+ * (`blockPeerDiffs`: listed and read at most once per ticket) so the review can name code moved
39
+ * between them in a `## moved code` packet section (round 1, rechecks and patch check alike).
33
40
  * The review NEVER approves an empty or failed session: that is `unavailable` (§4.2 stub guard).
34
41
  * A throw inside the round is `unavailable` too, with the state saved as `next: retry` (see
35
42
  * `fixLoopRound`): the next ticket re-runs that round, never a fresh round 1.
@@ -42,9 +49,12 @@ import { askJev } from '../decide/jev-client.mjs';
42
49
  import { planAndRecord, tierOf } from '../review/budget.mjs';
43
50
  import { budgetFor } from '../review/packet.mjs';
44
51
  import { reviewFile, rulesRisk } from '../review/engine.mjs';
52
+ import { peerDiffReader } from '../review/moved.mjs';
45
53
  import { computeFileSet, ownsFile } from '../gates/scope.mjs';
46
54
  import { extraRoundsFor, newFileState, reopenForExtraRound, runRound } from '../review/fixloop.mjs';
47
55
  import { readRun } from '../state/run.mjs';
56
+ import { ledgerSchemaHistory } from '../review/schema-fallback.mjs';
57
+ import { verifyRow } from '../state/signer.mjs';
48
58
  import { redact } from '../util/redact.mjs';
49
59
  import { keyedLock } from '../util/locks.mjs';
50
60
  import { blockAnchors, blockRungUsed, loadState, saveState } from './review-state.mjs';
@@ -200,19 +210,60 @@ export async function reviewTicket(ticket, ctx) {
200
210
  return { status: 'unavailable', reason: err instanceof Error ? err.message : 'run-record-unreadable', approved: false, engine: 'adaptive', sessions: [] };
201
211
  }
202
212
  if (entry.kind === null) entry = { ...entry, kind: blockKind({ owned: [ticket.file] }) };
213
+ const peerDiffs = blockPeerDiffs(ctx.repoRoot, { base: entry.base, owned: entry.owned, file: ticket.file });
203
214
  try {
204
215
  if (!ctx.key) {
205
216
  return await reviewFile(
206
- { repoRoot: ctx.repoRoot, file: ticket.file, base: entry.base, cfg: ctx.cfg, kind: entry.kind, workDir, factsExcerpt: acceptanceExcerpt(entry.acceptance, ctx.cfg) },
217
+ { repoRoot: ctx.repoRoot, file: ticket.file, base: entry.base, cfg: ctx.cfg, kind: entry.kind, workDir, factsExcerpt: acceptanceExcerpt(entry.acceptance, ctx.cfg), peerDiffs },
207
218
  { spawn: ctx.spawn, ...(ctx.writeRow ? { writeRow: ctx.writeRow } : {}) },
208
219
  );
209
220
  }
210
- return await fixLoopRound(ticket, ctx, { base: entry.base, level: entry.level, owned: entry.owned, blockKindOf: entry.kind, key: ctx.key, workDir, factsExcerpt: acceptanceExcerpt(entry.acceptance, ctx.cfg) });
221
+ return await fixLoopRound(ticket, ctx, { base: entry.base, level: entry.level, owned: entry.owned, blockKindOf: entry.kind, key: ctx.key, workDir, factsExcerpt: acceptanceExcerpt(entry.acceptance, ctx.cfg), peerDiffs });
211
222
  } finally {
212
223
  rmSync(workDir, { recursive: true, force: true });
213
224
  }
214
225
  }
215
226
 
227
+ /**
228
+ * B56: the block's OTHER changed files — the §4.1 file set since the block base (tracked changes ∪
229
+ * untracked), filtered to the block's owned files (`ownsFile`, as the budget row does), minus the
230
+ * ticket's file. The review names code moved between them (`## moved code`). No base, no owned
231
+ * list, or a git failure ⇒ [] (no hint; the review itself goes on unchanged).
232
+ * @param {string} repoRoot @param {{base: string | null, owned: string[], file: string}} opts
233
+ * @returns {Promise<string[]>}
234
+ */
235
+ export async function blockPeers(repoRoot, { base, owned, file }) {
236
+ if (base === null || !Array.isArray(owned) || owned.length === 0) return [];
237
+ try {
238
+ return (await computeFileSet({ cwd: repoRoot, base })).all.filter((f) => f !== file && ownsFile(owned, f));
239
+ } catch {
240
+ return [];
241
+ }
242
+ }
243
+
244
+ /**
245
+ * B56: ONE memoised reader per ticket of the block's other changed files' diffs: the file set is
246
+ * listed and the diffs read (`peerDiffReader`) at most once, however many packets ask — lazily, so
247
+ * a round with no packet (a stopped file) runs no git call. Any throw or rejection on the way ⇒
248
+ * no peer diffs (no hint), memoised like a success.
249
+ * @param {string} repoRoot @param {{base: string | null, owned: string[], file: string}} opts
250
+ * @returns {() => Promise<import('../review/moved.mjs').PeerDiff[]>}
251
+ */
252
+ export function blockPeerDiffs(repoRoot, { base, owned, file }) {
253
+ /** @type {Promise<import('../review/moved.mjs').PeerDiff[]> | null} */
254
+ let once = null;
255
+ return () => {
256
+ once ??= (async () => {
257
+ try {
258
+ return await peerDiffReader({ repoRoot, base, file, peers: await blockPeers(repoRoot, { base, owned, file }) })();
259
+ } catch {
260
+ return [];
261
+ }
262
+ })();
263
+ return once;
264
+ };
265
+ }
266
+
216
267
  /**
217
268
  * The key of a block's lock (B42): the block's budget row and its L3 rung are taken under it.
218
269
  * It carries the run, so two runs in one process never share a lock.
@@ -275,10 +326,10 @@ export function mayTakeRung(state, kind, cfg, rungUsed) {
275
326
  * first full round, or any round once the rung is gone) runs outside the block lock, in parallel
276
327
  * with the block's other files.
277
328
  * @param {import('./queue.mjs').Ticket} ticket @param {ReviewContext} ctx
278
- * @param {{base: string | null, level: string, owned: string[], blockKindOf: string, key: Buffer, workDir: string, factsExcerpt: string}} opts
329
+ * @param {{base: string | null, level: string, owned: string[], blockKindOf: string, key: Buffer, workDir: string, factsExcerpt: string, peerDiffs?: () => Promise<import('../review/moved.mjs').PeerDiff[]>}} opts
279
330
  * @returns {Promise<ReviewOutcome>}
280
331
  */
281
- async function fixLoopRound(ticket, ctx, { base, level, owned, blockKindOf, key, workDir, factsExcerpt }) {
332
+ async function fixLoopRound(ticket, ctx, { base, level, owned, blockKindOf, key, workDir, factsExcerpt, peerDiffs }) {
282
333
  if (!ctx.readRows || !ctx.writeRow) return { status: 'unavailable', reason: 'no-ledger', approved: false, engine: 'adaptive', sessions: [] };
283
334
  const readRows = ctx.readRows;
284
335
  /** @type {{outcome: ReviewOutcome} | {round: () => Promise<ReviewOutcome>}} */
@@ -318,7 +369,7 @@ async function fixLoopRound(ticket, ctx, { base, level, owned, blockKindOf, key,
318
369
  state.l3_rung_used = true; // the orchestrator ran the block's one L3 patch before this ticket
319
370
  }
320
371
  const fixed = state;
321
- const round = () => playRound(ticket, ctx, { state: fixed, kind, where, seq: loaded.seq, base, blockKindOf, workDir, factsExcerpt, extraRounds });
372
+ const round = () => playRound(ticket, ctx, { state: fixed, kind, where, seq: loaded.seq, base, blockKindOf, workDir, factsExcerpt, extraRounds, peerDiffs });
322
373
  // a round that could take the rung keeps the block lock until its state is saved
323
374
  if (mayTakeRung(fixed, kind, ctx.cfg, rungUsed)) return { outcome: await round() };
324
375
  return { round };
@@ -329,10 +380,10 @@ async function fixLoopRound(ticket, ctx, { base, level, owned, blockKindOf, key,
329
380
  /**
330
381
  * Run the prepared round and save the file's state: the ticket's outcome.
331
382
  * @param {import('./queue.mjs').Ticket} ticket @param {ReviewContext} ctx
332
- * @param {{state: import('../review/fixloop.mjs').FileState, kind: 'full' | 'recheck' | 'patch_check', where: import('./review-state.mjs').Where, seq: number, base: string | null, blockKindOf: string, workDir: string, factsExcerpt: string, extraRounds?: number}} opts
383
+ * @param {{state: import('../review/fixloop.mjs').FileState, kind: 'full' | 'recheck' | 'patch_check', where: import('./review-state.mjs').Where, seq: number, base: string | null, blockKindOf: string, workDir: string, factsExcerpt: string, extraRounds?: number, peerDiffs?: () => Promise<import('../review/moved.mjs').PeerDiff[]>}} opts
333
384
  * @returns {Promise<ReviewOutcome>}
334
385
  */
335
- async function playRound(ticket, ctx, { state, kind, where, seq, base, blockKindOf, workDir, factsExcerpt, extraRounds = 0 }) {
386
+ async function playRound(ticket, ctx, { state, kind, where, seq, base, blockKindOf, workDir, factsExcerpt, extraRounds = 0, peerDiffs }) {
336
387
  /** @type {Record<string, any> | null} */
337
388
  let engineOutcome = null;
338
389
  let approvalWritten = false;
@@ -355,6 +406,23 @@ async function playRound(ticket, ctx, { state, kind, where, seq, base, blockKind
355
406
  /** @type {string | null} */
356
407
  let crashed = null;
357
408
  try {
409
+ // B55: a packet's schema misses are counted from the run's signed rows for this run, file,
410
+ // content hash AND packet hash (`ledgerSchemaHistory` is asked per packet). Built inside the
411
+ // guarded block: a throw here is this round's `unavailable` with `next: retry`, like any other;
412
+ // a ledger read that throws later only disables the second-level try (with one warning line).
413
+ // Once per packet holds because this ticket holds the (block, file) file lock (`loop.mjs`) for
414
+ // the whole round — round 1, its retry, a recheck and the patch_check alike — and
415
+ // `schema-fallback.mjs` adds a per-packet lock around read-history → write-mark.
416
+ // The rows are matched on the values the worker's writeRow stamps on them (`loop.mjs`:
417
+ // `{...row, block, file, content_hash: ticket.content_hash}`): `ticket.file` and
418
+ // `ticket.content_hash` here are the same ticket's, for round 1 and rechecks alike (a recheck
419
+ // ticket's hash is the fixed content's). `where.key` is the run key: `loop.mjs` puts the run's
420
+ // `key` in `ctx.key` (`key, // signs the fix-loop state`), `reviewTicket` passes `key: ctx.key`
421
+ // to `fixLoopRound`, which builds `where = {…, key, rows}`. A row whose check throws is not
422
+ // counted (`ledgerSchemaHistory` catches per row).
423
+ const schemaHistory = ctx.readRows
424
+ ? ledgerSchemaHistory({ readRows: ctx.readRows, verify: (row) => verifyRow(row, where.key).ok, runId: ctx.runId, file: ticket.file, contentHash: ticket.content_hash })
425
+ : undefined;
358
426
  await runRound(
359
427
  state,
360
428
  {
@@ -365,9 +433,11 @@ async function playRound(ticket, ctx, { state, kind, where, seq, base, blockKind
365
433
  writeRow,
366
434
  factsExcerpt,
367
435
  extraRounds,
436
+ ...(peerDiffs ? { peerDiffs } : {}),
368
437
  ...(jev ? { jev } : {}),
438
+ ...(schemaHistory ? { schemaHistory } : {}),
369
439
  review: async () => {
370
- engineOutcome = await reviewFile({ repoRoot: ctx.repoRoot, file: ticket.file, base, cfg: ctx.cfg, kind: blockKindOf, workDir, factsExcerpt }, { spawn: ctx.spawn, writeRow });
440
+ engineOutcome = await reviewFile({ repoRoot: ctx.repoRoot, file: ticket.file, base, cfg: ctx.cfg, kind: blockKindOf, workDir, factsExcerpt, ...(peerDiffs ? { peerDiffs } : {}) }, { spawn: ctx.spawn, writeRow, ...(schemaHistory ? { schemaHistory } : {}) });
371
441
  return engineOutcome;
372
442
  },
373
443
  spawn: async (opts) => {