kortext 2.2.3 → 3.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 (275) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +175 -65
  3. package/agents/architect.md +85 -0
  4. package/agents/compliance-expert.md +36 -75
  5. package/agents/copywriter.md +36 -92
  6. package/agents/db-admin.md +43 -91
  7. package/agents/designer.md +40 -77
  8. package/agents/devops-engineer.md +37 -113
  9. package/agents/growth-expert.md +45 -88
  10. package/agents/product-manager.md +38 -83
  11. package/agents/qa-engineer.md +51 -99
  12. package/agents/security-engineer.md +47 -92
  13. package/bin/kortext.js +2 -0
  14. package/dist/app.js +829 -0
  15. package/dist/cli-spawn.js +137 -0
  16. package/dist/daemon.js +40 -0
  17. package/dist/db.js +78 -0
  18. package/dist/design-preview.js +533 -0
  19. package/dist/docs.js +701 -0
  20. package/dist/engines.js +225 -0
  21. package/dist/index.js +109 -0
  22. package/dist/pick-directory.js +38 -0
  23. package/dist/projects.js +189 -0
  24. package/dist/readiness.js +288 -0
  25. package/dist/runner.js +771 -0
  26. package/dist/update.js +55 -0
  27. package/docs/CHANGELOG.md +11 -0
  28. package/package.json +53 -13
  29. package/templates/AGENTS.md +62 -0
  30. package/templates/docs/API.md +67 -0
  31. package/templates/docs/ARCHITECTURE.md +31 -0
  32. package/templates/docs/BRIEF.md +27 -0
  33. package/templates/docs/CONTENT.md +66 -0
  34. package/templates/docs/DATABASE.md +63 -0
  35. package/templates/docs/DESIGN.md +124 -0
  36. package/templates/docs/ENGINEERING.md +66 -0
  37. package/templates/docs/ENVIRONMENT.md +102 -0
  38. package/templates/docs/GROWTH.md +44 -0
  39. package/templates/docs/LEGAL.md +48 -0
  40. package/templates/docs/PRODUCT.md +43 -0
  41. package/templates/docs/SECURITY.md +65 -0
  42. package/templates/docs/STACK.md +57 -0
  43. package/templates/docs/STRUCTURE.md +73 -0
  44. package/templates/docs/TEST.md +62 -0
  45. package/ui/dist/assets/abnfDiagram-VCTEODGH-Bm7Q2BML.js +1 -0
  46. package/ui/dist/assets/arc-BV9hs0Hu.js +1 -0
  47. package/ui/dist/assets/architecture-7GRP2DOG-RckR3gBI.js +1 -0
  48. package/ui/dist/assets/architectureDiagram-5GKGNRK7-CHiJ6JFT.js +36 -0
  49. package/ui/dist/assets/array-BifhSqXX.js +1 -0
  50. package/ui/dist/assets/blockDiagram-I7D4REHJ-qCbaKVhd.js +129 -0
  51. package/ui/dist/assets/c4Diagram-7LVT6UL2-DidCDJrv.js +38 -0
  52. package/ui/dist/assets/channel-CNUQ11ne.js +1 -0
  53. package/ui/dist/assets/chunk-2Q5K7J3B-C1jixKkw.js +1 -0
  54. package/ui/dist/assets/chunk-4HAMMTFA-CDGFjfVp.js +62 -0
  55. package/ui/dist/assets/chunk-5VM5RSS4-ZNzvKenW.js +15 -0
  56. package/ui/dist/assets/chunk-75Z2AOVW-CbNT4sDh.js +2 -0
  57. package/ui/dist/assets/chunk-DU6HZSFF-Cq2BVv26.js +127 -0
  58. package/ui/dist/assets/chunk-F27PBJKO-CE365m0v.js +1 -0
  59. package/ui/dist/assets/chunk-FOHPRMQF-DHwB1DNv.js +161 -0
  60. package/ui/dist/assets/chunk-GMAD6QVW-DxWL1VC-.js +72 -0
  61. package/ui/dist/assets/chunk-GVQU2GXP-aARV8YbK.js +1 -0
  62. package/ui/dist/assets/chunk-IMKFNOWR-CC790Kt2.js +231 -0
  63. package/ui/dist/assets/chunk-JWPE2WC7-DVXcaiue.js +1 -0
  64. package/ui/dist/assets/chunk-L3NEJ4N5-BfE4VE8J.js +1 -0
  65. package/ui/dist/assets/chunk-OSK3NFVY-BImQbSWH.js +10 -0
  66. package/ui/dist/assets/chunk-P2QGCYS3-CN3XD313.js +1 -0
  67. package/ui/dist/assets/chunk-POPQ4Y6H-Dqb6jOrX.js +1 -0
  68. package/ui/dist/assets/chunk-PWAF6VOD-DfXX6rBk.js +1 -0
  69. package/ui/dist/assets/chunk-SHT3W25Y-DATyyqPK.js +168 -0
  70. package/ui/dist/assets/chunk-SVP7TREG-r1chwRmg.js +88 -0
  71. package/ui/dist/assets/chunk-TICWLB2K-DL4atHUC.js +206 -0
  72. package/ui/dist/assets/chunk-XXDRQBXY-DXrM6jh7.js +1 -0
  73. package/ui/dist/assets/chunk-Y2CYZVJY-DsF7k-Jl.js +1 -0
  74. package/ui/dist/assets/classDiagram-ZZMXUADV-BpQxfIQ7.js +1 -0
  75. package/ui/dist/assets/classDiagram-v2-VYDZK3BY-BpQxfIQ7.js +1 -0
  76. package/ui/dist/assets/cose-bilkent-JH36ORCC-Bhe-ZBlS.js +1 -0
  77. package/ui/dist/assets/cynefin-OW5HDTMX-D1koLd-G.js +1 -0
  78. package/ui/dist/assets/cynefinDiagram-5FMLGOSQ-BMcw40hV.js +62 -0
  79. package/ui/dist/assets/cytoscape.esm-CGd-uY3x.js +321 -0
  80. package/ui/dist/assets/dagre-GXQ25YYZ-DBun-Rs2.js +4 -0
  81. package/ui/dist/assets/dagre-PrKaheQc.js +1 -0
  82. package/ui/dist/assets/defaultLocale-BFoDCU3G.js +1 -0
  83. package/ui/dist/assets/diagram-S7CK7UJ4-C1fUIGvo.js +30 -0
  84. package/ui/dist/assets/diagram-UQ7AKVKN-B5FDfkvB.js +41 -0
  85. package/ui/dist/assets/diagram-VSXAHHWV-CCIpOCNC.js +3 -0
  86. package/ui/dist/assets/diagram-VX7I27RA-DV0q2xo_.js +24 -0
  87. package/ui/dist/assets/diagram-Z3DM3KII-B3LCdNgI.js +24 -0
  88. package/ui/dist/assets/dist-EcW76Zvr.js +1 -0
  89. package/ui/dist/assets/ebnfDiagram-PWID7BFC-De_oRbYe.js +1 -0
  90. package/ui/dist/assets/erDiagram-RLTQ6QDP-B9NikMr1.js +99 -0
  91. package/ui/dist/assets/eventmodeling-NTZA5JFV-DZ4t581k.js +1 -0
  92. package/ui/dist/assets/flowDiagram-HODETNUW-DuTOJp1C.js +1 -0
  93. package/ui/dist/assets/ganttDiagram-EL5Y4UJY-BVTqqlPw.js +292 -0
  94. package/ui/dist/assets/gitGraph-4MIJSDKK-xRFaEJdZ.js +1 -0
  95. package/ui/dist/assets/gitGraphDiagram-WWUBYQGX-CK5_GCmX.js +106 -0
  96. package/ui/dist/assets/graphlib-DS17s2tU.js +1 -0
  97. package/ui/dist/assets/index-CjkpNftW.js +46 -0
  98. package/ui/dist/assets/index-DVIDTWin.css +1 -0
  99. package/ui/dist/assets/info-A6RAGUB7-Dg7z-CTI.js +1 -0
  100. package/ui/dist/assets/infoDiagram-27XIBGKW-YaqsOXh0.js +2 -0
  101. package/ui/dist/assets/init-C-OQMol4.js +1 -0
  102. package/ui/dist/assets/ishikawaDiagram-5VMMS53U-C-wtxZPa.js +70 -0
  103. package/ui/dist/assets/journeyDiagram-3NMN7TZE-DsXSXf16.js +139 -0
  104. package/ui/dist/assets/kanban-definition-UXKFOSKX-Ct59fuWk.js +89 -0
  105. package/ui/dist/assets/katex-CXMH3UgJ.js +257 -0
  106. package/ui/dist/assets/line-BUVw9Mhs.js +1 -0
  107. package/ui/dist/assets/linear-D8ncLO0X.js +1 -0
  108. package/ui/dist/assets/mermaid-parser.core-BDC4bG-O.js +7 -0
  109. package/ui/dist/assets/mermaid.core-DZY3WwzX.js +44 -0
  110. package/ui/dist/assets/mindmap-definition-YA3MSWOX-DtiuF6Ce.js +96 -0
  111. package/ui/dist/assets/ordinal-BDEzSJ7C.js +1 -0
  112. package/ui/dist/assets/packet-AYTQ26CC-BX2-w9rD.js +1 -0
  113. package/ui/dist/assets/path-fybaL0A-.js +1 -0
  114. package/ui/dist/assets/pegDiagram-XKGWAZYB-CotwuIHB.js +1 -0
  115. package/ui/dist/assets/pie-WAS4IAKB-Hth8ZVpm.js +1 -0
  116. package/ui/dist/assets/pieDiagram-E7YTZNPT-D9-INs8B.js +39 -0
  117. package/ui/dist/assets/quadrantDiagram-AXDQQJYC-CMtBXZGV.js +7 -0
  118. package/ui/dist/assets/radar-RG4KPBEZ-6UuwQPsw.js +1 -0
  119. package/ui/dist/assets/railroad-74A4TZTK-BF5TZgj0.js +1 -0
  120. package/ui/dist/assets/railroad-abnf-HS5TGJTU-3KYovtYT.js +1 -0
  121. package/ui/dist/assets/railroad-ebnf-LZEXJU2U-DeP4WedE.js +1 -0
  122. package/ui/dist/assets/railroad-peg-WCYAUIDC-i42N1-yk.js +1 -0
  123. package/ui/dist/assets/railroadDiagram-O6MQD6OU-BS6zkU3Z.js +1 -0
  124. package/ui/dist/assets/requirementDiagram-BXWQKSXE-J4IOB-kP.js +84 -0
  125. package/ui/dist/assets/rough.esm-Dy-Kn_BL.js +1 -0
  126. package/ui/dist/assets/sankeyDiagram-P5KCCOFB-jYTCuYzQ.js +40 -0
  127. package/ui/dist/assets/sequenceDiagram-WJ2MYXX4-Cd2r6PW-.js +162 -0
  128. package/ui/dist/assets/sizeCapture-INFHLROL-B0uUizjq.js +1 -0
  129. package/ui/dist/assets/src-7TsfZXRO.js +1 -0
  130. package/ui/dist/assets/stateDiagram-D77RDMKH-DXOdtOKe.js +1 -0
  131. package/ui/dist/assets/stateDiagram-v2-MP3YSRHH-9h7sIf3R.js +1 -0
  132. package/ui/dist/assets/swimlanes-42K2YHIH-BvJGmGs1.js +1 -0
  133. package/ui/dist/assets/swimlanesDiagram-VR7AAH4N-QA4gPi6a.js +8 -0
  134. package/ui/dist/assets/timeline-definition-24CTP7MA-CAaD6-yd.js +120 -0
  135. package/ui/dist/assets/treeView-Q6P3EWNA-4gDEXIyJ.js +1 -0
  136. package/ui/dist/assets/treemap-WGGIJYW6-CGel5TwW.js +1 -0
  137. package/ui/dist/assets/vennDiagram-4TSXK5OY-DpxsKboL.js +34 -0
  138. package/ui/dist/assets/wardley-WFR3VGLG-Ck38v5ns.js +1 -0
  139. package/ui/dist/assets/wardleyDiagram-VM6X3IG4-Ba0grrqs.js +78 -0
  140. package/ui/dist/assets/xychartDiagram-S5SC5T6Z-DH_gC96K.js +7 -0
  141. package/ui/dist/index.html +13 -0
  142. package/ui/dist/kortext-logo-dark.svg +41 -0
  143. package/ui/dist/kortext-logo.svg +41 -0
  144. package/workflows/existing-project-analysis.md +87 -0
  145. package/workflows/new-project-analysis.md +98 -0
  146. package/workflows/planning-pipeline.md +61 -0
  147. package/AGENTS.md +0 -28
  148. package/agents/backend-developer.md +0 -132
  149. package/agents/delivery-manager.md +0 -121
  150. package/agents/engineering-manager.md +0 -208
  151. package/agents/frontend-developer.md +0 -174
  152. package/agents/operation-manager.md +0 -122
  153. package/hooks/audit-logger.sh +0 -25
  154. package/hooks/auto-locker.sh +0 -74
  155. package/hooks/auto-unlocker.sh +0 -17
  156. package/hooks/backlog-sync-guard.sh +0 -12
  157. package/hooks/branch-guard.sh +0 -32
  158. package/hooks/commit-msg-guard.sh +0 -43
  159. package/hooks/git-pre-commit.sh +0 -76
  160. package/hooks/git-pre-push.sh +0 -17
  161. package/hooks/handover-guard.sh +0 -33
  162. package/hooks/kortext-init.sh +0 -194
  163. package/hooks/kortext-lib.sh +0 -151
  164. package/hooks/lint-guard.sh +0 -50
  165. package/hooks/secret-scanner.sh +0 -89
  166. package/hooks/size-guard.sh +0 -48
  167. package/hooks/snapshot-guard.sh +0 -39
  168. package/hooks/write-guard.sh +0 -77
  169. package/rules/behavior.md +0 -252
  170. package/rules/branching.md +0 -82
  171. package/rules/commands.md +0 -112
  172. package/rules/emergency.md +0 -160
  173. package/rules/mcp.md +0 -47
  174. package/rules/models.md +0 -93
  175. package/scripts/kortext-backlog-add.py +0 -123
  176. package/scripts/kortext-backlog-health.py +0 -112
  177. package/scripts/kortext-backlog-sync.py +0 -117
  178. package/scripts/kortext-bulk-plan.py +0 -63
  179. package/scripts/kortext-cli.py +0 -376
  180. package/scripts/kortext-consistency-check.py +0 -74
  181. package/scripts/kortext-context-check.py +0 -146
  182. package/scripts/kortext-handover.py +0 -107
  183. package/scripts/kortext-item-check.py +0 -61
  184. package/scripts/kortext-item-start.py +0 -70
  185. package/scripts/kortext-item-transition.py +0 -78
  186. package/scripts/kortext-lock.py +0 -174
  187. package/scripts/kortext-session-start.py +0 -127
  188. package/scripts/lock_kortext.sh +0 -34
  189. package/settings/.claude-settings.template.json +0 -48
  190. package/settings/CHANGELOG.md +0 -176
  191. package/settings/INTEGRATION-MAP.md +0 -276
  192. package/settings/README.md +0 -255
  193. package/settings/USER-GUIDE.md +0 -502
  194. package/settings/VERSION +0 -1
  195. package/settings/config.md +0 -8
  196. package/settings/runtime-adapters.md +0 -104
  197. package/skills/backend-developer/.gitkeep +0 -0
  198. package/skills/compliance-expert/.gitkeep +0 -0
  199. package/skills/copywriter/.gitkeep +0 -0
  200. package/skills/db-admin/.gitkeep +0 -0
  201. package/skills/delivery-manager/.gitkeep +0 -0
  202. package/skills/designer/.gitkeep +0 -0
  203. package/skills/devops-engineer/.gitkeep +0 -0
  204. package/skills/engineering-manager/.gitkeep +0 -0
  205. package/skills/frontend-developer/.gitkeep +0 -0
  206. package/skills/growth-expert/.gitkeep +0 -0
  207. package/skills/operation-manager/.gitkeep +0 -0
  208. package/skills/product-manager/.gitkeep +0 -0
  209. package/skills/qa-engineer/.gitkeep +0 -0
  210. package/skills/security-engineer/.gitkeep +0 -0
  211. package/workflows/00-kortext-setup.md +0 -53
  212. package/workflows/01a-analysis-pipeline.md +0 -117
  213. package/workflows/01b-onboarding-pipeline.md +0 -110
  214. package/workflows/02-planning-pipeline.md +0 -146
  215. package/workflows/02b-spike-workflow.md +0 -120
  216. package/workflows/03-environment-setup.md +0 -99
  217. package/workflows/04-development-cycle.md +0 -203
  218. package/workflows/05-test-cycle.md +0 -123
  219. package/workflows/06-deployment-cycle.md +0 -224
  220. package/workflows/07-rollback-pipeline.md +0 -203
  221. package/workflows/08-hotfix-pipeline.md +0 -196
  222. package/workflows/09-maintenance-cycle.md +0 -133
  223. package/workspace/archive/.gitkeep +0 -0
  224. package/workspace/backups/.gitkeep +0 -0
  225. package/workspace/memory/backlog/.gitkeep +0 -0
  226. package/workspace/memory/backlog/BXX-[bug-name].md +0 -2
  227. package/workspace/memory/backlog/DXX-[debt-name].md +0 -2
  228. package/workspace/memory/backlog/README.md +0 -31
  229. package/workspace/memory/backlog/TXX-[task-name].md +0 -2
  230. package/workspace/memory/backlog/debt-dashboard.md +0 -3
  231. package/workspace/memory/backlog/epic-dashboard.md +0 -10
  232. package/workspace/memory/backlog/version-dashboard.md +0 -7
  233. package/workspace/memory/context/.gitkeep +0 -0
  234. package/workspace/memory/context/README.md +0 -35
  235. package/workspace/memory/context/[agent-name]-active.md +0 -2
  236. package/workspace/memory/decisions.md +0 -17
  237. package/workspace/memory/handover.md +0 -40
  238. package/workspace/memory/learned.md +0 -21
  239. package/workspace/references/access.md +0 -47
  240. package/workspace/references/api-reference.md +0 -59
  241. package/workspace/references/blueprint.md +0 -29
  242. package/workspace/references/content-strategy.md +0 -39
  243. package/workspace/references/db-schema.md +0 -51
  244. package/workspace/references/design-system.md +0 -107
  245. package/workspace/references/dictionary.md +0 -42
  246. package/workspace/references/file-system.md +0 -42
  247. package/workspace/references/growth-strategy.md +0 -46
  248. package/workspace/references/legal-strategy.md +0 -35
  249. package/workspace/references/required-skills.md +0 -11
  250. package/workspace/references/security-rules.md +0 -34
  251. package/workspace/references/tech-stack.md +0 -49
  252. package/workspace/references/test-strategy.md +0 -48
  253. package/workspace/reports/analysis-reports.md +0 -40
  254. package/workspace/reports/content-reports.md +0 -29
  255. package/workspace/reports/delivery-reports.md +0 -26
  256. package/workspace/reports/growth-reports.md +0 -9
  257. package/workspace/reports/legal-reports.md +0 -29
  258. package/workspace/reports/product-requirements.md +0 -28
  259. package/workspace/reports/release-notes.md +0 -20
  260. package/workspace/reports/security-reports.md +0 -27
  261. package/workspace/reports/status-reports.md +0 -48
  262. package/workspace/reports/tech-requirements.md +0 -30
  263. package/workspace/reports/test-reports.md +0 -61
  264. package/workspace/templates/BXX-[bug-name].md +0 -34
  265. package/workspace/templates/DXX-[debt-name].md +0 -35
  266. package/workspace/templates/EXX-[epic-name].md +0 -48
  267. package/workspace/templates/HXX-[hotfix-name].md +0 -73
  268. package/workspace/templates/SXX-[spike-name].md +0 -51
  269. package/workspace/templates/TXX-[task-name].md +0 -34
  270. package/workspace/templates/[agent-name]-active.md +0 -4
  271. package/workspace/templates/[original-name]_[YYYY-MM-DD_HHMMSS].md +0 -5
  272. package/workspace/templates/adr-XXX.md +0 -66
  273. package/workspace/templates/debt-dashboard.md +0 -10
  274. package/workspace/templates/epic-dashboard.md +0 -10
  275. package/workspace/templates/version-dashboard.md +0 -10
