@geraldmaron/construct 1.4.2 → 1.5.0

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 (519) hide show
  1. package/.env.example +36 -0
  2. package/README.md +41 -7
  3. package/bin/construct +1027 -64
  4. package/examples/distribution/sources/deck-one-pager.md +1 -1
  5. package/lib/acp/server.mjs +21 -7
  6. package/lib/adapters-sync.mjs +2 -1
  7. package/lib/audit-trail.mjs +185 -2
  8. package/lib/beads/auto-close.mjs +2 -1
  9. package/lib/beads/drift.mjs +2 -1
  10. package/lib/bridges/copilot-proxy.mjs +20 -5
  11. package/lib/certification/skill-inventory.mjs +10 -1
  12. package/lib/cli/approvals.mjs +147 -0
  13. package/lib/cli-commands.mjs +105 -14
  14. package/lib/comment-lint.mjs +127 -8
  15. package/lib/config/schema.mjs +6 -29
  16. package/lib/config/source-target-registry.mjs +70 -0
  17. package/lib/config/source-targets.mjs +179 -205
  18. package/lib/contracts/coverage.mjs +77 -0
  19. package/lib/contracts/validate.mjs +102 -13
  20. package/lib/contracts/violation-log.mjs +50 -4
  21. package/lib/db/migrate.mjs +69 -0
  22. package/lib/db/migrations/001_orchestration_runs.sql +9 -0
  23. package/lib/db/migrations/002_queue_provider.sql +50 -0
  24. package/lib/db/migrations/003_worker_registry.sql +17 -0
  25. package/lib/db/migrations/004_trace_events.sql +16 -0
  26. package/lib/db/migrations/005_shared_memory.sql +15 -0
  27. package/lib/db/migrations/006_orchestration_runs_tenant.sql +5 -0
  28. package/lib/decisions/enforced-baseline.json +0 -2
  29. package/lib/decisions/registry.mjs +3 -2
  30. package/lib/deployment/parity-contract.mjs +1 -1
  31. package/lib/deployment-mode.mjs +78 -2
  32. package/lib/diagram-export.mjs +10 -1
  33. package/lib/distill.mjs +5 -2
  34. package/lib/docs-verify.mjs +2 -1
  35. package/lib/doctor/cli.mjs +1 -0
  36. package/lib/doctor/command-on-path.mjs +24 -0
  37. package/lib/doctor/diagnosis.mjs +89 -0
  38. package/lib/doctor/embedding-health.mjs +50 -0
  39. package/lib/doctor/engine-health.mjs +93 -0
  40. package/lib/doctor/graph-validate.mjs +47 -0
  41. package/lib/doctor/index.mjs +18 -4
  42. package/lib/doctor/sidecar-providers.mjs +56 -0
  43. package/lib/doctor/watchers/consistency.mjs +93 -1
  44. package/lib/doctor/watchers/cx-budget.mjs +1 -1
  45. package/lib/doctor/watchers/graph-staleness.mjs +1 -1
  46. package/lib/doctor/watchers/mcp-protocol.mjs +17 -0
  47. package/lib/doctor/watchers/oracle-liveness.mjs +92 -0
  48. package/lib/doctor/watchers/orchestration-runs.mjs +108 -0
  49. package/lib/doctor/watchers/provider-breaker.mjs +78 -0
  50. package/lib/document-export.mjs +52 -27
  51. package/lib/document-extract/docling-client.mjs +9 -5
  52. package/lib/document-extract/docling-sidecar.py +30 -0
  53. package/lib/document-extract.mjs +1 -1
  54. package/lib/document-ingest.mjs +9 -0
  55. package/lib/embed/approval-queue.mjs +184 -88
  56. package/lib/embed/authority-guard.mjs +65 -2
  57. package/lib/embed/capability-jobs.mjs +365 -0
  58. package/lib/embed/capability-lifecycle.mjs +219 -0
  59. package/lib/embed/capability-loader.mjs +303 -0
  60. package/lib/embed/capability-runtime.mjs +58 -0
  61. package/lib/embed/cli.mjs +185 -8
  62. package/lib/embed/config.mjs +4 -0
  63. package/lib/embed/daemon.mjs +125 -6
  64. package/lib/embed/demand-fetch.mjs +190 -54
  65. package/lib/embed/inbox.mjs +12 -10
  66. package/lib/embed/presets/ops-triage.mjs +236 -0
  67. package/lib/embed/presets/pm-feedback.mjs +341 -0
  68. package/lib/embed/presets/tpm.mjs +401 -0
  69. package/lib/embed/providers/jira.mjs +72 -1
  70. package/lib/embed/providers/registry.mjs +140 -33
  71. package/lib/embedded-contract/capability.mjs +4 -0
  72. package/lib/embedded-contract/execution.mjs +1 -1
  73. package/lib/embedded-contract/model-resolve.mjs +53 -3
  74. package/lib/embedded-contract/workflow-defs.mjs +48 -73
  75. package/lib/embedded-contract/workflow-invoke.mjs +144 -4
  76. package/lib/embedded-contract/workflows/architecture-review.manifest.json +15 -0
  77. package/lib/embedded-contract/workflows/data-structure.manifest.json +14 -0
  78. package/lib/embedded-contract/workflows/evidence-ingest.manifest.json +14 -0
  79. package/lib/embedded-contract/workflows/memo-draft.manifest.json +14 -0
  80. package/lib/embedded-contract/workflows/operations-triage.manifest.json +18 -0
  81. package/lib/embedded-contract/workflows/operations.manifest.json +18 -0
  82. package/lib/embedded-contract/workflows/pm-feedback.manifest.json +18 -0
  83. package/lib/embedded-contract/workflows/prd-draft.manifest.json +15 -0
  84. package/lib/embedded-contract/workflows/proposal-review.manifest.json +15 -0
  85. package/lib/embedded-contract/workflows/research-synthesis.manifest.json +14 -0
  86. package/lib/embedded-contract/workflows/risk-review.manifest.json +15 -0
  87. package/lib/embedded-contract/workflows/structure-notes.manifest.json +14 -0
  88. package/lib/embedded-contract/workflows/transcript-process.manifest.json +14 -0
  89. package/lib/embedded-contract/workflows/triage.manifest.json +14 -0
  90. package/lib/env-config.mjs +50 -9
  91. package/lib/extensions/index.mjs +27 -0
  92. package/lib/extensions/loader.mjs +120 -0
  93. package/lib/extensions/manifest-schema.mjs +141 -0
  94. package/lib/extensions/manifests/anthropic.manifest.json +12 -0
  95. package/lib/extensions/manifests/atlassian-confluence.manifest.json +12 -0
  96. package/lib/extensions/manifests/atlassian-jira.manifest.json +53 -0
  97. package/lib/extensions/manifests/directory.manifest.json +12 -0
  98. package/lib/extensions/manifests/docling.manifest.json +37 -0
  99. package/lib/extensions/manifests/echo.manifest.json +8 -0
  100. package/lib/extensions/manifests/feedback.manifest.json +12 -0
  101. package/lib/extensions/manifests/github-copilot.manifest.json +12 -0
  102. package/lib/extensions/manifests/github.manifest.json +52 -0
  103. package/lib/extensions/manifests/linear.manifest.json +50 -0
  104. package/lib/extensions/manifests/local.manifest.json +12 -0
  105. package/lib/extensions/manifests/ollama.manifest.json +12 -0
  106. package/lib/extensions/manifests/openai.manifest.json +12 -0
  107. package/lib/extensions/manifests/openrouter-anthropic.manifest.json +12 -0
  108. package/lib/extensions/manifests/openrouter-deepseek.manifest.json +12 -0
  109. package/lib/extensions/manifests/openrouter-google.manifest.json +12 -0
  110. package/lib/extensions/manifests/openrouter-llama.manifest.json +12 -0
  111. package/lib/extensions/manifests/openrouter-qwen.manifest.json +12 -0
  112. package/lib/extensions/manifests/openrouter.manifest.json +12 -0
  113. package/lib/extensions/manifests/postgres.manifest.json +12 -0
  114. package/lib/extensions/manifests/salesforce.manifest.json +12 -0
  115. package/lib/extensions/manifests/slack.manifest.json +58 -0
  116. package/lib/extensions/manifests/whisper.manifest.json +36 -0
  117. package/lib/extensions/validate.mjs +175 -0
  118. package/lib/features.mjs +13 -9
  119. package/lib/flows/checkpoint.mjs +184 -0
  120. package/lib/flows/constants.mjs +27 -0
  121. package/lib/flows/define.mjs +146 -0
  122. package/lib/flows/engine.mjs +249 -0
  123. package/lib/flows/errors.mjs +19 -0
  124. package/lib/flows/index.mjs +27 -0
  125. package/lib/flows/joins.mjs +18 -0
  126. package/lib/flows/schema.mjs +90 -0
  127. package/lib/flows/state.mjs +31 -0
  128. package/lib/frameworks/loader.mjs +99 -0
  129. package/lib/frameworks/schema.mjs +157 -0
  130. package/lib/graph/build-from-corpus.mjs +67 -0
  131. package/lib/graph/build-from-embed.mjs +82 -0
  132. package/lib/graph/build-from-registry.mjs +164 -3
  133. package/lib/graph/cli.mjs +456 -12
  134. package/lib/graph/gap-queries.mjs +156 -0
  135. package/lib/graph/gaps.mjs +41 -0
  136. package/lib/graph/impacted.mjs +129 -0
  137. package/lib/graph/runtime-evidence.mjs +177 -0
  138. package/lib/graph/security-coverage.mjs +113 -0
  139. package/lib/graph/staleness.mjs +241 -10
  140. package/lib/graph/store.mjs +24 -7
  141. package/lib/graph/validate.mjs +161 -0
  142. package/lib/headhunt.mjs +9 -8
  143. package/lib/health-check.mjs +15 -7
  144. package/lib/hook-health.mjs +1 -1
  145. package/lib/hooks/agent-tracker.mjs +10 -4
  146. package/lib/hooks/edit-guard.mjs +3 -3
  147. package/lib/hooks/graph-impact-advisory.mjs +2 -2
  148. package/lib/hooks/guard-bash.mjs +41 -15
  149. package/lib/hooks/mcp-health-check.mjs +11 -4
  150. package/lib/hooks/model-fallback.mjs +50 -6
  151. package/lib/hooks/orchestration-dispatch-guard.mjs +14 -3
  152. package/lib/hooks/pre-compact.mjs +181 -173
  153. package/lib/hooks/rule-verifier.mjs +2 -1
  154. package/lib/hooks/session-reflect.mjs +2 -1
  155. package/lib/hooks/session-start.mjs +3 -3
  156. package/lib/host-capabilities.mjs +13 -2
  157. package/lib/host-disposition.mjs +19 -7
  158. package/lib/identity.mjs +92 -0
  159. package/lib/ingest/degraded-extract.mjs +96 -0
  160. package/lib/ingest/docling-remote.mjs +2 -1
  161. package/lib/ingest/provider-extract.mjs +7 -19
  162. package/lib/ingest/sidecar-providers.mjs +196 -0
  163. package/lib/ingest-tooling.mjs +11 -5
  164. package/lib/init-unified.mjs +55 -38
  165. package/lib/init-update.mjs +2 -1
  166. package/lib/install/legacy-global-cleanup.mjs +25 -0
  167. package/lib/intake/daemon.mjs +99 -8
  168. package/lib/intake/git-queue.mjs +110 -38
  169. package/lib/intake/queue-registry.mjs +50 -0
  170. package/lib/intake/queue.mjs +133 -17
  171. package/lib/intake/session-prelude.mjs +57 -8
  172. package/lib/integrations/intake-integrations.mjs +61 -111
  173. package/lib/intent-classifier.mjs +43 -5
  174. package/lib/libreoffice-export.mjs +8 -8
  175. package/lib/logging/rotate.mjs +5 -4
  176. package/lib/mcp/broker.mjs +332 -17
  177. package/lib/mcp/denied-store.mjs +95 -0
  178. package/lib/mcp/destructive-gate.mjs +30 -0
  179. package/lib/mcp/dispatch-envelope.mjs +199 -0
  180. package/lib/mcp/memory-bridge.mjs +2 -1
  181. package/lib/mcp/server.mjs +154 -1411
  182. package/lib/mcp/tool-definitions-memory.mjs +376 -0
  183. package/lib/mcp/tool-definitions-project.mjs +271 -0
  184. package/lib/mcp/tool-definitions-skills.mjs +311 -0
  185. package/lib/mcp/tool-definitions-workflow.mjs +398 -0
  186. package/lib/mcp/tool-definitions.mjs +25 -0
  187. package/lib/mcp/tool-registry.mjs +107 -0
  188. package/lib/mcp/tool-safety.mjs +1 -0
  189. package/lib/mcp/tools/orchestration-delegation-next.tool.mjs +68 -0
  190. package/lib/mcp/tools/orchestration-run.mjs +165 -25
  191. package/lib/mcp/tools/orchestration-task-result.tool.mjs +77 -0
  192. package/lib/mcp/tools/provider-write.mjs +187 -0
  193. package/lib/mcp/tools/scope.mjs +0 -3
  194. package/lib/mcp/tools/skills.mjs +1 -1
  195. package/lib/mcp/tools/storage.mjs +0 -8
  196. package/lib/mcp/transport/auth.mjs +238 -0
  197. package/lib/mcp/transport/http.mjs +136 -0
  198. package/lib/mcp/transport/mode.mjs +31 -0
  199. package/lib/mcp/transport/stdio.mjs +22 -0
  200. package/lib/mcp-catalog.json +4 -4
  201. package/lib/mcp-manager.mjs +34 -23
  202. package/lib/mcp-platform-config.mjs +31 -7
  203. package/lib/mode-capabilities.mjs +109 -0
  204. package/lib/model-router.mjs +42 -9
  205. package/lib/models/catalog.mjs +40 -1
  206. package/lib/net-guard.mjs +247 -0
  207. package/lib/observation-store.mjs +6 -2
  208. package/lib/ollama/provision-context.mjs +2 -1
  209. package/lib/opencode-config.mjs +22 -3
  210. package/lib/opencode-runtime-plugin.mjs +120 -2
  211. package/lib/oracle/actions.mjs +204 -10
  212. package/lib/oracle/cli.mjs +24 -3
  213. package/lib/oracle/dispatch.mjs +2 -1
  214. package/lib/oracle/execute.mjs +2 -2
  215. package/lib/oracle/gaps.mjs +3 -3
  216. package/lib/oracle/heartbeat.mjs +53 -0
  217. package/lib/oracle/read-model.mjs +69 -13
  218. package/lib/oracle/reconcile.mjs +3 -46
  219. package/lib/oracle/routing.mjs +37 -31
  220. package/lib/oracle/synthesize.mjs +1 -1
  221. package/lib/orchestration/classification.mjs +434 -0
  222. package/lib/orchestration/delegation-flow.mjs +132 -0
  223. package/lib/orchestration/flow-selection.mjs +418 -0
  224. package/lib/orchestration/gates.mjs +244 -0
  225. package/lib/orchestration/host-sampling.mjs +121 -0
  226. package/lib/orchestration/policy-constants.mjs +48 -0
  227. package/lib/orchestration/provider-outcome.mjs +197 -0
  228. package/lib/orchestration/readiness.mjs +114 -13
  229. package/lib/orchestration/run-store-postgres.mjs +32 -26
  230. package/lib/orchestration/run-store-sqlite.mjs +8 -14
  231. package/lib/orchestration/run-store.mjs +25 -12
  232. package/lib/orchestration/runtime.mjs +446 -39
  233. package/lib/orchestration/store.mjs +87 -12
  234. package/lib/orchestration/trace-store.mjs +125 -0
  235. package/lib/orchestration/web-capability.mjs +3 -2
  236. package/lib/orchestration/worker-runtime.mjs +162 -0
  237. package/lib/orchestration/worker.mjs +528 -89
  238. package/lib/orchestration-policy.mjs +67 -1111
  239. package/lib/packs/cli.mjs +144 -0
  240. package/lib/packs/core-pack.mjs +112 -0
  241. package/lib/packs/enablement.mjs +187 -0
  242. package/lib/packs/index.mjs +23 -0
  243. package/lib/packs/loader.mjs +176 -0
  244. package/lib/packs/manifest-schema.mjs +58 -0
  245. package/lib/packs/prompts.mjs +120 -0
  246. package/lib/packs/validate.mjs +208 -0
  247. package/lib/plugin-registry.mjs +4 -5
  248. package/lib/policy/audit-gate.mjs +42 -0
  249. package/lib/policy/consumption-budget.mjs +149 -0
  250. package/lib/policy/engine.mjs +81 -6
  251. package/lib/policy/role-authority.mjs +89 -0
  252. package/lib/prompt-composer.js +19 -3
  253. package/lib/providers/contract/adapters/confluence/governed-write.mjs +215 -0
  254. package/lib/providers/contract/adapters/confluence/manifest.json +17 -0
  255. package/lib/providers/contract/adapters/confluence/transport.mjs +135 -0
  256. package/lib/providers/contract/adapters/github/governed-write.mjs +121 -0
  257. package/lib/providers/contract/adapters/github/index.mjs +38 -3
  258. package/lib/providers/contract/adapters/jira/adf.mjs +151 -0
  259. package/lib/providers/contract/adapters/jira/createmeta.mjs +143 -0
  260. package/lib/providers/contract/adapters/jira/governed-write.mjs +182 -0
  261. package/lib/providers/contract/adapters/jira/manifest.json +17 -0
  262. package/lib/providers/contract/adapters/jira/transport.mjs +119 -0
  263. package/lib/providers/contract.mjs +207 -0
  264. package/lib/providers/credential-bootstrap.mjs +7 -4
  265. package/lib/providers/credential-sources.mjs +14 -2
  266. package/lib/providers/directory/index.mjs +261 -0
  267. package/lib/providers/feedback/index.mjs +392 -0
  268. package/lib/providers/filter-audit.mjs +68 -0
  269. package/lib/providers/filter-schema.mjs +54 -0
  270. package/lib/providers/github/index.mjs +31 -0
  271. package/lib/providers/instance-config.mjs +215 -0
  272. package/lib/providers/op-locate.mjs +96 -0
  273. package/lib/providers/op-run.mjs +64 -9
  274. package/lib/providers/registry.mjs +26 -3
  275. package/lib/providers/secret-audit-wiring.mjs +6 -0
  276. package/lib/providers/secret-resolver.mjs +240 -23
  277. package/lib/publish-template.mjs +6 -3
  278. package/lib/publish-tooling.mjs +4 -0
  279. package/lib/publish.mjs +32 -4
  280. package/lib/queue/pg-queue.mjs +395 -0
  281. package/lib/reflect.mjs +2 -1
  282. package/lib/registry/assemble.mjs +28 -2
  283. package/lib/registry/consolidation.mjs +8 -1
  284. package/lib/registry/custom-scaffold.mjs +200 -0
  285. package/lib/registry/custom-schema.mjs +137 -0
  286. package/lib/registry/loader.mjs +8 -3
  287. package/lib/registry/manifests/format-engines.default.json +15 -0
  288. package/lib/registry/manifests/surface-map.default.json +46 -0
  289. package/lib/registry/retired-paths.mjs +1 -0
  290. package/lib/registry/surface-map.mjs +100 -50
  291. package/lib/render-visual-check.mjs +240 -0
  292. package/lib/resources/budget.mjs +40 -17
  293. package/lib/roles/flavor-bindings.mjs +36 -14
  294. package/lib/roles/gateway.mjs +15 -8
  295. package/lib/roots.mjs +107 -0
  296. package/lib/runtime/uv-bootstrap.mjs +32 -6
  297. package/lib/runtime/whisper-bootstrap.mjs +11 -3
  298. package/lib/runtime-env.mjs +5 -2
  299. package/lib/scheduler/index.mjs +8 -20
  300. package/lib/schema-infer.mjs +31 -2
  301. package/lib/scopes/lifecycle.mjs +29 -25
  302. package/lib/scopes/loader.mjs +49 -12
  303. package/lib/scopes/teams.mjs +1 -1
  304. package/lib/security/ingest-boundary.mjs +80 -0
  305. package/lib/security/recall-wrapper.mjs +99 -0
  306. package/lib/security/trust.mjs +132 -0
  307. package/lib/service-manager.mjs +38 -19
  308. package/lib/setup.mjs +41 -23
  309. package/lib/skills-apply.mjs +4 -2
  310. package/lib/specialists/postconditions.mjs +2 -2
  311. package/lib/state-root.mjs +147 -0
  312. package/lib/status.mjs +597 -6
  313. package/lib/storage/admin.mjs +77 -5
  314. package/lib/storage/backend-registry.mjs +73 -0
  315. package/lib/storage/backend.mjs +63 -9
  316. package/lib/storage/embeddings-openai.mjs +12 -2
  317. package/lib/storage/hybrid-query.mjs +11 -3
  318. package/lib/storage/retrieval-hardening.mjs +384 -0
  319. package/lib/storage/shared-memory.mjs +158 -0
  320. package/lib/storage/vector-client.mjs +69 -2
  321. package/lib/team/health.mjs +72 -0
  322. package/lib/telemetry/backends/local.mjs +9 -3
  323. package/lib/telemetry/client.mjs +3 -7
  324. package/lib/template-registry.mjs +10 -12
  325. package/lib/tenant/context.mjs +82 -0
  326. package/lib/tenant/isolation.mjs +129 -0
  327. package/lib/test-corpus-inventory.mjs +103 -2
  328. package/lib/uninstall/uninstall.mjs +78 -12
  329. package/lib/validator.mjs +4 -6
  330. package/lib/worker/entrypoint.mjs +2 -1
  331. package/lib/worker/run.mjs +2 -2
  332. package/lib/worker/trace.mjs +5 -11
  333. package/lib/workflow-state.mjs +52 -8
  334. package/lib/workflows/liveness.mjs +203 -0
  335. package/lib/workflows/loader.mjs +177 -0
  336. package/lib/workflows/manifest-schema.mjs +71 -0
  337. package/lib/workflows/surface-parity.mjs +118 -0
  338. package/lib/workflows/validate.mjs +127 -0
  339. package/lib/writes/control-plane.mjs +140 -0
  340. package/lib/writes/envelope.mjs +206 -0
  341. package/lib/writes/sent-log.mjs +86 -0
  342. package/lib/writes/write-intent.mjs +109 -0
  343. package/package.json +16 -8
  344. package/personas/construct.md +12 -3
  345. package/registry/capabilities.json +143 -36
  346. package/rules/common/comments.md +1 -0
  347. package/schemas/project-config.schema.json +0 -12
  348. package/schemas/unified-registry.schema.json +2 -4
  349. package/scripts/sync-specialists.mjs +284 -81
  350. package/skills/docs/adr-workflow.md +1 -1
  351. package/skills/docs/codebase-research-workflow.md +3 -3
  352. package/skills/docs/prd-workflow.md +5 -6
  353. package/skills/docs/runbook-workflow.md +2 -2
  354. package/skills/docs/user-research-workflow.md +3 -3
  355. package/skills/operating/fleet-health-routing.md +77 -0
  356. package/skills/roles/ai-engineer.md +1 -1
  357. package/skills/roles/architect.md +0 -1
  358. package/skills/roles/business-strategist.md +1 -1
  359. package/skills/roles/data-analyst.telemetry.md +1 -1
  360. package/skills/roles/data-engineer.md +1 -1
  361. package/skills/roles/data-engineer.pipeline.md +1 -1
  362. package/skills/roles/data-engineer.vector-retrieval.md +1 -2
  363. package/skills/roles/data-engineer.warehouse.md +1 -1
  364. package/skills/roles/designer.accessibility.md +1 -1
  365. package/skills/roles/designer.md +0 -1
  366. package/skills/roles/devil-advocate.md +1 -1
  367. package/skills/roles/docs-keeper.md +1 -1
  368. package/skills/roles/evaluator.md +1 -1
  369. package/skills/roles/explorer.md +1 -1
  370. package/skills/roles/platform-engineer.md +1 -1
  371. package/skills/roles/qa.ai-eval.md +1 -2
  372. package/skills/roles/qa.api-contract.md +0 -1
  373. package/skills/roles/qa.data-pipeline.md +0 -1
  374. package/skills/roles/qa.md +0 -1
  375. package/skills/roles/qa.web-ui.md +0 -1
  376. package/skills/roles/release-manager.md +1 -1
  377. package/skills/roles/researcher.md +0 -2
  378. package/skills/roles/reviewer.md +0 -3
  379. package/skills/roles/security.ai.md +1 -1
  380. package/skills/roles/security.legal-compliance.md +1 -1
  381. package/skills/roles/security.md +0 -1
  382. package/skills/roles/security.privacy.md +0 -1
  383. package/skills/roles/security.supply-chain.md +1 -1
  384. package/skills/roles/sre.md +1 -1
  385. package/skills/roles/test-automation.md +1 -1
  386. package/skills/roles/trace-reviewer.md +1 -1
  387. package/skills/roles/ux-researcher.md +1 -1
  388. package/specialists/artifact-manifest.json +36 -36
  389. package/specialists/org/contracts/accessibility-to-qa.json +11 -3
  390. package/specialists/org/contracts/any-to-business-strategist.json +19 -7
  391. package/specialists/org/contracts/any-to-debugger.json +7 -1
  392. package/specialists/org/contracts/any-to-designer.json +7 -1
  393. package/specialists/org/contracts/any-to-docs-keeper.json +18 -4
  394. package/specialists/org/contracts/any-to-explorer.json +13 -4
  395. package/specialists/org/contracts/any-to-sre-incident.json +6 -2
  396. package/specialists/org/contracts/any-to-trace-reviewer.json +16 -4
  397. package/specialists/org/contracts/architect-to-ai-engineer.json +6 -2
  398. package/specialists/org/contracts/architect-to-data-engineer.json +17 -5
  399. package/specialists/org/contracts/architect-to-devil-advocate.json +11 -3
  400. package/specialists/org/contracts/architect-to-engineer.json +20 -4
  401. package/specialists/org/contracts/architect-to-evaluator.json +15 -5
  402. package/specialists/org/contracts/architect-to-legal-compliance.json +15 -5
  403. package/specialists/org/contracts/architect-to-operations.json +17 -4
  404. package/specialists/org/contracts/architect-to-platform-engineer.json +20 -4
  405. package/specialists/org/contracts/construct-to-orchestrator.json +10 -2
  406. package/specialists/org/contracts/data-analyst-to-product-manager.json +11 -2
  407. package/specialists/org/contracts/engineer-to-qa.json +20 -4
  408. package/specialists/org/contracts/engineer-to-reviewer.json +29 -5
  409. package/specialists/org/contracts/explorer-to-engineer.json +11 -3
  410. package/specialists/org/contracts/legal-compliance-to-release-manager.json +12 -4
  411. package/specialists/org/contracts/operations-to-user.json +53 -0
  412. package/specialists/org/contracts/operations-triage-output.json +53 -0
  413. package/specialists/org/contracts/pm-requirements-candidates.json +53 -0
  414. package/specialists/org/contracts/product-manager-to-architect.json +30 -10
  415. package/specialists/org/contracts/product-manager-to-data-analyst.json +13 -3
  416. package/specialists/org/contracts/product-manager-to-ux-researcher.json +15 -5
  417. package/specialists/org/contracts/qa-to-release-manager.json +11 -3
  418. package/specialists/org/contracts/researcher-to-architect.json +10 -2
  419. package/specialists/org/contracts/researcher-to-product-manager.json +16 -5
  420. package/specialists/org/contracts/reviewer-to-security.json +17 -3
  421. package/specialists/org/contracts/test-automation-to-engineer.json +11 -3
  422. package/specialists/org/contracts/trace-reviewer-to-sre.json +12 -4
  423. package/specialists/org/contracts/user-to-construct.json +11 -4
  424. package/specialists/org/frameworks/cx-architect-constraint-option-failure.md +66 -0
  425. package/specialists/org/frameworks/cx-engineer-feasibility-blast-radius.md +66 -0
  426. package/specialists/org/frameworks/cx-ops-dependency-sequencing.md +74 -0
  427. package/specialists/org/frameworks/cx-pm-value-tradeoff.md +66 -0
  428. package/specialists/org/frameworks/cx-qa-risk-based-coverage.md +69 -0
  429. package/specialists/org/groups/engineering-group.json +2 -6
  430. package/specialists/org/groups/governance-group.json +2 -3
  431. package/specialists/org/groups/operations-group.json +5 -8
  432. package/specialists/org/groups/product-group.json +4 -8
  433. package/specialists/org/groups/quality-group.json +3 -7
  434. package/specialists/org/groups/strategy-group.json +6 -9
  435. package/specialists/org/models.json.example +8 -0
  436. package/specialists/org/scopes/creative.json +6 -1
  437. package/specialists/org/scopes/operations.json +7 -1
  438. package/specialists/org/scopes/research.json +6 -1
  439. package/specialists/org/scopes/rnd.json +7 -1
  440. package/specialists/org/specialists/cx-architect.json +27 -8
  441. package/specialists/org/specialists/cx-designer.json +22 -8
  442. package/specialists/org/specialists/cx-engineer.json +71 -7
  443. package/specialists/org/specialists/cx-operations.json +89 -15
  444. package/specialists/org/specialists/cx-orchestrator.json +16 -4
  445. package/specialists/org/specialists/cx-product-manager.json +28 -9
  446. package/specialists/org/specialists/cx-qa.json +16 -6
  447. package/specialists/org/specialists/cx-researcher.json +40 -7
  448. package/specialists/org/specialists/cx-reviewer.json +61 -11
  449. package/specialists/org/specialists/cx-security.json +30 -10
  450. package/specialists/org/teams/design-team.json +3 -4
  451. package/specialists/org/teams/engineering-team.json +3 -10
  452. package/specialists/org/teams/governance-team.json +3 -5
  453. package/specialists/org/teams/operations-team.json +6 -12
  454. package/specialists/org/teams/product-management-team.json +2 -3
  455. package/specialists/org/teams/quality-team.json +4 -12
  456. package/specialists/org/teams/research-team.json +3 -3
  457. package/specialists/org/teams/strategy-team.json +7 -14
  458. package/specialists/prompts/cx-architect.md +20 -1
  459. package/specialists/prompts/cx-data-analyst.md +1 -1
  460. package/specialists/prompts/cx-designer.md +12 -4
  461. package/specialists/prompts/cx-engineer.md +15 -1
  462. package/specialists/prompts/cx-operations.md +14 -2
  463. package/specialists/prompts/cx-orchestrator.md +9 -5
  464. package/specialists/prompts/cx-product-manager.md +5 -1
  465. package/specialists/prompts/cx-qa.md +6 -2
  466. package/specialists/prompts/cx-researcher.md +25 -30
  467. package/specialists/prompts/cx-reviewer.md +19 -1
  468. package/specialists/prompts/cx-security.md +5 -1
  469. package/templates/distribution/construct-brand.typ +123 -59
  470. package/templates/distribution/construct-decision.typ +6 -3
  471. package/templates/distribution/construct-pdf.typ +11 -4
  472. package/templates/distribution/construct-prd.typ +6 -3
  473. package/templates/distribution/construct-research.typ +7 -4
  474. package/lib/providers/contract/adapters/git/index.mjs +0 -115
  475. package/lib/providers/contract/adapters/slack/index.mjs +0 -175
  476. package/specialists/org/contracts/business-strategist-to-product-manager.json +0 -39
  477. package/specialists/org/contracts/construct-to-rd-lead.json +0 -53
  478. package/specialists/org/contracts/data-engineer-to-platform-engineer.json +0 -36
  479. package/specialists/org/contracts/designer-to-accessibility.json +0 -36
  480. package/specialists/org/contracts/platform-engineer-to-engineer.json +0 -31
  481. package/specialists/org/contracts/qa-to-test-automation.json +0 -42
  482. package/specialists/org/contracts/rd-lead-to-architect.json +0 -38
  483. package/specialists/org/contracts/sre-to-release-manager.json +0 -36
  484. package/specialists/org/specialists/cx-accessibility.json +0 -69
  485. package/specialists/org/specialists/cx-ai-engineer.json +0 -75
  486. package/specialists/org/specialists/cx-business-strategist.json +0 -72
  487. package/specialists/org/specialists/cx-data-engineer.json +0 -71
  488. package/specialists/org/specialists/cx-devil-advocate.json +0 -71
  489. package/specialists/org/specialists/cx-docs-keeper.json +0 -82
  490. package/specialists/org/specialists/cx-evaluator.json +0 -69
  491. package/specialists/org/specialists/cx-explorer.json +0 -69
  492. package/specialists/org/specialists/cx-legal-compliance.json +0 -74
  493. package/specialists/org/specialists/cx-oracle.json +0 -46
  494. package/specialists/org/specialists/cx-platform-engineer.json +0 -80
  495. package/specialists/org/specialists/cx-rd-lead.json +0 -73
  496. package/specialists/org/specialists/cx-release-manager.json +0 -77
  497. package/specialists/org/specialists/cx-sre.json +0 -94
  498. package/specialists/org/specialists/cx-test-automation.json +0 -69
  499. package/specialists/org/specialists/cx-trace-reviewer.json +0 -69
  500. package/specialists/org/specialists/cx-ux-researcher.json +0 -68
  501. package/specialists/org/teams/accessibility-team.json +0 -39
  502. package/specialists/org/teams/ux-research-team.json +0 -39
  503. package/specialists/prompts/cx-accessibility.md +0 -55
  504. package/specialists/prompts/cx-ai-engineer.md +0 -124
  505. package/specialists/prompts/cx-business-strategist.md +0 -58
  506. package/specialists/prompts/cx-data-engineer.md +0 -55
  507. package/specialists/prompts/cx-devil-advocate.md +0 -59
  508. package/specialists/prompts/cx-docs-keeper.md +0 -164
  509. package/specialists/prompts/cx-evaluator.md +0 -49
  510. package/specialists/prompts/cx-explorer.md +0 -71
  511. package/specialists/prompts/cx-legal-compliance.md +0 -58
  512. package/specialists/prompts/cx-oracle.md +0 -98
  513. package/specialists/prompts/cx-platform-engineer.md +0 -97
  514. package/specialists/prompts/cx-rd-lead.md +0 -59
  515. package/specialists/prompts/cx-release-manager.md +0 -54
  516. package/specialists/prompts/cx-sre.md +0 -111
  517. package/specialists/prompts/cx-test-automation.md +0 -55
  518. package/specialists/prompts/cx-trace-reviewer.md +0 -101
  519. package/specialists/prompts/cx-ux-researcher.md +0 -53
