@tendrilapp/cli 0.1.50 → 0.1.51

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/tendril.js CHANGED
@@ -2584,8 +2584,8 @@ var init_src = __esm({
2584
2584
  function variableNameToPath(name) {
2585
2585
  return name.split("/").map(canonicalCssIdentPart).filter((part) => part.length > 0);
2586
2586
  }
2587
- function tokenPathToCssVar(path60) {
2588
- return `--${path60.join("-")}`;
2587
+ function tokenPathToCssVar(path62) {
2588
+ return `--${path62.join("-")}`;
2589
2589
  }
2590
2590
  function toDtcgToken(variable, defaultMode) {
2591
2591
  const modes = Object.keys(variable.valuesByMode);
@@ -2629,11 +2629,11 @@ function toDtcgToken(variable, defaultMode) {
2629
2629
  }
2630
2630
  function mapVariablesToDtcg(variables, defaultMode = "light") {
2631
2631
  const entries = variables.map((variable) => {
2632
- const path60 = variableNameToPath(variable.name);
2633
- if (path60.length === 0) {
2632
+ const path62 = variableNameToPath(variable.name);
2633
+ if (path62.length === 0) {
2634
2634
  throw new DtcgMappingError("empty-name", `Figma variable ${variable.id} has an empty name`);
2635
2635
  }
2636
- return { variable, path: path60 };
2636
+ return { variable, path: path62 };
2637
2637
  });
2638
2638
  const groupPrefixes = /* @__PURE__ */ new Set();
2639
2639
  for (const e of entries) {
@@ -2654,21 +2654,21 @@ function mapVariablesToDtcg(variables, defaultMode = "light") {
2654
2654
  }
2655
2655
  const tokens = {};
2656
2656
  const flat = [];
2657
- for (const { variable, path: path60 } of entries) {
2657
+ for (const { variable, path: path62 } of entries) {
2658
2658
  const token = toDtcgToken(variable, defaultMode);
2659
2659
  let group = tokens;
2660
- for (const segment of path60.slice(0, -1)) {
2660
+ for (const segment of path62.slice(0, -1)) {
2661
2661
  const existing = group[segment];
2662
2662
  group = existing ?? (group[segment] = {});
2663
2663
  }
2664
- const leaf = path60[path60.length - 1];
2664
+ const leaf = path62[path62.length - 1];
2665
2665
  if (group[leaf] !== void 0) {
2666
- throw new DtcgMappingError("duplicate-path", `Duplicate token path "${path60.join(".")}" (variable ${variable.id})`);
2666
+ throw new DtcgMappingError("duplicate-path", `Duplicate token path "${path62.join(".")}" (variable ${variable.id})`);
2667
2667
  }
2668
2668
  group[leaf] = token;
2669
2669
  flat.push({
2670
- path: path60.join("."),
2671
- cssVar: tokenPathToCssVar(path60),
2670
+ path: path62.join("."),
2671
+ cssVar: tokenPathToCssVar(path62),
2672
2672
  type: token.$type,
2673
2673
  value: token.$value
2674
2674
  });
@@ -2857,9 +2857,9 @@ function boundId(value) {
2857
2857
  return isObject(value) && typeof value["boundVariableId"] === "string" ? value["boundVariableId"] : void 0;
2858
2858
  }
2859
2859
  function resolveBinding(ctx, id) {
2860
- const path60 = ctx.pathById.get(id);
2861
- if (path60 === void 0) ctx.unresolved.add(id);
2862
- return path60;
2860
+ const path62 = ctx.pathById.get(id);
2861
+ if (path62 === void 0) ctx.unresolved.add(id);
2862
+ return path62;
2863
2863
  }
2864
2864
  function parseVariantProps(name) {
2865
2865
  if (!name.includes("=")) return void 0;
@@ -2894,8 +2894,8 @@ function walk(ctx, raw) {
2894
2894
  if (!isObject(paint) || paint["visible"] === false) continue;
2895
2895
  const id = boundId(paint);
2896
2896
  if (id !== void 0) {
2897
- const path60 = resolveBinding(ctx, id);
2898
- if (path60 !== void 0) tokens.add(path60);
2897
+ const path62 = resolveBinding(ctx, id);
2898
+ if (path62 !== void 0) tokens.add(path62);
2899
2899
  } else if (typeof paint["color"] === "string") {
2900
2900
  ctx.hardcoded.push({ node: name, property, value: paint["color"] });
2901
2901
  }
@@ -2903,8 +2903,8 @@ function walk(ctx, raw) {
2903
2903
  }
2904
2904
  const radiusId = boundId(raw["cornerRadius"]);
2905
2905
  if (radiusId !== void 0) {
2906
- const path60 = resolveBinding(ctx, radiusId);
2907
- if (path60 !== void 0) tokens.add(path60);
2906
+ const path62 = resolveBinding(ctx, radiusId);
2907
+ if (path62 !== void 0) tokens.add(path62);
2908
2908
  } else if (typeof raw["cornerRadius"] === "number" && raw["cornerRadius"] !== 0) {
2909
2909
  ctx.hardcoded.push({ node: name, property: "border-radius", value: `${raw["cornerRadius"]}px` });
2910
2910
  }
@@ -2914,10 +2914,10 @@ function walk(ctx, raw) {
2914
2914
  layout = { mode: layoutMode === "HORIZONTAL" ? "flex-row" : "flex-column" };
2915
2915
  const gapId = boundId(raw["itemSpacing"]);
2916
2916
  if (gapId !== void 0) {
2917
- const path60 = resolveBinding(ctx, gapId);
2918
- if (path60 !== void 0) {
2919
- layout.gap = path60;
2920
- tokens.add(path60);
2917
+ const path62 = resolveBinding(ctx, gapId);
2918
+ if (path62 !== void 0) {
2919
+ layout.gap = path62;
2920
+ tokens.add(path62);
2921
2921
  }
2922
2922
  } else if (typeof raw["itemSpacing"] === "number" && raw["itemSpacing"] !== 0) {
2923
2923
  ctx.hardcoded.push({ node: name, property: "gap", value: `${raw["itemSpacing"]}px` });
@@ -2926,10 +2926,10 @@ function walk(ctx, raw) {
2926
2926
  for (const field of PADDING_FIELDS) {
2927
2927
  const id = boundId(raw[field]);
2928
2928
  if (id !== void 0) {
2929
- const path60 = resolveBinding(ctx, id);
2930
- if (path60 !== void 0) {
2931
- paddingPaths.push(path60);
2932
- tokens.add(path60);
2929
+ const path62 = resolveBinding(ctx, id);
2930
+ if (path62 !== void 0) {
2931
+ paddingPaths.push(path62);
2932
+ tokens.add(path62);
2933
2933
  }
2934
2934
  } else if (typeof raw[field] === "number" && raw[field] !== 0) {
2935
2935
  ctx.hardcoded.push({ node: name, property: field, value: `${raw[field]}px` });
@@ -5179,10 +5179,10 @@ async function resolveFonts(family, weights, cacheDir = DEFAULT_FONT_CACHE) {
5179
5179
  continue;
5180
5180
  }
5181
5181
  const bytes = new Uint8Array(await fileRes.arrayBuffer());
5182
- const sha256 = createHash4("sha256").update(bytes).digest("hex");
5182
+ const sha2562 = createHash4("sha256").update(bytes).digest("hex");
5183
5183
  const file = path14.join(cacheDir, `${family.toLowerCase().replace(/\s+/g, "-")}-${weight}.woff2`);
5184
5184
  writeFileSync3(file, bytes);
5185
- resolved.push({ family, weight, source: url, sha256, file, license: meta.license, ...meta.family !== null ? { servedFamily: meta.family } : {} });
5185
+ resolved.push({ family, weight, source: url, sha256: sha2562, file, license: meta.license, ...meta.family !== null ? { servedFamily: meta.family } : {} });
5186
5186
  } catch (err) {
5187
5187
  failures.push({ family, weight, reason: `download failed: ${err instanceof Error ? err.message : String(err)}` });
5188
5188
  }
@@ -5220,10 +5220,10 @@ ${shown}`
5220
5220
  storedExt = ".ttf";
5221
5221
  }
5222
5222
  mkdirSync2(cacheDir, { recursive: true });
5223
- const sha256 = createHash4("sha256").update(bytes).digest("hex");
5223
+ const sha2562 = createHash4("sha256").update(bytes).digest("hex");
5224
5224
  const file = path14.join(cacheDir, `${family.toLowerCase().replace(/\s+/g, "-")}-${weight}${storedExt}`);
5225
5225
  writeFileSync3(file, bytes);
5226
- const face = { family, weight, source: `${provenance}:${path14.basename(src)}`, sha256, file, license: "unknown" };
5226
+ const face = { family, weight, source: `${provenance}:${path14.basename(src)}`, sha256: sha2562, file, license: "unknown" };
5227
5227
  const mPath = path14.join(cacheDir, "manifest.json");
5228
5228
  const prior = existsSync10(mPath) ? JSON.parse(readFileSync7(mPath, "utf8")) : [];
5229
5229
  const merged = [...prior.filter((p) => !(p.family === family && p.weight === weight)), { ...face, file: path14.basename(file) }];
@@ -6719,7 +6719,7 @@ function cssProvenanceComment(input) {
6719
6719
  const unrecorded = input.latticeConfigs === null ? "; coverage denominator unknown" : `; ${Math.max(0, input.latticeConfigs - input.scored)} lattice configs unverified`;
6720
6720
  return `/* tendril bundle v${BUNDLE_VERSION} \u2014 ${input.pass}/${input.scored} recorded configs \u2265 pass bar, ${input.certified} certified${unrecorded}. Non-authoritative claim AT STAMP TIME \u2014 any edit invalidates it; recompute with \`tendril verify\`. */`;
6721
6721
  }
6722
- function hashRecordingSet(relPaths, readFile, sha256) {
6722
+ function hashRecordingSet(relPaths, readFile, sha2562) {
6723
6723
  const sorted = [...relPaths].sort();
6724
6724
  const chunks = [];
6725
6725
  const encoder = new TextEncoder();
@@ -6728,7 +6728,7 @@ function hashRecordingSet(relPaths, readFile, sha256) {
6728
6728
  `));
6729
6729
  chunks.push(readFile(p));
6730
6730
  }
6731
- return sha256(chunks);
6731
+ return sha2562(chunks);
6732
6732
  }
6733
6733
  var BUNDLE_VERSION, ConfigStatusSchema, PinnedPropSchema, PropAdapterSchema, RequiredFontSchema, ConfigClaimSchema, BehaviorClaimSchema, BundleProvenanceSchema, BundleComponentJsonSchema, MAX_BUNDLE_MANIFEST_BYTES, MAX_BUNDLE_SOURCE_BYTES;
6734
6734
  var init_bundle = __esm({
@@ -6857,43 +6857,43 @@ function classifyBundleSurface(files, opts) {
6857
6857
  const excluded = [];
6858
6858
  const unknown = [];
6859
6859
  for (const raw of files) {
6860
- const path60 = raw.replace(/\\/g, "/").replace(/^\.\//, "");
6861
- const inEvidence = path60.startsWith(`${EVIDENCE_DIR}/`);
6862
- if (path60.startsWith("fonts/")) {
6863
- const fname = path60.slice("fonts/".length);
6860
+ const path62 = raw.replace(/\\/g, "/").replace(/^\.\//, "");
6861
+ const inEvidence = path62.startsWith(`${EVIDENCE_DIR}/`);
6862
+ if (path62.startsWith("fonts/")) {
6863
+ const fname = path62.slice("fonts/".length);
6864
6864
  if (!fname.includes("/") && (/\.(woff2?|ttf|otf)$/i.test(fname) || /^(NOTICE|LICENSE|LICENCE)[^/]*\.txt$/i.test(fname))) {
6865
- excluded.push({ path: path60, reason: "font payload \u2014 not published (fonts policy pending); faces are sha-pinned in component.json requiredFonts" });
6865
+ excluded.push({ path: path62, reason: "font payload \u2014 not published (fonts policy pending); faces are sha-pinned in component.json requiredFonts" });
6866
6866
  continue;
6867
6867
  }
6868
- unknown.push(path60);
6868
+ unknown.push(path62);
6869
6869
  continue;
6870
6870
  }
6871
- const name = inEvidence ? path60.slice(EVIDENCE_DIR.length + 1) : path60;
6871
+ const name = inEvidence ? path62.slice(EVIDENCE_DIR.length + 1) : path62;
6872
6872
  if (name.includes("/")) {
6873
- unknown.push(path60);
6873
+ unknown.push(path62);
6874
6874
  continue;
6875
6875
  }
6876
6876
  if (inEvidence) {
6877
- if (name === "verify-report.json") published.push({ path: path60, role: "verify-report" });
6878
- else if (name === "diff-legend.txt") published.push({ path: path60, role: "diff-legend" });
6879
- else if (name === "inspect.html") published.push({ path: path60, role: "inspect-sheet" });
6880
- else if (/-mount-failure\.png$/.test(name)) excluded.push({ path: path60, reason: "harness failure diagnostic (regenerated every verify run, never published)" });
6877
+ if (name === "verify-report.json") published.push({ path: path62, role: "verify-report" });
6878
+ else if (name === "diff-legend.txt") published.push({ path: path62, role: "diff-legend" });
6879
+ else if (name === "inspect.html") published.push({ path: path62, role: "inspect-sheet" });
6880
+ else if (/-mount-failure\.png$/.test(name)) excluded.push({ path: path62, reason: "harness failure diagnostic (regenerated every verify run, never published)" });
6881
6881
  else {
6882
6882
  const hit = EVIDENCE_PATTERNS.find((p) => p.re.test(name));
6883
- if (hit !== void 0) published.push({ path: path60, role: hit.role });
6884
- else unknown.push(path60);
6883
+ if (hit !== void 0) published.push({ path: path62, role: hit.role });
6884
+ else unknown.push(path62);
6885
6885
  }
6886
6886
  continue;
6887
6887
  }
6888
- if (name === opts.entry) published.push({ path: path60, role: "entry" });
6889
- else if (name === "styles.css") published.push({ path: path60, role: "styles" });
6890
- else if (name === "tokens.css") published.push({ path: path60, role: "tokens" });
6891
- else if (name === "fonts.css") published.push({ path: path60, role: "fonts" });
6892
- else if (name === "component.json") published.push({ path: path60, role: "manifest" });
6888
+ if (name === opts.entry) published.push({ path: path62, role: "entry" });
6889
+ else if (name === "styles.css") published.push({ path: path62, role: "styles" });
6890
+ else if (name === "tokens.css") published.push({ path: path62, role: "tokens" });
6891
+ else if (name === "fonts.css") published.push({ path: path62, role: "fonts" });
6892
+ else if (name === "component.json") published.push({ path: path62, role: "manifest" });
6893
6893
  else {
6894
6894
  const skip = EXCLUDED.find((e) => e.test(name));
6895
- if (skip !== void 0) excluded.push({ path: path60, reason: skip.reason });
6896
- else unknown.push(path60);
6895
+ if (skip !== void 0) excluded.push({ path: path62, reason: skip.reason });
6896
+ else unknown.push(path62);
6897
6897
  }
6898
6898
  }
6899
6899
  const roles = new Set(published.map((p) => p.role));
@@ -6903,8 +6903,8 @@ function missingInspectCrops(sheetText, publishedPaths) {
6903
6903
  const held = new Set(publishedPaths);
6904
6904
  const missing = /* @__PURE__ */ new Set();
6905
6905
  for (const [, name] of sheetText.matchAll(/src="\.\/([^"]+)"/g)) {
6906
- const path60 = `${EVIDENCE_DIR}/${name}`;
6907
- if (!held.has(path60)) missing.add(path60);
6906
+ const path62 = `${EVIDENCE_DIR}/${name}`;
6907
+ if (!held.has(path62)) missing.add(path62);
6908
6908
  }
6909
6909
  return [...missing].sort();
6910
6910
  }
@@ -7012,10 +7012,10 @@ function readScoredFiles(report) {
7012
7012
  const entries = Object.entries(value);
7013
7013
  if (entries.length === 0) return void 0;
7014
7014
  const out = {};
7015
- for (const [path60, digest] of entries) {
7016
- if (path60 === "" || path60.startsWith("/") || path60.includes("..")) return void 0;
7015
+ for (const [path62, digest] of entries) {
7016
+ if (path62 === "" || path62.startsWith("/") || path62.includes("..")) return void 0;
7017
7017
  if (!isSetHash(digest)) return void 0;
7018
- out[path60] = digest;
7018
+ out[path62] = digest;
7019
7019
  }
7020
7020
  return out;
7021
7021
  }
@@ -7023,11 +7023,11 @@ function compareScoredFiles(recorded, actual) {
7023
7023
  const missing = [];
7024
7024
  const unscored = [];
7025
7025
  const changed = [];
7026
- for (const [path60, digest] of Object.entries(recorded)) {
7027
- if (!(path60 in actual)) missing.push(path60);
7028
- else if (actual[path60] !== digest) changed.push(path60);
7026
+ for (const [path62, digest] of Object.entries(recorded)) {
7027
+ if (!(path62 in actual)) missing.push(path62);
7028
+ else if (actual[path62] !== digest) changed.push(path62);
7029
7029
  }
7030
- for (const path60 of Object.keys(actual)) if (!(path60 in recorded)) unscored.push(path60);
7030
+ for (const path62 of Object.keys(actual)) if (!(path62 in recorded)) unscored.push(path62);
7031
7031
  return { missing: missing.sort(), unscored: unscored.sort(), changed: changed.sort() };
7032
7032
  }
7033
7033
  function scoredRecordingSetHash(report) {
@@ -7068,8 +7068,318 @@ var init_motion_css = __esm({
7068
7068
  }
7069
7069
  });
7070
7070
 
7071
- // packages/metadata/src/profile.ts
7071
+ // packages/metadata/src/recording-archive.ts
7072
7072
  import { z as z11 } from "zod";
7073
+ function bytesToBase64(bytes) {
7074
+ let out = "";
7075
+ for (let i = 0; i < bytes.length; i += 3) {
7076
+ const a = bytes[i];
7077
+ const b = i + 1 < bytes.length ? bytes[i + 1] : 0;
7078
+ const c = i + 2 < bytes.length ? bytes[i + 2] : 0;
7079
+ out += B64_ALPHABET[a >> 2] + B64_ALPHABET[(a & 3) << 4 | b >> 4];
7080
+ out += i + 1 < bytes.length ? B64_ALPHABET[(b & 15) << 2 | c >> 6] : "=";
7081
+ out += i + 2 < bytes.length ? B64_ALPHABET[c & 63] : "=";
7082
+ }
7083
+ return out;
7084
+ }
7085
+ function base64ToBytes(text) {
7086
+ if (text.length % 4 !== 0) return void 0;
7087
+ const padIdx = text.indexOf("=");
7088
+ const pad = padIdx === -1 ? 0 : text.length - padIdx;
7089
+ if (pad > 2 || padIdx !== -1 && !/^={1,2}$/.test(text.slice(padIdx))) return void 0;
7090
+ const body = padIdx === -1 ? text : text.slice(0, padIdx);
7091
+ const out = new Uint8Array(text.length / 4 * 3 - pad);
7092
+ let o = 0;
7093
+ let buffer = 0;
7094
+ let bits = 0;
7095
+ for (const ch of body) {
7096
+ const v = B64_ALPHABET.indexOf(ch);
7097
+ if (v === -1) return void 0;
7098
+ buffer = buffer << 6 | v;
7099
+ bits += 6;
7100
+ if (bits >= 8) {
7101
+ bits -= 8;
7102
+ out[o++] = buffer >> bits & 255;
7103
+ }
7104
+ }
7105
+ if (bits > 0 && (buffer & (1 << bits) - 1) !== 0) return void 0;
7106
+ return out;
7107
+ }
7108
+ function readSetShape(manifestBytes) {
7109
+ let parsed;
7110
+ try {
7111
+ parsed = JSON.parse(new TextDecoder("utf-8", { fatal: true }).decode(manifestBytes));
7112
+ } catch {
7113
+ return { ok: false, refusal: "the carried recording-set.json is not readable JSON" };
7114
+ }
7115
+ const m = parsed;
7116
+ if (typeof m.component !== "string" || m.component === "") return { ok: false, refusal: "the carried recording-set.json names no component" };
7117
+ if (!Array.isArray(m.reps) || m.reps.length === 0) return { ok: false, refusal: "the carried recording-set.json declares no reps" };
7118
+ const reps = [];
7119
+ for (const rep of m.reps) {
7120
+ const slug = rep.slug;
7121
+ if (typeof slug !== "string" || !safeSegment(slug)) {
7122
+ return { ok: false, refusal: `the carried recording-set.json declares a rep slug that is not a safe directory name (${JSON.stringify(slug).slice(0, 80)})` };
7123
+ }
7124
+ if (reps.includes(slug)) return { ok: false, refusal: `the carried recording-set.json declares the rep ${slug} twice` };
7125
+ reps.push(slug);
7126
+ }
7127
+ return {
7128
+ ok: true,
7129
+ shape: {
7130
+ component: m.component,
7131
+ ...typeof m.figmaFile === "string" ? { figmaFile: m.figmaFile } : {},
7132
+ channeled: m.channel !== void 0,
7133
+ reps
7134
+ }
7135
+ };
7136
+ }
7137
+ function recordingSetEnumeration(input, fs4) {
7138
+ const relPaths = [];
7139
+ const setLevel = ["recording-set.json", "completeness-manifest.json", "get_variable_defs.json", "get_metadata.json", ...input.channeled ? ["get_motion_context.json"] : []];
7140
+ for (const name of setLevel) {
7141
+ if (fs4.exists(name)) relPaths.push(name);
7142
+ }
7143
+ const perRep = [
7144
+ "get_design_context.json",
7145
+ "get_metadata.json",
7146
+ "get_screenshot.json",
7147
+ "get_variable_defs.json",
7148
+ ...input.channeled ? ["get_metadata_interior.json", "rest_nodes.json", "rest_metadata.json", "rest_screenshot.json"] : []
7149
+ ];
7150
+ for (const rep of input.reps) {
7151
+ for (const f of perRep) {
7152
+ if (fs4.exists(`${rep}/${f}`)) relPaths.push(`${rep}/${f}`);
7153
+ }
7154
+ for (const asset of fs4.listRep(rep).filter((f) => f.startsWith("asset-"))) {
7155
+ relPaths.push(`${rep}/${asset}`);
7156
+ }
7157
+ }
7158
+ return relPaths;
7159
+ }
7160
+ function archiveMemberAllowed(shape, memberPath) {
7161
+ const setLevel = /* @__PURE__ */ new Set([
7162
+ "recording-set.json",
7163
+ "completeness-manifest.json",
7164
+ "get_variable_defs.json",
7165
+ "get_metadata.json",
7166
+ "get_motion_context.json",
7167
+ ...SET_LEVEL_EXTRAS
7168
+ ]);
7169
+ if (setLevel.has(memberPath)) return true;
7170
+ const slash = memberPath.indexOf("/");
7171
+ if (slash === -1) return false;
7172
+ const rep = memberPath.slice(0, slash);
7173
+ const name = memberPath.slice(slash + 1);
7174
+ if (!shape.reps.includes(rep)) return false;
7175
+ const perRep = /* @__PURE__ */ new Set([
7176
+ "get_design_context.json",
7177
+ "get_metadata.json",
7178
+ "get_screenshot.json",
7179
+ "get_variable_defs.json",
7180
+ "get_metadata_interior.json",
7181
+ "rest_nodes.json",
7182
+ "rest_metadata.json",
7183
+ "rest_screenshot.json",
7184
+ ...PER_REP_EXTRAS
7185
+ ]);
7186
+ if (perRep.has(name)) return true;
7187
+ return name.startsWith("asset-") && safeSegment(name);
7188
+ }
7189
+ function carriedExtrasHash(members, enumeration, sha2562) {
7190
+ const hashed = new Set(enumeration);
7191
+ const extras = [...members.keys()].filter((p) => !hashed.has(p)).sort();
7192
+ return hashRecordingSet(extras, (p) => members.get(p), sha2562);
7193
+ }
7194
+ function validateRecordingArchive(envelopeText2, opts) {
7195
+ let parsed;
7196
+ try {
7197
+ parsed = JSON.parse(envelopeText2);
7198
+ } catch {
7199
+ return { ok: false, refusal: "the recording archive is not readable JSON" };
7200
+ }
7201
+ const env = RecordingArchiveEnvelopeSchema.safeParse(parsed);
7202
+ if (!env.success) {
7203
+ return { ok: false, refusal: `the recording archive is not a v${String(RECORDING_ARCHIVE_VERSION)} envelope (${env.error.issues[0]?.message ?? "invalid"})` };
7204
+ }
7205
+ const members = /* @__PURE__ */ new Map();
7206
+ const folded = /* @__PURE__ */ new Set();
7207
+ let previous = "";
7208
+ for (const f of env.data.files) {
7209
+ if (f.path <= previous) {
7210
+ return { ok: false, refusal: `the archive's members are not in canonical order (${JSON.stringify(f.path)} after ${JSON.stringify(previous)})` };
7211
+ }
7212
+ previous = f.path;
7213
+ const lower = f.path.toLowerCase();
7214
+ if (folded.has(lower)) return { ok: false, refusal: `the archive carries two members that collide on a case-insensitive filesystem (${f.path})` };
7215
+ folded.add(lower);
7216
+ const bytes = base64ToBytes(f.bytes);
7217
+ if (bytes === void 0) return { ok: false, refusal: `the archive member ${f.path} is not canonical base64` };
7218
+ if (bytes.length > MAX_MEMBER_BYTES) return { ok: false, refusal: `the archive member ${f.path} exceeds ${String(MAX_MEMBER_BYTES)} bytes` };
7219
+ members.set(f.path, bytes);
7220
+ }
7221
+ const manifestBytes = members.get("recording-set.json");
7222
+ if (manifestBytes === void 0) return { ok: false, refusal: "the archive carries no recording-set.json \u2014 there is no set to reconstruct" };
7223
+ const read = readSetShape(manifestBytes);
7224
+ if (!read.ok) return read;
7225
+ const shape = read.shape;
7226
+ for (const memberPath of members.keys()) {
7227
+ if (!archiveMemberAllowed(shape, memberPath)) {
7228
+ return { ok: false, refusal: `the archive carries a file outside the recording-set vocabulary (${memberPath}) \u2014 refused, never silently unpacked` };
7229
+ }
7230
+ }
7231
+ const enumeration = recordingSetEnumeration(
7232
+ { channeled: shape.channeled, reps: shape.reps },
7233
+ {
7234
+ exists: (p) => members.has(p),
7235
+ listRep: (rep) => [...members.keys()].filter((p) => p.startsWith(`${rep}/`)).map((p) => p.slice(rep.length + 1))
7236
+ }
7237
+ );
7238
+ const enumerated = new Set(enumeration);
7239
+ for (const memberPath of members.keys()) {
7240
+ if (enumerated.has(memberPath)) continue;
7241
+ const isExtra = SET_LEVEL_EXTRAS.includes(memberPath) || PER_REP_EXTRAS.some((name) => shape.reps.some((rep) => memberPath === `${rep}/${name}`));
7242
+ if (!isExtra) return { ok: false, refusal: `the archive member ${memberPath} is neither hashed nor a named extra for this set's channel` };
7243
+ }
7244
+ const utf8 = new TextDecoder("utf-8", { fatal: true });
7245
+ for (const [memberPath, bytes] of members) {
7246
+ if (!memberPath.endsWith(".json")) continue;
7247
+ try {
7248
+ JSON.parse(utf8.decode(bytes));
7249
+ } catch {
7250
+ return { ok: false, refusal: `the archive member ${memberPath} is not readable JSON \u2014 these are not verbatim recorded envelopes` };
7251
+ }
7252
+ }
7253
+ for (const rep of shape.reps) {
7254
+ if (!members.has(`${rep}/get_metadata.json`)) {
7255
+ return { ok: false, refusal: `the archive carries no get_metadata.json for the rep ${rep} \u2014 a set missing recorded metadata cannot be reconstructed` };
7256
+ }
7257
+ }
7258
+ const derived = hashRecordingSet(enumeration, (p) => members.get(p), opts.sha256);
7259
+ if (derived !== env.data.recordingSetHash) {
7260
+ return { ok: false, refusal: `the archive does not re-derive its own declared recording-set hash (${derived.slice(0, 12)}\u2026 vs ${env.data.recordingSetHash.slice(0, 12)}\u2026)` };
7261
+ }
7262
+ if (opts.expectedSetHash !== void 0 && derived !== opts.expectedSetHash) {
7263
+ return { ok: false, refusal: `the archive re-derives ${derived.slice(0, 12)}\u2026 but the publication's scored recording set is ${opts.expectedSetHash.slice(0, 12)}\u2026 \u2014 these are not the recordings the verdict was measured against` };
7264
+ }
7265
+ const extras = carriedExtrasHash(members, enumeration, opts.sha256);
7266
+ if (extras !== env.data.carriedExtrasHash) {
7267
+ return {
7268
+ ok: false,
7269
+ refusal: `the archive does not re-derive the digest of the recordings it carries outside the scored set (${extras.slice(0, 12)}\u2026 vs ${env.data.carriedExtrasHash.slice(0, 12)}\u2026) \u2014 the motion, interior geometry or bindings travelling with this set are not the ones it was packed with`
7270
+ };
7271
+ }
7272
+ if (env.data.component !== shape.component) {
7273
+ return { ok: false, refusal: `the archive claims component ${JSON.stringify(env.data.component)} but its recording-set.json says ${JSON.stringify(shape.component)}` };
7274
+ }
7275
+ if ((env.data.figmaFile ?? void 0) !== (shape.figmaFile ?? void 0)) {
7276
+ return { ok: false, refusal: "the archive's declared figmaFile disagrees with its recording-set.json" };
7277
+ }
7278
+ for (const [memberPath, bytes] of members) {
7279
+ if (!memberPath.endsWith("/rest_instances.json")) continue;
7280
+ const rep = memberPath.slice(0, -"/rest_instances.json".length);
7281
+ const check = restInstancesSelfCheck(rep, bytes, members, opts.sha256);
7282
+ if (check !== void 0) return { ok: false, refusal: check };
7283
+ }
7284
+ return { ok: true, shape, setHash: derived, members };
7285
+ }
7286
+ function restInstancesSelfCheck(rep, bytes, members, sha2562) {
7287
+ let parsed;
7288
+ try {
7289
+ parsed = JSON.parse(new TextDecoder("utf-8", { fatal: true }).decode(bytes));
7290
+ } catch {
7291
+ return `${rep}/rest_instances.json is not readable JSON`;
7292
+ }
7293
+ const va = parsed.verifiedAgainst;
7294
+ if (va === void 0 || typeof va.metadataSha256 !== "string") return `${rep}/rest_instances.json carries no verifiedAgainst record`;
7295
+ const pairs = [
7296
+ [va.metadataSha256, `${rep}/get_metadata.json`],
7297
+ ...va.screenshotSha256 !== void 0 ? [[va.screenshotSha256, `${rep}/get_screenshot.json`]] : [],
7298
+ ...va.contextSha256 !== void 0 ? [[va.contextSha256, `${rep}/get_design_context.json`]] : []
7299
+ ];
7300
+ for (const [declared, memberPath] of pairs) {
7301
+ const target = members.get(memberPath);
7302
+ if (target === void 0) return `${rep}/rest_instances.json is verified against ${memberPath}, which the archive does not carry`;
7303
+ if (typeof declared !== "string" || !SHA64.test(declared) || sha2562([target]) !== declared) {
7304
+ return `${rep}/rest_instances.json does not verify against the carried ${memberPath} \u2014 carried enrichment must not outrank carried truth`;
7305
+ }
7306
+ }
7307
+ return void 0;
7308
+ }
7309
+ function packRecordingArchive(fs4, opts) {
7310
+ if (!fs4.exists("recording-set.json")) return { ok: false, refusal: "this directory holds no recording-set.json" };
7311
+ const read = readSetShape(fs4.read("recording-set.json"));
7312
+ if (!read.ok) return read;
7313
+ const shape = read.shape;
7314
+ const listRepSafe = (rep) => fs4.listRep(rep).filter((name) => safeSegment(name));
7315
+ const enumeration = recordingSetEnumeration({ channeled: shape.channeled, reps: shape.reps }, { exists: fs4.exists, listRep: listRepSafe });
7316
+ const paths = new Set(enumeration);
7317
+ for (const extra of SET_LEVEL_EXTRAS) {
7318
+ if (fs4.exists(extra)) paths.add(extra);
7319
+ }
7320
+ for (const rep of shape.reps) {
7321
+ for (const extra of PER_REP_EXTRAS) {
7322
+ if (fs4.exists(`${rep}/${extra}`)) paths.add(`${rep}/${extra}`);
7323
+ }
7324
+ }
7325
+ const members = /* @__PURE__ */ new Map();
7326
+ for (const p of paths) members.set(p, fs4.read(p));
7327
+ const staleEnrichment = [];
7328
+ for (const memberPath of [...members.keys()]) {
7329
+ if (!memberPath.endsWith("/rest_instances.json")) continue;
7330
+ const rep = memberPath.slice(0, -"/rest_instances.json".length);
7331
+ if (restInstancesSelfCheck(rep, members.get(memberPath), members, opts.sha256) !== void 0) {
7332
+ members.delete(memberPath);
7333
+ staleEnrichment.push(memberPath);
7334
+ }
7335
+ }
7336
+ const setHash = hashRecordingSet(enumeration, (p) => members.get(p), opts.sha256);
7337
+ const envelope = {
7338
+ version: RECORDING_ARCHIVE_VERSION,
7339
+ component: shape.component,
7340
+ ...shape.figmaFile !== void 0 ? { figmaFile: shape.figmaFile } : {},
7341
+ recordingSetHash: setHash,
7342
+ // Computed AFTER stale enrichment is dropped, so the digest covers
7343
+ // what actually travels rather than what was on disk.
7344
+ carriedExtrasHash: carriedExtrasHash(members, enumeration, opts.sha256),
7345
+ files: [...members.keys()].sort().map((p) => ({ path: p, bytes: bytesToBase64(members.get(p)) }))
7346
+ };
7347
+ const envelopeText2 = JSON.stringify(envelope);
7348
+ const check = validateRecordingArchive(envelopeText2, { sha256: opts.sha256, expectedSetHash: setHash });
7349
+ if (!check.ok) return { ok: false, refusal: `the packed archive failed its own gate: ${check.refusal}` };
7350
+ return { ok: true, envelopeText: envelopeText2, setHash, staleEnrichment };
7351
+ }
7352
+ var RECORDING_ARCHIVE_VERSION, MAX_ARCHIVE_MEMBERS, MAX_MEMBER_BYTES, SHA64, B64_ALPHABET, safeSegment, SET_LEVEL_EXTRAS, PER_REP_EXTRAS, RecordingArchiveEnvelopeSchema;
7353
+ var init_recording_archive = __esm({
7354
+ "packages/metadata/src/recording-archive.ts"() {
7355
+ "use strict";
7356
+ init_bundle();
7357
+ RECORDING_ARCHIVE_VERSION = 1;
7358
+ MAX_ARCHIVE_MEMBERS = 4096;
7359
+ MAX_MEMBER_BYTES = 5e6;
7360
+ SHA64 = /^[0-9a-f]{64}$/;
7361
+ B64_ALPHABET = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
7362
+ safeSegment = (s) => s !== "" && s !== "." && s !== ".." && s.length <= 200 && !/[/\\\0\s]/.test(s) && !s.startsWith(".");
7363
+ SET_LEVEL_EXTRAS = ["get_motion_context.json"];
7364
+ PER_REP_EXTRAS = ["get_metadata_interior.json", "rest_instances.json"];
7365
+ RecordingArchiveEnvelopeSchema = z11.object({
7366
+ version: z11.literal(RECORDING_ARCHIVE_VERSION),
7367
+ component: z11.string().min(1).max(256),
7368
+ figmaFile: z11.string().min(1).max(128).optional(),
7369
+ recordingSetHash: z11.string().regex(SHA64),
7370
+ /** The carried-but-unhashed members, digested (see
7371
+ * `carriedExtrasHash`). Required: no v1 envelope has ever existed
7372
+ * without it — the carrier has never shipped and migration 0011 is
7373
+ * applied nowhere — so this is additive at zero cost rather than a
7374
+ * version bump nobody would be able to interpret later. */
7375
+ carriedExtrasHash: z11.string().regex(SHA64),
7376
+ files: z11.array(z11.object({ path: z11.string().min(1).max(1024), bytes: z11.string() }).strict()).min(1).max(MAX_ARCHIVE_MEMBERS)
7377
+ }).strict();
7378
+ }
7379
+ });
7380
+
7381
+ // packages/metadata/src/profile.ts
7382
+ import { z as z12 } from "zod";
7073
7383
  function readCodebaseProfile(raw) {
7074
7384
  return CodebaseProfileSchema.parse(JSON.parse(raw));
7075
7385
  }
@@ -7104,11 +7414,11 @@ var init_profile = __esm({
7104
7414
  maxCustomProperties: 5e3,
7105
7415
  maxComponents: 5e3
7106
7416
  };
7107
- CssCustomPropertyName = z11.string().min(3).max(200).regex(/^--[^\s{}();:'"]+$/u);
7108
- RelPath = z11.string().min(1).max(400).refine((p) => !p.startsWith("/") && !p.includes("\\") && !p.includes(".."), {
7417
+ CssCustomPropertyName = z12.string().min(3).max(200).regex(/^--[^\s{}();:'"]+$/u);
7418
+ RelPath = z12.string().min(1).max(400).refine((p) => !p.startsWith("/") && !p.includes("\\") && !p.includes(".."), {
7109
7419
  message: "path must be repo-relative POSIX"
7110
7420
  });
7111
- StylingDialectSchema = z11.enum([
7421
+ StylingDialectSchema = z12.enum([
7112
7422
  "tailwind-v3",
7113
7423
  "tailwind-v4",
7114
7424
  "css-modules",
@@ -7123,26 +7433,26 @@ var init_profile = __esm({
7123
7433
  "styled-jsx",
7124
7434
  "shadcn-style"
7125
7435
  ]);
7126
- DialectEvidenceSchema = z11.object({
7436
+ DialectEvidenceSchema = z12.object({
7127
7437
  dialect: StylingDialectSchema,
7128
7438
  /** A file proves a dialect is USED. A dependency only proves it is
7129
7439
  * INSTALLED — kept separate because a stale devDependency is not a
7130
7440
  * convention, and only file evidence may set `primary`. */
7131
- source: z11.enum(["file", "dependency"]),
7132
- detail: z11.string().max(200)
7441
+ source: z12.enum(["file", "dependency"]),
7442
+ detail: z12.string().max(200)
7133
7443
  });
7134
- CustomPropertyDeclarationSchema = z11.object({
7444
+ CustomPropertyDeclarationSchema = z12.object({
7135
7445
  file: RelPath,
7136
- line: z11.number().int().nonnegative(),
7137
- selector: z11.string().max(300),
7138
- scope: z11.enum(["root", "theme-block", "class", "id", "attribute", "media", "supports", "other"]),
7446
+ line: z12.number().int().nonnegative(),
7447
+ selector: z12.string().max(300),
7448
+ scope: z12.enum(["root", "theme-block", "class", "id", "attribute", "media", "supports", "other"]),
7139
7449
  /** Present only for conditional at-rules, e.g. "(prefers-color-scheme: dark)". */
7140
- condition: z11.string().max(300).optional(),
7141
- value: z11.string().max(500),
7450
+ condition: z12.string().max(300).optional(),
7451
+ value: z12.string().max(500),
7142
7452
  /** A terminal literal, when the var() chain could be followed without
7143
7453
  * leaving this declaration's scope group. */
7144
- resolved: z11.string().max(500).nullable(),
7145
- resolvedVia: z11.enum([
7454
+ resolved: z12.string().max(500).nullable(),
7455
+ resolvedVia: z12.enum([
7146
7456
  "literal",
7147
7457
  "alias-chain",
7148
7458
  /** Target is declared in the scan, but only under a different scope —
@@ -7157,11 +7467,11 @@ var init_profile = __esm({
7157
7467
  "unresolved-function"
7158
7468
  ])
7159
7469
  });
7160
- CustomPropertySchema = z11.object({
7470
+ CustomPropertySchema = z12.object({
7161
7471
  name: CssCustomPropertyName,
7162
- declarations: z11.array(CustomPropertyDeclarationSchema).min(1).max(64)
7472
+ declarations: z12.array(CustomPropertyDeclarationSchema).min(1).max(64)
7163
7473
  });
7164
- ComponentOutcomeSchema = z11.enum([
7474
+ ComponentOutcomeSchema = z12.enum([
7165
7475
  "parsed",
7166
7476
  "parsed-no-props",
7167
7477
  "no-component-found",
@@ -7169,7 +7479,7 @@ var init_profile = __esm({
7169
7479
  "skipped-too-large",
7170
7480
  "skipped-cap"
7171
7481
  ]);
7172
- ComponentBlindSpotSchema = z11.enum([
7482
+ ComponentBlindSpotSchema = z12.enum([
7173
7483
  "multiple-components-in-file",
7174
7484
  "re-export-only",
7175
7485
  "cva-variants",
@@ -7177,40 +7487,40 @@ var init_profile = __esm({
7177
7487
  "hoc-wrapper",
7178
7488
  "props-from-imported-types"
7179
7489
  ]);
7180
- ComponentEntrySchema = z11.object({
7490
+ ComponentEntrySchema = z12.object({
7181
7491
  file: RelPath,
7182
7492
  outcome: ComponentOutcomeSchema,
7183
7493
  /** Identifier-shaped, from the source. Absent for anonymous exports. */
7184
- names: z11.array(z11.string().max(120).regex(/^[A-Za-z_$][A-Za-z0-9_$]*$/)).max(40),
7185
- propCount: z11.number().int().nonnegative(),
7186
- exportStyle: z11.enum(["named", "default", "both", "none"]),
7187
- blindSpots: z11.array(ComponentBlindSpotSchema).max(8)
7188
- });
7189
- HistogramSchema = z11.object({
7190
- counts: z11.record(z11.string().max(60), z11.number().int().nonnegative()),
7191
- total: z11.number().int().nonnegative(),
7192
- dominant: z11.string().max(60).nullable()
7193
- });
7194
- FormattingSchema = z11.object({
7195
- source: z11.enum(["prettier-config", "editorconfig", "source-histogram", "none"]),
7196
- indentStyle: z11.enum(["space", "tab"]).nullable(),
7197
- indentWidth: z11.number().int().min(1).max(16).nullable(),
7198
- quotes: z11.enum(["single", "double"]).nullable(),
7199
- semicolons: z11.boolean().nullable(),
7200
- trailingComma: z11.enum(["none", "es5", "all"]).nullable()
7201
- });
7202
- CodebaseProfileSchema = z11.object({
7203
- profileVersion: z11.literal(CODEBASE_PROFILE_VERSION),
7494
+ names: z12.array(z12.string().max(120).regex(/^[A-Za-z_$][A-Za-z0-9_$]*$/)).max(40),
7495
+ propCount: z12.number().int().nonnegative(),
7496
+ exportStyle: z12.enum(["named", "default", "both", "none"]),
7497
+ blindSpots: z12.array(ComponentBlindSpotSchema).max(8)
7498
+ });
7499
+ HistogramSchema = z12.object({
7500
+ counts: z12.record(z12.string().max(60), z12.number().int().nonnegative()),
7501
+ total: z12.number().int().nonnegative(),
7502
+ dominant: z12.string().max(60).nullable()
7503
+ });
7504
+ FormattingSchema = z12.object({
7505
+ source: z12.enum(["prettier-config", "editorconfig", "source-histogram", "none"]),
7506
+ indentStyle: z12.enum(["space", "tab"]).nullable(),
7507
+ indentWidth: z12.number().int().min(1).max(16).nullable(),
7508
+ quotes: z12.enum(["single", "double"]).nullable(),
7509
+ semicolons: z12.boolean().nullable(),
7510
+ trailingComma: z12.enum(["none", "es5", "all"]).nullable()
7511
+ });
7512
+ CodebaseProfileSchema = z12.object({
7513
+ profileVersion: z12.literal(CODEBASE_PROFILE_VERSION),
7204
7514
  /** The scan's identity. Sha256 over every file actually read (path + bytes),
7205
7515
  * in sorted order — so two scans of the same tree agree and a changed tree
7206
7516
  * does not. */
7207
- inputsHash: z11.string().regex(/^[0-9a-f]{64}$/),
7208
- scannedAt: z11.string(),
7517
+ inputsHash: z12.string().regex(/^[0-9a-f]{64}$/),
7518
+ scannedAt: z12.string(),
7209
7519
  /** Whether the walk finished without hitting a cap. When false,
7210
7520
  * `limitsHit` names which. */
7211
- complete: z11.boolean(),
7212
- limitsHit: z11.array(
7213
- z11.enum([
7521
+ complete: z12.boolean(),
7522
+ limitsHit: z12.array(
7523
+ z12.enum([
7214
7524
  "files",
7215
7525
  "depth",
7216
7526
  "totalBytes",
@@ -7221,51 +7531,51 @@ var init_profile = __esm({
7221
7531
  "stylingEvidence"
7222
7532
  ])
7223
7533
  ).max(8),
7224
- read: z11.object({
7225
- files: z11.number().int().nonnegative(),
7226
- bytes: z11.number().int().nonnegative(),
7227
- packages: z11.number().int().nonnegative(),
7534
+ read: z12.object({
7535
+ files: z12.number().int().nonnegative(),
7536
+ bytes: z12.number().int().nonnegative(),
7537
+ packages: z12.number().int().nonnegative(),
7228
7538
  /** Files the walk saw but could not parse, with why. */
7229
- unparsed: z11.array(
7230
- z11.object({
7539
+ unparsed: z12.array(
7540
+ z12.object({
7231
7541
  file: RelPath,
7232
- reason: z11.enum(["preprocessor-syntax", "parse-error", "too-large", "unreadable"])
7542
+ reason: z12.enum(["preprocessor-syntax", "parse-error", "too-large", "unreadable"])
7233
7543
  })
7234
7544
  ).max(500)
7235
7545
  }),
7236
- styling: z11.object({
7546
+ styling: z12.object({
7237
7547
  /** Set ONLY when exactly one dialect has FILE evidence. Never guessed —
7238
7548
  * a repo with three styling systems gets null and `ambiguous: true`,
7239
7549
  * which is the truth. */
7240
7550
  primary: StylingDialectSchema.nullable(),
7241
- ambiguous: z11.boolean(),
7242
- detected: z11.array(DialectEvidenceSchema).max(40)
7551
+ ambiguous: z12.boolean(),
7552
+ detected: z12.array(DialectEvidenceSchema).max(40)
7243
7553
  }),
7244
- css: z11.object({
7245
- customProperties: z11.array(CustomPropertySchema).max(PROFILE_LIMITS.maxCustomProperties),
7554
+ css: z12.object({
7555
+ customProperties: z12.array(CustomPropertySchema).max(PROFILE_LIMITS.maxCustomProperties),
7246
7556
  /** Names rejected by the ident class, and values too long to carry.
7247
7557
  * Counted so their absence is never read as "the repo declares none". */
7248
- droppedNames: z11.number().int().nonnegative(),
7249
- droppedValues: z11.number().int().nonnegative()
7558
+ droppedNames: z12.number().int().nonnegative(),
7559
+ droppedValues: z12.number().int().nonnegative()
7250
7560
  }),
7251
- components: z11.object({
7252
- entries: z11.array(ComponentEntrySchema).max(PROFILE_LIMITS.maxComponents),
7561
+ components: z12.object({
7562
+ entries: z12.array(ComponentEntrySchema).max(PROFILE_LIMITS.maxComponents),
7253
7563
  fileNaming: HistogramSchema,
7254
7564
  directoryLayout: HistogramSchema,
7255
7565
  exportStyle: HistogramSchema,
7256
7566
  classNameStyle: HistogramSchema,
7257
7567
  colocation: HistogramSchema,
7258
- barrelFiles: z11.number().int().nonnegative(),
7259
- refForwarding: z11.object({
7260
- forwardRef: z11.number().int().nonnegative(),
7261
- asChild: z11.number().int().nonnegative()
7568
+ barrelFiles: z12.number().int().nonnegative(),
7569
+ refForwarding: z12.object({
7570
+ forwardRef: z12.number().int().nonnegative(),
7571
+ asChild: z12.number().int().nonnegative()
7262
7572
  }),
7263
7573
  formatting: FormattingSchema
7264
7574
  }),
7265
7575
  /** Capability limits this scan hit, in the user's words. A Tailwind v3
7266
7576
  * theme lives in a JS config we refuse to execute; that is a real,
7267
7577
  * disclosed limit, not an oversight. */
7268
- disclosures: z11.array(z11.string().max(400)).max(30)
7578
+ disclosures: z12.array(z12.string().max(400)).max(30)
7269
7579
  });
7270
7580
  }
7271
7581
  });
@@ -7282,6 +7592,7 @@ var init_src4 = __esm({
7282
7592
  init_verify_report();
7283
7593
  init_published_surface();
7284
7594
  init_motion_css();
7595
+ init_recording_archive();
7285
7596
  init_profile();
7286
7597
  }
7287
7598
  });
@@ -9106,6 +9417,20 @@ var init_publish_client = __esm({
9106
9417
  retryable: true
9107
9418
  });
9108
9419
  }
9420
+ resolveRecordings(input) {
9421
+ const query = new URLSearchParams({ component: input.component, ...input.figmaFile === void 0 ? {} : { figmaFile: input.figmaFile } });
9422
+ return this.request("GET", `/api/recordings?${query.toString()}`, { body: "", contentType: "application/json", retryable: true });
9423
+ }
9424
+ downloadRecordings(input) {
9425
+ return this.requestBytes("GET", `/api/publications/${encodeURIComponent(input.publicationId)}/recordings`);
9426
+ }
9427
+ carryRecordings(input) {
9428
+ return this.request("PUT", `/api/publications/${encodeURIComponent(input.publicationId)}/recordings`, {
9429
+ body: new TextEncoder().encode(input.envelopeText),
9430
+ contentType: "application/octet-stream",
9431
+ retryable: true
9432
+ });
9433
+ }
9109
9434
  commit(input) {
9110
9435
  return this.json("POST", `/api/publications/${encodeURIComponent(input.publicationId)}/commit`, {}, true);
9111
9436
  }
@@ -9132,7 +9457,15 @@ var init_publish_client = __esm({
9132
9457
  json(method, pathname, body, retryable = false) {
9133
9458
  return this.request(method, pathname, { body: JSON.stringify(body), contentType: "application/json", retryable });
9134
9459
  }
9135
- async request(method, pathname, init) {
9460
+ /**
9461
+ * The transport, minus any opinion about what comes back.
9462
+ *
9463
+ * Split out when the recordings carrier needed a response read as
9464
+ * BYTES rather than JSON: retries, timeouts and the unreachable-host
9465
+ * refusal are properties of the call, not of the body format, and two
9466
+ * copies of a retry policy is two policies.
9467
+ */
9468
+ async attempt(method, pathname, init) {
9136
9469
  let response;
9137
9470
  let lastError = "";
9138
9471
  const attempts = init.retryable === true ? RETRIES : 1;
@@ -9168,6 +9501,12 @@ var init_publish_client = __esm({
9168
9501
  }
9169
9502
  break;
9170
9503
  }
9504
+ return { ok: true, response };
9505
+ }
9506
+ async request(method, pathname, init) {
9507
+ const sent = await this.attempt(method, pathname, init);
9508
+ if (!sent.ok) return sent;
9509
+ const { response } = sent;
9171
9510
  const text = await response.text();
9172
9511
  let parsed;
9173
9512
  try {
@@ -9179,15 +9518,46 @@ var init_publish_client = __esm({
9179
9518
  refusal: `${this.origin} answered with ${String(response.status)} and something that is not JSON, so it is probably not a Tendril portal`
9180
9519
  };
9181
9520
  }
9182
- const record = parsed ?? {};
9183
9521
  if (response.ok) return { ok: true, value: parsed };
9522
+ return this.refusalFrom(response.status, parsed);
9523
+ }
9524
+ /**
9525
+ * A response read as BYTES — the recording archive, which is a
9526
+ * document to store rather than a shape this client owns.
9527
+ *
9528
+ * The SUCCESS path never parses; the refusal path still does, because
9529
+ * a portal refusal is JSON whatever the caller asked for.
9530
+ */
9531
+ async requestBytes(method, pathname) {
9532
+ const sent = await this.attempt(method, pathname, { body: "", contentType: "application/json", retryable: true });
9533
+ if (!sent.ok) return sent;
9534
+ const { response } = sent;
9535
+ if (response.ok) return { ok: true, value: new Uint8Array(await response.arrayBuffer()) };
9536
+ const text = await response.text();
9537
+ let parsed;
9538
+ try {
9539
+ parsed = JSON.parse(text);
9540
+ } catch {
9541
+ return {
9542
+ ok: false,
9543
+ status: response.status,
9544
+ refusal: `${this.origin} answered with ${String(response.status)} and something that is not JSON, so it is probably not a Tendril portal`
9545
+ };
9546
+ }
9547
+ return this.refusalFrom(response.status, parsed);
9548
+ }
9549
+ refusalFrom(status, parsed) {
9550
+ const record = parsed ?? {};
9184
9551
  return {
9185
9552
  ok: false,
9186
- status: response.status,
9187
- refusal: typeof record["refusal"] === "string" ? record["refusal"] : `the portal refused with ${String(response.status)}`,
9553
+ status,
9554
+ refusal: typeof record["refusal"] === "string" ? record["refusal"] : `the portal refused with ${String(status)}`,
9188
9555
  ...Array.isArray(record["detail"]) ? { detail: record["detail"] } : {},
9189
9556
  ...Array.isArray(record["missing"]) ? { missing: record["missing"] } : {},
9190
- ...isRecord(record["needsConfirmation"]) ? { needsConfirmation: record["needsConfirmation"] } : {}
9557
+ ...isRecord(record["needsConfirmation"]) ? { needsConfirmation: record["needsConfirmation"] } : {},
9558
+ ...Array.isArray(record["candidates"]) ? { candidates: record["candidates"] } : {},
9559
+ ...record["alreadyCarried"] === true ? { alreadyCarried: true } : {},
9560
+ ...typeof record["setHash"] === "string" ? { setHash: record["setHash"] } : {}
9191
9561
  };
9192
9562
  }
9193
9563
  };
@@ -9642,7 +10012,7 @@ var init_doctor = __esm({
9642
10012
  });
9643
10013
 
9644
10014
  // packages/llm/src/model-config.ts
9645
- import { z as z12 } from "zod";
10015
+ import { z as z13 } from "zod";
9646
10016
  function resolveModel(config, requestedId) {
9647
10017
  const entry = config.allowlist.find((m) => m.id === requestedId);
9648
10018
  if (entry) return { ok: true, entry };
@@ -9664,46 +10034,46 @@ var ModelStatusSchema, ModelEntrySchema, ModelConfigSchema, DEFAULT_MODEL_CONFIG
9664
10034
  var init_model_config = __esm({
9665
10035
  "packages/llm/src/model-config.ts"() {
9666
10036
  "use strict";
9667
- ModelStatusSchema = z12.enum(["verified", "degraded"]);
9668
- ModelEntrySchema = z12.object({
10037
+ ModelStatusSchema = z13.enum(["verified", "degraded"]);
10038
+ ModelEntrySchema = z13.object({
9669
10039
  /** OpenRouter model id, e.g. "deepseek/deepseek-v4-flash". */
9670
- id: z12.string(),
10040
+ id: z13.string(),
9671
10041
  status: ModelStatusSchema,
9672
10042
  /** Roles this model may fill in the pipeline. */
9673
- roles: z12.array(z12.enum(["bulk", "polish"])),
10043
+ roles: z13.array(z13.enum(["bulk", "polish"])),
9674
10044
  /** Accepts image input. Text-only models get the screenshot dropped with a
9675
10045
  * warning instead of a provider 404 (Carbon run 1 finding). */
9676
- vision: z12.boolean().default(false),
10046
+ vision: z13.boolean().default(false),
9677
10047
  /** Engine-adapter params shipped WITH the entry (ADR-011 §2): the
9678
10048
  * eval-proven settings ride the allowlist so users never rediscover a
9679
10049
  * model's quirks at their own expense (K2.7-code burned its whole
9680
10050
  * completion budget on mandatory reasoning and emitted nothing). */
9681
- adapter: z12.object({
10051
+ adapter: z13.object({
9682
10052
  /** Minimum completion budget the model needs to emit full files. */
9683
- maxTokens: z12.number().optional(),
10053
+ maxTokens: z13.number().optional(),
9684
10054
  /** "mandatory": provider refuses reasoning-off; budget accordingly. */
9685
- reasoning: z12.enum(["mandatory", "optional", "none"]).optional()
10055
+ reasoning: z13.enum(["mandatory", "optional", "none"]).optional()
9686
10056
  }).optional(),
9687
10057
  /** One-line pointer to the evidence behind `status` (which eval, when).
9688
10058
  * Pricing intentionally has no static field — the resolution path is
9689
10059
  * the provider's models endpoint at run time (ADR-011 §2). */
9690
- evidence: z12.string().optional()
10060
+ evidence: z13.string().optional()
9691
10061
  });
9692
- ModelConfigSchema = z12.object({
10062
+ ModelConfigSchema = z13.object({
9693
10063
  /** Default model for codegen + repair (pipeline steps 4–5, facts path). */
9694
- bulk: z12.string(),
10064
+ bulk: z13.string(),
9695
10065
  /** Optional premium polish model (pipeline step 6, aesthetics/naming only). */
9696
- polish: z12.string().optional(),
10066
+ polish: z13.string().optional(),
9697
10067
  /** ADR-011 §2 default-model pointer for the curated recorded-truth
9698
10068
  * engine (a separate axis from the facts-era `bulk`; `generate` flips
9699
10069
  * to it in ADR-010 slice 6). Moves only via the reference-corpus gate. */
9700
- curatedDefault: z12.string().optional(),
10070
+ curatedDefault: z13.string().optional(),
9701
10071
  /** ADR-011 §3a Rung-1 escalation target: the model the curated loop
9702
10072
  * offers for configs still sub-bar at plateau. Designated ONLY via
9703
10073
  * the register's gate (a real curated cert-target run on the measured
9704
10074
  * escalation case); empty means no Rung-1 offer, never a default. */
9705
- escalationTarget: z12.string().optional(),
9706
- allowlist: z12.array(ModelEntrySchema)
10075
+ escalationTarget: z13.string().optional(),
10076
+ allowlist: z13.array(ModelEntrySchema)
9707
10077
  });
9708
10078
  DEFAULT_MODEL_CONFIG = {
9709
10079
  bulk: "google/gemini-3.1-flash-lite",
@@ -9882,7 +10252,7 @@ var init_openrouter = __esm({
9882
10252
  });
9883
10253
 
9884
10254
  // packages/llm/src/semantics.ts
9885
- import { z as z13 } from "zod";
10255
+ import { z as z14 } from "zod";
9886
10256
  function portable(node) {
9887
10257
  if (Array.isArray(node)) return node.map(portable);
9888
10258
  if (typeof node !== "object" || node === null) return node;
@@ -9906,7 +10276,7 @@ function portable(node) {
9906
10276
  return out;
9907
10277
  }
9908
10278
  function toPortableJsonSchema(schema) {
9909
- return portable(z13.toJSONSchema(schema));
10279
+ return portable(z14.toJSONSchema(schema));
9910
10280
  }
9911
10281
  function semanticsJsonSchema() {
9912
10282
  return toPortableJsonSchema(SemanticsSchema);
@@ -9921,18 +10291,18 @@ var PropTypeSchema, SemanticsSchema, CodegenOutputSchema, DocsOutputSchema;
9921
10291
  var init_semantics = __esm({
9922
10292
  "packages/llm/src/semantics.ts"() {
9923
10293
  "use strict";
9924
- PropTypeSchema = z13.discriminatedUnion("kind", [
9925
- z13.object({ kind: z13.literal("enum"), values: z13.array(z13.string()).min(1) }),
9926
- z13.object({ kind: z13.literal("boolean") }),
9927
- z13.object({ kind: z13.literal("string") }),
9928
- z13.object({ kind: z13.literal("reactNode") }),
9929
- z13.object({ kind: z13.literal("handler") })
10294
+ PropTypeSchema = z14.discriminatedUnion("kind", [
10295
+ z14.object({ kind: z14.literal("enum"), values: z14.array(z14.string()).min(1) }),
10296
+ z14.object({ kind: z14.literal("boolean") }),
10297
+ z14.object({ kind: z14.literal("string") }),
10298
+ z14.object({ kind: z14.literal("reactNode") }),
10299
+ z14.object({ kind: z14.literal("handler") })
9930
10300
  ]);
9931
- SemanticsSchema = z13.object({
10301
+ SemanticsSchema = z14.object({
9932
10302
  /** PascalCase component name. */
9933
- componentName: z13.string().regex(/^[A-Z][A-Za-z0-9]*$/),
10303
+ componentName: z14.string().regex(/^[A-Z][A-Za-z0-9]*$/),
9934
10304
  /** Semantic HTML element for the root node. */
9935
- element: z13.enum([
10305
+ element: z14.enum([
9936
10306
  "button",
9937
10307
  "a",
9938
10308
  "div",
@@ -9946,33 +10316,33 @@ var init_semantics = __esm({
9946
10316
  "ul",
9947
10317
  "li"
9948
10318
  ]),
9949
- props: z13.array(
9950
- z13.object({
9951
- name: z13.string().regex(/^[a-z][A-Za-z0-9]*$/),
10319
+ props: z14.array(
10320
+ z14.object({
10321
+ name: z14.string().regex(/^[a-z][A-Za-z0-9]*$/),
9952
10322
  type: PropTypeSchema,
9953
- required: z13.boolean(),
10323
+ required: z14.boolean(),
9954
10324
  /** nullish, not optional: models routinely encode "no default" as
9955
10325
  * null (observed live: kimi-k2.7-code, Carbon run 8). */
9956
- defaultValue: z13.string().nullish()
10326
+ defaultValue: z14.string().nullish()
9957
10327
  })
9958
10328
  ),
9959
10329
  /** Variant axes that become discriminated unions (invalid combos must not compile). */
9960
- discriminatedUnions: z13.array(
9961
- z13.object({
9962
- discriminant: z13.string(),
9963
- arms: z13.array(
9964
- z13.object({
9965
- value: z13.string(),
10330
+ discriminatedUnions: z14.array(
10331
+ z14.object({
10332
+ discriminant: z14.string(),
10333
+ arms: z14.array(
10334
+ z14.object({
10335
+ value: z14.string(),
9966
10336
  /** Props only valid for this arm. */
9967
- extraProps: z13.array(z13.string())
10337
+ extraProps: z14.array(z14.string())
9968
10338
  })
9969
10339
  )
9970
10340
  })
9971
10341
  ),
9972
- a11y: z13.object({
9973
- role: z13.string().nullish(),
9974
- keyboard: z13.array(z13.string()),
9975
- aria: z13.array(z13.string())
10342
+ a11y: z14.object({
10343
+ role: z14.string().nullish(),
10344
+ keyboard: z14.array(z14.string()),
10345
+ aria: z14.array(z14.string())
9976
10346
  }),
9977
10347
  /**
9978
10348
  * How each Figma axis value is realized through the generated API —
@@ -9981,29 +10351,29 @@ var init_semantics = __esm({
9981
10351
  * The visual fact check verifies these claims against real renders, so a
9982
10352
  * wrong mapping fails measurement instead of being trusted.
9983
10353
  */
9984
- variantMapping: z13.array(
9985
- z13.object({
9986
- axis: z13.string(),
9987
- value: z13.string(),
9988
- prop: z13.string(),
9989
- propValue: z13.union([z13.string(), z13.boolean()])
10354
+ variantMapping: z14.array(
10355
+ z14.object({
10356
+ axis: z14.string(),
10357
+ value: z14.string(),
10358
+ prop: z14.string(),
10359
+ propValue: z14.union([z14.string(), z14.boolean()])
9990
10360
  })
9991
10361
  )
9992
10362
  });
9993
- CodegenOutputSchema = z13.object({
10363
+ CodegenOutputSchema = z14.object({
9994
10364
  /** Component source (.tsx). Token-only styling via the provided CSS file. */
9995
- tsx: z13.string().min(1),
10365
+ tsx: z14.string().min(1),
9996
10366
  /** Stylesheet (.css) — themable properties reference var(--token) only. */
9997
- css: z13.string().min(1)
10367
+ css: z14.string().min(1)
9998
10368
  });
9999
- DocsOutputSchema = z13.object({
10000
- usageMd: z13.string().min(1),
10001
- aiHints: z13.object({ selectionCriteria: z13.array(z13.string()).min(1) }),
10002
- antiPatterns: z13.array(
10003
- z13.object({ scenario: z13.string(), reason: z13.string(), alternative: z13.string() })
10369
+ DocsOutputSchema = z14.object({
10370
+ usageMd: z14.string().min(1),
10371
+ aiHints: z14.object({ selectionCriteria: z14.array(z14.string()).min(1) }),
10372
+ antiPatterns: z14.array(
10373
+ z14.object({ scenario: z14.string(), reason: z14.string(), alternative: z14.string() })
10004
10374
  ),
10005
- composition: z13.array(z13.string()),
10006
- examples: z13.array(z13.object({ title: z13.string(), code: z13.string() }))
10375
+ composition: z14.array(z14.string()),
10376
+ examples: z14.array(z14.object({ title: z14.string(), code: z14.string() }))
10007
10377
  });
10008
10378
  }
10009
10379
  });
@@ -10729,7 +11099,7 @@ var init_engine_curated = __esm({
10729
11099
  // packages/generate/src/loop.ts
10730
11100
  import { existsSync as existsSync27, mkdirSync as mkdirSync8, readFileSync as readFileSync24, renameSync, writeFileSync as writeFileSync12 } from "node:fs";
10731
11101
  import path34 from "node:path";
10732
- import { z as z14 } from "zod";
11102
+ import { z as z15 } from "zod";
10733
11103
  function objective(scores, behaviors) {
10734
11104
  const vals = scores.map((s) => Math.min(s.similarity, s.inkRecall));
10735
11105
  return [
@@ -10928,42 +11298,42 @@ var init_loop2 = __esm({
10928
11298
  "packages/generate/src/loop.ts"() {
10929
11299
  "use strict";
10930
11300
  better = (a, b) => a[0] !== b[0] ? a[0] > b[0] : a[1] !== b[1] ? a[1] > b[1] : a[2] > b[2];
10931
- RegionSchema = z14.object({ x0: z14.number(), y0: z14.number(), x1: z14.number(), y1: z14.number(), density: z14.number() });
10932
- ConfigScoreSchema = z14.object({
10933
- rep: z14.string(),
10934
- similarity: z14.number(),
10935
- inkRecall: z14.number(),
10936
- pass: z14.boolean(),
10937
- error: z14.string().optional(),
11301
+ RegionSchema = z15.object({ x0: z15.number(), y0: z15.number(), x1: z15.number(), y1: z15.number(), density: z15.number() });
11302
+ ConfigScoreSchema = z15.object({
11303
+ rep: z15.string(),
11304
+ similarity: z15.number(),
11305
+ inkRecall: z15.number(),
11306
+ pass: z15.boolean(),
11307
+ error: z15.string().optional(),
10938
11308
  region: RegionSchema.optional()
10939
11309
  });
10940
- BehaviorResultSchema = z14.object({ id: z14.string(), pass: z14.boolean(), detail: z14.string().optional() });
10941
- LoopStateSchema = z14.object({
10942
- version: z14.literal(1),
10943
- spentUsd: z14.number(),
10944
- attempts: z14.array(
10945
- z14.object({
10946
- candidate: z14.object({
10947
- files: z14.record(z14.string(), z14.string()),
10948
- raw: z14.string().optional(),
10949
- usage: z14.object({ usd: z14.number(), inTokens: z14.number(), outTokens: z14.number(), modelMs: z14.number(), finishReason: z14.string().optional() }).optional()
11310
+ BehaviorResultSchema = z15.object({ id: z15.string(), pass: z15.boolean(), detail: z15.string().optional() });
11311
+ LoopStateSchema = z15.object({
11312
+ version: z15.literal(1),
11313
+ spentUsd: z15.number(),
11314
+ attempts: z15.array(
11315
+ z15.object({
11316
+ candidate: z15.object({
11317
+ files: z15.record(z15.string(), z15.string()),
11318
+ raw: z15.string().optional(),
11319
+ usage: z15.object({ usd: z15.number(), inTokens: z15.number(), outTokens: z15.number(), modelMs: z15.number(), finishReason: z15.string().optional() }).optional()
10950
11320
  }),
10951
- rulerScore: z14.object({ passCount: z14.number(), floor: z14.number(), mean: z14.number() }),
10952
- accepted: z14.boolean(),
10953
- feedback: z14.string()
11321
+ rulerScore: z15.object({ passCount: z15.number(), floor: z15.number(), mean: z15.number() }),
11322
+ accepted: z15.boolean(),
11323
+ feedback: z15.string()
10954
11324
  })
10955
11325
  ),
10956
- iterations: z14.array(
10957
- z14.object({
10958
- iter: z14.number(),
10959
- usd: z14.number(),
10960
- modelMs: z14.number().optional(),
10961
- scoreMs: z14.number().optional(),
10962
- objective: z14.tuple([z14.number(), z14.number(), z14.number()]),
10963
- accepted: z14.boolean(),
10964
- scores: z14.array(ConfigScoreSchema),
10965
- behaviors: z14.array(BehaviorResultSchema).optional(),
10966
- parseError: z14.string().optional()
11326
+ iterations: z15.array(
11327
+ z15.object({
11328
+ iter: z15.number(),
11329
+ usd: z15.number(),
11330
+ modelMs: z15.number().optional(),
11331
+ scoreMs: z15.number().optional(),
11332
+ objective: z15.tuple([z15.number(), z15.number(), z15.number()]),
11333
+ accepted: z15.boolean(),
11334
+ scores: z15.array(ConfigScoreSchema),
11335
+ behaviors: z15.array(BehaviorResultSchema).optional(),
11336
+ parseError: z15.string().optional()
10967
11337
  })
10968
11338
  )
10969
11339
  });
@@ -12355,28 +12725,13 @@ function recordingSetHash(setDir, configs) {
12355
12725
  channeled = JSON.parse(readFileSync27(path37.join(setDir, "recording-set.json"), "utf8")).channel !== void 0;
12356
12726
  } catch {
12357
12727
  }
12358
- const relPaths = [];
12359
- const setLevel = ["recording-set.json", "completeness-manifest.json", "get_variable_defs.json", "get_metadata.json", ...channeled ? ["get_motion_context.json"] : []];
12360
- for (const name of setLevel) {
12361
- if (existsSync30(path37.join(setDir, name))) relPaths.push(name);
12362
- }
12363
- const perRep = [
12364
- "get_design_context.json",
12365
- "get_metadata.json",
12366
- "get_screenshot.json",
12367
- "get_variable_defs.json",
12368
- ...channeled ? ["get_metadata_interior.json", "rest_nodes.json", "rest_metadata.json", "rest_screenshot.json"] : []
12369
- ];
12370
- for (const cfg of configs) {
12371
- for (const f of perRep) {
12372
- if (existsSync30(path37.join(setDir, cfg.rep, f))) relPaths.push(`${cfg.rep}/${f}`);
12373
- }
12374
- if (existsSync30(path37.join(setDir, cfg.rep))) {
12375
- for (const asset of readdirSync11(path37.join(setDir, cfg.rep)).filter((f) => f.startsWith("asset-"))) {
12376
- relPaths.push(`${cfg.rep}/${asset}`);
12377
- }
12728
+ const relPaths = recordingSetEnumeration(
12729
+ { channeled, reps: configs.map((c) => c.rep) },
12730
+ {
12731
+ exists: (p) => existsSync30(path37.join(setDir, p)),
12732
+ listRep: (rep) => existsSync30(path37.join(setDir, rep)) ? readdirSync11(path37.join(setDir, rep)) : []
12378
12733
  }
12379
- }
12734
+ );
12380
12735
  return hashRecordingSet(
12381
12736
  relPaths,
12382
12737
  (p) => new Uint8Array(readFileSync27(path37.join(setDir, p))),
@@ -13043,7 +13398,7 @@ function composedPins(hostSet, libraryRoots) {
13043
13398
  failures.push(`${rel}: partner bundle manifest at ${bundleDir} became unreadable`);
13044
13399
  continue;
13045
13400
  }
13046
- if (!safeSegment(manifest.name) || !safeSegment(manifest.entry)) {
13401
+ if (!safeSegment2(manifest.name) || !safeSegment2(manifest.entry)) {
13047
13402
  failures.push(
13048
13403
  `${rel}: partner bundle at ${bundleDir} declares an unsafe name/entry (${JSON.stringify(manifest.name)} / ${JSON.stringify(manifest.entry)}) \u2014 refused; both must be a single plain path segment`
13049
13404
  );
@@ -13148,7 +13503,7 @@ function composedChecks(candidateDir, hostEntry, pins) {
13148
13503
  }
13149
13504
  const wrong = [];
13150
13505
  for (const f of pin.moduleFiles) {
13151
- if (!safeSegment(f.name)) {
13506
+ if (!safeSegment2(f.name)) {
13152
13507
  wrong.push(`${f.name} refused (not a plain path segment)`);
13153
13508
  continue;
13154
13509
  }
@@ -13207,7 +13562,7 @@ function regionOverrides(hostSet, partnerSet, instances) {
13207
13562
  }
13208
13563
  return out;
13209
13564
  }
13210
- var composedModuleDir, safeSegment, MAX_PINNED_FILE_BYTES, CONTEXT_LAYOUT_TOKENS;
13565
+ var composedModuleDir, safeSegment2, MAX_PINNED_FILE_BYTES, CONTEXT_LAYOUT_TOKENS;
13211
13566
  var init_compose_pins = __esm({
13212
13567
  "packages/generate/src/compose-pins.ts"() {
13213
13568
  "use strict";
@@ -13216,7 +13571,7 @@ var init_compose_pins = __esm({
13216
13571
  init_brief();
13217
13572
  init_bundle_emit();
13218
13573
  composedModuleDir = (partnerName) => path38.posix.join("composed", partnerName);
13219
- safeSegment = (s) => /^[A-Za-z0-9][A-Za-z0-9._ -]*$/.test(s) && !s.includes("..");
13574
+ safeSegment2 = (s) => /^[A-Za-z0-9][A-Za-z0-9._ -]*$/.test(s) && !s.includes("..");
13220
13575
  MAX_PINNED_FILE_BYTES = 1024 * 1024;
13221
13576
  CONTEXT_LAYOUT_TOKENS = /* @__PURE__ */ new Set(["shrink-0", "grow", "flex-1", "basis-0", "self-stretch", "w-full", "h-full"]);
13222
13577
  }
@@ -18750,7 +19105,7 @@ import { existsSync as existsSync43, mkdtempSync as mkdtempSync3, readFileSync a
18750
19105
  import os8 from "node:os";
18751
19106
  import path53 from "node:path";
18752
19107
  import { fileURLToPath as fileURLToPath6 } from "node:url";
18753
- import { z as z15 } from "zod";
19108
+ import { z as z16 } from "zod";
18754
19109
  function sourceHash() {
18755
19110
  const dir = path53.dirname(fileURLToPath6(import.meta.url));
18756
19111
  const h = createHash12("sha256");
@@ -18768,23 +19123,23 @@ var init_server = __esm({
18768
19123
  CLI_BIN = path53.join(REPO_ROOT3, "packages", "cli", "src", "bin.ts");
18769
19124
  BUNDLED_CLI = path53.join(path53.dirname(fileURLToPath6(import.meta.url)), "tendril.js");
18770
19125
  CLI_SPAWN = existsSync43(BUNDLED_CLI) ? { cmd: process.execPath, prefix: [BUNDLED_CLI] } : { cmd: "npx", prefix: ["tsx", CLI_BIN] };
18771
- str = (d) => z15.string().describe(d);
18772
- optStr = (d) => z15.string().optional().describe(d);
19126
+ str = (d) => z16.string().describe(d);
19127
+ optStr = (d) => z16.string().optional().describe(d);
18773
19128
  TOOLS = [
18774
19129
  {
18775
19130
  name: "tendril_record_plan",
18776
19131
  description: "ENTRY POINT for implementing/building a React component from a Figma design or figma.com URL \u2014 start the Tendril pipeline here (after loading the tendril skill, if installed). Plans a recording session: computes the rep queue (anchor + one-factor + conflict crosses) from the verbatim get_metadata response (pass its blocks via metadataParts \u2014 no file to write) and persists the set manifest. Resumes if the set already exists. The output may carry USER QUESTIONS \u2014 defaultsToConfirm (which pose is the component's default) or a multiple-component-sets error (which set to record): render them to a present user and apply the answers via `defaults` / `componentSet`; non-interactive runs follow each question's stated fallback. It may also carry `interactionStatesToConfirm` \u2014 the recording holds no hover/focus/pressed state, so nothing shows how the component behaves when someone uses it: say its `statement` and `designFix` in that SAME one message (the fix is a Figma variant, not code). It is a disclosure, not a gate \u2014 no answer is required and recording proceeds regardless. The output also carries `feasibilityCheck`: the call arithmetic for this queue plus the free `whoami` check that turns it into a verdict \u2014 complete that handshake BEFORE the first recording call, and surface the verdict to the user when the set does not fit their daily allowance.",
18777
- schema: z15.object({
19132
+ schema: z16.object({
18778
19133
  setDir: str("recording set directory to create/resume"),
18779
19134
  component: str("component/system name"),
18780
19135
  // Parts array FIRST, same reason as ingest_rep: real responses
18781
19136
  // are usually multi-block, and the file param made plan the ONE
18782
19137
  // remaining hand-built-envelope entry point (run 10: the agent
18783
19138
  // wrote the file twice — once as text, once as JSON envelope).
18784
- metadataParts: z15.array(z15.string()).optional().describe("the frame-level get_metadata response blocks, every block in order, each verbatim \u2014 never hand-join or save them yourself; this is the NORMAL param"),
19139
+ metadataParts: z16.array(z16.string()).optional().describe("the frame-level get_metadata response blocks, every block in order, each verbatim \u2014 never hand-join or save them yourself; this is the NORMAL param"),
18785
19140
  metadata: optStr("ONLY when get_metadata genuinely returned one single block: its text verbatim (otherwise use metadataParts)"),
18786
- metadataFiles: z15.array(z15.string()).optional().describe('saved verbatim get_metadata envelope file paths \u2014 JSON shape {"content":[{"type":"text","text":"<frame \u2026>"}]}; optionally <file>@<frameId>. Prefer metadataParts: no file to write'),
18787
- defaults: z15.array(z15.string()).optional().describe(`axis defaults as "Axis=Value" (from the user's defaultsToConfirm answers; may re-plan a set with nothing recorded yet)`),
19141
+ metadataFiles: z16.array(z16.string()).optional().describe('saved verbatim get_metadata envelope file paths \u2014 JSON shape {"content":[{"type":"text","text":"<frame \u2026>"}]}; optionally <file>@<frameId>. Prefer metadataParts: no file to write'),
19142
+ defaults: z16.array(z16.string()).optional().describe(`axis defaults as "Axis=Value" (from the user's defaultsToConfirm answers; may re-plan a set with nothing recorded yet)`),
18788
19143
  componentSet: optStr("record only this component set (the user's pick from a multiple-component-sets error)"),
18789
19144
  figmaFile: optStr("the Figma file key from the design URL you were handed \u2014 figma.com/design/<KEY>/\u2026, pass exactly <KEY>. ALWAYS pass it on a fresh plan: it is the set's recorded file identity for cross-bundle composition (ADR-013), captured at plan time only and never backfillable later")
18790
19145
  }),
@@ -18820,10 +19175,10 @@ var init_server = __esm({
18820
19175
  {
18821
19176
  name: "tendril_permissions",
18822
19177
  description: "Install the Claude Code permission allowlist for the Tendril pipeline (write: true \u2014 merges the MCP per-tool entries, the run's shell surface, and project-scoped Write/Edit with deny guards for ./.claude and ./.git into the project's .claude/settings.local.json, idempotent, never touches other keys), or list the entries without writing. The shell/Write grants are CONVENIENCE, not a security boundary \u2014 the output's note names exactly what they trade; RELAY it with the offer. OFFER THIS AT PIPELINE START whenever NO merged Claude settings file (project .claude/settings.local.json or .claude/settings.json, or user ~/.claude/settings.json) contains tendril MCP entries \u2014 plugin installs use mcp__plugin_tendril_tendril__*, direct claude-mcp-add installs use mcp__<server>__* \u2014 when this session's tool names differ from the plugin defaults, pass figmaPrefix/tendrilPrefix with the prefixes you actually see, or the written entries never match. A FILE check, never prompt-watching \u2014 agents cannot observe permission prompts. ONE approval here replaces a prompt per pipeline call. Never run it unoffered; the user must reload the session for new settings to apply \u2014 say so.",
18823
- schema: z15.object({
18824
- write: z15.boolean().optional().describe("true = install into .claude/settings.local.json (the point of this tool); false/absent = just return the entries"),
18825
- figmaPrefix: z15.string().optional().describe("The Figma server's entry prefix AS THIS SESSION NAMES ITS TOOLS \u2014 the part before __get_metadata (e.g. mcp__figma-remote, mcp__plugin_figma_figma). Pass it whenever the session's Figma tools are not mcp__plugin_figma_figma__* \u2014 entries written under the wrong prefix never match anything (measured: six dead entries in a figma-remote session)."),
18826
- tendrilPrefix: z15.string().optional().describe("Same for the tendril server when this session's tendril tools are not mcp__plugin_tendril_tendril__* (e.g. mcp__tendril for claude mcp add installs).")
19178
+ schema: z16.object({
19179
+ write: z16.boolean().optional().describe("true = install into .claude/settings.local.json (the point of this tool); false/absent = just return the entries"),
19180
+ figmaPrefix: z16.string().optional().describe("The Figma server's entry prefix AS THIS SESSION NAMES ITS TOOLS \u2014 the part before __get_metadata (e.g. mcp__figma-remote, mcp__plugin_figma_figma). Pass it whenever the session's Figma tools are not mcp__plugin_figma_figma__* \u2014 entries written under the wrong prefix never match anything (measured: six dead entries in a figma-remote session)."),
19181
+ tendrilPrefix: z16.string().optional().describe("Same for the tendril server when this session's tendril tools are not mcp__plugin_tendril_tendril__* (e.g. mcp__tendril for claude mcp add installs).")
18827
19182
  }),
18828
19183
  argv: (i) => [
18829
19184
  "permissions",
@@ -18837,13 +19192,13 @@ var init_server = __esm({
18837
19192
  name: "tendril_doctor",
18838
19193
  annotations: { readOnlyHint: true },
18839
19194
  description: "Machine readiness + version status in one shot: installed vs latest version (with publish date and update remediation), browser identity, font-cache state, Figma desktop MCP reachability, and whether this machine holds a LIVE portal session (checked against the portal itself; informational \u2014 publishing needs it, verification never does; when absent, offer tendril_login). Use to self-diagnose before recording/scoring, or whenever versions are in question. Exit 1 = something not ready; the report says exactly what and how to fix it.",
18840
- schema: z15.object({}),
19195
+ schema: z16.object({}),
18841
19196
  argv: () => ["doctor"]
18842
19197
  },
18843
19198
  {
18844
19199
  name: "tendril_login",
18845
19200
  description: "Connect this machine to the user's Tendril portal account WITHOUT the user touching a terminal \u2014 phase one of the browser-approve sign-in. Starts the handshake, opens their browser to the approve page, and returns a verification link plus a short code. RELAY BOTH to the user verbatim: they click Approve on the page, and they must check the page shows EXACTLY this code (that check is the phishing defence \u2014 an attacker can send someone else's approve link). Then call tendril_login_wait to finish. Offer this whenever tendril_doctor reports no portal session and the user wants to publish or share; publishing needs it, verification never does.",
18846
- schema: z15.object({
19201
+ schema: z16.object({
18847
19202
  portal: optStr("portal origin override for self-hosted portals (defaults to https://app.trytendril.com)")
18848
19203
  }),
18849
19204
  argv: (i) => ["login", "--device-start", ...typeof i["portal"] === "string" ? ["--to", i["portal"]] : []]
@@ -18851,13 +19206,13 @@ var init_server = __esm({
18851
19206
  {
18852
19207
  name: "tendril_login_wait",
18853
19208
  description: "Phase two of the browser-approve sign-in: waits (up to ten minutes, with live progress) for the user to click Approve on the page tendril_login opened, then stores the session on this machine. Call it right after relaying the link and code. A denial, a lapse, and success each come back as their own sentence; confirm the result to the user, and on success they are ready to publish.",
18854
- schema: z15.object({}),
19209
+ schema: z16.object({}),
18855
19210
  argv: () => ["login", "--device-wait"]
18856
19211
  },
18857
19212
  {
18858
19213
  name: "tendril_figma_connect",
18859
19214
  description: "Grant this machine read access to the user's Figma files over Figma's API \u2014 phase one of the browser Figma connection. Recording over the Figma MCP transport pays a hard per-day call quota; this connection lets recording fetch over Figma's REST API instead (per-minute, batched), so large component sets stop hitting daily limits. Starts the handshake and returns a connect link \u2014 RELAY IT to the user verbatim: they sign in to their portal account if asked, then click Allow on Figma's consent screen (read-only file access; revocable any time in their Figma settings). Then call tendril_figma_connect_wait to finish. You cannot consent for them: the portal only accepts the browser's signed-in click, never this machine's token. Offer it when a recording plan warns about Figma call limits; requires a portal session (tendril_login).",
18860
- schema: z15.object({
19215
+ schema: z16.object({
18861
19216
  portal: optStr("portal origin override for self-hosted portals (defaults to the stored session's portal)")
18862
19217
  }),
18863
19218
  argv: (i) => ["figma-connect", "--start", ...typeof i["portal"] === "string" ? ["--to", i["portal"]] : []]
@@ -18865,13 +19220,13 @@ var init_server = __esm({
18865
19220
  {
18866
19221
  name: "tendril_figma_connect_wait",
18867
19222
  description: "Phase two of the browser Figma connection: waits (with live progress) for the user's Allow click on Figma's consent screen, then stores the credential on this machine (it renews itself from then on). Call it right after relaying the connect link. A decline, a lapse, and success each come back as their own sentence \u2014 report the outcome to the user.",
18868
- schema: z15.object({}),
19223
+ schema: z16.object({}),
18869
19224
  argv: () => ["figma-connect", "--wait"]
18870
19225
  },
18871
19226
  {
18872
19227
  name: "tendril_publish",
18873
19228
  description: "Publish a VERIFIED bundle to the user's portal \u2014 phase one of the browser-approved publish. Re-publishing an already-published component completes in one call. A component's FIRST publish is a human-only decision the portal enforces: this call requests the approval and returns the approve-page link \u2014 RELAY IT to the user verbatim, along with which account the result says to be signed in as (they click Approve in the browser; approving includes accepting the design system's publishing terms, which is their decision to make, never yours to urge). Then call tendril_publish_wait to finish. You cannot approve this yourself: the portal only accepts the decision from their signed-in browser, never from this machine's token. Requires a green verify (the CLI refuses a declined run) and a portal session (tendril_login).",
18874
- schema: z15.object({
19229
+ schema: z16.object({
18875
19230
  bundleDir: str("bundle directory (verified \u2014 carries component.json and verify-evidence)"),
18876
19231
  portal: optStr("portal origin override for self-hosted portals (defaults to the stored session's portal)")
18877
19232
  }),
@@ -18880,38 +19235,55 @@ var init_server = __esm({
18880
19235
  {
18881
19236
  name: "tendril_publish_wait",
18882
19237
  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).',
18883
- schema: z15.object({
19238
+ schema: z16.object({
18884
19239
  bundleDir: str("the same bundle directory tendril_publish was called with"),
18885
19240
  portal: optStr("portal origin override (must match tendril_publish's)")
18886
19241
  }),
18887
19242
  argv: (i) => ["publish", i["bundleDir"], "--approve-wait", "--wait-window", "55", ...typeof i["portal"] === "string" ? ["--to", i["portal"]] : []]
18888
19243
  },
19244
+ {
19245
+ name: "tendril_pull",
19246
+ description: "Bring a published component's RECORDING SET onto this machine \u2014 the recorded design truth its verdict was measured against. Use it when the recordings are not here: a fresh machine, a clone with no recordings/ directory, or a component someone else on the team published. ONE bounded call, no wait twin. Needs a portal session (tendril_login) because a recording set belongs to the account that published it; everything afterwards \u2014 verify, compose, republish \u2014 is local, offline and account-less as always. The name is EXACT and the portal's refusals list the user's real component names, so a name that does not resolve is a one-step correction rather than a dead end; when one name spans two design systems, pass `figmaFile`. It writes to ./recordings/<component>/ and never overwrites a different set already there. AFTER pulling, call tendril_record_status on each set you pulled, then continue the connect prompt from its step 3.",
19247
+ schema: z16.object({
19248
+ component: str("the component name, exactly as the user's library shows it"),
19249
+ figmaFile: optStr("the design system's Figma file key \u2014 only when one name spans several of the user's design systems (the portal's refusal says when)"),
19250
+ dest: optStr("where the set lands (default: ./recordings/<component>)"),
19251
+ portal: optStr("portal origin override for self-hosted portals (defaults to the stored session's portal)")
19252
+ }),
19253
+ argv: (i) => [
19254
+ "pull",
19255
+ i["component"],
19256
+ ...typeof i["figmaFile"] === "string" ? ["--figma-file", i["figmaFile"]] : [],
19257
+ ...typeof i["dest"] === "string" ? ["--dest", i["dest"]] : [],
19258
+ ...typeof i["portal"] === "string" ? ["--to", i["portal"]] : []
19259
+ ]
19260
+ },
18889
19261
  {
18890
19262
  name: "tendril_record_next",
18891
19263
  annotations: { readOnlyHint: true },
18892
19264
  description: "Get the next pending recording instruction (which Figma MCP tool to call for which node, and how to save it). RARELY NEEDED: every ingest/fetch response already carries `next` \u2014 use this only to resume an interrupted session. The full queue is known from plan, so independent reps may be recorded in any order (and in parallel).",
18893
- schema: z15.object({ setDir: str("recording set directory") }),
19265
+ schema: z16.object({ setDir: str("recording set directory") }),
18894
19266
  argv: (i) => ["record", "next", "--set", i["setDir"]]
18895
19267
  },
18896
19268
  {
18897
19269
  name: "tendril_record_rest_fetch",
18898
19270
  description: "Fetch a recording set's pixels+geometry over Figma's REST API \u2014 the step `record next` names as rest_fetch when the set's REST channel is active. The CLI makes these requests itself with the user's connected Figma credential (tendril_figma_connect); you make NO Figma call for it and none can substitute. It first self-checks one or two probe poses against their MCP-recorded references (zero-pixel tolerance) and falls the whole set back to MCP recording if the transports disagree; then it bulk-fetches the rest, batched and paced, spending no MCP quota. Requires the probe poses to be recorded first \u2014 record next tells you when.",
18899
- schema: z15.object({ setDir: str("recording set directory") }),
19271
+ schema: z16.object({ setDir: str("recording set directory") }),
18900
19272
  argv: (i) => ["record", "rest-fetch", "--set", i["setDir"]]
18901
19273
  },
18902
19274
  {
18903
19275
  name: "tendril_record_bindings",
18904
19276
  description: "Fetch Figma's instance\u2192component bindings for an EXISTING MCP-recorded set \u2014 one or two batched REST calls, no re-recording, pixels/geometry/identity untouched. This is how components that are ALREADY recorded and generated become connectable: the bindings make cross-component pairing id-backed, so `tendril compose` can propose it for the human's confirm. Every binding is congruence-verified against the recorded metadata (a design that changed since recording refuses per pose, named). The CLI makes the requests itself with the user's connected Figma credential (tendril_figma_connect); you make NO Figma call. The result names the next step \u2014 including when no partner set is visible in the scanned root (co-locate the sets or pass compose --library). For sets recorded before file identity was captured, pass the design's file key as `file`.",
18905
- schema: z15.object({ setDir: str("recording set directory"), file: optStr("the design's file key (figma.com/design/<KEY>/\u2026) \u2014 only for sets whose manifest lacks figmaFile; an operator assertion, congruence still gates every binding") }),
19277
+ schema: z16.object({ setDir: str("recording set directory"), file: optStr("the design's file key (figma.com/design/<KEY>/\u2026) \u2014 only for sets whose manifest lacks figmaFile; an operator assertion, congruence still gates every binding") }),
18906
19278
  argv: (i) => ["record", "bindings", "--set", i["setDir"], ...typeof i["file"] === "string" ? ["--file", i["file"]] : []]
18907
19279
  },
18908
19280
  {
18909
19281
  name: "tendril_compose",
18910
19282
  description: "Put ONE composition pairing in front of the user's BROWSER \u2014 phase one of the browser-approved connect (invariant-5 migration; the terminal `--confirm-compositions` path remains for humans at a TTY). Requires a portal session (tendril_login \u2014 one browser Approve). Returns the approve-page link: RELAY IT verbatim, name the account the result says to approve as, and never urge the decision \u2014 the card carries the engine's honest disclosures and the human reads them. Deny on the card is NOT NOW (this request only, never a permanent decline). You cannot decide this yourself on any channel: the portal accepts the decision only from the user's signed-in browser, and the terminal flag only from an interactive TTY. A host with several open pairings needs `pair` (one card = one decision). Then finish with tendril_compose_wait. Both recording sets must be visible in one scanned workspace \u2014 co-locate them or pass `library`.",
18911
- schema: z15.object({
19283
+ schema: z16.object({
18912
19284
  setDir: str("the HOST recording set directory (the component that embeds the partner)"),
18913
19285
  pair: optStr("the pair-key to request when the host has several open pairings"),
18914
- library: z15.array(z15.string()).optional().describe("workspace root(s) holding the partner recording set(s) \u2014 required when they live in another project root"),
19286
+ library: z16.array(z16.string()).optional().describe("workspace root(s) holding the partner recording set(s) \u2014 required when they live in another project root"),
18915
19287
  portal: optStr("portal origin override (defaults to the stored session's portal)")
18916
19288
  }),
18917
19289
  argv: (i) => [
@@ -18927,7 +19299,7 @@ var init_server = __esm({
18927
19299
  {
18928
19300
  name: "tendril_compose_wait",
18929
19301
  description: 'Phase two of the browser-approved connect. Waits in a BOUNDED window (~1 minute per call) for the user\'s decision on the card tendril_compose returned; on Approve it records the SAME manifest entry the terminal confirm writes \u2014 after re-deriving the pairing from the CURRENT recordings and refusing if anything changed underneath the click (the click is then not wrong; the project moved \u2014 run the connect again). While undecided, each call returns `status: "approval-pending"` \u2014 a heartbeat, not a failure: one short liveness line to the user, then call again; the request stays live ~30 minutes, and a decided card can take moments to land. NOT NOW, a lapse, and success each arrive as their own sentence \u2014 report the one you got. After success: regenerate the HOST bundle with the same library roots (brief \u2192 generate \u2192 score \u2192 verify), then republish \u2014 announce the republish in one line first. Compose waits follow the same heartbeat contract as publish waits: never urge, never call an in-flight wait stuck.',
18930
- schema: z15.object({
19302
+ schema: z16.object({
18931
19303
  setDir: str("the same HOST set directory tendril_compose was called with"),
18932
19304
  portal: optStr("portal origin override (must match tendril_compose's)")
18933
19305
  }),
@@ -18936,10 +19308,10 @@ var init_server = __esm({
18936
19308
  {
18937
19309
  name: "tendril_record_fetch",
18938
19310
  description: "Download a Figma asset URL (from get_screenshot's image_url) straight to disk and ingest it as the rep's envelope \u2014 the fallback when only the screenshot piece needs (re-)recording; for a rep's standard three recordings PREFER tendril_record_ingest_rep. Never download the image yourself: the bytes must not pass through your context.",
18939
- schema: z15.object({
19311
+ schema: z16.object({
18940
19312
  setDir: str("recording set directory"),
18941
19313
  rep: str("planned rep slug"),
18942
- tool: z15.enum(["get_screenshot"]).describe("get_screenshot"),
19314
+ tool: z16.enum(["get_screenshot"]).describe("get_screenshot"),
18943
19315
  url: str("image_url from the Figma response, verbatim")
18944
19316
  }),
18945
19317
  argv: (i) => ["record", "fetch", "--set", i["setDir"], "--rep", i["rep"], "--tool", i["tool"], "--url", i["url"]]
@@ -18947,15 +19319,15 @@ var init_server = __esm({
18947
19319
  {
18948
19320
  name: "tendril_record_ingest_rep",
18949
19321
  description: "Ingest a rep's ENTIRE recording in ONE call \u2014 the get_metadata response, the get_design_context response, and the get_screenshot image_url together. Make the three Figma calls first, in protocol order (get_metadata, then get_design_context with excludeScreenshot=true, then get_screenshot with contentsOnly=true and maxDimension=4096 \u2014 isolation keeps the editor's component-set chrome out of the padded export, where it would otherwise score as unpaintable reference ink), then pass all three here VERBATIM. PREFER THIS over three separate ingest/fetch calls: one approvable operation per rep instead of three. Pieces land independently: on a partial failure the error names exactly which piece(s) to re-record \u2014 the rest are already on disk. The response carries `next` and, for design context, `assets` (auto-fetched server-side; only listed failures need record_asset).",
18950
- schema: z15.object({
19322
+ schema: z16.object({
18951
19323
  setDir: str("recording set directory"),
18952
19324
  rep: str("planned rep slug"),
18953
19325
  // Parts arrays FIRST: in the field, EVERY real Figma response is
18954
19326
  // multi-block (run 6: 49/49 reps — metadata 2 blocks, design
18955
19327
  // context 5-6), so the arrays are the norm and the single-string
18956
19328
  // params the rare case, not the reverse.
18957
- metadataParts: z15.array(z15.string()).optional().describe("get_metadata response blocks, every block in order, each verbatim \u2014 never hand-join blocks yourself. Multi-block responses are common (run 6: 49/49 reps); a single block wrapped in a one-element array is equally fine."),
18958
- contextParts: z15.array(z15.string()).optional().describe("get_design_context response blocks, every block in order, each verbatim \u2014 the NORMAL param (real responses arrive as 5-6 blocks)"),
19329
+ metadataParts: z16.array(z16.string()).optional().describe("get_metadata response blocks, every block in order, each verbatim \u2014 never hand-join blocks yourself. Multi-block responses are common (run 6: 49/49 reps); a single block wrapped in a one-element array is equally fine."),
19330
+ contextParts: z16.array(z16.string()).optional().describe("get_design_context response blocks, every block in order, each verbatim \u2014 the NORMAL param (real responses arrive as 5-6 blocks)"),
18959
19331
  screenshotUrl: optStr("image_url from the get_screenshot response, verbatim \u2014 the CLI downloads it; the bytes never pass through your context"),
18960
19332
  metadata: optStr("ONLY when get_metadata genuinely returned one single block: its text verbatim (otherwise use metadataParts)"),
18961
19333
  context: optStr("ONLY when get_design_context genuinely returned one single block: its text verbatim (otherwise use contextParts)")
@@ -18988,7 +19360,7 @@ var init_server = __esm({
18988
19360
  {
18989
19361
  name: "tendril_record_ingest",
18990
19362
  description: "Single-piece ingest of a VERBATIM Figma tool-response \u2014 the fallback path (re-recording one failed piece, the set-level get_variable_defs and get_motion_context steps, get_metadata_interior for mains); for a rep's standard three recordings PREFER tendril_record_ingest_rep, which takes them all in one call. Pass `text` (single block) or `texts` (response split into multiple output blocks \u2014 each block verbatim, in order; NEVER hand-join them): the CLI constructs the envelope from the same bytes. The response includes `next` and, for get_design_context, `assets` (auto-fetched; only listed failures need manual handling).",
18991
- schema: z15.object({
19363
+ schema: z16.object({
18992
19364
  setDir: str("recording set directory"),
18993
19365
  rep: str("planned rep slug, or __set__ for the set-level steps (get_variable_defs, get_motion_context)"),
18994
19366
  // Enumerated, not a free string: this value reaches a file path.
@@ -19001,9 +19373,9 @@ var init_server = __esm({
19001
19373
  // dead-ended every MCP-driven recording at the motion step with
19002
19374
  // no MCP path to complete (the CLI accepted what the primary
19003
19375
  // surface could not send). The pin lives in server.test.ts.
19004
- tool: z15.enum(["get_design_context", "get_metadata", "get_screenshot", "get_variable_defs", "get_metadata_interior", "get_motion_context"]),
19376
+ tool: z16.enum(["get_design_context", "get_metadata", "get_screenshot", "get_variable_defs", "get_metadata_interior", "get_motion_context"]),
19005
19377
  text: optStr("the tool response text VERBATIM \u2014 exactly as returned, unmodified (single-block responses; text tools only \u2014 screenshots go through record_fetch)"),
19006
- texts: z15.array(z15.string()).optional().describe("when the response arrived as MULTIPLE output blocks: every block, in order, each verbatim \u2014 never hand-join blocks yourself"),
19378
+ texts: z16.array(z16.string()).optional().describe("when the response arrived as MULTIPLE output blocks: every block, in order, each verbatim \u2014 never hand-join blocks yourself"),
19007
19379
  file: optStr("path to a saved envelope JSON (alternative to text/texts)")
19008
19380
  }),
19009
19381
  // The text rides a temp file, never argv: Windows caps a command
@@ -19027,7 +19399,7 @@ var init_server = __esm({
19027
19399
  {
19028
19400
  name: "tendril_record_asset",
19029
19401
  description: "FALLBACK ONLY \u2014 ingest auto-fetches design-context assets; use this just for assets listed in an ingest response's `assets.failed`. Batch mode: pass `dir` to ingest every asset-*.<ext> in a directory in ONE call. SVGs with active content are rejected; sizes are capped.",
19030
- schema: z15.object({
19402
+ schema: z16.object({
19031
19403
  setDir: str("recording set directory"),
19032
19404
  rep: str("planned rep slug"),
19033
19405
  name: optStr("asset-<id>.<ext> (single-asset mode)"),
@@ -19048,13 +19420,13 @@ var init_server = __esm({
19048
19420
  name: "tendril_record_status",
19049
19421
  annotations: { readOnlyHint: true },
19050
19422
  description: "Recording-set completeness: per-rep recorded/missing tools, files that exist but are unusable (invalid \u2014 re-record those), and whether the set-level steps are recorded (get_variable_defs always; get_motion_context when the plan asked \u2014 ADR-016). `complete` means the RECORDING is usable by the next pipeline step \u2014 it does NOT mean nothing is left to decide: read the `composition` field too (always present: openPairs lists unconfirmed partner pairs with pair-keys \u2014 surface those to the user BEFORE generating the host, partner-first; {unavailable} means discovery could not run, which is unknown, not 'no pairs').",
19051
- schema: z15.object({ setDir: str("recording set directory") }),
19423
+ schema: z16.object({ setDir: str("recording set directory") }),
19052
19424
  argv: (i) => ["record", "status", "--set", i["setDir"]]
19053
19425
  },
19054
19426
  {
19055
19427
  name: "tendril_engine_brief",
19056
19428
  description: "AGENT-HARNESS engine, step 1: emits the task payload file (system brief + every recorded config's emission, box, assets, tokens) and the protocol. YOU (the calling agent) implement the bundle; the CLI is the only judge. Read the payload file completely before proposing.",
19057
- schema: z15.object({
19429
+ schema: z16.object({
19058
19430
  taskOrSet: str("a recording-set directory (from tendril_record), or a reference task name \u2014 an unknown name returns the valid list in the error"),
19059
19431
  // Required by design, not convenience: the model choice must be
19060
19432
  // settled BEFORE generation starts. A smoke run picked its own
@@ -19081,13 +19453,13 @@ var init_server = __esm({
19081
19453
  {
19082
19454
  name: "tendril_engine_score",
19083
19455
  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.",
19084
- schema: z15.object({
19456
+ schema: z16.object({
19085
19457
  taskOrSet: str("reference task name or recording-set directory"),
19086
19458
  candidateDir: str("directory containing the proposed bundle files"),
19087
19459
  bar: optStr("pass (default) or cert"),
19088
19460
  host: optStr("your host identity (e.g. claude-code, cursor, codex) \u2014 recorded as self-reported provenance"),
19089
19461
  model: str("the model that proposed the candidate \u2014 REQUIRED; recorded as self-reported provenance, and the CLI refuses to score without it"),
19090
- rebind: z15.boolean().optional().describe("explicitly re-bind an already-bound bundle to a DIFFERENT recording set \u2014 scoring refuses this otherwise, because rebinding silently rewrites the bundle's verification identity; only pass after telling the user"),
19462
+ rebind: z16.boolean().optional().describe("explicitly re-bind an already-bound bundle to a DIFFERENT recording set \u2014 scoring refuses this otherwise, because rebinding silently rewrites the bundle's verification identity; only pass after telling the user"),
19091
19463
  library: optStr("workspace root holding confirmed partners generated bundles (ADR-013 composed pins; default: the server's working directory) \u2014 pass the same root the brief used")
19092
19464
  }),
19093
19465
  argv: (i) => [
@@ -19106,7 +19478,7 @@ var init_server = __esm({
19106
19478
  {
19107
19479
  name: "tendril_codeconnect",
19108
19480
  description: "Emit a Figma Code Connect template (.figma.ts) for a certified bundle \u2014 every Figma variant value mapped to its verified prop fragment from recorded truth, stamped with the bundle's trust statement. EXTRA VALUE step after verify passes: offer it to the user. Publishing is the USER'S action (their Figma token, Organization/Enterprise plan) \u2014 via npx @figma/code-connect connect publish, or the Figma MCP's own add_code_connect_map/send_code_connect_mappings tools if available in this session.",
19109
- schema: z15.object({
19481
+ schema: z16.object({
19110
19482
  bundleDir: str("bundle directory (carries component.json)"),
19111
19483
  figmaUrl: str("figma.com /design/ URL of the COMPONENT SET, with node-id (ask the user to Copy link to selection if you don't have it)"),
19112
19484
  set: optStr("recording set override (default: the bundle's provenance path)"),
@@ -19124,7 +19496,7 @@ var init_server = __esm({
19124
19496
  {
19125
19497
  name: "tendril_verify",
19126
19498
  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.",
19127
- schema: z15.object({
19499
+ schema: z16.object({
19128
19500
  bundleDir: str("bundle directory to verify"),
19129
19501
  bar: optStr("pass (default) or cert"),
19130
19502
  set: optStr("recording-set directory override"),
@@ -19141,12 +19513,12 @@ var init_server = __esm({
19141
19513
  {
19142
19514
  name: "tendril_generate_curated",
19143
19515
  description: "CURATED engine (explicit alternative path): generation by an allowlisted API model over the user's OpenRouter-compatible key, with cost consent, hard spend caps, and resume. Use ONLY when the user asks for API-model generation instead of implementing it yourself.",
19144
- schema: z15.object({
19516
+ schema: z16.object({
19145
19517
  input: str("reference task name or recording-set directory"),
19146
19518
  model: optStr("OpenRouter model id (default: allowlist pointer)"),
19147
19519
  bar: optStr("pass (default) or cert"),
19148
19520
  cap: optStr("spend cap in USD (default 1.50)"),
19149
- yes: z15.boolean().optional().describe("accept the cost consent (the user must have approved the spend)")
19521
+ yes: z16.boolean().optional().describe("accept the cost consent (the user must have approved the spend)")
19150
19522
  }),
19151
19523
  argv: (i) => [
19152
19524
  "generate",
@@ -19411,6 +19783,57 @@ var init_permissions = __esm({
19411
19783
  }
19412
19784
  });
19413
19785
 
19786
+ // packages/cli/src/commands/publish-recordings.ts
19787
+ import { existsSync as existsSync45, readFileSync as readFileSync41, readdirSync as readdirSync18, statSync as statSync6 } from "node:fs";
19788
+ import path55 from "node:path";
19789
+ import { createHash as createHash13 } from "node:crypto";
19790
+ function sha256Sync(chunks) {
19791
+ const h = createHash13("sha256");
19792
+ for (const chunk of chunks) h.update(chunk);
19793
+ return h.digest("hex");
19794
+ }
19795
+ function planRecordingCarry(input) {
19796
+ const chosen = input.override ?? input.provenancePath;
19797
+ const setDir = path55.resolve(input.cwd, chosen);
19798
+ const named = input.override === void 0 ? "the recording set this bundle names" : "the recording set you named";
19799
+ const without = (why) => ({
19800
+ carried: false,
19801
+ note: `publishing without the recording set: ${why} (looked in ${setDir})`
19802
+ });
19803
+ if (!existsSync45(setDir)) return without(`${named} is not on this machine`);
19804
+ try {
19805
+ if (!statSync6(setDir).isDirectory()) return without(`${named} is not a directory`);
19806
+ } catch (error) {
19807
+ return without(`${named} could not be read (${error.message})`);
19808
+ }
19809
+ let packed;
19810
+ try {
19811
+ packed = packRecordingArchive(
19812
+ {
19813
+ exists: (relPath) => existsSync45(path55.join(setDir, relPath)),
19814
+ read: (relPath) => new Uint8Array(readFileSync41(path55.join(setDir, relPath))),
19815
+ listRep: (rep) => existsSync45(path55.join(setDir, rep)) ? readdirSync18(path55.join(setDir, rep)) : []
19816
+ },
19817
+ { sha256: sha256Sync }
19818
+ );
19819
+ } catch (error) {
19820
+ return without(`${named} could not be packed (${error.message})`);
19821
+ }
19822
+ if (!packed.ok) return without(packed.refusal);
19823
+ if (packed.setHash !== input.scoredSetHash) {
19824
+ return without(
19825
+ `${named} is not the one this verdict was measured against (the set here derives ${packed.setHash.slice(0, 12)}\u2026, the report was scored on ${input.scoredSetHash.slice(0, 12)}\u2026)`
19826
+ );
19827
+ }
19828
+ return { carried: true, envelopeText: packed.envelopeText, setHash: packed.setHash, setDir, staleEnrichment: packed.staleEnrichment };
19829
+ }
19830
+ var init_publish_recordings = __esm({
19831
+ "packages/cli/src/commands/publish-recordings.ts"() {
19832
+ "use strict";
19833
+ init_src4();
19834
+ }
19835
+ });
19836
+
19414
19837
  // packages/cli/src/commands/publish.ts
19415
19838
  var publish_exports = {};
19416
19839
  __export(publish_exports, {
@@ -19419,8 +19842,8 @@ __export(publish_exports, {
19419
19842
  runPublish: () => runPublish,
19420
19843
  spendPendingApproval: () => spendPendingApproval
19421
19844
  });
19422
- import { existsSync as existsSync45, readFileSync as readFileSync41, rmSync as rmSync8, writeFileSync as writeFileSync23 } from "node:fs";
19423
- import path55 from "node:path";
19845
+ import { existsSync as existsSync46, readFileSync as readFileSync42, rmSync as rmSync8, writeFileSync as writeFileSync23 } from "node:fs";
19846
+ import path56 from "node:path";
19424
19847
  async function runPublish(opts) {
19425
19848
  if (opts.waitWindowSeconds !== void 0 && opts.approveWait !== true) {
19426
19849
  fail(opts, ExitCode.InputValidation, {
@@ -19436,7 +19859,7 @@ async function runPublish(opts) {
19436
19859
  remediation: "Pass e.g. --wait-window 55."
19437
19860
  });
19438
19861
  }
19439
- const bundleDir = path55.resolve(opts.bundleDir);
19862
+ const bundleDir = path56.resolve(opts.bundleDir);
19440
19863
  const bundle = readBundle(opts, bundleDir);
19441
19864
  const report = bundle.report;
19442
19865
  if (typeof report["rulerExit"] !== "number" || !Number.isInteger(report["rulerExit"])) {
@@ -19481,7 +19904,7 @@ async function runPublish(opts) {
19481
19904
  const sheetEntry = surface.published.find((p) => p.role === "inspect-sheet");
19482
19905
  if (sheetEntry !== void 0) {
19483
19906
  const missingCrops = missingInspectCrops(
19484
- readFileSync41(path55.join(bundleDir, sheetEntry.path), "utf8"),
19907
+ readFileSync42(path56.join(bundleDir, sheetEntry.path), "utf8"),
19485
19908
  surface.published.map((p) => p.path)
19486
19909
  );
19487
19910
  if (missingCrops.length > 0) {
@@ -19583,8 +20006,8 @@ async function runPublish(opts) {
19583
20006
  if (opts.approveWait === true) spendPendingApproval();
19584
20007
  const uploaded = [];
19585
20008
  for (const object of opened.value.plan.objects) {
19586
- const file = path55.join(bundleDir, object.relPath);
19587
- if (!existsSync45(file)) {
20009
+ const file = path56.join(bundleDir, object.relPath);
20010
+ if (!existsSync46(file)) {
19588
20011
  fail(opts, ExitCode.InputValidation, {
19589
20012
  error: `the portal expects ${object.relPath}, and it is not in ${opts.bundleDir}`,
19590
20013
  code: "planned-file-missing",
@@ -19594,12 +20017,49 @@ async function runPublish(opts) {
19594
20017
  const sent = await client.upload({
19595
20018
  publicationId: opened.value.publicationId,
19596
20019
  relPath: object.relPath,
19597
- bytes: new Uint8Array(readFileSync41(file))
20020
+ bytes: new Uint8Array(readFileSync42(file))
19598
20021
  });
19599
20022
  if (!sent.ok) refuse(opts, sent, "upload-refused", true);
19600
20023
  uploaded.push({ relPath: object.relPath, deduplicated: sent.value.deduplicated });
19601
20024
  emitProgress(uploaded.length, opened.value.plan.objects.length, `uploading ${object.relPath}`);
19602
20025
  }
20026
+ let recordingNote;
20027
+ let carried;
20028
+ if (opts.noRecordings === true) {
20029
+ } else if (client.carryRecordings === void 0) {
20030
+ recordingNote = "publishing without the recording set: this client cannot carry recordings";
20031
+ } else {
20032
+ const plan = planRecordingCarry({
20033
+ bundleDir,
20034
+ cwd: process.cwd(),
20035
+ ...opts.recordings === void 0 ? {} : { override: opts.recordings },
20036
+ provenancePath: bundle.manifest.provenance.recordingSet.path,
20037
+ scoredSetHash: scoredRecordingSetHash(report)
20038
+ });
20039
+ if (!plan.carried) {
20040
+ recordingNote = plan.note;
20041
+ } else {
20042
+ emitProgress(uploaded.length, uploaded.length, "carrying the recording set");
20043
+ let sent;
20044
+ try {
20045
+ sent = await client.carryRecordings({ publicationId: opened.value.publicationId, envelopeText: plan.envelopeText });
20046
+ } catch (error) {
20047
+ sent = { ok: false, status: 0, refusal: `the recording set could not be sent (${error.message})` };
20048
+ }
20049
+ if (sent.ok) {
20050
+ carried = { setHash: plan.setHash, setDir: plan.setDir, sizeBytes: plan.envelopeText.length, staleEnrichment: plan.staleEnrichment };
20051
+ } else if (sent.alreadyCarried === true) {
20052
+ carried = { setHash: sent.setHash ?? plan.setHash, setDir: plan.setDir, sizeBytes: plan.envelopeText.length, staleEnrichment: plan.staleEnrichment };
20053
+ recordingNote = "this publication already carried its recording set from an earlier attempt, and that copy stands \u2014 the set on this machine has since changed outside the scored hash (a `record bindings` re-run does this), which changes nothing about the verdict";
20054
+ } else {
20055
+ recordingNote = sent.status === 404 ? "publishing without the recording set: this portal does not carry recording sets yet \u2014 the component published normally" : `publishing without the recording set: ${sent.refusal}`;
20056
+ recordingNote += ` (the set here is ${plan.setDir}, ${plan.setHash.slice(0, 12)}\u2026)`;
20057
+ if (sent.needsConfirmation?.approvePath !== void 0) {
20058
+ recordingNote += ` \u2014 approve it once at ${origin}${sent.needsConfirmation.approvePath}, then publish again`;
20059
+ }
20060
+ }
20061
+ }
20062
+ }
19603
20063
  emitProgress(uploaded.length, uploaded.length, "upload complete \u2014 committing the publication");
19604
20064
  const committed = await client.commit({ publicationId: opened.value.publicationId });
19605
20065
  if (committed.ok) {
@@ -19624,7 +20084,15 @@ async function runPublish(opts) {
19624
20084
  figmaFile: opened.value.figmaFile,
19625
20085
  rulerVersion: opened.value.rulerVersion,
19626
20086
  resumed: opened.value.resumed === true,
19627
- files: uploaded
20087
+ files: uploaded,
20088
+ recordings: carried === void 0 ? { carried: false, ...recordingNote === void 0 ? {} : { note: recordingNote } } : {
20089
+ carried: true,
20090
+ setHash: carried.setHash,
20091
+ setDir: carried.setDir,
20092
+ sizeBytes: carried.sizeBytes,
20093
+ staleEnrichment: carried.staleEnrichment,
20094
+ ...recordingNote === void 0 ? {} : { note: recordingNote }
20095
+ }
19628
20096
  },
19629
20097
  () => {
19630
20098
  const reused = uploaded.filter((u) => u.deduplicated).length;
@@ -19637,27 +20105,42 @@ async function runPublish(opts) {
19637
20105
  `);
19638
20106
  process.stdout.write(` verdict as scored by ruler ${opened.value.rulerVersion}
19639
20107
  `);
20108
+ if (carried !== void 0) {
20109
+ process.stdout.write(` recording set carried \u2014 pull it on another machine with \`${tendrilCommand(`pull ${JSON.stringify(componentName)}`)}\`
20110
+ `);
20111
+ if (recordingNote !== void 0) process.stdout.write(` ${recordingNote}
20112
+ `);
20113
+ if (carried.staleEnrichment.length > 0) {
20114
+ process.stdout.write(
20115
+ ` left ${String(carried.staleEnrichment.length)} stale binding record(s) behind (${carried.staleEnrichment.slice(0, 3).join(", ")}) \u2014 re-run \`${tendrilCommand("record bindings")}\` after pulling
20116
+ `
20117
+ );
20118
+ }
20119
+ } else if (recordingNote !== void 0) {
20120
+ process.stdout.write(` ${recordingNote}
20121
+ `);
20122
+ }
19640
20123
  }
19641
20124
  );
19642
20125
  }
19643
20126
  function readBundle(opts, bundleDir) {
19644
- const manifestPath2 = path55.join(bundleDir, "component.json");
19645
- const reportPath = path55.join(bundleDir, EVIDENCE_DIR, VERIFY_REPORT_FILENAME);
19646
- if (!existsSync45(manifestPath2)) {
20127
+ const manifestPath2 = path56.join(bundleDir, "component.json");
20128
+ const reportPath = path56.join(bundleDir, EVIDENCE_DIR, VERIFY_REPORT_FILENAME);
20129
+ if (!existsSync46(manifestPath2)) {
19647
20130
  fail(opts, ExitCode.InputValidation, {
19648
20131
  error: `there is no component.json in ${opts.bundleDir}, so this is not a bundle`,
19649
20132
  code: "not-a-bundle",
19650
20133
  remediation: `Point publish at a directory a Tendril run produced. \`${tendrilCommand("generate --help")}\` shows how one is made.`
19651
20134
  });
19652
20135
  }
19653
- if (!existsSync45(reportPath)) {
20136
+ if (!existsSync46(reportPath)) {
19654
20137
  fail(opts, ExitCode.InputValidation, {
19655
20138
  error: `this bundle has never been verified \u2014 there is no ${EVIDENCE_DIR}/${VERIFY_REPORT_FILENAME}`,
19656
20139
  code: "bundle-not-verified",
19657
20140
  remediation: `Run \`${tendrilCommand(`verify ${opts.bundleDir}`)}\` first. Verification is free and needs no account; publishing without it would put a page up with no verdict on it.`
19658
20141
  });
19659
20142
  }
19660
- const { manifest } = readBundleManifest(readFileSync41(manifestPath2, "utf8"));
20143
+ const { manifest } = readBundleManifest(readFileSync42(manifestPath2, "utf8"));
19661
20144
  if (manifest === void 0) {
19662
20145
  fail(opts, ExitCode.InputValidation, {
19663
20146
  error: "component.json did not parse as a bundle manifest",
@@ -19665,7 +20148,7 @@ function readBundle(opts, bundleDir) {
19665
20148
  remediation: `Re-emit the bundle \u2014 \`${tendrilCommand(`verify ${opts.bundleDir}`)}\` rewrites component.json \u2014 then publish.`
19666
20149
  });
19667
20150
  }
19668
- const reportText = readFileSync41(reportPath, "utf8");
20151
+ const reportText = readFileSync42(reportPath, "utf8");
19669
20152
  let report;
19670
20153
  try {
19671
20154
  report = JSON.parse(reportText);
@@ -19733,7 +20216,7 @@ function refuse(opts, sent, code, rejoins = false) {
19733
20216
  });
19734
20217
  }
19735
20218
  function pendingApprovalPath() {
19736
- return path55.join(path55.dirname(sessionPath()), "pending-publish.json");
20219
+ return path56.join(path56.dirname(sessionPath()), "pending-publish.json");
19737
20220
  }
19738
20221
  async function runApprovalFlow(opts, client, bundleDir, input) {
19739
20222
  const requested = await client.requestApproval({ componentName: input.componentName, figmaFile: input.figmaFile });
@@ -19780,9 +20263,9 @@ async function runApprovalFlow(opts, client, bundleDir, input) {
19780
20263
  async function approveWaitPhase(opts, client, bundleDir, subject) {
19781
20264
  const file = pendingApprovalPath();
19782
20265
  let pending;
19783
- if (existsSync45(file)) {
20266
+ if (existsSync46(file)) {
19784
20267
  try {
19785
- const parsed = JSON.parse(readFileSync41(file, "utf8"));
20268
+ const parsed = JSON.parse(readFileSync42(file, "utf8"));
19786
20269
  if (typeof parsed.approvalId === "string" && typeof parsed.approveUrl === "string" && typeof parsed.expiresAt === "string") {
19787
20270
  pending = { pollSeconds: 3, bundleDir, ...parsed };
19788
20271
  }
@@ -19871,6 +20354,7 @@ var init_publish = __esm({
19871
20354
  init_src4();
19872
20355
  init_invocation();
19873
20356
  init_output();
20357
+ init_publish_recordings();
19874
20358
  init_publish_client();
19875
20359
  init_run_presence();
19876
20360
  APPROVAL_WAIT_CAP_MS = 31 * 6e4;
@@ -19886,9 +20370,9 @@ __export(compose_approve_exports, {
19886
20370
  runComposeApproveStart: () => runComposeApproveStart,
19887
20371
  runComposeApproveWait: () => runComposeApproveWait
19888
20372
  });
19889
- import { createHash as createHash13 } from "node:crypto";
19890
- import { existsSync as existsSync46, mkdirSync as mkdirSync14, readFileSync as readFileSync42, rmSync as rmSync9, writeFileSync as writeFileSync24 } from "node:fs";
19891
- import path56 from "node:path";
20373
+ import { createHash as createHash14 } from "node:crypto";
20374
+ import { existsSync as existsSync47, mkdirSync as mkdirSync14, readFileSync as readFileSync43, rmSync as rmSync9, writeFileSync as writeFileSync24 } from "node:fs";
20375
+ import path57 from "node:path";
19892
20376
  function composeSubjectDigest(subject) {
19893
20377
  const preimage = JSON.stringify([
19894
20378
  subject.hostComponent,
@@ -19899,16 +20383,16 @@ function composeSubjectDigest(subject) {
19899
20383
  subject.instances.map((i) => [i.hostRep, i.instanceId, i.poseVariantNodeId]),
19900
20384
  subject.disclosures
19901
20385
  ]);
19902
- return createHash13("sha256").update(preimage, "utf8").digest("hex");
20386
+ return createHash14("sha256").update(preimage, "utf8").digest("hex");
19903
20387
  }
19904
20388
  function composeSubjectFor(hostSet, pair) {
19905
20389
  const manifest = loadManifest(hostSet);
19906
20390
  const canonical = {
19907
20391
  hostComponent: manifest.component,
19908
20392
  hostFigmaFile: manifest.figmaFile ?? "unidentified",
19909
- hostManifestSha256: createHash13("sha256").update(readFileSync42(path56.join(hostSet, "recording-set.json"))).digest("hex"),
20393
+ hostManifestSha256: createHash14("sha256").update(readFileSync43(path57.join(hostSet, "recording-set.json"))).digest("hex"),
19910
20394
  pairKey: pair.key,
19911
- partnerManifestSha256: pair.partnerDirs.map((d) => [fromStoredRel(path56.relative(hostSet, d)), createHash13("sha256").update(readFileSync42(path56.join(d, "recording-set.json"))).digest("hex")]).sort((a, b) => a[0] < b[0] ? -1 : 1),
20395
+ partnerManifestSha256: pair.partnerDirs.map((d) => [fromStoredRel(path57.relative(hostSet, d)), createHash14("sha256").update(readFileSync43(path57.join(d, "recording-set.json"))).digest("hex")]).sort((a, b) => a[0] < b[0] ? -1 : 1),
19912
20396
  instances: [...pair.instances].map((i) => ({ hostRep: i.hostRep, instanceId: i.instanceId, poseVariantNodeId: i.poseVariantNodeId })).sort((a, b) => a.hostRep + a.instanceId < b.hostRep + b.instanceId ? -1 : 1),
19913
20397
  disclosures: [...pair.disclosures]
19914
20398
  };
@@ -19961,10 +20445,10 @@ function composePortalClient(flags) {
19961
20445
  return new HttpPublishClient({ origin, token: found.token });
19962
20446
  }
19963
20447
  function pendingComposePath() {
19964
- return path56.join(path56.dirname(sessionPath()), "pending-compose.json");
20448
+ return path57.join(path57.dirname(sessionPath()), "pending-compose.json");
19965
20449
  }
19966
20450
  function openPairsFor(hostSet, roots) {
19967
- const scanRoots = [.../* @__PURE__ */ new Set([...roots, path56.dirname(hostSet)])];
20451
+ const scanRoots = [.../* @__PURE__ */ new Set([...roots, path57.dirname(hostSet)])];
19968
20452
  const edges = composeReport(buildComposeIndex(scanRoots));
19969
20453
  const pairs = substitutionPairs(edges, hostSet);
19970
20454
  const { raw } = readManifestFile(hostSet);
@@ -19975,7 +20459,7 @@ function openPairsFor(hostSet, roots) {
19975
20459
  return pairs.filter((p) => !decidedKeys.has(p.key));
19976
20460
  }
19977
20461
  async function runComposeApproveStart(flags, hostSet, roots) {
19978
- if (!existsSync46(path56.join(hostSet, "recording-set.json"))) {
20462
+ if (!existsSync47(path57.join(hostSet, "recording-set.json"))) {
19979
20463
  fail(flags, ExitCode.InputValidation, { error: `no recording-set.json in ${hostSet}`, code: "no-recording-set", remediation: "Point --set at a recorded host set." });
19980
20464
  }
19981
20465
  const open = openPairsFor(hostSet, roots);
@@ -20030,7 +20514,7 @@ async function runComposeApproveStart(flags, hostSet, roots) {
20030
20514
  roots,
20031
20515
  requestedAt: (/* @__PURE__ */ new Date()).toISOString()
20032
20516
  };
20033
- mkdirSync14(path56.dirname(pendingComposePath()), { recursive: true });
20517
+ mkdirSync14(path57.dirname(pendingComposePath()), { recursive: true });
20034
20518
  writeFileSync24(pendingComposePath(), `${JSON.stringify(pending, null, 2)}
20035
20519
  `, { mode: 384 });
20036
20520
  emitData(
@@ -20058,9 +20542,9 @@ async function runComposeApproveStart(flags, hostSet, roots) {
20058
20542
  async function runComposeApproveWait(flags, hostSet) {
20059
20543
  const file = pendingComposePath();
20060
20544
  let pending;
20061
- if (existsSync46(file)) {
20545
+ if (existsSync47(file)) {
20062
20546
  try {
20063
- const parsed = JSON.parse(readFileSync42(file, "utf8"));
20547
+ const parsed = JSON.parse(readFileSync43(file, "utf8"));
20064
20548
  if (typeof parsed.approvalId === "string" && typeof parsed.subjectDigest === "string" && typeof parsed.hostSet === "string" && typeof parsed.pairKey === "string") {
20065
20549
  pending = parsed;
20066
20550
  }
@@ -20074,7 +20558,7 @@ async function runComposeApproveWait(flags, hostSet) {
20074
20558
  remediation: `Start one first: ${tendrilCommand(`compose --set ${quoteArg(hostSet)} --approve-start`)} (the tendril_compose tool).`
20075
20559
  });
20076
20560
  }
20077
- if (path56.resolve(pending.hostSet) !== path56.resolve(hostSet)) {
20561
+ if (path57.resolve(pending.hostSet) !== path57.resolve(hostSet)) {
20078
20562
  fail(flags, ExitCode.InputValidation, {
20079
20563
  error: `the waiting approval is for ${pending.hostSet}, and this wait is for ${hostSet}`,
20080
20564
  code: "pending-compose-mismatch",
@@ -20206,7 +20690,7 @@ async function runComposeApproveWait(flags, hostSet) {
20206
20690
  }
20207
20691
  async function runComposeApprove(flags) {
20208
20692
  const base = process.env["INIT_CWD"] ?? process.cwd();
20209
- const roots = flags.library !== void 0 && flags.library.length > 0 ? flags.library.map((d) => path56.resolve(base, d)) : [base];
20693
+ const roots = flags.library !== void 0 && flags.library.length > 0 ? flags.library.map((d) => path57.resolve(base, d)) : [base];
20210
20694
  if (flags.set === void 0) {
20211
20695
  fail(flags, ExitCode.InputValidation, {
20212
20696
  error: "a compose decision flag requires --set <host> \u2014 a decision needs the host set it decides for",
@@ -20228,7 +20712,7 @@ async function runComposeApprove(flags) {
20228
20712
  remediation: "Pass e.g. --wait-window 55."
20229
20713
  });
20230
20714
  }
20231
- const hostSet = path56.resolve(base, flags.set);
20715
+ const hostSet = path57.resolve(base, flags.set);
20232
20716
  if (flags.approveStart === true) {
20233
20717
  await runComposeApproveStart(flags, hostSet, roots);
20234
20718
  return;
@@ -20259,8 +20743,8 @@ __export(login_exports, {
20259
20743
  runLogout: () => runLogout
20260
20744
  });
20261
20745
  import { spawn } from "node:child_process";
20262
- import { existsSync as existsSync47, mkdirSync as mkdirSync15, readFileSync as readFileSync43, rmSync as rmSync10, writeFileSync as writeFileSync25 } from "node:fs";
20263
- import path57 from "node:path";
20746
+ import { existsSync as existsSync48, mkdirSync as mkdirSync15, readFileSync as readFileSync44, rmSync as rmSync10, writeFileSync as writeFileSync25 } from "node:fs";
20747
+ import path58 from "node:path";
20264
20748
  import { isCancel as isCancel3, password as password2 } from "@clack/prompts";
20265
20749
  async function runLogin(opts, deps) {
20266
20750
  const origin = (opts.to ?? process.env["TENDRIL_PORTAL_URL"] ?? DEFAULT_PORTAL_ORIGIN).replace(/\/+$/, "");
@@ -20354,12 +20838,12 @@ function settleDecision(opts, origin, outcome) {
20354
20838
  }
20355
20839
  }
20356
20840
  function pendingLoginPath() {
20357
- return path57.join(path57.dirname(sessionPath()), "pending-login.json");
20841
+ return path58.join(path58.dirname(sessionPath()), "pending-login.json");
20358
20842
  }
20359
20843
  async function deviceStartPhase(opts, origin, deps) {
20360
20844
  const started = await startHandshake(opts, origin, deps);
20361
20845
  const file = pendingLoginPath();
20362
- mkdirSync15(path57.dirname(file), { recursive: true });
20846
+ mkdirSync15(path58.dirname(file), { recursive: true });
20363
20847
  writeFileSync25(file, `${JSON.stringify({ origin, ...started }, null, 2)}
20364
20848
  `, { mode: 384 });
20365
20849
  deps.openBrowser(started.verificationUrl);
@@ -20385,9 +20869,9 @@ async function deviceStartPhase(opts, origin, deps) {
20385
20869
  async function deviceWaitPhase(opts, deps) {
20386
20870
  const file = pendingLoginPath();
20387
20871
  let pending;
20388
- if (existsSync47(file)) {
20872
+ if (existsSync48(file)) {
20389
20873
  try {
20390
- const parsed = JSON.parse(readFileSync43(file, "utf8"));
20874
+ const parsed = JSON.parse(readFileSync44(file, "utf8"));
20391
20875
  if (typeof parsed.origin === "string" && typeof parsed.deviceCode === "string" && typeof parsed.intervalSeconds === "number") {
20392
20876
  pending = parsed;
20393
20877
  }
@@ -20529,10 +21013,10 @@ var figma_connect_exports = {};
20529
21013
  __export(figma_connect_exports, {
20530
21014
  runFigmaConnect: () => runFigmaConnect
20531
21015
  });
20532
- import { existsSync as existsSync48, mkdirSync as mkdirSync16, readFileSync as readFileSync44, rmSync as rmSync11, writeFileSync as writeFileSync26 } from "node:fs";
20533
- import path58 from "node:path";
21016
+ import { existsSync as existsSync49, mkdirSync as mkdirSync16, readFileSync as readFileSync45, rmSync as rmSync11, writeFileSync as writeFileSync26 } from "node:fs";
21017
+ import path59 from "node:path";
20534
21018
  function pendingConnectPath() {
20535
- return path58.join(path58.dirname(sessionPath()), "pending-figma-connect.json");
21019
+ return path59.join(path59.dirname(sessionPath()), "pending-figma-connect.json");
20536
21020
  }
20537
21021
  function resolveOrigin2(opts) {
20538
21022
  const named = (opts.to ?? process.env["TENDRIL_PORTAL_URL"] ?? "").replace(/\/+$/, "");
@@ -20569,7 +21053,7 @@ async function runFigmaConnect(opts) {
20569
21053
  const started = await startConnect(opts, send, origin, token);
20570
21054
  if (opts.start === true) {
20571
21055
  const pending = { origin, ...started };
20572
- mkdirSync16(path58.dirname(pendingConnectPath()), { recursive: true });
21056
+ mkdirSync16(path59.dirname(pendingConnectPath()), { recursive: true });
20573
21057
  writeFileSync26(pendingConnectPath(), `${JSON.stringify(pending, null, 2)}
20574
21058
  `, { mode: 384 });
20575
21059
  (opts.openBrowser ?? (() => {
@@ -20623,9 +21107,9 @@ async function startConnect(opts, send, origin, token) {
20623
21107
  async function waitPhase(opts, send) {
20624
21108
  const file = pendingConnectPath();
20625
21109
  let pending;
20626
- if (existsSync48(file)) {
21110
+ if (existsSync49(file)) {
20627
21111
  try {
20628
- const parsed = JSON.parse(readFileSync44(file, "utf8"));
21112
+ const parsed = JSON.parse(readFileSync45(file, "utf8"));
20629
21113
  if (typeof parsed.origin === "string" && typeof parsed.connectId === "string") pending = parsed;
20630
21114
  } catch {
20631
21115
  }
@@ -20823,6 +21307,314 @@ var init_share = __esm({
20823
21307
  }
20824
21308
  });
20825
21309
 
21310
+ // packages/cli/src/commands/pull.ts
21311
+ var pull_exports = {};
21312
+ __export(pull_exports, {
21313
+ recordingSlug: () => recordingSlug,
21314
+ runPull: () => runPull
21315
+ });
21316
+ import { createHash as createHash15 } from "node:crypto";
21317
+ import { existsSync as existsSync50, mkdirSync as mkdirSync17, mkdtempSync as mkdtempSync4, readFileSync as readFileSync46, readdirSync as readdirSync19, renameSync as renameSync2, rmSync as rmSync12, writeFileSync as writeFileSync27 } from "node:fs";
21318
+ import { tmpdir } from "node:os";
21319
+ import path60 from "node:path";
21320
+ function setHashFromDisk(dir) {
21321
+ const manifest = path60.join(dir, "recording-set.json");
21322
+ if (!existsSync50(manifest)) return { ok: false, refusal: "it carries no recording-set.json" };
21323
+ const shape = readSetShape(new Uint8Array(readFileSync46(manifest)));
21324
+ if (!shape.ok) return { ok: false, refusal: shape.refusal };
21325
+ const enumeration = recordingSetEnumeration(
21326
+ { channeled: shape.shape.channeled, reps: shape.shape.reps },
21327
+ {
21328
+ exists: (relPath) => existsSync50(path60.join(dir, relPath)),
21329
+ listRep: (rep) => existsSync50(path60.join(dir, rep)) ? readdirSync19(path60.join(dir, rep)) : []
21330
+ }
21331
+ );
21332
+ return { ok: true, setHash: hashRecordingSet(enumeration, (relPath) => new Uint8Array(readFileSync46(path60.join(dir, relPath))), sha256) };
21333
+ }
21334
+ function recordingSlug(componentName) {
21335
+ const collapsed = componentName.replace(/[^A-Za-z0-9_-]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 64);
21336
+ return collapsed === "" ? "recording-set" : collapsed;
21337
+ }
21338
+ function portalClient(opts) {
21339
+ if (opts.client !== void 0) return opts.client;
21340
+ const origin = resolveOrigin({ to: opts.to });
21341
+ if (origin === "") {
21342
+ fail(opts, ExitCode.InputValidation, {
21343
+ error: "no portal to pull from",
21344
+ code: "no-portal-configured",
21345
+ remediation: "Pass --to <url> or set TENDRIL_PORTAL_URL. Recording sets live in the account that published them."
21346
+ });
21347
+ }
21348
+ if (!isSecureOrigin(origin)) {
21349
+ fail(opts, ExitCode.InputValidation, {
21350
+ error: `${origin} is not https, so a session token sent there would travel in the clear`,
21351
+ code: "portal-not-https",
21352
+ remediation: "Use the https:// address of your portal. Only 127.0.0.1 and localhost are exempt, for local development."
21353
+ });
21354
+ }
21355
+ const found = tokenFor(origin);
21356
+ if (!found.ok && found.reason === "origin-mismatch") {
21357
+ fail(opts, ExitCode.Auth, {
21358
+ error: `the session available here belongs to ${found.boundTo}, and this would ask ${origin}`,
21359
+ code: "session-belongs-to-another-portal",
21360
+ remediation: "Sign in to the portal that holds these recordings."
21361
+ });
21362
+ }
21363
+ if (!found.ok) {
21364
+ fail(opts, ExitCode.Auth, {
21365
+ error: `no session for ${origin}`,
21366
+ code: "not-signed-in",
21367
+ remediation: "A recording set belongs to the account that published it, so this one call needs you signed in. Agents: run tendril_login \u2014 the user signs in with ONE browser Approve, then re-run this. (Verifying a bundle you already have needs no account, ever.)"
21368
+ });
21369
+ }
21370
+ return new HttpPublishClient({ origin, token: found.token });
21371
+ }
21372
+ async function runPull(opts) {
21373
+ const component = opts.component.trim();
21374
+ if (component === "") {
21375
+ fail(opts, ExitCode.InputValidation, {
21376
+ error: "name the component whose recording set you want",
21377
+ code: "pull-names-no-component",
21378
+ remediation: `Use the name your library shows, e.g. \`${tendrilCommand('pull "Radio group"')}\`.`
21379
+ });
21380
+ }
21381
+ const client = portalClient(opts);
21382
+ if (client.resolveRecordings === void 0 || client.downloadRecordings === void 0) {
21383
+ fail(opts, ExitCode.General, {
21384
+ error: "this client cannot pull recordings",
21385
+ code: "pull-unsupported-client",
21386
+ remediation: "Update the CLI (npx resolves @latest on its own; a pinned install wants a newer @tendrilapp/cli)."
21387
+ });
21388
+ }
21389
+ emitProgress(0, 3, "asking the portal which publication holds these recordings");
21390
+ const resolved = await client.resolveRecordings({ component, ...opts.figmaFile === void 0 ? {} : { figmaFile: opts.figmaFile } });
21391
+ if (!resolved.ok) {
21392
+ const authFailed = resolved.status === 401 || resolved.status === 403;
21393
+ if (resolved.status === 404 && resolved.candidates === void 0) {
21394
+ fail(opts, ExitCode.General, {
21395
+ error: "this portal does not carry recording sets yet",
21396
+ code: "pull-unsupported-portal",
21397
+ remediation: "Nothing is wrong with what you asked for. The recordings have to come from the machine that recorded them until this portal is updated \u2014 copy the recording-set directory across, or re-publish from that machine once it is."
21398
+ });
21399
+ }
21400
+ fail(opts, authFailed ? ExitCode.Auth : ExitCode.InputValidation, {
21401
+ error: resolved.refusal,
21402
+ code: authFailed ? "pull-not-signed-in" : "pull-not-resolved",
21403
+ remediation: authFailed ? "This session is no longer valid for that portal. Agents: run tendril_login \u2014 the user signs in with ONE browser Approve, then re-run this. (Verifying a bundle you already have needs no account, ever.)" : resolved.candidates === void 0 || resolved.candidates.length === 0 ? `Publish the component from the machine that holds its recordings first \u2014 a publish carries them by default.` : `Pull one of these instead: ${resolved.candidates.map((c) => `${quoteArg(c.componentName)}${c.figmaFile === "unidentified" ? "" : ` (--figma-file ${c.figmaFile})`}${c.carried ? "" : " \u2014 carries no recording set"}`).join("; ")}`
21404
+ });
21405
+ }
21406
+ const answer = resolved.value;
21407
+ emitProgress(1, 3, `downloading ${String(answer.sizeBytes)} bytes of recorded design`);
21408
+ const downloaded = await client.downloadRecordings({ publicationId: answer.publicationId });
21409
+ if (!downloaded.ok) {
21410
+ fail(opts, downloaded.status === 401 || downloaded.status === 403 ? ExitCode.Auth : ExitCode.General, {
21411
+ error: downloaded.refusal,
21412
+ code: "pull-download-refused",
21413
+ remediation: "Try again; if the portal keeps refusing, re-publish the component from the machine that holds its recordings."
21414
+ });
21415
+ }
21416
+ const bytes = downloaded.value;
21417
+ if (bytes.length > MAX_ARCHIVE_BYTES) {
21418
+ fail(opts, ExitCode.General, {
21419
+ error: `the portal returned ${String(bytes.length)} bytes for a recording set, and no archive may exceed ${String(MAX_ARCHIVE_BYTES)}`,
21420
+ code: "pull-archive-too-large",
21421
+ remediation: "Nothing was written. Check that TENDRIL_PORTAL_URL points at your portal and not at something in between."
21422
+ });
21423
+ }
21424
+ const actual = sha256([bytes]);
21425
+ if (actual !== answer.contentSha256) {
21426
+ fail(opts, ExitCode.General, {
21427
+ error: `the downloaded recording set is not the one the portal named (got ${actual.slice(0, 12)}\u2026, expected ${answer.contentSha256.slice(0, 12)}\u2026)`,
21428
+ code: "pull-bytes-do-not-match",
21429
+ remediation: "Run this again. If it repeats, something between you and the portal is changing the response."
21430
+ });
21431
+ }
21432
+ let text;
21433
+ try {
21434
+ text = new TextDecoder("utf-8", { fatal: true }).decode(bytes);
21435
+ } catch {
21436
+ fail(opts, ExitCode.General, {
21437
+ error: "the downloaded recording set is not UTF-8 text",
21438
+ code: "pull-archive-unreadable",
21439
+ remediation: "Run this again; if it repeats, re-publish the component."
21440
+ });
21441
+ return;
21442
+ }
21443
+ const validated = validateRecordingArchive(text, { sha256, expectedSetHash: answer.setHash });
21444
+ if (!validated.ok) {
21445
+ fail(opts, ExitCode.General, {
21446
+ error: validated.refusal,
21447
+ code: "pull-archive-refused",
21448
+ remediation: "Nothing was written. Re-publish the component from the machine that holds its recordings, then pull again."
21449
+ });
21450
+ return;
21451
+ }
21452
+ const carried = validated.shape;
21453
+ const carriedKit = carried.figmaFile?.trim();
21454
+ if (carriedKit !== void 0 && carriedKit !== "" && answer.figmaFile !== "unidentified" && carriedKit !== answer.figmaFile.trim()) {
21455
+ fail(opts, ExitCode.General, {
21456
+ error: `the portal says this recording set belongs to design system ${answer.figmaFile}, and the set itself says ${carried.figmaFile}`,
21457
+ code: "pull-identity-disagrees",
21458
+ remediation: "Nothing was written. Re-publish the component from the machine that holds its recordings; if this repeats, the portal is answering about a different publication than the one it served."
21459
+ });
21460
+ return;
21461
+ }
21462
+ const slug = recordingSlug(component);
21463
+ const dest = path60.resolve(process.cwd(), opts.dest ?? path60.join("recordings", slug));
21464
+ if (existsSync50(dest)) {
21465
+ const existing = existingSetHash(dest);
21466
+ if (existing === validated.setHash) {
21467
+ emitData(
21468
+ opts,
21469
+ {
21470
+ component: carried.component,
21471
+ ...answer.componentName === carried.component ? {} : { libraryLabel: answer.componentName },
21472
+ figmaFile: carried.figmaFile ?? "unidentified",
21473
+ setHash: validated.setHash,
21474
+ dir: dest,
21475
+ files: validated.members.size,
21476
+ alreadyHere: true
21477
+ },
21478
+ () => {
21479
+ process.stdout.write(`already here \u2014 ${dest}
21480
+ `);
21481
+ process.stdout.write(` the same recording set (${validated.setHash.slice(0, 12)}\u2026), nothing to do
21482
+ `);
21483
+ }
21484
+ );
21485
+ return;
21486
+ }
21487
+ fail(opts, ExitCode.InputValidation, {
21488
+ error: existing === void 0 ? `${dest} already exists and is not a recording set \u2014 pulling would write into it` : `${dest} already holds a DIFFERENT recording set (${existing.slice(0, 12)}\u2026 here, ${validated.setHash.slice(0, 12)}\u2026 on the portal)`,
21489
+ code: "pull-destination-occupied",
21490
+ remediation: `Move it aside and pull again, or pull somewhere else with \`--dest <dir>\`. Nothing is overwritten \u2014 a recording set is the ground truth a verdict was measured against, and replacing one silently is how a verdict comes to describe bytes nobody has.`
21491
+ });
21492
+ }
21493
+ emitProgress(2, 3, "unpacking and re-deriving the set hash from disk");
21494
+ const staging = mkdtempSync4(path60.join(tmpdir(), "tendril-pull-"));
21495
+ let refusal;
21496
+ try {
21497
+ for (const [relPath, memberBytes] of validated.members) {
21498
+ const target = path60.join(staging, relPath);
21499
+ mkdirSync17(path60.dirname(target), { recursive: true });
21500
+ writeFileSync27(target, memberBytes);
21501
+ }
21502
+ const fromDisk = setHashFromDisk(staging);
21503
+ if (!fromDisk.ok) {
21504
+ refusal = {
21505
+ exit: ExitCode.General,
21506
+ error: `the recording set did not survive being written to this filesystem \u2014 ${fromDisk.refusal}`,
21507
+ code: "pull-set-changed-on-disk",
21508
+ remediation: "Nothing was written where you asked, and nothing was left behind. This means the filesystem altered the files on the way down \u2014 pull onto a different volume with `--dest <dir>`."
21509
+ };
21510
+ } else if (fromDisk.setHash !== validated.setHash) {
21511
+ refusal = {
21512
+ exit: ExitCode.General,
21513
+ error: `the recording set does not survive being written to this filesystem (on disk it derives ${fromDisk.setHash.slice(0, 12)}\u2026, the portal's is ${validated.setHash.slice(0, 12)}\u2026)`,
21514
+ code: "pull-set-changed-on-disk",
21515
+ remediation: "Nothing was written where you asked, and nothing was left behind. This usually means the filesystem renamed a file \u2014 pull onto a different volume with `--dest <dir>`."
21516
+ };
21517
+ } else {
21518
+ mkdirSync17(path60.dirname(dest), { recursive: true });
21519
+ try {
21520
+ renameSync2(staging, dest);
21521
+ } catch (error) {
21522
+ const code = error.code;
21523
+ if (code === "EEXIST" || code === "ENOTEMPTY") {
21524
+ refusal = {
21525
+ exit: ExitCode.InputValidation,
21526
+ error: `${dest} appeared while this was verifying \u2014 on a case-insensitive filesystem it may be a directory whose name differs only in capitals`,
21527
+ code: "pull-destination-collided",
21528
+ remediation: `Pull somewhere else with \`--dest <dir>\`, or move the existing directory aside.`
21529
+ };
21530
+ } else if (code === "EXDEV") {
21531
+ const beside = `${dest}.tendril-partial`;
21532
+ rmSync12(beside, { recursive: true, force: true });
21533
+ try {
21534
+ copyTree(staging, beside);
21535
+ renameSync2(beside, dest);
21536
+ } finally {
21537
+ rmSync12(beside, { recursive: true, force: true });
21538
+ }
21539
+ } else {
21540
+ throw error;
21541
+ }
21542
+ }
21543
+ }
21544
+ } finally {
21545
+ rmSync12(staging, { recursive: true, force: true });
21546
+ }
21547
+ if (refusal !== void 0) {
21548
+ fail(opts, refusal.exit, { error: refusal.error, code: refusal.code, remediation: refusal.remediation });
21549
+ }
21550
+ emitData(
21551
+ opts,
21552
+ {
21553
+ // THE CARRIED MANIFEST'S OWN NAME, not the portal's label — the
21554
+ // pinned identity rather than the steerable one. `libraryLabel`
21555
+ // carries the portal's word for it, named as what it is, and only
21556
+ // when the two differ (`publish --name` makes that legitimate).
21557
+ component: carried.component,
21558
+ ...answer.componentName === carried.component ? {} : { libraryLabel: answer.componentName },
21559
+ figmaFile: carried.figmaFile ?? "unidentified",
21560
+ publicationId: answer.publicationId,
21561
+ setHash: validated.setHash,
21562
+ dir: dest,
21563
+ files: validated.members.size,
21564
+ alreadyHere: false
21565
+ },
21566
+ () => {
21567
+ process.stdout.write(`pulled ${carried.component} \u2014 ${String(validated.members.size)} recorded files
21568
+ `);
21569
+ if (answer.componentName !== carried.component) {
21570
+ process.stdout.write(` your library calls it ${quoteArg(answer.componentName)}; the recording set names itself ${quoteArg(carried.component)}
21571
+ `);
21572
+ }
21573
+ process.stdout.write(` ${dest}
21574
+ `);
21575
+ process.stdout.write(` set ${validated.setHash.slice(0, 12)}\u2026 \u2014 the recordings this component's verdict was measured against
21576
+ `);
21577
+ if (recordingSlug(component) !== component && opts.dest === void 0) {
21578
+ process.stdout.write(` (${quoteArg(component)} lands in the directory ${slug})
21579
+ `);
21580
+ }
21581
+ process.stdout.write(` next: \`${tendrilCommand(`record status --set ${quoteArg(path60.relative(process.cwd(), dest) || dest)}`)}\`
21582
+ `);
21583
+ }
21584
+ );
21585
+ }
21586
+ function existingSetHash(dir) {
21587
+ const derived = setHashFromDisk(dir);
21588
+ return derived.ok ? derived.setHash : void 0;
21589
+ }
21590
+ function copyTree(from, to) {
21591
+ mkdirSync17(to, { recursive: true });
21592
+ for (const entry of readdirSync19(from, { withFileTypes: true })) {
21593
+ const src = path60.join(from, entry.name);
21594
+ const dst = path60.join(to, entry.name);
21595
+ if (entry.isDirectory()) copyTree(src, dst);
21596
+ else writeFileSync27(dst, readFileSync46(src));
21597
+ }
21598
+ }
21599
+ var MAX_ARCHIVE_BYTES, sha256;
21600
+ var init_pull = __esm({
21601
+ "packages/cli/src/commands/pull.ts"() {
21602
+ "use strict";
21603
+ init_src3();
21604
+ init_src4();
21605
+ init_invocation();
21606
+ init_output();
21607
+ init_publish_client();
21608
+ init_publish();
21609
+ MAX_ARCHIVE_BYTES = 2e7;
21610
+ sha256 = (chunks) => {
21611
+ const h = createHash15("sha256");
21612
+ for (const chunk of chunks) h.update(chunk);
21613
+ return h.digest("hex");
21614
+ };
21615
+ }
21616
+ });
21617
+
20826
21618
  // packages/cli/src/commands/generate-route.ts
20827
21619
  var generate_route_exports = {};
20828
21620
  __export(generate_route_exports, {
@@ -20847,17 +21639,17 @@ __export(generate_recorded_exports, {
20847
21639
  runGenerateRecorded: () => runGenerateRecorded
20848
21640
  });
20849
21641
  import { confirm as confirm3, isCancel as isCancel4 } from "@clack/prompts";
20850
- import { existsSync as existsSync49, readFileSync as readFileSync45 } from "node:fs";
20851
- import path59 from "node:path";
21642
+ import { existsSync as existsSync51, readFileSync as readFileSync47 } from "node:fs";
21643
+ import path61 from "node:path";
20852
21644
  async function runGenerateRecorded(opts) {
20853
21645
  const callerCwd = process.env["INIT_CWD"] ?? process.cwd();
20854
- const outDirAbs = path59.resolve(callerCwd, opts.out);
20855
- const recordedAsPath = path59.resolve(callerCwd, opts.recorded);
21646
+ const outDirAbs = path61.resolve(callerCwd, opts.out);
21647
+ const recordedAsPath = path61.resolve(callerCwd, opts.recorded);
20856
21648
  let task;
20857
21649
  let taskName;
20858
21650
  let authoredApi;
20859
21651
  let composition;
20860
- const isSet = existsSync49(path59.join(recordedAsPath, "recording-set.json"));
21652
+ const isSet = existsSync51(path61.join(recordedAsPath, "recording-set.json"));
20861
21653
  const registry = TASKS[opts.recorded];
20862
21654
  if (registry !== void 0 && !isSet) {
20863
21655
  task = registry;
@@ -20866,7 +21658,7 @@ async function runGenerateRecorded(opts) {
20866
21658
  try {
20867
21659
  const authored = authorTaskFromSet(recordedAsPath);
20868
21660
  task = authored.task;
20869
- taskName = path59.basename(recordedAsPath);
21661
+ taskName = path61.basename(recordedAsPath);
20870
21662
  authoredApi = authored.api;
20871
21663
  const roles = RolesSchema.safeParse(loadManifest(recordedAsPath).roles);
20872
21664
  if (roles.success) composition = roles.data;
@@ -20900,7 +21692,7 @@ async function runGenerateRecorded(opts) {
20900
21692
  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)`);
20901
21693
  }
20902
21694
  const missing = task.configs.filter(
20903
- (c) => !repEnvelopeExists(task.set, c.rep, "screenshot") || !repEnvelopeExists(task.set, c.rep, "metadata") || !existsSync49(path59.join(task.set, c.rep, "get_design_context.json"))
21695
+ (c) => !repEnvelopeExists(task.set, c.rep, "screenshot") || !repEnvelopeExists(task.set, c.rep, "metadata") || !existsSync51(path61.join(task.set, c.rep, "get_design_context.json"))
20904
21696
  );
20905
21697
  if (missing.length > 0) {
20906
21698
  fail(opts, ExitCode.RecordingIncomplete, {
@@ -20970,8 +21762,8 @@ async function runGenerateRecorded(opts) {
20970
21762
  ` : `${line}
20971
21763
  `);
20972
21764
  if (opts.dryRun) {
20973
- emitData(opts, { dryRun: true, task: taskName, model: modelId, consent, wouldWrite: path59.join(outDirAbs, taskName) }, () => {
20974
- process.stdout.write(`dry-run: nothing sent, nothing written (would write ${path59.join(outDirAbs, taskName)})
21765
+ emitData(opts, { dryRun: true, task: taskName, model: modelId, consent, wouldWrite: path61.join(outDirAbs, taskName) }, () => {
21766
+ process.stdout.write(`dry-run: nothing sent, nothing written (would write ${path61.join(outDirAbs, taskName)})
20975
21767
  `);
20976
21768
  });
20977
21769
  return;
@@ -20994,10 +21786,10 @@ async function runGenerateRecorded(opts) {
20994
21786
  });
20995
21787
  }
20996
21788
  }
20997
- const bundleDir = path59.join(outDirAbs, taskName);
20998
- if (existsSync49(path59.join(bundleDir, "component.json"))) {
21789
+ const bundleDir = path61.join(outDirAbs, taskName);
21790
+ if (existsSync51(path61.join(bundleDir, "component.json"))) {
20999
21791
  try {
21000
- const prior = readBundleManifest(readFileSync45(path59.join(bundleDir, "component.json"), "utf8")).manifest;
21792
+ const prior = readBundleManifest(readFileSync47(path61.join(bundleDir, "component.json"), "utf8")).manifest;
21001
21793
  if (prior !== void 0 && prior.provenance.recordingSet.hash !== recordingSetHash(task.set, task.configs)) {
21002
21794
  fail(opts, ExitCode.InputValidation, {
21003
21795
  error: `${bundleDir} already holds a bundle bound to recording set "${prior.provenance.recordingSet.path}" \u2014 generating here against a different set would silently rewrite its verification identity`,
@@ -22329,9 +23121,10 @@ function buildProgram() {
22329
23121
  ...local["revoke"] !== void 0 ? { revoke: local["revoke"] } : {}
22330
23122
  });
22331
23123
  });
22332
- 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) => {
23124
+ 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").option("--recordings <dir>", "the recording set to carry, when the bundle's stamped path is stale (a bundle moved between machines usually has one)").option("--no-recordings", "publish without carrying the recording set \u2014 the component still publishes, but no other machine can pull the recordings it was measured against").action(async (bundleDir, _o, cmd) => {
22333
23125
  const flags = globalFlags(cmd.parent);
22334
23126
  const local = cmd.opts();
23127
+ const argv = cmd.parent?.args ?? process.argv;
22335
23128
  const { runPublish: runPublish2 } = await Promise.resolve().then(() => (init_publish(), publish_exports));
22336
23129
  await runPublish2({
22337
23130
  ...flags,
@@ -22342,7 +23135,33 @@ function buildProgram() {
22342
23135
  ...local["approveWait"] !== void 0 ? { approveWait: local["approveWait"] } : {},
22343
23136
  // Parsed, never filtered: a bad value must refuse loudly in
22344
23137
  // runPublish, not silently become the unbounded wait.
22345
- ...local["waitWindow"] !== void 0 ? { waitWindowSeconds: Number.parseFloat(local["waitWindow"]) } : {}
23138
+ ...local["waitWindow"] !== void 0 ? { waitWindowSeconds: Number.parseFloat(local["waitWindow"]) } : {},
23139
+ // OPT-OUT WINS, whatever the order. Commander merges these into
23140
+ // one option with last-flag-wins, so `--no-recordings
23141
+ // --recordings X` CARRIED — a user who wrote the opt-out and
23142
+ // then a directory got the custody expansion anyway. For a flag
23143
+ // whose whole job is declining to upload the customer's design
23144
+ // structure, argument order is not a thing to be clever about.
23145
+ ...argv.includes("--no-recordings") ? { noRecordings: true } : {},
23146
+ // `--recordings <dir>` and `--no-recordings` are ONE commander
23147
+ // option, so the value is a string, `false`, or the default
23148
+ // `true`. Splitting it here keeps the two meanings apart in the
23149
+ // options object rather than making the command re-read
23150
+ // commander's convention.
23151
+ ...typeof local["recordings"] === "string" ? { recordings: local["recordings"] } : {},
23152
+ ...local["recordings"] === false ? { noRecordings: true } : {}
23153
+ });
23154
+ });
23155
+ program.command("pull").description("Bring a published component's RECORDING SET onto this machine \u2014 the design truth its verdict was measured against, so a machine with only portal access can carry the work on.").argument("<component>", "the component name, exactly as your library shows it").option("--figma-file <key>", "the design system, when one name spans several of yours").option("--dest <dir>", "where the set lands (default: ./recordings/<component>)").option("--to <url>", "the portal to pull from (or set TENDRIL_PORTAL_URL)").action(async (component, _o, cmd) => {
23156
+ const flags = globalFlags(cmd.parent);
23157
+ const local = cmd.opts();
23158
+ const { runPull: runPull2 } = await Promise.resolve().then(() => (init_pull(), pull_exports));
23159
+ await runPull2({
23160
+ ...flags,
23161
+ component,
23162
+ ...local["figmaFile"] !== void 0 ? { figmaFile: local["figmaFile"] } : {},
23163
+ ...local["dest"] !== void 0 ? { dest: local["dest"] } : {},
23164
+ ...local["to"] !== void 0 ? { to: local["to"] } : {}
22346
23165
  });
22347
23166
  });
22348
23167
  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) => {