@geraldmaron/construct 1.1.1 → 1.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (426) hide show
  1. package/README.md +30 -19
  2. package/bin/construct +487 -87
  3. package/bin/construct-postinstall.mjs +30 -2
  4. package/examples/distribution/README.md +42 -0
  5. package/examples/distribution/manifest.json +53 -0
  6. package/examples/distribution/sources/adr.md +84 -0
  7. package/examples/distribution/sources/deck-one-pager.md +65 -0
  8. package/examples/distribution/sources/prd-platform.md +161 -0
  9. package/examples/distribution/sources/research-brief.md +80 -0
  10. package/examples/distribution/sources/rfc-platform.md +82 -0
  11. package/examples/distribution/sources/runbook.md +103 -0
  12. package/examples/distribution/sources/strategy.md +88 -0
  13. package/lib/adapters-sync.mjs +67 -0
  14. package/lib/artifact-gate-notice.mjs +38 -0
  15. package/lib/artifact-manifest.mjs +258 -0
  16. package/lib/artifact-release-gate.mjs +171 -0
  17. package/lib/artifact-reviewers.mjs +68 -0
  18. package/lib/artifact-type-from-path.mjs +79 -0
  19. package/lib/artifact-workflow.mjs +252 -0
  20. package/lib/audit-skills.mjs +4 -0
  21. package/lib/audit-specialists.mjs +285 -0
  22. package/lib/audit-trail.mjs +1 -1
  23. package/lib/auto-docs.mjs +90 -19
  24. package/lib/beads/drift.mjs +27 -6
  25. package/lib/beads-client.mjs +49 -121
  26. package/lib/beads-optimistic.mjs +5 -12
  27. package/lib/brand-fonts.mjs +93 -0
  28. package/lib/brand-prose.mjs +214 -0
  29. package/lib/brand-tokens.mjs +92 -0
  30. package/lib/bridges/copilot-proxy.mjs +13 -26
  31. package/lib/capability-ledger.mjs +156 -0
  32. package/lib/certification/artifact-fixtures.mjs +132 -0
  33. package/lib/certification/artifact-gates.mjs +63 -0
  34. package/lib/certification/artifact-provenance.mjs +97 -0
  35. package/lib/certification/canonical-scenarios.mjs +78 -0
  36. package/lib/certification/cli.mjs +191 -0
  37. package/lib/certification/dashboard-api.mjs +71 -0
  38. package/lib/certification/demo-parity.mjs +116 -0
  39. package/lib/certification/document-io-fixtures.mjs +246 -0
  40. package/lib/certification/document-workflow.mjs +97 -0
  41. package/lib/certification/eval-bridge.mjs +77 -0
  42. package/lib/certification/model-routing.mjs +149 -0
  43. package/lib/certification/prompt-budget.mjs +119 -0
  44. package/lib/certification/rc-gate.mjs +206 -0
  45. package/lib/certification/real-llm-scenarios.mjs +303 -0
  46. package/lib/certification/role-cards.mjs +113 -0
  47. package/lib/certification/role-overlays.mjs +117 -0
  48. package/lib/certification/run.mjs +122 -0
  49. package/lib/certification/runner.mjs +323 -0
  50. package/lib/certification/scenarios.mjs +51 -0
  51. package/lib/certification/skill-inventory.mjs +289 -0
  52. package/lib/certification/skill-scenarios.mjs +147 -0
  53. package/lib/certification/specialist-contracts.mjs +85 -0
  54. package/lib/certification/specialist-scenarios.mjs +175 -0
  55. package/lib/certification/stale-impact.mjs +146 -0
  56. package/lib/certification/status.mjs +252 -0
  57. package/lib/certification/store.mjs +77 -0
  58. package/lib/chat/cli.mjs +333 -0
  59. package/lib/chat/command-suggest.mjs +161 -0
  60. package/lib/chat/commands.mjs +215 -0
  61. package/lib/chat/config.mjs +142 -0
  62. package/lib/chat/context-compactor.mjs +250 -0
  63. package/lib/chat/context-continuation.mjs +253 -0
  64. package/lib/chat/continuation-source.mjs +58 -0
  65. package/lib/chat/demo-guide.mjs +61 -0
  66. package/lib/chat/design-tokens.mjs +91 -0
  67. package/lib/chat/desktop-binary.mjs +79 -0
  68. package/lib/chat/desktop-build.mjs +130 -0
  69. package/lib/chat/desktop-launcher.mjs +133 -0
  70. package/lib/chat/evidence.mjs +145 -0
  71. package/lib/chat/export.mjs +74 -0
  72. package/lib/chat/harness/driver.mjs +91 -0
  73. package/lib/chat/list-picker.mjs +112 -0
  74. package/lib/chat/model-picker.mjs +356 -0
  75. package/lib/chat/openrouter-fallback.mjs +151 -0
  76. package/lib/chat/permission-prompt.mjs +33 -0
  77. package/lib/chat/picker-catalog.mjs +45 -0
  78. package/lib/chat/policy-telemetry.mjs +34 -0
  79. package/lib/chat/present.mjs +246 -0
  80. package/lib/chat/session-context.mjs +39 -0
  81. package/lib/chat/session-persist.mjs +73 -0
  82. package/lib/chat/session-restore.mjs +71 -0
  83. package/lib/chat/session-settings.mjs +53 -0
  84. package/lib/chat/system-prompt.mjs +52 -0
  85. package/lib/chat/transparency.mjs +93 -0
  86. package/lib/chat/tui/color-scheme.mjs +42 -0
  87. package/lib/chat/tui/markdown.mjs +123 -0
  88. package/lib/chat/tui/presentation.mjs +100 -0
  89. package/lib/chat/tui/render.mjs +500 -0
  90. package/lib/chat/tui/turn-block.mjs +284 -0
  91. package/lib/chat/tui/turn-present.mjs +18 -0
  92. package/lib/chat/tui/turn-state.mjs +88 -0
  93. package/lib/chat/tui/usage.mjs +122 -0
  94. package/lib/chat/web-commands.mjs +146 -0
  95. package/lib/chat/web-launcher.mjs +63 -0
  96. package/lib/chat/web-picker-keys.mjs +46 -0
  97. package/lib/chat/web-session.mjs +159 -0
  98. package/lib/cli-commands.mjs +146 -15
  99. package/lib/cli-service-inventory.mjs +42 -0
  100. package/lib/comment-lint.mjs +6 -4
  101. package/lib/config/intake-policy.mjs +209 -0
  102. package/lib/config/project-config.mjs +11 -2
  103. package/lib/config/schema.mjs +86 -6
  104. package/lib/config/source-targets.mjs +311 -0
  105. package/lib/contract-schemas/decision.json +50 -0
  106. package/lib/contract-schemas/implementation.json +51 -0
  107. package/lib/contract-schemas/review-report.json +32 -0
  108. package/lib/contract-schemas/test-report.json +43 -0
  109. package/lib/contracts/construct-handoff.mjs +60 -0
  110. package/lib/contracts/validate.mjs +32 -3
  111. package/lib/contracts/violation-log.mjs +58 -5
  112. package/lib/dashboard-demo.mjs +71 -0
  113. package/lib/dashboard-static.mjs +19 -5
  114. package/lib/decisions/registry.mjs +5 -3
  115. package/lib/deck-export-pptx.mjs +1152 -0
  116. package/lib/demo-recording.mjs +142 -0
  117. package/lib/demo-script.mjs +114 -0
  118. package/lib/demo-surface.mjs +249 -0
  119. package/lib/demo.mjs +598 -140
  120. package/lib/diagram-export.mjs +192 -0
  121. package/lib/diagram.mjs +13 -11
  122. package/lib/docs-verify.mjs +25 -7
  123. package/lib/doctor/index.mjs +3 -1
  124. package/lib/doctor/project-adapters.mjs +44 -0
  125. package/lib/doctor/source-checkout.mjs +16 -0
  126. package/lib/doctor/watchers/cx-budget.mjs +98 -0
  127. package/lib/doctor/watchers/graph-staleness.mjs +52 -0
  128. package/lib/doctor/watchers/process-pressure.mjs +4 -3
  129. package/lib/document-export.mjs +384 -38
  130. package/lib/document-extract.mjs +44 -4
  131. package/lib/document-ingest.mjs +29 -20
  132. package/lib/embed/auto-sources.mjs +44 -0
  133. package/lib/embed/cli.mjs +4 -3
  134. package/lib/embed/daemon.mjs +18 -46
  135. package/lib/embed/demand-fetch.mjs +50 -19
  136. package/lib/embed/inbox.mjs +6 -10
  137. package/lib/embed/worker.mjs +4 -3
  138. package/lib/embedded-contract/index.mjs +11 -0
  139. package/lib/embedded-contract/model-resolve.mjs +10 -9
  140. package/lib/env-config.mjs +26 -0
  141. package/lib/evals/dataset.mjs +137 -0
  142. package/lib/evals/gates.mjs +175 -0
  143. package/lib/export-branding.mjs +32 -0
  144. package/lib/graph/build-co-change.mjs +54 -0
  145. package/lib/graph/build-from-registry.mjs +136 -0
  146. package/lib/graph/build-import-graph.mjs +153 -0
  147. package/lib/graph/cli.mjs +110 -0
  148. package/lib/graph/impact-cli.mjs +89 -0
  149. package/lib/graph/impact.mjs +108 -0
  150. package/lib/graph/staleness.mjs +44 -0
  151. package/lib/graph/store.mjs +172 -0
  152. package/lib/health-check.mjs +90 -76
  153. package/lib/hooks/artifact-release-gate.mjs +43 -0
  154. package/lib/hooks/brand-prose-lint.mjs +38 -0
  155. package/lib/hooks/graph-impact-advisory.mjs +62 -0
  156. package/lib/hooks/session-optimize.mjs +84 -207
  157. package/lib/hooks/session-start.mjs +18 -16
  158. package/lib/host-disposition.mjs +7 -0
  159. package/lib/improvement/cli.mjs +189 -0
  160. package/lib/improvement/controller.mjs +137 -0
  161. package/lib/improvement/proposal.mjs +120 -0
  162. package/lib/improvement/specialist-loop.mjs +192 -0
  163. package/lib/improvement/store.mjs +89 -0
  164. package/lib/improvement/surface.mjs +219 -0
  165. package/lib/ingest-tooling.mjs +97 -0
  166. package/lib/init/doc-lanes.mjs +165 -0
  167. package/lib/init-docs.mjs +31 -178
  168. package/lib/init-unified.mjs +50 -119
  169. package/lib/init-update-guide.mjs +102 -0
  170. package/lib/init-update.mjs +32 -1
  171. package/lib/init.mjs +8 -0
  172. package/lib/install/desktop-binary-download.mjs +85 -0
  173. package/lib/install/legacy-global-cleanup.mjs +189 -0
  174. package/lib/intake/constants.mjs +29 -0
  175. package/lib/intake/daemon.mjs +2 -0
  176. package/lib/intake/git-queue.mjs +9 -8
  177. package/lib/intake/intake-config.mjs +52 -105
  178. package/lib/intake/legacy-paths.mjs +5 -0
  179. package/lib/intake/queue.mjs +2 -1
  180. package/lib/intake/session-prelude.mjs +90 -1
  181. package/lib/libreoffice-export.mjs +97 -0
  182. package/lib/logging/rotate.mjs +9 -0
  183. package/lib/maintenance/docker-reclaim.mjs +206 -0
  184. package/lib/mcp/external-schema-cost.mjs +74 -0
  185. package/lib/mcp/server.mjs +103 -10
  186. package/lib/mcp/stdio-mcp-probe.mjs +188 -0
  187. package/lib/mcp/tool-budget.mjs +55 -4
  188. package/lib/mcp/tools/document.mjs +18 -4
  189. package/lib/mcp/tools/embedded-contract.mjs +14 -0
  190. package/lib/mcp/tools/skills.mjs +32 -3
  191. package/lib/mcp/tools/workflow.mjs +13 -1
  192. package/lib/model-free-selector.mjs +18 -0
  193. package/lib/model-registry.mjs +13 -229
  194. package/lib/model-router.mjs +373 -92
  195. package/lib/models/behavior-matrix.mjs +289 -0
  196. package/lib/models/catalog.mjs +209 -0
  197. package/lib/models/execution-capability-profile.mjs +196 -0
  198. package/lib/models/execution-policy.mjs +307 -0
  199. package/lib/models/provider-poll.mjs +383 -0
  200. package/lib/npm-spawn-env.mjs +17 -0
  201. package/lib/ollama/installed-models.mjs +129 -0
  202. package/lib/oracle/actions.mjs +187 -0
  203. package/lib/oracle/artifact-gate.mjs +99 -0
  204. package/lib/oracle/cli.mjs +204 -0
  205. package/lib/oracle/daemon-entry.mjs +14 -0
  206. package/lib/oracle/dispatch.mjs +81 -0
  207. package/lib/oracle/execute.mjs +143 -0
  208. package/lib/oracle/gaps.mjs +76 -0
  209. package/lib/oracle/index.mjs +84 -0
  210. package/lib/oracle/issues.mjs +164 -0
  211. package/lib/oracle/org-graph.mjs +170 -0
  212. package/lib/oracle/policy.mjs +80 -0
  213. package/lib/oracle/read-model.mjs +398 -0
  214. package/lib/oracle/reconcile.mjs +191 -0
  215. package/lib/oracle/routing.mjs +89 -0
  216. package/lib/oracle/synthesize.mjs +384 -0
  217. package/lib/oracle/verdicts.mjs +51 -0
  218. package/lib/orchestration/worker.mjs +88 -27
  219. package/lib/orchestration-policy.mjs +50 -0
  220. package/lib/parity.mjs +96 -2
  221. package/lib/playwright-demo.mjs +254 -0
  222. package/lib/project-init-shared.mjs +4 -1
  223. package/lib/prompt-composer.js +7 -3
  224. package/lib/prompt-validation-contract.mjs +24 -0
  225. package/lib/provider-capabilities.js +57 -11
  226. package/lib/providers/contract/adapters/confluence/index.mjs +181 -0
  227. package/lib/providers/contract/adapters/git/index.mjs +115 -0
  228. package/lib/providers/contract/adapters/github/index.mjs +166 -0
  229. package/lib/providers/contract/adapters/jira/index.mjs +187 -0
  230. package/lib/providers/contract/adapters/slack/index.mjs +175 -0
  231. package/lib/providers/contract/contract-tests.mjs +57 -0
  232. package/lib/providers/contract/errors.mjs +48 -0
  233. package/lib/providers/contract/interface.mjs +50 -0
  234. package/lib/providers/contract/registry.mjs +102 -0
  235. package/lib/providers/copilot-auth.mjs +297 -0
  236. package/lib/providers/credential-bootstrap.mjs +178 -0
  237. package/lib/providers/credential-catalog.mjs +46 -0
  238. package/lib/providers/credential-sources.mjs +63 -0
  239. package/lib/providers/creds.mjs +5 -2
  240. package/lib/providers/op-run.mjs +59 -0
  241. package/lib/providers/secret-resolver.mjs +159 -0
  242. package/lib/publish-template.mjs +163 -0
  243. package/lib/publish-tooling.mjs +119 -0
  244. package/lib/publish.mjs +305 -0
  245. package/lib/registry/cli.mjs +82 -0
  246. package/lib/registry/consolidation.mjs +147 -0
  247. package/lib/registry/generate-docs.mjs +75 -0
  248. package/lib/registry/skill-verification.mjs +57 -0
  249. package/lib/registry/surface-map.mjs +76 -0
  250. package/lib/registry/validate.mjs +135 -0
  251. package/lib/resources/budget.mjs +82 -0
  252. package/lib/resources/process-budget.mjs +45 -0
  253. package/lib/rules-delivery.mjs +9 -2
  254. package/lib/rules-read.mjs +26 -0
  255. package/lib/runtime-env.mjs +23 -0
  256. package/lib/runtime-pressure.mjs +51 -7
  257. package/lib/schema-infer.mjs +13 -2
  258. package/lib/server/chat-loop.mjs +622 -0
  259. package/lib/server/demo-preview.mjs +63 -0
  260. package/lib/server/index.mjs +259 -9
  261. package/lib/server/rate-limit.mjs +5 -3
  262. package/lib/server/webhook.mjs +3 -2
  263. package/lib/service-manager.mjs +60 -8
  264. package/lib/setup.mjs +70 -7
  265. package/lib/specialists/prompt-schema.mjs +19 -10
  266. package/lib/specialists/roster.mjs +43 -0
  267. package/lib/specialists/scaffold.mjs +56 -0
  268. package/lib/storage/backend.mjs +6 -6
  269. package/lib/storage/hybrid-query.mjs +7 -4
  270. package/lib/storage/state-source.mjs +5 -5
  271. package/lib/storage/sync.mjs +2 -3
  272. package/lib/telemetry/rule-calls.mjs +52 -0
  273. package/lib/template-registry.mjs +2 -2
  274. package/lib/templates/visual-requirements.mjs +27 -51
  275. package/lib/test-corpus-inventory.mjs +313 -0
  276. package/lib/uninstall/uninstall.mjs +19 -7
  277. package/lib/update.mjs +12 -0
  278. package/lib/upgrade.mjs +14 -0
  279. package/lib/wireframe.mjs +20 -14
  280. package/lib/worker/run.mjs +17 -4
  281. package/lib/worker/trace.mjs +11 -3
  282. package/package.json +28 -14
  283. package/personas/construct.md +2 -2
  284. package/platforms/claude/settings.template.json +36 -0
  285. package/rules/common/patterns.md +1 -1
  286. package/rules/common/release-gates.md +4 -3
  287. package/scripts/sync-specialists.mjs +41 -28
  288. package/skills/devops/data-engineering.md +1 -1
  289. package/skills/docs/adr-workflow.md +1 -0
  290. package/skills/docs/backlog-proposal-workflow.md +1 -0
  291. package/skills/docs/codebase-research-workflow.md +40 -0
  292. package/skills/docs/customer-profile-workflow.md +1 -0
  293. package/skills/docs/document-ingest-workflow.md +1 -0
  294. package/skills/docs/evidence-ingest-workflow.md +1 -0
  295. package/skills/docs/init-docs.md +2 -2
  296. package/skills/docs/init-project.md +7 -2
  297. package/skills/docs/prd-workflow.md +24 -1
  298. package/skills/docs/prfaq-workflow.md +1 -0
  299. package/skills/docs/product-intelligence-workflow.md +1 -0
  300. package/skills/docs/product-signal-workflow.md +1 -0
  301. package/skills/docs/research-workflow.md +54 -37
  302. package/skills/docs/runbook-workflow.md +1 -0
  303. package/skills/docs/strategy-workflow.md +1 -0
  304. package/skills/docs/user-research-workflow.md +40 -0
  305. package/skills/operating/orchestration-reference.md +1 -1
  306. package/skills/roles/architect.md +5 -0
  307. package/skills/roles/operator.docs.md +4 -0
  308. package/skills/routing.md +4 -2
  309. package/specialists/artifact-manifest.json +489 -0
  310. package/specialists/artifact-manifest.schema.json +83 -0
  311. package/specialists/audit-enrichments.json +454 -0
  312. package/specialists/contracts.json +37 -25
  313. package/specialists/prompts/_shared/validation-contract.md +26 -0
  314. package/specialists/prompts/cx-accessibility.md +21 -2
  315. package/specialists/prompts/cx-ai-engineer.md +24 -1
  316. package/specialists/prompts/cx-architect.md +4 -1
  317. package/specialists/prompts/cx-business-strategist.md +23 -2
  318. package/specialists/prompts/cx-data-analyst.md +22 -1
  319. package/specialists/prompts/cx-data-engineer.md +23 -2
  320. package/specialists/prompts/cx-debugger.md +21 -2
  321. package/specialists/prompts/cx-designer.md +26 -3
  322. package/specialists/prompts/cx-devil-advocate.md +19 -2
  323. package/specialists/prompts/cx-docs-keeper.md +28 -1
  324. package/specialists/prompts/cx-engineer.md +22 -1
  325. package/specialists/prompts/cx-evaluator.md +18 -1
  326. package/specialists/prompts/cx-explorer.md +21 -2
  327. package/specialists/prompts/cx-legal-compliance.md +21 -2
  328. package/specialists/prompts/cx-operations.md +21 -2
  329. package/specialists/prompts/cx-oracle.md +94 -0
  330. package/specialists/prompts/cx-orchestrator.md +20 -1
  331. package/specialists/prompts/cx-platform-engineer.md +25 -2
  332. package/specialists/prompts/cx-product-manager.md +32 -1
  333. package/specialists/prompts/cx-qa.md +24 -2
  334. package/specialists/prompts/cx-rd-lead.md +23 -2
  335. package/specialists/prompts/cx-release-manager.md +23 -2
  336. package/specialists/prompts/cx-researcher.md +22 -2
  337. package/specialists/prompts/cx-reviewer.md +18 -1
  338. package/specialists/prompts/cx-security.md +22 -2
  339. package/specialists/prompts/cx-sre.md +30 -3
  340. package/specialists/prompts/cx-test-automation.md +7 -1
  341. package/specialists/prompts/cx-trace-reviewer.md +22 -3
  342. package/specialists/prompts/cx-ux-researcher.md +21 -2
  343. package/specialists/registry.json +52 -173
  344. package/specialists/tone-profiles.json +42 -0
  345. package/templates/demos/playwright/demo-recording.config.mjs +47 -0
  346. package/templates/demos/recordings/agentic-platforms-prd.json +19 -0
  347. package/templates/demos/scripts/agentic-platforms-prd.json +44 -0
  348. package/templates/demos/specs/_helpers/scroll-artifact.ts +89 -0
  349. package/templates/demos/tapes/agentic-platforms-prd.tape +49 -0
  350. package/templates/demos/tapes/resource-guard-rails.tape +49 -0
  351. package/templates/demos/vhs/construct-cockpit.json +24 -0
  352. package/templates/distribution/construct-brand.typ +446 -0
  353. package/templates/distribution/construct-decision.typ +38 -0
  354. package/templates/distribution/construct-deck.html +95 -0
  355. package/templates/distribution/construct-pdf.typ +38 -0
  356. package/templates/distribution/construct-prd.typ +38 -0
  357. package/templates/distribution/construct-reference.docx +0 -0
  358. package/templates/distribution/construct-research.typ +38 -0
  359. package/templates/distribution/construct-web.html +92 -0
  360. package/templates/distribution/fonts/Geist-Bold.ttf +0 -0
  361. package/templates/distribution/fonts/Geist-Medium.ttf +0 -0
  362. package/templates/distribution/fonts/Geist-Regular.ttf +0 -0
  363. package/templates/distribution/fonts/Geist-SemiBold.ttf +0 -0
  364. package/templates/distribution/fonts/GeistMono-Medium.ttf +0 -0
  365. package/templates/distribution/fonts/GeistMono-Regular.ttf +0 -0
  366. package/templates/distribution/fonts/GeistMono-SemiBold.ttf +0 -0
  367. package/templates/distribution/fonts/IBMPlexMono-Regular.otf +0 -0
  368. package/templates/distribution/fonts/JetBrainsMono-Medium.ttf +0 -0
  369. package/templates/distribution/fonts/JetBrainsMono-Regular.ttf +0 -0
  370. package/templates/distribution/fonts/JetBrainsMono-SemiBold.ttf +0 -0
  371. package/templates/distribution/fonts/PlusJakartaSans-Bold.ttf +0 -0
  372. package/templates/distribution/fonts/PlusJakartaSans-Medium.ttf +0 -0
  373. package/templates/distribution/fonts/PlusJakartaSans-Regular.ttf +0 -0
  374. package/templates/distribution/fonts/PlusJakartaSans-SemiBold.ttf +0 -0
  375. package/templates/distribution/fonts/README.md +36 -0
  376. package/templates/distribution/fonts/SpaceGrotesk-Variable.ttf +0 -0
  377. package/templates/distribution/fonts/handwritten/Caveat.ttf +0 -0
  378. package/templates/distribution/fonts/legacy/IBMPlexMono-Regular.otf +0 -0
  379. package/templates/distribution/fonts/legacy/Inter-Medium.otf +0 -0
  380. package/templates/distribution/fonts/legacy/Inter-Regular.otf +0 -0
  381. package/templates/distribution/fonts/legacy/Inter-SemiBold.otf +0 -0
  382. package/templates/distribution/fonts/legacy/InterDisplay-SemiBold.otf +0 -0
  383. package/templates/distribution/fonts/legacy/SourceSerif4-Regular.otf +0 -0
  384. package/templates/distribution/fonts/legacy/SourceSerif4-Semibold.otf +0 -0
  385. package/templates/distribution/icons/activity.svg +15 -0
  386. package/templates/distribution/icons/alert-triangle.svg +17 -0
  387. package/templates/distribution/icons/book-open.svg +16 -0
  388. package/templates/distribution/icons/bot.svg +20 -0
  389. package/templates/distribution/icons/brain.svg +22 -0
  390. package/templates/distribution/icons/circle-check.svg +16 -0
  391. package/templates/distribution/icons/clipboard-check.svg +17 -0
  392. package/templates/distribution/icons/cpu.svg +28 -0
  393. package/templates/distribution/icons/database.svg +17 -0
  394. package/templates/distribution/icons/eye.svg +16 -0
  395. package/templates/distribution/icons/file-text.svg +19 -0
  396. package/templates/distribution/icons/gauge.svg +16 -0
  397. package/templates/distribution/icons/git-branch.svg +17 -0
  398. package/templates/distribution/icons/key.svg +17 -0
  399. package/templates/distribution/icons/layers.svg +17 -0
  400. package/templates/distribution/icons/list-checks.svg +19 -0
  401. package/templates/distribution/icons/lock.svg +16 -0
  402. package/templates/distribution/icons/message-square.svg +15 -0
  403. package/templates/distribution/icons/network.svg +19 -0
  404. package/templates/distribution/icons/route.svg +17 -0
  405. package/templates/distribution/icons/scale.svg +19 -0
  406. package/templates/distribution/icons/search.svg +16 -0
  407. package/templates/distribution/icons/send.svg +16 -0
  408. package/templates/distribution/icons/server.svg +18 -0
  409. package/templates/distribution/icons/shield-check.svg +16 -0
  410. package/templates/distribution/icons/users.svg +18 -0
  411. package/templates/distribution/icons/webhook.svg +17 -0
  412. package/templates/distribution/icons/workflow.svg +17 -0
  413. package/templates/distribution/icons/wrench.svg +15 -0
  414. package/templates/distribution/run.mjs +18 -1
  415. package/templates/docs/adr.md +7 -0
  416. package/templates/docs/construct_guide.md +8 -8
  417. package/templates/docs/customer-profile.md +4 -0
  418. package/templates/docs/one-pager.md +4 -0
  419. package/templates/docs/postmortem.md +31 -0
  420. package/templates/docs/prd-platform.md +19 -0
  421. package/templates/docs/prd.md +15 -0
  422. package/templates/docs/prfaq.md +7 -0
  423. package/templates/docs/qa-strategy.md +103 -0
  424. package/templates/docs/research-brief.md +8 -0
  425. package/templates/docs/rfc-platform.md +12 -0
  426. package/templates/docs/test-plan.md +7 -0
