okengine 0.1.4

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 (674) hide show
  1. package/AGENTS.md +91 -0
  2. package/README.md +327 -0
  3. package/docs/spec/complete-example.md +1188 -0
  4. package/docs/spec/console.md +739 -0
  5. package/docs/spec/four-applications.md +1188 -0
  6. package/docs/spec/unified-theory.md +489 -0
  7. package/package.json +152 -0
  8. package/spec/manifest.v1.schema.json +624 -0
  9. package/src/auth/api-keys.ts +168 -0
  10. package/src/auth/attenuation.ts +116 -0
  11. package/src/auth/auth.test.ts +273 -0
  12. package/src/auth/cross-plane.ts +98 -0
  13. package/src/auth/index.ts +112 -0
  14. package/src/auth/invites.ts +66 -0
  15. package/src/auth/invoke-as.ts +49 -0
  16. package/src/auth/operator.ts +178 -0
  17. package/src/auth/planes.ts +102 -0
  18. package/src/auth/plugin.ts +44 -0
  19. package/src/auth/roles.ts +84 -0
  20. package/src/auth/sessions.ts +418 -0
  21. package/src/auth/tables.ts +131 -0
  22. package/src/cli/args.ts +50 -0
  23. package/src/cli/branch.ts +75 -0
  24. package/src/cli/build.ts +86 -0
  25. package/src/cli/client-add.test.ts +140 -0
  26. package/src/cli/client-add.ts +164 -0
  27. package/src/cli/completion.test.ts +58 -0
  28. package/src/cli/completion.ts +296 -0
  29. package/src/cli/dev-app-runner.ts +77 -0
  30. package/src/cli/dev.test.ts +435 -0
  31. package/src/cli/dev.ts +594 -0
  32. package/src/cli/doc-drift.test.ts +146 -0
  33. package/src/cli/doc-drift.ts +397 -0
  34. package/src/cli/docker-cli.test.ts +221 -0
  35. package/src/cli/docker.ts +127 -0
  36. package/src/cli/doctor-diff-examples.ts +92 -0
  37. package/src/cli/doctor-diff.test.ts +91 -0
  38. package/src/cli/doctor-diff.ts +274 -0
  39. package/src/cli/doctor-pii.test.ts +98 -0
  40. package/src/cli/doctor-pii.ts +151 -0
  41. package/src/cli/doctor.test.ts +150 -0
  42. package/src/cli/doctor.ts +223 -0
  43. package/src/cli/eval.ts +122 -0
  44. package/src/cli/exit.ts +19 -0
  45. package/src/cli/gates-list.test.ts +47 -0
  46. package/src/cli/gates-list.ts +84 -0
  47. package/src/cli/images.ts +277 -0
  48. package/src/cli/index.ts +99 -0
  49. package/src/cli/json-out.test.ts +129 -0
  50. package/src/cli/load-config.ts +71 -0
  51. package/src/cli/mcp-from-console.ts +50 -0
  52. package/src/cli/privacy-erase.ts +73 -0
  53. package/src/cli/registry.ts +455 -0
  54. package/src/cli/safe-defaults.test.ts +92 -0
  55. package/src/cli/schema.ts +196 -0
  56. package/src/cli/stack.ts +82 -0
  57. package/src/cli/start.ts +106 -0
  58. package/src/cli/upgrade.ts +105 -0
  59. package/src/cli/vault-cmd.ts +140 -0
  60. package/src/client/budget-entry.ts +34 -0
  61. package/src/client/budget.test.ts +17 -0
  62. package/src/client/create.test.ts +159 -0
  63. package/src/client/create.ts +152 -0
  64. package/src/client/errors.ts +60 -0
  65. package/src/client/index.ts +35 -0
  66. package/src/client/notes-contract.test.ts +289 -0
  67. package/src/client/transport.test.ts +123 -0
  68. package/src/client/transport.ts +254 -0
  69. package/src/client/types.ts +282 -0
  70. package/src/compiler/aot.test.ts +248 -0
  71. package/src/compiler/aot.ts +182 -0
  72. package/src/compiler/differential.test.ts +241 -0
  73. package/src/compiler/dynamic.ts +54 -0
  74. package/src/compiler/effects-infer.ts +541 -0
  75. package/src/compiler/emit.ts +40 -0
  76. package/src/compiler/extract.test.ts +177 -0
  77. package/src/compiler/extract.ts +1368 -0
  78. package/src/compiler/fixtures/raw/raw-unannotated.ts +28 -0
  79. package/src/compiler/fixtures/skyport/oke.config.ts +24 -0
  80. package/src/compiler/fixtures/skyport/src/ai.ts +15 -0
  81. package/src/compiler/fixtures/skyport/src/app.ts +10 -0
  82. package/src/compiler/fixtures/skyport/src/channels.ts +6 -0
  83. package/src/compiler/fixtures/skyport/src/core.ts +3 -0
  84. package/src/compiler/fixtures/skyport/src/flows/bookings/index.ts +57 -0
  85. package/src/compiler/fixtures/skyport/src/flows/bookings/signals.ts +11 -0
  86. package/src/compiler/fixtures/skyport/src/flows/payments/index.ts +14 -0
  87. package/src/compiler/fixtures/skyport/src/flows/support/index.ts +20 -0
  88. package/src/compiler/fixtures/skyport/src/gates.ts +14 -0
  89. package/src/compiler/fixtures/skyport/src/journeys.ts +9 -0
  90. package/src/compiler/fixtures/skyport/src/schema.ts +3 -0
  91. package/src/compiler/fixtures/skyport/src/vault.ts +6 -0
  92. package/src/compiler/fixtures/skyport.expected.json +241 -0
  93. package/src/compiler/fixtures/triggers/five-triggers.ts +69 -0
  94. package/src/compiler/http-parse.ts +214 -0
  95. package/src/compiler/index.ts +67 -0
  96. package/src/compiler/response.ts +89 -0
  97. package/src/compiler/sucrose.ts +238 -0
  98. package/src/config/index.ts +167 -0
  99. package/src/console/budget.test.ts +73 -0
  100. package/src/console/index.ts +30 -0
  101. package/src/console/server/access.test.ts +370 -0
  102. package/src/console/server/access.ts +777 -0
  103. package/src/console/server/ai.test.ts +172 -0
  104. package/src/console/server/ai.ts +632 -0
  105. package/src/console/server/app.ts +294 -0
  106. package/src/console/server/auth-rate.test.ts +81 -0
  107. package/src/console/server/auth-rate.ts +72 -0
  108. package/src/console/server/bind.ts +25 -0
  109. package/src/console/server/channels.test.ts +179 -0
  110. package/src/console/server/channels.ts +469 -0
  111. package/src/console/server/claim.test.ts +66 -0
  112. package/src/console/server/claim.ts +134 -0
  113. package/src/console/server/clock.test.ts +239 -0
  114. package/src/console/server/clock.ts +390 -0
  115. package/src/console/server/console.test.ts +338 -0
  116. package/src/console/server/diff.test.ts +299 -0
  117. package/src/console/server/diff.ts +352 -0
  118. package/src/console/server/dry-run.audit.test.ts +118 -0
  119. package/src/console/server/flows-invoke.test.ts +148 -0
  120. package/src/console/server/flows.ts +3633 -0
  121. package/src/console/server/gates.test.ts +360 -0
  122. package/src/console/server/gates.ts +869 -0
  123. package/src/console/server/index.ts +141 -0
  124. package/src/console/server/live.test.ts +30 -0
  125. package/src/console/server/live.ts +109 -0
  126. package/src/console/server/plugin.ts +50 -0
  127. package/src/console/server/plugins.test.ts +131 -0
  128. package/src/console/server/plugins.ts +429 -0
  129. package/src/console/server/runs-pii.ts +98 -0
  130. package/src/console/server/security-headers.ts +97 -0
  131. package/src/console/server/security.gate.test.ts +286 -0
  132. package/src/console/server/serve.ts +319 -0
  133. package/src/console/server/signals.test.ts +88 -0
  134. package/src/console/server/signals.ts +282 -0
  135. package/src/console/server/state.ts +982 -0
  136. package/src/console/server/store.test.ts +233 -0
  137. package/src/console/server/store.ts +861 -0
  138. package/src/console/server/structural.ts +95 -0
  139. package/src/console/server/vault.test.ts +251 -0
  140. package/src/console/server/vault.ts +430 -0
  141. package/src/console/ui/access/AccessA11yView.tsx +237 -0
  142. package/src/console/ui/access/a11y.test.tsx +122 -0
  143. package/src/console/ui/access/acknowledgement.test.ts +13 -0
  144. package/src/console/ui/access/acknowledgement.ts +35 -0
  145. package/src/console/ui/access/blast-radius.test.ts +26 -0
  146. package/src/console/ui/access/blast-radius.ts +40 -0
  147. package/src/console/ui/access/confirmation.test.ts +38 -0
  148. package/src/console/ui/access/confirmation.ts +36 -0
  149. package/src/console/ui/access/fixture.ts +203 -0
  150. package/src/console/ui/access/grantable.test.ts +17 -0
  151. package/src/console/ui/access/grantable.ts +36 -0
  152. package/src/console/ui/access/hygiene.test.ts +12 -0
  153. package/src/console/ui/access/hygiene.ts +39 -0
  154. package/src/console/ui/access/index.ts +54 -0
  155. package/src/console/ui/access/provenance.test.ts +12 -0
  156. package/src/console/ui/access/provenance.ts +29 -0
  157. package/src/console/ui/access/search.test.ts +17 -0
  158. package/src/console/ui/access/search.ts +72 -0
  159. package/src/console/ui/access/types.ts +111 -0
  160. package/src/console/ui/ai/AiA11yView.tsx +265 -0
  161. package/src/console/ui/ai/a11y.test.tsx +99 -0
  162. package/src/console/ui/ai/findings.ts +41 -0
  163. package/src/console/ui/ai/fixture.ts +182 -0
  164. package/src/console/ui/ai/format.ts +90 -0
  165. package/src/console/ui/ai/group.test.ts +37 -0
  166. package/src/console/ui/ai/group.ts +88 -0
  167. package/src/console/ui/ai/index.ts +66 -0
  168. package/src/console/ui/ai/promotion.test.ts +77 -0
  169. package/src/console/ui/ai/promotion.ts +150 -0
  170. package/src/console/ui/ai/search.test.ts +40 -0
  171. package/src/console/ui/ai/search.ts +89 -0
  172. package/src/console/ui/ai/types.ts +139 -0
  173. package/src/console/ui/architecture/ArchitectureA11yView.tsx +163 -0
  174. package/src/console/ui/architecture/a11y.test.tsx +97 -0
  175. package/src/console/ui/architecture/boundary.ts +42 -0
  176. package/src/console/ui/architecture/declared.ts +165 -0
  177. package/src/console/ui/architecture/fixture.ts +74 -0
  178. package/src/console/ui/architecture/index.ts +84 -0
  179. package/src/console/ui/architecture/layers.ts +64 -0
  180. package/src/console/ui/architecture/layout.ts +68 -0
  181. package/src/console/ui/architecture/pathologies.ts +260 -0
  182. package/src/console/ui/architecture/search.ts +145 -0
  183. package/src/console/ui/architecture/traffic.ts +102 -0
  184. package/src/console/ui/architecture/types.ts +144 -0
  185. package/src/console/ui/architecture/view.test.ts +165 -0
  186. package/src/console/ui/architecture/view.ts +516 -0
  187. package/src/console/ui/channels/ChannelsA11yView.tsx +180 -0
  188. package/src/console/ui/channels/a11y.test.tsx +96 -0
  189. package/src/console/ui/channels/confirmation.test.ts +37 -0
  190. package/src/console/ui/channels/confirmation.ts +27 -0
  191. package/src/console/ui/channels/fallback.test.ts +15 -0
  192. package/src/console/ui/channels/fallback.ts +20 -0
  193. package/src/console/ui/channels/findings.ts +38 -0
  194. package/src/console/ui/channels/fixture.ts +134 -0
  195. package/src/console/ui/channels/group.ts +25 -0
  196. package/src/console/ui/channels/index.ts +57 -0
  197. package/src/console/ui/channels/locale.test.ts +17 -0
  198. package/src/console/ui/channels/locale.ts +31 -0
  199. package/src/console/ui/channels/mask.test.ts +14 -0
  200. package/src/console/ui/channels/mask.ts +12 -0
  201. package/src/console/ui/channels/search.ts +58 -0
  202. package/src/console/ui/channels/taxonomy.test.ts +17 -0
  203. package/src/console/ui/channels/taxonomy.ts +49 -0
  204. package/src/console/ui/channels/types.ts +117 -0
  205. package/src/console/ui/clock/ClockA11yView.tsx +182 -0
  206. package/src/console/ui/clock/a11y.test.tsx +99 -0
  207. package/src/console/ui/clock/confirmation.test.ts +32 -0
  208. package/src/console/ui/clock/confirmation.ts +34 -0
  209. package/src/console/ui/clock/findings.ts +40 -0
  210. package/src/console/ui/clock/fixture.ts +111 -0
  211. package/src/console/ui/clock/health.test.ts +25 -0
  212. package/src/console/ui/clock/health.ts +61 -0
  213. package/src/console/ui/clock/index.ts +49 -0
  214. package/src/console/ui/clock/search.test.ts +39 -0
  215. package/src/console/ui/clock/search.ts +76 -0
  216. package/src/console/ui/clock/timeline.test.ts +44 -0
  217. package/src/console/ui/clock/timeline.ts +41 -0
  218. package/src/console/ui/clock/types.ts +78 -0
  219. package/src/console/ui/clock/waiting-on.test.ts +34 -0
  220. package/src/console/ui/clock/waiting-on.ts +83 -0
  221. package/src/console/ui/confirmation.unify.test.ts +92 -0
  222. package/src/console/ui/diff/DiffA11yView.tsx +85 -0
  223. package/src/console/ui/diff/a11y.test.tsx +99 -0
  224. package/src/console/ui/diff/fixture.ts +85 -0
  225. package/src/console/ui/diff/group.test.ts +58 -0
  226. package/src/console/ui/diff/group.ts +97 -0
  227. package/src/console/ui/diff/index.ts +38 -0
  228. package/src/console/ui/diff/search.test.ts +32 -0
  229. package/src/console/ui/diff/search.ts +75 -0
  230. package/src/console/ui/diff/summary.ts +74 -0
  231. package/src/console/ui/diff/types.ts +48 -0
  232. package/src/console/ui/dist/assets/index-B71Yl_SS.js +10 -0
  233. package/src/console/ui/dist/assets/panel-access-Dd37LU2c.js +64 -0
  234. package/src/console/ui/dist/assets/panel-ai-CC7LR6-J.js +1 -0
  235. package/src/console/ui/dist/assets/panel-architecture-B5b3iKCz.js +1 -0
  236. package/src/console/ui/dist/assets/panel-channels-CeNjTKXp.js +1 -0
  237. package/src/console/ui/dist/assets/panel-clock-DgFTLoHV.js +1 -0
  238. package/src/console/ui/dist/assets/panel-diff-DxehccqB.js +1 -0
  239. package/src/console/ui/dist/assets/panel-flows-BtrVn-Eg.js +45 -0
  240. package/src/console/ui/dist/assets/panel-gates-Z9MKRGdH.js +1 -0
  241. package/src/console/ui/dist/assets/panel-overview-Bd48d9km.js +1 -0
  242. package/src/console/ui/dist/assets/panel-plugins-DWd0TowH.js +1 -0
  243. package/src/console/ui/dist/assets/panel-runs-BwsWqKeB.js +1 -0
  244. package/src/console/ui/dist/assets/panel-signals-9najbZY2.js +1 -0
  245. package/src/console/ui/dist/assets/panel-store-OHkP2pDp.js +1 -0
  246. package/src/console/ui/dist/assets/panel-traces-tn2JoY8U.js +1 -0
  247. package/src/console/ui/dist/assets/panel-vault-BbfWdox0.js +1 -0
  248. package/src/console/ui/dist/assets/rolldown-runtime-CNC7AqOf.js +1 -0
  249. package/src/console/ui/dist/assets/style-Cnl7WLya.css +3 -0
  250. package/src/console/ui/dist/index.html +14 -0
  251. package/src/console/ui/flows/FlowsA11yView.tsx +155 -0
  252. package/src/console/ui/flows/a11y.test.tsx +105 -0
  253. package/src/console/ui/flows/buffer.test.ts +91 -0
  254. package/src/console/ui/flows/buffer.ts +118 -0
  255. package/src/console/ui/flows/confirmation.ts +131 -0
  256. package/src/console/ui/flows/contract.test.ts +89 -0
  257. package/src/console/ui/flows/contract.ts +353 -0
  258. package/src/console/ui/flows/fixture.ts +81 -0
  259. package/src/console/ui/flows/graph.test.ts +101 -0
  260. package/src/console/ui/flows/graph.ts +549 -0
  261. package/src/console/ui/flows/index.ts +87 -0
  262. package/src/console/ui/flows/save-as-test.test.ts +47 -0
  263. package/src/console/ui/flows/save-as-test.ts +99 -0
  264. package/src/console/ui/flows/search.ts +255 -0
  265. package/src/console/ui/flows/tiers.ts +140 -0
  266. package/src/console/ui/gates/GatesA11yView.tsx +192 -0
  267. package/src/console/ui/gates/a11y.test.tsx +116 -0
  268. package/src/console/ui/gates/audit.test.ts +40 -0
  269. package/src/console/ui/gates/audit.ts +63 -0
  270. package/src/console/ui/gates/denial.test.ts +36 -0
  271. package/src/console/ui/gates/denial.ts +51 -0
  272. package/src/console/ui/gates/findings.ts +19 -0
  273. package/src/console/ui/gates/fixture.ts +189 -0
  274. package/src/console/ui/gates/group.test.ts +30 -0
  275. package/src/console/ui/gates/group.ts +102 -0
  276. package/src/console/ui/gates/index.ts +47 -0
  277. package/src/console/ui/gates/search.test.ts +49 -0
  278. package/src/console/ui/gates/search.ts +115 -0
  279. package/src/console/ui/gates/types.ts +121 -0
  280. package/src/console/ui/overview/OverviewA11yView.tsx +152 -0
  281. package/src/console/ui/overview/a11y.test.tsx +113 -0
  282. package/src/console/ui/overview/busiest.ts +26 -0
  283. package/src/console/ui/overview/compose.ts +133 -0
  284. package/src/console/ui/overview/cost.test.ts +39 -0
  285. package/src/console/ui/overview/cost.ts +138 -0
  286. package/src/console/ui/overview/fixture.ts +161 -0
  287. package/src/console/ui/overview/golden.ts +57 -0
  288. package/src/console/ui/overview/index.ts +59 -0
  289. package/src/console/ui/overview/rank.test.ts +136 -0
  290. package/src/console/ui/overview/rank.ts +210 -0
  291. package/src/console/ui/overview/slo.test.ts +73 -0
  292. package/src/console/ui/overview/slo.ts +229 -0
  293. package/src/console/ui/overview/types.ts +121 -0
  294. package/src/console/ui/overview/verdict.ts +131 -0
  295. package/src/console/ui/plugins/PluginsA11yView.tsx +227 -0
  296. package/src/console/ui/plugins/a11y.test.tsx +97 -0
  297. package/src/console/ui/plugins/command.test.ts +22 -0
  298. package/src/console/ui/plugins/command.ts +27 -0
  299. package/src/console/ui/plugins/findings.ts +39 -0
  300. package/src/console/ui/plugins/fixture.ts +233 -0
  301. package/src/console/ui/plugins/group.test.ts +38 -0
  302. package/src/console/ui/plugins/group.ts +67 -0
  303. package/src/console/ui/plugins/index.ts +40 -0
  304. package/src/console/ui/plugins/search.ts +67 -0
  305. package/src/console/ui/plugins/types.ts +115 -0
  306. package/src/console/ui/runs/RunsA11yView.tsx +267 -0
  307. package/src/console/ui/runs/a11y.test.tsx +99 -0
  308. package/src/console/ui/runs/explain.test.ts +23 -0
  309. package/src/console/ui/runs/explain.ts +32 -0
  310. package/src/console/ui/runs/fixture.ts +189 -0
  311. package/src/console/ui/runs/group.test.ts +24 -0
  312. package/src/console/ui/runs/group.ts +85 -0
  313. package/src/console/ui/runs/histogram.test.ts +31 -0
  314. package/src/console/ui/runs/histogram.ts +85 -0
  315. package/src/console/ui/runs/index.ts +75 -0
  316. package/src/console/ui/runs/project.ts +122 -0
  317. package/src/console/ui/runs/query.test.ts +50 -0
  318. package/src/console/ui/runs/query.ts +283 -0
  319. package/src/console/ui/runs/search.test.ts +45 -0
  320. package/src/console/ui/runs/search.ts +141 -0
  321. package/src/console/ui/runs/trace-link.test.ts +30 -0
  322. package/src/console/ui/runs/trace-link.ts +99 -0
  323. package/src/console/ui/runs/types.ts +137 -0
  324. package/src/console/ui/shell/App.tsx +56 -0
  325. package/src/console/ui/shell/client.ts +1715 -0
  326. package/src/console/ui/shell/components/ui.tsx +110 -0
  327. package/src/console/ui/shell/index.html +12 -0
  328. package/src/console/ui/shell/layout/Shell.tsx +95 -0
  329. package/src/console/ui/shell/main.tsx +281 -0
  330. package/src/console/ui/shell/panels/Access.tsx +5 -0
  331. package/src/console/ui/shell/panels/Ai.tsx +5 -0
  332. package/src/console/ui/shell/panels/Architecture.tsx +5 -0
  333. package/src/console/ui/shell/panels/Channels.tsx +5 -0
  334. package/src/console/ui/shell/panels/Clock.tsx +5 -0
  335. package/src/console/ui/shell/panels/Diff.tsx +5 -0
  336. package/src/console/ui/shell/panels/Flows.tsx +5 -0
  337. package/src/console/ui/shell/panels/Gates.tsx +5 -0
  338. package/src/console/ui/shell/panels/Overview.tsx +6 -0
  339. package/src/console/ui/shell/panels/Plugins.tsx +5 -0
  340. package/src/console/ui/shell/panels/Runs.tsx +5 -0
  341. package/src/console/ui/shell/panels/Signals.tsx +5 -0
  342. package/src/console/ui/shell/panels/Store.tsx +5 -0
  343. package/src/console/ui/shell/panels/Traces.tsx +5 -0
  344. package/src/console/ui/shell/panels/Vault.tsx +5 -0
  345. package/src/console/ui/shell/panels/access/AccessPanel.tsx +813 -0
  346. package/src/console/ui/shell/panels/ai/AiPanel.tsx +563 -0
  347. package/src/console/ui/shell/panels/architecture/ArchitecturePanel.tsx +377 -0
  348. package/src/console/ui/shell/panels/channels/ChannelsPanel.tsx +454 -0
  349. package/src/console/ui/shell/panels/clock/ClockPanel.tsx +479 -0
  350. package/src/console/ui/shell/panels/diff/DiffPanel.tsx +275 -0
  351. package/src/console/ui/shell/panels/flows/ContractEditor.tsx +301 -0
  352. package/src/console/ui/shell/panels/flows/FlowDrawer.tsx +350 -0
  353. package/src/console/ui/shell/panels/flows/FlowsPanel.tsx +502 -0
  354. package/src/console/ui/shell/panels/gates/GatesPanel.tsx +482 -0
  355. package/src/console/ui/shell/panels/overview/OverviewPanel.tsx +428 -0
  356. package/src/console/ui/shell/panels/plugins/PluginsPanel.tsx +413 -0
  357. package/src/console/ui/shell/panels/runs/RunsPanel.tsx +649 -0
  358. package/src/console/ui/shell/panels/signals/SignalsPanel.tsx +840 -0
  359. package/src/console/ui/shell/panels/store/StorePanel.tsx +787 -0
  360. package/src/console/ui/shell/panels/traces/TracesPanel.tsx +539 -0
  361. package/src/console/ui/shell/panels/vault/VaultPanel.tsx +449 -0
  362. package/src/console/ui/shell/plugin/Sandbox.tsx +29 -0
  363. package/src/console/ui/shell/setup/Wizard.tsx +189 -0
  364. package/src/console/ui/shell/styles.css +75 -0
  365. package/src/console/ui/signals/SignalsA11yView.tsx +276 -0
  366. package/src/console/ui/signals/a11y.test.tsx +99 -0
  367. package/src/console/ui/signals/confirmation.test.ts +39 -0
  368. package/src/console/ui/signals/confirmation.ts +63 -0
  369. package/src/console/ui/signals/dry-run.test.ts +18 -0
  370. package/src/console/ui/signals/dry-run.ts +36 -0
  371. package/src/console/ui/signals/durable.test.ts +22 -0
  372. package/src/console/ui/signals/durable.ts +41 -0
  373. package/src/console/ui/signals/findings.ts +33 -0
  374. package/src/console/ui/signals/fixture.ts +242 -0
  375. package/src/console/ui/signals/group.test.ts +32 -0
  376. package/src/console/ui/signals/group.ts +49 -0
  377. package/src/console/ui/signals/index.ts +52 -0
  378. package/src/console/ui/signals/monitor.test.ts +30 -0
  379. package/src/console/ui/signals/monitor.ts +79 -0
  380. package/src/console/ui/signals/schema-form.test.ts +34 -0
  381. package/src/console/ui/signals/schema-form.ts +112 -0
  382. package/src/console/ui/signals/search.ts +102 -0
  383. package/src/console/ui/signals/types.ts +71 -0
  384. package/src/console/ui/store/StoreA11yView.tsx +199 -0
  385. package/src/console/ui/store/a11y.test.tsx +99 -0
  386. package/src/console/ui/store/cache-view.test.ts +13 -0
  387. package/src/console/ui/store/cache-view.ts +36 -0
  388. package/src/console/ui/store/confirmation.test.ts +38 -0
  389. package/src/console/ui/store/confirmation.ts +55 -0
  390. package/src/console/ui/store/dry-run.test.ts +15 -0
  391. package/src/console/ui/store/dry-run.ts +29 -0
  392. package/src/console/ui/store/fixture.ts +163 -0
  393. package/src/console/ui/store/group.test.ts +21 -0
  394. package/src/console/ui/store/group.ts +41 -0
  395. package/src/console/ui/store/index.ts +40 -0
  396. package/src/console/ui/store/search.ts +78 -0
  397. package/src/console/ui/store/types.ts +65 -0
  398. package/src/console/ui/store/will-not-fire.test.ts +19 -0
  399. package/src/console/ui/store/will-not-fire.ts +40 -0
  400. package/src/console/ui/traces/TracesA11yView.tsx +200 -0
  401. package/src/console/ui/traces/a11y.test.tsx +97 -0
  402. package/src/console/ui/traces/chain.test.ts +61 -0
  403. package/src/console/ui/traces/chain.ts +144 -0
  404. package/src/console/ui/traces/critical-path.test.ts +24 -0
  405. package/src/console/ui/traces/critical-path.ts +59 -0
  406. package/src/console/ui/traces/filter.test.ts +78 -0
  407. package/src/console/ui/traces/filter.ts +112 -0
  408. package/src/console/ui/traces/fixture.ts +126 -0
  409. package/src/console/ui/traces/fold.test.ts +67 -0
  410. package/src/console/ui/traces/fold.ts +170 -0
  411. package/src/console/ui/traces/index.ts +64 -0
  412. package/src/console/ui/traces/mini.ts +65 -0
  413. package/src/console/ui/traces/replay.test.ts +24 -0
  414. package/src/console/ui/traces/replay.ts +35 -0
  415. package/src/console/ui/traces/sampling.test.ts +26 -0
  416. package/src/console/ui/traces/sampling.ts +66 -0
  417. package/src/console/ui/traces/search.ts +148 -0
  418. package/src/console/ui/traces/tier.ts +72 -0
  419. package/src/console/ui/traces/types.ts +125 -0
  420. package/src/console/ui/tsconfig.json +20 -0
  421. package/src/console/ui/vault/VaultA11yView.tsx +177 -0
  422. package/src/console/ui/vault/a11y.test.tsx +97 -0
  423. package/src/console/ui/vault/blast-radius.test.ts +35 -0
  424. package/src/console/ui/vault/blast-radius.ts +55 -0
  425. package/src/console/ui/vault/confirmation.test.ts +41 -0
  426. package/src/console/ui/vault/confirmation.ts +39 -0
  427. package/src/console/ui/vault/dormant.test.ts +37 -0
  428. package/src/console/ui/vault/dormant.ts +28 -0
  429. package/src/console/ui/vault/export-safe.test.ts +30 -0
  430. package/src/console/ui/vault/export-safe.ts +65 -0
  431. package/src/console/ui/vault/fixture.ts +75 -0
  432. package/src/console/ui/vault/group.test.ts +25 -0
  433. package/src/console/ui/vault/group.ts +50 -0
  434. package/src/console/ui/vault/index.ts +45 -0
  435. package/src/console/ui/vault/search.ts +62 -0
  436. package/src/console/ui/vault/types.ts +57 -0
  437. package/src/console/ui/vite.config.ts +84 -0
  438. package/src/console/xss.gate.test.ts +118 -0
  439. package/src/docker/compose.ts +226 -0
  440. package/src/docker/credentials.ts +25 -0
  441. package/src/docker/derive.ts +83 -0
  442. package/src/docker/docker.test.ts +185 -0
  443. package/src/docker/dockerfile.integration.test.ts +170 -0
  444. package/src/docker/dockerfile.ts +39 -0
  445. package/src/docker/helpers.ts +103 -0
  446. package/src/docker/index.ts +52 -0
  447. package/src/docker/pin.ts +90 -0
  448. package/src/docker/recipes/index.ts +33 -0
  449. package/src/docker/recipes/postgres.ts +18 -0
  450. package/src/docker/recipes/redis.ts +18 -0
  451. package/src/docker/stack.integration.test.ts +140 -0
  452. package/src/docker/stack.ts +62 -0
  453. package/src/docker/types.ts +134 -0
  454. package/src/drivers/ai-anthropic.ts +128 -0
  455. package/src/drivers/ai-mock.ts +82 -0
  456. package/src/drivers/ai-openai-compatible.ts +139 -0
  457. package/src/drivers/ai-providers.test.ts +163 -0
  458. package/src/drivers/ai-types.ts +83 -0
  459. package/src/drivers/bun-native-completeness.test.ts +155 -0
  460. package/src/drivers/channel-console.ts +118 -0
  461. package/src/drivers/channel-fcm.ts +96 -0
  462. package/src/drivers/channel-resend.ts +27 -0
  463. package/src/drivers/channel-smtp.ts +33 -0
  464. package/src/drivers/channel-types.ts +160 -0
  465. package/src/drivers/channel-unifonic.ts +79 -0
  466. package/src/drivers/channel-wa-cloud.ts +84 -0
  467. package/src/drivers/channel-webpush.ts +317 -0
  468. package/src/drivers/conformance.test.ts +89 -0
  469. package/src/drivers/conformance.ts +162 -0
  470. package/src/drivers/fs.ts +73 -0
  471. package/src/drivers/index.ts +194 -0
  472. package/src/drivers/kv-lua.ts +222 -0
  473. package/src/drivers/memory.ts +472 -0
  474. package/src/drivers/pgvector.ts +130 -0
  475. package/src/drivers/postgres.ts +221 -0
  476. package/src/drivers/redis.ts +254 -0
  477. package/src/drivers/s3.ts +129 -0
  478. package/src/drivers/signal-engine.ts +690 -0
  479. package/src/drivers/signal-memory.ts +27 -0
  480. package/src/drivers/signal-nats.ts +131 -0
  481. package/src/drivers/signal-postgres.ts +1089 -0
  482. package/src/drivers/signal-redis.ts +275 -0
  483. package/src/drivers/signal-types.ts +328 -0
  484. package/src/drivers/sqlite.ts +53 -0
  485. package/src/drivers/types.ts +334 -0
  486. package/src/drivers/vault-dotenv-parse.ts +56 -0
  487. package/src/drivers/vault-env.ts +101 -0
  488. package/src/drivers/vault-infisical.ts +61 -0
  489. package/src/drivers/vault-managed.ts +42 -0
  490. package/src/drivers/vault-memory.ts +38 -0
  491. package/src/drivers/vault-openbao.ts +80 -0
  492. package/src/drivers/vault-sops.ts +252 -0
  493. package/src/drivers/vault-types.ts +84 -0
  494. package/src/elements/ai/declare.ts +206 -0
  495. package/src/elements/ai/eval.ts +113 -0
  496. package/src/elements/ai/pii.ts +108 -0
  497. package/src/elements/ai/runtime.ts +540 -0
  498. package/src/elements/ai/schema.ts +194 -0
  499. package/src/elements/ai.test.ts +393 -0
  500. package/src/elements/ai.ts +56 -0
  501. package/src/elements/channel/consent.ts +81 -0
  502. package/src/elements/channel/costs.test.ts +62 -0
  503. package/src/elements/channel/costs.ts +130 -0
  504. package/src/elements/channel/declare.ts +141 -0
  505. package/src/elements/channel/email-auth.test.ts +47 -0
  506. package/src/elements/channel/email-auth.ts +109 -0
  507. package/src/elements/channel/locale.ts +97 -0
  508. package/src/elements/channel/mask.test.ts +19 -0
  509. package/src/elements/channel/mask.ts +60 -0
  510. package/src/elements/channel/mime.ts +23 -0
  511. package/src/elements/channel/outcomes.test.ts +40 -0
  512. package/src/elements/channel/outcomes.ts +126 -0
  513. package/src/elements/channel/receipts.ts +121 -0
  514. package/src/elements/channel/runtime.ts +500 -0
  515. package/src/elements/channel/suppression.ts +136 -0
  516. package/src/elements/channel.test.ts +232 -0
  517. package/src/elements/channel.ts +109 -0
  518. package/src/elements/clock/actions.ts +170 -0
  519. package/src/elements/clock/declare.ts +58 -0
  520. package/src/elements/clock/dst.ts +197 -0
  521. package/src/elements/clock/durable.ts +140 -0
  522. package/src/elements/clock/duration.ts +29 -0
  523. package/src/elements/clock/health.test.ts +103 -0
  524. package/src/elements/clock/health.ts +112 -0
  525. package/src/elements/clock/leader.ts +127 -0
  526. package/src/elements/clock/reconcile.ts +241 -0
  527. package/src/elements/clock/runtime.ts +243 -0
  528. package/src/elements/clock/schedule.ts +262 -0
  529. package/src/elements/clock/time-travel.ts +53 -0
  530. package/src/elements/clock.test.ts +365 -0
  531. package/src/elements/clock.ts +80 -0
  532. package/src/elements/gate/declare.ts +134 -0
  533. package/src/elements/gate/permissions.ts +104 -0
  534. package/src/elements/gate/runtime.ts +177 -0
  535. package/src/elements/gate/strategies.ts +388 -0
  536. package/src/elements/gate.test.ts +145 -0
  537. package/src/elements/gate.ts +41 -0
  538. package/src/elements/index.ts +169 -0
  539. package/src/elements/signal/chaos-child.ts +47 -0
  540. package/src/elements/signal/declare.ts +77 -0
  541. package/src/elements/signal/dry-run-replay.test.ts +95 -0
  542. package/src/elements/signal/dry-run-write-isolation.test.ts +94 -0
  543. package/src/elements/signal/reconcile.test.ts +23 -0
  544. package/src/elements/signal/reconcile.ts +122 -0
  545. package/src/elements/signal/runtime.ts +94 -0
  546. package/src/elements/signal.test.ts +457 -0
  547. package/src/elements/signal.ts +26 -0
  548. package/src/elements/store/cache.ts +197 -0
  549. package/src/elements/store/classify.ts +125 -0
  550. package/src/elements/store/declare.ts +200 -0
  551. package/src/elements/store/files-policy.test.ts +31 -0
  552. package/src/elements/store/files-policy.ts +70 -0
  553. package/src/elements/store/replica.ts +68 -0
  554. package/src/elements/store/runtime.ts +393 -0
  555. package/src/elements/store/sql-condition.ts +205 -0
  556. package/src/elements/store/sql-session.ts +579 -0
  557. package/src/elements/store/table.ts +282 -0
  558. package/src/elements/store.test.ts +370 -0
  559. package/src/elements/store.ts +99 -0
  560. package/src/elements/vault/declare.ts +143 -0
  561. package/src/elements/vault/fingerprint.ts +30 -0
  562. package/src/elements/vault/redact.ts +52 -0
  563. package/src/elements/vault/runtime.ts +461 -0
  564. package/src/elements/vault.test.ts +296 -0
  565. package/src/elements/vault.ts +44 -0
  566. package/src/index.ts +183 -0
  567. package/src/kernel/adopt-routes.ts +194 -0
  568. package/src/kernel/app.ts +1068 -0
  569. package/src/kernel/auth-resolve.ts +93 -0
  570. package/src/kernel/boot.test.ts +173 -0
  571. package/src/kernel/boot.ts +439 -0
  572. package/src/kernel/budget-entry.ts +55 -0
  573. package/src/kernel/budget.test.ts +19 -0
  574. package/src/kernel/call.test.ts +83 -0
  575. package/src/kernel/capability.ts +113 -0
  576. package/src/kernel/dry-run.test.ts +64 -0
  577. package/src/kernel/dry-run.ts +171 -0
  578. package/src/kernel/effects.test.ts +89 -0
  579. package/src/kernel/effects.ts +140 -0
  580. package/src/kernel/errors.registry.test.ts +39 -0
  581. package/src/kernel/errors.ts +225 -0
  582. package/src/kernel/flow.test.ts +154 -0
  583. package/src/kernel/flow.ts +290 -0
  584. package/src/kernel/fx.test.ts +245 -0
  585. package/src/kernel/fx.ts +996 -0
  586. package/src/kernel/hook-timing.test.ts +72 -0
  587. package/src/kernel/hook-timing.ts +190 -0
  588. package/src/kernel/hooks.test.ts +181 -0
  589. package/src/kernel/hooks.ts +275 -0
  590. package/src/kernel/index.ts +278 -0
  591. package/src/kernel/journal.ts +405 -0
  592. package/src/kernel/journey.ts +75 -0
  593. package/src/kernel/on.ts +74 -0
  594. package/src/kernel/pipeline.test.ts +349 -0
  595. package/src/kernel/pipeline.ts +245 -0
  596. package/src/kernel/plug.ts +82 -0
  597. package/src/kernel/plugin/capabilities.test.ts +84 -0
  598. package/src/kernel/plugin/conflicts.test.ts +106 -0
  599. package/src/kernel/plugin/decorate.test.ts +111 -0
  600. package/src/kernel/plugin/scoping.test.ts +138 -0
  601. package/src/kernel/plugin.ts +485 -0
  602. package/src/kernel/rate-limit.ts +25 -0
  603. package/src/kernel/registry.ts +398 -0
  604. package/src/kernel/router.test.ts +114 -0
  605. package/src/kernel/router.ts +556 -0
  606. package/src/kernel/routing-budget.test.ts +19 -0
  607. package/src/kernel/run-telemetry.ts +70 -0
  608. package/src/kernel/triggers.ts +324 -0
  609. package/src/kernel/unit.ts +88 -0
  610. package/src/manifest/diff.test.ts +866 -0
  611. package/src/manifest/diff.ts +1670 -0
  612. package/src/manifest/fixtures/base.manifest.json +112 -0
  613. package/src/manifest/fixtures/skyport.excerpt.json +51 -0
  614. package/src/manifest/fixtures/skyport.manifest.json +153 -0
  615. package/src/manifest/index.ts +27 -0
  616. package/src/manifest/types.ts +358 -0
  617. package/src/manifest/undeclared.test.ts +82 -0
  618. package/src/manifest/undeclared.ts +65 -0
  619. package/src/manifest/validate.test.ts +105 -0
  620. package/src/manifest/validate.ts +174 -0
  621. package/src/mcp/authorization.ts +340 -0
  622. package/src/mcp/confirmation.ts +174 -0
  623. package/src/mcp/data.ts +90 -0
  624. package/src/mcp/index.ts +80 -0
  625. package/src/mcp/injection.gate.test.ts +176 -0
  626. package/src/mcp/mcp.test.ts +348 -0
  627. package/src/mcp/protocol.ts +172 -0
  628. package/src/mcp/server.ts +283 -0
  629. package/src/mcp/session.ts +139 -0
  630. package/src/mcp/tools.ts +391 -0
  631. package/src/plugins/catalogue.test.ts +48 -0
  632. package/src/plugins/catalogue.ts +113 -0
  633. package/src/plugins/node-import-scan.test.ts +28 -0
  634. package/src/plugins/node-import-scan.ts +98 -0
  635. package/src/plugins/supply-chain.test.ts +48 -0
  636. package/src/plugins/supply-chain.ts +460 -0
  637. package/src/release/index.ts +30 -0
  638. package/src/release/limits.ts +20 -0
  639. package/src/release/measure.ts +443 -0
  640. package/src/release/publish.ts +56 -0
  641. package/src/runs/bench.test.ts +93 -0
  642. package/src/runs/collect.ts +144 -0
  643. package/src/runs/drivers/clickhouse.ts +108 -0
  644. package/src/runs/drivers/files.ts +201 -0
  645. package/src/runs/drivers/memory.ts +94 -0
  646. package/src/runs/drivers/postgres.ts +110 -0
  647. package/src/runs/duckdb.ts +99 -0
  648. package/src/runs/index.ts +82 -0
  649. package/src/runs/outlier.ts +180 -0
  650. package/src/runs/parquet.ts +210 -0
  651. package/src/runs/privacy.ts +49 -0
  652. package/src/runs/runs.test.ts +324 -0
  653. package/src/runs/runtime.ts +229 -0
  654. package/src/runs/shred.ts +243 -0
  655. package/src/runs/telemetry.ts +10 -0
  656. package/src/runs/types.ts +179 -0
  657. package/src/runtime/bun.ts +140 -0
  658. package/src/runtime/cold-start.bench.ts +20 -0
  659. package/src/runtime/index.ts +46 -0
  660. package/src/runtime/primitives.ts +242 -0
  661. package/src/runtime/security.test.ts +170 -0
  662. package/src/runtime/security.ts +168 -0
  663. package/src/runtime/serve.test.ts +223 -0
  664. package/src/runtime/types.ts +217 -0
  665. package/src/runtime/web-standard.ts +77 -0
  666. package/src/test/create-test-app.test.ts +161 -0
  667. package/src/test/create-test-app.ts +533 -0
  668. package/src/test/index.ts +21 -0
  669. package/src/test/provisions.integration.test.ts +174 -0
  670. package/src/upgrade/codemods.test.ts +58 -0
  671. package/src/upgrade/codemods.ts +83 -0
  672. package/src/validation/index.ts +23 -0
  673. package/src/validation/standard-schema.test.ts +96 -0
  674. package/src/validation/standard-schema.ts +316 -0
