spec-layer 0.7.0 → 0.8.2

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 +4 -1
  2. package/dist/cli.js +180 -11
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -62,7 +62,10 @@ Module 2025.10 files, rather than `ai/foundation.yaml`, needs 0.4.0 or later.
62
62
  `ai/components/`, `path` in place of `aiPath` in the manifest, and the
63
63
  `outputs` block need 0.6.0 or later. `component-specs/` beside `tokens/`,
64
64
  `componentSpecsDir`, cwd-relative manifest paths, and the `tokens/` directory
65
- need 0.7.0 or later.
65
+ need 0.7.0 or later. The `census` and `config_hash` blocks inside
66
+ `resolver.json`, and the `transform` and `resolved` fields in
67
+ `spec-layer.meta.json`, need 0.8.0 or later; an earlier version pulls the same
68
+ files without those fields.
66
69
 
67
70
  ## Commands
68
71
 
package/dist/cli.js CHANGED
@@ -816,6 +816,15 @@ var SUPPORTED_DURATION_UNITS = ["ms", "s"];
816
816
 
817
817
  // ../extractor/src/v5/canonical.ts
818
818
  var import_js_sha2562 = __toESM(require_sha256(), 1);
819
+ var SCHEMA_VERSION = "5.1.0";
820
+ function canonicalJson(value) {
821
+ if (Array.isArray(value)) return `[${value.map(canonicalJson).join(",")}]`;
822
+ if (value && typeof value === "object") {
823
+ const entries = Object.entries(value).filter(([, v]) => v !== void 0).sort(([a], [b]) => compareCodeUnits(a, b)).map(([k, v]) => `${JSON.stringify(k)}:${canonicalJson(v)}`);
824
+ return `{${entries.join(",")}}`;
825
+ }
826
+ return JSON.stringify(value);
827
+ }
819
828
 
820
829
  // ../extractor/src/v5/validate.ts
821
830
  var ROOT = "<artifact>";
@@ -1317,6 +1326,7 @@ function validateLevel1(artifact) {
1317
1326
  }
1318
1327
 
1319
1328
  // ../extractor/src/v5/dtcg.ts
