@geraldmaron/construct 1.1.1 → 1.2.1

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 (426) hide show
  1. package/README.md +30 -19
  2. package/bin/construct +487 -87
  3. package/bin/construct-postinstall.mjs +30 -2
  4. package/examples/distribution/README.md +42 -0
  5. package/examples/distribution/manifest.json +53 -0
  6. package/examples/distribution/sources/adr.md +84 -0
  7. package/examples/distribution/sources/deck-one-pager.md +65 -0
  8. package/examples/distribution/sources/prd-platform.md +161 -0
  9. package/examples/distribution/sources/research-brief.md +80 -0
  10. package/examples/distribution/sources/rfc-platform.md +82 -0
  11. package/examples/distribution/sources/runbook.md +103 -0
  12. package/examples/distribution/sources/strategy.md +88 -0
  13. package/lib/adapters-sync.mjs +67 -0
  14. package/lib/artifact-gate-notice.mjs +38 -0
  15. package/lib/artifact-manifest.mjs +258 -0
  16. package/lib/artifact-release-gate.mjs +171 -0
  17. package/lib/artifact-reviewers.mjs +68 -0
  18. package/lib/artifact-type-from-path.mjs +79 -0
  19. package/lib/artifact-workflow.mjs +252 -0
  20. package/lib/audit-skills.mjs +4 -0
  21. package/lib/audit-specialists.mjs +285 -0
  22. package/lib/audit-trail.mjs +1 -1
  23. package/lib/auto-docs.mjs +90 -19
  24. package/lib/beads/drift.mjs +27 -6
  25. package/lib/beads-client.mjs +49 -121
  26. package/lib/beads-optimistic.mjs +5 -12
  27. package/lib/brand-fonts.mjs +93 -0
  28. package/lib/brand-prose.mjs +214 -0
  29. package/lib/brand-tokens.mjs +92 -0
  30. package/lib/bridges/copilot-proxy.mjs +13 -26
  31. package/lib/capability-ledger.mjs +156 -0
  32. package/lib/certification/artifact-fixtures.mjs +132 -0
  33. package/lib/certification/artifact-gates.mjs +63 -0
  34. package/lib/certification/artifact-provenance.mjs +97 -0
  35. package/lib/certification/canonical-scenarios.mjs +78 -0
  36. package/lib/certification/cli.mjs +191 -0
  37. package/lib/certification/dashboard-api.mjs +71 -0
  38. package/lib/certification/demo-parity.mjs +116 -0
  39. package/lib/certification/document-io-fixtures.mjs +246 -0
  40. package/lib/certification/document-workflow.mjs +97 -0
  41. package/lib/certification/eval-bridge.mjs +77 -0
  42. package/lib/certification/model-routing.mjs +149 -0
  43. package/lib/certification/prompt-budget.mjs +119 -0
  44. package/lib/certification/rc-gate.mjs +206 -0
  45. package/lib/certification/real-llm-scenarios.mjs +303 -0
  46. package/lib/certification/role-cards.mjs +113 -0
  47. package/lib/certification/role-overlays.mjs +117 -0
  48. package/lib/certification/run.mjs +122 -0
  49. package/lib/certification/runner.mjs +323 -0
  50. package/lib/certification/scenarios.mjs +51 -0
  51. package/lib/certification/skill-inventory.mjs +289 -0
  52. package/lib/certification/skill-scenarios.mjs +147 -0
  53. package/lib/certification/specialist-contracts.mjs +85 -0
  54. package/lib/certification/specialist-scenarios.mjs +175 -0
  55. package/lib/certification/stale-impact.mjs +146 -0
  56. package/lib/certification/status.mjs +252 -0
  57. package/lib/certification/store.mjs +77 -0
  58. package/lib/chat/cli.mjs +333 -0
  59. package/lib/chat/command-suggest.mjs +161 -0
  60. package/lib/chat/commands.mjs +215 -0
  61. package/lib/chat/config.mjs +142 -0
  62. package/lib/chat/context-compactor.mjs +250 -0
  63. package/lib/chat/context-continuation.mjs +253 -0
  64. package/lib/chat/continuation-source.mjs +58 -0
  65. package/lib/chat/demo-guide.mjs +61 -0
  66. package/lib/chat/design-tokens.mjs +91 -0
  67. package/lib/chat/desktop-binary.mjs +79 -0
  68. package/lib/chat/desktop-build.mjs +130 -0
  69. package/lib/chat/desktop-launcher.mjs +133 -0
  70. package/lib/chat/evidence.mjs +145 -0
  71. package/lib/chat/export.mjs +74 -0
  72. package/lib/chat/harness/driver.mjs +91 -0
  73. package/lib/chat/list-picker.mjs +112 -0
  74. package/lib/chat/model-picker.mjs +356 -0
  75. package/lib/chat/openrouter-fallback.mjs +151 -0
  76. package/lib/chat/permission-prompt.mjs +33 -0
  77. package/lib/chat/picker-catalog.mjs +45 -0
  78. package/lib/chat/policy-telemetry.mjs +34 -0
  79. package/lib/chat/present.mjs +246 -0
  80. package/lib/chat/session-context.mjs +39 -0
  81. package/lib/chat/session-persist.mjs +73 -0
  82. package/lib/chat/session-restore.mjs +71 -0
  83. package/lib/chat/session-settings.mjs +53 -0
  84. package/lib/chat/system-prompt.mjs +52 -0
  85. package/lib/chat/transparency.mjs +93 -0
  86. package/lib/chat/tui/color-scheme.mjs +42 -0
  87. package/lib/chat/tui/markdown.mjs +123 -0
  88. package/lib/chat/tui/presentation.mjs +100 -0
  89. package/lib/chat/tui/render.mjs +500 -0
  90. package/lib/chat/tui/turn-block.mjs +284 -0
  91. package/lib/chat/tui/turn-present.mjs +18 -0
  92. package/lib/chat/tui/turn-state.mjs +88 -0
  93. package/lib/chat/tui/usage.mjs +122 -0
  94. package/lib/chat/web-commands.mjs +146 -0
  95. package/lib/chat/web-launcher.mjs +63 -0
  96. package/lib/chat/web-picker-keys.mjs +46 -0
  97. package/lib/chat/web-session.mjs +159 -0
  98. package/lib/cli-commands.mjs +146 -15
  99. package/lib/cli-service-inventory.mjs +42 -0
  100. package/lib/comment-lint.mjs +6 -4
  101. package/lib/config/intake-policy.mjs +209 -0
  102. package/lib/config/project-config.mjs +11 -2
  103. package/lib/config/schema.mjs +86 -6
  104. package/lib/config/source-targets.mjs +311 -0
  105. package/lib/contract-schemas/decision.json +50 -0
  106. package/lib/contract-schemas/implementation.json +51 -0
  107. package/lib/contract-schemas/review-report.json +32 -0
  108. package/lib/contract-schemas/test-report.json +43 -0
  109. package/lib/contracts/construct-handoff.mjs +60 -0
  110. package/lib/contracts/validate.mjs +32 -3
  111. package/lib/contracts/violation-log.mjs +58 -5
  112. package/lib/dashboard-demo.mjs +71 -0
  113. package/lib/dashboard-static.mjs +19 -5
  114. package/lib/decisions/registry.mjs +5 -3
  115. package/lib/deck-export-pptx.mjs +1152 -0
  116. package/lib/demo-recording.mjs +142 -0
  117. package/lib/demo-script.mjs +114 -0
  118. package/lib/demo-surface.mjs +249 -0
  119. package/lib/demo.mjs +598 -140
  120. package/lib/diagram-export.mjs +192 -0
  121. package/lib/diagram.mjs +13 -11
  122. package/lib/docs-verify.mjs +25 -7
  123. package/lib/doctor/index.mjs +3 -1
  124. package/lib/doctor/project-adapters.mjs +44 -0
  125. package/lib/doctor/source-checkout.mjs +16 -0
  126. package/lib/doctor/watchers/cx-budget.mjs +98 -0
  127. package/lib/doctor/watchers/graph-staleness.mjs +52 -0
  128. package/lib/doctor/watchers/process-pressure.mjs +4 -3
  129. package/lib/document-export.mjs +384 -38
  130. package/lib/document-extract.mjs +44 -4
  131. package/lib/document-ingest.mjs +29 -20
  132. package/lib/embed/auto-sources.mjs +44 -0
  133. package/lib/embed/cli.mjs +4 -3
  134. package/lib/embed/daemon.mjs +18 -46
  135. package/lib/embed/demand-fetch.mjs +50 -19
  136. package/lib/embed/inbox.mjs +6 -10
  137. package/lib/embed/worker.mjs +4 -3
  138. package/lib/embedded-contract/index.mjs +11 -0
  139. package/lib/embedded-contract/model-resolve.mjs +10 -9
  140. package/lib/env-config.mjs +26 -0
  141. package/lib/evals/dataset.mjs +137 -0
  142. package/lib/evals/gates.mjs +175 -0
  143. package/lib/export-branding.mjs +32 -0
  144. package/lib/graph/build-co-change.mjs +54 -0
  145. package/lib/graph/build-from-registry.mjs +136 -0
  146. package/lib/graph/build-import-graph.mjs +153 -0
  147. package/lib/graph/cli.mjs +110 -0
  148. package/lib/graph/impact-cli.mjs +89 -0
  149. package/lib/graph/impact.mjs +108 -0
  150. package/lib/graph/staleness.mjs +44 -0
  151. package/lib/graph/store.mjs +172 -0
  152. package/lib/health-check.mjs +90 -76
  153. package/lib/hooks/artifact-release-gate.mjs +43 -0
  154. package/lib/hooks/brand-prose-lint.mjs +38 -0
  155. package/lib/hooks/graph-impact-advisory.mjs +62 -0
  156. package/lib/hooks/session-optimize.mjs +84 -207
  157. package/lib/hooks/session-start.mjs +18 -16
  158. package/lib/host-disposition.mjs +7 -0
  159. package/lib/improvement/cli.mjs +189 -0
  160. package/lib/improvement/controller.mjs +137 -0
  161. package/lib/improvement/proposal.mjs +120 -0
  162. package/lib/improvement/specialist-loop.mjs +192 -0
  163. package/lib/improvement/store.mjs +89 -0
  164. package/lib/improvement/surface.mjs +219 -0
  165. package/lib/ingest-tooling.mjs +97 -0
  166. package/lib/init/doc-lanes.mjs +165 -0
  167. package/lib/init-docs.mjs +31 -178
  168. package/lib/init-unified.mjs +50 -119
  169. package/lib/init-update-guide.mjs +102 -0
  170. package/lib/init-update.mjs +32 -1
  171. package/lib/init.mjs +8 -0
  172. package/lib/install/desktop-binary-download.mjs +85 -0
  173. package/lib/install/legacy-global-cleanup.mjs +189 -0
  174. package/lib/intake/constants.mjs +29 -0
  175. package/lib/intake/daemon.mjs +2 -0
  176. package/lib/intake/git-queue.mjs +9 -8
  177. package/lib/intake/intake-config.mjs +52 -105
  178. package/lib/intake/legacy-paths.mjs +5 -0
  179. package/lib/intake/queue.mjs +2 -1
  180. package/lib/intake/session-prelude.mjs +90 -1
  181. package/lib/libreoffice-export.mjs +97 -0
  182. package/lib/logging/rotate.mjs +9 -0
  183. package/lib/maintenance/docker-reclaim.mjs +206 -0
  184. package/lib/mcp/external-schema-cost.mjs +74 -0
  185. package/lib/mcp/server.mjs +103 -10
  186. package/lib/mcp/stdio-mcp-probe.mjs +188 -0
  187. package/lib/mcp/tool-budget.mjs +55 -4
  188. package/lib/mcp/tools/document.mjs +18 -4
  189. package/lib/mcp/tools/embedded-contract.mjs +14 -0
  190. package/lib/mcp/tools/skills.mjs +32 -3
  191. package/lib/mcp/tools/workflow.mjs +13 -1
  192. package/lib/model-free-selector.mjs +18 -0
  193. package/lib/model-registry.mjs +13 -229
  194. package/lib/model-router.mjs +373 -92
  195. package/lib/models/behavior-matrix.mjs +289 -0
  196. package/lib/models/catalog.mjs +209 -0
  197. package/lib/models/execution-capability-profile.mjs +196 -0
  198. package/lib/models/execution-policy.mjs +307 -0
  199. package/lib/models/provider-poll.mjs +383 -0
  200. package/lib/npm-spawn-env.mjs +17 -0
  201. package/lib/ollama/installed-models.mjs +129 -0
  202. package/lib/oracle/actions.mjs +187 -0
  203. package/lib/oracle/artifact-gate.mjs +99 -0
  204. package/lib/oracle/cli.mjs +204 -0
  205. package/lib/oracle/daemon-entry.mjs +14 -0
  206. package/lib/oracle/dispatch.mjs +81 -0
  207. package/lib/oracle/execute.mjs +143 -0
  208. package/lib/oracle/gaps.mjs +76 -0
  209. package/lib/oracle/index.mjs +84 -0
  210. package/lib/oracle/issues.mjs +164 -0
  211. package/lib/oracle/org-graph.mjs +170 -0
  212. package/lib/oracle/policy.mjs +80 -0
  213. package/lib/oracle/read-model.mjs +398 -0
  214. package/lib/oracle/reconcile.mjs +191 -0
  215. package/lib/oracle/routing.mjs +89 -0
  216. package/lib/oracle/synthesize.mjs +384 -0
  217. package/lib/oracle/verdicts.mjs +51 -0
  218. package/lib/orchestration/worker.mjs +88 -27
  219. package/lib/orchestration-policy.mjs +50 -0
  220. package/lib/parity.mjs +96 -2
  221. package/lib/playwright-demo.mjs +254 -0
  222. package/lib/project-init-shared.mjs +4 -1
  223. package/lib/prompt-composer.js +7 -3
  224. package/lib/prompt-validation-contract.mjs +24 -0
  225. package/lib/provider-capabilities.js +57 -11
  226. package/lib/providers/contract/adapters/confluence/index.mjs +181 -0
  227. package/lib/providers/contract/adapters/git/index.mjs +115 -0
  228. package/lib/providers/contract/adapters/github/index.mjs +166 -0
  229. package/lib/providers/contract/adapters/jira/index.mjs +187 -0
  230. package/lib/providers/contract/adapters/slack/index.mjs +175 -0
  231. package/lib/providers/contract/contract-tests.mjs +57 -0
  232. package/lib/providers/contract/errors.mjs +48 -0
  233. package/lib/providers/contract/interface.mjs +50 -0
  234. package/lib/providers/contract/registry.mjs +102 -0
  235. package/lib/providers/copilot-auth.mjs +297 -0
  236. package/lib/providers/credential-bootstrap.mjs +178 -0
  237. package/lib/providers/credential-catalog.mjs +46 -0
  238. package/lib/providers/credential-sources.mjs +63 -0
  239. package/lib/providers/creds.mjs +5 -2
  240. package/lib/providers/op-run.mjs +59 -0
  241. package/lib/providers/secret-resolver.mjs +159 -0
  242. package/lib/publish-template.mjs +163 -0
  243. package/lib/publish-tooling.mjs +119 -0
  244. package/lib/publish.mjs +305 -0
  245. package/lib/registry/cli.mjs +82 -0
  246. package/lib/registry/consolidation.mjs +147 -0
  247. package/lib/registry/generate-docs.mjs +75 -0
  248. package/lib/registry/skill-verification.mjs +57 -0
  249. package/lib/registry/surface-map.mjs +76 -0
  250. package/lib/registry/validate.mjs +135 -0
  251. package/lib/resources/budget.mjs +82 -0
  252. package/lib/resources/process-budget.mjs +45 -0
  253. package/lib/rules-delivery.mjs +9 -2
  254. package/lib/rules-read.mjs +26 -0
  255. package/lib/runtime-env.mjs +23 -0
  256. package/lib/runtime-pressure.mjs +51 -7
  257. package/lib/schema-infer.mjs +13 -2
  258. package/lib/server/chat-loop.mjs +622 -0
  259. package/lib/server/demo-preview.mjs +63 -0
  260. package/lib/server/index.mjs +259 -9
  261. package/lib/server/rate-limit.mjs +5 -3
  262. package/lib/server/webhook.mjs +3 -2
  263. package/lib/service-manager.mjs +60 -8
  264. package/lib/setup.mjs +70 -7
  265. package/lib/specialists/prompt-schema.mjs +19 -10
  266. package/lib/specialists/roster.mjs +43 -0
  267. package/lib/specialists/scaffold.mjs +56 -0
  268. package/lib/storage/backend.mjs +6 -6
  269. package/lib/storage/hybrid-query.mjs +7 -4
  270. package/lib/storage/state-source.mjs +5 -5
  271. package/lib/storage/sync.mjs +2 -3
  272. package/lib/telemetry/rule-calls.mjs +52 -0
  273. package/lib/template-registry.mjs +2 -2
  274. package/lib/templates/visual-requirements.mjs +27 -51
  275. package/lib/test-corpus-inventory.mjs +313 -0
  276. package/lib/uninstall/uninstall.mjs +19 -7
  277. package/lib/update.mjs +12 -0
  278. package/lib/upgrade.mjs +14 -0
  279. package/lib/wireframe.mjs +20 -14
  280. package/lib/worker/run.mjs +17 -4
  281. package/lib/worker/trace.mjs +11 -3
  282. package/package.json +28 -14
  283. package/personas/construct.md +2 -2
  284. package/platforms/claude/settings.template.json +36 -0
  285. package/rules/common/patterns.md +1 -1
  286. package/rules/common/release-gates.md +4 -3
  287. package/scripts/sync-specialists.mjs +41 -28
  288. package/skills/devops/data-engineering.md +1 -1
  289. package/skills/docs/adr-workflow.md +1 -0
  290. package/skills/docs/backlog-proposal-workflow.md +1 -0
  291. package/skills/docs/codebase-research-workflow.md +40 -0
  292. package/skills/docs/customer-profile-workflow.md +1 -0
  293. package/skills/docs/document-ingest-workflow.md +1 -0
  294. package/skills/docs/evidence-ingest-workflow.md +1 -0
  295. package/skills/docs/init-docs.md +2 -2
  296. package/skills/docs/init-project.md +7 -2
  297. package/skills/docs/prd-workflow.md +24 -1
  298. package/skills/docs/prfaq-workflow.md +1 -0
  299. package/skills/docs/product-intelligence-workflow.md +1 -0
  300. package/skills/docs/product-signal-workflow.md +1 -0
  301. package/skills/docs/research-workflow.md +54 -37
  302. package/skills/docs/runbook-workflow.md +1 -0
  303. package/skills/docs/strategy-workflow.md +1 -0
  304. package/skills/docs/user-research-workflow.md +40 -0
  305. package/skills/operating/orchestration-reference.md +1 -1
  306. package/skills/roles/architect.md +5 -0
  307. package/skills/roles/operator.docs.md +4 -0
  308. package/skills/routing.md +4 -2
  309. package/specialists/artifact-manifest.json +489 -0
  310. package/specialists/artifact-manifest.schema.json +83 -0
  311. package/specialists/audit-enrichments.json +454 -0
  312. package/specialists/contracts.json +37 -25
  313. package/specialists/prompts/_shared/validation-contract.md +26 -0
  314. package/specialists/prompts/cx-accessibility.md +21 -2
  315. package/specialists/prompts/cx-ai-engineer.md +24 -1
  316. package/specialists/prompts/cx-architect.md +4 -1
  317. package/specialists/prompts/cx-business-strategist.md +23 -2
  318. package/specialists/prompts/cx-data-analyst.md +22 -1
  319. package/specialists/prompts/cx-data-engineer.md +23 -2
  320. package/specialists/prompts/cx-debugger.md +21 -2
  321. package/specialists/prompts/cx-designer.md +26 -3
  322. package/specialists/prompts/cx-devil-advocate.md +19 -2
  323. package/specialists/prompts/cx-docs-keeper.md +28 -1
  324. package/specialists/prompts/cx-engineer.md +22 -1
  325. package/specialists/prompts/cx-evaluator.md +18 -1
  326. package/specialists/prompts/cx-explorer.md +21 -2
  327. package/specialists/prompts/cx-legal-compliance.md +21 -2
  328. package/specialists/prompts/cx-operations.md +21 -2
  329. package/specialists/prompts/cx-oracle.md +94 -0
  330. package/specialists/prompts/cx-orchestrator.md +20 -1
  331. package/specialists/prompts/cx-platform-engineer.md +25 -2
  332. package/specialists/prompts/cx-product-manager.md +32 -1
  333. package/specialists/prompts/cx-qa.md +24 -2
  334. package/specialists/prompts/cx-rd-lead.md +23 -2
  335. package/specialists/prompts/cx-release-manager.md +23 -2
  336. package/specialists/prompts/cx-researcher.md +22 -2
  337. package/specialists/prompts/cx-reviewer.md +18 -1
  338. package/specialists/prompts/cx-security.md +22 -2
  339. package/specialists/prompts/cx-sre.md +30 -3
  340. package/specialists/prompts/cx-test-automation.md +7 -1
  341. package/specialists/prompts/cx-trace-reviewer.md +22 -3
  342. package/specialists/prompts/cx-ux-researcher.md +21 -2
  343. package/specialists/registry.json +52 -173
  344. package/specialists/tone-profiles.json +42 -0
  345. package/templates/demos/playwright/demo-recording.config.mjs +47 -0
  346. package/templates/demos/recordings/agentic-platforms-prd.json +19 -0
  347. package/templates/demos/scripts/agentic-platforms-prd.json +44 -0
  348. package/templates/demos/specs/_helpers/scroll-artifact.ts +89 -0
  349. package/templates/demos/tapes/agentic-platforms-prd.tape +49 -0
  350. package/templates/demos/tapes/resource-guard-rails.tape +49 -0
  351. package/templates/demos/vhs/construct-cockpit.json +24 -0
  352. package/templates/distribution/construct-brand.typ +446 -0
  353. package/templates/distribution/construct-decision.typ +38 -0
  354. package/templates/distribution/construct-deck.html +95 -0
  355. package/templates/distribution/construct-pdf.typ +38 -0
  356. package/templates/distribution/construct-prd.typ +38 -0
  357. package/templates/distribution/construct-reference.docx +0 -0
  358. package/templates/distribution/construct-research.typ +38 -0
  359. package/templates/distribution/construct-web.html +92 -0
  360. package/templates/distribution/fonts/Geist-Bold.ttf +0 -0
  361. package/templates/distribution/fonts/Geist-Medium.ttf +0 -0
  362. package/templates/distribution/fonts/Geist-Regular.ttf +0 -0
  363. package/templates/distribution/fonts/Geist-SemiBold.ttf +0 -0
  364. package/templates/distribution/fonts/GeistMono-Medium.ttf +0 -0
  365. package/templates/distribution/fonts/GeistMono-Regular.ttf +0 -0
  366. package/templates/distribution/fonts/GeistMono-SemiBold.ttf +0 -0
  367. package/templates/distribution/fonts/IBMPlexMono-Regular.otf +0 -0
  368. package/templates/distribution/fonts/JetBrainsMono-Medium.ttf +0 -0
  369. package/templates/distribution/fonts/JetBrainsMono-Regular.ttf +0 -0
  370. package/templates/distribution/fonts/JetBrainsMono-SemiBold.ttf +0 -0
  371. package/templates/distribution/fonts/PlusJakartaSans-Bold.ttf +0 -0
  372. package/templates/distribution/fonts/PlusJakartaSans-Medium.ttf +0 -0
  373. package/templates/distribution/fonts/PlusJakartaSans-Regular.ttf +0 -0
  374. package/templates/distribution/fonts/PlusJakartaSans-SemiBold.ttf +0 -0
  375. package/templates/distribution/fonts/README.md +36 -0
  376. package/templates/distribution/fonts/SpaceGrotesk-Variable.ttf +0 -0
  377. package/templates/distribution/fonts/handwritten/Caveat.ttf +0 -0
  378. package/templates/distribution/fonts/legacy/IBMPlexMono-Regular.otf +0 -0
  379. package/templates/distribution/fonts/legacy/Inter-Medium.otf +0 -0
  380. package/templates/distribution/fonts/legacy/Inter-Regular.otf +0 -0
  381. package/templates/distribution/fonts/legacy/Inter-SemiBold.otf +0 -0
  382. package/templates/distribution/fonts/legacy/InterDisplay-SemiBold.otf +0 -0
  383. package/templates/distribution/fonts/legacy/SourceSerif4-Regular.otf +0 -0
  384. package/templates/distribution/fonts/legacy/SourceSerif4-Semibold.otf +0 -0
  385. package/templates/distribution/icons/activity.svg +15 -0
  386. package/templates/distribution/icons/alert-triangle.svg +17 -0
  387. package/templates/distribution/icons/book-open.svg +16 -0
  388. package/templates/distribution/icons/bot.svg +20 -0
  389. package/templates/distribution/icons/brain.svg +22 -0
  390. package/templates/distribution/icons/circle-check.svg +16 -0
  391. package/templates/distribution/icons/clipboard-check.svg +17 -0
  392. package/templates/distribution/icons/cpu.svg +28 -0
  393. package/templates/distribution/icons/database.svg +17 -0
  394. package/templates/distribution/icons/eye.svg +16 -0
  395. package/templates/distribution/icons/file-text.svg +19 -0
  396. package/templates/distribution/icons/gauge.svg +16 -0
  397. package/templates/distribution/icons/git-branch.svg +17 -0
  398. package/templates/distribution/icons/key.svg +17 -0
  399. package/templates/distribution/icons/layers.svg +17 -0
  400. package/templates/distribution/icons/list-checks.svg +19 -0
  401. package/templates/distribution/icons/lock.svg +16 -0
  402. package/templates/distribution/icons/message-square.svg +15 -0
  403. package/templates/distribution/icons/network.svg +19 -0
  404. package/templates/distribution/icons/route.svg +17 -0
  405. package/templates/distribution/icons/scale.svg +19 -0
  406. package/templates/distribution/icons/search.svg +16 -0
  407. package/templates/distribution/icons/send.svg +16 -0
  408. package/templates/distribution/icons/server.svg +18 -0
  409. package/templates/distribution/icons/shield-check.svg +16 -0
  410. package/templates/distribution/icons/users.svg +18 -0
  411. package/templates/distribution/icons/webhook.svg +17 -0
  412. package/templates/distribution/icons/workflow.svg +17 -0
  413. package/templates/distribution/icons/wrench.svg +15 -0
  414. package/templates/distribution/run.mjs +18 -1
  415. package/templates/docs/adr.md +7 -0
  416. package/templates/docs/construct_guide.md +8 -8
  417. package/templates/docs/customer-profile.md +4 -0
  418. package/templates/docs/one-pager.md +4 -0
  419. package/templates/docs/postmortem.md +31 -0
  420. package/templates/docs/prd-platform.md +19 -0
  421. package/templates/docs/prd.md +15 -0
  422. package/templates/docs/prfaq.md +7 -0
  423. package/templates/docs/qa-strategy.md +103 -0
  424. package/templates/docs/research-brief.md +8 -0
  425. package/templates/docs/rfc-platform.md +12 -0
  426. package/templates/docs/test-plan.md +7 -0
