@geraldmaron/construct 1.1.1 → 1.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (426) hide show
  1. package/README.md +30 -19
  2. package/bin/construct +487 -87
  3. package/bin/construct-postinstall.mjs +30 -2
  4. package/examples/distribution/README.md +42 -0
  5. package/examples/distribution/manifest.json +53 -0
  6. package/examples/distribution/sources/adr.md +84 -0
  7. package/examples/distribution/sources/deck-one-pager.md +65 -0
  8. package/examples/distribution/sources/prd-platform.md +161 -0
  9. package/examples/distribution/sources/research-brief.md +80 -0
  10. package/examples/distribution/sources/rfc-platform.md +82 -0
  11. package/examples/distribution/sources/runbook.md +103 -0
  12. package/examples/distribution/sources/strategy.md +88 -0
  13. package/lib/adapters-sync.mjs +67 -0
  14. package/lib/artifact-gate-notice.mjs +38 -0
  15. package/lib/artifact-manifest.mjs +258 -0
  16. package/lib/artifact-release-gate.mjs +171 -0
  17. package/lib/artifact-reviewers.mjs +68 -0
  18. package/lib/artifact-type-from-path.mjs +79 -0
  19. package/lib/artifact-workflow.mjs +252 -0
  20. package/lib/audit-skills.mjs +4 -0
  21. package/lib/audit-specialists.mjs +285 -0
  22. package/lib/audit-trail.mjs +1 -1
  23. package/lib/auto-docs.mjs +90 -19
  24. package/lib/beads/drift.mjs +27 -6
  25. package/lib/beads-client.mjs +49 -121
  26. package/lib/beads-optimistic.mjs +5 -12
  27. package/lib/brand-fonts.mjs +93 -0
  28. package/lib/brand-prose.mjs +214 -0
  29. package/lib/brand-tokens.mjs +92 -0
  30. package/lib/bridges/copilot-proxy.mjs +13 -26
  31. package/lib/capability-ledger.mjs +156 -0
  32. package/lib/certification/artifact-fixtures.mjs +132 -0
  33. package/lib/certification/artifact-gates.mjs +63 -0
  34. package/lib/certification/artifact-provenance.mjs +97 -0
  35. package/lib/certification/canonical-scenarios.mjs +78 -0
  36. package/lib/certification/cli.mjs +191 -0
  37. package/lib/certification/dashboard-api.mjs +71 -0
  38. package/lib/certification/demo-parity.mjs +116 -0
  39. package/lib/certification/document-io-fixtures.mjs +246 -0
  40. package/lib/certification/document-workflow.mjs +97 -0
  41. package/lib/certification/eval-bridge.mjs +77 -0
  42. package/lib/certification/model-routing.mjs +149 -0
  43. package/lib/certification/prompt-budget.mjs +119 -0
  44. package/lib/certification/rc-gate.mjs +206 -0
  45. package/lib/certification/real-llm-scenarios.mjs +303 -0
  46. package/lib/certification/role-cards.mjs +113 -0
  47. package/lib/certification/role-overlays.mjs +117 -0
  48. package/lib/certification/run.mjs +122 -0
  49. package/lib/certification/runner.mjs +323 -0
  50. package/lib/certification/scenarios.mjs +51 -0
  51. package/lib/certification/skill-inventory.mjs +289 -0
  52. package/lib/certification/skill-scenarios.mjs +147 -0
  53. package/lib/certification/specialist-contracts.mjs +85 -0
  54. package/lib/certification/specialist-scenarios.mjs +175 -0
  55. package/lib/certification/stale-impact.mjs +146 -0
  56. package/lib/certification/status.mjs +252 -0
  57. package/lib/certification/store.mjs +77 -0
  58. package/lib/chat/cli.mjs +333 -0
  59. package/lib/chat/command-suggest.mjs +161 -0
  60. package/lib/chat/commands.mjs +215 -0
  61. package/lib/chat/config.mjs +142 -0
  62. package/lib/chat/context-compactor.mjs +250 -0
  63. package/lib/chat/context-continuation.mjs +253 -0
  64. package/lib/chat/continuation-source.mjs +58 -0
  65. package/lib/chat/demo-guide.mjs +61 -0
  66. package/lib/chat/design-tokens.mjs +91 -0
  67. package/lib/chat/desktop-binary.mjs +79 -0
  68. package/lib/chat/desktop-build.mjs +130 -0
  69. package/lib/chat/desktop-launcher.mjs +133 -0
  70. package/lib/chat/evidence.mjs +145 -0
  71. package/lib/chat/export.mjs +74 -0
  72. package/lib/chat/harness/driver.mjs +91 -0
  73. package/lib/chat/list-picker.mjs +112 -0
  74. package/lib/chat/model-picker.mjs +356 -0
  75. package/lib/chat/openrouter-fallback.mjs +151 -0
  76. package/lib/chat/permission-prompt.mjs +33 -0
  77. package/lib/chat/picker-catalog.mjs +45 -0
  78. package/lib/chat/policy-telemetry.mjs +34 -0
  79. package/lib/chat/present.mjs +246 -0
  80. package/lib/chat/session-context.mjs +39 -0
  81. package/lib/chat/session-persist.mjs +73 -0
  82. package/lib/chat/session-restore.mjs +71 -0
  83. package/lib/chat/session-settings.mjs +53 -0
  84. package/lib/chat/system-prompt.mjs +52 -0
  85. package/lib/chat/transparency.mjs +93 -0
  86. package/lib/chat/tui/color-scheme.mjs +42 -0
  87. package/lib/chat/tui/markdown.mjs +123 -0
  88. package/lib/chat/tui/presentation.mjs +100 -0
  89. package/lib/chat/tui/render.mjs +500 -0
  90. package/lib/chat/tui/turn-block.mjs +284 -0
  91. package/lib/chat/tui/turn-present.mjs +18 -0
  92. package/lib/chat/tui/turn-state.mjs +88 -0
  93. package/lib/chat/tui/usage.mjs +122 -0
  94. package/lib/chat/web-commands.mjs +146 -0
  95. package/lib/chat/web-launcher.mjs +63 -0
  96. package/lib/chat/web-picker-keys.mjs +46 -0
  97. package/lib/chat/web-session.mjs +159 -0
  98. package/lib/cli-commands.mjs +146 -15
  99. package/lib/cli-service-inventory.mjs +42 -0
  100. package/lib/comment-lint.mjs +6 -4
  101. package/lib/config/intake-policy.mjs +209 -0
  102. package/lib/config/project-config.mjs +11 -2
  103. package/lib/config/schema.mjs +86 -6
  104. package/lib/config/source-targets.mjs +311 -0
  105. package/lib/contract-schemas/decision.json +50 -0
  106. package/lib/contract-schemas/implementation.json +51 -0
  107. package/lib/contract-schemas/review-report.json +32 -0
  108. package/lib/contract-schemas/test-report.json +43 -0
  109. package/lib/contracts/construct-handoff.mjs +60 -0
  110. package/lib/contracts/validate.mjs +32 -3
  111. package/lib/contracts/violation-log.mjs +58 -5
  112. package/lib/dashboard-demo.mjs +71 -0
  113. package/lib/dashboard-static.mjs +19 -5
  114. package/lib/decisions/registry.mjs +5 -3
  115. package/lib/deck-export-pptx.mjs +1152 -0
  116. package/lib/demo-recording.mjs +142 -0
  117. package/lib/demo-script.mjs +114 -0
  118. package/lib/demo-surface.mjs +249 -0
  119. package/lib/demo.mjs +598 -140
  120. package/lib/diagram-export.mjs +192 -0
  121. package/lib/diagram.mjs +13 -11
  122. package/lib/docs-verify.mjs +25 -7
  123. package/lib/doctor/index.mjs +3 -1
  124. package/lib/doctor/project-adapters.mjs +44 -0
  125. package/lib/doctor/source-checkout.mjs +16 -0
  126. package/lib/doctor/watchers/cx-budget.mjs +98 -0
  127. package/lib/doctor/watchers/graph-staleness.mjs +52 -0
  128. package/lib/doctor/watchers/process-pressure.mjs +4 -3
  129. package/lib/document-export.mjs +384 -38
  130. package/lib/document-extract.mjs +44 -4
  131. package/lib/document-ingest.mjs +29 -20
  132. package/lib/embed/auto-sources.mjs +44 -0
  133. package/lib/embed/cli.mjs +4 -3
  134. package/lib/embed/daemon.mjs +18 -46
  135. package/lib/embed/demand-fetch.mjs +50 -19
  136. package/lib/embed/inbox.mjs +6 -10
  137. package/lib/embed/worker.mjs +4 -3
  138. package/lib/embedded-contract/index.mjs +11 -0
  139. package/lib/embedded-contract/model-resolve.mjs +10 -9
  140. package/lib/env-config.mjs +26 -0
  141. package/lib/evals/dataset.mjs +137 -0
  142. package/lib/evals/gates.mjs +175 -0
  143. package/lib/export-branding.mjs +32 -0
  144. package/lib/graph/build-co-change.mjs +54 -0
  145. package/lib/graph/build-from-registry.mjs +136 -0
  146. package/lib/graph/build-import-graph.mjs +153 -0
  147. package/lib/graph/cli.mjs +110 -0
  148. package/lib/graph/impact-cli.mjs +89 -0
  149. package/lib/graph/impact.mjs +108 -0
  150. package/lib/graph/staleness.mjs +44 -0
  151. package/lib/graph/store.mjs +172 -0
  152. package/lib/health-check.mjs +90 -76
  153. package/lib/hooks/artifact-release-gate.mjs +43 -0
  154. package/lib/hooks/brand-prose-lint.mjs +38 -0
  155. package/lib/hooks/graph-impact-advisory.mjs +62 -0
  156. package/lib/hooks/session-optimize.mjs +84 -207
  157. package/lib/hooks/session-start.mjs +18 -16
  158. package/lib/host-disposition.mjs +7 -0
  159. package/lib/improvement/cli.mjs +189 -0
  160. package/lib/improvement/controller.mjs +137 -0
  161. package/lib/improvement/proposal.mjs +120 -0
  162. package/lib/improvement/specialist-loop.mjs +192 -0
  163. package/lib/improvement/store.mjs +89 -0
  164. package/lib/improvement/surface.mjs +219 -0
  165. package/lib/ingest-tooling.mjs +97 -0
  166. package/lib/init/doc-lanes.mjs +165 -0
  167. package/lib/init-docs.mjs +31 -178
  168. package/lib/init-unified.mjs +50 -119
  169. package/lib/init-update-guide.mjs +102 -0
  170. package/lib/init-update.mjs +32 -1
  171. package/lib/init.mjs +8 -0
  172. package/lib/install/desktop-binary-download.mjs +85 -0
  173. package/lib/install/legacy-global-cleanup.mjs +189 -0
  174. package/lib/intake/constants.mjs +29 -0
  175. package/lib/intake/daemon.mjs +2 -0
  176. package/lib/intake/git-queue.mjs +9 -8
  177. package/lib/intake/intake-config.mjs +52 -105
  178. package/lib/intake/legacy-paths.mjs +5 -0
  179. package/lib/intake/queue.mjs +2 -1
  180. package/lib/intake/session-prelude.mjs +90 -1
  181. package/lib/libreoffice-export.mjs +97 -0
  182. package/lib/logging/rotate.mjs +9 -0
  183. package/lib/maintenance/docker-reclaim.mjs +206 -0
  184. package/lib/mcp/external-schema-cost.mjs +74 -0
  185. package/lib/mcp/server.mjs +103 -10
  186. package/lib/mcp/stdio-mcp-probe.mjs +188 -0
  187. package/lib/mcp/tool-budget.mjs +55 -4
  188. package/lib/mcp/tools/document.mjs +18 -4
  189. package/lib/mcp/tools/embedded-contract.mjs +14 -0
  190. package/lib/mcp/tools/skills.mjs +32 -3
  191. package/lib/mcp/tools/workflow.mjs +13 -1
  192. package/lib/model-free-selector.mjs +18 -0
  193. package/lib/model-registry.mjs +13 -229
  194. package/lib/model-router.mjs +373 -92
  195. package/lib/models/behavior-matrix.mjs +289 -0
  196. package/lib/models/catalog.mjs +209 -0
  197. package/lib/models/execution-capability-profile.mjs +196 -0
  198. package/lib/models/execution-policy.mjs +307 -0
  199. package/lib/models/provider-poll.mjs +383 -0
  200. package/lib/npm-spawn-env.mjs +17 -0
  201. package/lib/ollama/installed-models.mjs +129 -0
  202. package/lib/oracle/actions.mjs +187 -0
  203. package/lib/oracle/artifact-gate.mjs +99 -0
  204. package/lib/oracle/cli.mjs +204 -0
  205. package/lib/oracle/daemon-entry.mjs +14 -0
  206. package/lib/oracle/dispatch.mjs +81 -0
  207. package/lib/oracle/execute.mjs +143 -0
  208. package/lib/oracle/gaps.mjs +76 -0
  209. package/lib/oracle/index.mjs +84 -0
  210. package/lib/oracle/issues.mjs +164 -0
  211. package/lib/oracle/org-graph.mjs +170 -0
  212. package/lib/oracle/policy.mjs +80 -0
  213. package/lib/oracle/read-model.mjs +398 -0
  214. package/lib/oracle/reconcile.mjs +191 -0
  215. package/lib/oracle/routing.mjs +89 -0
  216. package/lib/oracle/synthesize.mjs +384 -0
  217. package/lib/oracle/verdicts.mjs +51 -0
  218. package/lib/orchestration/worker.mjs +88 -27
  219. package/lib/orchestration-policy.mjs +50 -0
  220. package/lib/parity.mjs +96 -2
  221. package/lib/playwright-demo.mjs +254 -0
  222. package/lib/project-init-shared.mjs +4 -1
  223. package/lib/prompt-composer.js +7 -3
  224. package/lib/prompt-validation-contract.mjs +24 -0
  225. package/lib/provider-capabilities.js +57 -11
  226. package/lib/providers/contract/adapters/confluence/index.mjs +181 -0
  227. package/lib/providers/contract/adapters/git/index.mjs +115 -0
  228. package/lib/providers/contract/adapters/github/index.mjs +166 -0
  229. package/lib/providers/contract/adapters/jira/index.mjs +187 -0
  230. package/lib/providers/contract/adapters/slack/index.mjs +175 -0
  231. package/lib/providers/contract/contract-tests.mjs +57 -0
  232. package/lib/providers/contract/errors.mjs +48 -0
  233. package/lib/providers/contract/interface.mjs +50 -0
  234. package/lib/providers/contract/registry.mjs +102 -0
  235. package/lib/providers/copilot-auth.mjs +297 -0
  236. package/lib/providers/credential-bootstrap.mjs +178 -0
  237. package/lib/providers/credential-catalog.mjs +46 -0
  238. package/lib/providers/credential-sources.mjs +63 -0
  239. package/lib/providers/creds.mjs +5 -2
  240. package/lib/providers/op-run.mjs +59 -0
  241. package/lib/providers/secret-resolver.mjs +159 -0
  242. package/lib/publish-template.mjs +163 -0
  243. package/lib/publish-tooling.mjs +119 -0
  244. package/lib/publish.mjs +305 -0
  245. package/lib/registry/cli.mjs +82 -0
  246. package/lib/registry/consolidation.mjs +147 -0
  247. package/lib/registry/generate-docs.mjs +75 -0
  248. package/lib/registry/skill-verification.mjs +57 -0
  249. package/lib/registry/surface-map.mjs +76 -0
  250. package/lib/registry/validate.mjs +135 -0
  251. package/lib/resources/budget.mjs +82 -0
  252. package/lib/resources/process-budget.mjs +45 -0
  253. package/lib/rules-delivery.mjs +9 -2
  254. package/lib/rules-read.mjs +26 -0
  255. package/lib/runtime-env.mjs +23 -0
  256. package/lib/runtime-pressure.mjs +51 -7
  257. package/lib/schema-infer.mjs +13 -2
  258. package/lib/server/chat-loop.mjs +622 -0
  259. package/lib/server/demo-preview.mjs +63 -0
  260. package/lib/server/index.mjs +259 -9
  261. package/lib/server/rate-limit.mjs +5 -3
  262. package/lib/server/webhook.mjs +3 -2
  263. package/lib/service-manager.mjs +60 -8
  264. package/lib/setup.mjs +70 -7
  265. package/lib/specialists/prompt-schema.mjs +19 -10
  266. package/lib/specialists/roster.mjs +43 -0
  267. package/lib/specialists/scaffold.mjs +56 -0
  268. package/lib/storage/backend.mjs +6 -6
  269. package/lib/storage/hybrid-query.mjs +7 -4
  270. package/lib/storage/state-source.mjs +5 -5
  271. package/lib/storage/sync.mjs +2 -3
  272. package/lib/telemetry/rule-calls.mjs +52 -0
  273. package/lib/template-registry.mjs +2 -2
  274. package/lib/templates/visual-requirements.mjs +27 -51
  275. package/lib/test-corpus-inventory.mjs +313 -0
  276. package/lib/uninstall/uninstall.mjs +19 -7
  277. package/lib/update.mjs +12 -0
  278. package/lib/upgrade.mjs +14 -0
  279. package/lib/wireframe.mjs +20 -14
  280. package/lib/worker/run.mjs +17 -4
  281. package/lib/worker/trace.mjs +11 -3
  282. package/package.json +28 -14
  283. package/personas/construct.md +2 -2
  284. package/platforms/claude/settings.template.json +36 -0
  285. package/rules/common/patterns.md +1 -1
  286. package/rules/common/release-gates.md +4 -3
  287. package/scripts/sync-specialists.mjs +41 -28
  288. package/skills/devops/data-engineering.md +1 -1
  289. package/skills/docs/adr-workflow.md +1 -0
  290. package/skills/docs/backlog-proposal-workflow.md +1 -0
  291. package/skills/docs/codebase-research-workflow.md +40 -0
  292. package/skills/docs/customer-profile-workflow.md +1 -0
  293. package/skills/docs/document-ingest-workflow.md +1 -0
  294. package/skills/docs/evidence-ingest-workflow.md +1 -0
  295. package/skills/docs/init-docs.md +2 -2
  296. package/skills/docs/init-project.md +7 -2
  297. package/skills/docs/prd-workflow.md +24 -1
  298. package/skills/docs/prfaq-workflow.md +1 -0
  299. package/skills/docs/product-intelligence-workflow.md +1 -0
  300. package/skills/docs/product-signal-workflow.md +1 -0
  301. package/skills/docs/research-workflow.md +54 -37
  302. package/skills/docs/runbook-workflow.md +1 -0
  303. package/skills/docs/strategy-workflow.md +1 -0
  304. package/skills/docs/user-research-workflow.md +40 -0
  305. package/skills/operating/orchestration-reference.md +1 -1
  306. package/skills/roles/architect.md +5 -0
  307. package/skills/roles/operator.docs.md +4 -0
  308. package/skills/routing.md +4 -2
  309. package/specialists/artifact-manifest.json +489 -0
  310. package/specialists/artifact-manifest.schema.json +83 -0
  311. package/specialists/audit-enrichments.json +454 -0
  312. package/specialists/contracts.json +37 -25
  313. package/specialists/prompts/_shared/validation-contract.md +26 -0
  314. package/specialists/prompts/cx-accessibility.md +21 -2
  315. package/specialists/prompts/cx-ai-engineer.md +24 -1
  316. package/specialists/prompts/cx-architect.md +4 -1
  317. package/specialists/prompts/cx-business-strategist.md +23 -2
  318. package/specialists/prompts/cx-data-analyst.md +22 -1
  319. package/specialists/prompts/cx-data-engineer.md +23 -2
  320. package/specialists/prompts/cx-debugger.md +21 -2
  321. package/specialists/prompts/cx-designer.md +26 -3
  322. package/specialists/prompts/cx-devil-advocate.md +19 -2
  323. package/specialists/prompts/cx-docs-keeper.md +28 -1
  324. package/specialists/prompts/cx-engineer.md +22 -1
  325. package/specialists/prompts/cx-evaluator.md +18 -1
  326. package/specialists/prompts/cx-explorer.md +21 -2
  327. package/specialists/prompts/cx-legal-compliance.md +21 -2
  328. package/specialists/prompts/cx-operations.md +21 -2
  329. package/specialists/prompts/cx-oracle.md +94 -0
  330. package/specialists/prompts/cx-orchestrator.md +20 -1
  331. package/specialists/prompts/cx-platform-engineer.md +25 -2
  332. package/specialists/prompts/cx-product-manager.md +32 -1
  333. package/specialists/prompts/cx-qa.md +24 -2
  334. package/specialists/prompts/cx-rd-lead.md +23 -2
  335. package/specialists/prompts/cx-release-manager.md +23 -2
  336. package/specialists/prompts/cx-researcher.md +22 -2
  337. package/specialists/prompts/cx-reviewer.md +18 -1
  338. package/specialists/prompts/cx-security.md +22 -2
  339. package/specialists/prompts/cx-sre.md +30 -3
  340. package/specialists/prompts/cx-test-automation.md +7 -1
  341. package/specialists/prompts/cx-trace-reviewer.md +22 -3
  342. package/specialists/prompts/cx-ux-researcher.md +21 -2
  343. package/specialists/registry.json +52 -173
  344. package/specialists/tone-profiles.json +42 -0
  345. package/templates/demos/playwright/demo-recording.config.mjs +47 -0
  346. package/templates/demos/recordings/agentic-platforms-prd.json +19 -0
  347. package/templates/demos/scripts/agentic-platforms-prd.json +44 -0
  348. package/templates/demos/specs/_helpers/scroll-artifact.ts +89 -0
  349. package/templates/demos/tapes/agentic-platforms-prd.tape +49 -0
  350. package/templates/demos/tapes/resource-guard-rails.tape +49 -0
  351. package/templates/demos/vhs/construct-cockpit.json +24 -0
  352. package/templates/distribution/construct-brand.typ +446 -0
  353. package/templates/distribution/construct-decision.typ +38 -0
  354. package/templates/distribution/construct-deck.html +95 -0
  355. package/templates/distribution/construct-pdf.typ +38 -0
  356. package/templates/distribution/construct-prd.typ +38 -0
  357. package/templates/distribution/construct-reference.docx +0 -0
  358. package/templates/distribution/construct-research.typ +38 -0
  359. package/templates/distribution/construct-web.html +92 -0
  360. package/templates/distribution/fonts/Geist-Bold.ttf +0 -0
  361. package/templates/distribution/fonts/Geist-Medium.ttf +0 -0
  362. package/templates/distribution/fonts/Geist-Regular.ttf +0 -0
  363. package/templates/distribution/fonts/Geist-SemiBold.ttf +0 -0
  364. package/templates/distribution/fonts/GeistMono-Medium.ttf +0 -0
  365. package/templates/distribution/fonts/GeistMono-Regular.ttf +0 -0
  366. package/templates/distribution/fonts/GeistMono-SemiBold.ttf +0 -0
  367. package/templates/distribution/fonts/IBMPlexMono-Regular.otf +0 -0
  368. package/templates/distribution/fonts/JetBrainsMono-Medium.ttf +0 -0
  369. package/templates/distribution/fonts/JetBrainsMono-Regular.ttf +0 -0
  370. package/templates/distribution/fonts/JetBrainsMono-SemiBold.ttf +0 -0
  371. package/templates/distribution/fonts/PlusJakartaSans-Bold.ttf +0 -0
  372. package/templates/distribution/fonts/PlusJakartaSans-Medium.ttf +0 -0
  373. package/templates/distribution/fonts/PlusJakartaSans-Regular.ttf +0 -0
  374. package/templates/distribution/fonts/PlusJakartaSans-SemiBold.ttf +0 -0
  375. package/templates/distribution/fonts/README.md +36 -0
  376. package/templates/distribution/fonts/SpaceGrotesk-Variable.ttf +0 -0
  377. package/templates/distribution/fonts/handwritten/Caveat.ttf +0 -0
  378. package/templates/distribution/fonts/legacy/IBMPlexMono-Regular.otf +0 -0
  379. package/templates/distribution/fonts/legacy/Inter-Medium.otf +0 -0
  380. package/templates/distribution/fonts/legacy/Inter-Regular.otf +0 -0
  381. package/templates/distribution/fonts/legacy/Inter-SemiBold.otf +0 -0
  382. package/templates/distribution/fonts/legacy/InterDisplay-SemiBold.otf +0 -0
  383. package/templates/distribution/fonts/legacy/SourceSerif4-Regular.otf +0 -0
  384. package/templates/distribution/fonts/legacy/SourceSerif4-Semibold.otf +0 -0
  385. package/templates/distribution/icons/activity.svg +15 -0
  386. package/templates/distribution/icons/alert-triangle.svg +17 -0
  387. package/templates/distribution/icons/book-open.svg +16 -0
  388. package/templates/distribution/icons/bot.svg +20 -0
  389. package/templates/distribution/icons/brain.svg +22 -0
  390. package/templates/distribution/icons/circle-check.svg +16 -0
  391. package/templates/distribution/icons/clipboard-check.svg +17 -0
  392. package/templates/distribution/icons/cpu.svg +28 -0
  393. package/templates/distribution/icons/database.svg +17 -0
  394. package/templates/distribution/icons/eye.svg +16 -0
  395. package/templates/distribution/icons/file-text.svg +19 -0
  396. package/templates/distribution/icons/gauge.svg +16 -0
  397. package/templates/distribution/icons/git-branch.svg +17 -0
  398. package/templates/distribution/icons/key.svg +17 -0
  399. package/templates/distribution/icons/layers.svg +17 -0
  400. package/templates/distribution/icons/list-checks.svg +19 -0
  401. package/templates/distribution/icons/lock.svg +16 -0
  402. package/templates/distribution/icons/message-square.svg +15 -0
  403. package/templates/distribution/icons/network.svg +19 -0
  404. package/templates/distribution/icons/route.svg +17 -0
  405. package/templates/distribution/icons/scale.svg +19 -0
  406. package/templates/distribution/icons/search.svg +16 -0
  407. package/templates/distribution/icons/send.svg +16 -0
  408. package/templates/distribution/icons/server.svg +18 -0
  409. package/templates/distribution/icons/shield-check.svg +16 -0
  410. package/templates/distribution/icons/users.svg +18 -0
  411. package/templates/distribution/icons/webhook.svg +17 -0
  412. package/templates/distribution/icons/workflow.svg +17 -0
  413. package/templates/distribution/icons/wrench.svg +15 -0
  414. package/templates/distribution/run.mjs +18 -1
  415. package/templates/docs/adr.md +7 -0
  416. package/templates/docs/construct_guide.md +8 -8
  417. package/templates/docs/customer-profile.md +4 -0
  418. package/templates/docs/one-pager.md +4 -0
  419. package/templates/docs/postmortem.md +31 -0
  420. package/templates/docs/prd-platform.md +19 -0
  421. package/templates/docs/prd.md +15 -0
  422. package/templates/docs/prfaq.md +7 -0
  423. package/templates/docs/qa-strategy.md +103 -0
  424. package/templates/docs/research-brief.md +8 -0
  425. package/templates/docs/rfc-platform.md +12 -0
  426. package/templates/docs/test-plan.md +7 -0
