@sableui/react 0.0.0-stage → 0.1.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 (247) hide show
  1. package/AGENTS.md +157 -0
  2. package/CHANGELOG.md +50 -0
  3. package/CONTRIBUTING.md +221 -0
  4. package/DESIGN.md +548 -0
  5. package/LICENSE +21 -0
  6. package/README.md +194 -2
  7. package/THIRD_PARTY_NOTICES.md +44 -0
  8. package/bin/sable-lint.mjs +0 -0
  9. package/bin/sable.mjs +1223 -0
  10. package/dist/_contract-CYScOdf5.d.ts +150 -0
  11. package/dist/chunk-3IWZYBGH.js +398 -0
  12. package/dist/chunk-5Z5KLOAU.js +167 -0
  13. package/dist/chunk-7MG4SHYX.js +15 -0
  14. package/dist/chunk-ADLLDWLW.js +15 -0
  15. package/dist/chunk-EP55KSYA.js +14 -0
  16. package/dist/chunk-J23ELQQZ.js +9396 -0
  17. package/dist/chunk-JI7WJP2L.js +286 -0
  18. package/dist/chunk-K7RUHUGC.js +14 -0
  19. package/dist/chunk-KO3NAEBF.js +15 -0
  20. package/dist/chunk-L4IEIQQA.js +14 -0
  21. package/dist/chunk-L6Y46C62.js +270 -0
  22. package/dist/chunk-NEQNXI2G.js +25 -0
  23. package/dist/chunk-PAOZBZME.js +40 -0
  24. package/dist/chunk-RWP4XBSK.js +139 -0
  25. package/dist/chunk-SRQWQUTW.js +63 -0
  26. package/dist/chunk-YXBOYWBN.js +15 -0
  27. package/dist/foundations/index.d.ts +88 -0
  28. package/dist/foundations/index.js +792 -0
  29. package/dist/icon/index.d.ts +60 -0
  30. package/dist/icon/index.js +2 -0
  31. package/dist/icon-sets/brands.d.ts +44 -0
  32. package/dist/icon-sets/brands.js +147 -0
  33. package/dist/icon-sets/reicon.d.ts +8 -0
  34. package/dist/icon-sets/reicon.js +1 -0
  35. package/dist/index.d.ts +7013 -0
  36. package/dist/index.js +15462 -0
  37. package/dist/ios/chrome.css +83 -0
  38. package/dist/ios/frame.css +5 -0
  39. package/dist/ios/index.d.ts +70 -0
  40. package/dist/ios/index.js +313 -0
  41. package/dist/ios/preview.d.ts +64 -0
  42. package/dist/ios/preview.js +133 -0
  43. package/dist/ios/styles.css +77 -0
  44. package/dist/layout/index.d.ts +280 -0
  45. package/dist/layout/index.js +2 -0
  46. package/dist/meta-6dUzgC6M.d.ts +205 -0
  47. package/dist/patterns/index.d.ts +36 -0
  48. package/dist/patterns/index.js +934 -0
  49. package/dist/presets/index.d.ts +37 -0
  50. package/dist/presets/index.js +117 -0
  51. package/dist/registry-CyL28OzJ.d.ts +28 -0
  52. package/dist/styles.css +1893 -0
  53. package/dist/theme-provider-BWcizhAt.d.ts +105 -0
  54. package/dist/themes/atelier/atelier.css +213 -0
  55. package/dist/themes/atelier/index.d.ts +9 -0
  56. package/dist/themes/atelier/index.js +1 -0
  57. package/dist/themes/base/base.css +443 -0
  58. package/dist/themes/base/index.d.ts +9 -0
  59. package/dist/themes/base/index.js +1 -0
  60. package/dist/themes/ember/ember.css +270 -0
  61. package/dist/themes/ember/index.d.ts +9 -0
  62. package/dist/themes/ember/index.js +1 -0
  63. package/dist/themes/index.d.ts +10 -0
  64. package/dist/themes/index.js +8 -0
  65. package/dist/themes/meridian/index.d.ts +9 -0
  66. package/dist/themes/meridian/index.js +1 -0
  67. package/dist/themes/meridian/meridian.css +255 -0
  68. package/dist/themes/poppy/index.d.ts +6 -0
  69. package/dist/themes/poppy/index.js +1 -0
  70. package/dist/themes/poppy/poppy.css +174 -0
  71. package/dist/themes/prism/index.d.ts +6 -0
  72. package/dist/themes/prism/index.js +1 -0
  73. package/dist/themes/prism/prism.css +183 -0
  74. package/dist/themes/verdant/index.d.ts +9 -0
  75. package/dist/themes/verdant/index.js +1 -0
  76. package/dist/themes/verdant/verdant.css +210 -0
  77. package/dist/themes.css +30 -0
  78. package/llms/components/accordion.md +93 -0
  79. package/llms/components/app-header.md +70 -0
  80. package/llms/components/app-shell.md +117 -0
  81. package/llms/components/attachment.md +105 -0
  82. package/llms/components/avatar.md +141 -0
  83. package/llms/components/badge.md +117 -0
  84. package/llms/components/banner.md +119 -0
  85. package/llms/components/bottom-nav.md +80 -0
  86. package/llms/components/breadcrumbs.md +75 -0
  87. package/llms/components/browser.md +67 -0
  88. package/llms/components/bubble.md +119 -0
  89. package/llms/components/button.md +199 -0
  90. package/llms/components/calendar.md +75 -0
  91. package/llms/components/card.md +135 -0
  92. package/llms/components/carousel.md +87 -0
  93. package/llms/components/chat-composer.md +126 -0
  94. package/llms/components/checkbox-card.md +98 -0
  95. package/llms/components/checkbox.md +90 -0
  96. package/llms/components/code-block.md +81 -0
  97. package/llms/components/code.md +62 -0
  98. package/llms/components/collapsible.md +83 -0
  99. package/llms/components/color-field.md +100 -0
  100. package/llms/components/combobox.md +119 -0
  101. package/llms/components/command-menu.md +158 -0
  102. package/llms/components/context-card.md +73 -0
  103. package/llms/components/context-menu.md +79 -0
  104. package/llms/components/data-table.md +99 -0
  105. package/llms/components/date-picker.md +97 -0
  106. package/llms/components/description.md +96 -0
  107. package/llms/components/destructive-action-modal.md +76 -0
  108. package/llms/components/dialog.md +127 -0
  109. package/llms/components/dots-menu.md +64 -0
  110. package/llms/components/empty-state.md +113 -0
  111. package/llms/components/entity.md +67 -0
  112. package/llms/components/feedback.md +63 -0
  113. package/llms/components/fieldset.md +106 -0
  114. package/llms/components/file-dropzone.md +103 -0
  115. package/llms/components/form.md +111 -0
  116. package/llms/components/gliding-highlight.md +84 -0
  117. package/llms/components/inline-status.md +62 -0
  118. package/llms/components/input.md +127 -0
  119. package/llms/components/kbd.md +57 -0
  120. package/llms/components/label.md +73 -0
  121. package/llms/components/marker.md +99 -0
  122. package/llms/components/marquee.md +66 -0
  123. package/llms/components/menu.md +122 -0
  124. package/llms/components/message-scroller.md +110 -0
  125. package/llms/components/message.md +114 -0
  126. package/llms/components/meter.md +124 -0
  127. package/llms/components/middle-truncate.md +62 -0
  128. package/llms/components/note.md +81 -0
  129. package/llms/components/notification-badge.md +67 -0
  130. package/llms/components/number-field.md +70 -0
  131. package/llms/components/otp-field.md +101 -0
  132. package/llms/components/page-header.md +76 -0
  133. package/llms/components/page-loader.md +67 -0
  134. package/llms/components/pagination.md +92 -0
  135. package/llms/components/panel-group.md +120 -0
  136. package/llms/components/panel-section.md +122 -0
  137. package/llms/components/phone.md +70 -0
  138. package/llms/components/pill.md +73 -0
  139. package/llms/components/popover.md +79 -0
  140. package/llms/components/progress.md +98 -0
  141. package/llms/components/radio-card.md +90 -0
  142. package/llms/components/radio.md +86 -0
  143. package/llms/components/relative-time.md +60 -0
  144. package/llms/components/scroller.md +88 -0
  145. package/llms/components/scrub-field.md +97 -0
  146. package/llms/components/search-input.md +84 -0
  147. package/llms/components/select.md +106 -0
  148. package/llms/components/separator.md +70 -0
  149. package/llms/components/settings-menu.md +139 -0
  150. package/llms/components/sheet.md +97 -0
  151. package/llms/components/show-more.md +59 -0
  152. package/llms/components/side-nav.md +106 -0
  153. package/llms/components/sidebar.md +87 -0
  154. package/llms/components/skeleton.md +62 -0
  155. package/llms/components/slider.md +114 -0
  156. package/llms/components/spinner.md +87 -0
  157. package/llms/components/status-bar.md +91 -0
  158. package/llms/components/status-dot.md +70 -0
  159. package/llms/components/stepper.md +125 -0
  160. package/llms/components/streaming-text.md +86 -0
  161. package/llms/components/switch.md +87 -0
  162. package/llms/components/table.md +112 -0
  163. package/llms/components/tabs.md +84 -0
  164. package/llms/components/textarea.md +88 -0
  165. package/llms/components/theme-selector.md +95 -0
  166. package/llms/components/theme-switcher.md +71 -0
  167. package/llms/components/thinking-orb.md +91 -0
  168. package/llms/components/toast.md +130 -0
  169. package/llms/components/toggle.md +95 -0
  170. package/llms/components/toolbar.md +168 -0
  171. package/llms/components/tooltip.md +93 -0
  172. package/llms/components/tree.md +227 -0
  173. package/llms/components/video.md +66 -0
  174. package/llms/components/workspace.md +138 -0
  175. package/llms/foundations/accessibility.md +60 -0
  176. package/llms/foundations/motion.md +96 -0
  177. package/llms/foundations/scrolling.md +41 -0
  178. package/llms/foundations/shape.md +73 -0
  179. package/llms/foundations/spacing.md +82 -0
  180. package/llms/foundations/surfaces.md +69 -0
  181. package/llms/foundations/typography.md +120 -0
  182. package/llms/layout/aspect-box.md +37 -0
  183. package/llms/layout/bleed.md +31 -0
  184. package/llms/layout/center.md +37 -0
  185. package/llms/layout/columns.md +51 -0
  186. package/llms/layout/grid-overlay.md +40 -0
  187. package/llms/layout/grid.md +47 -0
  188. package/llms/layout/page.md +39 -0
  189. package/llms/layout/region.md +49 -0
  190. package/llms/layout/row.md +40 -0
  191. package/llms/layout/safe-area.md +37 -0
  192. package/llms/layout/spacer.md +39 -0
  193. package/llms/layout/split-pane.md +41 -0
  194. package/llms/layout/stack.md +39 -0
  195. package/llms/patterns/auth-page.md +68 -0
  196. package/llms/patterns/brand-landing-page.md +423 -0
  197. package/llms/patterns/chat-panel.md +210 -0
  198. package/llms/patterns/dashboard-page.md +170 -0
  199. package/llms/patterns/feed-page.md +103 -0
  200. package/llms/patterns/landing-page.md +122 -0
  201. package/llms/patterns/list-detail-page.md +100 -0
  202. package/llms/patterns/settings-page.md +121 -0
  203. package/llms/reference/token-intent.md +324 -0
  204. package/llms/reference/utilities.md +129 -0
  205. package/llms/tasks/adding-a-component.md +113 -0
  206. package/llms/tasks/auditing-an-app.md +216 -0
  207. package/llms/tasks/building-a-page.md +109 -0
  208. package/llms/tasks/building-a-tool.md +141 -0
  209. package/llms/tasks/building-an-ios-prototype.md +105 -0
  210. package/llms/tasks/building-with-sable.md +73 -0
  211. package/llms/tasks/choosing-a-component.md +109 -0
  212. package/llms/tasks/converting-an-app.md +139 -0
  213. package/llms/tasks/creating-a-preset.md +43 -0
  214. package/llms/tasks/creating-design-assets.md +96 -0
  215. package/llms/tasks/designing-a-surface.md +115 -0
  216. package/llms/tasks/developing-taste.md +157 -0
  217. package/llms/tasks/getting-started.md +63 -0
  218. package/llms/tasks/learning-from-builds.md +74 -0
  219. package/llms/tasks/reviewing-a-design.md +114 -0
  220. package/llms/tasks/reviewing-a-journey.md +84 -0
  221. package/llms/tasks/reviewing-motion.md +92 -0
  222. package/llms/tasks/using-presets.md +87 -0
  223. package/llms/taste/asset-plan.template.json +53 -0
  224. package/llms/taste/composition-follows-content.md +20 -0
  225. package/llms/taste/hierarchy-is-a-relationship.md +22 -0
  226. package/llms/taste/lessons.json +57 -0
  227. package/llms/taste/motion-needs-a-reason.md +21 -0
  228. package/llms/taste/surface-states-travel-together.md +20 -0
  229. package/llms-full.txt +14906 -0
  230. package/llms.txt +405 -0
  231. package/package.json +185 -3
  232. package/registry.json +18812 -0
  233. package/scripts/agent-setup.mjs +54 -0
  234. package/scripts/design-atlas.mjs +221 -0
  235. package/scripts/design-directions.mjs +123 -0
  236. package/scripts/design-memory.mjs +166 -0
  237. package/scripts/design-preferences.mjs +227 -0
  238. package/scripts/design-presets.mjs +48 -0
  239. package/scripts/grade.mjs +509 -0
  240. package/scripts/lint-rules.mjs +1065 -0
  241. package/scripts/review-evidence.mjs +120 -0
  242. package/scripts/skeleton.mjs +248 -0
  243. package/skills/entry/SKILL.md +34 -0
  244. package/skills/sable/SKILL.md +187 -0
  245. package/skills/sable/agents/openai.yaml +6 -0
  246. package/targets/ios.json +982 -0
  247. package/third-party/reicon-LICENSE.txt +20 -0
