@tendrilapp/cli 0.1.38 → 0.1.39
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/tendril.js +72 -4
- package/package.json +1 -1
package/dist/tendril.js
CHANGED
|
@@ -1203,6 +1203,33 @@ function containedPath(setDir, ...segments) {
|
|
|
1203
1203
|
}
|
|
1204
1204
|
return target;
|
|
1205
1205
|
}
|
|
1206
|
+
function clampedReference(setDir, slug, payload) {
|
|
1207
|
+
const metaFile = containedPath(setDir, slug, "get_metadata.json");
|
|
1208
|
+
if (!existsSync(metaFile)) return void 0;
|
|
1209
|
+
let box;
|
|
1210
|
+
try {
|
|
1211
|
+
const m = /width="([\d.]+)"\s+height="([\d.]+)"/.exec(envelopeTextContent(JSON.parse(readFileSync(metaFile, "utf8"))));
|
|
1212
|
+
if (m !== null) box = { w: Math.round(Number(m[1])), h: Math.round(Number(m[2])) };
|
|
1213
|
+
} catch {
|
|
1214
|
+
return void 0;
|
|
1215
|
+
}
|
|
1216
|
+
if (box === void 0 || box.w === 0 || box.h === 0) return void 0;
|
|
1217
|
+
const dims = referencePngDims(payload);
|
|
1218
|
+
if (dims === void 0) return void 0;
|
|
1219
|
+
if (dims.w >= box.w - 1 && dims.h >= box.h - 1) return void 0;
|
|
1220
|
+
return `the reference image is ${dims.w}\xD7${dims.h} but the recorded box is ${box.w}\xD7${box.h} \u2014 it was SCALED DOWN, so it is not pixel ground truth. Re-run get_screenshot passing an explicit maxDimension of at least ${Math.max(box.w, box.h)} (the tool defaults to 1024 and scales to fit)`;
|
|
1221
|
+
}
|
|
1222
|
+
function referencePngDims(payload) {
|
|
1223
|
+
const parts = payload?.content;
|
|
1224
|
+
if (!Array.isArray(parts)) return void 0;
|
|
1225
|
+
for (const p of parts) {
|
|
1226
|
+
if (p?.type !== "image" || typeof p.data !== "string") continue;
|
|
1227
|
+
const buf = Buffer.from(p.data, "base64");
|
|
1228
|
+
if (buf.length < 24) return void 0;
|
|
1229
|
+
return { w: buf.readUInt32BE(16), h: buf.readUInt32BE(20) };
|
|
1230
|
+
}
|
|
1231
|
+
return void 0;
|
|
1232
|
+
}
|
|
1206
1233
|
function ingestEnvelope(setDir, slug, tool, payload) {
|
|
1207
1234
|
const manifest = loadManifest(setDir);
|
|
1208
1235
|
if (!manifest.reps.some((r) => r.slug === slug)) throw new Error(`unknown rep "${slug}" \u2014 not in the planned manifest`);
|
|
@@ -1218,6 +1245,10 @@ function ingestEnvelope(setDir, slug, tool, payload) {
|
|
|
1218
1245
|
if (!parsed.success) {
|
|
1219
1246
|
throw new Error(`${slug}/${tool} was NOT recorded \u2014 envelope rejected at the boundary: ${parsed.error.issues[0]?.message ?? "invalid"}. ${REINGEST_GUIDANCE}`);
|
|
1220
1247
|
}
|
|
1248
|
+
const clamped = tool === "get_screenshot" ? clampedReference(setDir, slug, payload) : void 0;
|
|
1249
|
+
if (clamped !== void 0) {
|
|
1250
|
+
throw new Error(`${slug}/${tool} was NOT recorded \u2014 ${clamped}. ${REINGEST_GUIDANCE}`);
|
|
1251
|
+
}
|
|
1221
1252
|
const file = containedPath(setDir, slug, `${tool}.json`);
|
|
1222
1253
|
mkdirSync(path.dirname(file), { recursive: true });
|
|
1223
1254
|
const overwrote = existsSync(file);
|
|
@@ -3514,7 +3545,7 @@ function localizeDifference(render, reference, masks = []) {
|
|
|
3514
3545
|
const boxArea = (x1 - x0 + 1) * (y1 - y0 + 1);
|
|
3515
3546
|
return { x0, y0, x1, y1, density: count / boxArea };
|
|
3516
3547
|
}
|
|
3517
|
-
function diffPng(render, reference, masks = []) {
|
|
3548
|
+
function diffPng(render, reference, masks = [], background = [255, 255, 255]) {
|
|
3518
3549
|
const a = PNG.sync.read(Buffer.from(render));
|
|
3519
3550
|
const b = PNG.sync.read(Buffer.from(reference));
|
|
3520
3551
|
const width = Math.max(a.width, b.width);
|
|
@@ -3527,6 +3558,29 @@ function diffPng(render, reference, masks = []) {
|
|
|
3527
3558
|
}
|
|
3528
3559
|
const out = new PNG({ width, height });
|
|
3529
3560
|
pixelmatch(canvasA.data, canvasB.data, out.data, width, height, { threshold: 0.15 });
|
|
3561
|
+
const inkAt = (data, index) => isInk(data, index * 4, background);
|
|
3562
|
+
for (let y = 0; y < height; y++) {
|
|
3563
|
+
for (let x = 0; x < width; x++) {
|
|
3564
|
+
const i = width * y + x;
|
|
3565
|
+
const o = i * 4;
|
|
3566
|
+
if (!(out.data[o] === 255 && out.data[o + 1] === 0 && out.data[o + 2] === 0)) continue;
|
|
3567
|
+
const refInk = inkAt(canvasB.data, i);
|
|
3568
|
+
const renInk = inkAt(canvasA.data, i);
|
|
3569
|
+
let covered = false;
|
|
3570
|
+
for (let dy = -1; dy <= 1 && !covered; dy++) {
|
|
3571
|
+
for (let dx = -1; dx <= 1 && !covered; dx++) {
|
|
3572
|
+
const nx = x + dx;
|
|
3573
|
+
const ny = y + dy;
|
|
3574
|
+
if (nx < 0 || ny < 0 || nx >= width || ny >= height) continue;
|
|
3575
|
+
if (inkAt(canvasA.data, width * ny + nx)) covered = true;
|
|
3576
|
+
}
|
|
3577
|
+
}
|
|
3578
|
+
const colour = refInk && !renInk ? covered ? DIFF_LEGEND.shift : DIFF_LEGEND.absent : !refInk && renInk ? DIFF_LEGEND.extra : DIFF_LEGEND.recolor;
|
|
3579
|
+
out.data[o] = colour[0];
|
|
3580
|
+
out.data[o + 1] = colour[1];
|
|
3581
|
+
out.data[o + 2] = colour[2];
|
|
3582
|
+
}
|
|
3583
|
+
}
|
|
3530
3584
|
return new Uint8Array(PNG.sync.write(out));
|
|
3531
3585
|
}
|
|
3532
3586
|
function cropPng(png, x, y, width, height) {
|
|
@@ -3550,13 +3604,27 @@ function cropPng(png, x, y, width, height) {
|
|
|
3550
3604
|
}
|
|
3551
3605
|
return new Uint8Array(PNG.sync.write(out));
|
|
3552
3606
|
}
|
|
3553
|
-
var INK_DELTA, ABSENT_RADIUS, ABSENT_MIN_PX;
|
|
3607
|
+
var INK_DELTA, ABSENT_RADIUS, ABSENT_MIN_PX, DIFF_LEGEND;
|
|
3554
3608
|
var init_image_diff = __esm({
|
|
3555
3609
|
"packages/verify/src/image-diff.ts"() {
|
|
3556
3610
|
"use strict";
|
|
3557
3611
|
INK_DELTA = 30;
|
|
3558
3612
|
ABSENT_RADIUS = 3;
|
|
3559
3613
|
ABSENT_MIN_PX = 6;
|
|
3614
|
+
DIFF_LEGEND = {
|
|
3615
|
+
/** Reference ink with no render ink within ±1px. THIS is the ink
|
|
3616
|
+
* deficit — the only category that moves inkRecall. Red. */
|
|
3617
|
+
absent: [255, 0, 0],
|
|
3618
|
+
/** Ink in both, different colour. Moves similarity only. Amber. */
|
|
3619
|
+
recolor: [255, 170, 0],
|
|
3620
|
+
/** Render painted ink where the reference records none. Blue. */
|
|
3621
|
+
extra: [0, 120, 255],
|
|
3622
|
+
/** Reference ink covered within the metric's ±1px neighbourhood —
|
|
3623
|
+
* ALREADY FORGIVEN by inkRecall. Dimmed grey, so it reads as tolerated
|
|
3624
|
+
* rather than as a defect to chase (measured at only 3.0% of red, but
|
|
3625
|
+
* chasing it is exactly what the nudge saga was). */
|
|
3626
|
+
shift: [150, 150, 150]
|
|
3627
|
+
};
|
|
3560
3628
|
}
|
|
3561
3629
|
});
|
|
3562
3630
|
|
|
@@ -6950,7 +7018,7 @@ body{margin:0;background:rgb(${backdrop.join(",")})}
|
|
|
6950
7018
|
if (opts.evidenceDir !== void 0) {
|
|
6951
7019
|
writeFileSync6(path20.join(opts.evidenceDir, `${cfg.rep}-render.png`), scored);
|
|
6952
7020
|
writeFileSync6(path20.join(opts.evidenceDir, `${cfg.rep}-ref.png`), ref);
|
|
6953
|
-
writeFileSync6(path20.join(opts.evidenceDir, `${cfg.rep}-diff.png`), diffPng(scored, ref));
|
|
7021
|
+
writeFileSync6(path20.join(opts.evidenceDir, `${cfg.rep}-diff.png`), diffPng(scored, ref, [], backdrop));
|
|
6954
7022
|
for (const [i, c] of absent.slice(0, 3).entries()) {
|
|
6955
7023
|
writeFileSync6(path20.join(opts.evidenceDir, `${cfg.rep}-absent-${i}-ref.png`), zoomCrop(ref, c));
|
|
6956
7024
|
writeFileSync6(path20.join(opts.evidenceDir, `${cfg.rep}-absent-${i}-render.png`), zoomCrop(scored, c));
|
|
@@ -14303,7 +14371,7 @@ var init_record = __esm({
|
|
|
14303
14371
|
const shown = items.length <= MAX_SPOKEN_VALUES ? items : [...items.slice(0, MAX_SPOKEN_VALUES), `${items.length - MAX_SPOKEN_VALUES} more`];
|
|
14304
14372
|
return shown.length <= 1 ? shown[0] ?? "" : `${shown.slice(0, -1).join(", ")} and ${shown[shown.length - 1]}`;
|
|
14305
14373
|
};
|
|
14306
|
-
ENVELOPE_HELP = `Envelope format: text tools save {"content":[{"type":"text","text":"<VERBATIM response text incl. any Currently-selected-nodes block>"}]}; get_screenshot: do NOT download the image yourself \u2014 pass its image_url to \`${tendrilCommand("record fetch")}\` (MCP: tendril_record_fetch), which pulls the bytes to disk directly. That keeps the pixel ground truth out of your context and costs one approval instead of a shell command per asset. Assets are asset-<first-8-hex-of-figma-uuid>.<ext>; their URLs appear as const declarations inside the design-context text and also expire.`;
|
|
14374
|
+
ENVELOPE_HELP = `Envelope format: text tools save {"content":[{"type":"text","text":"<VERBATIM response text incl. any Currently-selected-nodes block>"}]}; VERBATIM MEANS EVERY PART, NOT JUST THE TEXT ONE: if a response carries additional content blocks \u2014 an image block especially \u2014 keep them in the envelope you save. get_design_context is documented to return a screenshot alongside the code, and whether it actually does is a question our whole recorded corpus cannot answer because every saved envelope holds text only. Saving what arrives settles it at no extra call. get_screenshot: pass an explicit maxDimension of AT LEAST the node's longer edge from the get_metadata you just recorded \u2014 the tool DEFAULTS TO 1024 and SCALES the node to fit, so a component wider or taller than that silently becomes a downscaled reference, which is not pixel ground truth (ingest now refuses a reference smaller than its own recorded box). Then do NOT download the image yourself \u2014 pass its image_url to \`${tendrilCommand("record fetch")}\` (MCP: tendril_record_fetch), which pulls the bytes to disk directly. That keeps the pixel ground truth out of your context and costs one approval instead of a shell command per asset. Assets are asset-<first-8-hex-of-figma-uuid>.<ext>; their URLs appear as const declarations inside the design-context text and also expire.`;
|
|
14307
14375
|
isAutoFetchAssetUrl = (url) => isFigmaAssetUrl(url) || isLocalAssetUrl(url);
|
|
14308
14376
|
describeRoleLoss = (loss) => loss.kind === "main" ? `drops main "${loss.main}"` : `stops "${loss.part}" being a part of main "${loss.main}"`;
|
|
14309
14377
|
}
|