@geraldmaron/construct 1.0.0 → 1.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.env.example +10 -7
- package/LICENSE +201 -21
- package/README.md +132 -257
- package/agents/contracts.json +387 -0
- package/agents/prompts/cx-accessibility.md +8 -0
- package/agents/prompts/cx-ai-engineer.md +92 -0
- package/agents/prompts/cx-architect.md +10 -2
- package/agents/prompts/cx-business-strategist.md +13 -0
- package/agents/prompts/cx-data-analyst.md +80 -0
- package/agents/prompts/cx-data-engineer.md +4 -0
- package/agents/prompts/cx-debugger.md +8 -0
- package/agents/prompts/cx-designer.md +19 -0
- package/agents/prompts/cx-devil-advocate.md +4 -0
- package/agents/prompts/cx-docs-keeper.md +128 -0
- package/agents/prompts/cx-engineer.md +13 -0
- package/agents/prompts/cx-evaluator.md +4 -0
- package/agents/prompts/cx-explorer.md +12 -0
- package/agents/prompts/cx-legal-compliance.md +13 -0
- package/agents/prompts/cx-operations.md +13 -0
- package/agents/prompts/cx-orchestrator.md +107 -4
- package/agents/prompts/cx-platform-engineer.md +75 -0
- package/agents/prompts/cx-product-manager.md +8 -0
- package/agents/prompts/cx-qa.md +100 -0
- package/agents/prompts/cx-rd-lead.md +13 -0
- package/agents/prompts/cx-release-manager.md +8 -0
- package/agents/prompts/cx-researcher.md +21 -1
- package/agents/prompts/cx-reviewer.md +8 -0
- package/agents/prompts/cx-security.md +104 -0
- package/agents/prompts/cx-sre.md +104 -0
- package/agents/prompts/cx-test-automation.md +4 -0
- package/agents/prompts/cx-trace-reviewer.md +7 -3
- package/agents/prompts/cx-ux-researcher.md +4 -0
- package/agents/registry.json +365 -90
- package/agents/role-manifests.json +217 -0
- package/bin/construct +3345 -141
- package/bin/construct-postinstall.mjs +87 -0
- package/commands/build/feature.md +6 -6
- package/commands/plan/feature.md +6 -5
- package/commands/plan/requirements.md +1 -1
- package/commands/ship/status.md +1 -1
- package/commands/work/drive.md +3 -3
- package/commands/work/optimize-prompts.md +3 -3
- package/db/{migrations → schema}/001_init.sql +2 -0
- package/db/schema/002_pgvector.sql +182 -0
- package/db/schema/003_intake.sql +47 -0
- package/examples/README.md +85 -0
- package/examples/internal/roles/architect/bad/clever-plan-without-contracts.md +30 -0
- package/examples/internal/roles/architect/golden/explicit-tradeoff-before-plan.md +23 -0
- package/examples/internal/roles/engineer/bad/speculative-abstraction.md +30 -0
- package/examples/internal/roles/engineer/golden/read-before-write.md +28 -0
- package/examples/internal/roles/orchestrator/bad/everything-becomes-multi-agent.md +29 -0
- package/examples/internal/roles/orchestrator/golden/minimal-dispatch.md +22 -0
- package/examples/internal/roles/qa/bad/coverage-theater.md +30 -0
- package/examples/internal/roles/qa/golden/regression-gate.md +23 -0
- package/examples/internal/roles/reviewer/bad/lgtm-without-verification.md +29 -0
- package/examples/internal/roles/reviewer/golden/find-structural-risk-first.md +28 -0
- package/examples/personas/construct/adversarial/ignore-instruction-to-skip-approval.md +22 -0
- package/examples/personas/construct/bad/commit-without-approval.md +30 -0
- package/examples/personas/construct/boundary/blocked-needs-main-input.md +29 -0
- package/examples/personas/construct/golden/branch-approval-before-mutation.md +36 -0
- package/examples/personas/construct/golden/focused-direct-answer.md +28 -0
- package/examples/provider-plugin/README.md +34 -0
- package/examples/provider-plugin/index.mjs +74 -0
- package/examples/provider-plugin/package.json +15 -0
- package/examples/seed-observations/README.md +38 -0
- package/examples/seed-observations/anti-patterns.md +44 -0
- package/examples/seed-observations/decisions.md +36 -0
- package/examples/seed-observations/patterns.md +42 -0
- package/lib/agent-contracts-enforce.mjs +158 -0
- package/lib/agent-contracts.mjs +231 -0
- package/lib/agents/postconditions.mjs +126 -0
- package/lib/agents/schema.mjs +124 -0
- package/lib/artifact-capture.mjs +183 -0
- package/lib/audit-trail.mjs +149 -0
- package/lib/auto-docs.mjs +245 -65
- package/lib/beads/auto-close.mjs +126 -0
- package/lib/beads/drift.mjs +171 -0
- package/lib/beads-automation.mjs +542 -0
- package/lib/beads-client.mjs +518 -0
- package/lib/beads-lock.mjs +377 -0
- package/lib/beads-optimistic.mjs +365 -0
- package/lib/bootstrap/built-ins.mjs +136 -0
- package/lib/bootstrap/lazy-install.mjs +161 -0
- package/lib/bootstrap/resources.mjs +120 -0
- package/lib/bootstrap.mjs +105 -0
- package/lib/cache-governor.js +213 -0
- package/lib/cache-strategy-anthropic.js +62 -0
- package/lib/cache-strategy-google.js +79 -0
- package/lib/cache-strategy-none.js +30 -0
- package/lib/cache-strategy-openai.js +48 -0
- package/lib/cache-strategy.js +91 -0
- package/lib/claude-allow.mjs +149 -0
- package/lib/cli-commands.mjs +413 -258
- package/lib/codex-config.mjs +3 -1
- package/lib/comment-lint.mjs +55 -6
- package/lib/completions.mjs +21 -6
- package/lib/config/alias.mjs +56 -0
- package/lib/config/project-config.mjs +335 -0
- package/lib/config/schema.mjs +159 -0
- package/lib/context-router.mjs +308 -0
- package/lib/cost-ledger.mjs +177 -0
- package/lib/cost.mjs +171 -10
- package/lib/dashboard-static.mjs +158 -0
- package/lib/deployment-mode.mjs +86 -0
- package/lib/deprecate.mjs +49 -0
- package/lib/dispatch-batch.js +183 -0
- package/lib/distill.mjs +21 -8
- package/lib/doc-stamp.mjs +164 -0
- package/lib/doc-verify.mjs +119 -0
- package/lib/docs-routing.mjs +89 -0
- package/lib/docs-verify.mjs +417 -0
- package/lib/doctor/audit.mjs +71 -0
- package/lib/doctor/cli.mjs +99 -0
- package/lib/doctor/escalate.mjs +29 -0
- package/lib/doctor/index.mjs +140 -0
- package/lib/doctor/report.mjs +170 -0
- package/lib/doctor/watchers/bd-watch.mjs +117 -0
- package/lib/doctor/watchers/cost.mjs +130 -0
- package/lib/doctor/watchers/disk.mjs +122 -0
- package/lib/doctor/watchers/handoffs.mjs +33 -0
- package/lib/doctor/watchers/process-pressure.mjs +60 -0
- package/lib/doctor/watchers/service-health.mjs +188 -0
- package/lib/document-extract.mjs +288 -0
- package/lib/document-ingest.mjs +230 -0
- package/lib/drop.mjs +282 -0
- package/lib/embed/approval-queue.mjs +176 -0
- package/lib/embed/artifact.mjs +349 -0
- package/lib/embed/authority-guard.mjs +155 -0
- package/lib/embed/cli.mjs +408 -0
- package/lib/embed/config.mjs +355 -0
- package/lib/embed/conflict-detection.mjs +264 -0
- package/lib/embed/customer-profiles.mjs +480 -0
- package/lib/embed/daemon.mjs +1309 -0
- package/lib/embed/demand-fetch.mjs +449 -0
- package/lib/embed/docs-lifecycle.mjs +349 -0
- package/lib/embed/inbox-live-watcher.mjs +119 -0
- package/lib/embed/inbox.mjs +343 -0
- package/lib/embed/intake-metrics.mjs +190 -0
- package/lib/embed/jobs/vector-sync.mjs +198 -0
- package/lib/embed/notifications.mjs +75 -0
- package/lib/embed/output.mjs +79 -0
- package/lib/embed/providers/github.mjs +295 -0
- package/lib/embed/providers/jira.mjs +192 -0
- package/lib/embed/providers/linear.mjs +186 -0
- package/lib/embed/providers/registry.mjs +115 -0
- package/lib/embed/providers/slack.mjs +203 -0
- package/lib/embed/recommendation-store.mjs +378 -0
- package/lib/embed/roadmap.mjs +374 -0
- package/lib/embed/role-framing.mjs +110 -0
- package/lib/embed/scheduler.mjs +99 -0
- package/lib/embed/semantic.mjs +325 -0
- package/lib/embed/snapshot.mjs +191 -0
- package/lib/embed/supervision.mjs +235 -0
- package/lib/embed/target-resolver.mjs +186 -0
- package/lib/embed/worker.mjs +62 -0
- package/lib/embed/workspaces.mjs +297 -0
- package/lib/engine/chunker-headings.mjs +110 -0
- package/lib/engine/compressor-heuristic.mjs +100 -0
- package/lib/engine/consolidate.mjs +287 -0
- package/lib/engine/contracts.mjs +126 -0
- package/lib/engine/defaults.mjs +129 -0
- package/lib/engine/eval-retrieval.mjs +148 -0
- package/lib/engine/fuser-rrf.mjs +62 -0
- package/lib/engine/index.mjs +37 -0
- package/lib/engine/registry.mjs +146 -0
- package/lib/engine/reranker-mmr.mjs +90 -0
- package/lib/engine/tokens.mjs +77 -0
- package/lib/entity-store.mjs +280 -0
- package/lib/env-config.mjs +69 -4
- package/lib/evals/retrieval-bench.mjs +159 -0
- package/lib/evaluator-optimizer.mjs +317 -0
- package/lib/features.mjs +159 -29
- package/lib/gates-audit.mjs +236 -0
- package/lib/git-hooks/prepare-commit-msg +58 -0
- package/lib/handoffs/cleanup.mjs +159 -0
- package/lib/handoffs/contract.mjs +162 -0
- package/lib/handoffs/inventory.mjs +120 -0
- package/lib/headhunt.mjs +28 -47
- package/lib/health-check.mjs +399 -0
- package/lib/hook-health.mjs +442 -0
- package/lib/hooks/_lib/log.mjs +82 -0
- package/lib/hooks/adaptive-lint.mjs +26 -2
- package/lib/hooks/agent-tracker.mjs +171 -14
- package/lib/hooks/audit-reads.mjs +109 -0
- package/lib/hooks/audit-trail.mjs +153 -0
- package/lib/hooks/bash-output-logger.mjs +71 -0
- package/lib/hooks/block-no-verify.mjs +41 -0
- package/lib/hooks/ci-status-check.mjs +82 -0
- package/lib/hooks/comment-lint.mjs +29 -7
- package/lib/hooks/config-protection.mjs +39 -18
- package/lib/hooks/context-watch.mjs +137 -0
- package/lib/hooks/context-window-recovery.mjs +4 -20
- package/lib/hooks/dep-audit.mjs +16 -0
- package/lib/hooks/doc-coupling-check.mjs +80 -0
- package/lib/hooks/edit-accumulator.mjs +3 -0
- package/lib/hooks/edit-error-recovery.mjs +3 -0
- package/lib/hooks/edit-guard.mjs +45 -4
- package/lib/hooks/env-check.mjs +3 -0
- package/lib/hooks/guard-bash.mjs +58 -1
- package/lib/hooks/mcp-audit.mjs +18 -20
- package/lib/hooks/mcp-health-check.mjs +36 -0
- package/lib/hooks/model-fallback.mjs +42 -56
- package/lib/hooks/policy-engine.mjs +209 -0
- package/lib/hooks/post-merge-docs-check.mjs +63 -0
- package/lib/hooks/pre-compact.mjs +4 -24
- package/lib/hooks/pre-push-gate.mjs +200 -37
- package/lib/hooks/proactive-activation.mjs +284 -0
- package/lib/hooks/read-tracker.mjs +27 -4
- package/lib/hooks/readme-age-check.mjs +77 -0
- package/lib/hooks/registry-sync.mjs +12 -5
- package/lib/hooks/scan-secrets.mjs +50 -7
- package/lib/hooks/session-optimize.mjs +311 -0
- package/lib/hooks/session-start.mjs +287 -28
- package/lib/hooks/stop-notify.mjs +236 -96
- package/lib/hooks/stop-typecheck.mjs +13 -1
- package/lib/hooks/test-watch.mjs +68 -0
- package/lib/host-capabilities.mjs +31 -10
- package/lib/init-docs.mjs +731 -291
- package/lib/init-unified.mjs +1116 -0
- package/lib/init-update.mjs +168 -0
- package/lib/init.mjs +107 -0
- package/lib/install/first-invocation.mjs +119 -0
- package/lib/install/stage-project.mjs +69 -0
- package/lib/intake/classify.mjs +278 -0
- package/lib/intake/feedback.mjs +273 -0
- package/lib/intake/filesystem-queue.mjs +158 -0
- package/lib/intake/intake-config.mjs +132 -0
- package/lib/intake/postgres-queue.mjs +198 -0
- package/lib/intake/prepare.mjs +139 -0
- package/lib/intake/queue.mjs +83 -0
- package/lib/intake/session-prelude.mjs +50 -0
- package/lib/integrations/intake-integrations.mjs +740 -0
- package/lib/intent-classifier.mjs +253 -0
- package/lib/knowledge/layout.mjs +72 -0
- package/lib/knowledge/rag.mjs +331 -0
- package/lib/knowledge/search.mjs +315 -0
- package/lib/knowledge/trends.mjs +261 -0
- package/lib/logger.mjs +85 -0
- package/lib/mcp/broker.mjs +124 -0
- package/lib/mcp/server.mjs +554 -854
- package/lib/mcp/tools/document.mjs +132 -0
- package/lib/mcp/tools/memory.mjs +209 -0
- package/lib/mcp/tools/project.mjs +349 -0
- package/lib/mcp/tools/skills.mjs +409 -0
- package/lib/mcp/tools/storage.mjs +60 -0
- package/lib/mcp/tools/telemetry.mjs +315 -0
- package/lib/mcp/tools/workflow.mjs +112 -0
- package/lib/mcp-catalog.json +54 -3
- package/lib/mcp-manager.mjs +96 -48
- package/lib/mcp-platform-config.mjs +22 -22
- package/lib/memory-stats.mjs +121 -0
- package/lib/mode-commands.mjs +124 -0
- package/lib/model-free-selector.mjs +184 -0
- package/lib/model-pricing.mjs +152 -0
- package/lib/model-registry.mjs +226 -0
- package/lib/model-router.mjs +362 -386
- package/lib/observation-store.mjs +391 -0
- package/lib/ollama-manager.mjs +428 -0
- package/lib/opencode-config.mjs +13 -3
- package/lib/opencode-runtime-plugin.mjs +70 -38
- package/lib/opencode-telemetry.mjs +64 -6
- package/lib/orchestration-policy.mjs +509 -6
- package/lib/overrides/resolver.mjs +207 -0
- package/lib/parity.mjs +147 -0
- package/lib/paths.mjs +33 -0
- package/lib/performance/generate.mjs +212 -0
- package/lib/plugin-registry.mjs +268 -0
- package/lib/policy/engine.mjs +130 -0
- package/lib/policy/unified-gates.mjs +96 -0
- package/lib/project-detection.mjs +129 -0
- package/lib/project-init-shared.mjs +272 -0
- package/lib/project-profile.mjs +447 -0
- package/lib/prompt-composer.js +434 -0
- package/lib/prompt-metadata.mjs +1 -1
- package/lib/provider-capabilities-anthropic.js +44 -0
- package/lib/provider-capabilities-deepseek.js +37 -0
- package/lib/provider-capabilities-generic.js +26 -0
- package/lib/provider-capabilities-google.js +47 -0
- package/lib/provider-capabilities-openai.js +45 -0
- package/lib/provider-capabilities.js +142 -0
- package/lib/providers/atlassian-confluence/index.mjs +103 -0
- package/lib/providers/atlassian-jira/index.mjs +100 -0
- package/lib/providers/auth-manager.mjs +126 -0
- package/lib/providers/circuit-breaker.mjs +124 -0
- package/lib/providers/contract.mjs +93 -0
- package/lib/providers/github/index.mjs +126 -0
- package/lib/providers/registry.mjs +184 -0
- package/lib/providers/salesforce/index.mjs +100 -0
- package/lib/providers/slack/index.mjs +80 -0
- package/lib/reflect.mjs +137 -0
- package/lib/research-lint.mjs +164 -0
- package/lib/resources/budget.mjs +259 -0
- package/lib/role-preload.mjs +21 -6
- package/lib/roles/approval-surface.mjs +54 -0
- package/lib/roles/cli.mjs +118 -0
- package/lib/roles/event-bus.mjs +79 -0
- package/lib/roles/fence.mjs +84 -0
- package/lib/roles/gateway.mjs +260 -0
- package/lib/roles/hook-emit.mjs +37 -0
- package/lib/roles/manifest.mjs +48 -0
- package/lib/roles/router.mjs +27 -0
- package/lib/runtime-pressure.mjs +360 -0
- package/lib/schema-artifact.mjs +134 -0
- package/lib/schema-infer.mjs +551 -0
- package/lib/server/auth.mjs +168 -0
- package/lib/server/chat.mjs +336 -0
- package/lib/server/cors.mjs +77 -0
- package/lib/server/csrf.mjs +91 -0
- package/lib/server/index.mjs +1927 -78
- package/lib/server/insights.mjs +765 -0
- package/lib/server/rate-limit.mjs +91 -0
- package/lib/server/static/assets/index-ab25c707.js +70 -0
- package/lib/server/static/assets/index-f0c80a2b.css +1 -0
- package/lib/server/static/index.html +12 -817
- package/lib/server/telemetry-login.mjs +108 -0
- package/lib/server/webhook.mjs +510 -0
- package/lib/service-manager.mjs +522 -58
- package/lib/services/pattern-promotion-service.mjs +167 -0
- package/lib/services/telemetry-backend.mjs +178 -0
- package/lib/session-store.mjs +374 -0
- package/lib/setup-prompts.mjs +96 -0
- package/lib/setup.mjs +525 -38
- package/lib/skills-apply.mjs +280 -0
- package/lib/skills-scope.mjs +118 -0
- package/lib/status.mjs +261 -70
- package/lib/storage/admin.mjs +355 -0
- package/lib/storage/backend.mjs +2 -1
- package/lib/storage/backup.mjs +347 -0
- package/lib/storage/embeddings-engine.mjs +133 -0
- package/lib/storage/embeddings-legacy.mjs +85 -0
- package/lib/storage/embeddings-local.mjs +108 -0
- package/lib/storage/embeddings-ollama.mjs +78 -0
- package/lib/storage/embeddings-openai.mjs +85 -0
- package/lib/storage/embeddings.mjs +92 -33
- package/lib/storage/file-lock.mjs +130 -0
- package/lib/storage/fusion.mjs +95 -0
- package/lib/storage/hybrid-query.mjs +34 -27
- package/lib/storage/migrations.mjs +187 -0
- package/lib/storage/postgres-backup.mjs +124 -0
- package/lib/storage/sql-store.mjs +5 -15
- package/lib/storage/state-source.mjs +12 -13
- package/lib/storage/store-version.mjs +115 -0
- package/lib/storage/sync.mjs +144 -35
- package/lib/storage/unified-storage.mjs +550 -0
- package/lib/storage/vector-client.mjs +286 -0
- package/lib/storage/vector-store.mjs +71 -30
- package/lib/task-graph/generate.mjs +135 -0
- package/lib/task-graph/schema.mjs +81 -0
- package/lib/task-graph/store.mjs +71 -0
- package/lib/telemetry/backends/local.mjs +62 -0
- package/lib/telemetry/backends/{langfuse.mjs → remote.mjs} +27 -14
- package/lib/telemetry/backfill.mjs +180 -0
- package/lib/telemetry/eval-datasets.mjs +203 -0
- package/lib/telemetry/{langfuse-ingest.mjs → ingest.mjs} +26 -20
- package/lib/telemetry/intent-verifications.mjs +86 -0
- package/lib/telemetry/llm-judge.mjs +350 -0
- package/lib/telemetry/model-pricing-catalog.mjs +557 -0
- package/lib/telemetry/setup.mjs +151 -0
- package/lib/telemetry/skill-calls.mjs +78 -0
- package/lib/telemetry/team-rollup.mjs +4 -4
- package/lib/token-engine.js +117 -0
- package/lib/token-estimator-anthropic.js +15 -0
- package/lib/token-estimator-deepseek.js +13 -0
- package/lib/token-estimator-default.js +13 -0
- package/lib/token-estimator-google.js +13 -0
- package/lib/token-estimator-openai.js +13 -0
- package/lib/toolkit-env.mjs +1 -1
- package/lib/tty-prompts.mjs +211 -0
- package/lib/uninstall/uninstall.mjs +423 -0
- package/lib/update.mjs +115 -0
- package/lib/upgrade.mjs +141 -0
- package/lib/validator.mjs +51 -2
- package/lib/validators/skills.mjs +142 -0
- package/lib/wireframe.mjs +422 -0
- package/lib/worker/entrypoint.mjs +241 -0
- package/lib/worker/evidence.mjs +107 -0
- package/lib/worker/run.mjs +154 -0
- package/lib/worker/trace.mjs +182 -0
- package/lib/workflow-state.mjs +14 -18
- package/package.json +21 -8
- package/personas/construct.md +53 -51
- package/platforms/claude/CLAUDE.md +1 -1
- package/platforms/claude/settings.template.json +115 -68
- package/platforms/opencode/config.template.json +3 -7
- package/rules/common/agents.md +11 -38
- package/rules/common/beads-hygiene.md +75 -0
- package/rules/common/code-review.md +11 -100
- package/rules/common/coding-style.md +5 -3
- package/rules/common/comments.md +30 -111
- package/rules/common/commit-approval.md +53 -0
- package/rules/common/cx-agent-routing.md +1 -0
- package/rules/common/development-workflow.md +23 -41
- package/rules/common/doc-ownership.md +54 -0
- package/rules/common/efficiency.md +57 -0
- package/rules/common/framing.md +76 -0
- package/rules/common/git-workflow.md +2 -4
- package/rules/common/patterns.md +3 -7
- package/rules/common/performance.md +15 -31
- package/rules/common/release-gates.md +69 -0
- package/rules/common/research.md +107 -0
- package/rules/common/security.md +6 -6
- package/rules/common/skill-composition.md +67 -0
- package/rules/common/testing.md +15 -27
- package/rules/golang/hooks.md +0 -4
- package/rules/policy/bootstrap.yaml +11 -0
- package/rules/policy/drive.yaml +8 -0
- package/rules/policy/task.yaml +9 -0
- package/rules/policy/workflow.yaml +9 -0
- package/rules/python/hooks.md +0 -4
- package/rules/swift/hooks.md +0 -4
- package/rules/typescript/hooks.md +0 -4
- package/rules/web/hooks.md +0 -2
- package/{sync-agents.mjs → scripts/sync-agents.mjs} +306 -61
- package/skills/ai/prompt-optimizer.md +7 -7
- package/skills/compliance/ai-disclosure.md +58 -0
- package/skills/compliance/data-privacy.md +46 -0
- package/skills/compliance/license-audit.md +40 -0
- package/skills/compliance/regulatory-review.md +61 -0
- package/skills/docs/document-ingest-workflow.md +52 -0
- package/skills/docs/evidence-ingest-workflow.md +9 -0
- package/skills/docs/init-docs.md +51 -18
- package/skills/docs/product-intelligence-workflow.md +12 -0
- package/skills/docs/product-signal-workflow.md +4 -0
- package/skills/docs/research-workflow.md +23 -8
- package/skills/docs/runbook-workflow.md +1 -1
- package/skills/operating/orchestration-reference.md +151 -0
- package/skills/roles/architect.md +5 -0
- package/skills/roles/engineer.md +32 -0
- package/skills/roles/operator.md +12 -0
- package/skills/roles/reviewer.md +23 -0
- package/skills/routing.md +1 -1
- package/templates/devcontainer/Dockerfile.devcontainer +38 -0
- package/templates/devcontainer/devcontainer.json +31 -0
- package/templates/distribution/bootstrap.ps1 +142 -0
- package/templates/distribution/bootstrap.sh +196 -0
- package/templates/distribution/run.mjs +187 -0
- package/templates/docs/adr.md +44 -8
- package/templates/docs/changelog-entry.md +43 -0
- package/templates/docs/construct_guide.md +149 -0
- package/templates/docs/evidence-brief.md +2 -2
- package/templates/docs/meta-prd.md +140 -19
- package/templates/docs/onboarding.md +57 -0
- package/templates/docs/prd.md +159 -15
- package/templates/docs/research-brief.md +4 -4
- package/templates/homebrew/construct.rb +67 -0
- package/langfuse/docker-compose.yml +0 -82
- package/lib/eval-harness.mjs +0 -59
- package/lib/hooks/bootstrap-guard.mjs +0 -90
- package/lib/hooks/console-warn.mjs +0 -43
- package/lib/hooks/continuation-enforcer.mjs +0 -72
- package/lib/hooks/drive-guard.mjs +0 -89
- package/lib/hooks/mcp-task-scope.mjs +0 -47
- package/lib/hooks/task-completed-guard.mjs +0 -43
- package/lib/hooks/teammate-idle-guard.mjs +0 -54
- package/lib/hooks/workflow-guard.mjs +0 -62
- package/lib/prompt-composer.mjs +0 -196
- package/lib/review.mjs +0 -429
- package/lib/server/static/app.js +0 -841
- package/lib/telemetry/langfuse-model-sync.mjs +0 -270
- package/rules/common/hooks.md +0 -35
- package/rules/zh/README.md +0 -113
- package/rules/zh/agents.md +0 -55
- package/rules/zh/code-review.md +0 -129
- package/rules/zh/coding-style.md +0 -53
- package/rules/zh/development-workflow.md +0 -49
- package/rules/zh/git-workflow.md +0 -29
- package/rules/zh/hooks.md +0 -35
- package/rules/zh/patterns.md +0 -36
- package/rules/zh/performance.md +0 -60
- package/rules/zh/security.md +0 -34
- package/rules/zh/testing.md +0 -34
|
@@ -0,0 +1,315 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* lib/knowledge/search.mjs — Self-knowledge search for Construct's own docs.
|
|
3
|
+
*
|
|
4
|
+
* Answers questions about what Construct is, how it works, and what it can do
|
|
5
|
+
* by reading Construct's own documentation tree. Designed for the
|
|
6
|
+
* `knowledge_search` MCP tool — no daemon, no network, no external deps.
|
|
7
|
+
*
|
|
8
|
+
* Sources searched (in priority order):
|
|
9
|
+
* 1. docs/concepts/*.md — architecture, agents, gates, durable state
|
|
10
|
+
* 2. docs/start/*.mdx — install + first task
|
|
11
|
+
* 3. docs/README.md — docs index + contract
|
|
12
|
+
* 4. .cx/knowledge/ — operator-written internal docs
|
|
13
|
+
* 5. Any *.md in docs/cookbook/ — task-oriented recipes
|
|
14
|
+
*
|
|
15
|
+
* Retrieval strategy:
|
|
16
|
+
* Token-based BM25-like scoring over 200-char chunks. Returns top-K chunks
|
|
17
|
+
* with their source file and a relevance score. Pure text — no embeddings.
|
|
18
|
+
*
|
|
19
|
+
* @module lib/knowledge/search
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
import { existsSync, readFileSync, readdirSync, statSync } from 'node:fs';
|
|
23
|
+
import { join, relative, basename } from 'node:path';
|
|
24
|
+
import { fileURLToPath } from 'node:url';
|
|
25
|
+
import { dirname } from 'node:path';
|
|
26
|
+
import { homedir } from 'node:os';
|
|
27
|
+
|
|
28
|
+
const MODULE_DIR = dirname(fileURLToPath(import.meta.url));
|
|
29
|
+
const REPO_ROOT = join(MODULE_DIR, '..', '..');
|
|
30
|
+
|
|
31
|
+
// ─── Source catalogue ────────────────────────────────────────────────────────
|
|
32
|
+
|
|
33
|
+
/** Priority-ordered list of source files to search. */
|
|
34
|
+
function buildSourceList(repoRoot = REPO_ROOT) {
|
|
35
|
+
const sources = [];
|
|
36
|
+
|
|
37
|
+
const priority = [
|
|
38
|
+
'docs/concepts/architecture.md',
|
|
39
|
+
'docs/concepts/agents-and-personas.mdx',
|
|
40
|
+
'docs/concepts/gates-and-enforcement.md',
|
|
41
|
+
'docs/concepts/beads-and-state.md',
|
|
42
|
+
'docs/concepts/prompt-surfaces.md',
|
|
43
|
+
'docs/concepts/knowledge-layout.md',
|
|
44
|
+
'docs/concepts/deployment-model.md',
|
|
45
|
+
'docs/concepts/embedding-boundary.md',
|
|
46
|
+
'docs/README.md',
|
|
47
|
+
'docs/start/install.mdx',
|
|
48
|
+
'docs/start/first-task.mdx',
|
|
49
|
+
];
|
|
50
|
+
|
|
51
|
+
for (const rel of priority) {
|
|
52
|
+
const full = join(repoRoot, rel);
|
|
53
|
+
if (existsSync(full)) sources.push({ path: full, rel, priority: 1 });
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// Cookbook recipes (task-oriented how-tos)
|
|
57
|
+
const cookbookDir = join(repoRoot, 'docs', 'cookbook');
|
|
58
|
+
if (existsSync(cookbookDir)) {
|
|
59
|
+
for (const file of readdirSync(cookbookDir)) {
|
|
60
|
+
if (file.endsWith('.md') || file.endsWith('.mdx')) {
|
|
61
|
+
const full = join(cookbookDir, file);
|
|
62
|
+
sources.push({ path: full, rel: `docs/cookbook/${file}`, priority: 2 });
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// Operator internal knowledge
|
|
68
|
+
const knowledgeDirs = [
|
|
69
|
+
join(repoRoot, '.cx', 'knowledge', 'internal'),
|
|
70
|
+
join(repoRoot, '.cx', 'knowledge', 'reference'),
|
|
71
|
+
join(repoRoot, '.cx', 'knowledge', 'how-tos'),
|
|
72
|
+
];
|
|
73
|
+
for (const dir of knowledgeDirs) {
|
|
74
|
+
if (!existsSync(dir)) continue;
|
|
75
|
+
for (const file of readdirSync(dir)) {
|
|
76
|
+
if (file.endsWith('.md')) {
|
|
77
|
+
const full = join(dir, file);
|
|
78
|
+
sources.push({ path: full, rel: relative(repoRoot, full), priority: 3 });
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
return sources;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// ─── Chunking ────────────────────────────────────────────────────────────────
|
|
87
|
+
|
|
88
|
+
const CHUNK_CHARS = 400;
|
|
89
|
+
const CHUNK_OVERLAP = 80;
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Split text into overlapping chunks, preserving markdown section boundaries
|
|
93
|
+
* where possible. Each chunk carries the nearest preceding heading as context.
|
|
94
|
+
*/
|
|
95
|
+
function chunkText(text, source) {
|
|
96
|
+
const chunks = [];
|
|
97
|
+
const lines = text.split('\n');
|
|
98
|
+
let heading = '';
|
|
99
|
+
let buffer = '';
|
|
100
|
+
let bufferStart = 0;
|
|
101
|
+
|
|
102
|
+
function flush(lineIdx) {
|
|
103
|
+
const trimmed = buffer.trim();
|
|
104
|
+
if (trimmed.length < 20) return;
|
|
105
|
+
chunks.push({ text: trimmed, heading, source, lineStart: bufferStart });
|
|
106
|
+
// Overlap: carry last CHUNK_OVERLAP chars into the next buffer
|
|
107
|
+
buffer = trimmed.length > CHUNK_OVERLAP ? trimmed.slice(-CHUNK_OVERLAP) : trimmed;
|
|
108
|
+
bufferStart = lineIdx;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
for (let i = 0; i < lines.length; i++) {
|
|
112
|
+
const line = lines[i];
|
|
113
|
+
if (/^#{1,3}\s/.test(line)) {
|
|
114
|
+
// Section boundary — flush current buffer, update heading
|
|
115
|
+
flush(i);
|
|
116
|
+
heading = line.replace(/^#+\s*/, '').trim();
|
|
117
|
+
buffer = line + '\n';
|
|
118
|
+
bufferStart = i;
|
|
119
|
+
} else {
|
|
120
|
+
buffer += line + '\n';
|
|
121
|
+
if (buffer.length >= CHUNK_CHARS) flush(i);
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
flush(lines.length);
|
|
125
|
+
return chunks;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// ─── Scoring ─────────────────────────────────────────────────────────────────
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Tokenise text into lowercase words, filtering stop words.
|
|
132
|
+
*/
|
|
133
|
+
const STOP = new Set(['the', 'a', 'an', 'and', 'or', 'is', 'are', 'it', 'in',
|
|
134
|
+
'of', 'to', 'for', 'with', 'on', 'at', 'by', 'from', 'that', 'this',
|
|
135
|
+
'be', 'as', 'was', 'will', 'can', 'its', 'not', 'you', 'your', 'how']);
|
|
136
|
+
|
|
137
|
+
function tokenise(text) {
|
|
138
|
+
return text.toLowerCase()
|
|
139
|
+
.replace(/[^a-z0-9\s-]/g, ' ')
|
|
140
|
+
.split(/\s+/)
|
|
141
|
+
.filter(t => t.length > 2 && !STOP.has(t));
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* BM25-inspired score: term frequency in chunk × inverse source frequency ×
|
|
146
|
+
* source priority bonus.
|
|
147
|
+
*/
|
|
148
|
+
function scoreChunk(chunk, queryTokens, idfMap) {
|
|
149
|
+
const chunkTokens = tokenise(chunk.text);
|
|
150
|
+
const tf = new Map();
|
|
151
|
+
for (const t of chunkTokens) tf.set(t, (tf.get(t) ?? 0) + 1);
|
|
152
|
+
|
|
153
|
+
let score = 0;
|
|
154
|
+
for (const qt of queryTokens) {
|
|
155
|
+
const freq = tf.get(qt) ?? 0;
|
|
156
|
+
if (freq > 0) {
|
|
157
|
+
const idf = idfMap.get(qt) ?? 1;
|
|
158
|
+
// BM25 k1=1.5, b=0 (no length normalisation — chunks are pre-sized)
|
|
159
|
+
score += idf * (freq * 2.5) / (freq + 1.5);
|
|
160
|
+
}
|
|
161
|
+
// Heading match bonus
|
|
162
|
+
if (chunk.heading.toLowerCase().includes(qt)) score += 2;
|
|
163
|
+
// File-name match bonus — rewards architecture.md for "architecture" queries
|
|
164
|
+
if (chunk.source.rel.toLowerCase().includes(qt)) score += 1.5;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
// Priority bonus: priority-1 (architecture, README) wins decisively over how-tos.
|
|
168
|
+
// Multiplier is additive-style: add a flat boost so low-scoring priority-1 chunks
|
|
169
|
+
// aren't simply outscored by high-TF how-to chunks.
|
|
170
|
+
if (chunk.source.priority === 1) score += 3;
|
|
171
|
+
else if (chunk.source.priority === 2) score *= 1.05;
|
|
172
|
+
|
|
173
|
+
return score;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
function buildIdf(queryTokens, chunks) {
|
|
177
|
+
const idf = new Map();
|
|
178
|
+
const N = chunks.length || 1;
|
|
179
|
+
for (const qt of queryTokens) {
|
|
180
|
+
const df = chunks.filter(c => tokenise(c.text).includes(qt)).length || 1;
|
|
181
|
+
idf.set(qt, Math.log((N - df + 0.5) / (df + 0.5) + 1));
|
|
182
|
+
}
|
|
183
|
+
return idf;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
// ─── Observation store loader ────────────────────────────────────────────────
|
|
187
|
+
|
|
188
|
+
/**
|
|
189
|
+
* Load distilled embed observations from `<rootDir>/.cx/observations/` and
|
|
190
|
+
* convert each one into a searchable text chunk. Returns [] if the directory
|
|
191
|
+
* doesn't exist or is empty.
|
|
192
|
+
*/
|
|
193
|
+
function buildObservationChunks(rootDir) {
|
|
194
|
+
const obsDir = join(rootDir, '.cx', 'observations');
|
|
195
|
+
if (!existsSync(obsDir)) return [];
|
|
196
|
+
|
|
197
|
+
const chunks = [];
|
|
198
|
+
let files;
|
|
199
|
+
try { files = readdirSync(obsDir).filter(f => f.endsWith('.json')); } catch { return []; }
|
|
200
|
+
|
|
201
|
+
for (const file of files) {
|
|
202
|
+
let obs;
|
|
203
|
+
try { obs = JSON.parse(readFileSync(join(obsDir, file), 'utf8')); } catch { continue; }
|
|
204
|
+
if (!obs || typeof obs !== 'object') continue;
|
|
205
|
+
|
|
206
|
+
const parts = [];
|
|
207
|
+
if (obs.summary) parts.push(obs.summary);
|
|
208
|
+
if (obs.content && obs.content !== obs.summary) parts.push(obs.content);
|
|
209
|
+
if (Array.isArray(obs.tags) && obs.tags.length) parts.push(`tags: ${obs.tags.join(', ')}`);
|
|
210
|
+
|
|
211
|
+
const text = parts.join('\n').trim();
|
|
212
|
+
if (!text) continue;
|
|
213
|
+
|
|
214
|
+
chunks.push({
|
|
215
|
+
text,
|
|
216
|
+
heading: obs.summary ?? '',
|
|
217
|
+
source: { path: join(obsDir, file), rel: `observations/${file}`, priority: 2 },
|
|
218
|
+
lineStart: 0,
|
|
219
|
+
});
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
return chunks;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
|
|
226
|
+
/**
|
|
227
|
+
* @typedef {object} KnowledgeSearchResult
|
|
228
|
+
* @property {boolean} ok
|
|
229
|
+
* @property {string} query
|
|
230
|
+
* @property {number} totalChunks
|
|
231
|
+
* @property {SearchHit[]} hits
|
|
232
|
+
* @property {string[]} sources — unique source files that contributed hits
|
|
233
|
+
* @property {string} [message]
|
|
234
|
+
*/
|
|
235
|
+
|
|
236
|
+
/**
|
|
237
|
+
* @typedef {object} SearchHit
|
|
238
|
+
* @property {string} text
|
|
239
|
+
* @property {string} heading
|
|
240
|
+
* @property {string} file — repo-relative path
|
|
241
|
+
* @property {number} score
|
|
242
|
+
* @property {number} lineStart
|
|
243
|
+
*/
|
|
244
|
+
|
|
245
|
+
/**
|
|
246
|
+
* Search Construct's own documentation for content relevant to `query`.
|
|
247
|
+
*
|
|
248
|
+
* @param {object} opts
|
|
249
|
+
* @param {string} opts.query — natural-language question or keyword
|
|
250
|
+
* @param {number} [opts.topK=5] — max hits to return
|
|
251
|
+
* @param {number} [opts.minScore=0.1] — discard hits below this score
|
|
252
|
+
* @param {string} [opts.repoRoot] — override repo root (for testing)
|
|
253
|
+
* @param {string} [opts.rootDir] — data dir where .cx/observations/ lives (default: homedir())
|
|
254
|
+
* @returns {KnowledgeSearchResult}
|
|
255
|
+
*/
|
|
256
|
+
export function knowledgeSearch({ query, topK = 5, minScore = 0.1, repoRoot, rootDir } = {}) {
|
|
257
|
+
if (!query || typeof query !== 'string') {
|
|
258
|
+
return { ok: false, query: query ?? '', totalChunks: 0, hits: [], sources: [], message: 'query is required' };
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
const root = repoRoot ?? REPO_ROOT;
|
|
262
|
+
const dataDir = rootDir ?? (process.env.CX_DATA_DIR?.trim() || homedir());
|
|
263
|
+
const sources = buildSourceList(root);
|
|
264
|
+
|
|
265
|
+
// Build corpus from docs + operator knowledge
|
|
266
|
+
const allChunks = [];
|
|
267
|
+
for (const src of sources) {
|
|
268
|
+
let text = '';
|
|
269
|
+
try { text = readFileSync(src.path, 'utf8'); } catch { continue; }
|
|
270
|
+
const chunks = chunkText(text, src);
|
|
271
|
+
allChunks.push(...chunks);
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
// Add distilled embed observations from the data dir
|
|
275
|
+
const obsChunks = buildObservationChunks(dataDir);
|
|
276
|
+
allChunks.push(...obsChunks);
|
|
277
|
+
|
|
278
|
+
if (!allChunks.length) {
|
|
279
|
+
return { ok: false, query, totalChunks: 0, hits: [], sources: [], message: 'No documentation or observation sources found' };
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
const queryTokens = tokenise(query);
|
|
283
|
+
if (!queryTokens.length) {
|
|
284
|
+
return { ok: false, query, totalChunks: allChunks.length, hits: [], sources: [], message: 'Query contains no searchable terms after stop-word removal' };
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
const idf = buildIdf(queryTokens, allChunks);
|
|
288
|
+
|
|
289
|
+
const scored = allChunks
|
|
290
|
+
.map(chunk => ({ chunk, score: scoreChunk(chunk, queryTokens, idf) }))
|
|
291
|
+
.filter(({ score }) => score >= minScore)
|
|
292
|
+
.sort((a, b) => b.score - a.score)
|
|
293
|
+
.slice(0, topK);
|
|
294
|
+
|
|
295
|
+
const hits = scored.map(({ chunk, score }) => ({
|
|
296
|
+
text: chunk.text,
|
|
297
|
+
heading: chunk.heading,
|
|
298
|
+
file: chunk.source.rel,
|
|
299
|
+
score: Math.round(score * 100) / 100,
|
|
300
|
+
lineStart: chunk.lineStart,
|
|
301
|
+
}));
|
|
302
|
+
|
|
303
|
+
const uniqueSources = [...new Set(hits.map(h => h.file))];
|
|
304
|
+
|
|
305
|
+
return {
|
|
306
|
+
ok: true,
|
|
307
|
+
query,
|
|
308
|
+
totalChunks: allChunks.length,
|
|
309
|
+
hits,
|
|
310
|
+
sources: uniqueSources,
|
|
311
|
+
message: hits.length
|
|
312
|
+
? `Found ${hits.length} relevant excerpt${hits.length === 1 ? '' : 's'} across ${uniqueSources.length} source${uniqueSources.length === 1 ? '' : 's'}`
|
|
313
|
+
: 'No relevant content found — try broader terms',
|
|
314
|
+
};
|
|
315
|
+
}
|
|
@@ -0,0 +1,261 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* lib/knowledge/trends.mjs — Trend detection over the observation corpus.
|
|
3
|
+
*
|
|
4
|
+
* Surfaces:
|
|
5
|
+
* - Recurring patterns: observations with similar embeddings that appear
|
|
6
|
+
* multiple times across roles or sessions.
|
|
7
|
+
* - Escalating risks: anti-pattern observations whose frequency is increasing
|
|
8
|
+
* over time (more recent occurrences than older ones).
|
|
9
|
+
* - Decision drift: decisions that have been re-opened or contradicted by
|
|
10
|
+
* later observations.
|
|
11
|
+
* - Hot topics: terms with the highest weighted TF across recent observations
|
|
12
|
+
* (BM25-derived importance, recency-weighted).
|
|
13
|
+
*
|
|
14
|
+
* All detection is zero-dep and deterministic — no external API calls.
|
|
15
|
+
* Results are plain objects suitable for JSON serialisation and dashboard display.
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
import { listObservations, getObservation } from '../observation-store.mjs';
|
|
19
|
+
import { cosineSimilarity, tokenize } from '../storage/embeddings.mjs';
|
|
20
|
+
import { embedSync as embedText } from '../storage/embeddings-legacy.mjs';
|
|
21
|
+
|
|
22
|
+
const RECURRENCE_SIMILARITY_THRESHOLD = 0.40;
|
|
23
|
+
const RECURRENCE_MIN_COUNT = 2;
|
|
24
|
+
const ESCALATION_WINDOW_DAYS = 7;
|
|
25
|
+
const HOT_TOPICS_LIMIT = 10;
|
|
26
|
+
const TREND_LIMIT = 20;
|
|
27
|
+
|
|
28
|
+
// ── Helpers ────────────────────────────────────────────────────────────────
|
|
29
|
+
|
|
30
|
+
function daysSince(isoDate) {
|
|
31
|
+
if (!isoDate) return Infinity;
|
|
32
|
+
return (Date.now() - new Date(isoDate).getTime()) / 86_400_000;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function recencyWeight(isoDate, halfLifeDays = 14) {
|
|
36
|
+
const age = daysSince(isoDate);
|
|
37
|
+
return Math.pow(0.5, age / halfLifeDays);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// ── Recurring patterns ─────────────────────────────────────────────────────
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Cluster observations by embedding similarity. Returns groups of observations
|
|
44
|
+
* that are semantically similar (likely the same recurring pattern).
|
|
45
|
+
*/
|
|
46
|
+
function clusterBySimilarity(observations) {
|
|
47
|
+
const embedded = observations.map((obs) => ({
|
|
48
|
+
...obs,
|
|
49
|
+
embedding: embedText(`${obs.summary || ''} ${obs.content || ''}`),
|
|
50
|
+
}));
|
|
51
|
+
|
|
52
|
+
const clusters = [];
|
|
53
|
+
const assigned = new Set();
|
|
54
|
+
|
|
55
|
+
for (let i = 0; i < embedded.length; i++) {
|
|
56
|
+
if (assigned.has(i)) continue;
|
|
57
|
+
const cluster = [embedded[i]];
|
|
58
|
+
assigned.add(i);
|
|
59
|
+
for (let j = i + 1; j < embedded.length; j++) {
|
|
60
|
+
if (assigned.has(j)) continue;
|
|
61
|
+
const sim = cosineSimilarity(embedded[i].embedding, embedded[j].embedding);
|
|
62
|
+
if (sim >= RECURRENCE_SIMILARITY_THRESHOLD) {
|
|
63
|
+
cluster.push(embedded[j]);
|
|
64
|
+
assigned.add(j);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
if (cluster.length >= RECURRENCE_MIN_COUNT) {
|
|
68
|
+
clusters.push(cluster);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
return clusters;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Detect recurring patterns: semantically similar observations seen across
|
|
77
|
+
* multiple sessions or roles.
|
|
78
|
+
*
|
|
79
|
+
* @returns {object[]} array of { summary, count, roles, categories, firstSeen, lastSeen, observations }
|
|
80
|
+
*/
|
|
81
|
+
export function detectRecurringPatterns(rootDir = process.cwd()) {
|
|
82
|
+
const all = listObservations(rootDir, {});
|
|
83
|
+
if (all.length === 0) return [];
|
|
84
|
+
|
|
85
|
+
const full = all.map((e) => ({ ...e, ...getObservation(rootDir, e.id) }));
|
|
86
|
+
const clusters = clusterBySimilarity(full);
|
|
87
|
+
|
|
88
|
+
return clusters
|
|
89
|
+
.map((cluster) => {
|
|
90
|
+
const roles = [...new Set(cluster.map((o) => o.role).filter(Boolean))];
|
|
91
|
+
const categories = [...new Set(cluster.map((o) => o.category).filter(Boolean))];
|
|
92
|
+
const dates = cluster.map((o) => o.createdAt).filter(Boolean).sort();
|
|
93
|
+
return {
|
|
94
|
+
type: 'recurring_pattern',
|
|
95
|
+
summary: cluster[0].summary || 'Recurring pattern',
|
|
96
|
+
count: cluster.length,
|
|
97
|
+
roles,
|
|
98
|
+
categories,
|
|
99
|
+
firstSeen: dates[0] || null,
|
|
100
|
+
lastSeen: dates[dates.length - 1] || null,
|
|
101
|
+
observations: cluster.map((o) => ({ id: o.id, summary: o.summary, role: o.role, createdAt: o.createdAt })),
|
|
102
|
+
};
|
|
103
|
+
})
|
|
104
|
+
.sort((a, b) => b.count - a.count)
|
|
105
|
+
.slice(0, TREND_LIMIT);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// ── Escalating risks ───────────────────────────────────────────────────────
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Detect escalating risks: anti-pattern observations where the frequency in
|
|
112
|
+
* the recent window is higher than the baseline rate.
|
|
113
|
+
*
|
|
114
|
+
* @returns {object[]} array of { summary, recentCount, olderCount, escalationScore, lastSeen }
|
|
115
|
+
*/
|
|
116
|
+
export function detectEscalatingRisks(rootDir = process.cwd()) {
|
|
117
|
+
const all = listObservations(rootDir, { category: 'anti-pattern' });
|
|
118
|
+
if (all.length === 0) return [];
|
|
119
|
+
|
|
120
|
+
const full = all.map((e) => ({ ...e, ...getObservation(rootDir, e.id) }));
|
|
121
|
+
const clusters = clusterBySimilarity(full);
|
|
122
|
+
|
|
123
|
+
return clusters
|
|
124
|
+
.map((cluster) => {
|
|
125
|
+
const recent = cluster.filter((o) => daysSince(o.createdAt) <= ESCALATION_WINDOW_DAYS);
|
|
126
|
+
const older = cluster.filter((o) => daysSince(o.createdAt) > ESCALATION_WINDOW_DAYS);
|
|
127
|
+
const recentRate = recent.length / Math.max(ESCALATION_WINDOW_DAYS, 1);
|
|
128
|
+
const olderSpan = Math.max(
|
|
129
|
+
cluster.reduce((span, o) => Math.max(span, daysSince(o.createdAt)), 0) - ESCALATION_WINDOW_DAYS,
|
|
130
|
+
1,
|
|
131
|
+
);
|
|
132
|
+
const olderRate = older.length / olderSpan;
|
|
133
|
+
const escalationScore = olderRate > 0 ? recentRate / olderRate : recentRate * 10;
|
|
134
|
+
const dates = cluster.map((o) => o.createdAt).filter(Boolean).sort();
|
|
135
|
+
return {
|
|
136
|
+
type: 'escalating_risk',
|
|
137
|
+
summary: cluster[0].summary || 'Escalating risk',
|
|
138
|
+
recentCount: recent.length,
|
|
139
|
+
olderCount: older.length,
|
|
140
|
+
escalationScore: Number(escalationScore.toFixed(2)),
|
|
141
|
+
lastSeen: dates[dates.length - 1] || null,
|
|
142
|
+
observations: cluster.map((o) => ({ id: o.id, summary: o.summary, createdAt: o.createdAt })),
|
|
143
|
+
};
|
|
144
|
+
})
|
|
145
|
+
.filter((r) => r.escalationScore > 1.5 && r.recentCount > 0)
|
|
146
|
+
.sort((a, b) => b.escalationScore - a.escalationScore)
|
|
147
|
+
.slice(0, TREND_LIMIT);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// ── Decision drift ─────────────────────────────────────────────────────────
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* Detect decision drift: decision observations that are semantically similar
|
|
154
|
+
* to later anti-pattern or insight observations (suggesting the decision was
|
|
155
|
+
* questioned or reversed).
|
|
156
|
+
*
|
|
157
|
+
* @returns {object[]} array of { decision, conflictingObservations, driftScore }
|
|
158
|
+
*/
|
|
159
|
+
export function detectDecisionDrift(rootDir = process.cwd()) {
|
|
160
|
+
const decisions = listObservations(rootDir, { category: 'decision' })
|
|
161
|
+
.map((e) => ({ ...e, ...getObservation(rootDir, e.id) }));
|
|
162
|
+
const signals = listObservations(rootDir, {})
|
|
163
|
+
.filter((e) => ['anti-pattern', 'insight'].includes(e.category))
|
|
164
|
+
.map((e) => ({ ...e, ...getObservation(rootDir, e.id) }));
|
|
165
|
+
|
|
166
|
+
if (decisions.length === 0 || signals.length === 0) return [];
|
|
167
|
+
|
|
168
|
+
return decisions
|
|
169
|
+
.map((decision) => {
|
|
170
|
+
const decEmbed = embedText(`${decision.summary || ''} ${decision.content || ''}`);
|
|
171
|
+
const conflicting = signals
|
|
172
|
+
.filter((s) => new Date(s.createdAt || 0) > new Date(decision.createdAt || 0))
|
|
173
|
+
.map((s) => ({
|
|
174
|
+
...s,
|
|
175
|
+
similarity: cosineSimilarity(decEmbed, embedText(`${s.summary || ''} ${s.content || ''}`)),
|
|
176
|
+
}))
|
|
177
|
+
.filter((s) => s.similarity >= 0.35)
|
|
178
|
+
.sort((a, b) => b.similarity - a.similarity)
|
|
179
|
+
.slice(0, 3);
|
|
180
|
+
|
|
181
|
+
if (conflicting.length === 0) return null;
|
|
182
|
+
|
|
183
|
+
const driftScore = conflicting.reduce((sum, s) => sum + s.similarity * recencyWeight(s.createdAt), 0);
|
|
184
|
+
return {
|
|
185
|
+
type: 'decision_drift',
|
|
186
|
+
decision: { id: decision.id, summary: decision.summary, createdAt: decision.createdAt },
|
|
187
|
+
conflictingObservations: conflicting.map((s) => ({
|
|
188
|
+
id: s.id,
|
|
189
|
+
summary: s.summary,
|
|
190
|
+
category: s.category,
|
|
191
|
+
similarity: Number(s.similarity.toFixed(3)),
|
|
192
|
+
createdAt: s.createdAt,
|
|
193
|
+
})),
|
|
194
|
+
driftScore: Number(driftScore.toFixed(3)),
|
|
195
|
+
};
|
|
196
|
+
})
|
|
197
|
+
.filter(Boolean)
|
|
198
|
+
.sort((a, b) => b.driftScore - a.driftScore)
|
|
199
|
+
.slice(0, TREND_LIMIT);
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
// ── Hot topics ─────────────────────────────────────────────────────────────
|
|
203
|
+
|
|
204
|
+
/**
|
|
205
|
+
* Surface the most frequently discussed topics across recent observations,
|
|
206
|
+
* weighted by recency (recent = higher weight).
|
|
207
|
+
*
|
|
208
|
+
* @returns {object[]} array of { term, weightedFrequency, recentCount, totalCount }
|
|
209
|
+
*/
|
|
210
|
+
export function detectHotTopics(rootDir = process.cwd()) {
|
|
211
|
+
const all = listObservations(rootDir, {});
|
|
212
|
+
if (all.length === 0) return [];
|
|
213
|
+
|
|
214
|
+
const STOPWORDS = new Set([
|
|
215
|
+
'the', 'and', 'for', 'are', 'was', 'with', 'this', 'that', 'from',
|
|
216
|
+
'have', 'has', 'had', 'not', 'but', 'been', 'more', 'when', 'will',
|
|
217
|
+
'its', 'use', 'used', 'using', 'should', 'can', 'all', 'any', 'each',
|
|
218
|
+
'into', 'also', 'than', 'they', 'their', 'which', 'where', 'there',
|
|
219
|
+
]);
|
|
220
|
+
|
|
221
|
+
const termStats = new Map();
|
|
222
|
+
|
|
223
|
+
for (const e of all) {
|
|
224
|
+
const full = getObservation(rootDir, e.id);
|
|
225
|
+
const text = `${full?.summary || ''} ${full?.content || ''}`;
|
|
226
|
+
const weight = recencyWeight(e.createdAt);
|
|
227
|
+
const isRecent = daysSince(e.createdAt) <= ESCALATION_WINDOW_DAYS;
|
|
228
|
+
const terms = [...new Set(tokenize(text).filter((t) => t.length >= 4 && !STOPWORDS.has(t)))];
|
|
229
|
+
for (const term of terms) {
|
|
230
|
+
const prev = termStats.get(term) || { weightedFrequency: 0, recentCount: 0, totalCount: 0 };
|
|
231
|
+
termStats.set(term, {
|
|
232
|
+
weightedFrequency: prev.weightedFrequency + weight,
|
|
233
|
+
recentCount: prev.recentCount + (isRecent ? 1 : 0),
|
|
234
|
+
totalCount: prev.totalCount + 1,
|
|
235
|
+
});
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
return [...termStats.entries()]
|
|
240
|
+
.map(([term, stats]) => ({ term, ...stats }))
|
|
241
|
+
.sort((a, b) => b.weightedFrequency - a.weightedFrequency)
|
|
242
|
+
.slice(0, HOT_TOPICS_LIMIT);
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
// ── Full trend report ──────────────────────────────────────────────────────
|
|
246
|
+
|
|
247
|
+
/**
|
|
248
|
+
* Run all detectors and return a consolidated trend report.
|
|
249
|
+
*
|
|
250
|
+
* @param {string} rootDir
|
|
251
|
+
* @returns {{ recurringPatterns, escalatingRisks, decisionDrift, hotTopics, generatedAt }}
|
|
252
|
+
*/
|
|
253
|
+
export function buildTrendReport(rootDir = process.cwd()) {
|
|
254
|
+
return {
|
|
255
|
+
recurringPatterns: detectRecurringPatterns(rootDir),
|
|
256
|
+
escalatingRisks: detectEscalatingRisks(rootDir),
|
|
257
|
+
decisionDrift: detectDecisionDrift(rootDir),
|
|
258
|
+
hotTopics: detectHotTopics(rootDir),
|
|
259
|
+
generatedAt: new Date().toISOString(),
|
|
260
|
+
};
|
|
261
|
+
}
|
package/lib/logger.mjs
ADDED
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* lib/logger.mjs — structured (JSON-line) logger for dashboard + server code.
|
|
3
|
+
*
|
|
4
|
+
* The CLI continues to use `println`/`errorln` for human-facing output. This
|
|
5
|
+
* logger is for non-interactive surfaces (HTTP server, embed daemon,
|
|
6
|
+
* supervised processes) where logs flow to a tail of stderr and a CloudWatch
|
|
7
|
+
* agent or `construct logs` reads them.
|
|
8
|
+
*
|
|
9
|
+
* Format: one JSON object per line, on stderr. Fields:
|
|
10
|
+
*
|
|
11
|
+
* ts ISO timestamp
|
|
12
|
+
* level 'debug' | 'info' | 'warn' | 'error'
|
|
13
|
+
* event short stable identifier (e.g. 'http.request', 'auth.fail')
|
|
14
|
+
* req_id correlation id (defaults to a fresh hex string)
|
|
15
|
+
* route request path (when applicable)
|
|
16
|
+
* actor token label / user identity (when authenticated)
|
|
17
|
+
* latency_ms request duration (server only)
|
|
18
|
+
* detail arbitrary subobject for context
|
|
19
|
+
*
|
|
20
|
+
* Operators set `CONSTRUCT_LOG_LEVEL=debug` to see verbose lines; default
|
|
21
|
+
* level is `info`. `CONSTRUCT_LOG_PRETTY=1` switches to a human-readable
|
|
22
|
+
* format for local dev. Otherwise output is strictly JSONL.
|
|
23
|
+
*/
|
|
24
|
+
|
|
25
|
+
import { randomBytes } from 'node:crypto';
|
|
26
|
+
|
|
27
|
+
const LEVELS = Object.freeze({ debug: 10, info: 20, warn: 30, error: 40 });
|
|
28
|
+
|
|
29
|
+
function thresholdFromEnv(env) {
|
|
30
|
+
const v = (env.CONSTRUCT_LOG_LEVEL || 'info').toLowerCase();
|
|
31
|
+
return LEVELS[v] ?? LEVELS.info;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function isPretty(env) {
|
|
35
|
+
return env.CONSTRUCT_LOG_PRETTY === '1' || env.CONSTRUCT_LOG_PRETTY === 'true';
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export function newRequestId() {
|
|
39
|
+
return randomBytes(8).toString('hex');
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export function makeLogger({ env = process.env, stream = process.stderr } = {}) {
|
|
43
|
+
const threshold = thresholdFromEnv(env);
|
|
44
|
+
const pretty = isPretty(env);
|
|
45
|
+
|
|
46
|
+
function emit(level, event, fields = {}) {
|
|
47
|
+
if (LEVELS[level] < threshold) return;
|
|
48
|
+
const record = {
|
|
49
|
+
ts: new Date().toISOString(),
|
|
50
|
+
level,
|
|
51
|
+
event,
|
|
52
|
+
...fields,
|
|
53
|
+
};
|
|
54
|
+
const line = pretty
|
|
55
|
+
? `[${record.ts}] ${level.padEnd(5)} ${event}${
|
|
56
|
+
Object.keys(fields).length ? ' ' + JSON.stringify(fields) : ''
|
|
57
|
+
}\n`
|
|
58
|
+
: JSON.stringify(record) + '\n';
|
|
59
|
+
try { stream.write(line); } catch { /* logging is best-effort */ }
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
return {
|
|
63
|
+
debug: (event, fields) => emit('debug', event, fields),
|
|
64
|
+
info: (event, fields) => emit('info', event, fields),
|
|
65
|
+
warn: (event, fields) => emit('warn', event, fields),
|
|
66
|
+
error: (event, fields) => emit('error', event, fields),
|
|
67
|
+
child(extra = {}) {
|
|
68
|
+
return makeLogger.scoped(this, extra);
|
|
69
|
+
},
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
makeLogger.scoped = (parent, extra) => ({
|
|
74
|
+
debug: (event, fields = {}) => parent.debug(event, { ...extra, ...fields }),
|
|
75
|
+
info: (event, fields = {}) => parent.info(event, { ...extra, ...fields }),
|
|
76
|
+
warn: (event, fields = {}) => parent.warn(event, { ...extra, ...fields }),
|
|
77
|
+
error: (event, fields = {}) => parent.error(event, { ...extra, ...fields }),
|
|
78
|
+
child(more = {}) { return makeLogger.scoped(parent, { ...extra, ...more }); },
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
let defaultLogger = null;
|
|
82
|
+
export function logger() {
|
|
83
|
+
if (!defaultLogger) defaultLogger = makeLogger();
|
|
84
|
+
return defaultLogger;
|
|
85
|
+
}
|