@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,323 @@
1
+ /**
2
+ * lib/certification/runner.mjs — execute certification scenarios and persist run records.
3
+ *
4
+ * Hermetic scenarios run deterministic gates only. Live scenarios require
5
+ * CONSTRUCT_CERTIFY_LIVE=1; without opt-in the runner records inconclusive and
6
+ * never promotes a skipped provider call to pass. Paid reference models require
7
+ * CONSTRUCT_CERTIFY_ALLOW_PAID=1 with operator ack recorded on the run artifact.
8
+ */
9
+
10
+ import path from 'node:path';
11
+ import fs from 'node:fs';
12
+ import { validateCapabilityLedger } from '../capability-ledger.mjs';
13
+ import { validateCorpusInventory } from '../test-corpus-inventory.mjs';
14
+ import { validateArtifactRelease } from '../artifact-release-gate.mjs';
15
+ import { validateRoleCards } from './role-cards.mjs';
16
+ import { auditSpecialistContracts } from './specialist-contracts.mjs';
17
+ import { validateAllGoldenArtifactGates } from './artifact-gates.mjs';
18
+ import { validateSkillScenarioFixture } from './skill-scenarios.mjs';
19
+ import { validateSpecialistScenarioFixture } from './specialist-scenarios.mjs';
20
+ import { validateAllRoleOverlays } from './role-overlays.mjs';
21
+ import { measurePromptBudgetChains } from './prompt-budget.mjs';
22
+ import { validateAllArtifactProvenance } from './artifact-provenance.mjs';
23
+ import { validateDocumentWorkflowCertification } from './document-workflow.mjs';
24
+ import { buildDemoParityReport } from './demo-parity.mjs';
25
+ import { runRealLlmS3, runRealLlmS8 } from './real-llm-scenarios.mjs';
26
+ import { decidePromotion } from '../evals/gates.mjs';
27
+ import { buildScenarioEvaluationReport, runScenarioEvalGates } from './eval-bridge.mjs';
28
+ import {
29
+ formatModelRoutingSummary,
30
+ listCertificationModels,
31
+ resolveCertificationModel,
32
+ } from './model-routing.mjs';
33
+ import { deriveVerdictFromExecution } from './run.mjs';
34
+ import { fixtureDigest, getScenario, newRunId } from './scenarios.mjs';
35
+ import { writeCertificationRun } from './store.mjs';
36
+
37
+ export const LIVE_OPT_IN_ENV = 'CONSTRUCT_CERTIFY_LIVE';
38
+
39
+ function liveOptInEnabled(env = process.env) {
40
+ return env[LIVE_OPT_IN_ENV] === '1';
41
+ }
42
+
43
+ async function runGate(gate, { root, scenario }) {
44
+ if (gate.type === 'capability-ledger-audit') {
45
+ const result = validateCapabilityLedger({ rootDir: root });
46
+ return { id: gate.id, pass: result.pass, detail: result.errors[0] ?? null };
47
+ }
48
+ if (gate.type === 'corpus-inventory-audit') {
49
+ const result = validateCorpusInventory({ rootDir: root });
50
+ return { id: gate.id, pass: result.pass, detail: result.errors[0] ?? null };
51
+ }
52
+ if (gate.type === 'artifact-release-gate') {
53
+ const rel = scenario.fixture.path;
54
+ const result = validateArtifactRelease({
55
+ filePath: path.join(root, rel),
56
+ type: gate.artifactType ?? 'prd',
57
+ rootDir: root,
58
+ });
59
+ return { id: gate.id, pass: result.ok === true, detail: result.errors?.[0] ?? null };
60
+ }
61
+ if (gate.type === 'role-cards-audit') {
62
+ const result = validateRoleCards({ rootDir: root });
63
+ return { id: gate.id, pass: result.pass, detail: result.errors[0] ?? null };
64
+ }
65
+ if (gate.type === 'specialist-contracts-audit') {
66
+ const result = auditSpecialistContracts({ rootDir: root });
67
+ return { id: gate.id, pass: result.pass, detail: result.failures[0]?.specialistId ?? null };
68
+ }
69
+ if (gate.type === 'skill-scenario-audit') {
70
+ const rel = scenario.fixture.path;
71
+ const fixture = JSON.parse(fs.readFileSync(path.join(root, rel), 'utf8'));
72
+ const result = validateSkillScenarioFixture(fixture, { rootDir: root });
73
+ return { id: gate.id, pass: result.pass, detail: result.errors[0] ?? null };
74
+ }
75
+ if (gate.type === 'artifact-golden-audit') {
76
+ const result = validateAllGoldenArtifactGates({ rootDir: root });
77
+ return { id: gate.id, pass: result.pass, detail: result.errors[0] ?? null };
78
+ }
79
+ if (gate.type === 'specialist-scenario-audit') {
80
+ const rel = scenario.fixture.path;
81
+ const fixture = JSON.parse(fs.readFileSync(path.join(root, rel), 'utf8'));
82
+ const result = validateSpecialistScenarioFixture(fixture, { rootDir: root });
83
+ return { id: gate.id, pass: result.pass, detail: result.errors[0] ?? null };
84
+ }
85
+ if (gate.type === 'role-overlays-audit') {
86
+ const result = validateAllRoleOverlays({ rootDir: root });
87
+ return { id: gate.id, pass: result.pass, detail: result.errors[0] ?? null };
88
+ }
89
+ if (gate.type === 'prompt-budget-audit') {
90
+ const result = measurePromptBudgetChains({ rootDir: root });
91
+ return { id: gate.id, pass: result.pass, detail: result.errors[0] ?? null };
92
+ }
93
+ if (gate.type === 'artifact-provenance-audit') {
94
+ const result = validateAllArtifactProvenance({ rootDir: root, strict: gate.strict !== false });
95
+ return { id: gate.id, pass: result.pass, detail: result.errors[0] ?? null };
96
+ }
97
+ if (gate.type === 'document-workflow-audit') {
98
+ const result = validateDocumentWorkflowCertification({ rootDir: root });
99
+ return { id: gate.id, pass: result.pass, detail: result.errors[0] ?? null };
100
+ }
101
+ if (gate.type === 'demo-parity-audit') {
102
+ const result = buildDemoParityReport({ rootDir: root });
103
+ return { id: gate.id, pass: result.pass, detail: result.mismatches[0]?.detail ?? null };
104
+ }
105
+ if (gate.type === 'real-llm-scenario') {
106
+ const runner = gate.scenario === 's8' ? runRealLlmS8 : runRealLlmS3;
107
+ const result = await runner({ env: process.env });
108
+ if (result.status === 'inconclusive') {
109
+ return { id: gate.id, pass: false, detail: result.skip ?? result.detail ?? 'inconclusive', inconclusive: true };
110
+ }
111
+ return { id: gate.id, pass: result.status === 'pass', detail: result.detail ?? result.status };
112
+ }
113
+ if (gate.type === 'live-provider-smoke') {
114
+ return { id: gate.id, pass: false, detail: 'live provider gate not executed in hermetic mode' };
115
+ }
116
+ return { id: gate.id, pass: false, detail: `unknown gate type: ${gate.type}` };
117
+ }
118
+
119
+ function deriveVerdictWithEval({ scenarioGates = [], evalReport = null, providerSkipped = false, qualitative = null, error = null } = {}) {
120
+ if (providerSkipped || error) {
121
+ return deriveVerdictFromExecution({ providerSkipped, error });
122
+ }
123
+
124
+ const scenarioFailed = scenarioGates.some((gate) => gate?.pass === false && !gate?.inconclusive);
125
+ const inconclusiveGate = scenarioGates.find((gate) => gate?.inconclusive);
126
+ if (inconclusiveGate) {
127
+ return deriveVerdictFromExecution({ providerSkipped: true, error: inconclusiveGate.detail });
128
+ }
129
+ if (scenarioFailed) {
130
+ return deriveVerdictFromExecution({ gates: scenarioGates });
131
+ }
132
+
133
+ const promotion = evalReport?.decision ?? decidePromotion({
134
+ deterministic: evalReport?.blocked != null
135
+ ? { blocked: evalReport.blocked, regressions: (evalReport.gates ?? []).filter((g) => !g.pass).map((g) => g.gate), gates: evalReport.gates ?? [] }
136
+ : null,
137
+ judges: evalReport?.evaluators ?? [],
138
+ });
139
+
140
+ if (!promotion.promotable) {
141
+ const source = evalReport?.blocked ? 'deterministic' : (evalReport?.evaluators?.some((j) => j.verdict === 'fail') ? 'qualitative' : 'deterministic');
142
+ return {
143
+ status: 'fail',
144
+ source,
145
+ reason: promotion.reason,
146
+ };
147
+ }
148
+
149
+ if (qualitative?.abstained) {
150
+ return deriveVerdictFromExecution({ gates: scenarioGates, qualitative });
151
+ }
152
+ if (typeof qualitative?.score === 'number' && qualitative.score < 0.5) {
153
+ return deriveVerdictFromExecution({ gates: scenarioGates, qualitative });
154
+ }
155
+
156
+ return deriveVerdictFromExecution({ gates: scenarioGates, qualitative });
157
+ }
158
+
159
+ export function previewCertificationRun(scenarioId, { repoRoot = process.cwd(), env = process.env, now = () => new Date().toISOString() } = {}) {
160
+ const { scenario } = getScenario(scenarioId, { repoRoot });
161
+ const model = resolveCertificationModel(scenario.model ?? {}, { env, now });
162
+ const mode = scenario.mode ?? 'hermetic';
163
+ const requiresLive = mode === 'live' || scenario.requiresEnv === LIVE_OPT_IN_ENV;
164
+ return {
165
+ scenarioId,
166
+ mode,
167
+ model,
168
+ modelSummary: formatModelRoutingSummary(model),
169
+ requiresLive,
170
+ liveOptIn: liveOptInEnabled(env),
171
+ modelsAvailable: listCertificationModels({ env }),
172
+ };
173
+ }
174
+
175
+ export async function runCertificationScenario(scenarioId, {
176
+ projectDir = process.cwd(),
177
+ repoRoot = projectDir,
178
+ env = process.env,
179
+ now = () => new Date().toISOString(),
180
+ dryRun = false,
181
+ judges = [],
182
+ } = {}) {
183
+ const startedAt = now();
184
+ const startMs = Date.now();
185
+ const { root, scenario } = getScenario(scenarioId, { repoRoot });
186
+ const fixturePath = scenario.fixture.path;
187
+ const sha256 = fixtureDigest(root, fixturePath);
188
+ const mode = scenario.mode ?? 'hermetic';
189
+ const requiresLive = mode === 'live' || scenario.requiresEnv === LIVE_OPT_IN_ENV;
190
+ const resolvedModel = resolveCertificationModel(scenario.model ?? {}, { env, now });
191
+
192
+ if (resolvedModel.blocked) {
193
+ const verdict = deriveVerdictFromExecution({
194
+ providerSkipped: true,
195
+ error: resolvedModel.blockReason,
196
+ });
197
+ const run = {
198
+ schemaVersion: 1,
199
+ id: newRunId(scenarioId),
200
+ scenarioId,
201
+ capabilityId: scenario.capabilityId,
202
+ model: {
203
+ provider: resolvedModel.provider,
204
+ requestedId: resolvedModel.requestedId,
205
+ resolvedId: resolvedModel.resolvedId,
206
+ tier: resolvedModel.tier,
207
+ paidOptIn: resolvedModel.paidOptIn,
208
+ operatorAckAt: resolvedModel.operatorAckAt,
209
+ },
210
+ fixture: { path: fixturePath, sha256 },
211
+ verdict: { ...verdict, reason: resolvedModel.blockReason },
212
+ gates: [],
213
+ evaluation: null,
214
+ qualitative: null,
215
+ timing: { latencyMs: Date.now() - startMs, startedAt, finishedAt: now() },
216
+ cost: null,
217
+ artifacts: null,
218
+ evidenceVersion: 'corpus-inventory:1',
219
+ createdAt: startedAt,
220
+ };
221
+ if (dryRun) return { dryRun: true, run, exitCode: 2, modelSummary: formatModelRoutingSummary(resolvedModel) };
222
+ writeCertificationRun(run, { rootDir: projectDir });
223
+ return { run, exitCode: 2, modelSummary: formatModelRoutingSummary(resolvedModel) };
224
+ }
225
+
226
+ if (requiresLive && !liveOptInEnabled(env)) {
227
+ const verdict = deriveVerdictFromExecution({
228
+ providerSkipped: true,
229
+ gates: [],
230
+ });
231
+ const run = {
232
+ schemaVersion: 1,
233
+ id: newRunId(scenarioId),
234
+ scenarioId,
235
+ capabilityId: scenario.capabilityId,
236
+ model: {
237
+ provider: resolvedModel.provider,
238
+ requestedId: resolvedModel.requestedId,
239
+ resolvedId: resolvedModel.resolvedId,
240
+ tier: resolvedModel.tier,
241
+ paidOptIn: resolvedModel.paidOptIn,
242
+ operatorAckAt: resolvedModel.operatorAckAt,
243
+ },
244
+ fixture: { path: fixturePath, sha256 },
245
+ verdict: { ...verdict, reason: `${LIVE_OPT_IN_ENV}=1 required for live scenarios` },
246
+ gates: [],
247
+ evaluation: null,
248
+ qualitative: null,
249
+ timing: { latencyMs: Date.now() - startMs, startedAt, finishedAt: now() },
250
+ cost: null,
251
+ artifacts: null,
252
+ evidenceVersion: 'corpus-inventory:1',
253
+ createdAt: startedAt,
254
+ };
255
+ if (dryRun) return { dryRun: true, run, exitCode: 2, modelSummary: formatModelRoutingSummary(resolvedModel) };
256
+ writeCertificationRun(run, { rootDir: projectDir });
257
+ return { run, exitCode: 2, modelSummary: formatModelRoutingSummary(resolvedModel) };
258
+ }
259
+
260
+ const scenarioGates = [];
261
+ for (const gate of scenario.gates ?? []) {
262
+ scenarioGates.push(await runGate(gate, { root, scenario }));
263
+ }
264
+
265
+ const { deterministic, evalGates } = runScenarioEvalGates(scenario);
266
+ const evaluation = buildScenarioEvaluationReport({
267
+ scenario,
268
+ deterministic,
269
+ judges,
270
+ });
271
+ const gates = [...scenarioGates, ...evalGates];
272
+
273
+ const qualitative = judges.length > 0
274
+ ? {
275
+ judgeModel: judges[0]?.model ?? null,
276
+ score: judges.every((j) => j.verdict !== 'fail') ? 1 : 0,
277
+ abstained: judges.every((j) => j.verdict === 'abstain'),
278
+ }
279
+ : null;
280
+
281
+ const verdict = deriveVerdictWithEval({
282
+ scenarioGates,
283
+ evalReport: evaluation,
284
+ qualitative,
285
+ });
286
+
287
+ const run = {
288
+ schemaVersion: 1,
289
+ id: newRunId(scenarioId),
290
+ scenarioId,
291
+ capabilityId: scenario.capabilityId,
292
+ model: {
293
+ provider: resolvedModel.provider,
294
+ requestedId: resolvedModel.requestedId,
295
+ resolvedId: resolvedModel.resolvedId,
296
+ tier: resolvedModel.tier,
297
+ paidOptIn: resolvedModel.paidOptIn,
298
+ operatorAckAt: resolvedModel.operatorAckAt,
299
+ },
300
+ fixture: { path: fixturePath, sha256 },
301
+ verdict,
302
+ gates,
303
+ evaluation,
304
+ qualitative,
305
+ timing: { latencyMs: Date.now() - startMs, startedAt, finishedAt: now() },
306
+ cost: null,
307
+ artifacts: null,
308
+ evidenceVersion: 'corpus-inventory:1',
309
+ createdAt: startedAt,
310
+ };
311
+
312
+ if (dryRun) {
313
+ return {
314
+ dryRun: true,
315
+ run,
316
+ exitCode: verdict.status === 'pass' ? 0 : verdict.status === 'fail' ? 1 : 2,
317
+ modelSummary: formatModelRoutingSummary(resolvedModel),
318
+ };
319
+ }
320
+ const persisted = writeCertificationRun(run, { rootDir: projectDir });
321
+ const exitCode = verdict.status === 'pass' ? 0 : verdict.status === 'fail' ? 1 : 2;
322
+ return { ...persisted, exitCode, modelSummary: formatModelRoutingSummary(resolvedModel) };
323
+ }
@@ -0,0 +1,51 @@
1
+ /**
2
+ * lib/certification/scenarios.mjs — load certification scenario catalog and fixtures.
3
+ */
4
+
5
+ import fs from 'node:fs';
6
+ import path from 'node:path';
7
+ import { createHash } from 'node:crypto';
8
+
9
+ function findConstructRoot(startPath = process.cwd()) {
10
+ let current = path.resolve(startPath);
11
+ while (true) {
12
+ if (fs.existsSync(path.join(current, 'package.json')) && fs.existsSync(path.join(current, 'tests'))) return current;
13
+ const parent = path.dirname(current);
14
+ if (parent === current) return path.resolve(startPath);
15
+ current = parent;
16
+ }
17
+ }
18
+
19
+ export function defaultScenarioCatalogPath(rootDir = process.cwd()) {
20
+ return path.join(findConstructRoot(rootDir), 'tests', 'certification', 'scenarios', 'catalog.json');
21
+ }
22
+
23
+ export function loadScenarioCatalog({ repoRoot } = {}) {
24
+ const root = findConstructRoot(repoRoot);
25
+ const filePath = defaultScenarioCatalogPath(root);
26
+ const catalog = JSON.parse(fs.readFileSync(filePath, 'utf8'));
27
+ return { root, filePath, catalog };
28
+ }
29
+
30
+ export function listScenarios({ repoRoot } = {}) {
31
+ const { catalog } = loadScenarioCatalog({ repoRoot });
32
+ return catalog.scenarios ?? [];
33
+ }
34
+
35
+ export function getScenario(scenarioId, { repoRoot } = {}) {
36
+ const { root, catalog } = loadScenarioCatalog({ repoRoot });
37
+ const scenario = (catalog.scenarios ?? []).find((entry) => entry.id === scenarioId);
38
+ if (!scenario) throw new Error(`unknown certification scenario: ${scenarioId}`);
39
+ return { root, scenario };
40
+ }
41
+
42
+ export function fixtureDigest(root, relPath) {
43
+ const absolute = path.join(root, relPath);
44
+ if (!fs.existsSync(absolute)) throw new Error(`fixture does not exist: ${relPath}`);
45
+ return createHash('sha256').update(fs.readFileSync(absolute)).digest('hex');
46
+ }
47
+
48
+ export function newRunId(scenarioId) {
49
+ const stamp = new Date().toISOString().replace(/[:.]/g, '-');
50
+ return `cert-${scenarioId.replace(/[^a-z0-9.-]+/gi, '-')}-${stamp}`;
51
+ }
@@ -0,0 +1,289 @@
1
+ /**
2
+ * lib/certification/skill-inventory.mjs — machine-readable skill corpus inventory.
3
+ *
4
+ * Walks skills/**, extracts id, owner specialist, activation triggers, inputs,
5
+ * outputs, and verification hooks. Flags orphan skills and conflicting output
6
+ * contracts for manifest workflow types. Output feeds tests/certification/skills/inventory.json.
7
+ */
8
+
9
+ import fs from 'node:fs';
10
+ import path from 'node:path';
11
+ import { createHash } from 'node:crypto';
12
+ import { artifactTypes, loadArtifactManifest } from '../artifact-manifest.mjs';
13
+
14
+ function findConstructRoot(startPath = process.cwd()) {
15
+ let current = path.resolve(startPath);
16
+ while (true) {
17
+ if (fs.existsSync(path.join(current, 'specialists', 'registry.json'))) return current;
18
+ const parent = path.dirname(current);
19
+ if (parent === current) return path.resolve(startPath);
20
+ current = parent;
21
+ }
22
+ }
23
+
24
+ function collectSkillFiles(skillsDir) {
25
+ const results = [];
26
+ const walk = (dir, prefix = '') => {
27
+ for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
28
+ if (entry.isDirectory()) {
29
+ walk(path.join(dir, entry.name), prefix ? `${prefix}/${entry.name}` : entry.name);
30
+ } else if (entry.name.endsWith('.md') && entry.name !== 'routing.md') {
31
+ const rel = prefix ? `${prefix}/${entry.name.replace(/\.md$/, '')}` : entry.name.replace(/\.md$/, '');
32
+ results.push(rel);
33
+ }
34
+ }
35
+ };
36
+ try { walk(skillsDir); } catch { /* skills dir missing */ }
37
+ return results.sort();
38
+ }
39
+
40
+ function collectProfileRoles(root) {
41
+ const dir = path.join(root, 'profiles');
42
+ const roles = new Set();
43
+ const scan = (node) => {
44
+ if (Array.isArray(node)) { node.forEach(scan); return; }
45
+ if (node && typeof node === 'object') {
46
+ if (Array.isArray(node.roles)) for (const r of node.roles) if (typeof r === 'string') roles.add(r);
47
+ for (const v of Object.values(node)) scan(v);
48
+ }
49
+ };
50
+ try {
51
+ for (const f of fs.readdirSync(dir)) {
52
+ if (!f.endsWith('.json')) continue;
53
+ try { scan(JSON.parse(fs.readFileSync(path.join(dir, f), 'utf8'))); } catch { /* skip malformed */ }
54
+ }
55
+ } catch { /* no profiles dir */ }
56
+ return roles;
57
+ }
58
+
59
+ function roleBase(skillId) {
60
+ const m = skillId.match(/^roles\/([^.]+)(?:\..*)?$/);
61
+ return m ? m[1] : null;
62
+ }
63
+
64
+ function parseFrontmatter(content) {
65
+ const match = content.match(/^---\r?\n([\s\S]*?)\r?\n---/);
66
+ if (!match) return { meta: {}, body: content };
67
+ const meta = {};
68
+ for (const line of match[1].split('\n')) {
69
+ const kv = line.match(/^([a-zA-Z0-9_-]+):\s*(.*)$/);
70
+ if (!kv) continue;
71
+ const [, key, raw] = kv;
72
+ if (raw.startsWith('[')) {
73
+ try { meta[key] = JSON.parse(raw); } catch { meta[key] = raw; }
74
+ } else {
75
+ meta[key] = raw.replace(/^["']|["']$/g, '');
76
+ }
77
+ }
78
+ return { meta, body: content.slice(match[0].length) };
79
+ }
80
+
81
+ function activationTriggers(meta, body) {
82
+ const triggers = [];
83
+ if (meta.description) triggers.push(meta.description);
84
+ const useWhen = body.match(/^Use when:\s*(.+)$/m)?.[1];
85
+ if (useWhen) triggers.push(useWhen.trim());
86
+ return [...new Set(triggers.filter(Boolean))];
87
+ }
88
+
89
+ function verificationHooks(meta, body) {
90
+ const hooks = [];
91
+ if (meta.verificationBar) hooks.push({ kind: 'verificationBar', value: meta.verificationBar });
92
+ if (/construct artifact validate/i.test(body)) hooks.push({ kind: 'cli', value: 'construct artifact validate' });
93
+ if (/construct certify/i.test(body)) hooks.push({ kind: 'cli', value: 'construct certify' });
94
+ if (/get_template\(/i.test(body)) hooks.push({ kind: 'mcp', value: 'get_template' });
95
+ return hooks;
96
+ }
97
+
98
+ function resolveOwners(skillId, { registry, declaredSkills, ownerBases }) {
99
+ const owners = new Set();
100
+ for (const agent of registry.specialists ?? []) {
101
+ if ((agent.skills ?? []).includes(skillId)) owners.add(`cx-${agent.name}`);
102
+ }
103
+ const base = roleBase(skillId);
104
+ if (base && ownerBases.has(base)) {
105
+ for (const agent of registry.specialists ?? []) {
106
+ if (agent.name === base || agent.name.replace(/^cx-/, '') === base) owners.add(`cx-${agent.name}`);
107
+ }
108
+ if (!owners.size) owners.add(`profile-role:${base}`);
109
+ }
110
+ if (declaredSkills.has(skillId) && owners.size === 0) {
111
+ for (const agent of registry.specialists ?? []) {
112
+ if ((agent.skills ?? []).includes(skillId)) owners.add(`cx-${agent.name}`);
113
+ }
114
+ }
115
+ return [...owners].sort();
116
+ }
117
+
118
+ function workflowSkillIds(manifest) {
119
+ const ids = new Set();
120
+ for (const entry of Object.values(manifest.artifacts ?? {})) {
121
+ if (entry.workflowSkill) ids.add(entry.workflowSkill);
122
+ }
123
+ return ids;
124
+ }
125
+
126
+ function detectConflictingOutputs(skills) {
127
+ const manifestTypes = new Set(artifactTypes());
128
+ const byType = new Map();
129
+ for (const skill of skills) {
130
+ const type = skill.outputs?.artifactType;
131
+ if (!type || !manifestTypes.has(type)) continue;
132
+ if (!skill.id.startsWith('docs/') && !skill.id.includes('-workflow')) continue;
133
+ const list = byType.get(type) ?? [];
134
+ list.push(skill);
135
+ byType.set(type, list);
136
+ }
137
+ const conflicts = [];
138
+ for (const [artifactType, entries] of byType) {
139
+ if (entries.length < 2) continue;
140
+ const bars = new Set(entries.map((e) => e.verificationHooks.find((h) => h.kind === 'verificationBar')?.value ?? ''));
141
+ conflicts.push({
142
+ artifactType,
143
+ skillIds: entries.map((e) => e.id),
144
+ reason: bars.size > 1 ? 'divergent verificationBar across workflow skills' : 'multiple workflow skills claim the same manifest artifact type',
145
+ });
146
+ }
147
+ return conflicts;
148
+ }
149
+
150
+ export function defaultSkillInventoryPath(rootDir = process.cwd()) {
151
+ return path.join(findConstructRoot(rootDir), 'tests', 'certification', 'skills', 'inventory.json');
152
+ }
153
+
154
+ export function buildSkillInventory({ rootDir } = {}) {
155
+ const root = findConstructRoot(rootDir);
156
+ const skillsDir = path.join(root, 'skills');
157
+ const registry = JSON.parse(fs.readFileSync(path.join(root, 'specialists', 'registry.json'), 'utf8'));
158
+ const manifest = loadArtifactManifest({ rootDir: root });
159
+ const declaredSkills = new Set();
160
+ for (const agent of registry.specialists ?? []) {
161
+ for (const skill of agent.skills ?? []) declaredSkills.add(skill);
162
+ }
163
+ const ownerBases = new Set([
164
+ ...(registry.specialists ?? []).map((a) => a.name.replace(/^cx-/, '')),
165
+ ...collectProfileRoles(root),
166
+ ]);
167
+ const workflowIds = workflowSkillIds(manifest);
168
+
169
+ const skills = collectSkillFiles(skillsDir).map((id) => {
170
+ const filePath = path.join(skillsDir, `${id}.md`);
171
+ const content = fs.readFileSync(filePath, 'utf8');
172
+ const { meta, body } = parseFrontmatter(content);
173
+ const owners = resolveOwners(id, { registry, declaredSkills, ownerBases });
174
+ const hooks = verificationHooks(meta, body);
175
+ return {
176
+ id,
177
+ owners,
178
+ activationTriggers: activationTriggers(meta, body),
179
+ inputs: Array.isArray(meta.inputs) ? meta.inputs : (meta.inputs ? [meta.inputs] : []),
180
+ outputs: {
181
+ artifactType: meta.artifactType ?? null,
182
+ name: meta.name ?? null,
183
+ },
184
+ verificationHooks: hooks,
185
+ workflowSkill: workflowIds.has(id),
186
+ };
187
+ });
188
+
189
+ const blockingFindings = [];
190
+ for (const skill of skills) {
191
+ if (skill.owners.length === 0) {
192
+ blockingFindings.push({ kind: 'no-owner', skillId: skill.id, severity: 'blocking' });
193
+ }
194
+ }
195
+ for (const conflict of detectConflictingOutputs(skills)) {
196
+ blockingFindings.push({
197
+ kind: 'conflicting-output-contract',
198
+ severity: 'blocking',
199
+ artifactType: conflict.artifactType,
200
+ skillIds: conflict.skillIds,
201
+ reason: conflict.reason,
202
+ });
203
+ }
204
+
205
+ const payload = {
206
+ version: 1,
207
+ generatedAt: new Date().toISOString(),
208
+ generator: 'lib/certification/skill-inventory.mjs',
209
+ skillCount: skills.length,
210
+ skills,
211
+ blockingFindings,
212
+ digest: null,
213
+ };
214
+ const canonical = { ...payload, digest: undefined };
215
+ payload.digest = createHash('sha256').update(JSON.stringify(canonical)).digest('hex');
216
+ return payload;
217
+ }
218
+
219
+ export function validateSkillInventory({ rootDir, inventory: supplied, checkFreshness = true } = {}) {
220
+ const root = findConstructRoot(rootDir);
221
+ const inventoryPath = defaultSkillInventoryPath(root);
222
+ const inventory = supplied ?? JSON.parse(fs.readFileSync(inventoryPath, 'utf8'));
223
+ const errors = [];
224
+ const warnings = [];
225
+
226
+ if (inventory.version !== 1) errors.push('inventory.version must equal 1');
227
+ if (!Array.isArray(inventory.skills) || inventory.skills.length === 0) errors.push('inventory.skills must be non-empty');
228
+
229
+ const onDisk = new Set(collectSkillFiles(path.join(root, 'skills')));
230
+ const indexed = new Set((inventory.skills ?? []).map((s) => s.id));
231
+ for (const id of onDisk) {
232
+ if (!indexed.has(id)) errors.push(`missing inventory entry: ${id}`);
233
+ }
234
+ for (const skill of inventory.skills ?? []) {
235
+ if (!onDisk.has(skill.id)) errors.push(`stale inventory entry: ${skill.id}`);
236
+ if (!Array.isArray(skill.owners)) errors.push(`${skill.id}: owners must be an array`);
237
+ if (!Array.isArray(skill.activationTriggers)) errors.push(`${skill.id}: activationTriggers must be an array`);
238
+ }
239
+
240
+ for (const finding of inventory.blockingFindings ?? []) {
241
+ if (finding.severity === 'blocking') warnings.push(`blocking finding recorded: ${finding.kind} ${finding.skillId ?? finding.artifactType ?? ''}`.trim());
242
+ }
243
+
244
+ if (checkFreshness) {
245
+ const fresh = buildSkillInventory({ rootDir: root });
246
+ const freshCanonical = { ...fresh, digest: undefined, generatedAt: undefined };
247
+ const committedCanonical = { ...inventory, digest: undefined, generatedAt: undefined };
248
+ if (JSON.stringify(freshCanonical) !== JSON.stringify(committedCanonical)) {
249
+ errors.push('inventory is stale — regenerate with node scripts/generate-skill-inventory.mjs');
250
+ }
251
+ }
252
+
253
+ return {
254
+ filePath: inventoryPath,
255
+ skillCount: inventory.skills?.length ?? 0,
256
+ blockingCount: (inventory.blockingFindings ?? []).filter((f) => f.severity === 'blocking').length,
257
+ errors,
258
+ warnings,
259
+ pass: errors.length === 0,
260
+ };
261
+ }
262
+
263
+ export function writeSkillInventory({ rootDir } = {}) {
264
+ const root = findConstructRoot(rootDir);
265
+ const inventory = buildSkillInventory({ rootDir: root });
266
+ const inventoryPath = defaultSkillInventoryPath(root);
267
+ fs.mkdirSync(path.dirname(inventoryPath), { recursive: true });
268
+ fs.writeFileSync(inventoryPath, `${JSON.stringify(inventory, null, 2)}\n`);
269
+ return { inventoryPath, inventory };
270
+ }
271
+
272
+ export function runSkillInventoryAuditCli(args = [], { rootDir } = {}) {
273
+ if (args.includes('--write')) {
274
+ const { inventoryPath, inventory } = writeSkillInventory({ rootDir });
275
+ process.stdout.write(`Wrote ${inventoryPath} (${inventory.skillCount} skills, ${inventory.blockingFindings.length} blocking findings)\n`);
276
+ return validateSkillInventory({ rootDir, inventory, checkFreshness: false });
277
+ }
278
+ const checkFreshness = !args.includes('--no-freshness');
279
+ const result = validateSkillInventory({ rootDir, checkFreshness });
280
+ if (args.includes('--json')) process.stdout.write(`${JSON.stringify(result, null, 2)}\n`);
281
+ else {
282
+ process.stdout.write(`Skill inventory: ${result.skillCount} skills (${result.blockingCount} blocking findings recorded)\n`);
283
+ if (result.errors.length) result.errors.forEach((error) => process.stderr.write(` ✗ ${error}\n`));
284
+ if (result.warnings.length) result.warnings.forEach((warning) => process.stderr.write(` ⚠ ${warning}\n`));
285
+ process.stdout.write(result.pass ? ' Result: PASS\n' : ' Result: FAIL\n');
286
+ }
287
+ if (!result.pass) process.exitCode = 1;
288
+ return result;
289
+ }