@tendrilapp/cli 0.1.20 → 0.1.21
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/SKILL.md +22 -10
- package/dist/tendril.js +58 -31
- package/package.json +1 -1
package/dist/SKILL.md
CHANGED
|
@@ -39,16 +39,28 @@ user must reload the session to apply; say that out loud. Offer ONCE
|
|
|
39
39
|
per session; never run it unoffered (it edits the user's settings),
|
|
40
40
|
and if declined, proceed without mentioning it again.
|
|
41
41
|
|
|
42
|
-
Long generator/score waits: silence is not a health signal
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
42
|
+
Long generator/score waits: silence is not a health signal, and POLL
|
|
43
|
+
cadence is not RELAY cadence (run 13: rounds landed 6–11 minutes apart,
|
|
44
|
+
so a correct 30s poll still left the user in 9-minute silences they
|
|
45
|
+
flagged as unacceptable). Two duties: relay each round's result the
|
|
46
|
+
moment `<candidateDir>/score-history.jsonl` gains a line ("round 3
|
|
47
|
+
scored: 15/21 at floor 0.885"), AND when more than ~60 seconds pass
|
|
48
|
+
with no round, emit a liveness line from the candidate dir's file
|
|
49
|
+
mtimes, which move while the generator is WRITING (thinking pauses are quiet — that is normal, not frozen) ("still
|
|
50
|
+
working — styles.css touched 20s ago"). The history file APPEARS ONLY
|
|
51
|
+
AFTER THE FIRST completed score, so its absence early is normal, not
|
|
52
|
+
frozen. A healthy run can take 30+ minutes; never kill on elapsed time
|
|
53
|
+
alone — count staleness from the LAST CHANGE to any signal, and when
|
|
54
|
+
past ~20 minutes of true staleness, ask the user before killing.
|
|
55
|
+
|
|
56
|
+
Before concluding "inherent limitation" on a stuck score: enumerate the
|
|
57
|
+
rendering inputs you have NOT verified — are the declared font faces
|
|
58
|
+
actually resolved (the kit's cached faces and weights vs what the CSS
|
|
59
|
+
asks for), computed styles vs authored styles, and
|
|
60
|
+
platform text antialiasing — and check them first. Run 13's "inherent
|
|
61
|
+
fidelity gap" was a missing font weight one check away; a stop rule is
|
|
62
|
+
for avoiding thrash, never for converting an unverified hypothesis into
|
|
63
|
+
a final answer.
|
|
52
64
|
|
|
53
65
|
Batch runs (several components in one session):
|
|
54
66
|
- Cap concurrent recorders at FOUR. All recorders share one Figma
|
package/dist/tendril.js
CHANGED
|
@@ -2274,6 +2274,31 @@ var init_report = __esm({
|
|
|
2274
2274
|
}
|
|
2275
2275
|
});
|
|
2276
2276
|
|
|
2277
|
+
// packages/verify/src/mount-limits.ts
|
|
2278
|
+
function lcdTextDisabled() {
|
|
2279
|
+
return process.env["TENDRIL_DISABLE_LCD_TEXT"] === "1";
|
|
2280
|
+
}
|
|
2281
|
+
function mountArgs() {
|
|
2282
|
+
return [MEMORY_CAP_ARG, ...lcdTextDisabled() ? ["--disable-lcd-text"] : []];
|
|
2283
|
+
}
|
|
2284
|
+
function raceMountDeadline(work, deadlineMs, onDeadline) {
|
|
2285
|
+
return Promise.race([
|
|
2286
|
+
work,
|
|
2287
|
+
new Promise((resolve) => {
|
|
2288
|
+
const timer = setTimeout(() => resolve(onDeadline()), deadlineMs);
|
|
2289
|
+
timer.unref();
|
|
2290
|
+
})
|
|
2291
|
+
]);
|
|
2292
|
+
}
|
|
2293
|
+
var MEMORY_CAP_ARG, ENGINE_MOUNT_DEADLINE_MS;
|
|
2294
|
+
var init_mount_limits = __esm({
|
|
2295
|
+
"packages/verify/src/mount-limits.ts"() {
|
|
2296
|
+
"use strict";
|
|
2297
|
+
MEMORY_CAP_ARG = "--js-flags=--max-old-space-size=512";
|
|
2298
|
+
ENGINE_MOUNT_DEADLINE_MS = 3e4;
|
|
2299
|
+
}
|
|
2300
|
+
});
|
|
2301
|
+
|
|
2277
2302
|
// packages/verify/src/image-diff.ts
|
|
2278
2303
|
import { PNG } from "pngjs";
|
|
2279
2304
|
import pixelmatch from "pixelmatch";
|
|
@@ -3030,7 +3055,7 @@ ${input.tokensCss}
|
|
|
3030
3055
|
${input.files.css}
|
|
3031
3056
|
#root { display: inline-block; }
|
|
3032
3057
|
</style></head><body><div id="root"></div><script>${js}</script></body></html>`;
|
|
3033
|
-
const browser = await chromium.launch({ executablePath, headless: true });
|
|
3058
|
+
const browser = await chromium.launch({ executablePath, headless: true, args: mountArgs() });
|
|
3034
3059
|
try {
|
|
3035
3060
|
const page = await browser.newPage({ viewport: { width: 800, height: 600 } });
|
|
3036
3061
|
await page.setContent(html, { waitUntil: "load" });
|
|
@@ -3268,6 +3293,7 @@ var init_visual_facts = __esm({
|
|
|
3268
3293
|
"packages/verify/src/visual-facts.ts"() {
|
|
3269
3294
|
"use strict";
|
|
3270
3295
|
init_browser();
|
|
3296
|
+
init_mount_limits();
|
|
3271
3297
|
init_image_diff();
|
|
3272
3298
|
RESOLVE_DIR = path7.resolve(path7.dirname(fileURLToPath2(import.meta.url)), "..");
|
|
3273
3299
|
TOLERANCE_PX = 2;
|
|
@@ -3485,25 +3511,6 @@ var init_font_faces = __esm({
|
|
|
3485
3511
|
}
|
|
3486
3512
|
});
|
|
3487
3513
|
|
|
3488
|
-
// packages/verify/src/mount-limits.ts
|
|
3489
|
-
function raceMountDeadline(work, deadlineMs, onDeadline) {
|
|
3490
|
-
return Promise.race([
|
|
3491
|
-
work,
|
|
3492
|
-
new Promise((resolve) => {
|
|
3493
|
-
const timer = setTimeout(() => resolve(onDeadline()), deadlineMs);
|
|
3494
|
-
timer.unref();
|
|
3495
|
-
})
|
|
3496
|
-
]);
|
|
3497
|
-
}
|
|
3498
|
-
var MEMORY_CAP_ARG, ENGINE_MOUNT_DEADLINE_MS;
|
|
3499
|
-
var init_mount_limits = __esm({
|
|
3500
|
-
"packages/verify/src/mount-limits.ts"() {
|
|
3501
|
-
"use strict";
|
|
3502
|
-
MEMORY_CAP_ARG = "--js-flags=--max-old-space-size=512";
|
|
3503
|
-
ENGINE_MOUNT_DEADLINE_MS = 3e4;
|
|
3504
|
-
}
|
|
3505
|
-
});
|
|
3506
|
-
|
|
3507
3514
|
// packages/verify/src/admission.ts
|
|
3508
3515
|
import { readFileSync as readFileSync5, readdirSync as readdirSync2, existsSync as existsSync7, writeFileSync as writeFileSync4 } from "node:fs";
|
|
3509
3516
|
import path11 from "node:path";
|
|
@@ -4080,7 +4087,7 @@ async function checkBehaviors(task, bundleDir, opts = {}) {
|
|
|
4080
4087
|
const js = await compileMount(task, bundleDir);
|
|
4081
4088
|
if (typeof js !== "string") return task.behaviors.map((b) => ({ id: b.id, pass: false, detail: js.error }));
|
|
4082
4089
|
const css = ["tokens.css", "styles.css"].map((f) => path13.join(bundleDir, f)).filter((f) => existsSync8(f)).map((f) => readFileSync6(f, "utf8")).join("\n");
|
|
4083
|
-
const server = await chromium3.launchServer({ executablePath: resolveChrome(), headless: true, args:
|
|
4090
|
+
const server = await chromium3.launchServer({ executablePath: resolveChrome(), headless: true, args: mountArgs() });
|
|
4084
4091
|
const browser = await chromium3.connect(server.wsEndpoint());
|
|
4085
4092
|
const results = [];
|
|
4086
4093
|
let deadlined = false;
|
|
@@ -4512,7 +4519,7 @@ if (root && C) createRoot(root).render(createElement(C, cfg.props));
|
|
|
4512
4519
|
}
|
|
4513
4520
|
return detectBackdrop(ref);
|
|
4514
4521
|
};
|
|
4515
|
-
const server = await chromium4.launchServer({ executablePath: resolveChrome(), headless: true, args:
|
|
4522
|
+
const server = await chromium4.launchServer({ executablePath: resolveChrome(), headless: true, args: mountArgs() });
|
|
4516
4523
|
const browser = await chromium4.connect(server.wsEndpoint());
|
|
4517
4524
|
const scores = [];
|
|
4518
4525
|
try {
|
|
@@ -4705,7 +4712,7 @@ async function checkHoverParity(task, bundleDir, opts = {}) {
|
|
|
4705
4712
|
const js = await compileMount(task, bundleDir);
|
|
4706
4713
|
if (typeof js !== "string") return configs.map((c) => ({ id: `parity:${c.rep}`, pass: false, detail: js.error }));
|
|
4707
4714
|
const css = ["tokens.css", "styles.css"].map((f) => path16.join(bundleDir, f)).filter((f) => existsSync11(f)).map((f) => readFileSync9(f, "utf8")).join("\n");
|
|
4708
|
-
const server = await chromium6.launchServer({ executablePath: resolveChrome(), headless: true, args:
|
|
4715
|
+
const server = await chromium6.launchServer({ executablePath: resolveChrome(), headless: true, args: mountArgs() });
|
|
4709
4716
|
const browser = await chromium6.connect(server.wsEndpoint());
|
|
4710
4717
|
const results = [];
|
|
4711
4718
|
try {
|
|
@@ -4879,7 +4886,7 @@ async function checkCropComposition(task, bundleDir, regions, opts = {}) {
|
|
|
4879
4886
|
return regions.map((r) => ({ id: cropId(r), pass: false, similarity: 0, inkRecall: 0, detail: js.error }));
|
|
4880
4887
|
}
|
|
4881
4888
|
const css = ["tokens.css", "styles.css"].map((f) => path17.join(bundleDir, f)).filter((f) => existsSync12(f)).map((f) => readFileSync10(f, "utf8")).join("\n");
|
|
4882
|
-
const server = await chromium7.launchServer({ executablePath: resolveChrome(), headless: true, args:
|
|
4889
|
+
const server = await chromium7.launchServer({ executablePath: resolveChrome(), headless: true, args: mountArgs() });
|
|
4883
4890
|
const browser = await chromium7.connect(server.wsEndpoint());
|
|
4884
4891
|
const PAD = 4;
|
|
4885
4892
|
try {
|
|
@@ -4984,7 +4991,7 @@ async function checkStructuralComposition(task, bundleDir, roles, opts = {}) {
|
|
|
4984
4991
|
const js = await compileInstrumentedMount(task, bundleDir);
|
|
4985
4992
|
if (typeof js !== "string") return [...results, ...mains.map((m) => ({ id: `composition:${m}`, pass: false, detail: js.error }))];
|
|
4986
4993
|
const css = ["tokens.css", "styles.css"].map((f) => path17.join(bundleDir, f)).filter((f) => existsSync12(f)).map((f) => readFileSync10(f, "utf8")).join("\n");
|
|
4987
|
-
const server = await chromium7.launchServer({ executablePath: resolveChrome(), headless: true, args:
|
|
4994
|
+
const server = await chromium7.launchServer({ executablePath: resolveChrome(), headless: true, args: mountArgs() });
|
|
4988
4995
|
const browser = await chromium7.connect(server.wsEndpoint());
|
|
4989
4996
|
try {
|
|
4990
4997
|
for (const mainSlug of mains) {
|
|
@@ -5124,7 +5131,7 @@ async function checkSiblingOcclusion(task, bundleDir, css, opts = {}) {
|
|
|
5124
5131
|
const js = await compileTwoUp(task, bundleDir);
|
|
5125
5132
|
if (typeof js !== "string") return [{ id: "sibling-overlay-hit-testable", pass: false, detail: js.error }];
|
|
5126
5133
|
const timeoutMs = opts.mountDeadlineMs ?? ENGINE_MOUNT_DEADLINE_MS;
|
|
5127
|
-
const server = await chromium8.launchServer({ executablePath: resolveChrome(), headless: true, args:
|
|
5134
|
+
const server = await chromium8.launchServer({ executablePath: resolveChrome(), headless: true, args: mountArgs() });
|
|
5128
5135
|
const browser = await chromium8.connect(server.wsEndpoint());
|
|
5129
5136
|
try {
|
|
5130
5137
|
const work = (async () => {
|
|
@@ -5250,6 +5257,7 @@ var init_src4 = __esm({
|
|
|
5250
5257
|
init_containment();
|
|
5251
5258
|
init_tasks();
|
|
5252
5259
|
init_prelude();
|
|
5260
|
+
init_mount_limits();
|
|
5253
5261
|
init_font_faces();
|
|
5254
5262
|
init_font_resolve();
|
|
5255
5263
|
init_paths();
|
|
@@ -5289,7 +5297,9 @@ function environmentStamp(taskFamilies) {
|
|
|
5289
5297
|
chrome = resolveChrome();
|
|
5290
5298
|
} catch {
|
|
5291
5299
|
}
|
|
5292
|
-
|
|
5300
|
+
const ver = chrome === "unavailable" ? null : chromeVersion();
|
|
5301
|
+
const lcdOff = lcdTextDisabled() ? " (lcd-text-disabled)" : "";
|
|
5302
|
+
return { chrome, chromeVersion: ver === null ? null : ver + lcdOff, fontsManifestSha256: fontsHash };
|
|
5293
5303
|
}
|
|
5294
5304
|
var init_environment = __esm({
|
|
5295
5305
|
"packages/cli/src/environment.ts"() {
|
|
@@ -5532,7 +5542,7 @@ async function runDoctorChecks(options) {
|
|
|
5532
5542
|
);
|
|
5533
5543
|
try {
|
|
5534
5544
|
const chrome = resolveChrome();
|
|
5535
|
-
checks.push({ name: "browser", ok: true, detail: `${chromeVersion() ?? "version unknown"} at ${chrome}` });
|
|
5545
|
+
checks.push({ name: "browser", ok: true, detail: `${chromeVersion() ?? "version unknown"}${lcdTextDisabled() ? " \u2014 EXPERIMENT override active (lcd-text-disabled): scores not comparable to default runs" : ""} at ${chrome}` });
|
|
5536
5546
|
} catch (err) {
|
|
5537
5547
|
checks.push({
|
|
5538
5548
|
name: "browser",
|
|
@@ -8164,7 +8174,7 @@ ${propLines.join("\n")}
|
|
|
8164
8174
|
|
|
8165
8175
|
${syntheticCombos.length > 0 ? `UNRECORDED REACHABLE COMBINATIONS: the design's exclusive axis cannot express ${syntheticCombos.join(", ")} \u2014 the API split makes them reachable with NO recorded truth. Compose them from the recorded per-axis truth (paint variables: one axis sets, the other consumes), never invent a bespoke look, and list them in your report.
|
|
8166
8176
|
` : ""}${slots.length > 0 ? `CONTENT PROPS: every string prop defaults to its RECORDED text \u2014 render the PROP, never a hardcoded literal. Configs pass per-pose recorded strings wherever the recording varies, and pixels enforce them: a hardcoded string fails those configs. Content presence (a heading that only exists in some poses) follows the AXIS props; the string prop only supplies the text. RICH-TEXT DEFAULTS: when the recorded content is a multi-run rich node (mixed weights, an underlined link span) whose concatenation is the prop's default string, a plain-string default would flatten the recorded formatting \u2014 default the parameter to undefined and render the recorded rich markup when the prop is absent; a caller-supplied string then renders plainly. That mirrors the design tool's own emission logic and keeps every recorded pose pixel-exact.
|
|
8167
|
-
` : ""}Rules:
|
|
8177
|
+
` : ""}Rules: generated element ids come from React's useId() \u2014 never Math.random() in render/state init (SSR hydration hazard; measured: two same-day bundles disagreed). Plain CSS in styles.css (tokens.css optional, loaded first) \u2014 no Tailwind, no imports beyond react/react-dom, and NEVER import the stylesheet from the entry module: the harness injects tokens.css and styles.css itself, and an entry that imports CSS does not compile here (measured cost: one full round, every config 0). The component renders the RECORDED content as its defaults \u2014 reproduce it from the emissions. ${forcingCanon}${fontsLine}The host sizes nothing: the component is its natural recorded size. FLUIDITY AFFORDANCE (emit it always): every ROOT width declaration rides width: var(--tendril-root-width, <recorded>px) with that pose's recorded width as the fallback \u2014 per-variant root widths reuse the SAME property name with their own recorded fallbacks. With the property unset this is byte-equivalent truth: identical pixels, identical operability measurement (measured: flipping roots to a bare 100% held 20/20 pixels but made a commit check unmeasurable \u2014 the recorded pin carries real signal). A consumer makes an instance fluid by setting --tendril-root-width (e.g. 100%) on a wrapper, no edit to certified CSS. Widths only: recorded heights and internal geometry stay literal \u2014 fixed heights are often genuine design intent.`;
|
|
8168
8178
|
const mappedTokens = /* @__PURE__ */ new Set([...forcedStates, ...props.filter((p) => p.kind === "boolean").map((p) => p.name)]);
|
|
8169
8179
|
const unmappedInteractionEvidence = interactionEvidence.filter((e) => {
|
|
8170
8180
|
const sel = e.endsWith(" (selection axis)");
|
|
@@ -9471,6 +9481,9 @@ async function runVerify(opts) {
|
|
|
9471
9481
|
remediation: fontsUnprovenRemediation(task.set)
|
|
9472
9482
|
});
|
|
9473
9483
|
}
|
|
9484
|
+
if (lcdTextDisabled()) {
|
|
9485
|
+
warn(opts, "EXPERIMENT override active (TENDRIL_DISABLE_LCD_TEXT=1): text renders greyscale \u2014 scores are NOT comparable to default runs; the environment stamp and report are marked");
|
|
9486
|
+
}
|
|
9474
9487
|
const substitutedFamilies = unprovisionedFamilies(task.set);
|
|
9475
9488
|
if (substitutedFamilies.length > 0) {
|
|
9476
9489
|
warn(opts, `substituted fonts: cache lacks ${substitutedFamilies.map((f) => `"${f}"`).join(", ")} \u2014 scores measure a substitute face and no config can be certified under one (the stamp covers only the provisioned subset); resolve the families to certify`);
|
|
@@ -9591,7 +9604,17 @@ async function runVerify(opts) {
|
|
|
9591
9604
|
// with zero pixel/behavior/composition failures was only
|
|
9592
9605
|
// explainable from stderr prose.
|
|
9593
9606
|
...certBlockedByAbsentInk.length > 0 ? { certBlockedByAbsentInk } : {},
|
|
9594
|
-
...weightGaps.length > 0 ? { fontWeightGaps: weightGaps } : {}
|
|
9607
|
+
...weightGaps.length > 0 ? { fontWeightGaps: weightGaps } : {},
|
|
9608
|
+
...lcdTextDisabled() ? { environmentOverrides: ["disable-lcd-text"] } : {},
|
|
9609
|
+
// Run 13 finding 1: the eye-check nudge lived only in the human
|
|
9610
|
+
// footer, so MCP-driven agents — the documented path — were never
|
|
9611
|
+
// told the check exists and shipped without ever opening a sheet.
|
|
9612
|
+
// Structured here so every consumer sees it.
|
|
9613
|
+
eyeCheck: {
|
|
9614
|
+
command: `tendril inspect "${opts.bundleDir}"`,
|
|
9615
|
+
sheetPath: path31.join(opts.bundleDir, "verify-evidence", "inspect.html"),
|
|
9616
|
+
note: "magnified recorded-vs-rendered crops of every small node; scores cannot see shape. The command WRITES/refreshes the sheet at sheetPath \u2014 re-run it after every verify; an existing sheet may show stale crops."
|
|
9617
|
+
}
|
|
9595
9618
|
};
|
|
9596
9619
|
emitData(opts, report, () => {
|
|
9597
9620
|
for (const s of statuses) {
|
|
@@ -9691,7 +9714,7 @@ ${certified}/${statuses.length} certified \xB7 ${report.coverage.pass}/${statuse
|
|
|
9691
9714
|
}
|
|
9692
9715
|
process.stdout.write(`evidence: ${evidenceDir} (render/ref/diff per config)
|
|
9693
9716
|
`);
|
|
9694
|
-
process.stdout.write(`eye check: tendril inspect ${opts.bundleDir} \u2014 magnified recorded-vs-rendered crops of every small node; scores cannot see shape
|
|
9717
|
+
process.stdout.write(`eye check: tendril inspect "${opts.bundleDir}" \u2014 magnified recorded-vs-rendered crops of every small node; scores cannot see shape (re-run after every verify)
|
|
9695
9718
|
`);
|
|
9696
9719
|
});
|
|
9697
9720
|
if (opts.bar === "cert" && substitutedFamilies.length > 0) {
|
|
@@ -9883,6 +9906,9 @@ async function runEngineScore(opts) {
|
|
|
9883
9906
|
remediation: fontsUnprovenRemediation(task.set)
|
|
9884
9907
|
});
|
|
9885
9908
|
}
|
|
9909
|
+
if (lcdTextDisabled()) {
|
|
9910
|
+
warn(opts, "EXPERIMENT override active (TENDRIL_DISABLE_LCD_TEXT=1): text renders greyscale \u2014 scores are NOT comparable to default runs; the environment stamp and report are marked");
|
|
9911
|
+
}
|
|
9886
9912
|
const substitutedFamilies = unprovisionedFamilies(task.set);
|
|
9887
9913
|
if (substitutedFamilies.length > 0) {
|
|
9888
9914
|
warn(opts, `substituted fonts: cache lacks ${substitutedFamilies.map((f) => `"${f}"`).join(", ")} \u2014 scores measure a substitute face and no config can be certified under one (the stamp covers only the provisioned subset); resolve the families to certify`);
|
|
@@ -9982,6 +10008,7 @@ METRIC DEADBAND (read before iterating on near-misses): the scored similarity/in
|
|
|
9982
10008
|
bundleManifest: emitted.written[0],
|
|
9983
10009
|
note: "styles.css was stamped with the bundle provenance comment on line 1 \u2014 preserve it in any post-score edit",
|
|
9984
10010
|
allPass,
|
|
10011
|
+
...lcdTextDisabled() ? { environmentOverrides: ["disable-lcd-text"] } : {},
|
|
9985
10012
|
// Run 11: generators read allPass:true and reported success on
|
|
9986
10013
|
// bundles verify then FAILED on the interaction-evidence gate —
|
|
9987
10014
|
// the oracle must say what verify will say, including this.
|