@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.
- package/README.md +30 -18
- package/bin/construct +392 -91
- 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 +112 -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/audit-skills.mjs +4 -0
- package/lib/audit-specialists.mjs +285 -0
- package/lib/audit-trail.mjs +77 -10
- package/lib/auto-docs.mjs +87 -18
- 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 +245 -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 +232 -11
- package/lib/comment-lint.mjs +6 -4
- package/lib/config/schema.mjs +36 -5
- 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 +703 -0
- package/lib/diagram-export.mjs +192 -0
- package/lib/diagram.mjs +302 -0
- package/lib/docs-verify.mjs +25 -7
- package/lib/doctor/index.mjs +4 -2
- package/lib/doctor/project-adapters.mjs +44 -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/doctor/watchers/service-health.mjs +1 -1
- package/lib/document-export.mjs +360 -36
- package/lib/document-extract.mjs +88 -0
- package/lib/document-ingest.mjs +32 -18
- package/lib/embed/cli.mjs +2 -2
- package/lib/embed/daemon.mjs +18 -46
- package/lib/embed/semantic.mjs +5 -2
- package/lib/embed/worker.mjs +2 -2
- 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/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/handoffs/cleanup.mjs +1 -1
- package/lib/health-check.mjs +90 -76
- package/lib/hooks/artifact-release-gate.mjs +43 -0
- package/lib/hooks/audit-trail.mjs +14 -28
- 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 +4 -5
- 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 +27 -97
- 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/daemon.mjs +2 -0
- package/lib/intake/git-queue.mjs +9 -8
- 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 +84 -9
- 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/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 +425 -93
- 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/persona-sections.mjs +66 -0
- package/lib/playwright-demo.mjs +252 -0
- package/lib/project-init-shared.mjs +4 -1
- package/lib/prompt-composer.js +9 -4
- 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 +229 -8
- package/lib/server/langfuse-login.mjs +3 -3
- package/lib/service-manager.mjs +61 -9
- package/lib/session-store.mjs +1 -1
- package/lib/setup.mjs +102 -2
- 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/embeddings-local.mjs +6 -3
- package/lib/storage/file-lock.mjs +18 -11
- 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 +29 -13
- package/personas/construct.md +13 -13
- 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 +187 -60
- 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 +8 -3
- 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 +480 -0
- package/specialists/artifact-manifest.schema.json +53 -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-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 +10 -10
- 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/ingest/chunker.mjs +0 -94
- package/lib/ingest/pipeline.mjs +0 -53
- package/lib/ingest/store.mjs +0 -82
- package/lib/mode-commands.mjs +0 -122
- package/lib/policy/unified-gates.mjs +0 -96
- package/lib/profiles/validate-custom.mjs +0 -114
- package/lib/services/telemetry-backend.mjs +0 -177
- package/lib/storage/fusion.mjs +0 -95
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* lib/diagram-export.mjs — distribution diagram styling for publish exports.
|
|
3
|
+
*
|
|
4
|
+
* Publish path renders hand-drawn D2 (--sketch geometry) and Mermaid (handDrawn
|
|
5
|
+
* look, Caveat handwriting) in a monochrome ink palette — black/grey strokes,
|
|
6
|
+
* light surfaces, near-black text — for an Excalidraw-adjacent human aesthetic
|
|
7
|
+
* that stays consistent with the document brand. Color is reserved for explicit
|
|
8
|
+
* per-node emphasis in the source, not the default theme.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import fs from 'node:fs';
|
|
12
|
+
import path from 'node:path';
|
|
13
|
+
import { fileURLToPath } from 'node:url';
|
|
14
|
+
|
|
15
|
+
const REPO_ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
|
|
16
|
+
const ICON_DIR = path.join(REPO_ROOT, 'templates', 'distribution', 'icons');
|
|
17
|
+
|
|
18
|
+
// Monochrome diagram palette, mirroring the ink ramp in construct-brand.typ so
|
|
19
|
+
// embedded diagrams and surrounding body share one design language.
|
|
20
|
+
|
|
21
|
+
const MONO = {
|
|
22
|
+
ink: '#0a0c10',
|
|
23
|
+
inkBody: '#23272e',
|
|
24
|
+
muted: '#565c66',
|
|
25
|
+
faint: '#9499a2',
|
|
26
|
+
hairline: '#e3e4e8',
|
|
27
|
+
hairlineStrong: '#cdd0d6',
|
|
28
|
+
surface: '#fafafa',
|
|
29
|
+
surfaceAlt: '#f3f4f6',
|
|
30
|
+
paper: '#ffffff',
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
export const DISTRIBUTION_D2_THEME = '0';
|
|
34
|
+
export const DISTRIBUTION_D2_PAD = '16';
|
|
35
|
+
export const DISTRIBUTION_D2_SCALE = '0.72';
|
|
36
|
+
export const DISTRIBUTION_D2_SKETCH = '1';
|
|
37
|
+
export const DISTRIBUTION_D2_FONT_SIZE = '14';
|
|
38
|
+
export const DISTRIBUTION_MERMAID_MIME = 'image/png';
|
|
39
|
+
export const DISTRIBUTION_MERMAID_WIDTH = '480';
|
|
40
|
+
export const DISTRIBUTION_MERMAID_SCALE = '1';
|
|
41
|
+
export const DISTRIBUTION_FIGURE_MAX_WIDTH = '58%';
|
|
42
|
+
|
|
43
|
+
// Handwritten font for Mermaid labels so its hand-drawn look matches D2 sketch
|
|
44
|
+
// mode. Caveat ships bundled (templates/distribution/fonts/handwritten) and is
|
|
45
|
+
// installed for the headless Chrome that mmdc drives; the fallbacks stay in the
|
|
46
|
+
// handwriting register so a missing face never drops to a serif.
|
|
47
|
+
|
|
48
|
+
const MERMAID_FONT = 'Caveat, Comic Sans MS, cursive';
|
|
49
|
+
|
|
50
|
+
const MERMAID_INIT = `%%{init: {
|
|
51
|
+
'theme': 'base',
|
|
52
|
+
'look': 'handDrawn',
|
|
53
|
+
'fontFamily': '${MERMAID_FONT}',
|
|
54
|
+
'themeVariables': {
|
|
55
|
+
'fontFamily': '${MERMAID_FONT}',
|
|
56
|
+
'fontSize': '19px',
|
|
57
|
+
'primaryColor': '${MONO.surfaceAlt}',
|
|
58
|
+
'primaryTextColor': '${MONO.ink}',
|
|
59
|
+
'primaryBorderColor': '${MONO.ink}',
|
|
60
|
+
'lineColor': '${MONO.muted}',
|
|
61
|
+
'secondaryColor': '${MONO.paper}',
|
|
62
|
+
'secondaryTextColor': '${MONO.ink}',
|
|
63
|
+
'secondaryBorderColor': '${MONO.ink}',
|
|
64
|
+
'tertiaryColor': '${MONO.surface}',
|
|
65
|
+
'tertiaryTextColor': '${MONO.ink}',
|
|
66
|
+
'tertiaryBorderColor': '${MONO.hairlineStrong}',
|
|
67
|
+
'clusterBkg': '${MONO.surface}',
|
|
68
|
+
'clusterBorder': '${MONO.hairlineStrong}',
|
|
69
|
+
'edgeLabelBackground': '${MONO.paper}',
|
|
70
|
+
'titleColor': '${MONO.ink}',
|
|
71
|
+
'noteBkgColor': '${MONO.surface}',
|
|
72
|
+
'noteTextColor': '${MONO.inkBody}',
|
|
73
|
+
'noteBorderColor': '${MONO.hairlineStrong}',
|
|
74
|
+
'actorBkg': '${MONO.surfaceAlt}',
|
|
75
|
+
'actorBorder': '${MONO.ink}',
|
|
76
|
+
'actorTextColor': '${MONO.ink}',
|
|
77
|
+
'actorLineColor': '${MONO.hairlineStrong}',
|
|
78
|
+
'signalColor': '${MONO.inkBody}',
|
|
79
|
+
'signalTextColor': '${MONO.inkBody}',
|
|
80
|
+
'labelBoxBkgColor': '${MONO.surface}',
|
|
81
|
+
'labelBoxBorderColor': '${MONO.hairlineStrong}',
|
|
82
|
+
'labelTextColor': '${MONO.ink}',
|
|
83
|
+
'loopTextColor': '${MONO.inkBody}',
|
|
84
|
+
'activationBkgColor': '${MONO.hairline}',
|
|
85
|
+
'activationBorderColor': '${MONO.muted}'
|
|
86
|
+
}
|
|
87
|
+
}}%%`;
|
|
88
|
+
|
|
89
|
+
const CHROME_CANDIDATES = [
|
|
90
|
+
'/Applications/Google Chrome.app/Contents/MacOS/Google Chrome',
|
|
91
|
+
'/Applications/Chromium.app/Contents/MacOS/Chromium',
|
|
92
|
+
'/usr/bin/google-chrome',
|
|
93
|
+
'/usr/bin/chromium',
|
|
94
|
+
'/usr/bin/chromium-browser',
|
|
95
|
+
];
|
|
96
|
+
|
|
97
|
+
export function resolvePuppeteerExecutable(env = process.env) {
|
|
98
|
+
if (env.PUPPETEER_EXECUTABLE_PATH) return env.PUPPETEER_EXECUTABLE_PATH;
|
|
99
|
+
for (const candidate of CHROME_CANDIDATES) {
|
|
100
|
+
try {
|
|
101
|
+
if (fs.existsSync(candidate)) return candidate;
|
|
102
|
+
} catch {
|
|
103
|
+
/* skip */
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
return null;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
export function injectMermaidBrandTheme(code) {
|
|
110
|
+
const body = String(code || '').trim();
|
|
111
|
+
if (!body) return body;
|
|
112
|
+
if (/%%\{init:/.test(body)) return body;
|
|
113
|
+
return `${MERMAID_INIT}\n${body}\n`;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// `icon: @name` in a d2 block resolves to the bundled monochrome Lucide icon at
|
|
117
|
+
// templates/distribution/icons/name.svg, keeping source markdown portable while
|
|
118
|
+
// d2 (which runs in a temp dir) still receives an absolute path it can read.
|
|
119
|
+
|
|
120
|
+
export function resolveIconTokens(code) {
|
|
121
|
+
return String(code || '').replace(
|
|
122
|
+
/icon:\s*@([a-z0-9-]+)/gi,
|
|
123
|
+
(match, name) => {
|
|
124
|
+
const file = path.join(ICON_DIR, `${name}.svg`);
|
|
125
|
+
return fs.existsSync(file) ? `icon: ${file}` : match;
|
|
126
|
+
},
|
|
127
|
+
);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
export function injectD2DistributionDefaults(code) {
|
|
131
|
+
const body = resolveIconTokens(String(code || '').trim());
|
|
132
|
+
if (!body) return body;
|
|
133
|
+
if (/CONSTRUCT_D2_DEFAULTS/.test(body)) return body;
|
|
134
|
+
return `**: {
|
|
135
|
+
style.font-size: ${DISTRIBUTION_D2_FONT_SIZE}
|
|
136
|
+
style.font-color: "${MONO.ink}"
|
|
137
|
+
style.stroke: "${MONO.ink}"
|
|
138
|
+
style.fill: "${MONO.surfaceAlt}"
|
|
139
|
+
}
|
|
140
|
+
(** -> **)[*]: {
|
|
141
|
+
style.stroke: "${MONO.muted}"
|
|
142
|
+
style.font-color: "${MONO.inkBody}"
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
${body}
|
|
146
|
+
`;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
export function preprocessMarkdownDiagrams(content) {
|
|
150
|
+
let out = String(content);
|
|
151
|
+
out = out.replace(/```mermaid\n([\s\S]*?)```/g, (_match, inner) => `\`\`\`mermaid\n${injectMermaidBrandTheme(inner)}\`\`\``);
|
|
152
|
+
out = out.replace(/```d2\n([\s\S]*?)```/g, (_match, inner) => `\`\`\`d2\n${injectD2DistributionDefaults(inner)}\`\`\``);
|
|
153
|
+
return out;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
export function countDiagramFences(content) {
|
|
157
|
+
const matches = String(content || '').match(/```(?:d2|mermaid)\n/g);
|
|
158
|
+
return matches ? matches.length : 0;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
export function buildDistributionDiagramEnv(baseEnv = process.env) {
|
|
162
|
+
const env = {
|
|
163
|
+
...baseEnv,
|
|
164
|
+
CONSTRUCT_D2_THEME: DISTRIBUTION_D2_THEME,
|
|
165
|
+
CONSTRUCT_D2_PAD: DISTRIBUTION_D2_PAD,
|
|
166
|
+
CONSTRUCT_D2_SCALE: DISTRIBUTION_D2_SCALE,
|
|
167
|
+
CONSTRUCT_D2_SKETCH: DISTRIBUTION_D2_SKETCH,
|
|
168
|
+
CONSTRUCT_MERMAID_THEME: 'construct',
|
|
169
|
+
CONSTRUCT_MERMAID_MIME: DISTRIBUTION_MERMAID_MIME,
|
|
170
|
+
CONSTRUCT_MERMAID_WIDTH: DISTRIBUTION_MERMAID_WIDTH,
|
|
171
|
+
CONSTRUCT_MERMAID_SCALE: DISTRIBUTION_MERMAID_SCALE,
|
|
172
|
+
};
|
|
173
|
+
const chrome = resolvePuppeteerExecutable(baseEnv);
|
|
174
|
+
if (chrome) env.PUPPETEER_EXECUTABLE_PATH = chrome;
|
|
175
|
+
return env;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
export function distributionDiagramDefaults() {
|
|
179
|
+
return {
|
|
180
|
+
d2Theme: 'neutral',
|
|
181
|
+
d2ThemeId: DISTRIBUTION_D2_THEME,
|
|
182
|
+
d2Sketch: true,
|
|
183
|
+
d2Scale: Number(DISTRIBUTION_D2_SCALE),
|
|
184
|
+
d2FontSize: Number(DISTRIBUTION_D2_FONT_SIZE),
|
|
185
|
+
figureMaxWidth: DISTRIBUTION_FIGURE_MAX_WIDTH,
|
|
186
|
+
mermaidLook: 'handDrawn',
|
|
187
|
+
mermaidTheme: 'construct',
|
|
188
|
+
mermaidWidth: Number(DISTRIBUTION_MERMAID_WIDTH),
|
|
189
|
+
mermaidScale: Number(DISTRIBUTION_MERMAID_SCALE),
|
|
190
|
+
accent: MONO.ink,
|
|
191
|
+
};
|
|
192
|
+
}
|
package/lib/diagram.mjs
ADDED
|
@@ -0,0 +1,302 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* lib/diagram.mjs — `construct diagram` command, generates code-driven
|
|
3
|
+
* architecture/flow diagrams from a natural-language description and renders
|
|
4
|
+
* them to SVG/PNG via an external diagram binary, degrading to source-only
|
|
5
|
+
* output when no renderer is installed.
|
|
6
|
+
*
|
|
7
|
+
* Tool evaluation (2026-06), against: visual distinctiveness, reproducible
|
|
8
|
+
* from code, headless Node/CI SVG+PNG render, theming, license:
|
|
9
|
+
* - D2 (terrastruct, MPL-2.0): single self-contained Go binary, clean neutral
|
|
10
|
+
* theme plus `--sketch` hand-drawn mode (publish distribution path), headless
|
|
11
|
+
* `d2 in.d2 out.svg|png`, deterministic from a text file. Chosen as PRIMARY.
|
|
12
|
+
* - Graphviz `dot` (EPL-1.0): ubiquitous, almost always already present on
|
|
13
|
+
* CI images and dev machines, headless `dot -Tsvg|-Tpng`. Chosen as
|
|
14
|
+
* FALLBACK for reach when D2 is absent.
|
|
15
|
+
* - Mermaid: renders natively in many viewers but headless image export
|
|
16
|
+
* needs mermaid-cli, which pulls a headless-Chromium npm tree — disallowed
|
|
17
|
+
* by ADR-0001 (zero-npm-core). Used only as the SOURCE we emit when no
|
|
18
|
+
* binary is present, reusing lib/wireframe.mjs's Mermaid generators so the
|
|
19
|
+
* description still yields a viewable diagram in GitHub/Notion/OpenCode.
|
|
20
|
+
*
|
|
21
|
+
* Degradation contract: when neither D2 nor dot is on PATH the command still
|
|
22
|
+
* succeeds (exit 0) by writing the diagram SOURCE (.d2 by default, or .mmd
|
|
23
|
+
* Mermaid for diagram kinds wireframe.mjs already models) plus an install
|
|
24
|
+
* hint. No bundled binaries, no npm dependencies; renderers are detected at
|
|
25
|
+
* runtime, mirroring lib/runtime/whisper-bootstrap.mjs.
|
|
26
|
+
*
|
|
27
|
+
* Output: .cx/diagrams/<slug>-<ts>.<ext>.
|
|
28
|
+
*/
|
|
29
|
+
import { existsSync, mkdirSync, writeFileSync } from 'node:fs';
|
|
30
|
+
import { spawnSync } from 'node:child_process';
|
|
31
|
+
import { join, resolve, dirname } from 'node:path';
|
|
32
|
+
|
|
33
|
+
import { generateWireframe } from './wireframe.mjs';
|
|
34
|
+
|
|
35
|
+
const TYPES = ['architecture', 'flow', 'sequence', 'state', 'er', 'class'];
|
|
36
|
+
const FORMATS = ['svg', 'png'];
|
|
37
|
+
|
|
38
|
+
function slugify(text) {
|
|
39
|
+
return String(text || '')
|
|
40
|
+
.toLowerCase()
|
|
41
|
+
.replace(/[^a-z0-9]+/g, '-')
|
|
42
|
+
.replace(/^-+|-+$/g, '')
|
|
43
|
+
.slice(0, 60) || 'diagram';
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function timestamp() {
|
|
47
|
+
return new Date().toISOString().replace(/[:.]/g, '-').slice(0, 19);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function which(bin) {
|
|
51
|
+
const result = spawnSync(process.platform === 'win32' ? 'where' : 'which', [bin], { encoding: 'utf8' });
|
|
52
|
+
if (result.status !== 0) return null;
|
|
53
|
+
return result.stdout.trim().split('\n')[0] || null;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// Renderer detection follows the optional-external-tool pattern: probe PATH,
|
|
57
|
+
// return a structured descriptor, never throw. D2 is preferred for its
|
|
58
|
+
// distinctive look; dot is the ubiquitous fallback.
|
|
59
|
+
|
|
60
|
+
export function locateRenderer() {
|
|
61
|
+
const d2 = which('d2');
|
|
62
|
+
if (d2) return { engine: 'd2', binary: d2, sourceExt: 'd2' };
|
|
63
|
+
const dot = which('dot');
|
|
64
|
+
if (dot) return { engine: 'dot', binary: dot, sourceExt: 'dot' };
|
|
65
|
+
return null;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export function installHint() {
|
|
69
|
+
if (process.platform === 'darwin') return 'brew install d2 (or: brew install graphviz)';
|
|
70
|
+
if (process.platform === 'linux') return 'curl -fsSL https://d2lang.com/install.sh | sh (or install graphviz via your distro)';
|
|
71
|
+
return 'See https://d2lang.com/tour/install or https://graphviz.org/download/';
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function inferType(description, explicit) {
|
|
75
|
+
if (explicit) return explicit;
|
|
76
|
+
const text = String(description || '').toLowerCase();
|
|
77
|
+
if (/\b(sequence|handshake|request|response|api call)\b/.test(text)) return 'sequence';
|
|
78
|
+
if (/\b(state|status|transition|machine|lifecycle)\b/.test(text)) return 'state';
|
|
79
|
+
if (/\b(entity|schema|database|table|relationship|foreign key)\b/.test(text)) return 'er';
|
|
80
|
+
if (/\b(class|inheritance|interface)\b/.test(text)) return 'class';
|
|
81
|
+
if (/\b(flow|funnel|step|decision|process)\b/.test(text)) return 'flow';
|
|
82
|
+
return 'architecture';
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// Parse `a -> b -> c` (or `a → b → c`) chains out of the description so a
|
|
86
|
+
// terse architecture prompt produces a real edge graph rather than a single
|
|
87
|
+
// node. Falls back to treating the whole description as one node.
|
|
88
|
+
|
|
89
|
+
function parseNodes(description) {
|
|
90
|
+
const text = String(description || '');
|
|
91
|
+
const arrowSplit = text.split(/\s*(?:->|→|=>)\s*/).map((s) => s.trim()).filter(Boolean);
|
|
92
|
+
if (arrowSplit.length >= 2) {
|
|
93
|
+
const stripped = arrowSplit.map((s) => s.replace(/^[a-z0-9\s]*:\s*/i, '').trim() || s);
|
|
94
|
+
return stripped.map((label) => label.split(/[,/]/)[0].trim()).filter(Boolean);
|
|
95
|
+
}
|
|
96
|
+
return [];
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function nodeId(label, index) {
|
|
100
|
+
const id = String(label).toLowerCase().replace(/[^a-z0-9]+/g, '_').replace(/^_+|_+$/g, '');
|
|
101
|
+
return id || `n${index}`;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function d2Source(description, nodes) {
|
|
105
|
+
const header = `# ${description}\n# D2 source — render: d2 this.d2 out.svg (https://d2lang.com)\n`;
|
|
106
|
+
if (nodes.length >= 2) {
|
|
107
|
+
const lines = nodes.map((label, i) => `${nodeId(label, i)}: ${label}`);
|
|
108
|
+
for (let i = 0; i < nodes.length - 1; i += 1) {
|
|
109
|
+
lines.push(`${nodeId(nodes[i], i)} -> ${nodeId(nodes[i + 1], i + 1)}`);
|
|
110
|
+
}
|
|
111
|
+
return `${header}${lines.join('\n')}\n`;
|
|
112
|
+
}
|
|
113
|
+
return `${header}node: ${description || 'Component'}\n`;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function dotSource(description, nodes) {
|
|
117
|
+
const header = `// ${description}\n// Graphviz DOT source — render: dot -Tsvg this.dot -o out.svg\n`;
|
|
118
|
+
const body = [];
|
|
119
|
+
if (nodes.length >= 2) {
|
|
120
|
+
nodes.forEach((label, i) => body.push(` ${nodeId(label, i)} [label="${label}"];`));
|
|
121
|
+
for (let i = 0; i < nodes.length - 1; i += 1) {
|
|
122
|
+
body.push(` ${nodeId(nodes[i], i)} -> ${nodeId(nodes[i + 1], i + 1)};`);
|
|
123
|
+
}
|
|
124
|
+
} else {
|
|
125
|
+
body.push(` node [label="${description || 'Component'}"];`);
|
|
126
|
+
}
|
|
127
|
+
return `${header}digraph G {\n rankdir=LR;\n${body.join('\n')}\n}\n`;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// For diagram kinds wireframe.mjs already models (flow/sequence/state/er) we
|
|
131
|
+
// reuse its Mermaid generators when emitting source-only output, so the
|
|
132
|
+
// description maps to a richer diagram than a bare D2 chain.
|
|
133
|
+
|
|
134
|
+
const WIREFRAME_KINDS = { flow: 'flow', sequence: 'sequence', state: 'state', er: 'er' };
|
|
135
|
+
|
|
136
|
+
export function generateDiagram({ description = '', type = null, theme = null } = {}) {
|
|
137
|
+
const kind = inferType(description, type);
|
|
138
|
+
const nodes = parseNodes(description);
|
|
139
|
+
|
|
140
|
+
let source;
|
|
141
|
+
let engineHint;
|
|
142
|
+
if (nodes.length >= 2 || kind === 'architecture' || kind === 'class') {
|
|
143
|
+
source = d2Source(description, nodes);
|
|
144
|
+
engineHint = 'd2';
|
|
145
|
+
} else {
|
|
146
|
+
const wf = generateWireframe({ description, type: WIREFRAME_KINDS[kind] || 'flow' });
|
|
147
|
+
source = wf.content;
|
|
148
|
+
engineHint = 'mermaid';
|
|
149
|
+
}
|
|
150
|
+
return { kind, nodes, source, engineHint, theme };
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// Render via the detected binary. D2 themes map to numeric ids; an invalid
|
|
154
|
+
// theme name is ignored rather than failing the render. Returns the render
|
|
155
|
+
// result so the caller can fall back to source-only on any non-zero exit.
|
|
156
|
+
|
|
157
|
+
function renderWithD2(binary, sourcePath, outPath, { format, theme }) {
|
|
158
|
+
const args = [];
|
|
159
|
+
const themeName = theme?.toLowerCase();
|
|
160
|
+
if (themeName === 'sketch') {
|
|
161
|
+
args.push('--sketch');
|
|
162
|
+
} else if (themeName) {
|
|
163
|
+
const themeId = D2_THEMES[themeName];
|
|
164
|
+
if (themeId != null) args.push('--theme', String(themeId));
|
|
165
|
+
}
|
|
166
|
+
args.push(sourcePath, outPath);
|
|
167
|
+
return spawnSync(binary, args, { encoding: 'utf8', timeout: 60_000 });
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
const D2_THEMES = {
|
|
171
|
+
neutral: 0,
|
|
172
|
+
'neutral-grey': 1,
|
|
173
|
+
'flagship-terrastruct': 3,
|
|
174
|
+
'cool-classics': 4,
|
|
175
|
+
'mixed-berry-blue': 5,
|
|
176
|
+
'grape-soda': 6,
|
|
177
|
+
aubergine: 7,
|
|
178
|
+
'colorblind-clear': 8,
|
|
179
|
+
'vanilla-nitro-cola': 100,
|
|
180
|
+
'orange-creamsicle': 101,
|
|
181
|
+
};
|
|
182
|
+
|
|
183
|
+
function renderWithDot(binary, sourcePath, outPath, { format }) {
|
|
184
|
+
return spawnSync(binary, [`-T${format}`, sourcePath, '-o', outPath], { encoding: 'utf8', timeout: 60_000 });
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
export function printHelp() {
|
|
188
|
+
console.log(`Usage: construct diagram <description> [options]
|
|
189
|
+
|
|
190
|
+
Generates a code-driven diagram from a description and renders it to SVG/PNG
|
|
191
|
+
via D2 (primary) or Graphviz dot (fallback). When neither renderer is
|
|
192
|
+
installed, writes the diagram SOURCE plus an install hint and exits 0.
|
|
193
|
+
|
|
194
|
+
Options:
|
|
195
|
+
--type <t> architecture (default) | flow | sequence | state | er | class
|
|
196
|
+
--format <f> svg (default) | png
|
|
197
|
+
--theme <name> D2 theme name (default: neutral; sketch enables --sketch hand-drawn)
|
|
198
|
+
--out <path> Output path (default: .cx/diagrams/<slug>-<ts>.<ext>)
|
|
199
|
+
--source-only Always write the source file; skip rendering
|
|
200
|
+
-h, --help Show this message
|
|
201
|
+
|
|
202
|
+
Renderers (detected at runtime, never bundled):
|
|
203
|
+
D2 ${process.platform === 'darwin' ? 'brew install d2' : 'https://d2lang.com/tour/install'}
|
|
204
|
+
dot ${process.platform === 'darwin' ? 'brew install graphviz' : 'https://graphviz.org/download/'}
|
|
205
|
+
|
|
206
|
+
Examples:
|
|
207
|
+
construct diagram "web app: client -> api -> db"
|
|
208
|
+
construct diagram "auth flow" --type flow
|
|
209
|
+
construct diagram "client -> api -> db" --format png --theme neutral
|
|
210
|
+
`);
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
function parseArgs(argv) {
|
|
214
|
+
const options = { type: null, format: 'svg', theme: null, out: null, sourceOnly: false, description: [] };
|
|
215
|
+
for (let i = 0; i < argv.length; i += 1) {
|
|
216
|
+
const arg = argv[i];
|
|
217
|
+
if (arg === '--help' || arg === '-h') { options.help = true; continue; }
|
|
218
|
+
if (arg === '--source-only') { options.sourceOnly = true; continue; }
|
|
219
|
+
if (arg === '--type') { options.type = argv[++i]; continue; }
|
|
220
|
+
if (arg.startsWith('--type=')) { options.type = arg.slice(7); continue; }
|
|
221
|
+
if (arg === '--format') { options.format = argv[++i]; continue; }
|
|
222
|
+
if (arg.startsWith('--format=')) { options.format = arg.slice(9); continue; }
|
|
223
|
+
if (arg === '--theme') { options.theme = argv[++i]; continue; }
|
|
224
|
+
if (arg.startsWith('--theme=')) { options.theme = arg.slice(8); continue; }
|
|
225
|
+
if (arg === '--out') { options.out = argv[++i]; continue; }
|
|
226
|
+
if (arg.startsWith('--out=')) { options.out = arg.slice(6); continue; }
|
|
227
|
+
options.description.push(arg);
|
|
228
|
+
}
|
|
229
|
+
options.description = options.description.join(' ').trim();
|
|
230
|
+
return options;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
export async function runDiagramCli(argv = [], { cwd = process.cwd() } = {}) {
|
|
234
|
+
const options = parseArgs(argv);
|
|
235
|
+
if (options.help || !options.description) {
|
|
236
|
+
printHelp();
|
|
237
|
+
if (!options.help && !options.description) process.exit(1);
|
|
238
|
+
return;
|
|
239
|
+
}
|
|
240
|
+
if (options.type && !TYPES.includes(options.type)) {
|
|
241
|
+
console.error(`Unknown type: ${options.type}. Valid: ${TYPES.join(', ')}`);
|
|
242
|
+
process.exit(1);
|
|
243
|
+
}
|
|
244
|
+
if (!FORMATS.includes(options.format)) {
|
|
245
|
+
console.error(`Unknown format: ${options.format}. Valid: ${FORMATS.join(', ')}`);
|
|
246
|
+
process.exit(1);
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
const effectiveTheme = options.theme || 'neutral';
|
|
250
|
+
const result = generateDiagram({ description: options.description, type: options.type, theme: effectiveTheme });
|
|
251
|
+
const renderer = options.sourceOnly ? null : locateRenderer();
|
|
252
|
+
|
|
253
|
+
const slug = slugify(options.description);
|
|
254
|
+
const dir = options.out ? dirname(resolve(cwd, options.out)) : join(cwd, '.cx', 'diagrams');
|
|
255
|
+
if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
|
|
256
|
+
|
|
257
|
+
// Source is always written. When the source is Mermaid (reused wireframe
|
|
258
|
+
// output) the .d2/.dot renderers can't consume it, so we keep the source
|
|
259
|
+
// extension honest and skip the binary render in that case.
|
|
260
|
+
|
|
261
|
+
const renderableSource = result.engineHint !== 'mermaid';
|
|
262
|
+
const sourceExt = result.engineHint === 'mermaid' ? 'md' : (renderer ? renderer.sourceExt : 'd2');
|
|
263
|
+
const baseName = options.out
|
|
264
|
+
? resolve(cwd, options.out).replace(/\.[^./]+$/, '')
|
|
265
|
+
: join(dir, `${slug}-${timestamp()}`);
|
|
266
|
+
const sourcePath = `${baseName}.${sourceExt}`;
|
|
267
|
+
|
|
268
|
+
let source = result.source;
|
|
269
|
+
if (renderer && renderableSource) {
|
|
270
|
+
source = renderer.engine === 'dot' ? dotSource(options.description, result.nodes) : d2Source(options.description, result.nodes);
|
|
271
|
+
}
|
|
272
|
+
writeFileSync(sourcePath, source, 'utf8');
|
|
273
|
+
|
|
274
|
+
if (!renderer || !renderableSource) {
|
|
275
|
+
console.log(`Diagram source (${result.kind}, ${result.engineHint}) written to:`);
|
|
276
|
+
console.log(` ${sourcePath}`);
|
|
277
|
+
if (!renderer) {
|
|
278
|
+
console.log(`\nNo diagram renderer found. To render SVG/PNG, install one:`);
|
|
279
|
+
console.log(` ${installHint()}`);
|
|
280
|
+
} else {
|
|
281
|
+
console.log(`\nThis diagram kind renders as Mermaid; paste into any Mermaid-aware viewer (GitHub, Notion, OpenCode).`);
|
|
282
|
+
}
|
|
283
|
+
return;
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
const outPath = `${baseName}.${options.format}`;
|
|
287
|
+
const render = renderer.engine === 'd2'
|
|
288
|
+
? renderWithD2(renderer.binary, sourcePath, outPath, { format: options.format, theme: effectiveTheme })
|
|
289
|
+
: renderWithDot(renderer.binary, sourcePath, outPath, { format: options.format });
|
|
290
|
+
|
|
291
|
+
if (render.status === 0 && existsSync(outPath)) {
|
|
292
|
+
console.log(`Diagram (${result.kind}) rendered via ${renderer.engine} to:`);
|
|
293
|
+
console.log(` ${outPath}`);
|
|
294
|
+
console.log(`Source: ${sourcePath}`);
|
|
295
|
+
return;
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
console.log(`Diagram source written to:`);
|
|
299
|
+
console.log(` ${sourcePath}`);
|
|
300
|
+
console.log(`\nRenderer ${renderer.engine} failed (exit ${render.status}); source preserved.`);
|
|
301
|
+
if (render.stderr) console.log(` ${render.stderr.trim().split('\n').slice(0, 3).join('\n ')}`);
|
|
302
|
+
}
|
package/lib/docs-verify.mjs
CHANGED
|
@@ -20,6 +20,7 @@
|
|
|
20
20
|
import fs from "node:fs";
|
|
21
21
|
import path from "node:path";
|
|
22
22
|
import { statSync, readFileSync, existsSync } from "node:fs";
|
|
23
|
+
import { spawnSync } from "node:child_process";
|
|
23
24
|
import { hasIntakeReference } from "./intake/traceability.mjs";
|
|
24
25
|
import { ensureCxDir } from "./project-init-shared.mjs";
|
|
25
26
|
|
|
@@ -27,6 +28,7 @@ const args = process.argv.slice(2);
|
|
|
27
28
|
const quickMode = args.includes("--quick");
|
|
28
29
|
const fixMode = args.includes("--fix");
|
|
29
30
|
const stagedMode = args.includes("--staged");
|
|
31
|
+
const strictMode = args.includes("--strict") || process.env.CI === 'true';
|
|
30
32
|
const cwdArg = args.find(arg => arg.startsWith("--cwd="));
|
|
31
33
|
const cwd = cwdArg ? path.resolve(cwdArg.split("=")[1]) : process.cwd();
|
|
32
34
|
|
|
@@ -195,6 +197,17 @@ function checkContextHasProgress(contextPath) {
|
|
|
195
197
|
// not every artifact necessarily comes from an intake packet — but the absence
|
|
196
198
|
// is a flag for the operator to either stamp the provenance or declare it
|
|
197
199
|
// intake-independent.
|
|
200
|
+
function gitTrackedMdFiles(cwd, dir) {
|
|
201
|
+
if (!fs.existsSync(dir)) return [];
|
|
202
|
+
const rel = path.relative(cwd, dir).split(path.sep).join('/');
|
|
203
|
+
const result = spawnSync('git', ['ls-files', '--', `${rel}/`], { cwd, encoding: 'utf8' });
|
|
204
|
+
if (result.status !== 0 || !result.stdout) return [];
|
|
205
|
+
return result.stdout
|
|
206
|
+
.split('\n')
|
|
207
|
+
.filter((line) => line.endsWith('.md') && !line.endsWith('/README.md'))
|
|
208
|
+
.map((line) => path.join(cwd, line));
|
|
209
|
+
}
|
|
210
|
+
|
|
198
211
|
function checkIntakeTraceability(cwd) {
|
|
199
212
|
const intakeFedDirs = [
|
|
200
213
|
path.join(cwd, 'docs', 'prd'),
|
|
@@ -203,13 +216,10 @@ function checkIntakeTraceability(cwd) {
|
|
|
203
216
|
];
|
|
204
217
|
let inspected = 0;
|
|
205
218
|
for (const dir of intakeFedDirs) {
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
if (!entry.isFile() || !entry.name.endsWith('.md')) continue;
|
|
211
|
-
if (entry.name.startsWith('_') || entry.name.toLowerCase() === 'readme.md') continue;
|
|
212
|
-
const filePath = path.join(dir, entry.name);
|
|
219
|
+
const files = gitTrackedMdFiles(cwd, dir);
|
|
220
|
+
for (const filePath of files) {
|
|
221
|
+
const base = path.basename(filePath);
|
|
222
|
+
if (base.startsWith('_')) continue;
|
|
213
223
|
inspected += 1;
|
|
214
224
|
if (!hasIntakeReference(filePath)) {
|
|
215
225
|
warnings.push(
|
|
@@ -438,6 +448,14 @@ function main() {
|
|
|
438
448
|
console.log("💡 Run `construct init:update` to update project to current standards");
|
|
439
449
|
process.exit(1);
|
|
440
450
|
}
|
|
451
|
+
|
|
452
|
+
if (strictMode && warnings.length > 0) {
|
|
453
|
+
console.log("❌ Strict mode: warnings are blocking:");
|
|
454
|
+
for (const warning of warnings) {
|
|
455
|
+
console.log(` • ${warning}`);
|
|
456
|
+
}
|
|
457
|
+
process.exit(1);
|
|
458
|
+
}
|
|
441
459
|
|
|
442
460
|
console.log("✅ Documentation validation passed with warnings");
|
|
443
461
|
process.exit(0);
|
package/lib/doctor/index.mjs
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* lib/doctor/index.mjs — `construct-doctor` daemon main loop.
|
|
3
3
|
*
|
|
4
|
-
* Long-running Node process spawned by `construct
|
|
4
|
+
* Long-running Node process spawned by `construct dev`. Holds the L0 watchers
|
|
5
5
|
* for resource pressure, service health, disk + log rotation, and cost. Each
|
|
6
6
|
* watcher ticks on its own interval; the loop is bounded so a slow watcher
|
|
7
7
|
* never starves the others. Termination via SIGTERM/SIGINT triggers a clean
|
|
@@ -24,9 +24,11 @@ import * as bdWatch from './watchers/bd-watch.mjs';
|
|
|
24
24
|
import * as handoffs from './watchers/handoffs.mjs';
|
|
25
25
|
import * as consistency from './watchers/consistency.mjs';
|
|
26
26
|
import * as mcpProtocol from './watchers/mcp-protocol.mjs';
|
|
27
|
+
import * as cxBudget from './watchers/cx-budget.mjs';
|
|
28
|
+
import * as graphStaleness from './watchers/graph-staleness.mjs';
|
|
27
29
|
|
|
28
30
|
const STATE_PATH = join(homedir(), '.construct', 'doctor.json');
|
|
29
|
-
const WATCHERS = [disk, cost, processPressure, serviceHealth, bdWatch, handoffs, consistency, mcpProtocol];
|
|
31
|
+
const WATCHERS = [disk, cost, processPressure, serviceHealth, bdWatch, handoffs, consistency, mcpProtocol, cxBudget, graphStaleness];
|
|
30
32
|
|
|
31
33
|
let running = false;
|
|
32
34
|
let timers = [];
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* lib/doctor/project-adapters.mjs — doctor checks for project-scoped host adapters.
|
|
3
|
+
*
|
|
4
|
+
* When inside a Construct-managed project, verifies gitignored adapter artifacts
|
|
5
|
+
* exist for installed hosts (.cursor/mcp.json, .vscode/mcp.json, construct.mdc).
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { checkProjectParity } from '../parity.mjs';
|
|
9
|
+
import { detectHostCapabilities } from '../host-capabilities.mjs';
|
|
10
|
+
|
|
11
|
+
const HOST_REQUIRES = {
|
|
12
|
+
cursor: ['cursor'],
|
|
13
|
+
vscode: ['vscode'],
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
export function checkProjectAdaptersForDoctor({ rootDir, projectDir = rootDir }) {
|
|
17
|
+
const parity = checkProjectParity({ rootDir, projectDir });
|
|
18
|
+
if (parity.skipped) return { ok: true, label: 'Project adapters (not a Construct project)', warning: false };
|
|
19
|
+
|
|
20
|
+
const installed = new Set(
|
|
21
|
+
detectHostCapabilities()
|
|
22
|
+
.filter((h) => h.availability === 'installed')
|
|
23
|
+
.map((h) => h.host.toLowerCase().replace(/\s+/g, '')),
|
|
24
|
+
);
|
|
25
|
+
const installedIds = new Set();
|
|
26
|
+
if (installed.has('claudecode') || installed.has('claude')) installedIds.add('claude');
|
|
27
|
+
if (installed.has('vscode')) installedIds.add('vscode');
|
|
28
|
+
if (installed.has('cursor')) installedIds.add('cursor');
|
|
29
|
+
|
|
30
|
+
const failures = [];
|
|
31
|
+
for (const surface of parity.surfaces) {
|
|
32
|
+
if (surface.status === 'absent' && (surface.surface === 'cursor' || surface.surface === 'vscode')) {
|
|
33
|
+
if (installedIds.has(surface.surface)) failures.push(surface);
|
|
34
|
+
}
|
|
35
|
+
if (surface.status === 'drift') failures.push(surface);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const ok = failures.length === 0;
|
|
39
|
+
const label = ok
|
|
40
|
+
? `Project adapters (${parity.summary.filter((s) => s.includes(': ok')).length}/${parity.surfaces.length} ok)`
|
|
41
|
+
: `Project adapters missing or stale — run \`npm run adapters\` (${failures.map((f) => f.surface).join(', ')})`;
|
|
42
|
+
|
|
43
|
+
return { ok, label, warning: !ok, parity };
|
|
44
|
+
}
|