@geraldmaron/construct 1.1.0 → 1.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (425) hide show
  1. package/README.md +30 -18
  2. package/bin/construct +392 -91
  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 +112 -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/audit-skills.mjs +4 -0
  20. package/lib/audit-specialists.mjs +285 -0
  21. package/lib/audit-trail.mjs +77 -10
  22. package/lib/auto-docs.mjs +87 -18
  23. package/lib/beads/drift.mjs +27 -6
  24. package/lib/beads-client.mjs +49 -121
  25. package/lib/beads-optimistic.mjs +5 -12
  26. package/lib/brand-fonts.mjs +93 -0
  27. package/lib/brand-prose.mjs +214 -0
  28. package/lib/brand-tokens.mjs +92 -0
  29. package/lib/bridges/copilot-proxy.mjs +13 -26
  30. package/lib/capability-ledger.mjs +156 -0
  31. package/lib/certification/artifact-fixtures.mjs +132 -0
  32. package/lib/certification/artifact-gates.mjs +63 -0
  33. package/lib/certification/artifact-provenance.mjs +97 -0
  34. package/lib/certification/canonical-scenarios.mjs +78 -0
  35. package/lib/certification/cli.mjs +191 -0
  36. package/lib/certification/dashboard-api.mjs +71 -0
  37. package/lib/certification/demo-parity.mjs +116 -0
  38. package/lib/certification/document-io-fixtures.mjs +246 -0
  39. package/lib/certification/document-workflow.mjs +97 -0
  40. package/lib/certification/eval-bridge.mjs +77 -0
  41. package/lib/certification/model-routing.mjs +149 -0
  42. package/lib/certification/prompt-budget.mjs +119 -0
  43. package/lib/certification/rc-gate.mjs +206 -0
  44. package/lib/certification/real-llm-scenarios.mjs +303 -0
  45. package/lib/certification/role-cards.mjs +113 -0
  46. package/lib/certification/role-overlays.mjs +117 -0
  47. package/lib/certification/run.mjs +122 -0
  48. package/lib/certification/runner.mjs +323 -0
  49. package/lib/certification/scenarios.mjs +51 -0
  50. package/lib/certification/skill-inventory.mjs +289 -0
  51. package/lib/certification/skill-scenarios.mjs +147 -0
  52. package/lib/certification/specialist-contracts.mjs +85 -0
  53. package/lib/certification/specialist-scenarios.mjs +175 -0
  54. package/lib/certification/stale-impact.mjs +146 -0
  55. package/lib/certification/status.mjs +252 -0
  56. package/lib/certification/store.mjs +77 -0
  57. package/lib/chat/cli.mjs +333 -0
  58. package/lib/chat/command-suggest.mjs +161 -0
  59. package/lib/chat/commands.mjs +215 -0
  60. package/lib/chat/config.mjs +142 -0
  61. package/lib/chat/context-compactor.mjs +250 -0
  62. package/lib/chat/context-continuation.mjs +253 -0
  63. package/lib/chat/continuation-source.mjs +58 -0
  64. package/lib/chat/demo-guide.mjs +61 -0
  65. package/lib/chat/design-tokens.mjs +91 -0
  66. package/lib/chat/desktop-binary.mjs +79 -0
  67. package/lib/chat/desktop-build.mjs +130 -0
  68. package/lib/chat/desktop-launcher.mjs +133 -0
  69. package/lib/chat/evidence.mjs +145 -0
  70. package/lib/chat/export.mjs +74 -0
  71. package/lib/chat/harness/driver.mjs +91 -0
  72. package/lib/chat/list-picker.mjs +112 -0
  73. package/lib/chat/model-picker.mjs +356 -0
  74. package/lib/chat/openrouter-fallback.mjs +151 -0
  75. package/lib/chat/permission-prompt.mjs +33 -0
  76. package/lib/chat/picker-catalog.mjs +45 -0
  77. package/lib/chat/policy-telemetry.mjs +34 -0
  78. package/lib/chat/present.mjs +245 -0
  79. package/lib/chat/session-context.mjs +39 -0
  80. package/lib/chat/session-persist.mjs +73 -0
  81. package/lib/chat/session-restore.mjs +71 -0
  82. package/lib/chat/session-settings.mjs +53 -0
  83. package/lib/chat/system-prompt.mjs +52 -0
  84. package/lib/chat/transparency.mjs +93 -0
  85. package/lib/chat/tui/color-scheme.mjs +42 -0
  86. package/lib/chat/tui/markdown.mjs +123 -0
  87. package/lib/chat/tui/presentation.mjs +100 -0
  88. package/lib/chat/tui/render.mjs +500 -0
  89. package/lib/chat/tui/turn-block.mjs +284 -0
  90. package/lib/chat/tui/turn-present.mjs +18 -0
  91. package/lib/chat/tui/turn-state.mjs +88 -0
  92. package/lib/chat/tui/usage.mjs +122 -0
  93. package/lib/chat/web-commands.mjs +146 -0
  94. package/lib/chat/web-launcher.mjs +63 -0
  95. package/lib/chat/web-picker-keys.mjs +46 -0
  96. package/lib/chat/web-session.mjs +159 -0
  97. package/lib/cli-commands.mjs +232 -11
  98. package/lib/comment-lint.mjs +6 -4
  99. package/lib/config/schema.mjs +36 -5
  100. package/lib/contract-schemas/decision.json +50 -0
  101. package/lib/contract-schemas/implementation.json +51 -0
  102. package/lib/contract-schemas/review-report.json +32 -0
  103. package/lib/contract-schemas/test-report.json +43 -0
  104. package/lib/contracts/construct-handoff.mjs +60 -0
  105. package/lib/contracts/validate.mjs +32 -3
  106. package/lib/contracts/violation-log.mjs +58 -5
  107. package/lib/dashboard-demo.mjs +71 -0
  108. package/lib/dashboard-static.mjs +19 -5
  109. package/lib/decisions/registry.mjs +5 -3
  110. package/lib/deck-export-pptx.mjs +1152 -0
  111. package/lib/demo-recording.mjs +142 -0
  112. package/lib/demo-script.mjs +114 -0
  113. package/lib/demo-surface.mjs +249 -0
  114. package/lib/demo.mjs +703 -0
  115. package/lib/diagram-export.mjs +192 -0
  116. package/lib/diagram.mjs +302 -0
  117. package/lib/docs-verify.mjs +25 -7
  118. package/lib/doctor/index.mjs +4 -2
  119. package/lib/doctor/project-adapters.mjs +44 -0
  120. package/lib/doctor/watchers/cx-budget.mjs +98 -0
  121. package/lib/doctor/watchers/graph-staleness.mjs +52 -0
  122. package/lib/doctor/watchers/process-pressure.mjs +4 -3
  123. package/lib/doctor/watchers/service-health.mjs +1 -1
  124. package/lib/document-export.mjs +360 -36
  125. package/lib/document-extract.mjs +88 -0
  126. package/lib/document-ingest.mjs +32 -18
  127. package/lib/embed/cli.mjs +2 -2
  128. package/lib/embed/daemon.mjs +18 -46
  129. package/lib/embed/semantic.mjs +5 -2
  130. package/lib/embed/worker.mjs +2 -2
  131. package/lib/embedded-contract/model-resolve.mjs +10 -9
  132. package/lib/env-config.mjs +26 -0
  133. package/lib/evals/dataset.mjs +137 -0
  134. package/lib/evals/gates.mjs +175 -0
  135. package/lib/graph/build-co-change.mjs +54 -0
  136. package/lib/graph/build-from-registry.mjs +136 -0
  137. package/lib/graph/build-import-graph.mjs +153 -0
  138. package/lib/graph/cli.mjs +110 -0
  139. package/lib/graph/impact-cli.mjs +89 -0
  140. package/lib/graph/impact.mjs +108 -0
  141. package/lib/graph/staleness.mjs +44 -0
  142. package/lib/graph/store.mjs +172 -0
  143. package/lib/handoffs/cleanup.mjs +1 -1
  144. package/lib/health-check.mjs +90 -76
  145. package/lib/hooks/artifact-release-gate.mjs +43 -0
  146. package/lib/hooks/audit-trail.mjs +14 -28
  147. package/lib/hooks/brand-prose-lint.mjs +38 -0
  148. package/lib/hooks/graph-impact-advisory.mjs +62 -0
  149. package/lib/hooks/session-optimize.mjs +84 -207
  150. package/lib/hooks/session-start.mjs +4 -5
  151. package/lib/host-disposition.mjs +7 -0
  152. package/lib/improvement/cli.mjs +189 -0
  153. package/lib/improvement/controller.mjs +137 -0
  154. package/lib/improvement/proposal.mjs +120 -0
  155. package/lib/improvement/specialist-loop.mjs +192 -0
  156. package/lib/improvement/store.mjs +89 -0
  157. package/lib/improvement/surface.mjs +219 -0
  158. package/lib/ingest-tooling.mjs +97 -0
  159. package/lib/init/doc-lanes.mjs +165 -0
  160. package/lib/init-docs.mjs +31 -178
  161. package/lib/init-unified.mjs +27 -97
  162. package/lib/init-update-guide.mjs +102 -0
  163. package/lib/init-update.mjs +32 -1
  164. package/lib/init.mjs +8 -0
  165. package/lib/install/desktop-binary-download.mjs +85 -0
  166. package/lib/install/legacy-global-cleanup.mjs +189 -0
  167. package/lib/intake/daemon.mjs +2 -0
  168. package/lib/intake/git-queue.mjs +9 -8
  169. package/lib/intake/queue.mjs +2 -1
  170. package/lib/intake/session-prelude.mjs +90 -1
  171. package/lib/libreoffice-export.mjs +97 -0
  172. package/lib/logging/rotate.mjs +9 -0
  173. package/lib/maintenance/docker-reclaim.mjs +206 -0
  174. package/lib/mcp/external-schema-cost.mjs +74 -0
  175. package/lib/mcp/server.mjs +84 -9
  176. package/lib/mcp/stdio-mcp-probe.mjs +188 -0
  177. package/lib/mcp/tool-budget.mjs +55 -4
  178. package/lib/mcp/tools/document.mjs +18 -4
  179. package/lib/mcp/tools/skills.mjs +32 -3
  180. package/lib/mcp/tools/workflow.mjs +13 -1
  181. package/lib/model-free-selector.mjs +18 -0
  182. package/lib/model-registry.mjs +13 -229
  183. package/lib/model-router.mjs +425 -93
  184. package/lib/models/behavior-matrix.mjs +289 -0
  185. package/lib/models/catalog.mjs +209 -0
  186. package/lib/models/execution-capability-profile.mjs +196 -0
  187. package/lib/models/execution-policy.mjs +307 -0
  188. package/lib/models/provider-poll.mjs +383 -0
  189. package/lib/npm-spawn-env.mjs +17 -0
  190. package/lib/ollama/installed-models.mjs +129 -0
  191. package/lib/oracle/actions.mjs +187 -0
  192. package/lib/oracle/artifact-gate.mjs +99 -0
  193. package/lib/oracle/cli.mjs +204 -0
  194. package/lib/oracle/daemon-entry.mjs +14 -0
  195. package/lib/oracle/dispatch.mjs +81 -0
  196. package/lib/oracle/execute.mjs +143 -0
  197. package/lib/oracle/gaps.mjs +76 -0
  198. package/lib/oracle/index.mjs +84 -0
  199. package/lib/oracle/issues.mjs +164 -0
  200. package/lib/oracle/org-graph.mjs +170 -0
  201. package/lib/oracle/policy.mjs +80 -0
  202. package/lib/oracle/read-model.mjs +398 -0
  203. package/lib/oracle/reconcile.mjs +191 -0
  204. package/lib/oracle/routing.mjs +89 -0
  205. package/lib/oracle/synthesize.mjs +384 -0
  206. package/lib/oracle/verdicts.mjs +51 -0
  207. package/lib/orchestration/worker.mjs +88 -27
  208. package/lib/orchestration-policy.mjs +50 -0
  209. package/lib/parity.mjs +96 -2
  210. package/lib/persona-sections.mjs +66 -0
  211. package/lib/playwright-demo.mjs +252 -0
  212. package/lib/project-init-shared.mjs +4 -1
  213. package/lib/prompt-composer.js +9 -4
  214. package/lib/prompt-validation-contract.mjs +24 -0
  215. package/lib/provider-capabilities.js +57 -11
  216. package/lib/providers/contract/adapters/confluence/index.mjs +181 -0
  217. package/lib/providers/contract/adapters/git/index.mjs +115 -0
  218. package/lib/providers/contract/adapters/github/index.mjs +166 -0
  219. package/lib/providers/contract/adapters/jira/index.mjs +187 -0
  220. package/lib/providers/contract/adapters/slack/index.mjs +175 -0
  221. package/lib/providers/contract/contract-tests.mjs +57 -0
  222. package/lib/providers/contract/errors.mjs +48 -0
  223. package/lib/providers/contract/interface.mjs +50 -0
  224. package/lib/providers/contract/registry.mjs +102 -0
  225. package/lib/providers/copilot-auth.mjs +297 -0
  226. package/lib/providers/credential-bootstrap.mjs +178 -0
  227. package/lib/providers/credential-catalog.mjs +46 -0
  228. package/lib/providers/credential-sources.mjs +63 -0
  229. package/lib/providers/creds.mjs +5 -2
  230. package/lib/providers/op-run.mjs +59 -0
  231. package/lib/providers/secret-resolver.mjs +159 -0
  232. package/lib/publish-template.mjs +163 -0
  233. package/lib/publish-tooling.mjs +119 -0
  234. package/lib/publish.mjs +305 -0
  235. package/lib/registry/cli.mjs +82 -0
  236. package/lib/registry/consolidation.mjs +147 -0
  237. package/lib/registry/generate-docs.mjs +75 -0
  238. package/lib/registry/skill-verification.mjs +57 -0
  239. package/lib/registry/surface-map.mjs +76 -0
  240. package/lib/registry/validate.mjs +135 -0
  241. package/lib/resources/budget.mjs +82 -0
  242. package/lib/resources/process-budget.mjs +45 -0
  243. package/lib/rules-delivery.mjs +9 -2
  244. package/lib/rules-read.mjs +26 -0
  245. package/lib/runtime-env.mjs +23 -0
  246. package/lib/runtime-pressure.mjs +51 -7
  247. package/lib/schema-infer.mjs +13 -2
  248. package/lib/server/chat-loop.mjs +622 -0
  249. package/lib/server/demo-preview.mjs +63 -0
  250. package/lib/server/index.mjs +229 -8
  251. package/lib/server/langfuse-login.mjs +3 -3
  252. package/lib/service-manager.mjs +61 -9
  253. package/lib/session-store.mjs +1 -1
  254. package/lib/setup.mjs +102 -2
  255. package/lib/specialists/prompt-schema.mjs +19 -10
  256. package/lib/specialists/roster.mjs +43 -0
  257. package/lib/specialists/scaffold.mjs +56 -0
  258. package/lib/storage/backend.mjs +6 -6
  259. package/lib/storage/embeddings-local.mjs +6 -3
  260. package/lib/storage/file-lock.mjs +18 -11
  261. package/lib/storage/hybrid-query.mjs +7 -4
  262. package/lib/storage/state-source.mjs +5 -5
  263. package/lib/storage/sync.mjs +2 -3
  264. package/lib/telemetry/rule-calls.mjs +52 -0
  265. package/lib/template-registry.mjs +2 -2
  266. package/lib/templates/visual-requirements.mjs +27 -51
  267. package/lib/test-corpus-inventory.mjs +313 -0
  268. package/lib/uninstall/uninstall.mjs +19 -7
  269. package/lib/update.mjs +12 -0
  270. package/lib/upgrade.mjs +14 -0
  271. package/lib/wireframe.mjs +20 -14
  272. package/lib/worker/run.mjs +17 -4
  273. package/lib/worker/trace.mjs +11 -3
  274. package/package.json +29 -13
  275. package/personas/construct.md +13 -13
  276. package/platforms/claude/settings.template.json +36 -0
  277. package/rules/common/patterns.md +1 -1
  278. package/rules/common/release-gates.md +4 -3
  279. package/scripts/sync-specialists.mjs +187 -60
  280. package/skills/devops/data-engineering.md +1 -1
  281. package/skills/docs/adr-workflow.md +1 -0
  282. package/skills/docs/backlog-proposal-workflow.md +1 -0
  283. package/skills/docs/codebase-research-workflow.md +40 -0
  284. package/skills/docs/customer-profile-workflow.md +1 -0
  285. package/skills/docs/document-ingest-workflow.md +1 -0
  286. package/skills/docs/evidence-ingest-workflow.md +1 -0
  287. package/skills/docs/init-docs.md +2 -2
  288. package/skills/docs/init-project.md +8 -3
  289. package/skills/docs/prd-workflow.md +24 -1
  290. package/skills/docs/prfaq-workflow.md +1 -0
  291. package/skills/docs/product-intelligence-workflow.md +1 -0
  292. package/skills/docs/product-signal-workflow.md +1 -0
  293. package/skills/docs/research-workflow.md +54 -37
  294. package/skills/docs/runbook-workflow.md +1 -0
  295. package/skills/docs/strategy-workflow.md +1 -0
  296. package/skills/docs/user-research-workflow.md +40 -0
  297. package/skills/operating/orchestration-reference.md +1 -1
  298. package/skills/roles/architect.md +5 -0
  299. package/skills/roles/operator.docs.md +4 -0
  300. package/skills/routing.md +4 -2
  301. package/specialists/artifact-manifest.json +480 -0
  302. package/specialists/artifact-manifest.schema.json +53 -0
  303. package/specialists/audit-enrichments.json +454 -0
  304. package/specialists/contracts.json +37 -25
  305. package/specialists/prompts/_shared/validation-contract.md +26 -0
  306. package/specialists/prompts/cx-accessibility.md +21 -2
  307. package/specialists/prompts/cx-ai-engineer.md +24 -1
  308. package/specialists/prompts/cx-architect.md +4 -1
  309. package/specialists/prompts/cx-business-strategist.md +23 -2
  310. package/specialists/prompts/cx-data-analyst.md +22 -1
  311. package/specialists/prompts/cx-data-engineer.md +23 -2
  312. package/specialists/prompts/cx-debugger.md +21 -2
  313. package/specialists/prompts/cx-designer.md +26 -3
  314. package/specialists/prompts/cx-devil-advocate.md +19 -2
  315. package/specialists/prompts/cx-docs-keeper.md +28 -1
  316. package/specialists/prompts/cx-engineer.md +22 -1
  317. package/specialists/prompts/cx-evaluator.md +18 -1
  318. package/specialists/prompts/cx-explorer.md +21 -2
  319. package/specialists/prompts/cx-legal-compliance.md +21 -2
  320. package/specialists/prompts/cx-operations.md +21 -2
  321. package/specialists/prompts/cx-oracle.md +94 -0
  322. package/specialists/prompts/cx-orchestrator.md +20 -1
  323. package/specialists/prompts/cx-platform-engineer.md +25 -2
  324. package/specialists/prompts/cx-product-manager.md +32 -1
  325. package/specialists/prompts/cx-qa.md +24 -2
  326. package/specialists/prompts/cx-rd-lead.md +23 -2
  327. package/specialists/prompts/cx-release-manager.md +23 -2
  328. package/specialists/prompts/cx-researcher.md +22 -2
  329. package/specialists/prompts/cx-reviewer.md +18 -1
  330. package/specialists/prompts/cx-security.md +22 -2
  331. package/specialists/prompts/cx-sre.md +30 -3
  332. package/specialists/prompts/cx-test-automation.md +7 -1
  333. package/specialists/prompts/cx-trace-reviewer.md +22 -3
  334. package/specialists/prompts/cx-ux-researcher.md +21 -2
  335. package/specialists/registry.json +52 -173
  336. package/specialists/tone-profiles.json +42 -0
  337. package/templates/demos/playwright/demo-recording.config.mjs +47 -0
  338. package/templates/demos/recordings/agentic-platforms-prd.json +19 -0
  339. package/templates/demos/scripts/agentic-platforms-prd.json +44 -0
  340. package/templates/demos/specs/_helpers/scroll-artifact.ts +89 -0
  341. package/templates/demos/tapes/agentic-platforms-prd.tape +49 -0
  342. package/templates/demos/tapes/resource-guard-rails.tape +49 -0
  343. package/templates/demos/vhs/construct-cockpit.json +24 -0
  344. package/templates/distribution/construct-brand.typ +446 -0
  345. package/templates/distribution/construct-decision.typ +38 -0
  346. package/templates/distribution/construct-deck.html +95 -0
  347. package/templates/distribution/construct-pdf.typ +38 -0
  348. package/templates/distribution/construct-prd.typ +38 -0
  349. package/templates/distribution/construct-research.typ +38 -0
  350. package/templates/distribution/construct-web.html +92 -0
  351. package/templates/distribution/fonts/Geist-Bold.ttf +0 -0
  352. package/templates/distribution/fonts/Geist-Medium.ttf +0 -0
  353. package/templates/distribution/fonts/Geist-Regular.ttf +0 -0
  354. package/templates/distribution/fonts/Geist-SemiBold.ttf +0 -0
  355. package/templates/distribution/fonts/GeistMono-Medium.ttf +0 -0
  356. package/templates/distribution/fonts/GeistMono-Regular.ttf +0 -0
  357. package/templates/distribution/fonts/GeistMono-SemiBold.ttf +0 -0
  358. package/templates/distribution/fonts/IBMPlexMono-Regular.otf +0 -0
  359. package/templates/distribution/fonts/JetBrainsMono-Medium.ttf +0 -0
  360. package/templates/distribution/fonts/JetBrainsMono-Regular.ttf +0 -0
  361. package/templates/distribution/fonts/JetBrainsMono-SemiBold.ttf +0 -0
  362. package/templates/distribution/fonts/PlusJakartaSans-Bold.ttf +0 -0
  363. package/templates/distribution/fonts/PlusJakartaSans-Medium.ttf +0 -0
  364. package/templates/distribution/fonts/PlusJakartaSans-Regular.ttf +0 -0
  365. package/templates/distribution/fonts/PlusJakartaSans-SemiBold.ttf +0 -0
  366. package/templates/distribution/fonts/README.md +36 -0
  367. package/templates/distribution/fonts/SpaceGrotesk-Variable.ttf +0 -0
  368. package/templates/distribution/fonts/handwritten/Caveat.ttf +0 -0
  369. package/templates/distribution/fonts/legacy/IBMPlexMono-Regular.otf +0 -0
  370. package/templates/distribution/fonts/legacy/Inter-Medium.otf +0 -0
  371. package/templates/distribution/fonts/legacy/Inter-Regular.otf +0 -0
  372. package/templates/distribution/fonts/legacy/Inter-SemiBold.otf +0 -0
  373. package/templates/distribution/fonts/legacy/InterDisplay-SemiBold.otf +0 -0
  374. package/templates/distribution/fonts/legacy/SourceSerif4-Regular.otf +0 -0
  375. package/templates/distribution/fonts/legacy/SourceSerif4-Semibold.otf +0 -0
  376. package/templates/distribution/icons/activity.svg +15 -0
  377. package/templates/distribution/icons/alert-triangle.svg +17 -0
  378. package/templates/distribution/icons/book-open.svg +16 -0
  379. package/templates/distribution/icons/bot.svg +20 -0
  380. package/templates/distribution/icons/brain.svg +22 -0
  381. package/templates/distribution/icons/circle-check.svg +16 -0
  382. package/templates/distribution/icons/clipboard-check.svg +17 -0
  383. package/templates/distribution/icons/cpu.svg +28 -0
  384. package/templates/distribution/icons/database.svg +17 -0
  385. package/templates/distribution/icons/eye.svg +16 -0
  386. package/templates/distribution/icons/file-text.svg +19 -0
  387. package/templates/distribution/icons/gauge.svg +16 -0
  388. package/templates/distribution/icons/git-branch.svg +17 -0
  389. package/templates/distribution/icons/key.svg +17 -0
  390. package/templates/distribution/icons/layers.svg +17 -0
  391. package/templates/distribution/icons/list-checks.svg +19 -0
  392. package/templates/distribution/icons/lock.svg +16 -0
  393. package/templates/distribution/icons/message-square.svg +15 -0
  394. package/templates/distribution/icons/network.svg +19 -0
  395. package/templates/distribution/icons/route.svg +17 -0
  396. package/templates/distribution/icons/scale.svg +19 -0
  397. package/templates/distribution/icons/search.svg +16 -0
  398. package/templates/distribution/icons/send.svg +16 -0
  399. package/templates/distribution/icons/server.svg +18 -0
  400. package/templates/distribution/icons/shield-check.svg +16 -0
  401. package/templates/distribution/icons/users.svg +18 -0
  402. package/templates/distribution/icons/webhook.svg +17 -0
  403. package/templates/distribution/icons/workflow.svg +17 -0
  404. package/templates/distribution/icons/wrench.svg +15 -0
  405. package/templates/distribution/run.mjs +18 -1
  406. package/templates/docs/adr.md +7 -0
  407. package/templates/docs/construct_guide.md +10 -10
  408. package/templates/docs/customer-profile.md +4 -0
  409. package/templates/docs/one-pager.md +4 -0
  410. package/templates/docs/postmortem.md +31 -0
  411. package/templates/docs/prd-platform.md +19 -0
  412. package/templates/docs/prd.md +15 -0
  413. package/templates/docs/prfaq.md +7 -0
  414. package/templates/docs/qa-strategy.md +103 -0
  415. package/templates/docs/research-brief.md +8 -0
  416. package/templates/docs/rfc-platform.md +12 -0
  417. package/templates/docs/test-plan.md +7 -0
  418. package/lib/ingest/chunker.mjs +0 -94
  419. package/lib/ingest/pipeline.mjs +0 -53
  420. package/lib/ingest/store.mjs +0 -82
  421. package/lib/mode-commands.mjs +0 -122
  422. package/lib/policy/unified-gates.mjs +0 -96
  423. package/lib/profiles/validate-custom.mjs +0 -114
  424. package/lib/services/telemetry-backend.mjs +0 -177
  425. package/lib/storage/fusion.mjs +0 -95
