@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,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
+ }