@@ -0,0 +1,209 @@
1
+ /**
2
+ * lib/config/intake-policy.mjs — project-scoped intake watcher policy.
3
+ *
4
+ * Resolves watch zones, scan depth, and additional directories from
5
+ * construct.config.json intakePolicy, with warned fallback to legacy
6
+ * .cx/intake-config.json and env overrides.
7
+ */
8
+
9
+ import { existsSync, readFileSync } from 'node:fs';
10
+ import { isAbsolute, join, resolve } from 'node:path';
11
+ import { loadProjectConfig, writeProjectConfig, findProjectConfigPath, PROJECT_CONFIG_FILENAME } from './project-config.mjs';
12
+ import {
13
+ INTAKE_DEFAULT_MAX_DEPTH,
14
+ INTAKE_HARD_MAX_DEPTH,
15
+ INTAKE_DEPTH_GUIDANCE,
16
+ describeIntakeDepth,
17
+ } from '../intake/constants.mjs';
18
+ import { INTAKE_CONFIG_FILE } from '../intake/legacy-paths.mjs';
19
+
20
+ export { INTAKE_DEFAULT_MAX_DEPTH, INTAKE_HARD_MAX_DEPTH, INTAKE_DEPTH_GUIDANCE, describeIntakeDepth };
21
+
22
+ export const DEFAULT_INTAKE_POLICY = Object.freeze({
23
+ maxDepth: INTAKE_DEFAULT_MAX_DEPTH,
24
+ zones: Object.freeze({
25
+ rootInbox: true,
26
+ projectInbox: true,
27
+ docsIntake: true,
28
+ }),
29
+ additionalDirs: [],
30
+ });
31
+
32
+ function clampDepth(value) {
33
+ const n = Number(value);
34
+ if (!Number.isFinite(n) || n < 0) return INTAKE_DEFAULT_MAX_DEPTH;
35
+ return Math.min(Math.floor(n), INTAKE_HARD_MAX_DEPTH);
36
+ }
37
+
38
+ function normalizeDir(dir, rootDir) {
39
+ if (typeof dir !== 'string') return null;
40
+ const trimmed = dir.trim();
41
+ if (!trimmed) return null;
42
+ return isAbsolute(trimmed) ? trimmed : resolve(rootDir, trimmed);
43
+ }
44
+
45
+ function parseEnvDirs(env) {
46
+ const raw = String(env?.CX_INBOX_DIRS ?? '').trim();
47
+ if (!raw) return [];
48
+ return raw.split(':').map((p) => p.trim()).filter(Boolean);
49
+ }
50
+
51
+ function readLegacyIntakeConfig(rootDir) {
52
+ const file = join(rootDir, INTAKE_CONFIG_FILE);
53
+ if (!existsSync(file)) return null;
54
+ try {
55
+ return JSON.parse(readFileSync(file, 'utf8'));
56
+ } catch {
57
+ return null;
58
+ }
59
+ }
60
+
61
+ export function intakePolicyFromProjectConfig(config) {
62
+ const raw = config?.intakePolicy ?? {};
63
+ const zones = raw.zones ?? {};
64
+ return {
65
+ maxDepth: clampDepth(raw.maxDepth ?? DEFAULT_INTAKE_POLICY.maxDepth),
66
+ zones: {
67
+ rootInbox: zones.rootInbox !== false,
68
+ projectInbox: zones.projectInbox !== false,
69
+ docsIntake: zones.docsIntake !== false,
70
+ },
71
+ additionalDirs: Array.isArray(raw.additionalDirs)
72
+ ? raw.additionalDirs.map((d) => String(d).trim()).filter(Boolean)
73
+ : [],
74
+ };
75
+ }
76
+
77
+ export function legacyIntakeToPolicy(stored = {}) {
78
+ return {
79
+ maxDepth: clampDepth(stored.maxDepth ?? INTAKE_DEFAULT_MAX_DEPTH),
80
+ zones: {
81
+ rootInbox: stored.includeArchetypeInbox === true || stored.includeRootInbox === true,
82
+ projectInbox: stored.includeProjectInbox !== false,
83
+ docsIntake: stored.includeDocsIntake !== false,
84
+ },
85
+ additionalDirs: Array.isArray(stored.parentDirs) ? stored.parentDirs : [],
86
+ };
87
+ }
88
+
89
+ export function policyToLegacyIntakeShape(policy) {
90
+ return {
91
+ parentDirs: policy.additionalDirs,
92
+ maxDepth: policy.maxDepth,
93
+ includeProjectInbox: policy.zones.projectInbox,
94
+ includeDocsIntake: policy.zones.docsIntake,
95
+ includeArchetypeInbox: policy.zones.rootInbox,
96
+ includeRootInbox: policy.zones.rootInbox,
97
+ };
98
+ }
99
+
100
+ export function loadIntakePolicy(rootDir, env = process.env) {
101
+ const { config, path: configPath, raw } = loadProjectConfig(rootDir, env);
102
+ const legacy = readLegacyIntakeConfig(rootDir);
103
+ let policy;
104
+ let source = 'default';
105
+ let legacyWarning = null;
106
+
107
+ if (configPath && raw?.intakePolicy) {
108
+ policy = intakePolicyFromProjectConfig(config);
109
+ source = 'project-config';
110
+ } else if (legacy) {
111
+ policy = legacyIntakeToPolicy(legacy);
112
+ source = 'legacy-intake-config';
113
+ legacyWarning = `Using deprecated ${INTAKE_CONFIG_FILE}; migrate with \`construct intake config migrate\``;
114
+ } else {
115
+ policy = structuredClone(DEFAULT_INTAKE_POLICY);
116
+ source = 'default';
117
+ }
118
+
119
+ const envDirs = parseEnvDirs(env).map((dir) => normalizeDir(dir, rootDir)).filter(Boolean);
120
+ const additionalDirs = []
121
+ .concat(policy.additionalDirs.map((dir) => normalizeDir(dir, rootDir)).filter(Boolean))
122
+ .concat(envDirs);
123
+
124
+ const seen = new Set();
125
+ const uniqueDirs = [];
126
+ for (const dir of additionalDirs) {
127
+ if (seen.has(dir)) continue;
128
+ seen.add(dir);
129
+ uniqueDirs.push(dir);
130
+ }
131
+
132
+ const envDepth = env?.CX_INTAKE_MAX_DEPTH;
133
+ const maxDepth = clampDepth(envDepth ?? policy.maxDepth);
134
+
135
+ return {
136
+ maxDepth,
137
+ zones: { ...policy.zones },
138
+ additionalDirs: uniqueDirs,
139
+ source,
140
+ legacyWarning,
141
+ };
142
+ }
143
+
144
+ export function resolvedIntakeConfig(rootDir, env = process.env) {
145
+ const policy = loadIntakePolicy(rootDir, env);
146
+ return {
147
+ parentDirs: policy.additionalDirs,
148
+ maxDepth: policy.maxDepth,
149
+ includeProjectInbox: policy.zones.projectInbox,
150
+ includeDocsIntake: policy.zones.docsIntake,
151
+ includeArchetypeInbox: policy.zones.rootInbox,
152
+ includeRootInbox: policy.zones.rootInbox,
153
+ source: policy.source,
154
+ legacyWarning: policy.legacyWarning,
155
+ };
156
+ }
157
+
158
+ export function saveIntakePolicy(rootDir, patch = {}, options = {}) {
159
+ const configPath = findProjectConfigPath(rootDir) ?? join(rootDir, PROJECT_CONFIG_FILENAME);
160
+ const { config } = loadProjectConfig(rootDir, {});
161
+ const current = intakePolicyFromProjectConfig(config.intakePolicy ? config : { intakePolicy: DEFAULT_INTAKE_POLICY });
162
+
163
+ const nextZones = { ...current.zones, ...(patch.zones ?? {}) };
164
+ const next = {
165
+ maxDepth: clampDepth(patch.maxDepth ?? current.maxDepth),
166
+ zones: {
167
+ rootInbox: patch.rootInbox !== undefined ? Boolean(patch.rootInbox) : (patch.zones?.rootInbox ?? nextZones.rootInbox),
168
+ projectInbox: patch.projectInbox !== undefined ? Boolean(patch.projectInbox) : (patch.zones?.projectInbox ?? nextZones.projectInbox),
169
+ docsIntake: patch.docsIntake !== undefined ? Boolean(patch.docsIntake) : (patch.zones?.docsIntake ?? nextZones.docsIntake),
170
+ },
171
+ additionalDirs: Array.isArray(patch.additionalDirs)
172
+ ? patch.additionalDirs.map((dir) => normalizeDir(dir, rootDir)).filter(Boolean)
173
+ : current.additionalDirs,
174
+ };
175
+
176
+ if (patch.addDir) {
177
+ const dir = normalizeDir(patch.addDir, rootDir);
178
+ if (dir && !next.additionalDirs.includes(dir)) {
179
+ next.additionalDirs = [...next.additionalDirs, dir];
180
+ }
181
+ }
182
+ if (patch.removeDir) {
183
+ const dir = normalizeDir(patch.removeDir, rootDir);
184
+ next.additionalDirs = next.additionalDirs.filter((d) => d !== dir);
185
+ }
186
+
187
+ const updated = {
188
+ ...config,
189
+ version: config.version ?? 1,
190
+ intakePolicy: next,
191
+ };
192
+
193
+ if (options.dryRun) {
194
+ return { policy: next, configPath, dryRun: true };
195
+ }
196
+
197
+ writeProjectConfig(configPath, updated);
198
+ return { policy: next, configPath };
199
+ }
200
+
201
+ export function migrateLegacyIntakeConfig(rootDir) {
202
+ const legacy = readLegacyIntakeConfig(rootDir);
203
+ if (!legacy) {
204
+ return { migrated: false, reason: 'no legacy file' };
205
+ }
206
+ const policy = legacyIntakeToPolicy(legacy);
207
+ saveIntakePolicy(rootDir, policy);
208
+ return { migrated: true, policy };
209
+ }
@@ -15,6 +15,7 @@
15
15
  import fs from 'node:fs';
