done-done-axi 0.6.8 → 0.6.9
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/dist/design.js +1 -1
- package/dist/dom.js +36 -5
- package/dist/judge.js +29 -13
- package/docs/reference.md +1 -1
- package/docs/releasing.md +3 -5
- package/package.json +1 -1
package/dist/design.js
CHANGED
|
@@ -11,7 +11,7 @@ export const KINDS = {
|
|
|
11
11
|
defect: 'An avatar, profile picture, app icon or logo mark (a symbol or a face, not a word) is drawn more than 1.3 times wider than tall, or more than 1.3 times taller than wide; or a circle is drawn as an oval; or a photo shows people or objects flattened or stretched.',
|
|
12
12
|
look: 'list each avatar, profile picture, app icon, logo mark and photo with its drawn width x height in pixels',
|
|
13
13
|
},
|
|
14
|
-
'clipped-text': { defect: 'Some text is cut: letters cut off by an edge of the image or
|
|
14
|
+
'clipped-text': { defect: 'Some text is cut: letters cut off by an edge of the image or their container, a word or line stopping mid-way with no ellipsis, a multi-line clamp cutting a visible line mid-height, or a single-line ellipsis leaving fewer than 3 complete words AND fewer than 20 visible characters before the dots. A single-line ellipsis with at least 3 complete words OR 20 visible characters before the dots is deliberate truncation, not clipped text; still flag any hard cut or mid-glyph cut.', look: 'name labels whose letters touch or cross an edge; for each single-line ellipsis count the complete words and visible characters before the dots, and exclude it only if at least 3 complete words OR 20 visible characters remain and no glyph is cut' },
|
|
15
15
|
overlap: { defect: 'Two elements are drawn over each other: text over text, a control over content, or content under the status bar, clock or system bars.', look: 'name any two elements that touch or cover each other' },
|
|
16
16
|
'off-screen': { defect: 'An element is cut by the left, right or top edge of the image, so part of it is missing. Content cut by the bottom edge of a scrolling list is not this.', look: 'name any element that touches or crosses the left, right or top edge' },
|
|
17
17
|
'blank-strip': {
|
package/dist/dom.js
CHANGED
|
@@ -34,7 +34,8 @@ export const SQUASHED_TEXT = `() => {
|
|
|
34
34
|
// The rest of the look measurement, run in the page by `done-done look` next to SQUASHED_TEXT. A part of an element only
|
|
35
35
|
// shows inside every ancestor that clips it (overflow other than visible; html and body clip as the viewport, so they are
|
|
36
36
|
// the edges, not containers). Text is clipped when part of it shows and part runs past a hidden or clip container on an
|
|
37
|
-
// axis
|
|
37
|
+
// axis. A single-line ellipsis is deliberate only with a readable head (3 complete words OR 20 visible characters);
|
|
38
|
+
// a line clamp may hide whole lines, not part of a visible line. An element is cut off when part of what shows lies past the
|
|
38
39
|
// left, right or top edge of the viewport (the page is at its top, so nothing above it can be scrolled to). Something
|
|
39
40
|
// entirely outside its container or the viewport is hidden on purpose (a closed drawer, a visually hidden label), not cut;
|
|
40
41
|
// a cut element's children are not listed again.
|
|
@@ -46,6 +47,32 @@ export const LOOK = `() => {
|
|
|
46
47
|
const own = (e) => [...e.childNodes].filter((n) => n.nodeType === 3 && n.data.trim()).map((n) => n.data).join(' ').replace(/\\s+/g, ' ').trim().slice(0, 40);
|
|
47
48
|
const clips = (e) => { const out = []; for (let a = e; a && a !== document.body && a !== document.documentElement; a = a.parentElement) { const s = getComputedStyle(a); if (s.overflowX !== 'visible' || s.overflowY !== 'visible') { const r = a.getBoundingClientRect(); out.push({ a, s, l: r.left + a.clientLeft, t: r.top + a.clientTop, r: r.left + a.clientLeft + a.clientWidth, b: r.top + a.clientTop + a.clientHeight }); } } return out; };
|
|
48
49
|
const shows = (b) => b.r - b.l > 1 && b.b - b.t > 1;
|
|
50
|
+
const readable = (c) => {
|
|
51
|
+
if (c.s.textOverflow !== 'ellipsis' || !['nowrap', 'pre'].includes(c.s.whiteSpace) || c.s.display === 'inline' || c.s.writingMode !== 'horizontal-tb') return false;
|
|
52
|
+
const ctx = document.createElement('canvas').getContext('2d');
|
|
53
|
+
ctx.font = c.s.fontStyle + ' ' + c.s.fontWeight + ' ' + c.s.fontSize + ' ' + c.s.fontFamily;
|
|
54
|
+
ctx.letterSpacing = c.s.letterSpacing === 'normal' ? '0px' : c.s.letterSpacing;
|
|
55
|
+
/* Reserve a pixel for browser glyph rounding at the ellipsis boundary. */
|
|
56
|
+
const dots = ctx.measureText('…').width + 1; const rtl = c.s.direction === 'rtl';
|
|
57
|
+
const left = c.l + parseFloat(c.s.paddingLeft) + (rtl ? dots : 0);
|
|
58
|
+
const right = c.r - parseFloat(c.s.paddingRight) - (rtl ? 0 : dots);
|
|
59
|
+
const walker = document.createTreeWalker(c.a, NodeFilter.SHOW_TEXT);
|
|
60
|
+
let head = ''; let next = ''; let top;
|
|
61
|
+
scan: for (let n = walker.nextNode(); n; n = walker.nextNode()) {
|
|
62
|
+
let offset = 0;
|
|
63
|
+
for (const ch of n.data) {
|
|
64
|
+
const g = document.createRange(); g.setStart(n, offset); offset += ch.length; g.setEnd(n, offset);
|
|
65
|
+
const q = g.getBoundingClientRect();
|
|
66
|
+
if (!q.width) continue;
|
|
67
|
+
if (top !== undefined && Math.abs(q.top - top) > 1) return false;
|
|
68
|
+
top = q.top;
|
|
69
|
+
if (q.left < left - 0.01 || q.right > right + 0.01) { next = ch; break scan; }
|
|
70
|
+
head += ch;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
const complete = (next && !/\\s/.test(next) ? head.replace(/\\S+$/, '') : head).trim();
|
|
74
|
+
return [...head.trim()].length >= 20 || (complete ? complete.split(/\\s+/).length : 0) >= 3;
|
|
75
|
+
};
|
|
49
76
|
for (const e of document.querySelectorAll('body *')) {
|
|
50
77
|
const s = getComputedStyle(e); const rect = e.getBoundingClientRect();
|
|
51
78
|
if (!e.getClientRects().length || s.visibility !== 'visible' || rect.width < 1 || rect.height < 1) continue;
|
|
@@ -57,13 +84,17 @@ export const LOOK = `() => {
|
|
|
57
84
|
const edges = [box.l < -1 && 'left', box.r > vw + 1 && 'right', box.t < -1 && 'top'].filter(Boolean);
|
|
58
85
|
if (edges.length && box.r > 0 && box.l < vw && box.b > 0 && box.t < vh && !parent) { cut.add(e); out.cut_off.push({ element: name(e), text, edges }); }
|
|
59
86
|
if (!text) continue;
|
|
60
|
-
const t = { l: Infinity, t: Infinity, r: -Infinity, b: -Infinity };
|
|
61
|
-
for (const n of e.childNodes) if (n.nodeType === 3 && n.data.trim()) { const g = document.createRange(); g.selectNodeContents(n); for (const q of g.getClientRects()) if (q.width) { t.l = Math.min(t.l, q.left); t.t = Math.min(t.t, q.top); t.r = Math.max(t.r, q.right); t.b = Math.max(t.b, q.bottom); } }
|
|
87
|
+
const t = { l: Infinity, t: Infinity, r: -Infinity, b: -Infinity }; const lines = [];
|
|
88
|
+
for (const n of e.childNodes) if (n.nodeType === 3 && n.data.trim()) { const g = document.createRange(); g.selectNodeContents(n); for (const q of g.getClientRects()) if (q.width) { lines.push(q); t.l = Math.min(t.l, q.left); t.t = Math.min(t.t, q.top); t.r = Math.max(t.r, q.right); t.b = Math.max(t.b, q.bottom); } }
|
|
62
89
|
if (t.l === Infinity || t.t >= vh || t.b <= 0) continue;
|
|
63
90
|
for (const c of clips(e)) {
|
|
64
91
|
if (c.r - c.l <= 1 || c.b - c.t <= 1 || !(t.r > c.l && t.l < c.r && t.b > c.t && t.t < c.b) || !['hidden', 'clip'].some((v) => v === c.s.overflowX || v === c.s.overflowY)) continue;
|
|
65
|
-
const
|
|
66
|
-
const
|
|
92
|
+
const overX = ['hidden', 'clip'].includes(c.s.overflowX) && (t.l < c.l - 1 || t.r > c.r + 1);
|
|
93
|
+
const x = overX && !readable(c);
|
|
94
|
+
const partial = lines.some((q) => (q.top < c.b - 1 && q.bottom > c.b + 1) || (q.top < c.t - 1 && q.bottom > c.t + 1));
|
|
95
|
+
const y = ['hidden', 'clip'].includes(c.s.overflowY) && (c.s.webkitLineClamp === 'none' || partial) && (t.t < c.t - 1 || t.b > c.b + 1);
|
|
96
|
+
/* Ancestors clip the painted ellipsis, not the invisible tail of its DOM Range. */
|
|
97
|
+
if (overX && !x) { t.l = Math.max(t.l, c.l); t.r = Math.min(t.r, c.r); }
|
|
67
98
|
if (x || y) { out.clipped_text.push({ element: name(e), text, by: c.a === e ? 'its own box' : name(c.a) }); break; }
|
|
68
99
|
}
|
|
69
100
|
}
|
package/dist/judge.js
CHANGED
|
@@ -6,11 +6,11 @@ import { agentRun, askAgent, claudeAccount, notStartable } from './agents.js';
|
|
|
6
6
|
import { home, KEY_ROUTES, parseDuration, tilde, UsageError } from './config.js';
|
|
7
7
|
import { downscaleToPng } from './media.js';
|
|
8
8
|
const MAX_SIDE = 1280; // long edge sent to the model, in pixels
|
|
9
|
-
const MAX_CALLS = 10; //
|
|
9
|
+
const MAX_CALLS = 10; // independent capture calls per check; oversized isolated sets are refused
|
|
10
10
|
const MAX_TEXT = 64 * 1024; // bytes of a text capture sent to the model; a longer one is unsure, never cut
|
|
11
11
|
// The reading rules: the model names the element the outcome claims and quotes it before it decides, and a text capture
|
|
12
12
|
// is read entry by entry. The floor is not here: the user sets it (judge.floor), and judge() applies it.
|
|
13
|
-
export const VERSION =
|
|
13
|
+
export const VERSION = 15; // bump when QUESTION, the options, the cache key or the stored verdict change, so older cached verdicts are not reused
|
|
14
14
|
const OPTIONS = {
|
|
15
15
|
correct: 'The screenshot shows the screen and state the outcome describes.',
|
|
16
16
|
wrong: 'The screenshot shows a different screen, an error, a crash, a dialog or overlay (such as a keyboard) hiding the content, or a state that contradicts the outcome. Choose it only when the quote you give shows that.',
|
|
@@ -208,7 +208,7 @@ export async function askJudge(cfg, be, choice, state, questions, images, shared
|
|
|
208
208
|
// One answer defines the identities before coverage is validated or blank/repeated labels are collapsed.
|
|
209
209
|
if (sharedParts)
|
|
210
210
|
for (const answer of Object.values(answers))
|
|
211
|
-
Object.assign(answer, { parts: lists(answers[sharedParts]).parts });
|
|
211
|
+
Object.assign(answer, { parts: Array.isArray(sharedParts) ? sharedParts : lists(answers[sharedParts]).parts });
|
|
212
212
|
return answers;
|
|
213
213
|
};
|
|
214
214
|
const malformed = (a) => Object.keys(questions).filter((k) => /^(malformed|no answer)/.test(a[k].reason ?? ''));
|
|
@@ -291,9 +291,8 @@ async function judgeBatch(cfg, choice, outcome, captures, records, dir) {
|
|
|
291
291
|
const stamp = new Date().toISOString().replace(/[:.]/g, '-');
|
|
292
292
|
const eligible = captures.filter(judgeable);
|
|
293
293
|
const notJudged = captures.filter((c) => !judgeable(c)).map((c) => c.name);
|
|
294
|
-
const
|
|
295
|
-
const
|
|
296
|
-
const rows = selected.map((c) => {
|
|
294
|
+
const isolated = eligible.some(screen);
|
|
295
|
+
const rows = eligible.map((c) => {
|
|
297
296
|
const pair = c.before && judgeable(c.before) && screen(c.before) === screen(c) ? c.before : undefined;
|
|
298
297
|
return { c, pair, row: { path: c.name, sha256: c.sha256, ...(pair && { before: pair.name }) } };
|
|
299
298
|
});
|
|
@@ -301,7 +300,7 @@ async function judgeBatch(cfg, choice, outcome, captures, records, dir) {
|
|
|
301
300
|
const key = createHash('sha256').update(JSON.stringify([VERSION, captures.map((c) => [c.name, c.sha256, c.before?.name, c.before?.sha256]), outcome, route, model, cfg.judge.floor])).digest('hex');
|
|
302
301
|
const hit = readCache(dir, key);
|
|
303
302
|
if (Array.isArray(hit?.judged) && hit.judged.length === rows.length && hit.judged.every((r) => ['pass', 'fail', 'unsure'].includes(r?.verdict) && typeof r.reason === 'string' && (r.parts === undefined || coverageOf(r))))
|
|
304
|
-
return { judged:
|
|
303
|
+
return { judged: hit.judged.map((r) => ({ ...r, cached: true })), notJudged, choice, missing: hit.missing ?? [] };
|
|
305
304
|
const questions = {};
|
|
306
305
|
const images = {};
|
|
307
306
|
const state = { outcome };
|
|
@@ -310,7 +309,7 @@ async function judgeBatch(cfg, choice, outcome, captures, records, dir) {
|
|
|
310
309
|
const input = screen(c) ? prepare(c) : c.bytes.length > MAX_TEXT ? `${c.name} text exceeds ${MAX_TEXT} bytes` : { text: c.bytes.toString('utf8') };
|
|
311
310
|
const before = pair && (screen(pair) ? prepare(pair) : pair.bytes.length > MAX_TEXT ? `${pair.name} text exceeds ${MAX_TEXT} bytes` : { text: pair.bytes.toString('utf8') });
|
|
312
311
|
if (typeof input === 'string' || typeof before === 'string')
|
|
313
|
-
throw new UsageError(`judge: ${c.name} cannot be sent to the judge: ${input}`, 'Capture it again as the error says, then re-run this command');
|
|
312
|
+
throw new UsageError(`judge: ${c.name} cannot be sent to the judge: ${typeof input === 'string' ? input : `its base capture ${pair.name}: ${before}`}`, 'Capture it again as the error says, then re-run this command');
|
|
314
313
|
const base = 'text' in input ? before ? TEXT_PAIR : TEXT_QUESTION : before ? PAIR : QUESTION;
|
|
315
314
|
const beforeId = pair && imageId(`before_${i}`, pair);
|
|
316
315
|
const captureId = imageId(`capture_${i}`, c);
|
|
@@ -319,11 +318,28 @@ async function judgeBatch(cfg, choice, outcome, captures, records, dir) {
|
|
|
319
318
|
if (beforeId && before && 'mime' in before)
|
|
320
319
|
images[beforeId] = before;
|
|
321
320
|
state[id] = { name: basename(c.name), ...(pair && { before_name: basename(pair.name) }), ...(before && 'text' in before && { before: before.text }), ...(typeof input === 'object' && 'text' in input && { capture: input.text }) };
|
|
322
|
-
questions[id] = { ...base, images: 'text' in input ? [] : beforeId && before && 'mime' in before ? [beforeId, captureId] : [captureId], instructions: `${base.instructions} ${PARTS} Use one shared parts list for this entire request: define it in capture_0.parts, in outcome order. Every capture's covers indexes that shared list, never a separately worded or reordered list
|
|
321
|
+
questions[id] = { ...base, images: 'text' in input ? [] : beforeId && before && 'mime' in before ? [beforeId, captureId] : [captureId], instructions: `${base.instructions} ${PARTS}${isolated ? ' Define parts for the ENTIRE outcome, including every separately named capture, not only the part this image shows. This list will be shared with later isolated calls.' : ' Use one shared parts list for this entire request: define it in capture_0.parts, in outcome order. Every capture\'s covers indexes that shared list, never a separately worded or reordered list.'}`, floor: cfg.judge.floor, ...(route === 'openai:key' ? {} : { answerFields: FIELDS }) };
|
|
322
|
+
}
|
|
323
|
+
const answers = {};
|
|
324
|
+
if (isolated) {
|
|
325
|
+
// Images from different citations must never share a model context. A before/after pair stays together.
|
|
326
|
+
if (rows.length > MAX_CALLS)
|
|
327
|
+
throw new UsageError(`judge: one check sends at most ${MAX_CALLS} captures to the model, so cite fewer`, 'Split the claim into smaller checks');
|
|
328
|
+
let parts;
|
|
329
|
+
for (const [id, question] of Object.entries(questions)) {
|
|
330
|
+
const shape = parts ? { ...question, answerFields: ['covers'], instructions: `${question.instructions} The shared parts list is fixed in state.shared_parts. Return covers positions in that exact list; do not redefine it.` } : question;
|
|
331
|
+
const one = { [id]: shape };
|
|
332
|
+
const call = join(records, `judge-${stamp}-${Object.keys(answers).length + 1}`);
|
|
333
|
+
const be = await backend(cfg, choice, (request) => `${call}-${request}.agent.json`, one);
|
|
334
|
+
Object.assign(answers, await askJudge(cfg, be, choice, { outcome, [id]: state[id], ...(parts && { shared_parts: parts }) }, one, Object.fromEntries((shape.images ?? []).map((image) => [image, images[image]])), parts ?? ''));
|
|
335
|
+
parts ??= lists(answers[id]).parts;
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
else {
|
|
339
|
+
const call = join(records, `judge-${stamp}-1`);
|
|
340
|
+
const be = await backend(cfg, choice, (request) => `${call}-${request}.agent.json`, questions);
|
|
341
|
+
Object.assign(answers, await askJudge(cfg, be, choice, state, questions, images, 'capture_0'));
|
|
323
342
|
}
|
|
324
|
-
const call = join(records, `judge-${stamp}-1`);
|
|
325
|
-
const be = await backend(cfg, choice, (request) => `${call}-${request}.agent.json`, questions);
|
|
326
|
-
const answers = await askJudge(cfg, be, choice, state, questions, images, 'capture_0');
|
|
327
343
|
const judged = rows.map(({ row, c }, i) => {
|
|
328
344
|
const a = answers[`capture_${i}`];
|
|
329
345
|
const shape = questions[`capture_${i}`];
|
|
@@ -333,7 +349,7 @@ async function judgeBatch(cfg, choice, outcome, captures, records, dir) {
|
|
|
333
349
|
});
|
|
334
350
|
const missing = missingParts(judged);
|
|
335
351
|
writeCache(dir, key, { judged, missing, route, model });
|
|
336
|
-
return { judged
|
|
352
|
+
return { judged, notJudged, choice, missing };
|
|
337
353
|
}
|
|
338
354
|
async function judgeOn(cfg, choose, outcome, captures, records) {
|
|
339
355
|
// Check the account even on a verdict cache hit: a broken configured picker must never produce a pass.
|
package/docs/reference.md
CHANGED
|
@@ -483,7 +483,7 @@ done-done check --from pr-body.md # always judged
|
|
|
483
483
|
|
|
484
484
|
Under `verify`, a `fail` or `unsure` verdict does not end the run: it parks it at its [gate](#the-gate-and-respond).
|
|
485
485
|
|
|
486
|
-
The result gains a `judge` block with the route, the model, the independence, the author and, for each judged file, its verdict, a short reason and whether it came from the cache, and, when the outcome names several parts, the parts it lists and the positions of the ones it shows (`parts`, `covers`). For the `claude-code` route, a missing `claude` binary or an `agent_path_override.claude` that is not an executable file, or a Node older than 22.18 is a config error (exit 2) whose `help` line is the command to fix it. A `claude` that is not signed in (checked only when a call fails) is exit 2 too, with `claude auth login` as the `help` line, the same sign-in you use in your shell. done-done does not check which plan that sign-in is on, so `claude` uses whatever sign-in your environment gives it. The kit cannot tell a bad `judge.model` from a used-up plan, so the error names both. For `openai:key`, an unset `OPENAI_API_KEY` is a config error (exit 2) too, and a refused key or a bad `judge.model` is an error with the HTTP status, naming both. The judge's confidence is the model's own estimate, not a calibrated score. `judge.floor` (global config, a number from 0 to 1, default `0.7`) is the least probability the judge's pick may carry and still count: a pick below it is `unsure` with `not confident enough (confidence <n> below floor <f>)`, and never a pass. It governs every judge: check and receipt relevance, maintain triage, and the design check. An outcome that names several parts (widths, themes or frames, such as "at 390 and 1440 wide") needs each one shown by a passing capture. Each image attachment's identifier includes its cited basename beside the actual bytes, and each question's state retains the cited capture's basename in `name` (and `before_name` for a base pair), never its owner or repository directory. The instructions tell the judge to compare a named still or an explicitly extracted recording frame against that file's own claim part, not another screen's part. Recordings themselves are probed, not frame-extracted or sent to the relevance judge. Renamed citations are new judge inputs, even with identical bytes.
|
|
486
|
+
The result gains a `judge` block with the route, the model, the independence, the author and, for each judged file, its verdict, a short reason and whether it came from the cache, and, when the outcome names several parts, the parts it lists and the positions of the ones it shows (`parts`, `covers`). For the `claude-code` route, a missing `claude` binary or an `agent_path_override.claude` that is not an executable file, or a Node older than 22.18 is a config error (exit 2) whose `help` line is the command to fix it. A `claude` that is not signed in (checked only when a call fails) is exit 2 too, with `claude auth login` as the `help` line, the same sign-in you use in your shell. done-done does not check which plan that sign-in is on, so `claude` uses whatever sign-in your environment gives it. The kit cannot tell a bad `judge.model` from a used-up plan, so the error names both. For `openai:key`, an unset `OPENAI_API_KEY` is a config error (exit 2) too, and a refused key or a bad `judge.model` is an error with the HTTP status, naming both. The judge's confidence is the model's own estimate, not a calibrated score. `judge.floor` (global config, a number from 0 to 1, default `0.7`) is the least probability the judge's pick may carry and still count: a pick below it is `unsure` with `not confident enough (confidence <n> below floor <f>)`, and never a pass. It governs every judge: check and receipt relevance, maintain triage, and the design check. An outcome that names several parts (widths, themes or frames, such as "at 390 and 1440 wide") needs each one shown by a passing capture. Each image attachment's identifier includes its cited basename beside the actual bytes, and each question's state retains the cited capture's basename in `name` (and `before_name` for a base pair), never its owner or repository directory. The instructions tell the judge to compare a named still or an explicitly extracted recording frame against that file's own claim part, not another screen's part. Recordings themselves are probed, not frame-extracted or sent to the relevance judge. Renamed citations are new judge inputs, even with identical bytes. When any citation is an image, each capture is judged in a fresh request containing only its own image (or its own before/after pair), never another citation's image or text. The first call defines `parts` for the entire outcome; later calls receive that fixed list and return `covers` positions in it. Text-only sets still use one batch request where `capture_0.parts` defines the shared list. Every capture's `covers` indexes that list. Other answers cannot redefine identities by relabelling or reordering their own `parts`: labels are display text, never matched across calls. Single-capture checks still cost one request, and an unchanged full set reuses the cache. A shared position no passing capture shows makes the check inconclusive, naming that part. The lists are the judge kit's answer fields (`answerFields` on the question), so they arrive on the answer the kit resolves; the `openai:key` route is asked for no such field, so it still requires no part (even for a multi-part claim). A missing shared list, missing `covers`, or a position outside the shared list is asked once more, then is an error (exit 2). A text batch that cannot fit its captures stops with its real cause and no result. An isolated set needing more than 10 capture requests is refused before calling the model; split it into smaller claims. A failed isolated request stops the whole check with its real cause and no result, never partial proof. Version-13 and earlier verdict caches are invalidated, including shared-context image verdicts: old results and independent per-capture labels are never reinterpreted as isolated named shared identities. Other routes already ignore legacy per-capture cache entries when judging a multi-capture set. Drive the five-part regression and its missing-part, cache and request-refusal controls with `done-done run shared-parts`; it uses deterministic provider adapters, not a hosted model. Each request asks the judge to name the element the outcome claims and quote what it sees before it decides, and a text capture (JSON, a log) is read entry by entry, every width and frame, before the judge answers; the reason shown is the model's one-sentence person reason, else its rationale; each judged file also shows the model's rationale, the quote it read, under `seen` when the model gave one. The floor is part of the verdict cache key, so a changed floor asks the judge again.
|
|
487
487
|
|
|
488
488
|
**Remaining limitation:** names bind attachments and cached verdicts, but do not isolate the model's view of a batch. In an adversarial control with two filenames swapped, the configured judge still substituted the other attached image and falsely passed. This change does not guarantee image-to-claim pairing accuracy; image isolation requires a separate follow-up.
|
|
489
489
|
|
package/docs/releasing.md
CHANGED
|
@@ -1,7 +1,5 @@
|
|
|
1
|
-
# 0.6.
|
|
1
|
+
# 0.6.9 patch release
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
This release treats readable one-line ellipses as deliberate when at least 3 words or 20 characters remain visible, and checks each cited image on its own so swapped images cannot support a false claim.
|
|
4
4
|
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
Do not publish an unmerged candidate. The existing `release` workflow on main tests and publishes an unpublished package version using npm trusted publishing. After merge, confirm `npm view done-done-axi@0.6.8 version dist.integrity` and smoke-test a clean `npx` install with `done-done --version`. Do not run `npm publish` manually unless the workflow fails and the release is blocked; if so, stop and report.
|
|
5
|
+
Do not publish an unmerged candidate. The existing `release` workflow on main tests and publishes an unpublished package version using npm trusted publishing. After merge, confirm `npm view done-done-axi@0.6.9 version dist.integrity` and smoke-test a clean `npx` install with `done-done --version`. Do not run `npm publish` manually unless the workflow fails and the release is blocked; if so, stop and report.
|
package/package.json
CHANGED