@@ -0,0 +1,384 @@
1
+ /**
2
+ * lib/storage/retrieval-hardening.mjs — Vector-store poisoning defenses for
3
+ * the retrieval/recall path.
4
+ *
5
+ * N1 (construct-9oi4.14.1, lib/security/trust.mjs) stamps ingested content
6
+ * with a trust label (`_trust.level`) but does nothing to the ranking or
7
+ * shape of what gets recalled — an adversarial embedding tuned to score high
8
+ * cosine similarity can still dominate assembled context. This module is the
9
+ * retrieval-side hardening layer: it takes an already similarity-ranked
10
+ * result array (from VectorClient.searchObservations/searchDocuments or the
11
+ * hybrid-query fusion) and applies, in order:
12
+ *
13
+ * 1. distance/similarity sanity filtering — drop results below a floor or
14
+ * carrying a non-finite/out-of-range similarity (malformed or
15
+ * adversarially-crafted embeddings can produce these).
16
+ * 2. duplicate-content collapse — near-identical bodies (same normalized
17
+ * text) keep only the highest-ranked copy, so an attacker cannot
18
+ * multiply one payload's presence in the assembled set.
19
+ * 3. trust-weighted re-ranking — at equal similarity, higher N1 trust wins;
20
+ * unstamped records are graded EXTERNAL_UNAUTHENTICATED per policy
21
+ * (lib/security/trust.mjs recallTrustGrade).
22
+ * 4. per-source recall cap — no single source may occupy more than a
23
+ * configurable fraction of the assembled set.
24
+ *
25
+ * `flagRetrievalAnomalies` is a separate, non-mutating check: it inspects
26
+ * per-source frequency across a result set (or a caller-supplied historical
27
+ * frequency table) and flags sources whose share is a statistical outlier,
28
+ * for callers that want to log/alert without altering ranking.
29
+ *
30
+ * Pure functions only — no I/O, no LanceDB dependency — so the module is
31
+ * unit-testable without a live vector store and composable into
32
+ * hybrid-query.mjs, observation-store.mjs, or any future retrieval caller.
33
+ *
34
+ * References: CX-AUDIT-LLMSEC-001, OWASP GenAI vector/embedding weaknesses,
35
+ * construct-9oi4.14.4 (depends on construct-9oi4.14.1).
36
+ */
37
+
38
+ import { TRUST_LEVELS, recallTrustGrade } from '../security/trust.mjs';
39
+
40
+ // ---------------------------------------------------------------------------
41
+ // Defaults
42
+ // ---------------------------------------------------------------------------
43
+
44
+ export const DEFAULT_HARDENING_OPTIONS = {
45
+ minSimilarity: 0.05,
46
+ maxSimilarity: 1.0001,
47
+ perSourceCapRatio: 0.34,
48
+ duplicateSimilarityThreshold: 0.995,
49
+ };
50
+
51
+ const TRUST_RANK = [
52
+ TRUST_LEVELS.EXTERNAL_UNAUTHENTICATED,
53
+ TRUST_LEVELS.EXTERNAL_AUTHENTICATED,
54
+ TRUST_LEVELS.TEAM_AUTHORED,
55
+ TRUST_LEVELS.TRUSTED_INTERNAL,
56
+ ];
57
+
58
+ // ---------------------------------------------------------------------------
59
+ // Trust grading
60
+ // ---------------------------------------------------------------------------
61
+
62
+ // Unstamped records are the highest-risk case (N1 policy: treat as the
63
+ // least-trusted level) rather than silently defaulting to a mid-tier grade.
64
+
65
+ function trustRankOf(record) {
66
+ const grade = recallTrustGrade(record);
67
+ const level = grade?.level ?? TRUST_LEVELS.EXTERNAL_UNAUTHENTICATED;
68
+ const idx = TRUST_RANK.indexOf(level);
69
+ return idx === -1 ? 0 : idx;
70
+ }
71
+
72
+ /**
73
+ * Resolve a stable source identity for a recalled record, for both the
74
+ * observations table (`source`) and the documents table (`source_path` /
75
+ * `sourcePath`), falling back to project scope so ungrouped records still
76
+ * collapse into one bucket rather than each counting as a unique source.
77
+ *
78
+ * @param {Record<string, unknown>} record
79
+ * @returns {string}
80
+ */
81
+ export function sourceIdentityOf(record) {
82
+ const raw = record?.source ?? record?.sourcePath ?? record?.source_path ?? record?.project ?? 'unknown';
83
+ return typeof raw === 'object' ? JSON.stringify(raw) : String(raw);
84
+ }
85
+
86
+ // ---------------------------------------------------------------------------
87
+ // Distance / similarity sanity thresholds
88
+ // ---------------------------------------------------------------------------
89
+
90
+ /**
91
+ * Drop results whose similarity is missing, non-finite, or outside the
92
+ * plausible [minSimilarity, maxSimilarity] band. Cosine similarity for a
93
+ * well-formed embedding pair lies in [-1, 1]; a value outside that band (or
94
+ * exactly saturating past 1) signals a malformed or adversarially crafted
95
+ * vector rather than a genuine semantic match.
96
+ *
97
+ * @param {Array<Record<string, unknown>>} results
98
+ * @param {{ minSimilarity?: number, maxSimilarity?: number }} [opts]
99
+ * @returns {Array<Record<string, unknown>>}
100
+ */
101
+ export function applySanityThreshold(results, opts = {}) {
102
+ const minSimilarity = opts.minSimilarity ?? DEFAULT_HARDENING_OPTIONS.minSimilarity;
103
+ const maxSimilarity = opts.maxSimilarity ?? DEFAULT_HARDENING_OPTIONS.maxSimilarity;
104
+ if (!Array.isArray(results)) return [];
105
+
106
+ return results.filter((r) => {
107
+ const sim = r?.similarity;
108
+ return Number.isFinite(sim) && sim >= minSimilarity && sim <= maxSimilarity;
109
+ });
110
+ }
111
+
112
+ // ---------------------------------------------------------------------------
113
+ // Duplicate-content collapse
114
+ // ---------------------------------------------------------------------------
115
+
116
+ function normalizeForDedup(record) {
117
+ const text = record?.content ?? record?.body ?? record?.summary ?? '';
118
+ return String(text).trim().toLowerCase().replace(/\s+/g, ' ');
119
+ }
120
+
121
+ /**
122
+ * Collapse near-identical content down to the single highest-similarity
123
+ * copy. Exact-normalized matches always collapse; near-duplicates above
124
+ * `duplicateSimilarityThreshold` collapse via a cheap Jaccard-on-shingles
125
+ * check so an attacker cannot pad recall share by injecting many
126
+ * near-identical restatements of one payload.
127
+ *
128
+ * @param {Array<Record<string, unknown>>} results Similarity-sorted, best first.
129
+ * @param {{ duplicateSimilarityThreshold?: number }} [opts]
130
+ * @returns {Array<Record<string, unknown>>}
131
+ */
132
+ export function collapseDuplicates(results, opts = {}) {
133
+ const threshold = opts.duplicateSimilarityThreshold ?? DEFAULT_HARDENING_OPTIONS.duplicateSimilarityThreshold;
134
+ if (!Array.isArray(results) || results.length === 0) return [];
135
+
136
+ const kept = [];
137
+ const keptNormalized = [];
138
+
139
+ for (const candidate of results) {
140
+ const normalized = normalizeForDedup(candidate);
141
+ const isDuplicate = keptNormalized.some((existing) => {
142
+ if (existing === normalized) return true;
143
+ if (!existing || !normalized) return false;
144
+ return shingleSimilarity(existing, normalized) >= threshold;
145
+ });
146
+ if (isDuplicate) continue;
147
+ kept.push(candidate);
148
+ keptNormalized.push(normalized);
149
+ }
150
+
151
+ return kept;
152
+ }
153
+
154
+ // Trigram-shingle Jaccard similarity — cheap, dependency-free near-duplicate
155
+ // detection that tolerates whitespace/punctuation noise between two restated
156
+ // copies of the same payload.
157
+
158
+ function shingleSimilarity(a, b) {
159
+ const shinglesA = shingles(a);
160
+ const shinglesB = shingles(b);
161
+ if (shinglesA.size === 0 || shinglesB.size === 0) return 0;
162
+
163
+ let intersection = 0;
164
+ for (const s of shinglesA) {
165
+ if (shinglesB.has(s)) intersection += 1;
166
+ }
167
+ const union = shinglesA.size + shinglesB.size - intersection;
168
+ return union === 0 ? 0 : intersection / union;
169
+ }
170
+
171
+ function shingles(text, size = 3) {
172
+ const set = new Set();
173
+ for (let i = 0; i <= text.length - size; i += 1) {
174
+ set.add(text.slice(i, i + size));
175
+ }
176
+ return set;
177
+ }
178
+
179
+ // ---------------------------------------------------------------------------
180
+ // Trust-weighted ranking
181
+ // ---------------------------------------------------------------------------
182
+
183
+ /**
184
+ * Re-rank results so that, at materially equal similarity, higher-trust
185
+ * records sort above lower-trust ones. Similarity remains the dominant sort
186
+ * key — trust only breaks ties within an `epsilon` similarity band — so a
187
+ * genuinely more relevant trusted result still loses to an overwhelmingly
188
+ * better-matching one; it only wins the recall competition an adversarial
189
+ * near-tie is designed to exploit.
190
+ *
191
+ * @param {Array<Record<string, unknown>>} results
192
+ * @param {{ epsilon?: number }} [opts]
193
+ * @returns {Array<Record<string, unknown>>}
194
+ */
195
+ export function applyTrustWeightedRanking(results, opts = {}) {
196
+ const epsilon = opts.epsilon ?? 0.02;
197
+ if (!Array.isArray(results)) return [];
198
+
199
+ return [...results].sort((a, b) => {
200
+ const simA = Number(a?.similarity) || 0;
201
+ const simB = Number(b?.similarity) || 0;
202
+ if (Math.abs(simA - simB) > epsilon) return simB - simA;
203
+
204
+ const trustDelta = trustRankOf(b) - trustRankOf(a);
205
+ if (trustDelta !== 0) return trustDelta;
206
+
207
+ return simB - simA;
208
+ });
209
+ }
210
+
211
+ // ---------------------------------------------------------------------------
212
+ // Per-source recall cap
213
+ // ---------------------------------------------------------------------------
214
+
215
+ /**
216
+ * Enforce that no single source occupies more than `perSourceCapRatio` of
217
+ * the FINAL assembled context set — not of the raw candidate pool. The cap
218
+ * is defined against `assembledSize` (the caller's `limit`, i.e. how many
219
+ * results actually get assembled into context) because that is the quantity
220
+ * OWASP/N4 policy constrains: an attacker cannot monopolize the context a
221
+ * model actually sees, regardless of how many candidates were fetched
222
+ * upstream. When no explicit `assembledSize`/`limit` is given, the candidate
223
+ * pool size after this stage is the best available proxy.
224
+ *
225
+ * Applied after ranking, so the highest-ranked items from each source are
226
+ * kept and the cap only trims a source's excess once it would otherwise
227
+ * crowd out other sources. A single pass keeps up to `cap` per source, then
228
+ * a second pass backfills any remaining assembled slots from the trimmed
229
+ * overflow (best-ranked first) so the cap never shrinks the final set below
230
+ * `assembledSize` while other sources have eligible candidates left.
231
+ *
232
+ * @param {Array<Record<string, unknown>>} results Already ranked, best first.
233
+ * @param {{ perSourceCapRatio?: number, assembledSize?: number, limit?: number }} [opts]
234
+ * @returns {Array<Record<string, unknown>>}
235
+ */
236
+ export function applyPerSourceCap(results, opts = {}) {
237
+ const ratio = opts.perSourceCapRatio ?? DEFAULT_HARDENING_OPTIONS.perSourceCapRatio;
238
+ if (!Array.isArray(results) || results.length === 0) return [];
239
+
240
+ const assembledSize = Math.min(
241
+ results.length,
242
+ opts.assembledSize ?? opts.limit ?? results.length,
243
+ );
244
+ const cap = Math.max(1, Math.floor(assembledSize * ratio));
245
+
246
+ const perSourceCount = new Map();
247
+ const kept = [];
248
+ const overflow = [];
249
+
250
+ for (const record of results) {
251
+ const source = sourceIdentityOf(record);
252
+ const count = perSourceCount.get(source) || 0;
253
+ if (count >= cap) {
254
+ overflow.push(record);
255
+ continue;
256
+ }
257
+ perSourceCount.set(source, count + 1);
258
+ kept.push(record);
259
+ }
260
+
261
+ // Backfill from overflow (still capped per source) only until the
262
+ // assembled set reaches its target size — the cap must keep binding, so
263
+ // a source already at its cap is never topped up again here.
264
+
265
+ for (const record of overflow) {
266
+ if (kept.length >= assembledSize) break;
267
+ const source = sourceIdentityOf(record);
268
+ const count = perSourceCount.get(source) || 0;
269
+ if (count >= cap) continue;
270
+ perSourceCount.set(source, count + 1);
271
+ kept.push(record);
272
+ }
273
+
274
+ return kept;
275
+ }
276
+
277
+ // ---------------------------------------------------------------------------
278
+ // Combined pipeline
279
+ // ---------------------------------------------------------------------------
280
+
281
+ /**
282
+ * Apply the full retrieval-hardening pipeline to a similarity-ranked result
283
+ * set: sanity threshold -> duplicate collapse -> trust-weighted ranking ->
284
+ * per-source cap -> limit. Intended as the single call site for any recall
285
+ * path that assembles context from VectorClient search results.
286
+ *
287
+ * @param {Array<Record<string, unknown>>} results
288
+ * @param {{
289
+ * minSimilarity?: number, maxSimilarity?: number,
290
+ * duplicateSimilarityThreshold?: number,
291
+ * perSourceCapRatio?: number, epsilon?: number, limit?: number,
292
+ * }} [opts]
293
+ * @returns {Array<Record<string, unknown>>}
294
+ */
295
+ export function hardenRetrieval(results, opts = {}) {
296
+ if (!Array.isArray(results)) return [];
297
+
298
+ const sane = applySanityThreshold(results, opts);
299
+ const deduped = collapseDuplicates(sane, opts);
300
+ const ranked = applyTrustWeightedRanking(deduped, opts);
301
+ const capped = applyPerSourceCap(ranked, opts);
302
+
303
+ return opts.limit != null ? capped.slice(0, opts.limit) : capped;
304
+ }
305
+
306
+ // ---------------------------------------------------------------------------
307
+ // Retrieval-frequency anomaly check
308
+ // ---------------------------------------------------------------------------
309
+
310
+ /**
311
+ * Flag sources whose share of a result set is a statistical outlier versus
312
+ * the other sources present. Uses a leave-one-out z-score: each source's
313
+ * count is compared against the mean/stddev of every OTHER source's count,
314
+ * not the whole population — scoring against the full population lets a
315
+ * large flood inflate its own reference mean/stddev enough to mask itself,
316
+ * especially with only a handful of distinct sources. A source more than
317
+ * `zThreshold` standard deviations above the other sources' mean is flagged;
318
+ * if the other sources all tie (zero spread) any source strictly above that
319
+ * shared count is flagged outright. With fewer than 3 distinct sources the
320
+ * check is inconclusive (no meaningful distribution) and returns no flags
321
+ * rather than a false positive.
322
+ *
323
+ * Non-mutating — this is a detection signal for logging/alerting, not a
324
+ * ranking transform. Callers that want enforcement should also apply
325
+ * `applyPerSourceCap`.
326
+ *
327
+ * @param {Array<Record<string, unknown>>} results
328
+ * @param {{ zThreshold?: number }} [opts]
329
+ * @returns {{
330
+ * flagged: Array<{ source: string, count: number, share: number, zScore: number }>,
331
+ * sourceCounts: Record<string, number>,
332
+ * }}
333
+ */
334
+ export function flagRetrievalAnomalies(results, opts = {}) {
335
+ const zThreshold = opts.zThreshold ?? 2;
336
+ const sourceCounts = {};
337
+
338
+ if (!Array.isArray(results) || results.length === 0) {
339
+ return { flagged: [], sourceCounts };
340
+ }
341
+
342
+ for (const record of results) {
343
+ const source = sourceIdentityOf(record);
344
+ sourceCounts[source] = (sourceCounts[source] || 0) + 1;
345
+ }
346
+
347
+ const entries = Object.entries(sourceCounts);
348
+ if (entries.length < 3) {
349
+ return { flagged: [], sourceCounts };
350
+ }
351
+
352
+ // Leave-one-out z-score: a flood source's own count inflates the
353
+ // population mean/stddev enough to mask itself in a plain z-score,
354
+ // especially with few distinct sources. Scoring each candidate against
355
+ // the mean/stddev of the OTHER sources only removes that self-masking.
356
+
357
+ const flagged = [];
358
+ for (const [source, count] of entries) {
359
+ const others = entries.filter(([s]) => s !== source).map(([, c]) => c);
360
+ const otherMean = others.reduce((sum, c) => sum + c, 0) / others.length;
361
+ const otherVariance = others.reduce((sum, c) => sum + (c - otherMean) ** 2, 0) / others.length;
362
+ const otherStddev = Math.sqrt(otherVariance);
363
+
364
+ // Zero-spread tie among the other sources leaves no stddev to divide by.
365
+ // Require the candidate to be a substantial multiple of that tied count
366
+ // (not merely +1) before flagging, or a one-off count on top of small
367
+ // uniform buckets (e.g. 4 vs 3,3,3) would falsely read as an outlier.
368
+
369
+ if (otherStddev === 0) {
370
+ if (otherMean > 0 && count >= otherMean * 2) {
371
+ flagged.push({ source, count, share: count / results.length, zScore: Infinity });
372
+ }
373
+ continue;
374
+ }
375
+
376
+ const zScore = (count - otherMean) / otherStddev;
377
+ if (zScore >= zThreshold) {
378
+ flagged.push({ source, count, share: count / results.length, zScore });
379
+ }
380
+ }
381
+
382
+ flagged.sort((a, b) => b.zScore - a.zScore);
383
+ return { flagged, sourceCounts };
384
+ }
@@ -0,0 +1,158 @@
1
+ /**
2
+ * lib/storage/shared-memory.mjs — explicit shared/private memory boundary (LMCP-G10).
3
+ *
4
+ * lib/observation-store.mjs persists every observation to a local,
5
+ * project-scoped store only — there is no tier a team member on another
6
+ * machine can read, and nothing currently distinguishes a private session
7
+ * scratch note from curated, team-durable project knowledge. This module is
8
+ * that explicit tier: a record only ever reaches the shared, postgres-backed
9
+ * store when it opts in with `visibility: 'shared-project'` AND carries
10
+ * non-empty `provenance` — anything else (no visibility field, `visibility:
11
+ * 'private'`, or a `sessionScratch` marker) is refused, never silently
12
+ * promoted.
13
+ *
14
+ * Solo mode (no reachable Postgres) resolves to a `kind:'none'` store whose
15
+ * write/list are no-ops — private local memory (observation-store.mjs) is
16
+ * unaffected either way.
17
+ *
18
+ * Tenant scoping (LMCP-H4, ADR-0057/A7 stage 1): every row carries a
19
+ * `tenant_id` column and every query filters on it, matching the run store
20
+ * (run-store-postgres.mjs) and trace store (trace-store.mjs). resolveShared-
21
+ * MemoryStore resolves the tenant context up front and fails closed —
22
+ * TenantResolutionError propagates uncaught — when enterprise mode has no
23
+ * resolvable tenant id. Stage 2 (physical per-tenant isolation, per-tenant
24
+ * encryption) is explicitly deferred and not advertised; this remains one
25
+ * shared table with row-level tenant scoping only.
26
+ */
27
+
28
+ import { createSqlClient } from './backend.mjs';
29
+ import { getDeploymentMode } from '../deployment-mode.mjs';
30
+ import { resolveTenantContext } from '../tenant/context.mjs';
31
+
32
+ export const SHARED_VISIBILITY = 'shared-project';
33
+
34
+ /**
35
+ * isShareable(record)
36
+ *
37
+ * A record is shareable only when every one of these holds:
38
+ * - `visibility` is exactly 'shared-project' (default/absent means private)
39
+ * - `provenance` is a non-empty object (who/what produced this, re-checkable)
40
+ * - it does not carry a `sessionScratch` marker (an explicit private-session flag)
41
+ *
42
+ * @param {object} record
43
+ * @returns {{ shareable: true } | { shareable: false, reason: string }}
44
+ */
45
+ export function isShareable(record) {
46
+ if (!record || typeof record !== 'object') {
47
+ return { shareable: false, reason: 'record must be an object' };
48
+ }
49
+ if (record.sessionScratch) {
50
+ return { shareable: false, reason: 'record is marked sessionScratch (private session state)' };
51
+ }
52
+ if (record.visibility !== SHARED_VISIBILITY) {
53
+ return { shareable: false, reason: `visibility must be '${SHARED_VISIBILITY}' (got ${JSON.stringify(record.visibility ?? null)})` };
54
+ }
55
+ if (!record.provenance || typeof record.provenance !== 'object' || Array.isArray(record.provenance) || Object.keys(record.provenance).length === 0) {
56
+ return { shareable: false, reason: 'provenance must be a non-empty object' };
57
+ }
58
+ if (!record.id) {
59
+ return { shareable: false, reason: 'record.id is required' };
60
+ }
61
+ if (!record.category) {
62
+ return { shareable: false, reason: 'record.category is required' };
63
+ }
64
+ return { shareable: true };
65
+ }
66
+
67
+ function noneStore() {
68
+ return {
69
+ kind: 'none',
70
+ writeSharedMemory: async () => ({ ok: false, reason: 'no-team-store-configured' }),
71
+ listSharedMemory: async () => [],
72
+ };
73
+ }
74
+
75
+ function postgresStore(sql, defaultTenantId) {
76
+ let ensured = null;
77
+ const ready = () => {
78
+ if (!ensured) {
79
+ ensured = (async () => {
80
+ const { applyMigrations } = await import('../db/migrate.mjs');
81
+ await applyMigrations(sql);
82
+ })();
83
+ }
84
+ return ensured;
85
+ };
86
+
87
+ return {
88
+ kind: 'postgres',
89
+ tenantId: defaultTenantId,
90
+ async writeSharedMemory(record, { project, tenantId = defaultTenantId } = {}) {
91
+ if (!project) throw new Error('writeSharedMemory: project is required');
92
+ const check = isShareable(record);
93
+ if (!check.shareable) return { ok: false, reason: check.reason };
94
+
95
+ await ready();
96
+ await sql`
97
+ INSERT INTO construct_shared_memory (
98
+ id, project, tenant_id, category, summary, content, tags, provenance, created_at
99
+ ) VALUES (
100
+ ${record.id}, ${project}, ${tenantId}, ${record.category},
101
+ ${record.summary || null}, ${record.content || null},
102
+ ${sql.json(record.tags || [])}, ${sql.json(record.provenance)},
103
+ ${record.createdAt || new Date().toISOString()}
104
+ )
105
+ ON CONFLICT (project, tenant_id, id) DO UPDATE SET
106
+ category = EXCLUDED.category,
107
+ summary = EXCLUDED.summary,
108
+ content = EXCLUDED.content,
109
+ tags = EXCLUDED.tags,
110
+ provenance = EXCLUDED.provenance
111
+ `;
112
+ return { ok: true };
113
+ },
114
+ async listSharedMemory({ project, tenantId = defaultTenantId, limit = 100 } = {}) {
115
+ if (!project) throw new Error('listSharedMemory: project is required');
116
+ await ready();
117
+ const rows = await sql`
118
+ SELECT id, category, summary, content, tags, provenance, created_at
119
+ FROM construct_shared_memory
120
+ WHERE project = ${project} AND tenant_id = ${tenantId}
121
+ ORDER BY created_at DESC
122
+ LIMIT ${limit}
123
+ `;
124
+ return rows.map((row) => ({
125
+ id: row.id,
126
+ category: row.category,
127
+ summary: row.summary,
128
+ content: row.content,
129
+ tags: row.tags,
130
+ provenance: row.provenance,
131
+ createdAt: row.created_at,
132
+ }));
133
+ },
134
+ };
135
+ }
136
+
137
+ /**
138
+ * resolveSharedMemoryStore(opts)
139
+ *
140
+ * `sql` accepts an injected client (matching lib/team/health.mjs's
141
+ * convention) so a caller — or a test backed by an in-memory fake — can
142
+ * supply a shared substrate directly instead of resolving one from env.
143
+ * Enterprise mode with no resolvable tenant id throws TenantResolutionError
144
+ * before any query runs (fail-closed).
145
+ *
146
+ * @param {{ env?: object, cwd?: string, config?: object, sql?: object }} [opts]
147
+ * @returns {{ kind: string, writeSharedMemory: Function, listSharedMemory: Function }}
148
+ */
149
+ export function resolveSharedMemoryStore({ env = process.env, cwd = process.cwd(), config = null, sql = null } = {}) {
150
+ const mode = getDeploymentMode(env, { cwd });
151
+ if (mode === 'solo' && !sql) return noneStore();
152
+
153
+ const ownSql = sql || createSqlClient(env);
154
+ if (!ownSql) return noneStore();
155
+
156
+ const { tenantId } = resolveTenantContext({ env, config, mode });
157
+ return postgresStore(ownSql, tenantId);
158
+ }
@@ -1,16 +1,31 @@
1
1
  /**
2
2
  * lib/storage/vector-client.mjs — Unified vector storage client for LanceDB.
3
+ *
4
+ * Unset CONSTRUCT_LANCEDB_PATH falls back to the calling project's
5
+ * machine-scoped state root (ADR-0066: `<stateRoot>/lancedb`, keyed off cwd).
6
+ * A managed install sets CONSTRUCT_LANCEDB_PATH to a single machine-wide
7
+ * index (lib/setup.mjs's defaultVectorIndexPath), which always takes
8
+ * precedence; the fallback path only applies to unmanaged/solo runs and tests.
9
+ *
10
+ * The index itself is lazy: _getDb() only connects/creates on the first
11
+ * actual store or search call, never at construction. pruneObservations()
12
+ * (lib/storage/admin.mjs's purgeExpiredData) is the TTL/size eviction path
13
+ * for the machine-scoped store and never creates observations_v1 as a side
14
+ * effect of checking or pruning it.
3
15
  */
