@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
@@ -1,32 +1,136 @@
1
1
  /**
2
- * lib/document-export.mjs — markdown PDF / DOCX / HTML export via external
3
- * binaries (Pandoc + Typst), per ADR-0024.
2
+ * lib/document-export.mjs — artifact export to PDF / DOCX / DOC / deck / PPTX / HTML /
3
+ * RTF / ODT / EPUB / LaTeX / TXT / MD / MDX via external binaries (Pandoc + Typst,
4
+ * LibreOffice for legacy .doc, pptxgenjs for PPTX), per ADR-0024.
4
5
  *
5
- * Bound to system-binary tooling discovered at runtime, never bundled in core
6
- * (ADR-0001 zero-npm-core; ADR-0014 optional-capability pattern). Both engines
7
- * are spawned as separate processesPandoc isolates its GPLv2+ licence at
8
- * the process boundary; Typst is the Apache-2.0 PDF engine driven through
9
- * Pandoc's `--pdf-engine=typst` flag, so a single Pandoc invocation produces
10
- * every target format the contract supports.
11
- *
12
- * detect(format) reports availability without spawning the engine for real.
13
- * exportMarkdown({ inputPath, outputPath, format, env }) spawns the engine and
14
- * returns a structured result. Both surface actionable "install X" guidance
15
- * when a required binary is absent; neither throws on missing tooling.
6
+ * Export **source** is Construct-authored markdown (typed artifacts). **Output**
7
+ * spans many distributable formats. Ingest (separate pipeline) accepts PDF, Office,
8
+ * email, AV, and plain textsee docs/reference/document-io.md.
16
9
  */
17
10
 
18
11
  import fs from 'node:fs';
19
12
  import path from 'node:path';
20
13
  import { spawnSync } from 'node:child_process';
14
+ import { fileURLToPath } from 'node:url';
15
+ import {
16
+ resolvePdfTemplatePath,
17
+ parseArtifactMetadata,
18
+ pandocMetadataArgs,
19
+ preprocessMarkdownForPdfExport,
20
+ distributionFontsDir,
21
+ } from './publish-template.mjs';
22
+ import {
23
+ preprocessMarkdownDiagrams,
24
+ buildDistributionDiagramEnv,
25
+ countDiagramFences,
26
+ } from './diagram-export.mjs';
27
+ import { exportDeckPptx, pptxgenPresent } from './deck-export-pptx.mjs';
28
+ import { pdfUsesBundledBrandSans } from './brand-fonts.mjs';
29
+ import { convertDocxToDoc, libreOfficeInstallHint, libreOfficePresent, resolveLibreOfficeBin } from './libreoffice-export.mjs';
30
+ import { resolveExportBranding } from './export-branding.mjs';
31
+
32
+ export const EXPORT_FORMATS = ['pdf', 'docx', 'doc', 'deck', 'pptx', 'html', 'rtf', 'odt', 'epub', 'tex', 'txt', 'md', 'mdx'];
33
+
34
+ const REPO_ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
21
35
 
22
- export const EXPORT_FORMATS = ['pdf', 'docx', 'html'];
36
+ // Pandoc drives typeset formats; pptxgenjs builds slide decks; legacy `.doc` has no
37
+ // pandoc writer — export DOCX first, then LibreOffice headless down-converts.
23
38
 
