@tendrilapp/cli 0.1.48 → 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 +648 -295
- package/package.json +1 -1
package/dist/tendril.js
CHANGED
|
@@ -1841,6 +1841,29 @@ function emissionTails(setDir, repSlug) {
|
|
|
1841
1841
|
}
|
|
1842
1842
|
return byHead;
|
|
1843
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
|
+
}
|
|
1844
1867
|
function hostInstances(setDir, repSlug) {
|
|
1845
1868
|
const metaFile = resolveRepEnvelopePathIn(path4.join(setDir, repSlug), "metadata");
|
|
1846
1869
|
if (!existsSync4(metaFile)) return [];
|
|
@@ -1871,10 +1894,11 @@ function composeReport(index) {
|
|
|
1871
1894
|
}
|
|
1872
1895
|
}
|
|
1873
1896
|
for (const [variantNodeId, slug] of host.repSlugByVariantNode) {
|
|
1874
|
-
void variantNodeId;
|
|
1875
1897
|
const instances = hostInstances(host.dir, slug);
|
|
1876
1898
|
if (instances.length === 0) continue;
|
|
1877
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);
|
|
1878
1902
|
for (const inst of instances) {
|
|
1879
1903
|
if (visibleEver.get(inst.id) !== true) {
|
|
1880
1904
|
edges.push({
|
|
@@ -1891,10 +1915,12 @@ function composeReport(index) {
|
|
|
1891
1915
|
continue;
|
|
1892
1916
|
}
|
|
1893
1917
|
const tails = tailsByHead.get(inst.id) ?? /* @__PURE__ */ new Map();
|
|
1918
|
+
const cid = cidByInstance.get(inst.id);
|
|
1894
1919
|
const disclosures = [];
|
|
1895
1920
|
const refused = [];
|
|
1896
1921
|
const idCands = index.filter((c) => {
|
|
1897
1922
|
if (c.dir === host.dir || sameComponent(c, host)) return false;
|
|
1923
|
+
if (cid !== void 0 && c.variantNodeIds.has(cid)) return true;
|
|
1898
1924
|
for (const t of tails.keys()) if (c.ownIds.has(t)) return true;
|
|
1899
1925
|
return false;
|
|
1900
1926
|
});
|
|
@@ -1942,7 +1968,15 @@ function composeReport(index) {
|
|
|
1942
1968
|
partners: nameProps.map((p) => ({ dir: p.dir, displayName: p.displayName, ...p.figmaFile !== void 0 ? { figmaFile: p.figmaFile } : {} })),
|
|
1943
1969
|
disclosures: [
|
|
1944
1970
|
...disclosures,
|
|
1945
|
-
`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
|
+
] : []
|
|
1946
1980
|
]
|
|
1947
1981
|
});
|
|
1948
1982
|
} else if (disclosures.length > 0) {
|
|
@@ -1965,8 +1999,10 @@ function composeReport(index) {
|
|
|
1965
1999
|
const group = eligible[0];
|
|
1966
2000
|
const ownedDepths = [];
|
|
1967
2001
|
for (const [t, d] of tails) if (group.some((m) => m.ownIds.has(t))) ownedDepths.push(d);
|
|
1968
|
-
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);
|
|
1969
2004
|
const poseVariants = /* @__PURE__ */ new Set();
|
|
2005
|
+
if (cidBacked) poseVariants.add(cid);
|
|
1970
2006
|
for (const m of group) {
|
|
1971
2007
|
for (const [t, d] of tails) {
|
|
1972
2008
|
if (d !== 1 || !m.ownIds.has(t)) continue;
|
|
@@ -1977,6 +2013,9 @@ function composeReport(index) {
|
|
|
1977
2013
|
}
|
|
1978
2014
|
}
|
|
1979
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
|
+
}
|
|
1980
2019
|
for (const p of nameProps) {
|
|
1981
2020
|
if (!group.some((m) => sameComponent(m, p))) {
|
|
1982
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`);
|
|
@@ -2020,7 +2059,7 @@ function composeReport(index) {
|
|
|
2020
2059
|
},
|
|
2021
2060
|
disclosures: [
|
|
2022
2061
|
...disclosures,
|
|
2023
|
-
|
|
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)`
|
|
2024
2063
|
]
|
|
2025
2064
|
});
|
|
2026
2065
|
}
|
|
@@ -2108,7 +2147,7 @@ function confirmedCompositionStatus(hostSet) {
|
|
|
2108
2147
|
status: ok ? stale ? "stale-supported" : "supported" : stale ? "stale-unsupported" : "unsupported",
|
|
2109
2148
|
instances: entry.instances,
|
|
2110
2149
|
affectedReps: ok ? [] : [...new Set(unsupported.map((i) => i.hostRep))],
|
|
2111
|
-
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" : ""}`
|
|
2112
2151
|
});
|
|
2113
2152
|
}
|
|
2114
2153
|
return { rows, ...malformedEntries.length > 0 ? { malformed: malformedEntries.join("; ") } : {} };
|
|
@@ -2116,6 +2155,105 @@ function confirmedCompositionStatus(hostSet) {
|
|
|
2116
2155
|
function createHashHex(bytes) {
|
|
2117
2156
|
return createHash2("sha256").update(bytes).digest("hex");
|
|
2118
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
|
+
}
|
|
2119
2257
|
var CompositionEntrySchema, norm, FOOTER, kitLabel, toPosixRel, fromStoredRel, pairKeyFor;
|
|
2120
2258
|
var init_compose = __esm({
|
|
2121
2259
|
"packages/figma/src/recording/compose.ts"() {
|
|
@@ -6652,7 +6790,7 @@ var init_bundle_files = __esm({
|
|
|
6652
6790
|
// packages/metadata/src/verify-report.ts
|
|
6653
6791
|
function verifyReportForTransport(report, bundleName) {
|
|
6654
6792
|
const { dir: _dir, ...evidence } = report.evidence;
|
|
6655
|
-
const { eyeCheck: _eyeCheck, ...rest } = report;
|
|
6793
|
+
const { eyeCheck: _eyeCheck, next: _next, composeOpportunities: _composeOpportunities, ...rest } = report;
|
|
6656
6794
|
return { ...rest, bundle: bundleName, evidence };
|
|
6657
6795
|
}
|
|
6658
6796
|
function nonEmptyArray(value) {
|
|
@@ -14439,7 +14577,16 @@ function compositionPairsFor(hostSet, roots) {
|
|
|
14439
14577
|
else invalid++;
|
|
14440
14578
|
}
|
|
14441
14579
|
const decidedKeys = new Set(standing.map((c) => fromStoredRel(c.partner.key)));
|
|
14442
|
-
|
|
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 } : {} };
|
|
14443
14590
|
}
|
|
14444
14591
|
function substitutionPairs(edges, hostSet) {
|
|
14445
14592
|
const pairs = /* @__PURE__ */ new Map();
|
|
@@ -14884,7 +15031,7 @@ async function runRecordPlan(opts) {
|
|
|
14884
15031
|
...opts.figmaFile !== void 0 ? { figmaFile: opts.figmaFile } : {},
|
|
14885
15032
|
...setIdentity
|
|
14886
15033
|
});
|
|
14887
|
-
const channelNote = await assignRestChannel(opts.setDir, manifest, resumed);
|
|
15034
|
+
const channelNote = await assignRestChannel(opts.setDir, manifest, resumed, opts.restClient);
|
|
14888
15035
|
const defaultReports = reportAxisDefaults(symbols, Object.keys(defaults).length > 0 ? defaults : void 0);
|
|
14889
15036
|
const toConfirm = resumed ? [] : defaultReports.filter((r) => (r.rule === "non-interaction" || r.rule === "frequency") && r.domain.length > 1);
|
|
14890
15037
|
const interactionStatesToConfirm = recordsInteractionState(defaultReports) ? void 0 : interactionDisclosure(manifest.component, defaultReports);
|
|
@@ -15074,7 +15221,7 @@ async function runRecordPlan(opts) {
|
|
|
15074
15221
|
}
|
|
15075
15222
|
);
|
|
15076
15223
|
}
|
|
15077
|
-
async function assignRestChannel(setDir, manifest, resumed) {
|
|
15224
|
+
async function assignRestChannel(setDir, manifest, resumed, injected) {
|
|
15078
15225
|
const current = loadManifest(setDir);
|
|
15079
15226
|
if (current.channel !== void 0) {
|
|
15080
15227
|
return { active: true, probeReps: current.probeReps ?? [], note: "REST channel already assigned (frozen with the plan)" };
|
|
@@ -15090,7 +15237,7 @@ async function assignRestChannel(setDir, manifest, resumed) {
|
|
|
15090
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"
|
|
15091
15238
|
};
|
|
15092
15239
|
}
|
|
15093
|
-
const client = await figmaRestClient();
|
|
15240
|
+
const client = injected !== void 0 ? { ok: true, value: injected } : await figmaRestClient();
|
|
15094
15241
|
if (!client.ok) {
|
|
15095
15242
|
return client.kind === "no-credential" ? {
|
|
15096
15243
|
active: false,
|
|
@@ -15151,7 +15298,7 @@ async function runRecordRestFetch(opts) {
|
|
|
15151
15298
|
remediation: `Record them first: ${tendrilCommand(`record next --set ${opts.setDir}`)} names the calls.`
|
|
15152
15299
|
});
|
|
15153
15300
|
}
|
|
15154
|
-
const client = await figmaRestClient();
|
|
15301
|
+
const client = opts.restClient !== void 0 ? { ok: true, value: opts.restClient } : await figmaRestClient();
|
|
15155
15302
|
if (!client.ok) {
|
|
15156
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." });
|
|
15157
15304
|
}
|
|
@@ -15654,9 +15801,14 @@ function runRecordStatus(opts) {
|
|
|
15654
15801
|
const composition = (() => {
|
|
15655
15802
|
try {
|
|
15656
15803
|
const setDir = path45.resolve(opts.setDir);
|
|
15657
|
-
const { open, standing, invalid } = compositionPairsFor(setDir, [path45.dirname(setDir)]);
|
|
15804
|
+
const { open, proposals, standing, invalid } = compositionPairsFor(setDir, [path45.dirname(setDir)]);
|
|
15658
15805
|
return {
|
|
15659
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 })),
|
|
15660
15812
|
confirmed: standing.filter((s) => s.status === "confirmed").length,
|
|
15661
15813
|
declined: standing.filter((s) => s.status === "declined").length,
|
|
15662
15814
|
invalid
|
|
@@ -15690,6 +15842,12 @@ function runRecordStatus(opts) {
|
|
|
15690
15842
|
process.stdout.write(`COMPOSITION ${composition.confirmed} confirmed partner pair(s) on this set (composed pins apply at generation)
|
|
15691
15843
|
`);
|
|
15692
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
|
+
}
|
|
15693
15851
|
if (!("unavailable" in composition) && composition.invalid > 0) {
|
|
15694
15852
|
process.stdout.write(
|
|
15695
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)
|
|
@@ -16255,6 +16413,177 @@ var init_profile_input = __esm({
|
|
|
16255
16413
|
}
|
|
16256
16414
|
});
|
|
16257
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
|
+
|
|
16258
16587
|
// packages/cli/src/commands/verify.ts
|
|
16259
16588
|
var verify_exports = {};
|
|
16260
16589
|
__export(verify_exports, {
|
|
@@ -16278,8 +16607,8 @@ __export(verify_exports, {
|
|
|
16278
16607
|
runVerify: () => runVerify,
|
|
16279
16608
|
verdictCaveatsFor: () => verdictCaveatsFor
|
|
16280
16609
|
});
|
|
16281
|
-
import { existsSync as
|
|
16282
|
-
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";
|
|
16283
16612
|
function interactionCoverage(behaviors) {
|
|
16284
16613
|
const parity = behaviors.filter((b) => b.id.startsWith("parity:"));
|
|
16285
16614
|
const content = behaviors.filter((b) => b.id.startsWith("content-prop-renders("));
|
|
@@ -16517,15 +16846,17 @@ function compositionReport(input) {
|
|
|
16517
16846
|
crops: input.crops ?? { unavailable: cropsUnavailable }
|
|
16518
16847
|
};
|
|
16519
16848
|
}
|
|
16520
|
-
function eyeCheck(bundleDir) {
|
|
16521
|
-
|
|
16849
|
+
function eyeCheck(bundleDir, sheet) {
|
|
16850
|
+
const base = {
|
|
16522
16851
|
command: tendrilCommand(`inspect "${bundleDir}"`),
|
|
16523
|
-
sheetPath:
|
|
16852
|
+
sheetPath: path50.join(bundleDir, "verify-evidence", "inspect.html"),
|
|
16524
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."
|
|
16525
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 } : {} };
|
|
16526
16857
|
}
|
|
16527
16858
|
function evidenceArtifacts(evidenceDir, reps) {
|
|
16528
|
-
const named = (name) =>
|
|
16859
|
+
const named = (name) => existsSync40(path50.join(evidenceDir, name)) ? name : null;
|
|
16529
16860
|
return {
|
|
16530
16861
|
legend: named("diff-legend.txt"),
|
|
16531
16862
|
configs: reps.map((rep) => {
|
|
@@ -16573,7 +16904,7 @@ function taskFromManifest(opts, manifest, setDir) {
|
|
|
16573
16904
|
const adapterSlugs = Object.keys(manifest.propAdapter);
|
|
16574
16905
|
const unmapped = recordedSlugs.filter((s) => !adapterSlugs.includes(s));
|
|
16575
16906
|
const adapterOnly = adapterSlugs.filter((s) => !recordedSlugs.includes(s));
|
|
16576
|
-
const registry = Object.values(TASKS).find((t) =>
|
|
16907
|
+
const registry = Object.values(TASKS).find((t) => path50.resolve(t.set) === path50.resolve(setDir));
|
|
16577
16908
|
const authored = (() => {
|
|
16578
16909
|
if (registry !== void 0) return void 0;
|
|
16579
16910
|
try {
|
|
@@ -16634,19 +16965,19 @@ function verdictCaveatsFor(input) {
|
|
|
16634
16965
|
async function runVerify(opts) {
|
|
16635
16966
|
const callerCwd = process.env["INIT_CWD"] ?? process.cwd();
|
|
16636
16967
|
let recordingSetDrift;
|
|
16637
|
-
const setOverride = opts.set !== void 0 ?
|
|
16638
|
-
opts = { ...opts, bundleDir:
|
|
16639
|
-
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)) {
|
|
16640
16971
|
fail(opts, ExitCode.InputValidation, {
|
|
16641
16972
|
error: `bundle directory not found: ${opts.bundleDir}`,
|
|
16642
16973
|
code: "bundle-missing",
|
|
16643
16974
|
remediation: "Point at a generated bundle directory (containing component.json, the entry module, and styles.css)."
|
|
16644
16975
|
});
|
|
16645
16976
|
}
|
|
16646
|
-
const manifestPath2 =
|
|
16977
|
+
const manifestPath2 = path50.join(opts.bundleDir, "component.json");
|
|
16647
16978
|
let manifest;
|
|
16648
|
-
if (
|
|
16649
|
-
const { manifest: parsed, issues } = readBundleManifest(
|
|
16979
|
+
if (existsSync40(manifestPath2)) {
|
|
16980
|
+
const { manifest: parsed, issues } = readBundleManifest(readFileSync36(manifestPath2, "utf8"));
|
|
16650
16981
|
if (issues.length > 0) {
|
|
16651
16982
|
fail(opts, ExitCode.InputValidation, {
|
|
16652
16983
|
error: `bundle manifest rejected at ingest: ${issues.map((i) => i.message).join("; ")}`,
|
|
@@ -16677,21 +17008,21 @@ async function runVerify(opts) {
|
|
|
16677
17008
|
task = registry;
|
|
16678
17009
|
} else if (manifest !== void 0) {
|
|
16679
17010
|
const resolveSetDir = (p) => {
|
|
16680
|
-
if (
|
|
16681
|
-
const fromRepo =
|
|
16682
|
-
if (
|
|
16683
|
-
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);
|
|
16684
17015
|
};
|
|
16685
17016
|
const setDir = opts.set ?? resolveSetDir(manifest.provenance.recordingSet.path);
|
|
16686
|
-
if (!
|
|
17017
|
+
if (!existsSync40(path50.join(setDir, "recording-set.json")) && !Object.values(TASKS).some((t) => path50.resolve(t.set) === path50.resolve(setDir))) {
|
|
16687
17018
|
fail(opts, ExitCode.RecordingIncomplete, {
|
|
16688
17019
|
error: `recording set not found or unmanifested: ${setDir}`,
|
|
16689
17020
|
code: "recording-set-missing",
|
|
16690
17021
|
remediation: "Pass --set <dir> pointing at the recording set this bundle was generated from."
|
|
16691
17022
|
});
|
|
16692
17023
|
}
|
|
16693
|
-
const registry = Object.values(TASKS).find((t) =>
|
|
16694
|
-
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"))) {
|
|
16695
17026
|
const recordedSlugs = registry.configs.map((c) => c.rep);
|
|
16696
17027
|
const adapterSlugs = Object.keys(manifest.propAdapter);
|
|
16697
17028
|
unmapped = recordedSlugs.filter((s) => !adapterSlugs.includes(s));
|
|
@@ -16723,9 +17054,9 @@ async function runVerify(opts) {
|
|
|
16723
17054
|
recordingSetDrift = { stamped: manifest.provenance.recordingSet.hash, current: hash };
|
|
16724
17055
|
}
|
|
16725
17056
|
for (const name of [manifest.entry, "styles.css", "tokens.css"]) {
|
|
16726
|
-
const p =
|
|
16727
|
-
if (!
|
|
16728
|
-
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)));
|
|
16729
17060
|
if (issues.length > 0) {
|
|
16730
17061
|
fail(opts, ExitCode.InputValidation, {
|
|
16731
17062
|
error: `bundle source rejected at ingest: ${issues.map((i) => i.message).join("; ")}`,
|
|
@@ -16773,7 +17104,7 @@ async function runVerify(opts) {
|
|
|
16773
17104
|
});
|
|
16774
17105
|
}
|
|
16775
17106
|
const bar = BARS2[opts.bar];
|
|
16776
|
-
const evidenceDir =
|
|
17107
|
+
const evidenceDir = path50.join(opts.bundleDir, "verify-evidence");
|
|
16777
17108
|
rmSync7(evidenceDir, { recursive: true, force: true });
|
|
16778
17109
|
const scores = await scoreBundleForTask(task, opts.bundleDir, bar, { evidenceDir, onProgress: emitProgress });
|
|
16779
17110
|
const quality = await checkBundleQuality(
|
|
@@ -16791,7 +17122,7 @@ async function runVerify(opts) {
|
|
|
16791
17122
|
// ASKED, never "follows every convention".
|
|
16792
17123
|
loadCodebaseProfile(opts, opts.profile) ?? void 0
|
|
16793
17124
|
);
|
|
16794
|
-
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");
|
|
16795
17126
|
const occlusion = await checkSiblingOcclusion(task, opts.bundleDir, bundleCss, { ...authorityConfigs !== void 0 ? { authority: authorityConfigs } : {} });
|
|
16796
17127
|
const parity = await checkHoverParity(task, opts.bundleDir, authorityConfigs ?? task.configs, {
|
|
16797
17128
|
...opts.hoverTimeoutMs !== void 0 ? { hoverBudgetMs: opts.hoverTimeoutMs } : {},
|
|
@@ -16812,10 +17143,10 @@ async function runVerify(opts) {
|
|
|
16812
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.`);
|
|
16813
17144
|
}
|
|
16814
17145
|
const verifyCallerCwd = process.env["INIT_CWD"] ?? process.cwd();
|
|
16815
|
-
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: [] };
|
|
16816
17147
|
const staticComposedChecks = composedChecks(opts.bundleDir, task.entry, verifyPins.pins);
|
|
16817
17148
|
const renderExpectations = verifyPins.pins.map((pin) => ({
|
|
16818
|
-
modulePath:
|
|
17149
|
+
modulePath: path50.join(composedModuleDir(pin.partnerName), pin.entryComponent),
|
|
16819
17150
|
component: pin.entryComponent,
|
|
16820
17151
|
stampName: `${pin.pairKey}:${pin.entryComponent}`,
|
|
16821
17152
|
hostReps: [...new Set(pin.instances.map((i) => i.hostRep))],
|
|
@@ -16874,6 +17205,59 @@ async function runVerify(opts) {
|
|
|
16874
17205
|
})();
|
|
16875
17206
|
const compositionBlock = compositionReport({ availability, structural, crops, regions: regionsOut, composedPairs: composedPairs.size });
|
|
16876
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
|
+
})();
|
|
16877
17261
|
const report = {
|
|
16878
17262
|
bundle: opts.bundleDir,
|
|
16879
17263
|
scoredFiles,
|
|
@@ -16967,7 +17351,7 @@ async function runVerify(opts) {
|
|
|
16967
17351
|
behaviors,
|
|
16968
17352
|
evidence: { dir: evidenceDir, ...evidenceArtifacts(evidenceDir, statuses.map((s) => s.rep)) },
|
|
16969
17353
|
composition: compositionBlock,
|
|
16970
|
-
verdict:
|
|
17354
|
+
verdict: verdictWord,
|
|
16971
17355
|
// Run-23 R5: the one-word verdict printed beside "coverage
|
|
16972
17356
|
// denominator unknown" and "operability unverified" read, to a
|
|
16973
17357
|
// JSON consumer who stops at `verdict`, as fully verified — the
|
|
@@ -16976,14 +17360,7 @@ async function runVerify(opts) {
|
|
|
16976
17360
|
// consumer cannot claim it was never offered. Exit codes are
|
|
16977
17361
|
// unchanged, deliberately: each claim in the report is true; the
|
|
16978
17362
|
// caveats say which questions were never answered.
|
|
16979
|
-
verdictCaveats
|
|
16980
|
-
latticeBlock,
|
|
16981
|
-
operability: coverage.operability,
|
|
16982
|
-
compositionUnavailable: "unavailable" in compositionBlock,
|
|
16983
|
-
fontsSubstituted: substitutedFamilies.length > 0,
|
|
16984
|
-
pixelOnlyInteractionPoses: unmappedInteractionEvidence.length,
|
|
16985
|
-
hoverBudgetNondefault: opts.hoverTimeoutMs !== void 0 && opts.hoverTimeoutMs !== DEFAULT_HOVER_BUDGET_MS
|
|
16986
|
-
}),
|
|
17363
|
+
verdictCaveats,
|
|
16987
17364
|
// Machine-readable cause (adversarial review): a cert-bar failure
|
|
16988
17365
|
// with zero pixel/behavior/composition failures was only
|
|
16989
17366
|
// explainable from stderr prose.
|
|
@@ -16996,7 +17373,25 @@ async function runVerify(opts) {
|
|
|
16996
17373
|
motion: motionDisclosure(opts.bundleDir, task.set),
|
|
16997
17374
|
...recordingSetDrift !== void 0 ? { recordingSetDrift } : {},
|
|
16998
17375
|
...substitutedFamilies.length > 0 ? { substitutedFamilies } : {},
|
|
16999
|
-
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 } : {}
|
|
17000
17395
|
};
|
|
17001
17396
|
emitData(opts, report, () => {
|
|
17002
17397
|
for (const s of statuses) {
|
|
@@ -17128,7 +17523,7 @@ ${certified}/${statuses.length} certified \xB7 ${report.coverage.pass}/${statuse
|
|
|
17128
17523
|
}
|
|
17129
17524
|
process.stdout.write(`environment: chrome=${report.environment.chromeVersion ?? report.environment.chrome} fonts=${report.environment.fontsManifestSha256 ?? "UNRESOLVED"}
|
|
17130
17525
|
`);
|
|
17131
|
-
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")));
|
|
17132
17527
|
if (shippedFonts.length > 0 && substitutedFamilies.length === 0) {
|
|
17133
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)
|
|
17134
17529
|
`);
|
|
@@ -17141,8 +17536,58 @@ ${certified}/${statuses.length} certified \xB7 ${report.coverage.pass}/${statuse
|
|
|
17141
17536
|
}
|
|
17142
17537
|
process.stdout.write(`evidence: ${evidenceDir} (render/ref/diff per config)
|
|
17143
17538
|
`);
|
|
17144
|
-
|
|
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"
|
|
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
|
|
17145
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
|
+
}
|
|
17146
17591
|
});
|
|
17147
17592
|
if (opts.bar === "cert" && substitutedFamilies.length > 0) {
|
|
17148
17593
|
const names = substitutedFamilies.map((f) => `"${f}"`).join(", ");
|
|
@@ -17183,7 +17628,7 @@ ${certified}/${statuses.length} certified \xB7 ${report.coverage.pass}/${statuse
|
|
|
17183
17628
|
persistReport(opts, report, evidenceDir);
|
|
17184
17629
|
}
|
|
17185
17630
|
function persistReport(opts, report, evidenceDir) {
|
|
17186
|
-
if (!
|
|
17631
|
+
if (!existsSync40(evidenceDir)) return;
|
|
17187
17632
|
const rulerExit = process.exitCode === void 0 ? 0 : Number(process.exitCode);
|
|
17188
17633
|
const withExit = {
|
|
17189
17634
|
...report,
|
|
@@ -17191,9 +17636,9 @@ function persistReport(opts, report, evidenceDir) {
|
|
|
17191
17636
|
...rulerExit !== 0 ? { rulerRefusal: EXIT_REFUSALS[rulerExit] ?? `ruler exited ${rulerExit}` } : {}
|
|
17192
17637
|
};
|
|
17193
17638
|
try {
|
|
17194
|
-
|
|
17195
|
-
|
|
17196
|
-
`${JSON.stringify(verifyReportForTransport(withExit,
|
|
17639
|
+
writeFileSync18(
|
|
17640
|
+
path50.join(evidenceDir, VERIFY_REPORT_FILENAME),
|
|
17641
|
+
`${JSON.stringify(verifyReportForTransport(withExit, path50.basename(opts.bundleDir)), null, 2)}
|
|
17197
17642
|
`
|
|
17198
17643
|
);
|
|
17199
17644
|
} catch (e) {
|
|
@@ -17211,6 +17656,8 @@ var init_verify = __esm({
|
|
|
17211
17656
|
init_src5();
|
|
17212
17657
|
init_environment();
|
|
17213
17658
|
init_font_guidance();
|
|
17659
|
+
init_compose2();
|
|
17660
|
+
init_inspect();
|
|
17214
17661
|
init_invocation();
|
|
17215
17662
|
init_output();
|
|
17216
17663
|
init_profile_input();
|
|
@@ -17243,17 +17690,17 @@ __export(engine_exports, {
|
|
|
17243
17690
|
runEngineBrief: () => runEngineBrief,
|
|
17244
17691
|
runEngineScore: () => runEngineScore
|
|
17245
17692
|
});
|
|
17246
|
-
import { appendFileSync, existsSync as
|
|
17247
|
-
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";
|
|
17248
17695
|
function resolveEngineTask(opts, callerCwd) {
|
|
17249
|
-
const asPath =
|
|
17250
|
-
const isSet =
|
|
17696
|
+
const asPath = path51.resolve(callerCwd, opts.taskOrSet);
|
|
17697
|
+
const isSet = existsSync41(path51.join(asPath, "recording-set.json"));
|
|
17251
17698
|
const registry = TASKS[opts.taskOrSet];
|
|
17252
17699
|
if (registry !== void 0 && !isSet) return { task: registry, name: opts.taskOrSet, ref: opts.taskOrSet, disclosures: [], interactionEvidence: void 0 };
|
|
17253
17700
|
if (isSet) {
|
|
17254
17701
|
const manifest = loadManifest(asPath);
|
|
17255
17702
|
const missing = manifest.reps.filter(
|
|
17256
|
-
(r) => !repEnvelopeExists(asPath, r.slug, "metadata") || !
|
|
17703
|
+
(r) => !repEnvelopeExists(asPath, r.slug, "metadata") || !existsSync41(path51.join(asPath, r.slug, "get_design_context.json"))
|
|
17257
17704
|
);
|
|
17258
17705
|
if (missing.length > 0) {
|
|
17259
17706
|
fail(opts, ExitCode.RecordingIncomplete, {
|
|
@@ -17267,7 +17714,7 @@ function resolveEngineTask(opts, callerCwd) {
|
|
|
17267
17714
|
for (const d of authored.disclosures) warn(opts, d);
|
|
17268
17715
|
return {
|
|
17269
17716
|
task: authored.task,
|
|
17270
|
-
name:
|
|
17717
|
+
name: path51.basename(asPath),
|
|
17271
17718
|
ref: asPath,
|
|
17272
17719
|
disclosures: authored.disclosures,
|
|
17273
17720
|
interactionEvidence: authored.api.interactionEvidence,
|
|
@@ -17296,9 +17743,9 @@ function runEngineBrief(opts) {
|
|
|
17296
17743
|
const { task, name, ref, disclosures } = resolveEngineTask(opts, callerCwd);
|
|
17297
17744
|
void reportRunPresence(name, "implementing");
|
|
17298
17745
|
const bar = BARS3[opts.bar];
|
|
17299
|
-
if (
|
|
17746
|
+
if (existsSync41(path51.join(task.set, "recording-set.json"))) {
|
|
17300
17747
|
try {
|
|
17301
|
-
const { open, skippedParent } = compositionPairsFor(
|
|
17748
|
+
const { open, proposals, skippedParent } = compositionPairsFor(path51.resolve(task.set), [opts.library !== void 0 ? path51.resolve(callerCwd, opts.library) : callerCwd]);
|
|
17302
17749
|
if (skippedParent !== void 0) {
|
|
17303
17750
|
disclosures.push(
|
|
17304
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.`
|
|
@@ -17307,7 +17754,12 @@ function runEngineBrief(opts) {
|
|
|
17307
17754
|
if (open.length > 0) {
|
|
17308
17755
|
const componentNames = [...new Set(open.map((p) => p.displayName))];
|
|
17309
17756
|
disclosures.push(
|
|
17310
|
-
`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."}`
|
|
17311
17763
|
);
|
|
17312
17764
|
}
|
|
17313
17765
|
} catch (err) {
|
|
@@ -17324,9 +17776,9 @@ ${disclosures.map((d) => `- ${d}`).join("\n")}` : "";
|
|
|
17324
17776
|
const brief = buildBrief(task.systemApi, bar, { colorScheme: recordingIsDark(task) ? "dark" : "light" }) + disclosureBlock + motionBriefSection(task.set) + conventions;
|
|
17325
17777
|
const segments = buildSegments(task, "files");
|
|
17326
17778
|
let notRecorded;
|
|
17327
|
-
const manifestPath2 =
|
|
17328
|
-
if (
|
|
17329
|
-
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;
|
|
17330
17782
|
}
|
|
17331
17783
|
const unverified = notRecorded !== void 0 && notRecorded !== "" ? `
|
|
17332
17784
|
|
|
@@ -17334,7 +17786,7 @@ ${disclosures.map((d) => `- ${d}`).join("\n")}` : "";
|
|
|
17334
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.
|
|
17335
17787
|
${notRecorded}` : "";
|
|
17336
17788
|
let fontProvisioning;
|
|
17337
|
-
if (
|
|
17789
|
+
if (existsSync41(manifestPath2)) {
|
|
17338
17790
|
const missingFams = unprovisionedFamilies(task.set);
|
|
17339
17791
|
const unprovided = unprovisionedFaces(task.set);
|
|
17340
17792
|
const weightOnly = missingFams.length === 0;
|
|
@@ -17356,7 +17808,7 @@ ${notRecorded}` : "";
|
|
|
17356
17808
|
};
|
|
17357
17809
|
}
|
|
17358
17810
|
}
|
|
17359
|
-
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]);
|
|
17360
17812
|
const jsxProp = ([k, v]) => typeof v === "string" ? `${k}=${JSON.stringify(v)}` : `${k}={${JSON.stringify(v)}}`;
|
|
17361
17813
|
const composedBlock = pinsResult.pins.length === 0 && pinsResult.issues.length === 0 ? "" : (pinsResult.pins.length > 0 ? `
|
|
17362
17814
|
|
|
@@ -17392,10 +17844,10 @@ ${pinsResult.issues.map((i) => `- ${i}`).join("\n")}` : "");
|
|
|
17392
17844
|
|
|
17393
17845
|
=== TASK PAYLOAD (recorded truth, verbatim) ===
|
|
17394
17846
|
${segments}`;
|
|
17395
|
-
const payloadFile =
|
|
17396
|
-
const candidateDirSuggestion =
|
|
17397
|
-
mkdirSync12(
|
|
17398
|
-
|
|
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);
|
|
17399
17851
|
emitData(
|
|
17400
17852
|
opts,
|
|
17401
17853
|
{
|
|
@@ -17441,7 +17893,7 @@ ${segments}`;
|
|
|
17441
17893
|
// command must search the same bundle roots the pins came
|
|
17442
17894
|
// from, or the oracle and the brief describe different worlds.
|
|
17443
17895
|
`Run \`${tendrilCommand(
|
|
17444
|
-
`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`
|
|
17445
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.`,
|
|
17446
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).",
|
|
17447
17899
|
"Never edit recording sets; never claim scores yourself \u2014 only the score command's output counts."
|
|
@@ -17456,8 +17908,8 @@ ${segments}`;
|
|
|
17456
17908
|
);
|
|
17457
17909
|
}
|
|
17458
17910
|
function appendScoreHistory(candidateDir, entry) {
|
|
17459
|
-
const file =
|
|
17460
|
-
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;
|
|
17461
17913
|
const round = entry.event === "round-start" ? starts + 1 : Math.max(1, starts);
|
|
17462
17914
|
appendFileSync(file, `${JSON.stringify({ at: (/* @__PURE__ */ new Date()).toISOString(), round, ...entry })}
|
|
17463
17915
|
`);
|
|
@@ -17465,10 +17917,10 @@ function appendScoreHistory(candidateDir, entry) {
|
|
|
17465
17917
|
async function runEngineScore(opts) {
|
|
17466
17918
|
requireEntitlement(opts);
|
|
17467
17919
|
const callerCwd = process.env["INIT_CWD"] ?? process.cwd();
|
|
17468
|
-
const candidateDir =
|
|
17920
|
+
const candidateDir = path51.resolve(callerCwd, opts.candidateDir);
|
|
17469
17921
|
const { task, name, apiPin, interactionEvidence, unmappedInteractionEvidence: interactionEvidenceUnmapped, satisfiabilityDemoted, expertContract } = resolveEngineTask(opts, callerCwd);
|
|
17470
17922
|
void reportRunPresence(name, "implementing");
|
|
17471
|
-
if (!
|
|
17923
|
+
if (!existsSync41(candidateDir)) {
|
|
17472
17924
|
fail(opts, ExitCode.InputValidation, {
|
|
17473
17925
|
error: `candidate directory not found: ${candidateDir}`,
|
|
17474
17926
|
code: "candidate-missing",
|
|
@@ -17493,10 +17945,10 @@ async function runEngineScore(opts) {
|
|
|
17493
17945
|
for (const g of missingWeights(task.set)) {
|
|
17494
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)`);
|
|
17495
17947
|
}
|
|
17496
|
-
if (opts.rebind !== true &&
|
|
17948
|
+
if (opts.rebind !== true && existsSync41(path51.join(candidateDir, "component.json"))) {
|
|
17497
17949
|
const prior = (() => {
|
|
17498
17950
|
try {
|
|
17499
|
-
const read = readBundleManifest(
|
|
17951
|
+
const read = readBundleManifest(readFileSync37(path51.join(candidateDir, "component.json"), "utf8"));
|
|
17500
17952
|
return read.manifest === void 0 ? { unreadable: true } : { hash: read.manifest.provenance.recordingSet.hash, path: read.manifest.provenance.recordingSet.path };
|
|
17501
17953
|
} catch {
|
|
17502
17954
|
return { unreadable: true };
|
|
@@ -17518,13 +17970,13 @@ async function runEngineScore(opts) {
|
|
|
17518
17970
|
}
|
|
17519
17971
|
}
|
|
17520
17972
|
const bar = BARS3[opts.bar];
|
|
17521
|
-
const evidenceDir =
|
|
17973
|
+
const evidenceDir = path51.join(candidateDir, "verify-evidence");
|
|
17522
17974
|
const scores = await scoreBundleForTask(task, candidateDir, bar, { evidenceDir, onProgress: emitProgress });
|
|
17523
17975
|
const hoverOpts = opts.hoverTimeoutMs !== void 0 ? { hoverBudgetMs: opts.hoverTimeoutMs } : {};
|
|
17524
17976
|
const parity = await checkHoverParity(task, candidateDir, task.configs, { ...hoverOpts, failureShotDir: evidenceDir });
|
|
17525
17977
|
const occlusionRows = await checkSiblingOcclusion(task, candidateDir, candidateCss(candidateDir), { authority: task.configs });
|
|
17526
17978
|
const occlusionFailures = occlusionRows.filter((o) => !o.pass);
|
|
17527
|
-
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]);
|
|
17528
17980
|
const composition = composedChecks(candidateDir, task.entry, scorePins.pins);
|
|
17529
17981
|
const behaviors = [...await checkBehaviors(task, candidateDir, hoverOpts), ...parity, ...composition];
|
|
17530
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)";
|
|
@@ -17662,7 +18114,18 @@ ${scorePins.issues.map((i) => `- ${i}`).join("\n")}` : "";
|
|
|
17662
18114
|
// bundles verify then FAILED on the interaction-evidence gate —
|
|
17663
18115
|
// the oracle must say what verify will say, including this.
|
|
17664
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" } : {},
|
|
17665
|
-
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
|
+
} : {}
|
|
17666
18129
|
},
|
|
17667
18130
|
() => {
|
|
17668
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}]` : ""}
|
|
@@ -17744,11 +18207,11 @@ var codeconnect_exports = {};
|
|
|
17744
18207
|
__export(codeconnect_exports, {
|
|
17745
18208
|
runCodeConnect: () => runCodeConnect
|
|
17746
18209
|
});
|
|
17747
|
-
import { existsSync as
|
|
17748
|
-
import
|
|
18210
|
+
import { existsSync as existsSync42, readFileSync as readFileSync38, writeFileSync as writeFileSync20 } from "node:fs";
|
|
18211
|
+
import path52 from "node:path";
|
|
17749
18212
|
function runCodeConnect(opts) {
|
|
17750
18213
|
const callerCwd = process.env["INIT_CWD"] ?? process.cwd();
|
|
17751
|
-
const bundleDir =
|
|
18214
|
+
const bundleDir = path52.resolve(callerCwd, opts.bundleDir);
|
|
17752
18215
|
let url;
|
|
17753
18216
|
try {
|
|
17754
18217
|
url = new URL(opts.figmaUrl);
|
|
@@ -17764,7 +18227,7 @@ function runCodeConnect(opts) {
|
|
|
17764
18227
|
}
|
|
17765
18228
|
let manifest;
|
|
17766
18229
|
try {
|
|
17767
|
-
const read = readBundleManifest(
|
|
18230
|
+
const read = readBundleManifest(readFileSync38(path52.join(bundleDir, "component.json"), "utf8"));
|
|
17768
18231
|
if (read.manifest === void 0) throw new Error(read.issues.map((i) => i.message).join("; ") || "no component.json");
|
|
17769
18232
|
manifest = read.manifest;
|
|
17770
18233
|
} catch (err) {
|
|
@@ -17774,8 +18237,8 @@ function runCodeConnect(opts) {
|
|
|
17774
18237
|
remediation: "Point at a bundle produced by the Tendril pipeline (it carries component.json), and verify it first."
|
|
17775
18238
|
});
|
|
17776
18239
|
}
|
|
17777
|
-
const setDir =
|
|
17778
|
-
if (!
|
|
18240
|
+
const setDir = path52.resolve(callerCwd, opts.set ?? manifest.provenance.recordingSet.path);
|
|
18241
|
+
if (!existsSync42(path52.join(setDir, "recording-set.json"))) {
|
|
17779
18242
|
fail(opts, ExitCode.InputValidation, {
|
|
17780
18243
|
error: `recording set not found at ${setDir}`,
|
|
17781
18244
|
code: "codeconnect-no-set",
|
|
@@ -17797,9 +18260,9 @@ function runCodeConnect(opts) {
|
|
|
17797
18260
|
const recManifest = loadManifest(setDir);
|
|
17798
18261
|
const poseNames = recManifest.latticeNames ?? recManifest.reps.map((r) => {
|
|
17799
18262
|
const meta = resolveRepEnvelopePath(setDir, r.slug, "metadata");
|
|
17800
|
-
if (!
|
|
18263
|
+
if (!existsSync42(meta)) return void 0;
|
|
17801
18264
|
try {
|
|
17802
|
-
return /name="([^"]*)"/.exec(envelopeTextContent(JSON.parse(
|
|
18265
|
+
return /name="([^"]*)"/.exec(envelopeTextContent(JSON.parse(readFileSync38(meta, "utf8"))))?.[1];
|
|
17803
18266
|
} catch {
|
|
17804
18267
|
return void 0;
|
|
17805
18268
|
}
|
|
@@ -17864,7 +18327,7 @@ function runCodeConnect(opts) {
|
|
|
17864
18327
|
axisLines.push(`const ${varName} = instance.getEnum(${q(axis)}, { ${entries.join(", ")} })`);
|
|
17865
18328
|
fragmentVars.push(varName);
|
|
17866
18329
|
}
|
|
17867
|
-
const entryRel =
|
|
18330
|
+
const entryRel = path52.relative(callerCwd, path52.join(bundleDir, manifest.entry));
|
|
17868
18331
|
const trust = manifest.trustStatement.split("\n")[0] ?? "";
|
|
17869
18332
|
const lines = [
|
|
17870
18333
|
`// url=${opts.figmaUrl}`,
|
|
@@ -17888,8 +18351,8 @@ function runCodeConnect(opts) {
|
|
|
17888
18351
|
`}`,
|
|
17889
18352
|
``
|
|
17890
18353
|
].join("\n");
|
|
17891
|
-
const outFile =
|
|
17892
|
-
|
|
18354
|
+
const outFile = path52.resolve(callerCwd, opts.out ?? path52.join(bundleDir, `${component}.figma.ts`));
|
|
18355
|
+
writeFileSync20(outFile, lines);
|
|
17893
18356
|
emitData(
|
|
17894
18357
|
opts,
|
|
17895
18358
|
{
|
|
@@ -17929,17 +18392,17 @@ var init_codeconnect = __esm({
|
|
|
17929
18392
|
|
|
17930
18393
|
// packages/mcp/src/server.ts
|
|
17931
18394
|
import { createHash as createHash11 } from "node:crypto";
|
|
17932
|
-
import { existsSync as
|
|
18395
|
+
import { existsSync as existsSync43, mkdtempSync as mkdtempSync3, readFileSync as readFileSync39, readdirSync as readdirSync17, writeFileSync as writeFileSync21 } from "node:fs";
|
|
17933
18396
|
import os8 from "node:os";
|
|
17934
|
-
import
|
|
18397
|
+
import path53 from "node:path";
|
|
17935
18398
|
import { fileURLToPath as fileURLToPath6 } from "node:url";
|
|
17936
18399
|
import { z as z15 } from "zod";
|
|
17937
18400
|
function sourceHash() {
|
|
17938
|
-
const dir =
|
|
18401
|
+
const dir = path53.dirname(fileURLToPath6(import.meta.url));
|
|
17939
18402
|
const h = createHash11("sha256");
|
|
17940
|
-
for (const f of
|
|
18403
|
+
for (const f of readdirSync17(dir).filter((n) => n.endsWith(".ts")).sort()) {
|
|
17941
18404
|
h.update(f);
|
|
17942
|
-
h.update(
|
|
18405
|
+
h.update(readFileSync39(path53.join(dir, f)));
|
|
17943
18406
|
}
|
|
17944
18407
|
return h.digest("hex").slice(0, 16);
|
|
17945
18408
|
}
|
|
@@ -17947,10 +18410,10 @@ var REPO_ROOT3, CLI_BIN, BUNDLED_CLI, CLI_SPAWN, str, optStr, TOOLS, BOOT_HASH;
|
|
|
17947
18410
|
var init_server = __esm({
|
|
17948
18411
|
"packages/mcp/src/server.ts"() {
|
|
17949
18412
|
"use strict";
|
|
17950
|
-
REPO_ROOT3 =
|
|
17951
|
-
CLI_BIN =
|
|
17952
|
-
BUNDLED_CLI =
|
|
17953
|
-
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] };
|
|
17954
18417
|
str = (d) => z15.string().describe(d);
|
|
17955
18418
|
optStr = (d) => z15.string().optional().describe(d);
|
|
17956
18419
|
TOOLS = [
|
|
@@ -17981,13 +18444,13 @@ var init_server = __esm({
|
|
|
17981
18444
|
const single = i["metadata"];
|
|
17982
18445
|
const parts = i["metadataParts"];
|
|
17983
18446
|
if (single !== void 0 || parts !== void 0) {
|
|
17984
|
-
const tmp =
|
|
18447
|
+
const tmp = path53.join(mkdtempSync3(path53.join(os8.tmpdir(), "tendril-envelope-")), "response.txt");
|
|
17985
18448
|
if (single !== void 0) {
|
|
17986
|
-
|
|
18449
|
+
writeFileSync21(tmp, single);
|
|
17987
18450
|
argvOut.push("--metadata-raw-file", tmp);
|
|
17988
18451
|
} else {
|
|
17989
18452
|
if (parts.length === 0) throw new Error("`metadataParts` must be a non-empty array of block texts");
|
|
17990
|
-
|
|
18453
|
+
writeFileSync21(tmp, JSON.stringify(parts));
|
|
17991
18454
|
argvOut.push("--metadata-raw-parts-file", tmp);
|
|
17992
18455
|
}
|
|
17993
18456
|
}
|
|
@@ -18062,12 +18525,12 @@ var init_server = __esm({
|
|
|
18062
18525
|
},
|
|
18063
18526
|
{
|
|
18064
18527
|
name: "tendril_publish_wait",
|
|
18065
|
-
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).',
|
|
18066
18529
|
schema: z15.object({
|
|
18067
18530
|
bundleDir: str("the same bundle directory tendril_publish was called with"),
|
|
18068
18531
|
portal: optStr("portal origin override (must match tendril_publish's)")
|
|
18069
18532
|
}),
|
|
18070
|
-
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"]] : []]
|
|
18071
18534
|
},
|
|
18072
18535
|
{
|
|
18073
18536
|
name: "tendril_record_next",
|
|
@@ -18116,14 +18579,14 @@ var init_server = __esm({
|
|
|
18116
18579
|
const bridge = (label, single, parts) => {
|
|
18117
18580
|
if (single !== void 0 && parts !== void 0) throw new Error(`pass at most one of \`${label}\` and \`${label}Parts\``);
|
|
18118
18581
|
if (single === void 0 && parts === void 0) return;
|
|
18119
|
-
const tmp =
|
|
18582
|
+
const tmp = path53.join(mkdtempSync3(path53.join(os8.tmpdir(), "tendril-envelope-")), "response.txt");
|
|
18120
18583
|
if (single !== void 0) {
|
|
18121
|
-
|
|
18584
|
+
writeFileSync21(tmp, single);
|
|
18122
18585
|
argvOut.push(`--${label}-file`, tmp);
|
|
18123
18586
|
} else {
|
|
18124
18587
|
const blocks = parts;
|
|
18125
18588
|
if (blocks.length === 0) throw new Error(`\`${label}Parts\` must be a non-empty array of block texts`);
|
|
18126
|
-
|
|
18589
|
+
writeFileSync21(tmp, JSON.stringify(blocks));
|
|
18127
18590
|
argvOut.push(`--${label}-parts-file`, tmp);
|
|
18128
18591
|
}
|
|
18129
18592
|
};
|
|
@@ -18164,12 +18627,12 @@ var init_server = __esm({
|
|
|
18164
18627
|
const file = i["file"];
|
|
18165
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)");
|
|
18166
18629
|
if (file !== void 0) return [...base, "--file", file];
|
|
18167
|
-
const tmp =
|
|
18630
|
+
const tmp = path53.join(mkdtempSync3(path53.join(os8.tmpdir(), "tendril-envelope-")), "response.txt");
|
|
18168
18631
|
if (text !== void 0) {
|
|
18169
|
-
|
|
18632
|
+
writeFileSync21(tmp, text);
|
|
18170
18633
|
return [...base, "--file", tmp, "--raw"];
|
|
18171
18634
|
}
|
|
18172
|
-
|
|
18635
|
+
writeFileSync21(tmp, JSON.stringify(texts));
|
|
18173
18636
|
return [...base, "--file", tmp, "--raw-parts"];
|
|
18174
18637
|
}
|
|
18175
18638
|
},
|
|
@@ -18229,7 +18692,7 @@ var init_server = __esm({
|
|
|
18229
18692
|
},
|
|
18230
18693
|
{
|
|
18231
18694
|
name: "tendril_engine_score",
|
|
18232
|
-
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.",
|
|
18233
18696
|
schema: z15.object({
|
|
18234
18697
|
taskOrSet: str("reference task name or recording-set directory"),
|
|
18235
18698
|
candidateDir: str("directory containing the proposed bundle files"),
|
|
@@ -18272,7 +18735,7 @@ var init_server = __esm({
|
|
|
18272
18735
|
},
|
|
18273
18736
|
{
|
|
18274
18737
|
name: "tendril_verify",
|
|
18275
|
-
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.",
|
|
18276
18739
|
schema: z15.object({
|
|
18277
18740
|
bundleDir: str("bundle directory to verify"),
|
|
18278
18741
|
bar: optStr("pass (default) or cert"),
|
|
@@ -18326,13 +18789,13 @@ __export(permissions_exports, {
|
|
|
18326
18789
|
runPermissions: () => runPermissions,
|
|
18327
18790
|
writeSelection: () => writeSelection
|
|
18328
18791
|
});
|
|
18329
|
-
import { existsSync as
|
|
18792
|
+
import { existsSync as existsSync44, mkdirSync as mkdirSync13, readFileSync as readFileSync40, writeFileSync as writeFileSync22 } from "node:fs";
|
|
18330
18793
|
import os9 from "node:os";
|
|
18331
|
-
import
|
|
18794
|
+
import path54 from "node:path";
|
|
18332
18795
|
function mergeAllowlist(file, entries, denyEntries = []) {
|
|
18333
18796
|
let settings = {};
|
|
18334
|
-
if (
|
|
18335
|
-
settings = JSON.parse(
|
|
18797
|
+
if (existsSync44(file) && readFileSync40(file, "utf8").trim() !== "") {
|
|
18798
|
+
settings = JSON.parse(readFileSync40(file, "utf8"));
|
|
18336
18799
|
if (settings === null || typeof settings !== "object" || Array.isArray(settings)) throw new Error("settings root is not an object");
|
|
18337
18800
|
}
|
|
18338
18801
|
const permissions = settings["permissions"] ??= {};
|
|
@@ -18352,8 +18815,8 @@ function mergeAllowlist(file, entries, denyEntries = []) {
|
|
|
18352
18815
|
}
|
|
18353
18816
|
if (added.length > 0 || denyAdded.length > 0) {
|
|
18354
18817
|
allow.push(...added);
|
|
18355
|
-
mkdirSync13(
|
|
18356
|
-
|
|
18818
|
+
mkdirSync13(path54.dirname(file), { recursive: true });
|
|
18819
|
+
writeFileSync22(file, `${JSON.stringify(settings, null, 2)}
|
|
18357
18820
|
`);
|
|
18358
18821
|
}
|
|
18359
18822
|
return { added, alreadyPresent, denyAdded };
|
|
@@ -18417,7 +18880,7 @@ async function runPermissions(flags) {
|
|
|
18417
18880
|
}
|
|
18418
18881
|
if (flags.write) {
|
|
18419
18882
|
const base = process.env["INIT_CWD"] ?? process.cwd();
|
|
18420
|
-
const file = flags.user ?
|
|
18883
|
+
const file = flags.user ? path54.join(os9.homedir(), ".claude", "settings.json") : path54.join(base, ".claude", "settings.local.json");
|
|
18421
18884
|
const { entries, denyEntries } = writeSelection(result, flags.user === true);
|
|
18422
18885
|
if (flags.dryRun) {
|
|
18423
18886
|
emitData(flags, { file, wouldAdd: entries, wouldDeny: denyEntries }, () => {
|
|
@@ -18560,168 +19023,6 @@ var init_permissions = __esm({
|
|
|
18560
19023
|
}
|
|
18561
19024
|
});
|
|
18562
19025
|
|
|
18563
|
-
// packages/cli/src/commands/inspect.ts
|
|
18564
|
-
var inspect_exports = {};
|
|
18565
|
-
__export(inspect_exports, {
|
|
18566
|
-
INSPECT_DESCRIPTION: () => INSPECT_DESCRIPTION,
|
|
18567
|
-
runInspect: () => runInspect
|
|
18568
|
-
});
|
|
18569
|
-
import { existsSync as existsSync44, readFileSync as readFileSync40, writeFileSync as writeFileSync22 } from "node:fs";
|
|
18570
|
-
import path54 from "node:path";
|
|
18571
|
-
function readVerifyReport(evidenceDir) {
|
|
18572
|
-
const p = path54.join(evidenceDir, VERIFY_REPORT_FILENAME);
|
|
18573
|
-
if (!existsSync44(p)) return void 0;
|
|
18574
|
-
try {
|
|
18575
|
-
return JSON.parse(readFileSync40(p, "utf8"));
|
|
18576
|
-
} catch {
|
|
18577
|
-
return void 0;
|
|
18578
|
-
}
|
|
18579
|
-
}
|
|
18580
|
-
function verdictBlock(report) {
|
|
18581
|
-
if (report === void 0) {
|
|
18582
|
-
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>`;
|
|
18583
|
-
}
|
|
18584
|
-
const buckets = /* @__PURE__ */ new Map();
|
|
18585
|
-
for (const c of report.configs) buckets.set(c.status, (buckets.get(c.status) ?? 0) + 1);
|
|
18586
|
-
const split = [...buckets.entries()].map(([k, n]) => `<span class="tier t-${esc(k)}">${n} ${esc(k)}</span>`).join("");
|
|
18587
|
-
const failed = report.behaviors.filter((b) => !b.pass);
|
|
18588
|
-
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>`;
|
|
18589
|
-
}
|
|
18590
|
-
function scoreLine(report, rep) {
|
|
18591
|
-
const c = report?.configs.find((x) => x.rep === rep);
|
|
18592
|
-
if (c === void 0) return "";
|
|
18593
|
-
const ex = c.exact !== void 0 ? ` <span class="exact">measured ${c.exact.similarity.toFixed(6)} / ${c.exact.inkRecall.toFixed(6)}</span>` : "";
|
|
18594
|
-
const why = Array.isArray(c.demotedBy) && c.demotedBy.length > 0 ? `<span class="demoted">demoted: ${esc(c.demotedBy.join("; "))}</span>` : "";
|
|
18595
|
-
const err = typeof c.error === "string" ? `<span class="demoted">${esc(c.error)}</span>` : "";
|
|
18596
|
-
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>`;
|
|
18597
|
-
}
|
|
18598
|
-
async function runInspect(opts) {
|
|
18599
|
-
if (opts.describe) {
|
|
18600
|
-
printDescription(INSPECT_DESCRIPTION);
|
|
18601
|
-
return;
|
|
18602
|
-
}
|
|
18603
|
-
const bundleDir = path54.resolve(opts.bundleDir);
|
|
18604
|
-
const evidenceDir = path54.join(bundleDir, "verify-evidence");
|
|
18605
|
-
const manifestPath2 = path54.join(bundleDir, "component.json");
|
|
18606
|
-
if (!existsSync44(evidenceDir) || !existsSync44(manifestPath2)) {
|
|
18607
|
-
fail(opts, ExitCode.InputValidation, {
|
|
18608
|
-
error: `nothing to inspect in ${bundleDir} \u2014 ${existsSync44(manifestPath2) ? "no verify-evidence directory" : "no component.json"}`,
|
|
18609
|
-
code: "no-evidence",
|
|
18610
|
-
remediation: `Run \`${tendrilCommand(`verify ${bundleDir}`)}\` first \u2014 inspect reads the ref/render evidence that run writes.`
|
|
18611
|
-
});
|
|
18612
|
-
}
|
|
18613
|
-
const { manifest } = readBundleManifest(readFileSync40(manifestPath2, "utf8"));
|
|
18614
|
-
if (manifest === void 0) {
|
|
18615
|
-
fail(opts, ExitCode.InputValidation, {
|
|
18616
|
-
error: "component.json did not parse as a bundle manifest",
|
|
18617
|
-
code: "no-mount-contract",
|
|
18618
|
-
remediation: "Re-emit the bundle (score/verify rewrite component.json), then re-run inspect."
|
|
18619
|
-
});
|
|
18620
|
-
}
|
|
18621
|
-
const setDir = path54.resolve(opts.set ?? manifest.provenance.recordingSet.path);
|
|
18622
|
-
const report = readVerifyReport(evidenceDir);
|
|
18623
|
-
const reps = Object.keys(manifest.propAdapter).filter((rep) => existsSync44(path54.join(evidenceDir, `${rep}-ref.png`)) && existsSync44(path54.join(evidenceDir, `${rep}-render.png`)));
|
|
18624
|
-
if (reps.length === 0) {
|
|
18625
|
-
fail(opts, ExitCode.InputValidation, {
|
|
18626
|
-
error: "verify-evidence holds no ref/render pairs for this bundle's configs",
|
|
18627
|
-
code: "no-evidence",
|
|
18628
|
-
remediation: `Run \`${tendrilCommand(`verify ${bundleDir}`)}\` first \u2014 inspect reads the ref/render evidence that run writes.`
|
|
18629
|
-
});
|
|
18630
|
-
}
|
|
18631
|
-
let crops = 0;
|
|
18632
|
-
const sections = [];
|
|
18633
|
-
for (const rep of reps) {
|
|
18634
|
-
const ref = new Uint8Array(readFileSync40(path54.join(evidenceDir, `${rep}-ref.png`)));
|
|
18635
|
-
const render = new Uint8Array(readFileSync40(path54.join(evidenceDir, `${rep}-render.png`)));
|
|
18636
|
-
const nodes = smallSemanticNodes(setDir, rep, opts.maxArea ?? 1024).slice(0, 12);
|
|
18637
|
-
const cells = [];
|
|
18638
|
-
for (const [i, n] of nodes.entries()) {
|
|
18639
|
-
const rect = { x: n.x, y: n.y, w: n.w, h: n.h };
|
|
18640
|
-
try {
|
|
18641
|
-
writeFileSync22(path54.join(evidenceDir, `${rep}-inspect-${i}-ref.png`), zoomCrop(ref, rect));
|
|
18642
|
-
writeFileSync22(path54.join(evidenceDir, `${rep}-inspect-${i}-render.png`), zoomCrop(render, rect));
|
|
18643
|
-
} catch {
|
|
18644
|
-
continue;
|
|
18645
|
-
}
|
|
18646
|
-
crops += 1;
|
|
18647
|
-
cells.push(
|
|
18648
|
-
`<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>`
|
|
18649
|
-
);
|
|
18650
|
-
}
|
|
18651
|
-
sections.push(
|
|
18652
|
-
`<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>`
|
|
18653
|
-
);
|
|
18654
|
-
}
|
|
18655
|
-
for (const c of report?.configs ?? []) {
|
|
18656
|
-
if (reps.includes(c.rep)) continue;
|
|
18657
|
-
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>`);
|
|
18658
|
-
}
|
|
18659
|
-
const sheet = path54.join(evidenceDir, "inspect.html");
|
|
18660
|
-
writeFileSync22(
|
|
18661
|
-
sheet,
|
|
18662
|
-
`<!doctype html><meta charset="utf-8"><title>${esc(manifest.name)} \u2014 tendril inspect</title><style>
|
|
18663
|
-
body{font:14px/1.45 system-ui,sans-serif;margin:24px;background:#fff;color:#111}
|
|
18664
|
-
h1{font-size:20px} h2{font-size:16px;border-top:1px solid #ddd;padding-top:16px}
|
|
18665
|
-
.pair,.full{display:flex;gap:12px;flex-wrap:wrap;align-items:flex-start}
|
|
18666
|
-
.full img{max-width:400px;border:1px solid #ccc} .pair img{border:1px solid #ccc;image-rendering:pixelated}
|
|
18667
|
-
figure{margin:0 0 16px} figcaption{margin-bottom:4px} em{display:block;color:#666;font-style:normal;font-size:12px}
|
|
18668
|
-
.grid{display:flex;flex-wrap:wrap;gap:20px;margin-top:12px} .none{color:#666}
|
|
18669
|
-
.verdict{border:1px solid #ddd;border-left-width:5px;padding:12px 16px;margin:16px 0;background:#fafafa}
|
|
18670
|
-
.verdict.ok{border-left-color:#1a7f37} .verdict.bad{border-left-color:#b35900} .verdict.none{border-left-color:#999}
|
|
18671
|
-
.verdict h2{border:0;padding:0;margin:0 0 8px} .verdict h3{font-size:13px;margin:12px 0 4px;color:#444}
|
|
18672
|
-
.verdict small{font-weight:400;color:#666}
|
|
18673
|
-
.tiers{margin:0;display:flex;gap:8px;flex-wrap:wrap}
|
|
18674
|
-
.tier{display:inline-block;padding:1px 8px;border:1px solid #ccc;border-radius:10px;font-size:12px;background:#fff}
|
|
18675
|
-
.t-certified{border-color:#1a7f37;color:#1a7f37} .t-pass{border-color:#8a6d00;color:#8a6d00} .t-fail{border-color:#b3261e;color:#b3261e}
|
|
18676
|
-
.caveats,.fails{margin:4px 0 0;padding-left:20px;color:#444} .caveats li,.fails li{margin:2px 0}
|
|
18677
|
-
.scores{margin:4px 0 10px;color:#333} .exact{color:#666;font-size:12px} .demoted{color:#b3261e;font-size:12px;margin-left:8px}
|
|
18678
|
-
.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}
|
|
18679
|
-
</style><h1>${esc(manifest.name)} \u2014 verdict and evidence</h1>
|
|
18680
|
-
${verdictBlock(report)}
|
|
18681
|
-
<p>Below, every crop is a recorded node small enough that global metrics weight it as a rounding error.
|
|
18682
|
-
Scan the pairs: anything present on the left and missing/invisible on the right is a defect,
|
|
18683
|
-
whatever the scores said. Verdicts come from <code>tendril verify</code> \u2014 this sheet only shows.</p>
|
|
18684
|
-
${sections.join("\n")}
|
|
18685
|
-
<section><h2>Diff colours</h2><div class="legend">${esc(DIFF_LEGEND_TEXT)}</div></section>
|
|
18686
|
-
`
|
|
18687
|
-
);
|
|
18688
|
-
emitData(opts, { sheet, configs: reps.length, crops }, () => {
|
|
18689
|
-
process.stdout.write(`inspect sheet: ${sheet}
|
|
18690
|
-
${reps.length} config(s), ${crops} detail crop pair(s) \u2014 open the sheet and scan recorded vs rendered
|
|
18691
|
-
`);
|
|
18692
|
-
});
|
|
18693
|
-
}
|
|
18694
|
-
var INSPECT_DESCRIPTION, esc;
|
|
18695
|
-
var init_inspect = __esm({
|
|
18696
|
-
"packages/cli/src/commands/inspect.ts"() {
|
|
18697
|
-
"use strict";
|
|
18698
|
-
init_src3();
|
|
18699
|
-
init_src4();
|
|
18700
|
-
init_src5();
|
|
18701
|
-
init_describe();
|
|
18702
|
-
init_invocation();
|
|
18703
|
-
init_output();
|
|
18704
|
-
INSPECT_DESCRIPTION = {
|
|
18705
|
-
name: "inspect",
|
|
18706
|
-
summary: "Build an eye-verifiable detail sheet from verify evidence: magnified ref-vs-render crops of every small recorded node (icons, controls, marks).",
|
|
18707
|
-
args: [{ name: "bundleDir", required: true, description: "bundle directory (must carry component.json and a verify-evidence dir from a prior `tendril verify`)" }],
|
|
18708
|
-
flags: [
|
|
18709
|
-
{ flag: "--set <dir>", description: "recording set override (default: the bundle's provenance path)" },
|
|
18710
|
-
{ flag: "--max-area <px2>", description: "node area ceiling for the detail sweep", default: "1024" },
|
|
18711
|
-
{ flag: "--json", description: "Machine-readable output" }
|
|
18712
|
-
],
|
|
18713
|
-
output: {
|
|
18714
|
-
sheet: "string \u2014 path to the generated inspect.html",
|
|
18715
|
-
configs: "number \u2014 configs with evidence found",
|
|
18716
|
-
crops: "number \u2014 detail crop pairs written"
|
|
18717
|
-
},
|
|
18718
|
-
exitCodes: { 0: "sheet written", 3: "no verify evidence to inspect (run `tendril verify` first)" },
|
|
18719
|
-
examples: ["tendril inspect ./src/components/Banner", "tendril inspect ./bundle --set ./tendril/recordings/banner"]
|
|
18720
|
-
};
|
|
18721
|
-
esc = (s) => s.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """);
|
|
18722
|
-
}
|
|
18723
|
-
});
|
|
18724
|
-
|
|
18725
19026
|
// packages/cli/src/commands/login.ts
|
|
18726
19027
|
var login_exports = {};
|
|
18727
19028
|
__export(login_exports, {
|
|
@@ -19297,12 +19598,28 @@ var init_share = __esm({
|
|
|
19297
19598
|
// packages/cli/src/commands/publish.ts
|
|
19298
19599
|
var publish_exports = {};
|
|
19299
19600
|
__export(publish_exports, {
|
|
19601
|
+
approveWaitPhase: () => approveWaitPhase,
|
|
19300
19602
|
resolveOrigin: () => resolveOrigin2,
|
|
19301
|
-
runPublish: () => runPublish
|
|
19603
|
+
runPublish: () => runPublish,
|
|
19604
|
+
spendPendingApproval: () => spendPendingApproval
|
|
19302
19605
|
});
|
|
19303
19606
|
import { existsSync as existsSync47, readFileSync as readFileSync43, rmSync as rmSync10, writeFileSync as writeFileSync25 } from "node:fs";
|
|
19304
19607
|
import path57 from "node:path";
|
|
19305
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
|
+
}
|
|
19306
19623
|
const bundleDir = path57.resolve(opts.bundleDir);
|
|
19307
19624
|
const bundle = readBundle(opts, bundleDir);
|
|
19308
19625
|
const report = bundle.report;
|
|
@@ -19416,9 +19733,11 @@ async function runPublish(opts) {
|
|
|
19416
19733
|
const origin = resolveOrigin2(opts);
|
|
19417
19734
|
const client = opts.client ?? httpClient(opts, origin);
|
|
19418
19735
|
if (opts.approveWait === true) {
|
|
19419
|
-
await approveWaitPhase(opts, client, bundleDir, { componentName, figmaFile });
|
|
19736
|
+
const phase = await approveWaitPhase(opts, client, bundleDir, { componentName, figmaFile });
|
|
19737
|
+
if (phase === "yielded") return;
|
|
19420
19738
|
}
|
|
19421
19739
|
void reportRunPresence(componentName, "publishing");
|
|
19740
|
+
emitProgress(0, 1, "requesting the upload plan from the portal");
|
|
19422
19741
|
let opened = await client.begin({
|
|
19423
19742
|
componentName,
|
|
19424
19743
|
figmaFile,
|
|
@@ -19445,6 +19764,7 @@ async function runPublish(opts) {
|
|
|
19445
19764
|
}
|
|
19446
19765
|
refuse2(opts, opened, "publish-refused");
|
|
19447
19766
|
}
|
|
19767
|
+
if (opts.approveWait === true) spendPendingApproval();
|
|
19448
19768
|
const uploaded = [];
|
|
19449
19769
|
for (const object of opened.value.plan.objects) {
|
|
19450
19770
|
const file = path57.join(bundleDir, object.relPath);
|
|
@@ -19464,6 +19784,7 @@ async function runPublish(opts) {
|
|
|
19464
19784
|
uploaded.push({ relPath: object.relPath, deduplicated: sent.value.deduplicated });
|
|
19465
19785
|
emitProgress(uploaded.length, opened.value.plan.objects.length, `uploading ${object.relPath}`);
|
|
19466
19786
|
}
|
|
19787
|
+
emitProgress(uploaded.length, uploaded.length, "upload complete \u2014 committing the publication");
|
|
19467
19788
|
const committed = await client.commit({ publicationId: opened.value.publicationId });
|
|
19468
19789
|
if (committed.ok) {
|
|
19469
19790
|
await endRunPresence(componentName);
|
|
@@ -19605,7 +19926,7 @@ async function runApprovalFlow(opts, client, bundleDir, input) {
|
|
|
19605
19926
|
const who = await client.whoami?.();
|
|
19606
19927
|
const asAccount = who?.ok === true && who.value.email !== "" ? ` signed in as ${who.value.email}` : "";
|
|
19607
19928
|
if (opts.approveStart === true) {
|
|
19608
|
-
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() };
|
|
19609
19930
|
writeFileSync25(pendingApprovalPath(), `${JSON.stringify(pending, null, 2)}
|
|
19610
19931
|
`, { mode: 384 });
|
|
19611
19932
|
await endRunPresence(input.componentName);
|
|
@@ -19638,7 +19959,7 @@ async function runApprovalFlow(opts, client, bundleDir, input) {
|
|
|
19638
19959
|
const decided = await waitForApproval(opts, client, approval);
|
|
19639
19960
|
if (decided === "approved") return input.begin();
|
|
19640
19961
|
await endRunPresence(input.componentName);
|
|
19641
|
-
failDecision(opts, decided);
|
|
19962
|
+
failDecision(opts, decided === "window-elapsed" ? "expired" : decided);
|
|
19642
19963
|
}
|
|
19643
19964
|
async function approveWaitPhase(opts, client, bundleDir, subject) {
|
|
19644
19965
|
const file = pendingApprovalPath();
|
|
@@ -19668,12 +19989,41 @@ async function approveWaitPhase(opts, client, bundleDir, subject) {
|
|
|
19668
19989
|
}
|
|
19669
19990
|
const done = () => rmSync10(file, { force: true });
|
|
19670
19991
|
const decided = await waitForApproval(opts, client, pending);
|
|
19671
|
-
|
|
19672
|
-
|
|
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 });
|
|
19673
20022
|
}
|
|
19674
20023
|
async function waitForApproval(opts, client, approval) {
|
|
19675
20024
|
const interval = Math.max(1, approval.pollSeconds) * 1e3;
|
|
19676
|
-
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);
|
|
19677
20027
|
for (let tick = 1; tick <= total; tick += 1) {
|
|
19678
20028
|
const polled = await client.pollApproval({ approvalId: approval.approvalId });
|
|
19679
20029
|
if (!polled.ok) refuse2(opts, polled, "approval-poll-refused");
|
|
@@ -19681,7 +20031,7 @@ async function waitForApproval(opts, client, approval) {
|
|
|
19681
20031
|
emitProgress(tick, total, "waiting for the browser approval");
|
|
19682
20032
|
await new Promise((resolve) => setTimeout(resolve, interval));
|
|
19683
20033
|
}
|
|
19684
|
-
return "expired";
|
|
20034
|
+
return opts.waitWindowSeconds !== void 0 ? "window-elapsed" : "expired";
|
|
19685
20035
|
}
|
|
19686
20036
|
function failDecision(opts, decided) {
|
|
19687
20037
|
if (decided === "denied") {
|
|
@@ -21195,7 +21545,7 @@ function buildProgram() {
|
|
|
21195
21545
|
...local["revoke"] !== void 0 ? { revoke: local["revoke"] } : {}
|
|
21196
21546
|
});
|
|
21197
21547
|
});
|
|
21198
|
-
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) => {
|
|
21199
21549
|
const flags = globalFlags(cmd.parent);
|
|
21200
21550
|
const local = cmd.opts();
|
|
21201
21551
|
const { runPublish: runPublish2 } = await Promise.resolve().then(() => (init_publish(), publish_exports));
|
|
@@ -21205,7 +21555,10 @@ function buildProgram() {
|
|
|
21205
21555
|
...local["to"] !== void 0 ? { to: local["to"] } : {},
|
|
21206
21556
|
...local["name"] !== void 0 ? { name: local["name"] } : {},
|
|
21207
21557
|
...local["approveStart"] !== void 0 ? { approveStart: local["approveStart"] } : {},
|
|
21208
|
-
...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"]) } : {}
|
|
21209
21562
|
});
|
|
21210
21563
|
});
|
|
21211
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) => {
|