@tendrilapp/cli 0.1.33 → 0.1.34
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/tendril.js +1402 -643
- package/package.json +1 -1
package/dist/tendril.js
CHANGED
|
@@ -449,7 +449,7 @@ function parseVariantAxes(nodeName) {
|
|
|
449
449
|
if (!nodeName.includes("=")) return void 0;
|
|
450
450
|
const axes = {};
|
|
451
451
|
for (const pair of nodeName.split(",")) {
|
|
452
|
-
const [key, value] = pair.split("=").map((s) => s.trim());
|
|
452
|
+
const [key, value] = pair.split("=").map((s) => decodeXmlEntities(s.trim()));
|
|
453
453
|
if (key !== void 0 && key !== "" && value !== void 0 && value !== "") {
|
|
454
454
|
axes[key] = [value];
|
|
455
455
|
}
|
|
@@ -785,7 +785,7 @@ function resolveAxisDefaultWithRule(domain, recorded, override) {
|
|
|
785
785
|
function resolveAxisDefault(domain, recorded, override) {
|
|
786
786
|
return resolveAxisDefaultWithRule(domain, recorded, override).value;
|
|
787
787
|
}
|
|
788
|
-
var INTERACTION_STATES, BOOLEAN_STATES, ENGAGED_STATES, INTERACTION_EVIDENCE_VALUES, kebab, OFF_VALUES, ON_VALUES;
|
|
788
|
+
var INTERACTION_STATES, BOOLEAN_STATES, ENGAGED_STATES, INTERACTION_EVIDENCE_VALUES, ENGAGED_STATE_VALUES, INTERACTION_TREATMENT_VALUES, kebab, OFF_VALUES, ON_VALUES;
|
|
789
789
|
var init_axis_defaults = __esm({
|
|
790
790
|
"packages/figma/src/recording/axis-defaults.ts"() {
|
|
791
791
|
"use strict";
|
|
@@ -793,6 +793,32 @@ var init_axis_defaults = __esm({
|
|
|
793
793
|
BOOLEAN_STATES = /* @__PURE__ */ new Set(["disabled", "loading"]);
|
|
794
794
|
ENGAGED_STATES = /* @__PURE__ */ new Set(["selected", "checked", "indeterminate", "mixed", "open", "expanded"]);
|
|
795
795
|
INTERACTION_EVIDENCE_VALUES = /* @__PURE__ */ new Set([
|
|
796
|
+
...INTERACTION_STATES,
|
|
797
|
+
...BOOLEAN_STATES,
|
|
798
|
+
"hovered",
|
|
799
|
+
"focused",
|
|
800
|
+
"press",
|
|
801
|
+
"selected",
|
|
802
|
+
"checked",
|
|
803
|
+
"unselected",
|
|
804
|
+
"unchecked",
|
|
805
|
+
"indeterminate",
|
|
806
|
+
// Cycle B C4: engaged/typing evidence. A dropdown's open pose and an
|
|
807
|
+
// input's typing pose PROVE the control is interactive — the sweep
|
|
808
|
+
// measured "the recording proves no interactive poses" printed for
|
|
809
|
+
// dropdowns and inputs (the trust anchor calling a control "not a
|
|
810
|
+
// control"). DELIBERATE VERDICT CHANGE, release-noted: a set with
|
|
811
|
+
// these poses and ZERO verified interaction behaviors now honestly
|
|
812
|
+
// fails the interaction-evidence gate instead of passing silently.
|
|
813
|
+
"open",
|
|
814
|
+
"expanded",
|
|
815
|
+
"typing",
|
|
816
|
+
"on",
|
|
817
|
+
"off",
|
|
818
|
+
"filled"
|
|
819
|
+
]);
|
|
820
|
+
ENGAGED_STATE_VALUES = /* @__PURE__ */ new Set(["open", "expanded", "typing", "on", "off", "filled"]);
|
|
821
|
+
INTERACTION_TREATMENT_VALUES = /* @__PURE__ */ new Set([
|
|
796
822
|
...INTERACTION_STATES,
|
|
797
823
|
...BOOLEAN_STATES,
|
|
798
824
|
"hovered",
|
|
@@ -1326,15 +1352,81 @@ var init_session = __esm({
|
|
|
1326
1352
|
}
|
|
1327
1353
|
});
|
|
1328
1354
|
|
|
1355
|
+
// packages/figma/src/recording/visibility.ts
|
|
1356
|
+
import { existsSync as existsSync2, readFileSync as readFileSync2 } from "node:fs";
|
|
1357
|
+
import path2 from "node:path";
|
|
1358
|
+
function effectivelyInvisible(node) {
|
|
1359
|
+
if (node.hidden === true) return true;
|
|
1360
|
+
if (node.width === 0 || node.height === 0) return true;
|
|
1361
|
+
return false;
|
|
1362
|
+
}
|
|
1363
|
+
function repVisibility(setDir, slug) {
|
|
1364
|
+
const file = path2.join(setDir, slug, "get_metadata.json");
|
|
1365
|
+
if (!existsSync2(file)) return void 0;
|
|
1366
|
+
let roots;
|
|
1367
|
+
try {
|
|
1368
|
+
roots = parseMetadataForest(envelopeTextContent(JSON.parse(readFileSync2(file, "utf8")))).roots;
|
|
1369
|
+
} catch {
|
|
1370
|
+
return void 0;
|
|
1371
|
+
}
|
|
1372
|
+
const byId = /* @__PURE__ */ new Map();
|
|
1373
|
+
const byName = /* @__PURE__ */ new Map();
|
|
1374
|
+
const hiddenIds = /* @__PURE__ */ new Set();
|
|
1375
|
+
const walk2 = (n, underHidden) => {
|
|
1376
|
+
const invisible = underHidden || effectivelyInvisible(n);
|
|
1377
|
+
byId.set(n.id, !invisible);
|
|
1378
|
+
if (invisible) hiddenIds.add(n.id);
|
|
1379
|
+
const norm2 = normalizedLayerName(n.name);
|
|
1380
|
+
if (norm2 !== "") byName.set(norm2, (byName.get(norm2) ?? false) || !invisible);
|
|
1381
|
+
for (const c of n.children) walk2(c, invisible);
|
|
1382
|
+
};
|
|
1383
|
+
for (const rootNode of roots) walk2(rootNode, false);
|
|
1384
|
+
return { byId, byName, hiddenIds };
|
|
1385
|
+
}
|
|
1386
|
+
function poseVisibility(setDir, repSlugs, match) {
|
|
1387
|
+
const presentIn = [];
|
|
1388
|
+
const visibleIn = [];
|
|
1389
|
+
const norm2 = match.name === void 0 ? void 0 : normalizedLayerName(match.name);
|
|
1390
|
+
for (const slug of repSlugs) {
|
|
1391
|
+
const rep = repVisibility(setDir, slug);
|
|
1392
|
+
if (rep === void 0) continue;
|
|
1393
|
+
let present = false;
|
|
1394
|
+
let visible = false;
|
|
1395
|
+
if (match.nodeId !== void 0 && rep.byId.has(match.nodeId)) {
|
|
1396
|
+
present = true;
|
|
1397
|
+
visible = rep.byId.get(match.nodeId) === true;
|
|
1398
|
+
}
|
|
1399
|
+
if (!present && norm2 !== void 0 && rep.byName.has(norm2)) {
|
|
1400
|
+
present = true;
|
|
1401
|
+
visible = rep.byName.get(norm2) === true;
|
|
1402
|
+
}
|
|
1403
|
+
if (present) {
|
|
1404
|
+
presentIn.push(slug);
|
|
1405
|
+
if (visible) visibleIn.push(slug);
|
|
1406
|
+
}
|
|
1407
|
+
}
|
|
1408
|
+
return { presentIn, visibleIn };
|
|
1409
|
+
}
|
|
1410
|
+
var normalizedLayerName, invisibleInEveryPose;
|
|
1411
|
+
var init_visibility = __esm({
|
|
1412
|
+
"packages/figma/src/recording/visibility.ts"() {
|
|
1413
|
+
"use strict";
|
|
1414
|
+
init_normalize();
|
|
1415
|
+
init_session();
|
|
1416
|
+
normalizedLayerName = (name) => name.toLowerCase().replace(/[^a-z0-9]/g, "");
|
|
1417
|
+
invisibleInEveryPose = (v) => v.presentIn.length > 0 && v.visibleIn.length === 0;
|
|
1418
|
+
}
|
|
1419
|
+
});
|
|
1420
|
+
|
|
1329
1421
|
// packages/figma/src/recording/compose.ts
|
|
1330
1422
|
import { createHash } from "node:crypto";
|
|
1331
|
-
import { existsSync as
|
|
1332
|
-
import
|
|
1423
|
+
import { existsSync as existsSync3, readFileSync as readFileSync3, readdirSync, statSync } from "node:fs";
|
|
1424
|
+
import path3 from "node:path";
|
|
1333
1425
|
import { z as z5 } from "zod";
|
|
1334
1426
|
function buildComposeIndex(roots, depth = 3) {
|
|
1335
1427
|
const found = [];
|
|
1336
1428
|
const walk2 = (dir, remaining) => {
|
|
1337
|
-
if (
|
|
1429
|
+
if (existsSync3(path3.join(dir, "recording-set.json"))) {
|
|
1338
1430
|
found.push(dir);
|
|
1339
1431
|
return;
|
|
1340
1432
|
}
|
|
@@ -1347,17 +1439,17 @@ function buildComposeIndex(roots, depth = 3) {
|
|
|
1347
1439
|
}
|
|
1348
1440
|
for (const e of entries2) {
|
|
1349
1441
|
if (e === "node_modules" || e.startsWith(".")) continue;
|
|
1350
|
-
const full =
|
|
1442
|
+
const full = path3.join(dir, e);
|
|
1351
1443
|
try {
|
|
1352
1444
|
if (statSync(full).isDirectory()) walk2(full, remaining - 1);
|
|
1353
1445
|
} catch {
|
|
1354
1446
|
}
|
|
1355
1447
|
}
|
|
1356
1448
|
};
|
|
1357
|
-
for (const r of roots) walk2(
|
|
1449
|
+
for (const r of roots) walk2(path3.resolve(r), depth);
|
|
1358
1450
|
const seen = /* @__PURE__ */ new Set();
|
|
1359
1451
|
const uniqueDirs = found.filter((d) => {
|
|
1360
|
-
const key =
|
|
1452
|
+
const key = path3.resolve(d);
|
|
1361
1453
|
if (seen.has(key)) return false;
|
|
1362
1454
|
seen.add(key);
|
|
1363
1455
|
return true;
|
|
@@ -1377,10 +1469,10 @@ function buildComposeIndex(roots, depth = 3) {
|
|
|
1377
1469
|
for (const rep of manifest.reps) {
|
|
1378
1470
|
variantNodeIds.add(rep.nodeId);
|
|
1379
1471
|
repSlugByVariantNode.set(rep.nodeId, rep.slug);
|
|
1380
|
-
const metaFile =
|
|
1381
|
-
if (!
|
|
1472
|
+
const metaFile = path3.join(dir, rep.slug, "get_metadata.json");
|
|
1473
|
+
if (!existsSync3(metaFile)) continue;
|
|
1382
1474
|
try {
|
|
1383
|
-
const text = envelopeTextContent(JSON.parse(
|
|
1475
|
+
const text = envelopeTextContent(JSON.parse(readFileSync3(metaFile, "utf8")));
|
|
1384
1476
|
const ids = /* @__PURE__ */ new Set();
|
|
1385
1477
|
const collect = (n) => {
|
|
1386
1478
|
if (n.id !== "") ids.add(n.id);
|
|
@@ -1413,11 +1505,11 @@ function sameComponent(a, b) {
|
|
|
1413
1505
|
return false;
|
|
1414
1506
|
}
|
|
1415
1507
|
function emissionTails(setDir, repSlug) {
|
|
1416
|
-
const file =
|
|
1417
|
-
if (!
|
|
1508
|
+
const file = path3.join(setDir, repSlug, "get_design_context.json");
|
|
1509
|
+
if (!existsSync3(file)) return /* @__PURE__ */ new Map();
|
|
1418
1510
|
let text;
|
|
1419
1511
|
try {
|
|
1420
|
-
text = envelopeTextContent(JSON.parse(
|
|
1512
|
+
text = envelopeTextContent(JSON.parse(readFileSync3(file, "utf8")));
|
|
1421
1513
|
} catch {
|
|
1422
1514
|
return /* @__PURE__ */ new Map();
|
|
1423
1515
|
}
|
|
@@ -1437,16 +1529,20 @@ function emissionTails(setDir, repSlug) {
|
|
|
1437
1529
|
return byHead;
|
|
1438
1530
|
}
|
|
1439
1531
|
function hostInstances(setDir, repSlug) {
|
|
1440
|
-
const metaFile =
|
|
1441
|
-
if (!
|
|
1532
|
+
const metaFile = path3.join(setDir, repSlug, "get_metadata.json");
|
|
1533
|
+
if (!existsSync3(metaFile)) return [];
|
|
1442
1534
|
try {
|
|
1443
|
-
const text = envelopeTextContent(JSON.parse(
|
|
1535
|
+
const text = envelopeTextContent(JSON.parse(readFileSync3(metaFile, "utf8")));
|
|
1444
1536
|
const out = [];
|
|
1445
|
-
const
|
|
1446
|
-
|
|
1447
|
-
|
|
1537
|
+
const walkRoot = (root) => {
|
|
1538
|
+
const walk2 = (n, underHidden) => {
|
|
1539
|
+
const invisible = underHidden || effectivelyInvisible(n);
|
|
1540
|
+
if (n.type === "INSTANCE") out.push({ id: n.id, name: n.name, visible: !invisible });
|
|
1541
|
+
for (const c of n.children) walk2(c, invisible);
|
|
1542
|
+
};
|
|
1543
|
+
walk2(root, false);
|
|
1448
1544
|
};
|
|
1449
|
-
for (const root of parseMetadataForest(text).roots)
|
|
1545
|
+
for (const root of parseMetadataForest(text).roots) walkRoot(root);
|
|
1450
1546
|
return out;
|
|
1451
1547
|
} catch {
|
|
1452
1548
|
return [];
|
|
@@ -1455,12 +1551,32 @@ function hostInstances(setDir, repSlug) {
|
|
|
1455
1551
|
function composeReport(index) {
|
|
1456
1552
|
const edges = [];
|
|
1457
1553
|
for (const host of index) {
|
|
1554
|
+
const visibleEver = /* @__PURE__ */ new Map();
|
|
1555
|
+
for (const [, repSlug] of host.repSlugByVariantNode) {
|
|
1556
|
+
for (const inst of hostInstances(host.dir, repSlug)) {
|
|
1557
|
+
visibleEver.set(inst.id, (visibleEver.get(inst.id) ?? false) || inst.visible);
|
|
1558
|
+
}
|
|
1559
|
+
}
|
|
1458
1560
|
for (const [variantNodeId, slug] of host.repSlugByVariantNode) {
|
|
1459
1561
|
void variantNodeId;
|
|
1460
1562
|
const instances = hostInstances(host.dir, slug);
|
|
1461
1563
|
if (instances.length === 0) continue;
|
|
1462
1564
|
const tailsByHead = emissionTails(host.dir, slug);
|
|
1463
1565
|
for (const inst of instances) {
|
|
1566
|
+
if (visibleEver.get(inst.id) !== true) {
|
|
1567
|
+
edges.push({
|
|
1568
|
+
hostSet: host.dir,
|
|
1569
|
+
hostRep: slug,
|
|
1570
|
+
instanceId: inst.id,
|
|
1571
|
+
instanceName: inst.name,
|
|
1572
|
+
kind: "hidden",
|
|
1573
|
+
partners: [],
|
|
1574
|
+
disclosures: [
|
|
1575
|
+
`HIDDEN in every recorded pose \u2014 the designer's off-switch is unambiguous, so no composition is proposed (obligations require visible recorded truth). If this region should compose, add a pose that SHOWS the instance and re-record.`
|
|
1576
|
+
]
|
|
1577
|
+
});
|
|
1578
|
+
continue;
|
|
1579
|
+
}
|
|
1464
1580
|
const tails = tailsByHead.get(inst.id) ?? /* @__PURE__ */ new Map();
|
|
1465
1581
|
const disclosures = [];
|
|
1466
1582
|
const refused = [];
|
|
@@ -1488,7 +1604,7 @@ function composeReport(index) {
|
|
|
1488
1604
|
}
|
|
1489
1605
|
const eligible = [];
|
|
1490
1606
|
for (const g of groups) {
|
|
1491
|
-
const uncaptured = [host.figmaFile === void 0 ? "the host set" : void 0, ...g.map((m) => m.figmaFile === void 0 ?
|
|
1607
|
+
const uncaptured = [host.figmaFile === void 0 ? "the host set" : void 0, ...g.map((m) => m.figmaFile === void 0 ? path3.basename(m.dir) : void 0)].filter(
|
|
1492
1608
|
(x) => x !== void 0
|
|
1493
1609
|
);
|
|
1494
1610
|
if (uncaptured.length > 0) {
|
|
@@ -1600,11 +1716,11 @@ function composeReport(index) {
|
|
|
1600
1716
|
return edges;
|
|
1601
1717
|
}
|
|
1602
1718
|
function confirmedCompositionStatus(hostSet) {
|
|
1603
|
-
const manifestFile =
|
|
1604
|
-
if (!
|
|
1719
|
+
const manifestFile = path3.join(hostSet, "recording-set.json");
|
|
1720
|
+
if (!existsSync3(manifestFile)) return { rows: [] };
|
|
1605
1721
|
let rawEntries;
|
|
1606
1722
|
try {
|
|
1607
|
-
const parsed = JSON.parse(
|
|
1723
|
+
const parsed = JSON.parse(readFileSync3(manifestFile, "utf8"));
|
|
1608
1724
|
rawEntries = Array.isArray(parsed["compositions"]) ? parsed["compositions"] : [];
|
|
1609
1725
|
} catch {
|
|
1610
1726
|
return { rows: [], malformed: "the recording-set manifest is not readable JSON" };
|
|
@@ -1623,11 +1739,11 @@ function confirmedCompositionStatus(hostSet) {
|
|
|
1623
1739
|
const rows = [];
|
|
1624
1740
|
for (const entry of confirmed) {
|
|
1625
1741
|
const partnerRels = Object.keys(entry.partner.manifestSha256).map(fromStoredRel);
|
|
1626
|
-
const partnerDirs = partnerRels.map((rel) =>
|
|
1742
|
+
const partnerDirs = partnerRels.map((rel) => path3.resolve(hostSet, rel));
|
|
1627
1743
|
const entryKey = fromStoredRel(entry.partner.key);
|
|
1628
1744
|
const affectedReps = [...new Set(entry.instances.map((i) => i.hostRep))];
|
|
1629
1745
|
const remediation = `re-run \`tendril compose --set ${hostSet}\` after repairing`;
|
|
1630
|
-
const missing = partnerDirs.filter((d) => !
|
|
1746
|
+
const missing = partnerDirs.filter((d) => !existsSync3(path3.join(d, "recording-set.json")));
|
|
1631
1747
|
if (missing.length > 0) {
|
|
1632
1748
|
rows.push({
|
|
1633
1749
|
key: entryKey,
|
|
@@ -1635,17 +1751,17 @@ function confirmedCompositionStatus(hostSet) {
|
|
|
1635
1751
|
status: "partner-missing",
|
|
1636
1752
|
instances: entry.instances,
|
|
1637
1753
|
affectedReps,
|
|
1638
|
-
detail: `confirmed partner set(s) not found: ${missing.map((d) => toPosixRel(
|
|
1754
|
+
detail: `confirmed partner set(s) not found: ${missing.map((d) => toPosixRel(path3.relative(hostSet, d))).join(", ")} \u2014 the decision names recordings that are not there; ${remediation}`
|
|
1639
1755
|
});
|
|
1640
1756
|
continue;
|
|
1641
1757
|
}
|
|
1642
1758
|
let stale = false;
|
|
1643
1759
|
let unreadable;
|
|
1644
1760
|
for (const rel of partnerRels) {
|
|
1645
|
-
const file =
|
|
1761
|
+
const file = path3.join(path3.resolve(hostSet, rel), "recording-set.json");
|
|
1646
1762
|
try {
|
|
1647
|
-
const bytes =
|
|
1648
|
-
loadManifest(
|
|
1763
|
+
const bytes = readFileSync3(file);
|
|
1764
|
+
loadManifest(path3.resolve(hostSet, rel));
|
|
1649
1765
|
const storedSha = entry.partner.manifestSha256[Object.keys(entry.partner.manifestSha256).find((k) => fromStoredRel(k) === rel)];
|
|
1650
1766
|
if (createHashHex(bytes) !== storedSha) stale = true;
|
|
1651
1767
|
} catch (err) {
|
|
@@ -1667,8 +1783,8 @@ function confirmedCompositionStatus(hostSet) {
|
|
|
1667
1783
|
const edges = composeReport(buildComposeIndex([hostSet, ...partnerDirs]));
|
|
1668
1784
|
const supportedInstances = /* @__PURE__ */ new Map();
|
|
1669
1785
|
for (const e of edges) {
|
|
1670
|
-
if (e.hostSet !==
|
|
1671
|
-
if (pairKeyFor(
|
|
1786
|
+
if (e.hostSet !== path3.resolve(hostSet) || e.kind !== "substitution" || e.pose === void 0) continue;
|
|
1787
|
+
if (pairKeyFor(path3.resolve(hostSet), e.partners.map((p) => p.dir)) !== entryKey) continue;
|
|
1672
1788
|
supportedInstances.set(`${e.hostRep}\0${e.instanceId}`, e.pose.variantNodeId);
|
|
1673
1789
|
}
|
|
1674
1790
|
const unsupported = entry.instances.filter((i) => supportedInstances.get(`${i.hostRep}\0${i.instanceId}`) !== i.poseVariantNodeId);
|
|
@@ -1679,7 +1795,7 @@ function confirmedCompositionStatus(hostSet) {
|
|
|
1679
1795
|
status: ok ? stale ? "stale-supported" : "supported" : stale ? "stale-unsupported" : "unsupported",
|
|
1680
1796
|
instances: entry.instances,
|
|
1681
1797
|
affectedReps: ok ? [] : [...new Set(unsupported.map((i) => i.hostRep))],
|
|
1682
|
-
detail: ok ? stale ? "the partner manifest changed since the decision (pinned bytes differ) \u2014 the CURRENT recordings still support every confirmed edge" : "the recordings support every confirmed edge (identity, rep attribution and pose re-derived)" : `${unsupported.length} confirmed instance(s) are NOT supported by the current recordings (${unsupported.map((i) => `${i.hostRep}/${i.instanceId}`).join(", ")}) \u2014 a confirmed composition the evidence does not derive; ${remediation}`
|
|
1798
|
+
detail: ok ? stale ? "the partner manifest changed since the decision (pinned bytes differ) \u2014 the CURRENT recordings still support every confirmed edge" : "the recordings support every confirmed edge (identity, rep attribution and pose re-derived)" : `${unsupported.length} confirmed instance(s) are NOT supported by the current recordings (${unsupported.map((i) => `${i.hostRep}/${i.instanceId}`).join(", ")}) \u2014 a confirmed composition the evidence does not derive; ${remediation}. If the instance is HIDDEN in every recorded pose, this confirmation predates the visibility principle (Cycle A) and re-running compose cannot re-open it (asked-once): either re-record a pose that SHOWS the instance, or remove this entry from the manifest's compositions array by hand \u2014 a retire flag is a named follow-up`
|
|
1683
1799
|
});
|
|
1684
1800
|
}
|
|
1685
1801
|
return { rows, ...malformedEntries.length > 0 ? { malformed: malformedEntries.join("; ") } : {} };
|
|
@@ -1693,6 +1809,7 @@ var init_compose = __esm({
|
|
|
1693
1809
|
"use strict";
|
|
1694
1810
|
init_session();
|
|
1695
1811
|
init_normalize();
|
|
1812
|
+
init_visibility();
|
|
1696
1813
|
CompositionEntrySchema = z5.object({
|
|
1697
1814
|
v: z5.literal(1),
|
|
1698
1815
|
partner: z5.object({
|
|
@@ -1706,39 +1823,53 @@ var init_compose = __esm({
|
|
|
1706
1823
|
});
|
|
1707
1824
|
norm = (s) => s.toLowerCase().replace(/\s+/g, " ").trim();
|
|
1708
1825
|
FOOTER = /Node ids have been added to the code as data attributes/i;
|
|
1709
|
-
kitLabel = (e) => `${e.displayName} [${
|
|
1710
|
-
toPosixRel = (rel) => rel.split(
|
|
1826
|
+
kitLabel = (e) => `${e.displayName} [${path3.basename(e.dir)}${e.figmaFile !== void 0 ? `, file ${e.figmaFile}` : ", file identity NOT captured"}]`;
|
|
1827
|
+
toPosixRel = (rel) => rel.split(path3.sep).join("/");
|
|
1711
1828
|
fromStoredRel = (rel) => rel.replace(/\\/g, "/");
|
|
1712
|
-
pairKeyFor = (hostSet, dirs) => dirs.map((d) => toPosixRel(
|
|
1829
|
+
pairKeyFor = (hostSet, dirs) => dirs.map((d) => toPosixRel(path3.relative(hostSet, d))).sort().join("+");
|
|
1713
1830
|
}
|
|
1714
1831
|
});
|
|
1715
1832
|
|
|
1716
1833
|
// packages/figma/src/recording/roles.ts
|
|
1717
|
-
import { existsSync as
|
|
1718
|
-
import
|
|
1834
|
+
import { existsSync as existsSync4, readFileSync as readFileSync4 } from "node:fs";
|
|
1835
|
+
import path4 from "node:path";
|
|
1719
1836
|
function emissionIdSets(emission) {
|
|
1720
1837
|
const own = /* @__PURE__ */ new Set();
|
|
1721
1838
|
const referenced = /* @__PURE__ */ new Set();
|
|
1839
|
+
const tailsByHead = /* @__PURE__ */ new Map();
|
|
1722
1840
|
for (const m of emission.matchAll(NODE_ID_RE)) {
|
|
1723
1841
|
const id = m[1];
|
|
1724
1842
|
if (id.startsWith("I")) {
|
|
1725
1843
|
const segments = id.slice(1).split(";");
|
|
1726
1844
|
const head = segments[0];
|
|
1727
|
-
if (head !== void 0)
|
|
1845
|
+
if (head !== void 0) {
|
|
1846
|
+
own.add(head);
|
|
1847
|
+
const tails = tailsByHead.get(head) ?? tailsByHead.set(head, /* @__PURE__ */ new Set()).get(head);
|
|
1848
|
+
for (const seg of segments.slice(1)) tails.add(seg);
|
|
1849
|
+
}
|
|
1728
1850
|
for (const seg of segments.slice(1)) referenced.add(seg);
|
|
1729
1851
|
} else {
|
|
1730
1852
|
own.add(id);
|
|
1731
1853
|
}
|
|
1732
1854
|
}
|
|
1733
|
-
return { own, referenced };
|
|
1855
|
+
return { own, referenced, tailsByHead };
|
|
1734
1856
|
}
|
|
1735
1857
|
function deriveRoles(setDir, manifest) {
|
|
1736
1858
|
const m = manifest ?? loadManifest(setDir);
|
|
1737
|
-
const reps = m.reps.filter((r) =>
|
|
1859
|
+
const reps = m.reps.filter((r) => existsSync4(path4.join(setDir, r.slug, "get_design_context.json")));
|
|
1738
1860
|
const sets = /* @__PURE__ */ new Map();
|
|
1739
1861
|
for (const rep of reps) {
|
|
1740
|
-
const env = JSON.parse(
|
|
1862
|
+
const env = JSON.parse(readFileSync4(path4.join(setDir, rep.slug, "get_design_context.json"), "utf8"));
|
|
1741
1863
|
const ids = emissionIdSets(env.content.map((c) => c.text ?? "").join("\n"));
|
|
1864
|
+
const hiddenHeads = repVisibility(setDir, rep.slug)?.hiddenIds;
|
|
1865
|
+
if (hiddenHeads !== void 0) {
|
|
1866
|
+
const visibleTails = /* @__PURE__ */ new Set();
|
|
1867
|
+
for (const [head, tails] of ids.tailsByHead) if (!hiddenHeads.has(head)) for (const t of tails) visibleTails.add(t);
|
|
1868
|
+
for (const [head, tails] of ids.tailsByHead) {
|
|
1869
|
+
if (!hiddenHeads.has(head)) continue;
|
|
1870
|
+
for (const t of tails) if (!visibleTails.has(t)) ids.referenced.delete(t);
|
|
1871
|
+
}
|
|
1872
|
+
}
|
|
1742
1873
|
ids.own.add(rep.nodeId);
|
|
1743
1874
|
sets.set(rep.slug, ids);
|
|
1744
1875
|
}
|
|
@@ -1785,6 +1916,7 @@ var init_roles = __esm({
|
|
|
1785
1916
|
"packages/figma/src/recording/roles.ts"() {
|
|
1786
1917
|
"use strict";
|
|
1787
1918
|
init_session();
|
|
1919
|
+
init_visibility();
|
|
1788
1920
|
NODE_ID_RE = /data-node-id="([^"]+)"/g;
|
|
1789
1921
|
}
|
|
1790
1922
|
});
|
|
@@ -1806,6 +1938,7 @@ var init_src = __esm({
|
|
|
1806
1938
|
init_axis_defaults();
|
|
1807
1939
|
init_plan();
|
|
1808
1940
|
init_compose();
|
|
1941
|
+
init_visibility();
|
|
1809
1942
|
init_session();
|
|
1810
1943
|
init_envelope_content();
|
|
1811
1944
|
init_roles();
|
|
@@ -1816,8 +1949,8 @@ var init_src = __esm({
|
|
|
1816
1949
|
function variableNameToPath(name) {
|
|
1817
1950
|
return name.split("/").map(canonicalCssIdentPart).filter((part) => part.length > 0);
|
|
1818
1951
|
}
|
|
1819
|
-
function tokenPathToCssVar(
|
|
1820
|
-
return `--${
|
|
1952
|
+
function tokenPathToCssVar(path46) {
|
|
1953
|
+
return `--${path46.join("-")}`;
|
|
1821
1954
|
}
|
|
1822
1955
|
function toDtcgToken(variable, defaultMode) {
|
|
1823
1956
|
const modes = Object.keys(variable.valuesByMode);
|
|
@@ -1861,11 +1994,11 @@ function toDtcgToken(variable, defaultMode) {
|
|
|
1861
1994
|
}
|
|
1862
1995
|
function mapVariablesToDtcg(variables, defaultMode = "light") {
|
|
1863
1996
|
const entries = variables.map((variable) => {
|
|
1864
|
-
const
|
|
1865
|
-
if (
|
|
1997
|
+
const path46 = variableNameToPath(variable.name);
|
|
1998
|
+
if (path46.length === 0) {
|
|
1866
1999
|
throw new DtcgMappingError("empty-name", `Figma variable ${variable.id} has an empty name`);
|
|
1867
2000
|
}
|
|
1868
|
-
return { variable, path:
|
|
2001
|
+
return { variable, path: path46 };
|
|
1869
2002
|
});
|
|
1870
2003
|
const groupPrefixes = /* @__PURE__ */ new Set();
|
|
1871
2004
|
for (const e of entries) {
|
|
@@ -1886,21 +2019,21 @@ function mapVariablesToDtcg(variables, defaultMode = "light") {
|
|
|
1886
2019
|
}
|
|
1887
2020
|
const tokens = {};
|
|
1888
2021
|
const flat = [];
|
|
1889
|
-
for (const { variable, path:
|
|
2022
|
+
for (const { variable, path: path46 } of entries) {
|
|
1890
2023
|
const token = toDtcgToken(variable, defaultMode);
|
|
1891
2024
|
let group = tokens;
|
|
1892
|
-
for (const segment of
|
|
2025
|
+
for (const segment of path46.slice(0, -1)) {
|
|
1893
2026
|
const existing = group[segment];
|
|
1894
2027
|
group = existing ?? (group[segment] = {});
|
|
1895
2028
|
}
|
|
1896
|
-
const leaf =
|
|
2029
|
+
const leaf = path46[path46.length - 1];
|
|
1897
2030
|
if (group[leaf] !== void 0) {
|
|
1898
|
-
throw new DtcgMappingError("duplicate-path", `Duplicate token path "${
|
|
2031
|
+
throw new DtcgMappingError("duplicate-path", `Duplicate token path "${path46.join(".")}" (variable ${variable.id})`);
|
|
1899
2032
|
}
|
|
1900
2033
|
group[leaf] = token;
|
|
1901
2034
|
flat.push({
|
|
1902
|
-
path:
|
|
1903
|
-
cssVar: tokenPathToCssVar(
|
|
2035
|
+
path: path46.join("."),
|
|
2036
|
+
cssVar: tokenPathToCssVar(path46),
|
|
1904
2037
|
type: token.$type,
|
|
1905
2038
|
value: token.$value
|
|
1906
2039
|
});
|
|
@@ -2089,9 +2222,9 @@ function boundId(value) {
|
|
|
2089
2222
|
return isObject(value) && typeof value["boundVariableId"] === "string" ? value["boundVariableId"] : void 0;
|
|
2090
2223
|
}
|
|
2091
2224
|
function resolveBinding(ctx, id) {
|
|
2092
|
-
const
|
|
2093
|
-
if (
|
|
2094
|
-
return
|
|
2225
|
+
const path46 = ctx.pathById.get(id);
|
|
2226
|
+
if (path46 === void 0) ctx.unresolved.add(id);
|
|
2227
|
+
return path46;
|
|
2095
2228
|
}
|
|
2096
2229
|
function parseVariantProps(name) {
|
|
2097
2230
|
if (!name.includes("=")) return void 0;
|
|
@@ -2126,8 +2259,8 @@ function walk(ctx, raw) {
|
|
|
2126
2259
|
if (!isObject(paint) || paint["visible"] === false) continue;
|
|
2127
2260
|
const id = boundId(paint);
|
|
2128
2261
|
if (id !== void 0) {
|
|
2129
|
-
const
|
|
2130
|
-
if (
|
|
2262
|
+
const path46 = resolveBinding(ctx, id);
|
|
2263
|
+
if (path46 !== void 0) tokens.add(path46);
|
|
2131
2264
|
} else if (typeof paint["color"] === "string") {
|
|
2132
2265
|
ctx.hardcoded.push({ node: name, property, value: paint["color"] });
|
|
2133
2266
|
}
|
|
@@ -2135,8 +2268,8 @@ function walk(ctx, raw) {
|
|
|
2135
2268
|
}
|
|
2136
2269
|
const radiusId = boundId(raw["cornerRadius"]);
|
|
2137
2270
|
if (radiusId !== void 0) {
|
|
2138
|
-
const
|
|
2139
|
-
if (
|
|
2271
|
+
const path46 = resolveBinding(ctx, radiusId);
|
|
2272
|
+
if (path46 !== void 0) tokens.add(path46);
|
|
2140
2273
|
} else if (typeof raw["cornerRadius"] === "number" && raw["cornerRadius"] !== 0) {
|
|
2141
2274
|
ctx.hardcoded.push({ node: name, property: "border-radius", value: `${raw["cornerRadius"]}px` });
|
|
2142
2275
|
}
|
|
@@ -2146,10 +2279,10 @@ function walk(ctx, raw) {
|
|
|
2146
2279
|
layout = { mode: layoutMode === "HORIZONTAL" ? "flex-row" : "flex-column" };
|
|
2147
2280
|
const gapId = boundId(raw["itemSpacing"]);
|
|
2148
2281
|
if (gapId !== void 0) {
|
|
2149
|
-
const
|
|
2150
|
-
if (
|
|
2151
|
-
layout.gap =
|
|
2152
|
-
tokens.add(
|
|
2282
|
+
const path46 = resolveBinding(ctx, gapId);
|
|
2283
|
+
if (path46 !== void 0) {
|
|
2284
|
+
layout.gap = path46;
|
|
2285
|
+
tokens.add(path46);
|
|
2153
2286
|
}
|
|
2154
2287
|
} else if (typeof raw["itemSpacing"] === "number" && raw["itemSpacing"] !== 0) {
|
|
2155
2288
|
ctx.hardcoded.push({ node: name, property: "gap", value: `${raw["itemSpacing"]}px` });
|
|
@@ -2158,10 +2291,10 @@ function walk(ctx, raw) {
|
|
|
2158
2291
|
for (const field of PADDING_FIELDS) {
|
|
2159
2292
|
const id = boundId(raw[field]);
|
|
2160
2293
|
if (id !== void 0) {
|
|
2161
|
-
const
|
|
2162
|
-
if (
|
|
2163
|
-
paddingPaths.push(
|
|
2164
|
-
tokens.add(
|
|
2294
|
+
const path46 = resolveBinding(ctx, id);
|
|
2295
|
+
if (path46 !== void 0) {
|
|
2296
|
+
paddingPaths.push(path46);
|
|
2297
|
+
tokens.add(path46);
|
|
2165
2298
|
}
|
|
2166
2299
|
} else if (typeof raw[field] === "number" && raw[field] !== 0) {
|
|
2167
2300
|
ctx.hardcoded.push({ node: name, property: field, value: `${raw[field]}px` });
|
|
@@ -2369,25 +2502,25 @@ var init_src3 = __esm({
|
|
|
2369
2502
|
});
|
|
2370
2503
|
|
|
2371
2504
|
// packages/cli/src/invocation.ts
|
|
2372
|
-
import { existsSync as
|
|
2373
|
-
import
|
|
2505
|
+
import { existsSync as existsSync5, realpathSync } from "node:fs";
|
|
2506
|
+
import path5 from "node:path";
|
|
2374
2507
|
import { fileURLToPath } from "node:url";
|
|
2375
2508
|
function findPathTendril(pathEnv, platform) {
|
|
2376
|
-
const dirs = pathEnv.split(
|
|
2509
|
+
const dirs = pathEnv.split(path5.delimiter).filter((d) => d !== "" && !/node_modules[\\/]\.bin/.test(d) && !/[\\/]_npx[\\/]/.test(d));
|
|
2377
2510
|
const names = platform === "win32" ? ["tendril.cmd", "tendril.bat"] : ["tendril"];
|
|
2378
2511
|
for (const dir of dirs) {
|
|
2379
2512
|
for (const name of names) {
|
|
2380
|
-
const candidate =
|
|
2381
|
-
if (
|
|
2513
|
+
const candidate = path5.join(dir, name);
|
|
2514
|
+
if (existsSync5(candidate)) return candidate;
|
|
2382
2515
|
}
|
|
2383
2516
|
}
|
|
2384
2517
|
return null;
|
|
2385
2518
|
}
|
|
2386
2519
|
function packageRootOf(file) {
|
|
2387
|
-
let dir =
|
|
2520
|
+
let dir = path5.dirname(file);
|
|
2388
2521
|
for (; ; ) {
|
|
2389
|
-
if (
|
|
2390
|
-
const parent =
|
|
2522
|
+
if (existsSync5(path5.join(dir, "package.json"))) return dir;
|
|
2523
|
+
const parent = path5.dirname(dir);
|
|
2391
2524
|
if (parent === dir) return null;
|
|
2392
2525
|
dir = parent;
|
|
2393
2526
|
}
|
|
@@ -2426,18 +2559,18 @@ var init_invocation = __esm({
|
|
|
2426
2559
|
|
|
2427
2560
|
// packages/verify/src/browser.ts
|
|
2428
2561
|
import { execFileSync } from "node:child_process";
|
|
2429
|
-
import { existsSync as
|
|
2430
|
-
import
|
|
2562
|
+
import { existsSync as existsSync6, readdirSync as readdirSync2 } from "node:fs";
|
|
2563
|
+
import path6 from "node:path";
|
|
2431
2564
|
function resolveChrome() {
|
|
2432
2565
|
const fromEnv = process.env["TENDRIL_CHROME"] ?? process.env["CHROME_PATH"];
|
|
2433
2566
|
if (fromEnv !== void 0 && fromEnv !== "") {
|
|
2434
|
-
if (!
|
|
2567
|
+
if (!existsSync6(fromEnv)) {
|
|
2435
2568
|
throw new Error(`CHROME_PATH points at ${fromEnv}, which does not exist \u2014 fix the variable or unset it to use discovery`);
|
|
2436
2569
|
}
|
|
2437
2570
|
return fromEnv;
|
|
2438
2571
|
}
|
|
2439
2572
|
const candidates = process.platform === "darwin" ? MAC_CANDIDATES : process.platform === "win32" ? WIN_CANDIDATES : [];
|
|
2440
|
-
for (const c of candidates) if (
|
|
2573
|
+
for (const c of candidates) if (existsSync6(c)) return c;
|
|
2441
2574
|
if (process.platform !== "win32") {
|
|
2442
2575
|
for (const name of PATH_NAMES) {
|
|
2443
2576
|
try {
|
|
@@ -2451,7 +2584,7 @@ function resolveChrome() {
|
|
|
2451
2584
|
}
|
|
2452
2585
|
function versionFromInstallDir(exePath) {
|
|
2453
2586
|
try {
|
|
2454
|
-
const builds = readdirSync2(
|
|
2587
|
+
const builds = readdirSync2(path6.dirname(exePath), { withFileTypes: true }).filter((e) => e.isDirectory() && /^\d+(\.\d+){3}$/.test(e.name)).map((e) => e.name.split(".").map(Number));
|
|
2455
2588
|
builds.sort((a, b) => a[0] - b[0] || a[1] - b[1] || a[2] - b[2] || a[3] - b[3]);
|
|
2456
2589
|
return builds.length === 0 ? null : builds[builds.length - 1].join(".");
|
|
2457
2590
|
} catch {
|
|
@@ -2463,7 +2596,7 @@ function windowsBrowserName(exePath) {
|
|
|
2463
2596
|
if (p.includes("\\google\\chrome\\")) return "Google Chrome";
|
|
2464
2597
|
if (p.includes("\\microsoft\\edge\\")) return "Microsoft Edge";
|
|
2465
2598
|
if (p.includes("chromium")) return "Chromium";
|
|
2466
|
-
return
|
|
2599
|
+
return path6.win32.basename(exePath, ".exe");
|
|
2467
2600
|
}
|
|
2468
2601
|
function chromeVersion() {
|
|
2469
2602
|
if (_version !== void 0) return _version;
|
|
@@ -2498,32 +2631,32 @@ var init_browser = __esm({
|
|
|
2498
2631
|
});
|
|
2499
2632
|
|
|
2500
2633
|
// packages/verify/src/runtime.ts
|
|
2501
|
-
import { existsSync as
|
|
2634
|
+
import { existsSync as existsSync7, mkdtempSync, symlinkSync } from "node:fs";
|
|
2502
2635
|
import { createRequire } from "node:module";
|
|
2503
2636
|
import os from "node:os";
|
|
2504
|
-
import
|
|
2637
|
+
import path7 from "node:path";
|
|
2505
2638
|
import { fileURLToPath as fileURLToPath2 } from "node:url";
|
|
2506
2639
|
function runtimePackageRoot() {
|
|
2507
2640
|
const env = process.env["TENDRIL_PACKAGE_ROOT"];
|
|
2508
|
-
if (env !== void 0 && env !== "") return
|
|
2509
|
-
return
|
|
2641
|
+
if (env !== void 0 && env !== "") return path7.resolve(env);
|
|
2642
|
+
return path7.resolve(path7.dirname(fileURLToPath2(import.meta.url)), "..");
|
|
2510
2643
|
}
|
|
2511
2644
|
function runtimeNodeModules() {
|
|
2512
2645
|
const root = runtimePackageRoot();
|
|
2513
|
-
for (let dir = root; ; dir =
|
|
2514
|
-
const candidate =
|
|
2515
|
-
if (
|
|
2516
|
-
if (
|
|
2646
|
+
for (let dir = root; ; dir = path7.dirname(dir)) {
|
|
2647
|
+
const candidate = path7.basename(dir) === "node_modules" ? dir : path7.join(dir, "node_modules");
|
|
2648
|
+
if (existsSync7(path7.join(candidate, "react"))) return candidate;
|
|
2649
|
+
if (path7.dirname(dir) === dir) break;
|
|
2517
2650
|
}
|
|
2518
2651
|
throw new Error(
|
|
2519
2652
|
`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.`
|
|
2520
2653
|
);
|
|
2521
2654
|
}
|
|
2522
2655
|
function newScratchDir(prefix) {
|
|
2523
|
-
const dir = mkdtempSync(
|
|
2656
|
+
const dir = mkdtempSync(path7.join(os.tmpdir(), `tendril-${prefix}-`));
|
|
2524
2657
|
const nodeModules = runtimeNodeModules();
|
|
2525
2658
|
try {
|
|
2526
|
-
symlinkSync(nodeModules,
|
|
2659
|
+
symlinkSync(nodeModules, path7.join(dir, "node_modules"), "junction");
|
|
2527
2660
|
} catch (err) {
|
|
2528
2661
|
throw new Error(
|
|
2529
2662
|
`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.`
|
|
@@ -2532,7 +2665,7 @@ function newScratchDir(prefix) {
|
|
|
2532
2665
|
return dir;
|
|
2533
2666
|
}
|
|
2534
2667
|
function reactPinPlugin() {
|
|
2535
|
-
const req = createRequire(
|
|
2668
|
+
const req = createRequire(path7.join(runtimePackageRoot(), "package.json"));
|
|
2536
2669
|
return {
|
|
2537
2670
|
name: "tendril-react-pin",
|
|
2538
2671
|
setup(b) {
|
|
@@ -2585,9 +2718,9 @@ var init_gates = __esm({
|
|
|
2585
2718
|
|
|
2586
2719
|
// packages/verify/src/tsc-check.ts
|
|
2587
2720
|
import ts from "typescript";
|
|
2588
|
-
import
|
|
2721
|
+
import path8 from "node:path";
|
|
2589
2722
|
function runTscStrict(files) {
|
|
2590
|
-
const program = ts.createProgram(files.map((f) =>
|
|
2723
|
+
const program = ts.createProgram(files.map((f) => path8.resolve(f)), STRICT_OPTIONS);
|
|
2591
2724
|
const diagnostics = ts.getPreEmitDiagnostics(program);
|
|
2592
2725
|
const mapped = diagnostics.map((d) => {
|
|
2593
2726
|
const file = d.file?.fileName;
|
|
@@ -2748,7 +2881,7 @@ var init_token_lint = __esm({
|
|
|
2748
2881
|
|
|
2749
2882
|
// packages/verify/src/loop.ts
|
|
2750
2883
|
import { rmSync, writeFileSync as writeFileSync2 } from "node:fs";
|
|
2751
|
-
import
|
|
2884
|
+
import path9 from "node:path";
|
|
2752
2885
|
async function runChecks(componentName, files, extraFiles, definedVars2) {
|
|
2753
2886
|
const findings = [];
|
|
2754
2887
|
for (const violation of scanForbiddenPatterns(files.tsx)) {
|
|
@@ -2763,10 +2896,10 @@ async function runChecks(componentName, files, extraFiles, definedVars2) {
|
|
|
2763
2896
|
}
|
|
2764
2897
|
const workDir = newScratchDir("loop");
|
|
2765
2898
|
try {
|
|
2766
|
-
const tsxPath =
|
|
2899
|
+
const tsxPath = path9.join(workDir, `${componentName}.tsx`);
|
|
2767
2900
|
writeFileSync2(tsxPath, files.tsx);
|
|
2768
2901
|
for (const [name, content] of Object.entries(extraFiles ?? {})) {
|
|
2769
|
-
writeFileSync2(
|
|
2902
|
+
writeFileSync2(path9.join(workDir, name), content);
|
|
2770
2903
|
}
|
|
2771
2904
|
const tsc = runTscStrict([tsxPath]);
|
|
2772
2905
|
for (const d of tsc.diagnostics) {
|
|
@@ -3417,7 +3550,7 @@ var init_image_diff = __esm({
|
|
|
3417
3550
|
});
|
|
3418
3551
|
|
|
3419
3552
|
// packages/verify/src/visual-facts.ts
|
|
3420
|
-
import
|
|
3553
|
+
import path10 from "node:path";
|
|
3421
3554
|
import { fileURLToPath as fileURLToPath3 } from "node:url";
|
|
3422
3555
|
import { build } from "esbuild";
|
|
3423
3556
|
import { chromium } from "playwright-core";
|
|
@@ -3962,7 +4095,7 @@ var init_visual_facts = __esm({
|
|
|
3962
4095
|
init_browser();
|
|
3963
4096
|
init_mount_limits();
|
|
3964
4097
|
init_image_diff();
|
|
3965
|
-
RESOLVE_DIR =
|
|
4098
|
+
RESOLVE_DIR = path10.resolve(path10.dirname(fileURLToPath3(import.meta.url)), "..");
|
|
3966
4099
|
TOLERANCE_PX = 2;
|
|
3967
4100
|
WIDTH_SLACK = 0.25;
|
|
3968
4101
|
IMAGE_SIMILARITY_FLOOR = 0.8;
|
|
@@ -3971,14 +4104,14 @@ var init_visual_facts = __esm({
|
|
|
3971
4104
|
});
|
|
3972
4105
|
|
|
3973
4106
|
// packages/verify/src/paths.ts
|
|
3974
|
-
import
|
|
4107
|
+
import path11 from "node:path";
|
|
3975
4108
|
import { fileURLToPath as fileURLToPath4 } from "node:url";
|
|
3976
4109
|
var VERIFY_PKG_DIR, REPO_ROOT;
|
|
3977
4110
|
var init_paths = __esm({
|
|
3978
4111
|
"packages/verify/src/paths.ts"() {
|
|
3979
4112
|
"use strict";
|
|
3980
|
-
VERIFY_PKG_DIR =
|
|
3981
|
-
REPO_ROOT =
|
|
4113
|
+
VERIFY_PKG_DIR = path11.resolve(path11.dirname(fileURLToPath4(import.meta.url)), "..");
|
|
4114
|
+
REPO_ROOT = path11.resolve(VERIFY_PKG_DIR, "..", "..");
|
|
3982
4115
|
}
|
|
3983
4116
|
});
|
|
3984
4117
|
|
|
@@ -4097,9 +4230,9 @@ var init_font_collection = __esm({
|
|
|
4097
4230
|
});
|
|
4098
4231
|
|
|
4099
4232
|
// packages/verify/src/font-discovery.ts
|
|
4100
|
-
import { existsSync as
|
|
4233
|
+
import { existsSync as existsSync8, readdirSync as readdirSync3, readFileSync as readFileSync5, realpathSync as realpathSync2 } from "node:fs";
|
|
4101
4234
|
import os2 from "node:os";
|
|
4102
|
-
import
|
|
4235
|
+
import path12 from "node:path";
|
|
4103
4236
|
function weightFromSubfamily(subfamily) {
|
|
4104
4237
|
for (const [re, w] of WEIGHT_TOKENS) if (re.test(subfamily)) return w;
|
|
4105
4238
|
return void 0;
|
|
@@ -4145,7 +4278,7 @@ function faceAt(bytes, view, dirOffset, file, faceIndex) {
|
|
|
4145
4278
|
}
|
|
4146
4279
|
function facesInFile(file) {
|
|
4147
4280
|
try {
|
|
4148
|
-
const bytes = new Uint8Array(
|
|
4281
|
+
const bytes = new Uint8Array(readFileSync5(file));
|
|
4149
4282
|
if (bytes.length < 12) return [];
|
|
4150
4283
|
const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
|
|
4151
4284
|
if (isCollection(bytes)) {
|
|
@@ -4165,13 +4298,13 @@ function facesInFile(file) {
|
|
|
4165
4298
|
}
|
|
4166
4299
|
function systemFontDirs() {
|
|
4167
4300
|
const env = process.env["TENDRIL_SYSTEM_FONT_DIRS"];
|
|
4168
|
-
if (env !== void 0 && env !== "") return env.split(
|
|
4301
|
+
if (env !== void 0 && env !== "") return env.split(path12.delimiter).filter((d) => d !== "" && existsSync8(d));
|
|
4169
4302
|
const home = os2.homedir();
|
|
4170
|
-
const dirs = process.platform === "darwin" ? ["/System/Library/Fonts", "/Library/Fonts",
|
|
4171
|
-
|
|
4172
|
-
...process.env["LOCALAPPDATA"] !== void 0 ? [
|
|
4173
|
-
] : ["/usr/share/fonts", "/usr/local/share/fonts",
|
|
4174
|
-
return dirs.filter((d) =>
|
|
4303
|
+
const dirs = process.platform === "darwin" ? ["/System/Library/Fonts", "/Library/Fonts", path12.join(home, "Library", "Fonts")] : process.platform === "win32" ? [
|
|
4304
|
+
path12.join(process.env["WINDIR"] ?? "C:\\Windows", "Fonts"),
|
|
4305
|
+
...process.env["LOCALAPPDATA"] !== void 0 ? [path12.join(process.env["LOCALAPPDATA"], "Microsoft", "Windows", "Fonts")] : []
|
|
4306
|
+
] : ["/usr/share/fonts", "/usr/local/share/fonts", path12.join(home, ".fonts"), path12.join(home, ".local", "share", "fonts")];
|
|
4307
|
+
return dirs.filter((d) => existsSync8(d));
|
|
4175
4308
|
}
|
|
4176
4309
|
function discoverSystemFaces(dirs = systemFontDirs(), depth = 3) {
|
|
4177
4310
|
const seen = /* @__PURE__ */ new Set();
|
|
@@ -4185,10 +4318,10 @@ function discoverSystemFaces(dirs = systemFontDirs(), depth = 3) {
|
|
|
4185
4318
|
continue;
|
|
4186
4319
|
}
|
|
4187
4320
|
for (const e of entries) {
|
|
4188
|
-
const full =
|
|
4321
|
+
const full = path12.join(dir, e.name);
|
|
4189
4322
|
if (e.isDirectory()) {
|
|
4190
4323
|
if (remaining > 1) faces.push(...walk2([full], remaining - 1));
|
|
4191
|
-
} else if (FONT_EXTENSIONS.has(
|
|
4324
|
+
} else if (FONT_EXTENSIONS.has(path12.extname(e.name).toLowerCase())) {
|
|
4192
4325
|
let key = full;
|
|
4193
4326
|
try {
|
|
4194
4327
|
key = realpathSync2(full);
|
|
@@ -4237,13 +4370,13 @@ var init_font_discovery = __esm({
|
|
|
4237
4370
|
|
|
4238
4371
|
// packages/verify/src/font-resolve.ts
|
|
4239
4372
|
import { createHash as createHash2 } from "node:crypto";
|
|
4240
|
-
import { existsSync as
|
|
4373
|
+
import { existsSync as existsSync9, mkdirSync as mkdirSync2, readFileSync as readFileSync6, writeFileSync as writeFileSync3 } from "node:fs";
|
|
4241
4374
|
import os3 from "node:os";
|
|
4242
|
-
import
|
|
4375
|
+
import path13 from "node:path";
|
|
4243
4376
|
function fontCacheDir() {
|
|
4244
4377
|
const env = process.env["TENDRIL_FONT_CACHE"];
|
|
4245
|
-
if (env !== void 0 && env !== "") return
|
|
4246
|
-
return
|
|
4378
|
+
if (env !== void 0 && env !== "") return path13.resolve(env);
|
|
4379
|
+
return path13.join(os3.homedir(), ".tendril", "fonts");
|
|
4247
4380
|
}
|
|
4248
4381
|
function normalizeFontLicense(value) {
|
|
4249
4382
|
return typeof value === "string" && FONT_LICENSES.includes(value) ? value : "unknown";
|
|
@@ -4308,29 +4441,29 @@ async function resolveFonts(family, weights, cacheDir = DEFAULT_FONT_CACHE) {
|
|
|
4308
4441
|
}
|
|
4309
4442
|
const bytes = new Uint8Array(await fileRes.arrayBuffer());
|
|
4310
4443
|
const sha256 = createHash2("sha256").update(bytes).digest("hex");
|
|
4311
|
-
const file =
|
|
4444
|
+
const file = path13.join(cacheDir, `${family.toLowerCase().replace(/\s+/g, "-")}-${weight}.woff2`);
|
|
4312
4445
|
writeFileSync3(file, bytes);
|
|
4313
4446
|
resolved.push({ family, weight, source: url, sha256, file, license: meta.license, ...meta.family !== null ? { servedFamily: meta.family } : {} });
|
|
4314
4447
|
} catch (err) {
|
|
4315
4448
|
failures.push({ family, weight, reason: `download failed: ${err instanceof Error ? err.message : String(err)}` });
|
|
4316
4449
|
}
|
|
4317
4450
|
}
|
|
4318
|
-
const mPath =
|
|
4319
|
-
const prior =
|
|
4320
|
-
const portable2 = resolved.map((m) => ({ ...m, file:
|
|
4451
|
+
const mPath = path13.join(cacheDir, "manifest.json");
|
|
4452
|
+
const prior = existsSync9(mPath) ? JSON.parse(readFileSync6(mPath, "utf8")) : [];
|
|
4453
|
+
const portable2 = resolved.map((m) => ({ ...m, file: path13.basename(m.file) }));
|
|
4321
4454
|
const merged = [...prior.filter((p) => !resolved.some((m) => m.family === p.family && m.weight === p.weight)), ...portable2];
|
|
4322
4455
|
if (resolved.length > 0) writeFileSync3(mPath, `${JSON.stringify(merged, null, 2)}
|
|
4323
4456
|
`);
|
|
4324
4457
|
return { resolved, failures };
|
|
4325
4458
|
}
|
|
4326
4459
|
function addLocalFont(family, weight, filePath, cacheDir = DEFAULT_FONT_CACHE, faceIndex, provenance = "local") {
|
|
4327
|
-
const src =
|
|
4328
|
-
if (!
|
|
4329
|
-
const ext =
|
|
4460
|
+
const src = path13.resolve(filePath);
|
|
4461
|
+
if (!existsSync9(src)) throw new Error(`font file not found: ${src}`);
|
|
4462
|
+
const ext = path13.extname(src).toLowerCase();
|
|
4330
4463
|
if (![".woff2", ".woff", ".ttf", ".otf", ".ttc"].includes(ext)) {
|
|
4331
4464
|
throw new Error(`unsupported font file "${ext}" \u2014 use .woff2 (preferred), .woff, .ttf, .otf or .ttc`);
|
|
4332
4465
|
}
|
|
4333
|
-
let bytes = new Uint8Array(
|
|
4466
|
+
let bytes = new Uint8Array(readFileSync6(src));
|
|
4334
4467
|
if (bytes.length === 0) throw new Error(`font file is empty: ${src}`);
|
|
4335
4468
|
let storedExt = ext;
|
|
4336
4469
|
if (isCollection(bytes)) {
|
|
@@ -4340,7 +4473,7 @@ function addLocalFont(family, weight, filePath, cacheDir = DEFAULT_FONT_CACHE, f
|
|
|
4340
4473
|
const all = listCollectionFaces(bytes);
|
|
4341
4474
|
const shown = (candidates.length > 0 ? candidates : all).map((f) => ` --face ${f.index} ${f.family ?? "(unnamed)"}${f.subfamily !== void 0 ? ` ${f.subfamily}` : ""}`).join("\n");
|
|
4342
4475
|
throw new Error(
|
|
4343
|
-
`${
|
|
4476
|
+
`${path13.basename(src)} is a collection of ${all.length} faces and ${candidates.length === 0 ? `none is named "${family}"` : `${candidates.length} match "${family}"`} \u2014 name the one you mean with --face <index>:
|
|
4344
4477
|
${shown}`
|
|
4345
4478
|
);
|
|
4346
4479
|
}
|
|
@@ -4349,20 +4482,20 @@ ${shown}`
|
|
|
4349
4482
|
}
|
|
4350
4483
|
mkdirSync2(cacheDir, { recursive: true });
|
|
4351
4484
|
const sha256 = createHash2("sha256").update(bytes).digest("hex");
|
|
4352
|
-
const file =
|
|
4485
|
+
const file = path13.join(cacheDir, `${family.toLowerCase().replace(/\s+/g, "-")}-${weight}${storedExt}`);
|
|
4353
4486
|
writeFileSync3(file, bytes);
|
|
4354
|
-
const face = { family, weight, source: `${provenance}:${
|
|
4355
|
-
const mPath =
|
|
4356
|
-
const prior =
|
|
4357
|
-
const merged = [...prior.filter((p) => !(p.family === family && p.weight === weight)), { ...face, file:
|
|
4487
|
+
const face = { family, weight, source: `${provenance}:${path13.basename(src)}`, sha256, file, license: "unknown" };
|
|
4488
|
+
const mPath = path13.join(cacheDir, "manifest.json");
|
|
4489
|
+
const prior = existsSync9(mPath) ? JSON.parse(readFileSync6(mPath, "utf8")) : [];
|
|
4490
|
+
const merged = [...prior.filter((p) => !(p.family === family && p.weight === weight)), { ...face, file: path13.basename(file) }];
|
|
4358
4491
|
writeFileSync3(mPath, `${JSON.stringify(merged, null, 2)}
|
|
4359
4492
|
`);
|
|
4360
4493
|
return face;
|
|
4361
4494
|
}
|
|
4362
4495
|
function checkFontLock(lockPath, cacheDir = DEFAULT_FONT_CACHE) {
|
|
4363
|
-
const lock = JSON.parse(
|
|
4364
|
-
const mPath =
|
|
4365
|
-
const manifest =
|
|
4496
|
+
const lock = JSON.parse(readFileSync6(lockPath, "utf8"));
|
|
4497
|
+
const mPath = path13.join(cacheDir, "manifest.json");
|
|
4498
|
+
const manifest = existsSync9(mPath) ? JSON.parse(readFileSync6(mPath, "utf8")) : [];
|
|
4366
4499
|
return lock.map((l) => {
|
|
4367
4500
|
const m = manifest.find((x) => x.family === l.family && x.weight === l.weight);
|
|
4368
4501
|
if (m === void 0) return { family: l.family, weight: l.weight, status: "missing" };
|
|
@@ -4370,11 +4503,11 @@ function checkFontLock(lockPath, cacheDir = DEFAULT_FONT_CACHE) {
|
|
|
4370
4503
|
});
|
|
4371
4504
|
}
|
|
4372
4505
|
function resolvedFontFamilies(cacheDir = DEFAULT_FONT_CACHE) {
|
|
4373
|
-
const mPath =
|
|
4374
|
-
if (!
|
|
4506
|
+
const mPath = path13.join(cacheDir, "manifest.json");
|
|
4507
|
+
if (!existsSync9(mPath)) return [];
|
|
4375
4508
|
let entries;
|
|
4376
4509
|
try {
|
|
4377
|
-
entries = JSON.parse(
|
|
4510
|
+
entries = JSON.parse(readFileSync6(mPath, "utf8"));
|
|
4378
4511
|
} catch {
|
|
4379
4512
|
return [];
|
|
4380
4513
|
}
|
|
@@ -4388,8 +4521,8 @@ function resolvedFontFamilies(cacheDir = DEFAULT_FONT_CACHE) {
|
|
|
4388
4521
|
return [...byFamily.entries()].map(([family, w]) => ({ family, weights: [...w].sort((a, b) => a - b) }));
|
|
4389
4522
|
}
|
|
4390
4523
|
function requiredFontsManifest(families, cacheDir = DEFAULT_FONT_CACHE) {
|
|
4391
|
-
const mPath =
|
|
4392
|
-
const manifest =
|
|
4524
|
+
const mPath = path13.join(cacheDir, "manifest.json");
|
|
4525
|
+
const manifest = existsSync9(mPath) ? JSON.parse(readFileSync6(mPath, "utf8")) : [];
|
|
4393
4526
|
const wanted = new Set(families.map((f) => f.toLowerCase()));
|
|
4394
4527
|
return manifest.filter((f) => wanted.has(f.family.toLowerCase())).map((f) => ({
|
|
4395
4528
|
family: f.family,
|
|
@@ -4402,11 +4535,11 @@ function requiredFontsManifest(families, cacheDir = DEFAULT_FONT_CACHE) {
|
|
|
4402
4535
|
}));
|
|
4403
4536
|
}
|
|
4404
4537
|
function facesMissingLicense(cacheDir = DEFAULT_FONT_CACHE) {
|
|
4405
|
-
const mPath =
|
|
4406
|
-
if (!
|
|
4538
|
+
const mPath = path13.join(cacheDir, "manifest.json");
|
|
4539
|
+
if (!existsSync9(mPath)) return [];
|
|
4407
4540
|
let entries;
|
|
4408
4541
|
try {
|
|
4409
|
-
entries = JSON.parse(
|
|
4542
|
+
entries = JSON.parse(readFileSync6(mPath, "utf8"));
|
|
4410
4543
|
} catch {
|
|
4411
4544
|
return [];
|
|
4412
4545
|
}
|
|
@@ -4415,20 +4548,20 @@ function facesMissingLicense(cacheDir = DEFAULT_FONT_CACHE) {
|
|
|
4415
4548
|
).map((e) => ({ family: e.family, weight: e.weight, sha256: e.sha256 }));
|
|
4416
4549
|
}
|
|
4417
4550
|
function verifiedFontFamilies(cacheDir = DEFAULT_FONT_CACHE) {
|
|
4418
|
-
const mPath =
|
|
4419
|
-
if (!
|
|
4551
|
+
const mPath = path13.join(cacheDir, "manifest.json");
|
|
4552
|
+
if (!existsSync9(mPath)) return [];
|
|
4420
4553
|
let entries;
|
|
4421
4554
|
try {
|
|
4422
|
-
entries = JSON.parse(
|
|
4555
|
+
entries = JSON.parse(readFileSync6(mPath, "utf8"));
|
|
4423
4556
|
} catch {
|
|
4424
4557
|
return [];
|
|
4425
4558
|
}
|
|
4426
4559
|
const byFamily = /* @__PURE__ */ new Map();
|
|
4427
4560
|
for (const e of entries) {
|
|
4428
4561
|
if (typeof e.family !== "string" || typeof e.weight !== "number" || typeof e.file !== "string") continue;
|
|
4429
|
-
const file =
|
|
4430
|
-
if (!
|
|
4431
|
-
if (createHash2("sha256").update(
|
|
4562
|
+
const file = path13.isAbsolute(e.file) && existsSync9(e.file) ? e.file : path13.resolve(cacheDir, path13.basename(e.file));
|
|
4563
|
+
if (!existsSync9(file)) continue;
|
|
4564
|
+
if (createHash2("sha256").update(readFileSync6(file)).digest("hex") !== e.sha256) continue;
|
|
4432
4565
|
const set = byFamily.get(e.family) ?? /* @__PURE__ */ new Set();
|
|
4433
4566
|
set.add(e.weight);
|
|
4434
4567
|
byFamily.set(e.family, set);
|
|
@@ -4449,8 +4582,8 @@ function addSystemFamily(family, opts = {}) {
|
|
|
4449
4582
|
const skipped = [];
|
|
4450
4583
|
const overwrote = [];
|
|
4451
4584
|
const cacheDir = opts.cacheDir ?? DEFAULT_FONT_CACHE;
|
|
4452
|
-
const manifestFile =
|
|
4453
|
-
const prior =
|
|
4585
|
+
const manifestFile = path13.join(cacheDir, "manifest.json");
|
|
4586
|
+
const prior = existsSync9(manifestFile) ? JSON.parse(readFileSync6(manifestFile, "utf8")) : [];
|
|
4454
4587
|
const taken = /* @__PURE__ */ new Set();
|
|
4455
4588
|
for (const face of faces) {
|
|
4456
4589
|
const skip = (reason) => skipped.push({ subfamily: face.subfamily, weight: face.weight, reason });
|
|
@@ -4512,17 +4645,17 @@ var init_font_resolve = __esm({
|
|
|
4512
4645
|
|
|
4513
4646
|
// packages/verify/src/font-faces.ts
|
|
4514
4647
|
import { createHash as createHash3 } from "node:crypto";
|
|
4515
|
-
import { existsSync as
|
|
4516
|
-
import
|
|
4648
|
+
import { existsSync as existsSync10, readFileSync as readFileSync7 } from "node:fs";
|
|
4649
|
+
import path14 from "node:path";
|
|
4517
4650
|
function injectedGroups(manifestPath2) {
|
|
4518
|
-
if (!
|
|
4519
|
-
const claimed = JSON.parse(
|
|
4520
|
-
const resolveFile = (f) =>
|
|
4651
|
+
if (!existsSync10(manifestPath2)) return { groups: [], shared: false };
|
|
4652
|
+
const claimed = JSON.parse(readFileSync7(manifestPath2, "utf8"));
|
|
4653
|
+
const resolveFile = (f) => path14.isAbsolute(f) && existsSync10(f) ? f : path14.resolve(path14.dirname(manifestPath2), path14.basename(f));
|
|
4521
4654
|
const byFile = /* @__PURE__ */ new Map();
|
|
4522
4655
|
for (const f of claimed) {
|
|
4523
4656
|
const file = resolveFile(f.file);
|
|
4524
|
-
if (!
|
|
4525
|
-
if (createHash3("sha256").update(
|
|
4657
|
+
if (!existsSync10(file)) continue;
|
|
4658
|
+
if (createHash3("sha256").update(readFileSync7(file)).digest("hex") !== f.sha256) continue;
|
|
4526
4659
|
const k = `${f.family}:${f.file}`;
|
|
4527
4660
|
const e = byFile.get(k) ?? { family: f.family, weights: [], file };
|
|
4528
4661
|
e.weights.push(f.weight);
|
|
@@ -4531,14 +4664,14 @@ function injectedGroups(manifestPath2) {
|
|
|
4531
4664
|
const groups = [...byFile.values()];
|
|
4532
4665
|
return { groups, shared: new Set(groups.map((e) => e.file)).size < groups.length };
|
|
4533
4666
|
}
|
|
4534
|
-
function fontFaceCss(manifestPath2 =
|
|
4667
|
+
function fontFaceCss(manifestPath2 = path14.join(fontCacheDir(), "manifest.json")) {
|
|
4535
4668
|
const { groups, shared } = injectedGroups(manifestPath2);
|
|
4536
4669
|
return groups.map((e) => {
|
|
4537
4670
|
const weight = shared || e.weights.length > 1 ? `${SPAN[0]} ${SPAN[1]}` : String(e.weights[0]);
|
|
4538
|
-
return `@font-face { font-family: '${e.family}'; font-weight: ${weight}; font-style: normal; src: url(data:font/woff2;base64,${
|
|
4671
|
+
return `@font-face { font-family: '${e.family}'; font-weight: ${weight}; font-style: normal; src: url(data:font/woff2;base64,${readFileSync7(e.file).toString("base64")}) format('woff2'); }`;
|
|
4539
4672
|
}).join("\n");
|
|
4540
4673
|
}
|
|
4541
|
-
function injectedFamilyWeights(manifestPath2 =
|
|
4674
|
+
function injectedFamilyWeights(manifestPath2 = path14.join(fontCacheDir(), "manifest.json")) {
|
|
4542
4675
|
const { groups, shared } = injectedGroups(manifestPath2);
|
|
4543
4676
|
const out = /* @__PURE__ */ new Map();
|
|
4544
4677
|
for (const g of groups) {
|
|
@@ -4563,17 +4696,17 @@ var init_font_faces = __esm({
|
|
|
4563
4696
|
});
|
|
4564
4697
|
|
|
4565
4698
|
// packages/verify/src/admission.ts
|
|
4566
|
-
import { readFileSync as
|
|
4567
|
-
import
|
|
4699
|
+
import { readFileSync as readFileSync8, readdirSync as readdirSync4, existsSync as existsSync11, writeFileSync as writeFileSync4 } from "node:fs";
|
|
4700
|
+
import path15 from "node:path";
|
|
4568
4701
|
import { build as build2 } from "esbuild";
|
|
4569
4702
|
import postcss from "postcss";
|
|
4570
4703
|
import tailwindcss from "tailwindcss";
|
|
4571
4704
|
import { chromium as chromium2 } from "playwright-core";
|
|
4572
4705
|
function fontWeightsByFamily() {
|
|
4573
|
-
const mPath =
|
|
4706
|
+
const mPath = path15.join(fontCacheDir(), "manifest.json");
|
|
4574
4707
|
const out = /* @__PURE__ */ new Map();
|
|
4575
|
-
if (!
|
|
4576
|
-
for (const f of JSON.parse(
|
|
4708
|
+
if (!existsSync11(mPath)) return out;
|
|
4709
|
+
for (const f of JSON.parse(readFileSync8(mPath, "utf8")))
|
|
4577
4710
|
out.set(f.family, [...out.get(f.family) ?? [], f.weight]);
|
|
4578
4711
|
return out;
|
|
4579
4712
|
}
|
|
@@ -4593,26 +4726,26 @@ var init_admission = __esm({
|
|
|
4593
4726
|
});
|
|
4594
4727
|
|
|
4595
4728
|
// packages/verify/src/candidate-css.ts
|
|
4596
|
-
import { existsSync as
|
|
4597
|
-
import
|
|
4729
|
+
import { existsSync as existsSync12, readFileSync as readFileSync9, readdirSync as readdirSync5, statSync as statSync2 } from "node:fs";
|
|
4730
|
+
import path16 from "node:path";
|
|
4598
4731
|
function candidateCss(bundleDir) {
|
|
4599
|
-
const files = ["tokens.css", "styles.css"].map((f) =>
|
|
4600
|
-
const composedRoot =
|
|
4732
|
+
const files = ["tokens.css", "styles.css"].map((f) => path16.join(bundleDir, f));
|
|
4733
|
+
const composedRoot = path16.join(bundleDir, "composed");
|
|
4601
4734
|
let composedDirs = [];
|
|
4602
4735
|
try {
|
|
4603
4736
|
composedDirs = readdirSync5(composedRoot).sort();
|
|
4604
4737
|
} catch {
|
|
4605
4738
|
}
|
|
4606
4739
|
for (const entry of composedDirs) {
|
|
4607
|
-
const dir =
|
|
4740
|
+
const dir = path16.join(composedRoot, entry);
|
|
4608
4741
|
try {
|
|
4609
4742
|
if (!statSync2(dir).isDirectory()) continue;
|
|
4610
4743
|
} catch {
|
|
4611
4744
|
continue;
|
|
4612
4745
|
}
|
|
4613
|
-
files.push(
|
|
4746
|
+
files.push(path16.join(dir, "tokens.css"), path16.join(dir, "styles.css"));
|
|
4614
4747
|
}
|
|
4615
|
-
return files.filter((f) =>
|
|
4748
|
+
return files.filter((f) => existsSync12(f)).map((f) => readFileSync9(f, "utf8")).join("\n");
|
|
4616
4749
|
}
|
|
4617
4750
|
var init_candidate_css = __esm({
|
|
4618
4751
|
"packages/verify/src/candidate-css.ts"() {
|
|
@@ -4621,12 +4754,18 @@ var init_candidate_css = __esm({
|
|
|
4621
4754
|
});
|
|
4622
4755
|
|
|
4623
4756
|
// packages/verify/src/tasks.ts
|
|
4624
|
-
import
|
|
4625
|
-
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;
|
|
4757
|
+
import path17 from "node:path";
|
|
4758
|
+
var CONTRACT_CHECK_IDS, 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;
|
|
4626
4759
|
var init_tasks = __esm({
|
|
4627
4760
|
"packages/verify/src/tasks.ts"() {
|
|
4628
4761
|
"use strict";
|
|
4629
4762
|
init_paths();
|
|
4763
|
+
CONTRACT_CHECK_IDS = /* @__PURE__ */ new Set([
|
|
4764
|
+
"ref-reaches-control",
|
|
4765
|
+
"rest-props-reach-control",
|
|
4766
|
+
"default-value-reaches-control",
|
|
4767
|
+
"error-pose-wires-aria"
|
|
4768
|
+
]);
|
|
4630
4769
|
CALENDAR_CONFIGS = [
|
|
4631
4770
|
{ rep: "calendar-default", component: "Calendar", props: { style: "Default", selectedDay: 7, today: 28 } },
|
|
4632
4771
|
{ rep: "calendar-new-york", component: "Calendar", props: { style: "New York", selectedDay: 7, today: 28 } },
|
|
@@ -4781,7 +4920,7 @@ BEHAVIORAL CONTRACT (machine-verified, gating): the primary action and close con
|
|
|
4781
4920
|
];
|
|
4782
4921
|
TASKS = {
|
|
4783
4922
|
calendar: {
|
|
4784
|
-
set:
|
|
4923
|
+
set: path17.join(REPO_ROOT, "examples/recordings/shadcn-poc-calendar"),
|
|
4785
4924
|
entry: "Calendar.tsx",
|
|
4786
4925
|
configs: CALENDAR_CONFIGS,
|
|
4787
4926
|
systemApi: CALENDAR_API,
|
|
@@ -4789,7 +4928,7 @@ BEHAVIORAL CONTRACT (machine-verified, gating): the primary action and close con
|
|
|
4789
4928
|
prelude: { controls: ['[data-tendril-part="day"]'], textInputs: [] }
|
|
4790
4929
|
},
|
|
4791
4930
|
"shadcn-button": {
|
|
4792
|
-
set:
|
|
4931
|
+
set: path17.join(REPO_ROOT, "examples/recordings/shadcn-poc-button"),
|
|
4793
4932
|
entry: "Button.tsx",
|
|
4794
4933
|
configs: BUTTON_CONFIGS,
|
|
4795
4934
|
systemApi: BUTTON_API,
|
|
@@ -4797,7 +4936,7 @@ BEHAVIORAL CONTRACT (machine-verified, gating): the primary action and close con
|
|
|
4797
4936
|
prelude: { controls: ["> *"], textInputs: [] }
|
|
4798
4937
|
},
|
|
4799
4938
|
combobox: {
|
|
4800
|
-
set:
|
|
4939
|
+
set: path17.join(REPO_ROOT, "examples/recordings/carbon-poc-combobox"),
|
|
4801
4940
|
entry: "ComboBox.tsx",
|
|
4802
4941
|
configs: COMBO_CONFIGS,
|
|
4803
4942
|
systemApi: COMBO_API,
|
|
@@ -4805,7 +4944,7 @@ BEHAVIORAL CONTRACT (machine-verified, gating): the primary action and close con
|
|
|
4805
4944
|
prelude: { controls: ['[role="option"]'], textInputs: ["input"], popover: { selector: '[role="listbox"]', trigger: "input" } }
|
|
4806
4945
|
},
|
|
4807
4946
|
modal: {
|
|
4808
|
-
set:
|
|
4947
|
+
set: path17.join(REPO_ROOT, "examples/recordings/carbon-poc-modal"),
|
|
4809
4948
|
entry: "Modal.tsx",
|
|
4810
4949
|
configs: MODAL_CONFIGS,
|
|
4811
4950
|
systemApi: MODAL_API,
|
|
@@ -4819,12 +4958,13 @@ BEHAVIORAL CONTRACT (machine-verified, gating): the primary action and close con
|
|
|
4819
4958
|
// packages/verify/src/behavior.ts
|
|
4820
4959
|
var behavior_exports = {};
|
|
4821
4960
|
__export(behavior_exports, {
|
|
4961
|
+
POSE_GAP_FLOOR: () => POSE_GAP_FLOOR,
|
|
4822
4962
|
checkBehaviors: () => checkBehaviors,
|
|
4823
4963
|
compileMount: () => compileMount,
|
|
4824
4964
|
recordingIsDark: () => recordingIsDark
|
|
4825
4965
|
});
|
|
4826
|
-
import { existsSync as
|
|
4827
|
-
import
|
|
4966
|
+
import { existsSync as existsSync13, readFileSync as readFileSync10 } from "node:fs";
|
|
4967
|
+
import path18 from "node:path";
|
|
4828
4968
|
import { build as build3 } from "esbuild";
|
|
4829
4969
|
import { chromium as chromium3 } from "playwright-core";
|
|
4830
4970
|
import { PNG as PNG2 } from "pngjs";
|
|
@@ -4833,13 +4973,13 @@ function getFontFaces() {
|
|
|
4833
4973
|
return _fontFaces;
|
|
4834
4974
|
}
|
|
4835
4975
|
async function compileMount(task, bundleDir) {
|
|
4836
|
-
const entryTsx =
|
|
4837
|
-
if (!
|
|
4976
|
+
const entryTsx = path18.join(bundleDir, task.entry);
|
|
4977
|
+
if (!existsSync13(entryTsx)) return { error: `${task.entry} missing` };
|
|
4838
4978
|
const mountSrc = `
|
|
4839
4979
|
import { createElement } from "react";
|
|
4840
4980
|
import { createRoot } from "react-dom/client";
|
|
4841
|
-
import * as B from ${JSON.stringify(
|
|
4842
|
-
const cfg = (window as unknown as { __cfg: { component: string; props: Record<string, unknown>; spyProps?: string[] } }).__cfg;
|
|
4981
|
+
import * as B from ${JSON.stringify(path18.resolve(entryTsx))};
|
|
4982
|
+
const cfg = (window as unknown as { __cfg: { component: string; props: Record<string, unknown>; spyProps?: string[]; refProbe?: boolean } }).__cfg;
|
|
4843
4983
|
const C = (B as Record<string, unknown>)[cfg.component] as Parameters<typeof createElement>[0] | undefined;
|
|
4844
4984
|
// Callbacks cannot ride the JSON config: specs NAME spy props and the
|
|
4845
4985
|
// mount builds the functions, recording firings for dismissNotifies.
|
|
@@ -4850,6 +4990,15 @@ for (const name of cfg.spyProps ?? []) {
|
|
|
4850
4990
|
(w.__tendrilFired ??= {})[name] = true;
|
|
4851
4991
|
};
|
|
4852
4992
|
}
|
|
4993
|
+
// Ref probe (Cycle D contract): attached ONLY when the spec carries
|
|
4994
|
+
// assertRefReachesControl \u2014 attaching a ref to a plain function
|
|
4995
|
+
// component logs a React warning, and legacy bundles must stay clean
|
|
4996
|
+
// on the checks that do not ask.
|
|
4997
|
+
if (cfg.refProbe === true) {
|
|
4998
|
+
const ref = { current: null as unknown };
|
|
4999
|
+
(window as unknown as { __tendrilRef?: { current: unknown } }).__tendrilRef = ref;
|
|
5000
|
+
(props as Record<string, unknown>)["ref"] = ref;
|
|
5001
|
+
}
|
|
4853
5002
|
const root = document.getElementById("root");
|
|
4854
5003
|
if (root && C) createRoot(root).render(createElement(C, props));
|
|
4855
5004
|
`;
|
|
@@ -4894,7 +5043,7 @@ async function runSteps(page, spec, renderPose) {
|
|
|
4894
5043
|
await page.waitForTimeout(delay);
|
|
4895
5044
|
await freezeAnimations(page);
|
|
4896
5045
|
if (!(await shotRoot(page)).equals(idle)) {
|
|
4897
|
-
return { id: spec.id, pass: false, detail: "unmeasurable: the component's pixels change at rest (free-running animation or timer) \u2014 the committed state cannot be observed through it" };
|
|
5046
|
+
return { id: spec.id, pass: false, detail: "unmeasurable: the component's pixels change at rest (free-running animation or a timer in YOUR candidate) \u2014 the committed state cannot be observed through it; this IS fixable from code: keep every scored pose reachable with all motion idle (freezable CSS animation, no JS-driven repaints)" };
|
|
4898
5047
|
}
|
|
4899
5048
|
}
|
|
4900
5049
|
try {
|
|
@@ -4914,7 +5063,11 @@ async function runSteps(page, spec, renderPose) {
|
|
|
4914
5063
|
const gap = 1 - fromIdle.similarity;
|
|
4915
5064
|
const residual = 1 - toTarget.similarity;
|
|
4916
5065
|
if (gap < POSE_GAP_FLOOR) {
|
|
4917
|
-
return {
|
|
5066
|
+
return {
|
|
5067
|
+
id: spec.id,
|
|
5068
|
+
pass: false,
|
|
5069
|
+
detail: `unmeasurable: this bundle renders the "${pose}" pose indistinguishably from its resting pose (gap ${gap.toFixed(4)}) \u2014 if the recording's own references DO differ (authored tasks verify this before prescribing the check), the candidate is not painting the committed treatment: fix that pose's paint; if they do not, no code change can help \u2014 report it instead of iterating`
|
|
5070
|
+
};
|
|
4918
5071
|
}
|
|
4919
5072
|
if (residual > (1 - COMMIT_GAP_CLOSED) * gap) {
|
|
4920
5073
|
return {
|
|
@@ -4934,7 +5087,7 @@ async function runSteps(page, spec, renderPose) {
|
|
|
4934
5087
|
await page.waitForTimeout(150);
|
|
4935
5088
|
} else if ("assertVisible" in step) {
|
|
4936
5089
|
const visible = await page.evaluate(
|
|
4937
|
-
`(() => { const el = document.querySelector('#root ${step.assertVisible}'); if (!el) return false; const r = el.getBoundingClientRect(); return r.width > 0 && r.height > 0 &&
|
|
5090
|
+
`(() => { const el = document.querySelector('#root ${step.assertVisible}'); if (!el) return false; const r = el.getBoundingClientRect(); const cs = getComputedStyle(el); return r.width > 0 && r.height > 0 && cs.visibility !== 'hidden' && cs.display !== 'none' && parseFloat(cs.opacity) > 0; })()`
|
|
4938
5091
|
);
|
|
4939
5092
|
if (visible !== true) return { id: spec.id, pass: false, detail: `expected visible: ${step.assertVisible}` };
|
|
4940
5093
|
} else if ("assertHidden" in step) {
|
|
@@ -4948,17 +5101,68 @@ async function runSteps(page, spec, renderPose) {
|
|
|
4948
5101
|
if (cursor !== want) return { id: spec.id, pass: false, detail: `cursor on ${sel}: got "${String(cursor)}", want "${want}"` };
|
|
4949
5102
|
} else if ("assertFocusable" in step) {
|
|
4950
5103
|
const ok = await page.evaluate(
|
|
4951
|
-
`(() => { const el = document.querySelector('#root ${step.assertFocusable}'); if (!el) return 'MISSING'; el.focus(); return document.activeElement === el ? true : 'not focusable (tag ' + el.tagName + ')'; })()`
|
|
5104
|
+
`(() => { const el = document.querySelector('#root ${step.assertFocusable}'); if (!el) return 'MISSING \u2014 no element matches: a real native control must exist here'; el.focus(); return document.activeElement === el ? true : 'not focusable (tag ' + el.tagName + ') \u2014 .focus() did not land: a display:none/visibility:hidden element cannot take focus, and a non-form element needs to be a native control (or the control must not be disabled)'; })()`
|
|
4952
5105
|
);
|
|
4953
5106
|
if (ok !== true) return { id: spec.id, pass: false, detail: `focus ${step.assertFocusable}: ${String(ok)}` };
|
|
5107
|
+
} else if ("assertRefReachesControl" in step) {
|
|
5108
|
+
const verdict = await page.evaluate(
|
|
5109
|
+
`(() => {
|
|
5110
|
+
const el = document.querySelector('#root ${step.assertRefReachesControl}');
|
|
5111
|
+
if (!el) return 'no element matches ${step.assertRefReachesControl} \u2014 the contract requires a native control';
|
|
5112
|
+
const probe = window.__tendrilRef;
|
|
5113
|
+
if (!probe || probe.current == null) return 'ref.current is null \u2014 the entry export must be a forwardRef component whose ref reaches the native control (a plain function component ignores the ref)';
|
|
5114
|
+
return probe.current === el ? true : 'ref.current is <' + String(probe.current.tagName).toLowerCase() + '> but the control is <' + el.tagName.toLowerCase() + '> \u2014 forward the ref to the control element itself, not a wrapper';
|
|
5115
|
+
})()`
|
|
5116
|
+
);
|
|
5117
|
+
if (verdict !== true) return { id: spec.id, pass: false, detail: `ref ${step.assertRefReachesControl}: ${String(verdict)}` };
|
|
4954
5118
|
} else if ("assertAnimated" in step) {
|
|
4955
5119
|
const animated = await page.evaluate(ANIMATED_PROBE);
|
|
4956
|
-
if (animated !== true) return { id: spec.id, pass: false, detail: "no running animation anywhere in the subtree" };
|
|
5120
|
+
if (animated !== true) return { id: spec.id, pass: false, detail: "no running animation anywhere in the subtree \u2014 this pose must carry a LIVE animation (CSS animation, a Web Animation, or SVG <animate>); a static spinner image satisfies pixels but fails this check" };
|
|
4957
5121
|
} else if ("type" in step) {
|
|
4958
5122
|
const [sel, text] = step.type;
|
|
4959
5123
|
await page.click(`#root ${sel}`, { timeout: 2e3 });
|
|
4960
5124
|
await page.keyboard.type(text, { delay: 20 });
|
|
4961
5125
|
await page.waitForTimeout(200);
|
|
5126
|
+
} else if ("typeChangesPixels" in step) {
|
|
5127
|
+
const [sel, text] = step.typeChangesPixels;
|
|
5128
|
+
await settle(page);
|
|
5129
|
+
const before = await shotRoot(page);
|
|
5130
|
+
await page.click(`#root ${sel}`, { timeout: 2e3 });
|
|
5131
|
+
await page.keyboard.type(text, { delay: 20 });
|
|
5132
|
+
await page.waitForTimeout(200);
|
|
5133
|
+
await freezeAnimations(page);
|
|
5134
|
+
const after = await shotRoot(page);
|
|
5135
|
+
if (before.equals(after)) {
|
|
5136
|
+
return {
|
|
5137
|
+
id: spec.id,
|
|
5138
|
+
pass: false,
|
|
5139
|
+
detail: `typing "${text}" into ${sel} changed nothing on screen \u2014 the typed value must be VISIBLY rendered (a hidden input that swallows typing while painted content stays fixed is a decoy, not a control)`
|
|
5140
|
+
};
|
|
5141
|
+
}
|
|
5142
|
+
} else if ("assertSpyFired" in step) {
|
|
5143
|
+
const fired = await page.evaluate(`(() => (window.__tendrilFired ?? {})[${JSON.stringify(step.assertSpyFired)}] === true)()`);
|
|
5144
|
+
if (fired !== true) {
|
|
5145
|
+
return {
|
|
5146
|
+
id: spec.id,
|
|
5147
|
+
pass: false,
|
|
5148
|
+
detail: `the caller's ${step.assertSpyFired} never fired \u2014 forward the caller's handler to the control (a stripped or overridden handler breaks every controlled consumer silently)`
|
|
5149
|
+
};
|
|
5150
|
+
}
|
|
5151
|
+
} else if ("assertDescribedByResolves" in step) {
|
|
5152
|
+
const [sel, expected] = step.assertDescribedByResolves;
|
|
5153
|
+
const verdict = await page.evaluate(
|
|
5154
|
+
`(() => {
|
|
5155
|
+
const el = document.querySelector('#root ${sel}');
|
|
5156
|
+
if (!el) return 'element missing';
|
|
5157
|
+
const ids = (el.getAttribute('aria-describedby') ?? '').trim().split(/\\s+/).filter(Boolean);
|
|
5158
|
+
if (ids.length === 0) return 'aria-describedby is missing/empty';
|
|
5159
|
+
const targets = ids.map((id) => document.getElementById(id));
|
|
5160
|
+
if (targets.some((t) => t === null)) return 'aria-describedby references an id that does not exist \u2014 a dangling idref is present, not wired';
|
|
5161
|
+
const text = targets.map((t) => (t && t.textContent) || '').join(' ');
|
|
5162
|
+
return text.includes(${JSON.stringify(expected)}) ? true : 'the referenced element(s) do not contain the message the prop supplied';
|
|
5163
|
+
})()`
|
|
5164
|
+
);
|
|
5165
|
+
if (verdict !== true) return { id: spec.id, pass: false, detail: `describedby on ${sel}: ${String(verdict)}` };
|
|
4962
5166
|
} else if ("assertValue" in step) {
|
|
4963
5167
|
const [sel, want] = step.assertValue;
|
|
4964
5168
|
const value = await page.evaluate(`(() => { const el = document.querySelector('#root ${sel}'); return el ? el.value : 'MISSING'; })()`);
|
|
@@ -4972,7 +5176,7 @@ async function runSteps(page, spec, renderPose) {
|
|
|
4972
5176
|
await page.keyboard.press("Tab");
|
|
4973
5177
|
await page.waitForTimeout(200);
|
|
4974
5178
|
const after = await page.screenshot();
|
|
4975
|
-
if (Buffer.compare(before, after) === 0) return { id: spec.id, pass: false, detail: "Tab focus changes nothing
|
|
5179
|
+
if (Buffer.compare(before, after) === 0) return { id: spec.id, pass: false, detail: "Tab focus changes nothing on screen \u2014 no visible focus indicator exists: style the control's :focus-visible state (shared declaration with its forced token) so keyboard focus paints the recorded ring" };
|
|
4976
5180
|
} else if ("clickOutside" in step) {
|
|
4977
5181
|
await page.mouse.click(870, 660);
|
|
4978
5182
|
await page.waitForTimeout(200);
|
|
@@ -4985,9 +5189,9 @@ async function runSteps(page, spec, renderPose) {
|
|
|
4985
5189
|
if (val !== want) return { id: spec.id, pass: false, detail: `${attr} on ${sel}: got ${String(val)}, want "${want}"` };
|
|
4986
5190
|
} else if ("assertNotFocusable" in step) {
|
|
4987
5191
|
const focused = await page.evaluate(
|
|
4988
|
-
`(() => { const
|
|
5192
|
+
`(() => { const els = [...document.querySelectorAll('#root ${step.assertNotFocusable}')]; if (els.length === 0) return 'MISSING'; for (const el of els) { el.focus(); if (document.activeElement === el) return true; } return false; })()`
|
|
4989
5193
|
);
|
|
4990
|
-
if (focused !== false) return { id: spec.id, pass: false, detail: focused === "MISSING" ? "element missing" : "disabled element took focus" };
|
|
5194
|
+
if (focused !== false) return { id: spec.id, pass: false, detail: focused === "MISSING" ? "element missing" : "disabled element took focus \u2014 use the native disabled attribute: pointer-events:none and aria-disabled leave the element focusable (Tab and .focus() still land); only disabled refuses focus" };
|
|
4991
5195
|
} else if ("reducedMotionNoAnimation" in step) {
|
|
4992
5196
|
await page.emulateMedia({ reducedMotion: "reduce" });
|
|
4993
5197
|
await page.waitForTimeout(250);
|
|
@@ -5013,7 +5217,11 @@ async function runSteps(page, spec, renderPose) {
|
|
|
5013
5217
|
await page.waitForTimeout(500);
|
|
5014
5218
|
after = await page.screenshot();
|
|
5015
5219
|
if (Buffer.compare(baseline, after) !== 0)
|
|
5016
|
-
return {
|
|
5220
|
+
return {
|
|
5221
|
+
id: spec.id,
|
|
5222
|
+
pass: false,
|
|
5223
|
+
detail: "pixels differ after mouse click + mouse-away \u2014 something persisted from a pointer click: usually a focus ring bound to bare :focus (bind rings to :focus-visible; a mouse click must not leave one). If clicking THIS pose legitimately commits a state change, the check is aimed at the wrong pose (it must target one where clicking commits nothing) \u2014 report that rather than restyling"
|
|
5224
|
+
};
|
|
5017
5225
|
}
|
|
5018
5226
|
} else if ("assertNoReflow" in step) {
|
|
5019
5227
|
const before = await page.evaluate("document.getElementById('probe').getBoundingClientRect().top");
|
|
@@ -5108,7 +5316,12 @@ async function runSteps(page, spec, renderPose) {
|
|
|
5108
5316
|
await page.waitForTimeout(200);
|
|
5109
5317
|
const after = await page.screenshot();
|
|
5110
5318
|
await page.mouse.move(0, 0);
|
|
5111
|
-
if (Buffer.compare(before, after) === 0)
|
|
5319
|
+
if (Buffer.compare(before, after) === 0)
|
|
5320
|
+
return {
|
|
5321
|
+
id: spec.id,
|
|
5322
|
+
pass: false,
|
|
5323
|
+
detail: `real :hover on ${step.hoverChangesPixels} changes nothing \u2014 the hover treatment must ride the REAL pseudo-class in a shared declaration with the forced token (e.g. :is(:hover, [data-tendril-state~="hover"])); a rule on the attribute selector alone never fires for a real pointer`
|
|
5324
|
+
};
|
|
5112
5325
|
}
|
|
5113
5326
|
} catch (err) {
|
|
5114
5327
|
return { id: spec.id, pass: false, detail: `step failed: ${err instanceof Error ? err.message.split("\n")[0] : String(err)}` };
|
|
@@ -5119,10 +5332,10 @@ async function runSteps(page, spec, renderPose) {
|
|
|
5119
5332
|
function recordingIsDark(task) {
|
|
5120
5333
|
const rep = task.configs[0]?.rep;
|
|
5121
5334
|
if (rep === void 0) return false;
|
|
5122
|
-
const f =
|
|
5123
|
-
if (!
|
|
5335
|
+
const f = path18.join(task.set, rep, "get_screenshot.json");
|
|
5336
|
+
if (!existsSync13(f)) return false;
|
|
5124
5337
|
try {
|
|
5125
|
-
const env = JSON.parse(
|
|
5338
|
+
const env = JSON.parse(readFileSync10(f, "utf8")).content.find((c) => c.type === "image");
|
|
5126
5339
|
if (env?.data === void 0) return false;
|
|
5127
5340
|
const png = PNG2.sync.read(Buffer.from(env.data, "base64"));
|
|
5128
5341
|
let sum = 0;
|
|
@@ -5172,6 +5385,10 @@ async function checkPrelude(page, task) {
|
|
|
5172
5385
|
push(`touch-action(${sel})`, cs.touchAction === "manipulation", `"${cs.touchAction}" (want manipulation)`);
|
|
5173
5386
|
push(`user-select(${sel})`, cs.userSelect === "none", `"${cs.userSelect}" (want none)`);
|
|
5174
5387
|
}
|
|
5388
|
+
for (const sel of task.prelude.touchTargets ?? []) {
|
|
5389
|
+
const ta = await page.evaluate(`(() => { const el = document.querySelector('#root ${sel}'); return el ? getComputedStyle(el).touchAction : null; })()`);
|
|
5390
|
+
push(`touch-action(${sel})`, ta === "manipulation", ta === null ? "element missing" : `"${ta}" (want manipulation)`);
|
|
5391
|
+
}
|
|
5175
5392
|
for (const sel of task.prelude.textInputs) {
|
|
5176
5393
|
const us = await page.evaluate(`(() => { const el = document.querySelector('#root ${sel}'); return el ? (getComputedStyle(el).userSelect ?? getComputedStyle(el).webkitUserSelect) : null; })()`);
|
|
5177
5394
|
push(`input-selectable(${sel})`, us !== null && us !== "none", us === null ? "element missing" : `input user-select "${us}" (must stay selectable)`);
|
|
@@ -5200,12 +5417,21 @@ async function checkBehaviors(task, bundleDir, opts = {}) {
|
|
|
5200
5417
|
const results = [];
|
|
5201
5418
|
let deadlined = false;
|
|
5202
5419
|
try {
|
|
5203
|
-
for (const
|
|
5420
|
+
for (const rawSpec of task.behaviors) {
|
|
5421
|
+
const spec = JSON.parse(
|
|
5422
|
+
JSON.stringify(rawSpec).replaceAll(JSON.stringify("__TENDRIL_MINT__"), JSON.stringify(`t-${Math.random().toString(36).slice(2, 10)}`))
|
|
5423
|
+
);
|
|
5204
5424
|
const cfg = task.configs.find((c) => c.rep === spec.config);
|
|
5205
5425
|
if (cfg === void 0) {
|
|
5206
5426
|
results.push({ id: spec.id, pass: false, detail: `unknown config ${spec.config}` });
|
|
5207
5427
|
continue;
|
|
5208
5428
|
}
|
|
5429
|
+
const cfgJson = JSON.stringify({
|
|
5430
|
+
component: cfg.component,
|
|
5431
|
+
props: { ...cfg.props, ...spec.props },
|
|
5432
|
+
...spec.spyProps !== void 0 ? { spyProps: spec.spyProps } : {},
|
|
5433
|
+
...spec.steps.some((s) => "assertRefReachesControl" in s) ? { refProbe: true } : {}
|
|
5434
|
+
});
|
|
5209
5435
|
const html = `<!doctype html><html><head><meta charset="utf-8"><style>
|
|
5210
5436
|
${getFontFaces()}
|
|
5211
5437
|
${css}
|
|
@@ -5213,12 +5439,12 @@ ${TRANSITION_INSTANT_CSS}
|
|
|
5213
5439
|
body{margin:0;padding:20px}
|
|
5214
5440
|
#root{position:static}
|
|
5215
5441
|
#probe{height:24px}
|
|
5216
|
-
</style></head><body><div id="root"></div><div id="probe">reflow probe</div><script>window.__cfg=${
|
|
5442
|
+
</style></head><body><div id="root"></div><div id="probe">reflow probe</div><script>window.__cfg=${cfgJson}</script><script>${js}</script></body></html>`;
|
|
5217
5443
|
const renderPose = async (rep) => {
|
|
5218
5444
|
const target = task.configs.find((c) => c.rep === rep);
|
|
5219
5445
|
if (target === void 0) return { error: `unknown pose ${rep}` };
|
|
5220
5446
|
const poseHtml = html.replace(
|
|
5221
|
-
`window.__cfg=${
|
|
5447
|
+
`window.__cfg=${cfgJson}`,
|
|
5222
5448
|
`window.__cfg=${JSON.stringify({ component: target.component, props: target.props })}`
|
|
5223
5449
|
);
|
|
5224
5450
|
const p = await browser.newPage({ viewport: { width: 900, height: 700 } });
|
|
@@ -5646,7 +5872,15 @@ function buildTrustStatement(input) {
|
|
|
5646
5872
|
const unrecorded = input.latticeConfigs === null ? null : Math.max(0, input.latticeConfigs - input.scored);
|
|
5647
5873
|
const interaction = input.interactionChecks === 0 ? "interaction behaviors NONE VERIFIED (0 checks)" : `interaction behaviors ${input.interactionPassed}/${input.interactionChecks}`;
|
|
5648
5874
|
const prelude = input.preludeChecks === 0 ? "" : `, page hygiene ${input.preludePassed}/${input.preludeChecks}`;
|
|
5649
|
-
|
|
5875
|
+
const content = (input.contentChecks ?? 0) === 0 ? "" : `, content slots ${input.contentPassed}/${input.contentChecks}`;
|
|
5876
|
+
const parity = (input.parityChecks ?? 0) === 0 ? "" : `, state parity ${input.parityPassed}/${input.parityChecks}`;
|
|
5877
|
+
return `Verified against recorded truth: ${input.pass}/${input.scored} recorded configs at or above the pass bar (${input.certified} certified), ${interaction}${content}${parity}${prelude}.` + // No hardcoded REASON for the unknown denominator: a fresh
|
|
5878
|
+
// selection-scoped set is not "predating lattice tracking" (the
|
|
5879
|
+
// run-21 defect, fixed on the verify channel then and found still
|
|
5880
|
+
// live HERE by the 2026-08-16 sweep) — this artifact cannot see
|
|
5881
|
+
// the manifest, so it states the fact and points at verify, which
|
|
5882
|
+
// knows the reason.
|
|
5883
|
+
(unrecorded === null ? " Coverage denominator UNKNOWN (the set's full variant lattice was not established at plan time \u2014 `tendril verify` states why): completeness is not established." : ` ${unrecorded} lattice configs are unrecorded and UNVERIFIED.`) + ` These numbers are claims: recompute them with \`tendril verify\` \u2014 certification authority lives in the CLI ruler, never in this file.`;
|
|
5650
5884
|
}
|
|
5651
5885
|
function cssProvenanceComment(input) {
|
|
5652
5886
|
const unrecorded = input.latticeConfigs === null ? "; coverage denominator unknown" : `; ${Math.max(0, input.latticeConfigs - input.scored)} lattice configs unverified`;
|
|
@@ -5674,7 +5908,12 @@ var init_bundle = __esm({
|
|
|
5674
5908
|
name: z9.string(),
|
|
5675
5909
|
type: z9.string(),
|
|
5676
5910
|
required: z9.boolean(),
|
|
5677
|
-
default: z9.string().optional()
|
|
5911
|
+
default: z9.string().optional(),
|
|
5912
|
+
/** Cycle D role-named slots ("error": presence drives the error
|
|
5913
|
+
* state; the public default is undefined). Declared so the field
|
|
5914
|
+
* SURVIVES the schema round-trip — zod was silently stripping it
|
|
5915
|
+
* from the public artifact (review). */
|
|
5916
|
+
role: z9.string().optional()
|
|
5678
5917
|
});
|
|
5679
5918
|
PropAdapterSchema = z9.record(
|
|
5680
5919
|
z9.string(),
|
|
@@ -5791,8 +6030,53 @@ var init_src4 = __esm({
|
|
|
5791
6030
|
});
|
|
5792
6031
|
|
|
5793
6032
|
// packages/verify/src/bundle-quality.ts
|
|
5794
|
-
import { readFileSync as
|
|
5795
|
-
import
|
|
6033
|
+
import { readFileSync as readFileSync11, rmSync as rmSync2, writeFileSync as writeFileSync5, existsSync as existsSync14 } from "node:fs";
|
|
6034
|
+
import path19 from "node:path";
|
|
6035
|
+
function expertLensFindings(entryFile, source, contract) {
|
|
6036
|
+
const findings = [];
|
|
6037
|
+
const lineOf = (index) => source.slice(0, index).split("\n").length;
|
|
6038
|
+
const spoof = /__tendril/i.exec(source);
|
|
6039
|
+
if (spoof !== null) {
|
|
6040
|
+
findings.push({
|
|
6041
|
+
kind: "expert-lens",
|
|
6042
|
+
file: entryFile,
|
|
6043
|
+
line: lineOf(spoof.index),
|
|
6044
|
+
message: "the bundle references a Tendril harness internal (__tendril*) \u2014 candidate code must never touch the measurement machinery; treat as a gaming attempt and inspect before trusting any verdict"
|
|
6045
|
+
});
|
|
6046
|
+
}
|
|
6047
|
+
const idx = /\[\s*key\s*:\s*string\s*\]\s*:/.exec(source);
|
|
6048
|
+
if (idx !== null) {
|
|
6049
|
+
findings.push({
|
|
6050
|
+
kind: "expert-lens",
|
|
6051
|
+
file: entryFile,
|
|
6052
|
+
line: lineOf(idx.index),
|
|
6053
|
+
message: "index-signature escape hatch ([key: string]) in the public props \u2014 extend the native control's prop surface (ComponentPropsWithoutRef) instead; the typed extension IS the rest-prop surface"
|
|
6054
|
+
});
|
|
6055
|
+
}
|
|
6056
|
+
if (contract.archetype !== "static") {
|
|
6057
|
+
if (contract.forwardRef && !/\bforwardRef\b/.test(source) && !/\bref\b/.test(source)) {
|
|
6058
|
+
findings.push({
|
|
6059
|
+
kind: "expert-lens",
|
|
6060
|
+
file: entryFile,
|
|
6061
|
+
message: "no ref path to the control (no forwardRef and no ref identifier) \u2014 control archetypes must let a consumer reach the native control (the ref-reaches-control check measures this)"
|
|
6062
|
+
});
|
|
6063
|
+
}
|
|
6064
|
+
for (const v of contract.derivedStateValues) {
|
|
6065
|
+
if ((contract.publicUnionValues ?? []).includes(v)) continue;
|
|
6066
|
+
const m = new RegExp(`\\w+\\?\\s*:[^;\\n]*"${v}"`).exec(source);
|
|
6067
|
+
if (m !== null) {
|
|
6068
|
+
findings.push({
|
|
6069
|
+
kind: "expert-lens",
|
|
6070
|
+
file: entryFile,
|
|
6071
|
+
line: lineOf(m.index),
|
|
6072
|
+
message: `a public prop's type carries the derived-state value "${v}" \u2014 the platform owns that state (focus/value/error semantics); derived states are never caller-set pose props`
|
|
6073
|
+
});
|
|
6074
|
+
break;
|
|
6075
|
+
}
|
|
6076
|
+
}
|
|
6077
|
+
}
|
|
6078
|
+
return findings;
|
|
6079
|
+
}
|
|
5796
6080
|
function definedVars(tokensCss) {
|
|
5797
6081
|
if (tokensCss === void 0) return void 0;
|
|
5798
6082
|
const names = /* @__PURE__ */ new Set();
|
|
@@ -5801,19 +6085,19 @@ function definedVars(tokensCss) {
|
|
|
5801
6085
|
}
|
|
5802
6086
|
function recordedTokenMapState(setDir, reps) {
|
|
5803
6087
|
const readMap = (file) => {
|
|
5804
|
-
if (!
|
|
6088
|
+
if (!existsSync14(file)) return void 0;
|
|
5805
6089
|
try {
|
|
5806
|
-
const parsed = JSON.parse(envelopeFirstTextPart(JSON.parse(
|
|
6090
|
+
const parsed = JSON.parse(envelopeFirstTextPart(JSON.parse(readFileSync11(file, "utf8"))) || "{}");
|
|
5807
6091
|
return parsed !== null && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : {};
|
|
5808
6092
|
} catch {
|
|
5809
6093
|
return {};
|
|
5810
6094
|
}
|
|
5811
6095
|
};
|
|
5812
|
-
const setLevel = readMap(
|
|
6096
|
+
const setLevel = readMap(path19.join(setDir, "get_variable_defs.json"));
|
|
5813
6097
|
if (setLevel !== void 0) return Object.keys(setLevel).length === 0 ? "empty" : "populated";
|
|
5814
6098
|
let recorded = false;
|
|
5815
6099
|
for (const rep of reps) {
|
|
5816
|
-
const m = readMap(
|
|
6100
|
+
const m = readMap(path19.join(setDir, rep, "get_variable_defs.json"));
|
|
5817
6101
|
if (m === void 0) continue;
|
|
5818
6102
|
recorded = true;
|
|
5819
6103
|
if (Object.keys(m).length > 0) return "populated";
|
|
@@ -5876,13 +6160,13 @@ function fontStackFindings(sheets, coverage) {
|
|
|
5876
6160
|
}
|
|
5877
6161
|
return findings;
|
|
5878
6162
|
}
|
|
5879
|
-
async function checkBundleQuality(bundleDir, entry, set, fontManifest, motion) {
|
|
6163
|
+
async function checkBundleQuality(bundleDir, entry, set, fontManifest, motion, contract) {
|
|
5880
6164
|
const findings = [];
|
|
5881
|
-
const entryPath =
|
|
5882
|
-
const cssPath =
|
|
5883
|
-
const tokensPath =
|
|
5884
|
-
const css =
|
|
5885
|
-
const tokensCss =
|
|
6165
|
+
const entryPath = path19.join(bundleDir, entry);
|
|
6166
|
+
const cssPath = path19.join(bundleDir, "styles.css");
|
|
6167
|
+
const tokensPath = path19.join(bundleDir, "tokens.css");
|
|
6168
|
+
const css = existsSync14(cssPath) ? readFileSync11(cssPath, "utf8") : "";
|
|
6169
|
+
const tokensCss = existsSync14(tokensPath) ? readFileSync11(tokensPath, "utf8") : void 0;
|
|
5886
6170
|
findings.push(
|
|
5887
6171
|
...fontStackFindings(
|
|
5888
6172
|
[
|
|
@@ -5892,18 +6176,22 @@ async function checkBundleQuality(bundleDir, entry, set, fontManifest, motion) {
|
|
|
5892
6176
|
injectedFamilyWeights(fontManifest)
|
|
5893
6177
|
)
|
|
5894
6178
|
);
|
|
5895
|
-
if (
|
|
6179
|
+
if (existsSync14(entryPath)) {
|
|
5896
6180
|
const workDir = newScratchDir("quality");
|
|
5897
6181
|
try {
|
|
5898
|
-
const tsxPath =
|
|
5899
|
-
writeFileSync5(tsxPath,
|
|
6182
|
+
const tsxPath = path19.join(workDir, entry);
|
|
6183
|
+
writeFileSync5(tsxPath, readFileSync11(entryPath, "utf8"));
|
|
5900
6184
|
for (const d of runTscStrict([tsxPath]).diagnostics) {
|
|
6185
|
+
if (d.code === 2307 && /['"]\.\/composed\//.test(d.message)) continue;
|
|
5901
6186
|
findings.push({ kind: "tsc", file: entry, ...d.line === void 0 ? {} : { line: d.line }, message: `TS${d.code}: ${d.message}` });
|
|
5902
6187
|
}
|
|
5903
6188
|
} finally {
|
|
5904
6189
|
rmSync2(workDir, { recursive: true, force: true });
|
|
5905
6190
|
}
|
|
5906
6191
|
}
|
|
6192
|
+
if (contract !== void 0 && existsSync14(entryPath)) {
|
|
6193
|
+
findings.push(...expertLensFindings(entry, readFileSync11(entryPath, "utf8"), contract));
|
|
6194
|
+
}
|
|
5907
6195
|
if (css !== "") {
|
|
5908
6196
|
const mapState = set === void 0 ? void 0 : recordedTokenMapState(set.dir, set.reps);
|
|
5909
6197
|
for (const v of (await runTokenLint(css, "styles.css", definedVars(tokensCss), mapState !== void 0 && mapState !== "populated")).violations) {
|
|
@@ -6021,8 +6309,8 @@ var init_effect_geometry = __esm({
|
|
|
6021
6309
|
});
|
|
6022
6310
|
|
|
6023
6311
|
// packages/verify/src/bundle-score.ts
|
|
6024
|
-
import { existsSync as
|
|
6025
|
-
import
|
|
6312
|
+
import { existsSync as existsSync15, mkdirSync as mkdirSync3, readFileSync as readFileSync12, writeFileSync as writeFileSync6 } from "node:fs";
|
|
6313
|
+
import path20 from "node:path";
|
|
6026
6314
|
import { build as build4 } from "esbuild";
|
|
6027
6315
|
import { chromium as chromium4 } from "playwright-core";
|
|
6028
6316
|
function getFontFaces2() {
|
|
@@ -6076,7 +6364,7 @@ function chooseSeat(cropAt, ref, padW, padH, backdrop, origin, extents) {
|
|
|
6076
6364
|
}
|
|
6077
6365
|
function metadataRoot(set, rep) {
|
|
6078
6366
|
try {
|
|
6079
|
-
const text = JSON.parse(
|
|
6367
|
+
const text = JSON.parse(readFileSync12(path20.join(set, rep, "get_metadata.json"), "utf8")).content.map((c) => c.text ?? "").join("\n");
|
|
6080
6368
|
return parseMetadataStructure(text);
|
|
6081
6369
|
} catch {
|
|
6082
6370
|
return void 0;
|
|
@@ -6131,19 +6419,19 @@ function smallSemanticNodes(set, rep, maxArea = 1024) {
|
|
|
6131
6419
|
});
|
|
6132
6420
|
}
|
|
6133
6421
|
function repMeta(set, rep) {
|
|
6134
|
-
const text = JSON.parse(
|
|
6422
|
+
const text = JSON.parse(readFileSync12(path20.join(set, rep, "get_metadata.json"), "utf8")).content.map((c) => c.text ?? "").join("\n");
|
|
6135
6423
|
const root = parseMetadataStructure(text);
|
|
6136
6424
|
return { w: Math.round(root.width ?? 100), h: Math.round(root.height ?? 40) };
|
|
6137
6425
|
}
|
|
6138
6426
|
function repRef(set, rep) {
|
|
6139
|
-
const env = JSON.parse(
|
|
6427
|
+
const env = JSON.parse(readFileSync12(path20.join(set, rep, "get_screenshot.json"), "utf8")).content.find((c) => c.type === "image");
|
|
6140
6428
|
return Uint8Array.from(Buffer.from(env?.data ?? "", "base64"));
|
|
6141
6429
|
}
|
|
6142
6430
|
function repEffectExtents(set, rep) {
|
|
6143
|
-
const file =
|
|
6144
|
-
if (!
|
|
6431
|
+
const file = path20.join(set, rep, "get_design_context.json");
|
|
6432
|
+
if (!existsSync15(file)) return void 0;
|
|
6145
6433
|
try {
|
|
6146
|
-
const text = JSON.parse(
|
|
6434
|
+
const text = JSON.parse(readFileSync12(file, "utf8")).content.map((c) => c.text ?? "").join("\n");
|
|
6147
6435
|
const extents = shadowExtents(text);
|
|
6148
6436
|
return extents.top + extents.right + extents.bottom + extents.left > 0 ? extents : void 0;
|
|
6149
6437
|
} catch {
|
|
@@ -6154,13 +6442,13 @@ async function scoreBundleForTask(task, bundleDir, bar = BAR, opts = {}) {
|
|
|
6154
6442
|
if (opts.evidenceDir !== void 0) mkdirSync3(opts.evidenceDir, { recursive: true });
|
|
6155
6443
|
const CONFIGS2 = task.configs;
|
|
6156
6444
|
const timeoutMs = opts.mountDeadlineMs ?? ENGINE_MOUNT_DEADLINE_MS;
|
|
6157
|
-
const entryTsx =
|
|
6158
|
-
if (!
|
|
6445
|
+
const entryTsx = path20.join(bundleDir, task.entry);
|
|
6446
|
+
if (!existsSync15(entryTsx)) return CONFIGS2.map((c) => ({ rep: c.rep, similarity: 0, inkRecall: 0, exact: { similarity: 0, inkRecall: 0 }, pass: false, error: `${task.entry} missing` }));
|
|
6159
6447
|
const css = candidateCss(bundleDir);
|
|
6160
6448
|
const mountSrc = `
|
|
6161
6449
|
import { createElement } from "react";
|
|
6162
6450
|
import { createRoot } from "react-dom/client";
|
|
6163
|
-
import * as B from ${JSON.stringify(
|
|
6451
|
+
import * as B from ${JSON.stringify(path20.resolve(entryTsx))};
|
|
6164
6452
|
const cfg = (window as unknown as { __cfg: { component: string; props: Record<string, unknown> } }).__cfg;
|
|
6165
6453
|
const C = (B as Record<string, unknown>)[cfg.component] as Parameters<typeof createElement>[0] | undefined;
|
|
6166
6454
|
const root = document.getElementById("root");
|
|
@@ -6258,12 +6546,12 @@ body{margin:0;background:rgb(${backdrop.join(",")})}
|
|
|
6258
6546
|
return name === void 0 ? c : { ...c, name };
|
|
6259
6547
|
});
|
|
6260
6548
|
if (opts.evidenceDir !== void 0) {
|
|
6261
|
-
writeFileSync6(
|
|
6262
|
-
writeFileSync6(
|
|
6263
|
-
writeFileSync6(
|
|
6549
|
+
writeFileSync6(path20.join(opts.evidenceDir, `${cfg.rep}-render.png`), scored);
|
|
6550
|
+
writeFileSync6(path20.join(opts.evidenceDir, `${cfg.rep}-ref.png`), ref);
|
|
6551
|
+
writeFileSync6(path20.join(opts.evidenceDir, `${cfg.rep}-diff.png`), diffPng(scored, ref));
|
|
6264
6552
|
for (const [i, c] of absent.slice(0, 3).entries()) {
|
|
6265
|
-
writeFileSync6(
|
|
6266
|
-
writeFileSync6(
|
|
6553
|
+
writeFileSync6(path20.join(opts.evidenceDir, `${cfg.rep}-absent-${i}-ref.png`), zoomCrop(ref, c));
|
|
6554
|
+
writeFileSync6(path20.join(opts.evidenceDir, `${cfg.rep}-absent-${i}-render.png`), zoomCrop(scored, c));
|
|
6267
6555
|
}
|
|
6268
6556
|
}
|
|
6269
6557
|
return {
|
|
@@ -6364,7 +6652,7 @@ var init_prelude = __esm({
|
|
|
6364
6652
|
PRELUDE_CONTRACT = `BUNDLE PRELUDE (machine-verified on computed styles \u2014 the harness page provides NONE of this; your CSS must):
|
|
6365
6653
|
- 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).
|
|
6366
6654
|
- 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).
|
|
6367
|
-
- Interactive controls (buttons, options): touch-action: manipulation and user-select: none. Text inputs stay selectable (never user-select: none on them).
|
|
6655
|
+
- Interactive controls (buttons, options): touch-action: manipulation and user-select: none. Text inputs stay selectable (never user-select: none on them) \u2014 and never put user-select: none on an ancestor wrapping an editable control either: it inherits into the control; a text-entry component's root gets touch-action: manipulation only.
|
|
6368
6656
|
- Scrollable popovers/menus: overscroll-behavior: contain.
|
|
6369
6657
|
- 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.
|
|
6370
6658
|
- Every animation wrapped in @media (prefers-reduced-motion: no-preference) or disabled under reduce.`;
|
|
@@ -6509,8 +6797,8 @@ var init_parity = __esm({
|
|
|
6509
6797
|
|
|
6510
6798
|
// packages/verify/src/composition.ts
|
|
6511
6799
|
import { createRequire as createRequire2 } from "node:module";
|
|
6512
|
-
import { existsSync as
|
|
6513
|
-
import
|
|
6800
|
+
import { existsSync as existsSync16 } from "node:fs";
|
|
6801
|
+
import path21 from "node:path";
|
|
6514
6802
|
import { build as build6 } from "esbuild";
|
|
6515
6803
|
import { chromium as chromium7 } from "playwright-core";
|
|
6516
6804
|
function getFontFaces4() {
|
|
@@ -6518,9 +6806,9 @@ function getFontFaces4() {
|
|
|
6518
6806
|
return _fontFaces4;
|
|
6519
6807
|
}
|
|
6520
6808
|
async function compileInstrumentedMount(task, bundleDir) {
|
|
6521
|
-
const entryTsx =
|
|
6522
|
-
if (!
|
|
6523
|
-
const requireFromVerify = createRequire2(
|
|
6809
|
+
const entryTsx = path21.join(bundleDir, task.entry);
|
|
6810
|
+
if (!existsSync16(entryTsx)) return { error: `${task.entry} missing` };
|
|
6811
|
+
const requireFromVerify = createRequire2(path21.join(VERIFY_PKG_DIR, "package.json"));
|
|
6524
6812
|
let realJsxPath;
|
|
6525
6813
|
try {
|
|
6526
6814
|
realJsxPath = requireFromVerify.resolve("react/jsx-runtime");
|
|
@@ -6531,7 +6819,7 @@ async function compileInstrumentedMount(task, bundleDir) {
|
|
|
6531
6819
|
import { createElement } from "react";
|
|
6532
6820
|
import { createRoot } from "react-dom/client";
|
|
6533
6821
|
import { __registerParts } from "react/jsx-runtime";
|
|
6534
|
-
import * as B from ${JSON.stringify(
|
|
6822
|
+
import * as B from ${JSON.stringify(path21.resolve(entryTsx))};
|
|
6535
6823
|
const cfg = (window as unknown as { __cfg: { component: string; props: Record<string, unknown>; partComponents: string[] } }).__cfg;
|
|
6536
6824
|
const pairs: Array<[unknown, string]> = [];
|
|
6537
6825
|
for (const name of cfg.partComponents) {
|
|
@@ -6586,7 +6874,7 @@ function expectedParts(task, roles, mainSlug) {
|
|
|
6586
6874
|
}
|
|
6587
6875
|
function interiorRegions(setDir, roles) {
|
|
6588
6876
|
const mains = roles.main;
|
|
6589
|
-
const withInterior = mains.filter((m) =>
|
|
6877
|
+
const withInterior = mains.filter((m) => existsSync16(path21.join(setDir, m, "get_metadata_interior.json")));
|
|
6590
6878
|
if (withInterior.length === 0) {
|
|
6591
6879
|
return { unavailable: "no interior geometry recorded for any main (pre-obligation set) \u2014 re-record with `tendril record` to enable crop checks" };
|
|
6592
6880
|
}
|
|
@@ -6802,17 +7090,17 @@ export const jsxs = (t, p, k) => real.jsxs(t, inject(t, p), k);
|
|
|
6802
7090
|
});
|
|
6803
7091
|
|
|
6804
7092
|
// packages/verify/src/occlusion.ts
|
|
6805
|
-
import { existsSync as
|
|
6806
|
-
import
|
|
7093
|
+
import { existsSync as existsSync17 } from "node:fs";
|
|
7094
|
+
import path22 from "node:path";
|
|
6807
7095
|
import { build as build7 } from "esbuild";
|
|
6808
7096
|
import { chromium as chromium8 } from "playwright-core";
|
|
6809
7097
|
async function compileTwoUp(task, bundleDir) {
|
|
6810
|
-
const entryTsx =
|
|
6811
|
-
if (!
|
|
7098
|
+
const entryTsx = path22.join(bundleDir, task.entry);
|
|
7099
|
+
if (!existsSync17(entryTsx)) return { error: `${task.entry} missing` };
|
|
6812
7100
|
const src = `
|
|
6813
7101
|
import { createElement } from "react";
|
|
6814
7102
|
import { createRoot } from "react-dom/client";
|
|
6815
|
-
import * as B from ${JSON.stringify(
|
|
7103
|
+
import * as B from ${JSON.stringify(path22.resolve(entryTsx))};
|
|
6816
7104
|
const cfg = (window as unknown as { __cfg: { component: string; props: Record<string, unknown> } }).__cfg;
|
|
6817
7105
|
const C = (B as Record<string, unknown>)[cfg.component] as Parameters<typeof createElement>[0] | undefined;
|
|
6818
7106
|
for (const id of ["first", "second"]) {
|
|
@@ -6837,16 +7125,24 @@ for (const id of ["first", "second"]) {
|
|
|
6837
7125
|
return { error: `does not compile: ${err instanceof Error ? err.message.split("\n")[0] : String(err)}` };
|
|
6838
7126
|
}
|
|
6839
7127
|
}
|
|
6840
|
-
function overlayPlan(task) {
|
|
6841
|
-
const
|
|
6842
|
-
|
|
7128
|
+
function overlayPlan(task, authority) {
|
|
7129
|
+
const isOpen = ([k, v]) => v === true && /^(open|isopen|menuopen|expanded|isexpanded)$/i.test(k) || // Authoring's own exclusive-axis shape (`state: "open"`) never
|
|
7130
|
+
// matched the boolean-only rule — even authority-sourced configs
|
|
7131
|
+
// unscheduled the gate (C2's second hole).
|
|
7132
|
+
typeof v === "string" && /^(open|expanded)$/i.test(v);
|
|
7133
|
+
const deciding = authority ?? task.configs;
|
|
7134
|
+
const openDecider = deciding.find((c) => Object.entries(c.props).some(isOpen));
|
|
7135
|
+
if (openDecider !== void 0) {
|
|
7136
|
+
const mounted = task.configs.find((c) => c.rep === openDecider.rep) ?? task.configs.find((c) => Object.entries(c.props).some(isOpen)) ?? openDecider;
|
|
7137
|
+
return { cfg: mounted, selector: OVERLAY_SELECTOR };
|
|
7138
|
+
}
|
|
6843
7139
|
const pop = task.prelude.popover;
|
|
6844
7140
|
const first = task.configs[0];
|
|
6845
7141
|
if (pop !== void 0 && first !== void 0) return { cfg: first, trigger: pop.trigger, selector: pop.selector };
|
|
6846
7142
|
return void 0;
|
|
6847
7143
|
}
|
|
6848
7144
|
async function checkSiblingOcclusion(task, bundleDir, css, opts = {}) {
|
|
6849
|
-
const plan = overlayPlan(task);
|
|
7145
|
+
const plan = overlayPlan(task, opts.authority);
|
|
6850
7146
|
if (plan === void 0) return [];
|
|
6851
7147
|
const pop = { selector: plan.selector, trigger: plan.trigger };
|
|
6852
7148
|
const cfg = plan.cfg;
|
|
@@ -6919,10 +7215,10 @@ body{margin:0;padding:16px;display:flex;flex-direction:column;gap:8px;align-item
|
|
|
6919
7215
|
};
|
|
6920
7216
|
})()`);
|
|
6921
7217
|
if (verdict.kind === "no-menu") {
|
|
6922
|
-
return [{ id: "sibling-overlay-hit-testable", pass: true, detail: `UNAVAILABLE: nothing matched "${pop.selector}" in the open config \u2014 occlusion was not measured` }];
|
|
7218
|
+
return [{ id: "sibling-overlay-hit-testable", pass: true, unmeasured: true, detail: `UNAVAILABLE: nothing matched "${pop.selector}" in the open config \u2014 occlusion was not measured` }];
|
|
6923
7219
|
}
|
|
6924
7220
|
if (verdict.kind === "no-overlap") {
|
|
6925
|
-
return [{ id: "sibling-overlay-hit-testable", pass: true, detail: "not applicable: the open overlay does not overlap the next instance" }];
|
|
7221
|
+
return [{ id: "sibling-overlay-hit-testable", pass: true, unmeasured: true, detail: "not applicable: the open overlay does not overlap the next instance" }];
|
|
6926
7222
|
}
|
|
6927
7223
|
if (verdict.kind !== "menu") {
|
|
6928
7224
|
return [
|
|
@@ -6996,23 +7292,23 @@ var init_src5 = __esm({
|
|
|
6996
7292
|
});
|
|
6997
7293
|
|
|
6998
7294
|
// packages/cli/src/environment.ts
|
|
6999
|
-
import { existsSync as
|
|
7000
|
-
import
|
|
7295
|
+
import { existsSync as existsSync18, readFileSync as readFileSync14 } from "node:fs";
|
|
7296
|
+
import path23 from "node:path";
|
|
7001
7297
|
import { createHash as createHash4 } from "node:crypto";
|
|
7002
7298
|
import { fileURLToPath as fileURLToPath5 } from "node:url";
|
|
7003
7299
|
function cliVersion() {
|
|
7004
7300
|
try {
|
|
7005
|
-
return JSON.parse(
|
|
7301
|
+
return JSON.parse(readFileSync14(path23.join(path23.dirname(fileURLToPath5(import.meta.url)), "..", "package.json"), "utf8")).version ?? "dev";
|
|
7006
7302
|
} catch {
|
|
7007
7303
|
return "dev";
|
|
7008
7304
|
}
|
|
7009
7305
|
}
|
|
7010
7306
|
function environmentStamp(taskFamilies) {
|
|
7011
|
-
const manifestPath2 =
|
|
7307
|
+
const manifestPath2 = path23.join(fontCacheDir(), "manifest.json");
|
|
7012
7308
|
let fontsHash = null;
|
|
7013
|
-
if (
|
|
7309
|
+
if (existsSync18(manifestPath2)) {
|
|
7014
7310
|
try {
|
|
7015
|
-
const entries = JSON.parse(
|
|
7311
|
+
const entries = JSON.parse(readFileSync14(manifestPath2, "utf8"));
|
|
7016
7312
|
const scoped = taskFamilies === void 0 || taskFamilies === null ? entries : entries.filter((f) => taskFamilies.some((fam) => fam.toLowerCase() === f.family.toLowerCase()));
|
|
7017
7313
|
const faces = scoped.map((f) => `${f.family}:${f.weight}:${f.sha256}`).sort();
|
|
7018
7314
|
fontsHash = faces.length === 0 ? null : createHash4("sha256").update(faces.join("\n")).digest("hex").slice(0, 16);
|
|
@@ -7056,8 +7352,8 @@ var init_describe = __esm({
|
|
|
7056
7352
|
});
|
|
7057
7353
|
|
|
7058
7354
|
// packages/cli/src/env.ts
|
|
7059
|
-
import { existsSync as
|
|
7060
|
-
import
|
|
7355
|
+
import { existsSync as existsSync19, readFileSync as readFileSync15 } from "node:fs";
|
|
7356
|
+
import path24 from "node:path";
|
|
7061
7357
|
function parseEnv(content) {
|
|
7062
7358
|
const entries = /* @__PURE__ */ new Map();
|
|
7063
7359
|
for (const line of content.split("\n")) {
|
|
@@ -7069,9 +7365,9 @@ function parseEnv(content) {
|
|
|
7069
7365
|
function resolveCredential(name) {
|
|
7070
7366
|
const fromProcess = process.env[name];
|
|
7071
7367
|
if (fromProcess) return fromProcess;
|
|
7072
|
-
const envPath =
|
|
7073
|
-
if (!
|
|
7074
|
-
return parseEnv(
|
|
7368
|
+
const envPath = path24.resolve(process.env["INIT_CWD"] ?? process.cwd(), ".env");
|
|
7369
|
+
if (!existsSync19(envPath)) return void 0;
|
|
7370
|
+
return parseEnv(readFileSync15(envPath, "utf8")).get(name);
|
|
7075
7371
|
}
|
|
7076
7372
|
var init_env = __esm({
|
|
7077
7373
|
"packages/cli/src/env.ts"() {
|
|
@@ -7131,17 +7427,17 @@ var init_output = __esm({
|
|
|
7131
7427
|
});
|
|
7132
7428
|
|
|
7133
7429
|
// packages/cli/src/entitlement.ts
|
|
7134
|
-
import { chmodSync, existsSync as
|
|
7430
|
+
import { chmodSync, existsSync as existsSync20, mkdirSync as mkdirSync4, readFileSync as readFileSync16, writeFileSync as writeFileSync7 } from "node:fs";
|
|
7135
7431
|
import crypto from "node:crypto";
|
|
7136
7432
|
import os4 from "node:os";
|
|
7137
|
-
import
|
|
7433
|
+
import path25 from "node:path";
|
|
7138
7434
|
function entitlementPath() {
|
|
7139
|
-
return process.env["TENDRIL_ENTITLEMENT_PATH"] ??
|
|
7435
|
+
return process.env["TENDRIL_ENTITLEMENT_PATH"] ?? path25.join(os4.homedir(), ".tendril", "entitlement.json");
|
|
7140
7436
|
}
|
|
7141
7437
|
function readStoredEntitlement(file = entitlementPath()) {
|
|
7142
|
-
if (!
|
|
7438
|
+
if (!existsSync20(file)) return void 0;
|
|
7143
7439
|
try {
|
|
7144
|
-
const parsed = JSON.parse(
|
|
7440
|
+
const parsed = JSON.parse(readFileSync16(file, "utf8"));
|
|
7145
7441
|
if (typeof parsed.token !== "string" || typeof parsed.lastRefreshAt !== "number") return void 0;
|
|
7146
7442
|
return { token: parsed.token, lastRefreshAt: parsed.lastRefreshAt };
|
|
7147
7443
|
} catch {
|
|
@@ -7149,7 +7445,7 @@ function readStoredEntitlement(file = entitlementPath()) {
|
|
|
7149
7445
|
}
|
|
7150
7446
|
}
|
|
7151
7447
|
function writeStoredEntitlement(stored, file = entitlementPath()) {
|
|
7152
|
-
mkdirSync4(
|
|
7448
|
+
mkdirSync4(path25.dirname(file), { recursive: true });
|
|
7153
7449
|
writeFileSync7(file, `${JSON.stringify(stored, null, 2)}
|
|
7154
7450
|
`);
|
|
7155
7451
|
chmodSync(file, 384);
|
|
@@ -7234,9 +7530,9 @@ var init_entitlement = __esm({
|
|
|
7234
7530
|
|
|
7235
7531
|
// packages/cli/src/commands/doctor.ts
|
|
7236
7532
|
import { spawnSync } from "node:child_process";
|
|
7237
|
-
import { existsSync as
|
|
7533
|
+
import { existsSync as existsSync21, readFileSync as readFileSync17, readdirSync as readdirSync6 } from "node:fs";
|
|
7238
7534
|
import os5 from "node:os";
|
|
7239
|
-
import
|
|
7535
|
+
import path26 from "node:path";
|
|
7240
7536
|
function withDeadline(work, ms) {
|
|
7241
7537
|
return Promise.race([
|
|
7242
7538
|
work,
|
|
@@ -7296,17 +7592,17 @@ async function runDoctorChecks(options) {
|
|
|
7296
7592
|
remediation: "Install Google Chrome (or Chromium), or set CHROME_PATH to a Chromium-family browser binary."
|
|
7297
7593
|
});
|
|
7298
7594
|
}
|
|
7299
|
-
const fontManifest =
|
|
7595
|
+
const fontManifest = path26.join(fontCacheDir(), "manifest.json");
|
|
7300
7596
|
checks.push(
|
|
7301
|
-
|
|
7597
|
+
existsSync21(fontManifest) ? { name: "font-cache", ok: true, detail: `resolved font cache at ${fontCacheDir()} (${JSON.parse(readFileSync17(fontManifest, "utf8")).length} faces)` } : {
|
|
7302
7598
|
name: "font-cache",
|
|
7303
7599
|
ok: true,
|
|
7304
7600
|
detail: `font cache empty at ${fontCacheDir()} \u2014 normal on a fresh machine; faces resolve per design system at first use`,
|
|
7305
7601
|
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.`
|
|
7306
7602
|
}
|
|
7307
7603
|
);
|
|
7308
|
-
const pluginRoot =
|
|
7309
|
-
if (
|
|
7604
|
+
const pluginRoot = path26.join(os5.homedir(), ".claude", "plugins", "cache", "tendrilapp", "tendril");
|
|
7605
|
+
if (existsSync21(pluginRoot)) {
|
|
7310
7606
|
try {
|
|
7311
7607
|
const versions = readdirSync6(pluginRoot).filter((v) => /^\d+\.\d+\.\d+$/.test(v));
|
|
7312
7608
|
const newest = versions.sort((a, b) => versionIsNewer(a, b) ? 1 : -1)[0];
|
|
@@ -8397,12 +8693,12 @@ __export(record_exports, {
|
|
|
8397
8693
|
runRecordPlan: () => runRecordPlan,
|
|
8398
8694
|
runRecordStatus: () => runRecordStatus
|
|
8399
8695
|
});
|
|
8400
|
-
import { existsSync as
|
|
8696
|
+
import { existsSync as existsSync23, mkdtempSync as mkdtempSync2, readFileSync as readFileSync19, readdirSync as readdirSync8 } from "node:fs";
|
|
8401
8697
|
import os6 from "node:os";
|
|
8402
|
-
import
|
|
8698
|
+
import path29 from "node:path";
|
|
8403
8699
|
import { writeFileSync as writeFileSync9 } from "node:fs";
|
|
8404
8700
|
function recordsInteractionState(reports) {
|
|
8405
|
-
const evident = (s) => stateTokens(s).some((t) =>
|
|
8701
|
+
const evident = (s) => stateTokens(s).some((t) => INTERACTION_TREATMENT_VALUES.has(t));
|
|
8406
8702
|
return reports.some((r) => evident(r.axis) || r.domain.some(evident));
|
|
8407
8703
|
}
|
|
8408
8704
|
function interactionDisclosure(component, reports) {
|
|
@@ -8422,7 +8718,7 @@ function interactionDisclosure(component, reports) {
|
|
|
8422
8718
|
};
|
|
8423
8719
|
}
|
|
8424
8720
|
function symbolsFromMetadataEnvelope(file, sourceFrame) {
|
|
8425
|
-
const env = JSON.parse(
|
|
8721
|
+
const env = JSON.parse(readFileSync19(file, "utf8"));
|
|
8426
8722
|
const text = env.content.map((c) => c.text ?? "").join("\n");
|
|
8427
8723
|
const symbols = [];
|
|
8428
8724
|
const walk2 = (node, ancestor) => {
|
|
@@ -8480,7 +8776,7 @@ function runRecordPlan(opts) {
|
|
|
8480
8776
|
if (rawFile !== void 0) {
|
|
8481
8777
|
try {
|
|
8482
8778
|
const envelope = rawEnvelopeFromFile(rawFile, opts.metadataRawPartsFile !== void 0);
|
|
8483
|
-
const tmp =
|
|
8779
|
+
const tmp = path29.join(mkdtempSync2(path29.join(os6.tmpdir(), "tendril-plan-meta-")), "get_metadata.json");
|
|
8484
8780
|
writeFileSync9(tmp, JSON.stringify(envelope));
|
|
8485
8781
|
metadataEntries.push({ file: tmp });
|
|
8486
8782
|
} catch (err) {
|
|
@@ -8502,7 +8798,7 @@ function runRecordPlan(opts) {
|
|
|
8502
8798
|
let metadataTruncated = false;
|
|
8503
8799
|
for (const { file, frame } of metadataEntries) {
|
|
8504
8800
|
try {
|
|
8505
|
-
const parsed = symbolsFromMetadataEnvelope(
|
|
8801
|
+
const parsed = symbolsFromMetadataEnvelope(path29.resolve(file), frame);
|
|
8506
8802
|
symbols.push(...parsed.symbols);
|
|
8507
8803
|
if (parsed.truncated) metadataTruncated = true;
|
|
8508
8804
|
} catch (err) {
|
|
@@ -8536,7 +8832,7 @@ function runRecordPlan(opts) {
|
|
|
8536
8832
|
if (symbols.length === 0) {
|
|
8537
8833
|
const leads = metadataEntries.flatMap(({ file }) => {
|
|
8538
8834
|
try {
|
|
8539
|
-
const env = JSON.parse(
|
|
8835
|
+
const env = JSON.parse(readFileSync19(path29.resolve(file), "utf8"));
|
|
8540
8836
|
return instanceLeads(env.content.map((c) => c.text ?? "").join("\n"));
|
|
8541
8837
|
} catch {
|
|
8542
8838
|
return [];
|
|
@@ -8625,7 +8921,7 @@ function runRecordPlan(opts) {
|
|
|
8625
8921
|
// a CLI flag and the MCP surface has no parameter for it.
|
|
8626
8922
|
decidedBy: "HUMAN ONLY \u2014 offer it, never choose it, and you cannot run it",
|
|
8627
8923
|
text: "Record a reduced pose set (anchor + one-factor sweeps + conflict crosses) instead of the full variant matrix. This buys calls with coverage \u2014 sampling is blind to multi-axis interactions and the report discloses the poses it skipped \u2014 so only the user may accept that trade. There is no tool parameter for it; the user runs it themselves in their own terminal, before any pose is recorded.",
|
|
8628
|
-
userRuns: [`rm ${quoteArg(
|
|
8924
|
+
userRuns: [`rm ${quoteArg(path29.join(opts.setDir, "recording-set.json"))}`, `${tendrilCommand(`record plan --set ${quoteArg(opts.setDir)} --component ${quoteArg(manifest.component)}`)} --sample`]
|
|
8629
8925
|
},
|
|
8630
8926
|
{
|
|
8631
8927
|
id: "larger-allowance",
|
|
@@ -8795,7 +9091,7 @@ function runRecordNext(opts) {
|
|
|
8795
9091
|
const progress = payload["progress"];
|
|
8796
9092
|
process.stdout.write(`[${progress.recordedReps}/${progress.totalReps} reps] ${payload["tool"]} for ${payload["slug"]} (node ${payload["nodeId"]})
|
|
8797
9093
|
\u2192 ${payload["note"]}
|
|
8798
|
-
\u2192 then: ${tendrilCommand(`record ingest --set ${
|
|
9094
|
+
\u2192 then: ${tendrilCommand(`record ingest --set ${path29.resolve(opts.setDir)} --rep ${payload["slug"]} --tool ${payload["tool"]} --file <envelope.json>`)}
|
|
8799
9095
|
`);
|
|
8800
9096
|
});
|
|
8801
9097
|
}
|
|
@@ -8869,7 +9165,7 @@ async function autoFetchAssets(setDir, rep, envelopeText2) {
|
|
|
8869
9165
|
const skipped = [];
|
|
8870
9166
|
const failed = [];
|
|
8871
9167
|
for (const { url, name } of assetUrlsFromEnvelopeText(envelopeText2)) {
|
|
8872
|
-
if (
|
|
9168
|
+
if (existsSync23(path29.join(setDir, rep, name))) {
|
|
8873
9169
|
skipped.push(name);
|
|
8874
9170
|
continue;
|
|
8875
9171
|
}
|
|
@@ -8891,16 +9187,16 @@ async function autoFetchAssets(setDir, rep, envelopeText2) {
|
|
|
8891
9187
|
}
|
|
8892
9188
|
function rawEnvelopeFromFile(file, parts) {
|
|
8893
9189
|
if (parts) {
|
|
8894
|
-
const blocks = JSON.parse(
|
|
9190
|
+
const blocks = JSON.parse(readFileSync19(path29.resolve(file), "utf8"));
|
|
8895
9191
|
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");
|
|
8896
9192
|
return { content: blocks.map((text) => ({ type: "text", text })) };
|
|
8897
9193
|
}
|
|
8898
|
-
return { content: [{ type: "text", text:
|
|
9194
|
+
return { content: [{ type: "text", text: readFileSync19(path29.resolve(file), "utf8") }] };
|
|
8899
9195
|
}
|
|
8900
9196
|
async function runRecordIngest(opts) {
|
|
8901
9197
|
let payload;
|
|
8902
9198
|
try {
|
|
8903
|
-
payload = opts.rawParts === true || opts.raw === true ? rawEnvelopeFromFile(opts.file, opts.rawParts === true) : JSON.parse(
|
|
9199
|
+
payload = opts.rawParts === true || opts.raw === true ? rawEnvelopeFromFile(opts.file, opts.rawParts === true) : JSON.parse(readFileSync19(path29.resolve(opts.file), "utf8"));
|
|
8904
9200
|
} catch (err) {
|
|
8905
9201
|
fail(opts, ExitCode.InputValidation, {
|
|
8906
9202
|
error: `cannot read envelope file: ${err instanceof Error ? err.message : String(err)}`,
|
|
@@ -8912,7 +9208,7 @@ async function runRecordIngest(opts) {
|
|
|
8912
9208
|
fail(opts, ExitCode.InputValidation, {
|
|
8913
9209
|
error: "--raw is for text tool responses; screenshots are binary",
|
|
8914
9210
|
code: "envelope-invalid",
|
|
8915
|
-
remediation: `Use \`${tendrilCommand(`record fetch --set ${
|
|
9211
|
+
remediation: `Use \`${tendrilCommand(`record fetch --set ${path29.resolve(opts.setDir)} --rep ${opts.rep} --tool ${opts.tool} --url <image_url>`)}\` instead.`
|
|
8916
9212
|
});
|
|
8917
9213
|
}
|
|
8918
9214
|
if (opts.rep === "__set__" && opts.tool === "get_variable_defs") {
|
|
@@ -8932,7 +9228,7 @@ async function runRecordIngest(opts) {
|
|
|
8932
9228
|
remediation: REINGEST_GUIDANCE
|
|
8933
9229
|
});
|
|
8934
9230
|
}
|
|
8935
|
-
writeFileSync9(
|
|
9231
|
+
writeFileSync9(path29.join(opts.setDir, "get_variable_defs.json"), `${JSON.stringify(payload, null, 1)}
|
|
8936
9232
|
`);
|
|
8937
9233
|
emitData(opts, { ingested: "get_variable_defs", rep: "__set__", setLevel: true, next: nextPayload(opts.setDir) }, () => {
|
|
8938
9234
|
process.stdout.write("set-level get_variable_defs ingested\n");
|
|
@@ -8956,7 +9252,7 @@ async function runRecordIngest(opts) {
|
|
|
8956
9252
|
remediation: REINGEST_GUIDANCE
|
|
8957
9253
|
});
|
|
8958
9254
|
}
|
|
8959
|
-
writeFileSync9(
|
|
9255
|
+
writeFileSync9(path29.join(opts.setDir, "get_motion_context.json"), `${JSON.stringify(payload, null, 1)}
|
|
8960
9256
|
`);
|
|
8961
9257
|
emitData(opts, { ingested: "get_motion_context", rep: "__set__", setLevel: true, next: nextPayload(opts.setDir) }, () => {
|
|
8962
9258
|
process.stdout.write("set-level get_motion_context ingested\n");
|
|
@@ -8972,7 +9268,7 @@ async function runRecordIngest(opts) {
|
|
|
8972
9268
|
if (assets !== void 0) {
|
|
8973
9269
|
for (const a of assets.fetched) process.stdout.write(` asset fetched ${a}
|
|
8974
9270
|
`);
|
|
8975
|
-
for (const f of assets.failed) process.stdout.write(` ASSET FAILED ${f.name}: ${f.reason} \u2014 download it, then: ${tendrilCommand(`record asset --set ${
|
|
9271
|
+
for (const f of assets.failed) process.stdout.write(` ASSET FAILED ${f.name}: ${f.reason} \u2014 download it, then: ${tendrilCommand(`record asset --set ${path29.resolve(opts.setDir)} --rep ${opts.rep} --name ${f.name} --file <downloaded-file>`)}
|
|
8976
9272
|
`);
|
|
8977
9273
|
}
|
|
8978
9274
|
});
|
|
@@ -9045,14 +9341,14 @@ async function runRecordIngestRep(opts) {
|
|
|
9045
9341
|
if (assets !== void 0) {
|
|
9046
9342
|
for (const a of assets.fetched) process.stdout.write(` asset fetched ${a}
|
|
9047
9343
|
`);
|
|
9048
|
-
for (const f of assets.failed) process.stdout.write(` ASSET FAILED ${f.name}: ${f.reason} \u2014 download it, then: ${tendrilCommand(`record asset --set ${
|
|
9344
|
+
for (const f of assets.failed) process.stdout.write(` ASSET FAILED ${f.name}: ${f.reason} \u2014 download it, then: ${tendrilCommand(`record asset --set ${path29.resolve(opts.setDir)} --rep ${opts.rep} --name ${f.name} --file <downloaded-file>`)}
|
|
9049
9345
|
`);
|
|
9050
9346
|
}
|
|
9051
9347
|
});
|
|
9052
9348
|
}
|
|
9053
9349
|
function runRecordAsset(opts) {
|
|
9054
9350
|
if (opts.dir !== void 0) {
|
|
9055
|
-
const dir =
|
|
9351
|
+
const dir = path29.resolve(opts.dir);
|
|
9056
9352
|
const names = readdirSync8(dir).filter((f) => /^asset-[\w.-]+\.(svg|png|jpe?g|webp|gif)$/i.test(f));
|
|
9057
9353
|
if (names.length === 0) {
|
|
9058
9354
|
fail(opts, ExitCode.InputValidation, {
|
|
@@ -9064,7 +9360,7 @@ function runRecordAsset(opts) {
|
|
|
9064
9360
|
const ingested = [];
|
|
9065
9361
|
try {
|
|
9066
9362
|
for (const name of names) {
|
|
9067
|
-
ingestAsset(opts.setDir, opts.rep, name,
|
|
9363
|
+
ingestAsset(opts.setDir, opts.rep, name, readFileSync19(path29.join(dir, name)));
|
|
9068
9364
|
ingested.push(name);
|
|
9069
9365
|
}
|
|
9070
9366
|
} catch (err) {
|
|
@@ -9084,11 +9380,11 @@ function runRecordAsset(opts) {
|
|
|
9084
9380
|
fail(opts, ExitCode.InputValidation, {
|
|
9085
9381
|
error: "pass --name and --file for a single asset, or --dir for a batch",
|
|
9086
9382
|
code: "asset-rejected",
|
|
9087
|
-
remediation: tendrilCommand(`record asset --set ${
|
|
9383
|
+
remediation: tendrilCommand(`record asset --set ${path29.resolve(opts.setDir)} --rep ${opts.rep} --dir <downloads-dir>`)
|
|
9088
9384
|
});
|
|
9089
9385
|
}
|
|
9090
9386
|
try {
|
|
9091
|
-
ingestAsset(opts.setDir, opts.rep, opts.name,
|
|
9387
|
+
ingestAsset(opts.setDir, opts.rep, opts.name, readFileSync19(path29.resolve(opts.file)));
|
|
9092
9388
|
emitData(opts, { rep: opts.rep, asset: opts.name }, () => {
|
|
9093
9389
|
process.stdout.write(`ingested ${opts.rep}/${opts.name}
|
|
9094
9390
|
`);
|
|
@@ -9143,7 +9439,7 @@ function narrowedRoles(derived, override) {
|
|
|
9143
9439
|
function rolesFromFile(opts, file, derived) {
|
|
9144
9440
|
let json;
|
|
9145
9441
|
try {
|
|
9146
|
-
json = JSON.parse(
|
|
9442
|
+
json = JSON.parse(readFileSync19(path29.resolve(file), "utf8"));
|
|
9147
9443
|
} catch (err) {
|
|
9148
9444
|
fail(opts, ExitCode.InputValidation, {
|
|
9149
9445
|
error: `cannot read roles file ${file}: ${err instanceof Error ? err.message.split("\n")[0] : String(err)}`,
|
|
@@ -9181,11 +9477,11 @@ function rolesFromFile(opts, file, derived) {
|
|
|
9181
9477
|
};
|
|
9182
9478
|
}
|
|
9183
9479
|
function runRecordFinish(opts) {
|
|
9184
|
-
if (!
|
|
9480
|
+
if (!existsSync23(path29.join(opts.setDir, "recording-set.json"))) {
|
|
9185
9481
|
fail(opts, ExitCode.InputValidation, {
|
|
9186
9482
|
error: `no recording-set.json in ${opts.setDir}`,
|
|
9187
9483
|
code: "no-recording-set",
|
|
9188
|
-
remediation: `Run \`${tendrilCommand(`record plan --set ${
|
|
9484
|
+
remediation: `Run \`${tendrilCommand(`record plan --set ${path29.resolve(opts.setDir)} --component <name> --metadata <envelope.json>`)}\` first \u2014 \`record finish\` closes a set the planner opened.`
|
|
9189
9485
|
});
|
|
9190
9486
|
}
|
|
9191
9487
|
const { manifest, raw } = readManifestFile(opts.setDir);
|
|
@@ -9213,17 +9509,17 @@ function runRecordFinish(opts) {
|
|
|
9213
9509
|
fail(opts, ExitCode.ConfirmationRequired, {
|
|
9214
9510
|
error: "--confirm-roles (and --roles-file) require an interactive terminal \u2014 this confirmation is human-only",
|
|
9215
9511
|
code: "roles-confirmation-not-interactive",
|
|
9216
|
-
remediation: `A human runs \`${tendrilCommand(`record finish --set ${
|
|
9512
|
+
remediation: `A human runs \`${tendrilCommand(`record finish --set ${path29.resolve(opts.setDir)} --confirm-roles`)}\` in their own terminal. Agents: show the proposal to your operator instead of confirming it.`
|
|
9217
9513
|
});
|
|
9218
9514
|
}
|
|
9219
9515
|
const merged = { ...raw, roles };
|
|
9220
|
-
const { issues } = validateRecordingSet(merged, (rel) =>
|
|
9516
|
+
const { issues } = validateRecordingSet(merged, (rel) => existsSync23(path29.join(opts.setDir, rel)));
|
|
9221
9517
|
const errors = issues.filter((i) => i.severity === "error");
|
|
9222
9518
|
if (errors.length > 0) {
|
|
9223
9519
|
fail(opts, ExitCode.InputValidation, {
|
|
9224
9520
|
error: `recording set rejected \u2014 roles NOT written: ${errors.map((e) => e.message).join("; ")}`,
|
|
9225
9521
|
code: "recording-set-invalid",
|
|
9226
|
-
remediation: `Fix the set (\`${tendrilCommand(`record status --set ${
|
|
9522
|
+
remediation: `Fix the set (\`${tendrilCommand(`record status --set ${path29.resolve(opts.setDir)}`)}\` lists missing envelopes) or the roles graph, then re-run \`record finish\`.`
|
|
9227
9523
|
});
|
|
9228
9524
|
}
|
|
9229
9525
|
for (const issue of issues) if (issue.severity === "warning") warn(opts, issue.message);
|
|
@@ -9270,6 +9566,74 @@ var init_record = __esm({
|
|
|
9270
9566
|
}
|
|
9271
9567
|
});
|
|
9272
9568
|
|
|
9569
|
+
// packages/generate/src/archetype.ts
|
|
9570
|
+
function deriveArchetype(signals) {
|
|
9571
|
+
if (signals.textEntry.length > 0) {
|
|
9572
|
+
return {
|
|
9573
|
+
archetype: "text-input",
|
|
9574
|
+
evidence: signals.textEntry,
|
|
9575
|
+
// textarea included: the recording cannot distinguish single-
|
|
9576
|
+
// from multi-line entry, and the stay-selectable/value/focus
|
|
9577
|
+
// contracts are identical for both.
|
|
9578
|
+
nativeControl: ":is(input, textarea)",
|
|
9579
|
+
derivedStates: { ...TEXT_INPUT_DERIVED }
|
|
9580
|
+
};
|
|
9581
|
+
}
|
|
9582
|
+
if (signals.selection) {
|
|
9583
|
+
return {
|
|
9584
|
+
archetype: "selectable-control",
|
|
9585
|
+
evidence: ["selection axis (boolean selected/checked prop)"],
|
|
9586
|
+
// The honest shapes for a selection control (Cycle C F11):
|
|
9587
|
+
// native input (radio/checkbox pattern) or an aria-pressed
|
|
9588
|
+
// button; select for listbox-shaped kits.
|
|
9589
|
+
nativeControl: ":is(input, button, select)",
|
|
9590
|
+
derivedStates: { ...CONTROL_DERIVED }
|
|
9591
|
+
};
|
|
9592
|
+
}
|
|
9593
|
+
const STRONG_ACTION_VALUES = /* @__PURE__ */ new Set(["hover", "hovered", "focus", "focused", "focus-visible", "active", "pressed", "press"]);
|
|
9594
|
+
const kebab5 = (s) => s.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "");
|
|
9595
|
+
const strongEvidence = signals.interactionEvidence.some((e) => {
|
|
9596
|
+
const value = kebab5(e.split("=").slice(1).join("="));
|
|
9597
|
+
return STRONG_ACTION_VALUES.has(value) || value.split("-").some((t) => STRONG_ACTION_VALUES.has(t));
|
|
9598
|
+
});
|
|
9599
|
+
if (signals.forcedStates.length > 0 || strongEvidence) {
|
|
9600
|
+
return {
|
|
9601
|
+
archetype: "action-control",
|
|
9602
|
+
evidence: signals.forcedStates.length > 0 ? signals.forcedStates.map((t) => `forced state "${t}"`) : signals.interactionEvidence,
|
|
9603
|
+
// a[href] included (review): the brief's own root-size text
|
|
9604
|
+
// endorses an <a href> root for link shapes — demanding <button>
|
|
9605
|
+
// would fail the principal-level implementation of a link kit.
|
|
9606
|
+
nativeControl: ":is(button, a[href])",
|
|
9607
|
+
derivedStates: { ...CONTROL_DERIVED }
|
|
9608
|
+
};
|
|
9609
|
+
}
|
|
9610
|
+
return { archetype: "static", evidence: [], nativeControl: "", derivedStates: {} };
|
|
9611
|
+
}
|
|
9612
|
+
function derivedStateKind(canon, kebabValue) {
|
|
9613
|
+
return canon.derivedStates[kebabValue];
|
|
9614
|
+
}
|
|
9615
|
+
var TEXT_INPUT_DERIVED, CONTROL_DERIVED;
|
|
9616
|
+
var init_archetype = __esm({
|
|
9617
|
+
"packages/generate/src/archetype.ts"() {
|
|
9618
|
+
"use strict";
|
|
9619
|
+
TEXT_INPUT_DERIVED = {
|
|
9620
|
+
focus: "focus",
|
|
9621
|
+
focused: "focus",
|
|
9622
|
+
"focus-visible": "focus",
|
|
9623
|
+
filled: "value",
|
|
9624
|
+
typing: "value",
|
|
9625
|
+
placeholder: "value",
|
|
9626
|
+
error: "error-presence",
|
|
9627
|
+
invalid: "error-presence"
|
|
9628
|
+
};
|
|
9629
|
+
CONTROL_DERIVED = {
|
|
9630
|
+
focus: "focus",
|
|
9631
|
+
focused: "focus",
|
|
9632
|
+
"focus-visible": "focus"
|
|
9633
|
+
};
|
|
9634
|
+
}
|
|
9635
|
+
});
|
|
9636
|
+
|
|
9273
9637
|
// packages/generate/src/engine.ts
|
|
9274
9638
|
var init_engine = __esm({
|
|
9275
9639
|
"packages/generate/src/engine.ts"() {
|
|
@@ -9457,8 +9821,8 @@ var init_engine_curated = __esm({
|
|
|
9457
9821
|
});
|
|
9458
9822
|
|
|
9459
9823
|
// packages/generate/src/loop.ts
|
|
9460
|
-
import { existsSync as
|
|
9461
|
-
import
|
|
9824
|
+
import { existsSync as existsSync24, mkdirSync as mkdirSync6, readFileSync as readFileSync20, renameSync, writeFileSync as writeFileSync10 } from "node:fs";
|
|
9825
|
+
import path30 from "node:path";
|
|
9462
9826
|
import { z as z12 } from "zod";
|
|
9463
9827
|
function objective(scores, behaviors) {
|
|
9464
9828
|
const vals = scores.map((s) => Math.min(s.similarity, s.inkRecall));
|
|
@@ -9470,7 +9834,9 @@ function objective(scores, behaviors) {
|
|
|
9470
9834
|
}
|
|
9471
9835
|
function buildFeedback(scores, behaviors, bar, mode = "fenced") {
|
|
9472
9836
|
const preludeLines = behaviors.filter((b) => !b.pass && b.id.startsWith("prelude:")).map((b) => `PRELUDE FAIL ${b.id}: ${b.detail ?? "failed"}`);
|
|
9473
|
-
const
|
|
9837
|
+
const unmeasurable = behaviors.filter((b) => !b.pass && !b.id.startsWith("prelude:") && (b.detail?.startsWith("unmeasurable:") ?? false));
|
|
9838
|
+
const unmeasurableLines = unmeasurable.map((b) => `UNMEASURABLE ${b.id}: ${b.detail ?? "failed"}`);
|
|
9839
|
+
const behLines = behaviors.filter((b) => !b.pass && !b.id.startsWith("prelude:") && !unmeasurable.includes(b)).map((b) => `BEHAVIOR FAIL ${b.id}: ${b.detail ?? "failed"}`);
|
|
9474
9840
|
const lines = scores.map((s) => {
|
|
9475
9841
|
const base = `${s.pass ? "PASS" : "FAIL"} ${s.rep}: sim=${s.similarity} ink=${s.inkRecall}`;
|
|
9476
9842
|
const err = s.error !== void 0 ? ` error=${s.error}` : "";
|
|
@@ -9484,7 +9850,10 @@ function buildFeedback(scores, behaviors, bar, mode = "fenced") {
|
|
|
9484
9850
|
${lines.join("\n")}${behLines.length > 0 ? `
|
|
9485
9851
|
|
|
9486
9852
|
BEHAVIORAL invariants (machine-verified, GATING \u2014 fix these with real handlers/animation/cursor, do not fake pixels):
|
|
9487
|
-
${behLines.join("\n")}` : ""}${
|
|
9853
|
+
${behLines.join("\n")}` : ""}${unmeasurableLines.length > 0 ? `
|
|
9854
|
+
|
|
9855
|
+
UNMEASURABLE checks (still failing, but read the cause BEFORE spending a round: each line says whether the blocker is your render \u2014 fixable, fix the named pose's paint \u2014 or the recording/at-rest motion, which NO component code can fix; in that case REPORT it and do not iterate on it):
|
|
9856
|
+
${unmeasurableLines.join("\n")}` : ""}${preludeLines.length > 0 ? `
|
|
9488
9857
|
|
|
9489
9858
|
PRELUDE parity (computed-style checks, GATING \u2014 fix the named CSS property on the named element):
|
|
9490
9859
|
${preludeLines.join("\n")}` : ""}${absentLines.length > 0 ? `
|
|
@@ -9495,9 +9864,9 @@ ${absentLines.join("\n")}` : ""}
|
|
|
9495
9864
|
Fix the FAIL configs (region coordinates are in the recorded screenshot's frame; ink<1 means recorded foreground pixels your render does not cover). Reading the pair: ink credits a recorded pixel when ANY render ink lands within 1px of it, so ink=1 proves coverage \u2014 not presence, shape, or colour. A wrong-shaped mark over the right region, or a wrong-coloured one that is still ink, scores 1.000; and a recorded mark within ~30 channel-sum of the backdrop (#f6f6f6 on white is 27) is not ink at all, so it can be absent at ink 1.000 with no MISSING line. With ink=1 AND low sim, coverage held while pixels differ, so start with the TWO causes below \u2014 but never read ink=1 as "nothing is missing": confirm every faint or small recorded mark (hairlines, dividers, low-contrast controls) exists in your render. GEOMETRY: wrong position/size/radius; the diff shows shifted edges and bands. WRONG TEXT WEIGHT OR FACE: the diff shows a uniform haze over glyph runs; CSS binds the font FAMILY before the weight, a later family in the stack is never consulted for a weight the first one lacks, and font-synthesis: none rules out faux-bold \u2014 so a stack led by a family the kit holds at one weight renders every other weight in that face, silently. Check the font stack against the kit's cached faces before concluding geometry is at fault. LOW ink with high sim means recorded marks are absent or painted invisibly. Do not regress PASS configs. ${mode === "files" ? "Update the files in your candidate directory and re-run the score." : "Reply with the complete corrected files in the same FILE format."}`;
|
|
9496
9865
|
}
|
|
9497
9866
|
function archivePriorRun(outDir) {
|
|
9498
|
-
if (!
|
|
9867
|
+
if (!existsSync24(path30.join(outDir, "run-log.json")) && !existsSync24(path30.join(outDir, "loop-state.json"))) return void 0;
|
|
9499
9868
|
let n = 1;
|
|
9500
|
-
while (
|
|
9869
|
+
while (existsSync24(`${outDir}-prev-${n}`)) n += 1;
|
|
9501
9870
|
renameSync(outDir, `${outDir}-prev-${n}`);
|
|
9502
9871
|
return `${outDir}-prev-${n}`;
|
|
9503
9872
|
}
|
|
@@ -9506,14 +9875,14 @@ async function runEngineLoop(opts) {
|
|
|
9506
9875
|
const plateau = opts.plateau ?? 2;
|
|
9507
9876
|
const progress = opts.onProgress ?? (() => {
|
|
9508
9877
|
});
|
|
9509
|
-
const statePath =
|
|
9510
|
-
const resuming = opts.resume === true &&
|
|
9878
|
+
const statePath = path30.join(opts.outDir, "loop-state.json");
|
|
9879
|
+
const resuming = opts.resume === true && existsSync24(statePath);
|
|
9511
9880
|
if (!resuming) {
|
|
9512
9881
|
const archived = archivePriorRun(opts.outDir);
|
|
9513
9882
|
if (archived !== void 0) progress(`previous run archived to ${archived}`);
|
|
9514
9883
|
}
|
|
9515
9884
|
mkdirSync6(opts.outDir, { recursive: true });
|
|
9516
|
-
const scratch =
|
|
9885
|
+
const scratch = path30.join(opts.outDir, ".candidate");
|
|
9517
9886
|
let attempts = [];
|
|
9518
9887
|
let log = [];
|
|
9519
9888
|
let best;
|
|
@@ -9521,7 +9890,7 @@ async function runEngineLoop(opts) {
|
|
|
9521
9890
|
let nonAccepted = 0;
|
|
9522
9891
|
let stopReason = "max-iterations";
|
|
9523
9892
|
if (resuming) {
|
|
9524
|
-
const restored = LoopStateSchema.parse(JSON.parse(
|
|
9893
|
+
const restored = LoopStateSchema.parse(JSON.parse(readFileSync20(statePath, "utf8")));
|
|
9525
9894
|
attempts = restored.attempts;
|
|
9526
9895
|
log = restored.iterations;
|
|
9527
9896
|
spentUsd = restored.spentUsd;
|
|
@@ -9541,7 +9910,7 @@ async function runEngineLoop(opts) {
|
|
|
9541
9910
|
};
|
|
9542
9911
|
const writeCandidate = (files) => {
|
|
9543
9912
|
mkdirSync6(scratch, { recursive: true });
|
|
9544
|
-
for (const [name, content] of Object.entries(files)) writeFileSync10(
|
|
9913
|
+
for (const [name, content] of Object.entries(files)) writeFileSync10(path30.join(scratch, name), content);
|
|
9545
9914
|
};
|
|
9546
9915
|
const scoreCandidate = async (candidate, iter, usd, modelMs) => {
|
|
9547
9916
|
writeCandidate(candidate.files);
|
|
@@ -9599,8 +9968,8 @@ async function runEngineLoop(opts) {
|
|
|
9599
9968
|
const usd = candidate.usage?.usd ?? 0;
|
|
9600
9969
|
spentUsd += usd;
|
|
9601
9970
|
if (candidate.raw !== void 0) {
|
|
9602
|
-
mkdirSync6(
|
|
9603
|
-
writeFileSync10(
|
|
9971
|
+
mkdirSync6(path30.join(opts.outDir, "responses"), { recursive: true });
|
|
9972
|
+
writeFileSync10(path30.join(opts.outDir, "responses", `iter-${iter}.md`), candidate.raw);
|
|
9604
9973
|
}
|
|
9605
9974
|
if (candidate.files[opts.entry] === void 0 || candidate.files["styles.css"] === void 0) {
|
|
9606
9975
|
const finish = candidate.usage?.finishReason ?? "?";
|
|
@@ -9626,10 +9995,10 @@ async function runEngineLoop(opts) {
|
|
|
9626
9995
|
}
|
|
9627
9996
|
}
|
|
9628
9997
|
}
|
|
9629
|
-
if (best !== void 0) for (const [name, content] of Object.entries(best.files)) writeFileSync10(
|
|
9998
|
+
if (best !== void 0) for (const [name, content] of Object.entries(best.files)) writeFileSync10(path30.join(opts.outDir, name), content);
|
|
9630
9999
|
const final = best !== void 0 ? await opts.score(opts.outDir) : { scores: [], behaviors: [] };
|
|
9631
10000
|
writeFileSync10(
|
|
9632
|
-
|
|
10001
|
+
path30.join(opts.outDir, "run-log.json"),
|
|
9633
10002
|
`${JSON.stringify(
|
|
9634
10003
|
{
|
|
9635
10004
|
...opts.meta,
|
|
@@ -9696,8 +10065,9 @@ var init_loop2 = __esm({
|
|
|
9696
10065
|
});
|
|
9697
10066
|
|
|
9698
10067
|
// packages/generate/src/brief.ts
|
|
9699
|
-
import { existsSync as
|
|
9700
|
-
import
|
|
10068
|
+
import { existsSync as existsSync25, readFileSync as readFileSync21 } from "node:fs";
|
|
10069
|
+
import path31 from "node:path";
|
|
10070
|
+
import { PNG as PNG3 } from "pngjs";
|
|
9701
10071
|
function singleAxes2(name) {
|
|
9702
10072
|
const parsed = parseVariantAxes(name);
|
|
9703
10073
|
if (parsed === void 0) return void 0;
|
|
@@ -9737,13 +10107,50 @@ function authorComponentApi(opts) {
|
|
|
9737
10107
|
if (/^(is)?(selected|checked)$/i.test(camel(key))) interactionEvidence.push(`${key} (selection axis)`);
|
|
9738
10108
|
for (const v of domains.get(key)) {
|
|
9739
10109
|
const k = kebab3(v);
|
|
9740
|
-
|
|
10110
|
+
const engaged = ENGAGED_STATE_VALUES.has(k) && kebab3(key) === "state";
|
|
10111
|
+
const classic = (t) => INTERACTION_EVIDENCE_VALUES.has(t) && !ENGAGED_STATE_VALUES.has(t);
|
|
10112
|
+
if (engaged || classic(k) || k.split("-").some(classic)) {
|
|
9741
10113
|
interactionEvidence.push(`${key}=${v}`);
|
|
9742
10114
|
}
|
|
9743
10115
|
}
|
|
9744
10116
|
}
|
|
10117
|
+
const TEXT_ENTRY_STATE_VALUES = /* @__PURE__ */ new Set(["typing", "filled", "placeholder"]);
|
|
10118
|
+
const TEXT_ENTRY_AXIS_NAMES = /* @__PURE__ */ new Set(["typing", "text-entered"]);
|
|
10119
|
+
const textEntry = [];
|
|
10120
|
+
for (const key of axisKeys) {
|
|
10121
|
+
const domain = domains.get(key);
|
|
10122
|
+
if (TEXT_ENTRY_AXIS_NAMES.has(kebab3(key)) && domain.every((v) => ["true", "false"].includes(kebab3(v)))) {
|
|
10123
|
+
textEntry.push(`${key} (boolean axis)`);
|
|
10124
|
+
}
|
|
10125
|
+
if (kebab3(key) !== "state") continue;
|
|
10126
|
+
if ((opts.textSlots ?? []).length === 0) continue;
|
|
10127
|
+
for (const v of domain) if (TEXT_ENTRY_STATE_VALUES.has(kebab3(v))) textEntry.push(`${key}=${v}`);
|
|
10128
|
+
}
|
|
10129
|
+
const preForcedStates = axisKeys.filter(isStateAxis).flatMap((key) => domains.get(key).map(kebab3).filter((k) => INTERACTION_STATES.has(k)));
|
|
10130
|
+
const archetype = deriveArchetype({
|
|
10131
|
+
textEntry,
|
|
10132
|
+
selection: interactionEvidence.some((e) => e.endsWith(" (selection axis)")),
|
|
10133
|
+
forcedStates: preForcedStates,
|
|
10134
|
+
interactionEvidence
|
|
10135
|
+
});
|
|
10136
|
+
const stateAxis = axisKeys.find(isStateAxis);
|
|
10137
|
+
const errorReps = new Set(
|
|
10138
|
+
stateAxis === void 0 ? [] : axed.filter((p) => derivedStateKind(archetype, kebab3(p.axes[stateAxis])) === "error-presence").map((p) => p.slug)
|
|
10139
|
+
);
|
|
10140
|
+
const errorSourceSlot = (opts.textSlots ?? []).find((s) => s.visibleIn.length > 0 && s.visibleIn.every((r) => errorReps.has(r)));
|
|
10141
|
+
const errorViaProp = errorSourceSlot !== void 0;
|
|
10142
|
+
const valueReps = new Set(
|
|
10143
|
+
stateAxis === void 0 ? [] : axed.filter((p) => {
|
|
10144
|
+
const k = kebab3(p.axes[stateAxis]);
|
|
10145
|
+
return derivedStateKind(archetype, k) === "value" && k !== "placeholder";
|
|
10146
|
+
}).map((p) => p.slug)
|
|
10147
|
+
);
|
|
10148
|
+
const valueSourceSlot = (opts.textSlots ?? []).find((s) => s !== errorSourceSlot && s.visibleIn.length > 0 && s.visibleIn.every((r) => valueReps.has(r)));
|
|
10149
|
+
const valueViaProp = valueSourceSlot !== void 0;
|
|
10150
|
+
const derivedPoseProps = {};
|
|
9745
10151
|
const props = [];
|
|
9746
10152
|
const forcedStates = [];
|
|
10153
|
+
const derivedStates = [];
|
|
9747
10154
|
const syntheticCombos = [];
|
|
9748
10155
|
const propNameFor = (axis) => axisPropName(opts.component, axis);
|
|
9749
10156
|
for (const key of axisKeys) {
|
|
@@ -9760,6 +10167,13 @@ function authorComponentApi(opts) {
|
|
|
9760
10167
|
} else if (BOOLEAN_STATES.has(k)) {
|
|
9761
10168
|
props.push({ name: k, kind: "boolean" });
|
|
9762
10169
|
splitBooleans.push(k);
|
|
10170
|
+
} else if (derivedStateKind(archetype, k) !== void 0) {
|
|
10171
|
+
const kind = derivedStateKind(archetype, k);
|
|
10172
|
+
if (kind === "error-presence" && errorViaProp || kind === "value" && valueViaProp && k !== "placeholder") {
|
|
10173
|
+
} else {
|
|
10174
|
+
if (!forcedStates.includes(k)) forcedStates.push(k);
|
|
10175
|
+
if (!derivedStates.includes(k)) derivedStates.push(k);
|
|
10176
|
+
}
|
|
9763
10177
|
} else {
|
|
9764
10178
|
unionValues.push(kebab3(v));
|
|
9765
10179
|
}
|
|
@@ -9777,14 +10191,19 @@ function authorComponentApi(opts) {
|
|
|
9777
10191
|
props.push({ name: propNameFor(key), kind: "union", values: ordered.map(kebab3), default: kebab3(def) });
|
|
9778
10192
|
}
|
|
9779
10193
|
}
|
|
9780
|
-
const
|
|
9781
|
-
|
|
9782
|
-
|
|
10194
|
+
const slotRoles = /* @__PURE__ */ new Map();
|
|
10195
|
+
const slots = (opts.textSlots ?? []).filter((slot) => slot !== valueSourceSlot).map((slot) => {
|
|
10196
|
+
let name = slot === errorSourceSlot ? "error" : slot.prop;
|
|
10197
|
+
if (name !== "children" && (props.some((pr) => pr.name === name) || RESERVED_PROPS.has(name.toLowerCase()) || NATIVE_ATTR_COLLISIONS.has(name.toLowerCase()))) name = `${name}Text`;
|
|
9783
10198
|
let n = 2;
|
|
9784
10199
|
while (props.some((pr) => pr.name === name)) name = `${slot.prop}Text${n++}`;
|
|
10200
|
+
if (slot === errorSourceSlot) slotRoles.set(name, "error");
|
|
9785
10201
|
return { ...slot, prop: name };
|
|
9786
10202
|
});
|
|
9787
|
-
for (const slot of slots)
|
|
10203
|
+
for (const slot of slots) {
|
|
10204
|
+
const role = slotRoles.get(slot.prop);
|
|
10205
|
+
props.push({ name: slot.prop, kind: "string", default: slot.default, ...role !== void 0 ? { role } : {} });
|
|
10206
|
+
}
|
|
9788
10207
|
const configs = [];
|
|
9789
10208
|
const seenAssignments = /* @__PURE__ */ new Map();
|
|
9790
10209
|
const componentIdent = pascal(opts.component);
|
|
@@ -9796,9 +10215,18 @@ function authorComponentApi(opts) {
|
|
|
9796
10215
|
if (v === defaults.get(key)) continue;
|
|
9797
10216
|
const k = kebab3(v);
|
|
9798
10217
|
if (isStateAxis(key)) {
|
|
9799
|
-
|
|
10218
|
+
const derived = derivedStateKind(archetype, k);
|
|
10219
|
+
const viaProp = derived === "error-presence" && errorViaProp || derived === "value" && valueViaProp && k !== "placeholder";
|
|
10220
|
+
if (INTERACTION_STATES.has(k) || derived !== void 0 && !viaProp) tokens.push(k);
|
|
9800
10221
|
else if (BOOLEAN_STATES.has(k)) assignment[k] = true;
|
|
9801
|
-
else
|
|
10222
|
+
else if (derived === "value" && viaProp) {
|
|
10223
|
+
const text = valueSourceSlot.overrides[p.slug] ?? valueSourceSlot.default;
|
|
10224
|
+
assignment["defaultValue"] = text;
|
|
10225
|
+
derivedPoseProps[k] ??= { defaultValue: text };
|
|
10226
|
+
} else if (derived === "error-presence" && viaProp) {
|
|
10227
|
+
const name = [...slotRoles.entries()].find(([, r]) => r === "error")?.[0];
|
|
10228
|
+
if (name !== void 0) derivedPoseProps[k] ??= { [name]: errorSourceSlot.overrides[p.slug] ?? errorSourceSlot.default };
|
|
10229
|
+
} else if (derived === void 0) assignment[propNameFor(key)] = k;
|
|
9802
10230
|
} else if (["true", "false"].includes(k)) {
|
|
9803
10231
|
assignment[propNameFor(key)] = k === "true";
|
|
9804
10232
|
} else {
|
|
@@ -9809,6 +10237,7 @@ function authorComponentApi(opts) {
|
|
|
9809
10237
|
for (const slot of slots) {
|
|
9810
10238
|
const v = slot.overrides[p.slug];
|
|
9811
10239
|
if (v !== void 0) assignment[slot.prop] = v;
|
|
10240
|
+
else if (slotRoles.get(slot.prop) === "error" && errorReps.has(p.slug)) assignment[slot.prop] = slot.default;
|
|
9812
10241
|
}
|
|
9813
10242
|
const canonical = JSON.stringify(Object.fromEntries(Object.entries(assignment).sort(([a], [b]) => a.localeCompare(b))));
|
|
9814
10243
|
const clash = seenAssignments.get(canonical);
|
|
@@ -9827,6 +10256,8 @@ function authorComponentApi(opts) {
|
|
|
9827
10256
|
props.push({ name: "onDismiss", kind: "callback", default: opts.dismissible });
|
|
9828
10257
|
}
|
|
9829
10258
|
const entry = `${componentIdent}.tsx`;
|
|
10259
|
+
const extendsNative = archetype.archetype === "text-input" ? "input" : archetype.archetype === "selectable-control" ? "input" : archetype.archetype === "action-control" ? "button" : "div";
|
|
10260
|
+
const isControl = archetype.archetype !== "static";
|
|
9830
10261
|
const apiPin = {
|
|
9831
10262
|
name: componentIdent,
|
|
9832
10263
|
props: props.map((pr) => ({
|
|
@@ -9834,14 +10265,19 @@ function authorComponentApi(opts) {
|
|
|
9834
10265
|
type: pr.kind === "boolean" ? "boolean" : pr.kind === "string" ? "string" : pr.kind === "callback" ? "() => void" : pr.values.map((v) => `"${v}"`).join(" | "),
|
|
9835
10266
|
required: false,
|
|
9836
10267
|
// A callback's `default` field carries detection EVIDENCE for the
|
|
9837
|
-
// prop line, not a value — it never enters the pin.
|
|
9838
|
-
|
|
10268
|
+
// prop line, not a value — it never enters the pin. An
|
|
10269
|
+
// error-role prop's public default is UNDEFINED (presence
|
|
10270
|
+
// semantics) — the recorded message is prose, never a default.
|
|
10271
|
+
...pr.default !== void 0 && pr.kind !== "callback" && pr.role !== "error" ? { default: pr.default } : {},
|
|
10272
|
+
...pr.role !== void 0 ? { role: pr.role } : {}
|
|
9839
10273
|
})),
|
|
9840
10274
|
forcedStates,
|
|
9841
|
-
poseCompleteness: { recordedPoses: opts.poses.length, expressible: configs.length }
|
|
10275
|
+
poseCompleteness: { recordedPoses: opts.poses.length, expressible: configs.length },
|
|
10276
|
+
archetype: archetype.archetype,
|
|
10277
|
+
contract: { extendsNative, forwardRef: isControl, restPropsTo: isControl ? "control" : "root" }
|
|
9842
10278
|
};
|
|
9843
10279
|
const propLines = props.map(
|
|
9844
|
-
(pr) => pr.kind === "boolean" ? ` ${pr.name}?: boolean;` : pr.kind === "string" ? ` ${pr.name}?: string; // CONTENT \u2014 recorded default ${JSON.stringify(pr.default)}` : pr.kind === "callback" ? ` ${pr.name}?: () => void; // NOTIFICATION \u2014 recorded evidence: ${pr.default ?? "the recorded affordance"}; the component owns its state (it hides/updates itself) and the callback informs, never controls` : ` ${pr.name}?: ${pr.values.map((v) => `"${v}"`).join(" | ")}; // default "${pr.default}"`
|
|
10280
|
+
(pr) => pr.kind === "boolean" ? ` ${pr.name}?: boolean;` : pr.role === "error" ? ` ${pr.name}?: string; // ERROR-AS-PRESENCE \u2014 default undefined (no error); when SET the component enters its error state: render the message, set aria-invalid on the control and aria-describedby pointing at the message element (machine-checked). Recorded message: ${JSON.stringify(pr.default)}` : pr.kind === "string" ? ` ${pr.name}?: string; // CONTENT \u2014 recorded default ${JSON.stringify(pr.default)}` : pr.kind === "callback" ? ` ${pr.name}?: () => void; // NOTIFICATION \u2014 recorded evidence: ${pr.default ?? "the recorded affordance"}; the component owns its state (it hides/updates itself) and the callback informs, never controls` : ` ${pr.name}?: ${pr.values.map((v) => `"${v}"`).join(" | ")}; // default "${pr.default}"`
|
|
9845
10281
|
);
|
|
9846
10282
|
const provided = opts.fonts ?? [];
|
|
9847
10283
|
const recorded = opts.recordedFonts ?? [];
|
|
@@ -9853,23 +10289,43 @@ function authorComponentApi(opts) {
|
|
|
9853
10289
|
}).filter((f) => f.missing.length > 0);
|
|
9854
10290
|
const weightsLine = (opts.providedFaces ?? []).length > 0 ? `Weights the harness can actually serve: ${(opts.providedFaces ?? []).map((f) => `'${f.family}' (${[...f.weights].sort((a, b) => a - b).join(", ")})`).join("; ")}. ` + (faceGaps.length > 0 ? `WEIGHT GAP \u2014 ${faceGaps.map((g) => `'${g.family}' is recorded at ${g.missing.join(", ")} which the cache does NOT hold`).join("; ")}. CSS binds the FAMILY before the weight and font-synthesis: none blocks the fall-through, so text at a missing weight renders in a weight this family DOES have, silently and at full score-cost. Do not lead a font stack with a family that cannot serve the weight you are setting; put a family that has it first, or report the gap rather than chasing the difference as geometry. ` : "") : "";
|
|
9855
10291
|
const fontsLine = weightsLine + (provided.length > 0 ? `Fonts provided by the harness: ${provided.map((f) => `'${f}'`).join(", ")}. ` : "") + (recorded.length > 0 ? `The recording's declared famil${recorded.length === 1 ? "y is" : "ies are"} ${recorded.map((f) => `'${f}'`).join(", ")}${unprovided.length > 0 ? ` \u2014 ${unprovided.map((f) => `'${f}'`).join(", ")} ${unprovided.length === 1 ? "is" : "are"} NOT provided: the mount will substitute a provided face, scoring reflects the recorded one, and the residual glyph delta is not closable from CSS \u2014 do not chase it` : ""}. ` : "");
|
|
9856
|
-
const forcingCanon = forcedStates.length > 0 ? `Interactive states are REAL (:hover, :focus-visible) AND statically forceable via the data-tendril-state attribute (a DOCUMENTED token-list attribute; tokens: ${forcedStates.map((t) => `"${t}"`).join(" | ")}) spread onto the root; forced and real selectors must share ONE declaration block, e.g. :is(:hover, [data-tendril-state~="hover"]) \u2014 match with ~= so compound states ("hover selected") work. ` : ""
|
|
9857
|
-
|
|
9858
|
-
|
|
10292
|
+
const forcingCanon = forcedStates.length > 0 ? `Interactive states are REAL (:hover, :focus-visible) AND statically forceable via the data-tendril-state attribute (a DOCUMENTED token-list attribute; tokens: ${forcedStates.map((t) => `"${t}"`).join(" | ")}) ${isControl ? "which arrives through the rest props and therefore lands on the NATIVE CONTROL \u2014 pair each forced token with its real condition on the control, and use :has() on ancestors where the recorded treatment paints outside it (the pinned Chromium supports :has())" : "spread onto the root"}; forced and real selectors must share ONE declaration block, e.g. :is(:hover, [data-tendril-state~="hover"]) \u2014 match with ~= so compound states ("hover selected") work. ` + // F12 (run-22 cycle): a kit that records "Focus" forces via a
|
|
10293
|
+
// "focus" token, but the prelude binds indicators to
|
|
10294
|
+
// :focus-visible, never bare :focus — left unsaid, the shared-
|
|
10295
|
+
// declaration example above points the generator straight at
|
|
10296
|
+
// the selector the prelude forbids.
|
|
10297
|
+
(forcedStates.includes("focus") ? `The recorded "focus" pose is KEYBOARD-focus treatment: pair its forced token with :focus-visible \u2014 :is(:focus-visible, [data-tendril-state~="focus"]) \u2014 never bare :focus (the prelude binds focus indicators to :focus-visible). ` : "") + // Cycle D (Output doctrine): derived states ride the SAME hook
|
|
10298
|
+
// but their real-use condition is platform state, not a prop —
|
|
10299
|
+
// the brief must name each pairing or the generator reinvents
|
|
10300
|
+
// the pose switchboard to satisfy the configs.
|
|
10301
|
+
(derivedStates.length > 0 ? `DERIVED STATES \u2014 ${derivedStates.map((t) => `"${t}"`).join(", ")} are states the PLATFORM owns; they are NEVER public props (no state/pose props in your API). In real use: focused comes from real focus (:focus-visible on the control or :focus-within on the root); filled/typing come from the control's VALUE (:not(:placeholder-shown), or state your component derives from its own value); placeholder is the empty-value look; error comes from your error/invalid semantics (aria-invalid). The forced token is the harness's STATIC channel for those poses \u2014 share each declaration between the real condition and its token, exactly like the interaction states above. ` : "") : "";
|
|
10302
|
+
const refElement = archetype.archetype === "text-input" ? "HTMLInputElement" : archetype.archetype === "selectable-control" ? "HTMLInputElement" : "HTMLButtonElement";
|
|
10303
|
+
const refElementNote = archetype.archetype === "text-input" ? " /* HTMLTextAreaElement if you build with a textarea */" : archetype.archetype === "selectable-control" ? " /* or HTMLButtonElement/HTMLSelectElement \u2014 match the control you pick */" : " /* HTMLAnchorElement for link shapes */";
|
|
10304
|
+
const signatureBlock = isControl ? `export const ${componentIdent} = forwardRef<${refElement}${refElementNote}, ${componentIdent}Props>(function ${componentIdent}(props, ref) { /* your implementation */ })
|
|
10305
|
+
interface ${componentIdent}Props extends ComponentPropsWithoutRef<"${extendsNative}"${archetype.archetype === "text-input" ? ' /* or "textarea" \u2014 whichever native control you honestly build with */' : archetype.archetype === "selectable-control" ? ' /* or "button"/"select" \u2014 whichever native control you honestly build with; the contract binds to the one you pick */' : ' /* or "a" for link shapes */'}> {
|
|
9859
10306
|
${propLines.join("\n")}
|
|
9860
|
-
|
|
9861
|
-
})
|
|
10307
|
+
}
|
|
10308
|
+
CONTRACT (the expert surface, part of the prescription): NO index-signature escape hatch ([key: string]: unknown is FORBIDDEN) \u2014 the native extension IS the rest-prop surface. Every prop not declared above (data-*, aria-*, native event handlers) forwards to the native control element, and the forwarded ref reaches that SAME element (import forwardRef and the ComponentPropsWithoutRef type from react; React-19 ref-as-prop is equally valid \u2014 the check binds ref.current to the control element, not to the mechanism). States the platform owns are DERIVED, never caller-set: no state/pose props beyond the interface above.` : `export function ${componentIdent}(props: ${componentIdent}Props)
|
|
10309
|
+
interface ${componentIdent}Props extends ComponentPropsWithoutRef<"div"> {
|
|
10310
|
+
${propLines.join("\n")}
|
|
10311
|
+
}
|
|
10312
|
+
CONTRACT: no index-signature escape hatch \u2014 the native extension is the rest-prop surface; undeclared props (incl. data-*) spread onto the root element.`;
|
|
10313
|
+
const systemApi = `Prescribed API (the harness mounts exactly this; deviation scores 0):
|
|
10314
|
+
${signatureBlock}
|
|
9862
10315
|
|
|
9863
10316
|
${syntheticCombos.length > 0 ? `UNRECORDED REACHABLE COMBINATIONS: the design's exclusive axis cannot express ${syntheticCombos.join(", ")} \u2014 the API split makes them reachable with NO recorded truth. Compose them from the recorded per-axis truth (paint variables: one axis sets, the other consumes), never invent a bespoke look, and list them in your report.
|
|
9864
|
-
` : ""}${slots.length > 0 ? `CONTENT PROPS: every string prop defaults to its RECORDED text \u2014 render the PROP, never a hardcoded literal. Configs pass per-pose recorded strings wherever the recording varies, and pixels enforce them: a hardcoded string fails those configs. Content presence (a heading that only exists in some poses) follows the AXIS props; the string prop only supplies the text. RICH-TEXT DEFAULTS: when the recorded content is a multi-run rich node (mixed weights, an underlined link span) whose concatenation is the prop's default string, a plain-string default would flatten the recorded formatting \u2014 default the parameter to undefined and render the recorded rich markup when the prop is absent; a caller-supplied string then renders plainly. That mirrors the design tool's own emission logic and keeps every recorded pose pixel-exact.
|
|
10317
|
+
` : ""}${slots.length > 0 ? `CONTENT PROPS: every string prop defaults to its RECORDED text \u2014 render the PROP, never a hardcoded literal. Configs pass per-pose recorded strings wherever the recording varies, and pixels enforce them: a hardcoded string fails those configs. Content presence (a heading that only exists in some poses) follows the AXIS props; the string prop only supplies the text. RICH-TEXT DEFAULTS: when the recorded content is a multi-run rich node (mixed weights, an underlined link span) whose concatenation is the prop's default string, a plain-string default would flatten the recorded formatting \u2014 default the parameter to undefined and render the recorded rich markup when the prop is absent; a caller-supplied string then renders plainly. That mirrors the design tool's own emission logic and keeps every recorded pose pixel-exact.${slots.some((s) => s.richIn.length > 0) ? ` RICH POSES ARE NEVER PROP-FED: poses whose recorded slot content is itself a multi-run rich node do NOT receive the string prop from the harness (a plain string cannot reproduce the recorded formatting) \u2014 when the prop is absent, render THAT pose's recorded rich markup, keyed off the pose's own axis props; a caller-supplied string still renders plainly.` : ""}
|
|
10318
|
+
` : ""}${valueViaProp ? `VALUE POSES RIDE THE NATIVE SURFACE: the recorded value text for filled/typing poses mounts through defaultValue on your control (the rest props carry it \u2014 never intercept it into a custom prop). Derive value-state styling from the control's own value (:not(:placeholder-shown), or state you track from the control's change events) \u2014 never from a pose prop and never by branching on the forcing attribute.
|
|
9865
10319
|
` : ""}Rules: generated element ids come from React's useId() \u2014 never Math.random() in render/state init (SSR hydration hazard; measured: two same-day bundles disagreed). Plain CSS in styles.css (tokens.css optional, loaded first) \u2014 no Tailwind, no imports beyond react/react-dom, and NEVER import the stylesheet from the entry module: the harness injects tokens.css and styles.css itself, and an entry that imports CSS does not compile here (measured cost: one full round, every config 0). The component renders the RECORDED content as its defaults \u2014 reproduce it from the emissions. ${forcingCanon}${fontsLine}ROOT SIZE IS YOURS AND PIXELS DO NOT CHECK IT, IN EITHER DIRECTION: the mount floors your root to the recorded box (#root > *{min-width:<recorded w>px;min-height:<recorded h>px} \u2014 a floor, never a cap), and the image the scorer compares is a CROP of that box, so paint outside it is never compared at all. Measured through the real scorer against a correct 120x40 root: an UNDERSIZED 80x28 root and OVERSIZED 160x52 and 124x44 roots ALL scored sim 1.000 / ink 1.000 with a byte-identical crop, as long as internals stay literal and top-left anchored. An oversized root shows only when its own border, radius or shadow \u2014 or a re-centred internal layout \u2014 lands back inside the crop. And some roots are not floored at all: a non-replaced INLINE root (a bare <label>, or an <a href> for a link variant) ignores width/height/min-width/min-height entirely and renders content-sized whatever you declare. So take both root dimensions from the recorded box in the payload, and give the root a display that honours them \u2014 no score, passing or failing, is evidence they are right. FLUIDITY AFFORDANCE (emit it always): every ROOT width declaration rides width: var(--tendril-root-width, <recorded>px) with that pose's recorded width as the fallback \u2014 per-variant root widths reuse the SAME property name with their own recorded fallbacks. With the property unset this is byte-equivalent truth: identical pixels, identical operability measurement (measured: flipping roots to a bare 100% held 20/20 pixels but made a commit check unmeasurable \u2014 the recorded pin carries real signal). A consumer makes an instance fluid by setting --tendril-root-width (e.g. 100%) on a wrapper, no edit to certified CSS. Widths only: recorded heights and internal geometry stay literal \u2014 fixed heights are often genuine design intent.`;
|
|
9866
|
-
const
|
|
10320
|
+
const CHECKED_STATE_TOKENS = /* @__PURE__ */ new Set(["hover", "focus-visible"]);
|
|
10321
|
+
const exercisedBooleans = props.filter((p) => p.kind === "boolean" && configs.some((c) => c.props[p.name] === true)).map((p) => p.name);
|
|
10322
|
+
const checkedTokens = /* @__PURE__ */ new Set([...forcedStates.filter((t) => CHECKED_STATE_TOKENS.has(t)), ...exercisedBooleans]);
|
|
9867
10323
|
const unmappedInteractionEvidence = interactionEvidence.filter((e) => {
|
|
9868
10324
|
const sel = e.endsWith(" (selection axis)");
|
|
9869
10325
|
const key = sel ? kebab3(e.slice(0, -" (selection axis)".length)) : kebab3(e.split("=").slice(1).join("="));
|
|
9870
|
-
return !
|
|
10326
|
+
return !checkedTokens.has(key);
|
|
9871
10327
|
});
|
|
9872
|
-
return { component: componentIdent, entry, props, forcedStates, interactionEvidence, unmappedInteractionEvidence, syntheticCombos, configs, apiPin, systemApi };
|
|
10328
|
+
return { component: componentIdent, entry, props, forcedStates, interactionEvidence, unmappedInteractionEvidence, satisfiabilityDemoted: [], syntheticCombos, textEntry, archetype, derivedPoseProps, derivedStateTokens: derivedStates, configs, apiPin, systemApi };
|
|
9873
10329
|
}
|
|
9874
10330
|
function authorBehaviors(api, extras = {}) {
|
|
9875
10331
|
const behaviors = [];
|
|
@@ -9878,43 +10334,141 @@ function authorBehaviors(api, extras = {}) {
|
|
|
9878
10334
|
if (anchor === void 0) return { behaviors, prelude: { controls: [], textInputs: [] }, disclosures };
|
|
9879
10335
|
const selects = api.props.find((p) => p.kind === "boolean" && /^(selected|checked|isselected|ischecked)$/i.test(p.name));
|
|
9880
10336
|
const committed = selects === void 0 ? void 0 : api.configs.find((c) => c.props[selects.name] === true && Object.keys(c.props).length === 1);
|
|
10337
|
+
const NATIVE_CONTROL = api.archetype.nativeControl !== "" ? api.archetype.nativeControl : ":is(input, button, select)";
|
|
10338
|
+
const recordedGapBelowFloor = (repA, repB) => {
|
|
10339
|
+
if (repB === void 0 || repB === repA) return void 0;
|
|
10340
|
+
const gap = extras.poseGap?.(repA, repB);
|
|
10341
|
+
return gap !== void 0 && gap < POSE_GAP_FLOOR ? gap : void 0;
|
|
10342
|
+
};
|
|
10343
|
+
const recordedIdentical = (repA, repB) => repB !== void 0 && repB !== repA && extras.posesIdentical?.(repA, repB) === true;
|
|
10344
|
+
const forcedPoseRep = (token) => api.configs.find((c) => {
|
|
10345
|
+
const keys = Object.keys(c.props);
|
|
10346
|
+
return keys.length === 1 && c.props["data-tendril-state"] === token;
|
|
10347
|
+
})?.rep;
|
|
10348
|
+
const demoteEvidence = (matches) => {
|
|
10349
|
+
for (const e of api.interactionEvidence) {
|
|
10350
|
+
if (!matches(e)) continue;
|
|
10351
|
+
if (!api.unmappedInteractionEvidence.includes(e)) api.unmappedInteractionEvidence.push(e);
|
|
10352
|
+
if (!api.satisfiabilityDemoted.includes(e)) api.satisfiabilityDemoted.push(e);
|
|
10353
|
+
}
|
|
10354
|
+
};
|
|
10355
|
+
const demoteTokenToPixelOnly = (token) => {
|
|
10356
|
+
demoteEvidence((e) => kebab3(e.split("=").slice(1).join("=")) === token);
|
|
10357
|
+
};
|
|
9881
10358
|
if (api.forcedStates.includes("hover")) {
|
|
9882
|
-
|
|
10359
|
+
if (!recordedIdentical(anchor.rep, forcedPoseRep("hover"))) {
|
|
10360
|
+
behaviors.push({ id: "real-hover-responds", config: anchor.rep, steps: [{ hoverChangesPixels: "> *" }] });
|
|
10361
|
+
} else {
|
|
10362
|
+
demoteTokenToPixelOnly("hover");
|
|
10363
|
+
disclosures.push(
|
|
10364
|
+
`real-hover-responds NOT authored: the recorded hover pose is PIXEL-IDENTICAL to rest \u2014 the recording paints no hover difference at all, and demanding a real-hover pixel change would require invented ink; hover stays forceable and pixel-verified only. If hover should respond, the design needs a distinct hover treatment; re-record and re-author`
|
|
10365
|
+
);
|
|
10366
|
+
}
|
|
9883
10367
|
}
|
|
9884
10368
|
if (api.forcedStates.includes("focus-visible")) {
|
|
9885
|
-
|
|
9886
|
-
|
|
10369
|
+
if (!recordedIdentical(anchor.rep, forcedPoseRep("focus-visible"))) {
|
|
10370
|
+
behaviors.push({ id: "focus-ring-visible", config: anchor.rep, steps: [{ tabFocusChangesPixels: true }] });
|
|
10371
|
+
} else {
|
|
10372
|
+
demoteTokenToPixelOnly("focus-visible");
|
|
10373
|
+
disclosures.push(
|
|
10374
|
+
`focus-ring-visible NOT authored: the recorded focus pose is PIXEL-IDENTICAL to rest \u2014 the recording paints no focus difference at all, and an invented ring is unrecorded pixels (the prelude keeps the browser's default indicator instead). If keyboard focus should be visible, the design needs a distinct focus treatment; re-record and re-author`
|
|
10375
|
+
);
|
|
10376
|
+
}
|
|
10377
|
+
if (selects === void 0) {
|
|
10378
|
+
behaviors.push({ id: "no-ring-after-mouse-click", config: anchor.rep, steps: [{ mouseClickNoPersistentRing: "> *" }] });
|
|
10379
|
+
} else if (committed !== void 0) {
|
|
10380
|
+
behaviors.push({ id: "no-ring-after-mouse-click", config: committed.rep, steps: [{ mouseClickNoPersistentRing: "> *" }] });
|
|
10381
|
+
} else {
|
|
10382
|
+
disclosures.push(
|
|
10383
|
+
`no-ring-after-mouse-click NOT authored: this selection-bearing component records no already-committed pose to aim the check at, and clicking any other pose legitimately commits a repaint \u2014 the check would fail honest components and pass only statues (it must target a pose where clicking commits nothing)`
|
|
10384
|
+
);
|
|
10385
|
+
}
|
|
9887
10386
|
}
|
|
9888
10387
|
const disabledCfg = api.configs.find((c) => c.props["disabled"] === true);
|
|
9889
10388
|
if (disabledCfg !== void 0) {
|
|
9890
|
-
behaviors.push({ id: "disabled-not-focusable", config: disabledCfg.rep, steps: [{ assertNotFocusable: selects === void 0 ? "> *" :
|
|
10389
|
+
behaviors.push({ id: "disabled-not-focusable", config: disabledCfg.rep, steps: [{ assertNotFocusable: selects === void 0 ? "> *" : NATIVE_CONTROL }] });
|
|
9891
10390
|
}
|
|
9892
10391
|
const loadingCfg = api.configs.find((c) => c.props["loading"] === true);
|
|
9893
10392
|
if (loadingCfg !== void 0) {
|
|
9894
|
-
|
|
10393
|
+
demoteTokenToPixelOnly("loading");
|
|
10394
|
+
disclosures.push(
|
|
10395
|
+
`spinner-animates NOT authored: the recorded loading pose is a still, and recorded motion truth (prototype transitions) cannot prove an in-pose loop \u2014 demanding a running spinner would invent motion the recording never states; the loading pose stays pixel-verified only (motion ships PRESCRIBED, NOT VERIFIED per ADR-016)`
|
|
10396
|
+
);
|
|
9895
10397
|
behaviors.push({ id: "reduced-motion-respected", config: loadingCfg.rep, steps: [{ reducedMotionNoAnimation: true }] });
|
|
9896
10398
|
}
|
|
9897
10399
|
const selectionProp = selects;
|
|
9898
10400
|
const committedPose = committed;
|
|
9899
10401
|
if (selectionProp !== void 0) {
|
|
9900
|
-
behaviors.push({ id: "native-control-present", config: anchor.rep, steps: [{
|
|
10402
|
+
behaviors.push({ id: "native-control-present", config: anchor.rep, steps: [{ assertFocusable: NATIVE_CONTROL }] });
|
|
9901
10403
|
behaviors.push({ id: "control-has-pointer-cursor", config: anchor.rep, steps: [{ assertCursor: ["> *", "pointer"] }] });
|
|
9902
10404
|
if (committedPose !== void 0) {
|
|
9903
|
-
|
|
9904
|
-
|
|
9905
|
-
|
|
9906
|
-
|
|
9907
|
-
|
|
10405
|
+
const flat = recordedGapBelowFloor(anchor.rep, committedPose.rep);
|
|
10406
|
+
if (flat === void 0) {
|
|
10407
|
+
behaviors.push({
|
|
10408
|
+
id: "activation-commits-recorded-state",
|
|
10409
|
+
config: anchor.rep,
|
|
10410
|
+
steps: [{ commitMatchesPose: { activate: NATIVE_CONTROL, pose: committedPose.rep } }]
|
|
10411
|
+
});
|
|
10412
|
+
} else {
|
|
10413
|
+
demoteEvidence((e) => e.endsWith(" (selection axis)") || kebab3(e.split("=").slice(1).join("=")) === kebab3(selectionProp.name));
|
|
10414
|
+
disclosures.push(
|
|
10415
|
+
`activation-commits-recorded-state NOT authored: the recorded committed pose ("${committedPose.rep}") is pixel-indistinguishable from rest in the recording itself (gap ${flat.toFixed(4)}, floor ${POSE_GAP_FLOOR}) \u2014 committing can never be observed through pixels, on any round, from any component code; selection is presence-checked only. If selection should be visible, the design needs a distinct selected treatment; re-record and re-author`
|
|
10416
|
+
);
|
|
10417
|
+
}
|
|
9908
10418
|
}
|
|
9909
10419
|
}
|
|
9910
10420
|
if (api.props.some((p) => p.kind === "callback" && p.name === "onDismiss")) {
|
|
9911
10421
|
behaviors.push({
|
|
9912
10422
|
id: "dismiss-notifies-and-commits",
|
|
9913
|
-
|
|
10423
|
+
// The pose whose recording SHOWS the affordance, when layer
|
|
10424
|
+
// evidence named one — the anchor may not even contain the
|
|
10425
|
+
// layer (run 22's over-constraint).
|
|
10426
|
+
config: extras.dismissTargetRep ?? anchor.rep,
|
|
9914
10427
|
spyProps: ["onDismiss"],
|
|
9915
10428
|
steps: [{ assertVisible: "button" }, { assertFocusable: "button" }, { dismissNotifies: { activate: "button", prop: "onDismiss" } }]
|
|
9916
10429
|
});
|
|
9917
10430
|
}
|
|
10431
|
+
if (api.archetype.archetype !== "static") {
|
|
10432
|
+
const control = api.archetype.nativeControl;
|
|
10433
|
+
behaviors.push({ id: "ref-reaches-control", config: anchor.rep, steps: [{ assertRefReachesControl: control }] });
|
|
10434
|
+
behaviors.push({
|
|
10435
|
+
id: "rest-props-reach-control",
|
|
10436
|
+
config: anchor.rep,
|
|
10437
|
+
// Minted per run by the checker — a fixed value is hardcodable
|
|
10438
|
+
// with zero collateral (review).
|
|
10439
|
+
props: { "data-tendril-probe": "__TENDRIL_MINT__" },
|
|
10440
|
+
steps: [{ assertAttr: [control, "data-tendril-probe", "__TENDRIL_MINT__"] }]
|
|
10441
|
+
});
|
|
10442
|
+
}
|
|
10443
|
+
const errorProp = api.props.find((p) => p.role === "error");
|
|
10444
|
+
if (errorProp !== void 0 && api.archetype.archetype !== "static") {
|
|
10445
|
+
const errCfg = api.configs.find((c) => typeof c.props[errorProp.name] === "string");
|
|
10446
|
+
if (errCfg !== void 0) {
|
|
10447
|
+
behaviors.push({
|
|
10448
|
+
id: "error-pose-wires-aria",
|
|
10449
|
+
config: errCfg.rep,
|
|
10450
|
+
steps: [
|
|
10451
|
+
{ assertAttr: [api.archetype.nativeControl, "aria-invalid", "true"] },
|
|
10452
|
+
// The idref must RESOLVE to an element carrying the message
|
|
10453
|
+
// (review: a dangling aria-describedby is present-not-wired,
|
|
10454
|
+
// the exact distinction the doctrine draws).
|
|
10455
|
+
{ assertDescribedByResolves: [api.archetype.nativeControl, String(errCfg.props[errorProp.name])] }
|
|
10456
|
+
]
|
|
10457
|
+
});
|
|
10458
|
+
}
|
|
10459
|
+
}
|
|
10460
|
+
if (api.archetype.archetype === "text-input") {
|
|
10461
|
+
const control = api.archetype.nativeControl;
|
|
10462
|
+
behaviors.push({ id: "value-entry-works", config: anchor.rep, props: { defaultValue: "" }, steps: [{ typeChangesPixels: [control, "ab"] }, { assertValue: [control, "ab"] }] });
|
|
10463
|
+
behaviors.push({ id: "default-value-reaches-control", config: anchor.rep, props: { defaultValue: "__TENDRIL_MINT__" }, steps: [{ assertValue: [control, "__TENDRIL_MINT__"] }] });
|
|
10464
|
+
behaviors.push({
|
|
10465
|
+
id: "caller-onchange-fires",
|
|
10466
|
+
config: anchor.rep,
|
|
10467
|
+
props: { defaultValue: "" },
|
|
10468
|
+
spyProps: ["onChange"],
|
|
10469
|
+
steps: [{ type: [control, "a"] }, { assertSpyFired: "onChange" }]
|
|
10470
|
+
});
|
|
10471
|
+
}
|
|
9918
10472
|
for (const sentinel of extras.sentinels ?? []) {
|
|
9919
10473
|
const marker = `TENDRIL SENTINEL ${sentinel.prop}`;
|
|
9920
10474
|
behaviors.push({
|
|
@@ -9932,31 +10486,114 @@ function authorBehaviors(api, extras = {}) {
|
|
|
9932
10486
|
}
|
|
9933
10487
|
if (api.unmappedInteractionEvidence.length > 0) {
|
|
9934
10488
|
disclosures.push(
|
|
9935
|
-
`interaction-looking poses PIXEL-VERIFIED ONLY (no operability check
|
|
10489
|
+
`interaction-looking poses PIXEL-VERIFIED ONLY (no operability check covers them): ${api.unmappedInteractionEvidence.join(", ")}`
|
|
9936
10490
|
);
|
|
9937
10491
|
}
|
|
9938
|
-
|
|
10492
|
+
const textEntry = api.textEntry.length > 0;
|
|
10493
|
+
if (textEntry) {
|
|
10494
|
+
disclosures.push(
|
|
10495
|
+
`text-entry archetype (recorded evidence: ${api.textEntry.join(", ")}) \u2014 the root is exempt from the prelude's user-select:none requirement (selection suppression inherits into the control the canon requires to stay selectable); the editable control is checked selectable, and the root keeps its touch-action check`
|
|
10496
|
+
);
|
|
10497
|
+
}
|
|
10498
|
+
return {
|
|
10499
|
+
behaviors,
|
|
10500
|
+
prelude: {
|
|
10501
|
+
controls: interactive && !textEntry ? ["> *"] : [],
|
|
10502
|
+
textInputs: textEntry ? [":is(input, textarea)"] : [],
|
|
10503
|
+
...interactive && textEntry ? { touchTargets: ["> *"] } : {}
|
|
10504
|
+
},
|
|
10505
|
+
disclosures
|
|
10506
|
+
};
|
|
9939
10507
|
}
|
|
9940
10508
|
function envelopeText(file) {
|
|
9941
|
-
return envelopeFirstTextPart(JSON.parse(
|
|
10509
|
+
return envelopeFirstTextPart(JSON.parse(readFileSync21(file, "utf8")));
|
|
9942
10510
|
}
|
|
9943
10511
|
function metadataText(file) {
|
|
9944
|
-
return envelopeTextContent(JSON.parse(
|
|
10512
|
+
return envelopeTextContent(JSON.parse(readFileSync21(file, "utf8")));
|
|
9945
10513
|
}
|
|
9946
10514
|
function dismissEvidence(setDir, repSlugs) {
|
|
9947
10515
|
for (const slug of repSlugs) {
|
|
9948
|
-
const f =
|
|
9949
|
-
if (!
|
|
10516
|
+
const f = path31.join(setDir, slug, "get_design_context.json");
|
|
10517
|
+
if (!existsSync25(f)) continue;
|
|
9950
10518
|
const text = envelopeText(f);
|
|
9951
|
-
const propHit = /[{,]\s*(\w*dismiss\w*)\s*=\s*
|
|
9952
|
-
if (propHit !== null) return `emission prop "${propHit[1]}"
|
|
9953
|
-
|
|
9954
|
-
|
|
9955
|
-
|
|
10519
|
+
const propHit = /[{,]\s*(\w*dismiss\w*)\s*=\s*true\b/i.exec(text);
|
|
10520
|
+
if (propHit !== null) return { evidence: `emission prop "${propHit[1]}"`, visibleReps: [] };
|
|
10521
|
+
const typedHit = /\b(\w*dismiss\w*)\??\s*:\s*boolean\b/i.exec(text);
|
|
10522
|
+
if (typedHit !== null) {
|
|
10523
|
+
return {
|
|
10524
|
+
visibleReps: [],
|
|
10525
|
+
demoted: `the kit's API declares a dismiss capability (typed member "${typedHit[1]}") but no recorded pose shows a default-on dismiss affordance \u2014 no onDismiss prop and no dismiss check were authored. If it should ship, record a pose with the affordance enabled and visible.`
|
|
10526
|
+
};
|
|
10527
|
+
}
|
|
10528
|
+
for (const m of text.matchAll(/<[^>]*?data-name="([^"]+)"[^>]*?>/g)) {
|
|
10529
|
+
const norm2 = normalizedLayerName(m[1]);
|
|
10530
|
+
if (!DISMISS_NAMES.has(norm2)) continue;
|
|
10531
|
+
const rawId = /data-node-id="([^"]+)"/.exec(m[0])?.[1];
|
|
10532
|
+
const head = rawId === void 0 ? void 0 : rawId.startsWith("I") ? rawId.slice(1).split(";")[0] : rawId;
|
|
10533
|
+
const byId = head === void 0 ? void 0 : poseVisibility(setDir, repSlugs, { nodeId: head });
|
|
10534
|
+
const vis = byId !== void 0 && byId.presentIn.length > 0 ? byId : poseVisibility(setDir, repSlugs, { name: m[1] });
|
|
10535
|
+
if (invisibleInEveryPose(vis)) {
|
|
10536
|
+
return {
|
|
10537
|
+
visibleReps: [],
|
|
10538
|
+
demoted: `a dismiss affordance (layer ${JSON.stringify(m[1])}) exists in the design but is HIDDEN in every recorded pose \u2014 nothing verified stands behind it, so no onDismiss prop and no dismiss check were authored. If it should ship, add a variant that SHOWS it and re-record.`
|
|
10539
|
+
};
|
|
10540
|
+
}
|
|
10541
|
+
const glyphIsTheComponent = (() => {
|
|
10542
|
+
const slugToCheck = vis.visibleIn[0];
|
|
10543
|
+
if (slugToCheck === void 0) return false;
|
|
10544
|
+
const metaFile = path31.join(setDir, slugToCheck, "get_metadata.json");
|
|
10545
|
+
try {
|
|
10546
|
+
const root = parseMetadataStructure(envelopeTextContent(JSON.parse(readFileSync21(metaFile, "utf8"))));
|
|
10547
|
+
if (root.children.length !== 1) return false;
|
|
10548
|
+
const contains = (n) => normalizedLayerName(n.name) === norm2 || n.children.some(contains);
|
|
10549
|
+
return contains(root.children[0]);
|
|
10550
|
+
} catch {
|
|
10551
|
+
return false;
|
|
10552
|
+
}
|
|
10553
|
+
})();
|
|
10554
|
+
if (glyphIsTheComponent) {
|
|
10555
|
+
return {
|
|
10556
|
+
visibleReps: [],
|
|
10557
|
+
demoted: `an x-shaped layer (${JSON.stringify(m[1])}) is the component's SOLE content \u2014 it reads as the component's own glyph (an icon button), not a dismiss affordance; no onDismiss was authored (the sweep's icon-button finding: a plain Control was ordered to "dismiss itself").`
|
|
10558
|
+
};
|
|
10559
|
+
}
|
|
10560
|
+
return { evidence: `layer ${JSON.stringify(m[1])}`, visibleReps: vis.visibleIn };
|
|
9956
10561
|
}
|
|
9957
10562
|
}
|
|
9958
10563
|
return void 0;
|
|
9959
10564
|
}
|
|
10565
|
+
function recordedReferencePng(setDir, slug) {
|
|
10566
|
+
const f = path31.join(setDir, slug, "get_screenshot.json");
|
|
10567
|
+
if (!existsSync25(f)) return void 0;
|
|
10568
|
+
try {
|
|
10569
|
+
const env = JSON.parse(readFileSync21(f, "utf8")).content.find((c) => c.type === "image");
|
|
10570
|
+
return env?.data === void 0 ? void 0 : Uint8Array.from(Buffer.from(env.data, "base64"));
|
|
10571
|
+
} catch {
|
|
10572
|
+
return void 0;
|
|
10573
|
+
}
|
|
10574
|
+
}
|
|
10575
|
+
function recordedPoseGap(setDir, repA, repB) {
|
|
10576
|
+
const a = recordedReferencePng(setDir, repA);
|
|
10577
|
+
const b = recordedReferencePng(setDir, repB);
|
|
10578
|
+
if (a === void 0 || b === void 0) return void 0;
|
|
10579
|
+
try {
|
|
10580
|
+
return 1 - compareRenderToReference(a, b).similarity;
|
|
10581
|
+
} catch {
|
|
10582
|
+
return void 0;
|
|
10583
|
+
}
|
|
10584
|
+
}
|
|
10585
|
+
function recordedPosesIdentical(setDir, repA, repB) {
|
|
10586
|
+
const a = recordedReferencePng(setDir, repA);
|
|
10587
|
+
const b = recordedReferencePng(setDir, repB);
|
|
10588
|
+
if (a === void 0 || b === void 0) return void 0;
|
|
10589
|
+
try {
|
|
10590
|
+
const pa = PNG3.sync.read(Buffer.from(a));
|
|
10591
|
+
const pb = PNG3.sync.read(Buffer.from(b));
|
|
10592
|
+
return pa.width === pb.width && pa.height === pb.height && Buffer.compare(pa.data, pb.data) === 0;
|
|
10593
|
+
} catch {
|
|
10594
|
+
return void 0;
|
|
10595
|
+
}
|
|
10596
|
+
}
|
|
9960
10597
|
function recordedFontNeeds(setDir, opts = {}) {
|
|
9961
10598
|
const byFamily = /* @__PURE__ */ new Map();
|
|
9962
10599
|
const unpaired = /* @__PURE__ */ new Set();
|
|
@@ -9990,13 +10627,13 @@ function recordedFontNeeds(setDir, opts = {}) {
|
|
|
9990
10627
|
}
|
|
9991
10628
|
};
|
|
9992
10629
|
const manifest = loadManifest(setDir);
|
|
9993
|
-
const setDefs =
|
|
9994
|
-
if (
|
|
10630
|
+
const setDefs = path31.join(setDir, "get_variable_defs.json");
|
|
10631
|
+
if (existsSync25(setDefs)) fromDefs(envelopeText(setDefs));
|
|
9995
10632
|
for (const rep of manifest.reps) {
|
|
9996
|
-
const ctx =
|
|
9997
|
-
if (
|
|
9998
|
-
const defs =
|
|
9999
|
-
if (
|
|
10633
|
+
const ctx = path31.join(setDir, rep.slug, "get_design_context.json");
|
|
10634
|
+
if (existsSync25(ctx)) fromEmission(envelopeText(ctx));
|
|
10635
|
+
const defs = path31.join(setDir, rep.slug, "get_variable_defs.json");
|
|
10636
|
+
if (existsSync25(defs)) fromDefs(envelopeText(defs));
|
|
10000
10637
|
}
|
|
10001
10638
|
return [...byFamily.entries()].map(([family, paired]) => {
|
|
10002
10639
|
const weights = /* @__PURE__ */ new Set([...paired, ...opts.pairedOnly === true ? [] : unpaired]);
|
|
@@ -10007,10 +10644,10 @@ function symbolFontGlyphCount(setDir, reps) {
|
|
|
10007
10644
|
const PUA = /[\uE000-\uF8FF\u{F0000}-\u{FFFFD}\u{100000}-\u{10FFFD}]/u;
|
|
10008
10645
|
const glyphs = /* @__PURE__ */ new Set();
|
|
10009
10646
|
for (const rep of reps) {
|
|
10010
|
-
const file =
|
|
10011
|
-
if (!
|
|
10647
|
+
const file = path31.join(setDir, rep, "get_metadata.json");
|
|
10648
|
+
if (!existsSync25(file)) continue;
|
|
10012
10649
|
try {
|
|
10013
|
-
const text = JSON.parse(
|
|
10650
|
+
const text = JSON.parse(readFileSync21(file, "utf8")).content.map((c) => c.text ?? "").join("\n");
|
|
10014
10651
|
for (const m of text.matchAll(/<text\s[^>]*name="([^"]*)"/g)) {
|
|
10015
10652
|
const name = decodeXmlEntities(m[1]).replace(/&#x([0-9a-fA-F]+);/g, (_, h) => String.fromCodePoint(parseInt(h, 16))).replace(/&#(\d+);/g, (_, d) => String.fromCodePoint(Number(d)));
|
|
10016
10653
|
if (PUA.test(name)) glyphs.add(name);
|
|
@@ -10036,8 +10673,8 @@ function recordedTextSlots(setDir, repSlugs) {
|
|
|
10036
10673
|
const propRep = [];
|
|
10037
10674
|
const perRep = [];
|
|
10038
10675
|
for (const slug of repSlugs) {
|
|
10039
|
-
const f =
|
|
10040
|
-
if (!
|
|
10676
|
+
const f = path31.join(setDir, slug, "get_design_context.json");
|
|
10677
|
+
if (!existsSync25(f)) continue;
|
|
10041
10678
|
const code = envelopeText(f);
|
|
10042
10679
|
const props = /* @__PURE__ */ new Map();
|
|
10043
10680
|
for (const m of code.matchAll(/[{,]\s*(\w+)\s*=\s*"((?:[^"\\]|\\.)*)"/g)) {
|
|
@@ -10061,15 +10698,21 @@ function recordedTextSlots(setDir, repSlugs) {
|
|
|
10061
10698
|
perRep.push({ slug, texts, code });
|
|
10062
10699
|
}
|
|
10063
10700
|
const axisValuesBySlug = /* @__PURE__ */ new Map();
|
|
10701
|
+
const valuesByAxis = /* @__PURE__ */ new Map();
|
|
10064
10702
|
for (const slug of repSlugs) {
|
|
10065
|
-
const metaFile =
|
|
10066
|
-
if (!
|
|
10703
|
+
const metaFile = path31.join(setDir, slug, "get_metadata.json");
|
|
10704
|
+
if (!existsSync25(metaFile)) continue;
|
|
10067
10705
|
const name = symbolName(metadataText(metaFile));
|
|
10068
10706
|
if (name === void 0) continue;
|
|
10069
10707
|
const values = /* @__PURE__ */ new Set();
|
|
10070
10708
|
for (const part of name.split(",")) {
|
|
10071
10709
|
const eq = part.indexOf("=");
|
|
10072
|
-
if (eq >= 0)
|
|
10710
|
+
if (eq >= 0) {
|
|
10711
|
+
const axis = kebab3(part.slice(0, eq).trim());
|
|
10712
|
+
const value = kebab3(part.slice(eq + 1).trim());
|
|
10713
|
+
(valuesByAxis.get(axis) ?? valuesByAxis.set(axis, /* @__PURE__ */ new Set()).get(axis)).add(value);
|
|
10714
|
+
values.add(value);
|
|
10715
|
+
}
|
|
10073
10716
|
}
|
|
10074
10717
|
axisValuesBySlug.set(slug, values);
|
|
10075
10718
|
}
|
|
@@ -10083,7 +10726,11 @@ function recordedTextSlots(setDir, repSlugs) {
|
|
|
10083
10726
|
}
|
|
10084
10727
|
return seen > 0;
|
|
10085
10728
|
};
|
|
10086
|
-
const
|
|
10729
|
+
const alnum = (s) => s.toLowerCase().replace(/[^a-z0-9]+/g, "");
|
|
10730
|
+
const isAxisNamed = (name) => [...valuesByAxis.entries()].some(
|
|
10731
|
+
([axis, values]) => alnum(axis) === alnum(name) && ![...values].every((v) => v === "true" || v === "false")
|
|
10732
|
+
);
|
|
10733
|
+
const propNames = [...new Set(propRep.flatMap((r) => [...r.props.keys()]))].filter((n) => !isAxisMirror(n) && !isAxisNamed(n));
|
|
10087
10734
|
if (propNames.length > 0) {
|
|
10088
10735
|
return propNames.map((name) => {
|
|
10089
10736
|
const counts = /* @__PURE__ */ new Map();
|
|
@@ -10092,18 +10739,24 @@ function recordedTextSlots(setDir, repSlugs) {
|
|
|
10092
10739
|
if (v !== void 0) counts.set(v, (counts.get(v) ?? 0) + 1);
|
|
10093
10740
|
}
|
|
10094
10741
|
const def = [...counts.entries()].sort((a, b) => b[1] - a[1])[0][0];
|
|
10742
|
+
const visibleIn = [];
|
|
10743
|
+
const richIn = [];
|
|
10744
|
+
for (const r of perRep) {
|
|
10745
|
+
const v = propRep.find((pr) => pr.slug === r.slug)?.props.get(name) ?? def;
|
|
10746
|
+
const norm2 = (s) => s.replace(/\s+/g, " ").trim();
|
|
10747
|
+
const singleRun = r.texts.some((t) => norm2(t).includes(norm2(v)));
|
|
10748
|
+
const assembled = !singleRun && norm2(r.texts.join(" ")).includes(norm2(v));
|
|
10749
|
+
if (singleRun || assembled) visibleIn.push(r.slug);
|
|
10750
|
+
if (assembled) richIn.push(r.slug);
|
|
10751
|
+
}
|
|
10095
10752
|
const overrides = {};
|
|
10096
10753
|
for (const r of propRep) {
|
|
10097
10754
|
const v = r.props.get(name);
|
|
10098
|
-
if (v !== void 0 && v !== def) overrides[r.slug] = v;
|
|
10755
|
+
if (v !== void 0 && v !== def && !richIn.includes(r.slug)) overrides[r.slug] = v;
|
|
10099
10756
|
}
|
|
10100
|
-
const visibleIn = perRep.filter((r) => {
|
|
10101
|
-
const v = propRep.find((pr) => pr.slug === r.slug)?.props.get(name) ?? def;
|
|
10102
|
-
return r.texts.some((t) => t.includes(v)) || r.texts.join(" ").replace(/\s+/g, " ").includes(v.replace(/\s+/g, " ").trim());
|
|
10103
|
-
}).map((r) => r.slug);
|
|
10104
10757
|
const refPattern = new RegExp(`\\{\\s*${name}\\s*\\}`);
|
|
10105
10758
|
const referencedIn = perRep.filter((r) => refPattern.test(r.code)).map((r) => r.slug);
|
|
10106
|
-
return { prop: name, default: def, overrides, varies: Object.keys(overrides).length > 0, visibleIn, referencedIn };
|
|
10759
|
+
return { prop: name, default: def, overrides, varies: Object.keys(overrides).length > 0, visibleIn, referencedIn, richIn };
|
|
10107
10760
|
});
|
|
10108
10761
|
}
|
|
10109
10762
|
if (perRep.length === 0) return [];
|
|
@@ -10150,7 +10803,7 @@ function recordedTextSlots(setDir, repSlugs) {
|
|
|
10150
10803
|
usedNames.add(prop);
|
|
10151
10804
|
const overrides = {};
|
|
10152
10805
|
for (const [slug, v] of sl.values) if (v !== def) overrides[slug] = v;
|
|
10153
|
-
return { prop, default: def, overrides, varies: Object.keys(overrides).length > 0, visibleIn: [...sl.values.keys()], referencedIn: [] };
|
|
10806
|
+
return { prop, default: def, overrides, varies: Object.keys(overrides).length > 0, visibleIn: [...sl.values.keys()], referencedIn: [], richIn: [] };
|
|
10154
10807
|
});
|
|
10155
10808
|
}
|
|
10156
10809
|
function authorTaskFromSet(setDir, opts = {}) {
|
|
@@ -10162,8 +10815,8 @@ function authorTaskFromSet(setDir, opts = {}) {
|
|
|
10162
10815
|
const poses = [];
|
|
10163
10816
|
const missing = [];
|
|
10164
10817
|
for (const rep of manifest.reps) {
|
|
10165
|
-
const metaFile =
|
|
10166
|
-
if (!
|
|
10818
|
+
const metaFile = path31.join(setDir, rep.slug, "get_metadata.json");
|
|
10819
|
+
if (!existsSync25(metaFile)) {
|
|
10167
10820
|
missing.push(rep.slug);
|
|
10168
10821
|
continue;
|
|
10169
10822
|
}
|
|
@@ -10177,8 +10830,8 @@ function authorTaskFromSet(setDir, opts = {}) {
|
|
|
10177
10830
|
if (missing.length > 0) {
|
|
10178
10831
|
throw new Error(`recording set ${setDir} is missing metadata for ${missing.length} rep(s): ${missing.slice(0, 5).join(", ")}${missing.length > 5 ? ", \u2026" : ""}`);
|
|
10179
10832
|
}
|
|
10180
|
-
const setMeta =
|
|
10181
|
-
const latticeNames = manifest.latticeNames ?? (
|
|
10833
|
+
const setMeta = path31.join(setDir, "get_metadata.json");
|
|
10834
|
+
const latticeNames = manifest.latticeNames ?? (existsSync25(setMeta) ? [...metadataText(setMeta).matchAll(/name="([^"]*)"/g)].map((m) => decodeXmlEntities(m[1])) : void 0);
|
|
10182
10835
|
const defaults = { ...manifest.defaults ?? {}, ...opts.defaults ?? {} };
|
|
10183
10836
|
const recordedFonts = recordedFontFamilies(setDir);
|
|
10184
10837
|
const textSlots = recordedTextSlots(setDir, manifest.reps.map((r) => r.slug));
|
|
@@ -10200,7 +10853,7 @@ function authorTaskFromSet(setDir, opts = {}) {
|
|
|
10200
10853
|
...recordedFonts.length > 0 ? { recordedFonts } : {},
|
|
10201
10854
|
...Object.keys(defaults).length > 0 ? { defaults } : {},
|
|
10202
10855
|
...textSlots.length > 0 ? { textSlots } : {},
|
|
10203
|
-
...dismissName !== void 0 ? { dismissible: dismissName } : {}
|
|
10856
|
+
...dismissName?.evidence !== void 0 ? { dismissible: dismissName.evidence } : {}
|
|
10204
10857
|
});
|
|
10205
10858
|
const anchorSlug = api.configs.find((c) => Object.keys(c.props).length === 0)?.rep ?? api.configs[0]?.rep;
|
|
10206
10859
|
const sentinels = textSlots.filter((slot) => !slot.varies).map((slot) => {
|
|
@@ -10224,19 +10877,35 @@ function authorTaskFromSet(setDir, opts = {}) {
|
|
|
10224
10877
|
}
|
|
10225
10878
|
return void 0;
|
|
10226
10879
|
}).filter((x) => x !== void 0);
|
|
10227
|
-
const { behaviors, prelude, disclosures } = authorBehaviors(api, {
|
|
10880
|
+
const { behaviors, prelude, disclosures } = authorBehaviors(api, {
|
|
10881
|
+
...sentinels.length > 0 ? { sentinels } : {},
|
|
10882
|
+
...dismissName !== void 0 && dismissName.visibleReps.length > 0 ? { dismissTargetRep: dismissName.visibleReps[0] } : {},
|
|
10883
|
+
// Author-time satisfiability inputs (run-22 cycle, Cycle C): the
|
|
10884
|
+
// recorded-PNG oracles — every prescribed pose-distance check is
|
|
10885
|
+
// gated on the recording proving it passable, each gate by the
|
|
10886
|
+
// measure its runtime check actually applies.
|
|
10887
|
+
poseGap: (a, b) => recordedPoseGap(setDir, a, b),
|
|
10888
|
+
posesIdentical: (a, b) => recordedPosesIdentical(setDir, a, b)
|
|
10889
|
+
});
|
|
10890
|
+
if (dismissName?.demoted !== void 0) disclosures.push(dismissName.demoted);
|
|
10228
10891
|
const puaGlyphs = symbolFontGlyphCount(setDir, manifest.reps.map((r) => r.slug));
|
|
10229
10892
|
if (puaGlyphs > 0) {
|
|
10230
10893
|
disclosures.push(
|
|
10231
10894
|
`SYMBOL-FONT ICONS: ${puaGlyphs} distinct recorded icon glyph(s) come from a symbol font (private-use codepoints) \u2014 the recording carries NO vector geometry for them. Reconstruct each mark against the reference pixels with extreme care, compare magnified crops after every score (run 11 shipped a checkmark where the reference was a dot, and a diamond where it was two chevrons \u2014 both scored above bar), and NEVER give two configs sharing the same recorded glyph different shapes`
|
|
10232
10895
|
);
|
|
10233
10896
|
}
|
|
10234
|
-
if (dismissName !== void 0) {
|
|
10235
|
-
disclosures.push(`dismiss affordance detected from the recording (${dismissName}) \u2014 onDismiss authored; dismiss-notifies-and-commits enforces the notification contract`);
|
|
10897
|
+
if (dismissName?.evidence !== void 0) {
|
|
10898
|
+
disclosures.push(`dismiss affordance detected from the recording (${dismissName.evidence}) \u2014 onDismiss authored; dismiss-notifies-and-commits enforces the notification contract`);
|
|
10236
10899
|
}
|
|
10237
10900
|
for (const combo of api.syntheticCombos) {
|
|
10238
10901
|
disclosures.push(`API split created a reachable pose with NO recorded truth: ${combo} (the design's exclusive axis cannot express it) \u2014 composed behavior only, disclosed to consumers`);
|
|
10239
10902
|
}
|
|
10903
|
+
for (const slot of textSlots) {
|
|
10904
|
+
if (slot.richIn.length === 0) continue;
|
|
10905
|
+
disclosures.push(
|
|
10906
|
+
`content prop (recorded ${JSON.stringify(slot.default)}) renders as RICH multi-run text in pose(s) ${slot.richIn.join(", ")} \u2014 those poses never receive the prop through configs (the prescribed contract renders caller strings plainly, which would contradict the recorded formatting on pixels); each renders its recorded rich markup prop-absent and stays pixel-verified that way`
|
|
10907
|
+
);
|
|
10908
|
+
}
|
|
10240
10909
|
for (const slot of textSlots) {
|
|
10241
10910
|
if (slot.varies) continue;
|
|
10242
10911
|
const sentineled = sentinels.some((sn) => api.props.some((pr) => pr.name === sn.prop && pr.default === slot.default));
|
|
@@ -10304,12 +10973,13 @@ ${PRELUDE_CONTRACT}
|
|
|
10304
10973
|
${opts.colorScheme === void 0 ? "" : `
|
|
10305
10974
|
RESOLVED FOR THIS RECORDING: it is ${opts.colorScheme}-mode truth, so pin color-scheme: ${opts.colorScheme} on the root. A conditional rule you have to resolve yourself is a rule you will get wrong \u2014 this is the answer, not the question.`}`;
|
|
10306
10975
|
}
|
|
10307
|
-
var PoseCompletenessError, kebab3, camel, pascal, RESERVED_PROPS, axisPropName, isStateAxis, DISMISS_NAMES, symbolName, STYLE_WEIGHTS2, styleWeight;
|
|
10976
|
+
var PoseCompletenessError, kebab3, camel, pascal, RESERVED_PROPS, NATIVE_ATTR_COLLISIONS, axisPropName, isStateAxis, DISMISS_NAMES, symbolName, STYLE_WEIGHTS2, styleWeight;
|
|
10308
10977
|
var init_brief = __esm({
|
|
10309
10978
|
"packages/generate/src/brief.ts"() {
|
|
10310
10979
|
"use strict";
|
|
10311
10980
|
init_src();
|
|
10312
10981
|
init_src5();
|
|
10982
|
+
init_archetype();
|
|
10313
10983
|
PoseCompletenessError = class extends Error {
|
|
10314
10984
|
inexpressible;
|
|
10315
10985
|
constructor(inexpressible) {
|
|
@@ -10328,9 +10998,10 @@ var init_brief = __esm({
|
|
|
10328
10998
|
return c === "" ? c : c[0].toUpperCase() + c.slice(1);
|
|
10329
10999
|
};
|
|
10330
11000
|
RESERVED_PROPS = /* @__PURE__ */ new Set(["style", "classname", "children", "key", "ref", "id"]);
|
|
11001
|
+
NATIVE_ATTR_COLLISIONS = /* @__PURE__ */ new Set(["size", "type", "checked", "multiple", "value"]);
|
|
10331
11002
|
axisPropName = (component, axis) => {
|
|
10332
11003
|
const name = camel(axis);
|
|
10333
|
-
return RESERVED_PROPS.has(name.toLowerCase()) ? camel(`${component} ${axis}`) : name;
|
|
11004
|
+
return RESERVED_PROPS.has(name.toLowerCase()) || NATIVE_ATTR_COLLISIONS.has(name.toLowerCase()) ? camel(`${component} ${axis}`) : name;
|
|
10334
11005
|
};
|
|
10335
11006
|
isStateAxis = (axis) => kebab3(axis) === "state";
|
|
10336
11007
|
DISMISS_NAMES = /* @__PURE__ */ new Set(["x", "close", "dismiss", "closebutton", "dismissbutton", "xbutton", "iconx", "iconclose", "icondismiss"]);
|
|
@@ -10361,10 +11032,10 @@ var init_brief = __esm({
|
|
|
10361
11032
|
});
|
|
10362
11033
|
|
|
10363
11034
|
// packages/generate/src/segments.ts
|
|
10364
|
-
import { existsSync as
|
|
10365
|
-
import
|
|
11035
|
+
import { existsSync as existsSync26, readFileSync as readFileSync22, readdirSync as readdirSync9 } from "node:fs";
|
|
11036
|
+
import path32 from "node:path";
|
|
10366
11037
|
function repText(set, rep, tool) {
|
|
10367
|
-
const env = JSON.parse(
|
|
11038
|
+
const env = JSON.parse(readFileSync22(path32.join(set, rep, `${tool}.json`), "utf8"));
|
|
10368
11039
|
return tool === "get_design_context" ? envelopeFirstTextPart(env) : envelopeTextContent(env);
|
|
10369
11040
|
}
|
|
10370
11041
|
function stripFigmaInstructions(emission) {
|
|
@@ -10424,20 +11095,20 @@ DROPPED from the map, untrustworthy in the source export \u2014 for these, each
|
|
|
10424
11095
|
}
|
|
10425
11096
|
function buildSegments(task, mode = "fenced") {
|
|
10426
11097
|
const SET = task.set;
|
|
10427
|
-
let defsRecorded =
|
|
11098
|
+
let defsRecorded = existsSync26(path32.join(SET, "get_variable_defs.json"));
|
|
10428
11099
|
let rawDefs = {};
|
|
10429
|
-
if (
|
|
10430
|
-
const text = envelopeFirstTextPart(JSON.parse(
|
|
11100
|
+
if (existsSync26(path32.join(SET, "get_variable_defs.json"))) {
|
|
11101
|
+
const text = envelopeFirstTextPart(JSON.parse(readFileSync22(path32.join(SET, "get_variable_defs.json"), "utf8"))) || "{}";
|
|
10431
11102
|
try {
|
|
10432
11103
|
rawDefs = JSON.parse(text);
|
|
10433
11104
|
} catch {
|
|
10434
11105
|
}
|
|
10435
11106
|
} else {
|
|
10436
11107
|
for (const cfg of task.configs) {
|
|
10437
|
-
const f =
|
|
10438
|
-
if (!
|
|
11108
|
+
const f = path32.join(SET, cfg.rep, "get_variable_defs.json");
|
|
11109
|
+
if (!existsSync26(f)) continue;
|
|
10439
11110
|
defsRecorded = true;
|
|
10440
|
-
const text = envelopeFirstTextPart(JSON.parse(
|
|
11111
|
+
const text = envelopeFirstTextPart(JSON.parse(readFileSync22(f, "utf8"))) || "{}";
|
|
10441
11112
|
try {
|
|
10442
11113
|
for (const [k, v] of Object.entries(JSON.parse(text))) rawDefs[k] ??= v;
|
|
10443
11114
|
} catch {
|
|
@@ -10445,8 +11116,8 @@ function buildSegments(task, mode = "fenced") {
|
|
|
10445
11116
|
}
|
|
10446
11117
|
}
|
|
10447
11118
|
const emissionTexts = task.configs.map((cfg) => {
|
|
10448
|
-
const f =
|
|
10449
|
-
return
|
|
11119
|
+
const f = path32.join(SET, cfg.rep, "get_design_context.json");
|
|
11120
|
+
return existsSync26(f) ? envelopeFirstTextPart(JSON.parse(readFileSync22(f, "utf8"))) : "";
|
|
10450
11121
|
});
|
|
10451
11122
|
const { map, note } = cleanTokenMap(rawDefs, emissionTexts);
|
|
10452
11123
|
const defs = JSON.stringify(map, null, 1);
|
|
@@ -10463,9 +11134,9 @@ TOKEN MAP NEVER RECORDED: this set holds no get_variable_defs envelope, so wheth
|
|
|
10463
11134
|
for (const cfg of task.configs) {
|
|
10464
11135
|
const meta = stripFigmaInstructions(repText(SET, cfg.rep, "get_metadata"));
|
|
10465
11136
|
const emission = stripFigmaInstructions(repText(SET, cfg.rep, "get_design_context"));
|
|
10466
|
-
const assets = readdirSync9(
|
|
11137
|
+
const assets = readdirSync9(path32.join(SET, cfg.rep)).filter((f) => f.startsWith("asset-") && f.endsWith(".svg")).map((f) => `asset ${f}:
|
|
10467
11138
|
\`\`\`svg
|
|
10468
|
-
${
|
|
11139
|
+
${readFileSync22(path32.join(SET, cfg.rep, f), "utf8")}
|
|
10469
11140
|
\`\`\``).join("\n");
|
|
10470
11141
|
parts.push(`
|
|
10471
11142
|
## Config ${cfg.rep} \u2192 ${cfg.component} ${JSON.stringify(cfg.props)}
|
|
@@ -10490,7 +11161,7 @@ Optionally a third block with \`/* FILE: tokens.css */\`.`);
|
|
|
10490
11161
|
} else {
|
|
10491
11162
|
parts.push(`
|
|
10492
11163
|
## Output format
|
|
10493
|
-
Write the COMPLETE files (${task.entry}, styles.css, optional tokens.css) into ONE candidate directory: \`tendril-out/${
|
|
11164
|
+
Write the COMPLETE files (${task.entry}, styles.css, optional tokens.css) into ONE candidate directory: \`tendril-out/${path32.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.`);
|
|
10494
11165
|
}
|
|
10495
11166
|
return parts.join("\n");
|
|
10496
11167
|
}
|
|
@@ -10558,8 +11229,8 @@ var init_adapter = __esm({
|
|
|
10558
11229
|
|
|
10559
11230
|
// packages/generate/src/bundle-emit.ts
|
|
10560
11231
|
import { createHash as createHash5 } from "node:crypto";
|
|
10561
|
-
import { copyFileSync, existsSync as
|
|
10562
|
-
import
|
|
11232
|
+
import { copyFileSync, existsSync as existsSync27, mkdirSync as mkdirSync7, readFileSync as readFileSync23, readdirSync as readdirSync10, rmSync as rmSync3, writeFileSync as writeFileSync11 } from "node:fs";
|
|
11233
|
+
import path33 from "node:path";
|
|
10563
11234
|
function pinFromConfigs(configs) {
|
|
10564
11235
|
const domains = /* @__PURE__ */ new Map();
|
|
10565
11236
|
const kinds = /* @__PURE__ */ new Map();
|
|
@@ -10628,9 +11299,9 @@ function fontsCssFor(faces, bundleDir, cacheDir = DEFAULT_FONT_CACHE, substitute
|
|
|
10628
11299
|
const notices = [];
|
|
10629
11300
|
const licenseTexts = /* @__PURE__ */ new Map();
|
|
10630
11301
|
for (const face of faces) {
|
|
10631
|
-
const src =
|
|
10632
|
-
const target = `./fonts/${
|
|
10633
|
-
const format = FONT_FORMATS[
|
|
11302
|
+
const src = path33.join(cacheDir, path33.basename(face.file));
|
|
11303
|
+
const target = `./fonts/${path33.basename(face.file)}`;
|
|
11304
|
+
const format = FONT_FORMATS[path33.extname(face.file).toLowerCase()] ?? "truetype";
|
|
10634
11305
|
const family = face.family.replace(/['\\]/g, "").replace(/\*\//g, "");
|
|
10635
11306
|
const decl = `@font-face { font-family: '${family}'; font-weight: ${face.weight}; font-style: normal; src: url(${target}) format('${format}'); }`;
|
|
10636
11307
|
const license = normalizeFontLicense(face.license);
|
|
@@ -10668,14 +11339,14 @@ function fontsCssFor(faces, bundleDir, cacheDir = DEFAULT_FONT_CACHE, substitute
|
|
|
10668
11339
|
`/* ${decl} */`
|
|
10669
11340
|
);
|
|
10670
11341
|
}
|
|
10671
|
-
} else if (
|
|
10672
|
-
mkdirSync7(
|
|
10673
|
-
copyFileSync(src,
|
|
11342
|
+
} else if (existsSync27(src) && createHash5("sha256").update(readFileSync23(src)).digest("hex") === face.sha256) {
|
|
11343
|
+
mkdirSync7(path33.join(bundleDir, "fonts"), { recursive: true });
|
|
11344
|
+
copyFileSync(src, path33.join(bundleDir, "fonts", path33.basename(face.file)));
|
|
10674
11345
|
licenseTexts.set(terms.file, terms.text);
|
|
10675
11346
|
const upstream = upstreamAttribution(face);
|
|
10676
11347
|
notices.push(
|
|
10677
11348
|
"",
|
|
10678
|
-
`${face.family.replace(/[\r\n]+/g, " ")} ${face.weight} \u2014 ./${
|
|
11349
|
+
`${face.family.replace(/[\r\n]+/g, " ")} ${face.weight} \u2014 ./${path33.basename(face.file)}`,
|
|
10679
11350
|
` licence: ${terms.title} \u2014 full text in ./${terms.file}`,
|
|
10680
11351
|
` source: ${face.source}`,
|
|
10681
11352
|
` sha256: ${face.sha256}`,
|
|
@@ -10689,9 +11360,9 @@ function fontsCssFor(faces, bundleDir, cacheDir = DEFAULT_FONT_CACHE, substitute
|
|
|
10689
11360
|
}
|
|
10690
11361
|
if (lines.length === 0) return null;
|
|
10691
11362
|
if (notices.length > 0) {
|
|
10692
|
-
const fontsDir =
|
|
10693
|
-
for (const [file, text] of licenseTexts) writeFileSync11(
|
|
10694
|
-
writeFileSync11(
|
|
11363
|
+
const fontsDir = path33.join(bundleDir, "fonts");
|
|
11364
|
+
for (const [file, text] of licenseTexts) writeFileSync11(path33.join(fontsDir, file), text);
|
|
11365
|
+
writeFileSync11(path33.join(fontsDir, "NOTICE.txt"), `${[NOTICE_PREAMBLE, ...notices].join("\n")}
|
|
10695
11366
|
`);
|
|
10696
11367
|
header.push(
|
|
10697
11368
|
"/* REDISTRIBUTION: the face files in ./fonts/ are redistributed under the licences",
|
|
@@ -10703,10 +11374,10 @@ function fontsCssFor(faces, bundleDir, cacheDir = DEFAULT_FONT_CACHE, substitute
|
|
|
10703
11374
|
`;
|
|
10704
11375
|
}
|
|
10705
11376
|
function countLatticeSymbols(setDir) {
|
|
10706
|
-
const manifestFile =
|
|
10707
|
-
if (
|
|
11377
|
+
const manifestFile = path33.join(setDir, "recording-set.json");
|
|
11378
|
+
if (existsSync27(manifestFile)) {
|
|
10708
11379
|
try {
|
|
10709
|
-
const stored = JSON.parse(
|
|
11380
|
+
const stored = JSON.parse(readFileSync23(manifestFile, "utf8"));
|
|
10710
11381
|
if (stored.variantScope !== "component-set") return null;
|
|
10711
11382
|
const lattice = stored.latticeNames;
|
|
10712
11383
|
if (lattice !== void 0 && lattice.length > 0) return lattice.length;
|
|
@@ -10714,13 +11385,13 @@ function countLatticeSymbols(setDir) {
|
|
|
10714
11385
|
}
|
|
10715
11386
|
}
|
|
10716
11387
|
const files = [
|
|
10717
|
-
|
|
10718
|
-
...
|
|
10719
|
-
].filter((f) =>
|
|
11388
|
+
path33.join(setDir, "get_metadata.json"),
|
|
11389
|
+
...existsSync27(setDir) ? readdirSync10(setDir).filter((f) => /^get_metadata-.*\.json$/.test(f)).map((f) => path33.join(setDir, f)) : []
|
|
11390
|
+
].filter((f) => existsSync27(f));
|
|
10720
11391
|
if (files.length === 0) return null;
|
|
10721
11392
|
let count = 0;
|
|
10722
11393
|
for (const f of files) {
|
|
10723
|
-
const text = envelopeTextContent(JSON.parse(
|
|
11394
|
+
const text = envelopeTextContent(JSON.parse(readFileSync23(f, "utf8")));
|
|
10724
11395
|
count += [...text.matchAll(/name="([^"]*)"/g)].filter((m) => m[1].includes("=")).length;
|
|
10725
11396
|
}
|
|
10726
11397
|
return count > 0 ? count : null;
|
|
@@ -10728,21 +11399,21 @@ function countLatticeSymbols(setDir) {
|
|
|
10728
11399
|
function recordingSetHash(setDir, configs) {
|
|
10729
11400
|
const relPaths = [];
|
|
10730
11401
|
for (const name of ["recording-set.json", "completeness-manifest.json", "get_variable_defs.json", "get_metadata.json"]) {
|
|
10731
|
-
if (
|
|
11402
|
+
if (existsSync27(path33.join(setDir, name))) relPaths.push(name);
|
|
10732
11403
|
}
|
|
10733
11404
|
for (const cfg of configs) {
|
|
10734
11405
|
for (const f of ["get_design_context.json", "get_metadata.json", "get_screenshot.json", "get_variable_defs.json"]) {
|
|
10735
|
-
if (
|
|
11406
|
+
if (existsSync27(path33.join(setDir, cfg.rep, f))) relPaths.push(`${cfg.rep}/${f}`);
|
|
10736
11407
|
}
|
|
10737
|
-
if (
|
|
10738
|
-
for (const asset of readdirSync10(
|
|
11408
|
+
if (existsSync27(path33.join(setDir, cfg.rep))) {
|
|
11409
|
+
for (const asset of readdirSync10(path33.join(setDir, cfg.rep)).filter((f) => f.startsWith("asset-"))) {
|
|
10739
11410
|
relPaths.push(`${cfg.rep}/${asset}`);
|
|
10740
11411
|
}
|
|
10741
11412
|
}
|
|
10742
11413
|
}
|
|
10743
11414
|
return hashRecordingSet(
|
|
10744
11415
|
relPaths,
|
|
10745
|
-
(p) => new Uint8Array(
|
|
11416
|
+
(p) => new Uint8Array(readFileSync23(path33.join(setDir, p))),
|
|
10746
11417
|
(chunks) => {
|
|
10747
11418
|
const h = createHash5("sha256");
|
|
10748
11419
|
for (const c of chunks) h.update(c);
|
|
@@ -10756,18 +11427,24 @@ function statusOf(s) {
|
|
|
10756
11427
|
}
|
|
10757
11428
|
function emitBundleV1(opts) {
|
|
10758
11429
|
const substituted = (opts.substitutedFamilies ?? []).length > 0;
|
|
11430
|
+
const parityFailed = new Set(opts.behaviors.filter((b) => b.id.startsWith("parity:") && !b.pass).map((b) => b.id.slice("parity:".length)));
|
|
10759
11431
|
const statuses = opts.scores.map((s) => {
|
|
10760
11432
|
const tier = statusOf(s);
|
|
10761
|
-
|
|
11433
|
+
const demoted = parityFailed.has(s.rep) ? "fail" : substituted && tier === "certified" ? "pass" : tier;
|
|
11434
|
+
return { rep: s.rep, similarity: s.similarity, inkRecall: s.inkRecall, ...s.exact !== void 0 ? { exact: s.exact } : {}, status: demoted };
|
|
10762
11435
|
});
|
|
10763
11436
|
const certified = statuses.filter((s) => s.status === "certified").length;
|
|
10764
11437
|
const pass = statuses.filter((s) => s.status !== "fail").length;
|
|
10765
11438
|
const lattice = countLatticeSymbols(opts.task.set);
|
|
10766
|
-
const interaction = opts.behaviors.filter(
|
|
11439
|
+
const interaction = opts.behaviors.filter(
|
|
11440
|
+
(b) => !b.id.startsWith("prelude:") && !b.id.startsWith("parity:") && !b.id.startsWith("composition:") && !b.id.startsWith("content-prop-renders(") && !CONTRACT_CHECK_IDS.has(b.id)
|
|
11441
|
+
);
|
|
11442
|
+
const content = opts.behaviors.filter((b) => b.id.startsWith("content-prop-renders("));
|
|
10767
11443
|
const prelude = opts.behaviors.filter((b) => b.id.startsWith("prelude:"));
|
|
10768
11444
|
const parity = opts.behaviors.filter((b) => b.id.startsWith("parity:"));
|
|
10769
|
-
const
|
|
10770
|
-
const
|
|
11445
|
+
const contract = opts.behaviors.filter((b) => CONTRACT_CHECK_IDS.has(b.id));
|
|
11446
|
+
const cssFiles = ["styles.css", "tokens.css"].map((f) => path33.join(opts.bundleDir, f)).filter((f) => existsSync27(f));
|
|
11447
|
+
const families = cssFontFamilies(cssFiles.map((f) => readFileSync23(f, "utf8")).join("\n"));
|
|
10771
11448
|
const requiredFonts = requiredFontsManifest(families, opts.fontCacheDir).map((f) => ({
|
|
10772
11449
|
family: f.family,
|
|
10773
11450
|
weight: f.weight,
|
|
@@ -10796,7 +11473,7 @@ function emitBundleV1(opts) {
|
|
|
10796
11473
|
// resolvable via verify's --set override).
|
|
10797
11474
|
path: (() => {
|
|
10798
11475
|
const base = process.env["INIT_CWD"] ?? process.cwd();
|
|
10799
|
-
const rel =
|
|
11476
|
+
const rel = path33.relative(base, opts.task.set);
|
|
10800
11477
|
return rel !== "" && !rel.startsWith("..") ? rel : opts.task.set;
|
|
10801
11478
|
})(),
|
|
10802
11479
|
component: opts.componentName,
|
|
@@ -10816,25 +11493,29 @@ function emitBundleV1(opts) {
|
|
|
10816
11493
|
interactionPassed: interaction.filter((b) => b.pass).length,
|
|
10817
11494
|
interactionChecks: interaction.length,
|
|
10818
11495
|
preludePassed: prelude.filter((b) => b.pass).length,
|
|
10819
|
-
preludeChecks: prelude.length
|
|
11496
|
+
preludeChecks: prelude.length,
|
|
11497
|
+
contentPassed: content.filter((b) => b.pass).length,
|
|
11498
|
+
contentChecks: content.length,
|
|
11499
|
+
parityPassed: parity.filter((b) => b.pass).length,
|
|
11500
|
+
parityChecks: parity.length
|
|
10820
11501
|
})
|
|
10821
11502
|
};
|
|
10822
11503
|
const written = [];
|
|
10823
|
-
const manifestPath2 =
|
|
11504
|
+
const manifestPath2 = path33.join(opts.bundleDir, "component.json");
|
|
10824
11505
|
writeFileSync11(manifestPath2, `${JSON.stringify(manifest, null, 2)}
|
|
10825
11506
|
`);
|
|
10826
11507
|
written.push(manifestPath2);
|
|
10827
|
-
const stylesPath =
|
|
10828
|
-
if (
|
|
11508
|
+
const stylesPath = path33.join(opts.bundleDir, "styles.css");
|
|
11509
|
+
if (existsSync27(stylesPath)) {
|
|
10829
11510
|
const comment = cssProvenanceComment({ pass, scored: statuses.length, certified, latticeConfigs: lattice });
|
|
10830
|
-
const current =
|
|
11511
|
+
const current = readFileSync23(stylesPath, "utf8");
|
|
10831
11512
|
const stripped = current.replace(/^\/\* tendril bundle v\d+ [^]*?\*\/\n/, "");
|
|
10832
11513
|
writeFileSync11(stylesPath, `${comment}
|
|
10833
11514
|
${stripped}`);
|
|
10834
11515
|
written.push(stylesPath);
|
|
10835
11516
|
}
|
|
10836
|
-
const fontsCssPath =
|
|
10837
|
-
rmSync3(
|
|
11517
|
+
const fontsCssPath = path33.join(opts.bundleDir, "fonts.css");
|
|
11518
|
+
rmSync3(path33.join(opts.bundleDir, "fonts"), { recursive: true, force: true });
|
|
10838
11519
|
rmSync3(fontsCssPath, { force: true });
|
|
10839
11520
|
const fontsCss = fontsCssFor(requiredFontsManifest(families, opts.fontCacheDir), opts.bundleDir, opts.fontCacheDir, opts.substitutedFamilies ?? []);
|
|
10840
11521
|
if (fontsCss !== null) {
|
|
@@ -10842,7 +11523,10 @@ ${stripped}`);
|
|
|
10842
11523
|
written.push(fontsCssPath);
|
|
10843
11524
|
}
|
|
10844
11525
|
const unrecorded = lattice === null ? null : Math.max(0, lattice - statuses.length);
|
|
10845
|
-
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}`) + (
|
|
11526
|
+
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}`) + (content.length === 0 ? "" : `, content slots ${content.filter((b) => b.pass).length}/${content.length}`) + (contract.length === 0 ? "" : `, component contract ${contract.filter((b) => b.pass).length}/${contract.length}`) + (parity.length === 0 ? "" : `, state parity ${parity.filter((b) => b.pass).length}/${parity.length}`) + (prelude.length === 0 ? "" : `, page hygiene ${prelude.filter((b) => b.pass).length}/${prelude.length}`) + // Silence at null was readable as complete coverage (the sweep's
|
|
11527
|
+
// M5 tail): the tri-state's worst spelling, in generate's one
|
|
11528
|
+
// non-suppressible line.
|
|
11529
|
+
(unrecorded === null ? "; coverage denominator UNKNOWN" : `; ${unrecorded} lattice configs UNVERIFIED`) + ` \u2014 claims in component.json, recompute with \`tendril verify\``;
|
|
10846
11530
|
return { manifest, statusLine, written };
|
|
10847
11531
|
}
|
|
10848
11532
|
var FONT_FORMATS, BARS, NOTICE_PREAMBLE, OFL_1_1_TEXT, UFL_1_0_TEXT, APACHE_2_0_TEXT, REDISTRIBUTABLE;
|
|
@@ -11264,8 +11948,8 @@ DEALINGS IN THE FONT SOFTWARE.
|
|
|
11264
11948
|
|
|
11265
11949
|
// packages/generate/src/compose-pins.ts
|
|
11266
11950
|
import { createHash as createHash6 } from "node:crypto";
|
|
11267
|
-
import { existsSync as
|
|
11268
|
-
import
|
|
11951
|
+
import { existsSync as existsSync28, readFileSync as readFileSync24, readdirSync as readdirSync11, realpathSync as realpathSync3, statSync as statSync3 } from "node:fs";
|
|
11952
|
+
import path34 from "node:path";
|
|
11269
11953
|
function bundleDirs(roots, depth = 4) {
|
|
11270
11954
|
const found = [];
|
|
11271
11955
|
const seen = /* @__PURE__ */ new Set();
|
|
@@ -11274,11 +11958,11 @@ function bundleDirs(roots, depth = 4) {
|
|
|
11274
11958
|
try {
|
|
11275
11959
|
key = realpathSync3(dir);
|
|
11276
11960
|
} catch {
|
|
11277
|
-
key =
|
|
11961
|
+
key = path34.resolve(dir);
|
|
11278
11962
|
}
|
|
11279
11963
|
if (seen.has(key)) return;
|
|
11280
11964
|
seen.add(key);
|
|
11281
|
-
if (
|
|
11965
|
+
if (existsSync28(path34.join(dir, "component.json"))) {
|
|
11282
11966
|
found.push(key);
|
|
11283
11967
|
return;
|
|
11284
11968
|
}
|
|
@@ -11291,14 +11975,14 @@ function bundleDirs(roots, depth = 4) {
|
|
|
11291
11975
|
}
|
|
11292
11976
|
for (const e of entries) {
|
|
11293
11977
|
if (e === "node_modules" || e.startsWith(".")) continue;
|
|
11294
|
-
const full =
|
|
11978
|
+
const full = path34.join(dir, e);
|
|
11295
11979
|
try {
|
|
11296
11980
|
if (statSync3(full).isDirectory()) walk2(full, remaining - 1);
|
|
11297
11981
|
} catch {
|
|
11298
11982
|
}
|
|
11299
11983
|
}
|
|
11300
11984
|
};
|
|
11301
|
-
for (const r of roots) walk2(
|
|
11985
|
+
for (const r of roots) walk2(path34.resolve(r), depth);
|
|
11302
11986
|
return found;
|
|
11303
11987
|
}
|
|
11304
11988
|
function composedPins(hostSet, libraryRoots) {
|
|
@@ -11317,7 +12001,7 @@ function composedPins(hostSet, libraryRoots) {
|
|
|
11317
12001
|
let pinned = false;
|
|
11318
12002
|
const failures = [];
|
|
11319
12003
|
for (const rel of partnerRels) {
|
|
11320
|
-
const partnerSet =
|
|
12004
|
+
const partnerSet = path34.resolve(hostSet, rel);
|
|
11321
12005
|
let partnerTask;
|
|
11322
12006
|
let partnerManifest;
|
|
11323
12007
|
try {
|
|
@@ -11346,7 +12030,7 @@ function composedPins(hostSet, libraryRoots) {
|
|
|
11346
12030
|
const wantHash = recordingSetHash(partnerSet, partnerTask.configs);
|
|
11347
12031
|
const matches = candidates.filter((dir) => {
|
|
11348
12032
|
try {
|
|
11349
|
-
const parsed = readBundleManifest(
|
|
12033
|
+
const parsed = readBundleManifest(readFileSync24(path34.join(dir, "component.json"), "utf8"));
|
|
11350
12034
|
return parsed.manifest?.provenance.recordingSet.hash === wantHash;
|
|
11351
12035
|
} catch {
|
|
11352
12036
|
return false;
|
|
@@ -11359,13 +12043,13 @@ function composedPins(hostSet, libraryRoots) {
|
|
|
11359
12043
|
continue;
|
|
11360
12044
|
}
|
|
11361
12045
|
if (matches.length > 1) {
|
|
11362
|
-
failures.push(`${rel}: ${matches.length} bundles join the same recording hash (${matches.map((m) =>
|
|
12046
|
+
failures.push(`${rel}: ${matches.length} bundles join the same recording hash (${matches.map((m) => path34.basename(m)).join(", ")}) \u2014 ambiguous; remove or point --library away from the duplicates`);
|
|
11363
12047
|
continue;
|
|
11364
12048
|
}
|
|
11365
12049
|
const bundleDir = matches[0];
|
|
11366
12050
|
let manifest;
|
|
11367
12051
|
try {
|
|
11368
|
-
manifest = readBundleManifest(
|
|
12052
|
+
manifest = readBundleManifest(readFileSync24(path34.join(bundleDir, "component.json"), "utf8")).manifest;
|
|
11369
12053
|
} catch {
|
|
11370
12054
|
manifest = void 0;
|
|
11371
12055
|
}
|
|
@@ -11382,8 +12066,8 @@ function composedPins(hostSet, libraryRoots) {
|
|
|
11382
12066
|
const moduleFiles = [];
|
|
11383
12067
|
let fileIssue;
|
|
11384
12068
|
for (const name of [manifest.entry, "styles.css", "tokens.css", "fonts.css"]) {
|
|
11385
|
-
const file =
|
|
11386
|
-
if (!
|
|
12069
|
+
const file = path34.join(bundleDir, name);
|
|
12070
|
+
if (!existsSync28(file)) {
|
|
11387
12071
|
if (name === manifest.entry || name === "styles.css") {
|
|
11388
12072
|
fileIssue = `${rel}: partner bundle is missing ${name}`;
|
|
11389
12073
|
break;
|
|
@@ -11392,7 +12076,7 @@ function composedPins(hostSet, libraryRoots) {
|
|
|
11392
12076
|
}
|
|
11393
12077
|
let bytes;
|
|
11394
12078
|
try {
|
|
11395
|
-
bytes =
|
|
12079
|
+
bytes = readFileSync24(file);
|
|
11396
12080
|
} catch {
|
|
11397
12081
|
fileIssue = `${rel}: partner file ${name} at ${bundleDir} is unreadable`;
|
|
11398
12082
|
break;
|
|
@@ -11463,14 +12147,14 @@ function composedChecks(candidateDir, hostEntry, pins) {
|
|
|
11463
12147
|
const checks = [];
|
|
11464
12148
|
let entrySource = "";
|
|
11465
12149
|
try {
|
|
11466
|
-
entrySource =
|
|
12150
|
+
entrySource = readFileSync24(path34.join(candidateDir, hostEntry), "utf8");
|
|
11467
12151
|
} catch {
|
|
11468
12152
|
}
|
|
11469
|
-
const candidateRoot =
|
|
12153
|
+
const candidateRoot = path34.resolve(candidateDir);
|
|
11470
12154
|
for (const pin of pins) {
|
|
11471
12155
|
const dir = composedModuleDir(pin.partnerName);
|
|
11472
|
-
const resolvedDir =
|
|
11473
|
-
if (!resolvedDir.startsWith(candidateRoot +
|
|
12156
|
+
const resolvedDir = path34.resolve(candidateDir, dir);
|
|
12157
|
+
if (!resolvedDir.startsWith(candidateRoot + path34.sep)) {
|
|
11474
12158
|
checks.push({ id: `composition:${pin.pairKey}:module-verbatim`, pass: false, detail: `pin path ${dir}/ escapes the candidate directory \u2014 refused` });
|
|
11475
12159
|
checks.push({ id: `composition:${pin.pairKey}:imported`, pass: false, detail: `pin path ${dir}/ escapes the candidate directory \u2014 refused` });
|
|
11476
12160
|
continue;
|
|
@@ -11481,12 +12165,12 @@ function composedChecks(candidateDir, hostEntry, pins) {
|
|
|
11481
12165
|
wrong.push(`${f.name} refused (not a plain path segment)`);
|
|
11482
12166
|
continue;
|
|
11483
12167
|
}
|
|
11484
|
-
const target =
|
|
11485
|
-
if (!
|
|
12168
|
+
const target = path34.join(candidateDir, dir, f.name);
|
|
12169
|
+
if (!existsSync28(target)) {
|
|
11486
12170
|
wrong.push(`${f.name} missing`);
|
|
11487
12171
|
continue;
|
|
11488
12172
|
}
|
|
11489
|
-
const sha = createHash6("sha256").update(
|
|
12173
|
+
const sha = createHash6("sha256").update(readFileSync24(target)).digest("hex");
|
|
11490
12174
|
if (sha !== f.sha256) wrong.push(`${f.name} differs from the pinned partner bytes`);
|
|
11491
12175
|
}
|
|
11492
12176
|
checks.push({
|
|
@@ -11512,15 +12196,15 @@ var init_compose_pins = __esm({
|
|
|
11512
12196
|
init_src4();
|
|
11513
12197
|
init_brief();
|
|
11514
12198
|
init_bundle_emit();
|
|
11515
|
-
composedModuleDir = (partnerName) =>
|
|
12199
|
+
composedModuleDir = (partnerName) => path34.posix.join("composed", partnerName);
|
|
11516
12200
|
safeSegment = (s) => /^[A-Za-z0-9][A-Za-z0-9._ -]*$/.test(s) && !s.includes("..");
|
|
11517
12201
|
MAX_PINNED_FILE_BYTES = 1024 * 1024;
|
|
11518
12202
|
}
|
|
11519
12203
|
});
|
|
11520
12204
|
|
|
11521
12205
|
// packages/generate/src/motion.ts
|
|
11522
|
-
import { existsSync as
|
|
11523
|
-
import
|
|
12206
|
+
import { existsSync as existsSync29, readFileSync as readFileSync25, readdirSync as readdirSync12, statSync as statSync4 } from "node:fs";
|
|
12207
|
+
import path35 from "node:path";
|
|
11524
12208
|
function springProgress(u, bounce) {
|
|
11525
12209
|
const decay = Math.log(100);
|
|
11526
12210
|
if (bounce <= 0) {
|
|
@@ -11550,7 +12234,7 @@ function motionCanonCss() {
|
|
|
11550
12234
|
` --tendril-motion-${tier}-ease: ${springLinearEasing(t.bounce)};`
|
|
11551
12235
|
]);
|
|
11552
12236
|
return [
|
|
11553
|
-
":root
|
|
12237
|
+
".\xABcomponent-root-class\xBB { /* substitute YOUR root class \u2014 never :root (token names on :root collide across composed bundles) */",
|
|
11554
12238
|
...lines,
|
|
11555
12239
|
"}",
|
|
11556
12240
|
"@media (prefers-reduced-motion: reduce) {",
|
|
@@ -11567,10 +12251,10 @@ function motionCanonCss() {
|
|
|
11567
12251
|
].join("\n");
|
|
11568
12252
|
}
|
|
11569
12253
|
function motionTruthFor(setDir) {
|
|
11570
|
-
const file =
|
|
11571
|
-
if (
|
|
12254
|
+
const file = path35.join(setDir, "get_motion_context.json");
|
|
12255
|
+
if (existsSync29(file) && usableEnvelope(file, "get_motion_context").ok) {
|
|
11572
12256
|
try {
|
|
11573
|
-
const text = envelopeTextContent(JSON.parse(
|
|
12257
|
+
const text = envelopeTextContent(JSON.parse(readFileSync25(file, "utf8")));
|
|
11574
12258
|
return text.trim() === "" ? { state: "recorded-empty" } : { state: "recorded", text };
|
|
11575
12259
|
} catch {
|
|
11576
12260
|
}
|
|
@@ -11582,21 +12266,21 @@ function motionTruthFor(setDir) {
|
|
|
11582
12266
|
}
|
|
11583
12267
|
}
|
|
11584
12268
|
function motionDisclosure(bundleDir, setDir) {
|
|
11585
|
-
const sheets = ["styles.css", "tokens.css"].map((f) =>
|
|
11586
|
-
const composedRoot =
|
|
12269
|
+
const sheets = ["styles.css", "tokens.css"].map((f) => path35.join(bundleDir, f));
|
|
12270
|
+
const composedRoot = path35.join(bundleDir, "composed");
|
|
11587
12271
|
try {
|
|
11588
12272
|
for (const entry of readdirSync12(composedRoot).sort()) {
|
|
11589
|
-
const dir =
|
|
12273
|
+
const dir = path35.join(composedRoot, entry);
|
|
11590
12274
|
try {
|
|
11591
12275
|
if (!statSync4(dir).isDirectory()) continue;
|
|
11592
12276
|
} catch {
|
|
11593
12277
|
continue;
|
|
11594
12278
|
}
|
|
11595
|
-
sheets.push(
|
|
12279
|
+
sheets.push(path35.join(dir, "styles.css"), path35.join(dir, "tokens.css"));
|
|
11596
12280
|
}
|
|
11597
12281
|
} catch {
|
|
11598
12282
|
}
|
|
11599
|
-
const css = sheets.filter((f) =>
|
|
12283
|
+
const css = sheets.filter((f) => existsSync29(f)).map((f) => readFileSync25(f, "utf8")).join("\n");
|
|
11600
12284
|
if (!MOTION_CSS_PATTERN.test(scannableCss(css))) return { present: false };
|
|
11601
12285
|
return {
|
|
11602
12286
|
present: true,
|
|
@@ -11615,7 +12299,7 @@ ${truth.text.length > RECORDED_QUOTE_CAP ? `${truth.text.slice(0, RECORDED_QUOTE
|
|
|
11615
12299
|
=== MOTION (PRESCRIBED, NOT VERIFIED \u2014 ADR-016) ===
|
|
11616
12300
|
${truthBlock}
|
|
11617
12301
|
DOCTRINE (motion is information, not decoration \u2014 apply where a state change benefits from continuity, never decoratively):
|
|
11618
|
-
- Copy these tokens
|
|
12302
|
+
- Copy these tokens into styles.css when you apply ANY motion \u2014 declarations VERBATIM, but substitute your component's root class for \xABcomponent-root-class\xBB (never :root: the token-scope rule applies to motion tokens too); no component invents its own duration \u2014 always the token:
|
|
11619
12303
|
${motionCanonCss()}
|
|
11620
12304
|
- Tier by SIZE of what moves: fast = hover/focus/selection/tooltips; moderate = local panels, indicators, switch thumbs; slow = dialogs and large surfaces.
|
|
11621
12305
|
- ENTERS use the tier's spring ease (var(--tendril-motion-<tier>-ease)) over var(--tendril-motion-<tier>); EXITS are plain quicker tweens: ease-out over var(--tendril-motion-<tier>-exit) \u2014 a dismissal reads as crisp and final, never the entrance replayed in reverse.
|
|
@@ -11647,6 +12331,7 @@ var init_motion = __esm({
|
|
|
11647
12331
|
var init_src7 = __esm({
|
|
11648
12332
|
"packages/generate/src/index.ts"() {
|
|
11649
12333
|
"use strict";
|
|
12334
|
+
init_archetype();
|
|
11650
12335
|
init_engine();
|
|
11651
12336
|
init_engine_curated();
|
|
11652
12337
|
init_loop2();
|
|
@@ -11662,9 +12347,9 @@ var init_src7 = __esm({
|
|
|
11662
12347
|
});
|
|
11663
12348
|
|
|
11664
12349
|
// packages/cli/src/font-guidance.ts
|
|
11665
|
-
import
|
|
12350
|
+
import path36 from "node:path";
|
|
11666
12351
|
function fontsUnprovenRemediation(setDir) {
|
|
11667
|
-
const set = setDir === void 0 ? void 0 :
|
|
12352
|
+
const set = setDir === void 0 ? void 0 : path36.resolve(setDir);
|
|
11668
12353
|
if (set !== void 0) {
|
|
11669
12354
|
try {
|
|
11670
12355
|
const needs = recordedFontNeeds(set);
|
|
@@ -11739,8 +12424,8 @@ __export(fonts_exports, {
|
|
|
11739
12424
|
runFontsResolveSet: () => runFontsResolveSet,
|
|
11740
12425
|
runFontsStatus: () => runFontsStatus
|
|
11741
12426
|
});
|
|
11742
|
-
import { existsSync as
|
|
11743
|
-
import
|
|
12427
|
+
import { existsSync as existsSync30, readFileSync as readFileSync26 } from "node:fs";
|
|
12428
|
+
import path37 from "node:path";
|
|
11744
12429
|
async function runFontsResolve(opts) {
|
|
11745
12430
|
const result = await resolveFonts(opts.family, opts.weights, opts.cacheDir);
|
|
11746
12431
|
const queryRefusal = systemFaceRefusal(opts.family.trim());
|
|
@@ -11761,7 +12446,7 @@ async function runFontsResolve(opts) {
|
|
|
11761
12446
|
}
|
|
11762
12447
|
}
|
|
11763
12448
|
async function runFontsResolveSet(opts) {
|
|
11764
|
-
const setDir =
|
|
12449
|
+
const setDir = path37.resolve(process.env["INIT_CWD"] ?? process.cwd(), opts.set);
|
|
11765
12450
|
let needs = [];
|
|
11766
12451
|
try {
|
|
11767
12452
|
needs = recordedFontNeeds(setDir);
|
|
@@ -11844,16 +12529,16 @@ async function runFontsResolveSet(opts) {
|
|
|
11844
12529
|
}
|
|
11845
12530
|
}
|
|
11846
12531
|
function runFontsStatus(opts) {
|
|
11847
|
-
const manifestPath2 =
|
|
11848
|
-
if (!
|
|
12532
|
+
const manifestPath2 = path37.join(opts.cacheDir, "manifest.json");
|
|
12533
|
+
if (!existsSync30(manifestPath2)) {
|
|
11849
12534
|
fail(opts, ExitCode.FontsUnproven, {
|
|
11850
12535
|
error: `no font cache at ${opts.cacheDir}`,
|
|
11851
12536
|
code: "fonts-unresolved",
|
|
11852
12537
|
remediation: `Run \`${tendrilCommand("fonts resolve --set <recording-dir>")}\` (or \`${tendrilCommand('fonts resolve "<Family>" --weights 400 500 600')}\`) first.`
|
|
11853
12538
|
});
|
|
11854
12539
|
}
|
|
11855
|
-
const faces = JSON.parse(
|
|
11856
|
-
const lockVerdicts = opts.lock !== void 0 ? checkFontLock(
|
|
12540
|
+
const faces = JSON.parse(readFileSync26(manifestPath2, "utf8"));
|
|
12541
|
+
const lockVerdicts = opts.lock !== void 0 ? checkFontLock(path37.resolve(opts.lock), opts.cacheDir) : null;
|
|
11857
12542
|
emitData(opts, { faces, lockVerdicts }, () => {
|
|
11858
12543
|
for (const f of faces) process.stdout.write(`cached ${f.family} ${f.weight} (${f.sha256.slice(0, 12)}\u2026)
|
|
11859
12544
|
`);
|
|
@@ -11897,13 +12582,13 @@ function familyMismatch(family, declared) {
|
|
|
11897
12582
|
}
|
|
11898
12583
|
function runFontsAdd(opts) {
|
|
11899
12584
|
if (opts.set !== void 0) {
|
|
11900
|
-
const declared = taskFontFamilies(
|
|
12585
|
+
const declared = taskFontFamilies(path37.resolve(opts.set)) ?? [];
|
|
11901
12586
|
const mismatch = familyMismatch(opts.family, declared);
|
|
11902
12587
|
if (mismatch !== void 0) {
|
|
11903
12588
|
fail(opts, ExitCode.InputValidation, {
|
|
11904
12589
|
error: `the recording set does not declare a family named "${opts.family}" \u2014 it declares ${declared.map((d) => `"${d}"`).join(", ")}. Adding it under this name would cache a face the mount never matches, and scoring would keep refusing for the family that is still missing.`,
|
|
11905
12590
|
code: "font-family-not-declared",
|
|
11906
|
-
remediation: mismatch.nearest !== void 0 ? `Did you mean "${mismatch.nearest}"? ${tendrilCommand(`fonts add "${mismatch.nearest}" ${opts.weight} ${opts.file} --set ${
|
|
12591
|
+
remediation: mismatch.nearest !== void 0 ? `Did you mean "${mismatch.nearest}"? ${tendrilCommand(`fonts add "${mismatch.nearest}" ${opts.weight} ${opts.file} --set ${path37.resolve(opts.set)}`)}` : `Use one of the declared families exactly as the recording spells it, or drop --set to add a family this set does not declare.`
|
|
11907
12592
|
});
|
|
11908
12593
|
}
|
|
11909
12594
|
} else {
|
|
@@ -11962,13 +12647,13 @@ cache them: ${tendrilCommand(`fonts add-system ${quoteArg(opts.family)}`)}
|
|
|
11962
12647
|
}
|
|
11963
12648
|
function runFontsAddSystem(opts) {
|
|
11964
12649
|
if (opts.set !== void 0) {
|
|
11965
|
-
const declared = taskFontFamilies(
|
|
12650
|
+
const declared = taskFontFamilies(path37.resolve(opts.set)) ?? [];
|
|
11966
12651
|
const mismatch = familyMismatch(opts.family, declared);
|
|
11967
12652
|
if (mismatch !== void 0) {
|
|
11968
12653
|
fail(opts, ExitCode.InputValidation, {
|
|
11969
12654
|
error: `the recording set does not declare a family named "${opts.family}" \u2014 it declares ${declared.map((d) => `"${d}"`).join(", ")}. A face cached under a name the mount never matches leaves scoring refusing for the family that is still missing.`,
|
|
11970
12655
|
code: "font-family-not-declared",
|
|
11971
|
-
remediation: mismatch.nearest !== void 0 ? `Did you mean "${mismatch.nearest}"? ${tendrilCommand(`fonts add-system ${quoteArg(mismatch.nearest)} --set ${quoteArg(
|
|
12656
|
+
remediation: mismatch.nearest !== void 0 ? `Did you mean "${mismatch.nearest}"? ${tendrilCommand(`fonts add-system ${quoteArg(mismatch.nearest)} --set ${quoteArg(path37.resolve(opts.set))}`)}` : `Use one of the declared families exactly as the recording spells it, or drop --set.`
|
|
11972
12657
|
});
|
|
11973
12658
|
}
|
|
11974
12659
|
} else {
|
|
@@ -12038,24 +12723,34 @@ __export(verify_exports, {
|
|
|
12038
12723
|
resolveComposition: () => resolveComposition,
|
|
12039
12724
|
runVerify: () => runVerify
|
|
12040
12725
|
});
|
|
12041
|
-
import { existsSync as
|
|
12042
|
-
import
|
|
12726
|
+
import { existsSync as existsSync31, readFileSync as readFileSync27 } from "node:fs";
|
|
12727
|
+
import path38 from "node:path";
|
|
12043
12728
|
function interactionCoverage(behaviors) {
|
|
12044
12729
|
const parity = behaviors.filter((b) => b.id.startsWith("parity:"));
|
|
12045
|
-
const
|
|
12730
|
+
const content = behaviors.filter((b) => b.id.startsWith("content-prop-renders("));
|
|
12731
|
+
const contract = behaviors.filter((b) => CONTRACT_CHECK_IDS.has(b.id));
|
|
12732
|
+
const interaction = behaviors.filter(
|
|
12733
|
+
(b) => !b.id.startsWith("prelude:") && !b.id.startsWith("parity:") && !b.id.startsWith("composition:") && !b.id.startsWith("content-prop-renders(") && !CONTRACT_CHECK_IDS.has(b.id)
|
|
12734
|
+
);
|
|
12046
12735
|
return {
|
|
12047
12736
|
interactionChecks: interaction.length,
|
|
12048
12737
|
interactionPassed: interaction.filter((b) => b.pass).length,
|
|
12049
12738
|
preludeChecks: behaviors.filter((b) => b.id.startsWith("prelude:")).length,
|
|
12739
|
+
contentChecks: content.length,
|
|
12740
|
+
contentPassed: content.filter((b) => b.pass).length,
|
|
12050
12741
|
parityChecks: parity.length,
|
|
12051
12742
|
parityPassed: parity.filter((b) => b.pass).length,
|
|
12743
|
+
contractChecks: contract.length,
|
|
12744
|
+
contractPassed: contract.filter((b) => b.pass).length,
|
|
12052
12745
|
// Asymmetric on purpose (review finding F3): a parity PASS never
|
|
12053
12746
|
// makes operability "verified" (parity is pixel-parity, not
|
|
12054
12747
|
// someone typing or clicking — the §0j miscount), but a parity
|
|
12055
12748
|
// FAIL still forbids it — "verified" printed beside a broken hover
|
|
12056
12749
|
// state is the one word doing too much lifting again, from the
|
|
12057
|
-
// other direction.
|
|
12058
|
-
|
|
12750
|
+
// other direction. Contract checks (Cycle D review) get the SAME
|
|
12751
|
+
// asymmetry: ref identity and attribute probes never mint
|
|
12752
|
+
// "verified", a broken contract still forbids it.
|
|
12753
|
+
operability: interaction.length > 0 && interaction.every((b) => b.pass) && parity.every((b) => b.pass) && contract.every((b) => b.pass) ? "verified" : "unverified"
|
|
12059
12754
|
};
|
|
12060
12755
|
}
|
|
12061
12756
|
function operabilityReport(input) {
|
|
@@ -12144,7 +12839,7 @@ function checkSummarySegments(input) {
|
|
|
12144
12839
|
const { availability } = input;
|
|
12145
12840
|
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(", ")}` : ""})`;
|
|
12146
12841
|
const occ = occlusionReport(input.occlusion);
|
|
12147
|
-
const occlusion = "unavailable" in occ ? `occlusion not applicable (${NO_OVERLAY_DECLARED})` : `occlusion ${
|
|
12842
|
+
const occlusion = "unavailable" in occ ? `occlusion not applicable (${NO_OVERLAY_DECLARED})` : `occlusion ${occ.passed}/${occ.checks}${(occ.unmeasured ?? 0) > 0 ? ` measured (+${occ.unmeasured} unmeasured)` : ""}`;
|
|
12148
12843
|
const operability = "short" in input.operability ? `operability UNVERIFIED (${input.operability.short})` : `operability ${input.operability.passed}/${input.operability.checks} interaction`;
|
|
12149
12844
|
const cross = input.crossComposition === void 0 || input.crossComposition.rows.length === 0 && input.crossComposition.malformed === void 0 ? "" : input.crossComposition.malformed !== void 0 && input.crossComposition.rows.length === 0 ? ` \xB7 cross-composition NOT CHECKED (entries REJECTED)` : (() => {
|
|
12150
12845
|
const supported = input.crossComposition.rows.filter((r) => r.status === "supported" || r.status === "stale-supported").length;
|
|
@@ -12224,7 +12919,9 @@ function occlusionReport(occlusion) {
|
|
|
12224
12919
|
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"
|
|
12225
12920
|
};
|
|
12226
12921
|
}
|
|
12227
|
-
|
|
12922
|
+
const measured = occlusion.filter((o) => o.unmeasured !== true);
|
|
12923
|
+
const unmeasured = occlusion.length - measured.length;
|
|
12924
|
+
return { checks: measured.length, passed: measured.filter((o) => o.pass).length, ...unmeasured > 0 ? { unmeasured } : {} };
|
|
12228
12925
|
}
|
|
12229
12926
|
function compositionReport(input) {
|
|
12230
12927
|
const { availability, regions } = input;
|
|
@@ -12245,7 +12942,7 @@ function compositionReport(input) {
|
|
|
12245
12942
|
function eyeCheck(bundleDir) {
|
|
12246
12943
|
return {
|
|
12247
12944
|
command: tendrilCommand(`inspect "${bundleDir}"`),
|
|
12248
|
-
sheetPath:
|
|
12945
|
+
sheetPath: path38.join(bundleDir, "verify-evidence", "inspect.html"),
|
|
12249
12946
|
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."
|
|
12250
12947
|
};
|
|
12251
12948
|
}
|
|
@@ -12257,7 +12954,7 @@ function taskFromManifest(opts, manifest, setDir) {
|
|
|
12257
12954
|
const adapterSlugs = Object.keys(manifest.propAdapter);
|
|
12258
12955
|
const unmapped = recordedSlugs.filter((s) => !adapterSlugs.includes(s));
|
|
12259
12956
|
const adapterOnly = adapterSlugs.filter((s) => !recordedSlugs.includes(s));
|
|
12260
|
-
const registry = Object.values(TASKS).find((t) =>
|
|
12957
|
+
const registry = Object.values(TASKS).find((t) => path38.resolve(t.set) === path38.resolve(setDir));
|
|
12261
12958
|
const authored = (() => {
|
|
12262
12959
|
if (registry !== void 0) return void 0;
|
|
12263
12960
|
try {
|
|
@@ -12291,24 +12988,27 @@ function taskFromManifest(opts, manifest, setDir) {
|
|
|
12291
12988
|
// runs, so their evidence is UNKNOWN, not empty: an empty list is
|
|
12292
12989
|
// spent downstream as "the recording holds no interactive pose".
|
|
12293
12990
|
interactionEvidence: authored?.api.interactionEvidence,
|
|
12294
|
-
unmappedInteractionEvidence: authored?.api.unmappedInteractionEvidence ?? []
|
|
12991
|
+
unmappedInteractionEvidence: authored?.api.unmappedInteractionEvidence ?? [],
|
|
12992
|
+
satisfiabilityDemoted: authored?.api.satisfiabilityDemoted ?? [],
|
|
12993
|
+
...authored !== void 0 ? { expertContract: { archetype: authored.api.archetype.archetype, forwardRef: authored.api.apiPin.contract.forwardRef, derivedStateValues: authored.api.derivedStateTokens, publicUnionValues: authored.api.props.flatMap((p) => p.values ?? []) } } : {}
|
|
12295
12994
|
};
|
|
12296
12995
|
}
|
|
12297
12996
|
async function runVerify(opts) {
|
|
12298
12997
|
const callerCwd = process.env["INIT_CWD"] ?? process.cwd();
|
|
12299
|
-
|
|
12300
|
-
|
|
12301
|
-
|
|
12998
|
+
let recordingSetDrift;
|
|
12999
|
+
const setOverride = opts.set !== void 0 ? path38.resolve(callerCwd, opts.set) : void 0;
|
|
13000
|
+
opts = { ...opts, bundleDir: path38.resolve(callerCwd, opts.bundleDir), ...setOverride !== void 0 ? { set: setOverride } : {} };
|
|
13001
|
+
if (!existsSync31(opts.bundleDir)) {
|
|
12302
13002
|
fail(opts, ExitCode.InputValidation, {
|
|
12303
13003
|
error: `bundle directory not found: ${opts.bundleDir}`,
|
|
12304
13004
|
code: "bundle-missing",
|
|
12305
13005
|
remediation: "Point at a generated bundle directory (containing component.json, the entry module, and styles.css)."
|
|
12306
13006
|
});
|
|
12307
13007
|
}
|
|
12308
|
-
const manifestPath2 =
|
|
13008
|
+
const manifestPath2 = path38.join(opts.bundleDir, "component.json");
|
|
12309
13009
|
let manifest;
|
|
12310
|
-
if (
|
|
12311
|
-
const { manifest: parsed, issues } = readBundleManifest(
|
|
13010
|
+
if (existsSync31(manifestPath2)) {
|
|
13011
|
+
const { manifest: parsed, issues } = readBundleManifest(readFileSync27(manifestPath2, "utf8"));
|
|
12312
13012
|
if (issues.length > 0) {
|
|
12313
13013
|
fail(opts, ExitCode.InputValidation, {
|
|
12314
13014
|
error: `bundle manifest rejected at ingest: ${issues.map((i) => i.message).join("; ")}`,
|
|
@@ -12322,6 +13022,8 @@ async function runVerify(opts) {
|
|
|
12322
13022
|
let unmapped = [];
|
|
12323
13023
|
let interactionEvidence;
|
|
12324
13024
|
let unmappedInteractionEvidence = [];
|
|
13025
|
+
let satisfiabilityDemoted = [];
|
|
13026
|
+
let expertContract;
|
|
12325
13027
|
let authorityConfigs;
|
|
12326
13028
|
let availability = ROLES_NOT_RESOLVED;
|
|
12327
13029
|
if (opts.task !== void 0) {
|
|
@@ -12336,21 +13038,21 @@ async function runVerify(opts) {
|
|
|
12336
13038
|
task = registry;
|
|
12337
13039
|
} else if (manifest !== void 0) {
|
|
12338
13040
|
const resolveSetDir = (p) => {
|
|
12339
|
-
if (
|
|
12340
|
-
const fromRepo =
|
|
12341
|
-
if (
|
|
12342
|
-
return
|
|
13041
|
+
if (path38.isAbsolute(p)) return p;
|
|
13042
|
+
const fromRepo = path38.resolve(REPO_ROOT, p);
|
|
13043
|
+
if (existsSync31(fromRepo)) return fromRepo;
|
|
13044
|
+
return path38.resolve(callerCwd, p);
|
|
12343
13045
|
};
|
|
12344
13046
|
const setDir = opts.set ?? resolveSetDir(manifest.provenance.recordingSet.path);
|
|
12345
|
-
if (!
|
|
13047
|
+
if (!existsSync31(path38.join(setDir, "recording-set.json")) && !Object.values(TASKS).some((t) => path38.resolve(t.set) === path38.resolve(setDir))) {
|
|
12346
13048
|
fail(opts, ExitCode.RecordingIncomplete, {
|
|
12347
13049
|
error: `recording set not found or unmanifested: ${setDir}`,
|
|
12348
13050
|
code: "recording-set-missing",
|
|
12349
13051
|
remediation: "Pass --set <dir> pointing at the recording set this bundle was generated from."
|
|
12350
13052
|
});
|
|
12351
13053
|
}
|
|
12352
|
-
const registry = Object.values(TASKS).find((t) =>
|
|
12353
|
-
if (registry !== void 0 && !
|
|
13054
|
+
const registry = Object.values(TASKS).find((t) => path38.resolve(t.set) === path38.resolve(setDir));
|
|
13055
|
+
if (registry !== void 0 && !existsSync31(path38.join(setDir, "recording-set.json"))) {
|
|
12354
13056
|
const recordedSlugs = registry.configs.map((c) => c.rep);
|
|
12355
13057
|
const adapterSlugs = Object.keys(manifest.propAdapter);
|
|
12356
13058
|
unmapped = recordedSlugs.filter((s) => !adapterSlugs.includes(s));
|
|
@@ -12367,6 +13069,8 @@ async function runVerify(opts) {
|
|
|
12367
13069
|
authorityConfigs = built.authorityConfigs;
|
|
12368
13070
|
interactionEvidence = built.interactionEvidence;
|
|
12369
13071
|
unmappedInteractionEvidence = built.unmappedInteractionEvidence;
|
|
13072
|
+
satisfiabilityDemoted = built.satisfiabilityDemoted;
|
|
13073
|
+
expertContract = built.expertContract;
|
|
12370
13074
|
for (const s of built.adapterOnly) warn(opts, `prop adapter maps "${s}" which is not in the recording set \u2014 ignored`);
|
|
12371
13075
|
availability = resolveComposition(loadManifest(setDir).roles);
|
|
12372
13076
|
if (availability.warn !== void 0) warn(opts, availability.warn);
|
|
@@ -12374,11 +13078,12 @@ async function runVerify(opts) {
|
|
|
12374
13078
|
const hash = recordingSetHash(setDir, task.configs);
|
|
12375
13079
|
if (hash !== manifest.provenance.recordingSet.hash) {
|
|
12376
13080
|
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`);
|
|
13081
|
+
recordingSetDrift = { stamped: manifest.provenance.recordingSet.hash, current: hash };
|
|
12377
13082
|
}
|
|
12378
13083
|
for (const name of [manifest.entry, "styles.css", "tokens.css"]) {
|
|
12379
|
-
const p =
|
|
12380
|
-
if (!
|
|
12381
|
-
const issues = checkBundleSourceFile(name, new Uint8Array(
|
|
13084
|
+
const p = path38.join(opts.bundleDir, name);
|
|
13085
|
+
if (!existsSync31(p)) continue;
|
|
13086
|
+
const issues = checkBundleSourceFile(name, new Uint8Array(readFileSync27(p)));
|
|
12382
13087
|
if (issues.length > 0) {
|
|
12383
13088
|
fail(opts, ExitCode.InputValidation, {
|
|
12384
13089
|
error: `bundle source rejected at ingest: ${issues.map((i) => i.message).join("; ")}`,
|
|
@@ -12416,7 +13121,7 @@ async function runVerify(opts) {
|
|
|
12416
13121
|
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)`);
|
|
12417
13122
|
}
|
|
12418
13123
|
const missing = task.configs.filter(
|
|
12419
|
-
(c) => !
|
|
13124
|
+
(c) => !existsSync31(path38.join(task.set, c.rep, "get_screenshot.json")) || !existsSync31(path38.join(task.set, c.rep, "get_metadata.json"))
|
|
12420
13125
|
);
|
|
12421
13126
|
if (missing.length > 0) {
|
|
12422
13127
|
fail(opts, ExitCode.RecordingIncomplete, {
|
|
@@ -12426,13 +13131,18 @@ async function runVerify(opts) {
|
|
|
12426
13131
|
});
|
|
12427
13132
|
}
|
|
12428
13133
|
const bar = BARS2[opts.bar];
|
|
12429
|
-
const evidenceDir =
|
|
13134
|
+
const evidenceDir = path38.join(opts.bundleDir, "verify-evidence");
|
|
12430
13135
|
const scores = await scoreBundleForTask(task, opts.bundleDir, bar, { evidenceDir, onProgress: emitProgress });
|
|
12431
|
-
const quality = await checkBundleQuality(
|
|
12432
|
-
|
|
12433
|
-
|
|
12434
|
-
|
|
12435
|
-
|
|
13136
|
+
const quality = await checkBundleQuality(
|
|
13137
|
+
opts.bundleDir,
|
|
13138
|
+
task.entry,
|
|
13139
|
+
{ dir: task.set, reps: task.configs.map((c) => c.rep) },
|
|
13140
|
+
void 0,
|
|
13141
|
+
{ recordedTruth: motionTruthFor(task.set).state === "recorded" },
|
|
13142
|
+
expertContract
|
|
13143
|
+
);
|
|
13144
|
+
const bundleCss = ["tokens.css", "styles.css"].map((f) => path38.join(opts.bundleDir, f)).filter((f) => existsSync31(f)).map((f) => readFileSync27(f, "utf8")).join("\n");
|
|
13145
|
+
const occlusion = await checkSiblingOcclusion(task, opts.bundleDir, bundleCss, { ...authorityConfigs !== void 0 ? { authority: authorityConfigs } : {} });
|
|
12436
13146
|
const parity = await checkHoverParity(task, opts.bundleDir, authorityConfigs ?? task.configs);
|
|
12437
13147
|
const behaviors = [...await checkBehaviors(task, opts.bundleDir), ...parity];
|
|
12438
13148
|
const structural = roles !== void 0 ? await checkStructuralComposition(task, opts.bundleDir, roles) : [];
|
|
@@ -12443,7 +13153,7 @@ async function runVerify(opts) {
|
|
|
12443
13153
|
warn(opts, `compositions extension REJECTED (${crossComposition.malformed}) \u2014 the cross-bundle backstop did NOT run over it; repair the manifest entry and re-verify. This is an instrument failure, not a clean bill.`);
|
|
12444
13154
|
}
|
|
12445
13155
|
const verifyCallerCwd = process.env["INIT_CWD"] ?? process.cwd();
|
|
12446
|
-
const verifyPins = crossComposition.rows.length > 0 ? composedPins(task.set, [opts.library !== void 0 ?
|
|
13156
|
+
const verifyPins = crossComposition.rows.length > 0 ? composedPins(task.set, [opts.library !== void 0 ? path38.resolve(verifyCallerCwd, opts.library) : verifyCallerCwd]) : { pins: [], issues: [] };
|
|
12447
13157
|
const verifyComposedChecks = composedChecks(opts.bundleDir, task.entry, verifyPins.pins);
|
|
12448
13158
|
const composedPairs = new Set(
|
|
12449
13159
|
verifyPins.pins.filter((pin) => verifyComposedChecks.filter((c) => c.id.startsWith(`composition:${pin.pairKey}:`)).every((c) => c.pass)).map((pin) => pin.pairKey)
|
|
@@ -12480,7 +13190,8 @@ async function runVerify(opts) {
|
|
|
12480
13190
|
const occlusionFailures = occlusion.filter((o) => !o.pass);
|
|
12481
13191
|
const coverage = interactionCoverage(behaviors);
|
|
12482
13192
|
const operability = operabilityReport({ behaviors, interactionEvidence });
|
|
12483
|
-
const
|
|
13193
|
+
const ungatedEvidence = (interactionEvidence ?? []).filter((e) => !satisfiabilityDemoted.includes(e));
|
|
13194
|
+
const evidenceUnverified = ungatedEvidence.length > 0 && coverage.interactionChecks === 0;
|
|
12484
13195
|
const okExceptDemotion = pixelFailures.length === 0 && behaviorFailures.length === 0 && structuralFailures.length === 0 && cropFailures.length === 0 && occlusionFailures.length === 0 && !evidenceUnverified;
|
|
12485
13196
|
const certBlockedByAbsentInk = opts.bar === "cert" ? absentInkDemoted : [];
|
|
12486
13197
|
const ok = okExceptDemotion && certBlockedByAbsentInk.length === 0;
|
|
@@ -12565,6 +13276,8 @@ async function runVerify(opts) {
|
|
|
12565
13276
|
// what it is (prescribed) and what it is not (verified), plus
|
|
12566
13277
|
// which truth the prescription stood on. Never a verdict input.
|
|
12567
13278
|
motion: motionDisclosure(opts.bundleDir, task.set),
|
|
13279
|
+
...recordingSetDrift !== void 0 ? { recordingSetDrift } : {},
|
|
13280
|
+
...substitutedFamilies.length > 0 ? { substitutedFamilies } : {},
|
|
12568
13281
|
eyeCheck: eyeCheck(opts.bundleDir)
|
|
12569
13282
|
};
|
|
12570
13283
|
emitData(opts, report, () => {
|
|
@@ -12680,7 +13393,7 @@ ${certified}/${statuses.length} certified \xB7 ${report.coverage.pass}/${statuse
|
|
|
12680
13393
|
}
|
|
12681
13394
|
process.stdout.write(`environment: chrome=${report.environment.chromeVersion ?? report.environment.chrome} fonts=${report.environment.fontsManifestSha256 ?? "UNRESOLVED"}
|
|
12682
13395
|
`);
|
|
12683
|
-
const shippedFonts = requiredFontsManifest(cssFontFamilies(["styles.css", "tokens.css"].map((f) =>
|
|
13396
|
+
const shippedFonts = requiredFontsManifest(cssFontFamilies(["styles.css", "tokens.css"].map((f) => path38.join(opts.bundleDir, f)).filter((f) => existsSync31(f)).map((f) => readFileSync27(f, "utf8")).join("\n")));
|
|
12684
13397
|
if (shippedFonts.length > 0 && substitutedFamilies.length === 0) {
|
|
12685
13398
|
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)
|
|
12686
13399
|
`);
|
|
@@ -12771,18 +13484,28 @@ __export(engine_exports, {
|
|
|
12771
13484
|
runEngineBrief: () => runEngineBrief,
|
|
12772
13485
|
runEngineScore: () => runEngineScore
|
|
12773
13486
|
});
|
|
12774
|
-
import { appendFileSync, existsSync as
|
|
12775
|
-
import
|
|
13487
|
+
import { appendFileSync, existsSync as existsSync32, mkdirSync as mkdirSync8, readFileSync as readFileSync28, writeFileSync as writeFileSync12 } from "node:fs";
|
|
13488
|
+
import path39 from "node:path";
|
|
12776
13489
|
function resolveEngineTask(opts, callerCwd) {
|
|
12777
|
-
const asPath =
|
|
12778
|
-
const isSet =
|
|
13490
|
+
const asPath = path39.resolve(callerCwd, opts.taskOrSet);
|
|
13491
|
+
const isSet = existsSync32(path39.join(asPath, "recording-set.json"));
|
|
12779
13492
|
const registry = TASKS[opts.taskOrSet];
|
|
12780
13493
|
if (registry !== void 0 && !isSet) return { task: registry, name: opts.taskOrSet, ref: opts.taskOrSet, disclosures: [], interactionEvidence: void 0 };
|
|
12781
13494
|
if (isSet) {
|
|
12782
13495
|
try {
|
|
12783
13496
|
const authored = authorTaskFromSet(asPath);
|
|
12784
13497
|
for (const d of authored.disclosures) warn(opts, d);
|
|
12785
|
-
return {
|
|
13498
|
+
return {
|
|
13499
|
+
task: authored.task,
|
|
13500
|
+
name: path39.basename(asPath),
|
|
13501
|
+
ref: asPath,
|
|
13502
|
+
disclosures: authored.disclosures,
|
|
13503
|
+
interactionEvidence: authored.api.interactionEvidence,
|
|
13504
|
+
unmappedInteractionEvidence: authored.api.unmappedInteractionEvidence,
|
|
13505
|
+
satisfiabilityDemoted: authored.api.satisfiabilityDemoted,
|
|
13506
|
+
expertContract: { archetype: authored.api.archetype.archetype, forwardRef: authored.api.apiPin.contract.forwardRef, derivedStateValues: authored.api.derivedStateTokens, publicUnionValues: authored.api.props.flatMap((p) => p.values ?? []) },
|
|
13507
|
+
apiPin: { props: authored.api.apiPin.props, forcedStates: authored.api.apiPin.forcedStates }
|
|
13508
|
+
};
|
|
12786
13509
|
} catch (err) {
|
|
12787
13510
|
fail(opts, ExitCode.InputValidation, {
|
|
12788
13511
|
error: `cannot author a task from ${asPath}: ${err instanceof Error ? err.message.split("\n")[0] : String(err)}`,
|
|
@@ -12809,9 +13532,9 @@ ${disclosures.map((d) => `- ${d}`).join("\n")}` : "";
|
|
|
12809
13532
|
const brief = buildBrief(task.systemApi, bar, { colorScheme: recordingIsDark(task) ? "dark" : "light" }) + disclosureBlock + motionBriefSection(task.set);
|
|
12810
13533
|
const segments = buildSegments(task, "files");
|
|
12811
13534
|
let notRecorded;
|
|
12812
|
-
const manifestPath2 =
|
|
12813
|
-
if (
|
|
12814
|
-
notRecorded = JSON.parse(
|
|
13535
|
+
const manifestPath2 = path39.join(task.set, "recording-set.json");
|
|
13536
|
+
if (existsSync32(manifestPath2)) {
|
|
13537
|
+
notRecorded = JSON.parse(readFileSync28(manifestPath2, "utf8")).notRecorded;
|
|
12815
13538
|
}
|
|
12816
13539
|
const unverified = notRecorded !== void 0 && notRecorded !== "" ? `
|
|
12817
13540
|
|
|
@@ -12819,7 +13542,7 @@ ${disclosures.map((d) => `- ${d}`).join("\n")}` : "";
|
|
|
12819
13542
|
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.
|
|
12820
13543
|
${notRecorded}` : "";
|
|
12821
13544
|
let fontProvisioning;
|
|
12822
|
-
if (
|
|
13545
|
+
if (existsSync32(manifestPath2)) {
|
|
12823
13546
|
const missingFams = unprovisionedFamilies(task.set);
|
|
12824
13547
|
const unprovided = unprovisionedFaces(task.set);
|
|
12825
13548
|
const weightOnly = missingFams.length === 0;
|
|
@@ -12841,7 +13564,7 @@ ${notRecorded}` : "";
|
|
|
12841
13564
|
};
|
|
12842
13565
|
}
|
|
12843
13566
|
}
|
|
12844
|
-
const pinsResult = composedPins(task.set, [opts.library !== void 0 ?
|
|
13567
|
+
const pinsResult = composedPins(task.set, [opts.library !== void 0 ? path39.resolve(callerCwd, opts.library) : callerCwd]);
|
|
12845
13568
|
const jsxProp = ([k, v]) => typeof v === "string" ? `${k}=${JSON.stringify(v)}` : `${k}={${JSON.stringify(v)}}`;
|
|
12846
13569
|
const composedBlock = pinsResult.pins.length === 0 && pinsResult.issues.length === 0 ? "" : (pinsResult.pins.length > 0 ? `
|
|
12847
13570
|
|
|
@@ -12863,9 +13586,9 @@ ${pinsResult.issues.map((i) => `- ${i}`).join("\n")}` : "");
|
|
|
12863
13586
|
|
|
12864
13587
|
=== TASK PAYLOAD (recorded truth, verbatim) ===
|
|
12865
13588
|
${segments}`;
|
|
12866
|
-
const payloadFile =
|
|
12867
|
-
const candidateDirSuggestion =
|
|
12868
|
-
mkdirSync8(
|
|
13589
|
+
const payloadFile = path39.resolve(callerCwd, opts.out ?? `tendril-out/${name}-brief.md`);
|
|
13590
|
+
const candidateDirSuggestion = path39.resolve(callerCwd, `tendril-out/${name}-candidate`);
|
|
13591
|
+
mkdirSync8(path39.dirname(payloadFile), { recursive: true });
|
|
12869
13592
|
writeFileSync12(payloadFile, payload);
|
|
12870
13593
|
emitData(
|
|
12871
13594
|
opts,
|
|
@@ -12912,7 +13635,7 @@ ${segments}`;
|
|
|
12912
13635
|
// command must search the same bundle roots the pins came
|
|
12913
13636
|
// from, or the oracle and the brief describe different worlds.
|
|
12914
13637
|
`Run \`${tendrilCommand(
|
|
12915
|
-
`engine score ${quoteArg(ref)} ${quoteArg(candidateDirSuggestion)} --bar ${opts.bar} --model ${opts.model ?? "<declared-model>"}${opts.library !== void 0 ? ` --library ${quoteArg(
|
|
13638
|
+
`engine score ${quoteArg(ref)} ${quoteArg(candidateDirSuggestion)} --bar ${opts.bar} --model ${opts.model ?? "<declared-model>"}${opts.library !== void 0 ? ` --library ${quoteArg(path39.resolve(callerCwd, opts.library))}` : ""} --json`
|
|
12916
13639
|
)}\` \u2014 the CLI is the sole judge, and it refuses to score an undeclared model. If you wrote the bundle elsewhere, pass that directory instead.`,
|
|
12917
13640
|
"Apply the returned feedback and re-score. Stop when all checks pass. Two consecutive non-improving scores mean INSPECT the verify-evidence diffs before deciding \u2014 stop only when inspection yields no fix hypothesis (a measured run was byte-identical twice, then went 27/27 after reading the diffs).",
|
|
12918
13641
|
"Never edit recording sets; never claim scores yourself \u2014 only the score command's output counts."
|
|
@@ -12927,8 +13650,8 @@ ${segments}`;
|
|
|
12927
13650
|
);
|
|
12928
13651
|
}
|
|
12929
13652
|
function appendScoreHistory(candidateDir, entry) {
|
|
12930
|
-
const file =
|
|
12931
|
-
const starts =
|
|
13653
|
+
const file = path39.join(candidateDir, "score-history.jsonl");
|
|
13654
|
+
const starts = existsSync32(file) ? readFileSync28(file, "utf8").split("\n").filter((l) => l.includes('"event":"round-start"')).length : 0;
|
|
12932
13655
|
const round = entry.event === "round-start" ? starts + 1 : Math.max(1, starts);
|
|
12933
13656
|
appendFileSync(file, `${JSON.stringify({ at: (/* @__PURE__ */ new Date()).toISOString(), round, ...entry })}
|
|
12934
13657
|
`);
|
|
@@ -12936,9 +13659,9 @@ function appendScoreHistory(candidateDir, entry) {
|
|
|
12936
13659
|
async function runEngineScore(opts) {
|
|
12937
13660
|
requireEntitlement(opts);
|
|
12938
13661
|
const callerCwd = process.env["INIT_CWD"] ?? process.cwd();
|
|
12939
|
-
const candidateDir =
|
|
12940
|
-
const { task, name, apiPin, interactionEvidence } = resolveEngineTask(opts, callerCwd);
|
|
12941
|
-
if (!
|
|
13662
|
+
const candidateDir = path39.resolve(callerCwd, opts.candidateDir);
|
|
13663
|
+
const { task, name, apiPin, interactionEvidence, unmappedInteractionEvidence: interactionEvidenceUnmapped, satisfiabilityDemoted, expertContract } = resolveEngineTask(opts, callerCwd);
|
|
13664
|
+
if (!existsSync32(candidateDir)) {
|
|
12942
13665
|
fail(opts, ExitCode.InputValidation, {
|
|
12943
13666
|
error: `candidate directory not found: ${candidateDir}`,
|
|
12944
13667
|
code: "candidate-missing",
|
|
@@ -12963,10 +13686,10 @@ async function runEngineScore(opts) {
|
|
|
12963
13686
|
for (const g of missingWeights(task.set)) {
|
|
12964
13687
|
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)`);
|
|
12965
13688
|
}
|
|
12966
|
-
if (opts.rebind !== true &&
|
|
13689
|
+
if (opts.rebind !== true && existsSync32(path39.join(candidateDir, "component.json"))) {
|
|
12967
13690
|
const prior = (() => {
|
|
12968
13691
|
try {
|
|
12969
|
-
const read = readBundleManifest(
|
|
13692
|
+
const read = readBundleManifest(readFileSync28(path39.join(candidateDir, "component.json"), "utf8"));
|
|
12970
13693
|
return read.manifest === void 0 ? { unreadable: true } : { hash: read.manifest.provenance.recordingSet.hash, path: read.manifest.provenance.recordingSet.path };
|
|
12971
13694
|
} catch {
|
|
12972
13695
|
return { unreadable: true };
|
|
@@ -12988,18 +13711,25 @@ async function runEngineScore(opts) {
|
|
|
12988
13711
|
}
|
|
12989
13712
|
}
|
|
12990
13713
|
const bar = BARS3[opts.bar];
|
|
12991
|
-
const evidenceDir =
|
|
13714
|
+
const evidenceDir = path39.join(candidateDir, "verify-evidence");
|
|
12992
13715
|
const scores = await scoreBundleForTask(task, candidateDir, bar, { evidenceDir, onProgress: emitProgress });
|
|
12993
13716
|
const parity = await checkHoverParity(task, candidateDir, task.configs);
|
|
12994
|
-
const
|
|
13717
|
+
const occlusionRows = await checkSiblingOcclusion(task, candidateDir, candidateCss(candidateDir), { authority: task.configs });
|
|
13718
|
+
const occlusionFailures = occlusionRows.filter((o) => !o.pass);
|
|
13719
|
+
const scorePins = composedPins(task.set, [opts.library !== void 0 ? path39.resolve(callerCwd, opts.library) : callerCwd]);
|
|
12995
13720
|
const composition = composedChecks(candidateDir, task.entry, scorePins.pins);
|
|
12996
13721
|
const behaviors = [...await checkBehaviors(task, candidateDir), ...parity, ...composition];
|
|
12997
13722
|
const parityCoverage = parity.length > 0 ? `${parity.filter((p) => p.pass).length}/${parity.length} hover-forced configs` : "not applicable (no hover-forced configs in this set)";
|
|
12998
13723
|
const obj = objective(scores, behaviors);
|
|
12999
13724
|
const total = scores.length + behaviors.length;
|
|
13000
|
-
const quality = await checkBundleQuality(
|
|
13001
|
-
|
|
13002
|
-
|
|
13725
|
+
const quality = await checkBundleQuality(
|
|
13726
|
+
candidateDir,
|
|
13727
|
+
task.entry,
|
|
13728
|
+
{ dir: task.set, reps: task.configs.map((c) => c.rep) },
|
|
13729
|
+
void 0,
|
|
13730
|
+
{ recordedTruth: motionTruthFor(task.set).state === "recorded" },
|
|
13731
|
+
expertContract
|
|
13732
|
+
);
|
|
13003
13733
|
const qualityFeedback = quality.findings.length === 0 && !quality.tokensAbsent ? "" : `
|
|
13004
13734
|
|
|
13005
13735
|
QUALITY (does not affect the bar \u2014 fix alongside the failing configs):
|
|
@@ -13008,7 +13738,7 @@ ${[
|
|
|
13008
13738
|
...quality.tokensAbsent ? ["- no design tokens: no tokens.css and no var(--\u2026) reference; every value is hardcoded"] : []
|
|
13009
13739
|
].join("\n")}`;
|
|
13010
13740
|
const absentAtCertBar = opts.bar === "cert" ? scores.filter((sc) => (sc.absentInk?.length ?? 0) > 0).map((sc) => sc.rep) : [];
|
|
13011
|
-
const allPass = obj[0] === total && total > 0 && absentAtCertBar.length === 0;
|
|
13741
|
+
const allPass = obj[0] === total && total > 0 && absentAtCertBar.length === 0 && occlusionFailures.length === 0;
|
|
13012
13742
|
const certBar = BARS3["cert"];
|
|
13013
13743
|
const parityDemoted = new Set(parity.filter((p) => !p.pass).map((p) => p.id.replace(/^parity:/, "")));
|
|
13014
13744
|
const certifiedReps = substitutedFamilies.length > 0 ? [] : scores.filter((sc) => tierOf(sc, certBar) === "certified" && !parityDemoted.has(sc.rep) && (sc.absentInk === void 0 || sc.absentInk.length === 0)).map((sc) => sc.rep);
|
|
@@ -13025,11 +13755,14 @@ METRIC DEADBAND (read before iterating on near-misses): the scored similarity/in
|
|
|
13025
13755
|
const stampNotice = `
|
|
13026
13756
|
|
|
13027
13757
|
PROVENANCE STAMP: this scoring call itself (the Tendril CLI) just wrote/refreshed a comment on line 1 of styles.css carrying these scores, marked non-authoritative. If your editor or host reports styles.css was modified externally, that modification is this scorer \u2014 expected, not tampering. Keep the comment; it self-invalidates on any edit and \`tendril verify\` recomputes it.`;
|
|
13758
|
+
const occlusionFeedback = occlusionFailures.length > 0 ? `
|
|
13759
|
+
|
|
13760
|
+
OCCLUSION FAILURES (these block allPass and verify GATES on them): ${occlusionFailures.map((o) => `${o.id}${o.detail !== void 0 ? ` \u2014 ${o.detail}` : ""}`).join("; ")}` : "";
|
|
13028
13761
|
const compositionFeedback = scorePins.issues.length > 0 ? `
|
|
13029
13762
|
|
|
13030
13763
|
COMPOSITION PINS (ADR-013): ${scorePins.issues.length} confirmed pair(s) could not be pinned \u2014 verify will disclose each as confirmed-but-not-composed; not fixable by editing the candidate:
|
|
13031
13764
|
${scorePins.issues.map((i) => `- ${i}`).join("\n")}` : "";
|
|
13032
|
-
const feedback = buildFeedback(scores, behaviors, bar, "files") + certificationFeedback + compositionFeedback + stampNotice + qualityFeedback;
|
|
13765
|
+
const feedback = buildFeedback(scores, behaviors, bar, "files") + certificationFeedback + compositionFeedback + occlusionFeedback + stampNotice + qualityFeedback;
|
|
13033
13766
|
const emitted = emitBundleV1({
|
|
13034
13767
|
bundleDir: candidateDir,
|
|
13035
13768
|
task,
|
|
@@ -13055,7 +13788,7 @@ ${scorePins.issues.map((i) => `- ${i}`).join("\n")}` : "";
|
|
|
13055
13788
|
});
|
|
13056
13789
|
const coverage = interactionCoverage(behaviors);
|
|
13057
13790
|
const operability = operabilityReport({ behaviors, interactionEvidence });
|
|
13058
|
-
const evidenceUnverified = (interactionEvidence
|
|
13791
|
+
const evidenceUnverified = (interactionEvidence ?? []).filter((e) => !(satisfiabilityDemoted ?? []).includes(e)).length > 0 && coverage.interactionChecks === 0;
|
|
13059
13792
|
const scoreMotion = motionDisclosure(candidateDir, task.set);
|
|
13060
13793
|
emitData(
|
|
13061
13794
|
opts,
|
|
@@ -13103,6 +13836,13 @@ ${scorePins.issues.map((i) => `- ${i}`).join("\n")}` : "";
|
|
|
13103
13836
|
// ADR-016 §3: same disclosure as verify — the oracle must say
|
|
13104
13837
|
// what verify will say about motion too.
|
|
13105
13838
|
motion: scoreMotion,
|
|
13839
|
+
// Cycle B M3 + M2: occlusion in the oracle's JSON, and the
|
|
13840
|
+
// pixel-only poses on the VERDICT surface (they rode stderr
|
|
13841
|
+
// while verify's report carried them — the run-22 focus channel,
|
|
13842
|
+
// now fixed on the score side too).
|
|
13843
|
+
occlusion: occlusionRows,
|
|
13844
|
+
occlusionCheck: occlusionReport(occlusionRows),
|
|
13845
|
+
...interactionEvidenceUnmapped !== void 0 && interactionEvidenceUnmapped.length > 0 ? { pixelOnlyInteractionPoses: interactionEvidenceUnmapped } : {},
|
|
13106
13846
|
allPass,
|
|
13107
13847
|
environment: { ...environmentStamp(taskFontFamilies(task.set)), ruler: cliVersion() },
|
|
13108
13848
|
...lcdTextEnabled() ? { environmentOverrides: ["enable-lcd-text"] } : {},
|
|
@@ -13118,9 +13858,15 @@ ${scorePins.issues.map((i) => `- ${i}`).join("\n")}` : "";
|
|
|
13118
13858
|
for (const b of behaviors) process.stdout.write(`${b.pass ? "PASS" : "FAIL"} ${b.id}${b.detail !== void 0 ? ` [${b.detail}]` : ""}
|
|
13119
13859
|
`);
|
|
13120
13860
|
for (const i of scorePins.issues) process.stdout.write(`PIN ISSUE ${i}
|
|
13861
|
+
`);
|
|
13862
|
+
for (const o of occlusionRows) process.stdout.write(`${o.pass ? "PASS" : "FAIL"} ${o.id}${o.detail !== void 0 ? ` [${o.detail}]` : ""}
|
|
13121
13863
|
`);
|
|
13122
13864
|
if (scoreMotion.present) process.stdout.write(`MOTION ${scoreMotion.note}
|
|
13123
13865
|
`);
|
|
13866
|
+
for (const p of interactionEvidenceUnmapped ?? []) {
|
|
13867
|
+
process.stdout.write(`PIXEL-ONLY ${p} \u2014 the recording proves this pose is interactive, but no operability check covers it
|
|
13868
|
+
`);
|
|
13869
|
+
}
|
|
13124
13870
|
process.stdout.write(`
|
|
13125
13871
|
${obj[0]}/${total} \xB7 certified ${certifiedReps.length}/${total} \xB7 floor=${obj[1].toFixed(3)} \xB7 mean=${obj[2].toFixed(3)} \xB7 evidence: ${evidenceDir}
|
|
13126
13872
|
`);
|
|
@@ -13134,6 +13880,9 @@ ${obj[0]}/${total} \xB7 certified ${certifiedReps.length}/${total} \xB7 floor=${
|
|
|
13134
13880
|
}
|
|
13135
13881
|
}
|
|
13136
13882
|
);
|
|
13883
|
+
if (occlusionFailures.length > 0) {
|
|
13884
|
+
warn(opts, `${occlusionFailures.length} occlusion failure(s) block allPass (${occlusionFailures.map((o) => o.id).join(", ")}) \u2014 the OCCLUSION FAILURES feedback block carries each cause`);
|
|
13885
|
+
}
|
|
13137
13886
|
if (absentAtCertBar.length > 0) {
|
|
13138
13887
|
warn(opts, `${absentAtCertBar.length} config(s) carry absent-ink clusters (${absentAtCertBar.join(", ")}) \u2014 at --bar cert these fail the run; the MISSING FEATURES block above names each one`);
|
|
13139
13888
|
}
|
|
@@ -13179,11 +13928,11 @@ var codeconnect_exports = {};
|
|
|
13179
13928
|
__export(codeconnect_exports, {
|
|
13180
13929
|
runCodeConnect: () => runCodeConnect
|
|
13181
13930
|
});
|
|
13182
|
-
import { existsSync as
|
|
13183
|
-
import
|
|
13931
|
+
import { existsSync as existsSync33, readFileSync as readFileSync29, writeFileSync as writeFileSync13 } from "node:fs";
|
|
13932
|
+
import path40 from "node:path";
|
|
13184
13933
|
function runCodeConnect(opts) {
|
|
13185
13934
|
const callerCwd = process.env["INIT_CWD"] ?? process.cwd();
|
|
13186
|
-
const bundleDir =
|
|
13935
|
+
const bundleDir = path40.resolve(callerCwd, opts.bundleDir);
|
|
13187
13936
|
let url;
|
|
13188
13937
|
try {
|
|
13189
13938
|
url = new URL(opts.figmaUrl);
|
|
@@ -13199,7 +13948,7 @@ function runCodeConnect(opts) {
|
|
|
13199
13948
|
}
|
|
13200
13949
|
let manifest;
|
|
13201
13950
|
try {
|
|
13202
|
-
const read = readBundleManifest(
|
|
13951
|
+
const read = readBundleManifest(readFileSync29(path40.join(bundleDir, "component.json"), "utf8"));
|
|
13203
13952
|
if (read.manifest === void 0) throw new Error(read.issues.map((i) => i.message).join("; ") || "no component.json");
|
|
13204
13953
|
manifest = read.manifest;
|
|
13205
13954
|
} catch (err) {
|
|
@@ -13209,8 +13958,8 @@ function runCodeConnect(opts) {
|
|
|
13209
13958
|
remediation: "Point at a bundle produced by the Tendril pipeline (it carries component.json), and verify it first."
|
|
13210
13959
|
});
|
|
13211
13960
|
}
|
|
13212
|
-
const setDir =
|
|
13213
|
-
if (!
|
|
13961
|
+
const setDir = path40.resolve(callerCwd, opts.set ?? manifest.provenance.recordingSet.path);
|
|
13962
|
+
if (!existsSync33(path40.join(setDir, "recording-set.json"))) {
|
|
13214
13963
|
fail(opts, ExitCode.InputValidation, {
|
|
13215
13964
|
error: `recording set not found at ${setDir}`,
|
|
13216
13965
|
code: "codeconnect-no-set",
|
|
@@ -13231,10 +13980,10 @@ function runCodeConnect(opts) {
|
|
|
13231
13980
|
const component = api.component;
|
|
13232
13981
|
const recManifest = loadManifest(setDir);
|
|
13233
13982
|
const poseNames = recManifest.latticeNames ?? recManifest.reps.map((r) => {
|
|
13234
|
-
const meta =
|
|
13235
|
-
if (!
|
|
13983
|
+
const meta = path40.join(setDir, r.slug, "get_metadata.json");
|
|
13984
|
+
if (!existsSync33(meta)) return void 0;
|
|
13236
13985
|
try {
|
|
13237
|
-
return /name="([^"]*)"/.exec(envelopeTextContent(JSON.parse(
|
|
13986
|
+
return /name="([^"]*)"/.exec(envelopeTextContent(JSON.parse(readFileSync29(meta, "utf8"))))?.[1];
|
|
13238
13987
|
} catch {
|
|
13239
13988
|
return void 0;
|
|
13240
13989
|
}
|
|
@@ -13256,6 +14005,7 @@ function runCodeConnect(opts) {
|
|
|
13256
14005
|
}
|
|
13257
14006
|
const boolProps = api.props.filter((p) => p.kind === "boolean");
|
|
13258
14007
|
const pixelOnly = [];
|
|
14008
|
+
const derivedTokenFragments = [];
|
|
13259
14009
|
const axisLines = [];
|
|
13260
14010
|
const fragmentVars = [];
|
|
13261
14011
|
for (const [axis, values] of Object.entries(axisDomains)) {
|
|
@@ -13270,8 +14020,11 @@ function runCodeConnect(opts) {
|
|
|
13270
14020
|
fragment = owner.default === kv ? "" : ` ${owner.name}="${kv}"`;
|
|
13271
14021
|
} else if (owner?.kind === "boolean") {
|
|
13272
14022
|
fragment = ["true", "on", "yes"].includes(kv) ? ` ${owner.name}` : "";
|
|
14023
|
+
} else if (api.derivedPoseProps[kv] !== void 0) {
|
|
14024
|
+
fragment = Object.entries(api.derivedPoseProps[kv]).map(([name, v]) => ` ${name}={${JSON.stringify(v)}}`).join("");
|
|
13273
14025
|
} else if (api.forcedStates.includes(kv)) {
|
|
13274
14026
|
fragment = ` data-tendril-state="${kv}"`;
|
|
14027
|
+
if (api.derivedStateTokens.includes(kv)) derivedTokenFragments.push(`${axis}=${value}`);
|
|
13275
14028
|
} else if (boolProps.some((p) => p.name === kv || kebab4(p.name) === kv)) {
|
|
13276
14029
|
fragment = ` ${boolProps.find((p) => p.name === kv || kebab4(p.name) === kv).name}`;
|
|
13277
14030
|
} else if (value === axisDefault) {
|
|
@@ -13284,7 +14037,10 @@ function runCodeConnect(opts) {
|
|
|
13284
14037
|
fail(opts, ExitCode.InputValidation, {
|
|
13285
14038
|
error: `axis "${axis}" value "${value}" maps to nothing in the authored API \u2014 an unmapped value silently breaks the Dev Mode snippet`,
|
|
13286
14039
|
code: "codeconnect-unmapped-value",
|
|
13287
|
-
|
|
14040
|
+
// Two distinct causes (review: the old text blamed the
|
|
14041
|
+
// recording even when the pose WAS recorded and authoring
|
|
14042
|
+
// simply had no mapping channel — an authoring gap is ours).
|
|
14043
|
+
remediation: "If this pose was never recorded, record it (the full matrix is the default plan) and re-emit. If it IS recorded, the authored API has no mapping channel for it \u2014 an authoring gap, not a recording gap: report it rather than re-recording."
|
|
13288
14044
|
});
|
|
13289
14045
|
}
|
|
13290
14046
|
entries.push(`${q(value)}: ${q(fragment)}`);
|
|
@@ -13292,7 +14048,7 @@ function runCodeConnect(opts) {
|
|
|
13292
14048
|
axisLines.push(`const ${varName} = instance.getEnum(${q(axis)}, { ${entries.join(", ")} })`);
|
|
13293
14049
|
fragmentVars.push(varName);
|
|
13294
14050
|
}
|
|
13295
|
-
const entryRel =
|
|
14051
|
+
const entryRel = path40.relative(callerCwd, path40.join(bundleDir, manifest.entry));
|
|
13296
14052
|
const trust = manifest.trustStatement.split("\n")[0] ?? "";
|
|
13297
14053
|
const lines = [
|
|
13298
14054
|
`// url=${opts.figmaUrl}`,
|
|
@@ -13301,6 +14057,9 @@ function runCodeConnect(opts) {
|
|
|
13301
14057
|
"// Generated by Tendril. Claims below are the bundle's self-reported verification \u2014 recompute them free and offline with: tendril verify",
|
|
13302
14058
|
`// ${trust}`,
|
|
13303
14059
|
...pixelOnly.length > 0 ? [`// PIXEL-VERIFIED ONLY (no operable mapping): ${pixelOnly.join(", ")}`] : [],
|
|
14060
|
+
...derivedTokenFragments.length > 0 ? [
|
|
14061
|
+
`// DERIVED STATES (${derivedTokenFragments.join(", ")}): the data-tendril-state fragment forces the pose for preview parity only \u2014 in real use these states derive from focus/value on the control, never from an attribute you set`
|
|
14062
|
+
] : [],
|
|
13304
14063
|
`import figma from 'figma'`,
|
|
13305
14064
|
``,
|
|
13306
14065
|
`const instance = figma.selectedInstance`,
|
|
@@ -13313,7 +14072,7 @@ function runCodeConnect(opts) {
|
|
|
13313
14072
|
`}`,
|
|
13314
14073
|
``
|
|
13315
14074
|
].join("\n");
|
|
13316
|
-
const outFile =
|
|
14075
|
+
const outFile = path40.resolve(callerCwd, opts.out ?? path40.join(bundleDir, `${component}.figma.ts`));
|
|
13317
14076
|
writeFileSync13(outFile, lines);
|
|
13318
14077
|
emitData(
|
|
13319
14078
|
opts,
|
|
@@ -13353,17 +14112,17 @@ var init_codeconnect = __esm({
|
|
|
13353
14112
|
|
|
13354
14113
|
// packages/mcp/src/server.ts
|
|
13355
14114
|
import { createHash as createHash7 } from "node:crypto";
|
|
13356
|
-
import { existsSync as
|
|
14115
|
+
import { existsSync as existsSync34, mkdtempSync as mkdtempSync3, readFileSync as readFileSync30, readdirSync as readdirSync13, writeFileSync as writeFileSync14 } from "node:fs";
|
|
13357
14116
|
import os7 from "node:os";
|
|
13358
|
-
import
|
|
14117
|
+
import path41 from "node:path";
|
|
13359
14118
|
import { fileURLToPath as fileURLToPath6 } from "node:url";
|
|
13360
14119
|
import { z as z13 } from "zod";
|
|
13361
14120
|
function sourceHash() {
|
|
13362
|
-
const dir =
|
|
14121
|
+
const dir = path41.dirname(fileURLToPath6(import.meta.url));
|
|
13363
14122
|
const h = createHash7("sha256");
|
|
13364
14123
|
for (const f of readdirSync13(dir).filter((n) => n.endsWith(".ts")).sort()) {
|
|
13365
14124
|
h.update(f);
|
|
13366
|
-
h.update(
|
|
14125
|
+
h.update(readFileSync30(path41.join(dir, f)));
|
|
13367
14126
|
}
|
|
13368
14127
|
return h.digest("hex").slice(0, 16);
|
|
13369
14128
|
}
|
|
@@ -13371,10 +14130,10 @@ var REPO_ROOT3, CLI_BIN, BUNDLED_CLI, CLI_SPAWN, str, optStr, TOOLS, BOOT_HASH;
|
|
|
13371
14130
|
var init_server = __esm({
|
|
13372
14131
|
"packages/mcp/src/server.ts"() {
|
|
13373
14132
|
"use strict";
|
|
13374
|
-
REPO_ROOT3 =
|
|
13375
|
-
CLI_BIN =
|
|
13376
|
-
BUNDLED_CLI =
|
|
13377
|
-
CLI_SPAWN =
|
|
14133
|
+
REPO_ROOT3 = path41.resolve(path41.dirname(fileURLToPath6(import.meta.url)), "..", "..", "..");
|
|
14134
|
+
CLI_BIN = path41.join(REPO_ROOT3, "packages", "cli", "src", "bin.ts");
|
|
14135
|
+
BUNDLED_CLI = path41.join(path41.dirname(fileURLToPath6(import.meta.url)), "tendril.js");
|
|
14136
|
+
CLI_SPAWN = existsSync34(BUNDLED_CLI) ? { cmd: process.execPath, prefix: [BUNDLED_CLI] } : { cmd: "npx", prefix: ["tsx", CLI_BIN] };
|
|
13378
14137
|
str = (d) => z13.string().describe(d);
|
|
13379
14138
|
optStr = (d) => z13.string().optional().describe(d);
|
|
13380
14139
|
TOOLS = [
|
|
@@ -13405,7 +14164,7 @@ var init_server = __esm({
|
|
|
13405
14164
|
const single = i["metadata"];
|
|
13406
14165
|
const parts = i["metadataParts"];
|
|
13407
14166
|
if (single !== void 0 || parts !== void 0) {
|
|
13408
|
-
const tmp =
|
|
14167
|
+
const tmp = path41.join(mkdtempSync3(path41.join(os7.tmpdir(), "tendril-envelope-")), "response.txt");
|
|
13409
14168
|
if (single !== void 0) {
|
|
13410
14169
|
writeFileSync14(tmp, single);
|
|
13411
14170
|
argvOut.push("--metadata-raw-file", tmp);
|
|
@@ -13488,7 +14247,7 @@ var init_server = __esm({
|
|
|
13488
14247
|
const bridge = (label, single, parts) => {
|
|
13489
14248
|
if (single !== void 0 && parts !== void 0) throw new Error(`pass at most one of \`${label}\` and \`${label}Parts\``);
|
|
13490
14249
|
if (single === void 0 && parts === void 0) return;
|
|
13491
|
-
const tmp =
|
|
14250
|
+
const tmp = path41.join(mkdtempSync3(path41.join(os7.tmpdir(), "tendril-envelope-")), "response.txt");
|
|
13492
14251
|
if (single !== void 0) {
|
|
13493
14252
|
writeFileSync14(tmp, single);
|
|
13494
14253
|
argvOut.push(`--${label}-file`, tmp);
|
|
@@ -13536,7 +14295,7 @@ var init_server = __esm({
|
|
|
13536
14295
|
const file = i["file"];
|
|
13537
14296
|
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)");
|
|
13538
14297
|
if (file !== void 0) return [...base, "--file", file];
|
|
13539
|
-
const tmp =
|
|
14298
|
+
const tmp = path41.join(mkdtempSync3(path41.join(os7.tmpdir(), "tendril-envelope-")), "response.txt");
|
|
13540
14299
|
if (text !== void 0) {
|
|
13541
14300
|
writeFileSync14(tmp, text);
|
|
13542
14301
|
return [...base, "--file", tmp, "--raw"];
|
|
@@ -13698,13 +14457,13 @@ __export(permissions_exports, {
|
|
|
13698
14457
|
runPermissions: () => runPermissions,
|
|
13699
14458
|
writeSelection: () => writeSelection
|
|
13700
14459
|
});
|
|
13701
|
-
import { existsSync as
|
|
14460
|
+
import { existsSync as existsSync35, mkdirSync as mkdirSync9, readFileSync as readFileSync31, writeFileSync as writeFileSync15 } from "node:fs";
|
|
13702
14461
|
import os8 from "node:os";
|
|
13703
|
-
import
|
|
14462
|
+
import path42 from "node:path";
|
|
13704
14463
|
function mergeAllowlist(file, entries, denyEntries = []) {
|
|
13705
14464
|
let settings = {};
|
|
13706
|
-
if (
|
|
13707
|
-
settings = JSON.parse(
|
|
14465
|
+
if (existsSync35(file) && readFileSync31(file, "utf8").trim() !== "") {
|
|
14466
|
+
settings = JSON.parse(readFileSync31(file, "utf8"));
|
|
13708
14467
|
if (settings === null || typeof settings !== "object" || Array.isArray(settings)) throw new Error("settings root is not an object");
|
|
13709
14468
|
}
|
|
13710
14469
|
const permissions = settings["permissions"] ??= {};
|
|
@@ -13724,7 +14483,7 @@ function mergeAllowlist(file, entries, denyEntries = []) {
|
|
|
13724
14483
|
}
|
|
13725
14484
|
if (added.length > 0 || denyAdded.length > 0) {
|
|
13726
14485
|
allow.push(...added);
|
|
13727
|
-
mkdirSync9(
|
|
14486
|
+
mkdirSync9(path42.dirname(file), { recursive: true });
|
|
13728
14487
|
writeFileSync15(file, `${JSON.stringify(settings, null, 2)}
|
|
13729
14488
|
`);
|
|
13730
14489
|
}
|
|
@@ -13789,7 +14548,7 @@ async function runPermissions(flags) {
|
|
|
13789
14548
|
}
|
|
13790
14549
|
if (flags.write) {
|
|
13791
14550
|
const base = process.env["INIT_CWD"] ?? process.cwd();
|
|
13792
|
-
const file = flags.user ?
|
|
14551
|
+
const file = flags.user ? path42.join(os8.homedir(), ".claude", "settings.json") : path42.join(base, ".claude", "settings.local.json");
|
|
13793
14552
|
const { entries, denyEntries } = writeSelection(result, flags.user === true);
|
|
13794
14553
|
if (flags.dryRun) {
|
|
13795
14554
|
emitData(flags, { file, wouldAdd: entries, wouldDeny: denyEntries }, () => {
|
|
@@ -13939,8 +14698,8 @@ __export(compose_exports, {
|
|
|
13939
14698
|
runCompose: () => runCompose
|
|
13940
14699
|
});
|
|
13941
14700
|
import { createHash as createHash8 } from "node:crypto";
|
|
13942
|
-
import { existsSync as
|
|
13943
|
-
import
|
|
14701
|
+
import { existsSync as existsSync36, readFileSync as readFileSync32 } from "node:fs";
|
|
14702
|
+
import path43 from "node:path";
|
|
13944
14703
|
function substitutionPairs(edges, hostSet) {
|
|
13945
14704
|
const pairs = /* @__PURE__ */ new Map();
|
|
13946
14705
|
for (const e of edges) {
|
|
@@ -13969,7 +14728,7 @@ function runCompose(flags) {
|
|
|
13969
14728
|
return;
|
|
13970
14729
|
}
|
|
13971
14730
|
const base = process.env["INIT_CWD"] ?? process.cwd();
|
|
13972
|
-
const roots = flags.library !== void 0 && flags.library.length > 0 ? flags.library.map((d) =>
|
|
14731
|
+
const roots = flags.library !== void 0 && flags.library.length > 0 ? flags.library.map((d) => path43.resolve(base, d)) : [base];
|
|
13973
14732
|
if (flags.set === void 0 && (flags.confirmCompositions === true || flags.decline !== void 0 && flags.decline.length > 0)) {
|
|
13974
14733
|
fail(flags, ExitCode.InputValidation, {
|
|
13975
14734
|
error: "--confirm-compositions/--decline require --set <host> \u2014 a decision needs the host set it decides for",
|
|
@@ -13978,7 +14737,7 @@ function runCompose(flags) {
|
|
|
13978
14737
|
});
|
|
13979
14738
|
}
|
|
13980
14739
|
if (flags.set !== void 0) {
|
|
13981
|
-
runComposeConfirm(flags,
|
|
14740
|
+
runComposeConfirm(flags, path43.resolve(base, flags.set), roots);
|
|
13982
14741
|
return;
|
|
13983
14742
|
}
|
|
13984
14743
|
const index = buildComposeIndex(roots);
|
|
@@ -13996,7 +14755,7 @@ function runCompose(flags) {
|
|
|
13996
14755
|
}
|
|
13997
14756
|
let lastHost = "";
|
|
13998
14757
|
for (const e of edges) {
|
|
13999
|
-
const host = `${
|
|
14758
|
+
const host = `${path43.basename(e.hostSet)}`;
|
|
14000
14759
|
if (host !== lastHost) {
|
|
14001
14760
|
process.stdout.write(`
|
|
14002
14761
|
${host}
|
|
@@ -14004,7 +14763,7 @@ ${host}
|
|
|
14004
14763
|
lastHost = host;
|
|
14005
14764
|
}
|
|
14006
14765
|
const partners = e.partners.map((p) => p.displayName).join(" / ") || "\u2014";
|
|
14007
|
-
const pose = e.pose !== void 0 ? ` pose=${e.pose.variantNodeId} (${e.pose.reps.map((r) => `${
|
|
14766
|
+
const pose = e.pose !== void 0 ? ` pose=${e.pose.variantNodeId} (${e.pose.reps.map((r) => `${path43.basename(r.dir)}:${r.slug}`).join(", ")})` : "";
|
|
14008
14767
|
process.stdout.write(` ${e.kind.toUpperCase().padEnd(12)} ${e.hostRep}/${e.instanceId} "${e.instanceName}" \u2192 ${partners}${pose}
|
|
14009
14768
|
`);
|
|
14010
14769
|
for (const d of e.disclosures) process.stdout.write(` \xB7 ${d}
|
|
@@ -14016,14 +14775,14 @@ ${NOTE}
|
|
|
14016
14775
|
});
|
|
14017
14776
|
}
|
|
14018
14777
|
function runComposeConfirm(flags, hostSet, roots) {
|
|
14019
|
-
if (!
|
|
14778
|
+
if (!existsSync36(path43.join(hostSet, "recording-set.json"))) {
|
|
14020
14779
|
fail(flags, ExitCode.InputValidation, {
|
|
14021
14780
|
error: `no recording-set.json in ${hostSet}`,
|
|
14022
14781
|
code: "no-recording-set",
|
|
14023
14782
|
remediation: "Point --set at a recorded host set (the directory holding recording-set.json)."
|
|
14024
14783
|
});
|
|
14025
14784
|
}
|
|
14026
|
-
const scanRoots = [.../* @__PURE__ */ new Set([...roots,
|
|
14785
|
+
const scanRoots = [.../* @__PURE__ */ new Set([...roots, path43.dirname(hostSet)])];
|
|
14027
14786
|
const index = buildComposeIndex(scanRoots);
|
|
14028
14787
|
const edges = composeReport(index);
|
|
14029
14788
|
const pairs = substitutionPairs(edges, hostSet);
|
|
@@ -14104,7 +14863,7 @@ PAIR ${p.displayName} [${p.key}]${p.figmaFile !== void 0 ? ` file ${p.figmaFile}
|
|
|
14104
14863
|
// full recording-set hash join lands with pin authoring, where
|
|
14105
14864
|
// task configs exist.)
|
|
14106
14865
|
manifestSha256: Object.fromEntries(
|
|
14107
|
-
p.partnerDirs.map((d) => [
|
|
14866
|
+
p.partnerDirs.map((d) => [path43.relative(hostSet, d), createHash8("sha256").update(readFileSync32(path43.join(d, "recording-set.json"))).digest("hex")])
|
|
14108
14867
|
)
|
|
14109
14868
|
},
|
|
14110
14869
|
instances: p.instances,
|
|
@@ -14145,7 +14904,7 @@ var init_compose2 = __esm({
|
|
|
14145
14904
|
],
|
|
14146
14905
|
output: {
|
|
14147
14906
|
sets: "string[] \u2014 recording sets indexed (--list)",
|
|
14148
|
-
edges: "per-instance edges: kind (substitution | nested | ask | proposal | external), partners, pose (variant node id + per-set rep slugs), disclosures (--list)",
|
|
14907
|
+
edges: "per-instance edges: kind (substitution | nested | ask | proposal | external | hidden \u2014 hidden = invisible in every recorded pose, disclosed and never confirmable), partners, pose (variant node id + per-set rep slugs), disclosures (--list)",
|
|
14149
14908
|
openPairs: "with --set: undecided id-backed pairs awaiting a human decision",
|
|
14150
14909
|
standing: "with --set: persisted confirmations/declines (asked once)",
|
|
14151
14910
|
note: "string \u2014 what this command does NOT do"
|
|
@@ -14163,24 +14922,24 @@ __export(inspect_exports, {
|
|
|
14163
14922
|
INSPECT_DESCRIPTION: () => INSPECT_DESCRIPTION,
|
|
14164
14923
|
runInspect: () => runInspect
|
|
14165
14924
|
});
|
|
14166
|
-
import { existsSync as
|
|
14167
|
-
import
|
|
14925
|
+
import { existsSync as existsSync37, readFileSync as readFileSync33, writeFileSync as writeFileSync16 } from "node:fs";
|
|
14926
|
+
import path44 from "node:path";
|
|
14168
14927
|
async function runInspect(opts) {
|
|
14169
14928
|
if (opts.describe) {
|
|
14170
14929
|
printDescription(INSPECT_DESCRIPTION);
|
|
14171
14930
|
return;
|
|
14172
14931
|
}
|
|
14173
|
-
const bundleDir =
|
|
14174
|
-
const evidenceDir =
|
|
14175
|
-
const manifestPath2 =
|
|
14176
|
-
if (!
|
|
14932
|
+
const bundleDir = path44.resolve(opts.bundleDir);
|
|
14933
|
+
const evidenceDir = path44.join(bundleDir, "verify-evidence");
|
|
14934
|
+
const manifestPath2 = path44.join(bundleDir, "component.json");
|
|
14935
|
+
if (!existsSync37(evidenceDir) || !existsSync37(manifestPath2)) {
|
|
14177
14936
|
fail(opts, ExitCode.InputValidation, {
|
|
14178
|
-
error: `nothing to inspect in ${bundleDir} \u2014 ${
|
|
14937
|
+
error: `nothing to inspect in ${bundleDir} \u2014 ${existsSync37(manifestPath2) ? "no verify-evidence directory" : "no component.json"}`,
|
|
14179
14938
|
code: "no-evidence",
|
|
14180
14939
|
remediation: `Run \`${tendrilCommand(`verify ${bundleDir}`)}\` first \u2014 inspect reads the ref/render evidence that run writes.`
|
|
14181
14940
|
});
|
|
14182
14941
|
}
|
|
14183
|
-
const { manifest } = readBundleManifest(
|
|
14942
|
+
const { manifest } = readBundleManifest(readFileSync33(manifestPath2, "utf8"));
|
|
14184
14943
|
if (manifest === void 0) {
|
|
14185
14944
|
fail(opts, ExitCode.InputValidation, {
|
|
14186
14945
|
error: "component.json did not parse as a bundle manifest",
|
|
@@ -14188,8 +14947,8 @@ async function runInspect(opts) {
|
|
|
14188
14947
|
remediation: "Re-emit the bundle (score/verify rewrite component.json), then re-run inspect."
|
|
14189
14948
|
});
|
|
14190
14949
|
}
|
|
14191
|
-
const setDir =
|
|
14192
|
-
const reps = Object.keys(manifest.propAdapter).filter((rep) =>
|
|
14950
|
+
const setDir = path44.resolve(opts.set ?? manifest.provenance.recordingSet.path);
|
|
14951
|
+
const reps = Object.keys(manifest.propAdapter).filter((rep) => existsSync37(path44.join(evidenceDir, `${rep}-ref.png`)) && existsSync37(path44.join(evidenceDir, `${rep}-render.png`)));
|
|
14193
14952
|
if (reps.length === 0) {
|
|
14194
14953
|
fail(opts, ExitCode.InputValidation, {
|
|
14195
14954
|
error: "verify-evidence holds no ref/render pairs for this bundle's configs",
|
|
@@ -14200,15 +14959,15 @@ async function runInspect(opts) {
|
|
|
14200
14959
|
let crops = 0;
|
|
14201
14960
|
const sections = [];
|
|
14202
14961
|
for (const rep of reps) {
|
|
14203
|
-
const ref = new Uint8Array(
|
|
14204
|
-
const render = new Uint8Array(
|
|
14962
|
+
const ref = new Uint8Array(readFileSync33(path44.join(evidenceDir, `${rep}-ref.png`)));
|
|
14963
|
+
const render = new Uint8Array(readFileSync33(path44.join(evidenceDir, `${rep}-render.png`)));
|
|
14205
14964
|
const nodes = smallSemanticNodes(setDir, rep, opts.maxArea ?? 1024).slice(0, 12);
|
|
14206
14965
|
const cells = [];
|
|
14207
14966
|
for (const [i, n] of nodes.entries()) {
|
|
14208
14967
|
const rect = { x: n.x, y: n.y, w: n.w, h: n.h };
|
|
14209
14968
|
try {
|
|
14210
|
-
writeFileSync16(
|
|
14211
|
-
writeFileSync16(
|
|
14969
|
+
writeFileSync16(path44.join(evidenceDir, `${rep}-inspect-${i}-ref.png`), zoomCrop(ref, rect));
|
|
14970
|
+
writeFileSync16(path44.join(evidenceDir, `${rep}-inspect-${i}-render.png`), zoomCrop(render, rect));
|
|
14212
14971
|
} catch {
|
|
14213
14972
|
continue;
|
|
14214
14973
|
}
|
|
@@ -14221,7 +14980,7 @@ async function runInspect(opts) {
|
|
|
14221
14980
|
`<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>`
|
|
14222
14981
|
);
|
|
14223
14982
|
}
|
|
14224
|
-
const sheet =
|
|
14983
|
+
const sheet = path44.join(evidenceDir, "inspect.html");
|
|
14225
14984
|
writeFileSync16(
|
|
14226
14985
|
sheet,
|
|
14227
14986
|
`<!doctype html><meta charset="utf-8"><title>${esc(manifest.name)} \u2014 tendril inspect</title><style>
|
|
@@ -14299,17 +15058,17 @@ __export(generate_recorded_exports, {
|
|
|
14299
15058
|
runGenerateRecorded: () => runGenerateRecorded
|
|
14300
15059
|
});
|
|
14301
15060
|
import { confirm as confirm3, isCancel as isCancel3 } from "@clack/prompts";
|
|
14302
|
-
import { existsSync as
|
|
14303
|
-
import
|
|
15061
|
+
import { existsSync as existsSync38, readFileSync as readFileSync34 } from "node:fs";
|
|
15062
|
+
import path45 from "node:path";
|
|
14304
15063
|
async function runGenerateRecorded(opts) {
|
|
14305
15064
|
const callerCwd = process.env["INIT_CWD"] ?? process.cwd();
|
|
14306
|
-
const outDirAbs =
|
|
14307
|
-
const recordedAsPath =
|
|
15065
|
+
const outDirAbs = path45.resolve(callerCwd, opts.out);
|
|
15066
|
+
const recordedAsPath = path45.resolve(callerCwd, opts.recorded);
|
|
14308
15067
|
let task;
|
|
14309
15068
|
let taskName;
|
|
14310
15069
|
let authoredApi;
|
|
14311
15070
|
let composition;
|
|
14312
|
-
const isSet =
|
|
15071
|
+
const isSet = existsSync38(path45.join(recordedAsPath, "recording-set.json"));
|
|
14313
15072
|
const registry = TASKS[opts.recorded];
|
|
14314
15073
|
if (registry !== void 0 && !isSet) {
|
|
14315
15074
|
task = registry;
|
|
@@ -14318,7 +15077,7 @@ async function runGenerateRecorded(opts) {
|
|
|
14318
15077
|
try {
|
|
14319
15078
|
const authored = authorTaskFromSet(recordedAsPath);
|
|
14320
15079
|
task = authored.task;
|
|
14321
|
-
taskName =
|
|
15080
|
+
taskName = path45.basename(recordedAsPath);
|
|
14322
15081
|
authoredApi = authored.api;
|
|
14323
15082
|
const roles = RolesSchema.safeParse(loadManifest(recordedAsPath).roles);
|
|
14324
15083
|
if (roles.success) composition = roles.data;
|
|
@@ -14352,7 +15111,7 @@ async function runGenerateRecorded(opts) {
|
|
|
14352
15111
|
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)`);
|
|
14353
15112
|
}
|
|
14354
15113
|
const missing = task.configs.filter(
|
|
14355
|
-
(c) => !
|
|
15114
|
+
(c) => !existsSync38(path45.join(task.set, c.rep, "get_screenshot.json")) || !existsSync38(path45.join(task.set, c.rep, "get_metadata.json")) || !existsSync38(path45.join(task.set, c.rep, "get_design_context.json"))
|
|
14356
15115
|
);
|
|
14357
15116
|
if (missing.length > 0) {
|
|
14358
15117
|
fail(opts, ExitCode.RecordingIncomplete, {
|
|
@@ -14422,8 +15181,8 @@ async function runGenerateRecorded(opts) {
|
|
|
14422
15181
|
` : `${line}
|
|
14423
15182
|
`);
|
|
14424
15183
|
if (opts.dryRun) {
|
|
14425
|
-
emitData(opts, { dryRun: true, task: taskName, model: modelId, consent, wouldWrite:
|
|
14426
|
-
process.stdout.write(`dry-run: nothing sent, nothing written (would write ${
|
|
15184
|
+
emitData(opts, { dryRun: true, task: taskName, model: modelId, consent, wouldWrite: path45.join(outDirAbs, taskName) }, () => {
|
|
15185
|
+
process.stdout.write(`dry-run: nothing sent, nothing written (would write ${path45.join(outDirAbs, taskName)})
|
|
14427
15186
|
`);
|
|
14428
15187
|
});
|
|
14429
15188
|
return;
|
|
@@ -14446,10 +15205,10 @@ async function runGenerateRecorded(opts) {
|
|
|
14446
15205
|
});
|
|
14447
15206
|
}
|
|
14448
15207
|
}
|
|
14449
|
-
const bundleDir =
|
|
14450
|
-
if (
|
|
15208
|
+
const bundleDir = path45.join(outDirAbs, taskName);
|
|
15209
|
+
if (existsSync38(path45.join(bundleDir, "component.json"))) {
|
|
14451
15210
|
try {
|
|
14452
|
-
const prior = readBundleManifest(
|
|
15211
|
+
const prior = readBundleManifest(readFileSync34(path45.join(bundleDir, "component.json"), "utf8")).manifest;
|
|
14453
15212
|
if (prior !== void 0 && prior.provenance.recordingSet.hash !== recordingSetHash(task.set, task.configs)) {
|
|
14454
15213
|
fail(opts, ExitCode.InputValidation, {
|
|
14455
15214
|
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`,
|
|
@@ -14615,7 +15374,7 @@ init_invocation();
|
|
|
14615
15374
|
init_output();
|
|
14616
15375
|
import { intro, isCancel, outro, password } from "@clack/prompts";
|
|
14617
15376
|
import fs from "node:fs";
|
|
14618
|
-
import
|
|
15377
|
+
import path27 from "node:path";
|
|
14619
15378
|
var INIT_DESCRIPTION = {
|
|
14620
15379
|
name: "init",
|
|
14621
15380
|
summary: "Configure Figma and OpenRouter credentials in .env (idempotent).",
|
|
@@ -14652,7 +15411,7 @@ async function runInit(flags) {
|
|
|
14652
15411
|
printDescription(INIT_DESCRIPTION);
|
|
14653
15412
|
return;
|
|
14654
15413
|
}
|
|
14655
|
-
const envPath =
|
|
15414
|
+
const envPath = path27.resolve(process.cwd(), ".env");
|
|
14656
15415
|
const existing = fs.existsSync(envPath) ? parseEnv(fs.readFileSync(envPath, "utf8")) : /* @__PURE__ */ new Map();
|
|
14657
15416
|
let figmaToken = flags.figmaToken ?? existing.get(ENV_KEYS.figma);
|
|
14658
15417
|
let openrouterKey = flags.openrouterKey ?? existing.get(ENV_KEYS.openrouter);
|
|
@@ -14673,7 +15432,7 @@ async function runInit(flags) {
|
|
|
14673
15432
|
next.set(ENV_KEYS.figma, figmaToken);
|
|
14674
15433
|
next.set(ENV_KEYS.openrouter, openrouterKey);
|
|
14675
15434
|
const changed = existing.get(ENV_KEYS.figma) !== next.get(ENV_KEYS.figma) || existing.get(ENV_KEYS.openrouter) !== next.get(ENV_KEYS.openrouter);
|
|
14676
|
-
const gitignorePath =
|
|
15435
|
+
const gitignorePath = path27.resolve(process.cwd(), ".gitignore");
|
|
14677
15436
|
const gitignore = fs.existsSync(gitignorePath) ? fs.readFileSync(gitignorePath, "utf8") : "";
|
|
14678
15437
|
const gitignoreCoversEnv = gitignore.split("\n").some((line) => [".env", ".env", ".env*"].includes(line.trim()));
|
|
14679
15438
|
if (flags.dryRun) {
|
|
@@ -14729,14 +15488,14 @@ init_invocation();
|
|
|
14729
15488
|
init_output();
|
|
14730
15489
|
init_entitlement();
|
|
14731
15490
|
import { confirm as confirm2, isCancel as isCancel2 } from "@clack/prompts";
|
|
14732
|
-
import { readFileSync as
|
|
15491
|
+
import { readFileSync as readFileSync18, readdirSync as readdirSync7, existsSync as existsSync22 } from "node:fs";
|
|
14733
15492
|
|
|
14734
15493
|
// packages/cli/src/pipeline.ts
|
|
14735
15494
|
init_src2();
|
|
14736
15495
|
init_src5();
|
|
14737
15496
|
init_src4();
|
|
14738
15497
|
import { mkdirSync as mkdirSync5, writeFileSync as writeFileSync8 } from "node:fs";
|
|
14739
|
-
import
|
|
15498
|
+
import path28 from "node:path";
|
|
14740
15499
|
|
|
14741
15500
|
// packages/cli/src/assets-module.ts
|
|
14742
15501
|
init_src();
|
|
@@ -15072,7 +15831,7 @@ async function runGenerationPipeline(input) {
|
|
|
15072
15831
|
});
|
|
15073
15832
|
const written = [];
|
|
15074
15833
|
if (!input.dryRun) {
|
|
15075
|
-
const dir =
|
|
15834
|
+
const dir = path28.resolve(input.outDir, semantics.componentName);
|
|
15076
15835
|
mkdirSync5(dir, { recursive: true });
|
|
15077
15836
|
const files = {
|
|
15078
15837
|
// Bundle-local tokens: THE emission the component's CSS resolves
|
|
@@ -15096,13 +15855,13 @@ async function runGenerationPipeline(input) {
|
|
|
15096
15855
|
`
|
|
15097
15856
|
};
|
|
15098
15857
|
for (const [name, content] of Object.entries(files)) {
|
|
15099
|
-
const filePath =
|
|
15858
|
+
const filePath = path28.join(dir, name);
|
|
15100
15859
|
writeFileSync8(filePath, content);
|
|
15101
15860
|
written.push(filePath);
|
|
15102
15861
|
}
|
|
15103
15862
|
for (const artifact of emitTokenArtifacts(input.mapping)) {
|
|
15104
|
-
const filePath =
|
|
15105
|
-
mkdirSync5(
|
|
15863
|
+
const filePath = path28.resolve(input.outDir, artifact.path);
|
|
15864
|
+
mkdirSync5(path28.dirname(filePath), { recursive: true });
|
|
15106
15865
|
writeFileSync8(filePath, artifact.content);
|
|
15107
15866
|
written.push(filePath);
|
|
15108
15867
|
}
|
|
@@ -15161,7 +15920,7 @@ var GENERATE_DESCRIPTION = {
|
|
|
15161
15920
|
function resolveProvidedSource(flags, contextFile) {
|
|
15162
15921
|
let raw;
|
|
15163
15922
|
try {
|
|
15164
|
-
raw =
|
|
15923
|
+
raw = readFileSync18(contextFile, "utf8");
|
|
15165
15924
|
} catch {
|
|
15166
15925
|
fail(flags, ExitCode.InputValidation, {
|
|
15167
15926
|
error: `Cannot read context file "${contextFile}".`,
|
|
@@ -15281,11 +16040,11 @@ token mapping (${mapping.flat.length} variables):
|
|
|
15281
16040
|
let initialCode;
|
|
15282
16041
|
let initialSemantics;
|
|
15283
16042
|
try {
|
|
15284
|
-
if (
|
|
16043
|
+
if (existsSync22(flags.out)) {
|
|
15285
16044
|
for (const entry of readdirSync7(flags.out)) {
|
|
15286
16045
|
const cjPath = `${flags.out}/${entry}/component.json`;
|
|
15287
|
-
if (!
|
|
15288
|
-
const cj = JSON.parse(
|
|
16046
|
+
if (!existsSync22(cjPath)) continue;
|
|
16047
|
+
const cj = JSON.parse(readFileSync18(cjPath, "utf8"));
|
|
15289
16048
|
if (cj.name !== void 0 && Array.isArray(cj.props)) {
|
|
15290
16049
|
previousApi = JSON.stringify({
|
|
15291
16050
|
componentName: cj.name,
|
|
@@ -15293,14 +16052,14 @@ token mapping (${mapping.flat.length} variables):
|
|
|
15293
16052
|
});
|
|
15294
16053
|
const tsxPath = `${flags.out}/${entry}/${cj.name}.tsx`;
|
|
15295
16054
|
const cssPath = `${flags.out}/${entry}/${cj.name}.css`;
|
|
15296
|
-
if (flags.refine &&
|
|
16055
|
+
if (flags.refine && existsSync22(tsxPath) && existsSync22(cssPath)) {
|
|
15297
16056
|
initialCode = {
|
|
15298
|
-
tsx:
|
|
15299
|
-
css:
|
|
16057
|
+
tsx: readFileSync18(tsxPath, "utf8"),
|
|
16058
|
+
css: readFileSync18(cssPath, "utf8")
|
|
15300
16059
|
};
|
|
15301
16060
|
const semPath = `${flags.out}/${entry}/semantics.json`;
|
|
15302
|
-
if (
|
|
15303
|
-
initialSemantics = JSON.parse(
|
|
16061
|
+
if (existsSync22(semPath)) {
|
|
16062
|
+
initialSemantics = JSON.parse(readFileSync18(semPath, "utf8"));
|
|
15304
16063
|
}
|
|
15305
16064
|
}
|
|
15306
16065
|
break;
|