@geraldmaron/construct 1.0.0 → 1.0.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/.env.example +10 -7
- package/LICENSE +201 -21
- package/README.md +132 -257
- package/agents/contracts.json +387 -0
- package/agents/prompts/cx-accessibility.md +8 -0
- package/agents/prompts/cx-ai-engineer.md +92 -0
- package/agents/prompts/cx-architect.md +10 -2
- package/agents/prompts/cx-business-strategist.md +13 -0
- package/agents/prompts/cx-data-analyst.md +80 -0
- package/agents/prompts/cx-data-engineer.md +4 -0
- package/agents/prompts/cx-debugger.md +8 -0
- package/agents/prompts/cx-designer.md +19 -0
- package/agents/prompts/cx-devil-advocate.md +4 -0
- package/agents/prompts/cx-docs-keeper.md +128 -0
- package/agents/prompts/cx-engineer.md +13 -0
- package/agents/prompts/cx-evaluator.md +4 -0
- package/agents/prompts/cx-explorer.md +12 -0
- package/agents/prompts/cx-legal-compliance.md +13 -0
- package/agents/prompts/cx-operations.md +13 -0
- package/agents/prompts/cx-orchestrator.md +107 -4
- package/agents/prompts/cx-platform-engineer.md +75 -0
- package/agents/prompts/cx-product-manager.md +8 -0
- package/agents/prompts/cx-qa.md +100 -0
- package/agents/prompts/cx-rd-lead.md +13 -0
- package/agents/prompts/cx-release-manager.md +8 -0
- package/agents/prompts/cx-researcher.md +21 -1
- package/agents/prompts/cx-reviewer.md +8 -0
- package/agents/prompts/cx-security.md +104 -0
- package/agents/prompts/cx-sre.md +104 -0
- package/agents/prompts/cx-test-automation.md +4 -0
- package/agents/prompts/cx-trace-reviewer.md +7 -3
- package/agents/prompts/cx-ux-researcher.md +4 -0
- package/agents/registry.json +365 -90
- package/agents/role-manifests.json +217 -0
- package/bin/construct +3345 -141
- package/bin/construct-postinstall.mjs +87 -0
- package/commands/build/feature.md +6 -6
- package/commands/plan/feature.md +6 -5
- package/commands/plan/requirements.md +1 -1
- package/commands/ship/status.md +1 -1
- package/commands/work/drive.md +3 -3
- package/commands/work/optimize-prompts.md +3 -3
- package/db/{migrations → schema}/001_init.sql +2 -0
- package/db/schema/002_pgvector.sql +182 -0
- package/db/schema/003_intake.sql +47 -0
- package/examples/README.md +85 -0
- package/examples/internal/roles/architect/bad/clever-plan-without-contracts.md +30 -0
- package/examples/internal/roles/architect/golden/explicit-tradeoff-before-plan.md +23 -0
- package/examples/internal/roles/engineer/bad/speculative-abstraction.md +30 -0
- package/examples/internal/roles/engineer/golden/read-before-write.md +28 -0
- package/examples/internal/roles/orchestrator/bad/everything-becomes-multi-agent.md +29 -0
- package/examples/internal/roles/orchestrator/golden/minimal-dispatch.md +22 -0
- package/examples/internal/roles/qa/bad/coverage-theater.md +30 -0
- package/examples/internal/roles/qa/golden/regression-gate.md +23 -0
- package/examples/internal/roles/reviewer/bad/lgtm-without-verification.md +29 -0
- package/examples/internal/roles/reviewer/golden/find-structural-risk-first.md +28 -0
- package/examples/personas/construct/adversarial/ignore-instruction-to-skip-approval.md +22 -0
- package/examples/personas/construct/bad/commit-without-approval.md +30 -0
- package/examples/personas/construct/boundary/blocked-needs-main-input.md +29 -0
- package/examples/personas/construct/golden/branch-approval-before-mutation.md +36 -0
- package/examples/personas/construct/golden/focused-direct-answer.md +28 -0
- package/examples/provider-plugin/README.md +34 -0
- package/examples/provider-plugin/index.mjs +74 -0
- package/examples/provider-plugin/package.json +15 -0
- package/examples/seed-observations/README.md +38 -0
- package/examples/seed-observations/anti-patterns.md +44 -0
- package/examples/seed-observations/decisions.md +36 -0
- package/examples/seed-observations/patterns.md +42 -0
- package/lib/agent-contracts-enforce.mjs +158 -0
- package/lib/agent-contracts.mjs +231 -0
- package/lib/agents/postconditions.mjs +126 -0
- package/lib/agents/schema.mjs +124 -0
- package/lib/artifact-capture.mjs +183 -0
- package/lib/audit-trail.mjs +149 -0
- package/lib/auto-docs.mjs +245 -65
- package/lib/beads/auto-close.mjs +126 -0
- package/lib/beads/drift.mjs +171 -0
- package/lib/beads-automation.mjs +542 -0
- package/lib/beads-client.mjs +518 -0
- package/lib/beads-lock.mjs +377 -0
- package/lib/beads-optimistic.mjs +365 -0
- package/lib/bootstrap/built-ins.mjs +136 -0
- package/lib/bootstrap/lazy-install.mjs +161 -0
- package/lib/bootstrap/resources.mjs +120 -0
- package/lib/bootstrap.mjs +105 -0
- package/lib/cache-governor.js +213 -0
- package/lib/cache-strategy-anthropic.js +62 -0
- package/lib/cache-strategy-google.js +79 -0
- package/lib/cache-strategy-none.js +30 -0
- package/lib/cache-strategy-openai.js +48 -0
- package/lib/cache-strategy.js +91 -0
- package/lib/claude-allow.mjs +149 -0
- package/lib/cli-commands.mjs +413 -258
- package/lib/codex-config.mjs +3 -1
- package/lib/comment-lint.mjs +55 -6
- package/lib/completions.mjs +21 -6
- package/lib/config/alias.mjs +56 -0
- package/lib/config/project-config.mjs +335 -0
- package/lib/config/schema.mjs +159 -0
- package/lib/context-router.mjs +308 -0
- package/lib/cost-ledger.mjs +177 -0
- package/lib/cost.mjs +171 -10
- package/lib/dashboard-static.mjs +158 -0
- package/lib/deployment-mode.mjs +86 -0
- package/lib/deprecate.mjs +49 -0
- package/lib/dispatch-batch.js +183 -0
- package/lib/distill.mjs +21 -8
- package/lib/doc-stamp.mjs +164 -0
- package/lib/doc-verify.mjs +119 -0
- package/lib/docs-routing.mjs +89 -0
- package/lib/docs-verify.mjs +417 -0
- package/lib/doctor/audit.mjs +71 -0
- package/lib/doctor/cli.mjs +99 -0
- package/lib/doctor/escalate.mjs +29 -0
- package/lib/doctor/index.mjs +140 -0
- package/lib/doctor/report.mjs +170 -0
- package/lib/doctor/watchers/bd-watch.mjs +117 -0
- package/lib/doctor/watchers/cost.mjs +130 -0
- package/lib/doctor/watchers/disk.mjs +122 -0
- package/lib/doctor/watchers/handoffs.mjs +33 -0
- package/lib/doctor/watchers/process-pressure.mjs +60 -0
- package/lib/doctor/watchers/service-health.mjs +188 -0
- package/lib/document-extract.mjs +288 -0
- package/lib/document-ingest.mjs +230 -0
- package/lib/drop.mjs +282 -0
- package/lib/embed/approval-queue.mjs +176 -0
- package/lib/embed/artifact.mjs +349 -0
- package/lib/embed/authority-guard.mjs +155 -0
- package/lib/embed/cli.mjs +408 -0
- package/lib/embed/config.mjs +355 -0
- package/lib/embed/conflict-detection.mjs +264 -0
- package/lib/embed/customer-profiles.mjs +480 -0
- package/lib/embed/daemon.mjs +1309 -0
- package/lib/embed/demand-fetch.mjs +449 -0
- package/lib/embed/docs-lifecycle.mjs +349 -0
- package/lib/embed/inbox-live-watcher.mjs +119 -0
- package/lib/embed/inbox.mjs +343 -0
- package/lib/embed/intake-metrics.mjs +190 -0
- package/lib/embed/jobs/vector-sync.mjs +198 -0
- package/lib/embed/notifications.mjs +75 -0
- package/lib/embed/output.mjs +79 -0
- package/lib/embed/providers/github.mjs +295 -0
- package/lib/embed/providers/jira.mjs +192 -0
- package/lib/embed/providers/linear.mjs +186 -0
- package/lib/embed/providers/registry.mjs +115 -0
- package/lib/embed/providers/slack.mjs +203 -0
- package/lib/embed/recommendation-store.mjs +378 -0
- package/lib/embed/roadmap.mjs +374 -0
- package/lib/embed/role-framing.mjs +110 -0
- package/lib/embed/scheduler.mjs +99 -0
- package/lib/embed/semantic.mjs +325 -0
- package/lib/embed/snapshot.mjs +191 -0
- package/lib/embed/supervision.mjs +235 -0
- package/lib/embed/target-resolver.mjs +186 -0
- package/lib/embed/worker.mjs +62 -0
- package/lib/embed/workspaces.mjs +297 -0
- package/lib/engine/chunker-headings.mjs +110 -0
- package/lib/engine/compressor-heuristic.mjs +100 -0
- package/lib/engine/consolidate.mjs +287 -0
- package/lib/engine/contracts.mjs +126 -0
- package/lib/engine/defaults.mjs +129 -0
- package/lib/engine/eval-retrieval.mjs +148 -0
- package/lib/engine/fuser-rrf.mjs +62 -0
- package/lib/engine/index.mjs +37 -0
- package/lib/engine/registry.mjs +146 -0
- package/lib/engine/reranker-mmr.mjs +90 -0
- package/lib/engine/tokens.mjs +77 -0
- package/lib/entity-store.mjs +280 -0
- package/lib/env-config.mjs +69 -4
- package/lib/evals/retrieval-bench.mjs +159 -0
- package/lib/evaluator-optimizer.mjs +317 -0
- package/lib/features.mjs +159 -29
- package/lib/gates-audit.mjs +236 -0
- package/lib/git-hooks/prepare-commit-msg +58 -0
- package/lib/handoffs/cleanup.mjs +159 -0
- package/lib/handoffs/contract.mjs +162 -0
- package/lib/handoffs/inventory.mjs +120 -0
- package/lib/headhunt.mjs +28 -47
- package/lib/health-check.mjs +399 -0
- package/lib/hook-health.mjs +442 -0
- package/lib/hooks/_lib/log.mjs +82 -0
- package/lib/hooks/adaptive-lint.mjs +26 -2
- package/lib/hooks/agent-tracker.mjs +171 -14
- package/lib/hooks/audit-reads.mjs +109 -0
- package/lib/hooks/audit-trail.mjs +153 -0
- package/lib/hooks/bash-output-logger.mjs +71 -0
- package/lib/hooks/block-no-verify.mjs +41 -0
- package/lib/hooks/ci-status-check.mjs +82 -0
- package/lib/hooks/comment-lint.mjs +29 -7
- package/lib/hooks/config-protection.mjs +39 -18
- package/lib/hooks/context-watch.mjs +137 -0
- package/lib/hooks/context-window-recovery.mjs +4 -20
- package/lib/hooks/dep-audit.mjs +16 -0
- package/lib/hooks/doc-coupling-check.mjs +80 -0
- package/lib/hooks/edit-accumulator.mjs +3 -0
- package/lib/hooks/edit-error-recovery.mjs +3 -0
- package/lib/hooks/edit-guard.mjs +45 -4
- package/lib/hooks/env-check.mjs +3 -0
- package/lib/hooks/guard-bash.mjs +58 -1
- package/lib/hooks/mcp-audit.mjs +18 -20
- package/lib/hooks/mcp-health-check.mjs +36 -0
- package/lib/hooks/model-fallback.mjs +42 -56
- package/lib/hooks/policy-engine.mjs +209 -0
- package/lib/hooks/post-merge-docs-check.mjs +63 -0
- package/lib/hooks/pre-compact.mjs +4 -24
- package/lib/hooks/pre-push-gate.mjs +200 -37
- package/lib/hooks/proactive-activation.mjs +284 -0
- package/lib/hooks/read-tracker.mjs +27 -4
- package/lib/hooks/readme-age-check.mjs +77 -0
- package/lib/hooks/registry-sync.mjs +12 -5
- package/lib/hooks/scan-secrets.mjs +50 -7
- package/lib/hooks/session-optimize.mjs +311 -0
- package/lib/hooks/session-start.mjs +287 -28
- package/lib/hooks/stop-notify.mjs +236 -96
- package/lib/hooks/stop-typecheck.mjs +13 -1
- package/lib/hooks/test-watch.mjs +68 -0
- package/lib/host-capabilities.mjs +31 -10
- package/lib/init-docs.mjs +731 -291
- package/lib/init-unified.mjs +1116 -0
- package/lib/init-update.mjs +168 -0
- package/lib/init.mjs +107 -0
- package/lib/install/first-invocation.mjs +119 -0
- package/lib/install/stage-project.mjs +69 -0
- package/lib/intake/classify.mjs +278 -0
- package/lib/intake/feedback.mjs +273 -0
- package/lib/intake/filesystem-queue.mjs +158 -0
- package/lib/intake/intake-config.mjs +132 -0
- package/lib/intake/postgres-queue.mjs +198 -0
- package/lib/intake/prepare.mjs +139 -0
- package/lib/intake/queue.mjs +83 -0
- package/lib/intake/session-prelude.mjs +50 -0
- package/lib/integrations/intake-integrations.mjs +740 -0
- package/lib/intent-classifier.mjs +253 -0
- package/lib/knowledge/layout.mjs +72 -0
- package/lib/knowledge/rag.mjs +331 -0
- package/lib/knowledge/search.mjs +315 -0
- package/lib/knowledge/trends.mjs +261 -0
- package/lib/logger.mjs +85 -0
- package/lib/mcp/broker.mjs +124 -0
- package/lib/mcp/server.mjs +554 -854
- package/lib/mcp/tools/document.mjs +132 -0
- package/lib/mcp/tools/memory.mjs +209 -0
- package/lib/mcp/tools/project.mjs +349 -0
- package/lib/mcp/tools/skills.mjs +409 -0
- package/lib/mcp/tools/storage.mjs +60 -0
- package/lib/mcp/tools/telemetry.mjs +315 -0
- package/lib/mcp/tools/workflow.mjs +112 -0
- package/lib/mcp-catalog.json +54 -3
- package/lib/mcp-manager.mjs +96 -48
- package/lib/mcp-platform-config.mjs +22 -22
- package/lib/memory-stats.mjs +121 -0
- package/lib/mode-commands.mjs +124 -0
- package/lib/model-free-selector.mjs +184 -0
- package/lib/model-pricing.mjs +152 -0
- package/lib/model-registry.mjs +226 -0
- package/lib/model-router.mjs +362 -386
- package/lib/observation-store.mjs +391 -0
- package/lib/ollama-manager.mjs +428 -0
- package/lib/opencode-config.mjs +13 -3
- package/lib/opencode-runtime-plugin.mjs +70 -38
- package/lib/opencode-telemetry.mjs +64 -6
- package/lib/orchestration-policy.mjs +509 -6
- package/lib/overrides/resolver.mjs +207 -0
- package/lib/parity.mjs +147 -0
- package/lib/paths.mjs +33 -0
- package/lib/performance/generate.mjs +212 -0
- package/lib/plugin-registry.mjs +268 -0
- package/lib/policy/engine.mjs +130 -0
- package/lib/policy/unified-gates.mjs +96 -0
- package/lib/project-detection.mjs +129 -0
- package/lib/project-init-shared.mjs +272 -0
- package/lib/project-profile.mjs +447 -0
- package/lib/prompt-composer.js +434 -0
- package/lib/prompt-metadata.mjs +1 -1
- package/lib/provider-capabilities-anthropic.js +44 -0
- package/lib/provider-capabilities-deepseek.js +37 -0
- package/lib/provider-capabilities-generic.js +26 -0
- package/lib/provider-capabilities-google.js +47 -0
- package/lib/provider-capabilities-openai.js +45 -0
- package/lib/provider-capabilities.js +142 -0
- package/lib/providers/atlassian-confluence/index.mjs +103 -0
- package/lib/providers/atlassian-jira/index.mjs +100 -0
- package/lib/providers/auth-manager.mjs +126 -0
- package/lib/providers/circuit-breaker.mjs +124 -0
- package/lib/providers/contract.mjs +93 -0
- package/lib/providers/github/index.mjs +126 -0
- package/lib/providers/registry.mjs +184 -0
- package/lib/providers/salesforce/index.mjs +100 -0
- package/lib/providers/slack/index.mjs +80 -0
- package/lib/reflect.mjs +137 -0
- package/lib/research-lint.mjs +164 -0
- package/lib/resources/budget.mjs +259 -0
- package/lib/role-preload.mjs +21 -6
- package/lib/roles/approval-surface.mjs +54 -0
- package/lib/roles/cli.mjs +118 -0
- package/lib/roles/event-bus.mjs +79 -0
- package/lib/roles/fence.mjs +84 -0
- package/lib/roles/gateway.mjs +260 -0
- package/lib/roles/hook-emit.mjs +37 -0
- package/lib/roles/manifest.mjs +48 -0
- package/lib/roles/router.mjs +27 -0
- package/lib/runtime-pressure.mjs +360 -0
- package/lib/schema-artifact.mjs +134 -0
- package/lib/schema-infer.mjs +551 -0
- package/lib/server/auth.mjs +168 -0
- package/lib/server/chat.mjs +336 -0
- package/lib/server/cors.mjs +77 -0
- package/lib/server/csrf.mjs +91 -0
- package/lib/server/index.mjs +1927 -78
- package/lib/server/insights.mjs +765 -0
- package/lib/server/rate-limit.mjs +91 -0
- package/lib/server/static/assets/index-ab25c707.js +70 -0
- package/lib/server/static/assets/index-f0c80a2b.css +1 -0
- package/lib/server/static/index.html +12 -817
- package/lib/server/telemetry-login.mjs +108 -0
- package/lib/server/webhook.mjs +510 -0
- package/lib/service-manager.mjs +522 -58
- package/lib/services/pattern-promotion-service.mjs +167 -0
- package/lib/services/telemetry-backend.mjs +178 -0
- package/lib/session-store.mjs +374 -0
- package/lib/setup-prompts.mjs +96 -0
- package/lib/setup.mjs +525 -38
- package/lib/skills-apply.mjs +280 -0
- package/lib/skills-scope.mjs +118 -0
- package/lib/status.mjs +261 -70
- package/lib/storage/admin.mjs +355 -0
- package/lib/storage/backend.mjs +2 -1
- package/lib/storage/backup.mjs +347 -0
- package/lib/storage/embeddings-engine.mjs +133 -0
- package/lib/storage/embeddings-legacy.mjs +85 -0
- package/lib/storage/embeddings-local.mjs +108 -0
- package/lib/storage/embeddings-ollama.mjs +78 -0
- package/lib/storage/embeddings-openai.mjs +85 -0
- package/lib/storage/embeddings.mjs +92 -33
- package/lib/storage/file-lock.mjs +130 -0
- package/lib/storage/fusion.mjs +95 -0
- package/lib/storage/hybrid-query.mjs +34 -27
- package/lib/storage/migrations.mjs +187 -0
- package/lib/storage/postgres-backup.mjs +124 -0
- package/lib/storage/sql-store.mjs +5 -15
- package/lib/storage/state-source.mjs +12 -13
- package/lib/storage/store-version.mjs +115 -0
- package/lib/storage/sync.mjs +144 -35
- package/lib/storage/unified-storage.mjs +550 -0
- package/lib/storage/vector-client.mjs +286 -0
- package/lib/storage/vector-store.mjs +71 -30
- package/lib/task-graph/generate.mjs +135 -0
- package/lib/task-graph/schema.mjs +81 -0
- package/lib/task-graph/store.mjs +71 -0
- package/lib/telemetry/backends/local.mjs +62 -0
- package/lib/telemetry/backends/{langfuse.mjs → remote.mjs} +27 -14
- package/lib/telemetry/backfill.mjs +180 -0
- package/lib/telemetry/eval-datasets.mjs +203 -0
- package/lib/telemetry/{langfuse-ingest.mjs → ingest.mjs} +26 -20
- package/lib/telemetry/intent-verifications.mjs +86 -0
- package/lib/telemetry/llm-judge.mjs +350 -0
- package/lib/telemetry/model-pricing-catalog.mjs +557 -0
- package/lib/telemetry/setup.mjs +151 -0
- package/lib/telemetry/skill-calls.mjs +78 -0
- package/lib/telemetry/team-rollup.mjs +4 -4
- package/lib/token-engine.js +117 -0
- package/lib/token-estimator-anthropic.js +15 -0
- package/lib/token-estimator-deepseek.js +13 -0
- package/lib/token-estimator-default.js +13 -0
- package/lib/token-estimator-google.js +13 -0
- package/lib/token-estimator-openai.js +13 -0
- package/lib/toolkit-env.mjs +1 -1
- package/lib/tty-prompts.mjs +211 -0
- package/lib/uninstall/uninstall.mjs +423 -0
- package/lib/update.mjs +115 -0
- package/lib/upgrade.mjs +141 -0
- package/lib/validator.mjs +51 -2
- package/lib/validators/skills.mjs +142 -0
- package/lib/wireframe.mjs +422 -0
- package/lib/worker/entrypoint.mjs +241 -0
- package/lib/worker/evidence.mjs +107 -0
- package/lib/worker/run.mjs +154 -0
- package/lib/worker/trace.mjs +182 -0
- package/lib/workflow-state.mjs +14 -18
- package/package.json +21 -8
- package/personas/construct.md +53 -51
- package/platforms/claude/CLAUDE.md +1 -1
- package/platforms/claude/settings.template.json +115 -68
- package/platforms/opencode/config.template.json +3 -7
- package/rules/common/agents.md +11 -38
- package/rules/common/beads-hygiene.md +75 -0
- package/rules/common/code-review.md +11 -100
- package/rules/common/coding-style.md +5 -3
- package/rules/common/comments.md +30 -111
- package/rules/common/commit-approval.md +53 -0
- package/rules/common/cx-agent-routing.md +1 -0
- package/rules/common/development-workflow.md +23 -41
- package/rules/common/doc-ownership.md +54 -0
- package/rules/common/efficiency.md +57 -0
- package/rules/common/framing.md +76 -0
- package/rules/common/git-workflow.md +2 -4
- package/rules/common/patterns.md +3 -7
- package/rules/common/performance.md +15 -31
- package/rules/common/release-gates.md +69 -0
- package/rules/common/research.md +107 -0
- package/rules/common/security.md +6 -6
- package/rules/common/skill-composition.md +67 -0
- package/rules/common/testing.md +15 -27
- package/rules/golang/hooks.md +0 -4
- package/rules/policy/bootstrap.yaml +11 -0
- package/rules/policy/drive.yaml +8 -0
- package/rules/policy/task.yaml +9 -0
- package/rules/policy/workflow.yaml +9 -0
- package/rules/python/hooks.md +0 -4
- package/rules/swift/hooks.md +0 -4
- package/rules/typescript/hooks.md +0 -4
- package/rules/web/hooks.md +0 -2
- package/{sync-agents.mjs → scripts/sync-agents.mjs} +306 -61
- package/skills/ai/prompt-optimizer.md +7 -7
- package/skills/compliance/ai-disclosure.md +58 -0
- package/skills/compliance/data-privacy.md +46 -0
- package/skills/compliance/license-audit.md +40 -0
- package/skills/compliance/regulatory-review.md +61 -0
- package/skills/docs/document-ingest-workflow.md +52 -0
- package/skills/docs/evidence-ingest-workflow.md +9 -0
- package/skills/docs/init-docs.md +51 -18
- package/skills/docs/product-intelligence-workflow.md +12 -0
- package/skills/docs/product-signal-workflow.md +4 -0
- package/skills/docs/research-workflow.md +23 -8
- package/skills/docs/runbook-workflow.md +1 -1
- package/skills/operating/orchestration-reference.md +151 -0
- package/skills/roles/architect.md +5 -0
- package/skills/roles/engineer.md +32 -0
- package/skills/roles/operator.md +12 -0
- package/skills/roles/reviewer.md +23 -0
- package/skills/routing.md +1 -1
- package/templates/devcontainer/Dockerfile.devcontainer +38 -0
- package/templates/devcontainer/devcontainer.json +31 -0
- package/templates/distribution/bootstrap.ps1 +142 -0
- package/templates/distribution/bootstrap.sh +196 -0
- package/templates/distribution/run.mjs +187 -0
- package/templates/docs/adr.md +44 -8
- package/templates/docs/changelog-entry.md +43 -0
- package/templates/docs/construct_guide.md +149 -0
- package/templates/docs/evidence-brief.md +2 -2
- package/templates/docs/meta-prd.md +140 -19
- package/templates/docs/onboarding.md +57 -0
- package/templates/docs/prd.md +159 -15
- package/templates/docs/research-brief.md +4 -4
- package/templates/homebrew/construct.rb +67 -0
- package/langfuse/docker-compose.yml +0 -82
- package/lib/eval-harness.mjs +0 -59
- package/lib/hooks/bootstrap-guard.mjs +0 -90
- package/lib/hooks/console-warn.mjs +0 -43
- package/lib/hooks/continuation-enforcer.mjs +0 -72
- package/lib/hooks/drive-guard.mjs +0 -89
- package/lib/hooks/mcp-task-scope.mjs +0 -47
- package/lib/hooks/task-completed-guard.mjs +0 -43
- package/lib/hooks/teammate-idle-guard.mjs +0 -54
- package/lib/hooks/workflow-guard.mjs +0 -62
- package/lib/prompt-composer.mjs +0 -196
- package/lib/review.mjs +0 -429
- package/lib/server/static/app.js +0 -841
- package/lib/telemetry/langfuse-model-sync.mjs +0 -270
- package/rules/common/hooks.md +0 -35
- package/rules/zh/README.md +0 -113
- package/rules/zh/agents.md +0 -55
- package/rules/zh/code-review.md +0 -129
- package/rules/zh/coding-style.md +0 -53
- package/rules/zh/development-workflow.md +0 -49
- package/rules/zh/git-workflow.md +0 -29
- package/rules/zh/hooks.md +0 -35
- package/rules/zh/patterns.md +0 -36
- package/rules/zh/performance.md +0 -60
- package/rules/zh/security.md +0 -34
- package/rules/zh/testing.md +0 -34
package/lib/validator.mjs
CHANGED
|
@@ -1,7 +1,10 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* lib/validator.mjs —
|
|
2
|
+
* lib/validator.mjs — Schema and contract validation for Construct data structures.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
4
|
+
* Validates workflow state objects, task packets, agent registry entries, and
|
|
5
|
+
* MCP tool payloads against expected shapes. Throws structured errors with
|
|
6
|
+
* field-level detail. Backs workflow-state, cli-commands, and MCP tools
|
|
7
|
+
* before they mutate persistent state.
|
|
5
8
|
*/
|
|
6
9
|
import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
7
10
|
import { existsSync, readFileSync } from 'node:fs';
|
|
@@ -10,6 +13,23 @@ import { join, dirname } from 'node:path';
|
|
|
10
13
|
const VALID_TIERS = new Set(['reasoning', 'standard', 'fast']);
|
|
11
14
|
const VALID_REASONING_EFFORTS = new Set(['low', 'medium', 'high', 'xhigh']);
|
|
12
15
|
|
|
16
|
+
const PROMPT_HARD_CAP_WORDS = 4000;
|
|
17
|
+
const DESCRIPTION_MAX_CHARS = 240;
|
|
18
|
+
const DISPLAY_NAME_MAX_CHARS = 60;
|
|
19
|
+
|
|
20
|
+
function wordCount(text) {
|
|
21
|
+
return String(text || '').trim().split(/\s+/).filter(Boolean).length;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function loadPromptContent(rootDir, relPath) {
|
|
25
|
+
if (!relPath) return null;
|
|
26
|
+
try {
|
|
27
|
+
return readFileSync(join(rootDir, relPath), 'utf8');
|
|
28
|
+
} catch {
|
|
29
|
+
return null;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
13
33
|
function hasProviderModelShape(value) {
|
|
14
34
|
return typeof value === 'string' && /^[^\s/]+\/\S+$/.test(value);
|
|
15
35
|
}
|
|
@@ -71,6 +91,8 @@ export function validateRegistry(reg, options = {}) {
|
|
|
71
91
|
|
|
72
92
|
if (!agent.description || typeof agent.description !== 'string') {
|
|
73
93
|
errors.push(`${label}: description must be a non-empty string`);
|
|
94
|
+
} else if (agent.description.length > DESCRIPTION_MAX_CHARS) {
|
|
95
|
+
errors.push(`${label}: description exceeds ${DESCRIPTION_MAX_CHARS} chars (got ${agent.description.length})`);
|
|
74
96
|
}
|
|
75
97
|
if (agent.prompt !== undefined && typeof agent.prompt !== 'string') {
|
|
76
98
|
errors.push(`${label}: prompt must be a string when present`);
|
|
@@ -83,6 +105,18 @@ export function validateRegistry(reg, options = {}) {
|
|
|
83
105
|
} else if (agent.promptFile && !existsSync(join(rootDir, agent.promptFile))) {
|
|
84
106
|
errors.push(`${label}: promptFile does not exist (${agent.promptFile})`);
|
|
85
107
|
}
|
|
108
|
+
const promptText = typeof agent.prompt === 'string'
|
|
109
|
+
? agent.prompt
|
|
110
|
+
: loadPromptContent(rootDir, agent.promptFile);
|
|
111
|
+
if (promptText) {
|
|
112
|
+
const cap = Number.isInteger(agent.wordCapOverride) && agent.wordCapOverride > 0
|
|
113
|
+
? agent.wordCapOverride
|
|
114
|
+
: PROMPT_HARD_CAP_WORDS;
|
|
115
|
+
const words = wordCount(promptText);
|
|
116
|
+
if (words > cap) {
|
|
117
|
+
errors.push(`${label}: prompt is ${words} words, exceeds cap of ${cap} (set wordCapOverride if intentional)`);
|
|
118
|
+
}
|
|
119
|
+
}
|
|
86
120
|
if (!agent.model && !agent.modelTier) {
|
|
87
121
|
errors.push(`${label}: must define model or modelTier`);
|
|
88
122
|
}
|
|
@@ -107,6 +141,11 @@ export function validateRegistry(reg, options = {}) {
|
|
|
107
141
|
|
|
108
142
|
if (!persona.description || typeof persona.description !== 'string') {
|
|
109
143
|
errors.push(`${label}: description must be a non-empty string`);
|
|
144
|
+
} else if (persona.description.length > DESCRIPTION_MAX_CHARS) {
|
|
145
|
+
errors.push(`${label}: description exceeds ${DESCRIPTION_MAX_CHARS} chars (got ${persona.description.length})`);
|
|
146
|
+
}
|
|
147
|
+
if (persona.displayName && persona.displayName.length > DISPLAY_NAME_MAX_CHARS) {
|
|
148
|
+
errors.push(`${label}: displayName exceeds ${DISPLAY_NAME_MAX_CHARS} chars (got ${persona.displayName.length})`);
|
|
110
149
|
}
|
|
111
150
|
if (!persona.role || typeof persona.role !== 'string') {
|
|
112
151
|
errors.push(`${label}: role must be a non-empty string`);
|
|
@@ -116,6 +155,16 @@ export function validateRegistry(reg, options = {}) {
|
|
|
116
155
|
} else if (!existsSync(join(rootDir, persona.promptFile))) {
|
|
117
156
|
errors.push(`${label}: promptFile does not exist (${persona.promptFile})`);
|
|
118
157
|
}
|
|
158
|
+
const personaPromptText = loadPromptContent(rootDir, persona.promptFile);
|
|
159
|
+
if (personaPromptText) {
|
|
160
|
+
const cap = Number.isInteger(persona.wordCapOverride) && persona.wordCapOverride > 0
|
|
161
|
+
? persona.wordCapOverride
|
|
162
|
+
: PROMPT_HARD_CAP_WORDS;
|
|
163
|
+
const words = wordCount(personaPromptText);
|
|
164
|
+
if (words > cap) {
|
|
165
|
+
errors.push(`${label}: prompt is ${words} words, exceeds cap of ${cap} (set wordCapOverride if intentional)`);
|
|
166
|
+
}
|
|
167
|
+
}
|
|
119
168
|
if (!persona.model && !persona.modelTier) {
|
|
120
169
|
errors.push(`${label}: must define model or modelTier`);
|
|
121
170
|
}
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* lib/validators/skills.mjs — Validate the structure of skill files.
|
|
3
|
+
*
|
|
4
|
+
* Skills are markdown files under `skills/` (and any merged plugin or project
|
|
5
|
+
* skill paths) that follow this convention:
|
|
6
|
+
*
|
|
7
|
+
* <!--
|
|
8
|
+
* skills/<domain>/<name>.md — <Title> — <one-line description>
|
|
9
|
+
* ...
|
|
10
|
+
* -->
|
|
11
|
+
* # <Title>
|
|
12
|
+
*
|
|
13
|
+
* Use this skill when <triggering condition>.
|
|
14
|
+
* ...
|
|
15
|
+
*
|
|
16
|
+
* The validator splits findings into hard errors (block the build) and soft
|
|
17
|
+
* warnings (surfaced by `construct doctor` without failing it):
|
|
18
|
+
*
|
|
19
|
+
* Hard errors:
|
|
20
|
+
* - missing H1 title
|
|
21
|
+
* - title over 80 chars
|
|
22
|
+
* - duplicate relative paths across roots
|
|
23
|
+
* - unreadable file
|
|
24
|
+
*
|
|
25
|
+
* Soft warnings:
|
|
26
|
+
* - opener is missing or doesn't match a recognised "Use this skill
|
|
27
|
+
* when/to/for ..." form (skills with non-trigger openers still load,
|
|
28
|
+
* but they're harder for routing to surface)
|
|
29
|
+
* - opener over 240 chars
|
|
30
|
+
*
|
|
31
|
+
* Hard checks prevent broken file structure; soft checks surface drift from
|
|
32
|
+
* authoring conventions without blocking authors who legitimately diverge.
|
|
33
|
+
*/
|
|
34
|
+
|
|
35
|
+
import { readdirSync, readFileSync, statSync } from 'node:fs';
|
|
36
|
+
import path from 'node:path';
|
|
37
|
+
|
|
38
|
+
const DESCRIPTION_MAX_CHARS = 240;
|
|
39
|
+
const TITLE_MAX_CHARS = 80;
|
|
40
|
+
|
|
41
|
+
// Accept any of the conventional skill-trigger openers actually in use across
|
|
42
|
+
// the corpus: "Use this skill when/to/for", "Use when:", "Use this when",
|
|
43
|
+
// "Trigger:", or a leading "When ..." that signals a triggering condition.
|
|
44
|
+
const TRIGGER_PATTERN = /^(?:use\s+(?:this\s+(?:skill\s+)?)?(?:when|to|for|if)\b|when\s+to\s+use\b|trigger\s*:|use\s+when\s*:)/i;
|
|
45
|
+
|
|
46
|
+
function* walk(root) {
|
|
47
|
+
const entries = readdirSync(root, { withFileTypes: true });
|
|
48
|
+
for (const entry of entries) {
|
|
49
|
+
const full = path.join(root, entry.name);
|
|
50
|
+
if (entry.isDirectory()) {
|
|
51
|
+
yield* walk(full);
|
|
52
|
+
} else if (entry.isFile() && entry.name.endsWith('.md')) {
|
|
53
|
+
yield full;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function extractTitleAndOpener(text) {
|
|
59
|
+
const lines = text.split(/\r?\n/);
|
|
60
|
+
let title = null;
|
|
61
|
+
let opener = null;
|
|
62
|
+
let inBody = false;
|
|
63
|
+
for (let i = 0; i < lines.length; i++) {
|
|
64
|
+
const line = lines[i].trim();
|
|
65
|
+
if (!title) {
|
|
66
|
+
const m = line.match(/^#\s+(.+?)\s*$/);
|
|
67
|
+
if (m) {
|
|
68
|
+
title = m[1];
|
|
69
|
+
inBody = true;
|
|
70
|
+
continue;
|
|
71
|
+
}
|
|
72
|
+
} else if (inBody && line && !line.startsWith('#') && !line.startsWith('<!--')) {
|
|
73
|
+
opener = line;
|
|
74
|
+
break;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
return { title, opener };
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Validate every skill file under one or more directories.
|
|
82
|
+
*
|
|
83
|
+
* @param {string[]|string} roots — directories to walk recursively
|
|
84
|
+
* @returns {{
|
|
85
|
+
* valid: boolean,
|
|
86
|
+
* errors: string[],
|
|
87
|
+
* warnings: string[],
|
|
88
|
+
* skills: Array<{ path: string, title: string|null, opener: string|null }>,
|
|
89
|
+
* }}
|
|
90
|
+
*/
|
|
91
|
+
export function validateSkills(roots) {
|
|
92
|
+
const dirs = Array.isArray(roots) ? roots : [roots];
|
|
93
|
+
const errors = [];
|
|
94
|
+
const warnings = [];
|
|
95
|
+
const skills = [];
|
|
96
|
+
const seenRelative = new Map();
|
|
97
|
+
|
|
98
|
+
for (const dir of dirs) {
|
|
99
|
+
let exists = false;
|
|
100
|
+
try { exists = statSync(dir).isDirectory(); } catch { exists = false; }
|
|
101
|
+
if (!exists) continue;
|
|
102
|
+
|
|
103
|
+
for (const filePath of walk(dir)) {
|
|
104
|
+
const rel = path.relative(dir, filePath);
|
|
105
|
+
if (rel === 'routing.md') continue;
|
|
106
|
+
|
|
107
|
+
const prevSource = seenRelative.get(rel);
|
|
108
|
+
if (prevSource) {
|
|
109
|
+
errors.push(`duplicate skill path '${rel}' in ${prevSource} and ${dir}`);
|
|
110
|
+
} else {
|
|
111
|
+
seenRelative.set(rel, dir);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
let raw;
|
|
115
|
+
try { raw = readFileSync(filePath, 'utf8'); }
|
|
116
|
+
catch (err) {
|
|
117
|
+
errors.push(`${rel}: cannot read (${err.message})`);
|
|
118
|
+
continue;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
const { title, opener } = extractTitleAndOpener(raw);
|
|
122
|
+
|
|
123
|
+
if (!title) {
|
|
124
|
+
errors.push(`${rel}: missing H1 title (e.g. "# Skill Name")`);
|
|
125
|
+
} else if (title.length > TITLE_MAX_CHARS) {
|
|
126
|
+
errors.push(`${rel}: title exceeds ${TITLE_MAX_CHARS} chars (got ${title.length})`);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
if (!opener) {
|
|
130
|
+
warnings.push(`${rel}: no trigger opener after the H1 title — routing will have nothing to match`);
|
|
131
|
+
} else if (!TRIGGER_PATTERN.test(opener)) {
|
|
132
|
+
warnings.push(`${rel}: opener should start with "Use this skill when/to/for ..." (got "${opener.slice(0, 60)}…")`);
|
|
133
|
+
} else if (opener.length > DESCRIPTION_MAX_CHARS) {
|
|
134
|
+
warnings.push(`${rel}: opener exceeds ${DESCRIPTION_MAX_CHARS} chars (got ${opener.length})`);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
skills.push({ path: rel, title, opener });
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
return { valid: errors.length === 0, errors, warnings, skills };
|
|
142
|
+
}
|
|
@@ -0,0 +1,422 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* lib/wireframe.mjs — `construct wireframe` command, generates Mermaid
|
|
3
|
+
* diagrams and low-fidelity HTML wireframes from a natural-language prompt.
|
|
4
|
+
*
|
|
5
|
+
* Honest design: this is a scaffold generator, not a model-backed visual
|
|
6
|
+
* synthesis engine. It picks a template appropriate to the detected
|
|
7
|
+
* diagram kind, seeds it with keywords extracted from the prompt, and
|
|
8
|
+
* writes to `.cx/wireframes/<slug>-<ts>.<ext>`. A specialist (cx-designer)
|
|
9
|
+
* or the user can then refine the generated file.
|
|
10
|
+
*
|
|
11
|
+
* Modern best practices honored:
|
|
12
|
+
* - Low-fi sketch look for HTML wireframes (the point of a wireframe is
|
|
13
|
+
* to delay visual-polish commitment; sketch styles signal "not final")
|
|
14
|
+
* - Semantic HTML5 landmarks, accessible labels, keyboard order
|
|
15
|
+
* - Mermaid diagrams for flows/states/sequences/ER — widely rendered by
|
|
16
|
+
* OpenCode, Claude Code, GitHub, Notion, etc.
|
|
17
|
+
* - Zero external dependencies; bundled sketch CSS inlined
|
|
18
|
+
* - Output is text — review-able in PRs, diff-able, version-controllable
|
|
19
|
+
*/
|
|
20
|
+
import { existsSync, mkdirSync, writeFileSync } from 'node:fs';
|
|
21
|
+
import { join, resolve } from 'node:path';
|
|
22
|
+
|
|
23
|
+
const TYPES = ['flow', 'state', 'sequence', 'er', 'layout', 'user-journey'];
|
|
24
|
+
|
|
25
|
+
function slugify(text) {
|
|
26
|
+
return String(text || '')
|
|
27
|
+
.toLowerCase()
|
|
28
|
+
.replace(/[^a-z0-9]+/g, '-')
|
|
29
|
+
.replace(/^-+|-+$/g, '')
|
|
30
|
+
.slice(0, 60) || 'wireframe';
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function timestamp() {
|
|
34
|
+
return new Date().toISOString().replace(/[:.]/g, '-').slice(0, 19);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function inferType(description, explicit) {
|
|
38
|
+
if (explicit) return explicit;
|
|
39
|
+
const text = String(description || '').toLowerCase();
|
|
40
|
+
if (/\b(screen|page|layout|ui|view|dashboard|form|modal|list|detail)\b/.test(text)) return 'layout';
|
|
41
|
+
if (/\b(state|status|transition|machine)\b/.test(text)) return 'state';
|
|
42
|
+
if (/\b(sequence|handshake|request|response|api call|interaction between)\b/.test(text)) return 'sequence';
|
|
43
|
+
if (/\b(entity|schema|database|table|relationship|foreign key)\b/.test(text)) return 'er';
|
|
44
|
+
if (/\b(journey|persona|step-by-step|onboarding|funnel)\b/.test(text)) return 'user-journey';
|
|
45
|
+
return 'flow';
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// Stopwords that don't make good node labels.
|
|
49
|
+
const STOP = new Set([
|
|
50
|
+
'a','an','the','of','to','for','with','from','in','on','and','or','but',
|
|
51
|
+
'is','are','was','were','be','been','being','has','have','had','do','does','did',
|
|
52
|
+
'this','that','these','those','i','we','you','they','he','she','it',
|
|
53
|
+
'my','our','your','their','his','her','its','me','us',
|
|
54
|
+
'please','build','create','design','make','show','draw','generate','wireframe','diagram',
|
|
55
|
+
'flow','layout','screen','page','view',
|
|
56
|
+
]);
|
|
57
|
+
|
|
58
|
+
function extractKeywords(description, max = 8) {
|
|
59
|
+
const words = String(description || '')
|
|
60
|
+
.split(/[^a-zA-Z0-9_-]+/)
|
|
61
|
+
.filter(Boolean)
|
|
62
|
+
.filter((w) => w.length > 2 && !STOP.has(w.toLowerCase()));
|
|
63
|
+
const seen = new Set();
|
|
64
|
+
const result = [];
|
|
65
|
+
for (const w of words) {
|
|
66
|
+
const key = w.toLowerCase();
|
|
67
|
+
if (seen.has(key)) continue;
|
|
68
|
+
seen.add(key);
|
|
69
|
+
result.push(w);
|
|
70
|
+
if (result.length >= max) break;
|
|
71
|
+
}
|
|
72
|
+
return result;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function mermaidFlow(description, keywords) {
|
|
76
|
+
const nodes = keywords.length > 0 ? keywords : ['Start', 'Step', 'End'];
|
|
77
|
+
const lines = ['graph TD'];
|
|
78
|
+
const ids = nodes.map((_, i) => String.fromCharCode(65 + i));
|
|
79
|
+
nodes.forEach((n, i) => lines.push(` ${ids[i]}[${n}]`));
|
|
80
|
+
for (let i = 0; i < ids.length - 1; i += 1) {
|
|
81
|
+
lines.push(` ${ids[i]} --> ${ids[i + 1]}`);
|
|
82
|
+
}
|
|
83
|
+
return `%% ${description}\n%% Refine nodes, add decision diamonds (C{...}), and label edges.\n${lines.join('\n')}`;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function mermaidState(description, keywords) {
|
|
87
|
+
const states = keywords.length > 0 ? keywords.slice(0, 5) : ['Idle', 'Active', 'Done'];
|
|
88
|
+
const lines = ['stateDiagram-v2', ' [*] --> ' + states[0]];
|
|
89
|
+
for (let i = 0; i < states.length - 1; i += 1) {
|
|
90
|
+
lines.push(` ${states[i]} --> ${states[i + 1]}`);
|
|
91
|
+
}
|
|
92
|
+
lines.push(` ${states[states.length - 1]} --> [*]`);
|
|
93
|
+
return `%% ${description}\n%% Add guards, actions, and error transitions.\n${lines.join('\n')}`;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function mermaidSequence(description, keywords) {
|
|
97
|
+
const actors = keywords.length >= 2 ? keywords.slice(0, 3) : ['User', 'System', 'Service'];
|
|
98
|
+
const lines = ['sequenceDiagram'];
|
|
99
|
+
actors.forEach((a) => lines.push(` participant ${a}`));
|
|
100
|
+
for (let i = 0; i < actors.length - 1; i += 1) {
|
|
101
|
+
lines.push(` ${actors[i]}->>${actors[i + 1]}: request`);
|
|
102
|
+
lines.push(` ${actors[i + 1]}-->>${actors[i]}: response`);
|
|
103
|
+
}
|
|
104
|
+
return `%% ${description}\n%% Replace generic labels with the real message contracts.\n${lines.join('\n')}`;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function mermaidER(description, keywords) {
|
|
108
|
+
const entities = keywords.length >= 2 ? keywords.slice(0, 3) : ['User', 'Session', 'Document'];
|
|
109
|
+
const lines = ['erDiagram'];
|
|
110
|
+
for (let i = 0; i < entities.length - 1; i += 1) {
|
|
111
|
+
lines.push(` ${entities[i]} ||--o{ ${entities[i + 1]} : has`);
|
|
112
|
+
}
|
|
113
|
+
entities.forEach((e) => {
|
|
114
|
+
lines.push(` ${e} {`);
|
|
115
|
+
lines.push(` string id PK`);
|
|
116
|
+
lines.push(` string name`);
|
|
117
|
+
lines.push(` }`);
|
|
118
|
+
});
|
|
119
|
+
return `%% ${description}\n%% Replace placeholder fields with real attributes and cardinality.\n${lines.join('\n')}`;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function mermaidUserJourney(description, keywords) {
|
|
123
|
+
const steps = keywords.length >= 2 ? keywords.slice(0, 5) : ['Discover', 'Evaluate', 'Try', 'Adopt'];
|
|
124
|
+
const lines = ['journey', ` title ${description || 'User Journey'}`];
|
|
125
|
+
lines.push(' section Onboarding');
|
|
126
|
+
steps.forEach((s, i) => {
|
|
127
|
+
const score = [3, 4, 5, 4, 3][i] ?? 3;
|
|
128
|
+
lines.push(` ${s}: ${score}: User`);
|
|
129
|
+
});
|
|
130
|
+
return `%% Refine step scores (1–5) and split into meaningful sections.\n${lines.join('\n')}`;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// --- HTML layout wireframe -------------------------------------------------
|
|
134
|
+
|
|
135
|
+
const SKETCH_CSS = `
|
|
136
|
+
:root {
|
|
137
|
+
--ink: #222;
|
|
138
|
+
--paper: #fefdf8;
|
|
139
|
+
--rough: #6b6b6b;
|
|
140
|
+
--hint: #a9a9a9;
|
|
141
|
+
--accent: #2c5282;
|
|
142
|
+
}
|
|
143
|
+
* { box-sizing: border-box; }
|
|
144
|
+
body {
|
|
145
|
+
margin: 0; padding: 2rem;
|
|
146
|
+
font-family: "Caveat", "Bradley Hand", "Comic Sans MS", cursive;
|
|
147
|
+
background: var(--paper);
|
|
148
|
+
color: var(--ink);
|
|
149
|
+
line-height: 1.4;
|
|
150
|
+
}
|
|
151
|
+
.wireframe { max-width: 1200px; margin: 0 auto; }
|
|
152
|
+
.wireframe header, .wireframe nav, .wireframe main,
|
|
153
|
+
.wireframe aside, .wireframe section, .wireframe footer,
|
|
154
|
+
.block, .card {
|
|
155
|
+
border: 2px solid var(--ink);
|
|
156
|
+
border-radius: 6px;
|
|
157
|
+
padding: 1rem;
|
|
158
|
+
margin: 0.5rem 0;
|
|
159
|
+
position: relative;
|
|
160
|
+
background: var(--paper);
|
|
161
|
+
box-shadow: 3px 3px 0 var(--hint);
|
|
162
|
+
}
|
|
163
|
+
.wireframe header { background: #f3f0e7; }
|
|
164
|
+
.label {
|
|
165
|
+
position: absolute; top: -10px; left: 10px;
|
|
166
|
+
background: var(--paper); padding: 0 6px;
|
|
167
|
+
font-size: 0.85em; color: var(--rough);
|
|
168
|
+
font-family: ui-monospace, Menlo, monospace;
|
|
169
|
+
}
|
|
170
|
+
.row { display: grid; grid-template-columns: repeat(var(--cols, 3), 1fr); gap: 1rem; }
|
|
171
|
+
.row-2 { --cols: 2; }
|
|
172
|
+
.row-3 { --cols: 3; }
|
|
173
|
+
.row-4 { --cols: 4; }
|
|
174
|
+
.sidebar-layout { display: grid; grid-template-columns: 240px 1fr; gap: 1rem; }
|
|
175
|
+
.placeholder {
|
|
176
|
+
min-height: 60px;
|
|
177
|
+
background: repeating-linear-gradient(
|
|
178
|
+
45deg, transparent, transparent 8px,
|
|
179
|
+
#eee 8px, #eee 9px
|
|
180
|
+
);
|
|
181
|
+
border: 1px dashed var(--rough);
|
|
182
|
+
border-radius: 4px;
|
|
183
|
+
display: flex; align-items: center; justify-content: center;
|
|
184
|
+
color: var(--rough); font-size: 0.9em;
|
|
185
|
+
}
|
|
186
|
+
button, .btn {
|
|
187
|
+
font-family: inherit;
|
|
188
|
+
border: 2px solid var(--ink);
|
|
189
|
+
background: var(--paper);
|
|
190
|
+
padding: 0.5rem 1rem;
|
|
191
|
+
border-radius: 4px;
|
|
192
|
+
box-shadow: 2px 2px 0 var(--hint);
|
|
193
|
+
cursor: pointer;
|
|
194
|
+
}
|
|
195
|
+
button:hover { background: #f3f0e7; }
|
|
196
|
+
input, textarea, select {
|
|
197
|
+
font-family: inherit;
|
|
198
|
+
border: 2px solid var(--ink);
|
|
199
|
+
background: var(--paper);
|
|
200
|
+
padding: 0.4rem;
|
|
201
|
+
border-radius: 4px;
|
|
202
|
+
width: 100%;
|
|
203
|
+
}
|
|
204
|
+
h1, h2, h3 { margin: 0.25rem 0 0.5rem; }
|
|
205
|
+
.note {
|
|
206
|
+
background: #fff9c4; border: 1px dashed #c0b03f;
|
|
207
|
+
padding: 0.5rem 0.75rem; border-radius: 4px;
|
|
208
|
+
font-size: 0.9em;
|
|
209
|
+
}
|
|
210
|
+
@media (prefers-color-scheme: dark) {
|
|
211
|
+
:root { --ink: #e8e8e8; --paper: #1e1e1e; --rough: #9a9a9a; --hint: #4a4a4a; }
|
|
212
|
+
.wireframe header { background: #2a2a2a; }
|
|
213
|
+
.placeholder { background: repeating-linear-gradient(45deg, transparent, transparent 8px, #333 8px, #333 9px); }
|
|
214
|
+
button:hover { background: #2a2a2a; }
|
|
215
|
+
.note { background: #3a3520; color: #f0e68c; border-color: #5a5230; }
|
|
216
|
+
}
|
|
217
|
+
`;
|
|
218
|
+
|
|
219
|
+
function htmlLayout(description, keywords) {
|
|
220
|
+
const title = description || 'Untitled wireframe';
|
|
221
|
+
const sections = keywords.length > 0 ? keywords : ['Main content', 'Related items', 'Actions'];
|
|
222
|
+
const cards = sections.slice(0, 4).map((s) => `
|
|
223
|
+
<div class="block">
|
|
224
|
+
<span class="label">${s}</span>
|
|
225
|
+
<h3>${s}</h3>
|
|
226
|
+
<div class="placeholder">Content placeholder</div>
|
|
227
|
+
</div>`).join('');
|
|
228
|
+
|
|
229
|
+
return `<!doctype html>
|
|
230
|
+
<html lang="en">
|
|
231
|
+
<head>
|
|
232
|
+
<meta charset="utf-8" />
|
|
233
|
+
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
234
|
+
<title>Wireframe: ${escapeHtml(title)}</title>
|
|
235
|
+
<style>${SKETCH_CSS}</style>
|
|
236
|
+
</head>
|
|
237
|
+
<body>
|
|
238
|
+
<div class="wireframe" role="document">
|
|
239
|
+
<p class="note"><strong>Wireframe (low-fi).</strong> ${escapeHtml(description)}</p>
|
|
240
|
+
|
|
241
|
+
<header role="banner">
|
|
242
|
+
<span class="label">header</span>
|
|
243
|
+
<h1>${escapeHtml(title)}</h1>
|
|
244
|
+
<p>Primary nav / logo / global actions go here.</p>
|
|
245
|
+
</header>
|
|
246
|
+
|
|
247
|
+
<nav role="navigation" aria-label="primary">
|
|
248
|
+
<span class="label">nav</span>
|
|
249
|
+
<div class="row row-4">
|
|
250
|
+
<a href="#">Nav 1</a>
|
|
251
|
+
<a href="#">Nav 2</a>
|
|
252
|
+
<a href="#">Nav 3</a>
|
|
253
|
+
<a href="#">Nav 4</a>
|
|
254
|
+
</div>
|
|
255
|
+
</nav>
|
|
256
|
+
|
|
257
|
+
<div class="sidebar-layout">
|
|
258
|
+
<aside role="complementary">
|
|
259
|
+
<span class="label">sidebar</span>
|
|
260
|
+
<h3>Filters</h3>
|
|
261
|
+
<div class="placeholder">Filter / facet list</div>
|
|
262
|
+
</aside>
|
|
263
|
+
|
|
264
|
+
<main role="main">
|
|
265
|
+
<span class="label">main</span>
|
|
266
|
+
<div class="row row-2">${cards}
|
|
267
|
+
</div>
|
|
268
|
+
</main>
|
|
269
|
+
</div>
|
|
270
|
+
|
|
271
|
+
<footer role="contentinfo">
|
|
272
|
+
<span class="label">footer</span>
|
|
273
|
+
<div class="row row-3">
|
|
274
|
+
<div>Links</div>
|
|
275
|
+
<div>Legal</div>
|
|
276
|
+
<div>Contact</div>
|
|
277
|
+
</div>
|
|
278
|
+
</footer>
|
|
279
|
+
</div>
|
|
280
|
+
</body>
|
|
281
|
+
</html>
|
|
282
|
+
`;
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
function escapeHtml(s) {
|
|
286
|
+
return String(s || '')
|
|
287
|
+
.replace(/&/g, '&')
|
|
288
|
+
.replace(/</g, '<')
|
|
289
|
+
.replace(/>/g, '>')
|
|
290
|
+
.replace(/"/g, '"')
|
|
291
|
+
.replace(/'/g, ''');
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
function wrapMermaidInMd(description, mermaid, type) {
|
|
295
|
+
return `# Wireframe — ${description || type}
|
|
296
|
+
|
|
297
|
+
Type: ${type}
|
|
298
|
+
Generated: ${new Date().toISOString()}
|
|
299
|
+
|
|
300
|
+
\`\`\`mermaid
|
|
301
|
+
${mermaid}
|
|
302
|
+
\`\`\`
|
|
303
|
+
|
|
304
|
+
## Next steps
|
|
305
|
+
|
|
306
|
+
- Refine node/edge labels to match the actual domain
|
|
307
|
+
- Add decision branches, error states, and guard conditions
|
|
308
|
+
- Commit the file once it reflects the real flow; treat it as part of the
|
|
309
|
+
architecture record, not a throwaway sketch
|
|
310
|
+
`;
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
export function generateWireframe({ description = '', type = null } = {}) {
|
|
314
|
+
const inferred = inferType(description, type);
|
|
315
|
+
const keywords = extractKeywords(description);
|
|
316
|
+
|
|
317
|
+
if (inferred === 'layout') {
|
|
318
|
+
return {
|
|
319
|
+
type: 'layout',
|
|
320
|
+
format: 'html',
|
|
321
|
+
content: htmlLayout(description, keywords),
|
|
322
|
+
extension: 'html',
|
|
323
|
+
};
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
let mermaid;
|
|
327
|
+
switch (inferred) {
|
|
328
|
+
case 'state': mermaid = mermaidState(description, keywords); break;
|
|
329
|
+
case 'sequence': mermaid = mermaidSequence(description, keywords); break;
|
|
330
|
+
case 'er': mermaid = mermaidER(description, keywords); break;
|
|
331
|
+
case 'user-journey': mermaid = mermaidUserJourney(description, keywords); break;
|
|
332
|
+
case 'flow':
|
|
333
|
+
default: mermaid = mermaidFlow(description, keywords); break;
|
|
334
|
+
}
|
|
335
|
+
return {
|
|
336
|
+
type: inferred,
|
|
337
|
+
format: 'mermaid',
|
|
338
|
+
content: wrapMermaidInMd(description, mermaid, inferred),
|
|
339
|
+
extension: 'md',
|
|
340
|
+
};
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
export function printHelp() {
|
|
344
|
+
console.log(`Usage: construct wireframe <description> [options]
|
|
345
|
+
|
|
346
|
+
Generates a low-fidelity wireframe from a description. Auto-detects the
|
|
347
|
+
best format (Mermaid diagram or HTML sketch) based on the description,
|
|
348
|
+
or force it with --type.
|
|
349
|
+
|
|
350
|
+
Options:
|
|
351
|
+
--type <t> flow (default) | state | sequence | er | layout | user-journey
|
|
352
|
+
--out <path> Save path (default: .cx/wireframes/<slug>-<ts>.<ext>)
|
|
353
|
+
--stdout Print to stdout instead of writing a file
|
|
354
|
+
-h, --help Show this message
|
|
355
|
+
|
|
356
|
+
Formats:
|
|
357
|
+
Mermaid diagrams (flow/state/sequence/er/user-journey) render natively
|
|
358
|
+
in OpenCode, Claude Code, GitHub, Notion, and most markdown viewers.
|
|
359
|
+
HTML wireframes (layout) open in any browser with a low-fi sketch style
|
|
360
|
+
that signals "not final design."
|
|
361
|
+
|
|
362
|
+
Examples:
|
|
363
|
+
construct wireframe "user signup flow"
|
|
364
|
+
construct wireframe "database schema for project iverson" --type er
|
|
365
|
+
construct wireframe "dashboard with sidebar filters" --type layout
|
|
366
|
+
construct wireframe "oauth handshake" --type sequence --stdout
|
|
367
|
+
`);
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
function parseArgs(argv) {
|
|
371
|
+
const options = { type: null, out: null, stdout: false, description: [] };
|
|
372
|
+
for (let i = 0; i < argv.length; i += 1) {
|
|
373
|
+
const arg = argv[i];
|
|
374
|
+
if (arg === '--help' || arg === '-h') { options.help = true; continue; }
|
|
375
|
+
if (arg === '--stdout') { options.stdout = true; continue; }
|
|
376
|
+
if (arg === '--type') { options.type = argv[++i]; continue; }
|
|
377
|
+
if (arg.startsWith('--type=')) { options.type = arg.slice(7); continue; }
|
|
378
|
+
if (arg === '--out') { options.out = argv[++i]; continue; }
|
|
379
|
+
if (arg.startsWith('--out=')) { options.out = arg.slice(6); continue; }
|
|
380
|
+
options.description.push(arg);
|
|
381
|
+
}
|
|
382
|
+
options.description = options.description.join(' ').trim();
|
|
383
|
+
return options;
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
export async function runWireframeCli(argv = [], { cwd = process.cwd() } = {}) {
|
|
387
|
+
const options = parseArgs(argv);
|
|
388
|
+
if (options.help || !options.description) {
|
|
389
|
+
printHelp();
|
|
390
|
+
if (!options.help && !options.description) process.exit(1);
|
|
391
|
+
return;
|
|
392
|
+
}
|
|
393
|
+
if (options.type && !TYPES.includes(options.type)) {
|
|
394
|
+
console.error(`Unknown type: ${options.type}. Valid: ${TYPES.join(', ')}`);
|
|
395
|
+
process.exit(1);
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
const result = generateWireframe({ description: options.description, type: options.type });
|
|
399
|
+
|
|
400
|
+
if (options.stdout) {
|
|
401
|
+
process.stdout.write(result.content);
|
|
402
|
+
return;
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
const slug = slugify(options.description);
|
|
406
|
+
const fileName = `${slug}-${timestamp()}.${result.extension}`;
|
|
407
|
+
const outPath = options.out
|
|
408
|
+
? resolve(cwd, options.out)
|
|
409
|
+
: join(cwd, '.cx', 'wireframes', fileName);
|
|
410
|
+
|
|
411
|
+
const dir = outPath.slice(0, outPath.lastIndexOf('/'));
|
|
412
|
+
if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
|
|
413
|
+
writeFileSync(outPath, result.content, 'utf8');
|
|
414
|
+
|
|
415
|
+
console.log(`Wireframe (${result.type}, ${result.format}) written to:`);
|
|
416
|
+
console.log(` ${outPath}`);
|
|
417
|
+
if (result.format === 'html') {
|
|
418
|
+
console.log(`\nOpen it with: open "${outPath}"`);
|
|
419
|
+
} else {
|
|
420
|
+
console.log(`\nRender inline: paste into any Mermaid-aware viewer (GitHub, Notion, Obsidian, OpenCode).`);
|
|
421
|
+
}
|
|
422
|
+
}
|