spec-layer 0.8.2 → 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 +68 -17
  2. package/dist/cli.js +3182 -319
  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;
@@ -559,9 +559,612 @@ var require_sha256 = __commonJS({
559
559
  import { parseArgs } from "node:util";
560
560
 
561
561
  // src/commands.ts
562
- import { existsSync as existsSync8 } from "node:fs";
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 = [
@@ -814,6 +2078,26 @@ var SUPPORTED_TOKEN_TYPES = [
814
2078
  var SUPPORTED_VALUE_KINDS = ["literal", "alias", "missing"];
815
2079
  var SUPPORTED_DURATION_UNITS = ["ms", "s"];
816
2080
 
2081
+ // ../extractor/src/v5/units.ts
2082
+ var UNIT_BY_SCOPE = {
2083
+ WIDTH_HEIGHT: "px",
2084
+ CORNER_RADIUS: "px",
2085
+ GAP: "px",
2086
+ FONT_SIZE: "px",
2087
+ STROKE_FLOAT: "px",
2088
+ PARAGRAPH_SPACING: "px",
2089
+ PARAGRAPH_INDENT: "px",
2090
+ EFFECT_FLOAT: "px",
2091
+ FONT_WEIGHT: "number",
2092
+ OPACITY: "number"
2093
+ };
2094
+ function scopesStateUnit(scopes) {
2095
+ return (scopes ?? []).some((s) => UNIT_BY_SCOPE[s] !== void 0);
2096
+ }
2097
+ function scopesStateNumber(scopes) {
2098
+ return (scopes ?? []).some((s) => UNIT_BY_SCOPE[s] === "number");
2099
+ }
2100
+
817
2101
  // ../extractor/src/v5/canonical.ts
818
2102
  var import_js_sha2562 = __toESM(require_sha256(), 1);
819
2103
  var SCHEMA_VERSION = "5.1.0";
@@ -825,6 +2109,14 @@ function canonicalJson(value) {
825
2109
  }
826
2110
  return JSON.stringify(value);
827
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
+ }
828
2120
 
829
2121
  // ../extractor/src/v5/validate.ts
830
2122
  var ROOT = "<artifact>";
@@ -1277,52 +2569,417 @@ function validateRootSections(artifact, out) {
1277
2569
  artifact.styles.typography.forEach((style, index) => validateTypographyStyle(style, index, out));
1278
2570
  artifact.styles.effects.forEach((style, index) => validateEffectStyle(style, index, out));
1279
2571
  }
1280
- if (!Array.isArray(artifact.diagnostics)) {
1281
- out.push(shape(ROOT, "`diagnostics` must be an array."));
2572
+ if (!Array.isArray(artifact.diagnostics)) {
2573
+ out.push(shape(ROOT, "`diagnostics` must be an array."));
2574
+ }
2575
+ if (!isRecord(artifact.statistics)) {
2576
+ out.push(shape(ROOT, "`statistics` must be an object."));
2577
+ }
2578
+ if (artifact.guidelines !== void 0) {
2579
+ const guidelines = artifact.guidelines;
2580
+ if (!isRecord(guidelines) || guidelines.origin !== "generated" || !isRecord(guidelines.group_descriptions)) {
2581
+ out.push(shape(
2582
+ ROOT,
2583
+ '`guidelines` must state origin "generated" and a group_descriptions object.'
2584
+ ));
2585
+ } else {
2586
+ for (const [collectionName, folders] of Object.entries(guidelines.group_descriptions)) {
2587
+ if (!isRecord(folders) || Object.values(folders).some((description) => typeof description !== "string")) {
2588
+ out.push(shape(
2589
+ ROOT,
2590
+ `guidelines.group_descriptions[${JSON.stringify(collectionName)}] must map folder names to strings.`
2591
+ ));
2592
+ }
2593
+ }
2594
+ }
2595
+ }
2596
+ }
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);
1282
2780
  }
1283
- if (!isRecord(artifact.statistics)) {
1284
- out.push(shape(ROOT, "`statistics` must be an object."));
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) };
1285
2798
  }
1286
- if (artifact.guidelines !== void 0) {
1287
- const guidelines = artifact.guidelines;
1288
- if (!isRecord(guidelines) || guidelines.origin !== "generated" || !isRecord(guidelines.group_descriptions)) {
1289
- out.push(shape(
1290
- ROOT,
1291
- '`guidelines` must state origin "generated" and a group_descriptions object.'
1292
- ));
1293
- } else {
1294
- for (const [collectionName, folders] of Object.entries(guidelines.group_descriptions)) {
1295
- if (!isRecord(folders) || Object.values(folders).some((description) => typeof description !== "string")) {
1296
- out.push(shape(
1297
- ROOT,
1298
- `guidelines.group_descriptions[${JSON.stringify(collectionName)}] must map folder names to strings.`
1299
- ));
1300
- }
1301
- }
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
1302
2837
  }
1303
- }
2838
+ };
1304
2839
  }
1305
- function validateLevel1(artifact) {
1306
- try {
1307
- const out = [];
1308
- if (!isRecord(artifact)) {
1309
- out.push(shape(ROOT, "The artifact root must be an object."));
1310
- return out;
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}`;
1311
2877
  }
1312
- validateRootSections(artifact, out);
1313
- if (!Array.isArray(artifact.tokens)) {
1314
- out.push(shape(ROOT, "`tokens` must be an array."));
1315
- } else {
1316
- artifact.tokens.forEach((token, index) => validateToken(token, index, out));
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}`;
1317
2882
  }
1318
- return out;
1319
- } catch (err) {
1320
- 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.";
1321
- return [diagnostic("INCONSISTENT_VALUE_SHAPE", {
1322
- entity_id: "artifact",
1323
- message
1324
- })];
1325
2883
  }
2884
+ return String(value);
2885
+ }
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);
2929
+ }
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
+ };
1326
2983
  }
1327
2984
 
1328
2985
  // ../extractor/src/v5/dtcg.ts
@@ -1344,6 +3001,9 @@ function dtcgSegments(name) {
1344
3001
  }
1345
3002
  return { segments, notes };
1346
3003
  }
3004
+ function dtcgPathOf(collectionName, tokenName) {
3005
+ return [...dtcgSegments(collectionName).segments, ...dtcgSegments(tokenName).segments].join(".");
3006
+ }
1347
3007
  function trimDashes(s) {
1348
3008
  let start = 0;
1349
3009
  let end = s.length;
@@ -1514,25 +3174,73 @@ function unitOverrideFor(p, token, collection) {
1514
3174
  }
1515
3175
  return void 0;
1516
3176
  }
