@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.
- package/CHANGELOG.md +20 -0
- package/README.md +15 -0
- package/docs/reference/blocks.json +33 -1
- package/package.json +1 -1
- package/skill/references/review.md +2 -2
- package/skill/scripts/forge +1 -1
- package/src/autopilot/binnacle.mjs +29 -1
- package/src/review/engine.mjs +135 -33
- package/src/review/fixloop.mjs +146 -33
- package/src/review/moved.mjs +399 -0
- package/src/review/packet.mjs +32 -23
- package/src/review/schema-fallback.mjs +351 -0
- package/src/review/sections.mjs +5 -4
- package/src/review/validate-review.mjs +11 -0
- package/src/session/spawn.mjs +3 -86
- package/src/util/diagnostic-tail.mjs +95 -0
- package/src/worker/engine.mjs +78 -8
package/src/worker/engine.mjs
CHANGED
|
@@ -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) => {
|