spec-layer 0.9.0 → 0.10.0

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 (3) hide show
  1. package/README.md +67 -16
  2. package/dist/cli.js +2556 -210
  3. package/package.json +1 -1
package/dist/cli.js CHANGED
@@ -262,7 +262,7 @@ var require_sha256 = __commonJS({
262
262
  }
263
263
  notString = true;
264
264
  }
265
- var code2, index = 0, i, length = message.length, blocks2 = this.blocks;
265
+ var code3, index = 0, i, length = message.length, blocks2 = this.blocks;
266
266
  while (index < length) {
267
267
  if (this.hashed) {
268
268
  this.hashed = false;
@@ -275,22 +275,22 @@ var require_sha256 = __commonJS({
275
275
  }
276
276
  } else {
277
277
  for (i = this.start; index < length && i < 64; ++index) {
278
- code2 = message.charCodeAt(index);
279
- if (code2 < 128) {
280
- blocks2[i >>> 2] |= code2 << SHIFT[i++ & 3];
281
- } else if (code2 < 2048) {
282
- blocks2[i >>> 2] |= (192 | code2 >>> 6) << SHIFT[i++ & 3];
283
- blocks2[i >>> 2] |= (128 | code2 & 63) << SHIFT[i++ & 3];
284
- } else if (code2 < 55296 || code2 >= 57344) {
285
- blocks2[i >>> 2] |= (224 | code2 >>> 12) << SHIFT[i++ & 3];
286
- blocks2[i >>> 2] |= (128 | code2 >>> 6 & 63) << SHIFT[i++ & 3];
287
- blocks2[i >>> 2] |= (128 | code2 & 63) << SHIFT[i++ & 3];
278
+ code3 = message.charCodeAt(index);
279
+ if (code3 < 128) {
280
+ blocks2[i >>> 2] |= code3 << SHIFT[i++ & 3];
281
+ } else if (code3 < 2048) {
282
+ blocks2[i >>> 2] |= (192 | code3 >>> 6) << SHIFT[i++ & 3];
283
+ blocks2[i >>> 2] |= (128 | code3 & 63) << SHIFT[i++ & 3];
284
+ } else if (code3 < 55296 || code3 >= 57344) {
285
+ blocks2[i >>> 2] |= (224 | code3 >>> 12) << SHIFT[i++ & 3];
286
+ blocks2[i >>> 2] |= (128 | code3 >>> 6 & 63) << SHIFT[i++ & 3];
287
+ blocks2[i >>> 2] |= (128 | code3 & 63) << SHIFT[i++ & 3];
288
288
  } else {
289
- code2 = 65536 + ((code2 & 1023) << 10 | message.charCodeAt(++index) & 1023);
290
- blocks2[i >>> 2] |= (240 | code2 >>> 18) << SHIFT[i++ & 3];
291
- blocks2[i >>> 2] |= (128 | code2 >>> 12 & 63) << SHIFT[i++ & 3];
292
- blocks2[i >>> 2] |= (128 | code2 >>> 6 & 63) << SHIFT[i++ & 3];
293
- blocks2[i >>> 2] |= (128 | code2 & 63) << SHIFT[i++ & 3];
289
+ code3 = 65536 + ((code3 & 1023) << 10 | message.charCodeAt(++index) & 1023);
290
+ blocks2[i >>> 2] |= (240 | code3 >>> 18) << SHIFT[i++ & 3];
291
+ blocks2[i >>> 2] |= (128 | code3 >>> 12 & 63) << SHIFT[i++ & 3];
292
+ blocks2[i >>> 2] |= (128 | code3 >>> 6 & 63) << SHIFT[i++ & 3];
293
+ blocks2[i >>> 2] |= (128 | code3 & 63) << SHIFT[i++ & 3];
294
294
  }
295
295
  }
296
296
  }
@@ -472,24 +472,24 @@ var require_sha256 = __commonJS({
472
472
  function HmacSha256(key, is224, sharedMemory) {
473
473
  var i, type = typeof key;
474
474
  if (type === "string") {
475
- var bytes = [], length = key.length, index = 0, code2;
475
+ var bytes = [], length = key.length, index = 0, code3;
476
476
  for (i = 0; i < length; ++i) {
477
- code2 = key.charCodeAt(i);
478
- if (code2 < 128) {
479
- bytes[index++] = code2;
480
- } else if (code2 < 2048) {
481
- bytes[index++] = 192 | code2 >>> 6;
482
- bytes[index++] = 128 | code2 & 63;
483
- } else if (code2 < 55296 || code2 >= 57344) {
484
- bytes[index++] = 224 | code2 >>> 12;
485
- bytes[index++] = 128 | code2 >>> 6 & 63;
486
- bytes[index++] = 128 | code2 & 63;
477
+ code3 = key.charCodeAt(i);
478
+ if (code3 < 128) {
479
+ bytes[index++] = code3;
480
+ } else if (code3 < 2048) {
481
+ bytes[index++] = 192 | code3 >>> 6;
482
+ bytes[index++] = 128 | code3 & 63;
483
+ } else if (code3 < 55296 || code3 >= 57344) {
484
+ bytes[index++] = 224 | code3 >>> 12;
485
+ bytes[index++] = 128 | code3 >>> 6 & 63;
486
+ bytes[index++] = 128 | code3 & 63;
487
487
  } else {
488
- code2 = 65536 + ((code2 & 1023) << 10 | key.charCodeAt(++i) & 1023);
489
- bytes[index++] = 240 | code2 >>> 18;
490
- bytes[index++] = 128 | code2 >>> 12 & 63;
491
- bytes[index++] = 128 | code2 >>> 6 & 63;
492
- bytes[index++] = 128 | code2 & 63;
488
+ code3 = 65536 + ((code3 & 1023) << 10 | key.charCodeAt(++i) & 1023);
489
+ bytes[index++] = 240 | code3 >>> 18;
490
+ bytes[index++] = 128 | code3 >>> 12 & 63;
491
+ bytes[index++] = 128 | code3 >>> 6 & 63;
492
+ bytes[index++] = 128 | code3 & 63;
493
493
  }
494
494
  }
495
495
  key = bytes;
@@ -562,6 +562,609 @@ import { parseArgs } from "node:util";
562
562
  import { existsSync as existsSync8, readFileSync as readFileSync9 } from "node:fs";
563
563
  import { join as join8, resolve as resolve5 } from "node:path";
564
564
 
565
+ // ../extractor/src/naming.ts
566
+ function parseVariantName(name) {
567
+ const out = {};
568
+ for (const segment of name.split(",")) {
569
+ const [axis, ...rest] = segment.split("=");
570
+ if (!rest.length) return null;
571
+ out[axis.trim()] = rest.join("=").trim();
572
+ }
573
+ return out;
574
+ }
575
+ var WHITESPACE = /\s/;
576
+ function cleanPartName(name) {
577
+ let end = name.length;
578
+ while (end > 0 && WHITESPACE.test(name[end - 1])) end--;
579
+ const afterHashes = end;
580
+ while (end > 0 && name[end - 1] === "#") end--;
581
+ return afterHashes === end ? name.trim() : name.slice(0, end).trim();
582
+ }
583
+ var cleanPropName = (raw) => raw.split("#")[0];
584
+ function siblingPartNames(children) {
585
+ const counts = /* @__PURE__ */ new Map();
586
+ const out = /* @__PURE__ */ new Map();
587
+ for (const child of children) {
588
+ const base = cleanPartName(child.name);
589
+ const n = (counts.get(base) ?? 0) + 1;
590
+ counts.set(base, n);
591
+ out.set(child, n === 1 ? base : `${base} (${n})`);
592
+ }
593
+ return out;
594
+ }
595
+ function joinPath(parentPath, part) {
596
+ const escaped = part.replace(/\//g, "\\/");
597
+ return parentPath ? `${parentPath}/${escaped}` : escaped;
598
+ }
599
+ function walkParts(root, rootName, visit, skipInvisible = false, parentPath = "") {
600
+ if (typeof skipInvisible === "function" ? skipInvisible(root) : skipInvisible && root.visible === false) return;
601
+ const path = joinPath(parentPath, rootName);
602
+ visit(root, rootName, path);
603
+ const kids = root.children ?? [];
604
+ const names = siblingPartNames(kids);
605
+ for (const child of kids) {
606
+ walkParts(child, names.get(child), visit, skipInvisible, path);
607
+ }
608
+ }
609
+
610
+ // ../extractor/src/anatomy.ts
611
+ var MAX_DEPTH = 3;
612
+ function defaultVariant(root) {
613
+ if (root.type !== "COMPONENT_SET" || !root.children?.length) return root;
614
+ const variants = root.children.filter((c) => c.type === "COMPONENT");
615
+ if (!variants.length) return root.children[0];
616
+ const declared = Object.entries(root.propertyDefinitions ?? {}).filter(([, d]) => d.type === "VARIANT" && typeof d.defaultValue === "string").map(([axis, d]) => [axis, d.defaultValue]);
617
+ if (declared.length) {
618
+ const match = variants.find((v) => {
619
+ const combo = parseVariantName(v.name);
620
+ return combo != null && declared.every(([axis, value]) => combo[axis] === value);
621
+ });
622
+ if (match) return match;
623
+ }
624
+ return variants[0];
625
+ }
626
+ function booleanPropertyKeys(root) {
627
+ return new Set(
628
+ Object.entries(root.propertyDefinitions ?? {}).filter(([, def]) => def.type === "BOOLEAN").map(([key]) => key)
629
+ );
630
+ }
631
+ function hiddenBoundTo(node, booleans) {
632
+ if (node.visible) return void 0;
633
+ if (node.visibleProperty === void 0 || !booleans.has(node.visibleProperty)) return void 0;
634
+ return cleanPropName(node.visibleProperty);
635
+ }
636
+ function isDocumentable(node, booleans) {
637
+ return node.visible || hiddenBoundTo(node, booleans) !== void 0;
638
+ }
639
+ function hiddenPartRules(root) {
640
+ const booleans = booleanPropertyKeys(root);
641
+ return {
642
+ prune: (node) => !isDocumentable(node, booleans),
643
+ shownBy: (node) => hiddenBoundTo(node, booleans)
644
+ };
645
+ }
646
+ function extractAnatomy(root) {
647
+ const parts = [];
648
+ const related = /* @__PURE__ */ new Set();
649
+ const isInSet = root.type === "COMPONENT_SET";
650
+ const def = defaultVariant(root);
651
+ const rootPath = isInSet ? "Container" : cleanPartName(def.name);
652
+ const booleans = booleanPropertyKeys(root);
653
+ let siblingSet = def.children ?? [];
654
+ let children = siblingSet.filter((c) => c.visible);
655
+ let parentPath = rootPath;
656
+ const skippedHidden = [];
657
+ while (children.length === 1 && (children[0].type === "FRAME" || children[0].type === "GROUP") && (children[0].children ?? []).filter((c) => c.visible).length > 0) {
658
+ for (const node of siblingSet) {
659
+ if (node !== children[0] && hiddenBoundTo(node, booleans) !== void 0) {
660
+ skippedHidden.push({ node, parentPath });
661
+ }
662
+ }
663
+ const names = siblingPartNames(siblingSet);
664
+ parentPath = joinPath(parentPath, names.get(children[0]));
665
+ siblingSet = children[0].children ?? [];
666
+ children = siblingSet.filter((c) => c.visible);
667
+ }
668
+ const namingOrder = [
669
+ ...children,
670
+ ...siblingSet.filter((c) => hiddenBoundTo(c, booleans) !== void 0),
671
+ ...skippedHidden.map((e) => e.node)
672
+ ];
673
+ const topNames = siblingPartNames(namingOrder);
674
+ const topLevel = [
675
+ ...siblingSet.filter((c) => isDocumentable(c, booleans)).map((node) => ({ node, parentPath })),
676
+ ...skippedHidden
677
+ ];
678
+ function pushPart(child, depth, childParentPath, name, inheritedShownBy) {
679
+ const shownBy = hiddenBoundTo(child, booleans) ?? inheritedShownBy;
680
+ const nested = child.type === "INSTANCE";
681
+ if (nested && child.mainComponent && shownBy === void 0) related.add(child.mainComponent.name);
682
+ const path = joinPath(childParentPath, name);
683
+ parts.push({
684
+ id: child.id,
685
+ name,
686
+ type: child.type,
687
+ nested,
688
+ depth,
689
+ path,
690
+ ...nested && child.mainComponent ? { component: child.mainComponent.name } : {},
691
+ ...child.text ? { text: child.text } : {},
692
+ ...shownBy !== void 0 ? { hiddenByDefault: true, shownBy } : {}
693
+ });
694
+ if (!nested && depth + 1 < MAX_DEPTH && child.children?.length) {
695
+ addParts(child.children, depth + 1, path, shownBy);
696
+ }
697
+ }
698
+ function addParts(nodes, depth, nodesParentPath, inheritedShownBy) {
699
+ const names = siblingPartNames(nodes);
700
+ for (const child of nodes) {
701
+ if (!isDocumentable(child, booleans)) continue;
702
+ pushPart(child, depth, nodesParentPath, names.get(child), inheritedShownBy);
703
+ }
704
+ }
705
+ for (const entry2 of topLevel) {
706
+ pushPart(entry2.node, 0, entry2.parentPath, topNames.get(entry2.node), void 0);
707
+ }
708
+ return { parts, related: [...related], componentId: def.id };
709
+ }
710
+
711
+ // ../extractor/src/effects.ts
712
+ function extractNodeEffects(root) {
713
+ const out = [];
714
+ const def = defaultVariant(root);
715
+ walkParts(
716
+ def,
717
+ root.type === "COMPONENT_SET" ? "Container" : cleanPartName(def.name),
718
+ (n, part, path) => {
719
+ if (n.effects && n.effects.length > 0) out.push({ part, path, effects: n.effects });
720
+ }
721
+ );
722
+ return out;
723
+ }
724
+
725
+ // ../extractor/src/tokens.ts
726
+ function variantAxisModel(root) {
727
+ const isInSet = root.type === "COMPONENT_SET";
728
+ const variants = isInSet ? (root.children ?? []).filter((c) => c.type === "COMPONENT") : [root];
729
+ if (!isInSet) return { variants, combos: variants.map(() => ({})) };
730
+ const parsed = variants.map((v) => parseVariantName(v.name));
731
+ const first = parsed[0];
732
+ const consistent = first != null && parsed.every(
733
+ (p) => p !== null && Object.keys(p).length === Object.keys(first).length && Object.keys(first).every((k) => k in p)
734
+ );
735
+ return {
736
+ variants,
737
+ combos: consistent ? parsed : variants.map((v) => ({ Variant: v.name }))
738
+ };
739
+ }
740
+ function formatConditions(conditions) {
741
+ const entries = Object.entries(conditions);
742
+ if (!entries.length) return "\u2014";
743
+ return entries.map(([axis, values]) => `${axis}=${values.join(" \xB7 ")}`).join(", ");
744
+ }
745
+ var SIMPLE_PROPERTY_MAP = {
746
+ fills: "fill",
747
+ strokes: "border",
748
+ cornerRadius: "border-radius",
749
+ itemSpacing: "gap",
750
+ fontSize: "font-size",
751
+ fontFamily: "font-family",
752
+ fontWeight: "font-weight",
753
+ fontStyle: "font-style",
754
+ lineHeight: "line-height",
755
+ letterSpacing: "letter-spacing",
756
+ strokeWeight: "border-width",
757
+ strokeTopWeight: "border-top-width",
758
+ strokeRightWeight: "border-right-width",
759
+ strokeBottomWeight: "border-bottom-width",
760
+ strokeLeftWeight: "border-left-width",
761
+ maxWidth: "max-width",
762
+ minWidth: "min-width",
763
+ maxHeight: "max-height",
764
+ minHeight: "min-height"
765
+ };
766
+ var RADIUS_PROPS = ["topLeftRadius", "topRightRadius", "bottomLeftRadius", "bottomRightRadius"];
767
+ var RADIUS_INDIVIDUAL_MAP = {
768
+ topLeftRadius: "border-top-left-radius",
769
+ topRightRadius: "border-top-right-radius",
770
+ bottomLeftRadius: "border-bottom-left-radius",
771
+ bottomRightRadius: "border-bottom-right-radius"
772
+ };
773
+ var RADIUS_BINDINGS = /* @__PURE__ */ new Set([
774
+ "cornerRadius",
775
+ "topLeftRadius",
776
+ "topRightRadius",
777
+ "bottomLeftRadius",
778
+ "bottomRightRadius"
779
+ ]);
780
+ var PADDING_RAW_PROPS = /* @__PURE__ */ new Set([
781
+ "paddingTop",
782
+ "paddingRight",
783
+ "paddingBottom",
784
+ "paddingLeft",
785
+ "verticalPadding",
786
+ "horizontalPadding"
787
+ ]);
788
+ var TYPOGRAPHY_SUBPROPS = /* @__PURE__ */ new Set(["fontSize", "fontFamily", "fontWeight", "fontStyle", "lineHeight", "letterSpacing"]);
789
+ var simpleProperty = (raw) => SIMPLE_PROPERTY_MAP[raw] ?? raw;
790
+ function paddingSides(top, right, bottom, left, key = String) {
791
+ const single = (xs) => xs.length === 1 ? xs[0] : null;
792
+ const sameKey = (a, b) => {
793
+ const x = single(a), y = single(b);
794
+ return x !== null && y !== null && key(x) === key(y);
795
+ };
796
+ const sides = [top, right, bottom, left];
797
+ const out = [];
798
+ if (sides.every((s) => single(s) !== null) && new Set(sides.map((s) => key(single(s)))).size === 1) {
799
+ out.push({ property: "padding", value: single(top) });
800
+ return out;
801
+ }
802
+ if (left.length && sameKey(left, right)) {
803
+ out.push({ property: "padding-x", value: single(left) });
804
+ } else {
805
+ for (const t of left) out.push({ property: "padding-left", value: t });
806
+ for (const t of right) out.push({ property: "padding-right", value: t });
807
+ }
808
+ if (top.length && sameKey(top, bottom)) {
809
+ out.push({ property: "padding-y", value: single(top) });
810
+ } else {
811
+ for (const t of top) out.push({ property: "padding-top", value: t });
812
+ for (const t of bottom) out.push({ property: "padding-bottom", value: t });
813
+ }
814
+ return out;
815
+ }
816
+ function normalizeBindings(raw) {
817
+ const byProp = /* @__PURE__ */ new Map();
818
+ for (const b of raw) {
819
+ const refs = byProp.get(b.property) ?? [];
820
+ if (!refs.some((r) => r.kind === b.kind && r.id === b.id)) refs.push(b);
821
+ byProp.set(b.property, refs);
822
+ }
823
+ const out = [];
824
+ const emit = (property, ref) => {
825
+ if (out.some((o) => o.property === property && o.kind === ref.kind && o.id === ref.id)) return;
826
+ out.push({ ...ref, property });
827
+ };
828
+ const radii = RADIUS_PROPS.filter((p) => byProp.has(p));
829
+ const radiusRefs = radii.flatMap((p) => byProp.get(p));
830
+ const distinctRadius = new Set(radiusRefs.map((r) => `${r.kind}|${r.id}`));
831
+ if (radii.length === RADIUS_PROPS.length && distinctRadius.size === 1) {
832
+ emit("border-radius", radiusRefs[0]);
833
+ } else {
834
+ for (const p of radii) for (const r of byProp.get(p)) emit(RADIUS_INDIVIDUAL_MAP[p], r);
835
+ }
836
+ const sideRefs = (...props) => props.flatMap((p) => byProp.get(p) ?? []);
837
+ for (const { property, value } of paddingSides(
838
+ sideRefs("paddingTop", "verticalPadding"),
839
+ sideRefs("paddingRight", "horizontalPadding"),
840
+ sideRefs("paddingBottom", "verticalPadding"),
841
+ sideRefs("paddingLeft", "horizontalPadding"),
842
+ (r) => `${r.kind}|${r.id}`
843
+ )) {
844
+ emit(property, value);
845
+ }
846
+ const hasTypography = byProp.has("typography");
847
+ for (const [prop, refs] of byProp) {
848
+ if (RADIUS_PROPS.includes(prop) || PADDING_RAW_PROPS.has(prop)) continue;
849
+ if (hasTypography && TYPOGRAPHY_SUBPROPS.has(prop)) continue;
850
+ const mapped = simpleProperty(prop);
851
+ for (const r of refs) emit(mapped, r);
852
+ }
853
+ return out;
854
+ }
855
+ var refKey = (r) => `${r.kind}|${r.id}`;
856
+ var ABSENT_KEY = "absent";
857
+ function extractTokens(root, model) {
858
+ const isInSet = root.type === "COMPONENT_SET";
859
+ const { variants, combos } = model ?? variantAxisModel(root);
860
+ if (!variants.length) return [];
861
+ const axisOrder = [];
862
+ const observedValues = /* @__PURE__ */ new Map();
863
+ for (const combo of combos) {
864
+ for (const [axis, value] of Object.entries(combo)) {
865
+ let vals = observedValues.get(axis);
866
+ if (!vals) {
867
+ axisOrder.push(axis);
868
+ observedValues.set(axis, vals = []);
869
+ }
870
+ if (!vals.includes(value)) vals.push(value);
871
+ }
872
+ }
873
+ const axisValues = /* @__PURE__ */ new Map();
874
+ for (const axis of axisOrder) {
875
+ const obs = observedValues.get(axis);
876
+ const declared = root.propertyDefinitions?.[axis]?.variantOptions;
877
+ axisValues.set(
878
+ axis,
879
+ declared ? [...declared.filter((v) => obs.includes(v)), ...obs.filter((v) => !declared.includes(v))] : obs
880
+ );
881
+ }
882
+ const gridKey = (path, property) => JSON.stringify([path, property]);
883
+ const cellsByPathProp = /* @__PURE__ */ new Map();
884
+ const pathOrder = [];
885
+ const propOrder = /* @__PURE__ */ new Map();
886
+ const partByPath = /* @__PURE__ */ new Map();
887
+ const shownByPath = /* @__PURE__ */ new Map();
888
+ const hidden = hiddenPartRules(root);
889
+ const refsByKey = /* @__PURE__ */ new Map();
890
+ variants.forEach((variant, idx) => {
891
+ const combo = combos[idx];
892
+ const variantRefs = /* @__PURE__ */ new Map();
893
+ const shownByNode = /* @__PURE__ */ new Map();
894
+ const markHidden = (n, inherited) => {
895
+ const own = hidden.shownBy(n) ?? inherited;
896
+ if (own !== void 0) shownByNode.set(n, own);
897
+ for (const child of n.children ?? []) markHidden(child, own);
898
+ };
899
+ for (const child of variant.children ?? []) markHidden(child, void 0);
900
+ walkParts(variant, isInSet ? "Container" : cleanPartName(variant.name), (n, part, path) => {
901
+ const shownBy = shownByNode.get(n);
902
+ if (shownBy !== void 0 && !shownByPath.has(path)) shownByPath.set(path, shownBy);
903
+ for (const ref of normalizeBindings(n.bindings ?? [])) {
904
+ const key = gridKey(path, ref.property);
905
+ partByPath.set(path, part);
906
+ const { property: _property, ...identity } = ref;
907
+ let inner = variantRefs.get(key);
908
+ if (!inner) variantRefs.set(key, inner = /* @__PURE__ */ new Map());
909
+ const rk = refKey(ref);
910
+ inner.set(rk, identity);
911
+ refsByKey.set(rk, identity);
912
+ }
913
+ }, hidden.prune);
914
+ for (const [key, inner] of variantRefs) {
915
+ let cells = cellsByPathProp.get(key);
916
+ if (!cells) {
917
+ cellsByPathProp.set(key, cells = []);
918
+ const [path, prop] = JSON.parse(key);
919
+ if (!propOrder.has(path)) {
920
+ pathOrder.push(path);
921
+ propOrder.set(path, []);
922
+ }
923
+ propOrder.get(path).push(prop);
924
+ }
925
+ cells.push({ combo, keys: [...inner.keys()].sort() });
926
+ }
927
+ });
928
+ for (const cells of cellsByPathProp.values()) {
929
+ const present = new Set(cells.map((c) => c.combo));
930
+ for (const combo of combos) {
931
+ if (!present.has(combo)) cells.push({ combo, keys: [ABSENT_KEY] });
932
+ }
933
+ }
934
+ const cellKey = (c) => JSON.stringify(c.keys);
935
+ const projKey = (combo, axes) => JSON.stringify(axes.map((a) => combo[a]));
936
+ const relevantAxes = (cells) => {
937
+ const relevant = [];
938
+ for (const axis of axisOrder) {
939
+ const present = new Set(cells.map((c) => c.combo[axis]));
940
+ if (present.size < axisValues.get(axis).length) {
941
+ relevant.push(axis);
942
+ continue;
943
+ }
944
+ const others = axisOrder.filter((a) => a !== axis);
945
+ const groups = /* @__PURE__ */ new Map();
946
+ for (const c of cells) {
947
+ const gk = projKey(c.combo, others);
948
+ const tk = cellKey(c);
949
+ const prev = groups.get(gk);
950
+ if (prev === void 0) groups.set(gk, tk);
951
+ else if (prev !== tk) {
952
+ relevant.push(axis);
953
+ break;
954
+ }
955
+ }
956
+ }
957
+ return relevant;
958
+ };
959
+ const hasConflict = (cells, axes) => {
960
+ const m = /* @__PURE__ */ new Map();
961
+ for (const c of cells) {
962
+ const k = projKey(c.combo, axes);
963
+ const tk = cellKey(c);
964
+ const prev = m.get(k);
965
+ if (prev === void 0) m.set(k, tk);
966
+ else if (prev !== tk) return true;
967
+ }
968
+ return false;
969
+ };
970
+ const buildRules = (cellsIn) => {
971
+ let cells = cellsIn;
972
+ let relevant = relevantAxes(cells);
973
+ for (const axis of axisOrder) {
974
+ if (!hasConflict(cells, relevant)) break;
975
+ if (!relevant.includes(axis)) relevant = axisOrder.filter((a) => relevant.includes(a) || a === axis);
976
+ }
977
+ if (hasConflict(cells, relevant)) {
978
+ relevant = [...axisOrder];
979
+ const byCombo = /* @__PURE__ */ new Map();
980
+ for (const c of cells) {
981
+ const k = projKey(c.combo, axisOrder);
982
+ if (!byCombo.has(k)) byCombo.set(k, c);
983
+ }
984
+ cells = [...byCombo.values()];
985
+ }
986
+ const groups = /* @__PURE__ */ new Map();
987
+ for (const c of cells) {
988
+ const k = projKey(c.combo, relevant);
989
+ let g = groups.get(k);
990
+ if (!g) groups.set(k, g = { combo: c.combo, keys: /* @__PURE__ */ new Set() });
991
+ c.keys.forEach((t) => g.keys.add(t));
992
+ }
993
+ let rules = [];
994
+ for (const g of groups.values()) {
995
+ for (const key of [...g.keys].sort()) {
996
+ rules.push({ key, values: new Map(relevant.map((a) => [a, /* @__PURE__ */ new Set([g.combo[a]])])) });
997
+ }
998
+ }
999
+ const conditionKey = (r, excludeAxis) => JSON.stringify(axisOrder.filter((a) => a !== excludeAxis).map((a) => r.values.has(a) ? [...r.values.get(a)].sort() : null));
1000
+ for (const axis of relevant) {
1001
+ const merged = /* @__PURE__ */ new Map();
1002
+ for (const r of rules) {
1003
+ const k = JSON.stringify([r.key, conditionKey(r, axis)]);
1004
+ const prev = merged.get(k);
1005
+ if (prev && prev.values.has(axis) && r.values.has(axis)) {
1006
+ r.values.get(axis).forEach((v) => prev.values.get(axis).add(v));
1007
+ } else if (!merged.has(k)) {
1008
+ merged.set(k, r);
1009
+ }
1010
+ }
1011
+ rules = [...merged.values()];
1012
+ }
1013
+ for (const r of rules) {
1014
+ for (const axis of [...r.values.keys()]) {
1015
+ const vals = r.values.get(axis);
1016
+ const observed = /* @__PURE__ */ new Set();
1017
+ for (const combo of combos) {
1018
+ const matchesOthers = [...r.values.entries()].every(
1019
+ ([a, vs]) => a === axis || vs.has(combo[a])
1020
+ );
1021
+ if (matchesOthers) observed.add(combo[axis]);
1022
+ }
1023
+ if ([...observed].every((v) => vals.has(v))) r.values.delete(axis);
1024
+ }
1025
+ }
1026
+ const seen = /* @__PURE__ */ new Map();
1027
+ for (const r of rules) {
1028
+ const k = JSON.stringify([r.key, conditionKey(r, null)]);
1029
+ if (!seen.has(k)) seen.set(k, r);
1030
+ }
1031
+ rules = [...seen.values()];
1032
+ rules = rules.filter(
1033
+ (r) => !rules.some(
1034
+ (other) => other !== r && other.key === r.key && other.values.size < r.values.size && [...other.values.entries()].every(
1035
+ ([a, vs]) => r.values.has(a) && [...r.values.get(a)].every((v) => vs.has(v))
1036
+ )
1037
+ )
1038
+ );
1039
+ return rules;
1040
+ };
1041
+ const defaultCombo = combos[0];
1042
+ const toTokenRule = (path, property, r) => {
1043
+ const conditions = {};
1044
+ for (const axis of axisOrder) {
1045
+ const vs = r.values.get(axis);
1046
+ if (!vs) continue;
1047
+ conditions[axis] = axisValues.get(axis).filter((v) => vs.has(v));
1048
+ }
1049
+ const shownBy = shownByPath.get(path);
1050
+ return {
1051
+ part: partByPath.get(path),
1052
+ path,
1053
+ property,
1054
+ conditions,
1055
+ ...refsByKey.get(r.key),
1056
+ // Absent, never undefined, on a rule that applies unconditionally: the
1057
+ // same contract AnatomyPart.shownBy keeps, so `'shownBy' in rule` is a
1058
+ // reliable test and the key never appears in output it does not apply to.
1059
+ ...shownBy !== void 0 ? { shownBy } : {}
1060
+ };
1061
+ };
1062
+ const ruleSortKey = (r) => {
1063
+ const matchesDefault = [...r.values.entries()].every(([a, vs]) => vs.has(defaultCombo[a]));
1064
+ const axisBits = axisOrder.map((a, i) => {
1065
+ const vs = r.values.get(a);
1066
+ if (!vs) return "";
1067
+ const indices = axisValues.get(a).map((v, vi) => vs.has(v) ? String(vi).padStart(3, "0") : "").filter(Boolean).join(".");
1068
+ return `${i}:${indices}`;
1069
+ }).filter(Boolean).join("|");
1070
+ return [
1071
+ matchesDefault ? "0" : "1",
1072
+ String(r.values.size).padStart(3, "0"),
1073
+ axisBits,
1074
+ // An absent rule has no reference and sorts first, exactly as the old
1075
+ // control-character sentinel did. It is dropped below either way.
1076
+ refsByKey.get(r.key)?.name ?? "",
1077
+ r.key
1078
+ ];
1079
+ };
1080
+ const compareKeys = (a, b) => {
1081
+ for (let i = 0; i < a.length; i++) {
1082
+ if (a[i] < b[i]) return -1;
1083
+ if (a[i] > b[i]) return 1;
1084
+ }
1085
+ return 0;
1086
+ };
1087
+ const out = [];
1088
+ for (const path of pathOrder) {
1089
+ for (const prop of propOrder.get(path)) {
1090
+ const cells = cellsByPathProp.get(gridKey(path, prop));
1091
+ const rules = buildRules(cells);
1092
+ rules.sort((a, b) => compareKeys(ruleSortKey(a), ruleSortKey(b)));
1093
+ for (const r of rules) {
1094
+ if (r.key === ABSENT_KEY) continue;
1095
+ out.push(toTokenRule(path, prop, r));
1096
+ }
1097
+ }
1098
+ }
1099
+ return out;
1100
+ }
1101
+ var TYPOGRAPHY_PROPS = ["typography", "fontSize", "fontFamily", "fontStyle", "fontWeight", "lineHeight", "letterSpacing"];
1102
+ var PADDING_PROPS = ["paddingTop", "paddingRight", "paddingBottom", "paddingLeft", "verticalPadding", "horizontalPadding"];
1103
+ function extractGaps(root) {
1104
+ const out = [];
1105
+ const seenGaps = /* @__PURE__ */ new Set();
1106
+ const pushGap = (part, path, property, issue, value) => {
1107
+ const key = `${path} ${property} ${issue}`;
1108
+ if (seenGaps.has(key)) return;
1109
+ seenGaps.add(key);
1110
+ out.push({ part, path, property, issue, ...value !== void 0 ? { value } : {} });
1111
+ };
1112
+ const isInSet = root.type === "COMPONENT_SET";
1113
+ const def = defaultVariant(root);
1114
+ walkParts(def, isInSet ? "Container" : cleanPartName(def.name), (n, part, path) => {
1115
+ const bound = new Set((n.bindings ?? []).map((b) => b.property));
1116
+ if (n.hasUnboundPaint) {
1117
+ pushGap(part, path, simpleProperty("fills"), "hardcoded-color", n.unboundFill);
1118
+ }
1119
+ if (n.hasUnboundStroke) {
1120
+ pushGap(part, path, simpleProperty("strokes"), "hardcoded-color", n.unboundStroke);
1121
+ }
1122
+ if (n.hasUnboundGradient) {
1123
+ pushGap(part, path, simpleProperty("fills"), "missing-token-binding");
1124
+ }
1125
+ if (n.hasUnboundEffect) {
1126
+ pushGap(part, path, simpleProperty("effects"), "missing-token-binding");
1127
+ }
1128
+ if (n.opacity !== void 0 && n.opacity !== 1 && !bound.has("opacity")) {
1129
+ pushGap(part, path, simpleProperty("opacity"), "hardcoded-value", Math.round(n.opacity * 1e4) / 1e4);
1130
+ }
1131
+ if (n.type === "TEXT" && !TYPOGRAPHY_PROPS.some((p) => bound.has(p))) {
1132
+ pushGap(part, path, simpleProperty("typography"), "missing-token-binding");
1133
+ }
1134
+ const l = n.layout;
1135
+ if (!l) return;
1136
+ if (l.itemSpacing !== void 0 && !bound.has("itemSpacing")) {
1137
+ pushGap(part, path, simpleProperty("itemSpacing"), "hardcoded-value", l.itemSpacing);
1138
+ }
1139
+ if (l.cornerRadius !== void 0 && ![...RADIUS_BINDINGS].some((p) => bound.has(p))) {
1140
+ pushGap(part, path, simpleProperty("cornerRadius"), "hardcoded-value", l.cornerRadius);
1141
+ }
1142
+ if (!PADDING_PROPS.some((p) => bound.has(p))) {
1143
+ const side = (v) => v !== void 0 ? [v] : [];
1144
+ for (const { property, value } of paddingSides(
1145
+ side(l.paddingTop),
1146
+ side(l.paddingRight),
1147
+ side(l.paddingBottom),
1148
+ side(l.paddingLeft)
1149
+ )) {
1150
+ pushGap(part, path, property, "hardcoded-value", value);
1151
+ }
1152
+ }
1153
+ });
1154
+ return out;
1155
+ }
1156
+
1157
+ // ../extractor/src/pivot.ts
1158
+ function isModifierAxis(axis) {
1159
+ if (axis.values.length !== 2) return false;
1160
+ const lower = axis.values.map((v) => v.toLowerCase());
1161
+ return lower.includes("true") && lower.includes("false");
1162
+ }
1163
+ function isStateAxisName(prop) {
1164
+ const n = prop.trim().toLowerCase();
1165
+ return n === "state" || n === "states";
1166
+ }
1167
+
565
1168
  // ../extractor/src/statesMatrix.ts
566
1169
  var STATE_ORDER = [
567
1170
  "default",
@@ -588,6 +1191,200 @@ var STATE_ORDER = [
588
1191
  "visited"
589
1192
  ];
590
1193
  var STATE_VOCAB = new Set(STATE_ORDER);
1194
+ var WHITESPACE2 = /\s/;
1195
+ function stateBaseName(v) {
1196
+ const lowered = v.trim().toLowerCase();
1197
+ let end = lowered.length;
1198
+ while (end > 0 && WHITESPACE2.test(lowered[end - 1])) end--;
1199
+ if (end === 0 || lowered[end - 1] !== ")") return lowered.trim();
1200
+ let open = -1;
1201
+ for (let i = end - 2; i >= 0; i--) {
1202
+ const ch = lowered[i];
1203
+ if (ch === ")") break;
1204
+ if (ch === "(") open = i;
1205
+ }
1206
+ if (open === -1) return lowered.trim();
1207
+ let cut = open;
1208
+ while (cut > 0 && WHITESPACE2.test(lowered[cut - 1])) cut--;
1209
+ return lowered.slice(0, cut).trim();
1210
+ }
1211
+ function stateRank(v) {
1212
+ const i = STATE_ORDER.indexOf(stateBaseName(v));
1213
+ return i === -1 ? STATE_ORDER.length : i;
1214
+ }
1215
+ function orderStates(values) {
1216
+ return values.map((v, i) => ({ v, i })).sort((a, b) => stateRank(a.v) - stateRank(b.v) || a.i - b.i).map((x) => x.v);
1217
+ }
1218
+ function isStateLike(axis) {
1219
+ const n = axis.prop.trim().toLowerCase();
1220
+ if (isStateAxisName(axis.prop) || n === "status") return true;
1221
+ const hits = axis.values.filter((v) => STATE_VOCAB.has(v.trim().toLowerCase())).length;
1222
+ return hits >= 2;
1223
+ }
1224
+ function isStateVocabName(prop) {
1225
+ const n = prop.trim().toLowerCase();
1226
+ return isStateAxisName(prop) || n === "status" || STATE_VOCAB.has(stateBaseName(prop));
1227
+ }
1228
+ function trueValueOf(axis) {
1229
+ return axis.values.find((v) => v.toLowerCase() === "true") ?? axis.values[axis.values.length - 1];
1230
+ }
1231
+ function detectStateMatrix(variants) {
1232
+ const stateAxis = variants.find(isStateLike) ?? null;
1233
+ if (stateAxis) {
1234
+ const rowAxis2 = variants.find((v) => v.prop !== stateAxis.prop && !isModifierAxis(v) && !isStateLike(v)) ?? null;
1235
+ const columns2 = orderStates(stateAxis.values).map((v) => ({
1236
+ label: v,
1237
+ override: { [stateAxis.prop]: v }
1238
+ }));
1239
+ return { encoding: "enum", columns: columns2, rowAxis: rowAxis2?.prop ?? null, axis: stateAxis.prop };
1240
+ }
1241
+ const flags = variants.filter((v) => isModifierAxis(v) && isStateVocabName(v.prop));
1242
+ if (flags.length === 0) return null;
1243
+ const orderedFlagProps = orderStates(flags.map((f) => f.prop));
1244
+ const orderedFlags = orderedFlagProps.map((p) => flags.find((f) => f.prop === p));
1245
+ const columns = [
1246
+ { label: "Default", override: {} },
1247
+ ...orderedFlags.map((f) => ({ label: f.prop, override: { [f.prop]: trueValueOf(f) } }))
1248
+ ];
1249
+ const rowAxis = variants.find((v) => !isModifierAxis(v) && !isStateLike(v)) ?? null;
1250
+ return { encoding: "flags", columns, rowAxis: rowAxis?.prop ?? null, axis: null };
1251
+ }
1252
+
1253
+ // ../extractor/src/props.ts
1254
+ var KIND_MAP = {
1255
+ VARIANT: "variant",
1256
+ BOOLEAN: "boolean",
1257
+ TEXT: "text",
1258
+ INSTANCE_SWAP: "instanceSwap"
1259
+ };
1260
+ function extractProps(root) {
1261
+ return Object.entries(root.propertyDefinitions ?? {}).map(([raw, def]) => ({
1262
+ name: cleanPropName(raw),
1263
+ kind: KIND_MAP[def.type],
1264
+ ...def.variantOptions !== void 0 ? { options: def.variantOptions } : {},
1265
+ default: def.defaultValue
1266
+ }));
1267
+ }
1268
+ function extractVariants(root) {
1269
+ return extractProps(root).filter((p) => p.kind === "variant").map((p) => ({ prop: p.name, values: p.options ?? [] }));
1270
+ }
1271
+ function extractStates(root) {
1272
+ const info = detectStateMatrix(extractVariants(root));
1273
+ if (!info) return ["Default"];
1274
+ const labels = info.columns.map((c) => c.label);
1275
+ return labels.length ? labels : ["Default"];
1276
+ }
1277
+
1278
+ // ../extractor/src/layout.ts
1279
+ function valuesOf(l) {
1280
+ return {
1281
+ ...l.cornerRadius !== void 0 ? { radius: l.cornerRadius } : {},
1282
+ ...l.itemSpacing !== void 0 ? { gap: l.itemSpacing } : {}
1283
+ };
1284
+ }
1285
+ function fmt(l) {
1286
+ const bits = [];
1287
+ if (l.mode) bits.push(l.mode.toLowerCase());
1288
+ const pads = [l.paddingTop ?? 0, l.paddingRight ?? 0, l.paddingBottom ?? 0, l.paddingLeft ?? 0];
1289
+ if (pads.some((p) => p > 0)) bits.push(`padding ${pads.join("/")}`);
1290
+ if (l.itemSpacing !== void 0) bits.push(`gap ${l.itemSpacing}`);
1291
+ if (l.cornerRadius !== void 0) bits.push(`radius ${l.cornerRadius}`);
1292
+ return bits.join(", ");
1293
+ }
1294
+ function extractLayout(root) {
1295
+ const out = [];
1296
+ const isInSet = root.type === "COMPONENT_SET";
1297
+ walkParts(defaultVariant(root), isInSet ? "Container" : cleanPartName(root.name), (n, _part, path) => {
1298
+ if (!n.layout) return;
1299
+ const summary = fmt(n.layout);
1300
+ if (summary) out.push({ part: n.name, path, summary, values: valuesOf(n.layout) });
1301
+ }, false);
1302
+ return out;
1303
+ }
1304
+
1305
+ // ../extractor/src/rawValues.ts
1306
+ var PADDING_BINDINGS = /* @__PURE__ */ new Set([
1307
+ "paddingTop",
1308
+ "paddingRight",
1309
+ "paddingBottom",
1310
+ "paddingLeft",
1311
+ "verticalPadding",
1312
+ "horizontalPadding"
1313
+ ]);
1314
+ function extractRawValues(root) {
1315
+ const out = [];
1316
+ const seen = /* @__PURE__ */ new Set();
1317
+ const push = (part, property, value) => {
1318
+ const k = JSON.stringify([part, property]);
1319
+ if (seen.has(k)) return;
1320
+ seen.add(k);
1321
+ out.push({ part, property, value });
1322
+ };
1323
+ const def = defaultVariant(root);
1324
+ walkParts(def, root.type === "COMPONENT_SET" ? "Container" : cleanPartName(def.name), (n, part) => {
1325
+ const bound = new Set((n.bindings ?? []).map((b) => b.property));
1326
+ if (n.unboundFill) push(part, "fill", n.unboundFill);
1327
+ const l = n.layout;
1328
+ if (l) {
1329
+ const t = l.paddingTop ?? 0, r = l.paddingRight ?? 0, b = l.paddingBottom ?? 0, lf = l.paddingLeft ?? 0;
1330
+ const hasPad = t > 0 || r > 0 || b > 0 || lf > 0;
1331
+ if (hasPad && ![...PADDING_BINDINGS].some((p) => bound.has(p))) {
1332
+ if (t === r && r === b && b === lf) push(part, "padding", String(t));
1333
+ else {
1334
+ if (lf === r && lf > 0) push(part, "padding-x", String(lf));
1335
+ else {
1336
+ if (lf > 0) push(part, "padding-left", String(lf));
1337
+ if (r > 0) push(part, "padding-right", String(r));
1338
+ }
1339
+ if (t === b && t > 0) push(part, "padding-y", String(t));
1340
+ else {
1341
+ if (t > 0) push(part, "padding-top", String(t));
1342
+ if (b > 0) push(part, "padding-bottom", String(b));
1343
+ }
1344
+ }
1345
+ }
1346
+ if (l.itemSpacing !== void 0 && l.itemSpacing > 0 && !bound.has("itemSpacing")) {
1347
+ push(part, "gap", String(l.itemSpacing));
1348
+ }
1349
+ if (l.cornerRadius !== void 0 && l.cornerRadius > 0 && ![...RADIUS_BINDINGS].some((p) => bound.has(p))) {
1350
+ push(part, "border-radius", String(l.cornerRadius));
1351
+ }
1352
+ }
1353
+ }, true);
1354
+ return out;
1355
+ }
1356
+
1357
+ // ../extractor/src/extract.ts
1358
+ function toVariantInstances(model) {
1359
+ return model.variants.map((v, i) => ({ nodeId: v.id, name: v.name, values: model.combos[i] }));
1360
+ }
1361
+ function extract(root, meta) {
1362
+ const { parts, related, componentId } = extractAnatomy(root);
1363
+ const model = variantAxisModel(root);
1364
+ return {
1365
+ name: root.name,
1366
+ figmaKey: root.key ?? "",
1367
+ figmaFile: meta.figmaFile,
1368
+ // Spread in only when a name was actually supplied, so an absent name is
1369
+ // an absent key rather than a key holding undefined.
1370
+ ...meta.figmaFileName ? { figmaFileName: meta.figmaFileName } : {},
1371
+ figmaNode: root.id,
1372
+ description: root.description ?? "",
1373
+ documentationLinks: root.documentationLinks ?? [],
1374
+ anatomy: parts,
1375
+ anatomyComponentId: componentId,
1376
+ props: extractProps(root),
1377
+ variants: extractVariants(root),
1378
+ variantInstances: toVariantInstances(model),
1379
+ states: extractStates(root),
1380
+ tokens: extractTokens(root, model),
1381
+ related,
1382
+ gaps: extractGaps(root),
1383
+ layout: extractLayout(root),
1384
+ rawValues: extractRawValues(root),
1385
+ nodeEffects: extractNodeEffects(root)
1386
+ };
1387
+ }
591
1388
 
592
1389
  // ../extractor/src/v5/precision.ts
593
1390
  var SIGNIFICANT_DIGITS = 7;
@@ -643,10 +1440,10 @@ var DEFAULT_SEVERITY = {
643
1440
  EXPORT_SCOPED: "info"
644
1441
  };
645
1442
  var compareCodeUnits = (a, b) => a < b ? -1 : a > b ? 1 : 0;
646
- function diagnostic(code2, fields) {
1443
+ function diagnostic(code3, fields) {
647
1444
  return {
648
- code: code2,
649
- severity: DEFAULT_SEVERITY[code2],
1445
+ code: code3,
1446
+ severity: DEFAULT_SEVERITY[code3],
650
1447
  entity_id: fields.entity_id,
651
1448
  ...fields.mode_id !== void 0 ? { mode_id: fields.mode_id } : {},
652
1449
  message: fields.message,
@@ -657,8 +1454,16 @@ function diagnostic(code2, fields) {
657
1454
  // ../extractor/src/hash.ts
658
1455
  var import_js_sha256 = __toESM(require_sha256(), 1);
659
1456
 
1457
+ // ../extractor/src/displayNames.ts
1458
+ function displayComponentName(raw) {
1459
+ const name = raw.replace(/^[._]+/, "").trim();
1460
+ if (!name) return "";
1461
+ if (/[A-Z]/.test(name)) return name;
1462
+ return name.charAt(0).toUpperCase() + name.slice(1);
1463
+ }
1464
+
660
1465
  // ../extractor/src/prose/prompt.ts
661
- var PROSE_SYSTEM_PROMPT = [
1466
+ var LEGACY_PROSE_SYSTEM_PROMPT = [
662
1467
  "You write component guideline prose for a design-system specification tool.",
663
1468
  "Your output fills three spec sections: Definition, Accessibility, and Do's & Don'ts,",
664
1469
  "in the voice of best-in-class design systems (Atlassian, Material, Polaris, Carbon).",
@@ -707,7 +1512,7 @@ var PROSE_SYSTEM_PROMPT = [
707
1512
  "Return only the JSON object requested in the user message. No preamble and no prose outside the",
708
1513
  "JSON."
709
1514
  ].join("\n");
710
- var FEW_SHOT_PROMPT = [
1515
+ var LEGACY_FEW_SHOT_PROMPT = [
711
1516
  "Component: Button",
712
1517
  "",
713
1518
  "Anatomy: Container, Label, Leading icon (component)",
@@ -719,7 +1524,7 @@ var FEW_SHOT_PROMPT = [
719
1524
  "",
720
1525
  `Return ONLY a JSON object with keys: definition (one sentence defining what it is, then a short benefit-led overview: where it is used, the value it gives people, its role, and a guiding principle; no style names and no when-to-use guide), variantsSummary (1-2 sentences on what varies across the options, then a bulleted "when to use which type" guide with bold type names when it has several types), anatomySummary (1-2 sentences orienting the reader to the component structure and the role of its key parts), anatomyParts (array of { name, description } where each name EXACTLY matches one of the Anatomy part names above and description is one concise sentence naming that part's role), accessibility (a bulleted list; give each bullet a short bold lead-in then the guidance; include one bullet flagging what cannot be known from the design file), interactions (Markdown under "### Mouse", "### Keyboard", "### Other" subheadings, 2-3 bullets each, anchored to the States above), designConsiderations (3-4 designer-facing bullets on contrast, state distinguishability, and missing-state flags), contentConsiderations (3-4 bullets on label writing, truncation, and internationalization), dos (string[], 3 to 5 items, each starting with a bold rule summary then the reason), donts (string[], 3 to 5 items, same shape). Use Markdown (bold lead-ins, lists, at most "###" subheadings); never "#" or "##" headings. Do not use em dashes. Do not include any prose outside the JSON.`
721
1526
  ].join("\n");
722
- var FEW_SHOT_RESPONSE = {
1527
+ var LEGACY_FEW_SHOT_RESPONSE = {
723
1528
  definition: "A Button triggers an action when activated. Used across products to perform common actions, it gives people a familiar, accessible way to engage with the interface and keeps frequent tasks fast and predictable. It is essential for guiding people through workflows and performing the key actions on a screen. Create buttons that are clear, easy to identify, and accessible.",
724
1529
  variantsSummary: [
725
1530
  "Style sets the visual weight and states cover the interactive feedback; all styles share the same anatomy.",
@@ -773,6 +1578,315 @@ var FEW_SHOT_RESPONSE = {
773
1578
  "**Don't disable a button without explaining why.** A disabled control gives no reason and drops out of the tab order, so use inline validation instead."
774
1579
  ]
775
1580
  };
1581
+ var LEGACY_FOUNDATION_SYSTEM_PROMPT = [
1582
+ "You write short descriptions of design-token groups for a design-system reference.",
1583
+ "Each description sits under a group heading in a generated documentation frame.",
1584
+ "",
1585
+ "You are given the token names in the group and their resolved values. That is ALL you know.",
1586
+ "Describe what the group is for, as its names and values actually show.",
1587
+ "",
1588
+ "Never invent: no component names the tokens do not mention, no counts, no accessibility",
1589
+ "claims, no history, no rules the names do not support. If the names are too generic to",
1590
+ "support a purpose, describe the shape of the set plainly instead and stop. A vague but true",
1591
+ "sentence is correct; a specific but invented one is a defect.",
1592
+ "",
1593
+ "Voice:",
1594
+ "- One or two sentences. Under 220 characters. No heading, no list, no markdown.",
1595
+ "- Plain and factual, the tone of a peer explaining their own file.",
1596
+ '- Say what the group is for and when to reach for it. Lead with the purpose, not "This group".',
1597
+ '- Write for people, not "the user".',
1598
+ "- Never use em dashes or en dashes. Use a period, comma, colon, or parentheses.",
1599
+ '- Do not restate the heading as a sentence ("Surface colours are colours for surfaces").',
1600
+ "",
1601
+ "Return ONLY a JSON object mapping each group key to its description string.",
1602
+ "No prose outside the JSON, no code fence."
1603
+ ].join("\n");
1604
+
1605
+ // ../extractor/src/prose/v2.ts
1606
+ var PROSE_V2_KEYS = [
1607
+ "overview",
1608
+ "whenToUse",
1609
+ "whenNotToUse",
1610
+ "variantsIntro",
1611
+ "variantsGuide",
1612
+ "anatomySummary",
1613
+ "anatomyParts",
1614
+ "properties",
1615
+ "states",
1616
+ "keyboard",
1617
+ "pointer",
1618
+ "semantics",
1619
+ "content",
1620
+ "guidelines"
1621
+ ];
1622
+ var KEYBOARD_KEYS = [
1623
+ "Tab",
1624
+ "Shift+Tab",
1625
+ "Enter",
1626
+ "Space",
1627
+ "Escape",
1628
+ "Arrow Up",
1629
+ "Arrow Down",
1630
+ "Arrow Left",
1631
+ "Arrow Right",
1632
+ "Home",
1633
+ "End",
1634
+ "Page Up",
1635
+ "Page Down",
1636
+ "Delete",
1637
+ "Backspace"
1638
+ ];
1639
+
1640
+ // ../extractor/src/prose/promptV2.ts
1641
+ var PROMPT_RETURN_ANCHOR = "\nReturn ONLY a JSON object with these keys: ";
1642
+ function partKind(part) {
1643
+ if (part.nested) return `nested component ${part.component ?? "component"}`;
1644
+ switch (part.type) {
1645
+ case "TEXT":
1646
+ return "text";
1647
+ case "VECTOR":
1648
+ case "BOOLEAN_OPERATION":
1649
+ case "STAR":
1650
+ case "POLYGON":
1651
+ case "LINE":
1652
+ return "vector";
1653
+ case "RECTANGLE":
1654
+ case "ELLIPSE":
1655
+ return "shape";
1656
+ case "FRAME":
1657
+ case "GROUP":
1658
+ case "SECTION":
1659
+ case "COMPONENT":
1660
+ case "INSTANCE":
1661
+ return "container";
1662
+ default:
1663
+ return part.type.toLowerCase();
1664
+ }
1665
+ }
1666
+ var PROSE_KEY_INSTRUCTIONS = {
1667
+ overview: "overview ({ lede, body }: lede is one sentence saying what the component is and what a person does with it; body is at most one short paragraph, one or two sentences, on where it appears and what it holds; describe, never explain or justify; no option names)",
1668
+ whenToUse: "whenToUse (string[], 2 to 4 concrete situations where this component is the right choice; each names a task or context, never a rule about how to use it)",
1669
+ whenNotToUse: 'whenNotToUse (string[], 2 to 3 situations where another control fits better, each phrased "For <situation>, use <alternative> instead" with the reason after a semicolon; never start with "Do not"; name another component only if it is listed under Related above)',
1670
+ variantsIntro: "variantsIntro (one or two sentences on what the option axes change; never mention states)",
1671
+ variantsGuide: "variantsGuide ({ name, guidance }[] with one entry per option value listed under Options above, name spelled exactly as listed, guidance one sentence on when to choose it; omit state values)",
1672
+ anatomySummary: "anatomySummary (one or two sentences on how the parts fit together)",
1673
+ anatomyParts: "anatomyParts ({ name, role }[] with name exactly as listed under Anatomy above and role one sentence on what the part does, not how it looks; skip a part you cannot describe without guessing)",
1674
+ properties: "properties ({ name, description }[] with name exactly as listed under Options, State axis or Properties above and description one sentence on what the property controls and when to change it)",
1675
+ states: "states ({ name, whenItApplies }[] with name exactly as listed under States above and whenItApplies one sentence on when the state applies)",
1676
+ // The vocabulary is read from KEYBOARD_KEYS rather than written out again:
1677
+ // `validateProseV2` drops a row whose key is not in that list, so a literal
1678
+ // here could ask for a key the validator then throws away, or leave out one
1679
+ // it would have accepted.
1680
+ keyboard: `keyboard ({ keys: string[], action }[] with each key one of ${KEYBOARD_KEYS.join(", ")}, and action one sentence; include only bindings this component really has, and leave the key out for a non-interactive component)`,
1681
+ pointer: "pointer (string[], 2 to 3 sentences on mouse and touch behaviour, including target size)",
1682
+ semantics: "semantics (string[], 3 to 4 sentences on roles, accessible names, and announcements, plus one on what the design file cannot encode)",
1683
+ content: "content (string[], 3 to 4 sentences on writing the text parts listed under Anatomy, on truncation, and on translation)",
1684
+ guidelines: "guidelines ({ do: { rule, reason }, dont: { rule, reason } }[], 3 pairs about using this component once chosen; each pair covers one topic drawn from its options, states or text parts, the dont mirrors the do, and no pair repeats a When to use or When not to use bullet; each rule one sentence, each reason one sentence)"
1685
+ };
1686
+ function axisDefault(spec, axis) {
1687
+ const prop = spec.props.find((p) => p.name === axis && p.kind === "variant");
1688
+ return typeof prop?.default === "string" ? prop.default : void 0;
1689
+ }
1690
+ var WHITESPACE3 = /\s/;
1691
+ function collapseLineBreaks(text) {
1692
+ let out = "";
1693
+ let i = 0;
1694
+ while (i < text.length) {
1695
+ if (!WHITESPACE3.test(text[i])) {
1696
+ out += text[i];
1697
+ i += 1;
1698
+ continue;
1699
+ }
1700
+ let j = i;
1701
+ let hasBreak = false;
1702
+ while (j < text.length && WHITESPACE3.test(text[j])) {
1703
+ if (text[j] === "\n") hasBreak = true;
1704
+ j += 1;
1705
+ }
1706
+ out += hasBreak ? " " : text.slice(i, j);
1707
+ i = j;
1708
+ }
1709
+ return out;
1710
+ }
1711
+ function buildProsePrompt(spec, requested) {
1712
+ const lines = [];
1713
+ const display = displayComponentName(spec.name);
1714
+ lines.push(display === spec.name ? `Component: ${spec.name}` : `Component: ${display} (layer name: ${spec.name})`);
1715
+ const description = spec.description.trim();
1716
+ if (description) {
1717
+ lines.push("");
1718
+ lines.push("Designer's description (authoritative; build on it, never contradict or restate it):");
1719
+ lines.push(` ${collapseLineBreaks(description)}`);
1720
+ }
1721
+ if (spec.anatomy.length) {
1722
+ lines.push("");
1723
+ lines.push("Anatomy (depth-first; indent marks nesting):");
1724
+ for (const part of spec.anatomy) {
1725
+ const indent = " ".repeat(part.depth + 1);
1726
+ const shown = part.shownBy ? `; shown by ${part.shownBy}` : "";
1727
+ lines.push(`${indent}${part.name}: ${partKind(part)}${shown}`);
1728
+ }
1729
+ }
1730
+ const matrix = detectStateMatrix(spec.variants);
1731
+ const stateAxis = matrix?.axis ?? null;
1732
+ const optionAxes = spec.variants.filter((v) => v.prop !== stateAxis);
1733
+ if (optionAxes.length) {
1734
+ lines.push("");
1735
+ lines.push("Options:");
1736
+ for (const v of optionAxes) {
1737
+ const def = axisDefault(spec, v.prop);
1738
+ lines.push(` ${v.prop}: ${v.values.join(" \xB7 ")}${def ? ` (default ${def})` : ""}`);
1739
+ }
1740
+ }
1741
+ if (stateAxis) {
1742
+ const axis = spec.variants.find((v) => v.prop === stateAxis);
1743
+ if (axis) {
1744
+ lines.push("");
1745
+ lines.push("State axis:");
1746
+ lines.push(` ${axis.prop}: ${axis.values.join(" \xB7 ")}`);
1747
+ }
1748
+ }
1749
+ const otherProps = spec.props.filter((p) => p.kind !== "variant");
1750
+ if (otherProps.length) {
1751
+ lines.push("");
1752
+ lines.push("Properties:");
1753
+ for (const p of otherProps) {
1754
+ const def = p.default !== void 0 ? ` (default: ${p.default})` : "";
1755
+ lines.push(` ${p.name} [${p.kind}]${def}`);
1756
+ }
1757
+ }
1758
+ if (spec.states.length) {
1759
+ lines.push("");
1760
+ lines.push(`States: ${spec.states.join(", ")}`);
1761
+ }
1762
+ if (spec.tokens.length) {
1763
+ lines.push("");
1764
+ lines.push("Design tokens:");
1765
+ for (const t of spec.tokens) {
1766
+ const condition = formatConditions(t.conditions);
1767
+ const qualifier = condition === "\u2014" ? "" : ` [${condition}]`;
1768
+ lines.push(` ${t.part}.${t.property}${qualifier} \u2192 ${t.name}`);
1769
+ }
1770
+ if (spec.tokens.some((t) => Object.keys(t.conditions).length)) {
1771
+ lines.push(" Note: a bracketed condition like [State=Hover] means the token applies only to variants matching those axis values; unbracketed lines apply to all variants.");
1772
+ }
1773
+ }
1774
+ if (spec.layout.length) {
1775
+ lines.push("");
1776
+ lines.push("Layout (default variant):");
1777
+ for (const l of spec.layout) lines.push(` ${l.part}: ${l.summary}`);
1778
+ }
1779
+ if (spec.related.length) {
1780
+ lines.push("");
1781
+ lines.push(`Related: ${spec.related.join(", ")}`);
1782
+ }
1783
+ const keys = requested ? PROSE_V2_KEYS.filter((k) => requested.has(k)) : [...PROSE_V2_KEYS];
1784
+ lines.push("");
1785
+ lines.push(
1786
+ PROMPT_RETURN_ANCHOR.trimStart() + keys.map((k) => PROSE_KEY_INSTRUCTIONS[k]).join("; ") + ". Leave out any key you cannot fill honestly. Markdown only as **bold** or `code` inside a sentence; no headings. No em dashes; keep sentences short. Return only the JSON object."
1787
+ );
1788
+ return lines.join("\n");
1789
+ }
1790
+ var BANNED_PHRASES = [
1791
+ "familiar",
1792
+ "essential",
1793
+ "intuitive",
1794
+ "seamless",
1795
+ "engage with the interface",
1796
+ "clear, easy to identify",
1797
+ "gives people a way to",
1798
+ "plays a key role"
1799
+ ];
1800
+ var PROSE_SYSTEM_PROMPT = [
1801
+ "You write component documentation for a design system, in the voice of a senior designer explaining their own component to a colleague.",
1802
+ "",
1803
+ "Voice:",
1804
+ "- Second person, verb first, one idea per sentence. Every rule carries its reason.",
1805
+ "- Anchor guidance in concrete situations: forms, dialogs, toolbars, lists, filters.",
1806
+ '- Write for people, not "the user".',
1807
+ '- Describe, do not argue. No sentence explains why the component reads or feels a certain way, and nothing is "rather than" something else.',
1808
+ "",
1809
+ "Facts:",
1810
+ "- Name only what the prompt lists: this component's parts, properties, option values, states, and related components. Never invent an option, a part, a state, or a component.",
1811
+ "- The designer's description, when given, is authoritative. Build on it. Never contradict it and never restate it.",
1812
+ "- States are not variants. The variants guide covers option axes only; states go in the states list.",
1813
+ "- Say each fact once. When to use and When not to use are about choosing this component over another; the guidelines are about using it well once chosen.",
1814
+ `- Keyboard rows use only these keys: ${KEYBOARD_KEYS.join(", ")}.`,
1815
+ "",
1816
+ "Words to avoid:",
1817
+ `- Do not write ${BANNED_PHRASES.map((p) => `"${p}"`).join(", ")}.`,
1818
+ "- Do not restate a heading as a sentence.",
1819
+ "",
1820
+ "Format:",
1821
+ "- No em dashes and no spaced en dashes. Use a comma, a colon, or a full stop.",
1822
+ "- No headings inside strings. Markdown only as **bold** or `code` inside a sentence.",
1823
+ "- Return only the JSON object the message asks for, with only the keys it lists. Leave out a key you cannot fill honestly."
1824
+ ].join("\n");
1825
+ function exemplarVariant(state) {
1826
+ const bind = (property, name) => ({
1827
+ property,
1828
+ id: `VariableID:${name}`,
1829
+ name,
1830
+ kind: "variable",
1831
+ remote: false,
1832
+ collectionId: "VariableCollectionId:exemplar"
1833
+ });
1834
+ const border = state === "Focused" ? "color/border/focus" : state === "Error" ? "color/border/error" : "color/field/border";
1835
+ return {
1836
+ id: `x:${state}`,
1837
+ name: `Size=Medium, Style=Filled, State=${state}`,
1838
+ type: "COMPONENT",
1839
+ visible: true,
1840
+ layout: { mode: "VERTICAL", itemSpacing: 4 },
1841
+ children: [
1842
+ { id: `x:${state}:label`, name: "Label", type: "TEXT", visible: true, bindings: [bind("fills", "color/text/secondary")] },
1843
+ {
1844
+ id: `x:${state}:input`,
1845
+ name: "Input",
1846
+ type: "FRAME",
1847
+ visible: true,
1848
+ layout: { mode: "HORIZONTAL", paddingLeft: 12, paddingRight: 12, itemSpacing: 8 },
1849
+ bindings: [bind("fills", "color/field/bg"), bind("strokes", border)],
1850
+ children: [
1851
+ {
1852
+ id: `x:${state}:icon`,
1853
+ name: "Leading icon",
1854
+ type: "INSTANCE",
1855
+ visible: false,
1856
+ visibleProperty: "Show leading icon",
1857
+ mainComponent: { name: "Icon", key: "exemplar-icon" }
1858
+ },
1859
+ { id: `x:${state}:placeholder`, name: "Placeholder", type: "TEXT", visible: true, bindings: [bind("fills", "color/text/placeholder")] }
1860
+ ]
1861
+ },
1862
+ { id: `x:${state}:helper`, name: "Helper text", type: "TEXT", visible: true, bindings: [bind("fills", "color/text/secondary")] }
1863
+ ]
1864
+ };
1865
+ }
1866
+ function exemplarNode() {
1867
+ return {
1868
+ id: "x:0",
1869
+ name: "Text field",
1870
+ type: "COMPONENT_SET",
1871
+ visible: true,
1872
+ key: "exemplar-text-field",
1873
+ description: "A single-line field where people type short, free-form text.",
1874
+ propertyDefinitions: {
1875
+ Size: { type: "VARIANT", defaultValue: "Medium", variantOptions: ["Small", "Medium"] },
1876
+ Style: { type: "VARIANT", defaultValue: "Filled", variantOptions: ["Filled", "Outlined"] },
1877
+ State: { type: "VARIANT", defaultValue: "Enabled", variantOptions: ["Enabled", "Hover", "Focused", "Error", "Disabled"] },
1878
+ "Show leading icon": { type: "BOOLEAN", defaultValue: false },
1879
+ "Show helper text": { type: "BOOLEAN", defaultValue: true },
1880
+ Label: { type: "TEXT", defaultValue: "Label" },
1881
+ Placeholder: { type: "TEXT", defaultValue: "Placeholder" }
1882
+ },
1883
+ children: ["Enabled", "Hover", "Focused", "Error", "Disabled"].map(exemplarVariant)
1884
+ };
1885
+ }
1886
+ function exemplarSpec() {
1887
+ return extract(exemplarNode(), { figmaFile: "exemplar" });
1888
+ }
1889
+ var EXEMPLAR_PROMPT = buildProsePrompt(exemplarSpec());
776
1890
 
777
1891
  // ../extractor/src/prose/foundationPrompt.ts
778
1892
  var FOUNDATION_SYSTEM_PROMPT = [
@@ -795,10 +1909,160 @@ var FOUNDATION_SYSTEM_PROMPT = [
795
1909
  "- Never use em dashes or en dashes. Use a period, comma, colon, or parentheses.",
796
1910
  '- Do not restate the heading as a sentence ("Surface colours are colours for surfaces").',
797
1911
  "",
798
- "Return ONLY a JSON object mapping each group key to its description string.",
1912
+ 'Each "<collection key>|overview" entry describes that collection in one paragraph: what it holds,',
1913
+ "how its modes differ, and which collections it draws from, exactly as the names, modes and alias",
1914
+ "counts show. Under 400 characters, no markdown, no invented usage. Leave it out if the names",
1915
+ "support nothing.",
1916
+ "",
1917
+ "Return ONLY a JSON object: each group key mapped to its description, and each",
1918
+ "collection overview key mapped to its overview.",
799
1919
  "No prose outside the JSON, no code fence."
800
1920
  ].join("\n");
801
1921
 
1922
+ // ../extractor/src/yaml.ts
1923
+ var RESERVED_WORD = /^(y|n|yes|no|true|false|on|off|null|~)$/i;
1924
+ var NUMERIC = /^[-+]?(\d[\d_]*(\.\d*)?([eE][-+]?\d+)?|\.\d+|0[xob][0-9a-fA-F_]+)$/;
1925
+ var SPECIAL_FLOAT = /^[-+]?\.(inf|nan)$/i;
1926
+ var LEADING_INDICATOR = /^[-?:,[\]{}#&*!|>'"%@`]/;
1927
+ var CONTROL_CHAR = /[\x00-\x1f]/;
1928
+ function needsQuote(s) {
1929
+ if (s === "") return true;
1930
+ if (LEADING_INDICATOR.test(s)) return true;
1931
+ if (/^\s|\s$/.test(s)) return true;
1932
+ if (s.includes(": ") || s.endsWith(":")) return true;
1933
+ if (s.includes(" #")) return true;
1934
+ if (RESERVED_WORD.test(s)) return true;
1935
+ if (SPECIAL_FLOAT.test(s)) return true;
1936
+ if (NUMERIC.test(s)) return true;
1937
+ if (CONTROL_CHAR.test(s)) return true;
1938
+ return false;
1939
+ }
1940
+ function unicodeEscape(ch) {
1941
+ return "\\u" + ch.charCodeAt(0).toString(16).padStart(4, "0");
1942
+ }
1943
+ function doubleQuote(s) {
1944
+ const body = s.replace(/\\/g, "\\\\").replace(/"/g, '\\"').replace(/\n/g, "\\n").replace(/\r/g, "\\r").replace(/\t/g, "\\t").replace(/[\x00-\x1f]/g, (ch) => unicodeEscape(ch));
1945
+ return `"${body}"`;
1946
+ }
1947
+ function inlineScalar(s) {
1948
+ return needsQuote(s) ? doubleQuote(s) : s;
1949
+ }
1950
+ function isInline(value) {
1951
+ if (value === null || typeof value === "boolean" || typeof value === "number") return true;
1952
+ if (typeof value === "string") return value.includes("\r") || !value.includes("\n");
1953
+ if (Array.isArray(value)) return value.length === 0;
1954
+ return Object.values(value).filter((v) => v !== void 0).length === 0;
1955
+ }
1956
+ function inlineText(value) {
1957
+ if (value === null) return "null";
1958
+ if (typeof value === "boolean") return String(value);
1959
+ if (typeof value === "number") {
1960
+ if (!Number.isFinite(value)) throw new Error(`yaml: cannot emit ${String(value)}`);
1961
+ return String(value);
1962
+ }
1963
+ if (typeof value === "string") return inlineScalar(value);
1964
+ if (Array.isArray(value)) return "[]";
1965
+ return "{}";
1966
+ }
1967
+ var FLOW_MAX = 72;
1968
+ function flowEligible(value, depth = 0) {
1969
+ if (isInline(value)) return true;
1970
+ if (typeof value === "string") return false;
1971
+ if (depth >= 2) return false;
1972
+ if (Array.isArray(value)) return value.every((m) => flowEligible(m, depth + 1));
1973
+ if (value === null || typeof value !== "object") {
1974
+ throw new Error(`yaml: flowEligible() called on a non-collection value: ${JSON.stringify(value)}`);
1975
+ }
1976
+ const members = Object.values(value).filter((v) => v !== void 0);
1977
+ if (members.length === 0) return true;
1978
+ return members.every((m) => flowEligible(m, depth + 1));
1979
+ }
1980
+ var FLOW_UNSAFE = /[,{}[\]]/;
1981
+ function flowScalar(value) {
1982
+ if (typeof value !== "string") return inlineText(value);
1983
+ return needsQuote(value) || FLOW_UNSAFE.test(value) ? doubleQuote(value) : value;
1984
+ }
1985
+ function flowText(value) {
1986
+ if (isInline(value)) return flowScalar(value);
1987
+ if (Array.isArray(value)) {
1988
+ return `[${value.map((v) => flowText(v)).join(", ")}]`;
1989
+ }
1990
+ if (value === null || typeof value !== "object") {
1991
+ throw new Error(`yaml: flowText() called on a non-collection value: ${JSON.stringify(value)}`);
1992
+ }
1993
+ const entries = Object.entries(value).filter(([, v]) => v !== void 0);
1994
+ return `{ ${entries.map(([k, v]) => `${flowScalar(k)}: ${flowText(v)}`).join(", ")} }`;
1995
+ }
1996
+ function asFlow(value) {
1997
+ if (isInline(value)) return null;
1998
+ if (!flowEligible(value)) return null;
1999
+ const text = flowText(value);
2000
+ return text.length <= FLOW_MAX ? text : null;
2001
+ }
2002
+ function blockScalarLines(s, indent) {
2003
+ const pad = " ".repeat(indent);
2004
+ let trailingNewlines = 0;
2005
+ while (trailingNewlines < s.length && s[s.length - 1 - trailingNewlines] === "\n") {
2006
+ trailingNewlines++;
2007
+ }
2008
+ const core = trailingNewlines === 0 ? s : s.slice(0, s.length - trailingNewlines);
2009
+ const indicator = trailingNewlines === 0 ? "|-" : "|+";
2010
+ const extraBlankLines = trailingNewlines >= 1 ? trailingNewlines - 1 : 0;
2011
+ const contentLines = core.split("\n").concat(Array(extraBlankLines).fill(""));
2012
+ return [indicator, ...contentLines.map((l) => l === "" ? "" : pad + l)];
2013
+ }
2014
+ function emitMapEntry(key, value, indent) {
2015
+ const pad = " ".repeat(indent);
2016
+ const k = inlineScalar(key);
2017
+ if (isInline(value)) {
2018
+ return [`${pad}${k}: ${inlineText(value)}`];
2019
+ }
2020
+ const flow = asFlow(value);
2021
+ if (flow !== null) {
2022
+ return [`${pad}${k}: ${flow}`];
2023
+ }
2024
+ if (typeof value === "string") {
2025
+ const [indicator, ...lines] = blockScalarLines(value, indent + 2);
2026
+ return [`${pad}${k}: ${indicator}`, ...lines];
2027
+ }
2028
+ return [`${pad}${k}:`, ...blockLines(value, indent + 2)];
2029
+ }
2030
+ function emitListItem(value, indent) {
2031
+ const pad = " ".repeat(indent);
2032
+ if (isInline(value)) {
2033
+ return [`${pad}- ${inlineText(value)}`];
2034
+ }
2035
+ const flow = asFlow(value);
2036
+ if (flow !== null) {
2037
+ return [`${pad}- ${flow}`];
2038
+ }
2039
+ if (typeof value === "string") {
2040
+ const [indicator, ...lines2] = blockScalarLines(value, indent + 2);
2041
+ return [`${pad}- ${indicator}`, ...lines2];
2042
+ }
2043
+ const lines = blockLines(value, indent + 2);
2044
+ const first = lines[0].slice(indent + 2);
2045
+ return [`${pad}- ${first}`, ...lines.slice(1)];
2046
+ }
2047
+ function blockLines(value, indent) {
2048
+ if (Array.isArray(value)) {
2049
+ return value.flatMap((item) => emitListItem(item, indent));
2050
+ }
2051
+ if (value === null || typeof value !== "object") {
2052
+ throw new Error(`yaml: blockLines() called on a non-collection value: ${JSON.stringify(value)}`);
2053
+ }
2054
+ const entries = Object.entries(value).filter((e) => e[1] !== void 0);
2055
+ return entries.flatMap(([k, v]) => emitMapEntry(k, v, indent));
2056
+ }
2057
+ function toYaml(value) {
2058
+ if (isInline(value)) return inlineText(value) + "\n";
2059
+ if (typeof value === "string") {
2060
+ const [indicator, ...lines] = blockScalarLines(value, 2);
2061
+ return [indicator, ...lines].join("\n") + "\n";
2062
+ }
2063
+ return blockLines(value, 0).join("\n") + "\n";
2064
+ }
2065
+
802
2066
  // ../extractor/src/v5/value.ts
803
2067
  var SUPPORTED_UNITS = ["px", "rem", "em", "%", "deg", "ms", "s"];
804
2068
  var SUPPORTED_TOKEN_TYPES = [
@@ -845,6 +2109,14 @@ function canonicalJson(value) {
845
2109
  }
846
2110
  return JSON.stringify(value);
847
2111
  }
2112
+ function semanticContentHash(payload) {
2113
+ return `sha256:${(0, import_js_sha2562.sha256)(canonicalJson({
2114
+ completeness: payload.completeness,
2115
+ collections: payload.collections,
2116
+ tokens: payload.tokens,
2117
+ styles: payload.styles
2118
+ }))}`;
2119
+ }
848
2120
 
849
2121
  // ../extractor/src/v5/validate.ts
850
2122
  var ROOT = "<artifact>";
@@ -1322,46 +2594,392 @@ function validateRootSections(artifact, out) {
1322
2594
  }
1323
2595
  }
1324
2596
  }
1325
- function validateLevel1(artifact) {
1326
- try {
1327
- const out = [];
1328
- if (!isRecord(artifact)) {
1329
- out.push(shape(ROOT, "The artifact root must be an object."));
1330
- return out;
2597
+ function validateLevel1(artifact) {
2598
+ try {
2599
+ const out = [];
2600
+ if (!isRecord(artifact)) {
2601
+ out.push(shape(ROOT, "The artifact root must be an object."));
2602
+ return out;
2603
+ }
2604
+ validateRootSections(artifact, out);
2605
+ if (!Array.isArray(artifact.tokens)) {
2606
+ out.push(shape(ROOT, "`tokens` must be an array."));
2607
+ } else {
2608
+ artifact.tokens.forEach((token, index) => validateToken(token, index, out));
2609
+ }
2610
+ return out;
2611
+ } catch (err) {
2612
+ const message = err instanceof Error && err.message ? `The artifact could not be read; accessing its properties threw: ${err.message}` : "The artifact could not be read; accessing its properties threw.";
2613
+ return [diagnostic("INCONSISTENT_VALUE_SHAPE", {
2614
+ entity_id: "artifact",
2615
+ message
2616
+ })];
2617
+ }
2618
+ }
2619
+
2620
+ // ../extractor/src/v5/fonts.ts
2621
+ function fontRequirements(artifact) {
2622
+ const byFamily = /* @__PURE__ */ new Map();
2623
+ for (const style of artifact.styles.typography) {
2624
+ const family = style.properties.font_family.resolved;
2625
+ if (family === null || family.type !== "font_family") continue;
2626
+ const bucket = byFamily.get(family.value) ?? { weights: /* @__PURE__ */ new Set(), used: /* @__PURE__ */ new Set() };
2627
+ const weight = style.properties.font_weight.resolved;
2628
+ if (weight !== null && weight.type === "number") bucket.weights.add(weight.value);
2629
+ bucket.used.add(style.name);
2630
+ byFamily.set(family.value, bucket);
2631
+ }
2632
+ return [...byFamily.entries()].sort((a, b) => compareCodeUnits(a[0], b[0])).map(([family, bucket]) => ({
2633
+ family,
2634
+ weights: [...bucket.weights].sort((a, b) => a - b),
2635
+ used_by: [...bucket.used].sort(compareCodeUnits)
2636
+ }));
2637
+ }
2638
+
2639
+ // ../extractor/src/v5/aiContext.ts
2640
+ function readableLabels(items, idOf, nameOf) {
2641
+ const counts = /* @__PURE__ */ new Map();
2642
+ for (const item of items) {
2643
+ const name = nameOf(item);
2644
+ counts.set(name, (counts.get(name) ?? 0) + 1);
2645
+ }
2646
+ const preferred = items.map((item) => {
2647
+ const id = idOf(item);
2648
+ const name = nameOf(item);
2649
+ return [id, counts.get(name) === 1 ? name : `${name} [${id}]`];
2650
+ });
2651
+ if (new Set(preferred.map(([, label]) => label)).size === preferred.length) {
2652
+ return new Map(preferred);
2653
+ }
2654
+ return new Map(items.map((item) => [
2655
+ idOf(item),
2656
+ `[${idOf(item)}] ${nameOf(item)}`
2657
+ ]));
2658
+ }
2659
+ function buildIndex(artifact) {
2660
+ const collectionById = new Map(artifact.collections.map((collection) => [collection.id, collection]));
2661
+ const tokenById = new Map(artifact.tokens.map((token) => [token.id, token]));
2662
+ const collectionLabelById = readableLabels(
2663
+ artifact.collections,
2664
+ ({ id }) => id,
2665
+ ({ name }) => name
2666
+ );
2667
+ const ambiguousEntityIds = /* @__PURE__ */ new Set();
2668
+ for (const collection of artifact.collections) {
2669
+ if (collectionLabelById.get(collection.id) !== collection.name) {
2670
+ ambiguousEntityIds.add(collection.id);
2671
+ }
2672
+ }
2673
+ const tokenLabelById = /* @__PURE__ */ new Map();
2674
+ for (const collection of artifact.collections) {
2675
+ const tokens = artifact.tokens.filter((token) => token.collection_id === collection.id);
2676
+ const localLabels = readableLabels(tokens, ({ id }) => id, ({ name }) => name);
2677
+ const collectionLabel = collectionLabelById.get(collection.id) ?? collection.name;
2678
+ for (const token of tokens) {
2679
+ const tokenLabel = localLabels.get(token.id) ?? token.name;
2680
+ if (tokenLabel !== token.name) ambiguousEntityIds.add(token.id);
2681
+ tokenLabelById.set(token.id, `${collectionLabel}/${tokenLabel}`);
2682
+ }
2683
+ }
2684
+ const entityLabelById = new Map(tokenLabelById);
2685
+ const addStyleLabels = (kind, styles) => {
2686
+ const labels = readableLabels(styles, ({ id }) => id, ({ name }) => name);
2687
+ for (const style of styles) {
2688
+ const label = labels.get(style.id) ?? style.name;
2689
+ if (label !== style.name) ambiguousEntityIds.add(style.id);
2690
+ entityLabelById.set(style.id, `${kind}/${label}`);
2691
+ }
2692
+ };
2693
+ addStyleLabels("Typography", artifact.styles.typography);
2694
+ addStyleLabels("Effects", artifact.styles.effects);
2695
+ const modeLabelByCollectionAndId = /* @__PURE__ */ new Map();
2696
+ for (const collection of artifact.collections) {
2697
+ modeLabelByCollectionAndId.set(collection.id, readableLabels(
2698
+ collection.modes,
2699
+ ({ id }) => id,
2700
+ ({ name }) => name
2701
+ ));
2702
+ }
2703
+ return {
2704
+ collectionById,
2705
+ tokenById,
2706
+ collectionLabelById,
2707
+ tokenLabelById,
2708
+ entityLabelById,
2709
+ modeLabelByCollectionAndId,
2710
+ ambiguousEntityIds
2711
+ };
2712
+ }
2713
+ function compactTypedValue(value, expectedType) {
2714
+ let compact;
2715
+ switch (value.type) {
2716
+ case "color":
2717
+ compact = value.alpha === 1 && value.channels === void 0 ? value.hex : {
2718
+ hex: value.hex,
2719
+ ...value.alpha !== 1 ? { alpha: value.alpha } : {},
2720
+ ...value.channels ? { channels: [...value.channels] } : {}
2721
+ };
2722
+ break;
2723
+ case "dimension":
2724
+ case "duration":
2725
+ compact = { number: value.number, unit: value.unit };
2726
+ break;
2727
+ case "cubic_bezier":
2728
+ compact = [...value.value];
2729
+ break;
2730
+ case "number":
2731
+ case "string":
2732
+ case "boolean":
2733
+ case "font_family":
2734
+ compact = value.value;
2735
+ break;
2736
+ default: {
2737
+ const exhaustive = value;
2738
+ return exhaustive;
2739
+ }
2740
+ }
2741
+ return expectedType !== void 0 && value.type !== expectedType ? { type: value.type, value: compact } : compact;
2742
+ }
2743
+ function referenceLabel(reference, index) {
2744
+ if (reference.target_id !== null) {
2745
+ const local = index.tokenLabelById.get(reference.target_id);
2746
+ if (local !== void 0) return local;
2747
+ }
2748
+ const path = reference.target_path.length > 0 ? reference.target_path.join("/") : reference.target_id ?? "<unknown target>";
2749
+ return reference.source_library_name ? `${reference.source_library_name}/${path}` : path;
2750
+ }
2751
+ function stepLabel(step, index) {
2752
+ const token = index.tokenById.get(step.token_id);
2753
+ if (token === void 0) return `${step.token_id} @ ${step.mode_id}`;
2754
+ const tokenLabel = index.tokenLabelById.get(token.id) ?? token.name;
2755
+ const modeLabel = index.modeLabelByCollectionAndId.get(token.collection_id)?.get(step.mode_id) ?? step.mode_id;
2756
+ return `${tokenLabel} @ ${modeLabel}`;
2757
+ }
2758
+ function compactCanonicalValue(value, expectedType, index) {
2759
+ if (value.kind === "literal") return compactTypedValue(value.value, expectedType);
2760
+ if (value.kind === "missing") return { missing: value.reason };
2761
+ const chain = value.resolved.chain.map((step) => stepLabel(step, index));
2762
+ const firstStep = value.resolved.chain[0];
2763
+ const alias = firstStep !== void 0 && firstStep.token_id === value.reference.target_id ? stepLabel(firstStep, index) : referenceLabel(value.reference, index);
2764
+ return {
2765
+ alias,
2766
+ ...value.resolved.status === "resolved" ? { resolved: compactTypedValue(value.resolved.value, expectedType) } : { unresolved: value.resolved.reason },
2767
+ ...chain.length > 1 || value.resolved.status === "unresolved" && chain.length > 0 ? { chain } : {}
2768
+ };
2769
+ }
2770
+ function compactLifecycle(lifecycle, index) {
2771
+ if (lifecycle === void 0) return void 0;
2772
+ const replacement = lifecycle.replacement_id === null ? void 0 : index.entityLabelById.get(lifecycle.replacement_id) ?? lifecycle.replacement_id;
2773
+ return { status: lifecycle.status, ...replacement ? { replacement } : {} };
2774
+ }
2775
+ function compactToken(token, collection, index, includeSourceIds) {
2776
+ const modeLabels2 = index.modeLabelByCollectionAndId.get(collection.id) ?? /* @__PURE__ */ new Map();
2777
+ const values = {};
2778
+ for (const [modeId, value] of Object.entries(token.values)) {
2779
+ values[modeLabels2.get(modeId) ?? modeId] = compactCanonicalValue(value, token.type, index);
2780
+ }
2781
+ const lifecycle = compactLifecycle(token.lifecycle, index);
2782
+ return {
2783
+ name: token.name,
2784
+ ...includeSourceIds || index.ambiguousEntityIds.has(token.id) ? { source_id: token.id } : {},
2785
+ ...token.suggested_code_name ? { suggested_code_name: token.suggested_code_name } : {},
2786
+ type: token.type,
2787
+ ...token.description.length > 0 ? { description: token.description } : {},
2788
+ ...token.scopes.length > 0 ? { scopes: token.scopes } : {},
2789
+ ...token.code_syntax ? { code_syntax: token.code_syntax } : {},
2790
+ ...token.publication ? { publication: token.publication } : {},
2791
+ ...lifecycle ? { lifecycle } : {},
2792
+ values
2793
+ };
2794
+ }
2795
+ function compactStyleProperty(property, index) {
2796
+ if (property.source.kind === "literal") {
2797
+ return property.resolved === null ? { missing: "source_unavailable" } : { type: property.resolved.type, value: compactTypedValue(property.resolved) };
2798
+ }
2799
+ const path = property.source.target_path.join("/");
2800
+ const target = property.source.target_id === null ? path || "<unknown target>" : (index.tokenLabelById.get(property.source.target_id) ?? path) || property.source.target_id;
2801
+ return {
2802
+ alias: target,
2803
+ ...property.resolved === null ? { unresolved: "target_not_found" } : { resolved: { type: property.resolved.type, value: compactTypedValue(property.resolved) } }
2804
+ };
2805
+ }
2806
+ function styleIdentity(style, index, includeSourceIds) {
2807
+ const lifecycle = compactLifecycle(style.lifecycle, index);
2808
+ return {
2809
+ name: style.name,
2810
+ ...includeSourceIds || index.ambiguousEntityIds.has(style.id) ? { source_id: style.id } : {},
2811
+ ...style.suggested_code_name ? { suggested_code_name: style.suggested_code_name } : {},
2812
+ ...style.publication ? { publication: {
2813
+ published: style.publication.published,
2814
+ hidden_from_publishing: style.publication.hidden_from_publishing
2815
+ } } : {},
2816
+ ...style.source ? { source: {
2817
+ remote: style.source.remote,
2818
+ ...style.source.library_name === null ? {} : { library_name: style.source.library_name }
2819
+ } } : {},
2820
+ ...lifecycle ? { lifecycle } : {}
2821
+ };
2822
+ }
2823
+ function compactTypography(style, index, includeSourceIds) {
2824
+ return {
2825
+ ...styleIdentity(style, index, includeSourceIds),
2826
+ ...style.description.length > 0 ? { description: style.description } : {},
2827
+ properties: {
2828
+ font_family: compactStyleProperty(style.properties.font_family, index),
2829
+ font_weight: compactStyleProperty(style.properties.font_weight, index),
2830
+ font_size: compactStyleProperty(style.properties.font_size, index),
2831
+ line_height: compactStyleProperty(style.properties.line_height, index),
2832
+ letter_spacing: compactStyleProperty(style.properties.letter_spacing, index),
2833
+ paragraph_spacing: compactStyleProperty(style.properties.paragraph_spacing, index),
2834
+ paragraph_indent: compactStyleProperty(style.properties.paragraph_indent, index),
2835
+ text_case: style.properties.text_case,
2836
+ text_decoration: style.properties.text_decoration
1331
2837
  }
1332
- validateRootSections(artifact, out);
1333
- if (!Array.isArray(artifact.tokens)) {
1334
- out.push(shape(ROOT, "`tokens` must be an array."));
1335
- } else {
1336
- artifact.tokens.forEach((token, index) => validateToken(token, index, out));
2838
+ };
2839
+ }
2840
+ function compactEffect(style, index, includeSourceIds) {
2841
+ const modeMatches = [...index.collectionById.values()].flatMap((collection) => {
2842
+ const label = index.modeLabelByCollectionAndId.get(collection.id)?.get(style.mode_id ?? "");
2843
+ return label === void 0 ? [] : [label];
2844
+ });
2845
+ const mode = style.mode_id === null ? null : modeMatches.length === 1 ? modeMatches[0] : style.mode_id;
2846
+ return {
2847
+ ...styleIdentity(style, index, includeSourceIds),
2848
+ mode,
2849
+ effects: style.effects.map((effect) => ({
2850
+ type: effect.type,
2851
+ visible: effect.visible,
2852
+ ...effect.blend_mode !== void 0 ? { blend_mode: effect.blend_mode } : {},
2853
+ ...effect.color ? { color: compactTypedValue(effect.color, "color") } : {},
2854
+ ...effect.offset_x ? { offset_x: compactTypedValue(effect.offset_x, "dimension") } : {},
2855
+ ...effect.offset_y ? { offset_y: compactTypedValue(effect.offset_y, "dimension") } : {},
2856
+ ...effect.blur ? { blur: compactTypedValue(effect.blur, "dimension") } : {},
2857
+ ...effect.spread ? { spread: compactTypedValue(effect.spread, "dimension") } : {},
2858
+ ...effect.show_behind_node !== void 0 ? { show_behind_node: effect.show_behind_node } : {}
2859
+ })),
2860
+ ...style.bindings && style.bindings.length > 0 ? { bindings: Object.fromEntries(style.bindings.map((binding) => [
2861
+ binding.property,
2862
+ index.tokenLabelById.get(binding.token_id) ?? binding.token_id
2863
+ ])) } : {}
2864
+ };
2865
+ }
2866
+ function kebab(code3) {
2867
+ return code3.toLowerCase().replace(/_/g, "-");
2868
+ }
2869
+ function valueText(value) {
2870
+ if (value === null || value === void 0) return "unknown";
2871
+ if (typeof value === "object") {
2872
+ const record = value;
2873
+ if ("value" in record) return String(record.value);
2874
+ if ("number" in record) {
2875
+ const unit = typeof record.unit === "string" ? record.unit : "";
2876
+ return `${String(record.number)}${unit}`;
2877
+ }
2878
+ if (typeof record.hex === "string") {
2879
+ const alpha = typeof record.alpha === "number" ? ` alpha ${record.alpha}` : "";
2880
+ const channels = Array.isArray(record.channels) ? ` channels ${JSON.stringify(record.channels)}` : "";
2881
+ return `${record.hex}${alpha}${channels}`;
1337
2882
  }
1338
- return out;
1339
- } catch (err) {
1340
- const message = err instanceof Error && err.message ? `The artifact could not be read; accessing its properties threw: ${err.message}` : "The artifact could not be read; accessing its properties threw.";
1341
- return [diagnostic("INCONSISTENT_VALUE_SHAPE", {
1342
- entity_id: "artifact",
1343
- message
1344
- })];
1345
2883
  }
2884
+ return String(value);
1346
2885
  }
1347
-
1348
- // ../extractor/src/v5/fonts.ts
1349
- function fontRequirements(artifact) {
1350
- const byFamily = /* @__PURE__ */ new Map();
1351
- for (const style of artifact.styles.typography) {
1352
- const family = style.properties.font_family.resolved;
1353
- if (family === null || family.type !== "font_family") continue;
1354
- const bucket = byFamily.get(family.value) ?? { weights: /* @__PURE__ */ new Set(), used: /* @__PURE__ */ new Set() };
1355
- const weight = style.properties.font_weight.resolved;
1356
- if (weight !== null && weight.type === "number") bucket.weights.add(weight.value);
1357
- bucket.used.add(style.name);
1358
- byFamily.set(family.value, bucket);
2886
+ var DIAGNOSTIC_MESSAGE = {
2887
+ STYLE_BINDING_DRIFT: (d) => `${String(d.details?.property)} is ${valueText(d.details?.style_value)} in the style but ${valueText(d.details?.token_value)} in the token it is bound to; the two disagree.`,
2888
+ UNIT_METADATA_UNAVAILABLE: (d) => `The numeric value is kept, but scopes ${JSON.stringify(d.details?.scopes ?? [])} state no unit, so a consumer cannot use it as a length.`,
2889
+ UNRESOLVED_REFERENCE: (d) => d.message
2890
+ };
2891
+ function diagnosticRows(diagnostics, index) {
2892
+ const rows = diagnostics.flatMap((d) => {
2893
+ const render = DIAGNOSTIC_MESSAGE[d.code];
2894
+ if (!render) return [];
2895
+ const property = d.details?.property;
2896
+ const path = index.entityLabelById.get(d.entity_id) ?? d.entity_id;
2897
+ return [{
2898
+ id: kebab(d.code),
2899
+ severity: d.severity,
2900
+ path,
2901
+ ...typeof property === "string" ? { property } : {},
2902
+ message: render(d)
2903
+ }];
2904
+ });
2905
+ return rows.sort((a, b) => compareCodeUnits(a.id, b.id) || compareCodeUnits(a.path ?? "", b.path ?? "") || compareCodeUnits(a.property ?? "", b.property ?? "") || compareCodeUnits(a.message, b.message));
2906
+ }
2907
+ function issueCounts(artifact) {
2908
+ const bySeverity = /* @__PURE__ */ new Map();
2909
+ for (const finding of artifact.diagnostics) {
2910
+ const byCode = bySeverity.get(finding.severity) ?? /* @__PURE__ */ new Map();
2911
+ byCode.set(finding.code, (byCode.get(finding.code) ?? 0) + 1);
2912
+ bySeverity.set(finding.severity, byCode);
2913
+ }
2914
+ if (bySeverity.size === 0) return void 0;
2915
+ return Object.fromEntries([...bySeverity].sort(([a], [b]) => compareCodeUnits(a, b)).map(
2916
+ ([severity, byCode]) => [severity, Object.fromEntries(
2917
+ [...byCode].sort(([a], [b]) => compareCodeUnits(a, b))
2918
+ )]
2919
+ ));
2920
+ }
2921
+ function foundationAiContext(artifact, options = {}) {
2922
+ const includeSourceIds = options.includeSourceIds === true;
2923
+ const index = buildIndex(artifact);
2924
+ const tokensByCollection = /* @__PURE__ */ new Map();
2925
+ for (const token of artifact.tokens) {
2926
+ const tokens = tokensByCollection.get(token.collection_id) ?? [];
2927
+ tokens.push(token);
2928
+ tokensByCollection.set(token.collection_id, tokens);
1359
2929
  }
1360
- return [...byFamily.entries()].sort((a, b) => compareCodeUnits(a[0], b[0])).map(([family, bucket]) => ({
1361
- family,
1362
- weights: [...bucket.weights].sort((a, b) => a - b),
1363
- used_by: [...bucket.used].sort(compareCodeUnits)
1364
- }));
2930
+ const collections = artifact.collections.map((collection) => {
2931
+ const tokens = tokensByCollection.get(collection.id) ?? [];
2932
+ const modeLabels2 = index.modeLabelByCollectionAndId.get(collection.id) ?? /* @__PURE__ */ new Map();
2933
+ return {
2934
+ name: collection.name,
2935
+ ...includeSourceIds || index.ambiguousEntityIds.has(collection.id) ? { source_id: collection.id } : {},
2936
+ ...collection.suggested_code_name ? { suggested_code_name: collection.suggested_code_name } : {},
2937
+ ...collection.publication ? { publication: collection.publication } : {},
2938
+ ...collection.source ? { source: {
2939
+ remote: collection.source.remote,
2940
+ ...collection.source.library_name === null ? {} : { library_name: collection.source.library_name }
2941
+ } } : {},
2942
+ default_mode: modeLabels2.get(collection.default_mode_id) ?? collection.default_mode_id,
2943
+ modes: collection.modes.map((mode) => modeLabels2.get(mode.id) ?? mode.name),
2944
+ tokens: tokens.map((token) => compactToken(
2945
+ token,
2946
+ collection,
2947
+ index,
2948
+ includeSourceIds
2949
+ ))
2950
+ };
2951
+ });
2952
+ const counts = issueCounts(artifact);
2953
+ const validation = diagnosticRows(artifact.diagnostics, index);
2954
+ return {
2955
+ spec_layer: {
2956
+ kind: "foundation",
2957
+ version: 5,
2958
+ profile: "ai",
2959
+ content_hash: artifact.spec_layer.export.content_hash,
2960
+ source: {
2961
+ provider: "figma",
2962
+ ...artifact.spec_layer.source.file_name === null ? {} : { file_name: artifact.spec_layer.source.file_name }
2963
+ }
2964
+ },
2965
+ completeness: artifact.completeness,
2966
+ collections,
2967
+ styles: {
2968
+ typography: artifact.styles.typography.map((style) => compactTypography(
2969
+ style,
2970
+ index,
2971
+ includeSourceIds
2972
+ )),
2973
+ effects: artifact.styles.effects.map((style) => compactEffect(
2974
+ style,
2975
+ index,
2976
+ includeSourceIds
2977
+ ))
2978
+ },
2979
+ ...validation.length > 0 ? { validation } : {},
2980
+ ...counts ? { issue_counts: counts } : {},
2981
+ ...artifact.guidelines ? { guidelines: artifact.guidelines.group_descriptions } : {}
2982
+ };
1365
2983
  }
1366
2984
 
1367
2985
  // ../extractor/src/v5/dtcg.ts
@@ -2318,6 +3936,598 @@ function dtcgExportFiles(out) {
2318
3936
  return files;
2319
3937
  }
2320
3938
 
3939
+ // ../extractor/src/v5/componentContext.ts
3940
+ var import_js_sha2564 = __toESM(require_sha256(), 1);
3941
+ function componentFoundationAiSlice(artifact) {
3942
+ const payload = artifact.references.foundation;
3943
+ if (!payload) return null;
3944
+ const dependencyArtifact = {
3945
+ ...payload,
3946
+ spec_layer: {
3947
+ kind: "foundation",
3948
+ // The Foundation schema's own version, since this slice is a Foundation
3949
+ // artifact: the two happen to agree today and need not tomorrow.
3950
+ schema_version: SCHEMA_VERSION,
3951
+ schema_uri: "https://spec-layer.com/schemas/foundation-context/v5.json",
3952
+ extractor: artifact.spec_layer.extractor,
3953
+ export: {
3954
+ id: `${artifact.spec_layer.export.id}:foundation-dependencies`,
3955
+ generated_at: artifact.spec_layer.export.generated_at,
3956
+ deterministic: true,
3957
+ content_hash: artifact.foundation_dependency_hash ?? semanticContentHash(payload)
3958
+ },
3959
+ source: {
3960
+ provider: "figma",
3961
+ file_id: null,
3962
+ file_name: artifact.spec_layer.source.file_name,
3963
+ file_version: null,
3964
+ library_enabled: null
3965
+ }
3966
+ },
3967
+ diagnostics: artifact.foundation_diagnostics ?? [],
3968
+ statistics: {}
3969
+ };
3970
+ return {
3971
+ dependency_hash: dependencyArtifact.spec_layer.export.content_hash,
3972
+ compact: foundationAiContext(dependencyArtifact, { includeSourceIds: true })
3973
+ };
3974
+ }
3975
+ function componentEnvelope(artifact, profile) {
3976
+ return {
3977
+ spec_layer: {
3978
+ kind: "component",
3979
+ version: 5,
3980
+ profile,
3981
+ content_hash: artifact.spec_layer.export.content_hash,
3982
+ ...artifact.foundation_content_hash ? { foundation_hash: artifact.foundation_content_hash } : {},
3983
+ source: {
3984
+ provider: "figma",
3985
+ ...artifact.spec_layer.source.file_name ? { file_name: artifact.spec_layer.source.file_name } : {}
3986
+ }
3987
+ },
3988
+ source: {
3989
+ node_id: artifact.spec_layer.source.node_id,
3990
+ node_name: artifact.spec_layer.source.node_name,
3991
+ ...artifact.spec_layer.source.component_key ? { component_key: artifact.spec_layer.source.component_key } : {}
3992
+ }
3993
+ };
3994
+ }
3995
+
3996
+ // ../extractor/src/v5/markdown.ts
3997
+ var COMPONENT_YAML_MARKER = "spec_layer:\n kind: component";
3998
+ var COMPONENT_MARKDOWN_MARKER = "---\nspec_layer:\n kind: component";
3999
+ function escapeInline(text) {
4000
+ return text.replace(/\r?\n/g, " ").replace(/([\\*_<>[\]])/g, "\\$1").trim();
4001
+ }
4002
+ function escapeCell(text) {
4003
+ return text.replace(/\r?\n/g, " ").replace(/([\\*_<>[\]|])/g, "\\$1").trim();
4004
+ }
4005
+ function escapeHeading(text) {
4006
+ return escapeCell(text).replace(/^#/, "\\#");
4007
+ }
4008
+ function escapeBlock(text) {
4009
+ return escapeInline(text).replace(/^(`{3,}|~{3,}|[#=+-])/, "\\$1").replace(/^(\d{1,9})([.)])/, "$1\\$2");
4010
+ }
4011
+ function code(text) {
4012
+ const flat = text.replace(/\r?\n/g, " ");
4013
+ const longest = (flat.match(/`+/g) ?? []).reduce((n, run) => Math.max(n, run.length), 0);
4014
+ const fence = "`".repeat(longest + 1);
4015
+ return longest === 0 ? `${fence}${flat}${fence}` : `${fence} ${flat} ${fence}`;
4016
+ }
4017
+ function codeCell(text) {
4018
+ if (text.includes("\\")) return escapeCell(text);
4019
+ return code(text).replace(/\|/g, "\\|");
4020
+ }
4021
+ function table(headers, rows) {
4022
+ const line = (cells) => `| ${cells.join(" | ")} |
4023
+ `;
4024
+ return line(headers) + `|${headers.map(() => "---").join("|")}|
4025
+ ` + rows.map(line).join("");
4026
+ }
4027
+ var asRecord = (value) => value !== null && typeof value === "object" && !Array.isArray(value) ? value : {};
4028
+ var str = (value) => typeof value === "string" && value.length > 0 ? value : void 0;
4029
+ var SCOPE_SENTENCE = {
4030
+ default_variant: "Default variant."
4031
+ };
4032
+ function bindingsSection(references) {
4033
+ const bindings = Array.isArray(references.bindings) ? references.bindings : [];
4034
+ if (bindings.length === 0) return void 0;
4035
+ const byId = new Map(
4036
+ (Array.isArray(references.used) ? references.used : []).map((r) => [
4037
+ asRecord(r).source_id,
4038
+ r
4039
+ ])
4040
+ );
4041
+ const rows = bindings.map((raw) => {
4042
+ const binding = asRecord(raw);
4043
+ const source_id = str(binding.source_id);
4044
+ const reference = source_id ? byId.get(source_id) : void 0;
4045
+ const ref = asRecord(reference);
4046
+ const name = str(ref.name) ?? source_id;
4047
+ const status = ref.status && ref.status !== "resolved" ? ` (${ref.status})` : "";
4048
+ const when = asRecord(binding.when);
4049
+ const whenStr = Object.entries(when).map(([axis, raw_values]) => {
4050
+ const values = Array.isArray(raw_values) ? raw_values : [];
4051
+ return `${escapeCell(axis)}: ${values.map((v) => escapeCell(String(v))).join(", ")}`;
4052
+ }).join("; ");
4053
+ const path = str(binding.path);
4054
+ const property = str(binding.property);
4055
+ return [
4056
+ path ? codeCell(path) : "",
4057
+ property ? escapeCell(property) : "",
4058
+ `${escapeCell(name ?? "")}${status}`,
4059
+ whenStr
4060
+ ];
4061
+ });
4062
+ return `## Token bindings
4063
+
4064
+ ${table(["Part", "Property", "Token", "When"], rows).trimEnd()}`;
4065
+ }
4066
+ function layoutSection(layout) {
4067
+ const items = Array.isArray(layout.items) ? layout.items : [];
4068
+ if (items.length === 0) return void 0;
4069
+ const rows = items.map((raw) => {
4070
+ const item = asRecord(raw);
4071
+ const path = str(item.path);
4072
+ return [path ? codeCell(path) : "", escapeCell(str(item.summary) ?? "")];
4073
+ });
4074
+ const scope = str(layout.scope);
4075
+ const sentence = scope ? SCOPE_SENTENCE[scope] : void 0;
4076
+ const parts = ["## Layout"];
4077
+ if (sentence) parts.push(sentence);
4078
+ parts.push(table(["Part", "Layout"], rows).trimEnd());
4079
+ return parts.join("\n\n");
4080
+ }
4081
+ function propertiesSection(api) {
4082
+ const rows = [];
4083
+ for (const [name, raw] of Object.entries(asRecord(api.variants))) {
4084
+ const axis = asRecord(raw);
4085
+ const options = Array.isArray(axis.options) ? axis.options.map((o) => escapeCell(String(o))).join(", ") : "";
4086
+ rows.push([escapeCell(name), "Variant", options, escapeCell(String(axis.default ?? ""))]);
4087
+ }
4088
+ for (const [name, raw] of Object.entries(asRecord(api.booleans))) {
4089
+ const b = asRecord(raw);
4090
+ rows.push([
4091
+ escapeCell(name),
4092
+ "Boolean",
4093
+ "",
4094
+ b.default === void 0 ? "" : escapeCell(String(b.default))
4095
+ ]);
4096
+ }
4097
+ for (const [name, raw] of Object.entries(asRecord(api.slots))) {
4098
+ const slot = asRecord(raw);
4099
+ const type = str(slot.type);
4100
+ const label = type ? type.charAt(0).toUpperCase() + type.slice(1) : "Slot";
4101
+ rows.push([
4102
+ escapeCell(name),
4103
+ label,
4104
+ "",
4105
+ slot.default === void 0 ? "" : escapeCell(String(slot.default))
4106
+ ]);
4107
+ }
4108
+ const states = Array.isArray(api.states) ? api.states.map((s) => escapeInline(String(s))) : [];
4109
+ if (rows.length === 0 && states.length === 0) return void 0;
4110
+ const parts = ["## Properties"];
4111
+ if (rows.length > 0) {
4112
+ parts.push(table(["Property", "Type", "Options", "Default"], rows).trimEnd());
4113
+ }
4114
+ if (states.length > 0) parts.push(`States: ${states.join(", ")}`);
4115
+ return parts.join("\n\n");
4116
+ }
4117
+ function anatomyBullets(nodes, depth) {
4118
+ const lines = [];
4119
+ for (const raw of nodes) {
4120
+ const node = asRecord(raw);
4121
+ const part = escapeInline(str(node.part) ?? "");
4122
+ const path = str(node.path);
4123
+ const type = str(node.type);
4124
+ const parts = [];
4125
+ if (path) parts.push(code(path));
4126
+ const component = str(node.component);
4127
+ if (component) parts.push(`instance of ${escapeInline(component)}`);
4128
+ else if (type) parts.push(escapeInline(type.toLowerCase()));
4129
+ const shownBy = str(node.shown_by);
4130
+ if (shownBy) parts.push(`shown when ${code(shownBy)} is true`);
4131
+ lines.push(`${" ".repeat(depth)}- ${part}: ${parts.join(", ")}`);
4132
+ if (Array.isArray(node.children)) {
4133
+ lines.push(...anatomyBullets(node.children, depth + 1));
4134
+ }
4135
+ }
4136
+ return lines;
4137
+ }
4138
+ var NOT_READ_SENTENCE = "Token values are not included: the foundations had not been read when this was exported.";
4139
+ function styleValueText(value) {
4140
+ if (value !== null && typeof value === "object") {
4141
+ const record = value;
4142
+ if (typeof record.missing === "string") return `missing: ${record.missing}`;
4143
+ if (typeof record.alias === "string") {
4144
+ if (record.resolved !== void 0) {
4145
+ const resolved2 = record.resolved;
4146
+ return `${record.alias} (resolved: ${valueText(resolved2.value)})`;
4147
+ }
4148
+ if (typeof record.unresolved === "string") {
4149
+ return `${record.alias} (unresolved: ${record.unresolved})`;
4150
+ }
4151
+ }
4152
+ if ("value" in record) return valueText(record.value);
4153
+ }
4154
+ return valueText(value);
4155
+ }
4156
+ function typographySection(items) {
4157
+ if (items.length === 0) return void 0;
4158
+ const rows = items.map((raw) => {
4159
+ const style = asRecord(raw);
4160
+ const properties = asRecord(style.properties);
4161
+ return [
4162
+ escapeCell(str(style.name) ?? ""),
4163
+ escapeCell(styleValueText(properties.font_family)),
4164
+ escapeCell(styleValueText(properties.font_weight)),
4165
+ escapeCell(styleValueText(properties.font_size)),
4166
+ escapeCell(styleValueText(properties.line_height)),
4167
+ escapeCell(styleValueText(properties.letter_spacing)),
4168
+ escapeCell(styleValueText(properties.paragraph_indent))
4169
+ ];
4170
+ });
4171
+ return [
4172
+ "### Typography styles",
4173
+ table(
4174
+ ["Style", "Font family", "Weight", "Size", "Line height", "Letter spacing", "Paragraph indent"],
4175
+ rows
4176
+ ).trimEnd()
4177
+ ].join("\n\n");
4178
+ }
4179
+ function effectSummary(effect) {
4180
+ const parts = [];
4181
+ const type = str(effect.type);
4182
+ if (type) parts.push(type);
4183
+ if (effect.offset_x !== void 0 || effect.offset_y !== void 0) {
4184
+ parts.push(`offset ${valueText(effect.offset_x)}/${valueText(effect.offset_y)}`);
4185
+ }
4186
+ if (effect.blur !== void 0) parts.push(`blur ${valueText(effect.blur)}`);
4187
+ if (effect.spread !== void 0) parts.push(`spread ${valueText(effect.spread)}`);
4188
+ if (effect.color !== void 0) parts.push(valueText(effect.color));
4189
+ const summary = parts.join(", ");
4190
+ return effect.visible === false ? `${summary} (hidden)` : summary;
4191
+ }
4192
+ function effectsSection(items) {
4193
+ if (items.length === 0) return void 0;
4194
+ const rows = items.map((raw) => {
4195
+ const style = asRecord(raw);
4196
+ const mode = str(style.mode);
4197
+ const effects = Array.isArray(style.effects) ? style.effects : [];
4198
+ const summary = effects.map((effect) => effectSummary(asRecord(effect))).join("; ");
4199
+ return [escapeCell(str(style.name) ?? ""), mode ? escapeCell(mode) : "", escapeCell(summary)];
4200
+ });
4201
+ return [
4202
+ "### Effect styles",
4203
+ table(["Style", "Mode", "Effects"], rows).trimEnd()
4204
+ ].join("\n\n");
4205
+ }
4206
+ function inlineEffectLayerText(raw) {
4207
+ const layer = asRecord(raw);
4208
+ const type = str(layer.type) ?? "unknown";
4209
+ const num = (value) => typeof value === "number" ? String(value) : "";
4210
+ let summary;
4211
+ switch (type) {
4212
+ case "drop-shadow":
4213
+ case "inner-shadow": {
4214
+ const offset = asRecord(layer.offset);
4215
+ const color = asRecord(layer.color);
4216
+ const parts = [type, `offset ${num(offset.x)}/${num(offset.y)}`, `radius ${num(layer.radius)}`];
4217
+ if (layer.spread !== void 0) parts.push(`spread ${num(layer.spread)}`);
4218
+ if (str(color.hex)) parts.push(`${str(color.hex)} alpha ${num(color.alpha)}`);
4219
+ summary = parts.join(", ");
4220
+ break;
4221
+ }
4222
+ case "layer-blur":
4223
+ case "background-blur": {
4224
+ const progressive = layer.blurType === "progressive";
4225
+ const parts = [progressive ? `${type} (progressive)` : type, `radius ${num(layer.radius)}`];
4226
+ if (progressive) {
4227
+ const startOffset = asRecord(layer.startOffset);
4228
+ const endOffset = asRecord(layer.endOffset);
4229
+ parts.push(`start radius ${num(layer.startRadius)}`);
4230
+ parts.push(`start offset ${num(startOffset.x)}/${num(startOffset.y)}`);
4231
+ parts.push(`end offset ${num(endOffset.x)}/${num(endOffset.y)}`);
4232
+ }
4233
+ summary = parts.join(", ");
4234
+ break;
4235
+ }
4236
+ case "noise": {
4237
+ const color = asRecord(layer.color);
4238
+ const noiseType = str(layer.noiseType);
4239
+ const parts = [noiseType ? `noise (${noiseType})` : "noise"];
4240
+ if (str(color.hex)) parts.push(`${str(color.hex)} alpha ${num(color.alpha)}`);
4241
+ parts.push(`size ${num(layer.noiseSize)}`);
4242
+ parts.push(`density ${num(layer.density)}`);
4243
+ const secondary = asRecord(layer.secondaryColor);
4244
+ if (str(secondary.hex)) parts.push(`secondary ${str(secondary.hex)} alpha ${num(secondary.alpha)}`);
4245
+ if (layer.opacity !== void 0) parts.push(`opacity ${num(layer.opacity)}`);
4246
+ summary = parts.join(", ");
4247
+ break;
4248
+ }
4249
+ case "texture": {
4250
+ const parts = [
4251
+ "texture",
4252
+ `size ${num(layer.noiseSize)}`,
4253
+ `radius ${num(layer.radius)}`,
4254
+ `clip ${layer.clipToShape === true ? "true" : "false"}`
4255
+ ];
4256
+ const vector = asRecord(layer.noiseSizeVector);
4257
+ if (vector.x !== void 0) parts.push(`vector ${num(vector.x)}/${num(vector.y)}`);
4258
+ summary = parts.join(", ");
4259
+ break;
4260
+ }
4261
+ case "glass": {
4262
+ summary = [
4263
+ "glass",
4264
+ `radius ${num(layer.radius)}`,
4265
+ `light intensity ${num(layer.lightIntensity)}`,
4266
+ `light angle ${num(layer.lightAngle)}`,
4267
+ `refraction ${num(layer.refraction)}`,
4268
+ `depth ${num(layer.depth)}`,
4269
+ `dispersion ${num(layer.dispersion)}`
4270
+ ].join(", ");
4271
+ break;
4272
+ }
4273
+ default: {
4274
+ const figmaType = str(layer.figma_type);
4275
+ summary = figmaType ? `unknown (${figmaType})` : "unknown";
4276
+ }
4277
+ }
4278
+ const bindings = layer.bindings !== void 0 ? asRecord(layer.bindings) : void 0;
4279
+ const boundText = bindings ? Object.entries(bindings).map(([field, reference]) => `${field} bound to ${str(asRecord(reference).name) ?? ""}`).join(", ") : "";
4280
+ const hidden = layer.visible === false ? " (hidden)" : "";
4281
+ return `${summary}${boundText ? `, ${boundText}` : ""}${hidden}`;
4282
+ }
4283
+ function effectsInlineSection(items) {
4284
+ if (items.length === 0) return void 0;
4285
+ const rows = items.map((raw) => {
4286
+ const item = asRecord(raw);
4287
+ const path = str(item.path);
4288
+ const layers = Array.isArray(item.layers) ? item.layers : [];
4289
+ const summary = layers.map((layer) => inlineEffectLayerText(layer)).join("; ");
4290
+ return [path ? codeCell(path) : "", escapeCell(summary)];
4291
+ });
4292
+ return `## Effects
4293
+
4294
+ ${table(["Part", "Effects"], rows).trimEnd()}`;
4295
+ }
4296
+ function unboundSection(unbound) {
4297
+ const entries = Array.isArray(unbound) ? unbound : [];
4298
+ if (entries.length === 0) return void 0;
4299
+ const rows = entries.map((raw) => {
4300
+ const entry2 = asRecord(raw);
4301
+ return [
4302
+ str(entry2.path) ? codeCell(str(entry2.path)) : "",
4303
+ escapeCell(str(entry2.property) ?? ""),
4304
+ escapeCell(str(entry2.issue) ?? ""),
4305
+ entry2.value === void 0 ? "" : escapeCell(String(entry2.value))
4306
+ ];
4307
+ });
4308
+ return `## Unbound values
4309
+
4310
+ ${table(["Part", "Property", "Issue", "Value"], rows).trimEnd()}`;
4311
+ }
4312
+ function issuesSection(artifact) {
4313
+ const validation = Array.isArray(artifact.validation) ? artifact.validation : [];
4314
+ const lines = [];
4315
+ for (const raw of validation) {
4316
+ const row = asRecord(raw);
4317
+ if (str(row.id) === "unbound-value") continue;
4318
+ lines.push(issueLine(row));
4319
+ }
4320
+ return lines.length === 0 ? void 0 : `## Issues
4321
+
4322
+ ${lines.join("\n")}`;
4323
+ }
4324
+ function issueLine(row) {
4325
+ const severity = escapeInline(str(row.severity) ?? "info");
4326
+ const message = escapeInline(str(row.message) ?? "");
4327
+ const where = [
4328
+ str(row.path) ? code(str(row.path)) : void 0,
4329
+ str(row.property) ? escapeInline(str(row.property)) : void 0
4330
+ ].filter((v) => v !== void 0);
4331
+ return `- ${severity}: ${message}${where.length > 0 ? ` (${where.join(", ")})` : ""}`;
4332
+ }
4333
+ function foundationIssuesSection(rows) {
4334
+ if (!rows || rows.length === 0) return void 0;
4335
+ return `### Foundation issues
4336
+
4337
+ ${rows.map((row) => issueLine(asRecord(row))).join("\n")}`;
4338
+ }
4339
+ function tokenValueText(value) {
4340
+ if (value !== null && typeof value === "object" && !Array.isArray(value)) {
4341
+ const record = value;
4342
+ if (typeof record.missing === "string") return `missing: ${record.missing}`;
4343
+ if (typeof record.alias === "string") {
4344
+ const chain = Array.isArray(record.chain) ? record.chain.filter((step) => typeof step === "string") : [];
4345
+ const steps = chain.length === 0 ? [record.alias] : chain[0] === record.alias ? chain : [record.alias, ...chain];
4346
+ const target = steps.join(" \u2192 ");
4347
+ if (record.resolved !== void 0) return `${target} (resolved: ${valueText(record.resolved)})`;
4348
+ if (typeof record.unresolved === "string") return `${target} (unresolved: ${record.unresolved})`;
4349
+ return target;
4350
+ }
4351
+ }
4352
+ return valueText(value);
4353
+ }
4354
+ function tokensUsedSection(artifact) {
4355
+ const slice = componentFoundationAiSlice(artifact);
4356
+ if (!slice) return `## Tokens used
4357
+
4358
+ ${NOT_READ_SENTENCE}`;
4359
+ const { compact } = slice;
4360
+ const parts = ["## Tokens used"];
4361
+ parts.push(
4362
+ `Foundation: collections ${compact.completeness.collections}, styles ${compact.completeness.styles}.`
4363
+ );
4364
+ if (compact.completeness.unavailable_sources.length > 0) {
4365
+ parts.push(`Unavailable sources: ${compact.completeness.unavailable_sources.map((source) => escapeInline(source)).join(", ")}.`);
4366
+ }
4367
+ for (const collection of compact.collections) {
4368
+ parts.push(`### ${escapeInline(collection.name)}`);
4369
+ const modes = collection.modes.map((mode) => mode === collection.default_mode ? `${escapeInline(mode)} (default)` : escapeInline(mode));
4370
+ parts.push(`Modes: ${modes.join(", ")}.`);
4371
+ const rows = collection.tokens.map((token) => [
4372
+ escapeCell(token.name),
4373
+ escapeCell(token.type),
4374
+ ...collection.modes.map((mode) => escapeCell(tokenValueText(token.values[mode]))),
4375
+ token.code_syntax ? Object.entries(token.code_syntax).map(([platform, id]) => id ? `${escapeCell(platform)} ${codeCell(id)}` : escapeCell(platform)).join(", ") : ""
4376
+ ]);
4377
+ parts.push(table(
4378
+ ["Token", "Type", ...collection.modes.map((mode) => escapeCell(mode)), "Code syntax"],
4379
+ rows
4380
+ ).trimEnd());
4381
+ }
4382
+ const typography = typographySection(compact.styles.typography);
4383
+ if (typography) parts.push(typography);
4384
+ const effects = effectsSection(compact.styles.effects);
4385
+ if (effects) parts.push(effects);
4386
+ const issues = foundationIssuesSection(compact.validation);
4387
+ if (issues) parts.push(issues);
4388
+ return parts.join("\n\n");
4389
+ }
4390
+ var AI_MARKER = "*Written by AI from the extracted facts, not read from Figma.*";
4391
+ var ATX_H1_H2 = /^ {0,3}(#{1,2})(?= |$)/;
4392
+ var FENCE_MARKER = /^(`{3,}|~{3,})/;
4393
+ var SETEXT_UNDERLINE = /^(?:=+|-+)$/;
4394
+ function demote(blob) {
4395
+ const lines = blob.split("\n");
4396
+ const out = [];
4397
+ let inFence = false;
4398
+ const isHeadingOrFence = (line) => {
4399
+ if (line === void 0) return false;
4400
+ const trimmed = line.trim();
4401
+ return /^#{1,6}(?= |$)/.test(trimmed) || FENCE_MARKER.test(trimmed);
4402
+ };
4403
+ for (const rawLine of lines) {
4404
+ const trimmed = rawLine.trim();
4405
+ if (FENCE_MARKER.test(trimmed)) {
4406
+ inFence = !inFence;
4407
+ out.push(rawLine);
4408
+ continue;
4409
+ }
4410
+ if (inFence) {
4411
+ out.push(rawLine);
4412
+ continue;
4413
+ }
4414
+ if (SETEXT_UNDERLINE.test(trimmed)) {
4415
+ const previous = out[out.length - 1];
4416
+ const previousIsBlank = previous === void 0 || previous.trim() === "";
4417
+ if (!previousIsBlank && !isHeadingOrFence(previous)) {
4418
+ out[out.length - 1] = `### ${previous.trim()}`;
4419
+ continue;
4420
+ }
4421
+ out.push(rawLine);
4422
+ continue;
4423
+ }
4424
+ out.push(rawLine.replace(ATX_H1_H2, "###"));
4425
+ }
4426
+ return out.join("\n").trimEnd();
4427
+ }
4428
+ function proseSection(heading, blob) {
4429
+ return `## ${heading}
4430
+
4431
+ ${AI_MARKER}
4432
+
4433
+ ${demote(blob)}`;
4434
+ }
4435
+ function overviewBlock(guidelines) {
4436
+ const blob = str(guidelines.definition);
4437
+ return blob ? proseSection("Overview", blob) : void 0;
4438
+ }
4439
+ function anatomyProseParagraph(guidelines) {
4440
+ const blob = str(guidelines.anatomy_summary);
4441
+ return blob ? `${AI_MARKER}
4442
+
4443
+ ${demote(blob)}` : void 0;
4444
+ }
4445
+ function ruleList(items) {
4446
+ const indent = (blob) => blob.split("\n").map((line, index) => index === 0 || line === "" ? line : ` ${line}`).join("\n");
4447
+ return items.map((item) => `- ${indent(demote(String(item)))}`).join("\n");
4448
+ }
4449
+ function restProseBlocks(guidelines) {
4450
+ const blocks = [];
4451
+ const add = (heading, key) => {
4452
+ const blob = str(guidelines[key]);
4453
+ if (blob) blocks.push(proseSection(heading, blob));
4454
+ };
4455
+ add("Variants", "variants_summary");
4456
+ const dos = Array.isArray(guidelines.dos) ? guidelines.dos : [];
4457
+ const donts = Array.isArray(guidelines.donts) ? guidelines.donts : [];
4458
+ if (dos.length > 0 || donts.length > 0) {
4459
+ const parts = [`## Do and don't`, AI_MARKER];
4460
+ if (dos.length > 0) parts.push("### Do", ruleList(dos));
4461
+ if (donts.length > 0) parts.push(`### Don't`, ruleList(donts));
4462
+ blocks.push(parts.join("\n\n"));
4463
+ }
4464
+ add("Accessibility", "accessibility");
4465
+ add("Interactions", "interactions");
4466
+ add("Content considerations", "content_considerations");
4467
+ add("Design considerations", "design_considerations");
4468
+ return blocks;
4469
+ }
4470
+ function frontMatter(artifact) {
4471
+ const envelope = componentEnvelope(artifact, "markdown");
4472
+ return `---
4473
+ ${toYaml(envelope)}---
4474
+ `;
4475
+ }
4476
+ function componentMarkdown(artifact) {
4477
+ const component = asRecord(artifact.component);
4478
+ const guidelines = asRecord(artifact.guidelines);
4479
+ const blocks = [];
4480
+ const name = str(component.name);
4481
+ if (name === void 0) throw new Error("componentMarkdown needs component.name; the artifact has none.");
4482
+ if (!Array.isArray(artifact.anatomy)) {
4483
+ throw new Error("componentMarkdown needs anatomy as an array; the artifact has none.");
4484
+ }
4485
+ blocks.push(`# ${escapeHeading(name)}`);
4486
+ const description = str(component.description);
4487
+ if (description) blocks.push(escapeBlock(description));
4488
+ const related = Array.isArray(component.related) ? component.related.filter((r) => typeof r === "string") : [];
4489
+ if (related.length > 0) {
4490
+ blocks.push(`Related: ${related.map((r) => escapeInline(r)).join(", ")}`);
4491
+ }
4492
+ const overview = overviewBlock(guidelines);
4493
+ if (overview) blocks.push(overview);
4494
+ if (artifact.api !== void 0) {
4495
+ const section = propertiesSection(asRecord(artifact.api));
4496
+ if (section) blocks.push(section);
4497
+ }
4498
+ if (artifact.anatomy.length > 0) {
4499
+ const anatomyProse = anatomyProseParagraph(guidelines);
4500
+ const bullets = anatomyBullets(artifact.anatomy, 0).join("\n");
4501
+ const body = anatomyProse ? `${anatomyProse}
4502
+
4503
+ ${bullets}` : bullets;
4504
+ blocks.push(`## Anatomy
4505
+
4506
+ ${body}`);
4507
+ }
4508
+ if (artifact.layout !== void 0) {
4509
+ const section = layoutSection(asRecord(artifact.layout));
4510
+ if (section) blocks.push(section);
4511
+ }
4512
+ const bindings = bindingsSection(asRecord(artifact.references));
4513
+ if (bindings) blocks.push(bindings);
4514
+ blocks.push(tokensUsedSection(artifact));
4515
+ if (artifact.effects_inline !== void 0) {
4516
+ const section = effectsInlineSection(
4517
+ Array.isArray(artifact.effects_inline) ? artifact.effects_inline : []
4518
+ );
4519
+ if (section) blocks.push(section);
4520
+ }
4521
+ const unbound = unboundSection(artifact.unbound);
4522
+ if (unbound) blocks.push(unbound);
4523
+ const issues = issuesSection(artifact);
4524
+ if (issues) blocks.push(issues);
4525
+ blocks.push(...restProseBlocks(guidelines));
4526
+ return `${frontMatter(artifact)}
4527
+ ${blocks.join("\n\n")}
4528
+ `;
4529
+ }
4530
+
2321
4531
  // ../extractor/src/v5/usageUnits.ts
2322
4532
  var LENGTH_SCOPES = ["CORNER_RADIUS", "WIDTH_HEIGHT", "GAP", "FONT_SIZE", "STROKE_FLOAT"];
2323
4533
  var LENGTH_PROPERTIES = [
@@ -2538,12 +4748,12 @@ function acceptCssDeclared(declared) {
2538
4748
  if (BARE_IDENT.test(declared)) return `--${declared}`;
2539
4749
  return null;
2540
4750
  }
2541
- var asRecord = (v) => typeof v === "object" && v !== null && !Array.isArray(v) ? v : null;
4751
+ var asRecord2 = (v) => typeof v === "object" && v !== null && !Array.isArray(v) ? v : null;
2542
4752
  function collectLeaves(tree, prefix, out) {
2543
- const node = asRecord(tree);
4753
+ const node = asRecord2(tree);
2544
4754
  if (!node) return;
2545
4755
  if ("$value" in node) {
2546
- const ours = asRecord(asRecord(node.$extensions)?.[EXT]);
4756
+ const ours = asRecord2(asRecord2(node.$extensions)?.[EXT]);
2547
4757
  out.push({
2548
4758
  path: prefix.join("."),
2549
4759
  type: typeof node.$type === "string" ? node.$type : "",
@@ -2559,7 +4769,7 @@ function collectLeaves(tree, prefix, out) {
2559
4769
  }
2560
4770
  var unpointer = (s) => s.replace(/~1/g, "/").replace(/~0/g, "~");
2561
4771
  var refFile = (src) => {
2562
- const r = asRecord(src);
4772
+ const r = asRecord2(src);
2563
4773
  return r && typeof r.$ref === "string" ? r.$ref : null;
2564
4774
  };
2565
4775
  function sourcesOf(resolver) {
@@ -2634,7 +4844,7 @@ function cssValue(ctx, type, value, property) {
2634
4844
  switch (type) {
2635
4845
  case "color": {
2636
4846
  if (typeof value === "string") return value;
2637
- const c = asRecord(value);
4847
+ const c = asRecord2(value);
2638
4848
  if (c && typeof c.hex === "string" && typeof c.alpha === "number") {
2639
4849
  if (c.alpha === 1) return c.hex;
2640
4850
  const [r, g, b] = hexChannels(c.hex);
@@ -2645,7 +4855,7 @@ function cssValue(ctx, type, value, property) {
2645
4855
  case "dimension":
2646
4856
  case "duration": {
2647
4857
  if (typeof value === "string") return value;
2648
- const d = asRecord(value);
4858
+ const d = asRecord2(value);
2649
4859
  if (d && typeof d.value === "number" && typeof d.unit === "string") return `${d.value}${d.unit}`;
2650
4860
  break;
2651
4861
  }
@@ -2707,7 +4917,7 @@ function typographyMemberKeys(value, ext) {
2707
4917
  }
2708
4918
  for (const key of ["lineHeight", "letterSpacing"]) {
2709
4919
  if (key in value) continue;
2710
- const d = asRecord(ext[key]);
4920
+ const d = asRecord2(ext[key]);
2711
4921
  if (d && typeof d.value === "number" && typeof d.unit === "string") keys.push(key);
2712
4922
  }
2713
4923
  for (const [key, extKey] of TEXT_MEMBERS) {
@@ -2724,7 +4934,7 @@ function converted(ctx, property, from, to) {
2724
4934
  });
2725
4935
  }
2726
4936
  function extensionDimension(ctx, ext, key, name, percentTo) {
2727
- const d = asRecord(ext[key]);
4937
+ const d = asRecord2(ext[key]);
2728
4938
  if (!d || typeof d.value !== "number" || typeof d.unit !== "string") return null;
2729
4939
  if (d.unit === "%") {
2730
4940
  const to = percentTo(canonicalNumber(d.value / 100));
@@ -2736,7 +4946,7 @@ function extensionDimension(ctx, ext, key, name, percentTo) {
2736
4946
  function typographyDecls(ctx, leaf, names) {
2737
4947
  const decls = [];
2738
4948
  const declaredPaths = [];
2739
- const value = asRecord(leaf.value) ?? {};
4949
+ const value = asRecord2(leaf.value) ?? {};
2740
4950
  const ext = leaf.ext ?? {};
2741
4951
  const nameFor = (key) => names.get(`${leaf.path}.${key}`);
2742
4952
  for (const [key, type] of TYPOGRAPHY_MEMBERS2) {
@@ -2769,12 +4979,12 @@ function typographyDecls(ctx, leaf, names) {
2769
4979
  }
2770
4980
  }
2771
4981
  }
2772
- for (const [key, extKey, table] of TEXT_MEMBERS) {
4982
+ for (const [key, extKey, table2] of TEXT_MEMBERS) {
2773
4983
  const raw = ext[extKey];
2774
4984
  if (typeof raw !== "string") continue;
2775
4985
  const name = nameFor(key);
2776
4986
  if (name === void 0) continue;
2777
- const css = table[raw];
4987
+ const css = table2[raw];
2778
4988
  if (css !== void 0) {
2779
4989
  decls.push(`${name}: ${css};`);
2780
4990
  declaredPaths.push(`${leaf.path}.${key}`);
@@ -2805,7 +5015,7 @@ function shadowDecl(ctx, leaf, name) {
2805
5015
  if (layers.length === 0) return omit("no_visible_shadow", "The style has no visible shadow, so no box-shadow was written.");
2806
5016
  const parts = [];
2807
5017
  for (const layer of layers) {
2808
- const l = asRecord(layer);
5018
+ const l = asRecord2(layer);
2809
5019
  if (!l) return omit("layer_not_an_object", "A shadow layer is not an object; the style was omitted.");
2810
5020
  const members = [];
2811
5021
  for (const [key, type] of SHADOW_MEMBERS) {
@@ -2930,7 +5140,7 @@ function cssOutput(exp, header, options = {}) {
2930
5140
  for (const leaves of leavesByFile.values()) {
2931
5141
  for (const leaf of leaves) {
2932
5142
  if (leaf.type === "typography") {
2933
- const value = asRecord(leaf.value) ?? {};
5143
+ const value = asRecord2(leaf.value) ?? {};
2934
5144
  const ext = leaf.ext ?? {};
2935
5145
  for (const key of typographyMemberKeys(value, ext)) candidatePaths.add(`${leaf.path}.${key}`);
2936
5146
  } else {
@@ -3002,16 +5212,13 @@ ${imports.join("\n")}
3002
5212
  return { files, map, report: sortReport(entries) };
3003
5213
  }
3004
5214
 
3005
- // ../extractor/src/v5/componentContext.ts
3006
- var import_js_sha2564 = __toESM(require_sha256(), 1);
3007
-
3008
5215
  // ../extractor/src/libraryBundle.ts
3009
5216
  var LIBRARY_BUNDLE_SCHEMA = "spec-layer-library-bundle";
3010
5217
  var LibraryBundleError = class extends Error {
3011
- constructor(code2, message) {
5218
+ constructor(code3, message) {
3012
5219
  super(message);
3013
5220
  this.name = "LibraryBundleError";
3014
- this.code = code2;
5221
+ this.code = code3;
3015
5222
  }
3016
5223
  };
3017
5224
  var isRecord3 = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
@@ -3020,11 +5227,26 @@ function hasContentHash(artifact) {
3020
5227
  const exp = artifact.spec_layer.export;
3021
5228
  return isRecord3(exp) && typeof exp.content_hash === "string";
3022
5229
  }
5230
+ function readVariants(value) {
5231
+ if (!Array.isArray(value)) return void 0;
5232
+ const out = [];
5233
+ for (const item of value) {
5234
+ if (!isRecord3(item) || typeof item.name !== "string" || !isRecord3(item.values)) return void 0;
5235
+ const values = {};
5236
+ for (const [axis, v] of Object.entries(item.values)) {
5237
+ if (typeof v !== "string") return void 0;
5238
+ values[axis] = v;
5239
+ }
5240
+ out.push({ name: item.name, values });
5241
+ }
5242
+ return out;
5243
+ }
3023
5244
  function entry(v, where) {
3024
5245
  if (!isRecord3(v) || typeof v.name !== "string" || typeof v.ai !== "string" || !hasContentHash(v.artifact)) {
3025
5246
  throw new LibraryBundleError("malformed", `The ${where} entry in this bundle is malformed.`);
3026
5247
  }
3027
- return { name: v.name, ai: v.ai, artifact: v.artifact };
5248
+ const variants = readVariants(v.variants);
5249
+ return { name: v.name, ai: v.ai, artifact: v.artifact, ...variants ? { variants } : {} };
3028
5250
  }
3029
5251
  function supportedVersion(version) {
3030
5252
  return typeof version === "string" && /^1\.\d+\.\d+$/.test(version);
@@ -3073,6 +5295,39 @@ function parseLibraryBundle(input) {
3073
5295
  // ../extractor/src/libraryBundleHash.ts
3074
5296
  var import_js_sha2565 = __toESM(require_sha256(), 1);
3075
5297
 
5298
+ // ../extractor/src/componentSlugs.ts
5299
+ var DASH = 45;
5300
+ function slugify(name) {
5301
+ const collapsed = name.toLowerCase().replace(/[^a-z0-9]+/g, "-");
5302
+ let start = 0;
5303
+ let end = collapsed.length;
5304
+ while (start < end && collapsed.charCodeAt(start) === DASH) start += 1;
5305
+ while (end > start && collapsed.charCodeAt(end - 1) === DASH) end -= 1;
5306
+ const slug2 = collapsed.slice(start, end);
5307
+ return slug2 || "component";
5308
+ }
5309
+ function componentSlugs(names) {
5310
+ const usedSlugs = /* @__PURE__ */ new Set();
5311
+ const nextSuffix = /* @__PURE__ */ new Map();
5312
+ return names.map((name) => {
5313
+ const base = slugify(name);
5314
+ let slug2 = base;
5315
+ if (usedSlugs.has(slug2)) {
5316
+ let n = (nextSuffix.get(base) ?? 1) + 1;
5317
+ slug2 = `${base}-${n}`;
5318
+ while (usedSlugs.has(slug2)) {
5319
+ n += 1;
5320
+ slug2 = `${base}-${n}`;
5321
+ }
5322
+ nextSuffix.set(base, n);
5323
+ } else {
5324
+ nextSuffix.set(base, 1);
5325
+ }
5326
+ usedSlugs.add(slug2);
5327
+ return slug2;
5328
+ });
5329
+ }
5330
+
3076
5331
  // src/bundle.ts
3077
5332
  function parseBundle(raw) {
3078
5333
  try {
@@ -3393,15 +5648,17 @@ var inside = (parent, child) => {
3393
5648
  return rel === "" || !rel.startsWith("..") && !isAbsolute(rel);
3394
5649
  };
3395
5650
  var isDotfile = (name) => name.startsWith(".");
5651
+ var markerList = (marker) => typeof marker === "string" ? [marker] : marker;
3396
5652
  function carriesMarker(abs, marker) {
3397
- const newlines = (marker.match(/\n/g) ?? []).length;
3398
- const wantBytes = Buffer.byteLength(marker, "utf8") + newlines;
5653
+ const markers = markerList(marker);
5654
+ if (markers.length === 0) return false;
5655
+ const wantBytes = Math.max(...markers.map((m) => Buffer.byteLength(m, "utf8") + (m.match(/\n/g) ?? []).length));
3399
5656
  const fd = openSync(abs, "r");
3400
5657
  try {
3401
5658
  const buf = Buffer.alloc(wantBytes);
3402
5659
  const read = readSync(fd, buf, 0, wantBytes, 0);
3403
5660
  const text = buf.subarray(0, read).toString("utf8").replace(/\r\n/g, "\n");
3404
- return text.startsWith(marker);
5661
+ return markers.some((m) => text.startsWith(m));
3405
5662
  } finally {
3406
5663
  closeSync(fd);
3407
5664
  }
@@ -3478,18 +5735,18 @@ function parseOutput(value, index) {
3478
5735
  }
3479
5736
  const spec = specOf(r.platform, r.format);
3480
5737
  if (!spec) throw new Error(`${at}: unknown platform/format "${r.platform}/${r.format}". Known: ${knownFormats()}.`);
3481
- const str = (key) => {
5738
+ const str2 = (key) => {
3482
5739
  if (r[key] !== void 0 && typeof r[key] !== "string") throw new Error(`${at} "${key}" must be a string.`);
3483
5740
  return r[key];
3484
5741
  };
3485
- const path = str("path") ?? spec.defaultPath;
5742
+ const path = str2("path") ?? spec.defaultPath;
3486
5743
  if (path === null) throw new Error(`${at} needs "path": ${spec.platform} has no default location.`);
3487
- const nameCase = str("case");
5744
+ const nameCase = str2("case");
3488
5745
  if (nameCase !== void 0 && !NAME_CASES.includes(nameCase)) {
3489
5746
  throw new Error(`${at} "case" takes ${NAME_CASES.join(", ")}.`);
3490
5747
  }
3491
- const root = str("root");
3492
- const modeSelector = str("modeSelector");
5748
+ const root = str2("root");
5749
+ const modeSelector = str2("modeSelector");
3493
5750
  let modes;
3494
5751
  if (r.modes !== void 0) {
3495
5752
  const m = r.modes;
@@ -3545,6 +5802,11 @@ var DEFAULT_API = "https://api.spec-layer.com";
3545
5802
  var DEFAULT_OUT_DIR = ".speclayer";
3546
5803
  var DEFAULT_COMPONENT_SPECS_DIR = "component-specs";
3547
5804
  var CONFIG_NAME = "speclayer.json";
5805
+ var COMPONENT_FORMATS = ["yaml", "md"];
5806
+ var DEFAULT_COMPONENT_FORMAT = "yaml";
5807
+ function isComponentFormat(value) {
5808
+ return typeof value === "string" && COMPONENT_FORMATS.includes(value);
5809
+ }
3548
5810
  var invalidConfig = () => new Error(`${CONFIG_NAME} is not valid JSON. Fix or delete it, then retry.`);
3549
5811
  function parseInclude(value) {
3550
5812
  if (typeof value !== "object" || value === null || Array.isArray(value)) throw invalidConfig();
@@ -3583,6 +5845,10 @@ function parseComponentSpecsDir(value) {
3583
5845
  if (dir.length === 0) throw new Error('speclayer.json "componentSpecsDir" must be a non-empty string.');
3584
5846
  return dir;
3585
5847
  }
5848
+ function parseComponentSpecsFormat(value) {
5849
+ if (!isComponentFormat(value)) throw new Error('speclayer.json "componentSpecsFormat" must be "yaml" or "md".');
5850
+ return value;
5851
+ }
3586
5852
  function parsePlatforms(value) {
3587
5853
  if (!Array.isArray(value) || !value.every((p) => typeof p === "string" && isPlatform(p))) {
3588
5854
  throw new Error(`speclayer.json "platforms" must be an array of ${PLATFORMS.join(", ")}.`);
@@ -3617,6 +5883,7 @@ function readConfig(cwd) {
3617
5883
  ...typeof record.libraryId === "string" ? { libraryId: record.libraryId } : {},
3618
5884
  ...typeof record.outDir === "string" ? { outDir: record.outDir } : {},
3619
5885
  ...record.componentSpecsDir !== void 0 ? { componentSpecsDir: parseComponentSpecsDir(record.componentSpecsDir) } : {},
5886
+ ...record.componentSpecsFormat !== void 0 ? { componentSpecsFormat: parseComponentSpecsFormat(record.componentSpecsFormat) } : {},
3620
5887
  ...record.include !== void 0 ? { include: parseInclude(record.include) } : {},
3621
5888
  ...record.dtcg !== void 0 ? { dtcg: parseDtcg(record.dtcg) } : {},
3622
5889
  ...record.platforms !== void 0 ? { platforms: parsePlatforms(record.platforms) } : {},
@@ -3628,6 +5895,7 @@ function writeConfig(cwd, config) {
3628
5895
  libraryId: config.libraryId,
3629
5896
  outDir: config.outDir,
3630
5897
  ...config.componentSpecsDir ? { componentSpecsDir: config.componentSpecsDir } : {},
5898
+ ...config.componentSpecsFormat ? { componentSpecsFormat: config.componentSpecsFormat } : {},
3631
5899
  ...config.include ? { include: config.include } : {},
3632
5900
  ...config.dtcg ? { dtcg: config.dtcg } : {},
3633
5901
  ...config.platforms && config.platforms.length > 0 ? { platforms: config.platforms } : {},
@@ -3657,6 +5925,7 @@ function resolveOptions(cwd, flags, env, manifestLibraryId) {
3657
5925
  // A trailing slash would build "//v1/..." paths the proxy router 404s on.
3658
5926
  api: (flags.api ?? env.SPEC_LAYER_API ?? DEFAULT_API).replace(/\/+$/, ""),
3659
5927
  key: supplied ?? storedKey,
5928
+ ...config?.componentSpecsFormat ? { componentSpecsFormat: config.componentSpecsFormat } : {},
3660
5929
  ...config?.include ? { include: config.include } : {},
3661
5930
  ...config?.dtcg ? { dtcg: config.dtcg } : {},
3662
5931
  ...config?.platforms ? { platforms: config.platforms } : {},
@@ -3680,7 +5949,8 @@ async function fetchBundle(opts) {
3680
5949
  } catch {
3681
5950
  return { kind: "error", message: `Could not reach ${opts.api}.` };
3682
5951
  }
3683
- if (res.status === 304) return { kind: "not_modified" };
5952
+ const version = res.headers.get("X-Library-Version");
5953
+ if (res.status === 304) return { kind: "not_modified", version };
3684
5954
  if (res.status === 401) {
3685
5955
  return {
3686
5956
  kind: "error",
@@ -3694,7 +5964,8 @@ async function fetchBundle(opts) {
3694
5964
  kind: "ok",
3695
5965
  raw,
3696
5966
  publishedAt: res.headers.get("X-Published-At") ?? "unknown",
3697
- bundleHash: createHash("sha256").update(raw).digest("hex")
5967
+ bundleHash: createHash("sha256").update(raw).digest("hex"),
5968
+ version
3698
5969
  };
3699
5970
  }
3700
5971
 
@@ -3745,11 +6016,21 @@ function cliVersion() {
3745
6016
  }
3746
6017
 
3747
6018
  // src/files.ts
3748
- function slugify(name) {
3749
- const slug2 = name.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
3750
- return slug2 || "component";
6019
+ var COMPONENT_SPEC_MARKER = COMPONENT_YAML_MARKER;
6020
+ var COMPONENT_SPEC_MARKERS = [COMPONENT_YAML_MARKER, COMPONENT_MARKDOWN_MARKER];
6021
+ function componentMarkdownPage(component) {
6022
+ const failed = () => new Error(
6023
+ `The published component context for ${component.name} could not be rendered as Markdown. Republish from the plugin, then pull again.`
6024
+ );
6025
+ let page;
6026
+ try {
6027
+ page = componentMarkdown(component.artifact);
6028
+ } catch {
6029
+ throw failed();
6030
+ }
6031
+ if (!page.startsWith(COMPONENT_MARKDOWN_MARKER)) throw failed();
6032
+ return page;
3751
6033
  }
3752
- var COMPONENT_SPEC_MARKER = "spec_layer:\n kind: component";
3753
6034
  function readManifest(outDir) {
3754
6035
  const path = join5(outDir, "manifest.json");
3755
6036
  if (!existsSync5(path)) return null;
@@ -3776,27 +6057,6 @@ function readLocalBundle(outDir) {
3776
6057
  throw new Error(`${path} could not be read as a library bundle. Run spec-layer pull again.`);
3777
6058
  }
3778
6059
  }
3779
- function componentSlugs(bundle) {
3780
- const usedSlugs = /* @__PURE__ */ new Set();
3781
- const nextSuffix = /* @__PURE__ */ new Map();
3782
- return bundle.components.map((component) => {
3783
- const base = slugify(component.name);
3784
- let slug2 = base;
3785
- if (usedSlugs.has(slug2)) {
3786
- let n = (nextSuffix.get(base) ?? 1) + 1;
3787
- slug2 = `${base}-${n}`;
3788
- while (usedSlugs.has(slug2)) {
3789
- n += 1;
3790
- slug2 = `${base}-${n}`;
3791
- }
3792
- nextSuffix.set(base, n);
3793
- } else {
3794
- nextSuffix.set(base, 1);
3795
- }
3796
- usedSlugs.add(slug2);
3797
- return slug2;
3798
- });
3799
- }
3800
6060
  function assertReplaceable(outDir, cwd) {
3801
6061
  const rel = relative2(resolve3(cwd), resolve3(outDir));
3802
6062
  if (rel === "" || rel.startsWith("..") || isAbsolute2(rel)) {
@@ -3810,12 +6070,13 @@ function writeBundleFiles(opts) {
3810
6070
  assertReplaceable(opts.outDir, opts.cwd);
3811
6071
  const selection = opts.selection ?? DEFAULT_SELECTION;
3812
6072
  const selected = selectComponents(opts.bundle, selection);
3813
- const slugs = componentSlugs(opts.bundle);
6073
+ const slugs = componentSlugs(opts.bundle.components.map((c) => c.name));
3814
6074
  const outputs = opts.outputs ?? [];
3815
6075
  const componentSpecsDir = opts.componentSpecsDir ?? DEFAULT_COMPONENT_SPECS_DIR;
6076
+ const componentSpecsFormat = opts.componentSpecsFormat ?? DEFAULT_COMPONENT_FORMAT;
3816
6077
  const outDirRel = relative2(resolve3(opts.cwd), resolve3(opts.outDir)).split(sep).join("/");
3817
6078
  const outputPaths = outputs.map((o) => o.path);
3818
- const specsProblem = visibleDirProblem(opts.cwd, outDirRel, componentSpecsDir, COMPONENT_SPEC_MARKER, outputPaths, "componentSpecsDir");
6079
+ const specsProblem = visibleDirProblem(opts.cwd, outDirRel, componentSpecsDir, COMPONENT_SPEC_MARKERS, outputPaths, "componentSpecsDir");
3819
6080
  if (specsProblem) throw new Error(specsProblem);
3820
6081
  for (const o of outputs) {
3821
6082
  const problem = outputPathProblem(opts.cwd, outDirRel, o, [componentSpecsDir, ...outputPaths.filter((p) => p !== o.path)]);
@@ -3824,6 +6085,10 @@ function writeBundleFiles(opts) {
3824
6085
  const briefs = {};
3825
6086
  opts.bundle.components.forEach((component, i) => {
3826
6087
  if (!selected[i]) return;
6088
+ if (componentSpecsFormat === "md") {
6089
+ briefs[`${slugs[i]}.md`] = componentMarkdownPage(component);
6090
+ return;
6091
+ }
3827
6092
  if (!component.ai.startsWith(COMPONENT_SPEC_MARKER)) {
3828
6093
  throw new Error(`The published brief for ${component.name} does not begin with the Spec Layer marker. Republish from the plugin, then pull again.`);
3829
6094
  }
@@ -3879,7 +6144,7 @@ function writeBundleFiles(opts) {
3879
6144
  kind: "component",
3880
6145
  name: component.name,
3881
6146
  contentHash: component.artifact.spec_layer.export.content_hash,
3882
- path: selected[i] ? `${componentSpecsDir}/${slugs[i]}.yaml` : null
6147
+ path: selected[i] ? `${componentSpecsDir}/${slugs[i]}.${componentSpecsFormat}` : null
3883
6148
  });
3884
6149
  });
3885
6150
  const manifest = {
@@ -3891,7 +6156,9 @@ function writeBundleFiles(opts) {
3891
6156
  cliVersion: cliVersion(),
3892
6157
  selection,
3893
6158
  componentSpecsDir,
6159
+ componentSpecsFormat,
3894
6160
  artifacts,
6161
+ ...opts.version ? { version: opts.version } : {},
3895
6162
  ...opts.dtcg && Object.keys(opts.dtcg).length > 0 ? { dtcg: opts.dtcg } : {},
3896
6163
  ...opts.platforms && opts.platforms.length > 0 ? { platforms: opts.platforms } : {},
3897
6164
  ...opts.outputs ? { outputs: opts.outputs } : {}
@@ -3903,7 +6170,7 @@ function writeBundleFiles(opts) {
3903
6170
  }
3904
6171
  rmSync2(opts.outDir, { recursive: true, force: true });
3905
6172
  renameSync2(staging, opts.outDir);
3906
- const componentSpecs = { path: componentSpecsDir, files: writeVisibleDir(opts.cwd, componentSpecsDir, COMPONENT_SPEC_MARKER, briefs) };
6173
+ const componentSpecs = { path: componentSpecsDir, files: writeVisibleDir(opts.cwd, componentSpecsDir, COMPONENT_SPEC_MARKERS, briefs) };
3907
6174
  const outputResults = [];
3908
6175
  for (const d of deliverables) {
3909
6176
  outputResults.push({ path: d.output.path, files: writeVisibleDir(opts.cwd, d.output.path, CSS_HEADER_PREFIX, d.files, CSS_INDEX_FILE) });
@@ -3979,7 +6246,7 @@ var PULL_EXITS = {
3979
6246
  var TOOLS = [
3980
6247
  {
3981
6248
  name: "setup",
3982
- usage: "spec-layer setup --id lib_... --key sl_... [--out DIR] [--platform web|ios|android|flutter]... [--only foundation|components] [--component NAME]...",
6249
+ usage: "spec-layer setup --id lib_... --key sl_... [--out DIR] [--platform web|ios|android|flutter]... [--component-format yaml|md] [--only foundation|components] [--component NAME]...",
3983
6250
  summary: "Records the library id, stores the pull key in a gitignored speclayer.local.json, then pulls.",
3984
6251
  when: "Once, with the command the plugin's Publish screen hands out. Re-run it after the key is rotated.",
3985
6252
  network: true,
@@ -3996,7 +6263,7 @@ var TOOLS = [
3996
6263
  },
3997
6264
  {
3998
6265
  name: "init",
3999
- usage: "spec-layer init --id lib_... [--out DIR] [--platform web|ios|android|flutter]... [--only foundation|components] [--component NAME]...",
6266
+ usage: "spec-layer init --id lib_... [--out DIR] [--platform web|ios|android|flutter]... [--component-format yaml|md] [--only foundation|components] [--component NAME]...",
4000
6267
  summary: "Writes speclayer.json, with the platforms and default outputs, so later commands need no flags. Stores no key and reaches no server.",
4001
6268
  when: "A repo that supplies the key from SPEC_LAYER_KEY instead of a stored file.",
4002
6269
  network: false,
@@ -4006,9 +6273,9 @@ var TOOLS = [
4006
6273
  },
4007
6274
  {
4008
6275
  name: "pull",
4009
- usage: "spec-layer pull [--id lib_...] [--key sl_...] [--out DIR] [--platform web|ios|android|flutter]... [--only foundation|components] [--component NAME]... [--strict]",
4010
- summary: "Fetches the published library and writes the record under the output directory (default .speclayer/), the briefs under componentSpecsDir, and the token files under outputs[].path. Prints a severity summary to stderr, even on a cached pull, when tokens/report.json or an outputs/*.report.json holds an error or warning.",
4011
- when: "After setup, whenever status says the local copy is behind, or after changing the include, dtcg, outputs, or componentSpecsDir blocks. Add --strict in CI to fail the build on an error-severity report entry.",
6276
+ usage: "spec-layer pull [--id lib_...] [--key sl_...] [--out DIR] [--platform web|ios|android|flutter]... [--component-format yaml|md] [--only foundation|components] [--component NAME]... [--strict]",
6277
+ summary: "Fetches the published library and writes the record under the output directory (default .speclayer/), the briefs under componentSpecsDir (YAML, or Markdown when componentSpecsFormat or --component-format says md), and the token files under outputs[].path. Prints a severity summary to stderr, even on a cached pull, when tokens/report.json or an outputs/*.report.json holds an error or warning.",
6278
+ when: "After setup, whenever status says the local copy is behind, or after changing the include, dtcg, outputs, componentSpecsDir, or componentSpecsFormat settings. Add --strict in CI to fail the build on an error-severity report entry.",
4012
6279
  network: true,
4013
6280
  needsKey: true,
4014
6281
  writes: [
@@ -4040,8 +6307,8 @@ var TOOLS = [
4040
6307
  },
4041
6308
  {
4042
6309
  name: "show",
4043
- usage: "spec-layer show foundation | component NAME [--canonical] [--out DIR]",
4044
- summary: "Prints one artifact to stdout: the Foundation DTCG document, or one component's AI YAML; --canonical prints the v5 JSON.",
6310
+ usage: "spec-layer show foundation | component NAME [--component-format yaml|md] [--canonical] [--out DIR]",
6311
+ summary: "Prints one artifact to stdout: the Foundation DTCG document, or one component's AI YAML or Markdown (by --component-format, then componentSpecsFormat, then the last pull); --canonical prints the v5 JSON.",
4045
6312
  when: "To read one component or the token document without opening files; it pipes cleanly.",
4046
6313
  network: false,
4047
6314
  needsKey: false,
@@ -4081,7 +6348,7 @@ function toolsText() {
4081
6348
  lines.push(` ${tool.summary}`);
4082
6349
  lines.push(` When: ${tool.when}`);
4083
6350
  lines.push(` Network: ${tool.network ? "yes" : "no"}. Key: ${tool.needsKey ? "required" : "not needed"}. Writes: ${tool.writes.length ? tool.writes.join(", ") : "nothing"}.`);
4084
- lines.push(` Exits: ${Object.entries(tool.exits).map(([code2, meaning]) => `${code2} ${meaning}`).join("; ")}.`);
6351
+ lines.push(` Exits: ${Object.entries(tool.exits).map(([code3, meaning]) => `${code3} ${meaning}`).join("; ")}.`);
4085
6352
  lines.push("");
4086
6353
  }
4087
6354
  lines.push("Flags every command accepts where they apply:");
@@ -4209,6 +6476,7 @@ function summarizePull(cwd, outDir, manifest) {
4209
6476
  publishedAt: manifest.publishedAt,
4210
6477
  pluginVersion: manifest.pluginVersion,
4211
6478
  componentSpecsDir: manifest.componentSpecsDir ?? DEFAULT_COMPONENT_SPECS_DIR,
6479
+ componentSpecsFormat: manifest.componentSpecsFormat ?? DEFAULT_COMPONENT_FORMAT,
4212
6480
  components,
4213
6481
  foundation,
4214
6482
  outputs: (manifest.outputs ?? []).map((o) => {
@@ -4237,7 +6505,7 @@ function summarizePull(cwd, outDir, manifest) {
4237
6505
  })
4238
6506
  };
4239
6507
  }
4240
- var code = (s) => `\`${s}\``;
6508
+ var code2 = (s) => `\`${s}\``;
4241
6509
  function stackSection(input) {
4242
6510
  const { profile, platforms, platformSource, pull } = input;
4243
6511
  const lines = ["## This codebase", ""];
@@ -4247,7 +6515,7 @@ function stackSection(input) {
4247
6515
  );
4248
6516
  } else {
4249
6517
  lines.push("Detected from the repository root (the file that carries each signal is named, and nothing deeper was read):", "");
4250
- for (const e of profile.evidence) lines.push(`- ${e.signal} (${code(e.file)})`);
6518
+ for (const e of profile.evidence) lines.push(`- ${e.signal} (${code2(e.file)})`);
4251
6519
  lines.push("");
4252
6520
  const facts = [];
4253
6521
  if (profile.languages.length) facts.push(`Languages: ${profile.languages.join(", ")}.`);
@@ -4257,7 +6525,7 @@ function stackSection(input) {
4257
6525
  }
4258
6526
  if (platformSource === "none") {
4259
6527
  lines.push(
4260
- `No target platform was detected, so the token advice below is generic. Re-run ${code("spec-layer skill --platform web|ios|android|flutter")} to write it for a platform, or pass ` + code("--install") + " with the same flag to update the installed copy.",
6528
+ `No target platform was detected, so the token advice below is generic. Re-run ${code2("spec-layer skill --platform web|ios|android|flutter")} to write it for a platform, or pass ` + code2("--install") + " with the same flag to update the installed copy.",
4261
6529
  ""
4262
6530
  );
4263
6531
  } else {
@@ -4269,7 +6537,7 @@ function stackSection(input) {
4269
6537
  const n = pull.foundation.unitlessNumbers;
4270
6538
  const cssReport = pull.outputs.find((o) => o.platform === "web" && o.format === "css" && o.written) ?? null;
4271
6539
  lines.push(
4272
- `**${n} token${n === 1 ? " has" : "s have"} no unit.** ${n === 1 ? "Its own Figma variable states" : "Their own Figma variables state"} none at all, not even that ${n === 1 ? "it is" : "they are"} a unitless number the way an opacity or a font weight would. Those are already excluded from this count, because Figma states that for them. So ${n === 1 ? "it is" : "they are"} written as ${code('$type: "number"')}: a bare number, not usable as a CSS length. ${code("height: 36")} is invalid CSS and the browser drops the declaration. ${n === 1 ? "Narrow the variable's" : "Narrow each variable's"} scope in Figma to a length (${code("CORNER_RADIUS")}, ${code("GAP")}, ${code("WIDTH_HEIGHT")}, ${code("FONT_SIZE")}, or ${code("STROKE_FLOAT")}), or to ${code("OPACITY")}/${code("FONT_WEIGHT")} if it genuinely carries none, and pull again. Only add ${code('"dtcg": { "units": { "<Collection>/<name glob>": "px" } }')} in ${code("speclayer.json")} once you already know the token is a length: the override applies to anything the glob matches that Figma has not already scoped as unitless, so a glob that is too broad can turn a genuine opacity or font weight into a fake length. Nothing is inferred from a name; ${code(`${tokensDir}spec-layer.meta.json`)} names each token's own Figma scopes` + (cssReport ? `, and ${code(`${input.outDir}/outputs/${cssReport.platform}-${cssReport.format}.report.json`)} names every one under ${code("unitless_number")}. Do not read that file's entry count as this number: it carries one entry per mode a token appears in, and it also names the tokens Figma does scope as a unitless number, which this count leaves out.` : "."),
6540
+ `**${n} token${n === 1 ? " has" : "s have"} no unit.** ${n === 1 ? "Its own Figma variable states" : "Their own Figma variables state"} none at all, not even that ${n === 1 ? "it is" : "they are"} a unitless number the way an opacity or a font weight would. Those are already excluded from this count, because Figma states that for them. So ${n === 1 ? "it is" : "they are"} written as ${code2('$type: "number"')}: a bare number, not usable as a CSS length. ${code2("height: 36")} is invalid CSS and the browser drops the declaration. ${n === 1 ? "Narrow the variable's" : "Narrow each variable's"} scope in Figma to a length (${code2("CORNER_RADIUS")}, ${code2("GAP")}, ${code2("WIDTH_HEIGHT")}, ${code2("FONT_SIZE")}, or ${code2("STROKE_FLOAT")}), or to ${code2("OPACITY")}/${code2("FONT_WEIGHT")} if it genuinely carries none, and pull again. Only add ${code2('"dtcg": { "units": { "<Collection>/<name glob>": "px" } }')} in ${code2("speclayer.json")} once you already know the token is a length: the override applies to anything the glob matches that Figma has not already scoped as unitless, so a glob that is too broad can turn a genuine opacity or font weight into a fake length. Nothing is inferred from a name; ${code2(`${tokensDir}spec-layer.meta.json`)} names each token's own Figma scopes` + (cssReport ? `, and ${code2(`${input.outDir}/outputs/${cssReport.platform}-${cssReport.format}.report.json`)} names every one under ${code2("unitless_number")}. Do not read that file's entry count as this number: it carries one entry per mode a token appears in, and it also names the tokens Figma does scope as a unitless number, which this count leaves out.` : "."),
4273
6541
  ""
4274
6542
  );
4275
6543
  }
@@ -4292,17 +6560,17 @@ function stackSection(input) {
4292
6560
  }
4293
6561
  const fallbackTarget = cssOut ? `${cssOut.path}/` : `${input.outDir}/`;
4294
6562
  lines.push(
4295
- `The token holds a family name and no fallback stack. Never write ${code("font-family")} from a token without appending a generic fallback, and never keep that fallback inside ${code(fallbackTarget)}: the next pull replaces it.`,
6563
+ `The token holds a family name and no fallback stack. Never write ${code2("font-family")} from a token without appending a generic fallback, and never keep that fallback inside ${code2(fallbackTarget)}: the next pull replaces it.`,
4296
6564
  ""
4297
6565
  );
4298
6566
  } else if (fontsStatus === "ok") {
4299
6567
  lines.push(
4300
- `${code("fonts.json")} names no font family. Either this library's typography styles reference none, or a reference failed to resolve one; ${code("npx spec-layer show foundation")} prints the styles themselves.`,
6568
+ `${code2("fonts.json")} names no font family. Either this library's typography styles reference none, or a reference failed to resolve one; ${code2("npx spec-layer show foundation")} prints the styles themselves.`,
4301
6569
  ""
4302
6570
  );
4303
6571
  } else {
4304
6572
  lines.push(
4305
- fontsStatus === "missing" ? `${code("fonts.json")} is missing, so the font requirement for this library is unknown here. Run ${code("npx spec-layer pull")} again: every pull that includes the Foundation writes this file, and a pull that finds it gone re-projects instead of reporting no change.` : `${code("fonts.json")} is not valid JSON, so the font requirement for this library is unknown here. Delete ${code(`${input.outDir}/fonts.json`)} and run ${code("npx spec-layer pull")} again, which rewrites it.`,
6573
+ fontsStatus === "missing" ? `${code2("fonts.json")} is missing, so the font requirement for this library is unknown here. Run ${code2("npx spec-layer pull")} again: every pull that includes the Foundation writes this file, and a pull that finds it gone re-projects instead of reporting no change.` : `${code2("fonts.json")} is not valid JSON, so the font requirement for this library is unknown here. Delete ${code2(`${input.outDir}/fonts.json`)} and run ${code2("npx spec-layer pull")} again, which rewrites it.`,
4306
6574
  ""
4307
6575
  );
4308
6576
  }
@@ -4310,58 +6578,58 @@ function stackSection(input) {
4310
6578
  if (cssOut) {
4311
6579
  const mapPath = `${input.outDir}/outputs/web-css.map.json`;
4312
6580
  lines.push(
4313
- `The CSS custom property for every token is in ${code(mapPath)}: source "code_syntax" when the designer declared it in Figma, "derived" when the CLI built it from the DTCG path by the stated rule (${cssOut.case} case, collection root included). Each entry names the file that declares the property. Use those names; never invent a third. ${code(`${tokensDir}spec-layer.meta.json`)} still holds the raw ${code("code_syntax.WEB")} the designer declared.`,
6581
+ `The CSS custom property for every token is in ${code2(mapPath)}: source "code_syntax" when the designer declared it in Figma, "derived" when the CLI built it from the DTCG path by the stated rule (${cssOut.case} case, collection root included). Each entry names the file that declares the property. Use those names; never invent a third. ${code2(`${tokensDir}spec-layer.meta.json`)} still holds the raw ${code2("code_syntax.WEB")} the designer declared.`,
4314
6582
  ""
4315
6583
  );
4316
6584
  const partFiles = cssOut.files.filter((f) => f !== CSS_INDEX_FILE);
4317
6585
  lines.push(
4318
- `Import ${code(`${cssOut.path}/index.css`)} from the root stylesheet. It imports one file per collection and mode: ${partFiles.join(", ")}. Sets and default modes are at ${code(":root")}; every other mode is a block under ${code(cssOut.modeSelector)} in its own file, so a mode can also be imported alone. To switch, set ${code("data-theme")} on ${code("<html>")} (or whatever the selector names). To let the OS choose, set that collection's selector to ${code(":root")} under ${code("outputs[].modes")} and import the mode's file yourself under ${code("@media (prefers-color-scheme: dark)")}; the CLI never assumes that.`,
6586
+ `Import ${code2(`${cssOut.path}/index.css`)} from the root stylesheet. It imports one file per collection and mode: ${partFiles.join(", ")}. Sets and default modes are at ${code2(":root")}; every other mode is a block under ${code2(cssOut.modeSelector)} in its own file, so a mode can also be imported alone. To switch, set ${code2("data-theme")} on ${code2("<html>")} (or whatever the selector names). To let the OS choose, set that collection's selector to ${code2(":root")} under ${code2("outputs[].modes")} and import the mode's file yourself under ${code2("@media (prefers-color-scheme: dark)")}; the CLI never assumes that.`,
4319
6587
  ""
4320
6588
  );
4321
6589
  if (profile.tokenTools.includes("style-dictionary") || profile.tokenTools.includes("tokens-studio")) {
4322
6590
  lines.push(
4323
- `${code(`${cssOut.path}/`)} is a projection of the same ${code(tokensDir)} files, not a second source. Import one or the other.`,
6591
+ `${code2(`${cssOut.path}/`)} is a projection of the same ${code2(tokensDir)} files, not a second source. Import one or the other.`,
4324
6592
  ""
4325
6593
  );
4326
6594
  }
4327
6595
  } else {
4328
6596
  lines.push(
4329
- `Token identifiers for code live in ${code(`${tokensDir}spec-layer.meta.json`)} under each token's ${code(`code_syntax.${key}`)}, when the designer declared one in Figma. When a token has no WEB entry, use the DTCG path as it appears in the token file and say in your change that the code name is not declared in Figma.`,
6597
+ `Token identifiers for code live in ${code2(`${tokensDir}spec-layer.meta.json`)} under each token's ${code2(`code_syntax.${key}`)}, when the designer declared one in Figma. When a token has no WEB entry, use the DTCG path as it appears in the token file and say in your change that the code name is not declared in Figma.`,
4330
6598
  ""
4331
6599
  );
4332
6600
  const configuredNotWritten = pull?.outputs.find((o) => o.platform === "web" && !o.written) ?? null;
4333
6601
  if (configuredNotWritten?.indexMissing) {
4334
6602
  lines.push(
4335
- `A web/css output is configured at ${code(`${configuredNotWritten.path}/`)} but its ${code("index.css")} is missing, so the file list is unknown. Run ${code("npx spec-layer pull")} to write it again.`,
6603
+ `A web/css output is configured at ${code2(`${configuredNotWritten.path}/`)} but its ${code2("index.css")} is missing, so the file list is unknown. Run ${code2("npx spec-layer pull")} to write it again.`,
4336
6604
  ""
4337
6605
  );
4338
6606
  } else if (configuredNotWritten) {
4339
6607
  lines.push(
4340
- `A web/css output is configured at ${code(`${configuredNotWritten.path}/`)} but was not written, because the last pull did not write the Foundation. Pull with the Foundation selected to write it.`,
6608
+ `A web/css output is configured at ${code2(`${configuredNotWritten.path}/`)} but was not written, because the last pull did not write the Foundation. Pull with the Foundation selected to write it.`,
4341
6609
  ""
4342
6610
  );
4343
6611
  } else if (pull?.foundation?.written) {
4344
6612
  lines.push(
4345
- `No token file was written for web. Add \`"outputs"\` in \`speclayer.json\` (or run \`spec-layer pull --platform web\` once) and pull again; the default lands at ${code("tokens/")}.`,
6613
+ `No token file was written for web. Add \`"outputs"\` in \`speclayer.json\` (or run \`spec-layer pull --platform web\` once) and pull again; the default lands at ${code2("tokens/")}.`,
4346
6614
  ""
4347
6615
  );
4348
6616
  }
4349
6617
  }
4350
6618
  if (profile.tokenTools.includes("tailwind")) {
4351
6619
  lines.push(
4352
- `Tailwind is present (${code("tailwindcss")}). Map DTCG ${code("color")} tokens to the theme's color scale and ${code("dimension")} tokens to spacing, radius, or font size by the collection and group they sit in. Keep the mapping in one place and reference token paths, not copied values, so a republish moves the code with it.`,
6620
+ `Tailwind is present (${code2("tailwindcss")}). Map DTCG ${code2("color")} tokens to the theme's color scale and ${code2("dimension")} tokens to spacing, radius, or font size by the collection and group they sit in. Keep the mapping in one place and reference token paths, not copied values, so a republish moves the code with it.`,
4353
6621
  ""
4354
6622
  );
4355
6623
  }
4356
6624
  if (profile.tokenTools.includes("style-dictionary")) {
4357
6625
  const major = profile.styleDictionaryMajor;
4358
6626
  lines.push(
4359
- `Style Dictionary is present${major !== null ? ` (major version ${major} in package.json)` : ""}. Point it at ${code(tokensDir)} and load the files ${code("resolver.json")} names for the mode you build. Exclude ${code("spec-layer.meta.json")} and ${code("report.json")} from token globs; they are not token files.`
6627
+ `Style Dictionary is present${major !== null ? ` (major version ${major} in package.json)` : ""}. Point it at ${code2(tokensDir)} and load the files ${code2("resolver.json")} names for the mode you build. Exclude ${code2("spec-layer.meta.json")} and ${code2("report.json")} from token globs; they are not token files.`
4360
6628
  );
4361
6629
  if (major !== null && major < 5) {
4362
6630
  lines.push(
4363
6631
  "",
4364
- `Style Dictionary ${major} reads the string value forms, not the 2025.10 object forms. Set ${code('"dtcg": { "values": "legacy" }')} in ${code("speclayer.json")} and run ${code("spec-layer pull")}; the change re-projects tokens/ without a republish.`
6632
+ `Style Dictionary ${major} reads the string value forms, not the 2025.10 object forms. Set ${code2('"dtcg": { "values": "legacy" }')} in ${code2("speclayer.json")} and run ${code2("spec-layer pull")}; the change re-projects tokens/ without a republish.`
4365
6633
  );
4366
6634
  }
4367
6635
  lines.push("");
@@ -4369,19 +6637,19 @@ function stackSection(input) {
4369
6637
  } else if (platform === "ios") {
4370
6638
  lines.push("### iOS", "");
4371
6639
  lines.push(
4372
- `Token identifiers for code live in ${code(`${tokensDir}spec-layer.meta.json`)} under each token's ${code(`code_syntax.${key}`)}, when the designer declared one in Figma. Use that as the Swift symbol. Colors arrive as hex strings or DTCG color objects with RGBA components; dimensions carry an explicit px or rem unit. A modifier with more than one context (for example a light and a dark mode) maps to a color-scheme-dependent value; a set without modes is a constant. Do not invent a dark variant for a collection that has one mode.`,
6640
+ `Token identifiers for code live in ${code2(`${tokensDir}spec-layer.meta.json`)} under each token's ${code2(`code_syntax.${key}`)}, when the designer declared one in Figma. Use that as the Swift symbol. Colors arrive as hex strings or DTCG color objects with RGBA components; dimensions carry an explicit px or rem unit. A modifier with more than one context (for example a light and a dark mode) maps to a color-scheme-dependent value; a set without modes is a constant. Do not invent a dark variant for a collection that has one mode.`,
4373
6641
  ""
4374
6642
  );
4375
6643
  } else if (platform === "android") {
4376
6644
  lines.push("### Android", "");
4377
6645
  lines.push(
4378
- `Token identifiers for code live in ${code(`${tokensDir}spec-layer.meta.json`)} under each token's ${code(`code_syntax.${key}`)}, when the designer declared one in Figma. Use that as the Kotlin or resource name. Dimensions carry an explicit px or rem unit and no density assumption; a value is only dp when your own convention says so, and that convention belongs in your code, not in the token file. Modes map to resource qualifiers or a Compose theme switch.`,
6646
+ `Token identifiers for code live in ${code2(`${tokensDir}spec-layer.meta.json`)} under each token's ${code2(`code_syntax.${key}`)}, when the designer declared one in Figma. Use that as the Kotlin or resource name. Dimensions carry an explicit px or rem unit and no density assumption; a value is only dp when your own convention says so, and that convention belongs in your code, not in the token file. Modes map to resource qualifiers or a Compose theme switch.`,
4379
6647
  ""
4380
6648
  );
4381
6649
  } else {
4382
6650
  lines.push("### Flutter", "");
4383
6651
  lines.push(
4384
- `Figma declares no code syntax for Flutter, so no identifier is provided for Dart. Name symbols after the DTCG path (for example ${code("Collection.group.name")} becomes a nested class or a camelCase constant) and keep the path in a comment so the source token stays traceable. Modes map to theme variants.`,
6652
+ `Figma declares no code syntax for Flutter, so no identifier is provided for Dart. Name symbols after the DTCG path (for example ${code2("Collection.group.name")} becomes a nested class or a camelCase constant) and keep the path in a comment so the source token stays traceable. Modes map to theme variants.`,
4385
6653
  ""
4386
6654
  );
4387
6655
  }
@@ -4393,46 +6661,46 @@ function pullSection(input) {
4393
6661
  const lines = ["## What is on disk", ""];
4394
6662
  if (!pull) {
4395
6663
  lines.push(
4396
- `No pull has been made in this directory yet, so nothing under ${code(outDir + "/")} can be described. Run ${code("npx spec-layer pull")} (or the setup command from the plugin if there is no ${code("speclayer.json")}), then ${code("npx spec-layer skill --install")} again to list the components and token collections here.`,
6664
+ `No pull has been made in this directory yet, so nothing under ${code2(outDir + "/")} can be described. Run ${code2("npx spec-layer pull")} (or the setup command from the plugin if there is no ${code2("speclayer.json")}), then ${code2("npx spec-layer skill --install")} again to list the components and token collections here.`,
4397
6665
  ""
4398
6666
  );
4399
6667
  return lines;
4400
6668
  }
4401
6669
  lines.push(
4402
- `Library ${code(pull.libraryId)}, published ${pull.publishedAt}${pull.pluginVersion ? ` by plugin ${pull.pluginVersion}` : ""}. Run ${code("npx spec-layer status")} first; exit code 2 means a newer publish exists and ${code("npx spec-layer pull")} fetches it.`,
6670
+ `Library ${code2(pull.libraryId)}, published ${pull.publishedAt}${pull.pluginVersion ? ` by plugin ${pull.pluginVersion}` : ""}. Run ${code2("npx spec-layer status")} first; exit code 2 means a newer publish exists and ${code2("npx spec-layer pull")} fetches it.`,
4403
6671
  ""
4404
6672
  );
4405
- lines.push(`- ${code(`${outDir}/manifest.json`)}: every artifact with its content hash and file path.`);
4406
- lines.push(`- ${code(`${outDir}/bundle.json`)}: the whole published library, including the canonical v5 JSON of every artifact.`);
6673
+ lines.push(`- ${code2(`${outDir}/manifest.json`)}: every artifact with its content hash and file path.`);
6674
+ lines.push(`- ${code2(`${outDir}/bundle.json`)}: the whole published library, including the canonical v5 JSON of every artifact.`);
4407
6675
  if (pull.foundation) {
4408
6676
  if (pull.foundation.written) {
4409
- lines.push(`- ${code(`${outDir}/tokens/`)}: the Foundation as Design Tokens Format Module 2025.10 files.`);
4410
- lines.push(` - ${code("resolver.json")}: sets, modifiers, and resolution order. Start here.`);
4411
- lines.push(` - ${code("spec-layer.meta.json")}: Figma ids, scopes, publication, and ${code("code_syntax")} per DTCG path.`);
4412
- lines.push(` - ${code("report.json")}: what the format could not express, with reasons. Never fill these gaps with a guess.`);
4413
- for (const f of pull.foundation.tokenFiles) lines.push(` - ${code(f)}`);
6677
+ lines.push(`- ${code2(`${outDir}/tokens/`)}: the Foundation as Design Tokens Format Module 2025.10 files.`);
6678
+ lines.push(` - ${code2("resolver.json")}: sets, modifiers, and resolution order. Start here.`);
6679
+ lines.push(` - ${code2("spec-layer.meta.json")}: Figma ids, scopes, publication, and ${code2("code_syntax")} per DTCG path.`);
6680
+ lines.push(` - ${code2("report.json")}: what the format could not express, with reasons. Never fill these gaps with a guess.`);
6681
+ for (const f of pull.foundation.tokenFiles) lines.push(` - ${code2(f)}`);
4414
6682
  } else {
4415
- lines.push(`- Foundation: present in the library but not written, because the selection excludes it. ${code("spec-layer show foundation")} still prints it.`);
6683
+ lines.push(`- Foundation: present in the library but not written, because the selection excludes it. ${code2("spec-layer show foundation")} still prints it.`);
4416
6684
  }
4417
6685
  } else {
4418
6686
  lines.push("- This library has no Foundation, so there is no tokens/ directory.");
4419
6687
  }
4420
- lines.push(`- ${code(`${pull.componentSpecsDir}/`)}: one YAML per component.`);
6688
+ lines.push(`- ${code2(`${pull.componentSpecsDir}/`)}: one ${pull.componentSpecsFormat === "md" ? "Markdown page" : "YAML"} per component.`);
4421
6689
  for (const o of pull.outputs) {
4422
- lines.push(o.written ? `- ${code(`${o.path}/`)}: ${o.platform}/${o.format} token files, ${o.case} names: ${o.files.join(", ")}. Non-default modes are under ${code(o.modeSelector)}, each in its own file. Names and provenance: ${code(`${outDir}/outputs/${o.platform}-${o.format}.map.json`)}; what it could not express: ${code(`${outDir}/outputs/${o.platform}-${o.format}.report.json`)}.` : o.indexMissing ? `- ${code(`${o.path}/`)}: ${o.platform}/${o.format} token files, but ${code("index.css")} is missing; run ${code("npx spec-layer pull")} to restore the directory.` : `- ${code(`${o.path}/`)}: ${o.platform}/${o.format} token files, configured but not written by the last pull (the Foundation was not written). Nothing is on disk at that path from Spec Layer.`);
6690
+ lines.push(o.written ? `- ${code2(`${o.path}/`)}: ${o.platform}/${o.format} token files, ${o.case} names: ${o.files.join(", ")}. Non-default modes are under ${code2(o.modeSelector)}, each in its own file. Names and provenance: ${code2(`${outDir}/outputs/${o.platform}-${o.format}.map.json`)}; what it could not express: ${code2(`${outDir}/outputs/${o.platform}-${o.format}.report.json`)}.` : o.indexMissing ? `- ${code2(`${o.path}/`)}: ${o.platform}/${o.format} token files, but ${code2("index.css")} is missing; run ${code2("npx spec-layer pull")} to restore the directory.` : `- ${code2(`${o.path}/`)}: ${o.platform}/${o.format} token files, configured but not written by the last pull (the Foundation was not written). Nothing is on disk at that path from Spec Layer.`);
4423
6691
  }
4424
6692
  lines.push("");
4425
6693
  if (pull.foundation && (pull.foundation.sets.length || pull.foundation.modifiers.length)) {
4426
6694
  lines.push("### Token collections", "");
4427
- for (const set of pull.foundation.sets) lines.push(`- ${code(set)}: one mode, always applied.`);
6695
+ for (const set of pull.foundation.sets) lines.push(`- ${code2(set)}: one mode, always applied.`);
4428
6696
  for (const m of pull.foundation.modifiers) {
4429
- lines.push(`- ${code(m.name)}: modes ${m.contexts.map(code).join(", ")}${m.default ? `, default ${code(m.default)}` : ""}.`);
6697
+ lines.push(`- ${code2(m.name)}: modes ${m.contexts.map(code2).join(", ")}${m.default ? `, default ${code2(m.default)}` : ""}.`);
4430
6698
  }
4431
6699
  lines.push("");
4432
6700
  const counts = Object.entries(pull.foundation.reportCounts);
4433
6701
  if (counts.length) {
4434
6702
  lines.push(
4435
- `${code("report.json")} lists ${counts.map(([c, n]) => `${n} ${code(c)}`).join(", ")}. Read it before assuming a token is missing.`,
6703
+ `${code2("report.json")} lists ${counts.map(([c, n]) => `${n} ${code2(c)}`).join(", ")}. Read it before assuming a token is missing.`,
4436
6704
  ""
4437
6705
  );
4438
6706
  }
@@ -4442,7 +6710,7 @@ function pullSection(input) {
4442
6710
  lines.push("The library documents no components.", "");
4443
6711
  } else {
4444
6712
  for (const c of pull.components) {
4445
- lines.push(c.path ? `- ${c.name}: ${code(c.path)}` : `- ${c.name}: not written (excluded by the selection). ${code(`spec-layer show component "${c.name}"`)} prints it.`);
6713
+ lines.push(c.path ? `- ${c.name}: ${code2(c.path)}` : `- ${c.name}: not written (excluded by the selection). ${code2(`spec-layer show component "${c.name}"`)} prints it.`);
4446
6714
  }
4447
6715
  lines.push("");
4448
6716
  }
@@ -4453,42 +6721,43 @@ function commandsSection() {
4453
6721
  const cell = (s) => s.replace(/\|/g, "\\|");
4454
6722
  lines.push("| Command | What it does | When | Network | Key | Writes |", "|---|---|---|---|---|---|");
4455
6723
  for (const t of TOOLS) {
4456
- lines.push(`| ${code(cell(t.usage))} | ${cell(t.summary)} | ${cell(t.when)} | ${t.network ? "yes" : "no"} | ${t.needsKey ? "required" : "no"} | ${t.writes.length ? t.writes.map((w) => code(cell(w))).join(", ") : "nothing"} |`);
6724
+ lines.push(`| ${code2(cell(t.usage))} | ${cell(t.summary)} | ${cell(t.when)} | ${t.network ? "yes" : "no"} | ${t.needsKey ? "required" : "no"} | ${t.writes.length ? t.writes.map((w) => code2(cell(w))).join(", ") : "nothing"} |`);
4457
6725
  }
4458
6726
  lines.push("");
4459
6727
  lines.push("Exit codes:", "");
4460
6728
  for (const t of TOOLS) {
4461
- lines.push(`- ${code(t.name)}: ${Object.entries(t.exits).map(([c, m]) => `${c} = ${m}`).join("; ")}.`);
6729
+ lines.push(`- ${code2(t.name)}: ${Object.entries(t.exits).map(([c, m]) => `${c} = ${m}`).join("; ")}.`);
4462
6730
  }
4463
6731
  lines.push("");
4464
- for (const f of GLOBAL_FLAGS) lines.push(`- ${code(f.flag)}: ${f.summary}`);
6732
+ for (const f of GLOBAL_FLAGS) lines.push(`- ${code2(f.flag)}: ${f.summary}`);
4465
6733
  lines.push("", KEY_RESOLUTION, "");
4466
- lines.push(`Run ${code("npx --yes spec-layer <command>")} in an unattended session so npx does not stop to ask before downloading the package. ${code("spec-layer tools --json")} prints this table for machines.`, "");
6734
+ lines.push(`Run ${code2("npx --yes spec-layer <command>")} in an unattended session so npx does not stop to ask before downloading the package. ${code2("spec-layer tools --json")} prints this table for machines.`, "");
4467
6735
  return lines;
4468
6736
  }
4469
6737
  function buildSkillGuide(input) {
4470
6738
  const { outDir } = input;
4471
6739
  const lines = [];
6740
+ const markdown = (input.pull?.componentSpecsFormat ?? input.config?.componentSpecsFormat ?? DEFAULT_COMPONENT_FORMAT) === "md";
4472
6741
  lines.push("# Spec Layer: design-system context for this repository", "");
4473
6742
  lines.push(
4474
- `The Spec Layer Figma plugin publishes a design system's components, variables, and styles as data. The ${code("spec-layer")} CLI (version ${input.version}) pulls that data into this repository under ${code(outDir + "/")}. Everything in those files is extracted deterministically from Figma and validated against a published schema; no model wrote any of it. Treat it as the source of truth for what the design system contains, and treat anything it does not state as unknown rather than as something to infer.`,
6743
+ `The Spec Layer Figma plugin publishes a design system's components, variables, and styles as data. The ${code2("spec-layer")} CLI (version ${input.version}) pulls that data into this repository under ${code2(outDir + "/")}. Everything in those files is extracted deterministically from Figma and validated against a published schema, ` + (markdown ? `with two exceptions that can carry model-written prose: a section of a component page that says it was written by AI, or a component's or the foundation's ${code2("guidelines")} block, marked ${code2("origin: generated")}; and a token group's ${code2("$description")}, which carries no marker ` : `with two exceptions that can carry model-written prose: a component's or the foundation's ${code2("guidelines")} block, marked ${code2("origin: generated")}, and a token group's ${code2("$description")}, which carries no marker `) + "and can be model-written even though it looks like an ordinary field. Treat the rest as the source of truth for what the design system contains, and treat anything it does not state as unknown rather than as something to infer.",
4475
6744
  ""
4476
6745
  );
4477
6746
  const componentSpecsDir = input.pull?.componentSpecsDir ?? input.config?.componentSpecsDir ?? DEFAULT_COMPONENT_SPECS_DIR;
4478
6747
  lines.push("## How to use it", "");
4479
- lines.push(`1. Run ${code("npx spec-layer status")}. Exit 0 means the local copy is current; exit 2 means run ${code("npx spec-layer pull")} first.`);
4480
- lines.push(`2. Building or changing a component: read its YAML under ${code(`${componentSpecsDir}/`)}, or ${code("npx spec-layer show component NAME")}. ${code("api")} gives variants, states, booleans, and slots; ${code("anatomy")} names the parts; ${code("references.bindings")} says which token each part's property uses and under which ${code("when")} conditions; ${code("unbound")} lists values that are hardcoded in Figma.`);
4481
- lines.push(`3. Working with colors, spacing, type, or effects: start at ${code(`${outDir}/tokens/resolver.json`)}, load the set and mode files it names, and look up ${code("code_syntax")} in ${code("spec-layer.meta.json")} for the name the designer declared for your platform.`);
6748
+ lines.push(`1. Run ${code2("npx spec-layer status")}. Exit 0 means the local copy is current; exit 2 means run ${code2("npx spec-layer pull")} first.`);
6749
+ lines.push(markdown ? `2. Building or changing a component: read its page under ${code2(`${componentSpecsDir}/`)}, or ${code2("npx spec-layer show component NAME")}. **Properties** gives variants, states, booleans, and slots; **Anatomy** names the parts; **Token bindings** says which token each part's property uses and under which **When** conditions; **Unbound values** lists values that are hardcoded in Figma.` : `2. Building or changing a component: read its YAML under ${code2(`${componentSpecsDir}/`)}, or ${code2("npx spec-layer show component NAME")}. ${code2("api")} gives variants, states, booleans, and slots; ${code2("anatomy")} names the parts; ${code2("references.bindings")} says which token each part's property uses and under which ${code2("when")} conditions; ${code2("unbound")} lists values that are hardcoded in Figma.`);
6750
+ lines.push(`3. Working with colors, spacing, type, or effects: start at ${code2(`${outDir}/tokens/resolver.json`)}, load the set and mode files it names, and look up ${code2("code_syntax")} in ${code2("spec-layer.meta.json")} for the name the designer declared for your platform.`);
4482
6751
  lines.push(`4. Reference tokens by name in code; never paste a resolved value where a token exists. A value the design system does not define is not a token: say so in your change rather than adding one.`);
4483
- lines.push(`5. An ${code("unbound")} entry is design debt reported from Figma. Do not silently promote it to a token; keep the literal and note that Figma has no binding for it.`);
6752
+ lines.push(markdown ? "5. A row under **Unbound values** is design debt reported from Figma. Do not silently promote it to a token; keep the literal and note that Figma has no binding for it." : `5. An ${code2("unbound")} entry is design debt reported from Figma. Do not silently promote it to a token; keep the literal and note that Figma has no binding for it.`);
4484
6753
  const writtenOutputs = input.pull?.outputs.filter((o) => o.written) ?? [];
4485
- const outputNote = writtenOutputs.length ? ` Never edit ${writtenOutputs.map((o) => code(`${o.path}/`)).join(", ")} either: pull replaces or removes files there.` : "";
4486
- lines.push(`6. Never edit files under ${code(outDir + "/")} or ${code(componentSpecsDir + "/")}: the next pull replaces or removes them.${outputNote} Configuration lives in ${code("speclayer.json")}. Never commit ${code(CREDENTIALS_NAME)}, and never print or copy the pull key.`);
6754
+ const outputNote = writtenOutputs.length ? ` Never edit ${writtenOutputs.map((o) => code2(`${o.path}/`)).join(", ")} either: pull replaces or removes files there.` : "";
6755
+ lines.push(`6. Never edit files under ${code2(outDir + "/")} or ${code2(componentSpecsDir + "/")}: the next pull replaces or removes them.${outputNote} Configuration lives in ${code2("speclayer.json")}. Never commit ${code2(CREDENTIALS_NAME)}, and never print or copy the pull key.`);
4487
6756
  lines.push("");
4488
6757
  lines.push(...pullSection(input));
4489
6758
  lines.push(...stackSection(input));
4490
6759
  lines.push(...commandsSection());
4491
- lines.push(`Generated by ${code("spec-layer skill")}. Re-run ${code("npx spec-layer skill --install")} after a pull that adds components, after changing ${code("outputs")} or ${code("componentSpecsDir")}, or when the codebase changes stack; the file is replaced, not appended.`);
6760
+ lines.push(`Generated by ${code2("spec-layer skill")}. Re-run ${code2("npx spec-layer skill --install")} after a pull that adds components, after changing ${code2("outputs")} or ${code2("componentSpecsDir")}, or when the codebase changes stack; the file is replaced, not appended.`);
4492
6761
  return `${lines.join("\n")}
4493
6762
  `;
4494
6763
  }
@@ -4561,19 +6830,27 @@ ${BLOCK_END}
4561
6830
  const sep2 = existing.length === 0 ? "" : existing.endsWith("\n\n") ? "" : existing.endsWith("\n") ? "\n" : "\n\n";
4562
6831
  return `${existing}${sep2}${block}`;
4563
6832
  }
6833
+ var SNAPSHOT_DIRS = ["components", "tokens"];
6834
+ function staleSnapshotDirs(cwd, target) {
6835
+ if (target.host !== "claude") return [];
6836
+ const dir = dirname3(target.path);
6837
+ return SNAPSHOT_DIRS.map((name) => `${dir}/${name}`).filter((rel) => existsSync7(join7(cwd, rel)));
6838
+ }
4564
6839
  function installSkill(cwd, host, guide) {
4565
6840
  const target = installTarget(host);
4566
6841
  const abs = join7(cwd, target.path);
4567
6842
  const existing = existsSync7(abs) ? readFileSync8(abs, "utf8") : null;
6843
+ const staleSnapshot = staleSnapshotDirs(cwd, target);
4568
6844
  const next = target.mode === "file" ? renderForHost(host, guide) : upsertBlock(existing, renderForHost(host, guide));
4569
- if (existing === next) return { path: target.path, result: "unchanged" };
6845
+ if (existing === next) return { path: target.path, result: "unchanged", staleSnapshot };
4570
6846
  mkdirSync3(dirname3(abs), { recursive: true });
4571
6847
  writeFileSync6(abs, next);
4572
- return { path: target.path, result: existing === null ? "created" : "updated" };
6848
+ return { path: target.path, result: existing === null ? "created" : "updated", staleSnapshot };
4573
6849
  }
4574
6850
 
4575
6851
  // src/commands.ts
4576
6852
  var NO_LOCAL_PULL = "No local pull found. Run spec-layer pull.";
6853
+ var FORMAT_NAME = { yaml: "YAML", md: "Markdown" };
4577
6854
  function manifestReader() {
4578
6855
  const cache = /* @__PURE__ */ new Map();
4579
6856
  return (outDir) => {
@@ -4584,7 +6861,7 @@ function manifestReader() {
4584
6861
  function sameOutput(a, b) {
4585
6862
  const selectionKey = (s) => JSON.stringify([s.foundation, s.components === null ? null : [...new Set(s.components.map(slugify))].sort()]);
4586
6863
  const key = (v) => JSON.stringify(sortKeys(v ?? {}));
4587
- return selectionKey(a.selection) === selectionKey(b.selection) && key(a.dtcg) === key(b.dtcg) && key(a.outputs ?? []) === key(b.outputs ?? []) && (a.componentSpecsDir ?? DEFAULT_COMPONENT_SPECS_DIR) === (b.componentSpecsDir ?? DEFAULT_COMPONENT_SPECS_DIR);
6864
+ return selectionKey(a.selection) === selectionKey(b.selection) && key(a.dtcg) === key(b.dtcg) && key(a.outputs ?? []) === key(b.outputs ?? []) && (a.componentSpecsDir ?? DEFAULT_COMPONENT_SPECS_DIR) === (b.componentSpecsDir ?? DEFAULT_COMPONENT_SPECS_DIR) && (a.componentSpecsFormat ?? DEFAULT_COMPONENT_FORMAT) === (b.componentSpecsFormat ?? DEFAULT_COMPONENT_FORMAT);
4588
6865
  }
4589
6866
  function sortKeys(value) {
4590
6867
  if (Array.isArray(value)) return value.map(sortKeys);
@@ -4605,6 +6882,13 @@ function platformsFromFlags(flags, io2) {
4605
6882
  }
4606
6883
  return out;
4607
6884
  }
6885
+ function componentFormatFromFlags(flags, io2) {
6886
+ const value = flags["component-format"];
6887
+ if (value === void 0) return void 0;
6888
+ if (isComponentFormat(value)) return value;
6889
+ io2.err(`--component-format takes ${COMPONENT_FORMATS.join(" or ")}, not "${value}".`);
6890
+ return null;
6891
+ }
4608
6892
  function resolvePlatforms(cwd, fromFlags, config) {
4609
6893
  if (fromFlags) return { platforms: fromFlags, source: "flag" };
4610
6894
  if (config?.platforms && config.platforms.length > 0) return { platforms: config.platforms, source: "config" };
@@ -4637,6 +6921,8 @@ function runInit(cwd, flags, io2) {
4637
6921
  }
4638
6922
  const fromFlags = platformsFromFlags(flags, io2);
4639
6923
  if (fromFlags === null) return 1;
6924
+ const format = componentFormatFromFlags(flags, io2);
6925
+ if (format === null) return 1;
4640
6926
  const { platforms, source } = resolvePlatforms(cwd, fromFlags, null);
4641
6927
  const outputs = defaultOutputs(platforms);
4642
6928
  const outDir = flags.out ?? DEFAULT_OUT_DIR;
@@ -4644,6 +6930,7 @@ function runInit(cwd, flags, io2) {
4644
6930
  libraryId: flags.id,
4645
6931
  outDir,
4646
6932
  componentSpecsDir: DEFAULT_COMPONENT_SPECS_DIR,
6933
+ ...format ? { componentSpecsFormat: format } : {},
4647
6934
  ...include ? { include } : {},
4648
6935
  ...platforms.length > 0 ? { platforms } : {},
4649
6936
  ...outputs.length > 0 ? { outputs } : {}
@@ -4715,16 +7002,20 @@ async function runSetup(cwd, flags, env, io2, fetcher) {
4715
7002
  }
4716
7003
  const fromFlags = platformsFromFlags(flags, io2);
4717
7004
  if (fromFlags === null) return 1;
7005
+ const format = componentFormatFromFlags(flags, io2);
7006
+ if (format === null) return 1;
4718
7007
  const outDir = flags.out ?? existing?.outDir ?? DEFAULT_OUT_DIR;
4719
7008
  const componentSpecsDir = existing?.componentSpecsDir ?? DEFAULT_COMPONENT_SPECS_DIR;
4720
7009
  const keptInclude = include ?? existing?.include ?? null;
4721
7010
  const keptDtcg = existing?.dtcg ?? null;
7011
+ const keptFormat = format ?? existing?.componentSpecsFormat ?? null;
4722
7012
  const { platforms } = resolvePlatforms(cwd, fromFlags, existing);
4723
7013
  const outputs = withDefaults(existing?.outputs ?? [], platforms);
4724
7014
  writeConfig(cwd, {
4725
7015
  libraryId: flags.id,
4726
7016
  outDir,
4727
7017
  componentSpecsDir,
7018
+ ...keptFormat ? { componentSpecsFormat: keptFormat } : {},
4728
7019
  ...keptInclude ? { include: keptInclude } : {},
4729
7020
  ...keptDtcg ? { dtcg: keptDtcg } : {},
4730
7021
  ...platforms.length > 0 ? { platforms } : {},
@@ -4768,8 +7059,8 @@ git rm --cached ${ignored.line}`);
4768
7059
  }
4769
7060
  const { replaced } = writeCredentials(cwd, { libraryId: flags.id, key });
4770
7061
  io2.out(replaced ? `Replaced the stored key in ${CREDENTIALS_NAME}.` : `Stored the pull key in ${CREDENTIALS_NAME}.`);
4771
- const code2 = await runPull(cwd, { ...flags, key }, env, io2, fetcher);
4772
- if (code2 !== 0) return code2;
7062
+ const code3 = await runPull(cwd, { ...flags, key }, env, io2, fetcher);
7063
+ if (code3 !== 0) return code3;
4773
7064
  const hosts = detectRepo(cwd).agents;
4774
7065
  io2.out("");
4775
7066
  io2.out("Next step for a coding agent: npx spec-layer skill --install");
@@ -4831,6 +7122,9 @@ function printReportSummary(cwd, outDir, outputs, io2) {
4831
7122
  }
4832
7123
  return errors;
4833
7124
  }
7125
+ function publishedPhrase(version, publishedAt) {
7126
+ return version ? `(v${version}, published ${publishedAt})` : `(published ${publishedAt})`;
7127
+ }
4834
7128
  async function runPull(cwd, flags, env, io2, fetcher) {
4835
7129
  const manifestAt = manifestReader();
4836
7130
  const opts = resolved(cwd, flags, env, io2, manifestAt);
@@ -4844,6 +7138,9 @@ async function runPull(cwd, flags, env, io2, fetcher) {
4844
7138
  }
4845
7139
  const fromFlags = platformsFromFlags(flags, io2);
4846
7140
  if (fromFlags === null) return 1;
7141
+ const flagFormat = componentFormatFromFlags(flags, io2);
7142
+ if (flagFormat === null) return 1;
7143
+ const componentSpecsFormat = flagFormat ?? opts.componentSpecsFormat ?? DEFAULT_COMPONENT_FORMAT;
4847
7144
  const { platforms, source } = resolvePlatforms(cwd, fromFlags, opts);
4848
7145
  const outputs = outputsForRun(fromFlags, opts, platforms);
4849
7146
  const manifest = manifestAt(join8(cwd, opts.outDir));
@@ -4851,8 +7148,14 @@ async function runPull(cwd, flags, env, io2, fetcher) {
4851
7148
  const willWriteFoundation = selection.foundation && foundationOnDisk;
4852
7149
  const briefsOnDisk = (manifest?.artifacts ?? []).filter((a) => a.kind === "component" && a.path !== null).every((a) => existsSync8(resolve5(cwd, a.path)));
4853
7150
  const etag = manifest && manifest.cliVersion === cliVersion() && sameOutput(
4854
- { selection: manifest.selection ?? DEFAULT_SELECTION, dtcg: manifest.dtcg, outputs: manifest.outputs, componentSpecsDir: manifest.componentSpecsDir },
4855
- { selection, dtcg: opts.dtcg, outputs, componentSpecsDir: opts.componentSpecsDir }
7151
+ {
7152
+ selection: manifest.selection ?? DEFAULT_SELECTION,
7153
+ dtcg: manifest.dtcg,
7154
+ outputs: manifest.outputs,
7155
+ componentSpecsDir: manifest.componentSpecsDir,
7156
+ componentSpecsFormat: manifest.componentSpecsFormat
7157
+ },
7158
+ { selection, dtcg: opts.dtcg, outputs, componentSpecsDir: opts.componentSpecsDir, componentSpecsFormat }
4856
7159
  ) && briefsOnDisk && (!willWriteFoundation || foundationFilesOnDisk(cwd, opts.outDir)) && (!willWriteFoundation || outputs.every((o) => outputFilesOnDisk(cwd, opts.outDir, o))) ? manifest.bundleHash : void 0;
4857
7160
  const result = await fetchBundle({
4858
7161
  api: opts.api,
@@ -4866,7 +7169,7 @@ async function runPull(cwd, flags, env, io2, fetcher) {
4866
7169
  return 1;
4867
7170
  }
4868
7171
  if (result.kind === "not_modified") {
4869
- io2.out(`Already up to date (published ${manifest?.publishedAt ?? "unknown"}).`);
7172
+ io2.out(`Already up to date ${publishedPhrase(result.version ?? manifest?.version, manifest?.publishedAt ?? "unknown")}.`);
4870
7173
  const cachedErrors = printReportSummary(cwd, opts.outDir, outputs, io2);
4871
7174
  if (flags.strict && cachedErrors > 0) return 1;
4872
7175
  return 0;
@@ -4886,16 +7189,18 @@ async function runPull(cwd, flags, env, io2, fetcher) {
4886
7189
  libraryId: opts.libraryId,
4887
7190
  publishedAt: result.publishedAt,
4888
7191
  bundleHash: result.bundleHash,
7192
+ version: result.version,
4889
7193
  dtcg: opts.dtcg,
4890
7194
  platforms,
4891
7195
  outputs,
4892
- componentSpecsDir: opts.componentSpecsDir
7196
+ componentSpecsDir: opts.componentSpecsDir,
7197
+ componentSpecsFormat
4893
7198
  });
4894
7199
  written = writeResult.written;
4895
7200
  componentSpecs = writeResult.componentSpecs;
4896
7201
  outputResults = writeResult.outputs;
4897
7202
  io2.out(
4898
- `Pulled ${bundle.fileName ?? opts.libraryId}: ${describePull(bundle, selection, selected)} (published ${result.publishedAt}).`
7203
+ `Pulled ${bundle.fileName ?? opts.libraryId}: ${describePull(bundle, selection, selected)} ${publishedPhrase(result.version, result.publishedAt)}.`
4899
7204
  );
4900
7205
  } catch (err) {
4901
7206
  io2.err(errorText(err));
@@ -4903,7 +7208,10 @@ async function runPull(cwd, flags, env, io2, fetcher) {
4903
7208
  }
4904
7209
  const count = (n) => `${n} file${n === 1 ? "" : "s"}`;
4905
7210
  io2.out(`Wrote ${written.length} files under ${opts.outDir}/.`);
4906
- if (componentSpecs.files.length > 0) io2.out(`Wrote ${componentSpecs.path}/ (${count(componentSpecs.files.length)}).`);
7211
+ if (componentSpecs.files.length > 0) {
7212
+ const n = componentSpecs.files.length;
7213
+ io2.out(`Wrote ${componentSpecs.path}/ (${n} ${FORMAT_NAME[componentSpecsFormat]} file${n === 1 ? "" : "s"}).`);
7214
+ }
4907
7215
  for (const r of outputResults) {
4908
7216
  const o = outputs.find((x) => x.path === r.path);
4909
7217
  if (o) io2.out(`Wrote ${r.path}/ (${count(r.files.length)}, ${o.platform}/${o.format}, ${o.case} names).`);
@@ -4948,10 +7256,10 @@ async function runStatus(cwd, flags, env, io2, fetcher) {
4948
7256
  return 1;
4949
7257
  }
4950
7258
  if (result.kind === "not_modified") {
4951
- io2.out(`Up to date (published ${manifest.publishedAt}).`);
7259
+ io2.out(`Up to date ${publishedPhrase(result.version ?? manifest.version, manifest.publishedAt)}.`);
4952
7260
  return 0;
4953
7261
  }
4954
- io2.out(`Behind: remote published ${result.publishedAt}. Run spec-layer pull.`);
7262
+ io2.out(result.version ? `Behind: remote is v${result.version}, published ${result.publishedAt}. Run spec-layer pull.` : `Behind: remote published ${result.publishedAt}. Run spec-layer pull.`);
4955
7263
  return 2;
4956
7264
  }
4957
7265
  function runList(cwd, flags, io2) {
@@ -4962,7 +7270,7 @@ function runList(cwd, flags, io2) {
4962
7270
  io2.err(NO_LOCAL_PULL);
4963
7271
  return 1;
4964
7272
  }
4965
- io2.out(`Library ${manifest.libraryId}, published ${manifest.publishedAt}.`);
7273
+ io2.out(manifest.version ? `Library ${manifest.libraryId}, v${manifest.version}, published ${manifest.publishedAt}.` : `Library ${manifest.libraryId}, published ${manifest.publishedAt}.`);
4966
7274
  const rows = manifest.artifacts.map((a) => [a.kind, a.name, a.path ?? "not written", a.contentHash]);
4967
7275
  const widths = [0, 1, 2].map((i) => Math.max(...rows.map((r) => r[i].length)));
4968
7276
  for (const row of rows) {
@@ -4983,6 +7291,12 @@ function runShow(cwd, flags, args, io2) {
4983
7291
  io2.err(SHOW_USAGE);
4984
7292
  return 1;
4985
7293
  }
7294
+ const flagFormat = componentFormatFromFlags(flags, io2);
7295
+ if (flagFormat === null) return 1;
7296
+ if (wantsFoundation && flagFormat !== void 0) {
7297
+ io2.err("--component-format applies to components. The Foundation prints as its DTCG document.");
7298
+ return 1;
7299
+ }
4986
7300
  const outDir = resolvedOutDir(cwd, flags, io2);
4987
7301
  if (!outDir) return 1;
4988
7302
  let bundle;
@@ -4997,6 +7311,7 @@ function runShow(cwd, flags, args, io2) {
4997
7311
  return 1;
4998
7312
  }
4999
7313
  let entry2;
7314
+ let component = null;
5000
7315
  if (wantsFoundation) {
5001
7316
  if (!bundle.foundation) {
5002
7317
  io2.err("This library has no Foundation. Run spec-layer list to see what it holds.");
@@ -5016,9 +7331,33 @@ Available: ${available || "none"}.`);
5016
7331
  return 1;
5017
7332
  }
5018
7333
  entry2 = matches[0];
7334
+ component = matches[0];
7335
+ }
7336
+ if (flags.canonical) {
7337
+ io2.write(`${JSON.stringify(entry2.artifact, null, 2)}
7338
+ `);
7339
+ return 0;
7340
+ }
7341
+ if (component) {
7342
+ let configFormat;
7343
+ try {
7344
+ configFormat = readConfig(cwd)?.componentSpecsFormat;
7345
+ } catch (err) {
7346
+ io2.err(errorText(err));
7347
+ return 1;
7348
+ }
7349
+ const format = flagFormat ?? configFormat ?? readManifest(outDir)?.componentSpecsFormat ?? DEFAULT_COMPONENT_FORMAT;
7350
+ if (format === "md") {
7351
+ try {
7352
+ io2.write(componentMarkdownPage(component));
7353
+ } catch (err) {
7354
+ io2.err(errorText(err));
7355
+ return 1;
7356
+ }
7357
+ return 0;
7358
+ }
5019
7359
  }
5020
- io2.write(flags.canonical ? `${JSON.stringify(entry2.artifact, null, 2)}
5021
- ` : entry2.ai);
7360
+ io2.write(entry2.ai);
5022
7361
  return 0;
5023
7362
  }
5024
7363
  function runTools(flags, io2) {
@@ -5090,6 +7429,11 @@ function runSkill(cwd, flags, io2) {
5090
7429
  }
5091
7430
  const verb = outcome.result === "created" ? "Wrote" : outcome.result === "updated" ? "Updated" : "Unchanged:";
5092
7431
  io2.out(`${verb} ${outcome.path} (${host}, ${chosen}).`);
7432
+ if (outcome.staleSnapshot.length > 0) {
7433
+ io2.out(
7434
+ `A downloaded snapshot is still in ${outcome.staleSnapshot.join(" and ")}. The guide above supersedes it, and those folders can be deleted.`
7435
+ );
7436
+ }
5093
7437
  }
5094
7438
  if (!input.pull) io2.out(`No local pull yet, so the guide lists no components. Run spec-layer pull, then spec-layer skill --install again.`);
5095
7439
  if (input.platformSource === "none") io2.out(`No target platform detected. Pass --platform ${PLATFORMS.join("|")} to write platform-specific token advice.`);
@@ -5100,17 +7444,17 @@ function runSkill(cwd, flags, io2) {
5100
7444
  var USAGE = `spec-layer <command>
5101
7445
 
5102
7446
  Commands:
5103
- setup --id lib_... --key sl_... [--out DIR] [selection] [--platform P]...
7447
+ setup --id lib_... --key sl_... [--out DIR] [selection] [--platform P]... [--component-format F]
5104
7448
  store the key, then pull
5105
- init --id lib_... [--out DIR] [selection] [--platform P]...
7449
+ init --id lib_... [--out DIR] [selection] [--platform P]... [--component-format F]
5106
7450
  write speclayer.json
5107
- pull [--id lib_...] [--key sl_...] [selection] [--platform P]... [--strict]
7451
+ pull [--id lib_...] [--key sl_...] [selection] [--platform P]... [--component-format F] [--strict]
5108
7452
  fetch the library into DIR (default .speclayer); the foundation lands as DTCG under DIR/tokens/;
5109
7453
  --strict exits 1 when tokens/report.json or an outputs/*.report.json holds an error-severity entry, even on a cached pull (default exit stays 0)
5110
7454
  status [--id lib_...] [--key sl_...] check freshness; exits 2 when behind
5111
7455
  list list every artifact in the last pull
5112
- show foundation | component NAME [--canonical]
5113
- print one artifact (foundation: the DTCG document; component: its AI YAML; --canonical for JSON)
7456
+ show foundation | component NAME [--component-format F] [--canonical]
7457
+ print one artifact (foundation: the DTCG document; component: its AI YAML or Markdown; --canonical for JSON)
5114
7458
  tools [--json] list every command with what it reaches and writes
5115
7459
  skill [--install] [--agent HOST]... [--platform P]... [--json]
5116
7460
  print a guide for a coding agent, adapted to this repo and the last pull;
@@ -5123,6 +7467,7 @@ Selection (setup, pull and init; flags replace the include block in speclayer.js
5123
7467
  Options:
5124
7468
  --api URL override the API origin (default https://api.spec-layer.com)
5125
7469
  --platform web|ios|android|flutter the target this repo builds for (repeatable); applies to setup, init, pull, and skill; setup and init store it, pull uses it for the run
7470
+ --component-format yaml|md how component-specs/ is written and show prints a component (default yaml); setup and init store it, pull and show use it for the run
5126
7471
  The pull key comes from --key, SPEC_LAYER_KEY, or speclayer.local.json written by setup.`;
5127
7472
  var io = {
5128
7473
  out: (l) => console.log(l),
@@ -5149,7 +7494,8 @@ async function main() {
5149
7494
  install: { type: "boolean" },
5150
7495
  strict: { type: "boolean" },
5151
7496
  agent: { type: "string", multiple: true },
5152
- platform: { type: "string", multiple: true }
7497
+ platform: { type: "string", multiple: true },
7498
+ "component-format": { type: "string" }
5153
7499
  }
5154
7500
  }));
5155
7501
  } catch {