Show compiler-generated claims with original evidence instead of missing source
This commit is contained in:
parent
088108a930
commit
70b8378109
5 changed files with 64 additions and 15 deletions
|
|
@ -113,6 +113,11 @@ separately shows approved or merging attempts. `GET /api/batches` supports `stat
|
||||||
`attemptState` (comma-separated attempt states), `author`, `before` and `limit`.
|
`attemptState` (comma-separated attempt states), `author`, `before` and `limit`.
|
||||||
Filters apply before pagination.
|
Filters apply before pagination.
|
||||||
|
|
||||||
|
Skip-claim reviews show the claim category and reason beside the original
|
||||||
|
instructions/pseudocode. They intentionally contain no submitted C++ or byte
|
||||||
|
comparison. Attempt details expose the original symbol identity independently of
|
||||||
|
compiled results so these reviews can load evidence without inventing a target.
|
||||||
|
|
||||||
The merge worker settles abandoned open code batches when they contain failed
|
The merge worker settles abandoned open code batches when they contain failed
|
||||||
attempts and no attempt remains live. It records the reason and an audit event,
|
attempts and no attempt remains live. It records the reason and an audit event,
|
||||||
preserving attempt feedback, task states, review votes and scores. This does not
|
preserving attempt feedback, task states, review votes and scores. This does not
|
||||||
|
|
|
||||||
|
|
@ -803,10 +803,16 @@ export function createApp(ctx) {
|
||||||
const result = a.result_json ? JSON.parse(a.result_json) : null;
|
const result = a.result_json ? JSON.parse(a.result_json) : null;
|
||||||
const batch = a.batch_id ? store.get('SELECT result_json FROM batches WHERE id = ?', a.batch_id) : null;
|
const batch = a.batch_id ? store.get('SELECT result_json FROM batches WHERE id = ?', a.batch_id) : null;
|
||||||
const batchResult = batch?.result_json ? JSON.parse(batch.result_json) : {};
|
const batchResult = batch?.result_json ? JSON.parse(batch.result_json) : {};
|
||||||
|
const symbol = store.get(
|
||||||
|
`SELECT s.id, s.name, ad.addr, ad.size FROM tasks t JOIN symbols s ON s.id = t.symbol_id
|
||||||
|
LEFT JOIN symbol_addresses ad ON ad.symbol_id = s.id AND ad.version_id = ? WHERE t.id = ?`,
|
||||||
|
store.currentVersion().id, a.task_id,
|
||||||
|
);
|
||||||
const isReviewer = hasRole(user, 'reviewer');
|
const isReviewer = hasRole(user, 'reviewer');
|
||||||
return {
|
return {
|
||||||
id: a.id,
|
id: a.id,
|
||||||
taskId: a.task_id,
|
taskId: a.task_id,
|
||||||
|
symbol: symbol ? { ...symbol, addr: symbol.addr === null ? null : '0x' + hex(symbol.addr) } : null,
|
||||||
author: a.author,
|
author: a.author,
|
||||||
state: a.state,
|
state: a.state,
|
||||||
score: a.score,
|
score: a.score,
|
||||||
|
|
@ -822,7 +828,7 @@ export function createApp(ctx) {
|
||||||
mergedFiles: isReviewer ? batchResult.mergedFiles ?? result?.mergedFiles ?? null : undefined,
|
mergedFiles: isReviewer ? batchResult.mergedFiles ?? result?.mergedFiles ?? null : undefined,
|
||||||
reviews: store.all('SELECT r.action, r.comment, r.created_at, u.name AS reviewer FROM reviews r JOIN users u ON u.id = r.reviewer_id WHERE r.attempt_id = ?', a.id),
|
reviews: store.all('SELECT r.action, r.comment, r.created_at, u.name AS reviewer FROM reviews r JOIN users u ON u.id = r.reviewer_id WHERE r.attempt_id = ?', a.id),
|
||||||
kind: a.kind,
|
kind: a.kind,
|
||||||
claim: a.kind === 'skip' ? { category: a.skip_category, reason: a.notes } : undefined,
|
claim: a.kind === 'skip' ? { category: a.skip_category, reason: a.notes, noSourceExpected: true } : undefined,
|
||||||
peerReviews: peers.verdicts(a.id),
|
peerReviews: peers.verdicts(a.id),
|
||||||
peerRequired: peers.needed(peers.counts(a.id).different), // "same" verdicts (by weight) that accept it as it stands
|
peerRequired: peers.needed(peers.counts(a.id).different), // "same" verdicts (by weight) that accept it as it stands
|
||||||
targetSource: targetSource(result?.feedback?.target?.id, batchResult.mergedFiles ?? store.attemptFiles(a)),
|
targetSource: targetSource(result?.feedback?.target?.id, batchResult.mergedFiles ?? store.attemptFiles(a)),
|
||||||
|
|
|
||||||
|
|
@ -1241,8 +1241,11 @@ async function openAttempt(id) {
|
||||||
if (!location.hash.startsWith(`#/review/${id}`)) history.replaceState(null, '', `#/review/${id}`);
|
if (!location.hash.startsWith(`#/review/${id}`)) history.replaceState(null, '', `#/review/${id}`);
|
||||||
const f = a.feedback || {};
|
const f = a.feedback || {};
|
||||||
const t = f.target;
|
const t = f.target;
|
||||||
$('attempt-title').textContent = `${t ? t.demangled || t.id : `Attempt #${a.id}`}`;
|
const identity = a.symbol ?? t;
|
||||||
$('attempt-meta').textContent = `#${a.id} by ${a.author} · ${t ? `${t.id} · ` : ''}${a.state.replace('_', ' ')} · ${((a.score || 0) * 100).toFixed(1)}% of bytes · size ${t?.size?.ours ?? '?'} / ${t?.size?.original ?? '?'} · base ${a.baseCommit.slice(0, 10)}`;
|
const claimed = a.kind === 'skip';
|
||||||
|
$('attempt-title').textContent = `${t?.demangled || identity?.name || identity?.id || `Attempt #${a.id}`}`;
|
||||||
|
const measurement = claimed ? 'no handwritten source submitted; byte matching does not apply' : `${a.score === null ? 'unmeasured' : `${(a.score * 100).toFixed(1)}% of bytes`} · size ${t?.size?.ours ?? '?'} / ${t?.size?.original ?? '?'}`;
|
||||||
|
$('attempt-meta').textContent = `#${a.id} by ${a.author} · ${identity?.id ? `${identity.id} · ` : ''}${a.state.replace('_', ' ')} · ${measurement} · base ${a.baseCommit.slice(0, 10)}`;
|
||||||
const n = (v) => (a.peerReviews ?? []).filter((p) => p.verdict === v).length;
|
const n = (v) => (a.peerReviews ?? []).filter((p) => p.verdict === v).length;
|
||||||
const vm = $('attempt-verdicts');
|
const vm = $('attempt-verdicts');
|
||||||
vm.replaceChildren(el('span', 'same'), pips(n('same'), 0, a.peerRequired ?? 5), el('span', 'different'), pips(0, n('different'), 0));
|
vm.replaceChildren(el('span', 'same'), pips(n('same'), 0, a.peerRequired ?? 5), el('span', 'different'), pips(0, n('different'), 0));
|
||||||
|
|
@ -1258,12 +1261,15 @@ async function openAttempt(id) {
|
||||||
r.append(d);
|
r.append(d);
|
||||||
}
|
}
|
||||||
|
|
||||||
compare = { source: a.targetSource, ourAsm: t?.asm ?? null, original: null, error: null };
|
compare = { source: a.targetSource, ourAsm: t?.asm ?? null, original: null, error: null, claim: a.claim ?? null };
|
||||||
showCompare(compareView);
|
const selectedCompare = compare;
|
||||||
if (t?.id) {
|
document.querySelector('#compare-tabs [data-view="code"]').textContent = claimed ? 'Claim and original pseudocode' : 'C++ vs pseudocode';
|
||||||
api(`/api/original/${t.id}`)
|
document.querySelector('#compare-tabs [data-view="asm"]').textContent = claimed ? 'Original assembly' : 'Assembly vs assembly';
|
||||||
.then((o) => (compare.original = o))
|
showCompare(claimed ? 'asm' : compareView);
|
||||||
.catch((e) => (compare.error = e.message))
|
if (identity?.id) {
|
||||||
|
api(`/api/original/${identity.id}`)
|
||||||
|
.then((o) => { if (currentAttempt === a) selectedCompare.original = o; })
|
||||||
|
.catch((e) => { if (currentAttempt === a) selectedCompare.error = e.message; })
|
||||||
.finally(() => currentAttempt === a && showCompare(compareView));
|
.finally(() => currentAttempt === a && showCompare(compareView));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1281,11 +1287,15 @@ async function openAttempt(id) {
|
||||||
fold(`Other functions in the file (${f.otherFunctions.length}, ${exact} byte-identical)`, table(['Function', 'Status', 'Score'], f.otherFunctions.map((x) => [x.id, x.status, `${(x.score * 100).toFixed(1)}%`])));
|
fold(`Other functions in the file (${f.otherFunctions.length}, ${exact} byte-identical)`, table(['Function', 'Status', 'Score'], f.otherFunctions.map((x) => [x.id, x.status, `${(x.score * 100).toFixed(1)}%`])));
|
||||||
}
|
}
|
||||||
const files = Object.entries(a.mergedFiles || a.files || {});
|
const files = Object.entries(a.mergedFiles || a.files || {});
|
||||||
fold(`Submitted files (${files.length})`, ...files.map(([p, content]) => {
|
if (claimed) {
|
||||||
const d = el('details');
|
fold('No source files expected', el('p', 'This is a claim about how the original function was produced, not a C++ implementation. Review the stated evidence against the original instructions.'));
|
||||||
d.append(el('summary', p), el('pre', content, 'code'));
|
} else {
|
||||||
return d;
|
fold(`Submitted files (${files.length})`, ...files.map(([p, content]) => {
|
||||||
}));
|
const d = el('details');
|
||||||
|
d.append(el('summary', p), el('pre', content, 'code'));
|
||||||
|
return d;
|
||||||
|
}));
|
||||||
|
}
|
||||||
fold(a.notes ? 'Notes from the author (untrusted)' : 'Notes from the author (none)', el('pre', a.notes || '(none)', 'notes'));
|
fold(a.notes ? 'Notes from the author (untrusted)' : 'Notes from the author (none)', el('pre', a.notes || '(none)', 'notes'));
|
||||||
|
|
||||||
// Deciding a function still in peer review is for maintainers.
|
// Deciding a function still in peer review is for maintainers.
|
||||||
|
|
@ -1309,6 +1319,14 @@ function showCompare(view) {
|
||||||
if (!compare) return;
|
if (!compare) return;
|
||||||
const o = compare.original;
|
const o = compare.original;
|
||||||
const pending = !o && !compare.error ? 'Loading the original…' : `Original not available${compare.error ? `: ${compare.error}` : ''}`;
|
const pending = !o && !compare.error ? 'Loading the original…' : `Original not available${compare.error ? `: ${compare.error}` : ''}`;
|
||||||
|
if (compare.claim) {
|
||||||
|
const claim = compare.claim;
|
||||||
|
const explanation = `No C++ implementation is expected for this claim (${claim.category}).\n\nReason (untrusted):\n${claim.reason}\n\nApprove only if the original instructions support the claim; otherwise request an implementation.`;
|
||||||
|
const original = view === 'asm' ? o?.asm : o?.pseudocode;
|
||||||
|
const title = view === 'asm' ? 'Original assembly' : 'Original pseudocode (reading aid)';
|
||||||
|
body.append(twoPanes(['No handwritten source required — claim', explanation], [o ? `${title} · ${o.addr}` : title, original ?? (o ? 'No original text is available in this view.' : pending)]));
|
||||||
|
return;
|
||||||
|
}
|
||||||
if (view === 'code') {
|
if (view === 'code') {
|
||||||
body.append(
|
body.append(
|
||||||
twoPanes(
|
twoPanes(
|
||||||
|
|
|
||||||
|
|
@ -413,6 +413,6 @@
|
||||||
<div class="hazard thin" aria-hidden="true"></div>
|
<div class="hazard thin" aria-hidden="true"></div>
|
||||||
<p>Clank Together · source is open: <code id="clone-cmd">git clone https://clank.rodecomp.online/git/clank/game-decomp.git</code></p>
|
<p>Clank Together · source is open: <code id="clone-cmd">git clone https://clank.rodecomp.online/git/clank/game-decomp.git</code></p>
|
||||||
</footer>
|
</footer>
|
||||||
<script src="/app.js?v=batch-attempts-20261027"></script>
|
<script src="/app.js?v=claim-review-20261027"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|
|
||||||
|
|
@ -1976,3 +1976,23 @@ test('the merger settles abandoned open batches without changing attempts, tasks
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('skip attempt details expose original identity without inventing a compiled target or source', async () => {
|
||||||
|
const token = await activeUser('skipdetail');
|
||||||
|
const user = ctx.accounts.byName('skipdetail');
|
||||||
|
const task = ctx.store.get(`SELECT t.id, t.symbol_id, s.name, a.addr, a.size FROM tasks t JOIN symbols s ON s.id=t.symbol_id JOIN symbol_addresses a ON a.symbol_id=s.id WHERE a.version_id=? LIMIT 1`, ctx.store.currentVersion().id);
|
||||||
|
const reason = 'Calls the ordinary destructor, tests flags bit 1, conditionally deletes storage and returns this.';
|
||||||
|
const id = Number(ctx.store.run(`INSERT INTO attempts (task_id,user_id,base_commit,files_json,state,kind,skip_category,notes,result_json,created_at) VALUES (?,?,?,'{}','peer_review','skip','compiler_generated',?,'{}',?)`, task.id,user.id,ctx.repo.head(),reason,now()).lastInsertRowid);
|
||||||
|
try {
|
||||||
|
const response = await call('GET', `/api/attempts/${id}`, null, token);
|
||||||
|
assert.equal(response.status, 200);
|
||||||
|
assert.deepEqual(response.body.symbol, { id: task.symbol_id, name: task.name, addr: '0x' + task.addr.toString(16).padStart(8, '0'), size: task.size });
|
||||||
|
assert.deepEqual(response.body.claim, { category: 'compiler_generated', reason, noSourceExpected: true });
|
||||||
|
assert.deepEqual(response.body.files, {});
|
||||||
|
assert.equal(response.body.score, null);
|
||||||
|
assert.equal(response.body.targetSource, null);
|
||||||
|
assert.equal(response.body.feedback, null);
|
||||||
|
} finally {
|
||||||
|
ctx.store.run('DELETE FROM attempts WHERE id = ?', id);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue