@tendrilapp/cli 0.1.47 → 0.1.49
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/SKILL.md +85 -9
- package/dist/tendril-mcp.js +10 -6
- package/dist/tendril.js +671 -301
- package/package.json +1 -1
package/dist/tendril.js
CHANGED
|
@@ -842,9 +842,12 @@ import { existsSync } from "node:fs";
|
|
|
842
842
|
import path from "node:path";
|
|
843
843
|
import { z as z4 } from "zod";
|
|
844
844
|
function resolveRepEnvelopePath(setDir, slug, kind) {
|
|
845
|
-
|
|
845
|
+
return resolveRepEnvelopePathIn(path.join(setDir, slug), kind);
|
|
846
|
+
}
|
|
847
|
+
function resolveRepEnvelopePathIn(repDir, kind) {
|
|
848
|
+
const mcp = path.join(repDir, kind === "metadata" ? "get_metadata.json" : "get_screenshot.json");
|
|
846
849
|
if (existsSync(mcp)) return mcp;
|
|
847
|
-
const rest = path.join(
|
|
850
|
+
const rest = path.join(repDir, kind === "metadata" ? REST_METADATA_FILE : REST_SCREENSHOT_FILE);
|
|
848
851
|
return existsSync(rest) ? rest : mcp;
|
|
849
852
|
}
|
|
850
853
|
function repEnvelopeExists(setDir, slug, kind) {
|
|
@@ -1670,7 +1673,7 @@ function effectivelyInvisible(node) {
|
|
|
1670
1673
|
return false;
|
|
1671
1674
|
}
|
|
1672
1675
|
function repVisibility(setDir, slug) {
|
|
1673
|
-
const file = path3.join(setDir, slug, "
|
|
1676
|
+
const file = resolveRepEnvelopePathIn(path3.join(setDir, slug), "metadata");
|
|
1674
1677
|
if (!existsSync3(file)) return void 0;
|
|
1675
1678
|
let roots;
|
|
1676
1679
|
try {
|
|
@@ -1721,6 +1724,7 @@ var init_visibility = __esm({
|
|
|
1721
1724
|
"packages/figma/src/recording/visibility.ts"() {
|
|
1722
1725
|
"use strict";
|
|
1723
1726
|
init_normalize();
|
|
1727
|
+
init_rest_envelopes();
|
|
1724
1728
|
init_session();
|
|
1725
1729
|
normalizedLayerName = (name) => name.toLowerCase().replace(/[^a-z0-9]/g, "");
|
|
1726
1730
|
invisibleInEveryPose = (v) => v.presentIn.length > 0 && v.visibleIn.length === 0;
|
|
@@ -1778,7 +1782,7 @@ function buildComposeIndex(roots, depth = 3) {
|
|
|
1778
1782
|
for (const rep of manifest.reps) {
|
|
1779
1783
|
variantNodeIds.add(rep.nodeId);
|
|
1780
1784
|
repSlugByVariantNode.set(rep.nodeId, rep.slug);
|
|
1781
|
-
const metaFile = path4.join(dir, rep.slug, "
|
|
1785
|
+
const metaFile = resolveRepEnvelopePathIn(path4.join(dir, rep.slug), "metadata");
|
|
1782
1786
|
if (!existsSync4(metaFile)) continue;
|
|
1783
1787
|
try {
|
|
1784
1788
|
const text = envelopeTextContent(JSON.parse(readFileSync3(metaFile, "utf8")));
|
|
@@ -1837,8 +1841,31 @@ function emissionTails(setDir, repSlug) {
|
|
|
1837
1841
|
}
|
|
1838
1842
|
return byHead;
|
|
1839
1843
|
}
|
|
1844
|
+
function restInstancePoses(setDir, repSlug, nodeId) {
|
|
1845
|
+
const repDir = path4.join(setDir, repSlug);
|
|
1846
|
+
if (!resolveRepEnvelopePathIn(repDir, "metadata").endsWith(REST_METADATA_FILE)) return /* @__PURE__ */ new Map();
|
|
1847
|
+
const file = path4.join(repDir, REST_NODES_FILE);
|
|
1848
|
+
if (!existsSync4(file)) return /* @__PURE__ */ new Map();
|
|
1849
|
+
let doc;
|
|
1850
|
+
try {
|
|
1851
|
+
const parsed = JSON.parse(readFileSync3(file, "utf8"));
|
|
1852
|
+
doc = parsed.nodes?.[nodeId]?.document;
|
|
1853
|
+
} catch {
|
|
1854
|
+
return /* @__PURE__ */ new Map();
|
|
1855
|
+
}
|
|
1856
|
+
const out = /* @__PURE__ */ new Map();
|
|
1857
|
+
const walk2 = (n) => {
|
|
1858
|
+
if (n.type === "INSTANCE") {
|
|
1859
|
+
if (typeof n.id === "string" && typeof n.componentId === "string") out.set(n.id, n.componentId);
|
|
1860
|
+
return;
|
|
1861
|
+
}
|
|
1862
|
+
if (Array.isArray(n.children)) for (const c of n.children) walk2(c);
|
|
1863
|
+
};
|
|
1864
|
+
if (doc !== null && typeof doc === "object") walk2(doc);
|
|
1865
|
+
return out;
|
|
1866
|
+
}
|
|
1840
1867
|
function hostInstances(setDir, repSlug) {
|
|
1841
|
-
const metaFile = path4.join(setDir, repSlug, "
|
|
1868
|
+
const metaFile = resolveRepEnvelopePathIn(path4.join(setDir, repSlug), "metadata");
|
|
1842
1869
|
if (!existsSync4(metaFile)) return [];
|
|
1843
1870
|
try {
|
|
1844
1871
|
const text = envelopeTextContent(JSON.parse(readFileSync3(metaFile, "utf8")));
|
|
@@ -1867,10 +1894,11 @@ function composeReport(index) {
|
|
|
1867
1894
|
}
|
|
1868
1895
|
}
|
|
1869
1896
|
for (const [variantNodeId, slug] of host.repSlugByVariantNode) {
|
|
1870
|
-
void variantNodeId;
|
|
1871
1897
|
const instances = hostInstances(host.dir, slug);
|
|
1872
1898
|
if (instances.length === 0) continue;
|
|
1873
1899
|
const tailsByHead = emissionTails(host.dir, slug);
|
|
1900
|
+
const cidByInstance = restInstancePoses(host.dir, slug, variantNodeId);
|
|
1901
|
+
const repIsMcpRecorded = !resolveRepEnvelopePathIn(path4.join(host.dir, slug), "metadata").endsWith(REST_METADATA_FILE);
|
|
1874
1902
|
for (const inst of instances) {
|
|
1875
1903
|
if (visibleEver.get(inst.id) !== true) {
|
|
1876
1904
|
edges.push({
|
|
@@ -1887,10 +1915,12 @@ function composeReport(index) {
|
|
|
1887
1915
|
continue;
|
|
1888
1916
|
}
|
|
1889
1917
|
const tails = tailsByHead.get(inst.id) ?? /* @__PURE__ */ new Map();
|
|
1918
|
+
const cid = cidByInstance.get(inst.id);
|
|
1890
1919
|
const disclosures = [];
|
|
1891
1920
|
const refused = [];
|
|
1892
1921
|
const idCands = index.filter((c) => {
|
|
1893
1922
|
if (c.dir === host.dir || sameComponent(c, host)) return false;
|
|
1923
|
+
if (cid !== void 0 && c.variantNodeIds.has(cid)) return true;
|
|
1894
1924
|
for (const t of tails.keys()) if (c.ownIds.has(t)) return true;
|
|
1895
1925
|
return false;
|
|
1896
1926
|
});
|
|
@@ -1938,7 +1968,15 @@ function composeReport(index) {
|
|
|
1938
1968
|
partners: nameProps.map((p) => ({ dir: p.dir, displayName: p.displayName, ...p.figmaFile !== void 0 ? { figmaFile: p.figmaFile } : {} })),
|
|
1939
1969
|
disclosures: [
|
|
1940
1970
|
...disclosures,
|
|
1941
|
-
`NAME-ONLY proposal \u2014 identity unproven, name evidence never auto-joins: instance "${inst.name}" names ${nameProps.map(kitLabel).join(" / ")}. Name-only edges are NOT confirmable (there is nothing sound to confirm); record the partner so an id-backed join can form, or ignore
|
|
1971
|
+
`NAME-ONLY proposal \u2014 identity unproven, name evidence never auto-joins: instance "${inst.name}" names ${nameProps.map(kitLabel).join(" / ")}. Name-only edges are NOT confirmable (there is nothing sound to confirm); record the partner so an id-backed join can form, or ignore`,
|
|
1972
|
+
// The remediation is CONDITIONAL (review): componentId
|
|
1973
|
+
// is a host-file-namespace id, so the promise holds
|
|
1974
|
+
// only for same-file partners — and only when the host
|
|
1975
|
+
// rep was recorded over MCP (a REST-recorded rep with
|
|
1976
|
+
// no binding genuinely has none to capture).
|
|
1977
|
+
...repIsMcpRecorded && host.figmaFile !== void 0 && nameProps.some((p) => p.figmaFile === host.figmaFile) ? [
|
|
1978
|
+
`this host records over MCP, so Figma's instance\u2192component binding was never captured \u2014 re-recording the HOST over the REST channel makes this pair id-backed (a fresh recording, then a full host regeneration and re-verify)`
|
|
1979
|
+
] : []
|
|
1942
1980
|
]
|
|
1943
1981
|
});
|
|
1944
1982
|
} else if (disclosures.length > 0) {
|
|
@@ -1961,8 +1999,10 @@ function composeReport(index) {
|
|
|
1961
1999
|
const group = eligible[0];
|
|
1962
2000
|
const ownedDepths = [];
|
|
1963
2001
|
for (const [t, d] of tails) if (group.some((m) => m.ownIds.has(t))) ownedDepths.push(d);
|
|
1964
|
-
const
|
|
2002
|
+
const cidBacked = cid !== void 0 && group.some((m) => m.variantNodeIds.has(cid));
|
|
2003
|
+
const substitution = (ownedDepths.length > 0 || cidBacked) && ownedDepths.every((d) => d === 1);
|
|
1965
2004
|
const poseVariants = /* @__PURE__ */ new Set();
|
|
2005
|
+
if (cidBacked) poseVariants.add(cid);
|
|
1966
2006
|
for (const m of group) {
|
|
1967
2007
|
for (const [t, d] of tails) {
|
|
1968
2008
|
if (d !== 1 || !m.ownIds.has(t)) continue;
|
|
@@ -1973,6 +2013,9 @@ function composeReport(index) {
|
|
|
1973
2013
|
}
|
|
1974
2014
|
}
|
|
1975
2015
|
}
|
|
2016
|
+
if (cid !== void 0 && !cidBacked && ownedDepths.length > 0) {
|
|
2017
|
+
disclosures.push(`Figma's instance binding names an UNRECORDED variant (${cid}) \u2014 this join stands on emission-id evidence alone`);
|
|
2018
|
+
}
|
|
1976
2019
|
for (const p of nameProps) {
|
|
1977
2020
|
if (!group.some((m) => sameComponent(m, p))) {
|
|
1978
2021
|
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`);
|
|
@@ -2016,7 +2059,7 @@ function composeReport(index) {
|
|
|
2016
2059
|
},
|
|
2017
2060
|
disclosures: [
|
|
2018
2061
|
...disclosures,
|
|
2019
|
-
|
|
2062
|
+
`substitution-grade (${[...ownedDepths.length > 0 ? ["every owned emission id at depth 1"] : [], ...cidBacked ? ["Figma's instance\u2192component binding (componentId), recorded verbatim over REST"] : []].join(" + ")}); pixel-neutrality is NOT asserted here \u2014 instance overrides are measured real, and a per-region check is still future work (whole-frame pixels remain the evidence for composed regions)`
|
|
2020
2063
|
]
|
|
2021
2064
|
});
|
|
2022
2065
|
}
|
|
@@ -2104,7 +2147,7 @@ function confirmedCompositionStatus(hostSet) {
|
|
|
2104
2147
|
status: ok ? stale ? "stale-supported" : "supported" : stale ? "stale-unsupported" : "unsupported",
|
|
2105
2148
|
instances: entry.instances,
|
|
2106
2149
|
affectedReps: ok ? [] : [...new Set(unsupported.map((i) => i.hostRep))],
|
|
2107
|
-
detail: ok ? stale ? "the partner manifest changed since the decision (pinned bytes differ) \u2014 the CURRENT recordings still support every confirmed edge" : "the recordings support every confirmed edge (identity, rep attribution and pose re-derived)" : `${unsupported.length} confirmed instance(s) are NOT supported by the current recordings (${unsupported.map((i) => `${i.hostRep}/${i.instanceId}`).join(", ")}) \u2014 a confirmed composition the evidence does not derive; ${remediation}. If the instance is HIDDEN in every recorded pose, this confirmation predates the visibility principle (Cycle A) and re-running compose cannot re-open it (asked-once): either re-record a pose that SHOWS the instance, or remove this entry from the manifest's compositions array by hand \u2014 a retire flag is a named follow-up`
|
|
2150
|
+
detail: ok ? stale ? "the partner manifest changed since the decision (pinned bytes differ) \u2014 the CURRENT recordings still support every confirmed edge" : "the recordings support every confirmed edge (identity, rep attribution and pose re-derived)" : `${unsupported.length} confirmed instance(s) are NOT supported by the current recordings (${unsupported.map((i) => `${i.hostRep}/${i.instanceId}`).join(", ")}) \u2014 a confirmed composition the evidence does not derive; ${remediation}. If the instance is HIDDEN in every recorded pose, this confirmation predates the visibility principle (Cycle A) and re-running compose cannot re-open it (asked-once): either re-record a pose that SHOWS the instance, or remove this entry from the manifest's compositions array by hand \u2014 a retire flag is a named follow-up${unsupported.some((i) => !existsSync4(path4.join(hostSet, i.hostRep, REST_NODES_FILE))) ? ". If this pair was confirmed from REST-recorded evidence and the host was since re-recorded over MCP, the evidence CLASS is gone rather than broken \u2014 re-record the host over the REST channel to restore it" : ""}`
|
|
2108
2151
|
});
|
|
2109
2152
|
}
|
|
2110
2153
|
return { rows, ...malformedEntries.length > 0 ? { malformed: malformedEntries.join("; ") } : {} };
|
|
@@ -2112,11 +2155,111 @@ function confirmedCompositionStatus(hostSet) {
|
|
|
2112
2155
|
function createHashHex(bytes) {
|
|
2113
2156
|
return createHash2("sha256").update(bytes).digest("hex");
|
|
2114
2157
|
}
|
|
2158
|
+
function liteIdentity(dir) {
|
|
2159
|
+
try {
|
|
2160
|
+
const manifest = loadManifest(dir);
|
|
2161
|
+
return {
|
|
2162
|
+
dir,
|
|
2163
|
+
displayName: manifest.component,
|
|
2164
|
+
...manifest.figmaFile !== void 0 ? { figmaFile: manifest.figmaFile } : {},
|
|
2165
|
+
...manifest.componentSetNode !== void 0 ? { componentSetNode: manifest.componentSetNode } : {},
|
|
2166
|
+
variantNodeIds: new Set(manifest.reps.map((r) => r.nodeId)),
|
|
2167
|
+
repSlugByVariantNode: new Map(manifest.reps.map((r) => [r.nodeId, r.slug])),
|
|
2168
|
+
ownIdsByRep: /* @__PURE__ */ new Map(),
|
|
2169
|
+
ownIds: /* @__PURE__ */ new Set()
|
|
2170
|
+
};
|
|
2171
|
+
} catch {
|
|
2172
|
+
return void 0;
|
|
2173
|
+
}
|
|
2174
|
+
}
|
|
2175
|
+
function partnerOpportunities(partnerSet, roots) {
|
|
2176
|
+
const abs = path4.resolve(partnerSet);
|
|
2177
|
+
const index = buildComposeIndex([.../* @__PURE__ */ new Set([...roots.map((r) => path4.resolve(r)), abs])]);
|
|
2178
|
+
const partnerEntry = index.find((e) => path4.resolve(e.dir) === abs);
|
|
2179
|
+
if (partnerEntry === void 0) return { opportunities: [], hostsWithUnreadableDecisions: [] };
|
|
2180
|
+
const entryFor = (dir) => index.find((e) => path4.resolve(e.dir) === path4.resolve(dir)) ?? liteIdentity(path4.resolve(dir));
|
|
2181
|
+
const isPartner = (dirs) => dirs.some((p) => {
|
|
2182
|
+
if (path4.resolve(p.dir) === abs) return true;
|
|
2183
|
+
const e = entryFor(p.dir);
|
|
2184
|
+
return e !== void 0 && sameComponent(e, partnerEntry);
|
|
2185
|
+
});
|
|
2186
|
+
const edges = composeReport(index);
|
|
2187
|
+
const byHost = /* @__PURE__ */ new Map();
|
|
2188
|
+
for (const e of edges) {
|
|
2189
|
+
if (path4.resolve(e.hostSet) === abs) continue;
|
|
2190
|
+
if (e.kind !== "substitution" && e.kind !== "ask" && e.kind !== "proposal") continue;
|
|
2191
|
+
if (!isPartner(e.partners)) continue;
|
|
2192
|
+
const list = byHost.get(e.hostSet) ?? [];
|
|
2193
|
+
list.push(e);
|
|
2194
|
+
byHost.set(e.hostSet, list);
|
|
2195
|
+
}
|
|
2196
|
+
const opportunities = [];
|
|
2197
|
+
const unreadable = [];
|
|
2198
|
+
for (const [hostSet, hostEdges] of byHost) {
|
|
2199
|
+
let decided = false;
|
|
2200
|
+
let malformed = 0;
|
|
2201
|
+
try {
|
|
2202
|
+
const raw = JSON.parse(readFileSync3(path4.join(hostSet, "recording-set.json"), "utf8"));
|
|
2203
|
+
for (const entry of Array.isArray(raw["compositions"]) ? raw["compositions"] : []) {
|
|
2204
|
+
const parsed = CompositionEntrySchema.safeParse(entry);
|
|
2205
|
+
if (!parsed.success) {
|
|
2206
|
+
malformed += 1;
|
|
2207
|
+
continue;
|
|
2208
|
+
}
|
|
2209
|
+
for (const rel of Object.keys(parsed.data.partner.manifestSha256).map(fromStoredRel)) {
|
|
2210
|
+
const e = entryFor(path4.resolve(hostSet, rel));
|
|
2211
|
+
if (e !== void 0 && sameComponent(e, partnerEntry)) decided = true;
|
|
2212
|
+
}
|
|
2213
|
+
}
|
|
2214
|
+
} catch {
|
|
2215
|
+
malformed += 1;
|
|
2216
|
+
}
|
|
2217
|
+
if (malformed > 0) unreadable.push({ hostSet, unreadable: malformed });
|
|
2218
|
+
if (decided) continue;
|
|
2219
|
+
const hostName = index.find((e) => path4.resolve(e.dir) === path4.resolve(hostSet))?.displayName ?? path4.basename(hostSet);
|
|
2220
|
+
const byKind = { substitution: [], ask: [], proposal: [] };
|
|
2221
|
+
for (const e of hostEdges) byKind[e.kind].push(e);
|
|
2222
|
+
if (byKind.substitution.length > 0) {
|
|
2223
|
+
const dirs = byKind.substitution[0].partners.map((p) => p.dir);
|
|
2224
|
+
opportunities.push({
|
|
2225
|
+
hostSet,
|
|
2226
|
+
hostDisplayName: hostName,
|
|
2227
|
+
kind: "confirmable",
|
|
2228
|
+
pairKey: pairKeyFor(path4.resolve(hostSet), dirs),
|
|
2229
|
+
instances: byKind.substitution.length,
|
|
2230
|
+
hostReps: [...new Set(byKind.substitution.map((e) => e.hostRep))],
|
|
2231
|
+
disclosures: [...new Set(byKind.substitution.flatMap((e) => e.disclosures))]
|
|
2232
|
+
});
|
|
2233
|
+
}
|
|
2234
|
+
if (byKind.ask.length > 0) {
|
|
2235
|
+
opportunities.push({
|
|
2236
|
+
hostSet,
|
|
2237
|
+
hostDisplayName: hostName,
|
|
2238
|
+
kind: "ambiguous",
|
|
2239
|
+
instances: byKind.ask.length,
|
|
2240
|
+
hostReps: [...new Set(byKind.ask.map((e) => e.hostRep))],
|
|
2241
|
+
disclosures: [...new Set(byKind.ask.flatMap((e) => e.disclosures))]
|
|
2242
|
+
});
|
|
2243
|
+
}
|
|
2244
|
+
if (byKind.proposal.length > 0) {
|
|
2245
|
+
opportunities.push({
|
|
2246
|
+
hostSet,
|
|
2247
|
+
hostDisplayName: hostName,
|
|
2248
|
+
kind: "name-only",
|
|
2249
|
+
instances: byKind.proposal.length,
|
|
2250
|
+
hostReps: [...new Set(byKind.proposal.map((e) => e.hostRep))],
|
|
2251
|
+
disclosures: [...new Set(byKind.proposal.flatMap((e) => e.disclosures))]
|
|
2252
|
+
});
|
|
2253
|
+
}
|
|
2254
|
+
}
|
|
2255
|
+
return { opportunities, hostsWithUnreadableDecisions: unreadable };
|
|
2256
|
+
}
|
|
2115
2257
|
var CompositionEntrySchema, norm, FOOTER, kitLabel, toPosixRel, fromStoredRel, pairKeyFor;
|
|
2116
2258
|
var init_compose = __esm({
|
|
2117
2259
|
"packages/figma/src/recording/compose.ts"() {
|
|
2118
2260
|
"use strict";
|
|
2119
2261
|
init_session();
|
|
2262
|
+
init_rest_envelopes();
|
|
2120
2263
|
init_normalize();
|
|
2121
2264
|
init_visibility();
|
|
2122
2265
|
CompositionEntrySchema = z6.object({
|
|
@@ -6647,7 +6790,7 @@ var init_bundle_files = __esm({
|
|
|
6647
6790
|
// packages/metadata/src/verify-report.ts
|
|
6648
6791
|
function verifyReportForTransport(report, bundleName) {
|
|
6649
6792
|
const { dir: _dir, ...evidence } = report.evidence;
|
|
6650
|
-
const { eyeCheck: _eyeCheck, ...rest } = report;
|
|
6793
|
+
const { eyeCheck: _eyeCheck, next: _next, composeOpportunities: _composeOpportunities, ...rest } = report;
|
|
6651
6794
|
return { ...rest, bundle: bundleName, evidence };
|
|
6652
6795
|
}
|
|
6653
6796
|
function nonEmptyArray(value) {
|
|
@@ -11631,7 +11774,8 @@ var init_brief = __esm({
|
|
|
11631
11774
|
import { existsSync as existsSync29, readFileSync as readFileSync26, readdirSync as readdirSync10 } from "node:fs";
|
|
11632
11775
|
import path36 from "node:path";
|
|
11633
11776
|
function repText(set, rep, tool) {
|
|
11634
|
-
const
|
|
11777
|
+
const file = tool === "get_metadata" ? resolveRepEnvelopePath(set, rep, "metadata") : tool === "get_screenshot" ? resolveRepEnvelopePath(set, rep, "screenshot") : path36.join(set, rep, `${tool}.json`);
|
|
11778
|
+
const env = JSON.parse(readFileSync26(file, "utf8"));
|
|
11635
11779
|
return tool === "get_design_context" ? envelopeFirstTextPart(env) : envelopeTextContent(env);
|
|
11636
11780
|
}
|
|
11637
11781
|
function refPngDims(set, rep) {
|
|
@@ -11791,7 +11935,6 @@ var init_segments = __esm({
|
|
|
11791
11935
|
"packages/generate/src/segments.ts"() {
|
|
11792
11936
|
"use strict";
|
|
11793
11937
|
init_src();
|
|
11794
|
-
init_src();
|
|
11795
11938
|
cssIdent = (name) => name.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "");
|
|
11796
11939
|
}
|
|
11797
11940
|
});
|
|
@@ -14434,7 +14577,16 @@ function compositionPairsFor(hostSet, roots) {
|
|
|
14434
14577
|
else invalid++;
|
|
14435
14578
|
}
|
|
14436
14579
|
const decidedKeys = new Set(standing.map((c) => fromStoredRel(c.partner.key)));
|
|
14437
|
-
|
|
14580
|
+
const proposalRows = /* @__PURE__ */ new Map();
|
|
14581
|
+
for (const e of edges) {
|
|
14582
|
+
if (e.hostSet !== hostSet || e.kind !== "proposal") continue;
|
|
14583
|
+
const key = e.partners.map((p) => p.dir).sort().join("+");
|
|
14584
|
+
const row = proposalRows.get(key) ?? { displayName: e.partners[0]?.displayName ?? e.instanceName, partnerDirs: e.partners.map((p) => p.dir), instances: 0, disclosures: [] };
|
|
14585
|
+
row.instances += 1;
|
|
14586
|
+
for (const d of e.disclosures) if (!row.disclosures.includes(d)) row.disclosures.push(d);
|
|
14587
|
+
proposalRows.set(key, row);
|
|
14588
|
+
}
|
|
14589
|
+
return { open: pairs.filter((p) => !decidedKeys.has(p.key)), proposals: [...proposalRows.values()], standing, invalid, ...skippedParent !== void 0 ? { skippedParent } : {} };
|
|
14438
14590
|
}
|
|
14439
14591
|
function substitutionPairs(edges, hostSet) {
|
|
14440
14592
|
const pairs = /* @__PURE__ */ new Map();
|
|
@@ -14879,7 +15031,7 @@ async function runRecordPlan(opts) {
|
|
|
14879
15031
|
...opts.figmaFile !== void 0 ? { figmaFile: opts.figmaFile } : {},
|
|
14880
15032
|
...setIdentity
|
|
14881
15033
|
});
|
|
14882
|
-
const channelNote = await assignRestChannel(opts.setDir, manifest, resumed);
|
|
15034
|
+
const channelNote = await assignRestChannel(opts.setDir, manifest, resumed, opts.restClient);
|
|
14883
15035
|
const defaultReports = reportAxisDefaults(symbols, Object.keys(defaults).length > 0 ? defaults : void 0);
|
|
14884
15036
|
const toConfirm = resumed ? [] : defaultReports.filter((r) => (r.rule === "non-interaction" || r.rule === "frequency") && r.domain.length > 1);
|
|
14885
15037
|
const interactionStatesToConfirm = recordsInteractionState(defaultReports) ? void 0 : interactionDisclosure(manifest.component, defaultReports);
|
|
@@ -15069,7 +15221,7 @@ async function runRecordPlan(opts) {
|
|
|
15069
15221
|
}
|
|
15070
15222
|
);
|
|
15071
15223
|
}
|
|
15072
|
-
async function assignRestChannel(setDir, manifest, resumed) {
|
|
15224
|
+
async function assignRestChannel(setDir, manifest, resumed, injected) {
|
|
15073
15225
|
const current = loadManifest(setDir);
|
|
15074
15226
|
if (current.channel !== void 0) {
|
|
15075
15227
|
return { active: true, probeReps: current.probeReps ?? [], note: "REST channel already assigned (frozen with the plan)" };
|
|
@@ -15085,7 +15237,7 @@ async function assignRestChannel(setDir, manifest, resumed) {
|
|
|
15085
15237
|
note: "REST channel unavailable: the plan carries no --figma-file key \u2014 pass it (from the design URL) to record pixels+geometry over Figma REST and escape the MCP daily limit"
|
|
15086
15238
|
};
|
|
15087
15239
|
}
|
|
15088
|
-
const client = await figmaRestClient();
|
|
15240
|
+
const client = injected !== void 0 ? { ok: true, value: injected } : await figmaRestClient();
|
|
15089
15241
|
if (!client.ok) {
|
|
15090
15242
|
return client.kind === "no-credential" ? {
|
|
15091
15243
|
active: false,
|
|
@@ -15146,7 +15298,7 @@ async function runRecordRestFetch(opts) {
|
|
|
15146
15298
|
remediation: `Record them first: ${tendrilCommand(`record next --set ${opts.setDir}`)} names the calls.`
|
|
15147
15299
|
});
|
|
15148
15300
|
}
|
|
15149
|
-
const client = await figmaRestClient();
|
|
15301
|
+
const client = opts.restClient !== void 0 ? { ok: true, value: opts.restClient } : await figmaRestClient();
|
|
15150
15302
|
if (!client.ok) {
|
|
15151
15303
|
fail(opts, ExitCode.General, { error: client.refusal, code: `figma-rest-${client.kind}`, remediation: client.kind === "no-credential" ? `Run ${tendrilCommand("figma-connect")} once, then re-run this.` : "Fix what is named above, or fall back: remove `channel`/`probeReps` from recording-set.json to record over MCP." });
|
|
15152
15304
|
}
|
|
@@ -15649,9 +15801,14 @@ function runRecordStatus(opts) {
|
|
|
15649
15801
|
const composition = (() => {
|
|
15650
15802
|
try {
|
|
15651
15803
|
const setDir = path45.resolve(opts.setDir);
|
|
15652
|
-
const { open, standing, invalid } = compositionPairsFor(setDir, [path45.dirname(setDir)]);
|
|
15804
|
+
const { open, proposals, standing, invalid } = compositionPairsFor(setDir, [path45.dirname(setDir)]);
|
|
15653
15805
|
return {
|
|
15654
15806
|
openPairs: open.map((p) => ({ displayName: p.displayName, pairKey: p.key, instances: p.instances.length })),
|
|
15807
|
+
// Name-only proposals (field, 2026-08-26): the host-side
|
|
15808
|
+
// surface an MCP-recorded host embedding a later partner never
|
|
15809
|
+
// had — not confirmable, but never silent either; the
|
|
15810
|
+
// disclosures carry the conditional re-record remediation.
|
|
15811
|
+
nameOnly: proposals.map((p) => ({ displayName: p.displayName, instances: p.instances, disclosures: p.disclosures })),
|
|
15655
15812
|
confirmed: standing.filter((s) => s.status === "confirmed").length,
|
|
15656
15813
|
declined: standing.filter((s) => s.status === "declined").length,
|
|
15657
15814
|
invalid
|
|
@@ -15685,6 +15842,12 @@ function runRecordStatus(opts) {
|
|
|
15685
15842
|
process.stdout.write(`COMPOSITION ${composition.confirmed} confirmed partner pair(s) on this set (composed pins apply at generation)
|
|
15686
15843
|
`);
|
|
15687
15844
|
}
|
|
15845
|
+
if (!("unavailable" in composition) && composition.nameOnly.length > 0) {
|
|
15846
|
+
for (const p of composition.nameOnly) {
|
|
15847
|
+
process.stdout.write(`COMPOSITION NAME-ONLY: instance name matches recorded component ${p.displayName} (${p.instances} instance(s)) \u2014 identity unproven, NOT confirmable. ${p.disclosures.find((d) => d.includes("re-record")) ?? "Record id evidence to make it confirmable, or ignore."}
|
|
15848
|
+
`);
|
|
15849
|
+
}
|
|
15850
|
+
}
|
|
15688
15851
|
if (!("unavailable" in composition) && composition.invalid > 0) {
|
|
15689
15852
|
process.stdout.write(
|
|
15690
15853
|
`COMPOSITION WARNING: ${composition.invalid} standing composition entr${composition.invalid === 1 ? "y is" : "ies are"} MALFORMED in recording-set.json \u2014 a corrupted DECLINE silently re-opens its pair; repair or remove the entry (it holds a human decision and is provenance-hashed)
|
|
@@ -16250,6 +16413,177 @@ var init_profile_input = __esm({
|
|
|
16250
16413
|
}
|
|
16251
16414
|
});
|
|
16252
16415
|
|
|
16416
|
+
// packages/cli/src/commands/inspect.ts
|
|
16417
|
+
var inspect_exports = {};
|
|
16418
|
+
__export(inspect_exports, {
|
|
16419
|
+
INSPECT_DESCRIPTION: () => INSPECT_DESCRIPTION,
|
|
16420
|
+
buildInspectSheet: () => buildInspectSheet,
|
|
16421
|
+
runInspect: () => runInspect
|
|
16422
|
+
});
|
|
16423
|
+
import { existsSync as existsSync39, readFileSync as readFileSync35, writeFileSync as writeFileSync17 } from "node:fs";
|
|
16424
|
+
import path49 from "node:path";
|
|
16425
|
+
function readVerifyReport(evidenceDir) {
|
|
16426
|
+
const p = path49.join(evidenceDir, VERIFY_REPORT_FILENAME);
|
|
16427
|
+
if (!existsSync39(p)) return void 0;
|
|
16428
|
+
try {
|
|
16429
|
+
return JSON.parse(readFileSync35(p, "utf8"));
|
|
16430
|
+
} catch {
|
|
16431
|
+
return void 0;
|
|
16432
|
+
}
|
|
16433
|
+
}
|
|
16434
|
+
function verdictBlock(report) {
|
|
16435
|
+
if (report === void 0) {
|
|
16436
|
+
return `<div class="verdict none"><h2>No verdict on file</h2><p>This evidence carries no <code>${VERIFY_REPORT_FILENAME}</code>, so these images are unlabelled pixels \u2014 nothing here says what passed. Re-run <code>tendril verify</code> to write one.</p></div>`;
|
|
16437
|
+
}
|
|
16438
|
+
const buckets = /* @__PURE__ */ new Map();
|
|
16439
|
+
for (const c of report.configs) buckets.set(c.status, (buckets.get(c.status) ?? 0) + 1);
|
|
16440
|
+
const split = [...buckets.entries()].map(([k, n]) => `<span class="tier t-${esc(k)}">${n} ${esc(k)}</span>`).join("");
|
|
16441
|
+
const failed = report.behaviors.filter((b) => !b.pass);
|
|
16442
|
+
return `<div class="verdict ${report.verdict === "verified" ? "ok" : "bad"}"><h2>${esc(report.verdict)} <small>at bar \u201C${esc(report.targetBar)}\u201D</small></h2><p class="tiers">${split}<span class="tier">${report.behaviors.length - failed.length}/${report.behaviors.length} behaviors</span></p>` + (failed.length > 0 ? `<ul class="fails">${failed.map((b) => `<li><code>${esc(b.id)}</code>${b.detail !== void 0 ? ` \u2014 ${esc(b.detail)}` : ""}</li>`).join("")}</ul>` : "") + (report.verdictCaveats.length > 0 ? `<h3>What was never asked</h3><ul class="caveats">${report.verdictCaveats.map((c) => `<li>${esc(c)}</li>`).join("")}</ul>` : `<p class="caveats">No caveats: every question this bar asks was answered.</p>`) + `</div>`;
|
|
16443
|
+
}
|
|
16444
|
+
function scoreLine(report, rep) {
|
|
16445
|
+
const c = report?.configs.find((x) => x.rep === rep);
|
|
16446
|
+
if (c === void 0) return "";
|
|
16447
|
+
const ex = c.exact !== void 0 ? ` <span class="exact">measured ${c.exact.similarity.toFixed(6)} / ${c.exact.inkRecall.toFixed(6)}</span>` : "";
|
|
16448
|
+
const why = Array.isArray(c.demotedBy) && c.demotedBy.length > 0 ? `<span class="demoted">demoted: ${esc(c.demotedBy.join("; "))}</span>` : "";
|
|
16449
|
+
const err = typeof c.error === "string" ? `<span class="demoted">${esc(c.error)}</span>` : "";
|
|
16450
|
+
return `<p class="scores"><span class="tier t-${esc(c.status)}">${esc(c.status)}</span> sim ${c.similarity} \xB7 ink ${c.inkRecall}${ex} ${why} ${err}</p>`;
|
|
16451
|
+
}
|
|
16452
|
+
async function runInspect(opts) {
|
|
16453
|
+
if (opts.describe) {
|
|
16454
|
+
printDescription(INSPECT_DESCRIPTION);
|
|
16455
|
+
return;
|
|
16456
|
+
}
|
|
16457
|
+
const bundleDir = path49.resolve(opts.bundleDir);
|
|
16458
|
+
const evidenceDir = path49.join(bundleDir, "verify-evidence");
|
|
16459
|
+
const manifestPath2 = path49.join(bundleDir, "component.json");
|
|
16460
|
+
if (!existsSync39(evidenceDir) || !existsSync39(manifestPath2)) {
|
|
16461
|
+
fail(opts, ExitCode.InputValidation, {
|
|
16462
|
+
error: `nothing to inspect in ${bundleDir} \u2014 ${existsSync39(manifestPath2) ? "no verify-evidence directory" : "no component.json"}`,
|
|
16463
|
+
code: "no-evidence",
|
|
16464
|
+
remediation: `Run \`${tendrilCommand(`verify ${bundleDir}`)}\` first \u2014 inspect reads the ref/render evidence that run writes.`
|
|
16465
|
+
});
|
|
16466
|
+
}
|
|
16467
|
+
const { manifest } = readBundleManifest(readFileSync35(manifestPath2, "utf8"));
|
|
16468
|
+
if (manifest === void 0) {
|
|
16469
|
+
fail(opts, ExitCode.InputValidation, {
|
|
16470
|
+
error: "component.json did not parse as a bundle manifest",
|
|
16471
|
+
code: "no-mount-contract",
|
|
16472
|
+
remediation: "Re-emit the bundle (score/verify rewrite component.json), then re-run inspect."
|
|
16473
|
+
});
|
|
16474
|
+
}
|
|
16475
|
+
const setDir = path49.resolve(opts.set ?? manifest.provenance.recordingSet.path);
|
|
16476
|
+
const report = readVerifyReport(evidenceDir);
|
|
16477
|
+
const reps = Object.keys(manifest.propAdapter).filter((rep) => existsSync39(path49.join(evidenceDir, `${rep}-ref.png`)) && existsSync39(path49.join(evidenceDir, `${rep}-render.png`)));
|
|
16478
|
+
if (reps.length === 0) {
|
|
16479
|
+
fail(opts, ExitCode.InputValidation, {
|
|
16480
|
+
error: "verify-evidence holds no ref/render pairs for this bundle's configs",
|
|
16481
|
+
code: "no-evidence",
|
|
16482
|
+
remediation: `Run \`${tendrilCommand(`verify ${bundleDir}`)}\` first \u2014 inspect reads the ref/render evidence that run writes.`
|
|
16483
|
+
});
|
|
16484
|
+
}
|
|
16485
|
+
const { sheet, crops } = buildInspectSheet({ bundleDir, setDir, title: manifest.name, repCandidates: reps, report, ...opts.maxArea !== void 0 ? { maxArea: opts.maxArea } : {} });
|
|
16486
|
+
emitData(opts, { sheet, configs: reps.length, crops }, () => {
|
|
16487
|
+
process.stdout.write(`inspect sheet: ${sheet}
|
|
16488
|
+
${reps.length} config(s), ${crops} detail crop pair(s) \u2014 open the sheet and scan recorded vs rendered
|
|
16489
|
+
`);
|
|
16490
|
+
});
|
|
16491
|
+
}
|
|
16492
|
+
function buildInspectSheet(input) {
|
|
16493
|
+
const evidenceDir = path49.join(path49.resolve(input.bundleDir), "verify-evidence");
|
|
16494
|
+
const setDir = input.setDir;
|
|
16495
|
+
const report = input.report;
|
|
16496
|
+
const reps = input.repCandidates.filter((rep) => existsSync39(path49.join(evidenceDir, `${rep}-ref.png`)) && existsSync39(path49.join(evidenceDir, `${rep}-render.png`)));
|
|
16497
|
+
let crops = 0;
|
|
16498
|
+
const sections = [];
|
|
16499
|
+
for (const rep of reps) {
|
|
16500
|
+
const ref = new Uint8Array(readFileSync35(path49.join(evidenceDir, `${rep}-ref.png`)));
|
|
16501
|
+
const render = new Uint8Array(readFileSync35(path49.join(evidenceDir, `${rep}-render.png`)));
|
|
16502
|
+
const nodes = smallSemanticNodes(setDir, rep, input.maxArea ?? 1024).slice(0, 12);
|
|
16503
|
+
const cells = [];
|
|
16504
|
+
for (const [i, n] of nodes.entries()) {
|
|
16505
|
+
const rect = { x: n.x, y: n.y, w: n.w, h: n.h };
|
|
16506
|
+
try {
|
|
16507
|
+
writeFileSync17(path49.join(evidenceDir, `${rep}-inspect-${i}-ref.png`), zoomCrop(ref, rect));
|
|
16508
|
+
writeFileSync17(path49.join(evidenceDir, `${rep}-inspect-${i}-render.png`), zoomCrop(render, rect));
|
|
16509
|
+
} catch {
|
|
16510
|
+
continue;
|
|
16511
|
+
}
|
|
16512
|
+
crops += 1;
|
|
16513
|
+
cells.push(
|
|
16514
|
+
`<figure><figcaption>${esc(n.name)} <code>${esc(n.id)}</code> \xB7 ${n.w}\xD7${n.h}</figcaption><div class="pair"><span><em>recorded</em><img src="./${rep}-inspect-${i}-ref.png" alt="recorded ${esc(n.name)}"></span><span><em>rendered</em><img src="./${rep}-inspect-${i}-render.png" alt="rendered ${esc(n.name)}"></span></div></figure>`
|
|
16515
|
+
);
|
|
16516
|
+
}
|
|
16517
|
+
sections.push(
|
|
16518
|
+
`<section><h2>${esc(rep)}</h2>` + scoreLine(report, rep) + `<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>`
|
|
16519
|
+
);
|
|
16520
|
+
}
|
|
16521
|
+
for (const c of report?.configs ?? []) {
|
|
16522
|
+
if (reps.includes(c.rep)) continue;
|
|
16523
|
+
sections.push(`<section class="missing"><h2>${esc(c.rep)}</h2>${scoreLine(report, c.rep)}<p class="none">No evidence images for this config \u2014 it was scored, but nothing was captured to look at.</p></section>`);
|
|
16524
|
+
}
|
|
16525
|
+
const sheet = path49.join(evidenceDir, "inspect.html");
|
|
16526
|
+
writeFileSync17(
|
|
16527
|
+
sheet,
|
|
16528
|
+
`<!doctype html><meta charset="utf-8"><title>${esc(input.title)} \u2014 tendril inspect</title><style>
|
|
16529
|
+
body{font:14px/1.45 system-ui,sans-serif;margin:24px;background:#fff;color:#111}
|
|
16530
|
+
h1{font-size:20px} h2{font-size:16px;border-top:1px solid #ddd;padding-top:16px}
|
|
16531
|
+
.pair,.full{display:flex;gap:12px;flex-wrap:wrap;align-items:flex-start}
|
|
16532
|
+
.full img{max-width:400px;border:1px solid #ccc} .pair img{border:1px solid #ccc;image-rendering:pixelated}
|
|
16533
|
+
figure{margin:0 0 16px} figcaption{margin-bottom:4px} em{display:block;color:#666;font-style:normal;font-size:12px}
|
|
16534
|
+
.grid{display:flex;flex-wrap:wrap;gap:20px;margin-top:12px} .none{color:#666}
|
|
16535
|
+
.verdict{border:1px solid #ddd;border-left-width:5px;padding:12px 16px;margin:16px 0;background:#fafafa}
|
|
16536
|
+
.verdict.ok{border-left-color:#1a7f37} .verdict.bad{border-left-color:#b35900} .verdict.none{border-left-color:#999}
|
|
16537
|
+
.verdict h2{border:0;padding:0;margin:0 0 8px} .verdict h3{font-size:13px;margin:12px 0 4px;color:#444}
|
|
16538
|
+
.verdict small{font-weight:400;color:#666}
|
|
16539
|
+
.tiers{margin:0;display:flex;gap:8px;flex-wrap:wrap}
|
|
16540
|
+
.tier{display:inline-block;padding:1px 8px;border:1px solid #ccc;border-radius:10px;font-size:12px;background:#fff}
|
|
16541
|
+
.t-certified{border-color:#1a7f37;color:#1a7f37} .t-pass{border-color:#8a6d00;color:#8a6d00} .t-fail{border-color:#b3261e;color:#b3261e}
|
|
16542
|
+
.caveats,.fails{margin:4px 0 0;padding-left:20px;color:#444} .caveats li,.fails li{margin:2px 0}
|
|
16543
|
+
.scores{margin:4px 0 10px;color:#333} .exact{color:#666;font-size:12px} .demoted{color:#b3261e;font-size:12px;margin-left:8px}
|
|
16544
|
+
.missing{opacity:.85} .legend{white-space:pre-wrap;font:12px/1.4 ui-monospace,SFMono-Regular,Menlo,monospace;background:#fafafa;border:1px solid #ddd;padding:12px;overflow-x:auto}
|
|
16545
|
+
</style><h1>${esc(input.title)} \u2014 verdict and evidence</h1>
|
|
16546
|
+
${verdictBlock(report)}
|
|
16547
|
+
<p>Below, every crop is a recorded node small enough that global metrics weight it as a rounding error.
|
|
16548
|
+
Scan the pairs: anything present on the left and missing/invisible on the right is a defect,
|
|
16549
|
+
whatever the scores said. Verdicts come from <code>tendril verify</code> \u2014 this sheet only shows.</p>
|
|
16550
|
+
${sections.join("\n")}
|
|
16551
|
+
<section><h2>Diff colours</h2><div class="legend">${esc(DIFF_LEGEND_TEXT)}</div></section>
|
|
16552
|
+
`
|
|
16553
|
+
);
|
|
16554
|
+
return { sheet, configs: reps.length, crops };
|
|
16555
|
+
}
|
|
16556
|
+
var INSPECT_DESCRIPTION, esc;
|
|
16557
|
+
var init_inspect = __esm({
|
|
16558
|
+
"packages/cli/src/commands/inspect.ts"() {
|
|
16559
|
+
"use strict";
|
|
16560
|
+
init_src3();
|
|
16561
|
+
init_src4();
|
|
16562
|
+
init_src5();
|
|
16563
|
+
init_describe();
|
|
16564
|
+
init_invocation();
|
|
16565
|
+
init_output();
|
|
16566
|
+
INSPECT_DESCRIPTION = {
|
|
16567
|
+
name: "inspect",
|
|
16568
|
+
summary: "Build an eye-verifiable detail sheet from verify evidence: magnified ref-vs-render crops of every small recorded node (icons, controls, marks).",
|
|
16569
|
+
args: [{ name: "bundleDir", required: true, description: "bundle directory (must carry component.json and a verify-evidence dir from a prior `tendril verify`)" }],
|
|
16570
|
+
flags: [
|
|
16571
|
+
{ flag: "--set <dir>", description: "recording set override (default: the bundle's provenance path)" },
|
|
16572
|
+
{ flag: "--max-area <px2>", description: "node area ceiling for the detail sweep", default: "1024" },
|
|
16573
|
+
{ flag: "--json", description: "Machine-readable output" }
|
|
16574
|
+
],
|
|
16575
|
+
output: {
|
|
16576
|
+
sheet: "string \u2014 path to the generated inspect.html",
|
|
16577
|
+
configs: "number \u2014 configs with evidence found",
|
|
16578
|
+
crops: "number \u2014 detail crop pairs written"
|
|
16579
|
+
},
|
|
16580
|
+
exitCodes: { 0: "sheet written", 3: "no verify evidence to inspect (run `tendril verify` first)" },
|
|
16581
|
+
examples: ["tendril inspect ./src/components/Banner", "tendril inspect ./bundle --set ./tendril/recordings/banner"]
|
|
16582
|
+
};
|
|
16583
|
+
esc = (s) => s.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """);
|
|
16584
|
+
}
|
|
16585
|
+
});
|
|
16586
|
+
|
|
16253
16587
|
// packages/cli/src/commands/verify.ts
|
|
16254
16588
|
var verify_exports = {};
|
|
16255
16589
|
__export(verify_exports, {
|
|
@@ -16273,8 +16607,8 @@ __export(verify_exports, {
|
|
|
16273
16607
|
runVerify: () => runVerify,
|
|
16274
16608
|
verdictCaveatsFor: () => verdictCaveatsFor
|
|
16275
16609
|
});
|
|
16276
|
-
import { existsSync as
|
|
16277
|
-
import
|
|
16610
|
+
import { existsSync as existsSync40, readFileSync as readFileSync36, readdirSync as readdirSync16, rmSync as rmSync7, writeFileSync as writeFileSync18 } from "node:fs";
|
|
16611
|
+
import path50 from "node:path";
|
|
16278
16612
|
function interactionCoverage(behaviors) {
|
|
16279
16613
|
const parity = behaviors.filter((b) => b.id.startsWith("parity:"));
|
|
16280
16614
|
const content = behaviors.filter((b) => b.id.startsWith("content-prop-renders("));
|
|
@@ -16512,15 +16846,17 @@ function compositionReport(input) {
|
|
|
16512
16846
|
crops: input.crops ?? { unavailable: cropsUnavailable }
|
|
16513
16847
|
};
|
|
16514
16848
|
}
|
|
16515
|
-
function eyeCheck(bundleDir) {
|
|
16516
|
-
|
|
16849
|
+
function eyeCheck(bundleDir, sheet) {
|
|
16850
|
+
const base = {
|
|
16517
16851
|
command: tendrilCommand(`inspect "${bundleDir}"`),
|
|
16518
|
-
sheetPath:
|
|
16852
|
+
sheetPath: path50.join(bundleDir, "verify-evidence", "inspect.html"),
|
|
16519
16853
|
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."
|
|
16520
16854
|
};
|
|
16855
|
+
if (sheet === void 0) return base;
|
|
16856
|
+
return { ...base, sheetBuilt: sheet.built, ...sheet.crops !== void 0 ? { sheetCrops: sheet.crops } : {}, ...sheet.error !== void 0 ? { sheetBuildError: sheet.error } : {} };
|
|
16521
16857
|
}
|
|
16522
16858
|
function evidenceArtifacts(evidenceDir, reps) {
|
|
16523
|
-
const named = (name) =>
|
|
16859
|
+
const named = (name) => existsSync40(path50.join(evidenceDir, name)) ? name : null;
|
|
16524
16860
|
return {
|
|
16525
16861
|
legend: named("diff-legend.txt"),
|
|
16526
16862
|
configs: reps.map((rep) => {
|
|
@@ -16568,7 +16904,7 @@ function taskFromManifest(opts, manifest, setDir) {
|
|
|
16568
16904
|
const adapterSlugs = Object.keys(manifest.propAdapter);
|
|
16569
16905
|
const unmapped = recordedSlugs.filter((s) => !adapterSlugs.includes(s));
|
|
16570
16906
|
const adapterOnly = adapterSlugs.filter((s) => !recordedSlugs.includes(s));
|
|
16571
|
-
const registry = Object.values(TASKS).find((t) =>
|
|
16907
|
+
const registry = Object.values(TASKS).find((t) => path50.resolve(t.set) === path50.resolve(setDir));
|
|
16572
16908
|
const authored = (() => {
|
|
16573
16909
|
if (registry !== void 0) return void 0;
|
|
16574
16910
|
try {
|
|
@@ -16629,19 +16965,19 @@ function verdictCaveatsFor(input) {
|
|
|
16629
16965
|
async function runVerify(opts) {
|
|
16630
16966
|
const callerCwd = process.env["INIT_CWD"] ?? process.cwd();
|
|
16631
16967
|
let recordingSetDrift;
|
|
16632
|
-
const setOverride = opts.set !== void 0 ?
|
|
16633
|
-
opts = { ...opts, bundleDir:
|
|
16634
|
-
if (!
|
|
16968
|
+
const setOverride = opts.set !== void 0 ? path50.resolve(callerCwd, opts.set) : void 0;
|
|
16969
|
+
opts = { ...opts, bundleDir: path50.resolve(callerCwd, opts.bundleDir), ...setOverride !== void 0 ? { set: setOverride } : {} };
|
|
16970
|
+
if (!existsSync40(opts.bundleDir)) {
|
|
16635
16971
|
fail(opts, ExitCode.InputValidation, {
|
|
16636
16972
|
error: `bundle directory not found: ${opts.bundleDir}`,
|
|
16637
16973
|
code: "bundle-missing",
|
|
16638
16974
|
remediation: "Point at a generated bundle directory (containing component.json, the entry module, and styles.css)."
|
|
16639
16975
|
});
|
|
16640
16976
|
}
|
|
16641
|
-
const manifestPath2 =
|
|
16977
|
+
const manifestPath2 = path50.join(opts.bundleDir, "component.json");
|
|
16642
16978
|
let manifest;
|
|
16643
|
-
if (
|
|
16644
|
-
const { manifest: parsed, issues } = readBundleManifest(
|
|
16979
|
+
if (existsSync40(manifestPath2)) {
|
|
16980
|
+
const { manifest: parsed, issues } = readBundleManifest(readFileSync36(manifestPath2, "utf8"));
|
|
16645
16981
|
if (issues.length > 0) {
|
|
16646
16982
|
fail(opts, ExitCode.InputValidation, {
|
|
16647
16983
|
error: `bundle manifest rejected at ingest: ${issues.map((i) => i.message).join("; ")}`,
|
|
@@ -16672,21 +17008,21 @@ async function runVerify(opts) {
|
|
|
16672
17008
|
task = registry;
|
|
16673
17009
|
} else if (manifest !== void 0) {
|
|
16674
17010
|
const resolveSetDir = (p) => {
|
|
16675
|
-
if (
|
|
16676
|
-
const fromRepo =
|
|
16677
|
-
if (
|
|
16678
|
-
return
|
|
17011
|
+
if (path50.isAbsolute(p)) return p;
|
|
17012
|
+
const fromRepo = path50.resolve(REPO_ROOT, p);
|
|
17013
|
+
if (existsSync40(fromRepo)) return fromRepo;
|
|
17014
|
+
return path50.resolve(callerCwd, p);
|
|
16679
17015
|
};
|
|
16680
17016
|
const setDir = opts.set ?? resolveSetDir(manifest.provenance.recordingSet.path);
|
|
16681
|
-
if (!
|
|
17017
|
+
if (!existsSync40(path50.join(setDir, "recording-set.json")) && !Object.values(TASKS).some((t) => path50.resolve(t.set) === path50.resolve(setDir))) {
|
|
16682
17018
|
fail(opts, ExitCode.RecordingIncomplete, {
|
|
16683
17019
|
error: `recording set not found or unmanifested: ${setDir}`,
|
|
16684
17020
|
code: "recording-set-missing",
|
|
16685
17021
|
remediation: "Pass --set <dir> pointing at the recording set this bundle was generated from."
|
|
16686
17022
|
});
|
|
16687
17023
|
}
|
|
16688
|
-
const registry = Object.values(TASKS).find((t) =>
|
|
16689
|
-
if (registry !== void 0 && !
|
|
17024
|
+
const registry = Object.values(TASKS).find((t) => path50.resolve(t.set) === path50.resolve(setDir));
|
|
17025
|
+
if (registry !== void 0 && !existsSync40(path50.join(setDir, "recording-set.json"))) {
|
|
16690
17026
|
const recordedSlugs = registry.configs.map((c) => c.rep);
|
|
16691
17027
|
const adapterSlugs = Object.keys(manifest.propAdapter);
|
|
16692
17028
|
unmapped = recordedSlugs.filter((s) => !adapterSlugs.includes(s));
|
|
@@ -16718,9 +17054,9 @@ async function runVerify(opts) {
|
|
|
16718
17054
|
recordingSetDrift = { stamped: manifest.provenance.recordingSet.hash, current: hash };
|
|
16719
17055
|
}
|
|
16720
17056
|
for (const name of [manifest.entry, "styles.css", "tokens.css"]) {
|
|
16721
|
-
const p =
|
|
16722
|
-
if (!
|
|
16723
|
-
const issues = checkBundleSourceFile(name, new Uint8Array(
|
|
17057
|
+
const p = path50.join(opts.bundleDir, name);
|
|
17058
|
+
if (!existsSync40(p)) continue;
|
|
17059
|
+
const issues = checkBundleSourceFile(name, new Uint8Array(readFileSync36(p)));
|
|
16724
17060
|
if (issues.length > 0) {
|
|
16725
17061
|
fail(opts, ExitCode.InputValidation, {
|
|
16726
17062
|
error: `bundle source rejected at ingest: ${issues.map((i) => i.message).join("; ")}`,
|
|
@@ -16768,7 +17104,7 @@ async function runVerify(opts) {
|
|
|
16768
17104
|
});
|
|
16769
17105
|
}
|
|
16770
17106
|
const bar = BARS2[opts.bar];
|
|
16771
|
-
const evidenceDir =
|
|
17107
|
+
const evidenceDir = path50.join(opts.bundleDir, "verify-evidence");
|
|
16772
17108
|
rmSync7(evidenceDir, { recursive: true, force: true });
|
|
16773
17109
|
const scores = await scoreBundleForTask(task, opts.bundleDir, bar, { evidenceDir, onProgress: emitProgress });
|
|
16774
17110
|
const quality = await checkBundleQuality(
|
|
@@ -16786,7 +17122,7 @@ async function runVerify(opts) {
|
|
|
16786
17122
|
// ASKED, never "follows every convention".
|
|
16787
17123
|
loadCodebaseProfile(opts, opts.profile) ?? void 0
|
|
16788
17124
|
);
|
|
16789
|
-
const bundleCss = ["tokens.css", "styles.css"].map((f) =>
|
|
17125
|
+
const bundleCss = ["tokens.css", "styles.css"].map((f) => path50.join(opts.bundleDir, f)).filter((f) => existsSync40(f)).map((f) => readFileSync36(f, "utf8")).join("\n");
|
|
16790
17126
|
const occlusion = await checkSiblingOcclusion(task, opts.bundleDir, bundleCss, { ...authorityConfigs !== void 0 ? { authority: authorityConfigs } : {} });
|
|
16791
17127
|
const parity = await checkHoverParity(task, opts.bundleDir, authorityConfigs ?? task.configs, {
|
|
16792
17128
|
...opts.hoverTimeoutMs !== void 0 ? { hoverBudgetMs: opts.hoverTimeoutMs } : {},
|
|
@@ -16807,10 +17143,10 @@ async function runVerify(opts) {
|
|
|
16807
17143
|
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.`);
|
|
16808
17144
|
}
|
|
16809
17145
|
const verifyCallerCwd = process.env["INIT_CWD"] ?? process.cwd();
|
|
16810
|
-
const verifyPins = crossComposition.rows.length > 0 ? composedPins(task.set, [opts.library !== void 0 ?
|
|
17146
|
+
const verifyPins = crossComposition.rows.length > 0 ? composedPins(task.set, [opts.library !== void 0 ? path50.resolve(verifyCallerCwd, opts.library) : verifyCallerCwd]) : { pins: [], issues: [] };
|
|
16811
17147
|
const staticComposedChecks = composedChecks(opts.bundleDir, task.entry, verifyPins.pins);
|
|
16812
17148
|
const renderExpectations = verifyPins.pins.map((pin) => ({
|
|
16813
|
-
modulePath:
|
|
17149
|
+
modulePath: path50.join(composedModuleDir(pin.partnerName), pin.entryComponent),
|
|
16814
17150
|
component: pin.entryComponent,
|
|
16815
17151
|
stampName: `${pin.pairKey}:${pin.entryComponent}`,
|
|
16816
17152
|
hostReps: [...new Set(pin.instances.map((i) => i.hostRep))],
|
|
@@ -16869,6 +17205,59 @@ async function runVerify(opts) {
|
|
|
16869
17205
|
})();
|
|
16870
17206
|
const compositionBlock = compositionReport({ availability, structural, crops, regions: regionsOut, composedPairs: composedPairs.size });
|
|
16871
17207
|
const scoredFiles = digestScoredFiles(opts.bundleDir, manifest !== void 0 ? manifest.entry : task.entry);
|
|
17208
|
+
const verdictWord = ok ? "verified" : "verification-failed";
|
|
17209
|
+
const verdictCaveats = verdictCaveatsFor({
|
|
17210
|
+
latticeBlock,
|
|
17211
|
+
operability: coverage.operability,
|
|
17212
|
+
compositionUnavailable: "unavailable" in compositionBlock,
|
|
17213
|
+
fontsSubstituted: substitutedFamilies.length > 0,
|
|
17214
|
+
pixelOnlyInteractionPoses: unmappedInteractionEvidence.length,
|
|
17215
|
+
hoverBudgetNondefault: opts.hoverTimeoutMs !== void 0 && opts.hoverTimeoutMs !== DEFAULT_HOVER_BUDGET_MS
|
|
17216
|
+
});
|
|
17217
|
+
const composeScan = (() => {
|
|
17218
|
+
if (!ok || opts.bar === "cert" && substitutedFamilies.length > 0) return void 0;
|
|
17219
|
+
try {
|
|
17220
|
+
const setAbs = path50.resolve(task.set);
|
|
17221
|
+
let scannedRoots;
|
|
17222
|
+
let skipped;
|
|
17223
|
+
if (opts.library !== void 0) {
|
|
17224
|
+
scannedRoots = [path50.resolve(opts.library)];
|
|
17225
|
+
} else {
|
|
17226
|
+
const parent = path50.dirname(setAbs);
|
|
17227
|
+
let entries = 0;
|
|
17228
|
+
try {
|
|
17229
|
+
entries = readdirSync16(parent).length;
|
|
17230
|
+
} catch {
|
|
17231
|
+
}
|
|
17232
|
+
if (entries > IMPLICIT_PARENT_SCAN_MAX_ENTRIES) {
|
|
17233
|
+
skipped = { dir: parent, entries };
|
|
17234
|
+
scannedRoots = [];
|
|
17235
|
+
} else {
|
|
17236
|
+
scannedRoots = [parent];
|
|
17237
|
+
}
|
|
17238
|
+
}
|
|
17239
|
+
const scan = scannedRoots.length > 0 ? partnerOpportunities(setAbs, scannedRoots) : { opportunities: [], hostsWithUnreadableDecisions: [] };
|
|
17240
|
+
return { scannedRoots, ...skipped !== void 0 ? { skipped } : {}, ...scan };
|
|
17241
|
+
} catch (err) {
|
|
17242
|
+
return { scannedRoots: [], opportunities: [], hostsWithUnreadableDecisions: [], error: err instanceof Error ? err.message.split("\n")[0] ?? String(err) : String(err) };
|
|
17243
|
+
}
|
|
17244
|
+
})();
|
|
17245
|
+
const green = ok && !(opts.bar === "cert" && substitutedFamilies.length > 0);
|
|
17246
|
+
const sheetOutcome = (() => {
|
|
17247
|
+
if (!green) return { built: false };
|
|
17248
|
+
try {
|
|
17249
|
+
const built = buildInspectSheet({
|
|
17250
|
+
bundleDir: opts.bundleDir,
|
|
17251
|
+
setDir: task.set,
|
|
17252
|
+
title: manifest !== void 0 ? manifest.name : path50.basename(opts.bundleDir),
|
|
17253
|
+
repCandidates: statuses.map((s) => s.rep),
|
|
17254
|
+
report: { configs: statuses, behaviors, verdict: verdictWord, targetBar: opts.bar, verdictCaveats }
|
|
17255
|
+
});
|
|
17256
|
+
return { built: true, crops: built.crops };
|
|
17257
|
+
} catch (error) {
|
|
17258
|
+
return { built: false, error: error instanceof Error ? error.message.split("\n")[0] ?? String(error) : String(error) };
|
|
17259
|
+
}
|
|
17260
|
+
})();
|
|
16872
17261
|
const report = {
|
|
16873
17262
|
bundle: opts.bundleDir,
|
|
16874
17263
|
scoredFiles,
|
|
@@ -16962,7 +17351,7 @@ async function runVerify(opts) {
|
|
|
16962
17351
|
behaviors,
|
|
16963
17352
|
evidence: { dir: evidenceDir, ...evidenceArtifacts(evidenceDir, statuses.map((s) => s.rep)) },
|
|
16964
17353
|
composition: compositionBlock,
|
|
16965
|
-
verdict:
|
|
17354
|
+
verdict: verdictWord,
|
|
16966
17355
|
// Run-23 R5: the one-word verdict printed beside "coverage
|
|
16967
17356
|
// denominator unknown" and "operability unverified" read, to a
|
|
16968
17357
|
// JSON consumer who stops at `verdict`, as fully verified — the
|
|
@@ -16971,14 +17360,7 @@ async function runVerify(opts) {
|
|
|
16971
17360
|
// consumer cannot claim it was never offered. Exit codes are
|
|
16972
17361
|
// unchanged, deliberately: each claim in the report is true; the
|
|
16973
17362
|
// caveats say which questions were never answered.
|
|
16974
|
-
verdictCaveats
|
|
16975
|
-
latticeBlock,
|
|
16976
|
-
operability: coverage.operability,
|
|
16977
|
-
compositionUnavailable: "unavailable" in compositionBlock,
|
|
16978
|
-
fontsSubstituted: substitutedFamilies.length > 0,
|
|
16979
|
-
pixelOnlyInteractionPoses: unmappedInteractionEvidence.length,
|
|
16980
|
-
hoverBudgetNondefault: opts.hoverTimeoutMs !== void 0 && opts.hoverTimeoutMs !== DEFAULT_HOVER_BUDGET_MS
|
|
16981
|
-
}),
|
|
17363
|
+
verdictCaveats,
|
|
16982
17364
|
// Machine-readable cause (adversarial review): a cert-bar failure
|
|
16983
17365
|
// with zero pixel/behavior/composition failures was only
|
|
16984
17366
|
// explainable from stderr prose.
|
|
@@ -16991,7 +17373,25 @@ async function runVerify(opts) {
|
|
|
16991
17373
|
motion: motionDisclosure(opts.bundleDir, task.set),
|
|
16992
17374
|
...recordingSetDrift !== void 0 ? { recordingSetDrift } : {},
|
|
16993
17375
|
...substitutedFamilies.length > 0 ? { substitutedFamilies } : {},
|
|
16994
|
-
eyeCheck: eyeCheck(opts.bundleDir)
|
|
17376
|
+
eyeCheck: eyeCheck(opts.bundleDir, sheetOutcome),
|
|
17377
|
+
// Field handoff 2026-08-26 (problem 2): a verified component's
|
|
17378
|
+
// value lands when it is LIVE — the terminal step of a green run
|
|
17379
|
+
// is publishing, and the report says so in the machine channel
|
|
17380
|
+
// too. Verify itself stays account-less and network-less
|
|
17381
|
+
// (ruler-authority invariant 3): this is a pointer, never a call.
|
|
17382
|
+
// Stdout-only guidance — verifyReportForTransport strips it, like
|
|
17383
|
+
// eyeCheck, before the report is persisted or uploaded.
|
|
17384
|
+
...green ? {
|
|
17385
|
+
next: {
|
|
17386
|
+
action: "publish",
|
|
17387
|
+
command: tendrilCommand(`publish "${opts.bundleDir}"`),
|
|
17388
|
+
mcpTools: ["tendril_publish", "tendril_publish_wait (repeat while it returns approval-pending)"],
|
|
17389
|
+
note: "MCP hosts: use the tools, never the shell command \u2014 the split phases keep the approve link in hand while the wait polls. A FIRST publish waits for the user's Approve in their signed-in browser (the CLI cannot approve); a re-publish of an already-published component completes in one call"
|
|
17390
|
+
}
|
|
17391
|
+
} : {},
|
|
17392
|
+
// Stdout-only, like `next` — verifyReportForTransport strips it
|
|
17393
|
+
// (absolute host paths must never travel to a published page).
|
|
17394
|
+
...composeScan !== void 0 ? { composeOpportunities: composeScan } : {}
|
|
16995
17395
|
};
|
|
16996
17396
|
emitData(opts, report, () => {
|
|
16997
17397
|
for (const s of statuses) {
|
|
@@ -17123,7 +17523,7 @@ ${certified}/${statuses.length} certified \xB7 ${report.coverage.pass}/${statuse
|
|
|
17123
17523
|
}
|
|
17124
17524
|
process.stdout.write(`environment: chrome=${report.environment.chromeVersion ?? report.environment.chrome} fonts=${report.environment.fontsManifestSha256 ?? "UNRESOLVED"}
|
|
17125
17525
|
`);
|
|
17126
|
-
const shippedFonts = requiredFontsManifest(cssFontFamilies(["styles.css", "tokens.css"].map((f) =>
|
|
17526
|
+
const shippedFonts = requiredFontsManifest(cssFontFamilies(["styles.css", "tokens.css"].map((f) => path50.join(opts.bundleDir, f)).filter((f) => existsSync40(f)).map((f) => readFileSync36(f, "utf8")).join("\n")));
|
|
17127
17527
|
if (shippedFonts.length > 0 && substitutedFamilies.length === 0) {
|
|
17128
17528
|
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)
|
|
17129
17529
|
`);
|
|
@@ -17136,8 +17536,58 @@ ${certified}/${statuses.length} certified \xB7 ${report.coverage.pass}/${statuse
|
|
|
17136
17536
|
}
|
|
17137
17537
|
process.stdout.write(`evidence: ${evidenceDir} (render/ref/diff per config)
|
|
17138
17538
|
`);
|
|
17139
|
-
|
|
17539
|
+
if (report.eyeCheck.sheetBuilt === true) {
|
|
17540
|
+
process.stdout.write(`eye check: sheet WRITTEN at ${report.eyeCheck.sheetPath} (${report.eyeCheck.sheetCrops ?? 0} detail crop pair(s)) \u2014 magnified recorded-vs-rendered crops of every small node; scores cannot see shape. Open it; ${report.eyeCheck.command} rebuilds it.
|
|
17541
|
+
`);
|
|
17542
|
+
} else {
|
|
17543
|
+
const buildNote = report.eyeCheck.sheetBuildError !== void 0 ? ` (this run tried to build it and could not: ${report.eyeCheck.sheetBuildError})` : "";
|
|
17544
|
+
process.stdout.write(`eye check: ${report.eyeCheck.command} \u2014 magnified recorded-vs-rendered crops of every small node; scores cannot see shape (re-run after every verify)${buildNote}
|
|
17545
|
+
`);
|
|
17546
|
+
}
|
|
17547
|
+
if (green) {
|
|
17548
|
+
process.stdout.write(
|
|
17549
|
+
`
|
|
17550
|
+
READY this component is verified \u2014 the run is not finished until it is LIVE: ${tendrilCommand(`publish "${opts.bundleDir}"`)} publishes it (a FIRST publish waits for the user's Approve in their signed-in browser \u2014 the CLI cannot approve; a re-publish completes in one call)
|
|
17551
|
+
`
|
|
17552
|
+
);
|
|
17553
|
+
}
|
|
17554
|
+
if (composeScan !== void 0) {
|
|
17555
|
+
const OPP_SHOWN = 5;
|
|
17556
|
+
if (composeScan.error !== void 0) {
|
|
17557
|
+
process.stdout.write(`COMPOSE discovery UNAVAILABLE (${composeScan.error}) \u2014 whether other recordings embed this component is UNKNOWN, not "no"
|
|
17140
17558
|
`);
|
|
17559
|
+
}
|
|
17560
|
+
for (const o of composeScan.opportunities.slice(0, OPP_SHOWN)) {
|
|
17561
|
+
const hostQ = quoteArg(o.hostSet);
|
|
17562
|
+
if (o.kind === "confirmable") {
|
|
17563
|
+
process.stdout.write(
|
|
17564
|
+
`COMPOSE ${o.hostDisplayName} [${o.hostSet}] embeds this component in ${o.instances} instance(s) across ${o.hostReps.length} pose(s) \u2014 id-backed and confirmable. A human decides it, in their terminal, against the HOST set: ${tendrilCommand(`compose --set ${hostQ}`)} (decline instead with ${tendrilCommand(`compose --set ${hostQ} --decline ${o.pairKey ?? ""}`)}). After confirming, regenerate the HOST bundle (an agent run: brief \u2192 generate \u2192 score \u2192 verify, with --library pointing at the workspace holding this bundle) \u2014 composition never retrofits an existing bundle.
|
|
17565
|
+
`
|
|
17566
|
+
);
|
|
17567
|
+
} else if (o.kind === "ambiguous") {
|
|
17568
|
+
process.stdout.write(
|
|
17569
|
+
`COMPOSE ${o.hostDisplayName} [${o.hostSet}]: id evidence reaches this component AMBIGUOUSLY (${o.instances} instance(s)) \u2014 the confirm channel cannot take it yet; a human looks via ${tendrilCommand("compose --list")}
|
|
17570
|
+
`
|
|
17571
|
+
);
|
|
17572
|
+
} else {
|
|
17573
|
+
process.stdout.write(`COMPOSE ${o.hostDisplayName} [${o.hostSet}]: name-only match (${o.instances} instance(s)) \u2014 identity unproven, not confirmable. ${o.disclosures.find((d) => d.includes("re-record")) ?? "Id evidence is needed for a pairing."}
|
|
17574
|
+
`);
|
|
17575
|
+
}
|
|
17576
|
+
}
|
|
17577
|
+
if (composeScan.opportunities.length > OPP_SHOWN) {
|
|
17578
|
+
process.stdout.write(`COMPOSE \u2026 and ${composeScan.opportunities.length - OPP_SHOWN} more \u2014 ${tendrilCommand("compose --list")} shows the full report
|
|
17579
|
+
`);
|
|
17580
|
+
}
|
|
17581
|
+
for (const u of composeScan.hostsWithUnreadableDecisions) {
|
|
17582
|
+
process.stdout.write(`COMPOSE NOTE: ${u.hostSet} has ${u.unreadable} unreadable standing composition entr${u.unreadable === 1 ? "y" : "ies"} \u2014 decisions may exist that this scan cannot see
|
|
17583
|
+
`);
|
|
17584
|
+
}
|
|
17585
|
+
process.stdout.write(
|
|
17586
|
+
composeScan.skipped !== void 0 ? `COMPOSE scanned: (none) \u2014 the set's parent (${composeScan.skipped.dir}) holds ${composeScan.skipped.entries} entries and was not scanned as a recordings library; pass --library <dir> to scan one deliberately
|
|
17587
|
+
` : `COMPOSE scanned: ${composeScan.scannedRoots.join(", ")} \u2014 recording sets elsewhere are NOT seen; pass --library <dir> to scan another workspace
|
|
17588
|
+
`
|
|
17589
|
+
);
|
|
17590
|
+
}
|
|
17141
17591
|
});
|
|
17142
17592
|
if (opts.bar === "cert" && substitutedFamilies.length > 0) {
|
|
17143
17593
|
const names = substitutedFamilies.map((f) => `"${f}"`).join(", ");
|
|
@@ -17178,7 +17628,7 @@ ${certified}/${statuses.length} certified \xB7 ${report.coverage.pass}/${statuse
|
|
|
17178
17628
|
persistReport(opts, report, evidenceDir);
|
|
17179
17629
|
}
|
|
17180
17630
|
function persistReport(opts, report, evidenceDir) {
|
|
17181
|
-
if (!
|
|
17631
|
+
if (!existsSync40(evidenceDir)) return;
|
|
17182
17632
|
const rulerExit = process.exitCode === void 0 ? 0 : Number(process.exitCode);
|
|
17183
17633
|
const withExit = {
|
|
17184
17634
|
...report,
|
|
@@ -17186,9 +17636,9 @@ function persistReport(opts, report, evidenceDir) {
|
|
|
17186
17636
|
...rulerExit !== 0 ? { rulerRefusal: EXIT_REFUSALS[rulerExit] ?? `ruler exited ${rulerExit}` } : {}
|
|
17187
17637
|
};
|
|
17188
17638
|
try {
|
|
17189
|
-
|
|
17190
|
-
|
|
17191
|
-
`${JSON.stringify(verifyReportForTransport(withExit,
|
|
17639
|
+
writeFileSync18(
|
|
17640
|
+
path50.join(evidenceDir, VERIFY_REPORT_FILENAME),
|
|
17641
|
+
`${JSON.stringify(verifyReportForTransport(withExit, path50.basename(opts.bundleDir)), null, 2)}
|
|
17192
17642
|
`
|
|
17193
17643
|
);
|
|
17194
17644
|
} catch (e) {
|
|
@@ -17206,6 +17656,8 @@ var init_verify = __esm({
|
|
|
17206
17656
|
init_src5();
|
|
17207
17657
|
init_environment();
|
|
17208
17658
|
init_font_guidance();
|
|
17659
|
+
init_compose2();
|
|
17660
|
+
init_inspect();
|
|
17209
17661
|
init_invocation();
|
|
17210
17662
|
init_output();
|
|
17211
17663
|
init_profile_input();
|
|
@@ -17238,20 +17690,31 @@ __export(engine_exports, {
|
|
|
17238
17690
|
runEngineBrief: () => runEngineBrief,
|
|
17239
17691
|
runEngineScore: () => runEngineScore
|
|
17240
17692
|
});
|
|
17241
|
-
import { appendFileSync, existsSync as
|
|
17242
|
-
import
|
|
17693
|
+
import { appendFileSync, existsSync as existsSync41, mkdirSync as mkdirSync12, readFileSync as readFileSync37, writeFileSync as writeFileSync19 } from "node:fs";
|
|
17694
|
+
import path51 from "node:path";
|
|
17243
17695
|
function resolveEngineTask(opts, callerCwd) {
|
|
17244
|
-
const asPath =
|
|
17245
|
-
const isSet =
|
|
17696
|
+
const asPath = path51.resolve(callerCwd, opts.taskOrSet);
|
|
17697
|
+
const isSet = existsSync41(path51.join(asPath, "recording-set.json"));
|
|
17246
17698
|
const registry = TASKS[opts.taskOrSet];
|
|
17247
17699
|
if (registry !== void 0 && !isSet) return { task: registry, name: opts.taskOrSet, ref: opts.taskOrSet, disclosures: [], interactionEvidence: void 0 };
|
|
17248
17700
|
if (isSet) {
|
|
17701
|
+
const manifest = loadManifest(asPath);
|
|
17702
|
+
const missing = manifest.reps.filter(
|
|
17703
|
+
(r) => !repEnvelopeExists(asPath, r.slug, "metadata") || !existsSync41(path51.join(asPath, r.slug, "get_design_context.json"))
|
|
17704
|
+
);
|
|
17705
|
+
if (missing.length > 0) {
|
|
17706
|
+
fail(opts, ExitCode.RecordingIncomplete, {
|
|
17707
|
+
error: `recording set ${asPath} is missing envelopes for ${missing.length} rep(s): ${missing.slice(0, 3).map((r) => r.slug).join(", ")}${missing.length > 3 ? ", \u2026" : ""}`,
|
|
17708
|
+
code: "recording-incomplete",
|
|
17709
|
+
remediation: `Resume the recording (\`${tendrilCommand(`record next --set ${asPath}`)}\`) until \`${tendrilCommand(`record status --set ${asPath}`)}\` reports the set complete, then re-run this.`
|
|
17710
|
+
});
|
|
17711
|
+
}
|
|
17249
17712
|
try {
|
|
17250
17713
|
const authored = authorTaskFromSet(asPath);
|
|
17251
17714
|
for (const d of authored.disclosures) warn(opts, d);
|
|
17252
17715
|
return {
|
|
17253
17716
|
task: authored.task,
|
|
17254
|
-
name:
|
|
17717
|
+
name: path51.basename(asPath),
|
|
17255
17718
|
ref: asPath,
|
|
17256
17719
|
disclosures: authored.disclosures,
|
|
17257
17720
|
interactionEvidence: authored.api.interactionEvidence,
|
|
@@ -17280,9 +17743,9 @@ function runEngineBrief(opts) {
|
|
|
17280
17743
|
const { task, name, ref, disclosures } = resolveEngineTask(opts, callerCwd);
|
|
17281
17744
|
void reportRunPresence(name, "implementing");
|
|
17282
17745
|
const bar = BARS3[opts.bar];
|
|
17283
|
-
if (
|
|
17746
|
+
if (existsSync41(path51.join(task.set, "recording-set.json"))) {
|
|
17284
17747
|
try {
|
|
17285
|
-
const { open, skippedParent } = compositionPairsFor(
|
|
17748
|
+
const { open, proposals, skippedParent } = compositionPairsFor(path51.resolve(task.set), [opts.library !== void 0 ? path51.resolve(callerCwd, opts.library) : callerCwd]);
|
|
17286
17749
|
if (skippedParent !== void 0) {
|
|
17287
17750
|
disclosures.push(
|
|
17288
17751
|
`COMPOSITION DISCOVERY PARTIAL: the set's parent directory (${skippedParent.dir}) holds ${String(skippedParent.entries)} entries and was not scanned as a recordings library \u2014 sibling sets there are invisible to pairing. Pass --library <dir> to scan a specific library deliberately.`
|
|
@@ -17291,7 +17754,12 @@ function runEngineBrief(opts) {
|
|
|
17291
17754
|
if (open.length > 0) {
|
|
17292
17755
|
const componentNames = [...new Set(open.map((p) => p.displayName))];
|
|
17293
17756
|
disclosures.push(
|
|
17294
|
-
`COMPOSITION PAIRS UNDECIDED: this recording references ${componentNames.length} other recorded component(s) (${open.map((p) => `${p.displayName} [pair-key ${p.key}]: ${p.instances.length} instance(s)`).join("; ")}) and no human has decided the pairing. This brief treats the component as SELF-CONTAINED \u2014 you will re-implement the partner's pixels locally. If composition is wanted: generate the partner bundle first, have a human run \`${tendrilCommand(`compose --set ${
|
|
17757
|
+
`COMPOSITION PAIRS UNDECIDED: this recording references ${componentNames.length} other recorded component(s) (${open.map((p) => `${p.displayName} [pair-key ${p.key}]: ${p.instances.length} instance(s)`).join("; ")}) and no human has decided the pairing. This brief treats the component as SELF-CONTAINED \u2014 you will re-implement the partner's pixels locally. If composition is wanted: generate the partner bundle first, have a human run \`${tendrilCommand(`compose --set ${path51.resolve(task.set)}`)}\` in their terminal, and re-emit this brief. Proceeding as-is is valid; it just is not composition.`
|
|
17758
|
+
);
|
|
17759
|
+
}
|
|
17760
|
+
if (proposals.length > 0) {
|
|
17761
|
+
disclosures.push(
|
|
17762
|
+
`COMPOSITION NAME-ONLY: instance names match ${proposals.length} recorded component(s) (${proposals.map((p) => `${p.displayName}: ${p.instances} instance(s)`).join("; ")}) but identity is UNPROVEN \u2014 name evidence never auto-joins, so these regions are implemented locally. ${proposals.flatMap((p) => p.disclosures).find((d) => d.includes("re-record")) ?? "Record id evidence to make a pairing confirmable, or proceed self-contained."}`
|
|
17295
17763
|
);
|
|
17296
17764
|
}
|
|
17297
17765
|
} catch (err) {
|
|
@@ -17308,9 +17776,9 @@ ${disclosures.map((d) => `- ${d}`).join("\n")}` : "";
|
|
|
17308
17776
|
const brief = buildBrief(task.systemApi, bar, { colorScheme: recordingIsDark(task) ? "dark" : "light" }) + disclosureBlock + motionBriefSection(task.set) + conventions;
|
|
17309
17777
|
const segments = buildSegments(task, "files");
|
|
17310
17778
|
let notRecorded;
|
|
17311
|
-
const manifestPath2 =
|
|
17312
|
-
if (
|
|
17313
|
-
notRecorded = JSON.parse(
|
|
17779
|
+
const manifestPath2 = path51.join(task.set, "recording-set.json");
|
|
17780
|
+
if (existsSync41(manifestPath2)) {
|
|
17781
|
+
notRecorded = JSON.parse(readFileSync37(manifestPath2, "utf8")).notRecorded;
|
|
17314
17782
|
}
|
|
17315
17783
|
const unverified = notRecorded !== void 0 && notRecorded !== "" ? `
|
|
17316
17784
|
|
|
@@ -17318,7 +17786,7 @@ ${disclosures.map((d) => `- ${d}`).join("\n")}` : "";
|
|
|
17318
17786
|
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.
|
|
17319
17787
|
${notRecorded}` : "";
|
|
17320
17788
|
let fontProvisioning;
|
|
17321
|
-
if (
|
|
17789
|
+
if (existsSync41(manifestPath2)) {
|
|
17322
17790
|
const missingFams = unprovisionedFamilies(task.set);
|
|
17323
17791
|
const unprovided = unprovisionedFaces(task.set);
|
|
17324
17792
|
const weightOnly = missingFams.length === 0;
|
|
@@ -17340,7 +17808,7 @@ ${notRecorded}` : "";
|
|
|
17340
17808
|
};
|
|
17341
17809
|
}
|
|
17342
17810
|
}
|
|
17343
|
-
const pinsResult = composedPins(task.set, [opts.library !== void 0 ?
|
|
17811
|
+
const pinsResult = composedPins(task.set, [opts.library !== void 0 ? path51.resolve(callerCwd, opts.library) : callerCwd]);
|
|
17344
17812
|
const jsxProp = ([k, v]) => typeof v === "string" ? `${k}=${JSON.stringify(v)}` : `${k}={${JSON.stringify(v)}}`;
|
|
17345
17813
|
const composedBlock = pinsResult.pins.length === 0 && pinsResult.issues.length === 0 ? "" : (pinsResult.pins.length > 0 ? `
|
|
17346
17814
|
|
|
@@ -17376,10 +17844,10 @@ ${pinsResult.issues.map((i) => `- ${i}`).join("\n")}` : "");
|
|
|
17376
17844
|
|
|
17377
17845
|
=== TASK PAYLOAD (recorded truth, verbatim) ===
|
|
17378
17846
|
${segments}`;
|
|
17379
|
-
const payloadFile =
|
|
17380
|
-
const candidateDirSuggestion =
|
|
17381
|
-
mkdirSync12(
|
|
17382
|
-
|
|
17847
|
+
const payloadFile = path51.resolve(callerCwd, opts.out ?? `tendril-out/${name}-brief.md`);
|
|
17848
|
+
const candidateDirSuggestion = path51.resolve(callerCwd, `tendril-out/${name}-candidate`);
|
|
17849
|
+
mkdirSync12(path51.dirname(payloadFile), { recursive: true });
|
|
17850
|
+
writeFileSync19(payloadFile, payload);
|
|
17383
17851
|
emitData(
|
|
17384
17852
|
opts,
|
|
17385
17853
|
{
|
|
@@ -17425,7 +17893,7 @@ ${segments}`;
|
|
|
17425
17893
|
// command must search the same bundle roots the pins came
|
|
17426
17894
|
// from, or the oracle and the brief describe different worlds.
|
|
17427
17895
|
`Run \`${tendrilCommand(
|
|
17428
|
-
`engine score ${quoteArg(ref)} ${quoteArg(candidateDirSuggestion)} --bar ${opts.bar} --model ${opts.model ?? "<declared-model>"}${opts.library !== void 0 ? ` --library ${quoteArg(
|
|
17896
|
+
`engine score ${quoteArg(ref)} ${quoteArg(candidateDirSuggestion)} --bar ${opts.bar} --model ${opts.model ?? "<declared-model>"}${opts.library !== void 0 ? ` --library ${quoteArg(path51.resolve(callerCwd, opts.library))}` : ""} --json`
|
|
17429
17897
|
)}\` \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.`,
|
|
17430
17898
|
"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).",
|
|
17431
17899
|
"Never edit recording sets; never claim scores yourself \u2014 only the score command's output counts."
|
|
@@ -17440,8 +17908,8 @@ ${segments}`;
|
|
|
17440
17908
|
);
|
|
17441
17909
|
}
|
|
17442
17910
|
function appendScoreHistory(candidateDir, entry) {
|
|
17443
|
-
const file =
|
|
17444
|
-
const starts =
|
|
17911
|
+
const file = path51.join(candidateDir, "score-history.jsonl");
|
|
17912
|
+
const starts = existsSync41(file) ? readFileSync37(file, "utf8").split("\n").filter((l) => l.includes('"event":"round-start"')).length : 0;
|
|
17445
17913
|
const round = entry.event === "round-start" ? starts + 1 : Math.max(1, starts);
|
|
17446
17914
|
appendFileSync(file, `${JSON.stringify({ at: (/* @__PURE__ */ new Date()).toISOString(), round, ...entry })}
|
|
17447
17915
|
`);
|
|
@@ -17449,10 +17917,10 @@ function appendScoreHistory(candidateDir, entry) {
|
|
|
17449
17917
|
async function runEngineScore(opts) {
|
|
17450
17918
|
requireEntitlement(opts);
|
|
17451
17919
|
const callerCwd = process.env["INIT_CWD"] ?? process.cwd();
|
|
17452
|
-
const candidateDir =
|
|
17920
|
+
const candidateDir = path51.resolve(callerCwd, opts.candidateDir);
|
|
17453
17921
|
const { task, name, apiPin, interactionEvidence, unmappedInteractionEvidence: interactionEvidenceUnmapped, satisfiabilityDemoted, expertContract } = resolveEngineTask(opts, callerCwd);
|
|
17454
17922
|
void reportRunPresence(name, "implementing");
|
|
17455
|
-
if (!
|
|
17923
|
+
if (!existsSync41(candidateDir)) {
|
|
17456
17924
|
fail(opts, ExitCode.InputValidation, {
|
|
17457
17925
|
error: `candidate directory not found: ${candidateDir}`,
|
|
17458
17926
|
code: "candidate-missing",
|
|
@@ -17477,10 +17945,10 @@ async function runEngineScore(opts) {
|
|
|
17477
17945
|
for (const g of missingWeights(task.set)) {
|
|
17478
17946
|
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)`);
|
|
17479
17947
|
}
|
|
17480
|
-
if (opts.rebind !== true &&
|
|
17948
|
+
if (opts.rebind !== true && existsSync41(path51.join(candidateDir, "component.json"))) {
|
|
17481
17949
|
const prior = (() => {
|
|
17482
17950
|
try {
|
|
17483
|
-
const read = readBundleManifest(
|
|
17951
|
+
const read = readBundleManifest(readFileSync37(path51.join(candidateDir, "component.json"), "utf8"));
|
|
17484
17952
|
return read.manifest === void 0 ? { unreadable: true } : { hash: read.manifest.provenance.recordingSet.hash, path: read.manifest.provenance.recordingSet.path };
|
|
17485
17953
|
} catch {
|
|
17486
17954
|
return { unreadable: true };
|
|
@@ -17502,13 +17970,13 @@ async function runEngineScore(opts) {
|
|
|
17502
17970
|
}
|
|
17503
17971
|
}
|
|
17504
17972
|
const bar = BARS3[opts.bar];
|
|
17505
|
-
const evidenceDir =
|
|
17973
|
+
const evidenceDir = path51.join(candidateDir, "verify-evidence");
|
|
17506
17974
|
const scores = await scoreBundleForTask(task, candidateDir, bar, { evidenceDir, onProgress: emitProgress });
|
|
17507
17975
|
const hoverOpts = opts.hoverTimeoutMs !== void 0 ? { hoverBudgetMs: opts.hoverTimeoutMs } : {};
|
|
17508
17976
|
const parity = await checkHoverParity(task, candidateDir, task.configs, { ...hoverOpts, failureShotDir: evidenceDir });
|
|
17509
17977
|
const occlusionRows = await checkSiblingOcclusion(task, candidateDir, candidateCss(candidateDir), { authority: task.configs });
|
|
17510
17978
|
const occlusionFailures = occlusionRows.filter((o) => !o.pass);
|
|
17511
|
-
const scorePins = composedPins(task.set, [opts.library !== void 0 ?
|
|
17979
|
+
const scorePins = composedPins(task.set, [opts.library !== void 0 ? path51.resolve(callerCwd, opts.library) : callerCwd]);
|
|
17512
17980
|
const composition = composedChecks(candidateDir, task.entry, scorePins.pins);
|
|
17513
17981
|
const behaviors = [...await checkBehaviors(task, candidateDir, hoverOpts), ...parity, ...composition];
|
|
17514
17982
|
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)";
|
|
@@ -17646,7 +18114,18 @@ ${scorePins.issues.map((i) => `- ${i}`).join("\n")}` : "";
|
|
|
17646
18114
|
// bundles verify then FAILED on the interaction-evidence gate —
|
|
17647
18115
|
// the oracle must say what verify will say, including this.
|
|
17648
18116
|
...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" } : {},
|
|
17649
|
-
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" }
|
|
18117
|
+
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" },
|
|
18118
|
+
// Field handoff 2026-08-26 (problem 2): the loop's green is not
|
|
18119
|
+
// the pipeline's end — a run that stops at allPass leaves the
|
|
18120
|
+
// component reachable only on local disk. The oracle names the
|
|
18121
|
+
// remaining steps so no surface has to remember them for it.
|
|
18122
|
+
...allPass ? {
|
|
18123
|
+
next: {
|
|
18124
|
+
action: "verify",
|
|
18125
|
+
command: tendrilCommand(`verify "${opts.candidateDir}" --bar ${certifiedReps.length === scores.length ? "cert" : "pass"}`),
|
|
18126
|
+
note: "allPass ends the LOOP, not the run: recompute with tendril_verify (the ruler's report + evidence), and a green verify's terminal step is publishing \u2014 the component is done when it is live, not when it scores"
|
|
18127
|
+
}
|
|
18128
|
+
} : {}
|
|
17650
18129
|
},
|
|
17651
18130
|
() => {
|
|
17652
18131
|
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}]` : ""}
|
|
@@ -17702,6 +18181,7 @@ var init_engine2 = __esm({
|
|
|
17702
18181
|
"packages/cli/src/commands/engine.ts"() {
|
|
17703
18182
|
"use strict";
|
|
17704
18183
|
init_src3();
|
|
18184
|
+
init_src();
|
|
17705
18185
|
init_src7();
|
|
17706
18186
|
init_src4();
|
|
17707
18187
|
init_environment();
|
|
@@ -17727,11 +18207,11 @@ var codeconnect_exports = {};
|
|
|
17727
18207
|
__export(codeconnect_exports, {
|
|
17728
18208
|
runCodeConnect: () => runCodeConnect
|
|
17729
18209
|
});
|
|
17730
|
-
import { existsSync as
|
|
17731
|
-
import
|
|
18210
|
+
import { existsSync as existsSync42, readFileSync as readFileSync38, writeFileSync as writeFileSync20 } from "node:fs";
|
|
18211
|
+
import path52 from "node:path";
|
|
17732
18212
|
function runCodeConnect(opts) {
|
|
17733
18213
|
const callerCwd = process.env["INIT_CWD"] ?? process.cwd();
|
|
17734
|
-
const bundleDir =
|
|
18214
|
+
const bundleDir = path52.resolve(callerCwd, opts.bundleDir);
|
|
17735
18215
|
let url;
|
|
17736
18216
|
try {
|
|
17737
18217
|
url = new URL(opts.figmaUrl);
|
|
@@ -17747,7 +18227,7 @@ function runCodeConnect(opts) {
|
|
|
17747
18227
|
}
|
|
17748
18228
|
let manifest;
|
|
17749
18229
|
try {
|
|
17750
|
-
const read = readBundleManifest(
|
|
18230
|
+
const read = readBundleManifest(readFileSync38(path52.join(bundleDir, "component.json"), "utf8"));
|
|
17751
18231
|
if (read.manifest === void 0) throw new Error(read.issues.map((i) => i.message).join("; ") || "no component.json");
|
|
17752
18232
|
manifest = read.manifest;
|
|
17753
18233
|
} catch (err) {
|
|
@@ -17757,8 +18237,8 @@ function runCodeConnect(opts) {
|
|
|
17757
18237
|
remediation: "Point at a bundle produced by the Tendril pipeline (it carries component.json), and verify it first."
|
|
17758
18238
|
});
|
|
17759
18239
|
}
|
|
17760
|
-
const setDir =
|
|
17761
|
-
if (!
|
|
18240
|
+
const setDir = path52.resolve(callerCwd, opts.set ?? manifest.provenance.recordingSet.path);
|
|
18241
|
+
if (!existsSync42(path52.join(setDir, "recording-set.json"))) {
|
|
17762
18242
|
fail(opts, ExitCode.InputValidation, {
|
|
17763
18243
|
error: `recording set not found at ${setDir}`,
|
|
17764
18244
|
code: "codeconnect-no-set",
|
|
@@ -17780,9 +18260,9 @@ function runCodeConnect(opts) {
|
|
|
17780
18260
|
const recManifest = loadManifest(setDir);
|
|
17781
18261
|
const poseNames = recManifest.latticeNames ?? recManifest.reps.map((r) => {
|
|
17782
18262
|
const meta = resolveRepEnvelopePath(setDir, r.slug, "metadata");
|
|
17783
|
-
if (!
|
|
18263
|
+
if (!existsSync42(meta)) return void 0;
|
|
17784
18264
|
try {
|
|
17785
|
-
return /name="([^"]*)"/.exec(envelopeTextContent(JSON.parse(
|
|
18265
|
+
return /name="([^"]*)"/.exec(envelopeTextContent(JSON.parse(readFileSync38(meta, "utf8"))))?.[1];
|
|
17786
18266
|
} catch {
|
|
17787
18267
|
return void 0;
|
|
17788
18268
|
}
|
|
@@ -17847,7 +18327,7 @@ function runCodeConnect(opts) {
|
|
|
17847
18327
|
axisLines.push(`const ${varName} = instance.getEnum(${q(axis)}, { ${entries.join(", ")} })`);
|
|
17848
18328
|
fragmentVars.push(varName);
|
|
17849
18329
|
}
|
|
17850
|
-
const entryRel =
|
|
18330
|
+
const entryRel = path52.relative(callerCwd, path52.join(bundleDir, manifest.entry));
|
|
17851
18331
|
const trust = manifest.trustStatement.split("\n")[0] ?? "";
|
|
17852
18332
|
const lines = [
|
|
17853
18333
|
`// url=${opts.figmaUrl}`,
|
|
@@ -17871,8 +18351,8 @@ function runCodeConnect(opts) {
|
|
|
17871
18351
|
`}`,
|
|
17872
18352
|
``
|
|
17873
18353
|
].join("\n");
|
|
17874
|
-
const outFile =
|
|
17875
|
-
|
|
18354
|
+
const outFile = path52.resolve(callerCwd, opts.out ?? path52.join(bundleDir, `${component}.figma.ts`));
|
|
18355
|
+
writeFileSync20(outFile, lines);
|
|
17876
18356
|
emitData(
|
|
17877
18357
|
opts,
|
|
17878
18358
|
{
|
|
@@ -17912,17 +18392,17 @@ var init_codeconnect = __esm({
|
|
|
17912
18392
|
|
|
17913
18393
|
// packages/mcp/src/server.ts
|
|
17914
18394
|
import { createHash as createHash11 } from "node:crypto";
|
|
17915
|
-
import { existsSync as
|
|
18395
|
+
import { existsSync as existsSync43, mkdtempSync as mkdtempSync3, readFileSync as readFileSync39, readdirSync as readdirSync17, writeFileSync as writeFileSync21 } from "node:fs";
|
|
17916
18396
|
import os8 from "node:os";
|
|
17917
|
-
import
|
|
18397
|
+
import path53 from "node:path";
|
|
17918
18398
|
import { fileURLToPath as fileURLToPath6 } from "node:url";
|
|
17919
18399
|
import { z as z15 } from "zod";
|
|
17920
18400
|
function sourceHash() {
|
|
17921
|
-
const dir =
|
|
18401
|
+
const dir = path53.dirname(fileURLToPath6(import.meta.url));
|
|
17922
18402
|
const h = createHash11("sha256");
|
|
17923
|
-
for (const f of
|
|
18403
|
+
for (const f of readdirSync17(dir).filter((n) => n.endsWith(".ts")).sort()) {
|
|
17924
18404
|
h.update(f);
|
|
17925
|
-
h.update(
|
|
18405
|
+
h.update(readFileSync39(path53.join(dir, f)));
|
|
17926
18406
|
}
|
|
17927
18407
|
return h.digest("hex").slice(0, 16);
|
|
17928
18408
|
}
|
|
@@ -17930,10 +18410,10 @@ var REPO_ROOT3, CLI_BIN, BUNDLED_CLI, CLI_SPAWN, str, optStr, TOOLS, BOOT_HASH;
|
|
|
17930
18410
|
var init_server = __esm({
|
|
17931
18411
|
"packages/mcp/src/server.ts"() {
|
|
17932
18412
|
"use strict";
|
|
17933
|
-
REPO_ROOT3 =
|
|
17934
|
-
CLI_BIN =
|
|
17935
|
-
BUNDLED_CLI =
|
|
17936
|
-
CLI_SPAWN =
|
|
18413
|
+
REPO_ROOT3 = path53.resolve(path53.dirname(fileURLToPath6(import.meta.url)), "..", "..", "..");
|
|
18414
|
+
CLI_BIN = path53.join(REPO_ROOT3, "packages", "cli", "src", "bin.ts");
|
|
18415
|
+
BUNDLED_CLI = path53.join(path53.dirname(fileURLToPath6(import.meta.url)), "tendril.js");
|
|
18416
|
+
CLI_SPAWN = existsSync43(BUNDLED_CLI) ? { cmd: process.execPath, prefix: [BUNDLED_CLI] } : { cmd: "npx", prefix: ["tsx", CLI_BIN] };
|
|
17937
18417
|
str = (d) => z15.string().describe(d);
|
|
17938
18418
|
optStr = (d) => z15.string().optional().describe(d);
|
|
17939
18419
|
TOOLS = [
|
|
@@ -17964,13 +18444,13 @@ var init_server = __esm({
|
|
|
17964
18444
|
const single = i["metadata"];
|
|
17965
18445
|
const parts = i["metadataParts"];
|
|
17966
18446
|
if (single !== void 0 || parts !== void 0) {
|
|
17967
|
-
const tmp =
|
|
18447
|
+
const tmp = path53.join(mkdtempSync3(path53.join(os8.tmpdir(), "tendril-envelope-")), "response.txt");
|
|
17968
18448
|
if (single !== void 0) {
|
|
17969
|
-
|
|
18449
|
+
writeFileSync21(tmp, single);
|
|
17970
18450
|
argvOut.push("--metadata-raw-file", tmp);
|
|
17971
18451
|
} else {
|
|
17972
18452
|
if (parts.length === 0) throw new Error("`metadataParts` must be a non-empty array of block texts");
|
|
17973
|
-
|
|
18453
|
+
writeFileSync21(tmp, JSON.stringify(parts));
|
|
17974
18454
|
argvOut.push("--metadata-raw-parts-file", tmp);
|
|
17975
18455
|
}
|
|
17976
18456
|
}
|
|
@@ -18045,12 +18525,12 @@ var init_server = __esm({
|
|
|
18045
18525
|
},
|
|
18046
18526
|
{
|
|
18047
18527
|
name: "tendril_publish_wait",
|
|
18048
|
-
description:
|
|
18528
|
+
description: 'Phase two of the browser-approved publish. Waits in a BOUNDED window (~1 minute per call) for the user\'s Approve click on the page tendril_publish returned, then uploads the bundle, commits it, and returns the live publication URL. Call it right after relaying the approve link. While the human has not decided, each call returns `status: "approval-pending"` \u2014 that is a heartbeat, not a failure: tell the user in one short line that you are still waiting (restate the approve link and the account to approve as, ONLY if they seem lost; never urge the decision), then call this again to keep waiting. The request stays live for ~30 minutes. A denial, a lapse, and success each come back as their own sentence \u2014 report the outcome, and on success lead with the live URL. A decided approval continues straight into upload and commit INSIDE the same call \u2014 that phase can take minutes and may render no progress in some hosts; that is normal, not stuck. Some hosts render no progress at all during a call; the bounded window IS the liveness, so never describe an in-flight call as stuck \u2014 and a brief liveness line roughly every few minutes is enough, you need not narrate every heartbeat (each pending return carries waitedTotalSeconds/remainingSeconds to say where the wait stands).',
|
|
18049
18529
|
schema: z15.object({
|
|
18050
18530
|
bundleDir: str("the same bundle directory tendril_publish was called with"),
|
|
18051
18531
|
portal: optStr("portal origin override (must match tendril_publish's)")
|
|
18052
18532
|
}),
|
|
18053
|
-
argv: (i) => ["publish", i["bundleDir"], "--approve-wait", ...typeof i["portal"] === "string" ? ["--to", i["portal"]] : []]
|
|
18533
|
+
argv: (i) => ["publish", i["bundleDir"], "--approve-wait", "--wait-window", "55", ...typeof i["portal"] === "string" ? ["--to", i["portal"]] : []]
|
|
18054
18534
|
},
|
|
18055
18535
|
{
|
|
18056
18536
|
name: "tendril_record_next",
|
|
@@ -18099,14 +18579,14 @@ var init_server = __esm({
|
|
|
18099
18579
|
const bridge = (label, single, parts) => {
|
|
18100
18580
|
if (single !== void 0 && parts !== void 0) throw new Error(`pass at most one of \`${label}\` and \`${label}Parts\``);
|
|
18101
18581
|
if (single === void 0 && parts === void 0) return;
|
|
18102
|
-
const tmp =
|
|
18582
|
+
const tmp = path53.join(mkdtempSync3(path53.join(os8.tmpdir(), "tendril-envelope-")), "response.txt");
|
|
18103
18583
|
if (single !== void 0) {
|
|
18104
|
-
|
|
18584
|
+
writeFileSync21(tmp, single);
|
|
18105
18585
|
argvOut.push(`--${label}-file`, tmp);
|
|
18106
18586
|
} else {
|
|
18107
18587
|
const blocks = parts;
|
|
18108
18588
|
if (blocks.length === 0) throw new Error(`\`${label}Parts\` must be a non-empty array of block texts`);
|
|
18109
|
-
|
|
18589
|
+
writeFileSync21(tmp, JSON.stringify(blocks));
|
|
18110
18590
|
argvOut.push(`--${label}-parts-file`, tmp);
|
|
18111
18591
|
}
|
|
18112
18592
|
};
|
|
@@ -18147,12 +18627,12 @@ var init_server = __esm({
|
|
|
18147
18627
|
const file = i["file"];
|
|
18148
18628
|
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)");
|
|
18149
18629
|
if (file !== void 0) return [...base, "--file", file];
|
|
18150
|
-
const tmp =
|
|
18630
|
+
const tmp = path53.join(mkdtempSync3(path53.join(os8.tmpdir(), "tendril-envelope-")), "response.txt");
|
|
18151
18631
|
if (text !== void 0) {
|
|
18152
|
-
|
|
18632
|
+
writeFileSync21(tmp, text);
|
|
18153
18633
|
return [...base, "--file", tmp, "--raw"];
|
|
18154
18634
|
}
|
|
18155
|
-
|
|
18635
|
+
writeFileSync21(tmp, JSON.stringify(texts));
|
|
18156
18636
|
return [...base, "--file", tmp, "--raw-parts"];
|
|
18157
18637
|
}
|
|
18158
18638
|
},
|
|
@@ -18212,7 +18692,7 @@ var init_server = __esm({
|
|
|
18212
18692
|
},
|
|
18213
18693
|
{
|
|
18214
18694
|
name: "tendril_engine_score",
|
|
18215
|
-
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.",
|
|
18695
|
+
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. allPass ends the LOOP, not the run: finish with tendril_verify, and a green verify's terminal step is publishing \u2014 the component is done when it is LIVE, not when it scores.",
|
|
18216
18696
|
schema: z15.object({
|
|
18217
18697
|
taskOrSet: str("reference task name or recording-set directory"),
|
|
18218
18698
|
candidateDir: str("directory containing the proposed bundle files"),
|
|
@@ -18255,7 +18735,7 @@ var init_server = __esm({
|
|
|
18255
18735
|
},
|
|
18256
18736
|
{
|
|
18257
18737
|
name: "tendril_verify",
|
|
18258
|
-
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.",
|
|
18738
|
+
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. A GREEN run writes the inspect sheet itself and returns `next`: its terminal step is publishing (tendril_publish \u2192 tendril_publish_wait) \u2014 a verified component reachable only on local disk is an unfinished run; a sub-bar run never publishes.",
|
|
18259
18739
|
schema: z15.object({
|
|
18260
18740
|
bundleDir: str("bundle directory to verify"),
|
|
18261
18741
|
bar: optStr("pass (default) or cert"),
|
|
@@ -18309,13 +18789,13 @@ __export(permissions_exports, {
|
|
|
18309
18789
|
runPermissions: () => runPermissions,
|
|
18310
18790
|
writeSelection: () => writeSelection
|
|
18311
18791
|
});
|
|
18312
|
-
import { existsSync as
|
|
18792
|
+
import { existsSync as existsSync44, mkdirSync as mkdirSync13, readFileSync as readFileSync40, writeFileSync as writeFileSync22 } from "node:fs";
|
|
18313
18793
|
import os9 from "node:os";
|
|
18314
|
-
import
|
|
18794
|
+
import path54 from "node:path";
|
|
18315
18795
|
function mergeAllowlist(file, entries, denyEntries = []) {
|
|
18316
18796
|
let settings = {};
|
|
18317
|
-
if (
|
|
18318
|
-
settings = JSON.parse(
|
|
18797
|
+
if (existsSync44(file) && readFileSync40(file, "utf8").trim() !== "") {
|
|
18798
|
+
settings = JSON.parse(readFileSync40(file, "utf8"));
|
|
18319
18799
|
if (settings === null || typeof settings !== "object" || Array.isArray(settings)) throw new Error("settings root is not an object");
|
|
18320
18800
|
}
|
|
18321
18801
|
const permissions = settings["permissions"] ??= {};
|
|
@@ -18335,8 +18815,8 @@ function mergeAllowlist(file, entries, denyEntries = []) {
|
|
|
18335
18815
|
}
|
|
18336
18816
|
if (added.length > 0 || denyAdded.length > 0) {
|
|
18337
18817
|
allow.push(...added);
|
|
18338
|
-
mkdirSync13(
|
|
18339
|
-
|
|
18818
|
+
mkdirSync13(path54.dirname(file), { recursive: true });
|
|
18819
|
+
writeFileSync22(file, `${JSON.stringify(settings, null, 2)}
|
|
18340
18820
|
`);
|
|
18341
18821
|
}
|
|
18342
18822
|
return { added, alreadyPresent, denyAdded };
|
|
@@ -18400,7 +18880,7 @@ async function runPermissions(flags) {
|
|
|
18400
18880
|
}
|
|
18401
18881
|
if (flags.write) {
|
|
18402
18882
|
const base = process.env["INIT_CWD"] ?? process.cwd();
|
|
18403
|
-
const file = flags.user ?
|
|
18883
|
+
const file = flags.user ? path54.join(os9.homedir(), ".claude", "settings.json") : path54.join(base, ".claude", "settings.local.json");
|
|
18404
18884
|
const { entries, denyEntries } = writeSelection(result, flags.user === true);
|
|
18405
18885
|
if (flags.dryRun) {
|
|
18406
18886
|
emitData(flags, { file, wouldAdd: entries, wouldDeny: denyEntries }, () => {
|
|
@@ -18543,168 +19023,6 @@ var init_permissions = __esm({
|
|
|
18543
19023
|
}
|
|
18544
19024
|
});
|
|
18545
19025
|
|
|
18546
|
-
// packages/cli/src/commands/inspect.ts
|
|
18547
|
-
var inspect_exports = {};
|
|
18548
|
-
__export(inspect_exports, {
|
|
18549
|
-
INSPECT_DESCRIPTION: () => INSPECT_DESCRIPTION,
|
|
18550
|
-
runInspect: () => runInspect
|
|
18551
|
-
});
|
|
18552
|
-
import { existsSync as existsSync44, readFileSync as readFileSync40, writeFileSync as writeFileSync22 } from "node:fs";
|
|
18553
|
-
import path54 from "node:path";
|
|
18554
|
-
function readVerifyReport(evidenceDir) {
|
|
18555
|
-
const p = path54.join(evidenceDir, VERIFY_REPORT_FILENAME);
|
|
18556
|
-
if (!existsSync44(p)) return void 0;
|
|
18557
|
-
try {
|
|
18558
|
-
return JSON.parse(readFileSync40(p, "utf8"));
|
|
18559
|
-
} catch {
|
|
18560
|
-
return void 0;
|
|
18561
|
-
}
|
|
18562
|
-
}
|
|
18563
|
-
function verdictBlock(report) {
|
|
18564
|
-
if (report === void 0) {
|
|
18565
|
-
return `<div class="verdict none"><h2>No verdict on file</h2><p>This evidence carries no <code>${VERIFY_REPORT_FILENAME}</code>, so these images are unlabelled pixels \u2014 nothing here says what passed. Re-run <code>tendril verify</code> to write one.</p></div>`;
|
|
18566
|
-
}
|
|
18567
|
-
const buckets = /* @__PURE__ */ new Map();
|
|
18568
|
-
for (const c of report.configs) buckets.set(c.status, (buckets.get(c.status) ?? 0) + 1);
|
|
18569
|
-
const split = [...buckets.entries()].map(([k, n]) => `<span class="tier t-${esc(k)}">${n} ${esc(k)}</span>`).join("");
|
|
18570
|
-
const failed = report.behaviors.filter((b) => !b.pass);
|
|
18571
|
-
return `<div class="verdict ${report.verdict === "verified" ? "ok" : "bad"}"><h2>${esc(report.verdict)} <small>at bar \u201C${esc(report.targetBar)}\u201D</small></h2><p class="tiers">${split}<span class="tier">${report.behaviors.length - failed.length}/${report.behaviors.length} behaviors</span></p>` + (failed.length > 0 ? `<ul class="fails">${failed.map((b) => `<li><code>${esc(b.id)}</code>${b.detail !== void 0 ? ` \u2014 ${esc(b.detail)}` : ""}</li>`).join("")}</ul>` : "") + (report.verdictCaveats.length > 0 ? `<h3>What was never asked</h3><ul class="caveats">${report.verdictCaveats.map((c) => `<li>${esc(c)}</li>`).join("")}</ul>` : `<p class="caveats">No caveats: every question this bar asks was answered.</p>`) + `</div>`;
|
|
18572
|
-
}
|
|
18573
|
-
function scoreLine(report, rep) {
|
|
18574
|
-
const c = report?.configs.find((x) => x.rep === rep);
|
|
18575
|
-
if (c === void 0) return "";
|
|
18576
|
-
const ex = c.exact !== void 0 ? ` <span class="exact">measured ${c.exact.similarity.toFixed(6)} / ${c.exact.inkRecall.toFixed(6)}</span>` : "";
|
|
18577
|
-
const why = Array.isArray(c.demotedBy) && c.demotedBy.length > 0 ? `<span class="demoted">demoted: ${esc(c.demotedBy.join("; "))}</span>` : "";
|
|
18578
|
-
const err = typeof c.error === "string" ? `<span class="demoted">${esc(c.error)}</span>` : "";
|
|
18579
|
-
return `<p class="scores"><span class="tier t-${esc(c.status)}">${esc(c.status)}</span> sim ${c.similarity} \xB7 ink ${c.inkRecall}${ex} ${why} ${err}</p>`;
|
|
18580
|
-
}
|
|
18581
|
-
async function runInspect(opts) {
|
|
18582
|
-
if (opts.describe) {
|
|
18583
|
-
printDescription(INSPECT_DESCRIPTION);
|
|
18584
|
-
return;
|
|
18585
|
-
}
|
|
18586
|
-
const bundleDir = path54.resolve(opts.bundleDir);
|
|
18587
|
-
const evidenceDir = path54.join(bundleDir, "verify-evidence");
|
|
18588
|
-
const manifestPath2 = path54.join(bundleDir, "component.json");
|
|
18589
|
-
if (!existsSync44(evidenceDir) || !existsSync44(manifestPath2)) {
|
|
18590
|
-
fail(opts, ExitCode.InputValidation, {
|
|
18591
|
-
error: `nothing to inspect in ${bundleDir} \u2014 ${existsSync44(manifestPath2) ? "no verify-evidence directory" : "no component.json"}`,
|
|
18592
|
-
code: "no-evidence",
|
|
18593
|
-
remediation: `Run \`${tendrilCommand(`verify ${bundleDir}`)}\` first \u2014 inspect reads the ref/render evidence that run writes.`
|
|
18594
|
-
});
|
|
18595
|
-
}
|
|
18596
|
-
const { manifest } = readBundleManifest(readFileSync40(manifestPath2, "utf8"));
|
|
18597
|
-
if (manifest === void 0) {
|
|
18598
|
-
fail(opts, ExitCode.InputValidation, {
|
|
18599
|
-
error: "component.json did not parse as a bundle manifest",
|
|
18600
|
-
code: "no-mount-contract",
|
|
18601
|
-
remediation: "Re-emit the bundle (score/verify rewrite component.json), then re-run inspect."
|
|
18602
|
-
});
|
|
18603
|
-
}
|
|
18604
|
-
const setDir = path54.resolve(opts.set ?? manifest.provenance.recordingSet.path);
|
|
18605
|
-
const report = readVerifyReport(evidenceDir);
|
|
18606
|
-
const reps = Object.keys(manifest.propAdapter).filter((rep) => existsSync44(path54.join(evidenceDir, `${rep}-ref.png`)) && existsSync44(path54.join(evidenceDir, `${rep}-render.png`)));
|
|
18607
|
-
if (reps.length === 0) {
|
|
18608
|
-
fail(opts, ExitCode.InputValidation, {
|
|
18609
|
-
error: "verify-evidence holds no ref/render pairs for this bundle's configs",
|
|
18610
|
-
code: "no-evidence",
|
|
18611
|
-
remediation: `Run \`${tendrilCommand(`verify ${bundleDir}`)}\` first \u2014 inspect reads the ref/render evidence that run writes.`
|
|
18612
|
-
});
|
|
18613
|
-
}
|
|
18614
|
-
let crops = 0;
|
|
18615
|
-
const sections = [];
|
|
18616
|
-
for (const rep of reps) {
|
|
18617
|
-
const ref = new Uint8Array(readFileSync40(path54.join(evidenceDir, `${rep}-ref.png`)));
|
|
18618
|
-
const render = new Uint8Array(readFileSync40(path54.join(evidenceDir, `${rep}-render.png`)));
|
|
18619
|
-
const nodes = smallSemanticNodes(setDir, rep, opts.maxArea ?? 1024).slice(0, 12);
|
|
18620
|
-
const cells = [];
|
|
18621
|
-
for (const [i, n] of nodes.entries()) {
|
|
18622
|
-
const rect = { x: n.x, y: n.y, w: n.w, h: n.h };
|
|
18623
|
-
try {
|
|
18624
|
-
writeFileSync22(path54.join(evidenceDir, `${rep}-inspect-${i}-ref.png`), zoomCrop(ref, rect));
|
|
18625
|
-
writeFileSync22(path54.join(evidenceDir, `${rep}-inspect-${i}-render.png`), zoomCrop(render, rect));
|
|
18626
|
-
} catch {
|
|
18627
|
-
continue;
|
|
18628
|
-
}
|
|
18629
|
-
crops += 1;
|
|
18630
|
-
cells.push(
|
|
18631
|
-
`<figure><figcaption>${esc(n.name)} <code>${esc(n.id)}</code> \xB7 ${n.w}\xD7${n.h}</figcaption><div class="pair"><span><em>recorded</em><img src="./${rep}-inspect-${i}-ref.png" alt="recorded ${esc(n.name)}"></span><span><em>rendered</em><img src="./${rep}-inspect-${i}-render.png" alt="rendered ${esc(n.name)}"></span></div></figure>`
|
|
18632
|
-
);
|
|
18633
|
-
}
|
|
18634
|
-
sections.push(
|
|
18635
|
-
`<section><h2>${esc(rep)}</h2>` + scoreLine(report, rep) + `<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>`
|
|
18636
|
-
);
|
|
18637
|
-
}
|
|
18638
|
-
for (const c of report?.configs ?? []) {
|
|
18639
|
-
if (reps.includes(c.rep)) continue;
|
|
18640
|
-
sections.push(`<section class="missing"><h2>${esc(c.rep)}</h2>${scoreLine(report, c.rep)}<p class="none">No evidence images for this config \u2014 it was scored, but nothing was captured to look at.</p></section>`);
|
|
18641
|
-
}
|
|
18642
|
-
const sheet = path54.join(evidenceDir, "inspect.html");
|
|
18643
|
-
writeFileSync22(
|
|
18644
|
-
sheet,
|
|
18645
|
-
`<!doctype html><meta charset="utf-8"><title>${esc(manifest.name)} \u2014 tendril inspect</title><style>
|
|
18646
|
-
body{font:14px/1.45 system-ui,sans-serif;margin:24px;background:#fff;color:#111}
|
|
18647
|
-
h1{font-size:20px} h2{font-size:16px;border-top:1px solid #ddd;padding-top:16px}
|
|
18648
|
-
.pair,.full{display:flex;gap:12px;flex-wrap:wrap;align-items:flex-start}
|
|
18649
|
-
.full img{max-width:400px;border:1px solid #ccc} .pair img{border:1px solid #ccc;image-rendering:pixelated}
|
|
18650
|
-
figure{margin:0 0 16px} figcaption{margin-bottom:4px} em{display:block;color:#666;font-style:normal;font-size:12px}
|
|
18651
|
-
.grid{display:flex;flex-wrap:wrap;gap:20px;margin-top:12px} .none{color:#666}
|
|
18652
|
-
.verdict{border:1px solid #ddd;border-left-width:5px;padding:12px 16px;margin:16px 0;background:#fafafa}
|
|
18653
|
-
.verdict.ok{border-left-color:#1a7f37} .verdict.bad{border-left-color:#b35900} .verdict.none{border-left-color:#999}
|
|
18654
|
-
.verdict h2{border:0;padding:0;margin:0 0 8px} .verdict h3{font-size:13px;margin:12px 0 4px;color:#444}
|
|
18655
|
-
.verdict small{font-weight:400;color:#666}
|
|
18656
|
-
.tiers{margin:0;display:flex;gap:8px;flex-wrap:wrap}
|
|
18657
|
-
.tier{display:inline-block;padding:1px 8px;border:1px solid #ccc;border-radius:10px;font-size:12px;background:#fff}
|
|
18658
|
-
.t-certified{border-color:#1a7f37;color:#1a7f37} .t-pass{border-color:#8a6d00;color:#8a6d00} .t-fail{border-color:#b3261e;color:#b3261e}
|
|
18659
|
-
.caveats,.fails{margin:4px 0 0;padding-left:20px;color:#444} .caveats li,.fails li{margin:2px 0}
|
|
18660
|
-
.scores{margin:4px 0 10px;color:#333} .exact{color:#666;font-size:12px} .demoted{color:#b3261e;font-size:12px;margin-left:8px}
|
|
18661
|
-
.missing{opacity:.85} .legend{white-space:pre-wrap;font:12px/1.4 ui-monospace,SFMono-Regular,Menlo,monospace;background:#fafafa;border:1px solid #ddd;padding:12px;overflow-x:auto}
|
|
18662
|
-
</style><h1>${esc(manifest.name)} \u2014 verdict and evidence</h1>
|
|
18663
|
-
${verdictBlock(report)}
|
|
18664
|
-
<p>Below, every crop is a recorded node small enough that global metrics weight it as a rounding error.
|
|
18665
|
-
Scan the pairs: anything present on the left and missing/invisible on the right is a defect,
|
|
18666
|
-
whatever the scores said. Verdicts come from <code>tendril verify</code> \u2014 this sheet only shows.</p>
|
|
18667
|
-
${sections.join("\n")}
|
|
18668
|
-
<section><h2>Diff colours</h2><div class="legend">${esc(DIFF_LEGEND_TEXT)}</div></section>
|
|
18669
|
-
`
|
|
18670
|
-
);
|
|
18671
|
-
emitData(opts, { sheet, configs: reps.length, crops }, () => {
|
|
18672
|
-
process.stdout.write(`inspect sheet: ${sheet}
|
|
18673
|
-
${reps.length} config(s), ${crops} detail crop pair(s) \u2014 open the sheet and scan recorded vs rendered
|
|
18674
|
-
`);
|
|
18675
|
-
});
|
|
18676
|
-
}
|
|
18677
|
-
var INSPECT_DESCRIPTION, esc;
|
|
18678
|
-
var init_inspect = __esm({
|
|
18679
|
-
"packages/cli/src/commands/inspect.ts"() {
|
|
18680
|
-
"use strict";
|
|
18681
|
-
init_src3();
|
|
18682
|
-
init_src4();
|
|
18683
|
-
init_src5();
|
|
18684
|
-
init_describe();
|
|
18685
|
-
init_invocation();
|
|
18686
|
-
init_output();
|
|
18687
|
-
INSPECT_DESCRIPTION = {
|
|
18688
|
-
name: "inspect",
|
|
18689
|
-
summary: "Build an eye-verifiable detail sheet from verify evidence: magnified ref-vs-render crops of every small recorded node (icons, controls, marks).",
|
|
18690
|
-
args: [{ name: "bundleDir", required: true, description: "bundle directory (must carry component.json and a verify-evidence dir from a prior `tendril verify`)" }],
|
|
18691
|
-
flags: [
|
|
18692
|
-
{ flag: "--set <dir>", description: "recording set override (default: the bundle's provenance path)" },
|
|
18693
|
-
{ flag: "--max-area <px2>", description: "node area ceiling for the detail sweep", default: "1024" },
|
|
18694
|
-
{ flag: "--json", description: "Machine-readable output" }
|
|
18695
|
-
],
|
|
18696
|
-
output: {
|
|
18697
|
-
sheet: "string \u2014 path to the generated inspect.html",
|
|
18698
|
-
configs: "number \u2014 configs with evidence found",
|
|
18699
|
-
crops: "number \u2014 detail crop pairs written"
|
|
18700
|
-
},
|
|
18701
|
-
exitCodes: { 0: "sheet written", 3: "no verify evidence to inspect (run `tendril verify` first)" },
|
|
18702
|
-
examples: ["tendril inspect ./src/components/Banner", "tendril inspect ./bundle --set ./tendril/recordings/banner"]
|
|
18703
|
-
};
|
|
18704
|
-
esc = (s) => s.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """);
|
|
18705
|
-
}
|
|
18706
|
-
});
|
|
18707
|
-
|
|
18708
19026
|
// packages/cli/src/commands/login.ts
|
|
18709
19027
|
var login_exports = {};
|
|
18710
19028
|
__export(login_exports, {
|
|
@@ -19280,12 +19598,28 @@ var init_share = __esm({
|
|
|
19280
19598
|
// packages/cli/src/commands/publish.ts
|
|
19281
19599
|
var publish_exports = {};
|
|
19282
19600
|
__export(publish_exports, {
|
|
19601
|
+
approveWaitPhase: () => approveWaitPhase,
|
|
19283
19602
|
resolveOrigin: () => resolveOrigin2,
|
|
19284
|
-
runPublish: () => runPublish
|
|
19603
|
+
runPublish: () => runPublish,
|
|
19604
|
+
spendPendingApproval: () => spendPendingApproval
|
|
19285
19605
|
});
|
|
19286
19606
|
import { existsSync as existsSync47, readFileSync as readFileSync43, rmSync as rmSync10, writeFileSync as writeFileSync25 } from "node:fs";
|
|
19287
19607
|
import path57 from "node:path";
|
|
19288
19608
|
async function runPublish(opts) {
|
|
19609
|
+
if (opts.waitWindowSeconds !== void 0 && opts.approveWait !== true) {
|
|
19610
|
+
fail(opts, ExitCode.InputValidation, {
|
|
19611
|
+
error: "--wait-window only bounds an --approve-wait poll",
|
|
19612
|
+
code: "wait-window-without-approve-wait",
|
|
19613
|
+
remediation: "Pass --approve-wait with it, or drop --wait-window."
|
|
19614
|
+
});
|
|
19615
|
+
}
|
|
19616
|
+
if (opts.waitWindowSeconds !== void 0 && (!Number.isFinite(opts.waitWindowSeconds) || opts.waitWindowSeconds <= 0)) {
|
|
19617
|
+
fail(opts, ExitCode.InputValidation, {
|
|
19618
|
+
error: "--wait-window needs a positive number of seconds",
|
|
19619
|
+
code: "wait-window-invalid",
|
|
19620
|
+
remediation: "Pass e.g. --wait-window 55."
|
|
19621
|
+
});
|
|
19622
|
+
}
|
|
19289
19623
|
const bundleDir = path57.resolve(opts.bundleDir);
|
|
19290
19624
|
const bundle = readBundle(opts, bundleDir);
|
|
19291
19625
|
const report = bundle.report;
|
|
@@ -19399,9 +19733,11 @@ async function runPublish(opts) {
|
|
|
19399
19733
|
const origin = resolveOrigin2(opts);
|
|
19400
19734
|
const client = opts.client ?? httpClient(opts, origin);
|
|
19401
19735
|
if (opts.approveWait === true) {
|
|
19402
|
-
await approveWaitPhase(opts, client, bundleDir, { componentName, figmaFile });
|
|
19736
|
+
const phase = await approveWaitPhase(opts, client, bundleDir, { componentName, figmaFile });
|
|
19737
|
+
if (phase === "yielded") return;
|
|
19403
19738
|
}
|
|
19404
19739
|
void reportRunPresence(componentName, "publishing");
|
|
19740
|
+
emitProgress(0, 1, "requesting the upload plan from the portal");
|
|
19405
19741
|
let opened = await client.begin({
|
|
19406
19742
|
componentName,
|
|
19407
19743
|
figmaFile,
|
|
@@ -19428,6 +19764,7 @@ async function runPublish(opts) {
|
|
|
19428
19764
|
}
|
|
19429
19765
|
refuse2(opts, opened, "publish-refused");
|
|
19430
19766
|
}
|
|
19767
|
+
if (opts.approveWait === true) spendPendingApproval();
|
|
19431
19768
|
const uploaded = [];
|
|
19432
19769
|
for (const object of opened.value.plan.objects) {
|
|
19433
19770
|
const file = path57.join(bundleDir, object.relPath);
|
|
@@ -19447,6 +19784,7 @@ async function runPublish(opts) {
|
|
|
19447
19784
|
uploaded.push({ relPath: object.relPath, deduplicated: sent.value.deduplicated });
|
|
19448
19785
|
emitProgress(uploaded.length, opened.value.plan.objects.length, `uploading ${object.relPath}`);
|
|
19449
19786
|
}
|
|
19787
|
+
emitProgress(uploaded.length, uploaded.length, "upload complete \u2014 committing the publication");
|
|
19450
19788
|
const committed = await client.commit({ publicationId: opened.value.publicationId });
|
|
19451
19789
|
if (committed.ok) {
|
|
19452
19790
|
await endRunPresence(componentName);
|
|
@@ -19588,7 +19926,7 @@ async function runApprovalFlow(opts, client, bundleDir, input) {
|
|
|
19588
19926
|
const who = await client.whoami?.();
|
|
19589
19927
|
const asAccount = who?.ok === true && who.value.email !== "" ? ` signed in as ${who.value.email}` : "";
|
|
19590
19928
|
if (opts.approveStart === true) {
|
|
19591
|
-
const pending = { ...approval, bundleDir, componentName: input.componentName, figmaFile: input.figmaFile };
|
|
19929
|
+
const pending = { ...approval, bundleDir, componentName: input.componentName, figmaFile: input.figmaFile, requestedAt: (/* @__PURE__ */ new Date()).toISOString() };
|
|
19592
19930
|
writeFileSync25(pendingApprovalPath(), `${JSON.stringify(pending, null, 2)}
|
|
19593
19931
|
`, { mode: 384 });
|
|
19594
19932
|
await endRunPresence(input.componentName);
|
|
@@ -19621,7 +19959,7 @@ async function runApprovalFlow(opts, client, bundleDir, input) {
|
|
|
19621
19959
|
const decided = await waitForApproval(opts, client, approval);
|
|
19622
19960
|
if (decided === "approved") return input.begin();
|
|
19623
19961
|
await endRunPresence(input.componentName);
|
|
19624
|
-
failDecision(opts, decided);
|
|
19962
|
+
failDecision(opts, decided === "window-elapsed" ? "expired" : decided);
|
|
19625
19963
|
}
|
|
19626
19964
|
async function approveWaitPhase(opts, client, bundleDir, subject) {
|
|
19627
19965
|
const file = pendingApprovalPath();
|
|
@@ -19651,12 +19989,41 @@ async function approveWaitPhase(opts, client, bundleDir, subject) {
|
|
|
19651
19989
|
}
|
|
19652
19990
|
const done = () => rmSync10(file, { force: true });
|
|
19653
19991
|
const decided = await waitForApproval(opts, client, pending);
|
|
19654
|
-
|
|
19655
|
-
|
|
19992
|
+
if (decided === "window-elapsed") {
|
|
19993
|
+
const who = await client.whoami?.();
|
|
19994
|
+
const asAccount = who?.ok === true && who.value.email !== "" ? who.value.email : void 0;
|
|
19995
|
+
emitData(
|
|
19996
|
+
opts,
|
|
19997
|
+
{
|
|
19998
|
+
status: "approval-pending",
|
|
19999
|
+
approveUrl: pending.approveUrl,
|
|
20000
|
+
...asAccount !== void 0 ? { approveAsAccount: asAccount } : {},
|
|
20001
|
+
expiresAt: pending.expiresAt,
|
|
20002
|
+
windowSeconds: opts.waitWindowSeconds ?? 0,
|
|
20003
|
+
...pending.requestedAt !== void 0 ? { waitedTotalSeconds: Math.max(0, Math.round((Date.now() - Date.parse(pending.requestedAt)) / 1e3)) } : {},
|
|
20004
|
+
remainingSeconds: Math.max(0, Math.round((Date.parse(pending.expiresAt) - Date.now()) / 1e3)),
|
|
20005
|
+
next: "the approval request is STILL LIVE and undecided \u2014 tell the user in one short line you are still waiting for their Approve click (restate the approve link and the account to approve as, ONLY if they seem lost; never urge the decision), then run the wait again to keep waiting"
|
|
20006
|
+
},
|
|
20007
|
+
() => {
|
|
20008
|
+
process.stdout.write(`Still waiting for the browser approval (wait window elapsed; the request stays live until ${pending.expiresAt.slice(11, 16)} UTC): ${pending.approveUrl}
|
|
20009
|
+
`);
|
|
20010
|
+
}
|
|
20011
|
+
);
|
|
20012
|
+
return "yielded";
|
|
20013
|
+
}
|
|
20014
|
+
if (decided !== "approved") {
|
|
20015
|
+
done();
|
|
20016
|
+
failDecision(opts, decided);
|
|
20017
|
+
}
|
|
20018
|
+
return "proceed";
|
|
20019
|
+
}
|
|
20020
|
+
function spendPendingApproval() {
|
|
20021
|
+
rmSync10(pendingApprovalPath(), { force: true });
|
|
19656
20022
|
}
|
|
19657
20023
|
async function waitForApproval(opts, client, approval) {
|
|
19658
20024
|
const interval = Math.max(1, approval.pollSeconds) * 1e3;
|
|
19659
|
-
const
|
|
20025
|
+
const capMs = opts.waitWindowSeconds !== void 0 ? Math.max(1, opts.waitWindowSeconds) * 1e3 : APPROVAL_WAIT_CAP_MS;
|
|
20026
|
+
const total = Math.ceil(capMs / interval);
|
|
19660
20027
|
for (let tick = 1; tick <= total; tick += 1) {
|
|
19661
20028
|
const polled = await client.pollApproval({ approvalId: approval.approvalId });
|
|
19662
20029
|
if (!polled.ok) refuse2(opts, polled, "approval-poll-refused");
|
|
@@ -19664,7 +20031,7 @@ async function waitForApproval(opts, client, approval) {
|
|
|
19664
20031
|
emitProgress(tick, total, "waiting for the browser approval");
|
|
19665
20032
|
await new Promise((resolve) => setTimeout(resolve, interval));
|
|
19666
20033
|
}
|
|
19667
|
-
return "expired";
|
|
20034
|
+
return opts.waitWindowSeconds !== void 0 ? "window-elapsed" : "expired";
|
|
19668
20035
|
}
|
|
19669
20036
|
function failDecision(opts, decided) {
|
|
19670
20037
|
if (decided === "denied") {
|
|
@@ -21178,7 +21545,7 @@ function buildProgram() {
|
|
|
21178
21545
|
...local["revoke"] !== void 0 ? { revoke: local["revoke"] } : {}
|
|
21179
21546
|
});
|
|
21180
21547
|
});
|
|
21181
|
-
program.command("publish").description("Put a verified bundle and its evidence on a page, at a URL you can send to someone. Refuses a run the ruler declined.").argument("<bundleDir>", "bundle directory that has already been verified").option("--to <url>", "the portal to publish to (or set TENDRIL_PORTAL_URL)").option("--name <name>", "library label for this component (default: the bundle manifest's own name)").option("--approve-start", "first publish only: request the browser approval, print the link, persist the pending state and exit \u2014 the tendril_publish tool's phase one").option("--approve-wait", "resume a pending approval: poll until the human decides in the browser, then publish \u2014 phase two").action(async (bundleDir, _o, cmd) => {
|
|
21548
|
+
program.command("publish").description("Put a verified bundle and its evidence on a page, at a URL you can send to someone. Refuses a run the ruler declined.").argument("<bundleDir>", "bundle directory that has already been verified").option("--to <url>", "the portal to publish to (or set TENDRIL_PORTAL_URL)").option("--name <name>", "library label for this component (default: the bundle manifest's own name)").option("--approve-start", "first publish only: request the browser approval, print the link, persist the pending state and exit \u2014 the tendril_publish tool's phase one").option("--approve-wait", "resume a pending approval: poll until the human decides in the browser, then publish \u2014 phase two").option("--wait-window <seconds>", "with --approve-wait: return after this many undecided seconds (exit 0, status approval-pending, the pending slot kept) instead of blocking to the 31-minute cap \u2014 the MCP bridge's bounded-poll shape").action(async (bundleDir, _o, cmd) => {
|
|
21182
21549
|
const flags = globalFlags(cmd.parent);
|
|
21183
21550
|
const local = cmd.opts();
|
|
21184
21551
|
const { runPublish: runPublish2 } = await Promise.resolve().then(() => (init_publish(), publish_exports));
|
|
@@ -21188,7 +21555,10 @@ function buildProgram() {
|
|
|
21188
21555
|
...local["to"] !== void 0 ? { to: local["to"] } : {},
|
|
21189
21556
|
...local["name"] !== void 0 ? { name: local["name"] } : {},
|
|
21190
21557
|
...local["approveStart"] !== void 0 ? { approveStart: local["approveStart"] } : {},
|
|
21191
|
-
...local["approveWait"] !== void 0 ? { approveWait: local["approveWait"] } : {}
|
|
21558
|
+
...local["approveWait"] !== void 0 ? { approveWait: local["approveWait"] } : {},
|
|
21559
|
+
// Parsed, never filtered: a bad value must refuse loudly in
|
|
21560
|
+
// runPublish, not silently become the unbounded wait.
|
|
21561
|
+
...local["waitWindow"] !== void 0 ? { waitWindowSeconds: Number.parseFloat(local["waitWindow"]) } : {}
|
|
21192
21562
|
});
|
|
21193
21563
|
});
|
|
21194
21564
|
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)").option("--profile <file>", "a `tendril profile` artifact; reports whether the bundle followed your codebase conventions (never gates the verdict)").option("--hover-timeout <ms>", "hover actionability budget in ms (default 2000) \u2014 for diagnosing a slow machine; a non-default value is recorded in the report as a verdict caveat, never silently").action(async (bundleDir, _opts, cmd) => {
|