@tendrilapp/cli 0.1.27 → 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 +884 -451
- package/package.json +1 -1
package/dist/tendril.js
CHANGED
|
@@ -1337,8 +1337,8 @@ var init_src = __esm({
|
|
|
1337
1337
|
function variableNameToPath(name) {
|
|
1338
1338
|
return name.split("/").map(canonicalCssIdentPart).filter((part) => part.length > 0);
|
|
1339
1339
|
}
|
|
1340
|
-
function tokenPathToCssVar(
|
|
1341
|
-
return `--${
|
|
1340
|
+
function tokenPathToCssVar(path41) {
|
|
1341
|
+
return `--${path41.join("-")}`;
|
|
1342
1342
|
}
|
|
1343
1343
|
function toDtcgToken(variable, defaultMode) {
|
|
1344
1344
|
const modes = Object.keys(variable.valuesByMode);
|
|
@@ -1382,11 +1382,11 @@ function toDtcgToken(variable, defaultMode) {
|
|
|
1382
1382
|
}
|
|
1383
1383
|
function mapVariablesToDtcg(variables, defaultMode = "light") {
|
|
1384
1384
|
const entries = variables.map((variable) => {
|
|
1385
|
-
const
|
|
1386
|
-
if (
|
|
1385
|
+
const path41 = variableNameToPath(variable.name);
|
|
1386
|
+
if (path41.length === 0) {
|
|
1387
1387
|
throw new DtcgMappingError("empty-name", `Figma variable ${variable.id} has an empty name`);
|
|
1388
1388
|
}
|
|
1389
|
-
return { variable, path:
|
|
1389
|
+
return { variable, path: path41 };
|
|
1390
1390
|
});
|
|
1391
1391
|
const groupPrefixes = /* @__PURE__ */ new Set();
|
|
1392
1392
|
for (const e of entries) {
|
|
@@ -1407,21 +1407,21 @@ function mapVariablesToDtcg(variables, defaultMode = "light") {
|
|
|
1407
1407
|
}
|
|
1408
1408
|
const tokens = {};
|
|
1409
1409
|
const flat = [];
|
|
1410
|
-
for (const { variable, path:
|
|
1410
|
+
for (const { variable, path: path41 } of entries) {
|
|
1411
1411
|
const token = toDtcgToken(variable, defaultMode);
|
|
1412
1412
|
let group = tokens;
|
|
1413
|
-
for (const segment of
|
|
1413
|
+
for (const segment of path41.slice(0, -1)) {
|
|
1414
1414
|
const existing = group[segment];
|
|
1415
1415
|
group = existing ?? (group[segment] = {});
|
|
1416
1416
|
}
|
|
1417
|
-
const leaf =
|
|
1417
|
+
const leaf = path41[path41.length - 1];
|
|
1418
1418
|
if (group[leaf] !== void 0) {
|
|
1419
|
-
throw new DtcgMappingError("duplicate-path", `Duplicate token path "${
|
|
1419
|
+
throw new DtcgMappingError("duplicate-path", `Duplicate token path "${path41.join(".")}" (variable ${variable.id})`);
|
|
1420
1420
|
}
|
|
1421
1421
|
group[leaf] = token;
|
|
1422
1422
|
flat.push({
|
|
1423
|
-
path:
|
|
1424
|
-
cssVar: tokenPathToCssVar(
|
|
1423
|
+
path: path41.join("."),
|
|
1424
|
+
cssVar: tokenPathToCssVar(path41),
|
|
1425
1425
|
type: token.$type,
|
|
1426
1426
|
value: token.$value
|
|
1427
1427
|
});
|
|
@@ -1610,9 +1610,9 @@ function boundId(value) {
|
|
|
1610
1610
|
return isObject(value) && typeof value["boundVariableId"] === "string" ? value["boundVariableId"] : void 0;
|
|
1611
1611
|
}
|
|
1612
1612
|
function resolveBinding(ctx, id) {
|
|
1613
|
-
const
|
|
1614
|
-
if (
|
|
1615
|
-
return
|
|
1613
|
+
const path41 = ctx.pathById.get(id);
|
|
1614
|
+
if (path41 === void 0) ctx.unresolved.add(id);
|
|
1615
|
+
return path41;
|
|
1616
1616
|
}
|
|
1617
1617
|
function parseVariantProps(name) {
|
|
1618
1618
|
if (!name.includes("=")) return void 0;
|
|
@@ -1647,8 +1647,8 @@ function walk(ctx, raw) {
|
|
|
1647
1647
|
if (!isObject(paint) || paint["visible"] === false) continue;
|
|
1648
1648
|
const id = boundId(paint);
|
|
1649
1649
|
if (id !== void 0) {
|
|
1650
|
-
const
|
|
1651
|
-
if (
|
|
1650
|
+
const path41 = resolveBinding(ctx, id);
|
|
1651
|
+
if (path41 !== void 0) tokens.add(path41);
|
|
1652
1652
|
} else if (typeof paint["color"] === "string") {
|
|
1653
1653
|
ctx.hardcoded.push({ node: name, property, value: paint["color"] });
|
|
1654
1654
|
}
|
|
@@ -1656,8 +1656,8 @@ function walk(ctx, raw) {
|
|
|
1656
1656
|
}
|
|
1657
1657
|
const radiusId = boundId(raw["cornerRadius"]);
|
|
1658
1658
|
if (radiusId !== void 0) {
|
|
1659
|
-
const
|
|
1660
|
-
if (
|
|
1659
|
+
const path41 = resolveBinding(ctx, radiusId);
|
|
1660
|
+
if (path41 !== void 0) tokens.add(path41);
|
|
1661
1661
|
} else if (typeof raw["cornerRadius"] === "number" && raw["cornerRadius"] !== 0) {
|
|
1662
1662
|
ctx.hardcoded.push({ node: name, property: "border-radius", value: `${raw["cornerRadius"]}px` });
|
|
1663
1663
|
}
|
|
@@ -1667,10 +1667,10 @@ function walk(ctx, raw) {
|
|
|
1667
1667
|
layout = { mode: layoutMode === "HORIZONTAL" ? "flex-row" : "flex-column" };
|
|
1668
1668
|
const gapId = boundId(raw["itemSpacing"]);
|
|
1669
1669
|
if (gapId !== void 0) {
|
|
1670
|
-
const
|
|
1671
|
-
if (
|
|
1672
|
-
layout.gap =
|
|
1673
|
-
tokens.add(
|
|
1670
|
+
const path41 = resolveBinding(ctx, gapId);
|
|
1671
|
+
if (path41 !== void 0) {
|
|
1672
|
+
layout.gap = path41;
|
|
1673
|
+
tokens.add(path41);
|
|
1674
1674
|
}
|
|
1675
1675
|
} else if (typeof raw["itemSpacing"] === "number" && raw["itemSpacing"] !== 0) {
|
|
1676
1676
|
ctx.hardcoded.push({ node: name, property: "gap", value: `${raw["itemSpacing"]}px` });
|
|
@@ -1679,10 +1679,10 @@ function walk(ctx, raw) {
|
|
|
1679
1679
|
for (const field of PADDING_FIELDS) {
|
|
1680
1680
|
const id = boundId(raw[field]);
|
|
1681
1681
|
if (id !== void 0) {
|
|
1682
|
-
const
|
|
1683
|
-
if (
|
|
1684
|
-
paddingPaths.push(
|
|
1685
|
-
tokens.add(
|
|
1682
|
+
const path41 = resolveBinding(ctx, id);
|
|
1683
|
+
if (path41 !== void 0) {
|
|
1684
|
+
paddingPaths.push(path41);
|
|
1685
|
+
tokens.add(path41);
|
|
1686
1686
|
}
|
|
1687
1687
|
} else if (typeof raw[field] === "number" && raw[field] !== 0) {
|
|
1688
1688
|
ctx.hardcoded.push({ node: name, property: field, value: `${raw[field]}px` });
|
|
@@ -3498,6 +3498,7 @@ function isCollection(bytes) {
|
|
|
3498
3498
|
}
|
|
3499
3499
|
function nameTableStrings(view, bytes, nameOffset, nameLength) {
|
|
3500
3500
|
const out = /* @__PURE__ */ new Map();
|
|
3501
|
+
const rank = /* @__PURE__ */ new Map();
|
|
3501
3502
|
if (nameOffset + 6 > bytes.length) return out;
|
|
3502
3503
|
const count = view.getUint16(nameOffset + 2);
|
|
3503
3504
|
const stringOffset = nameOffset + view.getUint16(nameOffset + 4);
|
|
@@ -3511,14 +3512,21 @@ function nameTableStrings(view, bytes, nameOffset, nameLength) {
|
|
|
3511
3512
|
const offset = stringOffset + view.getUint16(rec + 10);
|
|
3512
3513
|
if (offset + length > bytes.length || offset + length > nameOffset + nameLength) continue;
|
|
3513
3514
|
const slice = bytes.subarray(offset, offset + length);
|
|
3514
|
-
const
|
|
3515
|
+
const languageId = view.getUint16(rec + 4);
|
|
3515
3516
|
let value = "";
|
|
3516
|
-
if (
|
|
3517
|
+
if (platformId === 3 || platformId === 0) {
|
|
3517
3518
|
for (let j = 0; j + 1 < slice.length; j += 2) value += String.fromCharCode(slice[j] << 8 | slice[j + 1]);
|
|
3518
|
-
} else {
|
|
3519
|
+
} else if (platformId === 1 && encodingId === 0) {
|
|
3519
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);
|
|
3520
3529
|
}
|
|
3521
|
-
if (value !== "" && !out.has(nameId)) out.set(nameId, value);
|
|
3522
3530
|
}
|
|
3523
3531
|
return out;
|
|
3524
3532
|
}
|
|
@@ -3598,15 +3606,151 @@ var init_font_collection = __esm({
|
|
|
3598
3606
|
}
|
|
3599
3607
|
});
|
|
3600
3608
|
|
|
3601
|
-
// packages/verify/src/font-
|
|
3602
|
-
import {
|
|
3603
|
-
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";
|
|
3604
3611
|
import os2 from "node:os";
|
|
3605
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";
|
|
3606
3750
|
function fontCacheDir() {
|
|
3607
3751
|
const env = process.env["TENDRIL_FONT_CACHE"];
|
|
3608
|
-
if (env !== void 0 && env !== "") return
|
|
3609
|
-
return
|
|
3752
|
+
if (env !== void 0 && env !== "") return path11.resolve(env);
|
|
3753
|
+
return path11.join(os3.homedir(), ".tendril", "fonts");
|
|
3610
3754
|
}
|
|
3611
3755
|
function normalizeFontLicense(value) {
|
|
3612
3756
|
return typeof value === "string" && FONT_LICENSES.includes(value) ? value : "unknown";
|
|
@@ -3671,29 +3815,29 @@ async function resolveFonts(family, weights, cacheDir = DEFAULT_FONT_CACHE) {
|
|
|
3671
3815
|
}
|
|
3672
3816
|
const bytes = new Uint8Array(await fileRes.arrayBuffer());
|
|
3673
3817
|
const sha256 = createHash("sha256").update(bytes).digest("hex");
|
|
3674
|
-
const file =
|
|
3818
|
+
const file = path11.join(cacheDir, `${family.toLowerCase().replace(/\s+/g, "-")}-${weight}.woff2`);
|
|
3675
3819
|
writeFileSync3(file, bytes);
|
|
3676
3820
|
resolved.push({ family, weight, source: url, sha256, file, license: meta.license, ...meta.family !== null ? { servedFamily: meta.family } : {} });
|
|
3677
3821
|
} catch (err) {
|
|
3678
3822
|
failures.push({ family, weight, reason: `download failed: ${err instanceof Error ? err.message : String(err)}` });
|
|
3679
3823
|
}
|
|
3680
3824
|
}
|
|
3681
|
-
const mPath =
|
|
3682
|
-
const prior =
|
|
3683
|
-
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) }));
|
|
3684
3828
|
const merged = [...prior.filter((p) => !resolved.some((m) => m.family === p.family && m.weight === p.weight)), ...portable2];
|
|
3685
3829
|
if (resolved.length > 0) writeFileSync3(mPath, `${JSON.stringify(merged, null, 2)}
|
|
3686
3830
|
`);
|
|
3687
3831
|
return { resolved, failures };
|
|
3688
3832
|
}
|
|
3689
|
-
function addLocalFont(family, weight, filePath, cacheDir = DEFAULT_FONT_CACHE, faceIndex) {
|
|
3690
|
-
const src =
|
|
3691
|
-
if (!
|
|
3692
|
-
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();
|
|
3693
3837
|
if (![".woff2", ".woff", ".ttf", ".otf", ".ttc"].includes(ext)) {
|
|
3694
3838
|
throw new Error(`unsupported font file "${ext}" \u2014 use .woff2 (preferred), .woff, .ttf, .otf or .ttc`);
|
|
3695
3839
|
}
|
|
3696
|
-
let bytes = new Uint8Array(
|
|
3840
|
+
let bytes = new Uint8Array(readFileSync4(src));
|
|
3697
3841
|
if (bytes.length === 0) throw new Error(`font file is empty: ${src}`);
|
|
3698
3842
|
let storedExt = ext;
|
|
3699
3843
|
if (isCollection(bytes)) {
|
|
@@ -3703,7 +3847,7 @@ function addLocalFont(family, weight, filePath, cacheDir = DEFAULT_FONT_CACHE, f
|
|
|
3703
3847
|
const all = listCollectionFaces(bytes);
|
|
3704
3848
|
const shown = (candidates.length > 0 ? candidates : all).map((f) => ` --face ${f.index} ${f.family ?? "(unnamed)"}${f.subfamily !== void 0 ? ` ${f.subfamily}` : ""}`).join("\n");
|
|
3705
3849
|
throw new Error(
|
|
3706
|
-
`${
|
|
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>:
|
|
3707
3851
|
${shown}`
|
|
3708
3852
|
);
|
|
3709
3853
|
}
|
|
@@ -3712,20 +3856,20 @@ ${shown}`
|
|
|
3712
3856
|
}
|
|
3713
3857
|
mkdirSync2(cacheDir, { recursive: true });
|
|
3714
3858
|
const sha256 = createHash("sha256").update(bytes).digest("hex");
|
|
3715
|
-
const file =
|
|
3859
|
+
const file = path11.join(cacheDir, `${family.toLowerCase().replace(/\s+/g, "-")}-${weight}${storedExt}`);
|
|
3716
3860
|
writeFileSync3(file, bytes);
|
|
3717
|
-
const face = { family, weight, source:
|
|
3718
|
-
const mPath =
|
|
3719
|
-
const prior =
|
|
3720
|
-
const merged = [...prior.filter((p) => !(p.family === family && p.weight === weight)), { ...face, file:
|
|
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) }];
|
|
3721
3865
|
writeFileSync3(mPath, `${JSON.stringify(merged, null, 2)}
|
|
3722
3866
|
`);
|
|
3723
3867
|
return face;
|
|
3724
3868
|
}
|
|
3725
3869
|
function checkFontLock(lockPath, cacheDir = DEFAULT_FONT_CACHE) {
|
|
3726
|
-
const lock = JSON.parse(
|
|
3727
|
-
const mPath =
|
|
3728
|
-
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")) : [];
|
|
3729
3873
|
return lock.map((l) => {
|
|
3730
3874
|
const m = manifest.find((x) => x.family === l.family && x.weight === l.weight);
|
|
3731
3875
|
if (m === void 0) return { family: l.family, weight: l.weight, status: "missing" };
|
|
@@ -3733,11 +3877,11 @@ function checkFontLock(lockPath, cacheDir = DEFAULT_FONT_CACHE) {
|
|
|
3733
3877
|
});
|
|
3734
3878
|
}
|
|
3735
3879
|
function resolvedFontFamilies(cacheDir = DEFAULT_FONT_CACHE) {
|
|
3736
|
-
const mPath =
|
|
3737
|
-
if (!
|
|
3880
|
+
const mPath = path11.join(cacheDir, "manifest.json");
|
|
3881
|
+
if (!existsSync7(mPath)) return [];
|
|
3738
3882
|
let entries;
|
|
3739
3883
|
try {
|
|
3740
|
-
entries = JSON.parse(
|
|
3884
|
+
entries = JSON.parse(readFileSync4(mPath, "utf8"));
|
|
3741
3885
|
} catch {
|
|
3742
3886
|
return [];
|
|
3743
3887
|
}
|
|
@@ -3751,8 +3895,8 @@ function resolvedFontFamilies(cacheDir = DEFAULT_FONT_CACHE) {
|
|
|
3751
3895
|
return [...byFamily.entries()].map(([family, w]) => ({ family, weights: [...w].sort((a, b) => a - b) }));
|
|
3752
3896
|
}
|
|
3753
3897
|
function requiredFontsManifest(families, cacheDir = DEFAULT_FONT_CACHE) {
|
|
3754
|
-
const mPath =
|
|
3755
|
-
const manifest =
|
|
3898
|
+
const mPath = path11.join(cacheDir, "manifest.json");
|
|
3899
|
+
const manifest = existsSync7(mPath) ? JSON.parse(readFileSync4(mPath, "utf8")) : [];
|
|
3756
3900
|
const wanted = new Set(families.map((f) => f.toLowerCase()));
|
|
3757
3901
|
return manifest.filter((f) => wanted.has(f.family.toLowerCase())).map((f) => ({
|
|
3758
3902
|
family: f.family,
|
|
@@ -3765,11 +3909,11 @@ function requiredFontsManifest(families, cacheDir = DEFAULT_FONT_CACHE) {
|
|
|
3765
3909
|
}));
|
|
3766
3910
|
}
|
|
3767
3911
|
function facesMissingLicense(cacheDir = DEFAULT_FONT_CACHE) {
|
|
3768
|
-
const mPath =
|
|
3769
|
-
if (!
|
|
3912
|
+
const mPath = path11.join(cacheDir, "manifest.json");
|
|
3913
|
+
if (!existsSync7(mPath)) return [];
|
|
3770
3914
|
let entries;
|
|
3771
3915
|
try {
|
|
3772
|
-
entries = JSON.parse(
|
|
3916
|
+
entries = JSON.parse(readFileSync4(mPath, "utf8"));
|
|
3773
3917
|
} catch {
|
|
3774
3918
|
return [];
|
|
3775
3919
|
}
|
|
@@ -3778,31 +3922,83 @@ function facesMissingLicense(cacheDir = DEFAULT_FONT_CACHE) {
|
|
|
3778
3922
|
).map((e) => ({ family: e.family, weight: e.weight, sha256: e.sha256 }));
|
|
3779
3923
|
}
|
|
3780
3924
|
function verifiedFontFamilies(cacheDir = DEFAULT_FONT_CACHE) {
|
|
3781
|
-
const mPath =
|
|
3782
|
-
if (!
|
|
3925
|
+
const mPath = path11.join(cacheDir, "manifest.json");
|
|
3926
|
+
if (!existsSync7(mPath)) return [];
|
|
3783
3927
|
let entries;
|
|
3784
3928
|
try {
|
|
3785
|
-
entries = JSON.parse(
|
|
3929
|
+
entries = JSON.parse(readFileSync4(mPath, "utf8"));
|
|
3786
3930
|
} catch {
|
|
3787
3931
|
return [];
|
|
3788
3932
|
}
|
|
3789
3933
|
const byFamily = /* @__PURE__ */ new Map();
|
|
3790
3934
|
for (const e of entries) {
|
|
3791
3935
|
if (typeof e.family !== "string" || typeof e.weight !== "number" || typeof e.file !== "string") continue;
|
|
3792
|
-
const file =
|
|
3793
|
-
if (!
|
|
3794
|
-
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;
|
|
3795
3939
|
const set = byFamily.get(e.family) ?? /* @__PURE__ */ new Set();
|
|
3796
3940
|
set.add(e.weight);
|
|
3797
3941
|
byFamily.set(e.family, set);
|
|
3798
3942
|
}
|
|
3799
3943
|
return [...byFamily.entries()].map(([family, w]) => ({ family, weights: [...w].sort((a, b) => a - b) }));
|
|
3800
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
|
+
}
|
|
3801
3996
|
var DEFAULT_FONT_CACHE, UA, FONT_LICENSES, GOOGLE_LICENSE_IDS, GOOGLE_FONT_FILE_PREFIX;
|
|
3802
3997
|
var init_font_resolve = __esm({
|
|
3803
3998
|
"packages/verify/src/font-resolve.ts"() {
|
|
3804
3999
|
"use strict";
|
|
3805
4000
|
init_font_collection();
|
|
4001
|
+
init_font_discovery();
|
|
3806
4002
|
DEFAULT_FONT_CACHE = fontCacheDir();
|
|
3807
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";
|
|
3808
4004
|
FONT_LICENSES = ["OFL-1.1", "Apache-2.0", "UFL-1.0", "proprietary", "unknown"];
|
|
@@ -3818,17 +4014,17 @@ var init_font_resolve = __esm({
|
|
|
3818
4014
|
|
|
3819
4015
|
// packages/verify/src/font-faces.ts
|
|
3820
4016
|
import { createHash as createHash2 } from "node:crypto";
|
|
3821
|
-
import { existsSync as
|
|
3822
|
-
import
|
|
4017
|
+
import { existsSync as existsSync8, readFileSync as readFileSync5 } from "node:fs";
|
|
4018
|
+
import path12 from "node:path";
|
|
3823
4019
|
function injectedGroups(manifestPath2) {
|
|
3824
|
-
if (!
|
|
3825
|
-
const claimed = JSON.parse(
|
|
3826
|
-
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));
|
|
3827
4023
|
const byFile = /* @__PURE__ */ new Map();
|
|
3828
4024
|
for (const f of claimed) {
|
|
3829
4025
|
const file = resolveFile(f.file);
|
|
3830
|
-
if (!
|
|
3831
|
-
if (createHash2("sha256").update(
|
|
4026
|
+
if (!existsSync8(file)) continue;
|
|
4027
|
+
if (createHash2("sha256").update(readFileSync5(file)).digest("hex") !== f.sha256) continue;
|
|
3832
4028
|
const k = `${f.family}:${f.file}`;
|
|
3833
4029
|
const e = byFile.get(k) ?? { family: f.family, weights: [], file };
|
|
3834
4030
|
e.weights.push(f.weight);
|
|
@@ -3837,14 +4033,14 @@ function injectedGroups(manifestPath2) {
|
|
|
3837
4033
|
const groups = [...byFile.values()];
|
|
3838
4034
|
return { groups, shared: new Set(groups.map((e) => e.file)).size < groups.length };
|
|
3839
4035
|
}
|
|
3840
|
-
function fontFaceCss(manifestPath2 =
|
|
4036
|
+
function fontFaceCss(manifestPath2 = path12.join(fontCacheDir(), "manifest.json")) {
|
|
3841
4037
|
const { groups, shared } = injectedGroups(manifestPath2);
|
|
3842
4038
|
return groups.map((e) => {
|
|
3843
4039
|
const weight = shared || e.weights.length > 1 ? `${SPAN[0]} ${SPAN[1]}` : String(e.weights[0]);
|
|
3844
|
-
return `@font-face { font-family: '${e.family}'; font-weight: ${weight}; font-style: normal; src: url(data:font/woff2;base64,${
|
|
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'); }`;
|
|
3845
4041
|
}).join("\n");
|
|
3846
4042
|
}
|
|
3847
|
-
function injectedFamilyWeights(manifestPath2 =
|
|
4043
|
+
function injectedFamilyWeights(manifestPath2 = path12.join(fontCacheDir(), "manifest.json")) {
|
|
3848
4044
|
const { groups, shared } = injectedGroups(manifestPath2);
|
|
3849
4045
|
const out = /* @__PURE__ */ new Map();
|
|
3850
4046
|
for (const g of groups) {
|
|
@@ -3869,17 +4065,17 @@ var init_font_faces = __esm({
|
|
|
3869
4065
|
});
|
|
3870
4066
|
|
|
3871
4067
|
// packages/verify/src/admission.ts
|
|
3872
|
-
import { readFileSync as
|
|
3873
|
-
import
|
|
4068
|
+
import { readFileSync as readFileSync6, readdirSync as readdirSync3, existsSync as existsSync9, writeFileSync as writeFileSync4 } from "node:fs";
|
|
4069
|
+
import path13 from "node:path";
|
|
3874
4070
|
import { build as build2 } from "esbuild";
|
|
3875
4071
|
import postcss from "postcss";
|
|
3876
4072
|
import tailwindcss from "tailwindcss";
|
|
3877
4073
|
import { chromium as chromium2 } from "playwright-core";
|
|
3878
4074
|
function fontWeightsByFamily() {
|
|
3879
|
-
const mPath =
|
|
4075
|
+
const mPath = path13.join(fontCacheDir(), "manifest.json");
|
|
3880
4076
|
const out = /* @__PURE__ */ new Map();
|
|
3881
|
-
if (!
|
|
3882
|
-
for (const f of JSON.parse(
|
|
4077
|
+
if (!existsSync9(mPath)) return out;
|
|
4078
|
+
for (const f of JSON.parse(readFileSync6(mPath, "utf8")))
|
|
3883
4079
|
out.set(f.family, [...out.get(f.family) ?? [], f.weight]);
|
|
3884
4080
|
return out;
|
|
3885
4081
|
}
|
|
@@ -3899,7 +4095,7 @@ var init_admission = __esm({
|
|
|
3899
4095
|
});
|
|
3900
4096
|
|
|
3901
4097
|
// packages/verify/src/tasks.ts
|
|
3902
|
-
import
|
|
4098
|
+
import path14 from "node:path";
|
|
3903
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;
|
|
3904
4100
|
var init_tasks = __esm({
|
|
3905
4101
|
"packages/verify/src/tasks.ts"() {
|
|
@@ -4059,7 +4255,7 @@ BEHAVIORAL CONTRACT (machine-verified, gating): the primary action and close con
|
|
|
4059
4255
|
];
|
|
4060
4256
|
TASKS = {
|
|
4061
4257
|
calendar: {
|
|
4062
|
-
set:
|
|
4258
|
+
set: path14.join(REPO_ROOT, "examples/recordings/shadcn-poc-calendar"),
|
|
4063
4259
|
entry: "Calendar.tsx",
|
|
4064
4260
|
configs: CALENDAR_CONFIGS,
|
|
4065
4261
|
systemApi: CALENDAR_API,
|
|
@@ -4067,7 +4263,7 @@ BEHAVIORAL CONTRACT (machine-verified, gating): the primary action and close con
|
|
|
4067
4263
|
prelude: { controls: ['[data-tendril-part="day"]'], textInputs: [] }
|
|
4068
4264
|
},
|
|
4069
4265
|
"shadcn-button": {
|
|
4070
|
-
set:
|
|
4266
|
+
set: path14.join(REPO_ROOT, "examples/recordings/shadcn-poc-button"),
|
|
4071
4267
|
entry: "Button.tsx",
|
|
4072
4268
|
configs: BUTTON_CONFIGS,
|
|
4073
4269
|
systemApi: BUTTON_API,
|
|
@@ -4075,7 +4271,7 @@ BEHAVIORAL CONTRACT (machine-verified, gating): the primary action and close con
|
|
|
4075
4271
|
prelude: { controls: ["> *"], textInputs: [] }
|
|
4076
4272
|
},
|
|
4077
4273
|
combobox: {
|
|
4078
|
-
set:
|
|
4274
|
+
set: path14.join(REPO_ROOT, "examples/recordings/carbon-poc-combobox"),
|
|
4079
4275
|
entry: "ComboBox.tsx",
|
|
4080
4276
|
configs: COMBO_CONFIGS,
|
|
4081
4277
|
systemApi: COMBO_API,
|
|
@@ -4083,7 +4279,7 @@ BEHAVIORAL CONTRACT (machine-verified, gating): the primary action and close con
|
|
|
4083
4279
|
prelude: { controls: ['[role="option"]'], textInputs: ["input"], popover: { selector: '[role="listbox"]', trigger: "input" } }
|
|
4084
4280
|
},
|
|
4085
4281
|
modal: {
|
|
4086
|
-
set:
|
|
4282
|
+
set: path14.join(REPO_ROOT, "examples/recordings/carbon-poc-modal"),
|
|
4087
4283
|
entry: "Modal.tsx",
|
|
4088
4284
|
configs: MODAL_CONFIGS,
|
|
4089
4285
|
systemApi: MODAL_API,
|
|
@@ -4101,8 +4297,8 @@ __export(behavior_exports, {
|
|
|
4101
4297
|
compileMount: () => compileMount,
|
|
4102
4298
|
recordingIsDark: () => recordingIsDark
|
|
4103
4299
|
});
|
|
4104
|
-
import { existsSync as
|
|
4105
|
-
import
|
|
4300
|
+
import { existsSync as existsSync10, readFileSync as readFileSync7 } from "node:fs";
|
|
4301
|
+
import path15 from "node:path";
|
|
4106
4302
|
import { build as build3 } from "esbuild";
|
|
4107
4303
|
import { chromium as chromium3 } from "playwright-core";
|
|
4108
4304
|
import { PNG as PNG2 } from "pngjs";
|
|
@@ -4111,12 +4307,12 @@ function getFontFaces() {
|
|
|
4111
4307
|
return _fontFaces;
|
|
4112
4308
|
}
|
|
4113
4309
|
async function compileMount(task, bundleDir) {
|
|
4114
|
-
const entryTsx =
|
|
4115
|
-
if (!
|
|
4310
|
+
const entryTsx = path15.join(bundleDir, task.entry);
|
|
4311
|
+
if (!existsSync10(entryTsx)) return { error: `${task.entry} missing` };
|
|
4116
4312
|
const mountSrc = `
|
|
4117
4313
|
import { createElement } from "react";
|
|
4118
4314
|
import { createRoot } from "react-dom/client";
|
|
4119
|
-
import * as B from ${JSON.stringify(
|
|
4315
|
+
import * as B from ${JSON.stringify(path15.resolve(entryTsx))};
|
|
4120
4316
|
const cfg = (window as unknown as { __cfg: { component: string; props: Record<string, unknown>; spyProps?: string[] } }).__cfg;
|
|
4121
4317
|
const C = (B as Record<string, unknown>)[cfg.component] as Parameters<typeof createElement>[0] | undefined;
|
|
4122
4318
|
// Callbacks cannot ride the JSON config: specs NAME spy props and the
|
|
@@ -4397,10 +4593,10 @@ async function runSteps(page, spec, renderPose) {
|
|
|
4397
4593
|
function recordingIsDark(task) {
|
|
4398
4594
|
const rep = task.configs[0]?.rep;
|
|
4399
4595
|
if (rep === void 0) return false;
|
|
4400
|
-
const f =
|
|
4401
|
-
if (!
|
|
4596
|
+
const f = path15.join(task.set, rep, "get_screenshot.json");
|
|
4597
|
+
if (!existsSync10(f)) return false;
|
|
4402
4598
|
try {
|
|
4403
|
-
const env = JSON.parse(
|
|
4599
|
+
const env = JSON.parse(readFileSync7(f, "utf8")).content.find((c) => c.type === "image");
|
|
4404
4600
|
if (env?.data === void 0) return false;
|
|
4405
4601
|
const png = PNG2.sync.read(Buffer.from(env.data, "base64"));
|
|
4406
4602
|
let sum = 0;
|
|
@@ -4472,7 +4668,7 @@ async function checkBehaviors(task, bundleDir, opts = {}) {
|
|
|
4472
4668
|
const deadlineMs = timeoutMs + 1e4;
|
|
4473
4669
|
const js = await compileMount(task, bundleDir);
|
|
4474
4670
|
if (typeof js !== "string") return task.behaviors.map((b) => ({ id: b.id, pass: false, detail: js.error }));
|
|
4475
|
-
const css = ["tokens.css", "styles.css"].map((f) =>
|
|
4671
|
+
const css = ["tokens.css", "styles.css"].map((f) => path15.join(bundleDir, f)).filter((f) => existsSync10(f)).map((f) => readFileSync7(f, "utf8")).join("\n");
|
|
4476
4672
|
const server = await chromium3.launchServer({ executablePath: resolveChrome(), headless: true, args: mountArgs() });
|
|
4477
4673
|
const browser = await chromium3.connect(server.wsEndpoint());
|
|
4478
4674
|
const results = [];
|
|
@@ -4615,8 +4811,8 @@ var init_behavior = __esm({
|
|
|
4615
4811
|
});
|
|
4616
4812
|
|
|
4617
4813
|
// packages/verify/src/bundle-quality.ts
|
|
4618
|
-
import { readFileSync as
|
|
4619
|
-
import
|
|
4814
|
+
import { readFileSync as readFileSync8, rmSync as rmSync2, writeFileSync as writeFileSync5, existsSync as existsSync11 } from "node:fs";
|
|
4815
|
+
import path16 from "node:path";
|
|
4620
4816
|
function definedVars(tokensCss) {
|
|
4621
4817
|
if (tokensCss === void 0) return void 0;
|
|
4622
4818
|
const names = /* @__PURE__ */ new Set();
|
|
@@ -4625,19 +4821,19 @@ function definedVars(tokensCss) {
|
|
|
4625
4821
|
}
|
|
4626
4822
|
function recordedTokenMapState(setDir, reps) {
|
|
4627
4823
|
const readMap = (file) => {
|
|
4628
|
-
if (!
|
|
4824
|
+
if (!existsSync11(file)) return void 0;
|
|
4629
4825
|
try {
|
|
4630
|
-
const parsed = JSON.parse(envelopeFirstTextPart(JSON.parse(
|
|
4826
|
+
const parsed = JSON.parse(envelopeFirstTextPart(JSON.parse(readFileSync8(file, "utf8"))) || "{}");
|
|
4631
4827
|
return parsed !== null && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : {};
|
|
4632
4828
|
} catch {
|
|
4633
4829
|
return {};
|
|
4634
4830
|
}
|
|
4635
4831
|
};
|
|
4636
|
-
const setLevel = readMap(
|
|
4832
|
+
const setLevel = readMap(path16.join(setDir, "get_variable_defs.json"));
|
|
4637
4833
|
if (setLevel !== void 0) return Object.keys(setLevel).length === 0 ? "empty" : "populated";
|
|
4638
4834
|
let recorded = false;
|
|
4639
4835
|
for (const rep of reps) {
|
|
4640
|
-
const m = readMap(
|
|
4836
|
+
const m = readMap(path16.join(setDir, rep, "get_variable_defs.json"));
|
|
4641
4837
|
if (m === void 0) continue;
|
|
4642
4838
|
recorded = true;
|
|
4643
4839
|
if (Object.keys(m).length > 0) return "populated";
|
|
@@ -4702,11 +4898,11 @@ function fontStackFindings(sheets, coverage) {
|
|
|
4702
4898
|
}
|
|
4703
4899
|
async function checkBundleQuality(bundleDir, entry, set, fontManifest) {
|
|
4704
4900
|
const findings = [];
|
|
4705
|
-
const entryPath =
|
|
4706
|
-
const cssPath =
|
|
4707
|
-
const tokensPath =
|
|
4708
|
-
const css =
|
|
4709
|
-
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;
|
|
4710
4906
|
findings.push(
|
|
4711
4907
|
...fontStackFindings(
|
|
4712
4908
|
[
|
|
@@ -4716,11 +4912,11 @@ async function checkBundleQuality(bundleDir, entry, set, fontManifest) {
|
|
|
4716
4912
|
injectedFamilyWeights(fontManifest)
|
|
4717
4913
|
)
|
|
4718
4914
|
);
|
|
4719
|
-
if (
|
|
4915
|
+
if (existsSync11(entryPath)) {
|
|
4720
4916
|
const workDir = newScratchDir("quality");
|
|
4721
4917
|
try {
|
|
4722
|
-
const tsxPath =
|
|
4723
|
-
writeFileSync5(tsxPath,
|
|
4918
|
+
const tsxPath = path16.join(workDir, entry);
|
|
4919
|
+
writeFileSync5(tsxPath, readFileSync8(entryPath, "utf8"));
|
|
4724
4920
|
for (const d of runTscStrict([tsxPath]).diagnostics) {
|
|
4725
4921
|
findings.push({ kind: "tsc", file: entry, ...d.line === void 0 ? {} : { line: d.line }, message: `TS${d.code}: ${d.message}` });
|
|
4726
4922
|
}
|
|
@@ -4795,8 +4991,8 @@ var init_effect_geometry = __esm({
|
|
|
4795
4991
|
});
|
|
4796
4992
|
|
|
4797
4993
|
// packages/verify/src/bundle-score.ts
|
|
4798
|
-
import { existsSync as
|
|
4799
|
-
import
|
|
4994
|
+
import { existsSync as existsSync12, mkdirSync as mkdirSync3, readFileSync as readFileSync9, writeFileSync as writeFileSync6 } from "node:fs";
|
|
4995
|
+
import path17 from "node:path";
|
|
4800
4996
|
import { build as build4 } from "esbuild";
|
|
4801
4997
|
import { chromium as chromium4 } from "playwright-core";
|
|
4802
4998
|
function getFontFaces2() {
|
|
@@ -4850,7 +5046,7 @@ function chooseSeat(cropAt, ref, padW, padH, backdrop, origin, extents) {
|
|
|
4850
5046
|
}
|
|
4851
5047
|
function metadataRoot(set, rep) {
|
|
4852
5048
|
try {
|
|
4853
|
-
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");
|
|
4854
5050
|
return parseMetadataStructure(text);
|
|
4855
5051
|
} catch {
|
|
4856
5052
|
return void 0;
|
|
@@ -4905,19 +5101,19 @@ function smallSemanticNodes(set, rep, maxArea = 1024) {
|
|
|
4905
5101
|
});
|
|
4906
5102
|
}
|
|
4907
5103
|
function repMeta(set, rep) {
|
|
4908
|
-
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");
|
|
4909
5105
|
const root = parseMetadataStructure(text);
|
|
4910
5106
|
return { w: Math.round(root.width ?? 100), h: Math.round(root.height ?? 40) };
|
|
4911
5107
|
}
|
|
4912
5108
|
function repRef(set, rep) {
|
|
4913
|
-
const env = JSON.parse(
|
|
5109
|
+
const env = JSON.parse(readFileSync9(path17.join(set, rep, "get_screenshot.json"), "utf8")).content.find((c) => c.type === "image");
|
|
4914
5110
|
return Uint8Array.from(Buffer.from(env?.data ?? "", "base64"));
|
|
4915
5111
|
}
|
|
4916
5112
|
function repEffectExtents(set, rep) {
|
|
4917
|
-
const file =
|
|
4918
|
-
if (!
|
|
5113
|
+
const file = path17.join(set, rep, "get_design_context.json");
|
|
5114
|
+
if (!existsSync12(file)) return void 0;
|
|
4919
5115
|
try {
|
|
4920
|
-
const text = JSON.parse(
|
|
5116
|
+
const text = JSON.parse(readFileSync9(file, "utf8")).content.map((c) => c.text ?? "").join("\n");
|
|
4921
5117
|
const extents = shadowExtents(text);
|
|
4922
5118
|
return extents.top + extents.right + extents.bottom + extents.left > 0 ? extents : void 0;
|
|
4923
5119
|
} catch {
|
|
@@ -4928,13 +5124,13 @@ async function scoreBundleForTask(task, bundleDir, bar = BAR, opts = {}) {
|
|
|
4928
5124
|
if (opts.evidenceDir !== void 0) mkdirSync3(opts.evidenceDir, { recursive: true });
|
|
4929
5125
|
const CONFIGS2 = task.configs;
|
|
4930
5126
|
const timeoutMs = opts.mountDeadlineMs ?? ENGINE_MOUNT_DEADLINE_MS;
|
|
4931
|
-
const entryTsx =
|
|
4932
|
-
if (!
|
|
4933
|
-
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");
|
|
4934
5130
|
const mountSrc = `
|
|
4935
5131
|
import { createElement } from "react";
|
|
4936
5132
|
import { createRoot } from "react-dom/client";
|
|
4937
|
-
import * as B from ${JSON.stringify(
|
|
5133
|
+
import * as B from ${JSON.stringify(path17.resolve(entryTsx))};
|
|
4938
5134
|
const cfg = (window as unknown as { __cfg: { component: string; props: Record<string, unknown> } }).__cfg;
|
|
4939
5135
|
const C = (B as Record<string, unknown>)[cfg.component] as Parameters<typeof createElement>[0] | undefined;
|
|
4940
5136
|
const root = document.getElementById("root");
|
|
@@ -5029,12 +5225,12 @@ body{margin:0;background:rgb(${backdrop.join(",")})}
|
|
|
5029
5225
|
return name === void 0 ? c : { ...c, name };
|
|
5030
5226
|
});
|
|
5031
5227
|
if (opts.evidenceDir !== void 0) {
|
|
5032
|
-
writeFileSync6(
|
|
5033
|
-
writeFileSync6(
|
|
5034
|
-
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));
|
|
5035
5231
|
for (const [i, c] of absent.slice(0, 3).entries()) {
|
|
5036
|
-
writeFileSync6(
|
|
5037
|
-
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));
|
|
5038
5234
|
}
|
|
5039
5235
|
}
|
|
5040
5236
|
return {
|
|
@@ -5142,15 +5338,15 @@ var init_prelude = __esm({
|
|
|
5142
5338
|
});
|
|
5143
5339
|
|
|
5144
5340
|
// packages/verify/src/parity.ts
|
|
5145
|
-
import { existsSync as
|
|
5146
|
-
import
|
|
5341
|
+
import { existsSync as existsSync13, readFileSync as readFileSync10 } from "node:fs";
|
|
5342
|
+
import path18 from "node:path";
|
|
5147
5343
|
import { chromium as chromium6 } from "playwright-core";
|
|
5148
5344
|
function getFontFaces3() {
|
|
5149
5345
|
_fontFaces3 ??= fontFaceCss();
|
|
5150
5346
|
return _fontFaces3;
|
|
5151
5347
|
}
|
|
5152
|
-
function hoverForcedConfigs(
|
|
5153
|
-
return
|
|
5348
|
+
function hoverForcedConfigs(authority) {
|
|
5349
|
+
return authority.filter((c) => {
|
|
5154
5350
|
const forced = c.props["data-tendril-state"];
|
|
5155
5351
|
return typeof forced === "string" && forced.split(/\s+/).includes("hover");
|
|
5156
5352
|
});
|
|
@@ -5165,27 +5361,37 @@ function withoutHoverToken(props) {
|
|
|
5165
5361
|
}
|
|
5166
5362
|
return rest;
|
|
5167
5363
|
}
|
|
5168
|
-
async function checkHoverParity(task, bundleDir, opts = {}) {
|
|
5169
|
-
const configs = hoverForcedConfigs(
|
|
5364
|
+
async function checkHoverParity(task, bundleDir, authority, opts = {}) {
|
|
5365
|
+
const configs = hoverForcedConfigs(authority);
|
|
5170
5366
|
if (configs.length === 0) return [];
|
|
5171
5367
|
const timeoutMs = opts.mountDeadlineMs ?? ENGINE_MOUNT_DEADLINE_MS;
|
|
5172
5368
|
const deadlineMs = timeoutMs + 1e4;
|
|
5173
5369
|
const js = await compileMount(task, bundleDir);
|
|
5174
5370
|
if (typeof js !== "string") return configs.map((c) => ({ id: `parity:${c.rep}`, pass: false, detail: js.error }));
|
|
5175
|
-
const css = ["tokens.css", "styles.css"].map((f) =>
|
|
5371
|
+
const css = ["tokens.css", "styles.css"].map((f) => path18.join(bundleDir, f)).filter((f) => existsSync13(f)).map((f) => readFileSync10(f, "utf8")).join("\n");
|
|
5176
5372
|
const server = await chromium6.launchServer({ executablePath: resolveChrome(), headless: true, args: mountArgs() });
|
|
5177
5373
|
const browser = await chromium6.connect(server.wsEndpoint());
|
|
5178
5374
|
const results = [];
|
|
5179
5375
|
try {
|
|
5180
5376
|
for (const cfg of configs) {
|
|
5181
|
-
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) => {
|
|
5182
5388
|
const html = `<!doctype html><html><head><meta charset="utf-8"><style>
|
|
5183
5389
|
${getFontFaces3()}
|
|
5184
5390
|
${css}
|
|
5185
5391
|
body{margin:0;padding:20px}
|
|
5186
|
-
|
|
5187
|
-
</style></head><body><div id="root"></div><script>window.__cfg=${JSON.stringify({ component
|
|
5188
|
-
const page = await browser.newPage({ viewport
|
|
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 });
|
|
5189
5395
|
try {
|
|
5190
5396
|
page.setDefaultTimeout(timeoutMs);
|
|
5191
5397
|
await page.route("**/*", (route) => route.request().url().startsWith("data:") ? route.continue() : route.abort());
|
|
@@ -5199,15 +5405,30 @@ body{margin:0;padding:20px}
|
|
|
5199
5405
|
} else {
|
|
5200
5406
|
await page.waitForTimeout(400);
|
|
5201
5407
|
}
|
|
5202
|
-
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 } });
|
|
5203
5409
|
} finally {
|
|
5204
5410
|
await page.close();
|
|
5205
5411
|
}
|
|
5206
5412
|
};
|
|
5207
5413
|
const work = (async () => {
|
|
5208
|
-
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);
|
|
5209
5419
|
if (!Buffer.isBuffer(forcedShot)) return { id: `parity:${cfg.rep}`, pass: false, detail: `forced mount: ${forcedShot.error}` };
|
|
5210
|
-
|
|
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);
|
|
5211
5432
|
if (!Buffer.isBuffer(realShot)) return { id: `parity:${cfg.rep}`, pass: false, detail: `real-hover mount: ${realShot.error}` };
|
|
5212
5433
|
if (Buffer.compare(forcedShot, realShot) !== 0) {
|
|
5213
5434
|
return {
|
|
@@ -5247,13 +5468,14 @@ var init_parity = __esm({
|
|
|
5247
5468
|
init_behavior();
|
|
5248
5469
|
init_font_faces();
|
|
5249
5470
|
init_mount_limits();
|
|
5471
|
+
init_bundle_score();
|
|
5250
5472
|
}
|
|
5251
5473
|
});
|
|
5252
5474
|
|
|
5253
5475
|
// packages/verify/src/composition.ts
|
|
5254
5476
|
import { createRequire as createRequire2 } from "node:module";
|
|
5255
|
-
import { existsSync as
|
|
5256
|
-
import
|
|
5477
|
+
import { existsSync as existsSync14, readFileSync as readFileSync11 } from "node:fs";
|
|
5478
|
+
import path19 from "node:path";
|
|
5257
5479
|
import { build as build6 } from "esbuild";
|
|
5258
5480
|
import { chromium as chromium7 } from "playwright-core";
|
|
5259
5481
|
function getFontFaces4() {
|
|
@@ -5261,9 +5483,9 @@ function getFontFaces4() {
|
|
|
5261
5483
|
return _fontFaces4;
|
|
5262
5484
|
}
|
|
5263
5485
|
async function compileInstrumentedMount(task, bundleDir) {
|
|
5264
|
-
const entryTsx =
|
|
5265
|
-
if (!
|
|
5266
|
-
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"));
|
|
5267
5489
|
let realJsxPath;
|
|
5268
5490
|
try {
|
|
5269
5491
|
realJsxPath = requireFromVerify.resolve("react/jsx-runtime");
|
|
@@ -5274,7 +5496,7 @@ async function compileInstrumentedMount(task, bundleDir) {
|
|
|
5274
5496
|
import { createElement } from "react";
|
|
5275
5497
|
import { createRoot } from "react-dom/client";
|
|
5276
5498
|
import { __registerParts } from "react/jsx-runtime";
|
|
5277
|
-
import * as B from ${JSON.stringify(
|
|
5499
|
+
import * as B from ${JSON.stringify(path19.resolve(entryTsx))};
|
|
5278
5500
|
const cfg = (window as unknown as { __cfg: { component: string; props: Record<string, unknown>; partComponents: string[] } }).__cfg;
|
|
5279
5501
|
const pairs: Array<[unknown, string]> = [];
|
|
5280
5502
|
for (const name of cfg.partComponents) {
|
|
@@ -5329,7 +5551,7 @@ function expectedParts(task, roles, mainSlug) {
|
|
|
5329
5551
|
}
|
|
5330
5552
|
function interiorRegions(setDir, roles) {
|
|
5331
5553
|
const mains = roles.main;
|
|
5332
|
-
const withInterior = mains.filter((m) =>
|
|
5554
|
+
const withInterior = mains.filter((m) => existsSync14(path19.join(setDir, m, "get_metadata_interior.json")));
|
|
5333
5555
|
if (withInterior.length === 0) {
|
|
5334
5556
|
return { unavailable: "no interior geometry recorded for any main (pre-obligation set) \u2014 re-record with `tendril record` to enable crop checks" };
|
|
5335
5557
|
}
|
|
@@ -5346,7 +5568,7 @@ async function checkCropComposition(task, bundleDir, regions, opts = {}) {
|
|
|
5346
5568
|
if (typeof js !== "string") {
|
|
5347
5569
|
return regions.map((r) => ({ id: cropId(r), pass: false, similarity: 0, inkRecall: 0, detail: js.error }));
|
|
5348
5570
|
}
|
|
5349
|
-
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");
|
|
5350
5572
|
const server = await chromium7.launchServer({ executablePath: resolveChrome(), headless: true, args: mountArgs() });
|
|
5351
5573
|
const browser = await chromium7.connect(server.wsEndpoint());
|
|
5352
5574
|
const PAD = 4;
|
|
@@ -5451,7 +5673,7 @@ async function checkStructuralComposition(task, bundleDir, roles, opts = {}) {
|
|
|
5451
5673
|
const deadlineMs = timeoutMs + 1e4;
|
|
5452
5674
|
const js = await compileInstrumentedMount(task, bundleDir);
|
|
5453
5675
|
if (typeof js !== "string") return [...results, ...mains.map((m) => ({ id: `composition:${m}`, pass: false, detail: js.error }))];
|
|
5454
|
-
const css = ["tokens.css", "styles.css"].map((f) =>
|
|
5676
|
+
const css = ["tokens.css", "styles.css"].map((f) => path19.join(bundleDir, f)).filter((f) => existsSync14(f)).map((f) => readFileSync11(f, "utf8")).join("\n");
|
|
5455
5677
|
const server = await chromium7.launchServer({ executablePath: resolveChrome(), headless: true, args: mountArgs() });
|
|
5456
5678
|
const browser = await chromium7.connect(server.wsEndpoint());
|
|
5457
5679
|
try {
|
|
@@ -5541,17 +5763,17 @@ export const jsxs = (t, p, k) => real.jsxs(t, inject(t, p), k);
|
|
|
5541
5763
|
});
|
|
5542
5764
|
|
|
5543
5765
|
// packages/verify/src/occlusion.ts
|
|
5544
|
-
import { existsSync as
|
|
5545
|
-
import
|
|
5766
|
+
import { existsSync as existsSync15 } from "node:fs";
|
|
5767
|
+
import path20 from "node:path";
|
|
5546
5768
|
import { build as build7 } from "esbuild";
|
|
5547
5769
|
import { chromium as chromium8 } from "playwright-core";
|
|
5548
5770
|
async function compileTwoUp(task, bundleDir) {
|
|
5549
|
-
const entryTsx =
|
|
5550
|
-
if (!
|
|
5771
|
+
const entryTsx = path20.join(bundleDir, task.entry);
|
|
5772
|
+
if (!existsSync15(entryTsx)) return { error: `${task.entry} missing` };
|
|
5551
5773
|
const src = `
|
|
5552
5774
|
import { createElement } from "react";
|
|
5553
5775
|
import { createRoot } from "react-dom/client";
|
|
5554
|
-
import * as B from ${JSON.stringify(
|
|
5776
|
+
import * as B from ${JSON.stringify(path20.resolve(entryTsx))};
|
|
5555
5777
|
const cfg = (window as unknown as { __cfg: { component: string; props: Record<string, unknown> } }).__cfg;
|
|
5556
5778
|
const C = (B as Record<string, unknown>)[cfg.component] as Parameters<typeof createElement>[0] | undefined;
|
|
5557
5779
|
for (const id of ["first", "second"]) {
|
|
@@ -5720,6 +5942,7 @@ var init_src4 = __esm({
|
|
|
5720
5942
|
init_prelude();
|
|
5721
5943
|
init_mount_limits();
|
|
5722
5944
|
init_font_collection();
|
|
5945
|
+
init_font_discovery();
|
|
5723
5946
|
init_font_faces();
|
|
5724
5947
|
init_font_resolve();
|
|
5725
5948
|
init_paths();
|
|
@@ -5730,23 +5953,23 @@ var init_src4 = __esm({
|
|
|
5730
5953
|
});
|
|
5731
5954
|
|
|
5732
5955
|
// packages/cli/src/environment.ts
|
|
5733
|
-
import { existsSync as
|
|
5734
|
-
import
|
|
5956
|
+
import { existsSync as existsSync16, readFileSync as readFileSync12 } from "node:fs";
|
|
5957
|
+
import path21 from "node:path";
|
|
5735
5958
|
import { createHash as createHash3 } from "node:crypto";
|
|
5736
5959
|
import { fileURLToPath as fileURLToPath5 } from "node:url";
|
|
5737
5960
|
function cliVersion() {
|
|
5738
5961
|
try {
|
|
5739
|
-
return JSON.parse(
|
|
5962
|
+
return JSON.parse(readFileSync12(path21.join(path21.dirname(fileURLToPath5(import.meta.url)), "..", "package.json"), "utf8")).version ?? "dev";
|
|
5740
5963
|
} catch {
|
|
5741
5964
|
return "dev";
|
|
5742
5965
|
}
|
|
5743
5966
|
}
|
|
5744
5967
|
function environmentStamp(taskFamilies) {
|
|
5745
|
-
const manifestPath2 =
|
|
5968
|
+
const manifestPath2 = path21.join(fontCacheDir(), "manifest.json");
|
|
5746
5969
|
let fontsHash = null;
|
|
5747
|
-
if (
|
|
5970
|
+
if (existsSync16(manifestPath2)) {
|
|
5748
5971
|
try {
|
|
5749
|
-
const entries = JSON.parse(
|
|
5972
|
+
const entries = JSON.parse(readFileSync12(manifestPath2, "utf8"));
|
|
5750
5973
|
const scoped = taskFamilies === void 0 || taskFamilies === null ? entries : entries.filter((f) => taskFamilies.some((fam) => fam.toLowerCase() === f.family.toLowerCase()));
|
|
5751
5974
|
const faces = scoped.map((f) => `${f.family}:${f.weight}:${f.sha256}`).sort();
|
|
5752
5975
|
fontsHash = faces.length === 0 ? null : createHash3("sha256").update(faces.join("\n")).digest("hex").slice(0, 16);
|
|
@@ -5790,8 +6013,8 @@ var init_describe = __esm({
|
|
|
5790
6013
|
});
|
|
5791
6014
|
|
|
5792
6015
|
// packages/cli/src/env.ts
|
|
5793
|
-
import { existsSync as
|
|
5794
|
-
import
|
|
6016
|
+
import { existsSync as existsSync17, readFileSync as readFileSync13 } from "node:fs";
|
|
6017
|
+
import path22 from "node:path";
|
|
5795
6018
|
function parseEnv(content) {
|
|
5796
6019
|
const entries = /* @__PURE__ */ new Map();
|
|
5797
6020
|
for (const line of content.split("\n")) {
|
|
@@ -5803,9 +6026,9 @@ function parseEnv(content) {
|
|
|
5803
6026
|
function resolveCredential(name) {
|
|
5804
6027
|
const fromProcess = process.env[name];
|
|
5805
6028
|
if (fromProcess) return fromProcess;
|
|
5806
|
-
const envPath =
|
|
5807
|
-
if (!
|
|
5808
|
-
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);
|
|
5809
6032
|
}
|
|
5810
6033
|
var init_env = __esm({
|
|
5811
6034
|
"packages/cli/src/env.ts"() {
|
|
@@ -5865,17 +6088,17 @@ var init_output = __esm({
|
|
|
5865
6088
|
});
|
|
5866
6089
|
|
|
5867
6090
|
// packages/cli/src/entitlement.ts
|
|
5868
|
-
import { chmodSync, existsSync as
|
|
6091
|
+
import { chmodSync, existsSync as existsSync18, mkdirSync as mkdirSync4, readFileSync as readFileSync14, writeFileSync as writeFileSync7 } from "node:fs";
|
|
5869
6092
|
import crypto from "node:crypto";
|
|
5870
|
-
import
|
|
5871
|
-
import
|
|
6093
|
+
import os4 from "node:os";
|
|
6094
|
+
import path23 from "node:path";
|
|
5872
6095
|
function entitlementPath() {
|
|
5873
|
-
return process.env["TENDRIL_ENTITLEMENT_PATH"] ??
|
|
6096
|
+
return process.env["TENDRIL_ENTITLEMENT_PATH"] ?? path23.join(os4.homedir(), ".tendril", "entitlement.json");
|
|
5874
6097
|
}
|
|
5875
6098
|
function readStoredEntitlement(file = entitlementPath()) {
|
|
5876
|
-
if (!
|
|
6099
|
+
if (!existsSync18(file)) return void 0;
|
|
5877
6100
|
try {
|
|
5878
|
-
const parsed = JSON.parse(
|
|
6101
|
+
const parsed = JSON.parse(readFileSync14(file, "utf8"));
|
|
5879
6102
|
if (typeof parsed.token !== "string" || typeof parsed.lastRefreshAt !== "number") return void 0;
|
|
5880
6103
|
return { token: parsed.token, lastRefreshAt: parsed.lastRefreshAt };
|
|
5881
6104
|
} catch {
|
|
@@ -5883,7 +6106,7 @@ function readStoredEntitlement(file = entitlementPath()) {
|
|
|
5883
6106
|
}
|
|
5884
6107
|
}
|
|
5885
6108
|
function writeStoredEntitlement(stored, file = entitlementPath()) {
|
|
5886
|
-
mkdirSync4(
|
|
6109
|
+
mkdirSync4(path23.dirname(file), { recursive: true });
|
|
5887
6110
|
writeFileSync7(file, `${JSON.stringify(stored, null, 2)}
|
|
5888
6111
|
`);
|
|
5889
6112
|
chmodSync(file, 384);
|
|
@@ -5968,9 +6191,9 @@ var init_entitlement = __esm({
|
|
|
5968
6191
|
|
|
5969
6192
|
// packages/cli/src/commands/doctor.ts
|
|
5970
6193
|
import { spawnSync } from "node:child_process";
|
|
5971
|
-
import { existsSync as
|
|
5972
|
-
import
|
|
5973
|
-
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";
|
|
5974
6197
|
function withDeadline(work, ms) {
|
|
5975
6198
|
return Promise.race([
|
|
5976
6199
|
work,
|
|
@@ -6030,19 +6253,19 @@ async function runDoctorChecks(options) {
|
|
|
6030
6253
|
remediation: "Install Google Chrome (or Chromium), or set CHROME_PATH to a Chromium-family browser binary."
|
|
6031
6254
|
});
|
|
6032
6255
|
}
|
|
6033
|
-
const fontManifest =
|
|
6256
|
+
const fontManifest = path24.join(fontCacheDir(), "manifest.json");
|
|
6034
6257
|
checks.push(
|
|
6035
|
-
|
|
6258
|
+
existsSync19(fontManifest) ? { name: "font-cache", ok: true, detail: `resolved font cache at ${fontCacheDir()} (${JSON.parse(readFileSync15(fontManifest, "utf8")).length} faces)` } : {
|
|
6036
6259
|
name: "font-cache",
|
|
6037
6260
|
ok: true,
|
|
6038
6261
|
detail: `font cache empty at ${fontCacheDir()} \u2014 normal on a fresh machine; faces resolve per design system at first use`,
|
|
6039
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.`
|
|
6040
6263
|
}
|
|
6041
6264
|
);
|
|
6042
|
-
const pluginRoot =
|
|
6043
|
-
if (
|
|
6265
|
+
const pluginRoot = path24.join(os5.homedir(), ".claude", "plugins", "cache", "tendrilapp", "tendril");
|
|
6266
|
+
if (existsSync19(pluginRoot)) {
|
|
6044
6267
|
try {
|
|
6045
|
-
const versions =
|
|
6268
|
+
const versions = readdirSync4(pluginRoot).filter((v) => /^\d+\.\d+\.\d+$/.test(v));
|
|
6046
6269
|
const newest = versions.sort((a, b) => versionIsNewer(a, b) ? 1 : -1)[0];
|
|
6047
6270
|
if (newest !== void 0) {
|
|
6048
6271
|
const skewed = versionIsNewer(newest, cliVersion());
|
|
@@ -7566,9 +7789,9 @@ __export(record_exports, {
|
|
|
7566
7789
|
runRecordPlan: () => runRecordPlan,
|
|
7567
7790
|
runRecordStatus: () => runRecordStatus
|
|
7568
7791
|
});
|
|
7569
|
-
import { existsSync as
|
|
7570
|
-
import
|
|
7571
|
-
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";
|
|
7572
7795
|
import { writeFileSync as writeFileSync9 } from "node:fs";
|
|
7573
7796
|
function recordsInteractionState(reports) {
|
|
7574
7797
|
const evident = (s) => stateTokens(s).some((t) => INTERACTION_EVIDENCE_VALUES.has(t));
|
|
@@ -7591,7 +7814,7 @@ function interactionDisclosure(component, reports) {
|
|
|
7591
7814
|
};
|
|
7592
7815
|
}
|
|
7593
7816
|
function symbolsFromMetadataEnvelope(file, sourceFrame) {
|
|
7594
|
-
const env = JSON.parse(
|
|
7817
|
+
const env = JSON.parse(readFileSync17(file, "utf8"));
|
|
7595
7818
|
const text = env.content.map((c) => c.text ?? "").join("\n");
|
|
7596
7819
|
const symbols = [];
|
|
7597
7820
|
const walk2 = (node, ancestor) => {
|
|
@@ -7649,7 +7872,7 @@ function runRecordPlan(opts) {
|
|
|
7649
7872
|
if (rawFile !== void 0) {
|
|
7650
7873
|
try {
|
|
7651
7874
|
const envelope = rawEnvelopeFromFile(rawFile, opts.metadataRawPartsFile !== void 0);
|
|
7652
|
-
const tmp =
|
|
7875
|
+
const tmp = path27.join(mkdtempSync2(path27.join(os6.tmpdir(), "tendril-plan-meta-")), "get_metadata.json");
|
|
7653
7876
|
writeFileSync9(tmp, JSON.stringify(envelope));
|
|
7654
7877
|
metadataEntries.push({ file: tmp });
|
|
7655
7878
|
} catch (err) {
|
|
@@ -7671,7 +7894,7 @@ function runRecordPlan(opts) {
|
|
|
7671
7894
|
let metadataTruncated = false;
|
|
7672
7895
|
for (const { file, frame } of metadataEntries) {
|
|
7673
7896
|
try {
|
|
7674
|
-
const parsed = symbolsFromMetadataEnvelope(
|
|
7897
|
+
const parsed = symbolsFromMetadataEnvelope(path27.resolve(file), frame);
|
|
7675
7898
|
symbols.push(...parsed.symbols);
|
|
7676
7899
|
if (parsed.truncated) metadataTruncated = true;
|
|
7677
7900
|
} catch (err) {
|
|
@@ -7705,7 +7928,7 @@ function runRecordPlan(opts) {
|
|
|
7705
7928
|
if (symbols.length === 0) {
|
|
7706
7929
|
const leads = metadataEntries.flatMap(({ file }) => {
|
|
7707
7930
|
try {
|
|
7708
|
-
const env = JSON.parse(
|
|
7931
|
+
const env = JSON.parse(readFileSync17(path27.resolve(file), "utf8"));
|
|
7709
7932
|
return instanceLeads(env.content.map((c) => c.text ?? "").join("\n"));
|
|
7710
7933
|
} catch {
|
|
7711
7934
|
return [];
|
|
@@ -7779,7 +8002,7 @@ function runRecordPlan(opts) {
|
|
|
7779
8002
|
// a CLI flag and the MCP surface has no parameter for it.
|
|
7780
8003
|
decidedBy: "HUMAN ONLY \u2014 offer it, never choose it, and you cannot run it",
|
|
7781
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.",
|
|
7782
|
-
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`]
|
|
7783
8006
|
},
|
|
7784
8007
|
{
|
|
7785
8008
|
id: "larger-allowance",
|
|
@@ -7906,7 +8129,7 @@ function nextPayload(setDir) {
|
|
|
7906
8129
|
const instruction = nextInstruction(setDir);
|
|
7907
8130
|
const status = sessionStatus(setDir);
|
|
7908
8131
|
const progress = { recordedReps: status.reps.filter((x) => x.missing.length === 0).length, totalReps: status.reps.length };
|
|
7909
|
-
if (instruction === null && !
|
|
8132
|
+
if (instruction === null && !existsSync21(path27.join(setDir, "get_variable_defs.json"))) {
|
|
7910
8133
|
const manifest = loadManifest(setDir);
|
|
7911
8134
|
const frameNode = Object.keys(manifest.sourceFrames ?? {})[0] ?? manifest.reps[0]?.nodeId ?? "";
|
|
7912
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 };
|
|
@@ -7935,7 +8158,7 @@ function runRecordNext(opts) {
|
|
|
7935
8158
|
const progress = payload["progress"];
|
|
7936
8159
|
process.stdout.write(`[${progress.recordedReps}/${progress.totalReps} reps] ${payload["tool"]} for ${payload["slug"]} (node ${payload["nodeId"]})
|
|
7937
8160
|
\u2192 ${payload["note"]}
|
|
7938
|
-
\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>`)}
|
|
7939
8162
|
`);
|
|
7940
8163
|
});
|
|
7941
8164
|
}
|
|
@@ -8009,7 +8232,7 @@ async function autoFetchAssets(setDir, rep, envelopeText2) {
|
|
|
8009
8232
|
const skipped = [];
|
|
8010
8233
|
const failed = [];
|
|
8011
8234
|
for (const { url, name } of assetUrlsFromEnvelopeText(envelopeText2)) {
|
|
8012
|
-
if (
|
|
8235
|
+
if (existsSync21(path27.join(setDir, rep, name))) {
|
|
8013
8236
|
skipped.push(name);
|
|
8014
8237
|
continue;
|
|
8015
8238
|
}
|
|
@@ -8031,16 +8254,16 @@ async function autoFetchAssets(setDir, rep, envelopeText2) {
|
|
|
8031
8254
|
}
|
|
8032
8255
|
function rawEnvelopeFromFile(file, parts) {
|
|
8033
8256
|
if (parts) {
|
|
8034
|
-
const blocks = JSON.parse(
|
|
8257
|
+
const blocks = JSON.parse(readFileSync17(path27.resolve(file), "utf8"));
|
|
8035
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");
|
|
8036
8259
|
return { content: blocks.map((text) => ({ type: "text", text })) };
|
|
8037
8260
|
}
|
|
8038
|
-
return { content: [{ type: "text", text:
|
|
8261
|
+
return { content: [{ type: "text", text: readFileSync17(path27.resolve(file), "utf8") }] };
|
|
8039
8262
|
}
|
|
8040
8263
|
async function runRecordIngest(opts) {
|
|
8041
8264
|
let payload;
|
|
8042
8265
|
try {
|
|
8043
|
-
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"));
|
|
8044
8267
|
} catch (err) {
|
|
8045
8268
|
fail(opts, ExitCode.InputValidation, {
|
|
8046
8269
|
error: `cannot read envelope file: ${err instanceof Error ? err.message : String(err)}`,
|
|
@@ -8052,7 +8275,7 @@ async function runRecordIngest(opts) {
|
|
|
8052
8275
|
fail(opts, ExitCode.InputValidation, {
|
|
8053
8276
|
error: "--raw is for text tool responses; screenshots are binary",
|
|
8054
8277
|
code: "envelope-invalid",
|
|
8055
|
-
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.`
|
|
8056
8279
|
});
|
|
8057
8280
|
}
|
|
8058
8281
|
if (opts.rep === "__set__" && opts.tool === "get_variable_defs") {
|
|
@@ -8072,7 +8295,7 @@ async function runRecordIngest(opts) {
|
|
|
8072
8295
|
remediation: REINGEST_GUIDANCE
|
|
8073
8296
|
});
|
|
8074
8297
|
}
|
|
8075
|
-
writeFileSync9(
|
|
8298
|
+
writeFileSync9(path27.join(opts.setDir, "get_variable_defs.json"), `${JSON.stringify(payload, null, 1)}
|
|
8076
8299
|
`);
|
|
8077
8300
|
emitData(opts, { ingested: "get_variable_defs", rep: "__set__", setLevel: true, next: nextPayload(opts.setDir) }, () => {
|
|
8078
8301
|
process.stdout.write("set-level get_variable_defs ingested\n");
|
|
@@ -8088,7 +8311,7 @@ async function runRecordIngest(opts) {
|
|
|
8088
8311
|
if (assets !== void 0) {
|
|
8089
8312
|
for (const a of assets.fetched) process.stdout.write(` asset fetched ${a}
|
|
8090
8313
|
`);
|
|
8091
|
-
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>`)}
|
|
8092
8315
|
`);
|
|
8093
8316
|
}
|
|
8094
8317
|
});
|
|
@@ -8161,15 +8384,15 @@ async function runRecordIngestRep(opts) {
|
|
|
8161
8384
|
if (assets !== void 0) {
|
|
8162
8385
|
for (const a of assets.fetched) process.stdout.write(` asset fetched ${a}
|
|
8163
8386
|
`);
|
|
8164
|
-
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>`)}
|
|
8165
8388
|
`);
|
|
8166
8389
|
}
|
|
8167
8390
|
});
|
|
8168
8391
|
}
|
|
8169
8392
|
function runRecordAsset(opts) {
|
|
8170
8393
|
if (opts.dir !== void 0) {
|
|
8171
|
-
const dir =
|
|
8172
|
-
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));
|
|
8173
8396
|
if (names.length === 0) {
|
|
8174
8397
|
fail(opts, ExitCode.InputValidation, {
|
|
8175
8398
|
error: `no asset-*.<ext> files found in ${dir}`,
|
|
@@ -8180,7 +8403,7 @@ function runRecordAsset(opts) {
|
|
|
8180
8403
|
const ingested = [];
|
|
8181
8404
|
try {
|
|
8182
8405
|
for (const name of names) {
|
|
8183
|
-
ingestAsset(opts.setDir, opts.rep, name,
|
|
8406
|
+
ingestAsset(opts.setDir, opts.rep, name, readFileSync17(path27.join(dir, name)));
|
|
8184
8407
|
ingested.push(name);
|
|
8185
8408
|
}
|
|
8186
8409
|
} catch (err) {
|
|
@@ -8200,11 +8423,11 @@ function runRecordAsset(opts) {
|
|
|
8200
8423
|
fail(opts, ExitCode.InputValidation, {
|
|
8201
8424
|
error: "pass --name and --file for a single asset, or --dir for a batch",
|
|
8202
8425
|
code: "asset-rejected",
|
|
8203
|
-
remediation: tendrilCommand(`record asset --set ${
|
|
8426
|
+
remediation: tendrilCommand(`record asset --set ${path27.resolve(opts.setDir)} --rep ${opts.rep} --dir <downloads-dir>`)
|
|
8204
8427
|
});
|
|
8205
8428
|
}
|
|
8206
8429
|
try {
|
|
8207
|
-
ingestAsset(opts.setDir, opts.rep, opts.name,
|
|
8430
|
+
ingestAsset(opts.setDir, opts.rep, opts.name, readFileSync17(path27.resolve(opts.file)));
|
|
8208
8431
|
emitData(opts, { rep: opts.rep, asset: opts.name }, () => {
|
|
8209
8432
|
process.stdout.write(`ingested ${opts.rep}/${opts.name}
|
|
8210
8433
|
`);
|
|
@@ -8240,7 +8463,7 @@ function narrowedRoles(derived, override) {
|
|
|
8240
8463
|
function rolesFromFile(opts, file, derived) {
|
|
8241
8464
|
let json;
|
|
8242
8465
|
try {
|
|
8243
|
-
json = JSON.parse(
|
|
8466
|
+
json = JSON.parse(readFileSync17(path27.resolve(file), "utf8"));
|
|
8244
8467
|
} catch (err) {
|
|
8245
8468
|
fail(opts, ExitCode.InputValidation, {
|
|
8246
8469
|
error: `cannot read roles file ${file}: ${err instanceof Error ? err.message.split("\n")[0] : String(err)}`,
|
|
@@ -8278,11 +8501,11 @@ function rolesFromFile(opts, file, derived) {
|
|
|
8278
8501
|
};
|
|
8279
8502
|
}
|
|
8280
8503
|
function runRecordFinish(opts) {
|
|
8281
|
-
if (!
|
|
8504
|
+
if (!existsSync21(path27.join(opts.setDir, "recording-set.json"))) {
|
|
8282
8505
|
fail(opts, ExitCode.InputValidation, {
|
|
8283
8506
|
error: `no recording-set.json in ${opts.setDir}`,
|
|
8284
8507
|
code: "no-recording-set",
|
|
8285
|
-
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.`
|
|
8286
8509
|
});
|
|
8287
8510
|
}
|
|
8288
8511
|
const { manifest, raw } = readManifestFile(opts.setDir);
|
|
@@ -8310,17 +8533,17 @@ function runRecordFinish(opts) {
|
|
|
8310
8533
|
fail(opts, ExitCode.ConfirmationRequired, {
|
|
8311
8534
|
error: "--confirm-roles (and --roles-file) require an interactive terminal \u2014 this confirmation is human-only",
|
|
8312
8535
|
code: "roles-confirmation-not-interactive",
|
|
8313
|
-
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.`
|
|
8314
8537
|
});
|
|
8315
8538
|
}
|
|
8316
8539
|
const merged = { ...raw, roles };
|
|
8317
|
-
const { issues } = validateRecordingSet(merged, (rel) =>
|
|
8540
|
+
const { issues } = validateRecordingSet(merged, (rel) => existsSync21(path27.join(opts.setDir, rel)));
|
|
8318
8541
|
const errors = issues.filter((i) => i.severity === "error");
|
|
8319
8542
|
if (errors.length > 0) {
|
|
8320
8543
|
fail(opts, ExitCode.InputValidation, {
|
|
8321
8544
|
error: `recording set rejected \u2014 roles NOT written: ${errors.map((e) => e.message).join("; ")}`,
|
|
8322
8545
|
code: "recording-set-invalid",
|
|
8323
|
-
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\`.`
|
|
8324
8547
|
});
|
|
8325
8548
|
}
|
|
8326
8549
|
for (const issue of issues) if (issue.severity === "warning") warn(opts, issue.message);
|
|
@@ -8554,8 +8777,8 @@ var init_engine_curated = __esm({
|
|
|
8554
8777
|
});
|
|
8555
8778
|
|
|
8556
8779
|
// packages/generate/src/loop.ts
|
|
8557
|
-
import { existsSync as
|
|
8558
|
-
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";
|
|
8559
8782
|
import { z as z11 } from "zod";
|
|
8560
8783
|
function objective(scores, behaviors) {
|
|
8561
8784
|
const vals = scores.map((s) => Math.min(s.similarity, s.inkRecall));
|
|
@@ -8592,9 +8815,9 @@ ${absentLines.join("\n")}` : ""}
|
|
|
8592
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."}`;
|
|
8593
8816
|
}
|
|
8594
8817
|
function archivePriorRun(outDir) {
|
|
8595
|
-
if (!
|
|
8818
|
+
if (!existsSync22(path28.join(outDir, "run-log.json")) && !existsSync22(path28.join(outDir, "loop-state.json"))) return void 0;
|
|
8596
8819
|
let n = 1;
|
|
8597
|
-
while (
|
|
8820
|
+
while (existsSync22(`${outDir}-prev-${n}`)) n += 1;
|
|
8598
8821
|
renameSync(outDir, `${outDir}-prev-${n}`);
|
|
8599
8822
|
return `${outDir}-prev-${n}`;
|
|
8600
8823
|
}
|
|
@@ -8603,14 +8826,14 @@ async function runEngineLoop(opts) {
|
|
|
8603
8826
|
const plateau = opts.plateau ?? 2;
|
|
8604
8827
|
const progress = opts.onProgress ?? (() => {
|
|
8605
8828
|
});
|
|
8606
|
-
const statePath =
|
|
8607
|
-
const resuming = opts.resume === true &&
|
|
8829
|
+
const statePath = path28.join(opts.outDir, "loop-state.json");
|
|
8830
|
+
const resuming = opts.resume === true && existsSync22(statePath);
|
|
8608
8831
|
if (!resuming) {
|
|
8609
8832
|
const archived = archivePriorRun(opts.outDir);
|
|
8610
8833
|
if (archived !== void 0) progress(`previous run archived to ${archived}`);
|
|
8611
8834
|
}
|
|
8612
8835
|
mkdirSync6(opts.outDir, { recursive: true });
|
|
8613
|
-
const scratch =
|
|
8836
|
+
const scratch = path28.join(opts.outDir, ".candidate");
|
|
8614
8837
|
let attempts = [];
|
|
8615
8838
|
let log = [];
|
|
8616
8839
|
let best;
|
|
@@ -8618,7 +8841,7 @@ async function runEngineLoop(opts) {
|
|
|
8618
8841
|
let nonAccepted = 0;
|
|
8619
8842
|
let stopReason = "max-iterations";
|
|
8620
8843
|
if (resuming) {
|
|
8621
|
-
const restored = LoopStateSchema.parse(JSON.parse(
|
|
8844
|
+
const restored = LoopStateSchema.parse(JSON.parse(readFileSync18(statePath, "utf8")));
|
|
8622
8845
|
attempts = restored.attempts;
|
|
8623
8846
|
log = restored.iterations;
|
|
8624
8847
|
spentUsd = restored.spentUsd;
|
|
@@ -8638,7 +8861,7 @@ async function runEngineLoop(opts) {
|
|
|
8638
8861
|
};
|
|
8639
8862
|
const writeCandidate = (files) => {
|
|
8640
8863
|
mkdirSync6(scratch, { recursive: true });
|
|
8641
|
-
for (const [name, content] of Object.entries(files)) writeFileSync10(
|
|
8864
|
+
for (const [name, content] of Object.entries(files)) writeFileSync10(path28.join(scratch, name), content);
|
|
8642
8865
|
};
|
|
8643
8866
|
const scoreCandidate = async (candidate, iter, usd, modelMs) => {
|
|
8644
8867
|
writeCandidate(candidate.files);
|
|
@@ -8696,8 +8919,8 @@ async function runEngineLoop(opts) {
|
|
|
8696
8919
|
const usd = candidate.usage?.usd ?? 0;
|
|
8697
8920
|
spentUsd += usd;
|
|
8698
8921
|
if (candidate.raw !== void 0) {
|
|
8699
|
-
mkdirSync6(
|
|
8700
|
-
writeFileSync10(
|
|
8922
|
+
mkdirSync6(path28.join(opts.outDir, "responses"), { recursive: true });
|
|
8923
|
+
writeFileSync10(path28.join(opts.outDir, "responses", `iter-${iter}.md`), candidate.raw);
|
|
8701
8924
|
}
|
|
8702
8925
|
if (candidate.files[opts.entry] === void 0 || candidate.files["styles.css"] === void 0) {
|
|
8703
8926
|
const finish = candidate.usage?.finishReason ?? "?";
|
|
@@ -8723,10 +8946,10 @@ async function runEngineLoop(opts) {
|
|
|
8723
8946
|
}
|
|
8724
8947
|
}
|
|
8725
8948
|
}
|
|
8726
|
-
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);
|
|
8727
8950
|
const final = best !== void 0 ? await opts.score(opts.outDir) : { scores: [], behaviors: [] };
|
|
8728
8951
|
writeFileSync10(
|
|
8729
|
-
|
|
8952
|
+
path28.join(opts.outDir, "run-log.json"),
|
|
8730
8953
|
`${JSON.stringify(
|
|
8731
8954
|
{
|
|
8732
8955
|
...opts.meta,
|
|
@@ -8793,8 +9016,8 @@ var init_loop2 = __esm({
|
|
|
8793
9016
|
});
|
|
8794
9017
|
|
|
8795
9018
|
// packages/generate/src/brief.ts
|
|
8796
|
-
import { existsSync as
|
|
8797
|
-
import
|
|
9019
|
+
import { existsSync as existsSync23, readFileSync as readFileSync19 } from "node:fs";
|
|
9020
|
+
import path29 from "node:path";
|
|
8798
9021
|
function singleAxes2(name) {
|
|
8799
9022
|
const parsed = parseVariantAxes(name);
|
|
8800
9023
|
if (parsed === void 0) return void 0;
|
|
@@ -9035,12 +9258,12 @@ function authorBehaviors(api, extras = {}) {
|
|
|
9035
9258
|
return { behaviors, prelude: { controls: interactive ? ["> *"] : [], textInputs: [] }, disclosures };
|
|
9036
9259
|
}
|
|
9037
9260
|
function envelopeText(file) {
|
|
9038
|
-
return envelopeFirstTextPart(JSON.parse(
|
|
9261
|
+
return envelopeFirstTextPart(JSON.parse(readFileSync19(file, "utf8")));
|
|
9039
9262
|
}
|
|
9040
9263
|
function dismissEvidence(setDir, repSlugs) {
|
|
9041
9264
|
for (const slug of repSlugs) {
|
|
9042
|
-
const f =
|
|
9043
|
-
if (!
|
|
9265
|
+
const f = path29.join(setDir, slug, "get_design_context.json");
|
|
9266
|
+
if (!existsSync23(f)) continue;
|
|
9044
9267
|
const text = envelopeText(f);
|
|
9045
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);
|
|
9046
9269
|
if (propHit !== null) return `emission prop "${propHit[1]}"`;
|
|
@@ -9084,13 +9307,13 @@ function recordedFontNeeds(setDir, opts = {}) {
|
|
|
9084
9307
|
}
|
|
9085
9308
|
};
|
|
9086
9309
|
const manifest = loadManifest(setDir);
|
|
9087
|
-
const setDefs =
|
|
9088
|
-
if (
|
|
9310
|
+
const setDefs = path29.join(setDir, "get_variable_defs.json");
|
|
9311
|
+
if (existsSync23(setDefs)) fromDefs(envelopeText(setDefs));
|
|
9089
9312
|
for (const rep of manifest.reps) {
|
|
9090
|
-
const ctx =
|
|
9091
|
-
if (
|
|
9092
|
-
const defs =
|
|
9093
|
-
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));
|
|
9094
9317
|
}
|
|
9095
9318
|
return [...byFamily.entries()].map(([family, paired]) => {
|
|
9096
9319
|
const weights = /* @__PURE__ */ new Set([...paired, ...opts.pairedOnly === true ? [] : unpaired]);
|
|
@@ -9101,10 +9324,10 @@ function symbolFontGlyphCount(setDir, reps) {
|
|
|
9101
9324
|
const PUA = /[\uE000-\uF8FF\u{F0000}-\u{FFFFD}\u{100000}-\u{10FFFD}]/u;
|
|
9102
9325
|
const glyphs = /* @__PURE__ */ new Set();
|
|
9103
9326
|
for (const rep of reps) {
|
|
9104
|
-
const file =
|
|
9105
|
-
if (!
|
|
9327
|
+
const file = path29.join(setDir, rep, "get_metadata.json");
|
|
9328
|
+
if (!existsSync23(file)) continue;
|
|
9106
9329
|
try {
|
|
9107
|
-
const text = JSON.parse(
|
|
9330
|
+
const text = JSON.parse(readFileSync19(file, "utf8")).content.map((c) => c.text ?? "").join("\n");
|
|
9108
9331
|
for (const m of text.matchAll(/<text\s[^>]*name="([^"]*)"/g)) {
|
|
9109
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)));
|
|
9110
9333
|
if (PUA.test(name)) glyphs.add(name);
|
|
@@ -9130,8 +9353,8 @@ function recordedTextSlots(setDir, repSlugs) {
|
|
|
9130
9353
|
const propRep = [];
|
|
9131
9354
|
const perRep = [];
|
|
9132
9355
|
for (const slug of repSlugs) {
|
|
9133
|
-
const f =
|
|
9134
|
-
if (!
|
|
9356
|
+
const f = path29.join(setDir, slug, "get_design_context.json");
|
|
9357
|
+
if (!existsSync23(f)) continue;
|
|
9135
9358
|
const code = envelopeText(f);
|
|
9136
9359
|
const props = /* @__PURE__ */ new Map();
|
|
9137
9360
|
for (const m of code.matchAll(/[{,]\s*(\w+)\s*=\s*"((?:[^"\\]|\\.)*)"/g)) {
|
|
@@ -9156,8 +9379,8 @@ function recordedTextSlots(setDir, repSlugs) {
|
|
|
9156
9379
|
}
|
|
9157
9380
|
const axisValuesBySlug = /* @__PURE__ */ new Map();
|
|
9158
9381
|
for (const slug of repSlugs) {
|
|
9159
|
-
const metaFile =
|
|
9160
|
-
if (!
|
|
9382
|
+
const metaFile = path29.join(setDir, slug, "get_metadata.json");
|
|
9383
|
+
if (!existsSync23(metaFile)) continue;
|
|
9161
9384
|
const name = symbolName(envelopeText(metaFile));
|
|
9162
9385
|
if (name === void 0) continue;
|
|
9163
9386
|
const values = /* @__PURE__ */ new Set();
|
|
@@ -9256,8 +9479,8 @@ function authorTaskFromSet(setDir, opts = {}) {
|
|
|
9256
9479
|
const poses = [];
|
|
9257
9480
|
const missing = [];
|
|
9258
9481
|
for (const rep of manifest.reps) {
|
|
9259
|
-
const metaFile =
|
|
9260
|
-
if (!
|
|
9482
|
+
const metaFile = path29.join(setDir, rep.slug, "get_metadata.json");
|
|
9483
|
+
if (!existsSync23(metaFile)) {
|
|
9261
9484
|
missing.push(rep.slug);
|
|
9262
9485
|
continue;
|
|
9263
9486
|
}
|
|
@@ -9271,8 +9494,8 @@ function authorTaskFromSet(setDir, opts = {}) {
|
|
|
9271
9494
|
if (missing.length > 0) {
|
|
9272
9495
|
throw new Error(`recording set ${setDir} is missing metadata for ${missing.length} rep(s): ${missing.slice(0, 5).join(", ")}${missing.length > 5 ? ", \u2026" : ""}`);
|
|
9273
9496
|
}
|
|
9274
|
-
const setMeta =
|
|
9275
|
-
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);
|
|
9276
9499
|
const defaults = { ...manifest.defaults ?? {}, ...opts.defaults ?? {} };
|
|
9277
9500
|
const recordedFonts = recordedFontFamilies(setDir);
|
|
9278
9501
|
const textSlots = recordedTextSlots(setDir, manifest.reps.map((r) => r.slug));
|
|
@@ -9455,10 +9678,10 @@ var init_brief = __esm({
|
|
|
9455
9678
|
});
|
|
9456
9679
|
|
|
9457
9680
|
// packages/generate/src/segments.ts
|
|
9458
|
-
import { existsSync as
|
|
9459
|
-
import
|
|
9681
|
+
import { existsSync as existsSync24, readFileSync as readFileSync20, readdirSync as readdirSync7 } from "node:fs";
|
|
9682
|
+
import path30 from "node:path";
|
|
9460
9683
|
function repText(set, rep, tool) {
|
|
9461
|
-
const env = JSON.parse(
|
|
9684
|
+
const env = JSON.parse(readFileSync20(path30.join(set, rep, `${tool}.json`), "utf8"));
|
|
9462
9685
|
return tool === "get_design_context" ? envelopeFirstTextPart(env) : envelopeTextContent(env);
|
|
9463
9686
|
}
|
|
9464
9687
|
function stripFigmaInstructions(emission) {
|
|
@@ -9518,18 +9741,20 @@ DROPPED from the map, untrustworthy in the source export \u2014 for these, each
|
|
|
9518
9741
|
}
|
|
9519
9742
|
function buildSegments(task, mode = "fenced") {
|
|
9520
9743
|
const SET = task.set;
|
|
9744
|
+
let defsRecorded = existsSync24(path30.join(SET, "get_variable_defs.json"));
|
|
9521
9745
|
let rawDefs = {};
|
|
9522
|
-
if (
|
|
9523
|
-
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"))) || "{}";
|
|
9524
9748
|
try {
|
|
9525
9749
|
rawDefs = JSON.parse(text);
|
|
9526
9750
|
} catch {
|
|
9527
9751
|
}
|
|
9528
9752
|
} else {
|
|
9529
9753
|
for (const cfg of task.configs) {
|
|
9530
|
-
const f =
|
|
9531
|
-
if (!
|
|
9532
|
-
|
|
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"))) || "{}";
|
|
9533
9758
|
try {
|
|
9534
9759
|
for (const [k, v] of Object.entries(JSON.parse(text))) rawDefs[k] ??= v;
|
|
9535
9760
|
} catch {
|
|
@@ -9537,25 +9762,27 @@ function buildSegments(task, mode = "fenced") {
|
|
|
9537
9762
|
}
|
|
9538
9763
|
}
|
|
9539
9764
|
const emissionTexts = task.configs.map((cfg) => {
|
|
9540
|
-
const f =
|
|
9541
|
-
return
|
|
9765
|
+
const f = path30.join(SET, cfg.rep, "get_design_context.json");
|
|
9766
|
+
return existsSync24(f) ? envelopeFirstTextPart(JSON.parse(readFileSync20(f, "utf8"))) : "";
|
|
9542
9767
|
});
|
|
9543
9768
|
const { map, note } = cleanTokenMap(rawDefs, emissionTexts);
|
|
9544
9769
|
const defs = JSON.stringify(map, null, 1);
|
|
9545
9770
|
const parts = [
|
|
9546
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).",
|
|
9547
|
-
`
|
|
9772
|
+
defsRecorded ? `
|
|
9548
9773
|
## Design tokens (use these CSS custom property names)
|
|
9549
9774
|
\`\`\`json
|
|
9550
9775
|
${defs}
|
|
9551
|
-
\`\`\`${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}`
|
|
9552
9779
|
];
|
|
9553
9780
|
for (const cfg of task.configs) {
|
|
9554
9781
|
const meta = stripFigmaInstructions(repText(SET, cfg.rep, "get_metadata"));
|
|
9555
9782
|
const emission = stripFigmaInstructions(repText(SET, cfg.rep, "get_design_context"));
|
|
9556
|
-
const assets =
|
|
9783
|
+
const assets = readdirSync7(path30.join(SET, cfg.rep)).filter((f) => f.startsWith("asset-") && f.endsWith(".svg")).map((f) => `asset ${f}:
|
|
9557
9784
|
\`\`\`svg
|
|
9558
|
-
${
|
|
9785
|
+
${readFileSync20(path30.join(SET, cfg.rep, f), "utf8")}
|
|
9559
9786
|
\`\`\``).join("\n");
|
|
9560
9787
|
parts.push(`
|
|
9561
9788
|
## Config ${cfg.rep} \u2192 ${cfg.component} ${JSON.stringify(cfg.props)}
|
|
@@ -9580,7 +9807,7 @@ Optionally a third block with \`/* FILE: tokens.css */\`.`);
|
|
|
9580
9807
|
} else {
|
|
9581
9808
|
parts.push(`
|
|
9582
9809
|
## Output format
|
|
9583
|
-
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.`);
|
|
9584
9811
|
}
|
|
9585
9812
|
return parts.join("\n");
|
|
9586
9813
|
}
|
|
@@ -9648,8 +9875,8 @@ var init_adapter = __esm({
|
|
|
9648
9875
|
|
|
9649
9876
|
// packages/generate/src/bundle-emit.ts
|
|
9650
9877
|
import { createHash as createHash4 } from "node:crypto";
|
|
9651
|
-
import { copyFileSync, existsSync as
|
|
9652
|
-
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";
|
|
9653
9880
|
function pinFromConfigs(configs) {
|
|
9654
9881
|
const domains = /* @__PURE__ */ new Map();
|
|
9655
9882
|
const kinds = /* @__PURE__ */ new Map();
|
|
@@ -9718,15 +9945,23 @@ function fontsCssFor(faces, bundleDir, cacheDir = DEFAULT_FONT_CACHE, substitute
|
|
|
9718
9945
|
const notices = [];
|
|
9719
9946
|
const licenseTexts = /* @__PURE__ */ new Map();
|
|
9720
9947
|
for (const face of faces) {
|
|
9721
|
-
const src =
|
|
9722
|
-
const target = `./fonts/${
|
|
9723
|
-
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";
|
|
9724
9951
|
const family = face.family.replace(/['\\]/g, "").replace(/\*\//g, "");
|
|
9725
9952
|
const decl = `@font-face { font-family: '${family}'; font-weight: ${face.weight}; font-style: normal; src: url(${target}) format('${format}'); }`;
|
|
9726
9953
|
const license = normalizeFontLicense(face.license);
|
|
9727
|
-
const terms = REDISTRIBUTABLE.get(license);
|
|
9954
|
+
const terms = face.source.startsWith("system:") ? void 0 : REDISTRIBUTABLE.get(license);
|
|
9728
9955
|
if (terms === void 0) {
|
|
9729
|
-
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:")) {
|
|
9730
9965
|
lines.push(
|
|
9731
9966
|
`/* '${family}' ${face.weight} is a user-licensed face (tendril fonts add; sha256 ${face.sha256.slice(0, 16)}\u2026).`,
|
|
9732
9967
|
` Licensed bytes are never copied into bundles \u2014 place your copy at ${target} and uncomment: */`,
|
|
@@ -9750,14 +9985,14 @@ function fontsCssFor(faces, bundleDir, cacheDir = DEFAULT_FONT_CACHE, substitute
|
|
|
9750
9985
|
`/* ${decl} */`
|
|
9751
9986
|
);
|
|
9752
9987
|
}
|
|
9753
|
-
} else if (
|
|
9754
|
-
mkdirSync7(
|
|
9755
|
-
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)));
|
|
9756
9991
|
licenseTexts.set(terms.file, terms.text);
|
|
9757
9992
|
const upstream = upstreamAttribution(face);
|
|
9758
9993
|
notices.push(
|
|
9759
9994
|
"",
|
|
9760
|
-
`${face.family.replace(/[\r\n]+/g, " ")} ${face.weight} \u2014 ./${
|
|
9995
|
+
`${face.family.replace(/[\r\n]+/g, " ")} ${face.weight} \u2014 ./${path31.basename(face.file)}`,
|
|
9761
9996
|
` licence: ${terms.title} \u2014 full text in ./${terms.file}`,
|
|
9762
9997
|
` source: ${face.source}`,
|
|
9763
9998
|
` sha256: ${face.sha256}`,
|
|
@@ -9771,9 +10006,9 @@ function fontsCssFor(faces, bundleDir, cacheDir = DEFAULT_FONT_CACHE, substitute
|
|
|
9771
10006
|
}
|
|
9772
10007
|
if (lines.length === 0) return null;
|
|
9773
10008
|
if (notices.length > 0) {
|
|
9774
|
-
const fontsDir =
|
|
9775
|
-
for (const [file, text] of licenseTexts) writeFileSync11(
|
|
9776
|
-
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")}
|
|
9777
10012
|
`);
|
|
9778
10013
|
header.push(
|
|
9779
10014
|
"/* REDISTRIBUTION: the face files in ./fonts/ are redistributed under the licences",
|
|
@@ -9785,10 +10020,10 @@ function fontsCssFor(faces, bundleDir, cacheDir = DEFAULT_FONT_CACHE, substitute
|
|
|
9785
10020
|
`;
|
|
9786
10021
|
}
|
|
9787
10022
|
function countLatticeSymbols(setDir) {
|
|
9788
|
-
const manifestFile =
|
|
9789
|
-
if (
|
|
10023
|
+
const manifestFile = path31.join(setDir, "recording-set.json");
|
|
10024
|
+
if (existsSync25(manifestFile)) {
|
|
9790
10025
|
try {
|
|
9791
|
-
const stored = JSON.parse(
|
|
10026
|
+
const stored = JSON.parse(readFileSync21(manifestFile, "utf8"));
|
|
9792
10027
|
if (stored.variantScope !== "component-set") return null;
|
|
9793
10028
|
const lattice = stored.latticeNames;
|
|
9794
10029
|
if (lattice !== void 0 && lattice.length > 0) return lattice.length;
|
|
@@ -9796,13 +10031,13 @@ function countLatticeSymbols(setDir) {
|
|
|
9796
10031
|
}
|
|
9797
10032
|
}
|
|
9798
10033
|
const files = [
|
|
9799
|
-
|
|
9800
|
-
...
|
|
9801
|
-
].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));
|
|
9802
10037
|
if (files.length === 0) return null;
|
|
9803
10038
|
let count = 0;
|
|
9804
10039
|
for (const f of files) {
|
|
9805
|
-
const text = envelopeTextContent(JSON.parse(
|
|
10040
|
+
const text = envelopeTextContent(JSON.parse(readFileSync21(f, "utf8")));
|
|
9806
10041
|
count += [...text.matchAll(/name="([^"]*)"/g)].filter((m) => m[1].includes("=")).length;
|
|
9807
10042
|
}
|
|
9808
10043
|
return count > 0 ? count : null;
|
|
@@ -9810,21 +10045,21 @@ function countLatticeSymbols(setDir) {
|
|
|
9810
10045
|
function recordingSetHash(setDir, configs) {
|
|
9811
10046
|
const relPaths = [];
|
|
9812
10047
|
for (const name of ["recording-set.json", "completeness-manifest.json", "get_variable_defs.json", "get_metadata.json"]) {
|
|
9813
|
-
if (
|
|
10048
|
+
if (existsSync25(path31.join(setDir, name))) relPaths.push(name);
|
|
9814
10049
|
}
|
|
9815
10050
|
for (const cfg of configs) {
|
|
9816
10051
|
for (const f of ["get_design_context.json", "get_metadata.json", "get_screenshot.json", "get_variable_defs.json"]) {
|
|
9817
|
-
if (
|
|
10052
|
+
if (existsSync25(path31.join(setDir, cfg.rep, f))) relPaths.push(`${cfg.rep}/${f}`);
|
|
9818
10053
|
}
|
|
9819
|
-
if (
|
|
9820
|
-
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-"))) {
|
|
9821
10056
|
relPaths.push(`${cfg.rep}/${asset}`);
|
|
9822
10057
|
}
|
|
9823
10058
|
}
|
|
9824
10059
|
}
|
|
9825
10060
|
return hashRecordingSet(
|
|
9826
10061
|
relPaths,
|
|
9827
|
-
(p) => new Uint8Array(
|
|
10062
|
+
(p) => new Uint8Array(readFileSync21(path31.join(setDir, p))),
|
|
9828
10063
|
(chunks) => {
|
|
9829
10064
|
const h = createHash4("sha256");
|
|
9830
10065
|
for (const c of chunks) h.update(c);
|
|
@@ -9845,10 +10080,11 @@ function emitBundleV1(opts) {
|
|
|
9845
10080
|
const certified = statuses.filter((s) => s.status === "certified").length;
|
|
9846
10081
|
const pass = statuses.filter((s) => s.status !== "fail").length;
|
|
9847
10082
|
const lattice = countLatticeSymbols(opts.task.set);
|
|
9848
|
-
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:"));
|
|
9849
10084
|
const prelude = opts.behaviors.filter((b) => b.id.startsWith("prelude:"));
|
|
9850
|
-
const
|
|
9851
|
-
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"));
|
|
9852
10088
|
const requiredFonts = requiredFontsManifest(families, opts.fontCacheDir).map((f) => ({
|
|
9853
10089
|
family: f.family,
|
|
9854
10090
|
weight: f.weight,
|
|
@@ -9877,7 +10113,7 @@ function emitBundleV1(opts) {
|
|
|
9877
10113
|
// resolvable via verify's --set override).
|
|
9878
10114
|
path: (() => {
|
|
9879
10115
|
const base = process.env["INIT_CWD"] ?? process.cwd();
|
|
9880
|
-
const rel =
|
|
10116
|
+
const rel = path31.relative(base, opts.task.set);
|
|
9881
10117
|
return rel !== "" && !rel.startsWith("..") ? rel : opts.task.set;
|
|
9882
10118
|
})(),
|
|
9883
10119
|
component: opts.componentName,
|
|
@@ -9901,21 +10137,21 @@ function emitBundleV1(opts) {
|
|
|
9901
10137
|
})
|
|
9902
10138
|
};
|
|
9903
10139
|
const written = [];
|
|
9904
|
-
const manifestPath2 =
|
|
10140
|
+
const manifestPath2 = path31.join(opts.bundleDir, "component.json");
|
|
9905
10141
|
writeFileSync11(manifestPath2, `${JSON.stringify(manifest, null, 2)}
|
|
9906
10142
|
`);
|
|
9907
10143
|
written.push(manifestPath2);
|
|
9908
|
-
const stylesPath =
|
|
9909
|
-
if (
|
|
10144
|
+
const stylesPath = path31.join(opts.bundleDir, "styles.css");
|
|
10145
|
+
if (existsSync25(stylesPath)) {
|
|
9910
10146
|
const comment = cssProvenanceComment({ pass, scored: statuses.length, certified, latticeConfigs: lattice });
|
|
9911
|
-
const current =
|
|
10147
|
+
const current = readFileSync21(stylesPath, "utf8");
|
|
9912
10148
|
const stripped = current.replace(/^\/\* tendril bundle v\d+ [^]*?\*\/\n/, "");
|
|
9913
10149
|
writeFileSync11(stylesPath, `${comment}
|
|
9914
10150
|
${stripped}`);
|
|
9915
10151
|
written.push(stylesPath);
|
|
9916
10152
|
}
|
|
9917
|
-
const fontsCssPath =
|
|
9918
|
-
rmSync3(
|
|
10153
|
+
const fontsCssPath = path31.join(opts.bundleDir, "fonts.css");
|
|
10154
|
+
rmSync3(path31.join(opts.bundleDir, "fonts"), { recursive: true, force: true });
|
|
9919
10155
|
rmSync3(fontsCssPath, { force: true });
|
|
9920
10156
|
const fontsCss = fontsCssFor(requiredFontsManifest(families, opts.fontCacheDir), opts.bundleDir, opts.fontCacheDir, opts.substitutedFamilies ?? []);
|
|
9921
10157
|
if (fontsCss !== null) {
|
|
@@ -9923,7 +10159,7 @@ ${stripped}`);
|
|
|
9923
10159
|
written.push(fontsCssPath);
|
|
9924
10160
|
}
|
|
9925
10161
|
const unrecorded = lattice === null ? null : Math.max(0, lattice - statuses.length);
|
|
9926
|
-
const statusLine = `bundle: ${pass}/${statuses.length} recorded configs \u2265 pass bar (${certified} certified), ` + (interaction.length === 0 ? "interaction behaviors NONE VERIFIED" : `interaction behaviors ${interaction.filter((b) => b.pass).length}/${interaction.length}`) + (prelude.length === 0 ? "" : `, page hygiene ${prelude.filter((b) => b.pass).length}/${prelude.length}`) + (unrecorded === null ? "" : `; ${unrecorded} lattice configs UNVERIFIED`) + ` \u2014 claims in component.json, recompute with \`tendril verify\``;
|
|
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\``;
|
|
9927
10163
|
return { manifest, statusLine, written };
|
|
9928
10164
|
}
|
|
9929
10165
|
var FONT_FORMATS, BARS, NOTICE_PREAMBLE, OFL_1_1_TEXT, UFL_1_0_TEXT, APACHE_2_0_TEXT, REDISTRIBUTABLE;
|
|
@@ -10360,9 +10596,9 @@ var init_src7 = __esm({
|
|
|
10360
10596
|
});
|
|
10361
10597
|
|
|
10362
10598
|
// packages/cli/src/font-guidance.ts
|
|
10363
|
-
import
|
|
10599
|
+
import path32 from "node:path";
|
|
10364
10600
|
function fontsUnprovenRemediation(setDir) {
|
|
10365
|
-
const set = setDir === void 0 ? void 0 :
|
|
10601
|
+
const set = setDir === void 0 ? void 0 : path32.resolve(setDir);
|
|
10366
10602
|
if (set !== void 0) {
|
|
10367
10603
|
try {
|
|
10368
10604
|
const needs = recordedFontNeeds(set);
|
|
@@ -10430,13 +10666,15 @@ __export(fonts_exports, {
|
|
|
10430
10666
|
DEFAULT_FONT_CACHE: () => DEFAULT_FONT_CACHE,
|
|
10431
10667
|
familyMismatch: () => familyMismatch,
|
|
10432
10668
|
runFontsAdd: () => runFontsAdd,
|
|
10669
|
+
runFontsAddSystem: () => runFontsAddSystem,
|
|
10670
|
+
runFontsDiscover: () => runFontsDiscover,
|
|
10433
10671
|
runFontsRequired: () => runFontsRequired,
|
|
10434
10672
|
runFontsResolve: () => runFontsResolve,
|
|
10435
10673
|
runFontsResolveSet: () => runFontsResolveSet,
|
|
10436
10674
|
runFontsStatus: () => runFontsStatus
|
|
10437
10675
|
});
|
|
10438
|
-
import { existsSync as
|
|
10439
|
-
import
|
|
10676
|
+
import { existsSync as existsSync26, readFileSync as readFileSync22 } from "node:fs";
|
|
10677
|
+
import path33 from "node:path";
|
|
10440
10678
|
async function runFontsResolve(opts) {
|
|
10441
10679
|
const result = await resolveFonts(opts.family, opts.weights, opts.cacheDir);
|
|
10442
10680
|
emitData(opts, result, () => {
|
|
@@ -10451,7 +10689,7 @@ async function runFontsResolve(opts) {
|
|
|
10451
10689
|
}
|
|
10452
10690
|
}
|
|
10453
10691
|
async function runFontsResolveSet(opts) {
|
|
10454
|
-
const setDir =
|
|
10692
|
+
const setDir = path33.resolve(process.env["INIT_CWD"] ?? process.cwd(), opts.set);
|
|
10455
10693
|
let needs = [];
|
|
10456
10694
|
try {
|
|
10457
10695
|
needs = recordedFontNeeds(setDir);
|
|
@@ -10514,6 +10752,12 @@ async function runFontsResolveSet(opts) {
|
|
|
10514
10752
|
`);
|
|
10515
10753
|
for (const f of failures) process.stdout.write(`FAILED ${f.family} ${f.weight}: ${f.reason}
|
|
10516
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
|
+
}
|
|
10517
10761
|
});
|
|
10518
10762
|
if (byteDrift.length > 0) {
|
|
10519
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`);
|
|
@@ -10524,16 +10768,16 @@ async function runFontsResolveSet(opts) {
|
|
|
10524
10768
|
}
|
|
10525
10769
|
}
|
|
10526
10770
|
function runFontsStatus(opts) {
|
|
10527
|
-
const manifestPath2 =
|
|
10528
|
-
if (!
|
|
10771
|
+
const manifestPath2 = path33.join(opts.cacheDir, "manifest.json");
|
|
10772
|
+
if (!existsSync26(manifestPath2)) {
|
|
10529
10773
|
fail(opts, ExitCode.FontsUnproven, {
|
|
10530
10774
|
error: `no font cache at ${opts.cacheDir}`,
|
|
10531
10775
|
code: "fonts-unresolved",
|
|
10532
10776
|
remediation: `Run \`${tendrilCommand("fonts resolve --set <recording-dir>")}\` (or \`${tendrilCommand('fonts resolve "<Family>" --weights 400 500 600')}\`) first.`
|
|
10533
10777
|
});
|
|
10534
10778
|
}
|
|
10535
|
-
const faces = JSON.parse(
|
|
10536
|
-
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;
|
|
10537
10781
|
emitData(opts, { faces, lockVerdicts }, () => {
|
|
10538
10782
|
for (const f of faces) process.stdout.write(`cached ${f.family} ${f.weight} (${f.sha256.slice(0, 12)}\u2026)
|
|
10539
10783
|
`);
|
|
@@ -10577,13 +10821,13 @@ function familyMismatch(family, declared) {
|
|
|
10577
10821
|
}
|
|
10578
10822
|
function runFontsAdd(opts) {
|
|
10579
10823
|
if (opts.set !== void 0) {
|
|
10580
|
-
const declared = taskFontFamilies(
|
|
10824
|
+
const declared = taskFontFamilies(path33.resolve(opts.set)) ?? [];
|
|
10581
10825
|
const mismatch = familyMismatch(opts.family, declared);
|
|
10582
10826
|
if (mismatch !== void 0) {
|
|
10583
10827
|
fail(opts, ExitCode.InputValidation, {
|
|
10584
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.`,
|
|
10585
10829
|
code: "font-family-not-declared",
|
|
10586
|
-
remediation: mismatch.nearest !== void 0 ? `Did you mean "${mismatch.nearest}"? ${tendrilCommand(`fonts add "${mismatch.nearest}" ${opts.weight} ${opts.file} --set ${
|
|
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.`
|
|
10587
10831
|
});
|
|
10588
10832
|
}
|
|
10589
10833
|
} else {
|
|
@@ -10610,6 +10854,73 @@ function runFontsAdd(opts) {
|
|
|
10610
10854
|
`);
|
|
10611
10855
|
});
|
|
10612
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
|
+
}
|
|
10613
10924
|
var init_fonts = __esm({
|
|
10614
10925
|
"packages/cli/src/commands/fonts.ts"() {
|
|
10615
10926
|
"use strict";
|
|
@@ -10640,15 +10951,24 @@ __export(verify_exports, {
|
|
|
10640
10951
|
resolveComposition: () => resolveComposition,
|
|
10641
10952
|
runVerify: () => runVerify
|
|
10642
10953
|
});
|
|
10643
|
-
import { existsSync as
|
|
10644
|
-
import
|
|
10954
|
+
import { existsSync as existsSync27, readFileSync as readFileSync23 } from "node:fs";
|
|
10955
|
+
import path34 from "node:path";
|
|
10645
10956
|
function interactionCoverage(behaviors) {
|
|
10646
|
-
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:"));
|
|
10647
10959
|
return {
|
|
10648
10960
|
interactionChecks: interaction.length,
|
|
10649
10961
|
interactionPassed: interaction.filter((b) => b.pass).length,
|
|
10650
|
-
preludeChecks: behaviors.length - interaction.length,
|
|
10651
|
-
|
|
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"
|
|
10652
10972
|
};
|
|
10653
10973
|
}
|
|
10654
10974
|
function operabilityReport(input) {
|
|
@@ -10681,6 +11001,16 @@ function operabilityReport(input) {
|
|
|
10681
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.`
|
|
10682
11002
|
};
|
|
10683
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
|
+
}
|
|
10684
11014
|
return { checks: interactionChecks, passed: interactionPassed };
|
|
10685
11015
|
}
|
|
10686
11016
|
function operabilityLine(state) {
|
|
@@ -10773,7 +11103,7 @@ function compositionReport(input) {
|
|
|
10773
11103
|
function eyeCheck(bundleDir) {
|
|
10774
11104
|
return {
|
|
10775
11105
|
command: tendrilCommand(`inspect "${bundleDir}"`),
|
|
10776
|
-
sheetPath:
|
|
11106
|
+
sheetPath: path34.join(bundleDir, "verify-evidence", "inspect.html"),
|
|
10777
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."
|
|
10778
11108
|
};
|
|
10779
11109
|
}
|
|
@@ -10785,7 +11115,7 @@ function taskFromManifest(opts, manifest, setDir) {
|
|
|
10785
11115
|
const adapterSlugs = Object.keys(manifest.propAdapter);
|
|
10786
11116
|
const unmapped = recordedSlugs.filter((s) => !adapterSlugs.includes(s));
|
|
10787
11117
|
const adapterOnly = adapterSlugs.filter((s) => !recordedSlugs.includes(s));
|
|
10788
|
-
const registry = Object.values(TASKS).find((t) =>
|
|
11118
|
+
const registry = Object.values(TASKS).find((t) => path34.resolve(t.set) === path34.resolve(setDir));
|
|
10789
11119
|
const authored = (() => {
|
|
10790
11120
|
if (registry !== void 0) return void 0;
|
|
10791
11121
|
try {
|
|
@@ -10811,6 +11141,10 @@ function taskFromManifest(opts, manifest, setDir) {
|
|
|
10811
11141
|
task,
|
|
10812
11142
|
unmapped,
|
|
10813
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,
|
|
10814
11148
|
// Registry sets carry hand-declared behaviors and no derivation
|
|
10815
11149
|
// runs, so their evidence is UNKNOWN, not empty: an empty list is
|
|
10816
11150
|
// spent downstream as "the recording holds no interactive pose".
|
|
@@ -10820,19 +11154,19 @@ function taskFromManifest(opts, manifest, setDir) {
|
|
|
10820
11154
|
}
|
|
10821
11155
|
async function runVerify(opts) {
|
|
10822
11156
|
const callerCwd = process.env["INIT_CWD"] ?? process.cwd();
|
|
10823
|
-
const setOverride = opts.set !== void 0 ?
|
|
10824
|
-
opts = { ...opts, bundleDir:
|
|
10825
|
-
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)) {
|
|
10826
11160
|
fail(opts, ExitCode.InputValidation, {
|
|
10827
11161
|
error: `bundle directory not found: ${opts.bundleDir}`,
|
|
10828
11162
|
code: "bundle-missing",
|
|
10829
11163
|
remediation: "Point at a generated bundle directory (containing component.json, the entry module, and styles.css)."
|
|
10830
11164
|
});
|
|
10831
11165
|
}
|
|
10832
|
-
const manifestPath2 =
|
|
11166
|
+
const manifestPath2 = path34.join(opts.bundleDir, "component.json");
|
|
10833
11167
|
let manifest;
|
|
10834
|
-
if (
|
|
10835
|
-
const { manifest: parsed, issues } = readBundleManifest(
|
|
11168
|
+
if (existsSync27(manifestPath2)) {
|
|
11169
|
+
const { manifest: parsed, issues } = readBundleManifest(readFileSync23(manifestPath2, "utf8"));
|
|
10836
11170
|
if (issues.length > 0) {
|
|
10837
11171
|
fail(opts, ExitCode.InputValidation, {
|
|
10838
11172
|
error: `bundle manifest rejected at ingest: ${issues.map((i) => i.message).join("; ")}`,
|
|
@@ -10846,6 +11180,7 @@ async function runVerify(opts) {
|
|
|
10846
11180
|
let unmapped = [];
|
|
10847
11181
|
let interactionEvidence;
|
|
10848
11182
|
let unmappedInteractionEvidence = [];
|
|
11183
|
+
let authorityConfigs;
|
|
10849
11184
|
let availability = ROLES_NOT_RESOLVED;
|
|
10850
11185
|
if (opts.task !== void 0) {
|
|
10851
11186
|
const registry = TASKS[opts.task];
|
|
@@ -10859,21 +11194,21 @@ async function runVerify(opts) {
|
|
|
10859
11194
|
task = registry;
|
|
10860
11195
|
} else if (manifest !== void 0) {
|
|
10861
11196
|
const resolveSetDir = (p) => {
|
|
10862
|
-
if (
|
|
10863
|
-
const fromRepo =
|
|
10864
|
-
if (
|
|
10865
|
-
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);
|
|
10866
11201
|
};
|
|
10867
11202
|
const setDir = opts.set ?? resolveSetDir(manifest.provenance.recordingSet.path);
|
|
10868
|
-
if (!
|
|
11203
|
+
if (!existsSync27(path34.join(setDir, "recording-set.json")) && !Object.values(TASKS).some((t) => path34.resolve(t.set) === path34.resolve(setDir))) {
|
|
10869
11204
|
fail(opts, ExitCode.RecordingIncomplete, {
|
|
10870
11205
|
error: `recording set not found or unmanifested: ${setDir}`,
|
|
10871
11206
|
code: "recording-set-missing",
|
|
10872
11207
|
remediation: "Pass --set <dir> pointing at the recording set this bundle was generated from."
|
|
10873
11208
|
});
|
|
10874
11209
|
}
|
|
10875
|
-
const registry = Object.values(TASKS).find((t) =>
|
|
10876
|
-
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"))) {
|
|
10877
11212
|
const recordedSlugs = registry.configs.map((c) => c.rep);
|
|
10878
11213
|
const adapterSlugs = Object.keys(manifest.propAdapter);
|
|
10879
11214
|
unmapped = recordedSlugs.filter((s) => !adapterSlugs.includes(s));
|
|
@@ -10882,10 +11217,12 @@ async function runVerify(opts) {
|
|
|
10882
11217
|
entry: manifest.entry,
|
|
10883
11218
|
configs: adapterSlugs.filter((s) => recordedSlugs.includes(s)).map((rep) => ({ rep, component: manifest.propAdapter[rep].component, props: manifest.propAdapter[rep].props }))
|
|
10884
11219
|
};
|
|
11220
|
+
authorityConfigs = registry.configs;
|
|
10885
11221
|
} else {
|
|
10886
11222
|
const built = taskFromManifest(opts, manifest, setDir);
|
|
10887
11223
|
task = built.task;
|
|
10888
11224
|
unmapped = built.unmapped;
|
|
11225
|
+
authorityConfigs = built.authorityConfigs;
|
|
10889
11226
|
interactionEvidence = built.interactionEvidence;
|
|
10890
11227
|
unmappedInteractionEvidence = built.unmappedInteractionEvidence;
|
|
10891
11228
|
for (const s of built.adapterOnly) warn(opts, `prop adapter maps "${s}" which is not in the recording set \u2014 ignored`);
|
|
@@ -10897,9 +11234,9 @@ async function runVerify(opts) {
|
|
|
10897
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`);
|
|
10898
11235
|
}
|
|
10899
11236
|
for (const name of [manifest.entry, "styles.css", "tokens.css"]) {
|
|
10900
|
-
const p =
|
|
10901
|
-
if (!
|
|
10902
|
-
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)));
|
|
10903
11240
|
if (issues.length > 0) {
|
|
10904
11241
|
fail(opts, ExitCode.InputValidation, {
|
|
10905
11242
|
error: `bundle source rejected at ingest: ${issues.map((i) => i.message).join("; ")}`,
|
|
@@ -10937,7 +11274,7 @@ async function runVerify(opts) {
|
|
|
10937
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)`);
|
|
10938
11275
|
}
|
|
10939
11276
|
const missing = task.configs.filter(
|
|
10940
|
-
(c) => !
|
|
11277
|
+
(c) => !existsSync27(path34.join(task.set, c.rep, "get_screenshot.json")) || !existsSync27(path34.join(task.set, c.rep, "get_metadata.json"))
|
|
10941
11278
|
);
|
|
10942
11279
|
if (missing.length > 0) {
|
|
10943
11280
|
fail(opts, ExitCode.RecordingIncomplete, {
|
|
@@ -10947,12 +11284,12 @@ async function runVerify(opts) {
|
|
|
10947
11284
|
});
|
|
10948
11285
|
}
|
|
10949
11286
|
const bar = BARS2[opts.bar];
|
|
10950
|
-
const evidenceDir =
|
|
11287
|
+
const evidenceDir = path34.join(opts.bundleDir, "verify-evidence");
|
|
10951
11288
|
const scores = await scoreBundleForTask(task, opts.bundleDir, bar, { evidenceDir, onProgress: emitProgress });
|
|
10952
11289
|
const quality = await checkBundleQuality(opts.bundleDir, task.entry, { dir: task.set, reps: task.configs.map((c) => c.rep) });
|
|
10953
|
-
const bundleCss = ["tokens.css", "styles.css"].map((f) =>
|
|
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");
|
|
10954
11291
|
const occlusion = await checkSiblingOcclusion(task, opts.bundleDir, bundleCss);
|
|
10955
|
-
const parity = await checkHoverParity(task, opts.bundleDir);
|
|
11292
|
+
const parity = await checkHoverParity(task, opts.bundleDir, authorityConfigs ?? task.configs);
|
|
10956
11293
|
const behaviors = [...await checkBehaviors(task, opts.bundleDir), ...parity];
|
|
10957
11294
|
const structural = roles !== void 0 ? await checkStructuralComposition(task, opts.bundleDir, roles) : [];
|
|
10958
11295
|
const regionsOut = roles !== void 0 ? interiorRegions(task.set, roles) : void 0;
|
|
@@ -11142,7 +11479,7 @@ ${certified}/${statuses.length} certified \xB7 ${report.coverage.pass}/${statuse
|
|
|
11142
11479
|
}
|
|
11143
11480
|
process.stdout.write(`environment: chrome=${report.environment.chromeVersion ?? report.environment.chrome} fonts=${report.environment.fontsManifestSha256 ?? "UNRESOLVED"}
|
|
11144
11481
|
`);
|
|
11145
|
-
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")));
|
|
11146
11483
|
if (shippedFonts.length > 0 && substitutedFamilies.length === 0) {
|
|
11147
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)
|
|
11148
11485
|
`);
|
|
@@ -11232,18 +11569,18 @@ __export(engine_exports, {
|
|
|
11232
11569
|
runEngineBrief: () => runEngineBrief,
|
|
11233
11570
|
runEngineScore: () => runEngineScore
|
|
11234
11571
|
});
|
|
11235
|
-
import { appendFileSync, existsSync as
|
|
11236
|
-
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";
|
|
11237
11574
|
function resolveEngineTask(opts, callerCwd) {
|
|
11238
|
-
const asPath =
|
|
11239
|
-
const isSet =
|
|
11575
|
+
const asPath = path35.resolve(callerCwd, opts.taskOrSet);
|
|
11576
|
+
const isSet = existsSync28(path35.join(asPath, "recording-set.json"));
|
|
11240
11577
|
const registry = TASKS[opts.taskOrSet];
|
|
11241
11578
|
if (registry !== void 0 && !isSet) return { task: registry, name: opts.taskOrSet, ref: opts.taskOrSet, disclosures: [], interactionEvidence: void 0 };
|
|
11242
11579
|
if (isSet) {
|
|
11243
11580
|
try {
|
|
11244
11581
|
const authored = authorTaskFromSet(asPath);
|
|
11245
11582
|
for (const d of authored.disclosures) warn(opts, d);
|
|
11246
|
-
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 } };
|
|
11247
11584
|
} catch (err) {
|
|
11248
11585
|
fail(opts, ExitCode.InputValidation, {
|
|
11249
11586
|
error: `cannot author a task from ${asPath}: ${err instanceof Error ? err.message.split("\n")[0] : String(err)}`,
|
|
@@ -11270,9 +11607,9 @@ ${disclosures.map((d) => `- ${d}`).join("\n")}` : "";
|
|
|
11270
11607
|
const brief = buildBrief(task.systemApi, bar, { colorScheme: recordingIsDark(task) ? "dark" : "light" }) + disclosureBlock;
|
|
11271
11608
|
const segments = buildSegments(task, "files");
|
|
11272
11609
|
let notRecorded;
|
|
11273
|
-
const manifestPath2 =
|
|
11274
|
-
if (
|
|
11275
|
-
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;
|
|
11276
11613
|
}
|
|
11277
11614
|
const unverified = notRecorded !== void 0 && notRecorded !== "" ? `
|
|
11278
11615
|
|
|
@@ -11280,7 +11617,7 @@ ${disclosures.map((d) => `- ${d}`).join("\n")}` : "";
|
|
|
11280
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.
|
|
11281
11618
|
${notRecorded}` : "";
|
|
11282
11619
|
let fontProvisioning;
|
|
11283
|
-
if (
|
|
11620
|
+
if (existsSync28(manifestPath2)) {
|
|
11284
11621
|
const missingFams = unprovisionedFamilies(task.set);
|
|
11285
11622
|
const unprovided = unprovisionedFaces(task.set);
|
|
11286
11623
|
const weightOnly = missingFams.length === 0;
|
|
@@ -11306,9 +11643,9 @@ ${notRecorded}` : "";
|
|
|
11306
11643
|
|
|
11307
11644
|
=== TASK PAYLOAD (recorded truth, verbatim) ===
|
|
11308
11645
|
${segments}`;
|
|
11309
|
-
const payloadFile =
|
|
11310
|
-
const candidateDirSuggestion =
|
|
11311
|
-
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 });
|
|
11312
11649
|
writeFileSync12(payloadFile, payload);
|
|
11313
11650
|
emitData(
|
|
11314
11651
|
opts,
|
|
@@ -11359,15 +11696,15 @@ ${segments}`;
|
|
|
11359
11696
|
);
|
|
11360
11697
|
}
|
|
11361
11698
|
function appendScoreHistory(candidateDir, entry) {
|
|
11362
|
-
appendFileSync(
|
|
11699
|
+
appendFileSync(path35.join(candidateDir, "score-history.jsonl"), `${JSON.stringify({ at: (/* @__PURE__ */ new Date()).toISOString(), ...entry })}
|
|
11363
11700
|
`);
|
|
11364
11701
|
}
|
|
11365
11702
|
async function runEngineScore(opts) {
|
|
11366
11703
|
requireEntitlement(opts);
|
|
11367
11704
|
const callerCwd = process.env["INIT_CWD"] ?? process.cwd();
|
|
11368
|
-
const candidateDir =
|
|
11705
|
+
const candidateDir = path35.resolve(callerCwd, opts.candidateDir);
|
|
11369
11706
|
const { task, name, apiPin, interactionEvidence } = resolveEngineTask(opts, callerCwd);
|
|
11370
|
-
if (!
|
|
11707
|
+
if (!existsSync28(candidateDir)) {
|
|
11371
11708
|
fail(opts, ExitCode.InputValidation, {
|
|
11372
11709
|
error: `candidate directory not found: ${candidateDir}`,
|
|
11373
11710
|
code: "candidate-missing",
|
|
@@ -11392,10 +11729,10 @@ async function runEngineScore(opts) {
|
|
|
11392
11729
|
for (const g of missingWeights(task.set)) {
|
|
11393
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)`);
|
|
11394
11731
|
}
|
|
11395
|
-
if (opts.rebind !== true &&
|
|
11732
|
+
if (opts.rebind !== true && existsSync28(path35.join(candidateDir, "component.json"))) {
|
|
11396
11733
|
const prior = (() => {
|
|
11397
11734
|
try {
|
|
11398
|
-
const read = readBundleManifest(
|
|
11735
|
+
const read = readBundleManifest(readFileSync24(path35.join(candidateDir, "component.json"), "utf8"));
|
|
11399
11736
|
return read.manifest === void 0 ? { unreadable: true } : { hash: read.manifest.provenance.recordingSet.hash, path: read.manifest.provenance.recordingSet.path };
|
|
11400
11737
|
} catch {
|
|
11401
11738
|
return { unreadable: true };
|
|
@@ -11417,9 +11754,9 @@ async function runEngineScore(opts) {
|
|
|
11417
11754
|
}
|
|
11418
11755
|
}
|
|
11419
11756
|
const bar = BARS3[opts.bar];
|
|
11420
|
-
const evidenceDir =
|
|
11757
|
+
const evidenceDir = path35.join(candidateDir, "verify-evidence");
|
|
11421
11758
|
const scores = await scoreBundleForTask(task, candidateDir, bar, { evidenceDir, onProgress: emitProgress });
|
|
11422
|
-
const parity = await checkHoverParity(task, candidateDir);
|
|
11759
|
+
const parity = await checkHoverParity(task, candidateDir, task.configs);
|
|
11423
11760
|
const behaviors = [...await checkBehaviors(task, candidateDir), ...parity];
|
|
11424
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)";
|
|
11425
11762
|
const obj = objective(scores, behaviors);
|
|
@@ -11460,7 +11797,17 @@ METRIC DEADBAND (read before iterating on near-misses): the scored similarity/in
|
|
|
11460
11797
|
environment: environmentStamp(taskFontFamilies(task.set)),
|
|
11461
11798
|
substitutedFamilies
|
|
11462
11799
|
});
|
|
11463
|
-
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
|
+
});
|
|
11464
11811
|
const coverage = interactionCoverage(behaviors);
|
|
11465
11812
|
const operability = operabilityReport({ behaviors, interactionEvidence });
|
|
11466
11813
|
const evidenceUnverified = (interactionEvidence?.length ?? 0) > 0 && coverage.interactionChecks === 0;
|
|
@@ -11575,11 +11922,11 @@ var codeconnect_exports = {};
|
|
|
11575
11922
|
__export(codeconnect_exports, {
|
|
11576
11923
|
runCodeConnect: () => runCodeConnect
|
|
11577
11924
|
});
|
|
11578
|
-
import { existsSync as
|
|
11579
|
-
import
|
|
11925
|
+
import { existsSync as existsSync29, readFileSync as readFileSync25, writeFileSync as writeFileSync13 } from "node:fs";
|
|
11926
|
+
import path36 from "node:path";
|
|
11580
11927
|
function runCodeConnect(opts) {
|
|
11581
11928
|
const callerCwd = process.env["INIT_CWD"] ?? process.cwd();
|
|
11582
|
-
const bundleDir =
|
|
11929
|
+
const bundleDir = path36.resolve(callerCwd, opts.bundleDir);
|
|
11583
11930
|
let url;
|
|
11584
11931
|
try {
|
|
11585
11932
|
url = new URL(opts.figmaUrl);
|
|
@@ -11595,7 +11942,7 @@ function runCodeConnect(opts) {
|
|
|
11595
11942
|
}
|
|
11596
11943
|
let manifest;
|
|
11597
11944
|
try {
|
|
11598
|
-
const read = readBundleManifest(
|
|
11945
|
+
const read = readBundleManifest(readFileSync25(path36.join(bundleDir, "component.json"), "utf8"));
|
|
11599
11946
|
if (read.manifest === void 0) throw new Error(read.issues.map((i) => i.message).join("; ") || "no component.json");
|
|
11600
11947
|
manifest = read.manifest;
|
|
11601
11948
|
} catch (err) {
|
|
@@ -11605,8 +11952,8 @@ function runCodeConnect(opts) {
|
|
|
11605
11952
|
remediation: "Point at a bundle produced by the Tendril pipeline (it carries component.json), and verify it first."
|
|
11606
11953
|
});
|
|
11607
11954
|
}
|
|
11608
|
-
const setDir =
|
|
11609
|
-
if (!
|
|
11955
|
+
const setDir = path36.resolve(callerCwd, opts.set ?? manifest.provenance.recordingSet.path);
|
|
11956
|
+
if (!existsSync29(path36.join(setDir, "recording-set.json"))) {
|
|
11610
11957
|
fail(opts, ExitCode.InputValidation, {
|
|
11611
11958
|
error: `recording set not found at ${setDir}`,
|
|
11612
11959
|
code: "codeconnect-no-set",
|
|
@@ -11627,10 +11974,10 @@ function runCodeConnect(opts) {
|
|
|
11627
11974
|
const component = api.component;
|
|
11628
11975
|
const recManifest = loadManifest(setDir);
|
|
11629
11976
|
const poseNames = recManifest.latticeNames ?? recManifest.reps.map((r) => {
|
|
11630
|
-
const meta =
|
|
11631
|
-
if (!
|
|
11977
|
+
const meta = path36.join(setDir, r.slug, "get_metadata.json");
|
|
11978
|
+
if (!existsSync29(meta)) return void 0;
|
|
11632
11979
|
try {
|
|
11633
|
-
return /name="([^"]*)"/.exec(envelopeFirstTextPart(JSON.parse(
|
|
11980
|
+
return /name="([^"]*)"/.exec(envelopeFirstTextPart(JSON.parse(readFileSync25(meta, "utf8"))))?.[1];
|
|
11634
11981
|
} catch {
|
|
11635
11982
|
return void 0;
|
|
11636
11983
|
}
|
|
@@ -11688,7 +12035,7 @@ function runCodeConnect(opts) {
|
|
|
11688
12035
|
axisLines.push(`const ${varName} = instance.getEnum(${q(axis)}, { ${entries.join(", ")} })`);
|
|
11689
12036
|
fragmentVars.push(varName);
|
|
11690
12037
|
}
|
|
11691
|
-
const entryRel =
|
|
12038
|
+
const entryRel = path36.relative(callerCwd, path36.join(bundleDir, manifest.entry));
|
|
11692
12039
|
const trust = manifest.trustStatement.split("\n")[0] ?? "";
|
|
11693
12040
|
const lines = [
|
|
11694
12041
|
`// url=${opts.figmaUrl}`,
|
|
@@ -11709,7 +12056,7 @@ function runCodeConnect(opts) {
|
|
|
11709
12056
|
`}`,
|
|
11710
12057
|
``
|
|
11711
12058
|
].join("\n");
|
|
11712
|
-
const outFile =
|
|
12059
|
+
const outFile = path36.resolve(callerCwd, opts.out ?? path36.join(bundleDir, `${component}.figma.ts`));
|
|
11713
12060
|
writeFileSync13(outFile, lines);
|
|
11714
12061
|
emitData(
|
|
11715
12062
|
opts,
|
|
@@ -11749,17 +12096,17 @@ var init_codeconnect = __esm({
|
|
|
11749
12096
|
|
|
11750
12097
|
// packages/mcp/src/server.ts
|
|
11751
12098
|
import { createHash as createHash5 } from "node:crypto";
|
|
11752
|
-
import { existsSync as
|
|
11753
|
-
import
|
|
11754
|
-
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";
|
|
11755
12102
|
import { fileURLToPath as fileURLToPath6 } from "node:url";
|
|
11756
12103
|
import { z as z12 } from "zod";
|
|
11757
12104
|
function sourceHash() {
|
|
11758
|
-
const dir =
|
|
12105
|
+
const dir = path37.dirname(fileURLToPath6(import.meta.url));
|
|
11759
12106
|
const h = createHash5("sha256");
|
|
11760
|
-
for (const f of
|
|
12107
|
+
for (const f of readdirSync9(dir).filter((n) => n.endsWith(".ts")).sort()) {
|
|
11761
12108
|
h.update(f);
|
|
11762
|
-
h.update(
|
|
12109
|
+
h.update(readFileSync26(path37.join(dir, f)));
|
|
11763
12110
|
}
|
|
11764
12111
|
return h.digest("hex").slice(0, 16);
|
|
11765
12112
|
}
|
|
@@ -11767,10 +12114,10 @@ var REPO_ROOT3, CLI_BIN, BUNDLED_CLI, CLI_SPAWN, str, optStr, TOOLS, BOOT_HASH;
|
|
|
11767
12114
|
var init_server = __esm({
|
|
11768
12115
|
"packages/mcp/src/server.ts"() {
|
|
11769
12116
|
"use strict";
|
|
11770
|
-
REPO_ROOT3 =
|
|
11771
|
-
CLI_BIN =
|
|
11772
|
-
BUNDLED_CLI =
|
|
11773
|
-
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] };
|
|
11774
12121
|
str = (d) => z12.string().describe(d);
|
|
11775
12122
|
optStr = (d) => z12.string().optional().describe(d);
|
|
11776
12123
|
TOOLS = [
|
|
@@ -11800,7 +12147,7 @@ var init_server = __esm({
|
|
|
11800
12147
|
const single = i["metadata"];
|
|
11801
12148
|
const parts = i["metadataParts"];
|
|
11802
12149
|
if (single !== void 0 || parts !== void 0) {
|
|
11803
|
-
const tmp =
|
|
12150
|
+
const tmp = path37.join(mkdtempSync3(path37.join(os7.tmpdir(), "tendril-envelope-")), "response.txt");
|
|
11804
12151
|
if (single !== void 0) {
|
|
11805
12152
|
writeFileSync14(tmp, single);
|
|
11806
12153
|
argvOut.push("--metadata-raw-file", tmp);
|
|
@@ -11817,7 +12164,7 @@ var init_server = __esm({
|
|
|
11817
12164
|
},
|
|
11818
12165
|
{
|
|
11819
12166
|
name: "tendril_permissions",
|
|
11820
|
-
description: "Install the Claude Code permission allowlist for the Tendril pipeline (write: true \u2014 merges per-tool entries into the project's .claude/settings.local.json, idempotent, never touches other
|
|
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.",
|
|
11821
12168
|
schema: z12.object({
|
|
11822
12169
|
write: z12.boolean().optional().describe("true = install into .claude/settings.local.json (the point of this tool); false/absent = just return the entries")
|
|
11823
12170
|
}),
|
|
@@ -11871,7 +12218,7 @@ var init_server = __esm({
|
|
|
11871
12218
|
const bridge = (label, single, parts) => {
|
|
11872
12219
|
if (single !== void 0 && parts !== void 0) throw new Error(`pass at most one of \`${label}\` and \`${label}Parts\``);
|
|
11873
12220
|
if (single === void 0 && parts === void 0) return;
|
|
11874
|
-
const tmp =
|
|
12221
|
+
const tmp = path37.join(mkdtempSync3(path37.join(os7.tmpdir(), "tendril-envelope-")), "response.txt");
|
|
11875
12222
|
if (single !== void 0) {
|
|
11876
12223
|
writeFileSync14(tmp, single);
|
|
11877
12224
|
argvOut.push(`--${label}-file`, tmp);
|
|
@@ -11914,7 +12261,7 @@ var init_server = __esm({
|
|
|
11914
12261
|
const file = i["file"];
|
|
11915
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)");
|
|
11916
12263
|
if (file !== void 0) return [...base, "--file", file];
|
|
11917
|
-
const tmp =
|
|
12264
|
+
const tmp = path37.join(mkdtempSync3(path37.join(os7.tmpdir(), "tendril-envelope-")), "response.txt");
|
|
11918
12265
|
if (text !== void 0) {
|
|
11919
12266
|
writeFileSync14(tmp, text);
|
|
11920
12267
|
return [...base, "--file", tmp, "--raw"];
|
|
@@ -11977,7 +12324,7 @@ var init_server = __esm({
|
|
|
11977
12324
|
},
|
|
11978
12325
|
{
|
|
11979
12326
|
name: "tendril_engine_score",
|
|
11980
|
-
description: "AGENT-HARNESS engine, step 2 (the oracle): scores a candidate bundle directory against recorded truth \u2014 per-config pixels, behaviors,
|
|
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.",
|
|
11981
12328
|
schema: z12.object({
|
|
11982
12329
|
taskOrSet: str("reference task name or recording-set directory"),
|
|
11983
12330
|
candidateDir: str("directory containing the proposed bundle files"),
|
|
@@ -12067,15 +12414,16 @@ __export(permissions_exports, {
|
|
|
12067
12414
|
PERMISSIONS_DESCRIPTION: () => PERMISSIONS_DESCRIPTION,
|
|
12068
12415
|
buildPermissions: () => buildPermissions,
|
|
12069
12416
|
mergeAllowlist: () => mergeAllowlist,
|
|
12070
|
-
runPermissions: () => runPermissions
|
|
12417
|
+
runPermissions: () => runPermissions,
|
|
12418
|
+
writeSelection: () => writeSelection
|
|
12071
12419
|
});
|
|
12072
|
-
import { existsSync as
|
|
12073
|
-
import
|
|
12074
|
-
import
|
|
12075
|
-
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 = []) {
|
|
12076
12424
|
let settings = {};
|
|
12077
|
-
if (
|
|
12078
|
-
settings = JSON.parse(
|
|
12425
|
+
if (existsSync31(file) && readFileSync27(file, "utf8").trim() !== "") {
|
|
12426
|
+
settings = JSON.parse(readFileSync27(file, "utf8"));
|
|
12079
12427
|
if (settings === null || typeof settings !== "object" || Array.isArray(settings)) throw new Error("settings root is not an object");
|
|
12080
12428
|
}
|
|
12081
12429
|
const permissions = settings["permissions"] ??= {};
|
|
@@ -12085,13 +12433,21 @@ function mergeAllowlist(file, entries) {
|
|
|
12085
12433
|
const present = new Set(allow.filter((x) => typeof x === "string"));
|
|
12086
12434
|
const added = entries.filter((e) => !present.has(e));
|
|
12087
12435
|
const alreadyPresent = entries.filter((e) => present.has(e));
|
|
12088
|
-
|
|
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) {
|
|
12089
12445
|
allow.push(...added);
|
|
12090
|
-
mkdirSync9(
|
|
12446
|
+
mkdirSync9(path38.dirname(file), { recursive: true });
|
|
12091
12447
|
writeFileSync15(file, `${JSON.stringify(settings, null, 2)}
|
|
12092
12448
|
`);
|
|
12093
12449
|
}
|
|
12094
|
-
return { added, alreadyPresent };
|
|
12450
|
+
return { added, alreadyPresent, denyAdded };
|
|
12095
12451
|
}
|
|
12096
12452
|
async function buildPermissions(options) {
|
|
12097
12453
|
const LEGAL_TOOL_NAME = /^[A-Za-z0-9_-]+$/;
|
|
@@ -12111,10 +12467,18 @@ async function buildPermissions(options) {
|
|
|
12111
12467
|
...TOOLS.map((t) => t.name).filter((n) => LEGAL_TOOL_NAME.test(n)).map((n) => `${TENDRIL_PLUGIN_PREFIX}__${n}`),
|
|
12112
12468
|
...figmaTools.map((name) => `${FIGMA_PLUGIN_PREFIX}__${name}`)
|
|
12113
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.",
|
|
12114
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.",
|
|
12115
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).`
|
|
12116
12477
|
};
|
|
12117
12478
|
}
|
|
12479
|
+
function writeSelection(result, user) {
|
|
12480
|
+
return user ? { entries: [...result.toolEntries], denyEntries: [] } : { entries: [...result.toolEntries, ...result.shellEntries, ...result.projectFileEntries], denyEntries: [...result.projectDenyEntries] };
|
|
12481
|
+
}
|
|
12118
12482
|
async function runPermissions(flags) {
|
|
12119
12483
|
if (flags.describe) {
|
|
12120
12484
|
printDescription(PERMISSIONS_DESCRIPTION);
|
|
@@ -12123,21 +12487,25 @@ async function runPermissions(flags) {
|
|
|
12123
12487
|
const result = await buildPermissions({ ...flags.mcpUrl ? { mcpUrl: flags.mcpUrl } : {} });
|
|
12124
12488
|
if (flags.write) {
|
|
12125
12489
|
const base = process.env["INIT_CWD"] ?? process.cwd();
|
|
12126
|
-
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);
|
|
12127
12492
|
if (flags.dryRun) {
|
|
12128
|
-
emitData(flags, { file, wouldAdd:
|
|
12129
|
-
process.stdout.write(
|
|
12130
|
-
`)
|
|
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
|
+
);
|
|
12131
12498
|
});
|
|
12132
12499
|
return;
|
|
12133
12500
|
}
|
|
12134
12501
|
try {
|
|
12135
|
-
const { added, alreadyPresent } = mergeAllowlist(file,
|
|
12136
|
-
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 } }, () => {
|
|
12137
12504
|
process.stdout.write(
|
|
12138
|
-
added.length === 0 ? `already installed: all ${alreadyPresent.length} Tendril pipeline entries present in ${file}
|
|
12139
|
-
` : `installed: ${added.length} allowlist entr${added.length === 1 ? "y" : "ies"} added to ${file}${alreadyPresent.length > 0 ? ` (${alreadyPresent.length} already present)` : ""}
|
|
12140
|
-
|
|
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
|
|
12141
12509
|
`
|
|
12142
12510
|
);
|
|
12143
12511
|
});
|
|
@@ -12162,6 +12530,24 @@ Or paste into .claude/settings.json under permissions.allow:
|
|
|
12162
12530
|
|
|
12163
12531
|
${quoted(result.toolEntries)}
|
|
12164
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
|
+
|
|
12165
12551
|
Shorter but broader \u2014 one entry per server:
|
|
12166
12552
|
|
|
12167
12553
|
${quoted(result.serverEntries)}
|
|
@@ -12173,7 +12559,7 @@ ${result.directConfigNote}
|
|
|
12173
12559
|
);
|
|
12174
12560
|
});
|
|
12175
12561
|
}
|
|
12176
|
-
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;
|
|
12177
12563
|
var init_permissions = __esm({
|
|
12178
12564
|
"packages/cli/src/commands/permissions.ts"() {
|
|
12179
12565
|
"use strict";
|
|
@@ -12185,14 +12571,44 @@ var init_permissions = __esm({
|
|
|
12185
12571
|
init_output();
|
|
12186
12572
|
init_doctor();
|
|
12187
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:*)"];
|
|
12188
12604
|
PERMISSIONS_DESCRIPTION = {
|
|
12189
12605
|
name: "permissions",
|
|
12190
12606
|
summary: "Install (or print) the Claude Code permission allowlist for the Tendril pipeline (tendril + Figma MCP tools).",
|
|
12191
12607
|
args: [],
|
|
12192
12608
|
flags: [
|
|
12193
12609
|
{ flag: "--claude", description: "Claude Code settings format (the default and currently only format)" },
|
|
12194
|
-
{ flag: "--write", description: "Merge the
|
|
12195
|
-
{ flag: "--user", description: "With --write: target ~/.claude/settings.json
|
|
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" },
|
|
12196
12612
|
{ flag: "--mcp-url <url>", description: "Figma MCP endpoint to list live tool names from", default: DEFAULT_MCP_URL },
|
|
12197
12613
|
{ flag: "--json", description: "Machine-readable output" }
|
|
12198
12614
|
],
|
|
@@ -12201,7 +12617,7 @@ var init_permissions = __esm({
|
|
|
12201
12617
|
serverEntries: "string[] \u2014 one entry per MCP server (allows every tool it serves)",
|
|
12202
12618
|
toolEntries: "string[] \u2014 per-tool entries for selective allowlists",
|
|
12203
12619
|
directConfigNote: "string \u2014 prefix rewrite for non-plugin (claude mcp add) installs",
|
|
12204
|
-
written: "with --write: { file, added, alreadyPresent }"
|
|
12620
|
+
written: "with --write: { file, added, alreadyPresent, denyAdded }"
|
|
12205
12621
|
},
|
|
12206
12622
|
exitCodes: { 0: "printed or written", 3: "with --write: the target file exists but is not JSON this command can safely rewrite" },
|
|
12207
12623
|
examples: ["tendril permissions --claude --write", "tendril permissions --claude", "tendril permissions --claude --json"]
|
|
@@ -12217,24 +12633,24 @@ __export(inspect_exports, {
|
|
|
12217
12633
|
INSPECT_DESCRIPTION: () => INSPECT_DESCRIPTION,
|
|
12218
12634
|
runInspect: () => runInspect
|
|
12219
12635
|
});
|
|
12220
|
-
import { existsSync as
|
|
12221
|
-
import
|
|
12636
|
+
import { existsSync as existsSync32, readFileSync as readFileSync28, writeFileSync as writeFileSync16 } from "node:fs";
|
|
12637
|
+
import path39 from "node:path";
|
|
12222
12638
|
async function runInspect(opts) {
|
|
12223
12639
|
if (opts.describe) {
|
|
12224
12640
|
printDescription(INSPECT_DESCRIPTION);
|
|
12225
12641
|
return;
|
|
12226
12642
|
}
|
|
12227
|
-
const bundleDir =
|
|
12228
|
-
const evidenceDir =
|
|
12229
|
-
const manifestPath2 =
|
|
12230
|
-
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)) {
|
|
12231
12647
|
fail(opts, ExitCode.InputValidation, {
|
|
12232
|
-
error: `nothing to inspect in ${bundleDir} \u2014 ${
|
|
12648
|
+
error: `nothing to inspect in ${bundleDir} \u2014 ${existsSync32(manifestPath2) ? "no verify-evidence directory" : "no component.json"}`,
|
|
12233
12649
|
code: "no-evidence",
|
|
12234
12650
|
remediation: `Run \`${tendrilCommand(`verify ${bundleDir}`)}\` first \u2014 inspect reads the ref/render evidence that run writes.`
|
|
12235
12651
|
});
|
|
12236
12652
|
}
|
|
12237
|
-
const { manifest } = readBundleManifest(
|
|
12653
|
+
const { manifest } = readBundleManifest(readFileSync28(manifestPath2, "utf8"));
|
|
12238
12654
|
if (manifest === void 0) {
|
|
12239
12655
|
fail(opts, ExitCode.InputValidation, {
|
|
12240
12656
|
error: "component.json did not parse as a bundle manifest",
|
|
@@ -12242,8 +12658,8 @@ async function runInspect(opts) {
|
|
|
12242
12658
|
remediation: "Re-emit the bundle (score/verify rewrite component.json), then re-run inspect."
|
|
12243
12659
|
});
|
|
12244
12660
|
}
|
|
12245
|
-
const setDir =
|
|
12246
|
-
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`)));
|
|
12247
12663
|
if (reps.length === 0) {
|
|
12248
12664
|
fail(opts, ExitCode.InputValidation, {
|
|
12249
12665
|
error: "verify-evidence holds no ref/render pairs for this bundle's configs",
|
|
@@ -12254,15 +12670,15 @@ async function runInspect(opts) {
|
|
|
12254
12670
|
let crops = 0;
|
|
12255
12671
|
const sections = [];
|
|
12256
12672
|
for (const rep of reps) {
|
|
12257
|
-
const ref = new Uint8Array(
|
|
12258
|
-
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`)));
|
|
12259
12675
|
const nodes = smallSemanticNodes(setDir, rep, opts.maxArea ?? 1024).slice(0, 12);
|
|
12260
12676
|
const cells = [];
|
|
12261
12677
|
for (const [i, n] of nodes.entries()) {
|
|
12262
12678
|
const rect = { x: n.x, y: n.y, w: n.w, h: n.h };
|
|
12263
12679
|
try {
|
|
12264
|
-
writeFileSync16(
|
|
12265
|
-
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));
|
|
12266
12682
|
} catch {
|
|
12267
12683
|
continue;
|
|
12268
12684
|
}
|
|
@@ -12275,7 +12691,7 @@ async function runInspect(opts) {
|
|
|
12275
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>`
|
|
12276
12692
|
);
|
|
12277
12693
|
}
|
|
12278
|
-
const sheet =
|
|
12694
|
+
const sheet = path39.join(evidenceDir, "inspect.html");
|
|
12279
12695
|
writeFileSync16(
|
|
12280
12696
|
sheet,
|
|
12281
12697
|
`<!doctype html><meta charset="utf-8"><title>${esc(manifest.name)} \u2014 tendril inspect</title><style>
|
|
@@ -12353,17 +12769,17 @@ __export(generate_recorded_exports, {
|
|
|
12353
12769
|
runGenerateRecorded: () => runGenerateRecorded
|
|
12354
12770
|
});
|
|
12355
12771
|
import { confirm as confirm3, isCancel as isCancel3 } from "@clack/prompts";
|
|
12356
|
-
import { existsSync as
|
|
12357
|
-
import
|
|
12772
|
+
import { existsSync as existsSync33, readFileSync as readFileSync29 } from "node:fs";
|
|
12773
|
+
import path40 from "node:path";
|
|
12358
12774
|
async function runGenerateRecorded(opts) {
|
|
12359
12775
|
const callerCwd = process.env["INIT_CWD"] ?? process.cwd();
|
|
12360
|
-
const outDirAbs =
|
|
12361
|
-
const recordedAsPath =
|
|
12776
|
+
const outDirAbs = path40.resolve(callerCwd, opts.out);
|
|
12777
|
+
const recordedAsPath = path40.resolve(callerCwd, opts.recorded);
|
|
12362
12778
|
let task;
|
|
12363
12779
|
let taskName;
|
|
12364
12780
|
let authoredApi;
|
|
12365
12781
|
let composition;
|
|
12366
|
-
const isSet =
|
|
12782
|
+
const isSet = existsSync33(path40.join(recordedAsPath, "recording-set.json"));
|
|
12367
12783
|
const registry = TASKS[opts.recorded];
|
|
12368
12784
|
if (registry !== void 0 && !isSet) {
|
|
12369
12785
|
task = registry;
|
|
@@ -12372,7 +12788,7 @@ async function runGenerateRecorded(opts) {
|
|
|
12372
12788
|
try {
|
|
12373
12789
|
const authored = authorTaskFromSet(recordedAsPath);
|
|
12374
12790
|
task = authored.task;
|
|
12375
|
-
taskName =
|
|
12791
|
+
taskName = path40.basename(recordedAsPath);
|
|
12376
12792
|
authoredApi = authored.api;
|
|
12377
12793
|
const roles = RolesSchema.safeParse(loadManifest(recordedAsPath).roles);
|
|
12378
12794
|
if (roles.success) composition = roles.data;
|
|
@@ -12406,7 +12822,7 @@ async function runGenerateRecorded(opts) {
|
|
|
12406
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)`);
|
|
12407
12823
|
}
|
|
12408
12824
|
const missing = task.configs.filter(
|
|
12409
|
-
(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"))
|
|
12410
12826
|
);
|
|
12411
12827
|
if (missing.length > 0) {
|
|
12412
12828
|
fail(opts, ExitCode.RecordingIncomplete, {
|
|
@@ -12476,8 +12892,8 @@ async function runGenerateRecorded(opts) {
|
|
|
12476
12892
|
` : `${line}
|
|
12477
12893
|
`);
|
|
12478
12894
|
if (opts.dryRun) {
|
|
12479
|
-
emitData(opts, { dryRun: true, task: taskName, model: modelId, consent, wouldWrite:
|
|
12480
|
-
process.stdout.write(`dry-run: nothing sent, nothing written (would write ${
|
|
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)})
|
|
12481
12897
|
`);
|
|
12482
12898
|
});
|
|
12483
12899
|
return;
|
|
@@ -12500,10 +12916,10 @@ async function runGenerateRecorded(opts) {
|
|
|
12500
12916
|
});
|
|
12501
12917
|
}
|
|
12502
12918
|
}
|
|
12503
|
-
const bundleDir =
|
|
12504
|
-
if (
|
|
12919
|
+
const bundleDir = path40.join(outDirAbs, taskName);
|
|
12920
|
+
if (existsSync33(path40.join(bundleDir, "component.json"))) {
|
|
12505
12921
|
try {
|
|
12506
|
-
const prior = readBundleManifest(
|
|
12922
|
+
const prior = readBundleManifest(readFileSync29(path40.join(bundleDir, "component.json"), "utf8")).manifest;
|
|
12507
12923
|
if (prior !== void 0 && prior.provenance.recordingSet.hash !== recordingSetHash(task.set, task.configs)) {
|
|
12508
12924
|
fail(opts, ExitCode.InputValidation, {
|
|
12509
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`,
|
|
@@ -12669,7 +13085,7 @@ init_invocation();
|
|
|
12669
13085
|
init_output();
|
|
12670
13086
|
import { intro, isCancel, outro, password } from "@clack/prompts";
|
|
12671
13087
|
import fs from "node:fs";
|
|
12672
|
-
import
|
|
13088
|
+
import path25 from "node:path";
|
|
12673
13089
|
var INIT_DESCRIPTION = {
|
|
12674
13090
|
name: "init",
|
|
12675
13091
|
summary: "Configure Figma and OpenRouter credentials in .env (idempotent).",
|
|
@@ -12706,7 +13122,7 @@ async function runInit(flags) {
|
|
|
12706
13122
|
printDescription(INIT_DESCRIPTION);
|
|
12707
13123
|
return;
|
|
12708
13124
|
}
|
|
12709
|
-
const envPath =
|
|
13125
|
+
const envPath = path25.resolve(process.cwd(), ".env");
|
|
12710
13126
|
const existing = fs.existsSync(envPath) ? parseEnv(fs.readFileSync(envPath, "utf8")) : /* @__PURE__ */ new Map();
|
|
12711
13127
|
let figmaToken = flags.figmaToken ?? existing.get(ENV_KEYS.figma);
|
|
12712
13128
|
let openrouterKey = flags.openrouterKey ?? existing.get(ENV_KEYS.openrouter);
|
|
@@ -12727,7 +13143,7 @@ async function runInit(flags) {
|
|
|
12727
13143
|
next.set(ENV_KEYS.figma, figmaToken);
|
|
12728
13144
|
next.set(ENV_KEYS.openrouter, openrouterKey);
|
|
12729
13145
|
const changed = existing.get(ENV_KEYS.figma) !== next.get(ENV_KEYS.figma) || existing.get(ENV_KEYS.openrouter) !== next.get(ENV_KEYS.openrouter);
|
|
12730
|
-
const gitignorePath =
|
|
13146
|
+
const gitignorePath = path25.resolve(process.cwd(), ".gitignore");
|
|
12731
13147
|
const gitignore = fs.existsSync(gitignorePath) ? fs.readFileSync(gitignorePath, "utf8") : "";
|
|
12732
13148
|
const gitignoreCoversEnv = gitignore.split("\n").some((line) => [".env", ".env", ".env*"].includes(line.trim()));
|
|
12733
13149
|
if (flags.dryRun) {
|
|
@@ -12783,14 +13199,14 @@ init_invocation();
|
|
|
12783
13199
|
init_output();
|
|
12784
13200
|
init_entitlement();
|
|
12785
13201
|
import { confirm as confirm2, isCancel as isCancel2 } from "@clack/prompts";
|
|
12786
|
-
import { readFileSync as
|
|
13202
|
+
import { readFileSync as readFileSync16, readdirSync as readdirSync5, existsSync as existsSync20 } from "node:fs";
|
|
12787
13203
|
|
|
12788
13204
|
// packages/cli/src/pipeline.ts
|
|
12789
13205
|
init_src2();
|
|
12790
13206
|
init_src4();
|
|
12791
13207
|
init_src6();
|
|
12792
13208
|
import { mkdirSync as mkdirSync5, writeFileSync as writeFileSync8 } from "node:fs";
|
|
12793
|
-
import
|
|
13209
|
+
import path26 from "node:path";
|
|
12794
13210
|
|
|
12795
13211
|
// packages/cli/src/assets-module.ts
|
|
12796
13212
|
init_src();
|
|
@@ -13126,7 +13542,7 @@ async function runGenerationPipeline(input) {
|
|
|
13126
13542
|
});
|
|
13127
13543
|
const written = [];
|
|
13128
13544
|
if (!input.dryRun) {
|
|
13129
|
-
const dir =
|
|
13545
|
+
const dir = path26.resolve(input.outDir, semantics.componentName);
|
|
13130
13546
|
mkdirSync5(dir, { recursive: true });
|
|
13131
13547
|
const files = {
|
|
13132
13548
|
// Bundle-local tokens: THE emission the component's CSS resolves
|
|
@@ -13150,13 +13566,13 @@ async function runGenerationPipeline(input) {
|
|
|
13150
13566
|
`
|
|
13151
13567
|
};
|
|
13152
13568
|
for (const [name, content] of Object.entries(files)) {
|
|
13153
|
-
const filePath =
|
|
13569
|
+
const filePath = path26.join(dir, name);
|
|
13154
13570
|
writeFileSync8(filePath, content);
|
|
13155
13571
|
written.push(filePath);
|
|
13156
13572
|
}
|
|
13157
13573
|
for (const artifact of emitTokenArtifacts(input.mapping)) {
|
|
13158
|
-
const filePath =
|
|
13159
|
-
mkdirSync5(
|
|
13574
|
+
const filePath = path26.resolve(input.outDir, artifact.path);
|
|
13575
|
+
mkdirSync5(path26.dirname(filePath), { recursive: true });
|
|
13160
13576
|
writeFileSync8(filePath, artifact.content);
|
|
13161
13577
|
written.push(filePath);
|
|
13162
13578
|
}
|
|
@@ -13215,7 +13631,7 @@ var GENERATE_DESCRIPTION = {
|
|
|
13215
13631
|
function resolveProvidedSource(flags, contextFile) {
|
|
13216
13632
|
let raw;
|
|
13217
13633
|
try {
|
|
13218
|
-
raw =
|
|
13634
|
+
raw = readFileSync16(contextFile, "utf8");
|
|
13219
13635
|
} catch {
|
|
13220
13636
|
fail(flags, ExitCode.InputValidation, {
|
|
13221
13637
|
error: `Cannot read context file "${contextFile}".`,
|
|
@@ -13335,11 +13751,11 @@ token mapping (${mapping.flat.length} variables):
|
|
|
13335
13751
|
let initialCode;
|
|
13336
13752
|
let initialSemantics;
|
|
13337
13753
|
try {
|
|
13338
|
-
if (
|
|
13339
|
-
for (const entry of
|
|
13754
|
+
if (existsSync20(flags.out)) {
|
|
13755
|
+
for (const entry of readdirSync5(flags.out)) {
|
|
13340
13756
|
const cjPath = `${flags.out}/${entry}/component.json`;
|
|
13341
|
-
if (!
|
|
13342
|
-
const cj = JSON.parse(
|
|
13757
|
+
if (!existsSync20(cjPath)) continue;
|
|
13758
|
+
const cj = JSON.parse(readFileSync16(cjPath, "utf8"));
|
|
13343
13759
|
if (cj.name !== void 0 && Array.isArray(cj.props)) {
|
|
13344
13760
|
previousApi = JSON.stringify({
|
|
13345
13761
|
componentName: cj.name,
|
|
@@ -13347,14 +13763,14 @@ token mapping (${mapping.flat.length} variables):
|
|
|
13347
13763
|
});
|
|
13348
13764
|
const tsxPath = `${flags.out}/${entry}/${cj.name}.tsx`;
|
|
13349
13765
|
const cssPath = `${flags.out}/${entry}/${cj.name}.css`;
|
|
13350
|
-
if (flags.refine &&
|
|
13766
|
+
if (flags.refine && existsSync20(tsxPath) && existsSync20(cssPath)) {
|
|
13351
13767
|
initialCode = {
|
|
13352
|
-
tsx:
|
|
13353
|
-
css:
|
|
13768
|
+
tsx: readFileSync16(tsxPath, "utf8"),
|
|
13769
|
+
css: readFileSync16(cssPath, "utf8")
|
|
13354
13770
|
};
|
|
13355
13771
|
const semPath = `${flags.out}/${entry}/semantics.json`;
|
|
13356
|
-
if (
|
|
13357
|
-
initialSemantics = JSON.parse(
|
|
13772
|
+
if (existsSync20(semPath)) {
|
|
13773
|
+
initialSemantics = JSON.parse(readFileSync16(semPath, "utf8"));
|
|
13358
13774
|
}
|
|
13359
13775
|
}
|
|
13360
13776
|
break;
|
|
@@ -13631,6 +14047,23 @@ function buildProgram() {
|
|
|
13631
14047
|
...local["face"] !== void 0 ? { face: Number(local["face"]) } : {}
|
|
13632
14048
|
});
|
|
13633
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
|
+
});
|
|
13634
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) => {
|
|
13635
14068
|
const flags = globalFlags(cmd.parent.parent);
|
|
13636
14069
|
const local = cmd.opts();
|