@@ -0,0 +1,1152 @@
1
+ /**
2
+ * lib/deck-export-pptx.mjs — markdown slide deck → branded PPTX via pptxgenjs.
3
+ *
4
+ * Parses slides (---), tables, lists, and inline bold into a 16:9 deck with
5
+ * Construct ink chrome: title band, heading rules, callout cards, styled tables,
6
+ * and footer bar matching templates/distribution/construct-deck.html.
7
+ */
8
+
9
+ import fs from 'node:fs';
10
+ import os from 'node:os';
11
+ import { createRequire } from 'node:module';
12
+ import { spawnSync, execSync } from 'node:child_process';
13
+ import { fileURLToPath, pathToFileURL } from 'node:url';
14
+ import path from 'node:path';
15
+ import { BRAND_TOKENS, INK } from './brand-tokens.mjs';
16
+ import { createPptxGenerator, embedBundledSansInPptx, embedBundledMonoInPptx, BRAND_SANS_FAMILY } from './brand-fonts.mjs';
17
+ import { parseArtifactMetadata } from './publish-template.mjs';
18
+
19
+ const require = createRequire(import.meta.url);
20
+ const MODULE_URL = pathToFileURL(fileURLToPath(import.meta.url)).href;
21
+ const REPO_ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
22
+
23
+ const EMU_PER_IN = 914400;
24
+
25
+ const W = 10;
26
+ const H = 5.625;
27
+ const MX = 0.54;
28
+ const MY = 0.41;
29
+ const CW = W - MX * 2;
30
+ const FOOT_BAR_H = 0.045;
31
+ const FOOT_Y = H - FOOT_BAR_H - 0.34;
32
+ const CONTENT_TOP = 1.02;
33
+ const CONTENT_MAX_Y = FOOT_Y - 0.1;
34
+ const SAFE_RIGHT = W - MX;
35
+ export const SLIDE_CONTENT_BUDGET_IN = CONTENT_MAX_Y - CONTENT_TOP;
36
+ export const SLIDE_W_IN = W;
37
+ export const SLIDE_H_IN = H;
38
+
39
+ const CARD_GAP = 0.04;
40
+ const CARD_PAD = 0.11;
41
+ const CARD_INK_BAR = 0.045;
42
+ const CARD_BADGE_W = 0.22;
43
+ const CARD_MIN_H = 0.28;
44
+
45
+ function ptSize(token) {
46
+ return parseFloat(String(token || '').replace('pt', '')) || 10;
47
+ }
48
+
49
+ const T = Object.freeze({
50
+ micro: ptSize(BRAND_TOKENS.typography.size.micro),
51
+ small: ptSize(BRAND_TOKENS.typography.size.small),
52
+ meta: ptSize(BRAND_TOKENS.typography.size.meta),
53
+ body: ptSize(BRAND_TOKENS.typography.size.body),
54
+ h3: ptSize(BRAND_TOKENS.typography.size.h3),
55
+ h2: ptSize(BRAND_TOKENS.typography.size.h2),
56
+ h1: ptSize(BRAND_TOKENS.typography.size.h1),
57
+ subtitle: ptSize(BRAND_TOKENS.typography.size.subtitle),
58
+ deckTitle: 26,
59
+ slideTitle: 20,
60
+ });
61
+
62
+ const INK_RAMP_STOPS = [
63
+ INK.ink,
64
+ INK.inkStrong,
65
+ INK.muted,
66
+ INK.faint,
67
+ INK.hairline,
68
+ INK.surface,
69
+ ];
70
+
71
+ function stripInlineMarkdown(text) {
72
+ return String(text || '')
73
+ .replace(/!\[[^\]]*]\([^)]+\)/g, '')
74
+ .replace(/\[([^\]]+)]\([^)]+\)/g, '$1')
75
+ .replace(/`([^`]+)`/g, '$1')
76
+ .replace(/\*\*([^*]+)\*\*/g, '$1')
77
+ .replace(/\*([^*]+)\*/g, '$1')
78
+ .trim();
79
+ }
80
+
81
+ function cellPlainLength(cell) {
82
+ return stripInlineMarkdown(cell).length;
83
+ }
84
+
85
+ function charsPerInchAtFont(fontSize, mono = false) {
86
+ const factor = mono ? 0.52 : 0.68;
87
+ return Math.max(6, Math.floor(fontSize * factor));
88
+ }
89
+
90
+ function itemUsesMono(text) {
91
+ return /`[^`]+`/.test(String(text || ''));
92
+ }
93
+
94
+ // One wrapped line occupies the leading that addWrappedText actually applies:
95
+ // lineSpacing = fontSize * 1.45 pt, i.e. fontSize * 1.45 / 72 inches. The prior
96
+ // 0.038x + 0.065 over-allocated ~2x, inflating table rows and text boxes until
97
+ // content bled past the footer band.
98
+
99
+ function lineHeightIn(fontSize) {
100
+ return (fontSize * 1.45) / 72;
101
+ }
102
+
103
+ function clampBox(x, y, w, h, { minW = 0.4, minH = 0.14 } = {}) {
104
+ const cx = Math.max(MX, x);
105
+ let cw = w;
106
+ if (cx + cw > SAFE_RIGHT) cw = SAFE_RIGHT - cx;
107
+ cw = Math.max(minW, cw);
108
+ let ch = h;
109
+ if (y + ch > CONTENT_MAX_Y) ch = CONTENT_MAX_Y - y;
110
+ ch = Math.max(minH, ch);
111
+ return { x: cx, y, w: cw, h: ch };
112
+ }
113
+
114
+ function textBoxHeight(text, widthIn, fontSize, padding = 0.12) {
115
+ const mono = itemUsesMono(text);
116
+ const lines = wrappedLineCount(text, widthIn, fontSize, mono);
117
+ return (padding + lines * lineHeightIn(fontSize)) * 1.2;
118
+ }
119
+
120
+ function wrappedLineCount(text, colWidthIn, fontSize, mono = itemUsesMono(text)) {
121
+ const plain = stripInlineMarkdown(text);
122
+ if (!plain) return 1;
123
+ const cpi = charsPerInchAtFont(fontSize, mono);
124
+ const charsPerLine = Math.max(8, Math.floor(colWidthIn * cpi));
125
+ return Math.max(1, Math.ceil(plain.length / charsPerLine));
126
+ }
127
+
128
+ function listTextWidth(layout, block) {
129
+ const numberedInset = block.type === 'number' ? CARD_BADGE_W + 0.18 : 0;
130
+ return layout.w - CARD_INK_BAR - CARD_PAD - numberedInset - 0.1;
131
+ }
132
+
133
+ function cardHeightForItem(item, layout, block, fontSize = T.body) {
134
+ const textW = listTextWidth(layout, block);
135
+ return Math.max(CARD_MIN_H, textBoxHeight(item, textW, fontSize, 0.12));
136
+ }
137
+
138
+ function contentRegionForTitle(title) {
139
+ return { x: MX, w: CW, mode: slideVisualMode(title) };
140
+ }
141
+
142
+ function textShapeOpts(extra = {}) {
143
+ return {
144
+ wrap: true,
145
+ fit: 'shrink',
146
+ valign: 'top',
147
+ inset: 0.06,
148
+ ...extra,
149
+ };
150
+ }
151
+
152
+ function addWrappedText(slide, text, box, fontSize, extra = {}) {
153
+ const plain = stripInlineMarkdown(text);
154
+ const sized = clampBox(box.x, box.y, box.w, box.h);
155
+ slide.addText(plain, {
156
+ ...sized,
157
+ ...textDefaults({ fontSize, ...extra }),
158
+ ...textShapeOpts(),
159
+ lineSpacing: Math.round(fontSize * 1.45),
160
+ });
161
+ }
162
+
163
+ function tableFontSize(headers, rows) {
164
+ const maxLen = Math.max(
165
+ 0,
166
+ ...headers.map(cellPlainLength),
167
+ ...rows.flat().map(cellPlainLength),
168
+ );
169
+ if (maxLen > 72) return 9;
170
+ if (maxLen > 48) return 10;
171
+ if (maxLen > 32) return 10;
172
+ return 11;
173
+ }
174
+
175
+ function computeTableColWidths(headers, rows) {
176
+ const colCount = headers.length;
177
+ if (colCount === 0) return [];
178
+ if (colCount === 1) return [CW];
179
+
180
+ const maxLens = Array.from({ length: colCount }, (_, ci) => Math.max(
181
+ cellPlainLength(headers[ci] || ''),
182
+ ...rows.map((row) => cellPlainLength(row[ci] || '')),
183
+ ));
184
+
185
+ if (colCount === 2) {
186
+ const total = maxLens[0] + maxLens[1] || 1;
187
+ const leftRatio = Math.max(0.18, Math.min(0.34, maxLens[0] / total));
188
+ return [CW * leftRatio, CW * (1 - leftRatio)];
189
+ }
190
+
191
+ const sum = maxLens.reduce((a, b) => a + b, 0) || colCount;
192
+ return maxLens.map((len) => CW * Math.max(0.12, len / sum));
193
+ }
194
+
195
+ // Single source of truth for row heights so the pre-export audit and the render
196
+ // agree. headerFont mirrors addTableBlock (one point smaller, floored at 9).
197
+
198
+ function tableRowHeights(block, colW, headerFont, bodyFont) {
199
+ const heights = [
200
+ Math.max(0.32, ...block.headers.map((cell, ci) => textBoxHeight(cell, (colW[ci] || CW) - 0.12, headerFont, 0.1))),
201
+ ];
202
+ for (const row of block.rows) {
203
+ let rh = 0.3;
204
+ row.forEach((cell, ci) => {
205
+ rh = Math.max(rh, textBoxHeight(cell, (colW[ci] || CW) - 0.12, bodyFont, 0.1));
206
+ });
207
+ heights.push(rh);
208
+ }
209
+ return heights;
210
+ }
211
+
212
+ function estimateTableHeight(block, colW, fontSize) {
213
+ const headerFont = Math.max(9, fontSize - 1);
214
+ const rowHeights = tableRowHeights(block, colW, headerFont, fontSize);
215
+ return rowHeights.reduce((sum, h) => sum + h, 0) + 0.1;
216
+ }
217
+
218
+ function estimateListHeight(block, layout) {
219
+ const fontSize = T.body;
220
+ let total = 0;
221
+ for (const item of block.items) {
222
+ total += cardHeightForItem(item, layout, block, fontSize) + CARD_GAP;
223
+ }
224
+ return total;
225
+ }
226
+
227
+ function estimateBlockHeight(block, layout = { w: CW }) {
228
+ if (block.type === 'heading') return 0;
229
+ if (block.type === 'text') {
230
+ const len = cellPlainLength(block.text);
231
+ const fontSize = len > 140 ? T.small : T.body;
232
+ return Math.min(2.2, textBoxHeight(block.text, layout.w, fontSize, 0.1));
233
+ }
234
+ if (block.type === 'bullet' || block.type === 'number') {
235
+ return Math.min(4.5, estimateListHeight(block, layout));
236
+ }
237
+ if (block.type === 'table') {
238
+ const colWidths = computeTableColWidths(block.headers, block.rows);
239
+ const fontSize = tableFontSize(block.headers, block.rows);
240
+ return estimateTableHeight(block, colWidths, fontSize) + 0.16;
241
+ }
242
+ return 0;
243
+ }
244
+
245
+ /**
246
+ * Pre-export layout audit: estimates vertical budget and horizontal wrap risk per slide.
247
+ * Returns structured issues so tests and export can fail closed before shipping bleed.
248
+ */
249
+ export function auditDeckMarkdownLayout(markdown, metadata = {}) {
250
+ const allChunks = splitSlides(String(markdown || ''));
251
+ if (allChunks.length === 0) {
252
+ return { ok: false, issues: [{ slide: 0, code: 'no_slides', detail: 'no slide chunks found' }], slides: [] };
253
+ }
254
+
255
+ let contentChunks = allChunks;
256
+ if (isTitleOnlyChunk(allChunks[0], metadata)) {
257
+ contentChunks = allChunks.slice(1);
258
+ }
259
+
260
+ const issues = [];
261
+ const slides = contentChunks.map((chunk, index) => {
262
+ const slideIndex = index + 2;
263
+ const blocks = slideBlocks(chunk);
264
+ const titleBlock = blocks.find((b) => b.type === 'heading');
265
+ const layout = contentRegionForTitle(titleBlock?.text || '');
266
+ let y = 0;
267
+ const slideIssues = [];
268
+
269
+ for (const block of blocks) {
270
+ if (block === titleBlock || block.type === 'heading') continue;
271
+
272
+ if (block.type === 'table') {
273
+ const colW = computeTableColWidths(block.headers, block.rows);
274
+ const fontSize = tableFontSize(block.headers, block.rows);
275
+ block.rows.forEach((row, ri) => {
276
+ row.forEach((cell, ci) => {
277
+ const lines = wrappedLineCount(cell, colW[ci], fontSize);
278
+ if (lines > 5) {
279
+ slideIssues.push({
280
+ code: 'table_cell_wrap_excess',
281
+ detail: `row ${ri + 1} col ${ci + 1} needs ${lines} lines at ${fontSize}pt`,
282
+ });
283
+ }
284
+ });
285
+ });
286
+ }
287
+
288
+ if (block.type === 'text' && cellPlainLength(block.text) > 200) {
289
+ slideIssues.push({
290
+ code: 'text_dense',
291
+ detail: `paragraph length ${cellPlainLength(block.text)} may need a slimmer slide or continuation`,
292
+ });
293
+ }
294
+
295
+ y += estimateBlockHeight(block, layout);
296
+ }
297
+
298
+ if (layout.mode === 'ink-panel') {
299
+ const panelBottom = 0.28 + 0.05 + 0.18 + 0.38 + 0.72;
300
+ if (panelBottom > SLIDE_CONTENT_BUDGET_IN * 0.55) {
301
+ slideIssues.push({
302
+ code: 'panel_tall',
303
+ detail: 'ink panel competes with stacked list content',
304
+ });
305
+ }
306
+ }
307
+
308
+ if (y > SLIDE_CONTENT_BUDGET_IN + 0.02) {
309
+ slideIssues.push({
310
+ code: 'vertical_overflow',
311
+ detail: `estimated ${y.toFixed(2)}in > budget ${SLIDE_CONTENT_BUDGET_IN.toFixed(2)}in`,
312
+ });
313
+ }
314
+
315
+ for (const issue of slideIssues) issues.push({ slide: slideIndex, ...issue });
316
+ return { slideIndex, estimatedHeightIn: Number(y.toFixed(3)), issues: slideIssues };
317
+ });
318
+
319
+ return { ok: issues.length === 0, issues, slides };
320
+ }
321
+
322
+ /**
323
+ * Post-export PPTX bounds audit: inspects text/table shape boxes in the content band.
324
+ */
325
+ export function readPptxSlideSizeIn(pptxPath) {
326
+ const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'pptx-size-'));
327
+ try {
328
+ execSync(`unzip -q ${JSON.stringify(pptxPath)} -d ${JSON.stringify(tmp)}`);
329
+ const pres = fs.readFileSync(path.join(tmp, 'ppt/presentation.xml'), 'utf8');
330
+ const m = pres.match(/<p:sldSz cx="(\d+)" cy="(\d+)"/);
331
+ if (!m) return { w: W, h: H };
332
+ return { w: +m[1] / EMU_PER_IN, h: +m[2] / EMU_PER_IN };
333
+ } finally {
334
+ fs.rmSync(tmp, { recursive: true, force: true });
335
+ }
336
+ }
337
+
338
+ export function auditPptxFile(pptxPath) {
339
+ const { w: slideW, h: slideH } = readPptxSlideSizeIn(pptxPath);
340
+ const safeRight = (slideW - MX) * EMU_PER_IN;
341
+ const contentTop = CONTENT_TOP * EMU_PER_IN;
342
+ const contentMax = CONTENT_MAX_Y * EMU_PER_IN;
343
+ const footerTop = FOOT_Y * EMU_PER_IN;
344
+ const bandPad = 0.02 * EMU_PER_IN;
345
+ const tol = 0.03 * EMU_PER_IN;
346
+ const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'pptx-audit-'));
347
+ const issues = [];
348
+
349
+ if (Math.abs(slideW - W) > 0.02 || Math.abs(slideH - H) > 0.02) {
350
+ issues.push({
351
+ slide: 0,
352
+ code: 'slide_size_mismatch',
353
+ detail: `expected ${W}x${H}, got ${slideW.toFixed(3)}x${slideH.toFixed(3)}`,
354
+ });
355
+ }
356
+
357
+ try {
358
+ execSync(`unzip -q ${JSON.stringify(pptxPath)} -d ${JSON.stringify(tmp)}`);
359
+ const slideDir = path.join(tmp, 'ppt/slides');
360
+ const slideFiles = fs.readdirSync(slideDir).filter((f) => /^slide\d+\.xml$/.test(f));
361
+
362
+ for (const file of slideFiles) {
363
+ const slideNum = +file.match(/\d+/)[0];
364
+ const xml = fs.readFileSync(path.join(slideDir, file), 'utf8');
365
+ if (xml.includes('[object Object]')) {
366
+ issues.push({ slide: slideNum, code: 'unresolved_rich_text', detail: file });
367
+ }
368
+ if (xml.includes('<a:graphicData uri="http://schemas.openxmlformats.org/drawingml/2006/table">')) {
369
+ issues.push({ slide: slideNum, code: 'native_table', detail: 'pptxgen tables lack cell wrap' });
370
+ }
371
+
372
+ // Scan every shape box, not a pptxgen-specific name. Header band (title,
373
+ // accents, heading rule) and footer chrome sit outside the content band and
374
+ // are skipped; anything else whose right/bottom exceeds the safe area is bleed.
375
+
376
+ const shapeRe = /<p:sp>([\s\S]*?)<\/p:sp>/g;
377
+ let m;
378
+ while ((m = shapeRe.exec(xml))) {
379
+ const inner = m[1];
380
+ const off = inner.match(/<a:off x="(-?\d+)" y="(-?\d+)"\/>\s*<a:ext cx="(\d+)" cy="(\d+)"\/>/);
381
+ if (!off) continue;
382
+ const x = +off[1];
383
+ const y = +off[2];
384
+ const right = x + +off[3];
385
+ const bottom = y + +off[4];
386
+ if (y < contentTop - bandPad || y > footerTop - bandPad) continue;
387
+ if (x < MX * EMU_PER_IN - bandPad) continue;
388
+ const textMatch = inner.match(/<a:t>([^<]*)<\/a:t>/);
389
+ const label = (textMatch ? textMatch[1] : 'shape').slice(0, 24) || 'shape';
390
+ if (right > safeRight + tol) {
391
+ issues.push({ slide: slideNum, code: 'horizontal_overflow', detail: `"${label}" right=${(right / EMU_PER_IN).toFixed(3)}in (max ${(slideW - MX).toFixed(3)})` });
392
+ }
393
+ if (bottom > contentMax + tol) {
394
+ issues.push({ slide: slideNum, code: 'vertical_overflow', detail: `"${label}" bottom=${(bottom / EMU_PER_IN).toFixed(3)}in (max ${CONTENT_MAX_Y.toFixed(3)})` });
395
+ }
396
+ }
397
+ }
398
+ } finally {
399
+ fs.rmSync(tmp, { recursive: true, force: true });
400
+ }
401
+
402
+ return { ok: issues.length === 0, issues };
403
+ }
404
+
405
+ function loadPptxGen() {
406
+ try {
407
+ require('pptxgenjs');
408
+ return true;
409
+ } catch {
410
+ return false;
411
+ }
412
+ }
413
+
414
+ export function pptxgenPresent() {
415
+ return loadPptxGen();
416
+ }
417
+
418
+ function hex(hexColor) {
419
+ return String(hexColor || '').replace(/^#/, '');
420
+ }
421
+
422
+ function splitSlides(markdown) {
423
+ let body = String(markdown || '');
424
+ if (body.startsWith('---')) {
425
+ const end = body.indexOf('\n---', 3);
426
+ if (end !== -1) body = body.slice(end + 4);
427
+ }
428
+ return body
429
+ .split(/\n(?:---|\*\*\*\*)\n/)
430
+ .map((chunk) => chunk.trim())
431
+ .filter(Boolean);
432
+ }
433
+
434
+ function parseInlineRuns(text, base = {}) {
435
+ const runs = [];
436
+ const src = String(text || '');
437
+ const re = /(\*\*[^*]+\*\*|`[^`]+`)/g;
438
+ let last = 0;
439
+ let m;
440
+ while ((m = re.exec(src)) !== null) {
441
+ if (m.index > last) {
442
+ runs.push({ text: src.slice(last, m.index), options: { ...base } });
443
+ }
444
+ const token = m[0];
445
+ if (token.startsWith('**')) {
446
+ runs.push({
447
+ text: token.slice(2, -2),
448
+ options: { ...base, bold: true, color: hex(BRAND_TOKENS.ink.default) },
449
+ });
450
+ } else {
451
+ runs.push({
452
+ text: token.slice(1, -1),
453
+ options: {
454
+ ...base,
455
+ fontFace: BRAND_TOKENS.typography.fontMono,
456
+ color: hex(BRAND_TOKENS.ink.strong),
457
+ },
458
+ });
459
+ }
460
+ last = m.index + token.length;
461
+ }
462
+ if (last < src.length) runs.push({ text: src.slice(last), options: { ...base } });
463
+ if (runs.length === 0) runs.push({ text: stripInlineMarkdown(src), options: { ...base } });
464
+ return runs;
465
+ }
466
+
467
+ function isTableLine(line) {
468
+ return /^\|.+\|$/.test(line.trim());
469
+ }
470
+
471
+ function isTableSeparator(line) {
472
+ return /^\|[-:\s|]+\|$/.test(line.trim());
473
+ }
474
+
475
+ function parseTableRow(line) {
476
+ return line
477
+ .trim()
478
+ .replace(/^\|/, '')
479
+ .replace(/\|$/, '')
480
+ .split('|')
481
+ .map((c) => c.trim());
482
+ }
483
+
484
+ function slideBlocks(chunk) {
485
+ const lines = chunk.split('\n');
486
+ const blocks = [];
487
+ let para = [];
488
+ let list = null;
489
+ let i = 0;
490
+
491
+ const flushPara = () => {
492
+ if (para.length) {
493
+ blocks.push({ type: 'text', text: para.join(' ') });
494
+ para = [];
495
+ }
496
+ };
497
+
498
+ const flushList = () => {
499
+ if (list?.items.length) blocks.push(list);
500
+ list = null;
501
+ };
502
+
503
+ while (i < lines.length) {
504
+ const raw = lines[i];
505
+ const line = raw.trimEnd();
506
+
507
+ if (isTableLine(line) && i + 1 < lines.length && isTableSeparator(lines[i + 1])) {
508
+ flushPara();
509
+ flushList();
510
+ const headers = parseTableRow(line);
511
+ i += 2;
512
+ const rows = [];
513
+ while (i < lines.length && isTableLine(lines[i])) {
514
+ rows.push(parseTableRow(lines[i]));
515
+ i += 1;
516
+ }
517
+ blocks.push({ type: 'table', headers, rows });
518
+ continue;
519
+ }
520
+
521
+ const bullet = line.match(/^[-*+]\s+(.+)/);
522
+ const numbered = line.match(/^\d+\.\s+(.+)/);
523
+ const heading = line.match(/^(#{1,3})\s+(.+)/);
524
+
525
+ if (heading) {
526
+ flushPara();
527
+ flushList();
528
+ blocks.push({ type: 'heading', level: heading[1].length, text: stripInlineMarkdown(heading[2]) });
529
+ i += 1;
530
+ continue;
531
+ }
532
+ if (bullet) {
533
+ flushPara();
534
+ if (!list || list.type !== 'bullet') {
535
+ flushList();
536
+ list = { type: 'bullet', items: [] };
537
+ }
538
+ list.items.push(bullet[1]);
539
+ i += 1;
540
+ continue;
541
+ }
542
+ if (numbered) {
543
+ flushPara();
544
+ if (!list || list.type !== 'number') {
545
+ flushList();
546
+ list = { type: 'number', items: [] };
547
+ }
548
+ list.items.push(numbered[1]);
549
+ i += 1;
550
+ continue;
551
+ }
552
+ if (line.trim() === '') {
553
+ flushPara();
554
+ flushList();
555
+ i += 1;
556
+ continue;
557
+ }
558
+ flushList();
559
+ para.push(line.trim());
560
+ i += 1;
561
+ }
562
+ flushPara();
563
+ flushList();
564
+ return blocks;
565
+ }
566
+
567
+ export function isTitleOnlyChunk(chunk, metadata = {}) {
568
+ const blocks = slideBlocks(chunk);
569
+ const headings = blocks.filter((b) => b.type === 'heading');
570
+ if (headings.some((h) => h.level > 1)) return false;
571
+ if (blocks.some((b) => b.type === 'bullet' || b.type === 'number' || b.type === 'table')) return false;
572
+ const h1 = headings.find((h) => h.level === 1);
573
+ if (!h1) return false;
574
+ if (metadata.title && h1.text.toLowerCase() !== metadata.title.toLowerCase()) return false;
575
+ return true;
576
+ }
577
+
578
+ function textDefaults(opts = {}) {
579
+ return {
580
+ fontFace: BRAND_TOKENS.typography.fontSans,
581
+ color: hex(opts.color || BRAND_TOKENS.ink.body),
582
+ fontSize: opts.fontSize ?? T.body,
583
+ bold: Boolean(opts.bold),
584
+ italic: Boolean(opts.italic),
585
+ };
586
+ }
587
+
588
+ function slideVisualMode(title) {
589
+ const lower = String(title || '').toLowerCase();
590
+ if (lower.includes('branded')) return 'ink-panel';
591
+ if (lower.includes('what construct')) return 'feature-grid';
592
+ return 'default';
593
+ }
594
+
595
+ function addSlideAccent(slide) {
596
+ INK_RAMP_STOPS.slice(0, 4).forEach((color, i) => {
597
+ const size = 0.08;
598
+ const x = SAFE_RIGHT - size - i * 0.11;
599
+ slide.addShape('rect', {
600
+ x,
601
+ y: MY + 0.04,
602
+ w: size,
603
+ h: size,
604
+ fill: { color: hex(color) },
605
+ line: { color: hex(color), width: 0 },
606
+ });
607
+ });
608
+ }
609
+
610
+ function addInkRampVisual(slide, x, y, w, h) {
611
+ const box = clampBox(x, y, w, h);
612
+ const segW = box.w / INK_RAMP_STOPS.length;
613
+ INK_RAMP_STOPS.forEach((color, i) => {
614
+ slide.addShape('rect', {
615
+ x: box.x + i * segW,
616
+ y: box.y,
617
+ w: segW,
618
+ h: box.h,
619
+ fill: { color: hex(color) },
620
+ line: { color: hex(color), width: 0 },
621
+ });
622
+ });
623
+ const label = clampBox(box.x, box.y + box.h + 0.04, box.w, 0.16);
624
+ slide.addText('INK RAMP', {
625
+ ...label,
626
+ ...textDefaults({ color: BRAND_TOKENS.ink.faint, fontSize: T.micro, bold: true }),
627
+ charSpacing: 1.2,
628
+ });
629
+ }
630
+
631
+ function addTypographyPanel(slide, pptx, x, y, w) {
632
+ const box = clampBox(x, y, w, 0.68);
633
+ slide.addShape(pptx.shapes.RECTANGLE, {
634
+ ...box,
635
+ fill: { color: hex(BRAND_TOKENS.surface.alt) },
636
+ line: { color: hex(BRAND_TOKENS.line.hairline), width: 0.75 },
637
+ rectRadius: 0.05,
638
+ });
639
+ slide.addText('Aa', {
640
+ x: box.x + 0.12,
641
+ y: box.y + 0.08,
642
+ w: 0.42,
643
+ h: 0.3,
644
+ ...textDefaults({ color: BRAND_TOKENS.ink.default, fontSize: 18, bold: true }),
645
+ });
646
+ slide.addText(BRAND_TOKENS.typography.fontSans, {
647
+ x: box.x + 0.5,
648
+ y: box.y + 0.1,
649
+ w: box.w - 0.58,
650
+ h: 0.2,
651
+ ...textDefaults({ color: BRAND_TOKENS.ink.strong, fontSize: T.small, bold: true }),
652
+ wrap: true,
653
+ fit: 'shrink',
654
+ });
655
+ slide.addText('const brand = tokens.ink;', {
656
+ x: box.x + 0.12,
657
+ y: box.y + 0.38,
658
+ w: box.w - 0.2,
659
+ h: 0.22,
660
+ fontFace: BRAND_TOKENS.typography.fontMono,
661
+ color: hex(BRAND_TOKENS.ink.muted),
662
+ fontSize: T.small,
663
+ wrap: true,
664
+ fit: 'shrink',
665
+ });
666
+ return box.y + box.h;
667
+ }
668
+
669
+ function addSlideChrome(slide, pptx, { slideIndex, totalSlides, title = '' }) {
670
+ slide.background = { color: hex(BRAND_TOKENS.surface.paper) };
671
+
672
+ slide.addShape(pptx.shapes.RECTANGLE, {
673
+ x: 0,
674
+ y: H - FOOT_BAR_H,
675
+ w: 0.42,
676
+ h: FOOT_BAR_H,
677
+ fill: { color: hex(BRAND_TOKENS.ink.default) },
678
+ line: { color: hex(BRAND_TOKENS.ink.default), width: 0 },
679
+ });
680
+ slide.addShape(pptx.shapes.RECTANGLE, {
681
+ x: 0.42,
682
+ y: H - FOOT_BAR_H,
683
+ w: W - 0.42,
684
+ h: FOOT_BAR_H,
685
+ fill: { color: hex(BRAND_TOKENS.line.hairline) },
686
+ line: { color: hex(BRAND_TOKENS.line.hairline), width: 0 },
687
+ });
688
+
689
+ slide.addShape(pptx.shapes.RECTANGLE, {
690
+ x: 0,
691
+ y: 0,
692
+ w: 0.06,
693
+ h: H - FOOT_BAR_H,
694
+ fill: { color: hex(BRAND_TOKENS.surface.alt) },
695
+ line: { color: hex(BRAND_TOKENS.surface.alt), width: 0 },
696
+ });
697
+
698
+ slide.addText('Construct', {
699
+ x: MX,
700
+ y: FOOT_Y,
701
+ w: 1.6,
702
+ h: 0.28,
703
+ ...textDefaults({ color: BRAND_TOKENS.ink.default, fontSize: 10, bold: true }),
704
+ });
705
+
706
+ if (title) {
707
+ const footTitle = title.length > 34 ? `${title.slice(0, 32)}…` : title;
708
+ slide.addText(footTitle, {
709
+ x: MX + 1.5,
710
+ y: FOOT_Y,
711
+ w: CW - 2.4,
712
+ h: 0.28,
713
+ ...textDefaults({ color: BRAND_TOKENS.ink.faint, fontSize: 9 }),
714
+ align: 'left',
715
+ ...textShapeOpts(),
716
+ });
717
+ }
718
+
719
+ slide.addText(`${slideIndex} / ${totalSlides}`, {
720
+ x: W - MX - 0.9,
721
+ y: FOOT_Y,
722
+ w: 0.9,
723
+ h: 0.28,
724
+ align: 'right',
725
+ ...textDefaults({ color: BRAND_TOKENS.ink.faint, fontSize: 9 }),
726
+ });
727
+ }
728
+
729
+ function addTitleSlide(pptx, metadata = {}, totalSlides) {
730
+ const slide = pptx.addSlide();
731
+ addSlideChrome(slide, pptx, { slideIndex: 1, totalSlides, title: metadata.title || '' });
732
+
733
+ slide.addShape(pptx.shapes.RECTANGLE, {
734
+ x: MX,
735
+ y: 0.86,
736
+ w: CW * 0.42,
737
+ h: 0.035,
738
+ fill: { color: hex(BRAND_TOKENS.ink.default) },
739
+ line: { color: hex(BRAND_TOKENS.ink.default), width: 0 },
740
+ });
741
+
742
+ const chips = [
743
+ metadata.artifactType,
744
+ metadata.docId,
745
+ metadata.version ? `v${metadata.version}` : '',
746
+ ].filter(Boolean);
747
+
748
+ let y = 1.0;
749
+ if (chips.length) {
750
+ slide.addText(chips.join(' · ').toUpperCase(), {
751
+ x: MX,
752
+ y,
753
+ w: CW,
754
+ h: 0.28,
755
+ ...textDefaults({ color: BRAND_TOKENS.ink.muted, fontSize: 11, bold: true }),
756
+ charSpacing: 1.5,
757
+ });
758
+ y += 0.34;
759
+ }
760
+
761
+ slide.addText(metadata.title || 'Untitled', {
762
+ x: MX,
763
+ y: 1.52,
764
+ w: CW,
765
+ h: 0.82,
766
+ ...textDefaults({ color: BRAND_TOKENS.ink.default, fontSize: T.deckTitle, bold: true }),
767
+ valign: 'top',
768
+ });
769
+
770
+ if (metadata.subtitle) {
771
+ slide.addText(metadata.subtitle, {
772
+ x: MX,
773
+ y: 2.38,
774
+ w: CW * 0.82,
775
+ h: 0.5,
776
+ ...textDefaults({ color: BRAND_TOKENS.ink.muted, fontSize: T.subtitle }),
777
+ valign: 'top',
778
+ });
779
+ }
780
+
781
+ const byline = [metadata.owner, metadata.date, metadata.status].filter(Boolean).join(' · ');
782
+ if (byline) {
783
+ const chipW = Math.min(CW * 0.72, byline.length * 0.07 + 0.4);
784
+ const chip = clampBox(MX, 3.02, chipW, 0.3);
785
+ slide.addShape(pptx.shapes.RECTANGLE, {
786
+ ...chip,
787
+ fill: { color: hex(BRAND_TOKENS.surface.alt) },
788
+ line: { color: hex(BRAND_TOKENS.line.hairline), width: 0.75 },
789
+ rectRadius: 0.06,
790
+ });
791
+ slide.addText(byline, {
792
+ x: chip.x + 0.14,
793
+ y: chip.y + 0.06,
794
+ w: chip.w - 0.22,
795
+ h: chip.h - 0.1,
796
+ ...textDefaults({ color: BRAND_TOKENS.ink.muted, fontSize: T.meta }),
797
+ wrap: true,
798
+ fit: 'shrink',
799
+ });
800
+ }
801
+
802
+ INK_RAMP_STOPS.forEach((color, i) => {
803
+ const size = Math.max(0.12, 0.34 - i * 0.05);
804
+ const x = SAFE_RIGHT - size - i * 0.08;
805
+ slide.addShape('rect', {
806
+ x,
807
+ y: 0.9 + i * 0.025,
808
+ w: size,
809
+ h: size,
810
+ fill: { color: hex(color) },
811
+ line: { color: hex(color), width: 0 },
812
+ });
813
+ });
814
+ }
815
+
816
+ function addHeadingRule(slide, y) {
817
+ slide.addShape('rect', {
818
+ x: MX,
819
+ y,
820
+ w: 0.48,
821
+ h: 0.045,
822
+ fill: { color: hex(BRAND_TOKENS.ink.default) },
823
+ line: { color: hex(BRAND_TOKENS.ink.default), width: 0 },
824
+ });
825
+ }
826
+
827
+ function fontSizeForItem(item) {
828
+ const len = cellPlainLength(item);
829
+ if (itemUsesMono(item) && len > 42) return T.small;
830
+ if (len > 90) return T.small;
831
+ return T.body;
832
+ }
833
+
834
+ function addCallout(slide, pptx, text, y, layout = {}) {
835
+ const { x = MX, w = CW } = layout;
836
+ const fontSize = T.h3;
837
+ const innerW = w - 0.34;
838
+ const h = Math.min(1.35, textBoxHeight(text, innerW, fontSize, 0.14));
839
+ const box = clampBox(x, y, w, h);
840
+ slide.addShape(pptx.shapes.RECTANGLE, {
841
+ ...box,
842
+ fill: { color: hex(BRAND_TOKENS.surface.alt) },
843
+ line: { color: hex(BRAND_TOKENS.line.hairline), width: 0.75 },
844
+ rectRadius: 0.05,
845
+ });
846
+ slide.addShape(pptx.shapes.RECTANGLE, {
847
+ x: box.x,
848
+ y: box.y,
849
+ w: CARD_INK_BAR,
850
+ h: box.h,
851
+ fill: { color: hex(BRAND_TOKENS.ink.default) },
852
+ line: { color: hex(BRAND_TOKENS.ink.default), width: 0 },
853
+ });
854
+ addWrappedText(slide, text, {
855
+ x: box.x + 0.18,
856
+ y: box.y + 0.08,
857
+ w: box.w - 0.3,
858
+ h: box.h - 0.12,
859
+ }, fontSize);
860
+ return box.y + box.h + 0.12;
861
+ }
862
+
863
+ function addTableBlock(slide, pptx, block, y, maxY) {
864
+ const colCount = block.headers.length;
865
+ if (colCount === 0) return y;
866
+ const colW = computeTableColWidths(block.headers, block.rows);
867
+ const bodyFont = tableFontSize(block.headers, block.rows);
868
+ const headerFont = Math.max(9, bodyFont - 1);
869
+
870
+ const rowHeights = tableRowHeights(block, colW, headerFont, bodyFont);
871
+
872
+ let cy = y;
873
+ const paintRow = (cells, rowIndex, isHeader) => {
874
+ const rh = rowHeights[rowIndex];
875
+ if (cy + rh > maxY) return false;
876
+ let cx = MX;
877
+ for (let ci = 0; ci < cells.length; ci += 1) {
878
+ const fw = colW[ci];
879
+ const cellBox = clampBox(cx, cy, fw, rh);
880
+ const fill = isHeader
881
+ ? BRAND_TOKENS.ink.default
882
+ : (rowIndex % 2 === 0 ? BRAND_TOKENS.surface.paper : BRAND_TOKENS.surface.alt);
883
+ const fs = isHeader ? headerFont : bodyFont;
884
+ slide.addShape(pptx.shapes.RECTANGLE, {
885
+ ...cellBox,
886
+ fill: { color: hex(fill) },
887
+ line: { color: hex(BRAND_TOKENS.line.hairline), width: 0.75 },
888
+ });
889
+ addWrappedText(slide, cells[ci], {
890
+ x: cellBox.x + 0.08,
891
+ y: cellBox.y + 0.05,
892
+ w: cellBox.w - 0.14,
893
+ h: cellBox.h - 0.08,
894
+ }, fs, { bold: isHeader, color: isHeader ? INK.paper : BRAND_TOKENS.ink.body });
895
+ cx += fw;
896
+ }
897
+ cy += rh;
898
+ return true;
899
+ };
900
+
901
+ if (!paintRow(block.headers, 0, true)) return y;
902
+ for (let ri = 0; ri < block.rows.length; ri += 1) {
903
+ if (!paintRow(block.rows[ri], ri + 1, false)) break;
904
+ }
905
+ return cy + 0.1;
906
+ }
907
+
908
+ function addListCards(slide, pptx, block, y, maxY, layout = {}) {
909
+ const { x = MX, w = CW } = layout;
910
+ let cy = y;
911
+
912
+ for (let idx = 0; idx < block.items.length; idx += 1) {
913
+ const item = block.items[idx];
914
+ const fontSize = fontSizeForItem(item);
915
+ const cardH = cardHeightForItem(item, layout, block, fontSize);
916
+ if (cy + cardH > maxY) break;
917
+
918
+ const box = clampBox(x, cy, w, cardH);
919
+ slide.addShape(pptx.shapes.RECTANGLE, {
920
+ ...box,
921
+ fill: { color: hex(BRAND_TOKENS.surface.alt) },
922
+ line: { color: hex(BRAND_TOKENS.line.hairline), width: 0.75 },
923
+ rectRadius: 0.04,
924
+ });
925
+ slide.addShape(pptx.shapes.RECTANGLE, {
926
+ x: box.x,
927
+ y: box.y,
928
+ w: CARD_INK_BAR,
929
+ h: box.h,
930
+ fill: { color: hex(BRAND_TOKENS.ink.default) },
931
+ line: { color: hex(BRAND_TOKENS.ink.default), width: 0 },
932
+ });
933
+
934
+ let textX = box.x + CARD_PAD;
935
+ if (block.type === 'number') {
936
+ const badgeX = box.x + 0.1;
937
+ const badgeY = box.y + (box.h - CARD_BADGE_W) / 2;
938
+ slide.addShape(pptx.shapes.RECTANGLE, {
939
+ x: badgeX,
940
+ y: badgeY,
941
+ w: CARD_BADGE_W,
942
+ h: CARD_BADGE_W,
943
+ fill: { color: hex(BRAND_TOKENS.ink.default) },
944
+ line: { color: hex(BRAND_TOKENS.ink.default), width: 0 },
945
+ rectRadius: 0.03,
946
+ });
947
+ slide.addText(String(idx + 1), {
948
+ x: badgeX,
949
+ y: badgeY,
950
+ w: CARD_BADGE_W,
951
+ h: CARD_BADGE_W,
952
+ align: 'center',
953
+ valign: 'mid',
954
+ ...textDefaults({ color: INK.paper, fontSize: T.small, bold: true }),
955
+ });
956
+ textX = box.x + CARD_BADGE_W + 0.2;
957
+ }
958
+ const textW = Math.min(listTextWidth(layout, block), SAFE_RIGHT - textX - 0.08);
959
+ addWrappedText(slide, item, {
960
+ x: textX,
961
+ y: box.y + 0.06,
962
+ w: textW,
963
+ h: box.h - 0.1,
964
+ }, fontSize);
965
+
966
+ cy += box.h + CARD_GAP;
967
+ }
968
+
969
+ return cy + 0.04;
970
+ }
971
+
972
+ function addContentSlide(pptx, chunk, slideIndex, totalSlides, deckTitle) {
973
+ const slide = pptx.addSlide();
974
+ addSlideChrome(slide, pptx, { slideIndex, totalSlides, title: deckTitle });
975
+ addSlideAccent(slide);
976
+
977
+ const blocks = slideBlocks(chunk);
978
+ const titleBlock = blocks.find((b) => b.type === 'heading') || { text: `Slide ${slideIndex}`, level: 2 };
979
+ const region = contentRegionForTitle(titleBlock.text);
980
+ const contentLayout = { x: region.x, w: region.w };
981
+
982
+ const titleBox = clampBox(MX, MY, CW, 0.4);
983
+ slide.addText(titleBlock.text, {
984
+ ...titleBox,
985
+ ...textDefaults({ color: BRAND_TOKENS.ink.default, fontSize: T.slideTitle, bold: true }),
986
+ ...textShapeOpts(),
987
+ });
988
+ addHeadingRule(slide, MY + 0.44);
989
+
990
+ let y = CONTENT_TOP;
991
+ if (region.mode === 'ink-panel') {
992
+ addInkRampVisual(slide, MX, MY + 0.52, CW, 0.08);
993
+ y = MY + 0.64;
994
+ }
995
+
996
+ const maxY = CONTENT_MAX_Y;
997
+
998
+ for (const block of blocks) {
999
+ if (block === titleBlock || block.type === 'heading') continue;
1000
+ if (y >= maxY - 0.12) break;
1001
+
1002
+ if (block.type === 'text') {
1003
+ const plainLen = cellPlainLength(block.text);
1004
+ const fontSize = plainLen > 140 ? T.small : T.body;
1005
+ const isLead = y < CONTENT_TOP + 0.35;
1006
+ if (isLead && plainLen < 140) {
1007
+ y = addCallout(slide, pptx, block.text, y, contentLayout);
1008
+ } else {
1009
+ const estH = textBoxHeight(block.text, contentLayout.w, fontSize, 0.08);
1010
+ const box = clampBox(contentLayout.x, y, contentLayout.w, estH);
1011
+ addWrappedText(slide, block.text, box, fontSize);
1012
+ y = box.y + box.h + 0.1;
1013
+ }
1014
+ } else if (block.type === 'bullet' || block.type === 'number') {
1015
+ y = addListCards(slide, pptx, block, y, maxY, contentLayout);
1016
+ } else if (block.type === 'table') {
1017
+ y = addTableBlock(slide, pptx, block, y, maxY);
1018
+ }
1019
+ }
1020
+ }
1021
+
1022
+ export async function exportDeckPptxAsync({
1023
+ inputPath,
1024
+ outputPath,
1025
+ metadata: metadataOverride = null,
1026
+ repoRoot = REPO_ROOT,
1027
+ } = {}) {
1028
+ if (!loadPptxGen()) {
1029
+ return {
1030
+ ok: false,
1031
+ format: 'pptx',
1032
+ inputPath,
1033
+ missing: ['pptxgenjs'],
1034
+ message: 'Install pptxgenjs to enable PPTX export (`npm install pptxgenjs` in the Construct package).',
1035
+ };
1036
+ }
1037
+ if (!inputPath) {
1038
+ return { ok: false, format: 'pptx', message: 'exportDeckPptx: inputPath is required.' };
1039
+ }
1040
+ if (!fs.existsSync(inputPath)) {
1041
+ return { ok: false, format: 'pptx', inputPath, message: `exportDeckPptx: input does not exist: ${inputPath}` };
1042
+ }
1043
+
1044
+ const metadata = { ...(metadataOverride || parseArtifactMetadata(inputPath)) };
1045
+ const source = fs.readFileSync(inputPath, 'utf8');
1046
+ const layout = auditDeckMarkdownLayout(source, metadata);
1047
+ if (!layout.ok) {
1048
+ return {
1049
+ ok: false,
1050
+ format: 'pptx',
1051
+ inputPath,
1052
+ layout,
1053
+ message: `PPTX layout audit failed: ${layout.issues.map((i) => `slide ${i.slide} ${i.code}`).join('; ')}`,
1054
+ };
1055
+ }
1056
+
1057
+ const allChunks = splitSlides(source);
1058
+ if (allChunks.length === 0) {
1059
+ return { ok: false, format: 'pptx', inputPath, message: 'exportDeckPptx: no slides found (separate slides with ---).' };
1060
+ }
1061
+
1062
+ let contentChunks = allChunks;
1063
+ if (isTitleOnlyChunk(allChunks[0], metadata)) {
1064
+ const intro = slideBlocks(allChunks[0]).find((b) => b.type === 'text');
1065
+ if (intro && !metadata.subtitle) metadata.subtitle = stripInlineMarkdown(intro.text);
1066
+ contentChunks = allChunks.slice(1);
1067
+ }
1068
+
1069
+ const totalSlides = 1 + contentChunks.length;
1070
+ const { PptxClass } = createPptxGenerator();
1071
+ const pptx = new PptxClass();
1072
+ pptx.layout = 'LAYOUT_16x9';
1073
+ pptx.author = metadata.owner || 'Construct';
1074
+ pptx.title = metadata.title || 'Construct deck';
1075
+ pptx.subject = metadata.subtitle || '';
1076
+ pptx.theme = { headFontFace: BRAND_SANS_FAMILY, bodyFontFace: BRAND_SANS_FAMILY };
1077
+
1078
+ const fontEmbed = await embedBundledSansInPptx(pptx, repoRoot);
1079
+ await embedBundledMonoInPptx(pptx, repoRoot);
1080
+
1081
+ addTitleSlide(pptx, metadata, totalSlides);
1082
+ contentChunks.forEach((chunk, index) => {
1083
+ addContentSlide(pptx, chunk, index + 2, totalSlides, metadata.title || '');
1084
+ });
1085
+
1086
+ try {
1087
+ await pptx.writeFile({ fileName: outputPath });
1088
+ } catch (err) {
1089
+ return {
1090
+ ok: false,
1091
+ format: 'pptx',
1092
+ inputPath,
1093
+ outputPath,
1094
+ message: `PPTX export failed: ${err.message}`,
1095
+ };
1096
+ }
1097
+
1098
+ return {
1099
+ ok: true,
1100
+ format: 'pptx',
1101
+ inputPath,
1102
+ outputPath,
1103
+ engine: 'pptxgenjs',
1104
+ slideCount: totalSlides,
1105
+ layout,
1106
+ fontsEmbedded: fontEmbed.embedded,
1107
+ message: `Wrote ${outputPath}`,
1108
+ };
1109
+ }
1110
+
1111
+ export function exportDeckPptx(opts = {}) {
1112
+ const PptxGenJS = loadPptxGen();
1113
+ if (!PptxGenJS) {
1114
+ return {
1115
+ ok: false,
1116
+ format: 'pptx',
1117
+ inputPath: opts.inputPath,
1118
+ missing: ['pptxgenjs'],
1119
+ message: 'Install pptxgenjs to enable PPTX export (`npm install pptxgenjs` in the Construct package).',
1120
+ };
1121
+ }
1122
+
1123
+ const payload = Buffer.from(JSON.stringify(opts)).toString('base64');
1124
+ const child = spawnSync(process.execPath, [
1125
+ '--input-type=module',
1126
+ '-e',
1127
+ `import { exportDeckPptxAsync } from ${JSON.stringify(MODULE_URL)};
1128
+ const result = await exportDeckPptxAsync(JSON.parse(Buffer.from(${JSON.stringify(payload)}, 'base64').toString('utf8')));
1129
+ process.stdout.write(JSON.stringify(result));`,
1130
+ ], { encoding: 'utf8', env: process.env });
1131
+
1132
+ if (child.status !== 0) {
1133
+ return {
1134
+ ok: false,
1135
+ format: 'pptx',
1136
+ inputPath: opts.inputPath,
1137
+ outputPath: opts.outputPath,
1138
+ message: `PPTX export failed: ${(child.stderr || child.stdout || '').trim().slice(0, 400)}`,
1139
+ };
1140
+ }
1141
+
1142
+ try {
1143
+ return JSON.parse(child.stdout || '{}');
1144
+ } catch {
1145
+ return {
1146
+ ok: false,
1147
+ format: 'pptx',
1148
+ inputPath: opts.inputPath,
1149
+ message: 'PPTX export failed: invalid worker response.',
1150
+ };
1151
+ }
1152
+ }