retake-demos 0.13.1 → 0.13.2
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/AGENTS.md +8 -3
- package/dist/cli.js +38 -10
- package/dist/dryrun.js +10 -1
- package/dist/ext/calibrate.js +94 -10
- package/dist/operator/tools.js +9 -7
- package/dist/ui/chat.html +12 -6
- package/dist/ui/guide.html +1 -1
- package/dist/ui/server.js +23 -6
- package/package.json +1 -1
- package/site/releases.json +14 -0
- package/skill/SKILL.md +12 -7
package/AGENTS.md
CHANGED
|
@@ -235,9 +235,14 @@ Most real demos start behind a sign-in. The agent never sees a password:
|
|
|
235
235
|
control where nothing is wrong, then reports recall and false positives.
|
|
236
236
|
Expensive: nine takes, about ten minutes, needs the app running. The report
|
|
237
237
|
it leaves is cheap to read, and **you should read it before telling anybody
|
|
238
|
-
how much a sweep finding is worth.** Its stated limits travel with it
|
|
239
|
-
|
|
240
|
-
upper bound
|
|
238
|
+
how much a sweep finding is worth.** Its stated limits travel with it, and
|
|
239
|
+
all four belong in anything you say about the numbers: the seeds were written
|
|
240
|
+
by somebody who has read the checklist, so recall is an upper bound; injected
|
|
241
|
+
faults are cleaner than real ones; the run names the checklist items nothing
|
|
242
|
+
seeds, and says nothing whatever about those; and a run whose control did not
|
|
243
|
+
record cleanly measured nothing at all and exits 3 rather than printing a
|
|
244
|
+
reassuring zero. From MCP you can only READ the last measurement — making one
|
|
245
|
+
is a terminal command, because it records for ten minutes.
|
|
241
246
|
- `retake intent` — what the product IS, in the builder's words: what it does,
|
|
242
247
|
who uses it, and what about it is deliberate. Every visual check reads it.
|
|
243
248
|
Without it they judge the app as a generic web page — measured on a real
|
package/dist/cli.js
CHANGED
|
@@ -45,7 +45,7 @@ import { notes } from "./ext/notes.js";
|
|
|
45
45
|
import { sense } from "./ext/sense.js";
|
|
46
46
|
import { sweep } from "./ext/sweep.js";
|
|
47
47
|
import { TEMPLATE, readIntent, writeIntent } from "./ext/intent.js";
|
|
48
|
-
import { planCalibration, report } from "./ext/calibrate.js";
|
|
48
|
+
import { planCalibration, report, usable } from "./ext/calibrate.js";
|
|
49
49
|
import { checkFlags, flag, unflag } from "./ext/flags.js";
|
|
50
50
|
import { describeOrphans, findOrphans, healOrphans } from "./ext/heal.js";
|
|
51
51
|
import { SHAPES, describePlan, describeTrials, planDestroy, refuseToRun, tryCandidates } from "./ext/destroy.js";
|
|
@@ -399,27 +399,55 @@ program
|
|
|
399
399
|
if (opts.plan)
|
|
400
400
|
return;
|
|
401
401
|
const results = [];
|
|
402
|
+
/** Sweep once, and give back what it said. */
|
|
403
|
+
const look = async (m, dir, fresh) => (await sweep(m, dir, () => { }, { demosDir: path.resolve("demos"), fresh })).frames.flatMap((f) => f.issues.map((i) => i.kind));
|
|
404
|
+
let stability = null;
|
|
402
405
|
for (const v of plan.variants) {
|
|
403
406
|
const dir = path.join(plan.root, v.name);
|
|
404
407
|
say(`\n${v.name}${v.defect ? ` — ${v.defect.looks}` : ""}`);
|
|
405
408
|
try {
|
|
406
409
|
const take = await record(v.manifest, { outDir: dir, manifestDir: loaded.dir, headed: false, log: () => { } });
|
|
407
410
|
await render(v.manifest, take, dir, { log: () => { }, noMaster: true });
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
+
// A recording that fell over, recorded nothing, or produced no frames
|
|
412
|
+
// measures nothing — whichever end of it broke. Asking sweep about it
|
|
413
|
+
// and reporting the empty answer is how a run against an app that was
|
|
414
|
+
// not even running came back reading "none on the control".
|
|
415
|
+
const fit = usable(dir, take);
|
|
416
|
+
if (!fit.ok) {
|
|
417
|
+
results.push({ variant: v.name, defect: v.defect, kinds: [], found: false, seeded: false, why: fit.why, other: 0 });
|
|
418
|
+
say(` → not measured: ${fit.why}`);
|
|
419
|
+
continue;
|
|
420
|
+
}
|
|
421
|
+
const kinds = await look(v.manifest, dir, false);
|
|
411
422
|
const found = !v.defect ? false : kinds.some((k) => k.toUpperCase().startsWith(v.defect.expect.split(" ")[0]));
|
|
412
|
-
results.push({ variant: v.name, defect: v.defect, kinds, found
|
|
413
|
-
say(` → ${kinds.join(", ") || "nothing"}
|
|
423
|
+
results.push({ variant: v.name, defect: v.defect, kinds, found, seeded: true, other: v.defect ? kinds.filter((k) => !k.toUpperCase().startsWith(v.defect.expect.split(" ")[0])).length : 0 });
|
|
424
|
+
say(` → ${kinds.join(", ") || "nothing"}`);
|
|
425
|
+
// The control, asked the same question about the same frames a second
|
|
426
|
+
// time. Judges are not deterministic, and a harness that never checks
|
|
427
|
+
// this cannot tell a real finding from a coin landing heads.
|
|
428
|
+
if (!v.defect) {
|
|
429
|
+
say(" asking the control again, to see whether the answer holds");
|
|
430
|
+
stability = { first: kinds, again: await look(v.manifest, dir, true) };
|
|
431
|
+
say(` → ${stability.again.join(", ") || "nothing"}`);
|
|
432
|
+
}
|
|
414
433
|
}
|
|
415
434
|
catch (e) {
|
|
416
|
-
|
|
417
|
-
|
|
435
|
+
const msg = String(e.message).split("\n")[0].slice(0, 90);
|
|
436
|
+
results.push({ variant: v.name, defect: v.defect, kinds: [], found: false, seeded: false, why: `the run threw — ${msg}`, other: 0, error: msg });
|
|
437
|
+
say(` → could not run: ${msg}`);
|
|
418
438
|
}
|
|
419
439
|
}
|
|
420
|
-
for (const l of report(results))
|
|
440
|
+
for (const l of report(results, stability))
|
|
421
441
|
say(l);
|
|
422
|
-
fs.writeFileSync(path.join(plan.root, "report.json"), JSON.stringify({ at: new Date().toISOString(), demo: loaded.manifest.name, results }, null, 2) + "\n");
|
|
442
|
+
fs.writeFileSync(path.join(plan.root, "report.json"), JSON.stringify({ at: new Date().toISOString(), demo: loaded.manifest.name, results, stability }, null, 2) + "\n");
|
|
443
|
+
// The false-positive rate is the number people act on. If the control did
|
|
444
|
+
// not record, this run produced no number at all, and exiting 0 says the
|
|
445
|
+
// opposite.
|
|
446
|
+
const control = results.find((r) => !r.defect);
|
|
447
|
+
if (!control?.seeded) {
|
|
448
|
+
process.exitCode = 3;
|
|
449
|
+
say("\nnothing was measured — fix the run before quoting any of this.");
|
|
450
|
+
}
|
|
423
451
|
});
|
|
424
452
|
program
|
|
425
453
|
.command("intent")
|
package/dist/dryrun.js
CHANGED
|
@@ -266,8 +266,17 @@ export async function dryRun(m, manifestDir, log, opts = {}) {
|
|
|
266
266
|
// records perfectly. The starter demo shipped with `retake init` hits
|
|
267
267
|
// it, so a new person's first dry run failed on a demo that works.
|
|
268
268
|
case "scroll":
|
|
269
|
-
|
|
269
|
+
// And actually scroll. Skipping it left the page at the top while
|
|
270
|
+
// every later step ran, so a dry run rehearsed a different page from
|
|
271
|
+
// the one the recording would see.
|
|
272
|
+
if (step.to === "top")
|
|
273
|
+
await page.evaluate(() => window.scrollTo(0, 0));
|
|
274
|
+
else if (step.to === "bottom")
|
|
275
|
+
await page.evaluate(() => window.scrollTo(0, document.body.scrollHeight));
|
|
276
|
+
else if (step.to) {
|
|
270
277
|
await page.locator(step.to).first().boundingBox({ timeout: short });
|
|
278
|
+
await page.locator(step.to).first().scrollIntoViewIfNeeded({ timeout: short });
|
|
279
|
+
}
|
|
271
280
|
break;
|
|
272
281
|
case "upload":
|
|
273
282
|
await page.locator(step.selector).first().waitFor({ timeout: short });
|
package/dist/ext/calibrate.js
CHANGED
|
@@ -45,6 +45,7 @@ import fs from "node:fs";
|
|
|
45
45
|
import path from "node:path";
|
|
46
46
|
import YAML from "yaml";
|
|
47
47
|
import { createHash } from "node:crypto";
|
|
48
|
+
import { LOOK_FOR } from "./sweep.js";
|
|
48
49
|
/**
|
|
49
50
|
* Deliberately mechanical. Every one of these is visible to a person in one
|
|
50
51
|
* glance, which is the bar: if a human cannot see it in the frame, a miss is
|
|
@@ -74,7 +75,12 @@ export const DEFECTS = [
|
|
|
74
75
|
},
|
|
75
76
|
{
|
|
76
77
|
name: "unreadable-contrast",
|
|
77
|
-
|
|
78
|
+
// Was `.replace(/\\s/g,'')`, which after string escaping built a regex for
|
|
79
|
+
// a literal backslash followed by "s" — so the spaces in "rgb(233, 235,
|
|
80
|
+
// 228)" were never stripped, the proof was never true, and this seed
|
|
81
|
+
// reported "did not land" on every app it was ever run against. It was
|
|
82
|
+
// counted as one of eight for two releases. No regex now.
|
|
83
|
+
proof: "getComputedStyle(document.body).color.split(' ').join('') === 'rgb(233,235,228)'",
|
|
78
84
|
expect: "CONTRAST",
|
|
79
85
|
looks: "body text almost the same colour as the page behind it",
|
|
80
86
|
css: "body,p,li,span{color:#e9ebe4!important}",
|
|
@@ -139,6 +145,12 @@ export const lab = (outRoot, demo) => path.join(outRoot, ".calibrate", demo);
|
|
|
139
145
|
export function planCalibration(m, outRoot, opts = {}) {
|
|
140
146
|
const root = lab(outRoot, m.name);
|
|
141
147
|
fs.mkdirSync(root, { recursive: true });
|
|
148
|
+
// `--only typo` used to match nothing and run a control alone, then print a
|
|
149
|
+
// recall line reading "0 of 0" — which is not a failure, it is a measurement
|
|
150
|
+
// of nothing wearing the clothes of one.
|
|
151
|
+
const unknown = (opts.only ?? []).filter((n) => !DEFECTS.some((d) => d.name === n));
|
|
152
|
+
if (unknown.length)
|
|
153
|
+
throw new Error(`no such defect: ${unknown.join(", ")} — try ${DEFECTS.map((d) => d.name).join(", ")}`);
|
|
142
154
|
const want = opts.only?.length ? DEFECTS.filter((d) => opts.only.includes(d.name)) : DEFECTS;
|
|
143
155
|
const variants = [];
|
|
144
156
|
const write = (name, defect) => {
|
|
@@ -166,6 +178,17 @@ export function planCalibration(m, outRoot, opts = {}) {
|
|
|
166
178
|
{ action: "evaluate", script: `{ if (!(${defect.proof})) throw new Error("the ${defect.name} seed did not take on this app"); }` },
|
|
167
179
|
{ action: "wait", ms: 300 },
|
|
168
180
|
];
|
|
181
|
+
// And again at the END. The setup proof says the seed was on the page
|
|
182
|
+
// before the camera rolled; it says nothing about whether it was still
|
|
183
|
+
// there for the frames that were actually judged. A single-page app that
|
|
184
|
+
// navigates or re-renders can drop it halfway through, and every frame
|
|
185
|
+
// after that point asks the check to find something that is not there.
|
|
186
|
+
// Failing the step is right: a run that lost its seed is not a miss, it
|
|
187
|
+
// is not a measurement.
|
|
188
|
+
clone.steps = [
|
|
189
|
+
...clone.steps,
|
|
190
|
+
{ action: "evaluate", script: `{ if (!(${defect.proof})) throw new Error("the ${defect.name} seed was gone by the end of the run"); }` },
|
|
191
|
+
];
|
|
169
192
|
}
|
|
170
193
|
const file = path.join(root, `${name}.yaml`);
|
|
171
194
|
fs.writeFileSync(file, [
|
|
@@ -220,8 +243,40 @@ export function landed(controlDir, variantDir) {
|
|
|
220
243
|
return true; // cannot tell; do not accuse the harness
|
|
221
244
|
return a.join() !== b.join();
|
|
222
245
|
}
|
|
246
|
+
/**
|
|
247
|
+
* Is this recording fit to measure anything at all?
|
|
248
|
+
*
|
|
249
|
+
* It was not asked. A control recorded against an app that was not running
|
|
250
|
+
* came back with an empty timeline, `ok: false` and no frames — and the report
|
|
251
|
+
* said "FALSE POSITIVES none on the control", which is the single number this
|
|
252
|
+
* harness exists to produce. Nothing was wrong with the control because
|
|
253
|
+
* nothing had happened in it. A blank measurement that reads as a clean one is
|
|
254
|
+
* worse than no measurement, and it exited 0.
|
|
255
|
+
*/
|
|
256
|
+
export function usable(dir, take) {
|
|
257
|
+
if (!take)
|
|
258
|
+
return { ok: false, why: "nothing was recorded" };
|
|
259
|
+
if (!take.timeline?.length)
|
|
260
|
+
return { ok: false, why: "the run recorded no steps at all" };
|
|
261
|
+
if (take.ok === false)
|
|
262
|
+
return { ok: false, why: "the run did not finish cleanly" };
|
|
263
|
+
let stills = 0;
|
|
264
|
+
try {
|
|
265
|
+
stills = fs.readdirSync(path.join(dir, "stills")).filter((f) => /\.(png|jpe?g)$/i.test(f)).length;
|
|
266
|
+
}
|
|
267
|
+
catch { /* none */ }
|
|
268
|
+
if (!stills)
|
|
269
|
+
return { ok: false, why: "the run produced no frames to look at" };
|
|
270
|
+
return { ok: true, why: "" };
|
|
271
|
+
}
|
|
272
|
+
/** The `sweep` items nothing here seeds, named rather than left to arithmetic. */
|
|
273
|
+
export function uncovered() {
|
|
274
|
+
const seeded = new Set(DEFECTS.map((d) => d.expect.toUpperCase()));
|
|
275
|
+
return LOOK_FOR.map((l) => l.split(" — ")[0].trim())
|
|
276
|
+
.filter((kind) => !seeded.has(kind.toUpperCase()));
|
|
277
|
+
}
|
|
223
278
|
/** Recall, false positives, and the sentence that says what they mean. */
|
|
224
|
-
export function report(rs) {
|
|
279
|
+
export function report(rs, stability = null) {
|
|
225
280
|
const all = rs.filter((r) => r.defect);
|
|
226
281
|
const seeded = all.filter((r) => r.seeded);
|
|
227
282
|
const unseeded = all.filter((r) => !r.seeded);
|
|
@@ -231,27 +286,56 @@ export function report(rs) {
|
|
|
231
286
|
out.push("");
|
|
232
287
|
out.push(`RECALL ${found} of ${seeded.length} defects that actually appeared were found`);
|
|
233
288
|
for (const r of seeded) {
|
|
234
|
-
out.push(` ${r.found ? "
|
|
289
|
+
out.push(` ${r.found ? "\u2713" : "\u2717"} ${r.variant.padEnd(20)} wanted ${String(r.defect.expect).padEnd(19)} got ${r.kinds.join(", ") || (r.error ? `\u2014 ${r.error}` : "nothing")}`);
|
|
235
290
|
}
|
|
236
291
|
if (unseeded.length) {
|
|
237
292
|
out.push("");
|
|
238
|
-
out.push(`NOT MEASURED ${unseeded.length}
|
|
293
|
+
out.push(`NOT MEASURED ${unseeded.length} of ${all.length} seeded runs asked the check nothing:`);
|
|
239
294
|
for (const r of unseeded)
|
|
240
|
-
out.push(`
|
|
295
|
+
out.push(` \u2014 ${r.variant.padEnd(20)} ${r.why ?? r.error ?? "the injection did not land"}. Fix the harness, not the checklist.`);
|
|
241
296
|
}
|
|
242
297
|
out.push("");
|
|
243
|
-
|
|
298
|
+
// The control is the half that needs no injection, so it is the half that
|
|
299
|
+
// travels — and the only one that can speak to false positives. If it did
|
|
300
|
+
// not record, nothing here may be claimed, in either direction.
|
|
301
|
+
if (!control) {
|
|
302
|
+
out.push("FALSE POSITIVES not measured \u2014 no control was run, so no rate can be claimed");
|
|
303
|
+
}
|
|
304
|
+
else if (!control.seeded) {
|
|
305
|
+
out.push(`FALSE POSITIVES NOT MEASURED \u2014 on the control, ${control.why ?? control.error ?? "the run did not record"}. No rate can be claimed from this run, and neither can the recall above: every variant recorded under the same conditions.`);
|
|
306
|
+
}
|
|
307
|
+
else {
|
|
244
308
|
out.push(control.kinds.length
|
|
245
309
|
? `FALSE POSITIVES ${control.kinds.length} finding(s) on the control, where nothing is wrong: ${control.kinds.join(", ")}`
|
|
246
310
|
: "FALSE POSITIVES none on the control");
|
|
247
311
|
}
|
|
248
|
-
else {
|
|
249
|
-
out.push("FALSE POSITIVES not measured — no control was run, so no rate can be claimed");
|
|
250
|
-
}
|
|
251
312
|
const noise = seeded.reduce((n, r) => n + r.other, 0);
|
|
252
313
|
if (noise)
|
|
253
314
|
out.push(` and ${noise} extra finding(s) across the variants, beyond the fault seeded in each`);
|
|
254
315
|
out.push("");
|
|
255
|
-
|
|
316
|
+
// Promised in this file's own header for two releases and never produced.
|
|
317
|
+
// A number nobody measures is a claim, and this harness exists because
|
|
318
|
+
// claims about the checks were not good enough.
|
|
319
|
+
if (!stability) {
|
|
320
|
+
out.push("STABILITY not measured this run \u2014 the control was asked once, so nothing here says whether the same frames give the same answer twice.");
|
|
321
|
+
}
|
|
322
|
+
else {
|
|
323
|
+
const a = [...stability.first].sort().join(", ") || "nothing";
|
|
324
|
+
const b = [...stability.again].sort().join(", ") || "nothing";
|
|
325
|
+
out.push(a === b
|
|
326
|
+
? `STABILITY the control gave the same answer twice: ${a}`
|
|
327
|
+
: `STABILITY the control gave DIFFERENT answers to the same frames \u2014 first ${a}, then ${b}. Treat single sweep results on this app as one opinion, not a reading.`);
|
|
328
|
+
}
|
|
329
|
+
out.push("");
|
|
330
|
+
const gaps = uncovered();
|
|
331
|
+
if (gaps.length) {
|
|
332
|
+
// The count is of what this run attempted, not of the catalogue — `--only`
|
|
333
|
+
// makes those different numbers, and printing the catalogue's would
|
|
334
|
+
// overstate a narrow run.
|
|
335
|
+
const tried = all.length || DEFECTS.length;
|
|
336
|
+
out.push(`NOT COVERED ${tried} seed${tried === 1 ? "" : "s"} against a ${LOOK_FOR.length}-item checklist. Nothing here measures ${gaps.join(" or ")} at all, so the recall above says nothing about ${gaps.length === 1 ? "it" : "them"}.`);
|
|
337
|
+
out.push("");
|
|
338
|
+
}
|
|
339
|
+
out.push("What this is and is not: the seeds were written by someone who has read the checklist, so recall here is an upper bound \u2014 a fault nobody thought to seed is still unmeasured. Injected faults are also cleaner than the ones real apps produce. Treat it as a floor for trust, not a score.");
|
|
256
340
|
return out;
|
|
257
341
|
}
|
package/dist/operator/tools.js
CHANGED
|
@@ -708,19 +708,21 @@ server.registerTool("intent", {
|
|
|
708
708
|
return text(`saved to ${path.relative(ROOT, f)} — every visual check reads it from now on.`);
|
|
709
709
|
});
|
|
710
710
|
server.registerTool("calibrate", {
|
|
711
|
-
|
|
712
|
-
|
|
711
|
+
// It used to take `run: true` and answer, always, with a sentence telling
|
|
712
|
+
// you to run the CLI instead. An advertised parameter that cannot do the
|
|
713
|
+
// thing it names is a worse lie than an absent one: an agent plans around
|
|
714
|
+
// it, calls it, and reports back that it measured something.
|
|
715
|
+
description: "HOW MUCH TO TRUST `sweep`. READS the last measurement; it cannot make one, because measuring records a full take per defect and takes about ten minutes — that is `retake calibrate demos/<name>.yaml` at a terminal. A working demo is recorded once per known visual defect — clipped text, overlap, unreadable contrast, content off the edge, a doubled element, a stuck spinner, broken media, a dev badge — plus a control where nothing is wrong. Recall is how many seeded faults were found; false positives are findings on the control; stability is whether the control gave the same answer twice. Read this BEFORE telling somebody how much a sweep result is worth, and quote the numbers rather than your impression. Its limits are printed with it and should be repeated: the seeds were written by somebody who has read the checklist, so recall is an upper bound; injected faults are cleaner than real ones; and the run names outright which checklist items nothing seeds.",
|
|
716
|
+
inputSchema: { name: z.string().describe("the demo whose calibration to read") },
|
|
713
717
|
annotations: READ_ONLY,
|
|
714
|
-
}, async ({ name
|
|
718
|
+
}, async ({ name }) => {
|
|
715
719
|
if (!safe(name))
|
|
716
720
|
return text(`no demo "${name}"`);
|
|
717
721
|
const file = path.join(OUT, ".calibrate", name, "report.json");
|
|
718
|
-
if (run === true)
|
|
719
|
-
return text(`Measuring takes about ten minutes and records nine takes, so it is not run from here. Ask the person to run:\n\n retake calibrate demos/${name}.yaml`);
|
|
720
722
|
if (!fs.existsSync(file))
|
|
721
|
-
return text(`${name} has never been calibrated, so there is no measured answer to how much its sweep results are worth. \`retake calibrate demos/${name}.yaml\` produces one.`);
|
|
723
|
+
return text(`${name} has never been calibrated, so there is no measured answer to how much its sweep results are worth. \`retake calibrate demos/${name}.yaml\` produces one — at a terminal, not from here.`);
|
|
722
724
|
const r = JSON.parse(fs.readFileSync(file, "utf8"));
|
|
723
|
-
return text([...describeCalibration(r.results), "", `measured ${r.at}`].join("\n"));
|
|
725
|
+
return text([...describeCalibration(r.results, r.stability ?? null), "", `measured ${r.at}`].join("\n"));
|
|
724
726
|
});
|
|
725
727
|
server.registerTool("done", { description: "Call when the demo is recorded and acceptable (or when you are stopping). One sentence for the person.", inputSchema: { summary: z.string(), demo: z.string().optional() }, annotations: RETAKE_WRITE }, async ({ summary, demo }) => {
|
|
726
728
|
await tell(`Done: ${summary}`);
|
package/dist/ui/chat.html
CHANGED
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
/* The review surface. Retake is driven from an agent (or the workshop);
|
|
12
12
|
this page is where a person watches, judges, and collects the output.
|
|
13
13
|
It accepts no prose on purpose. */
|
|
14
|
-
:root { --paper:#edefe8; --card:#f8f9f5; --ink:#191d19; --muted:#
|
|
14
|
+
:root { --paper:#edefe8; --card:#f8f9f5; --ink:#191d19; --muted:#656c63; --rule:#dbdfd3; --rule2:#c9cec0; --accent:#1a1c1e; --amber:#e8b04b; --ok:#2f5d3f; --bad:#a23b2c; }
|
|
15
15
|
* { box-sizing:border-box; }
|
|
16
16
|
html,body { margin:0; height:100%; background:var(--paper); color:var(--ink); font:15px/1.6 -apple-system, BlinkMacSystemFont, "SF Pro Text", "Helvetica Neue", Inter, Arial, sans-serif; -webkit-font-smoothing:antialiased; }
|
|
17
17
|
button, input { font:inherit; color:inherit; }
|
|
@@ -105,7 +105,9 @@
|
|
|
105
105
|
the recording that were missing; they are optional services you can run. */
|
|
106
106
|
.review { margin-top:14px; border:1px solid var(--rule); border-radius:12px; padding:10px 14px 12px; background:var(--card); }
|
|
107
107
|
.rhead { font-size:11.5px; letter-spacing:.04em; text-transform:uppercase; color:var(--muted); display:flex; gap:10px; align-items:baseline; flex-wrap:wrap; }
|
|
108
|
-
.
|
|
108
|
+
/* No opacity. It took a colour already at the AA line and pushed it under.
|
|
109
|
+
Case and letter-spacing separate this from the label beside it already. */
|
|
110
|
+
.rhint { text-transform:none; letter-spacing:0; font-size:12px; }
|
|
109
111
|
.checked { margin-top:8px; font-size:12.5px; color:var(--muted); display:flex; gap:14px; flex-wrap:wrap; align-items:baseline; }
|
|
110
112
|
/* One per row on a phone: four of these side by side break into fragments
|
|
111
113
|
that read as unrelated words. */
|
|
@@ -116,7 +118,6 @@
|
|
|
116
118
|
.checked .c { display:inline-flex; gap:5px; align-items:baseline; }
|
|
117
119
|
.checked .c b { font-weight:500; color:var(--ink); }
|
|
118
120
|
.checked .c.none b { color:var(--muted); }
|
|
119
|
-
.checked .c.old { opacity:.6; }
|
|
120
121
|
.checked .c.bad b { color:var(--bad); }
|
|
121
122
|
.checked .why { font-size:12px; }
|
|
122
123
|
.flagged { margin-top:16px; border:1px solid var(--rule); background:var(--card); border-radius:16px; padding:14px 16px; }
|
|
@@ -396,7 +397,9 @@ $('newsBtn').onclick = async () => {
|
|
|
396
397
|
localStorage.setItem(SEEN_KEY, rs[0].version);
|
|
397
398
|
$('newsBtn').querySelector('.ndot')?.remove();
|
|
398
399
|
veil.onclick = (e) => { if (e.target === veil) veil.remove(); };
|
|
399
|
-
|
|
400
|
+
// `shut()` does not exist here — Escape threw a ReferenceError and left the
|
|
401
|
+
// modal sitting there. Copied from a dialog that had one.
|
|
402
|
+
document.addEventListener('keydown', function esc2(e) { if (e.key === 'Escape') { veil.remove(); document.removeEventListener('keydown', esc2); } });
|
|
400
403
|
};
|
|
401
404
|
|
|
402
405
|
document.getElementById('gremlinBtn').onclick = async () => {
|
|
@@ -480,9 +483,12 @@ async function pin(name) {
|
|
|
480
483
|
$('vidName').textContent = name;
|
|
481
484
|
// What the RECORDING did — separate from anything optional that looked at it.
|
|
482
485
|
const stateEl = $('vidState');
|
|
483
|
-
|
|
486
|
+
// Failure first, always. `brisk` was tested before `ok`, so a brisk take
|
|
487
|
+
// whose steps fell over displayed the neutral "Brisk take" and nothing said
|
|
488
|
+
// it had failed — on the one path people use while iterating fast.
|
|
489
|
+
if (t.ok === false) { stateEl.className = 'vstate bad'; stateEl.textContent = t.brisk ? 'Brisk take — steps failed' : 'Steps failed'; stateEl.title = 'Some steps did not pass — see the proof log.'; }
|
|
490
|
+
else if (t.brisk) { stateEl.className = 'vstate part'; stateEl.textContent = 'Brisk take'; stateEl.title = 'All the steps, none of the pacing. For iterating, not for keeping.'; }
|
|
484
491
|
else if (t.partial) { stateEl.className = 'vstate part'; stateEl.textContent = 'Partial'; stateEl.title = t.partial; }
|
|
485
|
-
else if (t.ok === false) { stateEl.className = 'vstate bad'; stateEl.textContent = 'Steps failed'; stateEl.title = 'Some steps did not pass — see the proof log.'; }
|
|
486
492
|
else { stateEl.className = 'vstate done'; stateEl.textContent = 'Recording complete'; stateEl.title = 'The take is sound: every step passed and the artifacts check out.'; }
|
|
487
493
|
$('vidMeta').textContent = `${(t.duration - t.trimBefore).toFixed(0)}s · ${t.ok ? 'all steps passed' : failed ? `stopped at step ${failed.index}: ${failed.summary}` : 'some steps failed'}${d.facts ? ` · ${d.facts.width}×${d.facts.height}` : ''}`;
|
|
488
494
|
$('vidMeta').title = failed?.screen ? 'On screen at that moment: ' + failed.screen : '';
|
package/dist/ui/guide.html
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
<title>How Retake works</title>
|
|
7
7
|
<link rel="icon" href="/favicon.svg" type="image/svg+xml" />
|
|
8
8
|
<style>
|
|
9
|
-
:root { --paper:#edefe8; --card:#f8f9f5; --ink:#191d19; --muted:#
|
|
9
|
+
:root { --paper:#edefe8; --card:#f8f9f5; --ink:#191d19; --muted:#656c63; --rule:#dbdfd3; --amber:#e8b04b; --accent:#1a1c1e; }
|
|
10
10
|
* { box-sizing:border-box; }
|
|
11
11
|
body { margin:0; background:var(--paper); color:var(--ink); font:16px/1.7 -apple-system, BlinkMacSystemFont, "SF Pro Text", "Helvetica Neue", Inter, Arial, sans-serif; -webkit-font-smoothing:antialiased; }
|
|
12
12
|
.top { display:flex; align-items:center; gap:10px; padding:14px 22px; --rule2:#c9cec0; }
|
package/dist/ui/server.js
CHANGED
|
@@ -202,14 +202,31 @@ function listDemos(project) {
|
|
|
202
202
|
return [];
|
|
203
203
|
const wanted = project?.trim() ? projectKey(project) : null;
|
|
204
204
|
const assignments = demoProjects();
|
|
205
|
+
// The precedence for what a demo's folder is called, in one place, because
|
|
206
|
+
// it was three places and the release note described a fourth:
|
|
207
|
+
//
|
|
208
|
+
// 1. a folder you assigned by hand
|
|
209
|
+
// 2. a folder assigned by hand to another demo of the same app
|
|
210
|
+
// 3. the name in this demo's own product note
|
|
211
|
+
// 4. the name in a product note written for another demo of the same app
|
|
212
|
+
// 5. the app's address, which is where this started and helps nobody
|
|
213
|
+
//
|
|
214
|
+
// Four is new. Writing `retake intent --demo checkout "# Acme"` named the
|
|
215
|
+
// checkout folder and left the other six Acme demos in a fold called
|
|
216
|
+
// "localhost:3200", which is the exact complaint the naming was added to
|
|
217
|
+
// answer.
|
|
205
218
|
const byUrl = new Map();
|
|
219
|
+
const namedByUrl = new Map();
|
|
206
220
|
for (const f of fs.readdirSync(DEMOS).filter((x) => /\.ya?ml$/.test(x))) {
|
|
207
221
|
const mm = cachedManifest(path.join(DEMOS, f));
|
|
208
|
-
if (mm)
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
222
|
+
if (!mm)
|
|
223
|
+
continue;
|
|
224
|
+
const asg = assignments[mm.name];
|
|
225
|
+
if (asg && mm.url && !byUrl.has(mm.url))
|
|
226
|
+
byUrl.set(mm.url, asg);
|
|
227
|
+
const named = productName(DEMOS, mm.name);
|
|
228
|
+
if (named && mm.url && !namedByUrl.has(mm.url))
|
|
229
|
+
namedByUrl.set(mm.url, named);
|
|
213
230
|
}
|
|
214
231
|
const listed = fs
|
|
215
232
|
.readdirSync(DEMOS)
|
|
@@ -243,7 +260,7 @@ function listDemos(project) {
|
|
|
243
260
|
// A product that has said what it is called is filed under that name.
|
|
244
261
|
// Grouping by URL gave folders like "localhost:3200" — true, and no help
|
|
245
262
|
// to anybody looking for their charter demos.
|
|
246
|
-
const group = assigned ? path.basename(assigned) : (productName(DEMOS, name) ?? shortGroup(url));
|
|
263
|
+
const group = assigned ? path.basename(assigned) : (productName(DEMOS, name) ?? (url ? namedByUrl.get(url) : undefined) ?? shortGroup(url));
|
|
247
264
|
// Heal a folder whose run was killed mid-recording: the previous take is
|
|
248
265
|
// in its stash and nothing has put it back yet. The window must never
|
|
249
266
|
// show someone an empty demo when their recording is safe on disk.
|
package/package.json
CHANGED
package/site/releases.json
CHANGED
|
@@ -1,6 +1,20 @@
|
|
|
1
1
|
{
|
|
2
2
|
"_readme": "What's new, written for the person using Retake — not for whoever wrote it. Both the landing page and the Retake window read THIS file; there is no second copy. Rules for adding one: say what changed for them, name no files, no flags they will not type, no words like refactor or regression. If a line only makes sense to someone who has read the source, it does not belong here. Newest first. Add an entry on every release that changes what people see.",
|
|
3
3
|
"releases": [
|
|
4
|
+
{
|
|
5
|
+
"version": "0.13.2",
|
|
6
|
+
"date": "2026-08-28",
|
|
7
|
+
"headline": "The thing that measures the checks was not measuring",
|
|
8
|
+
"notes": [
|
|
9
|
+
"Calibration is the number that says how much a visual finding is worth. Run against an app that was not even running, it used to print \"no false positives on the control\" and finish happily — because nothing had happened, so nothing was wrong. It now refuses: a run that fell over, recorded no steps, or produced no frames measures nothing, says which, and does not pretend otherwise.",
|
|
10
|
+
"One of its eight seeded faults could never have worked. A typo in the unreadable-contrast seed meant its own self-check could never be true, so that fault reported \"did not land\" on every app it was ever tried on. Fixed, and every seed is now tested in a real browser so a silently dead one cannot happen again.",
|
|
11
|
+
"Two more honest things it now says out loud: that two of the ten things it looks for have no seeded test at all, so the score says nothing about them; and whether asking the same question about the same pictures twice gives the same answer. That second one was promised in the code for two releases and never actually measured.",
|
|
12
|
+
"The checks run in a browser, and the browser was never installed on the build server — so every automated run since those tests were written failed before it started, and the build and packaging checks after them never ran. Installed now.",
|
|
13
|
+
"Fixed: pressing Escape on the What's New window did nothing. Fixed: a quick take whose steps failed showed a neutral \"Brisk take\" and never said it had failed. Fixed: the small print in the review area was too faint to meet the readability standard for text that size. Fixed: naming one demo's product now names the folder for every demo of the same app, not just that one. Fixed: a dry run now really scrolls when the demo says to scroll.",
|
|
14
|
+
"The agent tool for calibration used to advertise a setting that would run a fresh measurement, and always answered by telling you to go and run it yourself. That setting is gone.",
|
|
15
|
+
"And the landing page said \"nothing uploaded\", which was true of recording and not of the optional visual checks — those send the frames they look at to whichever model you point them at. It now says so."
|
|
16
|
+
]
|
|
17
|
+
},
|
|
4
18
|
{
|
|
5
19
|
"version": "0.13.1",
|
|
6
20
|
"date": "2026-08-28",
|
package/skill/SKILL.md
CHANGED
|
@@ -134,13 +134,18 @@ Three things about reading what it says:
|
|
|
134
134
|
- **A clean sweep is a floor, not a ceiling.** A closed checklist cannot find
|
|
135
135
|
what is not on it. It catches the ten things that go wrong in every app; it
|
|
136
136
|
does not certify the frame.
|
|
137
|
-
- **Say what it is worth, if anybody has measured.** `calibrate` seeds
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
137
|
+
- **Say what it is worth, if anybody has measured.** `calibrate` seeds known
|
|
138
|
+
defects into a real recording of the app and counts what `sweep` finds. Read
|
|
139
|
+
the report before telling somebody how much a finding is worth, and quote its
|
|
140
|
+
limits rather than only its number: the seeds were written by somebody who
|
|
141
|
+
had read the checklist, so recall is an upper bound; two of the ten checklist
|
|
142
|
+
items have no seed at all, so the number says nothing about them; and a run
|
|
143
|
+
whose control did not record cleanly measured nothing. Earlier versions of
|
|
144
|
+
this file quoted "seven or eight of eight, no false positives" — one of those
|
|
145
|
+
eight seeds could never have landed, because its proof was broken, so the
|
|
146
|
+
denominator was wrong and the claim about repeated runs had never been
|
|
147
|
+
measured. Quote the report you have, not a remembered figure. Never run it
|
|
148
|
+
unasked: nine takes, about ten minutes, and only from a terminal.
|
|
144
149
|
- **One pass is not reliable, and this is measured.** On a frame with a real
|
|
145
150
|
overlap on it, three consecutive sweeps missed it and a fourth found it. The
|
|
146
151
|
judge is not deterministic. So a frame that has already been looked at is
|