@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
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* lib/storage/migrations.mjs — Postgres migration runner with hash tracking.
|
|
3
|
+
*
|
|
4
|
+
* Walks `db/schema/*.sql` in lexical order and applies any not yet
|
|
5
|
+
* recorded in `construct_schema_migrations`. Each applied file's SHA256 is
|
|
6
|
+
* stored alongside the filename. Re-running the runner against an unchanged
|
|
7
|
+
* tree is a no-op.
|
|
8
|
+
*
|
|
9
|
+
* Drift detection: if an applied file's SHA changes between runs, the runner
|
|
10
|
+
* fails fast with a precise error. SQL files are append-only by convention —
|
|
11
|
+
* to evolve schema after a migration ships, write a new file with a higher
|
|
12
|
+
* sequence number.
|
|
13
|
+
*
|
|
14
|
+
* Repair mode: long-lived developer databases pre-date the append-only policy
|
|
15
|
+
* and carry stale SHAs for files that have since evolved. Call runMigrations
|
|
16
|
+
* with `repair: true` (or `construct storage repair-migrations`) to re-apply
|
|
17
|
+
* each drifted file. The repair refuses on any file containing a destructive
|
|
18
|
+
* statement (`DROP`, `TRUNCATE`, `ALTER … DROP`) so the safety bar stays
|
|
19
|
+
* "only fully-idempotent migrations may be re-recorded."
|
|
20
|
+
*
|
|
21
|
+
* The bookkeeping table (`construct_schema_migrations`) is created inline
|
|
22
|
+
* before applied state is checked, so the runner self-bootstraps and a fresh
|
|
23
|
+
* database can run from any starting migration without a prerequisite file.
|
|
24
|
+
*/
|
|
25
|
+
|
|
26
|
+
import { readdirSync, readFileSync } from 'node:fs';
|
|
27
|
+
import { join, dirname } from 'node:path';
|
|
28
|
+
import { fileURLToPath } from 'node:url';
|
|
29
|
+
import { createHash } from 'node:crypto';
|
|
30
|
+
|
|
31
|
+
const MODULE_DIR = dirname(fileURLToPath(import.meta.url));
|
|
32
|
+
const DEFAULT_MIGRATIONS_DIR = join(MODULE_DIR, '..', '..', 'db', 'schema');
|
|
33
|
+
|
|
34
|
+
function sha256(text) {
|
|
35
|
+
return createHash('sha256').update(text).digest('hex');
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function listMigrationFiles(dir) {
|
|
39
|
+
return readdirSync(dir)
|
|
40
|
+
.filter((name) => name.endsWith('.sql'))
|
|
41
|
+
.sort();
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const BOOKKEEPING_DDL = `
|
|
45
|
+
CREATE TABLE IF NOT EXISTS construct_schema_migrations (
|
|
46
|
+
filename TEXT PRIMARY KEY,
|
|
47
|
+
sha TEXT NOT NULL,
|
|
48
|
+
applied_at TIMESTAMPTZ NOT NULL DEFAULT now()
|
|
49
|
+
);
|
|
50
|
+
`;
|
|
51
|
+
|
|
52
|
+
// Heuristic: a migration is safe to re-apply (idempotent) when it contains no
|
|
53
|
+
// destructive statements. Comments are stripped first so a `-- DROP …` note in
|
|
54
|
+
// a docstring does not falsely flag the file.
|
|
55
|
+
const DESTRUCTIVE_RE = /\b(drop\s+(table|index|view|column|schema|extension|function|sequence)\b|truncate\b|alter\s+table\s+[^;]*?\bdrop\b|delete\s+from\b)/i;
|
|
56
|
+
|
|
57
|
+
function stripSqlComments(sql) {
|
|
58
|
+
return sql
|
|
59
|
+
.replace(/--[^\n]*/g, '')
|
|
60
|
+
.replace(/\/\*[\s\S]*?\*\//g, '');
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export function isIdempotentMigration(sql) {
|
|
64
|
+
return !DESTRUCTIVE_RE.test(stripSqlComments(sql));
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Apply pending migrations. Returns a per-file outcome summary.
|
|
69
|
+
*
|
|
70
|
+
* @param {object} client — postgres client (from createSqlClient)
|
|
71
|
+
* @param {object} [opts]
|
|
72
|
+
* @param {string} [opts.dir] — schema/migrations directory, defaults to db/schema
|
|
73
|
+
* @param {boolean} [opts.repair] — when true, re-apply drifted files that are
|
|
74
|
+
* verifiably idempotent and update their recorded SHA. Destructive drift
|
|
75
|
+
* still throws. Default false.
|
|
76
|
+
* @returns {Promise<{ applied: string[], skipped: string[], repaired: string[], drift: Array<{ filename: string, expected: string, actual: string, idempotent: boolean }> }>}
|
|
77
|
+
*/
|
|
78
|
+
export async function runMigrations(client, { dir = DEFAULT_MIGRATIONS_DIR, repair = false } = {}) {
|
|
79
|
+
if (!client) throw new Error('runMigrations requires a postgres client');
|
|
80
|
+
|
|
81
|
+
await client.unsafe(BOOKKEEPING_DDL);
|
|
82
|
+
|
|
83
|
+
const recorded = await client`
|
|
84
|
+
SELECT filename, sha FROM construct_schema_migrations
|
|
85
|
+
`;
|
|
86
|
+
const recordedMap = new Map(recorded.map((r) => [r.filename, r.sha]));
|
|
87
|
+
|
|
88
|
+
const files = listMigrationFiles(dir);
|
|
89
|
+
const applied = [];
|
|
90
|
+
const skipped = [];
|
|
91
|
+
const repaired = [];
|
|
92
|
+
const drift = [];
|
|
93
|
+
|
|
94
|
+
for (const filename of files) {
|
|
95
|
+
const path = join(dir, filename);
|
|
96
|
+
const contents = readFileSync(path, 'utf8');
|
|
97
|
+
const sha = sha256(contents);
|
|
98
|
+
|
|
99
|
+
const prev = recordedMap.get(filename);
|
|
100
|
+
if (prev) {
|
|
101
|
+
if (prev !== sha) {
|
|
102
|
+
const idempotent = isIdempotentMigration(contents);
|
|
103
|
+
if (repair && idempotent) {
|
|
104
|
+
// Re-apply the migration (its IF NOT EXISTS clauses make this a
|
|
105
|
+
// no-op against the live schema) and update the recorded SHA.
|
|
106
|
+
await client.begin(async (tx) => {
|
|
107
|
+
await tx.unsafe(contents);
|
|
108
|
+
await tx`
|
|
109
|
+
UPDATE construct_schema_migrations
|
|
110
|
+
SET sha = ${sha}, applied_at = now()
|
|
111
|
+
WHERE filename = ${filename}
|
|
112
|
+
`;
|
|
113
|
+
});
|
|
114
|
+
repaired.push(filename);
|
|
115
|
+
} else {
|
|
116
|
+
drift.push({ filename, expected: prev, actual: sha, idempotent });
|
|
117
|
+
}
|
|
118
|
+
} else {
|
|
119
|
+
skipped.push(filename);
|
|
120
|
+
}
|
|
121
|
+
continue;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
await client.begin(async (tx) => {
|
|
125
|
+
await tx.unsafe(contents);
|
|
126
|
+
await tx`
|
|
127
|
+
INSERT INTO construct_schema_migrations (filename, sha)
|
|
128
|
+
VALUES (${filename}, ${sha})
|
|
129
|
+
ON CONFLICT (filename) DO UPDATE SET sha = EXCLUDED.sha, applied_at = now()
|
|
130
|
+
`;
|
|
131
|
+
});
|
|
132
|
+
applied.push(filename);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
if (drift.length > 0) {
|
|
136
|
+
const detail = drift
|
|
137
|
+
.map((d) => ` ${d.filename}: applied SHA ${d.expected.slice(0, 12)}…, on-disk SHA ${d.actual.slice(0, 12)}…${d.idempotent ? ' (idempotent — repairable via `construct storage repair-migrations`)' : ' (NOT idempotent — write a new migration file)'}`)
|
|
138
|
+
.join('\n');
|
|
139
|
+
throw new Error(
|
|
140
|
+
`Migration drift detected (a previously-applied migration file has changed). ` +
|
|
141
|
+
`Migrations are append-only — write a new file with a higher sequence number ` +
|
|
142
|
+
`to evolve the schema. Drift:\n${detail}`
|
|
143
|
+
);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
return { applied, skipped, repaired, drift };
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* Diagnostic for `construct doctor`: returns last applied filename, count,
|
|
151
|
+
* and any drift, without throwing.
|
|
152
|
+
*/
|
|
153
|
+
export async function describeMigrations(client, { dir = DEFAULT_MIGRATIONS_DIR } = {}) {
|
|
154
|
+
if (!client) return { ok: false, reason: 'no_sql_client' };
|
|
155
|
+
try {
|
|
156
|
+
await client.unsafe(BOOKKEEPING_DDL);
|
|
157
|
+
const rows = await client`
|
|
158
|
+
SELECT filename, sha, applied_at
|
|
159
|
+
FROM construct_schema_migrations
|
|
160
|
+
ORDER BY applied_at DESC, filename DESC
|
|
161
|
+
LIMIT 1
|
|
162
|
+
`;
|
|
163
|
+
const onDisk = listMigrationFiles(dir);
|
|
164
|
+
const applied = await client`SELECT count(*)::int AS n FROM construct_schema_migrations`;
|
|
165
|
+
const recorded = await client`SELECT filename, sha FROM construct_schema_migrations`;
|
|
166
|
+
const recordedMap = new Map(recorded.map((r) => [r.filename, r.sha]));
|
|
167
|
+
const drift = [];
|
|
168
|
+
for (const filename of onDisk) {
|
|
169
|
+
const contents = readFileSync(join(dir, filename), 'utf8');
|
|
170
|
+
const sha = sha256(contents);
|
|
171
|
+
const prev = recordedMap.get(filename);
|
|
172
|
+
if (prev && prev !== sha) {
|
|
173
|
+
drift.push({ filename, idempotent: isIdempotentMigration(contents) });
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
return {
|
|
177
|
+
ok: true,
|
|
178
|
+
lastApplied: rows[0]?.filename || null,
|
|
179
|
+
lastAppliedAt: rows[0]?.applied_at || null,
|
|
180
|
+
appliedCount: applied[0]?.n ?? 0,
|
|
181
|
+
onDiskCount: onDisk.length,
|
|
182
|
+
drift,
|
|
183
|
+
};
|
|
184
|
+
} catch (err) {
|
|
185
|
+
return { ok: false, reason: err?.message || 'describe-migrations failed' };
|
|
186
|
+
}
|
|
187
|
+
}
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* lib/storage/postgres-backup.mjs — durable stash/restore for the managed construct-postgres container.
|
|
4
|
+
*
|
|
5
|
+
* Mirrors the telemetry backup pattern in service-manager.mjs. Entry points:
|
|
6
|
+
* - construct down → stashConstructDb (dump before container stop)
|
|
7
|
+
* - construct setup → restoreConstructDb (reload after fresh container start)
|
|
8
|
+
*
|
|
9
|
+
* Dumps are stored in ~/.construct/backups/postgres/ as pg_dump custom-format
|
|
10
|
+
* files. The N most recent are kept; older ones are pruned automatically.
|
|
11
|
+
*/
|
|
12
|
+
import fs from 'node:fs';
|
|
13
|
+
import os from 'node:os';
|
|
14
|
+
import path from 'node:path';
|
|
15
|
+
import { spawnSync } from 'node:child_process';
|
|
16
|
+
|
|
17
|
+
const POSTGRES_CONTAINER = 'construct-postgres';
|
|
18
|
+
const POSTGRES_USER = 'construct';
|
|
19
|
+
const POSTGRES_DB = 'construct';
|
|
20
|
+
const DEFAULT_KEEP = 5;
|
|
21
|
+
|
|
22
|
+
function pruneBackupDir(dir, keep) {
|
|
23
|
+
try {
|
|
24
|
+
const dumps = fs.readdirSync(dir)
|
|
25
|
+
.filter((f) => f.startsWith('construct-') && f.endsWith('.dump'))
|
|
26
|
+
.sort()
|
|
27
|
+
.reverse();
|
|
28
|
+
for (const dump of dumps.slice(keep)) {
|
|
29
|
+
fs.rmSync(path.join(dir, dump), { force: true });
|
|
30
|
+
fs.rmSync(path.join(dir, dump.replace('.dump', '.json')), { force: true });
|
|
31
|
+
}
|
|
32
|
+
} catch { /* non-critical */ }
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Dump the construct Postgres database to a timestamped file under
|
|
37
|
+
* ~/.construct/backups/postgres/. Safe to call when the container is not
|
|
38
|
+
* running — returns { status: 'no-data' } silently.
|
|
39
|
+
*/
|
|
40
|
+
export function stashConstructDb({
|
|
41
|
+
homeDir = os.homedir(),
|
|
42
|
+
spawnSyncFn = spawnSync,
|
|
43
|
+
keep = DEFAULT_KEEP,
|
|
44
|
+
} = {}) {
|
|
45
|
+
const stashDir = path.join(homeDir, '.construct', 'backups', 'postgres');
|
|
46
|
+
fs.mkdirSync(stashDir, { recursive: true });
|
|
47
|
+
|
|
48
|
+
const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
|
|
49
|
+
const dumpFile = path.join(stashDir, `construct-${timestamp}.dump`);
|
|
50
|
+
const manifestFile = path.join(stashDir, `construct-${timestamp}.json`);
|
|
51
|
+
|
|
52
|
+
const dump = spawnSyncFn('docker', [
|
|
53
|
+
'exec', POSTGRES_CONTAINER,
|
|
54
|
+
'pg_dump', '-U', POSTGRES_USER, '-d', POSTGRES_DB,
|
|
55
|
+
'-Fc', '--data-only',
|
|
56
|
+
], { stdio: ['ignore', 'pipe', 'ignore'], maxBuffer: 200 * 1024 * 1024 });
|
|
57
|
+
|
|
58
|
+
const hasData = dump.status === 0 && dump.stdout?.length > 100;
|
|
59
|
+
if (!hasData) return { status: 'no-data', stashPath: null };
|
|
60
|
+
|
|
61
|
+
fs.writeFileSync(dumpFile, dump.stdout);
|
|
62
|
+
fs.writeFileSync(manifestFile, JSON.stringify({
|
|
63
|
+
version: 1,
|
|
64
|
+
createdAt: new Date().toISOString(),
|
|
65
|
+
reason: 'pre-shutdown-stash',
|
|
66
|
+
dumpFile: path.basename(dumpFile),
|
|
67
|
+
dumpBytes: dump.stdout.length,
|
|
68
|
+
}, null, 2) + '\n');
|
|
69
|
+
|
|
70
|
+
pruneBackupDir(stashDir, keep);
|
|
71
|
+
return { status: 'ok', stashPath: dumpFile, bytes: dump.stdout.length };
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Restore the most recent stash into a running construct-postgres container.
|
|
76
|
+
* Safe to call when no stash exists — returns { status: 'no-stash' } silently.
|
|
77
|
+
* The schema must already exist (run migration before calling this).
|
|
78
|
+
*/
|
|
79
|
+
export function restoreConstructDb({
|
|
80
|
+
homeDir = os.homedir(),
|
|
81
|
+
spawnSyncFn = spawnSync,
|
|
82
|
+
} = {}) {
|
|
83
|
+
const stashDir = path.join(homeDir, '.construct', 'backups', 'postgres');
|
|
84
|
+
if (!fs.existsSync(stashDir)) return { status: 'no-stash' };
|
|
85
|
+
|
|
86
|
+
const dumps = fs.readdirSync(stashDir)
|
|
87
|
+
.filter((f) => f.startsWith('construct-') && f.endsWith('.dump'))
|
|
88
|
+
.sort()
|
|
89
|
+
.reverse();
|
|
90
|
+
if (dumps.length === 0) return { status: 'no-stash' };
|
|
91
|
+
|
|
92
|
+
const latestDump = path.join(stashDir, dumps[0]);
|
|
93
|
+
|
|
94
|
+
const cp = spawnSyncFn('docker', [
|
|
95
|
+
'cp', latestDump, `${POSTGRES_CONTAINER}:/tmp/construct.dump`,
|
|
96
|
+
], { stdio: 'ignore' });
|
|
97
|
+
if (cp.status !== 0) return { status: 'copy-failed', stashPath: latestDump };
|
|
98
|
+
|
|
99
|
+
const restore = spawnSyncFn('docker', [
|
|
100
|
+
'exec', POSTGRES_CONTAINER,
|
|
101
|
+
'pg_restore', '-U', POSTGRES_USER, '-d', POSTGRES_DB,
|
|
102
|
+
'--data-only', '--disable-triggers', '--no-owner',
|
|
103
|
+
'--if-exists',
|
|
104
|
+
'/tmp/construct.dump',
|
|
105
|
+
], { stdio: 'ignore' });
|
|
106
|
+
|
|
107
|
+
return {
|
|
108
|
+
status: restore.status === 0 ? 'restored' : 'restore-failed',
|
|
109
|
+
stashPath: latestDump,
|
|
110
|
+
exitCode: restore.status,
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Delete all stashed backups under ~/.construct/backups/postgres/.
|
|
116
|
+
* Called by `construct storage reset` and uninstall flows.
|
|
117
|
+
*/
|
|
118
|
+
export function purgeConstructDbStashes({ homeDir = os.homedir() } = {}) {
|
|
119
|
+
const stashDir = path.join(homeDir, '.construct', 'backups', 'postgres');
|
|
120
|
+
if (!fs.existsSync(stashDir)) return { status: 'ok', deletedCount: 0 };
|
|
121
|
+
const files = fs.readdirSync(stashDir).map((f) => path.join(stashDir, f));
|
|
122
|
+
for (const f of files) fs.rmSync(f, { force: true });
|
|
123
|
+
return { status: 'ok', deletedCount: files.length };
|
|
124
|
+
}
|
|
@@ -2,16 +2,15 @@
|
|
|
2
2
|
/**
|
|
3
3
|
* lib/storage/sql-store.mjs — lightweight shared SQL storage facade.
|
|
4
4
|
*/
|
|
5
|
-
import { existsSync } from 'node:fs';
|
|
6
5
|
import { createSqlClient, probeSqlClient } from './backend.mjs';
|
|
6
|
+
import { resolveDatabaseUrl } from '../env-config.mjs';
|
|
7
7
|
|
|
8
8
|
export function hasSqlStore(env = process.env) {
|
|
9
|
-
return Boolean(env
|
|
9
|
+
return Boolean(resolveDatabaseUrl(env));
|
|
10
10
|
}
|
|
11
11
|
|
|
12
12
|
export function detectStoreMode(env = process.env) {
|
|
13
|
-
if (env
|
|
14
|
-
if (env.CONSTRUCT_SQLITE_PATH || env.CONSTRUCT_DB_PATH) return 'sqlite';
|
|
13
|
+
if (resolveDatabaseUrl(env)) return 'postgres';
|
|
15
14
|
return 'file';
|
|
16
15
|
}
|
|
17
16
|
|
|
@@ -21,9 +20,8 @@ export function describeSqlStore(env = process.env) {
|
|
|
21
20
|
mode,
|
|
22
21
|
configured: mode !== 'file',
|
|
23
22
|
sharedReady: mode === 'postgres',
|
|
24
|
-
fallbackAvailable: mode === 'file'
|
|
25
|
-
|
|
26
|
-
hasDatabaseUrl: Boolean(env.DATABASE_URL),
|
|
23
|
+
fallbackAvailable: mode === 'file',
|
|
24
|
+
hasDatabaseUrl: Boolean(resolveDatabaseUrl(env)),
|
|
27
25
|
};
|
|
28
26
|
}
|
|
29
27
|
|
|
@@ -43,13 +41,5 @@ export function sqlStoreHealth(env = process.env) {
|
|
|
43
41
|
if (store.mode === 'postgres') {
|
|
44
42
|
return { status: 'configured', message: 'Shared Postgres store is configured' };
|
|
45
43
|
}
|
|
46
|
-
if (store.mode === 'sqlite') {
|
|
47
|
-
return { status: 'configured', message: 'SQLite-backed store is configured' };
|
|
48
|
-
}
|
|
49
44
|
return { status: 'unavailable', message: 'No SQL store configured; using file-state only' };
|
|
50
45
|
}
|
|
51
|
-
|
|
52
|
-
export function canOpenLocalSqlite(env = process.env) {
|
|
53
|
-
const candidate = env.CONSTRUCT_SQLITE_PATH || env.CONSTRUCT_DB_PATH || null;
|
|
54
|
-
return candidate ? existsSync(candidate) : false;
|
|
55
|
-
}
|
|
@@ -2,8 +2,9 @@
|
|
|
2
2
|
/**
|
|
3
3
|
* lib/storage/state-source.mjs — normalize file-state artifacts for hybrid retrieval.
|
|
4
4
|
*/
|
|
5
|
-
import { existsSync, readdirSync,
|
|
5
|
+
import { existsSync, readdirSync, statSync, readFileSync } from 'node:fs';
|
|
6
6
|
import { basename, extname, join, relative } from 'node:path';
|
|
7
|
+
import { EXTRACTABLE_DOCUMENT_EXTS, extractDocumentText } from '../document-extract.mjs';
|
|
7
8
|
|
|
8
9
|
function readJsonIfExists(filePath) {
|
|
9
10
|
if (!existsSync(filePath)) return null;
|
|
@@ -27,7 +28,7 @@ function titleFromPath(filePath) {
|
|
|
27
28
|
return basename(filePath, extname(filePath)).replace(/[-_]+/g, ' ');
|
|
28
29
|
}
|
|
29
30
|
|
|
30
|
-
function collectTextFiles(rootDir, dirs, { extensions =
|
|
31
|
+
function collectTextFiles(rootDir, dirs, { extensions = EXTRACTABLE_DOCUMENT_EXTS } = {}) {
|
|
31
32
|
const files = [];
|
|
32
33
|
for (const dir of dirs) {
|
|
33
34
|
const abs = join(rootDir, dir);
|
|
@@ -41,11 +42,12 @@ function collectTextFiles(rootDir, dirs, { extensions = new Set(['.md', '.json']
|
|
|
41
42
|
stack.push(full);
|
|
42
43
|
continue;
|
|
43
44
|
}
|
|
44
|
-
|
|
45
|
+
const extension = extname(entry.name).toLowerCase();
|
|
46
|
+
if (!entry.isFile() || !extensions.has(extension)) continue;
|
|
45
47
|
try {
|
|
46
48
|
const stat = statSync(full);
|
|
47
|
-
if (stat.size >
|
|
48
|
-
const body =
|
|
49
|
+
if (stat.size > 5_000_000) continue;
|
|
50
|
+
const body = extractDocumentText(full, { maxChars: 200_000 }).text;
|
|
49
51
|
files.push({
|
|
50
52
|
path: relative(rootDir, full),
|
|
51
53
|
title: titleFromPath(full),
|
|
@@ -62,24 +64,24 @@ function collectTextFiles(rootDir, dirs, { extensions = new Set(['.md', '.json']
|
|
|
62
64
|
|
|
63
65
|
export function loadStateSnapshot(rootDir) {
|
|
64
66
|
const contextJsonPath = join(rootDir, '.cx', 'context.json');
|
|
65
|
-
const
|
|
67
|
+
const planPath = join(rootDir, 'plan.md');
|
|
66
68
|
const architecturePath = join(rootDir, 'docs', 'architecture.md');
|
|
67
69
|
const docsReadmePath = join(rootDir, 'docs', 'README.md');
|
|
68
70
|
const productIntelDocs = collectTextFiles(rootDir, [
|
|
69
|
-
'.cx/
|
|
71
|
+
'.cx/knowledge',
|
|
70
72
|
'docs/prd',
|
|
71
73
|
'docs/meta-prd',
|
|
72
74
|
]);
|
|
73
75
|
|
|
74
76
|
return {
|
|
75
77
|
context: readJsonIfExists(contextJsonPath),
|
|
76
|
-
|
|
78
|
+
plan: readTextIfExists(planPath),
|
|
77
79
|
architecture: readTextIfExists(architecturePath),
|
|
78
80
|
docsReadme: readTextIfExists(docsReadmePath),
|
|
79
81
|
productIntelDocs,
|
|
80
82
|
paths: {
|
|
81
83
|
contextJsonPath,
|
|
82
|
-
|
|
84
|
+
planPath,
|
|
83
85
|
architecturePath,
|
|
84
86
|
docsReadmePath,
|
|
85
87
|
},
|
|
@@ -89,11 +91,8 @@ export function loadStateSnapshot(rootDir) {
|
|
|
89
91
|
export function summarizeStateSnapshot(snapshot) {
|
|
90
92
|
return {
|
|
91
93
|
contextSummary: snapshot.context?.contextSummary ?? snapshot.context?.summary ?? null,
|
|
92
|
-
|
|
93
|
-
workflowPhase: snapshot.workflow?.phase ?? null,
|
|
94
|
-
workflowStatus: snapshot.workflow?.status ?? null,
|
|
94
|
+
hasPlan: Boolean(snapshot.plan),
|
|
95
95
|
lastContextSavedAt: snapshot.context?.savedAt ?? null,
|
|
96
|
-
lastWorkflowUpdatedAt: snapshot.workflow?.updatedAt ?? null,
|
|
97
96
|
hasArchitectureDoc: Boolean(snapshot.architecture),
|
|
98
97
|
hasDocsReadme: Boolean(snapshot.docsReadme),
|
|
99
98
|
productIntelDocCount: snapshot.productIntelDocs?.length ?? 0,
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* lib/storage/store-version.mjs — versioned schema for the JSON stores.
|
|
3
|
+
*
|
|
4
|
+
* The observation, entity, and session stores write JSON arrays (or objects)
|
|
5
|
+
* to disk. Until now, the on-disk format had no version stamp, which meant
|
|
6
|
+
* any future schema change had to either fork the file or risk silently
|
|
7
|
+
* breaking older data. This module provides a thin, opt-in versioning
|
|
8
|
+
* layer:
|
|
9
|
+
*
|
|
10
|
+
* readVersioned(filePath, storeId)
|
|
11
|
+
* Returns the on-disk records, lazily upgrading them through the
|
|
12
|
+
* store's migration chain if the embedded version is older than the
|
|
13
|
+
* current one. Existing unversioned files (plain arrays) are treated
|
|
14
|
+
* as version 1 and stamped on the next write.
|
|
15
|
+
*
|
|
16
|
+
* writeVersioned(filePath, storeId, records)
|
|
17
|
+
* Wraps the records in `{ schemaVersion, storeId, records }` and
|
|
18
|
+
* writes atomically. Callers always read/write through this helper
|
|
19
|
+
* so the on-disk format stays canonical.
|
|
20
|
+
*
|
|
21
|
+
* registerMigration(storeId, fromVersion, toVersion, migrate)
|
|
22
|
+
* Record a one-step migration. The runtime walks migrations in
|
|
23
|
+
* order until the records reach the current version.
|
|
24
|
+
*
|
|
25
|
+
* Migrations are pure functions over records; they cannot reach the network,
|
|
26
|
+
* fork processes, or call other stores. That keeps lazy upgrade safe to run
|
|
27
|
+
* inside a hot Read path.
|
|
28
|
+
*/
|
|
29
|
+
|
|
30
|
+
import { readFileSync, writeFileSync, existsSync, mkdirSync } from 'node:fs';
|
|
31
|
+
import { dirname } from 'node:path';
|
|
32
|
+
|
|
33
|
+
const CURRENT_VERSIONS = {
|
|
34
|
+
observations: 1,
|
|
35
|
+
observations_vectors: 1,
|
|
36
|
+
observations_index: 1,
|
|
37
|
+
entities: 1,
|
|
38
|
+
entity_vectors: 1,
|
|
39
|
+
sessions: 1,
|
|
40
|
+
session_vectors: 1,
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
const MIGRATIONS = new Map();
|
|
44
|
+
|
|
45
|
+
export function registerMigration(storeId, fromVersion, migrate) {
|
|
46
|
+
const key = `${storeId}@${fromVersion}`;
|
|
47
|
+
if (MIGRATIONS.has(key)) {
|
|
48
|
+
throw new Error(`Migration already registered: ${key}`);
|
|
49
|
+
}
|
|
50
|
+
MIGRATIONS.set(key, migrate);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export function currentVersion(storeId) {
|
|
54
|
+
return CURRENT_VERSIONS[storeId] ?? 1;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function unwrap(parsed) {
|
|
58
|
+
if (parsed && typeof parsed === 'object' && Array.isArray(parsed.records)) {
|
|
59
|
+
return {
|
|
60
|
+
schemaVersion: Number.isInteger(parsed.schemaVersion) ? parsed.schemaVersion : 1,
|
|
61
|
+
records: parsed.records,
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
if (Array.isArray(parsed)) {
|
|
65
|
+
return { schemaVersion: 1, records: parsed };
|
|
66
|
+
}
|
|
67
|
+
return { schemaVersion: 1, records: [] };
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function migrateOnce(storeId, schemaVersion, records) {
|
|
71
|
+
const migrate = MIGRATIONS.get(`${storeId}@${schemaVersion}`);
|
|
72
|
+
if (!migrate) return null;
|
|
73
|
+
const next = migrate(records);
|
|
74
|
+
if (!Array.isArray(next)) {
|
|
75
|
+
throw new Error(`Migration ${storeId}@${schemaVersion}→ must return an array of records`);
|
|
76
|
+
}
|
|
77
|
+
return next;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Read a versioned JSON store. Lazily applies migrations to bring records up
|
|
82
|
+
* to the current version. Returns plain records (caller doesn't see the
|
|
83
|
+
* version envelope).
|
|
84
|
+
*/
|
|
85
|
+
export function readVersioned(filePath, storeId, fallback = []) {
|
|
86
|
+
if (!existsSync(filePath)) return fallback;
|
|
87
|
+
let parsed;
|
|
88
|
+
try { parsed = JSON.parse(readFileSync(filePath, 'utf8')); }
|
|
89
|
+
catch { return fallback; }
|
|
90
|
+
|
|
91
|
+
let { schemaVersion, records } = unwrap(parsed);
|
|
92
|
+
const target = currentVersion(storeId);
|
|
93
|
+
|
|
94
|
+
while (schemaVersion < target) {
|
|
95
|
+
const next = migrateOnce(storeId, schemaVersion, records);
|
|
96
|
+
if (next === null) break;
|
|
97
|
+
records = next;
|
|
98
|
+
schemaVersion += 1;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
return records;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Write a versioned JSON store. Always stamps the current version.
|
|
106
|
+
*/
|
|
107
|
+
export function writeVersioned(filePath, storeId, records) {
|
|
108
|
+
mkdirSync(dirname(filePath), { recursive: true });
|
|
109
|
+
const payload = {
|
|
110
|
+
schemaVersion: currentVersion(storeId),
|
|
111
|
+
storeId,
|
|
112
|
+
records: Array.isArray(records) ? records : [],
|
|
113
|
+
};
|
|
114
|
+
writeFileSync(filePath, JSON.stringify(payload, null, 2) + '\n');
|
|
115
|
+
}
|