@tendrilapp/cli 0.1.17 → 0.1.19
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 +30 -2
- package/dist/tendril-mcp.js +44 -18
- package/dist/tendril.js +1294 -296
- package/package.json +1 -1
package/dist/tendril.js
CHANGED
|
@@ -762,11 +762,13 @@ function resolveAxisDefaultWithRule(domain, recorded, override) {
|
|
|
762
762
|
if (off !== void 0) return { value: off, rule: "boolean-off" };
|
|
763
763
|
}
|
|
764
764
|
const avoid = (v) => {
|
|
765
|
-
const
|
|
766
|
-
return INTERACTION_STATES.has(
|
|
765
|
+
const tokens = kebab(v).split("-");
|
|
766
|
+
return tokens.some((t) => INTERACTION_STATES.has(t) || BOOLEAN_STATES.has(t) || ENGAGED_STATES.has(t));
|
|
767
767
|
};
|
|
768
768
|
const anyAvoided = recorded.some((v) => avoid(v)) && recorded.some((v) => !avoid(v));
|
|
769
|
-
|
|
769
|
+
let candidates = recorded.some((v) => !avoid(v)) ? recorded.filter((v) => !avoid(v)) : recorded;
|
|
770
|
+
const dotted = candidates.filter((v) => v.split(/\s*-\s*|\s+/).some((part) => part.startsWith(".")));
|
|
771
|
+
if (dotted.length > 0) candidates = dotted;
|
|
770
772
|
const counts = /* @__PURE__ */ new Map();
|
|
771
773
|
for (const v of candidates) counts.set(v, (counts.get(v) ?? 0) + 1);
|
|
772
774
|
let best;
|
|
@@ -789,7 +791,7 @@ var init_axis_defaults = __esm({
|
|
|
789
791
|
"use strict";
|
|
790
792
|
INTERACTION_STATES = /* @__PURE__ */ new Set(["hover", "focus", "focus-visible", "active", "pressed"]);
|
|
791
793
|
BOOLEAN_STATES = /* @__PURE__ */ new Set(["disabled", "loading"]);
|
|
792
|
-
ENGAGED_STATES = /* @__PURE__ */ new Set(["selected", "checked", "indeterminate", "open", "expanded"]);
|
|
794
|
+
ENGAGED_STATES = /* @__PURE__ */ new Set(["selected", "checked", "indeterminate", "mixed", "open", "expanded"]);
|
|
793
795
|
INTERACTION_EVIDENCE_VALUES = /* @__PURE__ */ new Set([
|
|
794
796
|
...INTERACTION_STATES,
|
|
795
797
|
...BOOLEAN_STATES,
|
|
@@ -1214,8 +1216,8 @@ var init_src = __esm({
|
|
|
1214
1216
|
function variableNameToPath(name) {
|
|
1215
1217
|
return name.split("/").map(canonicalCssIdentPart).filter((part) => part.length > 0);
|
|
1216
1218
|
}
|
|
1217
|
-
function tokenPathToCssVar(
|
|
1218
|
-
return `--${
|
|
1219
|
+
function tokenPathToCssVar(path38) {
|
|
1220
|
+
return `--${path38.join("-")}`;
|
|
1219
1221
|
}
|
|
1220
1222
|
function toDtcgToken(variable, defaultMode) {
|
|
1221
1223
|
const modes = Object.keys(variable.valuesByMode);
|
|
@@ -1259,11 +1261,11 @@ function toDtcgToken(variable, defaultMode) {
|
|
|
1259
1261
|
}
|
|
1260
1262
|
function mapVariablesToDtcg(variables, defaultMode = "light") {
|
|
1261
1263
|
const entries = variables.map((variable) => {
|
|
1262
|
-
const
|
|
1263
|
-
if (
|
|
1264
|
+
const path38 = variableNameToPath(variable.name);
|
|
1265
|
+
if (path38.length === 0) {
|
|
1264
1266
|
throw new DtcgMappingError("empty-name", `Figma variable ${variable.id} has an empty name`);
|
|
1265
1267
|
}
|
|
1266
|
-
return { variable, path:
|
|
1268
|
+
return { variable, path: path38 };
|
|
1267
1269
|
});
|
|
1268
1270
|
const groupPrefixes = /* @__PURE__ */ new Set();
|
|
1269
1271
|
for (const e of entries) {
|
|
@@ -1284,21 +1286,21 @@ function mapVariablesToDtcg(variables, defaultMode = "light") {
|
|
|
1284
1286
|
}
|
|
1285
1287
|
const tokens = {};
|
|
1286
1288
|
const flat = [];
|
|
1287
|
-
for (const { variable, path:
|
|
1289
|
+
for (const { variable, path: path38 } of entries) {
|
|
1288
1290
|
const token = toDtcgToken(variable, defaultMode);
|
|
1289
1291
|
let group = tokens;
|
|
1290
|
-
for (const segment of
|
|
1292
|
+
for (const segment of path38.slice(0, -1)) {
|
|
1291
1293
|
const existing = group[segment];
|
|
1292
1294
|
group = existing ?? (group[segment] = {});
|
|
1293
1295
|
}
|
|
1294
|
-
const leaf =
|
|
1296
|
+
const leaf = path38[path38.length - 1];
|
|
1295
1297
|
if (group[leaf] !== void 0) {
|
|
1296
|
-
throw new DtcgMappingError("duplicate-path", `Duplicate token path "${
|
|
1298
|
+
throw new DtcgMappingError("duplicate-path", `Duplicate token path "${path38.join(".")}" (variable ${variable.id})`);
|
|
1297
1299
|
}
|
|
1298
1300
|
group[leaf] = token;
|
|
1299
1301
|
flat.push({
|
|
1300
|
-
path:
|
|
1301
|
-
cssVar: tokenPathToCssVar(
|
|
1302
|
+
path: path38.join("."),
|
|
1303
|
+
cssVar: tokenPathToCssVar(path38),
|
|
1302
1304
|
type: token.$type,
|
|
1303
1305
|
value: token.$value
|
|
1304
1306
|
});
|
|
@@ -1487,9 +1489,9 @@ function boundId(value) {
|
|
|
1487
1489
|
return isObject(value) && typeof value["boundVariableId"] === "string" ? value["boundVariableId"] : void 0;
|
|
1488
1490
|
}
|
|
1489
1491
|
function resolveBinding(ctx, id) {
|
|
1490
|
-
const
|
|
1491
|
-
if (
|
|
1492
|
-
return
|
|
1492
|
+
const path38 = ctx.pathById.get(id);
|
|
1493
|
+
if (path38 === void 0) ctx.unresolved.add(id);
|
|
1494
|
+
return path38;
|
|
1493
1495
|
}
|
|
1494
1496
|
function parseVariantProps(name) {
|
|
1495
1497
|
if (!name.includes("=")) return void 0;
|
|
@@ -1524,8 +1526,8 @@ function walk(ctx, raw) {
|
|
|
1524
1526
|
if (!isObject(paint) || paint["visible"] === false) continue;
|
|
1525
1527
|
const id = boundId(paint);
|
|
1526
1528
|
if (id !== void 0) {
|
|
1527
|
-
const
|
|
1528
|
-
if (
|
|
1529
|
+
const path38 = resolveBinding(ctx, id);
|
|
1530
|
+
if (path38 !== void 0) tokens.add(path38);
|
|
1529
1531
|
} else if (typeof paint["color"] === "string") {
|
|
1530
1532
|
ctx.hardcoded.push({ node: name, property, value: paint["color"] });
|
|
1531
1533
|
}
|
|
@@ -1533,8 +1535,8 @@ function walk(ctx, raw) {
|
|
|
1533
1535
|
}
|
|
1534
1536
|
const radiusId = boundId(raw["cornerRadius"]);
|
|
1535
1537
|
if (radiusId !== void 0) {
|
|
1536
|
-
const
|
|
1537
|
-
if (
|
|
1538
|
+
const path38 = resolveBinding(ctx, radiusId);
|
|
1539
|
+
if (path38 !== void 0) tokens.add(path38);
|
|
1538
1540
|
} else if (typeof raw["cornerRadius"] === "number" && raw["cornerRadius"] !== 0) {
|
|
1539
1541
|
ctx.hardcoded.push({ node: name, property: "border-radius", value: `${raw["cornerRadius"]}px` });
|
|
1540
1542
|
}
|
|
@@ -1544,10 +1546,10 @@ function walk(ctx, raw) {
|
|
|
1544
1546
|
layout = { mode: layoutMode === "HORIZONTAL" ? "flex-row" : "flex-column" };
|
|
1545
1547
|
const gapId = boundId(raw["itemSpacing"]);
|
|
1546
1548
|
if (gapId !== void 0) {
|
|
1547
|
-
const
|
|
1548
|
-
if (
|
|
1549
|
-
layout.gap =
|
|
1550
|
-
tokens.add(
|
|
1549
|
+
const path38 = resolveBinding(ctx, gapId);
|
|
1550
|
+
if (path38 !== void 0) {
|
|
1551
|
+
layout.gap = path38;
|
|
1552
|
+
tokens.add(path38);
|
|
1551
1553
|
}
|
|
1552
1554
|
} else if (typeof raw["itemSpacing"] === "number" && raw["itemSpacing"] !== 0) {
|
|
1553
1555
|
ctx.hardcoded.push({ node: name, property: "gap", value: `${raw["itemSpacing"]}px` });
|
|
@@ -1556,10 +1558,10 @@ function walk(ctx, raw) {
|
|
|
1556
1558
|
for (const field of PADDING_FIELDS) {
|
|
1557
1559
|
const id = boundId(raw[field]);
|
|
1558
1560
|
if (id !== void 0) {
|
|
1559
|
-
const
|
|
1560
|
-
if (
|
|
1561
|
-
paddingPaths.push(
|
|
1562
|
-
tokens.add(
|
|
1561
|
+
const path38 = resolveBinding(ctx, id);
|
|
1562
|
+
if (path38 !== void 0) {
|
|
1563
|
+
paddingPaths.push(path38);
|
|
1564
|
+
tokens.add(path38);
|
|
1563
1565
|
}
|
|
1564
1566
|
} else if (typeof raw[field] === "number" && raw[field] !== 0) {
|
|
1565
1567
|
ctx.hardcoded.push({ node: name, property: field, value: `${raw[field]}px` });
|
|
@@ -1968,13 +1970,13 @@ var init_tsc_check = __esm({
|
|
|
1968
1970
|
// packages/verify/src/token-lint.ts
|
|
1969
1971
|
import strictValue from "stylelint-declaration-strict-value";
|
|
1970
1972
|
import stylelint from "stylelint";
|
|
1971
|
-
async function runTokenLint(css, fileLabel = "generated.css", definedVars2) {
|
|
1973
|
+
async function runTokenLint(css, fileLabel = "generated.css", definedVars2, recordedMapEmpty = false) {
|
|
1972
1974
|
const result = await stylelint.lint({
|
|
1973
1975
|
code: css,
|
|
1974
1976
|
codeFilename: fileLabel,
|
|
1975
1977
|
config: STYLELINT_CONFIG
|
|
1976
1978
|
});
|
|
1977
|
-
const mapKnownEmpty = definedVars2 !== void 0 && definedVars2.size === 0;
|
|
1979
|
+
const mapKnownEmpty = recordedMapEmpty || definedVars2 !== void 0 && definedVars2.size === 0;
|
|
1978
1980
|
const violations = result.results.flatMap(
|
|
1979
1981
|
(r) => r.warnings.filter((w) => !(mapKnownEmpty && w.rule === "scale-unlimited/declaration-strict-value")).map((w) => ({
|
|
1980
1982
|
file: fileLabel,
|
|
@@ -3409,7 +3411,29 @@ function resolvedFontFamilies(cacheDir = DEFAULT_FONT_CACHE) {
|
|
|
3409
3411
|
function requiredFontsManifest(families, cacheDir = DEFAULT_FONT_CACHE) {
|
|
3410
3412
|
const mPath = path9.join(cacheDir, "manifest.json");
|
|
3411
3413
|
const manifest = existsSync5(mPath) ? JSON.parse(readFileSync3(mPath, "utf8")) : [];
|
|
3412
|
-
|
|
3414
|
+
const wanted = new Set(families.map((f) => f.toLowerCase()));
|
|
3415
|
+
return manifest.filter((f) => wanted.has(f.family.toLowerCase()));
|
|
3416
|
+
}
|
|
3417
|
+
function verifiedFontFamilies(cacheDir = DEFAULT_FONT_CACHE) {
|
|
3418
|
+
const mPath = path9.join(cacheDir, "manifest.json");
|
|
3419
|
+
if (!existsSync5(mPath)) return [];
|
|
3420
|
+
let entries;
|
|
3421
|
+
try {
|
|
3422
|
+
entries = JSON.parse(readFileSync3(mPath, "utf8"));
|
|
3423
|
+
} catch {
|
|
3424
|
+
return [];
|
|
3425
|
+
}
|
|
3426
|
+
const byFamily = /* @__PURE__ */ new Map();
|
|
3427
|
+
for (const e of entries) {
|
|
3428
|
+
if (typeof e.family !== "string" || typeof e.weight !== "number" || typeof e.file !== "string") continue;
|
|
3429
|
+
const file = path9.isAbsolute(e.file) && existsSync5(e.file) ? e.file : path9.resolve(cacheDir, path9.basename(e.file));
|
|
3430
|
+
if (!existsSync5(file)) continue;
|
|
3431
|
+
if (createHash("sha256").update(readFileSync3(file)).digest("hex") !== e.sha256) continue;
|
|
3432
|
+
const set = byFamily.get(e.family) ?? /* @__PURE__ */ new Set();
|
|
3433
|
+
set.add(e.weight);
|
|
3434
|
+
byFamily.set(e.family, set);
|
|
3435
|
+
}
|
|
3436
|
+
return [...byFamily.entries()].map(([family, w]) => ({ family, weights: [...w].sort((a, b) => a - b) }));
|
|
3413
3437
|
}
|
|
3414
3438
|
var DEFAULT_FONT_CACHE, UA;
|
|
3415
3439
|
var init_font_resolve = __esm({
|
|
@@ -3421,11 +3445,21 @@ var init_font_resolve = __esm({
|
|
|
3421
3445
|
});
|
|
3422
3446
|
|
|
3423
3447
|
// packages/verify/src/font-faces.ts
|
|
3448
|
+
import { createHash as createHash2 } from "node:crypto";
|
|
3424
3449
|
import { existsSync as existsSync6, readFileSync as readFileSync4 } from "node:fs";
|
|
3425
3450
|
import path10 from "node:path";
|
|
3426
3451
|
function fontFaceCss(manifestPath2 = path10.join(fontCacheDir(), "manifest.json")) {
|
|
3427
3452
|
if (!existsSync6(manifestPath2)) return "";
|
|
3428
|
-
const
|
|
3453
|
+
const claimed = JSON.parse(readFileSync4(manifestPath2, "utf8"));
|
|
3454
|
+
const resolveEntry = (f) => {
|
|
3455
|
+
if (path10.isAbsolute(f) && existsSync6(f)) return f;
|
|
3456
|
+
return path10.resolve(path10.dirname(manifestPath2), path10.basename(f));
|
|
3457
|
+
};
|
|
3458
|
+
const manifest = claimed.filter((f) => {
|
|
3459
|
+
const file = resolveEntry(f.file);
|
|
3460
|
+
if (!existsSync6(file)) return false;
|
|
3461
|
+
return createHash2("sha256").update(readFileSync4(file)).digest("hex") === f.sha256;
|
|
3462
|
+
});
|
|
3429
3463
|
const resolveFile = (f) => {
|
|
3430
3464
|
if (path10.isAbsolute(f) && existsSync6(f)) return f;
|
|
3431
3465
|
return path10.resolve(path10.dirname(manifestPath2), path10.basename(f));
|
|
@@ -4196,7 +4230,28 @@ function definedVars(tokensCss) {
|
|
|
4196
4230
|
for (const m of tokensCss.matchAll(/(--[\w-]+)\s*:/g)) names.add(m[1]);
|
|
4197
4231
|
return names;
|
|
4198
4232
|
}
|
|
4199
|
-
|
|
4233
|
+
function recordedTokenMapEmpty(setDir, reps) {
|
|
4234
|
+
const readMap = (file) => {
|
|
4235
|
+
if (!existsSync9(file)) return void 0;
|
|
4236
|
+
try {
|
|
4237
|
+
const parsed = JSON.parse(envelopeFirstTextPart(JSON.parse(readFileSync7(file, "utf8"))) || "{}");
|
|
4238
|
+
return parsed !== null && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : {};
|
|
4239
|
+
} catch {
|
|
4240
|
+
return {};
|
|
4241
|
+
}
|
|
4242
|
+
};
|
|
4243
|
+
const setLevel = readMap(path14.join(setDir, "get_variable_defs.json"));
|
|
4244
|
+
if (setLevel !== void 0) return Object.keys(setLevel).length === 0;
|
|
4245
|
+
let recorded = false;
|
|
4246
|
+
for (const rep of reps) {
|
|
4247
|
+
const m = readMap(path14.join(setDir, rep, "get_variable_defs.json"));
|
|
4248
|
+
if (m === void 0) continue;
|
|
4249
|
+
recorded = true;
|
|
4250
|
+
if (Object.keys(m).length > 0) return false;
|
|
4251
|
+
}
|
|
4252
|
+
return recorded;
|
|
4253
|
+
}
|
|
4254
|
+
async function checkBundleQuality(bundleDir, entry, set) {
|
|
4200
4255
|
const findings = [];
|
|
4201
4256
|
const entryPath = path14.join(bundleDir, entry);
|
|
4202
4257
|
const cssPath = path14.join(bundleDir, "styles.css");
|
|
@@ -4216,7 +4271,8 @@ async function checkBundleQuality(bundleDir, entry) {
|
|
|
4216
4271
|
}
|
|
4217
4272
|
}
|
|
4218
4273
|
if (css !== "") {
|
|
4219
|
-
|
|
4274
|
+
const recordedEmpty = set !== void 0 && recordedTokenMapEmpty(set.dir, set.reps);
|
|
4275
|
+
for (const v of (await runTokenLint(css, "styles.css", definedVars(tokensCss), recordedEmpty)).violations) {
|
|
4220
4276
|
findings.push({ kind: "token-lint", file: "styles.css", line: v.line, message: v.message });
|
|
4221
4277
|
}
|
|
4222
4278
|
}
|
|
@@ -4225,6 +4281,7 @@ async function checkBundleQuality(bundleDir, entry) {
|
|
|
4225
4281
|
var init_bundle_quality = __esm({
|
|
4226
4282
|
"packages/verify/src/bundle-quality.ts"() {
|
|
4227
4283
|
"use strict";
|
|
4284
|
+
init_src();
|
|
4228
4285
|
init_runtime();
|
|
4229
4286
|
init_tsc_check();
|
|
4230
4287
|
init_token_lint();
|
|
@@ -4323,14 +4380,17 @@ function chooseSeat(cropAt, ref, padW, padH, backdrop, origin, extents) {
|
|
|
4323
4380
|
}
|
|
4324
4381
|
return { comparison, seat, scored: cropAt(origin - seat.x, origin - seat.y) };
|
|
4325
4382
|
}
|
|
4326
|
-
function
|
|
4327
|
-
let root;
|
|
4383
|
+
function metadataRoot(set, rep) {
|
|
4328
4384
|
try {
|
|
4329
4385
|
const text = JSON.parse(readFileSync8(path15.join(set, rep, "get_metadata.json"), "utf8")).content.map((c) => c.text ?? "").join("\n");
|
|
4330
|
-
|
|
4386
|
+
return parseMetadataStructure(text);
|
|
4331
4387
|
} catch {
|
|
4332
4388
|
return void 0;
|
|
4333
4389
|
}
|
|
4390
|
+
}
|
|
4391
|
+
function deepestNodeNameAt(set, rep, px, py) {
|
|
4392
|
+
const root = metadataRoot(set, rep);
|
|
4393
|
+
if (root === void 0) return void 0;
|
|
4334
4394
|
let best;
|
|
4335
4395
|
const walk2 = (node, ox, oy, isRoot) => {
|
|
4336
4396
|
if (node.hidden === true) return;
|
|
@@ -4346,7 +4406,35 @@ function deepestNodeNameAt(set, rep, px, py) {
|
|
|
4346
4406
|
for (const child of node.children) walk2(child, nx, ny, false);
|
|
4347
4407
|
};
|
|
4348
4408
|
walk2(root, 0, 0, true);
|
|
4349
|
-
return best
|
|
4409
|
+
if (best !== void 0) return best.name;
|
|
4410
|
+
if (root.name !== "" && px >= 0 && py >= 0 && px < (root.width ?? 0) && py < (root.height ?? 0)) {
|
|
4411
|
+
return `${root.name} (${root.id}) (edge region, no named child at this point)`;
|
|
4412
|
+
}
|
|
4413
|
+
return void 0;
|
|
4414
|
+
}
|
|
4415
|
+
function smallSemanticNodes(set, rep, maxArea = 1024) {
|
|
4416
|
+
const root = metadataRoot(set, rep);
|
|
4417
|
+
if (root === void 0) return [];
|
|
4418
|
+
const found = [];
|
|
4419
|
+
const walk2 = (node, ox, oy, isRoot) => {
|
|
4420
|
+
if (node.hidden === true) return;
|
|
4421
|
+
const nx = isRoot ? 0 : ox + (node.x ?? 0);
|
|
4422
|
+
const ny = isRoot ? 0 : oy + (node.y ?? 0);
|
|
4423
|
+
const w = node.width ?? 0;
|
|
4424
|
+
const h = node.height ?? 0;
|
|
4425
|
+
if (!isRoot && node.name !== "" && w >= 2 && h >= 2 && w * h <= maxArea) {
|
|
4426
|
+
found.push({ name: node.name, id: node.id, x: nx, y: ny, w, h });
|
|
4427
|
+
}
|
|
4428
|
+
for (const child of node.children) walk2(child, nx, ny, false);
|
|
4429
|
+
};
|
|
4430
|
+
walk2(root, 0, 0, true);
|
|
4431
|
+
const seen = /* @__PURE__ */ new Set();
|
|
4432
|
+
return found.sort((a, b) => a.w * a.h - b.w * b.h).filter((n) => {
|
|
4433
|
+
const key = `${n.x}:${n.y}:${n.w}:${n.h}`;
|
|
4434
|
+
if (seen.has(key)) return false;
|
|
4435
|
+
seen.add(key);
|
|
4436
|
+
return true;
|
|
4437
|
+
});
|
|
4350
4438
|
}
|
|
4351
4439
|
function repMeta(set, rep) {
|
|
4352
4440
|
const text = JSON.parse(readFileSync8(path15.join(set, rep, "get_metadata.json"), "utf8")).content.map((c) => c.text ?? "").join("\n");
|
|
@@ -5174,7 +5262,7 @@ var init_src4 = __esm({
|
|
|
5174
5262
|
// packages/cli/src/environment.ts
|
|
5175
5263
|
import { existsSync as existsSync14, readFileSync as readFileSync11 } from "node:fs";
|
|
5176
5264
|
import path19 from "node:path";
|
|
5177
|
-
import { createHash as
|
|
5265
|
+
import { createHash as createHash3 } from "node:crypto";
|
|
5178
5266
|
import { fileURLToPath as fileURLToPath4 } from "node:url";
|
|
5179
5267
|
function cliVersion() {
|
|
5180
5268
|
try {
|
|
@@ -5191,7 +5279,7 @@ function environmentStamp(taskFamilies) {
|
|
|
5191
5279
|
const entries = JSON.parse(readFileSync11(manifestPath2, "utf8"));
|
|
5192
5280
|
const scoped = taskFamilies === void 0 || taskFamilies === null ? entries : entries.filter((f) => taskFamilies.some((fam) => fam.toLowerCase() === f.family.toLowerCase()));
|
|
5193
5281
|
const faces = scoped.map((f) => `${f.family}:${f.weight}:${f.sha256}`).sort();
|
|
5194
|
-
fontsHash = faces.length === 0 ? null :
|
|
5282
|
+
fontsHash = faces.length === 0 ? null : createHash3("sha256").update(faces.join("\n")).digest("hex").slice(0, 16);
|
|
5195
5283
|
} catch {
|
|
5196
5284
|
fontsHash = null;
|
|
5197
5285
|
}
|
|
@@ -5210,6 +5298,25 @@ var init_environment = __esm({
|
|
|
5210
5298
|
}
|
|
5211
5299
|
});
|
|
5212
5300
|
|
|
5301
|
+
// packages/cli/src/describe.ts
|
|
5302
|
+
function printDescription(description) {
|
|
5303
|
+
process.stdout.write(`${JSON.stringify(description, null, 2)}
|
|
5304
|
+
`);
|
|
5305
|
+
}
|
|
5306
|
+
var COMMON_EXIT_CODES;
|
|
5307
|
+
var init_describe = __esm({
|
|
5308
|
+
"packages/cli/src/describe.ts"() {
|
|
5309
|
+
"use strict";
|
|
5310
|
+
COMMON_EXIT_CODES = {
|
|
5311
|
+
0: "success",
|
|
5312
|
+
1: "general error",
|
|
5313
|
+
2: "authentication error",
|
|
5314
|
+
3: "input validation error",
|
|
5315
|
+
4: "confirmation required (re-run with --yes or answer the prompt)"
|
|
5316
|
+
};
|
|
5317
|
+
}
|
|
5318
|
+
});
|
|
5319
|
+
|
|
5213
5320
|
// packages/cli/src/env.ts
|
|
5214
5321
|
import { existsSync as existsSync15, readFileSync as readFileSync12 } from "node:fs";
|
|
5215
5322
|
import path20 from "node:path";
|
|
@@ -5386,6 +5493,211 @@ var init_entitlement = __esm({
|
|
|
5386
5493
|
}
|
|
5387
5494
|
});
|
|
5388
5495
|
|
|
5496
|
+
// packages/cli/src/commands/doctor.ts
|
|
5497
|
+
import { spawnSync } from "node:child_process";
|
|
5498
|
+
import { existsSync as existsSync17, readFileSync as readFileSync14, readdirSync as readdirSync3 } from "node:fs";
|
|
5499
|
+
import os4 from "node:os";
|
|
5500
|
+
import path22 from "node:path";
|
|
5501
|
+
async function runDoctorChecks(options) {
|
|
5502
|
+
const checks = [];
|
|
5503
|
+
const mcpUrl = options.mcpUrl ?? DEFAULT_MCP_URL;
|
|
5504
|
+
const client = new McpHttpClient({
|
|
5505
|
+
url: mcpUrl,
|
|
5506
|
+
...options.fetchImpl ? { fetchImpl: options.fetchImpl } : {}
|
|
5507
|
+
});
|
|
5508
|
+
try {
|
|
5509
|
+
const info = await client.initialize();
|
|
5510
|
+
const tools = await client.listTools();
|
|
5511
|
+
checks.push({
|
|
5512
|
+
name: "figma-desktop-mcp",
|
|
5513
|
+
ok: true,
|
|
5514
|
+
detail: `${info.name} ${info.version} (protocol ${info.protocolVersion}), ${tools.length} tools: ${tools.map((t) => t.name).join(", ")}`
|
|
5515
|
+
});
|
|
5516
|
+
} catch (err) {
|
|
5517
|
+
checks.push({
|
|
5518
|
+
name: "figma-desktop-mcp",
|
|
5519
|
+
ok: false,
|
|
5520
|
+
detail: err instanceof Error ? err.message : String(err),
|
|
5521
|
+
remediation: err instanceof McpUnavailableError ? "Open the Figma desktop app, sign in, then Figma menu \u2192 Preferences \u2192 Enable local MCP server (Dev or Full seat on a paid plan)." : "The server answered but the exchange failed \u2014 check the signed-in account's seat, then retry."
|
|
5522
|
+
});
|
|
5523
|
+
}
|
|
5524
|
+
const openrouterKey = resolveCredential("OPENROUTER_API_KEY");
|
|
5525
|
+
checks.push(
|
|
5526
|
+
openrouterKey ? { name: "openrouter-key", ok: true, detail: "OPENROUTER_API_KEY configured" } : {
|
|
5527
|
+
name: "openrouter-key",
|
|
5528
|
+
ok: false,
|
|
5529
|
+
detail: "OPENROUTER_API_KEY not set \u2014 optional; only the curated API-model engine needs it",
|
|
5530
|
+
remediation: "Run `tendril init` to store your OpenRouter key (BYOK) if you plan to use the curated engine."
|
|
5531
|
+
}
|
|
5532
|
+
);
|
|
5533
|
+
try {
|
|
5534
|
+
const chrome = resolveChrome();
|
|
5535
|
+
checks.push({ name: "browser", ok: true, detail: `${chromeVersion() ?? "version unknown"} at ${chrome}` });
|
|
5536
|
+
} catch (err) {
|
|
5537
|
+
checks.push({
|
|
5538
|
+
name: "browser",
|
|
5539
|
+
ok: false,
|
|
5540
|
+
detail: err instanceof Error ? err.message : String(err),
|
|
5541
|
+
remediation: "Install Google Chrome (or Chromium), or set CHROME_PATH to a Chromium-family browser binary."
|
|
5542
|
+
});
|
|
5543
|
+
}
|
|
5544
|
+
const fontManifest = path22.join(fontCacheDir(), "manifest.json");
|
|
5545
|
+
checks.push(
|
|
5546
|
+
existsSync17(fontManifest) ? { name: "font-cache", ok: true, detail: `resolved font cache at ${fontCacheDir()} (${JSON.parse(readFileSync14(fontManifest, "utf8")).length} faces)` } : {
|
|
5547
|
+
name: "font-cache",
|
|
5548
|
+
ok: true,
|
|
5549
|
+
detail: `font cache empty at ${fontCacheDir()} \u2014 normal on a fresh machine; faces resolve per design system at first use`,
|
|
5550
|
+
remediation: "Nothing to do now: `tendril fonts resolve --set <recording-dir>` fetches exactly what a recording declares, and generate/verify name that command when they need it."
|
|
5551
|
+
}
|
|
5552
|
+
);
|
|
5553
|
+
const pluginRoot = path22.join(os4.homedir(), ".claude", "plugins", "cache", "tendrilapp", "tendril");
|
|
5554
|
+
if (existsSync17(pluginRoot)) {
|
|
5555
|
+
try {
|
|
5556
|
+
const versions = readdirSync3(pluginRoot).filter((v) => /^\d+\.\d+\.\d+$/.test(v));
|
|
5557
|
+
const newest = versions.sort((a, b) => versionIsNewer(a, b) ? 1 : -1)[0];
|
|
5558
|
+
if (newest !== void 0) {
|
|
5559
|
+
const skewed = versionIsNewer(newest, cliVersion());
|
|
5560
|
+
checks.push(
|
|
5561
|
+
skewed ? {
|
|
5562
|
+
name: "plugin-skew",
|
|
5563
|
+
ok: false,
|
|
5564
|
+
detail: `Claude Code plugin cache holds v${newest} while this CLI is ${cliVersion()} \u2014 the plugin's skill/agents are STALE and will silently shadow current doctrine`,
|
|
5565
|
+
remediation: "Update the plugin \u2014 REFRESH THE MARKETPLACE FIRST (its local clone goes stale and a reinstall faithfully reinstalls the old version): terminal Claude Code \u2014 `claude plugin marketplace update tendrilapp` then `claude plugin update tendril`; VS Code extension \u2014 /plugins panel \u2192 Marketplaces tab \u2192 refresh tendrilapp, THEN Plugins tab \u2192 uninstall + reinstall tendril, reopen the chat panel. If the refresh doesn't take, remove the tendrilapp marketplace entirely and re-add TendrilApp/claude-plugin (a fresh clone cannot be stale)."
|
|
5566
|
+
} : { name: "plugin-skew", ok: true, detail: `Claude Code plugin v${newest} matches this CLI` }
|
|
5567
|
+
);
|
|
5568
|
+
}
|
|
5569
|
+
} catch {
|
|
5570
|
+
}
|
|
5571
|
+
}
|
|
5572
|
+
const pathBinary = cliVersion() === "0.0.0" ? null : findPathTendril(process.env["PATH"] ?? "", process.platform);
|
|
5573
|
+
if (pathBinary !== null) {
|
|
5574
|
+
const reported = probeVersion(pathBinary);
|
|
5575
|
+
if (reported !== null) {
|
|
5576
|
+
checks.push(
|
|
5577
|
+
reported === cliVersion() ? { name: "path-skew", ok: true, detail: `tendril on PATH (${pathBinary}) is this CLI (${reported})` } : {
|
|
5578
|
+
name: "path-skew",
|
|
5579
|
+
ok: false,
|
|
5580
|
+
detail: `tendril on PATH (${pathBinary}) reports ${reported} while this CLI is ${cliVersion()} \u2014 a stale global install silently shadows the current release in terminals`,
|
|
5581
|
+
remediation: "npm rm -g tendrilapp (or npm install -g @tendrilapp/cli@latest) so the PATH binary matches; plugin/npx sessions already run the current release."
|
|
5582
|
+
}
|
|
5583
|
+
);
|
|
5584
|
+
}
|
|
5585
|
+
}
|
|
5586
|
+
const ent = checkEntitlement();
|
|
5587
|
+
checks.push(
|
|
5588
|
+
ent.ok ? ent.mode === "pre-launch" ? { name: "entitlement", ok: true, detail: "pre-launch build \u2014 entitlement gates present but unarmed (no public key shipped); record/generate run freely" } : { name: "entitlement", ok: true, detail: `active plan "${ent.claims.plan}" until ${new Date(ent.claims.exp).toISOString().slice(0, 10)}${ent.stale ? " (stale \u2014 renews at next opportunity)" : ""}` } : { name: "entitlement", ok: false, detail: ent.error, remediation: ent.remediation }
|
|
5589
|
+
);
|
|
5590
|
+
const figmaToken = resolveCredential("FIGMA_TOKEN");
|
|
5591
|
+
checks.push({
|
|
5592
|
+
name: "figma-pat",
|
|
5593
|
+
ok: true,
|
|
5594
|
+
detail: figmaToken ? "FIGMA_TOKEN configured (REST fallback available)" : "FIGMA_TOKEN not set \u2014 optional; only needed for the REST fallback transport"
|
|
5595
|
+
});
|
|
5596
|
+
return { ok: checks.filter((c) => c.name !== "figma-pat" && c.name !== "openrouter-key" && c.name !== "path-skew").every((c) => c.ok), checks };
|
|
5597
|
+
}
|
|
5598
|
+
function findPathTendril(pathEnv, platform) {
|
|
5599
|
+
const dirs = pathEnv.split(path22.delimiter).filter((d) => d !== "" && !/node_modules[\\/]\.bin/.test(d) && !/[\\/]_npx[\\/]/.test(d));
|
|
5600
|
+
const names = platform === "win32" ? ["tendril.cmd", "tendril.bat"] : ["tendril"];
|
|
5601
|
+
for (const dir of dirs) {
|
|
5602
|
+
for (const name of names) {
|
|
5603
|
+
const candidate = path22.join(dir, name);
|
|
5604
|
+
if (existsSync17(candidate)) return candidate;
|
|
5605
|
+
}
|
|
5606
|
+
}
|
|
5607
|
+
return null;
|
|
5608
|
+
}
|
|
5609
|
+
function probeVersion(binary) {
|
|
5610
|
+
const windowsShim = /\.(cmd|bat)$/i.test(binary);
|
|
5611
|
+
const res = windowsShim ? spawnSync(`"${binary}" --version`, { shell: true, timeout: 5e3, encoding: "utf8" }) : spawnSync(binary, ["--version"], { timeout: 5e3, encoding: "utf8" });
|
|
5612
|
+
if (res.error !== void 0 || res.status !== 0) return null;
|
|
5613
|
+
const m = /^(\d+\.\d+\.\d+)\s*$/m.exec(res.stdout ?? "");
|
|
5614
|
+
return m === null ? null : m[1];
|
|
5615
|
+
}
|
|
5616
|
+
function versionIsNewer(a, b) {
|
|
5617
|
+
const pa = a.split(".").map(Number);
|
|
5618
|
+
const pb = b.split(".").map(Number);
|
|
5619
|
+
for (let i = 0; i < 3; i++) {
|
|
5620
|
+
if ((pb[i] ?? 0) > (pa[i] ?? 0)) return true;
|
|
5621
|
+
if ((pb[i] ?? 0) < (pa[i] ?? 0)) return false;
|
|
5622
|
+
}
|
|
5623
|
+
return false;
|
|
5624
|
+
}
|
|
5625
|
+
async function latestVersionInfo() {
|
|
5626
|
+
try {
|
|
5627
|
+
const ctl = new AbortController();
|
|
5628
|
+
const timer = setTimeout(() => ctl.abort(), 2500);
|
|
5629
|
+
const res = await fetch("https://registry.npmjs.org/@tendrilapp%2fcli", { signal: ctl.signal });
|
|
5630
|
+
clearTimeout(timer);
|
|
5631
|
+
if (!res.ok) return null;
|
|
5632
|
+
const doc = await res.json();
|
|
5633
|
+
const latest = doc["dist-tags"]?.latest;
|
|
5634
|
+
if (latest === void 0) return null;
|
|
5635
|
+
const stamp = doc.time?.[latest];
|
|
5636
|
+
return { latest, ...stamp !== void 0 ? { publishedAt: stamp.slice(0, 10) } : {} };
|
|
5637
|
+
} catch {
|
|
5638
|
+
return null;
|
|
5639
|
+
}
|
|
5640
|
+
}
|
|
5641
|
+
async function runDoctor(flags) {
|
|
5642
|
+
if (flags.describe) {
|
|
5643
|
+
printDescription(DOCTOR_DESCRIPTION);
|
|
5644
|
+
return;
|
|
5645
|
+
}
|
|
5646
|
+
const [report, latest] = await Promise.all([runDoctorChecks({ ...flags.mcpUrl ? { mcpUrl: flags.mcpUrl } : {} }), latestVersionInfo()]);
|
|
5647
|
+
emitData(flags, { version: cliVersion(), ...latest !== null ? { latest: latest.latest, latestPublishedAt: latest.publishedAt ?? null, updateAvailable: versionIsNewer(cliVersion(), latest.latest) } : {}, ...report }, () => {
|
|
5648
|
+
const version = cliVersion();
|
|
5649
|
+
if (latest === null) {
|
|
5650
|
+
process.stdout.write(`tendril ${version} (latest: unreachable)
|
|
5651
|
+
`);
|
|
5652
|
+
} else if (!versionIsNewer(version, latest.latest)) {
|
|
5653
|
+
process.stdout.write(`tendril ${version} (${latest.latest === version ? "latest" : `ahead of registry ${latest.latest}`}${latest.publishedAt !== void 0 ? `, published ${latest.publishedAt}` : ""})
|
|
5654
|
+
`);
|
|
5655
|
+
} else {
|
|
5656
|
+
process.stdout.write(`tendril ${version} \u2014 UPDATE AVAILABLE: ${latest.latest}${latest.publishedAt !== void 0 ? ` (published ${latest.publishedAt})` : ""}
|
|
5657
|
+
`);
|
|
5658
|
+
process.stdout.write(` \u2192 npm install -g @tendrilapp/cli@latest (plugin MCP users update automatically at next session; a stale npx cache clears with npm cache clean --force)
|
|
5659
|
+
`);
|
|
5660
|
+
}
|
|
5661
|
+
for (const check of report.checks) {
|
|
5662
|
+
process.stdout.write(`${check.ok ? "\u2713" : "\u2717"} ${check.name}: ${check.detail}
|
|
5663
|
+
`);
|
|
5664
|
+
if (check.remediation) process.stdout.write(` \u2192 ${check.remediation}
|
|
5665
|
+
`);
|
|
5666
|
+
}
|
|
5667
|
+
process.stdout.write(report.ok ? "\nready to generate\n" : "\nnot ready \u2014 fix the items above\n");
|
|
5668
|
+
});
|
|
5669
|
+
if (!report.ok) process.exit(1);
|
|
5670
|
+
}
|
|
5671
|
+
var DEFAULT_MCP_URL, DOCTOR_DESCRIPTION;
|
|
5672
|
+
var init_doctor = __esm({
|
|
5673
|
+
"packages/cli/src/commands/doctor.ts"() {
|
|
5674
|
+
"use strict";
|
|
5675
|
+
init_src4();
|
|
5676
|
+
init_src();
|
|
5677
|
+
init_describe();
|
|
5678
|
+
init_env();
|
|
5679
|
+
init_environment();
|
|
5680
|
+
init_output();
|
|
5681
|
+
init_entitlement();
|
|
5682
|
+
DEFAULT_MCP_URL = "http://127.0.0.1:3845/mcp";
|
|
5683
|
+
DOCTOR_DESCRIPTION = {
|
|
5684
|
+
name: "doctor",
|
|
5685
|
+
summary: "Check whether this machine can run tendril generate end to end.",
|
|
5686
|
+
args: [],
|
|
5687
|
+
flags: [
|
|
5688
|
+
{ flag: "--mcp-url <url>", description: "Figma MCP endpoint", default: DEFAULT_MCP_URL },
|
|
5689
|
+
{ flag: "--json", description: "Machine-readable output" }
|
|
5690
|
+
],
|
|
5691
|
+
output: {
|
|
5692
|
+
ok: "boolean \u2014 Figma desktop MCP reachable AND a scoring browser found (fonts resolve lazily per design system; keys are informational)",
|
|
5693
|
+
checks: "[{ name, ok, detail, remediation? }]"
|
|
5694
|
+
},
|
|
5695
|
+
exitCodes: { 0: "healthy", 1: "one or more required checks failed" },
|
|
5696
|
+
examples: ["tendril doctor", "tendril doctor --json"]
|
|
5697
|
+
};
|
|
5698
|
+
}
|
|
5699
|
+
});
|
|
5700
|
+
|
|
5389
5701
|
// packages/llm/src/model-config.ts
|
|
5390
5702
|
import { z as z6 } from "zod";
|
|
5391
5703
|
function resolveModel(config, requestedId) {
|
|
@@ -6531,7 +6843,13 @@ var init_bundle = __esm({
|
|
|
6531
6843
|
// insufficient identity (two builds, one path, incomparable
|
|
6532
6844
|
// numbers); optional so pre-epoch bundles keep validating.
|
|
6533
6845
|
chromeVersion: z10.string().nullable().optional(),
|
|
6534
|
-
fontsManifestSha256: z10.string().nullable()
|
|
6846
|
+
fontsManifestSha256: z10.string().nullable(),
|
|
6847
|
+
// Run 11: a bundle scored under a substitute face carried CSS
|
|
6848
|
+
// compensation (font-variation-settings tuned to the substitute)
|
|
6849
|
+
// with nothing marking it conditional — the provenance now names
|
|
6850
|
+
// the families that were substituted at scoring time. Optional so
|
|
6851
|
+
// pre-0.1.19 bundles keep validating; absent means none.
|
|
6852
|
+
substitutedFamilies: z10.array(z10.string()).optional()
|
|
6535
6853
|
}),
|
|
6536
6854
|
/** Coverage denominator (Q2 honesty): recorded vs full-lattice size
|
|
6537
6855
|
* when the lattice is known (null when the kit exposes no lattice). */
|
|
@@ -6687,7 +7005,8 @@ __export(record_exports, {
|
|
|
6687
7005
|
runRecordPlan: () => runRecordPlan,
|
|
6688
7006
|
runRecordStatus: () => runRecordStatus
|
|
6689
7007
|
});
|
|
6690
|
-
import { existsSync as existsSync19, readFileSync as readFileSync16, readdirSync as readdirSync5 } from "node:fs";
|
|
7008
|
+
import { existsSync as existsSync19, mkdtempSync as mkdtempSync2, readFileSync as readFileSync16, readdirSync as readdirSync5 } from "node:fs";
|
|
7009
|
+
import os5 from "node:os";
|
|
6691
7010
|
import path25 from "node:path";
|
|
6692
7011
|
import { writeFileSync as writeFileSync9 } from "node:fs";
|
|
6693
7012
|
function symbolsFromMetadataEnvelope(file, sourceFrame) {
|
|
@@ -6734,10 +7053,42 @@ function runRecordPlan(opts) {
|
|
|
6734
7053
|
}
|
|
6735
7054
|
defaults[spec.slice(0, eq)] = spec.slice(eq + 1);
|
|
6736
7055
|
}
|
|
7056
|
+
if (opts.metadataRawFile !== void 0 && opts.metadataRawPartsFile !== void 0) {
|
|
7057
|
+
fail(opts, ExitCode.InputValidation, {
|
|
7058
|
+
error: "pass at most one of --metadata-raw-file and --metadata-raw-parts-file",
|
|
7059
|
+
code: "bad-envelope",
|
|
7060
|
+
remediation: "One get_metadata response: single block \u2192 --metadata-raw-file; multi-block \u2192 --metadata-raw-parts-file (a JSON array of the blocks)."
|
|
7061
|
+
});
|
|
7062
|
+
}
|
|
7063
|
+
const metadataEntries = (opts.metadataFiles ?? []).map((spec) => {
|
|
7064
|
+
const [file, frame] = spec.split("@");
|
|
7065
|
+
return frame === void 0 ? { file } : { file, frame };
|
|
7066
|
+
});
|
|
7067
|
+
const rawFile = opts.metadataRawPartsFile ?? opts.metadataRawFile;
|
|
7068
|
+
if (rawFile !== void 0) {
|
|
7069
|
+
try {
|
|
7070
|
+
const envelope = rawEnvelopeFromFile(rawFile, opts.metadataRawPartsFile !== void 0);
|
|
7071
|
+
const tmp = path25.join(mkdtempSync2(path25.join(os5.tmpdir(), "tendril-plan-meta-")), "get_metadata.json");
|
|
7072
|
+
writeFileSync9(tmp, JSON.stringify(envelope));
|
|
7073
|
+
metadataEntries.push({ file: tmp });
|
|
7074
|
+
} catch (err) {
|
|
7075
|
+
fail(opts, ExitCode.InputValidation, {
|
|
7076
|
+
error: `could not read raw metadata ${rawFile}: ${err instanceof Error ? err.message.split("\n")[0] : String(err)}`,
|
|
7077
|
+
code: "bad-envelope",
|
|
7078
|
+
remediation: "The raw file holds the get_metadata response text exactly as received (or, for --metadata-raw-parts-file, a JSON array of its blocks)."
|
|
7079
|
+
});
|
|
7080
|
+
}
|
|
7081
|
+
}
|
|
7082
|
+
if (metadataEntries.length === 0) {
|
|
7083
|
+
fail(opts, ExitCode.InputValidation, {
|
|
7084
|
+
error: "no metadata provided",
|
|
7085
|
+
code: "bad-envelope",
|
|
7086
|
+
remediation: "Pass --metadata <envelope.json> (repeatable), or the response text via --metadata-raw-file / --metadata-raw-parts-file."
|
|
7087
|
+
});
|
|
7088
|
+
}
|
|
6737
7089
|
let symbols = [];
|
|
6738
7090
|
let metadataTruncated = false;
|
|
6739
|
-
for (const
|
|
6740
|
-
const [file, frame] = spec.split("@");
|
|
7091
|
+
for (const { file, frame } of metadataEntries) {
|
|
6741
7092
|
try {
|
|
6742
7093
|
const parsed = symbolsFromMetadataEnvelope(path25.resolve(file), frame);
|
|
6743
7094
|
symbols.push(...parsed.symbols);
|
|
@@ -6770,8 +7121,7 @@ function runRecordPlan(opts) {
|
|
|
6770
7121
|
});
|
|
6771
7122
|
}
|
|
6772
7123
|
if (symbols.length === 0) {
|
|
6773
|
-
const leads =
|
|
6774
|
-
const [file] = spec.split("@");
|
|
7124
|
+
const leads = metadataEntries.flatMap(({ file }) => {
|
|
6775
7125
|
try {
|
|
6776
7126
|
const env = JSON.parse(readFileSync16(path25.resolve(file), "utf8"));
|
|
6777
7127
|
return instanceLeads(env.content.map((c) => c.text ?? "").join("\n"));
|
|
@@ -7435,6 +7785,9 @@ function buildFeedback(scores, behaviors, bar, mode = "fenced") {
|
|
|
7435
7785
|
const reg = s.region !== void 0 ? ` worst-region=[x ${s.region.x0}-${s.region.x1}, y ${s.region.y0}-${s.region.y1}, density ${s.region.density}]` : "";
|
|
7436
7786
|
return base + err + reg;
|
|
7437
7787
|
});
|
|
7788
|
+
const absentLines = scores.flatMap(
|
|
7789
|
+
(s) => (s.absentInk ?? []).map((c) => `MISSING ${s.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}]`)
|
|
7790
|
+
);
|
|
7438
7791
|
return `Scores (bar: sim \u2265${bar.sim} AND ink \u2265${bar.ink} per config):
|
|
7439
7792
|
${lines.join("\n")}${behLines.length > 0 ? `
|
|
7440
7793
|
|
|
@@ -7442,9 +7795,12 @@ BEHAVIORAL invariants (machine-verified, GATING \u2014 fix these with real handl
|
|
|
7442
7795
|
${behLines.join("\n")}` : ""}${preludeLines.length > 0 ? `
|
|
7443
7796
|
|
|
7444
7797
|
PRELUDE parity (computed-style checks, GATING \u2014 fix the named CSS property on the named element):
|
|
7445
|
-
${preludeLines.join("\n")}` : ""}
|
|
7798
|
+
${preludeLines.join("\n")}` : ""}${absentLines.length > 0 ? `
|
|
7446
7799
|
|
|
7447
|
-
|
|
7800
|
+
MISSING FEATURES (absent-ink clusters \u2014 recorded marks your render leaves out or paints invisibly; GATING at the cert bar. Fix the named node's ink \u2014 a config carrying one cannot certify):
|
|
7801
|
+
${absentLines.join("\n")}` : ""}
|
|
7802
|
+
|
|
7803
|
+
Fix the FAIL configs (region coordinates are in the recorded screenshot's frame; ink<1 means recorded foreground pixels your render does not cover). Reading the pair: ink=1 with LOW sim means nothing is missing \u2014 shapes or positions are wrong, do not chase missing ink; LOW ink with high sim means recorded marks are absent or painted invisibly. Do not regress PASS configs. ${mode === "files" ? "Update the files in your candidate directory and re-run the score." : "Reply with the complete corrected files in the same FILE format."}`;
|
|
7448
7804
|
}
|
|
7449
7805
|
function archivePriorRun(outDir) {
|
|
7450
7806
|
if (!existsSync20(path26.join(outDir, "run-log.json")) && !existsSync20(path26.join(outDir, "loop-state.json"))) return void 0;
|
|
@@ -7514,14 +7870,14 @@ async function runEngineLoop(opts) {
|
|
|
7514
7870
|
progress(
|
|
7515
7871
|
`iter ${iter}: pass=${obj[0]}/${total} floor=${obj[1].toFixed(3)} mean=${obj[2].toFixed(3)} ${accepted ? "ACCEPT" : "reject"} $${usd.toFixed(3)} (total $${spentUsd.toFixed(3)})${modelMs !== void 0 ? ` model=${Math.round(modelMs / 1e3)}s` : ""} score=${Math.round(scoreMs / 1e3)}s`
|
|
7516
7872
|
);
|
|
7517
|
-
return { allPass: obj[0] === total && total > 0 };
|
|
7873
|
+
return { allPass: obj[0] === total && total > 0 && (opts.absentInkGates !== true || scores.every((s) => (s.absentInk?.length ?? 0) === 0)) };
|
|
7518
7874
|
};
|
|
7519
7875
|
if (opts.seed !== void 0 && !resuming) {
|
|
7520
7876
|
const { allPass } = await scoreCandidate(opts.seed, 0, 0);
|
|
7521
7877
|
if (allPass) stopReason = "all-pass";
|
|
7522
7878
|
}
|
|
7523
7879
|
const lastLog = log[log.length - 1];
|
|
7524
|
-
if (resuming && lastLog !== void 0 && lastLog.parseError === void 0 && lastLog.scores.length > 0 && lastLog.objective[0] === lastLog.scores.length + (lastLog.behaviors?.length ?? 0)) {
|
|
7880
|
+
if (resuming && lastLog !== void 0 && lastLog.parseError === void 0 && lastLog.scores.length > 0 && lastLog.objective[0] === lastLog.scores.length + (lastLog.behaviors?.length ?? 0) && (opts.absentInkGates !== true || lastLog.scores.every((s) => (s.absentInk?.length ?? 0) === 0))) {
|
|
7525
7881
|
stopReason = "all-pass";
|
|
7526
7882
|
}
|
|
7527
7883
|
const state = () => ({
|
|
@@ -7946,6 +8302,23 @@ function recordedFontNeeds(setDir) {
|
|
|
7946
8302
|
return { family, weights: weights.size === 0 ? [400] : [...weights].sort((a, b) => a - b) };
|
|
7947
8303
|
});
|
|
7948
8304
|
}
|
|
8305
|
+
function symbolFontGlyphCount(setDir, reps) {
|
|
8306
|
+
const PUA = /[\uE000-\uF8FF\u{F0000}-\u{FFFFD}\u{100000}-\u{10FFFD}]/u;
|
|
8307
|
+
const glyphs = /* @__PURE__ */ new Set();
|
|
8308
|
+
for (const rep of reps) {
|
|
8309
|
+
const file = path27.join(setDir, rep, "get_metadata.json");
|
|
8310
|
+
if (!existsSync21(file)) continue;
|
|
8311
|
+
try {
|
|
8312
|
+
const text = JSON.parse(readFileSync18(file, "utf8")).content.map((c) => c.text ?? "").join("\n");
|
|
8313
|
+
for (const m of text.matchAll(/<text\s[^>]*name="([^"]*)"/g)) {
|
|
8314
|
+
const name = decodeXmlEntities(m[1]).replace(/&#x([0-9a-fA-F]+);/g, (_, h) => String.fromCodePoint(parseInt(h, 16))).replace(/&#(\d+);/g, (_, d) => String.fromCodePoint(Number(d)));
|
|
8315
|
+
if (PUA.test(name)) glyphs.add(name);
|
|
8316
|
+
}
|
|
8317
|
+
} catch {
|
|
8318
|
+
}
|
|
8319
|
+
}
|
|
8320
|
+
return glyphs.size;
|
|
8321
|
+
}
|
|
7949
8322
|
function recordedFontFamilies(setDir) {
|
|
7950
8323
|
return recordedFontNeeds(setDir).map((n) => n.family);
|
|
7951
8324
|
}
|
|
@@ -8142,6 +8515,12 @@ function authorTaskFromSet(setDir, opts = {}) {
|
|
|
8142
8515
|
return void 0;
|
|
8143
8516
|
}).filter((x) => x !== void 0);
|
|
8144
8517
|
const { behaviors, prelude, disclosures } = authorBehaviors(api, { ...sentinels.length > 0 ? { sentinels } : {} });
|
|
8518
|
+
const puaGlyphs = symbolFontGlyphCount(setDir, manifest.reps.map((r) => r.slug));
|
|
8519
|
+
if (puaGlyphs > 0) {
|
|
8520
|
+
disclosures.push(
|
|
8521
|
+
`SYMBOL-FONT ICONS: ${puaGlyphs} distinct recorded icon glyph(s) come from a symbol font (private-use codepoints) \u2014 the recording carries NO vector geometry for them. Reconstruct each mark against the reference pixels with extreme care, compare magnified crops after every score (run 11 shipped a checkmark where the reference was a dot, and a diamond where it was two chevrons \u2014 both scored above bar), and NEVER give two configs sharing the same recorded glyph different shapes`
|
|
8522
|
+
);
|
|
8523
|
+
}
|
|
8145
8524
|
if (dismissName !== void 0) {
|
|
8146
8525
|
disclosures.push(`dismiss affordance detected from the recording (${dismissName}) \u2014 onDismiss authored; dismiss-notifies-and-commits enforces the notification contract`);
|
|
8147
8526
|
}
|
|
@@ -8464,8 +8843,8 @@ var init_adapter = __esm({
|
|
|
8464
8843
|
});
|
|
8465
8844
|
|
|
8466
8845
|
// packages/generate/src/bundle-emit.ts
|
|
8467
|
-
import { createHash as
|
|
8468
|
-
import { existsSync as existsSync23, readFileSync as readFileSync20, readdirSync as readdirSync7, writeFileSync as writeFileSync11 } from "node:fs";
|
|
8846
|
+
import { createHash as createHash4 } from "node:crypto";
|
|
8847
|
+
import { copyFileSync, existsSync as existsSync23, mkdirSync as mkdirSync7, readFileSync as readFileSync20, readdirSync as readdirSync7, rmSync as rmSync3, writeFileSync as writeFileSync11 } from "node:fs";
|
|
8469
8848
|
import path29 from "node:path";
|
|
8470
8849
|
function pinFromConfigs(configs) {
|
|
8471
8850
|
const domains = /* @__PURE__ */ new Map();
|
|
@@ -8514,8 +8893,44 @@ function cssFontFamilies(css) {
|
|
|
8514
8893
|
}
|
|
8515
8894
|
return [...out];
|
|
8516
8895
|
}
|
|
8517
|
-
function
|
|
8518
|
-
const
|
|
8896
|
+
function fontsCssFor(faces, bundleDir, cacheDir = DEFAULT_FONT_CACHE, substitutedFamilies = []) {
|
|
8897
|
+
const header = [
|
|
8898
|
+
"/* tendril fonts.css \u2014 the exact faces this bundle was scored with (sha-pinned in",
|
|
8899
|
+
" component.json requiredFonts). Load it alongside styles.css so the component",
|
|
8900
|
+
" renders in the recorded typeface \u2014 without it the browser substitutes, which is",
|
|
8901
|
+
" exactly the drift verification exists to rule out. Not read by scoring. */",
|
|
8902
|
+
...substitutedFamilies.length > 0 ? [
|
|
8903
|
+
`/* GENERATED UNDER FONT SUBSTITUTION: ${substitutedFamilies.map((f) => `'${f}'`).join(", ")} was/were NOT provisioned \u2014`,
|
|
8904
|
+
" scores measured a substitute face and no config certified. This file covers only",
|
|
8905
|
+
" cache-provided faces; resolve the real families and re-score for the full set. */"
|
|
8906
|
+
] : []
|
|
8907
|
+
];
|
|
8908
|
+
const lines = [];
|
|
8909
|
+
for (const face of faces) {
|
|
8910
|
+
const src = path29.join(cacheDir, path29.basename(face.file));
|
|
8911
|
+
const target = `./fonts/${path29.basename(face.file)}`;
|
|
8912
|
+
const format = FONT_FORMATS[path29.extname(face.file).toLowerCase()] ?? "truetype";
|
|
8913
|
+
const family = face.family.replace(/['\\]/g, "").replace(/\*\//g, "");
|
|
8914
|
+
const decl = `@font-face { font-family: '${family}'; font-weight: ${face.weight}; font-style: normal; src: url(${target}) format('${format}'); }`;
|
|
8915
|
+
if (face.source.startsWith("local:")) {
|
|
8916
|
+
lines.push(
|
|
8917
|
+
`/* '${family}' ${face.weight} is a user-licensed face (tendril fonts add; sha256 ${face.sha256.slice(0, 16)}\u2026).`,
|
|
8918
|
+
` Licensed bytes are never copied into bundles \u2014 place your copy at ${target} and uncomment: */`,
|
|
8919
|
+
`/* ${decl} */`
|
|
8920
|
+
);
|
|
8921
|
+
} else if (existsSync23(src) && createHash4("sha256").update(readFileSync20(src)).digest("hex") === face.sha256) {
|
|
8922
|
+
mkdirSync7(path29.join(bundleDir, "fonts"), { recursive: true });
|
|
8923
|
+
copyFileSync(src, path29.join(bundleDir, "fonts", path29.basename(face.file)));
|
|
8924
|
+
lines.push(decl);
|
|
8925
|
+
} else {
|
|
8926
|
+
lines.push(`/* '${family}' ${face.weight} (sha256 ${face.sha256.slice(0, 16)}\u2026) could not be shipped: cache bytes missing or hash mismatch \u2014 re-run \`tendril fonts resolve\` and re-emit. */`);
|
|
8927
|
+
}
|
|
8928
|
+
}
|
|
8929
|
+
return lines.length === 0 ? null : `${[...header, ...lines].join("\n")}
|
|
8930
|
+
`;
|
|
8931
|
+
}
|
|
8932
|
+
function countLatticeSymbols(setDir) {
|
|
8933
|
+
const manifestFile = path29.join(setDir, "recording-set.json");
|
|
8519
8934
|
if (existsSync23(manifestFile)) {
|
|
8520
8935
|
try {
|
|
8521
8936
|
const lattice = JSON.parse(readFileSync20(manifestFile, "utf8")).latticeNames;
|
|
@@ -8554,17 +8969,22 @@ function recordingSetHash(setDir, configs) {
|
|
|
8554
8969
|
relPaths,
|
|
8555
8970
|
(p) => new Uint8Array(readFileSync20(path29.join(setDir, p))),
|
|
8556
8971
|
(chunks) => {
|
|
8557
|
-
const h =
|
|
8972
|
+
const h = createHash4("sha256");
|
|
8558
8973
|
for (const c of chunks) h.update(c);
|
|
8559
8974
|
return h.digest("hex");
|
|
8560
8975
|
}
|
|
8561
8976
|
);
|
|
8562
8977
|
}
|
|
8563
8978
|
function statusOf(s) {
|
|
8564
|
-
|
|
8979
|
+
const tier = tierOf(s, BARS.cert);
|
|
8980
|
+
return tier === "certified" && (s.absentInk?.length ?? 0) > 0 ? "pass" : tier;
|
|
8565
8981
|
}
|
|
8566
8982
|
function emitBundleV1(opts) {
|
|
8567
|
-
const
|
|
8983
|
+
const substituted = (opts.substitutedFamilies ?? []).length > 0;
|
|
8984
|
+
const statuses = opts.scores.map((s) => {
|
|
8985
|
+
const tier = statusOf(s);
|
|
8986
|
+
return { rep: s.rep, similarity: s.similarity, inkRecall: s.inkRecall, ...s.exact !== void 0 ? { exact: s.exact } : {}, status: substituted && tier === "certified" ? "pass" : tier };
|
|
8987
|
+
});
|
|
8568
8988
|
const certified = statuses.filter((s) => s.status === "certified").length;
|
|
8569
8989
|
const pass = statuses.filter((s) => s.status !== "fail").length;
|
|
8570
8990
|
const lattice = countLatticeSymbols(opts.task.set);
|
|
@@ -8607,7 +9027,7 @@ function emitBundleV1(opts) {
|
|
|
8607
9027
|
...opts.licenseNote !== void 0 ? { licenseNote: opts.licenseNote } : {},
|
|
8608
9028
|
hash: recordingSetHash(opts.task.set, opts.task.configs)
|
|
8609
9029
|
},
|
|
8610
|
-
environment: opts.environment,
|
|
9030
|
+
environment: { ...opts.environment, ...(opts.substitutedFamilies ?? []).length > 0 ? { substitutedFamilies: opts.substitutedFamilies } : {} },
|
|
8611
9031
|
coverage: { recordedConfigs: statuses.length, latticeConfigs: lattice },
|
|
8612
9032
|
generatedAt: opts.generatedAt ?? (/* @__PURE__ */ new Date()).toISOString(),
|
|
8613
9033
|
...opts.spentUsd !== void 0 ? { spentUsd: opts.spentUsd } : {}
|
|
@@ -8637,17 +9057,26 @@ function emitBundleV1(opts) {
|
|
|
8637
9057
|
${stripped}`);
|
|
8638
9058
|
written.push(stylesPath);
|
|
8639
9059
|
}
|
|
9060
|
+
const fontsCssPath = path29.join(opts.bundleDir, "fonts.css");
|
|
9061
|
+
rmSync3(path29.join(opts.bundleDir, "fonts"), { recursive: true, force: true });
|
|
9062
|
+
rmSync3(fontsCssPath, { force: true });
|
|
9063
|
+
const fontsCss = fontsCssFor(requiredFontsManifest(families, opts.fontCacheDir), opts.bundleDir, opts.fontCacheDir, opts.substitutedFamilies ?? []);
|
|
9064
|
+
if (fontsCss !== null) {
|
|
9065
|
+
writeFileSync11(fontsCssPath, fontsCss);
|
|
9066
|
+
written.push(fontsCssPath);
|
|
9067
|
+
}
|
|
8640
9068
|
const unrecorded = lattice === null ? null : Math.max(0, lattice - statuses.length);
|
|
8641
9069
|
const statusLine = `bundle: ${pass}/${statuses.length} recorded configs \u2265 pass bar (${certified} certified), ` + (interaction.length === 0 ? "interaction behaviors NONE VERIFIED" : `interaction behaviors ${interaction.filter((b) => b.pass).length}/${interaction.length}`) + (prelude.length === 0 ? "" : `, page hygiene ${prelude.filter((b) => b.pass).length}/${prelude.length}`) + (unrecorded === null ? "" : `; ${unrecorded} lattice configs UNVERIFIED`) + ` \u2014 claims in component.json, recompute with \`tendril verify\``;
|
|
8642
9070
|
return { manifest, statusLine, written };
|
|
8643
9071
|
}
|
|
8644
|
-
var BARS;
|
|
9072
|
+
var FONT_FORMATS, BARS;
|
|
8645
9073
|
var init_bundle_emit = __esm({
|
|
8646
9074
|
"packages/generate/src/bundle-emit.ts"() {
|
|
8647
9075
|
"use strict";
|
|
8648
9076
|
init_src6();
|
|
8649
9077
|
init_src();
|
|
8650
9078
|
init_src4();
|
|
9079
|
+
FONT_FORMATS = { ".woff2": "woff2", ".woff": "woff", ".ttf": "truetype", ".otf": "opentype" };
|
|
8651
9080
|
BARS = {
|
|
8652
9081
|
pass: { sim: 0.95, ink: 0.95 },
|
|
8653
9082
|
cert: { sim: 0.97, ink: 0.95 }
|
|
@@ -8826,6 +9255,28 @@ function taskFontFamilies(setDir) {
|
|
|
8826
9255
|
return null;
|
|
8827
9256
|
}
|
|
8828
9257
|
}
|
|
9258
|
+
function unprovisionedFamilies(setDir, cacheDir) {
|
|
9259
|
+
const declared = taskFontFamilies(setDir);
|
|
9260
|
+
if (declared === null) return [];
|
|
9261
|
+
const provided = new Set(verifiedFontFamilies(cacheDir).map((f) => f.family.toLowerCase()));
|
|
9262
|
+
return declared.filter((f) => !provided.has(f.toLowerCase()));
|
|
9263
|
+
}
|
|
9264
|
+
function missingWeights(setDir, cacheDir) {
|
|
9265
|
+
let needs;
|
|
9266
|
+
try {
|
|
9267
|
+
needs = recordedFontNeeds(setDir);
|
|
9268
|
+
} catch {
|
|
9269
|
+
return [];
|
|
9270
|
+
}
|
|
9271
|
+
const provided = new Map(verifiedFontFamilies(cacheDir).map((f) => [f.family.toLowerCase(), f.weights]));
|
|
9272
|
+
const gaps = [];
|
|
9273
|
+
for (const n of needs) {
|
|
9274
|
+
const have = provided.get(n.family.toLowerCase());
|
|
9275
|
+
if (have === void 0) continue;
|
|
9276
|
+
if (n.weights.some((w) => !have.includes(w))) gaps.push({ family: n.family, declared: n.weights, provided: have });
|
|
9277
|
+
}
|
|
9278
|
+
return gaps;
|
|
9279
|
+
}
|
|
8829
9280
|
var init_font_guidance = __esm({
|
|
8830
9281
|
"packages/cli/src/font-guidance.ts"() {
|
|
8831
9282
|
"use strict";
|
|
@@ -8837,6 +9288,7 @@ var init_font_guidance = __esm({
|
|
|
8837
9288
|
// packages/cli/src/commands/verify.ts
|
|
8838
9289
|
var verify_exports = {};
|
|
8839
9290
|
__export(verify_exports, {
|
|
9291
|
+
foldConfigStatus: () => foldConfigStatus,
|
|
8840
9292
|
interactionCoverage: () => interactionCoverage,
|
|
8841
9293
|
runVerify: () => runVerify
|
|
8842
9294
|
});
|
|
@@ -8851,6 +9303,26 @@ function interactionCoverage(behaviors) {
|
|
|
8851
9303
|
operability: interaction.length > 0 && interaction.every((b) => b.pass) ? "verified" : "unverified"
|
|
8852
9304
|
};
|
|
8853
9305
|
}
|
|
9306
|
+
function foldConfigStatus(s, failDemotions, substitutedFamilies) {
|
|
9307
|
+
const { exact: _exact, ...reported } = s;
|
|
9308
|
+
let status = tierOf(s, BARS2.cert);
|
|
9309
|
+
const certDemote = [];
|
|
9310
|
+
let absentInkDemoted = false;
|
|
9311
|
+
if (status === "certified" && s.absentInk !== void 0 && s.absentInk.length > 0) {
|
|
9312
|
+
status = "pass";
|
|
9313
|
+
absentInkDemoted = true;
|
|
9314
|
+
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)`));
|
|
9315
|
+
}
|
|
9316
|
+
if (status === "certified" && substitutedFamilies.length > 0) {
|
|
9317
|
+
status = "pass";
|
|
9318
|
+
certDemote.push(`substituted fonts: cache lacks ${substitutedFamilies.map((f) => `"${f}"`).join(", ")} \u2014 certification never measures a substitute face`);
|
|
9319
|
+
}
|
|
9320
|
+
const base = certDemote.length > 0 ? { ...reported, status, demotedBy: certDemote } : { ...reported, status };
|
|
9321
|
+
return {
|
|
9322
|
+
row: failDemotions === void 0 ? base : { ...base, status: "fail", demotedBy: [...certDemote, ...failDemotions] },
|
|
9323
|
+
absentInkDemoted
|
|
9324
|
+
};
|
|
9325
|
+
}
|
|
8854
9326
|
function taskFromManifest(opts, manifest, setDir) {
|
|
8855
9327
|
const recordedSlugs = loadManifest(setDir).reps.map((r) => r.slug);
|
|
8856
9328
|
const adapterSlugs = Object.keys(manifest.propAdapter);
|
|
@@ -8990,6 +9462,15 @@ async function runVerify(opts) {
|
|
|
8990
9462
|
remediation: fontsUnprovenRemediation(task.set)
|
|
8991
9463
|
});
|
|
8992
9464
|
}
|
|
9465
|
+
const substitutedFamilies = unprovisionedFamilies(task.set);
|
|
9466
|
+
if (substitutedFamilies.length > 0) {
|
|
9467
|
+
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`);
|
|
9468
|
+
} else if (opts.bar === "cert" && taskFontFamilies(task.set) === null) {
|
|
9469
|
+
warn(opts, "family coverage could not be established from this recording (no declared families) \u2014 the certification gate covers only cache non-emptiness here");
|
|
9470
|
+
}
|
|
9471
|
+
for (const g of missingWeights(task.set)) {
|
|
9472
|
+
warn(opts, `font weights (advisory): the recording implies weights ${g.declared.join(", ")} for "${g.family}" \u2014 cache provides ${g.provided.join(", ")}; nearest-weight rendering may shift stroke density (certification stays family-gated; implied weights can leak across families)`);
|
|
9473
|
+
}
|
|
8993
9474
|
const missing = task.configs.filter(
|
|
8994
9475
|
(c) => !existsSync25(path31.join(task.set, c.rep, "get_screenshot.json")) || !existsSync25(path31.join(task.set, c.rep, "get_metadata.json"))
|
|
8995
9476
|
);
|
|
@@ -9003,7 +9484,7 @@ async function runVerify(opts) {
|
|
|
9003
9484
|
const bar = BARS2[opts.bar];
|
|
9004
9485
|
const evidenceDir = path31.join(opts.bundleDir, "verify-evidence");
|
|
9005
9486
|
const scores = await scoreBundleForTask(task, opts.bundleDir, bar, { evidenceDir, onProgress: emitProgress });
|
|
9006
|
-
const quality = await checkBundleQuality(opts.bundleDir, task.entry);
|
|
9487
|
+
const quality = await checkBundleQuality(opts.bundleDir, task.entry, { dir: task.set, reps: task.configs.map((c) => c.rep) });
|
|
9007
9488
|
const bundleCss = ["tokens.css", "styles.css"].map((f) => path31.join(opts.bundleDir, f)).filter((f) => existsSync25(f)).map((f) => readFileSync22(f, "utf8")).join("\n");
|
|
9008
9489
|
const occlusion = await checkSiblingOcclusion(task, opts.bundleDir, bundleCss);
|
|
9009
9490
|
const parity = await checkHoverParity(task, opts.bundleDir);
|
|
@@ -9020,19 +9501,10 @@ async function runVerify(opts) {
|
|
|
9020
9501
|
if (crops !== void 0) {
|
|
9021
9502
|
for (const c of crops) if (!c.pass) demote(c.id.split(":")[1] ?? "", "composition crop failed (A1.4)");
|
|
9022
9503
|
}
|
|
9504
|
+
const folded = scores.map((s) => foldConfigStatus(s, demotions.get(s.rep), substitutedFamilies));
|
|
9505
|
+
const absentInkDemoted = scores.filter((_, i) => folded[i].absentInkDemoted).map((s) => s.rep);
|
|
9023
9506
|
const statuses = [
|
|
9024
|
-
...
|
|
9025
|
-
const { exact: _exact, ...reported } = s;
|
|
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 };
|
|
9033
|
-
const reasons = demotions.get(s.rep);
|
|
9034
|
-
return reasons === void 0 ? base : { ...base, status: "fail", demotedBy: [...certDemote.length > 0 ? certDemote : [], ...reasons] };
|
|
9035
|
-
}),
|
|
9507
|
+
...folded.map((f) => f.row),
|
|
9036
9508
|
// ADR-010 §2 anti-gaming: recorded configs the adapter does not map
|
|
9037
9509
|
// are FAILs, never silently absent.
|
|
9038
9510
|
...unmapped.map((rep) => ({ rep, similarity: 0, inkRecall: 0, pass: false, status: "fail", error: "not mapped by the bundle's prop adapter" }))
|
|
@@ -9045,7 +9517,9 @@ async function runVerify(opts) {
|
|
|
9045
9517
|
const occlusionFailures = occlusion.filter((o) => !o.pass);
|
|
9046
9518
|
const coverage = interactionCoverage(behaviors);
|
|
9047
9519
|
const evidenceUnverified = interactionEvidence.length > 0 && coverage.interactionChecks === 0;
|
|
9048
|
-
const
|
|
9520
|
+
const okExceptDemotion = pixelFailures.length === 0 && behaviorFailures.length === 0 && structuralFailures.length === 0 && cropFailures.length === 0 && occlusionFailures.length === 0 && !evidenceUnverified;
|
|
9521
|
+
const certBlockedByAbsentInk = opts.bar === "cert" ? absentInkDemoted : [];
|
|
9522
|
+
const ok = okExceptDemotion && certBlockedByAbsentInk.length === 0;
|
|
9049
9523
|
const report = {
|
|
9050
9524
|
bundle: opts.bundleDir,
|
|
9051
9525
|
...opts.task !== void 0 ? { task: opts.task } : {},
|
|
@@ -9102,7 +9576,11 @@ async function runVerify(opts) {
|
|
|
9102
9576
|
crops: crops ?? { unavailable: regionsOut !== void 0 && "unavailable" in regionsOut ? regionsOut.unavailable : "no role manifest" }
|
|
9103
9577
|
}
|
|
9104
9578
|
} : {},
|
|
9105
|
-
verdict: ok ? "verified" : "verification-failed"
|
|
9579
|
+
verdict: ok ? "verified" : "verification-failed",
|
|
9580
|
+
// Machine-readable cause (adversarial review): a cert-bar failure
|
|
9581
|
+
// with zero pixel/behavior/composition failures was only
|
|
9582
|
+
// explainable from stderr prose.
|
|
9583
|
+
...certBlockedByAbsentInk.length > 0 ? { certBlockedByAbsentInk } : {}
|
|
9106
9584
|
};
|
|
9107
9585
|
emitData(opts, report, () => {
|
|
9108
9586
|
for (const s of statuses) {
|
|
@@ -9162,7 +9640,7 @@ ${certified}/${statuses.length} certified \xB7 ${report.coverage.pass}/${statuse
|
|
|
9162
9640
|
}
|
|
9163
9641
|
if (evidenceUnverified) {
|
|
9164
9642
|
process.stdout.write(
|
|
9165
|
-
`FAIL interaction-evidence \u2014 the recording proves interactive poses (${interactionEvidence.join(", ")}) and ZERO interaction behaviors were verified. The authoring vocabulary did not map them; this is an instrument failure, not a verified component.
|
|
9643
|
+
`FAIL interaction-evidence \u2014 the recording proves interactive poses (${interactionEvidence.join(", ")}) and ZERO interaction behaviors were verified. The authoring vocabulary did not map them; this is an instrument failure, not a verified component. Mappable today: ${[...INTERACTION_STATES, ...BOOLEAN_STATES].join("/")} \u2014 on an axis named State (checked/selected state axes are recorded as evidence but not yet authorable \u2014 widening is on the roadmap; this is not fixable from component code).
|
|
9166
9644
|
`
|
|
9167
9645
|
);
|
|
9168
9646
|
} else if (ic.interactionChecks === 0) {
|
|
@@ -9191,13 +9669,39 @@ ${certified}/${statuses.length} certified \xB7 ${report.coverage.pass}/${statuse
|
|
|
9191
9669
|
}
|
|
9192
9670
|
process.stdout.write(`environment: chrome=${report.environment.chromeVersion ?? report.environment.chrome} fonts=${report.environment.fontsManifestSha256 ?? "UNRESOLVED"}
|
|
9193
9671
|
`);
|
|
9672
|
+
const shippedFonts = requiredFontsManifest(cssFontFamilies(["styles.css", "tokens.css"].map((f) => path31.join(opts.bundleDir, f)).filter((f) => existsSync25(f)).map((f) => readFileSync22(f, "utf8")).join("\n")));
|
|
9673
|
+
if (shippedFonts.length > 0 && substitutedFamilies.length === 0) {
|
|
9674
|
+
process.stdout.write(`fonts: scored with Tendril-cache faces \u2014 a consuming app must provision the same families (the bundle ships fonts.css when faces are shippable; sha-pinned list in component.json requiredFonts)
|
|
9675
|
+
`);
|
|
9676
|
+
}
|
|
9194
9677
|
process.stdout.write(`evidence: ${evidenceDir} (render/ref/diff per config)
|
|
9195
9678
|
`);
|
|
9196
9679
|
});
|
|
9197
|
-
if (
|
|
9680
|
+
if (opts.bar === "cert" && substitutedFamilies.length > 0) {
|
|
9681
|
+
const names = substitutedFamilies.map((f) => `"${f}"`).join(", ");
|
|
9682
|
+
const alsoAbsent = absentInkDemoted.length > 0 ? ` \u2014 and ${absentInkDemoted.length} config(s) also carry absent-ink demotions (named in the report); certification requires fixing those too` : "";
|
|
9683
|
+
if (opts.json) {
|
|
9684
|
+
process.stderr.write(`${JSON.stringify({ error: `font cache lacks ${names} \u2014 certification never measures a substitute face (scores above are pass-capped)${alsoAbsent}`, code: "fonts-unproven", remediation: fontsUnprovenRemediation(task.set) })}
|
|
9685
|
+
`);
|
|
9686
|
+
} else {
|
|
9687
|
+
process.stderr.write(`error (fonts-unproven): font cache lacks ${names} \u2014 certification never measures a substitute face (scores above are pass-capped)${alsoAbsent}
|
|
9688
|
+
\u2192 ${fontsUnprovenRemediation(task.set)}
|
|
9689
|
+
`);
|
|
9690
|
+
}
|
|
9691
|
+
process.exitCode = ExitCode.FontsUnproven;
|
|
9692
|
+
return;
|
|
9693
|
+
}
|
|
9694
|
+
if (certBlockedByAbsentInk.length > 0) {
|
|
9695
|
+
warn(
|
|
9696
|
+
opts,
|
|
9697
|
+
`${certBlockedByAbsentInk.length} config(s) demoted by absent-ink clusters (${certBlockedByAbsentInk.join(", ")}) \u2014 certification never ships a missing or invisible feature; each cluster is named in the report above. Fix the component and re-verify, or verify at --bar pass for the disclosed result.`
|
|
9698
|
+
);
|
|
9699
|
+
process.exitCode = ExitCode.VerificationFailed;
|
|
9700
|
+
}
|
|
9701
|
+
if (!okExceptDemotion) {
|
|
9198
9702
|
warn(
|
|
9199
9703
|
opts,
|
|
9200
|
-
`${pixelFailures.length} config(s), ${behaviorFailures.length} behavior(s), ${structuralFailures.length + cropFailures.length + occlusionFailures.length} composition check(s) below the ${opts.bar} bar \u2014 bundle written, verdict honest (Q4)`
|
|
9704
|
+
`${pixelFailures.length} config(s), ${behaviorFailures.length} behavior(s), ${structuralFailures.length + cropFailures.length + occlusionFailures.length} composition check(s) below the ${opts.bar} bar${evidenceUnverified ? "; PLUS the interaction-evidence instrument gate failed (operability could not be verified \u2014 see FAIL interaction-evidence above)" : ""} \u2014 bundle written, verdict honest (Q4)`
|
|
9201
9705
|
);
|
|
9202
9706
|
process.exitCode = ExitCode.VerificationFailed;
|
|
9203
9707
|
}
|
|
@@ -9227,18 +9731,18 @@ __export(engine_exports, {
|
|
|
9227
9731
|
runEngineBrief: () => runEngineBrief,
|
|
9228
9732
|
runEngineScore: () => runEngineScore
|
|
9229
9733
|
});
|
|
9230
|
-
import { existsSync as existsSync26, mkdirSync as
|
|
9734
|
+
import { appendFileSync, existsSync as existsSync26, mkdirSync as mkdirSync8, readFileSync as readFileSync23, writeFileSync as writeFileSync12 } from "node:fs";
|
|
9231
9735
|
import path32 from "node:path";
|
|
9232
9736
|
function resolveEngineTask(opts, callerCwd) {
|
|
9233
9737
|
const asPath = path32.resolve(callerCwd, opts.taskOrSet);
|
|
9234
9738
|
const isSet = existsSync26(path32.join(asPath, "recording-set.json"));
|
|
9235
9739
|
const registry = TASKS[opts.taskOrSet];
|
|
9236
|
-
if (registry !== void 0 && !isSet) return { task: registry, name: opts.taskOrSet, disclosures: [] };
|
|
9740
|
+
if (registry !== void 0 && !isSet) return { task: registry, name: opts.taskOrSet, disclosures: [], interactionEvidence: [] };
|
|
9237
9741
|
if (isSet) {
|
|
9238
9742
|
try {
|
|
9239
9743
|
const authored = authorTaskFromSet(asPath);
|
|
9240
9744
|
for (const d of authored.disclosures) warn(opts, d);
|
|
9241
|
-
return { task: authored.task, name: path32.basename(asPath), disclosures: authored.disclosures, apiPin: { props: authored.api.apiPin.props, forcedStates: authored.api.apiPin.forcedStates } };
|
|
9745
|
+
return { task: authored.task, name: path32.basename(asPath), disclosures: authored.disclosures, interactionEvidence: authored.api.interactionEvidence, apiPin: { props: authored.api.apiPin.props, forcedStates: authored.api.apiPin.forcedStates } };
|
|
9242
9746
|
} catch (err) {
|
|
9243
9747
|
fail(opts, ExitCode.InputValidation, {
|
|
9244
9748
|
error: `cannot author a task from ${asPath}: ${err instanceof Error ? err.message.split("\n")[0] : String(err)}`,
|
|
@@ -9276,20 +9780,22 @@ DO NOT INVENT PIXELS FOR THESE POSES. Implement them ONLY as the composition of
|
|
|
9276
9780
|
${notRecorded}` : "";
|
|
9277
9781
|
let fontProvisioning;
|
|
9278
9782
|
if (existsSync26(manifestPath2)) {
|
|
9279
|
-
const provided =
|
|
9783
|
+
const provided = verifiedFontFamilies().map((f) => f.family);
|
|
9280
9784
|
const unprovided = recordedFontFamilies(task.set).filter((r) => !provided.some((p) => p.toLowerCase() === r.toLowerCase()));
|
|
9281
9785
|
if (unprovided.length > 0) {
|
|
9282
9786
|
const names = unprovided.map((f) => `'${f}'`).join(", ");
|
|
9787
|
+
const PROPRIETARY = /^(sf pro|sf compact|sf mono|new york|pingfang|segoe ui|helvetica neue|proxima nova|avenir)/i;
|
|
9788
|
+
const allProprietary = unprovided.every((f) => PROPRIETARY.test(f.trim()));
|
|
9283
9789
|
fontProvisioning = {
|
|
9284
9790
|
unprovided,
|
|
9285
9791
|
question: {
|
|
9286
9792
|
prompt: `This design's text uses ${names}, which is not in the local font kit. How should it be handled?`,
|
|
9287
9793
|
options: [
|
|
9288
|
-
`(Recommended) Run \`tendril fonts resolve --set ${task.set}\` \u2014 open-source families are fetched automatically. Any face that fails there is one you license privately: run \`tendril fonts add "<Family>" <weight> <file>\` with the .woff2/.ttf/.otf you hold (the file never leaves this machine). Then re-run this brief \u2014 exact text fidelity.`,
|
|
9289
|
-
|
|
9794
|
+
allProprietary ? `(Recommended) ${names} ${unprovided.length === 1 ? "is a proprietary face" : "are proprietary faces"} \u2014 Google Fonts cannot serve ${unprovided.length === 1 ? "it" : "them"}, so skip \`fonts resolve\`: run \`tendril fonts add "<Family>" <weight> <file>\` with the .woff2/.ttf/.otf you hold a licence for (the file never leaves this machine), then re-run this brief \u2014 exact text fidelity.` : `(Recommended) Run \`tendril fonts resolve --set ${task.set}\` \u2014 open-source families are fetched automatically. Any face that fails there is one you license privately: run \`tendril fonts add "<Family>" <weight> <file>\` with the .woff2/.ttf/.otf you hold (the file never leaves this machine). Then re-run this brief \u2014 exact text fidelity.`,
|
|
9795
|
+
'Continue with a substitute face: generation proceeds in a provided family, no config can score "certified" under it (cert-bar runs exit fonts-unproven after their report), the substitution is disclosed, and small text differences are expected and not fixable from CSS.'
|
|
9290
9796
|
]
|
|
9291
9797
|
},
|
|
9292
|
-
nonInteractive: "Continue with the substitute and state the substitution in your report."
|
|
9798
|
+
nonInteractive: opts.bar === "cert" ? `Resolve the families first (\`tendril fonts resolve --set ${task.set}\`) \u2014 this loop targets the cert bar, and scoring exits fonts-unproven under a substitute, so continuing without resolving is a dead end.` : "Continue with the substitute and state the substitution in your report."
|
|
9293
9799
|
};
|
|
9294
9800
|
}
|
|
9295
9801
|
}
|
|
@@ -9298,7 +9804,7 @@ ${notRecorded}` : "";
|
|
|
9298
9804
|
=== TASK PAYLOAD (recorded truth, verbatim) ===
|
|
9299
9805
|
${segments}`;
|
|
9300
9806
|
const payloadFile = path32.resolve(callerCwd, opts.out ?? `tendril-out/${name}-brief.md`);
|
|
9301
|
-
|
|
9807
|
+
mkdirSync8(path32.dirname(payloadFile), { recursive: true });
|
|
9302
9808
|
writeFileSync12(payloadFile, payload);
|
|
9303
9809
|
emitData(
|
|
9304
9810
|
opts,
|
|
@@ -9344,7 +9850,7 @@ async function runEngineScore(opts) {
|
|
|
9344
9850
|
requireEntitlement(opts);
|
|
9345
9851
|
const callerCwd = process.env["INIT_CWD"] ?? process.cwd();
|
|
9346
9852
|
const candidateDir = path32.resolve(callerCwd, opts.candidateDir);
|
|
9347
|
-
const { task, name, apiPin } = resolveEngineTask(opts, callerCwd);
|
|
9853
|
+
const { task, name, apiPin, interactionEvidence } = resolveEngineTask(opts, callerCwd);
|
|
9348
9854
|
if (!existsSync26(candidateDir)) {
|
|
9349
9855
|
fail(opts, ExitCode.InputValidation, {
|
|
9350
9856
|
error: `candidate directory not found: ${candidateDir}`,
|
|
@@ -9359,6 +9865,13 @@ async function runEngineScore(opts) {
|
|
|
9359
9865
|
remediation: fontsUnprovenRemediation(task.set)
|
|
9360
9866
|
});
|
|
9361
9867
|
}
|
|
9868
|
+
const substitutedFamilies = unprovisionedFamilies(task.set);
|
|
9869
|
+
if (substitutedFamilies.length > 0) {
|
|
9870
|
+
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`);
|
|
9871
|
+
}
|
|
9872
|
+
for (const g of missingWeights(task.set)) {
|
|
9873
|
+
warn(opts, `font weights (advisory): the recording implies weights ${g.declared.join(", ")} for "${g.family}" \u2014 cache provides ${g.provided.join(", ")}; nearest-weight rendering may shift stroke density (certification stays family-gated; implied weights can leak across families)`);
|
|
9874
|
+
}
|
|
9362
9875
|
if (opts.rebind !== true && existsSync26(path32.join(candidateDir, "component.json"))) {
|
|
9363
9876
|
const prior = (() => {
|
|
9364
9877
|
try {
|
|
@@ -9391,7 +9904,7 @@ async function runEngineScore(opts) {
|
|
|
9391
9904
|
const parityCoverage = parity.length > 0 ? `${parity.filter((p) => p.pass).length}/${parity.length} hover-forced configs` : "not applicable (no hover-forced configs in this set)";
|
|
9392
9905
|
const obj = objective(scores, behaviors);
|
|
9393
9906
|
const total = scores.length + behaviors.length;
|
|
9394
|
-
const quality = await checkBundleQuality(candidateDir, task.entry);
|
|
9907
|
+
const quality = await checkBundleQuality(candidateDir, task.entry, { dir: task.set, reps: task.configs.map((c) => c.rep) });
|
|
9395
9908
|
const qualityFeedback = quality.findings.length === 0 && !quality.tokensAbsent ? "" : `
|
|
9396
9909
|
|
|
9397
9910
|
QUALITY (does not affect the bar \u2014 fix alongside the failing configs):
|
|
@@ -9399,12 +9912,13 @@ ${[
|
|
|
9399
9912
|
...quality.findings.map((f) => `- ${f.kind} ${f.file}${f.line === void 0 ? "" : `:${f.line}`} \u2014 ${f.message}`),
|
|
9400
9913
|
...quality.tokensAbsent ? ["- no design tokens: no tokens.css and no var(--\u2026) reference; every value is hardcoded"] : []
|
|
9401
9914
|
].join("\n")}`;
|
|
9402
|
-
const
|
|
9915
|
+
const absentAtCertBar = opts.bar === "cert" ? scores.filter((sc) => (sc.absentInk?.length ?? 0) > 0).map((sc) => sc.rep) : [];
|
|
9916
|
+
const allPass = obj[0] === total && total > 0 && absentAtCertBar.length === 0;
|
|
9403
9917
|
const certBar = BARS3["cert"];
|
|
9404
9918
|
const parityDemoted = new Set(parity.filter((p) => !p.pass).map((p) => p.id.replace(/^parity:/, "")));
|
|
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);
|
|
9919
|
+
const certifiedReps = substitutedFamilies.length > 0 ? [] : scores.filter((sc) => tierOf(sc, certBar) === "certified" && !parityDemoted.has(sc.rep) && (sc.absentInk === void 0 || sc.absentInk.length === 0)).map((sc) => sc.rep);
|
|
9406
9920
|
const certifiedSet = new Set(certifiedReps);
|
|
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.`));
|
|
9921
|
+
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, and at --bar cert it FAILS the run (exit 5).`));
|
|
9408
9922
|
const absentBlock = absentFindings.length > 0 ? `
|
|
9409
9923
|
|
|
9410
9924
|
MISSING FEATURES (absent-ink clusters \u2014 recorded ink your render leaves nowhere near covered; fix these first, the global numbers cannot see them):
|
|
@@ -9423,8 +9937,14 @@ METRIC DEADBAND (read before iterating on near-misses): the scored similarity/in
|
|
|
9423
9937
|
model: `${opts.model} (self-reported${opts.host !== void 0 ? ` via ${opts.host}` : ""})`,
|
|
9424
9938
|
scores,
|
|
9425
9939
|
behaviors,
|
|
9426
|
-
environment: environmentStamp(taskFontFamilies(task.set))
|
|
9940
|
+
environment: environmentStamp(taskFontFamilies(task.set)),
|
|
9941
|
+
substitutedFamilies
|
|
9427
9942
|
});
|
|
9943
|
+
appendFileSync(
|
|
9944
|
+
path32.join(candidateDir, "score-history.jsonl"),
|
|
9945
|
+
`${JSON.stringify({ at: (/* @__PURE__ */ new Date()).toISOString(), bar: opts.bar, pass: obj[0], total, certified: certifiedReps.length, floor: obj[1], mean: obj[2] })}
|
|
9946
|
+
`
|
|
9947
|
+
);
|
|
9428
9948
|
emitData(
|
|
9429
9949
|
opts,
|
|
9430
9950
|
{
|
|
@@ -9444,6 +9964,10 @@ METRIC DEADBAND (read before iterating on near-misses): the scored similarity/in
|
|
|
9444
9964
|
bundleManifest: emitted.written[0],
|
|
9445
9965
|
note: "styles.css was stamped with the bundle provenance comment on line 1 \u2014 preserve it in any post-score edit",
|
|
9446
9966
|
allPass,
|
|
9967
|
+
// Run 11: generators read allPass:true and reported success on
|
|
9968
|
+
// bundles verify then FAILED on the interaction-evidence gate —
|
|
9969
|
+
// the oracle must say what verify will say, including this.
|
|
9970
|
+
...interactionEvidence.length > 0 && interactionCoverage(behaviors).interactionChecks === 0 ? { interactionEvidenceUnverified: true, verifyWillFail: "interaction-evidence \u2014 the recording proves interactive poses none of the authored behaviors cover; not fixable from component code; REPORT it, do not iterate on it" } : {},
|
|
9447
9971
|
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" }
|
|
9448
9972
|
},
|
|
9449
9973
|
() => {
|
|
@@ -9455,12 +9979,31 @@ METRIC DEADBAND (read before iterating on near-misses): the scored similarity/in
|
|
|
9455
9979
|
${obj[0]}/${total} \xB7 certified ${certifiedReps.length}/${total} \xB7 floor=${obj[1].toFixed(3)} \xB7 mean=${obj[2].toFixed(3)} \xB7 evidence: ${evidenceDir}
|
|
9456
9980
|
`);
|
|
9457
9981
|
const ic = interactionCoverage(behaviors);
|
|
9458
|
-
if (ic.interactionChecks === 0) {
|
|
9982
|
+
if (interactionEvidence.length > 0 && ic.interactionChecks === 0) {
|
|
9983
|
+
process.stdout.write(`INSTRUMENT GAP \u2014 the recording proves interactive poses (${interactionEvidence.join(", ")}) and zero interaction behaviors were authored; verify WILL fail this bundle (interaction-evidence). Not fixable from component code \u2014 report it, do not iterate on it.
|
|
9984
|
+
`);
|
|
9985
|
+
} else if (ic.interactionChecks === 0) {
|
|
9459
9986
|
process.stdout.write(`UNVERIFIED operability \u2014 0 interaction checks; the behaviour passes above are page-level style hygiene
|
|
9460
9987
|
`);
|
|
9461
9988
|
}
|
|
9462
9989
|
}
|
|
9463
9990
|
);
|
|
9991
|
+
if (absentAtCertBar.length > 0) {
|
|
9992
|
+
warn(opts, `${absentAtCertBar.length} config(s) carry absent-ink clusters (${absentAtCertBar.join(", ")}) \u2014 at --bar cert these fail the run; the MISSING FEATURES block above names each one`);
|
|
9993
|
+
}
|
|
9994
|
+
if (opts.bar === "cert" && substitutedFamilies.length > 0) {
|
|
9995
|
+
const names = substitutedFamilies.map((f) => `"${f}"`).join(", ");
|
|
9996
|
+
if (opts.json) {
|
|
9997
|
+
process.stderr.write(`${JSON.stringify({ error: `font cache lacks ${names} \u2014 certification never measures a substitute face (scores above are pass-capped)`, code: "fonts-unproven", remediation: fontsUnprovenRemediation(task.set) })}
|
|
9998
|
+
`);
|
|
9999
|
+
} else {
|
|
10000
|
+
process.stderr.write(`error (fonts-unproven): font cache lacks ${names} \u2014 certification never measures a substitute face (scores above are pass-capped)
|
|
10001
|
+
\u2192 ${fontsUnprovenRemediation(task.set)}
|
|
10002
|
+
`);
|
|
10003
|
+
}
|
|
10004
|
+
process.exitCode = ExitCode.FontsUnproven;
|
|
10005
|
+
return;
|
|
10006
|
+
}
|
|
9464
10007
|
if (!allPass) process.exitCode = ExitCode.VerificationFailed;
|
|
9465
10008
|
}
|
|
9466
10009
|
var BARS3;
|
|
@@ -9660,6 +10203,586 @@ var init_codeconnect = __esm({
|
|
|
9660
10203
|
}
|
|
9661
10204
|
});
|
|
9662
10205
|
|
|
10206
|
+
// packages/mcp/src/server.ts
|
|
10207
|
+
import { createHash as createHash5 } from "node:crypto";
|
|
10208
|
+
import { existsSync as existsSync28, mkdtempSync as mkdtempSync3, readFileSync as readFileSync25, readdirSync as readdirSync8, writeFileSync as writeFileSync14 } from "node:fs";
|
|
10209
|
+
import os6 from "node:os";
|
|
10210
|
+
import path34 from "node:path";
|
|
10211
|
+
import { fileURLToPath as fileURLToPath5 } from "node:url";
|
|
10212
|
+
import { z as z12 } from "zod";
|
|
10213
|
+
function sourceHash() {
|
|
10214
|
+
const dir = path34.dirname(fileURLToPath5(import.meta.url));
|
|
10215
|
+
const h = createHash5("sha256");
|
|
10216
|
+
for (const f of readdirSync8(dir).filter((n) => n.endsWith(".ts")).sort()) {
|
|
10217
|
+
h.update(f);
|
|
10218
|
+
h.update(readFileSync25(path34.join(dir, f)));
|
|
10219
|
+
}
|
|
10220
|
+
return h.digest("hex").slice(0, 16);
|
|
10221
|
+
}
|
|
10222
|
+
var REPO_ROOT3, CLI_BIN, BUNDLED_CLI, CLI_SPAWN, str, optStr, TOOLS, BOOT_HASH;
|
|
10223
|
+
var init_server = __esm({
|
|
10224
|
+
"packages/mcp/src/server.ts"() {
|
|
10225
|
+
"use strict";
|
|
10226
|
+
REPO_ROOT3 = path34.resolve(path34.dirname(fileURLToPath5(import.meta.url)), "..", "..", "..");
|
|
10227
|
+
CLI_BIN = path34.join(REPO_ROOT3, "packages", "cli", "src", "bin.ts");
|
|
10228
|
+
BUNDLED_CLI = path34.join(path34.dirname(fileURLToPath5(import.meta.url)), "tendril.js");
|
|
10229
|
+
CLI_SPAWN = existsSync28(BUNDLED_CLI) ? { cmd: process.execPath, prefix: [BUNDLED_CLI] } : { cmd: "npx", prefix: ["tsx", CLI_BIN] };
|
|
10230
|
+
str = (d) => z12.string().describe(d);
|
|
10231
|
+
optStr = (d) => z12.string().optional().describe(d);
|
|
10232
|
+
TOOLS = [
|
|
10233
|
+
{
|
|
10234
|
+
name: "tendril_record_plan",
|
|
10235
|
+
description: "ENTRY POINT for implementing/building a React component from a Figma design or figma.com URL \u2014 start the Tendril pipeline here (after loading the tendril skill, if installed). Plans a recording session: computes the rep queue (anchor + one-factor + conflict crosses) from the verbatim get_metadata response (pass its blocks via metadataParts \u2014 no file to write) and persists the set manifest. Resumes if the set already exists. The output may carry USER QUESTIONS \u2014 defaultsToConfirm (which pose is the component's default) or a multiple-component-sets error (which set to record): render them to a present user and apply the answers via `defaults` / `componentSet`; non-interactive runs follow each question's stated fallback. Recording cost is stated in figmaCallEstimate, never asked about \u2014 proceed with what the user provided.",
|
|
10236
|
+
schema: z12.object({
|
|
10237
|
+
setDir: str("recording set directory to create/resume"),
|
|
10238
|
+
component: str("component/system name"),
|
|
10239
|
+
// Parts array FIRST, same reason as ingest_rep: real responses
|
|
10240
|
+
// are usually multi-block, and the file param made plan the ONE
|
|
10241
|
+
// remaining hand-built-envelope entry point (run 10: the agent
|
|
10242
|
+
// wrote the file twice — once as text, once as JSON envelope).
|
|
10243
|
+
metadataParts: z12.array(z12.string()).optional().describe("the frame-level get_metadata response blocks, every block in order, each verbatim \u2014 never hand-join or save them yourself; this is the NORMAL param"),
|
|
10244
|
+
metadata: optStr("ONLY when get_metadata genuinely returned one single block: its text verbatim (otherwise use metadataParts)"),
|
|
10245
|
+
metadataFiles: z12.array(z12.string()).optional().describe('saved verbatim get_metadata envelope file paths \u2014 JSON shape {"content":[{"type":"text","text":"<frame \u2026>"}]}; optionally <file>@<frameId>. Prefer metadataParts: no file to write'),
|
|
10246
|
+
defaults: z12.array(z12.string()).optional().describe(`axis defaults as "Axis=Value" (from the user's defaultsToConfirm answers; may re-plan a set with nothing recorded yet)`),
|
|
10247
|
+
componentSet: optStr("record only this component set (the user's pick from a multiple-component-sets error)")
|
|
10248
|
+
}),
|
|
10249
|
+
// Texts ride temp files, never argv: Windows caps a command line at
|
|
10250
|
+
// ~32 KB and metadata envelopes can exceed it.
|
|
10251
|
+
argv: (i) => {
|
|
10252
|
+
const argvOut = ["record", "plan", "--set", i["setDir"], "--component", i["component"]];
|
|
10253
|
+
if (i["metadata"] !== void 0 && i["metadataParts"] !== void 0) throw new Error("pass at most one of `metadata` and `metadataParts`");
|
|
10254
|
+
const files = i["metadataFiles"];
|
|
10255
|
+
if (files !== void 0 && files.length > 0) argvOut.push("--metadata", ...files);
|
|
10256
|
+
const single = i["metadata"];
|
|
10257
|
+
const parts = i["metadataParts"];
|
|
10258
|
+
if (single !== void 0 || parts !== void 0) {
|
|
10259
|
+
const tmp = path34.join(mkdtempSync3(path34.join(os6.tmpdir(), "tendril-envelope-")), "response.txt");
|
|
10260
|
+
if (single !== void 0) {
|
|
10261
|
+
writeFileSync14(tmp, single);
|
|
10262
|
+
argvOut.push("--metadata-raw-file", tmp);
|
|
10263
|
+
} else {
|
|
10264
|
+
if (parts.length === 0) throw new Error("`metadataParts` must be a non-empty array of block texts");
|
|
10265
|
+
writeFileSync14(tmp, JSON.stringify(parts));
|
|
10266
|
+
argvOut.push("--metadata-raw-parts-file", tmp);
|
|
10267
|
+
}
|
|
10268
|
+
}
|
|
10269
|
+
if (argvOut.length === 6) throw new Error("nothing to plan from \u2014 pass metadataParts (normal), metadata (single-block), or metadataFiles");
|
|
10270
|
+
argvOut.push(...i["defaults"] !== void 0 ? ["--default", ...i["defaults"]] : [], ...i["componentSet"] !== void 0 ? ["--component-set", i["componentSet"]] : []);
|
|
10271
|
+
return argvOut;
|
|
10272
|
+
}
|
|
10273
|
+
},
|
|
10274
|
+
{
|
|
10275
|
+
name: "tendril_permissions",
|
|
10276
|
+
description: "Install the Claude Code permission allowlist for the Tendril pipeline (write: true \u2014 merges per-tool entries into the project's .claude/settings.local.json, idempotent, never touches other settings), or list the entries without writing. USE THIS when tendril/Figma tool calls keep hitting permission prompts: offer it to the user once \u2014 ONE approval here replaces a prompt per pipeline call (run 8 measured 16+ prompts for a single component's recording). The user must reload the session for new settings to apply.",
|
|
10277
|
+
schema: z12.object({
|
|
10278
|
+
write: z12.boolean().optional().describe("true = install into .claude/settings.local.json (the point of this tool); false/absent = just return the entries")
|
|
10279
|
+
}),
|
|
10280
|
+
argv: (i) => ["permissions", "--claude", ...i["write"] === true ? ["--write"] : []]
|
|
10281
|
+
},
|
|
10282
|
+
{
|
|
10283
|
+
name: "tendril_doctor",
|
|
10284
|
+
annotations: { readOnlyHint: true },
|
|
10285
|
+
description: "Machine readiness + version status in one shot: installed vs latest version (with publish date and update remediation), browser identity, font-cache state, Figma desktop MCP reachability. Use to self-diagnose before recording/scoring, or whenever versions are in question. Exit 1 = something not ready; the report says exactly what and how to fix it.",
|
|
10286
|
+
schema: z12.object({}),
|
|
10287
|
+
argv: () => ["doctor"]
|
|
10288
|
+
},
|
|
10289
|
+
{
|
|
10290
|
+
name: "tendril_record_next",
|
|
10291
|
+
annotations: { readOnlyHint: true },
|
|
10292
|
+
description: "Get the next pending recording instruction (which Figma MCP tool to call for which node, and how to save it). RARELY NEEDED: every ingest/fetch response already carries `next` \u2014 use this only to resume an interrupted session. The full queue is known from plan, so independent reps may be recorded in any order (and in parallel).",
|
|
10293
|
+
schema: z12.object({ setDir: str("recording set directory") }),
|
|
10294
|
+
argv: (i) => ["record", "next", "--set", i["setDir"]]
|
|
10295
|
+
},
|
|
10296
|
+
{
|
|
10297
|
+
name: "tendril_record_fetch",
|
|
10298
|
+
description: "Download a Figma asset URL (from get_screenshot's image_url) straight to disk and ingest it as the rep's envelope \u2014 the fallback when only the screenshot piece needs (re-)recording; for a rep's standard three recordings PREFER tendril_record_ingest_rep. Never download the image yourself: the bytes must not pass through your context.",
|
|
10299
|
+
schema: z12.object({
|
|
10300
|
+
setDir: str("recording set directory"),
|
|
10301
|
+
rep: str("planned rep slug"),
|
|
10302
|
+
tool: z12.enum(["get_screenshot"]).describe("get_screenshot"),
|
|
10303
|
+
url: str("image_url from the Figma response, verbatim")
|
|
10304
|
+
}),
|
|
10305
|
+
argv: (i) => ["record", "fetch", "--set", i["setDir"], "--rep", i["rep"], "--tool", i["tool"], "--url", i["url"]]
|
|
10306
|
+
},
|
|
10307
|
+
{
|
|
10308
|
+
name: "tendril_record_ingest_rep",
|
|
10309
|
+
description: "Ingest a rep's ENTIRE recording in ONE call \u2014 the get_metadata response, the get_design_context response, and the get_screenshot image_url together. Make the three Figma calls first, in protocol order (get_metadata, then get_design_context with excludeScreenshot=true, then get_screenshot), then pass all three here VERBATIM. PREFER THIS over three separate ingest/fetch calls: one approvable operation per rep instead of three. Pieces land independently: on a partial failure the error names exactly which piece(s) to re-record \u2014 the rest are already on disk. The response carries `next` and, for design context, `assets` (auto-fetched server-side; only listed failures need record_asset).",
|
|
10310
|
+
schema: z12.object({
|
|
10311
|
+
setDir: str("recording set directory"),
|
|
10312
|
+
rep: str("planned rep slug"),
|
|
10313
|
+
// Parts arrays FIRST: in the field, EVERY real Figma response is
|
|
10314
|
+
// multi-block (run 6: 49/49 reps — metadata 2 blocks, design
|
|
10315
|
+
// context 5-6), so the arrays are the norm and the single-string
|
|
10316
|
+
// params the rare case, not the reverse.
|
|
10317
|
+
metadataParts: z12.array(z12.string()).optional().describe("get_metadata response blocks, every block in order, each verbatim \u2014 never hand-join blocks yourself. Multi-block responses are common (run 6: 49/49 reps); a single block wrapped in a one-element array is equally fine."),
|
|
10318
|
+
contextParts: z12.array(z12.string()).optional().describe("get_design_context response blocks, every block in order, each verbatim \u2014 the NORMAL param (real responses arrive as 5-6 blocks)"),
|
|
10319
|
+
screenshotUrl: optStr("image_url from the get_screenshot response, verbatim \u2014 the CLI downloads it; the bytes never pass through your context"),
|
|
10320
|
+
metadata: optStr("ONLY when get_metadata genuinely returned one single block: its text verbatim (otherwise use metadataParts)"),
|
|
10321
|
+
context: optStr("ONLY when get_design_context genuinely returned one single block: its text verbatim (otherwise use contextParts)")
|
|
10322
|
+
}),
|
|
10323
|
+
// Texts ride temp files, never argv: Windows caps a command line at
|
|
10324
|
+
// ~32 KB and design-context envelopes routinely exceed it.
|
|
10325
|
+
argv: (i) => {
|
|
10326
|
+
const argvOut = ["record", "ingest-rep", "--set", i["setDir"], "--rep", i["rep"]];
|
|
10327
|
+
const bridge = (label, single, parts) => {
|
|
10328
|
+
if (single !== void 0 && parts !== void 0) throw new Error(`pass at most one of \`${label}\` and \`${label}Parts\``);
|
|
10329
|
+
if (single === void 0 && parts === void 0) return;
|
|
10330
|
+
const tmp = path34.join(mkdtempSync3(path34.join(os6.tmpdir(), "tendril-envelope-")), "response.txt");
|
|
10331
|
+
if (single !== void 0) {
|
|
10332
|
+
writeFileSync14(tmp, single);
|
|
10333
|
+
argvOut.push(`--${label}-file`, tmp);
|
|
10334
|
+
} else {
|
|
10335
|
+
const blocks = parts;
|
|
10336
|
+
if (blocks.length === 0) throw new Error(`\`${label}Parts\` must be a non-empty array of block texts`);
|
|
10337
|
+
writeFileSync14(tmp, JSON.stringify(blocks));
|
|
10338
|
+
argvOut.push(`--${label}-parts-file`, tmp);
|
|
10339
|
+
}
|
|
10340
|
+
};
|
|
10341
|
+
bridge("metadata", i["metadata"], i["metadataParts"]);
|
|
10342
|
+
bridge("context", i["context"], i["contextParts"]);
|
|
10343
|
+
if (i["screenshotUrl"] !== void 0) argvOut.push("--screenshot-url", i["screenshotUrl"]);
|
|
10344
|
+
if (argvOut.length === 6) throw new Error("nothing to ingest \u2014 pass at least one of metadata/metadataParts, context/contextParts, screenshotUrl");
|
|
10345
|
+
return argvOut;
|
|
10346
|
+
}
|
|
10347
|
+
},
|
|
10348
|
+
{
|
|
10349
|
+
name: "tendril_record_ingest",
|
|
10350
|
+
description: "Single-piece ingest of a VERBATIM Figma tool-response \u2014 the fallback path (re-recording one failed piece, set-level get_variable_defs, get_metadata_interior for mains); for a rep's standard three recordings PREFER tendril_record_ingest_rep, which takes them all in one call. Pass `text` (single block) or `texts` (response split into multiple output blocks \u2014 each block verbatim, in order; NEVER hand-join them): the CLI constructs the envelope from the same bytes. The response includes `next` and, for get_design_context, `assets` (auto-fetched; only listed failures need manual handling).",
|
|
10351
|
+
schema: z12.object({
|
|
10352
|
+
setDir: str("recording set directory"),
|
|
10353
|
+
rep: str("planned rep slug, or __set__ for the set-level get_variable_defs"),
|
|
10354
|
+
// Enumerated, not a free string: this value reaches a file path.
|
|
10355
|
+
// As a bare string it was an arbitrary relative-path overwrite
|
|
10356
|
+
// (--tool "../../outside/victim" replaced a file outside the set,
|
|
10357
|
+
// exit 0). The sink in session.ts now contains the path too — this
|
|
10358
|
+
// is the second layer, and it makes the tool self-documenting.
|
|
10359
|
+
tool: z12.enum(["get_design_context", "get_metadata", "get_screenshot", "get_variable_defs", "get_metadata_interior"]),
|
|
10360
|
+
text: optStr("the tool response text VERBATIM \u2014 exactly as returned, unmodified (single-block responses; text tools only \u2014 screenshots go through record_fetch)"),
|
|
10361
|
+
texts: z12.array(z12.string()).optional().describe("when the response arrived as MULTIPLE output blocks: every block, in order, each verbatim \u2014 never hand-join blocks yourself"),
|
|
10362
|
+
file: optStr("path to a saved envelope JSON (alternative to text/texts)")
|
|
10363
|
+
}),
|
|
10364
|
+
// The text rides a temp file, never argv: Windows caps a command
|
|
10365
|
+
// line at ~32 KB and design-context envelopes routinely exceed it.
|
|
10366
|
+
argv: (i) => {
|
|
10367
|
+
const base = ["record", "ingest", "--set", i["setDir"], "--rep", i["rep"], "--tool", i["tool"]];
|
|
10368
|
+
const text = i["text"];
|
|
10369
|
+
const texts = i["texts"];
|
|
10370
|
+
const file = i["file"];
|
|
10371
|
+
if ([text, texts, file].filter((x) => x !== void 0).length !== 1) throw new Error("pass exactly one of `text` (single-block response), `texts` (multi-block response), or `file` (a saved envelope)");
|
|
10372
|
+
if (file !== void 0) return [...base, "--file", file];
|
|
10373
|
+
const tmp = path34.join(mkdtempSync3(path34.join(os6.tmpdir(), "tendril-envelope-")), "response.txt");
|
|
10374
|
+
if (text !== void 0) {
|
|
10375
|
+
writeFileSync14(tmp, text);
|
|
10376
|
+
return [...base, "--file", tmp, "--raw"];
|
|
10377
|
+
}
|
|
10378
|
+
writeFileSync14(tmp, JSON.stringify(texts));
|
|
10379
|
+
return [...base, "--file", tmp, "--raw-parts"];
|
|
10380
|
+
}
|
|
10381
|
+
},
|
|
10382
|
+
{
|
|
10383
|
+
name: "tendril_record_asset",
|
|
10384
|
+
description: "FALLBACK ONLY \u2014 ingest auto-fetches design-context assets; use this just for assets listed in an ingest response's `assets.failed`. Batch mode: pass `dir` to ingest every asset-*.<ext> in a directory in ONE call. SVGs with active content are rejected; sizes are capped.",
|
|
10385
|
+
schema: z12.object({
|
|
10386
|
+
setDir: str("recording set directory"),
|
|
10387
|
+
rep: str("planned rep slug"),
|
|
10388
|
+
name: optStr("asset-<id>.<ext> (single-asset mode)"),
|
|
10389
|
+
file: optStr("downloaded asset file path (single-asset mode)"),
|
|
10390
|
+
dir: optStr("batch mode: directory holding downloaded asset-*.<ext> files")
|
|
10391
|
+
}),
|
|
10392
|
+
argv: (i) => [
|
|
10393
|
+
"record",
|
|
10394
|
+
"asset",
|
|
10395
|
+
"--set",
|
|
10396
|
+
i["setDir"],
|
|
10397
|
+
"--rep",
|
|
10398
|
+
i["rep"],
|
|
10399
|
+
...i["dir"] !== void 0 ? ["--dir", i["dir"]] : ["--name", i["name"], "--file", i["file"]]
|
|
10400
|
+
]
|
|
10401
|
+
},
|
|
10402
|
+
{
|
|
10403
|
+
name: "tendril_record_status",
|
|
10404
|
+
annotations: { readOnlyHint: true },
|
|
10405
|
+
description: "Recording-set completeness: per-rep recorded/missing tools.",
|
|
10406
|
+
schema: z12.object({ setDir: str("recording set directory") }),
|
|
10407
|
+
argv: (i) => ["record", "status", "--set", i["setDir"]]
|
|
10408
|
+
},
|
|
10409
|
+
{
|
|
10410
|
+
name: "tendril_engine_brief",
|
|
10411
|
+
description: "AGENT-HARNESS engine, step 1: emits the task payload file (system brief + every recorded config's emission, box, assets, tokens) and the protocol. YOU (the calling agent) implement the bundle; the CLI is the only judge. Read the payload file completely before proposing.",
|
|
10412
|
+
schema: z12.object({
|
|
10413
|
+
taskOrSet: str("a recording-set directory (from tendril_record), or a reference task name \u2014 an unknown name returns the valid list in the error"),
|
|
10414
|
+
// Required by design, not convenience: the model choice must be
|
|
10415
|
+
// settled BEFORE generation starts. A smoke run picked its own
|
|
10416
|
+
// model silently because the ask lived in instruction text, which
|
|
10417
|
+
// evaporates in non-interactive sessions; a required parameter
|
|
10418
|
+
// cannot evaporate. Ask the user when one is present; otherwise
|
|
10419
|
+
// choose, declare, and state the reason in your report.
|
|
10420
|
+
model: str("the model that will WRITE the implementation \u2014 ask the user when interactive; declare your reasoned choice when not"),
|
|
10421
|
+
bar: optStr("pass (default) or cert"),
|
|
10422
|
+
out: optStr("payload file path override")
|
|
10423
|
+
}),
|
|
10424
|
+
argv: (i) => [
|
|
10425
|
+
"engine",
|
|
10426
|
+
"brief",
|
|
10427
|
+
i["taskOrSet"],
|
|
10428
|
+
"--model",
|
|
10429
|
+
i["model"],
|
|
10430
|
+
...i["bar"] !== void 0 ? ["--bar", i["bar"]] : [],
|
|
10431
|
+
...i["out"] !== void 0 ? ["--out", i["out"]] : []
|
|
10432
|
+
]
|
|
10433
|
+
},
|
|
10434
|
+
{
|
|
10435
|
+
name: "tendril_engine_score",
|
|
10436
|
+
description: "AGENT-HARNESS engine, step 2 (the oracle): scores a candidate bundle directory against recorded truth \u2014 per-config pixels, behaviors, hover parity \u2014 and returns feedback plus evidence artifacts. Iterate until allPass or two non-improving rounds. Only THIS tool's output counts as a score; never claim numbers yourself.",
|
|
10437
|
+
schema: z12.object({
|
|
10438
|
+
taskOrSet: str("reference task name or recording-set directory"),
|
|
10439
|
+
candidateDir: str("directory containing the proposed bundle files"),
|
|
10440
|
+
bar: optStr("pass (default) or cert"),
|
|
10441
|
+
host: optStr("your host identity (e.g. claude-code, cursor, codex) \u2014 recorded as self-reported provenance"),
|
|
10442
|
+
model: str("the model that proposed the candidate \u2014 REQUIRED; recorded as self-reported provenance, and the CLI refuses to score without it"),
|
|
10443
|
+
rebind: z12.boolean().optional().describe("explicitly re-bind an already-bound bundle to a DIFFERENT recording set \u2014 scoring refuses this otherwise, because rebinding silently rewrites the bundle's verification identity; only pass after telling the user")
|
|
10444
|
+
}),
|
|
10445
|
+
argv: (i) => [
|
|
10446
|
+
"engine",
|
|
10447
|
+
"score",
|
|
10448
|
+
i["taskOrSet"],
|
|
10449
|
+
i["candidateDir"],
|
|
10450
|
+
...i["bar"] !== void 0 ? ["--bar", i["bar"]] : [],
|
|
10451
|
+
...i["host"] !== void 0 ? ["--host", i["host"]] : [],
|
|
10452
|
+
"--model",
|
|
10453
|
+
i["model"],
|
|
10454
|
+
...i["rebind"] === true ? ["--rebind"] : []
|
|
10455
|
+
]
|
|
10456
|
+
},
|
|
10457
|
+
{
|
|
10458
|
+
name: "tendril_codeconnect",
|
|
10459
|
+
description: "Emit a Figma Code Connect template (.figma.ts) for a certified bundle \u2014 every Figma variant value mapped to its verified prop fragment from recorded truth, stamped with the bundle's trust statement. EXTRA VALUE step after verify passes: offer it to the user. Publishing is the USER'S action (their Figma token, Organization/Enterprise plan) \u2014 via npx @figma/code-connect connect publish, or the Figma MCP's own add_code_connect_map/send_code_connect_mappings tools if available in this session.",
|
|
10460
|
+
schema: z12.object({
|
|
10461
|
+
bundleDir: str("bundle directory (carries component.json)"),
|
|
10462
|
+
figmaUrl: str("figma.com /design/ URL of the COMPONENT SET, with node-id (ask the user to Copy link to selection if you don't have it)"),
|
|
10463
|
+
set: optStr("recording set override (default: the bundle's provenance path)"),
|
|
10464
|
+
out: optStr("output file path (default: <bundle>/<Component>.figma.ts)")
|
|
10465
|
+
}),
|
|
10466
|
+
argv: (i) => [
|
|
10467
|
+
"codeconnect",
|
|
10468
|
+
i["bundleDir"],
|
|
10469
|
+
"--figma-url",
|
|
10470
|
+
i["figmaUrl"],
|
|
10471
|
+
...i["set"] !== void 0 ? ["--set", i["set"]] : [],
|
|
10472
|
+
...i["out"] !== void 0 ? ["--out", i["out"]] : []
|
|
10473
|
+
]
|
|
10474
|
+
},
|
|
10475
|
+
{
|
|
10476
|
+
name: "tendril_verify",
|
|
10477
|
+
description: "Verify/check that a component matches its Figma design \u2014 use when the user asks whether an implementation is faithful to the design, or to re-certify an existing Tendril bundle. Recomputes full verification (per-config status, behaviors, composition, evidence artifacts). Free, account-less, network-less \u2014 the trust anchor. Exit 5 means below the target bar with an honest report \u2014 sub-bar scores, or at bar cert a config demoted by absent-ink clusters.",
|
|
10478
|
+
schema: z12.object({
|
|
10479
|
+
bundleDir: str("bundle directory to verify"),
|
|
10480
|
+
bar: optStr("pass (default) or cert"),
|
|
10481
|
+
set: optStr("recording-set directory override")
|
|
10482
|
+
}),
|
|
10483
|
+
argv: (i) => [
|
|
10484
|
+
"verify",
|
|
10485
|
+
i["bundleDir"],
|
|
10486
|
+
...i["bar"] !== void 0 ? ["--bar", i["bar"]] : [],
|
|
10487
|
+
...i["set"] !== void 0 ? ["--set", i["set"]] : []
|
|
10488
|
+
]
|
|
10489
|
+
},
|
|
10490
|
+
{
|
|
10491
|
+
name: "tendril_generate_curated",
|
|
10492
|
+
description: "CURATED engine (explicit alternative path): generation by an allowlisted API model over the user's OpenRouter-compatible key, with cost consent, hard spend caps, and resume. Use ONLY when the user asks for API-model generation instead of implementing it yourself.",
|
|
10493
|
+
schema: z12.object({
|
|
10494
|
+
input: str("reference task name or recording-set directory"),
|
|
10495
|
+
model: optStr("OpenRouter model id (default: allowlist pointer)"),
|
|
10496
|
+
bar: optStr("pass (default) or cert"),
|
|
10497
|
+
cap: optStr("spend cap in USD (default 1.50)"),
|
|
10498
|
+
yes: z12.boolean().optional().describe("accept the cost consent (the user must have approved the spend)")
|
|
10499
|
+
}),
|
|
10500
|
+
argv: (i) => [
|
|
10501
|
+
"generate",
|
|
10502
|
+
i["input"],
|
|
10503
|
+
...i["model"] !== void 0 ? ["--model", i["model"]] : [],
|
|
10504
|
+
...i["bar"] !== void 0 ? ["--bar", i["bar"]] : [],
|
|
10505
|
+
...i["cap"] !== void 0 ? ["--cap", i["cap"]] : [],
|
|
10506
|
+
...i["yes"] === true ? ["--yes"] : []
|
|
10507
|
+
]
|
|
10508
|
+
}
|
|
10509
|
+
];
|
|
10510
|
+
BOOT_HASH = (() => {
|
|
10511
|
+
try {
|
|
10512
|
+
return sourceHash();
|
|
10513
|
+
} catch {
|
|
10514
|
+
return "unknown";
|
|
10515
|
+
}
|
|
10516
|
+
})();
|
|
10517
|
+
}
|
|
10518
|
+
});
|
|
10519
|
+
|
|
10520
|
+
// packages/cli/src/commands/permissions.ts
|
|
10521
|
+
var permissions_exports = {};
|
|
10522
|
+
__export(permissions_exports, {
|
|
10523
|
+
PERMISSIONS_DESCRIPTION: () => PERMISSIONS_DESCRIPTION,
|
|
10524
|
+
buildPermissions: () => buildPermissions,
|
|
10525
|
+
mergeAllowlist: () => mergeAllowlist,
|
|
10526
|
+
runPermissions: () => runPermissions
|
|
10527
|
+
});
|
|
10528
|
+
import { existsSync as existsSync29, mkdirSync as mkdirSync9, readFileSync as readFileSync26, writeFileSync as writeFileSync15 } from "node:fs";
|
|
10529
|
+
import os7 from "node:os";
|
|
10530
|
+
import path35 from "node:path";
|
|
10531
|
+
function mergeAllowlist(file, entries) {
|
|
10532
|
+
let settings = {};
|
|
10533
|
+
if (existsSync29(file) && readFileSync26(file, "utf8").trim() !== "") {
|
|
10534
|
+
settings = JSON.parse(readFileSync26(file, "utf8"));
|
|
10535
|
+
if (settings === null || typeof settings !== "object" || Array.isArray(settings)) throw new Error("settings root is not an object");
|
|
10536
|
+
}
|
|
10537
|
+
const permissions = settings["permissions"] ??= {};
|
|
10538
|
+
if (permissions === null || typeof permissions !== "object" || Array.isArray(permissions)) throw new Error("permissions is not an object");
|
|
10539
|
+
const allow = permissions["allow"] ??= [];
|
|
10540
|
+
if (!Array.isArray(allow)) throw new Error("permissions.allow is not an array");
|
|
10541
|
+
const present = new Set(allow.filter((x) => typeof x === "string"));
|
|
10542
|
+
const added = entries.filter((e) => !present.has(e));
|
|
10543
|
+
const alreadyPresent = entries.filter((e) => present.has(e));
|
|
10544
|
+
if (added.length > 0) {
|
|
10545
|
+
allow.push(...added);
|
|
10546
|
+
mkdirSync9(path35.dirname(file), { recursive: true });
|
|
10547
|
+
writeFileSync15(file, `${JSON.stringify(settings, null, 2)}
|
|
10548
|
+
`);
|
|
10549
|
+
}
|
|
10550
|
+
return { added, alreadyPresent };
|
|
10551
|
+
}
|
|
10552
|
+
async function buildPermissions(options) {
|
|
10553
|
+
const LEGAL_TOOL_NAME = /^[A-Za-z0-9_-]+$/;
|
|
10554
|
+
const pipeline = new Set(FIGMA_TOOL_FALLBACK);
|
|
10555
|
+
let figmaTools = FIGMA_TOOL_FALLBACK;
|
|
10556
|
+
try {
|
|
10557
|
+
const client = new McpHttpClient({ url: options.mcpUrl ?? DEFAULT_MCP_URL, ...options.fetchImpl ? { fetchImpl: options.fetchImpl } : {} });
|
|
10558
|
+
await client.initialize();
|
|
10559
|
+
const live = (await client.listTools()).map((t) => t.name).filter((n) => LEGAL_TOOL_NAME.test(n) && pipeline.has(n));
|
|
10560
|
+
if (live.length > 0) figmaTools = live;
|
|
10561
|
+
} catch {
|
|
10562
|
+
}
|
|
10563
|
+
return {
|
|
10564
|
+
host: "claude",
|
|
10565
|
+
serverEntries: [TENDRIL_PLUGIN_PREFIX, FIGMA_PLUGIN_PREFIX],
|
|
10566
|
+
toolEntries: [
|
|
10567
|
+
...TOOLS.map((t) => t.name).filter((n) => LEGAL_TOOL_NAME.test(n)).map((n) => `${TENDRIL_PLUGIN_PREFIX}__${n}`),
|
|
10568
|
+
...figmaTools.map((name) => `${FIGMA_PLUGIN_PREFIX}__${name}`)
|
|
10569
|
+
],
|
|
10570
|
+
serverEntriesCaution: "The server-wide entries allow EVERY tool those servers serve \u2014 for tendril that includes tendril_generate_curated (spends your OpenRouter budget when configured), and for Figma every tool the desktop server exposes, beyond the read tools this pipeline uses. The per-tool list is the recommended default.",
|
|
10571
|
+
directConfigNote: `Installed via claude mcp add instead of the plugin? Replace the prefixes: ${TENDRIL_PLUGIN_PREFIX}__<tool> becomes mcp__<your-server-name>__<tool> (same for Figma).`
|
|
10572
|
+
};
|
|
10573
|
+
}
|
|
10574
|
+
async function runPermissions(flags) {
|
|
10575
|
+
if (flags.describe) {
|
|
10576
|
+
printDescription(PERMISSIONS_DESCRIPTION);
|
|
10577
|
+
return;
|
|
10578
|
+
}
|
|
10579
|
+
const result = await buildPermissions({ ...flags.mcpUrl ? { mcpUrl: flags.mcpUrl } : {} });
|
|
10580
|
+
if (flags.write) {
|
|
10581
|
+
const base = process.env["INIT_CWD"] ?? process.cwd();
|
|
10582
|
+
const file = flags.user ? path35.join(os7.homedir(), ".claude", "settings.json") : path35.join(base, ".claude", "settings.local.json");
|
|
10583
|
+
if (flags.dryRun) {
|
|
10584
|
+
emitData(flags, { file, wouldAdd: result.toolEntries }, () => {
|
|
10585
|
+
process.stdout.write(`dry-run: would merge ${result.toolEntries.length} per-tool entries into ${file}
|
|
10586
|
+
`);
|
|
10587
|
+
});
|
|
10588
|
+
return;
|
|
10589
|
+
}
|
|
10590
|
+
try {
|
|
10591
|
+
const { added, alreadyPresent } = mergeAllowlist(file, result.toolEntries);
|
|
10592
|
+
emitData(flags, { ...result, written: { file, added, alreadyPresent } }, () => {
|
|
10593
|
+
process.stdout.write(
|
|
10594
|
+
added.length === 0 ? `already installed: all ${alreadyPresent.length} Tendril pipeline entries present in ${file}
|
|
10595
|
+
` : `installed: ${added.length} allowlist entr${added.length === 1 ? "y" : "ies"} added to ${file}${alreadyPresent.length > 0 ? ` (${alreadyPresent.length} already present)` : ""}
|
|
10596
|
+
restart or reopen the Claude Code session to pick up settings changes
|
|
10597
|
+
`
|
|
10598
|
+
);
|
|
10599
|
+
});
|
|
10600
|
+
} catch (err) {
|
|
10601
|
+
fail(flags, ExitCode.InputValidation, {
|
|
10602
|
+
error: `cannot rewrite ${file}: ${err instanceof Error ? err.message.split("\n")[0] : String(err)}`,
|
|
10603
|
+
code: "settings-unwritable",
|
|
10604
|
+
remediation: "The file is not plain JSON this command can faithfully re-emit (comments or an unexpected shape). Add the entries by hand: run without --write for the paste-ready block."
|
|
10605
|
+
});
|
|
10606
|
+
}
|
|
10607
|
+
return;
|
|
10608
|
+
}
|
|
10609
|
+
emitData(flags, result, () => {
|
|
10610
|
+
const quoted = (xs) => xs.map((x) => ` ${JSON.stringify(x)}`).join(",\n");
|
|
10611
|
+
process.stdout.write(
|
|
10612
|
+
`Claude Code allowlist for the Tendril pipeline.
|
|
10613
|
+
One command installs it (project-local, idempotent):
|
|
10614
|
+
|
|
10615
|
+
tendril permissions --claude --write
|
|
10616
|
+
|
|
10617
|
+
Or paste into .claude/settings.json under permissions.allow:
|
|
10618
|
+
|
|
10619
|
+
${quoted(result.toolEntries)}
|
|
10620
|
+
|
|
10621
|
+
Shorter but broader \u2014 one entry per server:
|
|
10622
|
+
|
|
10623
|
+
${quoted(result.serverEntries)}
|
|
10624
|
+
|
|
10625
|
+
\u26A0 ${result.serverEntriesCaution}
|
|
10626
|
+
|
|
10627
|
+
${result.directConfigNote}
|
|
10628
|
+
`
|
|
10629
|
+
);
|
|
10630
|
+
});
|
|
10631
|
+
}
|
|
10632
|
+
var FIGMA_TOOL_FALLBACK, PERMISSIONS_DESCRIPTION, TENDRIL_PLUGIN_PREFIX, FIGMA_PLUGIN_PREFIX;
|
|
10633
|
+
var init_permissions = __esm({
|
|
10634
|
+
"packages/cli/src/commands/permissions.ts"() {
|
|
10635
|
+
"use strict";
|
|
10636
|
+
init_src3();
|
|
10637
|
+
init_src();
|
|
10638
|
+
init_server();
|
|
10639
|
+
init_describe();
|
|
10640
|
+
init_output();
|
|
10641
|
+
init_doctor();
|
|
10642
|
+
FIGMA_TOOL_FALLBACK = ["get_metadata", "get_design_context", "get_screenshot", "get_variable_defs", "get_motion_context", "get_figjam"];
|
|
10643
|
+
PERMISSIONS_DESCRIPTION = {
|
|
10644
|
+
name: "permissions",
|
|
10645
|
+
summary: "Install (or print) the Claude Code permission allowlist for the Tendril pipeline (tendril + Figma MCP tools).",
|
|
10646
|
+
args: [],
|
|
10647
|
+
flags: [
|
|
10648
|
+
{ flag: "--claude", description: "Claude Code settings format (the default and currently only format)" },
|
|
10649
|
+
{ flag: "--write", description: "Merge the per-tool entries into .claude/settings.local.json in the current project (idempotent; creates the file; never touches other keys)" },
|
|
10650
|
+
{ flag: "--user", description: "With --write: target ~/.claude/settings.json (every project) instead of the project-local file" },
|
|
10651
|
+
{ flag: "--mcp-url <url>", description: "Figma MCP endpoint to list live tool names from", default: DEFAULT_MCP_URL },
|
|
10652
|
+
{ flag: "--json", description: "Machine-readable output" }
|
|
10653
|
+
],
|
|
10654
|
+
output: {
|
|
10655
|
+
host: '"claude"',
|
|
10656
|
+
serverEntries: "string[] \u2014 one entry per MCP server (allows every tool it serves)",
|
|
10657
|
+
toolEntries: "string[] \u2014 per-tool entries for selective allowlists",
|
|
10658
|
+
directConfigNote: "string \u2014 prefix rewrite for non-plugin (claude mcp add) installs",
|
|
10659
|
+
written: "with --write: { file, added, alreadyPresent }"
|
|
10660
|
+
},
|
|
10661
|
+
exitCodes: { 0: "printed or written", 3: "with --write: the target file exists but is not JSON this command can safely rewrite" },
|
|
10662
|
+
examples: ["tendril permissions --claude --write", "tendril permissions --claude", "tendril permissions --claude --json"]
|
|
10663
|
+
};
|
|
10664
|
+
TENDRIL_PLUGIN_PREFIX = "mcp__plugin_tendril_tendril";
|
|
10665
|
+
FIGMA_PLUGIN_PREFIX = "mcp__plugin_figma_figma";
|
|
10666
|
+
}
|
|
10667
|
+
});
|
|
10668
|
+
|
|
10669
|
+
// packages/cli/src/commands/inspect.ts
|
|
10670
|
+
var inspect_exports = {};
|
|
10671
|
+
__export(inspect_exports, {
|
|
10672
|
+
INSPECT_DESCRIPTION: () => INSPECT_DESCRIPTION,
|
|
10673
|
+
runInspect: () => runInspect
|
|
10674
|
+
});
|
|
10675
|
+
import { existsSync as existsSync30, readFileSync as readFileSync27, writeFileSync as writeFileSync16 } from "node:fs";
|
|
10676
|
+
import path36 from "node:path";
|
|
10677
|
+
async function runInspect(opts) {
|
|
10678
|
+
if (opts.describe) {
|
|
10679
|
+
printDescription(INSPECT_DESCRIPTION);
|
|
10680
|
+
return;
|
|
10681
|
+
}
|
|
10682
|
+
const bundleDir = path36.resolve(opts.bundleDir);
|
|
10683
|
+
const evidenceDir = path36.join(bundleDir, "verify-evidence");
|
|
10684
|
+
const manifestPath2 = path36.join(bundleDir, "component.json");
|
|
10685
|
+
if (!existsSync30(evidenceDir) || !existsSync30(manifestPath2)) {
|
|
10686
|
+
fail(opts, ExitCode.InputValidation, {
|
|
10687
|
+
error: `nothing to inspect in ${bundleDir} \u2014 ${existsSync30(manifestPath2) ? "no verify-evidence directory" : "no component.json"}`,
|
|
10688
|
+
code: "no-evidence",
|
|
10689
|
+
remediation: "Run `tendril verify <bundleDir>` first \u2014 inspect reads the ref/render evidence that run writes."
|
|
10690
|
+
});
|
|
10691
|
+
}
|
|
10692
|
+
const { manifest } = readBundleManifest(readFileSync27(manifestPath2, "utf8"));
|
|
10693
|
+
if (manifest === void 0) {
|
|
10694
|
+
fail(opts, ExitCode.InputValidation, {
|
|
10695
|
+
error: "component.json did not parse as a bundle manifest",
|
|
10696
|
+
code: "no-mount-contract",
|
|
10697
|
+
remediation: "Re-emit the bundle (score/verify rewrite component.json), then re-run inspect."
|
|
10698
|
+
});
|
|
10699
|
+
}
|
|
10700
|
+
const setDir = path36.resolve(opts.set ?? manifest.provenance.recordingSet.path);
|
|
10701
|
+
const reps = Object.keys(manifest.propAdapter).filter((rep) => existsSync30(path36.join(evidenceDir, `${rep}-ref.png`)) && existsSync30(path36.join(evidenceDir, `${rep}-render.png`)));
|
|
10702
|
+
if (reps.length === 0) {
|
|
10703
|
+
fail(opts, ExitCode.InputValidation, {
|
|
10704
|
+
error: "verify-evidence holds no ref/render pairs for this bundle's configs",
|
|
10705
|
+
code: "no-evidence",
|
|
10706
|
+
remediation: "Run `tendril verify <bundleDir>` first \u2014 inspect reads the ref/render evidence that run writes."
|
|
10707
|
+
});
|
|
10708
|
+
}
|
|
10709
|
+
let crops = 0;
|
|
10710
|
+
const sections = [];
|
|
10711
|
+
for (const rep of reps) {
|
|
10712
|
+
const ref = new Uint8Array(readFileSync27(path36.join(evidenceDir, `${rep}-ref.png`)));
|
|
10713
|
+
const render = new Uint8Array(readFileSync27(path36.join(evidenceDir, `${rep}-render.png`)));
|
|
10714
|
+
const nodes = smallSemanticNodes(setDir, rep, opts.maxArea ?? 1024).slice(0, 12);
|
|
10715
|
+
const cells = [];
|
|
10716
|
+
for (const [i, n] of nodes.entries()) {
|
|
10717
|
+
const rect = { x: n.x, y: n.y, w: n.w, h: n.h };
|
|
10718
|
+
try {
|
|
10719
|
+
writeFileSync16(path36.join(evidenceDir, `${rep}-inspect-${i}-ref.png`), zoomCrop(ref, rect));
|
|
10720
|
+
writeFileSync16(path36.join(evidenceDir, `${rep}-inspect-${i}-render.png`), zoomCrop(render, rect));
|
|
10721
|
+
} catch {
|
|
10722
|
+
continue;
|
|
10723
|
+
}
|
|
10724
|
+
crops += 1;
|
|
10725
|
+
cells.push(
|
|
10726
|
+
`<figure><figcaption>${esc(n.name)} <code>${esc(n.id)}</code> \xB7 ${n.w}\xD7${n.h}</figcaption><div class="pair"><span><em>recorded</em><img src="./${rep}-inspect-${i}-ref.png" alt="recorded ${esc(n.name)}"></span><span><em>rendered</em><img src="./${rep}-inspect-${i}-render.png" alt="rendered ${esc(n.name)}"></span></div></figure>`
|
|
10727
|
+
);
|
|
10728
|
+
}
|
|
10729
|
+
sections.push(
|
|
10730
|
+
`<section><h2>${esc(rep)}</h2><div class="full"><span><em>recorded</em><img src="./${rep}-ref.png"></span><span><em>rendered</em><img src="./${rep}-render.png"></span><span><em>diff</em><img src="./${rep}-diff.png"></span></div>` + (cells.length > 0 ? `<div class="grid">${cells.join("")}</div>` : `<p class="none">no small recorded nodes in this config's sweep</p>`) + `</section>`
|
|
10731
|
+
);
|
|
10732
|
+
}
|
|
10733
|
+
const sheet = path36.join(evidenceDir, "inspect.html");
|
|
10734
|
+
writeFileSync16(
|
|
10735
|
+
sheet,
|
|
10736
|
+
`<!doctype html><meta charset="utf-8"><title>${esc(manifest.name)} \u2014 tendril inspect</title><style>
|
|
10737
|
+
body{font:14px/1.45 system-ui,sans-serif;margin:24px;background:#fff;color:#111}
|
|
10738
|
+
h1{font-size:20px} h2{font-size:16px;border-top:1px solid #ddd;padding-top:16px}
|
|
10739
|
+
.pair,.full{display:flex;gap:12px;flex-wrap:wrap;align-items:flex-start}
|
|
10740
|
+
.full img{max-width:400px;border:1px solid #ccc} .pair img{border:1px solid #ccc;image-rendering:pixelated}
|
|
10741
|
+
figure{margin:0 0 16px} figcaption{margin-bottom:4px} em{display:block;color:#666;font-style:normal;font-size:12px}
|
|
10742
|
+
.grid{display:flex;flex-wrap:wrap;gap:20px;margin-top:12px} .none{color:#666}
|
|
10743
|
+
</style><h1>${esc(manifest.name)} \u2014 detail sheet (small recorded nodes, recorded vs rendered)</h1>
|
|
10744
|
+
<p>Every crop is a recorded node small enough that global metrics weight it as a rounding error.
|
|
10745
|
+
Scan the pairs: anything present on the left and missing/invisible on the right is a defect,
|
|
10746
|
+
whatever the scores said. Verdicts come from <code>tendril verify</code> \u2014 this sheet only shows.</p>
|
|
10747
|
+
${sections.join("\n")}
|
|
10748
|
+
`
|
|
10749
|
+
);
|
|
10750
|
+
emitData(opts, { sheet, configs: reps.length, crops }, () => {
|
|
10751
|
+
process.stdout.write(`inspect sheet: ${sheet}
|
|
10752
|
+
${reps.length} config(s), ${crops} detail crop pair(s) \u2014 open the sheet and scan recorded vs rendered
|
|
10753
|
+
`);
|
|
10754
|
+
});
|
|
10755
|
+
}
|
|
10756
|
+
var INSPECT_DESCRIPTION, esc;
|
|
10757
|
+
var init_inspect = __esm({
|
|
10758
|
+
"packages/cli/src/commands/inspect.ts"() {
|
|
10759
|
+
"use strict";
|
|
10760
|
+
init_src3();
|
|
10761
|
+
init_src6();
|
|
10762
|
+
init_src4();
|
|
10763
|
+
init_describe();
|
|
10764
|
+
init_output();
|
|
10765
|
+
INSPECT_DESCRIPTION = {
|
|
10766
|
+
name: "inspect",
|
|
10767
|
+
summary: "Build an eye-verifiable detail sheet from verify evidence: magnified ref-vs-render crops of every small recorded node (icons, controls, marks).",
|
|
10768
|
+
args: [{ name: "bundleDir", required: true, description: "bundle directory (must carry component.json and a verify-evidence dir from a prior `tendril verify`)" }],
|
|
10769
|
+
flags: [
|
|
10770
|
+
{ flag: "--set <dir>", description: "recording set override (default: the bundle's provenance path)" },
|
|
10771
|
+
{ flag: "--max-area <px2>", description: "node area ceiling for the detail sweep", default: "1024" },
|
|
10772
|
+
{ flag: "--json", description: "Machine-readable output" }
|
|
10773
|
+
],
|
|
10774
|
+
output: {
|
|
10775
|
+
sheet: "string \u2014 path to the generated inspect.html",
|
|
10776
|
+
configs: "number \u2014 configs with evidence found",
|
|
10777
|
+
crops: "number \u2014 detail crop pairs written"
|
|
10778
|
+
},
|
|
10779
|
+
exitCodes: { 0: "sheet written", 3: "no verify evidence to inspect (run `tendril verify` first)" },
|
|
10780
|
+
examples: ["tendril inspect ./src/components/Banner", "tendril inspect ./bundle --set ./tendril/recordings/banner"]
|
|
10781
|
+
};
|
|
10782
|
+
esc = (s) => s.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """);
|
|
10783
|
+
}
|
|
10784
|
+
});
|
|
10785
|
+
|
|
9663
10786
|
// packages/cli/src/commands/generate-route.ts
|
|
9664
10787
|
var generate_route_exports = {};
|
|
9665
10788
|
__export(generate_route_exports, {
|
|
@@ -9684,17 +10807,17 @@ __export(generate_recorded_exports, {
|
|
|
9684
10807
|
runGenerateRecorded: () => runGenerateRecorded
|
|
9685
10808
|
});
|
|
9686
10809
|
import { confirm as confirm3, isCancel as isCancel3 } from "@clack/prompts";
|
|
9687
|
-
import { existsSync as
|
|
9688
|
-
import
|
|
10810
|
+
import { existsSync as existsSync31, readFileSync as readFileSync28 } from "node:fs";
|
|
10811
|
+
import path37 from "node:path";
|
|
9689
10812
|
async function runGenerateRecorded(opts) {
|
|
9690
10813
|
const callerCwd = process.env["INIT_CWD"] ?? process.cwd();
|
|
9691
|
-
const outDirAbs =
|
|
9692
|
-
const recordedAsPath =
|
|
10814
|
+
const outDirAbs = path37.resolve(callerCwd, opts.out);
|
|
10815
|
+
const recordedAsPath = path37.resolve(callerCwd, opts.recorded);
|
|
9693
10816
|
let task;
|
|
9694
10817
|
let taskName;
|
|
9695
10818
|
let authoredApi;
|
|
9696
10819
|
let composition;
|
|
9697
|
-
const isSet =
|
|
10820
|
+
const isSet = existsSync31(path37.join(recordedAsPath, "recording-set.json"));
|
|
9698
10821
|
const registry = TASKS[opts.recorded];
|
|
9699
10822
|
if (registry !== void 0 && !isSet) {
|
|
9700
10823
|
task = registry;
|
|
@@ -9703,7 +10826,7 @@ async function runGenerateRecorded(opts) {
|
|
|
9703
10826
|
try {
|
|
9704
10827
|
const authored = authorTaskFromSet(recordedAsPath);
|
|
9705
10828
|
task = authored.task;
|
|
9706
|
-
taskName =
|
|
10829
|
+
taskName = path37.basename(recordedAsPath);
|
|
9707
10830
|
authoredApi = authored.api;
|
|
9708
10831
|
const roles = RolesSchema.safeParse(loadManifest(recordedAsPath).roles);
|
|
9709
10832
|
if (roles.success) composition = roles.data;
|
|
@@ -9729,8 +10852,15 @@ async function runGenerateRecorded(opts) {
|
|
|
9729
10852
|
remediation: fontsUnprovenRemediation(task.set)
|
|
9730
10853
|
});
|
|
9731
10854
|
}
|
|
10855
|
+
const substitutedFamilies = unprovisionedFamilies(task.set);
|
|
10856
|
+
if (substitutedFamilies.length > 0) {
|
|
10857
|
+
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`);
|
|
10858
|
+
}
|
|
10859
|
+
for (const g of missingWeights(task.set)) {
|
|
10860
|
+
warn(opts, `font weights (advisory): the recording implies weights ${g.declared.join(", ")} for "${g.family}" \u2014 cache provides ${g.provided.join(", ")}; nearest-weight rendering may shift stroke density (certification stays family-gated; implied weights can leak across families)`);
|
|
10861
|
+
}
|
|
9732
10862
|
const missing = task.configs.filter(
|
|
9733
|
-
(c) => !
|
|
10863
|
+
(c) => !existsSync31(path37.join(task.set, c.rep, "get_screenshot.json")) || !existsSync31(path37.join(task.set, c.rep, "get_metadata.json")) || !existsSync31(path37.join(task.set, c.rep, "get_design_context.json"))
|
|
9734
10864
|
);
|
|
9735
10865
|
if (missing.length > 0) {
|
|
9736
10866
|
fail(opts, ExitCode.RecordingIncomplete, {
|
|
@@ -9800,8 +10930,8 @@ async function runGenerateRecorded(opts) {
|
|
|
9800
10930
|
` : `${line}
|
|
9801
10931
|
`);
|
|
9802
10932
|
if (opts.dryRun) {
|
|
9803
|
-
emitData(opts, { dryRun: true, task: taskName, model: modelId, consent, wouldWrite:
|
|
9804
|
-
process.stdout.write(`dry-run: nothing sent, nothing written (would write ${
|
|
10933
|
+
emitData(opts, { dryRun: true, task: taskName, model: modelId, consent, wouldWrite: path37.join(outDirAbs, taskName) }, () => {
|
|
10934
|
+
process.stdout.write(`dry-run: nothing sent, nothing written (would write ${path37.join(outDirAbs, taskName)})
|
|
9805
10935
|
`);
|
|
9806
10936
|
});
|
|
9807
10937
|
return;
|
|
@@ -9824,10 +10954,10 @@ async function runGenerateRecorded(opts) {
|
|
|
9824
10954
|
});
|
|
9825
10955
|
}
|
|
9826
10956
|
}
|
|
9827
|
-
const bundleDir =
|
|
9828
|
-
if (
|
|
10957
|
+
const bundleDir = path37.join(outDirAbs, taskName);
|
|
10958
|
+
if (existsSync31(path37.join(bundleDir, "component.json"))) {
|
|
9829
10959
|
try {
|
|
9830
|
-
const prior = readBundleManifest(
|
|
10960
|
+
const prior = readBundleManifest(readFileSync28(path37.join(bundleDir, "component.json"), "utf8")).manifest;
|
|
9831
10961
|
if (prior !== void 0 && prior.provenance.recordingSet.hash !== recordingSetHash(task.set, task.configs)) {
|
|
9832
10962
|
fail(opts, ExitCode.InputValidation, {
|
|
9833
10963
|
error: `${bundleDir} already holds a bundle bound to recording set "${prior.provenance.recordingSet.path}" \u2014 generating here against a different set would silently rewrite its verification identity`,
|
|
@@ -9840,6 +10970,7 @@ async function runGenerateRecorded(opts) {
|
|
|
9840
10970
|
}
|
|
9841
10971
|
}
|
|
9842
10972
|
const result = await runEngineLoop({
|
|
10973
|
+
absentInkGates: opts.bar === "cert",
|
|
9843
10974
|
engine,
|
|
9844
10975
|
segments,
|
|
9845
10976
|
brief,
|
|
@@ -9859,12 +10990,13 @@ async function runGenerateRecorded(opts) {
|
|
|
9859
10990
|
});
|
|
9860
10991
|
const statuses = result.finalScores.map((s) => ({
|
|
9861
10992
|
...s,
|
|
9862
|
-
status: s.similarity >= BARS4.cert.sim && s.inkRecall >= BARS4.cert.ink ? "certified" : s.pass ? "pass" : "fail"
|
|
10993
|
+
status: (s.exact?.similarity ?? s.similarity) >= BARS4.cert.sim && (s.exact?.inkRecall ?? s.inkRecall) >= BARS4.cert.ink && substitutedFamilies.length === 0 && (s.absentInk?.length ?? 0) === 0 ? "certified" : s.pass ? "pass" : "fail"
|
|
9863
10994
|
}));
|
|
9864
10995
|
const behaviorFailures = result.finalBehaviors.filter((b) => !b.pass);
|
|
9865
10996
|
const pixelFailures = statuses.filter((s) => s.status === "fail");
|
|
9866
10997
|
const certified = statuses.filter((s) => s.status === "certified").length;
|
|
9867
|
-
const
|
|
10998
|
+
const absentAtCertBar = opts.bar === "cert" ? result.finalScores.filter((s) => (s.absentInk?.length ?? 0) > 0).map((s) => s.rep) : [];
|
|
10999
|
+
const ok = result.best !== void 0 && pixelFailures.length === 0 && behaviorFailures.length === 0 && absentAtCertBar.length === 0;
|
|
9868
11000
|
let trustStatement;
|
|
9869
11001
|
if (result.best !== void 0) {
|
|
9870
11002
|
const emitted = emitBundleV1({
|
|
@@ -9878,7 +11010,8 @@ async function runGenerateRecorded(opts) {
|
|
|
9878
11010
|
scores: result.finalScores,
|
|
9879
11011
|
behaviors: result.finalBehaviors,
|
|
9880
11012
|
environment: environmentStamp(taskFontFamilies(task.set)),
|
|
9881
|
-
spentUsd: result.spentUsd
|
|
11013
|
+
spentUsd: result.spentUsd,
|
|
11014
|
+
substitutedFamilies
|
|
9882
11015
|
});
|
|
9883
11016
|
trustStatement = emitted.manifest.trustStatement;
|
|
9884
11017
|
process.stderr.write(opts.json ? `${JSON.stringify({ status: emitted.statusLine })}
|
|
@@ -9925,7 +11058,25 @@ ${certified}/${statuses.length} certified \xB7 ${statuses.length - pixelFailures
|
|
|
9925
11058
|
`);
|
|
9926
11059
|
}
|
|
9927
11060
|
);
|
|
9928
|
-
if (
|
|
11061
|
+
if (opts.bar === "cert" && substitutedFamilies.length > 0) {
|
|
11062
|
+
const names = substitutedFamilies.map((f) => `"${f}"`).join(", ");
|
|
11063
|
+
const alsoAbsent = absentAtCertBar.length > 0 ? ` \u2014 and ${absentAtCertBar.length} config(s) also carry absent-ink clusters; certification requires fixing those too` : "";
|
|
11064
|
+
if (opts.json) {
|
|
11065
|
+
process.stderr.write(`${JSON.stringify({ error: `font cache lacks ${names} \u2014 certification never measures a substitute face (scores above are pass-capped)${alsoAbsent}`, code: "fonts-unproven", remediation: fontsUnprovenRemediation(task.set) })}
|
|
11066
|
+
`);
|
|
11067
|
+
} else {
|
|
11068
|
+
process.stderr.write(`error (fonts-unproven): font cache lacks ${names} \u2014 certification never measures a substitute face (scores above are pass-capped)${alsoAbsent}
|
|
11069
|
+
\u2192 ${fontsUnprovenRemediation(task.set)}
|
|
11070
|
+
`);
|
|
11071
|
+
}
|
|
11072
|
+
process.exitCode = ExitCode.FontsUnproven;
|
|
11073
|
+
return;
|
|
11074
|
+
}
|
|
11075
|
+
if (absentAtCertBar.length > 0) {
|
|
11076
|
+
warn(opts, `${absentAtCertBar.length} config(s) carry absent-ink clusters (${absentAtCertBar.join(", ")}) \u2014 certification never ships a missing or invisible feature (1B); fix the component or run at --bar pass for the disclosed result`);
|
|
11077
|
+
process.exitCode = ExitCode.VerificationFailed;
|
|
11078
|
+
}
|
|
11079
|
+
if (!ok && (pixelFailures.length > 0 || behaviorFailures.length > 0 || result.best === void 0)) {
|
|
9929
11080
|
warn(opts, `${pixelFailures.length} config(s) and ${behaviorFailures.length} behavior(s) below the ${opts.bar} bar \u2014 bundle written, verdict honest (Q4)`);
|
|
9930
11081
|
process.exitCode = ExitCode.VerificationFailed;
|
|
9931
11082
|
}
|
|
@@ -9958,195 +11109,17 @@ import { CommanderError } from "commander";
|
|
|
9958
11109
|
// packages/cli/src/program.ts
|
|
9959
11110
|
init_src3();
|
|
9960
11111
|
init_environment();
|
|
11112
|
+
init_doctor();
|
|
9961
11113
|
import { Command } from "commander";
|
|
9962
11114
|
|
|
9963
|
-
// packages/cli/src/commands/doctor.ts
|
|
9964
|
-
init_src4();
|
|
9965
|
-
init_src();
|
|
9966
|
-
import { existsSync as existsSync17, readFileSync as readFileSync14, readdirSync as readdirSync3 } from "node:fs";
|
|
9967
|
-
import os4 from "node:os";
|
|
9968
|
-
import path22 from "node:path";
|
|
9969
|
-
|
|
9970
|
-
// packages/cli/src/describe.ts
|
|
9971
|
-
var COMMON_EXIT_CODES = {
|
|
9972
|
-
0: "success",
|
|
9973
|
-
1: "general error",
|
|
9974
|
-
2: "authentication error",
|
|
9975
|
-
3: "input validation error",
|
|
9976
|
-
4: "confirmation required (re-run with --yes or answer the prompt)"
|
|
9977
|
-
};
|
|
9978
|
-
function printDescription(description) {
|
|
9979
|
-
process.stdout.write(`${JSON.stringify(description, null, 2)}
|
|
9980
|
-
`);
|
|
9981
|
-
}
|
|
9982
|
-
|
|
9983
|
-
// packages/cli/src/commands/doctor.ts
|
|
9984
|
-
init_env();
|
|
9985
|
-
init_environment();
|
|
9986
|
-
init_output();
|
|
9987
|
-
init_entitlement();
|
|
9988
|
-
var DEFAULT_MCP_URL = "http://127.0.0.1:3845/mcp";
|
|
9989
|
-
var DOCTOR_DESCRIPTION = {
|
|
9990
|
-
name: "doctor",
|
|
9991
|
-
summary: "Check whether this machine can run tendril generate end to end.",
|
|
9992
|
-
args: [],
|
|
9993
|
-
flags: [
|
|
9994
|
-
{ flag: "--mcp-url <url>", description: "Figma MCP endpoint", default: DEFAULT_MCP_URL },
|
|
9995
|
-
{ flag: "--json", description: "Machine-readable output" }
|
|
9996
|
-
],
|
|
9997
|
-
output: {
|
|
9998
|
-
ok: "boolean \u2014 Figma desktop MCP reachable AND a scoring browser found (fonts resolve lazily per design system; keys are informational)",
|
|
9999
|
-
checks: "[{ name, ok, detail, remediation? }]"
|
|
10000
|
-
},
|
|
10001
|
-
exitCodes: { 0: "healthy", 1: "one or more required checks failed" },
|
|
10002
|
-
examples: ["tendril doctor", "tendril doctor --json"]
|
|
10003
|
-
};
|
|
10004
|
-
async function runDoctorChecks(options) {
|
|
10005
|
-
const checks = [];
|
|
10006
|
-
const mcpUrl = options.mcpUrl ?? DEFAULT_MCP_URL;
|
|
10007
|
-
const client = new McpHttpClient({
|
|
10008
|
-
url: mcpUrl,
|
|
10009
|
-
...options.fetchImpl ? { fetchImpl: options.fetchImpl } : {}
|
|
10010
|
-
});
|
|
10011
|
-
try {
|
|
10012
|
-
const info = await client.initialize();
|
|
10013
|
-
const tools = await client.listTools();
|
|
10014
|
-
checks.push({
|
|
10015
|
-
name: "figma-desktop-mcp",
|
|
10016
|
-
ok: true,
|
|
10017
|
-
detail: `${info.name} ${info.version} (protocol ${info.protocolVersion}), ${tools.length} tools: ${tools.map((t) => t.name).join(", ")}`
|
|
10018
|
-
});
|
|
10019
|
-
} catch (err) {
|
|
10020
|
-
checks.push({
|
|
10021
|
-
name: "figma-desktop-mcp",
|
|
10022
|
-
ok: false,
|
|
10023
|
-
detail: err instanceof Error ? err.message : String(err),
|
|
10024
|
-
remediation: err instanceof McpUnavailableError ? "Open the Figma desktop app, sign in, then Figma menu \u2192 Preferences \u2192 Enable local MCP server (Dev or Full seat on a paid plan)." : "The server answered but the exchange failed \u2014 check the signed-in account's seat, then retry."
|
|
10025
|
-
});
|
|
10026
|
-
}
|
|
10027
|
-
const openrouterKey = resolveCredential("OPENROUTER_API_KEY");
|
|
10028
|
-
checks.push(
|
|
10029
|
-
openrouterKey ? { name: "openrouter-key", ok: true, detail: "OPENROUTER_API_KEY configured" } : {
|
|
10030
|
-
name: "openrouter-key",
|
|
10031
|
-
ok: false,
|
|
10032
|
-
detail: "OPENROUTER_API_KEY not set \u2014 optional; only the curated API-model engine needs it",
|
|
10033
|
-
remediation: "Run `tendril init` to store your OpenRouter key (BYOK) if you plan to use the curated engine."
|
|
10034
|
-
}
|
|
10035
|
-
);
|
|
10036
|
-
try {
|
|
10037
|
-
const chrome = resolveChrome();
|
|
10038
|
-
checks.push({ name: "browser", ok: true, detail: `${chromeVersion() ?? "version unknown"} at ${chrome}` });
|
|
10039
|
-
} catch (err) {
|
|
10040
|
-
checks.push({
|
|
10041
|
-
name: "browser",
|
|
10042
|
-
ok: false,
|
|
10043
|
-
detail: err instanceof Error ? err.message : String(err),
|
|
10044
|
-
remediation: "Install Google Chrome (or Chromium), or set CHROME_PATH to a Chromium-family browser binary."
|
|
10045
|
-
});
|
|
10046
|
-
}
|
|
10047
|
-
const fontManifest = path22.join(fontCacheDir(), "manifest.json");
|
|
10048
|
-
checks.push(
|
|
10049
|
-
existsSync17(fontManifest) ? { name: "font-cache", ok: true, detail: `resolved font cache at ${fontCacheDir()} (${JSON.parse(readFileSync14(fontManifest, "utf8")).length} faces)` } : {
|
|
10050
|
-
name: "font-cache",
|
|
10051
|
-
ok: true,
|
|
10052
|
-
detail: `font cache empty at ${fontCacheDir()} \u2014 normal on a fresh machine; faces resolve per design system at first use`,
|
|
10053
|
-
remediation: "Nothing to do now: `tendril fonts resolve --set <recording-dir>` fetches exactly what a recording declares, and generate/verify name that command when they need it."
|
|
10054
|
-
}
|
|
10055
|
-
);
|
|
10056
|
-
const pluginRoot = path22.join(os4.homedir(), ".claude", "plugins", "cache", "tendrilapp", "tendril");
|
|
10057
|
-
if (existsSync17(pluginRoot)) {
|
|
10058
|
-
try {
|
|
10059
|
-
const versions = readdirSync3(pluginRoot).filter((v) => /^\d+\.\d+\.\d+$/.test(v));
|
|
10060
|
-
const newest = versions.sort((a, b) => versionIsNewer(a, b) ? 1 : -1)[0];
|
|
10061
|
-
if (newest !== void 0) {
|
|
10062
|
-
const skewed = versionIsNewer(newest, cliVersion());
|
|
10063
|
-
checks.push(
|
|
10064
|
-
skewed ? {
|
|
10065
|
-
name: "plugin-skew",
|
|
10066
|
-
ok: false,
|
|
10067
|
-
detail: `Claude Code plugin cache holds v${newest} while this CLI is ${cliVersion()} \u2014 the plugin's skill/agents are STALE and will silently shadow current doctrine`,
|
|
10068
|
-
remediation: "Update the plugin \u2014 REFRESH THE MARKETPLACE FIRST (its local clone goes stale and a reinstall faithfully reinstalls the old version): terminal Claude Code \u2014 `claude plugin marketplace update tendrilapp` then `claude plugin update tendril`; VS Code extension \u2014 /plugins panel \u2192 Marketplaces tab \u2192 refresh tendrilapp, THEN Plugins tab \u2192 uninstall + reinstall tendril, reopen the chat panel. If the refresh doesn't take, remove the tendrilapp marketplace entirely and re-add TendrilApp/claude-plugin (a fresh clone cannot be stale)."
|
|
10069
|
-
} : { name: "plugin-skew", ok: true, detail: `Claude Code plugin v${newest} matches this CLI` }
|
|
10070
|
-
);
|
|
10071
|
-
}
|
|
10072
|
-
} catch {
|
|
10073
|
-
}
|
|
10074
|
-
}
|
|
10075
|
-
const ent = checkEntitlement();
|
|
10076
|
-
checks.push(
|
|
10077
|
-
ent.ok ? ent.mode === "pre-launch" ? { name: "entitlement", ok: true, detail: "pre-launch build \u2014 entitlement gates present but unarmed (no public key shipped); record/generate run freely" } : { name: "entitlement", ok: true, detail: `active plan "${ent.claims.plan}" until ${new Date(ent.claims.exp).toISOString().slice(0, 10)}${ent.stale ? " (stale \u2014 renews at next opportunity)" : ""}` } : { name: "entitlement", ok: false, detail: ent.error, remediation: ent.remediation }
|
|
10078
|
-
);
|
|
10079
|
-
const figmaToken = resolveCredential("FIGMA_TOKEN");
|
|
10080
|
-
checks.push({
|
|
10081
|
-
name: "figma-pat",
|
|
10082
|
-
ok: true,
|
|
10083
|
-
detail: figmaToken ? "FIGMA_TOKEN configured (REST fallback available)" : "FIGMA_TOKEN not set \u2014 optional; only needed for the REST fallback transport"
|
|
10084
|
-
});
|
|
10085
|
-
return { ok: checks.filter((c) => c.name !== "figma-pat" && c.name !== "openrouter-key").every((c) => c.ok), checks };
|
|
10086
|
-
}
|
|
10087
|
-
function versionIsNewer(a, b) {
|
|
10088
|
-
const pa = a.split(".").map(Number);
|
|
10089
|
-
const pb = b.split(".").map(Number);
|
|
10090
|
-
for (let i = 0; i < 3; i++) {
|
|
10091
|
-
if ((pb[i] ?? 0) > (pa[i] ?? 0)) return true;
|
|
10092
|
-
if ((pb[i] ?? 0) < (pa[i] ?? 0)) return false;
|
|
10093
|
-
}
|
|
10094
|
-
return false;
|
|
10095
|
-
}
|
|
10096
|
-
async function latestVersionInfo() {
|
|
10097
|
-
try {
|
|
10098
|
-
const ctl = new AbortController();
|
|
10099
|
-
const timer = setTimeout(() => ctl.abort(), 2500);
|
|
10100
|
-
const res = await fetch("https://registry.npmjs.org/@tendrilapp%2fcli", { signal: ctl.signal });
|
|
10101
|
-
clearTimeout(timer);
|
|
10102
|
-
if (!res.ok) return null;
|
|
10103
|
-
const doc = await res.json();
|
|
10104
|
-
const latest = doc["dist-tags"]?.latest;
|
|
10105
|
-
if (latest === void 0) return null;
|
|
10106
|
-
const stamp = doc.time?.[latest];
|
|
10107
|
-
return { latest, ...stamp !== void 0 ? { publishedAt: stamp.slice(0, 10) } : {} };
|
|
10108
|
-
} catch {
|
|
10109
|
-
return null;
|
|
10110
|
-
}
|
|
10111
|
-
}
|
|
10112
|
-
async function runDoctor(flags) {
|
|
10113
|
-
if (flags.describe) {
|
|
10114
|
-
printDescription(DOCTOR_DESCRIPTION);
|
|
10115
|
-
return;
|
|
10116
|
-
}
|
|
10117
|
-
const [report, latest] = await Promise.all([runDoctorChecks({ ...flags.mcpUrl ? { mcpUrl: flags.mcpUrl } : {} }), latestVersionInfo()]);
|
|
10118
|
-
emitData(flags, { version: cliVersion(), ...latest !== null ? { latest: latest.latest, latestPublishedAt: latest.publishedAt ?? null, updateAvailable: versionIsNewer(cliVersion(), latest.latest) } : {}, ...report }, () => {
|
|
10119
|
-
const version = cliVersion();
|
|
10120
|
-
if (latest === null) {
|
|
10121
|
-
process.stdout.write(`tendril ${version} (latest: unreachable)
|
|
10122
|
-
`);
|
|
10123
|
-
} else if (!versionIsNewer(version, latest.latest)) {
|
|
10124
|
-
process.stdout.write(`tendril ${version} (${latest.latest === version ? "latest" : `ahead of registry ${latest.latest}`}${latest.publishedAt !== void 0 ? `, published ${latest.publishedAt}` : ""})
|
|
10125
|
-
`);
|
|
10126
|
-
} else {
|
|
10127
|
-
process.stdout.write(`tendril ${version} \u2014 UPDATE AVAILABLE: ${latest.latest}${latest.publishedAt !== void 0 ? ` (published ${latest.publishedAt})` : ""}
|
|
10128
|
-
`);
|
|
10129
|
-
process.stdout.write(` \u2192 npm install -g @tendrilapp/cli@latest (plugin MCP users update automatically at next session; a stale npx cache clears with npm cache clean --force)
|
|
10130
|
-
`);
|
|
10131
|
-
}
|
|
10132
|
-
for (const check of report.checks) {
|
|
10133
|
-
process.stdout.write(`${check.ok ? "\u2713" : "\u2717"} ${check.name}: ${check.detail}
|
|
10134
|
-
`);
|
|
10135
|
-
if (check.remediation) process.stdout.write(` \u2192 ${check.remediation}
|
|
10136
|
-
`);
|
|
10137
|
-
}
|
|
10138
|
-
process.stdout.write(report.ok ? "\nready to generate\n" : "\nnot ready \u2014 fix the items above\n");
|
|
10139
|
-
});
|
|
10140
|
-
if (!report.ok) process.exit(1);
|
|
10141
|
-
}
|
|
10142
|
-
|
|
10143
11115
|
// packages/cli/src/commands/init.ts
|
|
10144
11116
|
init_src3();
|
|
11117
|
+
init_describe();
|
|
11118
|
+
init_env();
|
|
11119
|
+
init_output();
|
|
10145
11120
|
import { intro, isCancel, outro, password } from "@clack/prompts";
|
|
10146
11121
|
import fs from "node:fs";
|
|
10147
11122
|
import path23 from "node:path";
|
|
10148
|
-
init_env();
|
|
10149
|
-
init_output();
|
|
10150
11123
|
var INIT_DESCRIPTION = {
|
|
10151
11124
|
name: "init",
|
|
10152
11125
|
summary: "Configure Figma and OpenRouter credentials in .env (idempotent).",
|
|
@@ -10254,11 +11227,12 @@ init_src3();
|
|
|
10254
11227
|
init_src();
|
|
10255
11228
|
init_src5();
|
|
10256
11229
|
init_src2();
|
|
10257
|
-
|
|
10258
|
-
import { readFileSync as readFileSync15, readdirSync as readdirSync4, existsSync as existsSync18 } from "node:fs";
|
|
11230
|
+
init_describe();
|
|
10259
11231
|
init_env();
|
|
10260
11232
|
init_output();
|
|
10261
11233
|
init_entitlement();
|
|
11234
|
+
import { confirm as confirm2, isCancel as isCancel2 } from "@clack/prompts";
|
|
11235
|
+
import { readFileSync as readFileSync15, readdirSync as readdirSync4, existsSync as existsSync18 } from "node:fs";
|
|
10262
11236
|
|
|
10263
11237
|
// packages/cli/src/pipeline.ts
|
|
10264
11238
|
init_src2();
|
|
@@ -11005,7 +11979,7 @@ function buildProgram() {
|
|
|
11005
11979
|
await runActivate2({ ...flags, serviceUrl: local["serviceUrl"] });
|
|
11006
11980
|
});
|
|
11007
11981
|
const record = program.command("record").description("Agent-driven recording protocol: plan the queue, get instructions, ingest verbatim envelopes, resume from disk.");
|
|
11008
|
-
record.command("plan").requiredOption("--set <dir>", "recording set directory").requiredOption("--component <name>", "component/system name").
|
|
11982
|
+
record.command("plan").requiredOption("--set <dir>", "recording set directory").requiredOption("--component <name>", "component/system name").option("--metadata <file...>", "verbatim get_metadata envelope(s), optionally <file>@<frameId>").option("--metadata-raw-file <file>", "the get_metadata response TEXT exactly as received (single block); the CLI builds the envelope itself").option("--metadata-raw-parts-file <file>", "a JSON array of the get_metadata response blocks, each verbatim, in order (multi-block responses)").option("--default <axis=value...>", "explicit axis default, e.g. --default State=Rest (repeatable; persisted to the manifest; may re-plan an unrecorded set)").option("--component-set <name>", "when the metadata holds several component sets, record only this one (name = the set label in the plan error)").option("--sample", "cost sampling: anchor + one-factor + conflict crosses only (the FULL variant matrix is the default; sampling is blind to multi-axis interactions)").action(async (_o, cmd) => {
|
|
11009
11983
|
const flags = globalFlags(cmd.parent.parent);
|
|
11010
11984
|
const local = cmd.opts();
|
|
11011
11985
|
const { runRecordPlan: runRecordPlan2 } = await Promise.resolve().then(() => (init_record(), record_exports));
|
|
@@ -11013,8 +11987,10 @@ function buildProgram() {
|
|
|
11013
11987
|
...flags,
|
|
11014
11988
|
setDir: local["set"],
|
|
11015
11989
|
component: local["component"],
|
|
11016
|
-
metadataFiles: local["metadata"],
|
|
11017
11990
|
sample: local["sample"],
|
|
11991
|
+
...local["metadata"] !== void 0 ? { metadataFiles: local["metadata"] } : {},
|
|
11992
|
+
...local["metadataRawFile"] !== void 0 ? { metadataRawFile: local["metadataRawFile"] } : {},
|
|
11993
|
+
...local["metadataRawPartsFile"] !== void 0 ? { metadataRawPartsFile: local["metadataRawPartsFile"] } : {},
|
|
11018
11994
|
...local["default"] !== void 0 ? { defaultSpecs: local["default"] } : {},
|
|
11019
11995
|
...local["componentSet"] !== void 0 ? { componentSet: local["componentSet"] } : {}
|
|
11020
11996
|
});
|
|
@@ -11146,6 +12122,28 @@ function buildProgram() {
|
|
|
11146
12122
|
...local["out"] !== void 0 ? { out: local["out"] } : {}
|
|
11147
12123
|
});
|
|
11148
12124
|
});
|
|
12125
|
+
program.command("permissions").description("Install (or print) the Claude Code permission allowlist for the Tendril pipeline (tendril + Figma MCP tools) \u2014 no more transcribing tool names from prompts.").option("--claude", "Claude Code settings format (the default and currently only format)").option("--write", "merge the per-tool entries into .claude/settings.local.json in the current project (idempotent; never touches other keys)").option("--user", "with --write: target ~/.claude/settings.json (every project) instead").option("--mcp-url <url>", "Figma MCP endpoint to list live tool names from").action(async (_o, cmd) => {
|
|
12126
|
+
const flags = globalFlags(cmd.parent);
|
|
12127
|
+
const local = cmd.opts();
|
|
12128
|
+
const { runPermissions: runPermissions2 } = await Promise.resolve().then(() => (init_permissions(), permissions_exports));
|
|
12129
|
+
await runPermissions2({
|
|
12130
|
+
...flags,
|
|
12131
|
+
...local["write"] !== void 0 ? { write: local["write"] } : {},
|
|
12132
|
+
...local["user"] !== void 0 ? { user: local["user"] } : {},
|
|
12133
|
+
...local["mcpUrl"] !== void 0 ? { mcpUrl: local["mcpUrl"] } : {}
|
|
12134
|
+
});
|
|
12135
|
+
});
|
|
12136
|
+
program.command("inspect").description("Build an eye-verifiable detail sheet from verify evidence: magnified recorded-vs-rendered crops of every small recorded node (icons, controls, marks) plus full-frame triples, in one static HTML page.").argument("<bundleDir>", "bundle directory with component.json and verify-evidence").option("--set <dir>", "recording set override (default: the bundle's provenance path)").option("--max-area <px2>", "node area ceiling for the detail sweep", "1024").action(async (bundleDir, _o, cmd) => {
|
|
12137
|
+
const flags = globalFlags(cmd.parent);
|
|
12138
|
+
const local = cmd.opts();
|
|
12139
|
+
const { runInspect: runInspect2 } = await Promise.resolve().then(() => (init_inspect(), inspect_exports));
|
|
12140
|
+
await runInspect2({
|
|
12141
|
+
...flags,
|
|
12142
|
+
bundleDir,
|
|
12143
|
+
...local["set"] !== void 0 ? { set: local["set"] } : {},
|
|
12144
|
+
...local["maxArea"] !== void 0 ? { maxArea: Number(local["maxArea"]) } : {}
|
|
12145
|
+
});
|
|
12146
|
+
});
|
|
11149
12147
|
program.command("verify").description("Recompute verification for a generated bundle against its recording set (per-config status, behaviors, honest exit codes).").argument("<bundleDir>", "bundle directory to verify").option("--task <name>", "legacy task adapter for bundles WITHOUT component.json (bundle v1 carries its own prop manifest)").option("--set <dir>", "recording set directory (overrides the bundle's provenance path)").option("--bar <bar>", "target bar: pass (0.95) or cert (0.97)", "pass").action(async (bundleDir, _opts, cmd) => {
|
|
11150
12148
|
const flags = globalFlags(cmd);
|
|
11151
12149
|
const local = cmd.opts();
|