24
39
  const FORMAT_ENGINES = {
25
- pdf: { engine: 'pandoc', pdfEngine: 'typst', extraBinaries: ['typst'] },
26
- docx: { engine: 'pandoc', pdfEngine: null, extraBinaries: [] },
27
- html: { engine: 'pandoc', pdfEngine: null, extraBinaries: [] },
40
+ pdf: { engine: 'pandoc', writer: null, pdfEngine: 'typst', extraBinaries: ['typst'] },
41
+ docx: { engine: 'pandoc', writer: null, pdfEngine: null, extraBinaries: [] },
42
+ doc: { engine: 'pandoc', writer: null, pdfEngine: null, extraBinaries: [], via: 'libreoffice', intermediate: 'docx' },
43
+ deck: { engine: 'pandoc', writer: null, pdfEngine: null, extraBinaries: [] },
44
+ pptx: { engine: 'pptxgenjs', writer: null, pdfEngine: null, extraBinaries: [] },
45
+ html: { engine: 'pandoc', writer: null, pdfEngine: null, extraBinaries: [] },
46
+ rtf: { engine: 'pandoc', writer: 'rtf', pdfEngine: null, extraBinaries: [] },
47
+ odt: { engine: 'pandoc', writer: 'odt', pdfEngine: null, extraBinaries: [] },
48
+ epub: { engine: 'pandoc', writer: 'epub', pdfEngine: null, extraBinaries: [] },
49
+ tex: { engine: 'pandoc', writer: 'latex', pdfEngine: null, extraBinaries: [] },
50
+ txt: { engine: 'pandoc', writer: 'plain', pdfEngine: null, extraBinaries: [] },
51
+ md: { engine: 'copy', writer: null, pdfEngine: null, extraBinaries: [] },
52
+ mdx: { engine: 'copy', writer: null, pdfEngine: null, extraBinaries: [] },
28
53
  };
29
54
 