@@ -0,0 +1,533 @@
1
+ // Render a preview from the tokens declared in DESIGN.md without inventing missing values.
2
+ import { existsSync, readFileSync, unlinkSync, writeFileSync } from 'node:fs';
3
+ import { join } from 'node:path';
4
+ import { readFrontmatter } from './docs.js';
5
+ const COLOR = /^(#[0-9a-f]{3,8}|rgba?\([^)]*\)|hsla?\([^)]*\)|oklch\([^)]*\)|oklab\([^)]*\)|color-mix\([^)]*\))$/i;
6
+ const LENGTH = /^-?\d*\.?\d+(px|rem|em|%|vh|vw|ch)$/i;
7
+ // Validate agent-written values before placing them in style attributes.
8
+ const CSS_SAFE = /^[#\w%.,()\-+\s/'"]*$/;
9
+ const unwrap = (s) => s.trim().replace(/^`|`$/g, '').trim();
10
+ function safe(value) {
11
+ const v = unwrap(value);
12
+ return v && v.length < 200 && CSS_SAFE.test(v) ? v : null;
13
+ }
14
+ function esc(s) {
15
+ return s
16
+ .replace(/&/g, '&amp;')
17
+ .replace(/</g, '&lt;')
18
+ .replace(/>/g, '&gt;')
19
+ .replace(/"/g, '&quot;');
20
+ }
21
+ function cells(line) {
22
+ return line
23
+ .trim()
24
+ .replace(/^\|/, '')
25
+ .replace(/\|$/, '')
26
+ .split('|')
27
+ .map((c) => unwrap(c));
28
+ }
29
+ const isSeparator = (row) => row.every((c) => /^:?-{2,}:?$/.test(c));
30
+ // Ignore unresolved template placeholders.
31
+ const decided = (v) => v !== '' && !/^\[.*\]$/.test(v);
32
+ function bucket(tokens, t) {
33
+ const n = t.name.toLowerCase();
34
+ if (COLOR.test(t.value))
35
+ tokens.colors.push(t);
36
+ else if (/shadow|elevation/.test(n))
37
+ tokens.shadows.push(t);
38
+ else if (/transition|duration|ease|motion/.test(n))
39
+ tokens.motion.push(t);
40
+ else if (/font|family/.test(n))
41
+ tokens.fonts.push(t);
42
+ else if (/radius|rounded/.test(n))
43
+ tokens.radius.push(t);
44
+ else if (/space|gap|gutter/.test(n) && LENGTH.test(t.value))
45
+ tokens.spacing.push(t);
46
+ else if (/container|screen|breakpoint|safe|max|width/.test(n) && LENGTH.test(t.value))
47
+ tokens.layout.push(t);
48
+ }
49
+ /**
50
+ * Reads token declarations wherever they sit: table rows (`| --color-primary |
51
+ * #111 | buttons |`) and bullets (`- --space-md: 16px`). Rows the designer left
52
+ * as prose contribute nothing rather than a broken swatch.
53
+ */
54
+ export function parseDesignTokens(md) {
55
+ const tokens = {
56
+ colors: [],
57
+ spacing: [],
58
+ radius: [],
59
+ shadows: [],
60
+ motion: [],
61
+ layout: [],
62
+ fonts: [],
63
+ type: [],
64
+ };
65
+ const seen = new Map();
66
+ let header = null;
67
+ // A dark palette is declared one of three ways, and all three occur: a second
68
+ // value column, a heading that says dark, or a `-dark` suffix on the name.
69
+ let darkColumn = null;
70
+ let darkSection = false;
71
+ // Merge dark-mode declarations into the corresponding token, regardless of declaration format.
72
+ const attach = (name, value, note, dark) => {
73
+ const base = name
74
+ .replace(/-?dark-?/, '-')
75
+ .replace(/--+$/, '')
76
+ .replace(/-$/, '');
77
+ const restates = darkSection
78
+ ? (seen.get(name) ?? seen.get(base))
79
+ : base !== name
80
+ ? seen.get(base)
81
+ : undefined;
82
+ if (restates) {
83
+ restates.dark = value;
84
+ return;
85
+ }
86
+ if (seen.has(name))
87
+ return;
88
+ const token = dark ? { name, value, note, dark } : { name, value, note };
89
+ seen.set(name, token);
90
+ bucket(tokens, token);
91
+ };
92
+ for (const line of md.split('\n')) {
93
+ const heading = line.match(/^#{1,6}\s+(.*)$/);
94
+ if (heading)
95
+ darkSection = /\bdark\b/i.test(heading[1] ?? '');
96
+ if (/^\s*\|/.test(line)) {
97
+ const row = cells(line);
98
+ if (isSeparator(row))
99
+ continue;
100
+ const lower = row.map((c) => c.toLowerCase());
101
+ if (lower.some((c) => /size/.test(c)) && lower.some((c) => /line-?height|weight/.test(c))) {
102
+ header = lower;
103
+ darkColumn = null;
104
+ continue;
105
+ }
106
+ // `| Token | Light | Dark | Usage |` — the header is the only place that
107
+ // says which column is which, so it is read before the rows.
108
+ if (lower.some((c) => /\bdark\b/.test(c)) && lower.some((c) => /token|name|light/.test(c))) {
109
+ darkColumn = lower.findIndex((c) => /\bdark\b/.test(c));
110
+ header = null;
111
+ continue;
112
+ }
113
+ if (header) {
114
+ const at = (re) => {
115
+ const i = header.findIndex((h) => re.test(h));
116
+ return i === -1 ? '' : (row[i] ?? '');
117
+ };
118
+ const role = row[0] ?? '';
119
+ if (decided(role)) {
120
+ tokens.type.push({
121
+ role,
122
+ family: at(/family|font/),
123
+ size: at(/size/),
124
+ lineHeight: at(/line-?height/),
125
+ weight: at(/weight/),
126
+ tracking: at(/letter|tracking/),
127
+ });
128
+ continue;
129
+ }
130
+ header = null;
131
+ }
132
+ // A token row: the name cell, then the first cell holding a real value.
133
+ const name = row[0] ?? '';
134
+ if (!/^--?[\w-]+$/.test(name))
135
+ continue;
136
+ const usable = (c, i) => i !== darkColumn && decided(c) && (COLOR.test(c) || LENGTH.test(c));
137
+ const value = row.slice(1).find((c, i) => usable(c, i + 1));
138
+ if (!value)
139
+ continue;
140
+ const darkCell = darkColumn === null ? '' : (row[darkColumn] ?? '');
141
+ const dark = decided(darkCell) && COLOR.test(darkCell) ? darkCell : undefined;
142
+ const note = row[row.length - 1] === value ? '' : (row[row.length - 1] ?? '');
143
+ if (darkSection && !dark)
144
+ attach(name, value, note);
145
+ else
146
+ attach(name, value, note, dark);
147
+ continue;
148
+ }
149
+ header = null;
150
+ darkColumn = null;
151
+ const m = line.match(/^\s*[-*+]\s+`?(--[\w-]+)`?\s*:\s*(.+)$/);
152
+ if (!m)
153
+ continue;
154
+ const name = m[1];
155
+ const rest = m[2];
156
+ const value = unwrap(rest.split(/\s{2,}|\s+\(|\s+—|\s+#\s/)[0] ?? '');
157
+ if (!decided(value))
158
+ continue;
159
+ const note = rest.slice(rest.indexOf(value) + value.length).replace(/^[\s(]+|[)\s]+$/g, '');
160
+ attach(name, value, note);
161
+ }
162
+ return tokens;
163
+ }
164
+ // --- contrast -------------------------------------------------------------
165
+ // Calculate contrast only for hex colors; unsupported formats receive no ratio.
166
+ function luminance(hex) {
167
+ const h = hex.replace('#', '');
168
+ const full = h.length === 3
169
+ ? h
170
+ .split('')
171
+ .map((c) => c + c)
172
+ .join('')
173
+ : h.slice(0, 6);
174
+ if (!/^[0-9a-f]{6}$/i.test(full))
175
+ return null;
176
+ const channel = (v) => (v <= 0.03928 ? v / 12.92 : ((v + 0.055) / 1.055) ** 2.4);
177
+ const [r, g, b] = [0, 2, 4].map((i) => channel(parseInt(full.slice(i, i + 2), 16) / 255));
178
+ return 0.2126 * r + 0.7152 * g + 0.0722 * b;
179
+ }
180
+ export function contrast(a, b) {
181
+ const la = luminance(a);
182
+ const lb = luminance(b);
183
+ if (la === null || lb === null)
184
+ return null;
185
+ return (Math.max(la, lb) + 0.05) / (Math.min(la, lb) + 0.05);
186
+ }
187
+ // --- rendering ------------------------------------------------------------
188
+ // Every color reaches the page as a variable, never as a literal, so one
189
+ // attribute on <html> repaints swatches, buttons and chrome together.
190
+ function varName(index) {
191
+ return `--kx-c${index}`;
192
+ }
193
+ // What a label on this color would actually be set in. On a light-blue button
194
+ // in dark mode that is the page's own near-black, not white and not the pale
195
+ // body text — grading it against those two reported a failure the design does
196
+ // not have.
197
+ function inks(theme) {
198
+ return [theme.fg, '#ffffff', theme.bg, '#111111'];
199
+ }
200
+ function bestInk(color, theme) {
201
+ let best = null;
202
+ for (const ink of inks(theme)) {
203
+ const ratio = contrast(color, ink);
204
+ if (ratio !== null && (!best || ratio > best.ratio))
205
+ best = { ink, ratio };
206
+ }
207
+ return best;
208
+ }
209
+ function readable(color, theme) {
210
+ return bestInk(color, theme)?.ink ?? theme.fg;
211
+ }
212
+ function palette(colors, light, dark) {
213
+ const painted = [];
214
+ const lightVars = [];
215
+ const darkVars = [];
216
+ colors.forEach((token, i) => {
217
+ const value = safe(token.value);
218
+ if (!value)
219
+ return;
220
+ const darkValue = token.dark ? (safe(token.dark) ?? value) : value;
221
+ lightVars.push(`${varName(i)}:${value};${varName(i)}-on:${readable(token.value, light)}`);
222
+ darkVars.push(`${varName(i)}:${darkValue};${varName(i)}-on:${readable(token.dark ?? token.value, dark)}`);
223
+ painted.push({ token, ref: `var(${varName(i)})`, on: `var(${varName(i)}-on)` });
224
+ });
225
+ return { painted, lightVars, darkVars };
226
+ }
227
+ // A badge only where a threshold actually applies. Measuring a background
228
+ // against itself produced a red 1.00:1 that meant nothing — a failing grade on
229
+ // a token that was never text.
230
+ function verdict(token, value, theme) {
231
+ const n = token.name.toLowerCase();
232
+ if (/text|fg|foreground/.test(n)) {
233
+ const ratio = contrast(value, theme.bg);
234
+ return ratio === null ? null : { ratio, label: 'on background', min: 4.5 };
235
+ }
236
+ if (/border|divider|outline/.test(n)) {
237
+ const ratio = contrast(value, theme.bg);
238
+ return ratio === null ? null : { ratio, label: 'on background', min: 3 };
239
+ }
240
+ if (/bg|background|surface|card/.test(n)) {
241
+ const ratio = contrast(value, theme.fg);
242
+ return ratio === null ? null : { ratio, label: 'text on it', min: 4.5 };
243
+ }
244
+ // Primary, semantic and accent colors carry a label — grade the text an
245
+ // implementation would actually put on them, light or dark.
246
+ const best = bestInk(value, theme);
247
+ if (!best)
248
+ return null;
249
+ const light = (contrast(best.ink, '#000000') ?? 0) > 8;
250
+ return { ratio: best.ratio, label: `${light ? 'light' : 'dark'} text on it`, min: 4.5 };
251
+ }
252
+ function badge(v, cls) {
253
+ if (!v)
254
+ return '';
255
+ const grade = v.ratio >= v.min ? 'ok' : v.ratio >= v.min - 1.5 ? 'warn' : 'bad';
256
+ return `<span class="badge ${grade} ${cls}">${v.ratio.toFixed(2)}:1 ${esc(v.label)}</span>`;
257
+ }
258
+ function colorSection(painted, light, dark) {
259
+ if (painted.length === 0)
260
+ return '';
261
+ const cards = painted
262
+ .map(({ token, ref }) => {
263
+ const darkValue = token.dark ?? token.value;
264
+ return `<div class="tok">
265
+ <div class="chip" style="background:${ref}"></div>
266
+ <div class="tk">
267
+ <b>${esc(token.name)}</b>
268
+ <code class="only-light">${esc(token.value)}</code>
269
+ <code class="only-dark">${esc(darkValue)}${token.dark ? '' : ' <span class="same">(same in dark)</span>'}</code>
270
+ ${token.note ? `<span class="note">${esc(token.note)}</span>` : ''}
271
+ ${badge(verdict(token, token.value, light), 'only-light')}
272
+ ${badge(verdict(token, darkValue, dark), 'only-dark')}
273
+ </div>
274
+ </div>`;
275
+ })
276
+ .join('');
277
+ return `<section><h2>Color</h2>
278
+ <p class="desc">Each token is graded against the contrast that actually applies to it: text against the background (WCAG AA wants 4.5:1), borders against the background (3:1), and label colors against the text that sits on them. Switch the mode above to grade the dark palette.</p>
279
+ <div class="tokrow">${cards}</div></section>`;
280
+ }
281
+ function typeSection(type, fonts) {
282
+ if (type.length === 0)
283
+ return '';
284
+ const fallback = fonts.map((f) => safe(f.value)).find(Boolean) ?? 'system-ui, sans-serif';
285
+ const rows = type
286
+ .map((r) => {
287
+ const family = safe(r.family) ?? fallback;
288
+ const size = safe(r.size) ?? '16px';
289
+ const lh = safe(r.lineHeight) ?? '1.5';
290
+ const weight = safe(r.weight) ?? '400';
291
+ const tracking = safe(r.tracking) ?? 'normal';
292
+ const style = `font-family:${esc(family)};font-size:${esc(size)};line-height:${esc(lh)};font-weight:${esc(weight)};letter-spacing:${esc(tracking)}`;
293
+ return `<div class="spec">
294
+ <div class="tag">${esc(r.role)}<span>${esc([r.size, r.weight, r.lineHeight].filter(Boolean).join(' · '))}</span></div>
295
+ <div class="sample" style="${style}">Grumpy wizards make toxic brew — 0123</div>
296
+ </div>`;
297
+ })
298
+ .join('');
299
+ return `<section><h2>Typography</h2><div class="surface">${rows}</div></section>`;
300
+ }
301
+ // 'bar' draws the value at size — honest for a spacing scale, meaningless for
302
+ // a 1200px container, which is why layout is listed rather than drawn.
303
+ function scaleSection(title, tokens, kind) {
304
+ if (tokens.length === 0)
305
+ return '';
306
+ const items = tokens
307
+ .map((t) => {
308
+ const value = safe(t.value);
309
+ if (!value)
310
+ return '';
311
+ const demo = kind === 'bar'
312
+ ? `<div class="bar" style="width:${esc(value)}"></div>`
313
+ : kind === 'box'
314
+ ? `<div class="box" style="border-radius:${esc(value)}"></div>`
315
+ : `<span class="note">${esc(t.note)}</span>`;
316
+ return `<div class="scale-row"><code>${esc(t.name)}</code><span class="val">${esc(t.value)}</span>${demo}</div>`;
317
+ })
318
+ .join('');
319
+ return `<section><h2>${esc(title)}</h2><div class="surface">${items}</div></section>`;
320
+ }
321
+ function shadowSection(shadows) {
322
+ if (shadows.length === 0)
323
+ return '';
324
+ const items = shadows
325
+ .map((t) => {
326
+ const value = safe(t.value);
327
+ if (!value)
328
+ return '';
329
+ return `<div class="shadow-cell"><div class="shadow-box" style="box-shadow:${esc(value)}"></div><code>${esc(t.name)}</code></div>`;
330
+ })
331
+ .join('');
332
+ return `<section><h2>Elevation</h2><div class="surface grid">${items}</div></section>`;
333
+ }
334
+ function paintedPick(painted, ...res) {
335
+ for (const re of res) {
336
+ const hit = painted.find((p) => re.test(p.token.name.toLowerCase()));
337
+ if (hit)
338
+ return hit;
339
+ }
340
+ return null;
341
+ }
342
+ function componentSection(painted, radius) {
343
+ const primary = paintedPick(painted, /primary(?!-hover)/, /accent/, /brand/);
344
+ if (!primary)
345
+ return '';
346
+ const hover = paintedPick(painted, /primary-hover|accent-hover/) ?? primary;
347
+ const border = paintedPick(painted, /border/);
348
+ const error = paintedPick(painted, /error|danger|destructive/);
349
+ const success = paintedPick(painted, /success/);
350
+ const surface = paintedPick(painted, /surface|card/);
351
+ const r = radius.map((t) => safe(t.value)).find(Boolean) ?? '8px';
352
+ const line = border?.ref ?? 'var(--line)';
353
+ const bad = error?.ref ?? '#dc2626';
354
+ const good = success?.ref ?? '#16a34a';
355
+ const b = (label, extra) => `<div class="demo"><button style="background:${primary.ref};color:${primary.on};border-radius:${esc(r)};${extra}">Primary action</button><span>${esc(label)}</span></div>`;
356
+ return `<section><h2>Components</h2>
357
+ <p class="desc">Built from the tokens above — a state that looks wrong here is a token that is wrong in the document.</p>
358
+ <div class="surface"${surface ? ` style="background:${surface.ref}"` : ''}>
359
+ <div class="grid">
360
+ ${b('default', '')}
361
+ ${b('hover', `background:${hover.ref}`)}
362
+ ${b('active', 'transform:scale(0.98)')}
363
+ ${b('disabled', 'opacity:0.5;cursor:not-allowed')}
364
+ ${b('focus', `outline:2px solid ${primary.ref};outline-offset:2px`)}
365
+ </div>
366
+ <div class="grid" style="margin-top:18px">
367
+ <div class="demo"><input placeholder="Input — default" style="border:1px solid ${line};border-radius:${esc(r)}"><span>default</span></div>
368
+ <div class="demo"><input placeholder="Input — focus" style="border:1px solid ${primary.ref};border-radius:${esc(r)};box-shadow:0 0 0 3px color-mix(in srgb, ${primary.ref} 30%, transparent)"><span>focus</span></div>
369
+ <div class="demo"><input placeholder="Input — error" style="border:1px solid ${bad};border-radius:${esc(r)}"><span>error</span></div>
370
+ </div>
371
+ <div class="grid" style="margin-top:18px">
372
+ <div class="demo"><span class="pill" style="background:color-mix(in srgb, ${good} 15%, transparent);color:${good};border-radius:${esc(r)}">Saved</span><span>success</span></div>
373
+ <div class="demo"><span class="pill" style="background:color-mix(in srgb, ${bad} 15%, transparent);color:${bad};border-radius:${esc(r)}">Could not save</span><span>error</span></div>
374
+ </div>
375
+ </div></section>`;
376
+ }
377
+ function pick(tokens, ...res) {
378
+ for (const re of res) {
379
+ const hit = tokens.find((t) => re.test(t.name.toLowerCase()));
380
+ if (hit)
381
+ return safe(hit.value);
382
+ }
383
+ return null;
384
+ }
385
+ function pickDark(tokens, ...res) {
386
+ for (const re of res) {
387
+ const hit = tokens.find((t) => re.test(t.name.toLowerCase()));
388
+ if (hit)
389
+ return safe(hit.dark ?? hit.value);
390
+ }
391
+ return null;
392
+ }
393
+ export function renderDesignPreview(md, projectName = 'project') {
394
+ const t = parseDesignTokens(md);
395
+ const status = readFrontmatter(md).status ?? 'uninitialized';
396
+ const light = {
397
+ bg: pick(t.colors, /bg-main|background|bg(?!-)/) ?? '#ffffff',
398
+ fg: pick(t.colors, /text-base|text(?!-muted)|fg/) ?? '#111111',
399
+ };
400
+ const dark = {
401
+ bg: pickDark(t.colors, /bg-main|background|bg(?!-)/) ?? '#111113',
402
+ fg: pickDark(t.colors, /text-base|text(?!-muted)|fg/) ?? '#f4f4f5',
403
+ };
404
+ const { painted, lightVars, darkVars } = palette(t.colors, light, dark);
405
+ const hasDark = t.colors.some((c) => c.dark);
406
+ const empty = t.colors.length === 0 && t.type.length === 0 && t.spacing.length === 0 && t.radius.length === 0;
407
+ const body = empty
408
+ ? `<section><div class="surface"><p class="desc">DESIGN.md carries no decided tokens yet — every value is still a <code>[placeholder]</code>. Run the designer step, then reopen this page.</p></div></section>`
409
+ : [
410
+ hasDark
411
+ ? ''
412
+ : `<div class="warn-strip only-dark">DESIGN.md declares no dark palette — only this page switches. Give each color a dark value (a <code>Dark</code> column, a <code>## Dark mode</code> table, or a <code>-dark</code> token) and the swatches below switch with it.</div>`,
413
+ colorSection(painted, light, dark),
414
+ typeSection(t.type, t.fonts),
415
+ scaleSection('Spacing', t.spacing, 'bar'),
416
+ scaleSection('Radius', t.radius, 'box'),
417
+ shadowSection(t.shadows),
418
+ componentSection(painted, t.radius),
419
+ scaleSection('Layout', t.layout, 'list'),
420
+ ].join('\n');
421
+ return `<!doctype html>
422
+ <html lang="en" data-theme="light"><head><meta charset="utf-8">
423
+ <meta name="viewport" content="width=device-width, initial-scale=1">
424
+ <title>${esc(projectName)} — Design tokens</title>
425
+ <style>
426
+ :root { --paper:#fbfbfa; --ink:#18181b; --muted:#71717a; --line:#e4e4e7; --card:#fff; ${lightVars.join(';')} }
427
+ :root[data-theme="dark"] { --paper:#111113; --ink:#f4f4f5; --muted:#a1a1aa; --line:#27272a; --card:#18181b; ${darkVars.join(';')} }
428
+ :root[data-theme="dark"] .only-light, :root[data-theme="light"] .only-dark { display:none; }
429
+ * { box-sizing: border-box; }
430
+ body { margin:0; background:var(--paper); color:var(--ink); font:14px/1.6 ui-sans-serif, system-ui, -apple-system, sans-serif; }
431
+ .wrap { max-width: 980px; margin: 0 auto; padding: 28px 22px 100px; }
432
+ .bar-top { display:flex; align-items:center; gap:12px; justify-content:space-between; margin-bottom:26px; }
433
+ .modes { display:inline-flex; border:1px solid var(--line); border-radius:999px; padding:3px; background:var(--card); }
434
+ .modes button { border:0; background:transparent; color:var(--muted); font:inherit; font-size:12px; padding:5px 14px; border-radius:999px; cursor:pointer; }
435
+ .modes button[aria-pressed="true"] { background:var(--ink); color:var(--paper); font-weight:600; }
436
+ header h1 { font-size: 28px; letter-spacing:-0.02em; margin: 0; }
437
+ header p { color: var(--muted); margin: 8px 0 0; }
438
+ .meta { font: 11px ui-monospace, monospace; color: var(--muted); text-transform: uppercase; letter-spacing: .06em; }
439
+ section { margin-top: 48px; }
440
+ section > h2 { font-size:12px; letter-spacing:.08em; text-transform:uppercase; color:var(--muted); margin:0 0 10px; }
441
+ .desc { color: var(--muted); margin: 0 0 14px; }
442
+ .warn-strip { margin-top:32px; padding:12px 16px; border:1px solid var(--line); border-left:3px solid #f59e0b; border-radius:8px; color:var(--muted); font-size:13px; }
443
+ .surface { background: var(--card); border:1px solid var(--line); border-radius:12px; padding:22px; }
444
+ .tokrow { display:grid; grid-template-columns:repeat(auto-fill,minmax(230px,1fr)); gap:10px; }
445
+ .tok { display:flex; gap:12px; align-items:flex-start; background:var(--card); border:1px solid var(--line); border-radius:10px; padding:10px; }
446
+ .chip { width:38px; height:38px; border-radius:8px; flex:none; box-shadow: inset 0 0 0 1px rgba(128,128,128,.25); }
447
+ .tk b { display:block; font-weight:600; font-size:12.5px; }
448
+ .tk code, .val, code { font-family: ui-monospace, SFMono-Regular, monospace; font-size:11.5px; color:var(--muted); }
449
+ .tk code { display:block; }
450
+ .tk .same { opacity:.65; }
451
+ .tk .note, .scale-row .note { color:var(--muted); font-size:11.5px; }
452
+ .tk .note { display:block; }
453
+ .badge { display:inline-block; margin-top:6px; padding:1px 7px; border-radius:999px; font-size:11px; font-weight:600; }
454
+ .badge.ok { background:#16a34a26; color:#22a05a; } .badge.warn { background:#f59e0b26; color:#c1820c; } .badge.bad { background:#dc262626; color:#e0574f; }
455
+ .spec { display:flex; gap:20px; align-items:baseline; padding:14px 0; border-bottom:1px solid var(--line); }
456
+ .spec:last-child { border-bottom:0; }
457
+ .spec .tag { width:150px; flex:none; font-family:ui-monospace,monospace; font-size:11.5px; color:var(--muted); }
458
+ .spec .tag span { display:block; font-size:10.5px; opacity:.75; }
459
+ .spec .sample { min-width:0; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
460
+ .scale-row { display:flex; align-items:center; gap:14px; padding:7px 0; }
461
+ .scale-row code { width:150px; flex:none; }
462
+ .scale-row .val { width:70px; flex:none; }
463
+ .bar { height:14px; background:currentColor; opacity:.75; border-radius:3px; max-width:100%; }
464
+ .box { width:56px; height:40px; border:2px solid currentColor; opacity:.7; }
465
+ .grid { display:flex; flex-wrap:wrap; gap:18px; }
466
+ .shadow-cell { text-align:center; }
467
+ .shadow-box { width:110px; height:66px; background:var(--card); border-radius:10px; margin-bottom:8px; border:1px solid var(--line); }
468
+ .demo { display:flex; flex-direction:column; gap:6px; align-items:flex-start; }
469
+ .demo > span { font-size:11px; color:var(--muted); font-family:ui-monospace,monospace; }
470
+ .demo button { border:0; padding:0 18px; height:44px; font:inherit; font-weight:600; cursor:pointer; }
471
+ .demo input { height:44px; padding:0 12px; font:inherit; background:transparent; color:inherit; min-width:220px; }
472
+ .pill { display:inline-block; padding:5px 12px; font-size:12.5px; font-weight:600; }
473
+ footer { margin-top:60px; color:var(--muted); font-size:12px; border-top:1px solid var(--line); padding-top:14px; }
474
+ </style></head>
475
+ <body><div class="wrap">
476
+ <div class="bar-top">
477
+ <div class="meta">${esc(status)} · generated from DESIGN.md</div>
478
+ <div class="modes" role="group" aria-label="Preview mode">
479
+ <button type="button" data-mode="light" aria-pressed="true">Light</button>
480
+ <button type="button" data-mode="dark" aria-pressed="false">Dark</button>
481
+ <button type="button" data-mode="system" aria-pressed="false">System</button>
482
+ </div>
483
+ </div>
484
+ <header>
485
+ <h1>${esc(projectName)} — design tokens</h1>
486
+ <p>Every value on this page is read out of <code>.kortext/DESIGN.md</code>. Edit the document, not this file.</p>
487
+ </header>
488
+ ${body}
489
+ <footer>Generated by kortext on ${new Date().toISOString().slice(0, 10)} — regenerated each time DESIGN.md changes.</footer>
490
+ </div>
491
+ <script>
492
+ // The mode is the reader's, not the document's: it survives a reload, and
493
+ // "system" keeps following the OS while the page is open.
494
+ (function () {
495
+ var root = document.documentElement;
496
+ var media = window.matchMedia('(prefers-color-scheme: dark)');
497
+ var mode = 'system';
498
+ try { mode = localStorage.getItem('kortext-preview-mode') || 'system'; } catch (e) {}
499
+ function paint() {
500
+ root.dataset.theme = mode === 'system' ? (media.matches ? 'dark' : 'light') : mode;
501
+ document.querySelectorAll('.modes button').forEach(function (b) {
502
+ b.setAttribute('aria-pressed', String(b.dataset.mode === mode));
503
+ });
504
+ }
505
+ document.querySelectorAll('.modes button').forEach(function (b) {
506
+ b.addEventListener('click', function () {
507
+ mode = b.dataset.mode;
508
+ try { localStorage.setItem('kortext-preview-mode', mode); } catch (e) {}
509
+ paint();
510
+ });
511
+ });
512
+ media.addEventListener('change', function () { if (mode === 'system') paint(); });
513
+ paint();
514
+ })();
515
+ </script>
516
+ </body></html>`;
517
+ }
518
+ /** Writes `.kortext/DESIGN.html` next to the document. No document, no page. */
519
+ export function writeDesignPreview(project) {
520
+ const md = join(project.repo_path, '.kortext', 'DESIGN.md');
521
+ const html = join(project.repo_path, '.kortext', 'DESIGN.html');
522
+ if (!existsSync(md))
523
+ return;
524
+ const content = readFileSync(md, 'utf8');
525
+ // A skeleton has nothing to preview: a page rendered from placeholders is a
526
+ // file nobody asked for, and the next author reports it as one.
527
+ if (readFrontmatter(content).status === 'uninitialized') {
528
+ if (existsSync(html))
529
+ unlinkSync(html);
530
+ return;
531
+ }
532
+ writeFileSync(html, renderDesignPreview(content, project.name), 'utf8');
533
+ }