@@ -0,0 +1,509 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * grade — score a folder of generated code against the Sable contract.
4
+ *
5
+ * node scripts/grade.mjs ../some-app/src
6
+ * node scripts/grade.mjs ./out --json
7
+ * node scripts/grade.mjs ./out --cli-log run.jsonl
8
+ *
9
+ * Why this exists: we have shipped a large amount of machinery aimed at making
10
+ * agents write correct Sable code — the registry, llms.txt, the task docs, the
11
+ * patterns tier, and now a retrieval CLI — and measured none of it. "Did that
12
+ * help?" has been answered by reading diffs and forming an impression. This
13
+ * turns the question into a number, with no model calls, so the same code can be
14
+ * scored repeatedly and compared across conditions.
15
+ *
16
+ * It is deliberately NOT a typechecker. `tsc` already catches unknown imports
17
+ * and bad prop types far better than any lexical pass, and a grader that
18
+ * duplicated it would mostly measure whether the sample compiles. What it
19
+ * measures instead is the stuff tsc structurally cannot see:
20
+ *
21
+ * - class-string drift, which Tailwind silently no-ops (the design contract)
22
+ * - Radix idioms hallucinated into Base UI code (`asChild`)
23
+ * - the density ladder bypassed with a hardcoded pixel height
24
+ * - icons named outside the contract
25
+ * - pattern similarity as a diagnostic, never as a reward for copying a shell
26
+ *
27
+ * Every check is lexical and dependency-free. Scores are comparative
28
+ * instruments, not grades in an absolute sense — a 78 means nothing on its own
29
+ * and everything next to the same task scored 61.
30
+ */
31
+
32
+ import { existsSync, readFileSync, readdirSync, statSync } from "node:fs"
33
+ import { createHash } from "node:crypto"
34
+ import { dirname, extname, join, relative, resolve } from "node:path"
35
+ import { fileURLToPath } from "node:url"
36
+ import { lintText, makeRules } from "./lint-rules.mjs"
37
+ import { skeletonize } from "./skeleton.mjs"
38
+ import { assessReview, reviewTemplate } from "./review-evidence.mjs"
39
+
40
+ const HERE = dirname(fileURLToPath(import.meta.url))
41
+ const PKG_ROOT = join(HERE, "..")
42
+ const CWD = process.cwd()
43
+
44
+ const argv = process.argv.slice(2)
45
+ const flags = new Map()
46
+ const targets = []
47
+ const BOOLEAN_FLAGS = new Set(["json", "help", "template", "strict"])
48
+ const VALUE_FLAGS = new Set(["cli-log", "review"])
49
+ const JSON_OUT = argv.includes("--json")
50
+ function abort(message) {
51
+ const error = { type: "error", code: "ERR_GRADE_INPUT", message, suggestions: ["sable grade --help", "sable grade --template"] }
52
+ process.stderr.write(JSON_OUT ? `${JSON.stringify(error)}\n` : `[grade] ${message}\n`)
53
+ process.exit(1)
54
+ }
55
+ for (let i = 0; i < argv.length; i++) {
56
+ const a = argv[i]
57
+ if (!a.startsWith("--")) targets.push(a)
58
+ else if (BOOLEAN_FLAGS.has(a.slice(2))) flags.set(a.slice(2), true)
59
+ else if (VALUE_FLAGS.has(a.slice(2))) {
60
+ if (!argv[i + 1] || argv[i + 1].startsWith("--")) abort(`${a} requires a file path.`)
61
+ flags.set(a.slice(2), argv[++i])
62
+ } else abort(`Unknown flag ${a}.`)
63
+ }
64
+
65
+ if (flags.has("template")) {
66
+ process.stdout.write(`${JSON.stringify(reviewTemplate(), null, 2)}\n`)
67
+ process.exit(0)
68
+ }
69
+
70
+ if (flags.has("help") || !targets.length) {
71
+ process.stdout.write(
72
+ [
73
+ "grade — check the static contract and recorded review evidence. No model calls.",
74
+ "",
75
+ " sable grade <dir...> [--json] [--review <review.json>] [--strict]",
76
+ "",
77
+ " --json machine-readable scorecard",
78
+ " --cli-log a SABLE_CLI_LOG jsonl, to report whether the CLI was used at all",
79
+ " --template print a review JSON template; fill after inspecting the rendered build",
80
+ " --review validate evidence files and observations; paths are relative to this JSON",
81
+ " --strict exit 1 for static findings or a missing/incomplete/unresolved review",
82
+ "",
83
+ "Checks: design-contract violations · hallucinated components and props ·",
84
+ "Radix idioms · hardcoded control heights · off-contract icon names ·",
85
+ "pattern similarity (informational only; original compositions carry no penalty).",
86
+ "A clean contract is not visual approval. This tool does not inspect screenshots.",
87
+ "",
88
+ ].join("\n"),
89
+ )
90
+ process.exit(flags.has("help") ? 0 : 1)
91
+ }
92
+
93
+ /* ── The contract. ─────────────────────────────────────────────────────────── */
94
+
95
+ const REGISTRY = JSON.parse(readFileSync(join(PKG_ROOT, "registry.json"), "utf8"))
96
+ const semanticTokens = new Set((REGISTRY.tokens?.semantic ?? []).map((t) => t.name))
97
+ const motionTokens = new Set(
98
+ [
99
+ ...(REGISTRY.tokens?.motion?.durations ?? []).map((d) => `motion-duration-${d}`),
100
+ ...(REGISTRY.tokens?.motion?.easings ?? []).map((e) => `motion-ease-${e}`),
101
+ REGISTRY.tokens?.motion?.scale,
102
+ ].filter(Boolean),
103
+ )
104
+ const RULES = makeRules({ semanticTokens, motionTokens }).filter((r) => r.scope !== "package")
105
+ const ICONS = new Set(REGISTRY.iconContract?.names ?? [])
106
+ const PATTERNS = REGISTRY.patterns?.items ?? []
107
+
108
+ /**
109
+ * Everything importable from the package. Read from the real barrel where it is
110
+ * available, because the registry describes components and misses the hooks,
111
+ * providers and helpers a page legitimately imports (`cn`, `useSableTheme`,
112
+ * `Presence`, …) — grading those as hallucinations would be worse than useless.
113
+ */
114
+ function exportSurface() {
115
+ const names = new Set()
116
+ for (const c of REGISTRY.components ?? []) {
117
+ names.add(c.name)
118
+ for (const s of c.subcomponents ?? []) names.add(typeof s === "string" ? s : s.name)
119
+ }
120
+ for (const m of REGISTRY.layoutPrimitives?.primitives ?? []) names.add(m.name)
121
+ for (const p of PATTERNS) names.add(p.name)
122
+ for (const n of REGISTRY.motionLayer?.policy ?? []) names.add(n)
123
+ for (const n of REGISTRY.motionLayer?.primitives ?? []) names.add(n)
124
+
125
+ for (const rel of ["dist/index.d.ts", "src/index.ts"]) {
126
+ const p = join(PKG_ROOT, rel)
127
+ if (!existsSync(p)) continue
128
+ const src = readFileSync(p, "utf8")
129
+ for (const m of src.matchAll(/export\s+(?:type\s+)?\{([^}]+)\}/g)) {
130
+ for (const raw of m[1].split(",")) {
131
+ const name = raw.trim().split(/\s+as\s+/).pop()?.trim()
132
+ if (name && /^[A-Za-z_$][\w$]*$/.test(name)) names.add(name)
133
+ }
134
+ }
135
+ for (const m of src.matchAll(/export\s+(?:declare\s+)?(?:const|function|class)\s+([\w$]+)/g)) names.add(m[1])
136
+ break
137
+ }
138
+ return names
139
+ }
140
+ const EXPORTS = exportSurface()
141
+
142
+ /** Props any element legitimately takes, so they never read as hallucinated. */
143
+ const UNIVERSAL_PROPS = new Set([
144
+ "key", "ref", "className", "style", "id", "role", "tabIndex", "title", "hidden", "slot", "children",
145
+ "render", "asChild", "dir", "lang", "translate", "draggable", "contentEditable", "spellCheck",
146
+ "inert", "popover", "form", "name", "value", "defaultValue", "checked", "defaultChecked",
147
+ "disabled", "required", "readOnly", "placeholder", "type", "href", "target", "rel", "src", "alt",
148
+ "width", "height", "colSpan", "rowSpan", "htmlFor", "autoFocus", "autoComplete", "maxLength",
149
+ "minLength", "min", "max", "step", "rows", "cols", "open", "selected", "multiple", "loading",
150
+ ])
151
+ const isEventProp = (p) => /^on[A-Z]/.test(p)
152
+ const isPassthrough = (p) => p.startsWith("data-") || p.startsWith("aria-") || p.startsWith("--")
153
+
154
+ /* ── Walk. ─────────────────────────────────────────────────────────────────── */
155
+
156
+ const SKIP = new Set(["node_modules", ".next", ".nuxt", "dist", "build", "out", "coverage", ".git", ".turbo"])
157
+ const CODE = /\.(tsx|ts|jsx|js|css|scss|html)$/
158
+ function* walk(dir, includeAssets = false) {
159
+ for (const name of readdirSync(dir)) {
160
+ if (SKIP.has(name)) continue
161
+ const full = join(dir, name)
162
+ const st = statSync(full)
163
+ if (st.isDirectory()) yield* walk(full, includeAssets)
164
+ else if ((includeAssets || CODE.test(name)) && !/\.(test|spec)\./.test(name)) yield full
165
+ }
166
+ }
167
+
168
+ const snapshotFiles = []
169
+ for (const t of targets) {
170
+ const abs = resolve(CWD, t)
171
+ if (!existsSync(abs)) abort(`Not found: ${t}`)
172
+ if (statSync(abs).isDirectory()) snapshotFiles.push(...walk(abs, true))
173
+ else snapshotFiles.push(abs)
174
+ }
175
+ snapshotFiles.splice(0, snapshotFiles.length, ...new Set(snapshotFiles.sort()))
176
+ const files = snapshotFiles.filter((file) => CODE.test(file))
177
+ if (!files.length) abort("No source files found.")
178
+ if (!files.some((file) => readFileSync(file, "utf8").trim().length > 0)) abort("No non-empty source content found.")
179
+ // Git checkouts may use CRLF on Windows and LF in CI. Normalize only known text
180
+ // formats for reproducible source identity; binary assets retain byte identity.
181
+ const TEXT_FILE = /\.(?:[cm]?[jt]sx?|css|scss|html?|json|md|mdx|txt|ya?ml|svg|xml|csv|tsv|map)$/i
182
+ function fingerprintBytes(file) {
183
+ const bytes = readFileSync(file)
184
+ return TEXT_FILE.test(file) ? bytes.toString("utf8").replace(/\r\n/g, "\n") : bytes
185
+ }
186
+ // Include the actual library implementation, not just its version string: a
187
+ // workspace library can change before its version is bumped. Installed consumers
188
+ // fingerprint dist; contributors fingerprint src (which their bundler uses).
189
+ const runtimeRoot = join(PKG_ROOT, existsSync(join(PKG_ROOT, "src")) ? "src" : "dist")
190
+ const packageFingerprint = createHash("sha256")
191
+ const packageFiles = [join(PKG_ROOT, "registry.json"), join(HERE, "lint-rules.mjs"), ...(existsSync(runtimeRoot) ? walk(runtimeRoot) : [])].sort()
192
+ for (const file of packageFiles) packageFingerprint.update(relative(PKG_ROOT, file).replaceAll("\\", "/")).update("\0").update(fingerprintBytes(file)).update("\0")
193
+ const packageHash = packageFingerprint.digest("hex")
194
+ const fingerprint = createHash("sha256")
195
+ fingerprint.update(packageHash).update("\0")
196
+ // Asset bytes inside supplied targets matter too. Pass public/ alongside src/
197
+ // when images or fonts live there; remote resources cannot be fingerprinted.
198
+ for (const file of snapshotFiles) fingerprint.update(relative(CWD, file).replaceAll("\\", "/")).update("\0").update(fingerprintBytes(file)).update("\0")
199
+ const sourceHash = fingerprint.digest("hex")
200
+
201
+ /* ── Checks. ───────────────────────────────────────────────────────────────── */
202
+
203
+ const findings = { contract: [], hallucinatedComponent: [], hallucinatedProp: [], radixIdiom: [], hardcodedControl: [], offContractIcon: [] }
204
+ let loc = 0
205
+ const routeSkeletons = []
206
+
207
+ /**
208
+ * Attribute names on one JSX open tag, stopping at the tag's own `>`.
209
+ *
210
+ * Comments are skipped explicitly. A `//` line comment is legal *inside* a JSX
211
+ * open tag and Sable's own patterns use them to explain a prop — without this,
212
+ * every word of the prose scans as a prop name and the component appears to be
213
+ * carrying five hallucinated props.
214
+ */
215
+ function attrsOf(source, from) {
216
+ const out = []
217
+ let depth = 0
218
+ let quote = null
219
+ for (let i = from; i < source.length; i++) {
220
+ const c = source[i]
221
+ if (quote) {
222
+ if (c === quote) quote = null
223
+ continue
224
+ }
225
+ if (c === "/" && source[i + 1] === "/") {
226
+ const nl = source.indexOf("\n", i)
227
+ if (nl === -1) break
228
+ i = nl
229
+ continue
230
+ }
231
+ if (c === "/" && source[i + 1] === "*") {
232
+ const end = source.indexOf("*/", i + 2)
233
+ if (end === -1) break
234
+ i = end + 1
235
+ continue
236
+ }
237
+ if (c === '"' || c === "'" || c === "`") { quote = c; continue }
238
+ if (c === "{") depth++
239
+ else if (c === "}") depth--
240
+ else if (c === ">" && depth === 0) break
241
+ else if (depth === 0 && /[A-Za-z_]/.test(c)) {
242
+ const m = source.slice(i).match(/^([A-Za-z_][\w:-]*)\s*(=)?/)
243
+ if (m) {
244
+ out.push(m[1])
245
+ i += m[1].length - 1
246
+ }
247
+ }
248
+ }
249
+ return out
250
+ }
251
+
252
+ for (const file of files) {
253
+ const rel = relative(CWD, file).split(/[\\/]/).join("/")
254
+ const text = readFileSync(file, "utf8")
255
+ loc += text.split("\n").length
256
+
257
+ /* 1. Same contract as sable-lint. An illustration exception uses the shared
258
+ reasoned disable syntax; the presence of a gradient cannot exempt an
259
+ unrelated inline color elsewhere on that source line. */
260
+ const ctx = { isStylesheet: /\.(css|scss)$/.test(rel) }
261
+ for (const v of lintText(text, RULES, ctx)) {
262
+ findings.contract.push({ ...v, file: rel })
263
+ }
264
+
265
+ if (!/\.(tsx|jsx|ts|js)$/.test(extname(file) ? file : "")) continue
266
+
267
+ /* 2. Imports that name something the package does not export. */
268
+ for (const m of text.matchAll(/import\s*(?:type\s*)?\{([^}]+)\}\s*from\s*["']@sableui\/react[^"']*["']/g)) {
269
+ for (const raw of m[1].split(",")) {
270
+ const name = raw.trim().split(/\s+as\s+/)[0]?.trim()
271
+ if (!name || !/^[A-Z]/.test(name)) continue
272
+ if (!EXPORTS.has(name)) {
273
+ findings.hallucinatedComponent.push({ file: rel, name, line: lineOf(text, m.index) })
274
+ }
275
+ }
276
+ }
277
+
278
+ /* 3. Props set on a Sable component that its contract does not declare. */
279
+ for (const m of text.matchAll(/<([A-Z][\w]*)[\s/>]/g)) {
280
+ const comp = REGISTRY.components?.find((c) => c.name === m[1])
281
+ if (!comp?.props?.length) continue
282
+ const declared = new Set(comp.props.map((p) => p.name))
283
+ for (const sub of comp.subcomponents ?? []) {
284
+ if (typeof sub !== "string") for (const p of sub.props ?? []) declared.add(p.name)
285
+ }
286
+ for (const attr of attrsOf(text, m.index + m[0].length - 1)) {
287
+ if (declared.has(attr) || UNIVERSAL_PROPS.has(attr) || isEventProp(attr) || isPassthrough(attr)) continue
288
+ findings.hallucinatedProp.push({ file: rel, component: m[1], prop: attr, line: lineOf(text, m.index) })
289
+ }
290
+ }
291
+
292
+ /* 4. Radix idioms. `asChild` is the single most-documented hallucination
293
+ against Base UI code — Sable composes with `render`. */
294
+ for (const m of text.matchAll(/\basChild\b/g)) findings.radixIdiom.push({ file: rel, what: "asChild", line: lineOf(text, m.index) })
295
+ for (const m of text.matchAll(/from\s*["'](@radix-ui\/[^"']+|@base-ui\/react[^"']*)["']/g)) {
296
+ findings.radixIdiom.push({ file: rel, what: `direct import ${m[1]}`, line: lineOf(text, m.index) })
297
+ }
298
+
299
+ /* 5. Hardcoded control heights — bypassing the density ladder means the
300
+ component cannot follow a `data-density` scope, which stays invisible
301
+ until someone opens the workspace register.
302
+ Bounded to the ladder's actual range (24-44px, widened to 20-56 for
303
+ slack). An unbounded `h-[\d+px]` flags every media box and collage strip
304
+ on the page: `h-[380px]` is an illustration, not a control. */
305
+ for (const m of text.matchAll(/\bh-\[(\d+)px\]/g)) {
306
+ const px = Number(m[1])
307
+ if (px >= 20 && px <= 56) findings.hardcodedControl.push({ file: rel, match: m[0], line: lineOf(text, m.index) })
308
+ }
309
+
310
+ /* 6. Icon names outside the contract render nothing at all. */
311
+ for (const m of text.matchAll(/<Icon\s+[^>]*name=["']([^"']+)["']/g)) {
312
+ if (!ICONS.has(m[1])) findings.offContractIcon.push({ file: rel, name: m[1], line: lineOf(text, m.index) })
313
+ }
314
+
315
+ /* 7. Composition. Anything exporting a component that returns a big tree is
316
+ treated as a route worth comparing against the canonical shells. */
317
+ if (/\.(tsx|jsx)$/.test(file) && /export\s+(default\s+)?function\s+[A-Z]/.test(text)) {
318
+ const { tree } = skeletonize(text)
319
+ if (tree.split("\n").length >= 6) routeSkeletons.push({ file: rel, tree })
320
+ }
321
+ }
322
+
323
+ function lineOf(text, index) {
324
+ return text.slice(0, index).split("\n").length
325
+ }
326
+
327
+ /* ── Pattern conformance. ──────────────────────────────────────────────────── */
328
+
329
+ /**
330
+ * How close a page's composition is to the nearest canonical shell.
331
+ *
332
+ * This is the measurement nothing else in the ecosystem takes. Every other
333
+ * signal here asks "is this line legal"; this one asks "is this page SHAPED
334
+ * like the system's own answer to this problem" — the documented gap where an
335
+ * agent produces a pile of individually-correct components in the wrong
336
+ * arrangement. Half structural vocabulary (which primitives appear at all),
337
+ * half order (do they nest and sequence the same way).
338
+ */
339
+ /**
340
+ * Only SABLE primitives count toward composition. Including raw html would score
341
+ * two pages as similar for both containing many `div`s — the question is whether
342
+ * the page reaches for the system's vocabulary in the system's arrangement, and
343
+ * `div`/`span`/`p` carry no signal about that either way.
344
+ */
345
+ const tagsOf = (tree) =>
346
+ tree
347
+ .split("\n")
348
+ .map((l) => l.trim().match(/^<([A-Za-z][\w.]*)/)?.[1])
349
+ .filter((t) => t && EXPORTS.has(t))
350
+
351
+ function lcs(a, b) {
352
+ const prev = new Array(b.length + 1).fill(0)
353
+ for (let i = 1; i <= a.length; i++) {
354
+ let diag = 0
355
+ for (let j = 1; j <= b.length; j++) {
356
+ const tmp = prev[j]
357
+ prev[j] = a[i - 1] === b[j - 1] ? diag + 1 : Math.max(prev[j], prev[j - 1])
358
+ diag = tmp
359
+ }
360
+ }
361
+ return prev[b.length]
362
+ }
363
+
364
+ function similarity(a, b) {
365
+ if (!a.length || !b.length) return 0
366
+ const sa = new Set(a)
367
+ const sb = new Set(b)
368
+ const inter = [...sa].filter((x) => sb.has(x)).length
369
+ const jaccard = inter / (sa.size + sb.size - inter)
370
+ const order = (2 * lcs(a, b)) / (a.length + b.length)
371
+ return 0.5 * jaccard + 0.5 * order
372
+ }
373
+
374
+ const patternTags = PATTERNS.map((p) => ({ name: p.name, tags: tagsOf(skeletonize(p.code).tree) }))
375
+ const conformance = routeSkeletons.map((r) => {
376
+ const tags = tagsOf(r.tree)
377
+ let best = { name: null, score: 0 }
378
+ for (const p of patternTags) {
379
+ const s = similarity(tags, p.tags)
380
+ if (s > best.score) best = { name: p.name, score: s }
381
+ }
382
+ return { file: r.file, nearest: best.name, score: +best.score.toFixed(3), depth: r.tree.split("\n").length }
383
+ })
384
+ const meanConformance = conformance.length
385
+ ? +(conformance.reduce((s, c) => s + c.score, 0) / conformance.length).toFixed(3)
386
+ : null
387
+
388
+ /* ── Did the agent use the retrieval surface at all? ───────────────────────── */
389
+
390
+ let cliUsage = null
391
+ const logPath = flags.get("cli-log")
392
+ if (typeof logPath === "string" && existsSync(resolve(CWD, logPath))) {
393
+ const lines = readFileSync(resolve(CWD, logPath), "utf8").split("\n").filter(Boolean)
394
+ const byCommand = {}
395
+ for (const l of lines) {
396
+ try {
397
+ const e = JSON.parse(l)
398
+ byCommand[e.command ?? "?"] = (byCommand[e.command ?? "?"] ?? 0) + 1
399
+ } catch {}
400
+ }
401
+ cliUsage = { invocations: lines.length, byCommand }
402
+ }
403
+
404
+ /* ── Score. ────────────────────────────────────────────────────────────────── */
405
+
406
+ const per100 = (n) => (loc ? +((n / loc) * 100).toFixed(2) : 0)
407
+ const counts = Object.fromEntries(Object.entries(findings).map(([k, v]) => [k, v.length]))
408
+
409
+ /*
410
+ * Weights are a judgment, stated here rather than buried: a hallucinated
411
+ * component or a Radix idiom does not compile or does not render, so it costs
412
+ * most; a contract violation renders but wrong. Shell similarity is diagnostic
413
+ * only: copying a pattern cannot compensate for a defect, and original
414
+ * compositions must be able to pass the contract without resembling fixtures.
415
+ */
416
+ const penalties = {
417
+ hallucinatedComponent: counts.hallucinatedComponent * 10, // does not compile
418
+ radixIdiom: counts.radixIdiom * 10, // compiles, silently does nothing
419
+ offContractIcon: counts.offContractIcon * 6, // renders nothing at all
420
+ hallucinatedProp: counts.hallucinatedProp * 3, // ignored prop
421
+ hardcodedControl: counts.hardcodedControl * 2, // breaks the register
422
+ contract: counts.contract * 1.5, // renders, but off-token
423
+ }
424
+ const deduction = Math.min(60, Object.values(penalties).reduce((a, b) => a + b, 0))
425
+ const score = Math.max(0, Math.min(100, Math.round(100 - deduction)))
426
+ let reviewDocument = null
427
+ const reviewPath = flags.get("review")
428
+ if (reviewPath) {
429
+ try { reviewDocument = JSON.parse(readFileSync(resolve(CWD, reviewPath), "utf8")) }
430
+ catch (error) { abort(`Cannot read review ${reviewPath}: ${error.message}`) }
431
+ }
432
+ const review = assessReview(reviewDocument, { baseDir: reviewPath ? dirname(resolve(CWD, reviewPath)) : CWD, sourceHash })
433
+ const clean = !Object.values(counts).some(Boolean)
434
+ const exitCode = flags.has("strict") && (!clean || review.status !== "review-recorded") ? 1 : 0
435
+
436
+ const result = {
437
+ version: REGISTRY.version,
438
+ sourceHash,
439
+ fingerprint: { algorithm: "sha256", files: snapshotFiles.length, packageHash, coverage: "All non-test files in supplied targets, plus Sable runtime sources, registry, and lint contract. Known text formats normalize CRLF to LF; binary assets retain their bytes. Pass public/ for external asset directories; remote resources are not covered." },
440
+ files: files.length,
441
+ loc,
442
+ score,
443
+ scoreMeaning: "Static contract only. Pattern similarity does not affect this score; visual quality is not scored.",
444
+ contract: { status: clean ? "pass" : "fail", score },
445
+ review,
446
+ counts,
447
+ per100loc: Object.fromEntries(Object.entries(counts).map(([k, v]) => [k, per100(v)])),
448
+ conformance: { informationalOnly: true, mean: meanConformance, routes: conformance },
449
+ cliUsage,
450
+ findings,
451
+ }
452
+
453
+ if (JSON_OUT) {
454
+ process.stdout.write(`${JSON.stringify(result, null, 1)}\n`)
455
+ process.exit(exitCode)
456
+ }
457
+
458
+ /* ── Report. ───────────────────────────────────────────────────────────────── */
459
+
460
+ const L = (s = "") => process.stdout.write(`${s}\n`)
461
+ L()
462
+ L(` grade — ${files.length} files, ${loc} lines, against @sableui/react ${REGISTRY.version}`)
463
+ L(` ${"─".repeat(66)}`)
464
+ const row = (label, n, note = "") =>
465
+ L(` ${label.padEnd(26)}${String(n).padStart(4)}${note ? ` ${note}` : ""}`)
466
+ row("contract violations", counts.contract, counts.contract ? `${per100(counts.contract)}/100 loc` : "clean")
467
+ row("hallucinated components", counts.hallucinatedComponent)
468
+ row("hallucinated props", counts.hallucinatedProp)
469
+ row("Radix idioms", counts.radixIdiom, counts.radixIdiom ? "asChild / direct primitive imports" : "")
470
+ row("hardcoded control heights", counts.hardcodedControl)
471
+ row("off-contract icon names", counts.offContractIcon)
472
+ L()
473
+ if (conformance.length) {
474
+ L(` pattern similarity (informational) — ${conformance.length} route-like file(s), mean ${meanConformance}`)
475
+ for (const c of conformance) {
476
+ const bar = "█".repeat(Math.round(c.score * 20)).padEnd(20, "·")
477
+ L(` ${bar} ${String(c.score).padEnd(6)} ${c.nearest ?? "—"} ${c.file}`)
478
+ }
479
+ } else {
480
+ L(" composition no route-like files found")
481
+ }
482
+ if (cliUsage) {
483
+ L()
484
+ L(` sable CLI ${cliUsage.invocations} invocation(s)`)
485
+ for (const [c, n] of Object.entries(cliUsage.byCommand)) L(` ${String(n).padStart(3)}× ${c}`)
486
+ }
487
+ L()
488
+ L(` static contract ${score}/100 · ${clean ? "pass" : "fail"}`)
489
+ L(` visual review: ${review.status}`)
490
+ for (const issue of review.issues) L(` ${issue}`)
491
+ for (const issue of review.openIssues ?? []) L(` open: ${typeof issue === "string" ? issue : JSON.stringify(issue)}`)
492
+ L(` sourceHash: ${sourceHash}`)
493
+ L(` ${"─".repeat(66)}`)
494
+ if (!JSON_OUT && Object.values(counts).some(Boolean)) {
495
+ L()
496
+ for (const [kind, list] of Object.entries(findings)) {
497
+ if (!list.length) continue
498
+ L(` ${kind}:`)
499
+ for (const f of list.slice(0, 8)) {
500
+ const what = f.name ?? f.prop ?? f.what ?? f.match ?? f.ruleId
501
+ L(` ${f.file}:${f.line ?? "?"} ${f.component ? `<${f.component}> ` : ""}${what}`)
502
+ }
503
+ if (list.length > 8) L(` … ${list.length - 8} more`)
504
+ L()
505
+ }
506
+ }
507
+ L(" Static compliance is not visual approval. This tool does not inspect screenshots.")
508
+ L()
509
+ process.exit(exitCode)