@@ -0,0 +1,289 @@
1
+ /**
2
+ * lib/models/behavior-matrix.mjs — provider/model behavior matrix (construct-6zga.1.4).
3
+ *
4
+ * The baseline evidence producer for the ExecutionCapabilityProfile
5
+ * (construct-6zga.1.8). It records measured execution behavior keyed by a
6
+ * structural capability CLASS — hosted-direct, hosted-routed, local-constrained,
7
+ * local-capable, unknown — not by vendor name. The class is derived only from the
8
+ * configured provider's transport (a configuration fact) plus measured signals;
9
+ * capability VALUES (tool support, context behavior, usage fields) come only from
10
+ * evidence, never from a model-name regex. Every observation carries an evidence
11
+ * provenance (hermetic_fixture | live_probe | provider_metadata | operator_override)
12
+ * so the profile can declare where each field came from.
13
+ *
14
+ * Hermetic fixtures are the CI source of truth. Live probing is opt-in only: it
15
+ * requires explicit operator confirmation, is run-bounded, redacts metadata, and
16
+ * appends evidence to a separate log — it never ranks providers and never mutates
17
+ * the active capability record. Reference shape: schemas/provider-behavior-matrix.schema.json.
18
+ */
19
+ import { readFileSync } from 'node:fs';
20
+
21
+ export const BEHAVIOR_MATRIX_SCHEMA_VERSION = 1;
22
+
23
+ export const CAPABILITY_CLASSES = Object.freeze([
24
+ 'hosted-direct',
25
+ 'hosted-routed',
26
+ 'local-constrained',
27
+ 'local-capable',
28
+ 'unknown',
29
+ ]);
30
+
31
+ export const TRANSPORTS = Object.freeze(['direct', 'routed', 'local', 'unknown']);
32
+
33
+ export const EVIDENCE_SOURCES = Object.freeze([
34
+ 'hermetic_fixture',
35
+ 'live_probe',
36
+ 'provider_metadata',
37
+ 'operator_override',
38
+ ]);
39
+
40
+ const SYSTEM_ACCEPTANCE = Object.freeze(['accepted', 'rejected', 'ignored', 'unknown']);
41
+ const TOOL_PARSE_SHAPES = Object.freeze(['native_tool_use', 'openai_function', 'none', 'unknown']);
42
+ const TOOL_RESULT_SHAPES = Object.freeze(['tool_result_block', 'message_role_tool', 'none', 'unknown']);
43
+ const CANCEL_MECHANISMS = Object.freeze(['abort_signal', 'none', 'unknown']);
44
+ const CONTEXT_FAILURES = Object.freeze(['error', 'truncate', 'silent_drop', 'unknown']);
45
+ const FALLBACK_CLASSES = Object.freeze(['retryable', 'fatal', 'rate_limited', 'model_unavailable', 'unknown']);
46
+
47
+ const MAX_PROBE_RUNS = 5;
48
+ const SECRET_KEY_RE = /(api[_-]?key|secret|token|authorization|password|bearer)/i;
49
+
50
+ // Transport is the provider's structural topology, taken from the configured
51
+ // provider group (the catalog source of truth in lib/model-router.mjs), mirroring
52
+ // providerGroupId in lib/models/provider-poll.mjs. It is not a capability guess:
53
+ // the aggregator routes a model, a local runtime hosts it, anything else is a
54
+ // direct hosted API. Unknown groups stay 'unknown' rather than assuming direct.
55
+
56
+ const LOCAL_GROUPS = Object.freeze(new Set(['ollama', 'local']));
57
+ const DIRECT_GROUPS = Object.freeze(new Set(['anthropic', 'openai', 'github-copilot']));
58
+
59
+ export function transportForProviderGroup(groupId) {
60
+ const gid = String(groupId || '');
61
+ if (gid.startsWith('openrouter')) return 'routed';
62
+ if (LOCAL_GROUPS.has(gid)) return 'local';
63
+ if (DIRECT_GROUPS.has(gid)) return 'direct';
64
+ return 'unknown';
65
+ }
66
+
67
+ // Class is structural for hosted transports; for local runtimes it splits on
68
+ // measured signals only. Unmeasured local models are 'unknown', never assumed
69
+ // capable — the conservative default the profile relies on.
70
+
71
+ export function classifyCapabilityClass({ transport, measured = {} } = {}) {
72
+ if (transport === 'direct') return 'hosted-direct';
73
+ if (transport === 'routed') return 'hosted-routed';
74
+ if (transport === 'local') {
75
+ if (measured.toolsKnown !== true || typeof measured.coherent !== 'boolean') return 'unknown';
76
+ return measured.tools === true && measured.coherent === true ? 'local-capable' : 'local-constrained';
77
+ }
78
+ return 'unknown';
79
+ }
80
+
81
+ function inEnum(values, value) {
82
+ return values.includes(value);
83
+ }
84
+
85
+ function defaultedString(value, allowed, fallback = 'unknown') {
86
+ return inEnum(allowed, value) ? value : fallback;
87
+ }
88
+
89
+ function nonNegativeInt(value) {
90
+ return Number.isInteger(value) && value >= 0 ? value : 0;
91
+ }
92
+
93
+ /**
94
+ * Normalize a raw measurement into a frozen, schema-shaped observation. Missing
95
+ * fields collapse to conservative 'unknown'/0 defaults rather than being invented.
96
+ */
97
+ export function buildObservation(input = {}) {
98
+ const requestShape = input.requestShape || {};
99
+ const toolCall = input.toolCall || {};
100
+ const cancellation = input.cancellation || {};
101
+ const usage = input.usage || {};
102
+ const evidence = input.evidence || {};
103
+
104
+ const observation = {
105
+ capabilityClass: defaultedString(input.capabilityClass, CAPABILITY_CLASSES),
106
+ transport: defaultedString(input.transport, TRANSPORTS),
107
+ adapterProtocol: typeof input.adapterProtocol === 'string' && input.adapterProtocol
108
+ ? input.adapterProtocol
109
+ : 'unknown',
110
+ responseModelId: typeof input.responseModelId === 'string' ? input.responseModelId : null,
111
+ requestShape: Object.freeze({
112
+ system: defaultedString(requestShape.system, SYSTEM_ACCEPTANCE),
113
+ toolSchemaCount: nonNegativeInt(requestShape.toolSchemaCount),
114
+ toolSchemaTokenEstimate: nonNegativeInt(requestShape.toolSchemaTokenEstimate),
115
+ }),
116
+ toolCall: Object.freeze({
117
+ supported: toolCall.supported === true,
118
+ parseShape: defaultedString(toolCall.parseShape, TOOL_PARSE_SHAPES),
119
+ resultShape: defaultedString(toolCall.resultShape, TOOL_RESULT_SHAPES),
120
+ }),
121
+ cancellation: Object.freeze({
122
+ supported: cancellation.supported === true,
123
+ mechanism: defaultedString(cancellation.mechanism, CANCEL_MECHANISMS),
124
+ }),
125
+ usage: Object.freeze({
126
+ fields: Array.isArray(usage.fields) ? Object.freeze(usage.fields.filter((f) => typeof f === 'string')) : Object.freeze([]),
127
+ cost: usage.cost === true,
128
+ }),
129
+ contextFailureBehavior: defaultedString(input.contextFailureBehavior, CONTEXT_FAILURES),
130
+ fallbackClassification: defaultedString(input.fallbackClassification, FALLBACK_CLASSES),
131
+ evidence: Object.freeze({
132
+ source: defaultedString(evidence.source, EVIDENCE_SOURCES, 'hermetic_fixture'),
133
+ capturedAt: typeof evidence.capturedAt === 'string' ? evidence.capturedAt : null,
134
+ redacted: evidence.redacted === true,
135
+ runs: Number.isInteger(evidence.runs) && evidence.runs >= 1 ? evidence.runs : 1,
136
+ distribution: evidence.distribution && typeof evidence.distribution === 'object' ? evidence.distribution : null,
137
+ }),
138
+ };
139
+ return Object.freeze(observation);
140
+ }
141
+
142
+ function validateObservation(obs, index, errors) {
143
+ const at = `observations[${index}]`;
144
+ if (!obs || typeof obs !== 'object') {
145
+ errors.push(`${at}: not an object`);
146
+ return;
147
+ }
148
+ if (!inEnum(CAPABILITY_CLASSES, obs.capabilityClass)) errors.push(`${at}.capabilityClass invalid: ${obs.capabilityClass}`);
149
+ if (!inEnum(TRANSPORTS, obs.transport)) errors.push(`${at}.transport invalid: ${obs.transport}`);
150
+ if (typeof obs.adapterProtocol !== 'string' || !obs.adapterProtocol) errors.push(`${at}.adapterProtocol missing`);
151
+ const rs = obs.requestShape || {};
152
+ if (!inEnum(SYSTEM_ACCEPTANCE, rs.system)) errors.push(`${at}.requestShape.system invalid: ${rs.system}`);
153
+ if (!Number.isInteger(rs.toolSchemaCount) || rs.toolSchemaCount < 0) errors.push(`${at}.requestShape.toolSchemaCount invalid`);
154
+ if (!Number.isInteger(rs.toolSchemaTokenEstimate) || rs.toolSchemaTokenEstimate < 0) errors.push(`${at}.requestShape.toolSchemaTokenEstimate invalid`);
155
+ const tc = obs.toolCall || {};
156
+ if (typeof tc.supported !== 'boolean') errors.push(`${at}.toolCall.supported invalid`);
157
+ if (!inEnum(TOOL_PARSE_SHAPES, tc.parseShape)) errors.push(`${at}.toolCall.parseShape invalid: ${tc.parseShape}`);
158
+ if (!inEnum(TOOL_RESULT_SHAPES, tc.resultShape)) errors.push(`${at}.toolCall.resultShape invalid: ${tc.resultShape}`);
159
+ const cancel = obs.cancellation || {};
160
+ if (typeof cancel.supported !== 'boolean') errors.push(`${at}.cancellation.supported invalid`);
161
+ if (!inEnum(CANCEL_MECHANISMS, cancel.mechanism)) errors.push(`${at}.cancellation.mechanism invalid: ${cancel.mechanism}`);
162
+ const usage = obs.usage || {};
163
+ if (!Array.isArray(usage.fields)) errors.push(`${at}.usage.fields invalid`);
164
+ if (typeof usage.cost !== 'boolean') errors.push(`${at}.usage.cost invalid`);
165
+ if (!inEnum(CONTEXT_FAILURES, obs.contextFailureBehavior)) errors.push(`${at}.contextFailureBehavior invalid: ${obs.contextFailureBehavior}`);
166
+ if (!inEnum(FALLBACK_CLASSES, obs.fallbackClassification)) errors.push(`${at}.fallbackClassification invalid: ${obs.fallbackClassification}`);
167
+ const ev = obs.evidence || {};
168
+ if (!inEnum(EVIDENCE_SOURCES, ev.source)) errors.push(`${at}.evidence.source invalid: ${ev.source}`);
169
+ if (typeof ev.redacted !== 'boolean') errors.push(`${at}.evidence.redacted invalid`);
170
+ if (!Number.isInteger(ev.runs) || ev.runs < 1) errors.push(`${at}.evidence.runs invalid`);
171
+ }
172
+
173
+ /**
174
+ * Hand-rolled validator (no ajv — Construct stays dependency-free at startup).
175
+ * Returns { valid, errors } against schemas/provider-behavior-matrix.schema.json.
176
+ */
177
+ export function validateBehaviorMatrix(doc) {
178
+ const errors = [];
179
+ if (!doc || typeof doc !== 'object') return { valid: false, errors: ['document is not an object'] };
180
+ if (!Number.isInteger(doc.version) || doc.version < 1) errors.push('version must be an integer >= 1');
181
+ if (!Array.isArray(doc.observations)) {
182
+ errors.push('observations must be an array');
183
+ return { valid: false, errors };
184
+ }
185
+ doc.observations.forEach((obs, i) => validateObservation(obs, i, errors));
186
+ return { valid: errors.length === 0, errors };
187
+ }
188
+
189
+ export function loadBehaviorMatrix(filePath) {
190
+ const doc = JSON.parse(readFileSync(filePath, 'utf8'));
191
+ const { valid, errors } = validateBehaviorMatrix(doc);
192
+ if (!valid) throw new Error(`invalid behavior matrix ${filePath}: ${errors.join('; ')}`);
193
+ return doc;
194
+ }
195
+
196
+ // Repeated probes of one target rarely agree on every field, so the matrix
197
+ // reports a distribution rather than a single verdict. Agreement is the share of
198
+ // runs at the modal class; deterministic means every run agreed.
199
+
200
+ export function summarizeProbeRuns(runs) {
201
+ const list = Array.isArray(runs) ? runs.filter(Boolean) : [];
202
+ if (!list.length) return { runs: 0, deterministic: true, classCounts: {}, modalClass: 'unknown', agreement: 0, toolSupportedRatio: 0 };
203
+ const classCounts = {};
204
+ let toolSupported = 0;
205
+ for (const obs of list) {
206
+ const cls = inEnum(CAPABILITY_CLASSES, obs.capabilityClass) ? obs.capabilityClass : 'unknown';
207
+ classCounts[cls] = (classCounts[cls] || 0) + 1;
208
+ if (obs.toolCall?.supported === true) toolSupported += 1;
209
+ }
210
+ let modalClass = 'unknown';
211
+ let modalCount = -1;
212
+ for (const [cls, count] of Object.entries(classCounts)) {
213
+ if (count > modalCount) { modalClass = cls; modalCount = count; }
214
+ }
215
+ return {
216
+ runs: list.length,
217
+ deterministic: Object.keys(classCounts).length === 1,
218
+ classCounts,
219
+ modalClass,
220
+ agreement: modalCount / list.length,
221
+ toolSupportedRatio: toolSupported / list.length,
222
+ };
223
+ }
224
+
225
+ function redactMetadata(meta) {
226
+ if (!meta || typeof meta !== 'object') return {};
227
+ const out = {};
228
+ for (const [key, value] of Object.entries(meta)) {
229
+ if (SECRET_KEY_RE.test(key)) { out[key] = '[redacted]'; continue; }
230
+ out[key] = typeof value === 'object' && value !== null ? redactMetadata(value) : value;
231
+ }
232
+ return out;
233
+ }
234
+
235
+ /**
236
+ * Opt-in live behavior probe (construct-6zga.1.4 acceptance #2).
237
+ *
238
+ * Requires explicit operator confirmation, bounds the run count, redacts captured
239
+ * metadata, and appends each observation to an append-only sink. It returns
240
+ * evidence and a distribution; it never ranks providers and never writes to the
241
+ * active capability record — promotion is the operator-gated job of the profile
242
+ * inventory (construct-6zga.1.8), not this producer.
243
+ *
244
+ * probeFn is injected so the producer stays provider-agnostic and CI-hermetic;
245
+ * a real run wires it to the W1 probeProviderCapabilities adapter path.
246
+ */
247
+ export async function runLiveBehaviorProbe(modelId, {
248
+ confirm = false,
249
+ runs = 1,
250
+ transport = 'unknown',
251
+ adapterProtocol = 'unknown',
252
+ probeFn,
253
+ appendSink,
254
+ now = () => new Date().toISOString(),
255
+ } = {}) {
256
+ if (confirm !== true) {
257
+ throw new Error('live behavior probe requires explicit operator confirmation (opts.confirm === true)');
258
+ }
259
+ if (typeof probeFn !== 'function') {
260
+ throw new Error('live behavior probe requires an injected probeFn (provider-agnostic, opt-in)');
261
+ }
262
+ const boundedRuns = Math.max(1, Math.min(MAX_PROBE_RUNS, Number.isInteger(runs) ? runs : 1));
263
+ const observations = [];
264
+ for (let i = 0; i < boundedRuns; i += 1) {
265
+ const raw = await probeFn(modelId, { run: i });
266
+ const measured = raw?.measured || {};
267
+ const observation = buildObservation({
268
+ ...raw,
269
+ transport,
270
+ adapterProtocol,
271
+ capabilityClass: classifyCapabilityClass({ transport, measured }),
272
+ evidence: {
273
+ source: 'live_probe',
274
+ capturedAt: now(),
275
+ redacted: true,
276
+ runs: 1,
277
+ distribution: null,
278
+ },
279
+ });
280
+ const record = Object.freeze({ modelId, metadata: Object.freeze(redactMetadata(raw?.metadata)), observation });
281
+ if (typeof appendSink === 'function') await appendSink(record);
282
+ observations.push(observation);
283
+ }
284
+ return {
285
+ modelId,
286
+ observations,
287
+ distribution: boundedRuns > 1 ? summarizeProbeRuns(observations) : null,
288
+ };
289
+ }
@@ -0,0 +1,209 @@
1
+ /**
2
+ * lib/models/catalog.mjs — live model catalog merge, cache, and visibility filter.
3
+ *
4
+ * Merges static provider-family hints with cached OpenRouter free-model polls,
5
+ * then applies construct.config.json models.visibility rules so chat, CLI, and
6
+ * dashboard share one filtered catalog surface.
7
+ */
8
+
9
+ import fs from 'node:fs';
10
+ import path from 'node:path';
11
+ import os from 'node:os';
12
+ import { pollFreeModels } from '../model-free-selector.mjs';
13
+ import { resolveFirstSecret } from '../providers/secret-resolver.mjs';
14
+ import { loadProjectConfig } from '../config/project-config.mjs';
15
+
16
+ export const MODEL_VISIBILITY_MODES = ['all_configured', 'tier_defaults', 'explicit'];
17
+
18
+ export const DEFAULT_MODELS_CONFIG = Object.freeze({
19
+ visibility: Object.freeze({
20
+ mode: 'all_configured',
21
+ include: [],
22
+ exclude: [],
23
+ providers: {},
24
+ }),
25
+ catalog: Object.freeze({
26
+ liveOpenRouter: true,
27
+ maxLiveFree: 24,
28
+ }),
29
+ });
30
+
31
+ const CACHE_FILENAME = 'model-catalog-cache.json';
32
+ const CACHE_TTL_MS = 10 * 60 * 1000;
33
+
34
+ function uniqueStrings(values = []) {
35
+ return [...new Set(values.filter((value) => typeof value === 'string' && value.trim()))];
36
+ }
37
+
38
+ function cachePath(homeDir = os.homedir()) {
39
+ return path.join(homeDir, '.cx', CACHE_FILENAME);
40
+ }
41
+
42
+ export function resolveModelsConfig(projectConfig = {}) {
43
+ const models = projectConfig?.models && typeof projectConfig.models === 'object'
44
+ ? projectConfig.models
45
+ : {};
46
+ const visibility = { ...DEFAULT_MODELS_CONFIG.visibility, ...(models.visibility || {}) };
47
+ const catalog = { ...DEFAULT_MODELS_CONFIG.catalog, ...(models.catalog || {}) };
48
+ if (!MODEL_VISIBILITY_MODES.includes(visibility.mode)) {
49
+ visibility.mode = DEFAULT_MODELS_CONFIG.visibility.mode;
50
+ }
51
+ visibility.include = Array.isArray(visibility.include) ? visibility.include : [];
52
+ visibility.exclude = Array.isArray(visibility.exclude) ? visibility.exclude : [];
53
+ visibility.providers = visibility.providers && typeof visibility.providers === 'object'
54
+ ? visibility.providers
55
+ : {};
56
+ catalog.maxLiveFree = Number.isFinite(catalog.maxLiveFree) ? catalog.maxLiveFree : DEFAULT_MODELS_CONFIG.catalog.maxLiveFree;
57
+ catalog.liveOpenRouter = catalog.liveOpenRouter !== false;
58
+ return { visibility, catalog };
59
+ }
60
+
61
+ export function readLiveCatalogCache({ homeDir = os.homedir(), maxAgeMs = CACHE_TTL_MS } = {}) {
62
+ const file = cachePath(homeDir);
63
+ try {
64
+ if (!fs.existsSync(file)) return null;
65
+ const parsed = JSON.parse(fs.readFileSync(file, 'utf8'));
66
+ if (!parsed?.fetchedAt || !Array.isArray(parsed.models)) return null;
67
+ if (Date.now() - parsed.fetchedAt > maxAgeMs) return null;
68
+ return parsed.models;
69
+ } catch {
70
+ return null;
71
+ }
72
+ }
73
+
74
+ export function writeLiveCatalogCache(models, { homeDir = os.homedir() } = {}) {
75
+ const file = cachePath(homeDir);
76
+ fs.mkdirSync(path.dirname(file), { recursive: true });
77
+ fs.writeFileSync(file, JSON.stringify({
78
+ fetchedAt: Date.now(),
79
+ models: models.map((m) => ({
80
+ id: m.id,
81
+ name: m.name ?? m.id,
82
+ isFree: m.isFree === true,
83
+ })),
84
+ }, null, 2));
85
+ }
86
+
87
+ export async function refreshLiveOpenRouterCatalog({ env = process.env, homeDir = os.homedir() } = {}) {
88
+ const apiKey = resolveFirstSecret(['OPENROUTER_API_KEY', 'OPEN_ROUTER_API_KEY'], { env });
89
+ if (!apiKey) return [];
90
+ const models = await pollFreeModels(apiKey);
91
+ writeLiveCatalogCache(models, { homeDir });
92
+ return models;
93
+ }
94
+
95
+ function providerFamilyEnabled(providerId, visibility) {
96
+ const map = visibility.providers || {};
97
+ if (Object.keys(map).length === 0) return true;
98
+ if (map[providerId] === false) return false;
99
+ if (providerId.startsWith('openrouter') && map.openrouter === false) return false;
100
+ return map[providerId] !== false;
101
+ }
102
+
103
+ function collectTierDefaultIds(registryModels = {}) {
104
+ const ids = [];
105
+ for (const tier of ['reasoning', 'standard', 'fast']) {
106
+ const def = registryModels[tier];
107
+ if (typeof def === 'string') ids.push(def);
108
+ else if (def && typeof def === 'object') {
109
+ if (def.primary) ids.push(def.primary);
110
+ if (Array.isArray(def.fallback)) ids.push(...def.fallback);
111
+ }
112
+ }
113
+ return uniqueStrings(ids);
114
+ }
115
+
116
+ export function mergeLiveModelsIntoProviders(providers, liveModels = [], { maxLiveFree = 24 } = {}) {
117
+ if (!liveModels.length) return providers;
118
+ const liveIds = liveModels.slice(0, maxLiveFree).map((m) => (
119
+ m.id?.startsWith('openrouter/') ? m.id : `openrouter/${m.id}`
120
+ ));
121
+ return providers.map((provider) => {
122
+ if (provider.id !== 'openrouter' && !provider.id.startsWith('openrouter')) return provider;
123
+ const options = {
124
+ reasoning: uniqueStrings([...(provider.options?.reasoning ?? []), ...liveIds]),
125
+ standard: uniqueStrings([...(provider.options?.standard ?? []), ...liveIds]),
126
+ fast: uniqueStrings([...(provider.options?.fast ?? []), ...liveIds]),
127
+ };
128
+ return { ...provider, options, liveModelCount: liveIds.length };
129
+ });
130
+ }
131
+
132
+ export function applyModelVisibilityFilter(catalog, {
133
+ visibility = DEFAULT_MODELS_CONFIG.visibility,
134
+ registryModels = {},
135
+ activeModelId = null,
136
+ } = {}) {
137
+ const includeSet = new Set(visibility.include || []);
138
+ const excludeSet = new Set(visibility.exclude || []);
139
+ const tierDefaults = collectTierDefaultIds(registryModels);
140
+
141
+ const modelAllowed = (modelId, providerId) => {
142
+ if (!modelId) return false;
143
+ if (modelId === activeModelId) return true;
144
+ if (excludeSet.has(modelId)) return false;
145
+ if (!providerFamilyEnabled(providerId, visibility)) return false;
146
+ if (visibility.mode === 'explicit') {
147
+ return includeSet.has(modelId);
148
+ }
149
+ if (visibility.mode === 'tier_defaults') {
150
+ return tierDefaults.includes(modelId);
151
+ }
152
+ return true;
153
+ };
154
+
155
+ const providers = catalog.providers
156
+ .filter((provider) => providerFamilyEnabled(provider.id, visibility))
157
+ .map((provider) => {
158
+ const options = {};
159
+ for (const tier of ['reasoning', 'standard', 'fast']) {
160
+ options[tier] = (provider.options?.[tier] ?? []).filter((id) => modelAllowed(id, provider.id));
161
+ }
162
+ const tiers = { ...provider.tiers };
163
+ for (const tier of ['reasoning', 'standard', 'fast']) {
164
+ if (tiers[tier] && !modelAllowed(tiers[tier], provider.id)) {
165
+ tiers[tier] = options[tier]?.[0] ?? null;
166
+ }
167
+ }
168
+ return { ...provider, options, tiers };
169
+ });
170
+
171
+ const tierOptions = {
172
+ reasoning: uniqueStrings(providers.flatMap((p) => p.options.reasoning)),
173
+ standard: uniqueStrings(providers.flatMap((p) => p.options.standard)),
174
+ fast: uniqueStrings(providers.flatMap((p) => p.options.fast)),
175
+ };
176
+
177
+ return { providers, tierOptions, visibility, activeModelId };
178
+ }
179
+
180
+ export function loadModelsCatalogContext({ cwd = process.cwd(), env = process.env, homeDir = os.homedir() } = {}) {
181
+ const { config } = loadProjectConfig(cwd, env);
182
+ const modelsConfig = resolveModelsConfig(config);
183
+ let registryModels = {};
184
+ try {
185
+ const registryPath = path.join(cwd, 'specialists', 'registry.json');
186
+ if (fs.existsSync(registryPath)) {
187
+ registryModels = JSON.parse(fs.readFileSync(registryPath, 'utf8')).models ?? {};
188
+ }
189
+ } catch { /* registry read is best-effort outside repo root */ }
190
+ const liveModels = modelsConfig.catalog.liveOpenRouter
191
+ ? (readLiveCatalogCache({ homeDir }) ?? [])
192
+ : [];
193
+ return { modelsConfig, registryModels, liveModels };
194
+ }
195
+
196
+ export function isModelVisible(modelId, {
197
+ visibility = DEFAULT_MODELS_CONFIG.visibility,
198
+ registryModels = {},
199
+ activeModelId = null,
200
+ providerId = null,
201
+ } = {}) {
202
+ const filtered = applyModelVisibilityFilter(
203
+ { providers: [{ id: providerId || 'openrouter', options: { reasoning: [modelId], standard: [modelId], fast: [modelId] }, tiers: {} }] },
204
+ { visibility, registryModels, activeModelId },
205
+ );
206
+ return filtered.providers.some((p) =>
207
+ ['reasoning', 'standard', 'fast'].some((tier) => (p.options[tier] ?? []).includes(modelId)),
208
+ );
209
+ }