rafters 0.0.84 → 0.0.85

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/index.js +201 -26
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -526,6 +526,10 @@ function generateThemeBlock(groups) {
526
526
  if (token.lineHeight) {
527
527
  lines.push(` --${token.name}--line-height: ${token.lineHeight};`);
528
528
  }
529
+ if (token.name.startsWith("line-height-")) {
530
+ const key = token.name.replace(/^line-height-/, "");
531
+ lines.push(` --leading-${key}: ${value};`);
532
+ }
529
533
  }
530
534
  lines.push("");
531
535
  }
@@ -1182,6 +1186,8 @@ function deriveCandidates(themeCSS) {
1182
1186
  candidates.add(`font-${name.slice(12)}`);
1183
1187
  } else if (name.startsWith("ease-")) {
1184
1188
  candidates.add(name);
1189
+ } else if (name.startsWith("leading-")) {
1190
+ candidates.add(name);
1185
1191
  } else if (name.startsWith("animate-")) {
1186
1192
  candidates.add(name);
1187
1193
  } else if (name.startsWith("text-") && !name.includes("--")) {
@@ -27811,17 +27817,26 @@ var TokenGraph = class {
27811
27817
  this.snapshot = /* @__PURE__ */ new Map();
27812
27818
  for (const [k, v] of this.nodes) this.snapshot.set(k, structuredClone(v));
27813
27819
  }
27820
+ // The caller (set/bind) takes a snapshot before invoking this. If any
27821
+ // dependent's transform throws partway through, we roll the whole graph
27822
+ // back to that snapshot and re-throw -- cascade is all-or-nothing, never
27823
+ // half-applied.
27814
27824
  cascadeFrom(changed) {
27815
- const dependents = this.collectDependents(changed);
27816
- if (dependents.size === 0) return;
27817
- const ordered = this.topoSort(dependents);
27818
- for (const name of ordered) {
27819
- const node = this.nodes.get(name);
27820
- if (!node || node.userOverride || !node.binding) continue;
27821
- const plugin = this.requirePlugin(node.binding.plugin);
27822
- const value = plugin.transform(node.binding.input, (n2) => this.get(n2));
27823
- plugin.outputSchema.parse(value);
27824
- this.nodes.set(name, { ...node, value });
27825
+ try {
27826
+ const dependents = this.collectDependents(changed);
27827
+ if (dependents.size === 0) return;
27828
+ const ordered = this.topoSort(dependents);
27829
+ for (const name of ordered) {
27830
+ const node = this.nodes.get(name);
27831
+ if (!node || node.userOverride || !node.binding) continue;
27832
+ const plugin = this.requirePlugin(node.binding.plugin);
27833
+ const value = plugin.transform(node.binding.input, (n2) => this.get(n2));
27834
+ plugin.outputSchema.parse(value);
27835
+ this.nodes.set(name, { ...node, value });
27836
+ }
27837
+ } catch (error47) {
27838
+ this.undo();
27839
+ throw error47;
27825
27840
  }
27826
27841
  }
27827
27842
  collectDependents(changed) {
@@ -27941,8 +27956,15 @@ function senseShadcnCss(css) {
27941
27956
 
27942
27957
  // ../design-tokens/src/outputs.ts
27943
27958
  import { existsSync, realpathSync } from "fs";
27944
- import { mkdir, writeFile } from "fs/promises";
27959
+ import { mkdir, unlink, writeFile } from "fs/promises";
27945
27960
  import { isAbsolute, join as join2, resolve } from "path";
27961
+ var KNOWN_OUTPUT_FILENAMES = [
27962
+ "rafters.css",
27963
+ "rafters.ts",
27964
+ "rafters.json",
27965
+ "rafters.standalone.css",
27966
+ "rafters.documentation.css"
27967
+ ];
27946
27968
  function fieldToPaths(field) {
27947
27969
  if (!field) return [];
27948
27970
  if (typeof field === "string") return [field];
@@ -28007,6 +28029,16 @@ async function regenerateOutputs(registry2, input, hooks2 = {}) {
28007
28029
  await writeFile(join2(outputDir2, "rafters.documentation.css"), doc);
28008
28030
  written.push("rafters.documentation.css");
28009
28031
  }
28032
+ const writtenSet = new Set(written);
28033
+ for (const filename of KNOWN_OUTPUT_FILENAMES) {
28034
+ if (writtenSet.has(filename)) continue;
28035
+ const filePath = join2(outputDir2, filename);
28036
+ if (!existsSync(filePath)) continue;
28037
+ try {
28038
+ await unlink(filePath);
28039
+ } catch {
28040
+ }
28041
+ }
28010
28042
  hooks2.notify?.();
28011
28043
  return written;
28012
28044
  }
@@ -28040,10 +28072,9 @@ var TokenRegistry = class {
28040
28072
  ...t.binding ? { binding: t.binding } : {}
28041
28073
  });
28042
28074
  }
28043
- for (const t of parsed) {
28044
- if (!t.binding) continue;
28045
- if (t.userOverride) continue;
28046
- this.graph.bind(t.name, t.binding.plugin, t.binding.input);
28075
+ const ordered = topoSortPass2(parsed, this.plugins);
28076
+ for (const entry of ordered) {
28077
+ this.graph.bind(entry.name, entry.binding.plugin, entry.binding.input);
28047
28078
  }
28048
28079
  }
28049
28080
  registerPlugin(plugin) {
@@ -28152,6 +28183,39 @@ function toUserOverrideField(override, baseValue) {
28152
28183
  if (override.kind) result.kind = override.kind;
28153
28184
  return result;
28154
28185
  }
28186
+ function topoSortPass2(parsed, plugins) {
28187
+ const entries = [];
28188
+ for (const t of parsed) {
28189
+ if (!t.binding || t.userOverride) continue;
28190
+ entries.push({ name: t.name, binding: t.binding });
28191
+ }
28192
+ const entryByName = /* @__PURE__ */ new Map();
28193
+ for (const e of entries) entryByName.set(e.name, e);
28194
+ const sorted = [];
28195
+ const visited = /* @__PURE__ */ new Set();
28196
+ const visiting = /* @__PURE__ */ new Set();
28197
+ const visit = (name, path) => {
28198
+ if (visited.has(name)) return;
28199
+ if (visiting.has(name)) {
28200
+ throw new CircularDependencyError([...path, name]);
28201
+ }
28202
+ visiting.add(name);
28203
+ const entry = entryByName.get(name);
28204
+ if (entry) {
28205
+ const plugin = plugins.get(entry.binding.plugin);
28206
+ if (plugin) {
28207
+ for (const dep of plugin.dependsOn(entry.binding.input)) {
28208
+ if (entryByName.has(dep)) visit(dep, [...path, name]);
28209
+ }
28210
+ }
28211
+ }
28212
+ visiting.delete(name);
28213
+ visited.add(name);
28214
+ if (entry) sorted.push(entry);
28215
+ };
28216
+ for (const e of entries) visit(e.name, []);
28217
+ return sorted;
28218
+ }
28155
28219
 
28156
28220
  // ../design-tokens/src/persistence.ts
28157
28221
  function loadRegistryFromDir(dir, plugins = []) {
@@ -31742,6 +31806,39 @@ var WORKSPACE_PARAM = {
31742
31806
  description: "Workspace name (directory basename). Required when the MCP session has multiple workspaces and none matches cwd. Call rafters_workspaces to list options."
31743
31807
  }
31744
31808
  };
31809
+ var GENERATE_LEADING_FILLER = [
31810
+ "give me a ",
31811
+ "give me an ",
31812
+ "give me ",
31813
+ "i need a ",
31814
+ "i need an ",
31815
+ "i need ",
31816
+ "i want a ",
31817
+ "i want an ",
31818
+ "i want ",
31819
+ "create a ",
31820
+ "create an ",
31821
+ "create ",
31822
+ "make me a ",
31823
+ "make me an ",
31824
+ "make a ",
31825
+ "make an ",
31826
+ "make ",
31827
+ "a ",
31828
+ "an ",
31829
+ "the "
31830
+ ];
31831
+ function normalizeGenerateQuery(intent) {
31832
+ const lowered = intent.trim().toLowerCase();
31833
+ let candidate = lowered;
31834
+ for (const filler of GENERATE_LEADING_FILLER) {
31835
+ if (candidate.startsWith(filler)) {
31836
+ candidate = candidate.slice(filler.length);
31837
+ break;
31838
+ }
31839
+ }
31840
+ return candidate.trim().replace(/[.,!?]+$/, "");
31841
+ }
31745
31842
  var TOOL_DEFINITIONS = [
31746
31843
  {
31747
31844
  name: "rafters_workspaces",
@@ -31799,12 +31896,12 @@ var TOOL_DEFINITIONS = [
31799
31896
  },
31800
31897
  {
31801
31898
  name: "rafters_generate",
31802
- description: "STUB (Issue E). Will produce a rafters-correct composition with visible placeholders for app-specific fields/actions/copy. Currently returns a structured not-implemented result.",
31899
+ description: 'Resolve a prose query to ONE registry component and return its verbatim, target-correct snippet with open content slots. A bare component name (e.g. "button", "give me a modal") resolves directly; a semantic question (e.g. "what do I use when it needs to be above everything") falls back to the intent door. Returns { component, target, snippet, slots } where snippet is the registry facet verbatim and each slot is left for the caller to fill. v1 serves single components only -- no parameterization, no composites, no writes.',
31803
31900
  inputSchema: {
31804
31901
  type: "object",
31805
31902
  properties: {
31806
31903
  ...WORKSPACE_PARAM,
31807
- intent: { type: "string", description: "What to generate" }
31904
+ intent: { type: "string", description: "A component name or a natural-language request" }
31808
31905
  },
31809
31906
  required: ["intent"]
31810
31907
  }
@@ -31862,6 +31959,11 @@ var TOOL_DEFINITIONS = [
31862
31959
  }
31863
31960
  ];
31864
31961
  var DEPRECATED_MSG = "use rafters_describe instead";
31962
+ var OverlayInstalledSchema = external_exports.object({
31963
+ components: external_exports.array(external_exports.string()).optional(),
31964
+ primitives: external_exports.array(external_exports.string()).optional(),
31965
+ composites: external_exports.array(external_exports.string()).optional()
31966
+ });
31865
31967
  var RaftersToolHandler = class {
31866
31968
  workspaces;
31867
31969
  defaultWorkspace;
@@ -32109,8 +32211,22 @@ var RaftersToolHandler = class {
32109
32211
  */
32110
32212
  async overlayContext(workspace) {
32111
32213
  const config2 = workspace ? await this.readConfig(workspace.root) : null;
32112
- const base = buildInstalledSet(config2 ?? {});
32113
- const components = /* @__PURE__ */ new Set([...base.components, ...config2?.installed?.primitives ?? []]);
32214
+ const parsedInstalled = OverlayInstalledSchema.safeParse(config2?.installed ?? {});
32215
+ if (!parsedInstalled.success) {
32216
+ const issue2 = parsedInstalled.error.issues[0];
32217
+ const field = issue2 && issue2.path.length > 0 ? `installed.${issue2.path.join(".")}` : "installed";
32218
+ const configPath2 = workspace ? getRaftersPaths(workspace.root).config : "config.rafters.json";
32219
+ return {
32220
+ configError: `malformed config at ${configPath2}: ${field} ${issue2?.message ?? "is invalid"}`
32221
+ };
32222
+ }
32223
+ const base = buildInstalledSet({
32224
+ installed: {
32225
+ components: parsedInstalled.data.components ?? [],
32226
+ composites: parsedInstalled.data.composites ?? []
32227
+ }
32228
+ });
32229
+ const components = /* @__PURE__ */ new Set([...base.components, ...parsedInstalled.data.primitives ?? []]);
32114
32230
  const parsedTarget = ComponentTargetSchema.safeParse(config2?.componentTarget);
32115
32231
  return {
32116
32232
  target: parsedTarget.success ? parsedTarget.data : void 0,
@@ -32139,23 +32255,79 @@ var RaftersToolHandler = class {
32139
32255
  return this.jsonResult(matchIntent(address, graph));
32140
32256
  }
32141
32257
  const ctx = await this.overlayContext(resolved);
32258
+ if ("configError" in ctx) {
32259
+ return this.errorResult(ctx.configError);
32260
+ }
32142
32261
  return this.jsonResult(describeWithOverlay(address, graph, ctx));
32143
32262
  }
32144
32263
  /**
32145
- * The `rafters_generate` stub (Issue E). Registered now so the surface shape
32146
- * (name, input schema) stabilizes before the generator lands; it never
32147
- * attempts real composition. Deliberately does NOT build the graph: nothing
32148
- * here reads it, and a build failure must not turn an honest stub into an
32149
- * error result.
32264
+ * The `rafters_generate` handler. A prose query resolves -- first through a
32265
+ * light, deterministic direct-name lookup (tier b), then through the existing
32266
+ * intent door (tier c) -- to a single registry component, and the tool returns
32267
+ * that component's authoritative, target-correct snippet verbatim with its
32268
+ * content slots marked open for the caller. Both tiers re-resolve the winning
32269
+ * id through `describe(id, graph, ctx.target)`, so the response always carries
32270
+ * the workspace target's facet, never the intent door's untargeted fields.
32271
+ * v1 serves components only: composites, parameterization, composition,
32272
+ * writes, and MCP-side validation are all out of scope.
32150
32273
  */
32151
32274
  async handleGenerate(intent, workspaceName) {
32152
32275
  const resolved = this.resolve(workspaceName);
32153
32276
  if (workspaceName && !resolved) {
32154
32277
  return this.workspaceRequiredError();
32155
32278
  }
32279
+ let graph;
32280
+ try {
32281
+ graph = await this.ensureGraph(resolved);
32282
+ } catch (err) {
32283
+ const message = err instanceof Error ? err.message : "Unknown error";
32284
+ return this.errorResult(`failed to build intel graph: ${message}`);
32285
+ }
32286
+ const ctx = await this.overlayContext(resolved);
32287
+ if ("configError" in ctx) {
32288
+ return this.errorResult(ctx.configError);
32289
+ }
32290
+ const candidate = normalizeGenerateQuery(intent);
32291
+ const directHit = graph.nodes.get(candidate);
32292
+ let nodeId;
32293
+ if (directHit && directHit.kind === "component") {
32294
+ nodeId = candidate;
32295
+ } else {
32296
+ const match = matchIntent(intent, graph);
32297
+ if ("use" in match) nodeId = match.use.id;
32298
+ }
32299
+ if (nodeId === void 0) {
32300
+ return this.errorResult("no registry component matches this query");
32301
+ }
32302
+ const result = describe(nodeId, graph, ctx.target);
32303
+ if (result === null || Array.isArray(result) || !("children" in result)) {
32304
+ return this.errorResult("no registry component matches this query");
32305
+ }
32306
+ const node = result;
32307
+ if (node.kind !== "component") {
32308
+ return this.errorResult(
32309
+ `${node.id} resolved, but is a composite -- rafters_generate v1 returns single components only`
32310
+ );
32311
+ }
32312
+ if (ctx.target === void 0) {
32313
+ return this.errorResult(
32314
+ `${node.id} resolved, but no componentTarget is configured for this workspace -- generate cannot choose a snippet without one`
32315
+ );
32316
+ }
32317
+ if (node.snippet === void 0) {
32318
+ return this.errorResult(
32319
+ `${node.id} resolved, but has no ${ctx.target} facet -- nothing to generate for this target`
32320
+ );
32321
+ }
32156
32322
  return this.jsonResult({
32157
- implemented: false,
32158
- note: `generate is a spike -- see issue E; not yet implemented (requested: ${intent})`
32323
+ component: node.id,
32324
+ target: ctx.target,
32325
+ snippet: node.snippet,
32326
+ slots: (node.slots ?? []).map((slot) => ({
32327
+ slot,
32328
+ ownedBy: "caller",
32329
+ status: "open"
32330
+ }))
32159
32331
  });
32160
32332
  }
32161
32333
  /**
@@ -32290,6 +32462,9 @@ var RaftersToolHandler = class {
32290
32462
  });
32291
32463
  }
32292
32464
  const ctx = await this.overlayContext(resolved);
32465
+ if ("configError" in ctx) {
32466
+ return this.errorResult(ctx.configError, { deprecated: DEPRECATED_MSG });
32467
+ }
32293
32468
  const out = describeWithOverlay(id, graph, ctx);
32294
32469
  return this.jsonResult(this.withDeprecated(out));
32295
32470
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "rafters",
3
- "version": "0.0.84",
3
+ "version": "0.0.85",
4
4
  "description": "Design Intelligence CLI. Scaffold tokens, import existing shadcn/Tailwind v4 sources, add components, and serve an MCP server so AI agents read decisions instead of guessing.",
5
5
  "homepage": "https://rafters.studio",
6
6
  "license": "MIT",