1517
- var STATED_NUMBER_SCOPES = ["FONT_WEIGHT", "OPACITY"];
1518
- function projectedLiteral(p, token, resolved2, onOverrideConflict) {
3177
+ function reportPathOf(p, token) {
3178
+ const path = p.pathById.get(token.id) ?? p.segmentsById.get(token.id)?.join(".") ?? token.name;
3179
+ return p.collidedIds.has(token.id) ? `${path} [${token.id}]` : path;
3180
+ }
3181
+ function ownerFor(p, token) {
3182
+ const path = reportPathOf(p, token);
3183
+ return {
3184
+ overrideConflict: (override) => {
3185
+ reportOnce(p, {
3186
+ code: "unit_override_conflicts_with_scope",
3187
+ severity: "warning",
3188
+ path,
3189
+ message: "A unit override names this token but its scopes state a unitless number; the override was ignored.",
3190
+ details: { id: token.id, override, scopes: [...token.scopes] }
3191
+ });
3192
+ },
3193
+ // Reported without a mode, like the override conflict above: the evidence
3194
+ // is a fact about the token, not about one of its values, so a token in
3195
+ // three modes earns one entry rather than three.
3196
+ derivedUnit: (evidence) => {
3197
+ reportOnce(p, {
3198
+ code: "unit_derived_from_usage",
3199
+ severity: "info",
3200
+ path,
3201
+ // "its own variable", not "no scope": for `via: 'alias-scope'` a scope
3202
+ // is exactly what stated the unit, and this same sentence goes on to
3203
+ // name it. What is true of both kinds of evidence is that the token's
3204
+ // OWN variable states nothing. The CSS header that points a reader at
3205
+ // this entry says it the same way, for the same reason.
3206
+ message: `This token's own variable states no unit, so ${evidence.unit} was taken from how the library uses it: ${evidence.source} ${evidence.via === "binding" ? "binds it to" : "is scoped"} ${evidence.reason}.`,
3207
+ details: {
3208
+ id: token.id,
3209
+ unit: evidence.unit,
3210
+ via: evidence.via,
3211
+ source: evidence.source,
3212
+ reason: evidence.reason
3213
+ }
3214
+ });
3215
+ }
3216
+ };
3217
+ }
3218
+ function projectedLiteral(p, token, resolved2, owner) {
1519
3219
  const collection = p.collectionById.get(token.collection_id);
1520
3220
  const override = collection ? unitOverrideFor(p, token, collection) : void 0;
1521
3221
  let literal = resolved2;
1522
3222
  let overrode = false;
3223
+ let derived;
1523
3224
  if (override !== void 0 && literal.type === "number") {
1524
- if (token.scopes.some((s) => STATED_NUMBER_SCOPES.includes(s))) onOverrideConflict?.(override);
3225
+ if (scopesStateNumber(token.scopes)) owner?.overrideConflict(override);
1525
3226
  else {
1526
3227
  literal = { type: "dimension", number: literal.value, unit: override };
1527
3228
  overrode = true;
1528
3229
  }
3230
+ } else if (literal.type === "number" && !scopesStateUnit(token.scopes)) {
3231
+ const evidence = p.derivedUnits.get(token.id);
3232
+ if (evidence !== void 0) {
3233
+ literal = { type: "dimension", number: literal.value, unit: evidence.unit };
3234
+ derived = evidence;
3235
+ owner?.derivedUnit(evidence);
3236
+ }
1529
3237
  }
1530
3238
  const converted2 = dtcgLiteral(literal, token.scopes, p.options.values);
1531
3239
  if ("omit" in converted2) return { converted: converted2, transform: null };
1532
- return {
1533
- converted: converted2,
1534
- transform: overrode ? "number-unit-override" : literalTransform(literal, token.scopes)
1535
- };
3240
+ let transform = literalTransform(literal, token.scopes);
3241
+ if (overrode) transform = "number-unit-override";
3242
+ else if (derived !== void 0) transform = "number-unit-usage";
3243
+ return { converted: converted2, transform };
1536
3244
  }
1537
3245
  function literalTransform(value, scopes) {
1538
3246
  switch (value.type) {
@@ -1559,7 +3267,24 @@ function literalTransform(value, scopes) {
1559
3267
  }
1560
3268
  function aliasLeafType(p, token, chain, resolved2) {
1561
3269
  const terminal = chain.length > 0 ? p.tokenById.get(chain[chain.length - 1].token_id) : void 0;
1562
- return projectedLiteral(p, terminal ?? token, resolved2).converted;
3270
+ const subject = terminal ?? token;
3271
+ return projectedLiteral(p, subject, resolved2, ownerFor(p, subject)).converted;
3272
+ }
3273
+ function terminalOwnType(p, chain) {
3274
+ const hop = chain.length > 0 ? chain[chain.length - 1] : void 0;
3275
+ const terminal = hop ? p.tokenById.get(hop.token_id) : void 0;
3276
+ const value = terminal && hop ? terminal.values[hop.mode_id] : void 0;
3277
+ if (!terminal || !value || value.kind !== "literal") return void 0;
3278
+ return projectedLiteral(p, terminal, value.value, ownerFor(p, terminal)).converted;
3279
+ }
3280
+ function reportAliasTypeMismatch(p, path, targetPath, ownType, targetType) {
3281
+ reportOnce(p, {
3282
+ code: "alias_type_mismatch",
3283
+ severity: "error",
3284
+ path,
3285
+ message: `This token is "${ownType}" but its alias target ${targetPath} is "${targetType}"; a consumer reading the declared type gets a value the target cannot carry.`,
3286
+ details: { target: targetPath, own_type: ownType, target_type: targetType }
3287
+ });
1563
3288
  }
1564
3289
  function modeLabels(collection) {
1565
3290
  const counts = /* @__PURE__ */ new Map();
@@ -1978,10 +3703,11 @@ function styleCensus(tree) {
1978
3703
  walk(tree);
1979
3704
  return { tokens: a.tokens, types: histogram(a.types), descriptions: { present: a.present, missing: a.missing } };
1980
3705
  }
1981
- function foundationDtcg(artifact, options = {}) {
3706
+ function foundationDtcg(artifact, options = {}, derivedUnits) {
1982
3707
  const p = {
1983
3708
  artifact,
1984
3709
  options: { values: options.values ?? "standard", ...options.units ? { units: options.units } : {} },
3710
+ derivedUnits: derivedUnits ?? /* @__PURE__ */ new Map(),
1985
3711
  tokenById: new Map(artifact.tokens.map((t) => [t.id, t])),
1986
3712
  tokenIds: new Set(artifact.tokens.map((t) => t.id)),
1987
3713
  collectionById: new Map(artifact.collections.map((c) => [c.id, c])),
@@ -2170,45 +3896,754 @@ function tokenLeaf(p, token, collection, modeId) {
2170
3896
  });
2171
3897
  return null;
2172
3898
  }
2173
- recordFact(p, token.id, mode, "alias", typed.$value);
2174
- return { $type: typed.$type, $value: `{${targetPath}}`, ...description };
3899
+ const terminalType = terminalOwnType(p, value.resolved.chain);
3900
+ if (terminalType && !("omit" in terminalType) && terminalType.$type !== typed.$type) {
3901
+ reportAliasTypeMismatch(p, path, targetPath, typed.$type, terminalType.$type);
3902
+ const transform = literalTransform(value.resolved.value, token.scopes);
3903
+ if (transform !== null) recordFact(p, token.id, mode, transform);
3904
+ return { $type: typed.$type, $value: typed.$value, ...description };
3905
+ }
3906
+ recordFact(p, token.id, mode, "alias", typed.$value);
3907
+ return { $type: typed.$type, $value: `{${targetPath}}`, ...description };
3908
+ }
3909
+ const projected = projectedLiteral(p, token, value.value, ownerFor(p, token));
3910
+ const converted2 = projected.converted;
3911
+ if ("omit" in converted2) {
3912
+ reportOnce(p, {
3913
+ code: converted2.omit,
3914
+ severity: "warning",
3915
+ path,
3916
+ mode,
3917
+ message: converted2.omit === "type_not_expressible" ? `DTCG has no ${String(converted2.details.type)} type; the value was omitted.` : `DTCG dimensions take px or rem; a ${String(converted2.details.unit)} value was omitted.`,
3918
+ details: { id: token.id, ...converted2.details }
3919
+ });
3920
+ return null;
3921
+ }
3922
+ if (projected.transform !== null) recordFact(p, token.id, mode, projected.transform);
3923
+ return { $type: converted2.$type, $value: converted2.$value, ...description };
3924
+ }
3925
+ function dtcgExportFiles(out) {
3926
+ const text = (v) => `${JSON.stringify(v, null, 2)}
3927
+ `;
3928
+ const files = {};
3929
+ for (const name of Object.keys(out.files).sort(compareCodeUnits)) files[name] = text(out.files[name]);
3930
+ files["resolver.json"] = text({
3931
+ ...out.resolver,
3932
+ $extensions: { "com.spec-layer": out.extension }
3933
+ });
3934
+ files["spec-layer.meta.json"] = text(out.meta);
3935
+ files["report.json"] = text(out.report);
3936
+ return files;
3937
+ }
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
+
4531
+ // ../extractor/src/v5/usageUnits.ts
4532
+ var LENGTH_SCOPES = ["CORNER_RADIUS", "WIDTH_HEIGHT", "GAP", "FONT_SIZE", "STROKE_FLOAT"];
4533
+ var LENGTH_PROPERTIES = [
4534
+ "border-radius",
4535
+ "border-top-left-radius",
4536
+ "border-top-right-radius",
4537
+ "border-bottom-left-radius",
4538
+ "border-bottom-right-radius",
4539
+ "gap",
4540
+ "height",
4541
+ "padding-x",
4542
+ "padding-y",
4543
+ "width"
4544
+ ];
4545
+ var MAX_ALIAS_DEPTH = 16;
4546
+ var isRecord2 = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
4547
+ function foundationOf(bundle) {
4548
+ const artifact = bundle.foundation?.artifact;
4549
+ if (!isRecord2(artifact)) return null;
4550
+ if (!Array.isArray(artifact.tokens) || !Array.isArray(artifact.collections)) return null;
4551
+ return artifact;
4552
+ }
4553
+ function precedes(a, b) {
4554
+ return (compareCodeUnits(a.via, b.via) || compareCodeUnits(a.source, b.source) || compareCodeUnits(a.reason, b.reason)) < 0;
4555
+ }
4556
+ function bindingsOf(artifact) {
4557
+ if (!isRecord2(artifact) || !isRecord2(artifact.references)) return [];
4558
+ const bindings = artifact.references.bindings;
4559
+ if (!Array.isArray(bindings)) return [];
4560
+ return bindings.filter((b) => isRecord2(b) && b.kind === "variable" && typeof b.source_id === "string" && typeof b.property === "string");
4561
+ }
4562
+ function aliasTargets(token) {
4563
+ const out = [];
4564
+ for (const value of Object.values(token.values)) {
4565
+ if (value.kind !== "alias" || value.reference.external) continue;
4566
+ const id = value.reference.target_id;
4567
+ if (id !== null && !out.includes(id)) out.push(id);
4568
+ }
4569
+ return out;
4570
+ }
4571
+ function usageUnits(bundle) {
4572
+ const evidence = /* @__PURE__ */ new Map();
4573
+ const artifact = foundationOf(bundle);
4574
+ if (artifact === null) return evidence;
4575
+ const tokenById = new Map(artifact.tokens.map((t) => [t.id, t]));
4576
+ const collectionById = new Map(artifact.collections.map((c) => [c.id, c]));
4577
+ const lengthUse = /* @__PURE__ */ new Map();
4578
+ const nonLengthUse = [];
4579
+ for (const component of bundle.components) {
4580
+ for (const binding of bindingsOf(component.artifact)) {
4581
+ if (!LENGTH_PROPERTIES.includes(binding.property)) {
4582
+ nonLengthUse.push(binding.source_id);
4583
+ continue;
4584
+ }
4585
+ const found = {
4586
+ unit: "px",
4587
+ via: "binding",
4588
+ source: component.name,
4589
+ reason: binding.property
4590
+ };
4591
+ const prior = lengthUse.get(binding.source_id);
4592
+ if (prior === void 0 || precedes(found, prior)) lengthUse.set(binding.source_id, found);
4593
+ }
4594
+ }
4595
+ const vetoedIds = /* @__PURE__ */ new Set();
4596
+ const walkChain = (startId, includeStart, visit) => {
4597
+ const seen = /* @__PURE__ */ new Set([startId]);
4598
+ let frontier = [startId];
4599
+ for (let depth = 0; depth <= MAX_ALIAS_DEPTH && frontier.length > 0; depth += 1) {
4600
+ const next = [];
4601
+ for (const id of frontier) {
4602
+ const token = tokenById.get(id);
4603
+ if (id !== startId && token !== void 0 && scopesStateUnit(token.scopes)) continue;
4604
+ if (id !== startId || includeStart) visit(id);
4605
+ if (token === void 0) continue;
4606
+ for (const target of aliasTargets(token)) {
4607
+ if (seen.has(target)) continue;
4608
+ seen.add(target);
4609
+ next.push(target);
4610
+ }
4611
+ }
4612
+ frontier = next;
4613
+ }
4614
+ };
4615
+ for (const id of nonLengthUse) walkChain(id, true, (reached) => vetoedIds.add(reached));
4616
+ for (const token of artifact.tokens) {
4617
+ if (!scopesStateNumber(token.scopes)) continue;
4618
+ walkChain(token.id, false, (reached) => vetoedIds.add(reached));
2175
4619
  }
2176
- const projected = projectedLiteral(p, token, value.value, (override) => {
2177
- reportOnce(p, {
2178
- code: "unit_override_conflicts_with_scope",
2179
- severity: "warning",
2180
- path,
2181
- message: "A unit override names this token but its scopes state a unitless number; the override was ignored.",
2182
- details: { id: token.id, override, scopes: [...token.scopes] }
2183
- });
2184
- });
2185
- const converted2 = projected.converted;
2186
- if ("omit" in converted2) {
2187
- reportOnce(p, {
2188
- code: converted2.omit,
2189
- severity: "warning",
2190
- path,
2191
- mode,
2192
- message: converted2.omit === "type_not_expressible" ? `DTCG has no ${String(converted2.details.type)} type; the value was omitted.` : `DTCG dimensions take px or rem; a ${String(converted2.details.unit)} value was omitted.`,
2193
- details: { id: token.id, ...converted2.details }
2194
- });
2195
- return null;
4620
+ const isCandidate = (id) => {
4621
+ const token = tokenById.get(id);
4622
+ return token !== void 0 && token.type === "number" && !scopesStateUnit(token.scopes);
4623
+ };
4624
+ const record = (id, found) => {
4625
+ if (!isCandidate(id) || vetoedIds.has(id)) return;
4626
+ const prior = evidence.get(id);
4627
+ if (prior === void 0 || precedes(found, prior)) evidence.set(id, found);
4628
+ };
4629
+ const pin = (startId, found, includeStart) => walkChain(startId, includeStart, (id) => record(id, found));
4630
+ for (const token of artifact.tokens) {
4631
+ const scopes = token.scopes.filter((s) => LENGTH_SCOPES.includes(s)).sort(compareCodeUnits);
4632
+ if (scopes.length === 0) continue;
4633
+ const collection = collectionById.get(token.collection_id);
4634
+ if (collection === void 0) continue;
4635
+ pin(token.id, {
4636
+ unit: "px",
4637
+ via: "alias-scope",
4638
+ source: dtcgPathOf(collection.name, token.name),
4639
+ reason: scopes[0]
4640
+ }, false);
2196
4641
  }
2197
- if (projected.transform !== null) recordFact(p, token.id, mode, projected.transform);
2198
- return { $type: converted2.$type, $value: converted2.$value, ...description };
2199
- }
2200
- function dtcgExportFiles(out) {
2201
- const text = (v) => `${JSON.stringify(v, null, 2)}
2202
- `;
2203
- const files = {};
2204
- for (const name of Object.keys(out.files).sort(compareCodeUnits)) files[name] = text(out.files[name]);
2205
- files["resolver.json"] = text({
2206
- ...out.resolver,
2207
- $extensions: { "com.spec-layer": out.extension }
2208
- });
2209
- files["spec-layer.meta.json"] = text(out.meta);
2210
- files["report.json"] = text(out.report);
2211
- return files;
4642
+ for (const token of artifact.tokens) {
4643
+ const found = lengthUse.get(token.id);
4644
+ if (found !== void 0) pin(token.id, found, true);
4645
+ }
4646
+ return evidence;
2212
4647
  }
2213
4648
 
2214
4649
  // ../extractor/src/v5/outputs/naming.ts
@@ -2313,12 +4748,12 @@ function acceptCssDeclared(declared) {
2313
4748
  if (BARE_IDENT.test(declared)) return `--${declared}`;
2314
4749
  return null;
2315
4750
  }
2316
- 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;
2317
4752
  function collectLeaves(tree, prefix, out) {
2318
- const node = asRecord(tree);
4753
+ const node = asRecord2(tree);
2319
4754
  if (!node) return;
2320
4755
  if ("$value" in node) {
2321
- const ours = asRecord(asRecord(node.$extensions)?.[EXT]);
4756
+ const ours = asRecord2(asRecord2(node.$extensions)?.[EXT]);
2322
4757
  out.push({
2323
4758
  path: prefix.join("."),
2324
4759
  type: typeof node.$type === "string" ? node.$type : "",
@@ -2334,7 +4769,7 @@ function collectLeaves(tree, prefix, out) {
2334
4769
  }
2335
4770
  var unpointer = (s) => s.replace(/~1/g, "/").replace(/~0/g, "~");
2336
4771
  var refFile = (src) => {
2337
- const r = asRecord(src);
4772
+ const r = asRecord2(src);
2338
4773
  return r && typeof r.$ref === "string" ? r.$ref : null;
2339
4774
  };
2340
4775
  function sourcesOf(resolver) {
@@ -2409,7 +4844,7 @@ function cssValue(ctx, type, value, property) {
2409
4844
  switch (type) {
2410
4845
  case "color": {
2411
4846
  if (typeof value === "string") return value;
2412
- const c = asRecord(value);
4847
+ const c = asRecord2(value);
2413
4848
  if (c && typeof c.hex === "string" && typeof c.alpha === "number") {
2414
4849
  if (c.alpha === 1) return c.hex;
2415
4850
  const [r, g, b] = hexChannels(c.hex);
@@ -2420,14 +4855,26 @@ function cssValue(ctx, type, value, property) {
2420
4855
  case "dimension":
2421
4856
  case "duration": {
2422
4857
  if (typeof value === "string") return value;
2423
- const d = asRecord(value);
4858
+ const d = asRecord2(value);
2424
4859
  if (d && typeof d.value === "number" && typeof d.unit === "string") return `${d.value}${d.unit}`;
2425
4860
  break;
2426
4861
  }
2427
- case "number":
2428
4862
  case "fontWeight":
2429
4863
  if (typeof value === "number") return String(value);
2430
4864
  break;
4865
+ case "number":
4866
+ if (typeof value === "number") {
4867
+ if (property !== "lineHeight") {
4868
+ report(ctx, {
4869
+ code: "unitless_number",
4870
+ severity: "warning",
4871
+ message: ctx.statesNumber ? "This token has no unit, because its Figma scopes state it is a unitless number. CSS reads it as a number, not a length, which is what those scopes ask for. Do not use it where a length is expected." : "This token has no unit, because its Figma variable states none. CSS reads it as a number, not a length. Narrow the variable's scopes in Figma and pull again.",
4872
+ details: { value, ...member }
4873
+ });
4874
+ }
4875
+ return String(value);
4876
+ }
4877
+ break;
2431
4878
  case "cubicBezier":
2432
4879
  if (Array.isArray(value) && value.length === 4 && value.every((n) => typeof n === "number")) {
2433
4880
  return `cubic-bezier(${value.join(", ")})`;
@@ -2470,7 +4917,7 @@ function typographyMemberKeys(value, ext) {
2470
4917
  }
2471
4918
  for (const key of ["lineHeight", "letterSpacing"]) {
2472
4919
  if (key in value) continue;
2473
- const d = asRecord(ext[key]);
4920
+ const d = asRecord2(ext[key]);
2474
4921
  if (d && typeof d.value === "number" && typeof d.unit === "string") keys.push(key);
2475
4922
  }
2476
4923
  for (const [key, extKey] of TEXT_MEMBERS) {
@@ -2487,7 +4934,7 @@ function converted(ctx, property, from, to) {
2487
4934
  });
2488
4935
  }
2489
4936
  function extensionDimension(ctx, ext, key, name, percentTo) {
2490
- const d = asRecord(ext[key]);
4937
+ const d = asRecord2(ext[key]);
2491
4938
  if (!d || typeof d.value !== "number" || typeof d.unit !== "string") return null;
2492
4939
  if (d.unit === "%") {
2493
4940
  const to = percentTo(canonicalNumber(d.value / 100));
@@ -2499,7 +4946,7 @@ function extensionDimension(ctx, ext, key, name, percentTo) {
2499
4946
  function typographyDecls(ctx, leaf, names) {
2500
4947
  const decls = [];
2501
4948
  const declaredPaths = [];
2502
- const value = asRecord(leaf.value) ?? {};
4949
+ const value = asRecord2(leaf.value) ?? {};
2503
4950
  const ext = leaf.ext ?? {};
2504
4951
  const nameFor = (key) => names.get(`${leaf.path}.${key}`);
2505
4952
  for (const [key, type] of TYPOGRAPHY_MEMBERS2) {
@@ -2532,12 +4979,12 @@ function typographyDecls(ctx, leaf, names) {
2532
4979
  }
2533
4980
  }
2534
4981
  }
2535
- for (const [key, extKey, table] of TEXT_MEMBERS) {
4982
+ for (const [key, extKey, table2] of TEXT_MEMBERS) {
2536
4983
  const raw = ext[extKey];
2537
4984
  if (typeof raw !== "string") continue;
2538
4985
  const name = nameFor(key);
2539
4986
  if (name === void 0) continue;
2540
- const css = table[raw];
4987
+ const css = table2[raw];
2541
4988
  if (css !== void 0) {
2542
4989
  decls.push(`${name}: ${css};`);
2543
4990
  declaredPaths.push(`${leaf.path}.${key}`);
@@ -2568,7 +5015,7 @@ function shadowDecl(ctx, leaf, name) {
2568
5015
  if (layers.length === 0) return omit("no_visible_shadow", "The style has no visible shadow, so no box-shadow was written.");
2569
5016
  const parts = [];
2570
5017
  for (const layer of layers) {
2571
- const l = asRecord(layer);
5018
+ const l = asRecord2(layer);
2572
5019
  if (!l) return omit("layer_not_an_object", "A shadow layer is not an object; the style was omitted.");
2573
5020
  const members = [];
2574
5021
  for (const [key, type] of SHADOW_MEMBERS) {
@@ -2582,22 +5029,59 @@ function shadowDecl(ctx, leaf, name) {
2582
5029
  return `${name}: ${parts.join(", ")};`;
2583
5030
  }
2584
5031
  var commentSafe = (text) => text.replace(/\*\//g, "* /").replace(/[\r\n]+/g, " ");
2585
- function headerText(header, nameCase) {
2586
- return `${CSS_HEADER_PREFIX} from library ${commentSafe(header.libraryId)}, foundation ${header.contentHash}, ${header.platform}/${header.format}/${nameCase}.
2587
- Do not edit. Change the design in Figma, republish, and run spec-layer pull. */`;
5032
+ function headerText(header, nameCase, unitlessCount = 0, derivedCount = 0) {
5033
+ const lines = [
5034
+ `${CSS_HEADER_PREFIX} from library ${commentSafe(header.libraryId)}, foundation ${header.contentHash}, ${header.platform}/${header.format}/${nameCase}.`,
5035
+ " Do not edit. Change the design in Figma, republish, and run spec-layer pull."
5036
+ ];
5037
+ if (unitlessCount > 0) {
5038
+ const reportFile = `${header.platform}-${header.format}.report.json`;
5039
+ if (unitlessCount === 1) {
5040
+ lines.push(
5041
+ " 1 property in this file has no unit, because its Figma variable states none.",
5042
+ ` CSS reads it as a number, not a length. See outputs/${reportFile} under your pull's output directory, or narrow the variable's scopes in Figma.`
5043
+ );
5044
+ } else {
5045
+ lines.push(
5046
+ ` ${unitlessCount} properties in this file have no unit, because their Figma variables state none.`,
5047
+ ` CSS reads them as numbers, not lengths. See outputs/${reportFile} under your pull's output directory, or narrow the variables' scopes in Figma.`
5048
+ );
5049
+ }
5050
+ }
5051
+ if (derivedCount > 0) {
5052
+ lines.push(
5053
+ derivedCount === 1 ? " 1 property in this file has a unit its own Figma variable does not state, taken from how the library uses the token." : ` ${derivedCount} properties in this file have a unit their own Figma variables do not state, taken from how the library uses those tokens.`,
5054
+ ` See tokens/report.json under your pull's output directory for what pinned ${derivedCount === 1 ? "it" : "each one"}.`
5055
+ );
5056
+ }
5057
+ return `${lines.join("\n")} */`;
2588
5058
  }
2589
- function emitPass(sources, leavesByFile, names, alive, root, template, modes) {
5059
+ function emitPass(sources, leavesByFile, names, alive, root, template, modes, facts) {
2590
5060
  const entries = [];
2591
5061
  const declared = /* @__PURE__ */ new Set();
2592
5062
  const firstFile = /* @__PURE__ */ new Map();
2593
5063
  const blocks = /* @__PURE__ */ new Map();
5064
+ const unitlessByFile = /* @__PURE__ */ new Map();
5065
+ const derivedByFile = /* @__PURE__ */ new Map();
5066
+ const note = (by, file, path) => {
5067
+ const set = by.get(file) ?? /* @__PURE__ */ new Set();
5068
+ set.add(path);
5069
+ by.set(file, set);
5070
+ };
2594
5071
  for (const s of sources) {
2595
5072
  const perCollection = modes?.[s.collection];
2596
5073
  const selector = s.isDefault ? root : (perCollection ?? template).replace(/\{mode\}/g, modeSlug(s.file)).replace(/\{collection\}/g, collectionSlug(s.file));
2597
5074
  const decls = [];
2598
5075
  const declaredHere = [];
2599
5076
  for (const leaf of leavesByFile.get(s.file) ?? []) {
2600
- const ctx = { names, alive, report: entries, path: leaf.path, ...s.mode !== null ? { mode: s.mode } : {} };
5077
+ const ctx = {
5078
+ names,
5079
+ alive,
5080
+ report: entries,
5081
+ path: leaf.path,
5082
+ statesNumber: facts.statesNumber.has(leaf.path),
5083
+ ...s.mode !== null ? { mode: s.mode } : {}
5084
+ };
2601
5085
  if (leaf.type === "typography") {
2602
5086
  const t = typographyDecls(ctx, leaf, names);
2603
5087
  decls.push(...t.decls);
@@ -2616,12 +5100,17 @@ function emitPass(sources, leavesByFile, names, alive, root, template, modes) {
2616
5100
  declaredHere.push(leaf.path);
2617
5101
  }
2618
5102
  } else {
5103
+ const before = entries.length;
2619
5104
  const v = cssValue(ctx, leaf.type, leaf.value);
2620
5105
  if (v !== null) {
2621
5106
  decls.push(`${name}: ${v};`);
2622
5107
  declared.add(leaf.path);
2623
5108
  declaredHere.push(leaf.path);
2624
5109
  }
5110
+ if (entries.length > before && entries[entries.length - 1].code === "unitless_number" && !ctx.statesNumber) {
5111
+ note(unitlessByFile, s.file, leaf.path);
5112
+ }
5113
+ if (v !== null && facts.derived.has(leaf.path)) note(derivedByFile, s.file, leaf.path);
2625
5114
  }
2626
5115
  }
2627
5116
  }
@@ -2632,7 +5121,7 @@ function emitPass(sources, leavesByFile, names, alive, root, template, modes) {
2632
5121
  if (existing) existing.decls.push(...decls);
2633
5122
  else blocks.set(s.file, { selector, comment, decls });
2634
5123
  }
2635
- return { blocks, entries, declared, firstFile };
5124
+ return { blocks, entries, declared, firstFile, unitlessByFile, derivedByFile };
2636
5125
  }
2637
5126
  function cssOutput(exp, header, options = {}) {
2638
5127
  const nameCase = options.case ?? CSS_DEFAULTS.case;
@@ -2651,7 +5140,7 @@ function cssOutput(exp, header, options = {}) {
2651
5140
  for (const leaves of leavesByFile.values()) {
2652
5141
  for (const leaf of leaves) {
2653
5142
  if (leaf.type === "typography") {
2654
- const value = asRecord(leaf.value) ?? {};
5143
+ const value = asRecord2(leaf.value) ?? {};
2655
5144
  const ext = leaf.ext ?? {};
2656
5145
  for (const key of typographyMemberKeys(value, ext)) candidatePaths.add(`${leaf.path}.${key}`);
2657
5146
  } else {
@@ -2666,7 +5155,11 @@ function cssOutput(exp, header, options = {}) {
2666
5155
  nameCase
2667
5156
  });
2668
5157
  const names = resolved2.names;
2669
- const emit = (alive2) => emitPass(sources, leavesByFile, names, alive2, root, template, options.modes);
5158
+ const facts = {
5159
+ statesNumber: new Set(Object.entries(exp.meta).filter(([, entry2]) => scopesStateNumber(entry2.scopes)).map(([path]) => path)),
5160
+ derived: new Set(exp.report.filter((entry2) => entry2.code === "unit_derived_from_usage").map((entry2) => entry2.path))
5161
+ };
5162
+ const emit = (alive2) => emitPass(sources, leavesByFile, names, alive2, root, template, options.modes, facts);
2670
5163
  let alive = new Set(names.keys());
2671
5164
  let pass = emit(alive);
2672
5165
  for (; ; ) {
@@ -2693,11 +5186,16 @@ function cssOutput(exp, header, options = {}) {
2693
5186
  });
2694
5187
  }
2695
5188
  }
2696
- const head = headerText(header, nameCase);
2697
5189
  const files = {};
2698
5190
  const imports = [];
2699
5191
  for (const [source, block] of pass.blocks) {
2700
5192
  const name = fileNames.get(source);
5193
+ const head = headerText(
5194
+ header,
5195
+ nameCase,
5196
+ pass.unitlessByFile.get(source)?.size ?? 0,
5197
+ pass.derivedByFile.get(source)?.size ?? 0
5198
+ );
2701
5199
  files[name] = `${head}
2702
5200
 
2703
5201
  ${block.selector} {
@@ -2707,36 +5205,48 @@ ${block.decls.map((d) => ` ${d}`).join("\n")}
2707
5205
  `;
2708
5206
  imports.push(block.comment, `@import "./${name}";`);
2709
5207
  }
2710
- if (imports.length > 0) files[CSS_INDEX_FILE] = `${head}
5208
+ if (imports.length > 0) files[CSS_INDEX_FILE] = `${headerText(header, nameCase)}
2711
5209
 
2712
5210
  ${imports.join("\n")}
2713
5211
  `;
2714
5212
  return { files, map, report: sortReport(entries) };
2715
5213
  }
2716
5214
 
2717
- // ../extractor/src/v5/componentContext.ts
2718
- var import_js_sha2564 = __toESM(require_sha256(), 1);
2719
-
2720
5215
  // ../extractor/src/libraryBundle.ts
2721
5216
  var LIBRARY_BUNDLE_SCHEMA = "spec-layer-library-bundle";
2722
5217
  var LibraryBundleError = class extends Error {
2723
- constructor(code2, message) {
5218
+ constructor(code3, message) {
2724
5219
  super(message);
2725
5220
  this.name = "LibraryBundleError";
2726
- this.code = code2;
5221
+ this.code = code3;
2727
5222
  }
2728
5223
  };
2729
- var isRecord2 = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
5224
+ var isRecord3 = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
2730
5225
  function hasContentHash(artifact) {
2731
- if (!isRecord2(artifact) || !isRecord2(artifact.spec_layer)) return false;
5226
+ if (!isRecord3(artifact) || !isRecord3(artifact.spec_layer)) return false;
2732
5227
  const exp = artifact.spec_layer.export;
2733
- return isRecord2(exp) && typeof exp.content_hash === "string";
5228
+ return isRecord3(exp) && typeof exp.content_hash === "string";
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;
2734
5243
  }
2735
5244
  function entry(v, where) {
2736
- if (!isRecord2(v) || typeof v.name !== "string" || typeof v.ai !== "string" || !hasContentHash(v.artifact)) {
5245
+ if (!isRecord3(v) || typeof v.name !== "string" || typeof v.ai !== "string" || !hasContentHash(v.artifact)) {
2737
5246
  throw new LibraryBundleError("malformed", `The ${where} entry in this bundle is malformed.`);
2738
5247
  }
2739
- 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 } : {} };
2740
5250
  }
2741
5251
  function supportedVersion(version) {
2742
5252
  return typeof version === "string" && /^1\.\d+\.\d+$/.test(version);
@@ -2750,7 +5260,7 @@ function parseLibraryBundle(input) {
2750
5260
  throw new LibraryBundleError("not_json", "This is not valid JSON.");
2751
5261
  }
2752
5262
  }
2753
- if (!isRecord2(parsed) || parsed.schema !== LIBRARY_BUNDLE_SCHEMA) {
5263
+ if (!isRecord3(parsed) || parsed.schema !== LIBRARY_BUNDLE_SCHEMA) {
2754
5264
  throw new LibraryBundleError("not_bundle", "This is not a Spec Layer library bundle.");
2755
5265
  }
2756
5266
  if (!supportedVersion(parsed.version)) {
@@ -2766,7 +5276,7 @@ function parseLibraryBundle(input) {
2766
5276
  const foundationRaw = parsed.foundation ?? null;
2767
5277
  let foundation = null;
2768
5278
  if (foundationRaw !== null) {
2769
- if (!isRecord2(foundationRaw) || typeof foundationRaw.ai !== "string" || !hasContentHash(foundationRaw.artifact)) {
5279
+ if (!isRecord3(foundationRaw) || typeof foundationRaw.ai !== "string" || !hasContentHash(foundationRaw.artifact)) {
2770
5280
  throw new LibraryBundleError("malformed", "The foundation entry in this bundle is malformed.");
2771
5281
  }
2772
5282
  foundation = { ai: foundationRaw.ai, artifact: foundationRaw.artifact };
@@ -2785,6 +5295,39 @@ function parseLibraryBundle(input) {
2785
5295
  // ../extractor/src/libraryBundleHash.ts
2786
5296
  var import_js_sha2565 = __toESM(require_sha256(), 1);
2787
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
+
2788
5331
  // src/bundle.ts
2789
5332
  function parseBundle(raw) {
2790
5333
  try {
@@ -2851,8 +5394,7 @@ var CODE_SYNTAX_KEY = {
2851
5394
  var PLATFORMS = ["web", "ios", "android", "flutter"];
2852
5395
  var AGENT_HOSTS = ["claude", "cursor", "copilot", "windsurf", "gemini", "agents-md"];
2853
5396
  var uniq = (xs) => [...new Set(xs)];
2854
- function readPackageJson(cwd) {
2855
- const path = join2(cwd, "package.json");
5397
+ function readJsonObject(path) {
2856
5398
  if (!existsSync2(path)) return null;
2857
5399
  let parsed;
2858
5400
  try {
@@ -2861,7 +5403,11 @@ function readPackageJson(cwd) {
2861
5403
  return null;
2862
5404
  }
2863
5405
  if (typeof parsed !== "object" || parsed === null) return null;
2864
- const record = parsed;
5406
+ return parsed;
5407
+ }
5408
+ function readPackageJson(cwd) {
5409
+ const record = readJsonObject(join2(cwd, "package.json"));
5410
+ if (!record) return null;
2865
5411
  const deps = {};
2866
5412
  for (const field of ["dependencies", "devDependencies", "peerDependencies"]) {
2867
5413
  const block = record[field];
@@ -2987,6 +5533,97 @@ function isPlatform(value) {
2987
5533
  function isAgentHost(value) {
2988
5534
  return AGENT_HOSTS.includes(value);
2989
5535
  }
5536
+ function fontPackageSlug(family) {
5537
+ return family.toLowerCase().split(" ").join("-");
5538
+ }
5539
+ function dependencyNamesFamily(dependencyName, slug2) {
5540
+ const lower = dependencyName.toLowerCase();
5541
+ return lower === slug2 || lower.endsWith(`/${slug2}`);
5542
+ }
5543
+ function cssNamesFamily(cssText, family) {
5544
+ if (!cssText.includes("@font-face")) return false;
5545
+ return cssText.includes(`"${family}"`) || cssText.includes(`'${family}'`);
5546
+ }
5547
+ function googleFontsLinkNamesFamily(htmlText, family) {
5548
+ for (const quoted of htmlText.split('"')) {
5549
+ for (const fragment of quoted.split("'")) {
5550
+ const trimmed = fragment.trim();
5551
+ const candidate = trimmed.startsWith("//") ? `https:${trimmed}` : trimmed;
5552
+ if (!candidate.startsWith("http")) continue;
5553
+ let url;
5554
+ try {
5555
+ url = new URL(candidate);
5556
+ } catch {
5557
+ continue;
5558
+ }
5559
+ if (url.hostname !== "fonts.googleapis.com") continue;
5560
+ for (const value of url.searchParams.getAll("family")) {
5561
+ if (value === family || value.startsWith(`${family}:`)) return true;
5562
+ }
5563
+ }
5564
+ }
5565
+ return false;
5566
+ }
5567
+ function missingFontSources(families, repo) {
5568
+ const dependencyNames = Object.keys({ ...repo.packageJson.dependencies, ...repo.packageJson.devDependencies });
5569
+ return families.filter((family) => {
5570
+ const slug2 = fontPackageSlug(family);
5571
+ if (dependencyNames.some((name) => dependencyNamesFamily(name, slug2))) return false;
5572
+ if (cssNamesFamily(repo.cssText, family)) return false;
5573
+ if (googleFontsLinkNamesFamily(repo.htmlText, family)) return false;
5574
+ return true;
5575
+ });
5576
+ }
5577
+ function dependencyField(record, field) {
5578
+ const block = record?.[field];
5579
+ if (typeof block !== "object" || block === null) return {};
5580
+ const out = {};
5581
+ for (const [name, range] of Object.entries(block)) {
5582
+ if (typeof range === "string") out[name] = range;
5583
+ }
5584
+ return out;
5585
+ }
5586
+ function readRootCssText(cwd) {
5587
+ let names = [];
5588
+ try {
5589
+ names = readdirSync(cwd);
5590
+ } catch {
5591
+ names = [];
5592
+ }
5593
+ const chunks = [];
5594
+ for (const name of names) {
5595
+ if (!name.endsWith(".css")) continue;
5596
+ try {
5597
+ chunks.push(readFileSync2(join2(cwd, name), "utf8"));
5598
+ } catch {
5599
+ }
5600
+ }
5601
+ return chunks.join("\n");
5602
+ }
5603
+ function readEntryHtmlText(cwd) {
5604
+ const chunks = [];
5605
+ for (const path of [join2(cwd, "index.html"), join2(cwd, "public", "index.html")]) {
5606
+ try {
5607
+ if (existsSync2(path)) chunks.push(readFileSync2(path, "utf8"));
5608
+ } catch {
5609
+ }
5610
+ }
5611
+ return chunks.join("\n");
5612
+ }
5613
+ function readFontRepoSignals(cwd) {
5614
+ const record = readJsonObject(join2(cwd, "package.json"));
5615
+ return {
5616
+ packageJson: {
5617
+ dependencies: dependencyField(record, "dependencies"),
5618
+ devDependencies: dependencyField(record, "devDependencies")
5619
+ },
5620
+ cssText: readRootCssText(cwd),
5621
+ htmlText: readEntryHtmlText(cwd)
5622
+ };
5623
+ }
5624
+ function missingFontSourcesInRepo(families, cwd) {
5625
+ return missingFontSources(families, readFontRepoSignals(cwd));
5626
+ }
2990
5627
 
2991
5628
  // src/outputs.ts
2992
5629
  import { readFileSync as readFileSync3 } from "node:fs";
@@ -3011,15 +5648,17 @@ var inside = (parent, child) => {
3011
5648
  return rel === "" || !rel.startsWith("..") && !isAbsolute(rel);
3012
5649
  };
3013
5650
  var isDotfile = (name) => name.startsWith(".");
5651
+ var markerList = (marker) => typeof marker === "string" ? [marker] : marker;
3014
5652
  function carriesMarker(abs, marker) {
3015
- const newlines = (marker.match(/\n/g) ?? []).length;
3016
- 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));
3017
5656
  const fd = openSync(abs, "r");
3018
5657
  try {
3019
5658
  const buf = Buffer.alloc(wantBytes);
3020
5659
  const read = readSync(fd, buf, 0, wantBytes, 0);
3021
5660
  const text = buf.subarray(0, read).toString("utf8").replace(/\r\n/g, "\n");
3022
- return text.startsWith(marker);
5661
+ return markers.some((m) => text.startsWith(m));
3023
5662
  } finally {
3024
5663
  closeSync(fd);
3025
5664
  }
@@ -3096,18 +5735,18 @@ function parseOutput(value, index) {
3096
5735
  }
3097
5736
  const spec = specOf(r.platform, r.format);
3098
5737
  if (!spec) throw new Error(`${at}: unknown platform/format "${r.platform}/${r.format}". Known: ${knownFormats()}.`);
3099
- const str = (key) => {
5738
+ const str2 = (key) => {
3100
5739
  if (r[key] !== void 0 && typeof r[key] !== "string") throw new Error(`${at} "${key}" must be a string.`);
3101
5740
  return r[key];
3102
5741
  };
3103
- const path = str("path") ?? spec.defaultPath;
5742
+ const path = str2("path") ?? spec.defaultPath;
3104
5743
  if (path === null) throw new Error(`${at} needs "path": ${spec.platform} has no default location.`);
3105
- const nameCase = str("case");
5744
+ const nameCase = str2("case");
3106
5745
  if (nameCase !== void 0 && !NAME_CASES.includes(nameCase)) {
3107
5746
  throw new Error(`${at} "case" takes ${NAME_CASES.join(", ")}.`);
3108
5747
  }
3109
- const root = str("root");
3110
- const modeSelector = str("modeSelector");
5748
+ const root = str2("root");
5749
+ const modeSelector = str2("modeSelector");
3111
5750
  let modes;
3112
5751
  if (r.modes !== void 0) {
3113
5752
  const m = r.modes;
@@ -3163,6 +5802,11 @@ var DEFAULT_API = "https://api.spec-layer.com";
3163
5802
  var DEFAULT_OUT_DIR = ".speclayer";
3164
5803
  var DEFAULT_COMPONENT_SPECS_DIR = "component-specs";
3165
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
+ }
3166
5810
  var invalidConfig = () => new Error(`${CONFIG_NAME} is not valid JSON. Fix or delete it, then retry.`);
3167
5811
  function parseInclude(value) {
3168
5812
  if (typeof value !== "object" || value === null || Array.isArray(value)) throw invalidConfig();
@@ -3201,6 +5845,10 @@ function parseComponentSpecsDir(value) {
3201
5845
  if (dir.length === 0) throw new Error('speclayer.json "componentSpecsDir" must be a non-empty string.');
3202
5846
  return dir;
3203
5847
  }
5848
+ function parseComponentSpecsFormat(value) {
5849
+ if (!isComponentFormat(value)) throw new Error('speclayer.json "componentSpecsFormat" must be "yaml" or "md".');
5850
+ return value;
5851
+ }
3204
5852
  function parsePlatforms(value) {
3205
5853
  if (!Array.isArray(value) || !value.every((p) => typeof p === "string" && isPlatform(p))) {
3206
5854
  throw new Error(`speclayer.json "platforms" must be an array of ${PLATFORMS.join(", ")}.`);
@@ -3235,6 +5883,7 @@ function readConfig(cwd) {
3235
5883
  ...typeof record.libraryId === "string" ? { libraryId: record.libraryId } : {},
3236
5884
  ...typeof record.outDir === "string" ? { outDir: record.outDir } : {},
3237
5885
  ...record.componentSpecsDir !== void 0 ? { componentSpecsDir: parseComponentSpecsDir(record.componentSpecsDir) } : {},
5886
+ ...record.componentSpecsFormat !== void 0 ? { componentSpecsFormat: parseComponentSpecsFormat(record.componentSpecsFormat) } : {},
3238
5887
  ...record.include !== void 0 ? { include: parseInclude(record.include) } : {},
3239
5888
  ...record.dtcg !== void 0 ? { dtcg: parseDtcg(record.dtcg) } : {},
3240
5889
  ...record.platforms !== void 0 ? { platforms: parsePlatforms(record.platforms) } : {},
@@ -3246,6 +5895,7 @@ function writeConfig(cwd, config) {
3246
5895
  libraryId: config.libraryId,
3247
5896
  outDir: config.outDir,
3248
5897
  ...config.componentSpecsDir ? { componentSpecsDir: config.componentSpecsDir } : {},
5898
+ ...config.componentSpecsFormat ? { componentSpecsFormat: config.componentSpecsFormat } : {},
3249
5899
  ...config.include ? { include: config.include } : {},
3250
5900
  ...config.dtcg ? { dtcg: config.dtcg } : {},
3251
5901
  ...config.platforms && config.platforms.length > 0 ? { platforms: config.platforms } : {},
@@ -3275,6 +5925,7 @@ function resolveOptions(cwd, flags, env, manifestLibraryId) {
3275
5925
  // A trailing slash would build "//v1/..." paths the proxy router 404s on.
3276
5926
  api: (flags.api ?? env.SPEC_LAYER_API ?? DEFAULT_API).replace(/\/+$/, ""),
3277
5927
  key: supplied ?? storedKey,
5928
+ ...config?.componentSpecsFormat ? { componentSpecsFormat: config.componentSpecsFormat } : {},
3278
5929
  ...config?.include ? { include: config.include } : {},
3279
5930
  ...config?.dtcg ? { dtcg: config.dtcg } : {},
3280
5931
  ...config?.platforms ? { platforms: config.platforms } : {},
@@ -3298,7 +5949,8 @@ async function fetchBundle(opts) {
3298
5949
  } catch {
3299
5950
  return { kind: "error", message: `Could not reach ${opts.api}.` };
3300
5951
  }
3301
- 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 };
3302
5954
  if (res.status === 401) {
3303
5955
  return {
3304
5956
  kind: "error",
@@ -3312,12 +5964,13 @@ async function fetchBundle(opts) {
3312
5964
  kind: "ok",
3313
5965
  raw,
3314
5966
  publishedAt: res.headers.get("X-Published-At") ?? "unknown",
3315
- bundleHash: createHash("sha256").update(raw).digest("hex")
5967
+ bundleHash: createHash("sha256").update(raw).digest("hex"),
5968
+ version
3316
5969
  };
3317
5970
  }
3318
5971
 
3319
5972
  // src/files.ts
3320
- import { mkdirSync as mkdirSync2, writeFileSync as writeFileSync4, readFileSync as readFileSync5, readdirSync as readdirSync3, rmSync as rmSync2, renameSync as renameSync2, existsSync as existsSync5 } from "node:fs";
5973
+ import { mkdirSync as mkdirSync2, writeFileSync as writeFileSync4, readFileSync as readFileSync6, readdirSync as readdirSync3, rmSync as rmSync2, renameSync as renameSync2, existsSync as existsSync5 } from "node:fs";
3321
5974
  import { join as join5, dirname, relative as relative2, resolve as resolve3, isAbsolute as isAbsolute2, sep } from "node:path";
3322
5975
 
3323
5976
  // src/selection.ts
@@ -3351,17 +6004,38 @@ Available: ${available || "none"}.`
3351
6004
  return bundle.components.map((c) => wanted.some((name) => matchesName(name, c.name)));
3352
6005
  }
3353
6006
 
6007
+ // src/version.ts
6008
+ import { readFileSync as readFileSync5 } from "node:fs";
6009
+ function cliVersion() {
6010
+ try {
6011
+ const parsed = JSON.parse(readFileSync5(new URL("../package.json", import.meta.url), "utf8"));
6012
+ return typeof parsed.version === "string" ? parsed.version : "unknown";
6013
+ } catch {
6014
+ return "unknown";
6015
+ }
6016
+ }
6017
+
3354
6018
  // src/files.ts
3355
- function slugify(name) {
3356
- const slug2 = name.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
3357
- 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;
3358
6033
  }
3359
- var COMPONENT_SPEC_MARKER = "spec_layer:\n kind: component";
3360
6034
  function readManifest(outDir) {
3361
6035
  const path = join5(outDir, "manifest.json");
3362
6036
  if (!existsSync5(path)) return null;
3363
6037
  try {
3364
- const parsed = JSON.parse(readFileSync5(path, "utf8"));
6038
+ const parsed = JSON.parse(readFileSync6(path, "utf8"));
3365
6039
  parsed.artifacts = parsed.artifacts.map((artifact) => {
3366
6040
  const { aiPath, ...rest } = artifact;
3367
6041
  return {
@@ -3378,32 +6052,11 @@ function readLocalBundle(outDir) {
3378
6052
  const path = join5(outDir, "bundle.json");
3379
6053
  if (!existsSync5(path)) return null;
3380
6054
  try {
3381
- return parseBundle(readFileSync5(path, "utf8"));
6055
+ return parseBundle(readFileSync6(path, "utf8"));
3382
6056
  } catch {
3383
6057
  throw new Error(`${path} could not be read as a library bundle. Run spec-layer pull again.`);
3384
6058
  }
3385
6059
  }
3386
- function componentSlugs(bundle) {
3387
- const usedSlugs = /* @__PURE__ */ new Set();
3388
- const nextSuffix = /* @__PURE__ */ new Map();
3389
- return bundle.components.map((component) => {
3390
- const base = slugify(component.name);
3391
- let slug2 = base;
3392
- if (usedSlugs.has(slug2)) {
3393
- let n = (nextSuffix.get(base) ?? 1) + 1;
3394
- slug2 = `${base}-${n}`;
3395
- while (usedSlugs.has(slug2)) {
3396
- n += 1;
3397
- slug2 = `${base}-${n}`;
3398
- }
3399
- nextSuffix.set(base, n);
3400
- } else {
3401
- nextSuffix.set(base, 1);
3402
- }
3403
- usedSlugs.add(slug2);
3404
- return slug2;
3405
- });
3406
- }
3407
6060
  function assertReplaceable(outDir, cwd) {
3408
6061
  const rel = relative2(resolve3(cwd), resolve3(outDir));
3409
6062
  if (rel === "" || rel.startsWith("..") || isAbsolute2(rel)) {
@@ -3417,12 +6070,13 @@ function writeBundleFiles(opts) {
3417
6070
  assertReplaceable(opts.outDir, opts.cwd);
3418
6071
  const selection = opts.selection ?? DEFAULT_SELECTION;
3419
6072
  const selected = selectComponents(opts.bundle, selection);
3420
- const slugs = componentSlugs(opts.bundle);
6073
+ const slugs = componentSlugs(opts.bundle.components.map((c) => c.name));
3421
6074
  const outputs = opts.outputs ?? [];
3422
6075
  const componentSpecsDir = opts.componentSpecsDir ?? DEFAULT_COMPONENT_SPECS_DIR;
6076
+ const componentSpecsFormat = opts.componentSpecsFormat ?? DEFAULT_COMPONENT_FORMAT;
3423
6077
  const outDirRel = relative2(resolve3(opts.cwd), resolve3(opts.outDir)).split(sep).join("/");
3424
6078
  const outputPaths = outputs.map((o) => o.path);
3425
- 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");
3426
6080
  if (specsProblem) throw new Error(specsProblem);
3427
6081
  for (const o of outputs) {
3428
6082
  const problem = outputPathProblem(opts.cwd, outDirRel, o, [componentSpecsDir, ...outputPaths.filter((p) => p !== o.path)]);
@@ -3431,6 +6085,10 @@ function writeBundleFiles(opts) {
3431
6085
  const briefs = {};
3432
6086
  opts.bundle.components.forEach((component, i) => {
3433
6087
  if (!selected[i]) return;
6088
+ if (componentSpecsFormat === "md") {
6089
+ briefs[`${slugs[i]}.md`] = componentMarkdownPage(component);
6090
+ return;
6091
+ }
3434
6092
  if (!component.ai.startsWith(COMPONENT_SPEC_MARKER)) {
3435
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.`);
3436
6094
  }
@@ -3458,7 +6116,12 @@ function writeBundleFiles(opts) {
3458
6116
  if (validateLevel1(artifact).some((d) => d.severity === "error")) {
3459
6117
  throw new Error("The published Foundation context did not pass schema validation. Republish from the plugin, then pull again.");
3460
6118
  }
3461
- const exp = foundationDtcg(artifact, opts.dtcg ?? {});
6119
+ put("fonts.json", json(fontRequirements(artifact)));
6120
+ const exp = foundationDtcg(
6121
+ artifact,
6122
+ opts.dtcg ?? {},
6123
+ usageUnits(opts.bundle)
6124
+ );
3462
6125
  for (const [name, text] of Object.entries(dtcgExportFiles(exp))) put(`tokens/${name}`, text);
3463
6126
  path = `${outDirRel}/tokens/resolver.json`;
3464
6127
  const header = { libraryId: opts.libraryId, contentHash: opts.bundle.foundation.artifact.spec_layer.export.content_hash };
@@ -3481,7 +6144,7 @@ function writeBundleFiles(opts) {
3481
6144
  kind: "component",
3482
6145
  name: component.name,
3483
6146
  contentHash: component.artifact.spec_layer.export.content_hash,
3484
- path: selected[i] ? `${componentSpecsDir}/${slugs[i]}.yaml` : null
6147
+ path: selected[i] ? `${componentSpecsDir}/${slugs[i]}.${componentSpecsFormat}` : null
3485
6148
  });
3486
6149
  });
3487
6150
  const manifest = {
@@ -3490,9 +6153,12 @@ function writeBundleFiles(opts) {
3490
6153
  bundleHash: opts.bundleHash,
3491
6154
  pluginVersion: opts.bundle.pluginVersion,
3492
6155
  extractorVersion: opts.bundle.extractorVersion,
6156
+ cliVersion: cliVersion(),
3493
6157
  selection,
3494
6158
  componentSpecsDir,
6159
+ componentSpecsFormat,
3495
6160
  artifacts,
6161
+ ...opts.version ? { version: opts.version } : {},
3496
6162
  ...opts.dtcg && Object.keys(opts.dtcg).length > 0 ? { dtcg: opts.dtcg } : {},
3497
6163
  ...opts.platforms && opts.platforms.length > 0 ? { platforms: opts.platforms } : {},
3498
6164
  ...opts.outputs ? { outputs: opts.outputs } : {}
@@ -3504,7 +6170,7 @@ function writeBundleFiles(opts) {
3504
6170
  }
3505
6171
  rmSync2(opts.outDir, { recursive: true, force: true });
3506
6172
  renameSync2(staging, opts.outDir);
3507
- 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) };
3508
6174
  const outputResults = [];
3509
6175
  for (const d of deliverables) {
3510
6176
  outputResults.push({ path: d.output.path, files: writeVisibleDir(opts.cwd, d.output.path, CSS_HEADER_PREFIX, d.files, CSS_INDEX_FILE) });
@@ -3513,7 +6179,7 @@ function writeBundleFiles(opts) {
3513
6179
  }
3514
6180
 
3515
6181
  // src/gitignore.ts
3516
- import { readFileSync as readFileSync6, writeFileSync as writeFileSync5, existsSync as existsSync6 } from "node:fs";
6182
+ import { readFileSync as readFileSync7, writeFileSync as writeFileSync5, existsSync as existsSync6 } from "node:fs";
3517
6183
  import { spawnSync } from "node:child_process";
3518
6184
  import { join as join6, dirname as dirname2, resolve as resolve4 } from "node:path";
3519
6185
  var COMMENT = "# Spec Layer pull key, not for committing";
@@ -3550,7 +6216,7 @@ function ensureIgnored(cwd, fileName) {
3550
6216
  ${fileName}
3551
6217
  `);
3552
6218
  } else {
3553
- const body = readFileSync6(path, "utf8");
6219
+ const body = readFileSync7(path, "utf8");
3554
6220
  if (!hasEntryLine(body, fileName)) {
3555
6221
  const lead = body.length === 0 || body.endsWith("\n") ? "" : "\n";
3556
6222
  writeFileSync5(path, `${body}${lead}${COMMENT}
@@ -3567,16 +6233,20 @@ ${fileName}
3567
6233
  }
3568
6234
 
3569
6235
  // src/skill.ts
3570
- import { existsSync as existsSync7, mkdirSync as mkdirSync3, readdirSync as readdirSync4, readFileSync as readFileSync7, writeFileSync as writeFileSync6 } from "node:fs";
6236
+ import { existsSync as existsSync7, mkdirSync as mkdirSync3, readdirSync as readdirSync4, readFileSync as readFileSync8, writeFileSync as writeFileSync6 } from "node:fs";
3571
6237
  import { dirname as dirname3, join as join7 } from "node:path";
3572
6238
 
3573
6239
  // src/tools.ts
3574
6240
  var OK_OR_ERROR = { "0": "success", "1": "usage error, bad key or id, or a network or server failure" };
3575
6241
  var LOCAL_ONLY = { "0": "success", "1": "no local pull, or a usage error" };
6242
+ var PULL_EXITS = {
6243
+ "0": "success, even when tokens/report.json or an outputs/*.report.json holds an error-severity entry (pass --strict to fail on that instead)",
6244
+ "1": "usage error, bad key or id, a network or server failure, or --strict with an error-severity entry in tokens/report.json or an outputs/*.report.json, including on a cached (304) pull"
6245
+ };
3576
6246
  var TOOLS = [
3577
6247
  {
3578
6248
  name: "setup",
3579
- 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]...",
3580
6250
  summary: "Records the library id, stores the pull key in a gitignored speclayer.local.json, then pulls.",
3581
6251
  when: "Once, with the command the plugin's Publish screen hands out. Re-run it after the key is rotated.",
3582
6252
  network: true,
@@ -3593,7 +6263,7 @@ var TOOLS = [
3593
6263
  },
3594
6264
  {
3595
6265
  name: "init",
3596
- 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]...",
3597
6267
  summary: "Writes speclayer.json, with the platforms and default outputs, so later commands need no flags. Stores no key and reaches no server.",
3598
6268
  when: "A repo that supplies the key from SPEC_LAYER_KEY instead of a stored file.",
3599
6269
  network: false,
@@ -3603,9 +6273,9 @@ var TOOLS = [
3603
6273
  },
3604
6274
  {
3605
6275
  name: "pull",
3606
- usage: "spec-layer pull [--id lib_...] [--key sl_...] [--out DIR] [--platform web|ios|android|flutter]... [--only foundation|components] [--component NAME]...",
3607
- 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.",
3608
- when: "After setup, whenever status says the local copy is behind, or after changing the include, dtcg, outputs, or componentSpecsDir blocks.",
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.",
3609
6279
  network: true,
3610
6280
  needsKey: true,
3611
6281
  writes: [
@@ -3613,7 +6283,7 @@ var TOOLS = [
3613
6283
  "outputs[].path from speclayer.json (default tokens/ for web), a directory written in place",
3614
6284
  "componentSpecsDir from speclayer.json (default component-specs/), written in place"
3615
6285
  ],
3616
- exits: OK_OR_ERROR
6286
+ exits: PULL_EXITS
3617
6287
  },
3618
6288
  {
3619
6289
  name: "status",
@@ -3637,8 +6307,8 @@ var TOOLS = [
3637
6307
  },
3638
6308
  {
3639
6309
  name: "show",
3640
- usage: "spec-layer show foundation | component NAME [--canonical] [--out DIR]",
3641
- 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.",
3642
6312
  when: "To read one component or the token document without opening files; it pipes cleanly.",
3643
6313
  network: false,
3644
6314
  needsKey: false,
@@ -3678,7 +6348,7 @@ function toolsText() {
3678
6348
  lines.push(` ${tool.summary}`);
3679
6349
  lines.push(` When: ${tool.when}`);
3680
6350
  lines.push(` Network: ${tool.network ? "yes" : "no"}. Key: ${tool.needsKey ? "required" : "not needed"}. Writes: ${tool.writes.length ? tool.writes.join(", ") : "nothing"}.`);
3681
- 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("; ")}.`);
3682
6352
  lines.push("");
3683
6353
  }
3684
6354
  lines.push("Flags every command accepts where they apply:");
@@ -3700,25 +6370,43 @@ function toolsJson(version) {
3700
6370
 
3701
6371
  // src/skill.ts
3702
6372
  var RESERVED = /* @__PURE__ */ new Set(["resolver.json", "spec-layer.meta.json", "report.json"]);
3703
- function countNumberTokens(tree) {
3704
- if (typeof tree !== "object" || tree === null || Array.isArray(tree)) return 0;
6373
+ function collectNumberTokenPaths(tree, path, legitimatelyUnitless, out) {
6374
+ if (typeof tree !== "object" || tree === null || Array.isArray(tree)) return;
3705
6375
  const record = tree;
3706
- if (record.$type === "number" && "$value" in record) return 1;
3707
- let n = 0;
6376
+ if (record.$type === "number" && "$value" in record) {
6377
+ const dotted = path.join(".");
6378
+ if (!legitimatelyUnitless.has(dotted)) out.add(dotted);
6379
+ return;
6380
+ }
3708
6381
  for (const [key, value] of Object.entries(record)) {
3709
6382
  if (key.startsWith("$")) continue;
3710
- n += countNumberTokens(value);
6383
+ collectNumberTokenPaths(value, [...path, key], legitimatelyUnitless, out);
3711
6384
  }
3712
- return n;
3713
6385
  }
3714
6386
  function readJson(path) {
3715
6387
  if (!existsSync7(path)) return null;
3716
6388
  try {
3717
- return JSON.parse(readFileSync7(path, "utf8"));
6389
+ return JSON.parse(readFileSync8(path, "utf8"));
3718
6390
  } catch {
3719
6391
  return null;
3720
6392
  }
3721
6393
  }
6394
+ function unitlessScopedPaths(tokensDir) {
6395
+ const meta = readJson(join7(tokensDir, "spec-layer.meta.json"));
6396
+ const out = /* @__PURE__ */ new Set();
6397
+ if (typeof meta !== "object" || meta === null || Array.isArray(meta)) return out;
6398
+ for (const [path, entry2] of Object.entries(meta)) {
6399
+ if (typeof entry2 !== "object" || entry2 === null) continue;
6400
+ const { type, scopes } = entry2;
6401
+ if (type === "number" && Array.isArray(scopes) && scopesStateNumber(scopes)) out.add(path);
6402
+ }
6403
+ return out;
6404
+ }
6405
+ function isFontRequirement(v) {
6406
+ if (typeof v !== "object" || v === null) return false;
6407
+ const r = v;
6408
+ return typeof r.family === "string" && Array.isArray(r.weights) && r.weights.every((w) => typeof w === "number") && Array.isArray(r.used_by) && r.used_by.every((u) => typeof u === "string");
6409
+ }
3722
6410
  function summarizePull(cwd, outDir, manifest) {
3723
6411
  if (!manifest) return null;
3724
6412
  const absOut = join7(cwd, outDir);
@@ -3735,17 +6423,37 @@ function summarizePull(cwd, outDir, manifest) {
3735
6423
  } catch {
3736
6424
  tokenFiles = [];
3737
6425
  }
3738
- let unitlessNumbers = 0;
6426
+ const legitimatelyUnitless = unitlessScopedPaths(tokensDir);
6427
+ const unitlessPaths = /* @__PURE__ */ new Set();
3739
6428
  for (const file of tokenFiles) {
3740
6429
  if (file.startsWith("styles.")) continue;
3741
- unitlessNumbers += countNumberTokens(readJson(join7(tokensDir, file)));
6430
+ collectNumberTokenPaths(readJson(join7(tokensDir, file)), [], legitimatelyUnitless, unitlessPaths);
3742
6431
  }
6432
+ const unitlessNumbers = unitlessPaths.size;
3743
6433
  const reportCounts = {};
3744
6434
  if (Array.isArray(report2)) {
3745
6435
  for (const entry2 of report2) {
3746
6436
  if (typeof entry2?.code === "string") reportCounts[entry2.code] = (reportCounts[entry2.code] ?? 0) + 1;
3747
6437
  }
3748
6438
  }
6439
+ const fontsPath = join7(absOut, "fonts.json");
6440
+ let fontsStatus = "missing";
6441
+ let fonts = [];
6442
+ if (existsSync7(fontsPath)) {
6443
+ let parsed;
6444
+ try {
6445
+ parsed = JSON.parse(readFileSync8(fontsPath, "utf8"));
6446
+ } catch {
6447
+ parsed = void 0;
6448
+ }
6449
+ if (Array.isArray(parsed)) {
6450
+ fontsStatus = "ok";
6451
+ fonts = parsed.filter(isFontRequirement);
6452
+ } else {
6453
+ fontsStatus = "unreadable";
6454
+ }
6455
+ }
6456
+ const missingFontFamilies = fontsStatus === "ok" && fonts.length > 0 ? missingFontSourcesInRepo(fonts.map((f) => f.family), cwd) : [];
3749
6457
  foundation = {
3750
6458
  written: foundationEntry.path !== null && resolver !== null,
3751
6459
  sets: resolver ? Object.keys(resolver.sets ?? {}) : [],
@@ -3756,7 +6464,10 @@ function summarizePull(cwd, outDir, manifest) {
3756
6464
  })) : [],
3757
6465
  tokenFiles,
3758
6466
  unitlessNumbers,
3759
- reportCounts
6467
+ reportCounts,
6468
+ fontsStatus,
6469
+ fonts,
6470
+ missingFontFamilies
3760
6471
  };
3761
6472
  }
3762
6473
  return {
@@ -3765,6 +6476,7 @@ function summarizePull(cwd, outDir, manifest) {
3765
6476
  publishedAt: manifest.publishedAt,
3766
6477
  pluginVersion: manifest.pluginVersion,
3767
6478
  componentSpecsDir: manifest.componentSpecsDir ?? DEFAULT_COMPONENT_SPECS_DIR,
6479
+ componentSpecsFormat: manifest.componentSpecsFormat ?? DEFAULT_COMPONENT_FORMAT,
3768
6480
  components,
3769
6481
  foundation,
3770
6482
  outputs: (manifest.outputs ?? []).map((o) => {
@@ -3793,7 +6505,7 @@ function summarizePull(cwd, outDir, manifest) {
3793
6505
  })
3794
6506
  };
3795
6507
  }
3796
- var code = (s) => `\`${s}\``;
6508
+ var code2 = (s) => `\`${s}\``;
3797
6509
  function stackSection(input) {
3798
6510
  const { profile, platforms, platformSource, pull } = input;
3799
6511
  const lines = ["## This codebase", ""];
@@ -3803,7 +6515,7 @@ function stackSection(input) {
3803
6515
  );
3804
6516
  } else {
3805
6517
  lines.push("Detected from the repository root (the file that carries each signal is named, and nothing deeper was read):", "");
3806
- 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)})`);
3807
6519
  lines.push("");
3808
6520
  const facts = [];
3809
6521
  if (profile.languages.length) facts.push(`Languages: ${profile.languages.join(", ")}.`);
@@ -3813,74 +6525,111 @@ function stackSection(input) {
3813
6525
  }
3814
6526
  if (platformSource === "none") {
3815
6527
  lines.push(
3816
- `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.",
3817
6529
  ""
3818
6530
  );
3819
6531
  } else {
3820
6532
  const label = platformSource === "flag" ? "chosen with --platform" : platformSource === "config" ? "set in speclayer.json" : "detected";
3821
6533
  lines.push(`Target platform${platforms.length > 1 ? "s" : ""} (${label}): ${platforms.join(", ")}.`, "");
3822
6534
  }
6535
+ const tokensDir = `${input.outDir}/tokens/`;
6536
+ if (pull?.foundation && pull.foundation.unitlessNumbers > 0) {
6537
+ const n = pull.foundation.unitlessNumbers;
6538
+ const cssReport = pull.outputs.find((o) => o.platform === "web" && o.format === "css" && o.written) ?? null;
6539
+ lines.push(
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.` : "."),
6541
+ ""
6542
+ );
6543
+ }
3823
6544
  for (const platform of platforms) {
3824
6545
  const key = CODE_SYNTAX_KEY[platform];
3825
- const tokensDir = `${input.outDir}/tokens/`;
3826
6546
  if (platform === "web") {
3827
6547
  lines.push("### Web", "");
3828
6548
  const cssOut = pull?.outputs.find((o) => o.platform === "web" && o.format === "css" && o.written) ?? null;
6549
+ if (pull?.foundation?.written) {
6550
+ const { fontsStatus, fonts, missingFontFamilies } = pull.foundation;
6551
+ if (fontsStatus === "ok" && fonts.length > 0) {
6552
+ const fontList = fonts.map((f) => `${f.family} at ${f.weights.join(", ")}`).join("; ");
6553
+ lines.push(
6554
+ `**Fonts.** This library's type is ${fontList}. Load every weight listed; a missing weight renders as a synthesised bold that matches nothing in the design.`
6555
+ );
6556
+ for (const family of missingFontFamilies) {
6557
+ lines.push(
6558
+ `${family}: nothing in this repository loads it. Add a font source before building UI, or every component that uses it renders in the browser default.`
6559
+ );
6560
+ }
6561
+ const fallbackTarget = cssOut ? `${cssOut.path}/` : `${input.outDir}/`;
6562
+ lines.push(
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.`,
6564
+ ""
6565
+ );
6566
+ } else if (fontsStatus === "ok") {
6567
+ lines.push(
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.`,
6569
+ ""
6570
+ );
6571
+ } else {
6572
+ lines.push(
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.`,
6574
+ ""
6575
+ );
6576
+ }
6577
+ }
3829
6578
  if (cssOut) {
3830
6579
  const mapPath = `${input.outDir}/outputs/web-css.map.json`;
3831
6580
  lines.push(
3832
- `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.`,
3833
6582
  ""
3834
6583
  );
3835
6584
  const partFiles = cssOut.files.filter((f) => f !== CSS_INDEX_FILE);
3836
6585
  lines.push(
3837
- `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.`,
3838
6587
  ""
3839
6588
  );
3840
6589
  if (profile.tokenTools.includes("style-dictionary") || profile.tokenTools.includes("tokens-studio")) {
3841
6590
  lines.push(
3842
- `${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.`,
3843
6592
  ""
3844
6593
  );
3845
6594
  }
3846
6595
  } else {
3847
6596
  lines.push(
3848
- `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.`,
3849
6598
  ""
3850
6599
  );
3851
6600
  const configuredNotWritten = pull?.outputs.find((o) => o.platform === "web" && !o.written) ?? null;
3852
6601
  if (configuredNotWritten?.indexMissing) {
3853
6602
  lines.push(
3854
- `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.`,
3855
6604
  ""
3856
6605
  );
3857
6606
  } else if (configuredNotWritten) {
3858
6607
  lines.push(
3859
- `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.`,
3860
6609
  ""
3861
6610
  );
3862
6611
  } else if (pull?.foundation?.written) {
3863
6612
  lines.push(
3864
- `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/")}.`,
3865
6614
  ""
3866
6615
  );
3867
6616
  }
3868
6617
  }
3869
6618
  if (profile.tokenTools.includes("tailwind")) {
3870
6619
  lines.push(
3871
- `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.`,
3872
6621
  ""
3873
6622
  );
3874
6623
  }
3875
6624
  if (profile.tokenTools.includes("style-dictionary")) {
3876
6625
  const major = profile.styleDictionaryMajor;
3877
6626
  lines.push(
3878
- `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.`
3879
6628
  );
3880
6629
  if (major !== null && major < 5) {
3881
6630
  lines.push(
3882
6631
  "",
3883
- `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.`
3884
6633
  );
3885
6634
  }
3886
6635
  lines.push("");
@@ -3888,30 +6637,23 @@ function stackSection(input) {
3888
6637
  } else if (platform === "ios") {
3889
6638
  lines.push("### iOS", "");
3890
6639
  lines.push(
3891
- `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.`,
3892
6641
  ""
3893
6642
  );
3894
6643
  } else if (platform === "android") {
3895
6644
  lines.push("### Android", "");
3896
6645
  lines.push(
3897
- `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.`,
3898
6647
  ""
3899
6648
  );
3900
6649
  } else {
3901
6650
  lines.push("### Flutter", "");
3902
6651
  lines.push(
3903
- `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.`,
3904
6653
  ""
3905
6654
  );
3906
6655
  }
3907
6656
  }
3908
- if (pull?.foundation && pull.foundation.unitlessNumbers > 0) {
3909
- const n = pull.foundation.unitlessNumbers;
3910
- lines.push(
3911
- `${n} token${n === 1 ? " is" : "s are"} exported as ${code('$type: "number"')} because the Figma scopes state no unit. If your code needs them as px or rem, declare it in ${code("speclayer.json")}: ${code('"dtcg": { "units": { "<Collection>/<name glob>": "px" } }')}, then run ${code("spec-layer pull")}. Nothing is inferred from a name; an override that contradicts a stated scope is ignored and listed in ${code("report.json")}.`,
3912
- ""
3913
- );
3914
- }
3915
6657
  return lines;
3916
6658
  }
3917
6659
  function pullSection(input) {
@@ -3919,46 +6661,46 @@ function pullSection(input) {
3919
6661
  const lines = ["## What is on disk", ""];
3920
6662
  if (!pull) {
3921
6663
  lines.push(
3922
- `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.`,
3923
6665
  ""
3924
6666
  );
3925
6667
  return lines;
3926
6668
  }
3927
6669
  lines.push(
3928
- `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.`,
3929
6671
  ""
3930
6672
  );
3931
- lines.push(`- ${code(`${outDir}/manifest.json`)}: every artifact with its content hash and file path.`);
3932
- 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.`);
3933
6675
  if (pull.foundation) {
3934
6676
  if (pull.foundation.written) {
3935
- lines.push(`- ${code(`${outDir}/tokens/`)}: the Foundation as Design Tokens Format Module 2025.10 files.`);
3936
- lines.push(` - ${code("resolver.json")}: sets, modifiers, and resolution order. Start here.`);
3937
- lines.push(` - ${code("spec-layer.meta.json")}: Figma ids, scopes, publication, and ${code("code_syntax")} per DTCG path.`);
3938
- lines.push(` - ${code("report.json")}: what the format could not express, with reasons. Never fill these gaps with a guess.`);
3939
- 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)}`);
3940
6682
  } else {
3941
- 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.`);
3942
6684
  }
3943
6685
  } else {
3944
6686
  lines.push("- This library has no Foundation, so there is no tokens/ directory.");
3945
6687
  }
3946
- lines.push(`- ${code(`${pull.componentSpecsDir}/`)}: one YAML per component.`);
6688
+ lines.push(`- ${code2(`${pull.componentSpecsDir}/`)}: one ${pull.componentSpecsFormat === "md" ? "Markdown page" : "YAML"} per component.`);
3947
6689
  for (const o of pull.outputs) {
3948
- 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.`);
3949
6691
  }
3950
6692
  lines.push("");
3951
6693
  if (pull.foundation && (pull.foundation.sets.length || pull.foundation.modifiers.length)) {
3952
6694
  lines.push("### Token collections", "");
3953
- 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.`);
3954
6696
  for (const m of pull.foundation.modifiers) {
3955
- 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)}` : ""}.`);
3956
6698
  }
3957
6699
  lines.push("");
3958
6700
  const counts = Object.entries(pull.foundation.reportCounts);
3959
6701
  if (counts.length) {
3960
6702
  lines.push(
3961
- `${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.`,
3962
6704
  ""
3963
6705
  );
3964
6706
  }
@@ -3968,7 +6710,7 @@ function pullSection(input) {
3968
6710
  lines.push("The library documents no components.", "");
3969
6711
  } else {
3970
6712
  for (const c of pull.components) {
3971
- 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.`);
3972
6714
  }
3973
6715
  lines.push("");
3974
6716
  }
@@ -3979,42 +6721,43 @@ function commandsSection() {
3979
6721
  const cell = (s) => s.replace(/\|/g, "\\|");
3980
6722
  lines.push("| Command | What it does | When | Network | Key | Writes |", "|---|---|---|---|---|---|");
3981
6723
  for (const t of TOOLS) {
3982
- 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"} |`);
3983
6725
  }
3984
6726
  lines.push("");
3985
6727
  lines.push("Exit codes:", "");
3986
6728
  for (const t of TOOLS) {
3987
- 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("; ")}.`);
3988
6730
  }
3989
6731
  lines.push("");
3990
- 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}`);
3991
6733
  lines.push("", KEY_RESOLUTION, "");
3992
- 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.`, "");
3993
6735
  return lines;
3994
6736
  }
3995
6737
  function buildSkillGuide(input) {
3996
6738
  const { outDir } = input;
3997
6739
  const lines = [];
6740
+ const markdown = (input.pull?.componentSpecsFormat ?? input.config?.componentSpecsFormat ?? DEFAULT_COMPONENT_FORMAT) === "md";
3998
6741
  lines.push("# Spec Layer: design-system context for this repository", "");
3999
6742
  lines.push(
4000
- `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.",
4001
6744
  ""
4002
6745
  );
4003
6746
  const componentSpecsDir = input.pull?.componentSpecsDir ?? input.config?.componentSpecsDir ?? DEFAULT_COMPONENT_SPECS_DIR;
4004
6747
  lines.push("## How to use it", "");
4005
- 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.`);
4006
- 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.`);
4007
- 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.`);
4008
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.`);
4009
- 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.`);
4010
6753
  const writtenOutputs = input.pull?.outputs.filter((o) => o.written) ?? [];
4011
- const outputNote = writtenOutputs.length ? ` Never edit ${writtenOutputs.map((o) => code(`${o.path}/`)).join(", ")} either: pull replaces or removes files there.` : "";
4012
- 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.`);
4013
6756
  lines.push("");
4014
6757
  lines.push(...pullSection(input));
4015
6758
  lines.push(...stackSection(input));
4016
6759
  lines.push(...commandsSection());
4017
- 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.`);
4018
6761
  return `${lines.join("\n")}
4019
6762
  `;
4020
6763
  }
@@ -4087,30 +6830,27 @@ ${BLOCK_END}
4087
6830
  const sep2 = existing.length === 0 ? "" : existing.endsWith("\n\n") ? "" : existing.endsWith("\n") ? "\n" : "\n\n";
4088
6831
  return `${existing}${sep2}${block}`;
4089
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
+ }
4090
6839
  function installSkill(cwd, host, guide) {
4091
6840
  const target = installTarget(host);
4092
6841
  const abs = join7(cwd, target.path);
4093
- const existing = existsSync7(abs) ? readFileSync7(abs, "utf8") : null;
6842
+ const existing = existsSync7(abs) ? readFileSync8(abs, "utf8") : null;
6843
+ const staleSnapshot = staleSnapshotDirs(cwd, target);
4094
6844
  const next = target.mode === "file" ? renderForHost(host, guide) : upsertBlock(existing, renderForHost(host, guide));
4095
- if (existing === next) return { path: target.path, result: "unchanged" };
6845
+ if (existing === next) return { path: target.path, result: "unchanged", staleSnapshot };
4096
6846
  mkdirSync3(dirname3(abs), { recursive: true });
4097
6847
  writeFileSync6(abs, next);
4098
- return { path: target.path, result: existing === null ? "created" : "updated" };
4099
- }
4100
-
4101
- // src/version.ts
4102
- import { readFileSync as readFileSync8 } from "node:fs";
4103
- function cliVersion() {
4104
- try {
4105
- const parsed = JSON.parse(readFileSync8(new URL("../package.json", import.meta.url), "utf8"));
4106
- return typeof parsed.version === "string" ? parsed.version : "unknown";
4107
- } catch {
4108
- return "unknown";
4109
- }
6848
+ return { path: target.path, result: existing === null ? "created" : "updated", staleSnapshot };
4110
6849
  }
4111
6850
 
4112
6851
  // src/commands.ts
4113
6852
  var NO_LOCAL_PULL = "No local pull found. Run spec-layer pull.";
6853
+ var FORMAT_NAME = { yaml: "YAML", md: "Markdown" };
4114
6854
  function manifestReader() {
4115
6855
  const cache = /* @__PURE__ */ new Map();
4116
6856
  return (outDir) => {
@@ -4121,7 +6861,7 @@ function manifestReader() {
4121
6861
  function sameOutput(a, b) {
4122
6862
  const selectionKey = (s) => JSON.stringify([s.foundation, s.components === null ? null : [...new Set(s.components.map(slugify))].sort()]);
4123
6863
  const key = (v) => JSON.stringify(sortKeys(v ?? {}));
4124
- 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);
4125
6865
  }
4126
6866
  function sortKeys(value) {
4127
6867
  if (Array.isArray(value)) return value.map(sortKeys);
@@ -4142,6 +6882,13 @@ function platformsFromFlags(flags, io2) {
4142
6882
  }
4143
6883
  return out;
4144
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
+ }
4145
6892
  function resolvePlatforms(cwd, fromFlags, config) {
4146
6893
  if (fromFlags) return { platforms: fromFlags, source: "flag" };
4147
6894
  if (config?.platforms && config.platforms.length > 0) return { platforms: config.platforms, source: "config" };
@@ -4174,6 +6921,8 @@ function runInit(cwd, flags, io2) {
4174
6921
  }
4175
6922
  const fromFlags = platformsFromFlags(flags, io2);
4176
6923
  if (fromFlags === null) return 1;
6924
+ const format = componentFormatFromFlags(flags, io2);
6925
+ if (format === null) return 1;
4177
6926
  const { platforms, source } = resolvePlatforms(cwd, fromFlags, null);
4178
6927
  const outputs = defaultOutputs(platforms);
4179
6928
  const outDir = flags.out ?? DEFAULT_OUT_DIR;
@@ -4181,6 +6930,7 @@ function runInit(cwd, flags, io2) {
4181
6930
  libraryId: flags.id,
4182
6931
  outDir,
4183
6932
  componentSpecsDir: DEFAULT_COMPONENT_SPECS_DIR,
6933
+ ...format ? { componentSpecsFormat: format } : {},
4184
6934
  ...include ? { include } : {},
4185
6935
  ...platforms.length > 0 ? { platforms } : {},
4186
6936
  ...outputs.length > 0 ? { outputs } : {}
@@ -4252,16 +7002,20 @@ async function runSetup(cwd, flags, env, io2, fetcher) {
4252
7002
  }
4253
7003
  const fromFlags = platformsFromFlags(flags, io2);
4254
7004
  if (fromFlags === null) return 1;
7005
+ const format = componentFormatFromFlags(flags, io2);
7006
+ if (format === null) return 1;
4255
7007
  const outDir = flags.out ?? existing?.outDir ?? DEFAULT_OUT_DIR;
4256
7008
  const componentSpecsDir = existing?.componentSpecsDir ?? DEFAULT_COMPONENT_SPECS_DIR;
4257
7009
  const keptInclude = include ?? existing?.include ?? null;
4258
7010
  const keptDtcg = existing?.dtcg ?? null;
7011
+ const keptFormat = format ?? existing?.componentSpecsFormat ?? null;
4259
7012
  const { platforms } = resolvePlatforms(cwd, fromFlags, existing);
4260
7013
  const outputs = withDefaults(existing?.outputs ?? [], platforms);
4261
7014
  writeConfig(cwd, {
4262
7015
  libraryId: flags.id,
4263
7016
  outDir,
4264
7017
  componentSpecsDir,
7018
+ ...keptFormat ? { componentSpecsFormat: keptFormat } : {},
4265
7019
  ...keptInclude ? { include: keptInclude } : {},
4266
7020
  ...keptDtcg ? { dtcg: keptDtcg } : {},
4267
7021
  ...platforms.length > 0 ? { platforms } : {},
@@ -4305,8 +7059,8 @@ git rm --cached ${ignored.line}`);
4305
7059
  }
4306
7060
  const { replaced } = writeCredentials(cwd, { libraryId: flags.id, key });
4307
7061
  io2.out(replaced ? `Replaced the stored key in ${CREDENTIALS_NAME}.` : `Stored the pull key in ${CREDENTIALS_NAME}.`);
4308
- const code2 = await runPull(cwd, { ...flags, key }, env, io2, fetcher);
4309
- if (code2 !== 0) return code2;
7062
+ const code3 = await runPull(cwd, { ...flags, key }, env, io2, fetcher);
7063
+ if (code3 !== 0) return code3;
4310
7064
  const hosts = detectRepo(cwd).agents;
4311
7065
  io2.out("");
4312
7066
  io2.out("Next step for a coding agent: npx spec-layer skill --install");
@@ -4315,11 +7069,62 @@ git rm --cached ${ignored.line}`);
4315
7069
  return 0;
4316
7070
  }
4317
7071
  function outputFilesOnDisk(cwd, outDir, o) {
4318
- const mapPath = join8(cwd, outDir, "outputs", `${outputId(o)}.map.json`);
4319
- if (!existsSync8(mapPath)) return false;
7072
+ const id = outputId(o);
7073
+ for (const rel of [`${id}.map.json`, `${id}.report.json`]) {
7074
+ if (!existsSync8(join8(cwd, outDir, "outputs", rel))) return false;
7075
+ }
4320
7076
  const imports = readIndexImports(cwd, o);
4321
7077
  return imports !== null && imports.every((f) => existsSync8(resolve5(cwd, o.path, f)));
4322
7078
  }
7079
+ function foundationFilesOnDisk(cwd, outDir) {
7080
+ return ["fonts.json", join8("tokens", "report.json"), join8("tokens", "resolver.json")].every((rel) => existsSync8(join8(cwd, outDir, rel)));
7081
+ }
7082
+ function readJsonArray(path) {
7083
+ if (!existsSync8(path)) return [];
7084
+ try {
7085
+ const parsed = JSON.parse(readFileSync9(path, "utf8"));
7086
+ return Array.isArray(parsed) ? parsed : [];
7087
+ } catch {
7088
+ return [];
7089
+ }
7090
+ }
7091
+ function readReportSeverities(path) {
7092
+ return readJsonArray(path).map((entry2) => entry2.severity).filter((s) => s === "error" || s === "warning" || s === "info");
7093
+ }
7094
+ function readFontFamilies(cwd, outDir) {
7095
+ return readJsonArray(join8(cwd, outDir, "fonts.json")).map((entry2) => entry2.family).filter((f) => typeof f === "string");
7096
+ }
7097
+ function plural(n, word) {
7098
+ return `${n} ${word}${n === 1 ? "" : "s"}`;
7099
+ }
7100
+ function printReportSummary(cwd, outDir, outputs, io2) {
7101
+ const reportPaths = [];
7102
+ let errors = 0;
7103
+ let warnings = 0;
7104
+ const add = (severities, path) => {
7105
+ if (severities.length === 0) return;
7106
+ errors += severities.filter((s) => s === "error").length;
7107
+ warnings += severities.filter((s) => s === "warning").length;
7108
+ reportPaths.push(path);
7109
+ };
7110
+ add(readReportSeverities(join8(cwd, outDir, "tokens", "report.json")), `${outDir}/tokens/report.json`);
7111
+ for (const o of outputs) {
7112
+ add(readReportSeverities(join8(cwd, outDir, "outputs", `${outputId(o)}.report.json`)), `${outDir}/outputs/${outputId(o)}.report.json`);
7113
+ }
7114
+ if (errors > 0 || warnings > 0) {
7115
+ io2.err(`${plural(errors, "error")}, ${plural(warnings, "warning")} in the token output. See ${reportPaths.join(", ")}.`);
7116
+ }
7117
+ const fontFamilies = readFontFamilies(cwd, outDir);
7118
+ if (fontFamilies.length > 0) {
7119
+ for (const family of missingFontSourcesInRepo(fontFamilies, cwd)) {
7120
+ io2.err(`This library needs ${family}, and nothing in this repository loads it. See fonts.json.`);
7121
+ }
7122
+ }
7123
+ return errors;
7124
+ }
7125
+ function publishedPhrase(version, publishedAt) {
7126
+ return version ? `(v${version}, published ${publishedAt})` : `(published ${publishedAt})`;
7127
+ }
4323
7128
  async function runPull(cwd, flags, env, io2, fetcher) {
4324
7129
  const manifestAt = manifestReader();
4325
7130
  const opts = resolved(cwd, flags, env, io2, manifestAt);
@@ -4333,16 +7138,25 @@ async function runPull(cwd, flags, env, io2, fetcher) {
4333
7138
  }
4334
7139
  const fromFlags = platformsFromFlags(flags, io2);
4335
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;
4336
7144
  const { platforms, source } = resolvePlatforms(cwd, fromFlags, opts);
4337
7145
  const outputs = outputsForRun(fromFlags, opts, platforms);
4338
7146
  const manifest = manifestAt(join8(cwd, opts.outDir));
4339
7147
  const foundationOnDisk = Boolean(manifest?.artifacts.find((a) => a.kind === "foundation")?.path);
4340
7148
  const willWriteFoundation = selection.foundation && foundationOnDisk;
4341
7149
  const briefsOnDisk = (manifest?.artifacts ?? []).filter((a) => a.kind === "component" && a.path !== null).every((a) => existsSync8(resolve5(cwd, a.path)));
4342
- const etag = manifest && sameOutput(
4343
- { selection: manifest.selection ?? DEFAULT_SELECTION, dtcg: manifest.dtcg, outputs: manifest.outputs, componentSpecsDir: manifest.componentSpecsDir },
4344
- { selection, dtcg: opts.dtcg, outputs, componentSpecsDir: opts.componentSpecsDir }
4345
- ) && briefsOnDisk && (!willWriteFoundation || outputs.every((o) => outputFilesOnDisk(cwd, opts.outDir, o))) ? manifest.bundleHash : void 0;
7150
+ const etag = manifest && manifest.cliVersion === cliVersion() && sameOutput(
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 }
7159
+ ) && briefsOnDisk && (!willWriteFoundation || foundationFilesOnDisk(cwd, opts.outDir)) && (!willWriteFoundation || outputs.every((o) => outputFilesOnDisk(cwd, opts.outDir, o))) ? manifest.bundleHash : void 0;
4346
7160
  const result = await fetchBundle({
4347
7161
  api: opts.api,
4348
7162
  libraryId: opts.libraryId,
@@ -4355,7 +7169,9 @@ async function runPull(cwd, flags, env, io2, fetcher) {
4355
7169
  return 1;
4356
7170
  }
4357
7171
  if (result.kind === "not_modified") {
4358
- 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")}.`);
7173
+ const cachedErrors = printReportSummary(cwd, opts.outDir, outputs, io2);
7174
+ if (flags.strict && cachedErrors > 0) return 1;
4359
7175
  return 0;
4360
7176
  }
4361
7177
  let written;
@@ -4373,16 +7189,18 @@ async function runPull(cwd, flags, env, io2, fetcher) {
4373
7189
  libraryId: opts.libraryId,
4374
7190
  publishedAt: result.publishedAt,
4375
7191
  bundleHash: result.bundleHash,
7192
+ version: result.version,
4376
7193
  dtcg: opts.dtcg,
4377
7194
  platforms,
4378
7195
  outputs,
4379
- componentSpecsDir: opts.componentSpecsDir
7196
+ componentSpecsDir: opts.componentSpecsDir,
7197
+ componentSpecsFormat
4380
7198
  });
4381
7199
  written = writeResult.written;
4382
7200
  componentSpecs = writeResult.componentSpecs;
4383
7201
  outputResults = writeResult.outputs;
4384
7202
  io2.out(
4385
- `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)}.`
4386
7204
  );
4387
7205
  } catch (err) {
4388
7206
  io2.err(errorText(err));
@@ -4390,7 +7208,10 @@ async function runPull(cwd, flags, env, io2, fetcher) {
4390
7208
  }
4391
7209
  const count = (n) => `${n} file${n === 1 ? "" : "s"}`;
4392
7210
  io2.out(`Wrote ${written.length} files under ${opts.outDir}/.`);
4393
- 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
+ }
4394
7215
  for (const r of outputResults) {
4395
7216
  const o = outputs.find((x) => x.path === r.path);
4396
7217
  if (o) io2.out(`Wrote ${r.path}/ (${count(r.files.length)}, ${o.platform}/${o.format}, ${o.case} names).`);
@@ -4410,6 +7231,8 @@ async function runPull(cwd, flags, env, io2, fetcher) {
4410
7231
  const missing = platformsMissingFormat(platforms);
4411
7232
  if (missing.length > 0) io2.out(missingFormatNote(missing));
4412
7233
  }
7234
+ const errors = printReportSummary(cwd, opts.outDir, outputs, io2);
7235
+ if (flags.strict && errors > 0) return 1;
4413
7236
  return 0;
4414
7237
  }
4415
7238
  async function runStatus(cwd, flags, env, io2, fetcher) {
@@ -4433,10 +7256,10 @@ async function runStatus(cwd, flags, env, io2, fetcher) {
4433
7256
  return 1;
4434
7257
  }
4435
7258
  if (result.kind === "not_modified") {
4436
- io2.out(`Up to date (published ${manifest.publishedAt}).`);
7259
+ io2.out(`Up to date ${publishedPhrase(result.version ?? manifest.version, manifest.publishedAt)}.`);
4437
7260
  return 0;
4438
7261
  }
4439
- 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.`);
4440
7263
  return 2;
4441
7264
  }
4442
7265
  function runList(cwd, flags, io2) {
@@ -4447,7 +7270,7 @@ function runList(cwd, flags, io2) {
4447
7270
  io2.err(NO_LOCAL_PULL);
4448
7271
  return 1;
4449
7272
  }
4450
- 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}.`);
4451
7274
  const rows = manifest.artifacts.map((a) => [a.kind, a.name, a.path ?? "not written", a.contentHash]);
4452
7275
  const widths = [0, 1, 2].map((i) => Math.max(...rows.map((r) => r[i].length)));
4453
7276
  for (const row of rows) {
@@ -4468,6 +7291,12 @@ function runShow(cwd, flags, args, io2) {
4468
7291
  io2.err(SHOW_USAGE);
4469
7292
  return 1;
4470
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
+ }
4471
7300
  const outDir = resolvedOutDir(cwd, flags, io2);
4472
7301
  if (!outDir) return 1;
4473
7302
  let bundle;
@@ -4482,6 +7311,7 @@ function runShow(cwd, flags, args, io2) {
4482
7311
  return 1;
4483
7312
  }
4484
7313
  let entry2;
7314
+ let component = null;
4485
7315
  if (wantsFoundation) {
4486
7316
  if (!bundle.foundation) {
4487
7317
  io2.err("This library has no Foundation. Run spec-layer list to see what it holds.");
@@ -4501,9 +7331,33 @@ Available: ${available || "none"}.`);
4501
7331
  return 1;
4502
7332
  }
4503
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
+ }
4504
7359
  }
4505
- io2.write(flags.canonical ? `${JSON.stringify(entry2.artifact, null, 2)}
4506
- ` : entry2.ai);
7360
+ io2.write(entry2.ai);
4507
7361
  return 0;
4508
7362
  }
4509
7363
  function runTools(flags, io2) {
@@ -4575,6 +7429,11 @@ function runSkill(cwd, flags, io2) {
4575
7429
  }
4576
7430
  const verb = outcome.result === "created" ? "Wrote" : outcome.result === "updated" ? "Updated" : "Unchanged:";
4577
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
+ }
4578
7437
  }
4579
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.`);
4580
7439
  if (input.platformSource === "none") io2.out(`No target platform detected. Pass --platform ${PLATFORMS.join("|")} to write platform-specific token advice.`);
@@ -4585,16 +7444,17 @@ function runSkill(cwd, flags, io2) {
4585
7444
  var USAGE = `spec-layer <command>
4586
7445
 
4587
7446
  Commands:
4588
- setup --id lib_... --key sl_... [--out DIR] [selection] [--platform P]...
7447
+ setup --id lib_... --key sl_... [--out DIR] [selection] [--platform P]... [--component-format F]
4589
7448
  store the key, then pull
4590
- init --id lib_... [--out DIR] [selection] [--platform P]...
7449
+ init --id lib_... [--out DIR] [selection] [--platform P]... [--component-format F]
4591
7450
  write speclayer.json
4592
- pull [--id lib_...] [--key sl_...] [selection] [--platform P]...
4593
- fetch the library into DIR (default .speclayer); the foundation lands as DTCG under DIR/tokens/
7451
+ pull [--id lib_...] [--key sl_...] [selection] [--platform P]... [--component-format F] [--strict]
7452
+ fetch the library into DIR (default .speclayer); the foundation lands as DTCG under DIR/tokens/;
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)
4594
7454
  status [--id lib_...] [--key sl_...] check freshness; exits 2 when behind
4595
7455
  list list every artifact in the last pull
4596
- show foundation | component NAME [--canonical]
4597
- 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)
4598
7458
  tools [--json] list every command with what it reaches and writes
4599
7459
  skill [--install] [--agent HOST]... [--platform P]... [--json]
4600
7460
  print a guide for a coding agent, adapted to this repo and the last pull;
@@ -4607,6 +7467,7 @@ Selection (setup, pull and init; flags replace the include block in speclayer.js
4607
7467
  Options:
4608
7468
  --api URL override the API origin (default https://api.spec-layer.com)
4609
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
4610
7471
  The pull key comes from --key, SPEC_LAYER_KEY, or speclayer.local.json written by setup.`;
4611
7472
  var io = {
4612
7473
  out: (l) => console.log(l),
@@ -4631,8 +7492,10 @@ async function main() {
4631
7492
  canonical: { type: "boolean" },
4632
7493
  json: { type: "boolean" },
4633
7494
  install: { type: "boolean" },
7495
+ strict: { type: "boolean" },
4634
7496
  agent: { type: "string", multiple: true },
4635
- platform: { type: "string", multiple: true }
7497
+ platform: { type: "string", multiple: true },
7498
+ "component-format": { type: "string" }
4636
7499
  }
4637
7500
  }));
4638
7501
  } catch {