@geraldmaron/construct 1.0.0 → 1.0.2
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 +9 -6
- 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 +1000 -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 +523 -36
- 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/codex-config.mjs
CHANGED
|
@@ -87,7 +87,9 @@ export function buildCodexMcpEntry(id, mcpDef, resolvedValues = {}) {
|
|
|
87
87
|
tool_timeout_sec: 60,
|
|
88
88
|
};
|
|
89
89
|
|
|
90
|
-
if (id
|
|
90
|
+
if (id === 'github') {
|
|
91
|
+
entry.bearer_token_env_var = 'GITHUB_TOKEN';
|
|
92
|
+
} else {
|
|
91
93
|
const authorization = mcpDef.headers?.Authorization;
|
|
92
94
|
const match = typeof authorization === 'string' ? authorization.match(/^Bearer __([A-Z0-9_]+)__$/) : null;
|
|
93
95
|
if (match) entry.bearer_token_env_var = match[1];
|
package/lib/comment-lint.mjs
CHANGED
|
@@ -35,8 +35,13 @@ function relPath(rootDir, absPath) {
|
|
|
35
35
|
|
|
36
36
|
function requiresHeader(rel) {
|
|
37
37
|
const ext = path.extname(rel);
|
|
38
|
+
if (['.yaml', '.yml', '.json', '.jsonl', '.toml'].includes(ext)) return { required: false, type: null };
|
|
38
39
|
const jsMatch = JS_HEADER_GLOBS.some(r => r.test(rel));
|
|
39
40
|
const mdMatch = MD_HEADER_GLOBS.some(r => r.test(rel));
|
|
41
|
+
// Exclude static assets under lib/server/static from requiring headers
|
|
42
|
+
if (rel.startsWith('lib/server/static/')) {
|
|
43
|
+
return { required: false, type: null };
|
|
44
|
+
}
|
|
40
45
|
const type = (jsMatch && ext !== '.html') ? 'js' : (jsMatch || mdMatch) ? 'md' : null;
|
|
41
46
|
return { required: jsMatch || mdMatch, type };
|
|
42
47
|
}
|
|
@@ -54,22 +59,66 @@ function hasHeader(content, type) {
|
|
|
54
59
|
}
|
|
55
60
|
|
|
56
61
|
// --- banned comment patterns ---
|
|
62
|
+
//
|
|
63
|
+
// Comment-line prefix matches both `//` line comments and JSDoc body lines
|
|
64
|
+
// (those start with optional whitespace + `*`). The leading `^\s*\*(?!\/)`
|
|
65
|
+
// keeps it from matching `*/` block-end lines or arithmetic.
|
|
66
|
+
|
|
67
|
+
const CMT = '(?:\\/\\/|^\\s*\\*(?!\\/))';
|
|
57
68
|
|
|
58
69
|
const BANNED = [
|
|
59
|
-
|
|
60
|
-
{ pattern:
|
|
61
|
-
{ pattern:
|
|
62
|
-
{ pattern:
|
|
70
|
+
// point-in-time
|
|
71
|
+
{ pattern: new RegExp(`${CMT}.*\\b(?:added for|added recently|recently added|just added|new:)\\b`, 'i'), label: 'point-in-time: "added for / recently / just / new:"' },
|
|
72
|
+
{ pattern: new RegExp(`${CMT}.*\\b(?:previously|no longer|used to|was replaced|replaced by)\\b`, 'i'), label: 'point-in-time: history belongs in git log, not source' },
|
|
73
|
+
{ pattern: new RegExp(`${CMT}.*\\b(?:replaces the (?:old|previous|prior)|consolidates what was|instead of (?:only|the|a) (?:old|prior|previous))\\b`, 'i'), label: 'comparative narrative: describe what the code does, not what it used to do' },
|
|
74
|
+
{ pattern: new RegExp(`${CMT}.*\\b(?:the (?:new|current) (?:path|approach|impl|implementation|design) (?:uses|writes|reads|does|fires|spins))\\b`, 'i'), label: 'comparative narrative: "the new X uses/does Y" — describe the design directly' },
|
|
75
|
+
{ pattern: new RegExp(`${CMT}.*(?:#\\d{3,}|GH-\\d+|JIRA-\\d+|closes #|fixes #|ticket )`, 'i'), label: 'issue/PR reference in source comment (put in commit message instead)' },
|
|
76
|
+
// narrative voice
|
|
77
|
+
{ pattern: new RegExp(`${CMT}\\s+(?:We |This |It |Now )\\w`, 'i'), label: 'narrative voice: avoid "We/This/It/Now" — describe the constraint, not the story' },
|
|
78
|
+
// noise sentinels
|
|
79
|
+
{ pattern: new RegExp(`${CMT}\\s*(?:ok|OK|skip|Skip|best effort)\\s*$`, 'i'), label: 'noise sentinel: "ok / skip / best effort" carries no decision content' },
|
|
80
|
+
// caller references
|
|
81
|
+
{ pattern: new RegExp(`${CMT}.*\\b(?:used by|called from|only consumer)\\b`, 'i'), label: 'caller reference: "used by / called from"' },
|
|
82
|
+
// step markers — only flag in `//` comments. Ordered lists are
|
|
83
|
+
// conventional inside JSDoc decision-tree and flow documentation.
|
|
84
|
+
{ pattern: /\/\/\s+\d+\.\s+\w/, label: 'step marker: use function names or block structure instead of "// 1. Step"' },
|
|
85
|
+
// markdown equivalents
|
|
63
86
|
{ pattern: /<!--.*\b(?:added for|added recently|just added|new:)\b.*-->/i, label: 'point-in-time in markdown comment' },
|
|
64
87
|
{ pattern: /<!--.*\b(?:used by|called from|only consumer)\b.*-->/i, label: 'caller reference in markdown comment' },
|
|
65
|
-
|
|
88
|
+
// TODO without owner
|
|
89
|
+
{ pattern: new RegExp(`${CMT}\\s*TODO(?:\\((\\w+)\\))?:?(?!\\s*\\(\\w+\\):)`, 'i'), label: 'TODO without owner — use: TODO(owner): what and why' }, // construct-lint-ignore
|
|
90
|
+
];
|
|
91
|
+
|
|
92
|
+
// Files whose content is authored precisely to demonstrate banned patterns
|
|
93
|
+
// (the linter itself, the canonical rule file, test fixtures that feed the
|
|
94
|
+
// linter banned strings, and teaching docs that show patterns to avoid).
|
|
95
|
+
// Linting them would flag the demonstrations as violations.
|
|
96
|
+
const BANNED_CHECK_SKIP = [
|
|
97
|
+
'lib/comment-lint.mjs',
|
|
98
|
+
'rules/common/comments.md',
|
|
99
|
+
'tests/comment-lint.test.mjs',
|
|
100
|
+
'commands/work/clean.md',
|
|
101
|
+
'skills/utility/clean-code.md',
|
|
66
102
|
];
|
|
67
103
|
|
|
104
|
+
function isBannedCheckSkipped(filePath) {
|
|
105
|
+
if (!filePath) return false;
|
|
106
|
+
return BANNED_CHECK_SKIP.some((suffix) => filePath.endsWith(suffix));
|
|
107
|
+
}
|
|
108
|
+
|
|
68
109
|
function checkBanned(content, filePath) {
|
|
69
|
-
if (
|
|
110
|
+
if (isBannedCheckSkipped(filePath)) return [];
|
|
111
|
+
|
|
112
|
+
// Banned patterns are JS/TS comment conventions — they don't apply to markdown prose.
|
|
113
|
+
if (filePath && path.extname(filePath) === '.md') return [];
|
|
114
|
+
|
|
70
115
|
const warnings = [];
|
|
71
116
|
const lines = content.split('\n');
|
|
72
117
|
for (let i = 0; i < lines.length; i++) {
|
|
118
|
+
// Per-line escape hatch for the rare legitimate case — a section label
|
|
119
|
+
// describing the pattern it detects, e.g. "// TODO/FIXME/HACK comments"
|
|
120
|
+
// in a detector. Marker must appear on the same line.
|
|
121
|
+
if (lines[i].includes('construct-lint-ignore')) continue;
|
|
73
122
|
for (const { pattern, label } of BANNED) {
|
|
74
123
|
if (pattern.test(lines[i])) {
|
|
75
124
|
warnings.push({ line: i + 1, label });
|
package/lib/completions.mjs
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* completions.mjs — generates bash and zsh completion scripts for construct.
|
|
3
3
|
*
|
|
4
|
-
* Called by sync-agents.mjs after each sync.
|
|
4
|
+
* Called by scripts/sync-agents.mjs after each sync.
|
|
5
5
|
* Outputs to a platform-appropriate per-user completions directory.
|
|
6
6
|
*/
|
|
7
7
|
|
|
@@ -14,13 +14,22 @@ const COMPLETIONS_DIR = process.platform === 'win32'
|
|
|
14
14
|
? join(process.env.APPDATA ?? join(homedir(), 'AppData', 'Roaming'), 'construct', 'completions')
|
|
15
15
|
: join(homedir(), '.local', 'share', 'construct', 'completions');
|
|
16
16
|
|
|
17
|
+
function subcommandName(subcommand) {
|
|
18
|
+
return typeof subcommand === 'string' ? subcommand : subcommand.name;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function subcommandDesc(subcommand) {
|
|
22
|
+
if (typeof subcommand === 'string') return '';
|
|
23
|
+
return subcommand.desc || '';
|
|
24
|
+
}
|
|
25
|
+
|
|
17
26
|
function buildBash() {
|
|
18
27
|
const names = CLI_COMMANDS.map(c => c.name).join(' ');
|
|
19
28
|
|
|
20
29
|
const subcommandCases = CLI_COMMANDS
|
|
21
30
|
.filter(c => c.subcommands?.length)
|
|
22
31
|
.map(c => {
|
|
23
|
-
const subs = c.subcommands.map(
|
|
32
|
+
const subs = c.subcommands.map(subcommandName).filter(Boolean).join(' ');
|
|
24
33
|
return ` ${c.name}) COMPREPLY=($(compgen -W "${subs}" -- "$cur")) ;;`;
|
|
25
34
|
})
|
|
26
35
|
.join('\n');
|
|
@@ -86,21 +95,21 @@ complete -F _construct_completions construct
|
|
|
86
95
|
|
|
87
96
|
function buildZsh() {
|
|
88
97
|
const commandDescriptions = CLI_COMMANDS
|
|
89
|
-
.map(c => ` '${c.name}:${c.emoji} ${c.description.replace(/'/g, "\\'")}' \\`)
|
|
98
|
+
.map(c => ` '${c.name}:${c.emoji || ''} ${(c.description || '').replace(/'/g, "\\'")}' \\`)
|
|
90
99
|
.join('\n');
|
|
91
100
|
|
|
92
101
|
const subcommandFunctions = CLI_COMMANDS
|
|
93
102
|
.filter(c => c.subcommands?.length)
|
|
94
103
|
.map(c => {
|
|
95
104
|
const subs = c.subcommands
|
|
96
|
-
.map(s => ` '${s
|
|
105
|
+
.map(s => ` '${(subcommandName(s) || '')}:${subcommandDesc(s).replace(/'/g, "\\'")}'`)
|
|
97
106
|
.join('\n');
|
|
98
|
-
return `_construct_${c.name.replace(/-/g, '_')}() {
|
|
107
|
+
return `_construct_${(c.name || '').replace(/-/g, '_')}() {
|
|
99
108
|
local subcmds
|
|
100
109
|
subcmds=(
|
|
101
110
|
${subs}
|
|
102
111
|
)
|
|
103
|
-
_describe -t subcommands '${c.name}
|
|
112
|
+
_describe -t subcommands '${c.name}' subcmds
|
|
104
113
|
}`;
|
|
105
114
|
})
|
|
106
115
|
.join('\n\n');
|
|
@@ -153,6 +162,12 @@ _construct
|
|
|
153
162
|
`;
|
|
154
163
|
}
|
|
155
164
|
|
|
165
|
+
export function getCompletionScript(shell) {
|
|
166
|
+
if (shell === 'bash') return buildBash();
|
|
167
|
+
if (shell === 'zsh') return buildZsh();
|
|
168
|
+
throw new Error(`Unsupported shell: ${shell}`);
|
|
169
|
+
}
|
|
170
|
+
|
|
156
171
|
export function generateCompletions() {
|
|
157
172
|
try {
|
|
158
173
|
mkdirSync(COMPLETIONS_DIR, { recursive: true });
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* lib/config/alias.mjs — resolves the user-facing Construct alias.
|
|
3
|
+
*
|
|
4
|
+
* Lookup precedence:
|
|
5
|
+
* 1. CONSTRUCT_ALIAS env var (per-session override)
|
|
6
|
+
* 2. ~/.construct/config.json.aliasOverride (user-level, lets each
|
|
7
|
+
* user on the same project see their own branding without
|
|
8
|
+
* committing it)
|
|
9
|
+
* 3. construct.config.json.alias (project-level, committed)
|
|
10
|
+
* 4. "Construct" (the default)
|
|
11
|
+
*
|
|
12
|
+
* Pure resolution — no fs writes. The alias flows to the dashboard
|
|
13
|
+
* wordmark (via /api/alias), persona prompts ({{alias}} template at
|
|
14
|
+
* sync time), and CLI banner output.
|
|
15
|
+
*/
|
|
16
|
+
import fs from 'node:fs';
|
|
17
|
+
import os from 'node:os';
|
|
18
|
+
import path from 'node:path';
|
|
19
|
+
import { loadProjectConfig } from './project-config.mjs';
|
|
20
|
+
|
|
21
|
+
export const DEFAULT_ALIAS = 'Construct';
|
|
22
|
+
export const ALIAS_ENV_KEY = 'CONSTRUCT_ALIAS';
|
|
23
|
+
export const ALIAS_TEMPLATE_TOKEN = /\{\{alias\}\}/g;
|
|
24
|
+
|
|
25
|
+
export function resolveAlias({ cwd = process.cwd(), env = process.env, homeDir = os.homedir() } = {}) {
|
|
26
|
+
const envAlias = env?.[ALIAS_ENV_KEY];
|
|
27
|
+
if (typeof envAlias === 'string' && envAlias.trim()) {
|
|
28
|
+
return { value: envAlias.trim(), source: 'env' };
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
try {
|
|
32
|
+
const userCfgPath = path.join(homeDir, '.construct', 'config.json');
|
|
33
|
+
if (fs.existsSync(userCfgPath)) {
|
|
34
|
+
const userCfg = JSON.parse(fs.readFileSync(userCfgPath, 'utf8'));
|
|
35
|
+
const userAlias = userCfg?.aliasOverride;
|
|
36
|
+
if (typeof userAlias === 'string' && userAlias.trim()) {
|
|
37
|
+
return { value: userAlias.trim(), source: 'user' };
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
} catch { /* user file is best-effort */ }
|
|
41
|
+
|
|
42
|
+
try {
|
|
43
|
+
const { config, source } = loadProjectConfig(cwd, env);
|
|
44
|
+
if (source === 'file' && typeof config?.alias === 'string' && config.alias.trim()) {
|
|
45
|
+
return { value: config.alias.trim(), source: 'project' };
|
|
46
|
+
}
|
|
47
|
+
} catch { /* project config is best-effort */ }
|
|
48
|
+
|
|
49
|
+
return { value: DEFAULT_ALIAS, source: 'default' };
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export function applyAliasToTemplate(text, alias) {
|
|
53
|
+
if (typeof text !== 'string') return text;
|
|
54
|
+
const safe = String(alias ?? DEFAULT_ALIAS).trim() || DEFAULT_ALIAS;
|
|
55
|
+
return text.replace(ALIAS_TEMPLATE_TOKEN, safe);
|
|
56
|
+
}
|
|
@@ -0,0 +1,335 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* lib/config/project-config.mjs — read and write construct.config.json.
|
|
3
|
+
*
|
|
4
|
+
* Source-of-truth loader for project-level Construct settings. Resolves
|
|
5
|
+
* the path by walking up from cwd (stops at git root or filesystem root),
|
|
6
|
+
* validates against the v1 schema, and applies secret interpolation:
|
|
7
|
+
* any string value of the form `$VAR_NAME` is replaced with the
|
|
8
|
+
* corresponding env-var value at load time. Pointers keep API keys in
|
|
9
|
+
* `.env` where they belong; the JSON config never sees a secret literal.
|
|
10
|
+
*
|
|
11
|
+
* Precedence rule for consumers: env var if set > config.json > default.
|
|
12
|
+
* `resolveSetting(config, jsonPath, env, envKey, default)` encodes it
|
|
13
|
+
* once so call sites stay consistent.
|
|
14
|
+
*/
|
|
15
|
+
import fs from 'node:fs';
|
|
16
|
+
import path from 'node:path';
|
|
17
|
+
import { CONFIG_SCHEMA_VERSION, DEFAULT_PROJECT_CONFIG, validateProjectConfig, FIELD_RULES } from './schema.mjs';
|
|
18
|
+
|
|
19
|
+
export const PROJECT_CONFIG_FILENAME = 'construct.config.json';
|
|
20
|
+
|
|
21
|
+
export function findProjectConfigPath(cwd = process.cwd()) {
|
|
22
|
+
let dir = path.resolve(cwd);
|
|
23
|
+
const root = path.parse(dir).root;
|
|
24
|
+
while (dir !== root) {
|
|
25
|
+
const candidate = path.join(dir, PROJECT_CONFIG_FILENAME);
|
|
26
|
+
if (fs.existsSync(candidate)) return candidate;
|
|
27
|
+
if (fs.existsSync(path.join(dir, '.git'))) {
|
|
28
|
+
const inGitRoot = path.join(dir, PROJECT_CONFIG_FILENAME);
|
|
29
|
+
return fs.existsSync(inGitRoot) ? inGitRoot : null;
|
|
30
|
+
}
|
|
31
|
+
dir = path.dirname(dir);
|
|
32
|
+
}
|
|
33
|
+
return null;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const ENV_POINTER_RE = /^\$([A-Z_][A-Z0-9_]*)$/;
|
|
37
|
+
|
|
38
|
+
export function interpolateSecrets(value, env = process.env) {
|
|
39
|
+
if (typeof value === 'string') {
|
|
40
|
+
const match = value.match(ENV_POINTER_RE);
|
|
41
|
+
if (!match) return value;
|
|
42
|
+
const resolved = env[match[1]];
|
|
43
|
+
return resolved === undefined ? null : resolved;
|
|
44
|
+
}
|
|
45
|
+
if (Array.isArray(value)) return value.map((v) => interpolateSecrets(v, env));
|
|
46
|
+
if (value !== null && typeof value === 'object') {
|
|
47
|
+
const out = {};
|
|
48
|
+
for (const [k, v] of Object.entries(value)) out[k] = interpolateSecrets(v, env);
|
|
49
|
+
return out;
|
|
50
|
+
}
|
|
51
|
+
return value;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function deepMerge(base, override) {
|
|
55
|
+
if (override === undefined) return base;
|
|
56
|
+
if (override === null || typeof override !== 'object' || Array.isArray(override)) return override;
|
|
57
|
+
const out = Array.isArray(base) ? [...(base || [])] : { ...(base || {}) };
|
|
58
|
+
for (const [k, v] of Object.entries(override)) {
|
|
59
|
+
out[k] = deepMerge(base?.[k], v);
|
|
60
|
+
}
|
|
61
|
+
return out;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export function loadProjectConfig(cwd = process.cwd(), env = process.env) {
|
|
65
|
+
const configPath = findProjectConfigPath(cwd);
|
|
66
|
+
if (!configPath) {
|
|
67
|
+
return {
|
|
68
|
+
path: null,
|
|
69
|
+
raw: null,
|
|
70
|
+
config: structuredClone(DEFAULT_PROJECT_CONFIG),
|
|
71
|
+
source: 'default',
|
|
72
|
+
errors: [],
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
let raw;
|
|
76
|
+
try {
|
|
77
|
+
raw = JSON.parse(fs.readFileSync(configPath, 'utf8'));
|
|
78
|
+
} catch (err) {
|
|
79
|
+
return {
|
|
80
|
+
path: configPath,
|
|
81
|
+
raw: null,
|
|
82
|
+
config: structuredClone(DEFAULT_PROJECT_CONFIG),
|
|
83
|
+
source: 'invalid',
|
|
84
|
+
errors: [`failed to parse ${configPath}: ${err.message}`],
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
const validation = validateProjectConfig(raw);
|
|
88
|
+
if (!validation.valid) {
|
|
89
|
+
return {
|
|
90
|
+
path: configPath,
|
|
91
|
+
raw,
|
|
92
|
+
config: structuredClone(DEFAULT_PROJECT_CONFIG),
|
|
93
|
+
source: 'invalid',
|
|
94
|
+
errors: validation.errors,
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
const merged = deepMerge(structuredClone(DEFAULT_PROJECT_CONFIG), raw);
|
|
98
|
+
const resolved = interpolateSecrets(merged, env);
|
|
99
|
+
return {
|
|
100
|
+
path: configPath,
|
|
101
|
+
raw,
|
|
102
|
+
config: resolved,
|
|
103
|
+
source: 'file',
|
|
104
|
+
errors: [],
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
export function writeProjectConfig(filePath, config, options = {}) {
|
|
109
|
+
const {
|
|
110
|
+
validate = true, // Perform validation before write
|
|
111
|
+
interactive = false, // Prompt for fixes in interactive mode
|
|
112
|
+
dryRun = false, // Validate only, don't write
|
|
113
|
+
strict = false, // Reject unknown fields
|
|
114
|
+
} = options;
|
|
115
|
+
|
|
116
|
+
// Pre-validation: Check for common mistakes
|
|
117
|
+
const preErrors = [];
|
|
118
|
+
const warnings = [];
|
|
119
|
+
|
|
120
|
+
// Check 1: Version mismatch
|
|
121
|
+
if (config.version && config.version !== CONFIG_SCHEMA_VERSION) {
|
|
122
|
+
preErrors.push(`version mismatch: file has v${config.version}, expected v${CONFIG_SCHEMA_VERSION}`);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// Check 2: Unknown top-level fields (warning in non-strict mode)
|
|
126
|
+
if (typeof config === 'object' && config !== null) {
|
|
127
|
+
const knownFields = Object.keys(FIELD_RULES);
|
|
128
|
+
const unknownFields = Object.keys(config).filter(k => !knownFields.includes(k));
|
|
129
|
+
if (unknownFields.length > 0) {
|
|
130
|
+
const msg = `unknown fields will be ignored: ${unknownFields.join(', ')}`;
|
|
131
|
+
if (strict) {
|
|
132
|
+
preErrors.push(msg);
|
|
133
|
+
} else {
|
|
134
|
+
warnings.push(msg);
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// Check 3: Secret literals (security warning)
|
|
140
|
+
const secretPattern = /(api[_-]?key|token|secret|password)/i;
|
|
141
|
+
function findSecretLiterals(obj, path = '') {
|
|
142
|
+
const found = [];
|
|
143
|
+
for (const [key, value] of Object.entries(obj)) {
|
|
144
|
+
const currentPath = path ? `${path}.${key}` : key;
|
|
145
|
+
if (secretPattern.test(key) && typeof value === 'string' && !value.startsWith('$')) {
|
|
146
|
+
found.push(currentPath);
|
|
147
|
+
}
|
|
148
|
+
if (typeof value === 'object' && value !== null) {
|
|
149
|
+
found.push(...findSecretLiterals(value, currentPath));
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
return found;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
const secretPaths = findSecretLiterals(config);
|
|
156
|
+
if (secretPaths.length > 0) {
|
|
157
|
+
warnings.push(`potential secrets detected at: ${secretPaths.join(', ')}. Use "$ENV_VAR" syntax instead.`);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
// Run full schema validation
|
|
161
|
+
let validation = { valid: true, errors: [] };
|
|
162
|
+
if (validate) {
|
|
163
|
+
validation = validateProjectConfig(config);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// Combine all errors (handle case where validation.errors is undefined)
|
|
167
|
+
const validationErrors = validation.errors || [];
|
|
168
|
+
const allErrors = [...preErrors, ...validationErrors];
|
|
169
|
+
|
|
170
|
+
// Build comprehensive feedback
|
|
171
|
+
const feedback = {
|
|
172
|
+
valid: allErrors.length === 0,
|
|
173
|
+
errors: allErrors,
|
|
174
|
+
warnings,
|
|
175
|
+
filePath,
|
|
176
|
+
wouldWrite: !dryRun && allErrors.length === 0,
|
|
177
|
+
};
|
|
178
|
+
|
|
179
|
+
// Throw on validation errors
|
|
180
|
+
if (allErrors.length > 0) {
|
|
181
|
+
const errorMessage = formatValidationErrors(allErrors, warnings, filePath);
|
|
182
|
+
throw new Error(errorMessage);
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
// Log warnings
|
|
186
|
+
if (warnings.length > 0 && !options.silent) {
|
|
187
|
+
console.error('Config warnings:');
|
|
188
|
+
warnings.forEach(w => console.error(` ⚠ ${w}`));
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
// Perform dry-run or actual write
|
|
192
|
+
if (dryRun) {
|
|
193
|
+
feedback.written = false;
|
|
194
|
+
return feedback;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
// Write the file
|
|
198
|
+
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
|
199
|
+
fs.writeFileSync(filePath, `${JSON.stringify(config, null, 2)}\n`);
|
|
200
|
+
|
|
201
|
+
feedback.written = true;
|
|
202
|
+
return feedback;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
function formatValidationErrors(errors, warnings, filePath) {
|
|
206
|
+
let msg = `refusing to write invalid config: ${errors.join('; ')}`;
|
|
207
|
+
msg += `\n\nInvalid config (${filePath}):\n\n`;
|
|
208
|
+
|
|
209
|
+
msg += 'Errors:\n';
|
|
210
|
+
errors.forEach(e => {
|
|
211
|
+
msg += ` ✗ ${e}\n`;
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
if (warnings.length > 0) {
|
|
215
|
+
msg += '\nWarnings:\n';
|
|
216
|
+
warnings.forEach(w => {
|
|
217
|
+
msg += ` ⚠ ${w}\n`;
|
|
218
|
+
});
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
msg += '\nFix these issues before the config can be saved.';
|
|
222
|
+
msg += '\nRun with --dry-run to validate without writing.';
|
|
223
|
+
|
|
224
|
+
return msg;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
export function initProjectConfig(cwd = process.cwd(), overrides = {}) {
|
|
228
|
+
const filePath = path.join(cwd, PROJECT_CONFIG_FILENAME);
|
|
229
|
+
if (fs.existsSync(filePath)) {
|
|
230
|
+
throw new Error(`${PROJECT_CONFIG_FILENAME} already exists at ${filePath}`);
|
|
231
|
+
}
|
|
232
|
+
const config = deepMerge(structuredClone(DEFAULT_PROJECT_CONFIG), { version: CONFIG_SCHEMA_VERSION, ...overrides });
|
|
233
|
+
writeProjectConfig(filePath, config);
|
|
234
|
+
return filePath;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
export function getConfigValue(config, keyPath, defaultValue) {
|
|
238
|
+
if (!keyPath) return config;
|
|
239
|
+
const parts = keyPath.split('.');
|
|
240
|
+
let cur = config;
|
|
241
|
+
for (const part of parts) {
|
|
242
|
+
if (cur === null || typeof cur !== 'object') return defaultValue;
|
|
243
|
+
cur = cur[part];
|
|
244
|
+
}
|
|
245
|
+
return cur === undefined ? defaultValue : cur;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
export function setConfigValue(config, keyPath, value) {
|
|
249
|
+
if (!keyPath) throw new Error('keyPath required');
|
|
250
|
+
const parts = keyPath.split('.');
|
|
251
|
+
const out = structuredClone(config);
|
|
252
|
+
let cur = out;
|
|
253
|
+
for (let i = 0; i < parts.length - 1; i++) {
|
|
254
|
+
const part = parts[i];
|
|
255
|
+
if (cur[part] === null || typeof cur[part] !== 'object') cur[part] = {};
|
|
256
|
+
cur = cur[part];
|
|
257
|
+
}
|
|
258
|
+
cur[parts[parts.length - 1]] = value;
|
|
259
|
+
return out;
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
/**
|
|
263
|
+
* Set a config value with immediate validation and feedback.
|
|
264
|
+
* Recommended way to update configuration from CLI/commands.
|
|
265
|
+
*
|
|
266
|
+
* @param {string} keyPath - Dot-notation path (e.g., 'deployment.mode')
|
|
267
|
+
* @param {any} value - Value to set
|
|
268
|
+
* @param {Object} options
|
|
269
|
+
* @returns {Object} Validation result with immediate feedback
|
|
270
|
+
*/
|
|
271
|
+
export function setConfigValueWithValidation(keyPath, value, options = {}) {
|
|
272
|
+
const { cwd = process.cwd(), dryRun = false } = options;
|
|
273
|
+
|
|
274
|
+
// Load current config
|
|
275
|
+
const current = loadProjectConfig(cwd);
|
|
276
|
+
const configPath = current.path || path.join(cwd, PROJECT_CONFIG_FILENAME);
|
|
277
|
+
|
|
278
|
+
// Apply the change
|
|
279
|
+
const newConfig = setConfigValue(current.config, keyPath, value);
|
|
280
|
+
|
|
281
|
+
// Validate before writing
|
|
282
|
+
const validation = validateProjectConfig(newConfig);
|
|
283
|
+
|
|
284
|
+
if (!validation.valid) {
|
|
285
|
+
return {
|
|
286
|
+
success: false,
|
|
287
|
+
keyPath,
|
|
288
|
+
value,
|
|
289
|
+
errors: validation.errors,
|
|
290
|
+
message: `Cannot set ${keyPath}: validation failed`,
|
|
291
|
+
};
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
// Write if not dry-run
|
|
295
|
+
if (!dryRun) {
|
|
296
|
+
try {
|
|
297
|
+
writeProjectConfig(configPath, newConfig, { validate: true });
|
|
298
|
+
return {
|
|
299
|
+
success: true,
|
|
300
|
+
keyPath,
|
|
301
|
+
value,
|
|
302
|
+
configPath,
|
|
303
|
+
message: `Set ${keyPath} = ${JSON.stringify(value)}`,
|
|
304
|
+
};
|
|
305
|
+
} catch (error) {
|
|
306
|
+
return {
|
|
307
|
+
success: false,
|
|
308
|
+
keyPath,
|
|
309
|
+
value,
|
|
310
|
+
errors: [error.message],
|
|
311
|
+
message: `Failed to write config: ${error.message}`,
|
|
312
|
+
};
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
// Dry-run mode
|
|
317
|
+
return {
|
|
318
|
+
success: true,
|
|
319
|
+
keyPath,
|
|
320
|
+
value,
|
|
321
|
+
dryRun: true,
|
|
322
|
+
message: `Would set ${keyPath} = ${JSON.stringify(value)} (dry-run)`,
|
|
323
|
+
};
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
export function resolveSetting({ config, jsonPath, env, envKey, defaultValue }) {
|
|
327
|
+
if (env && envKey && env[envKey] !== undefined && env[envKey] !== '') {
|
|
328
|
+
return { value: env[envKey], source: 'env', envKey };
|
|
329
|
+
}
|
|
330
|
+
const fromJson = getConfigValue(config, jsonPath, undefined);
|
|
331
|
+
if (fromJson !== undefined && fromJson !== null) {
|
|
332
|
+
return { value: fromJson, source: 'config', jsonPath };
|
|
333
|
+
}
|
|
334
|
+
return { value: defaultValue, source: 'default' };
|
|
335
|
+
}
|