feat(plan): the operative line map and a deterministic line diff for the critic loop [skip-docs]

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Kjell Tore Guttormsen 2026-10-15 16:49:21 +02:00
commit 3a0c2d0c54
Signed by: ktg
SSH key fingerprint: SHA256:JakMjO6FTBBzN0Bhfj9saOoEjaFxlSdYuZQQpM/lF9Q
3 changed files with 506 additions and 0 deletions

View file

@ -100,6 +100,47 @@ export function operativeText(text) {
.trim() + '\n';
}
/**
* The operative text with its line map, built from the same transforms as
* `operativeText` (whose output it returns unchanged).
* @returns {{text: string, fullToOp: Array<number|null>, opToFull: Array<number|null>}}
* 1-based both ways (index 0 is null). fullToOp[i] is null for a review-log
* line, the quality line or a collapsed blank; opToFull[j] is null for an
* operative line no file line carries (a blank left where a log section stood).
*/
export function operativeLines(text) {
const src = norm(text);
const full = src.split('\n');
const excluded = new Set();
for (const s of splitSections(src, 2)) {
if (!isLogTitle(s.title)) continue;
const n = logPart(s.text).log.split('\n').length;
for (let i = s.start; i < s.start + n; i++) excluded.add(i);
}
full.forEach((l, i) => { if (QUALITY_LINE.test(l)) excluded.add(i); });
const op = operativeText(text);
const opL = op.split('\n');
const opCount = opL.length - 1; // the text ends with '\n'; the last split element is not a line
const fullToOp = new Array(full.length + 1).fill(null);
const opToFull = new Array(opCount + 1).fill(null);
let j = 0;
for (let i = 0; i < full.length && j < opCount; i++) {
if (excluded.has(i)) continue;
let l = full[i].replace(/[ \t]+$/, '');
if (j === 0) l = l.replace(/^\s+/, '');
// Only blanks lose their partner, so a non-blank kept line facing an operative
// blank means that blank has no file line: step past it.
while (l !== '' && opL[j] === '' && j < opCount - 1) j += 1;
const want = opL[j];
if (l === want || (j === opCount - 1 && l.trimEnd() === want)) {
fullToOp[i + 1] = j + 1;
opToFull[j + 1] = i + 1;
j += 1;
}
}
return { text: op, fullToOp, opToFull };
}
export function planHash(text) {
return createHash('sha256').update(operativeText(text)).digest('hex');
}