16
16
  import path from 'node:path';
17
17
  import { CONFIG_SCHEMA_VERSION, DEFAULT_PROJECT_CONFIG, validateProjectConfig, FIELD_RULES } from './schema.mjs';
18
+ import { validateSourceTargets } from './source-targets.mjs';
18
19
 
19
20
  export const PROJECT_CONFIG_FILENAME = 'construct.config.json';
20
21
 
@@ -85,13 +86,17 @@ export function loadProjectConfig(cwd = process.cwd(), env = process.env) {
85
86
  };
86
87
  }
87
88
  const validation = validateProjectConfig(raw);
88
- if (!validation.valid) {
89
+ const targetErrors = raw?.sources?.targets !== undefined
90
+ ? validateSourceTargets(raw.sources.targets)
91
+ : [];
92
+ const allErrors = [...(validation.errors ?? []), ...targetErrors];
93
+ if (!validation.valid || targetErrors.length) {
89
94
  return {
90
95
  path: configPath,
91
96
  raw,
92
97
  config: structuredClone(DEFAULT_PROJECT_CONFIG),
93
98
  source: 'invalid',
94
- errors: validation.errors,
99
+ errors: allErrors,
95
100
  };
96
101
  }
97
102
  const merged = deepMerge(structuredClone(DEFAULT_PROJECT_CONFIG), raw);
@@ -161,6 +166,10 @@ export function writeProjectConfig(filePath, config, options = {}) {
161
166
  let validation = { valid: true, errors: [] };
162
167
  if (validate) {
163
168
  validation = validateProjectConfig(config);
169
+ if (config?.sources?.targets !== undefined) {
170
+ validation.errors = [...(validation.errors ?? []), ...validateSourceTargets(config.sources.targets)];
171
+ validation.valid = validation.errors.length === 0;
172
+ }
164
173
  }
165
174
 
166
175
  // Combine all errors (handle case where validation.errors is undefined)
@@ -5,7 +5,8 @@
5
5
  * AJV/zod to keep Construct dependency-free at startup — the loader runs
6
6
  * before npm install completes in some bootstrap paths.
7
7
  *
8
- * Schema v1 covers: alias, deployment, providers, autoEmbed, ingest, telemetry.
8
+ * Schema v1 covers: alias, deployment, providers, sources, intakePolicy,
9
+ * autoEmbed, ingest, telemetry.
9
10
  * Tier model selection lives in `specialists/registry.json` only — this config
10
11
  * file is intentionally not a second edit surface for model assignments.
11
12
  * Later phases (6, 7b) extend it with `resources` and `costs` blocks
@@ -47,6 +48,22 @@ export const DEFAULT_PROJECT_CONFIG = Object.freeze({
47
48
  tenantId: null,
48
49
  }),
49
50
  providers: Object.freeze({}),
51
+ sources: Object.freeze({
52
+ targets: [],
53
+ }),
54
+ intakePolicy: Object.freeze({
55
+ maxDepth: 4,
56
+ zones: Object.freeze({
57
+ rootInbox: true,
58
+ projectInbox: true,
59
+ docsIntake: true,
60
+ }),
61
+ additionalDirs: [],
62
+ }),
63
+ artifactWorkflow: Object.freeze({
64
+ defaults: Object.freeze({}),
65
+ types: Object.freeze({}),
66
+ }),
50
67
  profile: DEFAULT_PROFILE_ID,
51
68
  autoEmbed: false,
52
69
  ingest: Object.freeze({
@@ -65,6 +82,18 @@ export const DEFAULT_PROJECT_CONFIG = Object.freeze({
65
82
  hooks: Object.freeze({
66
83
  outputMode: 'auto',
67
84
  }),
85
+ models: Object.freeze({
86
+ visibility: Object.freeze({
87
+ mode: 'all_configured',
88
+ include: [],
89
+ exclude: [],
90
+ providers: {},
91
+ }),
92
+ catalog: Object.freeze({
93
+ liveOpenRouter: true,
94
+ maxLiveFree: 24,
95
+ }),
96
+ }),
68
97
  roleSelection: Object.freeze({
69
98
  primary: null,
70
99
  secondary: null,
@@ -109,6 +138,38 @@ export const FIELD_RULES = {
109
138
  },
110
139
  },
111
140
  providers: { type: 'object', required: false },
141
+ sources: {
142
+ type: 'object',
143
+ required: false,
144
+ fields: {
145
+ targets: { type: 'array' },
146
+ },
147
+ },
148
+ intakePolicy: {
149
+ type: 'object',
150
+ required: false,
151
+ fields: {
152
+ maxDepth: { type: 'number' },
153
+ additionalDirs: { type: 'array' },
154
+ zones: {
155
+ type: 'object',
156
+ required: false,
157
+ fields: {
158
+ rootInbox: { type: 'boolean' },
159
+ projectInbox: { type: 'boolean' },
160
+ docsIntake: { type: 'boolean' },
161
+ },
162
+ },
163
+ },
164
+ },
165
+ artifactWorkflow: {
166
+ type: 'object',
167
+ required: false,
168
+ fields: {
169
+ defaults: { type: 'object', required: false },
170
+ types: { type: 'object', required: false },
171
+ },
172
+ },
112
173
  profile: { type: 'string', required: false, maxLength: 40 },
113
174
  autoEmbed: { type: 'boolean', required: false },
114
175
  ingest: {
@@ -171,14 +232,33 @@ export const FIELD_RULES = {
171
232
  billingMode: { type: 'string', enum: ['metered', 'subscription', 'mixed'] },
172
233
  enforce: { type: 'boolean' },
173
234
  budgets: { type: 'object' },
174
- // Per-provider billing-mode overrides. The global billingMode is the
175
- // fallback; entries here win for the provider id (anthropic, openai,
176
- // openrouter, gemini, ollama, local, github-copilot, …). Subscription
177
- // entries contribute $0 to "actual spend" headline numbers; the
178
- // metered-equivalent stays in the footnote.
179
235
  providers: { type: 'object', required: false },
180
236
  },
181
237
  },
238
+ models: {
239
+ type: 'object',
240
+ required: false,
241
+ fields: {
242
+ visibility: {
243
+ type: 'object',
244
+ required: false,
245
+ fields: {
246
+ mode: { type: 'string', enum: ['all_configured', 'tier_defaults', 'explicit'] },
247
+ include: { type: 'array' },
248
+ exclude: { type: 'array' },
249
+ providers: { type: 'object' },
250
+ },
251
+ },
252
+ catalog: {
253
+ type: 'object',
254
+ required: false,
255
+ fields: {
256
+ liveOpenRouter: { type: 'boolean' },
257
+ maxLiveFree: { type: 'number' },
258
+ },
259
+ },
260
+ },
261
+ },
182
262
  };
183
263
 
184
264
  function checkType(value, expected) {
@@ -0,0 +1,311 @@
1
+ /**
2
+ * lib/config/source-targets.mjs — typed integration source targets in construct.config.json.
3
+ *
4
+ * Validates GitHub, Jira, Linear, and Slack selector records, merges legacy env
5
+ * target lists additively, deduplicates by provider+selector signature, and
6
+ * converts resolved targets into embed provider source records for auto-discovery.
7
+ */
8
+
9
+ export const SOURCE_PROVIDERS = Object.freeze(['github', 'jira', 'linear', 'slack']);
10
+
11
+ export const SLACK_INTENTS = Object.freeze(['internal', 'risk', 'decision', 'external', 'how-to']);
12
+
13
+ const ID_RE = /^[a-z][a-z0-9_-]{0,63}$/i;
14
+ const GITHUB_REPO_RE = /^[A-Za-z0-9_.-]+\/[A-Za-z0-9_.-]+$/;
15
+ const JIRA_PROJECT_RE = /^[A-Z][A-Z0-9]{1,9}$/;
16
+
17
+ export function validateSourceTarget(target, index = 0) {
18
+ const errors = [];
19
+ const at = `sources.targets[${index}]`;
20
+
21
+ if (!target || typeof target !== 'object' || Array.isArray(target)) {
22
+ return [`${at}: must be an object`];
23
+ }
24
+
25
+ if (typeof target.id !== 'string' || !ID_RE.test(target.id)) {
26
+ errors.push(`${at}.id: required stable id (letters, numbers, hyphens, underscores; max 64 chars)`);
27
+ }
28
+
29
+ if (!SOURCE_PROVIDERS.includes(target.provider)) {
30
+ errors.push(`${at}.provider: must be one of ${SOURCE_PROVIDERS.join(', ')}`);
31
+ }
32
+
33
+ if (!target.selector || typeof target.selector !== 'object' || Array.isArray(target.selector)) {
34
+ errors.push(`${at}.selector: required object`);
35
+ return errors;
36
+ }
37
+
38
+ const sel = target.selector;
39
+ switch (target.provider) {
40
+ case 'github':
41
+ if (typeof sel.repo !== 'string' || !GITHUB_REPO_RE.test(sel.repo.trim())) {
42
+ errors.push(`${at}.selector.repo: required owner/repo slug`);
43
+ }
44
+ break;
45
+ case 'jira':
46
+ if (typeof sel.project !== 'string' || !JIRA_PROJECT_RE.test(sel.project.trim().toUpperCase())) {
47
+ errors.push(`${at}.selector.project: required Jira project key (2–10 uppercase letters/digits)`);
48
+ }
49
+ break;
50
+ case 'linear':
51
+ if (typeof sel.team !== 'string' || !sel.team.trim()) {
52
+ errors.push(`${at}.selector.team: required Linear team key or name`);
53
+ }
54
+ break;
55
+ case 'slack': {
56
+ if (typeof sel.channel !== 'string' || !sel.channel.trim()) {
57
+ errors.push(`${at}.selector.channel: required Slack channel name or ID`);
58
+ }
59
+ if (sel.intent !== undefined && !SLACK_INTENTS.includes(sel.intent)) {
60
+ errors.push(`${at}.selector.intent: must be one of ${SLACK_INTENTS.join(', ')}`);
61
+ }
62
+ break;
63
+ }
64
+ default:
65
+ break;
66
+ }
67
+
68
+ return errors;
69
+ }
70
+
71
+ export function validateSourceTargets(targets) {
72
+ const errors = [];
73
+ if (targets === undefined) return errors;
74
+ if (!Array.isArray(targets)) {
75
+ return ['sources.targets: must be an array'];
76
+ }
77
+
78
+ const seenIds = new Set();
79
+ for (let i = 0; i < targets.length; i++) {
80
+ errors.push(...validateSourceTarget(targets[i], i));
81
+ const id = targets[i]?.id;
82
+ if (typeof id === 'string') {
83
+ const key = id.toLowerCase();
84
+ if (seenIds.has(key)) {
85
+ errors.push(`sources.targets[${i}].id: duplicate id "${id}"`);
86
+ }
87
+ seenIds.add(key);
88
+ }
89
+ }
90
+ return errors;
91
+ }
92
+
93
+ function parseCsv(value) {
94
+ return String(value ?? '')
95
+ .split(',')
96
+ .map((s) => s.trim())
97
+ .filter(Boolean);
98
+ }
99
+
100
+ function slugId(value) {
101
+ return String(value).toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-|-$/g, '');
102
+ }
103
+
104
+ function parseSlackChannelEntry(raw) {
105
+ const trimmed = String(raw).trim().replace(/^#/, '');
106
+ const colon = trimmed.indexOf(':');
107
+ if (colon === -1) {
108
+ return { channel: trimmed, intent: 'internal' };
109
+ }
110
+ return { channel: trimmed.slice(0, colon), intent: trimmed.slice(colon + 1) || 'internal' };
111
+ }
112
+
113
+ export function legacyEnvSourceTargets(env = process.env) {
114
+ const targets = [];
115
+
116
+ for (const repo of parseCsv(env.GITHUB_REPOS ?? env.GITHUB_REPO)) {
117
+ targets.push({
118
+ id: `env-github-${slugId(repo)}`,
119
+ provider: 'github',
120
+ selector: { repo },
121
+ provenance: 'env:GITHUB_REPOS',
122
+ });
123
+ }
124
+
125
+ for (const project of parseCsv(env.JIRA_PROJECTS)) {
126
+ const key = project.toUpperCase();
127
+ targets.push({
128
+ id: `env-jira-${slugId(key)}`,
129
+ provider: 'jira',
130
+ selector: { project: key },
131
+ provenance: 'env:JIRA_PROJECTS',
132
+ });
133
+ }
134
+
135
+ for (const team of parseCsv(env.LINEAR_TEAMS)) {
136
+ targets.push({
137
+ id: `env-linear-${slugId(team)}`,
138
+ provider: 'linear',
139
+ selector: { team },
140
+ provenance: 'env:LINEAR_TEAMS',
141
+ });
142
+ }
143
+
144
+ for (const entry of parseCsv(env.SLACK_CHANNELS ?? env.SLACK_CHANNEL)) {
145
+ const { channel, intent } = parseSlackChannelEntry(entry);
146
+ targets.push({
147
+ id: `env-slack-${slugId(channel)}`,
148
+ provider: 'slack',
149
+ selector: { channel, intent: SLACK_INTENTS.includes(intent) ? intent : 'internal' },
150
+ provenance: 'env:SLACK_CHANNELS',
151
+ });
152
+ }
153
+
154
+ return targets;
155
+ }
156
+
157
+ export function targetSignature(target) {
158
+ const sel = target.selector ?? {};
159
+ switch (target.provider) {
160
+ case 'github':
161
+ return `github:repo:${sel.repo?.trim().toLowerCase()}`;
162
+ case 'jira':
163
+ return `jira:project:${String(sel.project ?? '').trim().toUpperCase()}`;
164
+ case 'linear':
165
+ return `linear:team:${String(sel.team ?? '').trim().toLowerCase()}`;
166
+ case 'slack':
167
+ return `slack:channel:${String(sel.channel ?? '').trim().toLowerCase()}:intent:${sel.intent ?? 'internal'}`;
168
+ default:
169
+ return `${target.provider}:${JSON.stringify(sel)}`;
170
+ }
171
+ }
172
+
173
+ export function mergeSourceTargets(configTargets = [], envTargets = []) {
174
+ const merged = [];
175
+ const seen = new Set();
176
+
177
+ for (const target of [...configTargets, ...envTargets]) {
178
+ const sig = targetSignature(target);
179
+ if (seen.has(sig)) continue;
180
+ seen.add(sig);
181
+ merged.push(target);
182
+ }
183
+ return merged;
184
+ }
185
+
186
+ export function normalizeConfigTarget(raw) {
187
+ const out = {
188
+ id: String(raw.id).trim(),
189
+ provider: raw.provider,
190
+ selector: { ...raw.selector },
191
+ provenance: raw.provenance ?? 'config',
192
+ };
193
+
194
+ if (out.provider === 'jira' && out.selector.project) {
195
+ out.selector.project = String(out.selector.project).trim().toUpperCase();
196
+ }
197
+ if (out.provider === 'github' && out.selector.repo) {
198
+ out.selector.repo = String(out.selector.repo).trim();
199
+ }
200
+ if (out.provider === 'linear' && out.selector.team) {
201
+ out.selector.team = String(out.selector.team).trim();
202
+ }
203
+ if (out.provider === 'slack' && out.selector.channel) {
204
+ out.selector.channel = String(out.selector.channel).trim().replace(/^#/, '');
205
+ if (!out.selector.intent) out.selector.intent = 'internal';
206
+ }
207
+ return out;
208
+ }
209
+
210
+ export function resolveEffectiveSourceTargetsFromConfig(config, env = process.env) {
211
+ const configTargets = (config?.sources?.targets ?? []).map(normalizeConfigTarget);
212
+ const envTargets = legacyEnvSourceTargets(env);
213
+ return mergeSourceTargets(configTargets, envTargets);
214
+ }
215
+
216
+ export function targetsToEmbedSources(targets) {
217
+ const sources = [];
218
+ const githubRepos = targets.filter((t) => t.provider === 'github').map((t) => t.selector.repo);
219
+ if (githubRepos.length) {
220
+ sources.push({
221
+ provider: 'github',
222
+ repos: githubRepos,
223
+ refs: ['prs', 'issues', 'commits'],
224
+ limit: 25,
225
+ });
226
+ sources.push({
227
+ provider: 'github',
228
+ repos: githubRepos,
229
+ refs: ['meta', 'readme', 'docs'],
230
+ limit: 25,
231
+ });
232
+ }
233
+
234
+ for (const target of targets.filter((t) => t.provider === 'jira')) {
235
+ sources.push({
236
+ provider: 'jira',
237
+ project: target.selector.project,
238
+ refs: ['issues'],
239
+ limit: 50,
240
+ });
241
+ }
242
+
243
+ for (const target of targets.filter((t) => t.provider === 'linear')) {
244
+ sources.push({
245
+ provider: 'linear',
246
+ team: target.selector.team,
247
+ refs: ['issues'],
248
+ limit: 50,
249
+ });
250
+ }
251
+
252
+ const slackByIntent = new Map();
253
+ for (const target of targets.filter((t) => t.provider === 'slack')) {
254
+ const intent = target.selector.intent ?? 'internal';
255
+ if (!slackByIntent.has(intent)) slackByIntent.set(intent, []);
256
+ slackByIntent.get(intent).push(target.selector.channel);
257
+ }
258
+ for (const [intent, channels] of slackByIntent) {
259
+ sources.push({
260
+ provider: 'slack',
261
+ channels,
262
+ intent,
263
+ refs: ['messages'],
264
+ oldest: 86400,
265
+ limit: 50,
266
+ });
267
+ }
268
+
269
+ return sources;
270
+ }
271
+
272
+ export function resolveKnownSourcesFromTargets(targets) {
273
+ const sources = [];
274
+
275
+ for (const target of targets) {
276
+ const sel = target.selector ?? {};
277
+ if (target.provider === 'github' && sel.repo) {
278
+ const repo = sel.repo;
279
+ const short = repo.split('/').pop();
280
+ sources.push({ id: repo.toLowerCase(), provider: 'github', ref: repo, display: repo, targetId: target.id });
281
+ sources.push({ id: short.toLowerCase(), provider: 'github', ref: repo, display: repo, targetId: target.id });
282
+ sources.push({ id: short.replace(/-/g, '').toLowerCase(), provider: 'github', ref: repo, display: repo, targetId: target.id });
283
+ const words = short.split('-');
284
+ if (words.length > 1) {
285
+ sources.push({ id: words[words.length - 1].toLowerCase(), provider: 'github', ref: repo, display: repo, targetId: target.id });
286
+ }
287
+ }
288
+ if (target.provider === 'jira' && sel.project) {
289
+ const key = sel.project;
290
+ sources.push({ id: key.toLowerCase(), provider: 'jira', ref: key, display: `Jira/${key}`, targetId: target.id });
291
+ }
292
+ if (target.provider === 'linear' && sel.team) {
293
+ const team = sel.team;
294
+ sources.push({ id: team.toLowerCase(), provider: 'linear', ref: team, display: `Linear/${team}`, targetId: target.id });
295
+ }
296
+ if (target.provider === 'slack' && sel.channel) {
297
+ const ch = sel.channel;
298
+ sources.push({ id: ch.toLowerCase(), provider: 'slack', ref: ch, display: `Slack/${ch}`, targetId: target.id });
299
+ }
300
+ }
301
+
302
+ const providers = new Set(targets.map((t) => t.provider));
303
+ if (providers.has('jira')) {
304
+ sources.push({ id: 'jira', provider: 'jira', ref: null, display: 'Jira' });
305
+ }
306
+ if (providers.has('linear')) {
307
+ sources.push({ id: 'linear', provider: 'linear', ref: null, display: 'Linear' });
308
+ }
309
+
310
+ return sources;
311
+ }