@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
@@ -14,6 +14,11 @@ import { createIntakeQueue } from './queue.mjs';
14
14
  import { formatTriageLine } from './classify.mjs';
15
15
  import { isBrokered } from '../mcp/broker.mjs';
16
16
  import { getRebrand } from '../profiles/rebrand.mjs';
17
+ import { homedir as osHomedir } from 'node:os';
18
+ import { readLastTick } from '../oracle/index.mjs';
19
+ import { listPending } from '../oracle/actions.mjs';
20
+ import { readLatestVerdict } from '../oracle/verdicts.mjs';
21
+ import { isVerdictOnlyGap } from '../oracle/policy.mjs';
17
22
 
18
23
  export function buildIntakePrelude({ cwd, env = process.env } = {}) {
19
24
  if (!cwd) return '';
@@ -48,9 +53,93 @@ export function buildBrokerStatusLine({ env = process.env } = {}) {
48
53
  export function buildSessionPrelude({ cwd, env = process.env } = {}) {
49
54
  const intake = buildIntakePrelude({ cwd, env });
50
55
  const broker = buildBrokerStatusLine({ env });
51
- if (!intake && !broker) return '';
56
+ const oracle = buildOraclePrelude({ cwd, env });
57
+ if (!intake && !broker && !oracle) return '';
52
58
  const parts = [];
53
59
  if (intake) parts.push(intake.trim());
60
+ if (oracle) parts.push(oracle.trim());
54
61
  if (broker) parts.push(broker);
55
62
  return parts.join('\n\n');
56
63
  }
64
+
65
+ /**
66
+ * Oracle verdict + pending queue for session-start (mirror doctor warnings).
67
+ */
68
+ export function buildOraclePrelude({ cwd, env = process.env, homeDir } = {}) {
69
+ if (!cwd) return '';
70
+ if (env.CONSTRUCT_ORACLE === 'off' || env.CONSTRUCT_ORACLE === '0') return '';
71
+ try {
72
+ const home = homeDir ?? osHomedir();
73
+ const last = readLastTick(home);
74
+ const verdict = readLatestVerdict(cwd);
75
+ const pending = listPending(cwd).filter((p) => p.status === 'pending');
76
+ const v = verdict?.verdict ?? last?.verdict ?? 'unknown';
77
+ if (v === 'healthy' && pending.length === 0) return '';
78
+ const lines = [`\n## Oracle overseer · verdict: **${v}**`];
79
+ const gapSource = verdict?.gaps ?? last?.gaps ?? [];
80
+ const top = gapSource.filter((g) => g.severity === 'high').slice(0, 3);
81
+ for (const g of top) {
82
+ if (isVerdictOnlyGap(g)) {
83
+ const hint = g.id === 'beads-hygiene'
84
+ ? 'run `construct beads drift`'
85
+ : g.id === 'workflow-misaligned'
86
+ ? 'run `construct workflow init` or `construct init`'
87
+ : 'see `construct oracle gaps`';
88
+ lines.push(`- [${g.severity}] ${g.id} (verdict-only): ${g.detail} — ${hint}`);
89
+ } else {
90
+ lines.push(`- [${g.severity}] ${g.id}: ${g.detail}`);
91
+ }
92
+ }
93
+ if (pending.length) {
94
+ lines.push(`Pending approvals (${pending.length}): \`construct oracle pending\``);
95
+ }
96
+ lines.push('Review: `construct oracle review` · Gaps: `construct oracle gaps` · Approve: `construct oracle approve <id>`\n');
97
+ return lines.join('\n');
98
+ } catch {
99
+ return '';
100
+ }
101
+ }
102
+
103
+ export function readOracleDockState({ cwd, env = process.env, homeDir } = {}) {
104
+ if (!cwd || env.CONSTRUCT_ORACLE === 'off' || env.CONSTRUCT_ORACLE === '0') {
105
+ return { visible: false, verdict: null, pendingCount: 0, topGaps: [], summary: '' };
106
+ }
107
+ try {
108
+ const home = homeDir ?? osHomedir();
109
+ const last = readLastTick(home);
110
+ const verdictDoc = readLatestVerdict(cwd);
111
+ const pending = listPending(cwd).filter((p) => p.status === 'pending');
112
+ const verdict = verdictDoc?.verdict ?? last?.verdict ?? 'unknown';
113
+ const gapSource = verdictDoc?.gaps ?? last?.gaps ?? [];
114
+ const topGaps = gapSource.filter((g) => g.severity === 'high').slice(0, 3);
115
+ const visible = verdict !== 'healthy' || pending.length > 0;
116
+ const parts = [`verdict ${verdict}`];
117
+ if (pending.length) parts.push(`${pending.length} pending`);
118
+ return {
119
+ visible,
120
+ verdict,
121
+ pendingCount: pending.length,
122
+ topGaps,
123
+ summary: parts.join(' · '),
124
+ };
125
+ } catch {
126
+ return { visible: false, verdict: null, pendingCount: 0, topGaps: [], summary: '' };
127
+ }
128
+ }
129
+
130
+ export function formatOracleDockDetail(state) {
131
+ if (!state?.visible) return 'Oracle: healthy — no pending approvals.';
132
+ const lines = [`Oracle overseer · ${state.summary}`];
133
+ for (const g of state.topGaps || []) {
134
+ if (isVerdictOnlyGap(g)) {
135
+ lines.push(` [${g.severity}] ${g.id} (verdict-only): ${g.detail}`);
136
+ } else {
137
+ lines.push(` [${g.severity}] ${g.id}: ${g.detail}`);
138
+ }
139
+ }
140
+ if (state.pendingCount > 0) {
141
+ lines.push(' Pending: construct oracle pending');
142
+ }
143
+ lines.push(' Review: construct oracle review · Gaps: construct oracle gaps');
144
+ return lines.join('\n');
145
+ }
@@ -0,0 +1,97 @@
1
+ /**
2
+ * lib/libreoffice-export.mjs — LibreOffice headless conversion for legacy .doc export.
3
+ *
4
+ * Pandoc has no `.doc` writer; Construct exports DOCX first, then down-converts via
5
+ * `soffice --headless --convert-to doc`. Binary is optional (ADR-0024).
6
+ */
7
+
8
+ import fs from 'node:fs';
9
+ import path from 'node:path';
10
+ import { spawnSync } from 'node:child_process';
11
+
12
+ const SOFFICE_CANDIDATES = [
13
+ 'soffice',
14
+ 'libreoffice',
15
+ '/Applications/LibreOffice.app/Contents/MacOS/soffice',
16
+ ];
17
+
18
+ function whichBin(name) {
19
+ const cmd = process.platform === 'win32' ? 'where' : 'which';
20
+ const result = spawnSync(cmd, [name], { encoding: 'utf8' });
21
+ if (result.status !== 0) return null;
22
+ return (result.stdout || '').trim().split('\n')[0] || null;
23
+ }
24
+
25
+ export function resolveLibreOfficeBin(env = process.env) {
26
+ const fromEnv = (env.CONSTRUCT_LIBREOFFICE_BIN || env.SOFFICE_BIN || '').trim();
27
+ if (fromEnv && fs.existsSync(fromEnv)) return fromEnv;
28
+ for (const candidate of SOFFICE_CANDIDATES) {
29
+ if (candidate.includes('/') && fs.existsSync(candidate)) return candidate;
30
+ const found = whichBin(candidate);
31
+ if (found) return found;
32
+ }
33
+ return null;
34
+ }
35
+
36
+ export function libreOfficePresent(env = process.env) {
37
+ return Boolean(resolveLibreOfficeBin(env));
38
+ }
39
+
40
+ export function libreOfficeInstallHint() {
41
+ return 'Install LibreOffice to enable legacy .doc export (e.g. `brew install --cask libreoffice` on macOS, `apt install libreoffice` on Debian/Ubuntu, or https://www.libreoffice.org/download/). Override with CONSTRUCT_LIBREOFFICE_BIN.';
42
+ }
43
+
44
+ export function convertDocxToDoc({
45
+ docxPath,
46
+ outputPath,
47
+ env = process.env,
48
+ spawnFn = spawnSync,
49
+ } = {}) {
50
+ const soffice = resolveLibreOfficeBin(env);
51
+ if (!soffice) {
52
+ return {
53
+ ok: false,
54
+ message: libreOfficeInstallHint(),
55
+ missing: ['libreoffice'],
56
+ };
57
+ }
58
+ if (!fs.existsSync(docxPath)) {
59
+ return { ok: false, message: `DOCX intermediate missing: ${docxPath}` };
60
+ }
61
+
62
+ const outDir = path.dirname(outputPath);
63
+ fs.mkdirSync(outDir, { recursive: true });
64
+
65
+ const result = spawnFn(soffice, [
66
+ '--headless',
67
+ '--convert-to', 'doc',
68
+ '--outdir', outDir,
69
+ path.resolve(docxPath),
70
+ ], { encoding: 'utf8', env });
71
+
72
+ if (result.status !== 0) {
73
+ return {
74
+ ok: false,
75
+ message: `LibreOffice conversion failed (exit ${result.status}): ${(result.stderr || result.stdout || '').trim().slice(0, 400)}`,
76
+ };
77
+ }
78
+
79
+ const expected = path.join(outDir, `${path.parse(docxPath).name}.doc`);
80
+ if (!fs.existsSync(expected)) {
81
+ return {
82
+ ok: false,
83
+ message: `LibreOffice did not produce expected output: ${expected}`,
84
+ };
85
+ }
86
+
87
+ if (path.resolve(expected) !== path.resolve(outputPath)) {
88
+ fs.renameSync(expected, outputPath);
89
+ }
90
+
91
+ return {
92
+ ok: true,
93
+ outputPath,
94
+ engine: 'libreoffice',
95
+ message: `Wrote ${path.relative(process.cwd(), outputPath)}`,
96
+ };
97
+ }
@@ -231,6 +231,15 @@ export const LIMITS = {
231
231
  envOverride: 'CONSTRUCT_HOOK_CALLS_MAX_MB',
232
232
  },
233
233
 
234
+ // Rule path reference telemetry. ~/.cx/rule-calls.jsonl.
235
+
236
+ 'rule-calls': {
237
+ maxBytes: 10 * 1024 * 1024,
238
+ maxSegments: 2,
239
+ gzip: true,
240
+ envOverride: 'CONSTRUCT_RULE_CALLS_MAX_MB',
241
+ },
242
+
234
243
  // Legacy-lock fallback firings on the beads write path. ~/.cx/beads-fallback.jsonl.
235
244
 
236
245
  'beads-fallback': {
@@ -0,0 +1,206 @@
1
+ /**
2
+ * lib/maintenance/docker-reclaim.mjs — reclaim abandoned per-home Postgres
3
+ * containers and data volumes that outlived the HOME that created them.
4
+ *
5
+ * Construct derives a Postgres container/volume namespace from a sha256 slice
6
+ * of the resolved home directory (lib/home-namespace.mjs) so isolated HOMEs —
7
+ * test sandboxes, ephemeral /tmp footprint runs, deleted accounts — never
8
+ * collide on one machine. Each container carries a `restart: unless-stopped`
9
+ * policy. When the owning HOME or its compose project directory is deleted,
10
+ * Docker keeps the container alive with nothing left to reap it: an orphan that
11
+ * pins memory and leaks disk through its named volume.
12
+ *
13
+ * The reclaim runs from the upgrade flows (lib/update.mjs, lib/upgrade.mjs)
14
+ * once per upgrade and from `construct cleanup`, never from `construct sync`,
15
+ * which fires constantly and must not mutate machine-global Docker state.
16
+ *
17
+ * An orphan is removed only when every signal agrees it is abandoned:
18
+ * - the name carries a Construct per-home suffix (construct-postgres-<8 hex>),
19
+ * so hand-named and third-party containers stay untouched;
20
+ * - the suffix differs from the current HOME's suffix, sparing the live install;
21
+ * - the name differs from an operator-pinned CONSTRUCT_PG_CONTAINER;
22
+ * - a container's compose working_dir label points at a path that is gone;
23
+ * - a volume has no surviving container for its suffix.
24
+ * The legacy singular `construct-postgres` (no suffix) is out of scope.
25
+ */
26
+
27
+ import { spawnSync } from 'node:child_process';
28
+ import fs from 'node:fs';
29
+ import os from 'node:os';
30
+
31
+ import { homeNamespaceSuffix } from '../home-namespace.mjs';
32
+
33
+ const CONTAINER_RE = /^construct-postgres-([0-9a-f]{8})$/;
34
+ const VOLUME_RE = /construct-postgres-([0-9a-f]{8})/;
35
+ const CONTAINER_FORMAT = '{{.Names}}\t{{.Label "com.docker.compose.project.working_dir"}}';
36
+ const DEFAULT_MAX = 25;
37
+
38
+ export function defaultRunDocker(args) {
39
+ const r = spawnSync('docker', args, { encoding: 'utf8' });
40
+ return {
41
+ status: r.status ?? 1,
42
+ stdout: r.stdout || '',
43
+ stderr: r.stderr || '',
44
+ error: r.error || null,
45
+ };
46
+ }
47
+
48
+ // `docker ps -a` lists every container with its compose working_dir label on a
49
+ // tab. Anything that does not match the per-home naming scheme is dropped, so
50
+ // the parser only surfaces Construct-managed candidates.
51
+
52
+ export function parseContainers(stdout) {
53
+ const out = [];
54
+ for (const raw of String(stdout || '').split('\n')) {
55
+ const line = raw.replace(/\s+$/, '');
56
+ if (!line) continue;
57
+ const tab = line.indexOf('\t');
58
+ const name = (tab === -1 ? line : line.slice(0, tab)).trim();
59
+ const workingDir = tab === -1 ? '' : line.slice(tab + 1).trim();
60
+ const match = CONTAINER_RE.exec(name);
61
+ if (!match) continue;
62
+ out.push({ name, suffix: match[1], workingDir });
63
+ }
64
+ return out;
65
+ }
66
+
67
+ export function parseVolumes(stdout) {
68
+ const out = [];
69
+ for (const raw of String(stdout || '').split('\n')) {
70
+ const name = raw.trim();
71
+ if (!name) continue;
72
+ const match = VOLUME_RE.exec(name);
73
+ if (!match) continue;
74
+ out.push({ name, suffix: match[1] });
75
+ }
76
+ return out;
77
+ }
78
+
79
+ // A container is an orphan when it belongs to a foreign home namespace, is not
80
+ // the operator-pinned container, and its compose project directory is gone. A
81
+ // volume is an orphan when its suffix is foreign and no surviving container —
82
+ // including containers spared this pass — still claims it.
83
+
84
+ export function selectOrphans({
85
+ containers,
86
+ volumes,
87
+ currentSuffix,
88
+ pinnedName = null,
89
+ pathExists = (p) => fs.existsSync(p),
90
+ }) {
91
+ const orphanContainers = containers.filter((c) =>
92
+ c.suffix !== currentSuffix &&
93
+ c.name !== pinnedName &&
94
+ c.workingDir &&
95
+ !pathExists(c.workingDir));
96
+
97
+ const orphanSuffixes = new Set(orphanContainers.map((c) => c.suffix));
98
+ const liveSuffixes = new Set(
99
+ containers.filter((c) => !orphanSuffixes.has(c.suffix)).map((c) => c.suffix),
100
+ );
101
+
102
+ const orphanVolumes = volumes.filter((v) =>
103
+ v.suffix &&
104
+ v.suffix !== currentSuffix &&
105
+ !liveSuffixes.has(v.suffix));
106
+
107
+ return { orphanContainers, orphanVolumes };
108
+ }
109
+
110
+ export function reclaimOrphanedDockerResources({
111
+ runDocker = defaultRunDocker,
112
+ homeDir = os.homedir(),
113
+ env = process.env,
114
+ dryRun = false,
115
+ max = DEFAULT_MAX,
116
+ pathExists = (p) => fs.existsSync(p),
117
+ } = {}) {
118
+ const summary = {
119
+ available: false,
120
+ reason: null,
121
+ scanned: { containers: 0, volumes: 0 },
122
+ removedContainers: [],
123
+ removedVolumes: [],
124
+ skipped: [],
125
+ errors: [],
126
+ dryRun,
127
+ };
128
+
129
+ // A non-zero `docker ps` means the daemon is down or Docker is absent; the
130
+ // machine then holds no reclaimable state and the pass is a clean no-op.
131
+
132
+ const psResult = runDocker(['ps', '-a', '--format', CONTAINER_FORMAT]);
133
+ if (!psResult || psResult.status !== 0) {
134
+ summary.reason = 'docker-unavailable';
135
+ return summary;
136
+ }
137
+ summary.available = true;
138
+
139
+ const containers = parseContainers(psResult.stdout);
140
+ summary.scanned.containers = containers.length;
141
+
142
+ const volResult = runDocker(['volume', 'ls', '--format', '{{.Name}}']);
143
+ const volumes = volResult && volResult.status === 0 ? parseVolumes(volResult.stdout) : [];
144
+ summary.scanned.volumes = volumes.length;
145
+
146
+ const currentSuffix = homeNamespaceSuffix(homeDir);
147
+ const pinnedName = (env.CONSTRUCT_PG_CONTAINER || '').trim() || null;
148
+
149
+ const { orphanContainers, orphanVolumes } = selectOrphans({
150
+ containers,
151
+ volumes,
152
+ currentSuffix,
153
+ pinnedName,
154
+ pathExists,
155
+ });
156
+
157
+ let budget = Number.isFinite(max) && max > 0 ? max : DEFAULT_MAX;
158
+
159
+ for (const c of orphanContainers) {
160
+ if (budget <= 0) {
161
+ summary.skipped.push({ name: c.name, reason: 'reclaim-budget-exhausted' });
162
+ continue;
163
+ }
164
+ budget -= 1;
165
+ if (dryRun) {
166
+ summary.removedContainers.push(c.name);
167
+ continue;
168
+ }
169
+ const r = runDocker(['rm', '-f', '-v', c.name]);
170
+ if (r && r.status === 0) summary.removedContainers.push(c.name);
171
+ else summary.errors.push({ name: c.name, error: (r?.stderr || '').trim() || `docker rm exited ${r?.status}` });
172
+ }
173
+
174
+ // A named data volume only drops once nothing references it. `docker volume
175
+ // rm` exits non-zero for an in-use or already-gone volume, so a failure here
176
+ // is recorded as skipped rather than a hard error.
177
+
178
+ for (const v of orphanVolumes) {
179
+ if (budget <= 0) {
180
+ summary.skipped.push({ name: v.name, reason: 'reclaim-budget-exhausted' });
181
+ continue;
182
+ }
183
+ budget -= 1;
184
+ if (dryRun) {
185
+ summary.removedVolumes.push(v.name);
186
+ continue;
187
+ }
188
+ const r = runDocker(['volume', 'rm', v.name]);
189
+ if (r && r.status === 0) summary.removedVolumes.push(v.name);
190
+ else summary.skipped.push({ name: v.name, reason: 'in-use-or-missing' });
191
+ }
192
+
193
+ return summary;
194
+ }
195
+
196
+ export function formatReclaim(summary) {
197
+ if (!summary || !summary.available) return null;
198
+ const containers = summary.removedContainers.length;
199
+ const volumes = summary.removedVolumes.length;
200
+ if (!containers && !volumes) return null;
201
+ const parts = [];
202
+ if (containers) parts.push(`${containers} orphaned Postgres container${containers === 1 ? '' : 's'}`);
203
+ if (volumes) parts.push(`${volumes} abandoned data volume${volumes === 1 ? '' : 's'}`);
204
+ const verb = summary.dryRun ? 'Would reclaim' : 'Reclaimed';
205
+ return `${verb} ${parts.join(' and ')} from deleted home namespaces.`;
206
+ }
@@ -0,0 +1,74 @@
1
+ /**
2
+ * lib/mcp/external-schema-cost.mjs — measured token cost for heavy external MCP servers.
3
+ *
4
+ * Committed fixtures under tests/fixtures/mcp-tool-schemas/ hold captured tools/list
5
+ * payloads per server. Regenerate with scripts/measure-external-mcp-schemas.mjs.
6
+ * Sums schemaTokens via estimateToolTokens (JSON length × 0.25) for sync-time trim
7
+ * comments and ADR-0032 footnotes.
8
+ */
9
+
10
+ import { readFileSync, existsSync } from 'node:fs';
11
+ import { dirname, join } from 'node:path';
12
+ import { fileURLToPath } from 'node:url';
13
+ import { HEAVY_EXTERNAL_MCP_IDS, estimateToolTokens } from './tool-budget.mjs';
14
+
15
+ const __dirname = dirname(fileURLToPath(import.meta.url));
16
+ const REPO_ROOT = join(__dirname, '..', '..');
17
+ export const EXTERNAL_MCP_SCHEMA_FIXTURE_DIR = join(REPO_ROOT, 'tests', 'fixtures', 'mcp-tool-schemas');
18
+
19
+ function readJson(path) {
20
+ return JSON.parse(readFileSync(path, 'utf8'));
21
+ }
22
+
23
+ // Load one server's committed fixture; missing files throw so CI fails loudly.
24
+
25
+ export function loadExternalMcpSchemaFixture(serverId) {
26
+ const path = join(EXTERNAL_MCP_SCHEMA_FIXTURE_DIR, `${serverId}.json`);
27
+ if (!existsSync(path)) {
28
+ throw new Error(`Missing external MCP schema fixture: ${path}`);
29
+ }
30
+ const doc = readJson(path);
31
+ const tools = Array.isArray(doc.tools) ? doc.tools : [];
32
+ const schemaTokens = Number.isFinite(doc.schemaTokens)
33
+ ? doc.schemaTokens
34
+ : estimateToolTokens(tools);
35
+ return {
36
+ serverId,
37
+ capturedAt: doc.capturedAt || null,
38
+ toolCount: tools.length,
39
+ schemaTokens,
40
+ tools,
41
+ };
42
+ }
43
+
44
+ // Per-server breakdown plus total for HEAVY_EXTERNAL_MCP_IDS.
45
+
46
+ export function measuredHeavyExternalMcpCosts() {
47
+ const servers = {};
48
+ let totalSchemaTokens = 0;
49
+ for (const id of HEAVY_EXTERNAL_MCP_IDS) {
50
+ const row = loadExternalMcpSchemaFixture(id);
51
+ servers[id] = {
52
+ toolCount: row.toolCount,
53
+ schemaTokens: row.schemaTokens,
54
+ capturedAt: row.capturedAt,
55
+ };
56
+ totalSchemaTokens += row.schemaTokens;
57
+ }
58
+ return { servers, totalSchemaTokens };
59
+ }
60
+
61
+ export function measuredHeavyExternalMcpTokenCost() {
62
+ return measuredHeavyExternalMcpCosts().totalSchemaTokens;
63
+ }
64
+
65
+ // Lazy constant — fixtures may be absent while scripts/measure-external-mcp-schemas.mjs runs.
66
+
67
+ let cachedMeasuredTotal = null;
68
+
69
+ export function getMeasuredHeavyExternalMcpTokenCost() {
70
+ if (cachedMeasuredTotal === null) {
71
+ cachedMeasuredTotal = measuredHeavyExternalMcpTokenCost();
72
+ }
73
+ return cachedMeasuredTotal;
74
+ }