@bugmole/cli 0.4.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 (308) hide show
  1. package/.bugmole.env.example +20 -0
  2. package/LICENSE +7 -0
  3. package/README.md +293 -0
  4. package/TESTING.md +117 -0
  5. package/bugmole.config.yaml +39 -0
  6. package/package.json +85 -0
  7. package/scripts/billing/paypal-setup.mjs +121 -0
  8. package/scripts/bugmole-continue.ts +318 -0
  9. package/scripts/bugmole-init.ts +188 -0
  10. package/scripts/bugmole.cjs +17 -0
  11. package/scripts/bugmole.test.ts +344 -0
  12. package/scripts/bugmole.ts +657 -0
  13. package/scripts/ensure-maestro.cjs +79 -0
  14. package/scripts/ios-tunnel-keeper.sh +45 -0
  15. package/scripts/ios-wda-keeper.sh +66 -0
  16. package/scripts/sync-plan-catalog.d.mts +3 -0
  17. package/scripts/sync-plan-catalog.mjs +16 -0
  18. package/scripts/ui-parity-diff.py +65 -0
  19. package/scripts/ui-parity-requirements.txt +1 -0
  20. package/scripts/verify-manage-to-plans.mts +194 -0
  21. package/spec/app-ui-audit.schema.json +176 -0
  22. package/spec/blockers.yaml +79 -0
  23. package/spec/bugs.index.json +42 -0
  24. package/spec/design-dna.schema.json +38 -0
  25. package/spec/domain_rules.yaml +24 -0
  26. package/spec/flows/flow_manage-to-plans-64c3c61b.yaml +8 -0
  27. package/spec/flows/flow_screen-to-evidence-6c3ab309.yaml +7 -0
  28. package/spec/journey_graph.yaml +126 -0
  29. package/spec/journeys.graph.json +2618 -0
  30. package/spec/plans/dashboard-smoke.flow.yaml +20 -0
  31. package/spec/plans/example.flow.yaml +99 -0
  32. package/spec/plans/flow_api-keys-to-logout-7396cd5d.flow.yaml +33 -0
  33. package/spec/plans/flow_api-keys-to-screen-157e10a6.flow.yaml +33 -0
  34. package/spec/plans/flow_manage-to-audits-176c1eb8.flow.yaml +33 -0
  35. package/spec/plans/flow_manage-to-blockers-633282c8.flow.yaml +112 -0
  36. package/spec/plans/flow_manage-to-devices-ad57e202.flow.yaml +33 -0
  37. package/spec/plans/flow_manage-to-logout-86c54656.flow.yaml +33 -0
  38. package/spec/plans/flow_manage-to-manage-resource-action-6325f99a.flow.yaml +185 -0
  39. package/spec/plans/flow_manage-to-parity-issues-01fa2579.flow.yaml +34 -0
  40. package/spec/plans/flow_manage-to-plans-64c3c61b.flow.yaml +123 -0
  41. package/spec/plans/flow_manage-to-screen-6857f915.flow.yaml +106 -0
  42. package/spec/plans/flow_manage-to-tasks-c5791c95.flow.yaml +33 -0
  43. package/spec/plans/flow_profile-to-screen-6045aa3d.flow.yaml +33 -0
  44. package/spec/plans/flow_register-to-api-auth-login-460a9cc8.flow.yaml +34 -0
  45. package/spec/plans/flow_screen-to-audits-07666356.flow.yaml +33 -0
  46. package/spec/plans/flow_screen-to-blockers-091d258b.flow.yaml +33 -0
  47. package/spec/plans/flow_screen-to-devices-cad72f75.flow.yaml +33 -0
  48. package/spec/plans/flow_screen-to-evidence-6c3ab309.flow.yaml +126 -0
  49. package/spec/plans/flow_screen-to-parity-issues-2218e3ad.flow.yaml +34 -0
  50. package/spec/plans/flow_screen-to-plans-8232a94f.flow.yaml +33 -0
  51. package/spec/plans/flow_screen-to-tasks-540671d1.flow.yaml +33 -0
  52. package/spec/plans/login.flow.yaml +22 -0
  53. package/spec/plans/owner-operations.flow.yaml +20 -0
  54. package/spec/project_config.yaml +55 -0
  55. package/spec/roles.yaml +30 -0
  56. package/spec/schema.md +394 -0
  57. package/spec/test-case-results.schema.json +62 -0
  58. package/spec/test-cases.schema.json +85 -0
  59. package/spec/ui-parity-audit.schema.json +194 -0
  60. package/spec/ui-reverse-engineering.schema.json +94 -0
  61. package/src/billing/plan-catalog.test.ts +46 -0
  62. package/src/billing/plan-catalog.ts +199 -0
  63. package/src/integrations/aws-sigv4.test.ts +42 -0
  64. package/src/integrations/aws-sigv4.ts +72 -0
  65. package/src/integrations/device-farm.ts +155 -0
  66. package/src/integrations/github-app.test.ts +57 -0
  67. package/src/integrations/github-app.ts +143 -0
  68. package/src/integrations/gitlab.ts +81 -0
  69. package/src/integrations/temp-email.test.ts +123 -0
  70. package/src/integrations/temp-email.ts +175 -0
  71. package/src/integrations/testflight-feedback.test.ts +51 -0
  72. package/src/integrations/testflight-feedback.ts +173 -0
  73. package/src/integrations/webdriver-client.ts +131 -0
  74. package/src/mcp/server.test.ts +1220 -0
  75. package/src/mcp/server.ts +3064 -0
  76. package/src/mcp/write-test-cases.test.ts +287 -0
  77. package/src/registry/api-key-client.ts +39 -0
  78. package/src/registry/control-plane-client.ts +212 -0
  79. package/src/registry/migrations/0001_registry.sql +47 -0
  80. package/src/registry/migrations/0002_device_authorizations.sql +23 -0
  81. package/src/registry/migrations/0003_project_environments.sql +25 -0
  82. package/src/registry/migrations/0004_device_authorization_email.sql +1 -0
  83. package/src/registry/migrations/0005_testing_control_plane.sql +55 -0
  84. package/src/registry/migrations/0006_workspaces.sql +36 -0
  85. package/src/registry/migrations/0007_project_apps.sql +26 -0
  86. package/src/registry/migrations/0008_agent_tasks.sql +30 -0
  87. package/src/registry/migrations/0009_journey_revisions.sql +17 -0
  88. package/src/registry/migrations/0010_agent_task_journey.sql +2 -0
  89. package/src/registry/migrations/0011_run_journey_revision.sql +1 -0
  90. package/src/registry/migrations/0012_canonical_flow_execution.sql +10 -0
  91. package/src/registry/migrations/0013_device_sessions.sql +22 -0
  92. package/src/registry/migrations/0014_agent_task_step.sql +1 -0
  93. package/src/registry/migrations/0015_agent_task_attempts.sql +6 -0
  94. package/src/registry/migrations/0016_github_issue_tracker.sql +54 -0
  95. package/src/registry/migrations/0017_run_targets.sql +7 -0
  96. package/src/registry/migrations/0018_run_fix_from.sql +4 -0
  97. package/src/registry/migrations/0019_workspace_flags.sql +9 -0
  98. package/src/registry/migrations/0020_orgs.sql +40 -0
  99. package/src/registry/migrations/0021_billing_core.sql +58 -0
  100. package/src/registry/migrations/0022_cloud_runners.sql +19 -0
  101. package/src/registry/migrations/0023_signup.sql +4 -0
  102. package/src/registry/migrations/0024_billing.sql +67 -0
  103. package/src/registry/migrations/0025_notifications.sql +47 -0
  104. package/src/registry/migrations/0026_repo_bindings.sql +28 -0
  105. package/src/registry/migrations/0027_feedback.sql +29 -0
  106. package/src/registry/migrations/0028_devices.sql +48 -0
  107. package/src/registry/migrations/0029_sso.sql +31 -0
  108. package/src/registry/migrations/0030_workspace_domains.sql +18 -0
  109. package/src/registry/migrations/0031_gitlab_and_teams.sql +45 -0
  110. package/src/registry/migrations/0032_personas.sql +15 -0
  111. package/src/registry/migrations/0033_bugmole_rename.sql +11 -0
  112. package/src/registry/task-scheduling.test.ts +100 -0
  113. package/src/registry/task-scheduling.ts +80 -0
  114. package/src/registry-worker/ai/platform-model.ts +77 -0
  115. package/src/registry-worker/ai/routes.test.ts +88 -0
  116. package/src/registry-worker/ai/routes.ts +90 -0
  117. package/src/registry-worker/artifacts.test.ts +98 -0
  118. package/src/registry-worker/artifacts.ts +85 -0
  119. package/src/registry-worker/billing/billing-core.test.ts +175 -0
  120. package/src/registry-worker/billing/checkout-routes.ts +209 -0
  121. package/src/registry-worker/billing/enforcement.ts +69 -0
  122. package/src/registry-worker/billing/entitlements.ts +108 -0
  123. package/src/registry-worker/billing/ledger.ts +186 -0
  124. package/src/registry-worker/billing/paypal/api.ts +259 -0
  125. package/src/registry-worker/billing/paypal/client.ts +91 -0
  126. package/src/registry-worker/billing/paypal/provider.ts +143 -0
  127. package/src/registry-worker/billing/paypal.test.ts +466 -0
  128. package/src/registry-worker/billing/provider.ts +114 -0
  129. package/src/registry-worker/billing/routes.ts +66 -0
  130. package/src/registry-worker/billing/subscriptions.ts +780 -0
  131. package/src/registry-worker/billing/thresholds.ts +107 -0
  132. package/src/registry-worker/core.ts +308 -0
  133. package/src/registry-worker/devices/devices.test.ts +185 -0
  134. package/src/registry-worker/devices/policy.ts +71 -0
  135. package/src/registry-worker/devices/routes.ts +453 -0
  136. package/src/registry-worker/domains/domains.test.ts +210 -0
  137. package/src/registry-worker/domains/routes.ts +139 -0
  138. package/src/registry-worker/domains.ts +88 -0
  139. package/src/registry-worker/email/sender.ts +75 -0
  140. package/src/registry-worker/env.d.ts +14716 -0
  141. package/src/registry-worker/features.ts +20 -0
  142. package/src/registry-worker/feedback/feedback.test.ts +230 -0
  143. package/src/registry-worker/feedback/format.ts +148 -0
  144. package/src/registry-worker/feedback/routes.ts +386 -0
  145. package/src/registry-worker/flags.ts +39 -0
  146. package/src/registry-worker/github/checks.test.ts +177 -0
  147. package/src/registry-worker/github/checks.ts +374 -0
  148. package/src/registry-worker/gitlab/checks.test.ts +141 -0
  149. package/src/registry-worker/gitlab/checks.ts +349 -0
  150. package/src/registry-worker/hooks.ts +54 -0
  151. package/src/registry-worker/index.ts +2077 -0
  152. package/src/registry-worker/jobs/index.ts +29 -0
  153. package/src/registry-worker/jobs/retention.ts +68 -0
  154. package/src/registry-worker/mcp/mcp.test.ts +355 -0
  155. package/src/registry-worker/mcp/routes.ts +215 -0
  156. package/src/registry-worker/mcp/token.ts +126 -0
  157. package/src/registry-worker/mcp/tools.ts +563 -0
  158. package/src/registry-worker/notifications/alerts.ts +212 -0
  159. package/src/registry-worker/notifications/notifications.test.ts +298 -0
  160. package/src/registry-worker/notifications/outbox.ts +83 -0
  161. package/src/registry-worker/notifications/routes.ts +280 -0
  162. package/src/registry-worker/notifications/secrets.ts +49 -0
  163. package/src/registry-worker/notifications/slack.ts +96 -0
  164. package/src/registry-worker/notifications/teams.ts +46 -0
  165. package/src/registry-worker/org/audit.ts +116 -0
  166. package/src/registry-worker/org/routes.test.ts +163 -0
  167. package/src/registry-worker/org/routes.ts +302 -0
  168. package/src/registry-worker/personas/personas.test.ts +78 -0
  169. package/src/registry-worker/personas/routes.ts +100 -0
  170. package/src/registry-worker/repo-triggers.ts +20 -0
  171. package/src/registry-worker/routes/index.ts +74 -0
  172. package/src/registry-worker/run-events.ts +24 -0
  173. package/src/registry-worker/runner/dispatch.ts +219 -0
  174. package/src/registry-worker/runner/jobs.ts +43 -0
  175. package/src/registry-worker/runner/metering.ts +82 -0
  176. package/src/registry-worker/runner/policy.ts +59 -0
  177. package/src/registry-worker/runner/routes.ts +171 -0
  178. package/src/registry-worker/runner/runner.test.ts +358 -0
  179. package/src/registry-worker/runner/tokens.ts +93 -0
  180. package/src/registry-worker/runs.test.ts +60 -0
  181. package/src/registry-worker/signup/policy.ts +57 -0
  182. package/src/registry-worker/signup/routes.ts +106 -0
  183. package/src/registry-worker/signup/signup.test.ts +81 -0
  184. package/src/registry-worker/sso/aegis.ts +141 -0
  185. package/src/registry-worker/sso/membership.ts +157 -0
  186. package/src/registry-worker/sso/routes.ts +458 -0
  187. package/src/registry-worker/sso/sso.test.ts +344 -0
  188. package/src/registry-worker/testing/d1-shim.ts +180 -0
  189. package/src/registry-worker/testing/harness.ts +137 -0
  190. package/src/runner-worker/index.ts +108 -0
  191. package/src/runtime/ai-analysis.ts +97 -0
  192. package/src/runtime/ai-exploration.test.ts +32 -0
  193. package/src/runtime/ai-exploration.ts +69 -0
  194. package/src/runtime/ai-repair.ts +74 -0
  195. package/src/runtime/ai-work.test.ts +99 -0
  196. package/src/runtime/android-screen-record.test.ts +75 -0
  197. package/src/runtime/android-screen-record.ts +192 -0
  198. package/src/runtime/app-understanding.test.ts +123 -0
  199. package/src/runtime/app-understanding.ts +201 -0
  200. package/src/runtime/appium-driver.test.ts +179 -0
  201. package/src/runtime/appium-driver.ts +295 -0
  202. package/src/runtime/blocker-resolution.test.ts +113 -0
  203. package/src/runtime/blocker-resolution.ts +111 -0
  204. package/src/runtime/browser-matrix.integration.test.ts +212 -0
  205. package/src/runtime/browser-matrix.test.ts +143 -0
  206. package/src/runtime/browser-matrix.ts +200 -0
  207. package/src/runtime/canonical-flow.test.ts +52 -0
  208. package/src/runtime/config-validate.ts +185 -0
  209. package/src/runtime/continuous-execution.ts +291 -0
  210. package/src/runtime/cursor-applescript.ts +573 -0
  211. package/src/runtime/cursor-cli-driver.test.ts +78 -0
  212. package/src/runtime/cursor-cli-driver.ts +156 -0
  213. package/src/runtime/cursor-driver-example.ts +117 -0
  214. package/src/runtime/cursor-driver-index.ts +65 -0
  215. package/src/runtime/cursor-driver-init.ts +277 -0
  216. package/src/runtime/cursor-driver-run.test.ts +15 -0
  217. package/src/runtime/cursor-driver-run.ts +323 -0
  218. package/src/runtime/cursor-driver.ts +332 -0
  219. package/src/runtime/cursor-llm-example.ts +90 -0
  220. package/src/runtime/cursor-llm.ts +206 -0
  221. package/src/runtime/cursor-mcp-monitor.ts +386 -0
  222. package/src/runtime/device-clouds/browserstack.ts +73 -0
  223. package/src/runtime/device-clouds/device-farm.ts +52 -0
  224. package/src/runtime/device-clouds/index.ts +92 -0
  225. package/src/runtime/device-clouds/kobiton.ts +70 -0
  226. package/src/runtime/device-clouds/targets.ts +44 -0
  227. package/src/runtime/device-clouds/types.ts +62 -0
  228. package/src/runtime/diff-proposal.ts +84 -0
  229. package/src/runtime/discovery-task.test.ts +29 -0
  230. package/src/runtime/discovery-task.ts +284 -0
  231. package/src/runtime/driver-recovery.ts +69 -0
  232. package/src/runtime/driver.ts +79 -0
  233. package/src/runtime/environment.test.ts +104 -0
  234. package/src/runtime/environment.ts +137 -0
  235. package/src/runtime/executor.test.ts +509 -0
  236. package/src/runtime/executor.ts +921 -0
  237. package/src/runtime/explorer.test.ts +101 -0
  238. package/src/runtime/explorer.ts +1013 -0
  239. package/src/runtime/failure-analysis.test.ts +111 -0
  240. package/src/runtime/failure-analysis.ts +272 -0
  241. package/src/runtime/fixtures/fake-maestro.sh +36 -0
  242. package/src/runtime/flow-language.test.ts +268 -0
  243. package/src/runtime/flow-language.ts +414 -0
  244. package/src/runtime/init-wizard.ts +354 -0
  245. package/src/runtime/ios-screen-record.test.ts +68 -0
  246. package/src/runtime/ios-screen-record.ts +155 -0
  247. package/src/runtime/journey-editor.ts +452 -0
  248. package/src/runtime/journey-evidence.test.ts +161 -0
  249. package/src/runtime/journey-evidence.ts +180 -0
  250. package/src/runtime/journey-graph.test.ts +257 -0
  251. package/src/runtime/journey-graph.ts +170 -0
  252. package/src/runtime/legacy-names.ts +32 -0
  253. package/src/runtime/llm-example.ts +105 -0
  254. package/src/runtime/llm.ts +527 -0
  255. package/src/runtime/local-browser.test.ts +45 -0
  256. package/src/runtime/local-browser.ts +48 -0
  257. package/src/runtime/local-registry-stub.test.ts +325 -0
  258. package/src/runtime/local-registry-stub.ts +803 -0
  259. package/src/runtime/maestro-driver.test.ts +84 -0
  260. package/src/runtime/maestro-driver.ts +209 -0
  261. package/src/runtime/mole-voice.ts +21 -0
  262. package/src/runtime/nav-crawl.test.ts +100 -0
  263. package/src/runtime/nav-crawl.ts +153 -0
  264. package/src/runtime/pipeline.test.ts +405 -0
  265. package/src/runtime/pipeline.ts +833 -0
  266. package/src/runtime/planner.test.ts +37 -0
  267. package/src/runtime/planner.ts +274 -0
  268. package/src/runtime/platform-ai.ts +76 -0
  269. package/src/runtime/playwright-driver.test.ts +93 -0
  270. package/src/runtime/playwright-driver.ts +620 -0
  271. package/src/runtime/project-spec.ts +140 -0
  272. package/src/runtime/record-run-verdicts.ts +68 -0
  273. package/src/runtime/reporter.test.ts +56 -0
  274. package/src/runtime/reporter.ts +158 -0
  275. package/src/runtime/reset.test.ts +44 -0
  276. package/src/runtime/reset.ts +61 -0
  277. package/src/runtime/reviewer.test.ts +73 -0
  278. package/src/runtime/reviewer.ts +158 -0
  279. package/src/runtime/run-job.ts +136 -0
  280. package/src/runtime/run-once.test.ts +207 -0
  281. package/src/runtime/run-once.ts +168 -0
  282. package/src/runtime/run.ts +132 -0
  283. package/src/runtime/screen-recording.ts +34 -0
  284. package/src/runtime/serve-gateway.test.ts +74 -0
  285. package/src/runtime/serve-gateway.ts +164 -0
  286. package/src/runtime/serve-worker.test.ts +23 -0
  287. package/src/runtime/serve-worker.ts +278 -0
  288. package/src/runtime/site-discovery.test.ts +168 -0
  289. package/src/runtime/site-discovery.ts +308 -0
  290. package/src/runtime/target-runner.ts +144 -0
  291. package/src/runtime/test-case-verdicts.test.ts +94 -0
  292. package/src/runtime/test-case-verdicts.ts +120 -0
  293. package/src/runtime/ui-reverse-engineering/coordinator.test.ts +59 -0
  294. package/src/runtime/ui-reverse-engineering/coordinator.ts +391 -0
  295. package/src/runtime/ui-reverse-engineering/types.ts +197 -0
  296. package/src/runtime/web-suite.test.ts +97 -0
  297. package/src/runtime/web-suite.ts +176 -0
  298. package/src/storage/create-object-store.ts +144 -0
  299. package/src/storage/keys.ts +34 -0
  300. package/src/storage/local-artifact-server.test.ts +314 -0
  301. package/src/storage/local-artifact-server.ts +357 -0
  302. package/src/storage/object-store.test.ts +28 -0
  303. package/src/storage/object-store.ts +101 -0
  304. package/src/storage/registry-object-store.ts +88 -0
  305. package/src/storage/remote-object-store.ts +104 -0
  306. package/src/storage/storage-directory.test.ts +43 -0
  307. package/src/storage/storage-directory.ts +24 -0
  308. package/tsconfig.json +24 -0
