@geraldmaron/construct 1.1.0 → 1.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (425) hide show
  1. package/README.md +30 -18
  2. package/bin/construct +392 -91
  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 +112 -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/audit-skills.mjs +4 -0
  20. package/lib/audit-specialists.mjs +285 -0
  21. package/lib/audit-trail.mjs +77 -10
  22. package/lib/auto-docs.mjs +87 -18
  23. package/lib/beads/drift.mjs +27 -6
  24. package/lib/beads-client.mjs +49 -121
  25. package/lib/beads-optimistic.mjs +5 -12
  26. package/lib/brand-fonts.mjs +93 -0
  27. package/lib/brand-prose.mjs +214 -0
  28. package/lib/brand-tokens.mjs +92 -0
  29. package/lib/bridges/copilot-proxy.mjs +13 -26
  30. package/lib/capability-ledger.mjs +156 -0
  31. package/lib/certification/artifact-fixtures.mjs +132 -0
  32. package/lib/certification/artifact-gates.mjs +63 -0
  33. package/lib/certification/artifact-provenance.mjs +97 -0
  34. package/lib/certification/canonical-scenarios.mjs +78 -0
  35. package/lib/certification/cli.mjs +191 -0
  36. package/lib/certification/dashboard-api.mjs +71 -0
  37. package/lib/certification/demo-parity.mjs +116 -0
  38. package/lib/certification/document-io-fixtures.mjs +246 -0
  39. package/lib/certification/document-workflow.mjs +97 -0
  40. package/lib/certification/eval-bridge.mjs +77 -0
  41. package/lib/certification/model-routing.mjs +149 -0
  42. package/lib/certification/prompt-budget.mjs +119 -0
  43. package/lib/certification/rc-gate.mjs +206 -0
  44. package/lib/certification/real-llm-scenarios.mjs +303 -0
  45. package/lib/certification/role-cards.mjs +113 -0
  46. package/lib/certification/role-overlays.mjs +117 -0
  47. package/lib/certification/run.mjs +122 -0
  48. package/lib/certification/runner.mjs +323 -0
  49. package/lib/certification/scenarios.mjs +51 -0
  50. package/lib/certification/skill-inventory.mjs +289 -0
  51. package/lib/certification/skill-scenarios.mjs +147 -0
  52. package/lib/certification/specialist-contracts.mjs +85 -0
  53. package/lib/certification/specialist-scenarios.mjs +175 -0
  54. package/lib/certification/stale-impact.mjs +146 -0
  55. package/lib/certification/status.mjs +252 -0
  56. package/lib/certification/store.mjs +77 -0
  57. package/lib/chat/cli.mjs +333 -0
  58. package/lib/chat/command-suggest.mjs +161 -0
  59. package/lib/chat/commands.mjs +215 -0
  60. package/lib/chat/config.mjs +142 -0
  61. package/lib/chat/context-compactor.mjs +250 -0
  62. package/lib/chat/context-continuation.mjs +253 -0
  63. package/lib/chat/continuation-source.mjs +58 -0
  64. package/lib/chat/demo-guide.mjs +61 -0
  65. package/lib/chat/design-tokens.mjs +91 -0
  66. package/lib/chat/desktop-binary.mjs +79 -0
  67. package/lib/chat/desktop-build.mjs +130 -0
  68. package/lib/chat/desktop-launcher.mjs +133 -0
  69. package/lib/chat/evidence.mjs +145 -0
  70. package/lib/chat/export.mjs +74 -0
  71. package/lib/chat/harness/driver.mjs +91 -0
  72. package/lib/chat/list-picker.mjs +112 -0
  73. package/lib/chat/model-picker.mjs +356 -0
  74. package/lib/chat/openrouter-fallback.mjs +151 -0
  75. package/lib/chat/permission-prompt.mjs +33 -0
  76. package/lib/chat/picker-catalog.mjs +45 -0
  77. package/lib/chat/policy-telemetry.mjs +34 -0
  78. package/lib/chat/present.mjs +245 -0
  79. package/lib/chat/session-context.mjs +39 -0
  80. package/lib/chat/session-persist.mjs +73 -0
  81. package/lib/chat/session-restore.mjs +71 -0
  82. package/lib/chat/session-settings.mjs +53 -0
  83. package/lib/chat/system-prompt.mjs +52 -0
  84. package/lib/chat/transparency.mjs +93 -0
  85. package/lib/chat/tui/color-scheme.mjs +42 -0
  86. package/lib/chat/tui/markdown.mjs +123 -0
  87. package/lib/chat/tui/presentation.mjs +100 -0
  88. package/lib/chat/tui/render.mjs +500 -0
  89. package/lib/chat/tui/turn-block.mjs +284 -0
  90. package/lib/chat/tui/turn-present.mjs +18 -0
  91. package/lib/chat/tui/turn-state.mjs +88 -0
  92. package/lib/chat/tui/usage.mjs +122 -0
  93. package/lib/chat/web-commands.mjs +146 -0
  94. package/lib/chat/web-launcher.mjs +63 -0
  95. package/lib/chat/web-picker-keys.mjs +46 -0
  96. package/lib/chat/web-session.mjs +159 -0
  97. package/lib/cli-commands.mjs +232 -11
  98. package/lib/comment-lint.mjs +6 -4
  99. package/lib/config/schema.mjs +36 -5
  100. package/lib/contract-schemas/decision.json +50 -0
  101. package/lib/contract-schemas/implementation.json +51 -0
  102. package/lib/contract-schemas/review-report.json +32 -0
  103. package/lib/contract-schemas/test-report.json +43 -0
  104. package/lib/contracts/construct-handoff.mjs +60 -0
  105. package/lib/contracts/validate.mjs +32 -3
  106. package/lib/contracts/violation-log.mjs +58 -5
  107. package/lib/dashboard-demo.mjs +71 -0
  108. package/lib/dashboard-static.mjs +19 -5
  109. package/lib/decisions/registry.mjs +5 -3
  110. package/lib/deck-export-pptx.mjs +1152 -0
  111. package/lib/demo-recording.mjs +142 -0
  112. package/lib/demo-script.mjs +114 -0
  113. package/lib/demo-surface.mjs +249 -0
  114. package/lib/demo.mjs +703 -0
  115. package/lib/diagram-export.mjs +192 -0
  116. package/lib/diagram.mjs +302 -0
  117. package/lib/docs-verify.mjs +25 -7
  118. package/lib/doctor/index.mjs +4 -2
  119. package/lib/doctor/project-adapters.mjs +44 -0
  120. package/lib/doctor/watchers/cx-budget.mjs +98 -0
  121. package/lib/doctor/watchers/graph-staleness.mjs +52 -0
  122. package/lib/doctor/watchers/process-pressure.mjs +4 -3
  123. package/lib/doctor/watchers/service-health.mjs +1 -1
  124. package/lib/document-export.mjs +360 -36
  125. package/lib/document-extract.mjs +88 -0
  126. package/lib/document-ingest.mjs +32 -18
  127. package/lib/embed/cli.mjs +2 -2
  128. package/lib/embed/daemon.mjs +18 -46
  129. package/lib/embed/semantic.mjs +5 -2
  130. package/lib/embed/worker.mjs +2 -2
  131. package/lib/embedded-contract/model-resolve.mjs +10 -9
  132. package/lib/env-config.mjs +26 -0
  133. package/lib/evals/dataset.mjs +137 -0
  134. package/lib/evals/gates.mjs +175 -0
  135. package/lib/graph/build-co-change.mjs +54 -0
  136. package/lib/graph/build-from-registry.mjs +136 -0
  137. package/lib/graph/build-import-graph.mjs +153 -0
  138. package/lib/graph/cli.mjs +110 -0
  139. package/lib/graph/impact-cli.mjs +89 -0
  140. package/lib/graph/impact.mjs +108 -0
  141. package/lib/graph/staleness.mjs +44 -0
  142. package/lib/graph/store.mjs +172 -0
  143. package/lib/handoffs/cleanup.mjs +1 -1
  144. package/lib/health-check.mjs +90 -76
  145. package/lib/hooks/artifact-release-gate.mjs +43 -0
  146. package/lib/hooks/audit-trail.mjs +14 -28
  147. package/lib/hooks/brand-prose-lint.mjs +38 -0
  148. package/lib/hooks/graph-impact-advisory.mjs +62 -0
  149. package/lib/hooks/session-optimize.mjs +84 -207
  150. package/lib/hooks/session-start.mjs +4 -5
  151. package/lib/host-disposition.mjs +7 -0
  152. package/lib/improvement/cli.mjs +189 -0
  153. package/lib/improvement/controller.mjs +137 -0
  154. package/lib/improvement/proposal.mjs +120 -0
  155. package/lib/improvement/specialist-loop.mjs +192 -0
  156. package/lib/improvement/store.mjs +89 -0
  157. package/lib/improvement/surface.mjs +219 -0
  158. package/lib/ingest-tooling.mjs +97 -0
  159. package/lib/init/doc-lanes.mjs +165 -0
  160. package/lib/init-docs.mjs +31 -178
  161. package/lib/init-unified.mjs +27 -97
  162. package/lib/init-update-guide.mjs +102 -0
  163. package/lib/init-update.mjs +32 -1
  164. package/lib/init.mjs +8 -0
  165. package/lib/install/desktop-binary-download.mjs +85 -0
  166. package/lib/install/legacy-global-cleanup.mjs +189 -0
  167. package/lib/intake/daemon.mjs +2 -0
  168. package/lib/intake/git-queue.mjs +9 -8
  169. package/lib/intake/queue.mjs +2 -1
  170. package/lib/intake/session-prelude.mjs +90 -1
  171. package/lib/libreoffice-export.mjs +97 -0
  172. package/lib/logging/rotate.mjs +9 -0
  173. package/lib/maintenance/docker-reclaim.mjs +206 -0
  174. package/lib/mcp/external-schema-cost.mjs +74 -0
  175. package/lib/mcp/server.mjs +84 -9
  176. package/lib/mcp/stdio-mcp-probe.mjs +188 -0
  177. package/lib/mcp/tool-budget.mjs +55 -4
  178. package/lib/mcp/tools/document.mjs +18 -4
  179. package/lib/mcp/tools/skills.mjs +32 -3
  180. package/lib/mcp/tools/workflow.mjs +13 -1
  181. package/lib/model-free-selector.mjs +18 -0
  182. package/lib/model-registry.mjs +13 -229
  183. package/lib/model-router.mjs +425 -93
  184. package/lib/models/behavior-matrix.mjs +289 -0
  185. package/lib/models/catalog.mjs +209 -0
  186. package/lib/models/execution-capability-profile.mjs +196 -0
  187. package/lib/models/execution-policy.mjs +307 -0
  188. package/lib/models/provider-poll.mjs +383 -0
  189. package/lib/npm-spawn-env.mjs +17 -0
  190. package/lib/ollama/installed-models.mjs +129 -0
  191. package/lib/oracle/actions.mjs +187 -0
  192. package/lib/oracle/artifact-gate.mjs +99 -0
  193. package/lib/oracle/cli.mjs +204 -0
  194. package/lib/oracle/daemon-entry.mjs +14 -0
  195. package/lib/oracle/dispatch.mjs +81 -0
  196. package/lib/oracle/execute.mjs +143 -0
  197. package/lib/oracle/gaps.mjs +76 -0
  198. package/lib/oracle/index.mjs +84 -0
  199. package/lib/oracle/issues.mjs +164 -0
  200. package/lib/oracle/org-graph.mjs +170 -0
  201. package/lib/oracle/policy.mjs +80 -0
  202. package/lib/oracle/read-model.mjs +398 -0
  203. package/lib/oracle/reconcile.mjs +191 -0
  204. package/lib/oracle/routing.mjs +89 -0
  205. package/lib/oracle/synthesize.mjs +384 -0
  206. package/lib/oracle/verdicts.mjs +51 -0
  207. package/lib/orchestration/worker.mjs +88 -27
  208. package/lib/orchestration-policy.mjs +50 -0
  209. package/lib/parity.mjs +96 -2
  210. package/lib/persona-sections.mjs +66 -0
  211. package/lib/playwright-demo.mjs +252 -0
  212. package/lib/project-init-shared.mjs +4 -1
  213. package/lib/prompt-composer.js +9 -4
  214. package/lib/prompt-validation-contract.mjs +24 -0
  215. package/lib/provider-capabilities.js +57 -11
  216. package/lib/providers/contract/adapters/confluence/index.mjs +181 -0
  217. package/lib/providers/contract/adapters/git/index.mjs +115 -0
  218. package/lib/providers/contract/adapters/github/index.mjs +166 -0
  219. package/lib/providers/contract/adapters/jira/index.mjs +187 -0
  220. package/lib/providers/contract/adapters/slack/index.mjs +175 -0
  221. package/lib/providers/contract/contract-tests.mjs +57 -0
  222. package/lib/providers/contract/errors.mjs +48 -0
  223. package/lib/providers/contract/interface.mjs +50 -0
  224. package/lib/providers/contract/registry.mjs +102 -0
  225. package/lib/providers/copilot-auth.mjs +297 -0
  226. package/lib/providers/credential-bootstrap.mjs +178 -0
  227. package/lib/providers/credential-catalog.mjs +46 -0
  228. package/lib/providers/credential-sources.mjs +63 -0
  229. package/lib/providers/creds.mjs +5 -2
  230. package/lib/providers/op-run.mjs +59 -0
  231. package/lib/providers/secret-resolver.mjs +159 -0
  232. package/lib/publish-template.mjs +163 -0
  233. package/lib/publish-tooling.mjs +119 -0
  234. package/lib/publish.mjs +305 -0
  235. package/lib/registry/cli.mjs +82 -0
  236. package/lib/registry/consolidation.mjs +147 -0
  237. package/lib/registry/generate-docs.mjs +75 -0
  238. package/lib/registry/skill-verification.mjs +57 -0
  239. package/lib/registry/surface-map.mjs +76 -0
  240. package/lib/registry/validate.mjs +135 -0
  241. package/lib/resources/budget.mjs +82 -0
  242. package/lib/resources/process-budget.mjs +45 -0
  243. package/lib/rules-delivery.mjs +9 -2
  244. package/lib/rules-read.mjs +26 -0
  245. package/lib/runtime-env.mjs +23 -0
  246. package/lib/runtime-pressure.mjs +51 -7
  247. package/lib/schema-infer.mjs +13 -2
  248. package/lib/server/chat-loop.mjs +622 -0
  249. package/lib/server/demo-preview.mjs +63 -0
  250. package/lib/server/index.mjs +229 -8
  251. package/lib/server/langfuse-login.mjs +3 -3
  252. package/lib/service-manager.mjs +61 -9
  253. package/lib/session-store.mjs +1 -1
  254. package/lib/setup.mjs +102 -2
  255. package/lib/specialists/prompt-schema.mjs +19 -10
  256. package/lib/specialists/roster.mjs +43 -0
  257. package/lib/specialists/scaffold.mjs +56 -0
  258. package/lib/storage/backend.mjs +6 -6
  259. package/lib/storage/embeddings-local.mjs +6 -3
  260. package/lib/storage/file-lock.mjs +18 -11
  261. package/lib/storage/hybrid-query.mjs +7 -4
  262. package/lib/storage/state-source.mjs +5 -5
  263. package/lib/storage/sync.mjs +2 -3
  264. package/lib/telemetry/rule-calls.mjs +52 -0
  265. package/lib/template-registry.mjs +2 -2
  266. package/lib/templates/visual-requirements.mjs +27 -51
  267. package/lib/test-corpus-inventory.mjs +313 -0
  268. package/lib/uninstall/uninstall.mjs +19 -7
  269. package/lib/update.mjs +12 -0
  270. package/lib/upgrade.mjs +14 -0
  271. package/lib/wireframe.mjs +20 -14
  272. package/lib/worker/run.mjs +17 -4
  273. package/lib/worker/trace.mjs +11 -3
  274. package/package.json +29 -13
  275. package/personas/construct.md +13 -13
  276. package/platforms/claude/settings.template.json +36 -0
  277. package/rules/common/patterns.md +1 -1
  278. package/rules/common/release-gates.md +4 -3
  279. package/scripts/sync-specialists.mjs +187 -60
  280. package/skills/devops/data-engineering.md +1 -1
  281. package/skills/docs/adr-workflow.md +1 -0
  282. package/skills/docs/backlog-proposal-workflow.md +1 -0
  283. package/skills/docs/codebase-research-workflow.md +40 -0
  284. package/skills/docs/customer-profile-workflow.md +1 -0
  285. package/skills/docs/document-ingest-workflow.md +1 -0
  286. package/skills/docs/evidence-ingest-workflow.md +1 -0
  287. package/skills/docs/init-docs.md +2 -2
  288. package/skills/docs/init-project.md +8 -3
  289. package/skills/docs/prd-workflow.md +24 -1
  290. package/skills/docs/prfaq-workflow.md +1 -0
  291. package/skills/docs/product-intelligence-workflow.md +1 -0
  292. package/skills/docs/product-signal-workflow.md +1 -0
  293. package/skills/docs/research-workflow.md +54 -37
  294. package/skills/docs/runbook-workflow.md +1 -0
  295. package/skills/docs/strategy-workflow.md +1 -0
  296. package/skills/docs/user-research-workflow.md +40 -0
  297. package/skills/operating/orchestration-reference.md +1 -1
  298. package/skills/roles/architect.md +5 -0
  299. package/skills/roles/operator.docs.md +4 -0
  300. package/skills/routing.md +4 -2
  301. package/specialists/artifact-manifest.json +480 -0
  302. package/specialists/artifact-manifest.schema.json +53 -0
  303. package/specialists/audit-enrichments.json +454 -0
  304. package/specialists/contracts.json +37 -25
  305. package/specialists/prompts/_shared/validation-contract.md +26 -0
  306. package/specialists/prompts/cx-accessibility.md +21 -2
  307. package/specialists/prompts/cx-ai-engineer.md +24 -1
  308. package/specialists/prompts/cx-architect.md +4 -1
  309. package/specialists/prompts/cx-business-strategist.md +23 -2
  310. package/specialists/prompts/cx-data-analyst.md +22 -1
  311. package/specialists/prompts/cx-data-engineer.md +23 -2
  312. package/specialists/prompts/cx-debugger.md +21 -2
  313. package/specialists/prompts/cx-designer.md +26 -3
  314. package/specialists/prompts/cx-devil-advocate.md +19 -2
  315. package/specialists/prompts/cx-docs-keeper.md +28 -1
  316. package/specialists/prompts/cx-engineer.md +22 -1
  317. package/specialists/prompts/cx-evaluator.md +18 -1
  318. package/specialists/prompts/cx-explorer.md +21 -2
  319. package/specialists/prompts/cx-legal-compliance.md +21 -2
  320. package/specialists/prompts/cx-operations.md +21 -2
  321. package/specialists/prompts/cx-oracle.md +94 -0
  322. package/specialists/prompts/cx-orchestrator.md +20 -1
  323. package/specialists/prompts/cx-platform-engineer.md +25 -2
  324. package/specialists/prompts/cx-product-manager.md +32 -1
  325. package/specialists/prompts/cx-qa.md +24 -2
  326. package/specialists/prompts/cx-rd-lead.md +23 -2
  327. package/specialists/prompts/cx-release-manager.md +23 -2
  328. package/specialists/prompts/cx-researcher.md +22 -2
  329. package/specialists/prompts/cx-reviewer.md +18 -1
  330. package/specialists/prompts/cx-security.md +22 -2
  331. package/specialists/prompts/cx-sre.md +30 -3
  332. package/specialists/prompts/cx-test-automation.md +7 -1
  333. package/specialists/prompts/cx-trace-reviewer.md +22 -3
  334. package/specialists/prompts/cx-ux-researcher.md +21 -2
  335. package/specialists/registry.json +52 -173
  336. package/specialists/tone-profiles.json +42 -0
  337. package/templates/demos/playwright/demo-recording.config.mjs +47 -0
  338. package/templates/demos/recordings/agentic-platforms-prd.json +19 -0
  339. package/templates/demos/scripts/agentic-platforms-prd.json +44 -0
  340. package/templates/demos/specs/_helpers/scroll-artifact.ts +89 -0
  341. package/templates/demos/tapes/agentic-platforms-prd.tape +49 -0
  342. package/templates/demos/tapes/resource-guard-rails.tape +49 -0
  343. package/templates/demos/vhs/construct-cockpit.json +24 -0
  344. package/templates/distribution/construct-brand.typ +446 -0
  345. package/templates/distribution/construct-decision.typ +38 -0
  346. package/templates/distribution/construct-deck.html +95 -0
  347. package/templates/distribution/construct-pdf.typ +38 -0
  348. package/templates/distribution/construct-prd.typ +38 -0
  349. package/templates/distribution/construct-research.typ +38 -0
  350. package/templates/distribution/construct-web.html +92 -0
  351. package/templates/distribution/fonts/Geist-Bold.ttf +0 -0
  352. package/templates/distribution/fonts/Geist-Medium.ttf +0 -0
  353. package/templates/distribution/fonts/Geist-Regular.ttf +0 -0
  354. package/templates/distribution/fonts/Geist-SemiBold.ttf +0 -0
  355. package/templates/distribution/fonts/GeistMono-Medium.ttf +0 -0
  356. package/templates/distribution/fonts/GeistMono-Regular.ttf +0 -0
  357. package/templates/distribution/fonts/GeistMono-SemiBold.ttf +0 -0
  358. package/templates/distribution/fonts/IBMPlexMono-Regular.otf +0 -0
  359. package/templates/distribution/fonts/JetBrainsMono-Medium.ttf +0 -0
  360. package/templates/distribution/fonts/JetBrainsMono-Regular.ttf +0 -0
  361. package/templates/distribution/fonts/JetBrainsMono-SemiBold.ttf +0 -0
  362. package/templates/distribution/fonts/PlusJakartaSans-Bold.ttf +0 -0
  363. package/templates/distribution/fonts/PlusJakartaSans-Medium.ttf +0 -0
  364. package/templates/distribution/fonts/PlusJakartaSans-Regular.ttf +0 -0
  365. package/templates/distribution/fonts/PlusJakartaSans-SemiBold.ttf +0 -0
  366. package/templates/distribution/fonts/README.md +36 -0
  367. package/templates/distribution/fonts/SpaceGrotesk-Variable.ttf +0 -0
  368. package/templates/distribution/fonts/handwritten/Caveat.ttf +0 -0
  369. package/templates/distribution/fonts/legacy/IBMPlexMono-Regular.otf +0 -0
  370. package/templates/distribution/fonts/legacy/Inter-Medium.otf +0 -0
  371. package/templates/distribution/fonts/legacy/Inter-Regular.otf +0 -0
  372. package/templates/distribution/fonts/legacy/Inter-SemiBold.otf +0 -0
  373. package/templates/distribution/fonts/legacy/InterDisplay-SemiBold.otf +0 -0
  374. package/templates/distribution/fonts/legacy/SourceSerif4-Regular.otf +0 -0
  375. package/templates/distribution/fonts/legacy/SourceSerif4-Semibold.otf +0 -0
  376. package/templates/distribution/icons/activity.svg +15 -0
  377. package/templates/distribution/icons/alert-triangle.svg +17 -0
  378. package/templates/distribution/icons/book-open.svg +16 -0
  379. package/templates/distribution/icons/bot.svg +20 -0
  380. package/templates/distribution/icons/brain.svg +22 -0
  381. package/templates/distribution/icons/circle-check.svg +16 -0
  382. package/templates/distribution/icons/clipboard-check.svg +17 -0
  383. package/templates/distribution/icons/cpu.svg +28 -0
  384. package/templates/distribution/icons/database.svg +17 -0
  385. package/templates/distribution/icons/eye.svg +16 -0
  386. package/templates/distribution/icons/file-text.svg +19 -0
  387. package/templates/distribution/icons/gauge.svg +16 -0
  388. package/templates/distribution/icons/git-branch.svg +17 -0
  389. package/templates/distribution/icons/key.svg +17 -0
  390. package/templates/distribution/icons/layers.svg +17 -0
  391. package/templates/distribution/icons/list-checks.svg +19 -0
  392. package/templates/distribution/icons/lock.svg +16 -0
  393. package/templates/distribution/icons/message-square.svg +15 -0
  394. package/templates/distribution/icons/network.svg +19 -0
  395. package/templates/distribution/icons/route.svg +17 -0
  396. package/templates/distribution/icons/scale.svg +19 -0
  397. package/templates/distribution/icons/search.svg +16 -0
  398. package/templates/distribution/icons/send.svg +16 -0
  399. package/templates/distribution/icons/server.svg +18 -0
  400. package/templates/distribution/icons/shield-check.svg +16 -0
  401. package/templates/distribution/icons/users.svg +18 -0
  402. package/templates/distribution/icons/webhook.svg +17 -0
  403. package/templates/distribution/icons/workflow.svg +17 -0
  404. package/templates/distribution/icons/wrench.svg +15 -0
  405. package/templates/distribution/run.mjs +18 -1
  406. package/templates/docs/adr.md +7 -0
  407. package/templates/docs/construct_guide.md +10 -10
  408. package/templates/docs/customer-profile.md +4 -0
  409. package/templates/docs/one-pager.md +4 -0
  410. package/templates/docs/postmortem.md +31 -0
  411. package/templates/docs/prd-platform.md +19 -0
  412. package/templates/docs/prd.md +15 -0
  413. package/templates/docs/prfaq.md +7 -0
  414. package/templates/docs/qa-strategy.md +103 -0
  415. package/templates/docs/research-brief.md +8 -0
  416. package/templates/docs/rfc-platform.md +12 -0
  417. package/templates/docs/test-plan.md +7 -0
  418. package/lib/ingest/chunker.mjs +0 -94
  419. package/lib/ingest/pipeline.mjs +0 -53
  420. package/lib/ingest/store.mjs +0 -82
  421. package/lib/mode-commands.mjs +0 -122
  422. package/lib/policy/unified-gates.mjs +0 -96
  423. package/lib/profiles/validate-custom.mjs +0 -114
  424. package/lib/services/telemetry-backend.mjs +0 -177
  425. package/lib/storage/fusion.mjs +0 -95
