Progress no longer drops when templates are recompiled; live progress

Templates and inline functions are compiled in every unit that uses them. A
merge that recompiled many units (a header change) let the last unit's copy
win, so functions that matched in one unit were recorded as match_unresolved
from another, and the regression check missed it because the better status
came from a unit it did not compile. Now each function counts with its best
compiled copy, and a check never lowers a status measured in a unit it did
not compile.

The progress gauge and the crew views refresh every 30 seconds; the gauge
moves from its previous reading.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RNwtXPYqFu3oKaaXwYwNWW
This commit is contained in:
Saty 2026-10-18 04:48:19 +02:00
commit 0710882703
4 changed files with 57 additions and 16 deletions

View file

@ -6,6 +6,7 @@ import { hasRole } from './accounts.js';
import { STATUS } from '../shared/compare.js';
import { returnBatch, batchAttempts, settleBatch } from './batches.js';
import { withoutFunctions } from './split.js';
import { RANK, bestResults } from './verify.js';
export class Merger {
constructor({ store, config, repo, project, tasks, verifier, getPe, peers, log = console }) {
@ -289,20 +290,24 @@ export class Merger {
for (const [decorated, id] of Object.entries(u.bindings)) {
this.store.run('UPDATE symbols SET decorated = ? WHERE id = ? AND decorated IS NULL', decorated, id);
}
for (const f of u.functions) {
}
const compiledUnits = new Set(check.units.filter((u) => u.compiled).map((u) => u.file));
for (const [id, { f, unit }] of bestResults(check)) {
// A status measured in a unit this check did not compile is not lowered from a partial view.
const old = this.store.get('SELECT status, file FROM func_status WHERE symbol_id = ?', id);
if (old && !compiledUnits.has(old.file) && RANK[old.status] > RANK[f.status]) continue;
this.store.run(
`INSERT INTO func_status (symbol_id, file, status, score, commit_hash, updated_at, confirmed, code_hash) VALUES (?, ?, ?, ?, ?, ?, ?, ?)
ON CONFLICT(symbol_id) DO UPDATE SET file = excluded.file, status = excluded.status, score = excluded.score, commit_hash = excluded.commit_hash, updated_at = excluded.updated_at, code_hash = COALESCE(excluded.code_hash, func_status.code_hash),
confirmed = CASE WHEN excluded.confirmed = 1 THEN 1 WHEN excluded.status IN ('exact', 'match_unresolved') THEN 0 ELSE func_status.confirmed END`,
f.id, u.file, f.status, f.score, commit, now(), confirmedIds.has(f.id) ? 1 : 0, f.codeHash ?? null,
f.id, unit, f.status, f.score, commit, now(), confirmedIds.has(f.id) ? 1 : 0, f.codeHash ?? null,
);
if (f.demangled) this.store.run('UPDATE symbols SET name = ? WHERE id = ? AND name IS NULL', f.demangled, f.id);
this.store.run('UPDATE symbols SET unit = ? WHERE id = ? AND unit IS NULL', u.file, f.id);
this.store.run('UPDATE symbols SET unit = ? WHERE id = ? AND unit IS NULL', unit, f.id);
if (f.status === STATUS.EXACT || f.status === STATUS.UNRESOLVED) {
this.store.run(`UPDATE tasks SET state = 'done', updated_at = ? WHERE symbol_id = ? AND state IN ('open', 'needs_cleanup', 'hard')`, now(), f.id);
}
}
}
for (const l of rec.learned) this.store.run('UPDATE symbols SET decorated = ? WHERE id = ? AND decorated IS NULL', l.decorated, l.id);
// Addresses in the current version found through matching code that references the symbol.
for (const l of rec.learnedAddresses ?? []) {

View file

@ -12,7 +12,22 @@ import { now } from './db.js';
import { returnBatch, batchAttempts } from './batches.js';
import { analyzeInWorkers } from './analyze.js';
const RANK = { [STATUS.EXACT]: 3, [STATUS.UNRESOLVED]: 2, [STATUS.MISMATCH]: 1, [STATUS.MISSING]: 0, [STATUS.NO_ADDRESS]: 0 };
export const RANK = { [STATUS.EXACT]: 3, [STATUS.UNRESOLVED]: 2, [STATUS.MISMATCH]: 1, [STATUS.MISSING]: 0, [STATUS.NO_ADDRESS]: 0 };
/**
* One result per function: templates and inline functions are compiled in every unit that uses
* them (and folded in the binary), so a function counts with its best compiled copy.
*/
export function bestResults(check) {
const best = new Map();
for (const u of check.units) {
for (const f of u.functions) {
const b = best.get(f.id);
if (!b || RANK[f.status] > RANK[b.f.status] || (RANK[f.status] === RANK[b.f.status] && f.score > b.f.score)) best.set(f.id, { f, unit: u.file });
}
}
return best;
}
export class Verifier {
constructor({ store, config, repo, project, tasks, getPe, peers, log = console }) {
@ -124,8 +139,7 @@ export class Verifier {
/** Functions that match on main but would no longer match with this change. */
regressions(check) {
const results = new Map();
for (const u of check.units) for (const f of u.functions) results.set(f.id, f);
const results = new Map([...bestResults(check)].map(([id, b]) => [id, b.f]));
const compiledFiles = new Set(check.units.map((u) => u.file));
const out = [];
for (const row of this.store.all(`SELECT * FROM func_status WHERE status IN ('exact', 'match_unresolved') OR confirmed = 1`)) {

View file

@ -61,6 +61,8 @@ function formData(form) {
// --- public
let lastPct = 0; // the gauge moves from the previous reading, not from zero, when it refreshes
async function loadProgress() {
const p = await api('/api/progress');
const body = $('progress-body');
@ -77,6 +79,8 @@ async function loadProgress() {
meter.setAttribute('aria-valuemax', '100');
meter.setAttribute('aria-valuenow', pct.toFixed(2));
const fill = el('div', null, 'fill');
fill.style.setProperty('width', `${Math.min(100, lastPct)}%`);
lastPct = pct;
meter.append(fill);
gauge.append(meter);
const conf = p.confirmed?.n ?? 0;
@ -647,5 +651,9 @@ api('/api/info')
.catch(() => {});
loadProgress().catch((e) => ($('progress-body').textContent = e.message));
loadCrew().catch((e) => ($('active-body').textContent = e.message));
setInterval(() => loadCrew().catch(() => {}), 60_000);
// Live numbers: progress and the crew views refresh on their own.
setInterval(() => {
loadProgress().catch(() => {});
loadCrew().catch(() => {});
}, 30_000);
refresh();

View file

@ -351,6 +351,20 @@ test('peer review: three other agents must agree, by reading only', async () =>
ctx.store.run(`UPDATE tasks SET state = 'done' WHERE id = ?`, task.id);
});
test('a function compiled in several units counts with its best copy', () => {
const v = ctx.store.currentVersion();
const id = ctx.store.ensureSymbol({ kind: 'function', versionId: v.id, addr: 0x401700, size: 8, unit: 'src/tpl_a.cpp' }).symbol.id;
const fn = (status, score) => ({ id, status, score, relocations: [], size: { ours: 8, original: 8 } });
const unit = (file, f) => ({ file, compiled: true, includes: [], bindings: {}, functions: [f] });
const rec = { conflicts: [], unknown: [], learned: [], learnedAddresses: [] };
// The template instance in a later unit references unnamed data; the earlier one matches.
ctx.merger.applyCheckToDatabase({ units: [unit('src/tpl_a.cpp', fn(STATUS.EXACT, 1)), unit('src/tpl_b.cpp', fn(STATUS.UNRESOLVED, 1))] }, rec, ctx.repo.head(), null);
assert.equal(ctx.store.get('SELECT status FROM func_status WHERE symbol_id = ?', id).status, STATUS.EXACT);
// A later check that only compiled tpl_b does not lower the status measured in tpl_a.
ctx.merger.applyCheckToDatabase({ units: [unit('src/tpl_b.cpp', fn(STATUS.UNRESOLVED, 1))] }, rec, ctx.repo.head(), null);
assert.equal(ctx.store.get('SELECT status FROM func_status WHERE symbol_id = ?', id).status, STATUS.EXACT);
});
test('the crew view is public and shows no account details', async () => {
const token = await activeUser('crewview');
await call('GET', '/api/tasks', null, token); // the agent checks in