55
+ export function htmlTemplatePath(repoRoot = REPO_ROOT) {
56
+ const p = path.join(repoRoot, 'templates', 'distribution', 'construct-web.html');
57
+ return fs.existsSync(p) ? p : null;
58
+ }
59
+
60
+ export function deckTemplatePath(repoRoot = REPO_ROOT) {
61
+ const p = path.join(repoRoot, 'templates', 'distribution', 'construct-deck.html');
62
+ return fs.existsSync(p) ? p : null;
63
+ }
64
+
65
+ export function docxReferencePath(repoRoot = REPO_ROOT) {
66
+ const p = path.join(repoRoot, 'templates', 'distribution', 'construct-reference.docx');
67
+ return fs.existsSync(p) ? p : null;
68
+ }
69
+
70
+ export { resolvePdfTemplatePath } from './publish-template.mjs';
71
+
72
+ export function pdfEngineFontOpts(repoRoot = REPO_ROOT) {
73
+ const fontPath = distributionFontsDir(repoRoot);
74
+ return [
75
+ '--pdf-engine-opt', `--font-path=${fontPath}`,
76
+ '--pdf-engine-opt', '--ignore-system-fonts',
77
+ '--pdf-engine-opt', '--ignore-embedded-fonts',
78
+ ];
79
+ }
80
+
81
+ export function countPdfEmbeddedImages(pdfPath) {
82
+ try {
83
+ const data = fs.readFileSync(pdfPath);
84
+ const text = data.toString('latin1');
85
+ const raster = (text.match(/\/Subtype\s*\/Image/g) || []).length;
86
+ const vector = (text.match(/\/Subtype\s*\/Form/g) || []).length;
87
+ return raster + vector;
88
+ } catch {
89
+ return 0;
90
+ }
91
+ }
92
+
93
+ export function pdfRenderedDiagrams(pdfPath, sourceContent) {
94
+ const expected = countDiagramFences(sourceContent);
95
+ if (expected === 0) return true;
96
+ const embedded = countPdfEmbeddedImages(pdfPath);
97
+ if (embedded >= expected) return true;
98
+ try {
99
+ const text = fs.readFileSync(pdfPath).toString('latin1');
100
+ const rawD2 = /direction:\s*(right|down|left|up)/.test(text);
101
+ const rawMermaid = /flowchart\s+(TD|LR|BT|RL)/.test(text);
102
+ return !rawD2 && !rawMermaid && embedded > 0;
103
+ } catch {
104
+ return false;
105
+ }
106
+ }
107
+
108
+ export { pdfUsesBundledBrandSans } from './brand-fonts.mjs';
109
+
110
+ /** @deprecated use pdfUsesBundledBrandSans */
111
+ export function pdfUsesBundledInter(pdfPath) {
112
+ return pdfUsesBundledBrandSans(pdfPath);
113
+ }
114
+
115
+ export function diagramFilterPath() {
116
+ return path.join(REPO_ROOT, 'vendor', 'pandoc-ext', 'diagram.lua');
117
+ }
118
+
119
+ export function detectPdfTemplate({
120
+ artifactType = null,
121
+ cwd = process.cwd(),
122
+ repoRoot = REPO_ROOT,
123
+ } = {}) {
124
+ const templatePath = resolvePdfTemplatePath({ artifactType, cwd, repoRoot });
125
+ return {
126
+ present: Boolean(templatePath),
127
+ path: templatePath,
128
+ message: templatePath
129
+ ? `PDF template: ${path.relative(repoRoot, templatePath)}`
130
+ : 'PDF template missing: templates/distribution/construct-pdf.typ',
131
+ };
132
+ }
133
+
30
134
  function whichBin(name, env = process.env) {
31
135
  const cmd = process.platform === 'win32' ? 'where' : 'which';
32
136
  const result = spawnSync(cmd, [name], { encoding: 'utf8', env });
@@ -45,28 +149,90 @@ function binVersion(name, args = ['--version'], env = process.env) {
45
149
  function installHint(name) {
46
150
  if (name === 'pandoc') return 'Install pandoc to enable document export (e.g. `brew install pandoc` on macOS, `apt install pandoc` on Debian/Ubuntu, or https://pandoc.org/installing.html).';
47
151
  if (name === 'typst') return 'Install typst to enable PDF export via Pandoc (`brew install typst` on macOS, https://github.com/typst/typst/releases for binaries).';
152
+ if (name === 'construct-pdf.typ') return 'Bundled PDF template missing from templates/distribution/construct-pdf.typ (Construct install integrity issue).';
153
+ if (name === 'd2') return 'Install d2 for diagram blocks (`brew install d2`).';
154
+ if (name === 'mmdc') return 'Install mermaid-cli for mermaid blocks (`npm install -g @mermaid-js/mermaid-cli`).';
155
+ if (name === 'pptxgenjs') return 'Install pptxgenjs to enable PPTX export (`npm install pptxgenjs` in the Construct package).';
156
+ if (name === 'construct-deck.html') return 'Bundled deck template missing from templates/distribution/construct-deck.html (Construct install integrity issue).';
157
+ if (name === 'construct-reference.docx') return 'Bundled branded reference doc missing from templates/distribution/construct-reference.docx; construct-branded DOCX export degrades to pandoc default styling. Regenerate from a source checkout with `node scripts/build-reference-docx.mjs`.';
158
+ if (name === 'libreoffice') return libreOfficeInstallHint();
48
159
  return `Install ${name} to enable this export format.`;
49
160
  }
50
161
 
51
- // detect returns the same structure regardless of format so callers can render
52
- // availability uniformly. `present` is the single boolean a caller should gate
53
- // on; `missing` enumerates the binaries to install when present is false.
162
+ export function detect(format, env = process.env, {
163
+ figures = false,
164
+ artifactType = null,
165
+ branding = 'construct',
166
+ cwd = process.cwd(),
167
+ repoRoot = REPO_ROOT,
168
+ } = {}) {
169
+ const brandingPolicy = resolveExportBranding(format, branding);
170
+ const config = format === 'pptx' && brandingPolicy.applied === 'plain'
171
+ ? { engine: 'pandoc', writer: 'pptx', pdfEngine: null, extraBinaries: [] }
172
+ : FORMAT_ENGINES[format];
173
+ if (!config) return { ok: false, format, branding: brandingPolicy, present: false, missing: [], message: `Unsupported format: ${format}. Supported: ${EXPORT_FORMATS.join(', ')}.` };
174
+ if (config.engine === 'copy') {
175
+ return { ok: true, format, branding: brandingPolicy, figures: false, present: true, binaries: [], missing: [], message: `Ready: source emit (${format})` };
176
+ }
177
+ const required = [];
178
+ if (config.engine === 'pptxgenjs') {
179
+ required.push('pptxgenjs');
180
+ } else {
181
+ required.push(config.engine, ...(config.extraBinaries || []));
182
+ }
183
+ if (format === 'pdf' && brandingPolicy.applied === 'construct') {
184
+ const template = detectPdfTemplate({ artifactType, cwd, repoRoot });
185
+ if (!template.present) required.push('construct-pdf.typ');
186
+ }
187
+ if (format === 'deck' && brandingPolicy.applied === 'construct' && !deckTemplatePath(repoRoot)) {
188
+ required.push('construct-deck.html');
189
+ }
190
+ if (format === 'html' && brandingPolicy.applied === 'construct' && !htmlTemplatePath(repoRoot)) required.push('construct-web.html');
191
+
192
+ // The DOCX reference doc only restyles pandoc output; pandoc emits a valid file without it and
193
+ // the export passes --reference-doc only when present, so unlike the PDF/HTML/deck templates a
194
+ // missing reference degrades gracefully rather than blocking the export.
54
195
 
55
- export function detect(format, env = process.env) {
56
- const config = FORMAT_ENGINES[format];
57
- if (!config) return { ok: false, format, present: false, missing: [], message: `Unsupported format: ${format}. Supported: ${EXPORT_FORMATS.join(', ')}.` };
58
- const required = [config.engine, ...(config.extraBinaries || [])];
59
- const status = required.map((name) => ({ name, path: whichBin(name, env), version: null }));
60
- for (const s of status) if (s.path) s.version = binVersion(s.name, ['--version'], env);
196
+ if (format === 'doc' && !libreOfficePresent(env)) {
197
+ required.push('libreoffice');
198
+ }
199
+ if (figures) {
200
+ if (!fs.existsSync(diagramFilterPath())) required.push('diagram.lua');
201
+ if (!whichBin('d2', env) && !whichBin('dot', env)) required.push('d2');
202
+ if (!whichBin('mmdc', env)) required.push('mmdc');
203
+ }
204
+ const status = required.map((name) => {
205
+ if (name.endsWith('.lua')) {
206
+ return { name, path: fs.existsSync(diagramFilterPath()) ? diagramFilterPath() : null, version: null };
207
+ }
208
+ if (name === 'construct-deck.html') {
209
+ return { name, path: deckTemplatePath(repoRoot), version: null };
210
+ }
211
+ if (name === 'construct-web.html') return { name, path: htmlTemplatePath(repoRoot), version: null };
212
+ if (name === 'pptxgenjs') {
213
+ return { name, path: pptxgenPresent() ? 'bundled' : null, version: null };
214
+ }
215
+ if (name === 'libreoffice') {
216
+ return { name, path: libreOfficePresent(env) ? resolveLibreOfficeBin(env) : null, version: null };
217
+ }
218
+ return { name, path: whichBin(name, env), version: null };
219
+ });
220
+ for (const s of status) {
221
+ if (s.path && !s.name.endsWith('.lua') && s.name !== 'pptxgenjs' && s.name !== 'libreoffice') {
222
+ s.version = binVersion(s.name, ['--version'], env);
223
+ }
224
+ }
61
225
  const missing = status.filter((s) => !s.path).map((s) => s.name);
62
226
  return {
63
227
  ok: true,
64
228
  format,
229
+ branding: brandingPolicy,
230
+ figures,
65
231
  present: missing.length === 0,
66
232
  binaries: status,
67
233
  missing,
68
234
  message: missing.length === 0
69
- ? `Ready: ${status.map((s) => `${s.name} (${s.version?.split(' ').slice(0, 2).join(' ') ?? 'unknown'})`).join(', ')}`
235
+ ? `Ready: ${status.map((s) => `${s.name} (${s.version?.split(' ').slice(0, 2).join(' ') ?? 'bundled'})`).join(', ')}`
70
236
  : missing.map(installHint).join(' '),
71
237
  };
72
238
  }
@@ -76,31 +242,194 @@ function defaultOutputPath(inputPath, format) {
76
242
  return path.join(parsed.dir, `${parsed.name}.${format}`);
77
243
  }
78
244
 
79
- // exportMarkdown is the single contract callers depend on (CLI / MCP / SDK).
80
- // Returns { ok, format, inputPath, outputPath, engine, missing?, message }.
81
- // ok=false with a populated `missing` means a required binary is absent — the
82
- // caller surfaces `message` (which carries the install hint) instead of
83
- // retrying or crashing.
245
+ export function parsePublishFrontmatter(inputPath) {
246
+ try {
247
+ const content = fs.readFileSync(inputPath, 'utf8');
248
+ if (!content.startsWith('---')) return null;
249
+ const end = content.indexOf('\n---', 3);
250
+ if (end === -1) return null;
251
+ const block = content.slice(3, end);
252
+ const publish = {};
253
+ let inPublish = false;
254
+ for (const line of block.split('\n')) {
255
+ if (/^publish:\s*$/.test(line)) { inPublish = true; continue; }
256
+ if (inPublish && /^\S/.test(line) && !line.startsWith(' ')) inPublish = false;
257
+ if (!inPublish) continue;
258
+ const m = line.match(/^\s+(\w+):\s*(.+)$/);
259
+ if (m) publish[m[1]] = m[2].replace(/^["']|["']$/g, '').trim();
260
+ }
261
+ return Object.keys(publish).length ? publish : null;
262
+ } catch {
263
+ return null;
264
+ }
265
+ }
84
266
 
85
- export function exportMarkdown({ inputPath, outputPath, format, env = process.env, spawnFn = spawnSync } = {}) {
267
+ function prepareExportInput(inputPath, { format = 'html', figures = false, metadata = null } = {}) {
268
+ let raw = fs.readFileSync(inputPath, 'utf8');
269
+ let processed = raw;
270
+ if ((format === 'pdf' || format === 'html') && metadata?.title) {
271
+ processed = preprocessMarkdownForPdfExport(processed, metadata);
272
+ }
273
+ if (figures) {
274
+ processed = preprocessMarkdownDiagrams(processed);
275
+ }
276
+ if (processed === raw) return { inputPath, cleanup: null };
277
+ const tmp = path.join(path.dirname(inputPath), `.construct-export-${path.basename(inputPath)}`);
278
+ fs.writeFileSync(tmp, processed, 'utf8');
279
+ return {
280
+ inputPath: tmp,
281
+ cleanup: () => { try { fs.unlinkSync(tmp); } catch { /* skip */ } },
282
+ };
283
+ }
284
+
285
+ // md and mdx are emitted straight from the authored source: the artifact already
286
+ // is Markdown, and Construct-authored Markdown is MDX-compatible (fenced diagrams,
287
+ // YAML frontmatter), so no transform is needed beyond writing the target file.
288
+
289
+ function exportSourceCopy({ inputPath, target, format }) {
290
+ const content = fs.readFileSync(inputPath, 'utf8');
291
+ fs.writeFileSync(target, content, 'utf8');
292
+ return {
293
+ ok: true,
294
+ format,
295
+ inputPath,
296
+ outputPath: target,
297
+ engine: 'copy',
298
+ message: `Wrote ${path.relative(process.cwd(), target)}`,
299
+ };
300
+ }
301
+
302
+ export function exportMarkdown({
303
+ inputPath,
304
+ outputPath,
305
+ format,
306
+ figures = false,
307
+ branding = 'construct',
308
+ artifactType = null,
309
+ env = process.env,
310
+ spawnFn = spawnSync,
311
+ cwd = process.cwd(),
312
+ repoRoot = REPO_ROOT,
313
+ } = {}) {
86
314
  if (!inputPath) return { ok: false, format, message: 'exportMarkdown: inputPath is required.' };
87
315
  if (!EXPORT_FORMATS.includes(format)) return { ok: false, format, inputPath, message: `Unsupported format: ${format}. Supported: ${EXPORT_FORMATS.join(', ')}.` };
88
316
  if (!fs.existsSync(inputPath)) return { ok: false, format, inputPath, message: `exportMarkdown: input does not exist: ${inputPath}` };
89
317
 
90
- const detection = detect(format, env);
318
+ const metadata = parseArtifactMetadata(inputPath);
319
+ const resolvedType = artifactType || metadata.artifactType || null;
320
+ const effectiveEnv = figures ? buildDistributionDiagramEnv(env) : env;
321
+
322
+ const brandingPolicy = resolveExportBranding(format, branding);
323
+ const detection = detect(format, effectiveEnv, { figures, artifactType: resolvedType, branding, cwd, repoRoot });
91
324
  if (!detection.present) {
92
325
  return { ok: false, format, inputPath, missing: detection.missing, message: detection.message };
93
326
  }
94
327
 
95
- const config = FORMAT_ENGINES[format];
96
- const target = outputPath || defaultOutputPath(inputPath, format);
328
+ const config = format === 'pptx' && brandingPolicy.applied === 'plain'
329
+ ? { engine: 'pandoc', writer: 'pptx', pdfEngine: null, extraBinaries: [] }
330
+ : FORMAT_ENGINES[format];
331
+ const target = path.resolve(outputPath || defaultOutputPath(inputPath, format));
97
332
  fs.mkdirSync(path.dirname(target), { recursive: true });
98
333
 
334
+ if (config.engine === 'copy') {
335
+ return { ...exportSourceCopy({ inputPath, target, format }), branding: brandingPolicy };
336
+ }
337
+
338
+ if (format === 'pptx' && brandingPolicy.applied === 'construct') {
339
+ return { ...exportDeckPptx({ inputPath, outputPath: target, metadata, repoRoot }), branding: brandingPolicy };
340
+ }
341
+
342
+ if (format === 'doc') {
343
+ const intermediate = path.join(path.dirname(target), `.construct-export-${path.basename(inputPath, path.extname(inputPath))}.docx`);
344
+ const docxResult = exportMarkdown({
345
+ inputPath,
346
+ outputPath: intermediate,
347
+ format: 'docx',
348
+ figures,
349
+ branding,
350
+ artifactType: resolvedType,
351
+ env: effectiveEnv,
352
+ spawnFn,
353
+ cwd,
354
+ repoRoot,
355
+ });
356
+ if (!docxResult.ok) return { ...docxResult, format: 'doc' };
357
+ const loResult = convertDocxToDoc({ docxPath: intermediate, outputPath: target, env: effectiveEnv, spawnFn });
358
+ try { fs.unlinkSync(intermediate); } catch { /* skip */ }
359
+ if (!loResult.ok) {
360
+ return {
361
+ ok: false,
362
+ format: 'doc',
363
+ inputPath,
364
+ outputPath: target,
365
+ missing: loResult.missing,
366
+ message: loResult.message,
367
+ };
368
+ }
369
+ return {
370
+ ok: true,
371
+ format: 'doc',
372
+ inputPath,
373
+ outputPath: target,
374
+ engine: 'libreoffice',
375
+ pdfEngine: null,
376
+ figures,
377
+ template: null,
378
+ branding: brandingPolicy,
379
+ message: loResult.message,
380
+ };
381
+ }
382
+
383
+ const prepared = prepareExportInput(inputPath, { format, figures, metadata });
384
+ const templatePath = format === 'pdf' && brandingPolicy.applied === 'construct'
385
+ ? resolvePdfTemplatePath({ artifactType: resolvedType, cwd, repoRoot })
386
+ : null;
387
+
388
+ const metadataArgs = pandocMetadataArgs({
389
+ title: metadata.title,
390
+ subtitle: metadata.subtitle,
391
+ date: metadata.date,
392
+ status: metadata.status,
393
+ owner: metadata.owner,
394
+ artifactType: resolvedType || metadata.artifactType,
395
+ version: metadata.version,
396
+ docId: metadata.docId,
397
+ classification: metadata.classification,
398
+ });
399
+
99
400
  const args = ['-f', 'markdown', '-o', target, '--standalone'];
401
+ if (config.writer) args.push('-t', config.writer);
100
402
  if (config.pdfEngine) args.push(`--pdf-engine=${config.pdfEngine}`);
101
- args.push(inputPath);
403
+ if (format === 'pdf') {
404
+ if (templatePath) args.push('--template', templatePath);
405
+ if (brandingPolicy.applied === 'construct') args.push(...pdfEngineFontOpts(repoRoot));
406
+ args.push(...metadataArgs);
407
+ } else if (format === 'html') {
408
+ const htmlTpl = htmlTemplatePath(repoRoot);
409
+ if (htmlTpl && brandingPolicy.applied === 'construct') args.push('--template', htmlTpl);
410
+ args.push('--embed-resources');
411
+ args.push(...metadataArgs);
412
+ } else if (format === 'deck') {
413
+ const deckTpl = deckTemplatePath(repoRoot);
414
+ if (deckTpl && brandingPolicy.applied === 'construct') args.push('--template', deckTpl);
415
+ args.push('--section-divs');
416
+ args.push('--embed-resources');
417
+ args.push(...metadataArgs);
418
+ } else if (format === 'docx') {
419
+ const ref = docxReferencePath(repoRoot);
420
+ if (ref && brandingPolicy.applied === 'construct') args.push('--reference-doc', ref);
421
+ args.push(...metadataArgs);
422
+ }
423
+ if (figures && fs.existsSync(diagramFilterPath())) {
424
+ args.push('--lua-filter', diagramFilterPath());
425
+ }
426
+ args.push(path.resolve(prepared.inputPath));
427
+
428
+ const spawnCwd = (format === 'pdf' && templatePath) ? path.dirname(templatePath) : path.dirname(prepared.inputPath);
429
+
430
+ const result = spawnFn(config.engine, args, { encoding: 'utf8', env: effectiveEnv, cwd: spawnCwd });
431
+ if (prepared.cleanup) prepared.cleanup();
102
432
 
103
- const result = spawnFn(config.engine, args, { encoding: 'utf8', env });
104
433
  if (result.status !== 0) {
105
434
  return {
106
435
  ok: false,
@@ -112,6 +441,20 @@ export function exportMarkdown({ inputPath, outputPath, format, env = process.en
112
441
  };
113
442
  }
114
443
 
444
+ if (format === 'pdf' && figures) {
445
+ const source = fs.readFileSync(inputPath, 'utf8');
446
+ const expectedDiagrams = countDiagramFences(source);
447
+ if (expectedDiagrams > 0 && !pdfRenderedDiagrams(target, source)) {
448
+ return {
449
+ ok: false,
450
+ format,
451
+ inputPath,
452
+ outputPath: target,
453
+ message: `Export wrote PDF but rendered 0/${expectedDiagrams} diagram(s). Ensure d2 and mmdc are installed; mmdc needs Chrome (set PUPPETEER_EXECUTABLE_PATH or install Google Chrome). Pandoc stderr: ${(result.stderr || '').trim().slice(0, 300)}`,
454
+ };
455
+ }
456
+ }
457
+
115
458
  return {
116
459
  ok: true,
117
460
  format,
@@ -119,6 +462,9 @@ export function exportMarkdown({ inputPath, outputPath, format, env = process.en
119
462
  outputPath: target,
120
463
  engine: config.engine,
121
464
  pdfEngine: config.pdfEngine,
465
+ figures,
466
+ template: templatePath,
467
+ branding: brandingPolicy,
122
468
  message: `Wrote ${path.relative(process.cwd(), target)}`,
123
469
  };
124
470
  }
@@ -45,6 +45,31 @@ export const CALENDAR_EXTS = new Set(['.ics']);
45
45
  export const AUDIO_VIDEO_EXTS = new Set(['.mp3', '.wav', '.m4a', '.mp4', '.mov', '.avi', '.mkv', '.flac', '.ogg', '.webm', '.m4v']);
46
46
 
47
47
  export const ZIP_DOCUMENT_EXTS = new Set(['.docx', '.xlsx', '.pptx', '.odt', '.ods']);
48
+
49
+ // Office zip formats with no Node-native backend — callers must route through docling
50
+ // or opt into the legacy zip-xml path via CONSTRUCT_ALLOW_LEGACY_EXTRACT=1.
51
+ const OFFICE_REQUIRES_DOCLING_EXTS = new Set(['.xlsx', '.pptx', '.odt', '.ods']);
52
+
53
+ function legacyExtractAllowed(env = process.env) {
54
+ return env.CONSTRUCT_ALLOW_LEGACY_EXTRACT === '1';
55
+ }
56
+
57
+ function makeOfficeRequiresDoclingError(extension) {
58
+ const err = new Error(
59
+ `${extension} extraction requires docling. Run \`construct install --with-docling\`, use high-fidelity ingest, or set CONSTRUCT_ALLOW_LEGACY_EXTRACT=1 for the legacy zip-xml path.`,
60
+ );
61
+ err.code = 'OFFICE_REQUIRES_DOCLING';
62
+ err.extension = extension;
63
+ return err;
64
+ }
65
+
66
+ function makePdfRequiresDoclingError() {
67
+ const err = new Error(
68
+ 'PDF extraction requires docling, the node-native unpdf backend, or CONSTRUCT_ALLOW_LEGACY_EXTRACT=1 for pdftotext/PDFKit.',
69
+ );
70
+ err.code = 'PDF_REQUIRES_DOCLING';
71
+ return err;
72
+ }
48
73
  export const RICH_TEXT_EXTS = new Set(['.doc', '.rtf']);
49
74
  export const MDLS_DOCUMENT_EXTS = new Set(['.xls', '.ppt', '.pages', '.numbers', '.key']);
50
75
  export const EMAIL_DOCUMENT_EXTS = new Set(['.eml', '.msg']);
@@ -685,6 +710,9 @@ export function extractDocumentText(filePath, { maxChars = null } = {}) {
685
710
  const envelope = extractCalendar(resolvedPath);
686
711
  extracted = { text: envelope.text, method: 'ics', droppedInfo: envelope.droppedInfo, structured: envelope.structured };
687
712
  } else if (ZIP_DOCUMENT_EXTS.has(extension)) {
713
+ if (!legacyExtractAllowed()) {
714
+ throw makeOfficeRequiresDoclingError(extension);
715
+ }
688
716
  extracted = extractZipDocument(resolvedPath, extension);
689
717
  } else if (RICH_TEXT_EXTS.has(extension)) {
690
718
  extracted = extractRichText(resolvedPath, extension);
@@ -693,6 +721,9 @@ export function extractDocumentText(filePath, { maxChars = null } = {}) {
693
721
  } else if (EMAIL_DOCUMENT_EXTS.has(extension)) {
694
722
  extracted = extractEml(resolvedPath);
695
723
  } else if (extension === '.pdf') {
724
+ if (!legacyExtractAllowed()) {
725
+ throw makePdfRequiresDoclingError();
726
+ }
696
727
  extracted = extractPdf(resolvedPath);
697
728
  } else {
698
729
  throw new Error(`Unsupported document type: ${extension || 'unknown'}`);
@@ -731,9 +762,9 @@ function buildExtractionResult(resolvedPath, extension, extracted, maxChars) {
731
762
  }
732
763
 
733
764
  // Node-native extraction for the everyday adapter path: unpdf (PDF) and mammoth
734
- // (DOCX) are optional pure-JS deps, so plain documents ingest with no Python venv
735
- // and no system binary. Missing dep, an empty parse, or any other type delegates
736
- // to the sync system-binary extractor the choice degrades, never fails hard.
765
+ // (DOCX) are optional pure-JS deps. XLSX/PPTX/ODT/ODS have no Node backend and
766
+ // fail loud with OFFICE_REQUIRES_DOCLING. Legacy zip-xml and pdftotext paths run
767
+ // only when CONSTRUCT_ALLOW_LEGACY_EXTRACT=1; otherwise extraction errors clearly.
737
768
 
738
769
  async function extractWithUnpdf(filePath) {
739
770
  const { extractText, getDocumentProxy } = await import('unpdf');
@@ -753,6 +784,10 @@ export async function extractDocumentTextNodeNative(filePath, { maxChars = null
753
784
  if (!existsSync(resolvedPath)) throw new Error(`File not found: ${resolvedPath}`);
754
785
  const extension = extname(resolvedPath).toLowerCase();
755
786
 
787
+ if (OFFICE_REQUIRES_DOCLING_EXTS.has(extension)) {
788
+ throw makeOfficeRequiresDoclingError(extension);
789
+ }
790
+
756
791
  const nodeBackend = extension === '.pdf' ? { fn: extractWithUnpdf, method: 'unpdf' }
757
792
  : extension === '.docx' ? { fn: extractWithMammoth, method: 'mammoth' }
758
793
  : null;
@@ -763,7 +798,12 @@ export async function extractDocumentTextNodeNative(filePath, { maxChars = null
763
798
  if (text) {
764
799
  return buildExtractionResult(resolvedPath, extension, { text, method: nodeBackend.method, droppedInfo: [], structured: null }, maxChars);
765
800
  }
766
- } catch { /* optional dep absent or parse failure — fall through to the sync extractor */ }
801
+ } catch { /* optional dep absent or parse failure — legacy path only when explicitly allowed */ }
802
+ if (!legacyExtractAllowed()) {
803
+ if (extension === '.docx') throw makeOfficeRequiresDoclingError(extension);
804
+ throw makePdfRequiresDoclingError();
805
+ }
806
+ return extractDocumentText(resolvedPath, { maxChars });
767
807
  }
768
808
 
769
809
  return extractDocumentText(resolvedPath, { maxChars });