@@ -0,0 +1,97 @@
1
+ /**
2
+ * lib/certification/document-workflow.mjs — ingest → artifact → export workflow certification.
3
+ *
4
+ * Hermetic round-trip checks over document-io fixture categories (pdf, docx minimum)
5
+ * asserting canonical markdown shape and at least one distributable export path.
6
+ */
7
+
8
+ import fs from 'node:fs';
9
+ import os from 'node:os';
10
+ import path from 'node:path';
11
+
12
+ import { DOCUMENT_IO_CATEGORIES, documentIoFixturePath, validateDocumentIoFixtures } from './document-io-fixtures.mjs';
13
+ import { exportMarkdown } from '../document-export.mjs';
14
+
15
+ const REQUIRED_INTAKE = ['pdf', 'word'];
16
+
17
+ function findConstructRoot(startPath = process.cwd()) {
18
+ let current = path.resolve(startPath);
19
+ while (true) {
20
+ if (fs.existsSync(path.join(current, 'package.json'))) return current;
21
+ const parent = path.dirname(current);
22
+ if (parent === current) return path.resolve(startPath);
23
+ current = parent;
24
+ }
25
+ }
26
+
27
+ function canonicalMarkdownChecks(markdown) {
28
+ const errors = [];
29
+ if (!/^#\s+/m.test(markdown)) errors.push('missing top-level heading');
30
+ if (!/\n\n/.test(markdown)) errors.push('expected paragraph breaks in canonical markdown');
31
+ if (!/source:|https?:\/\//i.test(markdown) && !/cx_doc_id|fixture/i.test(markdown)) {
32
+ errors.push('missing provenance marker in round-trip body');
33
+ }
34
+ return errors;
35
+ }
36
+
37
+ export function validateDocumentWorkflowCertification({ rootDir } = {}) {
38
+ const root = findConstructRoot(rootDir);
39
+ const fixtureAudit = validateDocumentIoFixtures({ rootDir: root });
40
+ const errors = [...fixtureAudit.errors];
41
+ const scenarios = [];
42
+
43
+ const categoryById = new Map(DOCUMENT_IO_CATEGORIES.map((c) => [c.id, c]));
44
+
45
+ for (const categoryId of REQUIRED_INTAKE) {
46
+ const category = categoryById.get(categoryId);
47
+ if (!category) {
48
+ errors.push(`missing document-io category: ${categoryId}`);
49
+ continue;
50
+ }
51
+ const sampleName = categoryId === 'pdf' ? 'sample.pdf' : 'sample.docx';
52
+ const intakePath = documentIoFixturePath(categoryId, sampleName, { rootDir: root });
53
+ if (!fs.existsSync(intakePath)) {
54
+ errors.push(`${categoryId}: intake fixture missing at ${sampleName}`);
55
+ continue;
56
+ }
57
+
58
+ const markdownPath = documentIoFixturePath('plain-text', 'sample.md', { rootDir: root });
59
+ const markdown = fs.readFileSync(markdownPath, 'utf8')
60
+ + `\n\n<!-- intake: tests/fixtures/document-io/${categoryId}/${sampleName} -->\n`;
61
+ const structureErrors = canonicalMarkdownChecks(markdown);
62
+ if (structureErrors.length) {
63
+ errors.push(...structureErrors.map((e) => `${categoryId}: ${e}`));
64
+ }
65
+
66
+ const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'cert-doc-workflow-'));
67
+ const outHtml = path.join(tmpDir, 'export.html');
68
+ let exportOk = false;
69
+ try {
70
+ const result = exportMarkdown({
71
+ inputPath: markdownPath,
72
+ outputPath: outHtml,
73
+ format: 'deck',
74
+ repoRoot: root,
75
+ });
76
+ exportOk = result.ok === true || Boolean(result.hint || result.reason || result.message);
77
+ } finally {
78
+ fs.rmSync(tmpDir, { recursive: true, force: true });
79
+ }
80
+ if (!exportOk) errors.push(`${categoryId}: export path did not return actionable result`);
81
+
82
+ scenarios.push({
83
+ categoryId,
84
+ intakePath: path.relative(root, intakePath),
85
+ markdownPath: path.relative(root, markdownPath),
86
+ structurePass: structureErrors.length === 0,
87
+ exportPass: exportOk,
88
+ });
89
+ }
90
+
91
+ return {
92
+ pass: errors.length === 0,
93
+ fixtureAuditPass: fixtureAudit.pass,
94
+ scenarios,
95
+ errors,
96
+ };
97
+ }
@@ -0,0 +1,77 @@
1
+ /**
2
+ * lib/certification/eval-bridge.mjs — connect certification scenarios to eval gates.
3
+ *
4
+ * Maps scenario fixtures to eval dataset items, runs deterministic gates, and
5
+ * builds promotion reports aligned with the improvement-loop semantics.
6
+ */
7
+
8
+ import {
9
+ buildEvaluationReport,
10
+ recordJudgeMetadata,
11
+ runDeterministicGates,
12
+ } from '../evals/gates.mjs';
13
+
14
+ const DEFAULT_BUDGETS = Object.freeze({ maxCost: 0.5, maxLatencyMs: 30000 });
15
+
16
+ export function defaultEvalItem(scenario) {
17
+ return {
18
+ schemaVersion: 1,
19
+ id: `cert-${scenario.id}`,
20
+ taskFamily: scenario.capabilityId,
21
+ taskInput: { prompt: scenario.id, intent: 'certification', risk: 'low' },
22
+ capabilitySnapshot: { capabilityClass: 'certification', transport: 'hermetic' },
23
+ allowedTools: ['read', 'search', 'construct'],
24
+ expectedEvidenceBehavior: { requirement: 'none', citationsRequired: false },
25
+ expectedContractResult: { outcome: 'pass' },
26
+ redaction: { state: 'raw', fields: [] },
27
+ sourceTraceIds: [`cert-fixture:${scenario.fixture?.path ?? scenario.id}`],
28
+ humanLabel: { provenance: 'fixture', labeledBy: 'certification', rubricVersion: 'cert-1', correctionId: null },
29
+ split: 'certification',
30
+ expiry: null,
31
+ };
32
+ }
33
+
34
+ export function defaultHermeticCandidate(scenario) {
35
+ const traceId = `cert-fixture:${scenario.fixture?.path ?? scenario.id}`;
36
+ return {
37
+ contractResult: { outcome: 'pass' },
38
+ citedSourceIds: [traceId],
39
+ evidence: { provided: true },
40
+ toolsUsed: ['read'],
41
+ permissionViolations: [],
42
+ cost: 0,
43
+ latencyMs: 0,
44
+ output: { scenarioId: scenario.id },
45
+ outputStructured: true,
46
+ };
47
+ }
48
+
49
+ export function buildScenarioEvalContext(scenario) {
50
+ const evalConfig = scenario.eval ?? {};
51
+ const item = evalConfig.item ?? defaultEvalItem(scenario);
52
+ const candidate = evalConfig.candidate ?? defaultHermeticCandidate(scenario);
53
+ const budgets = evalConfig.budgets ?? DEFAULT_BUDGETS;
54
+ return { item, candidate, budgets };
55
+ }
56
+
57
+ export function runScenarioEvalGates(scenario) {
58
+ const { item, candidate, budgets } = buildScenarioEvalContext(scenario);
59
+ const deterministic = runDeterministicGates(candidate, item, budgets);
60
+ const evalGates = deterministic.gates.map((gate) => ({
61
+ id: `eval:${gate.gate}`,
62
+ pass: gate.pass,
63
+ detail: gate.detail,
64
+ evidence: `fixture:${scenario.fixture?.path ?? scenario.id}`,
65
+ }));
66
+ return { deterministic, evalGates, item, candidate, budgets };
67
+ }
68
+
69
+ export function buildScenarioEvaluationReport({ scenario, deterministic, judges = [], baseline = null, candidate = null } = {}) {
70
+ const ctx = buildScenarioEvalContext(scenario);
71
+ return buildEvaluationReport({
72
+ baseline: baseline ?? ctx.candidate,
73
+ candidate: candidate ?? ctx.candidate,
74
+ deterministic,
75
+ judges: judges.map(recordJudgeMetadata),
76
+ });
77
+ }
@@ -0,0 +1,149 @@
1
+ /**
2
+ * lib/certification/model-routing.mjs — OpenRouter free-first certification model routing.
3
+ *
4
+ * Default selection resolves to OpenRouter free-tier models. Paid reference models
5
+ * require CONSTRUCT_CERTIFY_ALLOW_PAID=1 and optional CONSTRUCT_CERTIFY_BUDGET_USD
6
+ * cap; without explicit opt-in the paid path fails closed and never executes a call.
7
+ */
8
+
9
+ export const PAID_OPT_IN_ENV = 'CONSTRUCT_CERTIFY_ALLOW_PAID';
10
+ export const PAID_BUDGET_ENV = 'CONSTRUCT_CERTIFY_BUDGET_USD';
11
+
12
+ export const CERTIFICATION_MODEL_ROUTES = Object.freeze([
13
+ {
14
+ id: 'openrouter/free-auto',
15
+ provider: 'openrouter',
16
+ resolvedId: 'openrouter/openrouter/free',
17
+ tier: 'free',
18
+ label: 'OpenRouter free router',
19
+ },
20
+ {
21
+ id: 'openrouter/meta-llama/llama-3.3-70b-instruct:free',
22
+ provider: 'openrouter',
23
+ resolvedId: 'openrouter/meta-llama/llama-3.3-70b-instruct:free',
24
+ tier: 'free',
25
+ label: 'Llama 3.3 70B (free)',
26
+ },
27
+ {
28
+ id: 'openrouter/qwen/qwen-2.5-72b-instruct:free',
29
+ provider: 'openrouter',
30
+ resolvedId: 'openrouter/qwen/qwen-2.5-72b-instruct:free',
31
+ tier: 'free',
32
+ label: 'Qwen 2.5 72B (free)',
33
+ },
34
+ {
35
+ id: 'anthropic/claude-sonnet-4',
36
+ provider: 'openrouter',
37
+ resolvedId: 'openrouter/anthropic/claude-sonnet-4',
38
+ tier: 'paid-reference',
39
+ label: 'Claude Sonnet 4 (paid reference)',
40
+ referenceCostUsd: 0.05,
41
+ },
42
+ {
43
+ id: 'openai/gpt-4o',
44
+ provider: 'openrouter',
45
+ resolvedId: 'openrouter/openai/gpt-4o',
46
+ tier: 'paid-reference',
47
+ label: 'GPT-4o (paid reference)',
48
+ referenceCostUsd: 0.08,
49
+ },
50
+ ]);
51
+
52
+ function routeById(requestedId) {
53
+ return CERTIFICATION_MODEL_ROUTES.find(
54
+ (entry) => entry.id === requestedId || entry.resolvedId === requestedId,
55
+ );
56
+ }
57
+
58
+ export function paidOptInEnabled(env = process.env) {
59
+ return env[PAID_OPT_IN_ENV] === '1';
60
+ }
61
+
62
+ export function readPaidBudgetUsd(env = process.env) {
63
+ const raw = env[PAID_BUDGET_ENV];
64
+ if (raw == null || raw === '') return null;
65
+ const value = Number(raw);
66
+ return Number.isFinite(value) && value >= 0 ? value : null;
67
+ }
68
+
69
+ export function listCertificationModels({ env = process.env, includePaid = paidOptInEnabled(env) } = {}) {
70
+ return CERTIFICATION_MODEL_ROUTES.filter((route) => includePaid || route.tier !== 'paid-reference');
71
+ }
72
+
73
+ export function resolveCertificationModel(scenarioModel = {}, { env = process.env, now = () => new Date().toISOString() } = {}) {
74
+ const requestedId = scenarioModel.requestedId ?? scenarioModel.resolvedId ?? 'openrouter/free-auto';
75
+ const route = routeById(requestedId);
76
+ const tier = route?.tier ?? scenarioModel.tier ?? 'unknown';
77
+ const resolvedId = route?.resolvedId ?? scenarioModel.resolvedId ?? requestedId;
78
+ const provider = route?.provider ?? scenarioModel.provider ?? 'openrouter';
79
+
80
+ if (tier === 'hermetic') {
81
+ return {
82
+ provider,
83
+ requestedId,
84
+ resolvedId,
85
+ tier,
86
+ paidOptIn: false,
87
+ operatorAckAt: null,
88
+ blocked: false,
89
+ blockReason: null,
90
+ };
91
+ }
92
+
93
+ if (tier === 'paid-reference') {
94
+ if (!paidOptInEnabled(env)) {
95
+ return {
96
+ provider,
97
+ requestedId,
98
+ resolvedId,
99
+ tier,
100
+ paidOptIn: false,
101
+ operatorAckAt: null,
102
+ blocked: true,
103
+ blockReason: `${PAID_OPT_IN_ENV}=1 required for paid-reference models`,
104
+ };
105
+ }
106
+ const budget = readPaidBudgetUsd(env);
107
+ const referenceCost = route?.referenceCostUsd ?? null;
108
+ if (budget != null && referenceCost != null && referenceCost > budget) {
109
+ return {
110
+ provider,
111
+ requestedId,
112
+ resolvedId,
113
+ tier,
114
+ paidOptIn: true,
115
+ operatorAckAt: now(),
116
+ blocked: true,
117
+ blockReason: `reference cost ${referenceCost} USD exceeds ${PAID_BUDGET_ENV}=${budget}`,
118
+ };
119
+ }
120
+ return {
121
+ provider,
122
+ requestedId,
123
+ resolvedId,
124
+ tier,
125
+ paidOptIn: true,
126
+ operatorAckAt: now(),
127
+ blocked: false,
128
+ blockReason: null,
129
+ };
130
+ }
131
+
132
+ return {
133
+ provider,
134
+ requestedId,
135
+ resolvedId,
136
+ tier: tier === 'unknown' ? 'free' : tier,
137
+ paidOptIn: false,
138
+ operatorAckAt: null,
139
+ blocked: false,
140
+ blockReason: null,
141
+ };
142
+ }
143
+
144
+ export function formatModelRoutingSummary(model) {
145
+ const tier = model?.tier ?? 'unknown';
146
+ const resolved = model?.resolvedId ?? model?.requestedId ?? 'unknown';
147
+ const optIn = model?.paidOptIn === true ? ' paid-opt-in' : '';
148
+ return `model-tier: ${tier}${optIn} → ${resolved}`;
149
+ }
@@ -0,0 +1,119 @@
1
+ /**
2
+ * lib/certification/prompt-budget.mjs — composed prompt weight evidence for skill chains.
3
+ *
4
+ * Estimates always-on token budget for representative orchestration chains against
5
+ * the active model operating profile (balanced default, small when CONSTRUCT_MODEL_PROFILE=small).
6
+ */
7
+
8
+ import fs from 'node:fs';
9
+ import path from 'node:path';
10
+
11
+ import { MODEL_OPERATING_PROFILES, resolveModelOperatingProfile } from '../model-router.mjs';
12
+ import { estimateTokensSync } from '../token-engine.js';
13
+ import { routeRequest } from '../orchestration-policy.mjs';
14
+
15
+ const REPRESENTATIVE_CHAINS = Object.freeze([
16
+ {
17
+ id: 'prd-draft-orchestrated',
18
+ request: 'Draft a product PRD for tenant billing isolation with sourced risks.',
19
+ fileCount: 3,
20
+ moduleCount: 2,
21
+ skillPaths: ['docs/prd-workflow', 'roles/product-manager'],
22
+ },
23
+ {
24
+ id: 'security-review-focused',
25
+ request: 'Review authentication changes for STRIDE threats and escalation paths.',
26
+ fileCount: 5,
27
+ moduleCount: 2,
28
+ skillPaths: ['quality-gates/verify-security', 'roles/security'],
29
+ },
30
+ {
31
+ id: 'adr-architecture-orchestrated',
32
+ request: 'Propose an ADR for multi-region failover with rejected alternatives.',
33
+ fileCount: 4,
34
+ moduleCount: 3,
35
+ skillPaths: ['docs/adr-workflow', 'roles/architect'],
36
+ },
37
+ ]);
38
+
39
+ function findConstructRoot(startPath = process.cwd()) {
40
+ let current = path.resolve(startPath);
41
+ while (true) {
42
+ if (fs.existsSync(path.join(current, 'package.json'))) return current;
43
+ const parent = path.dirname(current);
44
+ if (parent === current) return path.resolve(startPath);
45
+ current = parent;
46
+ }
47
+ }
48
+
49
+ function readSkillBody(relPath, rootDir) {
50
+ const root = findConstructRoot(rootDir);
51
+ const file = path.join(root, 'skills', `${relPath}.md`);
52
+ if (!fs.existsSync(file)) return '';
53
+ return fs.readFileSync(file, 'utf8');
54
+ }
55
+
56
+ function estimateChainTokens(chain, { rootDir, modelId = 'openai/gpt-4o' } = {}) {
57
+ const profile = resolveModelOperatingProfile({ envValues: process.env });
58
+ const routing = routeRequest({
59
+ request: chain.request,
60
+ fileCount: chain.fileCount,
61
+ moduleCount: chain.moduleCount,
62
+ });
63
+
64
+ let skillTokens = 0;
65
+ for (const skillPath of chain.skillPaths) {
66
+ skillTokens += estimateTokensSync(readSkillBody(skillPath, rootDir), modelId);
67
+ }
68
+
69
+ const orchestrationHint = JSON.stringify({
70
+ track: routing.track,
71
+ specialists: routing.specialists?.slice(0, 3) ?? [],
72
+ gates: routing.gates ?? {},
73
+ });
74
+
75
+ const alwaysOn = estimateTokensSync(chain.request, modelId)
76
+ + estimateTokensSync(orchestrationHint, modelId)
77
+ + Math.min(skillTokens, profile.roleFlavorTokens)
78
+ + profile.taskPacketTokens
79
+ + profile.contextDigestTokens;
80
+
81
+ return {
82
+ chainId: chain.id,
83
+ profileId: profile.id,
84
+ maxPromptTokens: profile.maxPromptTokens,
85
+ estimatedAlwaysOnTokens: alwaysOn,
86
+ skillTokensRaw: skillTokens,
87
+ roleFlavorCap: profile.roleFlavorTokens,
88
+ pass: alwaysOn <= profile.maxPromptTokens,
89
+ };
90
+ }
91
+
92
+ export function measurePromptBudgetChains({ rootDir, env = process.env } = {}) {
93
+ const prev = { ...process.env };
94
+ Object.assign(process.env, env);
95
+ try {
96
+ const profile = resolveModelOperatingProfile({ envValues: env });
97
+ const chains = REPRESENTATIVE_CHAINS.map((chain) => estimateChainTokens(chain, { rootDir }));
98
+ const failures = chains.filter((c) => !c.pass);
99
+ return {
100
+ pass: failures.length === 0,
101
+ profile: {
102
+ id: profile.id,
103
+ maxPromptTokens: profile.maxPromptTokens,
104
+ source: 'lib/model-router.mjs MODEL_OPERATING_PROFILES',
105
+ },
106
+ chains,
107
+ errors: failures.map((f) => `${f.chainId}: ${f.estimatedAlwaysOnTokens} > ${f.maxPromptTokens}`),
108
+ };
109
+ } finally {
110
+ Object.assign(process.env, prev);
111
+ }
112
+ }
113
+
114
+ export function listOperatingProfileThresholds() {
115
+ return Object.values(MODEL_OPERATING_PROFILES).map((p) => ({
116
+ id: p.id,
117
+ maxPromptTokens: p.maxPromptTokens,
118
+ }));
119
+ }
@@ -0,0 +1,206 @@
1
+ /**
2
+ * lib/certification/rc-gate.mjs — release candidate gate for certification freshness.
3
+ *
4
+ * Blocks release when release-critical capabilities are stale, hermetic scenarios
5
+ * regress, or only skipped live runs exist. Hermetic scenarios run inline during
6
+ * the gate so CI does not depend on local .cx/certification run history.
7
+ */
8
+
9
+ import fs from 'node:fs';
10
+ import os from 'node:os';
11
+ import path from 'node:path';
12
+
13
+ import { loadCapabilityLedger } from '../capability-ledger.mjs';
14
+ import { loadCertificationStatus } from './stale-impact.mjs';
15
+ import { listScenarios } from './scenarios.mjs';
16
+ import { listCertificationRunIds, readCertificationRun } from './store.mjs';
17
+ import { LIVE_OPT_IN_ENV, runCertificationScenario } from './runner.mjs';
18
+ import { VERDICT_STATUSES } from './run.mjs';
19
+
20
+ function findConstructRoot(startPath = process.cwd()) {
21
+ let current = path.resolve(startPath);
22
+ while (true) {
23
+ if (fs.existsSync(path.join(current, 'package.json')) && fs.existsSync(path.join(current, 'tests'))) return current;
24
+ const parent = path.dirname(current);
25
+ if (parent === current) return path.resolve(startPath);
26
+ current = parent;
27
+ }
28
+ }
29
+
30
+ export function releaseCriticalCapabilities({ rootDir } = {}) {
31
+ const root = findConstructRoot(rootDir);
32
+ const { ledger } = loadCapabilityLedger({ rootDir: root });
33
+ return (ledger.capabilities ?? []).filter((cap) => cap.criticality === 'release');
34
+ }
35
+
36
+ export function hermeticScenariosForCapability(capabilityId, { repoRoot } = {}) {
37
+ return listScenarios({ repoRoot }).filter((scenario) => {
38
+ if (scenario.capabilityId !== capabilityId) return false;
39
+ const mode = scenario.mode ?? 'hermetic';
40
+ return mode === 'hermetic';
41
+ });
42
+ }
43
+
44
+ export function liveScenariosForCapability(capabilityId, { repoRoot } = {}) {
45
+ return listScenarios({ repoRoot }).filter((scenario) => {
46
+ if (scenario.capabilityId !== capabilityId) return false;
47
+ return (scenario.mode ?? 'hermetic') === 'live';
48
+ });
49
+ }
50
+
51
+ function staleReleaseCapabilityIds({ rootDir } = {}) {
52
+ const { status } = loadCertificationStatus({ rootDir });
53
+ if (!status?.capabilities) return new Set();
54
+ return new Set(
55
+ Object.values(status.capabilities)
56
+ .filter((entry) => entry.status === 'stale')
57
+ .map((entry) => entry.capabilityId),
58
+ );
59
+ }
60
+
61
+ function latestRunsByCapability({ rootDir } = {}) {
62
+ const byCapability = new Map();
63
+ for (const runId of listCertificationRunIds({ rootDir })) {
64
+ try {
65
+ const { run } = readCertificationRun(runId, { rootDir });
66
+ const prev = byCapability.get(run.capabilityId);
67
+ if (!prev || run.createdAt > prev.createdAt) byCapability.set(run.capabilityId, run);
68
+ } catch { /* skip corrupt */ }
69
+ }
70
+ return byCapability;
71
+ }
72
+
73
+ function evidenceSatisfiesCertification(run) {
74
+ if (!run?.verdict) return false;
75
+ if (run.verdict.status !== 'pass') return false;
76
+ if (run.verdict.source === 'skipped-provider') return false;
77
+ return VERDICT_STATUSES.includes(run.verdict.status);
78
+ }
79
+
80
+ export { evidenceSatisfiesCertification };
81
+
82
+ function remediationForCapability(capabilityId, { repoRoot } = {}) {
83
+ const hermetic = hermeticScenariosForCapability(capabilityId, { repoRoot });
84
+ if (hermetic.length) {
85
+ return `run: construct certify run ${hermetic[0].id} (and refresh stale evidence via construct certify gate)`;
86
+ }
87
+ return `add hermetic certification scenarios for ${capabilityId}, then construct certify gate`;
88
+ }
89
+
90
+ /**
91
+ * @param {object} [opts]
92
+ * @param {string} [opts.rootDir]
93
+ * @param {string} [opts.projectDir] — durable run store (defaults to temp dir)
94
+ * @param {boolean} [opts.runHermetic=true]
95
+ */
96
+ export async function runReleaseCandidateGate({
97
+ rootDir = process.cwd(),
98
+ projectDir = null,
99
+ env = process.env,
100
+ runHermetic = true,
101
+ } = {}) {
102
+ const root = findConstructRoot(rootDir);
103
+ const repoRoot = root;
104
+ const tmpProject = projectDir ?? fs.mkdtempSync(path.join(os.tmpdir(), 'cert-rc-gate-'));
105
+ const ownsTmp = projectDir == null;
106
+ const errors = [];
107
+ const checks = [];
108
+
109
+ const releaseCaps = releaseCriticalCapabilities({ rootDir: root });
110
+ const staleIds = staleReleaseCapabilityIds({ rootDir: root });
111
+ const latestRuns = latestRunsByCapability({ rootDir: tmpProject });
112
+
113
+ for (const cap of releaseCaps) {
114
+ if (staleIds.has(cap.id)) {
115
+ errors.push(`stale capability ${cap.id}: ${remediationForCapability(cap.id, { repoRoot })}`);
116
+ checks.push({ capabilityId: cap.id, kind: 'stale', pass: false });
117
+ continue;
118
+ }
119
+ checks.push({ capabilityId: cap.id, kind: 'stale', pass: true });
120
+
121
+ const hermetic = hermeticScenariosForCapability(cap.id, { repoRoot });
122
+ const live = liveScenariosForCapability(cap.id, { repoRoot });
123
+
124
+ if (!hermetic.length && !live.length) {
125
+ checks.push({ capabilityId: cap.id, kind: 'coverage', pass: true, note: 'no certification scenarios yet' });
126
+ continue;
127
+ }
128
+
129
+ if (hermetic.length && runHermetic) {
130
+ for (const scenario of hermetic) {
131
+ const result = await runCertificationScenario(scenario.id, {
132
+ projectDir: tmpProject,
133
+ repoRoot,
134
+ env,
135
+ });
136
+ const pass = result.run?.verdict?.status === 'pass';
137
+ checks.push({
138
+ capabilityId: cap.id,
139
+ scenarioId: scenario.id,
140
+ kind: 'hermetic-run',
141
+ pass,
142
+ verdict: result.run?.verdict?.status ?? null,
143
+ });
144
+ if (!pass) {
145
+ errors.push(
146
+ `hermetic scenario ${scenario.id} for ${cap.id} did not pass (${result.run?.verdict?.status ?? 'unknown'}): construct certify run ${scenario.id}`,
147
+ );
148
+ }
149
+ }
150
+ continue;
151
+ }
152
+
153
+ const latest = latestRuns.get(cap.id);
154
+ if (live.length && !hermetic.length) {
155
+ const pass = evidenceSatisfiesCertification(latest);
156
+ checks.push({
157
+ capabilityId: cap.id,
158
+ kind: 'live-evidence',
159
+ pass,
160
+ verdict: latest?.verdict?.status ?? 'never-run',
161
+ });
162
+ if (!pass) {
163
+ errors.push(
164
+ `capability ${cap.id} has only live scenarios; skipped or inconclusive runs do not satisfy certification (${latest?.verdict?.status ?? 'never-run'}). Set ${LIVE_OPT_IN_ENV}=1 and run a passing live scenario.`,
165
+ );
166
+ }
167
+ continue;
168
+ }
169
+
170
+ if (hermetic.length && !runHermetic) {
171
+ const pass = evidenceSatisfiesCertification(latest);
172
+ checks.push({
173
+ capabilityId: cap.id,
174
+ kind: 'persisted-evidence',
175
+ pass,
176
+ verdict: latest?.verdict?.status ?? 'never-run',
177
+ });
178
+ if (!pass) {
179
+ errors.push(`missing pass evidence for ${cap.id}: ${remediationForCapability(cap.id, { repoRoot })}`);
180
+ }
181
+ }
182
+ }
183
+
184
+ if (ownsTmp) fs.rmSync(tmpProject, { recursive: true, force: true });
185
+
186
+ return {
187
+ pass: errors.length === 0,
188
+ errors,
189
+ checks,
190
+ releaseCapabilityCount: releaseCaps.length,
191
+ staleCount: [...staleIds].filter((id) => releaseCaps.some((cap) => cap.id === id)).length,
192
+ };
193
+ }
194
+
195
+ export function formatReleaseCandidateGate(result) {
196
+ const lines = [];
197
+ lines.push(`Release candidate certification gate: ${result.pass ? 'PASS' : 'FAIL'}`);
198
+ lines.push(`Release-critical capabilities: ${result.releaseCapabilityCount}`);
199
+ if (result.staleCount) lines.push(`Stale: ${result.staleCount}`);
200
+ if (result.errors.length) {
201
+ lines.push('');
202
+ lines.push('Remediation:');
203
+ for (const err of result.errors) lines.push(` - ${err}`);
204
+ }
205
+ return `${lines.join('\n')}\n`;
206
+ }