@smallpen/cli 0.1.0-alpha.11 → 0.1.0-alpha.13

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.
@@ -17,7 +17,7 @@ export const OUTPUT_LIMIT_BYTES = 8192;
17
17
  export const RESULT_RETENTION_MS = 30 * 60 * 1000;
18
18
  const uuid = "[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}";
19
19
  const role =
20
- "(?:result|render(?:-matrix-\\d{2})?|outline|wireframe|text|undo|changes|evidence|matrix|validation|flow-connections|flow-verification)";
20
+ "(?:result|render(?:-matrix-\\d{2})?|outline|spec|wireframe|text|undo|changes|evidence|matrix|validation|flow-connections|flow-verification)";
21
21
  const managedName = new RegExp(
22
22
  `^(smallpen-\\d{13}-${uuid})-(${role}|owner-(\\d+))\\.(?:png|json|txt)(?:\\.smallpen-(\\d+)-${uuid}\\.tmp)?$`,
23
23
  );
package/bin/smallpen.mjs CHANGED
@@ -100,7 +100,6 @@ import {
100
100
  } from "@smallpen/core";
101
101
  import {
102
102
  applyOperationBatch,
103
- createEvidence,
104
103
  defaultLibraryCacheRoot,
105
104
  fontVariantName,
106
105
  importDraft,
@@ -115,7 +114,6 @@ import {
115
114
  readBatchHistory,
116
115
  openRemoteLibrary,
117
116
  measureProjectionText,
118
- renderProjection,
119
117
  replayRecordedBatch,
120
118
  resolveWorkspace,
121
119
  sfntToWoff,
@@ -155,12 +153,13 @@ import {
155
153
  componentSummaryText,
156
154
  platformHints,
157
155
  localWireframe,
158
- specTree,
156
+ resolveDesignLayout,
159
157
  outlineTree,
160
158
  selectedDesign,
161
159
  themeSettings,
162
160
  validateDesign,
163
161
  } from "./design-output.mjs";
162
+ import { inspectionCoverage, inspectionReport, inspectionText } from "./inspection-output.mjs";
164
163
  import { definitionReuseAdvice } from "./reuse-advice.mjs";
165
164
  import { warningOutput } from "./warning-output.mjs";
166
165
  import {
@@ -471,6 +470,9 @@ function printJson(value) {
471
470
  "mimeType",
472
471
  "output",
473
472
  "imagePath",
473
+ "specPath",
474
+ "wireframePath",
475
+ "layout",
474
476
  "evidencePath",
475
477
  "matrixPath",
476
478
  "status",
@@ -626,6 +628,9 @@ function printJson(value) {
626
628
  "answersTemplate",
627
629
  "output",
628
630
  "imagePath",
631
+ "specPath",
632
+ "wireframePath",
633
+ "layout",
629
634
  "evidencePath",
630
635
  ]) {
631
636
  if (Buffer.byteLength(json) + 1 <= limitBytes) break;
@@ -1641,7 +1646,7 @@ function nameDiagnostics(diagnostics = [], projection) {
1641
1646
  const rest = [];
1642
1647
  for (const diagnostic of diagnostics) {
1643
1648
  if (diagnostic.code === "layout_projection_partial") continue;
1644
- if (diagnostic.code === "font_render_fallback" && !diagnostic.availableFonts) {
1649
+ if (diagnostic.code === "font_render_fallback" && !Array.isArray(diagnostic.nodeIds)) {
1645
1650
  const family = diagnostic.details?.requestedFont ?? diagnostic.message;
1646
1651
  const entry = fallbacks.get(family);
1647
1652
  if (entry) {
@@ -1675,120 +1680,6 @@ function nameDiagnostics(diagnostics = [], projection) {
1675
1680
  });
1676
1681
  }
1677
1682
 
1678
- // The renderer reports one font_render_fallback per text node. Collapse them
1679
- // to one diagnostic per missing family that names the fonts it can draw and
1680
- // the import-font command that adds the missing one.
1681
- async function renderEvidence(product, options) {
1682
- const bundle = await createEvidence(product, options);
1683
- const fallbacks = new Map();
1684
- const diagnostics = [];
1685
- for (const diagnostic of bundle.render.diagnostics) {
1686
- if (diagnostic.code !== "font_render_fallback") {
1687
- diagnostics.push(diagnostic);
1688
- continue;
1689
- }
1690
- const requestedFont =
1691
- diagnostic.details?.requestedFont ??
1692
- /substituted .+ for (.+)$/.exec(diagnostic.message)?.[1] ??
1693
- "<family>";
1694
- let collapsed = fallbacks.get(requestedFont);
1695
- if (!collapsed) {
1696
- collapsed = { diagnostic, nodeIds: [], paths: [], requestedFont };
1697
- fallbacks.set(requestedFont, collapsed);
1698
- diagnostics.push(collapsed);
1699
- }
1700
- if (diagnostic.nodeId !== undefined)
1701
- collapsed.nodeIds.push(diagnostic.nodeId);
1702
- if (diagnostic.path !== undefined) collapsed.paths.push(diagnostic.path);
1703
- }
1704
- if (fallbacks.size === 0) return bundle;
1705
- const imported = [
1706
- ...new Set(
1707
- [options.foundation, ...(options.libraries ?? []), product]
1708
- .filter(Boolean)
1709
- .flatMap((snapshot) =>
1710
- snapshot.manifest.entries.assets.flatMap((entry) =>
1711
- (snapshot.entries[entry].fonts ?? []).map(({ family }) => family),
1712
- ),
1713
- ),
1714
- ),
1715
- ].sort(compareText);
1716
- const availableFonts = [
1717
- "Source Sans Pro (bundled)",
1718
- ...imported.map((family) => `${family} (imported)`),
1719
- ];
1720
- const collapsedEntries = new Set(fallbacks.values());
1721
- const explained = diagnostics.map((entry) => {
1722
- if (!collapsedEntries.has(entry)) return entry;
1723
- const { requestedFont } = entry;
1724
- return {
1725
- availableFonts,
1726
- code: entry.diagnostic.code,
1727
- nodeIds: entry.nodeIds,
1728
- details: {
1729
- ...entry.diagnostic.details,
1730
- requestedFont,
1731
- substituteFont:
1732
- entry.diagnostic.details?.substituteFont ?? "Source Sans Pro",
1733
- },
1734
- message:
1735
- `${entry.diagnostic.message} on ${entry.nodeIds.length} text node(s): ` +
1736
- `${requestedFont} is neither bundled nor imported. Available fonts: ` +
1737
- `${availableFonts.join(", ")}. Import ${requestedFont} with smallpen asset font import ` +
1738
- "(once per weight/style; see nextOperations), or use an available family.",
1739
- nextOperations: [
1740
- {
1741
- argv: [
1742
- "import-font",
1743
- product.locator,
1744
- "--file",
1745
- `<${requestedFont} .ttf|.otf|.woff file>`,
1746
- "--family",
1747
- requestedFont,
1748
- "--weight",
1749
- "<400|700|...>",
1750
- "--json",
1751
- ],
1752
- operation: "smallpen.import-font",
1753
- },
1754
- ],
1755
- nodeIds: entry.nodeIds,
1756
- paths: entry.paths,
1757
- requestedFont,
1758
- };
1759
- });
1760
- bundle.render.diagnostics = explained;
1761
- bundle.evidence.diagnostics = explained;
1762
- return bundle;
1763
- }
1764
-
1765
- async function deliverImage(
1766
- bundle,
1767
- { product, foundation, libraries },
1768
- output,
1769
- includeBase64 = false,
1770
- ) {
1771
- return {
1772
- diagnostics: bundle.render.diagnostics,
1773
- height: bundle.render.height,
1774
- mimeType: bundle.render.mimeType,
1775
- packageId: product.manifest.packageId,
1776
- renderHash: bundle.render.renderHash,
1777
- ...workspaceRevisions(product, foundation, libraries),
1778
- selection: bundle.evidence.selector,
1779
- width: bundle.render.width,
1780
- ...(output === undefined
1781
- ? {}
1782
- : { output: await writeAtomic(output, bundle.render.bytes) }),
1783
- ...(includeBase64
1784
- ? { base64: Buffer.from(bundle.render.bytes).toString("base64") }
1785
- : {}),
1786
- };
1787
- }
1788
-
1789
- // The generated Design System page, the same node tree the editor draws:
1790
- // every Token Cell and representative samples of each Component Set.
1791
- // part narrows the page: {only, tokenPrefix, componentSetIds, allVariants}.
1792
1683
  async function designSystemProjection(workspace, args, part) {
1793
1684
  const { foundation, libraries, product } = workspace;
1794
1685
  const runtime = await createWorkspaceRuntime(product, { foundation });
@@ -1830,25 +1721,6 @@ async function designSystemProjection(workspace, args, part) {
1830
1721
  return tree;
1831
1722
  }
1832
1723
 
1833
- async function renderDesignSystem(workspace, args) {
1834
- const { product, foundation, libraries } = workspace;
1835
- const tree = await designSystemProjection(workspace, args);
1836
- const render = await renderProjection(product, tree, {
1837
- foundation,
1838
- libraries,
1839
- scale: numberOption(args, "--scale"),
1840
- });
1841
- return {
1842
- evidence: {
1843
- selector: {
1844
- designSystem: true,
1845
- locale: option(args, "--locale") ?? "en",
1846
- },
1847
- },
1848
- render,
1849
- };
1850
- }
1851
-
1852
1724
  async function designForCommand(workspace, args) {
1853
1725
  if (flag(args, "--design-system")) {
1854
1726
  const projection = await designSystemProjection(workspace, args);
@@ -1872,9 +1744,22 @@ async function designForCommand(workspace, args) {
1872
1744
 
1873
1745
  async function designCommand(packagePath, args, override) {
1874
1746
  const workspace = await tokenWorkspace(packagePath, args);
1875
- const design = override ?? (await designForCommand(workspace, args));
1747
+ let design = override ?? (await designForCommand(workspace, args));
1876
1748
  const format =
1877
1749
  option(args, "--format") ?? (args[0] === "view" ? "outline" : "wireframe");
1750
+ if (["spec", "wireframe", "png"].includes(format))
1751
+ design = await resolveDesignLayout(workspace, design, {
1752
+ raster: format === "png", scale: numberOption(args, "--scale"),
1753
+ forExport: flag(args, "--evidence"),
1754
+ });
1755
+ let report;
1756
+ if (design.layoutSnapshot) {
1757
+ design.validation = await validateDesign(workspace, design);
1758
+ report = inspectionReport(workspace, design, {
1759
+ components: componentLookup(workspace), stored: storedNodesOf(workspace, design.selection),
1760
+ full: flag(args, "--full"),
1761
+ });
1762
+ }
1878
1763
  const receipt = {
1879
1764
  packageId: workspace.product.manifest.packageId,
1880
1765
  ...workspaceRevisions(
@@ -1891,7 +1776,58 @@ async function designCommand(packagePath, args, override) {
1891
1776
  : design.tokenSources?.map(({ path, value }) => ({ path, value })),
1892
1777
  }),
1893
1778
  diagnostics: nameDiagnostics(design.projection.diagnostics ?? [], design.projection),
1779
+ ...(report ? {
1780
+ layout: { ...report.snapshot, diagnostics: undefined, diagnosticCount: report.snapshot.diagnostics.length },
1781
+ coverage: flag(args, "--full") ? report.coverage : inspectionCoverage(report.coverage),
1782
+ issueCount: report.issues.length,
1783
+ ...(flag(args, "--full") ? { inspection: report } : {}),
1784
+ } : {}),
1894
1785
  };
1786
+ if (flag(args, "--evidence")) {
1787
+ const prefix = option(args, "--output");
1788
+ if (flag(args, "--base64") && !prefix) return {
1789
+ ...receipt, renderHash: design.render.renderHash, semanticTree: design.tree,
1790
+ specification: inspectionText(report), wireframe: localWireframe(design),
1791
+ image: { imageBase64: Buffer.from(design.render.bytes).toString("base64"),
1792
+ ...workspaceRevisions(workspace.product, workspace.foundation, workspace.libraries),
1793
+ width: design.render.width, height: design.render.height,
1794
+ mimeType: "image/png", renderHash: design.render.renderHash },
1795
+ };
1796
+ const path = (kind, extension) => prefix ? `${prefix}${kind ? `.${kind}` : ""}.${extension}` : artifactPath(kind || "render", extension);
1797
+ const imagePath = await writeAtomic(path("", "png"), design.render.bytes);
1798
+ const specPath = await writeAtomic(path("spec", "txt"), inspectionText(report));
1799
+ const wireframePath = await writeAtomic(path("wireframe", "txt"), localWireframe(design));
1800
+ const regions = [];
1801
+ const viewport = design.layoutSnapshot.viewport;
1802
+ const scale = numberOption(args, "--scale") ?? 1;
1803
+ const regionsOf = node => {
1804
+ if (node.bounds) {
1805
+ const left = Math.max(0, Math.min(design.render.width, (node.bounds.x - viewport.x) * scale));
1806
+ const top = Math.max(0, Math.min(design.render.height, (node.bounds.y - viewport.y) * scale));
1807
+ const right = Math.max(0, Math.min(design.render.width, (node.bounds.x + node.bounds.width - viewport.x) * scale));
1808
+ const bottom = Math.max(0, Math.min(design.render.height, (node.bounds.y + node.bounds.height - viewport.y) * scale));
1809
+ regions.push({ nodeId: node.id, x: left, y: top, width: Math.max(0, right - left), height: Math.max(0, bottom - top) });
1810
+ }
1811
+ node.children.forEach(regionsOf);
1812
+ };
1813
+ regionsOf(design.tree.root);
1814
+ const evidencePath = await writeAtomic(prefix ? `${prefix}.json` : artifactPath("evidence", "json"), canonicalJSON({
1815
+ ...report, semanticTree: { ...design.tree, selection: design.selection }, renderHash: design.render.renderHash,
1816
+ packageId: workspace.product.manifest.packageId,
1817
+ packageHash: workspace.foundation
1818
+ ? createHash("sha256").update(`${workspace.product.revision}\0${workspace.foundation.revision}`).digest("hex")
1819
+ : workspace.product.revision,
1820
+ ...workspaceRevisions(workspace.product, workspace.foundation, workspace.libraries),
1821
+ renderer: design.layoutSnapshot.renderer, kernelHash: design.layoutSnapshot.kernelHash,
1822
+ selector: design.selection,
1823
+ snapshotId: design.layoutSnapshot.id, nodeRegions: regions,
1824
+ viewport: { width: design.render.width, height: design.render.height, scale },
1825
+ }));
1826
+ return { ...receipt, imagePath, specPath, wireframePath, evidencePath,
1827
+ renderHash: design.render.renderHash, width: design.render.width, height: design.render.height,
1828
+ ...(flag(args, "--base64") ? { imageBase64: Buffer.from(design.render.bytes).toString("base64") } : {}),
1829
+ };
1830
+ }
1895
1831
  const hints = platformHints(workspace, design);
1896
1832
  if (hints.length) receipt.hints = hints;
1897
1833
  const textLayout = {
@@ -1951,12 +1887,7 @@ async function designCommand(packagePath, args, override) {
1951
1887
  format === "wireframe"
1952
1888
  ? localWireframe(design, { full: flag(args, "--full") })
1953
1889
  : format === "spec"
1954
- ? specTree(design.tree, design.projection, {
1955
- components,
1956
- tokenName: tokenNamer(workspace),
1957
- stored: storedNodesOf(workspace, design.selection),
1958
- heading: `Spec · px, x/y from the top-left of ${design.tree.root.name} · themes ${themeSettings(workspace).selection.themes.join(", ") || "defaults"} · {path} = Token`,
1959
- })
1890
+ ? inspectionText(report, { details: flag(args, "--full") || Boolean(design.selection.nodeId) })
1960
1891
  : outline;
1961
1892
  const lines = text.split("\n");
1962
1893
  // A page outline reads whole; long ones page at 80 lines.
@@ -1996,11 +1927,7 @@ async function designCommand(packagePath, args, override) {
1996
1927
  };
1997
1928
  }
1998
1929
  if (format === "png") {
1999
- const render = await renderProjection(
2000
- workspace.product,
2001
- design.renderProjection ?? design.projection,
2002
- { ...workspace, crop: design.crop, scale: numberOption(args, "--scale") },
2003
- );
1930
+ const render = design.render;
2004
1931
  const output = await writeAtomic(
2005
1932
  option(args, "--output") ?? artifactPath("render", "png"),
2006
1933
  render.bytes,
@@ -2021,7 +1948,7 @@ async function designCommand(packagePath, args, override) {
2021
1948
  ? { imageBase64: Buffer.from(render.bytes).toString("base64") }
2022
1949
  : {}),
2023
1950
  diagnostics: nameDiagnostics(
2024
- render.diagnostics.filter(
1951
+ design.layoutSnapshot.diagnostics.filter(
2025
1952
  (diagnostic) =>
2026
1953
  diagnostic.code !== "layout_projection_partial" ||
2027
1954
  relevant.has(diagnostic.nodeId),
@@ -2030,7 +1957,8 @@ async function designCommand(packagePath, args, override) {
2030
1957
  ),
2031
1958
  };
2032
1959
  }
2033
- const text = `${JSON.stringify(receipt)}\n\n${format === "wireframe" ? localWireframe(design) : outline}\n`;
1960
+ const text = format === "spec" ? `${inspectionText(report, { details: flag(args, "--full") || Boolean(design.selection.nodeId) })}\n`
1961
+ : `${JSON.stringify(receipt)}\n\n${format === "wireframe" ? localWireframe(design) : outline}\n`;
2034
1962
  const output = await writeAtomic(
2035
1963
  option(args, "--output") ?? artifactPath(format, "txt"),
2036
1964
  text,
@@ -2105,88 +2033,6 @@ function designArgs(args) {
2105
2033
  return [...result, "--json"];
2106
2034
  }
2107
2035
 
2108
- async function renderCommand(packagePath, args, evidenceOnly) {
2109
- const workspace = await tokenWorkspace(packagePath, args);
2110
- const { foundation, libraries, product } = workspace;
2111
- if (!evidenceOnly && flag(args, "--design-system")) {
2112
- const bundle = await renderDesignSystem(workspace, args);
2113
- const inline = flag(args, "--base64");
2114
- const output =
2115
- option(args, "--output") ??
2116
- (inline ? undefined : artifactPath("render", "png"));
2117
- return deliverImage(
2118
- bundle,
2119
- workspace,
2120
- output,
2121
- (inline && output === undefined) || flag(args, "--base64"),
2122
- );
2123
- }
2124
- const bundle = await renderEvidence(product, {
2125
- foundation,
2126
- libraries,
2127
- scale: numberOption(args, "--scale"),
2128
- selector: designSelector(args, "screenshot"),
2129
- forExport: true,
2130
- });
2131
- const full = flag(args, "--full");
2132
- const inline = flag(args, "--base64");
2133
- const output =
2134
- option(args, "--output") ??
2135
- (inline ? undefined : artifactPath("render", "png"));
2136
- const evidenceJson = evidenceOnly
2137
- ? canonicalJSON({
2138
- ...bundle.evidence,
2139
- ...workspaceRevisions(product, foundation, libraries),
2140
- })
2141
- : undefined;
2142
- if (evidenceOnly && !inline) {
2143
- const prefix = option(args, "--output");
2144
- const imagePath = await writeAtomic(
2145
- prefix === undefined ? output : `${prefix}.png`,
2146
- bundle.render.bytes,
2147
- );
2148
- const evidencePath = await writeAtomic(
2149
- prefix === undefined
2150
- ? artifactPath("evidence", "json")
2151
- : `${prefix}.json`,
2152
- evidenceJson,
2153
- );
2154
- return {
2155
- ...(full ? bundle.evidence : {}),
2156
- diagnostics: nameDiagnostics(bundle.render.diagnostics, bundle.projection),
2157
- renderHash: bundle.render.renderHash,
2158
- selection: bundle.evidence.selector,
2159
- packageId: product.manifest.packageId,
2160
- ...workspaceRevisions(product, foundation, libraries),
2161
- evidencePath,
2162
- imagePath,
2163
- };
2164
- }
2165
- if (evidenceOnly && output !== undefined) {
2166
- const imagePath = await writeAtomic(`${output}.png`, bundle.render.bytes);
2167
- const evidencePath = await writeAtomic(`${output}.json`, evidenceJson);
2168
- return {
2169
- ...bundle.evidence,
2170
- ...workspaceRevisions(product, foundation, libraries),
2171
- imagePath,
2172
- evidencePath,
2173
- };
2174
- }
2175
- const image = await deliverImage(
2176
- bundle,
2177
- workspace,
2178
- output,
2179
- (output === undefined && inline) || flag(args, "--base64"),
2180
- );
2181
- return evidenceOnly
2182
- ? {
2183
- ...bundle.evidence,
2184
- ...workspaceRevisions(product, foundation, libraries),
2185
- image,
2186
- }
2187
- : image;
2188
- }
2189
-
2190
2036
  function tokenImpact(snapshot, tokenId, packageId) {
2191
2037
  const locations = [];
2192
2038
  const visit = (value, location) => {
@@ -3104,6 +2950,8 @@ async function lookCommand(packagePath, args) {
3104
2950
  "too_many_view_targets",
3105
2951
  "Look at one thing per call: --page, --component, --token, --tokens, --components or --pages",
3106
2952
  );
2953
+ if (as === "spec" && page === undefined && component === undefined)
2954
+ throw new SmallPenError("missing_spec_target", "Choose one --page or --component for its specification; use --as text to navigate first");
3107
2955
  const needs = (name, parents, text) => {
3108
2956
  if (option(args, name) !== undefined && !parents.some((parent) => args.includes(parent)))
3109
2957
  throw new SmallPenError("incomplete_view_target", `${name} ${text}`);
@@ -3881,14 +3729,6 @@ function storedNodesOf(workspace, selection = {}) {
3881
3729
  return {};
3882
3730
  }
3883
3731
 
3884
- // Token id -> name across the workspace, for specs.
3885
- function tokenNamer(workspace) {
3886
- const names = new Map();
3887
- for (const snapshot of [workspace.product, workspace.foundation, ...(workspace.libraries ?? [])].filter(Boolean))
3888
- for (const token of snapshot.domain.tokens.values()) if (!names.has(token.id)) names.set(token.id, token.path);
3889
- return (reference) => names.get(reference?.assetId ?? reference);
3890
- }
3891
-
3892
3732
  // --token: one Token by name, or every Token under a name ("color").
3893
3733
  function tokenNamed(path, wanted) {
3894
3734
  if (wanted === undefined) return true;
@@ -4364,16 +4204,7 @@ async function main(args) {
4364
4204
  if (command === "export" && flag(args, "--evidence")) {
4365
4205
  if (option(args, "--format") !== "png")
4366
4206
  throw new SmallPenError("missing_png_format", "--evidence requires --format png");
4367
- const translated = ["export", packagePath];
4368
- for (let i = 2; i < args.length; i++) {
4369
- if (args[i] === "--evidence") continue;
4370
- if (args[i] === "--format") {
4371
- i++;
4372
- continue;
4373
- }
4374
- translated.push(args[i]);
4375
- }
4376
- printJson(await renderCommand(packagePath, translated, true));
4207
+ printJson(await designCommand(packagePath, args));
4377
4208
  return;
4378
4209
  }
4379
4210
 
package/bin/workflow.mjs CHANGED
@@ -19,11 +19,11 @@ export const CLI_RULES = Object.freeze({
19
19
  "token theme list returns groups/options/defaults; token list --definitions [--group GROUP] reads stored option values; token list/show/explain resolve values and sources.",
20
20
  flows:
21
21
  "flow list shows starts and links by page and element name; flow link adds a link, flow unlink removes an element's links, flow start --page makes a start and --remove undoes it.",
22
- view: "view looks at anything by name: nothing (whole package), --tokens/--token, --components/--component [--variant --element], --pages/--page [--platform --element]; --as text|wireframe|png|issues. Outlines collapse component internals and group repeated instances. changes --since REVISION lists, by name, what changed after a revision, including edits made in the App.",
22
+ view: "view looks at anything by name: nothing (whole package), --tokens/--token, --components/--component [--variant --element], --pages/--page [--platform --element]; --as text|spec|wireframe|png|issues. Text navigates; spec implements and traces sources; wireframe shows structure; PNG reviews appearance. Outlines collapse component internals and group repeated instances. changes --since REVISION lists, by name, what changed after a revision, including edits made in the App.",
23
23
  validate:
24
24
  "validate checks canonical validity and reports scoped text/layout/contrast issues separately. status valid means valid data, not complete visual approval. Always read issueCount, visualStatus and coverage.skipped. Pagination does not reduce the checked scope.",
25
25
  export:
26
- "export saves text, page/component/region wireframe or PNG and returns its path/hash/bytes. Pass the same target and themes used for validation.",
26
+ "export saves a page/component/region specification, wireframe or PNG. --format png --evidence writes all three from one native solve. Compare layout.hash, target, themes and revision; read coverage and unresolved fields.",
27
27
  },
28
28
  modelling: {
29
29
  tokenLayers:
@@ -44,9 +44,9 @@ export const CLI_RULES = Object.freeze({
44
44
  output: {
45
45
  budgetBytes: 8192,
46
46
  strategy:
47
- "Discover IDs and read compact text/issues first; request one target or region before expanding data with pagination or --full. Use a text wireframe for unclear spatial relations. Decode JSON strings, keep LF newlines and spaces, display monospace without soft wrapping. Only request PNG inspection when text/wireframes are insufficient and the caller supports image input; verify the current PNG against renderHash; temporary paths are unique per call. Outline page.total/hasMore counts text lines; wireframe is complete with lineCount. Large complete results use resultFile.path (JSON) and resultFile.text.path for exact LF/space text. Use jq/rg/sed to read just what is needed. --full expands data, --stdout explicitly permits large stdout. All temporary files share one system temporary folder and the format smallpen-TIMESTAMP-UUID-KIND.ext. Each call cleans CLI files with mtime older than 30 minutes, skipping active calls. Consume within 30 minutes; concurrent invocations have separate results. --output retains a chosen deliverable. Omitted stdout detail never means a check passed.",
47
+ "Choose one page/component/variant/element. Read spec for implementation: authored fixed/fill/hug constraints, native bounds, field-level Token and component sources, overrides and node diagnostics. Read wireframe for spatial orientation; its character grid is approximate. Read PNG for high-fidelity appearance when the caller supports images. Text is a compact navigation outline, not an implementation specification. Specification, wireframe and PNG use native App layout; compare layout.hash, revision and selection, not revision alone. export --format png --evidence writes all three and JSON evidence from one solve. Unknown geometry stays ?/partial with a reason; seed rectangles are never actual coordinates. Coverage applies only to the selected size/themes/state, not untested combinations. --element expands details; --full expands the selected tree. Read resultFile.text.path for exact LF/space text, resultFile.path for complete JSON. Temporary paths are unique, expire after 30 minutes and must be consumed promptly; --output retains deliverables. Omitted stdout detail never means a check passed.",
48
48
  imagePolicy:
49
- "view defaults to a text outline; export defaults to a text wireframe, including with --full. PNG requires export --format png. The CLI cannot detect the caller's image capability and returns artifact paths by default. Models without image input must use text/wireframes for review; they may export PNG deliverables but must not open/embed them or claim visual inspection.",
49
+ "view defaults to a text outline for navigation; implementation starts with view --as spec. export defaults to a wireframe; --format spec writes a specification and --format png writes a high-fidelity image. A PNG cannot prove Token bindings, auto-sizing intent or interaction rules. A specification cannot prove visual quality. Models without image input must use spec/issues for structural checks; they may export PNG but must not claim visual inspection. Raster errors fail explicitly; no substitute painter fills in missing geometry.",
50
50
  protocol:
51
51
  "All stdout JSON is compact, including --full. Default directories expose only public identity, ownership, selection and count fields; read one target for its description or definition. Omit normal public visibility and false deprecation; actual private restrictions, deprecation and replacements remain actionable. Internal object fields never become public directory fields automatically. schema command project list or component list/search documents the output contract. token theme list omits unrelated stored App selection unless --full is explicitly requested. This omission policy must not discard meaningful design values such as 0, false, empty text or a node's hidden state.",
52
52
  writes:
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@smallpen/cli",
3
- "version": "0.1.0-alpha.11",
3
+ "version": "0.1.0-alpha.13",
4
4
  "publishConfig": {
5
5
  "access": "public"
6
6
  },
@@ -15,8 +15,8 @@
15
15
  "bin"
16
16
  ],
17
17
  "dependencies": {
18
- "@smallpen/core": "0.1.0-alpha.11",
19
- "@smallpen/local-package": "0.1.0-alpha.11"
18
+ "@smallpen/core": "0.1.0-alpha.13",
19
+ "@smallpen/local-package": "0.1.0-alpha.13"
20
20
  },
21
21
  "repository": {
22
22
  "type": "git",