@@ -0,0 +1,1670 @@
1
+ /**
2
+ * Pure Manifest Diff engine (Console §9.12).
3
+ *
4
+ * Classifies every behavioural change into exactly one of:
5
+ * contract-breaking · permission-widening · effect-widening · no-impact
6
+ *
7
+ * No I/O. Suitable for the Console panel and the CI gate.
8
+ */
9
+
10
+ import type {
11
+ Ai,
12
+ AiAgent,
13
+ AiPrompt,
14
+ Channel,
15
+ Clock,
16
+ DiffCategory,
17
+ Effects,
18
+ Flow,
19
+ FlowErrors,
20
+ Gate,
21
+ I18n,
22
+ Journey,
23
+ JsonSchema,
24
+ Manifest,
25
+ ManifestChange,
26
+ ManifestDiffResult,
27
+ Plugin,
28
+ SecretContract,
29
+ Signal,
30
+ Slo,
31
+ Store,
32
+ Table,
33
+ Tenancy,
34
+ Trigger,
35
+ } from "./types.ts";
36
+
37
+ const CATEGORY_RANK: Record<DiffCategory, number> = {
38
+ "contract-breaking": 3,
39
+ "permission-widening": 2,
40
+ "effect-widening": 1,
41
+ "no-impact": 0,
42
+ };
43
+
44
+ const EFFECT_KEYS = [
45
+ "reads",
46
+ "writes",
47
+ "emits",
48
+ "sends",
49
+ "asks",
50
+ "secrets",
51
+ "calls",
52
+ ] as const;
53
+
54
+ /** Isolation strength: higher = stronger tenant separation. */
55
+ const ISOLATION_RANK: Record<string, number> = {
56
+ row: 1,
57
+ schema: 2,
58
+ database: 3,
59
+ };
60
+
61
+ /**
62
+ * Diff two manifests and classify every behavioural change.
63
+ */
64
+ export function diffManifest(
65
+ before: Manifest,
66
+ after: Manifest,
67
+ ): ManifestDiffResult {
68
+ const changes: ManifestChange[] = [];
69
+
70
+ if (before.app !== after.app) {
71
+ changes.push(
72
+ change(
73
+ "/app",
74
+ "contract-breaking",
75
+ "changed",
76
+ before.app,
77
+ after.app,
78
+ `app identity changed: ${before.app} → ${after.app}`,
79
+ ),
80
+ );
81
+ }
82
+
83
+ if (before.oke !== after.oke) {
84
+ changes.push(
85
+ change(
86
+ "/oke",
87
+ "contract-breaking",
88
+ "changed",
89
+ before.oke,
90
+ after.oke,
91
+ `manifest schema version changed: ${before.oke} → ${after.oke}`,
92
+ ),
93
+ );
94
+ }
95
+
96
+ diffRecord(before.flows, after.flows, "/flows", diffFlow, changes);
97
+ diffRecord(before.signals, after.signals, "/signals", diffSignal, changes);
98
+ diffRecord(before.stores, after.stores, "/stores", diffStore, changes);
99
+ diffRecord(before.clocks, after.clocks, "/clocks", diffClock, changes);
100
+ diffRecord(
101
+ before.gates,
102
+ after.gates,
103
+ "/gates",
104
+ diffGateDef,
105
+ changes,
106
+ "permission-widening",
107
+ "no-impact",
108
+ );
109
+ diffRecord(before.vault, after.vault, "/vault", diffSecret, changes);
110
+ diffRecord(before.channels, after.channels, "/channels", diffChannel, changes);
111
+ diffRecord(before.plugins, after.plugins, "/plugins", diffPlugin, changes);
112
+ diffRecord(before.journeys, after.journeys, "/journeys", diffJourney, changes);
113
+ diffAi(before.ai, after.ai, changes);
114
+ diffDrivers(before.drivers, after.drivers, changes);
115
+ diffTenancy(before.tenancy, after.tenancy, changes);
116
+ diffI18n(before.i18n, after.i18n, changes);
117
+ diffTopology(before.topology, after.topology, changes);
118
+ diffImages(before.images, after.images, changes);
119
+
120
+ return { changes, severity: highestSeverity(changes) };
121
+ }
122
+
123
+ /**
124
+ * Highest blast-radius category among changes, or `null` if none.
125
+ */
126
+ export function highestSeverity(
127
+ changes: readonly ManifestChange[],
128
+ ): DiffCategory | null {
129
+ let best: DiffCategory | null = null;
130
+ for (const c of changes) {
131
+ if (best === null || CATEGORY_RANK[c.category] > CATEGORY_RANK[best]) {
132
+ best = c.category;
133
+ }
134
+ }
135
+ return best;
136
+ }
137
+
138
+ function change(
139
+ path: string,
140
+ category: DiffCategory,
141
+ kind: ManifestChange["kind"],
142
+ before: unknown,
143
+ after: unknown,
144
+ summary: string,
145
+ ): ManifestChange {
146
+ const entry: ManifestChange = { path, category, kind, summary };
147
+ if (before !== undefined) entry.before = before;
148
+ if (after !== undefined) entry.after = after;
149
+ return entry;
150
+ }
151
+
152
+ function diffRecord<T>(
153
+ before: Record<string, T> | undefined,
154
+ after: Record<string, T> | undefined,
155
+ base: string,
156
+ compare: (b: T, a: T, path: string, out: ManifestChange[]) => void,
157
+ out: ManifestChange[],
158
+ removedCategory: DiffCategory = "contract-breaking",
159
+ addedCategory: DiffCategory = "no-impact",
160
+ ): void {
161
+ const bKeys = new Set(Object.keys(before ?? {}));
162
+ const aKeys = new Set(Object.keys(after ?? {}));
163
+
164
+ for (const key of sorted(bKeys)) {
165
+ const path = `${base}/${escape(key)}`;
166
+ if (!aKeys.has(key)) {
167
+ out.push(
168
+ change(
169
+ path,
170
+ removedCategory,
171
+ "removed",
172
+ before![key],
173
+ undefined,
174
+ `removed ${path}`,
175
+ ),
176
+ );
177
+ continue;
178
+ }
179
+ compare(before![key]!, after![key]!, path, out);
180
+ }
181
+
182
+ for (const key of sorted(aKeys)) {
183
+ if (bKeys.has(key)) continue;
184
+ const path = `${base}/${escape(key)}`;
185
+ out.push(
186
+ change(
187
+ path,
188
+ addedCategory,
189
+ "added",
190
+ undefined,
191
+ after![key],
192
+ `added ${path}`,
193
+ ),
194
+ );
195
+ }
196
+ }
197
+
198
+ function diffFlow(
199
+ before: Flow,
200
+ after: Flow,
201
+ path: string,
202
+ out: ManifestChange[],
203
+ ): void {
204
+ diffTrigger(before.trigger, after.trigger, `${path}/trigger`, out);
205
+ diffGatesList(before.gates, after.gates, `${path}/gates`, out);
206
+ diffContractSchema(before.in, after.in, `${path}/in`, "in", out);
207
+ diffContractSchema(before.out, after.out, `${path}/out`, "out", out);
208
+ diffErrors(before.errors, after.errors, `${path}/errors`, out);
209
+ diffEffects(before.effects, after.effects, `${path}/effects`, out);
210
+ diffSlo(before.slo, after.slo, `${path}/slo`, out);
211
+
212
+ if (before.plane !== after.plane && (before.plane !== undefined || after.plane !== undefined)) {
213
+ let category: DiffCategory = "contract-breaking";
214
+ if (before.plane === "operator" && after.plane === "user") {
215
+ category = "permission-widening";
216
+ } else if (after.plane === undefined) {
217
+ category = "no-impact";
218
+ }
219
+ out.push(
220
+ change(
221
+ `${path}/plane`,
222
+ category,
223
+ kindOf(before.plane, after.plane),
224
+ before.plane,
225
+ after.plane,
226
+ `plane ${before.plane ?? "∅"} → ${after.plane ?? "∅"}`,
227
+ ),
228
+ );
229
+ }
230
+
231
+ if (before.durable !== after.durable && (before.durable !== undefined || after.durable !== undefined)) {
232
+ out.push(
233
+ change(
234
+ `${path}/durable`,
235
+ after.durable === true ? "effect-widening" : before.durable === true ? "contract-breaking" : "no-impact",
236
+ kindOf(before.durable, after.durable),
237
+ before.durable,
238
+ after.durable,
239
+ `durable ${String(before.durable)} → ${String(after.durable)}`,
240
+ ),
241
+ );
242
+ }
243
+
244
+ if (before.live !== after.live && (before.live !== undefined || after.live !== undefined)) {
245
+ out.push(
246
+ change(
247
+ `${path}/live`,
248
+ after.live === true ? "effect-widening" : "no-impact",
249
+ kindOf(before.live, after.live),
250
+ before.live,
251
+ after.live,
252
+ `live ${String(before.live)} → ${String(after.live)}`,
253
+ ),
254
+ );
255
+ }
256
+
257
+ if (!deepEqual(before.cache, after.cache)) {
258
+ out.push(
259
+ change(
260
+ `${path}/cache`,
261
+ "no-impact",
262
+ kindOf(before.cache, after.cache),
263
+ before.cache,
264
+ after.cache,
265
+ "cache policy changed",
266
+ ),
267
+ );
268
+ }
269
+
270
+ if (before.cacheKeys !== after.cacheKeys && (before.cacheKeys !== undefined || after.cacheKeys !== undefined)) {
271
+ out.push(
272
+ change(
273
+ `${path}/cacheKeys`,
274
+ "no-impact",
275
+ kindOf(before.cacheKeys, after.cacheKeys),
276
+ before.cacheKeys,
277
+ after.cacheKeys,
278
+ "cacheKeys changed",
279
+ ),
280
+ );
281
+ }
282
+
283
+ if (before.source !== after.source && (before.source !== undefined || after.source !== undefined)) {
284
+ out.push(
285
+ change(
286
+ `${path}/source`,
287
+ "no-impact",
288
+ kindOf(before.source, after.source),
289
+ before.source,
290
+ after.source,
291
+ "source location changed",
292
+ ),
293
+ );
294
+ }
295
+
296
+ if (!deepEqual(before.deprecated, after.deprecated)) {
297
+ out.push(
298
+ change(
299
+ `${path}/deprecated`,
300
+ "no-impact",
301
+ kindOf(before.deprecated, after.deprecated),
302
+ before.deprecated,
303
+ after.deprecated,
304
+ "deprecation marker changed",
305
+ ),
306
+ );
307
+ }
308
+
309
+ if (!setEqual(before.steps, after.steps)) {
310
+ const added = setDiff(after.steps, before.steps);
311
+ const removed = setDiff(before.steps, after.steps);
312
+ if (removed.length > 0) {
313
+ out.push(
314
+ change(
315
+ `${path}/steps`,
316
+ "contract-breaking",
317
+ "changed",
318
+ before.steps,
319
+ after.steps,
320
+ `removed durable steps: ${removed.join(", ")}`,
321
+ ),
322
+ );
323
+ } else if (added.length > 0) {
324
+ out.push(
325
+ change(
326
+ `${path}/steps`,
327
+ "effect-widening",
328
+ "changed",
329
+ before.steps,
330
+ after.steps,
331
+ `added durable steps: ${added.join(", ")}`,
332
+ ),
333
+ );
334
+ }
335
+ }
336
+
337
+ if (before.nondeterministic !== after.nondeterministic) {
338
+ if (after.nondeterministic === true) {
339
+ out.push(
340
+ change(
341
+ `${path}/nondeterministic`,
342
+ "effect-widening",
343
+ kindOf(before.nondeterministic, after.nondeterministic),
344
+ before.nondeterministic,
345
+ after.nondeterministic,
346
+ "flow became nondeterministic",
347
+ ),
348
+ );
349
+ } else if (before.nondeterministic === true) {
350
+ out.push(
351
+ change(
352
+ `${path}/nondeterministic`,
353
+ "no-impact",
354
+ kindOf(before.nondeterministic, after.nondeterministic),
355
+ before.nondeterministic,
356
+ after.nondeterministic,
357
+ "flow no longer marked nondeterministic",
358
+ ),
359
+ );
360
+ }
361
+ }
362
+
363
+ if (!deepEqual(before.cost, after.cost)) {
364
+ const beforeBudget = before.cost?.budget ?? before.cost?.estimatePerCall ?? 0;
365
+ const afterBudget = after.cost?.budget ?? after.cost?.estimatePerCall ?? 0;
366
+ out.push(
367
+ change(
368
+ `${path}/cost`,
369
+ afterBudget > beforeBudget ? "effect-widening" : "no-impact",
370
+ kindOf(before.cost, after.cost),
371
+ before.cost,
372
+ after.cost,
373
+ "cost declaration changed",
374
+ ),
375
+ );
376
+ }
377
+
378
+ if (before.pii !== after.pii && (before.pii !== undefined || after.pii !== undefined)) {
379
+ const category: DiffCategory =
380
+ after.pii === "allow" || (before.pii === "denied" && after.pii === "masked")
381
+ ? "permission-widening"
382
+ : "no-impact";
383
+ out.push(
384
+ change(
385
+ `${path}/pii`,
386
+ category,
387
+ kindOf(before.pii, after.pii),
388
+ before.pii,
389
+ after.pii,
390
+ `pii ${before.pii ?? "∅"} → ${after.pii ?? "∅"}`,
391
+ ),
392
+ );
393
+ }
394
+
395
+ if (before.breaking !== after.breaking) {
396
+ out.push(
397
+ change(
398
+ `${path}/breaking`,
399
+ "no-impact",
400
+ kindOf(before.breaking, after.breaking),
401
+ before.breaking,
402
+ after.breaking,
403
+ "breaking acknowledgement changed",
404
+ ),
405
+ );
406
+ }
407
+ }
408
+
409
+ function diffTrigger(
410
+ before: Trigger | undefined,
411
+ after: Trigger | undefined,
412
+ path: string,
413
+ out: ManifestChange[],
414
+ ): void {
415
+ if (deepEqual(before, after)) return;
416
+ if (before === undefined && after !== undefined) {
417
+ out.push(change(path, "contract-breaking", "added", before, after, "trigger added"));
418
+ return;
419
+ }
420
+ if (before !== undefined && after === undefined) {
421
+ out.push(change(path, "contract-breaking", "removed", before, after, "trigger removed"));
422
+ return;
423
+ }
424
+ out.push(
425
+ change(path, "contract-breaking", "changed", before, after, "trigger changed"),
426
+ );
427
+ }
428
+
429
+ function diffGatesList(
430
+ before: string[] | undefined,
431
+ after: string[] | undefined,
432
+ path: string,
433
+ out: ManifestChange[],
434
+ ): void {
435
+ const b = before ?? [];
436
+ const a = after ?? [];
437
+ if (setEqual(b, a) && listEqual(b, a)) return;
438
+ if (setEqual(b, a)) {
439
+ out.push(
440
+ change(path, "no-impact", "changed", b, a, "gates reordered"),
441
+ );
442
+ return;
443
+ }
444
+
445
+ const removed = setDiff(b, a);
446
+ const added = setDiff(a, b);
447
+
448
+ if (b.length > 0 && a.length === 0) {
449
+ out.push(
450
+ change(
451
+ path,
452
+ "permission-widening",
453
+ "changed",
454
+ b,
455
+ a,
456
+ "flow became public (all gates removed)",
457
+ ),
458
+ );
459
+ return;
460
+ }
461
+
462
+ for (const gate of removed) {
463
+ out.push(
464
+ change(
465
+ `${path}/${escape(gate)}`,
466
+ "permission-widening",
467
+ "removed",
468
+ gate,
469
+ undefined,
470
+ `gate removed: ${gate}`,
471
+ ),
472
+ );
473
+ }
474
+
475
+ for (const gate of added) {
476
+ out.push(
477
+ change(
478
+ `${path}/${escape(gate)}`,
479
+ "no-impact",
480
+ "added",
481
+ undefined,
482
+ gate,
483
+ `gate added: ${gate}`,
484
+ ),
485
+ );
486
+ }
487
+
488
+ // Detect rate-limit expression changes when both sides have a rate:* gate with same strategy.
489
+ const bRates = b.filter((g) => g.startsWith("rate:"));
490
+ const aRates = a.filter((g) => g.startsWith("rate:"));
491
+ for (const br of bRates) {
492
+ const bParsed = parseRateGate(br);
493
+ if (!bParsed) continue;
494
+ for (const ar of aRates) {
495
+ if (br === ar) continue;
496
+ const aParsed = parseRateGate(ar);
497
+ if (!aParsed) continue;
498
+ if (bParsed.strategy !== aParsed.strategy) continue;
499
+ if (removed.includes(br) && added.includes(ar) && aParsed.max > bParsed.max) {
500
+ out.push(
501
+ change(
502
+ path,
503
+ "permission-widening",
504
+ "changed",
505
+ br,
506
+ ar,
507
+ `rate limit widened: ${br} → ${ar}`,
508
+ ),
509
+ );
510
+ }
511
+ }
512
+ }
513
+ }
514
+
515
+ function diffContractSchema(
516
+ before: JsonSchema | undefined,
517
+ after: JsonSchema | undefined,
518
+ path: string,
519
+ role: "in" | "out",
520
+ out: ManifestChange[],
521
+ ): void {
522
+ if (deepEqual(before, after)) return;
523
+ if (before === undefined && after !== undefined) {
524
+ out.push(
525
+ change(
526
+ path,
527
+ role === "in" ? "contract-breaking" : "no-impact",
528
+ "added",
529
+ before,
530
+ after,
531
+ `${role} schema added`,
532
+ ),
533
+ );
534
+ return;
535
+ }
536
+ if (before !== undefined && after === undefined) {
537
+ out.push(
538
+ change(
539
+ path,
540
+ role === "out" ? "contract-breaking" : "no-impact",
541
+ "removed",
542
+ before,
543
+ after,
544
+ `${role} schema removed`,
545
+ ),
546
+ );
547
+ return;
548
+ }
549
+ if (typeof before === "string" || typeof after === "string") {
550
+ out.push(
551
+ change(path, "contract-breaking", "changed", before, after, `${role} schema ref changed`),
552
+ );
553
+ return;
554
+ }
555
+ if (schemaBreaksClients(before as Record<string, unknown>, after as Record<string, unknown>, role)) {
556
+ out.push(
557
+ change(path, "contract-breaking", "changed", before, after, `${role} schema contract broke`),
558
+ );
559
+ return;
560
+ }
561
+ out.push(
562
+ change(path, "no-impact", "changed", before, after, `${role} schema changed without client break`),
563
+ );
564
+ }
565
+
566
+ function diffErrors(
567
+ before: FlowErrors | undefined,
568
+ after: FlowErrors | undefined,
569
+ path: string,
570
+ out: ManifestChange[],
571
+ ): void {
572
+ const bNames = errorNames(before);
573
+ const aNames = errorNames(after);
574
+ if (setEqual(bNames, aNames) && deepEqual(before, after)) return;
575
+
576
+ for (const name of setDiff(bNames, aNames)) {
577
+ out.push(
578
+ change(
579
+ `${path}/${escape(name)}`,
580
+ "contract-breaking",
581
+ "removed",
582
+ name,
583
+ undefined,
584
+ `error removed: ${name}`,
585
+ ),
586
+ );
587
+ }
588
+ for (const name of setDiff(aNames, bNames)) {
589
+ out.push(
590
+ change(
591
+ `${path}/${escape(name)}`,
592
+ "no-impact",
593
+ "added",
594
+ undefined,
595
+ name,
596
+ `error added: ${name}`,
597
+ ),
598
+ );
599
+ }
600
+
601
+ if (
602
+ before &&
603
+ after &&
604
+ !Array.isArray(before) &&
605
+ !Array.isArray(after)
606
+ ) {
607
+ for (const name of intersection(bNames, aNames)) {
608
+ if (!deepEqual(before[name], after[name])) {
609
+ out.push(
610
+ change(
611
+ `${path}/${escape(name)}`,
612
+ "contract-breaking",
613
+ "changed",
614
+ before[name],
615
+ after[name],
616
+ `error schema changed: ${name}`,
617
+ ),
618
+ );
619
+ }
620
+ }
621
+ } else if (setEqual(bNames, aNames) && !deepEqual(before, after)) {
622
+ out.push(
623
+ change(path, "no-impact", "changed", before, after, "errors reordered"),
624
+ );
625
+ }
626
+ }
627
+
628
+ function diffEffects(
629
+ before: Effects | undefined,
630
+ after: Effects | undefined,
631
+ path: string,
632
+ out: ManifestChange[],
633
+ ): void {
634
+ for (const key of EFFECT_KEYS) {
635
+ const b = before?.[key] ?? [];
636
+ const a = after?.[key] ?? [];
637
+ if (setEqual(b, a)) {
638
+ if (!listEqual(b, a) && b.length > 0) {
639
+ out.push(
640
+ change(
641
+ `${path}/${key}`,
642
+ "no-impact",
643
+ "changed",
644
+ b,
645
+ a,
646
+ `${key} reordered`,
647
+ ),
648
+ );
649
+ }
650
+ continue;
651
+ }
652
+ for (const item of setDiff(a, b)) {
653
+ out.push(
654
+ change(
655
+ `${path}/${key}/${escape(item)}`,
656
+ "effect-widening",
657
+ "added",
658
+ undefined,
659
+ item,
660
+ `effect widened: ${key} += ${item}`,
661
+ ),
662
+ );
663
+ }
664
+ for (const item of setDiff(b, a)) {
665
+ out.push(
666
+ change(
667
+ `${path}/${key}/${escape(item)}`,
668
+ "no-impact",
669
+ "removed",
670
+ item,
671
+ undefined,
672
+ `effect narrowed: ${key} -= ${item}`,
673
+ ),
674
+ );
675
+ }
676
+ }
677
+ }
678
+
679
+ function diffSlo(
680
+ before: Slo | undefined,
681
+ after: Slo | undefined,
682
+ path: string,
683
+ out: ManifestChange[],
684
+ ): void {
685
+ if (deepEqual(before, after)) return;
686
+ if (before === undefined && after !== undefined) {
687
+ out.push(change(path, "no-impact", "added", before, after, "slo added"));
688
+ return;
689
+ }
690
+ if (before !== undefined && after === undefined) {
691
+ out.push(change(path, "contract-breaking", "removed", before, after, "slo removed"));
692
+ return;
693
+ }
694
+ const bAvail = parsePercent(before?.availability);
695
+ const aAvail = parsePercent(after?.availability);
696
+ if (bAvail !== undefined && aAvail !== undefined && aAvail < bAvail) {
697
+ out.push(
698
+ change(
699
+ `${path}/availability`,
700
+ "contract-breaking",
701
+ "changed",
702
+ before?.availability,
703
+ after?.availability,
704
+ `availability target lowered: ${before?.availability} → ${after?.availability}`,
705
+ ),
706
+ );
707
+ } else if (before?.availability !== after?.availability) {
708
+ out.push(
709
+ change(
710
+ `${path}/availability`,
711
+ "no-impact",
712
+ "changed",
713
+ before?.availability,
714
+ after?.availability,
715
+ "availability target changed",
716
+ ),
717
+ );
718
+ }
719
+
720
+ if (!deepEqual(before?.latency, after?.latency)) {
721
+ const loosened = latencyLoosened(before?.latency, after?.latency);
722
+ out.push(
723
+ change(
724
+ `${path}/latency`,
725
+ loosened ? "contract-breaking" : "no-impact",
726
+ "changed",
727
+ before?.latency,
728
+ after?.latency,
729
+ "latency objective changed",
730
+ ),
731
+ );
732
+ }
733
+ }
734
+
735
+ function diffSignal(
736
+ before: Signal,
737
+ after: Signal,
738
+ path: string,
739
+ out: ManifestChange[],
740
+ ): void {
741
+ if (before.delivery !== after.delivery) {
742
+ out.push(
743
+ change(
744
+ `${path}/delivery`,
745
+ "contract-breaking",
746
+ "changed",
747
+ before.delivery,
748
+ after.delivery,
749
+ `delivery changed: ${before.delivery} → ${after.delivery}`,
750
+ ),
751
+ );
752
+ }
753
+ if (before.retries !== after.retries) {
754
+ const b = before.retries ?? 0;
755
+ const a = after.retries ?? 0;
756
+ out.push(
757
+ change(
758
+ `${path}/retries`,
759
+ a < b ? "contract-breaking" : "no-impact",
760
+ kindOf(before.retries, after.retries),
761
+ before.retries,
762
+ after.retries,
763
+ `retries ${b} → ${a}`,
764
+ ),
765
+ );
766
+ }
767
+ if (before.deadLetter !== after.deadLetter) {
768
+ out.push(
769
+ change(
770
+ `${path}/deadLetter`,
771
+ before.deadLetter === true && after.deadLetter === false
772
+ ? "contract-breaking"
773
+ : "no-impact",
774
+ kindOf(before.deadLetter, after.deadLetter),
775
+ before.deadLetter,
776
+ after.deadLetter,
777
+ `deadLetter ${String(before.deadLetter)} → ${String(after.deadLetter)}`,
778
+ ),
779
+ );
780
+ }
781
+ if (!deepEqual(before.schema, after.schema)) {
782
+ diffContractSchema(before.schema, after.schema, `${path}/schema`, "in", out);
783
+ }
784
+ if (before.optional !== after.optional) {
785
+ out.push(
786
+ change(
787
+ `${path}/optional`,
788
+ before.optional !== true && after.optional === true
789
+ ? "no-impact"
790
+ : after.optional === false
791
+ ? "contract-breaking"
792
+ : "no-impact",
793
+ kindOf(before.optional, after.optional),
794
+ before.optional,
795
+ after.optional,
796
+ "optional flag changed",
797
+ ),
798
+ );
799
+ }
800
+ }
801
+
802
+ function diffStore(
803
+ before: Store,
804
+ after: Store,
805
+ path: string,
806
+ out: ManifestChange[],
807
+ ): void {
808
+ if (before.facet !== after.facet) {
809
+ out.push(
810
+ change(
811
+ `${path}/facet`,
812
+ "contract-breaking",
813
+ "changed",
814
+ before.facet,
815
+ after.facet,
816
+ `store facet changed: ${before.facet} → ${after.facet}`,
817
+ ),
818
+ );
819
+ }
820
+ diffRecord(before.tables, after.tables, `${path}/tables`, diffTable, out);
821
+ diffStringSet(before.namespaces, after.namespaces, `${path}/namespaces`, out);
822
+ diffStringSet(before.buckets, after.buckets, `${path}/buckets`, out);
823
+ diffStringSet(before.indexes, after.indexes, `${path}/indexes`, out);
824
+ if (!deepEqual(before.classifications, after.classifications)) {
825
+ out.push(
826
+ change(
827
+ `${path}/classifications`,
828
+ classificationWeakened(before.classifications, after.classifications)
829
+ ? "permission-widening"
830
+ : "no-impact",
831
+ "changed",
832
+ before.classifications,
833
+ after.classifications,
834
+ "store classifications changed",
835
+ ),
836
+ );
837
+ }
838
+ }
839
+
840
+ function diffTable(
841
+ before: Table,
842
+ after: Table,
843
+ path: string,
844
+ out: ManifestChange[],
845
+ ): void {
846
+ if (!deepEqual(before.columns, after.columns)) {
847
+ out.push(
848
+ change(
849
+ `${path}/columns`,
850
+ classificationWeakened(before.columns, after.columns)
851
+ ? "permission-widening"
852
+ : "no-impact",
853
+ "changed",
854
+ before.columns,
855
+ after.columns,
856
+ "table columns/classifications changed",
857
+ ),
858
+ );
859
+ }
860
+ if (!deepEqual(before.classifications, after.classifications)) {
861
+ out.push(
862
+ change(
863
+ `${path}/classifications`,
864
+ classificationWeakened(before.classifications, after.classifications)
865
+ ? "permission-widening"
866
+ : "no-impact",
867
+ "changed",
868
+ before.classifications,
869
+ after.classifications,
870
+ "table classifications changed",
871
+ ),
872
+ );
873
+ }
874
+ }
875
+
876
+ function diffClock(
877
+ before: Clock,
878
+ after: Clock,
879
+ path: string,
880
+ out: ManifestChange[],
881
+ ): void {
882
+ if (deepEqual(before, after)) return;
883
+ const scheduleChanged =
884
+ before.cron !== after.cron || before.every !== after.every;
885
+ out.push(
886
+ change(
887
+ path,
888
+ scheduleChanged ? "contract-breaking" : "no-impact",
889
+ "changed",
890
+ before,
891
+ after,
892
+ scheduleChanged ? "clock schedule changed" : "clock metadata changed",
893
+ ),
894
+ );
895
+ }
896
+
897
+ function diffGateDef(
898
+ before: Gate,
899
+ after: Gate,
900
+ path: string,
901
+ out: ManifestChange[],
902
+ ): void {
903
+ if (deepEqual(before, after)) return;
904
+
905
+ const rolesRemoved = setDiff(before.roles, after.roles);
906
+ const scopesRemoved = setDiff(before.scopes, after.scopes);
907
+ const maxWidened =
908
+ before.max !== undefined &&
909
+ after.max !== undefined &&
910
+ after.max > before.max;
911
+
912
+ // Widened membership (e.g. roles: [staff] → [staff, member] or roles replaced by broader set)
913
+ const rolesAdded = setDiff(after.roles, before.roles);
914
+ const scopesAdded = setDiff(after.scopes, before.scopes);
915
+
916
+ if (maxWidened || rolesAdded.length > 0 || scopesAdded.length > 0) {
917
+ out.push(
918
+ change(
919
+ path,
920
+ "permission-widening",
921
+ "changed",
922
+ before,
923
+ after,
924
+ maxWidened
925
+ ? `rate max widened: ${before.max} → ${after.max}`
926
+ : `gate widened: +${[...rolesAdded, ...scopesAdded].join(", ")}`,
927
+ ),
928
+ );
929
+ return;
930
+ }
931
+
932
+ if (rolesRemoved.length > 0 || scopesRemoved.length > 0) {
933
+ out.push(
934
+ change(
935
+ path,
936
+ "no-impact",
937
+ "changed",
938
+ before,
939
+ after,
940
+ "gate narrowed",
941
+ ),
942
+ );
943
+ return;
944
+ }
945
+
946
+ if (before.strategy !== after.strategy && before.strategy && after.strategy) {
947
+ out.push(
948
+ change(
949
+ `${path}/strategy`,
950
+ "contract-breaking",
951
+ "changed",
952
+ before.strategy,
953
+ after.strategy,
954
+ `rate strategy changed: ${before.strategy} → ${after.strategy}`,
955
+ ),
956
+ );
957
+ return;
958
+ }
959
+
960
+ out.push(
961
+ change(path, "no-impact", "changed", before, after, "gate definition changed"),
962
+ );
963
+ }
964
+
965
+ function diffSecret(
966
+ before: SecretContract,
967
+ after: SecretContract,
968
+ path: string,
969
+ out: ManifestChange[],
970
+ ): void {
971
+ if (deepEqual(before, after)) return;
972
+ if (!deepEqual(before.schema, after.schema)) {
973
+ out.push(
974
+ change(
975
+ `${path}/schema`,
976
+ "contract-breaking",
977
+ "changed",
978
+ before.schema,
979
+ after.schema,
980
+ "secret contract schema changed",
981
+ ),
982
+ );
983
+ return;
984
+ }
985
+ out.push(
986
+ change(path, "no-impact", "changed", before, after, "secret contract metadata changed"),
987
+ );
988
+ }
989
+
990
+ function diffChannel(
991
+ before: Channel,
992
+ after: Channel,
993
+ path: string,
994
+ out: ManifestChange[],
995
+ ): void {
996
+ if (deepEqual(before, after)) return;
997
+ if (before.medium !== after.medium && before.medium && after.medium) {
998
+ out.push(
999
+ change(
1000
+ `${path}/medium`,
1001
+ "contract-breaking",
1002
+ "changed",
1003
+ before.medium,
1004
+ after.medium,
1005
+ `channel medium changed: ${before.medium} → ${after.medium}`,
1006
+ ),
1007
+ );
1008
+ }
1009
+ const localesRemoved = setDiff(before.locales, after.locales);
1010
+ const localesAdded = setDiff(after.locales, before.locales);
1011
+ for (const locale of localesRemoved) {
1012
+ out.push(
1013
+ change(
1014
+ `${path}/locales/${escape(locale)}`,
1015
+ "contract-breaking",
1016
+ "removed",
1017
+ locale,
1018
+ undefined,
1019
+ `locale removed: ${locale}`,
1020
+ ),
1021
+ );
1022
+ }
1023
+ for (const locale of localesAdded) {
1024
+ out.push(
1025
+ change(
1026
+ `${path}/locales/${escape(locale)}`,
1027
+ "no-impact",
1028
+ "added",
1029
+ undefined,
1030
+ locale,
1031
+ `locale added: ${locale}`,
1032
+ ),
1033
+ );
1034
+ }
1035
+ if (
1036
+ localesRemoved.length === 0 &&
1037
+ localesAdded.length === 0 &&
1038
+ before.medium === after.medium &&
1039
+ !deepEqual(before, after)
1040
+ ) {
1041
+ out.push(
1042
+ change(path, "no-impact", "changed", before, after, "channel metadata changed"),
1043
+ );
1044
+ }
1045
+ }
1046
+
1047
+ function diffPlugin(
1048
+ before: Plugin,
1049
+ after: Plugin,
1050
+ path: string,
1051
+ out: ManifestChange[],
1052
+ ): void {
1053
+ if (deepEqual(before, after)) return;
1054
+ const interceptsAdded = setDiff(after.intercepts, before.intercepts);
1055
+ const declaresAdded = setDiff(after.declares, before.declares);
1056
+ if (interceptsAdded.length > 0 || declaresAdded.length > 0) {
1057
+ out.push(
1058
+ change(
1059
+ path,
1060
+ "permission-widening",
1061
+ "changed",
1062
+ before,
1063
+ after,
1064
+ `plugin capabilities widened: ${[...interceptsAdded, ...declaresAdded].join(", ")}`,
1065
+ ),
1066
+ );
1067
+ return;
1068
+ }
1069
+ out.push(
1070
+ change(path, "no-impact", "changed", before, after, "plugin metadata changed"),
1071
+ );
1072
+ }
1073
+
1074
+ function diffJourney(
1075
+ before: Journey,
1076
+ after: Journey,
1077
+ path: string,
1078
+ out: ManifestChange[],
1079
+ ): void {
1080
+ if (deepEqual(before, after)) return;
1081
+ diffSlo(before.slo, after.slo, `${path}/slo`, out);
1082
+ if (before.composes !== after.composes) {
1083
+ const b = parsePercent(before.composes);
1084
+ const a = parsePercent(after.composes);
1085
+ out.push(
1086
+ change(
1087
+ `${path}/composes`,
1088
+ b !== undefined && a !== undefined && a < b
1089
+ ? "contract-breaking"
1090
+ : "no-impact",
1091
+ kindOf(before.composes, after.composes),
1092
+ before.composes,
1093
+ after.composes,
1094
+ "journey composition changed",
1095
+ ),
1096
+ );
1097
+ }
1098
+ if (!setEqual(before.flows, after.flows)) {
1099
+ out.push(
1100
+ change(
1101
+ `${path}/flows`,
1102
+ setDiff(before.flows, after.flows).length > 0
1103
+ ? "contract-breaking"
1104
+ : "no-impact",
1105
+ "changed",
1106
+ before.flows,
1107
+ after.flows,
1108
+ "journey member flows changed",
1109
+ ),
1110
+ );
1111
+ }
1112
+ }
1113
+
1114
+ function diffAi(
1115
+ before: Ai | undefined,
1116
+ after: Ai | undefined,
1117
+ out: ManifestChange[],
1118
+ ): void {
1119
+ diffRecord(before?.models, after?.models, "/ai/models", (b, a, path, o) => {
1120
+ if (!deepEqual(b, a)) {
1121
+ o.push(change(path, "contract-breaking", "changed", b, a, "ai model changed"));
1122
+ }
1123
+ }, out);
1124
+ diffRecord(before?.prompts, after?.prompts, "/ai/prompts", diffPrompt, out);
1125
+ diffRecord(before?.agents, after?.agents, "/ai/agents", diffAgent, out);
1126
+ }
1127
+
1128
+ function diffPrompt(
1129
+ before: AiPrompt,
1130
+ after: AiPrompt,
1131
+ path: string,
1132
+ out: ManifestChange[],
1133
+ ): void {
1134
+ if (deepEqual(before, after)) return;
1135
+ if (before.version !== after.version) {
1136
+ out.push(
1137
+ change(
1138
+ `${path}/version`,
1139
+ "effect-widening",
1140
+ "changed",
1141
+ before.version,
1142
+ after.version,
1143
+ `prompt version ${before.version} → ${after.version}`,
1144
+ ),
1145
+ );
1146
+ }
1147
+ const bCost = before.budget?.maxCostPerCall ?? before.budget?.maxCostPerRun ?? 0;
1148
+ const aCost = after.budget?.maxCostPerCall ?? after.budget?.maxCostPerRun ?? 0;
1149
+ if (aCost > bCost) {
1150
+ out.push(
1151
+ change(
1152
+ `${path}/budget`,
1153
+ "effect-widening",
1154
+ "changed",
1155
+ before.budget,
1156
+ after.budget,
1157
+ "prompt budget increased",
1158
+ ),
1159
+ );
1160
+ } else if (!deepEqual(before.budget, after.budget) && before.version === after.version) {
1161
+ out.push(
1162
+ change(
1163
+ `${path}/budget`,
1164
+ "no-impact",
1165
+ "changed",
1166
+ before.budget,
1167
+ after.budget,
1168
+ "prompt budget changed",
1169
+ ),
1170
+ );
1171
+ }
1172
+ if (before.evals !== after.evals && before.version === after.version) {
1173
+ out.push(
1174
+ change(
1175
+ `${path}/evals`,
1176
+ "no-impact",
1177
+ kindOf(before.evals, after.evals),
1178
+ before.evals,
1179
+ after.evals,
1180
+ "prompt evals path changed",
1181
+ ),
1182
+ );
1183
+ }
1184
+ if (!deepEqual(before.in, after.in) || !deepEqual(before.out, after.out)) {
1185
+ if (!deepEqual(before.in, after.in)) {
1186
+ diffContractSchema(before.in, after.in, `${path}/in`, "in", out);
1187
+ }
1188
+ if (!deepEqual(before.out, after.out)) {
1189
+ diffContractSchema(before.out, after.out, `${path}/out`, "out", out);
1190
+ }
1191
+ }
1192
+ }
1193
+
1194
+ function diffAgent(
1195
+ before: AiAgent,
1196
+ after: AiAgent,
1197
+ path: string,
1198
+ out: ManifestChange[],
1199
+ ): void {
1200
+ if (deepEqual(before, after)) return;
1201
+ const toolsAdded = setDiff(after.tools, before.tools);
1202
+ const toolsRemoved = setDiff(before.tools, after.tools);
1203
+ for (const tool of toolsAdded) {
1204
+ out.push(
1205
+ change(
1206
+ `${path}/tools/${escape(tool)}`,
1207
+ "effect-widening",
1208
+ "added",
1209
+ undefined,
1210
+ tool,
1211
+ `agent tool added: ${tool}`,
1212
+ ),
1213
+ );
1214
+ }
1215
+ for (const tool of toolsRemoved) {
1216
+ out.push(
1217
+ change(
1218
+ `${path}/tools/${escape(tool)}`,
1219
+ "contract-breaking",
1220
+ "removed",
1221
+ tool,
1222
+ undefined,
1223
+ `agent tool removed: ${tool}`,
1224
+ ),
1225
+ );
1226
+ }
1227
+ if (before.maxSteps !== after.maxSteps) {
1228
+ const b = before.maxSteps ?? 0;
1229
+ const a = after.maxSteps ?? 0;
1230
+ out.push(
1231
+ change(
1232
+ `${path}/maxSteps`,
1233
+ a > b ? "effect-widening" : "no-impact",
1234
+ kindOf(before.maxSteps, after.maxSteps),
1235
+ before.maxSteps,
1236
+ after.maxSteps,
1237
+ `agent maxSteps ${b} → ${a}`,
1238
+ ),
1239
+ );
1240
+ }
1241
+ if (!deepEqual(before.budget, after.budget) && toolsAdded.length === 0 && toolsRemoved.length === 0) {
1242
+ const bCost = before.budget?.maxCostPerRun ?? before.budget?.maxCostPerCall ?? 0;
1243
+ const aCost = after.budget?.maxCostPerRun ?? after.budget?.maxCostPerCall ?? 0;
1244
+ out.push(
1245
+ change(
1246
+ `${path}/budget`,
1247
+ aCost > bCost ? "effect-widening" : "no-impact",
1248
+ "changed",
1249
+ before.budget,
1250
+ after.budget,
1251
+ "agent budget changed",
1252
+ ),
1253
+ );
1254
+ }
1255
+ }
1256
+
1257
+ function diffDrivers(
1258
+ before: Record<string, string[]> | undefined,
1259
+ after: Record<string, string[]> | undefined,
1260
+ out: ManifestChange[],
1261
+ ): void {
1262
+ const envs = new Set([...Object.keys(before ?? {}), ...Object.keys(after ?? {})]);
1263
+ for (const env of sorted(envs)) {
1264
+ const path = `/drivers/${escape(env)}`;
1265
+ const b = before?.[env];
1266
+ const a = after?.[env];
1267
+ if (b === undefined && a !== undefined) {
1268
+ out.push(change(path, "no-impact", "added", b, a, `drivers for ${env} added`));
1269
+ continue;
1270
+ }
1271
+ if (b !== undefined && a === undefined) {
1272
+ out.push(change(path, "contract-breaking", "removed", b, a, `drivers for ${env} removed`));
1273
+ continue;
1274
+ }
1275
+ if (b && a && !setEqual(b, a)) {
1276
+ const removed = setDiff(b, a);
1277
+ const added = setDiff(a, b);
1278
+ if (removed.length > 0) {
1279
+ out.push(
1280
+ change(
1281
+ path,
1282
+ "contract-breaking",
1283
+ "changed",
1284
+ b,
1285
+ a,
1286
+ `drivers removed from ${env}: ${removed.join(", ")}`,
1287
+ ),
1288
+ );
1289
+ }
1290
+ if (added.length > 0) {
1291
+ out.push(
1292
+ change(
1293
+ path,
1294
+ "no-impact",
1295
+ "changed",
1296
+ b,
1297
+ a,
1298
+ `drivers added to ${env}: ${added.join(", ")}`,
1299
+ ),
1300
+ );
1301
+ }
1302
+ } else if (b && a && !listEqual(b, a)) {
1303
+ out.push(change(path, "no-impact", "changed", b, a, `drivers reordered for ${env}`));
1304
+ }
1305
+ }
1306
+ }
1307
+
1308
+ function diffTenancy(
1309
+ before: Tenancy | undefined,
1310
+ after: Tenancy | undefined,
1311
+ out: ManifestChange[],
1312
+ ): void {
1313
+ if (deepEqual(before, after)) return;
1314
+ if (before === undefined && after !== undefined) {
1315
+ out.push(change("/tenancy", "no-impact", "added", before, after, "tenancy enabled"));
1316
+ return;
1317
+ }
1318
+ if (before !== undefined && after === undefined) {
1319
+ out.push(
1320
+ change("/tenancy", "permission-widening", "removed", before, after, "tenancy disabled"),
1321
+ );
1322
+ return;
1323
+ }
1324
+ const b = before?.isolation ? ISOLATION_RANK[before.isolation] : undefined;
1325
+ const a = after?.isolation ? ISOLATION_RANK[after.isolation] : undefined;
1326
+ out.push(
1327
+ change(
1328
+ "/tenancy/isolation",
1329
+ b !== undefined && a !== undefined && a < b
1330
+ ? "permission-widening"
1331
+ : "no-impact",
1332
+ "changed",
1333
+ before?.isolation,
1334
+ after?.isolation,
1335
+ `tenancy isolation ${before?.isolation} → ${after?.isolation}`,
1336
+ ),
1337
+ );
1338
+ }
1339
+
1340
+ function diffI18n(
1341
+ before: I18n | undefined,
1342
+ after: I18n | undefined,
1343
+ out: ManifestChange[],
1344
+ ): void {
1345
+ if (deepEqual(before, after)) return;
1346
+ if (before === undefined && after !== undefined) {
1347
+ out.push(change("/i18n", "no-impact", "added", before, after, "i18n added"));
1348
+ return;
1349
+ }
1350
+ if (before !== undefined && after === undefined) {
1351
+ out.push(change("/i18n", "contract-breaking", "removed", before, after, "i18n removed"));
1352
+ return;
1353
+ }
1354
+ for (const locale of setDiff(before?.locales, after?.locales)) {
1355
+ out.push(
1356
+ change(
1357
+ `/i18n/locales/${escape(locale)}`,
1358
+ "contract-breaking",
1359
+ "removed",
1360
+ locale,
1361
+ undefined,
1362
+ `locale removed: ${locale}`,
1363
+ ),
1364
+ );
1365
+ }
1366
+ for (const locale of setDiff(after?.locales, before?.locales)) {
1367
+ out.push(
1368
+ change(
1369
+ `/i18n/locales/${escape(locale)}`,
1370
+ "no-impact",
1371
+ "added",
1372
+ undefined,
1373
+ locale,
1374
+ `locale added: ${locale}`,
1375
+ ),
1376
+ );
1377
+ }
1378
+ if (before?.default !== after?.default) {
1379
+ out.push(
1380
+ change(
1381
+ "/i18n/default",
1382
+ "no-impact",
1383
+ "changed",
1384
+ before?.default,
1385
+ after?.default,
1386
+ "default locale changed",
1387
+ ),
1388
+ );
1389
+ }
1390
+ if (!deepEqual(before?.dir, after?.dir) && setEqual(before?.locales, after?.locales)) {
1391
+ out.push(
1392
+ change("/i18n/dir", "no-impact", "changed", before?.dir, after?.dir, "locale dir map changed"),
1393
+ );
1394
+ }
1395
+ }
1396
+
1397
+ function diffTopology(
1398
+ before: Manifest["topology"],
1399
+ after: Manifest["topology"],
1400
+ out: ManifestChange[],
1401
+ ): void {
1402
+ if (before === after) return;
1403
+ const category: DiffCategory =
1404
+ before !== undefined && after !== undefined
1405
+ ? "contract-breaking"
1406
+ : "no-impact";
1407
+ out.push(
1408
+ change(
1409
+ "/topology",
1410
+ category,
1411
+ kindOf(before, after),
1412
+ before,
1413
+ after,
1414
+ `topology ${before ?? "∅"} → ${after ?? "∅"}`,
1415
+ ),
1416
+ );
1417
+ }
1418
+
1419
+ function diffImages(
1420
+ before: Record<string, string> | undefined,
1421
+ after: Record<string, string> | undefined,
1422
+ out: ManifestChange[],
1423
+ ): void {
1424
+ if (deepEqual(before, after)) return;
1425
+ out.push(
1426
+ change("/images", "no-impact", "changed", before, after, "images changed"),
1427
+ );
1428
+ }
1429
+
1430
+ function diffStringSet(
1431
+ before: string[] | undefined,
1432
+ after: string[] | undefined,
1433
+ path: string,
1434
+ out: ManifestChange[],
1435
+ ): void {
1436
+ if (setEqual(before, after)) {
1437
+ if (before && after && !listEqual(before, after)) {
1438
+ out.push(change(path, "no-impact", "changed", before, after, `${path} reordered`));
1439
+ }
1440
+ return;
1441
+ }
1442
+ for (const item of setDiff(before, after)) {
1443
+ out.push(
1444
+ change(
1445
+ `${path}/${escape(item)}`,
1446
+ "contract-breaking",
1447
+ "removed",
1448
+ item,
1449
+ undefined,
1450
+ `removed ${item}`,
1451
+ ),
1452
+ );
1453
+ }
1454
+ for (const item of setDiff(after, before)) {
1455
+ out.push(
1456
+ change(
1457
+ `${path}/${escape(item)}`,
1458
+ "no-impact",
1459
+ "added",
1460
+ undefined,
1461
+ item,
1462
+ `added ${item}`,
1463
+ ),
1464
+ );
1465
+ }
1466
+ }
1467
+
1468
+ // ── helpers ──────────────────────────────────────────────────────────
1469
+
1470
+ function kindOf(before: unknown, after: unknown): ManifestChange["kind"] {
1471
+ if (before === undefined) return "added";
1472
+ if (after === undefined) return "removed";
1473
+ return "changed";
1474
+ }
1475
+
1476
+ function errorNames(errors: FlowErrors | undefined): string[] {
1477
+ if (!errors) return [];
1478
+ if (Array.isArray(errors)) return errors;
1479
+ return Object.keys(errors);
1480
+ }
1481
+
1482
+ function schemaBreaksClients(
1483
+ before: Record<string, unknown>,
1484
+ after: Record<string, unknown>,
1485
+ role: "in" | "out",
1486
+ ): boolean {
1487
+ if (before.type !== undefined && after.type !== undefined && before.type !== after.type) {
1488
+ return true;
1489
+ }
1490
+
1491
+ const bProps = (before.properties ?? {}) as Record<string, unknown>;
1492
+ const aProps = (after.properties ?? {}) as Record<string, unknown>;
1493
+ const bReq = new Set(asStringArray(before.required));
1494
+ const aReq = new Set(asStringArray(after.required));
1495
+
1496
+ if (role === "in") {
1497
+ for (const key of aReq) {
1498
+ if (!bReq.has(key)) return true;
1499
+ }
1500
+ for (const key of Object.keys(bProps)) {
1501
+ if (!(key in aProps)) continue;
1502
+ if (!deepEqual(bProps[key], aProps[key])) {
1503
+ const bt = (bProps[key] as { type?: unknown } | undefined)?.type;
1504
+ const at = (aProps[key] as { type?: unknown } | undefined)?.type;
1505
+ if (bt !== undefined && at !== undefined && bt !== at) return true;
1506
+ const bEnum = asUnknownArray((bProps[key] as { enum?: unknown })?.enum);
1507
+ const aEnum = asUnknownArray((aProps[key] as { enum?: unknown })?.enum);
1508
+ if (bEnum && aEnum && setDiff(bEnum.map(String), aEnum.map(String)).length > 0) {
1509
+ return true;
1510
+ }
1511
+ }
1512
+ }
1513
+ return false;
1514
+ }
1515
+
1516
+ // out: removing a property or changing its type breaks clients
1517
+ for (const key of Object.keys(bProps)) {
1518
+ if (!(key in aProps)) return true;
1519
+ const bt = (bProps[key] as { type?: unknown } | undefined)?.type;
1520
+ const at = (aProps[key] as { type?: unknown } | undefined)?.type;
1521
+ if (bt !== undefined && at !== undefined && bt !== at) return true;
1522
+ }
1523
+ return false;
1524
+ }
1525
+
1526
+ function classificationWeakened(
1527
+ before: unknown,
1528
+ after: unknown,
1529
+ ): boolean {
1530
+ if (!before || typeof before !== "object") return false;
1531
+ if (!after || typeof after !== "object") return true;
1532
+ const b = before as Record<string, unknown>;
1533
+ const a = after as Record<string, unknown>;
1534
+ for (const key of Object.keys(b)) {
1535
+ if (!(key in a)) return true;
1536
+ const bv = b[key];
1537
+ const av = a[key];
1538
+ if (isPii(bv) && !isPii(av)) return true;
1539
+ if (isSensitive(bv) && !isSensitive(av)) return true;
1540
+ }
1541
+ return false;
1542
+ }
1543
+
1544
+ function isPii(value: unknown): boolean {
1545
+ if (value === "pii") return true;
1546
+ if (Array.isArray(value)) return value.includes("pii");
1547
+ if (value && typeof value === "object") {
1548
+ return (value as { pii?: boolean }).pii === true;
1549
+ }
1550
+ return false;
1551
+ }
1552
+
1553
+ function isSensitive(value: unknown): boolean {
1554
+ if (value === "sensitive") return true;
1555
+ if (Array.isArray(value)) return value.includes("sensitive");
1556
+ if (value && typeof value === "object") {
1557
+ return (value as { sensitive?: boolean }).sensitive === true;
1558
+ }
1559
+ return false;
1560
+ }
1561
+
1562
+ function parseRateGate(
1563
+ gate: string,
1564
+ ): { strategy: string; max: number; per: string } | undefined {
1565
+ // rate:sliding-window-counter:300/1m
1566
+ const m = /^rate:([^:]+):(\d+)\/(.+)$/.exec(gate);
1567
+ if (!m) return undefined;
1568
+ return { strategy: m[1]!, max: Number(m[2]), per: m[3]! };
1569
+ }
1570
+
1571
+ function parsePercent(value: string | undefined): number | undefined {
1572
+ if (!value) return undefined;
1573
+ const m = /^(\d+(?:\.\d+)?)%$/.exec(value);
1574
+ if (!m) return undefined;
1575
+ return Number(m[1]);
1576
+ }
1577
+
1578
+ function latencyLoosened(
1579
+ before: Record<string, string> | undefined,
1580
+ after: Record<string, string> | undefined,
1581
+ ): boolean {
1582
+ if (!before || !after) return false;
1583
+ for (const key of Object.keys(before)) {
1584
+ if (!(key in after)) continue;
1585
+ const b = parseDurationMs(before[key]!);
1586
+ const a = parseDurationMs(after[key]!);
1587
+ if (b !== undefined && a !== undefined && a > b) return true;
1588
+ }
1589
+ return false;
1590
+ }
1591
+
1592
+ function parseDurationMs(value: string): number | undefined {
1593
+ const m = /^(\d+(?:\.\d+)?)(ms|s|m|h)$/.exec(value);
1594
+ if (!m) return undefined;
1595
+ const n = Number(m[1]);
1596
+ const unit = m[2] as "ms" | "s" | "m" | "h";
1597
+ if (unit === "ms") return n;
1598
+ if (unit === "s") return n * 1000;
1599
+ if (unit === "m") return n * 60_000;
1600
+ return n * 3_600_000;
1601
+ }
1602
+
1603
+ function asStringArray(value: unknown): string[] {
1604
+ if (!Array.isArray(value)) return [];
1605
+ return value.filter((v): v is string => typeof v === "string");
1606
+ }
1607
+
1608
+ function asUnknownArray(value: unknown): unknown[] | undefined {
1609
+ return Array.isArray(value) ? value : undefined;
1610
+ }
1611
+
1612
+ function setEqual(
1613
+ a: readonly string[] | undefined,
1614
+ b: readonly string[] | undefined,
1615
+ ): boolean {
1616
+ const as = new Set(a ?? []);
1617
+ const bs = new Set(b ?? []);
1618
+ if (as.size !== bs.size) return false;
1619
+ for (const x of as) if (!bs.has(x)) return false;
1620
+ return true;
1621
+ }
1622
+
1623
+ function listEqual(
1624
+ a: readonly string[],
1625
+ b: readonly string[],
1626
+ ): boolean {
1627
+ if (a.length !== b.length) return false;
1628
+ for (let i = 0; i < a.length; i++) if (a[i] !== b[i]) return false;
1629
+ return true;
1630
+ }
1631
+
1632
+ function setDiff(
1633
+ a: readonly string[] | undefined,
1634
+ b: readonly string[] | undefined,
1635
+ ): string[] {
1636
+ const bs = new Set(b ?? []);
1637
+ return [...new Set(a ?? [])].filter((x) => !bs.has(x)).sort();
1638
+ }
1639
+
1640
+ function intersection(
1641
+ a: readonly string[],
1642
+ b: readonly string[],
1643
+ ): string[] {
1644
+ const bs = new Set(b);
1645
+ return a.filter((x) => bs.has(x));
1646
+ }
1647
+
1648
+ function sorted(keys: Iterable<string>): string[] {
1649
+ return [...keys].sort();
1650
+ }
1651
+
1652
+ function escape(key: string): string {
1653
+ return key.replace(/~/g, "~0").replace(/\//g, "~1");
1654
+ }
1655
+
1656
+ function deepEqual(a: unknown, b: unknown): boolean {
1657
+ if (Object.is(a, b)) return true;
1658
+ return JSON.stringify(a) === JSON.stringify(b);
1659
+ }
1660
+
1661
+ /** @internal helpers exposed for exhaustive unit coverage of pure branches. */
1662
+ export const __test__ = {
1663
+ parseRateGate,
1664
+ parsePercent,
1665
+ parseDurationMs,
1666
+ schemaBreaksClients,
1667
+ classificationWeakened,
1668
+ latencyLoosened,
1669
+ highestSeverity,
1670
+ };