decantr 0.9.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 (382) hide show
  1. package/AGENTS.md +868 -0
  2. package/CHANGELOG.md +255 -0
  3. package/CLAUDE.md +178 -0
  4. package/LICENSE +21 -0
  5. package/README.md +229 -0
  6. package/cli/art.js +127 -0
  7. package/cli/commands/a11y.js +61 -0
  8. package/cli/commands/audit.js +225 -0
  9. package/cli/commands/build.js +38 -0
  10. package/cli/commands/dev.js +18 -0
  11. package/cli/commands/doctor.js +197 -0
  12. package/cli/commands/figma-sync.js +48 -0
  13. package/cli/commands/figma-tokens.js +55 -0
  14. package/cli/commands/generate.js +26 -0
  15. package/cli/commands/init.js +116 -0
  16. package/cli/commands/lint.js +209 -0
  17. package/cli/commands/mcp.js +530 -0
  18. package/cli/commands/migrate.js +175 -0
  19. package/cli/commands/test.js +38 -0
  20. package/cli/commands/validate.js +354 -0
  21. package/cli/index.js +113 -0
  22. package/package.json +95 -0
  23. package/reference/atoms.md +517 -0
  24. package/reference/behaviors.md +384 -0
  25. package/reference/build-tooling.md +275 -0
  26. package/reference/color-guidelines.md +965 -0
  27. package/reference/component-lifecycle.md +137 -0
  28. package/reference/compound-spacing.md +95 -0
  29. package/reference/decantation-process.md +499 -0
  30. package/reference/dev-server-routes.md +93 -0
  31. package/reference/form-system.md +253 -0
  32. package/reference/i18n.md +336 -0
  33. package/reference/icons.md +576 -0
  34. package/reference/llm-primer.md +953 -0
  35. package/reference/plugins.md +252 -0
  36. package/reference/registry-consumption.md +76 -0
  37. package/reference/router.md +217 -0
  38. package/reference/shells.md +116 -0
  39. package/reference/spatial-guidelines.md +541 -0
  40. package/reference/ssr.md +234 -0
  41. package/reference/state-data.md +215 -0
  42. package/reference/state-patterns.md +166 -0
  43. package/reference/state.md +194 -0
  44. package/reference/style-system.md +110 -0
  45. package/reference/tokens.md +460 -0
  46. package/src/app.js +19 -0
  47. package/src/chart/_animate.js +266 -0
  48. package/src/chart/_base.js +109 -0
  49. package/src/chart/_data.js +209 -0
  50. package/src/chart/_format.js +106 -0
  51. package/src/chart/_interact.js +364 -0
  52. package/src/chart/_palette.js +105 -0
  53. package/src/chart/_renderer.js +52 -0
  54. package/src/chart/_scene.js +262 -0
  55. package/src/chart/_shared.js +371 -0
  56. package/src/chart/index.js +637 -0
  57. package/src/chart/layouts/_layout-base.js +328 -0
  58. package/src/chart/layouts/cartesian.js +148 -0
  59. package/src/chart/layouts/hierarchy.js +562 -0
  60. package/src/chart/layouts/polar.js +101 -0
  61. package/src/chart/renderers/canvas.js +179 -0
  62. package/src/chart/renderers/svg.js +256 -0
  63. package/src/chart/renderers/webgpu.js +715 -0
  64. package/src/chart/types/_type-base.js +26 -0
  65. package/src/chart/types/area.js +134 -0
  66. package/src/chart/types/bar.js +173 -0
  67. package/src/chart/types/box-plot.js +125 -0
  68. package/src/chart/types/bubble.js +63 -0
  69. package/src/chart/types/candlestick.js +115 -0
  70. package/src/chart/types/chord.js +85 -0
  71. package/src/chart/types/combination.js +108 -0
  72. package/src/chart/types/funnel.js +68 -0
  73. package/src/chart/types/gauge.js +163 -0
  74. package/src/chart/types/heatmap.js +98 -0
  75. package/src/chart/types/histogram.js +71 -0
  76. package/src/chart/types/line.js +111 -0
  77. package/src/chart/types/org-chart.js +93 -0
  78. package/src/chart/types/pie.js +81 -0
  79. package/src/chart/types/radar.js +96 -0
  80. package/src/chart/types/radial.js +68 -0
  81. package/src/chart/types/range-area.js +55 -0
  82. package/src/chart/types/range-bar.js +61 -0
  83. package/src/chart/types/sankey.js +73 -0
  84. package/src/chart/types/scatter.js +66 -0
  85. package/src/chart/types/sparkline.js +81 -0
  86. package/src/chart/types/sunburst.js +69 -0
  87. package/src/chart/types/swimlane.js +88 -0
  88. package/src/chart/types/treemap.js +62 -0
  89. package/src/chart/types/waterfall.js +100 -0
  90. package/src/components/_base.js +1658 -0
  91. package/src/components/_behaviors.js +1140 -0
  92. package/src/components/_primitives.js +534 -0
  93. package/src/components/_qr-encoder.js +539 -0
  94. package/src/components/accordion.js +207 -0
  95. package/src/components/affix.js +62 -0
  96. package/src/components/alert-dialog.js +75 -0
  97. package/src/components/alert.js +47 -0
  98. package/src/components/aspect-ratio.js +24 -0
  99. package/src/components/avatar-group.js +55 -0
  100. package/src/components/avatar.js +38 -0
  101. package/src/components/back-top.js +75 -0
  102. package/src/components/badge.js +74 -0
  103. package/src/components/banner.js +68 -0
  104. package/src/components/breadcrumb.js +162 -0
  105. package/src/components/button.js +115 -0
  106. package/src/components/calendar.js +131 -0
  107. package/src/components/card.js +192 -0
  108. package/src/components/carousel.js +98 -0
  109. package/src/components/cascader.js +261 -0
  110. package/src/components/checkbox.js +80 -0
  111. package/src/components/chip.js +81 -0
  112. package/src/components/code-block.js +82 -0
  113. package/src/components/collapsible.js +50 -0
  114. package/src/components/color-palette.js +438 -0
  115. package/src/components/color-picker.js +314 -0
  116. package/src/components/combobox.js +181 -0
  117. package/src/components/command.js +174 -0
  118. package/src/components/comment.js +206 -0
  119. package/src/components/context-menu.js +76 -0
  120. package/src/components/data-table.js +724 -0
  121. package/src/components/date-picker.js +217 -0
  122. package/src/components/date-range-picker.js +244 -0
  123. package/src/components/datetime-picker.js +271 -0
  124. package/src/components/descriptions.js +68 -0
  125. package/src/components/drawer.js +179 -0
  126. package/src/components/dropdown.js +88 -0
  127. package/src/components/empty.js +41 -0
  128. package/src/components/float-button.js +90 -0
  129. package/src/components/form.js +106 -0
  130. package/src/components/hover-card.js +49 -0
  131. package/src/components/icon.js +87 -0
  132. package/src/components/image.js +97 -0
  133. package/src/components/index.js +117 -0
  134. package/src/components/input-group.js +75 -0
  135. package/src/components/input-number.js +155 -0
  136. package/src/components/input-otp.js +178 -0
  137. package/src/components/input.js +91 -0
  138. package/src/components/kbd.js +36 -0
  139. package/src/components/label.js +25 -0
  140. package/src/components/list.js +118 -0
  141. package/src/components/masked-input.js +236 -0
  142. package/src/components/mentions.js +165 -0
  143. package/src/components/menu.js +259 -0
  144. package/src/components/message.js +80 -0
  145. package/src/components/modal.js +147 -0
  146. package/src/components/navigation-menu.js +166 -0
  147. package/src/components/notification.js +84 -0
  148. package/src/components/pagination.js +104 -0
  149. package/src/components/placeholder.js +132 -0
  150. package/src/components/popconfirm.js +70 -0
  151. package/src/components/popover.js +58 -0
  152. package/src/components/progress.js +61 -0
  153. package/src/components/qrcode.js +251 -0
  154. package/src/components/radiogroup.js +120 -0
  155. package/src/components/range-slider.js +176 -0
  156. package/src/components/rate.js +186 -0
  157. package/src/components/resizable.js +83 -0
  158. package/src/components/result.js +57 -0
  159. package/src/components/scroll-area.js +43 -0
  160. package/src/components/segmented.js +97 -0
  161. package/src/components/select.js +165 -0
  162. package/src/components/separator.js +31 -0
  163. package/src/components/shell.js +407 -0
  164. package/src/components/skeleton.js +39 -0
  165. package/src/components/slider.js +141 -0
  166. package/src/components/sortable-list.js +176 -0
  167. package/src/components/space.js +42 -0
  168. package/src/components/spinner.js +112 -0
  169. package/src/components/splitter.js +147 -0
  170. package/src/components/statistic.js +136 -0
  171. package/src/components/steps.js +99 -0
  172. package/src/components/switch.js +95 -0
  173. package/src/components/table.js +44 -0
  174. package/src/components/tabs.js +216 -0
  175. package/src/components/tag.js +115 -0
  176. package/src/components/textarea.js +82 -0
  177. package/src/components/time-picker.js +153 -0
  178. package/src/components/time-range-picker.js +170 -0
  179. package/src/components/timeline.js +226 -0
  180. package/src/components/toast.js +71 -0
  181. package/src/components/toggle.js +213 -0
  182. package/src/components/tooltip.js +57 -0
  183. package/src/components/tour.js +159 -0
  184. package/src/components/transfer.js +163 -0
  185. package/src/components/tree-select.js +274 -0
  186. package/src/components/tree.js +141 -0
  187. package/src/components/typography.js +136 -0
  188. package/src/components/upload.js +118 -0
  189. package/src/components/visually-hidden.js +20 -0
  190. package/src/components/watermark.js +124 -0
  191. package/src/core/index.js +539 -0
  192. package/src/core/lifecycle.js +69 -0
  193. package/src/css/atoms.js +651 -0
  194. package/src/css/components.js +940 -0
  195. package/src/css/derive.js +1296 -0
  196. package/src/css/index.js +265 -0
  197. package/src/css/runtime.js +268 -0
  198. package/src/css/styles/addons/bioluminescent.js +93 -0
  199. package/src/css/styles/addons/clay.js +70 -0
  200. package/src/css/styles/addons/clean.js +57 -0
  201. package/src/css/styles/addons/command-center.js +143 -0
  202. package/src/css/styles/addons/dopamine.js +83 -0
  203. package/src/css/styles/addons/editorial.js +80 -0
  204. package/src/css/styles/addons/glassmorphism.js +99 -0
  205. package/src/css/styles/addons/liquid-glass.js +105 -0
  206. package/src/css/styles/addons/prismatic.js +100 -0
  207. package/src/css/styles/addons/retro.js +63 -0
  208. package/src/css/styles/auradecantism.js +96 -0
  209. package/src/css/theme-registry.js +444 -0
  210. package/src/data/entity.js +281 -0
  211. package/src/data/index.js +13 -0
  212. package/src/data/persist.js +225 -0
  213. package/src/data/query.js +839 -0
  214. package/src/data/realtime.js +299 -0
  215. package/src/data/url.js +177 -0
  216. package/src/data/worker.js +134 -0
  217. package/src/explorer/archetypes.js +243 -0
  218. package/src/explorer/atoms.js +228 -0
  219. package/src/explorer/charts.js +497 -0
  220. package/src/explorer/components.js +129 -0
  221. package/src/explorer/foundations.js +949 -0
  222. package/src/explorer/icons.js +178 -0
  223. package/src/explorer/patterns.js +247 -0
  224. package/src/explorer/recipes.js +194 -0
  225. package/src/explorer/shared/pattern-examples.js +1337 -0
  226. package/src/explorer/shared/showcase-renderer.js +958 -0
  227. package/src/explorer/shared/spec-table.js +41 -0
  228. package/src/explorer/shared/usage-links.js +87 -0
  229. package/src/explorer/shell-config.js +10 -0
  230. package/src/explorer/shells.js +551 -0
  231. package/src/explorer/styles.js +161 -0
  232. package/src/explorer/tokens.js +262 -0
  233. package/src/explorer/tools.js +525 -0
  234. package/src/form/index.js +804 -0
  235. package/src/i18n/index.js +251 -0
  236. package/src/icons/essential.js +479 -0
  237. package/src/icons/index.js +53 -0
  238. package/src/plugins/index.js +282 -0
  239. package/src/registry/archetypes/content-site.json +71 -0
  240. package/src/registry/archetypes/docs-explorer.json +23 -0
  241. package/src/registry/archetypes/ecommerce.json +104 -0
  242. package/src/registry/archetypes/financial-dashboard.json +77 -0
  243. package/src/registry/archetypes/index.json +41 -0
  244. package/src/registry/archetypes/portfolio.json +82 -0
  245. package/src/registry/archetypes/recipe-community.json +159 -0
  246. package/src/registry/archetypes/saas-dashboard.json +86 -0
  247. package/src/registry/architect/cross-cutting.json +45 -0
  248. package/src/registry/architect/domains/ecommerce.json +294 -0
  249. package/src/registry/architect/domains/financial-services.json +302 -0
  250. package/src/registry/architect/index.json +26 -0
  251. package/src/registry/architect/traits.json +379 -0
  252. package/src/registry/atoms.json +16 -0
  253. package/src/registry/chart-showcase.json +160 -0
  254. package/src/registry/chart.json +136 -0
  255. package/src/registry/components.json +8616 -0
  256. package/src/registry/core.json +216 -0
  257. package/src/registry/css.json +319 -0
  258. package/src/registry/data.json +135 -0
  259. package/src/registry/foundations.json +11 -0
  260. package/src/registry/icons.json +463 -0
  261. package/src/registry/index.json +101 -0
  262. package/src/registry/patterns/activity-feed.json +37 -0
  263. package/src/registry/patterns/article-content.json +27 -0
  264. package/src/registry/patterns/auth-form.json +37 -0
  265. package/src/registry/patterns/author-card.json +20 -0
  266. package/src/registry/patterns/card-grid.json +127 -0
  267. package/src/registry/patterns/category-nav.json +26 -0
  268. package/src/registry/patterns/chart-grid.json +36 -0
  269. package/src/registry/patterns/chat-interface.json +37 -0
  270. package/src/registry/patterns/checklist-card.json +55 -0
  271. package/src/registry/patterns/comparison-panel.json +27 -0
  272. package/src/registry/patterns/component-showcase.json +24 -0
  273. package/src/registry/patterns/contact-form.json +31 -0
  274. package/src/registry/patterns/cta-section.json +20 -0
  275. package/src/registry/patterns/data-table.json +37 -0
  276. package/src/registry/patterns/detail-header.json +83 -0
  277. package/src/registry/patterns/detail-panel.json +27 -0
  278. package/src/registry/patterns/explorer-shell.json +22 -0
  279. package/src/registry/patterns/filter-bar.json +33 -0
  280. package/src/registry/patterns/filter-sidebar.json +27 -0
  281. package/src/registry/patterns/form-sections.json +110 -0
  282. package/src/registry/patterns/goal-tracker.json +27 -0
  283. package/src/registry/patterns/hero.json +107 -0
  284. package/src/registry/patterns/index.json +47 -0
  285. package/src/registry/patterns/kpi-grid.json +36 -0
  286. package/src/registry/patterns/media-gallery.json +20 -0
  287. package/src/registry/patterns/order-history.json +20 -0
  288. package/src/registry/patterns/pagination.json +19 -0
  289. package/src/registry/patterns/photo-to-recipe.json +36 -0
  290. package/src/registry/patterns/pipeline-tracker.json +28 -0
  291. package/src/registry/patterns/post-list.json +27 -0
  292. package/src/registry/patterns/pricing-table.json +32 -0
  293. package/src/registry/patterns/scorecard.json +28 -0
  294. package/src/registry/patterns/search-bar.json +20 -0
  295. package/src/registry/patterns/specimen-grid.json +19 -0
  296. package/src/registry/patterns/stat-card.json +55 -0
  297. package/src/registry/patterns/stats-bar.json +55 -0
  298. package/src/registry/patterns/steps-card.json +55 -0
  299. package/src/registry/patterns/table-of-contents.json +19 -0
  300. package/src/registry/patterns/testimonials.json +21 -0
  301. package/src/registry/patterns/timeline.json +27 -0
  302. package/src/registry/patterns/token-inspector.json +21 -0
  303. package/src/registry/patterns/wizard.json +27 -0
  304. package/src/registry/recipe-auradecantism.json +69 -0
  305. package/src/registry/recipe-clean.json +65 -0
  306. package/src/registry/recipe-command-center.json +78 -0
  307. package/src/registry/router.json +73 -0
  308. package/src/registry/schema/README.md +197 -0
  309. package/src/registry/skeletons.json +259 -0
  310. package/src/registry/state.json +137 -0
  311. package/src/registry/tokens.json +40 -0
  312. package/src/router/hash.js +17 -0
  313. package/src/router/history.js +18 -0
  314. package/src/router/index.js +598 -0
  315. package/src/ssr/index.js +922 -0
  316. package/src/state/arrays.js +181 -0
  317. package/src/state/devtools.js +647 -0
  318. package/src/state/index.js +498 -0
  319. package/src/state/middleware.js +288 -0
  320. package/src/state/scheduler.js +206 -0
  321. package/src/state/store.js +300 -0
  322. package/src/tags/index.js +19 -0
  323. package/src/tannins/auth.js +396 -0
  324. package/src/test/dom.js +352 -0
  325. package/src/test/index.js +62 -0
  326. package/src/test/state.js +306 -0
  327. package/tools/a11y-audit.js +487 -0
  328. package/tools/analyzer.js +315 -0
  329. package/tools/audit.js +706 -0
  330. package/tools/builder.js +1422 -0
  331. package/tools/css-extract.js +188 -0
  332. package/tools/dev-server.js +316 -0
  333. package/tools/dts-gen.js +1260 -0
  334. package/tools/figma-components.js +329 -0
  335. package/tools/figma-patterns.js +516 -0
  336. package/tools/figma-plugin/code.js +453 -0
  337. package/tools/figma-plugin/manifest.json +14 -0
  338. package/tools/figma-plugin/ui.html +268 -0
  339. package/tools/figma-render.js +293 -0
  340. package/tools/figma-tokens.js +712 -0
  341. package/tools/figma-upload.js +318 -0
  342. package/tools/generate.js +738 -0
  343. package/tools/icons.js +133 -0
  344. package/tools/init-templates.js +265 -0
  345. package/tools/install-hooks.sh +5 -0
  346. package/tools/migrations/0.5.0.js +53 -0
  347. package/tools/migrations/0.6.0.js +95 -0
  348. package/tools/minify.js +170 -0
  349. package/tools/pre-commit +4 -0
  350. package/tools/registry.js +662 -0
  351. package/tools/reset-playground.js +61 -0
  352. package/tools/starter-templates/content-site/app.js +49 -0
  353. package/tools/starter-templates/content-site/essence.js +19 -0
  354. package/tools/starter-templates/content-site/pages.js +31 -0
  355. package/tools/starter-templates/ecommerce/app.js +50 -0
  356. package/tools/starter-templates/ecommerce/essence.js +19 -0
  357. package/tools/starter-templates/ecommerce/pages.js +31 -0
  358. package/tools/starter-templates/landing-page/app.js +38 -0
  359. package/tools/starter-templates/landing-page/essence.js +18 -0
  360. package/tools/starter-templates/landing-page/pages.js +21 -0
  361. package/tools/starter-templates/portfolio/app.js +45 -0
  362. package/tools/starter-templates/portfolio/essence.js +19 -0
  363. package/tools/starter-templates/portfolio/pages.js +33 -0
  364. package/tools/starter-templates/saas-dashboard/app.js +70 -0
  365. package/tools/starter-templates/saas-dashboard/essence.js +19 -0
  366. package/tools/starter-templates/saas-dashboard/pages.js +31 -0
  367. package/tools/verify-pack.js +203 -0
  368. package/types/chart.d.ts +77 -0
  369. package/types/components.d.ts +587 -0
  370. package/types/core.d.ts +89 -0
  371. package/types/css.d.ts +149 -0
  372. package/types/data.d.ts +238 -0
  373. package/types/form.d.ts +164 -0
  374. package/types/i18n.d.ts +51 -0
  375. package/types/icons.d.ts +27 -0
  376. package/types/index.d.ts +13 -0
  377. package/types/router.d.ts +116 -0
  378. package/types/ssr.d.ts +102 -0
  379. package/types/state.d.ts +83 -0
  380. package/types/tags.d.ts +62 -0
  381. package/types/tannins.d.ts +63 -0
  382. package/types/test.d.ts +48 -0
