@tendrilapp/cli 0.1.27 → 0.1.29
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 +50 -15
- package/dist/tendril-mcp.js +13 -5
- package/dist/tendril.js +1033 -476
- package/package.json +1 -1
package/dist/tendril.js
CHANGED
|
@@ -1083,14 +1083,54 @@ function planSet(setDir, component, symbols, opts = {}) {
|
|
|
1083
1083
|
writeManifest(setDir, manifest);
|
|
1084
1084
|
return { manifest, plan, resumed: false };
|
|
1085
1085
|
}
|
|
1086
|
+
function usableEnvelope(file, tool) {
|
|
1087
|
+
let payload;
|
|
1088
|
+
try {
|
|
1089
|
+
payload = JSON.parse(readFileSync(file, "utf8"));
|
|
1090
|
+
} catch {
|
|
1091
|
+
return { ok: false, reason: "the recorded file is not JSON" };
|
|
1092
|
+
}
|
|
1093
|
+
const schema = tool === "get_screenshot" ? ImageEnvelopeSchema : TextEnvelopeSchema;
|
|
1094
|
+
if (!schema.safeParse(payload).success) {
|
|
1095
|
+
return { ok: false, reason: "not a verbatim tool envelope (structure the ingest boundary would refuse)" };
|
|
1096
|
+
}
|
|
1097
|
+
const content = checkEnvelopeContent(tool, payload);
|
|
1098
|
+
if (!content.ok) return { ok: false, reason: content.reason };
|
|
1099
|
+
if (tool === "get_metadata" && !/name="/.test(envelopeTextContent(payload))) {
|
|
1100
|
+
return { ok: false, reason: "node markup carries no name attribute \u2014 task authoring cannot derive the pose" };
|
|
1101
|
+
}
|
|
1102
|
+
return { ok: true };
|
|
1103
|
+
}
|
|
1086
1104
|
function sessionStatus(setDir) {
|
|
1087
1105
|
const manifest = loadManifest(setDir);
|
|
1088
1106
|
const reps = manifest.reps.map((rep) => {
|
|
1089
|
-
const
|
|
1090
|
-
const
|
|
1091
|
-
|
|
1107
|
+
const required = requiredToolsFor(manifest, rep.slug);
|
|
1108
|
+
const invalid = [];
|
|
1109
|
+
const recorded = RECORD_TOOLS.filter((t) => {
|
|
1110
|
+
const file = path.join(setDir, rep.slug, `${t}.json`);
|
|
1111
|
+
if (!existsSync(file)) return false;
|
|
1112
|
+
const usable = usableEnvelope(file, t);
|
|
1113
|
+
if (!usable.ok) {
|
|
1114
|
+
invalid.push({
|
|
1115
|
+
tool: t,
|
|
1116
|
+
reason: required.includes(t) ? usable.reason : `${usable.reason} (optional for this rep \u2014 not counted against completeness; re-record only if you need this evidence)`
|
|
1117
|
+
});
|
|
1118
|
+
return false;
|
|
1119
|
+
}
|
|
1120
|
+
return true;
|
|
1121
|
+
});
|
|
1122
|
+
const missing = byProtocolOrder(required.filter((t) => !recorded.includes(t)));
|
|
1123
|
+
return { slug: rep.slug, nodeId: rep.nodeId, recorded, missing, ...invalid.length > 0 ? { invalid } : {} };
|
|
1092
1124
|
});
|
|
1093
|
-
|
|
1125
|
+
const setDefsFile = path.join(setDir, "get_variable_defs.json");
|
|
1126
|
+
const setDefs = existsSync(setDefsFile) ? usableEnvelope(setDefsFile, "get_variable_defs") : { ok: false, reason: "never recorded" };
|
|
1127
|
+
const setTokenMapRecorded = setDefs.ok;
|
|
1128
|
+
return {
|
|
1129
|
+
reps,
|
|
1130
|
+
setTokenMapRecorded,
|
|
1131
|
+
...!setDefs.ok && setDefs.reason !== "never recorded" ? { setTokenMapInvalid: setDefs.reason } : {},
|
|
1132
|
+
complete: reps.every((r) => r.missing.length === 0) && setTokenMapRecorded
|
|
1133
|
+
};
|
|
1094
1134
|
}
|
|
1095
1135
|
function nextInstruction(setDir) {
|
|
1096
1136
|
const { reps } = sessionStatus(setDir);
|
|
@@ -1337,8 +1377,8 @@ var init_src = __esm({
|
|
|
1337
1377
|
function variableNameToPath(name) {
|
|
1338
1378
|
return name.split("/").map(canonicalCssIdentPart).filter((part) => part.length > 0);
|
|
1339
1379
|
}
|
|
1340
|
-
function tokenPathToCssVar(
|
|
1341
|
-
return `--${
|
|
1380
|
+
function tokenPathToCssVar(path41) {
|
|
1381
|
+
return `--${path41.join("-")}`;
|
|
1342
1382
|
}
|
|
1343
1383
|
function toDtcgToken(variable, defaultMode) {
|
|
1344
1384
|
const modes = Object.keys(variable.valuesByMode);
|
|
@@ -1382,11 +1422,11 @@ function toDtcgToken(variable, defaultMode) {
|
|
|
1382
1422
|
}
|
|
1383
1423
|
function mapVariablesToDtcg(variables, defaultMode = "light") {
|
|
1384
1424
|
const entries = variables.map((variable) => {
|
|
1385
|
-
const
|
|
1386
|
-
if (
|
|
1425
|
+
const path41 = variableNameToPath(variable.name);
|
|
1426
|
+
if (path41.length === 0) {
|
|
1387
1427
|
throw new DtcgMappingError("empty-name", `Figma variable ${variable.id} has an empty name`);
|
|
1388
1428
|
}
|
|
1389
|
-
return { variable, path:
|
|
1429
|
+
return { variable, path: path41 };
|
|
1390
1430
|
});
|
|
1391
1431
|
const groupPrefixes = /* @__PURE__ */ new Set();
|
|
1392
1432
|
for (const e of entries) {
|
|
@@ -1407,21 +1447,21 @@ function mapVariablesToDtcg(variables, defaultMode = "light") {
|
|
|
1407
1447
|
}
|
|
1408
1448
|
const tokens = {};
|
|
1409
1449
|
const flat = [];
|
|
1410
|
-
for (const { variable, path:
|
|
1450
|
+
for (const { variable, path: path41 } of entries) {
|
|
1411
1451
|
const token = toDtcgToken(variable, defaultMode);
|
|
1412
1452
|
let group = tokens;
|
|
1413
|
-
for (const segment of
|
|
1453
|
+
for (const segment of path41.slice(0, -1)) {
|
|
1414
1454
|
const existing = group[segment];
|
|
1415
1455
|
group = existing ?? (group[segment] = {});
|
|
1416
1456
|
}
|
|
1417
|
-
const leaf =
|
|
1457
|
+
const leaf = path41[path41.length - 1];
|
|
1418
1458
|
if (group[leaf] !== void 0) {
|
|
1419
|
-
throw new DtcgMappingError("duplicate-path", `Duplicate token path "${
|
|
1459
|
+
throw new DtcgMappingError("duplicate-path", `Duplicate token path "${path41.join(".")}" (variable ${variable.id})`);
|
|
1420
1460
|
}
|
|
1421
1461
|
group[leaf] = token;
|
|
1422
1462
|
flat.push({
|
|
1423
|
-
path:
|
|
1424
|
-
cssVar: tokenPathToCssVar(
|
|
1463
|
+
path: path41.join("."),
|
|
1464
|
+
cssVar: tokenPathToCssVar(path41),
|
|
1425
1465
|
type: token.$type,
|
|
1426
1466
|
value: token.$value
|
|
1427
1467
|
});
|
|
@@ -1610,9 +1650,9 @@ function boundId(value) {
|
|
|
1610
1650
|
return isObject(value) && typeof value["boundVariableId"] === "string" ? value["boundVariableId"] : void 0;
|
|
1611
1651
|
}
|
|
1612
1652
|
function resolveBinding(ctx, id) {
|
|
1613
|
-
const
|
|
1614
|
-
if (
|
|
1615
|
-
return
|
|
1653
|
+
const path41 = ctx.pathById.get(id);
|
|
1654
|
+
if (path41 === void 0) ctx.unresolved.add(id);
|
|
1655
|
+
return path41;
|
|
1616
1656
|
}
|
|
1617
1657
|
function parseVariantProps(name) {
|
|
1618
1658
|
if (!name.includes("=")) return void 0;
|
|
@@ -1647,8 +1687,8 @@ function walk(ctx, raw) {
|
|
|
1647
1687
|
if (!isObject(paint) || paint["visible"] === false) continue;
|
|
1648
1688
|
const id = boundId(paint);
|
|
1649
1689
|
if (id !== void 0) {
|
|
1650
|
-
const
|
|
1651
|
-
if (
|
|
1690
|
+
const path41 = resolveBinding(ctx, id);
|
|
1691
|
+
if (path41 !== void 0) tokens.add(path41);
|
|
1652
1692
|
} else if (typeof paint["color"] === "string") {
|
|
1653
1693
|
ctx.hardcoded.push({ node: name, property, value: paint["color"] });
|
|
1654
1694
|
}
|
|
@@ -1656,8 +1696,8 @@ function walk(ctx, raw) {
|
|
|
1656
1696
|
}
|
|
1657
1697
|
const radiusId = boundId(raw["cornerRadius"]);
|
|
1658
1698
|
if (radiusId !== void 0) {
|
|
1659
|
-
const
|
|
1660
|
-
if (
|
|
1699
|
+
const path41 = resolveBinding(ctx, radiusId);
|
|
1700
|
+
if (path41 !== void 0) tokens.add(path41);
|
|
1661
1701
|
} else if (typeof raw["cornerRadius"] === "number" && raw["cornerRadius"] !== 0) {
|
|
1662
1702
|
ctx.hardcoded.push({ node: name, property: "border-radius", value: `${raw["cornerRadius"]}px` });
|
|
1663
1703
|
}
|
|
@@ -1667,10 +1707,10 @@ function walk(ctx, raw) {
|
|
|
1667
1707
|
layout = { mode: layoutMode === "HORIZONTAL" ? "flex-row" : "flex-column" };
|
|
1668
1708
|
const gapId = boundId(raw["itemSpacing"]);
|
|
1669
1709
|
if (gapId !== void 0) {
|
|
1670
|
-
const
|
|
1671
|
-
if (
|
|
1672
|
-
layout.gap =
|
|
1673
|
-
tokens.add(
|
|
1710
|
+
const path41 = resolveBinding(ctx, gapId);
|
|
1711
|
+
if (path41 !== void 0) {
|
|
1712
|
+
layout.gap = path41;
|
|
1713
|
+
tokens.add(path41);
|
|
1674
1714
|
}
|
|
1675
1715
|
} else if (typeof raw["itemSpacing"] === "number" && raw["itemSpacing"] !== 0) {
|
|
1676
1716
|
ctx.hardcoded.push({ node: name, property: "gap", value: `${raw["itemSpacing"]}px` });
|
|
@@ -1679,10 +1719,10 @@ function walk(ctx, raw) {
|
|
|
1679
1719
|
for (const field of PADDING_FIELDS) {
|
|
1680
1720
|
const id = boundId(raw[field]);
|
|
1681
1721
|
if (id !== void 0) {
|
|
1682
|
-
const
|
|
1683
|
-
if (
|
|
1684
|
-
paddingPaths.push(
|
|
1685
|
-
tokens.add(
|
|
1722
|
+
const path41 = resolveBinding(ctx, id);
|
|
1723
|
+
if (path41 !== void 0) {
|
|
1724
|
+
paddingPaths.push(path41);
|
|
1725
|
+
tokens.add(path41);
|
|
1686
1726
|
}
|
|
1687
1727
|
} else if (typeof raw[field] === "number" && raw[field] !== 0) {
|
|
1688
1728
|
ctx.hardcoded.push({ node: name, property: field, value: `${raw[field]}px` });
|
|
@@ -3498,6 +3538,7 @@ function isCollection(bytes) {
|
|
|
3498
3538
|
}
|
|
3499
3539
|
function nameTableStrings(view, bytes, nameOffset, nameLength) {
|
|
3500
3540
|
const out = /* @__PURE__ */ new Map();
|
|
3541
|
+
const rank = /* @__PURE__ */ new Map();
|
|
3501
3542
|
if (nameOffset + 6 > bytes.length) return out;
|
|
3502
3543
|
const count = view.getUint16(nameOffset + 2);
|
|
3503
3544
|
const stringOffset = nameOffset + view.getUint16(nameOffset + 4);
|
|
@@ -3511,14 +3552,21 @@ function nameTableStrings(view, bytes, nameOffset, nameLength) {
|
|
|
3511
3552
|
const offset = stringOffset + view.getUint16(rec + 10);
|
|
3512
3553
|
if (offset + length > bytes.length || offset + length > nameOffset + nameLength) continue;
|
|
3513
3554
|
const slice = bytes.subarray(offset, offset + length);
|
|
3514
|
-
const
|
|
3555
|
+
const languageId = view.getUint16(rec + 4);
|
|
3515
3556
|
let value = "";
|
|
3516
|
-
if (
|
|
3557
|
+
if (platformId === 3 || platformId === 0) {
|
|
3517
3558
|
for (let j = 0; j + 1 < slice.length; j += 2) value += String.fromCharCode(slice[j] << 8 | slice[j + 1]);
|
|
3518
|
-
} else {
|
|
3559
|
+
} else if (platformId === 1 && encodingId === 0) {
|
|
3519
3560
|
for (const b of slice) value += String.fromCharCode(b);
|
|
3561
|
+
} else {
|
|
3562
|
+
continue;
|
|
3563
|
+
}
|
|
3564
|
+
if (value === "") continue;
|
|
3565
|
+
const score = platformId === 3 && languageId === 1033 ? 5 : platformId === 0 ? 4 : platformId === 1 && languageId === 0 ? 3 : platformId === 3 ? 2 : 1;
|
|
3566
|
+
if (score > (rank.get(nameId) ?? 0)) {
|
|
3567
|
+
rank.set(nameId, score);
|
|
3568
|
+
out.set(nameId, value);
|
|
3520
3569
|
}
|
|
3521
|
-
if (value !== "" && !out.has(nameId)) out.set(nameId, value);
|
|
3522
3570
|
}
|
|
3523
3571
|
return out;
|
|
3524
3572
|
}
|
|
@@ -3598,15 +3646,154 @@ var init_font_collection = __esm({
|
|
|
3598
3646
|
}
|
|
3599
3647
|
});
|
|
3600
3648
|
|
|
3601
|
-
// packages/verify/src/font-
|
|
3602
|
-
import {
|
|
3603
|
-
import { existsSync as existsSync6, mkdirSync as mkdirSync2, readFileSync as readFileSync3, writeFileSync as writeFileSync3 } from "node:fs";
|
|
3649
|
+
// packages/verify/src/font-discovery.ts
|
|
3650
|
+
import { existsSync as existsSync6, readdirSync as readdirSync2, readFileSync as readFileSync3, realpathSync as realpathSync2 } from "node:fs";
|
|
3604
3651
|
import os2 from "node:os";
|
|
3605
3652
|
import path10 from "node:path";
|
|
3653
|
+
function weightFromSubfamily(subfamily) {
|
|
3654
|
+
for (const [re, w] of WEIGHT_TOKENS) if (re.test(subfamily)) return w;
|
|
3655
|
+
return void 0;
|
|
3656
|
+
}
|
|
3657
|
+
function systemFaceRefusal(family) {
|
|
3658
|
+
if (/^\.?SF(NS|[ -]|$)/i.test(family) || /^San Francisco/i.test(family) || /^\.?Apple/i.test(family) || /^\.?New York\b/i.test(family) || /^\.?System Font\b/i.test(family)) {
|
|
3659
|
+
return "Apple's bundled system faces (San Francisco, New York, the Apple-named families) are licensed only for mock-ups of interfaces running on Apple operating systems, by registered Apple developers \u2014 a grant Tendril cannot verify, so these faces are never cached (ROADMAP 0i).";
|
|
3660
|
+
}
|
|
3661
|
+
if (/^\./.test(family)) {
|
|
3662
|
+
return "dot-prefixed families are the operating system's own hidden UI faces (macOS marks internal faces with a leading dot) \u2014 not user-licensable, so they are never cached (ROADMAP 0i).";
|
|
3663
|
+
}
|
|
3664
|
+
if (/^Segoe UI Variable/i.test(family)) {
|
|
3665
|
+
return "Segoe UI Variable cannot be licensed for use outside Microsoft products at all \u2014 an 'I have the rights' assertion has nothing it could attach to, so it is never cached (ROADMAP 0i).";
|
|
3666
|
+
}
|
|
3667
|
+
return void 0;
|
|
3668
|
+
}
|
|
3669
|
+
function faceAt(bytes, view, dirOffset, file, faceIndex) {
|
|
3670
|
+
if (dirOffset + 12 > bytes.length || !SFNT_VERSIONS.has(view.getUint32(dirOffset))) return null;
|
|
3671
|
+
const numTables = view.getUint16(dirOffset + 4);
|
|
3672
|
+
let names = /* @__PURE__ */ new Map();
|
|
3673
|
+
let os2Weight;
|
|
3674
|
+
let os2Italic = false;
|
|
3675
|
+
let variable = false;
|
|
3676
|
+
for (let t = 0; t < numTables; t++) {
|
|
3677
|
+
const rec = dirOffset + 12 + t * 16;
|
|
3678
|
+
if (rec + 16 > bytes.length) break;
|
|
3679
|
+
const tag = String.fromCharCode(bytes[rec], bytes[rec + 1], bytes[rec + 2], bytes[rec + 3]);
|
|
3680
|
+
const offset = view.getUint32(rec + 8);
|
|
3681
|
+
const length = view.getUint32(rec + 12);
|
|
3682
|
+
if (tag === "name") names = nameTableStrings(view, bytes, offset, length);
|
|
3683
|
+
else if (tag === "OS/2" && offset + 64 <= bytes.length) {
|
|
3684
|
+
const w = view.getUint16(offset + 4);
|
|
3685
|
+
if (w >= 1 && w <= 1e3) os2Weight = w;
|
|
3686
|
+
os2Italic = (view.getUint16(offset + 62) & 1) !== 0;
|
|
3687
|
+
} else if (tag === "fvar") variable = true;
|
|
3688
|
+
}
|
|
3689
|
+
const family = names.get(16) ?? names.get(1);
|
|
3690
|
+
if (family === void 0 || family === "") return null;
|
|
3691
|
+
const subfamily = names.get(17) ?? names.get(2) ?? "";
|
|
3692
|
+
const italic = /italic|oblique|inclined|slanted/i.test(subfamily) || os2Italic;
|
|
3693
|
+
const weight = weightFromSubfamily(subfamily) ?? os2Weight ?? 400;
|
|
3694
|
+
return { family, subfamily, weight, italic, variable, file, ...faceIndex !== void 0 ? { faceIndex } : {} };
|
|
3695
|
+
}
|
|
3696
|
+
function facesInFile(file) {
|
|
3697
|
+
try {
|
|
3698
|
+
const bytes = new Uint8Array(readFileSync3(file));
|
|
3699
|
+
if (bytes.length < 12) return [];
|
|
3700
|
+
const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
|
|
3701
|
+
if (isCollection(bytes)) {
|
|
3702
|
+
const numFonts = view.getUint32(8);
|
|
3703
|
+
const faces = [];
|
|
3704
|
+
for (let i = 0; i < numFonts; i++) {
|
|
3705
|
+
const f = faceAt(bytes, view, view.getUint32(12 + i * 4), file, i);
|
|
3706
|
+
if (f !== null) faces.push(f);
|
|
3707
|
+
}
|
|
3708
|
+
return faces;
|
|
3709
|
+
}
|
|
3710
|
+
const single = faceAt(bytes, view, 0, file);
|
|
3711
|
+
return single === null ? [] : [single];
|
|
3712
|
+
} catch {
|
|
3713
|
+
return [];
|
|
3714
|
+
}
|
|
3715
|
+
}
|
|
3716
|
+
function systemFontDirs() {
|
|
3717
|
+
const env = process.env["TENDRIL_SYSTEM_FONT_DIRS"];
|
|
3718
|
+
if (env !== void 0 && env !== "") return env.split(path10.delimiter).filter((d) => d !== "" && existsSync6(d));
|
|
3719
|
+
const home = os2.homedir();
|
|
3720
|
+
const dirs = process.platform === "darwin" ? ["/System/Library/Fonts", "/Library/Fonts", path10.join(home, "Library", "Fonts")] : process.platform === "win32" ? [
|
|
3721
|
+
path10.join(process.env["WINDIR"] ?? "C:\\Windows", "Fonts"),
|
|
3722
|
+
...process.env["LOCALAPPDATA"] !== void 0 ? [path10.join(process.env["LOCALAPPDATA"], "Microsoft", "Windows", "Fonts")] : []
|
|
3723
|
+
] : ["/usr/share/fonts", "/usr/local/share/fonts", path10.join(home, ".fonts"), path10.join(home, ".local", "share", "fonts")];
|
|
3724
|
+
return dirs.filter((d) => existsSync6(d));
|
|
3725
|
+
}
|
|
3726
|
+
function discoverSystemFaces(dirs = systemFontDirs(), depth = 3) {
|
|
3727
|
+
const seen = /* @__PURE__ */ new Set();
|
|
3728
|
+
const walk2 = (dirList, remaining) => {
|
|
3729
|
+
const faces = [];
|
|
3730
|
+
for (const dir of dirList) {
|
|
3731
|
+
let entries;
|
|
3732
|
+
try {
|
|
3733
|
+
entries = readdirSync2(dir, { withFileTypes: true });
|
|
3734
|
+
} catch {
|
|
3735
|
+
continue;
|
|
3736
|
+
}
|
|
3737
|
+
for (const e of entries) {
|
|
3738
|
+
const full = path10.join(dir, e.name);
|
|
3739
|
+
if (e.isDirectory()) {
|
|
3740
|
+
if (remaining > 1) faces.push(...walk2([full], remaining - 1));
|
|
3741
|
+
} else if (FONT_EXTENSIONS.has(path10.extname(e.name).toLowerCase())) {
|
|
3742
|
+
let key = full;
|
|
3743
|
+
try {
|
|
3744
|
+
key = realpathSync2(full);
|
|
3745
|
+
} catch {
|
|
3746
|
+
}
|
|
3747
|
+
if (seen.has(key)) continue;
|
|
3748
|
+
seen.add(key);
|
|
3749
|
+
faces.push(...facesInFile(full));
|
|
3750
|
+
}
|
|
3751
|
+
}
|
|
3752
|
+
}
|
|
3753
|
+
return faces;
|
|
3754
|
+
};
|
|
3755
|
+
return walk2(dirs, depth);
|
|
3756
|
+
}
|
|
3757
|
+
function systemFacesForFamily(family, dirs) {
|
|
3758
|
+
const want = normalize(family);
|
|
3759
|
+
return discoverSystemFaces(dirs).filter((f) => normalize(f.family) === want);
|
|
3760
|
+
}
|
|
3761
|
+
var WEIGHT_TOKENS, SFNT_VERSIONS, FONT_EXTENSIONS, normalize;
|
|
3762
|
+
var init_font_discovery = __esm({
|
|
3763
|
+
"packages/verify/src/font-discovery.ts"() {
|
|
3764
|
+
"use strict";
|
|
3765
|
+
init_font_collection();
|
|
3766
|
+
WEIGHT_TOKENS = [
|
|
3767
|
+
[/extra\s*light|ultra\s*light/i, 200],
|
|
3768
|
+
[/extra\s*bold|ultra\s*bold/i, 800],
|
|
3769
|
+
[/semi\s*bold|demi\s*bold|demi\b/i, 600],
|
|
3770
|
+
[/\bthin\b|\bhairline\b/i, 100],
|
|
3771
|
+
[/\blight\b/i, 300],
|
|
3772
|
+
[/\bmedium\b/i, 500],
|
|
3773
|
+
[/\bbold\b/i, 700],
|
|
3774
|
+
[/\bblack\b|\bheavy\b/i, 900],
|
|
3775
|
+
[/\bregular\b|\bnormal\b|\bbook\b|\broman\b/i, 400]
|
|
3776
|
+
];
|
|
3777
|
+
SFNT_VERSIONS = /* @__PURE__ */ new Set([
|
|
3778
|
+
65536,
|
|
3779
|
+
1330926671,
|
|
3780
|
+
1953658213
|
|
3781
|
+
/* true */
|
|
3782
|
+
]);
|
|
3783
|
+
FONT_EXTENSIONS = /* @__PURE__ */ new Set([".ttf", ".otf", ".ttc"]);
|
|
3784
|
+
normalize = (s) => s.toLowerCase().replace(/\s+/g, " ").trim();
|
|
3785
|
+
}
|
|
3786
|
+
});
|
|
3787
|
+
|
|
3788
|
+
// packages/verify/src/font-resolve.ts
|
|
3789
|
+
import { createHash } from "node:crypto";
|
|
3790
|
+
import { existsSync as existsSync7, mkdirSync as mkdirSync2, readFileSync as readFileSync4, writeFileSync as writeFileSync3 } from "node:fs";
|
|
3791
|
+
import os3 from "node:os";
|
|
3792
|
+
import path11 from "node:path";
|
|
3606
3793
|
function fontCacheDir() {
|
|
3607
3794
|
const env = process.env["TENDRIL_FONT_CACHE"];
|
|
3608
|
-
if (env !== void 0 && env !== "") return
|
|
3609
|
-
return
|
|
3795
|
+
if (env !== void 0 && env !== "") return path11.resolve(env);
|
|
3796
|
+
return path11.join(os3.homedir(), ".tendril", "fonts");
|
|
3610
3797
|
}
|
|
3611
3798
|
function normalizeFontLicense(value) {
|
|
3612
3799
|
return typeof value === "string" && FONT_LICENSES.includes(value) ? value : "unknown";
|
|
@@ -3671,29 +3858,29 @@ async function resolveFonts(family, weights, cacheDir = DEFAULT_FONT_CACHE) {
|
|
|
3671
3858
|
}
|
|
3672
3859
|
const bytes = new Uint8Array(await fileRes.arrayBuffer());
|
|
3673
3860
|
const sha256 = createHash("sha256").update(bytes).digest("hex");
|
|
3674
|
-
const file =
|
|
3861
|
+
const file = path11.join(cacheDir, `${family.toLowerCase().replace(/\s+/g, "-")}-${weight}.woff2`);
|
|
3675
3862
|
writeFileSync3(file, bytes);
|
|
3676
3863
|
resolved.push({ family, weight, source: url, sha256, file, license: meta.license, ...meta.family !== null ? { servedFamily: meta.family } : {} });
|
|
3677
3864
|
} catch (err) {
|
|
3678
3865
|
failures.push({ family, weight, reason: `download failed: ${err instanceof Error ? err.message : String(err)}` });
|
|
3679
3866
|
}
|
|
3680
3867
|
}
|
|
3681
|
-
const mPath =
|
|
3682
|
-
const prior =
|
|
3683
|
-
const portable2 = resolved.map((m) => ({ ...m, file:
|
|
3868
|
+
const mPath = path11.join(cacheDir, "manifest.json");
|
|
3869
|
+
const prior = existsSync7(mPath) ? JSON.parse(readFileSync4(mPath, "utf8")) : [];
|
|
3870
|
+
const portable2 = resolved.map((m) => ({ ...m, file: path11.basename(m.file) }));
|
|
3684
3871
|
const merged = [...prior.filter((p) => !resolved.some((m) => m.family === p.family && m.weight === p.weight)), ...portable2];
|
|
3685
3872
|
if (resolved.length > 0) writeFileSync3(mPath, `${JSON.stringify(merged, null, 2)}
|
|
3686
3873
|
`);
|
|
3687
3874
|
return { resolved, failures };
|
|
3688
3875
|
}
|
|
3689
|
-
function addLocalFont(family, weight, filePath, cacheDir = DEFAULT_FONT_CACHE, faceIndex) {
|
|
3690
|
-
const src =
|
|
3691
|
-
if (!
|
|
3692
|
-
const ext =
|
|
3876
|
+
function addLocalFont(family, weight, filePath, cacheDir = DEFAULT_FONT_CACHE, faceIndex, provenance = "local") {
|
|
3877
|
+
const src = path11.resolve(filePath);
|
|
3878
|
+
if (!existsSync7(src)) throw new Error(`font file not found: ${src}`);
|
|
3879
|
+
const ext = path11.extname(src).toLowerCase();
|
|
3693
3880
|
if (![".woff2", ".woff", ".ttf", ".otf", ".ttc"].includes(ext)) {
|
|
3694
3881
|
throw new Error(`unsupported font file "${ext}" \u2014 use .woff2 (preferred), .woff, .ttf, .otf or .ttc`);
|
|
3695
3882
|
}
|
|
3696
|
-
let bytes = new Uint8Array(
|
|
3883
|
+
let bytes = new Uint8Array(readFileSync4(src));
|
|
3697
3884
|
if (bytes.length === 0) throw new Error(`font file is empty: ${src}`);
|
|
3698
3885
|
let storedExt = ext;
|
|
3699
3886
|
if (isCollection(bytes)) {
|
|
@@ -3703,7 +3890,7 @@ function addLocalFont(family, weight, filePath, cacheDir = DEFAULT_FONT_CACHE, f
|
|
|
3703
3890
|
const all = listCollectionFaces(bytes);
|
|
3704
3891
|
const shown = (candidates.length > 0 ? candidates : all).map((f) => ` --face ${f.index} ${f.family ?? "(unnamed)"}${f.subfamily !== void 0 ? ` ${f.subfamily}` : ""}`).join("\n");
|
|
3705
3892
|
throw new Error(
|
|
3706
|
-
`${
|
|
3893
|
+
`${path11.basename(src)} is a collection of ${all.length} faces and ${candidates.length === 0 ? `none is named "${family}"` : `${candidates.length} match "${family}"`} \u2014 name the one you mean with --face <index>:
|
|
3707
3894
|
${shown}`
|
|
3708
3895
|
);
|
|
3709
3896
|
}
|
|
@@ -3712,20 +3899,20 @@ ${shown}`
|
|
|
3712
3899
|
}
|
|
3713
3900
|
mkdirSync2(cacheDir, { recursive: true });
|
|
3714
3901
|
const sha256 = createHash("sha256").update(bytes).digest("hex");
|
|
3715
|
-
const file =
|
|
3902
|
+
const file = path11.join(cacheDir, `${family.toLowerCase().replace(/\s+/g, "-")}-${weight}${storedExt}`);
|
|
3716
3903
|
writeFileSync3(file, bytes);
|
|
3717
|
-
const face = { family, weight, source:
|
|
3718
|
-
const mPath =
|
|
3719
|
-
const prior =
|
|
3720
|
-
const merged = [...prior.filter((p) => !(p.family === family && p.weight === weight)), { ...face, file:
|
|
3904
|
+
const face = { family, weight, source: `${provenance}:${path11.basename(src)}`, sha256, file, license: "unknown" };
|
|
3905
|
+
const mPath = path11.join(cacheDir, "manifest.json");
|
|
3906
|
+
const prior = existsSync7(mPath) ? JSON.parse(readFileSync4(mPath, "utf8")) : [];
|
|
3907
|
+
const merged = [...prior.filter((p) => !(p.family === family && p.weight === weight)), { ...face, file: path11.basename(file) }];
|
|
3721
3908
|
writeFileSync3(mPath, `${JSON.stringify(merged, null, 2)}
|
|
3722
3909
|
`);
|
|
3723
3910
|
return face;
|
|
3724
3911
|
}
|
|
3725
3912
|
function checkFontLock(lockPath, cacheDir = DEFAULT_FONT_CACHE) {
|
|
3726
|
-
const lock = JSON.parse(
|
|
3727
|
-
const mPath =
|
|
3728
|
-
const manifest =
|
|
3913
|
+
const lock = JSON.parse(readFileSync4(lockPath, "utf8"));
|
|
3914
|
+
const mPath = path11.join(cacheDir, "manifest.json");
|
|
3915
|
+
const manifest = existsSync7(mPath) ? JSON.parse(readFileSync4(mPath, "utf8")) : [];
|
|
3729
3916
|
return lock.map((l) => {
|
|
3730
3917
|
const m = manifest.find((x) => x.family === l.family && x.weight === l.weight);
|
|
3731
3918
|
if (m === void 0) return { family: l.family, weight: l.weight, status: "missing" };
|
|
@@ -3733,11 +3920,11 @@ function checkFontLock(lockPath, cacheDir = DEFAULT_FONT_CACHE) {
|
|
|
3733
3920
|
});
|
|
3734
3921
|
}
|
|
3735
3922
|
function resolvedFontFamilies(cacheDir = DEFAULT_FONT_CACHE) {
|
|
3736
|
-
const mPath =
|
|
3737
|
-
if (!
|
|
3923
|
+
const mPath = path11.join(cacheDir, "manifest.json");
|
|
3924
|
+
if (!existsSync7(mPath)) return [];
|
|
3738
3925
|
let entries;
|
|
3739
3926
|
try {
|
|
3740
|
-
entries = JSON.parse(
|
|
3927
|
+
entries = JSON.parse(readFileSync4(mPath, "utf8"));
|
|
3741
3928
|
} catch {
|
|
3742
3929
|
return [];
|
|
3743
3930
|
}
|
|
@@ -3751,8 +3938,8 @@ function resolvedFontFamilies(cacheDir = DEFAULT_FONT_CACHE) {
|
|
|
3751
3938
|
return [...byFamily.entries()].map(([family, w]) => ({ family, weights: [...w].sort((a, b) => a - b) }));
|
|
3752
3939
|
}
|
|
3753
3940
|
function requiredFontsManifest(families, cacheDir = DEFAULT_FONT_CACHE) {
|
|
3754
|
-
const mPath =
|
|
3755
|
-
const manifest =
|
|
3941
|
+
const mPath = path11.join(cacheDir, "manifest.json");
|
|
3942
|
+
const manifest = existsSync7(mPath) ? JSON.parse(readFileSync4(mPath, "utf8")) : [];
|
|
3756
3943
|
const wanted = new Set(families.map((f) => f.toLowerCase()));
|
|
3757
3944
|
return manifest.filter((f) => wanted.has(f.family.toLowerCase())).map((f) => ({
|
|
3758
3945
|
family: f.family,
|
|
@@ -3765,11 +3952,11 @@ function requiredFontsManifest(families, cacheDir = DEFAULT_FONT_CACHE) {
|
|
|
3765
3952
|
}));
|
|
3766
3953
|
}
|
|
3767
3954
|
function facesMissingLicense(cacheDir = DEFAULT_FONT_CACHE) {
|
|
3768
|
-
const mPath =
|
|
3769
|
-
if (!
|
|
3955
|
+
const mPath = path11.join(cacheDir, "manifest.json");
|
|
3956
|
+
if (!existsSync7(mPath)) return [];
|
|
3770
3957
|
let entries;
|
|
3771
3958
|
try {
|
|
3772
|
-
entries = JSON.parse(
|
|
3959
|
+
entries = JSON.parse(readFileSync4(mPath, "utf8"));
|
|
3773
3960
|
} catch {
|
|
3774
3961
|
return [];
|
|
3775
3962
|
}
|
|
@@ -3778,31 +3965,88 @@ function facesMissingLicense(cacheDir = DEFAULT_FONT_CACHE) {
|
|
|
3778
3965
|
).map((e) => ({ family: e.family, weight: e.weight, sha256: e.sha256 }));
|
|
3779
3966
|
}
|
|
3780
3967
|
function verifiedFontFamilies(cacheDir = DEFAULT_FONT_CACHE) {
|
|
3781
|
-
const mPath =
|
|
3782
|
-
if (!
|
|
3968
|
+
const mPath = path11.join(cacheDir, "manifest.json");
|
|
3969
|
+
if (!existsSync7(mPath)) return [];
|
|
3783
3970
|
let entries;
|
|
3784
3971
|
try {
|
|
3785
|
-
entries = JSON.parse(
|
|
3972
|
+
entries = JSON.parse(readFileSync4(mPath, "utf8"));
|
|
3786
3973
|
} catch {
|
|
3787
3974
|
return [];
|
|
3788
3975
|
}
|
|
3789
3976
|
const byFamily = /* @__PURE__ */ new Map();
|
|
3790
3977
|
for (const e of entries) {
|
|
3791
3978
|
if (typeof e.family !== "string" || typeof e.weight !== "number" || typeof e.file !== "string") continue;
|
|
3792
|
-
const file =
|
|
3793
|
-
if (!
|
|
3794
|
-
if (createHash("sha256").update(
|
|
3979
|
+
const file = path11.isAbsolute(e.file) && existsSync7(e.file) ? e.file : path11.resolve(cacheDir, path11.basename(e.file));
|
|
3980
|
+
if (!existsSync7(file)) continue;
|
|
3981
|
+
if (createHash("sha256").update(readFileSync4(file)).digest("hex") !== e.sha256) continue;
|
|
3795
3982
|
const set = byFamily.get(e.family) ?? /* @__PURE__ */ new Set();
|
|
3796
3983
|
set.add(e.weight);
|
|
3797
3984
|
byFamily.set(e.family, set);
|
|
3798
3985
|
}
|
|
3799
3986
|
return [...byFamily.entries()].map(([family, w]) => ({ family, weights: [...w].sort((a, b) => a - b) }));
|
|
3800
3987
|
}
|
|
3988
|
+
function addSystemFamily(family, opts = {}) {
|
|
3989
|
+
const refusal = systemFaceRefusal(family.trim());
|
|
3990
|
+
if (refusal !== void 0) return { added: [], skipped: [], overwrote: [], refusal };
|
|
3991
|
+
const plainStyle = (sub) => weightFromSubfamily(sub) !== void 0 || /^(regular|plain|)$/i.test(sub.trim());
|
|
3992
|
+
const faces = [...systemFacesForFamily(family, opts.dirs)].sort((a, b) => {
|
|
3993
|
+
const pa = plainStyle(a.subfamily) ? 0 : 1;
|
|
3994
|
+
const pb = plainStyle(b.subfamily) ? 0 : 1;
|
|
3995
|
+
if (pa !== pb) return pa - pb;
|
|
3996
|
+
return a.subfamily.localeCompare(b.subfamily) || a.file.localeCompare(b.file) || (a.faceIndex ?? 0) - (b.faceIndex ?? 0);
|
|
3997
|
+
});
|
|
3998
|
+
const added = [];
|
|
3999
|
+
const skipped = [];
|
|
4000
|
+
const overwrote = [];
|
|
4001
|
+
const cacheDir = opts.cacheDir ?? DEFAULT_FONT_CACHE;
|
|
4002
|
+
const manifestFile = path11.join(cacheDir, "manifest.json");
|
|
4003
|
+
const prior = existsSync7(manifestFile) ? JSON.parse(readFileSync4(manifestFile, "utf8")) : [];
|
|
4004
|
+
const taken = /* @__PURE__ */ new Set();
|
|
4005
|
+
for (const face of faces) {
|
|
4006
|
+
const skip = (reason) => skipped.push({ subfamily: face.subfamily, weight: face.weight, reason });
|
|
4007
|
+
const faceRefusal = systemFaceRefusal(face.family.trim());
|
|
4008
|
+
if (faceRefusal !== void 0) {
|
|
4009
|
+
skip(`refusal-class family \u2014 ${faceRefusal}`);
|
|
4010
|
+
continue;
|
|
4011
|
+
}
|
|
4012
|
+
if (opts.weights !== void 0 && !opts.weights.includes(face.weight)) continue;
|
|
4013
|
+
if (face.italic) {
|
|
4014
|
+
skip("italic \u2014 the cache cannot represent italic faces end to end yet; the upright face serves the family");
|
|
4015
|
+
continue;
|
|
4016
|
+
}
|
|
4017
|
+
if (face.variable) {
|
|
4018
|
+
skip("variable font \u2014 the mount injects STATIC faces (a variable file renders thinner stems than recorded rasters, measured); install or point at a static instance");
|
|
4019
|
+
continue;
|
|
4020
|
+
}
|
|
4021
|
+
if (/condensed|narrow|compressed|expanded|extended/i.test(face.subfamily)) {
|
|
4022
|
+
skip("width variant \u2014 a Condensed/Expanded face is a different design, not a weight of this family");
|
|
4023
|
+
continue;
|
|
4024
|
+
}
|
|
4025
|
+
if (taken.has(face.weight)) {
|
|
4026
|
+
skip(`another face already provided weight ${face.weight}`);
|
|
4027
|
+
continue;
|
|
4028
|
+
}
|
|
4029
|
+
taken.add(face.weight);
|
|
4030
|
+
const existing = prior.find((p) => p.family.toLowerCase() === face.family.toLowerCase() && p.weight === face.weight);
|
|
4031
|
+
if (existing !== void 0 && !existing.source.startsWith("system:")) {
|
|
4032
|
+
overwrote.push({
|
|
4033
|
+
family: face.family,
|
|
4034
|
+
weight: face.weight,
|
|
4035
|
+
priorSource: existing.source,
|
|
4036
|
+
priorSha256: existing.sha256,
|
|
4037
|
+
priorLicense: normalizeFontLicense(existing.license)
|
|
4038
|
+
});
|
|
4039
|
+
}
|
|
4040
|
+
added.push({ ...addLocalFont(face.family, face.weight, face.file, cacheDir, face.faceIndex, "system"), subfamily: face.subfamily || "Regular" });
|
|
4041
|
+
}
|
|
4042
|
+
return { added, skipped, overwrote };
|
|
4043
|
+
}
|
|
3801
4044
|
var DEFAULT_FONT_CACHE, UA, FONT_LICENSES, GOOGLE_LICENSE_IDS, GOOGLE_FONT_FILE_PREFIX;
|
|
3802
4045
|
var init_font_resolve = __esm({
|
|
3803
4046
|
"packages/verify/src/font-resolve.ts"() {
|
|
3804
4047
|
"use strict";
|
|
3805
4048
|
init_font_collection();
|
|
4049
|
+
init_font_discovery();
|
|
3806
4050
|
DEFAULT_FONT_CACHE = fontCacheDir();
|
|
3807
4051
|
UA = "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_11_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/49.0.2623.87 Safari/537.36";
|
|
3808
4052
|
FONT_LICENSES = ["OFL-1.1", "Apache-2.0", "UFL-1.0", "proprietary", "unknown"];
|
|
@@ -3818,17 +4062,17 @@ var init_font_resolve = __esm({
|
|
|
3818
4062
|
|
|
3819
4063
|
// packages/verify/src/font-faces.ts
|
|
3820
4064
|
import { createHash as createHash2 } from "node:crypto";
|
|
3821
|
-
import { existsSync as
|
|
3822
|
-
import
|
|
4065
|
+
import { existsSync as existsSync8, readFileSync as readFileSync5 } from "node:fs";
|
|
4066
|
+
import path12 from "node:path";
|
|
3823
4067
|
function injectedGroups(manifestPath2) {
|
|
3824
|
-
if (!
|
|
3825
|
-
const claimed = JSON.parse(
|
|
3826
|
-
const resolveFile = (f) =>
|
|
4068
|
+
if (!existsSync8(manifestPath2)) return { groups: [], shared: false };
|
|
4069
|
+
const claimed = JSON.parse(readFileSync5(manifestPath2, "utf8"));
|
|
4070
|
+
const resolveFile = (f) => path12.isAbsolute(f) && existsSync8(f) ? f : path12.resolve(path12.dirname(manifestPath2), path12.basename(f));
|
|
3827
4071
|
const byFile = /* @__PURE__ */ new Map();
|
|
3828
4072
|
for (const f of claimed) {
|
|
3829
4073
|
const file = resolveFile(f.file);
|
|
3830
|
-
if (!
|
|
3831
|
-
if (createHash2("sha256").update(
|
|
4074
|
+
if (!existsSync8(file)) continue;
|
|
4075
|
+
if (createHash2("sha256").update(readFileSync5(file)).digest("hex") !== f.sha256) continue;
|
|
3832
4076
|
const k = `${f.family}:${f.file}`;
|
|
3833
4077
|
const e = byFile.get(k) ?? { family: f.family, weights: [], file };
|
|
3834
4078
|
e.weights.push(f.weight);
|
|
@@ -3837,14 +4081,14 @@ function injectedGroups(manifestPath2) {
|
|
|
3837
4081
|
const groups = [...byFile.values()];
|
|
3838
4082
|
return { groups, shared: new Set(groups.map((e) => e.file)).size < groups.length };
|
|
3839
4083
|
}
|
|
3840
|
-
function fontFaceCss(manifestPath2 =
|
|
4084
|
+
function fontFaceCss(manifestPath2 = path12.join(fontCacheDir(), "manifest.json")) {
|
|
3841
4085
|
const { groups, shared } = injectedGroups(manifestPath2);
|
|
3842
4086
|
return groups.map((e) => {
|
|
3843
4087
|
const weight = shared || e.weights.length > 1 ? `${SPAN[0]} ${SPAN[1]}` : String(e.weights[0]);
|
|
3844
|
-
return `@font-face { font-family: '${e.family}'; font-weight: ${weight}; font-style: normal; src: url(data:font/woff2;base64,${
|
|
4088
|
+
return `@font-face { font-family: '${e.family}'; font-weight: ${weight}; font-style: normal; src: url(data:font/woff2;base64,${readFileSync5(e.file).toString("base64")}) format('woff2'); }`;
|
|
3845
4089
|
}).join("\n");
|
|
3846
4090
|
}
|
|
3847
|
-
function injectedFamilyWeights(manifestPath2 =
|
|
4091
|
+
function injectedFamilyWeights(manifestPath2 = path12.join(fontCacheDir(), "manifest.json")) {
|
|
3848
4092
|
const { groups, shared } = injectedGroups(manifestPath2);
|
|
3849
4093
|
const out = /* @__PURE__ */ new Map();
|
|
3850
4094
|
for (const g of groups) {
|
|
@@ -3869,17 +4113,17 @@ var init_font_faces = __esm({
|
|
|
3869
4113
|
});
|
|
3870
4114
|
|
|
3871
4115
|
// packages/verify/src/admission.ts
|
|
3872
|
-
import { readFileSync as
|
|
3873
|
-
import
|
|
4116
|
+
import { readFileSync as readFileSync6, readdirSync as readdirSync3, existsSync as existsSync9, writeFileSync as writeFileSync4 } from "node:fs";
|
|
4117
|
+
import path13 from "node:path";
|
|
3874
4118
|
import { build as build2 } from "esbuild";
|
|
3875
4119
|
import postcss from "postcss";
|
|
3876
4120
|
import tailwindcss from "tailwindcss";
|
|
3877
4121
|
import { chromium as chromium2 } from "playwright-core";
|
|
3878
4122
|
function fontWeightsByFamily() {
|
|
3879
|
-
const mPath =
|
|
4123
|
+
const mPath = path13.join(fontCacheDir(), "manifest.json");
|
|
3880
4124
|
const out = /* @__PURE__ */ new Map();
|
|
3881
|
-
if (!
|
|
3882
|
-
for (const f of JSON.parse(
|
|
4125
|
+
if (!existsSync9(mPath)) return out;
|
|
4126
|
+
for (const f of JSON.parse(readFileSync6(mPath, "utf8")))
|
|
3883
4127
|
out.set(f.family, [...out.get(f.family) ?? [], f.weight]);
|
|
3884
4128
|
return out;
|
|
3885
4129
|
}
|
|
@@ -3899,7 +4143,7 @@ var init_admission = __esm({
|
|
|
3899
4143
|
});
|
|
3900
4144
|
|
|
3901
4145
|
// packages/verify/src/tasks.ts
|
|
3902
|
-
import
|
|
4146
|
+
import path14 from "node:path";
|
|
3903
4147
|
var CALENDAR_CONFIGS, CALENDAR_API, CALENDAR_BEHAVIORS, BUTTON_CONFIGS, BUTTON_API, COMBO_FIX, COMBO_CONFIGS, COMBO_API, MODAL_CONFIGS, MODAL_API, BUTTON_BEHAVIORS, COMBO_BEHAVIORS, MODAL_BEHAVIORS, TASKS;
|
|
3904
4148
|
var init_tasks = __esm({
|
|
3905
4149
|
"packages/verify/src/tasks.ts"() {
|
|
@@ -4059,7 +4303,7 @@ BEHAVIORAL CONTRACT (machine-verified, gating): the primary action and close con
|
|
|
4059
4303
|
];
|
|
4060
4304
|
TASKS = {
|
|
4061
4305
|
calendar: {
|
|
4062
|
-
set:
|
|
4306
|
+
set: path14.join(REPO_ROOT, "examples/recordings/shadcn-poc-calendar"),
|
|
4063
4307
|
entry: "Calendar.tsx",
|
|
4064
4308
|
configs: CALENDAR_CONFIGS,
|
|
4065
4309
|
systemApi: CALENDAR_API,
|
|
@@ -4067,7 +4311,7 @@ BEHAVIORAL CONTRACT (machine-verified, gating): the primary action and close con
|
|
|
4067
4311
|
prelude: { controls: ['[data-tendril-part="day"]'], textInputs: [] }
|
|
4068
4312
|
},
|
|
4069
4313
|
"shadcn-button": {
|
|
4070
|
-
set:
|
|
4314
|
+
set: path14.join(REPO_ROOT, "examples/recordings/shadcn-poc-button"),
|
|
4071
4315
|
entry: "Button.tsx",
|
|
4072
4316
|
configs: BUTTON_CONFIGS,
|
|
4073
4317
|
systemApi: BUTTON_API,
|
|
@@ -4075,7 +4319,7 @@ BEHAVIORAL CONTRACT (machine-verified, gating): the primary action and close con
|
|
|
4075
4319
|
prelude: { controls: ["> *"], textInputs: [] }
|
|
4076
4320
|
},
|
|
4077
4321
|
combobox: {
|
|
4078
|
-
set:
|
|
4322
|
+
set: path14.join(REPO_ROOT, "examples/recordings/carbon-poc-combobox"),
|
|
4079
4323
|
entry: "ComboBox.tsx",
|
|
4080
4324
|
configs: COMBO_CONFIGS,
|
|
4081
4325
|
systemApi: COMBO_API,
|
|
@@ -4083,7 +4327,7 @@ BEHAVIORAL CONTRACT (machine-verified, gating): the primary action and close con
|
|
|
4083
4327
|
prelude: { controls: ['[role="option"]'], textInputs: ["input"], popover: { selector: '[role="listbox"]', trigger: "input" } }
|
|
4084
4328
|
},
|
|
4085
4329
|
modal: {
|
|
4086
|
-
set:
|
|
4330
|
+
set: path14.join(REPO_ROOT, "examples/recordings/carbon-poc-modal"),
|
|
4087
4331
|
entry: "Modal.tsx",
|
|
4088
4332
|
configs: MODAL_CONFIGS,
|
|
4089
4333
|
systemApi: MODAL_API,
|
|
@@ -4101,8 +4345,8 @@ __export(behavior_exports, {
|
|
|
4101
4345
|
compileMount: () => compileMount,
|
|
4102
4346
|
recordingIsDark: () => recordingIsDark
|
|
4103
4347
|
});
|
|
4104
|
-
import { existsSync as
|
|
4105
|
-
import
|
|
4348
|
+
import { existsSync as existsSync10, readFileSync as readFileSync7 } from "node:fs";
|
|
4349
|
+
import path15 from "node:path";
|
|
4106
4350
|
import { build as build3 } from "esbuild";
|
|
4107
4351
|
import { chromium as chromium3 } from "playwright-core";
|
|
4108
4352
|
import { PNG as PNG2 } from "pngjs";
|
|
@@ -4111,12 +4355,12 @@ function getFontFaces() {
|
|
|
4111
4355
|
return _fontFaces;
|
|
4112
4356
|
}
|
|
4113
4357
|
async function compileMount(task, bundleDir) {
|
|
4114
|
-
const entryTsx =
|
|
4115
|
-
if (!
|
|
4358
|
+
const entryTsx = path15.join(bundleDir, task.entry);
|
|
4359
|
+
if (!existsSync10(entryTsx)) return { error: `${task.entry} missing` };
|
|
4116
4360
|
const mountSrc = `
|
|
4117
4361
|
import { createElement } from "react";
|
|
4118
4362
|
import { createRoot } from "react-dom/client";
|
|
4119
|
-
import * as B from ${JSON.stringify(
|
|
4363
|
+
import * as B from ${JSON.stringify(path15.resolve(entryTsx))};
|
|
4120
4364
|
const cfg = (window as unknown as { __cfg: { component: string; props: Record<string, unknown>; spyProps?: string[] } }).__cfg;
|
|
4121
4365
|
const C = (B as Record<string, unknown>)[cfg.component] as Parameters<typeof createElement>[0] | undefined;
|
|
4122
4366
|
// Callbacks cannot ride the JSON config: specs NAME spy props and the
|
|
@@ -4397,10 +4641,10 @@ async function runSteps(page, spec, renderPose) {
|
|
|
4397
4641
|
function recordingIsDark(task) {
|
|
4398
4642
|
const rep = task.configs[0]?.rep;
|
|
4399
4643
|
if (rep === void 0) return false;
|
|
4400
|
-
const f =
|
|
4401
|
-
if (!
|
|
4644
|
+
const f = path15.join(task.set, rep, "get_screenshot.json");
|
|
4645
|
+
if (!existsSync10(f)) return false;
|
|
4402
4646
|
try {
|
|
4403
|
-
const env = JSON.parse(
|
|
4647
|
+
const env = JSON.parse(readFileSync7(f, "utf8")).content.find((c) => c.type === "image");
|
|
4404
4648
|
if (env?.data === void 0) return false;
|
|
4405
4649
|
const png = PNG2.sync.read(Buffer.from(env.data, "base64"));
|
|
4406
4650
|
let sum = 0;
|
|
@@ -4472,7 +4716,7 @@ async function checkBehaviors(task, bundleDir, opts = {}) {
|
|
|
4472
4716
|
const deadlineMs = timeoutMs + 1e4;
|
|
4473
4717
|
const js = await compileMount(task, bundleDir);
|
|
4474
4718
|
if (typeof js !== "string") return task.behaviors.map((b) => ({ id: b.id, pass: false, detail: js.error }));
|
|
4475
|
-
const css = ["tokens.css", "styles.css"].map((f) =>
|
|
4719
|
+
const css = ["tokens.css", "styles.css"].map((f) => path15.join(bundleDir, f)).filter((f) => existsSync10(f)).map((f) => readFileSync7(f, "utf8")).join("\n");
|
|
4476
4720
|
const server = await chromium3.launchServer({ executablePath: resolveChrome(), headless: true, args: mountArgs() });
|
|
4477
4721
|
const browser = await chromium3.connect(server.wsEndpoint());
|
|
4478
4722
|
const results = [];
|
|
@@ -4615,8 +4859,8 @@ var init_behavior = __esm({
|
|
|
4615
4859
|
});
|
|
4616
4860
|
|
|
4617
4861
|
// packages/verify/src/bundle-quality.ts
|
|
4618
|
-
import { readFileSync as
|
|
4619
|
-
import
|
|
4862
|
+
import { readFileSync as readFileSync8, rmSync as rmSync2, writeFileSync as writeFileSync5, existsSync as existsSync11 } from "node:fs";
|
|
4863
|
+
import path16 from "node:path";
|
|
4620
4864
|
function definedVars(tokensCss) {
|
|
4621
4865
|
if (tokensCss === void 0) return void 0;
|
|
4622
4866
|
const names = /* @__PURE__ */ new Set();
|
|
@@ -4625,19 +4869,19 @@ function definedVars(tokensCss) {
|
|
|
4625
4869
|
}
|
|
4626
4870
|
function recordedTokenMapState(setDir, reps) {
|
|
4627
4871
|
const readMap = (file) => {
|
|
4628
|
-
if (!
|
|
4872
|
+
if (!existsSync11(file)) return void 0;
|
|
4629
4873
|
try {
|
|
4630
|
-
const parsed = JSON.parse(envelopeFirstTextPart(JSON.parse(
|
|
4874
|
+
const parsed = JSON.parse(envelopeFirstTextPart(JSON.parse(readFileSync8(file, "utf8"))) || "{}");
|
|
4631
4875
|
return parsed !== null && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : {};
|
|
4632
4876
|
} catch {
|
|
4633
4877
|
return {};
|
|
4634
4878
|
}
|
|
4635
4879
|
};
|
|
4636
|
-
const setLevel = readMap(
|
|
4880
|
+
const setLevel = readMap(path16.join(setDir, "get_variable_defs.json"));
|
|
4637
4881
|
if (setLevel !== void 0) return Object.keys(setLevel).length === 0 ? "empty" : "populated";
|
|
4638
4882
|
let recorded = false;
|
|
4639
4883
|
for (const rep of reps) {
|
|
4640
|
-
const m = readMap(
|
|
4884
|
+
const m = readMap(path16.join(setDir, rep, "get_variable_defs.json"));
|
|
4641
4885
|
if (m === void 0) continue;
|
|
4642
4886
|
recorded = true;
|
|
4643
4887
|
if (Object.keys(m).length > 0) return "populated";
|
|
@@ -4702,11 +4946,11 @@ function fontStackFindings(sheets, coverage) {
|
|
|
4702
4946
|
}
|
|
4703
4947
|
async function checkBundleQuality(bundleDir, entry, set, fontManifest) {
|
|
4704
4948
|
const findings = [];
|
|
4705
|
-
const entryPath =
|
|
4706
|
-
const cssPath =
|
|
4707
|
-
const tokensPath =
|
|
4708
|
-
const css =
|
|
4709
|
-
const tokensCss =
|
|
4949
|
+
const entryPath = path16.join(bundleDir, entry);
|
|
4950
|
+
const cssPath = path16.join(bundleDir, "styles.css");
|
|
4951
|
+
const tokensPath = path16.join(bundleDir, "tokens.css");
|
|
4952
|
+
const css = existsSync11(cssPath) ? readFileSync8(cssPath, "utf8") : "";
|
|
4953
|
+
const tokensCss = existsSync11(tokensPath) ? readFileSync8(tokensPath, "utf8") : void 0;
|
|
4710
4954
|
findings.push(
|
|
4711
4955
|
...fontStackFindings(
|
|
4712
4956
|
[
|
|
@@ -4716,11 +4960,11 @@ async function checkBundleQuality(bundleDir, entry, set, fontManifest) {
|
|
|
4716
4960
|
injectedFamilyWeights(fontManifest)
|
|
4717
4961
|
)
|
|
4718
4962
|
);
|
|
4719
|
-
if (
|
|
4963
|
+
if (existsSync11(entryPath)) {
|
|
4720
4964
|
const workDir = newScratchDir("quality");
|
|
4721
4965
|
try {
|
|
4722
|
-
const tsxPath =
|
|
4723
|
-
writeFileSync5(tsxPath,
|
|
4966
|
+
const tsxPath = path16.join(workDir, entry);
|
|
4967
|
+
writeFileSync5(tsxPath, readFileSync8(entryPath, "utf8"));
|
|
4724
4968
|
for (const d of runTscStrict([tsxPath]).diagnostics) {
|
|
4725
4969
|
findings.push({ kind: "tsc", file: entry, ...d.line === void 0 ? {} : { line: d.line }, message: `TS${d.code}: ${d.message}` });
|
|
4726
4970
|
}
|
|
@@ -4795,8 +5039,8 @@ var init_effect_geometry = __esm({
|
|
|
4795
5039
|
});
|
|
4796
5040
|
|
|
4797
5041
|
// packages/verify/src/bundle-score.ts
|
|
4798
|
-
import { existsSync as
|
|
4799
|
-
import
|
|
5042
|
+
import { existsSync as existsSync12, mkdirSync as mkdirSync3, readFileSync as readFileSync9, writeFileSync as writeFileSync6 } from "node:fs";
|
|
5043
|
+
import path17 from "node:path";
|
|
4800
5044
|
import { build as build4 } from "esbuild";
|
|
4801
5045
|
import { chromium as chromium4 } from "playwright-core";
|
|
4802
5046
|
function getFontFaces2() {
|
|
@@ -4850,7 +5094,7 @@ function chooseSeat(cropAt, ref, padW, padH, backdrop, origin, extents) {
|
|
|
4850
5094
|
}
|
|
4851
5095
|
function metadataRoot(set, rep) {
|
|
4852
5096
|
try {
|
|
4853
|
-
const text = JSON.parse(
|
|
5097
|
+
const text = JSON.parse(readFileSync9(path17.join(set, rep, "get_metadata.json"), "utf8")).content.map((c) => c.text ?? "").join("\n");
|
|
4854
5098
|
return parseMetadataStructure(text);
|
|
4855
5099
|
} catch {
|
|
4856
5100
|
return void 0;
|
|
@@ -4905,19 +5149,19 @@ function smallSemanticNodes(set, rep, maxArea = 1024) {
|
|
|
4905
5149
|
});
|
|
4906
5150
|
}
|
|
4907
5151
|
function repMeta(set, rep) {
|
|
4908
|
-
const text = JSON.parse(
|
|
5152
|
+
const text = JSON.parse(readFileSync9(path17.join(set, rep, "get_metadata.json"), "utf8")).content.map((c) => c.text ?? "").join("\n");
|
|
4909
5153
|
const root = parseMetadataStructure(text);
|
|
4910
5154
|
return { w: Math.round(root.width ?? 100), h: Math.round(root.height ?? 40) };
|
|
4911
5155
|
}
|
|
4912
5156
|
function repRef(set, rep) {
|
|
4913
|
-
const env = JSON.parse(
|
|
5157
|
+
const env = JSON.parse(readFileSync9(path17.join(set, rep, "get_screenshot.json"), "utf8")).content.find((c) => c.type === "image");
|
|
4914
5158
|
return Uint8Array.from(Buffer.from(env?.data ?? "", "base64"));
|
|
4915
5159
|
}
|
|
4916
5160
|
function repEffectExtents(set, rep) {
|
|
4917
|
-
const file =
|
|
4918
|
-
if (!
|
|
5161
|
+
const file = path17.join(set, rep, "get_design_context.json");
|
|
5162
|
+
if (!existsSync12(file)) return void 0;
|
|
4919
5163
|
try {
|
|
4920
|
-
const text = JSON.parse(
|
|
5164
|
+
const text = JSON.parse(readFileSync9(file, "utf8")).content.map((c) => c.text ?? "").join("\n");
|
|
4921
5165
|
const extents = shadowExtents(text);
|
|
4922
5166
|
return extents.top + extents.right + extents.bottom + extents.left > 0 ? extents : void 0;
|
|
4923
5167
|
} catch {
|
|
@@ -4928,13 +5172,13 @@ async function scoreBundleForTask(task, bundleDir, bar = BAR, opts = {}) {
|
|
|
4928
5172
|
if (opts.evidenceDir !== void 0) mkdirSync3(opts.evidenceDir, { recursive: true });
|
|
4929
5173
|
const CONFIGS2 = task.configs;
|
|
4930
5174
|
const timeoutMs = opts.mountDeadlineMs ?? ENGINE_MOUNT_DEADLINE_MS;
|
|
4931
|
-
const entryTsx =
|
|
4932
|
-
if (!
|
|
4933
|
-
const css = ["tokens.css", "styles.css"].map((f) =>
|
|
5175
|
+
const entryTsx = path17.join(bundleDir, task.entry);
|
|
5176
|
+
if (!existsSync12(entryTsx)) return CONFIGS2.map((c) => ({ rep: c.rep, similarity: 0, inkRecall: 0, exact: { similarity: 0, inkRecall: 0 }, pass: false, error: `${task.entry} missing` }));
|
|
5177
|
+
const css = ["tokens.css", "styles.css"].map((f) => path17.join(bundleDir, f)).filter((f) => existsSync12(f)).map((f) => readFileSync9(f, "utf8")).join("\n");
|
|
4934
5178
|
const mountSrc = `
|
|
4935
5179
|
import { createElement } from "react";
|
|
4936
5180
|
import { createRoot } from "react-dom/client";
|
|
4937
|
-
import * as B from ${JSON.stringify(
|
|
5181
|
+
import * as B from ${JSON.stringify(path17.resolve(entryTsx))};
|
|
4938
5182
|
const cfg = (window as unknown as { __cfg: { component: string; props: Record<string, unknown> } }).__cfg;
|
|
4939
5183
|
const C = (B as Record<string, unknown>)[cfg.component] as Parameters<typeof createElement>[0] | undefined;
|
|
4940
5184
|
const root = document.getElementById("root");
|
|
@@ -5029,12 +5273,12 @@ body{margin:0;background:rgb(${backdrop.join(",")})}
|
|
|
5029
5273
|
return name === void 0 ? c : { ...c, name };
|
|
5030
5274
|
});
|
|
5031
5275
|
if (opts.evidenceDir !== void 0) {
|
|
5032
|
-
writeFileSync6(
|
|
5033
|
-
writeFileSync6(
|
|
5034
|
-
writeFileSync6(
|
|
5276
|
+
writeFileSync6(path17.join(opts.evidenceDir, `${cfg.rep}-render.png`), scored);
|
|
5277
|
+
writeFileSync6(path17.join(opts.evidenceDir, `${cfg.rep}-ref.png`), ref);
|
|
5278
|
+
writeFileSync6(path17.join(opts.evidenceDir, `${cfg.rep}-diff.png`), diffPng(scored, ref));
|
|
5035
5279
|
for (const [i, c] of absent.slice(0, 3).entries()) {
|
|
5036
|
-
writeFileSync6(
|
|
5037
|
-
writeFileSync6(
|
|
5280
|
+
writeFileSync6(path17.join(opts.evidenceDir, `${cfg.rep}-absent-${i}-ref.png`), zoomCrop(ref, c));
|
|
5281
|
+
writeFileSync6(path17.join(opts.evidenceDir, `${cfg.rep}-absent-${i}-render.png`), zoomCrop(scored, c));
|
|
5038
5282
|
}
|
|
5039
5283
|
}
|
|
5040
5284
|
return {
|
|
@@ -5142,15 +5386,15 @@ var init_prelude = __esm({
|
|
|
5142
5386
|
});
|
|
5143
5387
|
|
|
5144
5388
|
// packages/verify/src/parity.ts
|
|
5145
|
-
import { existsSync as
|
|
5146
|
-
import
|
|
5389
|
+
import { existsSync as existsSync13, readFileSync as readFileSync10 } from "node:fs";
|
|
5390
|
+
import path18 from "node:path";
|
|
5147
5391
|
import { chromium as chromium6 } from "playwright-core";
|
|
5148
5392
|
function getFontFaces3() {
|
|
5149
5393
|
_fontFaces3 ??= fontFaceCss();
|
|
5150
5394
|
return _fontFaces3;
|
|
5151
5395
|
}
|
|
5152
|
-
function hoverForcedConfigs(
|
|
5153
|
-
return
|
|
5396
|
+
function hoverForcedConfigs(authority) {
|
|
5397
|
+
return authority.filter((c) => {
|
|
5154
5398
|
const forced = c.props["data-tendril-state"];
|
|
5155
5399
|
return typeof forced === "string" && forced.split(/\s+/).includes("hover");
|
|
5156
5400
|
});
|
|
@@ -5165,27 +5409,37 @@ function withoutHoverToken(props) {
|
|
|
5165
5409
|
}
|
|
5166
5410
|
return rest;
|
|
5167
5411
|
}
|
|
5168
|
-
async function checkHoverParity(task, bundleDir, opts = {}) {
|
|
5169
|
-
const configs = hoverForcedConfigs(
|
|
5412
|
+
async function checkHoverParity(task, bundleDir, authority, opts = {}) {
|
|
5413
|
+
const configs = hoverForcedConfigs(authority);
|
|
5170
5414
|
if (configs.length === 0) return [];
|
|
5171
5415
|
const timeoutMs = opts.mountDeadlineMs ?? ENGINE_MOUNT_DEADLINE_MS;
|
|
5172
5416
|
const deadlineMs = timeoutMs + 1e4;
|
|
5173
5417
|
const js = await compileMount(task, bundleDir);
|
|
5174
5418
|
if (typeof js !== "string") return configs.map((c) => ({ id: `parity:${c.rep}`, pass: false, detail: js.error }));
|
|
5175
|
-
const css = ["tokens.css", "styles.css"].map((f) =>
|
|
5419
|
+
const css = ["tokens.css", "styles.css"].map((f) => path18.join(bundleDir, f)).filter((f) => existsSync13(f)).map((f) => readFileSync10(f, "utf8")).join("\n");
|
|
5176
5420
|
const server = await chromium6.launchServer({ executablePath: resolveChrome(), headless: true, args: mountArgs() });
|
|
5177
5421
|
const browser = await chromium6.connect(server.wsEndpoint());
|
|
5178
5422
|
const results = [];
|
|
5179
5423
|
try {
|
|
5180
5424
|
for (const cfg of configs) {
|
|
5181
|
-
const
|
|
5425
|
+
const box = (() => {
|
|
5426
|
+
try {
|
|
5427
|
+
return repMeta(task.set, cfg.rep);
|
|
5428
|
+
} catch {
|
|
5429
|
+
return void 0;
|
|
5430
|
+
}
|
|
5431
|
+
})();
|
|
5432
|
+
const rootCss = box === void 0 ? "#root{position:static;display:inline-block}" : `#root{width:${box.w}px;height:${box.h}px;position:absolute;left:20px;top:20px}
|
|
5433
|
+
#root > *{min-width:${box.w}px;min-height:${box.h}px}`;
|
|
5434
|
+
const viewport = box === void 0 ? { width: 900, height: 700 } : { width: box.w + 48, height: box.h + 48 };
|
|
5435
|
+
const shoot = async (component, props, realHover) => {
|
|
5182
5436
|
const html = `<!doctype html><html><head><meta charset="utf-8"><style>
|
|
5183
5437
|
${getFontFaces3()}
|
|
5184
5438
|
${css}
|
|
5185
5439
|
body{margin:0;padding:20px}
|
|
5186
|
-
|
|
5187
|
-
</style></head><body><div id="root"></div><script>window.__cfg=${JSON.stringify({ component
|
|
5188
|
-
const page = await browser.newPage({ viewport
|
|
5440
|
+
${rootCss}
|
|
5441
|
+
</style></head><body><div id="root"></div><script>window.__cfg=${JSON.stringify({ component, props })}</script><script>${js}</script></body></html>`;
|
|
5442
|
+
const page = await browser.newPage({ viewport });
|
|
5189
5443
|
try {
|
|
5190
5444
|
page.setDefaultTimeout(timeoutMs);
|
|
5191
5445
|
await page.route("**/*", (route) => route.request().url().startsWith("data:") ? route.continue() : route.abort());
|
|
@@ -5199,15 +5453,30 @@ body{margin:0;padding:20px}
|
|
|
5199
5453
|
} else {
|
|
5200
5454
|
await page.waitForTimeout(400);
|
|
5201
5455
|
}
|
|
5202
|
-
return await page.screenshot({ clip: { x: 0, y: 0, width:
|
|
5456
|
+
return await page.screenshot({ clip: { x: 0, y: 0, width: viewport.width, height: viewport.height } });
|
|
5203
5457
|
} finally {
|
|
5204
5458
|
await page.close();
|
|
5205
5459
|
}
|
|
5206
5460
|
};
|
|
5207
5461
|
const work = (async () => {
|
|
5208
|
-
const
|
|
5462
|
+
const adapter = task.configs.find((c) => c.rep === cfg.rep);
|
|
5463
|
+
if (adapter === void 0) {
|
|
5464
|
+
return { id: `parity:${cfg.rep}`, pass: false, detail: "the recording proves this hover pose and the bundle's adapter does not map it \u2014 an unmapped config is a FAIL, not an exemption from the check" };
|
|
5465
|
+
}
|
|
5466
|
+
const forcedShot = await shoot(adapter.component, cfg.props, false);
|
|
5209
5467
|
if (!Buffer.isBuffer(forcedShot)) return { id: `parity:${cfg.rep}`, pass: false, detail: `forced mount: ${forcedShot.error}` };
|
|
5210
|
-
|
|
5468
|
+
if (JSON.stringify(adapter.props) !== JSON.stringify(cfg.props)) {
|
|
5469
|
+
const adapterShot = await shoot(adapter.component, adapter.props, false);
|
|
5470
|
+
if (!Buffer.isBuffer(adapterShot)) return { id: `parity:${cfg.rep}`, pass: false, detail: `adapter mount: ${adapterShot.error}` };
|
|
5471
|
+
if (Buffer.compare(adapterShot, forcedShot) !== 0) {
|
|
5472
|
+
return {
|
|
5473
|
+
id: `parity:${cfg.rep}`,
|
|
5474
|
+
pass: false,
|
|
5475
|
+
detail: `the adapter drives this hover pose through props that do not render what the prescribed API renders (adapter ${JSON.stringify(adapter.props)} vs authored ${JSON.stringify(cfg.props)}) \u2014 the forcing contract is part of the certified surface; align the adapter with the authored API`
|
|
5476
|
+
};
|
|
5477
|
+
}
|
|
5478
|
+
}
|
|
5479
|
+
const realShot = await shoot(adapter.component, withoutHoverToken(cfg.props), true);
|
|
5211
5480
|
if (!Buffer.isBuffer(realShot)) return { id: `parity:${cfg.rep}`, pass: false, detail: `real-hover mount: ${realShot.error}` };
|
|
5212
5481
|
if (Buffer.compare(forcedShot, realShot) !== 0) {
|
|
5213
5482
|
return {
|
|
@@ -5247,13 +5516,14 @@ var init_parity = __esm({
|
|
|
5247
5516
|
init_behavior();
|
|
5248
5517
|
init_font_faces();
|
|
5249
5518
|
init_mount_limits();
|
|
5519
|
+
init_bundle_score();
|
|
5250
5520
|
}
|
|
5251
5521
|
});
|
|
5252
5522
|
|
|
5253
5523
|
// packages/verify/src/composition.ts
|
|
5254
5524
|
import { createRequire as createRequire2 } from "node:module";
|
|
5255
|
-
import { existsSync as
|
|
5256
|
-
import
|
|
5525
|
+
import { existsSync as existsSync14, readFileSync as readFileSync11 } from "node:fs";
|
|
5526
|
+
import path19 from "node:path";
|
|
5257
5527
|
import { build as build6 } from "esbuild";
|
|
5258
5528
|
import { chromium as chromium7 } from "playwright-core";
|
|
5259
5529
|
function getFontFaces4() {
|
|
@@ -5261,9 +5531,9 @@ function getFontFaces4() {
|
|
|
5261
5531
|
return _fontFaces4;
|
|
5262
5532
|
}
|
|
5263
5533
|
async function compileInstrumentedMount(task, bundleDir) {
|
|
5264
|
-
const entryTsx =
|
|
5265
|
-
if (!
|
|
5266
|
-
const requireFromVerify = createRequire2(
|
|
5534
|
+
const entryTsx = path19.join(bundleDir, task.entry);
|
|
5535
|
+
if (!existsSync14(entryTsx)) return { error: `${task.entry} missing` };
|
|
5536
|
+
const requireFromVerify = createRequire2(path19.join(VERIFY_PKG_DIR, "package.json"));
|
|
5267
5537
|
let realJsxPath;
|
|
5268
5538
|
try {
|
|
5269
5539
|
realJsxPath = requireFromVerify.resolve("react/jsx-runtime");
|
|
@@ -5274,7 +5544,7 @@ async function compileInstrumentedMount(task, bundleDir) {
|
|
|
5274
5544
|
import { createElement } from "react";
|
|
5275
5545
|
import { createRoot } from "react-dom/client";
|
|
5276
5546
|
import { __registerParts } from "react/jsx-runtime";
|
|
5277
|
-
import * as B from ${JSON.stringify(
|
|
5547
|
+
import * as B from ${JSON.stringify(path19.resolve(entryTsx))};
|
|
5278
5548
|
const cfg = (window as unknown as { __cfg: { component: string; props: Record<string, unknown>; partComponents: string[] } }).__cfg;
|
|
5279
5549
|
const pairs: Array<[unknown, string]> = [];
|
|
5280
5550
|
for (const name of cfg.partComponents) {
|
|
@@ -5329,7 +5599,7 @@ function expectedParts(task, roles, mainSlug) {
|
|
|
5329
5599
|
}
|
|
5330
5600
|
function interiorRegions(setDir, roles) {
|
|
5331
5601
|
const mains = roles.main;
|
|
5332
|
-
const withInterior = mains.filter((m) =>
|
|
5602
|
+
const withInterior = mains.filter((m) => existsSync14(path19.join(setDir, m, "get_metadata_interior.json")));
|
|
5333
5603
|
if (withInterior.length === 0) {
|
|
5334
5604
|
return { unavailable: "no interior geometry recorded for any main (pre-obligation set) \u2014 re-record with `tendril record` to enable crop checks" };
|
|
5335
5605
|
}
|
|
@@ -5346,7 +5616,7 @@ async function checkCropComposition(task, bundleDir, regions, opts = {}) {
|
|
|
5346
5616
|
if (typeof js !== "string") {
|
|
5347
5617
|
return regions.map((r) => ({ id: cropId(r), pass: false, similarity: 0, inkRecall: 0, detail: js.error }));
|
|
5348
5618
|
}
|
|
5349
|
-
const css = ["tokens.css", "styles.css"].map((f) =>
|
|
5619
|
+
const css = ["tokens.css", "styles.css"].map((f) => path19.join(bundleDir, f)).filter((f) => existsSync14(f)).map((f) => readFileSync11(f, "utf8")).join("\n");
|
|
5350
5620
|
const server = await chromium7.launchServer({ executablePath: resolveChrome(), headless: true, args: mountArgs() });
|
|
5351
5621
|
const browser = await chromium7.connect(server.wsEndpoint());
|
|
5352
5622
|
const PAD = 4;
|
|
@@ -5451,7 +5721,7 @@ async function checkStructuralComposition(task, bundleDir, roles, opts = {}) {
|
|
|
5451
5721
|
const deadlineMs = timeoutMs + 1e4;
|
|
5452
5722
|
const js = await compileInstrumentedMount(task, bundleDir);
|
|
5453
5723
|
if (typeof js !== "string") return [...results, ...mains.map((m) => ({ id: `composition:${m}`, pass: false, detail: js.error }))];
|
|
5454
|
-
const css = ["tokens.css", "styles.css"].map((f) =>
|
|
5724
|
+
const css = ["tokens.css", "styles.css"].map((f) => path19.join(bundleDir, f)).filter((f) => existsSync14(f)).map((f) => readFileSync11(f, "utf8")).join("\n");
|
|
5455
5725
|
const server = await chromium7.launchServer({ executablePath: resolveChrome(), headless: true, args: mountArgs() });
|
|
5456
5726
|
const browser = await chromium7.connect(server.wsEndpoint());
|
|
5457
5727
|
try {
|
|
@@ -5541,17 +5811,17 @@ export const jsxs = (t, p, k) => real.jsxs(t, inject(t, p), k);
|
|
|
5541
5811
|
});
|
|
5542
5812
|
|
|
5543
5813
|
// packages/verify/src/occlusion.ts
|
|
5544
|
-
import { existsSync as
|
|
5545
|
-
import
|
|
5814
|
+
import { existsSync as existsSync15 } from "node:fs";
|
|
5815
|
+
import path20 from "node:path";
|
|
5546
5816
|
import { build as build7 } from "esbuild";
|
|
5547
5817
|
import { chromium as chromium8 } from "playwright-core";
|
|
5548
5818
|
async function compileTwoUp(task, bundleDir) {
|
|
5549
|
-
const entryTsx =
|
|
5550
|
-
if (!
|
|
5819
|
+
const entryTsx = path20.join(bundleDir, task.entry);
|
|
5820
|
+
if (!existsSync15(entryTsx)) return { error: `${task.entry} missing` };
|
|
5551
5821
|
const src = `
|
|
5552
5822
|
import { createElement } from "react";
|
|
5553
5823
|
import { createRoot } from "react-dom/client";
|
|
5554
|
-
import * as B from ${JSON.stringify(
|
|
5824
|
+
import * as B from ${JSON.stringify(path20.resolve(entryTsx))};
|
|
5555
5825
|
const cfg = (window as unknown as { __cfg: { component: string; props: Record<string, unknown> } }).__cfg;
|
|
5556
5826
|
const C = (B as Record<string, unknown>)[cfg.component] as Parameters<typeof createElement>[0] | undefined;
|
|
5557
5827
|
for (const id of ["first", "second"]) {
|
|
@@ -5720,6 +5990,7 @@ var init_src4 = __esm({
|
|
|
5720
5990
|
init_prelude();
|
|
5721
5991
|
init_mount_limits();
|
|
5722
5992
|
init_font_collection();
|
|
5993
|
+
init_font_discovery();
|
|
5723
5994
|
init_font_faces();
|
|
5724
5995
|
init_font_resolve();
|
|
5725
5996
|
init_paths();
|
|
@@ -5730,23 +6001,23 @@ var init_src4 = __esm({
|
|
|
5730
6001
|
});
|
|
5731
6002
|
|
|
5732
6003
|
// packages/cli/src/environment.ts
|
|
5733
|
-
import { existsSync as
|
|
5734
|
-
import
|
|
6004
|
+
import { existsSync as existsSync16, readFileSync as readFileSync12 } from "node:fs";
|
|
6005
|
+
import path21 from "node:path";
|
|
5735
6006
|
import { createHash as createHash3 } from "node:crypto";
|
|
5736
6007
|
import { fileURLToPath as fileURLToPath5 } from "node:url";
|
|
5737
6008
|
function cliVersion() {
|
|
5738
6009
|
try {
|
|
5739
|
-
return JSON.parse(
|
|
6010
|
+
return JSON.parse(readFileSync12(path21.join(path21.dirname(fileURLToPath5(import.meta.url)), "..", "package.json"), "utf8")).version ?? "dev";
|
|
5740
6011
|
} catch {
|
|
5741
6012
|
return "dev";
|
|
5742
6013
|
}
|
|
5743
6014
|
}
|
|
5744
6015
|
function environmentStamp(taskFamilies) {
|
|
5745
|
-
const manifestPath2 =
|
|
6016
|
+
const manifestPath2 = path21.join(fontCacheDir(), "manifest.json");
|
|
5746
6017
|
let fontsHash = null;
|
|
5747
|
-
if (
|
|
6018
|
+
if (existsSync16(manifestPath2)) {
|
|
5748
6019
|
try {
|
|
5749
|
-
const entries = JSON.parse(
|
|
6020
|
+
const entries = JSON.parse(readFileSync12(manifestPath2, "utf8"));
|
|
5750
6021
|
const scoped = taskFamilies === void 0 || taskFamilies === null ? entries : entries.filter((f) => taskFamilies.some((fam) => fam.toLowerCase() === f.family.toLowerCase()));
|
|
5751
6022
|
const faces = scoped.map((f) => `${f.family}:${f.weight}:${f.sha256}`).sort();
|
|
5752
6023
|
fontsHash = faces.length === 0 ? null : createHash3("sha256").update(faces.join("\n")).digest("hex").slice(0, 16);
|
|
@@ -5790,8 +6061,8 @@ var init_describe = __esm({
|
|
|
5790
6061
|
});
|
|
5791
6062
|
|
|
5792
6063
|
// packages/cli/src/env.ts
|
|
5793
|
-
import { existsSync as
|
|
5794
|
-
import
|
|
6064
|
+
import { existsSync as existsSync17, readFileSync as readFileSync13 } from "node:fs";
|
|
6065
|
+
import path22 from "node:path";
|
|
5795
6066
|
function parseEnv(content) {
|
|
5796
6067
|
const entries = /* @__PURE__ */ new Map();
|
|
5797
6068
|
for (const line of content.split("\n")) {
|
|
@@ -5803,9 +6074,9 @@ function parseEnv(content) {
|
|
|
5803
6074
|
function resolveCredential(name) {
|
|
5804
6075
|
const fromProcess = process.env[name];
|
|
5805
6076
|
if (fromProcess) return fromProcess;
|
|
5806
|
-
const envPath =
|
|
5807
|
-
if (!
|
|
5808
|
-
return parseEnv(
|
|
6077
|
+
const envPath = path22.resolve(process.env["INIT_CWD"] ?? process.cwd(), ".env");
|
|
6078
|
+
if (!existsSync17(envPath)) return void 0;
|
|
6079
|
+
return parseEnv(readFileSync13(envPath, "utf8")).get(name);
|
|
5809
6080
|
}
|
|
5810
6081
|
var init_env = __esm({
|
|
5811
6082
|
"packages/cli/src/env.ts"() {
|
|
@@ -5865,17 +6136,17 @@ var init_output = __esm({
|
|
|
5865
6136
|
});
|
|
5866
6137
|
|
|
5867
6138
|
// packages/cli/src/entitlement.ts
|
|
5868
|
-
import { chmodSync, existsSync as
|
|
6139
|
+
import { chmodSync, existsSync as existsSync18, mkdirSync as mkdirSync4, readFileSync as readFileSync14, writeFileSync as writeFileSync7 } from "node:fs";
|
|
5869
6140
|
import crypto from "node:crypto";
|
|
5870
|
-
import
|
|
5871
|
-
import
|
|
6141
|
+
import os4 from "node:os";
|
|
6142
|
+
import path23 from "node:path";
|
|
5872
6143
|
function entitlementPath() {
|
|
5873
|
-
return process.env["TENDRIL_ENTITLEMENT_PATH"] ??
|
|
6144
|
+
return process.env["TENDRIL_ENTITLEMENT_PATH"] ?? path23.join(os4.homedir(), ".tendril", "entitlement.json");
|
|
5874
6145
|
}
|
|
5875
6146
|
function readStoredEntitlement(file = entitlementPath()) {
|
|
5876
|
-
if (!
|
|
6147
|
+
if (!existsSync18(file)) return void 0;
|
|
5877
6148
|
try {
|
|
5878
|
-
const parsed = JSON.parse(
|
|
6149
|
+
const parsed = JSON.parse(readFileSync14(file, "utf8"));
|
|
5879
6150
|
if (typeof parsed.token !== "string" || typeof parsed.lastRefreshAt !== "number") return void 0;
|
|
5880
6151
|
return { token: parsed.token, lastRefreshAt: parsed.lastRefreshAt };
|
|
5881
6152
|
} catch {
|
|
@@ -5883,7 +6154,7 @@ function readStoredEntitlement(file = entitlementPath()) {
|
|
|
5883
6154
|
}
|
|
5884
6155
|
}
|
|
5885
6156
|
function writeStoredEntitlement(stored, file = entitlementPath()) {
|
|
5886
|
-
mkdirSync4(
|
|
6157
|
+
mkdirSync4(path23.dirname(file), { recursive: true });
|
|
5887
6158
|
writeFileSync7(file, `${JSON.stringify(stored, null, 2)}
|
|
5888
6159
|
`);
|
|
5889
6160
|
chmodSync(file, 384);
|
|
@@ -5968,9 +6239,9 @@ var init_entitlement = __esm({
|
|
|
5968
6239
|
|
|
5969
6240
|
// packages/cli/src/commands/doctor.ts
|
|
5970
6241
|
import { spawnSync } from "node:child_process";
|
|
5971
|
-
import { existsSync as
|
|
5972
|
-
import
|
|
5973
|
-
import
|
|
6242
|
+
import { existsSync as existsSync19, readFileSync as readFileSync15, readdirSync as readdirSync4 } from "node:fs";
|
|
6243
|
+
import os5 from "node:os";
|
|
6244
|
+
import path24 from "node:path";
|
|
5974
6245
|
function withDeadline(work, ms) {
|
|
5975
6246
|
return Promise.race([
|
|
5976
6247
|
work,
|
|
@@ -6030,19 +6301,19 @@ async function runDoctorChecks(options) {
|
|
|
6030
6301
|
remediation: "Install Google Chrome (or Chromium), or set CHROME_PATH to a Chromium-family browser binary."
|
|
6031
6302
|
});
|
|
6032
6303
|
}
|
|
6033
|
-
const fontManifest =
|
|
6304
|
+
const fontManifest = path24.join(fontCacheDir(), "manifest.json");
|
|
6034
6305
|
checks.push(
|
|
6035
|
-
|
|
6306
|
+
existsSync19(fontManifest) ? { name: "font-cache", ok: true, detail: `resolved font cache at ${fontCacheDir()} (${JSON.parse(readFileSync15(fontManifest, "utf8")).length} faces)` } : {
|
|
6036
6307
|
name: "font-cache",
|
|
6037
6308
|
ok: true,
|
|
6038
6309
|
detail: `font cache empty at ${fontCacheDir()} \u2014 normal on a fresh machine; faces resolve per design system at first use`,
|
|
6039
6310
|
remediation: `Nothing to do now: \`${tendrilCommand("fonts resolve --set <recording-dir>")}\` fetches exactly what a recording declares, and generate/verify name that command \u2014 with the set filled in \u2014 when they need it.`
|
|
6040
6311
|
}
|
|
6041
6312
|
);
|
|
6042
|
-
const pluginRoot =
|
|
6043
|
-
if (
|
|
6313
|
+
const pluginRoot = path24.join(os5.homedir(), ".claude", "plugins", "cache", "tendrilapp", "tendril");
|
|
6314
|
+
if (existsSync19(pluginRoot)) {
|
|
6044
6315
|
try {
|
|
6045
|
-
const versions =
|
|
6316
|
+
const versions = readdirSync4(pluginRoot).filter((v) => /^\d+\.\d+\.\d+$/.test(v));
|
|
6046
6317
|
const newest = versions.sort((a, b) => versionIsNewer(a, b) ? 1 : -1)[0];
|
|
6047
6318
|
if (newest !== void 0) {
|
|
6048
6319
|
const skewed = versionIsNewer(newest, cliVersion());
|
|
@@ -7566,9 +7837,9 @@ __export(record_exports, {
|
|
|
7566
7837
|
runRecordPlan: () => runRecordPlan,
|
|
7567
7838
|
runRecordStatus: () => runRecordStatus
|
|
7568
7839
|
});
|
|
7569
|
-
import { existsSync as
|
|
7570
|
-
import
|
|
7571
|
-
import
|
|
7840
|
+
import { existsSync as existsSync21, mkdtempSync as mkdtempSync2, readFileSync as readFileSync17, readdirSync as readdirSync6 } from "node:fs";
|
|
7841
|
+
import os6 from "node:os";
|
|
7842
|
+
import path27 from "node:path";
|
|
7572
7843
|
import { writeFileSync as writeFileSync9 } from "node:fs";
|
|
7573
7844
|
function recordsInteractionState(reports) {
|
|
7574
7845
|
const evident = (s) => stateTokens(s).some((t) => INTERACTION_EVIDENCE_VALUES.has(t));
|
|
@@ -7591,7 +7862,7 @@ function interactionDisclosure(component, reports) {
|
|
|
7591
7862
|
};
|
|
7592
7863
|
}
|
|
7593
7864
|
function symbolsFromMetadataEnvelope(file, sourceFrame) {
|
|
7594
|
-
const env = JSON.parse(
|
|
7865
|
+
const env = JSON.parse(readFileSync17(file, "utf8"));
|
|
7595
7866
|
const text = env.content.map((c) => c.text ?? "").join("\n");
|
|
7596
7867
|
const symbols = [];
|
|
7597
7868
|
const walk2 = (node, ancestor) => {
|
|
@@ -7649,7 +7920,7 @@ function runRecordPlan(opts) {
|
|
|
7649
7920
|
if (rawFile !== void 0) {
|
|
7650
7921
|
try {
|
|
7651
7922
|
const envelope = rawEnvelopeFromFile(rawFile, opts.metadataRawPartsFile !== void 0);
|
|
7652
|
-
const tmp =
|
|
7923
|
+
const tmp = path27.join(mkdtempSync2(path27.join(os6.tmpdir(), "tendril-plan-meta-")), "get_metadata.json");
|
|
7653
7924
|
writeFileSync9(tmp, JSON.stringify(envelope));
|
|
7654
7925
|
metadataEntries.push({ file: tmp });
|
|
7655
7926
|
} catch (err) {
|
|
@@ -7671,7 +7942,7 @@ function runRecordPlan(opts) {
|
|
|
7671
7942
|
let metadataTruncated = false;
|
|
7672
7943
|
for (const { file, frame } of metadataEntries) {
|
|
7673
7944
|
try {
|
|
7674
|
-
const parsed = symbolsFromMetadataEnvelope(
|
|
7945
|
+
const parsed = symbolsFromMetadataEnvelope(path27.resolve(file), frame);
|
|
7675
7946
|
symbols.push(...parsed.symbols);
|
|
7676
7947
|
if (parsed.truncated) metadataTruncated = true;
|
|
7677
7948
|
} catch (err) {
|
|
@@ -7705,7 +7976,7 @@ function runRecordPlan(opts) {
|
|
|
7705
7976
|
if (symbols.length === 0) {
|
|
7706
7977
|
const leads = metadataEntries.flatMap(({ file }) => {
|
|
7707
7978
|
try {
|
|
7708
|
-
const env = JSON.parse(
|
|
7979
|
+
const env = JSON.parse(readFileSync17(path27.resolve(file), "utf8"));
|
|
7709
7980
|
return instanceLeads(env.content.map((c) => c.text ?? "").join("\n"));
|
|
7710
7981
|
} catch {
|
|
7711
7982
|
return [];
|
|
@@ -7779,7 +8050,7 @@ function runRecordPlan(opts) {
|
|
|
7779
8050
|
// a CLI flag and the MCP surface has no parameter for it.
|
|
7780
8051
|
decidedBy: "HUMAN ONLY \u2014 offer it, never choose it, and you cannot run it",
|
|
7781
8052
|
text: "Record a reduced pose set (anchor + one-factor sweeps + conflict crosses) instead of the full variant matrix. This buys calls with coverage \u2014 sampling is blind to multi-axis interactions and the report discloses the poses it skipped \u2014 so only the user may accept that trade. There is no tool parameter for it; the user runs it themselves in their own terminal, before any pose is recorded.",
|
|
7782
|
-
userRuns: [`rm ${quoteArg(
|
|
8053
|
+
userRuns: [`rm ${quoteArg(path27.join(opts.setDir, "recording-set.json"))}`, `${tendrilCommand(`record plan --set ${quoteArg(opts.setDir)} --component ${quoteArg(manifest.component)}`)} --sample`]
|
|
7783
8054
|
},
|
|
7784
8055
|
{
|
|
7785
8056
|
id: "larger-allowance",
|
|
@@ -7906,10 +8177,11 @@ function nextPayload(setDir) {
|
|
|
7906
8177
|
const instruction = nextInstruction(setDir);
|
|
7907
8178
|
const status = sessionStatus(setDir);
|
|
7908
8179
|
const progress = { recordedReps: status.reps.filter((x) => x.missing.length === 0).length, totalReps: status.reps.length };
|
|
7909
|
-
if (instruction === null && !
|
|
8180
|
+
if (instruction === null && !status.setTokenMapRecorded) {
|
|
7910
8181
|
const manifest = loadManifest(setDir);
|
|
7911
8182
|
const frameNode = Object.keys(manifest.sourceFrames ?? {})[0] ?? manifest.reps[0]?.nodeId ?? "";
|
|
7912
|
-
|
|
8183
|
+
const invalidNote = status.setTokenMapInvalid !== void 0 ? ` (the existing set-level file is UNUSABLE \u2014 ${status.setTokenMapInvalid} \u2014 re-record it)` : "";
|
|
8184
|
+
return { slug: "__set__", nodeId: frameNode, tool: "get_variable_defs", note: `SET-LEVEL: call get_variable_defs on the component frame and ingest with --rep __set__.${invalidNote} ${ENVELOPE_HELP}`, progress };
|
|
7913
8185
|
}
|
|
7914
8186
|
return instruction === null ? { complete: true, progress } : {
|
|
7915
8187
|
...instruction,
|
|
@@ -7929,13 +8201,13 @@ function runRecordNext(opts) {
|
|
|
7929
8201
|
const payload = nextPayload(opts.setDir);
|
|
7930
8202
|
emitData(opts, payload, () => {
|
|
7931
8203
|
if (payload["complete"] === true) {
|
|
7932
|
-
process.stdout.write("set complete \u2014 every planned rep has its required envelopes\n");
|
|
8204
|
+
process.stdout.write("set complete \u2014 every planned rep has its required envelopes, all usable, and the set-level token map is recorded\n");
|
|
7933
8205
|
return;
|
|
7934
8206
|
}
|
|
7935
8207
|
const progress = payload["progress"];
|
|
7936
8208
|
process.stdout.write(`[${progress.recordedReps}/${progress.totalReps} reps] ${payload["tool"]} for ${payload["slug"]} (node ${payload["nodeId"]})
|
|
7937
8209
|
\u2192 ${payload["note"]}
|
|
7938
|
-
\u2192 then: ${tendrilCommand(`record ingest --set ${
|
|
8210
|
+
\u2192 then: ${tendrilCommand(`record ingest --set ${path27.resolve(opts.setDir)} --rep ${payload["slug"]} --tool ${payload["tool"]} --file <envelope.json>`)}
|
|
7939
8211
|
`);
|
|
7940
8212
|
});
|
|
7941
8213
|
}
|
|
@@ -8009,7 +8281,7 @@ async function autoFetchAssets(setDir, rep, envelopeText2) {
|
|
|
8009
8281
|
const skipped = [];
|
|
8010
8282
|
const failed = [];
|
|
8011
8283
|
for (const { url, name } of assetUrlsFromEnvelopeText(envelopeText2)) {
|
|
8012
|
-
if (
|
|
8284
|
+
if (existsSync21(path27.join(setDir, rep, name))) {
|
|
8013
8285
|
skipped.push(name);
|
|
8014
8286
|
continue;
|
|
8015
8287
|
}
|
|
@@ -8031,16 +8303,16 @@ async function autoFetchAssets(setDir, rep, envelopeText2) {
|
|
|
8031
8303
|
}
|
|
8032
8304
|
function rawEnvelopeFromFile(file, parts) {
|
|
8033
8305
|
if (parts) {
|
|
8034
|
-
const blocks = JSON.parse(
|
|
8306
|
+
const blocks = JSON.parse(readFileSync17(path27.resolve(file), "utf8"));
|
|
8035
8307
|
if (!Array.isArray(blocks) || blocks.length === 0 || blocks.some((p) => typeof p !== "string")) throw new Error("parts file must be a non-empty JSON array of strings");
|
|
8036
8308
|
return { content: blocks.map((text) => ({ type: "text", text })) };
|
|
8037
8309
|
}
|
|
8038
|
-
return { content: [{ type: "text", text:
|
|
8310
|
+
return { content: [{ type: "text", text: readFileSync17(path27.resolve(file), "utf8") }] };
|
|
8039
8311
|
}
|
|
8040
8312
|
async function runRecordIngest(opts) {
|
|
8041
8313
|
let payload;
|
|
8042
8314
|
try {
|
|
8043
|
-
payload = opts.rawParts === true || opts.raw === true ? rawEnvelopeFromFile(opts.file, opts.rawParts === true) : JSON.parse(
|
|
8315
|
+
payload = opts.rawParts === true || opts.raw === true ? rawEnvelopeFromFile(opts.file, opts.rawParts === true) : JSON.parse(readFileSync17(path27.resolve(opts.file), "utf8"));
|
|
8044
8316
|
} catch (err) {
|
|
8045
8317
|
fail(opts, ExitCode.InputValidation, {
|
|
8046
8318
|
error: `cannot read envelope file: ${err instanceof Error ? err.message : String(err)}`,
|
|
@@ -8052,7 +8324,7 @@ async function runRecordIngest(opts) {
|
|
|
8052
8324
|
fail(opts, ExitCode.InputValidation, {
|
|
8053
8325
|
error: "--raw is for text tool responses; screenshots are binary",
|
|
8054
8326
|
code: "envelope-invalid",
|
|
8055
|
-
remediation: `Use \`${tendrilCommand(`record fetch --set ${
|
|
8327
|
+
remediation: `Use \`${tendrilCommand(`record fetch --set ${path27.resolve(opts.setDir)} --rep ${opts.rep} --tool ${opts.tool} --url <image_url>`)}\` instead.`
|
|
8056
8328
|
});
|
|
8057
8329
|
}
|
|
8058
8330
|
if (opts.rep === "__set__" && opts.tool === "get_variable_defs") {
|
|
@@ -8072,7 +8344,7 @@ async function runRecordIngest(opts) {
|
|
|
8072
8344
|
remediation: REINGEST_GUIDANCE
|
|
8073
8345
|
});
|
|
8074
8346
|
}
|
|
8075
|
-
writeFileSync9(
|
|
8347
|
+
writeFileSync9(path27.join(opts.setDir, "get_variable_defs.json"), `${JSON.stringify(payload, null, 1)}
|
|
8076
8348
|
`);
|
|
8077
8349
|
emitData(opts, { ingested: "get_variable_defs", rep: "__set__", setLevel: true, next: nextPayload(opts.setDir) }, () => {
|
|
8078
8350
|
process.stdout.write("set-level get_variable_defs ingested\n");
|
|
@@ -8088,7 +8360,7 @@ async function runRecordIngest(opts) {
|
|
|
8088
8360
|
if (assets !== void 0) {
|
|
8089
8361
|
for (const a of assets.fetched) process.stdout.write(` asset fetched ${a}
|
|
8090
8362
|
`);
|
|
8091
|
-
for (const f of assets.failed) process.stdout.write(` ASSET FAILED ${f.name}: ${f.reason} \u2014 download it, then: ${tendrilCommand(`record asset --set ${
|
|
8363
|
+
for (const f of assets.failed) process.stdout.write(` ASSET FAILED ${f.name}: ${f.reason} \u2014 download it, then: ${tendrilCommand(`record asset --set ${path27.resolve(opts.setDir)} --rep ${opts.rep} --name ${f.name} --file <downloaded-file>`)}
|
|
8092
8364
|
`);
|
|
8093
8365
|
}
|
|
8094
8366
|
});
|
|
@@ -8161,15 +8433,15 @@ async function runRecordIngestRep(opts) {
|
|
|
8161
8433
|
if (assets !== void 0) {
|
|
8162
8434
|
for (const a of assets.fetched) process.stdout.write(` asset fetched ${a}
|
|
8163
8435
|
`);
|
|
8164
|
-
for (const f of assets.failed) process.stdout.write(` ASSET FAILED ${f.name}: ${f.reason} \u2014 download it, then: ${tendrilCommand(`record asset --set ${
|
|
8436
|
+
for (const f of assets.failed) process.stdout.write(` ASSET FAILED ${f.name}: ${f.reason} \u2014 download it, then: ${tendrilCommand(`record asset --set ${path27.resolve(opts.setDir)} --rep ${opts.rep} --name ${f.name} --file <downloaded-file>`)}
|
|
8165
8437
|
`);
|
|
8166
8438
|
}
|
|
8167
8439
|
});
|
|
8168
8440
|
}
|
|
8169
8441
|
function runRecordAsset(opts) {
|
|
8170
8442
|
if (opts.dir !== void 0) {
|
|
8171
|
-
const dir =
|
|
8172
|
-
const names =
|
|
8443
|
+
const dir = path27.resolve(opts.dir);
|
|
8444
|
+
const names = readdirSync6(dir).filter((f) => /^asset-[\w.-]+\.(svg|png|jpe?g|webp|gif)$/i.test(f));
|
|
8173
8445
|
if (names.length === 0) {
|
|
8174
8446
|
fail(opts, ExitCode.InputValidation, {
|
|
8175
8447
|
error: `no asset-*.<ext> files found in ${dir}`,
|
|
@@ -8180,7 +8452,7 @@ function runRecordAsset(opts) {
|
|
|
8180
8452
|
const ingested = [];
|
|
8181
8453
|
try {
|
|
8182
8454
|
for (const name of names) {
|
|
8183
|
-
ingestAsset(opts.setDir, opts.rep, name,
|
|
8455
|
+
ingestAsset(opts.setDir, opts.rep, name, readFileSync17(path27.join(dir, name)));
|
|
8184
8456
|
ingested.push(name);
|
|
8185
8457
|
}
|
|
8186
8458
|
} catch (err) {
|
|
@@ -8200,11 +8472,11 @@ function runRecordAsset(opts) {
|
|
|
8200
8472
|
fail(opts, ExitCode.InputValidation, {
|
|
8201
8473
|
error: "pass --name and --file for a single asset, or --dir for a batch",
|
|
8202
8474
|
code: "asset-rejected",
|
|
8203
|
-
remediation: tendrilCommand(`record asset --set ${
|
|
8475
|
+
remediation: tendrilCommand(`record asset --set ${path27.resolve(opts.setDir)} --rep ${opts.rep} --dir <downloads-dir>`)
|
|
8204
8476
|
});
|
|
8205
8477
|
}
|
|
8206
8478
|
try {
|
|
8207
|
-
ingestAsset(opts.setDir, opts.rep, opts.name,
|
|
8479
|
+
ingestAsset(opts.setDir, opts.rep, opts.name, readFileSync17(path27.resolve(opts.file)));
|
|
8208
8480
|
emitData(opts, { rep: opts.rep, asset: opts.name }, () => {
|
|
8209
8481
|
process.stdout.write(`ingested ${opts.rep}/${opts.name}
|
|
8210
8482
|
`);
|
|
@@ -8223,10 +8495,21 @@ function runRecordStatus(opts) {
|
|
|
8223
8495
|
for (const r of status.reps) {
|
|
8224
8496
|
process.stdout.write(`${r.missing.length === 0 ? "DONE " : "TODO "} ${r.slug.padEnd(24)} recorded=[${r.recorded.join(",")}]${r.missing.length > 0 ? ` missing=[${r.missing.join(",")}]` : ""}
|
|
8225
8497
|
`);
|
|
8226
|
-
|
|
8227
|
-
|
|
8228
|
-
${status.reps.filter((r) => r.missing.length > 0).length} rep(s) pending
|
|
8498
|
+
for (const inv of r.invalid ?? []) {
|
|
8499
|
+
process.stdout.write(` ${r.slug}/${inv.tool}: recorded file is UNUSABLE \u2014 ${inv.reason} (re-record this call)
|
|
8229
8500
|
`);
|
|
8501
|
+
}
|
|
8502
|
+
}
|
|
8503
|
+
const pending = status.reps.filter((r) => r.missing.length > 0).length;
|
|
8504
|
+
process.stdout.write(
|
|
8505
|
+
status.complete ? "\nset complete\n" : pending > 0 ? `
|
|
8506
|
+
${pending} rep(s) pending
|
|
8507
|
+
` : status.setTokenMapInvalid !== void 0 ? `
|
|
8508
|
+
all reps recorded, but the SET-LEVEL token map file is UNUSABLE (${status.setTokenMapInvalid}) \u2014 re-record it via \`record next\`
|
|
8509
|
+
` : `
|
|
8510
|
+
all reps recorded, but the SET-LEVEL token map is missing \u2014 run \`record next\` for the get_variable_defs step (without it briefs ship an empty token table)
|
|
8511
|
+
`
|
|
8512
|
+
);
|
|
8230
8513
|
});
|
|
8231
8514
|
}
|
|
8232
8515
|
function narrowedRoles(derived, override) {
|
|
@@ -8240,7 +8523,7 @@ function narrowedRoles(derived, override) {
|
|
|
8240
8523
|
function rolesFromFile(opts, file, derived) {
|
|
8241
8524
|
let json;
|
|
8242
8525
|
try {
|
|
8243
|
-
json = JSON.parse(
|
|
8526
|
+
json = JSON.parse(readFileSync17(path27.resolve(file), "utf8"));
|
|
8244
8527
|
} catch (err) {
|
|
8245
8528
|
fail(opts, ExitCode.InputValidation, {
|
|
8246
8529
|
error: `cannot read roles file ${file}: ${err instanceof Error ? err.message.split("\n")[0] : String(err)}`,
|
|
@@ -8278,11 +8561,11 @@ function rolesFromFile(opts, file, derived) {
|
|
|
8278
8561
|
};
|
|
8279
8562
|
}
|
|
8280
8563
|
function runRecordFinish(opts) {
|
|
8281
|
-
if (!
|
|
8564
|
+
if (!existsSync21(path27.join(opts.setDir, "recording-set.json"))) {
|
|
8282
8565
|
fail(opts, ExitCode.InputValidation, {
|
|
8283
8566
|
error: `no recording-set.json in ${opts.setDir}`,
|
|
8284
8567
|
code: "no-recording-set",
|
|
8285
|
-
remediation: `Run \`${tendrilCommand(`record plan --set ${
|
|
8568
|
+
remediation: `Run \`${tendrilCommand(`record plan --set ${path27.resolve(opts.setDir)} --component <name> --metadata <envelope.json>`)}\` first \u2014 \`record finish\` closes a set the planner opened.`
|
|
8286
8569
|
});
|
|
8287
8570
|
}
|
|
8288
8571
|
const { manifest, raw } = readManifestFile(opts.setDir);
|
|
@@ -8310,17 +8593,17 @@ function runRecordFinish(opts) {
|
|
|
8310
8593
|
fail(opts, ExitCode.ConfirmationRequired, {
|
|
8311
8594
|
error: "--confirm-roles (and --roles-file) require an interactive terminal \u2014 this confirmation is human-only",
|
|
8312
8595
|
code: "roles-confirmation-not-interactive",
|
|
8313
|
-
remediation: `A human runs \`${tendrilCommand(`record finish --set ${
|
|
8596
|
+
remediation: `A human runs \`${tendrilCommand(`record finish --set ${path27.resolve(opts.setDir)} --confirm-roles`)}\` in their own terminal. Agents: show the proposal to your operator instead of confirming it.`
|
|
8314
8597
|
});
|
|
8315
8598
|
}
|
|
8316
8599
|
const merged = { ...raw, roles };
|
|
8317
|
-
const { issues } = validateRecordingSet(merged, (rel) =>
|
|
8600
|
+
const { issues } = validateRecordingSet(merged, (rel) => existsSync21(path27.join(opts.setDir, rel)));
|
|
8318
8601
|
const errors = issues.filter((i) => i.severity === "error");
|
|
8319
8602
|
if (errors.length > 0) {
|
|
8320
8603
|
fail(opts, ExitCode.InputValidation, {
|
|
8321
8604
|
error: `recording set rejected \u2014 roles NOT written: ${errors.map((e) => e.message).join("; ")}`,
|
|
8322
8605
|
code: "recording-set-invalid",
|
|
8323
|
-
remediation: `Fix the set (\`${tendrilCommand(`record status --set ${
|
|
8606
|
+
remediation: `Fix the set (\`${tendrilCommand(`record status --set ${path27.resolve(opts.setDir)}`)}\` lists missing envelopes) or the roles graph, then re-run \`record finish\`.`
|
|
8324
8607
|
});
|
|
8325
8608
|
}
|
|
8326
8609
|
for (const issue of issues) if (issue.severity === "warning") warn(opts, issue.message);
|
|
@@ -8554,8 +8837,8 @@ var init_engine_curated = __esm({
|
|
|
8554
8837
|
});
|
|
8555
8838
|
|
|
8556
8839
|
// packages/generate/src/loop.ts
|
|
8557
|
-
import { existsSync as
|
|
8558
|
-
import
|
|
8840
|
+
import { existsSync as existsSync22, mkdirSync as mkdirSync6, readFileSync as readFileSync18, renameSync, writeFileSync as writeFileSync10 } from "node:fs";
|
|
8841
|
+
import path28 from "node:path";
|
|
8559
8842
|
import { z as z11 } from "zod";
|
|
8560
8843
|
function objective(scores, behaviors) {
|
|
8561
8844
|
const vals = scores.map((s) => Math.min(s.similarity, s.inkRecall));
|
|
@@ -8592,9 +8875,9 @@ ${absentLines.join("\n")}` : ""}
|
|
|
8592
8875
|
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 credits a recorded pixel when ANY render ink lands within 1px of it, so ink=1 proves coverage \u2014 not presence, shape, or colour. A wrong-shaped mark over the right region, or a wrong-coloured one that is still ink, scores 1.000; and a recorded mark within ~30 channel-sum of the backdrop (#f6f6f6 on white is 27) is not ink at all, so it can be absent at ink 1.000 with no MISSING line. With ink=1 AND low sim, coverage held while pixels differ, so start with the TWO causes below \u2014 but never read ink=1 as "nothing is missing": confirm every faint or small recorded mark (hairlines, dividers, low-contrast controls) exists in your render. GEOMETRY: wrong position/size/radius; the diff shows shifted edges and bands. WRONG TEXT WEIGHT OR FACE: the diff shows a uniform haze over glyph runs; CSS binds the font FAMILY before the weight, a later family in the stack is never consulted for a weight the first one lacks, and font-synthesis: none rules out faux-bold \u2014 so a stack led by a family the kit holds at one weight renders every other weight in that face, silently. Check the font stack against the kit's cached faces before concluding geometry is at fault. 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."}`;
|
|
8593
8876
|
}
|
|
8594
8877
|
function archivePriorRun(outDir) {
|
|
8595
|
-
if (!
|
|
8878
|
+
if (!existsSync22(path28.join(outDir, "run-log.json")) && !existsSync22(path28.join(outDir, "loop-state.json"))) return void 0;
|
|
8596
8879
|
let n = 1;
|
|
8597
|
-
while (
|
|
8880
|
+
while (existsSync22(`${outDir}-prev-${n}`)) n += 1;
|
|
8598
8881
|
renameSync(outDir, `${outDir}-prev-${n}`);
|
|
8599
8882
|
return `${outDir}-prev-${n}`;
|
|
8600
8883
|
}
|
|
@@ -8603,14 +8886,14 @@ async function runEngineLoop(opts) {
|
|
|
8603
8886
|
const plateau = opts.plateau ?? 2;
|
|
8604
8887
|
const progress = opts.onProgress ?? (() => {
|
|
8605
8888
|
});
|
|
8606
|
-
const statePath =
|
|
8607
|
-
const resuming = opts.resume === true &&
|
|
8889
|
+
const statePath = path28.join(opts.outDir, "loop-state.json");
|
|
8890
|
+
const resuming = opts.resume === true && existsSync22(statePath);
|
|
8608
8891
|
if (!resuming) {
|
|
8609
8892
|
const archived = archivePriorRun(opts.outDir);
|
|
8610
8893
|
if (archived !== void 0) progress(`previous run archived to ${archived}`);
|
|
8611
8894
|
}
|
|
8612
8895
|
mkdirSync6(opts.outDir, { recursive: true });
|
|
8613
|
-
const scratch =
|
|
8896
|
+
const scratch = path28.join(opts.outDir, ".candidate");
|
|
8614
8897
|
let attempts = [];
|
|
8615
8898
|
let log = [];
|
|
8616
8899
|
let best;
|
|
@@ -8618,7 +8901,7 @@ async function runEngineLoop(opts) {
|
|
|
8618
8901
|
let nonAccepted = 0;
|
|
8619
8902
|
let stopReason = "max-iterations";
|
|
8620
8903
|
if (resuming) {
|
|
8621
|
-
const restored = LoopStateSchema.parse(JSON.parse(
|
|
8904
|
+
const restored = LoopStateSchema.parse(JSON.parse(readFileSync18(statePath, "utf8")));
|
|
8622
8905
|
attempts = restored.attempts;
|
|
8623
8906
|
log = restored.iterations;
|
|
8624
8907
|
spentUsd = restored.spentUsd;
|
|
@@ -8638,7 +8921,7 @@ async function runEngineLoop(opts) {
|
|
|
8638
8921
|
};
|
|
8639
8922
|
const writeCandidate = (files) => {
|
|
8640
8923
|
mkdirSync6(scratch, { recursive: true });
|
|
8641
|
-
for (const [name, content] of Object.entries(files)) writeFileSync10(
|
|
8924
|
+
for (const [name, content] of Object.entries(files)) writeFileSync10(path28.join(scratch, name), content);
|
|
8642
8925
|
};
|
|
8643
8926
|
const scoreCandidate = async (candidate, iter, usd, modelMs) => {
|
|
8644
8927
|
writeCandidate(candidate.files);
|
|
@@ -8696,8 +8979,8 @@ async function runEngineLoop(opts) {
|
|
|
8696
8979
|
const usd = candidate.usage?.usd ?? 0;
|
|
8697
8980
|
spentUsd += usd;
|
|
8698
8981
|
if (candidate.raw !== void 0) {
|
|
8699
|
-
mkdirSync6(
|
|
8700
|
-
writeFileSync10(
|
|
8982
|
+
mkdirSync6(path28.join(opts.outDir, "responses"), { recursive: true });
|
|
8983
|
+
writeFileSync10(path28.join(opts.outDir, "responses", `iter-${iter}.md`), candidate.raw);
|
|
8701
8984
|
}
|
|
8702
8985
|
if (candidate.files[opts.entry] === void 0 || candidate.files["styles.css"] === void 0) {
|
|
8703
8986
|
const finish = candidate.usage?.finishReason ?? "?";
|
|
@@ -8723,10 +9006,10 @@ async function runEngineLoop(opts) {
|
|
|
8723
9006
|
}
|
|
8724
9007
|
}
|
|
8725
9008
|
}
|
|
8726
|
-
if (best !== void 0) for (const [name, content] of Object.entries(best.files)) writeFileSync10(
|
|
9009
|
+
if (best !== void 0) for (const [name, content] of Object.entries(best.files)) writeFileSync10(path28.join(opts.outDir, name), content);
|
|
8727
9010
|
const final = best !== void 0 ? await opts.score(opts.outDir) : { scores: [], behaviors: [] };
|
|
8728
9011
|
writeFileSync10(
|
|
8729
|
-
|
|
9012
|
+
path28.join(opts.outDir, "run-log.json"),
|
|
8730
9013
|
`${JSON.stringify(
|
|
8731
9014
|
{
|
|
8732
9015
|
...opts.meta,
|
|
@@ -8793,8 +9076,8 @@ var init_loop2 = __esm({
|
|
|
8793
9076
|
});
|
|
8794
9077
|
|
|
8795
9078
|
// packages/generate/src/brief.ts
|
|
8796
|
-
import { existsSync as
|
|
8797
|
-
import
|
|
9079
|
+
import { existsSync as existsSync23, readFileSync as readFileSync19 } from "node:fs";
|
|
9080
|
+
import path29 from "node:path";
|
|
8798
9081
|
function singleAxes2(name) {
|
|
8799
9082
|
const parsed = parseVariantAxes(name);
|
|
8800
9083
|
if (parsed === void 0) return void 0;
|
|
@@ -9035,12 +9318,15 @@ function authorBehaviors(api, extras = {}) {
|
|
|
9035
9318
|
return { behaviors, prelude: { controls: interactive ? ["> *"] : [], textInputs: [] }, disclosures };
|
|
9036
9319
|
}
|
|
9037
9320
|
function envelopeText(file) {
|
|
9038
|
-
return envelopeFirstTextPart(JSON.parse(
|
|
9321
|
+
return envelopeFirstTextPart(JSON.parse(readFileSync19(file, "utf8")));
|
|
9322
|
+
}
|
|
9323
|
+
function metadataText(file) {
|
|
9324
|
+
return envelopeTextContent(JSON.parse(readFileSync19(file, "utf8")));
|
|
9039
9325
|
}
|
|
9040
9326
|
function dismissEvidence(setDir, repSlugs) {
|
|
9041
9327
|
for (const slug of repSlugs) {
|
|
9042
|
-
const f =
|
|
9043
|
-
if (!
|
|
9328
|
+
const f = path29.join(setDir, slug, "get_design_context.json");
|
|
9329
|
+
if (!existsSync23(f)) continue;
|
|
9044
9330
|
const text = envelopeText(f);
|
|
9045
9331
|
const propHit = /[{,]\s*(\w*dismiss\w*)\s*=\s*(?:true|false)\b/i.exec(text) ?? /\b(\w*dismiss\w*)\??\s*:\s*boolean\b/i.exec(text);
|
|
9046
9332
|
if (propHit !== null) return `emission prop "${propHit[1]}"`;
|
|
@@ -9084,13 +9370,13 @@ function recordedFontNeeds(setDir, opts = {}) {
|
|
|
9084
9370
|
}
|
|
9085
9371
|
};
|
|
9086
9372
|
const manifest = loadManifest(setDir);
|
|
9087
|
-
const setDefs =
|
|
9088
|
-
if (
|
|
9373
|
+
const setDefs = path29.join(setDir, "get_variable_defs.json");
|
|
9374
|
+
if (existsSync23(setDefs)) fromDefs(envelopeText(setDefs));
|
|
9089
9375
|
for (const rep of manifest.reps) {
|
|
9090
|
-
const ctx =
|
|
9091
|
-
if (
|
|
9092
|
-
const defs =
|
|
9093
|
-
if (
|
|
9376
|
+
const ctx = path29.join(setDir, rep.slug, "get_design_context.json");
|
|
9377
|
+
if (existsSync23(ctx)) fromEmission(envelopeText(ctx));
|
|
9378
|
+
const defs = path29.join(setDir, rep.slug, "get_variable_defs.json");
|
|
9379
|
+
if (existsSync23(defs)) fromDefs(envelopeText(defs));
|
|
9094
9380
|
}
|
|
9095
9381
|
return [...byFamily.entries()].map(([family, paired]) => {
|
|
9096
9382
|
const weights = /* @__PURE__ */ new Set([...paired, ...opts.pairedOnly === true ? [] : unpaired]);
|
|
@@ -9101,10 +9387,10 @@ function symbolFontGlyphCount(setDir, reps) {
|
|
|
9101
9387
|
const PUA = /[\uE000-\uF8FF\u{F0000}-\u{FFFFD}\u{100000}-\u{10FFFD}]/u;
|
|
9102
9388
|
const glyphs = /* @__PURE__ */ new Set();
|
|
9103
9389
|
for (const rep of reps) {
|
|
9104
|
-
const file =
|
|
9105
|
-
if (!
|
|
9390
|
+
const file = path29.join(setDir, rep, "get_metadata.json");
|
|
9391
|
+
if (!existsSync23(file)) continue;
|
|
9106
9392
|
try {
|
|
9107
|
-
const text = JSON.parse(
|
|
9393
|
+
const text = JSON.parse(readFileSync19(file, "utf8")).content.map((c) => c.text ?? "").join("\n");
|
|
9108
9394
|
for (const m of text.matchAll(/<text\s[^>]*name="([^"]*)"/g)) {
|
|
9109
9395
|
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)));
|
|
9110
9396
|
if (PUA.test(name)) glyphs.add(name);
|
|
@@ -9130,8 +9416,8 @@ function recordedTextSlots(setDir, repSlugs) {
|
|
|
9130
9416
|
const propRep = [];
|
|
9131
9417
|
const perRep = [];
|
|
9132
9418
|
for (const slug of repSlugs) {
|
|
9133
|
-
const f =
|
|
9134
|
-
if (!
|
|
9419
|
+
const f = path29.join(setDir, slug, "get_design_context.json");
|
|
9420
|
+
if (!existsSync23(f)) continue;
|
|
9135
9421
|
const code = envelopeText(f);
|
|
9136
9422
|
const props = /* @__PURE__ */ new Map();
|
|
9137
9423
|
for (const m of code.matchAll(/[{,]\s*(\w+)\s*=\s*"((?:[^"\\]|\\.)*)"/g)) {
|
|
@@ -9156,9 +9442,9 @@ function recordedTextSlots(setDir, repSlugs) {
|
|
|
9156
9442
|
}
|
|
9157
9443
|
const axisValuesBySlug = /* @__PURE__ */ new Map();
|
|
9158
9444
|
for (const slug of repSlugs) {
|
|
9159
|
-
const metaFile =
|
|
9160
|
-
if (!
|
|
9161
|
-
const name = symbolName(
|
|
9445
|
+
const metaFile = path29.join(setDir, slug, "get_metadata.json");
|
|
9446
|
+
if (!existsSync23(metaFile)) continue;
|
|
9447
|
+
const name = symbolName(metadataText(metaFile));
|
|
9162
9448
|
if (name === void 0) continue;
|
|
9163
9449
|
const values = /* @__PURE__ */ new Set();
|
|
9164
9450
|
for (const part of name.split(",")) {
|
|
@@ -9256,12 +9542,12 @@ function authorTaskFromSet(setDir, opts = {}) {
|
|
|
9256
9542
|
const poses = [];
|
|
9257
9543
|
const missing = [];
|
|
9258
9544
|
for (const rep of manifest.reps) {
|
|
9259
|
-
const metaFile =
|
|
9260
|
-
if (!
|
|
9545
|
+
const metaFile = path29.join(setDir, rep.slug, "get_metadata.json");
|
|
9546
|
+
if (!existsSync23(metaFile)) {
|
|
9261
9547
|
missing.push(rep.slug);
|
|
9262
9548
|
continue;
|
|
9263
9549
|
}
|
|
9264
|
-
const name = symbolName(
|
|
9550
|
+
const name = symbolName(metadataText(metaFile));
|
|
9265
9551
|
if (name === void 0) {
|
|
9266
9552
|
missing.push(rep.slug);
|
|
9267
9553
|
continue;
|
|
@@ -9271,8 +9557,8 @@ function authorTaskFromSet(setDir, opts = {}) {
|
|
|
9271
9557
|
if (missing.length > 0) {
|
|
9272
9558
|
throw new Error(`recording set ${setDir} is missing metadata for ${missing.length} rep(s): ${missing.slice(0, 5).join(", ")}${missing.length > 5 ? ", \u2026" : ""}`);
|
|
9273
9559
|
}
|
|
9274
|
-
const setMeta =
|
|
9275
|
-
const latticeNames = manifest.latticeNames ?? (
|
|
9560
|
+
const setMeta = path29.join(setDir, "get_metadata.json");
|
|
9561
|
+
const latticeNames = manifest.latticeNames ?? (existsSync23(setMeta) ? [...metadataText(setMeta).matchAll(/name="([^"]*)"/g)].map((m) => decodeXmlEntities(m[1])) : void 0);
|
|
9276
9562
|
const defaults = { ...manifest.defaults ?? {}, ...opts.defaults ?? {} };
|
|
9277
9563
|
const recordedFonts = recordedFontFamilies(setDir);
|
|
9278
9564
|
const textSlots = recordedTextSlots(setDir, manifest.reps.map((r) => r.slug));
|
|
@@ -9455,10 +9741,10 @@ var init_brief = __esm({
|
|
|
9455
9741
|
});
|
|
9456
9742
|
|
|
9457
9743
|
// packages/generate/src/segments.ts
|
|
9458
|
-
import { existsSync as
|
|
9459
|
-
import
|
|
9744
|
+
import { existsSync as existsSync24, readFileSync as readFileSync20, readdirSync as readdirSync7 } from "node:fs";
|
|
9745
|
+
import path30 from "node:path";
|
|
9460
9746
|
function repText(set, rep, tool) {
|
|
9461
|
-
const env = JSON.parse(
|
|
9747
|
+
const env = JSON.parse(readFileSync20(path30.join(set, rep, `${tool}.json`), "utf8"));
|
|
9462
9748
|
return tool === "get_design_context" ? envelopeFirstTextPart(env) : envelopeTextContent(env);
|
|
9463
9749
|
}
|
|
9464
9750
|
function stripFigmaInstructions(emission) {
|
|
@@ -9518,18 +9804,20 @@ DROPPED from the map, untrustworthy in the source export \u2014 for these, each
|
|
|
9518
9804
|
}
|
|
9519
9805
|
function buildSegments(task, mode = "fenced") {
|
|
9520
9806
|
const SET = task.set;
|
|
9807
|
+
let defsRecorded = existsSync24(path30.join(SET, "get_variable_defs.json"));
|
|
9521
9808
|
let rawDefs = {};
|
|
9522
|
-
if (
|
|
9523
|
-
const text = envelopeFirstTextPart(JSON.parse(
|
|
9809
|
+
if (existsSync24(path30.join(SET, "get_variable_defs.json"))) {
|
|
9810
|
+
const text = envelopeFirstTextPart(JSON.parse(readFileSync20(path30.join(SET, "get_variable_defs.json"), "utf8"))) || "{}";
|
|
9524
9811
|
try {
|
|
9525
9812
|
rawDefs = JSON.parse(text);
|
|
9526
9813
|
} catch {
|
|
9527
9814
|
}
|
|
9528
9815
|
} else {
|
|
9529
9816
|
for (const cfg of task.configs) {
|
|
9530
|
-
const f =
|
|
9531
|
-
if (!
|
|
9532
|
-
|
|
9817
|
+
const f = path30.join(SET, cfg.rep, "get_variable_defs.json");
|
|
9818
|
+
if (!existsSync24(f)) continue;
|
|
9819
|
+
defsRecorded = true;
|
|
9820
|
+
const text = envelopeFirstTextPart(JSON.parse(readFileSync20(f, "utf8"))) || "{}";
|
|
9533
9821
|
try {
|
|
9534
9822
|
for (const [k, v] of Object.entries(JSON.parse(text))) rawDefs[k] ??= v;
|
|
9535
9823
|
} catch {
|
|
@@ -9537,25 +9825,27 @@ function buildSegments(task, mode = "fenced") {
|
|
|
9537
9825
|
}
|
|
9538
9826
|
}
|
|
9539
9827
|
const emissionTexts = task.configs.map((cfg) => {
|
|
9540
|
-
const f =
|
|
9541
|
-
return
|
|
9828
|
+
const f = path30.join(SET, cfg.rep, "get_design_context.json");
|
|
9829
|
+
return existsSync24(f) ? envelopeFirstTextPart(JSON.parse(readFileSync20(f, "utf8"))) : "";
|
|
9542
9830
|
});
|
|
9543
9831
|
const { map, note } = cleanTokenMap(rawDefs, emissionTexts);
|
|
9544
9832
|
const defs = JSON.stringify(map, null, 1);
|
|
9545
9833
|
const parts = [
|
|
9546
9834
|
"Implement the component family below. For each recorded config: the symbol box, Figma's emission (intent \u2014 write your own clean implementation; emissions are VERBATIM Figma output and may contain non-compiling artifacts like duplicate consts \u2014 never copy them as code), and its SVG assets (inline these; network is denied at render).",
|
|
9547
|
-
`
|
|
9835
|
+
defsRecorded ? `
|
|
9548
9836
|
## Design tokens (use these CSS custom property names)
|
|
9549
9837
|
\`\`\`json
|
|
9550
9838
|
${defs}
|
|
9551
|
-
\`\`\`${note}`
|
|
9839
|
+
\`\`\`${note}` : `
|
|
9840
|
+
## Design tokens
|
|
9841
|
+
TOKEN MAP NEVER RECORDED: this set holds no get_variable_defs envelope, so whether the kit exposes tokens is UNKNOWN \u2014 not answered. Literal values are your only option here and are correct to use; do NOT invent token names. (The set can record the map later: \`record next\` asks for get_variable_defs once, at set level.)${note}`
|
|
9552
9842
|
];
|
|
9553
9843
|
for (const cfg of task.configs) {
|
|
9554
9844
|
const meta = stripFigmaInstructions(repText(SET, cfg.rep, "get_metadata"));
|
|
9555
9845
|
const emission = stripFigmaInstructions(repText(SET, cfg.rep, "get_design_context"));
|
|
9556
|
-
const assets =
|
|
9846
|
+
const assets = readdirSync7(path30.join(SET, cfg.rep)).filter((f) => f.startsWith("asset-") && f.endsWith(".svg")).map((f) => `asset ${f}:
|
|
9557
9847
|
\`\`\`svg
|
|
9558
|
-
${
|
|
9848
|
+
${readFileSync20(path30.join(SET, cfg.rep, f), "utf8")}
|
|
9559
9849
|
\`\`\``).join("\n");
|
|
9560
9850
|
parts.push(`
|
|
9561
9851
|
## Config ${cfg.rep} \u2192 ${cfg.component} ${JSON.stringify(cfg.props)}
|
|
@@ -9580,7 +9870,7 @@ Optionally a third block with \`/* FILE: tokens.css */\`.`);
|
|
|
9580
9870
|
} else {
|
|
9581
9871
|
parts.push(`
|
|
9582
9872
|
## Output format
|
|
9583
|
-
Write the COMPLETE files (${task.entry}, styles.css, optional tokens.css) into ONE candidate directory: \`tendril-out/${
|
|
9873
|
+
Write the COMPLETE files (${task.entry}, styles.css, optional tokens.css) into ONE candidate directory: \`tendril-out/${path30.basename(task.set)}-candidate/\` unless you were handed another path. Pass that same directory to \`tendril engine score\` every round and keep writing into it \u2014 the scorer stamps the bundle there, writes its evidence beside your files, and appends its score-history.jsonl lines there (one when a round starts, one when it scores); a fresh directory each round throws all of that away. Do not paste file contents into chat \u2014 the scorer reads the directory.`);
|
|
9584
9874
|
}
|
|
9585
9875
|
return parts.join("\n");
|
|
9586
9876
|
}
|
|
@@ -9648,8 +9938,8 @@ var init_adapter = __esm({
|
|
|
9648
9938
|
|
|
9649
9939
|
// packages/generate/src/bundle-emit.ts
|
|
9650
9940
|
import { createHash as createHash4 } from "node:crypto";
|
|
9651
|
-
import { copyFileSync, existsSync as
|
|
9652
|
-
import
|
|
9941
|
+
import { copyFileSync, existsSync as existsSync25, mkdirSync as mkdirSync7, readFileSync as readFileSync21, readdirSync as readdirSync8, rmSync as rmSync3, writeFileSync as writeFileSync11 } from "node:fs";
|
|
9942
|
+
import path31 from "node:path";
|
|
9653
9943
|
function pinFromConfigs(configs) {
|
|
9654
9944
|
const domains = /* @__PURE__ */ new Map();
|
|
9655
9945
|
const kinds = /* @__PURE__ */ new Map();
|
|
@@ -9718,15 +10008,23 @@ function fontsCssFor(faces, bundleDir, cacheDir = DEFAULT_FONT_CACHE, substitute
|
|
|
9718
10008
|
const notices = [];
|
|
9719
10009
|
const licenseTexts = /* @__PURE__ */ new Map();
|
|
9720
10010
|
for (const face of faces) {
|
|
9721
|
-
const src =
|
|
9722
|
-
const target = `./fonts/${
|
|
9723
|
-
const format = FONT_FORMATS[
|
|
10011
|
+
const src = path31.join(cacheDir, path31.basename(face.file));
|
|
10012
|
+
const target = `./fonts/${path31.basename(face.file)}`;
|
|
10013
|
+
const format = FONT_FORMATS[path31.extname(face.file).toLowerCase()] ?? "truetype";
|
|
9724
10014
|
const family = face.family.replace(/['\\]/g, "").replace(/\*\//g, "");
|
|
9725
10015
|
const decl = `@font-face { font-family: '${family}'; font-weight: ${face.weight}; font-style: normal; src: url(${target}) format('${format}'); }`;
|
|
9726
10016
|
const license = normalizeFontLicense(face.license);
|
|
9727
|
-
const terms = REDISTRIBUTABLE.get(license);
|
|
10017
|
+
const terms = face.source.startsWith("system:") || face.source.startsWith("local:") ? void 0 : REDISTRIBUTABLE.get(license);
|
|
9728
10018
|
if (terms === void 0) {
|
|
9729
|
-
if (face.source.startsWith("
|
|
10019
|
+
if (face.source.startsWith("system:")) {
|
|
10020
|
+
lines.push(
|
|
10021
|
+
`/* '${family}' ${face.weight} came from this machine's installed system fonts (tendril fonts`,
|
|
10022
|
+
` add-system; sha256 ${face.sha256.slice(0, 16)}\u2026). OS-bundled faces are never copied into`,
|
|
10023
|
+
" bundles regardless of any recorded licence \u2014 the consuming machine provides its own copy,",
|
|
10024
|
+
` or you place one you licence at ${target} and uncomment: */`,
|
|
10025
|
+
`/* ${decl} */`
|
|
10026
|
+
);
|
|
10027
|
+
} else if (face.source.startsWith("local:")) {
|
|
9730
10028
|
lines.push(
|
|
9731
10029
|
`/* '${family}' ${face.weight} is a user-licensed face (tendril fonts add; sha256 ${face.sha256.slice(0, 16)}\u2026).`,
|
|
9732
10030
|
` Licensed bytes are never copied into bundles \u2014 place your copy at ${target} and uncomment: */`,
|
|
@@ -9750,14 +10048,14 @@ function fontsCssFor(faces, bundleDir, cacheDir = DEFAULT_FONT_CACHE, substitute
|
|
|
9750
10048
|
`/* ${decl} */`
|
|
9751
10049
|
);
|
|
9752
10050
|
}
|
|
9753
|
-
} else if (
|
|
9754
|
-
mkdirSync7(
|
|
9755
|
-
copyFileSync(src,
|
|
10051
|
+
} else if (existsSync25(src) && createHash4("sha256").update(readFileSync21(src)).digest("hex") === face.sha256) {
|
|
10052
|
+
mkdirSync7(path31.join(bundleDir, "fonts"), { recursive: true });
|
|
10053
|
+
copyFileSync(src, path31.join(bundleDir, "fonts", path31.basename(face.file)));
|
|
9756
10054
|
licenseTexts.set(terms.file, terms.text);
|
|
9757
10055
|
const upstream = upstreamAttribution(face);
|
|
9758
10056
|
notices.push(
|
|
9759
10057
|
"",
|
|
9760
|
-
`${face.family.replace(/[\r\n]+/g, " ")} ${face.weight} \u2014 ./${
|
|
10058
|
+
`${face.family.replace(/[\r\n]+/g, " ")} ${face.weight} \u2014 ./${path31.basename(face.file)}`,
|
|
9761
10059
|
` licence: ${terms.title} \u2014 full text in ./${terms.file}`,
|
|
9762
10060
|
` source: ${face.source}`,
|
|
9763
10061
|
` sha256: ${face.sha256}`,
|
|
@@ -9771,9 +10069,9 @@ function fontsCssFor(faces, bundleDir, cacheDir = DEFAULT_FONT_CACHE, substitute
|
|
|
9771
10069
|
}
|
|
9772
10070
|
if (lines.length === 0) return null;
|
|
9773
10071
|
if (notices.length > 0) {
|
|
9774
|
-
const fontsDir =
|
|
9775
|
-
for (const [file, text] of licenseTexts) writeFileSync11(
|
|
9776
|
-
writeFileSync11(
|
|
10072
|
+
const fontsDir = path31.join(bundleDir, "fonts");
|
|
10073
|
+
for (const [file, text] of licenseTexts) writeFileSync11(path31.join(fontsDir, file), text);
|
|
10074
|
+
writeFileSync11(path31.join(fontsDir, "NOTICE.txt"), `${[NOTICE_PREAMBLE, ...notices].join("\n")}
|
|
9777
10075
|
`);
|
|
9778
10076
|
header.push(
|
|
9779
10077
|
"/* REDISTRIBUTION: the face files in ./fonts/ are redistributed under the licences",
|
|
@@ -9785,10 +10083,10 @@ function fontsCssFor(faces, bundleDir, cacheDir = DEFAULT_FONT_CACHE, substitute
|
|
|
9785
10083
|
`;
|
|
9786
10084
|
}
|
|
9787
10085
|
function countLatticeSymbols(setDir) {
|
|
9788
|
-
const manifestFile =
|
|
9789
|
-
if (
|
|
10086
|
+
const manifestFile = path31.join(setDir, "recording-set.json");
|
|
10087
|
+
if (existsSync25(manifestFile)) {
|
|
9790
10088
|
try {
|
|
9791
|
-
const stored = JSON.parse(
|
|
10089
|
+
const stored = JSON.parse(readFileSync21(manifestFile, "utf8"));
|
|
9792
10090
|
if (stored.variantScope !== "component-set") return null;
|
|
9793
10091
|
const lattice = stored.latticeNames;
|
|
9794
10092
|
if (lattice !== void 0 && lattice.length > 0) return lattice.length;
|
|
@@ -9796,13 +10094,13 @@ function countLatticeSymbols(setDir) {
|
|
|
9796
10094
|
}
|
|
9797
10095
|
}
|
|
9798
10096
|
const files = [
|
|
9799
|
-
|
|
9800
|
-
...
|
|
9801
|
-
].filter((f) =>
|
|
10097
|
+
path31.join(setDir, "get_metadata.json"),
|
|
10098
|
+
...existsSync25(setDir) ? readdirSync8(setDir).filter((f) => /^get_metadata-.*\.json$/.test(f)).map((f) => path31.join(setDir, f)) : []
|
|
10099
|
+
].filter((f) => existsSync25(f));
|
|
9802
10100
|
if (files.length === 0) return null;
|
|
9803
10101
|
let count = 0;
|
|
9804
10102
|
for (const f of files) {
|
|
9805
|
-
const text = envelopeTextContent(JSON.parse(
|
|
10103
|
+
const text = envelopeTextContent(JSON.parse(readFileSync21(f, "utf8")));
|
|
9806
10104
|
count += [...text.matchAll(/name="([^"]*)"/g)].filter((m) => m[1].includes("=")).length;
|
|
9807
10105
|
}
|
|
9808
10106
|
return count > 0 ? count : null;
|
|
@@ -9810,21 +10108,21 @@ function countLatticeSymbols(setDir) {
|
|
|
9810
10108
|
function recordingSetHash(setDir, configs) {
|
|
9811
10109
|
const relPaths = [];
|
|
9812
10110
|
for (const name of ["recording-set.json", "completeness-manifest.json", "get_variable_defs.json", "get_metadata.json"]) {
|
|
9813
|
-
if (
|
|
10111
|
+
if (existsSync25(path31.join(setDir, name))) relPaths.push(name);
|
|
9814
10112
|
}
|
|
9815
10113
|
for (const cfg of configs) {
|
|
9816
10114
|
for (const f of ["get_design_context.json", "get_metadata.json", "get_screenshot.json", "get_variable_defs.json"]) {
|
|
9817
|
-
if (
|
|
10115
|
+
if (existsSync25(path31.join(setDir, cfg.rep, f))) relPaths.push(`${cfg.rep}/${f}`);
|
|
9818
10116
|
}
|
|
9819
|
-
if (
|
|
9820
|
-
for (const asset of
|
|
10117
|
+
if (existsSync25(path31.join(setDir, cfg.rep))) {
|
|
10118
|
+
for (const asset of readdirSync8(path31.join(setDir, cfg.rep)).filter((f) => f.startsWith("asset-"))) {
|
|
9821
10119
|
relPaths.push(`${cfg.rep}/${asset}`);
|
|
9822
10120
|
}
|
|
9823
10121
|
}
|
|
9824
10122
|
}
|
|
9825
10123
|
return hashRecordingSet(
|
|
9826
10124
|
relPaths,
|
|
9827
|
-
(p) => new Uint8Array(
|
|
10125
|
+
(p) => new Uint8Array(readFileSync21(path31.join(setDir, p))),
|
|
9828
10126
|
(chunks) => {
|
|
9829
10127
|
const h = createHash4("sha256");
|
|
9830
10128
|
for (const c of chunks) h.update(c);
|
|
@@ -9845,10 +10143,11 @@ function emitBundleV1(opts) {
|
|
|
9845
10143
|
const certified = statuses.filter((s) => s.status === "certified").length;
|
|
9846
10144
|
const pass = statuses.filter((s) => s.status !== "fail").length;
|
|
9847
10145
|
const lattice = countLatticeSymbols(opts.task.set);
|
|
9848
|
-
const interaction = opts.behaviors.filter((b) => !b.id.startsWith("prelude:"));
|
|
10146
|
+
const interaction = opts.behaviors.filter((b) => !b.id.startsWith("prelude:") && !b.id.startsWith("parity:"));
|
|
9849
10147
|
const prelude = opts.behaviors.filter((b) => b.id.startsWith("prelude:"));
|
|
9850
|
-
const
|
|
9851
|
-
const
|
|
10148
|
+
const parity = opts.behaviors.filter((b) => b.id.startsWith("parity:"));
|
|
10149
|
+
const cssFiles = ["styles.css", "tokens.css"].map((f) => path31.join(opts.bundleDir, f)).filter((f) => existsSync25(f));
|
|
10150
|
+
const families = cssFontFamilies(cssFiles.map((f) => readFileSync21(f, "utf8")).join("\n"));
|
|
9852
10151
|
const requiredFonts = requiredFontsManifest(families, opts.fontCacheDir).map((f) => ({
|
|
9853
10152
|
family: f.family,
|
|
9854
10153
|
weight: f.weight,
|
|
@@ -9877,7 +10176,7 @@ function emitBundleV1(opts) {
|
|
|
9877
10176
|
// resolvable via verify's --set override).
|
|
9878
10177
|
path: (() => {
|
|
9879
10178
|
const base = process.env["INIT_CWD"] ?? process.cwd();
|
|
9880
|
-
const rel =
|
|
10179
|
+
const rel = path31.relative(base, opts.task.set);
|
|
9881
10180
|
return rel !== "" && !rel.startsWith("..") ? rel : opts.task.set;
|
|
9882
10181
|
})(),
|
|
9883
10182
|
component: opts.componentName,
|
|
@@ -9901,21 +10200,21 @@ function emitBundleV1(opts) {
|
|
|
9901
10200
|
})
|
|
9902
10201
|
};
|
|
9903
10202
|
const written = [];
|
|
9904
|
-
const manifestPath2 =
|
|
10203
|
+
const manifestPath2 = path31.join(opts.bundleDir, "component.json");
|
|
9905
10204
|
writeFileSync11(manifestPath2, `${JSON.stringify(manifest, null, 2)}
|
|
9906
10205
|
`);
|
|
9907
10206
|
written.push(manifestPath2);
|
|
9908
|
-
const stylesPath =
|
|
9909
|
-
if (
|
|
10207
|
+
const stylesPath = path31.join(opts.bundleDir, "styles.css");
|
|
10208
|
+
if (existsSync25(stylesPath)) {
|
|
9910
10209
|
const comment = cssProvenanceComment({ pass, scored: statuses.length, certified, latticeConfigs: lattice });
|
|
9911
|
-
const current =
|
|
10210
|
+
const current = readFileSync21(stylesPath, "utf8");
|
|
9912
10211
|
const stripped = current.replace(/^\/\* tendril bundle v\d+ [^]*?\*\/\n/, "");
|
|
9913
10212
|
writeFileSync11(stylesPath, `${comment}
|
|
9914
10213
|
${stripped}`);
|
|
9915
10214
|
written.push(stylesPath);
|
|
9916
10215
|
}
|
|
9917
|
-
const fontsCssPath =
|
|
9918
|
-
rmSync3(
|
|
10216
|
+
const fontsCssPath = path31.join(opts.bundleDir, "fonts.css");
|
|
10217
|
+
rmSync3(path31.join(opts.bundleDir, "fonts"), { recursive: true, force: true });
|
|
9919
10218
|
rmSync3(fontsCssPath, { force: true });
|
|
9920
10219
|
const fontsCss = fontsCssFor(requiredFontsManifest(families, opts.fontCacheDir), opts.bundleDir, opts.fontCacheDir, opts.substitutedFamilies ?? []);
|
|
9921
10220
|
if (fontsCss !== null) {
|
|
@@ -9923,7 +10222,7 @@ ${stripped}`);
|
|
|
9923
10222
|
written.push(fontsCssPath);
|
|
9924
10223
|
}
|
|
9925
10224
|
const unrecorded = lattice === null ? null : Math.max(0, lattice - statuses.length);
|
|
9926
|
-
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\``;
|
|
10225
|
+
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}`) + (parity.length === 0 ? "" : `, state parity ${parity.filter((b) => b.pass).length}/${parity.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\``;
|
|
9927
10226
|
return { manifest, statusLine, written };
|
|
9928
10227
|
}
|
|
9929
10228
|
var FONT_FORMATS, BARS, NOTICE_PREAMBLE, OFL_1_1_TEXT, UFL_1_0_TEXT, APACHE_2_0_TEXT, REDISTRIBUTABLE;
|
|
@@ -10360,9 +10659,9 @@ var init_src7 = __esm({
|
|
|
10360
10659
|
});
|
|
10361
10660
|
|
|
10362
10661
|
// packages/cli/src/font-guidance.ts
|
|
10363
|
-
import
|
|
10662
|
+
import path32 from "node:path";
|
|
10364
10663
|
function fontsUnprovenRemediation(setDir) {
|
|
10365
|
-
const set = setDir === void 0 ? void 0 :
|
|
10664
|
+
const set = setDir === void 0 ? void 0 : path32.resolve(setDir);
|
|
10366
10665
|
if (set !== void 0) {
|
|
10367
10666
|
try {
|
|
10368
10667
|
const needs = recordedFontNeeds(set);
|
|
@@ -10430,20 +10729,28 @@ __export(fonts_exports, {
|
|
|
10430
10729
|
DEFAULT_FONT_CACHE: () => DEFAULT_FONT_CACHE,
|
|
10431
10730
|
familyMismatch: () => familyMismatch,
|
|
10432
10731
|
runFontsAdd: () => runFontsAdd,
|
|
10732
|
+
runFontsAddSystem: () => runFontsAddSystem,
|
|
10733
|
+
runFontsDiscover: () => runFontsDiscover,
|
|
10433
10734
|
runFontsRequired: () => runFontsRequired,
|
|
10434
10735
|
runFontsResolve: () => runFontsResolve,
|
|
10435
10736
|
runFontsResolveSet: () => runFontsResolveSet,
|
|
10436
10737
|
runFontsStatus: () => runFontsStatus
|
|
10437
10738
|
});
|
|
10438
|
-
import { existsSync as
|
|
10439
|
-
import
|
|
10739
|
+
import { existsSync as existsSync26, readFileSync as readFileSync22 } from "node:fs";
|
|
10740
|
+
import path33 from "node:path";
|
|
10440
10741
|
async function runFontsResolve(opts) {
|
|
10441
10742
|
const result = await resolveFonts(opts.family, opts.weights, opts.cacheDir);
|
|
10442
|
-
|
|
10743
|
+
const queryRefusal = systemFaceRefusal(opts.family.trim());
|
|
10744
|
+
emitData(opts, { ...result, ...queryRefusal !== void 0 ? { queryRefusal } : {} }, () => {
|
|
10443
10745
|
for (const f of result.resolved) process.stdout.write(`resolved ${f.family} ${f.weight} (${f.sha256.slice(0, 12)}\u2026)
|
|
10444
10746
|
`);
|
|
10445
10747
|
for (const f of result.failures) process.stdout.write(`FAILED ${f.family} ${f.weight}: ${f.reason}
|
|
10446
10748
|
`);
|
|
10749
|
+
if (queryRefusal !== void 0 && result.failures.length > 0) {
|
|
10750
|
+
process.stdout.write(` "${opts.family}" is a refusal-class family: ${queryRefusal}
|
|
10751
|
+
Options: proceed under a disclosed substitute (rules out certification), or register a file you license yourself: ${tendrilCommand(`fonts add ${quoteArg(opts.family)} <weight> <file>`)} \u2014 bundles declare the face and never carry its bytes.
|
|
10752
|
+
`);
|
|
10753
|
+
}
|
|
10447
10754
|
});
|
|
10448
10755
|
if (result.failures.length > 0) {
|
|
10449
10756
|
warn(opts, `${result.failures.length} face(s) unresolved \u2014 verification will refuse to score under substitution`);
|
|
@@ -10451,7 +10758,7 @@ async function runFontsResolve(opts) {
|
|
|
10451
10758
|
}
|
|
10452
10759
|
}
|
|
10453
10760
|
async function runFontsResolveSet(opts) {
|
|
10454
|
-
const setDir =
|
|
10761
|
+
const setDir = path33.resolve(process.env["INIT_CWD"] ?? process.cwd(), opts.set);
|
|
10455
10762
|
let needs = [];
|
|
10456
10763
|
try {
|
|
10457
10764
|
needs = recordedFontNeeds(setDir);
|
|
@@ -10501,7 +10808,8 @@ async function runFontsResolveSet(opts) {
|
|
|
10501
10808
|
unlicensed.push(...result.failures);
|
|
10502
10809
|
}
|
|
10503
10810
|
}
|
|
10504
|
-
|
|
10811
|
+
const refusals = [...new Set(failures.map((f) => f.family))].map((family) => ({ family, reason: systemFaceRefusal(family.trim()) })).filter((r) => r.reason !== void 0);
|
|
10812
|
+
emitData(opts, { set: setDir, needs, cached: cached2, resolved, relicensed, unlicensed, failures, ...refusals.length > 0 ? { refusals } : {} }, () => {
|
|
10505
10813
|
process.stdout.write(`set declares: ${needs.map((n) => `${n.family} (${n.weights.join(", ")})`).join(" \xB7 ")}
|
|
10506
10814
|
`);
|
|
10507
10815
|
for (const f of cached2) process.stdout.write(`cached ${f.family} ${f.weight}
|
|
@@ -10514,6 +10822,15 @@ async function runFontsResolveSet(opts) {
|
|
|
10514
10822
|
`);
|
|
10515
10823
|
for (const f of failures) process.stdout.write(`FAILED ${f.family} ${f.weight}: ${f.reason}
|
|
10516
10824
|
`);
|
|
10825
|
+
for (const fam of [...new Set(failures.map((f) => f.family))]) {
|
|
10826
|
+
const refusal = systemFaceRefusal(fam);
|
|
10827
|
+
process.stdout.write(
|
|
10828
|
+
refusal !== void 0 ? ` "${fam}" is a refusal-class family: ${refusal}
|
|
10829
|
+
Options: proceed and let the mount substitute a provided face (disclosed; rules out certification), or register a file you license yourself: ${tendrilCommand(`fonts add ${quoteArg(fam)} <weight> <file>`)} \u2014 licensing is yours to judge; bundles declare the face and never carry its bytes.
|
|
10830
|
+
` : ` if "${fam}" is installed on this machine: ${tendrilCommand(`fonts discover ${quoteArg(fam)}`)} then ${tendrilCommand(`fonts add-system ${quoteArg(fam)} --set ${quoteArg(setDir)}`)} (licence stays unknown; bundles declare, never copy)
|
|
10831
|
+
`
|
|
10832
|
+
);
|
|
10833
|
+
}
|
|
10517
10834
|
});
|
|
10518
10835
|
if (byteDrift.length > 0) {
|
|
10519
10836
|
warn(opts, `${byteDrift.map((f) => `${f.family} ${f.weight}`).join(", ")}: the foundry now serves different bytes than the cache held \u2014 the faces were re-fetched to record their licence, so re-pin any font lock deliberately`);
|
|
@@ -10524,16 +10841,16 @@ async function runFontsResolveSet(opts) {
|
|
|
10524
10841
|
}
|
|
10525
10842
|
}
|
|
10526
10843
|
function runFontsStatus(opts) {
|
|
10527
|
-
const manifestPath2 =
|
|
10528
|
-
if (!
|
|
10844
|
+
const manifestPath2 = path33.join(opts.cacheDir, "manifest.json");
|
|
10845
|
+
if (!existsSync26(manifestPath2)) {
|
|
10529
10846
|
fail(opts, ExitCode.FontsUnproven, {
|
|
10530
10847
|
error: `no font cache at ${opts.cacheDir}`,
|
|
10531
10848
|
code: "fonts-unresolved",
|
|
10532
10849
|
remediation: `Run \`${tendrilCommand("fonts resolve --set <recording-dir>")}\` (or \`${tendrilCommand('fonts resolve "<Family>" --weights 400 500 600')}\`) first.`
|
|
10533
10850
|
});
|
|
10534
10851
|
}
|
|
10535
|
-
const faces = JSON.parse(
|
|
10536
|
-
const lockVerdicts = opts.lock !== void 0 ? checkFontLock(
|
|
10852
|
+
const faces = JSON.parse(readFileSync22(manifestPath2, "utf8"));
|
|
10853
|
+
const lockVerdicts = opts.lock !== void 0 ? checkFontLock(path33.resolve(opts.lock), opts.cacheDir) : null;
|
|
10537
10854
|
emitData(opts, { faces, lockVerdicts }, () => {
|
|
10538
10855
|
for (const f of faces) process.stdout.write(`cached ${f.family} ${f.weight} (${f.sha256.slice(0, 12)}\u2026)
|
|
10539
10856
|
`);
|
|
@@ -10577,13 +10894,13 @@ function familyMismatch(family, declared) {
|
|
|
10577
10894
|
}
|
|
10578
10895
|
function runFontsAdd(opts) {
|
|
10579
10896
|
if (opts.set !== void 0) {
|
|
10580
|
-
const declared = taskFontFamilies(
|
|
10897
|
+
const declared = taskFontFamilies(path33.resolve(opts.set)) ?? [];
|
|
10581
10898
|
const mismatch = familyMismatch(opts.family, declared);
|
|
10582
10899
|
if (mismatch !== void 0) {
|
|
10583
10900
|
fail(opts, ExitCode.InputValidation, {
|
|
10584
10901
|
error: `the recording set does not declare a family named "${opts.family}" \u2014 it declares ${declared.map((d) => `"${d}"`).join(", ")}. Adding it under this name would cache a face the mount never matches, and scoring would keep refusing for the family that is still missing.`,
|
|
10585
10902
|
code: "font-family-not-declared",
|
|
10586
|
-
remediation: mismatch.nearest !== void 0 ? `Did you mean "${mismatch.nearest}"? ${tendrilCommand(`fonts add "${mismatch.nearest}" ${opts.weight} ${opts.file} --set ${
|
|
10903
|
+
remediation: mismatch.nearest !== void 0 ? `Did you mean "${mismatch.nearest}"? ${tendrilCommand(`fonts add "${mismatch.nearest}" ${opts.weight} ${opts.file} --set ${path33.resolve(opts.set)}`)}` : `Use one of the declared families exactly as the recording spells it, or drop --set to add a family this set does not declare.`
|
|
10587
10904
|
});
|
|
10588
10905
|
}
|
|
10589
10906
|
} else {
|
|
@@ -10610,6 +10927,81 @@ function runFontsAdd(opts) {
|
|
|
10610
10927
|
`);
|
|
10611
10928
|
});
|
|
10612
10929
|
}
|
|
10930
|
+
function runFontsDiscover(opts) {
|
|
10931
|
+
const faces = opts.family !== void 0 ? systemFacesForFamily(opts.family, opts.dirs) : discoverSystemFaces(opts.dirs);
|
|
10932
|
+
const rows = faces.map((f) => ({ ...f, ...systemFaceRefusal(f.family) !== void 0 ? { refused: systemFaceRefusal(f.family) } : {} }));
|
|
10933
|
+
const queryRefusal = opts.family !== void 0 ? systemFaceRefusal(opts.family) : void 0;
|
|
10934
|
+
emitData(opts, { faces: rows, dirs: opts.dirs ?? systemFontDirs(), ...queryRefusal !== void 0 ? { queryRefusal } : {} }, () => {
|
|
10935
|
+
if (rows.length === 0) {
|
|
10936
|
+
process.stdout.write(`no matching system faces found (scanned: ${(opts.dirs ?? systemFontDirs()).join(", ")})
|
|
10937
|
+
`);
|
|
10938
|
+
if (queryRefusal !== void 0) {
|
|
10939
|
+
process.stdout.write(
|
|
10940
|
+
`note: "${opts.family}" is a refusal-class family regardless \u2014 ${queryRefusal}
|
|
10941
|
+
A file you license yourself can still be registered for local scoring: ${tendrilCommand(`fonts add ${quoteArg(opts.family ?? "")} <weight> <file>`)} \u2014 licensing is yours to judge, and bundles declare the face but never carry its bytes.
|
|
10942
|
+
`
|
|
10943
|
+
);
|
|
10944
|
+
}
|
|
10945
|
+
return;
|
|
10946
|
+
}
|
|
10947
|
+
for (const f of rows) {
|
|
10948
|
+
const flags = [f.refused !== void 0 ? "REFUSED" : "", f.italic ? "italic" : "", f.variable ? "variable" : ""].filter((x) => x !== "").join(" ");
|
|
10949
|
+
process.stdout.write(`${f.family} \u2014 ${f.subfamily || "Regular"} (${f.weight})${flags === "" ? "" : ` [${flags}]`} ${f.file}${f.faceIndex !== void 0 ? ` #${f.faceIndex}` : ""}
|
|
10950
|
+
`);
|
|
10951
|
+
}
|
|
10952
|
+
const eligible = rows.filter((f) => f.refused === void 0 && !f.italic && !f.variable);
|
|
10953
|
+
if (eligible.length > 0 && opts.family !== void 0) {
|
|
10954
|
+
process.stdout.write(`
|
|
10955
|
+
cache them: ${tendrilCommand(`fonts add-system ${quoteArg(opts.family)}`)}
|
|
10956
|
+
`);
|
|
10957
|
+
}
|
|
10958
|
+
});
|
|
10959
|
+
}
|
|
10960
|
+
function runFontsAddSystem(opts) {
|
|
10961
|
+
if (opts.set !== void 0) {
|
|
10962
|
+
const declared = taskFontFamilies(path33.resolve(opts.set)) ?? [];
|
|
10963
|
+
const mismatch = familyMismatch(opts.family, declared);
|
|
10964
|
+
if (mismatch !== void 0) {
|
|
10965
|
+
fail(opts, ExitCode.InputValidation, {
|
|
10966
|
+
error: `the recording set does not declare a family named "${opts.family}" \u2014 it declares ${declared.map((d) => `"${d}"`).join(", ")}. A face cached under a name the mount never matches leaves scoring refusing for the family that is still missing.`,
|
|
10967
|
+
code: "font-family-not-declared",
|
|
10968
|
+
remediation: mismatch.nearest !== void 0 ? `Did you mean "${mismatch.nearest}"? ${tendrilCommand(`fonts add-system ${quoteArg(mismatch.nearest)} --set ${quoteArg(path33.resolve(opts.set))}`)}` : `Use one of the declared families exactly as the recording spells it, or drop --set.`
|
|
10969
|
+
});
|
|
10970
|
+
}
|
|
10971
|
+
} else {
|
|
10972
|
+
warn(
|
|
10973
|
+
opts,
|
|
10974
|
+
`family "${opts.family}" was NOT checked against a recording \u2014 pass --set <recording-dir> to have the spelling verified, since a face cached under a name the mount does not match leaves scoring refusing for a family that still looks provided`
|
|
10975
|
+
);
|
|
10976
|
+
}
|
|
10977
|
+
const result = addSystemFamily(opts.family, { cacheDir: opts.cacheDir, ...opts.weights !== void 0 ? { weights: opts.weights } : {} });
|
|
10978
|
+
if (result.refusal !== void 0) {
|
|
10979
|
+
fail(opts, ExitCode.InputValidation, {
|
|
10980
|
+
error: `"${opts.family}" is a refusal-class family: ${result.refusal}`,
|
|
10981
|
+
code: "font-family-refused",
|
|
10982
|
+
remediation: `Pick a face the design system can actually license for rendering, or resolve a served family: ${tendrilCommand('fonts resolve "<Family>" --weights 400 500 600')}.`
|
|
10983
|
+
});
|
|
10984
|
+
}
|
|
10985
|
+
if (result.added.length === 0) {
|
|
10986
|
+
fail(opts, ExitCode.InputValidation, {
|
|
10987
|
+
error: result.skipped.length > 0 ? `no cacheable face for "${opts.family}" \u2014 every match was skipped: ${result.skipped.map((s) => `${s.subfamily || "Regular"} (${s.reason})`).join("; ")}` : `no installed face matches "${opts.family}" \u2014 nothing in ${systemFontDirs().join(", ")} declares that family`,
|
|
10988
|
+
code: "font-family-not-installed",
|
|
10989
|
+
remediation: `See what IS installed: ${tendrilCommand(`fonts discover ${quoteArg(opts.family)}`)} (or all faces with ${tendrilCommand("fonts discover")}).`
|
|
10990
|
+
});
|
|
10991
|
+
}
|
|
10992
|
+
for (const o of result.overwrote) {
|
|
10993
|
+
warn(
|
|
10994
|
+
opts,
|
|
10995
|
+
`OVERWROTE ${o.family} ${o.weight}: the cache held a ${o.priorLicense} face from ${o.priorSource} (sha ${o.priorSha256.slice(0, 12)}\u2026) and it has been REPLACED by this machine's system bytes \u2014 the environment stamp changes, and any bundle emitted under the old face now fails checkFontLock against this cache. Restore the foundry face with ${tendrilCommand(`fonts resolve "${o.family}" --weights ${o.weight}`)} if that was not intended`
|
|
10996
|
+
);
|
|
10997
|
+
}
|
|
10998
|
+
emitData(opts, result, () => {
|
|
10999
|
+
for (const f of result.added) process.stdout.write(`added ${f.family} ${f.weight} \u2014 ${f.subfamily} \u2014 from ${f.source} (${f.sha256.slice(0, 12)}\u2026) \u2014 licence unknown: bundles DECLARE this face, its bytes never ship
|
|
11000
|
+
`);
|
|
11001
|
+
for (const s of result.skipped) process.stdout.write(`skipped ${s.subfamily || "Regular"} ${s.weight}: ${s.reason}
|
|
11002
|
+
`);
|
|
11003
|
+
});
|
|
11004
|
+
}
|
|
10613
11005
|
var init_fonts = __esm({
|
|
10614
11006
|
"packages/cli/src/commands/fonts.ts"() {
|
|
10615
11007
|
"use strict";
|
|
@@ -10640,15 +11032,24 @@ __export(verify_exports, {
|
|
|
10640
11032
|
resolveComposition: () => resolveComposition,
|
|
10641
11033
|
runVerify: () => runVerify
|
|
10642
11034
|
});
|
|
10643
|
-
import { existsSync as
|
|
10644
|
-
import
|
|
11035
|
+
import { existsSync as existsSync27, readFileSync as readFileSync23 } from "node:fs";
|
|
11036
|
+
import path34 from "node:path";
|
|
10645
11037
|
function interactionCoverage(behaviors) {
|
|
10646
|
-
const
|
|
11038
|
+
const parity = behaviors.filter((b) => b.id.startsWith("parity:"));
|
|
11039
|
+
const interaction = behaviors.filter((b) => !b.id.startsWith("prelude:") && !b.id.startsWith("parity:"));
|
|
10647
11040
|
return {
|
|
10648
11041
|
interactionChecks: interaction.length,
|
|
10649
11042
|
interactionPassed: interaction.filter((b) => b.pass).length,
|
|
10650
|
-
preludeChecks: behaviors.length - interaction.length,
|
|
10651
|
-
|
|
11043
|
+
preludeChecks: behaviors.length - interaction.length - parity.length,
|
|
11044
|
+
parityChecks: parity.length,
|
|
11045
|
+
parityPassed: parity.filter((b) => b.pass).length,
|
|
11046
|
+
// Asymmetric on purpose (review finding F3): a parity PASS never
|
|
11047
|
+
// makes operability "verified" (parity is pixel-parity, not
|
|
11048
|
+
// someone typing or clicking — the §0j miscount), but a parity
|
|
11049
|
+
// FAIL still forbids it — "verified" printed beside a broken hover
|
|
11050
|
+
// state is the one word doing too much lifting again, from the
|
|
11051
|
+
// other direction.
|
|
11052
|
+
operability: interaction.length > 0 && interaction.every((b) => b.pass) && parity.every((b) => b.pass) ? "verified" : "unverified"
|
|
10652
11053
|
};
|
|
10653
11054
|
}
|
|
10654
11055
|
function operabilityReport(input) {
|
|
@@ -10681,6 +11082,16 @@ function operabilityReport(input) {
|
|
|
10681
11082
|
unverified: `the interaction checks RAN and ${failed} of ${interactionChecks} failed (each named in the FAIL behavior rows) \u2014 this component WAS exercised and did not behave as the recording requires; unverified here means measured and wrong, not unmeasured.`
|
|
10682
11083
|
};
|
|
10683
11084
|
}
|
|
11085
|
+
const { parityChecks, parityPassed } = interactionCoverage(input.behaviors);
|
|
11086
|
+
if (parityPassed < parityChecks) {
|
|
11087
|
+
const failed = parityChecks - parityPassed;
|
|
11088
|
+
return {
|
|
11089
|
+
checks: interactionChecks,
|
|
11090
|
+
passed: interactionPassed,
|
|
11091
|
+
short: `${failed} of ${parityChecks} state-parity check(s) failed`,
|
|
11092
|
+
unverified: `the interaction checks passed but ${failed} of ${parityChecks} state-parity check(s) FAILED (the parity: rows) \u2014 a certified forced state is masking a broken real one, so operability cannot read as verified beside it.`
|
|
11093
|
+
};
|
|
11094
|
+
}
|
|
10684
11095
|
return { checks: interactionChecks, passed: interactionPassed };
|
|
10685
11096
|
}
|
|
10686
11097
|
function operabilityLine(state) {
|
|
@@ -10773,7 +11184,7 @@ function compositionReport(input) {
|
|
|
10773
11184
|
function eyeCheck(bundleDir) {
|
|
10774
11185
|
return {
|
|
10775
11186
|
command: tendrilCommand(`inspect "${bundleDir}"`),
|
|
10776
|
-
sheetPath:
|
|
11187
|
+
sheetPath: path34.join(bundleDir, "verify-evidence", "inspect.html"),
|
|
10777
11188
|
note: "magnified recorded-vs-rendered crops of every small node; scores cannot see shape. The command WRITES/refreshes the sheet at sheetPath \u2014 re-run it after every verify; an existing sheet may show stale crops."
|
|
10778
11189
|
};
|
|
10779
11190
|
}
|
|
@@ -10785,7 +11196,7 @@ function taskFromManifest(opts, manifest, setDir) {
|
|
|
10785
11196
|
const adapterSlugs = Object.keys(manifest.propAdapter);
|
|
10786
11197
|
const unmapped = recordedSlugs.filter((s) => !adapterSlugs.includes(s));
|
|
10787
11198
|
const adapterOnly = adapterSlugs.filter((s) => !recordedSlugs.includes(s));
|
|
10788
|
-
const registry = Object.values(TASKS).find((t) =>
|
|
11199
|
+
const registry = Object.values(TASKS).find((t) => path34.resolve(t.set) === path34.resolve(setDir));
|
|
10789
11200
|
const authored = (() => {
|
|
10790
11201
|
if (registry !== void 0) return void 0;
|
|
10791
11202
|
try {
|
|
@@ -10811,6 +11222,10 @@ function taskFromManifest(opts, manifest, setDir) {
|
|
|
10811
11222
|
task,
|
|
10812
11223
|
unmapped,
|
|
10813
11224
|
adapterOnly,
|
|
11225
|
+
// The parity AUTHORITY: CLI-owned configs for the same poses —
|
|
11226
|
+
// recording-derived (authored) or registry-declared — never the
|
|
11227
|
+
// adapter, which is the graded artifact framing itself (§0j).
|
|
11228
|
+
authorityConfigs: behaviorSource.configs,
|
|
10814
11229
|
// Registry sets carry hand-declared behaviors and no derivation
|
|
10815
11230
|
// runs, so their evidence is UNKNOWN, not empty: an empty list is
|
|
10816
11231
|
// spent downstream as "the recording holds no interactive pose".
|
|
@@ -10820,19 +11235,19 @@ function taskFromManifest(opts, manifest, setDir) {
|
|
|
10820
11235
|
}
|
|
10821
11236
|
async function runVerify(opts) {
|
|
10822
11237
|
const callerCwd = process.env["INIT_CWD"] ?? process.cwd();
|
|
10823
|
-
const setOverride = opts.set !== void 0 ?
|
|
10824
|
-
opts = { ...opts, bundleDir:
|
|
10825
|
-
if (!
|
|
11238
|
+
const setOverride = opts.set !== void 0 ? path34.resolve(callerCwd, opts.set) : void 0;
|
|
11239
|
+
opts = { ...opts, bundleDir: path34.resolve(callerCwd, opts.bundleDir), ...setOverride !== void 0 ? { set: setOverride } : {} };
|
|
11240
|
+
if (!existsSync27(opts.bundleDir)) {
|
|
10826
11241
|
fail(opts, ExitCode.InputValidation, {
|
|
10827
11242
|
error: `bundle directory not found: ${opts.bundleDir}`,
|
|
10828
11243
|
code: "bundle-missing",
|
|
10829
11244
|
remediation: "Point at a generated bundle directory (containing component.json, the entry module, and styles.css)."
|
|
10830
11245
|
});
|
|
10831
11246
|
}
|
|
10832
|
-
const manifestPath2 =
|
|
11247
|
+
const manifestPath2 = path34.join(opts.bundleDir, "component.json");
|
|
10833
11248
|
let manifest;
|
|
10834
|
-
if (
|
|
10835
|
-
const { manifest: parsed, issues } = readBundleManifest(
|
|
11249
|
+
if (existsSync27(manifestPath2)) {
|
|
11250
|
+
const { manifest: parsed, issues } = readBundleManifest(readFileSync23(manifestPath2, "utf8"));
|
|
10836
11251
|
if (issues.length > 0) {
|
|
10837
11252
|
fail(opts, ExitCode.InputValidation, {
|
|
10838
11253
|
error: `bundle manifest rejected at ingest: ${issues.map((i) => i.message).join("; ")}`,
|
|
@@ -10846,6 +11261,7 @@ async function runVerify(opts) {
|
|
|
10846
11261
|
let unmapped = [];
|
|
10847
11262
|
let interactionEvidence;
|
|
10848
11263
|
let unmappedInteractionEvidence = [];
|
|
11264
|
+
let authorityConfigs;
|
|
10849
11265
|
let availability = ROLES_NOT_RESOLVED;
|
|
10850
11266
|
if (opts.task !== void 0) {
|
|
10851
11267
|
const registry = TASKS[opts.task];
|
|
@@ -10859,21 +11275,21 @@ async function runVerify(opts) {
|
|
|
10859
11275
|
task = registry;
|
|
10860
11276
|
} else if (manifest !== void 0) {
|
|
10861
11277
|
const resolveSetDir = (p) => {
|
|
10862
|
-
if (
|
|
10863
|
-
const fromRepo =
|
|
10864
|
-
if (
|
|
10865
|
-
return
|
|
11278
|
+
if (path34.isAbsolute(p)) return p;
|
|
11279
|
+
const fromRepo = path34.resolve(REPO_ROOT, p);
|
|
11280
|
+
if (existsSync27(fromRepo)) return fromRepo;
|
|
11281
|
+
return path34.resolve(callerCwd, p);
|
|
10866
11282
|
};
|
|
10867
11283
|
const setDir = opts.set ?? resolveSetDir(manifest.provenance.recordingSet.path);
|
|
10868
|
-
if (!
|
|
11284
|
+
if (!existsSync27(path34.join(setDir, "recording-set.json")) && !Object.values(TASKS).some((t) => path34.resolve(t.set) === path34.resolve(setDir))) {
|
|
10869
11285
|
fail(opts, ExitCode.RecordingIncomplete, {
|
|
10870
11286
|
error: `recording set not found or unmanifested: ${setDir}`,
|
|
10871
11287
|
code: "recording-set-missing",
|
|
10872
11288
|
remediation: "Pass --set <dir> pointing at the recording set this bundle was generated from."
|
|
10873
11289
|
});
|
|
10874
11290
|
}
|
|
10875
|
-
const registry = Object.values(TASKS).find((t) =>
|
|
10876
|
-
if (registry !== void 0 && !
|
|
11291
|
+
const registry = Object.values(TASKS).find((t) => path34.resolve(t.set) === path34.resolve(setDir));
|
|
11292
|
+
if (registry !== void 0 && !existsSync27(path34.join(setDir, "recording-set.json"))) {
|
|
10877
11293
|
const recordedSlugs = registry.configs.map((c) => c.rep);
|
|
10878
11294
|
const adapterSlugs = Object.keys(manifest.propAdapter);
|
|
10879
11295
|
unmapped = recordedSlugs.filter((s) => !adapterSlugs.includes(s));
|
|
@@ -10882,10 +11298,12 @@ async function runVerify(opts) {
|
|
|
10882
11298
|
entry: manifest.entry,
|
|
10883
11299
|
configs: adapterSlugs.filter((s) => recordedSlugs.includes(s)).map((rep) => ({ rep, component: manifest.propAdapter[rep].component, props: manifest.propAdapter[rep].props }))
|
|
10884
11300
|
};
|
|
11301
|
+
authorityConfigs = registry.configs;
|
|
10885
11302
|
} else {
|
|
10886
11303
|
const built = taskFromManifest(opts, manifest, setDir);
|
|
10887
11304
|
task = built.task;
|
|
10888
11305
|
unmapped = built.unmapped;
|
|
11306
|
+
authorityConfigs = built.authorityConfigs;
|
|
10889
11307
|
interactionEvidence = built.interactionEvidence;
|
|
10890
11308
|
unmappedInteractionEvidence = built.unmappedInteractionEvidence;
|
|
10891
11309
|
for (const s of built.adapterOnly) warn(opts, `prop adapter maps "${s}" which is not in the recording set \u2014 ignored`);
|
|
@@ -10897,9 +11315,9 @@ async function runVerify(opts) {
|
|
|
10897
11315
|
warn(opts, `recording set content differs from the bundle's provenance stamp (${hash.slice(0, 12)}\u2026 vs ${manifest.provenance.recordingSet.hash.slice(0, 12)}\u2026) \u2014 scores apply to the CURRENT set`);
|
|
10898
11316
|
}
|
|
10899
11317
|
for (const name of [manifest.entry, "styles.css", "tokens.css"]) {
|
|
10900
|
-
const p =
|
|
10901
|
-
if (!
|
|
10902
|
-
const issues = checkBundleSourceFile(name, new Uint8Array(
|
|
11318
|
+
const p = path34.join(opts.bundleDir, name);
|
|
11319
|
+
if (!existsSync27(p)) continue;
|
|
11320
|
+
const issues = checkBundleSourceFile(name, new Uint8Array(readFileSync23(p)));
|
|
10903
11321
|
if (issues.length > 0) {
|
|
10904
11322
|
fail(opts, ExitCode.InputValidation, {
|
|
10905
11323
|
error: `bundle source rejected at ingest: ${issues.map((i) => i.message).join("; ")}`,
|
|
@@ -10937,7 +11355,7 @@ async function runVerify(opts) {
|
|
|
10937
11355
|
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)`);
|
|
10938
11356
|
}
|
|
10939
11357
|
const missing = task.configs.filter(
|
|
10940
|
-
(c) => !
|
|
11358
|
+
(c) => !existsSync27(path34.join(task.set, c.rep, "get_screenshot.json")) || !existsSync27(path34.join(task.set, c.rep, "get_metadata.json"))
|
|
10941
11359
|
);
|
|
10942
11360
|
if (missing.length > 0) {
|
|
10943
11361
|
fail(opts, ExitCode.RecordingIncomplete, {
|
|
@@ -10947,12 +11365,12 @@ async function runVerify(opts) {
|
|
|
10947
11365
|
});
|
|
10948
11366
|
}
|
|
10949
11367
|
const bar = BARS2[opts.bar];
|
|
10950
|
-
const evidenceDir =
|
|
11368
|
+
const evidenceDir = path34.join(opts.bundleDir, "verify-evidence");
|
|
10951
11369
|
const scores = await scoreBundleForTask(task, opts.bundleDir, bar, { evidenceDir, onProgress: emitProgress });
|
|
10952
11370
|
const quality = await checkBundleQuality(opts.bundleDir, task.entry, { dir: task.set, reps: task.configs.map((c) => c.rep) });
|
|
10953
|
-
const bundleCss = ["tokens.css", "styles.css"].map((f) =>
|
|
11371
|
+
const bundleCss = ["tokens.css", "styles.css"].map((f) => path34.join(opts.bundleDir, f)).filter((f) => existsSync27(f)).map((f) => readFileSync23(f, "utf8")).join("\n");
|
|
10954
11372
|
const occlusion = await checkSiblingOcclusion(task, opts.bundleDir, bundleCss);
|
|
10955
|
-
const parity = await checkHoverParity(task, opts.bundleDir);
|
|
11373
|
+
const parity = await checkHoverParity(task, opts.bundleDir, authorityConfigs ?? task.configs);
|
|
10956
11374
|
const behaviors = [...await checkBehaviors(task, opts.bundleDir), ...parity];
|
|
10957
11375
|
const structural = roles !== void 0 ? await checkStructuralComposition(task, opts.bundleDir, roles) : [];
|
|
10958
11376
|
const regionsOut = roles !== void 0 ? interiorRegions(task.set, roles) : void 0;
|
|
@@ -11135,6 +11553,9 @@ ${certified}/${statuses.length} certified \xB7 ${report.coverage.pass}/${statuse
|
|
|
11135
11553
|
`INCOMPLETE ${cov.unrecordedConfigs} of ${cov.latticeConfigs} lattice poses were NEVER RECORDED \u2014 nothing verifies them; any implementation of those poses is inference, not verified truth. Re-plan the set (full matrix is the default) and record the missing poses.
|
|
11136
11554
|
`
|
|
11137
11555
|
);
|
|
11556
|
+
} else if (typeof cov.latticeConfigs === "number") {
|
|
11557
|
+
process.stdout.write(`LATTICE all ${cov.latticeConfigs} poses of the component set's own lattice are recorded and scored \u2014 the denominator is the set's, not the recorded subset's
|
|
11558
|
+
`);
|
|
11138
11559
|
} else if (cov.latticeConfigs === null) {
|
|
11139
11560
|
process.stdout.write(`COVERAGE denominator unknown (set predates lattice tracking) \u2014 scored configs are verified; completeness is not established
|
|
11140
11561
|
`);
|
|
@@ -11142,7 +11563,7 @@ ${certified}/${statuses.length} certified \xB7 ${report.coverage.pass}/${statuse
|
|
|
11142
11563
|
}
|
|
11143
11564
|
process.stdout.write(`environment: chrome=${report.environment.chromeVersion ?? report.environment.chrome} fonts=${report.environment.fontsManifestSha256 ?? "UNRESOLVED"}
|
|
11144
11565
|
`);
|
|
11145
|
-
const shippedFonts = requiredFontsManifest(cssFontFamilies(["styles.css", "tokens.css"].map((f) =>
|
|
11566
|
+
const shippedFonts = requiredFontsManifest(cssFontFamilies(["styles.css", "tokens.css"].map((f) => path34.join(opts.bundleDir, f)).filter((f) => existsSync27(f)).map((f) => readFileSync23(f, "utf8")).join("\n")));
|
|
11146
11567
|
if (shippedFonts.length > 0 && substitutedFamilies.length === 0) {
|
|
11147
11568
|
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)
|
|
11148
11569
|
`);
|
|
@@ -11232,18 +11653,18 @@ __export(engine_exports, {
|
|
|
11232
11653
|
runEngineBrief: () => runEngineBrief,
|
|
11233
11654
|
runEngineScore: () => runEngineScore
|
|
11234
11655
|
});
|
|
11235
|
-
import { appendFileSync, existsSync as
|
|
11236
|
-
import
|
|
11656
|
+
import { appendFileSync, existsSync as existsSync28, mkdirSync as mkdirSync8, readFileSync as readFileSync24, writeFileSync as writeFileSync12 } from "node:fs";
|
|
11657
|
+
import path35 from "node:path";
|
|
11237
11658
|
function resolveEngineTask(opts, callerCwd) {
|
|
11238
|
-
const asPath =
|
|
11239
|
-
const isSet =
|
|
11659
|
+
const asPath = path35.resolve(callerCwd, opts.taskOrSet);
|
|
11660
|
+
const isSet = existsSync28(path35.join(asPath, "recording-set.json"));
|
|
11240
11661
|
const registry = TASKS[opts.taskOrSet];
|
|
11241
11662
|
if (registry !== void 0 && !isSet) return { task: registry, name: opts.taskOrSet, ref: opts.taskOrSet, disclosures: [], interactionEvidence: void 0 };
|
|
11242
11663
|
if (isSet) {
|
|
11243
11664
|
try {
|
|
11244
11665
|
const authored = authorTaskFromSet(asPath);
|
|
11245
11666
|
for (const d of authored.disclosures) warn(opts, d);
|
|
11246
|
-
return { task: authored.task, name:
|
|
11667
|
+
return { task: authored.task, name: path35.basename(asPath), ref: asPath, disclosures: authored.disclosures, interactionEvidence: authored.api.interactionEvidence, apiPin: { props: authored.api.apiPin.props, forcedStates: authored.api.apiPin.forcedStates } };
|
|
11247
11668
|
} catch (err) {
|
|
11248
11669
|
fail(opts, ExitCode.InputValidation, {
|
|
11249
11670
|
error: `cannot author a task from ${asPath}: ${err instanceof Error ? err.message.split("\n")[0] : String(err)}`,
|
|
@@ -11270,9 +11691,9 @@ ${disclosures.map((d) => `- ${d}`).join("\n")}` : "";
|
|
|
11270
11691
|
const brief = buildBrief(task.systemApi, bar, { colorScheme: recordingIsDark(task) ? "dark" : "light" }) + disclosureBlock;
|
|
11271
11692
|
const segments = buildSegments(task, "files");
|
|
11272
11693
|
let notRecorded;
|
|
11273
|
-
const manifestPath2 =
|
|
11274
|
-
if (
|
|
11275
|
-
notRecorded = JSON.parse(
|
|
11694
|
+
const manifestPath2 = path35.join(task.set, "recording-set.json");
|
|
11695
|
+
if (existsSync28(manifestPath2)) {
|
|
11696
|
+
notRecorded = JSON.parse(readFileSync24(manifestPath2, "utf8")).notRecorded;
|
|
11276
11697
|
}
|
|
11277
11698
|
const unverified = notRecorded !== void 0 && notRecorded !== "" ? `
|
|
11278
11699
|
|
|
@@ -11280,7 +11701,7 @@ ${disclosures.map((d) => `- ${d}`).join("\n")}` : "";
|
|
|
11280
11701
|
DO NOT INVENT PIXELS FOR THESE POSES. Implement them ONLY as the composition of recorded per-axis truth (the custom-property composition rule: one paint axis sets variables, the other consumes) and list every such pose in your report as UNRECORDED-COMPOSED. Recording them is the real fix: re-plan the set (full matrix is the default) and record the missing poses.
|
|
11281
11702
|
${notRecorded}` : "";
|
|
11282
11703
|
let fontProvisioning;
|
|
11283
|
-
if (
|
|
11704
|
+
if (existsSync28(manifestPath2)) {
|
|
11284
11705
|
const missingFams = unprovisionedFamilies(task.set);
|
|
11285
11706
|
const unprovided = unprovisionedFaces(task.set);
|
|
11286
11707
|
const weightOnly = missingFams.length === 0;
|
|
@@ -11306,9 +11727,9 @@ ${notRecorded}` : "";
|
|
|
11306
11727
|
|
|
11307
11728
|
=== TASK PAYLOAD (recorded truth, verbatim) ===
|
|
11308
11729
|
${segments}`;
|
|
11309
|
-
const payloadFile =
|
|
11310
|
-
const candidateDirSuggestion =
|
|
11311
|
-
mkdirSync8(
|
|
11730
|
+
const payloadFile = path35.resolve(callerCwd, opts.out ?? `tendril-out/${name}-brief.md`);
|
|
11731
|
+
const candidateDirSuggestion = path35.resolve(callerCwd, `tendril-out/${name}-candidate`);
|
|
11732
|
+
mkdirSync8(path35.dirname(payloadFile), { recursive: true });
|
|
11312
11733
|
writeFileSync12(payloadFile, payload);
|
|
11313
11734
|
emitData(
|
|
11314
11735
|
opts,
|
|
@@ -11359,15 +11780,18 @@ ${segments}`;
|
|
|
11359
11780
|
);
|
|
11360
11781
|
}
|
|
11361
11782
|
function appendScoreHistory(candidateDir, entry) {
|
|
11362
|
-
|
|
11783
|
+
const file = path35.join(candidateDir, "score-history.jsonl");
|
|
11784
|
+
const starts = existsSync28(file) ? readFileSync24(file, "utf8").split("\n").filter((l) => l.includes('"event":"round-start"')).length : 0;
|
|
11785
|
+
const round = entry.event === "round-start" ? starts + 1 : Math.max(1, starts);
|
|
11786
|
+
appendFileSync(file, `${JSON.stringify({ at: (/* @__PURE__ */ new Date()).toISOString(), round, ...entry })}
|
|
11363
11787
|
`);
|
|
11364
11788
|
}
|
|
11365
11789
|
async function runEngineScore(opts) {
|
|
11366
11790
|
requireEntitlement(opts);
|
|
11367
11791
|
const callerCwd = process.env["INIT_CWD"] ?? process.cwd();
|
|
11368
|
-
const candidateDir =
|
|
11792
|
+
const candidateDir = path35.resolve(callerCwd, opts.candidateDir);
|
|
11369
11793
|
const { task, name, apiPin, interactionEvidence } = resolveEngineTask(opts, callerCwd);
|
|
11370
|
-
if (!
|
|
11794
|
+
if (!existsSync28(candidateDir)) {
|
|
11371
11795
|
fail(opts, ExitCode.InputValidation, {
|
|
11372
11796
|
error: `candidate directory not found: ${candidateDir}`,
|
|
11373
11797
|
code: "candidate-missing",
|
|
@@ -11392,10 +11816,10 @@ async function runEngineScore(opts) {
|
|
|
11392
11816
|
for (const g of missingWeights(task.set)) {
|
|
11393
11817
|
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)`);
|
|
11394
11818
|
}
|
|
11395
|
-
if (opts.rebind !== true &&
|
|
11819
|
+
if (opts.rebind !== true && existsSync28(path35.join(candidateDir, "component.json"))) {
|
|
11396
11820
|
const prior = (() => {
|
|
11397
11821
|
try {
|
|
11398
|
-
const read = readBundleManifest(
|
|
11822
|
+
const read = readBundleManifest(readFileSync24(path35.join(candidateDir, "component.json"), "utf8"));
|
|
11399
11823
|
return read.manifest === void 0 ? { unreadable: true } : { hash: read.manifest.provenance.recordingSet.hash, path: read.manifest.provenance.recordingSet.path };
|
|
11400
11824
|
} catch {
|
|
11401
11825
|
return { unreadable: true };
|
|
@@ -11417,9 +11841,9 @@ async function runEngineScore(opts) {
|
|
|
11417
11841
|
}
|
|
11418
11842
|
}
|
|
11419
11843
|
const bar = BARS3[opts.bar];
|
|
11420
|
-
const evidenceDir =
|
|
11844
|
+
const evidenceDir = path35.join(candidateDir, "verify-evidence");
|
|
11421
11845
|
const scores = await scoreBundleForTask(task, candidateDir, bar, { evidenceDir, onProgress: emitProgress });
|
|
11422
|
-
const parity = await checkHoverParity(task, candidateDir);
|
|
11846
|
+
const parity = await checkHoverParity(task, candidateDir, task.configs);
|
|
11423
11847
|
const behaviors = [...await checkBehaviors(task, candidateDir), ...parity];
|
|
11424
11848
|
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)";
|
|
11425
11849
|
const obj = objective(scores, behaviors);
|
|
@@ -11447,7 +11871,10 @@ ${absentFindings.join("\n")}` : "";
|
|
|
11447
11871
|
|
|
11448
11872
|
CERTIFICATION: ${certifiedReps.length}/${scores.length} configs at the certification bar (sim \u2265${certBar.sim} AND ink \u2265${certBar.ink}, exact values, after parity demotion \u2014 composition checks at verify can demote further).${certifiedReps.length < scores.length ? ` Below cert: ${scores.filter((sc) => !certifiedSet.has(sc.rep)).map((sc) => sc.rep).join(", ")}.` : ""}
|
|
11449
11873
|
METRIC DEADBAND (read before iterating on near-misses): the scored similarity/ink deliberately tolerate \xB11px edge shift and antialiased-edge differences \u2014 cross-rasterizer noise absorption. A change entirely inside that band moves these numbers by EXACTLY ZERO (working as designed, not a stuck scorer). The per-config \`exact\` fields in the JSON are tolerance-free and move first: compare exact across rounds to confirm a small fix landed, and stop iterating when only exact moves \u2014 the bar reads the tolerant numbers.`;
|
|
11450
|
-
const
|
|
11874
|
+
const stampNotice = `
|
|
11875
|
+
|
|
11876
|
+
PROVENANCE STAMP: this scoring call itself (the Tendril CLI) just wrote/refreshed a comment on line 1 of styles.css carrying these scores, marked non-authoritative. If your editor or host reports styles.css was modified externally, that modification is this scorer \u2014 expected, not tampering. Keep the comment; it self-invalidates on any edit and \`tendril verify\` recomputes it.`;
|
|
11877
|
+
const feedback = buildFeedback(scores, behaviors, bar, "files") + certificationFeedback + stampNotice + qualityFeedback;
|
|
11451
11878
|
const emitted = emitBundleV1({
|
|
11452
11879
|
bundleDir: candidateDir,
|
|
11453
11880
|
task,
|
|
@@ -11460,7 +11887,17 @@ METRIC DEADBAND (read before iterating on near-misses): the scored similarity/in
|
|
|
11460
11887
|
environment: environmentStamp(taskFontFamilies(task.set)),
|
|
11461
11888
|
substitutedFamilies
|
|
11462
11889
|
});
|
|
11463
|
-
appendScoreHistory(candidateDir, {
|
|
11890
|
+
appendScoreHistory(candidateDir, {
|
|
11891
|
+
event: "round-scored",
|
|
11892
|
+
bar: opts.bar,
|
|
11893
|
+
pass: obj[0],
|
|
11894
|
+
total,
|
|
11895
|
+
pixelConfigsPassing: scores.filter((s) => s.pass).length,
|
|
11896
|
+
pixelConfigs: scores.length,
|
|
11897
|
+
certified: certifiedReps.length,
|
|
11898
|
+
floor: obj[1],
|
|
11899
|
+
mean: obj[2]
|
|
11900
|
+
});
|
|
11464
11901
|
const coverage = interactionCoverage(behaviors);
|
|
11465
11902
|
const operability = operabilityReport({ behaviors, interactionEvidence });
|
|
11466
11903
|
const evidenceUnverified = (interactionEvidence?.length ?? 0) > 0 && coverage.interactionChecks === 0;
|
|
@@ -11502,7 +11939,7 @@ METRIC DEADBAND (read before iterating on near-misses): the scored similarity/in
|
|
|
11502
11939
|
parityCoverage,
|
|
11503
11940
|
evidenceDir,
|
|
11504
11941
|
bundleManifest: emitted.written[0],
|
|
11505
|
-
note: "
|
|
11942
|
+
note: "the Tendril CLI (this scoring call) wrote the bundle provenance comment onto styles.css line 1 \u2014 an EXPECTED external modification, not tampering; preserve it in any post-score edit (it self-invalidates on edit and verify recomputes it)",
|
|
11506
11943
|
allPass,
|
|
11507
11944
|
environment: { ...environmentStamp(taskFontFamilies(task.set)), ruler: cliVersion() },
|
|
11508
11945
|
...lcdTextEnabled() ? { environmentOverrides: ["enable-lcd-text"] } : {},
|
|
@@ -11575,11 +12012,11 @@ var codeconnect_exports = {};
|
|
|
11575
12012
|
__export(codeconnect_exports, {
|
|
11576
12013
|
runCodeConnect: () => runCodeConnect
|
|
11577
12014
|
});
|
|
11578
|
-
import { existsSync as
|
|
11579
|
-
import
|
|
12015
|
+
import { existsSync as existsSync29, readFileSync as readFileSync25, writeFileSync as writeFileSync13 } from "node:fs";
|
|
12016
|
+
import path36 from "node:path";
|
|
11580
12017
|
function runCodeConnect(opts) {
|
|
11581
12018
|
const callerCwd = process.env["INIT_CWD"] ?? process.cwd();
|
|
11582
|
-
const bundleDir =
|
|
12019
|
+
const bundleDir = path36.resolve(callerCwd, opts.bundleDir);
|
|
11583
12020
|
let url;
|
|
11584
12021
|
try {
|
|
11585
12022
|
url = new URL(opts.figmaUrl);
|
|
@@ -11595,7 +12032,7 @@ function runCodeConnect(opts) {
|
|
|
11595
12032
|
}
|
|
11596
12033
|
let manifest;
|
|
11597
12034
|
try {
|
|
11598
|
-
const read = readBundleManifest(
|
|
12035
|
+
const read = readBundleManifest(readFileSync25(path36.join(bundleDir, "component.json"), "utf8"));
|
|
11599
12036
|
if (read.manifest === void 0) throw new Error(read.issues.map((i) => i.message).join("; ") || "no component.json");
|
|
11600
12037
|
manifest = read.manifest;
|
|
11601
12038
|
} catch (err) {
|
|
@@ -11605,8 +12042,8 @@ function runCodeConnect(opts) {
|
|
|
11605
12042
|
remediation: "Point at a bundle produced by the Tendril pipeline (it carries component.json), and verify it first."
|
|
11606
12043
|
});
|
|
11607
12044
|
}
|
|
11608
|
-
const setDir =
|
|
11609
|
-
if (!
|
|
12045
|
+
const setDir = path36.resolve(callerCwd, opts.set ?? manifest.provenance.recordingSet.path);
|
|
12046
|
+
if (!existsSync29(path36.join(setDir, "recording-set.json"))) {
|
|
11610
12047
|
fail(opts, ExitCode.InputValidation, {
|
|
11611
12048
|
error: `recording set not found at ${setDir}`,
|
|
11612
12049
|
code: "codeconnect-no-set",
|
|
@@ -11627,10 +12064,10 @@ function runCodeConnect(opts) {
|
|
|
11627
12064
|
const component = api.component;
|
|
11628
12065
|
const recManifest = loadManifest(setDir);
|
|
11629
12066
|
const poseNames = recManifest.latticeNames ?? recManifest.reps.map((r) => {
|
|
11630
|
-
const meta =
|
|
11631
|
-
if (!
|
|
12067
|
+
const meta = path36.join(setDir, r.slug, "get_metadata.json");
|
|
12068
|
+
if (!existsSync29(meta)) return void 0;
|
|
11632
12069
|
try {
|
|
11633
|
-
return /name="([^"]*)"/.exec(
|
|
12070
|
+
return /name="([^"]*)"/.exec(envelopeTextContent(JSON.parse(readFileSync25(meta, "utf8"))))?.[1];
|
|
11634
12071
|
} catch {
|
|
11635
12072
|
return void 0;
|
|
11636
12073
|
}
|
|
@@ -11688,7 +12125,7 @@ function runCodeConnect(opts) {
|
|
|
11688
12125
|
axisLines.push(`const ${varName} = instance.getEnum(${q(axis)}, { ${entries.join(", ")} })`);
|
|
11689
12126
|
fragmentVars.push(varName);
|
|
11690
12127
|
}
|
|
11691
|
-
const entryRel =
|
|
12128
|
+
const entryRel = path36.relative(callerCwd, path36.join(bundleDir, manifest.entry));
|
|
11692
12129
|
const trust = manifest.trustStatement.split("\n")[0] ?? "";
|
|
11693
12130
|
const lines = [
|
|
11694
12131
|
`// url=${opts.figmaUrl}`,
|
|
@@ -11709,7 +12146,7 @@ function runCodeConnect(opts) {
|
|
|
11709
12146
|
`}`,
|
|
11710
12147
|
``
|
|
11711
12148
|
].join("\n");
|
|
11712
|
-
const outFile =
|
|
12149
|
+
const outFile = path36.resolve(callerCwd, opts.out ?? path36.join(bundleDir, `${component}.figma.ts`));
|
|
11713
12150
|
writeFileSync13(outFile, lines);
|
|
11714
12151
|
emitData(
|
|
11715
12152
|
opts,
|
|
@@ -11749,17 +12186,17 @@ var init_codeconnect = __esm({
|
|
|
11749
12186
|
|
|
11750
12187
|
// packages/mcp/src/server.ts
|
|
11751
12188
|
import { createHash as createHash5 } from "node:crypto";
|
|
11752
|
-
import { existsSync as
|
|
11753
|
-
import
|
|
11754
|
-
import
|
|
12189
|
+
import { existsSync as existsSync30, mkdtempSync as mkdtempSync3, readFileSync as readFileSync26, readdirSync as readdirSync9, writeFileSync as writeFileSync14 } from "node:fs";
|
|
12190
|
+
import os7 from "node:os";
|
|
12191
|
+
import path37 from "node:path";
|
|
11755
12192
|
import { fileURLToPath as fileURLToPath6 } from "node:url";
|
|
11756
12193
|
import { z as z12 } from "zod";
|
|
11757
12194
|
function sourceHash() {
|
|
11758
|
-
const dir =
|
|
12195
|
+
const dir = path37.dirname(fileURLToPath6(import.meta.url));
|
|
11759
12196
|
const h = createHash5("sha256");
|
|
11760
|
-
for (const f of
|
|
12197
|
+
for (const f of readdirSync9(dir).filter((n) => n.endsWith(".ts")).sort()) {
|
|
11761
12198
|
h.update(f);
|
|
11762
|
-
h.update(
|
|
12199
|
+
h.update(readFileSync26(path37.join(dir, f)));
|
|
11763
12200
|
}
|
|
11764
12201
|
return h.digest("hex").slice(0, 16);
|
|
11765
12202
|
}
|
|
@@ -11767,10 +12204,10 @@ var REPO_ROOT3, CLI_BIN, BUNDLED_CLI, CLI_SPAWN, str, optStr, TOOLS, BOOT_HASH;
|
|
|
11767
12204
|
var init_server = __esm({
|
|
11768
12205
|
"packages/mcp/src/server.ts"() {
|
|
11769
12206
|
"use strict";
|
|
11770
|
-
REPO_ROOT3 =
|
|
11771
|
-
CLI_BIN =
|
|
11772
|
-
BUNDLED_CLI =
|
|
11773
|
-
CLI_SPAWN =
|
|
12207
|
+
REPO_ROOT3 = path37.resolve(path37.dirname(fileURLToPath6(import.meta.url)), "..", "..", "..");
|
|
12208
|
+
CLI_BIN = path37.join(REPO_ROOT3, "packages", "cli", "src", "bin.ts");
|
|
12209
|
+
BUNDLED_CLI = path37.join(path37.dirname(fileURLToPath6(import.meta.url)), "tendril.js");
|
|
12210
|
+
CLI_SPAWN = existsSync30(BUNDLED_CLI) ? { cmd: process.execPath, prefix: [BUNDLED_CLI] } : { cmd: "npx", prefix: ["tsx", CLI_BIN] };
|
|
11774
12211
|
str = (d) => z12.string().describe(d);
|
|
11775
12212
|
optStr = (d) => z12.string().optional().describe(d);
|
|
11776
12213
|
TOOLS = [
|
|
@@ -11800,7 +12237,7 @@ var init_server = __esm({
|
|
|
11800
12237
|
const single = i["metadata"];
|
|
11801
12238
|
const parts = i["metadataParts"];
|
|
11802
12239
|
if (single !== void 0 || parts !== void 0) {
|
|
11803
|
-
const tmp =
|
|
12240
|
+
const tmp = path37.join(mkdtempSync3(path37.join(os7.tmpdir(), "tendril-envelope-")), "response.txt");
|
|
11804
12241
|
if (single !== void 0) {
|
|
11805
12242
|
writeFileSync14(tmp, single);
|
|
11806
12243
|
argvOut.push("--metadata-raw-file", tmp);
|
|
@@ -11817,11 +12254,19 @@ var init_server = __esm({
|
|
|
11817
12254
|
},
|
|
11818
12255
|
{
|
|
11819
12256
|
name: "tendril_permissions",
|
|
11820
|
-
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
|
|
12257
|
+
description: "Install the Claude Code permission allowlist for the Tendril pipeline (write: true \u2014 merges the MCP per-tool entries, the run's shell surface, and project-scoped Write/Edit with deny guards for ./.claude and ./.git into the project's .claude/settings.local.json, idempotent, never touches other keys), or list the entries without writing. The shell/Write grants are CONVENIENCE, not a security boundary \u2014 the output's note names exactly what they trade; RELAY it with the offer. OFFER THIS AT PIPELINE START whenever NO merged Claude settings file (project .claude/settings.local.json or .claude/settings.json, or user ~/.claude/settings.json) contains tendril MCP entries \u2014 plugin installs use mcp__plugin_tendril_tendril__*, direct claude-mcp-add installs use mcp__<server>__* \u2014 when this session's tool names differ from the plugin defaults, pass figmaPrefix/tendrilPrefix with the prefixes you actually see, or the written entries never match. A FILE check, never prompt-watching \u2014 agents cannot observe permission prompts. ONE approval here replaces a prompt per pipeline call. Never run it unoffered; the user must reload the session for new settings to apply \u2014 say so.",
|
|
11821
12258
|
schema: z12.object({
|
|
11822
|
-
write: z12.boolean().optional().describe("true = install into .claude/settings.local.json (the point of this tool); false/absent = just return the entries")
|
|
12259
|
+
write: z12.boolean().optional().describe("true = install into .claude/settings.local.json (the point of this tool); false/absent = just return the entries"),
|
|
12260
|
+
figmaPrefix: z12.string().optional().describe("The Figma server's entry prefix AS THIS SESSION NAMES ITS TOOLS \u2014 the part before __get_metadata (e.g. mcp__figma-remote, mcp__plugin_figma_figma). Pass it whenever the session's Figma tools are not mcp__plugin_figma_figma__* \u2014 entries written under the wrong prefix never match anything (measured: six dead entries in a figma-remote session)."),
|
|
12261
|
+
tendrilPrefix: z12.string().optional().describe("Same for the tendril server when this session's tendril tools are not mcp__plugin_tendril_tendril__* (e.g. mcp__tendril for claude mcp add installs).")
|
|
11823
12262
|
}),
|
|
11824
|
-
argv: (i) => [
|
|
12263
|
+
argv: (i) => [
|
|
12264
|
+
"permissions",
|
|
12265
|
+
"--claude",
|
|
12266
|
+
...i["write"] === true ? ["--write"] : [],
|
|
12267
|
+
...typeof i["figmaPrefix"] === "string" ? ["--figma-prefix", i["figmaPrefix"]] : [],
|
|
12268
|
+
...typeof i["tendrilPrefix"] === "string" ? ["--tendril-prefix", i["tendrilPrefix"]] : []
|
|
12269
|
+
]
|
|
11825
12270
|
},
|
|
11826
12271
|
{
|
|
11827
12272
|
name: "tendril_doctor",
|
|
@@ -11871,7 +12316,7 @@ var init_server = __esm({
|
|
|
11871
12316
|
const bridge = (label, single, parts) => {
|
|
11872
12317
|
if (single !== void 0 && parts !== void 0) throw new Error(`pass at most one of \`${label}\` and \`${label}Parts\``);
|
|
11873
12318
|
if (single === void 0 && parts === void 0) return;
|
|
11874
|
-
const tmp =
|
|
12319
|
+
const tmp = path37.join(mkdtempSync3(path37.join(os7.tmpdir(), "tendril-envelope-")), "response.txt");
|
|
11875
12320
|
if (single !== void 0) {
|
|
11876
12321
|
writeFileSync14(tmp, single);
|
|
11877
12322
|
argvOut.push(`--${label}-file`, tmp);
|
|
@@ -11914,7 +12359,7 @@ var init_server = __esm({
|
|
|
11914
12359
|
const file = i["file"];
|
|
11915
12360
|
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)");
|
|
11916
12361
|
if (file !== void 0) return [...base, "--file", file];
|
|
11917
|
-
const tmp =
|
|
12362
|
+
const tmp = path37.join(mkdtempSync3(path37.join(os7.tmpdir(), "tendril-envelope-")), "response.txt");
|
|
11918
12363
|
if (text !== void 0) {
|
|
11919
12364
|
writeFileSync14(tmp, text);
|
|
11920
12365
|
return [...base, "--file", tmp, "--raw"];
|
|
@@ -11946,7 +12391,7 @@ var init_server = __esm({
|
|
|
11946
12391
|
{
|
|
11947
12392
|
name: "tendril_record_status",
|
|
11948
12393
|
annotations: { readOnlyHint: true },
|
|
11949
|
-
description: "Recording-set completeness: per-rep recorded/missing tools.",
|
|
12394
|
+
description: "Recording-set completeness: per-rep recorded/missing tools, files that exist but are unusable (invalid \u2014 re-record those), and whether the set-level token map is recorded. `complete` means USABLE by the next pipeline step, including the set-level get_variable_defs.",
|
|
11950
12395
|
schema: z12.object({ setDir: str("recording set directory") }),
|
|
11951
12396
|
argv: (i) => ["record", "status", "--set", i["setDir"]]
|
|
11952
12397
|
},
|
|
@@ -11977,7 +12422,7 @@ var init_server = __esm({
|
|
|
11977
12422
|
},
|
|
11978
12423
|
{
|
|
11979
12424
|
name: "tendril_engine_score",
|
|
11980
|
-
description: "AGENT-HARNESS engine, step 2 (the oracle): scores a candidate bundle directory against recorded truth \u2014 per-config pixels, behaviors,
|
|
12425
|
+
description: "AGENT-HARNESS engine, step 2 (the oracle): scores a candidate bundle directory against recorded truth \u2014 per-config pixels, behaviors, state parity (recording-selected \u2014 a bundle cannot unschedule it) \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.",
|
|
11981
12426
|
schema: z12.object({
|
|
11982
12427
|
taskOrSet: str("reference task name or recording-set directory"),
|
|
11983
12428
|
candidateDir: str("directory containing the proposed bundle files"),
|
|
@@ -12067,15 +12512,16 @@ __export(permissions_exports, {
|
|
|
12067
12512
|
PERMISSIONS_DESCRIPTION: () => PERMISSIONS_DESCRIPTION,
|
|
12068
12513
|
buildPermissions: () => buildPermissions,
|
|
12069
12514
|
mergeAllowlist: () => mergeAllowlist,
|
|
12070
|
-
runPermissions: () => runPermissions
|
|
12515
|
+
runPermissions: () => runPermissions,
|
|
12516
|
+
writeSelection: () => writeSelection
|
|
12071
12517
|
});
|
|
12072
|
-
import { existsSync as
|
|
12073
|
-
import
|
|
12074
|
-
import
|
|
12075
|
-
function mergeAllowlist(file, entries) {
|
|
12518
|
+
import { existsSync as existsSync31, mkdirSync as mkdirSync9, readFileSync as readFileSync27, writeFileSync as writeFileSync15 } from "node:fs";
|
|
12519
|
+
import os8 from "node:os";
|
|
12520
|
+
import path38 from "node:path";
|
|
12521
|
+
function mergeAllowlist(file, entries, denyEntries = []) {
|
|
12076
12522
|
let settings = {};
|
|
12077
|
-
if (
|
|
12078
|
-
settings = JSON.parse(
|
|
12523
|
+
if (existsSync31(file) && readFileSync27(file, "utf8").trim() !== "") {
|
|
12524
|
+
settings = JSON.parse(readFileSync27(file, "utf8"));
|
|
12079
12525
|
if (settings === null || typeof settings !== "object" || Array.isArray(settings)) throw new Error("settings root is not an object");
|
|
12080
12526
|
}
|
|
12081
12527
|
const permissions = settings["permissions"] ??= {};
|
|
@@ -12085,16 +12531,31 @@ function mergeAllowlist(file, entries) {
|
|
|
12085
12531
|
const present = new Set(allow.filter((x) => typeof x === "string"));
|
|
12086
12532
|
const added = entries.filter((e) => !present.has(e));
|
|
12087
12533
|
const alreadyPresent = entries.filter((e) => present.has(e));
|
|
12088
|
-
|
|
12534
|
+
let denyAdded = [];
|
|
12535
|
+
if (denyEntries.length > 0) {
|
|
12536
|
+
const deny = permissions["deny"] ??= [];
|
|
12537
|
+
if (!Array.isArray(deny)) throw new Error("permissions.deny is not an array");
|
|
12538
|
+
const denyPresent = new Set(deny.filter((x) => typeof x === "string"));
|
|
12539
|
+
denyAdded = denyEntries.filter((e) => !denyPresent.has(e));
|
|
12540
|
+
deny.push(...denyAdded);
|
|
12541
|
+
}
|
|
12542
|
+
if (added.length > 0 || denyAdded.length > 0) {
|
|
12089
12543
|
allow.push(...added);
|
|
12090
|
-
mkdirSync9(
|
|
12544
|
+
mkdirSync9(path38.dirname(file), { recursive: true });
|
|
12091
12545
|
writeFileSync15(file, `${JSON.stringify(settings, null, 2)}
|
|
12092
12546
|
`);
|
|
12093
12547
|
}
|
|
12094
|
-
return { added, alreadyPresent };
|
|
12548
|
+
return { added, alreadyPresent, denyAdded };
|
|
12095
12549
|
}
|
|
12096
12550
|
async function buildPermissions(options) {
|
|
12097
12551
|
const LEGAL_TOOL_NAME = /^[A-Za-z0-9_-]+$/;
|
|
12552
|
+
for (const [flag, value] of [["--figma-prefix", options.figmaPrefix], ["--tendril-prefix", options.tendrilPrefix]]) {
|
|
12553
|
+
if (value !== void 0 && !LEGAL_MCP_PREFIX.test(value)) {
|
|
12554
|
+
throw new Error(`${flag} must look like mcp__<server-name> (letters, digits, _ and - only) \u2014 got ${JSON.stringify(value)}`);
|
|
12555
|
+
}
|
|
12556
|
+
}
|
|
12557
|
+
const figmaPrefix = options.figmaPrefix ?? FIGMA_PLUGIN_PREFIX;
|
|
12558
|
+
const tendrilPrefix = options.tendrilPrefix ?? TENDRIL_PLUGIN_PREFIX;
|
|
12098
12559
|
const pipeline = new Set(FIGMA_TOOL_FALLBACK);
|
|
12099
12560
|
let figmaTools = FIGMA_TOOL_FALLBACK;
|
|
12100
12561
|
try {
|
|
@@ -12106,38 +12567,64 @@ async function buildPermissions(options) {
|
|
|
12106
12567
|
}
|
|
12107
12568
|
return {
|
|
12108
12569
|
host: "claude",
|
|
12109
|
-
serverEntries: [
|
|
12570
|
+
serverEntries: [tendrilPrefix, figmaPrefix],
|
|
12110
12571
|
toolEntries: [
|
|
12111
|
-
...TOOLS.map((t) => t.name).filter((n) => LEGAL_TOOL_NAME.test(n)).map((n) => `${
|
|
12112
|
-
...figmaTools.map((name) => `${
|
|
12572
|
+
...TOOLS.map((t) => t.name).filter((n) => LEGAL_TOOL_NAME.test(n)).map((n) => `${tendrilPrefix}__${n}`),
|
|
12573
|
+
...figmaTools.map((name) => `${figmaPrefix}__${name}`)
|
|
12113
12574
|
],
|
|
12575
|
+
shellEntries: [...SHELL_READONLY_ENTRIES, ...SHELL_PIPELINE_ENTRIES],
|
|
12576
|
+
projectFileEntries: PROJECT_FILE_ENTRIES,
|
|
12577
|
+
projectDenyEntries: PROJECT_DENY_ENTRIES,
|
|
12578
|
+
manualCautionEntries: MANUAL_CAUTION_ENTRIES,
|
|
12579
|
+
shellEntriesNote: "These are CONVENIENCE grants replacing the ~300 prompts a measured run costs \u2014 not a security boundary. What they trade, named: (1) the read verbs (cat/grep/head/\u2026) let the agent read ANY file without a prompt, secrets included \u2014 remove those entries if that is unacceptable; (2) Claude Code Bash patterns are prefix matches and redirection is not a command separator (`cat x > file` rides `Bash(cat:*)`) \u2014 the host's docs call argument patterns best-effort, not an enforcement boundary (pipes and `&&`/`;` chains do decompose, each piece prompting on its own); (3) the per-subcommand CLI grants carry those subcommands' own write surface (recordings, bundles, score history, --out paths) and the npx form trusts future @latest publishes. Never auto-written: a blanket `tendril` grant (an agent could silently widen this very allowlist via `tendril permissions --write`), `find` (arbitrary delete/exec via -delete/-exec), clobber verbs \u2014 those stay a manual, deliberate addition. Write/Edit are project-scoped, written only to the project-local file, with deny entries for ./.claude/** and ./.git/** so the grant can never rewrite this settings file or plant git hooks. Every entry is removable any time.",
|
|
12114
12580
|
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.",
|
|
12115
|
-
directConfigNote: `
|
|
12581
|
+
directConfigNote: `Prefixes default to the plugin install shape. A different install (claude mcp add, the remote Figma server, \u2026) names its tools mcp__<server-name>__<tool> \u2014 pass the real prefixes with --figma-prefix / --tendril-prefix (MCP: figmaPrefix / tendrilPrefix) so the written entries actually match; run 20 measured six Figma entries that could never fire because the session's server was figma-remote.`
|
|
12116
12582
|
};
|
|
12117
12583
|
}
|
|
12584
|
+
function writeSelection(result, user) {
|
|
12585
|
+
return user ? { entries: [...result.toolEntries], denyEntries: [] } : { entries: [...result.toolEntries, ...result.shellEntries, ...result.projectFileEntries], denyEntries: [...result.projectDenyEntries] };
|
|
12586
|
+
}
|
|
12118
12587
|
async function runPermissions(flags) {
|
|
12119
12588
|
if (flags.describe) {
|
|
12120
12589
|
printDescription(PERMISSIONS_DESCRIPTION);
|
|
12121
12590
|
return;
|
|
12122
12591
|
}
|
|
12123
|
-
|
|
12592
|
+
let result;
|
|
12593
|
+
try {
|
|
12594
|
+
result = await buildPermissions({
|
|
12595
|
+
...flags.mcpUrl ? { mcpUrl: flags.mcpUrl } : {},
|
|
12596
|
+
...flags.figmaPrefix !== void 0 ? { figmaPrefix: flags.figmaPrefix } : {},
|
|
12597
|
+
...flags.tendrilPrefix !== void 0 ? { tendrilPrefix: flags.tendrilPrefix } : {}
|
|
12598
|
+
});
|
|
12599
|
+
} catch (err) {
|
|
12600
|
+
fail(flags, ExitCode.InputValidation, {
|
|
12601
|
+
error: err instanceof Error ? err.message : String(err),
|
|
12602
|
+
code: "bad-prefix",
|
|
12603
|
+
remediation: "Pass the prefix exactly as the session names its MCP tools: the part before the final __<tool> (e.g. mcp__figma-remote, mcp__plugin_figma_figma)."
|
|
12604
|
+
});
|
|
12605
|
+
return;
|
|
12606
|
+
}
|
|
12124
12607
|
if (flags.write) {
|
|
12125
12608
|
const base = process.env["INIT_CWD"] ?? process.cwd();
|
|
12126
|
-
const file = flags.user ?
|
|
12609
|
+
const file = flags.user ? path38.join(os8.homedir(), ".claude", "settings.json") : path38.join(base, ".claude", "settings.local.json");
|
|
12610
|
+
const { entries, denyEntries } = writeSelection(result, flags.user === true);
|
|
12127
12611
|
if (flags.dryRun) {
|
|
12128
|
-
emitData(flags, { file, wouldAdd:
|
|
12129
|
-
process.stdout.write(
|
|
12130
|
-
`)
|
|
12612
|
+
emitData(flags, { file, wouldAdd: entries, wouldDeny: denyEntries }, () => {
|
|
12613
|
+
process.stdout.write(
|
|
12614
|
+
`dry-run: would merge ${entries.length} allow entries (${flags.user ? "MCP tools only \u2014 shell and Write/Edit grants stay project-local" : "MCP tools + shell surface + project Write/Edit"})${denyEntries.length > 0 ? ` and ${denyEntries.length} deny guards (./.claude/**, ./.git/**)` : ""} into ${file}
|
|
12615
|
+
`
|
|
12616
|
+
);
|
|
12131
12617
|
});
|
|
12132
12618
|
return;
|
|
12133
12619
|
}
|
|
12134
12620
|
try {
|
|
12135
|
-
const { added, alreadyPresent } = mergeAllowlist(file,
|
|
12136
|
-
emitData(flags, { ...result, written: { file, added, alreadyPresent } }, () => {
|
|
12621
|
+
const { added, alreadyPresent, denyAdded } = mergeAllowlist(file, entries, denyEntries);
|
|
12622
|
+
emitData(flags, { ...result, written: { file, added, alreadyPresent, denyAdded } }, () => {
|
|
12137
12623
|
process.stdout.write(
|
|
12138
|
-
added.length === 0 ? `already installed: all ${alreadyPresent.length} Tendril pipeline entries present in ${file}
|
|
12139
|
-
` : `installed: ${added.length} allowlist entr${added.length === 1 ? "y" : "ies"} added to ${file}${alreadyPresent.length > 0 ? ` (${alreadyPresent.length} already present)` : ""}
|
|
12140
|
-
|
|
12624
|
+
added.length === 0 && denyAdded.length === 0 ? `already installed: all ${alreadyPresent.length} Tendril pipeline entries present in ${file}
|
|
12625
|
+
` : `installed: ${added.length} allowlist entr${added.length === 1 ? "y" : "ies"}${denyAdded.length > 0 ? ` and ${denyAdded.length} deny guard${denyAdded.length === 1 ? "" : "s"} (Write/Edit cannot touch ./.claude/** or ./.git/**)` : ""} added to ${file}${alreadyPresent.length > 0 ? ` (${alreadyPresent.length} already present)` : ""}
|
|
12626
|
+
${flags.user ? "" : ` \u26A0 ${result.shellEntriesNote}
|
|
12627
|
+
`} restart or reopen the Claude Code session to pick up settings changes
|
|
12141
12628
|
`
|
|
12142
12629
|
);
|
|
12143
12630
|
});
|
|
@@ -12162,6 +12649,24 @@ Or paste into .claude/settings.json under permissions.allow:
|
|
|
12162
12649
|
|
|
12163
12650
|
${quoted(result.toolEntries)}
|
|
12164
12651
|
|
|
12652
|
+
The SHELL surface a run actually uses:
|
|
12653
|
+
|
|
12654
|
+
${quoted(result.shellEntries)}
|
|
12655
|
+
|
|
12656
|
+
\u26A0 ${result.shellEntriesNote}
|
|
12657
|
+
|
|
12658
|
+
Project-scoped file access (written only to the project-local file):
|
|
12659
|
+
|
|
12660
|
+
${quoted(result.projectFileEntries)}
|
|
12661
|
+
|
|
12662
|
+
\u2026with these under permissions.deny (so the grant above can never rewrite the settings file or .git):
|
|
12663
|
+
|
|
12664
|
+
${quoted(result.projectDenyEntries)}
|
|
12665
|
+
|
|
12666
|
+
Deliberate manual additions \u2014 clobber- or exec-capable, never auto-written:
|
|
12667
|
+
|
|
12668
|
+
${quoted(result.manualCautionEntries)}
|
|
12669
|
+
|
|
12165
12670
|
Shorter but broader \u2014 one entry per server:
|
|
12166
12671
|
|
|
12167
12672
|
${quoted(result.serverEntries)}
|
|
@@ -12173,7 +12678,7 @@ ${result.directConfigNote}
|
|
|
12173
12678
|
);
|
|
12174
12679
|
});
|
|
12175
12680
|
}
|
|
12176
|
-
var FIGMA_TOOL_FALLBACK, PERMISSIONS_DESCRIPTION, TENDRIL_PLUGIN_PREFIX, FIGMA_PLUGIN_PREFIX;
|
|
12681
|
+
var FIGMA_TOOL_FALLBACK, SHELL_READONLY_ENTRIES, SHELL_PIPELINE_ENTRIES, PROJECT_FILE_ENTRIES, PROJECT_DENY_ENTRIES, MANUAL_CAUTION_ENTRIES, PERMISSIONS_DESCRIPTION, TENDRIL_PLUGIN_PREFIX, FIGMA_PLUGIN_PREFIX, LEGAL_MCP_PREFIX;
|
|
12177
12682
|
var init_permissions = __esm({
|
|
12178
12683
|
"packages/cli/src/commands/permissions.ts"() {
|
|
12179
12684
|
"use strict";
|
|
@@ -12185,15 +12690,47 @@ var init_permissions = __esm({
|
|
|
12185
12690
|
init_output();
|
|
12186
12691
|
init_doctor();
|
|
12187
12692
|
FIGMA_TOOL_FALLBACK = ["get_metadata", "get_design_context", "get_screenshot", "get_variable_defs", "get_motion_context", "get_figjam"];
|
|
12693
|
+
SHELL_READONLY_ENTRIES = [
|
|
12694
|
+
"Bash(date:*)",
|
|
12695
|
+
"Bash(ls:*)",
|
|
12696
|
+
"Bash(stat:*)",
|
|
12697
|
+
"Bash(wc:*)",
|
|
12698
|
+
"Bash(head:*)",
|
|
12699
|
+
"Bash(tail:*)",
|
|
12700
|
+
"Bash(cat:*)",
|
|
12701
|
+
"Bash(shasum:*)",
|
|
12702
|
+
"Bash(grep:*)"
|
|
12703
|
+
];
|
|
12704
|
+
SHELL_PIPELINE_ENTRIES = [
|
|
12705
|
+
// The skill's own rule: candidate dirs are created with a bare
|
|
12706
|
+
// `mkdir -p`, no compounds — one grant covers the whole batch.
|
|
12707
|
+
"Bash(mkdir -p:*)",
|
|
12708
|
+
// The published CLI, PER SUBCOMMAND — never `tendril *`. The CLI
|
|
12709
|
+
// writes the settings file through Node fs, not the host's Write
|
|
12710
|
+
// tool, so the protected-path guard that stops `Write(./**)` from
|
|
12711
|
+
// touching .claude/ does NOT stop `tendril permissions --write`: a
|
|
12712
|
+
// blanket grant would let an allowlisted agent silently widen its own
|
|
12713
|
+
// allowlist (review finding, confirmed from the host's docs).
|
|
12714
|
+
// `permissions`, `activate` and `init` are deliberately absent.
|
|
12715
|
+
...["verify", "inspect", "doctor", "record", "engine", "fonts", "generate"].flatMap((sub) => [
|
|
12716
|
+
`Bash(tendril ${sub}:*)`,
|
|
12717
|
+
`Bash(npx -y -p @tendrilapp/cli@latest tendril ${sub}:*)`
|
|
12718
|
+
])
|
|
12719
|
+
];
|
|
12720
|
+
PROJECT_FILE_ENTRIES = ["Write(./**)", "Edit(./**)"];
|
|
12721
|
+
PROJECT_DENY_ENTRIES = ["Write(./.claude/**)", "Edit(./.claude/**)", "Write(./.git/**)", "Edit(./.git/**)"];
|
|
12722
|
+
MANUAL_CAUTION_ENTRIES = ["Bash(cp -R:*)", "Bash(find:*)"];
|
|
12188
12723
|
PERMISSIONS_DESCRIPTION = {
|
|
12189
12724
|
name: "permissions",
|
|
12190
12725
|
summary: "Install (or print) the Claude Code permission allowlist for the Tendril pipeline (tendril + Figma MCP tools).",
|
|
12191
12726
|
args: [],
|
|
12192
12727
|
flags: [
|
|
12193
12728
|
{ flag: "--claude", description: "Claude Code settings format (the default and currently only format)" },
|
|
12194
|
-
{ flag: "--write", description: "Merge the
|
|
12195
|
-
{ flag: "--user", description: "With --write: target ~/.claude/settings.json
|
|
12729
|
+
{ flag: "--write", description: "Merge the pipeline allowlist (MCP tools + shell surface + project Write/Edit, with deny guards for ./.claude and ./.git) into .claude/settings.local.json in the current project (idempotent; creates the file; never touches other keys)" },
|
|
12730
|
+
{ flag: "--user", description: "With --write: target ~/.claude/settings.json instead \u2014 MCP tool entries ONLY; shell and Write/Edit grants stay project-local, never every-project" },
|
|
12196
12731
|
{ flag: "--mcp-url <url>", description: "Figma MCP endpoint to list live tool names from", default: DEFAULT_MCP_URL },
|
|
12732
|
+
{ flag: "--figma-prefix <prefix>", description: "The session's real Figma MCP entry prefix (e.g. mcp__figma-remote) \u2014 the plugin-install default cannot match other install shapes" },
|
|
12733
|
+
{ flag: "--tendril-prefix <prefix>", description: "The session's real tendril MCP entry prefix (e.g. mcp__tendril for claude mcp add installs)" },
|
|
12197
12734
|
{ flag: "--json", description: "Machine-readable output" }
|
|
12198
12735
|
],
|
|
12199
12736
|
output: {
|
|
@@ -12201,13 +12738,14 @@ var init_permissions = __esm({
|
|
|
12201
12738
|
serverEntries: "string[] \u2014 one entry per MCP server (allows every tool it serves)",
|
|
12202
12739
|
toolEntries: "string[] \u2014 per-tool entries for selective allowlists",
|
|
12203
12740
|
directConfigNote: "string \u2014 prefix rewrite for non-plugin (claude mcp add) installs",
|
|
12204
|
-
written: "with --write: { file, added, alreadyPresent }"
|
|
12741
|
+
written: "with --write: { file, added, alreadyPresent, denyAdded }"
|
|
12205
12742
|
},
|
|
12206
12743
|
exitCodes: { 0: "printed or written", 3: "with --write: the target file exists but is not JSON this command can safely rewrite" },
|
|
12207
12744
|
examples: ["tendril permissions --claude --write", "tendril permissions --claude", "tendril permissions --claude --json"]
|
|
12208
12745
|
};
|
|
12209
12746
|
TENDRIL_PLUGIN_PREFIX = "mcp__plugin_tendril_tendril";
|
|
12210
12747
|
FIGMA_PLUGIN_PREFIX = "mcp__plugin_figma_figma";
|
|
12748
|
+
LEGAL_MCP_PREFIX = /^mcp__[A-Za-z0-9_-]+$/;
|
|
12211
12749
|
}
|
|
12212
12750
|
});
|
|
12213
12751
|
|
|
@@ -12217,24 +12755,24 @@ __export(inspect_exports, {
|
|
|
12217
12755
|
INSPECT_DESCRIPTION: () => INSPECT_DESCRIPTION,
|
|
12218
12756
|
runInspect: () => runInspect
|
|
12219
12757
|
});
|
|
12220
|
-
import { existsSync as
|
|
12221
|
-
import
|
|
12758
|
+
import { existsSync as existsSync32, readFileSync as readFileSync28, writeFileSync as writeFileSync16 } from "node:fs";
|
|
12759
|
+
import path39 from "node:path";
|
|
12222
12760
|
async function runInspect(opts) {
|
|
12223
12761
|
if (opts.describe) {
|
|
12224
12762
|
printDescription(INSPECT_DESCRIPTION);
|
|
12225
12763
|
return;
|
|
12226
12764
|
}
|
|
12227
|
-
const bundleDir =
|
|
12228
|
-
const evidenceDir =
|
|
12229
|
-
const manifestPath2 =
|
|
12230
|
-
if (!
|
|
12765
|
+
const bundleDir = path39.resolve(opts.bundleDir);
|
|
12766
|
+
const evidenceDir = path39.join(bundleDir, "verify-evidence");
|
|
12767
|
+
const manifestPath2 = path39.join(bundleDir, "component.json");
|
|
12768
|
+
if (!existsSync32(evidenceDir) || !existsSync32(manifestPath2)) {
|
|
12231
12769
|
fail(opts, ExitCode.InputValidation, {
|
|
12232
|
-
error: `nothing to inspect in ${bundleDir} \u2014 ${
|
|
12770
|
+
error: `nothing to inspect in ${bundleDir} \u2014 ${existsSync32(manifestPath2) ? "no verify-evidence directory" : "no component.json"}`,
|
|
12233
12771
|
code: "no-evidence",
|
|
12234
12772
|
remediation: `Run \`${tendrilCommand(`verify ${bundleDir}`)}\` first \u2014 inspect reads the ref/render evidence that run writes.`
|
|
12235
12773
|
});
|
|
12236
12774
|
}
|
|
12237
|
-
const { manifest } = readBundleManifest(
|
|
12775
|
+
const { manifest } = readBundleManifest(readFileSync28(manifestPath2, "utf8"));
|
|
12238
12776
|
if (manifest === void 0) {
|
|
12239
12777
|
fail(opts, ExitCode.InputValidation, {
|
|
12240
12778
|
error: "component.json did not parse as a bundle manifest",
|
|
@@ -12242,8 +12780,8 @@ async function runInspect(opts) {
|
|
|
12242
12780
|
remediation: "Re-emit the bundle (score/verify rewrite component.json), then re-run inspect."
|
|
12243
12781
|
});
|
|
12244
12782
|
}
|
|
12245
|
-
const setDir =
|
|
12246
|
-
const reps = Object.keys(manifest.propAdapter).filter((rep) =>
|
|
12783
|
+
const setDir = path39.resolve(opts.set ?? manifest.provenance.recordingSet.path);
|
|
12784
|
+
const reps = Object.keys(manifest.propAdapter).filter((rep) => existsSync32(path39.join(evidenceDir, `${rep}-ref.png`)) && existsSync32(path39.join(evidenceDir, `${rep}-render.png`)));
|
|
12247
12785
|
if (reps.length === 0) {
|
|
12248
12786
|
fail(opts, ExitCode.InputValidation, {
|
|
12249
12787
|
error: "verify-evidence holds no ref/render pairs for this bundle's configs",
|
|
@@ -12254,15 +12792,15 @@ async function runInspect(opts) {
|
|
|
12254
12792
|
let crops = 0;
|
|
12255
12793
|
const sections = [];
|
|
12256
12794
|
for (const rep of reps) {
|
|
12257
|
-
const ref = new Uint8Array(
|
|
12258
|
-
const render = new Uint8Array(
|
|
12795
|
+
const ref = new Uint8Array(readFileSync28(path39.join(evidenceDir, `${rep}-ref.png`)));
|
|
12796
|
+
const render = new Uint8Array(readFileSync28(path39.join(evidenceDir, `${rep}-render.png`)));
|
|
12259
12797
|
const nodes = smallSemanticNodes(setDir, rep, opts.maxArea ?? 1024).slice(0, 12);
|
|
12260
12798
|
const cells = [];
|
|
12261
12799
|
for (const [i, n] of nodes.entries()) {
|
|
12262
12800
|
const rect = { x: n.x, y: n.y, w: n.w, h: n.h };
|
|
12263
12801
|
try {
|
|
12264
|
-
writeFileSync16(
|
|
12265
|
-
writeFileSync16(
|
|
12802
|
+
writeFileSync16(path39.join(evidenceDir, `${rep}-inspect-${i}-ref.png`), zoomCrop(ref, rect));
|
|
12803
|
+
writeFileSync16(path39.join(evidenceDir, `${rep}-inspect-${i}-render.png`), zoomCrop(render, rect));
|
|
12266
12804
|
} catch {
|
|
12267
12805
|
continue;
|
|
12268
12806
|
}
|
|
@@ -12275,7 +12813,7 @@ async function runInspect(opts) {
|
|
|
12275
12813
|
`<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>`
|
|
12276
12814
|
);
|
|
12277
12815
|
}
|
|
12278
|
-
const sheet =
|
|
12816
|
+
const sheet = path39.join(evidenceDir, "inspect.html");
|
|
12279
12817
|
writeFileSync16(
|
|
12280
12818
|
sheet,
|
|
12281
12819
|
`<!doctype html><meta charset="utf-8"><title>${esc(manifest.name)} \u2014 tendril inspect</title><style>
|
|
@@ -12353,17 +12891,17 @@ __export(generate_recorded_exports, {
|
|
|
12353
12891
|
runGenerateRecorded: () => runGenerateRecorded
|
|
12354
12892
|
});
|
|
12355
12893
|
import { confirm as confirm3, isCancel as isCancel3 } from "@clack/prompts";
|
|
12356
|
-
import { existsSync as
|
|
12357
|
-
import
|
|
12894
|
+
import { existsSync as existsSync33, readFileSync as readFileSync29 } from "node:fs";
|
|
12895
|
+
import path40 from "node:path";
|
|
12358
12896
|
async function runGenerateRecorded(opts) {
|
|
12359
12897
|
const callerCwd = process.env["INIT_CWD"] ?? process.cwd();
|
|
12360
|
-
const outDirAbs =
|
|
12361
|
-
const recordedAsPath =
|
|
12898
|
+
const outDirAbs = path40.resolve(callerCwd, opts.out);
|
|
12899
|
+
const recordedAsPath = path40.resolve(callerCwd, opts.recorded);
|
|
12362
12900
|
let task;
|
|
12363
12901
|
let taskName;
|
|
12364
12902
|
let authoredApi;
|
|
12365
12903
|
let composition;
|
|
12366
|
-
const isSet =
|
|
12904
|
+
const isSet = existsSync33(path40.join(recordedAsPath, "recording-set.json"));
|
|
12367
12905
|
const registry = TASKS[opts.recorded];
|
|
12368
12906
|
if (registry !== void 0 && !isSet) {
|
|
12369
12907
|
task = registry;
|
|
@@ -12372,7 +12910,7 @@ async function runGenerateRecorded(opts) {
|
|
|
12372
12910
|
try {
|
|
12373
12911
|
const authored = authorTaskFromSet(recordedAsPath);
|
|
12374
12912
|
task = authored.task;
|
|
12375
|
-
taskName =
|
|
12913
|
+
taskName = path40.basename(recordedAsPath);
|
|
12376
12914
|
authoredApi = authored.api;
|
|
12377
12915
|
const roles = RolesSchema.safeParse(loadManifest(recordedAsPath).roles);
|
|
12378
12916
|
if (roles.success) composition = roles.data;
|
|
@@ -12406,7 +12944,7 @@ async function runGenerateRecorded(opts) {
|
|
|
12406
12944
|
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)`);
|
|
12407
12945
|
}
|
|
12408
12946
|
const missing = task.configs.filter(
|
|
12409
|
-
(c) => !
|
|
12947
|
+
(c) => !existsSync33(path40.join(task.set, c.rep, "get_screenshot.json")) || !existsSync33(path40.join(task.set, c.rep, "get_metadata.json")) || !existsSync33(path40.join(task.set, c.rep, "get_design_context.json"))
|
|
12410
12948
|
);
|
|
12411
12949
|
if (missing.length > 0) {
|
|
12412
12950
|
fail(opts, ExitCode.RecordingIncomplete, {
|
|
@@ -12476,8 +13014,8 @@ async function runGenerateRecorded(opts) {
|
|
|
12476
13014
|
` : `${line}
|
|
12477
13015
|
`);
|
|
12478
13016
|
if (opts.dryRun) {
|
|
12479
|
-
emitData(opts, { dryRun: true, task: taskName, model: modelId, consent, wouldWrite:
|
|
12480
|
-
process.stdout.write(`dry-run: nothing sent, nothing written (would write ${
|
|
13017
|
+
emitData(opts, { dryRun: true, task: taskName, model: modelId, consent, wouldWrite: path40.join(outDirAbs, taskName) }, () => {
|
|
13018
|
+
process.stdout.write(`dry-run: nothing sent, nothing written (would write ${path40.join(outDirAbs, taskName)})
|
|
12481
13019
|
`);
|
|
12482
13020
|
});
|
|
12483
13021
|
return;
|
|
@@ -12500,10 +13038,10 @@ async function runGenerateRecorded(opts) {
|
|
|
12500
13038
|
});
|
|
12501
13039
|
}
|
|
12502
13040
|
}
|
|
12503
|
-
const bundleDir =
|
|
12504
|
-
if (
|
|
13041
|
+
const bundleDir = path40.join(outDirAbs, taskName);
|
|
13042
|
+
if (existsSync33(path40.join(bundleDir, "component.json"))) {
|
|
12505
13043
|
try {
|
|
12506
|
-
const prior = readBundleManifest(
|
|
13044
|
+
const prior = readBundleManifest(readFileSync29(path40.join(bundleDir, "component.json"), "utf8")).manifest;
|
|
12507
13045
|
if (prior !== void 0 && prior.provenance.recordingSet.hash !== recordingSetHash(task.set, task.configs)) {
|
|
12508
13046
|
fail(opts, ExitCode.InputValidation, {
|
|
12509
13047
|
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`,
|
|
@@ -12669,7 +13207,7 @@ init_invocation();
|
|
|
12669
13207
|
init_output();
|
|
12670
13208
|
import { intro, isCancel, outro, password } from "@clack/prompts";
|
|
12671
13209
|
import fs from "node:fs";
|
|
12672
|
-
import
|
|
13210
|
+
import path25 from "node:path";
|
|
12673
13211
|
var INIT_DESCRIPTION = {
|
|
12674
13212
|
name: "init",
|
|
12675
13213
|
summary: "Configure Figma and OpenRouter credentials in .env (idempotent).",
|
|
@@ -12706,7 +13244,7 @@ async function runInit(flags) {
|
|
|
12706
13244
|
printDescription(INIT_DESCRIPTION);
|
|
12707
13245
|
return;
|
|
12708
13246
|
}
|
|
12709
|
-
const envPath =
|
|
13247
|
+
const envPath = path25.resolve(process.cwd(), ".env");
|
|
12710
13248
|
const existing = fs.existsSync(envPath) ? parseEnv(fs.readFileSync(envPath, "utf8")) : /* @__PURE__ */ new Map();
|
|
12711
13249
|
let figmaToken = flags.figmaToken ?? existing.get(ENV_KEYS.figma);
|
|
12712
13250
|
let openrouterKey = flags.openrouterKey ?? existing.get(ENV_KEYS.openrouter);
|
|
@@ -12727,7 +13265,7 @@ async function runInit(flags) {
|
|
|
12727
13265
|
next.set(ENV_KEYS.figma, figmaToken);
|
|
12728
13266
|
next.set(ENV_KEYS.openrouter, openrouterKey);
|
|
12729
13267
|
const changed = existing.get(ENV_KEYS.figma) !== next.get(ENV_KEYS.figma) || existing.get(ENV_KEYS.openrouter) !== next.get(ENV_KEYS.openrouter);
|
|
12730
|
-
const gitignorePath =
|
|
13268
|
+
const gitignorePath = path25.resolve(process.cwd(), ".gitignore");
|
|
12731
13269
|
const gitignore = fs.existsSync(gitignorePath) ? fs.readFileSync(gitignorePath, "utf8") : "";
|
|
12732
13270
|
const gitignoreCoversEnv = gitignore.split("\n").some((line) => [".env", ".env", ".env*"].includes(line.trim()));
|
|
12733
13271
|
if (flags.dryRun) {
|
|
@@ -12783,14 +13321,14 @@ init_invocation();
|
|
|
12783
13321
|
init_output();
|
|
12784
13322
|
init_entitlement();
|
|
12785
13323
|
import { confirm as confirm2, isCancel as isCancel2 } from "@clack/prompts";
|
|
12786
|
-
import { readFileSync as
|
|
13324
|
+
import { readFileSync as readFileSync16, readdirSync as readdirSync5, existsSync as existsSync20 } from "node:fs";
|
|
12787
13325
|
|
|
12788
13326
|
// packages/cli/src/pipeline.ts
|
|
12789
13327
|
init_src2();
|
|
12790
13328
|
init_src4();
|
|
12791
13329
|
init_src6();
|
|
12792
13330
|
import { mkdirSync as mkdirSync5, writeFileSync as writeFileSync8 } from "node:fs";
|
|
12793
|
-
import
|
|
13331
|
+
import path26 from "node:path";
|
|
12794
13332
|
|
|
12795
13333
|
// packages/cli/src/assets-module.ts
|
|
12796
13334
|
init_src();
|
|
@@ -13126,7 +13664,7 @@ async function runGenerationPipeline(input) {
|
|
|
13126
13664
|
});
|
|
13127
13665
|
const written = [];
|
|
13128
13666
|
if (!input.dryRun) {
|
|
13129
|
-
const dir =
|
|
13667
|
+
const dir = path26.resolve(input.outDir, semantics.componentName);
|
|
13130
13668
|
mkdirSync5(dir, { recursive: true });
|
|
13131
13669
|
const files = {
|
|
13132
13670
|
// Bundle-local tokens: THE emission the component's CSS resolves
|
|
@@ -13150,13 +13688,13 @@ async function runGenerationPipeline(input) {
|
|
|
13150
13688
|
`
|
|
13151
13689
|
};
|
|
13152
13690
|
for (const [name, content] of Object.entries(files)) {
|
|
13153
|
-
const filePath =
|
|
13691
|
+
const filePath = path26.join(dir, name);
|
|
13154
13692
|
writeFileSync8(filePath, content);
|
|
13155
13693
|
written.push(filePath);
|
|
13156
13694
|
}
|
|
13157
13695
|
for (const artifact of emitTokenArtifacts(input.mapping)) {
|
|
13158
|
-
const filePath =
|
|
13159
|
-
mkdirSync5(
|
|
13696
|
+
const filePath = path26.resolve(input.outDir, artifact.path);
|
|
13697
|
+
mkdirSync5(path26.dirname(filePath), { recursive: true });
|
|
13160
13698
|
writeFileSync8(filePath, artifact.content);
|
|
13161
13699
|
written.push(filePath);
|
|
13162
13700
|
}
|
|
@@ -13215,7 +13753,7 @@ var GENERATE_DESCRIPTION = {
|
|
|
13215
13753
|
function resolveProvidedSource(flags, contextFile) {
|
|
13216
13754
|
let raw;
|
|
13217
13755
|
try {
|
|
13218
|
-
raw =
|
|
13756
|
+
raw = readFileSync16(contextFile, "utf8");
|
|
13219
13757
|
} catch {
|
|
13220
13758
|
fail(flags, ExitCode.InputValidation, {
|
|
13221
13759
|
error: `Cannot read context file "${contextFile}".`,
|
|
@@ -13335,11 +13873,11 @@ token mapping (${mapping.flat.length} variables):
|
|
|
13335
13873
|
let initialCode;
|
|
13336
13874
|
let initialSemantics;
|
|
13337
13875
|
try {
|
|
13338
|
-
if (
|
|
13339
|
-
for (const entry of
|
|
13876
|
+
if (existsSync20(flags.out)) {
|
|
13877
|
+
for (const entry of readdirSync5(flags.out)) {
|
|
13340
13878
|
const cjPath = `${flags.out}/${entry}/component.json`;
|
|
13341
|
-
if (!
|
|
13342
|
-
const cj = JSON.parse(
|
|
13879
|
+
if (!existsSync20(cjPath)) continue;
|
|
13880
|
+
const cj = JSON.parse(readFileSync16(cjPath, "utf8"));
|
|
13343
13881
|
if (cj.name !== void 0 && Array.isArray(cj.props)) {
|
|
13344
13882
|
previousApi = JSON.stringify({
|
|
13345
13883
|
componentName: cj.name,
|
|
@@ -13347,14 +13885,14 @@ token mapping (${mapping.flat.length} variables):
|
|
|
13347
13885
|
});
|
|
13348
13886
|
const tsxPath = `${flags.out}/${entry}/${cj.name}.tsx`;
|
|
13349
13887
|
const cssPath = `${flags.out}/${entry}/${cj.name}.css`;
|
|
13350
|
-
if (flags.refine &&
|
|
13888
|
+
if (flags.refine && existsSync20(tsxPath) && existsSync20(cssPath)) {
|
|
13351
13889
|
initialCode = {
|
|
13352
|
-
tsx:
|
|
13353
|
-
css:
|
|
13890
|
+
tsx: readFileSync16(tsxPath, "utf8"),
|
|
13891
|
+
css: readFileSync16(cssPath, "utf8")
|
|
13354
13892
|
};
|
|
13355
13893
|
const semPath = `${flags.out}/${entry}/semantics.json`;
|
|
13356
|
-
if (
|
|
13357
|
-
initialSemantics = JSON.parse(
|
|
13894
|
+
if (existsSync20(semPath)) {
|
|
13895
|
+
initialSemantics = JSON.parse(readFileSync16(semPath, "utf8"));
|
|
13358
13896
|
}
|
|
13359
13897
|
}
|
|
13360
13898
|
break;
|
|
@@ -13631,6 +14169,23 @@ function buildProgram() {
|
|
|
13631
14169
|
...local["face"] !== void 0 ? { face: Number(local["face"]) } : {}
|
|
13632
14170
|
});
|
|
13633
14171
|
});
|
|
14172
|
+
fonts.command("discover").description("List faces installed on THIS machine (name-table parse; no OS tools, no network). Refusal-class, italic and variable faces are listed with why they cannot be cached.").argument("[family]", "family to match exactly (case/space-insensitive); omit to list everything").option("--cache <dir>", "cache directory (default: the per-user font cache)").action(async (family, _o, cmd) => {
|
|
14173
|
+
const flags = globalFlags(cmd.parent.parent);
|
|
14174
|
+
const { runFontsDiscover: runFontsDiscover2 } = await Promise.resolve().then(() => (init_fonts(), fonts_exports));
|
|
14175
|
+
runFontsDiscover2({ ...flags, cacheDir: cmd.opts()["cache"] ?? DEFAULT_FONT_CACHE, ...family !== void 0 ? { family } : {} });
|
|
14176
|
+
});
|
|
14177
|
+
fonts.command("add-system").description("Cache a family from the installed fonts so a licensed kit can be SCORED locally. Licence lands as unknown: bundles declare the face; its bytes never ship.").argument("<family>", "family exactly as fonts discover lists it").option("--weights <w...>", "only these numeric weights (default: every eligible static upright weight)").option("--set <dir>", "recording set to verify the family spelling against").option("--cache <dir>", "cache directory (default: the per-user font cache)").action(async (family, _o, cmd) => {
|
|
14178
|
+
const flags = globalFlags(cmd.parent.parent);
|
|
14179
|
+
const local = cmd.opts();
|
|
14180
|
+
const { runFontsAddSystem: runFontsAddSystem2 } = await Promise.resolve().then(() => (init_fonts(), fonts_exports));
|
|
14181
|
+
runFontsAddSystem2({
|
|
14182
|
+
...flags,
|
|
14183
|
+
family,
|
|
14184
|
+
cacheDir: local["cache"] ?? DEFAULT_FONT_CACHE,
|
|
14185
|
+
...local["weights"] !== void 0 ? { weights: local["weights"].map(Number) } : {},
|
|
14186
|
+
...local["set"] !== void 0 ? { set: local["set"] } : {}
|
|
14187
|
+
});
|
|
14188
|
+
});
|
|
13634
14189
|
fonts.command("status").option("--lock <file>", "hash lock to verify against (e.g. packages/verify/fixtures/fonts.lock.json)").option("--cache <dir>", "cache directory (default: the per-user font cache)").action(async (_o, cmd) => {
|
|
13635
14190
|
const flags = globalFlags(cmd.parent.parent);
|
|
13636
14191
|
const local = cmd.opts();
|
|
@@ -13681,7 +14236,7 @@ function buildProgram() {
|
|
|
13681
14236
|
...local["out"] !== void 0 ? { out: local["out"] } : {}
|
|
13682
14237
|
});
|
|
13683
14238
|
});
|
|
13684
|
-
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
|
|
14239
|
+
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 \u2014 MCP tool entries only; shell and Write/Edit grants stay project-local").option("--mcp-url <url>", "Figma MCP endpoint to list live tool names from").option("--figma-prefix <prefix>", "the session's real Figma MCP entry prefix (e.g. mcp__figma-remote) when it is not the plugin default").option("--tendril-prefix <prefix>", "the session's real tendril MCP entry prefix (e.g. mcp__tendril for claude mcp add installs)").action(async (_o, cmd) => {
|
|
13685
14240
|
const flags = globalFlags(cmd.parent);
|
|
13686
14241
|
const local = cmd.opts();
|
|
13687
14242
|
const { runPermissions: runPermissions2 } = await Promise.resolve().then(() => (init_permissions(), permissions_exports));
|
|
@@ -13689,7 +14244,9 @@ function buildProgram() {
|
|
|
13689
14244
|
...flags,
|
|
13690
14245
|
...local["write"] !== void 0 ? { write: local["write"] } : {},
|
|
13691
14246
|
...local["user"] !== void 0 ? { user: local["user"] } : {},
|
|
13692
|
-
...local["mcpUrl"] !== void 0 ? { mcpUrl: local["mcpUrl"] } : {}
|
|
14247
|
+
...local["mcpUrl"] !== void 0 ? { mcpUrl: local["mcpUrl"] } : {},
|
|
14248
|
+
...local["figmaPrefix"] !== void 0 ? { figmaPrefix: local["figmaPrefix"] } : {},
|
|
14249
|
+
...local["tendrilPrefix"] !== void 0 ? { tendrilPrefix: local["tendrilPrefix"] } : {}
|
|
13693
14250
|
});
|
|
13694
14251
|
});
|
|
13695
14252
|
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) => {
|