@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,287 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* lib/engine/consolidate.mjs — sleep-time consolidation for the observation store.
|
|
3
|
+
*
|
|
4
|
+
* Periodically clusters near-duplicate observations by cosine similarity and
|
|
5
|
+
* merges each cluster into a single consolidated insight that records the
|
|
6
|
+
* representative, hit count, member ids, and last-seen timestamp. Optionally
|
|
7
|
+
* summarises clusters via a Compressor plugin (or via any LM the operator
|
|
8
|
+
* wires in) — when no summariser is provided, the representative observation's
|
|
9
|
+
* own summary is kept verbatim.
|
|
10
|
+
*
|
|
11
|
+
* Outputs:
|
|
12
|
+
* .cx/observations/consolidated.json list of insight clusters
|
|
13
|
+
* .cx/observations/archive/<id>.json observations demoted to cold archive
|
|
14
|
+
*
|
|
15
|
+
* The pass is idempotent: re-running it on a stable corpus produces the same
|
|
16
|
+
* cluster set. It is safe to schedule as a cron / launchd job, run in the
|
|
17
|
+
* embed daemon, or invoke ad-hoc as `construct memory consolidate`.
|
|
18
|
+
*
|
|
19
|
+
* Plugin entry points:
|
|
20
|
+
* `summariser` - any object with `compress(text, opts)` (Compressor contract)
|
|
21
|
+
* that generates a one-line cluster summary from member text.
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
import fs from 'node:fs';
|
|
25
|
+
import path from 'node:path';
|
|
26
|
+
import { cosineSimilarity } from '../storage/embeddings.mjs';
|
|
27
|
+
|
|
28
|
+
const OBS_DIR = '.cx/observations';
|
|
29
|
+
const ARCHIVE_DIR = '.cx/observations/archive';
|
|
30
|
+
const CONSOLIDATED_FILE = 'consolidated.json';
|
|
31
|
+
const VECTORS_FILE = 'vectors.json';
|
|
32
|
+
const INDEX_FILE = 'index.json';
|
|
33
|
+
|
|
34
|
+
const DEFAULTS = {
|
|
35
|
+
similarityThreshold: 0.95,
|
|
36
|
+
archiveAfterDays: 60,
|
|
37
|
+
archiveBelowConfidence: 0.5,
|
|
38
|
+
maxStored: 5000,
|
|
39
|
+
// Archive retention: keep at most this many archived files, and delete
|
|
40
|
+
// anything older than archiveRetainDays. Both bounds apply.
|
|
41
|
+
archiveRetainDays: 365,
|
|
42
|
+
archiveMaxFiles: 1000,
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
function readJsonOrEmpty(filePath, fallback) {
|
|
46
|
+
try {
|
|
47
|
+
return JSON.parse(fs.readFileSync(filePath, 'utf8'));
|
|
48
|
+
} catch {
|
|
49
|
+
return fallback;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function readObservation(rootDir, id) {
|
|
54
|
+
const filePath = path.join(rootDir, OBS_DIR, `${id}.json`);
|
|
55
|
+
if (!fs.existsSync(filePath)) return null;
|
|
56
|
+
return readJsonOrEmpty(filePath, null);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function archiveObservation(rootDir, id) {
|
|
60
|
+
const src = path.join(rootDir, OBS_DIR, `${id}.json`);
|
|
61
|
+
if (!fs.existsSync(src)) return false;
|
|
62
|
+
const archiveDir = path.join(rootDir, ARCHIVE_DIR);
|
|
63
|
+
fs.mkdirSync(archiveDir, { recursive: true });
|
|
64
|
+
const dest = path.join(archiveDir, `${id}.json`);
|
|
65
|
+
fs.renameSync(src, dest);
|
|
66
|
+
return true;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function daysAgo(dateStr) {
|
|
70
|
+
if (!dateStr) return Infinity;
|
|
71
|
+
return (Date.now() - new Date(dateStr).getTime()) / (24 * 60 * 60 * 1000);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Greedy single-pass clustering. Each observation is added to the existing
|
|
76
|
+
* cluster whose centroid has highest cosine similarity, provided that
|
|
77
|
+
* similarity exceeds `threshold`. Otherwise a new cluster is opened. The
|
|
78
|
+
* centroid is recomputed as a running mean as members join.
|
|
79
|
+
*
|
|
80
|
+
* Stable across runs because the input is sorted by id before clustering.
|
|
81
|
+
*/
|
|
82
|
+
function greedyCluster(records, threshold) {
|
|
83
|
+
const sorted = [...records].sort((a, b) => String(a.id).localeCompare(String(b.id)));
|
|
84
|
+
const clusters = [];
|
|
85
|
+
|
|
86
|
+
for (const record of sorted) {
|
|
87
|
+
if (!record.embedding || record.embedding.length === 0) {
|
|
88
|
+
clusters.push({
|
|
89
|
+
centroid: [],
|
|
90
|
+
members: [record],
|
|
91
|
+
});
|
|
92
|
+
continue;
|
|
93
|
+
}
|
|
94
|
+
let bestIdx = -1;
|
|
95
|
+
let bestSim = threshold;
|
|
96
|
+
for (let i = 0; i < clusters.length; i++) {
|
|
97
|
+
const cluster = clusters[i];
|
|
98
|
+
if (cluster.centroid.length !== record.embedding.length) continue;
|
|
99
|
+
const sim = cosineSimilarity(record.embedding, cluster.centroid);
|
|
100
|
+
if (sim > bestSim) {
|
|
101
|
+
bestSim = sim;
|
|
102
|
+
bestIdx = i;
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
if (bestIdx >= 0) {
|
|
106
|
+
const cluster = clusters[bestIdx];
|
|
107
|
+
const n = cluster.members.length;
|
|
108
|
+
const next = new Array(record.embedding.length);
|
|
109
|
+
for (let j = 0; j < record.embedding.length; j++) {
|
|
110
|
+
next[j] = (cluster.centroid[j] * n + record.embedding[j]) / (n + 1);
|
|
111
|
+
}
|
|
112
|
+
cluster.centroid = next;
|
|
113
|
+
cluster.members.push(record);
|
|
114
|
+
} else {
|
|
115
|
+
clusters.push({
|
|
116
|
+
centroid: [...record.embedding],
|
|
117
|
+
members: [record],
|
|
118
|
+
});
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
return clusters;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
async function buildSummary(cluster, summariser) {
|
|
126
|
+
const representative = cluster.members[0];
|
|
127
|
+
const baseText = representative?.summary || representative?.content || '';
|
|
128
|
+
if (!summariser || typeof summariser.compress !== 'function') return baseText;
|
|
129
|
+
try {
|
|
130
|
+
const merged = cluster.members
|
|
131
|
+
.map((m) => m?.summary || m?.content || '')
|
|
132
|
+
.filter(Boolean)
|
|
133
|
+
.join(' ');
|
|
134
|
+
return await summariser.compress(merged, { ratio: 0.4, maxTokens: 60 });
|
|
135
|
+
} catch {
|
|
136
|
+
return baseText;
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* Run a consolidation pass against a project root.
|
|
142
|
+
*
|
|
143
|
+
* @param {string} rootDir
|
|
144
|
+
* @param {object} [opts]
|
|
145
|
+
* @param {number} [opts.similarityThreshold=0.95]
|
|
146
|
+
* @param {number} [opts.archiveAfterDays=60]
|
|
147
|
+
* @param {number} [opts.archiveBelowConfidence=0.5]
|
|
148
|
+
* @param {number} [opts.maxStored=5000]
|
|
149
|
+
* @param {object} [opts.summariser] - Compressor-shaped plugin
|
|
150
|
+
* @returns {Promise<{ clustersBefore: number, clusters: number, archived: string[], stored: number }>}
|
|
151
|
+
*/
|
|
152
|
+
export async function consolidate(rootDir, opts = {}) {
|
|
153
|
+
const settings = { ...DEFAULTS, ...opts };
|
|
154
|
+
const obsRoot = path.join(rootDir, OBS_DIR);
|
|
155
|
+
if (!fs.existsSync(obsRoot)) {
|
|
156
|
+
return { clustersBefore: 0, clusters: 0, archived: [], stored: 0 };
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
const vectorsPath = path.join(obsRoot, VECTORS_FILE);
|
|
160
|
+
const vectors = readJsonOrEmpty(vectorsPath, []);
|
|
161
|
+
const records = vectors.map((v) => {
|
|
162
|
+
const full = readObservation(rootDir, v.id);
|
|
163
|
+
if (!full) return null;
|
|
164
|
+
return {
|
|
165
|
+
id: v.id,
|
|
166
|
+
embedding: v.embedding || [],
|
|
167
|
+
summary: full.summary || '',
|
|
168
|
+
content: full.content || '',
|
|
169
|
+
tags: full.tags || [],
|
|
170
|
+
role: full.role || null,
|
|
171
|
+
category: full.category || null,
|
|
172
|
+
confidence: typeof full.confidence === 'number' ? full.confidence : 0.8,
|
|
173
|
+
createdAt: full.createdAt || null,
|
|
174
|
+
};
|
|
175
|
+
}).filter(Boolean);
|
|
176
|
+
|
|
177
|
+
const clustersBefore = records.length;
|
|
178
|
+
|
|
179
|
+
const archived = [];
|
|
180
|
+
const liveRecords = records.filter((r) => {
|
|
181
|
+
const old = daysAgo(r.createdAt) > settings.archiveAfterDays;
|
|
182
|
+
const lowConfidence = r.confidence < settings.archiveBelowConfidence;
|
|
183
|
+
if (old && lowConfidence) {
|
|
184
|
+
if (archiveObservation(rootDir, r.id)) archived.push(r.id);
|
|
185
|
+
return false;
|
|
186
|
+
}
|
|
187
|
+
return true;
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
const clusters = greedyCluster(liveRecords, settings.similarityThreshold);
|
|
191
|
+
|
|
192
|
+
const consolidated = [];
|
|
193
|
+
for (const cluster of clusters) {
|
|
194
|
+
const representative = cluster.members[0];
|
|
195
|
+
const summary = await buildSummary(cluster, settings.summariser);
|
|
196
|
+
const lastSeen = cluster.members.reduce((acc, m) => {
|
|
197
|
+
if (!m.createdAt) return acc;
|
|
198
|
+
return acc && new Date(acc) > new Date(m.createdAt) ? acc : m.createdAt;
|
|
199
|
+
}, null);
|
|
200
|
+
consolidated.push({
|
|
201
|
+
id: `consolidated:${representative.id}`,
|
|
202
|
+
representativeId: representative.id,
|
|
203
|
+
hitCount: cluster.members.length,
|
|
204
|
+
summary,
|
|
205
|
+
role: representative.role,
|
|
206
|
+
category: representative.category,
|
|
207
|
+
tags: [...new Set(cluster.members.flatMap((m) => m.tags || []))].slice(0, 10),
|
|
208
|
+
memberIds: cluster.members.map((m) => m.id),
|
|
209
|
+
avgConfidence:
|
|
210
|
+
cluster.members.reduce((acc, m) => acc + (m.confidence || 0), 0) /
|
|
211
|
+
Math.max(cluster.members.length, 1),
|
|
212
|
+
lastSeen,
|
|
213
|
+
});
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
consolidated.sort((a, b) => (b.hitCount - a.hitCount) || (a.id < b.id ? -1 : 1));
|
|
217
|
+
|
|
218
|
+
if (consolidated.length > settings.maxStored) {
|
|
219
|
+
const dropped = consolidated.slice(settings.maxStored);
|
|
220
|
+
for (const insight of dropped) {
|
|
221
|
+
for (const memberId of insight.memberIds) {
|
|
222
|
+
if (archiveObservation(rootDir, memberId)) archived.push(memberId);
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
consolidated.length = settings.maxStored;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
fs.mkdirSync(obsRoot, { recursive: true });
|
|
229
|
+
fs.writeFileSync(
|
|
230
|
+
path.join(obsRoot, CONSOLIDATED_FILE),
|
|
231
|
+
JSON.stringify(consolidated, null, 2) + '\n'
|
|
232
|
+
);
|
|
233
|
+
|
|
234
|
+
if (archived.length > 0) {
|
|
235
|
+
const indexPath = path.join(obsRoot, INDEX_FILE);
|
|
236
|
+
const index = readJsonOrEmpty(indexPath, []);
|
|
237
|
+
const archivedSet = new Set(archived);
|
|
238
|
+
const filtered = index.filter((entry) => !archivedSet.has(entry.id));
|
|
239
|
+
if (filtered.length !== index.length) {
|
|
240
|
+
fs.writeFileSync(indexPath, JSON.stringify(filtered, null, 2) + '\n');
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
const archivePruned = pruneArchive(rootDir, settings);
|
|
245
|
+
|
|
246
|
+
return {
|
|
247
|
+
clustersBefore,
|
|
248
|
+
clusters: consolidated.length,
|
|
249
|
+
archived,
|
|
250
|
+
archivePruned,
|
|
251
|
+
stored: consolidated.length,
|
|
252
|
+
};
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
// Prune the cold archive directory: delete files older than archiveRetainDays,
|
|
256
|
+
// then keep at most archiveMaxFiles by mtime descending. Returns number deleted.
|
|
257
|
+
export function pruneArchive(rootDir, opts = {}) {
|
|
258
|
+
const settings = { ...DEFAULTS, ...opts };
|
|
259
|
+
const archiveDir = path.join(rootDir, ARCHIVE_DIR);
|
|
260
|
+
if (!fs.existsSync(archiveDir)) return 0;
|
|
261
|
+
|
|
262
|
+
const cutoffMs = Date.now() - settings.archiveRetainDays * 24 * 60 * 60 * 1000;
|
|
263
|
+
const files = fs.readdirSync(archiveDir)
|
|
264
|
+
.filter((f) => f.endsWith('.json'))
|
|
265
|
+
.map((f) => {
|
|
266
|
+
const full = path.join(archiveDir, f);
|
|
267
|
+
const stat = fs.statSync(full);
|
|
268
|
+
return { name: f, path: full, mtime: stat.mtimeMs };
|
|
269
|
+
})
|
|
270
|
+
.sort((a, b) => b.mtime - a.mtime);
|
|
271
|
+
|
|
272
|
+
let deleted = 0;
|
|
273
|
+
const kept = [];
|
|
274
|
+
for (const file of files) {
|
|
275
|
+
if (file.mtime < cutoffMs) {
|
|
276
|
+
try { fs.unlinkSync(file.path); deleted += 1; } catch { /* best effort */ }
|
|
277
|
+
} else {
|
|
278
|
+
kept.push(file);
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
if (kept.length > settings.archiveMaxFiles) {
|
|
282
|
+
for (const file of kept.slice(settings.archiveMaxFiles)) {
|
|
283
|
+
try { fs.unlinkSync(file.path); deleted += 1; } catch { /* best effort */ }
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
return deleted;
|
|
287
|
+
}
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* lib/engine/contracts.mjs — Six stable plugin contracts for the Construct retrieval engine.
|
|
3
|
+
*
|
|
4
|
+
* Each layer of the recall stack is defined here as a runtime-checkable contract:
|
|
5
|
+
* Embedder, Chunker, Indexer, Fuser, Reranker, Compressor.
|
|
6
|
+
*
|
|
7
|
+
* External git projects can satisfy any contract by exporting a factory that returns
|
|
8
|
+
* an object with the listed methods and a `meta` object. The resolver
|
|
9
|
+
* (lib/engine/registry.mjs) validates against `assertContract(layer, plugin)` before
|
|
10
|
+
* accepting a plugin; failures fall back to the default impl and are logged via
|
|
11
|
+
* lib/hooks/_lib/log.mjs.
|
|
12
|
+
*
|
|
13
|
+
* Contracts are intentionally narrow. They describe what the engine needs, nothing
|
|
14
|
+
* about how a plugin implements it. Capabilities are declared on `plugin.meta` so
|
|
15
|
+
* callers can reason about batch support, async cost, dimensions, max tokens, etc.
|
|
16
|
+
*
|
|
17
|
+
* Layer dependencies:
|
|
18
|
+
* Embedder is required by Indexer (for query embedding) and Reranker.
|
|
19
|
+
* Indexer is required at retrieve time.
|
|
20
|
+
* Fuser combines Indexer outputs.
|
|
21
|
+
* Reranker reorders Fuser output.
|
|
22
|
+
* Compressor is applied independently to text blocks (chunks, prompts, injections).
|
|
23
|
+
* Chunker is upstream of Embedder at index-build time only.
|
|
24
|
+
*/
|
|
25
|
+
|
|
26
|
+
export const LAYERS = Object.freeze([
|
|
27
|
+
'embedder',
|
|
28
|
+
'chunker',
|
|
29
|
+
'indexer',
|
|
30
|
+
'fuser',
|
|
31
|
+
'reranker',
|
|
32
|
+
'compressor',
|
|
33
|
+
]);
|
|
34
|
+
|
|
35
|
+
// Contract definitions. Each entry lists the required `meta` fields and method
|
|
36
|
+
// signatures the plugin must expose. Method bodies may be sync OR async — the
|
|
37
|
+
// engine awaits all results, so plugins are free to be either.
|
|
38
|
+
|
|
39
|
+
const CONTRACTS = {
|
|
40
|
+
embedder: {
|
|
41
|
+
meta: ['id', 'modelId', 'dimensions'],
|
|
42
|
+
methods: ['embed', 'embedBatch'],
|
|
43
|
+
},
|
|
44
|
+
chunker: {
|
|
45
|
+
meta: ['id'],
|
|
46
|
+
methods: ['chunk'],
|
|
47
|
+
},
|
|
48
|
+
indexer: {
|
|
49
|
+
meta: ['id'],
|
|
50
|
+
methods: ['store', 'query', 'health'],
|
|
51
|
+
},
|
|
52
|
+
fuser: {
|
|
53
|
+
meta: ['id'],
|
|
54
|
+
methods: ['fuse'],
|
|
55
|
+
},
|
|
56
|
+
reranker: {
|
|
57
|
+
meta: ['id'],
|
|
58
|
+
methods: ['rerank'],
|
|
59
|
+
},
|
|
60
|
+
compressor: {
|
|
61
|
+
meta: ['id'],
|
|
62
|
+
methods: ['compress'],
|
|
63
|
+
},
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Throws if `plugin` does not satisfy the contract for `layer`.
|
|
68
|
+
*
|
|
69
|
+
* @param {string} layer — one of LAYERS
|
|
70
|
+
* @param {object} plugin — plugin instance returned from a factory
|
|
71
|
+
*/
|
|
72
|
+
export function assertContract(layer, plugin) {
|
|
73
|
+
if (!LAYERS.includes(layer)) {
|
|
74
|
+
throw new Error(`Unknown plugin layer: ${layer}`);
|
|
75
|
+
}
|
|
76
|
+
if (!plugin || typeof plugin !== 'object') {
|
|
77
|
+
throw new Error(`${layer} plugin: must be a non-null object`);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
const spec = CONTRACTS[layer];
|
|
81
|
+
const meta = plugin.meta;
|
|
82
|
+
if (!meta || typeof meta !== 'object') {
|
|
83
|
+
throw new Error(`${layer} plugin: missing required 'meta' object`);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
for (const key of spec.meta) {
|
|
87
|
+
if (meta[key] === undefined || meta[key] === null || meta[key] === '') {
|
|
88
|
+
throw new Error(`${layer} plugin: meta.${key} is required`);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
for (const fn of spec.methods) {
|
|
93
|
+
if (typeof plugin[fn] !== 'function') {
|
|
94
|
+
throw new Error(`${layer} plugin (${meta.id}): missing method '${fn}'`);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// Embedder must declare positive integer dimensions so downstream Indexer can
|
|
99
|
+
// assert schema compatibility before the first store call.
|
|
100
|
+
if (layer === 'embedder') {
|
|
101
|
+
const dim = meta.dimensions;
|
|
102
|
+
if (!Number.isInteger(dim) || dim <= 0) {
|
|
103
|
+
throw new Error(`embedder plugin (${meta.id}): meta.dimensions must be a positive integer`);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Lightweight, dependency-free contract test for use in `construct doctor` and
|
|
110
|
+
* plugin author CI. Returns `{ ok, errors }` instead of throwing so a single run
|
|
111
|
+
* can report on every plugin.
|
|
112
|
+
*
|
|
113
|
+
* @param {string} layer
|
|
114
|
+
* @param {object} plugin
|
|
115
|
+
* @returns {{ ok: boolean, errors: string[] }}
|
|
116
|
+
*/
|
|
117
|
+
export function checkContract(layer, plugin) {
|
|
118
|
+
try {
|
|
119
|
+
assertContract(layer, plugin);
|
|
120
|
+
return { ok: true, errors: [] };
|
|
121
|
+
} catch (err) {
|
|
122
|
+
return { ok: false, errors: [err.message] };
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
export const CONTRACT_DEFINITIONS = CONTRACTS;
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* lib/engine/defaults.mjs — Built-in default plugin implementations.
|
|
3
|
+
*
|
|
4
|
+
* Each export is a factory that returns a contract-satisfying plugin instance.
|
|
5
|
+
* Defaults are deliberately the minimum needed to keep the contract live; the
|
|
6
|
+
* stronger implementations live in their own modules and are wired in below.
|
|
7
|
+
*
|
|
8
|
+
* Embedder default is `local-onnx` (Xenova/all-MiniLM-L6-v2, 384d). The default
|
|
9
|
+
* wraps `lib/storage/embeddings-engine.mjs` under the Embedder contract so
|
|
10
|
+
* callers go through `engine.layers.embedder` exclusively.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
// The Embedder default routes through the model-agnostic engine so the
|
|
14
|
+
// configured model (local | openai | ollama | hashing) drives dimensionality
|
|
15
|
+
// and behavior end-to-end through one call site.
|
|
16
|
+
|
|
17
|
+
import {
|
|
18
|
+
embedText as embedTextEngine,
|
|
19
|
+
embedBatch as embedBatchEngine,
|
|
20
|
+
getEmbeddingModelInfo,
|
|
21
|
+
} from '../storage/embeddings-engine.mjs';
|
|
22
|
+
|
|
23
|
+
// ── Embedder default ──────────────────────────────────────────────────────
|
|
24
|
+
|
|
25
|
+
export function createDefaultEmbedder() {
|
|
26
|
+
return {
|
|
27
|
+
meta: {
|
|
28
|
+
id: 'construct-default-embedder',
|
|
29
|
+
modelId: 'pending-init',
|
|
30
|
+
dimensions: 1,
|
|
31
|
+
},
|
|
32
|
+
async init() {
|
|
33
|
+
const info = await getEmbeddingModelInfo();
|
|
34
|
+
this.meta.modelId = info.model;
|
|
35
|
+
this.meta.dimensions = info.dimensions;
|
|
36
|
+
return this;
|
|
37
|
+
},
|
|
38
|
+
async embed(text) {
|
|
39
|
+
const r = await embedTextEngine(text);
|
|
40
|
+
return r.embedding;
|
|
41
|
+
},
|
|
42
|
+
async embedBatch(texts) {
|
|
43
|
+
const results = await embedBatchEngine(texts);
|
|
44
|
+
return results.map((r) => r.embedding);
|
|
45
|
+
},
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// ── Chunker default ───────────────────────────────────────────────────────
|
|
50
|
+
|
|
51
|
+
// Heading-prefix chunker: splits markdown at heading boundaries and
|
|
52
|
+
// prepends each chunk with its heading chain so retrieved chunks carry
|
|
53
|
+
// doc-relative context. Operators that want LM-generated per-chunk context
|
|
54
|
+
// (the strongest known retrieval technique) plug it in via the Chunker slot.
|
|
55
|
+
|
|
56
|
+
import { create as createHeadingsChunker } from './chunker-headings.mjs';
|
|
57
|
+
|
|
58
|
+
export function createDefaultChunker() {
|
|
59
|
+
return createHeadingsChunker();
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// ── Indexer default ───────────────────────────────────────────────────────
|
|
63
|
+
|
|
64
|
+
// Storage is delegated to `lib/storage/vector-client.mjs` and
|
|
65
|
+
// `lib/storage/sync.mjs`. The default indexer here is a thin contract
|
|
66
|
+
// surface; richer indexer plugins (e.g. external vector DBs) can replace it.
|
|
67
|
+
|
|
68
|
+
export function createDefaultIndexer() {
|
|
69
|
+
return {
|
|
70
|
+
meta: { id: 'construct-default-indexer', mode: 'pgvector-or-file' },
|
|
71
|
+
async store(/* chunks, embeddings */) {
|
|
72
|
+
return { stored: 0, mode: 'deferred', note: 'store delegated to lib/storage/sync.mjs' };
|
|
73
|
+
},
|
|
74
|
+
async query(/* queryEmbedding, opts */) {
|
|
75
|
+
return [];
|
|
76
|
+
},
|
|
77
|
+
async health() {
|
|
78
|
+
return { ok: true, mode: 'delegated', note: 'health checks live in lib/storage/vector-client.mjs' };
|
|
79
|
+
},
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// ── Fuser default ─────────────────────────────────────────────────────────
|
|
84
|
+
|
|
85
|
+
// Reciprocal Rank Fusion (RRF) — corpus-agnostic, parameter-light, removes
|
|
86
|
+
// the BM25/cosine top-K asymmetry that the previous weighted-sum had.
|
|
87
|
+
|
|
88
|
+
import { create as createRrfFuser } from './fuser-rrf.mjs';
|
|
89
|
+
import { create as createMmrReranker } from './reranker-mmr.mjs';
|
|
90
|
+
|
|
91
|
+
export function createDefaultFuser() {
|
|
92
|
+
return createRrfFuser();
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// ── Reranker default ──────────────────────────────────────────────────────
|
|
96
|
+
|
|
97
|
+
// Maximal Marginal Relevance (MMR) — balances relevance against diversity to
|
|
98
|
+
// drop near-duplicates without a hard threshold. λ=0.7 favors relevance.
|
|
99
|
+
|
|
100
|
+
export function createDefaultReranker() {
|
|
101
|
+
return createMmrReranker();
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// ── Compressor default ────────────────────────────────────────────────────
|
|
105
|
+
|
|
106
|
+
// TF-IDF sentence-selector compressor — zero deps, modest compression that
|
|
107
|
+
// runs on every machine. Operators that want learned compression
|
|
108
|
+
// (LLMLingua-style) plug it in via the Compressor slot.
|
|
109
|
+
|
|
110
|
+
import { create as createHeuristicCompressor } from './compressor-heuristic.mjs';
|
|
111
|
+
|
|
112
|
+
export function createDefaultCompressor() {
|
|
113
|
+
return createHeuristicCompressor();
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// ── Aggregate loader ──────────────────────────────────────────────────────
|
|
117
|
+
|
|
118
|
+
export async function loadDefaults() {
|
|
119
|
+
const embedder = createDefaultEmbedder();
|
|
120
|
+
if (typeof embedder.init === 'function') await embedder.init();
|
|
121
|
+
return {
|
|
122
|
+
embedder,
|
|
123
|
+
chunker: createDefaultChunker(),
|
|
124
|
+
indexer: createDefaultIndexer(),
|
|
125
|
+
fuser: createDefaultFuser(),
|
|
126
|
+
reranker: createDefaultReranker(),
|
|
127
|
+
compressor: createDefaultCompressor(),
|
|
128
|
+
};
|
|
129
|
+
}
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* lib/engine/eval-retrieval.mjs — Retrieval-quality evaluation harness.
|
|
3
|
+
*
|
|
4
|
+
* Runs a fixed query set against a corpus through the engine's retrieval
|
|
5
|
+
* pipeline (BM25 + cosine → RRF fuse → MMR rerank, with whatever plugins
|
|
6
|
+
* are active) and reports standard IR metrics:
|
|
7
|
+
*
|
|
8
|
+
* - Recall@k fraction of expected docs that appear in the top-k
|
|
9
|
+
* - MRR mean reciprocal rank of the first expected doc
|
|
10
|
+
* - NDCG@k discounted cumulative gain over the top-k
|
|
11
|
+
*
|
|
12
|
+
* The harness is corpus-agnostic — callers pass in chunks (each with id,
|
|
13
|
+
* title, body) and a list of queries with expected doc ids. The pipeline
|
|
14
|
+
* itself is the same one `lib/knowledge/rag.mjs::retrieve` uses, so any
|
|
15
|
+
* plugin swap (Fuser, Reranker, etc.) is reflected in the metrics.
|
|
16
|
+
*
|
|
17
|
+
* The CLI surface (`construct evals retrieval`) loads a fixture file
|
|
18
|
+
* shipped at `tests/fixtures/retrieval-eval/queries.json` against an
|
|
19
|
+
* inline mini-corpus, but callers can also provide their own datasets.
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
import { readFileSync } from 'node:fs';
|
|
23
|
+
import { rankByBm25, cosineSimilarity } from '../storage/embeddings.mjs';
|
|
24
|
+
import { embedSync } from '../storage/embeddings-legacy.mjs';
|
|
25
|
+
import { getEngine } from './index.mjs';
|
|
26
|
+
|
|
27
|
+
function buildEmbeddedCorpus(chunks) {
|
|
28
|
+
return chunks.map((c) => ({
|
|
29
|
+
...c,
|
|
30
|
+
embedding: embedSync(`${c.title || ''} ${c.body || ''}`),
|
|
31
|
+
}));
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
async function retrieveOne(query, corpus, engine, topK) {
|
|
35
|
+
const queryEmbedding = embedSync(query);
|
|
36
|
+
const bm25Ranked = rankByBm25(
|
|
37
|
+
corpus.map((c) => ({ ...c, text: `${c.title || ''} ${c.body || ''}` })),
|
|
38
|
+
query,
|
|
39
|
+
{ limit: corpus.length },
|
|
40
|
+
);
|
|
41
|
+
const cosineRanked = corpus
|
|
42
|
+
.map((chunk) => ({
|
|
43
|
+
...chunk,
|
|
44
|
+
score: cosineSimilarity(queryEmbedding, chunk.embedding || []),
|
|
45
|
+
}))
|
|
46
|
+
.filter((c) => c.score > 0)
|
|
47
|
+
.sort((a, b) => b.score - a.score);
|
|
48
|
+
|
|
49
|
+
const fused = engine.layers.fuser.fuse({ bm25: bm25Ranked, cosine: cosineRanked });
|
|
50
|
+
const reranked = await engine.layers.reranker.rerank(query, fused, {
|
|
51
|
+
queryEmbedding,
|
|
52
|
+
topK,
|
|
53
|
+
});
|
|
54
|
+
return reranked;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function recallAt(ranked, expectedIds, k) {
|
|
58
|
+
const top = new Set(ranked.slice(0, k).map((r) => r.id));
|
|
59
|
+
let hits = 0;
|
|
60
|
+
for (const id of expectedIds) if (top.has(id)) hits++;
|
|
61
|
+
return expectedIds.length === 0 ? 0 : hits / expectedIds.length;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function reciprocalRank(ranked, expectedIds) {
|
|
65
|
+
for (let i = 0; i < ranked.length; i++) {
|
|
66
|
+
if (expectedIds.includes(ranked[i].id)) return 1 / (i + 1);
|
|
67
|
+
}
|
|
68
|
+
return 0;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function ndcgAt(ranked, expectedIds, k) {
|
|
72
|
+
const expected = new Set(expectedIds);
|
|
73
|
+
let dcg = 0;
|
|
74
|
+
for (let i = 0; i < Math.min(k, ranked.length); i++) {
|
|
75
|
+
if (expected.has(ranked[i].id)) {
|
|
76
|
+
dcg += 1 / Math.log2(i + 2);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
let idcg = 0;
|
|
80
|
+
for (let i = 0; i < Math.min(k, expectedIds.length); i++) {
|
|
81
|
+
idcg += 1 / Math.log2(i + 2);
|
|
82
|
+
}
|
|
83
|
+
return idcg === 0 ? 0 : dcg / idcg;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Run the evaluation. Returns aggregate metrics + a per-query breakdown.
|
|
88
|
+
*
|
|
89
|
+
* @param {object} args
|
|
90
|
+
* @param {Array<{ id, title, body }>} args.corpus
|
|
91
|
+
* @param {Array<{ query, expected: string[] }>} args.queries
|
|
92
|
+
* @param {object} [args.engine] — pre-resolved engine (for tests)
|
|
93
|
+
* @param {number} [args.topK=5]
|
|
94
|
+
* @returns {Promise<{
|
|
95
|
+
* recallAt1, recallAt5, mrr, ndcgAt5,
|
|
96
|
+
* queries: Array<{ query, expected, ranked: string[], recallAt1, recallAt5, mrr, ndcgAt5 }>,
|
|
97
|
+
* }>}
|
|
98
|
+
*/
|
|
99
|
+
export async function evaluateRetrieval({ corpus, queries, engine, topK = 5 }) {
|
|
100
|
+
const eng = engine || (await getEngine());
|
|
101
|
+
const embedded = buildEmbeddedCorpus(corpus);
|
|
102
|
+
|
|
103
|
+
const results = [];
|
|
104
|
+
for (const { query, expected } of queries) {
|
|
105
|
+
const ranked = await retrieveOne(query, embedded, eng, Math.max(topK, 5));
|
|
106
|
+
results.push({
|
|
107
|
+
query,
|
|
108
|
+
expected,
|
|
109
|
+
ranked: ranked.slice(0, topK).map((r) => r.id),
|
|
110
|
+
recallAt1: recallAt(ranked, expected, 1),
|
|
111
|
+
recallAt5: recallAt(ranked, expected, 5),
|
|
112
|
+
mrr: reciprocalRank(ranked, expected),
|
|
113
|
+
ndcgAt5: ndcgAt(ranked, expected, 5),
|
|
114
|
+
});
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
const mean = (key) => results.reduce((acc, r) => acc + r[key], 0) / Math.max(results.length, 1);
|
|
118
|
+
|
|
119
|
+
return {
|
|
120
|
+
queryCount: results.length,
|
|
121
|
+
corpusSize: corpus.length,
|
|
122
|
+
recallAt1: mean('recallAt1'),
|
|
123
|
+
recallAt5: mean('recallAt5'),
|
|
124
|
+
mrr: mean('mrr'),
|
|
125
|
+
ndcgAt5: mean('ndcgAt5'),
|
|
126
|
+
queries: results,
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Convenience: load a JSON fixture { corpus, queries } from disk and run.
|
|
132
|
+
*/
|
|
133
|
+
export async function evaluateFixture(fixturePath, opts = {}) {
|
|
134
|
+
const data = JSON.parse(readFileSync(fixturePath, 'utf8'));
|
|
135
|
+
return evaluateRetrieval({ ...data, ...opts });
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
export function formatReport(report) {
|
|
139
|
+
const pct = (x) => `${(x * 100).toFixed(1)}%`;
|
|
140
|
+
const lines = [
|
|
141
|
+
`Corpus: ${report.corpusSize} chunks · Queries: ${report.queryCount}`,
|
|
142
|
+
`Recall@1: ${pct(report.recallAt1)}`,
|
|
143
|
+
`Recall@5: ${pct(report.recallAt5)}`,
|
|
144
|
+
`MRR: ${report.mrr.toFixed(3)}`,
|
|
145
|
+
`NDCG@5: ${report.ndcgAt5.toFixed(3)}`,
|
|
146
|
+
];
|
|
147
|
+
return lines.join('\n');
|
|
148
|
+
}
|