@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.
- package/README.md +30 -19
- package/bin/construct +487 -87
- package/bin/construct-postinstall.mjs +30 -2
- package/examples/distribution/README.md +42 -0
- package/examples/distribution/manifest.json +53 -0
- package/examples/distribution/sources/adr.md +84 -0
- package/examples/distribution/sources/deck-one-pager.md +65 -0
- package/examples/distribution/sources/prd-platform.md +161 -0
- package/examples/distribution/sources/research-brief.md +80 -0
- package/examples/distribution/sources/rfc-platform.md +82 -0
- package/examples/distribution/sources/runbook.md +103 -0
- package/examples/distribution/sources/strategy.md +88 -0
- package/lib/adapters-sync.mjs +67 -0
- package/lib/artifact-gate-notice.mjs +38 -0
- package/lib/artifact-manifest.mjs +258 -0
- package/lib/artifact-release-gate.mjs +171 -0
- package/lib/artifact-reviewers.mjs +68 -0
- package/lib/artifact-type-from-path.mjs +79 -0
- package/lib/artifact-workflow.mjs +252 -0
- package/lib/audit-skills.mjs +4 -0
- package/lib/audit-specialists.mjs +285 -0
- package/lib/audit-trail.mjs +1 -1
- package/lib/auto-docs.mjs +90 -19
- package/lib/beads/drift.mjs +27 -6
- package/lib/beads-client.mjs +49 -121
- package/lib/beads-optimistic.mjs +5 -12
- package/lib/brand-fonts.mjs +93 -0
- package/lib/brand-prose.mjs +214 -0
- package/lib/brand-tokens.mjs +92 -0
- package/lib/bridges/copilot-proxy.mjs +13 -26
- package/lib/capability-ledger.mjs +156 -0
- package/lib/certification/artifact-fixtures.mjs +132 -0
- package/lib/certification/artifact-gates.mjs +63 -0
- package/lib/certification/artifact-provenance.mjs +97 -0
- package/lib/certification/canonical-scenarios.mjs +78 -0
- package/lib/certification/cli.mjs +191 -0
- package/lib/certification/dashboard-api.mjs +71 -0
- package/lib/certification/demo-parity.mjs +116 -0
- package/lib/certification/document-io-fixtures.mjs +246 -0
- package/lib/certification/document-workflow.mjs +97 -0
- package/lib/certification/eval-bridge.mjs +77 -0
- package/lib/certification/model-routing.mjs +149 -0
- package/lib/certification/prompt-budget.mjs +119 -0
- package/lib/certification/rc-gate.mjs +206 -0
- package/lib/certification/real-llm-scenarios.mjs +303 -0
- package/lib/certification/role-cards.mjs +113 -0
- package/lib/certification/role-overlays.mjs +117 -0
- package/lib/certification/run.mjs +122 -0
- package/lib/certification/runner.mjs +323 -0
- package/lib/certification/scenarios.mjs +51 -0
- package/lib/certification/skill-inventory.mjs +289 -0
- package/lib/certification/skill-scenarios.mjs +147 -0
- package/lib/certification/specialist-contracts.mjs +85 -0
- package/lib/certification/specialist-scenarios.mjs +175 -0
- package/lib/certification/stale-impact.mjs +146 -0
- package/lib/certification/status.mjs +252 -0
- package/lib/certification/store.mjs +77 -0
- package/lib/chat/cli.mjs +333 -0
- package/lib/chat/command-suggest.mjs +161 -0
- package/lib/chat/commands.mjs +215 -0
- package/lib/chat/config.mjs +142 -0
- package/lib/chat/context-compactor.mjs +250 -0
- package/lib/chat/context-continuation.mjs +253 -0
- package/lib/chat/continuation-source.mjs +58 -0
- package/lib/chat/demo-guide.mjs +61 -0
- package/lib/chat/design-tokens.mjs +91 -0
- package/lib/chat/desktop-binary.mjs +79 -0
- package/lib/chat/desktop-build.mjs +130 -0
- package/lib/chat/desktop-launcher.mjs +133 -0
- package/lib/chat/evidence.mjs +145 -0
- package/lib/chat/export.mjs +74 -0
- package/lib/chat/harness/driver.mjs +91 -0
- package/lib/chat/list-picker.mjs +112 -0
- package/lib/chat/model-picker.mjs +356 -0
- package/lib/chat/openrouter-fallback.mjs +151 -0
- package/lib/chat/permission-prompt.mjs +33 -0
- package/lib/chat/picker-catalog.mjs +45 -0
- package/lib/chat/policy-telemetry.mjs +34 -0
- package/lib/chat/present.mjs +246 -0
- package/lib/chat/session-context.mjs +39 -0
- package/lib/chat/session-persist.mjs +73 -0
- package/lib/chat/session-restore.mjs +71 -0
- package/lib/chat/session-settings.mjs +53 -0
- package/lib/chat/system-prompt.mjs +52 -0
- package/lib/chat/transparency.mjs +93 -0
- package/lib/chat/tui/color-scheme.mjs +42 -0
- package/lib/chat/tui/markdown.mjs +123 -0
- package/lib/chat/tui/presentation.mjs +100 -0
- package/lib/chat/tui/render.mjs +500 -0
- package/lib/chat/tui/turn-block.mjs +284 -0
- package/lib/chat/tui/turn-present.mjs +18 -0
- package/lib/chat/tui/turn-state.mjs +88 -0
- package/lib/chat/tui/usage.mjs +122 -0
- package/lib/chat/web-commands.mjs +146 -0
- package/lib/chat/web-launcher.mjs +63 -0
- package/lib/chat/web-picker-keys.mjs +46 -0
- package/lib/chat/web-session.mjs +159 -0
- package/lib/cli-commands.mjs +146 -15
- package/lib/cli-service-inventory.mjs +42 -0
- package/lib/comment-lint.mjs +6 -4
- package/lib/config/intake-policy.mjs +209 -0
- package/lib/config/project-config.mjs +11 -2
- package/lib/config/schema.mjs +86 -6
- package/lib/config/source-targets.mjs +311 -0
- package/lib/contract-schemas/decision.json +50 -0
- package/lib/contract-schemas/implementation.json +51 -0
- package/lib/contract-schemas/review-report.json +32 -0
- package/lib/contract-schemas/test-report.json +43 -0
- package/lib/contracts/construct-handoff.mjs +60 -0
- package/lib/contracts/validate.mjs +32 -3
- package/lib/contracts/violation-log.mjs +58 -5
- package/lib/dashboard-demo.mjs +71 -0
- package/lib/dashboard-static.mjs +19 -5
- package/lib/decisions/registry.mjs +5 -3
- package/lib/deck-export-pptx.mjs +1152 -0
- package/lib/demo-recording.mjs +142 -0
- package/lib/demo-script.mjs +114 -0
- package/lib/demo-surface.mjs +249 -0
- package/lib/demo.mjs +598 -140
- package/lib/diagram-export.mjs +192 -0
- package/lib/diagram.mjs +13 -11
- package/lib/docs-verify.mjs +25 -7
- package/lib/doctor/index.mjs +3 -1
- package/lib/doctor/project-adapters.mjs +44 -0
- package/lib/doctor/source-checkout.mjs +16 -0
- package/lib/doctor/watchers/cx-budget.mjs +98 -0
- package/lib/doctor/watchers/graph-staleness.mjs +52 -0
- package/lib/doctor/watchers/process-pressure.mjs +4 -3
- package/lib/document-export.mjs +384 -38
- package/lib/document-extract.mjs +44 -4
- package/lib/document-ingest.mjs +29 -20
- package/lib/embed/auto-sources.mjs +44 -0
- package/lib/embed/cli.mjs +4 -3
- package/lib/embed/daemon.mjs +18 -46
- package/lib/embed/demand-fetch.mjs +50 -19
- package/lib/embed/inbox.mjs +6 -10
- package/lib/embed/worker.mjs +4 -3
- package/lib/embedded-contract/index.mjs +11 -0
- package/lib/embedded-contract/model-resolve.mjs +10 -9
- package/lib/env-config.mjs +26 -0
- package/lib/evals/dataset.mjs +137 -0
- package/lib/evals/gates.mjs +175 -0
- package/lib/export-branding.mjs +32 -0
- package/lib/graph/build-co-change.mjs +54 -0
- package/lib/graph/build-from-registry.mjs +136 -0
- package/lib/graph/build-import-graph.mjs +153 -0
- package/lib/graph/cli.mjs +110 -0
- package/lib/graph/impact-cli.mjs +89 -0
- package/lib/graph/impact.mjs +108 -0
- package/lib/graph/staleness.mjs +44 -0
- package/lib/graph/store.mjs +172 -0
- package/lib/health-check.mjs +90 -76
- package/lib/hooks/artifact-release-gate.mjs +43 -0
- package/lib/hooks/brand-prose-lint.mjs +38 -0
- package/lib/hooks/graph-impact-advisory.mjs +62 -0
- package/lib/hooks/session-optimize.mjs +84 -207
- package/lib/hooks/session-start.mjs +18 -16
- package/lib/host-disposition.mjs +7 -0
- package/lib/improvement/cli.mjs +189 -0
- package/lib/improvement/controller.mjs +137 -0
- package/lib/improvement/proposal.mjs +120 -0
- package/lib/improvement/specialist-loop.mjs +192 -0
- package/lib/improvement/store.mjs +89 -0
- package/lib/improvement/surface.mjs +219 -0
- package/lib/ingest-tooling.mjs +97 -0
- package/lib/init/doc-lanes.mjs +165 -0
- package/lib/init-docs.mjs +31 -178
- package/lib/init-unified.mjs +50 -119
- package/lib/init-update-guide.mjs +102 -0
- package/lib/init-update.mjs +32 -1
- package/lib/init.mjs +8 -0
- package/lib/install/desktop-binary-download.mjs +85 -0
- package/lib/install/legacy-global-cleanup.mjs +189 -0
- package/lib/intake/constants.mjs +29 -0
- package/lib/intake/daemon.mjs +2 -0
- package/lib/intake/git-queue.mjs +9 -8
- package/lib/intake/intake-config.mjs +52 -105
- package/lib/intake/legacy-paths.mjs +5 -0
- package/lib/intake/queue.mjs +2 -1
- package/lib/intake/session-prelude.mjs +90 -1
- package/lib/libreoffice-export.mjs +97 -0
- package/lib/logging/rotate.mjs +9 -0
- package/lib/maintenance/docker-reclaim.mjs +206 -0
- package/lib/mcp/external-schema-cost.mjs +74 -0
- package/lib/mcp/server.mjs +103 -10
- package/lib/mcp/stdio-mcp-probe.mjs +188 -0
- package/lib/mcp/tool-budget.mjs +55 -4
- package/lib/mcp/tools/document.mjs +18 -4
- package/lib/mcp/tools/embedded-contract.mjs +14 -0
- package/lib/mcp/tools/skills.mjs +32 -3
- package/lib/mcp/tools/workflow.mjs +13 -1
- package/lib/model-free-selector.mjs +18 -0
- package/lib/model-registry.mjs +13 -229
- package/lib/model-router.mjs +373 -92
- package/lib/models/behavior-matrix.mjs +289 -0
- package/lib/models/catalog.mjs +209 -0
- package/lib/models/execution-capability-profile.mjs +196 -0
- package/lib/models/execution-policy.mjs +307 -0
- package/lib/models/provider-poll.mjs +383 -0
- package/lib/npm-spawn-env.mjs +17 -0
- package/lib/ollama/installed-models.mjs +129 -0
- package/lib/oracle/actions.mjs +187 -0
- package/lib/oracle/artifact-gate.mjs +99 -0
- package/lib/oracle/cli.mjs +204 -0
- package/lib/oracle/daemon-entry.mjs +14 -0
- package/lib/oracle/dispatch.mjs +81 -0
- package/lib/oracle/execute.mjs +143 -0
- package/lib/oracle/gaps.mjs +76 -0
- package/lib/oracle/index.mjs +84 -0
- package/lib/oracle/issues.mjs +164 -0
- package/lib/oracle/org-graph.mjs +170 -0
- package/lib/oracle/policy.mjs +80 -0
- package/lib/oracle/read-model.mjs +398 -0
- package/lib/oracle/reconcile.mjs +191 -0
- package/lib/oracle/routing.mjs +89 -0
- package/lib/oracle/synthesize.mjs +384 -0
- package/lib/oracle/verdicts.mjs +51 -0
- package/lib/orchestration/worker.mjs +88 -27
- package/lib/orchestration-policy.mjs +50 -0
- package/lib/parity.mjs +96 -2
- package/lib/playwright-demo.mjs +254 -0
- package/lib/project-init-shared.mjs +4 -1
- package/lib/prompt-composer.js +7 -3
- package/lib/prompt-validation-contract.mjs +24 -0
- package/lib/provider-capabilities.js +57 -11
- package/lib/providers/contract/adapters/confluence/index.mjs +181 -0
- package/lib/providers/contract/adapters/git/index.mjs +115 -0
- package/lib/providers/contract/adapters/github/index.mjs +166 -0
- package/lib/providers/contract/adapters/jira/index.mjs +187 -0
- package/lib/providers/contract/adapters/slack/index.mjs +175 -0
- package/lib/providers/contract/contract-tests.mjs +57 -0
- package/lib/providers/contract/errors.mjs +48 -0
- package/lib/providers/contract/interface.mjs +50 -0
- package/lib/providers/contract/registry.mjs +102 -0
- package/lib/providers/copilot-auth.mjs +297 -0
- package/lib/providers/credential-bootstrap.mjs +178 -0
- package/lib/providers/credential-catalog.mjs +46 -0
- package/lib/providers/credential-sources.mjs +63 -0
- package/lib/providers/creds.mjs +5 -2
- package/lib/providers/op-run.mjs +59 -0
- package/lib/providers/secret-resolver.mjs +159 -0
- package/lib/publish-template.mjs +163 -0
- package/lib/publish-tooling.mjs +119 -0
- package/lib/publish.mjs +305 -0
- package/lib/registry/cli.mjs +82 -0
- package/lib/registry/consolidation.mjs +147 -0
- package/lib/registry/generate-docs.mjs +75 -0
- package/lib/registry/skill-verification.mjs +57 -0
- package/lib/registry/surface-map.mjs +76 -0
- package/lib/registry/validate.mjs +135 -0
- package/lib/resources/budget.mjs +82 -0
- package/lib/resources/process-budget.mjs +45 -0
- package/lib/rules-delivery.mjs +9 -2
- package/lib/rules-read.mjs +26 -0
- package/lib/runtime-env.mjs +23 -0
- package/lib/runtime-pressure.mjs +51 -7
- package/lib/schema-infer.mjs +13 -2
- package/lib/server/chat-loop.mjs +622 -0
- package/lib/server/demo-preview.mjs +63 -0
- package/lib/server/index.mjs +259 -9
- package/lib/server/rate-limit.mjs +5 -3
- package/lib/server/webhook.mjs +3 -2
- package/lib/service-manager.mjs +60 -8
- package/lib/setup.mjs +70 -7
- package/lib/specialists/prompt-schema.mjs +19 -10
- package/lib/specialists/roster.mjs +43 -0
- package/lib/specialists/scaffold.mjs +56 -0
- package/lib/storage/backend.mjs +6 -6
- package/lib/storage/hybrid-query.mjs +7 -4
- package/lib/storage/state-source.mjs +5 -5
- package/lib/storage/sync.mjs +2 -3
- package/lib/telemetry/rule-calls.mjs +52 -0
- package/lib/template-registry.mjs +2 -2
- package/lib/templates/visual-requirements.mjs +27 -51
- package/lib/test-corpus-inventory.mjs +313 -0
- package/lib/uninstall/uninstall.mjs +19 -7
- package/lib/update.mjs +12 -0
- package/lib/upgrade.mjs +14 -0
- package/lib/wireframe.mjs +20 -14
- package/lib/worker/run.mjs +17 -4
- package/lib/worker/trace.mjs +11 -3
- package/package.json +28 -14
- package/personas/construct.md +2 -2
- package/platforms/claude/settings.template.json +36 -0
- package/rules/common/patterns.md +1 -1
- package/rules/common/release-gates.md +4 -3
- package/scripts/sync-specialists.mjs +41 -28
- package/skills/devops/data-engineering.md +1 -1
- package/skills/docs/adr-workflow.md +1 -0
- package/skills/docs/backlog-proposal-workflow.md +1 -0
- package/skills/docs/codebase-research-workflow.md +40 -0
- package/skills/docs/customer-profile-workflow.md +1 -0
- package/skills/docs/document-ingest-workflow.md +1 -0
- package/skills/docs/evidence-ingest-workflow.md +1 -0
- package/skills/docs/init-docs.md +2 -2
- package/skills/docs/init-project.md +7 -2
- package/skills/docs/prd-workflow.md +24 -1
- package/skills/docs/prfaq-workflow.md +1 -0
- package/skills/docs/product-intelligence-workflow.md +1 -0
- package/skills/docs/product-signal-workflow.md +1 -0
- package/skills/docs/research-workflow.md +54 -37
- package/skills/docs/runbook-workflow.md +1 -0
- package/skills/docs/strategy-workflow.md +1 -0
- package/skills/docs/user-research-workflow.md +40 -0
- package/skills/operating/orchestration-reference.md +1 -1
- package/skills/roles/architect.md +5 -0
- package/skills/roles/operator.docs.md +4 -0
- package/skills/routing.md +4 -2
- package/specialists/artifact-manifest.json +489 -0
- package/specialists/artifact-manifest.schema.json +83 -0
- package/specialists/audit-enrichments.json +454 -0
- package/specialists/contracts.json +37 -25
- package/specialists/prompts/_shared/validation-contract.md +26 -0
- package/specialists/prompts/cx-accessibility.md +21 -2
- package/specialists/prompts/cx-ai-engineer.md +24 -1
- package/specialists/prompts/cx-architect.md +4 -1
- package/specialists/prompts/cx-business-strategist.md +23 -2
- package/specialists/prompts/cx-data-analyst.md +22 -1
- package/specialists/prompts/cx-data-engineer.md +23 -2
- package/specialists/prompts/cx-debugger.md +21 -2
- package/specialists/prompts/cx-designer.md +26 -3
- package/specialists/prompts/cx-devil-advocate.md +19 -2
- package/specialists/prompts/cx-docs-keeper.md +28 -1
- package/specialists/prompts/cx-engineer.md +22 -1
- package/specialists/prompts/cx-evaluator.md +18 -1
- package/specialists/prompts/cx-explorer.md +21 -2
- package/specialists/prompts/cx-legal-compliance.md +21 -2
- package/specialists/prompts/cx-operations.md +21 -2
- package/specialists/prompts/cx-oracle.md +94 -0
- package/specialists/prompts/cx-orchestrator.md +20 -1
- package/specialists/prompts/cx-platform-engineer.md +25 -2
- package/specialists/prompts/cx-product-manager.md +32 -1
- package/specialists/prompts/cx-qa.md +24 -2
- package/specialists/prompts/cx-rd-lead.md +23 -2
- package/specialists/prompts/cx-release-manager.md +23 -2
- package/specialists/prompts/cx-researcher.md +22 -2
- package/specialists/prompts/cx-reviewer.md +18 -1
- package/specialists/prompts/cx-security.md +22 -2
- package/specialists/prompts/cx-sre.md +30 -3
- package/specialists/prompts/cx-test-automation.md +7 -1
- package/specialists/prompts/cx-trace-reviewer.md +22 -3
- package/specialists/prompts/cx-ux-researcher.md +21 -2
- package/specialists/registry.json +52 -173
- package/specialists/tone-profiles.json +42 -0
- package/templates/demos/playwright/demo-recording.config.mjs +47 -0
- package/templates/demos/recordings/agentic-platforms-prd.json +19 -0
- package/templates/demos/scripts/agentic-platforms-prd.json +44 -0
- package/templates/demos/specs/_helpers/scroll-artifact.ts +89 -0
- package/templates/demos/tapes/agentic-platforms-prd.tape +49 -0
- package/templates/demos/tapes/resource-guard-rails.tape +49 -0
- package/templates/demos/vhs/construct-cockpit.json +24 -0
- package/templates/distribution/construct-brand.typ +446 -0
- package/templates/distribution/construct-decision.typ +38 -0
- package/templates/distribution/construct-deck.html +95 -0
- package/templates/distribution/construct-pdf.typ +38 -0
- package/templates/distribution/construct-prd.typ +38 -0
- package/templates/distribution/construct-reference.docx +0 -0
- package/templates/distribution/construct-research.typ +38 -0
- package/templates/distribution/construct-web.html +92 -0
- package/templates/distribution/fonts/Geist-Bold.ttf +0 -0
- package/templates/distribution/fonts/Geist-Medium.ttf +0 -0
- package/templates/distribution/fonts/Geist-Regular.ttf +0 -0
- package/templates/distribution/fonts/Geist-SemiBold.ttf +0 -0
- package/templates/distribution/fonts/GeistMono-Medium.ttf +0 -0
- package/templates/distribution/fonts/GeistMono-Regular.ttf +0 -0
- package/templates/distribution/fonts/GeistMono-SemiBold.ttf +0 -0
- package/templates/distribution/fonts/IBMPlexMono-Regular.otf +0 -0
- package/templates/distribution/fonts/JetBrainsMono-Medium.ttf +0 -0
- package/templates/distribution/fonts/JetBrainsMono-Regular.ttf +0 -0
- package/templates/distribution/fonts/JetBrainsMono-SemiBold.ttf +0 -0
- package/templates/distribution/fonts/PlusJakartaSans-Bold.ttf +0 -0
- package/templates/distribution/fonts/PlusJakartaSans-Medium.ttf +0 -0
- package/templates/distribution/fonts/PlusJakartaSans-Regular.ttf +0 -0
- package/templates/distribution/fonts/PlusJakartaSans-SemiBold.ttf +0 -0
- package/templates/distribution/fonts/README.md +36 -0
- package/templates/distribution/fonts/SpaceGrotesk-Variable.ttf +0 -0
- package/templates/distribution/fonts/handwritten/Caveat.ttf +0 -0
- package/templates/distribution/fonts/legacy/IBMPlexMono-Regular.otf +0 -0
- package/templates/distribution/fonts/legacy/Inter-Medium.otf +0 -0
- package/templates/distribution/fonts/legacy/Inter-Regular.otf +0 -0
- package/templates/distribution/fonts/legacy/Inter-SemiBold.otf +0 -0
- package/templates/distribution/fonts/legacy/InterDisplay-SemiBold.otf +0 -0
- package/templates/distribution/fonts/legacy/SourceSerif4-Regular.otf +0 -0
- package/templates/distribution/fonts/legacy/SourceSerif4-Semibold.otf +0 -0
- package/templates/distribution/icons/activity.svg +15 -0
- package/templates/distribution/icons/alert-triangle.svg +17 -0
- package/templates/distribution/icons/book-open.svg +16 -0
- package/templates/distribution/icons/bot.svg +20 -0
- package/templates/distribution/icons/brain.svg +22 -0
- package/templates/distribution/icons/circle-check.svg +16 -0
- package/templates/distribution/icons/clipboard-check.svg +17 -0
- package/templates/distribution/icons/cpu.svg +28 -0
- package/templates/distribution/icons/database.svg +17 -0
- package/templates/distribution/icons/eye.svg +16 -0
- package/templates/distribution/icons/file-text.svg +19 -0
- package/templates/distribution/icons/gauge.svg +16 -0
- package/templates/distribution/icons/git-branch.svg +17 -0
- package/templates/distribution/icons/key.svg +17 -0
- package/templates/distribution/icons/layers.svg +17 -0
- package/templates/distribution/icons/list-checks.svg +19 -0
- package/templates/distribution/icons/lock.svg +16 -0
- package/templates/distribution/icons/message-square.svg +15 -0
- package/templates/distribution/icons/network.svg +19 -0
- package/templates/distribution/icons/route.svg +17 -0
- package/templates/distribution/icons/scale.svg +19 -0
- package/templates/distribution/icons/search.svg +16 -0
- package/templates/distribution/icons/send.svg +16 -0
- package/templates/distribution/icons/server.svg +18 -0
- package/templates/distribution/icons/shield-check.svg +16 -0
- package/templates/distribution/icons/users.svg +18 -0
- package/templates/distribution/icons/webhook.svg +17 -0
- package/templates/distribution/icons/workflow.svg +17 -0
- package/templates/distribution/icons/wrench.svg +15 -0
- package/templates/distribution/run.mjs +18 -1
- package/templates/docs/adr.md +7 -0
- package/templates/docs/construct_guide.md +8 -8
- package/templates/docs/customer-profile.md +4 -0
- package/templates/docs/one-pager.md +4 -0
- package/templates/docs/postmortem.md +31 -0
- package/templates/docs/prd-platform.md +19 -0
- package/templates/docs/prd.md +15 -0
- package/templates/docs/prfaq.md +7 -0
- package/templates/docs/qa-strategy.md +103 -0
- package/templates/docs/research-brief.md +8 -0
- package/templates/docs/rfc-platform.md +12 -0
- package/templates/docs/test-plan.md +7 -0
package/lib/document-export.mjs
CHANGED
|
@@ -1,32 +1,136 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* lib/document-export.mjs —
|
|
3
|
-
* binaries (Pandoc + Typst
|
|
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
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
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 text — see 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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
52
|
-
|
|
53
|
-
|
|
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
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
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(' ') ?? '
|
|
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
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
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
|
-
|
|
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
|
|
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 =
|
|
96
|
-
|
|
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
|
-
|
|
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
|
}
|
package/lib/document-extract.mjs
CHANGED
|
@@ -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
|
|
735
|
-
//
|
|
736
|
-
//
|
|
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 —
|
|
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 });
|