@tendrilapp/cli 0.1.26 → 0.1.28
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 +39 -15
- package/dist/tendril-mcp.js +2 -2
- package/dist/tendril.js +912 -453
- package/package.json +1 -1
package/dist/tendril.js
CHANGED
|
@@ -1028,6 +1028,14 @@ function planSet(setDir, component, symbols, opts = {}) {
|
|
|
1028
1028
|
const { manifest: existing, raw } = readManifestFile(setDir);
|
|
1029
1029
|
const wantsNewDefaults = opts.defaults !== void 0 && JSON.stringify(opts.defaults) !== JSON.stringify(existing.defaults ?? {});
|
|
1030
1030
|
const anythingRecorded = existing.reps.some((r) => RECORD_TOOLS.some((t) => existsSync(path.join(setDir, r.slug, `${t}.json`))));
|
|
1031
|
+
const scopeUpgraded = opts.variantScope === "component-set" && existing.variantScope !== "component-set";
|
|
1032
|
+
if (opts.variantScope === "component-set") {
|
|
1033
|
+
raw["variantScope"] = "component-set";
|
|
1034
|
+
const variants = symbols.map((s) => s.name).filter((n) => n.includes("="));
|
|
1035
|
+
if (variants.length > 0 && (scopeUpgraded || variants.length > (existing.latticeNames ?? []).length)) {
|
|
1036
|
+
raw["latticeNames"] = variants;
|
|
1037
|
+
}
|
|
1038
|
+
}
|
|
1031
1039
|
if (!wantsNewDefaults || anythingRecorded) {
|
|
1032
1040
|
if (opts.sample !== true) {
|
|
1033
1041
|
const known = new Set(existing.reps.map((r) => r.nodeId));
|
|
@@ -1049,6 +1057,10 @@ function planSet(setDir, component, symbols, opts = {}) {
|
|
|
1049
1057
|
return { manifest: manifest2, plan: { reps: [], notRecorded: [] }, resumed: true, toppedUp: appended.map((a) => ({ slug: a.slug, nodeId: a.nodeId })) };
|
|
1050
1058
|
}
|
|
1051
1059
|
}
|
|
1060
|
+
if (scopeUpgraded) {
|
|
1061
|
+
const manifest2 = writeManifest(setDir, raw);
|
|
1062
|
+
return { manifest: manifest2, plan: { reps: [], notRecorded: [] }, resumed: true };
|
|
1063
|
+
}
|
|
1052
1064
|
return { manifest: existing, plan: { reps: [], notRecorded: [] }, resumed: true };
|
|
1053
1065
|
}
|
|
1054
1066
|
}
|
|
@@ -1325,8 +1337,8 @@ var init_src = __esm({
|
|
|
1325
1337
|
function variableNameToPath(name) {
|
|
1326
1338
|
return name.split("/").map(canonicalCssIdentPart).filter((part) => part.length > 0);
|
|
1327
1339
|
}
|
|
1328
|
-
function tokenPathToCssVar(
|
|
1329
|
-
return `--${
|
|
1340
|
+
function tokenPathToCssVar(path41) {
|
|
1341
|
+
return `--${path41.join("-")}`;
|
|
1330
1342
|
}
|
|
1331
1343
|
function toDtcgToken(variable, defaultMode) {
|
|
1332
1344
|
const modes = Object.keys(variable.valuesByMode);
|
|
@@ -1370,11 +1382,11 @@ function toDtcgToken(variable, defaultMode) {
|
|
|
1370
1382
|
}
|
|
1371
1383
|
function mapVariablesToDtcg(variables, defaultMode = "light") {
|
|
1372
1384
|
const entries = variables.map((variable) => {
|
|
1373
|
-
const
|
|
1374
|
-
if (
|
|
1385
|
+
const path41 = variableNameToPath(variable.name);
|
|
1386
|
+
if (path41.length === 0) {
|
|
1375
1387
|
throw new DtcgMappingError("empty-name", `Figma variable ${variable.id} has an empty name`);
|
|
1376
1388
|
}
|
|
1377
|
-
return { variable, path:
|
|
1389
|
+
return { variable, path: path41 };
|
|
1378
1390
|
});
|
|
1379
1391
|
const groupPrefixes = /* @__PURE__ */ new Set();
|
|
1380
1392
|
for (const e of entries) {
|
|
@@ -1395,21 +1407,21 @@ function mapVariablesToDtcg(variables, defaultMode = "light") {
|
|
|
1395
1407
|
}
|
|
1396
1408
|
const tokens = {};
|
|
1397
1409
|
const flat = [];
|
|
1398
|
-
for (const { variable, path:
|
|
1410
|
+
for (const { variable, path: path41 } of entries) {
|
|
1399
1411
|
const token = toDtcgToken(variable, defaultMode);
|
|
1400
1412
|
let group = tokens;
|
|
1401
|
-
for (const segment of
|
|
1413
|
+
for (const segment of path41.slice(0, -1)) {
|
|
1402
1414
|
const existing = group[segment];
|
|
1403
1415
|
group = existing ?? (group[segment] = {});
|
|
1404
1416
|
}
|
|
1405
|
-
const leaf =
|
|
1417
|
+
const leaf = path41[path41.length - 1];
|
|
1406
1418
|
if (group[leaf] !== void 0) {
|
|
1407
|
-
throw new DtcgMappingError("duplicate-path", `Duplicate token path "${
|
|
1419
|
+
throw new DtcgMappingError("duplicate-path", `Duplicate token path "${path41.join(".")}" (variable ${variable.id})`);
|
|
1408
1420
|
}
|
|
1409
1421
|
group[leaf] = token;
|
|
1410
1422
|
flat.push({
|
|
1411
|
-
path:
|
|
1412
|
-
cssVar: tokenPathToCssVar(
|
|
1423
|
+
path: path41.join("."),
|
|
1424
|
+
cssVar: tokenPathToCssVar(path41),
|
|
1413
1425
|
type: token.$type,
|
|
1414
1426
|
value: token.$value
|
|
1415
1427
|
});
|
|
@@ -1598,9 +1610,9 @@ function boundId(value) {
|
|
|
1598
1610
|
return isObject(value) && typeof value["boundVariableId"] === "string" ? value["boundVariableId"] : void 0;
|
|
1599
1611
|
}
|
|
1600
1612
|
function resolveBinding(ctx, id) {
|
|
1601
|
-
const
|
|
1602
|
-
if (
|
|
1603
|
-
return
|
|
1613
|
+
const path41 = ctx.pathById.get(id);
|
|
1614
|
+
if (path41 === void 0) ctx.unresolved.add(id);
|
|
1615
|
+
return path41;
|
|
1604
1616
|
}
|
|
1605
1617
|
function parseVariantProps(name) {
|
|
1606
1618
|
if (!name.includes("=")) return void 0;
|
|
@@ -1635,8 +1647,8 @@ function walk(ctx, raw) {
|
|
|
1635
1647
|
if (!isObject(paint) || paint["visible"] === false) continue;
|
|
1636
1648
|
const id = boundId(paint);
|
|
1637
1649
|
if (id !== void 0) {
|
|
1638
|
-
const
|
|
1639
|
-
if (
|
|
1650
|
+
const path41 = resolveBinding(ctx, id);
|
|
1651
|
+
if (path41 !== void 0) tokens.add(path41);
|
|
1640
1652
|
} else if (typeof paint["color"] === "string") {
|
|
1641
1653
|
ctx.hardcoded.push({ node: name, property, value: paint["color"] });
|
|
1642
1654
|
}
|
|
@@ -1644,8 +1656,8 @@ function walk(ctx, raw) {
|
|
|
1644
1656
|
}
|
|
1645
1657
|
const radiusId = boundId(raw["cornerRadius"]);
|
|
1646
1658
|
if (radiusId !== void 0) {
|
|
1647
|
-
const
|
|
1648
|
-
if (
|
|
1659
|
+
const path41 = resolveBinding(ctx, radiusId);
|
|
1660
|
+
if (path41 !== void 0) tokens.add(path41);
|
|
1649
1661
|
} else if (typeof raw["cornerRadius"] === "number" && raw["cornerRadius"] !== 0) {
|
|
1650
1662
|
ctx.hardcoded.push({ node: name, property: "border-radius", value: `${raw["cornerRadius"]}px` });
|
|
1651
1663
|
}
|
|
@@ -1655,10 +1667,10 @@ function walk(ctx, raw) {
|
|
|
1655
1667
|
layout = { mode: layoutMode === "HORIZONTAL" ? "flex-row" : "flex-column" };
|
|
1656
1668
|
const gapId = boundId(raw["itemSpacing"]);
|
|
1657
1669
|
if (gapId !== void 0) {
|
|
1658
|
-
const
|
|
1659
|
-
if (
|
|
1660
|
-
layout.gap =
|
|
1661
|
-
tokens.add(
|
|
1670
|
+
const path41 = resolveBinding(ctx, gapId);
|
|
1671
|
+
if (path41 !== void 0) {
|
|
1672
|
+
layout.gap = path41;
|
|
1673
|
+
tokens.add(path41);
|
|
1662
1674
|
}
|
|
1663
1675
|
} else if (typeof raw["itemSpacing"] === "number" && raw["itemSpacing"] !== 0) {
|
|
1664
1676
|
ctx.hardcoded.push({ node: name, property: "gap", value: `${raw["itemSpacing"]}px` });
|
|
@@ -1667,10 +1679,10 @@ function walk(ctx, raw) {
|
|
|
1667
1679
|
for (const field of PADDING_FIELDS) {
|
|
1668
1680
|
const id = boundId(raw[field]);
|
|
1669
1681
|
if (id !== void 0) {
|
|
1670
|
-
const
|
|
1671
|
-
if (
|
|
1672
|
-
paddingPaths.push(
|
|
1673
|
-
tokens.add(
|
|
1682
|
+
const path41 = resolveBinding(ctx, id);
|
|
1683
|
+
if (path41 !== void 0) {
|
|
1684
|
+
paddingPaths.push(path41);
|
|
1685
|
+
tokens.add(path41);
|
|
1674
1686
|
}
|
|
1675
1687
|
} else if (typeof raw[field] === "number" && raw[field] !== 0) {
|
|
1676
1688
|
ctx.hardcoded.push({ node: name, property: field, value: `${raw[field]}px` });
|
|
@@ -1921,7 +1933,7 @@ function tendrilCommand(args) {
|
|
|
1921
1933
|
return `${tendrilInvocation()} ${args}`;
|
|
1922
1934
|
}
|
|
1923
1935
|
function quoteArg(value) {
|
|
1924
|
-
if (/["
|
|
1936
|
+
if (/["`$]/.test(value)) throw new Error(`refusing to emit a shell argument containing a double quote, backtick or $: ${value}`);
|
|
1925
1937
|
return /[\s'*?[\]()&;|<>#~]/.test(value) ? `"${value}"` : value;
|
|
1926
1938
|
}
|
|
1927
1939
|
var NPX_INVOCATION, cached;
|
|
@@ -3486,6 +3498,7 @@ function isCollection(bytes) {
|
|
|
3486
3498
|
}
|
|
3487
3499
|
function nameTableStrings(view, bytes, nameOffset, nameLength) {
|
|
3488
3500
|
const out = /* @__PURE__ */ new Map();
|
|
3501
|
+
const rank = /* @__PURE__ */ new Map();
|
|
3489
3502
|
if (nameOffset + 6 > bytes.length) return out;
|
|
3490
3503
|
const count = view.getUint16(nameOffset + 2);
|
|
3491
3504
|
const stringOffset = nameOffset + view.getUint16(nameOffset + 4);
|
|
@@ -3499,14 +3512,21 @@ function nameTableStrings(view, bytes, nameOffset, nameLength) {
|
|
|
3499
3512
|
const offset = stringOffset + view.getUint16(rec + 10);
|
|
3500
3513
|
if (offset + length > bytes.length || offset + length > nameOffset + nameLength) continue;
|
|
3501
3514
|
const slice = bytes.subarray(offset, offset + length);
|
|
3502
|
-
const
|
|
3515
|
+
const languageId = view.getUint16(rec + 4);
|
|
3503
3516
|
let value = "";
|
|
3504
|
-
if (
|
|
3517
|
+
if (platformId === 3 || platformId === 0) {
|
|
3505
3518
|
for (let j = 0; j + 1 < slice.length; j += 2) value += String.fromCharCode(slice[j] << 8 | slice[j + 1]);
|
|
3506
|
-
} else {
|
|
3519
|
+
} else if (platformId === 1 && encodingId === 0) {
|
|
3507
3520
|
for (const b of slice) value += String.fromCharCode(b);
|
|
3521
|
+
} else {
|
|
3522
|
+
continue;
|
|
3523
|
+
}
|
|
3524
|
+
if (value === "") continue;
|
|
3525
|
+
const score = platformId === 3 && languageId === 1033 ? 5 : platformId === 0 ? 4 : platformId === 1 && languageId === 0 ? 3 : platformId === 3 ? 2 : 1;
|
|
3526
|
+
if (score > (rank.get(nameId) ?? 0)) {
|
|
3527
|
+
rank.set(nameId, score);
|
|
3528
|
+
out.set(nameId, value);
|
|
3508
3529
|
}
|
|
3509
|
-
if (value !== "" && !out.has(nameId)) out.set(nameId, value);
|
|
3510
3530
|
}
|
|
3511
3531
|
return out;
|
|
3512
3532
|
}
|
|
@@ -3586,15 +3606,151 @@ var init_font_collection = __esm({
|
|
|
3586
3606
|
}
|
|
3587
3607
|
});
|
|
3588
3608
|
|
|
3589
|
-
// packages/verify/src/font-
|
|
3590
|
-
import {
|
|
3591
|
-
import { existsSync as existsSync6, mkdirSync as mkdirSync2, readFileSync as readFileSync3, writeFileSync as writeFileSync3 } from "node:fs";
|
|
3609
|
+
// packages/verify/src/font-discovery.ts
|
|
3610
|
+
import { existsSync as existsSync6, readdirSync as readdirSync2, readFileSync as readFileSync3, realpathSync as realpathSync2 } from "node:fs";
|
|
3592
3611
|
import os2 from "node:os";
|
|
3593
3612
|
import path10 from "node:path";
|
|
3613
|
+
function weightFromSubfamily(subfamily) {
|
|
3614
|
+
for (const [re, w] of WEIGHT_TOKENS) if (re.test(subfamily)) return w;
|
|
3615
|
+
return void 0;
|
|
3616
|
+
}
|
|
3617
|
+
function systemFaceRefusal(family) {
|
|
3618
|
+
if (/^\.?SF([ -]|$)/i.test(family) || /^San Francisco/i.test(family) || /^\.?Apple/i.test(family) || /^\.?New York$/i.test(family)) {
|
|
3619
|
+
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).";
|
|
3620
|
+
}
|
|
3621
|
+
if (/^Segoe UI Variable/i.test(family)) {
|
|
3622
|
+
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).";
|
|
3623
|
+
}
|
|
3624
|
+
return void 0;
|
|
3625
|
+
}
|
|
3626
|
+
function faceAt(bytes, view, dirOffset, file, faceIndex) {
|
|
3627
|
+
if (dirOffset + 12 > bytes.length || !SFNT_VERSIONS.has(view.getUint32(dirOffset))) return null;
|
|
3628
|
+
const numTables = view.getUint16(dirOffset + 4);
|
|
3629
|
+
let names = /* @__PURE__ */ new Map();
|
|
3630
|
+
let os2Weight;
|
|
3631
|
+
let os2Italic = false;
|
|
3632
|
+
let variable = false;
|
|
3633
|
+
for (let t = 0; t < numTables; t++) {
|
|
3634
|
+
const rec = dirOffset + 12 + t * 16;
|
|
3635
|
+
if (rec + 16 > bytes.length) break;
|
|
3636
|
+
const tag = String.fromCharCode(bytes[rec], bytes[rec + 1], bytes[rec + 2], bytes[rec + 3]);
|
|
3637
|
+
const offset = view.getUint32(rec + 8);
|
|
3638
|
+
const length = view.getUint32(rec + 12);
|
|
3639
|
+
if (tag === "name") names = nameTableStrings(view, bytes, offset, length);
|
|
3640
|
+
else if (tag === "OS/2" && offset + 64 <= bytes.length) {
|
|
3641
|
+
const w = view.getUint16(offset + 4);
|
|
3642
|
+
if (w >= 1 && w <= 1e3) os2Weight = w;
|
|
3643
|
+
os2Italic = (view.getUint16(offset + 62) & 1) !== 0;
|
|
3644
|
+
} else if (tag === "fvar") variable = true;
|
|
3645
|
+
}
|
|
3646
|
+
const family = names.get(16) ?? names.get(1);
|
|
3647
|
+
if (family === void 0 || family === "") return null;
|
|
3648
|
+
const subfamily = names.get(17) ?? names.get(2) ?? "";
|
|
3649
|
+
const italic = /italic|oblique|inclined|slanted/i.test(subfamily) || os2Italic;
|
|
3650
|
+
const weight = weightFromSubfamily(subfamily) ?? os2Weight ?? 400;
|
|
3651
|
+
return { family, subfamily, weight, italic, variable, file, ...faceIndex !== void 0 ? { faceIndex } : {} };
|
|
3652
|
+
}
|
|
3653
|
+
function facesInFile(file) {
|
|
3654
|
+
try {
|
|
3655
|
+
const bytes = new Uint8Array(readFileSync3(file));
|
|
3656
|
+
if (bytes.length < 12) return [];
|
|
3657
|
+
const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
|
|
3658
|
+
if (isCollection(bytes)) {
|
|
3659
|
+
const numFonts = view.getUint32(8);
|
|
3660
|
+
const faces = [];
|
|
3661
|
+
for (let i = 0; i < numFonts; i++) {
|
|
3662
|
+
const f = faceAt(bytes, view, view.getUint32(12 + i * 4), file, i);
|
|
3663
|
+
if (f !== null) faces.push(f);
|
|
3664
|
+
}
|
|
3665
|
+
return faces;
|
|
3666
|
+
}
|
|
3667
|
+
const single = faceAt(bytes, view, 0, file);
|
|
3668
|
+
return single === null ? [] : [single];
|
|
3669
|
+
} catch {
|
|
3670
|
+
return [];
|
|
3671
|
+
}
|
|
3672
|
+
}
|
|
3673
|
+
function systemFontDirs() {
|
|
3674
|
+
const env = process.env["TENDRIL_SYSTEM_FONT_DIRS"];
|
|
3675
|
+
if (env !== void 0 && env !== "") return env.split(path10.delimiter).filter((d) => d !== "" && existsSync6(d));
|
|
3676
|
+
const home = os2.homedir();
|
|
3677
|
+
const dirs = process.platform === "darwin" ? ["/System/Library/Fonts", "/Library/Fonts", path10.join(home, "Library", "Fonts")] : process.platform === "win32" ? [
|
|
3678
|
+
path10.join(process.env["WINDIR"] ?? "C:\\Windows", "Fonts"),
|
|
3679
|
+
...process.env["LOCALAPPDATA"] !== void 0 ? [path10.join(process.env["LOCALAPPDATA"], "Microsoft", "Windows", "Fonts")] : []
|
|
3680
|
+
] : ["/usr/share/fonts", "/usr/local/share/fonts", path10.join(home, ".fonts"), path10.join(home, ".local", "share", "fonts")];
|
|
3681
|
+
return dirs.filter((d) => existsSync6(d));
|
|
3682
|
+
}
|
|
3683
|
+
function discoverSystemFaces(dirs = systemFontDirs(), depth = 3) {
|
|
3684
|
+
const seen = /* @__PURE__ */ new Set();
|
|
3685
|
+
const walk2 = (dirList, remaining) => {
|
|
3686
|
+
const faces = [];
|
|
3687
|
+
for (const dir of dirList) {
|
|
3688
|
+
let entries;
|
|
3689
|
+
try {
|
|
3690
|
+
entries = readdirSync2(dir, { withFileTypes: true });
|
|
3691
|
+
} catch {
|
|
3692
|
+
continue;
|
|
3693
|
+
}
|
|
3694
|
+
for (const e of entries) {
|
|
3695
|
+
const full = path10.join(dir, e.name);
|
|
3696
|
+
if (e.isDirectory()) {
|
|
3697
|
+
if (remaining > 1) faces.push(...walk2([full], remaining - 1));
|
|
3698
|
+
} else if (FONT_EXTENSIONS.has(path10.extname(e.name).toLowerCase())) {
|
|
3699
|
+
let key = full;
|
|
3700
|
+
try {
|
|
3701
|
+
key = realpathSync2(full);
|
|
3702
|
+
} catch {
|
|
3703
|
+
}
|
|
3704
|
+
if (seen.has(key)) continue;
|
|
3705
|
+
seen.add(key);
|
|
3706
|
+
faces.push(...facesInFile(full));
|
|
3707
|
+
}
|
|
3708
|
+
}
|
|
3709
|
+
}
|
|
3710
|
+
return faces;
|
|
3711
|
+
};
|
|
3712
|
+
return walk2(dirs, depth);
|
|
3713
|
+
}
|
|
3714
|
+
function systemFacesForFamily(family, dirs) {
|
|
3715
|
+
const want = normalize(family);
|
|
3716
|
+
return discoverSystemFaces(dirs).filter((f) => normalize(f.family) === want);
|
|
3717
|
+
}
|
|
3718
|
+
var WEIGHT_TOKENS, SFNT_VERSIONS, FONT_EXTENSIONS, normalize;
|
|
3719
|
+
var init_font_discovery = __esm({
|
|
3720
|
+
"packages/verify/src/font-discovery.ts"() {
|
|
3721
|
+
"use strict";
|
|
3722
|
+
init_font_collection();
|
|
3723
|
+
WEIGHT_TOKENS = [
|
|
3724
|
+
[/extra\s*light|ultra\s*light/i, 200],
|
|
3725
|
+
[/extra\s*bold|ultra\s*bold/i, 800],
|
|
3726
|
+
[/semi\s*bold|demi\s*bold|demi\b/i, 600],
|
|
3727
|
+
[/\bthin\b|\bhairline\b/i, 100],
|
|
3728
|
+
[/\blight\b/i, 300],
|
|
3729
|
+
[/\bmedium\b/i, 500],
|
|
3730
|
+
[/\bbold\b/i, 700],
|
|
3731
|
+
[/\bblack\b|\bheavy\b/i, 900],
|
|
3732
|
+
[/\bregular\b|\bnormal\b|\bbook\b|\broman\b/i, 400]
|
|
3733
|
+
];
|
|
3734
|
+
SFNT_VERSIONS = /* @__PURE__ */ new Set([
|
|
3735
|
+
65536,
|
|
3736
|
+
1330926671,
|
|
3737
|
+
1953658213
|
|
3738
|
+
/* true */
|
|
3739
|
+
]);
|
|
3740
|
+
FONT_EXTENSIONS = /* @__PURE__ */ new Set([".ttf", ".otf", ".ttc"]);
|
|
3741
|
+
normalize = (s) => s.toLowerCase().replace(/\s+/g, " ").trim();
|
|
3742
|
+
}
|
|
3743
|
+
});
|
|
3744
|
+
|
|
3745
|
+
// packages/verify/src/font-resolve.ts
|
|
3746
|
+
import { createHash } from "node:crypto";
|
|
3747
|
+
import { existsSync as existsSync7, mkdirSync as mkdirSync2, readFileSync as readFileSync4, writeFileSync as writeFileSync3 } from "node:fs";
|
|
3748
|
+
import os3 from "node:os";
|
|
3749
|
+
import path11 from "node:path";
|
|
3594
3750
|
function fontCacheDir() {
|
|
3595
3751
|
const env = process.env["TENDRIL_FONT_CACHE"];
|
|
3596
|
-
if (env !== void 0 && env !== "") return
|
|
3597
|
-
return
|
|
3752
|
+
if (env !== void 0 && env !== "") return path11.resolve(env);
|
|
3753
|
+
return path11.join(os3.homedir(), ".tendril", "fonts");
|
|
3598
3754
|
}
|
|
3599
3755
|
function normalizeFontLicense(value) {
|
|
3600
3756
|
return typeof value === "string" && FONT_LICENSES.includes(value) ? value : "unknown";
|
|
@@ -3659,29 +3815,29 @@ async function resolveFonts(family, weights, cacheDir = DEFAULT_FONT_CACHE) {
|
|
|
3659
3815
|
}
|
|
3660
3816
|
const bytes = new Uint8Array(await fileRes.arrayBuffer());
|
|
3661
3817
|
const sha256 = createHash("sha256").update(bytes).digest("hex");
|
|
3662
|
-
const file =
|
|
3818
|
+
const file = path11.join(cacheDir, `${family.toLowerCase().replace(/\s+/g, "-")}-${weight}.woff2`);
|
|
3663
3819
|
writeFileSync3(file, bytes);
|
|
3664
3820
|
resolved.push({ family, weight, source: url, sha256, file, license: meta.license, ...meta.family !== null ? { servedFamily: meta.family } : {} });
|
|
3665
3821
|
} catch (err) {
|
|
3666
3822
|
failures.push({ family, weight, reason: `download failed: ${err instanceof Error ? err.message : String(err)}` });
|
|
3667
3823
|
}
|
|
3668
3824
|
}
|
|
3669
|
-
const mPath =
|
|
3670
|
-
const prior =
|
|
3671
|
-
const portable2 = resolved.map((m) => ({ ...m, file:
|
|
3825
|
+
const mPath = path11.join(cacheDir, "manifest.json");
|
|
3826
|
+
const prior = existsSync7(mPath) ? JSON.parse(readFileSync4(mPath, "utf8")) : [];
|
|
3827
|
+
const portable2 = resolved.map((m) => ({ ...m, file: path11.basename(m.file) }));
|
|
3672
3828
|
const merged = [...prior.filter((p) => !resolved.some((m) => m.family === p.family && m.weight === p.weight)), ...portable2];
|
|
3673
3829
|
if (resolved.length > 0) writeFileSync3(mPath, `${JSON.stringify(merged, null, 2)}
|
|
3674
3830
|
`);
|
|
3675
3831
|
return { resolved, failures };
|
|
3676
3832
|
}
|
|
3677
|
-
function addLocalFont(family, weight, filePath, cacheDir = DEFAULT_FONT_CACHE, faceIndex) {
|
|
3678
|
-
const src =
|
|
3679
|
-
if (!
|
|
3680
|
-
const ext =
|
|
3833
|
+
function addLocalFont(family, weight, filePath, cacheDir = DEFAULT_FONT_CACHE, faceIndex, provenance = "local") {
|
|
3834
|
+
const src = path11.resolve(filePath);
|
|
3835
|
+
if (!existsSync7(src)) throw new Error(`font file not found: ${src}`);
|
|
3836
|
+
const ext = path11.extname(src).toLowerCase();
|
|
3681
3837
|
if (![".woff2", ".woff", ".ttf", ".otf", ".ttc"].includes(ext)) {
|
|
3682
3838
|
throw new Error(`unsupported font file "${ext}" \u2014 use .woff2 (preferred), .woff, .ttf, .otf or .ttc`);
|
|
3683
3839
|
}
|
|
3684
|
-
let bytes = new Uint8Array(
|
|
3840
|
+
let bytes = new Uint8Array(readFileSync4(src));
|
|
3685
3841
|
if (bytes.length === 0) throw new Error(`font file is empty: ${src}`);
|
|
3686
3842
|
let storedExt = ext;
|
|
3687
3843
|
if (isCollection(bytes)) {
|
|
@@ -3691,7 +3847,7 @@ function addLocalFont(family, weight, filePath, cacheDir = DEFAULT_FONT_CACHE, f
|
|
|
3691
3847
|
const all = listCollectionFaces(bytes);
|
|
3692
3848
|
const shown = (candidates.length > 0 ? candidates : all).map((f) => ` --face ${f.index} ${f.family ?? "(unnamed)"}${f.subfamily !== void 0 ? ` ${f.subfamily}` : ""}`).join("\n");
|
|
3693
3849
|
throw new Error(
|
|
3694
|
-
`${
|
|
3850
|
+
`${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>:
|
|
3695
3851
|
${shown}`
|
|
3696
3852
|
);
|
|
3697
3853
|
}
|
|
@@ -3700,20 +3856,20 @@ ${shown}`
|
|
|
3700
3856
|
}
|
|
3701
3857
|
mkdirSync2(cacheDir, { recursive: true });
|
|
3702
3858
|
const sha256 = createHash("sha256").update(bytes).digest("hex");
|
|
3703
|
-
const file =
|
|
3859
|
+
const file = path11.join(cacheDir, `${family.toLowerCase().replace(/\s+/g, "-")}-${weight}${storedExt}`);
|
|
3704
3860
|
writeFileSync3(file, bytes);
|
|
3705
|
-
const face = { family, weight, source:
|
|
3706
|
-
const mPath =
|
|
3707
|
-
const prior =
|
|
3708
|
-
const merged = [...prior.filter((p) => !(p.family === family && p.weight === weight)), { ...face, file:
|
|
3861
|
+
const face = { family, weight, source: `${provenance}:${path11.basename(src)}`, sha256, file, license: "unknown" };
|
|
3862
|
+
const mPath = path11.join(cacheDir, "manifest.json");
|
|
3863
|
+
const prior = existsSync7(mPath) ? JSON.parse(readFileSync4(mPath, "utf8")) : [];
|
|
3864
|
+
const merged = [...prior.filter((p) => !(p.family === family && p.weight === weight)), { ...face, file: path11.basename(file) }];
|
|
3709
3865
|
writeFileSync3(mPath, `${JSON.stringify(merged, null, 2)}
|
|
3710
3866
|
`);
|
|
3711
3867
|
return face;
|
|
3712
3868
|
}
|
|
3713
3869
|
function checkFontLock(lockPath, cacheDir = DEFAULT_FONT_CACHE) {
|
|
3714
|
-
const lock = JSON.parse(
|
|
3715
|
-
const mPath =
|
|
3716
|
-
const manifest =
|
|
3870
|
+
const lock = JSON.parse(readFileSync4(lockPath, "utf8"));
|
|
3871
|
+
const mPath = path11.join(cacheDir, "manifest.json");
|
|
3872
|
+
const manifest = existsSync7(mPath) ? JSON.parse(readFileSync4(mPath, "utf8")) : [];
|
|
3717
3873
|
return lock.map((l) => {
|
|
3718
3874
|
const m = manifest.find((x) => x.family === l.family && x.weight === l.weight);
|
|
3719
3875
|
if (m === void 0) return { family: l.family, weight: l.weight, status: "missing" };
|
|
@@ -3721,11 +3877,11 @@ function checkFontLock(lockPath, cacheDir = DEFAULT_FONT_CACHE) {
|
|
|
3721
3877
|
});
|
|
3722
3878
|
}
|
|
3723
3879
|
function resolvedFontFamilies(cacheDir = DEFAULT_FONT_CACHE) {
|
|
3724
|
-
const mPath =
|
|
3725
|
-
if (!
|
|
3880
|
+
const mPath = path11.join(cacheDir, "manifest.json");
|
|
3881
|
+
if (!existsSync7(mPath)) return [];
|
|
3726
3882
|
let entries;
|
|
3727
3883
|
try {
|
|
3728
|
-
entries = JSON.parse(
|
|
3884
|
+
entries = JSON.parse(readFileSync4(mPath, "utf8"));
|
|
3729
3885
|
} catch {
|
|
3730
3886
|
return [];
|
|
3731
3887
|
}
|
|
@@ -3739,8 +3895,8 @@ function resolvedFontFamilies(cacheDir = DEFAULT_FONT_CACHE) {
|
|
|
3739
3895
|
return [...byFamily.entries()].map(([family, w]) => ({ family, weights: [...w].sort((a, b) => a - b) }));
|
|
3740
3896
|
}
|
|
3741
3897
|
function requiredFontsManifest(families, cacheDir = DEFAULT_FONT_CACHE) {
|
|
3742
|
-
const mPath =
|
|
3743
|
-
const manifest =
|
|
3898
|
+
const mPath = path11.join(cacheDir, "manifest.json");
|
|
3899
|
+
const manifest = existsSync7(mPath) ? JSON.parse(readFileSync4(mPath, "utf8")) : [];
|
|
3744
3900
|
const wanted = new Set(families.map((f) => f.toLowerCase()));
|
|
3745
3901
|
return manifest.filter((f) => wanted.has(f.family.toLowerCase())).map((f) => ({
|
|
3746
3902
|
family: f.family,
|
|
@@ -3753,11 +3909,11 @@ function requiredFontsManifest(families, cacheDir = DEFAULT_FONT_CACHE) {
|
|
|
3753
3909
|
}));
|
|
3754
3910
|
}
|
|
3755
3911
|
function facesMissingLicense(cacheDir = DEFAULT_FONT_CACHE) {
|
|
3756
|
-
const mPath =
|
|
3757
|
-
if (!
|
|
3912
|
+
const mPath = path11.join(cacheDir, "manifest.json");
|
|
3913
|
+
if (!existsSync7(mPath)) return [];
|
|
3758
3914
|
let entries;
|
|
3759
3915
|
try {
|
|
3760
|
-
entries = JSON.parse(
|
|
3916
|
+
entries = JSON.parse(readFileSync4(mPath, "utf8"));
|
|
3761
3917
|
} catch {
|
|
3762
3918
|
return [];
|
|
3763
3919
|
}
|
|
@@ -3766,31 +3922,83 @@ function facesMissingLicense(cacheDir = DEFAULT_FONT_CACHE) {
|
|
|
3766
3922
|
).map((e) => ({ family: e.family, weight: e.weight, sha256: e.sha256 }));
|
|
3767
3923
|
}
|
|
3768
3924
|
function verifiedFontFamilies(cacheDir = DEFAULT_FONT_CACHE) {
|
|
3769
|
-
const mPath =
|
|
3770
|
-
if (!
|
|
3925
|
+
const mPath = path11.join(cacheDir, "manifest.json");
|
|
3926
|
+
if (!existsSync7(mPath)) return [];
|
|
3771
3927
|
let entries;
|
|
3772
3928
|
try {
|
|
3773
|
-
entries = JSON.parse(
|
|
3929
|
+
entries = JSON.parse(readFileSync4(mPath, "utf8"));
|
|
3774
3930
|
} catch {
|
|
3775
3931
|
return [];
|
|
3776
3932
|
}
|
|
3777
3933
|
const byFamily = /* @__PURE__ */ new Map();
|
|
3778
3934
|
for (const e of entries) {
|
|
3779
3935
|
if (typeof e.family !== "string" || typeof e.weight !== "number" || typeof e.file !== "string") continue;
|
|
3780
|
-
const file =
|
|
3781
|
-
if (!
|
|
3782
|
-
if (createHash("sha256").update(
|
|
3936
|
+
const file = path11.isAbsolute(e.file) && existsSync7(e.file) ? e.file : path11.resolve(cacheDir, path11.basename(e.file));
|
|
3937
|
+
if (!existsSync7(file)) continue;
|
|
3938
|
+
if (createHash("sha256").update(readFileSync4(file)).digest("hex") !== e.sha256) continue;
|
|
3783
3939
|
const set = byFamily.get(e.family) ?? /* @__PURE__ */ new Set();
|
|
3784
3940
|
set.add(e.weight);
|
|
3785
3941
|
byFamily.set(e.family, set);
|
|
3786
3942
|
}
|
|
3787
3943
|
return [...byFamily.entries()].map(([family, w]) => ({ family, weights: [...w].sort((a, b) => a - b) }));
|
|
3788
3944
|
}
|
|
3945
|
+
function addSystemFamily(family, opts = {}) {
|
|
3946
|
+
const refusal = systemFaceRefusal(family);
|
|
3947
|
+
if (refusal !== void 0) return { added: [], skipped: [], overwrote: [], refusal };
|
|
3948
|
+
const plainStyle = (sub) => weightFromSubfamily(sub) !== void 0 || /^(regular|plain|)$/i.test(sub.trim());
|
|
3949
|
+
const faces = [...systemFacesForFamily(family, opts.dirs)].sort((a, b) => {
|
|
3950
|
+
const pa = plainStyle(a.subfamily) ? 0 : 1;
|
|
3951
|
+
const pb = plainStyle(b.subfamily) ? 0 : 1;
|
|
3952
|
+
if (pa !== pb) return pa - pb;
|
|
3953
|
+
return a.subfamily.localeCompare(b.subfamily) || a.file.localeCompare(b.file) || (a.faceIndex ?? 0) - (b.faceIndex ?? 0);
|
|
3954
|
+
});
|
|
3955
|
+
const added = [];
|
|
3956
|
+
const skipped = [];
|
|
3957
|
+
const overwrote = [];
|
|
3958
|
+
const cacheDir = opts.cacheDir ?? DEFAULT_FONT_CACHE;
|
|
3959
|
+
const manifestFile = path11.join(cacheDir, "manifest.json");
|
|
3960
|
+
const prior = existsSync7(manifestFile) ? JSON.parse(readFileSync4(manifestFile, "utf8")) : [];
|
|
3961
|
+
const taken = /* @__PURE__ */ new Set();
|
|
3962
|
+
for (const face of faces) {
|
|
3963
|
+
const skip = (reason) => skipped.push({ subfamily: face.subfamily, weight: face.weight, reason });
|
|
3964
|
+
if (opts.weights !== void 0 && !opts.weights.includes(face.weight)) continue;
|
|
3965
|
+
if (face.italic) {
|
|
3966
|
+
skip("italic \u2014 the cache cannot represent italic faces end to end yet; the upright face serves the family");
|
|
3967
|
+
continue;
|
|
3968
|
+
}
|
|
3969
|
+
if (face.variable) {
|
|
3970
|
+
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");
|
|
3971
|
+
continue;
|
|
3972
|
+
}
|
|
3973
|
+
if (/condensed|narrow|compressed|expanded|extended/i.test(face.subfamily)) {
|
|
3974
|
+
skip("width variant \u2014 a Condensed/Expanded face is a different design, not a weight of this family");
|
|
3975
|
+
continue;
|
|
3976
|
+
}
|
|
3977
|
+
if (taken.has(face.weight)) {
|
|
3978
|
+
skip(`another face already provided weight ${face.weight}`);
|
|
3979
|
+
continue;
|
|
3980
|
+
}
|
|
3981
|
+
taken.add(face.weight);
|
|
3982
|
+
const existing = prior.find((p) => p.family.toLowerCase() === face.family.toLowerCase() && p.weight === face.weight);
|
|
3983
|
+
if (existing !== void 0 && !existing.source.startsWith("system:")) {
|
|
3984
|
+
overwrote.push({
|
|
3985
|
+
family: face.family,
|
|
3986
|
+
weight: face.weight,
|
|
3987
|
+
priorSource: existing.source,
|
|
3988
|
+
priorSha256: existing.sha256,
|
|
3989
|
+
priorLicense: normalizeFontLicense(existing.license)
|
|
3990
|
+
});
|
|
3991
|
+
}
|
|
3992
|
+
added.push({ ...addLocalFont(face.family, face.weight, face.file, cacheDir, face.faceIndex, "system"), subfamily: face.subfamily || "Regular" });
|
|
3993
|
+
}
|
|
3994
|
+
return { added, skipped, overwrote };
|
|
3995
|
+
}
|
|
3789
3996
|
var DEFAULT_FONT_CACHE, UA, FONT_LICENSES, GOOGLE_LICENSE_IDS, GOOGLE_FONT_FILE_PREFIX;
|
|
3790
3997
|
var init_font_resolve = __esm({
|
|
3791
3998
|
"packages/verify/src/font-resolve.ts"() {
|
|
3792
3999
|
"use strict";
|
|
3793
4000
|
init_font_collection();
|
|
4001
|
+
init_font_discovery();
|
|
3794
4002
|
DEFAULT_FONT_CACHE = fontCacheDir();
|
|
3795
4003
|
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";
|
|
3796
4004
|
FONT_LICENSES = ["OFL-1.1", "Apache-2.0", "UFL-1.0", "proprietary", "unknown"];
|
|
@@ -3806,17 +4014,17 @@ var init_font_resolve = __esm({
|
|
|
3806
4014
|
|
|
3807
4015
|
// packages/verify/src/font-faces.ts
|
|
3808
4016
|
import { createHash as createHash2 } from "node:crypto";
|
|
3809
|
-
import { existsSync as
|
|
3810
|
-
import
|
|
4017
|
+
import { existsSync as existsSync8, readFileSync as readFileSync5 } from "node:fs";
|
|
4018
|
+
import path12 from "node:path";
|
|
3811
4019
|
function injectedGroups(manifestPath2) {
|
|
3812
|
-
if (!
|
|
3813
|
-
const claimed = JSON.parse(
|
|
3814
|
-
const resolveFile = (f) =>
|
|
4020
|
+
if (!existsSync8(manifestPath2)) return { groups: [], shared: false };
|
|
4021
|
+
const claimed = JSON.parse(readFileSync5(manifestPath2, "utf8"));
|
|
4022
|
+
const resolveFile = (f) => path12.isAbsolute(f) && existsSync8(f) ? f : path12.resolve(path12.dirname(manifestPath2), path12.basename(f));
|
|
3815
4023
|
const byFile = /* @__PURE__ */ new Map();
|
|
3816
4024
|
for (const f of claimed) {
|
|
3817
4025
|
const file = resolveFile(f.file);
|
|
3818
|
-
if (!
|
|
3819
|
-
if (createHash2("sha256").update(
|
|
4026
|
+
if (!existsSync8(file)) continue;
|
|
4027
|
+
if (createHash2("sha256").update(readFileSync5(file)).digest("hex") !== f.sha256) continue;
|
|
3820
4028
|
const k = `${f.family}:${f.file}`;
|
|
3821
4029
|
const e = byFile.get(k) ?? { family: f.family, weights: [], file };
|
|
3822
4030
|
e.weights.push(f.weight);
|
|
@@ -3825,14 +4033,14 @@ function injectedGroups(manifestPath2) {
|
|
|
3825
4033
|
const groups = [...byFile.values()];
|
|
3826
4034
|
return { groups, shared: new Set(groups.map((e) => e.file)).size < groups.length };
|
|
3827
4035
|
}
|
|
3828
|
-
function fontFaceCss(manifestPath2 =
|
|
4036
|
+
function fontFaceCss(manifestPath2 = path12.join(fontCacheDir(), "manifest.json")) {
|
|
3829
4037
|
const { groups, shared } = injectedGroups(manifestPath2);
|
|
3830
4038
|
return groups.map((e) => {
|
|
3831
4039
|
const weight = shared || e.weights.length > 1 ? `${SPAN[0]} ${SPAN[1]}` : String(e.weights[0]);
|
|
3832
|
-
return `@font-face { font-family: '${e.family}'; font-weight: ${weight}; font-style: normal; src: url(data:font/woff2;base64,${
|
|
4040
|
+
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'); }`;
|
|
3833
4041
|
}).join("\n");
|
|
3834
4042
|
}
|
|
3835
|
-
function injectedFamilyWeights(manifestPath2 =
|
|
4043
|
+
function injectedFamilyWeights(manifestPath2 = path12.join(fontCacheDir(), "manifest.json")) {
|
|
3836
4044
|
const { groups, shared } = injectedGroups(manifestPath2);
|
|
3837
4045
|
const out = /* @__PURE__ */ new Map();
|
|
3838
4046
|
for (const g of groups) {
|
|
@@ -3857,17 +4065,17 @@ var init_font_faces = __esm({
|
|
|
3857
4065
|
});
|
|
3858
4066
|
|
|
3859
4067
|
// packages/verify/src/admission.ts
|
|
3860
|
-
import { readFileSync as
|
|
3861
|
-
import
|
|
4068
|
+
import { readFileSync as readFileSync6, readdirSync as readdirSync3, existsSync as existsSync9, writeFileSync as writeFileSync4 } from "node:fs";
|
|
4069
|
+
import path13 from "node:path";
|
|
3862
4070
|
import { build as build2 } from "esbuild";
|
|
3863
4071
|
import postcss from "postcss";
|
|
3864
4072
|
import tailwindcss from "tailwindcss";
|
|
3865
4073
|
import { chromium as chromium2 } from "playwright-core";
|
|
3866
4074
|
function fontWeightsByFamily() {
|
|
3867
|
-
const mPath =
|
|
4075
|
+
const mPath = path13.join(fontCacheDir(), "manifest.json");
|
|
3868
4076
|
const out = /* @__PURE__ */ new Map();
|
|
3869
|
-
if (!
|
|
3870
|
-
for (const f of JSON.parse(
|
|
4077
|
+
if (!existsSync9(mPath)) return out;
|
|
4078
|
+
for (const f of JSON.parse(readFileSync6(mPath, "utf8")))
|
|
3871
4079
|
out.set(f.family, [...out.get(f.family) ?? [], f.weight]);
|
|
3872
4080
|
return out;
|
|
3873
4081
|
}
|
|
@@ -3887,7 +4095,7 @@ var init_admission = __esm({
|
|
|
3887
4095
|
});
|
|
3888
4096
|
|
|
3889
4097
|
// packages/verify/src/tasks.ts
|
|
3890
|
-
import
|
|
4098
|
+
import path14 from "node:path";
|
|
3891
4099
|
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;
|
|
3892
4100
|
var init_tasks = __esm({
|
|
3893
4101
|
"packages/verify/src/tasks.ts"() {
|
|
@@ -4047,7 +4255,7 @@ BEHAVIORAL CONTRACT (machine-verified, gating): the primary action and close con
|
|
|
4047
4255
|
];
|
|
4048
4256
|
TASKS = {
|
|
4049
4257
|
calendar: {
|
|
4050
|
-
set:
|
|
4258
|
+
set: path14.join(REPO_ROOT, "examples/recordings/shadcn-poc-calendar"),
|
|
4051
4259
|
entry: "Calendar.tsx",
|
|
4052
4260
|
configs: CALENDAR_CONFIGS,
|
|
4053
4261
|
systemApi: CALENDAR_API,
|
|
@@ -4055,7 +4263,7 @@ BEHAVIORAL CONTRACT (machine-verified, gating): the primary action and close con
|
|
|
4055
4263
|
prelude: { controls: ['[data-tendril-part="day"]'], textInputs: [] }
|
|
4056
4264
|
},
|
|
4057
4265
|
"shadcn-button": {
|
|
4058
|
-
set:
|
|
4266
|
+
set: path14.join(REPO_ROOT, "examples/recordings/shadcn-poc-button"),
|
|
4059
4267
|
entry: "Button.tsx",
|
|
4060
4268
|
configs: BUTTON_CONFIGS,
|
|
4061
4269
|
systemApi: BUTTON_API,
|
|
@@ -4063,7 +4271,7 @@ BEHAVIORAL CONTRACT (machine-verified, gating): the primary action and close con
|
|
|
4063
4271
|
prelude: { controls: ["> *"], textInputs: [] }
|
|
4064
4272
|
},
|
|
4065
4273
|
combobox: {
|
|
4066
|
-
set:
|
|
4274
|
+
set: path14.join(REPO_ROOT, "examples/recordings/carbon-poc-combobox"),
|
|
4067
4275
|
entry: "ComboBox.tsx",
|
|
4068
4276
|
configs: COMBO_CONFIGS,
|
|
4069
4277
|
systemApi: COMBO_API,
|
|
@@ -4071,7 +4279,7 @@ BEHAVIORAL CONTRACT (machine-verified, gating): the primary action and close con
|
|
|
4071
4279
|
prelude: { controls: ['[role="option"]'], textInputs: ["input"], popover: { selector: '[role="listbox"]', trigger: "input" } }
|
|
4072
4280
|
},
|
|
4073
4281
|
modal: {
|
|
4074
|
-
set:
|
|
4282
|
+
set: path14.join(REPO_ROOT, "examples/recordings/carbon-poc-modal"),
|
|
4075
4283
|
entry: "Modal.tsx",
|
|
4076
4284
|
configs: MODAL_CONFIGS,
|
|
4077
4285
|
systemApi: MODAL_API,
|
|
@@ -4089,8 +4297,8 @@ __export(behavior_exports, {
|
|
|
4089
4297
|
compileMount: () => compileMount,
|
|
4090
4298
|
recordingIsDark: () => recordingIsDark
|
|
4091
4299
|
});
|
|
4092
|
-
import { existsSync as
|
|
4093
|
-
import
|
|
4300
|
+
import { existsSync as existsSync10, readFileSync as readFileSync7 } from "node:fs";
|
|
4301
|
+
import path15 from "node:path";
|
|
4094
4302
|
import { build as build3 } from "esbuild";
|
|
4095
4303
|
import { chromium as chromium3 } from "playwright-core";
|
|
4096
4304
|
import { PNG as PNG2 } from "pngjs";
|
|
@@ -4099,12 +4307,12 @@ function getFontFaces() {
|
|
|
4099
4307
|
return _fontFaces;
|
|
4100
4308
|
}
|
|
4101
4309
|
async function compileMount(task, bundleDir) {
|
|
4102
|
-
const entryTsx =
|
|
4103
|
-
if (!
|
|
4310
|
+
const entryTsx = path15.join(bundleDir, task.entry);
|
|
4311
|
+
if (!existsSync10(entryTsx)) return { error: `${task.entry} missing` };
|
|
4104
4312
|
const mountSrc = `
|
|
4105
4313
|
import { createElement } from "react";
|
|
4106
4314
|
import { createRoot } from "react-dom/client";
|
|
4107
|
-
import * as B from ${JSON.stringify(
|
|
4315
|
+
import * as B from ${JSON.stringify(path15.resolve(entryTsx))};
|
|
4108
4316
|
const cfg = (window as unknown as { __cfg: { component: string; props: Record<string, unknown>; spyProps?: string[] } }).__cfg;
|
|
4109
4317
|
const C = (B as Record<string, unknown>)[cfg.component] as Parameters<typeof createElement>[0] | undefined;
|
|
4110
4318
|
// Callbacks cannot ride the JSON config: specs NAME spy props and the
|
|
@@ -4314,6 +4522,19 @@ async function runSteps(page, spec, renderPose) {
|
|
|
4314
4522
|
// a data-* or a name= is a place to park a string, which is
|
|
4315
4523
|
// the dodge this check exists to catch.
|
|
4316
4524
|
const PERCEIVED = ['placeholder', 'alt', 'aria-label', 'title', 'value'];
|
|
4525
|
+
// 'value' counts ONLY where the platform RENDERS it as the
|
|
4526
|
+
// control's text: text-entry inputs, textarea, and the
|
|
4527
|
+
// input-typed buttons whose value IS their label. Nearly
|
|
4528
|
+
// every element carries a value PROPERTY \u2014 a visible
|
|
4529
|
+
// checkbox or <button> with value={sentinel} would satisfy
|
|
4530
|
+
// an unrestricted check while no user ever perceives the
|
|
4531
|
+
// string, which reopens the exact dodge this check exists
|
|
4532
|
+
// to close (post-release review, 2026-08-14). password is
|
|
4533
|
+
// excluded because its value renders as dots, not as the
|
|
4534
|
+
// string. Fail-closed allowlist, not a blocklist.
|
|
4535
|
+
const VALUE_RENDERS = ['text', 'search', 'email', 'url', 'tel', 'number', 'submit', 'reset', 'button'];
|
|
4536
|
+
const valueRenders = (el) =>
|
|
4537
|
+
el instanceof HTMLTextAreaElement || (el instanceof HTMLInputElement && VALUE_RENDERS.includes(el.type));
|
|
4317
4538
|
const visible = (el) => {
|
|
4318
4539
|
const r = el.getBoundingClientRect();
|
|
4319
4540
|
const cs = getComputedStyle(el);
|
|
@@ -4326,7 +4547,8 @@ async function runSteps(page, spec, renderPose) {
|
|
|
4326
4547
|
PERCEIVED.some((a) => {
|
|
4327
4548
|
// The LIVE value for form controls: React's defaultValue
|
|
4328
4549
|
// sets the property, and the attribute may never appear.
|
|
4329
|
-
|
|
4550
|
+
if (a === 'value' && !valueRenders(el)) return false;
|
|
4551
|
+
const v = a === 'value' ? el.value : el.getAttribute(a);
|
|
4330
4552
|
return typeof v === 'string' && v.includes(needle);
|
|
4331
4553
|
}),
|
|
4332
4554
|
);
|
|
@@ -4371,10 +4593,10 @@ async function runSteps(page, spec, renderPose) {
|
|
|
4371
4593
|
function recordingIsDark(task) {
|
|
4372
4594
|
const rep = task.configs[0]?.rep;
|
|
4373
4595
|
if (rep === void 0) return false;
|
|
4374
|
-
const f =
|
|
4375
|
-
if (!
|
|
4596
|
+
const f = path15.join(task.set, rep, "get_screenshot.json");
|
|
4597
|
+
if (!existsSync10(f)) return false;
|
|
4376
4598
|
try {
|
|
4377
|
-
const env = JSON.parse(
|
|
4599
|
+
const env = JSON.parse(readFileSync7(f, "utf8")).content.find((c) => c.type === "image");
|
|
4378
4600
|
if (env?.data === void 0) return false;
|
|
4379
4601
|
const png = PNG2.sync.read(Buffer.from(env.data, "base64"));
|
|
4380
4602
|
let sum = 0;
|
|
@@ -4446,7 +4668,7 @@ async function checkBehaviors(task, bundleDir, opts = {}) {
|
|
|
4446
4668
|
const deadlineMs = timeoutMs + 1e4;
|
|
4447
4669
|
const js = await compileMount(task, bundleDir);
|
|
4448
4670
|
if (typeof js !== "string") return task.behaviors.map((b) => ({ id: b.id, pass: false, detail: js.error }));
|
|
4449
|
-
const css = ["tokens.css", "styles.css"].map((f) =>
|
|
4671
|
+
const css = ["tokens.css", "styles.css"].map((f) => path15.join(bundleDir, f)).filter((f) => existsSync10(f)).map((f) => readFileSync7(f, "utf8")).join("\n");
|
|
4450
4672
|
const server = await chromium3.launchServer({ executablePath: resolveChrome(), headless: true, args: mountArgs() });
|
|
4451
4673
|
const browser = await chromium3.connect(server.wsEndpoint());
|
|
4452
4674
|
const results = [];
|
|
@@ -4589,8 +4811,8 @@ var init_behavior = __esm({
|
|
|
4589
4811
|
});
|
|
4590
4812
|
|
|
4591
4813
|
// packages/verify/src/bundle-quality.ts
|
|
4592
|
-
import { readFileSync as
|
|
4593
|
-
import
|
|
4814
|
+
import { readFileSync as readFileSync8, rmSync as rmSync2, writeFileSync as writeFileSync5, existsSync as existsSync11 } from "node:fs";
|
|
4815
|
+
import path16 from "node:path";
|
|
4594
4816
|
function definedVars(tokensCss) {
|
|
4595
4817
|
if (tokensCss === void 0) return void 0;
|
|
4596
4818
|
const names = /* @__PURE__ */ new Set();
|
|
@@ -4599,19 +4821,19 @@ function definedVars(tokensCss) {
|
|
|
4599
4821
|
}
|
|
4600
4822
|
function recordedTokenMapState(setDir, reps) {
|
|
4601
4823
|
const readMap = (file) => {
|
|
4602
|
-
if (!
|
|
4824
|
+
if (!existsSync11(file)) return void 0;
|
|
4603
4825
|
try {
|
|
4604
|
-
const parsed = JSON.parse(envelopeFirstTextPart(JSON.parse(
|
|
4826
|
+
const parsed = JSON.parse(envelopeFirstTextPart(JSON.parse(readFileSync8(file, "utf8"))) || "{}");
|
|
4605
4827
|
return parsed !== null && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : {};
|
|
4606
4828
|
} catch {
|
|
4607
4829
|
return {};
|
|
4608
4830
|
}
|
|
4609
4831
|
};
|
|
4610
|
-
const setLevel = readMap(
|
|
4832
|
+
const setLevel = readMap(path16.join(setDir, "get_variable_defs.json"));
|
|
4611
4833
|
if (setLevel !== void 0) return Object.keys(setLevel).length === 0 ? "empty" : "populated";
|
|
4612
4834
|
let recorded = false;
|
|
4613
4835
|
for (const rep of reps) {
|
|
4614
|
-
const m = readMap(
|
|
4836
|
+
const m = readMap(path16.join(setDir, rep, "get_variable_defs.json"));
|
|
4615
4837
|
if (m === void 0) continue;
|
|
4616
4838
|
recorded = true;
|
|
4617
4839
|
if (Object.keys(m).length > 0) return "populated";
|
|
@@ -4676,11 +4898,11 @@ function fontStackFindings(sheets, coverage) {
|
|
|
4676
4898
|
}
|
|
4677
4899
|
async function checkBundleQuality(bundleDir, entry, set, fontManifest) {
|
|
4678
4900
|
const findings = [];
|
|
4679
|
-
const entryPath =
|
|
4680
|
-
const cssPath =
|
|
4681
|
-
const tokensPath =
|
|
4682
|
-
const css =
|
|
4683
|
-
const tokensCss =
|
|
4901
|
+
const entryPath = path16.join(bundleDir, entry);
|
|
4902
|
+
const cssPath = path16.join(bundleDir, "styles.css");
|
|
4903
|
+
const tokensPath = path16.join(bundleDir, "tokens.css");
|
|
4904
|
+
const css = existsSync11(cssPath) ? readFileSync8(cssPath, "utf8") : "";
|
|
4905
|
+
const tokensCss = existsSync11(tokensPath) ? readFileSync8(tokensPath, "utf8") : void 0;
|
|
4684
4906
|
findings.push(
|
|
4685
4907
|
...fontStackFindings(
|
|
4686
4908
|
[
|
|
@@ -4690,11 +4912,11 @@ async function checkBundleQuality(bundleDir, entry, set, fontManifest) {
|
|
|
4690
4912
|
injectedFamilyWeights(fontManifest)
|
|
4691
4913
|
)
|
|
4692
4914
|
);
|
|
4693
|
-
if (
|
|
4915
|
+
if (existsSync11(entryPath)) {
|
|
4694
4916
|
const workDir = newScratchDir("quality");
|
|
4695
4917
|
try {
|
|
4696
|
-
const tsxPath =
|
|
4697
|
-
writeFileSync5(tsxPath,
|
|
4918
|
+
const tsxPath = path16.join(workDir, entry);
|
|
4919
|
+
writeFileSync5(tsxPath, readFileSync8(entryPath, "utf8"));
|
|
4698
4920
|
for (const d of runTscStrict([tsxPath]).diagnostics) {
|
|
4699
4921
|
findings.push({ kind: "tsc", file: entry, ...d.line === void 0 ? {} : { line: d.line }, message: `TS${d.code}: ${d.message}` });
|
|
4700
4922
|
}
|
|
@@ -4769,8 +4991,8 @@ var init_effect_geometry = __esm({
|
|
|
4769
4991
|
});
|
|
4770
4992
|
|
|
4771
4993
|
// packages/verify/src/bundle-score.ts
|
|
4772
|
-
import { existsSync as
|
|
4773
|
-
import
|
|
4994
|
+
import { existsSync as existsSync12, mkdirSync as mkdirSync3, readFileSync as readFileSync9, writeFileSync as writeFileSync6 } from "node:fs";
|
|
4995
|
+
import path17 from "node:path";
|
|
4774
4996
|
import { build as build4 } from "esbuild";
|
|
4775
4997
|
import { chromium as chromium4 } from "playwright-core";
|
|
4776
4998
|
function getFontFaces2() {
|
|
@@ -4824,7 +5046,7 @@ function chooseSeat(cropAt, ref, padW, padH, backdrop, origin, extents) {
|
|
|
4824
5046
|
}
|
|
4825
5047
|
function metadataRoot(set, rep) {
|
|
4826
5048
|
try {
|
|
4827
|
-
const text = JSON.parse(
|
|
5049
|
+
const text = JSON.parse(readFileSync9(path17.join(set, rep, "get_metadata.json"), "utf8")).content.map((c) => c.text ?? "").join("\n");
|
|
4828
5050
|
return parseMetadataStructure(text);
|
|
4829
5051
|
} catch {
|
|
4830
5052
|
return void 0;
|
|
@@ -4879,19 +5101,19 @@ function smallSemanticNodes(set, rep, maxArea = 1024) {
|
|
|
4879
5101
|
});
|
|
4880
5102
|
}
|
|
4881
5103
|
function repMeta(set, rep) {
|
|
4882
|
-
const text = JSON.parse(
|
|
5104
|
+
const text = JSON.parse(readFileSync9(path17.join(set, rep, "get_metadata.json"), "utf8")).content.map((c) => c.text ?? "").join("\n");
|
|
4883
5105
|
const root = parseMetadataStructure(text);
|
|
4884
5106
|
return { w: Math.round(root.width ?? 100), h: Math.round(root.height ?? 40) };
|
|
4885
5107
|
}
|
|
4886
5108
|
function repRef(set, rep) {
|
|
4887
|
-
const env = JSON.parse(
|
|
5109
|
+
const env = JSON.parse(readFileSync9(path17.join(set, rep, "get_screenshot.json"), "utf8")).content.find((c) => c.type === "image");
|
|
4888
5110
|
return Uint8Array.from(Buffer.from(env?.data ?? "", "base64"));
|
|
4889
5111
|
}
|
|
4890
5112
|
function repEffectExtents(set, rep) {
|
|
4891
|
-
const file =
|
|
4892
|
-
if (!
|
|
5113
|
+
const file = path17.join(set, rep, "get_design_context.json");
|
|
5114
|
+
if (!existsSync12(file)) return void 0;
|
|
4893
5115
|
try {
|
|
4894
|
-
const text = JSON.parse(
|
|
5116
|
+
const text = JSON.parse(readFileSync9(file, "utf8")).content.map((c) => c.text ?? "").join("\n");
|
|
4895
5117
|
const extents = shadowExtents(text);
|
|
4896
5118
|
return extents.top + extents.right + extents.bottom + extents.left > 0 ? extents : void 0;
|
|
4897
5119
|
} catch {
|
|
@@ -4902,13 +5124,13 @@ async function scoreBundleForTask(task, bundleDir, bar = BAR, opts = {}) {
|
|
|
4902
5124
|
if (opts.evidenceDir !== void 0) mkdirSync3(opts.evidenceDir, { recursive: true });
|
|
4903
5125
|
const CONFIGS2 = task.configs;
|
|
4904
5126
|
const timeoutMs = opts.mountDeadlineMs ?? ENGINE_MOUNT_DEADLINE_MS;
|
|
4905
|
-
const entryTsx =
|
|
4906
|
-
if (!
|
|
4907
|
-
const css = ["tokens.css", "styles.css"].map((f) =>
|
|
5127
|
+
const entryTsx = path17.join(bundleDir, task.entry);
|
|
5128
|
+
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` }));
|
|
5129
|
+
const css = ["tokens.css", "styles.css"].map((f) => path17.join(bundleDir, f)).filter((f) => existsSync12(f)).map((f) => readFileSync9(f, "utf8")).join("\n");
|
|
4908
5130
|
const mountSrc = `
|
|
4909
5131
|
import { createElement } from "react";
|
|
4910
5132
|
import { createRoot } from "react-dom/client";
|
|
4911
|
-
import * as B from ${JSON.stringify(
|
|
5133
|
+
import * as B from ${JSON.stringify(path17.resolve(entryTsx))};
|
|
4912
5134
|
const cfg = (window as unknown as { __cfg: { component: string; props: Record<string, unknown> } }).__cfg;
|
|
4913
5135
|
const C = (B as Record<string, unknown>)[cfg.component] as Parameters<typeof createElement>[0] | undefined;
|
|
4914
5136
|
const root = document.getElementById("root");
|
|
@@ -5003,12 +5225,12 @@ body{margin:0;background:rgb(${backdrop.join(",")})}
|
|
|
5003
5225
|
return name === void 0 ? c : { ...c, name };
|
|
5004
5226
|
});
|
|
5005
5227
|
if (opts.evidenceDir !== void 0) {
|
|
5006
|
-
writeFileSync6(
|
|
5007
|
-
writeFileSync6(
|
|
5008
|
-
writeFileSync6(
|
|
5228
|
+
writeFileSync6(path17.join(opts.evidenceDir, `${cfg.rep}-render.png`), scored);
|
|
5229
|
+
writeFileSync6(path17.join(opts.evidenceDir, `${cfg.rep}-ref.png`), ref);
|
|
5230
|
+
writeFileSync6(path17.join(opts.evidenceDir, `${cfg.rep}-diff.png`), diffPng(scored, ref));
|
|
5009
5231
|
for (const [i, c] of absent.slice(0, 3).entries()) {
|
|
5010
|
-
writeFileSync6(
|
|
5011
|
-
writeFileSync6(
|
|
5232
|
+
writeFileSync6(path17.join(opts.evidenceDir, `${cfg.rep}-absent-${i}-ref.png`), zoomCrop(ref, c));
|
|
5233
|
+
writeFileSync6(path17.join(opts.evidenceDir, `${cfg.rep}-absent-${i}-render.png`), zoomCrop(scored, c));
|
|
5012
5234
|
}
|
|
5013
5235
|
}
|
|
5014
5236
|
return {
|
|
@@ -5116,15 +5338,15 @@ var init_prelude = __esm({
|
|
|
5116
5338
|
});
|
|
5117
5339
|
|
|
5118
5340
|
// packages/verify/src/parity.ts
|
|
5119
|
-
import { existsSync as
|
|
5120
|
-
import
|
|
5341
|
+
import { existsSync as existsSync13, readFileSync as readFileSync10 } from "node:fs";
|
|
5342
|
+
import path18 from "node:path";
|
|
5121
5343
|
import { chromium as chromium6 } from "playwright-core";
|
|
5122
5344
|
function getFontFaces3() {
|
|
5123
5345
|
_fontFaces3 ??= fontFaceCss();
|
|
5124
5346
|
return _fontFaces3;
|
|
5125
5347
|
}
|
|
5126
|
-
function hoverForcedConfigs(
|
|
5127
|
-
return
|
|
5348
|
+
function hoverForcedConfigs(authority) {
|
|
5349
|
+
return authority.filter((c) => {
|
|
5128
5350
|
const forced = c.props["data-tendril-state"];
|
|
5129
5351
|
return typeof forced === "string" && forced.split(/\s+/).includes("hover");
|
|
5130
5352
|
});
|
|
@@ -5139,27 +5361,37 @@ function withoutHoverToken(props) {
|
|
|
5139
5361
|
}
|
|
5140
5362
|
return rest;
|
|
5141
5363
|
}
|
|
5142
|
-
async function checkHoverParity(task, bundleDir, opts = {}) {
|
|
5143
|
-
const configs = hoverForcedConfigs(
|
|
5364
|
+
async function checkHoverParity(task, bundleDir, authority, opts = {}) {
|
|
5365
|
+
const configs = hoverForcedConfigs(authority);
|
|
5144
5366
|
if (configs.length === 0) return [];
|
|
5145
5367
|
const timeoutMs = opts.mountDeadlineMs ?? ENGINE_MOUNT_DEADLINE_MS;
|
|
5146
5368
|
const deadlineMs = timeoutMs + 1e4;
|
|
5147
5369
|
const js = await compileMount(task, bundleDir);
|
|
5148
5370
|
if (typeof js !== "string") return configs.map((c) => ({ id: `parity:${c.rep}`, pass: false, detail: js.error }));
|
|
5149
|
-
const css = ["tokens.css", "styles.css"].map((f) =>
|
|
5371
|
+
const css = ["tokens.css", "styles.css"].map((f) => path18.join(bundleDir, f)).filter((f) => existsSync13(f)).map((f) => readFileSync10(f, "utf8")).join("\n");
|
|
5150
5372
|
const server = await chromium6.launchServer({ executablePath: resolveChrome(), headless: true, args: mountArgs() });
|
|
5151
5373
|
const browser = await chromium6.connect(server.wsEndpoint());
|
|
5152
5374
|
const results = [];
|
|
5153
5375
|
try {
|
|
5154
5376
|
for (const cfg of configs) {
|
|
5155
|
-
const
|
|
5377
|
+
const box = (() => {
|
|
5378
|
+
try {
|
|
5379
|
+
return repMeta(task.set, cfg.rep);
|
|
5380
|
+
} catch {
|
|
5381
|
+
return void 0;
|
|
5382
|
+
}
|
|
5383
|
+
})();
|
|
5384
|
+
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}
|
|
5385
|
+
#root > *{min-width:${box.w}px;min-height:${box.h}px}`;
|
|
5386
|
+
const viewport = box === void 0 ? { width: 900, height: 700 } : { width: box.w + 48, height: box.h + 48 };
|
|
5387
|
+
const shoot = async (component, props, realHover) => {
|
|
5156
5388
|
const html = `<!doctype html><html><head><meta charset="utf-8"><style>
|
|
5157
5389
|
${getFontFaces3()}
|
|
5158
5390
|
${css}
|
|
5159
5391
|
body{margin:0;padding:20px}
|
|
5160
|
-
|
|
5161
|
-
</style></head><body><div id="root"></div><script>window.__cfg=${JSON.stringify({ component
|
|
5162
|
-
const page = await browser.newPage({ viewport
|
|
5392
|
+
${rootCss}
|
|
5393
|
+
</style></head><body><div id="root"></div><script>window.__cfg=${JSON.stringify({ component, props })}</script><script>${js}</script></body></html>`;
|
|
5394
|
+
const page = await browser.newPage({ viewport });
|
|
5163
5395
|
try {
|
|
5164
5396
|
page.setDefaultTimeout(timeoutMs);
|
|
5165
5397
|
await page.route("**/*", (route) => route.request().url().startsWith("data:") ? route.continue() : route.abort());
|
|
@@ -5173,15 +5405,30 @@ body{margin:0;padding:20px}
|
|
|
5173
5405
|
} else {
|
|
5174
5406
|
await page.waitForTimeout(400);
|
|
5175
5407
|
}
|
|
5176
|
-
return await page.screenshot({ clip: { x: 0, y: 0, width:
|
|
5408
|
+
return await page.screenshot({ clip: { x: 0, y: 0, width: viewport.width, height: viewport.height } });
|
|
5177
5409
|
} finally {
|
|
5178
5410
|
await page.close();
|
|
5179
5411
|
}
|
|
5180
5412
|
};
|
|
5181
5413
|
const work = (async () => {
|
|
5182
|
-
const
|
|
5414
|
+
const adapter = task.configs.find((c) => c.rep === cfg.rep);
|
|
5415
|
+
if (adapter === void 0) {
|
|
5416
|
+
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" };
|
|
5417
|
+
}
|
|
5418
|
+
const forcedShot = await shoot(adapter.component, cfg.props, false);
|
|
5183
5419
|
if (!Buffer.isBuffer(forcedShot)) return { id: `parity:${cfg.rep}`, pass: false, detail: `forced mount: ${forcedShot.error}` };
|
|
5184
|
-
|
|
5420
|
+
if (JSON.stringify(adapter.props) !== JSON.stringify(cfg.props)) {
|
|
5421
|
+
const adapterShot = await shoot(adapter.component, adapter.props, false);
|
|
5422
|
+
if (!Buffer.isBuffer(adapterShot)) return { id: `parity:${cfg.rep}`, pass: false, detail: `adapter mount: ${adapterShot.error}` };
|
|
5423
|
+
if (Buffer.compare(adapterShot, forcedShot) !== 0) {
|
|
5424
|
+
return {
|
|
5425
|
+
id: `parity:${cfg.rep}`,
|
|
5426
|
+
pass: false,
|
|
5427
|
+
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`
|
|
5428
|
+
};
|
|
5429
|
+
}
|
|
5430
|
+
}
|
|
5431
|
+
const realShot = await shoot(adapter.component, withoutHoverToken(cfg.props), true);
|
|
5185
5432
|
if (!Buffer.isBuffer(realShot)) return { id: `parity:${cfg.rep}`, pass: false, detail: `real-hover mount: ${realShot.error}` };
|
|
5186
5433
|
if (Buffer.compare(forcedShot, realShot) !== 0) {
|
|
5187
5434
|
return {
|
|
@@ -5221,13 +5468,14 @@ var init_parity = __esm({
|
|
|
5221
5468
|
init_behavior();
|
|
5222
5469
|
init_font_faces();
|
|
5223
5470
|
init_mount_limits();
|
|
5471
|
+
init_bundle_score();
|
|
5224
5472
|
}
|
|
5225
5473
|
});
|
|
5226
5474
|
|
|
5227
5475
|
// packages/verify/src/composition.ts
|
|
5228
5476
|
import { createRequire as createRequire2 } from "node:module";
|
|
5229
|
-
import { existsSync as
|
|
5230
|
-
import
|
|
5477
|
+
import { existsSync as existsSync14, readFileSync as readFileSync11 } from "node:fs";
|
|
5478
|
+
import path19 from "node:path";
|
|
5231
5479
|
import { build as build6 } from "esbuild";
|
|
5232
5480
|
import { chromium as chromium7 } from "playwright-core";
|
|
5233
5481
|
function getFontFaces4() {
|
|
@@ -5235,9 +5483,9 @@ function getFontFaces4() {
|
|
|
5235
5483
|
return _fontFaces4;
|
|
5236
5484
|
}
|
|
5237
5485
|
async function compileInstrumentedMount(task, bundleDir) {
|
|
5238
|
-
const entryTsx =
|
|
5239
|
-
if (!
|
|
5240
|
-
const requireFromVerify = createRequire2(
|
|
5486
|
+
const entryTsx = path19.join(bundleDir, task.entry);
|
|
5487
|
+
if (!existsSync14(entryTsx)) return { error: `${task.entry} missing` };
|
|
5488
|
+
const requireFromVerify = createRequire2(path19.join(VERIFY_PKG_DIR, "package.json"));
|
|
5241
5489
|
let realJsxPath;
|
|
5242
5490
|
try {
|
|
5243
5491
|
realJsxPath = requireFromVerify.resolve("react/jsx-runtime");
|
|
@@ -5248,7 +5496,7 @@ async function compileInstrumentedMount(task, bundleDir) {
|
|
|
5248
5496
|
import { createElement } from "react";
|
|
5249
5497
|
import { createRoot } from "react-dom/client";
|
|
5250
5498
|
import { __registerParts } from "react/jsx-runtime";
|
|
5251
|
-
import * as B from ${JSON.stringify(
|
|
5499
|
+
import * as B from ${JSON.stringify(path19.resolve(entryTsx))};
|
|
5252
5500
|
const cfg = (window as unknown as { __cfg: { component: string; props: Record<string, unknown>; partComponents: string[] } }).__cfg;
|
|
5253
5501
|
const pairs: Array<[unknown, string]> = [];
|
|
5254
5502
|
for (const name of cfg.partComponents) {
|
|
@@ -5303,7 +5551,7 @@ function expectedParts(task, roles, mainSlug) {
|
|
|
5303
5551
|
}
|
|
5304
5552
|
function interiorRegions(setDir, roles) {
|
|
5305
5553
|
const mains = roles.main;
|
|
5306
|
-
const withInterior = mains.filter((m) =>
|
|
5554
|
+
const withInterior = mains.filter((m) => existsSync14(path19.join(setDir, m, "get_metadata_interior.json")));
|
|
5307
5555
|
if (withInterior.length === 0) {
|
|
5308
5556
|
return { unavailable: "no interior geometry recorded for any main (pre-obligation set) \u2014 re-record with `tendril record` to enable crop checks" };
|
|
5309
5557
|
}
|
|
@@ -5320,7 +5568,7 @@ async function checkCropComposition(task, bundleDir, regions, opts = {}) {
|
|
|
5320
5568
|
if (typeof js !== "string") {
|
|
5321
5569
|
return regions.map((r) => ({ id: cropId(r), pass: false, similarity: 0, inkRecall: 0, detail: js.error }));
|
|
5322
5570
|
}
|
|
5323
|
-
const css = ["tokens.css", "styles.css"].map((f) =>
|
|
5571
|
+
const css = ["tokens.css", "styles.css"].map((f) => path19.join(bundleDir, f)).filter((f) => existsSync14(f)).map((f) => readFileSync11(f, "utf8")).join("\n");
|
|
5324
5572
|
const server = await chromium7.launchServer({ executablePath: resolveChrome(), headless: true, args: mountArgs() });
|
|
5325
5573
|
const browser = await chromium7.connect(server.wsEndpoint());
|
|
5326
5574
|
const PAD = 4;
|
|
@@ -5425,7 +5673,7 @@ async function checkStructuralComposition(task, bundleDir, roles, opts = {}) {
|
|
|
5425
5673
|
const deadlineMs = timeoutMs + 1e4;
|
|
5426
5674
|
const js = await compileInstrumentedMount(task, bundleDir);
|
|
5427
5675
|
if (typeof js !== "string") return [...results, ...mains.map((m) => ({ id: `composition:${m}`, pass: false, detail: js.error }))];
|
|
5428
|
-
const css = ["tokens.css", "styles.css"].map((f) =>
|
|
5676
|
+
const css = ["tokens.css", "styles.css"].map((f) => path19.join(bundleDir, f)).filter((f) => existsSync14(f)).map((f) => readFileSync11(f, "utf8")).join("\n");
|
|
5429
5677
|
const server = await chromium7.launchServer({ executablePath: resolveChrome(), headless: true, args: mountArgs() });
|
|
5430
5678
|
const browser = await chromium7.connect(server.wsEndpoint());
|
|
5431
5679
|
try {
|
|
@@ -5515,17 +5763,17 @@ export const jsxs = (t, p, k) => real.jsxs(t, inject(t, p), k);
|
|
|
5515
5763
|
});
|
|
5516
5764
|
|
|
5517
5765
|
// packages/verify/src/occlusion.ts
|
|
5518
|
-
import { existsSync as
|
|
5519
|
-
import
|
|
5766
|
+
import { existsSync as existsSync15 } from "node:fs";
|
|
5767
|
+
import path20 from "node:path";
|
|
5520
5768
|
import { build as build7 } from "esbuild";
|
|
5521
5769
|
import { chromium as chromium8 } from "playwright-core";
|
|
5522
5770
|
async function compileTwoUp(task, bundleDir) {
|
|
5523
|
-
const entryTsx =
|
|
5524
|
-
if (!
|
|
5771
|
+
const entryTsx = path20.join(bundleDir, task.entry);
|
|
5772
|
+
if (!existsSync15(entryTsx)) return { error: `${task.entry} missing` };
|
|
5525
5773
|
const src = `
|
|
5526
5774
|
import { createElement } from "react";
|
|
5527
5775
|
import { createRoot } from "react-dom/client";
|
|
5528
|
-
import * as B from ${JSON.stringify(
|
|
5776
|
+
import * as B from ${JSON.stringify(path20.resolve(entryTsx))};
|
|
5529
5777
|
const cfg = (window as unknown as { __cfg: { component: string; props: Record<string, unknown> } }).__cfg;
|
|
5530
5778
|
const C = (B as Record<string, unknown>)[cfg.component] as Parameters<typeof createElement>[0] | undefined;
|
|
5531
5779
|
for (const id of ["first", "second"]) {
|
|
@@ -5694,6 +5942,7 @@ var init_src4 = __esm({
|
|
|
5694
5942
|
init_prelude();
|
|
5695
5943
|
init_mount_limits();
|
|
5696
5944
|
init_font_collection();
|
|
5945
|
+
init_font_discovery();
|
|
5697
5946
|
init_font_faces();
|
|
5698
5947
|
init_font_resolve();
|
|
5699
5948
|
init_paths();
|
|
@@ -5704,23 +5953,23 @@ var init_src4 = __esm({
|
|
|
5704
5953
|
});
|
|
5705
5954
|
|
|
5706
5955
|
// packages/cli/src/environment.ts
|
|
5707
|
-
import { existsSync as
|
|
5708
|
-
import
|
|
5956
|
+
import { existsSync as existsSync16, readFileSync as readFileSync12 } from "node:fs";
|
|
5957
|
+
import path21 from "node:path";
|
|
5709
5958
|
import { createHash as createHash3 } from "node:crypto";
|
|
5710
5959
|
import { fileURLToPath as fileURLToPath5 } from "node:url";
|
|
5711
5960
|
function cliVersion() {
|
|
5712
5961
|
try {
|
|
5713
|
-
return JSON.parse(
|
|
5962
|
+
return JSON.parse(readFileSync12(path21.join(path21.dirname(fileURLToPath5(import.meta.url)), "..", "package.json"), "utf8")).version ?? "dev";
|
|
5714
5963
|
} catch {
|
|
5715
5964
|
return "dev";
|
|
5716
5965
|
}
|
|
5717
5966
|
}
|
|
5718
5967
|
function environmentStamp(taskFamilies) {
|
|
5719
|
-
const manifestPath2 =
|
|
5968
|
+
const manifestPath2 = path21.join(fontCacheDir(), "manifest.json");
|
|
5720
5969
|
let fontsHash = null;
|
|
5721
|
-
if (
|
|
5970
|
+
if (existsSync16(manifestPath2)) {
|
|
5722
5971
|
try {
|
|
5723
|
-
const entries = JSON.parse(
|
|
5972
|
+
const entries = JSON.parse(readFileSync12(manifestPath2, "utf8"));
|
|
5724
5973
|
const scoped = taskFamilies === void 0 || taskFamilies === null ? entries : entries.filter((f) => taskFamilies.some((fam) => fam.toLowerCase() === f.family.toLowerCase()));
|
|
5725
5974
|
const faces = scoped.map((f) => `${f.family}:${f.weight}:${f.sha256}`).sort();
|
|
5726
5975
|
fontsHash = faces.length === 0 ? null : createHash3("sha256").update(faces.join("\n")).digest("hex").slice(0, 16);
|
|
@@ -5764,8 +6013,8 @@ var init_describe = __esm({
|
|
|
5764
6013
|
});
|
|
5765
6014
|
|
|
5766
6015
|
// packages/cli/src/env.ts
|
|
5767
|
-
import { existsSync as
|
|
5768
|
-
import
|
|
6016
|
+
import { existsSync as existsSync17, readFileSync as readFileSync13 } from "node:fs";
|
|
6017
|
+
import path22 from "node:path";
|
|
5769
6018
|
function parseEnv(content) {
|
|
5770
6019
|
const entries = /* @__PURE__ */ new Map();
|
|
5771
6020
|
for (const line of content.split("\n")) {
|
|
@@ -5777,9 +6026,9 @@ function parseEnv(content) {
|
|
|
5777
6026
|
function resolveCredential(name) {
|
|
5778
6027
|
const fromProcess = process.env[name];
|
|
5779
6028
|
if (fromProcess) return fromProcess;
|
|
5780
|
-
const envPath =
|
|
5781
|
-
if (!
|
|
5782
|
-
return parseEnv(
|
|
6029
|
+
const envPath = path22.resolve(process.env["INIT_CWD"] ?? process.cwd(), ".env");
|
|
6030
|
+
if (!existsSync17(envPath)) return void 0;
|
|
6031
|
+
return parseEnv(readFileSync13(envPath, "utf8")).get(name);
|
|
5783
6032
|
}
|
|
5784
6033
|
var init_env = __esm({
|
|
5785
6034
|
"packages/cli/src/env.ts"() {
|
|
@@ -5839,17 +6088,17 @@ var init_output = __esm({
|
|
|
5839
6088
|
});
|
|
5840
6089
|
|
|
5841
6090
|
// packages/cli/src/entitlement.ts
|
|
5842
|
-
import { chmodSync, existsSync as
|
|
6091
|
+
import { chmodSync, existsSync as existsSync18, mkdirSync as mkdirSync4, readFileSync as readFileSync14, writeFileSync as writeFileSync7 } from "node:fs";
|
|
5843
6092
|
import crypto from "node:crypto";
|
|
5844
|
-
import
|
|
5845
|
-
import
|
|
6093
|
+
import os4 from "node:os";
|
|
6094
|
+
import path23 from "node:path";
|
|
5846
6095
|
function entitlementPath() {
|
|
5847
|
-
return process.env["TENDRIL_ENTITLEMENT_PATH"] ??
|
|
6096
|
+
return process.env["TENDRIL_ENTITLEMENT_PATH"] ?? path23.join(os4.homedir(), ".tendril", "entitlement.json");
|
|
5848
6097
|
}
|
|
5849
6098
|
function readStoredEntitlement(file = entitlementPath()) {
|
|
5850
|
-
if (!
|
|
6099
|
+
if (!existsSync18(file)) return void 0;
|
|
5851
6100
|
try {
|
|
5852
|
-
const parsed = JSON.parse(
|
|
6101
|
+
const parsed = JSON.parse(readFileSync14(file, "utf8"));
|
|
5853
6102
|
if (typeof parsed.token !== "string" || typeof parsed.lastRefreshAt !== "number") return void 0;
|
|
5854
6103
|
return { token: parsed.token, lastRefreshAt: parsed.lastRefreshAt };
|
|
5855
6104
|
} catch {
|
|
@@ -5857,7 +6106,7 @@ function readStoredEntitlement(file = entitlementPath()) {
|
|
|
5857
6106
|
}
|
|
5858
6107
|
}
|
|
5859
6108
|
function writeStoredEntitlement(stored, file = entitlementPath()) {
|
|
5860
|
-
mkdirSync4(
|
|
6109
|
+
mkdirSync4(path23.dirname(file), { recursive: true });
|
|
5861
6110
|
writeFileSync7(file, `${JSON.stringify(stored, null, 2)}
|
|
5862
6111
|
`);
|
|
5863
6112
|
chmodSync(file, 384);
|
|
@@ -5942,9 +6191,9 @@ var init_entitlement = __esm({
|
|
|
5942
6191
|
|
|
5943
6192
|
// packages/cli/src/commands/doctor.ts
|
|
5944
6193
|
import { spawnSync } from "node:child_process";
|
|
5945
|
-
import { existsSync as
|
|
5946
|
-
import
|
|
5947
|
-
import
|
|
6194
|
+
import { existsSync as existsSync19, readFileSync as readFileSync15, readdirSync as readdirSync4 } from "node:fs";
|
|
6195
|
+
import os5 from "node:os";
|
|
6196
|
+
import path24 from "node:path";
|
|
5948
6197
|
function withDeadline(work, ms) {
|
|
5949
6198
|
return Promise.race([
|
|
5950
6199
|
work,
|
|
@@ -6004,19 +6253,19 @@ async function runDoctorChecks(options) {
|
|
|
6004
6253
|
remediation: "Install Google Chrome (or Chromium), or set CHROME_PATH to a Chromium-family browser binary."
|
|
6005
6254
|
});
|
|
6006
6255
|
}
|
|
6007
|
-
const fontManifest =
|
|
6256
|
+
const fontManifest = path24.join(fontCacheDir(), "manifest.json");
|
|
6008
6257
|
checks.push(
|
|
6009
|
-
|
|
6258
|
+
existsSync19(fontManifest) ? { name: "font-cache", ok: true, detail: `resolved font cache at ${fontCacheDir()} (${JSON.parse(readFileSync15(fontManifest, "utf8")).length} faces)` } : {
|
|
6010
6259
|
name: "font-cache",
|
|
6011
6260
|
ok: true,
|
|
6012
6261
|
detail: `font cache empty at ${fontCacheDir()} \u2014 normal on a fresh machine; faces resolve per design system at first use`,
|
|
6013
6262
|
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.`
|
|
6014
6263
|
}
|
|
6015
6264
|
);
|
|
6016
|
-
const pluginRoot =
|
|
6017
|
-
if (
|
|
6265
|
+
const pluginRoot = path24.join(os5.homedir(), ".claude", "plugins", "cache", "tendrilapp", "tendril");
|
|
6266
|
+
if (existsSync19(pluginRoot)) {
|
|
6018
6267
|
try {
|
|
6019
|
-
const versions =
|
|
6268
|
+
const versions = readdirSync4(pluginRoot).filter((v) => /^\d+\.\d+\.\d+$/.test(v));
|
|
6020
6269
|
const newest = versions.sort((a, b) => versionIsNewer(a, b) ? 1 : -1)[0];
|
|
6021
6270
|
if (newest !== void 0) {
|
|
6022
6271
|
const skewed = versionIsNewer(newest, cliVersion());
|
|
@@ -7540,9 +7789,9 @@ __export(record_exports, {
|
|
|
7540
7789
|
runRecordPlan: () => runRecordPlan,
|
|
7541
7790
|
runRecordStatus: () => runRecordStatus
|
|
7542
7791
|
});
|
|
7543
|
-
import { existsSync as
|
|
7544
|
-
import
|
|
7545
|
-
import
|
|
7792
|
+
import { existsSync as existsSync21, mkdtempSync as mkdtempSync2, readFileSync as readFileSync17, readdirSync as readdirSync6 } from "node:fs";
|
|
7793
|
+
import os6 from "node:os";
|
|
7794
|
+
import path27 from "node:path";
|
|
7546
7795
|
import { writeFileSync as writeFileSync9 } from "node:fs";
|
|
7547
7796
|
function recordsInteractionState(reports) {
|
|
7548
7797
|
const evident = (s) => stateTokens(s).some((t) => INTERACTION_EVIDENCE_VALUES.has(t));
|
|
@@ -7565,7 +7814,7 @@ function interactionDisclosure(component, reports) {
|
|
|
7565
7814
|
};
|
|
7566
7815
|
}
|
|
7567
7816
|
function symbolsFromMetadataEnvelope(file, sourceFrame) {
|
|
7568
|
-
const env = JSON.parse(
|
|
7817
|
+
const env = JSON.parse(readFileSync17(file, "utf8"));
|
|
7569
7818
|
const text = env.content.map((c) => c.text ?? "").join("\n");
|
|
7570
7819
|
const symbols = [];
|
|
7571
7820
|
const walk2 = (node, ancestor) => {
|
|
@@ -7623,7 +7872,7 @@ function runRecordPlan(opts) {
|
|
|
7623
7872
|
if (rawFile !== void 0) {
|
|
7624
7873
|
try {
|
|
7625
7874
|
const envelope = rawEnvelopeFromFile(rawFile, opts.metadataRawPartsFile !== void 0);
|
|
7626
|
-
const tmp =
|
|
7875
|
+
const tmp = path27.join(mkdtempSync2(path27.join(os6.tmpdir(), "tendril-plan-meta-")), "get_metadata.json");
|
|
7627
7876
|
writeFileSync9(tmp, JSON.stringify(envelope));
|
|
7628
7877
|
metadataEntries.push({ file: tmp });
|
|
7629
7878
|
} catch (err) {
|
|
@@ -7645,7 +7894,7 @@ function runRecordPlan(opts) {
|
|
|
7645
7894
|
let metadataTruncated = false;
|
|
7646
7895
|
for (const { file, frame } of metadataEntries) {
|
|
7647
7896
|
try {
|
|
7648
|
-
const parsed = symbolsFromMetadataEnvelope(
|
|
7897
|
+
const parsed = symbolsFromMetadataEnvelope(path27.resolve(file), frame);
|
|
7649
7898
|
symbols.push(...parsed.symbols);
|
|
7650
7899
|
if (parsed.truncated) metadataTruncated = true;
|
|
7651
7900
|
} catch (err) {
|
|
@@ -7679,7 +7928,7 @@ function runRecordPlan(opts) {
|
|
|
7679
7928
|
if (symbols.length === 0) {
|
|
7680
7929
|
const leads = metadataEntries.flatMap(({ file }) => {
|
|
7681
7930
|
try {
|
|
7682
|
-
const env = JSON.parse(
|
|
7931
|
+
const env = JSON.parse(readFileSync17(path27.resolve(file), "utf8"));
|
|
7683
7932
|
return instanceLeads(env.content.map((c) => c.text ?? "").join("\n"));
|
|
7684
7933
|
} catch {
|
|
7685
7934
|
return [];
|
|
@@ -7753,7 +8002,7 @@ function runRecordPlan(opts) {
|
|
|
7753
8002
|
// a CLI flag and the MCP surface has no parameter for it.
|
|
7754
8003
|
decidedBy: "HUMAN ONLY \u2014 offer it, never choose it, and you cannot run it",
|
|
7755
8004
|
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.",
|
|
7756
|
-
userRuns: [`rm ${quoteArg(
|
|
8005
|
+
userRuns: [`rm ${quoteArg(path27.join(opts.setDir, "recording-set.json"))}`, `${tendrilCommand(`record plan --set ${quoteArg(opts.setDir)} --component ${quoteArg(manifest.component)}`)} --sample`]
|
|
7757
8006
|
},
|
|
7758
8007
|
{
|
|
7759
8008
|
id: "larger-allowance",
|
|
@@ -7880,7 +8129,7 @@ function nextPayload(setDir) {
|
|
|
7880
8129
|
const instruction = nextInstruction(setDir);
|
|
7881
8130
|
const status = sessionStatus(setDir);
|
|
7882
8131
|
const progress = { recordedReps: status.reps.filter((x) => x.missing.length === 0).length, totalReps: status.reps.length };
|
|
7883
|
-
if (instruction === null && !
|
|
8132
|
+
if (instruction === null && !existsSync21(path27.join(setDir, "get_variable_defs.json"))) {
|
|
7884
8133
|
const manifest = loadManifest(setDir);
|
|
7885
8134
|
const frameNode = Object.keys(manifest.sourceFrames ?? {})[0] ?? manifest.reps[0]?.nodeId ?? "";
|
|
7886
8135
|
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__. ${ENVELOPE_HELP}`, progress };
|
|
@@ -7909,7 +8158,7 @@ function runRecordNext(opts) {
|
|
|
7909
8158
|
const progress = payload["progress"];
|
|
7910
8159
|
process.stdout.write(`[${progress.recordedReps}/${progress.totalReps} reps] ${payload["tool"]} for ${payload["slug"]} (node ${payload["nodeId"]})
|
|
7911
8160
|
\u2192 ${payload["note"]}
|
|
7912
|
-
\u2192 then: ${tendrilCommand(`record ingest --set ${
|
|
8161
|
+
\u2192 then: ${tendrilCommand(`record ingest --set ${path27.resolve(opts.setDir)} --rep ${payload["slug"]} --tool ${payload["tool"]} --file <envelope.json>`)}
|
|
7913
8162
|
`);
|
|
7914
8163
|
});
|
|
7915
8164
|
}
|
|
@@ -7983,7 +8232,7 @@ async function autoFetchAssets(setDir, rep, envelopeText2) {
|
|
|
7983
8232
|
const skipped = [];
|
|
7984
8233
|
const failed = [];
|
|
7985
8234
|
for (const { url, name } of assetUrlsFromEnvelopeText(envelopeText2)) {
|
|
7986
|
-
if (
|
|
8235
|
+
if (existsSync21(path27.join(setDir, rep, name))) {
|
|
7987
8236
|
skipped.push(name);
|
|
7988
8237
|
continue;
|
|
7989
8238
|
}
|
|
@@ -8005,16 +8254,16 @@ async function autoFetchAssets(setDir, rep, envelopeText2) {
|
|
|
8005
8254
|
}
|
|
8006
8255
|
function rawEnvelopeFromFile(file, parts) {
|
|
8007
8256
|
if (parts) {
|
|
8008
|
-
const blocks = JSON.parse(
|
|
8257
|
+
const blocks = JSON.parse(readFileSync17(path27.resolve(file), "utf8"));
|
|
8009
8258
|
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");
|
|
8010
8259
|
return { content: blocks.map((text) => ({ type: "text", text })) };
|
|
8011
8260
|
}
|
|
8012
|
-
return { content: [{ type: "text", text:
|
|
8261
|
+
return { content: [{ type: "text", text: readFileSync17(path27.resolve(file), "utf8") }] };
|
|
8013
8262
|
}
|
|
8014
8263
|
async function runRecordIngest(opts) {
|
|
8015
8264
|
let payload;
|
|
8016
8265
|
try {
|
|
8017
|
-
payload = opts.rawParts === true || opts.raw === true ? rawEnvelopeFromFile(opts.file, opts.rawParts === true) : JSON.parse(
|
|
8266
|
+
payload = opts.rawParts === true || opts.raw === true ? rawEnvelopeFromFile(opts.file, opts.rawParts === true) : JSON.parse(readFileSync17(path27.resolve(opts.file), "utf8"));
|
|
8018
8267
|
} catch (err) {
|
|
8019
8268
|
fail(opts, ExitCode.InputValidation, {
|
|
8020
8269
|
error: `cannot read envelope file: ${err instanceof Error ? err.message : String(err)}`,
|
|
@@ -8026,7 +8275,7 @@ async function runRecordIngest(opts) {
|
|
|
8026
8275
|
fail(opts, ExitCode.InputValidation, {
|
|
8027
8276
|
error: "--raw is for text tool responses; screenshots are binary",
|
|
8028
8277
|
code: "envelope-invalid",
|
|
8029
|
-
remediation: `Use \`${tendrilCommand(`record fetch --set ${
|
|
8278
|
+
remediation: `Use \`${tendrilCommand(`record fetch --set ${path27.resolve(opts.setDir)} --rep ${opts.rep} --tool ${opts.tool} --url <image_url>`)}\` instead.`
|
|
8030
8279
|
});
|
|
8031
8280
|
}
|
|
8032
8281
|
if (opts.rep === "__set__" && opts.tool === "get_variable_defs") {
|
|
@@ -8046,7 +8295,7 @@ async function runRecordIngest(opts) {
|
|
|
8046
8295
|
remediation: REINGEST_GUIDANCE
|
|
8047
8296
|
});
|
|
8048
8297
|
}
|
|
8049
|
-
writeFileSync9(
|
|
8298
|
+
writeFileSync9(path27.join(opts.setDir, "get_variable_defs.json"), `${JSON.stringify(payload, null, 1)}
|
|
8050
8299
|
`);
|
|
8051
8300
|
emitData(opts, { ingested: "get_variable_defs", rep: "__set__", setLevel: true, next: nextPayload(opts.setDir) }, () => {
|
|
8052
8301
|
process.stdout.write("set-level get_variable_defs ingested\n");
|
|
@@ -8062,7 +8311,7 @@ async function runRecordIngest(opts) {
|
|
|
8062
8311
|
if (assets !== void 0) {
|
|
8063
8312
|
for (const a of assets.fetched) process.stdout.write(` asset fetched ${a}
|
|
8064
8313
|
`);
|
|
8065
|
-
for (const f of assets.failed) process.stdout.write(` ASSET FAILED ${f.name}: ${f.reason} \u2014 download it, then: ${tendrilCommand(`record asset --set ${
|
|
8314
|
+
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>`)}
|
|
8066
8315
|
`);
|
|
8067
8316
|
}
|
|
8068
8317
|
});
|
|
@@ -8135,15 +8384,15 @@ async function runRecordIngestRep(opts) {
|
|
|
8135
8384
|
if (assets !== void 0) {
|
|
8136
8385
|
for (const a of assets.fetched) process.stdout.write(` asset fetched ${a}
|
|
8137
8386
|
`);
|
|
8138
|
-
for (const f of assets.failed) process.stdout.write(` ASSET FAILED ${f.name}: ${f.reason} \u2014 download it, then: ${tendrilCommand(`record asset --set ${
|
|
8387
|
+
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>`)}
|
|
8139
8388
|
`);
|
|
8140
8389
|
}
|
|
8141
8390
|
});
|
|
8142
8391
|
}
|
|
8143
8392
|
function runRecordAsset(opts) {
|
|
8144
8393
|
if (opts.dir !== void 0) {
|
|
8145
|
-
const dir =
|
|
8146
|
-
const names =
|
|
8394
|
+
const dir = path27.resolve(opts.dir);
|
|
8395
|
+
const names = readdirSync6(dir).filter((f) => /^asset-[\w.-]+\.(svg|png|jpe?g|webp|gif)$/i.test(f));
|
|
8147
8396
|
if (names.length === 0) {
|
|
8148
8397
|
fail(opts, ExitCode.InputValidation, {
|
|
8149
8398
|
error: `no asset-*.<ext> files found in ${dir}`,
|
|
@@ -8154,7 +8403,7 @@ function runRecordAsset(opts) {
|
|
|
8154
8403
|
const ingested = [];
|
|
8155
8404
|
try {
|
|
8156
8405
|
for (const name of names) {
|
|
8157
|
-
ingestAsset(opts.setDir, opts.rep, name,
|
|
8406
|
+
ingestAsset(opts.setDir, opts.rep, name, readFileSync17(path27.join(dir, name)));
|
|
8158
8407
|
ingested.push(name);
|
|
8159
8408
|
}
|
|
8160
8409
|
} catch (err) {
|
|
@@ -8174,11 +8423,11 @@ function runRecordAsset(opts) {
|
|
|
8174
8423
|
fail(opts, ExitCode.InputValidation, {
|
|
8175
8424
|
error: "pass --name and --file for a single asset, or --dir for a batch",
|
|
8176
8425
|
code: "asset-rejected",
|
|
8177
|
-
remediation: tendrilCommand(`record asset --set ${
|
|
8426
|
+
remediation: tendrilCommand(`record asset --set ${path27.resolve(opts.setDir)} --rep ${opts.rep} --dir <downloads-dir>`)
|
|
8178
8427
|
});
|
|
8179
8428
|
}
|
|
8180
8429
|
try {
|
|
8181
|
-
ingestAsset(opts.setDir, opts.rep, opts.name,
|
|
8430
|
+
ingestAsset(opts.setDir, opts.rep, opts.name, readFileSync17(path27.resolve(opts.file)));
|
|
8182
8431
|
emitData(opts, { rep: opts.rep, asset: opts.name }, () => {
|
|
8183
8432
|
process.stdout.write(`ingested ${opts.rep}/${opts.name}
|
|
8184
8433
|
`);
|
|
@@ -8214,7 +8463,7 @@ function narrowedRoles(derived, override) {
|
|
|
8214
8463
|
function rolesFromFile(opts, file, derived) {
|
|
8215
8464
|
let json;
|
|
8216
8465
|
try {
|
|
8217
|
-
json = JSON.parse(
|
|
8466
|
+
json = JSON.parse(readFileSync17(path27.resolve(file), "utf8"));
|
|
8218
8467
|
} catch (err) {
|
|
8219
8468
|
fail(opts, ExitCode.InputValidation, {
|
|
8220
8469
|
error: `cannot read roles file ${file}: ${err instanceof Error ? err.message.split("\n")[0] : String(err)}`,
|
|
@@ -8252,11 +8501,11 @@ function rolesFromFile(opts, file, derived) {
|
|
|
8252
8501
|
};
|
|
8253
8502
|
}
|
|
8254
8503
|
function runRecordFinish(opts) {
|
|
8255
|
-
if (!
|
|
8504
|
+
if (!existsSync21(path27.join(opts.setDir, "recording-set.json"))) {
|
|
8256
8505
|
fail(opts, ExitCode.InputValidation, {
|
|
8257
8506
|
error: `no recording-set.json in ${opts.setDir}`,
|
|
8258
8507
|
code: "no-recording-set",
|
|
8259
|
-
remediation: `Run \`${tendrilCommand(`record plan --set ${
|
|
8508
|
+
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.`
|
|
8260
8509
|
});
|
|
8261
8510
|
}
|
|
8262
8511
|
const { manifest, raw } = readManifestFile(opts.setDir);
|
|
@@ -8284,17 +8533,17 @@ function runRecordFinish(opts) {
|
|
|
8284
8533
|
fail(opts, ExitCode.ConfirmationRequired, {
|
|
8285
8534
|
error: "--confirm-roles (and --roles-file) require an interactive terminal \u2014 this confirmation is human-only",
|
|
8286
8535
|
code: "roles-confirmation-not-interactive",
|
|
8287
|
-
remediation: `A human runs \`${tendrilCommand(`record finish --set ${
|
|
8536
|
+
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.`
|
|
8288
8537
|
});
|
|
8289
8538
|
}
|
|
8290
8539
|
const merged = { ...raw, roles };
|
|
8291
|
-
const { issues } = validateRecordingSet(merged, (rel) =>
|
|
8540
|
+
const { issues } = validateRecordingSet(merged, (rel) => existsSync21(path27.join(opts.setDir, rel)));
|
|
8292
8541
|
const errors = issues.filter((i) => i.severity === "error");
|
|
8293
8542
|
if (errors.length > 0) {
|
|
8294
8543
|
fail(opts, ExitCode.InputValidation, {
|
|
8295
8544
|
error: `recording set rejected \u2014 roles NOT written: ${errors.map((e) => e.message).join("; ")}`,
|
|
8296
8545
|
code: "recording-set-invalid",
|
|
8297
|
-
remediation: `Fix the set (\`${tendrilCommand(`record status --set ${
|
|
8546
|
+
remediation: `Fix the set (\`${tendrilCommand(`record status --set ${path27.resolve(opts.setDir)}`)}\` lists missing envelopes) or the roles graph, then re-run \`record finish\`.`
|
|
8298
8547
|
});
|
|
8299
8548
|
}
|
|
8300
8549
|
for (const issue of issues) if (issue.severity === "warning") warn(opts, issue.message);
|
|
@@ -8528,8 +8777,8 @@ var init_engine_curated = __esm({
|
|
|
8528
8777
|
});
|
|
8529
8778
|
|
|
8530
8779
|
// packages/generate/src/loop.ts
|
|
8531
|
-
import { existsSync as
|
|
8532
|
-
import
|
|
8780
|
+
import { existsSync as existsSync22, mkdirSync as mkdirSync6, readFileSync as readFileSync18, renameSync, writeFileSync as writeFileSync10 } from "node:fs";
|
|
8781
|
+
import path28 from "node:path";
|
|
8533
8782
|
import { z as z11 } from "zod";
|
|
8534
8783
|
function objective(scores, behaviors) {
|
|
8535
8784
|
const vals = scores.map((s) => Math.min(s.similarity, s.inkRecall));
|
|
@@ -8566,9 +8815,9 @@ ${absentLines.join("\n")}` : ""}
|
|
|
8566
8815
|
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."}`;
|
|
8567
8816
|
}
|
|
8568
8817
|
function archivePriorRun(outDir) {
|
|
8569
|
-
if (!
|
|
8818
|
+
if (!existsSync22(path28.join(outDir, "run-log.json")) && !existsSync22(path28.join(outDir, "loop-state.json"))) return void 0;
|
|
8570
8819
|
let n = 1;
|
|
8571
|
-
while (
|
|
8820
|
+
while (existsSync22(`${outDir}-prev-${n}`)) n += 1;
|
|
8572
8821
|
renameSync(outDir, `${outDir}-prev-${n}`);
|
|
8573
8822
|
return `${outDir}-prev-${n}`;
|
|
8574
8823
|
}
|
|
@@ -8577,14 +8826,14 @@ async function runEngineLoop(opts) {
|
|
|
8577
8826
|
const plateau = opts.plateau ?? 2;
|
|
8578
8827
|
const progress = opts.onProgress ?? (() => {
|
|
8579
8828
|
});
|
|
8580
|
-
const statePath =
|
|
8581
|
-
const resuming = opts.resume === true &&
|
|
8829
|
+
const statePath = path28.join(opts.outDir, "loop-state.json");
|
|
8830
|
+
const resuming = opts.resume === true && existsSync22(statePath);
|
|
8582
8831
|
if (!resuming) {
|
|
8583
8832
|
const archived = archivePriorRun(opts.outDir);
|
|
8584
8833
|
if (archived !== void 0) progress(`previous run archived to ${archived}`);
|
|
8585
8834
|
}
|
|
8586
8835
|
mkdirSync6(opts.outDir, { recursive: true });
|
|
8587
|
-
const scratch =
|
|
8836
|
+
const scratch = path28.join(opts.outDir, ".candidate");
|
|
8588
8837
|
let attempts = [];
|
|
8589
8838
|
let log = [];
|
|
8590
8839
|
let best;
|
|
@@ -8592,7 +8841,7 @@ async function runEngineLoop(opts) {
|
|
|
8592
8841
|
let nonAccepted = 0;
|
|
8593
8842
|
let stopReason = "max-iterations";
|
|
8594
8843
|
if (resuming) {
|
|
8595
|
-
const restored = LoopStateSchema.parse(JSON.parse(
|
|
8844
|
+
const restored = LoopStateSchema.parse(JSON.parse(readFileSync18(statePath, "utf8")));
|
|
8596
8845
|
attempts = restored.attempts;
|
|
8597
8846
|
log = restored.iterations;
|
|
8598
8847
|
spentUsd = restored.spentUsd;
|
|
@@ -8612,7 +8861,7 @@ async function runEngineLoop(opts) {
|
|
|
8612
8861
|
};
|
|
8613
8862
|
const writeCandidate = (files) => {
|
|
8614
8863
|
mkdirSync6(scratch, { recursive: true });
|
|
8615
|
-
for (const [name, content] of Object.entries(files)) writeFileSync10(
|
|
8864
|
+
for (const [name, content] of Object.entries(files)) writeFileSync10(path28.join(scratch, name), content);
|
|
8616
8865
|
};
|
|
8617
8866
|
const scoreCandidate = async (candidate, iter, usd, modelMs) => {
|
|
8618
8867
|
writeCandidate(candidate.files);
|
|
@@ -8670,8 +8919,8 @@ async function runEngineLoop(opts) {
|
|
|
8670
8919
|
const usd = candidate.usage?.usd ?? 0;
|
|
8671
8920
|
spentUsd += usd;
|
|
8672
8921
|
if (candidate.raw !== void 0) {
|
|
8673
|
-
mkdirSync6(
|
|
8674
|
-
writeFileSync10(
|
|
8922
|
+
mkdirSync6(path28.join(opts.outDir, "responses"), { recursive: true });
|
|
8923
|
+
writeFileSync10(path28.join(opts.outDir, "responses", `iter-${iter}.md`), candidate.raw);
|
|
8675
8924
|
}
|
|
8676
8925
|
if (candidate.files[opts.entry] === void 0 || candidate.files["styles.css"] === void 0) {
|
|
8677
8926
|
const finish = candidate.usage?.finishReason ?? "?";
|
|
@@ -8697,10 +8946,10 @@ async function runEngineLoop(opts) {
|
|
|
8697
8946
|
}
|
|
8698
8947
|
}
|
|
8699
8948
|
}
|
|
8700
|
-
if (best !== void 0) for (const [name, content] of Object.entries(best.files)) writeFileSync10(
|
|
8949
|
+
if (best !== void 0) for (const [name, content] of Object.entries(best.files)) writeFileSync10(path28.join(opts.outDir, name), content);
|
|
8701
8950
|
const final = best !== void 0 ? await opts.score(opts.outDir) : { scores: [], behaviors: [] };
|
|
8702
8951
|
writeFileSync10(
|
|
8703
|
-
|
|
8952
|
+
path28.join(opts.outDir, "run-log.json"),
|
|
8704
8953
|
`${JSON.stringify(
|
|
8705
8954
|
{
|
|
8706
8955
|
...opts.meta,
|
|
@@ -8767,8 +9016,8 @@ var init_loop2 = __esm({
|
|
|
8767
9016
|
});
|
|
8768
9017
|
|
|
8769
9018
|
// packages/generate/src/brief.ts
|
|
8770
|
-
import { existsSync as
|
|
8771
|
-
import
|
|
9019
|
+
import { existsSync as existsSync23, readFileSync as readFileSync19 } from "node:fs";
|
|
9020
|
+
import path29 from "node:path";
|
|
8772
9021
|
function singleAxes2(name) {
|
|
8773
9022
|
const parsed = parseVariantAxes(name);
|
|
8774
9023
|
if (parsed === void 0) return void 0;
|
|
@@ -9009,12 +9258,12 @@ function authorBehaviors(api, extras = {}) {
|
|
|
9009
9258
|
return { behaviors, prelude: { controls: interactive ? ["> *"] : [], textInputs: [] }, disclosures };
|
|
9010
9259
|
}
|
|
9011
9260
|
function envelopeText(file) {
|
|
9012
|
-
return envelopeFirstTextPart(JSON.parse(
|
|
9261
|
+
return envelopeFirstTextPart(JSON.parse(readFileSync19(file, "utf8")));
|
|
9013
9262
|
}
|
|
9014
9263
|
function dismissEvidence(setDir, repSlugs) {
|
|
9015
9264
|
for (const slug of repSlugs) {
|
|
9016
|
-
const f =
|
|
9017
|
-
if (!
|
|
9265
|
+
const f = path29.join(setDir, slug, "get_design_context.json");
|
|
9266
|
+
if (!existsSync23(f)) continue;
|
|
9018
9267
|
const text = envelopeText(f);
|
|
9019
9268
|
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);
|
|
9020
9269
|
if (propHit !== null) return `emission prop "${propHit[1]}"`;
|
|
@@ -9058,13 +9307,13 @@ function recordedFontNeeds(setDir, opts = {}) {
|
|
|
9058
9307
|
}
|
|
9059
9308
|
};
|
|
9060
9309
|
const manifest = loadManifest(setDir);
|
|
9061
|
-
const setDefs =
|
|
9062
|
-
if (
|
|
9310
|
+
const setDefs = path29.join(setDir, "get_variable_defs.json");
|
|
9311
|
+
if (existsSync23(setDefs)) fromDefs(envelopeText(setDefs));
|
|
9063
9312
|
for (const rep of manifest.reps) {
|
|
9064
|
-
const ctx =
|
|
9065
|
-
if (
|
|
9066
|
-
const defs =
|
|
9067
|
-
if (
|
|
9313
|
+
const ctx = path29.join(setDir, rep.slug, "get_design_context.json");
|
|
9314
|
+
if (existsSync23(ctx)) fromEmission(envelopeText(ctx));
|
|
9315
|
+
const defs = path29.join(setDir, rep.slug, "get_variable_defs.json");
|
|
9316
|
+
if (existsSync23(defs)) fromDefs(envelopeText(defs));
|
|
9068
9317
|
}
|
|
9069
9318
|
return [...byFamily.entries()].map(([family, paired]) => {
|
|
9070
9319
|
const weights = /* @__PURE__ */ new Set([...paired, ...opts.pairedOnly === true ? [] : unpaired]);
|
|
@@ -9075,10 +9324,10 @@ function symbolFontGlyphCount(setDir, reps) {
|
|
|
9075
9324
|
const PUA = /[\uE000-\uF8FF\u{F0000}-\u{FFFFD}\u{100000}-\u{10FFFD}]/u;
|
|
9076
9325
|
const glyphs = /* @__PURE__ */ new Set();
|
|
9077
9326
|
for (const rep of reps) {
|
|
9078
|
-
const file =
|
|
9079
|
-
if (!
|
|
9327
|
+
const file = path29.join(setDir, rep, "get_metadata.json");
|
|
9328
|
+
if (!existsSync23(file)) continue;
|
|
9080
9329
|
try {
|
|
9081
|
-
const text = JSON.parse(
|
|
9330
|
+
const text = JSON.parse(readFileSync19(file, "utf8")).content.map((c) => c.text ?? "").join("\n");
|
|
9082
9331
|
for (const m of text.matchAll(/<text\s[^>]*name="([^"]*)"/g)) {
|
|
9083
9332
|
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)));
|
|
9084
9333
|
if (PUA.test(name)) glyphs.add(name);
|
|
@@ -9104,8 +9353,8 @@ function recordedTextSlots(setDir, repSlugs) {
|
|
|
9104
9353
|
const propRep = [];
|
|
9105
9354
|
const perRep = [];
|
|
9106
9355
|
for (const slug of repSlugs) {
|
|
9107
|
-
const f =
|
|
9108
|
-
if (!
|
|
9356
|
+
const f = path29.join(setDir, slug, "get_design_context.json");
|
|
9357
|
+
if (!existsSync23(f)) continue;
|
|
9109
9358
|
const code = envelopeText(f);
|
|
9110
9359
|
const props = /* @__PURE__ */ new Map();
|
|
9111
9360
|
for (const m of code.matchAll(/[{,]\s*(\w+)\s*=\s*"((?:[^"\\]|\\.)*)"/g)) {
|
|
@@ -9130,8 +9379,8 @@ function recordedTextSlots(setDir, repSlugs) {
|
|
|
9130
9379
|
}
|
|
9131
9380
|
const axisValuesBySlug = /* @__PURE__ */ new Map();
|
|
9132
9381
|
for (const slug of repSlugs) {
|
|
9133
|
-
const metaFile =
|
|
9134
|
-
if (!
|
|
9382
|
+
const metaFile = path29.join(setDir, slug, "get_metadata.json");
|
|
9383
|
+
if (!existsSync23(metaFile)) continue;
|
|
9135
9384
|
const name = symbolName(envelopeText(metaFile));
|
|
9136
9385
|
if (name === void 0) continue;
|
|
9137
9386
|
const values = /* @__PURE__ */ new Set();
|
|
@@ -9230,8 +9479,8 @@ function authorTaskFromSet(setDir, opts = {}) {
|
|
|
9230
9479
|
const poses = [];
|
|
9231
9480
|
const missing = [];
|
|
9232
9481
|
for (const rep of manifest.reps) {
|
|
9233
|
-
const metaFile =
|
|
9234
|
-
if (!
|
|
9482
|
+
const metaFile = path29.join(setDir, rep.slug, "get_metadata.json");
|
|
9483
|
+
if (!existsSync23(metaFile)) {
|
|
9235
9484
|
missing.push(rep.slug);
|
|
9236
9485
|
continue;
|
|
9237
9486
|
}
|
|
@@ -9245,8 +9494,8 @@ function authorTaskFromSet(setDir, opts = {}) {
|
|
|
9245
9494
|
if (missing.length > 0) {
|
|
9246
9495
|
throw new Error(`recording set ${setDir} is missing metadata for ${missing.length} rep(s): ${missing.slice(0, 5).join(", ")}${missing.length > 5 ? ", \u2026" : ""}`);
|
|
9247
9496
|
}
|
|
9248
|
-
const setMeta =
|
|
9249
|
-
const latticeNames = manifest.latticeNames ?? (
|
|
9497
|
+
const setMeta = path29.join(setDir, "get_metadata.json");
|
|
9498
|
+
const latticeNames = manifest.latticeNames ?? (existsSync23(setMeta) ? [...envelopeText(setMeta).matchAll(/name="([^"]*)"/g)].map((m) => decodeXmlEntities(m[1])) : void 0);
|
|
9250
9499
|
const defaults = { ...manifest.defaults ?? {}, ...opts.defaults ?? {} };
|
|
9251
9500
|
const recordedFonts = recordedFontFamilies(setDir);
|
|
9252
9501
|
const textSlots = recordedTextSlots(setDir, manifest.reps.map((r) => r.slug));
|
|
@@ -9429,10 +9678,10 @@ var init_brief = __esm({
|
|
|
9429
9678
|
});
|
|
9430
9679
|
|
|
9431
9680
|
// packages/generate/src/segments.ts
|
|
9432
|
-
import { existsSync as
|
|
9433
|
-
import
|
|
9681
|
+
import { existsSync as existsSync24, readFileSync as readFileSync20, readdirSync as readdirSync7 } from "node:fs";
|
|
9682
|
+
import path30 from "node:path";
|
|
9434
9683
|
function repText(set, rep, tool) {
|
|
9435
|
-
const env = JSON.parse(
|
|
9684
|
+
const env = JSON.parse(readFileSync20(path30.join(set, rep, `${tool}.json`), "utf8"));
|
|
9436
9685
|
return tool === "get_design_context" ? envelopeFirstTextPart(env) : envelopeTextContent(env);
|
|
9437
9686
|
}
|
|
9438
9687
|
function stripFigmaInstructions(emission) {
|
|
@@ -9492,18 +9741,20 @@ DROPPED from the map, untrustworthy in the source export \u2014 for these, each
|
|
|
9492
9741
|
}
|
|
9493
9742
|
function buildSegments(task, mode = "fenced") {
|
|
9494
9743
|
const SET = task.set;
|
|
9744
|
+
let defsRecorded = existsSync24(path30.join(SET, "get_variable_defs.json"));
|
|
9495
9745
|
let rawDefs = {};
|
|
9496
|
-
if (
|
|
9497
|
-
const text = envelopeFirstTextPart(JSON.parse(
|
|
9746
|
+
if (existsSync24(path30.join(SET, "get_variable_defs.json"))) {
|
|
9747
|
+
const text = envelopeFirstTextPart(JSON.parse(readFileSync20(path30.join(SET, "get_variable_defs.json"), "utf8"))) || "{}";
|
|
9498
9748
|
try {
|
|
9499
9749
|
rawDefs = JSON.parse(text);
|
|
9500
9750
|
} catch {
|
|
9501
9751
|
}
|
|
9502
9752
|
} else {
|
|
9503
9753
|
for (const cfg of task.configs) {
|
|
9504
|
-
const f =
|
|
9505
|
-
if (!
|
|
9506
|
-
|
|
9754
|
+
const f = path30.join(SET, cfg.rep, "get_variable_defs.json");
|
|
9755
|
+
if (!existsSync24(f)) continue;
|
|
9756
|
+
defsRecorded = true;
|
|
9757
|
+
const text = envelopeFirstTextPart(JSON.parse(readFileSync20(f, "utf8"))) || "{}";
|
|
9507
9758
|
try {
|
|
9508
9759
|
for (const [k, v] of Object.entries(JSON.parse(text))) rawDefs[k] ??= v;
|
|
9509
9760
|
} catch {
|
|
@@ -9511,25 +9762,27 @@ function buildSegments(task, mode = "fenced") {
|
|
|
9511
9762
|
}
|
|
9512
9763
|
}
|
|
9513
9764
|
const emissionTexts = task.configs.map((cfg) => {
|
|
9514
|
-
const f =
|
|
9515
|
-
return
|
|
9765
|
+
const f = path30.join(SET, cfg.rep, "get_design_context.json");
|
|
9766
|
+
return existsSync24(f) ? envelopeFirstTextPart(JSON.parse(readFileSync20(f, "utf8"))) : "";
|
|
9516
9767
|
});
|
|
9517
9768
|
const { map, note } = cleanTokenMap(rawDefs, emissionTexts);
|
|
9518
9769
|
const defs = JSON.stringify(map, null, 1);
|
|
9519
9770
|
const parts = [
|
|
9520
9771
|
"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).",
|
|
9521
|
-
`
|
|
9772
|
+
defsRecorded ? `
|
|
9522
9773
|
## Design tokens (use these CSS custom property names)
|
|
9523
9774
|
\`\`\`json
|
|
9524
9775
|
${defs}
|
|
9525
|
-
\`\`\`${note}`
|
|
9776
|
+
\`\`\`${note}` : `
|
|
9777
|
+
## Design tokens
|
|
9778
|
+
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}`
|
|
9526
9779
|
];
|
|
9527
9780
|
for (const cfg of task.configs) {
|
|
9528
9781
|
const meta = stripFigmaInstructions(repText(SET, cfg.rep, "get_metadata"));
|
|
9529
9782
|
const emission = stripFigmaInstructions(repText(SET, cfg.rep, "get_design_context"));
|
|
9530
|
-
const assets =
|
|
9783
|
+
const assets = readdirSync7(path30.join(SET, cfg.rep)).filter((f) => f.startsWith("asset-") && f.endsWith(".svg")).map((f) => `asset ${f}:
|
|
9531
9784
|
\`\`\`svg
|
|
9532
|
-
${
|
|
9785
|
+
${readFileSync20(path30.join(SET, cfg.rep, f), "utf8")}
|
|
9533
9786
|
\`\`\``).join("\n");
|
|
9534
9787
|
parts.push(`
|
|
9535
9788
|
## Config ${cfg.rep} \u2192 ${cfg.component} ${JSON.stringify(cfg.props)}
|
|
@@ -9554,7 +9807,7 @@ Optionally a third block with \`/* FILE: tokens.css */\`.`);
|
|
|
9554
9807
|
} else {
|
|
9555
9808
|
parts.push(`
|
|
9556
9809
|
## Output format
|
|
9557
|
-
Write the COMPLETE files (${task.entry}, styles.css, optional tokens.css) into ONE candidate directory: \`tendril-out/${
|
|
9810
|
+
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.`);
|
|
9558
9811
|
}
|
|
9559
9812
|
return parts.join("\n");
|
|
9560
9813
|
}
|
|
@@ -9622,8 +9875,8 @@ var init_adapter = __esm({
|
|
|
9622
9875
|
|
|
9623
9876
|
// packages/generate/src/bundle-emit.ts
|
|
9624
9877
|
import { createHash as createHash4 } from "node:crypto";
|
|
9625
|
-
import { copyFileSync, existsSync as
|
|
9626
|
-
import
|
|
9878
|
+
import { copyFileSync, existsSync as existsSync25, mkdirSync as mkdirSync7, readFileSync as readFileSync21, readdirSync as readdirSync8, rmSync as rmSync3, writeFileSync as writeFileSync11 } from "node:fs";
|
|
9879
|
+
import path31 from "node:path";
|
|
9627
9880
|
function pinFromConfigs(configs) {
|
|
9628
9881
|
const domains = /* @__PURE__ */ new Map();
|
|
9629
9882
|
const kinds = /* @__PURE__ */ new Map();
|
|
@@ -9692,15 +9945,23 @@ function fontsCssFor(faces, bundleDir, cacheDir = DEFAULT_FONT_CACHE, substitute
|
|
|
9692
9945
|
const notices = [];
|
|
9693
9946
|
const licenseTexts = /* @__PURE__ */ new Map();
|
|
9694
9947
|
for (const face of faces) {
|
|
9695
|
-
const src =
|
|
9696
|
-
const target = `./fonts/${
|
|
9697
|
-
const format = FONT_FORMATS[
|
|
9948
|
+
const src = path31.join(cacheDir, path31.basename(face.file));
|
|
9949
|
+
const target = `./fonts/${path31.basename(face.file)}`;
|
|
9950
|
+
const format = FONT_FORMATS[path31.extname(face.file).toLowerCase()] ?? "truetype";
|
|
9698
9951
|
const family = face.family.replace(/['\\]/g, "").replace(/\*\//g, "");
|
|
9699
9952
|
const decl = `@font-face { font-family: '${family}'; font-weight: ${face.weight}; font-style: normal; src: url(${target}) format('${format}'); }`;
|
|
9700
9953
|
const license = normalizeFontLicense(face.license);
|
|
9701
|
-
const terms = REDISTRIBUTABLE.get(license);
|
|
9954
|
+
const terms = face.source.startsWith("system:") ? void 0 : REDISTRIBUTABLE.get(license);
|
|
9702
9955
|
if (terms === void 0) {
|
|
9703
|
-
if (face.source.startsWith("
|
|
9956
|
+
if (face.source.startsWith("system:")) {
|
|
9957
|
+
lines.push(
|
|
9958
|
+
`/* '${family}' ${face.weight} came from this machine's installed system fonts (tendril fonts`,
|
|
9959
|
+
` add-system; sha256 ${face.sha256.slice(0, 16)}\u2026). OS-bundled faces are never copied into`,
|
|
9960
|
+
" bundles regardless of any recorded licence \u2014 the consuming machine provides its own copy,",
|
|
9961
|
+
` or you place one you licence at ${target} and uncomment: */`,
|
|
9962
|
+
`/* ${decl} */`
|
|
9963
|
+
);
|
|
9964
|
+
} else if (face.source.startsWith("local:")) {
|
|
9704
9965
|
lines.push(
|
|
9705
9966
|
`/* '${family}' ${face.weight} is a user-licensed face (tendril fonts add; sha256 ${face.sha256.slice(0, 16)}\u2026).`,
|
|
9706
9967
|
` Licensed bytes are never copied into bundles \u2014 place your copy at ${target} and uncomment: */`,
|
|
@@ -9724,14 +9985,14 @@ function fontsCssFor(faces, bundleDir, cacheDir = DEFAULT_FONT_CACHE, substitute
|
|
|
9724
9985
|
`/* ${decl} */`
|
|
9725
9986
|
);
|
|
9726
9987
|
}
|
|
9727
|
-
} else if (
|
|
9728
|
-
mkdirSync7(
|
|
9729
|
-
copyFileSync(src,
|
|
9988
|
+
} else if (existsSync25(src) && createHash4("sha256").update(readFileSync21(src)).digest("hex") === face.sha256) {
|
|
9989
|
+
mkdirSync7(path31.join(bundleDir, "fonts"), { recursive: true });
|
|
9990
|
+
copyFileSync(src, path31.join(bundleDir, "fonts", path31.basename(face.file)));
|
|
9730
9991
|
licenseTexts.set(terms.file, terms.text);
|
|
9731
9992
|
const upstream = upstreamAttribution(face);
|
|
9732
9993
|
notices.push(
|
|
9733
9994
|
"",
|
|
9734
|
-
`${face.family.replace(/[\r\n]+/g, " ")} ${face.weight} \u2014 ./${
|
|
9995
|
+
`${face.family.replace(/[\r\n]+/g, " ")} ${face.weight} \u2014 ./${path31.basename(face.file)}`,
|
|
9735
9996
|
` licence: ${terms.title} \u2014 full text in ./${terms.file}`,
|
|
9736
9997
|
` source: ${face.source}`,
|
|
9737
9998
|
` sha256: ${face.sha256}`,
|
|
@@ -9745,9 +10006,9 @@ function fontsCssFor(faces, bundleDir, cacheDir = DEFAULT_FONT_CACHE, substitute
|
|
|
9745
10006
|
}
|
|
9746
10007
|
if (lines.length === 0) return null;
|
|
9747
10008
|
if (notices.length > 0) {
|
|
9748
|
-
const fontsDir =
|
|
9749
|
-
for (const [file, text] of licenseTexts) writeFileSync11(
|
|
9750
|
-
writeFileSync11(
|
|
10009
|
+
const fontsDir = path31.join(bundleDir, "fonts");
|
|
10010
|
+
for (const [file, text] of licenseTexts) writeFileSync11(path31.join(fontsDir, file), text);
|
|
10011
|
+
writeFileSync11(path31.join(fontsDir, "NOTICE.txt"), `${[NOTICE_PREAMBLE, ...notices].join("\n")}
|
|
9751
10012
|
`);
|
|
9752
10013
|
header.push(
|
|
9753
10014
|
"/* REDISTRIBUTION: the face files in ./fonts/ are redistributed under the licences",
|
|
@@ -9759,10 +10020,10 @@ function fontsCssFor(faces, bundleDir, cacheDir = DEFAULT_FONT_CACHE, substitute
|
|
|
9759
10020
|
`;
|
|
9760
10021
|
}
|
|
9761
10022
|
function countLatticeSymbols(setDir) {
|
|
9762
|
-
const manifestFile =
|
|
9763
|
-
if (
|
|
10023
|
+
const manifestFile = path31.join(setDir, "recording-set.json");
|
|
10024
|
+
if (existsSync25(manifestFile)) {
|
|
9764
10025
|
try {
|
|
9765
|
-
const stored = JSON.parse(
|
|
10026
|
+
const stored = JSON.parse(readFileSync21(manifestFile, "utf8"));
|
|
9766
10027
|
if (stored.variantScope !== "component-set") return null;
|
|
9767
10028
|
const lattice = stored.latticeNames;
|
|
9768
10029
|
if (lattice !== void 0 && lattice.length > 0) return lattice.length;
|
|
@@ -9770,13 +10031,13 @@ function countLatticeSymbols(setDir) {
|
|
|
9770
10031
|
}
|
|
9771
10032
|
}
|
|
9772
10033
|
const files = [
|
|
9773
|
-
|
|
9774
|
-
...
|
|
9775
|
-
].filter((f) =>
|
|
10034
|
+
path31.join(setDir, "get_metadata.json"),
|
|
10035
|
+
...existsSync25(setDir) ? readdirSync8(setDir).filter((f) => /^get_metadata-.*\.json$/.test(f)).map((f) => path31.join(setDir, f)) : []
|
|
10036
|
+
].filter((f) => existsSync25(f));
|
|
9776
10037
|
if (files.length === 0) return null;
|
|
9777
10038
|
let count = 0;
|
|
9778
10039
|
for (const f of files) {
|
|
9779
|
-
const text = envelopeTextContent(JSON.parse(
|
|
10040
|
+
const text = envelopeTextContent(JSON.parse(readFileSync21(f, "utf8")));
|
|
9780
10041
|
count += [...text.matchAll(/name="([^"]*)"/g)].filter((m) => m[1].includes("=")).length;
|
|
9781
10042
|
}
|
|
9782
10043
|
return count > 0 ? count : null;
|
|
@@ -9784,21 +10045,21 @@ function countLatticeSymbols(setDir) {
|
|
|
9784
10045
|
function recordingSetHash(setDir, configs) {
|
|
9785
10046
|
const relPaths = [];
|
|
9786
10047
|
for (const name of ["recording-set.json", "completeness-manifest.json", "get_variable_defs.json", "get_metadata.json"]) {
|
|
9787
|
-
if (
|
|
10048
|
+
if (existsSync25(path31.join(setDir, name))) relPaths.push(name);
|
|
9788
10049
|
}
|
|
9789
10050
|
for (const cfg of configs) {
|
|
9790
10051
|
for (const f of ["get_design_context.json", "get_metadata.json", "get_screenshot.json", "get_variable_defs.json"]) {
|
|
9791
|
-
if (
|
|
10052
|
+
if (existsSync25(path31.join(setDir, cfg.rep, f))) relPaths.push(`${cfg.rep}/${f}`);
|
|
9792
10053
|
}
|
|
9793
|
-
if (
|
|
9794
|
-
for (const asset of
|
|
10054
|
+
if (existsSync25(path31.join(setDir, cfg.rep))) {
|
|
10055
|
+
for (const asset of readdirSync8(path31.join(setDir, cfg.rep)).filter((f) => f.startsWith("asset-"))) {
|
|
9795
10056
|
relPaths.push(`${cfg.rep}/${asset}`);
|
|
9796
10057
|
}
|
|
9797
10058
|
}
|
|
9798
10059
|
}
|
|
9799
10060
|
return hashRecordingSet(
|
|
9800
10061
|
relPaths,
|
|
9801
|
-
(p) => new Uint8Array(
|
|
10062
|
+
(p) => new Uint8Array(readFileSync21(path31.join(setDir, p))),
|
|
9802
10063
|
(chunks) => {
|
|
9803
10064
|
const h = createHash4("sha256");
|
|
9804
10065
|
for (const c of chunks) h.update(c);
|
|
@@ -9819,10 +10080,11 @@ function emitBundleV1(opts) {
|
|
|
9819
10080
|
const certified = statuses.filter((s) => s.status === "certified").length;
|
|
9820
10081
|
const pass = statuses.filter((s) => s.status !== "fail").length;
|
|
9821
10082
|
const lattice = countLatticeSymbols(opts.task.set);
|
|
9822
|
-
const interaction = opts.behaviors.filter((b) => !b.id.startsWith("prelude:"));
|
|
10083
|
+
const interaction = opts.behaviors.filter((b) => !b.id.startsWith("prelude:") && !b.id.startsWith("parity:"));
|
|
9823
10084
|
const prelude = opts.behaviors.filter((b) => b.id.startsWith("prelude:"));
|
|
9824
|
-
const
|
|
9825
|
-
const
|
|
10085
|
+
const parity = opts.behaviors.filter((b) => b.id.startsWith("parity:"));
|
|
10086
|
+
const cssFiles = ["styles.css", "tokens.css"].map((f) => path31.join(opts.bundleDir, f)).filter((f) => existsSync25(f));
|
|
10087
|
+
const families = cssFontFamilies(cssFiles.map((f) => readFileSync21(f, "utf8")).join("\n"));
|
|
9826
10088
|
const requiredFonts = requiredFontsManifest(families, opts.fontCacheDir).map((f) => ({
|
|
9827
10089
|
family: f.family,
|
|
9828
10090
|
weight: f.weight,
|
|
@@ -9851,7 +10113,7 @@ function emitBundleV1(opts) {
|
|
|
9851
10113
|
// resolvable via verify's --set override).
|
|
9852
10114
|
path: (() => {
|
|
9853
10115
|
const base = process.env["INIT_CWD"] ?? process.cwd();
|
|
9854
|
-
const rel =
|
|
10116
|
+
const rel = path31.relative(base, opts.task.set);
|
|
9855
10117
|
return rel !== "" && !rel.startsWith("..") ? rel : opts.task.set;
|
|
9856
10118
|
})(),
|
|
9857
10119
|
component: opts.componentName,
|
|
@@ -9875,21 +10137,21 @@ function emitBundleV1(opts) {
|
|
|
9875
10137
|
})
|
|
9876
10138
|
};
|
|
9877
10139
|
const written = [];
|
|
9878
|
-
const manifestPath2 =
|
|
10140
|
+
const manifestPath2 = path31.join(opts.bundleDir, "component.json");
|
|
9879
10141
|
writeFileSync11(manifestPath2, `${JSON.stringify(manifest, null, 2)}
|
|
9880
10142
|
`);
|
|
9881
10143
|
written.push(manifestPath2);
|
|
9882
|
-
const stylesPath =
|
|
9883
|
-
if (
|
|
10144
|
+
const stylesPath = path31.join(opts.bundleDir, "styles.css");
|
|
10145
|
+
if (existsSync25(stylesPath)) {
|
|
9884
10146
|
const comment = cssProvenanceComment({ pass, scored: statuses.length, certified, latticeConfigs: lattice });
|
|
9885
|
-
const current =
|
|
10147
|
+
const current = readFileSync21(stylesPath, "utf8");
|
|
9886
10148
|
const stripped = current.replace(/^\/\* tendril bundle v\d+ [^]*?\*\/\n/, "");
|
|
9887
10149
|
writeFileSync11(stylesPath, `${comment}
|
|
9888
10150
|
${stripped}`);
|
|
9889
10151
|
written.push(stylesPath);
|
|
9890
10152
|
}
|
|
9891
|
-
const fontsCssPath =
|
|
9892
|
-
rmSync3(
|
|
10153
|
+
const fontsCssPath = path31.join(opts.bundleDir, "fonts.css");
|
|
10154
|
+
rmSync3(path31.join(opts.bundleDir, "fonts"), { recursive: true, force: true });
|
|
9893
10155
|
rmSync3(fontsCssPath, { force: true });
|
|
9894
10156
|
const fontsCss = fontsCssFor(requiredFontsManifest(families, opts.fontCacheDir), opts.bundleDir, opts.fontCacheDir, opts.substitutedFamilies ?? []);
|
|
9895
10157
|
if (fontsCss !== null) {
|
|
@@ -9897,7 +10159,7 @@ ${stripped}`);
|
|
|
9897
10159
|
written.push(fontsCssPath);
|
|
9898
10160
|
}
|
|
9899
10161
|
const unrecorded = lattice === null ? null : Math.max(0, lattice - statuses.length);
|
|
9900
|
-
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\``;
|
|
10162
|
+
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\``;
|
|
9901
10163
|
return { manifest, statusLine, written };
|
|
9902
10164
|
}
|
|
9903
10165
|
var FONT_FORMATS, BARS, NOTICE_PREAMBLE, OFL_1_1_TEXT, UFL_1_0_TEXT, APACHE_2_0_TEXT, REDISTRIBUTABLE;
|
|
@@ -10334,9 +10596,9 @@ var init_src7 = __esm({
|
|
|
10334
10596
|
});
|
|
10335
10597
|
|
|
10336
10598
|
// packages/cli/src/font-guidance.ts
|
|
10337
|
-
import
|
|
10599
|
+
import path32 from "node:path";
|
|
10338
10600
|
function fontsUnprovenRemediation(setDir) {
|
|
10339
|
-
const set = setDir === void 0 ? void 0 :
|
|
10601
|
+
const set = setDir === void 0 ? void 0 : path32.resolve(setDir);
|
|
10340
10602
|
if (set !== void 0) {
|
|
10341
10603
|
try {
|
|
10342
10604
|
const needs = recordedFontNeeds(set);
|
|
@@ -10404,13 +10666,15 @@ __export(fonts_exports, {
|
|
|
10404
10666
|
DEFAULT_FONT_CACHE: () => DEFAULT_FONT_CACHE,
|
|
10405
10667
|
familyMismatch: () => familyMismatch,
|
|
10406
10668
|
runFontsAdd: () => runFontsAdd,
|
|
10669
|
+
runFontsAddSystem: () => runFontsAddSystem,
|
|
10670
|
+
runFontsDiscover: () => runFontsDiscover,
|
|
10407
10671
|
runFontsRequired: () => runFontsRequired,
|
|
10408
10672
|
runFontsResolve: () => runFontsResolve,
|
|
10409
10673
|
runFontsResolveSet: () => runFontsResolveSet,
|
|
10410
10674
|
runFontsStatus: () => runFontsStatus
|
|
10411
10675
|
});
|
|
10412
|
-
import { existsSync as
|
|
10413
|
-
import
|
|
10676
|
+
import { existsSync as existsSync26, readFileSync as readFileSync22 } from "node:fs";
|
|
10677
|
+
import path33 from "node:path";
|
|
10414
10678
|
async function runFontsResolve(opts) {
|
|
10415
10679
|
const result = await resolveFonts(opts.family, opts.weights, opts.cacheDir);
|
|
10416
10680
|
emitData(opts, result, () => {
|
|
@@ -10425,7 +10689,7 @@ async function runFontsResolve(opts) {
|
|
|
10425
10689
|
}
|
|
10426
10690
|
}
|
|
10427
10691
|
async function runFontsResolveSet(opts) {
|
|
10428
|
-
const setDir =
|
|
10692
|
+
const setDir = path33.resolve(process.env["INIT_CWD"] ?? process.cwd(), opts.set);
|
|
10429
10693
|
let needs = [];
|
|
10430
10694
|
try {
|
|
10431
10695
|
needs = recordedFontNeeds(setDir);
|
|
@@ -10488,6 +10752,12 @@ async function runFontsResolveSet(opts) {
|
|
|
10488
10752
|
`);
|
|
10489
10753
|
for (const f of failures) process.stdout.write(`FAILED ${f.family} ${f.weight}: ${f.reason}
|
|
10490
10754
|
`);
|
|
10755
|
+
for (const fam of [...new Set(failures.map((f) => f.family))]) {
|
|
10756
|
+
process.stdout.write(
|
|
10757
|
+
` 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)
|
|
10758
|
+
`
|
|
10759
|
+
);
|
|
10760
|
+
}
|
|
10491
10761
|
});
|
|
10492
10762
|
if (byteDrift.length > 0) {
|
|
10493
10763
|
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`);
|
|
@@ -10498,16 +10768,16 @@ async function runFontsResolveSet(opts) {
|
|
|
10498
10768
|
}
|
|
10499
10769
|
}
|
|
10500
10770
|
function runFontsStatus(opts) {
|
|
10501
|
-
const manifestPath2 =
|
|
10502
|
-
if (!
|
|
10771
|
+
const manifestPath2 = path33.join(opts.cacheDir, "manifest.json");
|
|
10772
|
+
if (!existsSync26(manifestPath2)) {
|
|
10503
10773
|
fail(opts, ExitCode.FontsUnproven, {
|
|
10504
10774
|
error: `no font cache at ${opts.cacheDir}`,
|
|
10505
10775
|
code: "fonts-unresolved",
|
|
10506
10776
|
remediation: `Run \`${tendrilCommand("fonts resolve --set <recording-dir>")}\` (or \`${tendrilCommand('fonts resolve "<Family>" --weights 400 500 600')}\`) first.`
|
|
10507
10777
|
});
|
|
10508
10778
|
}
|
|
10509
|
-
const faces = JSON.parse(
|
|
10510
|
-
const lockVerdicts = opts.lock !== void 0 ? checkFontLock(
|
|
10779
|
+
const faces = JSON.parse(readFileSync22(manifestPath2, "utf8"));
|
|
10780
|
+
const lockVerdicts = opts.lock !== void 0 ? checkFontLock(path33.resolve(opts.lock), opts.cacheDir) : null;
|
|
10511
10781
|
emitData(opts, { faces, lockVerdicts }, () => {
|
|
10512
10782
|
for (const f of faces) process.stdout.write(`cached ${f.family} ${f.weight} (${f.sha256.slice(0, 12)}\u2026)
|
|
10513
10783
|
`);
|
|
@@ -10551,13 +10821,13 @@ function familyMismatch(family, declared) {
|
|
|
10551
10821
|
}
|
|
10552
10822
|
function runFontsAdd(opts) {
|
|
10553
10823
|
if (opts.set !== void 0) {
|
|
10554
|
-
const declared = taskFontFamilies(
|
|
10824
|
+
const declared = taskFontFamilies(path33.resolve(opts.set)) ?? [];
|
|
10555
10825
|
const mismatch = familyMismatch(opts.family, declared);
|
|
10556
10826
|
if (mismatch !== void 0) {
|
|
10557
10827
|
fail(opts, ExitCode.InputValidation, {
|
|
10558
10828
|
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.`,
|
|
10559
10829
|
code: "font-family-not-declared",
|
|
10560
|
-
remediation: mismatch.nearest !== void 0 ? `Did you mean "${mismatch.nearest}"? ${tendrilCommand(`fonts add "${mismatch.nearest}" ${opts.weight} ${opts.file} --set ${
|
|
10830
|
+
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.`
|
|
10561
10831
|
});
|
|
10562
10832
|
}
|
|
10563
10833
|
} else {
|
|
@@ -10584,6 +10854,73 @@ function runFontsAdd(opts) {
|
|
|
10584
10854
|
`);
|
|
10585
10855
|
});
|
|
10586
10856
|
}
|
|
10857
|
+
function runFontsDiscover(opts) {
|
|
10858
|
+
const faces = opts.family !== void 0 ? systemFacesForFamily(opts.family) : discoverSystemFaces();
|
|
10859
|
+
const rows = faces.map((f) => ({ ...f, ...systemFaceRefusal(f.family) !== void 0 ? { refused: systemFaceRefusal(f.family) } : {} }));
|
|
10860
|
+
emitData(opts, { faces: rows, dirs: systemFontDirs() }, () => {
|
|
10861
|
+
if (rows.length === 0) {
|
|
10862
|
+
process.stdout.write(`no matching system faces found (scanned: ${systemFontDirs().join(", ")})
|
|
10863
|
+
`);
|
|
10864
|
+
return;
|
|
10865
|
+
}
|
|
10866
|
+
for (const f of rows) {
|
|
10867
|
+
const flags = [f.refused !== void 0 ? "REFUSED" : "", f.italic ? "italic" : "", f.variable ? "variable" : ""].filter((x) => x !== "").join(" ");
|
|
10868
|
+
process.stdout.write(`${f.family} \u2014 ${f.subfamily || "Regular"} (${f.weight})${flags === "" ? "" : ` [${flags}]`} ${f.file}${f.faceIndex !== void 0 ? ` #${f.faceIndex}` : ""}
|
|
10869
|
+
`);
|
|
10870
|
+
}
|
|
10871
|
+
const eligible = rows.filter((f) => f.refused === void 0 && !f.italic && !f.variable);
|
|
10872
|
+
if (eligible.length > 0 && opts.family !== void 0) {
|
|
10873
|
+
process.stdout.write(`
|
|
10874
|
+
cache them: ${tendrilCommand(`fonts add-system ${quoteArg(opts.family)}`)}
|
|
10875
|
+
`);
|
|
10876
|
+
}
|
|
10877
|
+
});
|
|
10878
|
+
}
|
|
10879
|
+
function runFontsAddSystem(opts) {
|
|
10880
|
+
if (opts.set !== void 0) {
|
|
10881
|
+
const declared = taskFontFamilies(path33.resolve(opts.set)) ?? [];
|
|
10882
|
+
const mismatch = familyMismatch(opts.family, declared);
|
|
10883
|
+
if (mismatch !== void 0) {
|
|
10884
|
+
fail(opts, ExitCode.InputValidation, {
|
|
10885
|
+
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.`,
|
|
10886
|
+
code: "font-family-not-declared",
|
|
10887
|
+
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.`
|
|
10888
|
+
});
|
|
10889
|
+
}
|
|
10890
|
+
} else {
|
|
10891
|
+
warn(
|
|
10892
|
+
opts,
|
|
10893
|
+
`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`
|
|
10894
|
+
);
|
|
10895
|
+
}
|
|
10896
|
+
const result = addSystemFamily(opts.family, { cacheDir: opts.cacheDir, ...opts.weights !== void 0 ? { weights: opts.weights } : {} });
|
|
10897
|
+
if (result.refusal !== void 0) {
|
|
10898
|
+
fail(opts, ExitCode.InputValidation, {
|
|
10899
|
+
error: `"${opts.family}" is a refusal-class family: ${result.refusal}`,
|
|
10900
|
+
code: "font-family-refused",
|
|
10901
|
+
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')}.`
|
|
10902
|
+
});
|
|
10903
|
+
}
|
|
10904
|
+
if (result.added.length === 0) {
|
|
10905
|
+
fail(opts, ExitCode.InputValidation, {
|
|
10906
|
+
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`,
|
|
10907
|
+
code: "font-family-not-installed",
|
|
10908
|
+
remediation: `See what IS installed: ${tendrilCommand(`fonts discover ${quoteArg(opts.family)}`)} (or all faces with ${tendrilCommand("fonts discover")}).`
|
|
10909
|
+
});
|
|
10910
|
+
}
|
|
10911
|
+
for (const o of result.overwrote) {
|
|
10912
|
+
warn(
|
|
10913
|
+
opts,
|
|
10914
|
+
`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`
|
|
10915
|
+
);
|
|
10916
|
+
}
|
|
10917
|
+
emitData(opts, result, () => {
|
|
10918
|
+
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
|
|
10919
|
+
`);
|
|
10920
|
+
for (const s of result.skipped) process.stdout.write(`skipped ${s.subfamily || "Regular"} ${s.weight}: ${s.reason}
|
|
10921
|
+
`);
|
|
10922
|
+
});
|
|
10923
|
+
}
|
|
10587
10924
|
var init_fonts = __esm({
|
|
10588
10925
|
"packages/cli/src/commands/fonts.ts"() {
|
|
10589
10926
|
"use strict";
|
|
@@ -10614,15 +10951,24 @@ __export(verify_exports, {
|
|
|
10614
10951
|
resolveComposition: () => resolveComposition,
|
|
10615
10952
|
runVerify: () => runVerify
|
|
10616
10953
|
});
|
|
10617
|
-
import { existsSync as
|
|
10618
|
-
import
|
|
10954
|
+
import { existsSync as existsSync27, readFileSync as readFileSync23 } from "node:fs";
|
|
10955
|
+
import path34 from "node:path";
|
|
10619
10956
|
function interactionCoverage(behaviors) {
|
|
10620
|
-
const
|
|
10957
|
+
const parity = behaviors.filter((b) => b.id.startsWith("parity:"));
|
|
10958
|
+
const interaction = behaviors.filter((b) => !b.id.startsWith("prelude:") && !b.id.startsWith("parity:"));
|
|
10621
10959
|
return {
|
|
10622
10960
|
interactionChecks: interaction.length,
|
|
10623
10961
|
interactionPassed: interaction.filter((b) => b.pass).length,
|
|
10624
|
-
preludeChecks: behaviors.length - interaction.length,
|
|
10625
|
-
|
|
10962
|
+
preludeChecks: behaviors.length - interaction.length - parity.length,
|
|
10963
|
+
parityChecks: parity.length,
|
|
10964
|
+
parityPassed: parity.filter((b) => b.pass).length,
|
|
10965
|
+
// Asymmetric on purpose (review finding F3): a parity PASS never
|
|
10966
|
+
// makes operability "verified" (parity is pixel-parity, not
|
|
10967
|
+
// someone typing or clicking — the §0j miscount), but a parity
|
|
10968
|
+
// FAIL still forbids it — "verified" printed beside a broken hover
|
|
10969
|
+
// state is the one word doing too much lifting again, from the
|
|
10970
|
+
// other direction.
|
|
10971
|
+
operability: interaction.length > 0 && interaction.every((b) => b.pass) && parity.every((b) => b.pass) ? "verified" : "unverified"
|
|
10626
10972
|
};
|
|
10627
10973
|
}
|
|
10628
10974
|
function operabilityReport(input) {
|
|
@@ -10655,6 +11001,16 @@ function operabilityReport(input) {
|
|
|
10655
11001
|
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.`
|
|
10656
11002
|
};
|
|
10657
11003
|
}
|
|
11004
|
+
const { parityChecks, parityPassed } = interactionCoverage(input.behaviors);
|
|
11005
|
+
if (parityPassed < parityChecks) {
|
|
11006
|
+
const failed = parityChecks - parityPassed;
|
|
11007
|
+
return {
|
|
11008
|
+
checks: interactionChecks,
|
|
11009
|
+
passed: interactionPassed,
|
|
11010
|
+
short: `${failed} of ${parityChecks} state-parity check(s) failed`,
|
|
11011
|
+
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.`
|
|
11012
|
+
};
|
|
11013
|
+
}
|
|
10658
11014
|
return { checks: interactionChecks, passed: interactionPassed };
|
|
10659
11015
|
}
|
|
10660
11016
|
function operabilityLine(state) {
|
|
@@ -10747,7 +11103,7 @@ function compositionReport(input) {
|
|
|
10747
11103
|
function eyeCheck(bundleDir) {
|
|
10748
11104
|
return {
|
|
10749
11105
|
command: tendrilCommand(`inspect "${bundleDir}"`),
|
|
10750
|
-
sheetPath:
|
|
11106
|
+
sheetPath: path34.join(bundleDir, "verify-evidence", "inspect.html"),
|
|
10751
11107
|
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."
|
|
10752
11108
|
};
|
|
10753
11109
|
}
|
|
@@ -10759,7 +11115,7 @@ function taskFromManifest(opts, manifest, setDir) {
|
|
|
10759
11115
|
const adapterSlugs = Object.keys(manifest.propAdapter);
|
|
10760
11116
|
const unmapped = recordedSlugs.filter((s) => !adapterSlugs.includes(s));
|
|
10761
11117
|
const adapterOnly = adapterSlugs.filter((s) => !recordedSlugs.includes(s));
|
|
10762
|
-
const registry = Object.values(TASKS).find((t) =>
|
|
11118
|
+
const registry = Object.values(TASKS).find((t) => path34.resolve(t.set) === path34.resolve(setDir));
|
|
10763
11119
|
const authored = (() => {
|
|
10764
11120
|
if (registry !== void 0) return void 0;
|
|
10765
11121
|
try {
|
|
@@ -10785,6 +11141,10 @@ function taskFromManifest(opts, manifest, setDir) {
|
|
|
10785
11141
|
task,
|
|
10786
11142
|
unmapped,
|
|
10787
11143
|
adapterOnly,
|
|
11144
|
+
// The parity AUTHORITY: CLI-owned configs for the same poses —
|
|
11145
|
+
// recording-derived (authored) or registry-declared — never the
|
|
11146
|
+
// adapter, which is the graded artifact framing itself (§0j).
|
|
11147
|
+
authorityConfigs: behaviorSource.configs,
|
|
10788
11148
|
// Registry sets carry hand-declared behaviors and no derivation
|
|
10789
11149
|
// runs, so their evidence is UNKNOWN, not empty: an empty list is
|
|
10790
11150
|
// spent downstream as "the recording holds no interactive pose".
|
|
@@ -10794,19 +11154,19 @@ function taskFromManifest(opts, manifest, setDir) {
|
|
|
10794
11154
|
}
|
|
10795
11155
|
async function runVerify(opts) {
|
|
10796
11156
|
const callerCwd = process.env["INIT_CWD"] ?? process.cwd();
|
|
10797
|
-
const setOverride = opts.set !== void 0 ?
|
|
10798
|
-
opts = { ...opts, bundleDir:
|
|
10799
|
-
if (!
|
|
11157
|
+
const setOverride = opts.set !== void 0 ? path34.resolve(callerCwd, opts.set) : void 0;
|
|
11158
|
+
opts = { ...opts, bundleDir: path34.resolve(callerCwd, opts.bundleDir), ...setOverride !== void 0 ? { set: setOverride } : {} };
|
|
11159
|
+
if (!existsSync27(opts.bundleDir)) {
|
|
10800
11160
|
fail(opts, ExitCode.InputValidation, {
|
|
10801
11161
|
error: `bundle directory not found: ${opts.bundleDir}`,
|
|
10802
11162
|
code: "bundle-missing",
|
|
10803
11163
|
remediation: "Point at a generated bundle directory (containing component.json, the entry module, and styles.css)."
|
|
10804
11164
|
});
|
|
10805
11165
|
}
|
|
10806
|
-
const manifestPath2 =
|
|
11166
|
+
const manifestPath2 = path34.join(opts.bundleDir, "component.json");
|
|
10807
11167
|
let manifest;
|
|
10808
|
-
if (
|
|
10809
|
-
const { manifest: parsed, issues } = readBundleManifest(
|
|
11168
|
+
if (existsSync27(manifestPath2)) {
|
|
11169
|
+
const { manifest: parsed, issues } = readBundleManifest(readFileSync23(manifestPath2, "utf8"));
|
|
10810
11170
|
if (issues.length > 0) {
|
|
10811
11171
|
fail(opts, ExitCode.InputValidation, {
|
|
10812
11172
|
error: `bundle manifest rejected at ingest: ${issues.map((i) => i.message).join("; ")}`,
|
|
@@ -10820,6 +11180,7 @@ async function runVerify(opts) {
|
|
|
10820
11180
|
let unmapped = [];
|
|
10821
11181
|
let interactionEvidence;
|
|
10822
11182
|
let unmappedInteractionEvidence = [];
|
|
11183
|
+
let authorityConfigs;
|
|
10823
11184
|
let availability = ROLES_NOT_RESOLVED;
|
|
10824
11185
|
if (opts.task !== void 0) {
|
|
10825
11186
|
const registry = TASKS[opts.task];
|
|
@@ -10833,21 +11194,21 @@ async function runVerify(opts) {
|
|
|
10833
11194
|
task = registry;
|
|
10834
11195
|
} else if (manifest !== void 0) {
|
|
10835
11196
|
const resolveSetDir = (p) => {
|
|
10836
|
-
if (
|
|
10837
|
-
const fromRepo =
|
|
10838
|
-
if (
|
|
10839
|
-
return
|
|
11197
|
+
if (path34.isAbsolute(p)) return p;
|
|
11198
|
+
const fromRepo = path34.resolve(REPO_ROOT, p);
|
|
11199
|
+
if (existsSync27(fromRepo)) return fromRepo;
|
|
11200
|
+
return path34.resolve(callerCwd, p);
|
|
10840
11201
|
};
|
|
10841
11202
|
const setDir = opts.set ?? resolveSetDir(manifest.provenance.recordingSet.path);
|
|
10842
|
-
if (!
|
|
11203
|
+
if (!existsSync27(path34.join(setDir, "recording-set.json")) && !Object.values(TASKS).some((t) => path34.resolve(t.set) === path34.resolve(setDir))) {
|
|
10843
11204
|
fail(opts, ExitCode.RecordingIncomplete, {
|
|
10844
11205
|
error: `recording set not found or unmanifested: ${setDir}`,
|
|
10845
11206
|
code: "recording-set-missing",
|
|
10846
11207
|
remediation: "Pass --set <dir> pointing at the recording set this bundle was generated from."
|
|
10847
11208
|
});
|
|
10848
11209
|
}
|
|
10849
|
-
const registry = Object.values(TASKS).find((t) =>
|
|
10850
|
-
if (registry !== void 0 && !
|
|
11210
|
+
const registry = Object.values(TASKS).find((t) => path34.resolve(t.set) === path34.resolve(setDir));
|
|
11211
|
+
if (registry !== void 0 && !existsSync27(path34.join(setDir, "recording-set.json"))) {
|
|
10851
11212
|
const recordedSlugs = registry.configs.map((c) => c.rep);
|
|
10852
11213
|
const adapterSlugs = Object.keys(manifest.propAdapter);
|
|
10853
11214
|
unmapped = recordedSlugs.filter((s) => !adapterSlugs.includes(s));
|
|
@@ -10856,10 +11217,12 @@ async function runVerify(opts) {
|
|
|
10856
11217
|
entry: manifest.entry,
|
|
10857
11218
|
configs: adapterSlugs.filter((s) => recordedSlugs.includes(s)).map((rep) => ({ rep, component: manifest.propAdapter[rep].component, props: manifest.propAdapter[rep].props }))
|
|
10858
11219
|
};
|
|
11220
|
+
authorityConfigs = registry.configs;
|
|
10859
11221
|
} else {
|
|
10860
11222
|
const built = taskFromManifest(opts, manifest, setDir);
|
|
10861
11223
|
task = built.task;
|
|
10862
11224
|
unmapped = built.unmapped;
|
|
11225
|
+
authorityConfigs = built.authorityConfigs;
|
|
10863
11226
|
interactionEvidence = built.interactionEvidence;
|
|
10864
11227
|
unmappedInteractionEvidence = built.unmappedInteractionEvidence;
|
|
10865
11228
|
for (const s of built.adapterOnly) warn(opts, `prop adapter maps "${s}" which is not in the recording set \u2014 ignored`);
|
|
@@ -10871,9 +11234,9 @@ async function runVerify(opts) {
|
|
|
10871
11234
|
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`);
|
|
10872
11235
|
}
|
|
10873
11236
|
for (const name of [manifest.entry, "styles.css", "tokens.css"]) {
|
|
10874
|
-
const p =
|
|
10875
|
-
if (!
|
|
10876
|
-
const issues = checkBundleSourceFile(name, new Uint8Array(
|
|
11237
|
+
const p = path34.join(opts.bundleDir, name);
|
|
11238
|
+
if (!existsSync27(p)) continue;
|
|
11239
|
+
const issues = checkBundleSourceFile(name, new Uint8Array(readFileSync23(p)));
|
|
10877
11240
|
if (issues.length > 0) {
|
|
10878
11241
|
fail(opts, ExitCode.InputValidation, {
|
|
10879
11242
|
error: `bundle source rejected at ingest: ${issues.map((i) => i.message).join("; ")}`,
|
|
@@ -10911,7 +11274,7 @@ async function runVerify(opts) {
|
|
|
10911
11274
|
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)`);
|
|
10912
11275
|
}
|
|
10913
11276
|
const missing = task.configs.filter(
|
|
10914
|
-
(c) => !
|
|
11277
|
+
(c) => !existsSync27(path34.join(task.set, c.rep, "get_screenshot.json")) || !existsSync27(path34.join(task.set, c.rep, "get_metadata.json"))
|
|
10915
11278
|
);
|
|
10916
11279
|
if (missing.length > 0) {
|
|
10917
11280
|
fail(opts, ExitCode.RecordingIncomplete, {
|
|
@@ -10921,12 +11284,12 @@ async function runVerify(opts) {
|
|
|
10921
11284
|
});
|
|
10922
11285
|
}
|
|
10923
11286
|
const bar = BARS2[opts.bar];
|
|
10924
|
-
const evidenceDir =
|
|
11287
|
+
const evidenceDir = path34.join(opts.bundleDir, "verify-evidence");
|
|
10925
11288
|
const scores = await scoreBundleForTask(task, opts.bundleDir, bar, { evidenceDir, onProgress: emitProgress });
|
|
10926
11289
|
const quality = await checkBundleQuality(opts.bundleDir, task.entry, { dir: task.set, reps: task.configs.map((c) => c.rep) });
|
|
10927
|
-
const bundleCss = ["tokens.css", "styles.css"].map((f) =>
|
|
11290
|
+
const bundleCss = ["tokens.css", "styles.css"].map((f) => path34.join(opts.bundleDir, f)).filter((f) => existsSync27(f)).map((f) => readFileSync23(f, "utf8")).join("\n");
|
|
10928
11291
|
const occlusion = await checkSiblingOcclusion(task, opts.bundleDir, bundleCss);
|
|
10929
|
-
const parity = await checkHoverParity(task, opts.bundleDir);
|
|
11292
|
+
const parity = await checkHoverParity(task, opts.bundleDir, authorityConfigs ?? task.configs);
|
|
10930
11293
|
const behaviors = [...await checkBehaviors(task, opts.bundleDir), ...parity];
|
|
10931
11294
|
const structural = roles !== void 0 ? await checkStructuralComposition(task, opts.bundleDir, roles) : [];
|
|
10932
11295
|
const regionsOut = roles !== void 0 ? interiorRegions(task.set, roles) : void 0;
|
|
@@ -11116,7 +11479,7 @@ ${certified}/${statuses.length} certified \xB7 ${report.coverage.pass}/${statuse
|
|
|
11116
11479
|
}
|
|
11117
11480
|
process.stdout.write(`environment: chrome=${report.environment.chromeVersion ?? report.environment.chrome} fonts=${report.environment.fontsManifestSha256 ?? "UNRESOLVED"}
|
|
11118
11481
|
`);
|
|
11119
|
-
const shippedFonts = requiredFontsManifest(cssFontFamilies(["styles.css", "tokens.css"].map((f) =>
|
|
11482
|
+
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")));
|
|
11120
11483
|
if (shippedFonts.length > 0 && substitutedFamilies.length === 0) {
|
|
11121
11484
|
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)
|
|
11122
11485
|
`);
|
|
@@ -11206,18 +11569,18 @@ __export(engine_exports, {
|
|
|
11206
11569
|
runEngineBrief: () => runEngineBrief,
|
|
11207
11570
|
runEngineScore: () => runEngineScore
|
|
11208
11571
|
});
|
|
11209
|
-
import { appendFileSync, existsSync as
|
|
11210
|
-
import
|
|
11572
|
+
import { appendFileSync, existsSync as existsSync28, mkdirSync as mkdirSync8, readFileSync as readFileSync24, writeFileSync as writeFileSync12 } from "node:fs";
|
|
11573
|
+
import path35 from "node:path";
|
|
11211
11574
|
function resolveEngineTask(opts, callerCwd) {
|
|
11212
|
-
const asPath =
|
|
11213
|
-
const isSet =
|
|
11575
|
+
const asPath = path35.resolve(callerCwd, opts.taskOrSet);
|
|
11576
|
+
const isSet = existsSync28(path35.join(asPath, "recording-set.json"));
|
|
11214
11577
|
const registry = TASKS[opts.taskOrSet];
|
|
11215
11578
|
if (registry !== void 0 && !isSet) return { task: registry, name: opts.taskOrSet, ref: opts.taskOrSet, disclosures: [], interactionEvidence: void 0 };
|
|
11216
11579
|
if (isSet) {
|
|
11217
11580
|
try {
|
|
11218
11581
|
const authored = authorTaskFromSet(asPath);
|
|
11219
11582
|
for (const d of authored.disclosures) warn(opts, d);
|
|
11220
|
-
return { task: authored.task, name:
|
|
11583
|
+
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 } };
|
|
11221
11584
|
} catch (err) {
|
|
11222
11585
|
fail(opts, ExitCode.InputValidation, {
|
|
11223
11586
|
error: `cannot author a task from ${asPath}: ${err instanceof Error ? err.message.split("\n")[0] : String(err)}`,
|
|
@@ -11244,9 +11607,9 @@ ${disclosures.map((d) => `- ${d}`).join("\n")}` : "";
|
|
|
11244
11607
|
const brief = buildBrief(task.systemApi, bar, { colorScheme: recordingIsDark(task) ? "dark" : "light" }) + disclosureBlock;
|
|
11245
11608
|
const segments = buildSegments(task, "files");
|
|
11246
11609
|
let notRecorded;
|
|
11247
|
-
const manifestPath2 =
|
|
11248
|
-
if (
|
|
11249
|
-
notRecorded = JSON.parse(
|
|
11610
|
+
const manifestPath2 = path35.join(task.set, "recording-set.json");
|
|
11611
|
+
if (existsSync28(manifestPath2)) {
|
|
11612
|
+
notRecorded = JSON.parse(readFileSync24(manifestPath2, "utf8")).notRecorded;
|
|
11250
11613
|
}
|
|
11251
11614
|
const unverified = notRecorded !== void 0 && notRecorded !== "" ? `
|
|
11252
11615
|
|
|
@@ -11254,7 +11617,7 @@ ${disclosures.map((d) => `- ${d}`).join("\n")}` : "";
|
|
|
11254
11617
|
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.
|
|
11255
11618
|
${notRecorded}` : "";
|
|
11256
11619
|
let fontProvisioning;
|
|
11257
|
-
if (
|
|
11620
|
+
if (existsSync28(manifestPath2)) {
|
|
11258
11621
|
const missingFams = unprovisionedFamilies(task.set);
|
|
11259
11622
|
const unprovided = unprovisionedFaces(task.set);
|
|
11260
11623
|
const weightOnly = missingFams.length === 0;
|
|
@@ -11280,9 +11643,9 @@ ${notRecorded}` : "";
|
|
|
11280
11643
|
|
|
11281
11644
|
=== TASK PAYLOAD (recorded truth, verbatim) ===
|
|
11282
11645
|
${segments}`;
|
|
11283
|
-
const payloadFile =
|
|
11284
|
-
const candidateDirSuggestion =
|
|
11285
|
-
mkdirSync8(
|
|
11646
|
+
const payloadFile = path35.resolve(callerCwd, opts.out ?? `tendril-out/${name}-brief.md`);
|
|
11647
|
+
const candidateDirSuggestion = path35.resolve(callerCwd, `tendril-out/${name}-candidate`);
|
|
11648
|
+
mkdirSync8(path35.dirname(payloadFile), { recursive: true });
|
|
11286
11649
|
writeFileSync12(payloadFile, payload);
|
|
11287
11650
|
emitData(
|
|
11288
11651
|
opts,
|
|
@@ -11333,15 +11696,15 @@ ${segments}`;
|
|
|
11333
11696
|
);
|
|
11334
11697
|
}
|
|
11335
11698
|
function appendScoreHistory(candidateDir, entry) {
|
|
11336
|
-
appendFileSync(
|
|
11699
|
+
appendFileSync(path35.join(candidateDir, "score-history.jsonl"), `${JSON.stringify({ at: (/* @__PURE__ */ new Date()).toISOString(), ...entry })}
|
|
11337
11700
|
`);
|
|
11338
11701
|
}
|
|
11339
11702
|
async function runEngineScore(opts) {
|
|
11340
11703
|
requireEntitlement(opts);
|
|
11341
11704
|
const callerCwd = process.env["INIT_CWD"] ?? process.cwd();
|
|
11342
|
-
const candidateDir =
|
|
11705
|
+
const candidateDir = path35.resolve(callerCwd, opts.candidateDir);
|
|
11343
11706
|
const { task, name, apiPin, interactionEvidence } = resolveEngineTask(opts, callerCwd);
|
|
11344
|
-
if (!
|
|
11707
|
+
if (!existsSync28(candidateDir)) {
|
|
11345
11708
|
fail(opts, ExitCode.InputValidation, {
|
|
11346
11709
|
error: `candidate directory not found: ${candidateDir}`,
|
|
11347
11710
|
code: "candidate-missing",
|
|
@@ -11366,10 +11729,10 @@ async function runEngineScore(opts) {
|
|
|
11366
11729
|
for (const g of missingWeights(task.set)) {
|
|
11367
11730
|
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)`);
|
|
11368
11731
|
}
|
|
11369
|
-
if (opts.rebind !== true &&
|
|
11732
|
+
if (opts.rebind !== true && existsSync28(path35.join(candidateDir, "component.json"))) {
|
|
11370
11733
|
const prior = (() => {
|
|
11371
11734
|
try {
|
|
11372
|
-
const read = readBundleManifest(
|
|
11735
|
+
const read = readBundleManifest(readFileSync24(path35.join(candidateDir, "component.json"), "utf8"));
|
|
11373
11736
|
return read.manifest === void 0 ? { unreadable: true } : { hash: read.manifest.provenance.recordingSet.hash, path: read.manifest.provenance.recordingSet.path };
|
|
11374
11737
|
} catch {
|
|
11375
11738
|
return { unreadable: true };
|
|
@@ -11391,9 +11754,9 @@ async function runEngineScore(opts) {
|
|
|
11391
11754
|
}
|
|
11392
11755
|
}
|
|
11393
11756
|
const bar = BARS3[opts.bar];
|
|
11394
|
-
const evidenceDir =
|
|
11757
|
+
const evidenceDir = path35.join(candidateDir, "verify-evidence");
|
|
11395
11758
|
const scores = await scoreBundleForTask(task, candidateDir, bar, { evidenceDir, onProgress: emitProgress });
|
|
11396
|
-
const parity = await checkHoverParity(task, candidateDir);
|
|
11759
|
+
const parity = await checkHoverParity(task, candidateDir, task.configs);
|
|
11397
11760
|
const behaviors = [...await checkBehaviors(task, candidateDir), ...parity];
|
|
11398
11761
|
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)";
|
|
11399
11762
|
const obj = objective(scores, behaviors);
|
|
@@ -11434,7 +11797,17 @@ METRIC DEADBAND (read before iterating on near-misses): the scored similarity/in
|
|
|
11434
11797
|
environment: environmentStamp(taskFontFamilies(task.set)),
|
|
11435
11798
|
substitutedFamilies
|
|
11436
11799
|
});
|
|
11437
|
-
appendScoreHistory(candidateDir, {
|
|
11800
|
+
appendScoreHistory(candidateDir, {
|
|
11801
|
+
event: "round-scored",
|
|
11802
|
+
bar: opts.bar,
|
|
11803
|
+
pass: obj[0],
|
|
11804
|
+
total,
|
|
11805
|
+
pixelConfigsPassing: scores.filter((s) => s.pass).length,
|
|
11806
|
+
pixelConfigs: scores.length,
|
|
11807
|
+
certified: certifiedReps.length,
|
|
11808
|
+
floor: obj[1],
|
|
11809
|
+
mean: obj[2]
|
|
11810
|
+
});
|
|
11438
11811
|
const coverage = interactionCoverage(behaviors);
|
|
11439
11812
|
const operability = operabilityReport({ behaviors, interactionEvidence });
|
|
11440
11813
|
const evidenceUnverified = (interactionEvidence?.length ?? 0) > 0 && coverage.interactionChecks === 0;
|
|
@@ -11549,11 +11922,11 @@ var codeconnect_exports = {};
|
|
|
11549
11922
|
__export(codeconnect_exports, {
|
|
11550
11923
|
runCodeConnect: () => runCodeConnect
|
|
11551
11924
|
});
|
|
11552
|
-
import { existsSync as
|
|
11553
|
-
import
|
|
11925
|
+
import { existsSync as existsSync29, readFileSync as readFileSync25, writeFileSync as writeFileSync13 } from "node:fs";
|
|
11926
|
+
import path36 from "node:path";
|
|
11554
11927
|
function runCodeConnect(opts) {
|
|
11555
11928
|
const callerCwd = process.env["INIT_CWD"] ?? process.cwd();
|
|
11556
|
-
const bundleDir =
|
|
11929
|
+
const bundleDir = path36.resolve(callerCwd, opts.bundleDir);
|
|
11557
11930
|
let url;
|
|
11558
11931
|
try {
|
|
11559
11932
|
url = new URL(opts.figmaUrl);
|
|
@@ -11569,7 +11942,7 @@ function runCodeConnect(opts) {
|
|
|
11569
11942
|
}
|
|
11570
11943
|
let manifest;
|
|
11571
11944
|
try {
|
|
11572
|
-
const read = readBundleManifest(
|
|
11945
|
+
const read = readBundleManifest(readFileSync25(path36.join(bundleDir, "component.json"), "utf8"));
|
|
11573
11946
|
if (read.manifest === void 0) throw new Error(read.issues.map((i) => i.message).join("; ") || "no component.json");
|
|
11574
11947
|
manifest = read.manifest;
|
|
11575
11948
|
} catch (err) {
|
|
@@ -11579,8 +11952,8 @@ function runCodeConnect(opts) {
|
|
|
11579
11952
|
remediation: "Point at a bundle produced by the Tendril pipeline (it carries component.json), and verify it first."
|
|
11580
11953
|
});
|
|
11581
11954
|
}
|
|
11582
|
-
const setDir =
|
|
11583
|
-
if (!
|
|
11955
|
+
const setDir = path36.resolve(callerCwd, opts.set ?? manifest.provenance.recordingSet.path);
|
|
11956
|
+
if (!existsSync29(path36.join(setDir, "recording-set.json"))) {
|
|
11584
11957
|
fail(opts, ExitCode.InputValidation, {
|
|
11585
11958
|
error: `recording set not found at ${setDir}`,
|
|
11586
11959
|
code: "codeconnect-no-set",
|
|
@@ -11601,10 +11974,10 @@ function runCodeConnect(opts) {
|
|
|
11601
11974
|
const component = api.component;
|
|
11602
11975
|
const recManifest = loadManifest(setDir);
|
|
11603
11976
|
const poseNames = recManifest.latticeNames ?? recManifest.reps.map((r) => {
|
|
11604
|
-
const meta =
|
|
11605
|
-
if (!
|
|
11977
|
+
const meta = path36.join(setDir, r.slug, "get_metadata.json");
|
|
11978
|
+
if (!existsSync29(meta)) return void 0;
|
|
11606
11979
|
try {
|
|
11607
|
-
return /name="([^"]*)"/.exec(envelopeFirstTextPart(JSON.parse(
|
|
11980
|
+
return /name="([^"]*)"/.exec(envelopeFirstTextPart(JSON.parse(readFileSync25(meta, "utf8"))))?.[1];
|
|
11608
11981
|
} catch {
|
|
11609
11982
|
return void 0;
|
|
11610
11983
|
}
|
|
@@ -11662,7 +12035,7 @@ function runCodeConnect(opts) {
|
|
|
11662
12035
|
axisLines.push(`const ${varName} = instance.getEnum(${q(axis)}, { ${entries.join(", ")} })`);
|
|
11663
12036
|
fragmentVars.push(varName);
|
|
11664
12037
|
}
|
|
11665
|
-
const entryRel =
|
|
12038
|
+
const entryRel = path36.relative(callerCwd, path36.join(bundleDir, manifest.entry));
|
|
11666
12039
|
const trust = manifest.trustStatement.split("\n")[0] ?? "";
|
|
11667
12040
|
const lines = [
|
|
11668
12041
|
`// url=${opts.figmaUrl}`,
|
|
@@ -11683,7 +12056,7 @@ function runCodeConnect(opts) {
|
|
|
11683
12056
|
`}`,
|
|
11684
12057
|
``
|
|
11685
12058
|
].join("\n");
|
|
11686
|
-
const outFile =
|
|
12059
|
+
const outFile = path36.resolve(callerCwd, opts.out ?? path36.join(bundleDir, `${component}.figma.ts`));
|
|
11687
12060
|
writeFileSync13(outFile, lines);
|
|
11688
12061
|
emitData(
|
|
11689
12062
|
opts,
|
|
@@ -11723,17 +12096,17 @@ var init_codeconnect = __esm({
|
|
|
11723
12096
|
|
|
11724
12097
|
// packages/mcp/src/server.ts
|
|
11725
12098
|
import { createHash as createHash5 } from "node:crypto";
|
|
11726
|
-
import { existsSync as
|
|
11727
|
-
import
|
|
11728
|
-
import
|
|
12099
|
+
import { existsSync as existsSync30, mkdtempSync as mkdtempSync3, readFileSync as readFileSync26, readdirSync as readdirSync9, writeFileSync as writeFileSync14 } from "node:fs";
|
|
12100
|
+
import os7 from "node:os";
|
|
12101
|
+
import path37 from "node:path";
|
|
11729
12102
|
import { fileURLToPath as fileURLToPath6 } from "node:url";
|
|
11730
12103
|
import { z as z12 } from "zod";
|
|
11731
12104
|
function sourceHash() {
|
|
11732
|
-
const dir =
|
|
12105
|
+
const dir = path37.dirname(fileURLToPath6(import.meta.url));
|
|
11733
12106
|
const h = createHash5("sha256");
|
|
11734
|
-
for (const f of
|
|
12107
|
+
for (const f of readdirSync9(dir).filter((n) => n.endsWith(".ts")).sort()) {
|
|
11735
12108
|
h.update(f);
|
|
11736
|
-
h.update(
|
|
12109
|
+
h.update(readFileSync26(path37.join(dir, f)));
|
|
11737
12110
|
}
|
|
11738
12111
|
return h.digest("hex").slice(0, 16);
|
|
11739
12112
|
}
|
|
@@ -11741,10 +12114,10 @@ var REPO_ROOT3, CLI_BIN, BUNDLED_CLI, CLI_SPAWN, str, optStr, TOOLS, BOOT_HASH;
|
|
|
11741
12114
|
var init_server = __esm({
|
|
11742
12115
|
"packages/mcp/src/server.ts"() {
|
|
11743
12116
|
"use strict";
|
|
11744
|
-
REPO_ROOT3 =
|
|
11745
|
-
CLI_BIN =
|
|
11746
|
-
BUNDLED_CLI =
|
|
11747
|
-
CLI_SPAWN =
|
|
12117
|
+
REPO_ROOT3 = path37.resolve(path37.dirname(fileURLToPath6(import.meta.url)), "..", "..", "..");
|
|
12118
|
+
CLI_BIN = path37.join(REPO_ROOT3, "packages", "cli", "src", "bin.ts");
|
|
12119
|
+
BUNDLED_CLI = path37.join(path37.dirname(fileURLToPath6(import.meta.url)), "tendril.js");
|
|
12120
|
+
CLI_SPAWN = existsSync30(BUNDLED_CLI) ? { cmd: process.execPath, prefix: [BUNDLED_CLI] } : { cmd: "npx", prefix: ["tsx", CLI_BIN] };
|
|
11748
12121
|
str = (d) => z12.string().describe(d);
|
|
11749
12122
|
optStr = (d) => z12.string().optional().describe(d);
|
|
11750
12123
|
TOOLS = [
|
|
@@ -11774,7 +12147,7 @@ var init_server = __esm({
|
|
|
11774
12147
|
const single = i["metadata"];
|
|
11775
12148
|
const parts = i["metadataParts"];
|
|
11776
12149
|
if (single !== void 0 || parts !== void 0) {
|
|
11777
|
-
const tmp =
|
|
12150
|
+
const tmp = path37.join(mkdtempSync3(path37.join(os7.tmpdir(), "tendril-envelope-")), "response.txt");
|
|
11778
12151
|
if (single !== void 0) {
|
|
11779
12152
|
writeFileSync14(tmp, single);
|
|
11780
12153
|
argvOut.push("--metadata-raw-file", tmp);
|
|
@@ -11791,7 +12164,7 @@ var init_server = __esm({
|
|
|
11791
12164
|
},
|
|
11792
12165
|
{
|
|
11793
12166
|
name: "tendril_permissions",
|
|
11794
|
-
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
|
|
12167
|
+
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>__* (for those, write:true installs PLUGIN-prefixed names that will not match: list without writing and adapt the prefix instead). 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.",
|
|
11795
12168
|
schema: z12.object({
|
|
11796
12169
|
write: z12.boolean().optional().describe("true = install into .claude/settings.local.json (the point of this tool); false/absent = just return the entries")
|
|
11797
12170
|
}),
|
|
@@ -11845,7 +12218,7 @@ var init_server = __esm({
|
|
|
11845
12218
|
const bridge = (label, single, parts) => {
|
|
11846
12219
|
if (single !== void 0 && parts !== void 0) throw new Error(`pass at most one of \`${label}\` and \`${label}Parts\``);
|
|
11847
12220
|
if (single === void 0 && parts === void 0) return;
|
|
11848
|
-
const tmp =
|
|
12221
|
+
const tmp = path37.join(mkdtempSync3(path37.join(os7.tmpdir(), "tendril-envelope-")), "response.txt");
|
|
11849
12222
|
if (single !== void 0) {
|
|
11850
12223
|
writeFileSync14(tmp, single);
|
|
11851
12224
|
argvOut.push(`--${label}-file`, tmp);
|
|
@@ -11888,7 +12261,7 @@ var init_server = __esm({
|
|
|
11888
12261
|
const file = i["file"];
|
|
11889
12262
|
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)");
|
|
11890
12263
|
if (file !== void 0) return [...base, "--file", file];
|
|
11891
|
-
const tmp =
|
|
12264
|
+
const tmp = path37.join(mkdtempSync3(path37.join(os7.tmpdir(), "tendril-envelope-")), "response.txt");
|
|
11892
12265
|
if (text !== void 0) {
|
|
11893
12266
|
writeFileSync14(tmp, text);
|
|
11894
12267
|
return [...base, "--file", tmp, "--raw"];
|
|
@@ -11951,7 +12324,7 @@ var init_server = __esm({
|
|
|
11951
12324
|
},
|
|
11952
12325
|
{
|
|
11953
12326
|
name: "tendril_engine_score",
|
|
11954
|
-
description: "AGENT-HARNESS engine, step 2 (the oracle): scores a candidate bundle directory against recorded truth \u2014 per-config pixels, behaviors,
|
|
12327
|
+
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.",
|
|
11955
12328
|
schema: z12.object({
|
|
11956
12329
|
taskOrSet: str("reference task name or recording-set directory"),
|
|
11957
12330
|
candidateDir: str("directory containing the proposed bundle files"),
|
|
@@ -12041,15 +12414,16 @@ __export(permissions_exports, {
|
|
|
12041
12414
|
PERMISSIONS_DESCRIPTION: () => PERMISSIONS_DESCRIPTION,
|
|
12042
12415
|
buildPermissions: () => buildPermissions,
|
|
12043
12416
|
mergeAllowlist: () => mergeAllowlist,
|
|
12044
|
-
runPermissions: () => runPermissions
|
|
12417
|
+
runPermissions: () => runPermissions,
|
|
12418
|
+
writeSelection: () => writeSelection
|
|
12045
12419
|
});
|
|
12046
|
-
import { existsSync as
|
|
12047
|
-
import
|
|
12048
|
-
import
|
|
12049
|
-
function mergeAllowlist(file, entries) {
|
|
12420
|
+
import { existsSync as existsSync31, mkdirSync as mkdirSync9, readFileSync as readFileSync27, writeFileSync as writeFileSync15 } from "node:fs";
|
|
12421
|
+
import os8 from "node:os";
|
|
12422
|
+
import path38 from "node:path";
|
|
12423
|
+
function mergeAllowlist(file, entries, denyEntries = []) {
|
|
12050
12424
|
let settings = {};
|
|
12051
|
-
if (
|
|
12052
|
-
settings = JSON.parse(
|
|
12425
|
+
if (existsSync31(file) && readFileSync27(file, "utf8").trim() !== "") {
|
|
12426
|
+
settings = JSON.parse(readFileSync27(file, "utf8"));
|
|
12053
12427
|
if (settings === null || typeof settings !== "object" || Array.isArray(settings)) throw new Error("settings root is not an object");
|
|
12054
12428
|
}
|
|
12055
12429
|
const permissions = settings["permissions"] ??= {};
|
|
@@ -12059,13 +12433,21 @@ function mergeAllowlist(file, entries) {
|
|
|
12059
12433
|
const present = new Set(allow.filter((x) => typeof x === "string"));
|
|
12060
12434
|
const added = entries.filter((e) => !present.has(e));
|
|
12061
12435
|
const alreadyPresent = entries.filter((e) => present.has(e));
|
|
12062
|
-
|
|
12436
|
+
let denyAdded = [];
|
|
12437
|
+
if (denyEntries.length > 0) {
|
|
12438
|
+
const deny = permissions["deny"] ??= [];
|
|
12439
|
+
if (!Array.isArray(deny)) throw new Error("permissions.deny is not an array");
|
|
12440
|
+
const denyPresent = new Set(deny.filter((x) => typeof x === "string"));
|
|
12441
|
+
denyAdded = denyEntries.filter((e) => !denyPresent.has(e));
|
|
12442
|
+
deny.push(...denyAdded);
|
|
12443
|
+
}
|
|
12444
|
+
if (added.length > 0 || denyAdded.length > 0) {
|
|
12063
12445
|
allow.push(...added);
|
|
12064
|
-
mkdirSync9(
|
|
12446
|
+
mkdirSync9(path38.dirname(file), { recursive: true });
|
|
12065
12447
|
writeFileSync15(file, `${JSON.stringify(settings, null, 2)}
|
|
12066
12448
|
`);
|
|
12067
12449
|
}
|
|
12068
|
-
return { added, alreadyPresent };
|
|
12450
|
+
return { added, alreadyPresent, denyAdded };
|
|
12069
12451
|
}
|
|
12070
12452
|
async function buildPermissions(options) {
|
|
12071
12453
|
const LEGAL_TOOL_NAME = /^[A-Za-z0-9_-]+$/;
|
|
@@ -12085,10 +12467,18 @@ async function buildPermissions(options) {
|
|
|
12085
12467
|
...TOOLS.map((t) => t.name).filter((n) => LEGAL_TOOL_NAME.test(n)).map((n) => `${TENDRIL_PLUGIN_PREFIX}__${n}`),
|
|
12086
12468
|
...figmaTools.map((name) => `${FIGMA_PLUGIN_PREFIX}__${name}`)
|
|
12087
12469
|
],
|
|
12470
|
+
shellEntries: [...SHELL_READONLY_ENTRIES, ...SHELL_PIPELINE_ENTRIES],
|
|
12471
|
+
projectFileEntries: PROJECT_FILE_ENTRIES,
|
|
12472
|
+
projectDenyEntries: PROJECT_DENY_ENTRIES,
|
|
12473
|
+
manualCautionEntries: MANUAL_CAUTION_ENTRIES,
|
|
12474
|
+
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.",
|
|
12088
12475
|
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.",
|
|
12089
12476
|
directConfigNote: `Installed via claude mcp add instead of the plugin? Replace the prefixes: ${TENDRIL_PLUGIN_PREFIX}__<tool> becomes mcp__<your-server-name>__<tool> (same for Figma).`
|
|
12090
12477
|
};
|
|
12091
12478
|
}
|
|
12479
|
+
function writeSelection(result, user) {
|
|
12480
|
+
return user ? { entries: [...result.toolEntries], denyEntries: [] } : { entries: [...result.toolEntries, ...result.shellEntries, ...result.projectFileEntries], denyEntries: [...result.projectDenyEntries] };
|
|
12481
|
+
}
|
|
12092
12482
|
async function runPermissions(flags) {
|
|
12093
12483
|
if (flags.describe) {
|
|
12094
12484
|
printDescription(PERMISSIONS_DESCRIPTION);
|
|
@@ -12097,21 +12487,25 @@ async function runPermissions(flags) {
|
|
|
12097
12487
|
const result = await buildPermissions({ ...flags.mcpUrl ? { mcpUrl: flags.mcpUrl } : {} });
|
|
12098
12488
|
if (flags.write) {
|
|
12099
12489
|
const base = process.env["INIT_CWD"] ?? process.cwd();
|
|
12100
|
-
const file = flags.user ?
|
|
12490
|
+
const file = flags.user ? path38.join(os8.homedir(), ".claude", "settings.json") : path38.join(base, ".claude", "settings.local.json");
|
|
12491
|
+
const { entries, denyEntries } = writeSelection(result, flags.user === true);
|
|
12101
12492
|
if (flags.dryRun) {
|
|
12102
|
-
emitData(flags, { file, wouldAdd:
|
|
12103
|
-
process.stdout.write(
|
|
12104
|
-
`)
|
|
12493
|
+
emitData(flags, { file, wouldAdd: entries, wouldDeny: denyEntries }, () => {
|
|
12494
|
+
process.stdout.write(
|
|
12495
|
+
`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}
|
|
12496
|
+
`
|
|
12497
|
+
);
|
|
12105
12498
|
});
|
|
12106
12499
|
return;
|
|
12107
12500
|
}
|
|
12108
12501
|
try {
|
|
12109
|
-
const { added, alreadyPresent } = mergeAllowlist(file,
|
|
12110
|
-
emitData(flags, { ...result, written: { file, added, alreadyPresent } }, () => {
|
|
12502
|
+
const { added, alreadyPresent, denyAdded } = mergeAllowlist(file, entries, denyEntries);
|
|
12503
|
+
emitData(flags, { ...result, written: { file, added, alreadyPresent, denyAdded } }, () => {
|
|
12111
12504
|
process.stdout.write(
|
|
12112
|
-
added.length === 0 ? `already installed: all ${alreadyPresent.length} Tendril pipeline entries present in ${file}
|
|
12113
|
-
` : `installed: ${added.length} allowlist entr${added.length === 1 ? "y" : "ies"} added to ${file}${alreadyPresent.length > 0 ? ` (${alreadyPresent.length} already present)` : ""}
|
|
12114
|
-
|
|
12505
|
+
added.length === 0 && denyAdded.length === 0 ? `already installed: all ${alreadyPresent.length} Tendril pipeline entries present in ${file}
|
|
12506
|
+
` : `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)` : ""}
|
|
12507
|
+
${flags.user ? "" : ` \u26A0 ${result.shellEntriesNote}
|
|
12508
|
+
`} restart or reopen the Claude Code session to pick up settings changes
|
|
12115
12509
|
`
|
|
12116
12510
|
);
|
|
12117
12511
|
});
|
|
@@ -12136,6 +12530,24 @@ Or paste into .claude/settings.json under permissions.allow:
|
|
|
12136
12530
|
|
|
12137
12531
|
${quoted(result.toolEntries)}
|
|
12138
12532
|
|
|
12533
|
+
The SHELL surface a run actually uses:
|
|
12534
|
+
|
|
12535
|
+
${quoted(result.shellEntries)}
|
|
12536
|
+
|
|
12537
|
+
\u26A0 ${result.shellEntriesNote}
|
|
12538
|
+
|
|
12539
|
+
Project-scoped file access (written only to the project-local file):
|
|
12540
|
+
|
|
12541
|
+
${quoted(result.projectFileEntries)}
|
|
12542
|
+
|
|
12543
|
+
\u2026with these under permissions.deny (so the grant above can never rewrite the settings file or .git):
|
|
12544
|
+
|
|
12545
|
+
${quoted(result.projectDenyEntries)}
|
|
12546
|
+
|
|
12547
|
+
Deliberate manual additions \u2014 clobber- or exec-capable, never auto-written:
|
|
12548
|
+
|
|
12549
|
+
${quoted(result.manualCautionEntries)}
|
|
12550
|
+
|
|
12139
12551
|
Shorter but broader \u2014 one entry per server:
|
|
12140
12552
|
|
|
12141
12553
|
${quoted(result.serverEntries)}
|
|
@@ -12147,7 +12559,7 @@ ${result.directConfigNote}
|
|
|
12147
12559
|
);
|
|
12148
12560
|
});
|
|
12149
12561
|
}
|
|
12150
|
-
var FIGMA_TOOL_FALLBACK, PERMISSIONS_DESCRIPTION, TENDRIL_PLUGIN_PREFIX, FIGMA_PLUGIN_PREFIX;
|
|
12562
|
+
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;
|
|
12151
12563
|
var init_permissions = __esm({
|
|
12152
12564
|
"packages/cli/src/commands/permissions.ts"() {
|
|
12153
12565
|
"use strict";
|
|
@@ -12159,14 +12571,44 @@ var init_permissions = __esm({
|
|
|
12159
12571
|
init_output();
|
|
12160
12572
|
init_doctor();
|
|
12161
12573
|
FIGMA_TOOL_FALLBACK = ["get_metadata", "get_design_context", "get_screenshot", "get_variable_defs", "get_motion_context", "get_figjam"];
|
|
12574
|
+
SHELL_READONLY_ENTRIES = [
|
|
12575
|
+
"Bash(date:*)",
|
|
12576
|
+
"Bash(ls:*)",
|
|
12577
|
+
"Bash(stat:*)",
|
|
12578
|
+
"Bash(wc:*)",
|
|
12579
|
+
"Bash(head:*)",
|
|
12580
|
+
"Bash(tail:*)",
|
|
12581
|
+
"Bash(cat:*)",
|
|
12582
|
+
"Bash(shasum:*)",
|
|
12583
|
+
"Bash(grep:*)"
|
|
12584
|
+
];
|
|
12585
|
+
SHELL_PIPELINE_ENTRIES = [
|
|
12586
|
+
// The skill's own rule: candidate dirs are created with a bare
|
|
12587
|
+
// `mkdir -p`, no compounds — one grant covers the whole batch.
|
|
12588
|
+
"Bash(mkdir -p:*)",
|
|
12589
|
+
// The published CLI, PER SUBCOMMAND — never `tendril *`. The CLI
|
|
12590
|
+
// writes the settings file through Node fs, not the host's Write
|
|
12591
|
+
// tool, so the protected-path guard that stops `Write(./**)` from
|
|
12592
|
+
// touching .claude/ does NOT stop `tendril permissions --write`: a
|
|
12593
|
+
// blanket grant would let an allowlisted agent silently widen its own
|
|
12594
|
+
// allowlist (review finding, confirmed from the host's docs).
|
|
12595
|
+
// `permissions`, `activate` and `init` are deliberately absent.
|
|
12596
|
+
...["verify", "inspect", "doctor", "record", "engine", "fonts", "generate"].flatMap((sub) => [
|
|
12597
|
+
`Bash(tendril ${sub}:*)`,
|
|
12598
|
+
`Bash(npx -y -p @tendrilapp/cli@latest tendril ${sub}:*)`
|
|
12599
|
+
])
|
|
12600
|
+
];
|
|
12601
|
+
PROJECT_FILE_ENTRIES = ["Write(./**)", "Edit(./**)"];
|
|
12602
|
+
PROJECT_DENY_ENTRIES = ["Write(./.claude/**)", "Edit(./.claude/**)", "Write(./.git/**)", "Edit(./.git/**)"];
|
|
12603
|
+
MANUAL_CAUTION_ENTRIES = ["Bash(cp -R:*)", "Bash(find:*)"];
|
|
12162
12604
|
PERMISSIONS_DESCRIPTION = {
|
|
12163
12605
|
name: "permissions",
|
|
12164
12606
|
summary: "Install (or print) the Claude Code permission allowlist for the Tendril pipeline (tendril + Figma MCP tools).",
|
|
12165
12607
|
args: [],
|
|
12166
12608
|
flags: [
|
|
12167
12609
|
{ flag: "--claude", description: "Claude Code settings format (the default and currently only format)" },
|
|
12168
|
-
{ flag: "--write", description: "Merge the
|
|
12169
|
-
{ flag: "--user", description: "With --write: target ~/.claude/settings.json
|
|
12610
|
+
{ 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)" },
|
|
12611
|
+
{ 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" },
|
|
12170
12612
|
{ flag: "--mcp-url <url>", description: "Figma MCP endpoint to list live tool names from", default: DEFAULT_MCP_URL },
|
|
12171
12613
|
{ flag: "--json", description: "Machine-readable output" }
|
|
12172
12614
|
],
|
|
@@ -12175,7 +12617,7 @@ var init_permissions = __esm({
|
|
|
12175
12617
|
serverEntries: "string[] \u2014 one entry per MCP server (allows every tool it serves)",
|
|
12176
12618
|
toolEntries: "string[] \u2014 per-tool entries for selective allowlists",
|
|
12177
12619
|
directConfigNote: "string \u2014 prefix rewrite for non-plugin (claude mcp add) installs",
|
|
12178
|
-
written: "with --write: { file, added, alreadyPresent }"
|
|
12620
|
+
written: "with --write: { file, added, alreadyPresent, denyAdded }"
|
|
12179
12621
|
},
|
|
12180
12622
|
exitCodes: { 0: "printed or written", 3: "with --write: the target file exists but is not JSON this command can safely rewrite" },
|
|
12181
12623
|
examples: ["tendril permissions --claude --write", "tendril permissions --claude", "tendril permissions --claude --json"]
|
|
@@ -12191,24 +12633,24 @@ __export(inspect_exports, {
|
|
|
12191
12633
|
INSPECT_DESCRIPTION: () => INSPECT_DESCRIPTION,
|
|
12192
12634
|
runInspect: () => runInspect
|
|
12193
12635
|
});
|
|
12194
|
-
import { existsSync as
|
|
12195
|
-
import
|
|
12636
|
+
import { existsSync as existsSync32, readFileSync as readFileSync28, writeFileSync as writeFileSync16 } from "node:fs";
|
|
12637
|
+
import path39 from "node:path";
|
|
12196
12638
|
async function runInspect(opts) {
|
|
12197
12639
|
if (opts.describe) {
|
|
12198
12640
|
printDescription(INSPECT_DESCRIPTION);
|
|
12199
12641
|
return;
|
|
12200
12642
|
}
|
|
12201
|
-
const bundleDir =
|
|
12202
|
-
const evidenceDir =
|
|
12203
|
-
const manifestPath2 =
|
|
12204
|
-
if (!
|
|
12643
|
+
const bundleDir = path39.resolve(opts.bundleDir);
|
|
12644
|
+
const evidenceDir = path39.join(bundleDir, "verify-evidence");
|
|
12645
|
+
const manifestPath2 = path39.join(bundleDir, "component.json");
|
|
12646
|
+
if (!existsSync32(evidenceDir) || !existsSync32(manifestPath2)) {
|
|
12205
12647
|
fail(opts, ExitCode.InputValidation, {
|
|
12206
|
-
error: `nothing to inspect in ${bundleDir} \u2014 ${
|
|
12648
|
+
error: `nothing to inspect in ${bundleDir} \u2014 ${existsSync32(manifestPath2) ? "no verify-evidence directory" : "no component.json"}`,
|
|
12207
12649
|
code: "no-evidence",
|
|
12208
12650
|
remediation: `Run \`${tendrilCommand(`verify ${bundleDir}`)}\` first \u2014 inspect reads the ref/render evidence that run writes.`
|
|
12209
12651
|
});
|
|
12210
12652
|
}
|
|
12211
|
-
const { manifest } = readBundleManifest(
|
|
12653
|
+
const { manifest } = readBundleManifest(readFileSync28(manifestPath2, "utf8"));
|
|
12212
12654
|
if (manifest === void 0) {
|
|
12213
12655
|
fail(opts, ExitCode.InputValidation, {
|
|
12214
12656
|
error: "component.json did not parse as a bundle manifest",
|
|
@@ -12216,8 +12658,8 @@ async function runInspect(opts) {
|
|
|
12216
12658
|
remediation: "Re-emit the bundle (score/verify rewrite component.json), then re-run inspect."
|
|
12217
12659
|
});
|
|
12218
12660
|
}
|
|
12219
|
-
const setDir =
|
|
12220
|
-
const reps = Object.keys(manifest.propAdapter).filter((rep) =>
|
|
12661
|
+
const setDir = path39.resolve(opts.set ?? manifest.provenance.recordingSet.path);
|
|
12662
|
+
const reps = Object.keys(manifest.propAdapter).filter((rep) => existsSync32(path39.join(evidenceDir, `${rep}-ref.png`)) && existsSync32(path39.join(evidenceDir, `${rep}-render.png`)));
|
|
12221
12663
|
if (reps.length === 0) {
|
|
12222
12664
|
fail(opts, ExitCode.InputValidation, {
|
|
12223
12665
|
error: "verify-evidence holds no ref/render pairs for this bundle's configs",
|
|
@@ -12228,15 +12670,15 @@ async function runInspect(opts) {
|
|
|
12228
12670
|
let crops = 0;
|
|
12229
12671
|
const sections = [];
|
|
12230
12672
|
for (const rep of reps) {
|
|
12231
|
-
const ref = new Uint8Array(
|
|
12232
|
-
const render = new Uint8Array(
|
|
12673
|
+
const ref = new Uint8Array(readFileSync28(path39.join(evidenceDir, `${rep}-ref.png`)));
|
|
12674
|
+
const render = new Uint8Array(readFileSync28(path39.join(evidenceDir, `${rep}-render.png`)));
|
|
12233
12675
|
const nodes = smallSemanticNodes(setDir, rep, opts.maxArea ?? 1024).slice(0, 12);
|
|
12234
12676
|
const cells = [];
|
|
12235
12677
|
for (const [i, n] of nodes.entries()) {
|
|
12236
12678
|
const rect = { x: n.x, y: n.y, w: n.w, h: n.h };
|
|
12237
12679
|
try {
|
|
12238
|
-
writeFileSync16(
|
|
12239
|
-
writeFileSync16(
|
|
12680
|
+
writeFileSync16(path39.join(evidenceDir, `${rep}-inspect-${i}-ref.png`), zoomCrop(ref, rect));
|
|
12681
|
+
writeFileSync16(path39.join(evidenceDir, `${rep}-inspect-${i}-render.png`), zoomCrop(render, rect));
|
|
12240
12682
|
} catch {
|
|
12241
12683
|
continue;
|
|
12242
12684
|
}
|
|
@@ -12249,7 +12691,7 @@ async function runInspect(opts) {
|
|
|
12249
12691
|
`<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>`
|
|
12250
12692
|
);
|
|
12251
12693
|
}
|
|
12252
|
-
const sheet =
|
|
12694
|
+
const sheet = path39.join(evidenceDir, "inspect.html");
|
|
12253
12695
|
writeFileSync16(
|
|
12254
12696
|
sheet,
|
|
12255
12697
|
`<!doctype html><meta charset="utf-8"><title>${esc(manifest.name)} \u2014 tendril inspect</title><style>
|
|
@@ -12327,17 +12769,17 @@ __export(generate_recorded_exports, {
|
|
|
12327
12769
|
runGenerateRecorded: () => runGenerateRecorded
|
|
12328
12770
|
});
|
|
12329
12771
|
import { confirm as confirm3, isCancel as isCancel3 } from "@clack/prompts";
|
|
12330
|
-
import { existsSync as
|
|
12331
|
-
import
|
|
12772
|
+
import { existsSync as existsSync33, readFileSync as readFileSync29 } from "node:fs";
|
|
12773
|
+
import path40 from "node:path";
|
|
12332
12774
|
async function runGenerateRecorded(opts) {
|
|
12333
12775
|
const callerCwd = process.env["INIT_CWD"] ?? process.cwd();
|
|
12334
|
-
const outDirAbs =
|
|
12335
|
-
const recordedAsPath =
|
|
12776
|
+
const outDirAbs = path40.resolve(callerCwd, opts.out);
|
|
12777
|
+
const recordedAsPath = path40.resolve(callerCwd, opts.recorded);
|
|
12336
12778
|
let task;
|
|
12337
12779
|
let taskName;
|
|
12338
12780
|
let authoredApi;
|
|
12339
12781
|
let composition;
|
|
12340
|
-
const isSet =
|
|
12782
|
+
const isSet = existsSync33(path40.join(recordedAsPath, "recording-set.json"));
|
|
12341
12783
|
const registry = TASKS[opts.recorded];
|
|
12342
12784
|
if (registry !== void 0 && !isSet) {
|
|
12343
12785
|
task = registry;
|
|
@@ -12346,7 +12788,7 @@ async function runGenerateRecorded(opts) {
|
|
|
12346
12788
|
try {
|
|
12347
12789
|
const authored = authorTaskFromSet(recordedAsPath);
|
|
12348
12790
|
task = authored.task;
|
|
12349
|
-
taskName =
|
|
12791
|
+
taskName = path40.basename(recordedAsPath);
|
|
12350
12792
|
authoredApi = authored.api;
|
|
12351
12793
|
const roles = RolesSchema.safeParse(loadManifest(recordedAsPath).roles);
|
|
12352
12794
|
if (roles.success) composition = roles.data;
|
|
@@ -12380,7 +12822,7 @@ async function runGenerateRecorded(opts) {
|
|
|
12380
12822
|
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)`);
|
|
12381
12823
|
}
|
|
12382
12824
|
const missing = task.configs.filter(
|
|
12383
|
-
(c) => !
|
|
12825
|
+
(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"))
|
|
12384
12826
|
);
|
|
12385
12827
|
if (missing.length > 0) {
|
|
12386
12828
|
fail(opts, ExitCode.RecordingIncomplete, {
|
|
@@ -12450,8 +12892,8 @@ async function runGenerateRecorded(opts) {
|
|
|
12450
12892
|
` : `${line}
|
|
12451
12893
|
`);
|
|
12452
12894
|
if (opts.dryRun) {
|
|
12453
|
-
emitData(opts, { dryRun: true, task: taskName, model: modelId, consent, wouldWrite:
|
|
12454
|
-
process.stdout.write(`dry-run: nothing sent, nothing written (would write ${
|
|
12895
|
+
emitData(opts, { dryRun: true, task: taskName, model: modelId, consent, wouldWrite: path40.join(outDirAbs, taskName) }, () => {
|
|
12896
|
+
process.stdout.write(`dry-run: nothing sent, nothing written (would write ${path40.join(outDirAbs, taskName)})
|
|
12455
12897
|
`);
|
|
12456
12898
|
});
|
|
12457
12899
|
return;
|
|
@@ -12474,10 +12916,10 @@ async function runGenerateRecorded(opts) {
|
|
|
12474
12916
|
});
|
|
12475
12917
|
}
|
|
12476
12918
|
}
|
|
12477
|
-
const bundleDir =
|
|
12478
|
-
if (
|
|
12919
|
+
const bundleDir = path40.join(outDirAbs, taskName);
|
|
12920
|
+
if (existsSync33(path40.join(bundleDir, "component.json"))) {
|
|
12479
12921
|
try {
|
|
12480
|
-
const prior = readBundleManifest(
|
|
12922
|
+
const prior = readBundleManifest(readFileSync29(path40.join(bundleDir, "component.json"), "utf8")).manifest;
|
|
12481
12923
|
if (prior !== void 0 && prior.provenance.recordingSet.hash !== recordingSetHash(task.set, task.configs)) {
|
|
12482
12924
|
fail(opts, ExitCode.InputValidation, {
|
|
12483
12925
|
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`,
|
|
@@ -12643,7 +13085,7 @@ init_invocation();
|
|
|
12643
13085
|
init_output();
|
|
12644
13086
|
import { intro, isCancel, outro, password } from "@clack/prompts";
|
|
12645
13087
|
import fs from "node:fs";
|
|
12646
|
-
import
|
|
13088
|
+
import path25 from "node:path";
|
|
12647
13089
|
var INIT_DESCRIPTION = {
|
|
12648
13090
|
name: "init",
|
|
12649
13091
|
summary: "Configure Figma and OpenRouter credentials in .env (idempotent).",
|
|
@@ -12680,7 +13122,7 @@ async function runInit(flags) {
|
|
|
12680
13122
|
printDescription(INIT_DESCRIPTION);
|
|
12681
13123
|
return;
|
|
12682
13124
|
}
|
|
12683
|
-
const envPath =
|
|
13125
|
+
const envPath = path25.resolve(process.cwd(), ".env");
|
|
12684
13126
|
const existing = fs.existsSync(envPath) ? parseEnv(fs.readFileSync(envPath, "utf8")) : /* @__PURE__ */ new Map();
|
|
12685
13127
|
let figmaToken = flags.figmaToken ?? existing.get(ENV_KEYS.figma);
|
|
12686
13128
|
let openrouterKey = flags.openrouterKey ?? existing.get(ENV_KEYS.openrouter);
|
|
@@ -12701,7 +13143,7 @@ async function runInit(flags) {
|
|
|
12701
13143
|
next.set(ENV_KEYS.figma, figmaToken);
|
|
12702
13144
|
next.set(ENV_KEYS.openrouter, openrouterKey);
|
|
12703
13145
|
const changed = existing.get(ENV_KEYS.figma) !== next.get(ENV_KEYS.figma) || existing.get(ENV_KEYS.openrouter) !== next.get(ENV_KEYS.openrouter);
|
|
12704
|
-
const gitignorePath =
|
|
13146
|
+
const gitignorePath = path25.resolve(process.cwd(), ".gitignore");
|
|
12705
13147
|
const gitignore = fs.existsSync(gitignorePath) ? fs.readFileSync(gitignorePath, "utf8") : "";
|
|
12706
13148
|
const gitignoreCoversEnv = gitignore.split("\n").some((line) => [".env", ".env", ".env*"].includes(line.trim()));
|
|
12707
13149
|
if (flags.dryRun) {
|
|
@@ -12757,14 +13199,14 @@ init_invocation();
|
|
|
12757
13199
|
init_output();
|
|
12758
13200
|
init_entitlement();
|
|
12759
13201
|
import { confirm as confirm2, isCancel as isCancel2 } from "@clack/prompts";
|
|
12760
|
-
import { readFileSync as
|
|
13202
|
+
import { readFileSync as readFileSync16, readdirSync as readdirSync5, existsSync as existsSync20 } from "node:fs";
|
|
12761
13203
|
|
|
12762
13204
|
// packages/cli/src/pipeline.ts
|
|
12763
13205
|
init_src2();
|
|
12764
13206
|
init_src4();
|
|
12765
13207
|
init_src6();
|
|
12766
13208
|
import { mkdirSync as mkdirSync5, writeFileSync as writeFileSync8 } from "node:fs";
|
|
12767
|
-
import
|
|
13209
|
+
import path26 from "node:path";
|
|
12768
13210
|
|
|
12769
13211
|
// packages/cli/src/assets-module.ts
|
|
12770
13212
|
init_src();
|
|
@@ -13100,7 +13542,7 @@ async function runGenerationPipeline(input) {
|
|
|
13100
13542
|
});
|
|
13101
13543
|
const written = [];
|
|
13102
13544
|
if (!input.dryRun) {
|
|
13103
|
-
const dir =
|
|
13545
|
+
const dir = path26.resolve(input.outDir, semantics.componentName);
|
|
13104
13546
|
mkdirSync5(dir, { recursive: true });
|
|
13105
13547
|
const files = {
|
|
13106
13548
|
// Bundle-local tokens: THE emission the component's CSS resolves
|
|
@@ -13124,13 +13566,13 @@ async function runGenerationPipeline(input) {
|
|
|
13124
13566
|
`
|
|
13125
13567
|
};
|
|
13126
13568
|
for (const [name, content] of Object.entries(files)) {
|
|
13127
|
-
const filePath =
|
|
13569
|
+
const filePath = path26.join(dir, name);
|
|
13128
13570
|
writeFileSync8(filePath, content);
|
|
13129
13571
|
written.push(filePath);
|
|
13130
13572
|
}
|
|
13131
13573
|
for (const artifact of emitTokenArtifacts(input.mapping)) {
|
|
13132
|
-
const filePath =
|
|
13133
|
-
mkdirSync5(
|
|
13574
|
+
const filePath = path26.resolve(input.outDir, artifact.path);
|
|
13575
|
+
mkdirSync5(path26.dirname(filePath), { recursive: true });
|
|
13134
13576
|
writeFileSync8(filePath, artifact.content);
|
|
13135
13577
|
written.push(filePath);
|
|
13136
13578
|
}
|
|
@@ -13189,7 +13631,7 @@ var GENERATE_DESCRIPTION = {
|
|
|
13189
13631
|
function resolveProvidedSource(flags, contextFile) {
|
|
13190
13632
|
let raw;
|
|
13191
13633
|
try {
|
|
13192
|
-
raw =
|
|
13634
|
+
raw = readFileSync16(contextFile, "utf8");
|
|
13193
13635
|
} catch {
|
|
13194
13636
|
fail(flags, ExitCode.InputValidation, {
|
|
13195
13637
|
error: `Cannot read context file "${contextFile}".`,
|
|
@@ -13309,11 +13751,11 @@ token mapping (${mapping.flat.length} variables):
|
|
|
13309
13751
|
let initialCode;
|
|
13310
13752
|
let initialSemantics;
|
|
13311
13753
|
try {
|
|
13312
|
-
if (
|
|
13313
|
-
for (const entry of
|
|
13754
|
+
if (existsSync20(flags.out)) {
|
|
13755
|
+
for (const entry of readdirSync5(flags.out)) {
|
|
13314
13756
|
const cjPath = `${flags.out}/${entry}/component.json`;
|
|
13315
|
-
if (!
|
|
13316
|
-
const cj = JSON.parse(
|
|
13757
|
+
if (!existsSync20(cjPath)) continue;
|
|
13758
|
+
const cj = JSON.parse(readFileSync16(cjPath, "utf8"));
|
|
13317
13759
|
if (cj.name !== void 0 && Array.isArray(cj.props)) {
|
|
13318
13760
|
previousApi = JSON.stringify({
|
|
13319
13761
|
componentName: cj.name,
|
|
@@ -13321,14 +13763,14 @@ token mapping (${mapping.flat.length} variables):
|
|
|
13321
13763
|
});
|
|
13322
13764
|
const tsxPath = `${flags.out}/${entry}/${cj.name}.tsx`;
|
|
13323
13765
|
const cssPath = `${flags.out}/${entry}/${cj.name}.css`;
|
|
13324
|
-
if (flags.refine &&
|
|
13766
|
+
if (flags.refine && existsSync20(tsxPath) && existsSync20(cssPath)) {
|
|
13325
13767
|
initialCode = {
|
|
13326
|
-
tsx:
|
|
13327
|
-
css:
|
|
13768
|
+
tsx: readFileSync16(tsxPath, "utf8"),
|
|
13769
|
+
css: readFileSync16(cssPath, "utf8")
|
|
13328
13770
|
};
|
|
13329
13771
|
const semPath = `${flags.out}/${entry}/semantics.json`;
|
|
13330
|
-
if (
|
|
13331
|
-
initialSemantics = JSON.parse(
|
|
13772
|
+
if (existsSync20(semPath)) {
|
|
13773
|
+
initialSemantics = JSON.parse(readFileSync16(semPath, "utf8"));
|
|
13332
13774
|
}
|
|
13333
13775
|
}
|
|
13334
13776
|
break;
|
|
@@ -13605,6 +14047,23 @@ function buildProgram() {
|
|
|
13605
14047
|
...local["face"] !== void 0 ? { face: Number(local["face"]) } : {}
|
|
13606
14048
|
});
|
|
13607
14049
|
});
|
|
14050
|
+
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) => {
|
|
14051
|
+
const flags = globalFlags(cmd.parent.parent);
|
|
14052
|
+
const { runFontsDiscover: runFontsDiscover2 } = await Promise.resolve().then(() => (init_fonts(), fonts_exports));
|
|
14053
|
+
runFontsDiscover2({ ...flags, cacheDir: cmd.opts()["cache"] ?? DEFAULT_FONT_CACHE, ...family !== void 0 ? { family } : {} });
|
|
14054
|
+
});
|
|
14055
|
+
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) => {
|
|
14056
|
+
const flags = globalFlags(cmd.parent.parent);
|
|
14057
|
+
const local = cmd.opts();
|
|
14058
|
+
const { runFontsAddSystem: runFontsAddSystem2 } = await Promise.resolve().then(() => (init_fonts(), fonts_exports));
|
|
14059
|
+
runFontsAddSystem2({
|
|
14060
|
+
...flags,
|
|
14061
|
+
family,
|
|
14062
|
+
cacheDir: local["cache"] ?? DEFAULT_FONT_CACHE,
|
|
14063
|
+
...local["weights"] !== void 0 ? { weights: local["weights"].map(Number) } : {},
|
|
14064
|
+
...local["set"] !== void 0 ? { set: local["set"] } : {}
|
|
14065
|
+
});
|
|
14066
|
+
});
|
|
13608
14067
|
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) => {
|
|
13609
14068
|
const flags = globalFlags(cmd.parent.parent);
|
|
13610
14069
|
const local = cmd.opts();
|