4
16
 
5
- import path from 'path';
6
17
  import fs from 'fs';
7
18
  import { getEmbeddingModelInfo } from './embeddings-engine.mjs';
19
+ import { resolveStateDir } from '../state-root.mjs';
8
20
 
9
21
  const FALLBACK_DIMENSIONS = 384;
10
22
 
11
23
  function getDbPath(env = process.env) {
12
24
  if (env.CONSTRUCT_LANCEDB_PATH) return env.CONSTRUCT_LANCEDB_PATH;
13
- return path.join(process.cwd(), '.cx', 'lancedb');
25
+ // ensureDir:false — this path is computed on every call (including ones
26
+ // that never reach a real connect, e.g. lancedb module load failure), so
27
+ // directory creation stays owned by _getDb() right before it connects.
28
+ return resolveStateDir(process.cwd(), 'lancedb', { ensureDir: false });
14
29
  }
15
30
 
16
31
  // LanceDB uses optimistic concurrency: two writes racing the same table fail
@@ -175,6 +190,58 @@ export class VectorClient {
175
190
  return this.isHealthy();
176
191
  }
177
192
 
193
+ // Existence check that never creates the table as a side effect — a purge
194
+ // or doctor report must not conjure observations_v1 just to find it empty;
195
+ // _getObservationsTable() would create it on a missing-table catch.
196
+
197
+ async hasObservationsTable() {
198
+ const db = await this._getDb();
199
+ const names = await db.tableNames();
200
+ return names.includes('observations_v1');
201
+ }
202
+
203
+ /**
204
+ * Evict rows from observations_v1 older than `maxAgeDays` and/or beyond
205
+ * the `maxRows` most-recent cap. Either bound is optional; passing neither
206
+ * is a no-op. Returns eviction stats so callers (doctor, `construct prune`,
207
+ * the post-sync retention hook) can report on the machine-scoped store
208
+ * without re-deriving it themselves.
209
+ */
210
+ async pruneObservations({ maxAgeDays, maxRows } = {}) {
211
+ if (!(await this.hasObservationsTable())) {
212
+ return { evictedCount: 0, remainingCount: 0, oldestRetainedAt: null };
213
+ }
214
+ const table = await this._getObservationsTable();
215
+ let evictedCount = 0;
216
+
217
+ if (typeof maxAgeDays === 'number' && maxAgeDays > 0) {
218
+ const cutoff = new Date(Date.now() - maxAgeDays * 24 * 60 * 60 * 1000).toISOString();
219
+ const before = await table.countRows();
220
+ await table.delete(`created_at < '${cutoff}'`);
221
+ evictedCount += Math.max(0, before - await table.countRows());
222
+ }
223
+
224
+ if (typeof maxRows === 'number' && maxRows > 0) {
225
+ const total = await table.countRows();
226
+ if (total > maxRows) {
227
+ const rows = await table.query().select(['created_at']).toArray();
228
+ const sortedTimestamps = rows.map((r) => r.created_at).sort();
229
+ const cutoff = sortedTimestamps[total - maxRows];
230
+ const before = await table.countRows();
231
+ await table.delete(`created_at < '${cutoff}'`);
232
+ evictedCount += Math.max(0, before - await table.countRows());
233
+ }
234
+ }
235
+
236
+ const remaining = await table.query().select(['created_at']).toArray();
237
+ const remainingTimestamps = remaining.map((r) => r.created_at).filter(Boolean).sort();
238
+ return {
239
+ evictedCount,
240
+ remainingCount: remaining.length,
241
+ oldestRetainedAt: remainingTimestamps[0] ?? null,
242
+ };
243
+ }
244
+
178
245
  async storeObservation(record) {
179
246
  return serializeWrite(getDbPath(this.env), () => withWriteRetry(() => this._storeObservation(record)));
180
247
  }