@@ -0,0 +1,98 @@
1
+ /**
2
+ * lib/doctor/watchers/cx-budget.mjs — .cx/ disk budget maintenance.
3
+ *
4
+ * When project .cx/ usage crosses the configured cap, runs age/count prune.
5
+ * When still over cap after retention prune, emergency-reclaims oldest
6
+ * traces and worker logs (hard-reject categories only).
7
+ *
8
+ * Tick: 15 min.
9
+ */
10
+
11
+ import { existsSync } from 'node:fs';
12
+ import { join } from 'node:path';
13
+
14
+ import { record } from '../audit.mjs';
15
+ import { escalate } from '../escalate.mjs';
16
+ import {
17
+ measureUsage,
18
+ planPrune,
19
+ executePrune,
20
+ planEmergencyReclaim,
21
+ } from '../../resources/budget.mjs';
22
+
23
+ export const name = 'cx-budget';
24
+ export const intervalMs = 15 * 60 * 1000;
25
+
26
+ function projectRoot() {
27
+ return process.env.CONSTRUCT_PROJECT_ROOT || process.cwd();
28
+ }
29
+
30
+ export async function tick() {
31
+ const actions = [];
32
+ const escalations = [];
33
+ const root = projectRoot();
34
+ const cxDir = join(root, '.cx');
35
+ if (!existsSync(cxDir)) {
36
+ return { actions, escalations, notes: [{ skipped: 'no .cx directory' }] };
37
+ }
38
+
39
+ const before = measureUsage(root, process.env);
40
+ if (before.totalCxUsageRatio <= 0.8) {
41
+ return { actions, escalations, notes: [{ usageRatio: before.totalCxUsageRatio }] };
42
+ }
43
+
44
+ const planned = planPrune(root, process.env);
45
+ let reclaimed = { removed: [], bytesFreed: 0 };
46
+ if (planned.length) {
47
+ reclaimed = executePrune(planned);
48
+ record({
49
+ kind: 'action',
50
+ watcher: name,
51
+ action: 'prune',
52
+ target: '.cx/',
53
+ summary: `retention prune removed ${reclaimed.removed.length} file(s), freed ${reclaimed.bytesFreed} bytes`,
54
+ context: { planned: planned.length, removed: reclaimed.removed.length },
55
+ });
56
+ actions.push({ type: 'prune', target: '.cx/', removed: reclaimed.removed.length });
57
+ }
58
+
59
+ const afterPrune = measureUsage(root, process.env);
60
+ if (afterPrune.totalCxUsageRatio > 1) {
61
+ const emergency = planEmergencyReclaim(root, 0, process.env);
62
+ if (emergency.length) {
63
+ const emergencyResult = executePrune(emergency);
64
+ record({
65
+ kind: 'action',
66
+ watcher: name,
67
+ action: 'emergency-reclaim',
68
+ target: '.cx/traces|.cx/runtime/worker',
69
+ summary: `emergency reclaim removed ${emergencyResult.removed.length} file(s), freed ${emergencyResult.bytesFreed} bytes`,
70
+ context: emergencyResult,
71
+ });
72
+ actions.push({ type: 'emergency-reclaim', removed: emergencyResult.removed.length });
73
+ }
74
+ }
75
+
76
+ const after = measureUsage(root, process.env);
77
+ if (after.totalCxUsageRatio > 1) {
78
+ const usedMb = Math.round(after.totalCxBytes / 1024 / 1024);
79
+ const capMb = Math.round(after.totalCxCap / 1024 / 1024);
80
+ const r = await escalate({
81
+ watcher: name,
82
+ eventType: 'service.down',
83
+ summary: `.cx/ still over cap after prune (${usedMb}MB / ${capMb}MB) — raise resources.disk.totalCxMaxMb or delete load-bearing state manually`,
84
+ context: { usageRatio: after.totalCxUsageRatio, usedMb, capMb },
85
+ });
86
+ escalations.push({ eventType: 'service.down', result: r });
87
+ }
88
+
89
+ return {
90
+ actions,
91
+ escalations,
92
+ notes: [{
93
+ usageBefore: before.totalCxUsageRatio,
94
+ usageAfter: after.totalCxUsageRatio,
95
+ bytesFreed: reclaimed.bytesFreed,
96
+ }],
97
+ };
98
+ }
@@ -0,0 +1,52 @@
1
+ /**
2
+ * lib/doctor/watchers/graph-staleness.mjs — dependency graph freshness watcher.
3
+ *
4
+ * Ticks every 30 minutes. When registry/contracts/workflow seeds changed since
5
+ * the last `construct graph build`, records an audit entry so doctor surfaces
6
+ * stale-graph drift without waiting for the next Oracle tick.
7
+ */
8
+
9
+ import { existsSync } from 'node:fs';
10
+ import { join } from 'node:path';
11
+
12
+ import { record } from '../audit.mjs';
13
+ import { checkGraphStaleness } from '../../graph/staleness.mjs';
14
+ import { isConstructPackageRepo } from '../../host-disposition.mjs';
15
+
16
+ export const name = 'graph-staleness';
17
+ export const intervalMs = 30 * 60 * 1000;
18
+
19
+ function projectRoot() {
20
+ return process.env.CONSTRUCT_PROJECT_ROOT || process.cwd();
21
+ }
22
+
23
+ export async function tick() {
24
+ const actions = [];
25
+ const escalations = [];
26
+ const root = projectRoot();
27
+ const graphDir = join(root, '.cx', 'graph');
28
+
29
+ if (!existsSync(graphDir) && !isConstructPackageRepo(root)) {
30
+ return { actions, escalations, notes: [{ skipped: 'no graph and not construct package repo' }] };
31
+ }
32
+
33
+ const state = checkGraphStaleness(root);
34
+ if (!state.present) {
35
+ return { actions, escalations, notes: [{ present: false }] };
36
+ }
37
+ if (!state.stale) {
38
+ return { actions, escalations, notes: [{ present: true, stale: false }] };
39
+ }
40
+
41
+ record({
42
+ kind: 'action',
43
+ watcher: name,
44
+ action: 'graph-stale',
45
+ target: '.cx/graph/',
46
+ summary: state.staleReason,
47
+ context: { storedHash: state.storedHash, currentHash: state.currentHash },
48
+ });
49
+ actions.push({ type: 'graph-stale', target: '.cx/graph/' });
50
+
51
+ return { actions, escalations, notes: [state] };
52
+ }
@@ -2,9 +2,10 @@
2
2
  * lib/doctor/watchers/process-pressure.mjs — continuous process pressure guard.
