@tendrilapp/cli 0.1.36 → 0.1.37

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.
Files changed (2) hide show
  1. package/dist/tendril.js +88 -17
  2. package/package.json +1 -1
package/dist/tendril.js CHANGED
@@ -1067,10 +1067,10 @@ function planSet(setDir, component, symbols, opts = {}) {
1067
1067
  const scopeUpgraded = opts.variantScope === "component-set" && existing.variantScope !== "component-set";
1068
1068
  if (opts.variantScope === "component-set") {
1069
1069
  raw["variantScope"] = "component-set";
1070
- const variants = symbols.map((s) => s.name).filter((n) => n.includes("="));
1071
- if (variants.length > 0 && (scopeUpgraded || variants.length > (existing.latticeNames ?? []).length)) {
1072
- raw["latticeNames"] = variants;
1073
- }
1070
+ }
1071
+ const variants = symbols.map((s) => s.name).filter((n) => n.includes("="));
1072
+ if (variants.length > 0 && (scopeUpgraded || variants.length > (existing.latticeNames ?? []).length)) {
1073
+ raw["latticeNames"] = variants;
1074
1074
  }
1075
1075
  if (!wantsNewDefaults || anythingRecorded) {
1076
1076
  if (opts.sample !== true) {
@@ -2759,7 +2759,7 @@ var init_tsc_check = __esm({
2759
2759
  // packages/verify/src/token-lint.ts
2760
2760
  import strictValue from "stylelint-declaration-strict-value";
2761
2761
  import stylelint from "stylelint";
2762
- async function runTokenLint(css, fileLabel = "generated.css", definedVars2, recordedMapEmpty = false) {
2762
+ async function runTokenLint(css, fileLabel = "generated.css", definedVars2, recordedMapEmpty = false, recordedTokensByValue2) {
2763
2763
  const result = await stylelint.lint({
2764
2764
  code: css,
2765
2765
  codeFilename: fileLabel,
@@ -2777,7 +2777,11 @@ async function runTokenLint(css, fileLabel = "generated.css", definedVars2, reco
2777
2777
  // noise in the highest-attention output. The finding stays (a
2778
2778
  // matching token SHOULD be referenced when one exists); the
2779
2779
  // message now carries the escape hatch.
2780
- message: w.rule === "scale-unlimited/declaration-strict-value" ? `${w.text} \u2014 ADVISORY: reference the design token if the kit provides one; if it does not, the literal is CORRECT \u2014 never invent a token name (the recorded token map captures one pose's variables and is known-partial)` : w.text
2780
+ message: w.rule === "scale-unlimited/declaration-strict-value" ? (() => {
2781
+ const literal = /"([^"]+)"/.exec(w.text)?.[1] ?? /\b(#[0-9a-fA-F]{3,8}|-?\d*\.?\d+(?:px|rem|em|%))\b/.exec(w.text)?.[1];
2782
+ const token = literal === void 0 ? void 0 : recordedTokensByValue2?.get(literal.trim().toLowerCase());
2783
+ return token !== void 0 ? `${w.text} \u2014 the recorded token map HAS a token for this value: "${token}". Reference it (and emit it in tokens.css) rather than the literal; this is not the known-partial case` : `${w.text} \u2014 ADVISORY: reference the design token if the kit provides one; if it does not, the literal is CORRECT \u2014 never invent a token name (the recorded token map captures one pose's variables and is known-partial)`;
2784
+ })() : w.text
2781
2785
  }))
2782
2786
  );
2783
2787
  const ownDefs = /* @__PURE__ */ new Set();
@@ -6183,6 +6187,28 @@ function recordedTokenMapState(setDir, reps) {
6183
6187
  }
6184
6188
  return recorded ? "empty" : "never-recorded";
6185
6189
  }
6190
+ function recordedTokensByValue(setDir, reps) {
6191
+ const byValue = /* @__PURE__ */ new Map();
6192
+ const readMap = (file) => {
6193
+ if (!existsSync14(file)) return void 0;
6194
+ try {
6195
+ const parsed = JSON.parse(envelopeFirstTextPart(JSON.parse(readFileSync11(file, "utf8"))) || "{}");
6196
+ return parsed !== null && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : {};
6197
+ } catch {
6198
+ return void 0;
6199
+ }
6200
+ };
6201
+ const absorb = (m) => {
6202
+ for (const [name, value] of Object.entries(m ?? {})) {
6203
+ if (typeof value !== "string") continue;
6204
+ const key = value.trim().toLowerCase();
6205
+ if (key !== "" && !byValue.has(key)) byValue.set(key, name);
6206
+ }
6207
+ };
6208
+ absorb(readMap(path19.join(setDir, "get_variable_defs.json")));
6209
+ for (const rep of reps) absorb(readMap(path19.join(setDir, rep, "get_variable_defs.json")));
6210
+ return byValue;
6211
+ }
6186
6212
  function scannable(css) {
6187
6213
  const blank = (m) => m.replace(/[^\n]/g, " ");
6188
6214
  return css.replace(/\/\*[^]*?\*\//g, blank).replace(/@font-face\s*{[^}]*}/g, blank);
@@ -6284,7 +6310,8 @@ async function checkBundleQuality(bundleDir, entry, set, fontManifest, motion, c
6284
6310
  }
6285
6311
  if (css !== "") {
6286
6312
  const mapState = set === void 0 ? void 0 : recordedTokenMapState(set.dir, set.reps);
6287
- for (const v of (await runTokenLint(css, "styles.css", definedVars(tokensCss), mapState !== void 0 && mapState !== "populated")).violations) {
6313
+ const tokensByValue = set === void 0 ? void 0 : recordedTokensByValue(set.dir, set.reps);
6314
+ for (const v of (await runTokenLint(css, "styles.css", definedVars(tokensCss), mapState !== void 0 && mapState !== "populated", tokensByValue)).violations) {
6288
6315
  findings.push({ kind: "token-lint", file: "styles.css", line: v.line, message: v.message });
6289
6316
  }
6290
6317
  if (mapState === "never-recorded") {
@@ -11885,7 +11912,7 @@ function motionBriefSection(setDir) {
11885
11912
  const truthBlock = truth.state === "recorded" ? `RECORDED MOTION TRUTH (verbatim from the set's get_motion_context recording \u2014 where it states concrete durations/curves for a transition, those WIN over the default tiers below for THAT transition; where it is ambiguous, fall back to the tiers and say so in your report \u2014 never invent motion the recording does not state):
11886
11913
  ${truth.text.length > RECORDED_QUOTE_CAP ? `${truth.text.slice(0, RECORDED_QUOTE_CAP)}
11887
11914
  [truncated at ${RECORDED_QUOTE_CAP} chars \u2014 the full recording is ${setDir}/get_motion_context.json]` : truth.text}
11888
- ` : truth.state === "recorded-no-motion" ? "RECORDED MOTION TRUTH: the set's get_motion_context recording came back with every motion field null \u2014 no keyframe tracks, no code snippets, no timeline duration. That is the INSTRUMENT reporting no keyframe/timeline animation on this node; it is NOT evidence that the design defines no transitions (the tool advertises keyframe/timeline data, and whether it reports prototype/Smart-Animate reactions is UNPROVEN \u2014 ADR-016 residual). The doctrine below is DEFAULT DOCTRINE, not recorded truth; report that recorded motion truth was UNAVAILABLE \u2014 never that the design has none.\n" : truth.state === "recorded-empty" ? "RECORDED MOTION TRUTH: the set's get_motion_context recording is EMPTY \u2014 the instrument returned no text at all. Treat motion as UNRECORDED, not as proof the design has none. Apply the default doctrine below.\n" : truth.state === "asked-not-recorded" ? "RECORDED MOTION TRUTH: none yet \u2014 the plan asked for get_motion_context but it is not recorded (or the recorded file is unusable; `record status` names which). The doctrine below is DEFAULT DOCTRINE, not recorded truth.\n" : "RECORDED MOTION TRUTH: never asked \u2014 this set predates the motion-capture obligation. The doctrine below is DEFAULT DOCTRINE, not recorded truth.\n";
11915
+ ` : truth.state === "recorded-no-motion" ? "RECORDED MOTION TRUTH: NONE CAPTURED. The set's get_motion_context recording came back with every motion field null \u2014 no keyframe tracks, no code snippets, no timeline duration. That is the INSTRUMENT reporting no keyframe/timeline animation on this node; it is NOT evidence that the design defines no transitions (the tool advertises keyframe/timeline data, and whether it reports prototype/Smart-Animate reactions is UNPROVEN \u2014 ADR-016 residual). The doctrine below is DEFAULT DOCTRINE, not recorded truth; report that recorded motion truth was UNAVAILABLE \u2014 never that the design has none.\n" : truth.state === "recorded-empty" ? "RECORDED MOTION TRUTH: NONE CAPTURED. The set's get_motion_context recording is EMPTY \u2014 the instrument returned no text at all. Treat motion as UNRECORDED, not as proof the design has none. Apply the default doctrine below.\n" : truth.state === "asked-not-recorded" ? "RECORDED MOTION TRUTH: none yet \u2014 the plan asked for get_motion_context but it is not recorded (or the recorded file is unusable; `record status` names which). The doctrine below is DEFAULT DOCTRINE, not recorded truth.\n" : "RECORDED MOTION TRUTH: never asked \u2014 this set predates the motion-capture obligation. The doctrine below is DEFAULT DOCTRINE, not recorded truth.\n";
11889
11916
  return `
11890
11917
 
11891
11918
  === MOTION (PRESCRIBED, NOT VERIFIED \u2014 ADR-016) ===
@@ -11948,6 +11975,7 @@ var init_src7 = __esm({
11948
11975
  // packages/cli/src/commands/record.ts
11949
11976
  var record_exports = {};
11950
11977
  __export(record_exports, {
11978
+ SELECTION_SCOPE_DISCLOSURE: () => SELECTION_SCOPE_DISCLOSURE,
11951
11979
  instanceLeads: () => instanceLeads,
11952
11980
  interactionDisclosure: () => interactionDisclosure,
11953
11981
  isFigmaAssetUrl: () => isFigmaAssetUrl,
@@ -12165,10 +12193,14 @@ function runRecordPlan(opts) {
12165
12193
  const poses = manifest.reps.length;
12166
12194
  const callLow = poses * FIGMA_CALLS_PER_REP;
12167
12195
  const callHigh = poses * FIGMA_CALLS_PER_REP_WORST;
12168
- const posesRemaining = sessionStatus(opts.setDir).reps.filter((r) => r.missing.length > 0).length;
12169
- const callsNeeded = posesRemaining * FIGMA_CALLS_PER_REP;
12170
- const callSentence = posesRemaining === poses ? `${poses} poses \xD7 ${FIGMA_CALLS_PER_REP} calls per pose = ${callsNeeded} Figma calls` : `${posesRemaining} poses still to record \xD7 ${FIGMA_CALLS_PER_REP} calls per pose = ${callsNeeded} Figma calls (${poses - posesRemaining} of ${poses} already on disk)`;
12171
- const arithmetic = `${callSentence} (up to ${posesRemaining * FIGMA_CALLS_PER_REP_WORST} if screenshots or assets need a second fetch)`;
12196
+ const planStatus = sessionStatus(opts.setDir);
12197
+ const posesRemaining = planStatus.reps.filter((r) => r.missing.length > 0).length;
12198
+ const setLevelRemaining = (planStatus.setTokenMapRecorded ? 0 : 1) + (planStatus.motion.asked && !planStatus.motion.recorded ? 1 : 0);
12199
+ const callsNeeded = posesRemaining * FIGMA_CALLS_PER_REP + setLevelRemaining;
12200
+ const setLevelClause = setLevelRemaining > 0 ? ` + ${setLevelRemaining} set-level call(s)` : "";
12201
+ const callSentence = posesRemaining === poses ? `${poses} poses \xD7 ${FIGMA_CALLS_PER_REP} calls per pose${setLevelClause} = ${callsNeeded} Figma calls` : `${posesRemaining} poses still to record \xD7 ${FIGMA_CALLS_PER_REP} calls per pose${setLevelClause} = ${callsNeeded} Figma calls (${poses - posesRemaining} of ${poses} already on disk)`;
12202
+ const planningSpent = variantScope === "selection" ? poses : 1;
12203
+ const arithmetic = `${callSentence} (up to ${posesRemaining * FIGMA_CALLS_PER_REP_WORST + setLevelRemaining} if screenshots or assets need a second fetch). ALREADY SPENT on planning, and counting against the same allowance: ~${planningSpent} get_metadata call(s)${variantScope === "selection" ? " \u2014 a selection-scoped plan pays one per node handed in, not one for the set" : ""}.`;
12172
12204
  const multiDay = callsNeeded > MULTI_DAY_CALL_THRESHOLD;
12173
12205
  const feasibilityCheck = {
12174
12206
  arithmetic,
@@ -12323,7 +12355,24 @@ function nextPayload(setDir) {
12323
12355
  const manifest = loadManifest(setDir);
12324
12356
  const frameNode = Object.keys(manifest.sourceFrames ?? {})[0] ?? manifest.reps[0]?.nodeId ?? "";
12325
12357
  const invalidNote = status.setTokenMapInvalid !== void 0 ? ` (the existing set-level file is UNUSABLE \u2014 ${status.setTokenMapInvalid} \u2014 re-record it)` : "";
12326
- return { slug: "__set__", nodeId: frameNode, tool: "get_variable_defs", note: `SET-LEVEL: call get_variable_defs on the component frame and ingest with --rep __set__.${invalidNote} ${ENVELOPE_HELP}`, progress };
12358
+ return {
12359
+ slug: "__set__",
12360
+ nodeId: frameNode,
12361
+ tool: "get_variable_defs",
12362
+ note: `SET-LEVEL (required \u2014 NOT a resume pointer; do this even under parallel recording): call get_variable_defs on the component frame and ingest with --rep __set__.${invalidNote} ${ENVELOPE_HELP}`,
12363
+ // SET-LEVEL REQUIRED, NOT A RESUME POINTER (run 25, the
12364
+ // SECOND field occurrence — this is a payload defect, not a
12365
+ // prompting accident). `next` overloads two different things:
12366
+ // a per-rep resume pointer, which parallel recorders are told
12367
+ // to IGNORE, and the set-level steps, which nobody may ignore
12368
+ // or the set never completes. Twice now a recorder followed
12369
+ // that instruction literally, refused this step, and reported
12370
+ // it as prompt injection. The scope field lets an ignore-next
12371
+ // instruction target only the pointer.
12372
+ scope: "set-level-required",
12373
+ ignorable: false,
12374
+ progress
12375
+ };
12327
12376
  }
12328
12377
  if (instruction === null && status.motion.asked && !status.motion.recorded) {
12329
12378
  const manifest = loadManifest(setDir);
@@ -12334,7 +12383,18 @@ function nextPayload(setDir) {
12334
12383
  slug: "__set__",
12335
12384
  nodeId: motionNode,
12336
12385
  tool: "get_motion_context",
12337
- note: `SET-LEVEL: call get_motion_context on ${nodeKind} and ingest with --rep __set__ \u2014 it returns Figma's KEYFRAME/TIMELINE data. A response whose motion fields are all null is a VALID recording (record it too): it means no keyframe animation was REPORTED for this node, which is not the same as the design defining no transitions (whether this tool reports prototype/Smart-Animate reactions is unproven \u2014 run 24).${invalidNote} ${ENVELOPE_HELP}`,
12386
+ note: `SET-LEVEL (required \u2014 NOT a resume pointer; do this even under parallel recording): call get_motion_context on ${nodeKind} and ingest with --rep __set__ \u2014 it returns Figma's KEYFRAME/TIMELINE data. A response whose motion fields are all null is a VALID recording (record it too): it means no keyframe animation was REPORTED for this node, which is not the same as the design defining no transitions (whether this tool reports prototype/Smart-Animate reactions is unproven \u2014 run 24).${invalidNote} ${ENVELOPE_HELP}`,
12387
+ // SET-LEVEL REQUIRED, NOT A RESUME POINTER (run 25, the
12388
+ // SECOND field occurrence — this is a payload defect, not a
12389
+ // prompting accident). `next` overloads two different things:
12390
+ // a per-rep resume pointer, which parallel recorders are told
12391
+ // to IGNORE, and the set-level steps, which nobody may ignore
12392
+ // or the set never completes. Twice now a recorder followed
12393
+ // that instruction literally, refused this step, and reported
12394
+ // it as prompt injection. The scope field lets an ignore-next
12395
+ // instruction target only the pointer.
12396
+ scope: "set-level-required",
12397
+ ignorable: false,
12338
12398
  progress
12339
12399
  };
12340
12400
  }
@@ -12848,7 +12908,7 @@ var init_record = __esm({
12848
12908
  FIGMA_CALLS_PER_REP = 3;
12849
12909
  FIGMA_CALLS_PER_REP_WORST = 4;
12850
12910
  MULTI_DAY_CALL_THRESHOLD = 600;
12851
- SELECTION_SCOPE_DISCLOSURE = "variantsFound counts the nodes you passed, NOT the component set: this metadata carried no enclosing set, so nothing here can tell whether the set holds more variants. Do NOT report the lattice as complete on this basis. To establish the real denominator, call get_metadata WITHOUT a nodeId (a page-level listing), find the component set these nodes sit under, and re-run plan against it \u2014 re-planning is free until the first envelope is ingested. If you proceed as-is, say plainly that coverage is unestablished.";
12911
+ SELECTION_SCOPE_DISCLOSURE = "variantsFound counts the nodes you passed, NOT the component set: this metadata carried no enclosing set, so nothing here can tell whether the set holds more variants. Do NOT report the lattice as complete on this basis. WORTH ONE ATTEMPT, NOT A HUNT: call get_metadata WITHOUT a nodeId (a page listing), and if a plausible enclosing component set is there, call get_metadata on that page and re-plan against the set \u2014 re-planning is free until the first envelope is ingested. STOP AFTER THAT. The set may not be reachable at all: it can sit outside any page you can list, and the component-listing fallback is gated behind an Org/Enterprise seat. That is a NORMAL outcome, not a failure \u2014 coverage stays UNKNOWN, every scored config is still verified, and the report says so honestly. Do not spend further calls chasing it; say plainly that coverage is unestablished and move on.";
12852
12912
  LIMIT_HINT = {
12853
12913
  status: "UNVERIFIED \u2014 recorded 2026-08-14 from Figma's published documentation and one field report; Figma changes these at will.",
12854
12914
  use: "Use ONLY if whoami states no allowance, and say it is unverified when you show it. The allowance whoami reports always wins.",
@@ -13434,7 +13494,16 @@ function latticeCoverage(setManifest, scoredConfigs) {
13434
13494
  latticeConfigs: null,
13435
13495
  // The count is still worth reporting — it is just not a
13436
13496
  // denominator, and the name has to stop implying that it is.
13437
- variantsPlanned: lattice,
13497
+ // A count SMALLER than what was scored is provably STALE (run 25:
13498
+ // a selection-scoped set topped up 1 → 16 reps kept its first
13499
+ // plan's count, and the integer sat two lines from
13500
+ // scoredConfigs: 16). Report the fact honestly instead of a
13501
+ // number that contradicts its own neighbour; the root fix
13502
+ // refreshes the manifest on re-plan, but manifests written
13503
+ // before it exist.
13504
+ ...lattice >= scoredConfigs ? { variantsPlanned: lattice } : {
13505
+ variantsPlannedUnreliable: `the manifest records ${lattice} planned variant name(s) but ${scoredConfigs} config(s) were scored \u2014 this count predates a top-up and is NOT a denominator; re-plan the set to refresh it`
13506
+ },
13438
13507
  latticeUnestablished,
13439
13508
  ...notRecorded
13440
13509
  };
@@ -13531,6 +13600,7 @@ function verdictCaveatsFor(input) {
13531
13600
  ...input.latticeBlock["latticeConfigs"] === null ? ["coverage-denominator-unknown"] : [],
13532
13601
  ...unrecorded || notRecorded ? ["poses-unrecorded"] : [],
13533
13602
  ...input.operability !== "verified" ? ["operability-unverified"] : [],
13603
+ ...(input.pixelOnlyInteractionPoses ?? 0) > 0 ? ["interaction-poses-unchecked"] : [],
13534
13604
  ...input.compositionUnavailable ? ["composition-not-checked"] : [],
13535
13605
  ...input.fontsSubstituted ? ["fonts-substituted"] : []
13536
13606
  ];
@@ -13836,7 +13906,8 @@ async function runVerify(opts) {
13836
13906
  latticeBlock,
13837
13907
  operability: coverage.operability,
13838
13908
  compositionUnavailable: "unavailable" in compositionBlock,
13839
- fontsSubstituted: substitutedFamilies.length > 0
13909
+ fontsSubstituted: substitutedFamilies.length > 0,
13910
+ pixelOnlyInteractionPoses: unmappedInteractionEvidence.length
13840
13911
  }),
13841
13912
  // Machine-readable cause (adversarial review): a cert-bar failure
13842
13913
  // with zero pixel/behavior/composition failures was only
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tendrilapp/cli",
3
- "version": "0.1.36",
3
+ "version": "0.1.37",
4
4
  "description": "Figma design systems → verified React components. CLI ruler + MCP server.",
5
5
  "license": "SEE LICENSE IN LICENSE",
6
6
  "type": "module",