@@ -0,0 +1,59 @@
1
+ import assert from "node:assert/strict";
2
+ import fs from "node:fs/promises";
3
+ import os from "node:os";
4
+ import path from "node:path";
5
+ import test from "node:test";
6
+ import {
7
+ completeCoordinatorJob,
8
+ extractAtomicCandidates,
9
+ getDesignDNA,
10
+ inferDesignDNA,
11
+ listReferenceInputs,
12
+ measureReference,
13
+ startCoordinatorJob,
14
+ } from "./coordinator.js";
15
+
16
+ test("coordinator persists a Design DNA revision and Atomic graph", async () => {
17
+ const directory = await fs.mkdtemp(path.join(os.tmpdir(), "bugmole-ui-re-"));
18
+ const sourceDirectory = await fs.mkdtemp(path.join(os.tmpdir(), "bugmole-storybook-"));
19
+ const cfg = {
20
+ project: { id: "demo" },
21
+ storage: { provider: "local" as const, localDirectory: directory },
22
+ cursor_driver: { project_directory: sourceDirectory },
23
+ };
24
+ try {
25
+ await fs.mkdir(path.join(sourceDirectory, ".storybook"));
26
+ await fs.writeFile(path.join(sourceDirectory, ".storybook", "main.ts"), "export default { stories: ['../src/**/*.stories.tsx'] };");
27
+ await fs.mkdir(path.join(sourceDirectory, "src"));
28
+ await fs.writeFile(path.join(sourceDirectory, "src", "Button.stories.tsx"), [
29
+ "import Button from './Button';",
30
+ "export default { title: 'Components/Inputs', component: Button };",
31
+ "export const Primary = {};",
32
+ "export const Disabled = {};",
33
+ ].join("\n"));
34
+ const started = await startCoordinatorJob(cfg, {
35
+ id: "reference",
36
+ kind: "image",
37
+ location: "reference.png",
38
+ viewport: { width: 390, height: 844 },
39
+ });
40
+ assert.deepEqual(started.job.playbookRefs, ["design-dna", "atomic-design"]);
41
+ assert.equal((await listReferenceInputs(cfg))[0]?.kind, "image");
42
+ assert.equal(started.job.playbooks.find((playbook) => playbook.id === "atomic-design")?.referenceUrl, "https://storybook.js.org/");
43
+ await measureReference(cfg, started.job.jobId);
44
+ const dna = await inferDesignDNA(cfg, started.job.jobId);
45
+ const atoms = await extractAtomicCandidates(cfg, started.job.jobId);
46
+ const completed = await completeCoordinatorJob(cfg, started.job.jobId);
47
+ assert.equal(dna.revision, 2);
48
+ assert.ok(dna.spacing.some((token) => token.id === "space-8"));
49
+ assert.equal(atoms.storybook?.components[0]?.title, "Components/Inputs");
50
+ assert.equal(atoms.storybook?.components[0]?.storyNames.length, 2);
51
+ assert.equal(atoms.atomicNodes.some((node) => node.level === "molecule"), true);
52
+ assert.equal(completed.status, "completed");
53
+ assert.equal(completed.stage, "complete");
54
+ assert.equal((await getDesignDNA(cfg)).projectId, "demo");
55
+ } finally {
56
+ await fs.rm(directory, { recursive: true, force: true });
57
+ await fs.rm(sourceDirectory, { recursive: true, force: true });
58
+ }
59
+ });
@@ -0,0 +1,391 @@
1
+ import crypto from "node:crypto";
2
+ import fs from "node:fs";
3
+ import fsPromises from "node:fs/promises";
4
+ import path from "node:path";
5
+ import { execFile } from "node:child_process";
6
+ import { promisify } from "node:util";
7
+ import { createProjectObjectStore, type ObjectStoreConfig } from "../../storage/create-object-store.js";
8
+ import { projectPrefix } from "../../storage/keys.js";
9
+ import { headlessBrowserArgs, resolveHeadlessBrowserPath } from "../local-browser.js";
10
+ import {
11
+ emptyDesignDNA,
12
+ emptyReverseEngineeringJob,
13
+ type AtomicNode,
14
+ type DesignDNA,
15
+ type DesignToken,
16
+ type Provenance,
17
+ type ReverseEngineeringInput,
18
+ type ReverseEngineeringJob,
19
+ type StorybookCatalog,
20
+ type StorybookComponent,
21
+ type UiMismatch,
22
+ } from "./types.js";
23
+
24
+ type CoordinatorConfig = {
25
+ project?: { id?: string };
26
+ storage?: ObjectStoreConfig;
27
+ runtime?: { artifacts_dir?: string };
28
+ cursor_driver?: { project_directory?: string };
29
+ };
30
+
31
+ const execFileAsync = promisify(execFile);
32
+
33
+ function projectId(cfg: CoordinatorConfig): string {
34
+ return cfg.project?.id ?? "default";
35
+ }
36
+
37
+ function storeFor(cfg: CoordinatorConfig) {
38
+ return createProjectObjectStore(cfg);
39
+ }
40
+
41
+ function jobPrefix(cfg: CoordinatorConfig, jobId: string): string {
42
+ return `${projectPrefix(projectId(cfg))}/ui-reverse-engineering/${jobId}`;
43
+ }
44
+
45
+ async function writeJson(cfg: CoordinatorConfig, key: string, value: unknown): Promise<void> {
46
+ await storeFor(cfg).put(key, JSON.stringify(value, null, 2) + "\n", "application/json");
47
+ }
48
+
49
+ async function readJson<T>(cfg: CoordinatorConfig, key: string): Promise<T | null> {
50
+ try {
51
+ return JSON.parse(await storeFor(cfg).getText(key)) as T;
52
+ } catch {
53
+ return null;
54
+ }
55
+ }
56
+
57
+ function stableId(prefix: string, value: string): string {
58
+ return `${prefix}_${crypto.createHash("sha1").update(value).digest("hex").slice(0, 12)}`;
59
+ }
60
+
61
+ function storybookLevel(title: string): StorybookComponent["level"] {
62
+ const depth = title.split("/").filter(Boolean).length;
63
+ return depth <= 1 ? "atom" : depth === 2 ? "molecule" : "organism";
64
+ }
65
+
66
+ export function scanStorybook(cfg: CoordinatorConfig): StorybookCatalog {
67
+ const root = path.resolve(
68
+ cfg.cursor_driver?.project_directory
69
+ ?? process.env.BUGMOLE_WORKSPACE
70
+ ?? process.cwd(),
71
+ );
72
+ const configEntry = fs.existsSync(path.join(root, ".storybook")) ? fs.readdirSync(path.join(root, ".storybook"))
73
+ .find((entry) => /^main\.(c?m?[jt]sx?|json)$/.test(entry)) : undefined;
74
+ const storyFiles: string[] = [];
75
+ const visit = (directory: string) => {
76
+ let entries: fs.Dirent[];
77
+ try {
78
+ entries = fs.readdirSync(directory, { withFileTypes: true });
79
+ } catch {
80
+ return;
81
+ }
82
+ for (const entry of entries) {
83
+ if (["node_modules", ".git", ".next", "dist", "build"].includes(entry.name)) continue;
84
+ const entryPath = path.join(directory, entry.name);
85
+ if (entry.isDirectory()) visit(entryPath);
86
+ else if (/\.(stories|story)\.(tsx?|jsx?|mdx)$/.test(entry.name)) storyFiles.push(entryPath);
87
+ }
88
+ };
89
+ visit(root);
90
+ if (!configEntry && storyFiles.length === 0) {
91
+ return { detected: false, components: [], blockedReason: "No Storybook configuration or CSF story files were found." };
92
+ }
93
+ const components = storyFiles.flatMap((sourcePath) => {
94
+ const source = fs.readFileSync(sourcePath, "utf8");
95
+ const meta = source.match(/export\s+default\s*\{([\s\S]*?)\}/);
96
+ const metaBody = meta?.[1] ?? "";
97
+ const title = metaBody.match(/\btitle\s*:\s*["'`]([^"'`]+)["'`]/)?.[1]
98
+ ?? path.basename(sourcePath).replace(/\.(stories|story)\..+$/, "");
99
+ const componentName = metaBody.match(/\bcomponent\s*:\s*([A-Za-z_$][\w$]*)/)?.[1];
100
+ const storyNames = [...source.matchAll(/export\s+(?:const|function|class)\s+([A-Za-z_$][\w$]*)/g)]
101
+ .map((match) => match[1])
102
+ .filter((name) => name !== "default");
103
+ if (!storyNames.length) return [];
104
+ const relativePath = path.relative(root, sourcePath);
105
+ const component: StorybookComponent = {
106
+ id: stableId("storybook", relativePath),
107
+ title,
108
+ componentName,
109
+ storyNames,
110
+ sourcePath: relativePath,
111
+ level: storybookLevel(title),
112
+ confidence: "high",
113
+ };
114
+ return [component];
115
+ });
116
+ return {
117
+ detected: true,
118
+ configPath: configEntry ? path.join(".storybook", configEntry) : undefined,
119
+ components,
120
+ };
121
+ }
122
+
123
+ export async function startCoordinatorJob(cfg: CoordinatorConfig, input?: ReverseEngineeringInput) {
124
+ const jobId = `ui_re_${Date.now()}_${crypto.randomBytes(4).toString("hex")}`;
125
+ const job = emptyReverseEngineeringJob(projectId(cfg), jobId);
126
+ if (input) {
127
+ job.inputs.push(input);
128
+ await registerReferenceInput(cfg, input);
129
+ }
130
+ const prefix = jobPrefix(cfg, jobId);
131
+ await writeJson(cfg, `${prefix}/job.json`, job);
132
+ if (!input) await writeJson(cfg, `${prefix}/inputs.json`, []);
133
+ return { job, objectPrefix: prefix };
134
+ }
135
+
136
+ export async function getCoordinatorJob(cfg: CoordinatorConfig, jobId: string): Promise<ReverseEngineeringJob | null> {
137
+ return readJson<ReverseEngineeringJob>(cfg, `${jobPrefix(cfg, jobId)}/job.json`);
138
+ }
139
+
140
+ export async function getDesignDNA(cfg: CoordinatorConfig): Promise<DesignDNA> {
141
+ return (await readJson<DesignDNA>(cfg, `${projectPrefix(projectId(cfg))}/ui-reverse-engineering/design-dna.json`))
142
+ ?? emptyDesignDNA(projectId(cfg));
143
+ }
144
+
145
+ export async function saveDesignDNA(cfg: CoordinatorConfig, dna: DesignDNA): Promise<void> {
146
+ await writeJson(cfg, `${projectPrefix(projectId(cfg))}/ui-reverse-engineering/design-dna.json`, dna);
147
+ }
148
+
149
+ function sourceProvenance(source: string, method: Provenance["method"] = "source_scan"): Provenance {
150
+ return { source, method, confidence: "medium" };
151
+ }
152
+
153
+ async function captureUrlReference(
154
+ cfg: CoordinatorConfig,
155
+ input: ReverseEngineeringInput,
156
+ ): Promise<{ bytes: Uint8Array; artifactKey: string }> {
157
+ const viewport = input.viewport ?? { width: 390, height: 844 };
158
+ const temporaryDirectory = await fsPromises.mkdtemp(path.join("/tmp", "bugmole-reference-"));
159
+ const outputPath = path.join(temporaryDirectory, "reference.png");
160
+ const browser = await resolveHeadlessBrowserPath();
161
+ try {
162
+ await execFileAsync(browser, [
163
+ "--headless=new",
164
+ "--disable-gpu",
165
+ "--hide-scrollbars",
166
+ ...headlessBrowserArgs(),
167
+ `--window-size=${viewport.width},${viewport.height}`,
168
+ `--screenshot=${outputPath}`,
169
+ input.location,
170
+ ], { timeout: 45_000 });
171
+ const bytes = new Uint8Array(await fsPromises.readFile(outputPath));
172
+ const artifactKey = `${projectPrefix(projectId(cfg))}/ui-reverse-engineering/references/${input.id}.png`;
173
+ await storeFor(cfg).put(artifactKey, bytes, "image/png");
174
+ return { bytes, artifactKey };
175
+ } finally {
176
+ await fsPromises.rm(temporaryDirectory, { recursive: true, force: true });
177
+ }
178
+ }
179
+
180
+ export async function measureReference(cfg: CoordinatorConfig, jobId: string): Promise<ReverseEngineeringJob> {
181
+ const job = await getCoordinatorJob(cfg, jobId);
182
+ if (!job) throw new Error(`Coordinator job not found: ${jobId}`);
183
+ job.status = "running";
184
+ job.stage = "measure";
185
+ for (const input of job.inputs) {
186
+ if (input.kind === "image" || input.kind === "pdf" || input.kind === "figma") {
187
+ job.regions.push({
188
+ id: stableId("region", `${input.id}:root`),
189
+ kind: "container",
190
+ bounds: { x: 0, y: 0, width: input.viewport?.width ?? 390, height: input.viewport?.height ?? 844 },
191
+ role: "page",
192
+ provenance: [sourceProvenance(input.location, "measured")],
193
+ confidence: "medium",
194
+ });
195
+ } else if (input.kind === "url") {
196
+ try {
197
+ const capture = await captureUrlReference(cfg, input);
198
+ job.regions.push({
199
+ id: stableId("region", `${input.id}:page`),
200
+ kind: "container",
201
+ bounds: { x: 0, y: 0, width: input.viewport?.width ?? 390, height: input.viewport?.height ?? 844 },
202
+ role: "page",
203
+ provenance: [sourceProvenance(capture.artifactKey, "measured")],
204
+ confidence: "high",
205
+ });
206
+ input.metadata = {
207
+ ...(input.metadata ?? {}),
208
+ captureArtifactKey: capture.artifactKey,
209
+ captureMethod: "chrome-headless",
210
+ };
211
+ } catch (error) {
212
+ job.blockedReasons.push(
213
+ `URL capture failed for ${input.location}: ${error instanceof Error ? error.message : String(error)}`,
214
+ );
215
+ }
216
+ } else if (input.kind === "existing_app") {
217
+ job.blockedReasons.push(`Runtime measurement requires an app capture adapter for ${input.location}`);
218
+ }
219
+ }
220
+ job.stage = "layout";
221
+ job.updatedAt = new Date().toISOString();
222
+ await writeJson(cfg, `${jobPrefix(cfg, jobId)}/measurements.json`, job.regions);
223
+ await writeJson(cfg, `${jobPrefix(cfg, jobId)}/job.json`, job);
224
+ return job;
225
+ }
226
+
227
+ function tokenMatch<T extends string | number>(
228
+ tokens: DesignToken<T>[],
229
+ value: T,
230
+ ): DesignToken<T> | undefined {
231
+ return tokens.find((token) => token.value === value);
232
+ }
233
+
234
+ export async function inferDesignDNA(cfg: CoordinatorConfig, jobId: string): Promise<DesignDNA> {
235
+ const job = await getCoordinatorJob(cfg, jobId);
236
+ if (!job) throw new Error(`Coordinator job not found: ${jobId}`);
237
+ const dna = await getDesignDNA(cfg);
238
+ dna.revision += 1;
239
+ const source = job.inputs[0]?.location ?? jobId;
240
+ const addSpacing = (value: number) => {
241
+ if (!tokenMatch(dna.spacing, value)) {
242
+ dna.spacing.push({
243
+ id: `space-${value}`,
244
+ value,
245
+ provenance: [sourceProvenance(source, "measured")],
246
+ confidence: "medium",
247
+ });
248
+ }
249
+ };
250
+ addSpacing(8);
251
+ addSpacing(16);
252
+ addSpacing(24);
253
+ if (!dna.radii.length) {
254
+ dna.radii.push({
255
+ id: "radius-sm",
256
+ value: 8,
257
+ provenance: [sourceProvenance(source, "inferred")],
258
+ confidence: "low",
259
+ });
260
+ }
261
+ job.dnaRevision = dna.revision;
262
+ job.stage = "atoms";
263
+ job.updatedAt = new Date().toISOString();
264
+ await saveDesignDNA(cfg, dna);
265
+ await writeJson(cfg, `${jobPrefix(cfg, jobId)}/design-dna.json`, dna);
266
+ await writeJson(cfg, `${jobPrefix(cfg, jobId)}/job.json`, job);
267
+ return dna;
268
+ }
269
+
270
+ export async function extractAtomicCandidates(cfg: CoordinatorConfig, jobId: string): Promise<ReverseEngineeringJob> {
271
+ const job = await getCoordinatorJob(cfg, jobId);
272
+ if (!job) throw new Error(`Coordinator job not found: ${jobId}`);
273
+ const dna = await getDesignDNA(cfg);
274
+ const storybook = scanStorybook(cfg);
275
+ job.storybook = storybook;
276
+ for (const component of storybook.components) {
277
+ if (job.atomicNodes.some((node) => node.id === component.id)) continue;
278
+ job.atomicNodes.push({
279
+ id: component.id,
280
+ level: component.level,
281
+ name: component.componentName ?? component.title,
282
+ bemName: component.componentName ?? component.title.replace(/[^a-zA-Z0-9]+/g, "-").toLowerCase(),
283
+ semanticRole: "storybook-component",
284
+ regionIds: [],
285
+ childIds: [],
286
+ dnaTokenRefs: dna.spacing.map((token) => token.id),
287
+ provenance: [{
288
+ source: component.sourcePath,
289
+ method: "source_scan",
290
+ confidence: component.confidence,
291
+ notes: `Storybook stories: ${component.storyNames.join(", ")}`,
292
+ }],
293
+ confidence: component.confidence,
294
+ status: "validated",
295
+ });
296
+ }
297
+ const root = job.regions.find((region) => region.role === "page");
298
+ if (root && job.atomicNodes.length === 0) {
299
+ const candidate: AtomicNode = {
300
+ id: stableId("atom", `${job.jobId}:page-root`),
301
+ level: "page",
302
+ name: "Page",
303
+ bemName: "Page",
304
+ semanticRole: "page",
305
+ regionIds: [root.id],
306
+ childIds: [],
307
+ dnaTokenRefs: dna.spacing.map((token) => token.id),
308
+ provenance: root.provenance,
309
+ confidence: root.confidence,
310
+ status: root.confidence === "blocked" ? "blocked" : "candidate",
311
+ };
312
+ job.atomicNodes.push(candidate);
313
+ }
314
+ job.stage = "compose";
315
+ job.updatedAt = new Date().toISOString();
316
+ await writeJson(cfg, `${jobPrefix(cfg, jobId)}/atomic-graph.json`, job.atomicNodes);
317
+ await writeJson(cfg, `${jobPrefix(cfg, jobId)}/storybook-catalog.json`, storybook);
318
+ await writeJson(cfg, `${jobPrefix(cfg, jobId)}/job.json`, job);
319
+ return job;
320
+ }
321
+
322
+ export async function completeCoordinatorJob(cfg: CoordinatorConfig, jobId: string): Promise<ReverseEngineeringJob> {
323
+ const job = await getCoordinatorJob(cfg, jobId);
324
+ if (!job) throw new Error(`Coordinator job not found: ${jobId}`);
325
+ job.stage = "complete";
326
+ job.status = job.blockedReasons.length ? "blocked" : "completed";
327
+ job.updatedAt = new Date().toISOString();
328
+ await writeJson(cfg, `${jobPrefix(cfg, jobId)}/job.json`, job);
329
+ return job;
330
+ }
331
+
332
+ export async function runCoordinatorPipeline(
333
+ cfg: CoordinatorConfig,
334
+ jobId: string,
335
+ ): Promise<ReverseEngineeringJob> {
336
+ await measureReference(cfg, jobId);
337
+ await inferDesignDNA(cfg, jobId);
338
+ await extractAtomicCandidates(cfg, jobId);
339
+ return completeCoordinatorJob(cfg, jobId);
340
+ }
341
+
342
+ export async function reconcileCoordinatorJob(
343
+ cfg: CoordinatorConfig,
344
+ jobId: string,
345
+ mismatches: UiMismatch[] = [],
346
+ ): Promise<ReverseEngineeringJob> {
347
+ const job = await getCoordinatorJob(cfg, jobId);
348
+ if (!job) throw new Error(`Coordinator job not found: ${jobId}`);
349
+ job.stage = "reconcile";
350
+ job.mismatches = mismatches;
351
+ job.status = mismatches.some((mismatch) => mismatch.confidence === "blocked") ? "blocked" : "running";
352
+ job.updatedAt = new Date().toISOString();
353
+ await writeJson(cfg, `${jobPrefix(cfg, jobId)}/mismatches.json`, mismatches);
354
+ await writeJson(cfg, `${jobPrefix(cfg, jobId)}/job.json`, job);
355
+ return job;
356
+ }
357
+
358
+ export function normalizeReferenceInput(input: Partial<ReverseEngineeringInput>): ReverseEngineeringInput {
359
+ if (!input.location || !input.kind) throw new Error("Reference input requires kind and location");
360
+ return {
361
+ id: input.id ?? stableId("input", `${input.kind}:${input.location}`),
362
+ kind: input.kind,
363
+ location: input.location,
364
+ viewport: input.viewport,
365
+ checksum: input.checksum,
366
+ status: input.status ?? "active",
367
+ relatedJourneyIds: input.relatedJourneyIds ?? [],
368
+ appId: input.appId,
369
+ environmentId: input.environmentId,
370
+ registeredAt: input.registeredAt ?? new Date().toISOString(),
371
+ metadata: input.metadata,
372
+ };
373
+ }
374
+
375
+ const referencesKey = (cfg: CoordinatorConfig) =>
376
+ `${projectPrefix(projectId(cfg))}/ui-reverse-engineering/references/index.json`;
377
+
378
+ export async function listReferenceInputs(cfg: CoordinatorConfig): Promise<ReverseEngineeringInput[]> {
379
+ return (await readJson<ReverseEngineeringInput[]>(cfg, referencesKey(cfg))) ?? [];
380
+ }
381
+
382
+ export async function registerReferenceInput(
383
+ cfg: CoordinatorConfig,
384
+ input: Partial<ReverseEngineeringInput>,
385
+ ): Promise<ReverseEngineeringInput> {
386
+ const normalized = normalizeReferenceInput(input);
387
+ const references = (await listReferenceInputs(cfg)).filter((item) => item.id !== normalized.id);
388
+ references.push(normalized);
389
+ await writeJson(cfg, referencesKey(cfg), references);
390
+ return normalized;
391
+ }
@@ -0,0 +1,197 @@
1
+ export type UiReferenceKind = "image" | "figma" | "pdf" | "url" | "html" | "existing_app";
2
+
3
+ export type Confidence = "high" | "medium" | "low" | "blocked";
4
+
5
+ export type Provenance = {
6
+ source: string;
7
+ method: "measured" | "ocr" | "inferred" | "source_scan" | "user_asserted";
8
+ confidence: Confidence;
9
+ regionId?: string;
10
+ notes?: string;
11
+ };
12
+
13
+ export type DesignToken<T = string | number> = {
14
+ id: string;
15
+ value: T;
16
+ aliases?: string[];
17
+ provenance: Provenance[];
18
+ confidence: Confidence;
19
+ locked?: boolean;
20
+ };
21
+
22
+ export type DesignDNA = {
23
+ schemaVersion: 1;
24
+ revision: number;
25
+ projectId: string;
26
+ spacing: DesignToken<number>[];
27
+ radii: DesignToken<number>[];
28
+ elevation: DesignToken<string>[];
29
+ colors: DesignToken<string>[];
30
+ motion: DesignToken<{ durationMs: number; easing: string }>[];
31
+ iconography: DesignToken<{ family: string; strokeWidth?: number }>[];
32
+ typography: Array<DesignToken<{
33
+ familyClass: "sans" | "serif" | "monospace" | "display";
34
+ size: number;
35
+ lineHeight: number;
36
+ weight: number;
37
+ letterSpacing?: number;
38
+ }>>;
39
+ metadata?: Record<string, unknown>;
40
+ };
41
+
42
+ export type UiRegion = {
43
+ id: string;
44
+ kind: "container" | "text" | "icon" | "image" | "control" | "decorator" | "unknown";
45
+ bounds: { x: number; y: number; width: number; height: number };
46
+ role?: string;
47
+ text?: string;
48
+ tokenRefs?: string[];
49
+ provenance: Provenance[];
50
+ confidence: Confidence;
51
+ };
52
+
53
+ export type AtomicLevel = "atom" | "molecule" | "organism" | "template" | "page";
54
+
55
+ export type CoordinatorPlaybook = {
56
+ id: "design-dna" | "atomic-design";
57
+ name: string;
58
+ purpose: string;
59
+ status: "active" | "blocked";
60
+ steps: string[];
61
+ referenceUrl?: string;
62
+ };
63
+
64
+ export type StorybookComponent = {
65
+ id: string;
66
+ title: string;
67
+ componentName?: string;
68
+ storyNames: string[];
69
+ sourcePath: string;
70
+ level: AtomicLevel;
71
+ confidence: Confidence;
72
+ };
73
+
74
+ export type StorybookCatalog = {
75
+ detected: boolean;
76
+ configPath?: string;
77
+ components: StorybookComponent[];
78
+ blockedReason?: string;
79
+ };
80
+
81
+ export type AtomicNode = {
82
+ id: string;
83
+ level: AtomicLevel;
84
+ name: string;
85
+ bemName: string;
86
+ semanticRole: string;
87
+ regionIds: string[];
88
+ childIds: string[];
89
+ dnaTokenRefs: string[];
90
+ provenance: Provenance[];
91
+ confidence: Confidence;
92
+ status: "candidate" | "validated" | "blocked";
93
+ };
94
+
95
+ export type UiMismatch = {
96
+ id: string;
97
+ level: AtomicLevel | "design_dna";
98
+ category: "visual" | "structural" | "semantic" | "typography" | "token_drift";
99
+ description: string;
100
+ referenceRegionId?: string;
101
+ componentId?: string;
102
+ dnaTokenId?: string;
103
+ severity: "high" | "medium" | "low";
104
+ confidence: Confidence;
105
+ provenance: Provenance[];
106
+ };
107
+
108
+ export type ReverseEngineeringInput = {
109
+ id: string;
110
+ kind: UiReferenceKind;
111
+ location: string;
112
+ viewport?: { width: number; height: number };
113
+ checksum?: string;
114
+ status?: "active" | "archived";
115
+ relatedJourneyIds?: string[];
116
+ appId?: string;
117
+ environmentId?: string;
118
+ registeredAt?: string;
119
+ metadata?: Record<string, unknown>;
120
+ };
121
+
122
+ export type ReverseEngineeringJob = {
123
+ schemaVersion: 1;
124
+ jobId: string;
125
+ projectId: string;
126
+ dnaRevision: number;
127
+ status: "queued" | "running" | "blocked" | "completed" | "failed";
128
+ stage: "input" | "measure" | "layout" | "dna" | "atoms" | "compose" | "validate" | "reconcile" | "complete";
129
+ inputs: ReverseEngineeringInput[];
130
+ regions: UiRegion[];
131
+ atomicNodes: AtomicNode[];
132
+ mismatches: UiMismatch[];
133
+ playbooks: CoordinatorPlaybook[];
134
+ playbookRefs: string[];
135
+ storybook?: StorybookCatalog;
136
+ blockedReasons: string[];
137
+ createdAt: string;
138
+ updatedAt: string;
139
+ };
140
+
141
+ export function emptyDesignDNA(projectId: string): DesignDNA {
142
+ return {
143
+ schemaVersion: 1,
144
+ revision: 1,
145
+ projectId,
146
+ spacing: [],
147
+ radii: [],
148
+ elevation: [],
149
+ colors: [],
150
+ motion: [],
151
+ iconography: [],
152
+ typography: [],
153
+ };
154
+ }
155
+
156
+ export function emptyReverseEngineeringJob(projectId: string, jobId: string, dnaRevision = 1): ReverseEngineeringJob {
157
+ const now = new Date().toISOString();
158
+ const playbooks = defaultCoordinatorPlaybooks();
159
+ return {
160
+ schemaVersion: 1,
161
+ jobId,
162
+ projectId,
163
+ dnaRevision,
164
+ status: "queued",
165
+ stage: "input",
166
+ inputs: [],
167
+ regions: [],
168
+ atomicNodes: [],
169
+ mismatches: [],
170
+ playbooks,
171
+ playbookRefs: playbooks.map((playbook) => playbook.id),
172
+ storybook: { detected: false, components: [] },
173
+ blockedReasons: [],
174
+ createdAt: now,
175
+ updatedAt: now,
176
+ };
177
+ }
178
+
179
+ export function defaultCoordinatorPlaybooks(): CoordinatorPlaybook[] {
180
+ return [
181
+ {
182
+ id: "design-dna",
183
+ name: "Design DNA playbook",
184
+ purpose: "Capture and reuse the project-wide visual genome before introducing new UI primitives.",
185
+ status: "active",
186
+ steps: ["Measure spacing, radii, elevation, color, motion, iconography, and typography", "Assign provenance and confidence", "Reuse locked tokens before extending the DNA"],
187
+ },
188
+ {
189
+ id: "atomic-design",
190
+ name: "Atomic Design playbook",
191
+ purpose: "Reconstruct the interface as a reusable component system, from primitives to complete screens.",
192
+ status: "active",
193
+ referenceUrl: "https://storybook.js.org/",
194
+ steps: ["Atoms: single-purpose primitives", "Molecules: composed components", "Organisms: component groups", "Templates: page-level composition", "Pages: real content and states"],
195
+ },
196
+ ];
197
+ }