@tendrilapp/cli 0.1.21 → 0.1.24
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/README.md +8 -0
- package/dist/SKILL.md +15 -8
- package/dist/tendril-mcp.js +15 -2
- package/dist/tendril.js +1433 -532
- package/package.json +1 -1
package/dist/tendril.js
CHANGED
|
@@ -939,14 +939,25 @@ function manifestMains(manifest) {
|
|
|
939
939
|
function requiredToolsFor(manifest, slug) {
|
|
940
940
|
return manifestMains(manifest).includes(slug) ? [...REQUIRED_TOOLS, INTERIOR_TOOL] : REQUIRED_TOOLS;
|
|
941
941
|
}
|
|
942
|
-
function
|
|
942
|
+
function readManifestFile(setDir) {
|
|
943
943
|
const p = manifestPath(setDir);
|
|
944
944
|
if (!existsSync(p)) throw new Error(`no recording-set.json in ${setDir} \u2014 run \`tendril record plan\` first`);
|
|
945
|
-
|
|
945
|
+
const raw = JSON.parse(readFileSync(p, "utf8"));
|
|
946
|
+
return { manifest: SessionManifestSchema.parse(raw), raw };
|
|
947
|
+
}
|
|
948
|
+
function loadManifest(setDir) {
|
|
949
|
+
return readManifestFile(setDir).manifest;
|
|
950
|
+
}
|
|
951
|
+
function writeManifest(setDir, raw) {
|
|
952
|
+
const manifest = SessionManifestSchema.parse(raw);
|
|
953
|
+
mkdirSync(setDir, { recursive: true });
|
|
954
|
+
writeFileSync(manifestPath(setDir), `${JSON.stringify(raw, null, 1)}
|
|
955
|
+
`);
|
|
956
|
+
return manifest;
|
|
946
957
|
}
|
|
947
958
|
function planSet(setDir, component, symbols, opts = {}) {
|
|
948
959
|
if (existsSync(manifestPath(setDir))) {
|
|
949
|
-
const existing =
|
|
960
|
+
const { manifest: existing, raw } = readManifestFile(setDir);
|
|
950
961
|
const wantsNewDefaults = opts.defaults !== void 0 && JSON.stringify(opts.defaults) !== JSON.stringify(existing.defaults ?? {});
|
|
951
962
|
const anythingRecorded = existing.reps.some((r) => RECORD_TOOLS.some((t) => existsSync(path.join(setDir, r.slug, `${t}.json`))));
|
|
952
963
|
if (!wantsNewDefaults || anythingRecorded) {
|
|
@@ -963,12 +974,11 @@ function planSet(setDir, component, symbols, opts = {}) {
|
|
|
963
974
|
usedSlugs.add(slug);
|
|
964
975
|
return { slug, nodeId: r.nodeId, ...r.sourceFrame !== void 0 ? { sourceFrame: r.sourceFrame } : {} };
|
|
965
976
|
});
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
delete
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
return { manifest: existing, plan: { reps: [], notRecorded: [] }, resumed: true, toppedUp: appended.map((a) => ({ slug: a.slug, nodeId: a.nodeId })) };
|
|
977
|
+
raw["reps"].push(...appended);
|
|
978
|
+
raw["planMode"] = "full";
|
|
979
|
+
delete raw["notRecorded"];
|
|
980
|
+
const manifest2 = writeManifest(setDir, raw);
|
|
981
|
+
return { manifest: manifest2, plan: { reps: [], notRecorded: [] }, resumed: true, toppedUp: appended.map((a) => ({ slug: a.slug, nodeId: a.nodeId })) };
|
|
972
982
|
}
|
|
973
983
|
}
|
|
974
984
|
return { manifest: existing, plan: { reps: [], notRecorded: [] }, resumed: true };
|
|
@@ -989,9 +999,7 @@ function planSet(setDir, component, symbols, opts = {}) {
|
|
|
989
999
|
planMode: opts.sample === true ? "sample" : "full",
|
|
990
1000
|
...plan.notRecorded.length > 0 ? { notRecorded: plan.notRecorded.map((n) => `${n.name} (${n.nodeId}): ${n.reason}`).join("; ") } : {}
|
|
991
1001
|
};
|
|
992
|
-
|
|
993
|
-
writeFileSync(manifestPath(setDir), `${JSON.stringify(manifest, null, 1)}
|
|
994
|
-
`);
|
|
1002
|
+
writeManifest(setDir, manifest);
|
|
995
1003
|
return { manifest, plan, resumed: false };
|
|
996
1004
|
}
|
|
997
1005
|
function sessionStatus(setDir) {
|
|
@@ -1216,8 +1224,8 @@ var init_src = __esm({
|
|
|
1216
1224
|
function variableNameToPath(name) {
|
|
1217
1225
|
return name.split("/").map(canonicalCssIdentPart).filter((part) => part.length > 0);
|
|
1218
1226
|
}
|
|
1219
|
-
function tokenPathToCssVar(
|
|
1220
|
-
return `--${
|
|
1227
|
+
function tokenPathToCssVar(path40) {
|
|
1228
|
+
return `--${path40.join("-")}`;
|
|
1221
1229
|
}
|
|
1222
1230
|
function toDtcgToken(variable, defaultMode) {
|
|
1223
1231
|
const modes = Object.keys(variable.valuesByMode);
|
|
@@ -1261,11 +1269,11 @@ function toDtcgToken(variable, defaultMode) {
|
|
|
1261
1269
|
}
|
|
1262
1270
|
function mapVariablesToDtcg(variables, defaultMode = "light") {
|
|
1263
1271
|
const entries = variables.map((variable) => {
|
|
1264
|
-
const
|
|
1265
|
-
if (
|
|
1272
|
+
const path40 = variableNameToPath(variable.name);
|
|
1273
|
+
if (path40.length === 0) {
|
|
1266
1274
|
throw new DtcgMappingError("empty-name", `Figma variable ${variable.id} has an empty name`);
|
|
1267
1275
|
}
|
|
1268
|
-
return { variable, path:
|
|
1276
|
+
return { variable, path: path40 };
|
|
1269
1277
|
});
|
|
1270
1278
|
const groupPrefixes = /* @__PURE__ */ new Set();
|
|
1271
1279
|
for (const e of entries) {
|
|
@@ -1286,21 +1294,21 @@ function mapVariablesToDtcg(variables, defaultMode = "light") {
|
|
|
1286
1294
|
}
|
|
1287
1295
|
const tokens = {};
|
|
1288
1296
|
const flat = [];
|
|
1289
|
-
for (const { variable, path:
|
|
1297
|
+
for (const { variable, path: path40 } of entries) {
|
|
1290
1298
|
const token = toDtcgToken(variable, defaultMode);
|
|
1291
1299
|
let group = tokens;
|
|
1292
|
-
for (const segment of
|
|
1300
|
+
for (const segment of path40.slice(0, -1)) {
|
|
1293
1301
|
const existing = group[segment];
|
|
1294
1302
|
group = existing ?? (group[segment] = {});
|
|
1295
1303
|
}
|
|
1296
|
-
const leaf =
|
|
1304
|
+
const leaf = path40[path40.length - 1];
|
|
1297
1305
|
if (group[leaf] !== void 0) {
|
|
1298
|
-
throw new DtcgMappingError("duplicate-path", `Duplicate token path "${
|
|
1306
|
+
throw new DtcgMappingError("duplicate-path", `Duplicate token path "${path40.join(".")}" (variable ${variable.id})`);
|
|
1299
1307
|
}
|
|
1300
1308
|
group[leaf] = token;
|
|
1301
1309
|
flat.push({
|
|
1302
|
-
path:
|
|
1303
|
-
cssVar: tokenPathToCssVar(
|
|
1310
|
+
path: path40.join("."),
|
|
1311
|
+
cssVar: tokenPathToCssVar(path40),
|
|
1304
1312
|
type: token.$type,
|
|
1305
1313
|
value: token.$value
|
|
1306
1314
|
});
|
|
@@ -1489,9 +1497,9 @@ function boundId(value) {
|
|
|
1489
1497
|
return isObject(value) && typeof value["boundVariableId"] === "string" ? value["boundVariableId"] : void 0;
|
|
1490
1498
|
}
|
|
1491
1499
|
function resolveBinding(ctx, id) {
|
|
1492
|
-
const
|
|
1493
|
-
if (
|
|
1494
|
-
return
|
|
1500
|
+
const path40 = ctx.pathById.get(id);
|
|
1501
|
+
if (path40 === void 0) ctx.unresolved.add(id);
|
|
1502
|
+
return path40;
|
|
1495
1503
|
}
|
|
1496
1504
|
function parseVariantProps(name) {
|
|
1497
1505
|
if (!name.includes("=")) return void 0;
|
|
@@ -1526,8 +1534,8 @@ function walk(ctx, raw) {
|
|
|
1526
1534
|
if (!isObject(paint) || paint["visible"] === false) continue;
|
|
1527
1535
|
const id = boundId(paint);
|
|
1528
1536
|
if (id !== void 0) {
|
|
1529
|
-
const
|
|
1530
|
-
if (
|
|
1537
|
+
const path40 = resolveBinding(ctx, id);
|
|
1538
|
+
if (path40 !== void 0) tokens.add(path40);
|
|
1531
1539
|
} else if (typeof paint["color"] === "string") {
|
|
1532
1540
|
ctx.hardcoded.push({ node: name, property, value: paint["color"] });
|
|
1533
1541
|
}
|
|
@@ -1535,8 +1543,8 @@ function walk(ctx, raw) {
|
|
|
1535
1543
|
}
|
|
1536
1544
|
const radiusId = boundId(raw["cornerRadius"]);
|
|
1537
1545
|
if (radiusId !== void 0) {
|
|
1538
|
-
const
|
|
1539
|
-
if (
|
|
1546
|
+
const path40 = resolveBinding(ctx, radiusId);
|
|
1547
|
+
if (path40 !== void 0) tokens.add(path40);
|
|
1540
1548
|
} else if (typeof raw["cornerRadius"] === "number" && raw["cornerRadius"] !== 0) {
|
|
1541
1549
|
ctx.hardcoded.push({ node: name, property: "border-radius", value: `${raw["cornerRadius"]}px` });
|
|
1542
1550
|
}
|
|
@@ -1546,10 +1554,10 @@ function walk(ctx, raw) {
|
|
|
1546
1554
|
layout = { mode: layoutMode === "HORIZONTAL" ? "flex-row" : "flex-column" };
|
|
1547
1555
|
const gapId = boundId(raw["itemSpacing"]);
|
|
1548
1556
|
if (gapId !== void 0) {
|
|
1549
|
-
const
|
|
1550
|
-
if (
|
|
1551
|
-
layout.gap =
|
|
1552
|
-
tokens.add(
|
|
1557
|
+
const path40 = resolveBinding(ctx, gapId);
|
|
1558
|
+
if (path40 !== void 0) {
|
|
1559
|
+
layout.gap = path40;
|
|
1560
|
+
tokens.add(path40);
|
|
1553
1561
|
}
|
|
1554
1562
|
} else if (typeof raw["itemSpacing"] === "number" && raw["itemSpacing"] !== 0) {
|
|
1555
1563
|
ctx.hardcoded.push({ node: name, property: "gap", value: `${raw["itemSpacing"]}px` });
|
|
@@ -1558,10 +1566,10 @@ function walk(ctx, raw) {
|
|
|
1558
1566
|
for (const field of PADDING_FIELDS) {
|
|
1559
1567
|
const id = boundId(raw[field]);
|
|
1560
1568
|
if (id !== void 0) {
|
|
1561
|
-
const
|
|
1562
|
-
if (
|
|
1563
|
-
paddingPaths.push(
|
|
1564
|
-
tokens.add(
|
|
1569
|
+
const path40 = resolveBinding(ctx, id);
|
|
1570
|
+
if (path40 !== void 0) {
|
|
1571
|
+
paddingPaths.push(path40);
|
|
1572
|
+
tokens.add(path40);
|
|
1565
1573
|
}
|
|
1566
1574
|
} else if (typeof raw[field] === "number" && raw[field] !== 0) {
|
|
1567
1575
|
ctx.hardcoded.push({ node: name, property: field, value: `${raw[field]}px` });
|
|
@@ -1768,20 +1776,72 @@ var init_src3 = __esm({
|
|
|
1768
1776
|
}
|
|
1769
1777
|
});
|
|
1770
1778
|
|
|
1779
|
+
// packages/cli/src/invocation.ts
|
|
1780
|
+
import { existsSync as existsSync3, realpathSync } from "node:fs";
|
|
1781
|
+
import path3 from "node:path";
|
|
1782
|
+
import { fileURLToPath } from "node:url";
|
|
1783
|
+
function findPathTendril(pathEnv, platform) {
|
|
1784
|
+
const dirs = pathEnv.split(path3.delimiter).filter((d) => d !== "" && !/node_modules[\\/]\.bin/.test(d) && !/[\\/]_npx[\\/]/.test(d));
|
|
1785
|
+
const names = platform === "win32" ? ["tendril.cmd", "tendril.bat"] : ["tendril"];
|
|
1786
|
+
for (const dir of dirs) {
|
|
1787
|
+
for (const name of names) {
|
|
1788
|
+
const candidate = path3.join(dir, name);
|
|
1789
|
+
if (existsSync3(candidate)) return candidate;
|
|
1790
|
+
}
|
|
1791
|
+
}
|
|
1792
|
+
return null;
|
|
1793
|
+
}
|
|
1794
|
+
function packageRootOf(file) {
|
|
1795
|
+
let dir = path3.dirname(file);
|
|
1796
|
+
for (; ; ) {
|
|
1797
|
+
if (existsSync3(path3.join(dir, "package.json"))) return dir;
|
|
1798
|
+
const parent = path3.dirname(dir);
|
|
1799
|
+
if (parent === dir) return null;
|
|
1800
|
+
dir = parent;
|
|
1801
|
+
}
|
|
1802
|
+
}
|
|
1803
|
+
function resolveInvocation(pathEnv, platform, selfFile = fileURLToPath(import.meta.url)) {
|
|
1804
|
+
const binary = findPathTendril(pathEnv, platform);
|
|
1805
|
+
if (binary === null) return NPX_INVOCATION;
|
|
1806
|
+
try {
|
|
1807
|
+
const onPath = packageRootOf(realpathSync(binary));
|
|
1808
|
+
const self = packageRootOf(realpathSync(selfFile));
|
|
1809
|
+
if (onPath === null || self === null) return NPX_INVOCATION;
|
|
1810
|
+
return realpathSync(onPath) === realpathSync(self) ? "tendril" : NPX_INVOCATION;
|
|
1811
|
+
} catch {
|
|
1812
|
+
return NPX_INVOCATION;
|
|
1813
|
+
}
|
|
1814
|
+
}
|
|
1815
|
+
function tendrilInvocation() {
|
|
1816
|
+
cached ??= resolveInvocation(process.env["PATH"] ?? "", process.platform);
|
|
1817
|
+
return cached;
|
|
1818
|
+
}
|
|
1819
|
+
function tendrilCommand(args) {
|
|
1820
|
+
return `${tendrilInvocation()} ${args}`;
|
|
1821
|
+
}
|
|
1822
|
+
var NPX_INVOCATION, cached;
|
|
1823
|
+
var init_invocation = __esm({
|
|
1824
|
+
"packages/cli/src/invocation.ts"() {
|
|
1825
|
+
"use strict";
|
|
1826
|
+
NPX_INVOCATION = "npx -y -p @tendrilapp/cli@latest tendril";
|
|
1827
|
+
cached = null;
|
|
1828
|
+
}
|
|
1829
|
+
});
|
|
1830
|
+
|
|
1771
1831
|
// packages/verify/src/browser.ts
|
|
1772
1832
|
import { execFileSync } from "node:child_process";
|
|
1773
|
-
import { existsSync as
|
|
1774
|
-
import
|
|
1833
|
+
import { existsSync as existsSync4, readdirSync } from "node:fs";
|
|
1834
|
+
import path4 from "node:path";
|
|
1775
1835
|
function resolveChrome() {
|
|
1776
1836
|
const fromEnv = process.env["TENDRIL_CHROME"] ?? process.env["CHROME_PATH"];
|
|
1777
1837
|
if (fromEnv !== void 0 && fromEnv !== "") {
|
|
1778
|
-
if (!
|
|
1838
|
+
if (!existsSync4(fromEnv)) {
|
|
1779
1839
|
throw new Error(`CHROME_PATH points at ${fromEnv}, which does not exist \u2014 fix the variable or unset it to use discovery`);
|
|
1780
1840
|
}
|
|
1781
1841
|
return fromEnv;
|
|
1782
1842
|
}
|
|
1783
1843
|
const candidates = process.platform === "darwin" ? MAC_CANDIDATES : process.platform === "win32" ? WIN_CANDIDATES : [];
|
|
1784
|
-
for (const c of candidates) if (
|
|
1844
|
+
for (const c of candidates) if (existsSync4(c)) return c;
|
|
1785
1845
|
if (process.platform !== "win32") {
|
|
1786
1846
|
for (const name of PATH_NAMES) {
|
|
1787
1847
|
try {
|
|
@@ -1795,7 +1855,7 @@ function resolveChrome() {
|
|
|
1795
1855
|
}
|
|
1796
1856
|
function versionFromInstallDir(exePath) {
|
|
1797
1857
|
try {
|
|
1798
|
-
const builds = readdirSync(
|
|
1858
|
+
const builds = readdirSync(path4.dirname(exePath), { withFileTypes: true }).filter((e) => e.isDirectory() && /^\d+(\.\d+){3}$/.test(e.name)).map((e) => e.name.split(".").map(Number));
|
|
1799
1859
|
builds.sort((a, b) => a[0] - b[0] || a[1] - b[1] || a[2] - b[2] || a[3] - b[3]);
|
|
1800
1860
|
return builds.length === 0 ? null : builds[builds.length - 1].join(".");
|
|
1801
1861
|
} catch {
|
|
@@ -1807,7 +1867,7 @@ function windowsBrowserName(exePath) {
|
|
|
1807
1867
|
if (p.includes("\\google\\chrome\\")) return "Google Chrome";
|
|
1808
1868
|
if (p.includes("\\microsoft\\edge\\")) return "Microsoft Edge";
|
|
1809
1869
|
if (p.includes("chromium")) return "Chromium";
|
|
1810
|
-
return
|
|
1870
|
+
return path4.win32.basename(exePath, ".exe");
|
|
1811
1871
|
}
|
|
1812
1872
|
function chromeVersion() {
|
|
1813
1873
|
if (_version !== void 0) return _version;
|
|
@@ -1842,32 +1902,32 @@ var init_browser = __esm({
|
|
|
1842
1902
|
});
|
|
1843
1903
|
|
|
1844
1904
|
// packages/verify/src/runtime.ts
|
|
1845
|
-
import { existsSync as
|
|
1905
|
+
import { existsSync as existsSync5, mkdtempSync, symlinkSync } from "node:fs";
|
|
1846
1906
|
import { createRequire } from "node:module";
|
|
1847
1907
|
import os from "node:os";
|
|
1848
|
-
import
|
|
1849
|
-
import { fileURLToPath } from "node:url";
|
|
1908
|
+
import path5 from "node:path";
|
|
1909
|
+
import { fileURLToPath as fileURLToPath2 } from "node:url";
|
|
1850
1910
|
function runtimePackageRoot() {
|
|
1851
1911
|
const env = process.env["TENDRIL_PACKAGE_ROOT"];
|
|
1852
|
-
if (env !== void 0 && env !== "") return
|
|
1853
|
-
return
|
|
1912
|
+
if (env !== void 0 && env !== "") return path5.resolve(env);
|
|
1913
|
+
return path5.resolve(path5.dirname(fileURLToPath2(import.meta.url)), "..");
|
|
1854
1914
|
}
|
|
1855
1915
|
function runtimeNodeModules() {
|
|
1856
1916
|
const root = runtimePackageRoot();
|
|
1857
|
-
for (let dir = root; ; dir =
|
|
1858
|
-
const candidate =
|
|
1859
|
-
if (
|
|
1860
|
-
if (
|
|
1917
|
+
for (let dir = root; ; dir = path5.dirname(dir)) {
|
|
1918
|
+
const candidate = path5.basename(dir) === "node_modules" ? dir : path5.join(dir, "node_modules");
|
|
1919
|
+
if (existsSync5(path5.join(candidate, "react"))) return candidate;
|
|
1920
|
+
if (path5.dirname(dir) === dir) break;
|
|
1861
1921
|
}
|
|
1862
1922
|
throw new Error(
|
|
1863
1923
|
`Tendril's installed dependencies are missing: no node_modules containing react found at or above ${root}. This is an INSTALLATION problem, not a component error \u2014 reinstall with \`npm install -g @tendrilapp/cli\` (or \`tendrilapp\`) and retry.`
|
|
1864
1924
|
);
|
|
1865
1925
|
}
|
|
1866
1926
|
function newScratchDir(prefix) {
|
|
1867
|
-
const dir = mkdtempSync(
|
|
1927
|
+
const dir = mkdtempSync(path5.join(os.tmpdir(), `tendril-${prefix}-`));
|
|
1868
1928
|
const nodeModules = runtimeNodeModules();
|
|
1869
1929
|
try {
|
|
1870
|
-
symlinkSync(nodeModules,
|
|
1930
|
+
symlinkSync(nodeModules, path5.join(dir, "node_modules"), "junction");
|
|
1871
1931
|
} catch (err) {
|
|
1872
1932
|
throw new Error(
|
|
1873
1933
|
`Tendril could not link its dependencies into the scratch dir (${nodeModules} -> ${dir}): ${err instanceof Error ? err.message : String(err)}. This is an environment problem, not a component error.`
|
|
@@ -1876,7 +1936,7 @@ function newScratchDir(prefix) {
|
|
|
1876
1936
|
return dir;
|
|
1877
1937
|
}
|
|
1878
1938
|
function reactPinPlugin() {
|
|
1879
|
-
const req = createRequire(
|
|
1939
|
+
const req = createRequire(path5.join(runtimePackageRoot(), "package.json"));
|
|
1880
1940
|
return {
|
|
1881
1941
|
name: "tendril-react-pin",
|
|
1882
1942
|
setup(b) {
|
|
@@ -1929,9 +1989,9 @@ var init_gates = __esm({
|
|
|
1929
1989
|
|
|
1930
1990
|
// packages/verify/src/tsc-check.ts
|
|
1931
1991
|
import ts from "typescript";
|
|
1932
|
-
import
|
|
1992
|
+
import path6 from "node:path";
|
|
1933
1993
|
function runTscStrict(files) {
|
|
1934
|
-
const program = ts.createProgram(files.map((f) =>
|
|
1994
|
+
const program = ts.createProgram(files.map((f) => path6.resolve(f)), STRICT_OPTIONS);
|
|
1935
1995
|
const diagnostics = ts.getPreEmitDiagnostics(program);
|
|
1936
1996
|
const mapped = diagnostics.map((d) => {
|
|
1937
1997
|
const file = d.file?.fileName;
|
|
@@ -2090,7 +2150,7 @@ var init_token_lint = __esm({
|
|
|
2090
2150
|
|
|
2091
2151
|
// packages/verify/src/loop.ts
|
|
2092
2152
|
import { rmSync, writeFileSync as writeFileSync2 } from "node:fs";
|
|
2093
|
-
import
|
|
2153
|
+
import path7 from "node:path";
|
|
2094
2154
|
async function runChecks(componentName, files, extraFiles, definedVars2) {
|
|
2095
2155
|
const findings = [];
|
|
2096
2156
|
for (const violation of scanForbiddenPatterns(files.tsx)) {
|
|
@@ -2105,10 +2165,10 @@ async function runChecks(componentName, files, extraFiles, definedVars2) {
|
|
|
2105
2165
|
}
|
|
2106
2166
|
const workDir = newScratchDir("loop");
|
|
2107
2167
|
try {
|
|
2108
|
-
const tsxPath =
|
|
2168
|
+
const tsxPath = path7.join(workDir, `${componentName}.tsx`);
|
|
2109
2169
|
writeFileSync2(tsxPath, files.tsx);
|
|
2110
2170
|
for (const [name, content] of Object.entries(extraFiles ?? {})) {
|
|
2111
|
-
writeFileSync2(
|
|
2171
|
+
writeFileSync2(path7.join(workDir, name), content);
|
|
2112
2172
|
}
|
|
2113
2173
|
const tsc = runTscStrict([tsxPath]);
|
|
2114
2174
|
for (const d of tsc.diagnostics) {
|
|
@@ -2275,11 +2335,11 @@ var init_report = __esm({
|
|
|
2275
2335
|
});
|
|
2276
2336
|
|
|
2277
2337
|
// packages/verify/src/mount-limits.ts
|
|
2278
|
-
function
|
|
2279
|
-
return process.env["
|
|
2338
|
+
function lcdTextEnabled() {
|
|
2339
|
+
return process.env["TENDRIL_ENABLE_LCD_TEXT"] === "1";
|
|
2280
2340
|
}
|
|
2281
2341
|
function mountArgs() {
|
|
2282
|
-
return [MEMORY_CAP_ARG, ...
|
|
2342
|
+
return [MEMORY_CAP_ARG, ...lcdTextEnabled() ? [] : ["--disable-lcd-text"]];
|
|
2283
2343
|
}
|
|
2284
2344
|
function raceMountDeadline(work, deadlineMs, onDeadline) {
|
|
2285
2345
|
return Promise.race([
|
|
@@ -2750,8 +2810,8 @@ var init_image_diff = __esm({
|
|
|
2750
2810
|
});
|
|
2751
2811
|
|
|
2752
2812
|
// packages/verify/src/visual-facts.ts
|
|
2753
|
-
import
|
|
2754
|
-
import { fileURLToPath as
|
|
2813
|
+
import path8 from "node:path";
|
|
2814
|
+
import { fileURLToPath as fileURLToPath3 } from "node:url";
|
|
2755
2815
|
import { build } from "esbuild";
|
|
2756
2816
|
import { chromium } from "playwright-core";
|
|
2757
2817
|
function resolveAxisValue(declared, props, axis, value) {
|
|
@@ -3295,7 +3355,7 @@ var init_visual_facts = __esm({
|
|
|
3295
3355
|
init_browser();
|
|
3296
3356
|
init_mount_limits();
|
|
3297
3357
|
init_image_diff();
|
|
3298
|
-
RESOLVE_DIR =
|
|
3358
|
+
RESOLVE_DIR = path8.resolve(path8.dirname(fileURLToPath3(import.meta.url)), "..");
|
|
3299
3359
|
TOLERANCE_PX = 2;
|
|
3300
3360
|
WIDTH_SLACK = 0.25;
|
|
3301
3361
|
IMAGE_SIMILARITY_FLOOR = 0.8;
|
|
@@ -3304,26 +3364,41 @@ var init_visual_facts = __esm({
|
|
|
3304
3364
|
});
|
|
3305
3365
|
|
|
3306
3366
|
// packages/verify/src/paths.ts
|
|
3307
|
-
import
|
|
3308
|
-
import { fileURLToPath as
|
|
3367
|
+
import path9 from "node:path";
|
|
3368
|
+
import { fileURLToPath as fileURLToPath4 } from "node:url";
|
|
3309
3369
|
var VERIFY_PKG_DIR, REPO_ROOT;
|
|
3310
3370
|
var init_paths = __esm({
|
|
3311
3371
|
"packages/verify/src/paths.ts"() {
|
|
3312
3372
|
"use strict";
|
|
3313
|
-
VERIFY_PKG_DIR =
|
|
3314
|
-
REPO_ROOT =
|
|
3373
|
+
VERIFY_PKG_DIR = path9.resolve(path9.dirname(fileURLToPath4(import.meta.url)), "..");
|
|
3374
|
+
REPO_ROOT = path9.resolve(VERIFY_PKG_DIR, "..", "..");
|
|
3315
3375
|
}
|
|
3316
3376
|
});
|
|
3317
3377
|
|
|
3318
3378
|
// packages/verify/src/font-resolve.ts
|
|
3319
3379
|
import { createHash } from "node:crypto";
|
|
3320
|
-
import { existsSync as
|
|
3380
|
+
import { existsSync as existsSync6, mkdirSync as mkdirSync2, readFileSync as readFileSync3, writeFileSync as writeFileSync3 } from "node:fs";
|
|
3321
3381
|
import os2 from "node:os";
|
|
3322
|
-
import
|
|
3382
|
+
import path10 from "node:path";
|
|
3323
3383
|
function fontCacheDir() {
|
|
3324
3384
|
const env = process.env["TENDRIL_FONT_CACHE"];
|
|
3325
|
-
if (env !== void 0 && env !== "") return
|
|
3326
|
-
return
|
|
3385
|
+
if (env !== void 0 && env !== "") return path10.resolve(env);
|
|
3386
|
+
return path10.join(os2.homedir(), ".tendril", "fonts");
|
|
3387
|
+
}
|
|
3388
|
+
function normalizeFontLicense(value) {
|
|
3389
|
+
return typeof value === "string" && FONT_LICENSES.includes(value) ? value : "unknown";
|
|
3390
|
+
}
|
|
3391
|
+
async function googleFontMetadata(queryName) {
|
|
3392
|
+
try {
|
|
3393
|
+
const res = await fetch(`https://fonts.google.com/metadata/fonts/${encodeURIComponent(queryName)}`, { headers: { "User-Agent": UA } });
|
|
3394
|
+
if (!res.ok) return { license: "unknown", family: null };
|
|
3395
|
+
const parsed = JSON.parse((await res.text()).replace(/^\)\]\}'\s*/, ""));
|
|
3396
|
+
const family = typeof parsed.family === "string" && parsed.family !== "" ? parsed.family : null;
|
|
3397
|
+
if (parsed.isOpenSource !== true || typeof parsed.license !== "string") return { license: "unknown", family };
|
|
3398
|
+
return { license: GOOGLE_LICENSE_IDS.get(parsed.license.toLowerCase()) ?? "unknown", family };
|
|
3399
|
+
} catch {
|
|
3400
|
+
return { license: "unknown", family: null };
|
|
3401
|
+
}
|
|
3327
3402
|
}
|
|
3328
3403
|
function googleQueryCandidates(family) {
|
|
3329
3404
|
const stripped = family.replace(/\s+(Variable|VF)$/i, "");
|
|
@@ -3335,12 +3410,14 @@ async function resolveFonts(family, weights, cacheDir = DEFAULT_FONT_CACHE) {
|
|
|
3335
3410
|
const failures = [];
|
|
3336
3411
|
let css = null;
|
|
3337
3412
|
let lastStatus = 0;
|
|
3413
|
+
let servedName = family;
|
|
3338
3414
|
try {
|
|
3339
3415
|
for (const queryName of googleQueryCandidates(family)) {
|
|
3340
3416
|
const cssUrl = `https://fonts.googleapis.com/css2?family=${encodeURIComponent(queryName).replace(/%20/g, "+")}:wght@${weights.join(";")}&display=swap`;
|
|
3341
3417
|
const res = await fetch(cssUrl, { headers: { "User-Agent": UA } });
|
|
3342
3418
|
if (res.ok) {
|
|
3343
3419
|
css = await res.text();
|
|
3420
|
+
servedName = queryName;
|
|
3344
3421
|
break;
|
|
3345
3422
|
}
|
|
3346
3423
|
lastStatus = res.status;
|
|
@@ -3354,6 +3431,7 @@ async function resolveFonts(family, weights, cacheDir = DEFAULT_FONT_CACHE) {
|
|
|
3354
3431
|
} catch (err) {
|
|
3355
3432
|
return { resolved, failures: weights.map((weight) => ({ family, weight, reason: `offline or fetch failed: ${err instanceof Error ? err.message : String(err)}` })) };
|
|
3356
3433
|
}
|
|
3434
|
+
const meta = await googleFontMetadata(servedName);
|
|
3357
3435
|
for (const weight of weights) {
|
|
3358
3436
|
const blocks = [...css.matchAll(/@font-face\s*{[^}]*}/g)].map((m) => m[0]);
|
|
3359
3437
|
const block = blocks.find((b) => b.includes(`font-weight: ${weight}`) && /unicode-range:[^;]*U\+0000/.test(b));
|
|
@@ -3370,25 +3448,25 @@ async function resolveFonts(family, weights, cacheDir = DEFAULT_FONT_CACHE) {
|
|
|
3370
3448
|
}
|
|
3371
3449
|
const bytes = new Uint8Array(await fileRes.arrayBuffer());
|
|
3372
3450
|
const sha256 = createHash("sha256").update(bytes).digest("hex");
|
|
3373
|
-
const file =
|
|
3451
|
+
const file = path10.join(cacheDir, `${family.toLowerCase().replace(/\s+/g, "-")}-${weight}.woff2`);
|
|
3374
3452
|
writeFileSync3(file, bytes);
|
|
3375
|
-
resolved.push({ family, weight, source: url, sha256, file });
|
|
3453
|
+
resolved.push({ family, weight, source: url, sha256, file, license: meta.license, ...meta.family !== null ? { servedFamily: meta.family } : {} });
|
|
3376
3454
|
} catch (err) {
|
|
3377
3455
|
failures.push({ family, weight, reason: `download failed: ${err instanceof Error ? err.message : String(err)}` });
|
|
3378
3456
|
}
|
|
3379
3457
|
}
|
|
3380
|
-
const mPath =
|
|
3381
|
-
const prior =
|
|
3382
|
-
const portable2 = resolved.map((m) => ({ ...m, file:
|
|
3458
|
+
const mPath = path10.join(cacheDir, "manifest.json");
|
|
3459
|
+
const prior = existsSync6(mPath) ? JSON.parse(readFileSync3(mPath, "utf8")) : [];
|
|
3460
|
+
const portable2 = resolved.map((m) => ({ ...m, file: path10.basename(m.file) }));
|
|
3383
3461
|
const merged = [...prior.filter((p) => !resolved.some((m) => m.family === p.family && m.weight === p.weight)), ...portable2];
|
|
3384
3462
|
if (resolved.length > 0) writeFileSync3(mPath, `${JSON.stringify(merged, null, 2)}
|
|
3385
3463
|
`);
|
|
3386
3464
|
return { resolved, failures };
|
|
3387
3465
|
}
|
|
3388
3466
|
function addLocalFont(family, weight, filePath, cacheDir = DEFAULT_FONT_CACHE) {
|
|
3389
|
-
const src =
|
|
3390
|
-
if (!
|
|
3391
|
-
const ext =
|
|
3467
|
+
const src = path10.resolve(filePath);
|
|
3468
|
+
if (!existsSync6(src)) throw new Error(`font file not found: ${src}`);
|
|
3469
|
+
const ext = path10.extname(src).toLowerCase();
|
|
3392
3470
|
if (![".woff2", ".woff", ".ttf", ".otf"].includes(ext)) {
|
|
3393
3471
|
throw new Error(`unsupported font file "${ext}" \u2014 use .woff2 (preferred), .woff, .ttf or .otf`);
|
|
3394
3472
|
}
|
|
@@ -3396,20 +3474,20 @@ function addLocalFont(family, weight, filePath, cacheDir = DEFAULT_FONT_CACHE) {
|
|
|
3396
3474
|
if (bytes.length === 0) throw new Error(`font file is empty: ${src}`);
|
|
3397
3475
|
mkdirSync2(cacheDir, { recursive: true });
|
|
3398
3476
|
const sha256 = createHash("sha256").update(bytes).digest("hex");
|
|
3399
|
-
const file =
|
|
3477
|
+
const file = path10.join(cacheDir, `${family.toLowerCase().replace(/\s+/g, "-")}-${weight}${ext}`);
|
|
3400
3478
|
writeFileSync3(file, bytes);
|
|
3401
|
-
const face = { family, weight, source: `local:${
|
|
3402
|
-
const mPath =
|
|
3403
|
-
const prior =
|
|
3404
|
-
const merged = [...prior.filter((p) => !(p.family === family && p.weight === weight)), { ...face, file:
|
|
3479
|
+
const face = { family, weight, source: `local:${path10.basename(src)}`, sha256, file, license: "unknown" };
|
|
3480
|
+
const mPath = path10.join(cacheDir, "manifest.json");
|
|
3481
|
+
const prior = existsSync6(mPath) ? JSON.parse(readFileSync3(mPath, "utf8")) : [];
|
|
3482
|
+
const merged = [...prior.filter((p) => !(p.family === family && p.weight === weight)), { ...face, file: path10.basename(file) }];
|
|
3405
3483
|
writeFileSync3(mPath, `${JSON.stringify(merged, null, 2)}
|
|
3406
3484
|
`);
|
|
3407
3485
|
return face;
|
|
3408
3486
|
}
|
|
3409
3487
|
function checkFontLock(lockPath, cacheDir = DEFAULT_FONT_CACHE) {
|
|
3410
3488
|
const lock = JSON.parse(readFileSync3(lockPath, "utf8"));
|
|
3411
|
-
const mPath =
|
|
3412
|
-
const manifest =
|
|
3489
|
+
const mPath = path10.join(cacheDir, "manifest.json");
|
|
3490
|
+
const manifest = existsSync6(mPath) ? JSON.parse(readFileSync3(mPath, "utf8")) : [];
|
|
3413
3491
|
return lock.map((l) => {
|
|
3414
3492
|
const m = manifest.find((x) => x.family === l.family && x.weight === l.weight);
|
|
3415
3493
|
if (m === void 0) return { family: l.family, weight: l.weight, status: "missing" };
|
|
@@ -3417,8 +3495,8 @@ function checkFontLock(lockPath, cacheDir = DEFAULT_FONT_CACHE) {
|
|
|
3417
3495
|
});
|
|
3418
3496
|
}
|
|
3419
3497
|
function resolvedFontFamilies(cacheDir = DEFAULT_FONT_CACHE) {
|
|
3420
|
-
const mPath =
|
|
3421
|
-
if (!
|
|
3498
|
+
const mPath = path10.join(cacheDir, "manifest.json");
|
|
3499
|
+
if (!existsSync6(mPath)) return [];
|
|
3422
3500
|
let entries;
|
|
3423
3501
|
try {
|
|
3424
3502
|
entries = JSON.parse(readFileSync3(mPath, "utf8"));
|
|
@@ -3435,14 +3513,35 @@ function resolvedFontFamilies(cacheDir = DEFAULT_FONT_CACHE) {
|
|
|
3435
3513
|
return [...byFamily.entries()].map(([family, w]) => ({ family, weights: [...w].sort((a, b) => a - b) }));
|
|
3436
3514
|
}
|
|
3437
3515
|
function requiredFontsManifest(families, cacheDir = DEFAULT_FONT_CACHE) {
|
|
3438
|
-
const mPath =
|
|
3439
|
-
const manifest =
|
|
3516
|
+
const mPath = path10.join(cacheDir, "manifest.json");
|
|
3517
|
+
const manifest = existsSync6(mPath) ? JSON.parse(readFileSync3(mPath, "utf8")) : [];
|
|
3440
3518
|
const wanted = new Set(families.map((f) => f.toLowerCase()));
|
|
3441
|
-
return manifest.filter((f) => wanted.has(f.family.toLowerCase()))
|
|
3519
|
+
return manifest.filter((f) => wanted.has(f.family.toLowerCase())).map((f) => ({
|
|
3520
|
+
family: f.family,
|
|
3521
|
+
weight: f.weight,
|
|
3522
|
+
source: f.source,
|
|
3523
|
+
sha256: f.sha256,
|
|
3524
|
+
file: f.file,
|
|
3525
|
+
license: normalizeFontLicense(f.license),
|
|
3526
|
+
...typeof f.servedFamily === "string" && f.servedFamily !== "" ? { servedFamily: f.servedFamily } : {}
|
|
3527
|
+
}));
|
|
3528
|
+
}
|
|
3529
|
+
function facesMissingLicense(cacheDir = DEFAULT_FONT_CACHE) {
|
|
3530
|
+
const mPath = path10.join(cacheDir, "manifest.json");
|
|
3531
|
+
if (!existsSync6(mPath)) return [];
|
|
3532
|
+
let entries;
|
|
3533
|
+
try {
|
|
3534
|
+
entries = JSON.parse(readFileSync3(mPath, "utf8"));
|
|
3535
|
+
} catch {
|
|
3536
|
+
return [];
|
|
3537
|
+
}
|
|
3538
|
+
return entries.filter(
|
|
3539
|
+
(e) => typeof e.family === "string" && typeof e.weight === "number" && typeof e.source === "string" && e.source.startsWith(GOOGLE_FONT_FILE_PREFIX) && !(typeof e.license === "string" && FONT_LICENSES.includes(e.license))
|
|
3540
|
+
).map((e) => ({ family: e.family, weight: e.weight, sha256: e.sha256 }));
|
|
3442
3541
|
}
|
|
3443
3542
|
function verifiedFontFamilies(cacheDir = DEFAULT_FONT_CACHE) {
|
|
3444
|
-
const mPath =
|
|
3445
|
-
if (!
|
|
3543
|
+
const mPath = path10.join(cacheDir, "manifest.json");
|
|
3544
|
+
if (!existsSync6(mPath)) return [];
|
|
3446
3545
|
let entries;
|
|
3447
3546
|
try {
|
|
3448
3547
|
entries = JSON.parse(readFileSync3(mPath, "utf8"));
|
|
@@ -3452,8 +3551,8 @@ function verifiedFontFamilies(cacheDir = DEFAULT_FONT_CACHE) {
|
|
|
3452
3551
|
const byFamily = /* @__PURE__ */ new Map();
|
|
3453
3552
|
for (const e of entries) {
|
|
3454
3553
|
if (typeof e.family !== "string" || typeof e.weight !== "number" || typeof e.file !== "string") continue;
|
|
3455
|
-
const file =
|
|
3456
|
-
if (!
|
|
3554
|
+
const file = path10.isAbsolute(e.file) && existsSync6(e.file) ? e.file : path10.resolve(cacheDir, path10.basename(e.file));
|
|
3555
|
+
if (!existsSync6(file)) continue;
|
|
3457
3556
|
if (createHash("sha256").update(readFileSync3(file)).digest("hex") !== e.sha256) continue;
|
|
3458
3557
|
const set = byFamily.get(e.family) ?? /* @__PURE__ */ new Set();
|
|
3459
3558
|
set.add(e.weight);
|
|
@@ -3461,67 +3560,86 @@ function verifiedFontFamilies(cacheDir = DEFAULT_FONT_CACHE) {
|
|
|
3461
3560
|
}
|
|
3462
3561
|
return [...byFamily.entries()].map(([family, w]) => ({ family, weights: [...w].sort((a, b) => a - b) }));
|
|
3463
3562
|
}
|
|
3464
|
-
var DEFAULT_FONT_CACHE, UA;
|
|
3563
|
+
var DEFAULT_FONT_CACHE, UA, FONT_LICENSES, GOOGLE_LICENSE_IDS, GOOGLE_FONT_FILE_PREFIX;
|
|
3465
3564
|
var init_font_resolve = __esm({
|
|
3466
3565
|
"packages/verify/src/font-resolve.ts"() {
|
|
3467
3566
|
"use strict";
|
|
3468
3567
|
DEFAULT_FONT_CACHE = fontCacheDir();
|
|
3469
3568
|
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";
|
|
3569
|
+
FONT_LICENSES = ["OFL-1.1", "Apache-2.0", "UFL-1.0", "proprietary", "unknown"];
|
|
3570
|
+
GOOGLE_LICENSE_IDS = /* @__PURE__ */ new Map([
|
|
3571
|
+
["ofl", "OFL-1.1"],
|
|
3572
|
+
["apache2", "Apache-2.0"],
|
|
3573
|
+
["apache", "Apache-2.0"],
|
|
3574
|
+
["ufl", "UFL-1.0"]
|
|
3575
|
+
]);
|
|
3576
|
+
GOOGLE_FONT_FILE_PREFIX = "https://fonts.gstatic.com/";
|
|
3470
3577
|
}
|
|
3471
3578
|
});
|
|
3472
3579
|
|
|
3473
3580
|
// packages/verify/src/font-faces.ts
|
|
3474
3581
|
import { createHash as createHash2 } from "node:crypto";
|
|
3475
|
-
import { existsSync as
|
|
3476
|
-
import
|
|
3477
|
-
function
|
|
3478
|
-
if (!
|
|
3582
|
+
import { existsSync as existsSync7, readFileSync as readFileSync4 } from "node:fs";
|
|
3583
|
+
import path11 from "node:path";
|
|
3584
|
+
function injectedGroups(manifestPath2) {
|
|
3585
|
+
if (!existsSync7(manifestPath2)) return { groups: [], shared: false };
|
|
3479
3586
|
const claimed = JSON.parse(readFileSync4(manifestPath2, "utf8"));
|
|
3480
|
-
const
|
|
3481
|
-
if (path10.isAbsolute(f) && existsSync6(f)) return f;
|
|
3482
|
-
return path10.resolve(path10.dirname(manifestPath2), path10.basename(f));
|
|
3483
|
-
};
|
|
3484
|
-
const manifest = claimed.filter((f) => {
|
|
3485
|
-
const file = resolveEntry(f.file);
|
|
3486
|
-
if (!existsSync6(file)) return false;
|
|
3487
|
-
return createHash2("sha256").update(readFileSync4(file)).digest("hex") === f.sha256;
|
|
3488
|
-
});
|
|
3489
|
-
const resolveFile = (f) => {
|
|
3490
|
-
if (path10.isAbsolute(f) && existsSync6(f)) return f;
|
|
3491
|
-
return path10.resolve(path10.dirname(manifestPath2), path10.basename(f));
|
|
3492
|
-
};
|
|
3587
|
+
const resolveFile = (f) => path11.isAbsolute(f) && existsSync7(f) ? f : path11.resolve(path11.dirname(manifestPath2), path11.basename(f));
|
|
3493
3588
|
const byFile = /* @__PURE__ */ new Map();
|
|
3494
|
-
for (const f of
|
|
3589
|
+
for (const f of claimed) {
|
|
3590
|
+
const file = resolveFile(f.file);
|
|
3591
|
+
if (!existsSync7(file)) continue;
|
|
3592
|
+
if (createHash2("sha256").update(readFileSync4(file)).digest("hex") !== f.sha256) continue;
|
|
3495
3593
|
const k = `${f.family}:${f.file}`;
|
|
3496
|
-
const e = byFile.get(k) ?? { family: f.family, weights: [], file
|
|
3594
|
+
const e = byFile.get(k) ?? { family: f.family, weights: [], file };
|
|
3497
3595
|
e.weights.push(f.weight);
|
|
3498
3596
|
byFile.set(k, e);
|
|
3499
3597
|
}
|
|
3500
|
-
const
|
|
3501
|
-
|
|
3502
|
-
|
|
3503
|
-
|
|
3598
|
+
const groups = [...byFile.values()];
|
|
3599
|
+
return { groups, shared: new Set(groups.map((e) => e.file)).size < groups.length };
|
|
3600
|
+
}
|
|
3601
|
+
function fontFaceCss(manifestPath2 = path11.join(fontCacheDir(), "manifest.json")) {
|
|
3602
|
+
const { groups, shared } = injectedGroups(manifestPath2);
|
|
3603
|
+
return groups.map((e) => {
|
|
3604
|
+
const weight = shared || e.weights.length > 1 ? `${SPAN[0]} ${SPAN[1]}` : String(e.weights[0]);
|
|
3504
3605
|
return `@font-face { font-family: '${e.family}'; font-weight: ${weight}; font-style: normal; src: url(data:font/woff2;base64,${readFileSync4(e.file).toString("base64")}) format('woff2'); }`;
|
|
3505
3606
|
}).join("\n");
|
|
3506
3607
|
}
|
|
3608
|
+
function injectedFamilyWeights(manifestPath2 = path11.join(fontCacheDir(), "manifest.json")) {
|
|
3609
|
+
const { groups, shared } = injectedGroups(manifestPath2);
|
|
3610
|
+
const out = /* @__PURE__ */ new Map();
|
|
3611
|
+
for (const g of groups) {
|
|
3612
|
+
const key = g.family.toLowerCase();
|
|
3613
|
+
const e = out.get(key) ?? { weights: /* @__PURE__ */ new Set(), span: false };
|
|
3614
|
+
for (const w of g.weights) e.weights.add(w);
|
|
3615
|
+
if (shared || g.weights.length > 1) e.span = true;
|
|
3616
|
+
out.set(key, e);
|
|
3617
|
+
}
|
|
3618
|
+
return out;
|
|
3619
|
+
}
|
|
3620
|
+
function servesWeight(coverage, weight) {
|
|
3621
|
+
return coverage.weights.has(weight) || coverage.span && weight >= SPAN[0] && weight <= SPAN[1];
|
|
3622
|
+
}
|
|
3623
|
+
var SPAN;
|
|
3507
3624
|
var init_font_faces = __esm({
|
|
3508
3625
|
"packages/verify/src/font-faces.ts"() {
|
|
3509
3626
|
"use strict";
|
|
3510
3627
|
init_font_resolve();
|
|
3628
|
+
SPAN = [100, 700];
|
|
3511
3629
|
}
|
|
3512
3630
|
});
|
|
3513
3631
|
|
|
3514
3632
|
// packages/verify/src/admission.ts
|
|
3515
|
-
import { readFileSync as readFileSync5, readdirSync as readdirSync2, existsSync as
|
|
3516
|
-
import
|
|
3633
|
+
import { readFileSync as readFileSync5, readdirSync as readdirSync2, existsSync as existsSync8, writeFileSync as writeFileSync4 } from "node:fs";
|
|
3634
|
+
import path12 from "node:path";
|
|
3517
3635
|
import { build as build2 } from "esbuild";
|
|
3518
3636
|
import postcss from "postcss";
|
|
3519
3637
|
import tailwindcss from "tailwindcss";
|
|
3520
3638
|
import { chromium as chromium2 } from "playwright-core";
|
|
3521
3639
|
function fontWeightsByFamily() {
|
|
3522
|
-
const mPath =
|
|
3640
|
+
const mPath = path12.join(fontCacheDir(), "manifest.json");
|
|
3523
3641
|
const out = /* @__PURE__ */ new Map();
|
|
3524
|
-
if (!
|
|
3642
|
+
if (!existsSync8(mPath)) return out;
|
|
3525
3643
|
for (const f of JSON.parse(readFileSync5(mPath, "utf8")))
|
|
3526
3644
|
out.set(f.family, [...out.get(f.family) ?? [], f.weight]);
|
|
3527
3645
|
return out;
|
|
@@ -3542,7 +3660,7 @@ var init_admission = __esm({
|
|
|
3542
3660
|
});
|
|
3543
3661
|
|
|
3544
3662
|
// packages/verify/src/tasks.ts
|
|
3545
|
-
import
|
|
3663
|
+
import path13 from "node:path";
|
|
3546
3664
|
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;
|
|
3547
3665
|
var init_tasks = __esm({
|
|
3548
3666
|
"packages/verify/src/tasks.ts"() {
|
|
@@ -3702,7 +3820,7 @@ BEHAVIORAL CONTRACT (machine-verified, gating): the primary action and close con
|
|
|
3702
3820
|
];
|
|
3703
3821
|
TASKS = {
|
|
3704
3822
|
calendar: {
|
|
3705
|
-
set:
|
|
3823
|
+
set: path13.join(REPO_ROOT, "examples/recordings/shadcn-poc-calendar"),
|
|
3706
3824
|
entry: "Calendar.tsx",
|
|
3707
3825
|
configs: CALENDAR_CONFIGS,
|
|
3708
3826
|
systemApi: CALENDAR_API,
|
|
@@ -3710,7 +3828,7 @@ BEHAVIORAL CONTRACT (machine-verified, gating): the primary action and close con
|
|
|
3710
3828
|
prelude: { controls: ['[data-tendril-part="day"]'], textInputs: [] }
|
|
3711
3829
|
},
|
|
3712
3830
|
"shadcn-button": {
|
|
3713
|
-
set:
|
|
3831
|
+
set: path13.join(REPO_ROOT, "examples/recordings/shadcn-poc-button"),
|
|
3714
3832
|
entry: "Button.tsx",
|
|
3715
3833
|
configs: BUTTON_CONFIGS,
|
|
3716
3834
|
systemApi: BUTTON_API,
|
|
@@ -3718,7 +3836,7 @@ BEHAVIORAL CONTRACT (machine-verified, gating): the primary action and close con
|
|
|
3718
3836
|
prelude: { controls: ["> *"], textInputs: [] }
|
|
3719
3837
|
},
|
|
3720
3838
|
combobox: {
|
|
3721
|
-
set:
|
|
3839
|
+
set: path13.join(REPO_ROOT, "examples/recordings/carbon-poc-combobox"),
|
|
3722
3840
|
entry: "ComboBox.tsx",
|
|
3723
3841
|
configs: COMBO_CONFIGS,
|
|
3724
3842
|
systemApi: COMBO_API,
|
|
@@ -3726,7 +3844,7 @@ BEHAVIORAL CONTRACT (machine-verified, gating): the primary action and close con
|
|
|
3726
3844
|
prelude: { controls: ['[role="option"]'], textInputs: ["input"], popover: { selector: '[role="listbox"]', trigger: "input" } }
|
|
3727
3845
|
},
|
|
3728
3846
|
modal: {
|
|
3729
|
-
set:
|
|
3847
|
+
set: path13.join(REPO_ROOT, "examples/recordings/carbon-poc-modal"),
|
|
3730
3848
|
entry: "Modal.tsx",
|
|
3731
3849
|
configs: MODAL_CONFIGS,
|
|
3732
3850
|
systemApi: MODAL_API,
|
|
@@ -3744,8 +3862,8 @@ __export(behavior_exports, {
|
|
|
3744
3862
|
compileMount: () => compileMount,
|
|
3745
3863
|
recordingIsDark: () => recordingIsDark
|
|
3746
3864
|
});
|
|
3747
|
-
import { existsSync as
|
|
3748
|
-
import
|
|
3865
|
+
import { existsSync as existsSync9, readFileSync as readFileSync6 } from "node:fs";
|
|
3866
|
+
import path14 from "node:path";
|
|
3749
3867
|
import { build as build3 } from "esbuild";
|
|
3750
3868
|
import { chromium as chromium3 } from "playwright-core";
|
|
3751
3869
|
import { PNG as PNG2 } from "pngjs";
|
|
@@ -3754,12 +3872,12 @@ function getFontFaces() {
|
|
|
3754
3872
|
return _fontFaces;
|
|
3755
3873
|
}
|
|
3756
3874
|
async function compileMount(task, bundleDir) {
|
|
3757
|
-
const entryTsx =
|
|
3758
|
-
if (!
|
|
3875
|
+
const entryTsx = path14.join(bundleDir, task.entry);
|
|
3876
|
+
if (!existsSync9(entryTsx)) return { error: `${task.entry} missing` };
|
|
3759
3877
|
const mountSrc = `
|
|
3760
3878
|
import { createElement } from "react";
|
|
3761
3879
|
import { createRoot } from "react-dom/client";
|
|
3762
|
-
import * as B from ${JSON.stringify(
|
|
3880
|
+
import * as B from ${JSON.stringify(path14.resolve(entryTsx))};
|
|
3763
3881
|
const cfg = (window as unknown as { __cfg: { component: string; props: Record<string, unknown>; spyProps?: string[] } }).__cfg;
|
|
3764
3882
|
const C = (B as Record<string, unknown>)[cfg.component] as Parameters<typeof createElement>[0] | undefined;
|
|
3765
3883
|
// Callbacks cannot ride the JSON config: specs NAME spy props and the
|
|
@@ -4011,8 +4129,8 @@ async function runSteps(page, spec, renderPose) {
|
|
|
4011
4129
|
function recordingIsDark(task) {
|
|
4012
4130
|
const rep = task.configs[0]?.rep;
|
|
4013
4131
|
if (rep === void 0) return false;
|
|
4014
|
-
const f =
|
|
4015
|
-
if (!
|
|
4132
|
+
const f = path14.join(task.set, rep, "get_screenshot.json");
|
|
4133
|
+
if (!existsSync9(f)) return false;
|
|
4016
4134
|
try {
|
|
4017
4135
|
const env = JSON.parse(readFileSync6(f, "utf8")).content.find((c) => c.type === "image");
|
|
4018
4136
|
if (env?.data === void 0) return false;
|
|
@@ -4086,7 +4204,7 @@ async function checkBehaviors(task, bundleDir, opts = {}) {
|
|
|
4086
4204
|
const deadlineMs = timeoutMs + 1e4;
|
|
4087
4205
|
const js = await compileMount(task, bundleDir);
|
|
4088
4206
|
if (typeof js !== "string") return task.behaviors.map((b) => ({ id: b.id, pass: false, detail: js.error }));
|
|
4089
|
-
const css = ["tokens.css", "styles.css"].map((f) =>
|
|
4207
|
+
const css = ["tokens.css", "styles.css"].map((f) => path14.join(bundleDir, f)).filter((f) => existsSync9(f)).map((f) => readFileSync6(f, "utf8")).join("\n");
|
|
4090
4208
|
const server = await chromium3.launchServer({ executablePath: resolveChrome(), headless: true, args: mountArgs() });
|
|
4091
4209
|
const browser = await chromium3.connect(server.wsEndpoint());
|
|
4092
4210
|
const results = [];
|
|
@@ -4229,8 +4347,8 @@ var init_behavior = __esm({
|
|
|
4229
4347
|
});
|
|
4230
4348
|
|
|
4231
4349
|
// packages/verify/src/bundle-quality.ts
|
|
4232
|
-
import { readFileSync as readFileSync7, rmSync as rmSync2, writeFileSync as writeFileSync5, existsSync as
|
|
4233
|
-
import
|
|
4350
|
+
import { readFileSync as readFileSync7, rmSync as rmSync2, writeFileSync as writeFileSync5, existsSync as existsSync10 } from "node:fs";
|
|
4351
|
+
import path15 from "node:path";
|
|
4234
4352
|
function definedVars(tokensCss) {
|
|
4235
4353
|
if (tokensCss === void 0) return void 0;
|
|
4236
4354
|
const names = /* @__PURE__ */ new Set();
|
|
@@ -4239,7 +4357,7 @@ function definedVars(tokensCss) {
|
|
|
4239
4357
|
}
|
|
4240
4358
|
function recordedTokenMapEmpty(setDir, reps) {
|
|
4241
4359
|
const readMap = (file) => {
|
|
4242
|
-
if (!
|
|
4360
|
+
if (!existsSync10(file)) return void 0;
|
|
4243
4361
|
try {
|
|
4244
4362
|
const parsed = JSON.parse(envelopeFirstTextPart(JSON.parse(readFileSync7(file, "utf8"))) || "{}");
|
|
4245
4363
|
return parsed !== null && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : {};
|
|
@@ -4247,28 +4365,93 @@ function recordedTokenMapEmpty(setDir, reps) {
|
|
|
4247
4365
|
return {};
|
|
4248
4366
|
}
|
|
4249
4367
|
};
|
|
4250
|
-
const setLevel = readMap(
|
|
4368
|
+
const setLevel = readMap(path15.join(setDir, "get_variable_defs.json"));
|
|
4251
4369
|
if (setLevel !== void 0) return Object.keys(setLevel).length === 0;
|
|
4252
4370
|
let recorded = false;
|
|
4253
4371
|
for (const rep of reps) {
|
|
4254
|
-
const m = readMap(
|
|
4372
|
+
const m = readMap(path15.join(setDir, rep, "get_variable_defs.json"));
|
|
4255
4373
|
if (m === void 0) continue;
|
|
4256
4374
|
recorded = true;
|
|
4257
4375
|
if (Object.keys(m).length > 0) return false;
|
|
4258
4376
|
}
|
|
4259
4377
|
return recorded;
|
|
4260
4378
|
}
|
|
4261
|
-
|
|
4379
|
+
function scannable(css) {
|
|
4380
|
+
const blank = (m) => m.replace(/[^\n]/g, " ");
|
|
4381
|
+
return css.replace(/\/\*[^]*?\*\//g, blank).replace(/@font-face\s*{[^}]*}/g, blank);
|
|
4382
|
+
}
|
|
4383
|
+
function usedWeights(css) {
|
|
4384
|
+
const out = /* @__PURE__ */ new Set();
|
|
4385
|
+
for (const m of css.matchAll(/font-weight\s*:\s*([^;}]+)/g)) {
|
|
4386
|
+
const v = declValue(m[1]).toLowerCase();
|
|
4387
|
+
if (v === "bold") out.add(700);
|
|
4388
|
+
else if (v === "normal") out.add(400);
|
|
4389
|
+
else if (/^\d{1,4}$/.test(v)) out.add(Number(v));
|
|
4390
|
+
}
|
|
4391
|
+
return [...out].sort((a, b) => a - b);
|
|
4392
|
+
}
|
|
4393
|
+
function fontStacks(css) {
|
|
4394
|
+
const out = [];
|
|
4395
|
+
for (const m of css.matchAll(/font-family\s*:\s*([^;}]+)/g)) {
|
|
4396
|
+
const text = declValue(m[1]);
|
|
4397
|
+
const families = text.split(",").map((f) => f.trim().replace(/^['"]|['"]$/g, "")).filter((f) => f !== "");
|
|
4398
|
+
if (families.length > 0) out.push({ families, line: css.slice(0, m.index).split("\n").length, text });
|
|
4399
|
+
}
|
|
4400
|
+
return out;
|
|
4401
|
+
}
|
|
4402
|
+
function fontStackFindings(sheets, coverage) {
|
|
4403
|
+
if (coverage.size === 0) return [];
|
|
4404
|
+
const scanned = sheets.map((s) => ({ file: s.file, css: scannable(s.css) }));
|
|
4405
|
+
const weights = [...new Set(scanned.flatMap((s) => usedWeights(s.css)))].sort((a, b) => a - b);
|
|
4406
|
+
if (weights.length === 0) return [];
|
|
4407
|
+
const findings = [];
|
|
4408
|
+
const seen = /* @__PURE__ */ new Set();
|
|
4409
|
+
for (const sheet of scanned) {
|
|
4410
|
+
for (const stack of fontStacks(sheet.css)) {
|
|
4411
|
+
if (seen.has(stack.text)) continue;
|
|
4412
|
+
seen.add(stack.text);
|
|
4413
|
+
const family = stack.families[0];
|
|
4414
|
+
const bound = coverage.get(family.toLowerCase());
|
|
4415
|
+
if (bound === void 0) continue;
|
|
4416
|
+
const unserved = weights.filter((w) => !servesWeight(bound, w));
|
|
4417
|
+
if (unserved.length === 0) continue;
|
|
4418
|
+
const rescue = stack.families.slice(1).find((f) => {
|
|
4419
|
+
const c = coverage.get(f.toLowerCase());
|
|
4420
|
+
return c !== void 0 && unserved.every((w) => servesWeight(c, w));
|
|
4421
|
+
});
|
|
4422
|
+
const has = bound.span ? "100\u2013700" : [...bound.weights].sort((a, b) => a - b).join(", ");
|
|
4423
|
+
const list = unserved.join(", ");
|
|
4424
|
+
const renders = !bound.span && bound.weights.size === 1 ? `renders in the ${[...bound.weights][0]} face` : "renders in whichever of those faces is nearest";
|
|
4425
|
+
findings.push({
|
|
4426
|
+
kind: "font-stack",
|
|
4427
|
+
file: sheet.file,
|
|
4428
|
+
line: stack.line,
|
|
4429
|
+
message: `font-family ${stack.text} binds to '${family}', which the font kit provides at ${has} \u2014 CSS matches the FAMILY first and picks a weight only inside it (a later family is never consulted for a missing weight, and the prelude's font-synthesis: none rules out faux-bold), so text at weight ${list} ${renders}, silently. ` + (rescue !== void 0 ? `'${rescue}' in this same stack provides ${list}: list it first.` : `No family in this stack provides ${list} \u2014 if the recording shows ${unserved.length === 1 ? "that weight" : "those weights"}, run \`tendril fonts resolve "${family}" --weights ${unserved.join(" ")}\` and re-score.`) + " ADVISORY: quality feedback only \u2014 it never moves a score, a tier, or the verdict."
|
|
4430
|
+
});
|
|
4431
|
+
}
|
|
4432
|
+
}
|
|
4433
|
+
return findings;
|
|
4434
|
+
}
|
|
4435
|
+
async function checkBundleQuality(bundleDir, entry, set, fontManifest) {
|
|
4262
4436
|
const findings = [];
|
|
4263
|
-
const entryPath =
|
|
4264
|
-
const cssPath =
|
|
4265
|
-
const tokensPath =
|
|
4266
|
-
const css =
|
|
4267
|
-
const tokensCss =
|
|
4268
|
-
|
|
4437
|
+
const entryPath = path15.join(bundleDir, entry);
|
|
4438
|
+
const cssPath = path15.join(bundleDir, "styles.css");
|
|
4439
|
+
const tokensPath = path15.join(bundleDir, "tokens.css");
|
|
4440
|
+
const css = existsSync10(cssPath) ? readFileSync7(cssPath, "utf8") : "";
|
|
4441
|
+
const tokensCss = existsSync10(tokensPath) ? readFileSync7(tokensPath, "utf8") : void 0;
|
|
4442
|
+
findings.push(
|
|
4443
|
+
...fontStackFindings(
|
|
4444
|
+
[
|
|
4445
|
+
{ file: "styles.css", css },
|
|
4446
|
+
...tokensCss === void 0 ? [] : [{ file: "tokens.css", css: tokensCss }]
|
|
4447
|
+
],
|
|
4448
|
+
injectedFamilyWeights(fontManifest)
|
|
4449
|
+
)
|
|
4450
|
+
);
|
|
4451
|
+
if (existsSync10(entryPath)) {
|
|
4269
4452
|
const workDir = newScratchDir("quality");
|
|
4270
4453
|
try {
|
|
4271
|
-
const tsxPath =
|
|
4454
|
+
const tsxPath = path15.join(workDir, entry);
|
|
4272
4455
|
writeFileSync5(tsxPath, readFileSync7(entryPath, "utf8"));
|
|
4273
4456
|
for (const d of runTscStrict([tsxPath]).diagnostics) {
|
|
4274
4457
|
findings.push({ kind: "tsc", file: entry, ...d.line === void 0 ? {} : { line: d.line }, message: `TS${d.code}: ${d.message}` });
|
|
@@ -4285,13 +4468,16 @@ async function checkBundleQuality(bundleDir, entry, set) {
|
|
|
4285
4468
|
}
|
|
4286
4469
|
return { findings, tokensAbsent: tokensCss === void 0 && !/var\(\s*--/.test(css) };
|
|
4287
4470
|
}
|
|
4471
|
+
var declValue;
|
|
4288
4472
|
var init_bundle_quality = __esm({
|
|
4289
4473
|
"packages/verify/src/bundle-quality.ts"() {
|
|
4290
4474
|
"use strict";
|
|
4291
4475
|
init_src();
|
|
4476
|
+
init_font_faces();
|
|
4292
4477
|
init_runtime();
|
|
4293
4478
|
init_tsc_check();
|
|
4294
4479
|
init_token_lint();
|
|
4480
|
+
declValue = (raw) => raw.trim().replace(/\s*!important$/i, "").trim();
|
|
4295
4481
|
}
|
|
4296
4482
|
});
|
|
4297
4483
|
|
|
@@ -4334,8 +4520,8 @@ var init_effect_geometry = __esm({
|
|
|
4334
4520
|
});
|
|
4335
4521
|
|
|
4336
4522
|
// packages/verify/src/bundle-score.ts
|
|
4337
|
-
import { existsSync as
|
|
4338
|
-
import
|
|
4523
|
+
import { existsSync as existsSync11, mkdirSync as mkdirSync3, readFileSync as readFileSync8, writeFileSync as writeFileSync6 } from "node:fs";
|
|
4524
|
+
import path16 from "node:path";
|
|
4339
4525
|
import { build as build4 } from "esbuild";
|
|
4340
4526
|
import { chromium as chromium4 } from "playwright-core";
|
|
4341
4527
|
function getFontFaces2() {
|
|
@@ -4389,7 +4575,7 @@ function chooseSeat(cropAt, ref, padW, padH, backdrop, origin, extents) {
|
|
|
4389
4575
|
}
|
|
4390
4576
|
function metadataRoot(set, rep) {
|
|
4391
4577
|
try {
|
|
4392
|
-
const text = JSON.parse(readFileSync8(
|
|
4578
|
+
const text = JSON.parse(readFileSync8(path16.join(set, rep, "get_metadata.json"), "utf8")).content.map((c) => c.text ?? "").join("\n");
|
|
4393
4579
|
return parseMetadataStructure(text);
|
|
4394
4580
|
} catch {
|
|
4395
4581
|
return void 0;
|
|
@@ -4444,17 +4630,17 @@ function smallSemanticNodes(set, rep, maxArea = 1024) {
|
|
|
4444
4630
|
});
|
|
4445
4631
|
}
|
|
4446
4632
|
function repMeta(set, rep) {
|
|
4447
|
-
const text = JSON.parse(readFileSync8(
|
|
4633
|
+
const text = JSON.parse(readFileSync8(path16.join(set, rep, "get_metadata.json"), "utf8")).content.map((c) => c.text ?? "").join("\n");
|
|
4448
4634
|
const root = parseMetadataStructure(text);
|
|
4449
4635
|
return { w: Math.round(root.width ?? 100), h: Math.round(root.height ?? 40) };
|
|
4450
4636
|
}
|
|
4451
4637
|
function repRef(set, rep) {
|
|
4452
|
-
const env = JSON.parse(readFileSync8(
|
|
4638
|
+
const env = JSON.parse(readFileSync8(path16.join(set, rep, "get_screenshot.json"), "utf8")).content.find((c) => c.type === "image");
|
|
4453
4639
|
return Uint8Array.from(Buffer.from(env?.data ?? "", "base64"));
|
|
4454
4640
|
}
|
|
4455
4641
|
function repEffectExtents(set, rep) {
|
|
4456
|
-
const file =
|
|
4457
|
-
if (!
|
|
4642
|
+
const file = path16.join(set, rep, "get_design_context.json");
|
|
4643
|
+
if (!existsSync11(file)) return void 0;
|
|
4458
4644
|
try {
|
|
4459
4645
|
const text = JSON.parse(readFileSync8(file, "utf8")).content.map((c) => c.text ?? "").join("\n");
|
|
4460
4646
|
const extents = shadowExtents(text);
|
|
@@ -4467,13 +4653,13 @@ async function scoreBundleForTask(task, bundleDir, bar = BAR, opts = {}) {
|
|
|
4467
4653
|
if (opts.evidenceDir !== void 0) mkdirSync3(opts.evidenceDir, { recursive: true });
|
|
4468
4654
|
const CONFIGS2 = task.configs;
|
|
4469
4655
|
const timeoutMs = opts.mountDeadlineMs ?? ENGINE_MOUNT_DEADLINE_MS;
|
|
4470
|
-
const entryTsx =
|
|
4471
|
-
if (!
|
|
4472
|
-
const css = ["tokens.css", "styles.css"].map((f) =>
|
|
4656
|
+
const entryTsx = path16.join(bundleDir, task.entry);
|
|
4657
|
+
if (!existsSync11(entryTsx)) return CONFIGS2.map((c) => ({ rep: c.rep, similarity: 0, inkRecall: 0, exact: { similarity: 0, inkRecall: 0 }, pass: false, error: `${task.entry} missing` }));
|
|
4658
|
+
const css = ["tokens.css", "styles.css"].map((f) => path16.join(bundleDir, f)).filter((f) => existsSync11(f)).map((f) => readFileSync8(f, "utf8")).join("\n");
|
|
4473
4659
|
const mountSrc = `
|
|
4474
4660
|
import { createElement } from "react";
|
|
4475
4661
|
import { createRoot } from "react-dom/client";
|
|
4476
|
-
import * as B from ${JSON.stringify(
|
|
4662
|
+
import * as B from ${JSON.stringify(path16.resolve(entryTsx))};
|
|
4477
4663
|
const cfg = (window as unknown as { __cfg: { component: string; props: Record<string, unknown> } }).__cfg;
|
|
4478
4664
|
const C = (B as Record<string, unknown>)[cfg.component] as Parameters<typeof createElement>[0] | undefined;
|
|
4479
4665
|
const root = document.getElementById("root");
|
|
@@ -4568,12 +4754,12 @@ body{margin:0;background:rgb(${backdrop.join(",")})}
|
|
|
4568
4754
|
return name === void 0 ? c : { ...c, name };
|
|
4569
4755
|
});
|
|
4570
4756
|
if (opts.evidenceDir !== void 0) {
|
|
4571
|
-
writeFileSync6(
|
|
4572
|
-
writeFileSync6(
|
|
4573
|
-
writeFileSync6(
|
|
4757
|
+
writeFileSync6(path16.join(opts.evidenceDir, `${cfg.rep}-render.png`), scored);
|
|
4758
|
+
writeFileSync6(path16.join(opts.evidenceDir, `${cfg.rep}-ref.png`), ref);
|
|
4759
|
+
writeFileSync6(path16.join(opts.evidenceDir, `${cfg.rep}-diff.png`), diffPng(scored, ref));
|
|
4574
4760
|
for (const [i, c] of absent.slice(0, 3).entries()) {
|
|
4575
|
-
writeFileSync6(
|
|
4576
|
-
writeFileSync6(
|
|
4761
|
+
writeFileSync6(path16.join(opts.evidenceDir, `${cfg.rep}-absent-${i}-ref.png`), zoomCrop(ref, c));
|
|
4762
|
+
writeFileSync6(path16.join(opts.evidenceDir, `${cfg.rep}-absent-${i}-render.png`), zoomCrop(scored, c));
|
|
4577
4763
|
}
|
|
4578
4764
|
}
|
|
4579
4765
|
return {
|
|
@@ -4672,7 +4858,7 @@ var init_prelude = __esm({
|
|
|
4672
4858
|
"use strict";
|
|
4673
4859
|
PRELUDE_CONTRACT = `BUNDLE PRELUDE (machine-verified on computed styles \u2014 the harness page provides NONE of this; your CSS must):
|
|
4674
4860
|
- Containment reset on the component subtree: margin 0, box-sizing border-box, line-height/letter-spacing/font-family inherited from the root (host globals must not leak in).
|
|
4675
|
-
- Component root: -webkit-font-smoothing: antialiased and -moz-osx-font-smoothing: grayscale (recorded rasterization); font-synthesis: none (
|
|
4861
|
+
- Component root: -webkit-font-smoothing: antialiased and -moz-osx-font-smoothing: grayscale (recorded rasterization); font-synthesis: none (no faux bold or italic \u2014 text renders in the face your stack actually binds, so a weight the kit cannot serve reaches the score instead of being faked; it does NOT fall through to the next family in the stack); color-scheme MATCHING YOUR RECORDING (light for a light capture, dark for a dark one) and direction: ltr \u2014 pin them, so the render cannot follow the viewer OS preference and drift from the capture it is graded against; isolation: isolate (own stacking context; overlay z-indexes never fight the host).
|
|
4676
4862
|
- Interactive controls (buttons, options): touch-action: manipulation and user-select: none. Text inputs stay selectable (never user-select: none on them).
|
|
4677
4863
|
- Scrollable popovers/menus: overscroll-behavior: contain.
|
|
4678
4864
|
- Focus indicators bind to :focus-visible, never bare :focus. If the recording contains a focus pose, style the indicator from that recorded truth. If NO focus pose is recorded, do NOT invent ring colors/widths/offsets \u2014 an invented ring is unrecorded pixels; keep the browser's default indicator for keyboard focus and state the gap in your report. Text inputs match :focus-visible even on mouse click BY SPEC; the ONE permitted refinement is suppressing the indicator on a POSITIVELY OBSERVED pointer press, fail-safe toward showing it (programmatic focus, restored focus, and assistive tech all count as keyboard and keep the ring \u2014 WCAG 2.4.7 binds keyboard operation). Never suppress more broadly than an observed press, and never restyle what you kept.
|
|
@@ -4681,8 +4867,8 @@ var init_prelude = __esm({
|
|
|
4681
4867
|
});
|
|
4682
4868
|
|
|
4683
4869
|
// packages/verify/src/parity.ts
|
|
4684
|
-
import { existsSync as
|
|
4685
|
-
import
|
|
4870
|
+
import { existsSync as existsSync12, readFileSync as readFileSync9 } from "node:fs";
|
|
4871
|
+
import path17 from "node:path";
|
|
4686
4872
|
import { chromium as chromium6 } from "playwright-core";
|
|
4687
4873
|
function getFontFaces3() {
|
|
4688
4874
|
_fontFaces3 ??= fontFaceCss();
|
|
@@ -4711,7 +4897,7 @@ async function checkHoverParity(task, bundleDir, opts = {}) {
|
|
|
4711
4897
|
const deadlineMs = timeoutMs + 1e4;
|
|
4712
4898
|
const js = await compileMount(task, bundleDir);
|
|
4713
4899
|
if (typeof js !== "string") return configs.map((c) => ({ id: `parity:${c.rep}`, pass: false, detail: js.error }));
|
|
4714
|
-
const css = ["tokens.css", "styles.css"].map((f) =>
|
|
4900
|
+
const css = ["tokens.css", "styles.css"].map((f) => path17.join(bundleDir, f)).filter((f) => existsSync12(f)).map((f) => readFileSync9(f, "utf8")).join("\n");
|
|
4715
4901
|
const server = await chromium6.launchServer({ executablePath: resolveChrome(), headless: true, args: mountArgs() });
|
|
4716
4902
|
const browser = await chromium6.connect(server.wsEndpoint());
|
|
4717
4903
|
const results = [];
|
|
@@ -4791,8 +4977,8 @@ var init_parity = __esm({
|
|
|
4791
4977
|
|
|
4792
4978
|
// packages/verify/src/composition.ts
|
|
4793
4979
|
import { createRequire as createRequire2 } from "node:module";
|
|
4794
|
-
import { existsSync as
|
|
4795
|
-
import
|
|
4980
|
+
import { existsSync as existsSync13, readFileSync as readFileSync10 } from "node:fs";
|
|
4981
|
+
import path18 from "node:path";
|
|
4796
4982
|
import { build as build6 } from "esbuild";
|
|
4797
4983
|
import { chromium as chromium7 } from "playwright-core";
|
|
4798
4984
|
function getFontFaces4() {
|
|
@@ -4800,9 +4986,9 @@ function getFontFaces4() {
|
|
|
4800
4986
|
return _fontFaces4;
|
|
4801
4987
|
}
|
|
4802
4988
|
async function compileInstrumentedMount(task, bundleDir) {
|
|
4803
|
-
const entryTsx =
|
|
4804
|
-
if (!
|
|
4805
|
-
const requireFromVerify = createRequire2(
|
|
4989
|
+
const entryTsx = path18.join(bundleDir, task.entry);
|
|
4990
|
+
if (!existsSync13(entryTsx)) return { error: `${task.entry} missing` };
|
|
4991
|
+
const requireFromVerify = createRequire2(path18.join(VERIFY_PKG_DIR, "package.json"));
|
|
4806
4992
|
let realJsxPath;
|
|
4807
4993
|
try {
|
|
4808
4994
|
realJsxPath = requireFromVerify.resolve("react/jsx-runtime");
|
|
@@ -4813,7 +4999,7 @@ async function compileInstrumentedMount(task, bundleDir) {
|
|
|
4813
4999
|
import { createElement } from "react";
|
|
4814
5000
|
import { createRoot } from "react-dom/client";
|
|
4815
5001
|
import { __registerParts } from "react/jsx-runtime";
|
|
4816
|
-
import * as B from ${JSON.stringify(
|
|
5002
|
+
import * as B from ${JSON.stringify(path18.resolve(entryTsx))};
|
|
4817
5003
|
const cfg = (window as unknown as { __cfg: { component: string; props: Record<string, unknown>; partComponents: string[] } }).__cfg;
|
|
4818
5004
|
const pairs: Array<[unknown, string]> = [];
|
|
4819
5005
|
for (const name of cfg.partComponents) {
|
|
@@ -4868,7 +5054,7 @@ function expectedParts(task, roles, mainSlug) {
|
|
|
4868
5054
|
}
|
|
4869
5055
|
function interiorRegions(setDir, roles) {
|
|
4870
5056
|
const mains = roles.main;
|
|
4871
|
-
const withInterior = mains.filter((m) =>
|
|
5057
|
+
const withInterior = mains.filter((m) => existsSync13(path18.join(setDir, m, "get_metadata_interior.json")));
|
|
4872
5058
|
if (withInterior.length === 0) {
|
|
4873
5059
|
return { unavailable: "no interior geometry recorded for any main (pre-obligation set) \u2014 re-record with `tendril record` to enable crop checks" };
|
|
4874
5060
|
}
|
|
@@ -4885,7 +5071,7 @@ async function checkCropComposition(task, bundleDir, regions, opts = {}) {
|
|
|
4885
5071
|
if (typeof js !== "string") {
|
|
4886
5072
|
return regions.map((r) => ({ id: cropId(r), pass: false, similarity: 0, inkRecall: 0, detail: js.error }));
|
|
4887
5073
|
}
|
|
4888
|
-
const css = ["tokens.css", "styles.css"].map((f) =>
|
|
5074
|
+
const css = ["tokens.css", "styles.css"].map((f) => path18.join(bundleDir, f)).filter((f) => existsSync13(f)).map((f) => readFileSync10(f, "utf8")).join("\n");
|
|
4889
5075
|
const server = await chromium7.launchServer({ executablePath: resolveChrome(), headless: true, args: mountArgs() });
|
|
4890
5076
|
const browser = await chromium7.connect(server.wsEndpoint());
|
|
4891
5077
|
const PAD = 4;
|
|
@@ -4990,7 +5176,7 @@ async function checkStructuralComposition(task, bundleDir, roles, opts = {}) {
|
|
|
4990
5176
|
const deadlineMs = timeoutMs + 1e4;
|
|
4991
5177
|
const js = await compileInstrumentedMount(task, bundleDir);
|
|
4992
5178
|
if (typeof js !== "string") return [...results, ...mains.map((m) => ({ id: `composition:${m}`, pass: false, detail: js.error }))];
|
|
4993
|
-
const css = ["tokens.css", "styles.css"].map((f) =>
|
|
5179
|
+
const css = ["tokens.css", "styles.css"].map((f) => path18.join(bundleDir, f)).filter((f) => existsSync13(f)).map((f) => readFileSync10(f, "utf8")).join("\n");
|
|
4994
5180
|
const server = await chromium7.launchServer({ executablePath: resolveChrome(), headless: true, args: mountArgs() });
|
|
4995
5181
|
const browser = await chromium7.connect(server.wsEndpoint());
|
|
4996
5182
|
try {
|
|
@@ -5080,17 +5266,17 @@ export const jsxs = (t, p, k) => real.jsxs(t, inject(t, p), k);
|
|
|
5080
5266
|
});
|
|
5081
5267
|
|
|
5082
5268
|
// packages/verify/src/occlusion.ts
|
|
5083
|
-
import { existsSync as
|
|
5084
|
-
import
|
|
5269
|
+
import { existsSync as existsSync14 } from "node:fs";
|
|
5270
|
+
import path19 from "node:path";
|
|
5085
5271
|
import { build as build7 } from "esbuild";
|
|
5086
5272
|
import { chromium as chromium8 } from "playwright-core";
|
|
5087
5273
|
async function compileTwoUp(task, bundleDir) {
|
|
5088
|
-
const entryTsx =
|
|
5089
|
-
if (!
|
|
5274
|
+
const entryTsx = path19.join(bundleDir, task.entry);
|
|
5275
|
+
if (!existsSync14(entryTsx)) return { error: `${task.entry} missing` };
|
|
5090
5276
|
const src = `
|
|
5091
5277
|
import { createElement } from "react";
|
|
5092
5278
|
import { createRoot } from "react-dom/client";
|
|
5093
|
-
import * as B from ${JSON.stringify(
|
|
5279
|
+
import * as B from ${JSON.stringify(path19.resolve(entryTsx))};
|
|
5094
5280
|
const cfg = (window as unknown as { __cfg: { component: string; props: Record<string, unknown> } }).__cfg;
|
|
5095
5281
|
const C = (B as Record<string, unknown>)[cfg.component] as Parameters<typeof createElement>[0] | undefined;
|
|
5096
5282
|
for (const id of ["first", "second"]) {
|
|
@@ -5268,21 +5454,21 @@ var init_src4 = __esm({
|
|
|
5268
5454
|
});
|
|
5269
5455
|
|
|
5270
5456
|
// packages/cli/src/environment.ts
|
|
5271
|
-
import { existsSync as
|
|
5272
|
-
import
|
|
5457
|
+
import { existsSync as existsSync15, readFileSync as readFileSync11 } from "node:fs";
|
|
5458
|
+
import path20 from "node:path";
|
|
5273
5459
|
import { createHash as createHash3 } from "node:crypto";
|
|
5274
|
-
import { fileURLToPath as
|
|
5460
|
+
import { fileURLToPath as fileURLToPath5 } from "node:url";
|
|
5275
5461
|
function cliVersion() {
|
|
5276
5462
|
try {
|
|
5277
|
-
return JSON.parse(readFileSync11(
|
|
5463
|
+
return JSON.parse(readFileSync11(path20.join(path20.dirname(fileURLToPath5(import.meta.url)), "..", "package.json"), "utf8")).version ?? "dev";
|
|
5278
5464
|
} catch {
|
|
5279
5465
|
return "dev";
|
|
5280
5466
|
}
|
|
5281
5467
|
}
|
|
5282
5468
|
function environmentStamp(taskFamilies) {
|
|
5283
|
-
const manifestPath2 =
|
|
5469
|
+
const manifestPath2 = path20.join(fontCacheDir(), "manifest.json");
|
|
5284
5470
|
let fontsHash = null;
|
|
5285
|
-
if (
|
|
5471
|
+
if (existsSync15(manifestPath2)) {
|
|
5286
5472
|
try {
|
|
5287
5473
|
const entries = JSON.parse(readFileSync11(manifestPath2, "utf8"));
|
|
5288
5474
|
const scoped = taskFamilies === void 0 || taskFamilies === null ? entries : entries.filter((f) => taskFamilies.some((fam) => fam.toLowerCase() === f.family.toLowerCase()));
|
|
@@ -5298,8 +5484,8 @@ function environmentStamp(taskFamilies) {
|
|
|
5298
5484
|
} catch {
|
|
5299
5485
|
}
|
|
5300
5486
|
const ver = chrome === "unavailable" ? null : chromeVersion();
|
|
5301
|
-
const
|
|
5302
|
-
return { chrome, chromeVersion: ver === null ? null : ver +
|
|
5487
|
+
const lcdSuffix = lcdTextEnabled() ? " (lcd-text-enabled)" : "";
|
|
5488
|
+
return { chrome, chromeVersion: ver === null ? null : ver + lcdSuffix, fontsManifestSha256: fontsHash };
|
|
5303
5489
|
}
|
|
5304
5490
|
var init_environment = __esm({
|
|
5305
5491
|
"packages/cli/src/environment.ts"() {
|
|
@@ -5328,8 +5514,8 @@ var init_describe = __esm({
|
|
|
5328
5514
|
});
|
|
5329
5515
|
|
|
5330
5516
|
// packages/cli/src/env.ts
|
|
5331
|
-
import { existsSync as
|
|
5332
|
-
import
|
|
5517
|
+
import { existsSync as existsSync16, readFileSync as readFileSync12 } from "node:fs";
|
|
5518
|
+
import path21 from "node:path";
|
|
5333
5519
|
function parseEnv(content) {
|
|
5334
5520
|
const entries = /* @__PURE__ */ new Map();
|
|
5335
5521
|
for (const line of content.split("\n")) {
|
|
@@ -5341,8 +5527,8 @@ function parseEnv(content) {
|
|
|
5341
5527
|
function resolveCredential(name) {
|
|
5342
5528
|
const fromProcess = process.env[name];
|
|
5343
5529
|
if (fromProcess) return fromProcess;
|
|
5344
|
-
const envPath =
|
|
5345
|
-
if (!
|
|
5530
|
+
const envPath = path21.resolve(process.env["INIT_CWD"] ?? process.cwd(), ".env");
|
|
5531
|
+
if (!existsSync16(envPath)) return void 0;
|
|
5346
5532
|
return parseEnv(readFileSync12(envPath, "utf8")).get(name);
|
|
5347
5533
|
}
|
|
5348
5534
|
var init_env = __esm({
|
|
@@ -5403,15 +5589,15 @@ var init_output = __esm({
|
|
|
5403
5589
|
});
|
|
5404
5590
|
|
|
5405
5591
|
// packages/cli/src/entitlement.ts
|
|
5406
|
-
import { chmodSync, existsSync as
|
|
5592
|
+
import { chmodSync, existsSync as existsSync17, mkdirSync as mkdirSync4, readFileSync as readFileSync13, writeFileSync as writeFileSync7 } from "node:fs";
|
|
5407
5593
|
import crypto from "node:crypto";
|
|
5408
5594
|
import os3 from "node:os";
|
|
5409
|
-
import
|
|
5595
|
+
import path22 from "node:path";
|
|
5410
5596
|
function entitlementPath() {
|
|
5411
|
-
return process.env["TENDRIL_ENTITLEMENT_PATH"] ??
|
|
5597
|
+
return process.env["TENDRIL_ENTITLEMENT_PATH"] ?? path22.join(os3.homedir(), ".tendril", "entitlement.json");
|
|
5412
5598
|
}
|
|
5413
5599
|
function readStoredEntitlement(file = entitlementPath()) {
|
|
5414
|
-
if (!
|
|
5600
|
+
if (!existsSync17(file)) return void 0;
|
|
5415
5601
|
try {
|
|
5416
5602
|
const parsed = JSON.parse(readFileSync13(file, "utf8"));
|
|
5417
5603
|
if (typeof parsed.token !== "string" || typeof parsed.lastRefreshAt !== "number") return void 0;
|
|
@@ -5421,7 +5607,7 @@ function readStoredEntitlement(file = entitlementPath()) {
|
|
|
5421
5607
|
}
|
|
5422
5608
|
}
|
|
5423
5609
|
function writeStoredEntitlement(stored, file = entitlementPath()) {
|
|
5424
|
-
mkdirSync4(
|
|
5610
|
+
mkdirSync4(path22.dirname(file), { recursive: true });
|
|
5425
5611
|
writeFileSync7(file, `${JSON.stringify(stored, null, 2)}
|
|
5426
5612
|
`);
|
|
5427
5613
|
chmodSync(file, 384);
|
|
@@ -5447,11 +5633,11 @@ function checkEntitlement(opts = {}) {
|
|
|
5447
5633
|
const now = opts.now ?? Date.now();
|
|
5448
5634
|
const stored = "stored" in opts ? opts.stored : readStoredEntitlement();
|
|
5449
5635
|
if (stored === void 0) {
|
|
5450
|
-
return { ok: false, code: "entitlement-required", error: "no entitlement on this machine \u2014 record and generate need an active Tendril plan (verify stays free, always)", remediation:
|
|
5636
|
+
return { ok: false, code: "entitlement-required", error: "no entitlement on this machine \u2014 record and generate need an active Tendril plan (verify stays free, always)", remediation: activateRemediation() };
|
|
5451
5637
|
}
|
|
5452
5638
|
const parsed = parseEntitlementToken(stored.token);
|
|
5453
5639
|
if ("error" in parsed) {
|
|
5454
|
-
return { ok: false, code: "entitlement-invalid", error: `stored entitlement is unreadable: ${parsed.error}`, remediation:
|
|
5640
|
+
return { ok: false, code: "entitlement-invalid", error: `stored entitlement is unreadable: ${parsed.error}`, remediation: activateRemediation() };
|
|
5455
5641
|
}
|
|
5456
5642
|
const pem = keys[parsed.claims.kid];
|
|
5457
5643
|
const valid = pem !== void 0 && (() => {
|
|
@@ -5462,14 +5648,14 @@ function checkEntitlement(opts = {}) {
|
|
|
5462
5648
|
}
|
|
5463
5649
|
})();
|
|
5464
5650
|
if (!valid) {
|
|
5465
|
-
return { ok: false, code: "entitlement-invalid", error: "stored entitlement failed signature verification", remediation:
|
|
5651
|
+
return { ok: false, code: "entitlement-invalid", error: "stored entitlement failed signature verification", remediation: activateRemediation() };
|
|
5466
5652
|
}
|
|
5467
5653
|
if (now > parsed.claims.exp + TOLERANCE_MS) {
|
|
5468
5654
|
return {
|
|
5469
5655
|
ok: false,
|
|
5470
5656
|
code: "entitlement-expired",
|
|
5471
5657
|
error: "entitlement expired (and the offline tolerance window has passed)",
|
|
5472
|
-
remediation: `Reconnect and ${
|
|
5658
|
+
remediation: `Reconnect and ${activateRemediation()}`
|
|
5473
5659
|
};
|
|
5474
5660
|
}
|
|
5475
5661
|
if (now < stored.lastRefreshAt - CLOCK_ROLLBACK_MS) {
|
|
@@ -5477,7 +5663,7 @@ function checkEntitlement(opts = {}) {
|
|
|
5477
5663
|
ok: false,
|
|
5478
5664
|
code: "entitlement-clock",
|
|
5479
5665
|
error: "system clock sits more than a day before the last entitlement refresh",
|
|
5480
|
-
remediation: `Fix the system clock, then ${
|
|
5666
|
+
remediation: `Fix the system clock, then ${activateRemediation()}`
|
|
5481
5667
|
};
|
|
5482
5668
|
}
|
|
5483
5669
|
return { ok: true, mode: "active", claims: parsed.claims, stale: now > parsed.claims.exp };
|
|
@@ -5488,26 +5674,27 @@ function requireEntitlement(flags) {
|
|
|
5488
5674
|
fail(flags, ExitCode.Auth, { error: status.error, code: status.code, remediation: status.remediation });
|
|
5489
5675
|
}
|
|
5490
5676
|
}
|
|
5491
|
-
var ENT_PREFIX, PUBLIC_KEYS, TOLERANCE_MS, CLOCK_ROLLBACK_MS, b64urlDecode,
|
|
5677
|
+
var ENT_PREFIX, PUBLIC_KEYS, TOLERANCE_MS, CLOCK_ROLLBACK_MS, b64urlDecode, activateRemediation;
|
|
5492
5678
|
var init_entitlement = __esm({
|
|
5493
5679
|
"packages/cli/src/entitlement.ts"() {
|
|
5494
5680
|
"use strict";
|
|
5495
5681
|
init_src3();
|
|
5682
|
+
init_invocation();
|
|
5496
5683
|
init_output();
|
|
5497
5684
|
ENT_PREFIX = "tendril-ent.v1.";
|
|
5498
5685
|
PUBLIC_KEYS = {};
|
|
5499
5686
|
TOLERANCE_MS = 24 * 60 * 60 * 1e3;
|
|
5500
5687
|
CLOCK_ROLLBACK_MS = 24 * 60 * 60 * 1e3;
|
|
5501
5688
|
b64urlDecode = (s) => Buffer.from(s, "base64url");
|
|
5502
|
-
|
|
5689
|
+
activateRemediation = () => `Run \`${tendrilCommand("activate")}\` in your terminal (a browser approval \u2014 never paste license material into an agent chat).`;
|
|
5503
5690
|
}
|
|
5504
5691
|
});
|
|
5505
5692
|
|
|
5506
5693
|
// packages/cli/src/commands/doctor.ts
|
|
5507
5694
|
import { spawnSync } from "node:child_process";
|
|
5508
|
-
import { existsSync as
|
|
5695
|
+
import { existsSync as existsSync18, readFileSync as readFileSync14, readdirSync as readdirSync3 } from "node:fs";
|
|
5509
5696
|
import os4 from "node:os";
|
|
5510
|
-
import
|
|
5697
|
+
import path23 from "node:path";
|
|
5511
5698
|
async function runDoctorChecks(options) {
|
|
5512
5699
|
const checks = [];
|
|
5513
5700
|
const mcpUrl = options.mcpUrl ?? DEFAULT_MCP_URL;
|
|
@@ -5537,12 +5724,12 @@ async function runDoctorChecks(options) {
|
|
|
5537
5724
|
name: "openrouter-key",
|
|
5538
5725
|
ok: false,
|
|
5539
5726
|
detail: "OPENROUTER_API_KEY not set \u2014 optional; only the curated API-model engine needs it",
|
|
5540
|
-
remediation:
|
|
5727
|
+
remediation: `Run \`${tendrilCommand("init")}\` to store your OpenRouter key (BYOK) if you plan to use the curated engine.`
|
|
5541
5728
|
}
|
|
5542
5729
|
);
|
|
5543
5730
|
try {
|
|
5544
5731
|
const chrome = resolveChrome();
|
|
5545
|
-
checks.push({ name: "browser", ok: true, detail: `${chromeVersion() ?? "version unknown"}${
|
|
5732
|
+
checks.push({ name: "browser", ok: true, detail: `${chromeVersion() ?? "version unknown"}${lcdTextEnabled() ? " \u2014 EXPERIMENT override active (lcd-text-enabled): scores not comparable to canonical greyscale runs" : ""} at ${chrome}` });
|
|
5546
5733
|
} catch (err) {
|
|
5547
5734
|
checks.push({
|
|
5548
5735
|
name: "browser",
|
|
@@ -5551,17 +5738,17 @@ async function runDoctorChecks(options) {
|
|
|
5551
5738
|
remediation: "Install Google Chrome (or Chromium), or set CHROME_PATH to a Chromium-family browser binary."
|
|
5552
5739
|
});
|
|
5553
5740
|
}
|
|
5554
|
-
const fontManifest =
|
|
5741
|
+
const fontManifest = path23.join(fontCacheDir(), "manifest.json");
|
|
5555
5742
|
checks.push(
|
|
5556
|
-
|
|
5743
|
+
existsSync18(fontManifest) ? { name: "font-cache", ok: true, detail: `resolved font cache at ${fontCacheDir()} (${JSON.parse(readFileSync14(fontManifest, "utf8")).length} faces)` } : {
|
|
5557
5744
|
name: "font-cache",
|
|
5558
5745
|
ok: true,
|
|
5559
5746
|
detail: `font cache empty at ${fontCacheDir()} \u2014 normal on a fresh machine; faces resolve per design system at first use`,
|
|
5560
|
-
remediation:
|
|
5747
|
+
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.`
|
|
5561
5748
|
}
|
|
5562
5749
|
);
|
|
5563
|
-
const pluginRoot =
|
|
5564
|
-
if (
|
|
5750
|
+
const pluginRoot = path23.join(os4.homedir(), ".claude", "plugins", "cache", "tendrilapp", "tendril");
|
|
5751
|
+
if (existsSync18(pluginRoot)) {
|
|
5565
5752
|
try {
|
|
5566
5753
|
const versions = readdirSync3(pluginRoot).filter((v) => /^\d+\.\d+\.\d+$/.test(v));
|
|
5567
5754
|
const newest = versions.sort((a, b) => versionIsNewer(a, b) ? 1 : -1)[0];
|
|
@@ -5605,17 +5792,6 @@ async function runDoctorChecks(options) {
|
|
|
5605
5792
|
});
|
|
5606
5793
|
return { ok: checks.filter((c) => c.name !== "figma-pat" && c.name !== "openrouter-key" && c.name !== "path-skew").every((c) => c.ok), checks };
|
|
5607
5794
|
}
|
|
5608
|
-
function findPathTendril(pathEnv, platform) {
|
|
5609
|
-
const dirs = pathEnv.split(path22.delimiter).filter((d) => d !== "" && !/node_modules[\\/]\.bin/.test(d) && !/[\\/]_npx[\\/]/.test(d));
|
|
5610
|
-
const names = platform === "win32" ? ["tendril.cmd", "tendril.bat"] : ["tendril"];
|
|
5611
|
-
for (const dir of dirs) {
|
|
5612
|
-
for (const name of names) {
|
|
5613
|
-
const candidate = path22.join(dir, name);
|
|
5614
|
-
if (existsSync17(candidate)) return candidate;
|
|
5615
|
-
}
|
|
5616
|
-
}
|
|
5617
|
-
return null;
|
|
5618
|
-
}
|
|
5619
5795
|
function probeVersion(binary) {
|
|
5620
5796
|
const windowsShim = /\.(cmd|bat)$/i.test(binary);
|
|
5621
5797
|
const res = windowsShim ? spawnSync(`"${binary}" --version`, { shell: true, timeout: 5e3, encoding: "utf8" }) : spawnSync(binary, ["--version"], { timeout: 5e3, encoding: "utf8" });
|
|
@@ -5687,6 +5863,7 @@ var init_doctor = __esm({
|
|
|
5687
5863
|
init_describe();
|
|
5688
5864
|
init_env();
|
|
5689
5865
|
init_environment();
|
|
5866
|
+
init_invocation();
|
|
5690
5867
|
init_output();
|
|
5691
5868
|
init_entitlement();
|
|
5692
5869
|
DEFAULT_MCP_URL = "http://127.0.0.1:3845/mcp";
|
|
@@ -6676,7 +6853,64 @@ var init_extract = __esm({
|
|
|
6676
6853
|
|
|
6677
6854
|
// packages/metadata/src/recording-set.ts
|
|
6678
6855
|
import { z as z9 } from "zod";
|
|
6679
|
-
|
|
6856
|
+
function roleLossToken(loss) {
|
|
6857
|
+
return loss.kind === "main" ? `main:${loss.main}` : `part:${loss.part}${EDGE_ARROW}${loss.main}`;
|
|
6858
|
+
}
|
|
6859
|
+
function parseRoleLossToken(token) {
|
|
6860
|
+
if (token.startsWith("main:")) {
|
|
6861
|
+
const main2 = token.slice("main:".length);
|
|
6862
|
+
return main2 === "" ? void 0 : { kind: "main", main: main2 };
|
|
6863
|
+
}
|
|
6864
|
+
if (!token.startsWith("part:")) return void 0;
|
|
6865
|
+
const [part, main, ...rest] = token.slice("part:".length).split(EDGE_ARROW);
|
|
6866
|
+
if (rest.length > 0 || part === void 0 || part === "" || main === void 0 || main === "") return void 0;
|
|
6867
|
+
return { kind: "edge", part, main };
|
|
6868
|
+
}
|
|
6869
|
+
function validateRecordingSet(manifest, fileExists) {
|
|
6870
|
+
const issues = [];
|
|
6871
|
+
const parsed = RecordingSetManifestSchema.safeParse(manifest);
|
|
6872
|
+
if (!parsed.success) {
|
|
6873
|
+
return {
|
|
6874
|
+
issues: [
|
|
6875
|
+
{
|
|
6876
|
+
severity: "error",
|
|
6877
|
+
message: `manifest does not match recording-set layout v${RECORDING_SET_VERSION}: ${parsed.error.issues.slice(0, 3).map((i) => `${i.path.join(".")}: ${i.message}`).join("; ")}`
|
|
6878
|
+
}
|
|
6879
|
+
]
|
|
6880
|
+
};
|
|
6881
|
+
}
|
|
6882
|
+
const m = parsed.data;
|
|
6883
|
+
const slugs = /* @__PURE__ */ new Set();
|
|
6884
|
+
for (const rep of m.reps) {
|
|
6885
|
+
if (slugs.has(rep.slug)) issues.push({ severity: "error", message: `duplicate rep slug "${rep.slug}"` });
|
|
6886
|
+
slugs.add(rep.slug);
|
|
6887
|
+
for (const f of REQUIRED_REP_FILES) {
|
|
6888
|
+
if (!fileExists(`${rep.slug}/${f}`)) issues.push({ severity: "error", message: `${rep.slug}: missing ${f}` });
|
|
6889
|
+
}
|
|
6890
|
+
if (rep.sourceFrame !== void 0 && m.sourceFrames?.[rep.sourceFrame] === void 0)
|
|
6891
|
+
issues.push({ severity: "warning", message: `${rep.slug}: sourceFrame ${rep.sourceFrame} not described in sourceFrames` });
|
|
6892
|
+
}
|
|
6893
|
+
if (m.roles !== void 0) {
|
|
6894
|
+
for (const slug of m.roles.main)
|
|
6895
|
+
if (!slugs.has(slug)) issues.push({ severity: "error", message: `roles.main references unknown slug "${slug}"` });
|
|
6896
|
+
for (const [slug, part] of Object.entries(m.roles.parts)) {
|
|
6897
|
+
if (!slugs.has(slug)) issues.push({ severity: "error", message: `roles.parts references unknown slug "${slug}"` });
|
|
6898
|
+
for (const of of part.partOf)
|
|
6899
|
+
if (!m.roles.main.includes(of)) issues.push({ severity: "error", message: `roles.parts["${slug}"].partOf references non-main "${of}"` });
|
|
6900
|
+
}
|
|
6901
|
+
for (const token of m.roles.narrowingAccepted ?? []) {
|
|
6902
|
+
const loss = parseRoleLossToken(token);
|
|
6903
|
+
if (loss === void 0) {
|
|
6904
|
+
issues.push({ severity: "error", message: `roles.narrowingAccepted entry "${token}" is not "main:<slug>" or "part:<part-slug>-><main-slug>"` });
|
|
6905
|
+
continue;
|
|
6906
|
+
}
|
|
6907
|
+
for (const slug of loss.kind === "main" ? [loss.main] : [loss.part, loss.main])
|
|
6908
|
+
if (!slugs.has(slug)) issues.push({ severity: "error", message: `roles.narrowingAccepted "${token}" references unknown slug "${slug}"` });
|
|
6909
|
+
}
|
|
6910
|
+
}
|
|
6911
|
+
return { manifest: m, issues };
|
|
6912
|
+
}
|
|
6913
|
+
var RECORDING_SET_VERSION, EnvelopeSchema, RepEntrySchema, RolesSchema, EDGE_ARROW, RecordingSetManifestSchema, REQUIRED_REP_FILES;
|
|
6680
6914
|
var init_recording_set = __esm({
|
|
6681
6915
|
"packages/metadata/src/recording-set.ts"() {
|
|
6682
6916
|
"use strict";
|
|
@@ -6709,10 +6943,21 @@ var init_recording_set = __esm({
|
|
|
6709
6943
|
* derivation — disclosed, never silently dropped (Calendar's Icon
|
|
6710
6944
|
* Buttons; unrecorded base variants). */
|
|
6711
6945
|
external: z9.array(z9.string()).optional(),
|
|
6712
|
-
/**
|
|
6713
|
-
*
|
|
6714
|
-
|
|
6946
|
+
/** Where this graph came from. A graph with empty `parts` makes the
|
|
6947
|
+
* structural composition check return an affirmative PASS, so a
|
|
6948
|
+
* human-authored override and a derived graph must never be
|
|
6949
|
+
* indistinguishable in a report. The CLI stamps this itself — a
|
|
6950
|
+
* supplied roles file cannot claim "derived" for itself. */
|
|
6951
|
+
rolesSource: z9.enum(["derived", "human-override"]).optional(),
|
|
6952
|
+
/** Losses in the DERIVED relation a human override deliberately
|
|
6953
|
+
* accepts, one token per loss (`roleLossToken`). Named one by one on
|
|
6954
|
+
* purpose: emptying out what composition measures — by dropping a
|
|
6955
|
+
* main, or by dropping/re-pointing a part→main edge — is exactly what
|
|
6956
|
+
* turns composition into a vacuous pass, so it is an acceptance
|
|
6957
|
+
* someone signs rather than an omission nobody notices. */
|
|
6958
|
+
narrowingAccepted: z9.array(z9.string()).optional()
|
|
6715
6959
|
});
|
|
6960
|
+
EDGE_ARROW = "->";
|
|
6716
6961
|
RecordingSetManifestSchema = z9.object({
|
|
6717
6962
|
version: z9.literal(RECORDING_SET_VERSION),
|
|
6718
6963
|
/** Human-readable component/system name. */
|
|
@@ -6724,6 +6969,7 @@ var init_recording_set = __esm({
|
|
|
6724
6969
|
notRecorded: z9.string().optional(),
|
|
6725
6970
|
roles: RolesSchema.optional()
|
|
6726
6971
|
});
|
|
6972
|
+
REQUIRED_REP_FILES = ["get_design_context.json", "get_metadata.json", "get_screenshot.json"];
|
|
6727
6973
|
}
|
|
6728
6974
|
});
|
|
6729
6975
|
|
|
@@ -6930,7 +7176,7 @@ async function runActivate(flags) {
|
|
|
6930
7176
|
fail(flags, ExitCode.Auth, {
|
|
6931
7177
|
error: `could not start activation: ${err instanceof Error ? err.message : String(err)}`,
|
|
6932
7178
|
code: "entitlement-service-unreachable",
|
|
6933
|
-
remediation:
|
|
7179
|
+
remediation: `Check your connection and retry \`${tendrilCommand("activate")}\`.`
|
|
6934
7180
|
});
|
|
6935
7181
|
}
|
|
6936
7182
|
process.stderr.write(`To activate Tendril, visit:
|
|
@@ -6957,7 +7203,7 @@ Waiting for approval\u2026
|
|
|
6957
7203
|
fail(flags, ExitCode.Auth, {
|
|
6958
7204
|
error: `activation was not approved (HTTP ${res.status})`,
|
|
6959
7205
|
code: "entitlement-denied",
|
|
6960
|
-
remediation:
|
|
7206
|
+
remediation: `Retry \`${tendrilCommand("activate")}\`; if it persists, check the account's plan in the portal.`
|
|
6961
7207
|
});
|
|
6962
7208
|
}
|
|
6963
7209
|
token = (await res.json()).token;
|
|
@@ -6967,7 +7213,7 @@ Waiting for approval\u2026
|
|
|
6967
7213
|
fail(flags, ExitCode.Auth, {
|
|
6968
7214
|
error: "activation timed out before the browser approval arrived",
|
|
6969
7215
|
code: "entitlement-timeout",
|
|
6970
|
-
remediation:
|
|
7216
|
+
remediation: `Run \`${tendrilCommand("activate")}\` again and complete the browser step within the shown window.`
|
|
6971
7217
|
});
|
|
6972
7218
|
}
|
|
6973
7219
|
const parsed = parseEntitlementToken(token);
|
|
@@ -6975,7 +7221,7 @@ Waiting for approval\u2026
|
|
|
6975
7221
|
fail(flags, ExitCode.Auth, {
|
|
6976
7222
|
error: `service returned an unusable token: ${parsed.error}`,
|
|
6977
7223
|
code: "entitlement-invalid",
|
|
6978
|
-
remediation:
|
|
7224
|
+
remediation: `Retry \`${tendrilCommand("activate")}\`; report this if it persists \u2014 it is a service-side fault.`
|
|
6979
7225
|
});
|
|
6980
7226
|
}
|
|
6981
7227
|
if (Object.keys(PUBLIC_KEYS).length > 0) {
|
|
@@ -6994,6 +7240,7 @@ var init_activate = __esm({
|
|
|
6994
7240
|
"use strict";
|
|
6995
7241
|
init_src3();
|
|
6996
7242
|
init_entitlement();
|
|
7243
|
+
init_invocation();
|
|
6997
7244
|
init_output();
|
|
6998
7245
|
ENTITLEMENT_SERVICE_URL = void 0;
|
|
6999
7246
|
}
|
|
@@ -7005,6 +7252,7 @@ __export(record_exports, {
|
|
|
7005
7252
|
instanceLeads: () => instanceLeads,
|
|
7006
7253
|
isFigmaAssetUrl: () => isFigmaAssetUrl,
|
|
7007
7254
|
isLocalAssetUrl: () => isLocalAssetUrl,
|
|
7255
|
+
narrowedRoles: () => narrowedRoles,
|
|
7008
7256
|
nextPayload: () => nextPayload,
|
|
7009
7257
|
runRecordAsset: () => runRecordAsset,
|
|
7010
7258
|
runRecordFetch: () => runRecordFetch,
|
|
@@ -7015,9 +7263,9 @@ __export(record_exports, {
|
|
|
7015
7263
|
runRecordPlan: () => runRecordPlan,
|
|
7016
7264
|
runRecordStatus: () => runRecordStatus
|
|
7017
7265
|
});
|
|
7018
|
-
import { existsSync as
|
|
7266
|
+
import { existsSync as existsSync20, mkdtempSync as mkdtempSync2, readFileSync as readFileSync16, readdirSync as readdirSync5 } from "node:fs";
|
|
7019
7267
|
import os5 from "node:os";
|
|
7020
|
-
import
|
|
7268
|
+
import path26 from "node:path";
|
|
7021
7269
|
import { writeFileSync as writeFileSync9 } from "node:fs";
|
|
7022
7270
|
function symbolsFromMetadataEnvelope(file, sourceFrame) {
|
|
7023
7271
|
const env = JSON.parse(readFileSync16(file, "utf8"));
|
|
@@ -7078,7 +7326,7 @@ function runRecordPlan(opts) {
|
|
|
7078
7326
|
if (rawFile !== void 0) {
|
|
7079
7327
|
try {
|
|
7080
7328
|
const envelope = rawEnvelopeFromFile(rawFile, opts.metadataRawPartsFile !== void 0);
|
|
7081
|
-
const tmp =
|
|
7329
|
+
const tmp = path26.join(mkdtempSync2(path26.join(os5.tmpdir(), "tendril-plan-meta-")), "get_metadata.json");
|
|
7082
7330
|
writeFileSync9(tmp, JSON.stringify(envelope));
|
|
7083
7331
|
metadataEntries.push({ file: tmp });
|
|
7084
7332
|
} catch (err) {
|
|
@@ -7100,7 +7348,7 @@ function runRecordPlan(opts) {
|
|
|
7100
7348
|
let metadataTruncated = false;
|
|
7101
7349
|
for (const { file, frame } of metadataEntries) {
|
|
7102
7350
|
try {
|
|
7103
|
-
const parsed = symbolsFromMetadataEnvelope(
|
|
7351
|
+
const parsed = symbolsFromMetadataEnvelope(path26.resolve(file), frame);
|
|
7104
7352
|
symbols.push(...parsed.symbols);
|
|
7105
7353
|
if (parsed.truncated) metadataTruncated = true;
|
|
7106
7354
|
} catch (err) {
|
|
@@ -7133,7 +7381,7 @@ function runRecordPlan(opts) {
|
|
|
7133
7381
|
if (symbols.length === 0) {
|
|
7134
7382
|
const leads = metadataEntries.flatMap(({ file }) => {
|
|
7135
7383
|
try {
|
|
7136
|
-
const env = JSON.parse(readFileSync16(
|
|
7384
|
+
const env = JSON.parse(readFileSync16(path26.resolve(file), "utf8"));
|
|
7137
7385
|
return instanceLeads(env.content.map((c) => c.text ?? "").join("\n"));
|
|
7138
7386
|
} catch {
|
|
7139
7387
|
return [];
|
|
@@ -7238,7 +7486,7 @@ function nextPayload(setDir) {
|
|
|
7238
7486
|
const instruction = nextInstruction(setDir);
|
|
7239
7487
|
const status = sessionStatus(setDir);
|
|
7240
7488
|
const progress = { recordedReps: status.reps.filter((x) => x.missing.length === 0).length, totalReps: status.reps.length };
|
|
7241
|
-
if (instruction === null && !
|
|
7489
|
+
if (instruction === null && !existsSync20(path26.join(setDir, "get_variable_defs.json"))) {
|
|
7242
7490
|
const manifest = loadManifest(setDir);
|
|
7243
7491
|
const frameNode = Object.keys(manifest.sourceFrames ?? {})[0] ?? manifest.reps[0]?.nodeId ?? "";
|
|
7244
7492
|
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 };
|
|
@@ -7255,7 +7503,7 @@ function runRecordNext(opts) {
|
|
|
7255
7503
|
const progress = payload["progress"];
|
|
7256
7504
|
process.stdout.write(`[${progress.recordedReps}/${progress.totalReps} reps] ${payload["tool"]} for ${payload["slug"]} (node ${payload["nodeId"]})
|
|
7257
7505
|
\u2192 ${payload["note"]}
|
|
7258
|
-
\u2192 then:
|
|
7506
|
+
\u2192 then: ${tendrilCommand(`record ingest --set ${path26.resolve(opts.setDir)} --rep ${payload["slug"]} --tool ${payload["tool"]} --file <envelope.json>`)}
|
|
7259
7507
|
`);
|
|
7260
7508
|
});
|
|
7261
7509
|
}
|
|
@@ -7329,7 +7577,7 @@ async function autoFetchAssets(setDir, rep, envelopeText2) {
|
|
|
7329
7577
|
const skipped = [];
|
|
7330
7578
|
const failed = [];
|
|
7331
7579
|
for (const { url, name } of assetUrlsFromEnvelopeText(envelopeText2)) {
|
|
7332
|
-
if (
|
|
7580
|
+
if (existsSync20(path26.join(setDir, rep, name))) {
|
|
7333
7581
|
skipped.push(name);
|
|
7334
7582
|
continue;
|
|
7335
7583
|
}
|
|
@@ -7351,16 +7599,16 @@ async function autoFetchAssets(setDir, rep, envelopeText2) {
|
|
|
7351
7599
|
}
|
|
7352
7600
|
function rawEnvelopeFromFile(file, parts) {
|
|
7353
7601
|
if (parts) {
|
|
7354
|
-
const blocks = JSON.parse(readFileSync16(
|
|
7602
|
+
const blocks = JSON.parse(readFileSync16(path26.resolve(file), "utf8"));
|
|
7355
7603
|
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");
|
|
7356
7604
|
return { content: blocks.map((text) => ({ type: "text", text })) };
|
|
7357
7605
|
}
|
|
7358
|
-
return { content: [{ type: "text", text: readFileSync16(
|
|
7606
|
+
return { content: [{ type: "text", text: readFileSync16(path26.resolve(file), "utf8") }] };
|
|
7359
7607
|
}
|
|
7360
7608
|
async function runRecordIngest(opts) {
|
|
7361
7609
|
let payload;
|
|
7362
7610
|
try {
|
|
7363
|
-
payload = opts.rawParts === true || opts.raw === true ? rawEnvelopeFromFile(opts.file, opts.rawParts === true) : JSON.parse(readFileSync16(
|
|
7611
|
+
payload = opts.rawParts === true || opts.raw === true ? rawEnvelopeFromFile(opts.file, opts.rawParts === true) : JSON.parse(readFileSync16(path26.resolve(opts.file), "utf8"));
|
|
7364
7612
|
} catch (err) {
|
|
7365
7613
|
fail(opts, ExitCode.InputValidation, {
|
|
7366
7614
|
error: `cannot read envelope file: ${err instanceof Error ? err.message : String(err)}`,
|
|
@@ -7372,7 +7620,7 @@ async function runRecordIngest(opts) {
|
|
|
7372
7620
|
fail(opts, ExitCode.InputValidation, {
|
|
7373
7621
|
error: "--raw is for text tool responses; screenshots are binary",
|
|
7374
7622
|
code: "envelope-invalid",
|
|
7375
|
-
remediation:
|
|
7623
|
+
remediation: `Use \`${tendrilCommand(`record fetch --set ${path26.resolve(opts.setDir)} --rep ${opts.rep} --tool ${opts.tool} --url <image_url>`)}\` instead.`
|
|
7376
7624
|
});
|
|
7377
7625
|
}
|
|
7378
7626
|
if (opts.rep === "__set__" && opts.tool === "get_variable_defs") {
|
|
@@ -7384,7 +7632,7 @@ async function runRecordIngest(opts) {
|
|
|
7384
7632
|
remediation: "Save the get_variable_defs response verbatim as a text envelope."
|
|
7385
7633
|
});
|
|
7386
7634
|
}
|
|
7387
|
-
writeFileSync9(
|
|
7635
|
+
writeFileSync9(path26.join(opts.setDir, "get_variable_defs.json"), `${JSON.stringify(payload, null, 1)}
|
|
7388
7636
|
`);
|
|
7389
7637
|
emitData(opts, { ingested: "get_variable_defs", rep: "__set__", setLevel: true, next: nextPayload(opts.setDir) }, () => {
|
|
7390
7638
|
process.stdout.write("set-level get_variable_defs ingested\n");
|
|
@@ -7400,7 +7648,7 @@ async function runRecordIngest(opts) {
|
|
|
7400
7648
|
if (assets !== void 0) {
|
|
7401
7649
|
for (const a of assets.fetched) process.stdout.write(` asset fetched ${a}
|
|
7402
7650
|
`);
|
|
7403
|
-
for (const f of assets.failed) process.stdout.write(` ASSET FAILED ${f.name}: ${f.reason} \u2014 download it
|
|
7651
|
+
for (const f of assets.failed) process.stdout.write(` ASSET FAILED ${f.name}: ${f.reason} \u2014 download it, then: ${tendrilCommand(`record asset --set ${path26.resolve(opts.setDir)} --rep ${opts.rep} --name ${f.name} --file <downloaded-file>`)}
|
|
7404
7652
|
`);
|
|
7405
7653
|
}
|
|
7406
7654
|
});
|
|
@@ -7473,14 +7721,14 @@ async function runRecordIngestRep(opts) {
|
|
|
7473
7721
|
if (assets !== void 0) {
|
|
7474
7722
|
for (const a of assets.fetched) process.stdout.write(` asset fetched ${a}
|
|
7475
7723
|
`);
|
|
7476
|
-
for (const f of assets.failed) process.stdout.write(` ASSET FAILED ${f.name}: ${f.reason} \u2014 download it
|
|
7724
|
+
for (const f of assets.failed) process.stdout.write(` ASSET FAILED ${f.name}: ${f.reason} \u2014 download it, then: ${tendrilCommand(`record asset --set ${path26.resolve(opts.setDir)} --rep ${opts.rep} --name ${f.name} --file <downloaded-file>`)}
|
|
7477
7725
|
`);
|
|
7478
7726
|
}
|
|
7479
7727
|
});
|
|
7480
7728
|
}
|
|
7481
7729
|
function runRecordAsset(opts) {
|
|
7482
7730
|
if (opts.dir !== void 0) {
|
|
7483
|
-
const dir =
|
|
7731
|
+
const dir = path26.resolve(opts.dir);
|
|
7484
7732
|
const names = readdirSync5(dir).filter((f) => /^asset-[\w.-]+\.(svg|png|jpe?g|webp|gif)$/i.test(f));
|
|
7485
7733
|
if (names.length === 0) {
|
|
7486
7734
|
fail(opts, ExitCode.InputValidation, {
|
|
@@ -7492,7 +7740,7 @@ function runRecordAsset(opts) {
|
|
|
7492
7740
|
const ingested = [];
|
|
7493
7741
|
try {
|
|
7494
7742
|
for (const name of names) {
|
|
7495
|
-
ingestAsset(opts.setDir, opts.rep, name, readFileSync16(
|
|
7743
|
+
ingestAsset(opts.setDir, opts.rep, name, readFileSync16(path26.join(dir, name)));
|
|
7496
7744
|
ingested.push(name);
|
|
7497
7745
|
}
|
|
7498
7746
|
} catch (err) {
|
|
@@ -7512,11 +7760,11 @@ function runRecordAsset(opts) {
|
|
|
7512
7760
|
fail(opts, ExitCode.InputValidation, {
|
|
7513
7761
|
error: "pass --name and --file for a single asset, or --dir for a batch",
|
|
7514
7762
|
code: "asset-rejected",
|
|
7515
|
-
remediation:
|
|
7763
|
+
remediation: tendrilCommand(`record asset --set ${path26.resolve(opts.setDir)} --rep ${opts.rep} --dir <downloads-dir>`)
|
|
7516
7764
|
});
|
|
7517
7765
|
}
|
|
7518
7766
|
try {
|
|
7519
|
-
ingestAsset(opts.setDir, opts.rep, opts.name, readFileSync16(
|
|
7767
|
+
ingestAsset(opts.setDir, opts.rep, opts.name, readFileSync16(path26.resolve(opts.file)));
|
|
7520
7768
|
emitData(opts, { rep: opts.rep, asset: opts.name }, () => {
|
|
7521
7769
|
process.stdout.write(`ingested ${opts.rep}/${opts.name}
|
|
7522
7770
|
`);
|
|
@@ -7541,11 +7789,66 @@ ${status.reps.filter((r) => r.missing.length > 0).length} rep(s) pending
|
|
|
7541
7789
|
`);
|
|
7542
7790
|
});
|
|
7543
7791
|
}
|
|
7792
|
+
function narrowedRoles(derived, override) {
|
|
7793
|
+
const losses = [];
|
|
7794
|
+
const mains = new Set(override.main);
|
|
7795
|
+
for (const main of derived.main) if (!mains.has(main)) losses.push({ kind: "main", main });
|
|
7796
|
+
for (const [part, entry] of Object.entries(derived.parts))
|
|
7797
|
+
for (const main of entry.partOf) if (override.parts[part]?.partOf.includes(main) !== true) losses.push({ kind: "edge", part, main });
|
|
7798
|
+
return losses.sort((a, b) => roleLossToken(a) < roleLossToken(b) ? -1 : 1);
|
|
7799
|
+
}
|
|
7800
|
+
function rolesFromFile(opts, file, derived) {
|
|
7801
|
+
let json;
|
|
7802
|
+
try {
|
|
7803
|
+
json = JSON.parse(readFileSync16(path26.resolve(file), "utf8"));
|
|
7804
|
+
} catch (err) {
|
|
7805
|
+
fail(opts, ExitCode.InputValidation, {
|
|
7806
|
+
error: `cannot read roles file ${file}: ${err instanceof Error ? err.message.split("\n")[0] : String(err)}`,
|
|
7807
|
+
code: "roles-file-unreadable",
|
|
7808
|
+
remediation: 'The roles file is JSON: {"main":["<slug>"],"parts":{"<slug>":{"partOf":["<main-slug>"]}}}.'
|
|
7809
|
+
});
|
|
7810
|
+
}
|
|
7811
|
+
const parsed = RolesSchema.safeParse(json);
|
|
7812
|
+
if (!parsed.success) {
|
|
7813
|
+
fail(opts, ExitCode.InputValidation, {
|
|
7814
|
+
error: `roles file rejected: ${parsed.error.issues.slice(0, 3).map((i) => `${i.path.join(".") || "(root)"}: ${i.message}`).join("; ")}`,
|
|
7815
|
+
code: "roles-file-invalid",
|
|
7816
|
+
remediation: 'The roles file is JSON: {"main":["<slug>"],"parts":{"<slug>":{"partOf":["<main-slug>"]}},"external":["\u2026"]}.'
|
|
7817
|
+
});
|
|
7818
|
+
}
|
|
7819
|
+
const accepted = parsed.data.narrowingAccepted ?? [];
|
|
7820
|
+
const unaccepted = narrowedRoles(derived, parsed.data).filter((loss) => !accepted.includes(roleLossToken(loss)));
|
|
7821
|
+
if (unaccepted.length > 0) {
|
|
7822
|
+
fail(opts, ExitCode.InputValidation, {
|
|
7823
|
+
error: `roles file narrows what this set's derivation found: it ${unaccepted.map(describeRoleLoss).join("; ")} \u2014 a main the graph never names is not composition-checked at all, and a main whose graph names no parts passes the check vacuously`,
|
|
7824
|
+
code: "roles-file-narrows",
|
|
7825
|
+
remediation: `Keep the derived mains and part edges in the graph, or accept each loss by name: add "narrowingAccepted": ${JSON.stringify(
|
|
7826
|
+
[.../* @__PURE__ */ new Set([...accepted, ...unaccepted.map(roleLossToken)])].sort()
|
|
7827
|
+
)} to the roles file.`
|
|
7828
|
+
});
|
|
7829
|
+
}
|
|
7830
|
+
return {
|
|
7831
|
+
main: parsed.data.main,
|
|
7832
|
+
parts: parsed.data.parts,
|
|
7833
|
+
...parsed.data.external !== void 0 ? { external: parsed.data.external } : {},
|
|
7834
|
+
...accepted.length > 0 ? { narrowingAccepted: accepted } : {},
|
|
7835
|
+
// Stamped by the CLI, never read from the file: a supplied graph
|
|
7836
|
+
// does not get to describe itself as derived.
|
|
7837
|
+
rolesSource: "human-override"
|
|
7838
|
+
};
|
|
7839
|
+
}
|
|
7544
7840
|
function runRecordFinish(opts) {
|
|
7545
|
-
|
|
7841
|
+
if (!existsSync20(path26.join(opts.setDir, "recording-set.json"))) {
|
|
7842
|
+
fail(opts, ExitCode.InputValidation, {
|
|
7843
|
+
error: `no recording-set.json in ${opts.setDir}`,
|
|
7844
|
+
code: "no-recording-set",
|
|
7845
|
+
remediation: `Run \`${tendrilCommand(`record plan --set ${path26.resolve(opts.setDir)} --component <name> --metadata <envelope.json>`)}\` first \u2014 \`record finish\` closes a set the planner opened.`
|
|
7846
|
+
});
|
|
7847
|
+
}
|
|
7848
|
+
const { manifest, raw } = readManifestFile(opts.setDir);
|
|
7546
7849
|
const derived = deriveRoles(opts.setDir, manifest);
|
|
7547
|
-
const roles = opts.rolesFile !== void 0 ?
|
|
7548
|
-
emitData(opts, { derived, confirmed: opts.confirmRoles }, () => {
|
|
7850
|
+
const roles = opts.rolesFile !== void 0 ? rolesFromFile(opts, opts.rolesFile, derived) : { main: derived.main, parts: derived.parts, external: derived.external, rolesSource: "derived" };
|
|
7851
|
+
emitData(opts, { derived, confirmed: opts.confirmRoles, rolesSource: roles.rolesSource, narrowingAccepted: roles.narrowingAccepted ?? [] }, () => {
|
|
7549
7852
|
process.stdout.write(`derived mains: ${derived.main.join(", ") || "(none)"}
|
|
7550
7853
|
`);
|
|
7551
7854
|
for (const [slug, part] of Object.entries(derived.parts)) process.stdout.write(` part ${slug} \u2192 ${part.partOf.join(", ")}
|
|
@@ -7567,24 +7870,40 @@ function runRecordFinish(opts) {
|
|
|
7567
7870
|
fail(opts, ExitCode.ConfirmationRequired, {
|
|
7568
7871
|
error: "--confirm-roles (and --roles-file) require an interactive terminal \u2014 this confirmation is human-only",
|
|
7569
7872
|
code: "roles-confirmation-not-interactive",
|
|
7570
|
-
remediation:
|
|
7873
|
+
remediation: `A human runs \`${tendrilCommand(`record finish --set ${path26.resolve(opts.setDir)} --confirm-roles`)}\` in their own terminal. Agents: show the proposal to your operator instead of confirming it.`
|
|
7874
|
+
});
|
|
7875
|
+
}
|
|
7876
|
+
const merged = { ...raw, roles };
|
|
7877
|
+
const { issues } = validateRecordingSet(merged, (rel) => existsSync20(path26.join(opts.setDir, rel)));
|
|
7878
|
+
const errors = issues.filter((i) => i.severity === "error");
|
|
7879
|
+
if (errors.length > 0) {
|
|
7880
|
+
fail(opts, ExitCode.InputValidation, {
|
|
7881
|
+
error: `recording set rejected \u2014 roles NOT written: ${errors.map((e) => e.message).join("; ")}`,
|
|
7882
|
+
code: "recording-set-invalid",
|
|
7883
|
+
remediation: `Fix the set (\`${tendrilCommand(`record status --set ${path26.resolve(opts.setDir)}`)}\` lists missing envelopes) or the roles graph, then re-run \`record finish\`.`
|
|
7571
7884
|
});
|
|
7572
7885
|
}
|
|
7573
|
-
const
|
|
7574
|
-
|
|
7886
|
+
for (const issue of issues) if (issue.severity === "warning") warn(opts, issue.message);
|
|
7887
|
+
writeManifest(opts.setDir, merged);
|
|
7888
|
+
if (!opts.json) {
|
|
7889
|
+
const accepted = roles.narrowingAccepted ?? [];
|
|
7890
|
+
process.stdout.write(`roles written to recording-set.json (source: ${roles.rolesSource})${accepted.length > 0 ? ` \u2014 narrowing accepted by name: ${accepted.join(", ")}` : ""}
|
|
7575
7891
|
`);
|
|
7576
|
-
|
|
7892
|
+
}
|
|
7577
7893
|
}
|
|
7578
|
-
var ENVELOPE_HELP, isAutoFetchAssetUrl;
|
|
7894
|
+
var ENVELOPE_HELP, isAutoFetchAssetUrl, describeRoleLoss;
|
|
7579
7895
|
var init_record = __esm({
|
|
7580
7896
|
"packages/cli/src/commands/record.ts"() {
|
|
7581
7897
|
"use strict";
|
|
7582
7898
|
init_src3();
|
|
7583
7899
|
init_src();
|
|
7900
|
+
init_src6();
|
|
7584
7901
|
init_output();
|
|
7585
7902
|
init_entitlement();
|
|
7586
|
-
|
|
7903
|
+
init_invocation();
|
|
7904
|
+
ENVELOPE_HELP = `Envelope format: text tools save {"content":[{"type":"text","text":"<VERBATIM response text incl. any Currently-selected-nodes block>"}]}; get_screenshot: do NOT download the image yourself \u2014 pass its image_url to \`${tendrilCommand("record fetch")}\` (MCP: tendril_record_fetch), which pulls the bytes to disk directly. That keeps the pixel ground truth out of your context and costs one approval instead of a shell command per asset. Assets are asset-<first-8-hex-of-figma-uuid>.<ext>; their URLs appear as const declarations inside the design-context text and also expire.`;
|
|
7587
7905
|
isAutoFetchAssetUrl = (url) => isFigmaAssetUrl(url) || isLocalAssetUrl(url);
|
|
7906
|
+
describeRoleLoss = (loss) => loss.kind === "main" ? `drops main "${loss.main}"` : `stops "${loss.part}" being a part of main "${loss.main}"`;
|
|
7588
7907
|
}
|
|
7589
7908
|
});
|
|
7590
7909
|
|
|
@@ -7775,8 +8094,8 @@ var init_engine_curated = __esm({
|
|
|
7775
8094
|
});
|
|
7776
8095
|
|
|
7777
8096
|
// packages/generate/src/loop.ts
|
|
7778
|
-
import { existsSync as
|
|
7779
|
-
import
|
|
8097
|
+
import { existsSync as existsSync21, mkdirSync as mkdirSync6, readFileSync as readFileSync17, renameSync, writeFileSync as writeFileSync10 } from "node:fs";
|
|
8098
|
+
import path27 from "node:path";
|
|
7780
8099
|
import { z as z11 } from "zod";
|
|
7781
8100
|
function objective(scores, behaviors) {
|
|
7782
8101
|
const vals = scores.map((s) => Math.min(s.similarity, s.inkRecall));
|
|
@@ -7810,12 +8129,12 @@ ${preludeLines.join("\n")}` : ""}${absentLines.length > 0 ? `
|
|
|
7810
8129
|
MISSING FEATURES (absent-ink clusters \u2014 recorded marks your render leaves out or paints invisibly; GATING at the cert bar. Fix the named node's ink \u2014 a config carrying one cannot certify):
|
|
7811
8130
|
${absentLines.join("\n")}` : ""}
|
|
7812
8131
|
|
|
7813
|
-
Fix the FAIL configs (region coordinates are in the recorded screenshot's frame; ink<1 means recorded foreground pixels your render does not cover). Reading the pair: ink=1 with LOW sim means nothing is missing \u2014
|
|
8132
|
+
Fix the FAIL configs (region coordinates are in the recorded screenshot's frame; ink<1 means recorded foreground pixels your render does not cover). Reading the pair: ink=1 with LOW sim means nothing is MISSING (do not chase missing ink) \u2014 the same pixels are being painted wrong, and there are TWO causes. 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."}`;
|
|
7814
8133
|
}
|
|
7815
8134
|
function archivePriorRun(outDir) {
|
|
7816
|
-
if (!
|
|
8135
|
+
if (!existsSync21(path27.join(outDir, "run-log.json")) && !existsSync21(path27.join(outDir, "loop-state.json"))) return void 0;
|
|
7817
8136
|
let n = 1;
|
|
7818
|
-
while (
|
|
8137
|
+
while (existsSync21(`${outDir}-prev-${n}`)) n += 1;
|
|
7819
8138
|
renameSync(outDir, `${outDir}-prev-${n}`);
|
|
7820
8139
|
return `${outDir}-prev-${n}`;
|
|
7821
8140
|
}
|
|
@@ -7824,14 +8143,14 @@ async function runEngineLoop(opts) {
|
|
|
7824
8143
|
const plateau = opts.plateau ?? 2;
|
|
7825
8144
|
const progress = opts.onProgress ?? (() => {
|
|
7826
8145
|
});
|
|
7827
|
-
const statePath =
|
|
7828
|
-
const resuming = opts.resume === true &&
|
|
8146
|
+
const statePath = path27.join(opts.outDir, "loop-state.json");
|
|
8147
|
+
const resuming = opts.resume === true && existsSync21(statePath);
|
|
7829
8148
|
if (!resuming) {
|
|
7830
8149
|
const archived = archivePriorRun(opts.outDir);
|
|
7831
8150
|
if (archived !== void 0) progress(`previous run archived to ${archived}`);
|
|
7832
8151
|
}
|
|
7833
8152
|
mkdirSync6(opts.outDir, { recursive: true });
|
|
7834
|
-
const scratch =
|
|
8153
|
+
const scratch = path27.join(opts.outDir, ".candidate");
|
|
7835
8154
|
let attempts = [];
|
|
7836
8155
|
let log = [];
|
|
7837
8156
|
let best;
|
|
@@ -7859,7 +8178,7 @@ async function runEngineLoop(opts) {
|
|
|
7859
8178
|
};
|
|
7860
8179
|
const writeCandidate = (files) => {
|
|
7861
8180
|
mkdirSync6(scratch, { recursive: true });
|
|
7862
|
-
for (const [name, content] of Object.entries(files)) writeFileSync10(
|
|
8181
|
+
for (const [name, content] of Object.entries(files)) writeFileSync10(path27.join(scratch, name), content);
|
|
7863
8182
|
};
|
|
7864
8183
|
const scoreCandidate = async (candidate, iter, usd, modelMs) => {
|
|
7865
8184
|
writeCandidate(candidate.files);
|
|
@@ -7917,8 +8236,8 @@ async function runEngineLoop(opts) {
|
|
|
7917
8236
|
const usd = candidate.usage?.usd ?? 0;
|
|
7918
8237
|
spentUsd += usd;
|
|
7919
8238
|
if (candidate.raw !== void 0) {
|
|
7920
|
-
mkdirSync6(
|
|
7921
|
-
writeFileSync10(
|
|
8239
|
+
mkdirSync6(path27.join(opts.outDir, "responses"), { recursive: true });
|
|
8240
|
+
writeFileSync10(path27.join(opts.outDir, "responses", `iter-${iter}.md`), candidate.raw);
|
|
7922
8241
|
}
|
|
7923
8242
|
if (candidate.files[opts.entry] === void 0 || candidate.files["styles.css"] === void 0) {
|
|
7924
8243
|
const finish = candidate.usage?.finishReason ?? "?";
|
|
@@ -7944,10 +8263,10 @@ async function runEngineLoop(opts) {
|
|
|
7944
8263
|
}
|
|
7945
8264
|
}
|
|
7946
8265
|
}
|
|
7947
|
-
if (best !== void 0) for (const [name, content] of Object.entries(best.files)) writeFileSync10(
|
|
8266
|
+
if (best !== void 0) for (const [name, content] of Object.entries(best.files)) writeFileSync10(path27.join(opts.outDir, name), content);
|
|
7948
8267
|
const final = best !== void 0 ? await opts.score(opts.outDir) : { scores: [], behaviors: [] };
|
|
7949
8268
|
writeFileSync10(
|
|
7950
|
-
|
|
8269
|
+
path27.join(opts.outDir, "run-log.json"),
|
|
7951
8270
|
`${JSON.stringify(
|
|
7952
8271
|
{
|
|
7953
8272
|
...opts.meta,
|
|
@@ -8014,8 +8333,8 @@ var init_loop2 = __esm({
|
|
|
8014
8333
|
});
|
|
8015
8334
|
|
|
8016
8335
|
// packages/generate/src/brief.ts
|
|
8017
|
-
import { existsSync as
|
|
8018
|
-
import
|
|
8336
|
+
import { existsSync as existsSync22, readFileSync as readFileSync18 } from "node:fs";
|
|
8337
|
+
import path28 from "node:path";
|
|
8019
8338
|
function singleAxes2(name) {
|
|
8020
8339
|
const parsed = parseVariantAxes(name);
|
|
8021
8340
|
if (parsed === void 0) return void 0;
|
|
@@ -8254,8 +8573,8 @@ function envelopeText(file) {
|
|
|
8254
8573
|
}
|
|
8255
8574
|
function dismissEvidence(setDir, repSlugs) {
|
|
8256
8575
|
for (const slug of repSlugs) {
|
|
8257
|
-
const f =
|
|
8258
|
-
if (!
|
|
8576
|
+
const f = path28.join(setDir, slug, "get_design_context.json");
|
|
8577
|
+
if (!existsSync22(f)) continue;
|
|
8259
8578
|
const text = envelopeText(f);
|
|
8260
8579
|
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);
|
|
8261
8580
|
if (propHit !== null) return `emission prop "${propHit[1]}"`;
|
|
@@ -8299,13 +8618,13 @@ function recordedFontNeeds(setDir, opts = {}) {
|
|
|
8299
8618
|
}
|
|
8300
8619
|
};
|
|
8301
8620
|
const manifest = loadManifest(setDir);
|
|
8302
|
-
const setDefs =
|
|
8303
|
-
if (
|
|
8621
|
+
const setDefs = path28.join(setDir, "get_variable_defs.json");
|
|
8622
|
+
if (existsSync22(setDefs)) fromDefs(envelopeText(setDefs));
|
|
8304
8623
|
for (const rep of manifest.reps) {
|
|
8305
|
-
const ctx =
|
|
8306
|
-
if (
|
|
8307
|
-
const defs =
|
|
8308
|
-
if (
|
|
8624
|
+
const ctx = path28.join(setDir, rep.slug, "get_design_context.json");
|
|
8625
|
+
if (existsSync22(ctx)) fromEmission(envelopeText(ctx));
|
|
8626
|
+
const defs = path28.join(setDir, rep.slug, "get_variable_defs.json");
|
|
8627
|
+
if (existsSync22(defs)) fromDefs(envelopeText(defs));
|
|
8309
8628
|
}
|
|
8310
8629
|
return [...byFamily.entries()].map(([family, paired]) => {
|
|
8311
8630
|
const weights = /* @__PURE__ */ new Set([...paired, ...opts.pairedOnly === true ? [] : unpaired]);
|
|
@@ -8316,8 +8635,8 @@ function symbolFontGlyphCount(setDir, reps) {
|
|
|
8316
8635
|
const PUA = /[\uE000-\uF8FF\u{F0000}-\u{FFFFD}\u{100000}-\u{10FFFD}]/u;
|
|
8317
8636
|
const glyphs = /* @__PURE__ */ new Set();
|
|
8318
8637
|
for (const rep of reps) {
|
|
8319
|
-
const file =
|
|
8320
|
-
if (!
|
|
8638
|
+
const file = path28.join(setDir, rep, "get_metadata.json");
|
|
8639
|
+
if (!existsSync22(file)) continue;
|
|
8321
8640
|
try {
|
|
8322
8641
|
const text = JSON.parse(readFileSync18(file, "utf8")).content.map((c) => c.text ?? "").join("\n");
|
|
8323
8642
|
for (const m of text.matchAll(/<text\s[^>]*name="([^"]*)"/g)) {
|
|
@@ -8345,8 +8664,8 @@ function recordedTextSlots(setDir, repSlugs) {
|
|
|
8345
8664
|
const propRep = [];
|
|
8346
8665
|
const perRep = [];
|
|
8347
8666
|
for (const slug of repSlugs) {
|
|
8348
|
-
const f =
|
|
8349
|
-
if (!
|
|
8667
|
+
const f = path28.join(setDir, slug, "get_design_context.json");
|
|
8668
|
+
if (!existsSync22(f)) continue;
|
|
8350
8669
|
const code = envelopeText(f);
|
|
8351
8670
|
const props = /* @__PURE__ */ new Map();
|
|
8352
8671
|
for (const m of code.matchAll(/[{,]\s*(\w+)\s*=\s*"((?:[^"\\]|\\.)*)"/g)) {
|
|
@@ -8371,8 +8690,8 @@ function recordedTextSlots(setDir, repSlugs) {
|
|
|
8371
8690
|
}
|
|
8372
8691
|
const axisValuesBySlug = /* @__PURE__ */ new Map();
|
|
8373
8692
|
for (const slug of repSlugs) {
|
|
8374
|
-
const metaFile =
|
|
8375
|
-
if (!
|
|
8693
|
+
const metaFile = path28.join(setDir, slug, "get_metadata.json");
|
|
8694
|
+
if (!existsSync22(metaFile)) continue;
|
|
8376
8695
|
const name = symbolName(envelopeText(metaFile));
|
|
8377
8696
|
if (name === void 0) continue;
|
|
8378
8697
|
const values = /* @__PURE__ */ new Set();
|
|
@@ -8471,8 +8790,8 @@ function authorTaskFromSet(setDir, opts = {}) {
|
|
|
8471
8790
|
const poses = [];
|
|
8472
8791
|
const missing = [];
|
|
8473
8792
|
for (const rep of manifest.reps) {
|
|
8474
|
-
const metaFile =
|
|
8475
|
-
if (!
|
|
8793
|
+
const metaFile = path28.join(setDir, rep.slug, "get_metadata.json");
|
|
8794
|
+
if (!existsSync22(metaFile)) {
|
|
8476
8795
|
missing.push(rep.slug);
|
|
8477
8796
|
continue;
|
|
8478
8797
|
}
|
|
@@ -8486,8 +8805,8 @@ function authorTaskFromSet(setDir, opts = {}) {
|
|
|
8486
8805
|
if (missing.length > 0) {
|
|
8487
8806
|
throw new Error(`recording set ${setDir} is missing metadata for ${missing.length} rep(s): ${missing.slice(0, 5).join(", ")}${missing.length > 5 ? ", \u2026" : ""}`);
|
|
8488
8807
|
}
|
|
8489
|
-
const setMeta =
|
|
8490
|
-
const latticeNames = manifest.latticeNames ?? (
|
|
8808
|
+
const setMeta = path28.join(setDir, "get_metadata.json");
|
|
8809
|
+
const latticeNames = manifest.latticeNames ?? (existsSync22(setMeta) ? [...envelopeText(setMeta).matchAll(/name="([^"]*)"/g)].map((m) => decodeXmlEntities(m[1])) : void 0);
|
|
8491
8810
|
const defaults = { ...manifest.defaults ?? {}, ...opts.defaults ?? {} };
|
|
8492
8811
|
const recordedFonts = recordedFontFamilies(setDir);
|
|
8493
8812
|
const textSlots = recordedTextSlots(setDir, manifest.reps.map((r) => r.slug));
|
|
@@ -8661,10 +8980,10 @@ var init_brief = __esm({
|
|
|
8661
8980
|
});
|
|
8662
8981
|
|
|
8663
8982
|
// packages/generate/src/segments.ts
|
|
8664
|
-
import { existsSync as
|
|
8665
|
-
import
|
|
8983
|
+
import { existsSync as existsSync23, readFileSync as readFileSync19, readdirSync as readdirSync6 } from "node:fs";
|
|
8984
|
+
import path29 from "node:path";
|
|
8666
8985
|
function repText(set, rep, tool) {
|
|
8667
|
-
const env = JSON.parse(readFileSync19(
|
|
8986
|
+
const env = JSON.parse(readFileSync19(path29.join(set, rep, `${tool}.json`), "utf8"));
|
|
8668
8987
|
return tool === "get_design_context" ? envelopeFirstTextPart(env) : envelopeTextContent(env);
|
|
8669
8988
|
}
|
|
8670
8989
|
function stripFigmaInstructions(emission) {
|
|
@@ -8725,16 +9044,16 @@ DROPPED from the map, untrustworthy in the source export \u2014 for these, each
|
|
|
8725
9044
|
function buildSegments(task, mode = "fenced") {
|
|
8726
9045
|
const SET = task.set;
|
|
8727
9046
|
let rawDefs = {};
|
|
8728
|
-
if (
|
|
8729
|
-
const text = envelopeFirstTextPart(JSON.parse(readFileSync19(
|
|
9047
|
+
if (existsSync23(path29.join(SET, "get_variable_defs.json"))) {
|
|
9048
|
+
const text = envelopeFirstTextPart(JSON.parse(readFileSync19(path29.join(SET, "get_variable_defs.json"), "utf8"))) || "{}";
|
|
8730
9049
|
try {
|
|
8731
9050
|
rawDefs = JSON.parse(text);
|
|
8732
9051
|
} catch {
|
|
8733
9052
|
}
|
|
8734
9053
|
} else {
|
|
8735
9054
|
for (const cfg of task.configs) {
|
|
8736
|
-
const f =
|
|
8737
|
-
if (!
|
|
9055
|
+
const f = path29.join(SET, cfg.rep, "get_variable_defs.json");
|
|
9056
|
+
if (!existsSync23(f)) continue;
|
|
8738
9057
|
const text = envelopeFirstTextPart(JSON.parse(readFileSync19(f, "utf8"))) || "{}";
|
|
8739
9058
|
try {
|
|
8740
9059
|
for (const [k, v] of Object.entries(JSON.parse(text))) rawDefs[k] ??= v;
|
|
@@ -8743,8 +9062,8 @@ function buildSegments(task, mode = "fenced") {
|
|
|
8743
9062
|
}
|
|
8744
9063
|
}
|
|
8745
9064
|
const emissionTexts = task.configs.map((cfg) => {
|
|
8746
|
-
const f =
|
|
8747
|
-
return
|
|
9065
|
+
const f = path29.join(SET, cfg.rep, "get_design_context.json");
|
|
9066
|
+
return existsSync23(f) ? envelopeFirstTextPart(JSON.parse(readFileSync19(f, "utf8"))) : "";
|
|
8748
9067
|
});
|
|
8749
9068
|
const { map, note } = cleanTokenMap(rawDefs, emissionTexts);
|
|
8750
9069
|
const defs = JSON.stringify(map, null, 1);
|
|
@@ -8759,9 +9078,9 @@ ${defs}
|
|
|
8759
9078
|
for (const cfg of task.configs) {
|
|
8760
9079
|
const meta = stripFigmaInstructions(repText(SET, cfg.rep, "get_metadata"));
|
|
8761
9080
|
const emission = stripFigmaInstructions(repText(SET, cfg.rep, "get_design_context"));
|
|
8762
|
-
const assets = readdirSync6(
|
|
9081
|
+
const assets = readdirSync6(path29.join(SET, cfg.rep)).filter((f) => f.startsWith("asset-") && f.endsWith(".svg")).map((f) => `asset ${f}:
|
|
8763
9082
|
\`\`\`svg
|
|
8764
|
-
${readFileSync19(
|
|
9083
|
+
${readFileSync19(path29.join(SET, cfg.rep, f), "utf8")}
|
|
8765
9084
|
\`\`\``).join("\n");
|
|
8766
9085
|
parts.push(`
|
|
8767
9086
|
## Config ${cfg.rep} \u2192 ${cfg.component} ${JSON.stringify(cfg.props)}
|
|
@@ -8786,7 +9105,7 @@ Optionally a third block with \`/* FILE: tokens.css */\`.`);
|
|
|
8786
9105
|
} else {
|
|
8787
9106
|
parts.push(`
|
|
8788
9107
|
## Output format
|
|
8789
|
-
Write the COMPLETE files (${task.entry}, styles.css, optional tokens.css) into
|
|
9108
|
+
Write the COMPLETE files (${task.entry}, styles.css, optional tokens.css) into ONE candidate directory: \`tendril-out/${path29.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.`);
|
|
8790
9109
|
}
|
|
8791
9110
|
return parts.join("\n");
|
|
8792
9111
|
}
|
|
@@ -8854,8 +9173,8 @@ var init_adapter = __esm({
|
|
|
8854
9173
|
|
|
8855
9174
|
// packages/generate/src/bundle-emit.ts
|
|
8856
9175
|
import { createHash as createHash4 } from "node:crypto";
|
|
8857
|
-
import { copyFileSync, existsSync as
|
|
8858
|
-
import
|
|
9176
|
+
import { copyFileSync, existsSync as existsSync24, mkdirSync as mkdirSync7, readFileSync as readFileSync20, readdirSync as readdirSync7, rmSync as rmSync3, writeFileSync as writeFileSync11 } from "node:fs";
|
|
9177
|
+
import path30 from "node:path";
|
|
8859
9178
|
function pinFromConfigs(configs) {
|
|
8860
9179
|
const domains = /* @__PURE__ */ new Map();
|
|
8861
9180
|
const kinds = /* @__PURE__ */ new Map();
|
|
@@ -8903,6 +9222,11 @@ function cssFontFamilies(css) {
|
|
|
8903
9222
|
}
|
|
8904
9223
|
return [...out];
|
|
8905
9224
|
}
|
|
9225
|
+
function upstreamAttribution(face) {
|
|
9226
|
+
if (!face.source.startsWith(GOOGLE_FONT_FILE_PREFIX)) return face.source;
|
|
9227
|
+
if (face.servedFamily === void 0) return `the Google Fonts specimen for "${face.family.replace(/[\r\n]+/g, " ")}"`;
|
|
9228
|
+
return `https://fonts.google.com/specimen/${encodeURIComponent(face.servedFamily).replace(/%20/g, "+")}/license`;
|
|
9229
|
+
}
|
|
8906
9230
|
function fontsCssFor(faces, bundleDir, cacheDir = DEFAULT_FONT_CACHE, substitutedFamilies = []) {
|
|
8907
9231
|
const header = [
|
|
8908
9232
|
"/* tendril fonts.css \u2014 the exact faces this bundle was scored with (sha-pinned in",
|
|
@@ -8916,32 +9240,78 @@ function fontsCssFor(faces, bundleDir, cacheDir = DEFAULT_FONT_CACHE, substitute
|
|
|
8916
9240
|
] : []
|
|
8917
9241
|
];
|
|
8918
9242
|
const lines = [];
|
|
9243
|
+
const notices = [];
|
|
9244
|
+
const licenseTexts = /* @__PURE__ */ new Map();
|
|
8919
9245
|
for (const face of faces) {
|
|
8920
|
-
const src =
|
|
8921
|
-
const target = `./fonts/${
|
|
8922
|
-
const format = FONT_FORMATS[
|
|
9246
|
+
const src = path30.join(cacheDir, path30.basename(face.file));
|
|
9247
|
+
const target = `./fonts/${path30.basename(face.file)}`;
|
|
9248
|
+
const format = FONT_FORMATS[path30.extname(face.file).toLowerCase()] ?? "truetype";
|
|
8923
9249
|
const family = face.family.replace(/['\\]/g, "").replace(/\*\//g, "");
|
|
8924
9250
|
const decl = `@font-face { font-family: '${family}'; font-weight: ${face.weight}; font-style: normal; src: url(${target}) format('${format}'); }`;
|
|
8925
|
-
|
|
8926
|
-
|
|
8927
|
-
|
|
8928
|
-
|
|
8929
|
-
|
|
9251
|
+
const license = normalizeFontLicense(face.license);
|
|
9252
|
+
const terms = REDISTRIBUTABLE.get(license);
|
|
9253
|
+
if (terms === void 0) {
|
|
9254
|
+
if (face.source.startsWith("local:")) {
|
|
9255
|
+
lines.push(
|
|
9256
|
+
`/* '${family}' ${face.weight} is a user-licensed face (tendril fonts add; sha256 ${face.sha256.slice(0, 16)}\u2026).`,
|
|
9257
|
+
` Licensed bytes are never copied into bundles \u2014 place your copy at ${target} and uncomment: */`,
|
|
9258
|
+
`/* ${decl} */`
|
|
9259
|
+
);
|
|
9260
|
+
} else if (license === "unknown") {
|
|
9261
|
+
lines.push(
|
|
9262
|
+
`/* '${family}' ${face.weight}: no licence on record (sha256 ${face.sha256.slice(0, 16)}\u2026). That is a gap`,
|
|
9263
|
+
" in the font cache, not a statement about your rights \u2014 Tendril copies font bytes into a",
|
|
9264
|
+
" bundle only under a licence it can name, so this face is declared here instead. Establish",
|
|
9265
|
+
` the licence with \`tendril fonts resolve "${family}" --weights ${face.weight}\``,
|
|
9266
|
+
" (or `tendril fonts resolve --set <recording-dir>`) and re-emit. If it is a face you licence",
|
|
9267
|
+
` rather than one free to redistribute, place your copy at ${target} and uncomment: */`,
|
|
9268
|
+
`/* ${decl} */`
|
|
9269
|
+
);
|
|
9270
|
+
} else {
|
|
9271
|
+
lines.push(
|
|
9272
|
+
`/* '${family}' ${face.weight} is recorded as ${license} (sha256 ${face.sha256.slice(0, 16)}\u2026), which does`,
|
|
9273
|
+
" not permit redistribution \u2014 Tendril never copies such bytes. Place your licensed copy",
|
|
9274
|
+
` at ${target} and uncomment: */`,
|
|
9275
|
+
`/* ${decl} */`
|
|
9276
|
+
);
|
|
9277
|
+
}
|
|
9278
|
+
} else if (existsSync24(src) && createHash4("sha256").update(readFileSync20(src)).digest("hex") === face.sha256) {
|
|
9279
|
+
mkdirSync7(path30.join(bundleDir, "fonts"), { recursive: true });
|
|
9280
|
+
copyFileSync(src, path30.join(bundleDir, "fonts", path30.basename(face.file)));
|
|
9281
|
+
licenseTexts.set(terms.file, terms.text);
|
|
9282
|
+
const upstream = upstreamAttribution(face);
|
|
9283
|
+
notices.push(
|
|
9284
|
+
"",
|
|
9285
|
+
`${face.family.replace(/[\r\n]+/g, " ")} ${face.weight} \u2014 ./${path30.basename(face.file)}`,
|
|
9286
|
+
` licence: ${terms.title} \u2014 full text in ./${terms.file}`,
|
|
9287
|
+
` source: ${face.source}`,
|
|
9288
|
+
` sha256: ${face.sha256}`,
|
|
9289
|
+
" copyright: this face's upstream copyright statement is published with its",
|
|
9290
|
+
` original release \u2014 ${upstream}`
|
|
8930
9291
|
);
|
|
8931
|
-
} else if (existsSync23(src) && createHash4("sha256").update(readFileSync20(src)).digest("hex") === face.sha256) {
|
|
8932
|
-
mkdirSync7(path29.join(bundleDir, "fonts"), { recursive: true });
|
|
8933
|
-
copyFileSync(src, path29.join(bundleDir, "fonts", path29.basename(face.file)));
|
|
8934
9292
|
lines.push(decl);
|
|
8935
9293
|
} else {
|
|
8936
9294
|
lines.push(`/* '${family}' ${face.weight} (sha256 ${face.sha256.slice(0, 16)}\u2026) could not be shipped: cache bytes missing or hash mismatch \u2014 re-run \`tendril fonts resolve\` and re-emit. */`);
|
|
8937
9295
|
}
|
|
8938
9296
|
}
|
|
8939
|
-
|
|
9297
|
+
if (lines.length === 0) return null;
|
|
9298
|
+
if (notices.length > 0) {
|
|
9299
|
+
const fontsDir = path30.join(bundleDir, "fonts");
|
|
9300
|
+
for (const [file, text] of licenseTexts) writeFileSync11(path30.join(fontsDir, file), text);
|
|
9301
|
+
writeFileSync11(path30.join(fontsDir, "NOTICE.txt"), `${[NOTICE_PREAMBLE, ...notices].join("\n")}
|
|
9302
|
+
`);
|
|
9303
|
+
header.push(
|
|
9304
|
+
"/* REDISTRIBUTION: the face files in ./fonts/ are redistributed under the licences",
|
|
9305
|
+
` recorded in ./fonts/NOTICE.txt; full texts in ${[...licenseTexts.keys()].map((f) => `./fonts/${f}`).join(", ")}.`,
|
|
9306
|
+
" Keep NOTICE.txt and the licence texts with the font files wherever they travel. */"
|
|
9307
|
+
);
|
|
9308
|
+
}
|
|
9309
|
+
return `${[...header, ...lines].join("\n")}
|
|
8940
9310
|
`;
|
|
8941
9311
|
}
|
|
8942
9312
|
function countLatticeSymbols(setDir) {
|
|
8943
|
-
const manifestFile =
|
|
8944
|
-
if (
|
|
9313
|
+
const manifestFile = path30.join(setDir, "recording-set.json");
|
|
9314
|
+
if (existsSync24(manifestFile)) {
|
|
8945
9315
|
try {
|
|
8946
9316
|
const lattice = JSON.parse(readFileSync20(manifestFile, "utf8")).latticeNames;
|
|
8947
9317
|
if (lattice !== void 0 && lattice.length > 0) return lattice.length;
|
|
@@ -8949,9 +9319,9 @@ function countLatticeSymbols(setDir) {
|
|
|
8949
9319
|
}
|
|
8950
9320
|
}
|
|
8951
9321
|
const files = [
|
|
8952
|
-
|
|
8953
|
-
...
|
|
8954
|
-
].filter((f) =>
|
|
9322
|
+
path30.join(setDir, "get_metadata.json"),
|
|
9323
|
+
...existsSync24(setDir) ? readdirSync7(setDir).filter((f) => /^get_metadata-.*\.json$/.test(f)).map((f) => path30.join(setDir, f)) : []
|
|
9324
|
+
].filter((f) => existsSync24(f));
|
|
8955
9325
|
if (files.length === 0) return null;
|
|
8956
9326
|
let count = 0;
|
|
8957
9327
|
for (const f of files) {
|
|
@@ -8963,21 +9333,21 @@ function countLatticeSymbols(setDir) {
|
|
|
8963
9333
|
function recordingSetHash(setDir, configs) {
|
|
8964
9334
|
const relPaths = [];
|
|
8965
9335
|
for (const name of ["recording-set.json", "completeness-manifest.json", "get_variable_defs.json", "get_metadata.json"]) {
|
|
8966
|
-
if (
|
|
9336
|
+
if (existsSync24(path30.join(setDir, name))) relPaths.push(name);
|
|
8967
9337
|
}
|
|
8968
9338
|
for (const cfg of configs) {
|
|
8969
9339
|
for (const f of ["get_design_context.json", "get_metadata.json", "get_screenshot.json", "get_variable_defs.json"]) {
|
|
8970
|
-
if (
|
|
9340
|
+
if (existsSync24(path30.join(setDir, cfg.rep, f))) relPaths.push(`${cfg.rep}/${f}`);
|
|
8971
9341
|
}
|
|
8972
|
-
if (
|
|
8973
|
-
for (const asset of readdirSync7(
|
|
9342
|
+
if (existsSync24(path30.join(setDir, cfg.rep))) {
|
|
9343
|
+
for (const asset of readdirSync7(path30.join(setDir, cfg.rep)).filter((f) => f.startsWith("asset-"))) {
|
|
8974
9344
|
relPaths.push(`${cfg.rep}/${asset}`);
|
|
8975
9345
|
}
|
|
8976
9346
|
}
|
|
8977
9347
|
}
|
|
8978
9348
|
return hashRecordingSet(
|
|
8979
9349
|
relPaths,
|
|
8980
|
-
(p) => new Uint8Array(readFileSync20(
|
|
9350
|
+
(p) => new Uint8Array(readFileSync20(path30.join(setDir, p))),
|
|
8981
9351
|
(chunks) => {
|
|
8982
9352
|
const h = createHash4("sha256");
|
|
8983
9353
|
for (const c of chunks) h.update(c);
|
|
@@ -9000,7 +9370,7 @@ function emitBundleV1(opts) {
|
|
|
9000
9370
|
const lattice = countLatticeSymbols(opts.task.set);
|
|
9001
9371
|
const interaction = opts.behaviors.filter((b) => !b.id.startsWith("prelude:"));
|
|
9002
9372
|
const prelude = opts.behaviors.filter((b) => b.id.startsWith("prelude:"));
|
|
9003
|
-
const cssFiles = ["styles.css", "tokens.css"].map((f) =>
|
|
9373
|
+
const cssFiles = ["styles.css", "tokens.css"].map((f) => path30.join(opts.bundleDir, f)).filter((f) => existsSync24(f));
|
|
9004
9374
|
const families = cssFontFamilies(cssFiles.map((f) => readFileSync20(f, "utf8")).join("\n"));
|
|
9005
9375
|
const requiredFonts = requiredFontsManifest(families, opts.fontCacheDir).map((f) => ({
|
|
9006
9376
|
family: f.family,
|
|
@@ -9030,7 +9400,7 @@ function emitBundleV1(opts) {
|
|
|
9030
9400
|
// resolvable via verify's --set override).
|
|
9031
9401
|
path: (() => {
|
|
9032
9402
|
const base = process.env["INIT_CWD"] ?? process.cwd();
|
|
9033
|
-
const rel =
|
|
9403
|
+
const rel = path30.relative(base, opts.task.set);
|
|
9034
9404
|
return rel !== "" && !rel.startsWith("..") ? rel : opts.task.set;
|
|
9035
9405
|
})(),
|
|
9036
9406
|
component: opts.componentName,
|
|
@@ -9054,12 +9424,12 @@ function emitBundleV1(opts) {
|
|
|
9054
9424
|
})
|
|
9055
9425
|
};
|
|
9056
9426
|
const written = [];
|
|
9057
|
-
const manifestPath2 =
|
|
9427
|
+
const manifestPath2 = path30.join(opts.bundleDir, "component.json");
|
|
9058
9428
|
writeFileSync11(manifestPath2, `${JSON.stringify(manifest, null, 2)}
|
|
9059
9429
|
`);
|
|
9060
9430
|
written.push(manifestPath2);
|
|
9061
|
-
const stylesPath =
|
|
9062
|
-
if (
|
|
9431
|
+
const stylesPath = path30.join(opts.bundleDir, "styles.css");
|
|
9432
|
+
if (existsSync24(stylesPath)) {
|
|
9063
9433
|
const comment = cssProvenanceComment({ pass, scored: statuses.length, certified, latticeConfigs: lattice });
|
|
9064
9434
|
const current = readFileSync20(stylesPath, "utf8");
|
|
9065
9435
|
const stripped = current.replace(/^\/\* tendril bundle v\d+ [^]*?\*\/\n/, "");
|
|
@@ -9067,8 +9437,8 @@ function emitBundleV1(opts) {
|
|
|
9067
9437
|
${stripped}`);
|
|
9068
9438
|
written.push(stylesPath);
|
|
9069
9439
|
}
|
|
9070
|
-
const fontsCssPath =
|
|
9071
|
-
rmSync3(
|
|
9440
|
+
const fontsCssPath = path30.join(opts.bundleDir, "fonts.css");
|
|
9441
|
+
rmSync3(path30.join(opts.bundleDir, "fonts"), { recursive: true, force: true });
|
|
9072
9442
|
rmSync3(fontsCssPath, { force: true });
|
|
9073
9443
|
const fontsCss = fontsCssFor(requiredFontsManifest(families, opts.fontCacheDir), opts.bundleDir, opts.fontCacheDir, opts.substitutedFamilies ?? []);
|
|
9074
9444
|
if (fontsCss !== null) {
|
|
@@ -9079,7 +9449,7 @@ ${stripped}`);
|
|
|
9079
9449
|
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\``;
|
|
9080
9450
|
return { manifest, statusLine, written };
|
|
9081
9451
|
}
|
|
9082
|
-
var FONT_FORMATS, BARS;
|
|
9452
|
+
var FONT_FORMATS, BARS, NOTICE_PREAMBLE, OFL_1_1_TEXT, UFL_1_0_TEXT, APACHE_2_0_TEXT, REDISTRIBUTABLE;
|
|
9083
9453
|
var init_bundle_emit = __esm({
|
|
9084
9454
|
"packages/generate/src/bundle-emit.ts"() {
|
|
9085
9455
|
"use strict";
|
|
@@ -9091,6 +9461,408 @@ var init_bundle_emit = __esm({
|
|
|
9091
9461
|
pass: { sim: 0.95, ink: 0.95 },
|
|
9092
9462
|
cert: { sim: 0.97, ink: 0.95 }
|
|
9093
9463
|
};
|
|
9464
|
+
NOTICE_PREAMBLE = [
|
|
9465
|
+
"Font redistribution notice \u2014 tendril bundle",
|
|
9466
|
+
"===========================================",
|
|
9467
|
+
"",
|
|
9468
|
+
"The font files in this directory were copied into the bundle because the",
|
|
9469
|
+
"licence they are served under permits redistribution. Each entry below names",
|
|
9470
|
+
"that licence, the exact file the bytes came from, and the sha256 the bundle",
|
|
9471
|
+
"was scored against. Full licence texts sit beside this file \u2014 keep them, and",
|
|
9472
|
+
"this notice, with the font files wherever they travel."
|
|
9473
|
+
].join("\n");
|
|
9474
|
+
OFL_1_1_TEXT = `-----------------------------------------------------------
|
|
9475
|
+
SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
|
|
9476
|
+
-----------------------------------------------------------
|
|
9477
|
+
|
|
9478
|
+
PREAMBLE
|
|
9479
|
+
The goals of the Open Font License (OFL) are to stimulate worldwide
|
|
9480
|
+
development of collaborative font projects, to support the font creation
|
|
9481
|
+
efforts of academic and linguistic communities, and to provide a free and
|
|
9482
|
+
open framework in which fonts may be shared and improved in partnership
|
|
9483
|
+
with others.
|
|
9484
|
+
|
|
9485
|
+
The OFL allows the licensed fonts to be used, studied, modified and
|
|
9486
|
+
redistributed freely as long as they are not sold by themselves. The
|
|
9487
|
+
fonts, including any derivative works, can be bundled, embedded,
|
|
9488
|
+
redistributed and/or sold with any software provided that any reserved
|
|
9489
|
+
names are not used by derivative works. The fonts and derivatives,
|
|
9490
|
+
however, cannot be released under any other type of license. The
|
|
9491
|
+
requirement for fonts to remain under this license does not apply
|
|
9492
|
+
to any document created using the fonts or their derivatives.
|
|
9493
|
+
|
|
9494
|
+
DEFINITIONS
|
|
9495
|
+
"Font Software" refers to the set of files released by the Copyright
|
|
9496
|
+
Holder(s) under this license and clearly marked as such. This may
|
|
9497
|
+
include source files, build scripts and documentation.
|
|
9498
|
+
|
|
9499
|
+
"Reserved Font Name" refers to any names specified as such after the
|
|
9500
|
+
copyright statement(s).
|
|
9501
|
+
|
|
9502
|
+
"Original Version" refers to the collection of Font Software components as
|
|
9503
|
+
distributed by the Copyright Holder(s).
|
|
9504
|
+
|
|
9505
|
+
"Modified Version" refers to any derivative made by adding to, deleting,
|
|
9506
|
+
or substituting -- in part or in whole -- any of the components of the
|
|
9507
|
+
Original Version, by changing formats or by porting the Font Software to a
|
|
9508
|
+
new environment.
|
|
9509
|
+
|
|
9510
|
+
"Author" refers to any designer, engineer, programmer, technical
|
|
9511
|
+
writer or other person who contributed to the Font Software.
|
|
9512
|
+
|
|
9513
|
+
PERMISSION & CONDITIONS
|
|
9514
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
|
9515
|
+
a copy of the Font Software, to use, study, copy, merge, embed, modify,
|
|
9516
|
+
redistribute, and sell modified and unmodified copies of the Font
|
|
9517
|
+
Software, subject to the following conditions:
|
|
9518
|
+
|
|
9519
|
+
1) Neither the Font Software nor any of its individual components,
|
|
9520
|
+
in Original or Modified Versions, may be sold by itself.
|
|
9521
|
+
|
|
9522
|
+
2) Original or Modified Versions of the Font Software may be bundled,
|
|
9523
|
+
redistributed and/or sold with any software, provided that each copy
|
|
9524
|
+
contains the above copyright notice and this license. These can be
|
|
9525
|
+
included either as stand-alone text files, human-readable headers or
|
|
9526
|
+
in the appropriate machine-readable metadata fields within text or
|
|
9527
|
+
binary files as long as those fields can be easily viewed by the user.
|
|
9528
|
+
|
|
9529
|
+
3) No Modified Version of the Font Software may use the Reserved Font
|
|
9530
|
+
Name(s) unless explicit written permission is granted by the corresponding
|
|
9531
|
+
Copyright Holder. This restriction only applies to the primary font name as
|
|
9532
|
+
presented to the users.
|
|
9533
|
+
|
|
9534
|
+
4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
|
|
9535
|
+
Software shall not be used to promote, endorse or advertise any
|
|
9536
|
+
Modified Version, except to acknowledge the contribution(s) of the
|
|
9537
|
+
Copyright Holder(s) and the Author(s) or with their explicit written
|
|
9538
|
+
permission.
|
|
9539
|
+
|
|
9540
|
+
5) The Font Software, modified or unmodified, in part or in whole,
|
|
9541
|
+
must be distributed entirely under this license, and must not be
|
|
9542
|
+
distributed under any other license. The requirement for fonts to
|
|
9543
|
+
remain under this license does not apply to any document created
|
|
9544
|
+
using the Font Software.
|
|
9545
|
+
|
|
9546
|
+
TERMINATION
|
|
9547
|
+
This license becomes null and void if any of the above conditions are
|
|
9548
|
+
not met.
|
|
9549
|
+
|
|
9550
|
+
DISCLAIMER
|
|
9551
|
+
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
|
9552
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
|
|
9553
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
|
|
9554
|
+
OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
|
|
9555
|
+
COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
|
|
9556
|
+
INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
|
|
9557
|
+
DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
|
9558
|
+
FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
|
|
9559
|
+
OTHER DEALINGS IN THE FONT SOFTWARE.
|
|
9560
|
+
`;
|
|
9561
|
+
UFL_1_0_TEXT = `-------------------------------
|
|
9562
|
+
UBUNTU FONT LICENCE Version 1.0
|
|
9563
|
+
-------------------------------
|
|
9564
|
+
|
|
9565
|
+
PREAMBLE
|
|
9566
|
+
This licence allows the licensed fonts to be used, studied, modified and
|
|
9567
|
+
redistributed freely. The fonts, including any derivative works, can be
|
|
9568
|
+
bundled, embedded, and redistributed provided the terms of this licence
|
|
9569
|
+
are met. The fonts and derivatives, however, cannot be released under
|
|
9570
|
+
any other licence. The requirement for fonts to remain under this
|
|
9571
|
+
licence does not require any document created using the fonts or their
|
|
9572
|
+
derivatives to be published under this licence, as long as the primary
|
|
9573
|
+
purpose of the document is not to be a vehicle for the distribution of
|
|
9574
|
+
the fonts.
|
|
9575
|
+
|
|
9576
|
+
DEFINITIONS
|
|
9577
|
+
"Font Software" refers to the set of files released by the Copyright
|
|
9578
|
+
Holder(s) under this licence and clearly marked as such. This may
|
|
9579
|
+
include source files, build scripts and documentation.
|
|
9580
|
+
|
|
9581
|
+
"Original Version" refers to the collection of Font Software components
|
|
9582
|
+
as received under this licence.
|
|
9583
|
+
|
|
9584
|
+
"Modified Version" refers to any derivative made by adding to, deleting,
|
|
9585
|
+
or substituting -- in part or in whole -- any of the components of the
|
|
9586
|
+
Original Version, by changing formats or by porting the Font Software to
|
|
9587
|
+
a new environment.
|
|
9588
|
+
|
|
9589
|
+
"Copyright Holder(s)" refers to all individuals and companies who have a
|
|
9590
|
+
copyright ownership of the Font Software.
|
|
9591
|
+
|
|
9592
|
+
"Substantially Changed" refers to Modified Versions which can be easily
|
|
9593
|
+
identified as dissimilar to the Font Software by users of the Font
|
|
9594
|
+
Software comparing the Original Version with the Modified Version.
|
|
9595
|
+
|
|
9596
|
+
To "Propagate" a work means to do anything with it that, without
|
|
9597
|
+
permission, would make you directly or secondarily liable for
|
|
9598
|
+
infringement under applicable copyright law, except executing it on a
|
|
9599
|
+
computer or modifying a private copy. Propagation includes copying,
|
|
9600
|
+
distribution (with or without modification and with or without charging
|
|
9601
|
+
a redistribution fee), making available to the public, and in some
|
|
9602
|
+
countries other activities as well.
|
|
9603
|
+
|
|
9604
|
+
PERMISSION & CONDITIONS
|
|
9605
|
+
This licence does not grant any rights under trademark law and all such
|
|
9606
|
+
rights are reserved.
|
|
9607
|
+
|
|
9608
|
+
Permission is hereby granted, free of charge, to any person obtaining a
|
|
9609
|
+
copy of the Font Software, to propagate the Font Software, subject to
|
|
9610
|
+
the below conditions:
|
|
9611
|
+
|
|
9612
|
+
1) Each copy of the Font Software must contain the above copyright
|
|
9613
|
+
notice and this licence. These can be included either as stand-alone
|
|
9614
|
+
text files, human-readable headers or in the appropriate machine-
|
|
9615
|
+
readable metadata fields within text or binary files as long as those
|
|
9616
|
+
fields can be easily viewed by the user.
|
|
9617
|
+
|
|
9618
|
+
2) The font name complies with the following:
|
|
9619
|
+
(a) The Original Version must retain its name, unmodified.
|
|
9620
|
+
(b) Modified Versions which are Substantially Changed must be renamed to
|
|
9621
|
+
avoid use of the name of the Original Version or similar names entirely.
|
|
9622
|
+
(c) Modified Versions which are not Substantially Changed must be
|
|
9623
|
+
renamed to both (i) retain the name of the Original Version and (ii) add
|
|
9624
|
+
additional naming elements to distinguish the Modified Version from the
|
|
9625
|
+
Original Version. The name of such Modified Versions must be the name of
|
|
9626
|
+
the Original Version, with "derivative X" where X represents the name of
|
|
9627
|
+
the new work, appended to that name.
|
|
9628
|
+
|
|
9629
|
+
3) The name(s) of the Copyright Holder(s) and any contributor to the
|
|
9630
|
+
Font Software shall not be used to promote, endorse or advertise any
|
|
9631
|
+
Modified Version, except (i) as required by this licence, (ii) to
|
|
9632
|
+
acknowledge the contribution(s) of the Copyright Holder(s) or (iii) with
|
|
9633
|
+
their explicit written permission.
|
|
9634
|
+
|
|
9635
|
+
4) The Font Software, modified or unmodified, in part or in whole, must
|
|
9636
|
+
be distributed entirely under this licence, and must not be distributed
|
|
9637
|
+
under any other licence. The requirement for fonts to remain under this
|
|
9638
|
+
licence does not affect any document created using the Font Software,
|
|
9639
|
+
except any version of the Font Software extracted from a document
|
|
9640
|
+
created using the Font Software may only be distributed under this
|
|
9641
|
+
licence.
|
|
9642
|
+
|
|
9643
|
+
TERMINATION
|
|
9644
|
+
This licence becomes null and void if any of the above conditions are
|
|
9645
|
+
not met.
|
|
9646
|
+
|
|
9647
|
+
DISCLAIMER
|
|
9648
|
+
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
|
9649
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
|
|
9650
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT OF
|
|
9651
|
+
COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
|
|
9652
|
+
COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
|
|
9653
|
+
INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
|
|
9654
|
+
DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
|
9655
|
+
FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM OTHER
|
|
9656
|
+
DEALINGS IN THE FONT SOFTWARE.
|
|
9657
|
+
`;
|
|
9658
|
+
APACHE_2_0_TEXT = `
|
|
9659
|
+
Apache License
|
|
9660
|
+
Version 2.0, January 2004
|
|
9661
|
+
http://www.apache.org/licenses/
|
|
9662
|
+
|
|
9663
|
+
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
|
9664
|
+
|
|
9665
|
+
1. Definitions.
|
|
9666
|
+
|
|
9667
|
+
"License" shall mean the terms and conditions for use, reproduction,
|
|
9668
|
+
and distribution as defined by Sections 1 through 9 of this document.
|
|
9669
|
+
|
|
9670
|
+
"Licensor" shall mean the copyright owner or entity authorized by
|
|
9671
|
+
the copyright owner that is granting the License.
|
|
9672
|
+
|
|
9673
|
+
"Legal Entity" shall mean the union of the acting entity and all
|
|
9674
|
+
other entities that control, are controlled by, or are under common
|
|
9675
|
+
control with that entity. For the purposes of this definition,
|
|
9676
|
+
"control" means (i) the power, direct or indirect, to cause the
|
|
9677
|
+
direction or management of such entity, whether by contract or
|
|
9678
|
+
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
|
9679
|
+
outstanding shares, or (iii) beneficial ownership of such entity.
|
|
9680
|
+
|
|
9681
|
+
"You" (or "Your") shall mean an individual or Legal Entity
|
|
9682
|
+
exercising permissions granted by this License.
|
|
9683
|
+
|
|
9684
|
+
"Source" form shall mean the preferred form for making modifications,
|
|
9685
|
+
including but not limited to software source code, documentation
|
|
9686
|
+
source, and configuration files.
|
|
9687
|
+
|
|
9688
|
+
"Object" form shall mean any form resulting from mechanical
|
|
9689
|
+
transformation or translation of a Source form, including but
|
|
9690
|
+
not limited to compiled object code, generated documentation,
|
|
9691
|
+
and conversions to other media types.
|
|
9692
|
+
|
|
9693
|
+
"Work" shall mean the work of authorship, whether in Source or
|
|
9694
|
+
Object form, made available under the License, as indicated by a
|
|
9695
|
+
copyright notice that is included in or attached to the work
|
|
9696
|
+
(an example is provided in the Appendix below).
|
|
9697
|
+
|
|
9698
|
+
"Derivative Works" shall mean any work, whether in Source or Object
|
|
9699
|
+
form, that is based on (or derived from) the Work and for which the
|
|
9700
|
+
editorial revisions, annotations, elaborations, or other modifications
|
|
9701
|
+
represent, as a whole, an original work of authorship. For the purposes
|
|
9702
|
+
of this License, Derivative Works shall not include works that remain
|
|
9703
|
+
separable from, or merely link (or bind by name) to the interfaces of,
|
|
9704
|
+
the Work and Derivative Works thereof.
|
|
9705
|
+
|
|
9706
|
+
"Contribution" shall mean any work of authorship, including
|
|
9707
|
+
the original version of the Work and any modifications or additions
|
|
9708
|
+
to that Work or Derivative Works thereof, that is intentionally
|
|
9709
|
+
submitted to Licensor for inclusion in the Work by the copyright owner
|
|
9710
|
+
or by an individual or Legal Entity authorized to submit on behalf of
|
|
9711
|
+
the copyright owner. For the purposes of this definition, "submitted"
|
|
9712
|
+
means any form of electronic, verbal, or written communication sent
|
|
9713
|
+
to the Licensor or its representatives, including but not limited to
|
|
9714
|
+
communication on electronic mailing lists, source code control systems,
|
|
9715
|
+
and issue tracking systems that are managed by, or on behalf of, the
|
|
9716
|
+
Licensor for the purpose of discussing and improving the Work, but
|
|
9717
|
+
excluding communication that is conspicuously marked or otherwise
|
|
9718
|
+
designated in writing by the copyright owner as "Not a Contribution."
|
|
9719
|
+
|
|
9720
|
+
"Contributor" shall mean Licensor and any individual or Legal Entity
|
|
9721
|
+
on behalf of whom a Contribution has been received by Licensor and
|
|
9722
|
+
subsequently incorporated within the Work.
|
|
9723
|
+
|
|
9724
|
+
2. Grant of Copyright License. Subject to the terms and conditions of
|
|
9725
|
+
this License, each Contributor hereby grants to You a perpetual,
|
|
9726
|
+
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
|
9727
|
+
copyright license to reproduce, prepare Derivative Works of,
|
|
9728
|
+
publicly display, publicly perform, sublicense, and distribute the
|
|
9729
|
+
Work and such Derivative Works in Source or Object form.
|
|
9730
|
+
|
|
9731
|
+
3. Grant of Patent License. Subject to the terms and conditions of
|
|
9732
|
+
this License, each Contributor hereby grants to You a perpetual,
|
|
9733
|
+
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
|
9734
|
+
(except as stated in this section) patent license to make, have made,
|
|
9735
|
+
use, offer to sell, sell, import, and otherwise transfer the Work,
|
|
9736
|
+
where such license applies only to those patent claims licensable
|
|
9737
|
+
by such Contributor that are necessarily infringed by their
|
|
9738
|
+
Contribution(s) alone or by combination of their Contribution(s)
|
|
9739
|
+
with the Work to which such Contribution(s) was submitted. If You
|
|
9740
|
+
institute patent litigation against any entity (including a
|
|
9741
|
+
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
|
9742
|
+
or a Contribution incorporated within the Work constitutes direct
|
|
9743
|
+
or contributory patent infringement, then any patent licenses
|
|
9744
|
+
granted to You under this License for that Work shall terminate
|
|
9745
|
+
as of the date such litigation is filed.
|
|
9746
|
+
|
|
9747
|
+
4. Redistribution. You may reproduce and distribute copies of the
|
|
9748
|
+
Work or Derivative Works thereof in any medium, with or without
|
|
9749
|
+
modifications, and in Source or Object form, provided that You
|
|
9750
|
+
meet the following conditions:
|
|
9751
|
+
|
|
9752
|
+
(a) You must give any other recipients of the Work or
|
|
9753
|
+
Derivative Works a copy of this License; and
|
|
9754
|
+
|
|
9755
|
+
(b) You must cause any modified files to carry prominent notices
|
|
9756
|
+
stating that You changed the files; and
|
|
9757
|
+
|
|
9758
|
+
(c) You must retain, in the Source form of any Derivative Works
|
|
9759
|
+
that You distribute, all copyright, patent, trademark, and
|
|
9760
|
+
attribution notices from the Source form of the Work,
|
|
9761
|
+
excluding those notices that do not pertain to any part of
|
|
9762
|
+
the Derivative Works; and
|
|
9763
|
+
|
|
9764
|
+
(d) If the Work includes a "NOTICE" text file as part of its
|
|
9765
|
+
distribution, then any Derivative Works that You distribute must
|
|
9766
|
+
include a readable copy of the attribution notices contained
|
|
9767
|
+
within such NOTICE file, excluding those notices that do not
|
|
9768
|
+
pertain to any part of the Derivative Works, in at least one
|
|
9769
|
+
of the following places: within a NOTICE text file distributed
|
|
9770
|
+
as part of the Derivative Works; within the Source form or
|
|
9771
|
+
documentation, if provided along with the Derivative Works; or,
|
|
9772
|
+
within a display generated by the Derivative Works, if and
|
|
9773
|
+
wherever such third-party notices normally appear. The contents
|
|
9774
|
+
of the NOTICE file are for informational purposes only and
|
|
9775
|
+
do not modify the License. You may add Your own attribution
|
|
9776
|
+
notices within Derivative Works that You distribute, alongside
|
|
9777
|
+
or as an addendum to the NOTICE text from the Work, provided
|
|
9778
|
+
that such additional attribution notices cannot be construed
|
|
9779
|
+
as modifying the License.
|
|
9780
|
+
|
|
9781
|
+
You may add Your own copyright statement to Your modifications and
|
|
9782
|
+
may provide additional or different license terms and conditions
|
|
9783
|
+
for use, reproduction, or distribution of Your modifications, or
|
|
9784
|
+
for any such Derivative Works as a whole, provided Your use,
|
|
9785
|
+
reproduction, and distribution of the Work otherwise complies with
|
|
9786
|
+
the conditions stated in this License.
|
|
9787
|
+
|
|
9788
|
+
5. Submission of Contributions. Unless You explicitly state otherwise,
|
|
9789
|
+
any Contribution intentionally submitted for inclusion in the Work
|
|
9790
|
+
by You to the Licensor shall be under the terms and conditions of
|
|
9791
|
+
this License, without any additional terms or conditions.
|
|
9792
|
+
Notwithstanding the above, nothing herein shall supersede or modify
|
|
9793
|
+
the terms of any separate license agreement you may have executed
|
|
9794
|
+
with Licensor regarding such Contributions.
|
|
9795
|
+
|
|
9796
|
+
6. Trademarks. This License does not grant permission to use the trade
|
|
9797
|
+
names, trademarks, service marks, or product names of the Licensor,
|
|
9798
|
+
except as required for reasonable and customary use in describing the
|
|
9799
|
+
origin of the Work and reproducing the content of the NOTICE file.
|
|
9800
|
+
|
|
9801
|
+
7. Disclaimer of Warranty. Unless required by applicable law or
|
|
9802
|
+
agreed to in writing, Licensor provides the Work (and each
|
|
9803
|
+
Contributor provides its Contributions) on an "AS IS" BASIS,
|
|
9804
|
+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
|
9805
|
+
implied, including, without limitation, any warranties or conditions
|
|
9806
|
+
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
|
9807
|
+
PARTICULAR PURPOSE. You are solely responsible for determining the
|
|
9808
|
+
appropriateness of using or redistributing the Work and assume any
|
|
9809
|
+
risks associated with Your exercise of permissions under this License.
|
|
9810
|
+
|
|
9811
|
+
8. Limitation of Liability. In no event and under no legal theory,
|
|
9812
|
+
whether in tort (including negligence), contract, or otherwise,
|
|
9813
|
+
unless required by applicable law (such as deliberate and grossly
|
|
9814
|
+
negligent acts) or agreed to in writing, shall any Contributor be
|
|
9815
|
+
liable to You for damages, including any direct, indirect, special,
|
|
9816
|
+
incidental, or consequential damages of any character arising as a
|
|
9817
|
+
result of this License or out of the use or inability to use the
|
|
9818
|
+
Work (including but not limited to damages for loss of goodwill,
|
|
9819
|
+
work stoppage, computer failure or malfunction, or any and all
|
|
9820
|
+
other commercial damages or losses), even if such Contributor
|
|
9821
|
+
has been advised of the possibility of such damages.
|
|
9822
|
+
|
|
9823
|
+
9. Accepting Warranty or Additional Liability. While redistributing
|
|
9824
|
+
the Work or Derivative Works thereof, You may choose to offer,
|
|
9825
|
+
and charge a fee for, acceptance of support, warranty, indemnity,
|
|
9826
|
+
or other liability obligations and/or rights consistent with this
|
|
9827
|
+
License. However, in accepting such obligations, You may act only
|
|
9828
|
+
on Your own behalf and on Your sole responsibility, not on behalf
|
|
9829
|
+
of any other Contributor, and only if You agree to indemnify,
|
|
9830
|
+
defend, and hold each Contributor harmless for any liability
|
|
9831
|
+
incurred by, or claims asserted against, such Contributor by reason
|
|
9832
|
+
of your accepting any such warranty or additional liability.
|
|
9833
|
+
|
|
9834
|
+
END OF TERMS AND CONDITIONS
|
|
9835
|
+
|
|
9836
|
+
APPENDIX: How to apply the Apache License to your work.
|
|
9837
|
+
|
|
9838
|
+
To apply the Apache License to your work, attach the following
|
|
9839
|
+
boilerplate notice, with the fields enclosed by brackets "[]"
|
|
9840
|
+
replaced with your own identifying information. (Don't include
|
|
9841
|
+
the brackets!) The text should be enclosed in the appropriate
|
|
9842
|
+
comment syntax for the file format. We also recommend that a
|
|
9843
|
+
file or class name and description of purpose be included on the
|
|
9844
|
+
same "printed page" as the copyright notice for easier
|
|
9845
|
+
identification within third-party archives.
|
|
9846
|
+
|
|
9847
|
+
Copyright [yyyy] [name of copyright owner]
|
|
9848
|
+
|
|
9849
|
+
Licensed under the Apache License, Version 2.0 (the "License");
|
|
9850
|
+
you may not use this file except in compliance with the License.
|
|
9851
|
+
You may obtain a copy of the License at
|
|
9852
|
+
|
|
9853
|
+
http://www.apache.org/licenses/LICENSE-2.0
|
|
9854
|
+
|
|
9855
|
+
Unless required by applicable law or agreed to in writing, software
|
|
9856
|
+
distributed under the License is distributed on an "AS IS" BASIS,
|
|
9857
|
+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
9858
|
+
See the License for the specific language governing permissions and
|
|
9859
|
+
limitations under the License.
|
|
9860
|
+
`;
|
|
9861
|
+
REDISTRIBUTABLE = /* @__PURE__ */ new Map([
|
|
9862
|
+
["OFL-1.1", { title: "SIL Open Font License 1.1", file: "LICENSE-OFL-1.1.txt", text: OFL_1_1_TEXT }],
|
|
9863
|
+
["Apache-2.0", { title: "Apache License 2.0", file: "LICENSE-Apache-2.0.txt", text: APACHE_2_0_TEXT }],
|
|
9864
|
+
["UFL-1.0", { title: "Ubuntu Font Licence 1.0", file: "LICENSE-UFL-1.0.txt", text: UFL_1_0_TEXT }]
|
|
9865
|
+
]);
|
|
9094
9866
|
}
|
|
9095
9867
|
});
|
|
9096
9868
|
|
|
@@ -9120,8 +9892,8 @@ __export(fonts_exports, {
|
|
|
9120
9892
|
runFontsResolveSet: () => runFontsResolveSet,
|
|
9121
9893
|
runFontsStatus: () => runFontsStatus
|
|
9122
9894
|
});
|
|
9123
|
-
import { existsSync as
|
|
9124
|
-
import
|
|
9895
|
+
import { existsSync as existsSync25, readFileSync as readFileSync21 } from "node:fs";
|
|
9896
|
+
import path31 from "node:path";
|
|
9125
9897
|
async function runFontsResolve(opts) {
|
|
9126
9898
|
const result = await resolveFonts(opts.family, opts.weights, opts.cacheDir);
|
|
9127
9899
|
emitData(opts, result, () => {
|
|
@@ -9136,7 +9908,7 @@ async function runFontsResolve(opts) {
|
|
|
9136
9908
|
}
|
|
9137
9909
|
}
|
|
9138
9910
|
async function runFontsResolveSet(opts) {
|
|
9139
|
-
const setDir =
|
|
9911
|
+
const setDir = path31.resolve(process.env["INIT_CWD"] ?? process.cwd(), opts.set);
|
|
9140
9912
|
let needs = [];
|
|
9141
9913
|
try {
|
|
9142
9914
|
needs = recordedFontNeeds(setDir);
|
|
@@ -9144,57 +9916,81 @@ async function runFontsResolveSet(opts) {
|
|
|
9144
9916
|
fail(opts, ExitCode.InputValidation, {
|
|
9145
9917
|
error: `cannot read font needs from ${setDir}: ${err instanceof Error ? err.message.split("\n")[0] : String(err)}`,
|
|
9146
9918
|
code: "font-needs-unreadable",
|
|
9147
|
-
remediation:
|
|
9919
|
+
remediation: `Point --set at a directory recorded with \`${tendrilCommand("record plan")}\` (it carries recording-set.json), or resolve a family explicitly: ${tendrilCommand('fonts resolve "<Family>" --weights 400 500 600')}.`
|
|
9148
9920
|
});
|
|
9149
9921
|
}
|
|
9150
9922
|
if (needs.length === 0) {
|
|
9151
9923
|
fail(opts, ExitCode.InputValidation, {
|
|
9152
9924
|
error: `recording set ${setDir} declares no detectable font families`,
|
|
9153
9925
|
code: "font-needs-empty",
|
|
9154
|
-
remediation:
|
|
9926
|
+
remediation: `Resolve the family the design uses explicitly: ${tendrilCommand('fonts resolve "<Family>" --weights 400 500 600')}.`
|
|
9155
9927
|
});
|
|
9156
9928
|
}
|
|
9157
9929
|
const inCache = new Set(resolvedFontFamilies(opts.cacheDir).flatMap((f) => f.weights.map((w) => `${f.family.toLowerCase()}:${w}`)));
|
|
9158
|
-
const
|
|
9930
|
+
const unstamped = new Map(facesMissingLicense(opts.cacheDir).map((f) => [`${f.family.toLowerCase()}:${f.weight}`, f.sha256]));
|
|
9931
|
+
const cached2 = [];
|
|
9159
9932
|
const resolved = [];
|
|
9160
9933
|
const failures = [];
|
|
9934
|
+
const relicensed = [];
|
|
9935
|
+
const unlicensed = [];
|
|
9936
|
+
const byteDrift = [];
|
|
9161
9937
|
for (const need of needs) {
|
|
9162
9938
|
const missing = [];
|
|
9939
|
+
const toStamp = [];
|
|
9163
9940
|
for (const w of need.weights) {
|
|
9164
|
-
|
|
9165
|
-
|
|
9941
|
+
const key = `${need.family.toLowerCase()}:${w}`;
|
|
9942
|
+
if (!inCache.has(key)) missing.push(w);
|
|
9943
|
+
else if (unstamped.has(key)) toStamp.push(w);
|
|
9944
|
+
else cached2.push({ family: need.family, weight: w });
|
|
9945
|
+
}
|
|
9946
|
+
if (missing.length > 0) {
|
|
9947
|
+
const result = await resolveFonts(need.family, missing, opts.cacheDir);
|
|
9948
|
+
resolved.push(...result.resolved);
|
|
9949
|
+
failures.push(...result.failures);
|
|
9950
|
+
}
|
|
9951
|
+
if (toStamp.length > 0) {
|
|
9952
|
+
const result = await resolveFonts(need.family, toStamp, opts.cacheDir);
|
|
9953
|
+
for (const face of result.resolved) {
|
|
9954
|
+
if (face.license === "unknown") unlicensed.push({ family: face.family, weight: face.weight, reason: "the foundry's metadata named no licence for this family" });
|
|
9955
|
+
else relicensed.push({ family: face.family, weight: face.weight, license: face.license });
|
|
9956
|
+
if (unstamped.get(`${face.family.toLowerCase()}:${face.weight}`) !== face.sha256) byteDrift.push({ family: face.family, weight: face.weight });
|
|
9957
|
+
}
|
|
9958
|
+
unlicensed.push(...result.failures);
|
|
9166
9959
|
}
|
|
9167
|
-
if (missing.length === 0) continue;
|
|
9168
|
-
const result = await resolveFonts(need.family, missing, opts.cacheDir);
|
|
9169
|
-
resolved.push(...result.resolved);
|
|
9170
|
-
failures.push(...result.failures);
|
|
9171
9960
|
}
|
|
9172
|
-
emitData(opts, { set: setDir, needs, cached, resolved, failures }, () => {
|
|
9961
|
+
emitData(opts, { set: setDir, needs, cached: cached2, resolved, relicensed, unlicensed, failures }, () => {
|
|
9173
9962
|
process.stdout.write(`set declares: ${needs.map((n) => `${n.family} (${n.weights.join(", ")})`).join(" \xB7 ")}
|
|
9174
9963
|
`);
|
|
9175
|
-
for (const f of
|
|
9964
|
+
for (const f of cached2) process.stdout.write(`cached ${f.family} ${f.weight}
|
|
9176
9965
|
`);
|
|
9177
9966
|
for (const f of resolved) process.stdout.write(`resolved ${f.family} ${f.weight} (${f.sha256.slice(0, 12)}\u2026)
|
|
9967
|
+
`);
|
|
9968
|
+
for (const f of relicensed) process.stdout.write(`licence ${f.family} ${f.weight}: ${f.license} \u2014 recorded now; bundles may ship these bytes with the licence text
|
|
9969
|
+
`);
|
|
9970
|
+
for (const f of unlicensed) process.stdout.write(`licence ${f.family} ${f.weight}: still not on record (${f.reason}) \u2014 the cached face renders as always; bundles declare it instead of copying it
|
|
9178
9971
|
`);
|
|
9179
9972
|
for (const f of failures) process.stdout.write(`FAILED ${f.family} ${f.weight}: ${f.reason}
|
|
9180
9973
|
`);
|
|
9181
9974
|
});
|
|
9975
|
+
if (byteDrift.length > 0) {
|
|
9976
|
+
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`);
|
|
9977
|
+
}
|
|
9182
9978
|
if (failures.length > 0) {
|
|
9183
9979
|
warn(opts, `${failures.length} face(s) unresolved \u2014 verification will refuse to score under substitution`);
|
|
9184
9980
|
process.exitCode = ExitCode.FontsUnproven;
|
|
9185
9981
|
}
|
|
9186
9982
|
}
|
|
9187
9983
|
function runFontsStatus(opts) {
|
|
9188
|
-
const manifestPath2 =
|
|
9189
|
-
if (!
|
|
9984
|
+
const manifestPath2 = path31.join(opts.cacheDir, "manifest.json");
|
|
9985
|
+
if (!existsSync25(manifestPath2)) {
|
|
9190
9986
|
fail(opts, ExitCode.FontsUnproven, {
|
|
9191
9987
|
error: `no font cache at ${opts.cacheDir}`,
|
|
9192
9988
|
code: "fonts-unresolved",
|
|
9193
|
-
remediation:
|
|
9989
|
+
remediation: `Run \`${tendrilCommand("fonts resolve --set <recording-dir>")}\` (or \`${tendrilCommand('fonts resolve "<Family>" --weights 400 500 600')}\`) first.`
|
|
9194
9990
|
});
|
|
9195
9991
|
}
|
|
9196
9992
|
const faces = JSON.parse(readFileSync21(manifestPath2, "utf8"));
|
|
9197
|
-
const lockVerdicts = opts.lock !== void 0 ? checkFontLock(
|
|
9993
|
+
const lockVerdicts = opts.lock !== void 0 ? checkFontLock(path31.resolve(opts.lock), opts.cacheDir) : null;
|
|
9198
9994
|
emitData(opts, { faces, lockVerdicts }, () => {
|
|
9199
9995
|
for (const f of faces) process.stdout.write(`cached ${f.family} ${f.weight} (${f.sha256.slice(0, 12)}\u2026)
|
|
9200
9996
|
`);
|
|
@@ -9225,7 +10021,10 @@ function runFontsAdd(opts) {
|
|
|
9225
10021
|
fail(opts, ExitCode.InputValidation, {
|
|
9226
10022
|
error: err instanceof Error ? err.message : String(err),
|
|
9227
10023
|
code: "font-add-failed",
|
|
9228
|
-
|
|
10024
|
+
// The face file is the user's own licensed asset: this command
|
|
10025
|
+
// can name every argument but that one, so it stays a marked
|
|
10026
|
+
// blank rather than a fabricated path that looks real.
|
|
10027
|
+
remediation: `Pass a font file you are licensed to use, by absolute path: ${tendrilCommand(`fonts add "${opts.family}" ${opts.weight} <absolute-path-to-face.woff2>`)}`
|
|
9229
10028
|
});
|
|
9230
10029
|
}
|
|
9231
10030
|
emitData(opts, face, () => {
|
|
@@ -9240,22 +10039,26 @@ var init_fonts = __esm({
|
|
|
9240
10039
|
init_src4();
|
|
9241
10040
|
init_src7();
|
|
9242
10041
|
init_output();
|
|
10042
|
+
init_invocation();
|
|
9243
10043
|
}
|
|
9244
10044
|
});
|
|
9245
10045
|
|
|
9246
10046
|
// packages/cli/src/font-guidance.ts
|
|
10047
|
+
import path32 from "node:path";
|
|
9247
10048
|
function fontsUnprovenRemediation(setDir) {
|
|
9248
|
-
|
|
10049
|
+
const set = setDir === void 0 ? void 0 : path32.resolve(setDir);
|
|
10050
|
+
if (set !== void 0) {
|
|
9249
10051
|
try {
|
|
9250
|
-
const needs = recordedFontNeeds(
|
|
10052
|
+
const needs = recordedFontNeeds(set);
|
|
9251
10053
|
if (needs.length > 0) {
|
|
9252
10054
|
const families = needs.map((n) => `"${n.family}"`).join(", ");
|
|
9253
|
-
return `Run
|
|
10055
|
+
return `Run \`${tendrilCommand(`fonts resolve --set ${set}`)}\` \u2014 the recording declares ${families}; faces land in ${DEFAULT_FONT_CACHE}.`;
|
|
9254
10056
|
}
|
|
9255
10057
|
} catch {
|
|
9256
10058
|
}
|
|
9257
10059
|
}
|
|
9258
|
-
|
|
10060
|
+
const target = set ?? "<recording-dir>";
|
|
10061
|
+
return `Run \`${tendrilCommand(`fonts resolve --set ${target}`)}\` (or \`${tendrilCommand('fonts resolve "<Family>" --weights 400 500 600')}\`) to populate the font cache \u2014 the cache read is ${DEFAULT_FONT_CACHE}.`;
|
|
9259
10062
|
}
|
|
9260
10063
|
function taskFontFamilies(setDir) {
|
|
9261
10064
|
try {
|
|
@@ -9301,18 +10104,27 @@ var init_font_guidance = __esm({
|
|
|
9301
10104
|
"use strict";
|
|
9302
10105
|
init_src4();
|
|
9303
10106
|
init_src7();
|
|
10107
|
+
init_invocation();
|
|
9304
10108
|
}
|
|
9305
10109
|
});
|
|
9306
10110
|
|
|
9307
10111
|
// packages/cli/src/commands/verify.ts
|
|
9308
10112
|
var verify_exports = {};
|
|
9309
10113
|
__export(verify_exports, {
|
|
10114
|
+
NO_ROLE_MANIFEST: () => NO_ROLE_MANIFEST,
|
|
10115
|
+
ROLES_NOT_RESOLVED: () => ROLES_NOT_RESOLVED,
|
|
10116
|
+
checkSummarySegments: () => checkSummarySegments,
|
|
10117
|
+
compositionReport: () => compositionReport,
|
|
10118
|
+
eyeCheck: () => eyeCheck,
|
|
10119
|
+
failureTally: () => failureTally,
|
|
9310
10120
|
foldConfigStatus: () => foldConfigStatus,
|
|
9311
10121
|
interactionCoverage: () => interactionCoverage,
|
|
10122
|
+
occlusionReport: () => occlusionReport,
|
|
10123
|
+
resolveComposition: () => resolveComposition,
|
|
9312
10124
|
runVerify: () => runVerify
|
|
9313
10125
|
});
|
|
9314
|
-
import { existsSync as
|
|
9315
|
-
import
|
|
10126
|
+
import { existsSync as existsSync26, readFileSync as readFileSync22 } from "node:fs";
|
|
10127
|
+
import path33 from "node:path";
|
|
9316
10128
|
function interactionCoverage(behaviors) {
|
|
9317
10129
|
const interaction = behaviors.filter((b) => !b.id.startsWith("prelude:"));
|
|
9318
10130
|
return {
|
|
@@ -9342,12 +10154,68 @@ function foldConfigStatus(s, failDemotions, substitutedFamilies) {
|
|
|
9342
10154
|
absentInkDemoted
|
|
9343
10155
|
};
|
|
9344
10156
|
}
|
|
10157
|
+
function resolveComposition(raw) {
|
|
10158
|
+
if (raw === void 0) return NO_ROLE_MANIFEST;
|
|
10159
|
+
const parsed = RolesSchema.safeParse(raw);
|
|
10160
|
+
if (parsed.success) return { roles: parsed.data };
|
|
10161
|
+
const why = parsed.error.issues.slice(0, 3).map((i) => `${i.path.join(".") === "" ? "roles" : i.path.join(".")}: ${i.message}`).join("; ");
|
|
10162
|
+
return {
|
|
10163
|
+
short: "roles block REJECTED",
|
|
10164
|
+
unavailable: `the recording set's roles block was REJECTED by the schema (${why}) \u2014 composition (structural + crop) was NEVER CHECKED`,
|
|
10165
|
+
warn: `recording set roles block REJECTED (${why}) \u2014 the composition checks (structural + crop) did NOT run, while the set still reads as role-confirmed. This is an instrument failure, not a clean bill. Repair the set's roles block and re-verify.`
|
|
10166
|
+
};
|
|
10167
|
+
}
|
|
10168
|
+
function rolesSourceLabel(roles) {
|
|
10169
|
+
return roles.rolesSource === "human-override" ? "HUMAN-OVERRIDE" : roles.rolesSource ?? UNSTAMPED_ROLES;
|
|
10170
|
+
}
|
|
10171
|
+
function checkSummarySegments(input) {
|
|
10172
|
+
const tally = (rows) => `${rows.filter((r) => r.pass).length}/${rows.length}`;
|
|
10173
|
+
const { availability } = input;
|
|
10174
|
+
const composition = availability.unavailable !== void 0 ? `composition NOT CHECKED (${availability.short})` : `composition ${tally(input.structural)} structural, ${input.crops !== void 0 ? `${tally(input.crops)} crops` : "crops unavailable"} (roles: ${rolesSourceLabel(availability.roles)}${(availability.roles.narrowingAccepted ?? []).length > 0 ? `, ${(availability.roles.narrowingAccepted ?? []).length} narrowing(s) accepted: ${(availability.roles.narrowingAccepted ?? []).join(", ")}` : ""})`;
|
|
10175
|
+
const occ = occlusionReport(input.occlusion);
|
|
10176
|
+
const occlusion = "unavailable" in occ ? `occlusion not applicable (${NO_OVERLAY_DECLARED})` : `occlusion ${tally(input.occlusion)}`;
|
|
10177
|
+
return ` \xB7 ${composition} \xB7 ${occlusion}`;
|
|
10178
|
+
}
|
|
10179
|
+
function occlusionReport(occlusion) {
|
|
10180
|
+
if (occlusion.length === 0) {
|
|
10181
|
+
return {
|
|
10182
|
+
unavailable: "the bundle declares no overlay (no open-state config and no prelude popover) \u2014 the sibling-occlusion check DOES NOT APPLY and nothing about overlay stacking was measured; an empty check list is this state, not a clean bill"
|
|
10183
|
+
};
|
|
10184
|
+
}
|
|
10185
|
+
return { checks: occlusion.length, passed: occlusion.filter((o) => o.pass).length };
|
|
10186
|
+
}
|
|
10187
|
+
function compositionReport(input) {
|
|
10188
|
+
const { availability, regions } = input;
|
|
10189
|
+
if (availability.unavailable !== void 0) return { unavailable: availability.unavailable };
|
|
10190
|
+
const cropsUnavailable = regions !== void 0 && "unavailable" in regions ? regions.unavailable : "interior geometry was never computed for this run";
|
|
10191
|
+
return {
|
|
10192
|
+
rolesSource: availability.roles.rolesSource ?? UNSTAMPED_ROLES,
|
|
10193
|
+
// Each token is a derived main, or a part→main edge, that a human
|
|
10194
|
+
// signed away at confirm time. Every one of them REMOVES something
|
|
10195
|
+
// the structural check would otherwise have had to satisfy, so the
|
|
10196
|
+
// list belongs beside the pass it enabled — not only in the manifest
|
|
10197
|
+
// that recorded it.
|
|
10198
|
+
narrowingAccepted: availability.roles.narrowingAccepted ?? [],
|
|
10199
|
+
structural: input.structural,
|
|
10200
|
+
crops: input.crops ?? { unavailable: cropsUnavailable }
|
|
10201
|
+
};
|
|
10202
|
+
}
|
|
10203
|
+
function eyeCheck(bundleDir) {
|
|
10204
|
+
return {
|
|
10205
|
+
command: tendrilCommand(`inspect "${bundleDir}"`),
|
|
10206
|
+
sheetPath: path33.join(bundleDir, "verify-evidence", "inspect.html"),
|
|
10207
|
+
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."
|
|
10208
|
+
};
|
|
10209
|
+
}
|
|
10210
|
+
function failureTally(t) {
|
|
10211
|
+
return `${t.configs.length} config(s), ${t.behaviors.length} behavior(s), ${t.structural.length + t.crops.length} composition check(s), ${t.occlusion.length} occlusion check(s) below the ${t.bar} bar${t.evidenceUnverified ? "; PLUS the interaction-evidence instrument gate failed (operability could not be verified \u2014 see FAIL interaction-evidence above)" : ""} \u2014 bundle written, verdict honest (Q4)`;
|
|
10212
|
+
}
|
|
9345
10213
|
function taskFromManifest(opts, manifest, setDir) {
|
|
9346
10214
|
const recordedSlugs = loadManifest(setDir).reps.map((r) => r.slug);
|
|
9347
10215
|
const adapterSlugs = Object.keys(manifest.propAdapter);
|
|
9348
10216
|
const unmapped = recordedSlugs.filter((s) => !adapterSlugs.includes(s));
|
|
9349
10217
|
const adapterOnly = adapterSlugs.filter((s) => !recordedSlugs.includes(s));
|
|
9350
|
-
const registry = Object.values(TASKS).find((t) =>
|
|
10218
|
+
const registry = Object.values(TASKS).find((t) => path33.resolve(t.set) === path33.resolve(setDir));
|
|
9351
10219
|
const authored = (() => {
|
|
9352
10220
|
if (registry !== void 0) return void 0;
|
|
9353
10221
|
try {
|
|
@@ -9356,7 +10224,7 @@ function taskFromManifest(opts, manifest, setDir) {
|
|
|
9356
10224
|
fail(opts, ExitCode.InputValidation, {
|
|
9357
10225
|
error: `cannot author the verification task from ${setDir}: ${err instanceof Error ? err.message.split("\n")[0] : String(err)}`,
|
|
9358
10226
|
code: "AUTHORING_FAILED",
|
|
9359
|
-
remediation:
|
|
10227
|
+
remediation: `Fix the recording set (duplicate/colliding poses fail authoring by design \u2014 check \`${tendrilCommand(`record status --set ${setDir}`)}\` and the set's variant names).`
|
|
9360
10228
|
});
|
|
9361
10229
|
}
|
|
9362
10230
|
})();
|
|
@@ -9379,18 +10247,18 @@ function taskFromManifest(opts, manifest, setDir) {
|
|
|
9379
10247
|
}
|
|
9380
10248
|
async function runVerify(opts) {
|
|
9381
10249
|
const callerCwd = process.env["INIT_CWD"] ?? process.cwd();
|
|
9382
|
-
const setOverride = opts.set !== void 0 ?
|
|
9383
|
-
opts = { ...opts, bundleDir:
|
|
9384
|
-
if (!
|
|
10250
|
+
const setOverride = opts.set !== void 0 ? path33.resolve(callerCwd, opts.set) : void 0;
|
|
10251
|
+
opts = { ...opts, bundleDir: path33.resolve(callerCwd, opts.bundleDir), ...setOverride !== void 0 ? { set: setOverride } : {} };
|
|
10252
|
+
if (!existsSync26(opts.bundleDir)) {
|
|
9385
10253
|
fail(opts, ExitCode.InputValidation, {
|
|
9386
10254
|
error: `bundle directory not found: ${opts.bundleDir}`,
|
|
9387
10255
|
code: "bundle-missing",
|
|
9388
10256
|
remediation: "Point at a generated bundle directory (containing component.json, the entry module, and styles.css)."
|
|
9389
10257
|
});
|
|
9390
10258
|
}
|
|
9391
|
-
const manifestPath2 =
|
|
10259
|
+
const manifestPath2 = path33.join(opts.bundleDir, "component.json");
|
|
9392
10260
|
let manifest;
|
|
9393
|
-
if (
|
|
10261
|
+
if (existsSync26(manifestPath2)) {
|
|
9394
10262
|
const { manifest: parsed, issues } = readBundleManifest(readFileSync22(manifestPath2, "utf8"));
|
|
9395
10263
|
if (issues.length > 0) {
|
|
9396
10264
|
fail(opts, ExitCode.InputValidation, {
|
|
@@ -9405,7 +10273,7 @@ async function runVerify(opts) {
|
|
|
9405
10273
|
let unmapped = [];
|
|
9406
10274
|
let interactionEvidence = [];
|
|
9407
10275
|
let unmappedInteractionEvidence = [];
|
|
9408
|
-
let
|
|
10276
|
+
let availability = ROLES_NOT_RESOLVED;
|
|
9409
10277
|
if (opts.task !== void 0) {
|
|
9410
10278
|
const registry = TASKS[opts.task];
|
|
9411
10279
|
if (registry === void 0) {
|
|
@@ -9418,21 +10286,21 @@ async function runVerify(opts) {
|
|
|
9418
10286
|
task = registry;
|
|
9419
10287
|
} else if (manifest !== void 0) {
|
|
9420
10288
|
const resolveSetDir = (p) => {
|
|
9421
|
-
if (
|
|
9422
|
-
const fromRepo =
|
|
9423
|
-
if (
|
|
9424
|
-
return
|
|
10289
|
+
if (path33.isAbsolute(p)) return p;
|
|
10290
|
+
const fromRepo = path33.resolve(REPO_ROOT, p);
|
|
10291
|
+
if (existsSync26(fromRepo)) return fromRepo;
|
|
10292
|
+
return path33.resolve(callerCwd, p);
|
|
9425
10293
|
};
|
|
9426
10294
|
const setDir = opts.set ?? resolveSetDir(manifest.provenance.recordingSet.path);
|
|
9427
|
-
if (!
|
|
10295
|
+
if (!existsSync26(path33.join(setDir, "recording-set.json")) && !Object.values(TASKS).some((t) => path33.resolve(t.set) === path33.resolve(setDir))) {
|
|
9428
10296
|
fail(opts, ExitCode.RecordingIncomplete, {
|
|
9429
10297
|
error: `recording set not found or unmanifested: ${setDir}`,
|
|
9430
10298
|
code: "recording-set-missing",
|
|
9431
10299
|
remediation: "Pass --set <dir> pointing at the recording set this bundle was generated from."
|
|
9432
10300
|
});
|
|
9433
10301
|
}
|
|
9434
|
-
const registry = Object.values(TASKS).find((t) =>
|
|
9435
|
-
if (registry !== void 0 && !
|
|
10302
|
+
const registry = Object.values(TASKS).find((t) => path33.resolve(t.set) === path33.resolve(setDir));
|
|
10303
|
+
if (registry !== void 0 && !existsSync26(path33.join(setDir, "recording-set.json"))) {
|
|
9436
10304
|
const recordedSlugs = registry.configs.map((c) => c.rep);
|
|
9437
10305
|
const adapterSlugs = Object.keys(manifest.propAdapter);
|
|
9438
10306
|
unmapped = recordedSlugs.filter((s) => !adapterSlugs.includes(s));
|
|
@@ -9448,16 +10316,16 @@ async function runVerify(opts) {
|
|
|
9448
10316
|
interactionEvidence = built.interactionEvidence;
|
|
9449
10317
|
unmappedInteractionEvidence = built.unmappedInteractionEvidence;
|
|
9450
10318
|
for (const s of built.adapterOnly) warn(opts, `prop adapter maps "${s}" which is not in the recording set \u2014 ignored`);
|
|
9451
|
-
|
|
9452
|
-
if (
|
|
10319
|
+
availability = resolveComposition(loadManifest(setDir).roles);
|
|
10320
|
+
if (availability.warn !== void 0) warn(opts, availability.warn);
|
|
9453
10321
|
}
|
|
9454
10322
|
const hash = recordingSetHash(setDir, task.configs);
|
|
9455
10323
|
if (hash !== manifest.provenance.recordingSet.hash) {
|
|
9456
10324
|
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`);
|
|
9457
10325
|
}
|
|
9458
10326
|
for (const name of [manifest.entry, "styles.css", "tokens.css"]) {
|
|
9459
|
-
const p =
|
|
9460
|
-
if (!
|
|
10327
|
+
const p = path33.join(opts.bundleDir, name);
|
|
10328
|
+
if (!existsSync26(p)) continue;
|
|
9461
10329
|
const issues = checkBundleSourceFile(name, new Uint8Array(readFileSync22(p)));
|
|
9462
10330
|
if (issues.length > 0) {
|
|
9463
10331
|
fail(opts, ExitCode.InputValidation, {
|
|
@@ -9474,6 +10342,7 @@ async function runVerify(opts) {
|
|
|
9474
10342
|
remediation: `Regenerate the bundle (bundle v1 carries its prop manifest), or pass --task <${Object.keys(TASKS).join("|")}> for legacy bundles.`
|
|
9475
10343
|
});
|
|
9476
10344
|
}
|
|
10345
|
+
const roles = availability.roles;
|
|
9477
10346
|
if (!fontsResolved()) {
|
|
9478
10347
|
fail(opts, ExitCode.FontsUnproven, {
|
|
9479
10348
|
error: "resolved font cache absent \u2014 verification under system font substitution is refused",
|
|
@@ -9481,8 +10350,8 @@ async function runVerify(opts) {
|
|
|
9481
10350
|
remediation: fontsUnprovenRemediation(task.set)
|
|
9482
10351
|
});
|
|
9483
10352
|
}
|
|
9484
|
-
if (
|
|
9485
|
-
warn(opts, "EXPERIMENT override active (
|
|
10353
|
+
if (lcdTextEnabled()) {
|
|
10354
|
+
warn(opts, "EXPERIMENT override active (TENDRIL_ENABLE_LCD_TEXT=1): text renders with platform LCD antialiasing instead of canonical greyscale \u2014 scores are NOT comparable to default runs; the environment stamp and report are marked");
|
|
9486
10355
|
}
|
|
9487
10356
|
const substitutedFamilies = unprovisionedFamilies(task.set);
|
|
9488
10357
|
if (substitutedFamilies.length > 0) {
|
|
@@ -9495,7 +10364,7 @@ async function runVerify(opts) {
|
|
|
9495
10364
|
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)`);
|
|
9496
10365
|
}
|
|
9497
10366
|
const missing = task.configs.filter(
|
|
9498
|
-
(c) => !
|
|
10367
|
+
(c) => !existsSync26(path33.join(task.set, c.rep, "get_screenshot.json")) || !existsSync26(path33.join(task.set, c.rep, "get_metadata.json"))
|
|
9499
10368
|
);
|
|
9500
10369
|
if (missing.length > 0) {
|
|
9501
10370
|
fail(opts, ExitCode.RecordingIncomplete, {
|
|
@@ -9505,10 +10374,10 @@ async function runVerify(opts) {
|
|
|
9505
10374
|
});
|
|
9506
10375
|
}
|
|
9507
10376
|
const bar = BARS2[opts.bar];
|
|
9508
|
-
const evidenceDir =
|
|
10377
|
+
const evidenceDir = path33.join(opts.bundleDir, "verify-evidence");
|
|
9509
10378
|
const scores = await scoreBundleForTask(task, opts.bundleDir, bar, { evidenceDir, onProgress: emitProgress });
|
|
9510
10379
|
const quality = await checkBundleQuality(opts.bundleDir, task.entry, { dir: task.set, reps: task.configs.map((c) => c.rep) });
|
|
9511
|
-
const bundleCss = ["tokens.css", "styles.css"].map((f) =>
|
|
10380
|
+
const bundleCss = ["tokens.css", "styles.css"].map((f) => path33.join(opts.bundleDir, f)).filter((f) => existsSync26(f)).map((f) => readFileSync22(f, "utf8")).join("\n");
|
|
9512
10381
|
const occlusion = await checkSiblingOcclusion(task, opts.bundleDir, bundleCss);
|
|
9513
10382
|
const parity = await checkHoverParity(task, opts.bundleDir);
|
|
9514
10383
|
const behaviors = [...await checkBehaviors(task, opts.bundleDir), ...parity];
|
|
@@ -9548,7 +10417,10 @@ async function runVerify(opts) {
|
|
|
9548
10417
|
...opts.task !== void 0 ? { task: opts.task } : {},
|
|
9549
10418
|
...manifest !== void 0 ? { bundleManifest: { name: manifest.name, bundleVersion: manifest.bundleVersion, recordingSetHash: manifest.provenance.recordingSet.hash } } : {},
|
|
9550
10419
|
targetBar: opts.bar,
|
|
9551
|
-
|
|
10420
|
+
// Canonical mount semantics are VERSION-scoped (0.1.22 review: the
|
|
10421
|
+
// greyscale flip changed Windows pixels under an identical-looking
|
|
10422
|
+
// stamp) — the ruler version is part of the comparability key.
|
|
10423
|
+
environment: { ...environmentStamp(taskFontFamilies(task.set)), ruler: cliVersion() },
|
|
9552
10424
|
coverage: {
|
|
9553
10425
|
scoredConfigs: statuses.length,
|
|
9554
10426
|
certified,
|
|
@@ -9590,31 +10462,22 @@ async function runVerify(opts) {
|
|
|
9590
10462
|
// silently inherited neither.
|
|
9591
10463
|
quality: { findings: quality.findings, tokensAbsent: quality.tokensAbsent },
|
|
9592
10464
|
occlusion,
|
|
10465
|
+
// The rows' STATE, which the rows alone cannot carry (see
|
|
10466
|
+
// occlusionReport): the agent channel had no counterpart to the
|
|
10467
|
+
// human line's "occlusion not applicable (no overlay declared)".
|
|
10468
|
+
occlusionCheck: occlusionReport(occlusion),
|
|
9593
10469
|
configs: statuses,
|
|
9594
10470
|
behaviors,
|
|
9595
10471
|
evidence: { dir: evidenceDir, files: "per config: <rep>-render.png, <rep>-ref.png, <rep>-diff.png" },
|
|
9596
|
-
|
|
9597
|
-
composition: {
|
|
9598
|
-
structural,
|
|
9599
|
-
crops: crops ?? { unavailable: regionsOut !== void 0 && "unavailable" in regionsOut ? regionsOut.unavailable : "no role manifest" }
|
|
9600
|
-
}
|
|
9601
|
-
} : {},
|
|
10472
|
+
composition: compositionReport({ availability, structural, crops, regions: regionsOut }),
|
|
9602
10473
|
verdict: ok ? "verified" : "verification-failed",
|
|
9603
10474
|
// Machine-readable cause (adversarial review): a cert-bar failure
|
|
9604
10475
|
// with zero pixel/behavior/composition failures was only
|
|
9605
10476
|
// explainable from stderr prose.
|
|
9606
10477
|
...certBlockedByAbsentInk.length > 0 ? { certBlockedByAbsentInk } : {},
|
|
9607
10478
|
...weightGaps.length > 0 ? { fontWeightGaps: weightGaps } : {},
|
|
9608
|
-
...
|
|
9609
|
-
|
|
9610
|
-
// footer, so MCP-driven agents — the documented path — were never
|
|
9611
|
-
// told the check exists and shipped without ever opening a sheet.
|
|
9612
|
-
// Structured here so every consumer sees it.
|
|
9613
|
-
eyeCheck: {
|
|
9614
|
-
command: `tendril inspect "${opts.bundleDir}"`,
|
|
9615
|
-
sheetPath: path31.join(opts.bundleDir, "verify-evidence", "inspect.html"),
|
|
9616
|
-
note: "magnified recorded-vs-rendered crops of every small node; scores cannot see shape. The command WRITES/refreshes the sheet at sheetPath \u2014 re-run it after every verify; an existing sheet may show stale crops."
|
|
9617
|
-
}
|
|
10479
|
+
...lcdTextEnabled() ? { environmentOverrides: ["enable-lcd-text"] } : {},
|
|
10480
|
+
eyeCheck: eyeCheck(opts.bundleDir)
|
|
9618
10481
|
};
|
|
9619
10482
|
emitData(opts, report, () => {
|
|
9620
10483
|
for (const s of statuses) {
|
|
@@ -9636,17 +10499,22 @@ async function runVerify(opts) {
|
|
|
9636
10499
|
process.stdout.write(`${o.pass ? "PASS" : "FAIL"} ${o.id}${o.detail !== void 0 ? ` [${o.detail}]` : ""}
|
|
9637
10500
|
`);
|
|
9638
10501
|
}
|
|
9639
|
-
if (
|
|
10502
|
+
if (availability.unavailable !== void 0) {
|
|
10503
|
+
process.stdout.write(`UNAVAILABLE composition \u2014 ${availability.unavailable}
|
|
10504
|
+
`);
|
|
10505
|
+
} else if (crops !== void 0) {
|
|
9640
10506
|
for (const c of crops) process.stdout.write(`${c.pass ? "PASS" : "FAIL"} ${c.id} sim=${c.similarity} ink=${c.inkRecall}${c.detail !== void 0 ? ` [${c.detail}]` : ""}
|
|
9641
10507
|
`);
|
|
9642
|
-
} else if (
|
|
10508
|
+
} else if (regionsOut !== void 0 && "unavailable" in regionsOut) {
|
|
9643
10509
|
process.stdout.write(`UNAVAILABLE composition crops \u2014 ${regionsOut.unavailable}
|
|
9644
10510
|
`);
|
|
9645
10511
|
}
|
|
9646
10512
|
const ic = interactionCoverage(behaviors);
|
|
9647
10513
|
process.stdout.write(
|
|
9648
10514
|
`
|
|
9649
|
-
${certified}/${statuses.length} certified \xB7 ${report.coverage.pass}/${statuses.length} \u2265 pass bar \xB7 behaviors ${behaviors.length - behaviorFailures.length}/${behaviors.length}${
|
|
10515
|
+
${certified}/${statuses.length} certified \xB7 ${report.coverage.pass}/${statuses.length} \u2265 pass bar \xB7 behaviors ${behaviors.length - behaviorFailures.length}/${behaviors.length}${checkSummarySegments(
|
|
10516
|
+
{ availability, structural, crops, occlusion }
|
|
10517
|
+
)}
|
|
9650
10518
|
`
|
|
9651
10519
|
);
|
|
9652
10520
|
const QUALITY_SHOWN = 8;
|
|
@@ -9703,18 +10571,20 @@ ${certified}/${statuses.length} certified \xB7 ${report.coverage.pass}/${statuse
|
|
|
9703
10571
|
}
|
|
9704
10572
|
process.stdout.write(`environment: chrome=${report.environment.chromeVersion ?? report.environment.chrome} fonts=${report.environment.fontsManifestSha256 ?? "UNRESOLVED"}
|
|
9705
10573
|
`);
|
|
9706
|
-
const shippedFonts = requiredFontsManifest(cssFontFamilies(["styles.css", "tokens.css"].map((f) =>
|
|
10574
|
+
const shippedFonts = requiredFontsManifest(cssFontFamilies(["styles.css", "tokens.css"].map((f) => path33.join(opts.bundleDir, f)).filter((f) => existsSync26(f)).map((f) => readFileSync22(f, "utf8")).join("\n")));
|
|
9707
10575
|
if (shippedFonts.length > 0 && substitutedFamilies.length === 0) {
|
|
9708
10576
|
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)
|
|
9709
10577
|
`);
|
|
9710
10578
|
}
|
|
9711
10579
|
for (const g of weightGaps) {
|
|
9712
|
-
process.stdout.write(
|
|
9713
|
-
`);
|
|
10580
|
+
process.stdout.write(
|
|
10581
|
+
`fonts (weight gap): "${g.family}" declares ${g.declared.join(", ")} \u2014 cache provides ${g.provided.join(", ")}; nearest-weight rendering may shift stroke density (advisory, never a gate). Fix: ${tendrilCommand(`fonts resolve --set ${task.set}`)}
|
|
10582
|
+
`
|
|
10583
|
+
);
|
|
9714
10584
|
}
|
|
9715
10585
|
process.stdout.write(`evidence: ${evidenceDir} (render/ref/diff per config)
|
|
9716
10586
|
`);
|
|
9717
|
-
process.stdout.write(`eye check:
|
|
10587
|
+
process.stdout.write(`eye check: ${report.eyeCheck.command} \u2014 magnified recorded-vs-rendered crops of every small node; scores cannot see shape (re-run after every verify)
|
|
9718
10588
|
`);
|
|
9719
10589
|
});
|
|
9720
10590
|
if (opts.bar === "cert" && substitutedFamilies.length > 0) {
|
|
@@ -9741,12 +10611,20 @@ ${certified}/${statuses.length} certified \xB7 ${report.coverage.pass}/${statuse
|
|
|
9741
10611
|
if (!okExceptDemotion) {
|
|
9742
10612
|
warn(
|
|
9743
10613
|
opts,
|
|
9744
|
-
|
|
10614
|
+
failureTally({
|
|
10615
|
+
configs: pixelFailures,
|
|
10616
|
+
behaviors: behaviorFailures,
|
|
10617
|
+
structural: structuralFailures,
|
|
10618
|
+
crops: cropFailures,
|
|
10619
|
+
occlusion: occlusionFailures,
|
|
10620
|
+
bar: opts.bar,
|
|
10621
|
+
evidenceUnverified
|
|
10622
|
+
})
|
|
9745
10623
|
);
|
|
9746
10624
|
process.exitCode = ExitCode.VerificationFailed;
|
|
9747
10625
|
}
|
|
9748
10626
|
}
|
|
9749
|
-
var BARS2;
|
|
10627
|
+
var BARS2, NO_ROLE_MANIFEST, ROLES_NOT_RESOLVED, UNSTAMPED_ROLES, NO_OVERLAY_DECLARED;
|
|
9750
10628
|
var init_verify = __esm({
|
|
9751
10629
|
"packages/cli/src/commands/verify.ts"() {
|
|
9752
10630
|
"use strict";
|
|
@@ -9757,11 +10635,22 @@ var init_verify = __esm({
|
|
|
9757
10635
|
init_src4();
|
|
9758
10636
|
init_environment();
|
|
9759
10637
|
init_font_guidance();
|
|
10638
|
+
init_invocation();
|
|
9760
10639
|
init_output();
|
|
9761
10640
|
BARS2 = {
|
|
9762
10641
|
pass: { sim: 0.95, ink: 0.95 },
|
|
9763
10642
|
cert: { sim: 0.97, ink: 0.95 }
|
|
9764
10643
|
};
|
|
10644
|
+
NO_ROLE_MANIFEST = Object.freeze({
|
|
10645
|
+
short: "no role manifest",
|
|
10646
|
+
unavailable: "the recording set declares no role manifest \u2014 composition (structural + crop) was NEVER CHECKED; nothing here says the main renders the SHIPPED part modules rather than a pixel-identical re-implementation"
|
|
10647
|
+
});
|
|
10648
|
+
ROLES_NOT_RESOLVED = Object.freeze({
|
|
10649
|
+
short: "roles never resolved on this path",
|
|
10650
|
+
unavailable: "this run never resolved a role manifest \u2014 the legacy --task adapter and pre-manifest reference sets skip the set's roles block entirely, so composition (structural + crop) was NEVER CHECKED and the set was never asked what it declares; nothing here says the main renders the SHIPPED part modules rather than a pixel-identical re-implementation"
|
|
10651
|
+
});
|
|
10652
|
+
UNSTAMPED_ROLES = "unstamped";
|
|
10653
|
+
NO_OVERLAY_DECLARED = "no overlay declared";
|
|
9765
10654
|
}
|
|
9766
10655
|
});
|
|
9767
10656
|
|
|
@@ -9771,36 +10660,36 @@ __export(engine_exports, {
|
|
|
9771
10660
|
runEngineBrief: () => runEngineBrief,
|
|
9772
10661
|
runEngineScore: () => runEngineScore
|
|
9773
10662
|
});
|
|
9774
|
-
import { appendFileSync, existsSync as
|
|
9775
|
-
import
|
|
10663
|
+
import { appendFileSync, existsSync as existsSync27, mkdirSync as mkdirSync8, readFileSync as readFileSync23, writeFileSync as writeFileSync12 } from "node:fs";
|
|
10664
|
+
import path34 from "node:path";
|
|
9776
10665
|
function resolveEngineTask(opts, callerCwd) {
|
|
9777
|
-
const asPath =
|
|
9778
|
-
const isSet =
|
|
10666
|
+
const asPath = path34.resolve(callerCwd, opts.taskOrSet);
|
|
10667
|
+
const isSet = existsSync27(path34.join(asPath, "recording-set.json"));
|
|
9779
10668
|
const registry = TASKS[opts.taskOrSet];
|
|
9780
|
-
if (registry !== void 0 && !isSet) return { task: registry, name: opts.taskOrSet, disclosures: [], interactionEvidence: [] };
|
|
10669
|
+
if (registry !== void 0 && !isSet) return { task: registry, name: opts.taskOrSet, ref: opts.taskOrSet, disclosures: [], interactionEvidence: [] };
|
|
9781
10670
|
if (isSet) {
|
|
9782
10671
|
try {
|
|
9783
10672
|
const authored = authorTaskFromSet(asPath);
|
|
9784
10673
|
for (const d of authored.disclosures) warn(opts, d);
|
|
9785
|
-
return { task: authored.task, name:
|
|
10674
|
+
return { task: authored.task, name: path34.basename(asPath), ref: asPath, disclosures: authored.disclosures, interactionEvidence: authored.api.interactionEvidence, apiPin: { props: authored.api.apiPin.props, forcedStates: authored.api.apiPin.forcedStates } };
|
|
9786
10675
|
} catch (err) {
|
|
9787
10676
|
fail(opts, ExitCode.InputValidation, {
|
|
9788
10677
|
error: `cannot author a task from ${asPath}: ${err instanceof Error ? err.message.split("\n")[0] : String(err)}`,
|
|
9789
10678
|
code: "AUTHORING_FAILED",
|
|
9790
|
-
remediation:
|
|
10679
|
+
remediation: `Complete the recording set (${tendrilCommand(`record status --set ${asPath}`)}) or use a reference task name.`
|
|
9791
10680
|
});
|
|
9792
10681
|
}
|
|
9793
10682
|
}
|
|
9794
10683
|
fail(opts, ExitCode.InputValidation, {
|
|
9795
10684
|
error: `"${opts.taskOrSet}" is neither a reference task nor a recorded set`,
|
|
9796
10685
|
code: "UNKNOWN_RECORDED_INPUT",
|
|
9797
|
-
remediation: `Pick a task (${Object.keys(TASKS).join(", ")}) or point at a set recorded with
|
|
10686
|
+
remediation: `Pick a task (${Object.keys(TASKS).join(", ")}) or point at a set recorded with \`${tendrilCommand("record plan")}\`.`
|
|
9798
10687
|
});
|
|
9799
10688
|
}
|
|
9800
10689
|
function runEngineBrief(opts) {
|
|
9801
10690
|
requireEntitlement(opts);
|
|
9802
10691
|
const callerCwd = process.env["INIT_CWD"] ?? process.cwd();
|
|
9803
|
-
const { task, name, disclosures } = resolveEngineTask(opts, callerCwd);
|
|
10692
|
+
const { task, name, ref, disclosures } = resolveEngineTask(opts, callerCwd);
|
|
9804
10693
|
const bar = BARS3[opts.bar];
|
|
9805
10694
|
const disclosureBlock = disclosures.length > 0 ? `
|
|
9806
10695
|
|
|
@@ -9809,8 +10698,8 @@ ${disclosures.map((d) => `- ${d}`).join("\n")}` : "";
|
|
|
9809
10698
|
const brief = buildBrief(task.systemApi, bar, { colorScheme: recordingIsDark(task) ? "dark" : "light" }) + disclosureBlock;
|
|
9810
10699
|
const segments = buildSegments(task, "files");
|
|
9811
10700
|
let notRecorded;
|
|
9812
|
-
const manifestPath2 =
|
|
9813
|
-
if (
|
|
10701
|
+
const manifestPath2 = path34.join(task.set, "recording-set.json");
|
|
10702
|
+
if (existsSync27(manifestPath2)) {
|
|
9814
10703
|
notRecorded = JSON.parse(readFileSync23(manifestPath2, "utf8")).notRecorded;
|
|
9815
10704
|
}
|
|
9816
10705
|
const unverified = notRecorded !== void 0 && notRecorded !== "" ? `
|
|
@@ -9819,7 +10708,7 @@ ${disclosures.map((d) => `- ${d}`).join("\n")}` : "";
|
|
|
9819
10708
|
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.
|
|
9820
10709
|
${notRecorded}` : "";
|
|
9821
10710
|
let fontProvisioning;
|
|
9822
|
-
if (
|
|
10711
|
+
if (existsSync27(manifestPath2)) {
|
|
9823
10712
|
const missingFams = unprovisionedFamilies(task.set);
|
|
9824
10713
|
const unprovided = unprovisionedFaces(task.set);
|
|
9825
10714
|
const weightOnly = missingFams.length === 0;
|
|
@@ -9827,16 +10716,17 @@ ${notRecorded}` : "";
|
|
|
9827
10716
|
const names = unprovided.map((f) => `'${f}'`).join(", ");
|
|
9828
10717
|
const PROPRIETARY = /^(sf pro|sf compact|sf mono|new york|pingfang|segoe ui|helvetica neue|proxima nova|avenir)/i;
|
|
9829
10718
|
const allProprietary = unprovided.every((f) => PROPRIETARY.test(f.trim()));
|
|
10719
|
+
const resolveCommand = tendrilCommand(`fonts resolve --set ${task.set}`);
|
|
9830
10720
|
fontProvisioning = {
|
|
9831
10721
|
unprovided,
|
|
9832
10722
|
question: {
|
|
9833
10723
|
prompt: weightOnly ? `This design implies font ${unprovided.length === 1 ? "weight" : "weights"} the local kit lacks: ${names} (the ${missingFams.length === 0 && unprovided.length === 1 ? "family itself is" : "families themselves are"} provisioned). How should it be handled?` : `This design's text uses ${names}, which is not in the local font kit. How should it be handled?`,
|
|
9834
10724
|
options: [
|
|
9835
|
-
allProprietary && !weightOnly ? `(Recommended) ${names} ${unprovided.length === 1 ? "is a proprietary face" : "are proprietary faces"} \u2014 Google Fonts cannot serve ${unprovided.length === 1 ? "it" : "them"}, so skip \`fonts resolve\`: run
|
|
10725
|
+
allProprietary && !weightOnly ? `(Recommended) ${names} ${unprovided.length === 1 ? "is a proprietary face" : "are proprietary faces"} \u2014 Google Fonts cannot serve ${unprovided.length === 1 ? "it" : "them"}, so skip \`fonts resolve\`: run \`${tendrilCommand('fonts add "<Family>" <weight> <file>')}\` with the .woff2/.ttf/.otf you hold a licence for (the file never leaves this machine), then re-run this brief \u2014 exact text fidelity.` : `(Recommended) Run \`${resolveCommand}\` \u2014 open-source families are fetched automatically. Any face that fails there is one you license privately: run \`${tendrilCommand('fonts add "<Family>" <weight> <file>')}\` with the .woff2/.ttf/.otf you hold (the file never leaves this machine). Then re-run this brief \u2014 exact text fidelity.`,
|
|
9836
10726
|
weightOnly ? "Continue with nearest-weight rendering: the family is provisioned, certification remains possible, and the gap rides the report as an advisory (fontWeightGaps); resolving removes it." : 'Continue with a substitute face: generation proceeds in a provided family, no config can score "certified" under it (cert-bar runs exit fonts-unproven after their report), the substitution is disclosed, and small text differences are expected and not fixable from CSS.'
|
|
9837
10727
|
]
|
|
9838
10728
|
},
|
|
9839
|
-
nonInteractive: weightOnly ? `Run
|
|
10729
|
+
nonInteractive: weightOnly ? `Run \`${resolveCommand}\` if quick, or continue \u2014 the weight gap is an advisory, never a gate.` : opts.bar === "cert" ? `Resolve the families first (\`${resolveCommand}\`) \u2014 this loop targets the cert bar, and scoring exits fonts-unproven under a substitute, so continuing without resolving is a dead end.` : "Continue with the substitute and state the substitution in your report."
|
|
9840
10730
|
};
|
|
9841
10731
|
}
|
|
9842
10732
|
}
|
|
@@ -9844,8 +10734,8 @@ ${notRecorded}` : "";
|
|
|
9844
10734
|
|
|
9845
10735
|
=== TASK PAYLOAD (recorded truth, verbatim) ===
|
|
9846
10736
|
${segments}`;
|
|
9847
|
-
const payloadFile =
|
|
9848
|
-
mkdirSync8(
|
|
10737
|
+
const payloadFile = path34.resolve(callerCwd, opts.out ?? `tendril-out/${name}-brief.md`);
|
|
10738
|
+
mkdirSync8(path34.dirname(payloadFile), { recursive: true });
|
|
9849
10739
|
writeFileSync12(payloadFile, payload);
|
|
9850
10740
|
emitData(
|
|
9851
10741
|
opts,
|
|
@@ -9874,7 +10764,7 @@ ${segments}`;
|
|
|
9874
10764
|
"Settle the proposer model (see modelSelection \u2014 ask the user when one is present), then:",
|
|
9875
10765
|
`Read ${payloadFile} completely \u2014 it is the system brief plus every recorded config's emission, box, and assets.`,
|
|
9876
10766
|
`Write the complete bundle files (${task.entry}, styles.css, optional tokens.css) into a candidate directory.`,
|
|
9877
|
-
`Run
|
|
10767
|
+
`Run \`${tendrilCommand(`engine score ${ref} <candidateDir> --bar ${opts.bar} --model ${opts.model ?? "<declared-model>"} --json`)}\` \u2014 the CLI is the sole judge, and it refuses to score an undeclared model.`,
|
|
9878
10768
|
"Apply the returned feedback and re-score. Stop when all checks pass or two consecutive scores fail to improve.",
|
|
9879
10769
|
"Never edit recording sets; never claim scores yourself \u2014 only the score command's output counts."
|
|
9880
10770
|
]
|
|
@@ -9887,18 +10777,23 @@ ${segments}`;
|
|
|
9887
10777
|
}
|
|
9888
10778
|
);
|
|
9889
10779
|
}
|
|
10780
|
+
function appendScoreHistory(candidateDir, entry) {
|
|
10781
|
+
appendFileSync(path34.join(candidateDir, "score-history.jsonl"), `${JSON.stringify({ at: (/* @__PURE__ */ new Date()).toISOString(), ...entry })}
|
|
10782
|
+
`);
|
|
10783
|
+
}
|
|
9890
10784
|
async function runEngineScore(opts) {
|
|
9891
10785
|
requireEntitlement(opts);
|
|
9892
10786
|
const callerCwd = process.env["INIT_CWD"] ?? process.cwd();
|
|
9893
|
-
const candidateDir =
|
|
10787
|
+
const candidateDir = path34.resolve(callerCwd, opts.candidateDir);
|
|
9894
10788
|
const { task, name, apiPin, interactionEvidence } = resolveEngineTask(opts, callerCwd);
|
|
9895
|
-
if (!
|
|
10789
|
+
if (!existsSync27(candidateDir)) {
|
|
9896
10790
|
fail(opts, ExitCode.InputValidation, {
|
|
9897
10791
|
error: `candidate directory not found: ${candidateDir}`,
|
|
9898
10792
|
code: "candidate-missing",
|
|
9899
10793
|
remediation: `Write ${task.entry} and styles.css into a directory and pass it.`
|
|
9900
10794
|
});
|
|
9901
10795
|
}
|
|
10796
|
+
appendScoreHistory(candidateDir, { event: "round-start", bar: opts.bar });
|
|
9902
10797
|
if (!fontsResolved()) {
|
|
9903
10798
|
fail(opts, ExitCode.FontsUnproven, {
|
|
9904
10799
|
error: "resolved font cache absent \u2014 scoring under system font substitution is refused",
|
|
@@ -9906,8 +10801,8 @@ async function runEngineScore(opts) {
|
|
|
9906
10801
|
remediation: fontsUnprovenRemediation(task.set)
|
|
9907
10802
|
});
|
|
9908
10803
|
}
|
|
9909
|
-
if (
|
|
9910
|
-
warn(opts, "EXPERIMENT override active (
|
|
10804
|
+
if (lcdTextEnabled()) {
|
|
10805
|
+
warn(opts, "EXPERIMENT override active (TENDRIL_ENABLE_LCD_TEXT=1): text renders with platform LCD antialiasing instead of canonical greyscale \u2014 scores are NOT comparable to default runs; the environment stamp and report are marked");
|
|
9911
10806
|
}
|
|
9912
10807
|
const substitutedFamilies = unprovisionedFamilies(task.set);
|
|
9913
10808
|
if (substitutedFamilies.length > 0) {
|
|
@@ -9916,10 +10811,10 @@ async function runEngineScore(opts) {
|
|
|
9916
10811
|
for (const g of missingWeights(task.set)) {
|
|
9917
10812
|
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)`);
|
|
9918
10813
|
}
|
|
9919
|
-
if (opts.rebind !== true &&
|
|
10814
|
+
if (opts.rebind !== true && existsSync27(path34.join(candidateDir, "component.json"))) {
|
|
9920
10815
|
const prior = (() => {
|
|
9921
10816
|
try {
|
|
9922
|
-
const read = readBundleManifest(readFileSync23(
|
|
10817
|
+
const read = readBundleManifest(readFileSync23(path34.join(candidateDir, "component.json"), "utf8"));
|
|
9923
10818
|
return read.manifest === void 0 ? { unreadable: true } : { hash: read.manifest.provenance.recordingSet.hash, path: read.manifest.provenance.recordingSet.path };
|
|
9924
10819
|
} catch {
|
|
9925
10820
|
return { unreadable: true };
|
|
@@ -9941,7 +10836,7 @@ async function runEngineScore(opts) {
|
|
|
9941
10836
|
}
|
|
9942
10837
|
}
|
|
9943
10838
|
const bar = BARS3[opts.bar];
|
|
9944
|
-
const evidenceDir =
|
|
10839
|
+
const evidenceDir = path34.join(candidateDir, "verify-evidence");
|
|
9945
10840
|
const scores = await scoreBundleForTask(task, candidateDir, bar, { evidenceDir, onProgress: emitProgress });
|
|
9946
10841
|
const parity = await checkHoverParity(task, candidateDir);
|
|
9947
10842
|
const behaviors = [...await checkBehaviors(task, candidateDir), ...parity];
|
|
@@ -9984,11 +10879,7 @@ METRIC DEADBAND (read before iterating on near-misses): the scored similarity/in
|
|
|
9984
10879
|
environment: environmentStamp(taskFontFamilies(task.set)),
|
|
9985
10880
|
substitutedFamilies
|
|
9986
10881
|
});
|
|
9987
|
-
|
|
9988
|
-
path32.join(candidateDir, "score-history.jsonl"),
|
|
9989
|
-
`${JSON.stringify({ at: (/* @__PURE__ */ new Date()).toISOString(), bar: opts.bar, pass: obj[0], total, certified: certifiedReps.length, floor: obj[1], mean: obj[2] })}
|
|
9990
|
-
`
|
|
9991
|
-
);
|
|
10882
|
+
appendScoreHistory(candidateDir, { event: "round-scored", bar: opts.bar, pass: obj[0], total, certified: certifiedReps.length, floor: obj[1], mean: obj[2] });
|
|
9992
10883
|
emitData(
|
|
9993
10884
|
opts,
|
|
9994
10885
|
{
|
|
@@ -10008,7 +10899,8 @@ METRIC DEADBAND (read before iterating on near-misses): the scored similarity/in
|
|
|
10008
10899
|
bundleManifest: emitted.written[0],
|
|
10009
10900
|
note: "styles.css was stamped with the bundle provenance comment on line 1 \u2014 preserve it in any post-score edit",
|
|
10010
10901
|
allPass,
|
|
10011
|
-
...
|
|
10902
|
+
environment: { ...environmentStamp(taskFontFamilies(task.set)), ruler: cliVersion() },
|
|
10903
|
+
...lcdTextEnabled() ? { environmentOverrides: ["enable-lcd-text"] } : {},
|
|
10012
10904
|
// Run 11: generators read allPass:true and reported success on
|
|
10013
10905
|
// bundles verify then FAILED on the interaction-evidence gate —
|
|
10014
10906
|
// the oracle must say what verify will say, including this.
|
|
@@ -10062,6 +10954,7 @@ var init_engine2 = __esm({
|
|
|
10062
10954
|
init_font_guidance();
|
|
10063
10955
|
init_src4();
|
|
10064
10956
|
init_output();
|
|
10957
|
+
init_invocation();
|
|
10065
10958
|
init_entitlement();
|
|
10066
10959
|
init_verify();
|
|
10067
10960
|
init_src4();
|
|
@@ -10077,11 +10970,11 @@ var codeconnect_exports = {};
|
|
|
10077
10970
|
__export(codeconnect_exports, {
|
|
10078
10971
|
runCodeConnect: () => runCodeConnect
|
|
10079
10972
|
});
|
|
10080
|
-
import { existsSync as
|
|
10081
|
-
import
|
|
10973
|
+
import { existsSync as existsSync28, readFileSync as readFileSync24, writeFileSync as writeFileSync13 } from "node:fs";
|
|
10974
|
+
import path35 from "node:path";
|
|
10082
10975
|
function runCodeConnect(opts) {
|
|
10083
10976
|
const callerCwd = process.env["INIT_CWD"] ?? process.cwd();
|
|
10084
|
-
const bundleDir =
|
|
10977
|
+
const bundleDir = path35.resolve(callerCwd, opts.bundleDir);
|
|
10085
10978
|
let url;
|
|
10086
10979
|
try {
|
|
10087
10980
|
url = new URL(opts.figmaUrl);
|
|
@@ -10097,7 +10990,7 @@ function runCodeConnect(opts) {
|
|
|
10097
10990
|
}
|
|
10098
10991
|
let manifest;
|
|
10099
10992
|
try {
|
|
10100
|
-
const read = readBundleManifest(readFileSync24(
|
|
10993
|
+
const read = readBundleManifest(readFileSync24(path35.join(bundleDir, "component.json"), "utf8"));
|
|
10101
10994
|
if (read.manifest === void 0) throw new Error(read.issues.map((i) => i.message).join("; ") || "no component.json");
|
|
10102
10995
|
manifest = read.manifest;
|
|
10103
10996
|
} catch (err) {
|
|
@@ -10107,8 +11000,8 @@ function runCodeConnect(opts) {
|
|
|
10107
11000
|
remediation: "Point at a bundle produced by the Tendril pipeline (it carries component.json), and verify it first."
|
|
10108
11001
|
});
|
|
10109
11002
|
}
|
|
10110
|
-
const setDir =
|
|
10111
|
-
if (!
|
|
11003
|
+
const setDir = path35.resolve(callerCwd, opts.set ?? manifest.provenance.recordingSet.path);
|
|
11004
|
+
if (!existsSync28(path35.join(setDir, "recording-set.json"))) {
|
|
10112
11005
|
fail(opts, ExitCode.InputValidation, {
|
|
10113
11006
|
error: `recording set not found at ${setDir}`,
|
|
10114
11007
|
code: "codeconnect-no-set",
|
|
@@ -10122,15 +11015,15 @@ function runCodeConnect(opts) {
|
|
|
10122
11015
|
fail(opts, ExitCode.InputValidation, {
|
|
10123
11016
|
error: `cannot author the API from ${setDir}: ${err instanceof Error ? err.message.split("\n")[0] : String(err)}`,
|
|
10124
11017
|
code: "codeconnect-authoring-failed",
|
|
10125
|
-
remediation:
|
|
11018
|
+
remediation: `The set must be a complete protocol recording (${tendrilCommand(`record status --set ${setDir}`)}).`
|
|
10126
11019
|
});
|
|
10127
11020
|
}
|
|
10128
11021
|
const api = authored.api;
|
|
10129
11022
|
const component = api.component;
|
|
10130
11023
|
const recManifest = loadManifest(setDir);
|
|
10131
11024
|
const poseNames = recManifest.latticeNames ?? recManifest.reps.map((r) => {
|
|
10132
|
-
const meta =
|
|
10133
|
-
if (!
|
|
11025
|
+
const meta = path35.join(setDir, r.slug, "get_metadata.json");
|
|
11026
|
+
if (!existsSync28(meta)) return void 0;
|
|
10134
11027
|
try {
|
|
10135
11028
|
return /name="([^"]*)"/.exec(envelopeFirstTextPart(JSON.parse(readFileSync24(meta, "utf8"))))?.[1];
|
|
10136
11029
|
} catch {
|
|
@@ -10149,7 +11042,7 @@ function runCodeConnect(opts) {
|
|
|
10149
11042
|
fail(opts, ExitCode.InputValidation, {
|
|
10150
11043
|
error: "the recording set's content no longer matches the hash this bundle was scored against \u2014 its verification claims describe a different recording",
|
|
10151
11044
|
code: "codeconnect-set-drift",
|
|
10152
|
-
remediation:
|
|
11045
|
+
remediation: `Re-run \`${tendrilCommand(`verify ${bundleDir} --set ${setDir}`)}\` (or engine score) against the current set, then re-emit.`
|
|
10153
11046
|
});
|
|
10154
11047
|
}
|
|
10155
11048
|
const boolProps = api.props.filter((p) => p.kind === "boolean");
|
|
@@ -10190,7 +11083,7 @@ function runCodeConnect(opts) {
|
|
|
10190
11083
|
axisLines.push(`const ${varName} = instance.getEnum(${q(axis)}, { ${entries.join(", ")} })`);
|
|
10191
11084
|
fragmentVars.push(varName);
|
|
10192
11085
|
}
|
|
10193
|
-
const entryRel =
|
|
11086
|
+
const entryRel = path35.relative(callerCwd, path35.join(bundleDir, manifest.entry));
|
|
10194
11087
|
const trust = manifest.trustStatement.split("\n")[0] ?? "";
|
|
10195
11088
|
const lines = [
|
|
10196
11089
|
`// url=${opts.figmaUrl}`,
|
|
@@ -10211,7 +11104,7 @@ function runCodeConnect(opts) {
|
|
|
10211
11104
|
`}`,
|
|
10212
11105
|
``
|
|
10213
11106
|
].join("\n");
|
|
10214
|
-
const outFile =
|
|
11107
|
+
const outFile = path35.resolve(callerCwd, opts.out ?? path35.join(bundleDir, `${component}.figma.ts`));
|
|
10215
11108
|
writeFileSync13(outFile, lines);
|
|
10216
11109
|
emitData(
|
|
10217
11110
|
opts,
|
|
@@ -10242,6 +11135,7 @@ var init_codeconnect = __esm({
|
|
|
10242
11135
|
init_src7();
|
|
10243
11136
|
init_src();
|
|
10244
11137
|
init_src6();
|
|
11138
|
+
init_invocation();
|
|
10245
11139
|
init_output();
|
|
10246
11140
|
kebab4 = (s) => s.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "");
|
|
10247
11141
|
q = (s) => `'${s.replace(/\\/g, "\\\\").replace(/'/g, "\\'").replace(/\n/g, "\\n").replace(/\r/g, "\\r").replace(/\u2028/g, "\\u2028").replace(/\u2029/g, "\\u2029")}'`;
|
|
@@ -10250,17 +11144,17 @@ var init_codeconnect = __esm({
|
|
|
10250
11144
|
|
|
10251
11145
|
// packages/mcp/src/server.ts
|
|
10252
11146
|
import { createHash as createHash5 } from "node:crypto";
|
|
10253
|
-
import { existsSync as
|
|
11147
|
+
import { existsSync as existsSync29, mkdtempSync as mkdtempSync3, readFileSync as readFileSync25, readdirSync as readdirSync8, writeFileSync as writeFileSync14 } from "node:fs";
|
|
10254
11148
|
import os6 from "node:os";
|
|
10255
|
-
import
|
|
10256
|
-
import { fileURLToPath as
|
|
11149
|
+
import path36 from "node:path";
|
|
11150
|
+
import { fileURLToPath as fileURLToPath6 } from "node:url";
|
|
10257
11151
|
import { z as z12 } from "zod";
|
|
10258
11152
|
function sourceHash() {
|
|
10259
|
-
const dir =
|
|
11153
|
+
const dir = path36.dirname(fileURLToPath6(import.meta.url));
|
|
10260
11154
|
const h = createHash5("sha256");
|
|
10261
11155
|
for (const f of readdirSync8(dir).filter((n) => n.endsWith(".ts")).sort()) {
|
|
10262
11156
|
h.update(f);
|
|
10263
|
-
h.update(readFileSync25(
|
|
11157
|
+
h.update(readFileSync25(path36.join(dir, f)));
|
|
10264
11158
|
}
|
|
10265
11159
|
return h.digest("hex").slice(0, 16);
|
|
10266
11160
|
}
|
|
@@ -10268,10 +11162,10 @@ var REPO_ROOT3, CLI_BIN, BUNDLED_CLI, CLI_SPAWN, str, optStr, TOOLS, BOOT_HASH;
|
|
|
10268
11162
|
var init_server = __esm({
|
|
10269
11163
|
"packages/mcp/src/server.ts"() {
|
|
10270
11164
|
"use strict";
|
|
10271
|
-
REPO_ROOT3 =
|
|
10272
|
-
CLI_BIN =
|
|
10273
|
-
BUNDLED_CLI =
|
|
10274
|
-
CLI_SPAWN =
|
|
11165
|
+
REPO_ROOT3 = path36.resolve(path36.dirname(fileURLToPath6(import.meta.url)), "..", "..", "..");
|
|
11166
|
+
CLI_BIN = path36.join(REPO_ROOT3, "packages", "cli", "src", "bin.ts");
|
|
11167
|
+
BUNDLED_CLI = path36.join(path36.dirname(fileURLToPath6(import.meta.url)), "tendril.js");
|
|
11168
|
+
CLI_SPAWN = existsSync29(BUNDLED_CLI) ? { cmd: process.execPath, prefix: [BUNDLED_CLI] } : { cmd: "npx", prefix: ["tsx", CLI_BIN] };
|
|
10275
11169
|
str = (d) => z12.string().describe(d);
|
|
10276
11170
|
optStr = (d) => z12.string().optional().describe(d);
|
|
10277
11171
|
TOOLS = [
|
|
@@ -10301,7 +11195,7 @@ var init_server = __esm({
|
|
|
10301
11195
|
const single = i["metadata"];
|
|
10302
11196
|
const parts = i["metadataParts"];
|
|
10303
11197
|
if (single !== void 0 || parts !== void 0) {
|
|
10304
|
-
const tmp =
|
|
11198
|
+
const tmp = path36.join(mkdtempSync3(path36.join(os6.tmpdir(), "tendril-envelope-")), "response.txt");
|
|
10305
11199
|
if (single !== void 0) {
|
|
10306
11200
|
writeFileSync14(tmp, single);
|
|
10307
11201
|
argvOut.push("--metadata-raw-file", tmp);
|
|
@@ -10372,7 +11266,7 @@ var init_server = __esm({
|
|
|
10372
11266
|
const bridge = (label, single, parts) => {
|
|
10373
11267
|
if (single !== void 0 && parts !== void 0) throw new Error(`pass at most one of \`${label}\` and \`${label}Parts\``);
|
|
10374
11268
|
if (single === void 0 && parts === void 0) return;
|
|
10375
|
-
const tmp =
|
|
11269
|
+
const tmp = path36.join(mkdtempSync3(path36.join(os6.tmpdir(), "tendril-envelope-")), "response.txt");
|
|
10376
11270
|
if (single !== void 0) {
|
|
10377
11271
|
writeFileSync14(tmp, single);
|
|
10378
11272
|
argvOut.push(`--${label}-file`, tmp);
|
|
@@ -10415,7 +11309,7 @@ var init_server = __esm({
|
|
|
10415
11309
|
const file = i["file"];
|
|
10416
11310
|
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)");
|
|
10417
11311
|
if (file !== void 0) return [...base, "--file", file];
|
|
10418
|
-
const tmp =
|
|
11312
|
+
const tmp = path36.join(mkdtempSync3(path36.join(os6.tmpdir(), "tendril-envelope-")), "response.txt");
|
|
10419
11313
|
if (text !== void 0) {
|
|
10420
11314
|
writeFileSync14(tmp, text);
|
|
10421
11315
|
return [...base, "--file", tmp, "--raw"];
|
|
@@ -10570,12 +11464,12 @@ __export(permissions_exports, {
|
|
|
10570
11464
|
mergeAllowlist: () => mergeAllowlist,
|
|
10571
11465
|
runPermissions: () => runPermissions
|
|
10572
11466
|
});
|
|
10573
|
-
import { existsSync as
|
|
11467
|
+
import { existsSync as existsSync30, mkdirSync as mkdirSync9, readFileSync as readFileSync26, writeFileSync as writeFileSync15 } from "node:fs";
|
|
10574
11468
|
import os7 from "node:os";
|
|
10575
|
-
import
|
|
11469
|
+
import path37 from "node:path";
|
|
10576
11470
|
function mergeAllowlist(file, entries) {
|
|
10577
11471
|
let settings = {};
|
|
10578
|
-
if (
|
|
11472
|
+
if (existsSync30(file) && readFileSync26(file, "utf8").trim() !== "") {
|
|
10579
11473
|
settings = JSON.parse(readFileSync26(file, "utf8"));
|
|
10580
11474
|
if (settings === null || typeof settings !== "object" || Array.isArray(settings)) throw new Error("settings root is not an object");
|
|
10581
11475
|
}
|
|
@@ -10588,7 +11482,7 @@ function mergeAllowlist(file, entries) {
|
|
|
10588
11482
|
const alreadyPresent = entries.filter((e) => present.has(e));
|
|
10589
11483
|
if (added.length > 0) {
|
|
10590
11484
|
allow.push(...added);
|
|
10591
|
-
mkdirSync9(
|
|
11485
|
+
mkdirSync9(path37.dirname(file), { recursive: true });
|
|
10592
11486
|
writeFileSync15(file, `${JSON.stringify(settings, null, 2)}
|
|
10593
11487
|
`);
|
|
10594
11488
|
}
|
|
@@ -10624,7 +11518,7 @@ async function runPermissions(flags) {
|
|
|
10624
11518
|
const result = await buildPermissions({ ...flags.mcpUrl ? { mcpUrl: flags.mcpUrl } : {} });
|
|
10625
11519
|
if (flags.write) {
|
|
10626
11520
|
const base = process.env["INIT_CWD"] ?? process.cwd();
|
|
10627
|
-
const file = flags.user ?
|
|
11521
|
+
const file = flags.user ? path37.join(os7.homedir(), ".claude", "settings.json") : path37.join(base, ".claude", "settings.local.json");
|
|
10628
11522
|
if (flags.dryRun) {
|
|
10629
11523
|
emitData(flags, { file, wouldAdd: result.toolEntries }, () => {
|
|
10630
11524
|
process.stdout.write(`dry-run: would merge ${result.toolEntries.length} per-tool entries into ${file}
|
|
@@ -10657,7 +11551,7 @@ async function runPermissions(flags) {
|
|
|
10657
11551
|
`Claude Code allowlist for the Tendril pipeline.
|
|
10658
11552
|
One command installs it (project-local, idempotent):
|
|
10659
11553
|
|
|
10660
|
-
|
|
11554
|
+
${tendrilCommand("permissions --claude --write")}
|
|
10661
11555
|
|
|
10662
11556
|
Or paste into .claude/settings.json under permissions.allow:
|
|
10663
11557
|
|
|
@@ -10682,6 +11576,7 @@ var init_permissions = __esm({
|
|
|
10682
11576
|
init_src();
|
|
10683
11577
|
init_server();
|
|
10684
11578
|
init_describe();
|
|
11579
|
+
init_invocation();
|
|
10685
11580
|
init_output();
|
|
10686
11581
|
init_doctor();
|
|
10687
11582
|
FIGMA_TOOL_FALLBACK = ["get_metadata", "get_design_context", "get_screenshot", "get_variable_defs", "get_motion_context", "get_figjam"];
|
|
@@ -10717,21 +11612,21 @@ __export(inspect_exports, {
|
|
|
10717
11612
|
INSPECT_DESCRIPTION: () => INSPECT_DESCRIPTION,
|
|
10718
11613
|
runInspect: () => runInspect
|
|
10719
11614
|
});
|
|
10720
|
-
import { existsSync as
|
|
10721
|
-
import
|
|
11615
|
+
import { existsSync as existsSync31, readFileSync as readFileSync27, writeFileSync as writeFileSync16 } from "node:fs";
|
|
11616
|
+
import path38 from "node:path";
|
|
10722
11617
|
async function runInspect(opts) {
|
|
10723
11618
|
if (opts.describe) {
|
|
10724
11619
|
printDescription(INSPECT_DESCRIPTION);
|
|
10725
11620
|
return;
|
|
10726
11621
|
}
|
|
10727
|
-
const bundleDir =
|
|
10728
|
-
const evidenceDir =
|
|
10729
|
-
const manifestPath2 =
|
|
10730
|
-
if (!
|
|
11622
|
+
const bundleDir = path38.resolve(opts.bundleDir);
|
|
11623
|
+
const evidenceDir = path38.join(bundleDir, "verify-evidence");
|
|
11624
|
+
const manifestPath2 = path38.join(bundleDir, "component.json");
|
|
11625
|
+
if (!existsSync31(evidenceDir) || !existsSync31(manifestPath2)) {
|
|
10731
11626
|
fail(opts, ExitCode.InputValidation, {
|
|
10732
|
-
error: `nothing to inspect in ${bundleDir} \u2014 ${
|
|
11627
|
+
error: `nothing to inspect in ${bundleDir} \u2014 ${existsSync31(manifestPath2) ? "no verify-evidence directory" : "no component.json"}`,
|
|
10733
11628
|
code: "no-evidence",
|
|
10734
|
-
remediation:
|
|
11629
|
+
remediation: `Run \`${tendrilCommand(`verify ${bundleDir}`)}\` first \u2014 inspect reads the ref/render evidence that run writes.`
|
|
10735
11630
|
});
|
|
10736
11631
|
}
|
|
10737
11632
|
const { manifest } = readBundleManifest(readFileSync27(manifestPath2, "utf8"));
|
|
@@ -10742,27 +11637,27 @@ async function runInspect(opts) {
|
|
|
10742
11637
|
remediation: "Re-emit the bundle (score/verify rewrite component.json), then re-run inspect."
|
|
10743
11638
|
});
|
|
10744
11639
|
}
|
|
10745
|
-
const setDir =
|
|
10746
|
-
const reps = Object.keys(manifest.propAdapter).filter((rep) =>
|
|
11640
|
+
const setDir = path38.resolve(opts.set ?? manifest.provenance.recordingSet.path);
|
|
11641
|
+
const reps = Object.keys(manifest.propAdapter).filter((rep) => existsSync31(path38.join(evidenceDir, `${rep}-ref.png`)) && existsSync31(path38.join(evidenceDir, `${rep}-render.png`)));
|
|
10747
11642
|
if (reps.length === 0) {
|
|
10748
11643
|
fail(opts, ExitCode.InputValidation, {
|
|
10749
11644
|
error: "verify-evidence holds no ref/render pairs for this bundle's configs",
|
|
10750
11645
|
code: "no-evidence",
|
|
10751
|
-
remediation:
|
|
11646
|
+
remediation: `Run \`${tendrilCommand(`verify ${bundleDir}`)}\` first \u2014 inspect reads the ref/render evidence that run writes.`
|
|
10752
11647
|
});
|
|
10753
11648
|
}
|
|
10754
11649
|
let crops = 0;
|
|
10755
11650
|
const sections = [];
|
|
10756
11651
|
for (const rep of reps) {
|
|
10757
|
-
const ref = new Uint8Array(readFileSync27(
|
|
10758
|
-
const render = new Uint8Array(readFileSync27(
|
|
11652
|
+
const ref = new Uint8Array(readFileSync27(path38.join(evidenceDir, `${rep}-ref.png`)));
|
|
11653
|
+
const render = new Uint8Array(readFileSync27(path38.join(evidenceDir, `${rep}-render.png`)));
|
|
10759
11654
|
const nodes = smallSemanticNodes(setDir, rep, opts.maxArea ?? 1024).slice(0, 12);
|
|
10760
11655
|
const cells = [];
|
|
10761
11656
|
for (const [i, n] of nodes.entries()) {
|
|
10762
11657
|
const rect = { x: n.x, y: n.y, w: n.w, h: n.h };
|
|
10763
11658
|
try {
|
|
10764
|
-
writeFileSync16(
|
|
10765
|
-
writeFileSync16(
|
|
11659
|
+
writeFileSync16(path38.join(evidenceDir, `${rep}-inspect-${i}-ref.png`), zoomCrop(ref, rect));
|
|
11660
|
+
writeFileSync16(path38.join(evidenceDir, `${rep}-inspect-${i}-render.png`), zoomCrop(render, rect));
|
|
10766
11661
|
} catch {
|
|
10767
11662
|
continue;
|
|
10768
11663
|
}
|
|
@@ -10775,7 +11670,7 @@ async function runInspect(opts) {
|
|
|
10775
11670
|
`<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>`
|
|
10776
11671
|
);
|
|
10777
11672
|
}
|
|
10778
|
-
const sheet =
|
|
11673
|
+
const sheet = path38.join(evidenceDir, "inspect.html");
|
|
10779
11674
|
writeFileSync16(
|
|
10780
11675
|
sheet,
|
|
10781
11676
|
`<!doctype html><meta charset="utf-8"><title>${esc(manifest.name)} \u2014 tendril inspect</title><style>
|
|
@@ -10806,6 +11701,7 @@ var init_inspect = __esm({
|
|
|
10806
11701
|
init_src6();
|
|
10807
11702
|
init_src4();
|
|
10808
11703
|
init_describe();
|
|
11704
|
+
init_invocation();
|
|
10809
11705
|
init_output();
|
|
10810
11706
|
INSPECT_DESCRIPTION = {
|
|
10811
11707
|
name: "inspect",
|
|
@@ -10852,17 +11748,17 @@ __export(generate_recorded_exports, {
|
|
|
10852
11748
|
runGenerateRecorded: () => runGenerateRecorded
|
|
10853
11749
|
});
|
|
10854
11750
|
import { confirm as confirm3, isCancel as isCancel3 } from "@clack/prompts";
|
|
10855
|
-
import { existsSync as
|
|
10856
|
-
import
|
|
11751
|
+
import { existsSync as existsSync32, readFileSync as readFileSync28 } from "node:fs";
|
|
11752
|
+
import path39 from "node:path";
|
|
10857
11753
|
async function runGenerateRecorded(opts) {
|
|
10858
11754
|
const callerCwd = process.env["INIT_CWD"] ?? process.cwd();
|
|
10859
|
-
const outDirAbs =
|
|
10860
|
-
const recordedAsPath =
|
|
11755
|
+
const outDirAbs = path39.resolve(callerCwd, opts.out);
|
|
11756
|
+
const recordedAsPath = path39.resolve(callerCwd, opts.recorded);
|
|
10861
11757
|
let task;
|
|
10862
11758
|
let taskName;
|
|
10863
11759
|
let authoredApi;
|
|
10864
11760
|
let composition;
|
|
10865
|
-
const isSet =
|
|
11761
|
+
const isSet = existsSync32(path39.join(recordedAsPath, "recording-set.json"));
|
|
10866
11762
|
const registry = TASKS[opts.recorded];
|
|
10867
11763
|
if (registry !== void 0 && !isSet) {
|
|
10868
11764
|
task = registry;
|
|
@@ -10871,7 +11767,7 @@ async function runGenerateRecorded(opts) {
|
|
|
10871
11767
|
try {
|
|
10872
11768
|
const authored = authorTaskFromSet(recordedAsPath);
|
|
10873
11769
|
task = authored.task;
|
|
10874
|
-
taskName =
|
|
11770
|
+
taskName = path39.basename(recordedAsPath);
|
|
10875
11771
|
authoredApi = authored.api;
|
|
10876
11772
|
const roles = RolesSchema.safeParse(loadManifest(recordedAsPath).roles);
|
|
10877
11773
|
if (roles.success) composition = roles.data;
|
|
@@ -10880,14 +11776,14 @@ async function runGenerateRecorded(opts) {
|
|
|
10880
11776
|
fail(opts, ExitCode.InputValidation, {
|
|
10881
11777
|
error: `cannot author a task from ${recordedAsPath}: ${err instanceof Error ? err.message.split("\n")[0] : String(err)}`,
|
|
10882
11778
|
code: "AUTHORING_FAILED",
|
|
10883
|
-
remediation:
|
|
11779
|
+
remediation: `Complete the recording set (${tendrilCommand(`record status --set ${recordedAsPath}`)}) or use a reference-corpus task name.`
|
|
10884
11780
|
});
|
|
10885
11781
|
}
|
|
10886
11782
|
} else {
|
|
10887
11783
|
fail(opts, ExitCode.InputValidation, {
|
|
10888
11784
|
error: `"${opts.recorded}" is neither a reference task nor a recorded set (no recording-set.json found)`,
|
|
10889
11785
|
code: "UNKNOWN_RECORDED_INPUT",
|
|
10890
|
-
remediation: `Pick a task (${Object.keys(TASKS).join(", ")}) or point at a set recorded with
|
|
11786
|
+
remediation: `Pick a task (${Object.keys(TASKS).join(", ")}) or point at a set recorded with \`${tendrilCommand("record plan")}\`.`
|
|
10891
11787
|
});
|
|
10892
11788
|
}
|
|
10893
11789
|
if (!fontsResolved()) {
|
|
@@ -10905,13 +11801,13 @@ async function runGenerateRecorded(opts) {
|
|
|
10905
11801
|
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)`);
|
|
10906
11802
|
}
|
|
10907
11803
|
const missing = task.configs.filter(
|
|
10908
|
-
(c) => !
|
|
11804
|
+
(c) => !existsSync32(path39.join(task.set, c.rep, "get_screenshot.json")) || !existsSync32(path39.join(task.set, c.rep, "get_metadata.json")) || !existsSync32(path39.join(task.set, c.rep, "get_design_context.json"))
|
|
10909
11805
|
);
|
|
10910
11806
|
if (missing.length > 0) {
|
|
10911
11807
|
fail(opts, ExitCode.RecordingIncomplete, {
|
|
10912
11808
|
error: `recording set ${task.set} is missing envelopes for ${missing.length} rep(s): ${missing.slice(0, 3).map((c) => c.rep).join(", ")}${missing.length > 3 ? ", \u2026" : ""}`,
|
|
10913
11809
|
code: "recording-incomplete",
|
|
10914
|
-
remediation:
|
|
11810
|
+
remediation: `Resume the recording session (\`${tendrilCommand(`record next --set ${task.set}`)}\`) until \`${tendrilCommand(`record status --set ${task.set}`)}\` reports the set complete.`
|
|
10915
11811
|
});
|
|
10916
11812
|
}
|
|
10917
11813
|
const modelId = opts.model ?? DEFAULT_MODEL_CONFIG.curatedDefault ?? DEFAULT_MODEL_CONFIG.bulk;
|
|
@@ -10932,7 +11828,7 @@ async function runGenerateRecorded(opts) {
|
|
|
10932
11828
|
fail(opts, ExitCode.Auth, {
|
|
10933
11829
|
error: "OPENROUTER_API_KEY is not configured \u2014 the curated engine needs an OpenRouter-compatible key.",
|
|
10934
11830
|
code: "MISSING_OPENROUTER_KEY",
|
|
10935
|
-
remediation:
|
|
11831
|
+
remediation: `Two paths: run \`${tendrilCommand("init")}\` to store your OpenRouter key (BYOK \u2014 your key, your inference), or drive the agent-harness engine explicitly with your own agent host.`
|
|
10936
11832
|
});
|
|
10937
11833
|
}
|
|
10938
11834
|
const bar = BARS4[opts.bar];
|
|
@@ -10975,8 +11871,8 @@ async function runGenerateRecorded(opts) {
|
|
|
10975
11871
|
` : `${line}
|
|
10976
11872
|
`);
|
|
10977
11873
|
if (opts.dryRun) {
|
|
10978
|
-
emitData(opts, { dryRun: true, task: taskName, model: modelId, consent, wouldWrite:
|
|
10979
|
-
process.stdout.write(`dry-run: nothing sent, nothing written (would write ${
|
|
11874
|
+
emitData(opts, { dryRun: true, task: taskName, model: modelId, consent, wouldWrite: path39.join(outDirAbs, taskName) }, () => {
|
|
11875
|
+
process.stdout.write(`dry-run: nothing sent, nothing written (would write ${path39.join(outDirAbs, taskName)})
|
|
10980
11876
|
`);
|
|
10981
11877
|
});
|
|
10982
11878
|
return;
|
|
@@ -10999,10 +11895,10 @@ async function runGenerateRecorded(opts) {
|
|
|
10999
11895
|
});
|
|
11000
11896
|
}
|
|
11001
11897
|
}
|
|
11002
|
-
const bundleDir =
|
|
11003
|
-
if (
|
|
11898
|
+
const bundleDir = path39.join(outDirAbs, taskName);
|
|
11899
|
+
if (existsSync32(path39.join(bundleDir, "component.json"))) {
|
|
11004
11900
|
try {
|
|
11005
|
-
const prior = readBundleManifest(readFileSync28(
|
|
11901
|
+
const prior = readBundleManifest(readFileSync28(path39.join(bundleDir, "component.json"), "utf8")).manifest;
|
|
11006
11902
|
if (prior !== void 0 && prior.provenance.recordingSet.hash !== recordingSetHash(task.set, task.configs)) {
|
|
11007
11903
|
fail(opts, ExitCode.InputValidation, {
|
|
11008
11904
|
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`,
|
|
@@ -11139,6 +12035,7 @@ var init_generate_recorded = __esm({
|
|
|
11139
12035
|
init_env();
|
|
11140
12036
|
init_environment();
|
|
11141
12037
|
init_font_guidance();
|
|
12038
|
+
init_invocation();
|
|
11142
12039
|
init_output();
|
|
11143
12040
|
BARS4 = {
|
|
11144
12041
|
pass: { sim: 0.95, ink: 0.95 },
|
|
@@ -11149,11 +12046,13 @@ var init_generate_recorded = __esm({
|
|
|
11149
12046
|
|
|
11150
12047
|
// packages/cli/src/bin.ts
|
|
11151
12048
|
init_src3();
|
|
12049
|
+
init_invocation();
|
|
11152
12050
|
import { CommanderError } from "commander";
|
|
11153
12051
|
|
|
11154
12052
|
// packages/cli/src/program.ts
|
|
11155
12053
|
init_src3();
|
|
11156
12054
|
init_environment();
|
|
12055
|
+
init_invocation();
|
|
11157
12056
|
init_doctor();
|
|
11158
12057
|
import { Command } from "commander";
|
|
11159
12058
|
|
|
@@ -11161,10 +12060,11 @@ import { Command } from "commander";
|
|
|
11161
12060
|
init_src3();
|
|
11162
12061
|
init_describe();
|
|
11163
12062
|
init_env();
|
|
12063
|
+
init_invocation();
|
|
11164
12064
|
init_output();
|
|
11165
12065
|
import { intro, isCancel, outro, password } from "@clack/prompts";
|
|
11166
12066
|
import fs from "node:fs";
|
|
11167
|
-
import
|
|
12067
|
+
import path24 from "node:path";
|
|
11168
12068
|
var INIT_DESCRIPTION = {
|
|
11169
12069
|
name: "init",
|
|
11170
12070
|
summary: "Configure Figma and OpenRouter credentials in .env (idempotent).",
|
|
@@ -11201,7 +12101,7 @@ async function runInit(flags) {
|
|
|
11201
12101
|
printDescription(INIT_DESCRIPTION);
|
|
11202
12102
|
return;
|
|
11203
12103
|
}
|
|
11204
|
-
const envPath =
|
|
12104
|
+
const envPath = path24.resolve(process.cwd(), ".env");
|
|
11205
12105
|
const existing = fs.existsSync(envPath) ? parseEnv(fs.readFileSync(envPath, "utf8")) : /* @__PURE__ */ new Map();
|
|
11206
12106
|
let figmaToken = flags.figmaToken ?? existing.get(ENV_KEYS.figma);
|
|
11207
12107
|
let openrouterKey = flags.openrouterKey ?? existing.get(ENV_KEYS.openrouter);
|
|
@@ -11210,7 +12110,7 @@ async function runInit(flags) {
|
|
|
11210
12110
|
fail(flags, ExitCode.InputValidation, {
|
|
11211
12111
|
error: "Missing credentials and prompts are disabled.",
|
|
11212
12112
|
code: "MISSING_CREDENTIALS",
|
|
11213
|
-
remediation:
|
|
12113
|
+
remediation: `Pass --figma-token <token> and --openrouter-key <key>, or run \`${tendrilCommand("init")}\` in an interactive terminal.`
|
|
11214
12114
|
});
|
|
11215
12115
|
}
|
|
11216
12116
|
if (interactive) {
|
|
@@ -11222,7 +12122,7 @@ async function runInit(flags) {
|
|
|
11222
12122
|
next.set(ENV_KEYS.figma, figmaToken);
|
|
11223
12123
|
next.set(ENV_KEYS.openrouter, openrouterKey);
|
|
11224
12124
|
const changed = existing.get(ENV_KEYS.figma) !== next.get(ENV_KEYS.figma) || existing.get(ENV_KEYS.openrouter) !== next.get(ENV_KEYS.openrouter);
|
|
11225
|
-
const gitignorePath =
|
|
12125
|
+
const gitignorePath = path24.resolve(process.cwd(), ".gitignore");
|
|
11226
12126
|
const gitignore = fs.existsSync(gitignorePath) ? fs.readFileSync(gitignorePath, "utf8") : "";
|
|
11227
12127
|
const gitignoreCoversEnv = gitignore.split("\n").some((line) => [".env", ".env", ".env*"].includes(line.trim()));
|
|
11228
12128
|
if (flags.dryRun) {
|
|
@@ -11274,17 +12174,18 @@ init_src5();
|
|
|
11274
12174
|
init_src2();
|
|
11275
12175
|
init_describe();
|
|
11276
12176
|
init_env();
|
|
12177
|
+
init_invocation();
|
|
11277
12178
|
init_output();
|
|
11278
12179
|
init_entitlement();
|
|
11279
12180
|
import { confirm as confirm2, isCancel as isCancel2 } from "@clack/prompts";
|
|
11280
|
-
import { readFileSync as readFileSync15, readdirSync as readdirSync4, existsSync as
|
|
12181
|
+
import { readFileSync as readFileSync15, readdirSync as readdirSync4, existsSync as existsSync19 } from "node:fs";
|
|
11281
12182
|
|
|
11282
12183
|
// packages/cli/src/pipeline.ts
|
|
11283
12184
|
init_src2();
|
|
11284
12185
|
init_src4();
|
|
11285
12186
|
init_src6();
|
|
11286
12187
|
import { mkdirSync as mkdirSync5, writeFileSync as writeFileSync8 } from "node:fs";
|
|
11287
|
-
import
|
|
12188
|
+
import path25 from "node:path";
|
|
11288
12189
|
|
|
11289
12190
|
// packages/cli/src/assets-module.ts
|
|
11290
12191
|
init_src();
|
|
@@ -11620,7 +12521,7 @@ async function runGenerationPipeline(input) {
|
|
|
11620
12521
|
});
|
|
11621
12522
|
const written = [];
|
|
11622
12523
|
if (!input.dryRun) {
|
|
11623
|
-
const dir =
|
|
12524
|
+
const dir = path25.resolve(input.outDir, semantics.componentName);
|
|
11624
12525
|
mkdirSync5(dir, { recursive: true });
|
|
11625
12526
|
const files = {
|
|
11626
12527
|
// Bundle-local tokens: THE emission the component's CSS resolves
|
|
@@ -11644,13 +12545,13 @@ async function runGenerationPipeline(input) {
|
|
|
11644
12545
|
`
|
|
11645
12546
|
};
|
|
11646
12547
|
for (const [name, content] of Object.entries(files)) {
|
|
11647
|
-
const filePath =
|
|
12548
|
+
const filePath = path25.join(dir, name);
|
|
11648
12549
|
writeFileSync8(filePath, content);
|
|
11649
12550
|
written.push(filePath);
|
|
11650
12551
|
}
|
|
11651
12552
|
for (const artifact of emitTokenArtifacts(input.mapping)) {
|
|
11652
|
-
const filePath =
|
|
11653
|
-
mkdirSync5(
|
|
12553
|
+
const filePath = path25.resolve(input.outDir, artifact.path);
|
|
12554
|
+
mkdirSync5(path25.dirname(filePath), { recursive: true });
|
|
11654
12555
|
writeFileSync8(filePath, artifact.content);
|
|
11655
12556
|
written.push(filePath);
|
|
11656
12557
|
}
|
|
@@ -11714,7 +12615,7 @@ function resolveProvidedSource(flags, contextFile) {
|
|
|
11714
12615
|
fail(flags, ExitCode.InputValidation, {
|
|
11715
12616
|
error: `Cannot read context file "${contextFile}".`,
|
|
11716
12617
|
code: "CONTEXT_UNREADABLE",
|
|
11717
|
-
remediation:
|
|
12618
|
+
remediation: `Pass a readable JSON file via --context; run \`${tendrilCommand("generate --describe")}\` for the payload shape.`
|
|
11718
12619
|
});
|
|
11719
12620
|
}
|
|
11720
12621
|
try {
|
|
@@ -11723,7 +12624,7 @@ function resolveProvidedSource(flags, contextFile) {
|
|
|
11723
12624
|
fail(flags, ExitCode.InputValidation, {
|
|
11724
12625
|
error: `Context file "${contextFile}" is not a valid design-context payload: ${err instanceof Error ? err.message.split("\n")[0] : String(err)}`,
|
|
11725
12626
|
code: "CONTEXT_INVALID",
|
|
11726
|
-
remediation:
|
|
12627
|
+
remediation: `The payload must match { nodeTree, variables[], metadata, screenshotBase64? } \u2014 see \`${tendrilCommand("generate --describe")}\` and docs/adr/003-figma-access-strategy.md.`
|
|
11727
12628
|
});
|
|
11728
12629
|
}
|
|
11729
12630
|
}
|
|
@@ -11733,7 +12634,7 @@ function resolveSource(flags, url) {
|
|
|
11733
12634
|
fail(flags, ExitCode.General, {
|
|
11734
12635
|
error: "Live Figma transports (desktop MCP / REST) are not wired up yet.",
|
|
11735
12636
|
code: "NOT_IMPLEMENTED",
|
|
11736
|
-
remediation:
|
|
12637
|
+
remediation: `Use \`${tendrilCommand("generate --context <file>")}\` with an agent-fetched payload, or mock://button for the offline fixture; live transports are task 1.2 in docs/PLAN.md.`
|
|
11737
12638
|
});
|
|
11738
12639
|
}
|
|
11739
12640
|
fail(flags, ExitCode.InputValidation, {
|
|
@@ -11752,7 +12653,7 @@ async function runGenerate(url, flags) {
|
|
|
11752
12653
|
fail(flags, ExitCode.InputValidation, {
|
|
11753
12654
|
error: "Missing input: pass a recording set or reference task (recorded truth, the default), mock://button, or --context <file>.",
|
|
11754
12655
|
code: "MISSING_ARGUMENT",
|
|
11755
|
-
remediation:
|
|
12656
|
+
remediation: `Run \`${tendrilCommand("generate --describe")}\` to see arguments and examples.`
|
|
11756
12657
|
});
|
|
11757
12658
|
}
|
|
11758
12659
|
url ??= `provided://${flags.context}`;
|
|
@@ -11829,10 +12730,10 @@ token mapping (${mapping.flat.length} variables):
|
|
|
11829
12730
|
let initialCode;
|
|
11830
12731
|
let initialSemantics;
|
|
11831
12732
|
try {
|
|
11832
|
-
if (
|
|
12733
|
+
if (existsSync19(flags.out)) {
|
|
11833
12734
|
for (const entry of readdirSync4(flags.out)) {
|
|
11834
12735
|
const cjPath = `${flags.out}/${entry}/component.json`;
|
|
11835
|
-
if (!
|
|
12736
|
+
if (!existsSync19(cjPath)) continue;
|
|
11836
12737
|
const cj = JSON.parse(readFileSync15(cjPath, "utf8"));
|
|
11837
12738
|
if (cj.name !== void 0 && Array.isArray(cj.props)) {
|
|
11838
12739
|
previousApi = JSON.stringify({
|
|
@@ -11841,13 +12742,13 @@ token mapping (${mapping.flat.length} variables):
|
|
|
11841
12742
|
});
|
|
11842
12743
|
const tsxPath = `${flags.out}/${entry}/${cj.name}.tsx`;
|
|
11843
12744
|
const cssPath = `${flags.out}/${entry}/${cj.name}.css`;
|
|
11844
|
-
if (flags.refine &&
|
|
12745
|
+
if (flags.refine && existsSync19(tsxPath) && existsSync19(cssPath)) {
|
|
11845
12746
|
initialCode = {
|
|
11846
12747
|
tsx: readFileSync15(tsxPath, "utf8"),
|
|
11847
12748
|
css: readFileSync15(cssPath, "utf8")
|
|
11848
12749
|
};
|
|
11849
12750
|
const semPath = `${flags.out}/${entry}/semantics.json`;
|
|
11850
|
-
if (
|
|
12751
|
+
if (existsSync19(semPath)) {
|
|
11851
12752
|
initialSemantics = JSON.parse(readFileSync15(semPath, "utf8"));
|
|
11852
12753
|
}
|
|
11853
12754
|
}
|
|
@@ -11877,7 +12778,7 @@ token mapping (${mapping.flat.length} variables):
|
|
|
11877
12778
|
fail(flags, ExitCode.Auth, {
|
|
11878
12779
|
error: "OPENROUTER_API_KEY is not configured.",
|
|
11879
12780
|
code: "MISSING_OPENROUTER_KEY",
|
|
11880
|
-
remediation:
|
|
12781
|
+
remediation: `Run \`${tendrilCommand("init")}\` to store your OpenRouter key in .env (BYOK \u2014 your key, your inference).`
|
|
11881
12782
|
});
|
|
11882
12783
|
}
|
|
11883
12784
|
genModel = new OpenRouterGenerationModel(new OpenRouterClient({ apiKey }), model.id, {
|
|
@@ -12100,7 +13001,7 @@ function buildProgram() {
|
|
|
12100
13001
|
fail2(flags, ExitCode.InputValidation, {
|
|
12101
13002
|
error: family === void 0 ? "nothing to resolve \u2014 pass a family name or --set <recording-dir>" : "pass a family name OR --set, not both \u2014 --set derives families from the recording itself",
|
|
12102
13003
|
code: "INPUT_VALIDATION",
|
|
12103
|
-
remediation:
|
|
13004
|
+
remediation: `${tendrilCommand("fonts resolve --set <recording-dir>")}, or ${tendrilCommand('fonts resolve "<Family>" --weights 400 500 600')}`
|
|
12104
13005
|
});
|
|
12105
13006
|
}
|
|
12106
13007
|
if (set !== void 0) {
|
|
@@ -12254,7 +13155,7 @@ try {
|
|
|
12254
13155
|
const cliError2 = {
|
|
12255
13156
|
error: err.message,
|
|
12256
13157
|
code: "INPUT_VALIDATION",
|
|
12257
|
-
remediation:
|
|
13158
|
+
remediation: `Run \`${tendrilCommand("--help")}\` or \`${tendrilCommand("<command> --describe")}\` for usage.`
|
|
12258
13159
|
};
|
|
12259
13160
|
process.stderr.write(`${JSON.stringify(cliError2)}
|
|
12260
13161
|
`);
|