@tendrilapp/cli 0.1.31 → 0.1.32
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-mcp.js +12 -6
- package/dist/tendril.js +1826 -768
- package/package.json +1 -1
package/dist/tendril.js
CHANGED
|
@@ -1297,9 +1297,396 @@ var init_session = __esm({
|
|
|
1297
1297
|
}
|
|
1298
1298
|
});
|
|
1299
1299
|
|
|
1300
|
-
// packages/figma/src/recording/
|
|
1301
|
-
import {
|
|
1300
|
+
// packages/figma/src/recording/compose.ts
|
|
1301
|
+
import { createHash } from "node:crypto";
|
|
1302
|
+
import { existsSync as existsSync2, readFileSync as readFileSync2, readdirSync, statSync } from "node:fs";
|
|
1302
1303
|
import path2 from "node:path";
|
|
1304
|
+
import { z as z5 } from "zod";
|
|
1305
|
+
function buildComposeIndex(roots, depth = 3) {
|
|
1306
|
+
const found = [];
|
|
1307
|
+
const walk2 = (dir, remaining) => {
|
|
1308
|
+
if (existsSync2(path2.join(dir, "recording-set.json"))) {
|
|
1309
|
+
found.push(dir);
|
|
1310
|
+
return;
|
|
1311
|
+
}
|
|
1312
|
+
if (remaining === 0) return;
|
|
1313
|
+
let entries2;
|
|
1314
|
+
try {
|
|
1315
|
+
entries2 = readdirSync(dir);
|
|
1316
|
+
} catch {
|
|
1317
|
+
return;
|
|
1318
|
+
}
|
|
1319
|
+
for (const e of entries2) {
|
|
1320
|
+
if (e === "node_modules" || e.startsWith(".")) continue;
|
|
1321
|
+
const full = path2.join(dir, e);
|
|
1322
|
+
try {
|
|
1323
|
+
if (statSync(full).isDirectory()) walk2(full, remaining - 1);
|
|
1324
|
+
} catch {
|
|
1325
|
+
}
|
|
1326
|
+
}
|
|
1327
|
+
};
|
|
1328
|
+
for (const r of roots) walk2(path2.resolve(r), depth);
|
|
1329
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1330
|
+
const uniqueDirs = found.filter((d) => {
|
|
1331
|
+
const key = path2.resolve(d);
|
|
1332
|
+
if (seen.has(key)) return false;
|
|
1333
|
+
seen.add(key);
|
|
1334
|
+
return true;
|
|
1335
|
+
});
|
|
1336
|
+
const entries = [];
|
|
1337
|
+
for (const dir of uniqueDirs) {
|
|
1338
|
+
let manifest;
|
|
1339
|
+
try {
|
|
1340
|
+
manifest = loadManifest(dir);
|
|
1341
|
+
} catch {
|
|
1342
|
+
continue;
|
|
1343
|
+
}
|
|
1344
|
+
const ownIdsByRep = /* @__PURE__ */ new Map();
|
|
1345
|
+
const ownIds = /* @__PURE__ */ new Set();
|
|
1346
|
+
const variantNodeIds = /* @__PURE__ */ new Set();
|
|
1347
|
+
const repSlugByVariantNode = /* @__PURE__ */ new Map();
|
|
1348
|
+
for (const rep of manifest.reps) {
|
|
1349
|
+
variantNodeIds.add(rep.nodeId);
|
|
1350
|
+
repSlugByVariantNode.set(rep.nodeId, rep.slug);
|
|
1351
|
+
const metaFile = path2.join(dir, rep.slug, "get_metadata.json");
|
|
1352
|
+
if (!existsSync2(metaFile)) continue;
|
|
1353
|
+
try {
|
|
1354
|
+
const text = envelopeTextContent(JSON.parse(readFileSync2(metaFile, "utf8")));
|
|
1355
|
+
const ids = /* @__PURE__ */ new Set();
|
|
1356
|
+
const collect = (n) => {
|
|
1357
|
+
if (n.id !== "") ids.add(n.id);
|
|
1358
|
+
for (const c of n.children) collect(c);
|
|
1359
|
+
};
|
|
1360
|
+
for (const root of parseMetadataForest(text).roots) collect(root);
|
|
1361
|
+
ownIdsByRep.set(rep.slug, ids);
|
|
1362
|
+
for (const id of ids) ownIds.add(id);
|
|
1363
|
+
} catch {
|
|
1364
|
+
}
|
|
1365
|
+
}
|
|
1366
|
+
entries.push({
|
|
1367
|
+
dir,
|
|
1368
|
+
displayName: manifest.component,
|
|
1369
|
+
...manifest.figmaFile !== void 0 ? { figmaFile: manifest.figmaFile } : {},
|
|
1370
|
+
...manifest.componentSetNode !== void 0 ? { componentSetNode: manifest.componentSetNode } : {},
|
|
1371
|
+
...manifest.figmaComponentName !== void 0 ? { figmaComponentName: manifest.figmaComponentName } : {},
|
|
1372
|
+
variantNodeIds,
|
|
1373
|
+
repSlugByVariantNode,
|
|
1374
|
+
ownIdsByRep,
|
|
1375
|
+
ownIds
|
|
1376
|
+
});
|
|
1377
|
+
}
|
|
1378
|
+
return entries;
|
|
1379
|
+
}
|
|
1380
|
+
function sameComponent(a, b) {
|
|
1381
|
+
if (a.figmaFile !== void 0 && b.figmaFile !== void 0 && a.figmaFile !== b.figmaFile) return false;
|
|
1382
|
+
if (a.componentSetNode !== void 0 && a.componentSetNode === b.componentSetNode && a.figmaFile === b.figmaFile) return true;
|
|
1383
|
+
for (const id of a.variantNodeIds) if (b.variantNodeIds.has(id)) return true;
|
|
1384
|
+
return false;
|
|
1385
|
+
}
|
|
1386
|
+
function emissionTails(setDir, repSlug) {
|
|
1387
|
+
const file = path2.join(setDir, repSlug, "get_design_context.json");
|
|
1388
|
+
if (!existsSync2(file)) return /* @__PURE__ */ new Map();
|
|
1389
|
+
let text;
|
|
1390
|
+
try {
|
|
1391
|
+
text = envelopeTextContent(JSON.parse(readFileSync2(file, "utf8")));
|
|
1392
|
+
} catch {
|
|
1393
|
+
return /* @__PURE__ */ new Map();
|
|
1394
|
+
}
|
|
1395
|
+
const cut = text.search(FOOTER);
|
|
1396
|
+
if (cut !== -1) text = text.slice(0, cut);
|
|
1397
|
+
const byHead = /* @__PURE__ */ new Map();
|
|
1398
|
+
for (const m of text.matchAll(/data-node-id="I([^"]+)"/g)) {
|
|
1399
|
+
const segs = m[1].split(";");
|
|
1400
|
+
const head = segs[0];
|
|
1401
|
+
if (!byHead.has(head)) byHead.set(head, /* @__PURE__ */ new Map());
|
|
1402
|
+
const tails = byHead.get(head);
|
|
1403
|
+
segs.slice(1).forEach((tail, i) => {
|
|
1404
|
+
const d = i + 1;
|
|
1405
|
+
if (!tails.has(tail) || tails.get(tail) > d) tails.set(tail, d);
|
|
1406
|
+
});
|
|
1407
|
+
}
|
|
1408
|
+
return byHead;
|
|
1409
|
+
}
|
|
1410
|
+
function hostInstances(setDir, repSlug) {
|
|
1411
|
+
const metaFile = path2.join(setDir, repSlug, "get_metadata.json");
|
|
1412
|
+
if (!existsSync2(metaFile)) return [];
|
|
1413
|
+
try {
|
|
1414
|
+
const text = envelopeTextContent(JSON.parse(readFileSync2(metaFile, "utf8")));
|
|
1415
|
+
const out = [];
|
|
1416
|
+
const walk2 = (n) => {
|
|
1417
|
+
if (n.type === "INSTANCE") out.push({ id: n.id, name: n.name });
|
|
1418
|
+
for (const c of n.children) walk2(c);
|
|
1419
|
+
};
|
|
1420
|
+
for (const root of parseMetadataForest(text).roots) walk2(root);
|
|
1421
|
+
return out;
|
|
1422
|
+
} catch {
|
|
1423
|
+
return [];
|
|
1424
|
+
}
|
|
1425
|
+
}
|
|
1426
|
+
function composeReport(index) {
|
|
1427
|
+
const edges = [];
|
|
1428
|
+
for (const host of index) {
|
|
1429
|
+
for (const [variantNodeId, slug] of host.repSlugByVariantNode) {
|
|
1430
|
+
void variantNodeId;
|
|
1431
|
+
const instances = hostInstances(host.dir, slug);
|
|
1432
|
+
if (instances.length === 0) continue;
|
|
1433
|
+
const tailsByHead = emissionTails(host.dir, slug);
|
|
1434
|
+
for (const inst of instances) {
|
|
1435
|
+
const tails = tailsByHead.get(inst.id) ?? /* @__PURE__ */ new Map();
|
|
1436
|
+
const disclosures = [];
|
|
1437
|
+
const refused = [];
|
|
1438
|
+
const idCands = index.filter((c) => {
|
|
1439
|
+
if (c.dir === host.dir || sameComponent(c, host)) return false;
|
|
1440
|
+
for (const t of tails.keys()) if (c.ownIds.has(t)) return true;
|
|
1441
|
+
return false;
|
|
1442
|
+
});
|
|
1443
|
+
const survivors = [];
|
|
1444
|
+
for (const c of idCands) {
|
|
1445
|
+
if (host.figmaFile !== void 0 && c.figmaFile !== void 0 && c.figmaFile !== host.figmaFile) {
|
|
1446
|
+
disclosures.push(
|
|
1447
|
+
`LIBRARY-EDGE (not auto-joined): id evidence points at ${kitLabel(c)} recorded from a DIFFERENT file \u2014 a genuine cross-file/library relationship the same-file rule refuses; the future cross-file ADR owns this class`
|
|
1448
|
+
);
|
|
1449
|
+
refused.push(c);
|
|
1450
|
+
continue;
|
|
1451
|
+
}
|
|
1452
|
+
survivors.push(c);
|
|
1453
|
+
}
|
|
1454
|
+
const groups = [];
|
|
1455
|
+
for (const c of survivors) {
|
|
1456
|
+
const g = groups.find((grp) => grp.some((m) => sameComponent(m, c)));
|
|
1457
|
+
if (g) g.push(c);
|
|
1458
|
+
else groups.push([c]);
|
|
1459
|
+
}
|
|
1460
|
+
const eligible = [];
|
|
1461
|
+
for (const g of groups) {
|
|
1462
|
+
const uncaptured = [host.figmaFile === void 0 ? "the host set" : void 0, ...g.map((m) => m.figmaFile === void 0 ? path2.basename(m.dir) : void 0)].filter(
|
|
1463
|
+
(x) => x !== void 0
|
|
1464
|
+
);
|
|
1465
|
+
if (uncaptured.length > 0) {
|
|
1466
|
+
disclosures.push(`identity unproven: file identity is not captured on ${uncaptured.join(", ")} (recorded before slice 1) \u2014 the join rests on id evidence alone`);
|
|
1467
|
+
}
|
|
1468
|
+
eligible.push(g);
|
|
1469
|
+
}
|
|
1470
|
+
const nameProps = index.filter((c) => {
|
|
1471
|
+
if (c.dir === host.dir || sameComponent(c, host)) return false;
|
|
1472
|
+
if (refused.some((r) => r.dir === c.dir || sameComponent(r, c))) return false;
|
|
1473
|
+
const identityName = c.figmaComponentName ?? c.displayName;
|
|
1474
|
+
return norm(identityName) === norm(inst.name);
|
|
1475
|
+
});
|
|
1476
|
+
if (eligible.length === 0) {
|
|
1477
|
+
if (nameProps.length > 0) {
|
|
1478
|
+
edges.push({
|
|
1479
|
+
hostSet: host.dir,
|
|
1480
|
+
hostRep: slug,
|
|
1481
|
+
instanceId: inst.id,
|
|
1482
|
+
instanceName: inst.name,
|
|
1483
|
+
kind: "proposal",
|
|
1484
|
+
partners: nameProps.map((p) => ({ dir: p.dir, displayName: p.displayName, ...p.figmaFile !== void 0 ? { figmaFile: p.figmaFile } : {} })),
|
|
1485
|
+
disclosures: [
|
|
1486
|
+
...disclosures,
|
|
1487
|
+
`NAME-ONLY proposal \u2014 identity unproven, name evidence never auto-joins: instance "${inst.name}" names ${nameProps.map(kitLabel).join(" / ")}; confirm to compose (a later slice), or ignore`
|
|
1488
|
+
]
|
|
1489
|
+
});
|
|
1490
|
+
} else if (disclosures.length > 0) {
|
|
1491
|
+
edges.push({ hostSet: host.dir, hostRep: slug, instanceId: inst.id, instanceName: inst.name, kind: "external", partners: [], disclosures });
|
|
1492
|
+
}
|
|
1493
|
+
continue;
|
|
1494
|
+
}
|
|
1495
|
+
if (eligible.length > 1) {
|
|
1496
|
+
edges.push({
|
|
1497
|
+
hostSet: host.dir,
|
|
1498
|
+
hostRep: slug,
|
|
1499
|
+
instanceId: inst.id,
|
|
1500
|
+
instanceName: inst.name,
|
|
1501
|
+
kind: "ask",
|
|
1502
|
+
partners: eligible.flat().map((p) => ({ dir: p.dir, displayName: p.displayName, ...p.figmaFile !== void 0 ? { figmaFile: p.figmaFile } : {} })),
|
|
1503
|
+
disclosures: [...disclosures, `AMBIGUOUS: id evidence reaches ${eligible.length} distinct components \u2014 a human picks, once, recorded (never guessed)`]
|
|
1504
|
+
});
|
|
1505
|
+
continue;
|
|
1506
|
+
}
|
|
1507
|
+
const group = eligible[0];
|
|
1508
|
+
const ownedDepths = [];
|
|
1509
|
+
for (const [t, d] of tails) if (group.some((m) => m.ownIds.has(t))) ownedDepths.push(d);
|
|
1510
|
+
const substitution = ownedDepths.length > 0 && ownedDepths.every((d) => d === 1);
|
|
1511
|
+
const poseVariants = /* @__PURE__ */ new Set();
|
|
1512
|
+
for (const m of group) {
|
|
1513
|
+
for (const [t, d] of tails) {
|
|
1514
|
+
if (d !== 1 || !m.ownIds.has(t)) continue;
|
|
1515
|
+
for (const [slug2, ids] of m.ownIdsByRep) {
|
|
1516
|
+
if (!ids.has(t)) continue;
|
|
1517
|
+
const variant = [...m.repSlugByVariantNode].find(([, s]) => s === slug2)?.[0];
|
|
1518
|
+
if (variant !== void 0) poseVariants.add(variant);
|
|
1519
|
+
}
|
|
1520
|
+
}
|
|
1521
|
+
}
|
|
1522
|
+
for (const p of nameProps) {
|
|
1523
|
+
if (!group.some((m) => sameComponent(m, p))) {
|
|
1524
|
+
disclosures.push(`NAME DISAGREES: instance name "${inst.name}" matches ${kitLabel(p)} while id evidence joins ${kitLabel(group[0])} \u2014 if the id join looks wrong, this is the signal`);
|
|
1525
|
+
}
|
|
1526
|
+
}
|
|
1527
|
+
if (!substitution) {
|
|
1528
|
+
edges.push({
|
|
1529
|
+
hostSet: host.dir,
|
|
1530
|
+
hostRep: slug,
|
|
1531
|
+
instanceId: inst.id,
|
|
1532
|
+
instanceName: inst.name,
|
|
1533
|
+
kind: "nested",
|
|
1534
|
+
partners: group.map((p) => ({ dir: p.dir, displayName: p.displayName, ...p.figmaFile !== void 0 ? { figmaFile: p.figmaFile } : {} })),
|
|
1535
|
+
disclosures: [...disclosures, "GENUINE-NESTED: id evidence sits below depth 1 (through an intermediate instance) \u2014 a real relationship, never a substitution target"]
|
|
1536
|
+
});
|
|
1537
|
+
continue;
|
|
1538
|
+
}
|
|
1539
|
+
if (poseVariants.size > 1) {
|
|
1540
|
+
edges.push({
|
|
1541
|
+
hostSet: host.dir,
|
|
1542
|
+
hostRep: slug,
|
|
1543
|
+
instanceId: inst.id,
|
|
1544
|
+
instanceName: inst.name,
|
|
1545
|
+
kind: "ask",
|
|
1546
|
+
partners: group.map((p) => ({ dir: p.dir, displayName: p.displayName, ...p.figmaFile !== void 0 ? { figmaFile: p.figmaFile } : {} })),
|
|
1547
|
+
disclosures: [...disclosures, `POSE-AMBIGUOUS: depth-1 evidence spans ${poseVariants.size} distinct variants \u2014 a human picks`]
|
|
1548
|
+
});
|
|
1549
|
+
continue;
|
|
1550
|
+
}
|
|
1551
|
+
const variantNode = [...poseVariants][0];
|
|
1552
|
+
edges.push({
|
|
1553
|
+
hostSet: host.dir,
|
|
1554
|
+
hostRep: slug,
|
|
1555
|
+
instanceId: inst.id,
|
|
1556
|
+
instanceName: inst.name,
|
|
1557
|
+
kind: "substitution",
|
|
1558
|
+
partners: group.map((p) => ({ dir: p.dir, displayName: p.displayName, ...p.figmaFile !== void 0 ? { figmaFile: p.figmaFile } : {} })),
|
|
1559
|
+
pose: {
|
|
1560
|
+
variantNodeId: variantNode,
|
|
1561
|
+
reps: group.filter((m) => m.repSlugByVariantNode.has(variantNode)).map((m) => ({ dir: m.dir, slug: m.repSlugByVariantNode.get(variantNode) }))
|
|
1562
|
+
},
|
|
1563
|
+
disclosures: [
|
|
1564
|
+
...disclosures,
|
|
1565
|
+
"substitution-grade: every owned tail at depth 1; pixel-neutrality is NOT asserted here \u2014 instance overrides are measured real, and the score-time check (a later slice) is the judge"
|
|
1566
|
+
]
|
|
1567
|
+
});
|
|
1568
|
+
}
|
|
1569
|
+
}
|
|
1570
|
+
}
|
|
1571
|
+
return edges;
|
|
1572
|
+
}
|
|
1573
|
+
function confirmedCompositionStatus(hostSet) {
|
|
1574
|
+
const manifestFile = path2.join(hostSet, "recording-set.json");
|
|
1575
|
+
if (!existsSync2(manifestFile)) return { rows: [] };
|
|
1576
|
+
let rawEntries;
|
|
1577
|
+
try {
|
|
1578
|
+
const parsed = JSON.parse(readFileSync2(manifestFile, "utf8"));
|
|
1579
|
+
rawEntries = Array.isArray(parsed["compositions"]) ? parsed["compositions"] : [];
|
|
1580
|
+
} catch {
|
|
1581
|
+
return { rows: [], malformed: "the recording-set manifest is not readable JSON" };
|
|
1582
|
+
}
|
|
1583
|
+
const entries = [];
|
|
1584
|
+
const malformedEntries = [];
|
|
1585
|
+
for (let i = 0; i < rawEntries.length; i++) {
|
|
1586
|
+
const parsed = CompositionEntrySchema.safeParse(rawEntries[i]);
|
|
1587
|
+
if (!parsed.success) {
|
|
1588
|
+
malformedEntries.push(`compositions[${i}] is not a valid v1 entry (${parsed.error.issues[0]?.message ?? "invalid"})`);
|
|
1589
|
+
continue;
|
|
1590
|
+
}
|
|
1591
|
+
entries.push(parsed.data);
|
|
1592
|
+
}
|
|
1593
|
+
const confirmed = entries.filter((e) => e.status === "confirmed");
|
|
1594
|
+
const rows = [];
|
|
1595
|
+
for (const entry of confirmed) {
|
|
1596
|
+
const partnerRels = Object.keys(entry.partner.manifestSha256).map(fromStoredRel);
|
|
1597
|
+
const partnerDirs = partnerRels.map((rel) => path2.resolve(hostSet, rel));
|
|
1598
|
+
const entryKey = fromStoredRel(entry.partner.key);
|
|
1599
|
+
const affectedReps = [...new Set(entry.instances.map((i) => i.hostRep))];
|
|
1600
|
+
const remediation = `re-run \`tendril compose --set ${hostSet}\` after repairing`;
|
|
1601
|
+
const missing = partnerDirs.filter((d) => !existsSync2(path2.join(d, "recording-set.json")));
|
|
1602
|
+
if (missing.length > 0) {
|
|
1603
|
+
rows.push({
|
|
1604
|
+
key: entryKey,
|
|
1605
|
+
displayName: entry.partner.displayName,
|
|
1606
|
+
status: "partner-missing",
|
|
1607
|
+
instances: entry.instances,
|
|
1608
|
+
affectedReps,
|
|
1609
|
+
detail: `confirmed partner set(s) not found: ${missing.map((d) => toPosixRel(path2.relative(hostSet, d))).join(", ")} \u2014 the decision names recordings that are not there; ${remediation}`
|
|
1610
|
+
});
|
|
1611
|
+
continue;
|
|
1612
|
+
}
|
|
1613
|
+
let stale = false;
|
|
1614
|
+
let unreadable;
|
|
1615
|
+
for (const rel of partnerRels) {
|
|
1616
|
+
const file = path2.join(path2.resolve(hostSet, rel), "recording-set.json");
|
|
1617
|
+
try {
|
|
1618
|
+
const bytes = readFileSync2(file);
|
|
1619
|
+
loadManifest(path2.resolve(hostSet, rel));
|
|
1620
|
+
const storedSha = entry.partner.manifestSha256[Object.keys(entry.partner.manifestSha256).find((k) => fromStoredRel(k) === rel)];
|
|
1621
|
+
if (createHashHex(bytes) !== storedSha) stale = true;
|
|
1622
|
+
} catch (err) {
|
|
1623
|
+
unreadable = `partner ${rel} is present but not readable as a recording set (${err instanceof Error ? err.message.split("\n")[0] : String(err)})`;
|
|
1624
|
+
break;
|
|
1625
|
+
}
|
|
1626
|
+
}
|
|
1627
|
+
if (unreadable !== void 0) {
|
|
1628
|
+
rows.push({
|
|
1629
|
+
key: entryKey,
|
|
1630
|
+
displayName: entry.partner.displayName,
|
|
1631
|
+
status: "partner-unreadable",
|
|
1632
|
+
instances: entry.instances,
|
|
1633
|
+
affectedReps,
|
|
1634
|
+
detail: `INSTRUMENT FAILURE, not an evidence verdict: ${unreadable} \u2014 the backstop could not re-derive; ${remediation}`
|
|
1635
|
+
});
|
|
1636
|
+
continue;
|
|
1637
|
+
}
|
|
1638
|
+
const edges = composeReport(buildComposeIndex([hostSet, ...partnerDirs]));
|
|
1639
|
+
const supportedInstances = /* @__PURE__ */ new Map();
|
|
1640
|
+
for (const e of edges) {
|
|
1641
|
+
if (e.hostSet !== path2.resolve(hostSet) || e.kind !== "substitution" || e.pose === void 0) continue;
|
|
1642
|
+
if (pairKeyFor(path2.resolve(hostSet), e.partners.map((p) => p.dir)) !== entryKey) continue;
|
|
1643
|
+
supportedInstances.set(`${e.hostRep}\0${e.instanceId}`, e.pose.variantNodeId);
|
|
1644
|
+
}
|
|
1645
|
+
const unsupported = entry.instances.filter((i) => supportedInstances.get(`${i.hostRep}\0${i.instanceId}`) !== i.poseVariantNodeId);
|
|
1646
|
+
const ok = unsupported.length === 0;
|
|
1647
|
+
rows.push({
|
|
1648
|
+
key: entryKey,
|
|
1649
|
+
displayName: entry.partner.displayName,
|
|
1650
|
+
status: ok ? stale ? "stale-supported" : "supported" : stale ? "stale-unsupported" : "unsupported",
|
|
1651
|
+
instances: entry.instances,
|
|
1652
|
+
affectedReps: ok ? [] : [...new Set(unsupported.map((i) => i.hostRep))],
|
|
1653
|
+
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}`
|
|
1654
|
+
});
|
|
1655
|
+
}
|
|
1656
|
+
return { rows, ...malformedEntries.length > 0 ? { malformed: malformedEntries.join("; ") } : {} };
|
|
1657
|
+
}
|
|
1658
|
+
function createHashHex(bytes) {
|
|
1659
|
+
return createHash("sha256").update(bytes).digest("hex");
|
|
1660
|
+
}
|
|
1661
|
+
var CompositionEntrySchema, norm, FOOTER, kitLabel, toPosixRel, fromStoredRel, pairKeyFor;
|
|
1662
|
+
var init_compose = __esm({
|
|
1663
|
+
"packages/figma/src/recording/compose.ts"() {
|
|
1664
|
+
"use strict";
|
|
1665
|
+
init_session();
|
|
1666
|
+
init_normalize();
|
|
1667
|
+
CompositionEntrySchema = z5.object({
|
|
1668
|
+
v: z5.literal(1),
|
|
1669
|
+
partner: z5.object({
|
|
1670
|
+
key: z5.string().min(1),
|
|
1671
|
+
displayName: z5.string(),
|
|
1672
|
+
figmaFile: z5.string().optional(),
|
|
1673
|
+
manifestSha256: z5.record(z5.string(), z5.string().regex(/^[0-9a-f]{64}$/))
|
|
1674
|
+
}),
|
|
1675
|
+
instances: z5.array(z5.object({ hostRep: z5.string(), instanceId: z5.string(), poseVariantNodeId: z5.string() })).min(1),
|
|
1676
|
+
status: z5.enum(["confirmed", "declined"])
|
|
1677
|
+
});
|
|
1678
|
+
norm = (s) => s.toLowerCase().replace(/\s+/g, " ").trim();
|
|
1679
|
+
FOOTER = /Node ids have been added to the code as data attributes/i;
|
|
1680
|
+
kitLabel = (e) => `${e.displayName} [${path2.basename(e.dir)}${e.figmaFile !== void 0 ? `, file ${e.figmaFile}` : ", file identity NOT captured"}]`;
|
|
1681
|
+
toPosixRel = (rel) => rel.split(path2.sep).join("/");
|
|
1682
|
+
fromStoredRel = (rel) => rel.replace(/\\/g, "/");
|
|
1683
|
+
pairKeyFor = (hostSet, dirs) => dirs.map((d) => toPosixRel(path2.relative(hostSet, d))).sort().join("+");
|
|
1684
|
+
}
|
|
1685
|
+
});
|
|
1686
|
+
|
|
1687
|
+
// packages/figma/src/recording/roles.ts
|
|
1688
|
+
import { existsSync as existsSync3, readFileSync as readFileSync3 } from "node:fs";
|
|
1689
|
+
import path3 from "node:path";
|
|
1303
1690
|
function emissionIdSets(emission) {
|
|
1304
1691
|
const own = /* @__PURE__ */ new Set();
|
|
1305
1692
|
const referenced = /* @__PURE__ */ new Set();
|
|
@@ -1318,10 +1705,10 @@ function emissionIdSets(emission) {
|
|
|
1318
1705
|
}
|
|
1319
1706
|
function deriveRoles(setDir, manifest) {
|
|
1320
1707
|
const m = manifest ?? loadManifest(setDir);
|
|
1321
|
-
const reps = m.reps.filter((r) =>
|
|
1708
|
+
const reps = m.reps.filter((r) => existsSync3(path3.join(setDir, r.slug, "get_design_context.json")));
|
|
1322
1709
|
const sets = /* @__PURE__ */ new Map();
|
|
1323
1710
|
for (const rep of reps) {
|
|
1324
|
-
const env = JSON.parse(
|
|
1711
|
+
const env = JSON.parse(readFileSync3(path3.join(setDir, rep.slug, "get_design_context.json"), "utf8"));
|
|
1325
1712
|
const ids = emissionIdSets(env.content.map((c) => c.text ?? "").join("\n"));
|
|
1326
1713
|
ids.own.add(rep.nodeId);
|
|
1327
1714
|
sets.set(rep.slug, ids);
|
|
@@ -1389,6 +1776,7 @@ var init_src = __esm({
|
|
|
1389
1776
|
init_provided_recording();
|
|
1390
1777
|
init_axis_defaults();
|
|
1391
1778
|
init_plan();
|
|
1779
|
+
init_compose();
|
|
1392
1780
|
init_session();
|
|
1393
1781
|
init_envelope_content();
|
|
1394
1782
|
init_roles();
|
|
@@ -1399,8 +1787,8 @@ var init_src = __esm({
|
|
|
1399
1787
|
function variableNameToPath(name) {
|
|
1400
1788
|
return name.split("/").map(canonicalCssIdentPart).filter((part) => part.length > 0);
|
|
1401
1789
|
}
|
|
1402
|
-
function tokenPathToCssVar(
|
|
1403
|
-
return `--${
|
|
1790
|
+
function tokenPathToCssVar(path44) {
|
|
1791
|
+
return `--${path44.join("-")}`;
|
|
1404
1792
|
}
|
|
1405
1793
|
function toDtcgToken(variable, defaultMode) {
|
|
1406
1794
|
const modes = Object.keys(variable.valuesByMode);
|
|
@@ -1444,11 +1832,11 @@ function toDtcgToken(variable, defaultMode) {
|
|
|
1444
1832
|
}
|
|
1445
1833
|
function mapVariablesToDtcg(variables, defaultMode = "light") {
|
|
1446
1834
|
const entries = variables.map((variable) => {
|
|
1447
|
-
const
|
|
1448
|
-
if (
|
|
1835
|
+
const path44 = variableNameToPath(variable.name);
|
|
1836
|
+
if (path44.length === 0) {
|
|
1449
1837
|
throw new DtcgMappingError("empty-name", `Figma variable ${variable.id} has an empty name`);
|
|
1450
1838
|
}
|
|
1451
|
-
return { variable, path:
|
|
1839
|
+
return { variable, path: path44 };
|
|
1452
1840
|
});
|
|
1453
1841
|
const groupPrefixes = /* @__PURE__ */ new Set();
|
|
1454
1842
|
for (const e of entries) {
|
|
@@ -1469,21 +1857,21 @@ function mapVariablesToDtcg(variables, defaultMode = "light") {
|
|
|
1469
1857
|
}
|
|
1470
1858
|
const tokens = {};
|
|
1471
1859
|
const flat = [];
|
|
1472
|
-
for (const { variable, path:
|
|
1860
|
+
for (const { variable, path: path44 } of entries) {
|
|
1473
1861
|
const token = toDtcgToken(variable, defaultMode);
|
|
1474
1862
|
let group = tokens;
|
|
1475
|
-
for (const segment of
|
|
1863
|
+
for (const segment of path44.slice(0, -1)) {
|
|
1476
1864
|
const existing = group[segment];
|
|
1477
1865
|
group = existing ?? (group[segment] = {});
|
|
1478
1866
|
}
|
|
1479
|
-
const leaf =
|
|
1867
|
+
const leaf = path44[path44.length - 1];
|
|
1480
1868
|
if (group[leaf] !== void 0) {
|
|
1481
|
-
throw new DtcgMappingError("duplicate-path", `Duplicate token path "${
|
|
1869
|
+
throw new DtcgMappingError("duplicate-path", `Duplicate token path "${path44.join(".")}" (variable ${variable.id})`);
|
|
1482
1870
|
}
|
|
1483
1871
|
group[leaf] = token;
|
|
1484
1872
|
flat.push({
|
|
1485
|
-
path:
|
|
1486
|
-
cssVar: tokenPathToCssVar(
|
|
1873
|
+
path: path44.join("."),
|
|
1874
|
+
cssVar: tokenPathToCssVar(path44),
|
|
1487
1875
|
type: token.$type,
|
|
1488
1876
|
value: token.$value
|
|
1489
1877
|
});
|
|
@@ -1672,9 +2060,9 @@ function boundId(value) {
|
|
|
1672
2060
|
return isObject(value) && typeof value["boundVariableId"] === "string" ? value["boundVariableId"] : void 0;
|
|
1673
2061
|
}
|
|
1674
2062
|
function resolveBinding(ctx, id) {
|
|
1675
|
-
const
|
|
1676
|
-
if (
|
|
1677
|
-
return
|
|
2063
|
+
const path44 = ctx.pathById.get(id);
|
|
2064
|
+
if (path44 === void 0) ctx.unresolved.add(id);
|
|
2065
|
+
return path44;
|
|
1678
2066
|
}
|
|
1679
2067
|
function parseVariantProps(name) {
|
|
1680
2068
|
if (!name.includes("=")) return void 0;
|
|
@@ -1709,8 +2097,8 @@ function walk(ctx, raw) {
|
|
|
1709
2097
|
if (!isObject(paint) || paint["visible"] === false) continue;
|
|
1710
2098
|
const id = boundId(paint);
|
|
1711
2099
|
if (id !== void 0) {
|
|
1712
|
-
const
|
|
1713
|
-
if (
|
|
2100
|
+
const path44 = resolveBinding(ctx, id);
|
|
2101
|
+
if (path44 !== void 0) tokens.add(path44);
|
|
1714
2102
|
} else if (typeof paint["color"] === "string") {
|
|
1715
2103
|
ctx.hardcoded.push({ node: name, property, value: paint["color"] });
|
|
1716
2104
|
}
|
|
@@ -1718,8 +2106,8 @@ function walk(ctx, raw) {
|
|
|
1718
2106
|
}
|
|
1719
2107
|
const radiusId = boundId(raw["cornerRadius"]);
|
|
1720
2108
|
if (radiusId !== void 0) {
|
|
1721
|
-
const
|
|
1722
|
-
if (
|
|
2109
|
+
const path44 = resolveBinding(ctx, radiusId);
|
|
2110
|
+
if (path44 !== void 0) tokens.add(path44);
|
|
1723
2111
|
} else if (typeof raw["cornerRadius"] === "number" && raw["cornerRadius"] !== 0) {
|
|
1724
2112
|
ctx.hardcoded.push({ node: name, property: "border-radius", value: `${raw["cornerRadius"]}px` });
|
|
1725
2113
|
}
|
|
@@ -1729,10 +2117,10 @@ function walk(ctx, raw) {
|
|
|
1729
2117
|
layout = { mode: layoutMode === "HORIZONTAL" ? "flex-row" : "flex-column" };
|
|
1730
2118
|
const gapId = boundId(raw["itemSpacing"]);
|
|
1731
2119
|
if (gapId !== void 0) {
|
|
1732
|
-
const
|
|
1733
|
-
if (
|
|
1734
|
-
layout.gap =
|
|
1735
|
-
tokens.add(
|
|
2120
|
+
const path44 = resolveBinding(ctx, gapId);
|
|
2121
|
+
if (path44 !== void 0) {
|
|
2122
|
+
layout.gap = path44;
|
|
2123
|
+
tokens.add(path44);
|
|
1736
2124
|
}
|
|
1737
2125
|
} else if (typeof raw["itemSpacing"] === "number" && raw["itemSpacing"] !== 0) {
|
|
1738
2126
|
ctx.hardcoded.push({ node: name, property: "gap", value: `${raw["itemSpacing"]}px` });
|
|
@@ -1741,10 +2129,10 @@ function walk(ctx, raw) {
|
|
|
1741
2129
|
for (const field of PADDING_FIELDS) {
|
|
1742
2130
|
const id = boundId(raw[field]);
|
|
1743
2131
|
if (id !== void 0) {
|
|
1744
|
-
const
|
|
1745
|
-
if (
|
|
1746
|
-
paddingPaths.push(
|
|
1747
|
-
tokens.add(
|
|
2132
|
+
const path44 = resolveBinding(ctx, id);
|
|
2133
|
+
if (path44 !== void 0) {
|
|
2134
|
+
paddingPaths.push(path44);
|
|
2135
|
+
tokens.add(path44);
|
|
1748
2136
|
}
|
|
1749
2137
|
} else if (typeof raw[field] === "number" && raw[field] !== 0) {
|
|
1750
2138
|
ctx.hardcoded.push({ node: name, property: field, value: `${raw[field]}px` });
|
|
@@ -1952,25 +2340,25 @@ var init_src3 = __esm({
|
|
|
1952
2340
|
});
|
|
1953
2341
|
|
|
1954
2342
|
// packages/cli/src/invocation.ts
|
|
1955
|
-
import { existsSync as
|
|
1956
|
-
import
|
|
2343
|
+
import { existsSync as existsSync4, realpathSync } from "node:fs";
|
|
2344
|
+
import path4 from "node:path";
|
|
1957
2345
|
import { fileURLToPath } from "node:url";
|
|
1958
2346
|
function findPathTendril(pathEnv, platform) {
|
|
1959
|
-
const dirs = pathEnv.split(
|
|
2347
|
+
const dirs = pathEnv.split(path4.delimiter).filter((d) => d !== "" && !/node_modules[\\/]\.bin/.test(d) && !/[\\/]_npx[\\/]/.test(d));
|
|
1960
2348
|
const names = platform === "win32" ? ["tendril.cmd", "tendril.bat"] : ["tendril"];
|
|
1961
2349
|
for (const dir of dirs) {
|
|
1962
2350
|
for (const name of names) {
|
|
1963
|
-
const candidate =
|
|
1964
|
-
if (
|
|
2351
|
+
const candidate = path4.join(dir, name);
|
|
2352
|
+
if (existsSync4(candidate)) return candidate;
|
|
1965
2353
|
}
|
|
1966
2354
|
}
|
|
1967
2355
|
return null;
|
|
1968
2356
|
}
|
|
1969
2357
|
function packageRootOf(file) {
|
|
1970
|
-
let dir =
|
|
2358
|
+
let dir = path4.dirname(file);
|
|
1971
2359
|
for (; ; ) {
|
|
1972
|
-
if (
|
|
1973
|
-
const parent =
|
|
2360
|
+
if (existsSync4(path4.join(dir, "package.json"))) return dir;
|
|
2361
|
+
const parent = path4.dirname(dir);
|
|
1974
2362
|
if (parent === dir) return null;
|
|
1975
2363
|
dir = parent;
|
|
1976
2364
|
}
|
|
@@ -2009,18 +2397,18 @@ var init_invocation = __esm({
|
|
|
2009
2397
|
|
|
2010
2398
|
// packages/verify/src/browser.ts
|
|
2011
2399
|
import { execFileSync } from "node:child_process";
|
|
2012
|
-
import { existsSync as
|
|
2013
|
-
import
|
|
2400
|
+
import { existsSync as existsSync5, readdirSync as readdirSync2 } from "node:fs";
|
|
2401
|
+
import path5 from "node:path";
|
|
2014
2402
|
function resolveChrome() {
|
|
2015
2403
|
const fromEnv = process.env["TENDRIL_CHROME"] ?? process.env["CHROME_PATH"];
|
|
2016
2404
|
if (fromEnv !== void 0 && fromEnv !== "") {
|
|
2017
|
-
if (!
|
|
2405
|
+
if (!existsSync5(fromEnv)) {
|
|
2018
2406
|
throw new Error(`CHROME_PATH points at ${fromEnv}, which does not exist \u2014 fix the variable or unset it to use discovery`);
|
|
2019
2407
|
}
|
|
2020
2408
|
return fromEnv;
|
|
2021
2409
|
}
|
|
2022
2410
|
const candidates = process.platform === "darwin" ? MAC_CANDIDATES : process.platform === "win32" ? WIN_CANDIDATES : [];
|
|
2023
|
-
for (const c of candidates) if (
|
|
2411
|
+
for (const c of candidates) if (existsSync5(c)) return c;
|
|
2024
2412
|
if (process.platform !== "win32") {
|
|
2025
2413
|
for (const name of PATH_NAMES) {
|
|
2026
2414
|
try {
|
|
@@ -2034,7 +2422,7 @@ function resolveChrome() {
|
|
|
2034
2422
|
}
|
|
2035
2423
|
function versionFromInstallDir(exePath) {
|
|
2036
2424
|
try {
|
|
2037
|
-
const builds =
|
|
2425
|
+
const builds = readdirSync2(path5.dirname(exePath), { withFileTypes: true }).filter((e) => e.isDirectory() && /^\d+(\.\d+){3}$/.test(e.name)).map((e) => e.name.split(".").map(Number));
|
|
2038
2426
|
builds.sort((a, b) => a[0] - b[0] || a[1] - b[1] || a[2] - b[2] || a[3] - b[3]);
|
|
2039
2427
|
return builds.length === 0 ? null : builds[builds.length - 1].join(".");
|
|
2040
2428
|
} catch {
|
|
@@ -2046,7 +2434,7 @@ function windowsBrowserName(exePath) {
|
|
|
2046
2434
|
if (p.includes("\\google\\chrome\\")) return "Google Chrome";
|
|
2047
2435
|
if (p.includes("\\microsoft\\edge\\")) return "Microsoft Edge";
|
|
2048
2436
|
if (p.includes("chromium")) return "Chromium";
|
|
2049
|
-
return
|
|
2437
|
+
return path5.win32.basename(exePath, ".exe");
|
|
2050
2438
|
}
|
|
2051
2439
|
function chromeVersion() {
|
|
2052
2440
|
if (_version !== void 0) return _version;
|
|
@@ -2081,32 +2469,32 @@ var init_browser = __esm({
|
|
|
2081
2469
|
});
|
|
2082
2470
|
|
|
2083
2471
|
// packages/verify/src/runtime.ts
|
|
2084
|
-
import { existsSync as
|
|
2472
|
+
import { existsSync as existsSync6, mkdtempSync, symlinkSync } from "node:fs";
|
|
2085
2473
|
import { createRequire } from "node:module";
|
|
2086
2474
|
import os from "node:os";
|
|
2087
|
-
import
|
|
2475
|
+
import path6 from "node:path";
|
|
2088
2476
|
import { fileURLToPath as fileURLToPath2 } from "node:url";
|
|
2089
2477
|
function runtimePackageRoot() {
|
|
2090
2478
|
const env = process.env["TENDRIL_PACKAGE_ROOT"];
|
|
2091
|
-
if (env !== void 0 && env !== "") return
|
|
2092
|
-
return
|
|
2479
|
+
if (env !== void 0 && env !== "") return path6.resolve(env);
|
|
2480
|
+
return path6.resolve(path6.dirname(fileURLToPath2(import.meta.url)), "..");
|
|
2093
2481
|
}
|
|
2094
2482
|
function runtimeNodeModules() {
|
|
2095
2483
|
const root = runtimePackageRoot();
|
|
2096
|
-
for (let dir = root; ; dir =
|
|
2097
|
-
const candidate =
|
|
2098
|
-
if (
|
|
2099
|
-
if (
|
|
2484
|
+
for (let dir = root; ; dir = path6.dirname(dir)) {
|
|
2485
|
+
const candidate = path6.basename(dir) === "node_modules" ? dir : path6.join(dir, "node_modules");
|
|
2486
|
+
if (existsSync6(path6.join(candidate, "react"))) return candidate;
|
|
2487
|
+
if (path6.dirname(dir) === dir) break;
|
|
2100
2488
|
}
|
|
2101
2489
|
throw new Error(
|
|
2102
2490
|
`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.`
|
|
2103
2491
|
);
|
|
2104
2492
|
}
|
|
2105
2493
|
function newScratchDir(prefix) {
|
|
2106
|
-
const dir = mkdtempSync(
|
|
2494
|
+
const dir = mkdtempSync(path6.join(os.tmpdir(), `tendril-${prefix}-`));
|
|
2107
2495
|
const nodeModules = runtimeNodeModules();
|
|
2108
2496
|
try {
|
|
2109
|
-
symlinkSync(nodeModules,
|
|
2497
|
+
symlinkSync(nodeModules, path6.join(dir, "node_modules"), "junction");
|
|
2110
2498
|
} catch (err) {
|
|
2111
2499
|
throw new Error(
|
|
2112
2500
|
`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.`
|
|
@@ -2115,7 +2503,7 @@ function newScratchDir(prefix) {
|
|
|
2115
2503
|
return dir;
|
|
2116
2504
|
}
|
|
2117
2505
|
function reactPinPlugin() {
|
|
2118
|
-
const req = createRequire(
|
|
2506
|
+
const req = createRequire(path6.join(runtimePackageRoot(), "package.json"));
|
|
2119
2507
|
return {
|
|
2120
2508
|
name: "tendril-react-pin",
|
|
2121
2509
|
setup(b) {
|
|
@@ -2168,9 +2556,9 @@ var init_gates = __esm({
|
|
|
2168
2556
|
|
|
2169
2557
|
// packages/verify/src/tsc-check.ts
|
|
2170
2558
|
import ts from "typescript";
|
|
2171
|
-
import
|
|
2559
|
+
import path7 from "node:path";
|
|
2172
2560
|
function runTscStrict(files) {
|
|
2173
|
-
const program = ts.createProgram(files.map((f) =>
|
|
2561
|
+
const program = ts.createProgram(files.map((f) => path7.resolve(f)), STRICT_OPTIONS);
|
|
2174
2562
|
const diagnostics = ts.getPreEmitDiagnostics(program);
|
|
2175
2563
|
const mapped = diagnostics.map((d) => {
|
|
2176
2564
|
const file = d.file?.fileName;
|
|
@@ -2331,7 +2719,7 @@ var init_token_lint = __esm({
|
|
|
2331
2719
|
|
|
2332
2720
|
// packages/verify/src/loop.ts
|
|
2333
2721
|
import { rmSync, writeFileSync as writeFileSync2 } from "node:fs";
|
|
2334
|
-
import
|
|
2722
|
+
import path8 from "node:path";
|
|
2335
2723
|
async function runChecks(componentName, files, extraFiles, definedVars2) {
|
|
2336
2724
|
const findings = [];
|
|
2337
2725
|
for (const violation of scanForbiddenPatterns(files.tsx)) {
|
|
@@ -2346,10 +2734,10 @@ async function runChecks(componentName, files, extraFiles, definedVars2) {
|
|
|
2346
2734
|
}
|
|
2347
2735
|
const workDir = newScratchDir("loop");
|
|
2348
2736
|
try {
|
|
2349
|
-
const tsxPath =
|
|
2737
|
+
const tsxPath = path8.join(workDir, `${componentName}.tsx`);
|
|
2350
2738
|
writeFileSync2(tsxPath, files.tsx);
|
|
2351
2739
|
for (const [name, content] of Object.entries(extraFiles ?? {})) {
|
|
2352
|
-
writeFileSync2(
|
|
2740
|
+
writeFileSync2(path8.join(workDir, name), content);
|
|
2353
2741
|
}
|
|
2354
2742
|
const tsc = runTscStrict([tsxPath]);
|
|
2355
2743
|
for (const d of tsc.diagnostics) {
|
|
@@ -2414,7 +2802,7 @@ var init_loop = __esm({
|
|
|
2414
2802
|
});
|
|
2415
2803
|
|
|
2416
2804
|
// packages/verify/src/report.ts
|
|
2417
|
-
import { z as
|
|
2805
|
+
import { z as z6 } from "zod";
|
|
2418
2806
|
function isTokenEnforcedProperty(property) {
|
|
2419
2807
|
return enforcedMatchers.some((m) => m.test(property));
|
|
2420
2808
|
}
|
|
@@ -2456,58 +2844,58 @@ var init_report = __esm({
|
|
|
2456
2844
|
"packages/verify/src/report.ts"() {
|
|
2457
2845
|
"use strict";
|
|
2458
2846
|
init_token_lint();
|
|
2459
|
-
ReportSchema =
|
|
2460
|
-
component:
|
|
2461
|
-
tokenAdherence:
|
|
2462
|
-
tokenViolations:
|
|
2463
|
-
tscPass:
|
|
2464
|
-
gatesClean:
|
|
2847
|
+
ReportSchema = z6.object({
|
|
2848
|
+
component: z6.string(),
|
|
2849
|
+
tokenAdherence: z6.number().min(0).max(100),
|
|
2850
|
+
tokenViolations: z6.number(),
|
|
2851
|
+
tscPass: z6.boolean(),
|
|
2852
|
+
gatesClean: z6.boolean(),
|
|
2465
2853
|
/** null = check skipped in this phase (recorded in skippedChecks). */
|
|
2466
|
-
axeViolations:
|
|
2467
|
-
visualDiff:
|
|
2468
|
-
method:
|
|
2469
|
-
score:
|
|
2470
|
-
flags:
|
|
2854
|
+
axeViolations: z6.number().nullable(),
|
|
2855
|
+
visualDiff: z6.object({
|
|
2856
|
+
method: z6.string(),
|
|
2857
|
+
score: z6.number().nullable(),
|
|
2858
|
+
flags: z6.array(z6.string()),
|
|
2471
2859
|
/** Reference-image oracle (ADR-004 pillar B). */
|
|
2472
|
-
imageSimilarity:
|
|
2473
|
-
variants:
|
|
2474
|
-
mean:
|
|
2475
|
-
worst:
|
|
2476
|
-
perVariant:
|
|
2477
|
-
|
|
2478
|
-
label:
|
|
2479
|
-
similarity:
|
|
2480
|
-
ink:
|
|
2481
|
-
region:
|
|
2860
|
+
imageSimilarity: z6.object({
|
|
2861
|
+
variants: z6.number(),
|
|
2862
|
+
mean: z6.number(),
|
|
2863
|
+
worst: z6.number(),
|
|
2864
|
+
perVariant: z6.array(
|
|
2865
|
+
z6.object({
|
|
2866
|
+
label: z6.string(),
|
|
2867
|
+
similarity: z6.number(),
|
|
2868
|
+
ink: z6.number().optional(),
|
|
2869
|
+
region: z6.string().optional()
|
|
2482
2870
|
})
|
|
2483
2871
|
).optional()
|
|
2484
2872
|
}).optional(),
|
|
2485
2873
|
/** What the score covers — the number carries its own caveats. */
|
|
2486
|
-
coverage:
|
|
2487
|
-
factChecks:
|
|
2488
|
-
imageVariants:
|
|
2489
|
-
unmeasured:
|
|
2874
|
+
coverage: z6.object({
|
|
2875
|
+
factChecks: z6.number(),
|
|
2876
|
+
imageVariants: z6.number(),
|
|
2877
|
+
unmeasured: z6.array(z6.string())
|
|
2490
2878
|
}).optional()
|
|
2491
2879
|
}),
|
|
2492
|
-
repairIterations:
|
|
2493
|
-
skippedChecks:
|
|
2494
|
-
model:
|
|
2880
|
+
repairIterations: z6.number(),
|
|
2881
|
+
skippedChecks: z6.array(z6.string()),
|
|
2882
|
+
model: z6.object({ id: z6.string(), status: z6.enum(["verified", "degraded"]) }),
|
|
2495
2883
|
/** Who authored the stylesheet — keeps eval numbers comparable if
|
|
2496
2884
|
* authorship ever changes (ADR-005; review finding on ADR-004). */
|
|
2497
|
-
styling:
|
|
2885
|
+
styling: z6.enum(["model", "compiled"]).default("model"),
|
|
2498
2886
|
/** Per-run model usage: BYOK economics must be observable, not folklore.
|
|
2499
2887
|
* Absent when the transport has no metering (mock runs). */
|
|
2500
|
-
usage:
|
|
2501
|
-
calls:
|
|
2502
|
-
promptTokens:
|
|
2503
|
-
completionTokens:
|
|
2888
|
+
usage: z6.object({
|
|
2889
|
+
calls: z6.number(),
|
|
2890
|
+
promptTokens: z6.number(),
|
|
2891
|
+
completionTokens: z6.number(),
|
|
2504
2892
|
/** OpenRouter-reported USD (0 when the provider didn't report). */
|
|
2505
|
-
cost:
|
|
2893
|
+
cost: z6.number()
|
|
2506
2894
|
}).optional(),
|
|
2507
|
-
transport:
|
|
2508
|
-
toolVersion:
|
|
2509
|
-
generatedAt:
|
|
2510
|
-
needsHumanReview:
|
|
2895
|
+
transport: z6.string(),
|
|
2896
|
+
toolVersion: z6.string(),
|
|
2897
|
+
generatedAt: z6.string(),
|
|
2898
|
+
needsHumanReview: z6.array(z6.string())
|
|
2511
2899
|
});
|
|
2512
2900
|
enforcedMatchers = TOKEN_ENFORCED_PROPERTIES.map(
|
|
2513
2901
|
(entry) => entry.startsWith("/") && entry.endsWith("/") ? new RegExp(entry.slice(1, -1)) : new RegExp(`^${entry}$`)
|
|
@@ -2991,7 +3379,7 @@ var init_image_diff = __esm({
|
|
|
2991
3379
|
});
|
|
2992
3380
|
|
|
2993
3381
|
// packages/verify/src/visual-facts.ts
|
|
2994
|
-
import
|
|
3382
|
+
import path9 from "node:path";
|
|
2995
3383
|
import { fileURLToPath as fileURLToPath3 } from "node:url";
|
|
2996
3384
|
import { build } from "esbuild";
|
|
2997
3385
|
import { chromium } from "playwright-core";
|
|
@@ -3536,7 +3924,7 @@ var init_visual_facts = __esm({
|
|
|
3536
3924
|
init_browser();
|
|
3537
3925
|
init_mount_limits();
|
|
3538
3926
|
init_image_diff();
|
|
3539
|
-
RESOLVE_DIR =
|
|
3927
|
+
RESOLVE_DIR = path9.resolve(path9.dirname(fileURLToPath3(import.meta.url)), "..");
|
|
3540
3928
|
TOLERANCE_PX = 2;
|
|
3541
3929
|
WIDTH_SLACK = 0.25;
|
|
3542
3930
|
IMAGE_SIMILARITY_FLOOR = 0.8;
|
|
@@ -3545,14 +3933,14 @@ var init_visual_facts = __esm({
|
|
|
3545
3933
|
});
|
|
3546
3934
|
|
|
3547
3935
|
// packages/verify/src/paths.ts
|
|
3548
|
-
import
|
|
3936
|
+
import path10 from "node:path";
|
|
3549
3937
|
import { fileURLToPath as fileURLToPath4 } from "node:url";
|
|
3550
3938
|
var VERIFY_PKG_DIR, REPO_ROOT;
|
|
3551
3939
|
var init_paths = __esm({
|
|
3552
3940
|
"packages/verify/src/paths.ts"() {
|
|
3553
3941
|
"use strict";
|
|
3554
|
-
VERIFY_PKG_DIR =
|
|
3555
|
-
REPO_ROOT =
|
|
3942
|
+
VERIFY_PKG_DIR = path10.resolve(path10.dirname(fileURLToPath4(import.meta.url)), "..");
|
|
3943
|
+
REPO_ROOT = path10.resolve(VERIFY_PKG_DIR, "..", "..");
|
|
3556
3944
|
}
|
|
3557
3945
|
});
|
|
3558
3946
|
|
|
@@ -3671,9 +4059,9 @@ var init_font_collection = __esm({
|
|
|
3671
4059
|
});
|
|
3672
4060
|
|
|
3673
4061
|
// packages/verify/src/font-discovery.ts
|
|
3674
|
-
import { existsSync as
|
|
4062
|
+
import { existsSync as existsSync7, readdirSync as readdirSync3, readFileSync as readFileSync4, realpathSync as realpathSync2 } from "node:fs";
|
|
3675
4063
|
import os2 from "node:os";
|
|
3676
|
-
import
|
|
4064
|
+
import path11 from "node:path";
|
|
3677
4065
|
function weightFromSubfamily(subfamily) {
|
|
3678
4066
|
for (const [re, w] of WEIGHT_TOKENS) if (re.test(subfamily)) return w;
|
|
3679
4067
|
return void 0;
|
|
@@ -3719,7 +4107,7 @@ function faceAt(bytes, view, dirOffset, file, faceIndex) {
|
|
|
3719
4107
|
}
|
|
3720
4108
|
function facesInFile(file) {
|
|
3721
4109
|
try {
|
|
3722
|
-
const bytes = new Uint8Array(
|
|
4110
|
+
const bytes = new Uint8Array(readFileSync4(file));
|
|
3723
4111
|
if (bytes.length < 12) return [];
|
|
3724
4112
|
const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
|
|
3725
4113
|
if (isCollection(bytes)) {
|
|
@@ -3739,13 +4127,13 @@ function facesInFile(file) {
|
|
|
3739
4127
|
}
|
|
3740
4128
|
function systemFontDirs() {
|
|
3741
4129
|
const env = process.env["TENDRIL_SYSTEM_FONT_DIRS"];
|
|
3742
|
-
if (env !== void 0 && env !== "") return env.split(
|
|
4130
|
+
if (env !== void 0 && env !== "") return env.split(path11.delimiter).filter((d) => d !== "" && existsSync7(d));
|
|
3743
4131
|
const home = os2.homedir();
|
|
3744
|
-
const dirs = process.platform === "darwin" ? ["/System/Library/Fonts", "/Library/Fonts",
|
|
3745
|
-
|
|
3746
|
-
...process.env["LOCALAPPDATA"] !== void 0 ? [
|
|
3747
|
-
] : ["/usr/share/fonts", "/usr/local/share/fonts",
|
|
3748
|
-
return dirs.filter((d) =>
|
|
4132
|
+
const dirs = process.platform === "darwin" ? ["/System/Library/Fonts", "/Library/Fonts", path11.join(home, "Library", "Fonts")] : process.platform === "win32" ? [
|
|
4133
|
+
path11.join(process.env["WINDIR"] ?? "C:\\Windows", "Fonts"),
|
|
4134
|
+
...process.env["LOCALAPPDATA"] !== void 0 ? [path11.join(process.env["LOCALAPPDATA"], "Microsoft", "Windows", "Fonts")] : []
|
|
4135
|
+
] : ["/usr/share/fonts", "/usr/local/share/fonts", path11.join(home, ".fonts"), path11.join(home, ".local", "share", "fonts")];
|
|
4136
|
+
return dirs.filter((d) => existsSync7(d));
|
|
3749
4137
|
}
|
|
3750
4138
|
function discoverSystemFaces(dirs = systemFontDirs(), depth = 3) {
|
|
3751
4139
|
const seen = /* @__PURE__ */ new Set();
|
|
@@ -3754,15 +4142,15 @@ function discoverSystemFaces(dirs = systemFontDirs(), depth = 3) {
|
|
|
3754
4142
|
for (const dir of dirList) {
|
|
3755
4143
|
let entries;
|
|
3756
4144
|
try {
|
|
3757
|
-
entries =
|
|
4145
|
+
entries = readdirSync3(dir, { withFileTypes: true });
|
|
3758
4146
|
} catch {
|
|
3759
4147
|
continue;
|
|
3760
4148
|
}
|
|
3761
4149
|
for (const e of entries) {
|
|
3762
|
-
const full =
|
|
4150
|
+
const full = path11.join(dir, e.name);
|
|
3763
4151
|
if (e.isDirectory()) {
|
|
3764
4152
|
if (remaining > 1) faces.push(...walk2([full], remaining - 1));
|
|
3765
|
-
} else if (FONT_EXTENSIONS.has(
|
|
4153
|
+
} else if (FONT_EXTENSIONS.has(path11.extname(e.name).toLowerCase())) {
|
|
3766
4154
|
let key = full;
|
|
3767
4155
|
try {
|
|
3768
4156
|
key = realpathSync2(full);
|
|
@@ -3810,14 +4198,14 @@ var init_font_discovery = __esm({
|
|
|
3810
4198
|
});
|
|
3811
4199
|
|
|
3812
4200
|
// packages/verify/src/font-resolve.ts
|
|
3813
|
-
import { createHash } from "node:crypto";
|
|
3814
|
-
import { existsSync as
|
|
4201
|
+
import { createHash as createHash2 } from "node:crypto";
|
|
4202
|
+
import { existsSync as existsSync8, mkdirSync as mkdirSync2, readFileSync as readFileSync5, writeFileSync as writeFileSync3 } from "node:fs";
|
|
3815
4203
|
import os3 from "node:os";
|
|
3816
|
-
import
|
|
4204
|
+
import path12 from "node:path";
|
|
3817
4205
|
function fontCacheDir() {
|
|
3818
4206
|
const env = process.env["TENDRIL_FONT_CACHE"];
|
|
3819
|
-
if (env !== void 0 && env !== "") return
|
|
3820
|
-
return
|
|
4207
|
+
if (env !== void 0 && env !== "") return path12.resolve(env);
|
|
4208
|
+
return path12.join(os3.homedir(), ".tendril", "fonts");
|
|
3821
4209
|
}
|
|
3822
4210
|
function normalizeFontLicense(value) {
|
|
3823
4211
|
return typeof value === "string" && FONT_LICENSES.includes(value) ? value : "unknown";
|
|
@@ -3881,30 +4269,30 @@ async function resolveFonts(family, weights, cacheDir = DEFAULT_FONT_CACHE) {
|
|
|
3881
4269
|
continue;
|
|
3882
4270
|
}
|
|
3883
4271
|
const bytes = new Uint8Array(await fileRes.arrayBuffer());
|
|
3884
|
-
const sha256 =
|
|
3885
|
-
const file =
|
|
4272
|
+
const sha256 = createHash2("sha256").update(bytes).digest("hex");
|
|
4273
|
+
const file = path12.join(cacheDir, `${family.toLowerCase().replace(/\s+/g, "-")}-${weight}.woff2`);
|
|
3886
4274
|
writeFileSync3(file, bytes);
|
|
3887
4275
|
resolved.push({ family, weight, source: url, sha256, file, license: meta.license, ...meta.family !== null ? { servedFamily: meta.family } : {} });
|
|
3888
4276
|
} catch (err) {
|
|
3889
4277
|
failures.push({ family, weight, reason: `download failed: ${err instanceof Error ? err.message : String(err)}` });
|
|
3890
4278
|
}
|
|
3891
4279
|
}
|
|
3892
|
-
const mPath =
|
|
3893
|
-
const prior =
|
|
3894
|
-
const portable2 = resolved.map((m) => ({ ...m, file:
|
|
4280
|
+
const mPath = path12.join(cacheDir, "manifest.json");
|
|
4281
|
+
const prior = existsSync8(mPath) ? JSON.parse(readFileSync5(mPath, "utf8")) : [];
|
|
4282
|
+
const portable2 = resolved.map((m) => ({ ...m, file: path12.basename(m.file) }));
|
|
3895
4283
|
const merged = [...prior.filter((p) => !resolved.some((m) => m.family === p.family && m.weight === p.weight)), ...portable2];
|
|
3896
4284
|
if (resolved.length > 0) writeFileSync3(mPath, `${JSON.stringify(merged, null, 2)}
|
|
3897
4285
|
`);
|
|
3898
4286
|
return { resolved, failures };
|
|
3899
4287
|
}
|
|
3900
4288
|
function addLocalFont(family, weight, filePath, cacheDir = DEFAULT_FONT_CACHE, faceIndex, provenance = "local") {
|
|
3901
|
-
const src =
|
|
3902
|
-
if (!
|
|
3903
|
-
const ext =
|
|
4289
|
+
const src = path12.resolve(filePath);
|
|
4290
|
+
if (!existsSync8(src)) throw new Error(`font file not found: ${src}`);
|
|
4291
|
+
const ext = path12.extname(src).toLowerCase();
|
|
3904
4292
|
if (![".woff2", ".woff", ".ttf", ".otf", ".ttc"].includes(ext)) {
|
|
3905
4293
|
throw new Error(`unsupported font file "${ext}" \u2014 use .woff2 (preferred), .woff, .ttf, .otf or .ttc`);
|
|
3906
4294
|
}
|
|
3907
|
-
let bytes = new Uint8Array(
|
|
4295
|
+
let bytes = new Uint8Array(readFileSync5(src));
|
|
3908
4296
|
if (bytes.length === 0) throw new Error(`font file is empty: ${src}`);
|
|
3909
4297
|
let storedExt = ext;
|
|
3910
4298
|
if (isCollection(bytes)) {
|
|
@@ -3914,7 +4302,7 @@ function addLocalFont(family, weight, filePath, cacheDir = DEFAULT_FONT_CACHE, f
|
|
|
3914
4302
|
const all = listCollectionFaces(bytes);
|
|
3915
4303
|
const shown = (candidates.length > 0 ? candidates : all).map((f) => ` --face ${f.index} ${f.family ?? "(unnamed)"}${f.subfamily !== void 0 ? ` ${f.subfamily}` : ""}`).join("\n");
|
|
3916
4304
|
throw new Error(
|
|
3917
|
-
`${
|
|
4305
|
+
`${path12.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>:
|
|
3918
4306
|
${shown}`
|
|
3919
4307
|
);
|
|
3920
4308
|
}
|
|
@@ -3922,21 +4310,21 @@ ${shown}`
|
|
|
3922
4310
|
storedExt = ".ttf";
|
|
3923
4311
|
}
|
|
3924
4312
|
mkdirSync2(cacheDir, { recursive: true });
|
|
3925
|
-
const sha256 =
|
|
3926
|
-
const file =
|
|
4313
|
+
const sha256 = createHash2("sha256").update(bytes).digest("hex");
|
|
4314
|
+
const file = path12.join(cacheDir, `${family.toLowerCase().replace(/\s+/g, "-")}-${weight}${storedExt}`);
|
|
3927
4315
|
writeFileSync3(file, bytes);
|
|
3928
|
-
const face = { family, weight, source: `${provenance}:${
|
|
3929
|
-
const mPath =
|
|
3930
|
-
const prior =
|
|
3931
|
-
const merged = [...prior.filter((p) => !(p.family === family && p.weight === weight)), { ...face, file:
|
|
4316
|
+
const face = { family, weight, source: `${provenance}:${path12.basename(src)}`, sha256, file, license: "unknown" };
|
|
4317
|
+
const mPath = path12.join(cacheDir, "manifest.json");
|
|
4318
|
+
const prior = existsSync8(mPath) ? JSON.parse(readFileSync5(mPath, "utf8")) : [];
|
|
4319
|
+
const merged = [...prior.filter((p) => !(p.family === family && p.weight === weight)), { ...face, file: path12.basename(file) }];
|
|
3932
4320
|
writeFileSync3(mPath, `${JSON.stringify(merged, null, 2)}
|
|
3933
4321
|
`);
|
|
3934
4322
|
return face;
|
|
3935
4323
|
}
|
|
3936
4324
|
function checkFontLock(lockPath, cacheDir = DEFAULT_FONT_CACHE) {
|
|
3937
|
-
const lock = JSON.parse(
|
|
3938
|
-
const mPath =
|
|
3939
|
-
const manifest =
|
|
4325
|
+
const lock = JSON.parse(readFileSync5(lockPath, "utf8"));
|
|
4326
|
+
const mPath = path12.join(cacheDir, "manifest.json");
|
|
4327
|
+
const manifest = existsSync8(mPath) ? JSON.parse(readFileSync5(mPath, "utf8")) : [];
|
|
3940
4328
|
return lock.map((l) => {
|
|
3941
4329
|
const m = manifest.find((x) => x.family === l.family && x.weight === l.weight);
|
|
3942
4330
|
if (m === void 0) return { family: l.family, weight: l.weight, status: "missing" };
|
|
@@ -3944,11 +4332,11 @@ function checkFontLock(lockPath, cacheDir = DEFAULT_FONT_CACHE) {
|
|
|
3944
4332
|
});
|
|
3945
4333
|
}
|
|
3946
4334
|
function resolvedFontFamilies(cacheDir = DEFAULT_FONT_CACHE) {
|
|
3947
|
-
const mPath =
|
|
3948
|
-
if (!
|
|
4335
|
+
const mPath = path12.join(cacheDir, "manifest.json");
|
|
4336
|
+
if (!existsSync8(mPath)) return [];
|
|
3949
4337
|
let entries;
|
|
3950
4338
|
try {
|
|
3951
|
-
entries = JSON.parse(
|
|
4339
|
+
entries = JSON.parse(readFileSync5(mPath, "utf8"));
|
|
3952
4340
|
} catch {
|
|
3953
4341
|
return [];
|
|
3954
4342
|
}
|
|
@@ -3962,8 +4350,8 @@ function resolvedFontFamilies(cacheDir = DEFAULT_FONT_CACHE) {
|
|
|
3962
4350
|
return [...byFamily.entries()].map(([family, w]) => ({ family, weights: [...w].sort((a, b) => a - b) }));
|
|
3963
4351
|
}
|
|
3964
4352
|
function requiredFontsManifest(families, cacheDir = DEFAULT_FONT_CACHE) {
|
|
3965
|
-
const mPath =
|
|
3966
|
-
const manifest =
|
|
4353
|
+
const mPath = path12.join(cacheDir, "manifest.json");
|
|
4354
|
+
const manifest = existsSync8(mPath) ? JSON.parse(readFileSync5(mPath, "utf8")) : [];
|
|
3967
4355
|
const wanted = new Set(families.map((f) => f.toLowerCase()));
|
|
3968
4356
|
return manifest.filter((f) => wanted.has(f.family.toLowerCase())).map((f) => ({
|
|
3969
4357
|
family: f.family,
|
|
@@ -3976,11 +4364,11 @@ function requiredFontsManifest(families, cacheDir = DEFAULT_FONT_CACHE) {
|
|
|
3976
4364
|
}));
|
|
3977
4365
|
}
|
|
3978
4366
|
function facesMissingLicense(cacheDir = DEFAULT_FONT_CACHE) {
|
|
3979
|
-
const mPath =
|
|
3980
|
-
if (!
|
|
4367
|
+
const mPath = path12.join(cacheDir, "manifest.json");
|
|
4368
|
+
if (!existsSync8(mPath)) return [];
|
|
3981
4369
|
let entries;
|
|
3982
4370
|
try {
|
|
3983
|
-
entries = JSON.parse(
|
|
4371
|
+
entries = JSON.parse(readFileSync5(mPath, "utf8"));
|
|
3984
4372
|
} catch {
|
|
3985
4373
|
return [];
|
|
3986
4374
|
}
|
|
@@ -3989,20 +4377,20 @@ function facesMissingLicense(cacheDir = DEFAULT_FONT_CACHE) {
|
|
|
3989
4377
|
).map((e) => ({ family: e.family, weight: e.weight, sha256: e.sha256 }));
|
|
3990
4378
|
}
|
|
3991
4379
|
function verifiedFontFamilies(cacheDir = DEFAULT_FONT_CACHE) {
|
|
3992
|
-
const mPath =
|
|
3993
|
-
if (!
|
|
4380
|
+
const mPath = path12.join(cacheDir, "manifest.json");
|
|
4381
|
+
if (!existsSync8(mPath)) return [];
|
|
3994
4382
|
let entries;
|
|
3995
4383
|
try {
|
|
3996
|
-
entries = JSON.parse(
|
|
4384
|
+
entries = JSON.parse(readFileSync5(mPath, "utf8"));
|
|
3997
4385
|
} catch {
|
|
3998
4386
|
return [];
|
|
3999
4387
|
}
|
|
4000
4388
|
const byFamily = /* @__PURE__ */ new Map();
|
|
4001
4389
|
for (const e of entries) {
|
|
4002
4390
|
if (typeof e.family !== "string" || typeof e.weight !== "number" || typeof e.file !== "string") continue;
|
|
4003
|
-
const file =
|
|
4004
|
-
if (!
|
|
4005
|
-
if (
|
|
4391
|
+
const file = path12.isAbsolute(e.file) && existsSync8(e.file) ? e.file : path12.resolve(cacheDir, path12.basename(e.file));
|
|
4392
|
+
if (!existsSync8(file)) continue;
|
|
4393
|
+
if (createHash2("sha256").update(readFileSync5(file)).digest("hex") !== e.sha256) continue;
|
|
4006
4394
|
const set = byFamily.get(e.family) ?? /* @__PURE__ */ new Set();
|
|
4007
4395
|
set.add(e.weight);
|
|
4008
4396
|
byFamily.set(e.family, set);
|
|
@@ -4023,8 +4411,8 @@ function addSystemFamily(family, opts = {}) {
|
|
|
4023
4411
|
const skipped = [];
|
|
4024
4412
|
const overwrote = [];
|
|
4025
4413
|
const cacheDir = opts.cacheDir ?? DEFAULT_FONT_CACHE;
|
|
4026
|
-
const manifestFile =
|
|
4027
|
-
const prior =
|
|
4414
|
+
const manifestFile = path12.join(cacheDir, "manifest.json");
|
|
4415
|
+
const prior = existsSync8(manifestFile) ? JSON.parse(readFileSync5(manifestFile, "utf8")) : [];
|
|
4028
4416
|
const taken = /* @__PURE__ */ new Set();
|
|
4029
4417
|
for (const face of faces) {
|
|
4030
4418
|
const skip = (reason) => skipped.push({ subfamily: face.subfamily, weight: face.weight, reason });
|
|
@@ -4085,18 +4473,18 @@ var init_font_resolve = __esm({
|
|
|
4085
4473
|
});
|
|
4086
4474
|
|
|
4087
4475
|
// packages/verify/src/font-faces.ts
|
|
4088
|
-
import { createHash as
|
|
4089
|
-
import { existsSync as
|
|
4090
|
-
import
|
|
4476
|
+
import { createHash as createHash3 } from "node:crypto";
|
|
4477
|
+
import { existsSync as existsSync9, readFileSync as readFileSync6 } from "node:fs";
|
|
4478
|
+
import path13 from "node:path";
|
|
4091
4479
|
function injectedGroups(manifestPath2) {
|
|
4092
|
-
if (!
|
|
4093
|
-
const claimed = JSON.parse(
|
|
4094
|
-
const resolveFile = (f) =>
|
|
4480
|
+
if (!existsSync9(manifestPath2)) return { groups: [], shared: false };
|
|
4481
|
+
const claimed = JSON.parse(readFileSync6(manifestPath2, "utf8"));
|
|
4482
|
+
const resolveFile = (f) => path13.isAbsolute(f) && existsSync9(f) ? f : path13.resolve(path13.dirname(manifestPath2), path13.basename(f));
|
|
4095
4483
|
const byFile = /* @__PURE__ */ new Map();
|
|
4096
4484
|
for (const f of claimed) {
|
|
4097
4485
|
const file = resolveFile(f.file);
|
|
4098
|
-
if (!
|
|
4099
|
-
if (
|
|
4486
|
+
if (!existsSync9(file)) continue;
|
|
4487
|
+
if (createHash3("sha256").update(readFileSync6(file)).digest("hex") !== f.sha256) continue;
|
|
4100
4488
|
const k = `${f.family}:${f.file}`;
|
|
4101
4489
|
const e = byFile.get(k) ?? { family: f.family, weights: [], file };
|
|
4102
4490
|
e.weights.push(f.weight);
|
|
@@ -4105,14 +4493,14 @@ function injectedGroups(manifestPath2) {
|
|
|
4105
4493
|
const groups = [...byFile.values()];
|
|
4106
4494
|
return { groups, shared: new Set(groups.map((e) => e.file)).size < groups.length };
|
|
4107
4495
|
}
|
|
4108
|
-
function fontFaceCss(manifestPath2 =
|
|
4496
|
+
function fontFaceCss(manifestPath2 = path13.join(fontCacheDir(), "manifest.json")) {
|
|
4109
4497
|
const { groups, shared } = injectedGroups(manifestPath2);
|
|
4110
4498
|
return groups.map((e) => {
|
|
4111
4499
|
const weight = shared || e.weights.length > 1 ? `${SPAN[0]} ${SPAN[1]}` : String(e.weights[0]);
|
|
4112
|
-
return `@font-face { font-family: '${e.family}'; font-weight: ${weight}; font-style: normal; src: url(data:font/woff2;base64,${
|
|
4500
|
+
return `@font-face { font-family: '${e.family}'; font-weight: ${weight}; font-style: normal; src: url(data:font/woff2;base64,${readFileSync6(e.file).toString("base64")}) format('woff2'); }`;
|
|
4113
4501
|
}).join("\n");
|
|
4114
4502
|
}
|
|
4115
|
-
function injectedFamilyWeights(manifestPath2 =
|
|
4503
|
+
function injectedFamilyWeights(manifestPath2 = path13.join(fontCacheDir(), "manifest.json")) {
|
|
4116
4504
|
const { groups, shared } = injectedGroups(manifestPath2);
|
|
4117
4505
|
const out = /* @__PURE__ */ new Map();
|
|
4118
4506
|
for (const g of groups) {
|
|
@@ -4137,17 +4525,17 @@ var init_font_faces = __esm({
|
|
|
4137
4525
|
});
|
|
4138
4526
|
|
|
4139
4527
|
// packages/verify/src/admission.ts
|
|
4140
|
-
import { readFileSync as
|
|
4141
|
-
import
|
|
4528
|
+
import { readFileSync as readFileSync7, readdirSync as readdirSync4, existsSync as existsSync10, writeFileSync as writeFileSync4 } from "node:fs";
|
|
4529
|
+
import path14 from "node:path";
|
|
4142
4530
|
import { build as build2 } from "esbuild";
|
|
4143
4531
|
import postcss from "postcss";
|
|
4144
4532
|
import tailwindcss from "tailwindcss";
|
|
4145
4533
|
import { chromium as chromium2 } from "playwright-core";
|
|
4146
4534
|
function fontWeightsByFamily() {
|
|
4147
|
-
const mPath =
|
|
4535
|
+
const mPath = path14.join(fontCacheDir(), "manifest.json");
|
|
4148
4536
|
const out = /* @__PURE__ */ new Map();
|
|
4149
|
-
if (!
|
|
4150
|
-
for (const f of JSON.parse(
|
|
4537
|
+
if (!existsSync10(mPath)) return out;
|
|
4538
|
+
for (const f of JSON.parse(readFileSync7(mPath, "utf8")))
|
|
4151
4539
|
out.set(f.family, [...out.get(f.family) ?? [], f.weight]);
|
|
4152
4540
|
return out;
|
|
4153
4541
|
}
|
|
@@ -4166,8 +4554,36 @@ var init_admission = __esm({
|
|
|
4166
4554
|
}
|
|
4167
4555
|
});
|
|
4168
4556
|
|
|
4557
|
+
// packages/verify/src/candidate-css.ts
|
|
4558
|
+
import { existsSync as existsSync11, readFileSync as readFileSync8, readdirSync as readdirSync5, statSync as statSync2 } from "node:fs";
|
|
4559
|
+
import path15 from "node:path";
|
|
4560
|
+
function candidateCss(bundleDir) {
|
|
4561
|
+
const files = ["tokens.css", "styles.css"].map((f) => path15.join(bundleDir, f));
|
|
4562
|
+
const composedRoot = path15.join(bundleDir, "composed");
|
|
4563
|
+
let composedDirs = [];
|
|
4564
|
+
try {
|
|
4565
|
+
composedDirs = readdirSync5(composedRoot).sort();
|
|
4566
|
+
} catch {
|
|
4567
|
+
}
|
|
4568
|
+
for (const entry of composedDirs) {
|
|
4569
|
+
const dir = path15.join(composedRoot, entry);
|
|
4570
|
+
try {
|
|
4571
|
+
if (!statSync2(dir).isDirectory()) continue;
|
|
4572
|
+
} catch {
|
|
4573
|
+
continue;
|
|
4574
|
+
}
|
|
4575
|
+
files.push(path15.join(dir, "tokens.css"), path15.join(dir, "styles.css"));
|
|
4576
|
+
}
|
|
4577
|
+
return files.filter((f) => existsSync11(f)).map((f) => readFileSync8(f, "utf8")).join("\n");
|
|
4578
|
+
}
|
|
4579
|
+
var init_candidate_css = __esm({
|
|
4580
|
+
"packages/verify/src/candidate-css.ts"() {
|
|
4581
|
+
"use strict";
|
|
4582
|
+
}
|
|
4583
|
+
});
|
|
4584
|
+
|
|
4169
4585
|
// packages/verify/src/tasks.ts
|
|
4170
|
-
import
|
|
4586
|
+
import path16 from "node:path";
|
|
4171
4587
|
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;
|
|
4172
4588
|
var init_tasks = __esm({
|
|
4173
4589
|
"packages/verify/src/tasks.ts"() {
|
|
@@ -4327,7 +4743,7 @@ BEHAVIORAL CONTRACT (machine-verified, gating): the primary action and close con
|
|
|
4327
4743
|
];
|
|
4328
4744
|
TASKS = {
|
|
4329
4745
|
calendar: {
|
|
4330
|
-
set:
|
|
4746
|
+
set: path16.join(REPO_ROOT, "examples/recordings/shadcn-poc-calendar"),
|
|
4331
4747
|
entry: "Calendar.tsx",
|
|
4332
4748
|
configs: CALENDAR_CONFIGS,
|
|
4333
4749
|
systemApi: CALENDAR_API,
|
|
@@ -4335,7 +4751,7 @@ BEHAVIORAL CONTRACT (machine-verified, gating): the primary action and close con
|
|
|
4335
4751
|
prelude: { controls: ['[data-tendril-part="day"]'], textInputs: [] }
|
|
4336
4752
|
},
|
|
4337
4753
|
"shadcn-button": {
|
|
4338
|
-
set:
|
|
4754
|
+
set: path16.join(REPO_ROOT, "examples/recordings/shadcn-poc-button"),
|
|
4339
4755
|
entry: "Button.tsx",
|
|
4340
4756
|
configs: BUTTON_CONFIGS,
|
|
4341
4757
|
systemApi: BUTTON_API,
|
|
@@ -4343,7 +4759,7 @@ BEHAVIORAL CONTRACT (machine-verified, gating): the primary action and close con
|
|
|
4343
4759
|
prelude: { controls: ["> *"], textInputs: [] }
|
|
4344
4760
|
},
|
|
4345
4761
|
combobox: {
|
|
4346
|
-
set:
|
|
4762
|
+
set: path16.join(REPO_ROOT, "examples/recordings/carbon-poc-combobox"),
|
|
4347
4763
|
entry: "ComboBox.tsx",
|
|
4348
4764
|
configs: COMBO_CONFIGS,
|
|
4349
4765
|
systemApi: COMBO_API,
|
|
@@ -4351,7 +4767,7 @@ BEHAVIORAL CONTRACT (machine-verified, gating): the primary action and close con
|
|
|
4351
4767
|
prelude: { controls: ['[role="option"]'], textInputs: ["input"], popover: { selector: '[role="listbox"]', trigger: "input" } }
|
|
4352
4768
|
},
|
|
4353
4769
|
modal: {
|
|
4354
|
-
set:
|
|
4770
|
+
set: path16.join(REPO_ROOT, "examples/recordings/carbon-poc-modal"),
|
|
4355
4771
|
entry: "Modal.tsx",
|
|
4356
4772
|
configs: MODAL_CONFIGS,
|
|
4357
4773
|
systemApi: MODAL_API,
|
|
@@ -4369,8 +4785,8 @@ __export(behavior_exports, {
|
|
|
4369
4785
|
compileMount: () => compileMount,
|
|
4370
4786
|
recordingIsDark: () => recordingIsDark
|
|
4371
4787
|
});
|
|
4372
|
-
import { existsSync as
|
|
4373
|
-
import
|
|
4788
|
+
import { existsSync as existsSync12, readFileSync as readFileSync9 } from "node:fs";
|
|
4789
|
+
import path17 from "node:path";
|
|
4374
4790
|
import { build as build3 } from "esbuild";
|
|
4375
4791
|
import { chromium as chromium3 } from "playwright-core";
|
|
4376
4792
|
import { PNG as PNG2 } from "pngjs";
|
|
@@ -4379,12 +4795,12 @@ function getFontFaces() {
|
|
|
4379
4795
|
return _fontFaces;
|
|
4380
4796
|
}
|
|
4381
4797
|
async function compileMount(task, bundleDir) {
|
|
4382
|
-
const entryTsx =
|
|
4383
|
-
if (!
|
|
4798
|
+
const entryTsx = path17.join(bundleDir, task.entry);
|
|
4799
|
+
if (!existsSync12(entryTsx)) return { error: `${task.entry} missing` };
|
|
4384
4800
|
const mountSrc = `
|
|
4385
4801
|
import { createElement } from "react";
|
|
4386
4802
|
import { createRoot } from "react-dom/client";
|
|
4387
|
-
import * as B from ${JSON.stringify(
|
|
4803
|
+
import * as B from ${JSON.stringify(path17.resolve(entryTsx))};
|
|
4388
4804
|
const cfg = (window as unknown as { __cfg: { component: string; props: Record<string, unknown>; spyProps?: string[] } }).__cfg;
|
|
4389
4805
|
const C = (B as Record<string, unknown>)[cfg.component] as Parameters<typeof createElement>[0] | undefined;
|
|
4390
4806
|
// Callbacks cannot ride the JSON config: specs NAME spy props and the
|
|
@@ -4665,10 +5081,10 @@ async function runSteps(page, spec, renderPose) {
|
|
|
4665
5081
|
function recordingIsDark(task) {
|
|
4666
5082
|
const rep = task.configs[0]?.rep;
|
|
4667
5083
|
if (rep === void 0) return false;
|
|
4668
|
-
const f =
|
|
4669
|
-
if (!
|
|
5084
|
+
const f = path17.join(task.set, rep, "get_screenshot.json");
|
|
5085
|
+
if (!existsSync12(f)) return false;
|
|
4670
5086
|
try {
|
|
4671
|
-
const env = JSON.parse(
|
|
5087
|
+
const env = JSON.parse(readFileSync9(f, "utf8")).content.find((c) => c.type === "image");
|
|
4672
5088
|
if (env?.data === void 0) return false;
|
|
4673
5089
|
const png = PNG2.sync.read(Buffer.from(env.data, "base64"));
|
|
4674
5090
|
let sum = 0;
|
|
@@ -4740,7 +5156,7 @@ async function checkBehaviors(task, bundleDir, opts = {}) {
|
|
|
4740
5156
|
const deadlineMs = timeoutMs + 1e4;
|
|
4741
5157
|
const js = await compileMount(task, bundleDir);
|
|
4742
5158
|
if (typeof js !== "string") return task.behaviors.map((b) => ({ id: b.id, pass: false, detail: js.error }));
|
|
4743
|
-
const css =
|
|
5159
|
+
const css = candidateCss(bundleDir);
|
|
4744
5160
|
const server = await chromium3.launchServer({ executablePath: resolveChrome(), headless: true, args: mountArgs() });
|
|
4745
5161
|
const browser = await chromium3.connect(server.wsEndpoint());
|
|
4746
5162
|
const results = [];
|
|
@@ -4851,6 +5267,7 @@ var init_behavior = __esm({
|
|
|
4851
5267
|
"use strict";
|
|
4852
5268
|
init_runtime();
|
|
4853
5269
|
init_browser();
|
|
5270
|
+
init_candidate_css();
|
|
4854
5271
|
init_font_faces();
|
|
4855
5272
|
init_image_diff();
|
|
4856
5273
|
init_mount_limits();
|
|
@@ -4883,8 +5300,8 @@ var init_behavior = __esm({
|
|
|
4883
5300
|
});
|
|
4884
5301
|
|
|
4885
5302
|
// packages/verify/src/bundle-quality.ts
|
|
4886
|
-
import { readFileSync as
|
|
4887
|
-
import
|
|
5303
|
+
import { readFileSync as readFileSync10, rmSync as rmSync2, writeFileSync as writeFileSync5, existsSync as existsSync13 } from "node:fs";
|
|
5304
|
+
import path18 from "node:path";
|
|
4888
5305
|
function definedVars(tokensCss) {
|
|
4889
5306
|
if (tokensCss === void 0) return void 0;
|
|
4890
5307
|
const names = /* @__PURE__ */ new Set();
|
|
@@ -4893,19 +5310,19 @@ function definedVars(tokensCss) {
|
|
|
4893
5310
|
}
|
|
4894
5311
|
function recordedTokenMapState(setDir, reps) {
|
|
4895
5312
|
const readMap = (file) => {
|
|
4896
|
-
if (!
|
|
5313
|
+
if (!existsSync13(file)) return void 0;
|
|
4897
5314
|
try {
|
|
4898
|
-
const parsed = JSON.parse(envelopeFirstTextPart(JSON.parse(
|
|
5315
|
+
const parsed = JSON.parse(envelopeFirstTextPart(JSON.parse(readFileSync10(file, "utf8"))) || "{}");
|
|
4899
5316
|
return parsed !== null && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : {};
|
|
4900
5317
|
} catch {
|
|
4901
5318
|
return {};
|
|
4902
5319
|
}
|
|
4903
5320
|
};
|
|
4904
|
-
const setLevel = readMap(
|
|
5321
|
+
const setLevel = readMap(path18.join(setDir, "get_variable_defs.json"));
|
|
4905
5322
|
if (setLevel !== void 0) return Object.keys(setLevel).length === 0 ? "empty" : "populated";
|
|
4906
5323
|
let recorded = false;
|
|
4907
5324
|
for (const rep of reps) {
|
|
4908
|
-
const m = readMap(
|
|
5325
|
+
const m = readMap(path18.join(setDir, rep, "get_variable_defs.json"));
|
|
4909
5326
|
if (m === void 0) continue;
|
|
4910
5327
|
recorded = true;
|
|
4911
5328
|
if (Object.keys(m).length > 0) return "populated";
|
|
@@ -4970,11 +5387,11 @@ function fontStackFindings(sheets, coverage) {
|
|
|
4970
5387
|
}
|
|
4971
5388
|
async function checkBundleQuality(bundleDir, entry, set, fontManifest) {
|
|
4972
5389
|
const findings = [];
|
|
4973
|
-
const entryPath =
|
|
4974
|
-
const cssPath =
|
|
4975
|
-
const tokensPath =
|
|
4976
|
-
const css =
|
|
4977
|
-
const tokensCss =
|
|
5390
|
+
const entryPath = path18.join(bundleDir, entry);
|
|
5391
|
+
const cssPath = path18.join(bundleDir, "styles.css");
|
|
5392
|
+
const tokensPath = path18.join(bundleDir, "tokens.css");
|
|
5393
|
+
const css = existsSync13(cssPath) ? readFileSync10(cssPath, "utf8") : "";
|
|
5394
|
+
const tokensCss = existsSync13(tokensPath) ? readFileSync10(tokensPath, "utf8") : void 0;
|
|
4978
5395
|
findings.push(
|
|
4979
5396
|
...fontStackFindings(
|
|
4980
5397
|
[
|
|
@@ -4984,11 +5401,11 @@ async function checkBundleQuality(bundleDir, entry, set, fontManifest) {
|
|
|
4984
5401
|
injectedFamilyWeights(fontManifest)
|
|
4985
5402
|
)
|
|
4986
5403
|
);
|
|
4987
|
-
if (
|
|
5404
|
+
if (existsSync13(entryPath)) {
|
|
4988
5405
|
const workDir = newScratchDir("quality");
|
|
4989
5406
|
try {
|
|
4990
|
-
const tsxPath =
|
|
4991
|
-
writeFileSync5(tsxPath,
|
|
5407
|
+
const tsxPath = path18.join(workDir, entry);
|
|
5408
|
+
writeFileSync5(tsxPath, readFileSync10(entryPath, "utf8"));
|
|
4992
5409
|
for (const d of runTscStrict([tsxPath]).diagnostics) {
|
|
4993
5410
|
findings.push({ kind: "tsc", file: entry, ...d.line === void 0 ? {} : { line: d.line }, message: `TS${d.code}: ${d.message}` });
|
|
4994
5411
|
}
|
|
@@ -5063,8 +5480,8 @@ var init_effect_geometry = __esm({
|
|
|
5063
5480
|
});
|
|
5064
5481
|
|
|
5065
5482
|
// packages/verify/src/bundle-score.ts
|
|
5066
|
-
import { existsSync as
|
|
5067
|
-
import
|
|
5483
|
+
import { existsSync as existsSync14, mkdirSync as mkdirSync3, readFileSync as readFileSync11, writeFileSync as writeFileSync6 } from "node:fs";
|
|
5484
|
+
import path19 from "node:path";
|
|
5068
5485
|
import { build as build4 } from "esbuild";
|
|
5069
5486
|
import { chromium as chromium4 } from "playwright-core";
|
|
5070
5487
|
function getFontFaces2() {
|
|
@@ -5118,7 +5535,7 @@ function chooseSeat(cropAt, ref, padW, padH, backdrop, origin, extents) {
|
|
|
5118
5535
|
}
|
|
5119
5536
|
function metadataRoot(set, rep) {
|
|
5120
5537
|
try {
|
|
5121
|
-
const text = JSON.parse(
|
|
5538
|
+
const text = JSON.parse(readFileSync11(path19.join(set, rep, "get_metadata.json"), "utf8")).content.map((c) => c.text ?? "").join("\n");
|
|
5122
5539
|
return parseMetadataStructure(text);
|
|
5123
5540
|
} catch {
|
|
5124
5541
|
return void 0;
|
|
@@ -5173,19 +5590,19 @@ function smallSemanticNodes(set, rep, maxArea = 1024) {
|
|
|
5173
5590
|
});
|
|
5174
5591
|
}
|
|
5175
5592
|
function repMeta(set, rep) {
|
|
5176
|
-
const text = JSON.parse(
|
|
5593
|
+
const text = JSON.parse(readFileSync11(path19.join(set, rep, "get_metadata.json"), "utf8")).content.map((c) => c.text ?? "").join("\n");
|
|
5177
5594
|
const root = parseMetadataStructure(text);
|
|
5178
5595
|
return { w: Math.round(root.width ?? 100), h: Math.round(root.height ?? 40) };
|
|
5179
5596
|
}
|
|
5180
5597
|
function repRef(set, rep) {
|
|
5181
|
-
const env = JSON.parse(
|
|
5598
|
+
const env = JSON.parse(readFileSync11(path19.join(set, rep, "get_screenshot.json"), "utf8")).content.find((c) => c.type === "image");
|
|
5182
5599
|
return Uint8Array.from(Buffer.from(env?.data ?? "", "base64"));
|
|
5183
5600
|
}
|
|
5184
5601
|
function repEffectExtents(set, rep) {
|
|
5185
|
-
const file =
|
|
5186
|
-
if (!
|
|
5602
|
+
const file = path19.join(set, rep, "get_design_context.json");
|
|
5603
|
+
if (!existsSync14(file)) return void 0;
|
|
5187
5604
|
try {
|
|
5188
|
-
const text = JSON.parse(
|
|
5605
|
+
const text = JSON.parse(readFileSync11(file, "utf8")).content.map((c) => c.text ?? "").join("\n");
|
|
5189
5606
|
const extents = shadowExtents(text);
|
|
5190
5607
|
return extents.top + extents.right + extents.bottom + extents.left > 0 ? extents : void 0;
|
|
5191
5608
|
} catch {
|
|
@@ -5196,13 +5613,13 @@ async function scoreBundleForTask(task, bundleDir, bar = BAR, opts = {}) {
|
|
|
5196
5613
|
if (opts.evidenceDir !== void 0) mkdirSync3(opts.evidenceDir, { recursive: true });
|
|
5197
5614
|
const CONFIGS2 = task.configs;
|
|
5198
5615
|
const timeoutMs = opts.mountDeadlineMs ?? ENGINE_MOUNT_DEADLINE_MS;
|
|
5199
|
-
const entryTsx =
|
|
5200
|
-
if (!
|
|
5201
|
-
const css =
|
|
5616
|
+
const entryTsx = path19.join(bundleDir, task.entry);
|
|
5617
|
+
if (!existsSync14(entryTsx)) return CONFIGS2.map((c) => ({ rep: c.rep, similarity: 0, inkRecall: 0, exact: { similarity: 0, inkRecall: 0 }, pass: false, error: `${task.entry} missing` }));
|
|
5618
|
+
const css = candidateCss(bundleDir);
|
|
5202
5619
|
const mountSrc = `
|
|
5203
5620
|
import { createElement } from "react";
|
|
5204
5621
|
import { createRoot } from "react-dom/client";
|
|
5205
|
-
import * as B from ${JSON.stringify(
|
|
5622
|
+
import * as B from ${JSON.stringify(path19.resolve(entryTsx))};
|
|
5206
5623
|
const cfg = (window as unknown as { __cfg: { component: string; props: Record<string, unknown> } }).__cfg;
|
|
5207
5624
|
const C = (B as Record<string, unknown>)[cfg.component] as Parameters<typeof createElement>[0] | undefined;
|
|
5208
5625
|
const root = document.getElementById("root");
|
|
@@ -5297,12 +5714,12 @@ body{margin:0;background:rgb(${backdrop.join(",")})}
|
|
|
5297
5714
|
return name === void 0 ? c : { ...c, name };
|
|
5298
5715
|
});
|
|
5299
5716
|
if (opts.evidenceDir !== void 0) {
|
|
5300
|
-
writeFileSync6(
|
|
5301
|
-
writeFileSync6(
|
|
5302
|
-
writeFileSync6(
|
|
5717
|
+
writeFileSync6(path19.join(opts.evidenceDir, `${cfg.rep}-render.png`), scored);
|
|
5718
|
+
writeFileSync6(path19.join(opts.evidenceDir, `${cfg.rep}-ref.png`), ref);
|
|
5719
|
+
writeFileSync6(path19.join(opts.evidenceDir, `${cfg.rep}-diff.png`), diffPng(scored, ref));
|
|
5303
5720
|
for (const [i, c] of absent.slice(0, 3).entries()) {
|
|
5304
|
-
writeFileSync6(
|
|
5305
|
-
writeFileSync6(
|
|
5721
|
+
writeFileSync6(path19.join(opts.evidenceDir, `${cfg.rep}-absent-${i}-ref.png`), zoomCrop(ref, c));
|
|
5722
|
+
writeFileSync6(path19.join(opts.evidenceDir, `${cfg.rep}-absent-${i}-render.png`), zoomCrop(scored, c));
|
|
5306
5723
|
}
|
|
5307
5724
|
}
|
|
5308
5725
|
return {
|
|
@@ -5366,6 +5783,7 @@ var init_bundle_score = __esm({
|
|
|
5366
5783
|
"use strict";
|
|
5367
5784
|
init_runtime();
|
|
5368
5785
|
init_browser();
|
|
5786
|
+
init_candidate_css();
|
|
5369
5787
|
init_src();
|
|
5370
5788
|
init_effect_geometry();
|
|
5371
5789
|
init_image_diff();
|
|
@@ -5410,8 +5828,6 @@ var init_prelude = __esm({
|
|
|
5410
5828
|
});
|
|
5411
5829
|
|
|
5412
5830
|
// packages/verify/src/parity.ts
|
|
5413
|
-
import { existsSync as existsSync13, readFileSync as readFileSync10 } from "node:fs";
|
|
5414
|
-
import path18 from "node:path";
|
|
5415
5831
|
import { chromium as chromium6 } from "playwright-core";
|
|
5416
5832
|
function getFontFaces3() {
|
|
5417
5833
|
_fontFaces3 ??= fontFaceCss();
|
|
@@ -5440,7 +5856,7 @@ async function checkHoverParity(task, bundleDir, authority, opts = {}) {
|
|
|
5440
5856
|
const deadlineMs = timeoutMs + 1e4;
|
|
5441
5857
|
const js = await compileMount(task, bundleDir);
|
|
5442
5858
|
if (typeof js !== "string") return configs.map((c) => ({ id: `parity:${c.rep}`, pass: false, detail: js.error }));
|
|
5443
|
-
const css =
|
|
5859
|
+
const css = candidateCss(bundleDir);
|
|
5444
5860
|
const server = await chromium6.launchServer({ executablePath: resolveChrome(), headless: true, args: mountArgs() });
|
|
5445
5861
|
const browser = await chromium6.connect(server.wsEndpoint());
|
|
5446
5862
|
const results = [];
|
|
@@ -5537,6 +5953,7 @@ var init_parity = __esm({
|
|
|
5537
5953
|
"packages/verify/src/parity.ts"() {
|
|
5538
5954
|
"use strict";
|
|
5539
5955
|
init_browser();
|
|
5956
|
+
init_candidate_css();
|
|
5540
5957
|
init_behavior();
|
|
5541
5958
|
init_font_faces();
|
|
5542
5959
|
init_mount_limits();
|
|
@@ -5546,8 +5963,8 @@ var init_parity = __esm({
|
|
|
5546
5963
|
|
|
5547
5964
|
// packages/verify/src/composition.ts
|
|
5548
5965
|
import { createRequire as createRequire2 } from "node:module";
|
|
5549
|
-
import { existsSync as
|
|
5550
|
-
import
|
|
5966
|
+
import { existsSync as existsSync15 } from "node:fs";
|
|
5967
|
+
import path20 from "node:path";
|
|
5551
5968
|
import { build as build6 } from "esbuild";
|
|
5552
5969
|
import { chromium as chromium7 } from "playwright-core";
|
|
5553
5970
|
function getFontFaces4() {
|
|
@@ -5555,9 +5972,9 @@ function getFontFaces4() {
|
|
|
5555
5972
|
return _fontFaces4;
|
|
5556
5973
|
}
|
|
5557
5974
|
async function compileInstrumentedMount(task, bundleDir) {
|
|
5558
|
-
const entryTsx =
|
|
5559
|
-
if (!
|
|
5560
|
-
const requireFromVerify = createRequire2(
|
|
5975
|
+
const entryTsx = path20.join(bundleDir, task.entry);
|
|
5976
|
+
if (!existsSync15(entryTsx)) return { error: `${task.entry} missing` };
|
|
5977
|
+
const requireFromVerify = createRequire2(path20.join(VERIFY_PKG_DIR, "package.json"));
|
|
5561
5978
|
let realJsxPath;
|
|
5562
5979
|
try {
|
|
5563
5980
|
realJsxPath = requireFromVerify.resolve("react/jsx-runtime");
|
|
@@ -5568,7 +5985,7 @@ async function compileInstrumentedMount(task, bundleDir) {
|
|
|
5568
5985
|
import { createElement } from "react";
|
|
5569
5986
|
import { createRoot } from "react-dom/client";
|
|
5570
5987
|
import { __registerParts } from "react/jsx-runtime";
|
|
5571
|
-
import * as B from ${JSON.stringify(
|
|
5988
|
+
import * as B from ${JSON.stringify(path20.resolve(entryTsx))};
|
|
5572
5989
|
const cfg = (window as unknown as { __cfg: { component: string; props: Record<string, unknown>; partComponents: string[] } }).__cfg;
|
|
5573
5990
|
const pairs: Array<[unknown, string]> = [];
|
|
5574
5991
|
for (const name of cfg.partComponents) {
|
|
@@ -5623,7 +6040,7 @@ function expectedParts(task, roles, mainSlug) {
|
|
|
5623
6040
|
}
|
|
5624
6041
|
function interiorRegions(setDir, roles) {
|
|
5625
6042
|
const mains = roles.main;
|
|
5626
|
-
const withInterior = mains.filter((m) =>
|
|
6043
|
+
const withInterior = mains.filter((m) => existsSync15(path20.join(setDir, m, "get_metadata_interior.json")));
|
|
5627
6044
|
if (withInterior.length === 0) {
|
|
5628
6045
|
return { unavailable: "no interior geometry recorded for any main (pre-obligation set) \u2014 re-record with `tendril record` to enable crop checks" };
|
|
5629
6046
|
}
|
|
@@ -5640,7 +6057,7 @@ async function checkCropComposition(task, bundleDir, regions, opts = {}) {
|
|
|
5640
6057
|
if (typeof js !== "string") {
|
|
5641
6058
|
return regions.map((r) => ({ id: cropId(r), pass: false, similarity: 0, inkRecall: 0, detail: js.error }));
|
|
5642
6059
|
}
|
|
5643
|
-
const css =
|
|
6060
|
+
const css = candidateCss(bundleDir);
|
|
5644
6061
|
const server = await chromium7.launchServer({ executablePath: resolveChrome(), headless: true, args: mountArgs() });
|
|
5645
6062
|
const browser = await chromium7.connect(server.wsEndpoint());
|
|
5646
6063
|
const PAD = 4;
|
|
@@ -5745,7 +6162,7 @@ async function checkStructuralComposition(task, bundleDir, roles, opts = {}) {
|
|
|
5745
6162
|
const deadlineMs = timeoutMs + 1e4;
|
|
5746
6163
|
const js = await compileInstrumentedMount(task, bundleDir);
|
|
5747
6164
|
if (typeof js !== "string") return [...results, ...mains.map((m) => ({ id: `composition:${m}`, pass: false, detail: js.error }))];
|
|
5748
|
-
const css =
|
|
6165
|
+
const css = candidateCss(bundleDir);
|
|
5749
6166
|
const server = await chromium7.launchServer({ executablePath: resolveChrome(), headless: true, args: mountArgs() });
|
|
5750
6167
|
const browser = await chromium7.connect(server.wsEndpoint());
|
|
5751
6168
|
try {
|
|
@@ -5814,6 +6231,7 @@ var init_composition = __esm({
|
|
|
5814
6231
|
"use strict";
|
|
5815
6232
|
init_runtime();
|
|
5816
6233
|
init_browser();
|
|
6234
|
+
init_candidate_css();
|
|
5817
6235
|
init_font_faces();
|
|
5818
6236
|
init_image_diff();
|
|
5819
6237
|
init_mount_limits();
|
|
@@ -5835,17 +6253,17 @@ export const jsxs = (t, p, k) => real.jsxs(t, inject(t, p), k);
|
|
|
5835
6253
|
});
|
|
5836
6254
|
|
|
5837
6255
|
// packages/verify/src/occlusion.ts
|
|
5838
|
-
import { existsSync as
|
|
5839
|
-
import
|
|
6256
|
+
import { existsSync as existsSync16 } from "node:fs";
|
|
6257
|
+
import path21 from "node:path";
|
|
5840
6258
|
import { build as build7 } from "esbuild";
|
|
5841
6259
|
import { chromium as chromium8 } from "playwright-core";
|
|
5842
6260
|
async function compileTwoUp(task, bundleDir) {
|
|
5843
|
-
const entryTsx =
|
|
5844
|
-
if (!
|
|
6261
|
+
const entryTsx = path21.join(bundleDir, task.entry);
|
|
6262
|
+
if (!existsSync16(entryTsx)) return { error: `${task.entry} missing` };
|
|
5845
6263
|
const src = `
|
|
5846
6264
|
import { createElement } from "react";
|
|
5847
6265
|
import { createRoot } from "react-dom/client";
|
|
5848
|
-
import * as B from ${JSON.stringify(
|
|
6266
|
+
import * as B from ${JSON.stringify(path21.resolve(entryTsx))};
|
|
5849
6267
|
const cfg = (window as unknown as { __cfg: { component: string; props: Record<string, unknown> } }).__cfg;
|
|
5850
6268
|
const C = (B as Record<string, unknown>)[cfg.component] as Parameters<typeof createElement>[0] | undefined;
|
|
5851
6269
|
for (const id of ["first", "second"]) {
|
|
@@ -6019,32 +6437,33 @@ var init_src4 = __esm({
|
|
|
6019
6437
|
init_font_resolve();
|
|
6020
6438
|
init_paths();
|
|
6021
6439
|
init_parity();
|
|
6440
|
+
init_candidate_css();
|
|
6022
6441
|
init_composition();
|
|
6023
6442
|
init_occlusion();
|
|
6024
6443
|
}
|
|
6025
6444
|
});
|
|
6026
6445
|
|
|
6027
6446
|
// packages/cli/src/environment.ts
|
|
6028
|
-
import { existsSync as
|
|
6029
|
-
import
|
|
6030
|
-
import { createHash as
|
|
6447
|
+
import { existsSync as existsSync17, readFileSync as readFileSync13 } from "node:fs";
|
|
6448
|
+
import path22 from "node:path";
|
|
6449
|
+
import { createHash as createHash4 } from "node:crypto";
|
|
6031
6450
|
import { fileURLToPath as fileURLToPath5 } from "node:url";
|
|
6032
6451
|
function cliVersion() {
|
|
6033
6452
|
try {
|
|
6034
|
-
return JSON.parse(
|
|
6453
|
+
return JSON.parse(readFileSync13(path22.join(path22.dirname(fileURLToPath5(import.meta.url)), "..", "package.json"), "utf8")).version ?? "dev";
|
|
6035
6454
|
} catch {
|
|
6036
6455
|
return "dev";
|
|
6037
6456
|
}
|
|
6038
6457
|
}
|
|
6039
6458
|
function environmentStamp(taskFamilies) {
|
|
6040
|
-
const manifestPath2 =
|
|
6459
|
+
const manifestPath2 = path22.join(fontCacheDir(), "manifest.json");
|
|
6041
6460
|
let fontsHash = null;
|
|
6042
|
-
if (
|
|
6461
|
+
if (existsSync17(manifestPath2)) {
|
|
6043
6462
|
try {
|
|
6044
|
-
const entries = JSON.parse(
|
|
6463
|
+
const entries = JSON.parse(readFileSync13(manifestPath2, "utf8"));
|
|
6045
6464
|
const scoped = taskFamilies === void 0 || taskFamilies === null ? entries : entries.filter((f) => taskFamilies.some((fam) => fam.toLowerCase() === f.family.toLowerCase()));
|
|
6046
6465
|
const faces = scoped.map((f) => `${f.family}:${f.weight}:${f.sha256}`).sort();
|
|
6047
|
-
fontsHash = faces.length === 0 ? null :
|
|
6466
|
+
fontsHash = faces.length === 0 ? null : createHash4("sha256").update(faces.join("\n")).digest("hex").slice(0, 16);
|
|
6048
6467
|
} catch {
|
|
6049
6468
|
fontsHash = null;
|
|
6050
6469
|
}
|
|
@@ -6085,8 +6504,8 @@ var init_describe = __esm({
|
|
|
6085
6504
|
});
|
|
6086
6505
|
|
|
6087
6506
|
// packages/cli/src/env.ts
|
|
6088
|
-
import { existsSync as
|
|
6089
|
-
import
|
|
6507
|
+
import { existsSync as existsSync18, readFileSync as readFileSync14 } from "node:fs";
|
|
6508
|
+
import path23 from "node:path";
|
|
6090
6509
|
function parseEnv(content) {
|
|
6091
6510
|
const entries = /* @__PURE__ */ new Map();
|
|
6092
6511
|
for (const line of content.split("\n")) {
|
|
@@ -6098,9 +6517,9 @@ function parseEnv(content) {
|
|
|
6098
6517
|
function resolveCredential(name) {
|
|
6099
6518
|
const fromProcess = process.env[name];
|
|
6100
6519
|
if (fromProcess) return fromProcess;
|
|
6101
|
-
const envPath =
|
|
6102
|
-
if (!
|
|
6103
|
-
return parseEnv(
|
|
6520
|
+
const envPath = path23.resolve(process.env["INIT_CWD"] ?? process.cwd(), ".env");
|
|
6521
|
+
if (!existsSync18(envPath)) return void 0;
|
|
6522
|
+
return parseEnv(readFileSync14(envPath, "utf8")).get(name);
|
|
6104
6523
|
}
|
|
6105
6524
|
var init_env = __esm({
|
|
6106
6525
|
"packages/cli/src/env.ts"() {
|
|
@@ -6160,17 +6579,17 @@ var init_output = __esm({
|
|
|
6160
6579
|
});
|
|
6161
6580
|
|
|
6162
6581
|
// packages/cli/src/entitlement.ts
|
|
6163
|
-
import { chmodSync, existsSync as
|
|
6582
|
+
import { chmodSync, existsSync as existsSync19, mkdirSync as mkdirSync4, readFileSync as readFileSync15, writeFileSync as writeFileSync7 } from "node:fs";
|
|
6164
6583
|
import crypto from "node:crypto";
|
|
6165
6584
|
import os4 from "node:os";
|
|
6166
|
-
import
|
|
6585
|
+
import path24 from "node:path";
|
|
6167
6586
|
function entitlementPath() {
|
|
6168
|
-
return process.env["TENDRIL_ENTITLEMENT_PATH"] ??
|
|
6587
|
+
return process.env["TENDRIL_ENTITLEMENT_PATH"] ?? path24.join(os4.homedir(), ".tendril", "entitlement.json");
|
|
6169
6588
|
}
|
|
6170
6589
|
function readStoredEntitlement(file = entitlementPath()) {
|
|
6171
|
-
if (!
|
|
6590
|
+
if (!existsSync19(file)) return void 0;
|
|
6172
6591
|
try {
|
|
6173
|
-
const parsed = JSON.parse(
|
|
6592
|
+
const parsed = JSON.parse(readFileSync15(file, "utf8"));
|
|
6174
6593
|
if (typeof parsed.token !== "string" || typeof parsed.lastRefreshAt !== "number") return void 0;
|
|
6175
6594
|
return { token: parsed.token, lastRefreshAt: parsed.lastRefreshAt };
|
|
6176
6595
|
} catch {
|
|
@@ -6178,7 +6597,7 @@ function readStoredEntitlement(file = entitlementPath()) {
|
|
|
6178
6597
|
}
|
|
6179
6598
|
}
|
|
6180
6599
|
function writeStoredEntitlement(stored, file = entitlementPath()) {
|
|
6181
|
-
mkdirSync4(
|
|
6600
|
+
mkdirSync4(path24.dirname(file), { recursive: true });
|
|
6182
6601
|
writeFileSync7(file, `${JSON.stringify(stored, null, 2)}
|
|
6183
6602
|
`);
|
|
6184
6603
|
chmodSync(file, 384);
|
|
@@ -6263,9 +6682,9 @@ var init_entitlement = __esm({
|
|
|
6263
6682
|
|
|
6264
6683
|
// packages/cli/src/commands/doctor.ts
|
|
6265
6684
|
import { spawnSync } from "node:child_process";
|
|
6266
|
-
import { existsSync as
|
|
6685
|
+
import { existsSync as existsSync20, readFileSync as readFileSync16, readdirSync as readdirSync6 } from "node:fs";
|
|
6267
6686
|
import os5 from "node:os";
|
|
6268
|
-
import
|
|
6687
|
+
import path25 from "node:path";
|
|
6269
6688
|
function withDeadline(work, ms) {
|
|
6270
6689
|
return Promise.race([
|
|
6271
6690
|
work,
|
|
@@ -6325,19 +6744,19 @@ async function runDoctorChecks(options) {
|
|
|
6325
6744
|
remediation: "Install Google Chrome (or Chromium), or set CHROME_PATH to a Chromium-family browser binary."
|
|
6326
6745
|
});
|
|
6327
6746
|
}
|
|
6328
|
-
const fontManifest =
|
|
6747
|
+
const fontManifest = path25.join(fontCacheDir(), "manifest.json");
|
|
6329
6748
|
checks.push(
|
|
6330
|
-
|
|
6749
|
+
existsSync20(fontManifest) ? { name: "font-cache", ok: true, detail: `resolved font cache at ${fontCacheDir()} (${JSON.parse(readFileSync16(fontManifest, "utf8")).length} faces)` } : {
|
|
6331
6750
|
name: "font-cache",
|
|
6332
6751
|
ok: true,
|
|
6333
6752
|
detail: `font cache empty at ${fontCacheDir()} \u2014 normal on a fresh machine; faces resolve per design system at first use`,
|
|
6334
6753
|
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.`
|
|
6335
6754
|
}
|
|
6336
6755
|
);
|
|
6337
|
-
const pluginRoot =
|
|
6338
|
-
if (
|
|
6756
|
+
const pluginRoot = path25.join(os5.homedir(), ".claude", "plugins", "cache", "tendrilapp", "tendril");
|
|
6757
|
+
if (existsSync20(pluginRoot)) {
|
|
6339
6758
|
try {
|
|
6340
|
-
const versions =
|
|
6759
|
+
const versions = readdirSync6(pluginRoot).filter((v) => /^\d+\.\d+\.\d+$/.test(v));
|
|
6341
6760
|
const newest = versions.sort((a, b) => versionIsNewer(a, b) ? 1 : -1)[0];
|
|
6342
6761
|
if (newest !== void 0) {
|
|
6343
6762
|
const skewed = versionIsNewer(newest, cliVersion());
|
|
@@ -6482,7 +6901,7 @@ var init_doctor = __esm({
|
|
|
6482
6901
|
});
|
|
6483
6902
|
|
|
6484
6903
|
// packages/llm/src/model-config.ts
|
|
6485
|
-
import { z as
|
|
6904
|
+
import { z as z7 } from "zod";
|
|
6486
6905
|
function resolveModel(config, requestedId) {
|
|
6487
6906
|
const entry = config.allowlist.find((m) => m.id === requestedId);
|
|
6488
6907
|
if (entry) return { ok: true, entry };
|
|
@@ -6504,46 +6923,46 @@ var ModelStatusSchema, ModelEntrySchema, ModelConfigSchema, DEFAULT_MODEL_CONFIG
|
|
|
6504
6923
|
var init_model_config = __esm({
|
|
6505
6924
|
"packages/llm/src/model-config.ts"() {
|
|
6506
6925
|
"use strict";
|
|
6507
|
-
ModelStatusSchema =
|
|
6508
|
-
ModelEntrySchema =
|
|
6926
|
+
ModelStatusSchema = z7.enum(["verified", "degraded"]);
|
|
6927
|
+
ModelEntrySchema = z7.object({
|
|
6509
6928
|
/** OpenRouter model id, e.g. "deepseek/deepseek-v4-flash". */
|
|
6510
|
-
id:
|
|
6929
|
+
id: z7.string(),
|
|
6511
6930
|
status: ModelStatusSchema,
|
|
6512
6931
|
/** Roles this model may fill in the pipeline. */
|
|
6513
|
-
roles:
|
|
6932
|
+
roles: z7.array(z7.enum(["bulk", "polish"])),
|
|
6514
6933
|
/** Accepts image input. Text-only models get the screenshot dropped with a
|
|
6515
6934
|
* warning instead of a provider 404 (Carbon run 1 finding). */
|
|
6516
|
-
vision:
|
|
6935
|
+
vision: z7.boolean().default(false),
|
|
6517
6936
|
/** Engine-adapter params shipped WITH the entry (ADR-011 §2): the
|
|
6518
6937
|
* eval-proven settings ride the allowlist so users never rediscover a
|
|
6519
6938
|
* model's quirks at their own expense (K2.7-code burned its whole
|
|
6520
6939
|
* completion budget on mandatory reasoning and emitted nothing). */
|
|
6521
|
-
adapter:
|
|
6940
|
+
adapter: z7.object({
|
|
6522
6941
|
/** Minimum completion budget the model needs to emit full files. */
|
|
6523
|
-
maxTokens:
|
|
6942
|
+
maxTokens: z7.number().optional(),
|
|
6524
6943
|
/** "mandatory": provider refuses reasoning-off; budget accordingly. */
|
|
6525
|
-
reasoning:
|
|
6944
|
+
reasoning: z7.enum(["mandatory", "optional", "none"]).optional()
|
|
6526
6945
|
}).optional(),
|
|
6527
6946
|
/** One-line pointer to the evidence behind `status` (which eval, when).
|
|
6528
6947
|
* Pricing intentionally has no static field — the resolution path is
|
|
6529
6948
|
* the provider's models endpoint at run time (ADR-011 §2). */
|
|
6530
|
-
evidence:
|
|
6949
|
+
evidence: z7.string().optional()
|
|
6531
6950
|
});
|
|
6532
|
-
ModelConfigSchema =
|
|
6951
|
+
ModelConfigSchema = z7.object({
|
|
6533
6952
|
/** Default model for codegen + repair (pipeline steps 4–5, facts path). */
|
|
6534
|
-
bulk:
|
|
6953
|
+
bulk: z7.string(),
|
|
6535
6954
|
/** Optional premium polish model (pipeline step 6, aesthetics/naming only). */
|
|
6536
|
-
polish:
|
|
6955
|
+
polish: z7.string().optional(),
|
|
6537
6956
|
/** ADR-011 §2 default-model pointer for the curated recorded-truth
|
|
6538
6957
|
* engine (a separate axis from the facts-era `bulk`; `generate` flips
|
|
6539
6958
|
* to it in ADR-010 slice 6). Moves only via the reference-corpus gate. */
|
|
6540
|
-
curatedDefault:
|
|
6959
|
+
curatedDefault: z7.string().optional(),
|
|
6541
6960
|
/** ADR-011 §3a Rung-1 escalation target: the model the curated loop
|
|
6542
6961
|
* offers for configs still sub-bar at plateau. Designated ONLY via
|
|
6543
6962
|
* the register's gate (a real curated cert-target run on the measured
|
|
6544
6963
|
* escalation case); empty means no Rung-1 offer, never a default. */
|
|
6545
|
-
escalationTarget:
|
|
6546
|
-
allowlist:
|
|
6964
|
+
escalationTarget: z7.string().optional(),
|
|
6965
|
+
allowlist: z7.array(ModelEntrySchema)
|
|
6547
6966
|
});
|
|
6548
6967
|
DEFAULT_MODEL_CONFIG = {
|
|
6549
6968
|
bulk: "google/gemini-3.1-flash-lite",
|
|
@@ -6722,7 +7141,7 @@ var init_openrouter = __esm({
|
|
|
6722
7141
|
});
|
|
6723
7142
|
|
|
6724
7143
|
// packages/llm/src/semantics.ts
|
|
6725
|
-
import { z as
|
|
7144
|
+
import { z as z8 } from "zod";
|
|
6726
7145
|
function portable(node) {
|
|
6727
7146
|
if (Array.isArray(node)) return node.map(portable);
|
|
6728
7147
|
if (typeof node !== "object" || node === null) return node;
|
|
@@ -6746,7 +7165,7 @@ function portable(node) {
|
|
|
6746
7165
|
return out;
|
|
6747
7166
|
}
|
|
6748
7167
|
function toPortableJsonSchema(schema) {
|
|
6749
|
-
return portable(
|
|
7168
|
+
return portable(z8.toJSONSchema(schema));
|
|
6750
7169
|
}
|
|
6751
7170
|
function semanticsJsonSchema() {
|
|
6752
7171
|
return toPortableJsonSchema(SemanticsSchema);
|
|
@@ -6761,18 +7180,18 @@ var PropTypeSchema, SemanticsSchema, CodegenOutputSchema, DocsOutputSchema;
|
|
|
6761
7180
|
var init_semantics = __esm({
|
|
6762
7181
|
"packages/llm/src/semantics.ts"() {
|
|
6763
7182
|
"use strict";
|
|
6764
|
-
PropTypeSchema =
|
|
6765
|
-
|
|
6766
|
-
|
|
6767
|
-
|
|
6768
|
-
|
|
6769
|
-
|
|
7183
|
+
PropTypeSchema = z8.discriminatedUnion("kind", [
|
|
7184
|
+
z8.object({ kind: z8.literal("enum"), values: z8.array(z8.string()).min(1) }),
|
|
7185
|
+
z8.object({ kind: z8.literal("boolean") }),
|
|
7186
|
+
z8.object({ kind: z8.literal("string") }),
|
|
7187
|
+
z8.object({ kind: z8.literal("reactNode") }),
|
|
7188
|
+
z8.object({ kind: z8.literal("handler") })
|
|
6770
7189
|
]);
|
|
6771
|
-
SemanticsSchema =
|
|
7190
|
+
SemanticsSchema = z8.object({
|
|
6772
7191
|
/** PascalCase component name. */
|
|
6773
|
-
componentName:
|
|
7192
|
+
componentName: z8.string().regex(/^[A-Z][A-Za-z0-9]*$/),
|
|
6774
7193
|
/** Semantic HTML element for the root node. */
|
|
6775
|
-
element:
|
|
7194
|
+
element: z8.enum([
|
|
6776
7195
|
"button",
|
|
6777
7196
|
"a",
|
|
6778
7197
|
"div",
|
|
@@ -6786,33 +7205,33 @@ var init_semantics = __esm({
|
|
|
6786
7205
|
"ul",
|
|
6787
7206
|
"li"
|
|
6788
7207
|
]),
|
|
6789
|
-
props:
|
|
6790
|
-
|
|
6791
|
-
name:
|
|
7208
|
+
props: z8.array(
|
|
7209
|
+
z8.object({
|
|
7210
|
+
name: z8.string().regex(/^[a-z][A-Za-z0-9]*$/),
|
|
6792
7211
|
type: PropTypeSchema,
|
|
6793
|
-
required:
|
|
7212
|
+
required: z8.boolean(),
|
|
6794
7213
|
/** nullish, not optional: models routinely encode "no default" as
|
|
6795
7214
|
* null (observed live: kimi-k2.7-code, Carbon run 8). */
|
|
6796
|
-
defaultValue:
|
|
7215
|
+
defaultValue: z8.string().nullish()
|
|
6797
7216
|
})
|
|
6798
7217
|
),
|
|
6799
7218
|
/** Variant axes that become discriminated unions (invalid combos must not compile). */
|
|
6800
|
-
discriminatedUnions:
|
|
6801
|
-
|
|
6802
|
-
discriminant:
|
|
6803
|
-
arms:
|
|
6804
|
-
|
|
6805
|
-
value:
|
|
7219
|
+
discriminatedUnions: z8.array(
|
|
7220
|
+
z8.object({
|
|
7221
|
+
discriminant: z8.string(),
|
|
7222
|
+
arms: z8.array(
|
|
7223
|
+
z8.object({
|
|
7224
|
+
value: z8.string(),
|
|
6806
7225
|
/** Props only valid for this arm. */
|
|
6807
|
-
extraProps:
|
|
7226
|
+
extraProps: z8.array(z8.string())
|
|
6808
7227
|
})
|
|
6809
7228
|
)
|
|
6810
7229
|
})
|
|
6811
7230
|
),
|
|
6812
|
-
a11y:
|
|
6813
|
-
role:
|
|
6814
|
-
keyboard:
|
|
6815
|
-
aria:
|
|
7231
|
+
a11y: z8.object({
|
|
7232
|
+
role: z8.string().nullish(),
|
|
7233
|
+
keyboard: z8.array(z8.string()),
|
|
7234
|
+
aria: z8.array(z8.string())
|
|
6816
7235
|
}),
|
|
6817
7236
|
/**
|
|
6818
7237
|
* How each Figma axis value is realized through the generated API —
|
|
@@ -6821,29 +7240,29 @@ var init_semantics = __esm({
|
|
|
6821
7240
|
* The visual fact check verifies these claims against real renders, so a
|
|
6822
7241
|
* wrong mapping fails measurement instead of being trusted.
|
|
6823
7242
|
*/
|
|
6824
|
-
variantMapping:
|
|
6825
|
-
|
|
6826
|
-
axis:
|
|
6827
|
-
value:
|
|
6828
|
-
prop:
|
|
6829
|
-
propValue:
|
|
7243
|
+
variantMapping: z8.array(
|
|
7244
|
+
z8.object({
|
|
7245
|
+
axis: z8.string(),
|
|
7246
|
+
value: z8.string(),
|
|
7247
|
+
prop: z8.string(),
|
|
7248
|
+
propValue: z8.union([z8.string(), z8.boolean()])
|
|
6830
7249
|
})
|
|
6831
7250
|
)
|
|
6832
7251
|
});
|
|
6833
|
-
CodegenOutputSchema =
|
|
7252
|
+
CodegenOutputSchema = z8.object({
|
|
6834
7253
|
/** Component source (.tsx). Token-only styling via the provided CSS file. */
|
|
6835
|
-
tsx:
|
|
7254
|
+
tsx: z8.string().min(1),
|
|
6836
7255
|
/** Stylesheet (.css) — themable properties reference var(--token) only. */
|
|
6837
|
-
css:
|
|
7256
|
+
css: z8.string().min(1)
|
|
6838
7257
|
});
|
|
6839
|
-
DocsOutputSchema =
|
|
6840
|
-
usageMd:
|
|
6841
|
-
aiHints:
|
|
6842
|
-
antiPatterns:
|
|
6843
|
-
|
|
7258
|
+
DocsOutputSchema = z8.object({
|
|
7259
|
+
usageMd: z8.string().min(1),
|
|
7260
|
+
aiHints: z8.object({ selectionCriteria: z8.array(z8.string()).min(1) }),
|
|
7261
|
+
antiPatterns: z8.array(
|
|
7262
|
+
z8.object({ scenario: z8.string(), reason: z8.string(), alternative: z8.string() })
|
|
6844
7263
|
),
|
|
6845
|
-
composition:
|
|
6846
|
-
examples:
|
|
7264
|
+
composition: z8.array(z8.string()),
|
|
7265
|
+
examples: z8.array(z8.object({ title: z8.string(), code: z8.string() }))
|
|
6847
7266
|
});
|
|
6848
7267
|
}
|
|
6849
7268
|
});
|
|
@@ -7313,56 +7732,56 @@ var init_src5 = __esm({
|
|
|
7313
7732
|
});
|
|
7314
7733
|
|
|
7315
7734
|
// packages/metadata/src/component-json.ts
|
|
7316
|
-
import { z as
|
|
7735
|
+
import { z as z9 } from "zod";
|
|
7317
7736
|
var TokenUseSchema, AntiPatternSchema, ComponentJsonSchema;
|
|
7318
7737
|
var init_component_json = __esm({
|
|
7319
7738
|
"packages/metadata/src/component-json.ts"() {
|
|
7320
7739
|
"use strict";
|
|
7321
|
-
TokenUseSchema =
|
|
7740
|
+
TokenUseSchema = z9.object({
|
|
7322
7741
|
/** DTCG token path, e.g. "color.primary". */
|
|
7323
|
-
path:
|
|
7324
|
-
cssVar:
|
|
7325
|
-
usage:
|
|
7326
|
-
});
|
|
7327
|
-
AntiPatternSchema =
|
|
7328
|
-
scenario:
|
|
7329
|
-
reason:
|
|
7330
|
-
alternative:
|
|
7331
|
-
});
|
|
7332
|
-
ComponentJsonSchema =
|
|
7333
|
-
name:
|
|
7334
|
-
version:
|
|
7742
|
+
path: z9.string(),
|
|
7743
|
+
cssVar: z9.string(),
|
|
7744
|
+
usage: z9.string()
|
|
7745
|
+
});
|
|
7746
|
+
AntiPatternSchema = z9.object({
|
|
7747
|
+
scenario: z9.string(),
|
|
7748
|
+
reason: z9.string(),
|
|
7749
|
+
alternative: z9.string()
|
|
7750
|
+
});
|
|
7751
|
+
ComponentJsonSchema = z9.object({
|
|
7752
|
+
name: z9.string(),
|
|
7753
|
+
version: z9.string(),
|
|
7335
7754
|
// --- Structural: generated from code by react-docgen. Never hand-edited. ---
|
|
7336
|
-
props:
|
|
7337
|
-
|
|
7338
|
-
name:
|
|
7339
|
-
type:
|
|
7340
|
-
required:
|
|
7341
|
-
defaultValue:
|
|
7342
|
-
description:
|
|
7755
|
+
props: z9.array(
|
|
7756
|
+
z9.object({
|
|
7757
|
+
name: z9.string(),
|
|
7758
|
+
type: z9.string(),
|
|
7759
|
+
required: z9.boolean(),
|
|
7760
|
+
defaultValue: z9.string().optional(),
|
|
7761
|
+
description: z9.string().optional()
|
|
7343
7762
|
})
|
|
7344
7763
|
),
|
|
7345
|
-
variants:
|
|
7346
|
-
states:
|
|
7347
|
-
tokensUsed:
|
|
7348
|
-
a11y:
|
|
7349
|
-
role:
|
|
7350
|
-
keyboard:
|
|
7351
|
-
aria:
|
|
7764
|
+
variants: z9.record(z9.string(), z9.array(z9.string())),
|
|
7765
|
+
states: z9.array(z9.string()),
|
|
7766
|
+
tokensUsed: z9.array(TokenUseSchema),
|
|
7767
|
+
a11y: z9.object({
|
|
7768
|
+
role: z9.string().optional(),
|
|
7769
|
+
keyboard: z9.array(z9.string()),
|
|
7770
|
+
aria: z9.array(z9.string())
|
|
7352
7771
|
}),
|
|
7353
7772
|
// --- LLM-authored, deterministically validated. ---
|
|
7354
|
-
aiHints:
|
|
7355
|
-
selectionCriteria:
|
|
7773
|
+
aiHints: z9.object({
|
|
7774
|
+
selectionCriteria: z9.array(z9.string())
|
|
7356
7775
|
}),
|
|
7357
|
-
antiPatterns:
|
|
7358
|
-
composition:
|
|
7359
|
-
examples:
|
|
7776
|
+
antiPatterns: z9.array(AntiPatternSchema),
|
|
7777
|
+
composition: z9.array(z9.string()),
|
|
7778
|
+
examples: z9.array(z9.object({ title: z9.string(), code: z9.string() })),
|
|
7360
7779
|
// --- Provenance. ---
|
|
7361
|
-
generated:
|
|
7362
|
-
tool:
|
|
7363
|
-
toolVersion:
|
|
7364
|
-
model:
|
|
7365
|
-
modelStatus:
|
|
7780
|
+
generated: z9.object({
|
|
7781
|
+
tool: z9.literal("tendril"),
|
|
7782
|
+
toolVersion: z9.string(),
|
|
7783
|
+
model: z9.string(),
|
|
7784
|
+
modelStatus: z9.enum(["verified", "degraded"])
|
|
7366
7785
|
})
|
|
7367
7786
|
});
|
|
7368
7787
|
}
|
|
@@ -7448,7 +7867,7 @@ var init_extract = __esm({
|
|
|
7448
7867
|
});
|
|
7449
7868
|
|
|
7450
7869
|
// packages/metadata/src/recording-set.ts
|
|
7451
|
-
import { z as
|
|
7870
|
+
import { z as z10 } from "zod";
|
|
7452
7871
|
function roleLossToken(loss) {
|
|
7453
7872
|
return loss.kind === "main" ? `main:${loss.main}` : `part:${loss.part}${EDGE_ARROW}${loss.main}`;
|
|
7454
7873
|
}
|
|
@@ -7511,58 +7930,58 @@ var init_recording_set = __esm({
|
|
|
7511
7930
|
"packages/metadata/src/recording-set.ts"() {
|
|
7512
7931
|
"use strict";
|
|
7513
7932
|
RECORDING_SET_VERSION = 1;
|
|
7514
|
-
EnvelopeSchema =
|
|
7515
|
-
content:
|
|
7516
|
-
|
|
7517
|
-
type:
|
|
7518
|
-
text:
|
|
7519
|
-
data:
|
|
7520
|
-
mimeType:
|
|
7933
|
+
EnvelopeSchema = z10.object({
|
|
7934
|
+
content: z10.array(
|
|
7935
|
+
z10.object({
|
|
7936
|
+
type: z10.string().optional(),
|
|
7937
|
+
text: z10.string().optional(),
|
|
7938
|
+
data: z10.string().optional(),
|
|
7939
|
+
mimeType: z10.string().optional()
|
|
7521
7940
|
})
|
|
7522
7941
|
)
|
|
7523
7942
|
});
|
|
7524
|
-
RepEntrySchema =
|
|
7943
|
+
RepEntrySchema = z10.object({
|
|
7525
7944
|
/** Directory slug under the set root. */
|
|
7526
|
-
slug:
|
|
7945
|
+
slug: z10.string().min(1),
|
|
7527
7946
|
/** Figma node id of the recorded symbol, e.g. "2222:10354". */
|
|
7528
|
-
nodeId:
|
|
7947
|
+
nodeId: z10.string().regex(/^\d+:\d+$/),
|
|
7529
7948
|
/** Source frame node id (multi-frame kits record where each symbol
|
|
7530
7949
|
* lives — the shadcn Calendar spans four frames). */
|
|
7531
|
-
sourceFrame:
|
|
7950
|
+
sourceFrame: z10.string().regex(/^\d+:\d+$/).optional()
|
|
7532
7951
|
});
|
|
7533
|
-
RolesSchema =
|
|
7952
|
+
RolesSchema = z10.object({
|
|
7534
7953
|
/** Slugs of reps whose symbols are MAIN components. */
|
|
7535
|
-
main:
|
|
7954
|
+
main: z10.array(z10.string()),
|
|
7536
7955
|
/** slug → the main slugs it is a part of. */
|
|
7537
|
-
parts:
|
|
7956
|
+
parts: z10.record(z10.string(), z10.object({ partOf: z10.array(z10.string()).min(1) })),
|
|
7538
7957
|
/** External/unrecorded component references discovered during
|
|
7539
7958
|
* derivation — disclosed, never silently dropped (Calendar's Icon
|
|
7540
7959
|
* Buttons; unrecorded base variants). */
|
|
7541
|
-
external:
|
|
7960
|
+
external: z10.array(z10.string()).optional(),
|
|
7542
7961
|
/** Where this graph came from. A graph with empty `parts` makes the
|
|
7543
7962
|
* structural composition check return an affirmative PASS, so a
|
|
7544
7963
|
* human-authored override and a derived graph must never be
|
|
7545
7964
|
* indistinguishable in a report. The CLI stamps this itself — a
|
|
7546
7965
|
* supplied roles file cannot claim "derived" for itself. */
|
|
7547
|
-
rolesSource:
|
|
7966
|
+
rolesSource: z10.enum(["derived", "human-override"]).optional(),
|
|
7548
7967
|
/** Losses in the DERIVED relation a human override deliberately
|
|
7549
7968
|
* accepts, one token per loss (`roleLossToken`). Named one by one on
|
|
7550
7969
|
* purpose: emptying out what composition measures — by dropping a
|
|
7551
7970
|
* main, or by dropping/re-pointing a part→main edge — is exactly what
|
|
7552
7971
|
* turns composition into a vacuous pass, so it is an acceptance
|
|
7553
7972
|
* someone signs rather than an omission nobody notices. */
|
|
7554
|
-
narrowingAccepted:
|
|
7973
|
+
narrowingAccepted: z10.array(z10.string()).optional()
|
|
7555
7974
|
});
|
|
7556
7975
|
EDGE_ARROW = "->";
|
|
7557
|
-
RecordingSetManifestSchema =
|
|
7558
|
-
version:
|
|
7976
|
+
RecordingSetManifestSchema = z10.object({
|
|
7977
|
+
version: z10.literal(RECORDING_SET_VERSION),
|
|
7559
7978
|
/** Human-readable component/system name. */
|
|
7560
|
-
component:
|
|
7979
|
+
component: z10.string().min(1),
|
|
7561
7980
|
/** Source frames: node id → short description. */
|
|
7562
|
-
sourceFrames:
|
|
7563
|
-
reps:
|
|
7981
|
+
sourceFrames: z10.record(z10.string(), z10.string()).optional(),
|
|
7982
|
+
reps: z10.array(RepEntrySchema).min(1),
|
|
7564
7983
|
/** Honesty contract: what was NOT recorded, in prose. */
|
|
7565
|
-
notRecorded:
|
|
7984
|
+
notRecorded: z10.string().optional(),
|
|
7566
7985
|
roles: RolesSchema.optional()
|
|
7567
7986
|
});
|
|
7568
7987
|
REQUIRED_REP_FILES = ["get_design_context.json", "get_metadata.json", "get_screenshot.json"];
|
|
@@ -7570,7 +7989,7 @@ var init_recording_set = __esm({
|
|
|
7570
7989
|
});
|
|
7571
7990
|
|
|
7572
7991
|
// packages/metadata/src/bundle.ts
|
|
7573
|
-
import { z as
|
|
7992
|
+
import { z as z11 } from "zod";
|
|
7574
7993
|
function readBundleManifest(raw) {
|
|
7575
7994
|
if (Buffer.byteLength(raw, "utf8") > MAX_BUNDLE_MANIFEST_BYTES) {
|
|
7576
7995
|
return { issues: [{ severity: "error", message: `component.json exceeds the ${MAX_BUNDLE_MANIFEST_BYTES}-byte ingest cap` }] };
|
|
@@ -7642,94 +8061,94 @@ var init_bundle = __esm({
|
|
|
7642
8061
|
"use strict";
|
|
7643
8062
|
init_recording_set();
|
|
7644
8063
|
BUNDLE_VERSION = 1;
|
|
7645
|
-
ConfigStatusSchema =
|
|
7646
|
-
PinnedPropSchema =
|
|
7647
|
-
name:
|
|
7648
|
-
type:
|
|
7649
|
-
required:
|
|
7650
|
-
default:
|
|
7651
|
-
});
|
|
7652
|
-
PropAdapterSchema =
|
|
7653
|
-
|
|
7654
|
-
|
|
7655
|
-
component:
|
|
7656
|
-
props:
|
|
8064
|
+
ConfigStatusSchema = z11.enum(["certified", "pass", "fail", "fallback-unverified", "unverified", "part-unverified-standalone"]);
|
|
8065
|
+
PinnedPropSchema = z11.object({
|
|
8066
|
+
name: z11.string(),
|
|
8067
|
+
type: z11.string(),
|
|
8068
|
+
required: z11.boolean(),
|
|
8069
|
+
default: z11.string().optional()
|
|
8070
|
+
});
|
|
8071
|
+
PropAdapterSchema = z11.record(
|
|
8072
|
+
z11.string(),
|
|
8073
|
+
z11.object({
|
|
8074
|
+
component: z11.string(),
|
|
8075
|
+
props: z11.record(z11.string(), z11.unknown())
|
|
7657
8076
|
})
|
|
7658
8077
|
);
|
|
7659
|
-
RequiredFontSchema =
|
|
7660
|
-
family:
|
|
7661
|
-
weight:
|
|
7662
|
-
source:
|
|
7663
|
-
sha256:
|
|
7664
|
-
});
|
|
7665
|
-
ConfigClaimSchema =
|
|
7666
|
-
rep:
|
|
7667
|
-
similarity:
|
|
7668
|
-
inkRecall:
|
|
8078
|
+
RequiredFontSchema = z11.object({
|
|
8079
|
+
family: z11.string(),
|
|
8080
|
+
weight: z11.number(),
|
|
8081
|
+
source: z11.string(),
|
|
8082
|
+
sha256: z11.string()
|
|
8083
|
+
});
|
|
8084
|
+
ConfigClaimSchema = z11.object({
|
|
8085
|
+
rep: z11.string(),
|
|
8086
|
+
similarity: z11.number(),
|
|
8087
|
+
inkRecall: z11.number(),
|
|
7669
8088
|
status: ConfigStatusSchema
|
|
7670
8089
|
});
|
|
7671
|
-
BehaviorClaimSchema =
|
|
7672
|
-
id:
|
|
7673
|
-
pass:
|
|
7674
|
-
detail:
|
|
8090
|
+
BehaviorClaimSchema = z11.object({
|
|
8091
|
+
id: z11.string(),
|
|
8092
|
+
pass: z11.boolean(),
|
|
8093
|
+
detail: z11.string().optional()
|
|
7675
8094
|
});
|
|
7676
|
-
BundleProvenanceSchema =
|
|
8095
|
+
BundleProvenanceSchema = z11.object({
|
|
7677
8096
|
/** Engine id that produced the accepted candidate. */
|
|
7678
|
-
engine:
|
|
8097
|
+
engine: z11.string(),
|
|
7679
8098
|
/** Resolved model id (curated engine) or agent identity, when known. */
|
|
7680
|
-
model:
|
|
7681
|
-
recordingSet:
|
|
8099
|
+
model: z11.string().optional(),
|
|
8100
|
+
recordingSet: z11.object({
|
|
7682
8101
|
/** Set path as seen at generate time — informational, not trusted. */
|
|
7683
|
-
path:
|
|
8102
|
+
path: z11.string(),
|
|
7684
8103
|
/** Kit/component name from the set manifest. */
|
|
7685
|
-
component:
|
|
8104
|
+
component: z11.string(),
|
|
7686
8105
|
/** Founder-visible license posture of the recorded kit, if noted. */
|
|
7687
|
-
licenseNote:
|
|
8106
|
+
licenseNote: z11.string().optional(),
|
|
7688
8107
|
/** Content hash over the set manifest + rep envelopes (the identity
|
|
7689
8108
|
* verify compares against, not the path). */
|
|
7690
|
-
hash:
|
|
8109
|
+
hash: z11.string()
|
|
7691
8110
|
}),
|
|
7692
|
-
environment:
|
|
7693
|
-
chrome:
|
|
8111
|
+
environment: z11.object({
|
|
8112
|
+
chrome: z11.string(),
|
|
7694
8113
|
// chromeVersion joined the stamp when a path alone proved
|
|
7695
8114
|
// insufficient identity (two builds, one path, incomparable
|
|
7696
8115
|
// numbers); optional so pre-epoch bundles keep validating.
|
|
7697
|
-
chromeVersion:
|
|
7698
|
-
fontsManifestSha256:
|
|
8116
|
+
chromeVersion: z11.string().nullable().optional(),
|
|
8117
|
+
fontsManifestSha256: z11.string().nullable(),
|
|
7699
8118
|
// Run 11: a bundle scored under a substitute face carried CSS
|
|
7700
8119
|
// compensation (font-variation-settings tuned to the substitute)
|
|
7701
8120
|
// with nothing marking it conditional — the provenance now names
|
|
7702
8121
|
// the families that were substituted at scoring time. Optional so
|
|
7703
8122
|
// pre-0.1.19 bundles keep validating; absent means none.
|
|
7704
|
-
substitutedFamilies:
|
|
8123
|
+
substitutedFamilies: z11.array(z11.string()).optional()
|
|
7705
8124
|
}),
|
|
7706
8125
|
/** Coverage denominator (Q2 honesty): recorded vs full-lattice size
|
|
7707
8126
|
* when the lattice is known (null when the kit exposes no lattice). */
|
|
7708
|
-
coverage:
|
|
7709
|
-
recordedConfigs:
|
|
7710
|
-
latticeConfigs:
|
|
8127
|
+
coverage: z11.object({
|
|
8128
|
+
recordedConfigs: z11.number().int().nonnegative(),
|
|
8129
|
+
latticeConfigs: z11.number().int().positive().nullable()
|
|
7711
8130
|
}),
|
|
7712
|
-
generatedAt:
|
|
7713
|
-
spentUsd:
|
|
8131
|
+
generatedAt: z11.string(),
|
|
8132
|
+
spentUsd: z11.number().optional()
|
|
7714
8133
|
});
|
|
7715
|
-
BundleComponentJsonSchema =
|
|
7716
|
-
bundleVersion:
|
|
7717
|
-
name:
|
|
8134
|
+
BundleComponentJsonSchema = z11.object({
|
|
8135
|
+
bundleVersion: z11.literal(BUNDLE_VERSION),
|
|
8136
|
+
name: z11.string().min(1),
|
|
7718
8137
|
/** Entry module file name (e.g. "Button.tsx"). */
|
|
7719
|
-
entry:
|
|
7720
|
-
props:
|
|
8138
|
+
entry: z11.string().min(1),
|
|
8139
|
+
props: z11.array(PinnedPropSchema),
|
|
7721
8140
|
/** data-tendril-state tokens the bundle supports (A1.3 forcing hook). */
|
|
7722
|
-
forcedStates:
|
|
8141
|
+
forcedStates: z11.array(z11.string()),
|
|
7723
8142
|
propAdapter: PropAdapterSchema,
|
|
7724
|
-
requiredFonts:
|
|
8143
|
+
requiredFonts: z11.array(RequiredFontSchema),
|
|
7725
8144
|
/** A1.2: mirrors the RECORDING's role manifest — verify takes the
|
|
7726
8145
|
* graph from the recording, so a bundle declaring no parts dodges
|
|
7727
8146
|
* nothing. Absent when the recording claims no roles. */
|
|
7728
8147
|
composition: RolesSchema.optional(),
|
|
7729
|
-
configStatuses:
|
|
7730
|
-
behaviorClaims:
|
|
8148
|
+
configStatuses: z11.array(ConfigClaimSchema),
|
|
8149
|
+
behaviorClaims: z11.array(BehaviorClaimSchema).optional(),
|
|
7731
8150
|
provenance: BundleProvenanceSchema,
|
|
7732
|
-
trustStatement:
|
|
8151
|
+
trustStatement: z11.string().min(1)
|
|
7733
8152
|
});
|
|
7734
8153
|
MAX_BUNDLE_MANIFEST_BYTES = 1e6;
|
|
7735
8154
|
MAX_BUNDLE_SOURCE_BYTES = 5e6;
|
|
@@ -7861,9 +8280,9 @@ __export(record_exports, {
|
|
|
7861
8280
|
runRecordPlan: () => runRecordPlan,
|
|
7862
8281
|
runRecordStatus: () => runRecordStatus
|
|
7863
8282
|
});
|
|
7864
|
-
import { existsSync as
|
|
8283
|
+
import { existsSync as existsSync22, mkdtempSync as mkdtempSync2, readFileSync as readFileSync18, readdirSync as readdirSync8 } from "node:fs";
|
|
7865
8284
|
import os6 from "node:os";
|
|
7866
|
-
import
|
|
8285
|
+
import path28 from "node:path";
|
|
7867
8286
|
import { writeFileSync as writeFileSync9 } from "node:fs";
|
|
7868
8287
|
function recordsInteractionState(reports) {
|
|
7869
8288
|
const evident = (s) => stateTokens(s).some((t) => INTERACTION_EVIDENCE_VALUES.has(t));
|
|
@@ -7886,7 +8305,7 @@ function interactionDisclosure(component, reports) {
|
|
|
7886
8305
|
};
|
|
7887
8306
|
}
|
|
7888
8307
|
function symbolsFromMetadataEnvelope(file, sourceFrame) {
|
|
7889
|
-
const env = JSON.parse(
|
|
8308
|
+
const env = JSON.parse(readFileSync18(file, "utf8"));
|
|
7890
8309
|
const text = env.content.map((c) => c.text ?? "").join("\n");
|
|
7891
8310
|
const symbols = [];
|
|
7892
8311
|
const walk2 = (node, ancestor) => {
|
|
@@ -7944,7 +8363,7 @@ function runRecordPlan(opts) {
|
|
|
7944
8363
|
if (rawFile !== void 0) {
|
|
7945
8364
|
try {
|
|
7946
8365
|
const envelope = rawEnvelopeFromFile(rawFile, opts.metadataRawPartsFile !== void 0);
|
|
7947
|
-
const tmp =
|
|
8366
|
+
const tmp = path28.join(mkdtempSync2(path28.join(os6.tmpdir(), "tendril-plan-meta-")), "get_metadata.json");
|
|
7948
8367
|
writeFileSync9(tmp, JSON.stringify(envelope));
|
|
7949
8368
|
metadataEntries.push({ file: tmp });
|
|
7950
8369
|
} catch (err) {
|
|
@@ -7966,7 +8385,7 @@ function runRecordPlan(opts) {
|
|
|
7966
8385
|
let metadataTruncated = false;
|
|
7967
8386
|
for (const { file, frame } of metadataEntries) {
|
|
7968
8387
|
try {
|
|
7969
|
-
const parsed = symbolsFromMetadataEnvelope(
|
|
8388
|
+
const parsed = symbolsFromMetadataEnvelope(path28.resolve(file), frame);
|
|
7970
8389
|
symbols.push(...parsed.symbols);
|
|
7971
8390
|
if (parsed.truncated) metadataTruncated = true;
|
|
7972
8391
|
} catch (err) {
|
|
@@ -8000,7 +8419,7 @@ function runRecordPlan(opts) {
|
|
|
8000
8419
|
if (symbols.length === 0) {
|
|
8001
8420
|
const leads = metadataEntries.flatMap(({ file }) => {
|
|
8002
8421
|
try {
|
|
8003
|
-
const env = JSON.parse(
|
|
8422
|
+
const env = JSON.parse(readFileSync18(path28.resolve(file), "utf8"));
|
|
8004
8423
|
return instanceLeads(env.content.map((c) => c.text ?? "").join("\n"));
|
|
8005
8424
|
} catch {
|
|
8006
8425
|
return [];
|
|
@@ -8089,7 +8508,7 @@ function runRecordPlan(opts) {
|
|
|
8089
8508
|
// a CLI flag and the MCP surface has no parameter for it.
|
|
8090
8509
|
decidedBy: "HUMAN ONLY \u2014 offer it, never choose it, and you cannot run it",
|
|
8091
8510
|
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.",
|
|
8092
|
-
userRuns: [`rm ${quoteArg(
|
|
8511
|
+
userRuns: [`rm ${quoteArg(path28.join(opts.setDir, "recording-set.json"))}`, `${tendrilCommand(`record plan --set ${quoteArg(opts.setDir)} --component ${quoteArg(manifest.component)}`)} --sample`]
|
|
8093
8512
|
},
|
|
8094
8513
|
{
|
|
8095
8514
|
id: "larger-allowance",
|
|
@@ -8246,7 +8665,7 @@ function runRecordNext(opts) {
|
|
|
8246
8665
|
const progress = payload["progress"];
|
|
8247
8666
|
process.stdout.write(`[${progress.recordedReps}/${progress.totalReps} reps] ${payload["tool"]} for ${payload["slug"]} (node ${payload["nodeId"]})
|
|
8248
8667
|
\u2192 ${payload["note"]}
|
|
8249
|
-
\u2192 then: ${tendrilCommand(`record ingest --set ${
|
|
8668
|
+
\u2192 then: ${tendrilCommand(`record ingest --set ${path28.resolve(opts.setDir)} --rep ${payload["slug"]} --tool ${payload["tool"]} --file <envelope.json>`)}
|
|
8250
8669
|
`);
|
|
8251
8670
|
});
|
|
8252
8671
|
}
|
|
@@ -8320,7 +8739,7 @@ async function autoFetchAssets(setDir, rep, envelopeText2) {
|
|
|
8320
8739
|
const skipped = [];
|
|
8321
8740
|
const failed = [];
|
|
8322
8741
|
for (const { url, name } of assetUrlsFromEnvelopeText(envelopeText2)) {
|
|
8323
|
-
if (
|
|
8742
|
+
if (existsSync22(path28.join(setDir, rep, name))) {
|
|
8324
8743
|
skipped.push(name);
|
|
8325
8744
|
continue;
|
|
8326
8745
|
}
|
|
@@ -8342,16 +8761,16 @@ async function autoFetchAssets(setDir, rep, envelopeText2) {
|
|
|
8342
8761
|
}
|
|
8343
8762
|
function rawEnvelopeFromFile(file, parts) {
|
|
8344
8763
|
if (parts) {
|
|
8345
|
-
const blocks = JSON.parse(
|
|
8764
|
+
const blocks = JSON.parse(readFileSync18(path28.resolve(file), "utf8"));
|
|
8346
8765
|
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");
|
|
8347
8766
|
return { content: blocks.map((text) => ({ type: "text", text })) };
|
|
8348
8767
|
}
|
|
8349
|
-
return { content: [{ type: "text", text:
|
|
8768
|
+
return { content: [{ type: "text", text: readFileSync18(path28.resolve(file), "utf8") }] };
|
|
8350
8769
|
}
|
|
8351
8770
|
async function runRecordIngest(opts) {
|
|
8352
8771
|
let payload;
|
|
8353
8772
|
try {
|
|
8354
|
-
payload = opts.rawParts === true || opts.raw === true ? rawEnvelopeFromFile(opts.file, opts.rawParts === true) : JSON.parse(
|
|
8773
|
+
payload = opts.rawParts === true || opts.raw === true ? rawEnvelopeFromFile(opts.file, opts.rawParts === true) : JSON.parse(readFileSync18(path28.resolve(opts.file), "utf8"));
|
|
8355
8774
|
} catch (err) {
|
|
8356
8775
|
fail(opts, ExitCode.InputValidation, {
|
|
8357
8776
|
error: `cannot read envelope file: ${err instanceof Error ? err.message : String(err)}`,
|
|
@@ -8363,7 +8782,7 @@ async function runRecordIngest(opts) {
|
|
|
8363
8782
|
fail(opts, ExitCode.InputValidation, {
|
|
8364
8783
|
error: "--raw is for text tool responses; screenshots are binary",
|
|
8365
8784
|
code: "envelope-invalid",
|
|
8366
|
-
remediation: `Use \`${tendrilCommand(`record fetch --set ${
|
|
8785
|
+
remediation: `Use \`${tendrilCommand(`record fetch --set ${path28.resolve(opts.setDir)} --rep ${opts.rep} --tool ${opts.tool} --url <image_url>`)}\` instead.`
|
|
8367
8786
|
});
|
|
8368
8787
|
}
|
|
8369
8788
|
if (opts.rep === "__set__" && opts.tool === "get_variable_defs") {
|
|
@@ -8383,7 +8802,7 @@ async function runRecordIngest(opts) {
|
|
|
8383
8802
|
remediation: REINGEST_GUIDANCE
|
|
8384
8803
|
});
|
|
8385
8804
|
}
|
|
8386
|
-
writeFileSync9(
|
|
8805
|
+
writeFileSync9(path28.join(opts.setDir, "get_variable_defs.json"), `${JSON.stringify(payload, null, 1)}
|
|
8387
8806
|
`);
|
|
8388
8807
|
emitData(opts, { ingested: "get_variable_defs", rep: "__set__", setLevel: true, next: nextPayload(opts.setDir) }, () => {
|
|
8389
8808
|
process.stdout.write("set-level get_variable_defs ingested\n");
|
|
@@ -8399,7 +8818,7 @@ async function runRecordIngest(opts) {
|
|
|
8399
8818
|
if (assets !== void 0) {
|
|
8400
8819
|
for (const a of assets.fetched) process.stdout.write(` asset fetched ${a}
|
|
8401
8820
|
`);
|
|
8402
|
-
for (const f of assets.failed) process.stdout.write(` ASSET FAILED ${f.name}: ${f.reason} \u2014 download it, then: ${tendrilCommand(`record asset --set ${
|
|
8821
|
+
for (const f of assets.failed) process.stdout.write(` ASSET FAILED ${f.name}: ${f.reason} \u2014 download it, then: ${tendrilCommand(`record asset --set ${path28.resolve(opts.setDir)} --rep ${opts.rep} --name ${f.name} --file <downloaded-file>`)}
|
|
8403
8822
|
`);
|
|
8404
8823
|
}
|
|
8405
8824
|
});
|
|
@@ -8472,15 +8891,15 @@ async function runRecordIngestRep(opts) {
|
|
|
8472
8891
|
if (assets !== void 0) {
|
|
8473
8892
|
for (const a of assets.fetched) process.stdout.write(` asset fetched ${a}
|
|
8474
8893
|
`);
|
|
8475
|
-
for (const f of assets.failed) process.stdout.write(` ASSET FAILED ${f.name}: ${f.reason} \u2014 download it, then: ${tendrilCommand(`record asset --set ${
|
|
8894
|
+
for (const f of assets.failed) process.stdout.write(` ASSET FAILED ${f.name}: ${f.reason} \u2014 download it, then: ${tendrilCommand(`record asset --set ${path28.resolve(opts.setDir)} --rep ${opts.rep} --name ${f.name} --file <downloaded-file>`)}
|
|
8476
8895
|
`);
|
|
8477
8896
|
}
|
|
8478
8897
|
});
|
|
8479
8898
|
}
|
|
8480
8899
|
function runRecordAsset(opts) {
|
|
8481
8900
|
if (opts.dir !== void 0) {
|
|
8482
|
-
const dir =
|
|
8483
|
-
const names =
|
|
8901
|
+
const dir = path28.resolve(opts.dir);
|
|
8902
|
+
const names = readdirSync8(dir).filter((f) => /^asset-[\w.-]+\.(svg|png|jpe?g|webp|gif)$/i.test(f));
|
|
8484
8903
|
if (names.length === 0) {
|
|
8485
8904
|
fail(opts, ExitCode.InputValidation, {
|
|
8486
8905
|
error: `no asset-*.<ext> files found in ${dir}`,
|
|
@@ -8491,7 +8910,7 @@ function runRecordAsset(opts) {
|
|
|
8491
8910
|
const ingested = [];
|
|
8492
8911
|
try {
|
|
8493
8912
|
for (const name of names) {
|
|
8494
|
-
ingestAsset(opts.setDir, opts.rep, name,
|
|
8913
|
+
ingestAsset(opts.setDir, opts.rep, name, readFileSync18(path28.join(dir, name)));
|
|
8495
8914
|
ingested.push(name);
|
|
8496
8915
|
}
|
|
8497
8916
|
} catch (err) {
|
|
@@ -8511,11 +8930,11 @@ function runRecordAsset(opts) {
|
|
|
8511
8930
|
fail(opts, ExitCode.InputValidation, {
|
|
8512
8931
|
error: "pass --name and --file for a single asset, or --dir for a batch",
|
|
8513
8932
|
code: "asset-rejected",
|
|
8514
|
-
remediation: tendrilCommand(`record asset --set ${
|
|
8933
|
+
remediation: tendrilCommand(`record asset --set ${path28.resolve(opts.setDir)} --rep ${opts.rep} --dir <downloads-dir>`)
|
|
8515
8934
|
});
|
|
8516
8935
|
}
|
|
8517
8936
|
try {
|
|
8518
|
-
ingestAsset(opts.setDir, opts.rep, opts.name,
|
|
8937
|
+
ingestAsset(opts.setDir, opts.rep, opts.name, readFileSync18(path28.resolve(opts.file)));
|
|
8519
8938
|
emitData(opts, { rep: opts.rep, asset: opts.name }, () => {
|
|
8520
8939
|
process.stdout.write(`ingested ${opts.rep}/${opts.name}
|
|
8521
8940
|
`);
|
|
@@ -8562,7 +8981,7 @@ function narrowedRoles(derived, override) {
|
|
|
8562
8981
|
function rolesFromFile(opts, file, derived) {
|
|
8563
8982
|
let json;
|
|
8564
8983
|
try {
|
|
8565
|
-
json = JSON.parse(
|
|
8984
|
+
json = JSON.parse(readFileSync18(path28.resolve(file), "utf8"));
|
|
8566
8985
|
} catch (err) {
|
|
8567
8986
|
fail(opts, ExitCode.InputValidation, {
|
|
8568
8987
|
error: `cannot read roles file ${file}: ${err instanceof Error ? err.message.split("\n")[0] : String(err)}`,
|
|
@@ -8600,11 +9019,11 @@ function rolesFromFile(opts, file, derived) {
|
|
|
8600
9019
|
};
|
|
8601
9020
|
}
|
|
8602
9021
|
function runRecordFinish(opts) {
|
|
8603
|
-
if (!
|
|
9022
|
+
if (!existsSync22(path28.join(opts.setDir, "recording-set.json"))) {
|
|
8604
9023
|
fail(opts, ExitCode.InputValidation, {
|
|
8605
9024
|
error: `no recording-set.json in ${opts.setDir}`,
|
|
8606
9025
|
code: "no-recording-set",
|
|
8607
|
-
remediation: `Run \`${tendrilCommand(`record plan --set ${
|
|
9026
|
+
remediation: `Run \`${tendrilCommand(`record plan --set ${path28.resolve(opts.setDir)} --component <name> --metadata <envelope.json>`)}\` first \u2014 \`record finish\` closes a set the planner opened.`
|
|
8608
9027
|
});
|
|
8609
9028
|
}
|
|
8610
9029
|
const { manifest, raw } = readManifestFile(opts.setDir);
|
|
@@ -8632,17 +9051,17 @@ function runRecordFinish(opts) {
|
|
|
8632
9051
|
fail(opts, ExitCode.ConfirmationRequired, {
|
|
8633
9052
|
error: "--confirm-roles (and --roles-file) require an interactive terminal \u2014 this confirmation is human-only",
|
|
8634
9053
|
code: "roles-confirmation-not-interactive",
|
|
8635
|
-
remediation: `A human runs \`${tendrilCommand(`record finish --set ${
|
|
9054
|
+
remediation: `A human runs \`${tendrilCommand(`record finish --set ${path28.resolve(opts.setDir)} --confirm-roles`)}\` in their own terminal. Agents: show the proposal to your operator instead of confirming it.`
|
|
8636
9055
|
});
|
|
8637
9056
|
}
|
|
8638
9057
|
const merged = { ...raw, roles };
|
|
8639
|
-
const { issues } = validateRecordingSet(merged, (rel) =>
|
|
9058
|
+
const { issues } = validateRecordingSet(merged, (rel) => existsSync22(path28.join(opts.setDir, rel)));
|
|
8640
9059
|
const errors = issues.filter((i) => i.severity === "error");
|
|
8641
9060
|
if (errors.length > 0) {
|
|
8642
9061
|
fail(opts, ExitCode.InputValidation, {
|
|
8643
9062
|
error: `recording set rejected \u2014 roles NOT written: ${errors.map((e) => e.message).join("; ")}`,
|
|
8644
9063
|
code: "recording-set-invalid",
|
|
8645
|
-
remediation: `Fix the set (\`${tendrilCommand(`record status --set ${
|
|
9064
|
+
remediation: `Fix the set (\`${tendrilCommand(`record status --set ${path28.resolve(opts.setDir)}`)}\` lists missing envelopes) or the roles graph, then re-run \`record finish\`.`
|
|
8646
9065
|
});
|
|
8647
9066
|
}
|
|
8648
9067
|
for (const issue of issues) if (issue.severity === "warning") warn(opts, issue.message);
|
|
@@ -8876,9 +9295,9 @@ var init_engine_curated = __esm({
|
|
|
8876
9295
|
});
|
|
8877
9296
|
|
|
8878
9297
|
// packages/generate/src/loop.ts
|
|
8879
|
-
import { existsSync as
|
|
8880
|
-
import
|
|
8881
|
-
import { z as
|
|
9298
|
+
import { existsSync as existsSync23, mkdirSync as mkdirSync6, readFileSync as readFileSync19, renameSync, writeFileSync as writeFileSync10 } from "node:fs";
|
|
9299
|
+
import path29 from "node:path";
|
|
9300
|
+
import { z as z12 } from "zod";
|
|
8882
9301
|
function objective(scores, behaviors) {
|
|
8883
9302
|
const vals = scores.map((s) => Math.min(s.similarity, s.inkRecall));
|
|
8884
9303
|
return [
|
|
@@ -8914,9 +9333,9 @@ ${absentLines.join("\n")}` : ""}
|
|
|
8914
9333
|
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."}`;
|
|
8915
9334
|
}
|
|
8916
9335
|
function archivePriorRun(outDir) {
|
|
8917
|
-
if (!
|
|
9336
|
+
if (!existsSync23(path29.join(outDir, "run-log.json")) && !existsSync23(path29.join(outDir, "loop-state.json"))) return void 0;
|
|
8918
9337
|
let n = 1;
|
|
8919
|
-
while (
|
|
9338
|
+
while (existsSync23(`${outDir}-prev-${n}`)) n += 1;
|
|
8920
9339
|
renameSync(outDir, `${outDir}-prev-${n}`);
|
|
8921
9340
|
return `${outDir}-prev-${n}`;
|
|
8922
9341
|
}
|
|
@@ -8925,14 +9344,14 @@ async function runEngineLoop(opts) {
|
|
|
8925
9344
|
const plateau = opts.plateau ?? 2;
|
|
8926
9345
|
const progress = opts.onProgress ?? (() => {
|
|
8927
9346
|
});
|
|
8928
|
-
const statePath =
|
|
8929
|
-
const resuming = opts.resume === true &&
|
|
9347
|
+
const statePath = path29.join(opts.outDir, "loop-state.json");
|
|
9348
|
+
const resuming = opts.resume === true && existsSync23(statePath);
|
|
8930
9349
|
if (!resuming) {
|
|
8931
9350
|
const archived = archivePriorRun(opts.outDir);
|
|
8932
9351
|
if (archived !== void 0) progress(`previous run archived to ${archived}`);
|
|
8933
9352
|
}
|
|
8934
9353
|
mkdirSync6(opts.outDir, { recursive: true });
|
|
8935
|
-
const scratch =
|
|
9354
|
+
const scratch = path29.join(opts.outDir, ".candidate");
|
|
8936
9355
|
let attempts = [];
|
|
8937
9356
|
let log = [];
|
|
8938
9357
|
let best;
|
|
@@ -8940,7 +9359,7 @@ async function runEngineLoop(opts) {
|
|
|
8940
9359
|
let nonAccepted = 0;
|
|
8941
9360
|
let stopReason = "max-iterations";
|
|
8942
9361
|
if (resuming) {
|
|
8943
|
-
const restored = LoopStateSchema.parse(JSON.parse(
|
|
9362
|
+
const restored = LoopStateSchema.parse(JSON.parse(readFileSync19(statePath, "utf8")));
|
|
8944
9363
|
attempts = restored.attempts;
|
|
8945
9364
|
log = restored.iterations;
|
|
8946
9365
|
spentUsd = restored.spentUsd;
|
|
@@ -8960,7 +9379,7 @@ async function runEngineLoop(opts) {
|
|
|
8960
9379
|
};
|
|
8961
9380
|
const writeCandidate = (files) => {
|
|
8962
9381
|
mkdirSync6(scratch, { recursive: true });
|
|
8963
|
-
for (const [name, content] of Object.entries(files)) writeFileSync10(
|
|
9382
|
+
for (const [name, content] of Object.entries(files)) writeFileSync10(path29.join(scratch, name), content);
|
|
8964
9383
|
};
|
|
8965
9384
|
const scoreCandidate = async (candidate, iter, usd, modelMs) => {
|
|
8966
9385
|
writeCandidate(candidate.files);
|
|
@@ -9018,8 +9437,8 @@ async function runEngineLoop(opts) {
|
|
|
9018
9437
|
const usd = candidate.usage?.usd ?? 0;
|
|
9019
9438
|
spentUsd += usd;
|
|
9020
9439
|
if (candidate.raw !== void 0) {
|
|
9021
|
-
mkdirSync6(
|
|
9022
|
-
writeFileSync10(
|
|
9440
|
+
mkdirSync6(path29.join(opts.outDir, "responses"), { recursive: true });
|
|
9441
|
+
writeFileSync10(path29.join(opts.outDir, "responses", `iter-${iter}.md`), candidate.raw);
|
|
9023
9442
|
}
|
|
9024
9443
|
if (candidate.files[opts.entry] === void 0 || candidate.files["styles.css"] === void 0) {
|
|
9025
9444
|
const finish = candidate.usage?.finishReason ?? "?";
|
|
@@ -9045,10 +9464,10 @@ async function runEngineLoop(opts) {
|
|
|
9045
9464
|
}
|
|
9046
9465
|
}
|
|
9047
9466
|
}
|
|
9048
|
-
if (best !== void 0) for (const [name, content] of Object.entries(best.files)) writeFileSync10(
|
|
9467
|
+
if (best !== void 0) for (const [name, content] of Object.entries(best.files)) writeFileSync10(path29.join(opts.outDir, name), content);
|
|
9049
9468
|
const final = best !== void 0 ? await opts.score(opts.outDir) : { scores: [], behaviors: [] };
|
|
9050
9469
|
writeFileSync10(
|
|
9051
|
-
|
|
9470
|
+
path29.join(opts.outDir, "run-log.json"),
|
|
9052
9471
|
`${JSON.stringify(
|
|
9053
9472
|
{
|
|
9054
9473
|
...opts.meta,
|
|
@@ -9072,42 +9491,42 @@ var init_loop2 = __esm({
|
|
|
9072
9491
|
"packages/generate/src/loop.ts"() {
|
|
9073
9492
|
"use strict";
|
|
9074
9493
|
better = (a, b) => a[0] !== b[0] ? a[0] > b[0] : a[1] !== b[1] ? a[1] > b[1] : a[2] > b[2];
|
|
9075
|
-
RegionSchema =
|
|
9076
|
-
ConfigScoreSchema =
|
|
9077
|
-
rep:
|
|
9078
|
-
similarity:
|
|
9079
|
-
inkRecall:
|
|
9080
|
-
pass:
|
|
9081
|
-
error:
|
|
9494
|
+
RegionSchema = z12.object({ x0: z12.number(), y0: z12.number(), x1: z12.number(), y1: z12.number(), density: z12.number() });
|
|
9495
|
+
ConfigScoreSchema = z12.object({
|
|
9496
|
+
rep: z12.string(),
|
|
9497
|
+
similarity: z12.number(),
|
|
9498
|
+
inkRecall: z12.number(),
|
|
9499
|
+
pass: z12.boolean(),
|
|
9500
|
+
error: z12.string().optional(),
|
|
9082
9501
|
region: RegionSchema.optional()
|
|
9083
9502
|
});
|
|
9084
|
-
BehaviorResultSchema =
|
|
9085
|
-
LoopStateSchema =
|
|
9086
|
-
version:
|
|
9087
|
-
spentUsd:
|
|
9088
|
-
attempts:
|
|
9089
|
-
|
|
9090
|
-
candidate:
|
|
9091
|
-
files:
|
|
9092
|
-
raw:
|
|
9093
|
-
usage:
|
|
9503
|
+
BehaviorResultSchema = z12.object({ id: z12.string(), pass: z12.boolean(), detail: z12.string().optional() });
|
|
9504
|
+
LoopStateSchema = z12.object({
|
|
9505
|
+
version: z12.literal(1),
|
|
9506
|
+
spentUsd: z12.number(),
|
|
9507
|
+
attempts: z12.array(
|
|
9508
|
+
z12.object({
|
|
9509
|
+
candidate: z12.object({
|
|
9510
|
+
files: z12.record(z12.string(), z12.string()),
|
|
9511
|
+
raw: z12.string().optional(),
|
|
9512
|
+
usage: z12.object({ usd: z12.number(), inTokens: z12.number(), outTokens: z12.number(), modelMs: z12.number(), finishReason: z12.string().optional() }).optional()
|
|
9094
9513
|
}),
|
|
9095
|
-
rulerScore:
|
|
9096
|
-
accepted:
|
|
9097
|
-
feedback:
|
|
9514
|
+
rulerScore: z12.object({ passCount: z12.number(), floor: z12.number(), mean: z12.number() }),
|
|
9515
|
+
accepted: z12.boolean(),
|
|
9516
|
+
feedback: z12.string()
|
|
9098
9517
|
})
|
|
9099
9518
|
),
|
|
9100
|
-
iterations:
|
|
9101
|
-
|
|
9102
|
-
iter:
|
|
9103
|
-
usd:
|
|
9104
|
-
modelMs:
|
|
9105
|
-
scoreMs:
|
|
9106
|
-
objective:
|
|
9107
|
-
accepted:
|
|
9108
|
-
scores:
|
|
9109
|
-
behaviors:
|
|
9110
|
-
parseError:
|
|
9519
|
+
iterations: z12.array(
|
|
9520
|
+
z12.object({
|
|
9521
|
+
iter: z12.number(),
|
|
9522
|
+
usd: z12.number(),
|
|
9523
|
+
modelMs: z12.number().optional(),
|
|
9524
|
+
scoreMs: z12.number().optional(),
|
|
9525
|
+
objective: z12.tuple([z12.number(), z12.number(), z12.number()]),
|
|
9526
|
+
accepted: z12.boolean(),
|
|
9527
|
+
scores: z12.array(ConfigScoreSchema),
|
|
9528
|
+
behaviors: z12.array(BehaviorResultSchema).optional(),
|
|
9529
|
+
parseError: z12.string().optional()
|
|
9111
9530
|
})
|
|
9112
9531
|
)
|
|
9113
9532
|
});
|
|
@@ -9115,8 +9534,8 @@ var init_loop2 = __esm({
|
|
|
9115
9534
|
});
|
|
9116
9535
|
|
|
9117
9536
|
// packages/generate/src/brief.ts
|
|
9118
|
-
import { existsSync as
|
|
9119
|
-
import
|
|
9537
|
+
import { existsSync as existsSync24, readFileSync as readFileSync20 } from "node:fs";
|
|
9538
|
+
import path30 from "node:path";
|
|
9120
9539
|
function singleAxes2(name) {
|
|
9121
9540
|
const parsed = parseVariantAxes(name);
|
|
9122
9541
|
if (parsed === void 0) return void 0;
|
|
@@ -9357,21 +9776,21 @@ function authorBehaviors(api, extras = {}) {
|
|
|
9357
9776
|
return { behaviors, prelude: { controls: interactive ? ["> *"] : [], textInputs: [] }, disclosures };
|
|
9358
9777
|
}
|
|
9359
9778
|
function envelopeText(file) {
|
|
9360
|
-
return envelopeFirstTextPart(JSON.parse(
|
|
9779
|
+
return envelopeFirstTextPart(JSON.parse(readFileSync20(file, "utf8")));
|
|
9361
9780
|
}
|
|
9362
9781
|
function metadataText(file) {
|
|
9363
|
-
return envelopeTextContent(JSON.parse(
|
|
9782
|
+
return envelopeTextContent(JSON.parse(readFileSync20(file, "utf8")));
|
|
9364
9783
|
}
|
|
9365
9784
|
function dismissEvidence(setDir, repSlugs) {
|
|
9366
9785
|
for (const slug of repSlugs) {
|
|
9367
|
-
const f =
|
|
9368
|
-
if (!
|
|
9786
|
+
const f = path30.join(setDir, slug, "get_design_context.json");
|
|
9787
|
+
if (!existsSync24(f)) continue;
|
|
9369
9788
|
const text = envelopeText(f);
|
|
9370
9789
|
const propHit = /[{,]\s*(\w*dismiss\w*)\s*=\s*(?:true|false)\b/i.exec(text) ?? /\b(\w*dismiss\w*)\??\s*:\s*boolean\b/i.exec(text);
|
|
9371
9790
|
if (propHit !== null) return `emission prop "${propHit[1]}"`;
|
|
9372
9791
|
for (const m of text.matchAll(/data-name="([^"]+)"/g)) {
|
|
9373
|
-
const
|
|
9374
|
-
if (DISMISS_NAMES.has(
|
|
9792
|
+
const norm2 = m[1].toLowerCase().replace(/[^a-z0-9]/g, "");
|
|
9793
|
+
if (DISMISS_NAMES.has(norm2)) return `layer ${JSON.stringify(m[1])}`;
|
|
9375
9794
|
}
|
|
9376
9795
|
}
|
|
9377
9796
|
return void 0;
|
|
@@ -9409,13 +9828,13 @@ function recordedFontNeeds(setDir, opts = {}) {
|
|
|
9409
9828
|
}
|
|
9410
9829
|
};
|
|
9411
9830
|
const manifest = loadManifest(setDir);
|
|
9412
|
-
const setDefs =
|
|
9413
|
-
if (
|
|
9831
|
+
const setDefs = path30.join(setDir, "get_variable_defs.json");
|
|
9832
|
+
if (existsSync24(setDefs)) fromDefs(envelopeText(setDefs));
|
|
9414
9833
|
for (const rep of manifest.reps) {
|
|
9415
|
-
const ctx =
|
|
9416
|
-
if (
|
|
9417
|
-
const defs =
|
|
9418
|
-
if (
|
|
9834
|
+
const ctx = path30.join(setDir, rep.slug, "get_design_context.json");
|
|
9835
|
+
if (existsSync24(ctx)) fromEmission(envelopeText(ctx));
|
|
9836
|
+
const defs = path30.join(setDir, rep.slug, "get_variable_defs.json");
|
|
9837
|
+
if (existsSync24(defs)) fromDefs(envelopeText(defs));
|
|
9419
9838
|
}
|
|
9420
9839
|
return [...byFamily.entries()].map(([family, paired]) => {
|
|
9421
9840
|
const weights = /* @__PURE__ */ new Set([...paired, ...opts.pairedOnly === true ? [] : unpaired]);
|
|
@@ -9426,10 +9845,10 @@ function symbolFontGlyphCount(setDir, reps) {
|
|
|
9426
9845
|
const PUA = /[\uE000-\uF8FF\u{F0000}-\u{FFFFD}\u{100000}-\u{10FFFD}]/u;
|
|
9427
9846
|
const glyphs = /* @__PURE__ */ new Set();
|
|
9428
9847
|
for (const rep of reps) {
|
|
9429
|
-
const file =
|
|
9430
|
-
if (!
|
|
9848
|
+
const file = path30.join(setDir, rep, "get_metadata.json");
|
|
9849
|
+
if (!existsSync24(file)) continue;
|
|
9431
9850
|
try {
|
|
9432
|
-
const text = JSON.parse(
|
|
9851
|
+
const text = JSON.parse(readFileSync20(file, "utf8")).content.map((c) => c.text ?? "").join("\n");
|
|
9433
9852
|
for (const m of text.matchAll(/<text\s[^>]*name="([^"]*)"/g)) {
|
|
9434
9853
|
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)));
|
|
9435
9854
|
if (PUA.test(name)) glyphs.add(name);
|
|
@@ -9455,8 +9874,8 @@ function recordedTextSlots(setDir, repSlugs) {
|
|
|
9455
9874
|
const propRep = [];
|
|
9456
9875
|
const perRep = [];
|
|
9457
9876
|
for (const slug of repSlugs) {
|
|
9458
|
-
const f =
|
|
9459
|
-
if (!
|
|
9877
|
+
const f = path30.join(setDir, slug, "get_design_context.json");
|
|
9878
|
+
if (!existsSync24(f)) continue;
|
|
9460
9879
|
const code = envelopeText(f);
|
|
9461
9880
|
const props = /* @__PURE__ */ new Map();
|
|
9462
9881
|
for (const m of code.matchAll(/[{,]\s*(\w+)\s*=\s*"((?:[^"\\]|\\.)*)"/g)) {
|
|
@@ -9481,8 +9900,8 @@ function recordedTextSlots(setDir, repSlugs) {
|
|
|
9481
9900
|
}
|
|
9482
9901
|
const axisValuesBySlug = /* @__PURE__ */ new Map();
|
|
9483
9902
|
for (const slug of repSlugs) {
|
|
9484
|
-
const metaFile =
|
|
9485
|
-
if (!
|
|
9903
|
+
const metaFile = path30.join(setDir, slug, "get_metadata.json");
|
|
9904
|
+
if (!existsSync24(metaFile)) continue;
|
|
9486
9905
|
const name = symbolName(metadataText(metaFile));
|
|
9487
9906
|
if (name === void 0) continue;
|
|
9488
9907
|
const values = /* @__PURE__ */ new Set();
|
|
@@ -9581,8 +10000,8 @@ function authorTaskFromSet(setDir, opts = {}) {
|
|
|
9581
10000
|
const poses = [];
|
|
9582
10001
|
const missing = [];
|
|
9583
10002
|
for (const rep of manifest.reps) {
|
|
9584
|
-
const metaFile =
|
|
9585
|
-
if (!
|
|
10003
|
+
const metaFile = path30.join(setDir, rep.slug, "get_metadata.json");
|
|
10004
|
+
if (!existsSync24(metaFile)) {
|
|
9586
10005
|
missing.push(rep.slug);
|
|
9587
10006
|
continue;
|
|
9588
10007
|
}
|
|
@@ -9596,8 +10015,8 @@ function authorTaskFromSet(setDir, opts = {}) {
|
|
|
9596
10015
|
if (missing.length > 0) {
|
|
9597
10016
|
throw new Error(`recording set ${setDir} is missing metadata for ${missing.length} rep(s): ${missing.slice(0, 5).join(", ")}${missing.length > 5 ? ", \u2026" : ""}`);
|
|
9598
10017
|
}
|
|
9599
|
-
const setMeta =
|
|
9600
|
-
const latticeNames = manifest.latticeNames ?? (
|
|
10018
|
+
const setMeta = path30.join(setDir, "get_metadata.json");
|
|
10019
|
+
const latticeNames = manifest.latticeNames ?? (existsSync24(setMeta) ? [...metadataText(setMeta).matchAll(/name="([^"]*)"/g)].map((m) => decodeXmlEntities(m[1])) : void 0);
|
|
9601
10020
|
const defaults = { ...manifest.defaults ?? {}, ...opts.defaults ?? {} };
|
|
9602
10021
|
const recordedFonts = recordedFontFamilies(setDir);
|
|
9603
10022
|
const textSlots = recordedTextSlots(setDir, manifest.reps.map((r) => r.slug));
|
|
@@ -9780,10 +10199,10 @@ var init_brief = __esm({
|
|
|
9780
10199
|
});
|
|
9781
10200
|
|
|
9782
10201
|
// packages/generate/src/segments.ts
|
|
9783
|
-
import { existsSync as
|
|
9784
|
-
import
|
|
10202
|
+
import { existsSync as existsSync25, readFileSync as readFileSync21, readdirSync as readdirSync9 } from "node:fs";
|
|
10203
|
+
import path31 from "node:path";
|
|
9785
10204
|
function repText(set, rep, tool) {
|
|
9786
|
-
const env = JSON.parse(
|
|
10205
|
+
const env = JSON.parse(readFileSync21(path31.join(set, rep, `${tool}.json`), "utf8"));
|
|
9787
10206
|
return tool === "get_design_context" ? envelopeFirstTextPart(env) : envelopeTextContent(env);
|
|
9788
10207
|
}
|
|
9789
10208
|
function stripFigmaInstructions(emission) {
|
|
@@ -9843,20 +10262,20 @@ DROPPED from the map, untrustworthy in the source export \u2014 for these, each
|
|
|
9843
10262
|
}
|
|
9844
10263
|
function buildSegments(task, mode = "fenced") {
|
|
9845
10264
|
const SET = task.set;
|
|
9846
|
-
let defsRecorded =
|
|
10265
|
+
let defsRecorded = existsSync25(path31.join(SET, "get_variable_defs.json"));
|
|
9847
10266
|
let rawDefs = {};
|
|
9848
|
-
if (
|
|
9849
|
-
const text = envelopeFirstTextPart(JSON.parse(
|
|
10267
|
+
if (existsSync25(path31.join(SET, "get_variable_defs.json"))) {
|
|
10268
|
+
const text = envelopeFirstTextPart(JSON.parse(readFileSync21(path31.join(SET, "get_variable_defs.json"), "utf8"))) || "{}";
|
|
9850
10269
|
try {
|
|
9851
10270
|
rawDefs = JSON.parse(text);
|
|
9852
10271
|
} catch {
|
|
9853
10272
|
}
|
|
9854
10273
|
} else {
|
|
9855
10274
|
for (const cfg of task.configs) {
|
|
9856
|
-
const f =
|
|
9857
|
-
if (!
|
|
10275
|
+
const f = path31.join(SET, cfg.rep, "get_variable_defs.json");
|
|
10276
|
+
if (!existsSync25(f)) continue;
|
|
9858
10277
|
defsRecorded = true;
|
|
9859
|
-
const text = envelopeFirstTextPart(JSON.parse(
|
|
10278
|
+
const text = envelopeFirstTextPart(JSON.parse(readFileSync21(f, "utf8"))) || "{}";
|
|
9860
10279
|
try {
|
|
9861
10280
|
for (const [k, v] of Object.entries(JSON.parse(text))) rawDefs[k] ??= v;
|
|
9862
10281
|
} catch {
|
|
@@ -9864,8 +10283,8 @@ function buildSegments(task, mode = "fenced") {
|
|
|
9864
10283
|
}
|
|
9865
10284
|
}
|
|
9866
10285
|
const emissionTexts = task.configs.map((cfg) => {
|
|
9867
|
-
const f =
|
|
9868
|
-
return
|
|
10286
|
+
const f = path31.join(SET, cfg.rep, "get_design_context.json");
|
|
10287
|
+
return existsSync25(f) ? envelopeFirstTextPart(JSON.parse(readFileSync21(f, "utf8"))) : "";
|
|
9869
10288
|
});
|
|
9870
10289
|
const { map, note } = cleanTokenMap(rawDefs, emissionTexts);
|
|
9871
10290
|
const defs = JSON.stringify(map, null, 1);
|
|
@@ -9882,9 +10301,9 @@ TOKEN MAP NEVER RECORDED: this set holds no get_variable_defs envelope, so wheth
|
|
|
9882
10301
|
for (const cfg of task.configs) {
|
|
9883
10302
|
const meta = stripFigmaInstructions(repText(SET, cfg.rep, "get_metadata"));
|
|
9884
10303
|
const emission = stripFigmaInstructions(repText(SET, cfg.rep, "get_design_context"));
|
|
9885
|
-
const assets =
|
|
10304
|
+
const assets = readdirSync9(path31.join(SET, cfg.rep)).filter((f) => f.startsWith("asset-") && f.endsWith(".svg")).map((f) => `asset ${f}:
|
|
9886
10305
|
\`\`\`svg
|
|
9887
|
-
${
|
|
10306
|
+
${readFileSync21(path31.join(SET, cfg.rep, f), "utf8")}
|
|
9888
10307
|
\`\`\``).join("\n");
|
|
9889
10308
|
parts.push(`
|
|
9890
10309
|
## Config ${cfg.rep} \u2192 ${cfg.component} ${JSON.stringify(cfg.props)}
|
|
@@ -9909,7 +10328,7 @@ Optionally a third block with \`/* FILE: tokens.css */\`.`);
|
|
|
9909
10328
|
} else {
|
|
9910
10329
|
parts.push(`
|
|
9911
10330
|
## Output format
|
|
9912
|
-
Write the COMPLETE files (${task.entry}, styles.css, optional tokens.css) into ONE candidate directory: \`tendril-out/${
|
|
10331
|
+
Write the COMPLETE files (${task.entry}, styles.css, optional tokens.css) into ONE candidate directory: \`tendril-out/${path31.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.`);
|
|
9913
10332
|
}
|
|
9914
10333
|
return parts.join("\n");
|
|
9915
10334
|
}
|
|
@@ -9976,9 +10395,9 @@ var init_adapter = __esm({
|
|
|
9976
10395
|
});
|
|
9977
10396
|
|
|
9978
10397
|
// packages/generate/src/bundle-emit.ts
|
|
9979
|
-
import { createHash as
|
|
9980
|
-
import { copyFileSync, existsSync as
|
|
9981
|
-
import
|
|
10398
|
+
import { createHash as createHash5 } from "node:crypto";
|
|
10399
|
+
import { copyFileSync, existsSync as existsSync26, mkdirSync as mkdirSync7, readFileSync as readFileSync22, readdirSync as readdirSync10, rmSync as rmSync3, writeFileSync as writeFileSync11 } from "node:fs";
|
|
10400
|
+
import path32 from "node:path";
|
|
9982
10401
|
function pinFromConfigs(configs) {
|
|
9983
10402
|
const domains = /* @__PURE__ */ new Map();
|
|
9984
10403
|
const kinds = /* @__PURE__ */ new Map();
|
|
@@ -10047,9 +10466,9 @@ function fontsCssFor(faces, bundleDir, cacheDir = DEFAULT_FONT_CACHE, substitute
|
|
|
10047
10466
|
const notices = [];
|
|
10048
10467
|
const licenseTexts = /* @__PURE__ */ new Map();
|
|
10049
10468
|
for (const face of faces) {
|
|
10050
|
-
const src =
|
|
10051
|
-
const target = `./fonts/${
|
|
10052
|
-
const format = FONT_FORMATS[
|
|
10469
|
+
const src = path32.join(cacheDir, path32.basename(face.file));
|
|
10470
|
+
const target = `./fonts/${path32.basename(face.file)}`;
|
|
10471
|
+
const format = FONT_FORMATS[path32.extname(face.file).toLowerCase()] ?? "truetype";
|
|
10053
10472
|
const family = face.family.replace(/['\\]/g, "").replace(/\*\//g, "");
|
|
10054
10473
|
const decl = `@font-face { font-family: '${family}'; font-weight: ${face.weight}; font-style: normal; src: url(${target}) format('${format}'); }`;
|
|
10055
10474
|
const license = normalizeFontLicense(face.license);
|
|
@@ -10087,14 +10506,14 @@ function fontsCssFor(faces, bundleDir, cacheDir = DEFAULT_FONT_CACHE, substitute
|
|
|
10087
10506
|
`/* ${decl} */`
|
|
10088
10507
|
);
|
|
10089
10508
|
}
|
|
10090
|
-
} else if (
|
|
10091
|
-
mkdirSync7(
|
|
10092
|
-
copyFileSync(src,
|
|
10509
|
+
} else if (existsSync26(src) && createHash5("sha256").update(readFileSync22(src)).digest("hex") === face.sha256) {
|
|
10510
|
+
mkdirSync7(path32.join(bundleDir, "fonts"), { recursive: true });
|
|
10511
|
+
copyFileSync(src, path32.join(bundleDir, "fonts", path32.basename(face.file)));
|
|
10093
10512
|
licenseTexts.set(terms.file, terms.text);
|
|
10094
10513
|
const upstream = upstreamAttribution(face);
|
|
10095
10514
|
notices.push(
|
|
10096
10515
|
"",
|
|
10097
|
-
`${face.family.replace(/[\r\n]+/g, " ")} ${face.weight} \u2014 ./${
|
|
10516
|
+
`${face.family.replace(/[\r\n]+/g, " ")} ${face.weight} \u2014 ./${path32.basename(face.file)}`,
|
|
10098
10517
|
` licence: ${terms.title} \u2014 full text in ./${terms.file}`,
|
|
10099
10518
|
` source: ${face.source}`,
|
|
10100
10519
|
` sha256: ${face.sha256}`,
|
|
@@ -10108,9 +10527,9 @@ function fontsCssFor(faces, bundleDir, cacheDir = DEFAULT_FONT_CACHE, substitute
|
|
|
10108
10527
|
}
|
|
10109
10528
|
if (lines.length === 0) return null;
|
|
10110
10529
|
if (notices.length > 0) {
|
|
10111
|
-
const fontsDir =
|
|
10112
|
-
for (const [file, text] of licenseTexts) writeFileSync11(
|
|
10113
|
-
writeFileSync11(
|
|
10530
|
+
const fontsDir = path32.join(bundleDir, "fonts");
|
|
10531
|
+
for (const [file, text] of licenseTexts) writeFileSync11(path32.join(fontsDir, file), text);
|
|
10532
|
+
writeFileSync11(path32.join(fontsDir, "NOTICE.txt"), `${[NOTICE_PREAMBLE, ...notices].join("\n")}
|
|
10114
10533
|
`);
|
|
10115
10534
|
header.push(
|
|
10116
10535
|
"/* REDISTRIBUTION: the face files in ./fonts/ are redistributed under the licences",
|
|
@@ -10122,10 +10541,10 @@ function fontsCssFor(faces, bundleDir, cacheDir = DEFAULT_FONT_CACHE, substitute
|
|
|
10122
10541
|
`;
|
|
10123
10542
|
}
|
|
10124
10543
|
function countLatticeSymbols(setDir) {
|
|
10125
|
-
const manifestFile =
|
|
10126
|
-
if (
|
|
10544
|
+
const manifestFile = path32.join(setDir, "recording-set.json");
|
|
10545
|
+
if (existsSync26(manifestFile)) {
|
|
10127
10546
|
try {
|
|
10128
|
-
const stored = JSON.parse(
|
|
10547
|
+
const stored = JSON.parse(readFileSync22(manifestFile, "utf8"));
|
|
10129
10548
|
if (stored.variantScope !== "component-set") return null;
|
|
10130
10549
|
const lattice = stored.latticeNames;
|
|
10131
10550
|
if (lattice !== void 0 && lattice.length > 0) return lattice.length;
|
|
@@ -10133,13 +10552,13 @@ function countLatticeSymbols(setDir) {
|
|
|
10133
10552
|
}
|
|
10134
10553
|
}
|
|
10135
10554
|
const files = [
|
|
10136
|
-
|
|
10137
|
-
...
|
|
10138
|
-
].filter((f) =>
|
|
10555
|
+
path32.join(setDir, "get_metadata.json"),
|
|
10556
|
+
...existsSync26(setDir) ? readdirSync10(setDir).filter((f) => /^get_metadata-.*\.json$/.test(f)).map((f) => path32.join(setDir, f)) : []
|
|
10557
|
+
].filter((f) => existsSync26(f));
|
|
10139
10558
|
if (files.length === 0) return null;
|
|
10140
10559
|
let count = 0;
|
|
10141
10560
|
for (const f of files) {
|
|
10142
|
-
const text = envelopeTextContent(JSON.parse(
|
|
10561
|
+
const text = envelopeTextContent(JSON.parse(readFileSync22(f, "utf8")));
|
|
10143
10562
|
count += [...text.matchAll(/name="([^"]*)"/g)].filter((m) => m[1].includes("=")).length;
|
|
10144
10563
|
}
|
|
10145
10564
|
return count > 0 ? count : null;
|
|
@@ -10147,23 +10566,23 @@ function countLatticeSymbols(setDir) {
|
|
|
10147
10566
|
function recordingSetHash(setDir, configs) {
|
|
10148
10567
|
const relPaths = [];
|
|
10149
10568
|
for (const name of ["recording-set.json", "completeness-manifest.json", "get_variable_defs.json", "get_metadata.json"]) {
|
|
10150
|
-
if (
|
|
10569
|
+
if (existsSync26(path32.join(setDir, name))) relPaths.push(name);
|
|
10151
10570
|
}
|
|
10152
10571
|
for (const cfg of configs) {
|
|
10153
10572
|
for (const f of ["get_design_context.json", "get_metadata.json", "get_screenshot.json", "get_variable_defs.json"]) {
|
|
10154
|
-
if (
|
|
10573
|
+
if (existsSync26(path32.join(setDir, cfg.rep, f))) relPaths.push(`${cfg.rep}/${f}`);
|
|
10155
10574
|
}
|
|
10156
|
-
if (
|
|
10157
|
-
for (const asset of
|
|
10575
|
+
if (existsSync26(path32.join(setDir, cfg.rep))) {
|
|
10576
|
+
for (const asset of readdirSync10(path32.join(setDir, cfg.rep)).filter((f) => f.startsWith("asset-"))) {
|
|
10158
10577
|
relPaths.push(`${cfg.rep}/${asset}`);
|
|
10159
10578
|
}
|
|
10160
10579
|
}
|
|
10161
10580
|
}
|
|
10162
10581
|
return hashRecordingSet(
|
|
10163
10582
|
relPaths,
|
|
10164
|
-
(p) => new Uint8Array(
|
|
10583
|
+
(p) => new Uint8Array(readFileSync22(path32.join(setDir, p))),
|
|
10165
10584
|
(chunks) => {
|
|
10166
|
-
const h =
|
|
10585
|
+
const h = createHash5("sha256");
|
|
10167
10586
|
for (const c of chunks) h.update(c);
|
|
10168
10587
|
return h.digest("hex");
|
|
10169
10588
|
}
|
|
@@ -10182,11 +10601,11 @@ function emitBundleV1(opts) {
|
|
|
10182
10601
|
const certified = statuses.filter((s) => s.status === "certified").length;
|
|
10183
10602
|
const pass = statuses.filter((s) => s.status !== "fail").length;
|
|
10184
10603
|
const lattice = countLatticeSymbols(opts.task.set);
|
|
10185
|
-
const interaction = opts.behaviors.filter((b) => !b.id.startsWith("prelude:") && !b.id.startsWith("parity:"));
|
|
10604
|
+
const interaction = opts.behaviors.filter((b) => !b.id.startsWith("prelude:") && !b.id.startsWith("parity:") && !b.id.startsWith("composition:"));
|
|
10186
10605
|
const prelude = opts.behaviors.filter((b) => b.id.startsWith("prelude:"));
|
|
10187
10606
|
const parity = opts.behaviors.filter((b) => b.id.startsWith("parity:"));
|
|
10188
|
-
const cssFiles = ["styles.css", "tokens.css"].map((f) =>
|
|
10189
|
-
const families = cssFontFamilies(cssFiles.map((f) =>
|
|
10607
|
+
const cssFiles = ["styles.css", "tokens.css"].map((f) => path32.join(opts.bundleDir, f)).filter((f) => existsSync26(f));
|
|
10608
|
+
const families = cssFontFamilies(cssFiles.map((f) => readFileSync22(f, "utf8")).join("\n"));
|
|
10190
10609
|
const requiredFonts = requiredFontsManifest(families, opts.fontCacheDir).map((f) => ({
|
|
10191
10610
|
family: f.family,
|
|
10192
10611
|
weight: f.weight,
|
|
@@ -10215,7 +10634,7 @@ function emitBundleV1(opts) {
|
|
|
10215
10634
|
// resolvable via verify's --set override).
|
|
10216
10635
|
path: (() => {
|
|
10217
10636
|
const base = process.env["INIT_CWD"] ?? process.cwd();
|
|
10218
|
-
const rel =
|
|
10637
|
+
const rel = path32.relative(base, opts.task.set);
|
|
10219
10638
|
return rel !== "" && !rel.startsWith("..") ? rel : opts.task.set;
|
|
10220
10639
|
})(),
|
|
10221
10640
|
component: opts.componentName,
|
|
@@ -10239,21 +10658,21 @@ function emitBundleV1(opts) {
|
|
|
10239
10658
|
})
|
|
10240
10659
|
};
|
|
10241
10660
|
const written = [];
|
|
10242
|
-
const manifestPath2 =
|
|
10661
|
+
const manifestPath2 = path32.join(opts.bundleDir, "component.json");
|
|
10243
10662
|
writeFileSync11(manifestPath2, `${JSON.stringify(manifest, null, 2)}
|
|
10244
10663
|
`);
|
|
10245
10664
|
written.push(manifestPath2);
|
|
10246
|
-
const stylesPath =
|
|
10247
|
-
if (
|
|
10665
|
+
const stylesPath = path32.join(opts.bundleDir, "styles.css");
|
|
10666
|
+
if (existsSync26(stylesPath)) {
|
|
10248
10667
|
const comment = cssProvenanceComment({ pass, scored: statuses.length, certified, latticeConfigs: lattice });
|
|
10249
|
-
const current =
|
|
10668
|
+
const current = readFileSync22(stylesPath, "utf8");
|
|
10250
10669
|
const stripped = current.replace(/^\/\* tendril bundle v\d+ [^]*?\*\/\n/, "");
|
|
10251
10670
|
writeFileSync11(stylesPath, `${comment}
|
|
10252
10671
|
${stripped}`);
|
|
10253
10672
|
written.push(stylesPath);
|
|
10254
10673
|
}
|
|
10255
|
-
const fontsCssPath =
|
|
10256
|
-
rmSync3(
|
|
10674
|
+
const fontsCssPath = path32.join(opts.bundleDir, "fonts.css");
|
|
10675
|
+
rmSync3(path32.join(opts.bundleDir, "fonts"), { recursive: true, force: true });
|
|
10257
10676
|
rmSync3(fontsCssPath, { force: true });
|
|
10258
10677
|
const fontsCss = fontsCssFor(requiredFontsManifest(families, opts.fontCacheDir), opts.bundleDir, opts.fontCacheDir, opts.substitutedFamilies ?? []);
|
|
10259
10678
|
if (fontsCss !== null) {
|
|
@@ -10681,6 +11100,262 @@ DEALINGS IN THE FONT SOFTWARE.
|
|
|
10681
11100
|
}
|
|
10682
11101
|
});
|
|
10683
11102
|
|
|
11103
|
+
// packages/generate/src/compose-pins.ts
|
|
11104
|
+
import { createHash as createHash6 } from "node:crypto";
|
|
11105
|
+
import { existsSync as existsSync27, readFileSync as readFileSync23, readdirSync as readdirSync11, realpathSync as realpathSync3, statSync as statSync3 } from "node:fs";
|
|
11106
|
+
import path33 from "node:path";
|
|
11107
|
+
function bundleDirs(roots, depth = 4) {
|
|
11108
|
+
const found = [];
|
|
11109
|
+
const seen = /* @__PURE__ */ new Set();
|
|
11110
|
+
const walk2 = (dir, remaining) => {
|
|
11111
|
+
let key;
|
|
11112
|
+
try {
|
|
11113
|
+
key = realpathSync3(dir);
|
|
11114
|
+
} catch {
|
|
11115
|
+
key = path33.resolve(dir);
|
|
11116
|
+
}
|
|
11117
|
+
if (seen.has(key)) return;
|
|
11118
|
+
seen.add(key);
|
|
11119
|
+
if (existsSync27(path33.join(dir, "component.json"))) {
|
|
11120
|
+
found.push(key);
|
|
11121
|
+
return;
|
|
11122
|
+
}
|
|
11123
|
+
if (remaining === 0) return;
|
|
11124
|
+
let entries;
|
|
11125
|
+
try {
|
|
11126
|
+
entries = readdirSync11(dir);
|
|
11127
|
+
} catch {
|
|
11128
|
+
return;
|
|
11129
|
+
}
|
|
11130
|
+
for (const e of entries) {
|
|
11131
|
+
if (e === "node_modules" || e.startsWith(".")) continue;
|
|
11132
|
+
const full = path33.join(dir, e);
|
|
11133
|
+
try {
|
|
11134
|
+
if (statSync3(full).isDirectory()) walk2(full, remaining - 1);
|
|
11135
|
+
} catch {
|
|
11136
|
+
}
|
|
11137
|
+
}
|
|
11138
|
+
};
|
|
11139
|
+
for (const r of roots) walk2(path33.resolve(r), depth);
|
|
11140
|
+
return found;
|
|
11141
|
+
}
|
|
11142
|
+
function composedPins(hostSet, libraryRoots) {
|
|
11143
|
+
const { rows, malformed } = confirmedCompositionStatus(hostSet);
|
|
11144
|
+
const issues = [];
|
|
11145
|
+
if (malformed !== void 0) issues.push(`compositions extension partially unreadable (${malformed}) \u2014 the affected entries are not pinned`);
|
|
11146
|
+
const usable = rows.filter((r) => r.status === "supported" || r.status === "stale-supported");
|
|
11147
|
+
for (const r of rows) {
|
|
11148
|
+
if (!usable.includes(r)) issues.push(`confirmed pair ${r.key} is ${r.status} \u2014 not pinned (${r.detail})`);
|
|
11149
|
+
}
|
|
11150
|
+
if (usable.length === 0) return { pins: [], issues };
|
|
11151
|
+
const candidates = bundleDirs(libraryRoots);
|
|
11152
|
+
const pins = [];
|
|
11153
|
+
for (const row of usable) {
|
|
11154
|
+
const partnerRels = row.key.split("+");
|
|
11155
|
+
let pinned = false;
|
|
11156
|
+
const failures = [];
|
|
11157
|
+
for (const rel of partnerRels) {
|
|
11158
|
+
const partnerSet = path33.resolve(hostSet, rel);
|
|
11159
|
+
let partnerTask;
|
|
11160
|
+
let partnerManifest;
|
|
11161
|
+
try {
|
|
11162
|
+
partnerManifest = loadManifest(partnerSet);
|
|
11163
|
+
partnerTask = authorTaskFromSet(partnerSet).task;
|
|
11164
|
+
} catch (err) {
|
|
11165
|
+
failures.push(`${rel}: partner set does not author (${err instanceof Error ? err.message.split("\n")[0] : String(err)})`);
|
|
11166
|
+
continue;
|
|
11167
|
+
}
|
|
11168
|
+
const slugByVariant = new Map(partnerManifest.reps.map((rep) => [rep.nodeId, rep.slug]));
|
|
11169
|
+
const wantedPoses = /* @__PURE__ */ new Map();
|
|
11170
|
+
let poseMissing = false;
|
|
11171
|
+
for (const inst of row.instances) {
|
|
11172
|
+
const slug = slugByVariant.get(inst.poseVariantNodeId);
|
|
11173
|
+
const cfg = slug !== void 0 ? partnerTask.configs.find((c) => c.rep === slug) : void 0;
|
|
11174
|
+
if (slug === void 0 || cfg === void 0) {
|
|
11175
|
+
poseMissing = true;
|
|
11176
|
+
break;
|
|
11177
|
+
}
|
|
11178
|
+
wantedPoses.set(inst.poseVariantNodeId, slug);
|
|
11179
|
+
}
|
|
11180
|
+
if (poseMissing) {
|
|
11181
|
+
failures.push(`${rel}: does not record the confirmed pose`);
|
|
11182
|
+
continue;
|
|
11183
|
+
}
|
|
11184
|
+
const wantHash = recordingSetHash(partnerSet, partnerTask.configs);
|
|
11185
|
+
const matches = candidates.filter((dir) => {
|
|
11186
|
+
try {
|
|
11187
|
+
const parsed = readBundleManifest(readFileSync23(path33.join(dir, "component.json"), "utf8"));
|
|
11188
|
+
return parsed.manifest?.provenance.recordingSet.hash === wantHash;
|
|
11189
|
+
} catch {
|
|
11190
|
+
return false;
|
|
11191
|
+
}
|
|
11192
|
+
});
|
|
11193
|
+
if (matches.length === 0) {
|
|
11194
|
+
failures.push(
|
|
11195
|
+
`${rel}: no generated bundle joins this set's CURRENT recording hash (searched ${libraryRoots.length} root(s) to depth 4) \u2014 generate the partner first (a stale partner bundle does not join; regenerate it), or pass --library <dir> if the bundle lives elsewhere`
|
|
11196
|
+
);
|
|
11197
|
+
continue;
|
|
11198
|
+
}
|
|
11199
|
+
if (matches.length > 1) {
|
|
11200
|
+
failures.push(`${rel}: ${matches.length} bundles join the same recording hash (${matches.map((m) => path33.basename(m)).join(", ")}) \u2014 ambiguous; remove or point --library away from the duplicates`);
|
|
11201
|
+
continue;
|
|
11202
|
+
}
|
|
11203
|
+
const bundleDir = matches[0];
|
|
11204
|
+
let manifest;
|
|
11205
|
+
try {
|
|
11206
|
+
manifest = readBundleManifest(readFileSync23(path33.join(bundleDir, "component.json"), "utf8")).manifest;
|
|
11207
|
+
} catch {
|
|
11208
|
+
manifest = void 0;
|
|
11209
|
+
}
|
|
11210
|
+
if (manifest === void 0) {
|
|
11211
|
+
failures.push(`${rel}: partner bundle manifest at ${bundleDir} became unreadable`);
|
|
11212
|
+
continue;
|
|
11213
|
+
}
|
|
11214
|
+
if (!safeSegment(manifest.name) || !safeSegment(manifest.entry)) {
|
|
11215
|
+
failures.push(
|
|
11216
|
+
`${rel}: partner bundle at ${bundleDir} declares an unsafe name/entry (${JSON.stringify(manifest.name)} / ${JSON.stringify(manifest.entry)}) \u2014 refused; both must be a single plain path segment`
|
|
11217
|
+
);
|
|
11218
|
+
continue;
|
|
11219
|
+
}
|
|
11220
|
+
const moduleFiles = [];
|
|
11221
|
+
let fileIssue;
|
|
11222
|
+
for (const name of [manifest.entry, "styles.css", "tokens.css", "fonts.css"]) {
|
|
11223
|
+
const file = path33.join(bundleDir, name);
|
|
11224
|
+
if (!existsSync27(file)) {
|
|
11225
|
+
if (name === manifest.entry || name === "styles.css") {
|
|
11226
|
+
fileIssue = `${rel}: partner bundle is missing ${name}`;
|
|
11227
|
+
break;
|
|
11228
|
+
}
|
|
11229
|
+
continue;
|
|
11230
|
+
}
|
|
11231
|
+
let bytes;
|
|
11232
|
+
try {
|
|
11233
|
+
bytes = readFileSync23(file);
|
|
11234
|
+
} catch {
|
|
11235
|
+
fileIssue = `${rel}: partner file ${name} at ${bundleDir} is unreadable`;
|
|
11236
|
+
break;
|
|
11237
|
+
}
|
|
11238
|
+
if (bytes.byteLength > MAX_PINNED_FILE_BYTES) {
|
|
11239
|
+
fileIssue = `${rel}: partner file ${name} exceeds the pin size cap (${bytes.byteLength} bytes)`;
|
|
11240
|
+
break;
|
|
11241
|
+
}
|
|
11242
|
+
moduleFiles.push({ name, content: bytes.toString("utf8"), sha256: createHash6("sha256").update(bytes).digest("hex") });
|
|
11243
|
+
}
|
|
11244
|
+
if (fileIssue !== void 0) {
|
|
11245
|
+
failures.push(fileIssue);
|
|
11246
|
+
continue;
|
|
11247
|
+
}
|
|
11248
|
+
pins.push({
|
|
11249
|
+
pairKey: row.key,
|
|
11250
|
+
partnerName: manifest.name,
|
|
11251
|
+
partnerBundleDir: bundleDir,
|
|
11252
|
+
moduleFiles,
|
|
11253
|
+
entryModule: manifest.entry,
|
|
11254
|
+
entryComponent: manifest.entry.replace(/\.tsx?$/, ""),
|
|
11255
|
+
instances: row.instances.map((inst) => {
|
|
11256
|
+
const slug = slugByVariant.get(inst.poseVariantNodeId);
|
|
11257
|
+
const cfg = partnerTask.configs.find((c) => c.rep === slug);
|
|
11258
|
+
return { hostRep: inst.hostRep, instanceId: inst.instanceId, poseVariantNodeId: inst.poseVariantNodeId, partnerRep: slug, props: { ...cfg.props } };
|
|
11259
|
+
})
|
|
11260
|
+
});
|
|
11261
|
+
pinned = true;
|
|
11262
|
+
break;
|
|
11263
|
+
}
|
|
11264
|
+
if (!pinned) {
|
|
11265
|
+
issues.push(`confirmed pair ${row.key} could not be pinned: ${failures.join("; ") || "no partner recording authored"}`);
|
|
11266
|
+
}
|
|
11267
|
+
}
|
|
11268
|
+
const byDir = /* @__PURE__ */ new Map();
|
|
11269
|
+
for (const pin of pins) {
|
|
11270
|
+
const dir = composedModuleDir(pin.partnerName);
|
|
11271
|
+
byDir.set(dir, [...byDir.get(dir) ?? [], pin]);
|
|
11272
|
+
}
|
|
11273
|
+
const conflicted = /* @__PURE__ */ new Set();
|
|
11274
|
+
for (const [dir, group] of byDir) {
|
|
11275
|
+
if (group.length < 2) continue;
|
|
11276
|
+
const signature = (p) => p.moduleFiles.map((f) => `${f.name}:${f.sha256}`).sort().join(",");
|
|
11277
|
+
if (new Set(group.map(signature)).size > 1) {
|
|
11278
|
+
for (const p of group) conflicted.add(p);
|
|
11279
|
+
issues.push(
|
|
11280
|
+
`pairs ${group.map((p) => p.pairKey).join(" and ")} both prescribe ${dir}/ with DIFFERENT bytes \u2014 mutually unsatisfiable, none pinned; rename one partner component or point --library away from one of the bundles`
|
|
11281
|
+
);
|
|
11282
|
+
}
|
|
11283
|
+
}
|
|
11284
|
+
return { pins: pins.filter((p) => !conflicted.has(p)), issues };
|
|
11285
|
+
}
|
|
11286
|
+
function declaredImports(source) {
|
|
11287
|
+
const stripped = source.replace(/\/\*[\s\S]*?\*\//g, "").replace(/\/\/[^\n]*/g, "");
|
|
11288
|
+
const specs = [];
|
|
11289
|
+
const patterns = [
|
|
11290
|
+
/(?:^|[^.\w])import\s[^"'`;]*?from\s*["']([^"']+)["']/g,
|
|
11291
|
+
/(?:^|[^.\w])export\s[^"'`;]*?from\s*["']([^"']+)["']/g,
|
|
11292
|
+
/(?:^|[^.\w])import\s*\(\s*["']([^"']+)["']\s*\)/g,
|
|
11293
|
+
/(?:^|[^.\w])require\s*\(\s*["']([^"']+)["']\s*\)/g
|
|
11294
|
+
];
|
|
11295
|
+
for (const re of patterns) {
|
|
11296
|
+
for (const m of stripped.matchAll(re)) specs.push(m[1]);
|
|
11297
|
+
}
|
|
11298
|
+
return specs;
|
|
11299
|
+
}
|
|
11300
|
+
function composedChecks(candidateDir, hostEntry, pins) {
|
|
11301
|
+
const checks = [];
|
|
11302
|
+
let entrySource = "";
|
|
11303
|
+
try {
|
|
11304
|
+
entrySource = readFileSync23(path33.join(candidateDir, hostEntry), "utf8");
|
|
11305
|
+
} catch {
|
|
11306
|
+
}
|
|
11307
|
+
const candidateRoot = path33.resolve(candidateDir);
|
|
11308
|
+
for (const pin of pins) {
|
|
11309
|
+
const dir = composedModuleDir(pin.partnerName);
|
|
11310
|
+
const resolvedDir = path33.resolve(candidateDir, dir);
|
|
11311
|
+
if (!resolvedDir.startsWith(candidateRoot + path33.sep)) {
|
|
11312
|
+
checks.push({ id: `composition:${pin.pairKey}:module-verbatim`, pass: false, detail: `pin path ${dir}/ escapes the candidate directory \u2014 refused` });
|
|
11313
|
+
checks.push({ id: `composition:${pin.pairKey}:imported`, pass: false, detail: `pin path ${dir}/ escapes the candidate directory \u2014 refused` });
|
|
11314
|
+
continue;
|
|
11315
|
+
}
|
|
11316
|
+
const wrong = [];
|
|
11317
|
+
for (const f of pin.moduleFiles) {
|
|
11318
|
+
if (!safeSegment(f.name)) {
|
|
11319
|
+
wrong.push(`${f.name} refused (not a plain path segment)`);
|
|
11320
|
+
continue;
|
|
11321
|
+
}
|
|
11322
|
+
const target = path33.join(candidateDir, dir, f.name);
|
|
11323
|
+
if (!existsSync27(target)) {
|
|
11324
|
+
wrong.push(`${f.name} missing`);
|
|
11325
|
+
continue;
|
|
11326
|
+
}
|
|
11327
|
+
const sha = createHash6("sha256").update(readFileSync23(target)).digest("hex");
|
|
11328
|
+
if (sha !== f.sha256) wrong.push(`${f.name} differs from the pinned partner bytes`);
|
|
11329
|
+
}
|
|
11330
|
+
checks.push({
|
|
11331
|
+
id: `composition:${pin.pairKey}:module-verbatim`,
|
|
11332
|
+
pass: wrong.length === 0,
|
|
11333
|
+
...wrong.length > 0 ? { detail: `pinned partner module not present verbatim at ${dir}/: ${wrong.join(", ")}` } : {}
|
|
11334
|
+
});
|
|
11335
|
+
const specifier = `./${dir}/${pin.entryComponent}`;
|
|
11336
|
+
const imported = declaredImports(entrySource).includes(specifier);
|
|
11337
|
+
checks.push({
|
|
11338
|
+
id: `composition:${pin.pairKey}:imported`,
|
|
11339
|
+
pass: imported,
|
|
11340
|
+
...imported ? {} : { detail: `host entry has no import declaration for the pinned module (expected \`from "${specifier}"\`)` }
|
|
11341
|
+
});
|
|
11342
|
+
}
|
|
11343
|
+
return checks;
|
|
11344
|
+
}
|
|
11345
|
+
var composedModuleDir, safeSegment, MAX_PINNED_FILE_BYTES;
|
|
11346
|
+
var init_compose_pins = __esm({
|
|
11347
|
+
"packages/generate/src/compose-pins.ts"() {
|
|
11348
|
+
"use strict";
|
|
11349
|
+
init_src();
|
|
11350
|
+
init_src6();
|
|
11351
|
+
init_brief();
|
|
11352
|
+
init_bundle_emit();
|
|
11353
|
+
composedModuleDir = (partnerName) => path33.posix.join("composed", partnerName);
|
|
11354
|
+
safeSegment = (s) => /^[A-Za-z0-9][A-Za-z0-9._ -]*$/.test(s) && !s.includes("..");
|
|
11355
|
+
MAX_PINNED_FILE_BYTES = 1024 * 1024;
|
|
11356
|
+
}
|
|
11357
|
+
});
|
|
11358
|
+
|
|
10684
11359
|
// packages/generate/src/index.ts
|
|
10685
11360
|
var init_src7 = __esm({
|
|
10686
11361
|
"packages/generate/src/index.ts"() {
|
|
@@ -10694,13 +11369,14 @@ var init_src7 = __esm({
|
|
|
10694
11369
|
init_consent();
|
|
10695
11370
|
init_adapter();
|
|
10696
11371
|
init_bundle_emit();
|
|
11372
|
+
init_compose_pins();
|
|
10697
11373
|
}
|
|
10698
11374
|
});
|
|
10699
11375
|
|
|
10700
11376
|
// packages/cli/src/font-guidance.ts
|
|
10701
|
-
import
|
|
11377
|
+
import path34 from "node:path";
|
|
10702
11378
|
function fontsUnprovenRemediation(setDir) {
|
|
10703
|
-
const set = setDir === void 0 ? void 0 :
|
|
11379
|
+
const set = setDir === void 0 ? void 0 : path34.resolve(setDir);
|
|
10704
11380
|
if (set !== void 0) {
|
|
10705
11381
|
try {
|
|
10706
11382
|
const needs = recordedFontNeeds(set);
|
|
@@ -10775,8 +11451,8 @@ __export(fonts_exports, {
|
|
|
10775
11451
|
runFontsResolveSet: () => runFontsResolveSet,
|
|
10776
11452
|
runFontsStatus: () => runFontsStatus
|
|
10777
11453
|
});
|
|
10778
|
-
import { existsSync as
|
|
10779
|
-
import
|
|
11454
|
+
import { existsSync as existsSync28, readFileSync as readFileSync24 } from "node:fs";
|
|
11455
|
+
import path35 from "node:path";
|
|
10780
11456
|
async function runFontsResolve(opts) {
|
|
10781
11457
|
const result = await resolveFonts(opts.family, opts.weights, opts.cacheDir);
|
|
10782
11458
|
const queryRefusal = systemFaceRefusal(opts.family.trim());
|
|
@@ -10797,7 +11473,7 @@ async function runFontsResolve(opts) {
|
|
|
10797
11473
|
}
|
|
10798
11474
|
}
|
|
10799
11475
|
async function runFontsResolveSet(opts) {
|
|
10800
|
-
const setDir =
|
|
11476
|
+
const setDir = path35.resolve(process.env["INIT_CWD"] ?? process.cwd(), opts.set);
|
|
10801
11477
|
let needs = [];
|
|
10802
11478
|
try {
|
|
10803
11479
|
needs = recordedFontNeeds(setDir);
|
|
@@ -10880,16 +11556,16 @@ async function runFontsResolveSet(opts) {
|
|
|
10880
11556
|
}
|
|
10881
11557
|
}
|
|
10882
11558
|
function runFontsStatus(opts) {
|
|
10883
|
-
const manifestPath2 =
|
|
10884
|
-
if (!
|
|
11559
|
+
const manifestPath2 = path35.join(opts.cacheDir, "manifest.json");
|
|
11560
|
+
if (!existsSync28(manifestPath2)) {
|
|
10885
11561
|
fail(opts, ExitCode.FontsUnproven, {
|
|
10886
11562
|
error: `no font cache at ${opts.cacheDir}`,
|
|
10887
11563
|
code: "fonts-unresolved",
|
|
10888
11564
|
remediation: `Run \`${tendrilCommand("fonts resolve --set <recording-dir>")}\` (or \`${tendrilCommand('fonts resolve "<Family>" --weights 400 500 600')}\`) first.`
|
|
10889
11565
|
});
|
|
10890
11566
|
}
|
|
10891
|
-
const faces = JSON.parse(
|
|
10892
|
-
const lockVerdicts = opts.lock !== void 0 ? checkFontLock(
|
|
11567
|
+
const faces = JSON.parse(readFileSync24(manifestPath2, "utf8"));
|
|
11568
|
+
const lockVerdicts = opts.lock !== void 0 ? checkFontLock(path35.resolve(opts.lock), opts.cacheDir) : null;
|
|
10893
11569
|
emitData(opts, { faces, lockVerdicts }, () => {
|
|
10894
11570
|
for (const f of faces) process.stdout.write(`cached ${f.family} ${f.weight} (${f.sha256.slice(0, 12)}\u2026)
|
|
10895
11571
|
`);
|
|
@@ -10933,13 +11609,13 @@ function familyMismatch(family, declared) {
|
|
|
10933
11609
|
}
|
|
10934
11610
|
function runFontsAdd(opts) {
|
|
10935
11611
|
if (opts.set !== void 0) {
|
|
10936
|
-
const declared = taskFontFamilies(
|
|
11612
|
+
const declared = taskFontFamilies(path35.resolve(opts.set)) ?? [];
|
|
10937
11613
|
const mismatch = familyMismatch(opts.family, declared);
|
|
10938
11614
|
if (mismatch !== void 0) {
|
|
10939
11615
|
fail(opts, ExitCode.InputValidation, {
|
|
10940
11616
|
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.`,
|
|
10941
11617
|
code: "font-family-not-declared",
|
|
10942
|
-
remediation: mismatch.nearest !== void 0 ? `Did you mean "${mismatch.nearest}"? ${tendrilCommand(`fonts add "${mismatch.nearest}" ${opts.weight} ${opts.file} --set ${
|
|
11618
|
+
remediation: mismatch.nearest !== void 0 ? `Did you mean "${mismatch.nearest}"? ${tendrilCommand(`fonts add "${mismatch.nearest}" ${opts.weight} ${opts.file} --set ${path35.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.`
|
|
10943
11619
|
});
|
|
10944
11620
|
}
|
|
10945
11621
|
} else {
|
|
@@ -10998,13 +11674,13 @@ cache them: ${tendrilCommand(`fonts add-system ${quoteArg(opts.family)}`)}
|
|
|
10998
11674
|
}
|
|
10999
11675
|
function runFontsAddSystem(opts) {
|
|
11000
11676
|
if (opts.set !== void 0) {
|
|
11001
|
-
const declared = taskFontFamilies(
|
|
11677
|
+
const declared = taskFontFamilies(path35.resolve(opts.set)) ?? [];
|
|
11002
11678
|
const mismatch = familyMismatch(opts.family, declared);
|
|
11003
11679
|
if (mismatch !== void 0) {
|
|
11004
11680
|
fail(opts, ExitCode.InputValidation, {
|
|
11005
11681
|
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.`,
|
|
11006
11682
|
code: "font-family-not-declared",
|
|
11007
|
-
remediation: mismatch.nearest !== void 0 ? `Did you mean "${mismatch.nearest}"? ${tendrilCommand(`fonts add-system ${quoteArg(mismatch.nearest)} --set ${quoteArg(
|
|
11683
|
+
remediation: mismatch.nearest !== void 0 ? `Did you mean "${mismatch.nearest}"? ${tendrilCommand(`fonts add-system ${quoteArg(mismatch.nearest)} --set ${quoteArg(path35.resolve(opts.set))}`)}` : `Use one of the declared families exactly as the recording spells it, or drop --set.`
|
|
11008
11684
|
});
|
|
11009
11685
|
}
|
|
11010
11686
|
} else {
|
|
@@ -11056,10 +11732,13 @@ var init_fonts = __esm({
|
|
|
11056
11732
|
// packages/cli/src/commands/verify.ts
|
|
11057
11733
|
var verify_exports = {};
|
|
11058
11734
|
__export(verify_exports, {
|
|
11735
|
+
COMPOSE_ON_GENERATE_ARMED: () => COMPOSE_ON_GENERATE_ARMED,
|
|
11059
11736
|
NO_ROLE_MANIFEST: () => NO_ROLE_MANIFEST,
|
|
11060
11737
|
ROLES_NOT_RESOLVED: () => ROLES_NOT_RESOLVED,
|
|
11061
11738
|
checkSummarySegments: () => checkSummarySegments,
|
|
11062
11739
|
compositionReport: () => compositionReport,
|
|
11740
|
+
crossCompositionDemotions: () => crossCompositionDemotions,
|
|
11741
|
+
crossCompositionLines: () => crossCompositionLines,
|
|
11063
11742
|
eyeCheck: () => eyeCheck,
|
|
11064
11743
|
failureTally: () => failureTally,
|
|
11065
11744
|
foldConfigStatus: () => foldConfigStatus,
|
|
@@ -11071,15 +11750,15 @@ __export(verify_exports, {
|
|
|
11071
11750
|
resolveComposition: () => resolveComposition,
|
|
11072
11751
|
runVerify: () => runVerify
|
|
11073
11752
|
});
|
|
11074
|
-
import { existsSync as
|
|
11075
|
-
import
|
|
11753
|
+
import { existsSync as existsSync29, readFileSync as readFileSync25 } from "node:fs";
|
|
11754
|
+
import path36 from "node:path";
|
|
11076
11755
|
function interactionCoverage(behaviors) {
|
|
11077
11756
|
const parity = behaviors.filter((b) => b.id.startsWith("parity:"));
|
|
11078
|
-
const interaction = behaviors.filter((b) => !b.id.startsWith("prelude:") && !b.id.startsWith("parity:"));
|
|
11757
|
+
const interaction = behaviors.filter((b) => !b.id.startsWith("prelude:") && !b.id.startsWith("parity:") && !b.id.startsWith("composition:"));
|
|
11079
11758
|
return {
|
|
11080
11759
|
interactionChecks: interaction.length,
|
|
11081
11760
|
interactionPassed: interaction.filter((b) => b.pass).length,
|
|
11082
|
-
preludeChecks: behaviors.
|
|
11761
|
+
preludeChecks: behaviors.filter((b) => b.id.startsWith("prelude:")).length,
|
|
11083
11762
|
parityChecks: parity.length,
|
|
11084
11763
|
parityPassed: parity.filter((b) => b.pass).length,
|
|
11085
11764
|
// Asymmetric on purpose (review finding F3): a parity PASS never
|
|
@@ -11179,7 +11858,61 @@ function checkSummarySegments(input) {
|
|
|
11179
11858
|
const occ = occlusionReport(input.occlusion);
|
|
11180
11859
|
const occlusion = "unavailable" in occ ? `occlusion not applicable (${NO_OVERLAY_DECLARED})` : `occlusion ${tally(input.occlusion)}`;
|
|
11181
11860
|
const operability = "short" in input.operability ? `operability UNVERIFIED (${input.operability.short})` : `operability ${input.operability.passed}/${input.operability.checks} interaction`;
|
|
11182
|
-
|
|
11861
|
+
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)` : (() => {
|
|
11862
|
+
const supported = input.crossComposition.rows.filter((r) => r.status === "supported" || r.status === "stale-supported").length;
|
|
11863
|
+
const composed = supported > 0 ? `, ${input.crossComposition.composed ?? 0} composed` : "";
|
|
11864
|
+
return ` \xB7 cross-composition ${supported}/${input.crossComposition.rows.length} supported${composed}${input.crossComposition.malformed !== void 0 ? " (some entries REJECTED)" : ""}`;
|
|
11865
|
+
})();
|
|
11866
|
+
return ` \xB7 ${operability} \xB7 ${composition} \xB7 ${occlusion}${cross}`;
|
|
11867
|
+
}
|
|
11868
|
+
function crossCompositionDemotions(rows, scoredReps, armed, composedPairs = /* @__PURE__ */ new Set()) {
|
|
11869
|
+
const demote = [];
|
|
11870
|
+
const orphanFailRows = [];
|
|
11871
|
+
for (const row of rows) {
|
|
11872
|
+
const failing = row.status === "partner-missing" || row.status === "partner-unreadable" || row.status === "unsupported" || row.status === "stale-unsupported";
|
|
11873
|
+
const arming = armed && (row.status === "supported" || row.status === "stale-supported") && !composedPairs.has(row.key);
|
|
11874
|
+
if (!failing && !arming) continue;
|
|
11875
|
+
const reps = arming ? [...new Set(row.instances.map((i) => i.hostRep))] : row.affectedReps;
|
|
11876
|
+
const reason = arming ? `confirmed composition NOT COMPOSED \u2014 the pinned partner module is not present verbatim with its import declared; re-run \`tendril engine brief\` for this set (the pins ride the payload) and regenerate (ADR-013, ${row.key})` : `confirmed composition ${row.status} \u2014 ${row.detail} (ADR-013)`;
|
|
11877
|
+
for (const rep of reps) {
|
|
11878
|
+
if (scoredReps.has(rep)) demote.push({ rep, reason });
|
|
11879
|
+
else orphanFailRows.push({ rep, error: `confirmed composition names rep "${rep}", which this set does not score (${row.status}, pair ${row.key}) \u2014 the claim cannot be cleared; repair or re-decide the composition` });
|
|
11880
|
+
}
|
|
11881
|
+
}
|
|
11882
|
+
return { demote, orphanFailRows };
|
|
11883
|
+
}
|
|
11884
|
+
function crossCompositionLines(input) {
|
|
11885
|
+
const lines = [];
|
|
11886
|
+
if (input.malformed !== void 0) {
|
|
11887
|
+
const plural = input.malformed.includes(";") ? "ies" : "y";
|
|
11888
|
+
lines.push(
|
|
11889
|
+
`UNAVAILABLE cross-composition entr${plural} REJECTED (${input.malformed}) \u2014 the backstop did NOT run over the rejected entr${plural}; repair the manifest and re-verify. Instrument failure, not a clean bill.`
|
|
11890
|
+
);
|
|
11891
|
+
}
|
|
11892
|
+
for (const row of input.rows) {
|
|
11893
|
+
const isSupported = row.status === "supported" || row.status === "stale-supported";
|
|
11894
|
+
const staleNote = row.status === "stale-supported" ? `; NOTE: ${row.detail}` : "";
|
|
11895
|
+
if (isSupported && input.composedPairs.has(row.key)) {
|
|
11896
|
+
lines.push(
|
|
11897
|
+
`COMPOSITION COMPOSED ${row.displayName} [${row.key}] \u2014 pinned partner module present verbatim, import declared${staleNote}. Rendered-mount stamping and region crops are the named next increment; whole-frame pixels remain the pixel evidence.`
|
|
11898
|
+
);
|
|
11899
|
+
} else if (isSupported && input.armed) {
|
|
11900
|
+
lines.push(
|
|
11901
|
+
`COMPOSITION FAIL ${row.displayName} [${row.key}] \u2014 confirmed but NOT COMPOSED (pinned module not present verbatim with its import declared)${staleNote} \u2014 the affected configs are demoted; re-run \`tendril engine brief\` for this set (the pins ride the payload) and regenerate`
|
|
11902
|
+
);
|
|
11903
|
+
} else if (isSupported) {
|
|
11904
|
+
lines.push(
|
|
11905
|
+
`COMPOSITION PENDING ${row.displayName} [${row.key}] \u2014 confirmed, not composed in this bundle${staleNote}; verdict unchanged while compose-on-generate is disarmed (ADR-013 \xA73; a PIN ISSUE line below carries the cause when pinning itself failed)`
|
|
11906
|
+
);
|
|
11907
|
+
} else {
|
|
11908
|
+
lines.push(`COMPOSITION FAIL ${row.displayName} [${row.key}] \u2014 ${row.detail} \u2014 the affected configs are demoted`);
|
|
11909
|
+
}
|
|
11910
|
+
}
|
|
11911
|
+
const restatesRow = /^confirmed pair .+ is (?:partner-missing|partner-unreadable|unsupported|stale-unsupported) — not pinned/;
|
|
11912
|
+
for (const issue of input.pinIssues.filter((i) => !restatesRow.test(i))) {
|
|
11913
|
+
lines.push(`COMPOSITION PIN ISSUE ${issue}`);
|
|
11914
|
+
}
|
|
11915
|
+
return lines;
|
|
11183
11916
|
}
|
|
11184
11917
|
function latticeCoverage(setManifest, scoredConfigs) {
|
|
11185
11918
|
const lattice = setManifest.latticeNames?.length;
|
|
@@ -11224,7 +11957,7 @@ function compositionReport(input) {
|
|
|
11224
11957
|
function eyeCheck(bundleDir) {
|
|
11225
11958
|
return {
|
|
11226
11959
|
command: tendrilCommand(`inspect "${bundleDir}"`),
|
|
11227
|
-
sheetPath:
|
|
11960
|
+
sheetPath: path36.join(bundleDir, "verify-evidence", "inspect.html"),
|
|
11228
11961
|
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."
|
|
11229
11962
|
};
|
|
11230
11963
|
}
|
|
@@ -11236,7 +11969,7 @@ function taskFromManifest(opts, manifest, setDir) {
|
|
|
11236
11969
|
const adapterSlugs = Object.keys(manifest.propAdapter);
|
|
11237
11970
|
const unmapped = recordedSlugs.filter((s) => !adapterSlugs.includes(s));
|
|
11238
11971
|
const adapterOnly = adapterSlugs.filter((s) => !recordedSlugs.includes(s));
|
|
11239
|
-
const registry = Object.values(TASKS).find((t) =>
|
|
11972
|
+
const registry = Object.values(TASKS).find((t) => path36.resolve(t.set) === path36.resolve(setDir));
|
|
11240
11973
|
const authored = (() => {
|
|
11241
11974
|
if (registry !== void 0) return void 0;
|
|
11242
11975
|
try {
|
|
@@ -11275,19 +12008,19 @@ function taskFromManifest(opts, manifest, setDir) {
|
|
|
11275
12008
|
}
|
|
11276
12009
|
async function runVerify(opts) {
|
|
11277
12010
|
const callerCwd = process.env["INIT_CWD"] ?? process.cwd();
|
|
11278
|
-
const setOverride = opts.set !== void 0 ?
|
|
11279
|
-
opts = { ...opts, bundleDir:
|
|
11280
|
-
if (!
|
|
12011
|
+
const setOverride = opts.set !== void 0 ? path36.resolve(callerCwd, opts.set) : void 0;
|
|
12012
|
+
opts = { ...opts, bundleDir: path36.resolve(callerCwd, opts.bundleDir), ...setOverride !== void 0 ? { set: setOverride } : {} };
|
|
12013
|
+
if (!existsSync29(opts.bundleDir)) {
|
|
11281
12014
|
fail(opts, ExitCode.InputValidation, {
|
|
11282
12015
|
error: `bundle directory not found: ${opts.bundleDir}`,
|
|
11283
12016
|
code: "bundle-missing",
|
|
11284
12017
|
remediation: "Point at a generated bundle directory (containing component.json, the entry module, and styles.css)."
|
|
11285
12018
|
});
|
|
11286
12019
|
}
|
|
11287
|
-
const manifestPath2 =
|
|
12020
|
+
const manifestPath2 = path36.join(opts.bundleDir, "component.json");
|
|
11288
12021
|
let manifest;
|
|
11289
|
-
if (
|
|
11290
|
-
const { manifest: parsed, issues } = readBundleManifest(
|
|
12022
|
+
if (existsSync29(manifestPath2)) {
|
|
12023
|
+
const { manifest: parsed, issues } = readBundleManifest(readFileSync25(manifestPath2, "utf8"));
|
|
11291
12024
|
if (issues.length > 0) {
|
|
11292
12025
|
fail(opts, ExitCode.InputValidation, {
|
|
11293
12026
|
error: `bundle manifest rejected at ingest: ${issues.map((i) => i.message).join("; ")}`,
|
|
@@ -11315,21 +12048,21 @@ async function runVerify(opts) {
|
|
|
11315
12048
|
task = registry;
|
|
11316
12049
|
} else if (manifest !== void 0) {
|
|
11317
12050
|
const resolveSetDir = (p) => {
|
|
11318
|
-
if (
|
|
11319
|
-
const fromRepo =
|
|
11320
|
-
if (
|
|
11321
|
-
return
|
|
12051
|
+
if (path36.isAbsolute(p)) return p;
|
|
12052
|
+
const fromRepo = path36.resolve(REPO_ROOT, p);
|
|
12053
|
+
if (existsSync29(fromRepo)) return fromRepo;
|
|
12054
|
+
return path36.resolve(callerCwd, p);
|
|
11322
12055
|
};
|
|
11323
12056
|
const setDir = opts.set ?? resolveSetDir(manifest.provenance.recordingSet.path);
|
|
11324
|
-
if (!
|
|
12057
|
+
if (!existsSync29(path36.join(setDir, "recording-set.json")) && !Object.values(TASKS).some((t) => path36.resolve(t.set) === path36.resolve(setDir))) {
|
|
11325
12058
|
fail(opts, ExitCode.RecordingIncomplete, {
|
|
11326
12059
|
error: `recording set not found or unmanifested: ${setDir}`,
|
|
11327
12060
|
code: "recording-set-missing",
|
|
11328
12061
|
remediation: "Pass --set <dir> pointing at the recording set this bundle was generated from."
|
|
11329
12062
|
});
|
|
11330
12063
|
}
|
|
11331
|
-
const registry = Object.values(TASKS).find((t) =>
|
|
11332
|
-
if (registry !== void 0 && !
|
|
12064
|
+
const registry = Object.values(TASKS).find((t) => path36.resolve(t.set) === path36.resolve(setDir));
|
|
12065
|
+
if (registry !== void 0 && !existsSync29(path36.join(setDir, "recording-set.json"))) {
|
|
11333
12066
|
const recordedSlugs = registry.configs.map((c) => c.rep);
|
|
11334
12067
|
const adapterSlugs = Object.keys(manifest.propAdapter);
|
|
11335
12068
|
unmapped = recordedSlugs.filter((s) => !adapterSlugs.includes(s));
|
|
@@ -11355,9 +12088,9 @@ async function runVerify(opts) {
|
|
|
11355
12088
|
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`);
|
|
11356
12089
|
}
|
|
11357
12090
|
for (const name of [manifest.entry, "styles.css", "tokens.css"]) {
|
|
11358
|
-
const p =
|
|
11359
|
-
if (!
|
|
11360
|
-
const issues = checkBundleSourceFile(name, new Uint8Array(
|
|
12091
|
+
const p = path36.join(opts.bundleDir, name);
|
|
12092
|
+
if (!existsSync29(p)) continue;
|
|
12093
|
+
const issues = checkBundleSourceFile(name, new Uint8Array(readFileSync25(p)));
|
|
11361
12094
|
if (issues.length > 0) {
|
|
11362
12095
|
fail(opts, ExitCode.InputValidation, {
|
|
11363
12096
|
error: `bundle source rejected at ingest: ${issues.map((i) => i.message).join("; ")}`,
|
|
@@ -11395,7 +12128,7 @@ async function runVerify(opts) {
|
|
|
11395
12128
|
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)`);
|
|
11396
12129
|
}
|
|
11397
12130
|
const missing = task.configs.filter(
|
|
11398
|
-
(c) => !
|
|
12131
|
+
(c) => !existsSync29(path36.join(task.set, c.rep, "get_screenshot.json")) || !existsSync29(path36.join(task.set, c.rep, "get_metadata.json"))
|
|
11399
12132
|
);
|
|
11400
12133
|
if (missing.length > 0) {
|
|
11401
12134
|
fail(opts, ExitCode.RecordingIncomplete, {
|
|
@@ -11405,16 +12138,26 @@ async function runVerify(opts) {
|
|
|
11405
12138
|
});
|
|
11406
12139
|
}
|
|
11407
12140
|
const bar = BARS2[opts.bar];
|
|
11408
|
-
const evidenceDir =
|
|
12141
|
+
const evidenceDir = path36.join(opts.bundleDir, "verify-evidence");
|
|
11409
12142
|
const scores = await scoreBundleForTask(task, opts.bundleDir, bar, { evidenceDir, onProgress: emitProgress });
|
|
11410
12143
|
const quality = await checkBundleQuality(opts.bundleDir, task.entry, { dir: task.set, reps: task.configs.map((c) => c.rep) });
|
|
11411
|
-
const bundleCss = ["tokens.css", "styles.css"].map((f) =>
|
|
12144
|
+
const bundleCss = ["tokens.css", "styles.css"].map((f) => path36.join(opts.bundleDir, f)).filter((f) => existsSync29(f)).map((f) => readFileSync25(f, "utf8")).join("\n");
|
|
11412
12145
|
const occlusion = await checkSiblingOcclusion(task, opts.bundleDir, bundleCss);
|
|
11413
12146
|
const parity = await checkHoverParity(task, opts.bundleDir, authorityConfigs ?? task.configs);
|
|
11414
12147
|
const behaviors = [...await checkBehaviors(task, opts.bundleDir), ...parity];
|
|
11415
12148
|
const structural = roles !== void 0 ? await checkStructuralComposition(task, opts.bundleDir, roles) : [];
|
|
11416
12149
|
const regionsOut = roles !== void 0 ? interiorRegions(task.set, roles) : void 0;
|
|
11417
12150
|
const crops = regionsOut !== void 0 && "regions" in regionsOut ? await checkCropComposition(task, opts.bundleDir, regionsOut.regions) : void 0;
|
|
12151
|
+
const crossComposition = confirmedCompositionStatus(task.set);
|
|
12152
|
+
if (crossComposition.malformed !== void 0) {
|
|
12153
|
+
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.`);
|
|
12154
|
+
}
|
|
12155
|
+
const verifyCallerCwd = process.env["INIT_CWD"] ?? process.cwd();
|
|
12156
|
+
const verifyPins = crossComposition.rows.length > 0 ? composedPins(task.set, [opts.library !== void 0 ? path36.resolve(verifyCallerCwd, opts.library) : verifyCallerCwd]) : { pins: [], issues: [] };
|
|
12157
|
+
const verifyComposedChecks = composedChecks(opts.bundleDir, task.entry, verifyPins.pins);
|
|
12158
|
+
const composedPairs = new Set(
|
|
12159
|
+
verifyPins.pins.filter((pin) => verifyComposedChecks.filter((c) => c.id.startsWith(`composition:${pin.pairKey}:`)).every((c) => c.pass)).map((pin) => pin.pairKey)
|
|
12160
|
+
);
|
|
11418
12161
|
const demotions = /* @__PURE__ */ new Map();
|
|
11419
12162
|
const demote = (rep, reason) => {
|
|
11420
12163
|
demotions.set(rep, [...demotions.get(rep) ?? [], reason]);
|
|
@@ -11424,13 +12167,20 @@ async function runVerify(opts) {
|
|
|
11424
12167
|
if (crops !== void 0) {
|
|
11425
12168
|
for (const c of crops) if (!c.pass) demote(c.id.split(":")[1] ?? "", "composition crop failed (A1.4)");
|
|
11426
12169
|
}
|
|
12170
|
+
const scoredReps = /* @__PURE__ */ new Set([...scores.map((s) => s.rep), ...unmapped]);
|
|
12171
|
+
const crossOutcome = crossCompositionDemotions(crossComposition.rows, scoredReps, COMPOSE_ON_GENERATE_ARMED, composedPairs);
|
|
12172
|
+
for (const d of crossOutcome.demote) demote(d.rep, d.reason);
|
|
11427
12173
|
const folded = scores.map((s) => foldConfigStatus(s, demotions.get(s.rep), substitutedFamilies));
|
|
11428
12174
|
const absentInkDemoted = scores.filter((_, i) => folded[i].absentInkDemoted).map((s) => s.rep);
|
|
11429
12175
|
const statuses = [
|
|
11430
12176
|
...folded.map((f) => f.row),
|
|
11431
12177
|
// ADR-010 §2 anti-gaming: recorded configs the adapter does not map
|
|
11432
12178
|
// are FAILs, never silently absent.
|
|
11433
|
-
...unmapped.map((rep) => ({ rep, similarity: 0, inkRecall: 0, pass: false, status: "fail", error: "not mapped by the bundle's prop adapter" }))
|
|
12179
|
+
...unmapped.map((rep) => ({ rep, similarity: 0, inkRecall: 0, pass: false, status: "fail", error: "not mapped by the bundle's prop adapter" })),
|
|
12180
|
+
// ADR-013 2c-i: a confirmed-composition claim naming a rep this
|
|
12181
|
+
// set does not score is a loud synthetic FAILURE, never a printed
|
|
12182
|
+
// contradiction over an unchanged verdict.
|
|
12183
|
+
...crossOutcome.orphanFailRows.map((o) => ({ rep: o.rep, similarity: 0, inkRecall: 0, pass: false, status: "fail", error: o.error }))
|
|
11434
12184
|
];
|
|
11435
12185
|
const behaviorFailures = behaviors.filter((b) => !b.pass);
|
|
11436
12186
|
const structuralFailures = structural.filter((s) => !s.pass);
|
|
@@ -11497,6 +12247,19 @@ async function runVerify(opts) {
|
|
|
11497
12247
|
// occlusionReport): the agent channel had no counterpart to the
|
|
11498
12248
|
// human line's "occlusion not applicable (no overlay declared)".
|
|
11499
12249
|
occlusionCheck: occlusionReport(occlusion),
|
|
12250
|
+
// ADR-013 2c: the confirmed-composition backstop rows (re-derived
|
|
12251
|
+
// from recordings, never trusted from the manifest), the composed
|
|
12252
|
+
// checks (pinned bytes verbatim + import), and the pin issues.
|
|
12253
|
+
...crossComposition.rows.length > 0 || crossComposition.malformed !== void 0 ? {
|
|
12254
|
+
crossComposition: {
|
|
12255
|
+
rows: crossComposition.rows,
|
|
12256
|
+
checks: verifyComposedChecks,
|
|
12257
|
+
composedPairs: [...composedPairs],
|
|
12258
|
+
pinIssues: verifyPins.issues,
|
|
12259
|
+
...crossComposition.malformed !== void 0 ? { malformed: crossComposition.malformed } : {},
|
|
12260
|
+
note: "COMPOSED means the pinned partner module is present verbatim with its import declared; while compose-on-generate is disarmed (ADR-013 \xA73, re-arms with rendered-mount stamping) an uncomposed supported pair is DISCLOSED, never demoted \u2014 and whole-frame pixels remain the pixel evidence for composed regions"
|
|
12261
|
+
}
|
|
12262
|
+
} : {},
|
|
11500
12263
|
configs: statuses,
|
|
11501
12264
|
behaviors,
|
|
11502
12265
|
evidence: { dir: evidenceDir, files: "per config: <rep>-render.png, <rep>-ref.png, <rep>-diff.png" },
|
|
@@ -11543,7 +12306,7 @@ async function runVerify(opts) {
|
|
|
11543
12306
|
process.stdout.write(
|
|
11544
12307
|
`
|
|
11545
12308
|
${certified}/${statuses.length} certified \xB7 ${report.coverage.pass}/${statuses.length} \u2265 pass bar \xB7 behaviors ${behaviors.length - behaviorFailures.length}/${behaviors.length}${checkSummarySegments(
|
|
11546
|
-
{ availability, structural, crops, occlusion, operability }
|
|
12309
|
+
{ availability, structural, crops, occlusion, operability, crossComposition: { rows: crossComposition.rows, ...crossComposition.malformed !== void 0 ? { malformed: crossComposition.malformed } : {}, composed: composedPairs.size } }
|
|
11547
12310
|
)}
|
|
11548
12311
|
`
|
|
11549
12312
|
);
|
|
@@ -11603,9 +12366,19 @@ ${certified}/${statuses.length} certified \xB7 ${report.coverage.pass}/${statuse
|
|
|
11603
12366
|
);
|
|
11604
12367
|
}
|
|
11605
12368
|
}
|
|
12369
|
+
for (const line of crossCompositionLines({
|
|
12370
|
+
rows: crossComposition.rows,
|
|
12371
|
+
composedPairs,
|
|
12372
|
+
armed: COMPOSE_ON_GENERATE_ARMED,
|
|
12373
|
+
pinIssues: verifyPins.issues,
|
|
12374
|
+
...crossComposition.malformed !== void 0 ? { malformed: crossComposition.malformed } : {}
|
|
12375
|
+
})) {
|
|
12376
|
+
process.stdout.write(`${line}
|
|
12377
|
+
`);
|
|
12378
|
+
}
|
|
11606
12379
|
process.stdout.write(`environment: chrome=${report.environment.chromeVersion ?? report.environment.chrome} fonts=${report.environment.fontsManifestSha256 ?? "UNRESOLVED"}
|
|
11607
12380
|
`);
|
|
11608
|
-
const shippedFonts = requiredFontsManifest(cssFontFamilies(["styles.css", "tokens.css"].map((f) =>
|
|
12381
|
+
const shippedFonts = requiredFontsManifest(cssFontFamilies(["styles.css", "tokens.css"].map((f) => path36.join(opts.bundleDir, f)).filter((f) => existsSync29(f)).map((f) => readFileSync25(f, "utf8")).join("\n")));
|
|
11609
12382
|
if (shippedFonts.length > 0 && substitutedFamilies.length === 0) {
|
|
11610
12383
|
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)
|
|
11611
12384
|
`);
|
|
@@ -11658,7 +12431,7 @@ ${certified}/${statuses.length} certified \xB7 ${report.coverage.pass}/${statuse
|
|
|
11658
12431
|
process.exitCode = ExitCode.VerificationFailed;
|
|
11659
12432
|
}
|
|
11660
12433
|
}
|
|
11661
|
-
var BARS2, NO_INTERACTIVE_POSES, NO_ROLE_MANIFEST, ROLES_NOT_RESOLVED, UNSTAMPED_ROLES, NO_OVERLAY_DECLARED;
|
|
12434
|
+
var BARS2, NO_INTERACTIVE_POSES, NO_ROLE_MANIFEST, ROLES_NOT_RESOLVED, UNSTAMPED_ROLES, COMPOSE_ON_GENERATE_ARMED, NO_OVERLAY_DECLARED;
|
|
11662
12435
|
var init_verify = __esm({
|
|
11663
12436
|
"packages/cli/src/commands/verify.ts"() {
|
|
11664
12437
|
"use strict";
|
|
@@ -11685,6 +12458,7 @@ var init_verify = __esm({
|
|
|
11685
12458
|
unavailable: "this run never resolved a role manifest \u2014 the legacy --task adapter and pre-manifest reference sets skip the set's roles block entirely, so composition (structural + crop) was NEVER CHECKED and the set was never asked what it declares; nothing here says the main renders the SHIPPED part modules rather than a pixel-identical re-implementation"
|
|
11686
12459
|
});
|
|
11687
12460
|
UNSTAMPED_ROLES = "unstamped";
|
|
12461
|
+
COMPOSE_ON_GENERATE_ARMED = false;
|
|
11688
12462
|
NO_OVERLAY_DECLARED = "no overlay declared";
|
|
11689
12463
|
}
|
|
11690
12464
|
});
|
|
@@ -11695,18 +12469,18 @@ __export(engine_exports, {
|
|
|
11695
12469
|
runEngineBrief: () => runEngineBrief,
|
|
11696
12470
|
runEngineScore: () => runEngineScore
|
|
11697
12471
|
});
|
|
11698
|
-
import { appendFileSync, existsSync as
|
|
11699
|
-
import
|
|
12472
|
+
import { appendFileSync, existsSync as existsSync30, mkdirSync as mkdirSync8, readFileSync as readFileSync26, writeFileSync as writeFileSync12 } from "node:fs";
|
|
12473
|
+
import path37 from "node:path";
|
|
11700
12474
|
function resolveEngineTask(opts, callerCwd) {
|
|
11701
|
-
const asPath =
|
|
11702
|
-
const isSet =
|
|
12475
|
+
const asPath = path37.resolve(callerCwd, opts.taskOrSet);
|
|
12476
|
+
const isSet = existsSync30(path37.join(asPath, "recording-set.json"));
|
|
11703
12477
|
const registry = TASKS[opts.taskOrSet];
|
|
11704
12478
|
if (registry !== void 0 && !isSet) return { task: registry, name: opts.taskOrSet, ref: opts.taskOrSet, disclosures: [], interactionEvidence: void 0 };
|
|
11705
12479
|
if (isSet) {
|
|
11706
12480
|
try {
|
|
11707
12481
|
const authored = authorTaskFromSet(asPath);
|
|
11708
12482
|
for (const d of authored.disclosures) warn(opts, d);
|
|
11709
|
-
return { task: authored.task, name:
|
|
12483
|
+
return { task: authored.task, name: path37.basename(asPath), ref: asPath, disclosures: authored.disclosures, interactionEvidence: authored.api.interactionEvidence, apiPin: { props: authored.api.apiPin.props, forcedStates: authored.api.apiPin.forcedStates } };
|
|
11710
12484
|
} catch (err) {
|
|
11711
12485
|
fail(opts, ExitCode.InputValidation, {
|
|
11712
12486
|
error: `cannot author a task from ${asPath}: ${err instanceof Error ? err.message.split("\n")[0] : String(err)}`,
|
|
@@ -11733,9 +12507,9 @@ ${disclosures.map((d) => `- ${d}`).join("\n")}` : "";
|
|
|
11733
12507
|
const brief = buildBrief(task.systemApi, bar, { colorScheme: recordingIsDark(task) ? "dark" : "light" }) + disclosureBlock;
|
|
11734
12508
|
const segments = buildSegments(task, "files");
|
|
11735
12509
|
let notRecorded;
|
|
11736
|
-
const manifestPath2 =
|
|
11737
|
-
if (
|
|
11738
|
-
notRecorded = JSON.parse(
|
|
12510
|
+
const manifestPath2 = path37.join(task.set, "recording-set.json");
|
|
12511
|
+
if (existsSync30(manifestPath2)) {
|
|
12512
|
+
notRecorded = JSON.parse(readFileSync26(manifestPath2, "utf8")).notRecorded;
|
|
11739
12513
|
}
|
|
11740
12514
|
const unverified = notRecorded !== void 0 && notRecorded !== "" ? `
|
|
11741
12515
|
|
|
@@ -11743,7 +12517,7 @@ ${disclosures.map((d) => `- ${d}`).join("\n")}` : "";
|
|
|
11743
12517
|
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.
|
|
11744
12518
|
${notRecorded}` : "";
|
|
11745
12519
|
let fontProvisioning;
|
|
11746
|
-
if (
|
|
12520
|
+
if (existsSync30(manifestPath2)) {
|
|
11747
12521
|
const missingFams = unprovisionedFamilies(task.set);
|
|
11748
12522
|
const unprovided = unprovisionedFaces(task.set);
|
|
11749
12523
|
const weightOnly = missingFams.length === 0;
|
|
@@ -11765,13 +12539,31 @@ ${notRecorded}` : "";
|
|
|
11765
12539
|
};
|
|
11766
12540
|
}
|
|
11767
12541
|
}
|
|
11768
|
-
const
|
|
12542
|
+
const pinsResult = composedPins(task.set, [opts.library !== void 0 ? path37.resolve(callerCwd, opts.library) : callerCwd]);
|
|
12543
|
+
const jsxProp = ([k, v]) => typeof v === "string" ? `${k}=${JSON.stringify(v)}` : `${k}={${JSON.stringify(v)}}`;
|
|
12544
|
+
const composedBlock = pinsResult.pins.length === 0 && pinsResult.issues.length === 0 ? "" : (pinsResult.pins.length > 0 ? `
|
|
12545
|
+
|
|
12546
|
+
=== COMPOSED REGIONS (ADR-013 \u2014 each pair below was CONFIRMED BY A HUMAN; the pinned modules are PRESCRIBED API) ===
|
|
12547
|
+
CANON AMENDMENT for this task: "no imports beyond react/react-dom" is amended to ADDITIONALLY allow importing exactly the pinned modules below \u2014 nothing else. Write every pinned file VERBATIM (byte-for-byte) at the given candidate path; any edit fails the composition checks. Render the pinned entry component for each listed region with exactly the listed props; do not re-implement those pixels. The pinned styles/tokens CSS is injected at scoring time alongside yours \u2014 do not copy its rules into your own stylesheets.
|
|
12548
|
+
` + pinsResult.pins.map(
|
|
12549
|
+
(pin) => `
|
|
12550
|
+
PIN ${pin.partnerName} [pair ${pin.pairKey}] \u2014 import from "./${composedModuleDir(pin.partnerName)}/${pin.entryComponent}"
|
|
12551
|
+
` + pin.instances.map((i) => ` region ${i.hostRep}/${i.instanceId}: <${pin.entryComponent} ${Object.entries(i.props).map(jsxProp).join(" ")} /> (pose ${i.partnerRep})`).join("\n") + pin.moduleFiles.map((f) => `
|
|
12552
|
+
--- write VERBATIM to ${composedModuleDir(pin.partnerName)}/${f.name} (sha256 ${f.sha256}) ---
|
|
12553
|
+
${f.content}`).join("")
|
|
12554
|
+
).join("\n") : `
|
|
12555
|
+
|
|
12556
|
+
=== COMPOSED REGIONS (ADR-013 \u2014 confirmed pairs exist, but NONE could be pinned) ===
|
|
12557
|
+
`) + (pinsResult.issues.length > 0 ? `
|
|
12558
|
+
NOT PINNED (named, never silent \u2014 these confirmed pairs could not be pinned): implement those regions inline and state it in your report. Verify will DISCLOSE each such pair as confirmed-but-not-composed; where the cause below is fixable (e.g. the partner bundle is missing), fixing it and re-running brief is the better path:
|
|
12559
|
+
${pinsResult.issues.map((i) => `- ${i}`).join("\n")}` : "");
|
|
12560
|
+
const payload = `${brief}${unverified}${composedBlock}
|
|
11769
12561
|
|
|
11770
12562
|
=== TASK PAYLOAD (recorded truth, verbatim) ===
|
|
11771
12563
|
${segments}`;
|
|
11772
|
-
const payloadFile =
|
|
11773
|
-
const candidateDirSuggestion =
|
|
11774
|
-
mkdirSync8(
|
|
12564
|
+
const payloadFile = path37.resolve(callerCwd, opts.out ?? `tendril-out/${name}-brief.md`);
|
|
12565
|
+
const candidateDirSuggestion = path37.resolve(callerCwd, `tendril-out/${name}-candidate`);
|
|
12566
|
+
mkdirSync8(path37.dirname(payloadFile), { recursive: true });
|
|
11775
12567
|
writeFileSync12(payloadFile, payload);
|
|
11776
12568
|
emitData(
|
|
11777
12569
|
opts,
|
|
@@ -11784,6 +12576,10 @@ ${segments}`;
|
|
|
11784
12576
|
...opts.model !== void 0 ? { declaredModel: opts.model } : {},
|
|
11785
12577
|
...notRecorded !== void 0 && notRecorded !== "" ? { notRecorded } : {},
|
|
11786
12578
|
...fontProvisioning !== void 0 ? { fontProvisioning } : {},
|
|
12579
|
+
// ADR-013: composition is IN PLAY for this task — an orchestrator
|
|
12580
|
+
// reading only this JSON must be able to see it (the payload file
|
|
12581
|
+
// carries the pins themselves).
|
|
12582
|
+
...pinsResult.pins.length > 0 || pinsResult.issues.length > 0 ? { composition: { pinnedPairs: pinsResult.pins.map((p) => p.pairKey), pinIssues: pinsResult.issues } } : {},
|
|
11787
12583
|
modelSelection: {
|
|
11788
12584
|
instruction: "The model declaration is MECHANICAL: `engine score` refuses to run without --model. ONLY ask when the answer can take effect \u2014 i.e. you can delegate implementation to an agent running the chosen model; if you cannot delegate in this session, skip the question, build as yourself, and declare your own model honestly (a question whose answer changes nothing wastes the user's trust \u2014 measured, second Windows run). When you do ask: BEFORE reading the payload, using the template below verbatim where your host renders option dialogs. The audience is non-technical: plain cost/quality language, no model knowledge assumed. In a non-interactive session pick the balanced tier, state the reason in your report, and declare it \u2014 a silent default is not possible.",
|
|
11789
12585
|
questionTemplate: {
|
|
@@ -11806,8 +12602,11 @@ ${segments}`;
|
|
|
11806
12602
|
// arguments, and a placeholder is the other half of the same
|
|
11807
12603
|
// defect — a command the reader has to finish is one they can
|
|
11808
12604
|
// finish wrongly.
|
|
12605
|
+
// --library rides along when brief ran with one: the score
|
|
12606
|
+
// command must search the same bundle roots the pins came
|
|
12607
|
+
// from, or the oracle and the brief describe different worlds.
|
|
11809
12608
|
`Run \`${tendrilCommand(
|
|
11810
|
-
`engine score ${quoteArg(ref)} ${quoteArg(candidateDirSuggestion)} --bar ${opts.bar} --model ${opts.model ?? "<declared-model>"} --json`
|
|
12609
|
+
`engine score ${quoteArg(ref)} ${quoteArg(candidateDirSuggestion)} --bar ${opts.bar} --model ${opts.model ?? "<declared-model>"}${opts.library !== void 0 ? ` --library ${quoteArg(path37.resolve(callerCwd, opts.library))}` : ""} --json`
|
|
11811
12610
|
)}\` \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.`,
|
|
11812
12611
|
"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).",
|
|
11813
12612
|
"Never edit recording sets; never claim scores yourself \u2014 only the score command's output counts."
|
|
@@ -11822,8 +12621,8 @@ ${segments}`;
|
|
|
11822
12621
|
);
|
|
11823
12622
|
}
|
|
11824
12623
|
function appendScoreHistory(candidateDir, entry) {
|
|
11825
|
-
const file =
|
|
11826
|
-
const starts =
|
|
12624
|
+
const file = path37.join(candidateDir, "score-history.jsonl");
|
|
12625
|
+
const starts = existsSync30(file) ? readFileSync26(file, "utf8").split("\n").filter((l) => l.includes('"event":"round-start"')).length : 0;
|
|
11827
12626
|
const round = entry.event === "round-start" ? starts + 1 : Math.max(1, starts);
|
|
11828
12627
|
appendFileSync(file, `${JSON.stringify({ at: (/* @__PURE__ */ new Date()).toISOString(), round, ...entry })}
|
|
11829
12628
|
`);
|
|
@@ -11831,9 +12630,9 @@ function appendScoreHistory(candidateDir, entry) {
|
|
|
11831
12630
|
async function runEngineScore(opts) {
|
|
11832
12631
|
requireEntitlement(opts);
|
|
11833
12632
|
const callerCwd = process.env["INIT_CWD"] ?? process.cwd();
|
|
11834
|
-
const candidateDir =
|
|
12633
|
+
const candidateDir = path37.resolve(callerCwd, opts.candidateDir);
|
|
11835
12634
|
const { task, name, apiPin, interactionEvidence } = resolveEngineTask(opts, callerCwd);
|
|
11836
|
-
if (!
|
|
12635
|
+
if (!existsSync30(candidateDir)) {
|
|
11837
12636
|
fail(opts, ExitCode.InputValidation, {
|
|
11838
12637
|
error: `candidate directory not found: ${candidateDir}`,
|
|
11839
12638
|
code: "candidate-missing",
|
|
@@ -11858,10 +12657,10 @@ async function runEngineScore(opts) {
|
|
|
11858
12657
|
for (const g of missingWeights(task.set)) {
|
|
11859
12658
|
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)`);
|
|
11860
12659
|
}
|
|
11861
|
-
if (opts.rebind !== true &&
|
|
12660
|
+
if (opts.rebind !== true && existsSync30(path37.join(candidateDir, "component.json"))) {
|
|
11862
12661
|
const prior = (() => {
|
|
11863
12662
|
try {
|
|
11864
|
-
const read = readBundleManifest(
|
|
12663
|
+
const read = readBundleManifest(readFileSync26(path37.join(candidateDir, "component.json"), "utf8"));
|
|
11865
12664
|
return read.manifest === void 0 ? { unreadable: true } : { hash: read.manifest.provenance.recordingSet.hash, path: read.manifest.provenance.recordingSet.path };
|
|
11866
12665
|
} catch {
|
|
11867
12666
|
return { unreadable: true };
|
|
@@ -11883,10 +12682,12 @@ async function runEngineScore(opts) {
|
|
|
11883
12682
|
}
|
|
11884
12683
|
}
|
|
11885
12684
|
const bar = BARS3[opts.bar];
|
|
11886
|
-
const evidenceDir =
|
|
12685
|
+
const evidenceDir = path37.join(candidateDir, "verify-evidence");
|
|
11887
12686
|
const scores = await scoreBundleForTask(task, candidateDir, bar, { evidenceDir, onProgress: emitProgress });
|
|
11888
12687
|
const parity = await checkHoverParity(task, candidateDir, task.configs);
|
|
11889
|
-
const
|
|
12688
|
+
const scorePins = composedPins(task.set, [opts.library !== void 0 ? path37.resolve(callerCwd, opts.library) : callerCwd]);
|
|
12689
|
+
const composition = composedChecks(candidateDir, task.entry, scorePins.pins);
|
|
12690
|
+
const behaviors = [...await checkBehaviors(task, candidateDir), ...parity, ...composition];
|
|
11890
12691
|
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)";
|
|
11891
12692
|
const obj = objective(scores, behaviors);
|
|
11892
12693
|
const total = scores.length + behaviors.length;
|
|
@@ -11911,12 +12712,16 @@ MISSING FEATURES (absent-ink clusters \u2014 recorded ink your render leaves now
|
|
|
11911
12712
|
${absentFindings.join("\n")}` : "";
|
|
11912
12713
|
const certificationFeedback = `${absentBlock}
|
|
11913
12714
|
|
|
11914
|
-
CERTIFICATION: ${certifiedReps.length}/${scores.length} configs at the certification bar (sim \u2265${certBar.sim} AND ink \u2265${certBar.ink}, exact values, after parity demotion \u2014
|
|
12715
|
+
CERTIFICATION: ${certifiedReps.length}/${scores.length} configs at the certification bar (sim \u2265${certBar.sim} AND ink \u2265${certBar.ink}, exact values, after parity demotion \u2014 verify's A1.4 structural/crop checks can demote further).${certifiedReps.length < scores.length ? ` Below cert: ${scores.filter((sc) => !certifiedSet.has(sc.rep)).map((sc) => sc.rep).join(", ")}.` : ""}
|
|
11915
12716
|
METRIC DEADBAND (read before iterating on near-misses): the scored similarity/ink deliberately tolerate \xB11px edge shift and antialiased-edge differences \u2014 cross-rasterizer noise absorption. A change entirely inside that band moves these numbers by EXACTLY ZERO (working as designed, not a stuck scorer). The per-config \`exact\` fields in the JSON are tolerance-free and move first: compare exact across rounds to confirm a small fix landed, and stop iterating when only exact moves \u2014 the bar reads the tolerant numbers.`;
|
|
11916
12717
|
const stampNotice = `
|
|
11917
12718
|
|
|
11918
12719
|
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.`;
|
|
11919
|
-
const
|
|
12720
|
+
const compositionFeedback = scorePins.issues.length > 0 ? `
|
|
12721
|
+
|
|
12722
|
+
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:
|
|
12723
|
+
${scorePins.issues.map((i) => `- ${i}`).join("\n")}` : "";
|
|
12724
|
+
const feedback = buildFeedback(scores, behaviors, bar, "files") + certificationFeedback + compositionFeedback + stampNotice + qualityFeedback;
|
|
11920
12725
|
const emitted = emitBundleV1({
|
|
11921
12726
|
bundleDir: candidateDir,
|
|
11922
12727
|
task,
|
|
@@ -11978,6 +12783,10 @@ PROVENANCE STAMP: this scoring call itself (the Tendril CLI) just wrote/refreshe
|
|
|
11978
12783
|
// fixing verify's channel and not this one would have left the
|
|
11979
12784
|
// reader who actually acts on it exactly where they were.
|
|
11980
12785
|
coverage: { ...coverage, operabilityCheck: operability },
|
|
12786
|
+
// Same naming as verify's report block: the checks themselves ride
|
|
12787
|
+
// `behaviors` (composition: ids); this carries what CANNOT ride
|
|
12788
|
+
// there — which pairs pinned and which could not, with causes.
|
|
12789
|
+
...scorePins.pins.length > 0 || scorePins.issues.length > 0 ? { crossComposition: { pinnedPairs: scorePins.pins.map((p) => p.pairKey), pinIssues: scorePins.issues } } : {},
|
|
11981
12790
|
parityCoverage,
|
|
11982
12791
|
evidenceDir,
|
|
11983
12792
|
bundleManifest: emitted.written[0],
|
|
@@ -11989,12 +12798,14 @@ PROVENANCE STAMP: this scoring call itself (the Tendril CLI) just wrote/refreshe
|
|
|
11989
12798
|
// bundles verify then FAILED on the interaction-evidence gate —
|
|
11990
12799
|
// the oracle must say what verify will say, including this.
|
|
11991
12800
|
...evidenceUnverified ? { interactionEvidenceUnverified: true, verifyWillFail: "interaction-evidence \u2014 the recording proves interactive poses none of the authored behaviors cover; not fixable from component code; REPORT it, do not iterate on it" } : {},
|
|
11992
|
-
certification: { certified: certifiedReps.length, total: scores.length, bar: certBar, note: "tierOf on exact values + parity and absent-ink demotion; verify's
|
|
12801
|
+
certification: { certified: certifiedReps.length, total: scores.length, bar: certBar, note: "tierOf on exact values + parity and absent-ink demotion; verify's A1.4 structural/crop checks can demote further" }
|
|
11993
12802
|
},
|
|
11994
12803
|
() => {
|
|
11995
12804
|
for (const s of scores) process.stdout.write(`${s.pass ? certifiedSet.has(s.rep) ? "CERT" : "PASS" : "FAIL"} ${s.rep}: sim=${s.similarity} ink=${s.inkRecall}${s.error !== void 0 ? ` [${s.error}]` : ""}
|
|
11996
12805
|
`);
|
|
11997
12806
|
for (const b of behaviors) process.stdout.write(`${b.pass ? "PASS" : "FAIL"} ${b.id}${b.detail !== void 0 ? ` [${b.detail}]` : ""}
|
|
12807
|
+
`);
|
|
12808
|
+
for (const i of scorePins.issues) process.stdout.write(`PIN ISSUE ${i}
|
|
11998
12809
|
`);
|
|
11999
12810
|
process.stdout.write(`
|
|
12000
12811
|
${obj[0]}/${total} \xB7 certified ${certifiedReps.length}/${total} \xB7 floor=${obj[1].toFixed(3)} \xB7 mean=${obj[2].toFixed(3)} \xB7 evidence: ${evidenceDir}
|
|
@@ -12054,11 +12865,11 @@ var codeconnect_exports = {};
|
|
|
12054
12865
|
__export(codeconnect_exports, {
|
|
12055
12866
|
runCodeConnect: () => runCodeConnect
|
|
12056
12867
|
});
|
|
12057
|
-
import { existsSync as
|
|
12058
|
-
import
|
|
12868
|
+
import { existsSync as existsSync31, readFileSync as readFileSync27, writeFileSync as writeFileSync13 } from "node:fs";
|
|
12869
|
+
import path38 from "node:path";
|
|
12059
12870
|
function runCodeConnect(opts) {
|
|
12060
12871
|
const callerCwd = process.env["INIT_CWD"] ?? process.cwd();
|
|
12061
|
-
const bundleDir =
|
|
12872
|
+
const bundleDir = path38.resolve(callerCwd, opts.bundleDir);
|
|
12062
12873
|
let url;
|
|
12063
12874
|
try {
|
|
12064
12875
|
url = new URL(opts.figmaUrl);
|
|
@@ -12074,7 +12885,7 @@ function runCodeConnect(opts) {
|
|
|
12074
12885
|
}
|
|
12075
12886
|
let manifest;
|
|
12076
12887
|
try {
|
|
12077
|
-
const read = readBundleManifest(
|
|
12888
|
+
const read = readBundleManifest(readFileSync27(path38.join(bundleDir, "component.json"), "utf8"));
|
|
12078
12889
|
if (read.manifest === void 0) throw new Error(read.issues.map((i) => i.message).join("; ") || "no component.json");
|
|
12079
12890
|
manifest = read.manifest;
|
|
12080
12891
|
} catch (err) {
|
|
@@ -12084,8 +12895,8 @@ function runCodeConnect(opts) {
|
|
|
12084
12895
|
remediation: "Point at a bundle produced by the Tendril pipeline (it carries component.json), and verify it first."
|
|
12085
12896
|
});
|
|
12086
12897
|
}
|
|
12087
|
-
const setDir =
|
|
12088
|
-
if (!
|
|
12898
|
+
const setDir = path38.resolve(callerCwd, opts.set ?? manifest.provenance.recordingSet.path);
|
|
12899
|
+
if (!existsSync31(path38.join(setDir, "recording-set.json"))) {
|
|
12089
12900
|
fail(opts, ExitCode.InputValidation, {
|
|
12090
12901
|
error: `recording set not found at ${setDir}`,
|
|
12091
12902
|
code: "codeconnect-no-set",
|
|
@@ -12106,10 +12917,10 @@ function runCodeConnect(opts) {
|
|
|
12106
12917
|
const component = api.component;
|
|
12107
12918
|
const recManifest = loadManifest(setDir);
|
|
12108
12919
|
const poseNames = recManifest.latticeNames ?? recManifest.reps.map((r) => {
|
|
12109
|
-
const meta =
|
|
12110
|
-
if (!
|
|
12920
|
+
const meta = path38.join(setDir, r.slug, "get_metadata.json");
|
|
12921
|
+
if (!existsSync31(meta)) return void 0;
|
|
12111
12922
|
try {
|
|
12112
|
-
return /name="([^"]*)"/.exec(envelopeTextContent(JSON.parse(
|
|
12923
|
+
return /name="([^"]*)"/.exec(envelopeTextContent(JSON.parse(readFileSync27(meta, "utf8"))))?.[1];
|
|
12113
12924
|
} catch {
|
|
12114
12925
|
return void 0;
|
|
12115
12926
|
}
|
|
@@ -12167,7 +12978,7 @@ function runCodeConnect(opts) {
|
|
|
12167
12978
|
axisLines.push(`const ${varName} = instance.getEnum(${q(axis)}, { ${entries.join(", ")} })`);
|
|
12168
12979
|
fragmentVars.push(varName);
|
|
12169
12980
|
}
|
|
12170
|
-
const entryRel =
|
|
12981
|
+
const entryRel = path38.relative(callerCwd, path38.join(bundleDir, manifest.entry));
|
|
12171
12982
|
const trust = manifest.trustStatement.split("\n")[0] ?? "";
|
|
12172
12983
|
const lines = [
|
|
12173
12984
|
`// url=${opts.figmaUrl}`,
|
|
@@ -12188,7 +12999,7 @@ function runCodeConnect(opts) {
|
|
|
12188
12999
|
`}`,
|
|
12189
13000
|
``
|
|
12190
13001
|
].join("\n");
|
|
12191
|
-
const outFile =
|
|
13002
|
+
const outFile = path38.resolve(callerCwd, opts.out ?? path38.join(bundleDir, `${component}.figma.ts`));
|
|
12192
13003
|
writeFileSync13(outFile, lines);
|
|
12193
13004
|
emitData(
|
|
12194
13005
|
opts,
|
|
@@ -12227,18 +13038,18 @@ var init_codeconnect = __esm({
|
|
|
12227
13038
|
});
|
|
12228
13039
|
|
|
12229
13040
|
// packages/mcp/src/server.ts
|
|
12230
|
-
import { createHash as
|
|
12231
|
-
import { existsSync as
|
|
13041
|
+
import { createHash as createHash7 } from "node:crypto";
|
|
13042
|
+
import { existsSync as existsSync32, mkdtempSync as mkdtempSync3, readFileSync as readFileSync28, readdirSync as readdirSync12, writeFileSync as writeFileSync14 } from "node:fs";
|
|
12232
13043
|
import os7 from "node:os";
|
|
12233
|
-
import
|
|
13044
|
+
import path39 from "node:path";
|
|
12234
13045
|
import { fileURLToPath as fileURLToPath6 } from "node:url";
|
|
12235
|
-
import { z as
|
|
13046
|
+
import { z as z13 } from "zod";
|
|
12236
13047
|
function sourceHash() {
|
|
12237
|
-
const dir =
|
|
12238
|
-
const h =
|
|
12239
|
-
for (const f of
|
|
13048
|
+
const dir = path39.dirname(fileURLToPath6(import.meta.url));
|
|
13049
|
+
const h = createHash7("sha256");
|
|
13050
|
+
for (const f of readdirSync12(dir).filter((n) => n.endsWith(".ts")).sort()) {
|
|
12240
13051
|
h.update(f);
|
|
12241
|
-
h.update(
|
|
13052
|
+
h.update(readFileSync28(path39.join(dir, f)));
|
|
12242
13053
|
}
|
|
12243
13054
|
return h.digest("hex").slice(0, 16);
|
|
12244
13055
|
}
|
|
@@ -12246,27 +13057,27 @@ var REPO_ROOT3, CLI_BIN, BUNDLED_CLI, CLI_SPAWN, str, optStr, TOOLS, BOOT_HASH;
|
|
|
12246
13057
|
var init_server = __esm({
|
|
12247
13058
|
"packages/mcp/src/server.ts"() {
|
|
12248
13059
|
"use strict";
|
|
12249
|
-
REPO_ROOT3 =
|
|
12250
|
-
CLI_BIN =
|
|
12251
|
-
BUNDLED_CLI =
|
|
12252
|
-
CLI_SPAWN =
|
|
12253
|
-
str = (d) =>
|
|
12254
|
-
optStr = (d) =>
|
|
13060
|
+
REPO_ROOT3 = path39.resolve(path39.dirname(fileURLToPath6(import.meta.url)), "..", "..", "..");
|
|
13061
|
+
CLI_BIN = path39.join(REPO_ROOT3, "packages", "cli", "src", "bin.ts");
|
|
13062
|
+
BUNDLED_CLI = path39.join(path39.dirname(fileURLToPath6(import.meta.url)), "tendril.js");
|
|
13063
|
+
CLI_SPAWN = existsSync32(BUNDLED_CLI) ? { cmd: process.execPath, prefix: [BUNDLED_CLI] } : { cmd: "npx", prefix: ["tsx", CLI_BIN] };
|
|
13064
|
+
str = (d) => z13.string().describe(d);
|
|
13065
|
+
optStr = (d) => z13.string().optional().describe(d);
|
|
12255
13066
|
TOOLS = [
|
|
12256
13067
|
{
|
|
12257
13068
|
name: "tendril_record_plan",
|
|
12258
13069
|
description: "ENTRY POINT for implementing/building a React component from a Figma design or figma.com URL \u2014 start the Tendril pipeline here (after loading the tendril skill, if installed). Plans a recording session: computes the rep queue (anchor + one-factor + conflict crosses) from the verbatim get_metadata response (pass its blocks via metadataParts \u2014 no file to write) and persists the set manifest. Resumes if the set already exists. The output may carry USER QUESTIONS \u2014 defaultsToConfirm (which pose is the component's default) or a multiple-component-sets error (which set to record): render them to a present user and apply the answers via `defaults` / `componentSet`; non-interactive runs follow each question's stated fallback. It may also carry `interactionStatesToConfirm` \u2014 the recording holds no hover/focus/pressed state, so nothing shows how the component behaves when someone uses it: say its `statement` and `designFix` in that SAME one message (the fix is a Figma variant, not code). It is a disclosure, not a gate \u2014 no answer is required and recording proceeds regardless. The output also carries `feasibilityCheck`: the call arithmetic for this queue plus the free `whoami` check that turns it into a verdict \u2014 complete that handshake BEFORE the first recording call, and surface the verdict to the user when the set does not fit their daily allowance.",
|
|
12259
|
-
schema:
|
|
13070
|
+
schema: z13.object({
|
|
12260
13071
|
setDir: str("recording set directory to create/resume"),
|
|
12261
13072
|
component: str("component/system name"),
|
|
12262
13073
|
// Parts array FIRST, same reason as ingest_rep: real responses
|
|
12263
13074
|
// are usually multi-block, and the file param made plan the ONE
|
|
12264
13075
|
// remaining hand-built-envelope entry point (run 10: the agent
|
|
12265
13076
|
// wrote the file twice — once as text, once as JSON envelope).
|
|
12266
|
-
metadataParts:
|
|
13077
|
+
metadataParts: z13.array(z13.string()).optional().describe("the frame-level get_metadata response blocks, every block in order, each verbatim \u2014 never hand-join or save them yourself; this is the NORMAL param"),
|
|
12267
13078
|
metadata: optStr("ONLY when get_metadata genuinely returned one single block: its text verbatim (otherwise use metadataParts)"),
|
|
12268
|
-
metadataFiles:
|
|
12269
|
-
defaults:
|
|
13079
|
+
metadataFiles: z13.array(z13.string()).optional().describe('saved verbatim get_metadata envelope file paths \u2014 JSON shape {"content":[{"type":"text","text":"<frame \u2026>"}]}; optionally <file>@<frameId>. Prefer metadataParts: no file to write'),
|
|
13080
|
+
defaults: z13.array(z13.string()).optional().describe(`axis defaults as "Axis=Value" (from the user's defaultsToConfirm answers; may re-plan a set with nothing recorded yet)`),
|
|
12270
13081
|
componentSet: optStr("record only this component set (the user's pick from a multiple-component-sets error)"),
|
|
12271
13082
|
figmaFile: optStr("the Figma file key from the design URL you were handed \u2014 figma.com/design/<KEY>/\u2026, pass exactly <KEY>. ALWAYS pass it on a fresh plan: it is the set's recorded file identity for cross-bundle composition (ADR-013), captured at plan time only and never backfillable later")
|
|
12272
13083
|
}),
|
|
@@ -12280,7 +13091,7 @@ var init_server = __esm({
|
|
|
12280
13091
|
const single = i["metadata"];
|
|
12281
13092
|
const parts = i["metadataParts"];
|
|
12282
13093
|
if (single !== void 0 || parts !== void 0) {
|
|
12283
|
-
const tmp =
|
|
13094
|
+
const tmp = path39.join(mkdtempSync3(path39.join(os7.tmpdir(), "tendril-envelope-")), "response.txt");
|
|
12284
13095
|
if (single !== void 0) {
|
|
12285
13096
|
writeFileSync14(tmp, single);
|
|
12286
13097
|
argvOut.push("--metadata-raw-file", tmp);
|
|
@@ -12302,10 +13113,10 @@ var init_server = __esm({
|
|
|
12302
13113
|
{
|
|
12303
13114
|
name: "tendril_permissions",
|
|
12304
13115
|
description: "Install the Claude Code permission allowlist for the Tendril pipeline (write: true \u2014 merges the MCP per-tool entries, the run's shell surface, and project-scoped Write/Edit with deny guards for ./.claude and ./.git into the project's .claude/settings.local.json, idempotent, never touches other keys), or list the entries without writing. The shell/Write grants are CONVENIENCE, not a security boundary \u2014 the output's note names exactly what they trade; RELAY it with the offer. OFFER THIS AT PIPELINE START whenever NO merged Claude settings file (project .claude/settings.local.json or .claude/settings.json, or user ~/.claude/settings.json) contains tendril MCP entries \u2014 plugin installs use mcp__plugin_tendril_tendril__*, direct claude-mcp-add installs use mcp__<server>__* \u2014 when this session's tool names differ from the plugin defaults, pass figmaPrefix/tendrilPrefix with the prefixes you actually see, or the written entries never match. A FILE check, never prompt-watching \u2014 agents cannot observe permission prompts. ONE approval here replaces a prompt per pipeline call. Never run it unoffered; the user must reload the session for new settings to apply \u2014 say so.",
|
|
12305
|
-
schema:
|
|
12306
|
-
write:
|
|
12307
|
-
figmaPrefix:
|
|
12308
|
-
tendrilPrefix:
|
|
13116
|
+
schema: z13.object({
|
|
13117
|
+
write: z13.boolean().optional().describe("true = install into .claude/settings.local.json (the point of this tool); false/absent = just return the entries"),
|
|
13118
|
+
figmaPrefix: z13.string().optional().describe("The Figma server's entry prefix AS THIS SESSION NAMES ITS TOOLS \u2014 the part before __get_metadata (e.g. mcp__figma-remote, mcp__plugin_figma_figma). Pass it whenever the session's Figma tools are not mcp__plugin_figma_figma__* \u2014 entries written under the wrong prefix never match anything (measured: six dead entries in a figma-remote session)."),
|
|
13119
|
+
tendrilPrefix: z13.string().optional().describe("Same for the tendril server when this session's tendril tools are not mcp__plugin_tendril_tendril__* (e.g. mcp__tendril for claude mcp add installs).")
|
|
12309
13120
|
}),
|
|
12310
13121
|
argv: (i) => [
|
|
12311
13122
|
"permissions",
|
|
@@ -12319,23 +13130,23 @@ var init_server = __esm({
|
|
|
12319
13130
|
name: "tendril_doctor",
|
|
12320
13131
|
annotations: { readOnlyHint: true },
|
|
12321
13132
|
description: "Machine readiness + version status in one shot: installed vs latest version (with publish date and update remediation), browser identity, font-cache state, Figma desktop MCP reachability. Use to self-diagnose before recording/scoring, or whenever versions are in question. Exit 1 = something not ready; the report says exactly what and how to fix it.",
|
|
12322
|
-
schema:
|
|
13133
|
+
schema: z13.object({}),
|
|
12323
13134
|
argv: () => ["doctor"]
|
|
12324
13135
|
},
|
|
12325
13136
|
{
|
|
12326
13137
|
name: "tendril_record_next",
|
|
12327
13138
|
annotations: { readOnlyHint: true },
|
|
12328
13139
|
description: "Get the next pending recording instruction (which Figma MCP tool to call for which node, and how to save it). RARELY NEEDED: every ingest/fetch response already carries `next` \u2014 use this only to resume an interrupted session. The full queue is known from plan, so independent reps may be recorded in any order (and in parallel).",
|
|
12329
|
-
schema:
|
|
13140
|
+
schema: z13.object({ setDir: str("recording set directory") }),
|
|
12330
13141
|
argv: (i) => ["record", "next", "--set", i["setDir"]]
|
|
12331
13142
|
},
|
|
12332
13143
|
{
|
|
12333
13144
|
name: "tendril_record_fetch",
|
|
12334
13145
|
description: "Download a Figma asset URL (from get_screenshot's image_url) straight to disk and ingest it as the rep's envelope \u2014 the fallback when only the screenshot piece needs (re-)recording; for a rep's standard three recordings PREFER tendril_record_ingest_rep. Never download the image yourself: the bytes must not pass through your context.",
|
|
12335
|
-
schema:
|
|
13146
|
+
schema: z13.object({
|
|
12336
13147
|
setDir: str("recording set directory"),
|
|
12337
13148
|
rep: str("planned rep slug"),
|
|
12338
|
-
tool:
|
|
13149
|
+
tool: z13.enum(["get_screenshot"]).describe("get_screenshot"),
|
|
12339
13150
|
url: str("image_url from the Figma response, verbatim")
|
|
12340
13151
|
}),
|
|
12341
13152
|
argv: (i) => ["record", "fetch", "--set", i["setDir"], "--rep", i["rep"], "--tool", i["tool"], "--url", i["url"]]
|
|
@@ -12343,15 +13154,15 @@ var init_server = __esm({
|
|
|
12343
13154
|
{
|
|
12344
13155
|
name: "tendril_record_ingest_rep",
|
|
12345
13156
|
description: "Ingest a rep's ENTIRE recording in ONE call \u2014 the get_metadata response, the get_design_context response, and the get_screenshot image_url together. Make the three Figma calls first, in protocol order (get_metadata, then get_design_context with excludeScreenshot=true, then get_screenshot), then pass all three here VERBATIM. PREFER THIS over three separate ingest/fetch calls: one approvable operation per rep instead of three. Pieces land independently: on a partial failure the error names exactly which piece(s) to re-record \u2014 the rest are already on disk. The response carries `next` and, for design context, `assets` (auto-fetched server-side; only listed failures need record_asset).",
|
|
12346
|
-
schema:
|
|
13157
|
+
schema: z13.object({
|
|
12347
13158
|
setDir: str("recording set directory"),
|
|
12348
13159
|
rep: str("planned rep slug"),
|
|
12349
13160
|
// Parts arrays FIRST: in the field, EVERY real Figma response is
|
|
12350
13161
|
// multi-block (run 6: 49/49 reps — metadata 2 blocks, design
|
|
12351
13162
|
// context 5-6), so the arrays are the norm and the single-string
|
|
12352
13163
|
// params the rare case, not the reverse.
|
|
12353
|
-
metadataParts:
|
|
12354
|
-
contextParts:
|
|
13164
|
+
metadataParts: z13.array(z13.string()).optional().describe("get_metadata response blocks, every block in order, each verbatim \u2014 never hand-join blocks yourself. Multi-block responses are common (run 6: 49/49 reps); a single block wrapped in a one-element array is equally fine."),
|
|
13165
|
+
contextParts: z13.array(z13.string()).optional().describe("get_design_context response blocks, every block in order, each verbatim \u2014 the NORMAL param (real responses arrive as 5-6 blocks)"),
|
|
12355
13166
|
screenshotUrl: optStr("image_url from the get_screenshot response, verbatim \u2014 the CLI downloads it; the bytes never pass through your context"),
|
|
12356
13167
|
metadata: optStr("ONLY when get_metadata genuinely returned one single block: its text verbatim (otherwise use metadataParts)"),
|
|
12357
13168
|
context: optStr("ONLY when get_design_context genuinely returned one single block: its text verbatim (otherwise use contextParts)")
|
|
@@ -12363,7 +13174,7 @@ var init_server = __esm({
|
|
|
12363
13174
|
const bridge = (label, single, parts) => {
|
|
12364
13175
|
if (single !== void 0 && parts !== void 0) throw new Error(`pass at most one of \`${label}\` and \`${label}Parts\``);
|
|
12365
13176
|
if (single === void 0 && parts === void 0) return;
|
|
12366
|
-
const tmp =
|
|
13177
|
+
const tmp = path39.join(mkdtempSync3(path39.join(os7.tmpdir(), "tendril-envelope-")), "response.txt");
|
|
12367
13178
|
if (single !== void 0) {
|
|
12368
13179
|
writeFileSync14(tmp, single);
|
|
12369
13180
|
argvOut.push(`--${label}-file`, tmp);
|
|
@@ -12384,7 +13195,7 @@ var init_server = __esm({
|
|
|
12384
13195
|
{
|
|
12385
13196
|
name: "tendril_record_ingest",
|
|
12386
13197
|
description: "Single-piece ingest of a VERBATIM Figma tool-response \u2014 the fallback path (re-recording one failed piece, set-level get_variable_defs, get_metadata_interior for mains); for a rep's standard three recordings PREFER tendril_record_ingest_rep, which takes them all in one call. Pass `text` (single block) or `texts` (response split into multiple output blocks \u2014 each block verbatim, in order; NEVER hand-join them): the CLI constructs the envelope from the same bytes. The response includes `next` and, for get_design_context, `assets` (auto-fetched; only listed failures need manual handling).",
|
|
12387
|
-
schema:
|
|
13198
|
+
schema: z13.object({
|
|
12388
13199
|
setDir: str("recording set directory"),
|
|
12389
13200
|
rep: str("planned rep slug, or __set__ for the set-level get_variable_defs"),
|
|
12390
13201
|
// Enumerated, not a free string: this value reaches a file path.
|
|
@@ -12392,9 +13203,9 @@ var init_server = __esm({
|
|
|
12392
13203
|
// (--tool "../../outside/victim" replaced a file outside the set,
|
|
12393
13204
|
// exit 0). The sink in session.ts now contains the path too — this
|
|
12394
13205
|
// is the second layer, and it makes the tool self-documenting.
|
|
12395
|
-
tool:
|
|
13206
|
+
tool: z13.enum(["get_design_context", "get_metadata", "get_screenshot", "get_variable_defs", "get_metadata_interior"]),
|
|
12396
13207
|
text: optStr("the tool response text VERBATIM \u2014 exactly as returned, unmodified (single-block responses; text tools only \u2014 screenshots go through record_fetch)"),
|
|
12397
|
-
texts:
|
|
13208
|
+
texts: z13.array(z13.string()).optional().describe("when the response arrived as MULTIPLE output blocks: every block, in order, each verbatim \u2014 never hand-join blocks yourself"),
|
|
12398
13209
|
file: optStr("path to a saved envelope JSON (alternative to text/texts)")
|
|
12399
13210
|
}),
|
|
12400
13211
|
// The text rides a temp file, never argv: Windows caps a command
|
|
@@ -12406,7 +13217,7 @@ var init_server = __esm({
|
|
|
12406
13217
|
const file = i["file"];
|
|
12407
13218
|
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)");
|
|
12408
13219
|
if (file !== void 0) return [...base, "--file", file];
|
|
12409
|
-
const tmp =
|
|
13220
|
+
const tmp = path39.join(mkdtempSync3(path39.join(os7.tmpdir(), "tendril-envelope-")), "response.txt");
|
|
12410
13221
|
if (text !== void 0) {
|
|
12411
13222
|
writeFileSync14(tmp, text);
|
|
12412
13223
|
return [...base, "--file", tmp, "--raw"];
|
|
@@ -12418,7 +13229,7 @@ var init_server = __esm({
|
|
|
12418
13229
|
{
|
|
12419
13230
|
name: "tendril_record_asset",
|
|
12420
13231
|
description: "FALLBACK ONLY \u2014 ingest auto-fetches design-context assets; use this just for assets listed in an ingest response's `assets.failed`. Batch mode: pass `dir` to ingest every asset-*.<ext> in a directory in ONE call. SVGs with active content are rejected; sizes are capped.",
|
|
12421
|
-
schema:
|
|
13232
|
+
schema: z13.object({
|
|
12422
13233
|
setDir: str("recording set directory"),
|
|
12423
13234
|
rep: str("planned rep slug"),
|
|
12424
13235
|
name: optStr("asset-<id>.<ext> (single-asset mode)"),
|
|
@@ -12439,13 +13250,13 @@ var init_server = __esm({
|
|
|
12439
13250
|
name: "tendril_record_status",
|
|
12440
13251
|
annotations: { readOnlyHint: true },
|
|
12441
13252
|
description: "Recording-set completeness: per-rep recorded/missing tools, files that exist but are unusable (invalid \u2014 re-record those), and whether the set-level token map is recorded. `complete` means USABLE by the next pipeline step, including the set-level get_variable_defs.",
|
|
12442
|
-
schema:
|
|
13253
|
+
schema: z13.object({ setDir: str("recording set directory") }),
|
|
12443
13254
|
argv: (i) => ["record", "status", "--set", i["setDir"]]
|
|
12444
13255
|
},
|
|
12445
13256
|
{
|
|
12446
13257
|
name: "tendril_engine_brief",
|
|
12447
13258
|
description: "AGENT-HARNESS engine, step 1: emits the task payload file (system brief + every recorded config's emission, box, assets, tokens) and the protocol. YOU (the calling agent) implement the bundle; the CLI is the only judge. Read the payload file completely before proposing.",
|
|
12448
|
-
schema:
|
|
13259
|
+
schema: z13.object({
|
|
12449
13260
|
taskOrSet: str("a recording-set directory (from tendril_record), or a reference task name \u2014 an unknown name returns the valid list in the error"),
|
|
12450
13261
|
// Required by design, not convenience: the model choice must be
|
|
12451
13262
|
// settled BEFORE generation starts. A smoke run picked its own
|
|
@@ -12455,7 +13266,8 @@ var init_server = __esm({
|
|
|
12455
13266
|
// choose, declare, and state the reason in your report.
|
|
12456
13267
|
model: str("the model that will WRITE the implementation \u2014 ask the user when interactive; declare your reasoned choice when not"),
|
|
12457
13268
|
bar: optStr("pass (default) or cert"),
|
|
12458
|
-
out: optStr("payload file path override")
|
|
13269
|
+
out: optStr("payload file path override"),
|
|
13270
|
+
library: optStr("workspace root holding confirmed partners generated bundles (ADR-013 composed pins; default: the server's working directory)")
|
|
12459
13271
|
}),
|
|
12460
13272
|
argv: (i) => [
|
|
12461
13273
|
"engine",
|
|
@@ -12464,19 +13276,21 @@ var init_server = __esm({
|
|
|
12464
13276
|
"--model",
|
|
12465
13277
|
i["model"],
|
|
12466
13278
|
...i["bar"] !== void 0 ? ["--bar", i["bar"]] : [],
|
|
12467
|
-
...i["out"] !== void 0 ? ["--out", i["out"]] : []
|
|
13279
|
+
...i["out"] !== void 0 ? ["--out", i["out"]] : [],
|
|
13280
|
+
...i["library"] !== void 0 ? ["--library", i["library"]] : []
|
|
12468
13281
|
]
|
|
12469
13282
|
},
|
|
12470
13283
|
{
|
|
12471
13284
|
name: "tendril_engine_score",
|
|
12472
13285
|
description: "AGENT-HARNESS engine, step 2 (the oracle): scores a candidate bundle directory against recorded truth \u2014 per-config pixels, behaviors, state parity (recording-selected \u2014 a bundle cannot unschedule it) \u2014 and returns feedback plus evidence artifacts. Iterate until allPass or two non-improving rounds. Only THIS tool's output counts as a score; never claim numbers yourself.",
|
|
12473
|
-
schema:
|
|
13286
|
+
schema: z13.object({
|
|
12474
13287
|
taskOrSet: str("reference task name or recording-set directory"),
|
|
12475
13288
|
candidateDir: str("directory containing the proposed bundle files"),
|
|
12476
13289
|
bar: optStr("pass (default) or cert"),
|
|
12477
13290
|
host: optStr("your host identity (e.g. claude-code, cursor, codex) \u2014 recorded as self-reported provenance"),
|
|
12478
13291
|
model: str("the model that proposed the candidate \u2014 REQUIRED; recorded as self-reported provenance, and the CLI refuses to score without it"),
|
|
12479
|
-
rebind:
|
|
13292
|
+
rebind: z13.boolean().optional().describe("explicitly re-bind an already-bound bundle to a DIFFERENT recording set \u2014 scoring refuses this otherwise, because rebinding silently rewrites the bundle's verification identity; only pass after telling the user"),
|
|
13293
|
+
library: optStr("workspace root holding confirmed partners generated bundles (ADR-013 composed pins; default: the server's working directory) \u2014 pass the same root the brief used")
|
|
12480
13294
|
}),
|
|
12481
13295
|
argv: (i) => [
|
|
12482
13296
|
"engine",
|
|
@@ -12487,13 +13301,14 @@ var init_server = __esm({
|
|
|
12487
13301
|
...i["host"] !== void 0 ? ["--host", i["host"]] : [],
|
|
12488
13302
|
"--model",
|
|
12489
13303
|
i["model"],
|
|
12490
|
-
...i["rebind"] === true ? ["--rebind"] : []
|
|
13304
|
+
...i["rebind"] === true ? ["--rebind"] : [],
|
|
13305
|
+
...i["library"] !== void 0 ? ["--library", i["library"]] : []
|
|
12491
13306
|
]
|
|
12492
13307
|
},
|
|
12493
13308
|
{
|
|
12494
13309
|
name: "tendril_codeconnect",
|
|
12495
13310
|
description: "Emit a Figma Code Connect template (.figma.ts) for a certified bundle \u2014 every Figma variant value mapped to its verified prop fragment from recorded truth, stamped with the bundle's trust statement. EXTRA VALUE step after verify passes: offer it to the user. Publishing is the USER'S action (their Figma token, Organization/Enterprise plan) \u2014 via npx @figma/code-connect connect publish, or the Figma MCP's own add_code_connect_map/send_code_connect_mappings tools if available in this session.",
|
|
12496
|
-
schema:
|
|
13311
|
+
schema: z13.object({
|
|
12497
13312
|
bundleDir: str("bundle directory (carries component.json)"),
|
|
12498
13313
|
figmaUrl: str("figma.com /design/ URL of the COMPONENT SET, with node-id (ask the user to Copy link to selection if you don't have it)"),
|
|
12499
13314
|
set: optStr("recording set override (default: the bundle's provenance path)"),
|
|
@@ -12511,27 +13326,29 @@ var init_server = __esm({
|
|
|
12511
13326
|
{
|
|
12512
13327
|
name: "tendril_verify",
|
|
12513
13328
|
description: "Verify/check that a component matches its Figma design \u2014 use when the user asks whether an implementation is faithful to the design, or to re-certify an existing Tendril bundle. Recomputes full verification (per-config status, behaviors, composition, evidence artifacts). Free, account-less, network-less \u2014 the trust anchor. Exit 5 means below the target bar with an honest report \u2014 sub-bar scores, or at bar cert a config demoted by absent-ink clusters.",
|
|
12514
|
-
schema:
|
|
13329
|
+
schema: z13.object({
|
|
12515
13330
|
bundleDir: str("bundle directory to verify"),
|
|
12516
13331
|
bar: optStr("pass (default) or cert"),
|
|
12517
|
-
set: optStr("recording-set directory override")
|
|
13332
|
+
set: optStr("recording-set directory override"),
|
|
13333
|
+
library: optStr("workspace root holding confirmed partners generated bundles (ADR-013 composed pins; default: the server's working directory)")
|
|
12518
13334
|
}),
|
|
12519
13335
|
argv: (i) => [
|
|
12520
13336
|
"verify",
|
|
12521
13337
|
i["bundleDir"],
|
|
12522
13338
|
...i["bar"] !== void 0 ? ["--bar", i["bar"]] : [],
|
|
12523
|
-
...i["set"] !== void 0 ? ["--set", i["set"]] : []
|
|
13339
|
+
...i["set"] !== void 0 ? ["--set", i["set"]] : [],
|
|
13340
|
+
...i["library"] !== void 0 ? ["--library", i["library"]] : []
|
|
12524
13341
|
]
|
|
12525
13342
|
},
|
|
12526
13343
|
{
|
|
12527
13344
|
name: "tendril_generate_curated",
|
|
12528
13345
|
description: "CURATED engine (explicit alternative path): generation by an allowlisted API model over the user's OpenRouter-compatible key, with cost consent, hard spend caps, and resume. Use ONLY when the user asks for API-model generation instead of implementing it yourself.",
|
|
12529
|
-
schema:
|
|
13346
|
+
schema: z13.object({
|
|
12530
13347
|
input: str("reference task name or recording-set directory"),
|
|
12531
13348
|
model: optStr("OpenRouter model id (default: allowlist pointer)"),
|
|
12532
13349
|
bar: optStr("pass (default) or cert"),
|
|
12533
13350
|
cap: optStr("spend cap in USD (default 1.50)"),
|
|
12534
|
-
yes:
|
|
13351
|
+
yes: z13.boolean().optional().describe("accept the cost consent (the user must have approved the spend)")
|
|
12535
13352
|
}),
|
|
12536
13353
|
argv: (i) => [
|
|
12537
13354
|
"generate",
|
|
@@ -12562,13 +13379,13 @@ __export(permissions_exports, {
|
|
|
12562
13379
|
runPermissions: () => runPermissions,
|
|
12563
13380
|
writeSelection: () => writeSelection
|
|
12564
13381
|
});
|
|
12565
|
-
import { existsSync as
|
|
13382
|
+
import { existsSync as existsSync33, mkdirSync as mkdirSync9, readFileSync as readFileSync29, writeFileSync as writeFileSync15 } from "node:fs";
|
|
12566
13383
|
import os8 from "node:os";
|
|
12567
|
-
import
|
|
13384
|
+
import path40 from "node:path";
|
|
12568
13385
|
function mergeAllowlist(file, entries, denyEntries = []) {
|
|
12569
13386
|
let settings = {};
|
|
12570
|
-
if (
|
|
12571
|
-
settings = JSON.parse(
|
|
13387
|
+
if (existsSync33(file) && readFileSync29(file, "utf8").trim() !== "") {
|
|
13388
|
+
settings = JSON.parse(readFileSync29(file, "utf8"));
|
|
12572
13389
|
if (settings === null || typeof settings !== "object" || Array.isArray(settings)) throw new Error("settings root is not an object");
|
|
12573
13390
|
}
|
|
12574
13391
|
const permissions = settings["permissions"] ??= {};
|
|
@@ -12588,7 +13405,7 @@ function mergeAllowlist(file, entries, denyEntries = []) {
|
|
|
12588
13405
|
}
|
|
12589
13406
|
if (added.length > 0 || denyAdded.length > 0) {
|
|
12590
13407
|
allow.push(...added);
|
|
12591
|
-
mkdirSync9(
|
|
13408
|
+
mkdirSync9(path40.dirname(file), { recursive: true });
|
|
12592
13409
|
writeFileSync15(file, `${JSON.stringify(settings, null, 2)}
|
|
12593
13410
|
`);
|
|
12594
13411
|
}
|
|
@@ -12653,7 +13470,7 @@ async function runPermissions(flags) {
|
|
|
12653
13470
|
}
|
|
12654
13471
|
if (flags.write) {
|
|
12655
13472
|
const base = process.env["INIT_CWD"] ?? process.cwd();
|
|
12656
|
-
const file = flags.user ?
|
|
13473
|
+
const file = flags.user ? path40.join(os8.homedir(), ".claude", "settings.json") : path40.join(base, ".claude", "settings.local.json");
|
|
12657
13474
|
const { entries, denyEntries } = writeSelection(result, flags.user === true);
|
|
12658
13475
|
if (flags.dryRun) {
|
|
12659
13476
|
emitData(flags, { file, wouldAdd: entries, wouldDeny: denyEntries }, () => {
|
|
@@ -12796,30 +13613,255 @@ var init_permissions = __esm({
|
|
|
12796
13613
|
}
|
|
12797
13614
|
});
|
|
12798
13615
|
|
|
13616
|
+
// packages/cli/src/commands/compose.ts
|
|
13617
|
+
var compose_exports = {};
|
|
13618
|
+
__export(compose_exports, {
|
|
13619
|
+
COMPOSE_DESCRIPTION: () => COMPOSE_DESCRIPTION,
|
|
13620
|
+
runCompose: () => runCompose
|
|
13621
|
+
});
|
|
13622
|
+
import { createHash as createHash8 } from "node:crypto";
|
|
13623
|
+
import { existsSync as existsSync34, readFileSync as readFileSync30 } from "node:fs";
|
|
13624
|
+
import path41 from "node:path";
|
|
13625
|
+
function substitutionPairs(edges, hostSet) {
|
|
13626
|
+
const pairs = /* @__PURE__ */ new Map();
|
|
13627
|
+
for (const e of edges) {
|
|
13628
|
+
if (e.hostSet !== hostSet || e.kind !== "substitution" || e.pose === void 0) continue;
|
|
13629
|
+
const dirs = e.partners.map((p) => p.dir);
|
|
13630
|
+
const key = pairKeyFor(hostSet, dirs);
|
|
13631
|
+
if (!pairs.has(key)) {
|
|
13632
|
+
pairs.set(key, {
|
|
13633
|
+
key,
|
|
13634
|
+
partnerDirs: dirs,
|
|
13635
|
+
displayName: e.partners[0].displayName,
|
|
13636
|
+
...e.partners[0].figmaFile !== void 0 ? { figmaFile: e.partners[0].figmaFile } : {},
|
|
13637
|
+
instances: [],
|
|
13638
|
+
disclosures: []
|
|
13639
|
+
});
|
|
13640
|
+
}
|
|
13641
|
+
const pair = pairs.get(key);
|
|
13642
|
+
pair.instances.push({ hostRep: e.hostRep, instanceId: e.instanceId, poseVariantNodeId: e.pose.variantNodeId });
|
|
13643
|
+
for (const d of e.disclosures) if (!pair.disclosures.includes(d)) pair.disclosures.push(d);
|
|
13644
|
+
}
|
|
13645
|
+
return [...pairs.values()];
|
|
13646
|
+
}
|
|
13647
|
+
function runCompose(flags) {
|
|
13648
|
+
if (flags.describe) {
|
|
13649
|
+
printDescription(COMPOSE_DESCRIPTION);
|
|
13650
|
+
return;
|
|
13651
|
+
}
|
|
13652
|
+
const base = process.env["INIT_CWD"] ?? process.cwd();
|
|
13653
|
+
const roots = flags.library !== void 0 && flags.library.length > 0 ? flags.library.map((d) => path41.resolve(base, d)) : [base];
|
|
13654
|
+
if (flags.set === void 0 && (flags.confirmCompositions === true || flags.decline !== void 0 && flags.decline.length > 0)) {
|
|
13655
|
+
fail(flags, ExitCode.InputValidation, {
|
|
13656
|
+
error: "--confirm-compositions/--decline require --set <host> \u2014 a decision needs the host set it decides for",
|
|
13657
|
+
code: "compose-decision-without-set",
|
|
13658
|
+
remediation: "Add --set <host recording-set dir>, or drop the decision flags to list opportunities."
|
|
13659
|
+
});
|
|
13660
|
+
}
|
|
13661
|
+
if (flags.set !== void 0) {
|
|
13662
|
+
runComposeConfirm(flags, path41.resolve(base, flags.set), roots);
|
|
13663
|
+
return;
|
|
13664
|
+
}
|
|
13665
|
+
const index = buildComposeIndex(roots);
|
|
13666
|
+
const edges = composeReport(index);
|
|
13667
|
+
emitData(flags, { sets: index.map((s) => s.dir), edges, note: NOTE }, () => {
|
|
13668
|
+
process.stdout.write(`indexed ${index.length} recording set(s) under ${roots.join(", ")}
|
|
13669
|
+
`);
|
|
13670
|
+
if (index.length === 0) {
|
|
13671
|
+
process.stdout.write("no recording sets found \u2014 point --library at a directory that holds recording-set.json sets\n");
|
|
13672
|
+
return;
|
|
13673
|
+
}
|
|
13674
|
+
if (edges.length === 0) {
|
|
13675
|
+
process.stdout.write("no composition edges: no recorded instance references another recorded component (externals stay disclosed in each set's own reports)\n");
|
|
13676
|
+
return;
|
|
13677
|
+
}
|
|
13678
|
+
let lastHost = "";
|
|
13679
|
+
for (const e of edges) {
|
|
13680
|
+
const host = `${path41.basename(e.hostSet)}`;
|
|
13681
|
+
if (host !== lastHost) {
|
|
13682
|
+
process.stdout.write(`
|
|
13683
|
+
${host}
|
|
13684
|
+
`);
|
|
13685
|
+
lastHost = host;
|
|
13686
|
+
}
|
|
13687
|
+
const partners = e.partners.map((p) => p.displayName).join(" / ") || "\u2014";
|
|
13688
|
+
const pose = e.pose !== void 0 ? ` pose=${e.pose.variantNodeId} (${e.pose.reps.map((r) => `${path41.basename(r.dir)}:${r.slug}`).join(", ")})` : "";
|
|
13689
|
+
process.stdout.write(` ${e.kind.toUpperCase().padEnd(12)} ${e.hostRep}/${e.instanceId} "${e.instanceName}" \u2192 ${partners}${pose}
|
|
13690
|
+
`);
|
|
13691
|
+
for (const d of e.disclosures) process.stdout.write(` \xB7 ${d}
|
|
13692
|
+
`);
|
|
13693
|
+
}
|
|
13694
|
+
process.stdout.write(`
|
|
13695
|
+
${NOTE}
|
|
13696
|
+
`);
|
|
13697
|
+
});
|
|
13698
|
+
}
|
|
13699
|
+
function runComposeConfirm(flags, hostSet, roots) {
|
|
13700
|
+
if (!existsSync34(path41.join(hostSet, "recording-set.json"))) {
|
|
13701
|
+
fail(flags, ExitCode.InputValidation, {
|
|
13702
|
+
error: `no recording-set.json in ${hostSet}`,
|
|
13703
|
+
code: "no-recording-set",
|
|
13704
|
+
remediation: "Point --set at a recorded host set (the directory holding recording-set.json)."
|
|
13705
|
+
});
|
|
13706
|
+
}
|
|
13707
|
+
const scanRoots = [.../* @__PURE__ */ new Set([...roots, path41.dirname(hostSet)])];
|
|
13708
|
+
const index = buildComposeIndex(scanRoots);
|
|
13709
|
+
const edges = composeReport(index);
|
|
13710
|
+
const pairs = substitutionPairs(edges, hostSet);
|
|
13711
|
+
const { raw } = readManifestFile(hostSet);
|
|
13712
|
+
const rawStanding = Array.isArray(raw["compositions"]) ? raw["compositions"] : [];
|
|
13713
|
+
const standing = [];
|
|
13714
|
+
for (let i = 0; i < rawStanding.length; i++) {
|
|
13715
|
+
const parsed = CompositionEntrySchema.safeParse(rawStanding[i]);
|
|
13716
|
+
if (!parsed.success) {
|
|
13717
|
+
fail(flags, ExitCode.InputValidation, {
|
|
13718
|
+
error: `compositions[${i}] in ${hostSet}/recording-set.json is not a valid v1 composition entry (${parsed.error.issues[0]?.message ?? "invalid"})`,
|
|
13719
|
+
code: "compositions-entry-invalid",
|
|
13720
|
+
remediation: "The manifest's compositions extension holds human decisions and is provenance-hashed \u2014 repair or remove the malformed entry by hand, then re-run. Nothing was changed."
|
|
13721
|
+
});
|
|
13722
|
+
}
|
|
13723
|
+
standing.push(parsed.data);
|
|
13724
|
+
}
|
|
13725
|
+
const decidedKeys = new Set(standing.map((c) => fromStoredRel(c.partner.key)));
|
|
13726
|
+
const open = pairs.filter((p) => !decidedKeys.has(p.key));
|
|
13727
|
+
const printProposal = () => {
|
|
13728
|
+
for (const s of standing) process.stdout.write(`standing ${s.status.toUpperCase()}: ${s.partner.displayName} [${s.partner.key}] (${s.instances.length} instance(s)) \u2014 asked once, not re-asked
|
|
13729
|
+
`);
|
|
13730
|
+
if (open.length === 0) {
|
|
13731
|
+
process.stdout.write(standing.length > 0 ? "no undecided composition pairs for this host\n" : "no id-backed composition pairs found for this host (name-only proposals, if any, are not confirmable \u2014 see compose --list)\n");
|
|
13732
|
+
return;
|
|
13733
|
+
}
|
|
13734
|
+
for (const p of open) {
|
|
13735
|
+
process.stdout.write(`
|
|
13736
|
+
PAIR ${p.displayName} [${p.key}]${p.figmaFile !== void 0 ? ` file ${p.figmaFile}` : " \u2014 file identity NOT captured"}
|
|
13737
|
+
`);
|
|
13738
|
+
for (const i of p.instances) process.stdout.write(` ${i.hostRep}/${i.instanceId} \u2192 pose ${i.poseVariantNodeId}
|
|
13739
|
+
`);
|
|
13740
|
+
for (const d of p.disclosures) process.stdout.write(` \xB7 ${d}
|
|
13741
|
+
`);
|
|
13742
|
+
}
|
|
13743
|
+
};
|
|
13744
|
+
const deciding = flags.confirmCompositions === true || flags.decline !== void 0 && flags.decline.length > 0;
|
|
13745
|
+
if (open.length === 0 || !deciding) {
|
|
13746
|
+
emitData(flags, { hostSet, openPairs: open, standing, note: "confirmations are human-only; declines persist; name-only proposals are never confirmable" }, printProposal);
|
|
13747
|
+
} else if (!flags.json) {
|
|
13748
|
+
printProposal();
|
|
13749
|
+
}
|
|
13750
|
+
if (open.length === 0) return;
|
|
13751
|
+
if (flags.confirmCompositions !== true && (flags.decline === void 0 || flags.decline.length === 0)) {
|
|
13752
|
+
fail(flags, ExitCode.ConfirmationRequired, {
|
|
13753
|
+
error: "composition pairs require a human decision",
|
|
13754
|
+
code: "compositions-unconfirmed",
|
|
13755
|
+
remediation: `Review the pairs above, then a HUMAN re-runs \`${tendrilCommand(`compose --set ${hostSet} --confirm-compositions`)}\` (confirm all listed) and/or \`--decline <pair-key>\` in their own terminal. Agents: surface this to your operator \u2014 the flag is refused without an interactive terminal, and composing without confirmation never happens.`
|
|
13756
|
+
});
|
|
13757
|
+
}
|
|
13758
|
+
if (process.stdin.isTTY !== true) {
|
|
13759
|
+
fail(flags, ExitCode.ConfirmationRequired, {
|
|
13760
|
+
error: "--confirm-compositions and --decline require an interactive terminal \u2014 this decision is human-only",
|
|
13761
|
+
code: "compositions-confirmation-not-interactive",
|
|
13762
|
+
remediation: `A human runs \`${tendrilCommand(`compose --set ${hostSet} --confirm-compositions`)}\` in their own terminal. Agents: show the proposal to your operator instead of confirming it.`
|
|
13763
|
+
});
|
|
13764
|
+
}
|
|
13765
|
+
const declineKeys = new Set(flags.decline ?? []);
|
|
13766
|
+
for (const k of declineKeys) {
|
|
13767
|
+
if (!open.some((p) => p.key === k)) {
|
|
13768
|
+
fail(flags, ExitCode.InputValidation, {
|
|
13769
|
+
error: `--decline "${k}" names no open pair (open: ${open.map((p) => p.key).join(", ")})`,
|
|
13770
|
+
code: "unknown-composition-pair",
|
|
13771
|
+
remediation: "Spell the pair key exactly as printed in brackets."
|
|
13772
|
+
});
|
|
13773
|
+
}
|
|
13774
|
+
}
|
|
13775
|
+
const entries = open.map((p) => ({
|
|
13776
|
+
v: 1,
|
|
13777
|
+
partner: {
|
|
13778
|
+
key: p.key,
|
|
13779
|
+
displayName: p.displayName,
|
|
13780
|
+
...p.figmaFile !== void 0 ? { figmaFile: p.figmaFile } : {},
|
|
13781
|
+
// The partner's manifest BYTES, pinned at decision time and
|
|
13782
|
+
// keyed by the same host-relative identity as the pair key —
|
|
13783
|
+
// any partner re-plan/roles/composition write flips it, which
|
|
13784
|
+
// is the staleness signal later slices compare against. (The
|
|
13785
|
+
// full recording-set hash join lands with pin authoring, where
|
|
13786
|
+
// task configs exist.)
|
|
13787
|
+
manifestSha256: Object.fromEntries(
|
|
13788
|
+
p.partnerDirs.map((d) => [path41.relative(hostSet, d), createHash8("sha256").update(readFileSync30(path41.join(d, "recording-set.json"))).digest("hex")])
|
|
13789
|
+
)
|
|
13790
|
+
},
|
|
13791
|
+
instances: p.instances,
|
|
13792
|
+
status: declineKeys.has(p.key) ? "declined" : "confirmed"
|
|
13793
|
+
}));
|
|
13794
|
+
const decided = flags.confirmCompositions === true ? entries : entries.filter((e) => declineKeys.has(e.partner.key));
|
|
13795
|
+
const merged = { ...raw, compositions: [...standing, ...decided] };
|
|
13796
|
+
writeManifest(hostSet, merged);
|
|
13797
|
+
warn(
|
|
13798
|
+
flags,
|
|
13799
|
+
"the host manifest changed \u2014 its bytes are hashed into bundle provenance, so bundles generated from this set before this decision now carry a stale set identity; regenerate to compose (`tendril engine brief` carries the pins)"
|
|
13800
|
+
);
|
|
13801
|
+
emitData(flags, { hostSet, openPairs: open, standing, written: decided, note: "confirmations are human-only; declines persist; name-only proposals are never confirmable" }, () => {
|
|
13802
|
+
for (const e of decided) process.stdout.write(`${e.status.toUpperCase()}: ${e.partner.displayName} [${e.partner.key}] (${e.instances.length} instance(s))
|
|
13803
|
+
`);
|
|
13804
|
+
});
|
|
13805
|
+
}
|
|
13806
|
+
var COMPOSE_DESCRIPTION, NOTE;
|
|
13807
|
+
var init_compose2 = __esm({
|
|
13808
|
+
"packages/cli/src/commands/compose.ts"() {
|
|
13809
|
+
"use strict";
|
|
13810
|
+
init_src3();
|
|
13811
|
+
init_src();
|
|
13812
|
+
init_describe();
|
|
13813
|
+
init_invocation();
|
|
13814
|
+
init_output();
|
|
13815
|
+
COMPOSE_DESCRIPTION = {
|
|
13816
|
+
name: "compose",
|
|
13817
|
+
summary: "Composition opportunities across a workspace's recording sets (ADR-013): --list is the read-only discovery surface; --set persists a HUMAN's confirm/decline decisions into the host manifest. Nothing composes yet \u2014 compose-on-generate is a later slice.",
|
|
13818
|
+
args: [],
|
|
13819
|
+
flags: [
|
|
13820
|
+
{ flag: "--list", description: "Derive the index and report edges (default)" },
|
|
13821
|
+
{ flag: "--library <dir...>", description: "Workspace root(s) to scan for recording sets", default: "current directory" },
|
|
13822
|
+
{ flag: "--set <dir>", description: "Confirmation mode: propose this HOST set's id-backed pairs for a human decision (confirmations and declines persist in the host manifest)" },
|
|
13823
|
+
{ flag: "--confirm-compositions", description: "HUMAN-ONLY: confirm every open pair printed for --set \u2014 refused without an interactive terminal; agents surface the proposal to their operator" },
|
|
13824
|
+
{ flag: "--decline <pair-key...>", description: "HUMAN-ONLY: persistently decline the named pair(s) \u2014 asked once, never re-asked" },
|
|
13825
|
+
{ flag: "--json", description: "Machine-readable output" }
|
|
13826
|
+
],
|
|
13827
|
+
output: {
|
|
13828
|
+
sets: "string[] \u2014 recording sets indexed (--list)",
|
|
13829
|
+
edges: "per-instance edges: kind (substitution | nested | ask | proposal | external), partners, pose (variant node id + per-set rep slugs), disclosures (--list)",
|
|
13830
|
+
openPairs: "with --set: undecided id-backed pairs awaiting a human decision",
|
|
13831
|
+
standing: "with --set: persisted confirmations/declines (asked once)",
|
|
13832
|
+
note: "string \u2014 what this command does NOT do"
|
|
13833
|
+
},
|
|
13834
|
+
exitCodes: { 0: "report printed (an empty one is a report, not an error)", 4: "with --set: open pairs need a human decision, or the flag arrived without an interactive terminal", 3: "with --set: bad host dir or unknown --decline key" },
|
|
13835
|
+
examples: ["tendril compose --list", "tendril compose --list --library ./recordings --json", "tendril compose --set ./recordings/dialog --confirm-compositions"]
|
|
13836
|
+
};
|
|
13837
|
+
NOTE = "Discovery only: these are PROPOSALS under the audited join rule (id evidence decides; name evidence only proposes). Nothing composes yet \u2014 the human confirmation channel and compose-on-generate land in later slices. Substitution-grade edges do NOT assert pixel-neutrality: instance overrides are measured-real and the score-time check is the judge.";
|
|
13838
|
+
}
|
|
13839
|
+
});
|
|
13840
|
+
|
|
12799
13841
|
// packages/cli/src/commands/inspect.ts
|
|
12800
13842
|
var inspect_exports = {};
|
|
12801
13843
|
__export(inspect_exports, {
|
|
12802
13844
|
INSPECT_DESCRIPTION: () => INSPECT_DESCRIPTION,
|
|
12803
13845
|
runInspect: () => runInspect
|
|
12804
13846
|
});
|
|
12805
|
-
import { existsSync as
|
|
12806
|
-
import
|
|
13847
|
+
import { existsSync as existsSync35, readFileSync as readFileSync31, writeFileSync as writeFileSync16 } from "node:fs";
|
|
13848
|
+
import path42 from "node:path";
|
|
12807
13849
|
async function runInspect(opts) {
|
|
12808
13850
|
if (opts.describe) {
|
|
12809
13851
|
printDescription(INSPECT_DESCRIPTION);
|
|
12810
13852
|
return;
|
|
12811
13853
|
}
|
|
12812
|
-
const bundleDir =
|
|
12813
|
-
const evidenceDir =
|
|
12814
|
-
const manifestPath2 =
|
|
12815
|
-
if (!
|
|
13854
|
+
const bundleDir = path42.resolve(opts.bundleDir);
|
|
13855
|
+
const evidenceDir = path42.join(bundleDir, "verify-evidence");
|
|
13856
|
+
const manifestPath2 = path42.join(bundleDir, "component.json");
|
|
13857
|
+
if (!existsSync35(evidenceDir) || !existsSync35(manifestPath2)) {
|
|
12816
13858
|
fail(opts, ExitCode.InputValidation, {
|
|
12817
|
-
error: `nothing to inspect in ${bundleDir} \u2014 ${
|
|
13859
|
+
error: `nothing to inspect in ${bundleDir} \u2014 ${existsSync35(manifestPath2) ? "no verify-evidence directory" : "no component.json"}`,
|
|
12818
13860
|
code: "no-evidence",
|
|
12819
13861
|
remediation: `Run \`${tendrilCommand(`verify ${bundleDir}`)}\` first \u2014 inspect reads the ref/render evidence that run writes.`
|
|
12820
13862
|
});
|
|
12821
13863
|
}
|
|
12822
|
-
const { manifest } = readBundleManifest(
|
|
13864
|
+
const { manifest } = readBundleManifest(readFileSync31(manifestPath2, "utf8"));
|
|
12823
13865
|
if (manifest === void 0) {
|
|
12824
13866
|
fail(opts, ExitCode.InputValidation, {
|
|
12825
13867
|
error: "component.json did not parse as a bundle manifest",
|
|
@@ -12827,8 +13869,8 @@ async function runInspect(opts) {
|
|
|
12827
13869
|
remediation: "Re-emit the bundle (score/verify rewrite component.json), then re-run inspect."
|
|
12828
13870
|
});
|
|
12829
13871
|
}
|
|
12830
|
-
const setDir =
|
|
12831
|
-
const reps = Object.keys(manifest.propAdapter).filter((rep) =>
|
|
13872
|
+
const setDir = path42.resolve(opts.set ?? manifest.provenance.recordingSet.path);
|
|
13873
|
+
const reps = Object.keys(manifest.propAdapter).filter((rep) => existsSync35(path42.join(evidenceDir, `${rep}-ref.png`)) && existsSync35(path42.join(evidenceDir, `${rep}-render.png`)));
|
|
12832
13874
|
if (reps.length === 0) {
|
|
12833
13875
|
fail(opts, ExitCode.InputValidation, {
|
|
12834
13876
|
error: "verify-evidence holds no ref/render pairs for this bundle's configs",
|
|
@@ -12839,15 +13881,15 @@ async function runInspect(opts) {
|
|
|
12839
13881
|
let crops = 0;
|
|
12840
13882
|
const sections = [];
|
|
12841
13883
|
for (const rep of reps) {
|
|
12842
|
-
const ref = new Uint8Array(
|
|
12843
|
-
const render = new Uint8Array(
|
|
13884
|
+
const ref = new Uint8Array(readFileSync31(path42.join(evidenceDir, `${rep}-ref.png`)));
|
|
13885
|
+
const render = new Uint8Array(readFileSync31(path42.join(evidenceDir, `${rep}-render.png`)));
|
|
12844
13886
|
const nodes = smallSemanticNodes(setDir, rep, opts.maxArea ?? 1024).slice(0, 12);
|
|
12845
13887
|
const cells = [];
|
|
12846
13888
|
for (const [i, n] of nodes.entries()) {
|
|
12847
13889
|
const rect = { x: n.x, y: n.y, w: n.w, h: n.h };
|
|
12848
13890
|
try {
|
|
12849
|
-
writeFileSync16(
|
|
12850
|
-
writeFileSync16(
|
|
13891
|
+
writeFileSync16(path42.join(evidenceDir, `${rep}-inspect-${i}-ref.png`), zoomCrop(ref, rect));
|
|
13892
|
+
writeFileSync16(path42.join(evidenceDir, `${rep}-inspect-${i}-render.png`), zoomCrop(render, rect));
|
|
12851
13893
|
} catch {
|
|
12852
13894
|
continue;
|
|
12853
13895
|
}
|
|
@@ -12860,7 +13902,7 @@ async function runInspect(opts) {
|
|
|
12860
13902
|
`<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>`
|
|
12861
13903
|
);
|
|
12862
13904
|
}
|
|
12863
|
-
const sheet =
|
|
13905
|
+
const sheet = path42.join(evidenceDir, "inspect.html");
|
|
12864
13906
|
writeFileSync16(
|
|
12865
13907
|
sheet,
|
|
12866
13908
|
`<!doctype html><meta charset="utf-8"><title>${esc(manifest.name)} \u2014 tendril inspect</title><style>
|
|
@@ -12938,17 +13980,17 @@ __export(generate_recorded_exports, {
|
|
|
12938
13980
|
runGenerateRecorded: () => runGenerateRecorded
|
|
12939
13981
|
});
|
|
12940
13982
|
import { confirm as confirm3, isCancel as isCancel3 } from "@clack/prompts";
|
|
12941
|
-
import { existsSync as
|
|
12942
|
-
import
|
|
13983
|
+
import { existsSync as existsSync36, readFileSync as readFileSync32 } from "node:fs";
|
|
13984
|
+
import path43 from "node:path";
|
|
12943
13985
|
async function runGenerateRecorded(opts) {
|
|
12944
13986
|
const callerCwd = process.env["INIT_CWD"] ?? process.cwd();
|
|
12945
|
-
const outDirAbs =
|
|
12946
|
-
const recordedAsPath =
|
|
13987
|
+
const outDirAbs = path43.resolve(callerCwd, opts.out);
|
|
13988
|
+
const recordedAsPath = path43.resolve(callerCwd, opts.recorded);
|
|
12947
13989
|
let task;
|
|
12948
13990
|
let taskName;
|
|
12949
13991
|
let authoredApi;
|
|
12950
13992
|
let composition;
|
|
12951
|
-
const isSet =
|
|
13993
|
+
const isSet = existsSync36(path43.join(recordedAsPath, "recording-set.json"));
|
|
12952
13994
|
const registry = TASKS[opts.recorded];
|
|
12953
13995
|
if (registry !== void 0 && !isSet) {
|
|
12954
13996
|
task = registry;
|
|
@@ -12957,7 +13999,7 @@ async function runGenerateRecorded(opts) {
|
|
|
12957
13999
|
try {
|
|
12958
14000
|
const authored = authorTaskFromSet(recordedAsPath);
|
|
12959
14001
|
task = authored.task;
|
|
12960
|
-
taskName =
|
|
14002
|
+
taskName = path43.basename(recordedAsPath);
|
|
12961
14003
|
authoredApi = authored.api;
|
|
12962
14004
|
const roles = RolesSchema.safeParse(loadManifest(recordedAsPath).roles);
|
|
12963
14005
|
if (roles.success) composition = roles.data;
|
|
@@ -12991,7 +14033,7 @@ async function runGenerateRecorded(opts) {
|
|
|
12991
14033
|
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)`);
|
|
12992
14034
|
}
|
|
12993
14035
|
const missing = task.configs.filter(
|
|
12994
|
-
(c) => !
|
|
14036
|
+
(c) => !existsSync36(path43.join(task.set, c.rep, "get_screenshot.json")) || !existsSync36(path43.join(task.set, c.rep, "get_metadata.json")) || !existsSync36(path43.join(task.set, c.rep, "get_design_context.json"))
|
|
12995
14037
|
);
|
|
12996
14038
|
if (missing.length > 0) {
|
|
12997
14039
|
fail(opts, ExitCode.RecordingIncomplete, {
|
|
@@ -13061,8 +14103,8 @@ async function runGenerateRecorded(opts) {
|
|
|
13061
14103
|
` : `${line}
|
|
13062
14104
|
`);
|
|
13063
14105
|
if (opts.dryRun) {
|
|
13064
|
-
emitData(opts, { dryRun: true, task: taskName, model: modelId, consent, wouldWrite:
|
|
13065
|
-
process.stdout.write(`dry-run: nothing sent, nothing written (would write ${
|
|
14106
|
+
emitData(opts, { dryRun: true, task: taskName, model: modelId, consent, wouldWrite: path43.join(outDirAbs, taskName) }, () => {
|
|
14107
|
+
process.stdout.write(`dry-run: nothing sent, nothing written (would write ${path43.join(outDirAbs, taskName)})
|
|
13066
14108
|
`);
|
|
13067
14109
|
});
|
|
13068
14110
|
return;
|
|
@@ -13085,10 +14127,10 @@ async function runGenerateRecorded(opts) {
|
|
|
13085
14127
|
});
|
|
13086
14128
|
}
|
|
13087
14129
|
}
|
|
13088
|
-
const bundleDir =
|
|
13089
|
-
if (
|
|
14130
|
+
const bundleDir = path43.join(outDirAbs, taskName);
|
|
14131
|
+
if (existsSync36(path43.join(bundleDir, "component.json"))) {
|
|
13090
14132
|
try {
|
|
13091
|
-
const prior = readBundleManifest(
|
|
14133
|
+
const prior = readBundleManifest(readFileSync32(path43.join(bundleDir, "component.json"), "utf8")).manifest;
|
|
13092
14134
|
if (prior !== void 0 && prior.provenance.recordingSet.hash !== recordingSetHash(task.set, task.configs)) {
|
|
13093
14135
|
fail(opts, ExitCode.InputValidation, {
|
|
13094
14136
|
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`,
|
|
@@ -13254,7 +14296,7 @@ init_invocation();
|
|
|
13254
14296
|
init_output();
|
|
13255
14297
|
import { intro, isCancel, outro, password } from "@clack/prompts";
|
|
13256
14298
|
import fs from "node:fs";
|
|
13257
|
-
import
|
|
14299
|
+
import path26 from "node:path";
|
|
13258
14300
|
var INIT_DESCRIPTION = {
|
|
13259
14301
|
name: "init",
|
|
13260
14302
|
summary: "Configure Figma and OpenRouter credentials in .env (idempotent).",
|
|
@@ -13291,7 +14333,7 @@ async function runInit(flags) {
|
|
|
13291
14333
|
printDescription(INIT_DESCRIPTION);
|
|
13292
14334
|
return;
|
|
13293
14335
|
}
|
|
13294
|
-
const envPath =
|
|
14336
|
+
const envPath = path26.resolve(process.cwd(), ".env");
|
|
13295
14337
|
const existing = fs.existsSync(envPath) ? parseEnv(fs.readFileSync(envPath, "utf8")) : /* @__PURE__ */ new Map();
|
|
13296
14338
|
let figmaToken = flags.figmaToken ?? existing.get(ENV_KEYS.figma);
|
|
13297
14339
|
let openrouterKey = flags.openrouterKey ?? existing.get(ENV_KEYS.openrouter);
|
|
@@ -13312,7 +14354,7 @@ async function runInit(flags) {
|
|
|
13312
14354
|
next.set(ENV_KEYS.figma, figmaToken);
|
|
13313
14355
|
next.set(ENV_KEYS.openrouter, openrouterKey);
|
|
13314
14356
|
const changed = existing.get(ENV_KEYS.figma) !== next.get(ENV_KEYS.figma) || existing.get(ENV_KEYS.openrouter) !== next.get(ENV_KEYS.openrouter);
|
|
13315
|
-
const gitignorePath =
|
|
14357
|
+
const gitignorePath = path26.resolve(process.cwd(), ".gitignore");
|
|
13316
14358
|
const gitignore = fs.existsSync(gitignorePath) ? fs.readFileSync(gitignorePath, "utf8") : "";
|
|
13317
14359
|
const gitignoreCoversEnv = gitignore.split("\n").some((line) => [".env", ".env", ".env*"].includes(line.trim()));
|
|
13318
14360
|
if (flags.dryRun) {
|
|
@@ -13368,14 +14410,14 @@ init_invocation();
|
|
|
13368
14410
|
init_output();
|
|
13369
14411
|
init_entitlement();
|
|
13370
14412
|
import { confirm as confirm2, isCancel as isCancel2 } from "@clack/prompts";
|
|
13371
|
-
import { readFileSync as
|
|
14413
|
+
import { readFileSync as readFileSync17, readdirSync as readdirSync7, existsSync as existsSync21 } from "node:fs";
|
|
13372
14414
|
|
|
13373
14415
|
// packages/cli/src/pipeline.ts
|
|
13374
14416
|
init_src2();
|
|
13375
14417
|
init_src4();
|
|
13376
14418
|
init_src6();
|
|
13377
14419
|
import { mkdirSync as mkdirSync5, writeFileSync as writeFileSync8 } from "node:fs";
|
|
13378
|
-
import
|
|
14420
|
+
import path27 from "node:path";
|
|
13379
14421
|
|
|
13380
14422
|
// packages/cli/src/assets-module.ts
|
|
13381
14423
|
init_src();
|
|
@@ -13711,7 +14753,7 @@ async function runGenerationPipeline(input) {
|
|
|
13711
14753
|
});
|
|
13712
14754
|
const written = [];
|
|
13713
14755
|
if (!input.dryRun) {
|
|
13714
|
-
const dir =
|
|
14756
|
+
const dir = path27.resolve(input.outDir, semantics.componentName);
|
|
13715
14757
|
mkdirSync5(dir, { recursive: true });
|
|
13716
14758
|
const files = {
|
|
13717
14759
|
// Bundle-local tokens: THE emission the component's CSS resolves
|
|
@@ -13735,13 +14777,13 @@ async function runGenerationPipeline(input) {
|
|
|
13735
14777
|
`
|
|
13736
14778
|
};
|
|
13737
14779
|
for (const [name, content] of Object.entries(files)) {
|
|
13738
|
-
const filePath =
|
|
14780
|
+
const filePath = path27.join(dir, name);
|
|
13739
14781
|
writeFileSync8(filePath, content);
|
|
13740
14782
|
written.push(filePath);
|
|
13741
14783
|
}
|
|
13742
14784
|
for (const artifact of emitTokenArtifacts(input.mapping)) {
|
|
13743
|
-
const filePath =
|
|
13744
|
-
mkdirSync5(
|
|
14785
|
+
const filePath = path27.resolve(input.outDir, artifact.path);
|
|
14786
|
+
mkdirSync5(path27.dirname(filePath), { recursive: true });
|
|
13745
14787
|
writeFileSync8(filePath, artifact.content);
|
|
13746
14788
|
written.push(filePath);
|
|
13747
14789
|
}
|
|
@@ -13800,7 +14842,7 @@ var GENERATE_DESCRIPTION = {
|
|
|
13800
14842
|
function resolveProvidedSource(flags, contextFile) {
|
|
13801
14843
|
let raw;
|
|
13802
14844
|
try {
|
|
13803
|
-
raw =
|
|
14845
|
+
raw = readFileSync17(contextFile, "utf8");
|
|
13804
14846
|
} catch {
|
|
13805
14847
|
fail(flags, ExitCode.InputValidation, {
|
|
13806
14848
|
error: `Cannot read context file "${contextFile}".`,
|
|
@@ -13920,11 +14962,11 @@ token mapping (${mapping.flat.length} variables):
|
|
|
13920
14962
|
let initialCode;
|
|
13921
14963
|
let initialSemantics;
|
|
13922
14964
|
try {
|
|
13923
|
-
if (
|
|
13924
|
-
for (const entry of
|
|
14965
|
+
if (existsSync21(flags.out)) {
|
|
14966
|
+
for (const entry of readdirSync7(flags.out)) {
|
|
13925
14967
|
const cjPath = `${flags.out}/${entry}/component.json`;
|
|
13926
|
-
if (!
|
|
13927
|
-
const cj = JSON.parse(
|
|
14968
|
+
if (!existsSync21(cjPath)) continue;
|
|
14969
|
+
const cj = JSON.parse(readFileSync17(cjPath, "utf8"));
|
|
13928
14970
|
if (cj.name !== void 0 && Array.isArray(cj.props)) {
|
|
13929
14971
|
previousApi = JSON.stringify({
|
|
13930
14972
|
componentName: cj.name,
|
|
@@ -13932,14 +14974,14 @@ token mapping (${mapping.flat.length} variables):
|
|
|
13932
14974
|
});
|
|
13933
14975
|
const tsxPath = `${flags.out}/${entry}/${cj.name}.tsx`;
|
|
13934
14976
|
const cssPath = `${flags.out}/${entry}/${cj.name}.css`;
|
|
13935
|
-
if (flags.refine &&
|
|
14977
|
+
if (flags.refine && existsSync21(tsxPath) && existsSync21(cssPath)) {
|
|
13936
14978
|
initialCode = {
|
|
13937
|
-
tsx:
|
|
13938
|
-
css:
|
|
14979
|
+
tsx: readFileSync17(tsxPath, "utf8"),
|
|
14980
|
+
css: readFileSync17(cssPath, "utf8")
|
|
13939
14981
|
};
|
|
13940
14982
|
const semPath = `${flags.out}/${entry}/semantics.json`;
|
|
13941
|
-
if (
|
|
13942
|
-
initialSemantics = JSON.parse(
|
|
14983
|
+
if (existsSync21(semPath)) {
|
|
14984
|
+
initialSemantics = JSON.parse(readFileSync17(semPath, "utf8"));
|
|
13943
14985
|
}
|
|
13944
14986
|
}
|
|
13945
14987
|
break;
|
|
@@ -14246,7 +15288,7 @@ function buildProgram() {
|
|
|
14246
15288
|
runFontsRequired2({ ...flags, families, cacheDir: cmd.opts()["cache"] ?? DEFAULT_FONT_CACHE });
|
|
14247
15289
|
});
|
|
14248
15290
|
const engine = program.command("engine").description("Agent-harness protocol (ADR-012): the host agent proposes; these commands are the CLI-owned brief and oracle.");
|
|
14249
|
-
engine.command("brief").argument("<taskOrSet>", "reference task name or recording-set directory").option("--bar <bar>", "target bar: pass (0.95) or cert (0.97)", "pass").option("--out <file>", "payload file path (default tendril-out/<name>-brief.md)").option("--model <id>", "declared proposer model (the MCP surface requires it; score refuses without one)").action(async (taskOrSet, _o, cmd) => {
|
|
15291
|
+
engine.command("brief").argument("<taskOrSet>", "reference task name or recording-set directory").option("--bar <bar>", "target bar: pass (0.95) or cert (0.97)", "pass").option("--out <file>", "payload file path (default tendril-out/<name>-brief.md)").option("--model <id>", "declared proposer model (the MCP surface requires it; score refuses without one)").option("--library <dir>", "workspace root holding confirmed partners generated bundles (ADR-013 composed pins; default: current directory)").action(async (taskOrSet, _o, cmd) => {
|
|
14250
15292
|
const flags = globalFlags(cmd.parent.parent);
|
|
14251
15293
|
const local = cmd.opts();
|
|
14252
15294
|
const { runEngineBrief: runEngineBrief2 } = await Promise.resolve().then(() => (init_engine2(), engine_exports));
|
|
@@ -14255,10 +15297,11 @@ function buildProgram() {
|
|
|
14255
15297
|
taskOrSet,
|
|
14256
15298
|
bar: local["bar"] === "cert" ? "cert" : "pass",
|
|
14257
15299
|
...local["out"] !== void 0 ? { out: local["out"] } : {},
|
|
14258
|
-
...local["model"] !== void 0 ? { model: local["model"] } : {}
|
|
15300
|
+
...local["model"] !== void 0 ? { model: local["model"] } : {},
|
|
15301
|
+
...local["library"] !== void 0 ? { library: local["library"] } : {}
|
|
14259
15302
|
});
|
|
14260
15303
|
});
|
|
14261
|
-
engine.command("score").argument("<taskOrSet>", "reference task name or recording-set directory").argument("<candidateDir>", "directory containing the proposed bundle files").option("--bar <bar>", "target bar: pass (0.95) or cert (0.97)", "pass").option("--host <name>", "self-reported host identity (recorded in provenance, labeled self-reported)").requiredOption("--model <id>", "proposer model, self-reported \u2014 required; a score without a declared model is not accepted").option("--rebind", "explicitly re-bind an already-bound bundle to a different recording set (refused otherwise \u2014 rebinding rewrites the bundle's verification identity)").action(async (taskOrSet, candidateDir, _o, cmd) => {
|
|
15304
|
+
engine.command("score").argument("<taskOrSet>", "reference task name or recording-set directory").argument("<candidateDir>", "directory containing the proposed bundle files").option("--bar <bar>", "target bar: pass (0.95) or cert (0.97)", "pass").option("--host <name>", "self-reported host identity (recorded in provenance, labeled self-reported)").requiredOption("--model <id>", "proposer model, self-reported \u2014 required; a score without a declared model is not accepted").option("--rebind", "explicitly re-bind an already-bound bundle to a different recording set (refused otherwise \u2014 rebinding rewrites the bundle's verification identity)").option("--library <dir>", "workspace root holding confirmed partners generated bundles (ADR-013 composed pins; default: current directory)").action(async (taskOrSet, candidateDir, _o, cmd) => {
|
|
14262
15305
|
const flags = globalFlags(cmd.parent.parent);
|
|
14263
15306
|
const local = cmd.opts();
|
|
14264
15307
|
const { runEngineScore: runEngineScore2 } = await Promise.resolve().then(() => (init_engine2(), engine_exports));
|
|
@@ -14269,7 +15312,8 @@ function buildProgram() {
|
|
|
14269
15312
|
bar: local["bar"] === "cert" ? "cert" : "pass",
|
|
14270
15313
|
...local["host"] !== void 0 ? { host: local["host"] } : {},
|
|
14271
15314
|
model: local["model"],
|
|
14272
|
-
rebind: local["rebind"]
|
|
15315
|
+
rebind: local["rebind"],
|
|
15316
|
+
...local["library"] !== void 0 ? { library: local["library"] } : {}
|
|
14273
15317
|
});
|
|
14274
15318
|
});
|
|
14275
15319
|
program.command("codeconnect").description("Emit a Figma Code Connect template (.figma.ts) for a certified bundle \u2014 every variant\u2192prop mapping from recorded truth, stamped with the trust statement. Publishing stays yours (figma connect publish; Org/Enterprise plan).").argument("<bundleDir>", "bundle directory (must carry component.json)").requiredOption("--figma-url <url>", "figma.com /design/ URL of the COMPONENT SET (Copy link to selection)").option("--set <dir>", "recording set override (default: the bundle's provenance path)").option("--out <file>", "output file (default: <bundle>/<Component>.figma.ts)").action(async (bundleDir, _o, cmd) => {
|
|
@@ -14297,6 +15341,19 @@ function buildProgram() {
|
|
|
14297
15341
|
...local["tendrilPrefix"] !== void 0 ? { tendrilPrefix: local["tendrilPrefix"] } : {}
|
|
14298
15342
|
});
|
|
14299
15343
|
});
|
|
15344
|
+
program.command("compose").description("Composition opportunities across a workspace's recording sets (ADR-013): --list discovers (read-only); --set persists a HUMAN's confirm/decline decisions into the host manifest. Nothing composes yet.").option("--list", "derive the index and report edges (default)").option("--library <dir...>", "workspace root(s) to scan for recording sets (default: current directory)").option("--set <dir>", "confirmation mode: propose this HOST set's id-backed pairs for a human decision").option("--confirm-compositions", "HUMAN-ONLY: confirm every open pair printed for --set (refused without an interactive terminal; agents surface the proposal instead)").option("--decline <pair-key...>", "HUMAN-ONLY: persistently decline the named pair(s) \u2014 asked once, never re-asked").action(async (_o, cmd) => {
|
|
15345
|
+
const flags = globalFlags(cmd.parent);
|
|
15346
|
+
const local = cmd.opts();
|
|
15347
|
+
const { runCompose: runCompose2 } = await Promise.resolve().then(() => (init_compose2(), compose_exports));
|
|
15348
|
+
runCompose2({
|
|
15349
|
+
...flags,
|
|
15350
|
+
...local["list"] !== void 0 ? { list: local["list"] } : {},
|
|
15351
|
+
...local["library"] !== void 0 ? { library: local["library"] } : {},
|
|
15352
|
+
...local["set"] !== void 0 ? { set: local["set"] } : {},
|
|
15353
|
+
...local["confirmCompositions"] !== void 0 ? { confirmCompositions: local["confirmCompositions"] } : {},
|
|
15354
|
+
...local["decline"] !== void 0 ? { decline: local["decline"] } : {}
|
|
15355
|
+
});
|
|
15356
|
+
});
|
|
14300
15357
|
program.command("inspect").description("Build an eye-verifiable detail sheet from verify evidence: magnified recorded-vs-rendered crops of every small recorded node (icons, controls, marks) plus full-frame triples, in one static HTML page.").argument("<bundleDir>", "bundle directory with component.json and verify-evidence").option("--set <dir>", "recording set override (default: the bundle's provenance path)").option("--max-area <px2>", "node area ceiling for the detail sweep", "1024").action(async (bundleDir, _o, cmd) => {
|
|
14301
15358
|
const flags = globalFlags(cmd.parent);
|
|
14302
15359
|
const local = cmd.opts();
|
|
@@ -14308,7 +15365,7 @@ function buildProgram() {
|
|
|
14308
15365
|
...local["maxArea"] !== void 0 ? { maxArea: Number(local["maxArea"]) } : {}
|
|
14309
15366
|
});
|
|
14310
15367
|
});
|
|
14311
|
-
program.command("verify").description("Recompute verification for a generated bundle against its recording set (per-config status, behaviors, honest exit codes).").argument("<bundleDir>", "bundle directory to verify").option("--task <name>", "legacy task adapter for bundles WITHOUT component.json (bundle v1 carries its own prop manifest)").option("--set <dir>", "recording set directory (overrides the bundle's provenance path)").option("--bar <bar>", "target bar: pass (0.95) or cert (0.97)", "pass").action(async (bundleDir, _opts, cmd) => {
|
|
15368
|
+
program.command("verify").description("Recompute verification for a generated bundle against its recording set (per-config status, behaviors, honest exit codes).").argument("<bundleDir>", "bundle directory to verify").option("--task <name>", "legacy task adapter for bundles WITHOUT component.json (bundle v1 carries its own prop manifest)").option("--set <dir>", "recording set directory (overrides the bundle's provenance path)").option("--bar <bar>", "target bar: pass (0.95) or cert (0.97)", "pass").option("--library <dir>", "workspace root holding confirmed partners generated bundles (ADR-013 composed pins; default: current directory)").action(async (bundleDir, _opts, cmd) => {
|
|
14312
15369
|
const flags = globalFlags(cmd);
|
|
14313
15370
|
const local = cmd.opts();
|
|
14314
15371
|
const bar = local["bar"] === "cert" ? "cert" : "pass";
|
|
@@ -14318,6 +15375,7 @@ function buildProgram() {
|
|
|
14318
15375
|
bundleDir,
|
|
14319
15376
|
...local["task"] !== void 0 ? { task: local["task"] } : {},
|
|
14320
15377
|
...local["set"] !== void 0 ? { set: local["set"] } : {},
|
|
15378
|
+
...local["library"] !== void 0 ? { library: local["library"] } : {},
|
|
14321
15379
|
bar
|
|
14322
15380
|
});
|
|
14323
15381
|
});
|