@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.
Files changed (471) hide show
  1. package/.env.example +10 -7
  2. package/LICENSE +201 -21
  3. package/README.md +132 -257
  4. package/agents/contracts.json +387 -0
  5. package/agents/prompts/cx-accessibility.md +8 -0
  6. package/agents/prompts/cx-ai-engineer.md +92 -0
  7. package/agents/prompts/cx-architect.md +10 -2
  8. package/agents/prompts/cx-business-strategist.md +13 -0
  9. package/agents/prompts/cx-data-analyst.md +80 -0
  10. package/agents/prompts/cx-data-engineer.md +4 -0
  11. package/agents/prompts/cx-debugger.md +8 -0
  12. package/agents/prompts/cx-designer.md +19 -0
  13. package/agents/prompts/cx-devil-advocate.md +4 -0
  14. package/agents/prompts/cx-docs-keeper.md +128 -0
  15. package/agents/prompts/cx-engineer.md +13 -0
  16. package/agents/prompts/cx-evaluator.md +4 -0
  17. package/agents/prompts/cx-explorer.md +12 -0
  18. package/agents/prompts/cx-legal-compliance.md +13 -0
  19. package/agents/prompts/cx-operations.md +13 -0
  20. package/agents/prompts/cx-orchestrator.md +107 -4
  21. package/agents/prompts/cx-platform-engineer.md +75 -0
  22. package/agents/prompts/cx-product-manager.md +8 -0
  23. package/agents/prompts/cx-qa.md +100 -0
  24. package/agents/prompts/cx-rd-lead.md +13 -0
  25. package/agents/prompts/cx-release-manager.md +8 -0
  26. package/agents/prompts/cx-researcher.md +21 -1
  27. package/agents/prompts/cx-reviewer.md +8 -0
  28. package/agents/prompts/cx-security.md +104 -0
  29. package/agents/prompts/cx-sre.md +104 -0
  30. package/agents/prompts/cx-test-automation.md +4 -0
  31. package/agents/prompts/cx-trace-reviewer.md +7 -3
  32. package/agents/prompts/cx-ux-researcher.md +4 -0
  33. package/agents/registry.json +365 -90
  34. package/agents/role-manifests.json +217 -0
  35. package/bin/construct +3345 -141
  36. package/bin/construct-postinstall.mjs +87 -0
  37. package/commands/build/feature.md +6 -6
  38. package/commands/plan/feature.md +6 -5
  39. package/commands/plan/requirements.md +1 -1
  40. package/commands/ship/status.md +1 -1
  41. package/commands/work/drive.md +3 -3
  42. package/commands/work/optimize-prompts.md +3 -3
  43. package/db/{migrations → schema}/001_init.sql +2 -0
  44. package/db/schema/002_pgvector.sql +182 -0
  45. package/db/schema/003_intake.sql +47 -0
  46. package/examples/README.md +85 -0
  47. package/examples/internal/roles/architect/bad/clever-plan-without-contracts.md +30 -0
  48. package/examples/internal/roles/architect/golden/explicit-tradeoff-before-plan.md +23 -0
  49. package/examples/internal/roles/engineer/bad/speculative-abstraction.md +30 -0
  50. package/examples/internal/roles/engineer/golden/read-before-write.md +28 -0
  51. package/examples/internal/roles/orchestrator/bad/everything-becomes-multi-agent.md +29 -0
  52. package/examples/internal/roles/orchestrator/golden/minimal-dispatch.md +22 -0
  53. package/examples/internal/roles/qa/bad/coverage-theater.md +30 -0
  54. package/examples/internal/roles/qa/golden/regression-gate.md +23 -0
  55. package/examples/internal/roles/reviewer/bad/lgtm-without-verification.md +29 -0
  56. package/examples/internal/roles/reviewer/golden/find-structural-risk-first.md +28 -0
  57. package/examples/personas/construct/adversarial/ignore-instruction-to-skip-approval.md +22 -0
  58. package/examples/personas/construct/bad/commit-without-approval.md +30 -0
  59. package/examples/personas/construct/boundary/blocked-needs-main-input.md +29 -0
  60. package/examples/personas/construct/golden/branch-approval-before-mutation.md +36 -0
  61. package/examples/personas/construct/golden/focused-direct-answer.md +28 -0
  62. package/examples/provider-plugin/README.md +34 -0
  63. package/examples/provider-plugin/index.mjs +74 -0
  64. package/examples/provider-plugin/package.json +15 -0
  65. package/examples/seed-observations/README.md +38 -0
  66. package/examples/seed-observations/anti-patterns.md +44 -0
  67. package/examples/seed-observations/decisions.md +36 -0
  68. package/examples/seed-observations/patterns.md +42 -0
  69. package/lib/agent-contracts-enforce.mjs +158 -0
  70. package/lib/agent-contracts.mjs +231 -0
  71. package/lib/agents/postconditions.mjs +126 -0
  72. package/lib/agents/schema.mjs +124 -0
  73. package/lib/artifact-capture.mjs +183 -0
  74. package/lib/audit-trail.mjs +149 -0
  75. package/lib/auto-docs.mjs +245 -65
  76. package/lib/beads/auto-close.mjs +126 -0
  77. package/lib/beads/drift.mjs +171 -0
  78. package/lib/beads-automation.mjs +542 -0
  79. package/lib/beads-client.mjs +518 -0
  80. package/lib/beads-lock.mjs +377 -0
  81. package/lib/beads-optimistic.mjs +365 -0
  82. package/lib/bootstrap/built-ins.mjs +136 -0
  83. package/lib/bootstrap/lazy-install.mjs +161 -0
  84. package/lib/bootstrap/resources.mjs +120 -0
  85. package/lib/bootstrap.mjs +105 -0
  86. package/lib/cache-governor.js +213 -0
  87. package/lib/cache-strategy-anthropic.js +62 -0
  88. package/lib/cache-strategy-google.js +79 -0
  89. package/lib/cache-strategy-none.js +30 -0
  90. package/lib/cache-strategy-openai.js +48 -0
  91. package/lib/cache-strategy.js +91 -0
  92. package/lib/claude-allow.mjs +149 -0
  93. package/lib/cli-commands.mjs +413 -258
  94. package/lib/codex-config.mjs +3 -1
  95. package/lib/comment-lint.mjs +55 -6
  96. package/lib/completions.mjs +21 -6
  97. package/lib/config/alias.mjs +56 -0
  98. package/lib/config/project-config.mjs +335 -0
  99. package/lib/config/schema.mjs +159 -0
  100. package/lib/context-router.mjs +308 -0
  101. package/lib/cost-ledger.mjs +177 -0
  102. package/lib/cost.mjs +171 -10
  103. package/lib/dashboard-static.mjs +158 -0
  104. package/lib/deployment-mode.mjs +86 -0
  105. package/lib/deprecate.mjs +49 -0
  106. package/lib/dispatch-batch.js +183 -0
  107. package/lib/distill.mjs +21 -8
  108. package/lib/doc-stamp.mjs +164 -0
  109. package/lib/doc-verify.mjs +119 -0
  110. package/lib/docs-routing.mjs +89 -0
  111. package/lib/docs-verify.mjs +417 -0
  112. package/lib/doctor/audit.mjs +71 -0
  113. package/lib/doctor/cli.mjs +99 -0
  114. package/lib/doctor/escalate.mjs +29 -0
  115. package/lib/doctor/index.mjs +140 -0
  116. package/lib/doctor/report.mjs +170 -0
  117. package/lib/doctor/watchers/bd-watch.mjs +117 -0
  118. package/lib/doctor/watchers/cost.mjs +130 -0
  119. package/lib/doctor/watchers/disk.mjs +122 -0
  120. package/lib/doctor/watchers/handoffs.mjs +33 -0
  121. package/lib/doctor/watchers/process-pressure.mjs +60 -0
  122. package/lib/doctor/watchers/service-health.mjs +188 -0
  123. package/lib/document-extract.mjs +288 -0
  124. package/lib/document-ingest.mjs +230 -0
  125. package/lib/drop.mjs +282 -0
  126. package/lib/embed/approval-queue.mjs +176 -0
  127. package/lib/embed/artifact.mjs +349 -0
  128. package/lib/embed/authority-guard.mjs +155 -0
  129. package/lib/embed/cli.mjs +408 -0
  130. package/lib/embed/config.mjs +355 -0
  131. package/lib/embed/conflict-detection.mjs +264 -0
  132. package/lib/embed/customer-profiles.mjs +480 -0
  133. package/lib/embed/daemon.mjs +1309 -0
  134. package/lib/embed/demand-fetch.mjs +449 -0
  135. package/lib/embed/docs-lifecycle.mjs +349 -0
  136. package/lib/embed/inbox-live-watcher.mjs +119 -0
  137. package/lib/embed/inbox.mjs +343 -0
  138. package/lib/embed/intake-metrics.mjs +190 -0
  139. package/lib/embed/jobs/vector-sync.mjs +198 -0
  140. package/lib/embed/notifications.mjs +75 -0
  141. package/lib/embed/output.mjs +79 -0
  142. package/lib/embed/providers/github.mjs +295 -0
  143. package/lib/embed/providers/jira.mjs +192 -0
  144. package/lib/embed/providers/linear.mjs +186 -0
  145. package/lib/embed/providers/registry.mjs +115 -0
  146. package/lib/embed/providers/slack.mjs +203 -0
  147. package/lib/embed/recommendation-store.mjs +378 -0
  148. package/lib/embed/roadmap.mjs +374 -0
  149. package/lib/embed/role-framing.mjs +110 -0
  150. package/lib/embed/scheduler.mjs +99 -0
  151. package/lib/embed/semantic.mjs +325 -0
  152. package/lib/embed/snapshot.mjs +191 -0
  153. package/lib/embed/supervision.mjs +235 -0
  154. package/lib/embed/target-resolver.mjs +186 -0
  155. package/lib/embed/worker.mjs +62 -0
  156. package/lib/embed/workspaces.mjs +297 -0
  157. package/lib/engine/chunker-headings.mjs +110 -0
  158. package/lib/engine/compressor-heuristic.mjs +100 -0
  159. package/lib/engine/consolidate.mjs +287 -0
  160. package/lib/engine/contracts.mjs +126 -0
  161. package/lib/engine/defaults.mjs +129 -0
  162. package/lib/engine/eval-retrieval.mjs +148 -0
  163. package/lib/engine/fuser-rrf.mjs +62 -0
  164. package/lib/engine/index.mjs +37 -0
  165. package/lib/engine/registry.mjs +146 -0
  166. package/lib/engine/reranker-mmr.mjs +90 -0
  167. package/lib/engine/tokens.mjs +77 -0
  168. package/lib/entity-store.mjs +280 -0
  169. package/lib/env-config.mjs +69 -4
  170. package/lib/evals/retrieval-bench.mjs +159 -0
  171. package/lib/evaluator-optimizer.mjs +317 -0
  172. package/lib/features.mjs +159 -29
  173. package/lib/gates-audit.mjs +236 -0
  174. package/lib/git-hooks/prepare-commit-msg +58 -0
  175. package/lib/handoffs/cleanup.mjs +159 -0
  176. package/lib/handoffs/contract.mjs +162 -0
  177. package/lib/handoffs/inventory.mjs +120 -0
  178. package/lib/headhunt.mjs +28 -47
  179. package/lib/health-check.mjs +399 -0
  180. package/lib/hook-health.mjs +442 -0
  181. package/lib/hooks/_lib/log.mjs +82 -0
  182. package/lib/hooks/adaptive-lint.mjs +26 -2
  183. package/lib/hooks/agent-tracker.mjs +171 -14
  184. package/lib/hooks/audit-reads.mjs +109 -0
  185. package/lib/hooks/audit-trail.mjs +153 -0
  186. package/lib/hooks/bash-output-logger.mjs +71 -0
  187. package/lib/hooks/block-no-verify.mjs +41 -0
  188. package/lib/hooks/ci-status-check.mjs +82 -0
  189. package/lib/hooks/comment-lint.mjs +29 -7
  190. package/lib/hooks/config-protection.mjs +39 -18
  191. package/lib/hooks/context-watch.mjs +137 -0
  192. package/lib/hooks/context-window-recovery.mjs +4 -20
  193. package/lib/hooks/dep-audit.mjs +16 -0
  194. package/lib/hooks/doc-coupling-check.mjs +80 -0
  195. package/lib/hooks/edit-accumulator.mjs +3 -0
  196. package/lib/hooks/edit-error-recovery.mjs +3 -0
  197. package/lib/hooks/edit-guard.mjs +45 -4
  198. package/lib/hooks/env-check.mjs +3 -0
  199. package/lib/hooks/guard-bash.mjs +58 -1
  200. package/lib/hooks/mcp-audit.mjs +18 -20
  201. package/lib/hooks/mcp-health-check.mjs +36 -0
  202. package/lib/hooks/model-fallback.mjs +42 -56
  203. package/lib/hooks/policy-engine.mjs +209 -0
  204. package/lib/hooks/post-merge-docs-check.mjs +63 -0
  205. package/lib/hooks/pre-compact.mjs +4 -24
  206. package/lib/hooks/pre-push-gate.mjs +200 -37
  207. package/lib/hooks/proactive-activation.mjs +284 -0
  208. package/lib/hooks/read-tracker.mjs +27 -4
  209. package/lib/hooks/readme-age-check.mjs +77 -0
  210. package/lib/hooks/registry-sync.mjs +12 -5
  211. package/lib/hooks/scan-secrets.mjs +50 -7
  212. package/lib/hooks/session-optimize.mjs +311 -0
  213. package/lib/hooks/session-start.mjs +287 -28
  214. package/lib/hooks/stop-notify.mjs +236 -96
  215. package/lib/hooks/stop-typecheck.mjs +13 -1
  216. package/lib/hooks/test-watch.mjs +68 -0
  217. package/lib/host-capabilities.mjs +31 -10
  218. package/lib/init-docs.mjs +731 -291
  219. package/lib/init-unified.mjs +1116 -0
  220. package/lib/init-update.mjs +168 -0
  221. package/lib/init.mjs +107 -0
  222. package/lib/install/first-invocation.mjs +119 -0
  223. package/lib/install/stage-project.mjs +69 -0
  224. package/lib/intake/classify.mjs +278 -0
  225. package/lib/intake/feedback.mjs +273 -0
  226. package/lib/intake/filesystem-queue.mjs +158 -0
  227. package/lib/intake/intake-config.mjs +132 -0
  228. package/lib/intake/postgres-queue.mjs +198 -0
  229. package/lib/intake/prepare.mjs +139 -0
  230. package/lib/intake/queue.mjs +83 -0
  231. package/lib/intake/session-prelude.mjs +50 -0
  232. package/lib/integrations/intake-integrations.mjs +740 -0
  233. package/lib/intent-classifier.mjs +253 -0
  234. package/lib/knowledge/layout.mjs +72 -0
  235. package/lib/knowledge/rag.mjs +331 -0
  236. package/lib/knowledge/search.mjs +315 -0
  237. package/lib/knowledge/trends.mjs +261 -0
  238. package/lib/logger.mjs +85 -0
  239. package/lib/mcp/broker.mjs +124 -0
  240. package/lib/mcp/server.mjs +554 -854
  241. package/lib/mcp/tools/document.mjs +132 -0
  242. package/lib/mcp/tools/memory.mjs +209 -0
  243. package/lib/mcp/tools/project.mjs +349 -0
  244. package/lib/mcp/tools/skills.mjs +409 -0
  245. package/lib/mcp/tools/storage.mjs +60 -0
  246. package/lib/mcp/tools/telemetry.mjs +315 -0
  247. package/lib/mcp/tools/workflow.mjs +112 -0
  248. package/lib/mcp-catalog.json +54 -3
  249. package/lib/mcp-manager.mjs +96 -48
  250. package/lib/mcp-platform-config.mjs +22 -22
  251. package/lib/memory-stats.mjs +121 -0
  252. package/lib/mode-commands.mjs +124 -0
  253. package/lib/model-free-selector.mjs +184 -0
  254. package/lib/model-pricing.mjs +152 -0
  255. package/lib/model-registry.mjs +226 -0
  256. package/lib/model-router.mjs +362 -386
  257. package/lib/observation-store.mjs +391 -0
  258. package/lib/ollama-manager.mjs +428 -0
  259. package/lib/opencode-config.mjs +13 -3
  260. package/lib/opencode-runtime-plugin.mjs +70 -38
  261. package/lib/opencode-telemetry.mjs +64 -6
  262. package/lib/orchestration-policy.mjs +509 -6
  263. package/lib/overrides/resolver.mjs +207 -0
  264. package/lib/parity.mjs +147 -0
  265. package/lib/paths.mjs +33 -0
  266. package/lib/performance/generate.mjs +212 -0
  267. package/lib/plugin-registry.mjs +268 -0
  268. package/lib/policy/engine.mjs +130 -0
  269. package/lib/policy/unified-gates.mjs +96 -0
  270. package/lib/project-detection.mjs +129 -0
  271. package/lib/project-init-shared.mjs +272 -0
  272. package/lib/project-profile.mjs +447 -0
  273. package/lib/prompt-composer.js +434 -0
  274. package/lib/prompt-metadata.mjs +1 -1
  275. package/lib/provider-capabilities-anthropic.js +44 -0
  276. package/lib/provider-capabilities-deepseek.js +37 -0
  277. package/lib/provider-capabilities-generic.js +26 -0
  278. package/lib/provider-capabilities-google.js +47 -0
  279. package/lib/provider-capabilities-openai.js +45 -0
  280. package/lib/provider-capabilities.js +142 -0
  281. package/lib/providers/atlassian-confluence/index.mjs +103 -0
  282. package/lib/providers/atlassian-jira/index.mjs +100 -0
  283. package/lib/providers/auth-manager.mjs +126 -0
  284. package/lib/providers/circuit-breaker.mjs +124 -0
  285. package/lib/providers/contract.mjs +93 -0
  286. package/lib/providers/github/index.mjs +126 -0
  287. package/lib/providers/registry.mjs +184 -0
  288. package/lib/providers/salesforce/index.mjs +100 -0
  289. package/lib/providers/slack/index.mjs +80 -0
  290. package/lib/reflect.mjs +137 -0
  291. package/lib/research-lint.mjs +164 -0
  292. package/lib/resources/budget.mjs +259 -0
  293. package/lib/role-preload.mjs +21 -6
  294. package/lib/roles/approval-surface.mjs +54 -0
  295. package/lib/roles/cli.mjs +118 -0
  296. package/lib/roles/event-bus.mjs +79 -0
  297. package/lib/roles/fence.mjs +84 -0
  298. package/lib/roles/gateway.mjs +260 -0
  299. package/lib/roles/hook-emit.mjs +37 -0
  300. package/lib/roles/manifest.mjs +48 -0
  301. package/lib/roles/router.mjs +27 -0
  302. package/lib/runtime-pressure.mjs +360 -0
  303. package/lib/schema-artifact.mjs +134 -0
  304. package/lib/schema-infer.mjs +551 -0
  305. package/lib/server/auth.mjs +168 -0
  306. package/lib/server/chat.mjs +336 -0
  307. package/lib/server/cors.mjs +77 -0
  308. package/lib/server/csrf.mjs +91 -0
  309. package/lib/server/index.mjs +1927 -78
  310. package/lib/server/insights.mjs +765 -0
  311. package/lib/server/rate-limit.mjs +91 -0
  312. package/lib/server/static/assets/index-ab25c707.js +70 -0
  313. package/lib/server/static/assets/index-f0c80a2b.css +1 -0
  314. package/lib/server/static/index.html +12 -817
  315. package/lib/server/telemetry-login.mjs +108 -0
  316. package/lib/server/webhook.mjs +510 -0
  317. package/lib/service-manager.mjs +522 -58
  318. package/lib/services/pattern-promotion-service.mjs +167 -0
  319. package/lib/services/telemetry-backend.mjs +178 -0
  320. package/lib/session-store.mjs +374 -0
  321. package/lib/setup-prompts.mjs +96 -0
  322. package/lib/setup.mjs +525 -38
  323. package/lib/skills-apply.mjs +280 -0
  324. package/lib/skills-scope.mjs +118 -0
  325. package/lib/status.mjs +261 -70
  326. package/lib/storage/admin.mjs +355 -0
  327. package/lib/storage/backend.mjs +2 -1
  328. package/lib/storage/backup.mjs +347 -0
  329. package/lib/storage/embeddings-engine.mjs +133 -0
  330. package/lib/storage/embeddings-legacy.mjs +85 -0
  331. package/lib/storage/embeddings-local.mjs +108 -0
  332. package/lib/storage/embeddings-ollama.mjs +78 -0
  333. package/lib/storage/embeddings-openai.mjs +85 -0
  334. package/lib/storage/embeddings.mjs +92 -33
  335. package/lib/storage/file-lock.mjs +130 -0
  336. package/lib/storage/fusion.mjs +95 -0
  337. package/lib/storage/hybrid-query.mjs +34 -27
  338. package/lib/storage/migrations.mjs +187 -0
  339. package/lib/storage/postgres-backup.mjs +124 -0
  340. package/lib/storage/sql-store.mjs +5 -15
  341. package/lib/storage/state-source.mjs +12 -13
  342. package/lib/storage/store-version.mjs +115 -0
  343. package/lib/storage/sync.mjs +144 -35
  344. package/lib/storage/unified-storage.mjs +550 -0
  345. package/lib/storage/vector-client.mjs +286 -0
  346. package/lib/storage/vector-store.mjs +71 -30
  347. package/lib/task-graph/generate.mjs +135 -0
  348. package/lib/task-graph/schema.mjs +81 -0
  349. package/lib/task-graph/store.mjs +71 -0
  350. package/lib/telemetry/backends/local.mjs +62 -0
  351. package/lib/telemetry/backends/{langfuse.mjs → remote.mjs} +27 -14
  352. package/lib/telemetry/backfill.mjs +180 -0
  353. package/lib/telemetry/eval-datasets.mjs +203 -0
  354. package/lib/telemetry/{langfuse-ingest.mjs → ingest.mjs} +26 -20
  355. package/lib/telemetry/intent-verifications.mjs +86 -0
  356. package/lib/telemetry/llm-judge.mjs +350 -0
  357. package/lib/telemetry/model-pricing-catalog.mjs +557 -0
  358. package/lib/telemetry/setup.mjs +151 -0
  359. package/lib/telemetry/skill-calls.mjs +78 -0
  360. package/lib/telemetry/team-rollup.mjs +4 -4
  361. package/lib/token-engine.js +117 -0
  362. package/lib/token-estimator-anthropic.js +15 -0
  363. package/lib/token-estimator-deepseek.js +13 -0
  364. package/lib/token-estimator-default.js +13 -0
  365. package/lib/token-estimator-google.js +13 -0
  366. package/lib/token-estimator-openai.js +13 -0
  367. package/lib/toolkit-env.mjs +1 -1
  368. package/lib/tty-prompts.mjs +211 -0
  369. package/lib/uninstall/uninstall.mjs +423 -0
  370. package/lib/update.mjs +115 -0
  371. package/lib/upgrade.mjs +141 -0
  372. package/lib/validator.mjs +51 -2
  373. package/lib/validators/skills.mjs +142 -0
  374. package/lib/wireframe.mjs +422 -0
  375. package/lib/worker/entrypoint.mjs +241 -0
  376. package/lib/worker/evidence.mjs +107 -0
  377. package/lib/worker/run.mjs +154 -0
  378. package/lib/worker/trace.mjs +182 -0
  379. package/lib/workflow-state.mjs +14 -18
  380. package/package.json +21 -8
  381. package/personas/construct.md +53 -51
  382. package/platforms/claude/CLAUDE.md +1 -1
  383. package/platforms/claude/settings.template.json +115 -68
  384. package/platforms/opencode/config.template.json +3 -7
  385. package/rules/common/agents.md +11 -38
  386. package/rules/common/beads-hygiene.md +75 -0
  387. package/rules/common/code-review.md +11 -100
  388. package/rules/common/coding-style.md +5 -3
  389. package/rules/common/comments.md +30 -111
  390. package/rules/common/commit-approval.md +53 -0
  391. package/rules/common/cx-agent-routing.md +1 -0
  392. package/rules/common/development-workflow.md +23 -41
  393. package/rules/common/doc-ownership.md +54 -0
  394. package/rules/common/efficiency.md +57 -0
  395. package/rules/common/framing.md +76 -0
  396. package/rules/common/git-workflow.md +2 -4
  397. package/rules/common/patterns.md +3 -7
  398. package/rules/common/performance.md +15 -31
  399. package/rules/common/release-gates.md +69 -0
  400. package/rules/common/research.md +107 -0
  401. package/rules/common/security.md +6 -6
  402. package/rules/common/skill-composition.md +67 -0
  403. package/rules/common/testing.md +15 -27
  404. package/rules/golang/hooks.md +0 -4
  405. package/rules/policy/bootstrap.yaml +11 -0
  406. package/rules/policy/drive.yaml +8 -0
  407. package/rules/policy/task.yaml +9 -0
  408. package/rules/policy/workflow.yaml +9 -0
  409. package/rules/python/hooks.md +0 -4
  410. package/rules/swift/hooks.md +0 -4
  411. package/rules/typescript/hooks.md +0 -4
  412. package/rules/web/hooks.md +0 -2
  413. package/{sync-agents.mjs → scripts/sync-agents.mjs} +306 -61
  414. package/skills/ai/prompt-optimizer.md +7 -7
  415. package/skills/compliance/ai-disclosure.md +58 -0
  416. package/skills/compliance/data-privacy.md +46 -0
  417. package/skills/compliance/license-audit.md +40 -0
  418. package/skills/compliance/regulatory-review.md +61 -0
  419. package/skills/docs/document-ingest-workflow.md +52 -0
  420. package/skills/docs/evidence-ingest-workflow.md +9 -0
  421. package/skills/docs/init-docs.md +51 -18
  422. package/skills/docs/product-intelligence-workflow.md +12 -0
  423. package/skills/docs/product-signal-workflow.md +4 -0
  424. package/skills/docs/research-workflow.md +23 -8
  425. package/skills/docs/runbook-workflow.md +1 -1
  426. package/skills/operating/orchestration-reference.md +151 -0
  427. package/skills/roles/architect.md +5 -0
  428. package/skills/roles/engineer.md +32 -0
  429. package/skills/roles/operator.md +12 -0
  430. package/skills/roles/reviewer.md +23 -0
  431. package/skills/routing.md +1 -1
  432. package/templates/devcontainer/Dockerfile.devcontainer +38 -0
  433. package/templates/devcontainer/devcontainer.json +31 -0
  434. package/templates/distribution/bootstrap.ps1 +142 -0
  435. package/templates/distribution/bootstrap.sh +196 -0
  436. package/templates/distribution/run.mjs +187 -0
  437. package/templates/docs/adr.md +44 -8
  438. package/templates/docs/changelog-entry.md +43 -0
  439. package/templates/docs/construct_guide.md +149 -0
  440. package/templates/docs/evidence-brief.md +2 -2
  441. package/templates/docs/meta-prd.md +140 -19
  442. package/templates/docs/onboarding.md +57 -0
  443. package/templates/docs/prd.md +159 -15
  444. package/templates/docs/research-brief.md +4 -4
  445. package/templates/homebrew/construct.rb +67 -0
  446. package/langfuse/docker-compose.yml +0 -82
  447. package/lib/eval-harness.mjs +0 -59
  448. package/lib/hooks/bootstrap-guard.mjs +0 -90
  449. package/lib/hooks/console-warn.mjs +0 -43
  450. package/lib/hooks/continuation-enforcer.mjs +0 -72
  451. package/lib/hooks/drive-guard.mjs +0 -89
  452. package/lib/hooks/mcp-task-scope.mjs +0 -47
  453. package/lib/hooks/task-completed-guard.mjs +0 -43
  454. package/lib/hooks/teammate-idle-guard.mjs +0 -54
  455. package/lib/hooks/workflow-guard.mjs +0 -62
  456. package/lib/prompt-composer.mjs +0 -196
  457. package/lib/review.mjs +0 -429
  458. package/lib/server/static/app.js +0 -841
  459. package/lib/telemetry/langfuse-model-sync.mjs +0 -270
  460. package/rules/common/hooks.md +0 -35
  461. package/rules/zh/README.md +0 -113
  462. package/rules/zh/agents.md +0 -55
  463. package/rules/zh/code-review.md +0 -129
  464. package/rules/zh/coding-style.md +0 -53
  465. package/rules/zh/development-workflow.md +0 -49
  466. package/rules/zh/git-workflow.md +0 -29
  467. package/rules/zh/hooks.md +0 -35
  468. package/rules/zh/patterns.md +0 -36
  469. package/rules/zh/performance.md +0 -60
  470. package/rules/zh/security.md +0 -34
  471. 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
+ }