@tendrilapp/cli 0.1.16 → 0.1.17
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 +169 -11
- package/package.json +1 -1
package/dist/tendril.js
CHANGED
|
@@ -1974,8 +1974,9 @@ async function runTokenLint(css, fileLabel = "generated.css", definedVars2) {
|
|
|
1974
1974
|
codeFilename: fileLabel,
|
|
1975
1975
|
config: STYLELINT_CONFIG
|
|
1976
1976
|
});
|
|
1977
|
+
const mapKnownEmpty = definedVars2 !== void 0 && definedVars2.size === 0;
|
|
1977
1978
|
const violations = result.results.flatMap(
|
|
1978
|
-
(r) => r.warnings.map((w) => ({
|
|
1979
|
+
(r) => r.warnings.filter((w) => !(mapKnownEmpty && w.rule === "scale-unlimited/declaration-strict-value")).map((w) => ({
|
|
1979
1980
|
file: fileLabel,
|
|
1980
1981
|
line: w.line,
|
|
1981
1982
|
property: w.rule,
|
|
@@ -2392,6 +2393,111 @@ function cornersMatchCanvas(ref, canvas) {
|
|
|
2392
2393
|
(c) => Math.abs(c[0] - canvas[0]) + Math.abs(c[1] - canvas[1]) + Math.abs(c[2] - canvas[2]) <= INK_DELTA
|
|
2393
2394
|
);
|
|
2394
2395
|
}
|
|
2396
|
+
function absentInkClusters(render, reference, background = [255, 255, 255]) {
|
|
2397
|
+
const a = PNG.sync.read(Buffer.from(render));
|
|
2398
|
+
const b = PNG.sync.read(Buffer.from(reference));
|
|
2399
|
+
const width = Math.max(a.width, b.width);
|
|
2400
|
+
const height = Math.max(a.height, b.height);
|
|
2401
|
+
const canvasA = onCanvas(a, width, height);
|
|
2402
|
+
const canvasB = onCanvas(b, width, height);
|
|
2403
|
+
const ring = ringModal(canvasB);
|
|
2404
|
+
const backgrounds = channelDistance(ring, background) > INK_DELTA ? [background, ring] : [background];
|
|
2405
|
+
const found = [];
|
|
2406
|
+
for (const bg of backgrounds) {
|
|
2407
|
+
const renderInk = new Uint8Array(width * height);
|
|
2408
|
+
for (let y = 0; y < height; y++) {
|
|
2409
|
+
for (let x = 0; x < width; x++) {
|
|
2410
|
+
if (isInk(canvasA.data, (width * y + x) * 4, bg)) renderInk[width * y + x] = 1;
|
|
2411
|
+
}
|
|
2412
|
+
}
|
|
2413
|
+
const candidate = new Uint8Array(width * height);
|
|
2414
|
+
for (let y = 0; y < height; y++) {
|
|
2415
|
+
for (let x = 0; x < width; x++) {
|
|
2416
|
+
if (!isInk(canvasB.data, (width * y + x) * 4, bg)) continue;
|
|
2417
|
+
let covered = false;
|
|
2418
|
+
scan: for (let dy = -ABSENT_RADIUS; dy <= ABSENT_RADIUS; dy++) {
|
|
2419
|
+
for (let dx = -ABSENT_RADIUS; dx <= ABSENT_RADIUS; dx++) {
|
|
2420
|
+
const nx = x + dx;
|
|
2421
|
+
const ny = y + dy;
|
|
2422
|
+
if (nx < 0 || ny < 0 || nx >= width || ny >= height) continue;
|
|
2423
|
+
if (renderInk[width * ny + nx] === 1) {
|
|
2424
|
+
covered = true;
|
|
2425
|
+
break scan;
|
|
2426
|
+
}
|
|
2427
|
+
}
|
|
2428
|
+
}
|
|
2429
|
+
if (!covered) candidate[width * y + x] = 1;
|
|
2430
|
+
}
|
|
2431
|
+
}
|
|
2432
|
+
const seen = new Uint8Array(width * height);
|
|
2433
|
+
for (let sy = 0; sy < height; sy++) {
|
|
2434
|
+
for (let sx = 0; sx < width; sx++) {
|
|
2435
|
+
const si = width * sy + sx;
|
|
2436
|
+
if (candidate[si] !== 1 || seen[si] === 1) continue;
|
|
2437
|
+
let x0 = sx;
|
|
2438
|
+
let y0 = sy;
|
|
2439
|
+
let x1 = sx;
|
|
2440
|
+
let y1 = sy;
|
|
2441
|
+
let px = 0;
|
|
2442
|
+
const stack = [si];
|
|
2443
|
+
seen[si] = 1;
|
|
2444
|
+
while (stack.length > 0) {
|
|
2445
|
+
const i = stack.pop();
|
|
2446
|
+
const cx = i % width;
|
|
2447
|
+
const cy = (i - cx) / width;
|
|
2448
|
+
px += 1;
|
|
2449
|
+
if (cx < x0) x0 = cx;
|
|
2450
|
+
if (cy < y0) y0 = cy;
|
|
2451
|
+
if (cx > x1) x1 = cx;
|
|
2452
|
+
if (cy > y1) y1 = cy;
|
|
2453
|
+
for (let dy = -1; dy <= 1; dy++) {
|
|
2454
|
+
for (let dx = -1; dx <= 1; dx++) {
|
|
2455
|
+
const nx = cx + dx;
|
|
2456
|
+
const ny = cy + dy;
|
|
2457
|
+
if (nx < 0 || ny < 0 || nx >= width || ny >= height) continue;
|
|
2458
|
+
const ni = width * ny + nx;
|
|
2459
|
+
if (candidate[ni] === 1 && seen[ni] === 0) {
|
|
2460
|
+
seen[ni] = 1;
|
|
2461
|
+
stack.push(ni);
|
|
2462
|
+
}
|
|
2463
|
+
}
|
|
2464
|
+
}
|
|
2465
|
+
}
|
|
2466
|
+
if (px >= ABSENT_MIN_PX) found.push({ x: x0, y: y0, w: x1 - x0 + 1, h: y1 - y0 + 1, px, memberX: sx, memberY: sy });
|
|
2467
|
+
}
|
|
2468
|
+
}
|
|
2469
|
+
}
|
|
2470
|
+
const merged = [];
|
|
2471
|
+
for (const c of found.sort((c1, c2) => c2.px - c1.px)) {
|
|
2472
|
+
const overlaps = merged.some((m) => c.x < m.x + m.w && m.x < c.x + c.w && c.y < m.y + m.h && m.y < c.y + c.h);
|
|
2473
|
+
if (!overlaps) merged.push(c);
|
|
2474
|
+
}
|
|
2475
|
+
return merged;
|
|
2476
|
+
}
|
|
2477
|
+
function zoomCrop(png, rect, scale = 8, pad = 6) {
|
|
2478
|
+
const src = PNG.sync.read(Buffer.from(png));
|
|
2479
|
+
const x0 = Math.max(0, rect.x - pad);
|
|
2480
|
+
const y0 = Math.max(0, rect.y - pad);
|
|
2481
|
+
const x1 = Math.min(src.width, rect.x + rect.w + pad);
|
|
2482
|
+
const y1 = Math.min(src.height, rect.y + rect.h + pad);
|
|
2483
|
+
const w = Math.max(1, x1 - x0);
|
|
2484
|
+
const h = Math.max(1, y1 - y0);
|
|
2485
|
+
const out = new PNG({ width: w * scale, height: h * scale });
|
|
2486
|
+
for (let y = 0; y < h * scale; y++) {
|
|
2487
|
+
for (let x = 0; x < w * scale; x++) {
|
|
2488
|
+
const sx = x0 + Math.floor(x / scale);
|
|
2489
|
+
const sy = y0 + Math.floor(y / scale);
|
|
2490
|
+
const si = (src.width * sy + sx) * 4;
|
|
2491
|
+
const di = (out.width * y + x) * 4;
|
|
2492
|
+
const alpha = src.data[si + 3] ?? 0;
|
|
2493
|
+
out.data[di] = Math.round(((src.data[si] ?? 0) * alpha + 255 * (255 - alpha)) / 255);
|
|
2494
|
+
out.data[di + 1] = Math.round(((src.data[si + 1] ?? 0) * alpha + 255 * (255 - alpha)) / 255);
|
|
2495
|
+
out.data[di + 2] = Math.round(((src.data[si + 2] ?? 0) * alpha + 255 * (255 - alpha)) / 255);
|
|
2496
|
+
out.data[di + 3] = 255;
|
|
2497
|
+
}
|
|
2498
|
+
}
|
|
2499
|
+
return Uint8Array.from(PNG.sync.write(out));
|
|
2500
|
+
}
|
|
2395
2501
|
function measureCanvasCoupling(ref, renderA, renderB, backdrop, box) {
|
|
2396
2502
|
const rRaw = PNG.sync.read(Buffer.from(ref));
|
|
2397
2503
|
const r = onCanvas(rRaw, rRaw.width, rRaw.height);
|
|
@@ -2606,11 +2712,13 @@ function cropPng(png, x, y, width, height) {
|
|
|
2606
2712
|
}
|
|
2607
2713
|
return new Uint8Array(PNG.sync.write(out));
|
|
2608
2714
|
}
|
|
2609
|
-
var INK_DELTA;
|
|
2715
|
+
var INK_DELTA, ABSENT_RADIUS, ABSENT_MIN_PX;
|
|
2610
2716
|
var init_image_diff = __esm({
|
|
2611
2717
|
"packages/verify/src/image-diff.ts"() {
|
|
2612
2718
|
"use strict";
|
|
2613
2719
|
INK_DELTA = 30;
|
|
2720
|
+
ABSENT_RADIUS = 3;
|
|
2721
|
+
ABSENT_MIN_PX = 6;
|
|
2614
2722
|
}
|
|
2615
2723
|
});
|
|
2616
2724
|
|
|
@@ -4215,6 +4323,31 @@ function chooseSeat(cropAt, ref, padW, padH, backdrop, origin, extents) {
|
|
|
4215
4323
|
}
|
|
4216
4324
|
return { comparison, seat, scored: cropAt(origin - seat.x, origin - seat.y) };
|
|
4217
4325
|
}
|
|
4326
|
+
function deepestNodeNameAt(set, rep, px, py) {
|
|
4327
|
+
let root;
|
|
4328
|
+
try {
|
|
4329
|
+
const text = JSON.parse(readFileSync8(path15.join(set, rep, "get_metadata.json"), "utf8")).content.map((c) => c.text ?? "").join("\n");
|
|
4330
|
+
root = parseMetadataStructure(text);
|
|
4331
|
+
} catch {
|
|
4332
|
+
return void 0;
|
|
4333
|
+
}
|
|
4334
|
+
let best;
|
|
4335
|
+
const walk2 = (node, ox, oy, isRoot) => {
|
|
4336
|
+
if (node.hidden === true) return;
|
|
4337
|
+
const nx = isRoot ? 0 : ox + (node.x ?? 0);
|
|
4338
|
+
const ny = isRoot ? 0 : oy + (node.y ?? 0);
|
|
4339
|
+
const w = node.width ?? 0;
|
|
4340
|
+
const h = node.height ?? 0;
|
|
4341
|
+
const contains = px >= nx && py >= ny && px < nx + w && py < ny + h;
|
|
4342
|
+
if (contains && !isRoot && node.name !== "") {
|
|
4343
|
+
const area = w * h;
|
|
4344
|
+
if (best === void 0 || area <= best.area) best = { name: `${node.name} (${node.id})`, area };
|
|
4345
|
+
}
|
|
4346
|
+
for (const child of node.children) walk2(child, nx, ny, false);
|
|
4347
|
+
};
|
|
4348
|
+
walk2(root, 0, 0, true);
|
|
4349
|
+
return best?.name;
|
|
4350
|
+
}
|
|
4218
4351
|
function repMeta(set, rep) {
|
|
4219
4352
|
const text = JSON.parse(readFileSync8(path15.join(set, rep, "get_metadata.json"), "utf8")).content.map((c) => c.text ?? "").join("\n");
|
|
4220
4353
|
const root = parseMetadataStructure(text);
|
|
@@ -4335,10 +4468,18 @@ body{margin:0;background:rgb(${backdrop.join(",")})}
|
|
|
4335
4468
|
const slab = realPadBoth && coupling.padCorners.masked >= 8 && coupling.padCorners.opaque / coupling.padCorners.masked >= 0.9;
|
|
4336
4469
|
const pass = r.similarity >= bar.sim && r.inkRecall >= bar.ink && !slab;
|
|
4337
4470
|
const region = pass ? void 0 : localizeDifference(scored, ref);
|
|
4471
|
+
const absent = absentInkClusters(scored, ref, backdrop).map((c) => {
|
|
4472
|
+
const name = deepestNodeNameAt(task.set, cfg.rep, c.memberX - seat.x, c.memberY - seat.y);
|
|
4473
|
+
return name === void 0 ? c : { ...c, name };
|
|
4474
|
+
});
|
|
4338
4475
|
if (opts.evidenceDir !== void 0) {
|
|
4339
4476
|
writeFileSync6(path15.join(opts.evidenceDir, `${cfg.rep}-render.png`), scored);
|
|
4340
4477
|
writeFileSync6(path15.join(opts.evidenceDir, `${cfg.rep}-ref.png`), ref);
|
|
4341
4478
|
writeFileSync6(path15.join(opts.evidenceDir, `${cfg.rep}-diff.png`), diffPng(scored, ref));
|
|
4479
|
+
for (const [i, c] of absent.slice(0, 3).entries()) {
|
|
4480
|
+
writeFileSync6(path15.join(opts.evidenceDir, `${cfg.rep}-absent-${i}-ref.png`), zoomCrop(ref, c));
|
|
4481
|
+
writeFileSync6(path15.join(opts.evidenceDir, `${cfg.rep}-absent-${i}-render.png`), zoomCrop(scored, c));
|
|
4482
|
+
}
|
|
4342
4483
|
}
|
|
4343
4484
|
return {
|
|
4344
4485
|
rep: cfg.rep,
|
|
@@ -4349,6 +4490,7 @@ body{margin:0;background:rgb(${backdrop.join(",")})}
|
|
|
4349
4490
|
...slab ? { error: `paints the page canvas to the pad corners (${coupling.padCorners.opaque}/${coupling.padCorners.masked} opaque) \u2014 a square slab carrying the recorded canvas past the frame, wrong on any real page` } : {},
|
|
4350
4491
|
...region === void 0 ? {} : { region: { x0: region.x0, y0: region.y0, x1: region.x1, y1: region.y1, density: Math.round(region.density * 100) / 100 } },
|
|
4351
4492
|
...seat.x === 0 && seat.y === 0 ? {} : { seat },
|
|
4493
|
+
...absent.length > 0 ? { absentInk: absent } : {},
|
|
4352
4494
|
...coupling.padMasked + coupling.inBoxMasked > 0 ? { canvasCoupling: coupling } : {}
|
|
4353
4495
|
};
|
|
4354
4496
|
} finally {
|
|
@@ -8052,6 +8194,7 @@ OVERLAYS GO IN THE TOP LAYER, NOT ON A Z-INDEX. The prelude requires isolation:
|
|
|
8052
8194
|
|
|
8053
8195
|
PAINT & PLATFORM TRAPS (each measured on a paid run; every one passed typecheck, token lint, and all behaviour checks and was caught only by pixels or by use):
|
|
8054
8196
|
- A composite custom property resolves on the element that DECLARES it. A composite token (e.g. --ring: 0 0 0 var(--w) var(--c)) may only reference variables declared at or above its own declaration site \u2014 declared higher while its parts are set on the component, it is invalid-at-computed-value-time and the property silently computes to nothing (measured: a hover ring vanished; three configs at ~0.78).
|
|
8197
|
+
- An <svg> inside an absolutely-positioned glyph layer defaults to display: inline \u2014 it seats on a text baseline, its painted content lands BELOW the box the layout allocated, and an overflow: hidden ancestor clips the displaced ink away entirely. Large glyphs survive; small marks (a 2px dot, a 1.5px stem) vanish while every geometry probe reports the boxes correct (measured: 12/16 configs shipped icons missing their inner marks, certified at ink 0.9995). Set display: block on every glyph svg.
|
|
8055
8198
|
- Chrome pixel-snaps an <svg> root's paint offset. A fractional inset on an svg element floors to a whole pixel and moves a hairline a full pixel; express fractional placement as transform: scale() about an integer-origin frame (measured: 0.943 \u2192 1.000 on six configs).
|
|
8056
8199
|
- Pre-composite translucent values over the ACTUAL parent surface, read off the reference pixels \u2014 a control sitting on an elevated surface composites against that surface, not against the page.
|
|
8057
8200
|
- A native <dialog> carries user-agent padding: 1em. Override every side, or the root grows past its recorded box and every band inside shifts.
|
|
@@ -8880,9 +9023,15 @@ async function runVerify(opts) {
|
|
|
8880
9023
|
const statuses = [
|
|
8881
9024
|
...scores.map((s) => {
|
|
8882
9025
|
const { exact: _exact, ...reported } = s;
|
|
8883
|
-
|
|
9026
|
+
let status = tierOf(s, BARS2.cert);
|
|
9027
|
+
const certDemote = [];
|
|
9028
|
+
if (status === "certified" && s.absentInk !== void 0 && s.absentInk.length > 0) {
|
|
9029
|
+
status = "pass";
|
|
9030
|
+
certDemote.push(...s.absentInk.map((c) => `absent ink: ${c.name ?? "unnamed region"} \u2014 ${c.px}px of recorded ink with no render ink within reach (missing or invisible feature)`));
|
|
9031
|
+
}
|
|
9032
|
+
const base = certDemote.length > 0 ? { ...reported, status, demotedBy: certDemote } : { ...reported, status };
|
|
8884
9033
|
const reasons = demotions.get(s.rep);
|
|
8885
|
-
return reasons === void 0 ? base : { ...base, status: "fail", demotedBy: reasons };
|
|
9034
|
+
return reasons === void 0 ? base : { ...base, status: "fail", demotedBy: [...certDemote.length > 0 ? certDemote : [], ...reasons] };
|
|
8886
9035
|
}),
|
|
8887
9036
|
// ADR-010 §2 anti-gaming: recorded configs the adapter does not map
|
|
8888
9037
|
// are FAILs, never silently absent.
|
|
@@ -9084,12 +9233,12 @@ function resolveEngineTask(opts, callerCwd) {
|
|
|
9084
9233
|
const asPath = path32.resolve(callerCwd, opts.taskOrSet);
|
|
9085
9234
|
const isSet = existsSync26(path32.join(asPath, "recording-set.json"));
|
|
9086
9235
|
const registry = TASKS[opts.taskOrSet];
|
|
9087
|
-
if (registry !== void 0 && !isSet) return { task: registry, name: opts.taskOrSet };
|
|
9236
|
+
if (registry !== void 0 && !isSet) return { task: registry, name: opts.taskOrSet, disclosures: [] };
|
|
9088
9237
|
if (isSet) {
|
|
9089
9238
|
try {
|
|
9090
9239
|
const authored = authorTaskFromSet(asPath);
|
|
9091
9240
|
for (const d of authored.disclosures) warn(opts, d);
|
|
9092
|
-
return { task: authored.task, name: path32.basename(asPath), apiPin: { props: authored.api.apiPin.props, forcedStates: authored.api.apiPin.forcedStates } };
|
|
9241
|
+
return { task: authored.task, name: path32.basename(asPath), disclosures: authored.disclosures, apiPin: { props: authored.api.apiPin.props, forcedStates: authored.api.apiPin.forcedStates } };
|
|
9093
9242
|
} catch (err) {
|
|
9094
9243
|
fail(opts, ExitCode.InputValidation, {
|
|
9095
9244
|
error: `cannot author a task from ${asPath}: ${err instanceof Error ? err.message.split("\n")[0] : String(err)}`,
|
|
@@ -9107,9 +9256,13 @@ function resolveEngineTask(opts, callerCwd) {
|
|
|
9107
9256
|
function runEngineBrief(opts) {
|
|
9108
9257
|
requireEntitlement(opts);
|
|
9109
9258
|
const callerCwd = process.env["INIT_CWD"] ?? process.cwd();
|
|
9110
|
-
const { task, name } = resolveEngineTask(opts, callerCwd);
|
|
9259
|
+
const { task, name, disclosures } = resolveEngineTask(opts, callerCwd);
|
|
9111
9260
|
const bar = BARS3[opts.bar];
|
|
9112
|
-
const
|
|
9261
|
+
const disclosureBlock = disclosures.length > 0 ? `
|
|
9262
|
+
|
|
9263
|
+
=== RECORDED-SET DISCLOSURES (facts about this task's coverage \u2014 read them) ===
|
|
9264
|
+
${disclosures.map((d) => `- ${d}`).join("\n")}` : "";
|
|
9265
|
+
const brief = buildBrief(task.systemApi, bar, { colorScheme: recordingIsDark(task) ? "dark" : "light" }) + disclosureBlock;
|
|
9113
9266
|
const segments = buildSegments(task, "files");
|
|
9114
9267
|
let notRecorded;
|
|
9115
9268
|
const manifestPath2 = path32.join(task.set, "recording-set.json");
|
|
@@ -9249,9 +9402,14 @@ ${[
|
|
|
9249
9402
|
const allPass = obj[0] === total && total > 0;
|
|
9250
9403
|
const certBar = BARS3["cert"];
|
|
9251
9404
|
const parityDemoted = new Set(parity.filter((p) => !p.pass).map((p) => p.id.replace(/^parity:/, "")));
|
|
9252
|
-
const certifiedReps = scores.filter((sc) => tierOf(sc, certBar) === "certified" && !parityDemoted.has(sc.rep)).map((sc) => sc.rep);
|
|
9405
|
+
const certifiedReps = scores.filter((sc) => tierOf(sc, certBar) === "certified" && !parityDemoted.has(sc.rep) && (sc.absentInk === void 0 || sc.absentInk.length === 0)).map((sc) => sc.rep);
|
|
9253
9406
|
const certifiedSet = new Set(certifiedReps);
|
|
9254
|
-
const
|
|
9407
|
+
const absentFindings = scores.flatMap((sc) => (sc.absentInk ?? []).map((c) => `- ${sc.rep}: ${c.name ?? "unnamed region"} \u2014 ${c.px}px of recorded ink with NO render ink within reach at [x ${c.x}-${c.x + c.w}, y ${c.y}-${c.y + c.h}] (missing or invisible feature; magnified crops in the evidence dir; coordinates are reference-space \u2014 subtract the config's seat for box-space). A certified-tier config with such a finding is demoted to pass.`));
|
|
9408
|
+
const absentBlock = absentFindings.length > 0 ? `
|
|
9409
|
+
|
|
9410
|
+
MISSING FEATURES (absent-ink clusters \u2014 recorded ink your render leaves nowhere near covered; fix these first, the global numbers cannot see them):
|
|
9411
|
+
${absentFindings.join("\n")}` : "";
|
|
9412
|
+
const certificationFeedback = `${absentBlock}
|
|
9255
9413
|
|
|
9256
9414
|
CERTIFICATION: ${certifiedReps.length}/${scores.length} configs at the certification bar (sim \u2265${certBar.sim} AND ink \u2265${certBar.ink}, exact values, after parity demotion \u2014 composition checks at verify can demote further).${certifiedReps.length < scores.length ? ` Below cert: ${scores.filter((sc) => !certifiedSet.has(sc.rep)).map((sc) => sc.rep).join(", ")}.` : ""}
|
|
9257
9415
|
METRIC DEADBAND (read before iterating on near-misses): the scored similarity/ink deliberately tolerate \xB11px edge shift and antialiased-edge differences \u2014 cross-rasterizer noise absorption. A change entirely inside that band moves these numbers by EXACTLY ZERO (working as designed, not a stuck scorer). The per-config \`exact\` fields in the JSON are tolerance-free and move first: compare exact across rounds to confirm a small fix landed, and stop iterating when only exact moves \u2014 the bar reads the tolerant numbers.`;
|
|
@@ -9286,7 +9444,7 @@ METRIC DEADBAND (read before iterating on near-misses): the scored similarity/in
|
|
|
9286
9444
|
bundleManifest: emitted.written[0],
|
|
9287
9445
|
note: "styles.css was stamped with the bundle provenance comment on line 1 \u2014 preserve it in any post-score edit",
|
|
9288
9446
|
allPass,
|
|
9289
|
-
certification: { certified: certifiedReps.length, total: scores.length, bar: certBar, note: "tierOf on exact values + parity demotion; verify's composition checks can demote further" }
|
|
9447
|
+
certification: { certified: certifiedReps.length, total: scores.length, bar: certBar, note: "tierOf on exact values + parity and absent-ink demotion; verify's composition checks can demote further" }
|
|
9290
9448
|
},
|
|
9291
9449
|
() => {
|
|
9292
9450
|
for (const s of scores) process.stdout.write(`${s.pass ? certifiedSet.has(s.rep) ? "CERT" : "PASS" : "FAIL"} ${s.rep}: sim=${s.similarity} ink=${s.inkRecall}${s.error !== void 0 ? ` [${s.error}]` : ""}
|