3
3
  *
4
4
  * Runs the runtime-pressure cleanup on a recurring tick so stale Construct
5
- * helpers don't accumulate between `construct up` invocations. Any killed
6
- * process is recorded; if the same service-class has been killed >=2 times
7
- * in the last hour, escalates so cx-sre can investigate the underlying churn.
5
+ * helpers and leaked VHS demo-recorder processes don't accumulate between
6
+ * `construct dev` invocations. Any killed process is recorded; if the same
7
+ * service-class has been killed >=2 times in the last hour, escalation lets
8
+ * cx-sre investigate the underlying churn.
8
9
  *
9
10
  * Tick: 60s.
10
11
  */
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * lib/doctor/watchers/service-health.mjs — continuous service probes.
3
3
  *
4
- * Probes the services `construct up` starts (postgres, dashboard,
4
+ * Probes the services `construct dev` starts (postgres, dashboard,
5
5
  * cm, opencode). On 2 consecutive probe failures, attempts a restart of
6
6
  * docker-backed services (postgres). On 2 consecutive failed
7
7
  * restarts, escalates `service.down` for cx-sre. Non-docker services log +
@@ -1,32 +1,135 @@
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
+
31
+ export const EXPORT_FORMATS = ['pdf', 'docx', 'doc', 'deck', 'pptx', 'html', 'rtf', 'odt', 'epub', 'tex', 'txt', 'md', 'mdx'];
32
+
33
+ const REPO_ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
21
34
 
22
- export const EXPORT_FORMATS = ['pdf', 'docx', 'html'];
35
+ // Pandoc drives typeset formats; pptxgenjs builds slide decks; legacy `.doc` has no
36
+ // pandoc writer — export DOCX first, then LibreOffice headless down-converts.
23
37
 
24
38
  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: [] },