package/tools/audit.js ADDED
@@ -0,0 +1,706 @@
1
+ import { readFile, readdir, stat } from 'node:fs/promises';
2
+ import { join, dirname, resolve, relative, extname } from 'node:path';
3
+ import { fileURLToPath } from 'node:url';
4
+ import { gzipSync, brotliCompressSync, constants as zlibConstants } from 'node:zlib';
5
+ import { performance } from 'node:perf_hooks';
6
+
7
+ const __dirname = dirname(fileURLToPath(import.meta.url));
8
+ const registryDir = resolve(__dirname, '..', 'src', 'registry');
9
+
10
+ // ─── Helpers ─────────────────────────────────────────────────────
11
+
12
+ /**
13
+ * Recursively find all .js files under a directory.
14
+ * @param {string} dir
15
+ * @returns {Promise<string[]>}
16
+ */
17
+ async function findJsFiles(dir) {
18
+ const results = [];
19
+ let entries;
20
+ try { entries = await readdir(dir, { withFileTypes: true }); } catch { return results; }
21
+ for (const entry of entries) {
22
+ const full = join(dir, entry.name);
23
+ if (entry.isDirectory()) {
24
+ // Skip node_modules, dist, .decantr
25
+ if (entry.name === 'node_modules' || entry.name === 'dist' || entry.name.startsWith('.')) continue;
26
+ results.push(...await findJsFiles(full));
27
+ } else if (entry.name.endsWith('.js')) {
28
+ results.push(full);
29
+ }
30
+ }
31
+ return results;
32
+ }
33
+
34
+ /**
35
+ * Recursively find all files under a directory.
36
+ * @param {string} dir
37
+ * @returns {Promise<string[]>}
38
+ */
39
+ async function findAllFiles(dir) {
40
+ const results = [];
41
+ let entries;
42
+ try { entries = await readdir(dir, { withFileTypes: true }); } catch { return results; }
43
+ for (const entry of entries) {
44
+ const full = join(dir, entry.name);
45
+ if (entry.isDirectory()) {
46
+ results.push(...await findAllFiles(full));
47
+ } else {
48
+ results.push(full);
49
+ }
50
+ }
51
+ return results;
52
+ }
53
+
54
+ function formatSize(bytes) {
55
+ if (bytes < 1024) return `${bytes} B`;
56
+ if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
57
+ return `${(bytes / (1024 * 1024)).toFixed(2)} MB`;
58
+ }
59
+
60
+ function brotliSize(content) {
61
+ return brotliCompressSync(Buffer.from(content), {
62
+ params: { [zlibConstants.BROTLI_PARAM_QUALITY]: 11 }
63
+ }).length;
64
+ }
65
+
66
+ /**
67
+ * Safely read and parse a JSON file. Returns null on failure.
68
+ * @param {string} filePath
69
+ * @returns {Promise<any>}
70
+ */
71
+ async function readJSON(filePath) {
72
+ try {
73
+ return JSON.parse(await readFile(filePath, 'utf-8'));
74
+ } catch {
75
+ return null;
76
+ }
77
+ }
78
+
79
+ // ─── Registry Loaders ────────────────────────────────────────────
80
+
81
+ /**
82
+ * Load known component names from components.json.
83
+ * @returns {Promise<string[]>}
84
+ */
85
+ async function loadComponentNames() {
86
+ const data = await readJSON(join(registryDir, 'components.json'));
87
+ if (!data || !data.components) return [];
88
+ return Object.keys(data.components);
89
+ }
90
+
91
+ /**
92
+ * Load known pattern names from patterns/index.json.
93
+ * @returns {Promise<string[]>}
94
+ */
95
+ async function loadPatternNames() {
96
+ const data = await readJSON(join(registryDir, 'patterns', 'index.json'));
97
+ if (!data || !data.patterns) return [];
98
+ return Object.keys(data.patterns);
99
+ }
100
+
101
+ // ─── Pass 1: Essence Compliance ──────────────────────────────────
102
+
103
+ const DECANTATION_STAGES = ['POUR', 'SETTLE', 'CLARIFY', 'DECANT', 'SERVE', 'AGE'];
104
+
105
+ /**
106
+ * Analyze essence file for decantation stage compliance.
107
+ * @param {string} projectRoot
108
+ * @returns {Promise<{valid: boolean, terroir: string|null, stagesCompleted: string[], stagesSkipped: string[], errors: string[]}>}
109
+ */
110
+ async function analyzeEssence(projectRoot) {
111
+ const result = {
112
+ valid: false,
113
+ terroir: null,
114
+ stagesCompleted: [],
115
+ stagesSkipped: [],
116
+ errors: []
117
+ };
118
+
119
+ const essencePath = join(projectRoot, 'decantr.essence.json');
120
+ const essence = await readJSON(essencePath);
121
+
122
+ if (!essence) {
123
+ result.errors.push('No decantr.essence.json found');
124
+ result.stagesSkipped = [...DECANTATION_STAGES];
125
+ return result;
126
+ }
127
+
128
+ result.valid = true;
129
+
130
+ // POUR: always complete if essence exists — it means intent was captured
131
+ result.stagesCompleted.push('POUR');
132
+
133
+ // SETTLE: terroir + vintage + character must be present
134
+ const hasTerroir = !!(essence.terroir || (essence.sections && essence.sections.length > 0));
135
+ const hasVintage = !!(essence.vintage || (essence.sections && essence.sections.some(s => s.vintage)));
136
+ const hasCharacter = !!(essence.character && Array.isArray(essence.character) && essence.character.length > 0);
137
+
138
+ if (hasTerroir) {
139
+ result.terroir = essence.terroir || essence.sections?.map(s => s.terroir).join(', ') || null;
140
+ }
141
+
142
+ if (hasTerroir && hasVintage && hasCharacter) {
143
+ result.stagesCompleted.push('SETTLE');
144
+ } else {
145
+ result.stagesSkipped.push('SETTLE');
146
+ if (!hasTerroir) result.errors.push('Missing terroir');
147
+ if (!hasVintage) result.errors.push('Missing vintage');
148
+ if (!hasCharacter) result.errors.push('Missing character');
149
+ }
150
+
151
+ // CLARIFY: structure + tannins present
152
+ const hasStructure = !!(
153
+ (essence.structure && Array.isArray(essence.structure) && essence.structure.length > 0) ||
154
+ (essence.sections && essence.sections.some(s => s.structure && s.structure.length > 0))
155
+ );
156
+ const hasTannins = !!(
157
+ (essence.tannins && Array.isArray(essence.tannins) && essence.tannins.length > 0) ||
158
+ (essence.sections && essence.sections.some(s => s.tannins && s.tannins.length > 0))
159
+ );
160
+
161
+ if (hasStructure && hasTannins) {
162
+ result.stagesCompleted.push('CLARIFY');
163
+ } else {
164
+ result.stagesSkipped.push('CLARIFY');
165
+ if (!hasStructure) result.errors.push('Missing structure');
166
+ if (!hasTannins) result.errors.push('Missing tannins');
167
+ }
168
+
169
+ // DECANT: style file exists in src/css/styles/
170
+ const vintage = essence.vintage || (essence.sections && essence.sections[0]?.vintage) || {};
171
+ const styleId = vintage.style;
172
+ if (styleId) {
173
+ const stylePath = resolve(__dirname, '..', 'src', 'css', 'styles', `${styleId}.js`);
174
+ try {
175
+ await stat(stylePath);
176
+ result.stagesCompleted.push('DECANT');
177
+ } catch {
178
+ result.stagesSkipped.push('DECANT');
179
+ result.errors.push(`Style file not found: ${styleId}.js`);
180
+ }
181
+ } else {
182
+ result.stagesSkipped.push('DECANT');
183
+ result.errors.push('No style specified in vintage');
184
+ }
185
+
186
+ // SERVE: pages exist in src/pages/
187
+ const pagesDir = join(projectRoot, 'src', 'pages');
188
+ try {
189
+ const pagesEntries = await readdir(pagesDir);
190
+ if (pagesEntries.length > 0) {
191
+ result.stagesCompleted.push('SERVE');
192
+ } else {
193
+ result.stagesSkipped.push('SERVE');
194
+ result.errors.push('src/pages/ exists but is empty');
195
+ }
196
+ } catch {
197
+ result.stagesSkipped.push('SERVE');
198
+ result.errors.push('No src/pages/ directory found');
199
+ }
200
+
201
+ // AGE: cork rules present
202
+ const hasCork = !!(essence.cork && (essence.cork.rules || essence.cork.enforce_style || essence.cork.enforce_recipe));
203
+ if (hasCork) {
204
+ result.stagesCompleted.push('AGE');
205
+ } else {
206
+ result.stagesSkipped.push('AGE');
207
+ result.errors.push('No cork rules defined');
208
+ }
209
+
210
+ return result;
211
+ }
212
+
213
+ // ─── Pass 2: Source Analysis ─────────────────────────────────────
214
+
215
+ /**
216
+ * @typedef {Object} SourceAnalysis
217
+ * @property {number} frameworkDerivedPct
218
+ * @property {number} projectSpecificPct
219
+ * @property {string[]} componentsUsed
220
+ * @property {number} componentsTotal
221
+ * @property {string[]} patternsUsed
222
+ * @property {number} patternsTotal
223
+ * @property {number} atomCalls
224
+ * @property {number} inlineStyleViolations
225
+ * @property {number} frameworkImports
226
+ * @property {number} totalStatements
227
+ */
228
+
229
+ /**
230
+ * Analyze source files for framework derivation vs improvised code.
231
+ * @param {string} projectRoot
232
+ * @param {string[]} knownComponents
233
+ * @param {string[]} knownPatterns
234
+ * @returns {Promise<SourceAnalysis>}
235
+ */
236
+ async function analyzeSource(projectRoot, knownComponents, knownPatterns) {
237
+ const srcDir = join(projectRoot, 'src');
238
+ const files = await findJsFiles(srcDir);
239
+
240
+ let frameworkImports = 0;
241
+ let atomCalls = 0;
242
+ let inlineStyleViolations = 0;
243
+ let totalStatements = 0;
244
+
245
+ const usedComponents = new Set();
246
+ const usedPatterns = new Set();
247
+
248
+ // Build regex patterns for component detection
249
+ // Match ComponentName( — function call usage
250
+ const componentCallRe = new RegExp(
251
+ '\\b(' + knownComponents.map(c => c.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')).join('|') + ')\\s*\\(',
252
+ 'g'
253
+ );
254
+
255
+ // Build pattern detection regex — pattern names in string literals or comments
256
+ const patternNameSet = new Set(knownPatterns);
257
+
258
+ for (const file of files) {
259
+ let source;
260
+ try { source = await readFile(file, 'utf-8'); } catch { continue; }
261
+
262
+ // Count approximate statements (lines with meaningful code)
263
+ const lines = source.split('\n');
264
+ for (const line of lines) {
265
+ const trimmed = line.trim();
266
+ if (trimmed && !trimmed.startsWith('//') && !trimmed.startsWith('/*') && !trimmed.startsWith('*')) {
267
+ totalStatements++;
268
+ }
269
+ }
270
+
271
+ // Count decantr imports
272
+ const importMatches = source.match(/from\s+['"]decantr\//g);
273
+ if (importMatches) frameworkImports += importMatches.length;
274
+
275
+ // Count css() calls
276
+ const cssMatches = source.match(/\bcss\s*\(/g);
277
+ if (cssMatches) atomCalls += cssMatches.length;
278
+
279
+ // Detect inline style violations — static px/rem/hex values in style: or .style.
280
+ // Match style: 'anything with px/rem/#hex' (static values only)
281
+ const styleAttrRe = /style:\s*['"`]([^'"`]*?)['"`]/g;
282
+ let styleMatch;
283
+ while ((styleMatch = styleAttrRe.exec(source)) !== null) {
284
+ const val = styleMatch[1];
285
+ // Flag if contains px, rem, em, hex color — these are static values
286
+ if (/\d+px|\d+rem|\d+em|#[0-9a-fA-F]{3,8}/.test(val)) {
287
+ inlineStyleViolations++;
288
+ }
289
+ }
290
+ // Also check .style.property = 'value' with static values
291
+ const dotStyleRe = /\.style\.\w+\s*=\s*['"`]([^'"`]*?)['"`]/g;
292
+ while ((styleMatch = dotStyleRe.exec(source)) !== null) {
293
+ const val = styleMatch[1];
294
+ if (/\d+px|\d+rem|\d+em|#[0-9a-fA-F]{3,8}/.test(val)) {
295
+ inlineStyleViolations++;
296
+ }
297
+ }
298
+
299
+ // Detect component usage
300
+ let compMatch;
301
+ while ((compMatch = componentCallRe.exec(source)) !== null) {
302
+ usedComponents.add(compMatch[1]);
303
+ }
304
+
305
+ // Detect pattern references in strings or comments
306
+ for (const pName of patternNameSet) {
307
+ if (source.includes(pName)) {
308
+ usedPatterns.add(pName);
309
+ }
310
+ }
311
+ }
312
+
313
+ // Compute derivation percentage
314
+ // Framework-derived signals: imports + atom calls + component uses
315
+ const frameworkSignals = frameworkImports + atomCalls + usedComponents.size;
316
+ // Total signals approximate: all statements
317
+ const derivedPct = totalStatements > 0
318
+ ? Math.min(100, Math.round((frameworkSignals / totalStatements) * 100))
319
+ : 0;
320
+
321
+ return {
322
+ frameworkDerivedPct: derivedPct,
323
+ projectSpecificPct: 100 - derivedPct,
324
+ componentsUsed: [...usedComponents].sort(),
325
+ componentsTotal: knownComponents.length,
326
+ patternsUsed: [...usedPatterns].sort(),
327
+ patternsTotal: knownPatterns.length,
328
+ atomCalls,
329
+ inlineStyleViolations,
330
+ frameworkImports,
331
+ totalStatements
332
+ };
333
+ }
334
+
335
+ // ─── Pass 3: Quality Checks ─────────────────────────────────────
336
+
337
+ /**
338
+ * @typedef {Object} QualityReport
339
+ * @property {{file: string, line: number, value: string}[]} hardcodedCSS
340
+ * @property {{file: string, line: number}[]} missingAria
341
+ * @property {{file: string, line: number}[]} leakedListeners
342
+ * @property {{file: string, line: number}[]} missingFocusTrap
343
+ */
344
+
345
+ /**
346
+ * Scan source files for quality issues.
347
+ * @param {string} projectRoot
348
+ * @returns {Promise<QualityReport>}
349
+ */
350
+ async function checkQuality(projectRoot) {
351
+ const srcDir = join(projectRoot, 'src');
352
+ const files = await findJsFiles(srcDir);
353
+
354
+ /** @type {QualityReport} */
355
+ const report = {
356
+ hardcodedCSS: [],
357
+ missingAria: [],
358
+ leakedListeners: [],
359
+ missingFocusTrap: []
360
+ };
361
+
362
+ for (const file of files) {
363
+ let source;
364
+ try { source = await readFile(file, 'utf-8'); } catch { continue; }
365
+
366
+ const relFile = relative(projectRoot, file);
367
+ const lines = source.split('\n');
368
+
369
+ // Track whether file has onDestroy somewhere
370
+ const hasOnDestroy = /\bonDestroy\b/.test(source);
371
+
372
+ // Track whether file has createFocusTrap
373
+ const hasFocusTrap = /\bcreateFocusTrap\b/.test(source);
374
+
375
+ // Track whether file creates overlays (Modal, Drawer, Popover)
376
+ const isOverlay = /\b(Modal|Drawer|Popover)\s*\(/.test(source) ||
377
+ /role:\s*['"]dialog['"]/.test(source);
378
+
379
+ for (let i = 0; i < lines.length; i++) {
380
+ const line = lines[i];
381
+ const lineNum = i + 1;
382
+
383
+ // Hardcoded CSS: style: with px/rem/hex static values
384
+ const styleAttrMatch = line.match(/style:\s*['"`]([^'"`]*?)['"`]/);
385
+ if (styleAttrMatch) {
386
+ const val = styleAttrMatch[1];
387
+ if (/\d+px|\d+rem|\d+em|#[0-9a-fA-F]{3,8}/.test(val)) {
388
+ report.hardcodedCSS.push({ file: relFile, line: lineNum, value: val.slice(0, 60) });
389
+ }
390
+ }
391
+ const dotStyleMatch = line.match(/\.style\.\w+\s*=\s*['"`]([^'"`]*?)['"`]/);
392
+ if (dotStyleMatch) {
393
+ const val = dotStyleMatch[1];
394
+ if (/\d+px|\d+rem|\d+em|#[0-9a-fA-F]{3,8}/.test(val)) {
395
+ report.hardcodedCSS.push({ file: relFile, line: lineNum, value: val.slice(0, 60) });
396
+ }
397
+ }
398
+
399
+ // Missing aria-label on icon-only buttons
400
+ // Detect icon( call near Button( without aria-label nearby
401
+ if (/\bicon\s*\(/.test(line)) {
402
+ // Look within a 5-line window for Button( without aria-label
403
+ const windowStart = Math.max(0, i - 3);
404
+ const windowEnd = Math.min(lines.length - 1, i + 3);
405
+ const window = lines.slice(windowStart, windowEnd + 1).join('\n');
406
+ if (/\bButton\s*\(/.test(window) && !/aria-label/.test(window)) {
407
+ report.missingAria.push({ file: relFile, line: lineNum });
408
+ }
409
+ }
410
+
411
+ // Leaked listeners: addEventListener without onDestroy in the same file
412
+ if (/\.addEventListener\s*\(/.test(line) || /document\.addEventListener/.test(line) || /window\.addEventListener/.test(line)) {
413
+ if (!hasOnDestroy) {
414
+ report.leakedListeners.push({ file: relFile, line: lineNum });
415
+ }
416
+ }
417
+ }
418
+
419
+ // Missing focus trap in overlay components
420
+ if (isOverlay && !hasFocusTrap) {
421
+ report.missingFocusTrap.push({ file: relFile, line: 1 });
422
+ }
423
+ }
424
+
425
+ return report;
426
+ }
427
+
428
+ // ─── Pass 4: Coverage Gaps ──────────────────────────────────────
429
+
430
+ /**
431
+ * @typedef {Object} CoverageGaps
432
+ * @property {string[]} unusedPatterns
433
+ * @property {string[]} unusedComponents
434
+ * @property {string[]} missingPages
435
+ * @property {string[]} unimplementedTannins
436
+ */
437
+
438
+ /**
439
+ * Compare used vs available patterns, components, pages, tannins.
440
+ * @param {string} projectRoot
441
+ * @param {string[]} usedComponents
442
+ * @param {string[]} usedPatterns
443
+ * @param {string[]} allComponents
444
+ * @param {string[]} allPatterns
445
+ * @returns {Promise<CoverageGaps>}
446
+ */
447
+ async function analyzeCoverageGaps(projectRoot, usedComponents, usedPatterns, allComponents, allPatterns) {
448
+ const usedCompSet = new Set(usedComponents);
449
+ const usedPatSet = new Set(usedPatterns);
450
+
451
+ const unusedComponents = allComponents.filter(c => !usedCompSet.has(c));
452
+ const unusedPatterns = allPatterns.filter(p => !usedPatSet.has(p));
453
+
454
+ // Pages: compare essence structure vs src/pages/ files
455
+ const missingPages = [];
456
+ const essence = await readJSON(join(projectRoot, 'decantr.essence.json'));
457
+
458
+ if (essence) {
459
+ const declaredPages = [];
460
+ if (essence.structure && Array.isArray(essence.structure)) {
461
+ for (const page of essence.structure) {
462
+ if (page.id) declaredPages.push(page.id);
463
+ }
464
+ }
465
+ if (essence.sections && Array.isArray(essence.sections)) {
466
+ for (const section of essence.sections) {
467
+ if (section.structure && Array.isArray(section.structure)) {
468
+ for (const page of section.structure) {
469
+ if (page.id) declaredPages.push(`${section.id}/${page.id}`);
470
+ }
471
+ }
472
+ }
473
+ }
474
+
475
+ // Check which pages have corresponding files
476
+ const pagesDir = join(projectRoot, 'src', 'pages');
477
+ let pageFiles = [];
478
+ try {
479
+ pageFiles = await findJsFiles(pagesDir);
480
+ pageFiles = pageFiles.map(f => relative(pagesDir, f).replace(/\.js$/, '').replace(/\\/g, '/'));
481
+ } catch { /* no pages dir */ }
482
+
483
+ const pageFileSet = new Set(pageFiles);
484
+ for (const pageId of declaredPages) {
485
+ // Check for exact match or index file
486
+ if (!pageFileSet.has(pageId) && !pageFileSet.has(`${pageId}/index`)) {
487
+ missingPages.push(pageId);
488
+ }
489
+ }
490
+ }
491
+
492
+ // Tannins: compare declared vs implemented
493
+ const unimplementedTannins = [];
494
+ if (essence) {
495
+ const declaredTannins = [];
496
+ if (essence.tannins && Array.isArray(essence.tannins)) {
497
+ declaredTannins.push(...essence.tannins);
498
+ }
499
+ if (essence.sections && Array.isArray(essence.sections)) {
500
+ for (const section of essence.sections) {
501
+ if (section.tannins && Array.isArray(section.tannins)) {
502
+ declaredTannins.push(...section.tannins);
503
+ }
504
+ }
505
+ }
506
+
507
+ // Look for tannin implementations in src/
508
+ const srcDir = join(projectRoot, 'src');
509
+ const allSrcFiles = await findJsFiles(srcDir);
510
+ const allSrcContent = [];
511
+ for (const f of allSrcFiles) {
512
+ try {
513
+ const content = await readFile(f, 'utf-8');
514
+ allSrcContent.push({ file: f, content });
515
+ } catch { /* skip */ }
516
+ }
517
+
518
+ for (const tannin of declaredTannins) {
519
+ // Normalize: check for file name match or string reference
520
+ const tanninSlug = tannin.replace(/[^a-zA-Z0-9]/g, '-').toLowerCase();
521
+ const found = allSrcContent.some(({ file, content }) => {
522
+ const fileName = relative(srcDir, file).toLowerCase();
523
+ return fileName.includes(tanninSlug) || content.includes(tannin);
524
+ });
525
+ if (!found) {
526
+ unimplementedTannins.push(tannin);
527
+ }
528
+ }
529
+ }
530
+
531
+ return {
532
+ unusedPatterns,
533
+ unusedComponents,
534
+ missingPages,
535
+ unimplementedTannins
536
+ };
537
+ }
538
+
539
+ // ─── Pass 5: Bundle Metrics ─────────────────────────────────────
540
+
541
+ /**
542
+ * @typedef {Object} SizeMetrics
543
+ * @property {number} raw
544
+ * @property {number} gzip
545
+ * @property {number} brotli
546
+ * @property {string} rawFormatted
547
+ * @property {string} gzipFormatted
548
+ * @property {string} brotliFormatted
549
+ */
550
+
551
+ /**
552
+ * @typedef {Object} BundleReport
553
+ * @property {number} buildTimeMs
554
+ * @property {SizeMetrics} js
555
+ * @property {SizeMetrics} css
556
+ * @property {SizeMetrics} html
557
+ * @property {SizeMetrics} total
558
+ * @property {number} moduleCount
559
+ * @property {string|null} error
560
+ */
561
+
562
+ /**
563
+ * Build the project and measure output sizes.
564
+ * @param {string} projectRoot
565
+ * @returns {Promise<BundleReport>}
566
+ */
567
+ async function measureBundle(projectRoot) {
568
+ /** @type {BundleReport} */
569
+ const report = {
570
+ buildTimeMs: 0,
571
+ js: { raw: 0, gzip: 0, brotli: 0, rawFormatted: '0 B', gzipFormatted: '0 B', brotliFormatted: '0 B' },
572
+ css: { raw: 0, gzip: 0, brotli: 0, rawFormatted: '0 B', gzipFormatted: '0 B', brotliFormatted: '0 B' },
573
+ html: { raw: 0, gzip: 0, brotli: 0, rawFormatted: '0 B', gzipFormatted: '0 B', brotliFormatted: '0 B' },
574
+ total: { raw: 0, gzip: 0, brotli: 0, rawFormatted: '0 B', gzipFormatted: '0 B', brotliFormatted: '0 B' },
575
+ moduleCount: 0,
576
+ error: null
577
+ };
578
+
579
+ try {
580
+ const { build } = await import('./builder.js');
581
+
582
+ const t0 = performance.now();
583
+ await build(projectRoot, {
584
+ sourcemap: false,
585
+ analyze: false,
586
+ incremental: false
587
+ });
588
+ const t1 = performance.now();
589
+ report.buildTimeMs = Math.round(t1 - t0);
590
+
591
+ // Scan dist/ for file sizes
592
+ const distDir = join(projectRoot, 'dist');
593
+ const distFiles = await findAllFiles(distDir);
594
+
595
+ let jsRaw = 0, cssRaw = 0, htmlRaw = 0;
596
+ let jsContent = '', cssContent = '', htmlContent = '';
597
+
598
+ for (const file of distFiles) {
599
+ const ext = extname(file).toLowerCase();
600
+ try {
601
+ const content = await readFile(file, 'utf-8');
602
+ const size = Buffer.byteLength(content);
603
+ if (ext === '.js') {
604
+ jsRaw += size;
605
+ jsContent += content;
606
+ report.moduleCount++;
607
+ } else if (ext === '.css') {
608
+ cssRaw += size;
609
+ cssContent += content;
610
+ } else if (ext === '.html') {
611
+ htmlRaw += size;
612
+ htmlContent += content;
613
+ }
614
+ // Skip .map and other files
615
+ } catch { /* binary files — skip */ }
616
+ }
617
+
618
+ function buildSizeMetrics(content, raw) {
619
+ if (raw === 0 || !content) {
620
+ return { raw: 0, gzip: 0, brotli: 0, rawFormatted: '0 B', gzipFormatted: '0 B', brotliFormatted: '0 B' };
621
+ }
622
+ const gz = gzipSync(content).length;
623
+ const br = brotliSize(content);
624
+ return {
625
+ raw,
626
+ gzip: gz,
627
+ brotli: br,
628
+ rawFormatted: formatSize(raw),
629
+ gzipFormatted: formatSize(gz),
630
+ brotliFormatted: formatSize(br)
631
+ };
632
+ }
633
+
634
+ report.js = buildSizeMetrics(jsContent, jsRaw);
635
+ report.css = buildSizeMetrics(cssContent, cssRaw);
636
+ report.html = buildSizeMetrics(htmlContent, htmlRaw);
637
+
638
+ const totalRaw = jsRaw + cssRaw + htmlRaw;
639
+ const totalContent = jsContent + cssContent + htmlContent;
640
+ report.total = buildSizeMetrics(totalContent, totalRaw);
641
+
642
+ } catch (err) {
643
+ report.error = err.message || String(err);
644
+ }
645
+
646
+ return report;
647
+ }
648
+
649
+ // ─── Main Audit Function ─────────────────────────────────────────
650
+
651
+ /**
652
+ * Run all 5 audit passes against a project.
653
+ *
654
+ * @param {string} projectRoot - Absolute path to the project root
655
+ * @returns {Promise<{
656
+ * essence: { valid: boolean, terroir: string|null, stagesCompleted: string[], stagesSkipped: string[], errors: string[] },
657
+ * coverage: { frameworkDerivedPct: number, componentsUsed: string[], componentsTotal: number, patternsUsed: string[], patternsTotal: number, atomCalls: number, violations: number },
658
+ * quality: { hardcodedCSS: {file: string, line: number, value: string}[], missingAria: {file: string, line: number}[], leakedListeners: {file: string, line: number}[], missingFocusTrap: {file: string, line: number}[] },
659
+ * gaps: { unusedPatterns: string[], unusedComponents: string[], missingPages: string[], unimplementedTannins: string[] },
660
+ * bundle: { buildTimeMs: number, js: SizeMetrics, css: SizeMetrics, html: SizeMetrics, total: SizeMetrics, moduleCount: number, error: string|null }
661
+ * }>}
662
+ */
663
+ export async function audit(projectRoot) {
664
+ projectRoot = resolve(projectRoot);
665
+
666
+ // Load registry data
667
+ const [knownComponents, knownPatterns] = await Promise.all([
668
+ loadComponentNames(),
669
+ loadPatternNames()
670
+ ]);
671
+
672
+ // Run pass 1 (essence) and pass 2 (source) and pass 3 (quality) in parallel
673
+ const [essenceResult, sourceResult, qualityResult] = await Promise.all([
674
+ analyzeEssence(projectRoot),
675
+ analyzeSource(projectRoot, knownComponents, knownPatterns),
676
+ checkQuality(projectRoot)
677
+ ]);
678
+
679
+ // Pass 4 depends on pass 2 results
680
+ const gapsResult = await analyzeCoverageGaps(
681
+ projectRoot,
682
+ sourceResult.componentsUsed,
683
+ sourceResult.patternsUsed,
684
+ knownComponents,
685
+ knownPatterns
686
+ );
687
+
688
+ // Pass 5: bundle metrics (runs build, so do it last)
689
+ const bundleResult = await measureBundle(projectRoot);
690
+
691
+ return {
692
+ essence: essenceResult,
693
+ coverage: {
694
+ frameworkDerivedPct: sourceResult.frameworkDerivedPct,
695
+ componentsUsed: sourceResult.componentsUsed,
696
+ componentsTotal: sourceResult.componentsTotal,
697
+ patternsUsed: sourceResult.patternsUsed,
698
+ patternsTotal: sourceResult.patternsTotal,
699
+ atomCalls: sourceResult.atomCalls,
700
+ violations: sourceResult.inlineStyleViolations
701
+ },
702
+ quality: qualityResult,
703
+ gaps: gapsResult,
704
+ bundle: bundleResult
705
+ };
706
+ }