@geraldmaron/construct 1.0.0 → 1.0.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (471) hide show
  1. package/.env.example +9 -6
  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 +1000 -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 +523 -36
  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,551 @@
1
+ /**
2
+ * lib/schema-infer.mjs — infer a structured field schema from extracted document text.
3
+ *
4
+ * Calls the model with a structured-output prompt and returns a normalised
5
+ * SchemaInferenceResult. Works with any document type already handled by
6
+ * lib/document-extract.mjs. Falls back gracefully when no API key is present.
7
+ * Uses the configured fast-tier model from model-router rather than a hardcoded ID.
8
+ *
9
+ * Output shape (SchemaInferenceResult):
10
+ * {
11
+ * document_type: string, // e.g. "invoice", "resume", "table", "report"
12
+ * fields: SchemaField[],
13
+ * relationships: Relationship[], // cross-field or cross-record links
14
+ * confidence: number, // 0.0 – 1.0
15
+ * notes: string[], // caveats about truncation, ambiguity, etc.
16
+ * model: string, // model used for inference
17
+ * inferred_at: string, // ISO timestamp
18
+ * }
19
+ *
20
+ * SchemaField:
21
+ * { name, type, description, required, pattern, example, enum, format, nested_fields }
22
+ */
23
+
24
+ import { extractDocumentText } from './document-extract.mjs';
25
+ import { readCurrentModels, readOpenRouterApiKeyFromOpenCodeConfig } from './model-router.mjs';
26
+ import { getUserEnvPath } from './env-config.mjs';
27
+ import { pollFreeModels, topForTier } from './model-free-selector.mjs';
28
+ import { execSync } from 'node:child_process';
29
+
30
+ // ---------------------------------------------------------------------------
31
+ // Telemetry helpers — fire-and-forget, never throw into the caller.
32
+ // ---------------------------------------------------------------------------
33
+
34
+ function resolveSessionMeta() {
35
+ const userId = process.env.USER || process.env.USERNAME || process.env.LOGNAME || undefined;
36
+ const sessionId = process.env.CONSTRUCT_SESSION_ID
37
+ || process.env.CLAUDE_SESSION_ID
38
+ || process.env.CX_SESSION_ID
39
+ || process.env.OPENCODE_SESSION_ID
40
+ || undefined;
41
+ const environment = process.env.NODE_ENV || process.env.CONSTRUCT_ENV || 'development';
42
+ let release;
43
+ try {
44
+ release = execSync('git rev-parse --short HEAD', { stdio: 'pipe', timeout: 2000 }).toString().trim() || undefined;
45
+ } catch { /* best effort */ }
46
+ return { userId, sessionId, environment, release };
47
+ }
48
+
49
+ function telemetryAvailable() {
50
+ return Boolean(process.env.CONSTRUCT_TELEMETRY_PUBLIC_KEY && process.env.CONSTRUCT_TELEMETRY_SECRET_KEY);
51
+ }
52
+
53
+ function telemetryBaseUrl() {
54
+ return (process.env.CONSTRUCT_TELEMETRY_URL ?? '').replace(/\/$/, '');
55
+ }
56
+
57
+ function telemetryHeaders() {
58
+ const key = process.env.CONSTRUCT_TELEMETRY_PUBLIC_KEY;
59
+ const secret = process.env.CONSTRUCT_TELEMETRY_SECRET_KEY;
60
+ return {
61
+ Authorization: `Basic ${Buffer.from(`${key}:${secret}`).toString('base64')}`,
62
+ 'Content-Type': 'application/json',
63
+ };
64
+ }
65
+
66
+ async function telIngest(body) {
67
+ if (!telemetryAvailable()) return;
68
+ try {
69
+ await fetch(`${telemetryBaseUrl()}/api/public/ingestion`, {
70
+ method: 'POST',
71
+ headers: telemetryHeaders(),
72
+ body: JSON.stringify({ batch: Array.isArray(body) ? body : [body] }),
73
+ });
74
+ } catch {
75
+ // Observability must never break the caller.
76
+ }
77
+ }
78
+
79
+ async function telTrace({ id, name, input, metadata, sessionId, userId, release, environment }) {
80
+ await telIngest({
81
+ id: crypto.randomUUID(),
82
+ type: 'trace-create',
83
+ timestamp: new Date().toISOString(),
84
+ body: { id, name, input, metadata, sessionId, userId, release, environment, tags: ['schema-infer'] },
85
+ });
86
+ }
87
+
88
+ async function telGeneration({ traceId, id, name, model, input, output, startTime, endTime, level, statusMessage, usage }) {
89
+ await telIngest({
90
+ id: crypto.randomUUID(),
91
+ type: 'generation-create',
92
+ timestamp: new Date().toISOString(),
93
+ body: {
94
+ id,
95
+ traceId,
96
+ name,
97
+ model,
98
+ input,
99
+ output,
100
+ startTime,
101
+ endTime,
102
+ level: level ?? 'DEFAULT',
103
+ statusMessage,
104
+ ...(usage ? { usage: { input: usage.input ?? 0, output: usage.output ?? 0, total: usage.total ?? 0, unit: 'TOKENS' } } : {}),
105
+ },
106
+ });
107
+ }
108
+
109
+ async function telScore({ traceId, name, value, comment }) {
110
+ if (!telemetryAvailable()) return;
111
+ try {
112
+ await fetch(`${telemetryBaseUrl()}/api/public/scores`, {
113
+ method: 'POST',
114
+ headers: telemetryHeaders(),
115
+ body: JSON.stringify({
116
+ id: crypto.randomUUID(),
117
+ traceId,
118
+ name,
119
+ value,
120
+ dataType: 'NUMERIC',
121
+ comment,
122
+ }),
123
+ });
124
+ } catch {
125
+ // Observability must never break the caller.
126
+ }
127
+ }
128
+
129
+ async function lfUpdateTrace({ id, output, metadata }) {
130
+ await telIngest({
131
+ id: crypto.randomUUID(),
132
+ type: 'trace-create',
133
+ timestamp: new Date().toISOString(),
134
+ body: { id, output, metadata },
135
+ });
136
+ }
137
+
138
+ const SYSTEM_PROMPT = `You are a data modeling expert. Analyse the document text provided and infer a structured schema.
139
+
140
+ Return ONLY a valid JSON object matching this exact shape — no markdown fences, no commentary:
141
+
142
+ {
143
+ "document_type": "<concise label, e.g. invoice | resume | table | report | contract | form | log | config>",
144
+ "fields": [
145
+ {
146
+ "name": "<normalized snake_case field name>",
147
+ "type": "<string | number | boolean | date | datetime | array | object | enum | unknown>",
148
+ "description": "<what this field represents>",
149
+ "required": <true|false>,
150
+ "pattern": "<regex pattern if applicable, or null>",
151
+ "example": "<representative value from the document, or null>",
152
+ "enum": [<allowed values if finite set, or null>],
153
+ "format": "<e.g. currency:USD | phone | email | url | iso-date | iso-datetime | or null>",
154
+ "nested_fields": [<recursive SchemaField array for object/array types, or null>]
155
+ }
156
+ ],
157
+ "relationships": [
158
+ {
159
+ "from_field": "<field name>",
160
+ "to_field": "<field name>",
161
+ "type": "<reference | composition | aggregation | derivation>",
162
+ "description": "<what the relationship means>"
163
+ }
164
+ ],
165
+ "confidence": <0.0 to 1.0>,
166
+ "notes": ["<caveat or observation about the schema, e.g. truncation, ambiguous fields>"]
167
+ }
168
+
169
+ Rules:
170
+ - Infer field names from labels, headers, column names, or key patterns in the text.
171
+ - For tabular data (CSV, spreadsheet, table), every column becomes a top-level field.
172
+ - For structured documents (invoices, forms), identify all distinct data points.
173
+ - For narrative documents, identify the key entities and their attributes.
174
+ - Set confidence lower when the document is truncated, sparse, or ambiguous.
175
+ - Do NOT invent fields not evidenced in the text.
176
+ - Keep field names snake_case, lowercase.`;
177
+
178
+ const INFERENCE_TIMEOUT_MS = 30_000;
179
+
180
+ async function fetchWithTimeout(url, options) {
181
+ const controller = new AbortController();
182
+ const timer = setTimeout(() => controller.abort(), INFERENCE_TIMEOUT_MS);
183
+ try {
184
+ return await fetch(url, { ...options, signal: controller.signal });
185
+ } finally {
186
+ clearTimeout(timer);
187
+ }
188
+ }
189
+
190
+ async function callModel(userContent, { traceId } = {}) {
191
+ const orKey = process.env.OPENROUTER_API_KEY || readOpenRouterApiKeyFromOpenCodeConfig();
192
+ const anthropicKey = process.env.ANTHROPIC_API_KEY;
193
+
194
+ if (!orKey && !anthropicKey) {
195
+ throw new Error('No API key found. Set ANTHROPIC_API_KEY or OPENROUTER_API_KEY to use schema inference.');
196
+ }
197
+
198
+ // Build a ranked candidate list: live free models (top 5) + configured fast tier as final fallback.
199
+ let candidates = [];
200
+ if (orKey) {
201
+ const freeModels = await pollFreeModels(orKey);
202
+ const envPath = getUserEnvPath();
203
+ const configured = readCurrentModels(envPath, {});
204
+ candidates = topForTier(freeModels, 'fast', 5).map((m) => m.id);
205
+
206
+ // Append configured fast tier as final fallback if not already in the list.
207
+ if (configured.fast && !candidates.includes(configured.fast)) {
208
+ candidates.push(configured.fast);
209
+ }
210
+ }
211
+
212
+ // If only Anthropic key available, use configured fast tier directly.
213
+ if (candidates.length === 0 && anthropicKey) {
214
+ const envPath = getUserEnvPath();
215
+ candidates = [readCurrentModels(envPath, {}).fast];
216
+ }
217
+
218
+ let lastError;
219
+ for (const modelId of candidates) {
220
+ const isAnthropicDirect = /^anthropic\//.test(modelId) && !/^openrouter\//.test(modelId);
221
+ const isOpenRouter = /^openrouter\//.test(modelId);
222
+
223
+ const anthropicModelId = modelId.replace(/^anthropic\//, '');
224
+ const openRouterModelId = isOpenRouter ? modelId.replace(/^openrouter\//, '') : modelId;
225
+
226
+ const genId = crypto.randomUUID();
227
+ const startTime = new Date().toISOString();
228
+
229
+ try {
230
+ if (isAnthropicDirect && anthropicKey) {
231
+ const res = await fetchWithTimeout('https://api.anthropic.com/v1/messages', {
232
+ method: 'POST',
233
+ headers: {
234
+ 'x-api-key': anthropicKey,
235
+ 'anthropic-version': '2023-06-01',
236
+ 'content-type': 'application/json',
237
+ },
238
+ body: JSON.stringify({
239
+ model: anthropicModelId,
240
+ max_tokens: 4096,
241
+ system: SYSTEM_PROMPT,
242
+ messages: [{ role: 'user', content: userContent }],
243
+ }),
244
+ });
245
+ if (res.status === 429) {
246
+ lastError = new Error(`Anthropic rate-limited on ${modelId}`);
247
+ void telGeneration({ traceId, id: genId, name: 'schema-infer-attempt', model: modelId, input: userContent, startTime, endTime: new Date().toISOString(), level: 'WARNING', statusMessage: 'rate-limited' });
248
+ continue;
249
+ }
250
+ if (!res.ok) throw new Error(`Anthropic API ${res.status}: ${await res.text()}`);
251
+ const data = await res.json();
252
+ const text = data.content[0].text;
253
+ const usage = data.usage ? { input: data.usage.input_tokens ?? 0, output: data.usage.output_tokens ?? 0, total: (data.usage.input_tokens ?? 0) + (data.usage.output_tokens ?? 0) } : undefined;
254
+ void telGeneration({ traceId, id: genId, name: 'schema-infer-attempt', model: modelId, input: userContent, output: text, startTime, endTime: new Date().toISOString(), usage });
255
+ return { text, model: modelId };
256
+ }
257
+
258
+ if (isOpenRouter && orKey) {
259
+ const res = await fetchWithTimeout('https://openrouter.ai/api/v1/chat/completions', {
260
+ method: 'POST',
261
+ headers: {
262
+ Authorization: `Bearer ${orKey}`,
263
+ 'content-type': 'application/json',
264
+ 'HTTP-Referer': 'https://github.com/construct',
265
+ },
266
+ body: JSON.stringify({
267
+ model: openRouterModelId,
268
+ max_tokens: 4096,
269
+ messages: [
270
+ { role: 'system', content: SYSTEM_PROMPT },
271
+ { role: 'user', content: userContent },
272
+ ],
273
+ }),
274
+ });
275
+ if (res.status === 429) {
276
+ lastError = new Error(`OpenRouter rate-limited on ${modelId}`);
277
+ void telGeneration({ traceId, id: genId, name: 'schema-infer-attempt', model: modelId, input: userContent, startTime, endTime: new Date().toISOString(), level: 'WARNING', statusMessage: 'rate-limited' });
278
+ continue;
279
+ }
280
+ if (!res.ok) throw new Error(`OpenRouter API ${res.status}: ${await res.text()}`);
281
+ const data = await res.json();
282
+ const content = data.choices?.[0]?.message?.content;
283
+ if (!content) {
284
+ lastError = new Error(`OpenRouter returned empty response from ${modelId}`);
285
+ void telGeneration({ traceId, id: genId, name: 'schema-infer-attempt', model: modelId, input: userContent, startTime, endTime: new Date().toISOString(), level: 'WARNING', statusMessage: 'empty-response' });
286
+ continue;
287
+ }
288
+ const usage = data.usage ? { input: data.usage.prompt_tokens ?? 0, output: data.usage.completion_tokens ?? 0, total: data.usage.total_tokens ?? 0 } : undefined;
289
+ void telGeneration({ traceId, id: genId, name: 'schema-infer-attempt', model: modelId, input: userContent, output: content, startTime, endTime: new Date().toISOString(), usage });
290
+ return { text: content, model: modelId };
291
+ }
292
+ } catch (err) {
293
+ // Re-throw non-rate-limit, non-timeout errors immediately.
294
+ if (!/rate.limit|429/i.test(err.message) && err.name !== 'AbortError') throw err;
295
+ lastError = err.name === 'AbortError'
296
+ ? new Error(`Timed out waiting for ${modelId} after ${INFERENCE_TIMEOUT_MS}ms`)
297
+ : err;
298
+ void telGeneration({ traceId, id: genId, name: 'schema-infer-attempt', model: modelId, input: userContent, startTime, endTime: new Date().toISOString(), level: 'ERROR', statusMessage: lastError.message });
299
+ }
300
+ }
301
+
302
+ throw lastError ?? new Error('All model candidates exhausted for schema inference');
303
+ }
304
+
305
+ function parseInferenceResponse(raw) {
306
+ const text = typeof raw === 'string' ? raw.trim() : '';
307
+
308
+ // Strip markdown fences if the model emits them despite instructions.
309
+ const stripped = text.replace(/^```(?:json)?\s*/i, '').replace(/\s*```\s*$/, '').trim();
310
+
311
+ try {
312
+ const parsed = JSON.parse(stripped);
313
+ if (!parsed || typeof parsed !== 'object') throw new Error('not an object');
314
+ return parsed;
315
+ } catch (err) {
316
+ throw new Error(`Schema inference returned invalid JSON: ${err.message}\n---\n${text.slice(0, 500)}`);
317
+ }
318
+ }
319
+
320
+ function buildUserPrompt(extracted) {
321
+ const truncationNote = extracted.truncated
322
+ ? `\n\n[NOTE: document was truncated to ${extracted.characters.toLocaleString()} characters]`
323
+ : '';
324
+
325
+ return [
326
+ `Document path: ${extracted.filePath}`,
327
+ `Document type hint: ${extracted.extension}`,
328
+ `Extraction method: ${extracted.extractionMethod}`,
329
+ '',
330
+ '--- BEGIN DOCUMENT TEXT ---',
331
+ extracted.text,
332
+ '--- END DOCUMENT TEXT ---',
333
+ truncationNote,
334
+ ].join('\n');
335
+ }
336
+
337
+ /**
338
+ * Infer a structured schema from a document file.
339
+ *
340
+ * @param {string} filePath Absolute or relative path to the source document.
341
+ * @param {object} opts
342
+ * @param {number} [opts.maxChars=40000] Characters of document text to send to the model.
343
+ * @returns {Promise<SchemaInferenceResult>}
344
+ */
345
+ export async function inferDocumentSchema(filePath, { maxChars = 40_000 } = {}) {
346
+ const traceId = crypto.randomUUID();
347
+ const traceName = 'schema-infer';
348
+ const { userId, sessionId, environment, release } = resolveSessionMeta();
349
+
350
+ void telTrace({
351
+ id: traceId,
352
+ name: traceName,
353
+ input: { filePath, maxChars },
354
+ metadata: { source: 'schema-infer', filePath },
355
+ sessionId,
356
+ userId,
357
+ release,
358
+ environment,
359
+ });
360
+
361
+ const extracted = extractDocumentText(filePath, { maxChars });
362
+ if (!extracted.text || extracted.text.trim().length === 0) {
363
+ void lfUpdateTrace({ id: traceId, output: { error: 'no-text-extracted' }, metadata: { status: 'error' } });
364
+ throw new Error(`No text could be extracted from: ${filePath}`);
365
+ }
366
+
367
+ const userPrompt = buildUserPrompt(extracted);
368
+
369
+ let callResult;
370
+ try {
371
+ callResult = await callModel(userPrompt, { traceId });
372
+ } catch (err) {
373
+ void lfUpdateTrace({ id: traceId, output: { error: err.message }, metadata: { status: 'error' } });
374
+ throw err;
375
+ }
376
+
377
+ const parsed = parseInferenceResponse(callResult.text);
378
+
379
+ const result = {
380
+ document_type: String(parsed.document_type || 'unknown'),
381
+ fields: Array.isArray(parsed.fields) ? parsed.fields : [],
382
+ relationships: Array.isArray(parsed.relationships) ? parsed.relationships : [],
383
+ confidence: typeof parsed.confidence === 'number' ? Math.min(1, Math.max(0, parsed.confidence)) : 0,
384
+ notes: Array.isArray(parsed.notes) ? parsed.notes : [],
385
+ source_path: extracted.filePath,
386
+ source_extension: extracted.extension,
387
+ source_characters: extracted.characters,
388
+ source_truncated: extracted.truncated,
389
+ model: callResult.model,
390
+ inferred_at: new Date().toISOString(),
391
+ };
392
+
393
+ void lfUpdateTrace({
394
+ id: traceId,
395
+ output: { document_type: result.document_type, field_count: result.fields.length, confidence: result.confidence, model: result.model },
396
+ metadata: { status: 'success', model: result.model },
397
+ });
398
+
399
+ void telScore({ traceId, name: 'confidence', value: result.confidence, comment: `${result.fields.length} fields inferred from ${result.source_extension ?? 'unknown'} document` });
400
+
401
+ return result;
402
+ }
403
+
404
+ /**
405
+ * Infer schemas from multiple documents and return a reconciled unified schema.
406
+ *
407
+ * Useful when N documents share a format (e.g. 50 invoices → one canonical schema).
408
+ * Fields that appear in ≥ threshold fraction of documents are kept; required is set
409
+ * to true only when required across all sampled documents.
410
+ *
411
+ * @param {string[]} filePaths
412
+ * @param {object} opts
413
+ * @param {number} [opts.maxChars=40000]
414
+ * @param {number} [opts.sampleSize=10] Max documents to sample.
415
+ * @param {number} [opts.threshold=0.5] Field inclusion threshold (0–1).
416
+ * @returns {Promise<UnifiedSchemaResult>}
417
+ */
418
+ export async function inferUnifiedSchema(filePaths, { maxChars = 40_000, sampleSize = 10, threshold = 0.5 } = {}) {
419
+ if (!Array.isArray(filePaths) || filePaths.length === 0) {
420
+ throw new Error('At least one file path is required');
421
+ }
422
+
423
+ const traceId = crypto.randomUUID();
424
+ const { userId, sessionId, environment, release } = resolveSessionMeta();
425
+
426
+ void telTrace({
427
+ id: traceId,
428
+ name: 'schema-infer-unified',
429
+ input: { filePaths, maxChars, sampleSize, threshold },
430
+ metadata: { source: 'schema-infer', fileCount: filePaths.length },
431
+ sessionId,
432
+ userId,
433
+ release,
434
+ environment,
435
+ });
436
+
437
+ const sample = filePaths.slice(0, sampleSize);
438
+ const results = await Promise.allSettled(sample.map((fp) => inferDocumentSchema(fp, { maxChars })));
439
+
440
+ const successes = results
441
+ .filter((r) => r.status === 'fulfilled')
442
+ .map((r) => r.value);
443
+
444
+ const failures = results
445
+ .filter((r) => r.status === 'rejected')
446
+ .map((r, i) => ({ file: sample[i], error: r.reason?.message || String(r.reason) }));
447
+
448
+ if (successes.length === 0) {
449
+ void lfUpdateTrace({ id: traceId, output: { error: 'all-documents-failed' }, metadata: { status: 'error' } });
450
+ throw new Error('Schema inference failed for all sampled documents');
451
+ }
452
+
453
+ // Reconcile: count field occurrences across all successful inferences.
454
+ const fieldMap = new Map();
455
+ for (const result of successes) {
456
+ const seen = new Set();
457
+ for (const field of result.fields) {
458
+ const key = field.name;
459
+ if (!key || seen.has(key)) continue;
460
+ seen.add(key);
461
+
462
+ if (!fieldMap.has(key)) {
463
+ fieldMap.set(key, { field: { ...field }, count: 0, requiredCount: 0 });
464
+ }
465
+ const entry = fieldMap.get(key);
466
+ entry.count += 1;
467
+ if (field.required) entry.requiredCount += 1;
468
+
469
+ // Accumulate examples and enums.
470
+ if (field.example && !entry.field.example) entry.field.example = field.example;
471
+ if (Array.isArray(field.enum) && field.enum.length > 0) {
472
+ entry.field.enum = [...new Set([...(entry.field.enum || []), ...field.enum])];
473
+ }
474
+ }
475
+ }
476
+
477
+ const minCount = Math.ceil(successes.length * threshold);
478
+ const unifiedFields = [];
479
+ for (const [, entry] of fieldMap) {
480
+ if (entry.count >= minCount) {
481
+ unifiedFields.push({
482
+ ...entry.field,
483
+ required: entry.requiredCount === successes.length,
484
+ occurrence_rate: Math.round((entry.count / successes.length) * 100) / 100,
485
+ });
486
+ }
487
+ }
488
+
489
+ // Sort by occurrence_rate descending then name ascending.
490
+ unifiedFields.sort((a, b) => (b.occurrence_rate - a.occurrence_rate) || a.name.localeCompare(b.name));
491
+
492
+ const avgConfidence = successes.reduce((s, r) => s + r.confidence, 0) / successes.length;
493
+ const documentTypes = [...new Set(successes.map((r) => r.document_type).filter(Boolean))];
494
+
495
+ const unified = {
496
+ document_type: documentTypes.length === 1 ? documentTypes[0] : documentTypes.join(' | '),
497
+ document_types_seen: documentTypes,
498
+ fields: unifiedFields,
499
+ confidence: Math.round(avgConfidence * 100) / 100,
500
+ sampled: sample.length,
501
+ succeeded: successes.length,
502
+ failed: failures.length,
503
+ failures,
504
+ threshold,
505
+ inferred_at: new Date().toISOString(),
506
+ };
507
+
508
+ void lfUpdateTrace({
509
+ id: traceId,
510
+ output: { document_type: unified.document_type, field_count: unifiedFields.length, confidence: unified.confidence, succeeded: unified.succeeded, failed: unified.failed },
511
+ metadata: { status: 'success' },
512
+ });
513
+ void telScore({ traceId, name: 'confidence', value: unified.confidence, comment: `${unifiedFields.length} unified fields from ${unified.succeeded}/${sample.length} documents` });
514
+
515
+ return unified;
516
+ }
517
+
518
+ /**
519
+ * CLI entrypoint for `construct infer`.
520
+ */
521
+ export async function runInferCli(argv = process.argv.slice(2), { cwd = process.cwd() } = {}) {
522
+ const inputs = [];
523
+ let maxChars = 40_000;
524
+ let unified = false;
525
+ let sampleSize = 10;
526
+ let threshold = 0.5;
527
+
528
+ for (const arg of argv) {
529
+ if (arg.startsWith('--max-chars=')) maxChars = Number(arg.split('=')[1]) || 40_000;
530
+ else if (arg.startsWith('--sample=')) sampleSize = Number(arg.split('=')[1]) || 10;
531
+ else if (arg.startsWith('--threshold=')) threshold = Number(arg.split('=')[1]) || 0.5;
532
+ else if (arg === '--unified') unified = true;
533
+ else inputs.push(arg);
534
+ }
535
+
536
+ if (inputs.length === 0) {
537
+ throw new Error(
538
+ 'Usage: construct infer <file> [more files] [--unified] [--max-chars=N] [--sample=N] [--threshold=0.5]'
539
+ );
540
+ }
541
+
542
+ // Resolve all inputs relative to cwd.
543
+ const { resolve: resolvePath } = await import('node:path');
544
+ const resolved = inputs.map((p) => (p.startsWith('/') ? p : resolvePath(cwd, p)));
545
+
546
+ if (unified || resolved.length > 1) {
547
+ return inferUnifiedSchema(resolved, { maxChars, sampleSize, threshold });
548
+ }
549
+
550
+ return inferDocumentSchema(resolved[0], { maxChars });
551
+ }