From 0710882703704da0e56006f9409a963cbc7caf62 Mon Sep 17 00:00:00 2001 From: Saty <5752427+SatyPardus@users.noreply.github.com> Date: Sun, 18 Oct 2026 04:48:19 +0200 Subject: [PATCH] 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 Claude-Session: https://claude.ai/code/session_01RNwtXPYqFu3oKaaXwYwNWW --- src/server/merge.js | 29 +++++++++++++++++------------ src/server/verify.js | 20 +++++++++++++++++--- src/server/web/app.js | 10 +++++++++- test/server.test.js | 14 ++++++++++++++ 4 files changed, 57 insertions(+), 16 deletions(-) diff --git a/src/server/merge.js b/src/server/merge.js index 424c11b..272b494 100644 --- a/src/server/merge.js +++ b/src/server/merge.js @@ -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,18 +290,22 @@ 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) { - 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, - ); - 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); - 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); - } + } + 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, 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', 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); diff --git a/src/server/verify.js b/src/server/verify.js index f408cb7..7bdaa1f 100644 --- a/src/server/verify.js +++ b/src/server/verify.js @@ -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`)) { diff --git a/src/server/web/app.js b/src/server/web/app.js index 6484f8d..8cfabce 100644 --- a/src/server/web/app.js +++ b/src/server/web/app.js @@ -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(); diff --git a/test/server.test.js b/test/server.test.js index 6ad59fd..fd8b498 100644 --- a/test/server.test.js +++ b/test/server.test.js @@ -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