1329
+ var import_js_sha2563 = __toESM(require_sha256(), 1);
1320
1330
  function dtcgSegments(name) {
1321
1331
  const segments = [];
1322
1332
  const notes = [];
@@ -1435,6 +1445,15 @@ function sortTree(value) {
1435
1445
  const keys = Object.keys(value).sort((a, b) => rank(a) - rank(b) || compareCodeUnits(a, b));
1436
1446
  return Object.fromEntries(keys.map((k) => [k, sortTree(value[k])]));
1437
1447
  }
1448
+ function recordFact(p, tokenId, mode, transform, resolved2) {
1449
+ let facts = p.factsById.get(tokenId);
1450
+ if (!facts) {
1451
+ facts = { transform: {}, resolved: {} };
1452
+ p.factsById.set(tokenId, facts);
1453
+ }
1454
+ facts.transform[mode] = transform;
1455
+ if (resolved2 !== void 0) facts.resolved[mode] = resolved2;
1456
+ }
1438
1457
  function reportOnce(p, entry2) {
1439
1458
  const key = JSON.stringify([entry2.code, entry2.path, entry2.mode ?? null, entry2.details]);
1440
1459
  if (p.reportKeys.has(key)) return;
@@ -1500,15 +1519,47 @@ function projectedLiteral(p, token, resolved2, onOverrideConflict) {
1500
1519
  const collection = p.collectionById.get(token.collection_id);
1501
1520
  const override = collection ? unitOverrideFor(p, token, collection) : void 0;
1502
1521
  let literal = resolved2;
1522
+ let overrode = false;
1503
1523
  if (override !== void 0 && literal.type === "number") {
1504
1524
  if (token.scopes.some((s) => STATED_NUMBER_SCOPES.includes(s))) onOverrideConflict?.(override);
1505
- else literal = { type: "dimension", number: literal.value, unit: override };
1525
+ else {
1526
+ literal = { type: "dimension", number: literal.value, unit: override };
1527
+ overrode = true;
1528
+ }
1529
+ }
1530
+ const converted2 = dtcgLiteral(literal, token.scopes, p.options.values);
1531
+ 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
+ };
1536
+ }
1537
+ function literalTransform(value, scopes) {
1538
+ switch (value.type) {
1539
+ case "color":
1540
+ return "color";
1541
+ case "dimension":
1542
+ return "dimension";
1543
+ case "duration":
1544
+ return "duration";
1545
+ case "number":
1546
+ return scopes.includes("FONT_WEIGHT") ? "font-weight" : "number";
1547
+ case "cubic_bezier":
1548
+ return "cubic-bezier";
1549
+ case "font_family":
1550
+ return "font-family";
1551
+ case "string":
1552
+ case "boolean":
1553
+ return null;
1554
+ default: {
1555
+ const exhaustive = value;
1556
+ return exhaustive;
1557
+ }
1506
1558
  }
1507
- return dtcgLiteral(literal, token.scopes, p.options.values);
1508
1559
  }
1509
1560
  function aliasLeafType(p, token, chain, resolved2) {
1510
1561
  const terminal = chain.length > 0 ? p.tokenById.get(chain[chain.length - 1].token_id) : void 0;
1511
- return projectedLiteral(p, terminal ?? token, resolved2);
1562
+ return projectedLiteral(p, terminal ?? token, resolved2).converted;
1512
1563
  }
1513
1564
  function modeLabels(collection) {
1514
1565
  const counts = /* @__PURE__ */ new Map();
@@ -1547,6 +1598,17 @@ function reportCollectionNameCollisions(p) {
1547
1598
  function asJson(value) {
1548
1599
  return JSON.parse(JSON.stringify(value));
1549
1600
  }
1601
+ function transformField(p, token) {
1602
+ const facts = p.factsById.get(token.id);
1603
+ if (!facts) return {};
1604
+ const sorted = (source) => {
1605
+ const keys = Object.keys(source).sort(compareCodeUnits);
1606
+ return keys.length === 0 ? void 0 : Object.fromEntries(keys.map((k) => [k, source[k]]));
1607
+ };
1608
+ const transform = sorted(facts.transform);
1609
+ const resolved2 = sorted(facts.resolved);
1610
+ return { ...transform ? { transform } : {}, ...resolved2 ? { resolved: resolved2 } : {} };
1611
+ }
1550
1612
  function metaEntry(p, token, collection) {
1551
1613
  const labels = p.modeLabelsById.get(collection.id) ?? modeLabels(collection);
1552
1614
  const omitted = p.omittedIds.has(token.id);
@@ -1559,6 +1621,7 @@ function metaEntry(p, token, collection) {
1559
1621
  collection_id: token.collection_id,
1560
1622
  type: token.type,
1561
1623
  scopes: [...token.scopes],
1624
+ ...omitted ? {} : transformField(p, token),
1562
1625
  ...token.code_syntax ? { code_syntax: token.code_syntax } : {},
1563
1626
  ...token.publication ? { publication: token.publication } : {},
1564
1627
  ...omitted ? {
@@ -1857,6 +1920,64 @@ function annotateGroups(p, tree, collection) {
1857
1920
  }
1858
1921
  }
1859
1922
  }
1923
+ var newAccumulator = () => ({
1924
+ tokens: 0,
1925
+ types: /* @__PURE__ */ new Map(),
1926
+ present: 0,
1927
+ missing: 0,
1928
+ aliases: 0,
1929
+ literals: 0,
1930
+ scopes: /* @__PURE__ */ new Map(),
1931
+ codeSyntaxPresent: 0,
1932
+ codeSyntaxMissing: 0,
1933
+ published: 0,
1934
+ hiddenFromPublishing: 0,
1935
+ unstated: 0,
1936
+ omitted: 0,
1937
+ collided: 0
1938
+ });
1939
+ var bump = (counts, key) => {
1940
+ counts.set(key, (counts.get(key) ?? 0) + 1);
1941
+ };
1942
+ var histogram = (counts) => Object.fromEntries([...counts.keys()].sort(compareCodeUnits).map((k) => [k, counts.get(k)]));
1943
+ function censusEntry(a) {
1944
+ return {
1945
+ tokens: a.tokens,
1946
+ types: histogram(a.types),
1947
+ descriptions: { present: a.present, missing: a.missing },
1948
+ aliases: a.aliases,
1949
+ literals: a.literals,
1950
+ scopes: histogram(a.scopes),
1951
+ code_syntax: { present: a.codeSyntaxPresent, missing: a.codeSyntaxMissing },
1952
+ publication: {
1953
+ published: a.published,
1954
+ hidden_from_publishing: a.hiddenFromPublishing,
1955
+ unstated: a.unstated
1956
+ },
1957
+ ...a.omitted > 0 ? { omitted: a.omitted } : {},
1958
+ ...a.collided > 0 ? { collided: a.collided } : {}
1959
+ };
1960
+ }
1961
+ function styleCensus(tree) {
1962
+ const a = newAccumulator();
1963
+ const walk = (node) => {
1964
+ if (typeof node !== "object" || node === null || Array.isArray(node)) return;
1965
+ const record = node;
1966
+ if ("$value" in record) {
1967
+ a.tokens += 1;
1968
+ bump(a.types, typeof record.$type === "string" ? record.$type : "unknown");
1969
+ if (typeof record.$description === "string" && record.$description.length > 0) a.present += 1;
1970
+ else a.missing += 1;
1971
+ return;
1972
+ }
1973
+ for (const [key, value] of Object.entries(record)) {
1974
+ if (key.startsWith("$")) continue;
1975
+ walk(value);
1976
+ }
1977
+ };
1978
+ walk(tree);
1979
+ return { tokens: a.tokens, types: histogram(a.types), descriptions: { present: a.present, missing: a.missing } };
1980
+ }
1860
1981
  function foundationDtcg(artifact, options = {}) {
1861
1982
  const p = {
1862
1983
  artifact,
@@ -1871,7 +1992,8 @@ function foundationDtcg(artifact, options = {}) {
1871
1992
  omittedIds: /* @__PURE__ */ new Set(),
1872
1993
  collidedIds: /* @__PURE__ */ new Set(),
1873
1994
  report: [],
1874
- reportKeys: /* @__PURE__ */ new Set()
1995
+ reportKeys: /* @__PURE__ */ new Set(),
1996
+ factsById: /* @__PURE__ */ new Map()
1875
1997
  };
1876
1998
  indexPaths(p);
1877
1999
  omitInexpressibleTypes(p);
@@ -1879,23 +2001,46 @@ function foundationDtcg(artifact, options = {}) {
1879
2001
  reportCollectionNameCollisions(p);
1880
2002
  const files = {};
1881
2003
  const plans = [];
2004
+ const census = {};
1882
2005
  const taken = new Set(RESERVED_FILE_NAMES);
1883
2006
  for (const collection of artifact.collections) {
1884
2007
  for (const mode of collection.modes) {
1885
2008
  const tree = {};
2009
+ const a = newAccumulator();
1886
2010
  for (const token of artifact.tokens) {
1887
- if (token.collection_id !== collection.id || p.omittedIds.has(token.id)) continue;
2011
+ if (token.collection_id !== collection.id) continue;
2012
+ if (p.omittedIds.has(token.id)) {
2013
+ a.omitted += 1;
2014
+ if (p.collidedIds.has(token.id)) a.collided += 1;
2015
+ continue;
2016
+ }
1888
2017
  const leaf = tokenLeaf(p, token, collection, mode.id);
1889
- if (leaf) setLeaf(tree, p.segmentsById.get(token.id) ?? [], leaf);
2018
+ if (!leaf) continue;
2019
+ setLeaf(tree, p.segmentsById.get(token.id) ?? [], leaf);
2020
+ a.tokens += 1;
2021
+ bump(a.types, typeof leaf.$type === "string" ? leaf.$type : "unknown");
2022
+ const modeLabel = modeLabelOf(p, collection, mode.id);
2023
+ if (p.factsById.get(token.id)?.transform[modeLabel] === "alias") a.aliases += 1;
2024
+ else a.literals += 1;
2025
+ if (token.description.length > 0) a.present += 1;
2026
+ else a.missing += 1;
2027
+ for (const scope of token.scopes) bump(a.scopes, scope);
2028
+ if (token.code_syntax) a.codeSyntaxPresent += 1;
2029
+ else a.codeSyntaxMissing += 1;
2030
+ if (token.publication?.published) a.published += 1;
2031
+ if (token.publication?.hidden_from_publishing) a.hiddenFromPublishing += 1;
2032
+ if (!token.publication) a.unstated += 1;
1890
2033
  }
1891
2034
  annotateGroups(p, tree, collection);
1892
2035
  const file = fileNameFor(collection, mode, taken);
1893
2036
  plans.push({ collection, modeId: mode.id, file });
1894
2037
  files[file] = sortTree(tree);
2038
+ census[file] = censusEntry(a);
1895
2039
  }
1896
2040
  }
1897
2041
  const styles = styleFiles(p);
1898
2042
  Object.assign(files, styles);
2043
+ for (const [file, tree] of Object.entries(styles)) census[file] = styleCensus(tree);
1899
2044
  const resolver = buildResolver(p, plans, Object.keys(styles).sort(compareCodeUnits));
1900
2045
  p.report.sort((a, b) => compareCodeUnits(a.path, b.path) || compareCodeUnits(a.code, b.code) || compareCodeUnits(a.mode ?? "", b.mode ?? ""));
1901
2046
  const meta = {};
@@ -1906,7 +2051,25 @@ function foundationDtcg(artifact, options = {}) {
1906
2051
  meta[p.collidedIds.has(token.id) ? `${path} [${token.id}]` : path] = metaEntry(p, token, collection);
1907
2052
  }
1908
2053
  const sortedMeta = Object.fromEntries(Object.entries(meta).sort(([a], [b]) => compareCodeUnits(a, b)));
1909
- return { files, resolver, meta: sortedMeta, report: p.report };
2054
+ const codeSyntax = {};
2055
+ for (const [path, entry2] of Object.entries(sortedMeta)) {
2056
+ if (entry2.code_syntax) codeSyntax[path] = entry2.code_syntax;
2057
+ }
2058
+ const sourceFileName = artifact.spec_layer.source.file_name;
2059
+ const extension = {
2060
+ schema_version: SCHEMA_VERSION,
2061
+ content_hash: artifact.spec_layer.export.content_hash,
2062
+ config_hash: `sha256:${(0, import_js_sha2563.sha256)(canonicalJson(p.options))}`,
2063
+ source: {
2064
+ provider: "figma",
2065
+ ...typeof sourceFileName === "string" && sourceFileName.length > 0 ? { file_name: sourceFileName } : {}
2066
+ },
2067
+ completeness: artifact.completeness,
2068
+ code_syntax: codeSyntax,
2069
+ census: Object.fromEntries(Object.keys(census).sort(compareCodeUnits).map((k) => [k, census[k]])),
2070
+ report: p.report
2071
+ };
2072
+ return { files, resolver, meta: sortedMeta, report: p.report, extension };
1910
2073
  }
1911
2074
  function omitInexpressibleTypes(p) {
1912
2075
  for (const token of p.artifact.tokens) {
@@ -2007,9 +2170,10 @@ function tokenLeaf(p, token, collection, modeId) {
2007
2170
  });
2008
2171
  return null;
2009
2172
  }
2173
+ recordFact(p, token.id, mode, "alias", typed.$value);
2010
2174
  return { $type: typed.$type, $value: `{${targetPath}}`, ...description };
2011
2175
  }
2012
- const converted2 = projectedLiteral(p, token, value.value, (override) => {
2176
+ const projected = projectedLiteral(p, token, value.value, (override) => {
2013
2177
  reportOnce(p, {
2014
2178
  code: "unit_override_conflicts_with_scope",
2015
2179
  severity: "warning",
@@ -2018,6 +2182,7 @@ function tokenLeaf(p, token, collection, modeId) {
2018
2182
  details: { id: token.id, override, scopes: [...token.scopes] }
2019
2183
  });
2020
2184
  });
2185
+ const converted2 = projected.converted;
2021
2186
  if ("omit" in converted2) {
2022
2187
  reportOnce(p, {
2023
2188
  code: converted2.omit,
@@ -2029,6 +2194,7 @@ function tokenLeaf(p, token, collection, modeId) {
2029
2194
  });
2030
2195
  return null;
2031
2196
  }
2197
+ if (projected.transform !== null) recordFact(p, token.id, mode, projected.transform);
2032
2198
  return { $type: converted2.$type, $value: converted2.$value, ...description };
2033
2199
  }
2034
2200
  function dtcgExportFiles(out) {
@@ -2036,7 +2202,10 @@ function dtcgExportFiles(out) {
2036
2202
  `;
2037
2203
  const files = {};
2038
2204
  for (const name of Object.keys(out.files).sort(compareCodeUnits)) files[name] = text(out.files[name]);
2039
- files["resolver.json"] = text(out.resolver);
2205
+ files["resolver.json"] = text({
2206
+ ...out.resolver,
2207
+ $extensions: { "com.spec-layer": out.extension }
2208
+ });
2040
2209
  files["spec-layer.meta.json"] = text(out.meta);
2041
2210
  files["report.json"] = text(out.report);
2042
2211
  return files;
@@ -2546,7 +2715,7 @@ ${imports.join("\n")}
2546
2715
  }
2547
2716
 
2548
2717
  // ../extractor/src/v5/componentContext.ts
2549
- var import_js_sha2563 = __toESM(require_sha256(), 1);
2718
+ var import_js_sha2564 = __toESM(require_sha256(), 1);
2550
2719
 
2551
2720
  // ../extractor/src/libraryBundle.ts
2552
2721
  var LIBRARY_BUNDLE_SCHEMA = "spec-layer-library-bundle";
@@ -2614,7 +2783,7 @@ function parseLibraryBundle(input) {
2614
2783
  }
2615
2784
 
2616
2785
  // ../extractor/src/libraryBundleHash.ts
2617
- var import_js_sha2564 = __toESM(require_sha256(), 1);
2786
+ var import_js_sha2565 = __toESM(require_sha256(), 1);
2618
2787
 
2619
2788
  // src/bundle.ts
2620
2789
  function parseBundle(raw) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "spec-layer",
3
- "version": "0.7.0",
3
+ "version": "0.8.2",
4
4
  "description": "Pull design-system context published by the Spec Layer Figma plugin",
5
5
  "license": "MIT",
6
6
  "type": "module",