39
+ pdf: { engine: 'pandoc', writer: null, pdfEngine: 'typst', extraBinaries: ['typst'] },
40
+ docx: { engine: 'pandoc', writer: null, pdfEngine: null, extraBinaries: [] },
41
+ doc: { engine: 'pandoc', writer: null, pdfEngine: null, extraBinaries: [], via: 'libreoffice', intermediate: 'docx' },
42
+ deck: { engine: 'pandoc', writer: null, pdfEngine: null, extraBinaries: [] },
43
+ pptx: { engine: 'pptxgenjs', writer: null, pdfEngine: null, extraBinaries: [] },
44
+ html: { engine: 'pandoc', writer: null, pdfEngine: null, extraBinaries: [] },
45
+ rtf: { engine: 'pandoc', writer: 'rtf', pdfEngine: null, extraBinaries: [] },
46
+ odt: { engine: 'pandoc', writer: 'odt', pdfEngine: null, extraBinaries: [] },
47
+ epub: { engine: 'pandoc', writer: 'epub', pdfEngine: null, extraBinaries: [] },
48
+ tex: { engine: 'pandoc', writer: 'latex', pdfEngine: null, extraBinaries: [] },
49
+ txt: { engine: 'pandoc', writer: 'plain', pdfEngine: null, extraBinaries: [] },
50
+ md: { engine: 'copy', writer: null, pdfEngine: null, extraBinaries: [] },
51
+ mdx: { engine: 'copy', writer: null, pdfEngine: null, extraBinaries: [] },
28
52
  };
29
53
 
54
+ export function htmlTemplatePath(repoRoot = REPO_ROOT) {
55
+ const p = path.join(repoRoot, 'templates', 'distribution', 'construct-web.html');
56
+ return fs.existsSync(p) ? p : null;
57
+ }
58
+
59
+ export function deckTemplatePath(repoRoot = REPO_ROOT) {
60
+ const p = path.join(repoRoot, 'templates', 'distribution', 'construct-deck.html');
61
+ return fs.existsSync(p) ? p : null;
62
+ }
63
+
64
+ export function docxReferencePath(repoRoot = REPO_ROOT) {
65
+ const p = path.join(repoRoot, 'templates', 'distribution', 'construct-reference.docx');
66
+ return fs.existsSync(p) ? p : null;
67
+ }
68
+
69
+ export { resolvePdfTemplatePath } from './publish-template.mjs';
70
+
71
+ export function pdfEngineFontOpts(repoRoot = REPO_ROOT) {
72
+ const fontPath = distributionFontsDir(repoRoot);
73
+ return [
74
+ '--pdf-engine-opt', `--font-path=${fontPath}`,
75
+ '--pdf-engine-opt', '--ignore-system-fonts',
76
+ '--pdf-engine-opt', '--ignore-embedded-fonts',
77
+ ];
78
+ }
79
+
80
+ export function countPdfEmbeddedImages(pdfPath) {
81
+ try {
82
+ const data = fs.readFileSync(pdfPath);
83
+ const text = data.toString('latin1');
84
+ const raster = (text.match(/\/Subtype\s*\/Image/g) || []).length;
85
+ const vector = (text.match(/\/Subtype\s*\/Form/g) || []).length;
86
+ return raster + vector;
87
+ } catch {
88
+ return 0;
89
+ }
90
+ }
91
+
92
+ export function pdfRenderedDiagrams(pdfPath, sourceContent) {
93
+ const expected = countDiagramFences(sourceContent);
94
+ if (expected === 0) return true;
95
+ const embedded = countPdfEmbeddedImages(pdfPath);
96
+ if (embedded >= expected) return true;
97
+ try {
98
+ const text = fs.readFileSync(pdfPath).toString('latin1');
99
+ const rawD2 = /direction:\s*(right|down|left|up)/.test(text);
100
+ const rawMermaid = /flowchart\s+(TD|LR|BT|RL)/.test(text);
101
+ return !rawD2 && !rawMermaid && embedded > 0;
102
+ } catch {
103
+ return false;
104
+ }
105
+ }
106
+
107
+ export { pdfUsesBundledBrandSans } from './brand-fonts.mjs';
108
+
109
+ /** @deprecated use pdfUsesBundledBrandSans */
110
+ export function pdfUsesBundledInter(pdfPath) {
111
+ return pdfUsesBundledBrandSans(pdfPath);
112
+ }
113
+
114
+ export function diagramFilterPath() {
115
+ return path.join(REPO_ROOT, 'vendor', 'pandoc-ext', 'diagram.lua');
116
+ }
117
+
118
+ export function detectPdfTemplate({
119
+ artifactType = null,
120
+ cwd = process.cwd(),
121
+ repoRoot = REPO_ROOT,
122
+ } = {}) {
123
+ const templatePath = resolvePdfTemplatePath({ artifactType, cwd, repoRoot });
124
+ return {
125
+ present: Boolean(templatePath),
126
+ path: templatePath,
127
+ message: templatePath
128
+ ? `PDF template: ${path.relative(repoRoot, templatePath)}`
129
+ : 'PDF template missing: templates/distribution/construct-pdf.typ',
130
+ };
131
+ }
132
+
30
133
  function whichBin(name, env = process.env) {
31
134
  const cmd = process.platform === 'win32' ? 'where' : 'which';
32
135
  const result = spawnSync(cmd, [name], { encoding: 'utf8', env });
@@ -45,28 +148,77 @@ function binVersion(name, args = ['--version'], env = process.env) {
45
148
  function installHint(name) {
46
149
  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
150
  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).';
151
+ if (name === 'construct-pdf.typ') return 'Bundled PDF template missing from templates/distribution/construct-pdf.typ (Construct install integrity issue).';
152
+ if (name === 'd2') return 'Install d2 for diagram blocks (`brew install d2`).';
153
+ if (name === 'mmdc') return 'Install mermaid-cli for mermaid blocks (`npm install -g @mermaid-js/mermaid-cli`).';
154
+ if (name === 'pptxgenjs') return 'Install pptxgenjs to enable PPTX export (`npm install pptxgenjs` in the Construct package).';
155
+ if (name === 'construct-deck.html') return 'Bundled deck template missing from templates/distribution/construct-deck.html (Construct install integrity issue).';
156
+ if (name === 'libreoffice') return libreOfficeInstallHint();
48
157
  return `Install ${name} to enable this export format.`;
49
158
  }
50
159
 
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.
54
-
55
- export function detect(format, env = process.env) {
160
+ export function detect(format, env = process.env, {
161
+ figures = false,
162
+ artifactType = null,
163
+ cwd = process.cwd(),
164
+ repoRoot = REPO_ROOT,
165
+ } = {}) {
56
166
  const config = FORMAT_ENGINES[format];
57
167
  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);
168
+ if (config.engine === 'copy') {
169
+ return { ok: true, format, figures: false, present: true, binaries: [], missing: [], message: `Ready: source emit (${format})` };
170
+ }
171
+ const required = [];
172
+ if (config.engine === 'pptxgenjs') {
173
+ required.push('pptxgenjs');
174
+ } else {
175
+ required.push(config.engine, ...(config.extraBinaries || []));
176
+ }
177
+ if (format === 'pdf') {
178
+ const template = detectPdfTemplate({ artifactType, cwd, repoRoot });
179
+ if (!template.present) required.push('construct-pdf.typ');
180
+ }
181
+ if (format === 'deck' && !deckTemplatePath(repoRoot)) {
182
+ required.push('construct-deck.html');
183
+ }
184
+ if (format === 'doc' && !libreOfficePresent(env)) {
185
+ required.push('libreoffice');
186
+ }
187
+ if (figures) {
188
+ if (!fs.existsSync(diagramFilterPath())) required.push('diagram.lua');
189
+ if (!whichBin('d2', env) && !whichBin('dot', env)) required.push('d2');
190
+ if (!whichBin('mmdc', env)) required.push('mmdc');
191
+ }
192
+ const status = required.map((name) => {
193
+ if (name.endsWith('.lua')) {
194
+ return { name, path: fs.existsSync(diagramFilterPath()) ? diagramFilterPath() : null, version: null };
195
+ }
196
+ if (name.endsWith('.html')) {
197
+ return { name, path: deckTemplatePath(repoRoot), version: null };
198
+ }
199
+ if (name === 'pptxgenjs') {
200
+ return { name, path: pptxgenPresent() ? 'bundled' : null, version: null };
201
+ }
202
+ if (name === 'libreoffice') {
203
+ return { name, path: libreOfficePresent(env) ? resolveLibreOfficeBin(env) : null, version: null };
204
+ }
205
+ return { name, path: whichBin(name, env), version: null };
206
+ });
207
+ for (const s of status) {
208
+ if (s.path && !s.name.endsWith('.lua') && s.name !== 'pptxgenjs' && s.name !== 'libreoffice') {
209
+ s.version = binVersion(s.name, ['--version'], env);
210
+ }
211
+ }
61
212
  const missing = status.filter((s) => !s.path).map((s) => s.name);
62
213
  return {
63
214
  ok: true,
64
215
  format,
216
+ figures,
65
217
  present: missing.length === 0,
66
218
  binaries: status,
67
219
  missing,
68
220
  message: missing.length === 0
69
- ? `Ready: ${status.map((s) => `${s.name} (${s.version?.split(' ').slice(0, 2).join(' ') ?? 'unknown'})`).join(', ')}`
221
+ ? `Ready: ${status.map((s) => `${s.name} (${s.version?.split(' ').slice(0, 2).join(' ') ?? 'bundled'})`).join(', ')}`
70
222
  : missing.map(installHint).join(' '),
71
223
  };
72
224
  }
@@ -76,31 +228,187 @@ function defaultOutputPath(inputPath, format) {
76
228
  return path.join(parsed.dir, `${parsed.name}.${format}`);
77
229
  }
78
230
 
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.
231
+ export function parsePublishFrontmatter(inputPath) {
232
+ try {
233
+ const content = fs.readFileSync(inputPath, 'utf8');
234
+ if (!content.startsWith('---')) return null;
235
+ const end = content.indexOf('\n---', 3);
236
+ if (end === -1) return null;
237
+ const block = content.slice(3, end);
238
+ const publish = {};
239
+ let inPublish = false;
240
+ for (const line of block.split('\n')) {
241
+ if (/^publish:\s*$/.test(line)) { inPublish = true; continue; }
242
+ if (inPublish && /^\S/.test(line) && !line.startsWith(' ')) inPublish = false;
243
+ if (!inPublish) continue;
244
+ const m = line.match(/^\s+(\w+):\s*(.+)$/);
245
+ if (m) publish[m[1]] = m[2].replace(/^["']|["']$/g, '').trim();
246
+ }
247
+ return Object.keys(publish).length ? publish : null;
248
+ } catch {
249
+ return null;
250
+ }
251
+ }
84
252
 
85
- export function exportMarkdown({ inputPath, outputPath, format, env = process.env, spawnFn = spawnSync } = {}) {
253
+ function prepareExportInput(inputPath, { format = 'html', figures = false, metadata = null } = {}) {
254
+ let raw = fs.readFileSync(inputPath, 'utf8');
255
+ let processed = raw;
256
+ if ((format === 'pdf' || format === 'html') && metadata?.title) {
257
+ processed = preprocessMarkdownForPdfExport(processed, metadata);
258
+ }
259
+ if (figures) {
260
+ processed = preprocessMarkdownDiagrams(processed);
261
+ }
262
+ if (processed === raw) return { inputPath, cleanup: null };
263
+ const tmp = path.join(path.dirname(inputPath), `.construct-export-${path.basename(inputPath)}`);
264
+ fs.writeFileSync(tmp, processed, 'utf8');
265
+ return {
266
+ inputPath: tmp,
267
+ cleanup: () => { try { fs.unlinkSync(tmp); } catch { /* skip */ } },
268
+ };
269
+ }
270
+
271
+ // md and mdx are emitted straight from the authored source: the artifact already
272
+ // is Markdown, and Construct-authored Markdown is MDX-compatible (fenced diagrams,
273
+ // YAML frontmatter), so no transform is needed beyond writing the target file.
274
+
275
+ function exportSourceCopy({ inputPath, target, format }) {
276
+ const content = fs.readFileSync(inputPath, 'utf8');
277
+ fs.writeFileSync(target, content, 'utf8');
278
+ return {
279
+ ok: true,
280
+ format,
281
+ inputPath,
282
+ outputPath: target,
283
+ engine: 'copy',
284
+ message: `Wrote ${path.relative(process.cwd(), target)}`,
285
+ };
286
+ }
287
+
288
+ export function exportMarkdown({
289
+ inputPath,
290
+ outputPath,
291
+ format,
292
+ figures = false,
293
+ artifactType = null,
294
+ env = process.env,
295
+ spawnFn = spawnSync,
296
+ cwd = process.cwd(),
297
+ repoRoot = REPO_ROOT,
298
+ } = {}) {
86
299
  if (!inputPath) return { ok: false, format, message: 'exportMarkdown: inputPath is required.' };
87
300
  if (!EXPORT_FORMATS.includes(format)) return { ok: false, format, inputPath, message: `Unsupported format: ${format}. Supported: ${EXPORT_FORMATS.join(', ')}.` };
88
301
  if (!fs.existsSync(inputPath)) return { ok: false, format, inputPath, message: `exportMarkdown: input does not exist: ${inputPath}` };
89
302
 
90
- const detection = detect(format, env);
303
+ const metadata = parseArtifactMetadata(inputPath);
304
+ const resolvedType = artifactType || metadata.artifactType || null;
305
+ const effectiveEnv = figures ? buildDistributionDiagramEnv(env) : env;
306
+
307
+ const detection = detect(format, effectiveEnv, { figures, artifactType: resolvedType, cwd, repoRoot });
91
308
  if (!detection.present) {
92
309
  return { ok: false, format, inputPath, missing: detection.missing, message: detection.message };
93
310
  }
94
311
 
95
312
  const config = FORMAT_ENGINES[format];
96
- const target = outputPath || defaultOutputPath(inputPath, format);
313
+ const target = path.resolve(outputPath || defaultOutputPath(inputPath, format));
97
314
  fs.mkdirSync(path.dirname(target), { recursive: true });
98
315
 
316
+ if (config.engine === 'copy') {
317
+ return exportSourceCopy({ inputPath, target, format });
318
+ }
319
+
320
+ if (format === 'pptx') {
321
+ return exportDeckPptx({ inputPath, outputPath: target, metadata, repoRoot });
322
+ }
323
+
324
+ if (format === 'doc') {
325
+ const intermediate = path.join(path.dirname(target), `.construct-export-${path.basename(inputPath, path.extname(inputPath))}.docx`);
326
+ const docxResult = exportMarkdown({
327
+ inputPath,
328
+ outputPath: intermediate,
329
+ format: 'docx',
330
+ figures,
331
+ artifactType: resolvedType,
332
+ env: effectiveEnv,
333
+ spawnFn,
334
+ cwd,
335
+ repoRoot,
336
+ });
337
+ if (!docxResult.ok) return { ...docxResult, format: 'doc' };
338
+ const loResult = convertDocxToDoc({ docxPath: intermediate, outputPath: target, env: effectiveEnv, spawnFn });
339
+ try { fs.unlinkSync(intermediate); } catch { /* skip */ }
340
+ if (!loResult.ok) {
341
+ return {
342
+ ok: false,
343
+ format: 'doc',
344
+ inputPath,
345
+ outputPath: target,
346
+ missing: loResult.missing,
347
+ message: loResult.message,
348
+ };
349
+ }
350
+ return {
351
+ ok: true,
352
+ format: 'doc',
353
+ inputPath,
354
+ outputPath: target,
355
+ engine: 'libreoffice',
356
+ pdfEngine: null,
357
+ figures,
358
+ template: null,
359
+ message: loResult.message,
360
+ };
361
+ }
362
+
363
+ const prepared = prepareExportInput(inputPath, { format, figures, metadata });
364
+ const templatePath = format === 'pdf'
365
+ ? resolvePdfTemplatePath({ artifactType: resolvedType, cwd, repoRoot })
366
+ : null;
367
+
368
+ const metadataArgs = pandocMetadataArgs({
369
+ title: metadata.title,
370
+ subtitle: metadata.subtitle,
371
+ date: metadata.date,
372
+ status: metadata.status,
373
+ owner: metadata.owner,
374
+ artifactType: resolvedType || metadata.artifactType,
375
+ version: metadata.version,
376
+ docId: metadata.docId,
377
+ classification: metadata.classification,
378
+ });
379
+
99
380
  const args = ['-f', 'markdown', '-o', target, '--standalone'];
100
381
  if (config.pdfEngine) args.push(`--pdf-engine=${config.pdfEngine}`);
101
- args.push(inputPath);
382
+ if (format === 'pdf') {
383
+ if (templatePath) args.push('--template', templatePath);
384
+ args.push(...pdfEngineFontOpts(repoRoot));
385
+ args.push(...metadataArgs);
386
+ } else if (format === 'html') {
387
+ const htmlTpl = htmlTemplatePath(repoRoot);
388
+ if (htmlTpl) args.push('--template', htmlTpl);
389
+ args.push('--embed-resources');
390
+ args.push(...metadataArgs);
391
+ } else if (format === 'deck') {
392
+ const deckTpl = deckTemplatePath(repoRoot);
393
+ if (deckTpl) args.push('--template', deckTpl);
394
+ args.push('--section-divs');
395
+ args.push('--embed-resources');
396
+ args.push(...metadataArgs);
397
+ } else if (format === 'docx') {
398
+ const ref = docxReferencePath(repoRoot);
399
+ if (ref) args.push('--reference-doc', ref);
400
+ args.push(...metadataArgs);
401
+ }
402
+ if (figures && fs.existsSync(diagramFilterPath())) {
403
+ args.push('--lua-filter', diagramFilterPath());
404
+ }
405
+ args.push(path.resolve(prepared.inputPath));
406
+
407
+ const spawnCwd = (format === 'pdf' && templatePath) ? path.dirname(templatePath) : path.dirname(prepared.inputPath);
408
+
409
+ const result = spawnFn(config.engine, args, { encoding: 'utf8', env: effectiveEnv, cwd: spawnCwd });
410
+ if (prepared.cleanup) prepared.cleanup();
102
411
 
103
- const result = spawnFn(config.engine, args, { encoding: 'utf8', env });
104
412
  if (result.status !== 0) {
105
413
  return {
106
414
  ok: false,
@@ -112,6 +420,20 @@ export function exportMarkdown({ inputPath, outputPath, format, env = process.en
112
420
  };
113
421
  }
114
422
 
423
+ if (format === 'pdf' && figures) {
424
+ const source = fs.readFileSync(inputPath, 'utf8');
425
+ const expectedDiagrams = countDiagramFences(source);
426
+ if (expectedDiagrams > 0 && !pdfRenderedDiagrams(target, source)) {
427
+ return {
428
+ ok: false,
429
+ format,
430
+ inputPath,
431
+ outputPath: target,
432
+ 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)}`,
433
+ };
434
+ }
435
+ }
436
+
115
437
  return {
116
438
  ok: true,
117
439
  format,
@@ -119,6 +441,8 @@ export function exportMarkdown({ inputPath, outputPath, format, env = process.en
119
441
  outputPath: target,
120
442
  engine: config.engine,
121
443
  pdfEngine: config.pdfEngine,
444
+ figures,
445
+ template: templatePath,
122
446
  message: `Wrote ${path.relative(process.cwd(), target)}`,
123
447
  };
124
448
  }