@kindgi/api 0.1.4-rc.5 → 0.1.5-rc.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (507) hide show
  1. package/dist/agent-binding.d.ts +13 -0
  2. package/dist/agent-binding.d.ts.map +1 -1
  3. package/dist/agent-pins.d.ts.map +1 -1
  4. package/dist/agent-pins.js +18 -2
  5. package/dist/agent-pins.js.map +1 -1
  6. package/dist/app.d.ts +110 -17
  7. package/dist/app.d.ts.map +1 -1
  8. package/dist/app.js +163 -35
  9. package/dist/app.js.map +1 -1
  10. package/dist/block-publish.d.ts +56 -0
  11. package/dist/block-publish.d.ts.map +1 -0
  12. package/dist/block-publish.js +132 -0
  13. package/dist/block-publish.js.map +1 -0
  14. package/dist/caller.d.ts +24 -0
  15. package/dist/caller.d.ts.map +1 -0
  16. package/dist/caller.js +47 -0
  17. package/dist/caller.js.map +1 -0
  18. package/dist/capability-binding.d.ts +11 -0
  19. package/dist/capability-binding.d.ts.map +1 -1
  20. package/dist/derive-agent-version.d.ts +7 -0
  21. package/dist/derive-agent-version.d.ts.map +1 -1
  22. package/dist/derive-agent-version.js +4 -0
  23. package/dist/derive-agent-version.js.map +1 -1
  24. package/dist/errors.d.ts.map +1 -1
  25. package/dist/errors.js +73 -7
  26. package/dist/errors.js.map +1 -1
  27. package/dist/eval-case-binding.d.ts +16 -4
  28. package/dist/eval-case-binding.d.ts.map +1 -1
  29. package/dist/eval-run-binding.d.ts +31 -0
  30. package/dist/eval-run-binding.d.ts.map +1 -1
  31. package/dist/eval-run-dispatcher.d.ts +7 -0
  32. package/dist/eval-run-dispatcher.d.ts.map +1 -1
  33. package/dist/eval-run-dispatcher.js.map +1 -1
  34. package/dist/eval-sample.d.ts +11 -0
  35. package/dist/eval-sample.d.ts.map +1 -0
  36. package/dist/eval-sample.js +34 -0
  37. package/dist/eval-sample.js.map +1 -0
  38. package/dist/eval-suite-binding.d.ts +13 -0
  39. package/dist/eval-suite-binding.d.ts.map +1 -1
  40. package/dist/eval-suite-binding.js.map +1 -1
  41. package/dist/flow-binding.d.ts +18 -0
  42. package/dist/flow-binding.d.ts.map +1 -1
  43. package/dist/flow-pins.d.ts.map +1 -1
  44. package/dist/flow-pins.js +18 -2
  45. package/dist/flow-pins.js.map +1 -1
  46. package/dist/gate.d.ts.map +1 -1
  47. package/dist/gate.js +25 -0
  48. package/dist/gate.js.map +1 -1
  49. package/dist/handler-binding.d.ts +32 -0
  50. package/dist/handler-binding.d.ts.map +1 -1
  51. package/dist/hitl-binding.d.ts +23 -0
  52. package/dist/hitl-binding.d.ts.map +1 -1
  53. package/dist/identity-directory-binding.d.ts +82 -4
  54. package/dist/identity-directory-binding.d.ts.map +1 -1
  55. package/dist/identity-provider-binding.d.ts +197 -17
  56. package/dist/identity-provider-binding.d.ts.map +1 -1
  57. package/dist/improvement-pass-binding.d.ts +196 -0
  58. package/dist/improvement-pass-binding.d.ts.map +1 -0
  59. package/dist/improvement-pass-binding.js +9 -0
  60. package/dist/improvement-pass-binding.js.map +1 -0
  61. package/dist/index.d.ts +35 -17
  62. package/dist/index.d.ts.map +1 -1
  63. package/dist/index.js +11 -3
  64. package/dist/index.js.map +1 -1
  65. package/dist/judged-dispatcher.d.ts +25 -4
  66. package/dist/judged-dispatcher.d.ts.map +1 -1
  67. package/dist/judged-dispatcher.js +43 -6
  68. package/dist/judged-dispatcher.js.map +1 -1
  69. package/dist/judgment-binding.d.ts +25 -1
  70. package/dist/judgment-binding.d.ts.map +1 -1
  71. package/dist/mcp-endpoint-binding.d.ts +7 -0
  72. package/dist/mcp-endpoint-binding.d.ts.map +1 -1
  73. package/dist/memory-binding.d.ts +124 -17
  74. package/dist/memory-binding.d.ts.map +1 -1
  75. package/dist/memory-erasure-binding.d.ts +145 -0
  76. package/dist/memory-erasure-binding.d.ts.map +1 -0
  77. package/dist/memory-erasure-binding.js +4 -0
  78. package/dist/memory-erasure-binding.js.map +1 -0
  79. package/dist/middleware/auth.d.ts +66 -13
  80. package/dist/middleware/auth.d.ts.map +1 -1
  81. package/dist/middleware/auth.js +150 -50
  82. package/dist/middleware/auth.js.map +1 -1
  83. package/dist/middleware/authorize.d.ts +6 -0
  84. package/dist/middleware/authorize.d.ts.map +1 -1
  85. package/dist/middleware/authorize.js +113 -4
  86. package/dist/middleware/authorize.js.map +1 -1
  87. package/dist/middleware/error-mapper.d.ts +5 -0
  88. package/dist/middleware/error-mapper.d.ts.map +1 -1
  89. package/dist/middleware/error-mapper.js +14 -1
  90. package/dist/middleware/error-mapper.js.map +1 -1
  91. package/dist/middleware/idempotency.d.ts +70 -5
  92. package/dist/middleware/idempotency.d.ts.map +1 -1
  93. package/dist/middleware/idempotency.js +174 -41
  94. package/dist/middleware/idempotency.js.map +1 -1
  95. package/dist/middleware/key-project.d.ts +17 -0
  96. package/dist/middleware/key-project.d.ts.map +1 -0
  97. package/dist/middleware/key-project.js +75 -0
  98. package/dist/middleware/key-project.js.map +1 -0
  99. package/dist/middleware/principal.d.ts.map +1 -1
  100. package/dist/middleware/principal.js +9 -5
  101. package/dist/middleware/principal.js.map +1 -1
  102. package/dist/middleware/project-ref.d.ts.map +1 -1
  103. package/dist/middleware/project-ref.js +3 -0
  104. package/dist/middleware/project-ref.js.map +1 -1
  105. package/dist/openapi/generate.d.ts.map +1 -1
  106. package/dist/openapi/generate.js +5 -2
  107. package/dist/openapi/generate.js.map +1 -1
  108. package/dist/openapi/operations.d.ts.map +1 -1
  109. package/dist/openapi/operations.js +918 -139
  110. package/dist/openapi/operations.js.map +1 -1
  111. package/dist/openapi/schemas.d.ts +92 -13
  112. package/dist/openapi/schemas.d.ts.map +1 -1
  113. package/dist/openapi/schemas.js +2122 -370
  114. package/dist/openapi/schemas.js.map +1 -1
  115. package/dist/person-grants-binding.d.ts +77 -0
  116. package/dist/person-grants-binding.d.ts.map +1 -0
  117. package/dist/person-grants-binding.js +4 -0
  118. package/dist/person-grants-binding.js.map +1 -0
  119. package/dist/proposal-service.d.ts +121 -0
  120. package/dist/proposal-service.d.ts.map +1 -0
  121. package/dist/proposal-service.js +447 -0
  122. package/dist/proposal-service.js.map +1 -0
  123. package/dist/proposal-status.d.ts +60 -0
  124. package/dist/proposal-status.d.ts.map +1 -0
  125. package/dist/proposal-status.js +109 -0
  126. package/dist/proposal-status.js.map +1 -0
  127. package/dist/provider-binding.d.ts +10 -0
  128. package/dist/provider-binding.d.ts.map +1 -1
  129. package/dist/publish-refused.d.ts +18 -3
  130. package/dist/publish-refused.d.ts.map +1 -1
  131. package/dist/publish-refused.js +18 -3
  132. package/dist/publish-refused.js.map +1 -1
  133. package/dist/routes/adapters.d.ts +2 -1
  134. package/dist/routes/adapters.d.ts.map +1 -1
  135. package/dist/routes/adapters.js +3 -1
  136. package/dist/routes/adapters.js.map +1 -1
  137. package/dist/routes/agent-releases.d.ts +55 -3
  138. package/dist/routes/agent-releases.d.ts.map +1 -1
  139. package/dist/routes/agent-releases.js +85 -55
  140. package/dist/routes/agent-releases.js.map +1 -1
  141. package/dist/routes/agents.d.ts +12 -1
  142. package/dist/routes/agents.d.ts.map +1 -1
  143. package/dist/routes/agents.js +100 -2
  144. package/dist/routes/agents.js.map +1 -1
  145. package/dist/routes/approvals.d.ts +15 -3
  146. package/dist/routes/approvals.d.ts.map +1 -1
  147. package/dist/routes/approvals.js +133 -122
  148. package/dist/routes/approvals.js.map +1 -1
  149. package/dist/routes/artifacts.d.ts +25 -3
  150. package/dist/routes/artifacts.d.ts.map +1 -1
  151. package/dist/routes/artifacts.js +116 -7
  152. package/dist/routes/artifacts.js.map +1 -1
  153. package/dist/routes/auth.d.ts +25 -1
  154. package/dist/routes/auth.d.ts.map +1 -1
  155. package/dist/routes/auth.js +655 -281
  156. package/dist/routes/auth.js.map +1 -1
  157. package/dist/routes/blocks.d.ts.map +1 -1
  158. package/dist/routes/blocks.js +2 -42
  159. package/dist/routes/blocks.js.map +1 -1
  160. package/dist/routes/capabilities.d.ts +2 -1
  161. package/dist/routes/capabilities.d.ts.map +1 -1
  162. package/dist/routes/capabilities.js +6 -1
  163. package/dist/routes/capabilities.js.map +1 -1
  164. package/dist/routes/compliance.d.ts +9 -10
  165. package/dist/routes/compliance.d.ts.map +1 -1
  166. package/dist/routes/compliance.js +42 -61
  167. package/dist/routes/compliance.js.map +1 -1
  168. package/dist/routes/conversations.d.ts +8 -1
  169. package/dist/routes/conversations.d.ts.map +1 -1
  170. package/dist/routes/conversations.js +87 -8
  171. package/dist/routes/conversations.js.map +1 -1
  172. package/dist/routes/cost.d.ts +7 -1
  173. package/dist/routes/cost.d.ts.map +1 -1
  174. package/dist/routes/cost.js +22 -3
  175. package/dist/routes/cost.js.map +1 -1
  176. package/dist/routes/denied.d.ts +11 -0
  177. package/dist/routes/denied.d.ts.map +1 -0
  178. package/dist/routes/denied.js +17 -0
  179. package/dist/routes/denied.js.map +1 -0
  180. package/dist/routes/deployments.d.ts +2 -1
  181. package/dist/routes/deployments.d.ts.map +1 -1
  182. package/dist/routes/deployments.js +44 -12
  183. package/dist/routes/deployments.js.map +1 -1
  184. package/dist/routes/eval-comparison.d.ts.map +1 -1
  185. package/dist/routes/eval-comparison.js +69 -1
  186. package/dist/routes/eval-comparison.js.map +1 -1
  187. package/dist/routes/eval-overrides.d.ts +18 -0
  188. package/dist/routes/eval-overrides.d.ts.map +1 -0
  189. package/dist/routes/eval-overrides.js +81 -0
  190. package/dist/routes/eval-overrides.js.map +1 -0
  191. package/dist/routes/eval-runs.d.ts +10 -1
  192. package/dist/routes/eval-runs.d.ts.map +1 -1
  193. package/dist/routes/eval-runs.js +63 -7
  194. package/dist/routes/eval-runs.js.map +1 -1
  195. package/dist/routes/eval-suites.d.ts.map +1 -1
  196. package/dist/routes/eval-suites.js +20 -1
  197. package/dist/routes/eval-suites.js.map +1 -1
  198. package/dist/routes/event-triggers.d.ts +5 -1
  199. package/dist/routes/event-triggers.d.ts.map +1 -1
  200. package/dist/routes/event-triggers.js +26 -3
  201. package/dist/routes/event-triggers.js.map +1 -1
  202. package/dist/routes/export-signing-keys.d.ts +12 -0
  203. package/dist/routes/export-signing-keys.d.ts.map +1 -0
  204. package/dist/routes/export-signing-keys.js +24 -0
  205. package/dist/routes/export-signing-keys.js.map +1 -0
  206. package/dist/routes/flows.d.ts.map +1 -1
  207. package/dist/routes/flows.js +9 -1
  208. package/dist/routes/flows.js.map +1 -1
  209. package/dist/routes/gate-policies.d.ts +4 -1
  210. package/dist/routes/gate-policies.d.ts.map +1 -1
  211. package/dist/routes/gate-policies.js +29 -5
  212. package/dist/routes/gate-policies.js.map +1 -1
  213. package/dist/routes/guardrails.d.ts +16 -14
  214. package/dist/routes/guardrails.d.ts.map +1 -1
  215. package/dist/routes/guardrails.js +39 -3
  216. package/dist/routes/guardrails.js.map +1 -1
  217. package/dist/routes/identity.d.ts +20 -4
  218. package/dist/routes/identity.d.ts.map +1 -1
  219. package/dist/routes/identity.js +246 -6
  220. package/dist/routes/identity.js.map +1 -1
  221. package/dist/routes/improvement-passes.d.ts +52 -0
  222. package/dist/routes/improvement-passes.d.ts.map +1 -0
  223. package/dist/routes/improvement-passes.js +449 -0
  224. package/dist/routes/improvement-passes.js.map +1 -0
  225. package/dist/routes/judged-suites.d.ts +50 -0
  226. package/dist/routes/judged-suites.d.ts.map +1 -1
  227. package/dist/routes/judged-suites.js +66 -27
  228. package/dist/routes/judged-suites.js.map +1 -1
  229. package/dist/routes/judgment-context.d.ts +18 -2
  230. package/dist/routes/judgment-context.d.ts.map +1 -1
  231. package/dist/routes/judgment-context.js +63 -5
  232. package/dist/routes/judgment-context.js.map +1 -1
  233. package/dist/routes/judgment-flow-context.d.ts.map +1 -1
  234. package/dist/routes/judgment-flow-context.js +14 -2
  235. package/dist/routes/judgment-flow-context.js.map +1 -1
  236. package/dist/routes/judgments.d.ts.map +1 -1
  237. package/dist/routes/judgments.js +8 -1
  238. package/dist/routes/judgments.js.map +1 -1
  239. package/dist/routes/mcp.d.ts.map +1 -1
  240. package/dist/routes/mcp.js +26 -1
  241. package/dist/routes/mcp.js.map +1 -1
  242. package/dist/routes/memory-access.d.ts +43 -0
  243. package/dist/routes/memory-access.d.ts.map +1 -0
  244. package/dist/routes/memory-access.js +129 -0
  245. package/dist/routes/memory-access.js.map +1 -0
  246. package/dist/routes/memory-erasures.d.ts +25 -0
  247. package/dist/routes/memory-erasures.d.ts.map +1 -0
  248. package/dist/routes/memory-erasures.js +214 -0
  249. package/dist/routes/memory-erasures.js.map +1 -0
  250. package/dist/routes/memory-parse.d.ts +38 -0
  251. package/dist/routes/memory-parse.d.ts.map +1 -0
  252. package/dist/routes/memory-parse.js +199 -0
  253. package/dist/routes/memory-parse.js.map +1 -0
  254. package/dist/routes/memory.d.ts +9 -4
  255. package/dist/routes/memory.d.ts.map +1 -1
  256. package/dist/routes/memory.js +226 -234
  257. package/dist/routes/memory.js.map +1 -1
  258. package/dist/routes/observations.d.ts +4 -1
  259. package/dist/routes/observations.d.ts.map +1 -1
  260. package/dist/routes/observations.js +8 -2
  261. package/dist/routes/observations.js.map +1 -1
  262. package/dist/routes/pagination.d.ts +7 -0
  263. package/dist/routes/pagination.d.ts.map +1 -1
  264. package/dist/routes/pagination.js +10 -0
  265. package/dist/routes/pagination.js.map +1 -1
  266. package/dist/routes/policies.d.ts +2 -1
  267. package/dist/routes/policies.d.ts.map +1 -1
  268. package/dist/routes/policies.js +3 -1
  269. package/dist/routes/policies.js.map +1 -1
  270. package/dist/routes/project-mismatch.d.ts +22 -0
  271. package/dist/routes/project-mismatch.d.ts.map +1 -0
  272. package/dist/routes/project-mismatch.js +34 -0
  273. package/dist/routes/project-mismatch.js.map +1 -0
  274. package/dist/routes/projects.d.ts +15 -4
  275. package/dist/routes/projects.d.ts.map +1 -1
  276. package/dist/routes/projects.js +100 -9
  277. package/dist/routes/projects.js.map +1 -1
  278. package/dist/routes/proposals.d.ts +25 -17
  279. package/dist/routes/proposals.d.ts.map +1 -1
  280. package/dist/routes/proposals.js +340 -507
  281. package/dist/routes/proposals.js.map +1 -1
  282. package/dist/routes/provenance.d.ts +17 -3
  283. package/dist/routes/provenance.d.ts.map +1 -1
  284. package/dist/routes/provenance.js +79 -103
  285. package/dist/routes/provenance.js.map +1 -1
  286. package/dist/routes/providers.d.ts +17 -1
  287. package/dist/routes/providers.d.ts.map +1 -1
  288. package/dist/routes/providers.js +188 -8
  289. package/dist/routes/providers.js.map +1 -1
  290. package/dist/routes/public-run-tokens.d.ts.map +1 -1
  291. package/dist/routes/public-run-tokens.js +3 -0
  292. package/dist/routes/public-run-tokens.js.map +1 -1
  293. package/dist/routes/runs.d.ts +10 -0
  294. package/dist/routes/runs.d.ts.map +1 -1
  295. package/dist/routes/runs.js +92 -42
  296. package/dist/routes/runs.js.map +1 -1
  297. package/dist/routes/schedules.d.ts +21 -13
  298. package/dist/routes/schedules.d.ts.map +1 -1
  299. package/dist/routes/schedules.js +416 -55
  300. package/dist/routes/schedules.js.map +1 -1
  301. package/dist/routes/segments.d.ts +2 -0
  302. package/dist/routes/segments.d.ts.map +1 -1
  303. package/dist/routes/segments.js +5 -0
  304. package/dist/routes/segments.js.map +1 -1
  305. package/dist/routes/service-accounts.d.ts +17 -0
  306. package/dist/routes/service-accounts.d.ts.map +1 -0
  307. package/dist/routes/service-accounts.js +212 -0
  308. package/dist/routes/service-accounts.js.map +1 -0
  309. package/dist/routes/sign-in-options.d.ts +50 -0
  310. package/dist/routes/sign-in-options.d.ts.map +1 -0
  311. package/dist/routes/sign-in-options.js +98 -0
  312. package/dist/routes/sign-in-options.js.map +1 -0
  313. package/dist/routes/signing-keys.d.ts +2 -1
  314. package/dist/routes/signing-keys.d.ts.map +1 -1
  315. package/dist/routes/signing-keys.js +3 -1
  316. package/dist/routes/signing-keys.js.map +1 -1
  317. package/dist/routes/tenant-access.d.ts +13 -0
  318. package/dist/routes/tenant-access.d.ts.map +1 -0
  319. package/dist/routes/tenant-access.js +26 -0
  320. package/dist/routes/tenant-access.js.map +1 -0
  321. package/dist/routes/tenant.d.ts.map +1 -1
  322. package/dist/routes/tenant.js +4 -0
  323. package/dist/routes/tenant.js.map +1 -1
  324. package/dist/routes/token-sign-in.d.ts +36 -0
  325. package/dist/routes/token-sign-in.d.ts.map +1 -0
  326. package/dist/routes/token-sign-in.js +101 -0
  327. package/dist/routes/token-sign-in.js.map +1 -0
  328. package/dist/routes/tokens.d.ts +15 -5
  329. package/dist/routes/tokens.d.ts.map +1 -1
  330. package/dist/routes/tokens.js +162 -43
  331. package/dist/routes/tokens.js.map +1 -1
  332. package/dist/routes/tools.d.ts.map +1 -1
  333. package/dist/routes/tools.js +9 -1
  334. package/dist/routes/tools.js.map +1 -1
  335. package/dist/routes/trigger-access.d.ts +29 -0
  336. package/dist/routes/trigger-access.d.ts.map +1 -0
  337. package/dist/routes/trigger-access.js +33 -0
  338. package/dist/routes/trigger-access.js.map +1 -0
  339. package/dist/routes/webhook-endpoints.d.ts +2 -1
  340. package/dist/routes/webhook-endpoints.d.ts.map +1 -1
  341. package/dist/routes/webhook-endpoints.js +11 -3
  342. package/dist/routes/webhook-endpoints.js.map +1 -1
  343. package/dist/routes/webhooks.d.ts +5 -1
  344. package/dist/routes/webhooks.d.ts.map +1 -1
  345. package/dist/routes/webhooks.js +25 -2
  346. package/dist/routes/webhooks.js.map +1 -1
  347. package/dist/run-failure.d.ts +21 -0
  348. package/dist/run-failure.d.ts.map +1 -0
  349. package/dist/run-failure.js +29 -0
  350. package/dist/run-failure.js.map +1 -0
  351. package/dist/service-account-binding.d.ts +108 -0
  352. package/dist/service-account-binding.d.ts.map +1 -0
  353. package/dist/service-account-binding.js +4 -0
  354. package/dist/service-account-binding.js.map +1 -0
  355. package/dist/session-store-binding.d.ts +61 -13
  356. package/dist/session-store-binding.d.ts.map +1 -1
  357. package/dist/signed-export.d.ts +89 -0
  358. package/dist/signed-export.d.ts.map +1 -0
  359. package/dist/signed-export.js +149 -0
  360. package/dist/signed-export.js.map +1 -0
  361. package/dist/supervisor-binding.d.ts +186 -413
  362. package/dist/supervisor-binding.d.ts.map +1 -1
  363. package/dist/testing/app-bindings.d.ts +33 -0
  364. package/dist/testing/app-bindings.d.ts.map +1 -0
  365. package/dist/testing/app-bindings.js +112 -0
  366. package/dist/testing/app-bindings.js.map +1 -0
  367. package/dist/testing/index.d.ts +11 -0
  368. package/dist/testing/index.d.ts.map +1 -0
  369. package/dist/testing/index.js +13 -0
  370. package/dist/testing/index.js.map +1 -0
  371. package/dist/testing/stub-binding.d.ts +34 -0
  372. package/dist/testing/stub-binding.d.ts.map +1 -0
  373. package/dist/testing/stub-binding.js +42 -0
  374. package/dist/testing/stub-binding.js.map +1 -0
  375. package/dist/testing/trigger-registry.d.ts +21 -0
  376. package/dist/testing/trigger-registry.d.ts.map +1 -0
  377. package/dist/testing/trigger-registry.js +206 -0
  378. package/dist/testing/trigger-registry.js.map +1 -0
  379. package/dist/token-admin.d.ts +46 -8
  380. package/dist/token-admin.d.ts.map +1 -1
  381. package/dist/token-admin.js.map +1 -1
  382. package/dist/tool-binding.d.ts +13 -0
  383. package/dist/tool-binding.d.ts.map +1 -1
  384. package/dist/trigger-binding.d.ts +2 -2
  385. package/dist/trigger-binding.d.ts.map +1 -1
  386. package/dist/trigger-binding.js +1 -1
  387. package/dist/trigger-binding.js.map +1 -1
  388. package/dist/types.d.ts +16 -0
  389. package/dist/types.d.ts.map +1 -1
  390. package/dist/webhook-endpoint-binding.d.ts +19 -3
  391. package/dist/webhook-endpoint-binding.d.ts.map +1 -1
  392. package/dist/webhook-endpoint-binding.js +1 -1
  393. package/dist/webhook-endpoint-binding.js.map +1 -1
  394. package/openapi.json +15286 -8653
  395. package/package.json +26 -22
  396. package/src/agent-binding.ts +14 -0
  397. package/src/agent-pins.ts +18 -2
  398. package/src/app.ts +366 -80
  399. package/src/block-publish.ts +181 -0
  400. package/src/caller.ts +62 -0
  401. package/src/capability-binding.ts +12 -0
  402. package/src/derive-agent-version.ts +9 -1
  403. package/src/errors.ts +73 -7
  404. package/src/eval-case-binding.ts +17 -1
  405. package/src/eval-run-binding.ts +31 -0
  406. package/src/eval-run-dispatcher.ts +7 -0
  407. package/src/eval-sample.ts +42 -0
  408. package/src/eval-suite-binding.ts +14 -0
  409. package/src/flow-binding.ts +19 -0
  410. package/src/flow-pins.ts +18 -2
  411. package/src/gate.ts +26 -0
  412. package/src/handler-binding.ts +36 -1
  413. package/src/hitl-binding.ts +24 -0
  414. package/src/identity-directory-binding.ts +85 -4
  415. package/src/identity-provider-binding.ts +216 -21
  416. package/src/improvement-pass-binding.ts +193 -0
  417. package/src/index.ts +153 -31
  418. package/src/judged-dispatcher.ts +78 -11
  419. package/src/judgment-binding.ts +25 -1
  420. package/src/mcp-endpoint-binding.ts +7 -0
  421. package/src/memory-binding.ts +121 -17
  422. package/src/memory-erasure-binding.ts +156 -0
  423. package/src/middleware/auth.ts +228 -62
  424. package/src/middleware/authorize.ts +139 -3
  425. package/src/middleware/error-mapper.ts +15 -1
  426. package/src/middleware/idempotency.ts +230 -43
  427. package/src/middleware/key-project.ts +86 -0
  428. package/src/middleware/principal.ts +9 -5
  429. package/src/middleware/project-ref.ts +3 -0
  430. package/src/openapi/generate.ts +11 -2
  431. package/src/openapi/operations.ts +1059 -142
  432. package/src/openapi/schemas.ts +2438 -485
  433. package/src/person-grants-binding.ts +76 -0
  434. package/src/proposal-service.ts +640 -0
  435. package/src/proposal-status.ts +176 -0
  436. package/src/provider-binding.ts +10 -0
  437. package/src/publish-refused.ts +38 -9
  438. package/src/routes/adapters.ts +4 -0
  439. package/src/routes/agent-releases.ts +121 -69
  440. package/src/routes/agents.ts +128 -1
  441. package/src/routes/approvals.ts +164 -154
  442. package/src/routes/artifacts.ts +181 -11
  443. package/src/routes/auth.ts +804 -336
  444. package/src/routes/blocks.ts +2 -53
  445. package/src/routes/capabilities.ts +10 -1
  446. package/src/routes/compliance.ts +60 -99
  447. package/src/routes/conversations.ts +101 -7
  448. package/src/routes/cost.ts +32 -3
  449. package/src/routes/denied.ts +28 -0
  450. package/src/routes/deployments.ts +62 -16
  451. package/src/routes/eval-comparison.ts +81 -2
  452. package/src/routes/eval-overrides.ts +108 -0
  453. package/src/routes/eval-runs.ts +86 -6
  454. package/src/routes/eval-suites.ts +25 -1
  455. package/src/routes/event-triggers.ts +27 -3
  456. package/src/routes/export-signing-keys.ts +31 -0
  457. package/src/routes/flows.ts +12 -1
  458. package/src/routes/gate-policies.ts +37 -6
  459. package/src/routes/guardrails.ts +61 -2
  460. package/src/routes/identity.ts +328 -10
  461. package/src/routes/improvement-passes.ts +541 -0
  462. package/src/routes/judged-suites.ts +113 -43
  463. package/src/routes/judgment-context.ts +76 -5
  464. package/src/routes/judgment-flow-context.ts +16 -2
  465. package/src/routes/judgments.ts +17 -3
  466. package/src/routes/mcp.ts +29 -1
  467. package/src/routes/memory-access.ts +179 -0
  468. package/src/routes/memory-erasures.ts +240 -0
  469. package/src/routes/memory-parse.ts +250 -0
  470. package/src/routes/memory.ts +244 -286
  471. package/src/routes/observations.ts +15 -2
  472. package/src/routes/pagination.ts +12 -0
  473. package/src/routes/policies.ts +7 -1
  474. package/src/routes/project-mismatch.ts +56 -0
  475. package/src/routes/projects.ts +107 -13
  476. package/src/routes/proposals.ts +370 -611
  477. package/src/routes/provenance.ts +99 -133
  478. package/src/routes/providers.ts +223 -8
  479. package/src/routes/public-run-tokens.ts +3 -0
  480. package/src/routes/runs.ts +119 -39
  481. package/src/routes/schedules.ts +472 -60
  482. package/src/routes/segments.ts +11 -0
  483. package/src/routes/service-accounts.ts +262 -0
  484. package/src/routes/sign-in-options.ts +168 -0
  485. package/src/routes/signing-keys.ts +7 -1
  486. package/src/routes/tenant-access.ts +37 -0
  487. package/src/routes/tenant.ts +6 -0
  488. package/src/routes/token-sign-in.ts +161 -0
  489. package/src/routes/tokens.ts +172 -46
  490. package/src/routes/tools.ts +12 -1
  491. package/src/routes/trigger-access.ts +72 -0
  492. package/src/routes/webhook-endpoints.ts +15 -3
  493. package/src/routes/webhooks.ts +26 -2
  494. package/src/run-failure.ts +42 -0
  495. package/src/service-account-binding.ts +113 -0
  496. package/src/session-store-binding.ts +65 -13
  497. package/src/signed-export.ts +250 -0
  498. package/src/supervisor-binding.ts +169 -409
  499. package/src/testing/app-bindings.ts +142 -0
  500. package/src/testing/index.ts +21 -0
  501. package/src/testing/stub-binding.ts +53 -0
  502. package/src/testing/trigger-registry.ts +259 -0
  503. package/src/token-admin.ts +40 -8
  504. package/src/tool-binding.ts +14 -0
  505. package/src/trigger-binding.ts +8 -1
  506. package/src/types.ts +16 -0
  507. package/src/webhook-endpoint-binding.ts +18 -3
@@ -3,157 +3,159 @@
3
3
 
4
4
  import { type Context, Hono } from 'hono';
5
5
 
6
- import type {
7
- AgentId,
8
- Cursor,
9
- FixProposalId,
10
- SupervisorId,
11
- TenantId,
12
- Timestamp,
13
- } from '@kindgi/types';
6
+ import type { AgentId } from '@kindgi/agents';
7
+ import { type Action, ref } from '@kindgi/authz';
8
+ import type { Cursor, FixProposalId, TenantId } from '@kindgi/types';
14
9
 
15
10
  import { statusFor, toWireError } from '../errors.js';
11
+ import type { EvalComparison } from '../eval-run-binding.js';
12
+ import type { Authorizer } from '../middleware/authorize.js';
13
+ import {
14
+ type ProposalFacts,
15
+ type ProposalService,
16
+ type ProposalServiceDeps,
17
+ type ProposalServiceError,
18
+ createProposalService,
19
+ proposalNotFound,
20
+ } from '../proposal-service.js';
21
+ import {
22
+ FIX_PROPOSAL_STATUSES,
23
+ type FixProposalStatus,
24
+ type ProposalEvaluationOutcome,
25
+ } from '../proposal-status.js';
16
26
  import type {
17
- FixProposal,
18
- FixProposalStatus,
19
- PassCriterion,
20
- PatternRef,
27
+ ProposalObjective,
28
+ ProposalTier,
21
29
  ProposedChange,
22
- SupervisorBinding,
30
+ StoredProposal,
23
31
  } from '../supervisor-binding.js';
24
32
  import type { AppEnv } from '../types.js';
33
+ import { actorOf, promotionResponse, scopeFromQuery } from './agent-releases.js';
34
+ import { parseComparison } from './eval-comparison.js';
35
+ import { liveScopeToWire, parseLiveScopeBody } from './live-scope-wire.js';
25
36
  import { clampLimit } from './pagination.js';
26
- import { parseScopeParams } from './scope-params.js';
27
-
28
- const FIX_PROPOSAL_STATUSES: ReadonlySet<FixProposalStatus> = new Set([
29
- 'draft',
30
- 'dry-running',
31
- 'dry-run-passed',
32
- 'dry-run-failed',
33
- 'proposed-for-review',
34
- 'approved',
35
- 'rejected',
36
- 'applied',
37
- 'rolled-back',
38
- 'withdrawn',
39
- ]);
40
-
41
- const PROPOSAL_TIERS: ReadonlySet<ProposedChange['tier']> = new Set([
42
- 'prompt',
43
- 'retrieval',
44
- 'tool-config',
45
- ]);
46
-
47
- const REVIEWER_ROLES: ReadonlySet<'standard' | 'senior' | 'admin'> = new Set([
48
- 'standard',
49
- 'senior',
50
- 'admin',
51
- ]);
37
+
38
+ /** What the proposal lifecycle runs on besides its own store. */
39
+ export type ProposalsRouteDeps = Omit<ProposalServiceDeps, 'store'>;
40
+
41
+ const TIERS: readonly ProposalTier[] = ['settings-block', 'prompt-block'];
42
+ const OBJECTIVES: readonly ProposalObjective[] = ['weightedYesShare', 'weightedPrecisionAtK'];
43
+ const MAX_TEXT = 2000;
44
+
45
+ type Ctx = Context<AppEnv>;
52
46
 
53
47
  /**
54
- * Supervisor proposals resource. Ships the full self-fix lifecycle
55
- * over HTTP: draft → dry-run → submit-review → apply / rollback /
56
- * withdraw — the complete supervisor loop.
48
+ * Improvement proposals (`/v1/proposals`): a change to one data block an
49
+ * agent version pins (new settings values, or a new prompt template),
50
+ * for one live scope, taken through the same comparison, gate and
51
+ * promotion as any other version (`createProposalService`).
57
52
  *
58
- * Persistence + runtime wiring is caller-plugged via
59
- * `SupervisorBinding` — the API package doesn't own supervisor
60
- * runtime, agent registry, HITL wiring, or eval invocation.
53
+ * GET / list (statuses derived): by agent, tier, status and live scope
54
+ * GET /:id one
55
+ * POST / a hand-written proposal (a draft)
56
+ * POST /:id/evaluate publish the block version and derive the agent
57
+ * version (inert until promoted), then compare it
58
+ * on a test set (202)
59
+ * POST /:id/request a gated promotion of the candidate for the scope
60
+ * (201 promoted, 202 in review, 422 gate-failed)
61
+ * POST /:id/rollback the scope goes back to what served it before
62
+ * POST /:id/withdraw closed by a person
61
63
  *
62
- * `supervisorId` scoping: every route requires an `X-Supervisor-Id`
63
- * request header. Missing / empty → `400 supervisor-header-missing`.
64
- *
65
- * State transitions: attempts to advance a proposal from a status
66
- * the runtime won't accept (e.g., apply on a proposal still in
67
- * `dry-running`) surface as `409 proposal-invalid-state-transition`.
68
- * This is a caller/state conflict per `docs/API-ROUTE-CONVENTIONS.md`
69
- * §4.3 — the request was well-formed but the resource is not in
70
- * the required state.
64
+ * Authorized on the proposal's agent: reading is `read`, drafting,
65
+ * evaluating and withdrawing are `publish` (they make versions), and
66
+ * requesting and rolling back are `promote`. A proposal the caller can't
67
+ * read answers 404, as one that doesn't exist.
71
68
  */
72
- export function proposalsRouter(binding: SupervisorBinding): Hono<AppEnv> {
69
+ export function proposalsRouter(
70
+ store: ProposalServiceDeps['store'],
71
+ deps: ProposalsRouteDeps,
72
+ authorizer?: Authorizer,
73
+ mount?: (r: Hono<AppEnv>, service: ProposalService) => void,
74
+ ): Hono<AppEnv> {
73
75
  const r = new Hono<AppEnv>();
76
+ const service = createProposalService({ ...deps, store });
77
+ // Routes mounted before `/:proposalId` (an improvement pass's `improve`).
78
+ mount?.(r, service);
79
+
80
+ /** `undefined` when allowed; else the 403. */
81
+ async function deny(c: Ctx, action: Action, agentId: string) {
82
+ if (authorizer === undefined) return undefined;
83
+ const decision = await authorizer.check(c, action, ref('agent', agentId));
84
+ if (decision.allowed) return undefined;
85
+ return fail(c, 'permission-denied', {
86
+ code: 'permission-denied',
87
+ message: `Permission denied: this needs ${action} on agent "${agentId}" (${decision.reason})`,
88
+ });
89
+ }
74
90
 
75
- // ---------- supervisor-id gate for the whole resource ----------
76
- r.use('*', async (c, next) => {
77
- const requestId = c.get('requestId');
78
- const supervisorId = c.req.header('x-supervisor-id');
79
- if (supervisorId === undefined || supervisorId.length === 0) {
80
- c.status(statusFor('supervisor-header-missing') as never);
81
- return c.json(
82
- toWireError(
83
- {
84
- code: 'supervisor-header-missing',
85
- message:
86
- 'X-Supervisor-Id header is required on all /v1/proposals routes; supply the SupervisorId scoping the request.',
87
- },
88
- requestId,
89
- ),
90
- );
91
- }
92
- await next();
93
- return;
94
- });
95
-
96
- // ---------- GET / (list, cursor-paginated) ----------
97
- r.get('/', async (c) => {
98
- const requestId = c.get('requestId');
99
- const tenantId = c.get('tenantId') as TenantId;
100
- const supervisorId = c.req.header('x-supervisor-id') as SupervisorId;
101
- const limit = clampLimit(c.req.query('limit'));
102
-
103
- const cursorRaw = c.req.query('cursor');
104
- const statusRaw = c.req.query('status');
105
- const agentIdRaw = c.req.query('agentId');
106
- const tierRaw = c.req.query('tier');
107
-
108
- let status: FixProposalStatus | undefined;
109
- if (statusRaw !== undefined && statusRaw.length > 0) {
110
- if (!FIX_PROPOSAL_STATUSES.has(statusRaw as FixProposalStatus)) {
111
- c.status(statusFor('bad-input') as never);
112
- return c.json(
113
- toWireError(
114
- { code: 'bad-input', message: `Unknown \`status\` value: ${statusRaw}` },
115
- requestId,
116
- ),
117
- );
118
- }
119
- status = statusRaw as FixProposalStatus;
120
- }
91
+ /** The proposal, if it exists and the caller may read it (else the 404). */
92
+ async function load(c: Ctx): Promise<StoredProposal | Response> {
93
+ const proposalId = c.req.param('proposalId') ?? '';
94
+ const stored = await store.getProposal({
95
+ tenantId: c.get('tenantId') as TenantId,
96
+ proposalId: proposalId as FixProposalId,
97
+ });
98
+ const hidden =
99
+ stored !== null &&
100
+ authorizer !== undefined &&
101
+ !(await authorizer.can(c, 'read', ref('agent', stored.agentId as unknown as string)));
102
+ if (stored === null || hidden) return failWith(c, proposalNotFound(proposalId));
103
+ return stored;
104
+ }
121
105
 
122
- let tier: ProposedChange['tier'] | undefined;
123
- if (tierRaw !== undefined && tierRaw.length > 0) {
124
- if (!PROPOSAL_TIERS.has(tierRaw as ProposedChange['tier'])) {
125
- c.status(statusFor('bad-input') as never);
126
- return c.json(
127
- toWireError(
128
- { code: 'bad-input', message: `Unknown \`tier\` value: ${tierRaw}` },
129
- requestId,
130
- ),
131
- );
132
- }
133
- tier = tierRaw as ProposedChange['tier'];
134
- }
106
+ async function answer(c: Ctx, stored: StoredProposal, status: 200 | 201 | 202 = 200) {
107
+ const f = await service.facts(c.get('tenantId') as TenantId)(stored);
108
+ c.status(status);
109
+ return c.json(serialize(stored, f));
110
+ }
135
111
 
136
- const scopeParsed = parseScopeParams(c.req.query(), { tenantId });
137
- if (scopeParsed.kind === 'err') {
138
- c.status(statusFor('scope-invalid') as never);
139
- return c.json(
140
- toWireError({ code: 'scope-invalid', message: scopeParsed.message }, requestId),
141
- );
142
- }
112
+ /** Read the body and the proposal, and check the caller may `action` it. */
113
+ async function prepare(
114
+ c: Ctx,
115
+ action: Action,
116
+ ): Promise<{ body: Record<string, unknown>; proposal: StoredProposal } | Response> {
117
+ const body = await readBody(c);
118
+ if (body === undefined) return badInput(c, 'Request body must be a JSON object');
119
+ const proposal = await load(c);
120
+ if (proposal instanceof Response) return proposal;
121
+ const denied = await deny(c, action, proposal.agentId as unknown as string);
122
+ return denied ?? { body, proposal };
123
+ }
143
124
 
144
- const page = await binding.listProposals({
125
+ // ---------- GET / ----------
126
+ r.get('/', async (c) => {
127
+ const tenantId = c.get('tenantId') as TenantId;
128
+ const status = c.req.query('status');
129
+ if (status !== undefined && !FIX_PROPOSAL_STATUSES.includes(status as FixProposalStatus)) {
130
+ return badInput(c, `Unknown \`status\`: ${status}`);
131
+ }
132
+ const tier = c.req.query('tier');
133
+ if (tier !== undefined && !TIERS.includes(tier as ProposalTier)) {
134
+ return badInput(c, `Unknown \`tier\`: ${tier} (one of ${TIERS.join(', ')})`);
135
+ }
136
+ const scope = scopeFromQuery((n) => c.req.query(n), c.req.queries('segment') ?? []);
137
+ if (scope.kind === 'err') return badInput(c, scope.message);
138
+ const agentId = c.req.query('agentId');
139
+ const cursor = c.req.query('cursor');
140
+ const page = await store.listProposals({
145
141
  tenantId,
146
- supervisorId,
147
- limit,
148
- ...(cursorRaw !== undefined && cursorRaw.length > 0 && { cursor: cursorRaw as Cursor }),
149
- ...(status !== undefined && { status }),
150
- ...(agentIdRaw !== undefined && agentIdRaw.length > 0 && { agentId: agentIdRaw as AgentId }),
151
- ...(tier !== undefined && { tier }),
152
- ...(scopeParsed.scope !== undefined && { scope: scopeParsed.scope }),
153
- ...(scopeParsed.inherit !== undefined && { inherit: scopeParsed.inherit }),
142
+ limit: clampLimit(c.req.query('limit')),
143
+ ...(cursor !== undefined && cursor !== '' && { cursor: cursor as Cursor }),
144
+ ...(agentId !== undefined && agentId !== '' && { agentId: agentId as AgentId }),
145
+ ...(tier !== undefined && { tier: tier as ProposalTier }),
146
+ ...(scope.scope !== undefined && { liveScope: scope.scope }),
154
147
  });
148
+ const readable =
149
+ authorizer === undefined
150
+ ? page.data
151
+ : await authorizer.filterByCan(c, 'read', page.data, (p) =>
152
+ ref('agent', p.agentId as unknown as string),
153
+ );
154
+ const read = service.facts(tenantId);
155
+ const rows = await Promise.all(readable.map(async (p) => serialize(p, await read(p))));
155
156
  return c.json({
156
- data: page.data.map(serializeProposal),
157
+ // A status filter applies after deriving, so a page can hold fewer rows than `limit`.
158
+ data: status === undefined ? rows : rows.filter((row) => row.status === status),
157
159
  hasMore: page.nextCursor !== undefined,
158
160
  ...(page.nextCursor !== undefined && { nextCursor: page.nextCursor as unknown as string }),
159
161
  });
@@ -161,559 +163,316 @@ export function proposalsRouter(binding: SupervisorBinding): Hono<AppEnv> {
161
163
 
162
164
  // ---------- GET /:proposalId ----------
163
165
  r.get('/:proposalId', async (c) => {
164
- const requestId = c.get('requestId');
165
- const tenantId = c.get('tenantId') as TenantId;
166
- const supervisorId = c.req.header('x-supervisor-id') as SupervisorId;
167
- const proposalId = c.req.param('proposalId') as FixProposalId;
168
-
169
- const proposal = await binding.getProposal({ tenantId, supervisorId, proposalId });
170
- if (proposal === null) {
171
- return proposalNotFound(c, requestId, proposalId);
172
- }
173
- return c.json(serializeProposal(proposal));
166
+ const stored = await load(c);
167
+ if (stored instanceof Response) return stored;
168
+ return answer(c, stored);
174
169
  });
175
170
 
176
- // ---------- POST / (draft) ----------
171
+ // ---------- POST / (a hand-written proposal) ----------
177
172
  r.post('/', async (c) => {
178
- const requestId = c.get('requestId');
179
- const tenantId = c.get('tenantId') as TenantId;
180
- const supervisorId = c.req.header('x-supervisor-id') as SupervisorId;
181
-
182
- let body: unknown;
183
- try {
184
- body = await c.req.json();
185
- } catch {
186
- return badInput(c, requestId, 'Request body must be valid JSON');
187
- }
188
- const parsed = parseDraftBody(body);
189
- if (parsed.kind === 'err') {
190
- return badInput(c, requestId, parsed.message);
191
- }
192
-
193
- const outcome = await binding.draftProposal({
194
- tenantId,
195
- supervisorId,
196
- agentId: parsed.value.agentId,
197
- agentVersion: parsed.value.agentVersion,
198
- tier: parsed.value.tier,
199
- change: parsed.value.change,
200
- patternRefs: parsed.value.patternRefs,
201
- hypothesis: parsed.value.hypothesis,
202
- proposerRuleId: parsed.value.proposerRuleId,
173
+ const body = await readBody(c);
174
+ if (body === undefined) return badInput(c, 'Request body must be a JSON object');
175
+ const parsed = parseCreateBody(body);
176
+ if (typeof parsed === 'string') return badInput(c, parsed);
177
+ const denied = await deny(c, 'publish', parsed.agentId);
178
+ if (denied !== undefined) return denied;
179
+ const actor = actorOf(c);
180
+ const outcome = await service.draft({
181
+ tenantId: c.get('tenantId') as TenantId,
182
+ agentId: parsed.agentId,
183
+ fromVersion: parsed.fromVersion,
184
+ scope: parsed.scope,
185
+ tier: parsed.tier,
186
+ blockId: parsed.blockId,
187
+ content: parsed.content,
188
+ hypothesis: parsed.hypothesis,
189
+ ...(parsed.judgmentIds !== undefined && { evidence: { judgmentIds: parsed.judgmentIds } }),
190
+ drafter: { kind: 'person', by: `${actor.kind}:${actor.id}` },
203
191
  });
204
- // Both `ok` and `dedup` return 201 — the caller ends up with a
205
- // proposal id it can advance. Signal dedup via a header so
206
- // consumers that care can distinguish (mirrors the
207
- // X-Idempotent-Replay pattern from the idempotency middleware).
208
- if (outcome.kind === 'dedup') {
192
+ if (outcome.kind === 'err') return failWith(c, outcome.error);
193
+ if (outcome.value.deduped) {
209
194
  c.header('X-Proposal-Deduped', 'true');
195
+ return answer(c, outcome.value.proposal);
210
196
  }
211
- c.status(201);
212
- return c.json(serializeProposal(outcome.proposal));
197
+ return answer(c, outcome.value.proposal, 201);
213
198
  });
214
199
 
215
- // ---------- POST /:proposalId/dry-run ----------
216
- r.post('/:proposalId/dry-run', async (c) => {
217
- const requestId = c.get('requestId');
218
- const tenantId = c.get('tenantId') as TenantId;
219
- const supervisorId = c.req.header('x-supervisor-id') as SupervisorId;
220
- const proposalId = c.req.param('proposalId') as FixProposalId;
221
-
222
- let body: unknown;
223
- try {
224
- body = await c.req.json();
225
- } catch {
226
- return badInput(c, requestId, 'Request body must be valid JSON');
227
- }
228
- const parsed = parseDryRunBody(body);
229
- if (parsed.kind === 'err') {
230
- return badInput(c, requestId, parsed.message);
231
- }
232
-
233
- const outcome = await binding.dryRunProposal({
234
- tenantId,
235
- supervisorId,
236
- proposalId,
237
- datasetId: parsed.value.datasetId,
238
- datasetVersion: parsed.value.datasetVersion,
239
- criterion: parsed.value.criterion,
240
- });
241
- if (outcome.kind === 'not-found') {
242
- return proposalNotFound(c, requestId, proposalId);
243
- }
244
- if (outcome.kind === 'invalid-transition') {
245
- return invalidTransition(c, requestId, outcome.proposalId, outcome.from, outcome.to);
246
- }
247
- if (outcome.kind === 'runtime-error') {
248
- return runtimeError(c, requestId, outcome.code, outcome.message);
249
- }
250
- return c.json({
251
- proposal: serializeProposal(outcome.proposal),
252
- passed: outcome.passed,
253
- });
254
- });
255
-
256
- // ---------- POST /:proposalId/submit-review ----------
257
- r.post('/:proposalId/submit-review', async (c) => {
258
- const requestId = c.get('requestId');
259
- const tenantId = c.get('tenantId') as TenantId;
260
- const supervisorId = c.req.header('x-supervisor-id') as SupervisorId;
261
- const proposalId = c.req.param('proposalId') as FixProposalId;
262
-
263
- // Body is optional — every field on submit-review is an override.
264
- let body: unknown = {};
265
- try {
266
- const raw = await c.req.text();
267
- if (raw.length > 0) body = JSON.parse(raw);
268
- } catch {
269
- return badInput(c, requestId, 'Request body must be valid JSON');
270
- }
271
- const parsed = parseSubmitReviewBody(body);
272
- if (parsed.kind === 'err') {
273
- return badInput(c, requestId, parsed.message);
274
- }
275
-
276
- const outcome = await binding.submitReview({
277
- tenantId,
278
- supervisorId,
279
- proposalId,
280
- ...(parsed.value.requiredRole !== undefined && { requiredRole: parsed.value.requiredRole }),
281
- ...(parsed.value.expiresAt !== undefined && { expiresAt: parsed.value.expiresAt }),
282
- });
283
- if (outcome.kind === 'not-found') {
284
- return proposalNotFound(c, requestId, proposalId);
285
- }
286
- if (outcome.kind === 'invalid-transition') {
287
- return invalidTransition(c, requestId, outcome.proposalId, outcome.from, outcome.to);
288
- }
289
- if (outcome.kind === 'ground-layer-violation') {
290
- c.status(statusFor('ground-layer-violation') as never);
291
- return c.json(
292
- toWireError(
293
- {
294
- code: 'ground-layer-violation',
295
- message: `Proposal ${outcome.proposalId as unknown as string} violated ground guardrail ${outcome.guardrailId}: ${outcome.reason}`,
296
- proposalId: outcome.proposalId as unknown as string,
297
- guardrailId: outcome.guardrailId,
298
- reason: outcome.reason,
299
- },
300
- requestId,
301
- ),
302
- );
303
- }
304
- if (outcome.kind === 'runtime-error') {
305
- return runtimeError(c, requestId, outcome.code, outcome.message);
306
- }
307
- return c.json({
308
- proposal: serializeProposal(outcome.proposal),
309
- approvalId: outcome.approvalId as unknown as string,
310
- metaFix: outcome.metaFix,
200
+ // ---------- POST /:proposalId/evaluate ----------
201
+ r.post('/:proposalId/evaluate', async (c) => {
202
+ const body = await readBody(c);
203
+ if (body === undefined) return badInput(c, 'Request body must be a JSON object');
204
+ const parsed = parseEvaluateBody(body);
205
+ if (typeof parsed === 'string') return badInput(c, parsed);
206
+ const ready = await prepare(c, 'publish');
207
+ if (ready instanceof Response) return ready;
208
+ const outcome = await service.evaluate({
209
+ tenantId: c.get('tenantId') as TenantId,
210
+ proposal: ready.proposal,
211
+ suiteId: parsed.suiteId,
212
+ objective: parsed.objective,
213
+ ...(parsed.comparison !== undefined && { comparison: parsed.comparison }),
214
+ actor: actorOf(c),
311
215
  });
216
+ if (outcome.kind === 'err') return failWith(c, outcome.error);
217
+ return answer(c, outcome.value, 202);
312
218
  });
313
219
 
314
- // ---------- POST /:proposalId/apply ----------
315
- r.post('/:proposalId/apply', async (c) => {
316
- const requestId = c.get('requestId');
317
- const tenantId = c.get('tenantId') as TenantId;
318
- const supervisorId = c.req.header('x-supervisor-id') as SupervisorId;
319
- const proposalId = c.req.param('proposalId') as FixProposalId;
320
-
321
- let body: unknown = {};
322
- try {
323
- const raw = await c.req.text();
324
- if (raw.length > 0) body = JSON.parse(raw);
325
- } catch {
326
- return badInput(c, requestId, 'Request body must be valid JSON');
327
- }
328
- const parsed = parseApplyBody(body);
329
- if (parsed.kind === 'err') {
330
- return badInput(c, requestId, parsed.message);
331
- }
332
-
333
- const outcome = await binding.applyProposal({
334
- tenantId,
335
- supervisorId,
336
- proposalId,
337
- ...(parsed.value.newVersion !== undefined && { newVersion: parsed.value.newVersion }),
220
+ // ---------- POST /:proposalId/request ----------
221
+ r.post('/:proposalId/request', async (c) => {
222
+ const ready = await prepare(c, 'promote');
223
+ if (ready instanceof Response) return ready;
224
+ const reason = optionalText(ready.body, 'reason');
225
+ if (reason.kind === 'err') return badInput(c, reason.message);
226
+ const outcome = await service.request({
227
+ tenantId: c.get('tenantId') as TenantId,
228
+ proposal: ready.proposal,
229
+ actor: actorOf(c),
230
+ ...(reason.value !== undefined && { reason: reason.value }),
338
231
  });
339
- if (outcome.kind === 'not-found') {
340
- return proposalNotFound(c, requestId, proposalId);
341
- }
342
- if (outcome.kind === 'invalid-transition') {
343
- return invalidTransition(c, requestId, outcome.proposalId, outcome.from, outcome.to);
232
+ if (outcome.kind === 'err') return failWith(c, outcome.error);
233
+ const { proposal, promotion } = outcome.value;
234
+ if (promotion.kind !== 'ok') return promotionResponse(c, promotion);
235
+ if (promotion.promotion.status === 'refused') {
236
+ return promotionResponse(c, promotion, { proposalId: proposal.id as unknown as string });
344
237
  }
345
- if (outcome.kind === 'runtime-error') {
346
- return runtimeError(c, requestId, outcome.code, outcome.message);
347
- }
348
- return c.json({
349
- proposalId: outcome.proposalId as unknown as string,
350
- appliedVersion: outcome.appliedVersion,
351
- appliedAt: outcome.appliedAt as unknown as string,
352
- });
238
+ return answer(c, proposal, promotion.promotion.status === 'pending-approval' ? 202 : 201);
353
239
  });
354
240
 
355
241
  // ---------- POST /:proposalId/rollback ----------
356
242
  r.post('/:proposalId/rollback', async (c) => {
357
- const requestId = c.get('requestId');
358
- const tenantId = c.get('tenantId') as TenantId;
359
- const supervisorId = c.req.header('x-supervisor-id') as SupervisorId;
360
- const proposalId = c.req.param('proposalId') as FixProposalId;
361
-
362
- let body: unknown;
363
- try {
364
- body = await c.req.json();
365
- } catch {
366
- return badInput(c, requestId, 'Request body must be valid JSON');
367
- }
368
- const parsed = parseReasonBody(body);
369
- if (parsed.kind === 'err') {
370
- return badInput(c, requestId, parsed.message);
371
- }
372
-
373
- const outcome = await binding.rollbackProposal({
374
- tenantId,
375
- supervisorId,
376
- proposalId,
377
- reason: parsed.value.reason,
378
- });
379
- if (outcome.kind === 'not-found') {
380
- return proposalNotFound(c, requestId, proposalId);
381
- }
382
- if (outcome.kind === 'invalid-transition') {
383
- return invalidTransition(c, requestId, outcome.proposalId, outcome.from, outcome.to);
384
- }
385
- if (outcome.kind === 'runtime-error') {
386
- return runtimeError(c, requestId, outcome.code, outcome.message);
387
- }
388
- return c.json({
389
- proposalId: outcome.proposalId as unknown as string,
390
- rolledBackAt: outcome.rolledBackAt as unknown as string,
243
+ const ready = await prepare(c, 'promote');
244
+ if (ready instanceof Response) return ready;
245
+ const reason = optionalText(ready.body, 'reason');
246
+ if (reason.kind === 'err') return badInput(c, reason.message);
247
+ const outcome = await service.rollback({
248
+ tenantId: c.get('tenantId') as TenantId,
249
+ proposal: ready.proposal,
250
+ actor: actorOf(c),
251
+ ...(reason.value !== undefined && { reason: reason.value }),
391
252
  });
253
+ if (outcome.kind === 'err') return failWith(c, outcome.error);
254
+ return answer(c, outcome.value);
392
255
  });
393
256
 
394
257
  // ---------- POST /:proposalId/withdraw ----------
395
258
  r.post('/:proposalId/withdraw', async (c) => {
396
- const requestId = c.get('requestId');
397
- const tenantId = c.get('tenantId') as TenantId;
398
- const supervisorId = c.req.header('x-supervisor-id') as SupervisorId;
399
- const proposalId = c.req.param('proposalId') as FixProposalId;
400
-
401
- let body: unknown;
402
- try {
403
- body = await c.req.json();
404
- } catch {
405
- return badInput(c, requestId, 'Request body must be valid JSON');
406
- }
407
- const parsed = parseReasonBody(body);
408
- if (parsed.kind === 'err') {
409
- return badInput(c, requestId, parsed.message);
410
- }
411
-
412
- const outcome = await binding.withdrawProposal({
413
- tenantId,
414
- supervisorId,
415
- proposalId,
416
- reason: parsed.value.reason,
259
+ const ready = await prepare(c, 'publish');
260
+ if (ready instanceof Response) return ready;
261
+ const reason = optionalText(ready.body, 'reason');
262
+ if (reason.kind === 'err') return badInput(c, reason.message);
263
+ if (reason.value === undefined) {
264
+ return badInput(c, '`reason` is required: why it was withdrawn');
265
+ }
266
+ const outcome = await service.withdraw({
267
+ tenantId: c.get('tenantId') as TenantId,
268
+ proposal: ready.proposal,
269
+ actor: actorOf(c),
270
+ reason: reason.value,
417
271
  });
418
- if (outcome.kind === 'not-found') {
419
- return proposalNotFound(c, requestId, proposalId);
420
- }
421
- if (outcome.kind === 'invalid-transition') {
422
- return invalidTransition(c, requestId, outcome.proposalId, outcome.from, outcome.to);
423
- }
424
- if (outcome.kind === 'runtime-error') {
425
- return runtimeError(c, requestId, outcome.code, outcome.message);
426
- }
427
- return c.json(serializeProposal(outcome.proposal));
272
+ if (outcome.kind === 'err') return failWith(c, outcome.error);
273
+ return answer(c, outcome.value);
428
274
  });
429
275
 
430
276
  return r;
431
277
  }
432
278
 
433
- // -------------------- serialization --------------------
279
+ // ---------- wire ----------
434
280
 
435
- function serializeProposal(p: FixProposal): Record<string, unknown> {
281
+ function evaluationWire(
282
+ stored: NonNullable<StoredProposal['evaluation']>,
283
+ seen: ProposalEvaluationOutcome | null | undefined,
284
+ ): Record<string, unknown> {
285
+ if (seen === undefined || seen === null) return { ...stored };
286
+ return {
287
+ ...stored,
288
+ runStatus: seen.runStatus,
289
+ ...(seen.baseline !== undefined && { baseline: seen.baseline }),
290
+ ...(seen.candidate !== undefined && { candidate: seen.candidate }),
291
+ ...(seen.delta !== undefined && { delta: seen.delta }),
292
+ ...(seen.spread !== undefined && { spread: seen.spread }),
293
+ ...(seen.cases !== undefined && { cases: seen.cases }),
294
+ ...(seen.better !== undefined && { better: seen.better }),
295
+ };
296
+ }
297
+
298
+ export function serialize(p: StoredProposal, f: ProposalFacts): Record<string, unknown> {
299
+ const promotion = f.promotion ?? undefined;
436
300
  return {
437
301
  id: p.id as unknown as string,
438
- tenantId: p.tenantId as unknown as string,
439
- supervisorId: p.supervisorId as unknown as string,
440
302
  agentId: p.agentId as unknown as string,
441
- agentVersion: p.agentVersion,
303
+ fromVersion: p.fromVersion,
304
+ scope: liveScopeToWire(p.scope),
442
305
  tier: p.tier,
443
306
  change: p.change,
444
- patternRefs: p.patternRefs,
445
307
  hypothesis: p.hypothesis,
446
- proposerRuleId: p.proposerRuleId,
447
- status: p.status,
448
- fingerprint: p.fingerprint,
449
- ...(p.resolutionReason !== undefined && { resolutionReason: p.resolutionReason }),
450
- ...(p.reviewApprovalId !== undefined && {
451
- reviewApprovalId: p.reviewApprovalId as unknown as string,
308
+ ...(p.evidence !== undefined && { evidence: p.evidence }),
309
+ drafter: p.drafter,
310
+ status: f.status,
311
+ ...(p.candidate !== undefined && { candidate: p.candidate }),
312
+ ...(p.evaluation !== undefined && { evaluation: evaluationWire(p.evaluation, f.evaluation) }),
313
+ ...(promotion !== undefined && {
314
+ promotion: {
315
+ id: promotion.id,
316
+ status: promotion.status ?? 'promoted',
317
+ ...(promotion.approvalId !== undefined && { approvalId: promotion.approvalId }),
318
+ ...(f.liveNow !== undefined && { liveNow: f.liveNow }),
319
+ },
452
320
  }),
453
- ...(p.appliedVersion !== undefined && { appliedVersion: p.appliedVersion }),
454
- ...(p.appliedAt !== undefined && { appliedAt: p.appliedAt as unknown as string }),
455
- ...(p.rolledBackAt !== undefined && { rolledBackAt: p.rolledBackAt as unknown as string }),
456
- createdAt: p.createdAt as unknown as string,
457
- updatedAt: p.updatedAt as unknown as string,
458
- ...(p.resolvedAt !== undefined && { resolvedAt: p.resolvedAt as unknown as string }),
321
+ ...(p.rolledBack !== undefined && { rolledBack: p.rolledBack }),
322
+ ...(p.withdrawn !== undefined && { withdrawn: p.withdrawn }),
323
+ createdAt: p.createdAt,
324
+ updatedAt: p.updatedAt,
459
325
  };
460
326
  }
461
327
 
462
- // -------------------- error helpers --------------------
328
+ // ---------- parsing ----------
463
329
 
464
- type Ctx = Context<AppEnv>;
465
-
466
- function badInput(c: Ctx, requestId: string, message: string): Response {
467
- c.status(statusFor('bad-input') as never);
468
- return c.json(toWireError({ code: 'bad-input', message }, requestId));
330
+ interface CreateBody {
331
+ readonly agentId: string;
332
+ readonly fromVersion: string;
333
+ readonly scope: StoredProposal['scope'];
334
+ readonly tier: ProposalTier;
335
+ readonly blockId: string;
336
+ readonly content: ProposedChange['change']['content'];
337
+ readonly hypothesis: string;
338
+ readonly judgmentIds?: readonly string[];
469
339
  }
470
340
 
471
- function proposalNotFound(c: Ctx, requestId: string, proposalId: FixProposalId): Response {
472
- c.status(statusFor('proposal-not-found') as never);
473
- return c.json(
474
- toWireError(
475
- {
476
- code: 'proposal-not-found',
477
- message: `No proposal with id "${proposalId as unknown as string}"`,
478
- proposalId: proposalId as unknown as string,
479
- },
480
- requestId,
481
- ),
482
- );
341
+ function obj(value: unknown): Record<string, unknown> | undefined {
342
+ return typeof value === 'object' && value !== null && !Array.isArray(value)
343
+ ? (value as Record<string, unknown>)
344
+ : undefined;
483
345
  }
484
346
 
485
- function invalidTransition(
486
- c: Ctx,
487
- requestId: string,
488
- proposalId: FixProposalId,
489
- from: FixProposalStatus,
490
- to: FixProposalStatus,
491
- ): Response {
492
- c.status(statusFor('proposal-invalid-state-transition') as never);
493
- return c.json(
494
- toWireError(
495
- {
496
- code: 'proposal-invalid-state-transition',
497
- message: `Cannot transition proposal ${proposalId as unknown as string} from ${from} to ${to}`,
498
- proposalId: proposalId as unknown as string,
499
- from,
500
- to,
501
- },
502
- requestId,
503
- ),
504
- );
347
+ function nonEmpty(value: unknown): value is string {
348
+ return typeof value === 'string' && value.trim() !== '' && value.length <= MAX_TEXT;
505
349
  }
506
350
 
507
- function runtimeError(c: Ctx, requestId: string, code: string, message: string): Response {
508
- c.status(statusFor(code) as never);
509
- return c.json(toWireError({ code, message }, requestId));
351
+ function parseContent(tier: ProposalTier, raw: unknown): string | undefined {
352
+ const content = obj(raw);
353
+ const keys = content === undefined ? [] : Object.keys(content);
354
+ if (tier === 'settings-block') {
355
+ return content !== undefined && obj(content.values) !== undefined && keys.length === 1
356
+ ? undefined
357
+ : '`change.content` for a settings block is { values: { … } } (the schema carries over)';
358
+ }
359
+ return content !== undefined && typeof content.template === 'string' && keys.length === 1
360
+ ? undefined
361
+ : '`change.content` for a prompt block is { template } (the parameters carry over)';
510
362
  }
511
363
 
512
- // -------------------- request parsing --------------------
513
-
514
- type Parsed<T> =
515
- | { readonly kind: 'ok'; readonly value: T }
516
- | { readonly kind: 'err'; readonly message: string };
517
-
518
- interface DraftPayload {
519
- readonly agentId: AgentId;
520
- readonly agentVersion: string;
521
- readonly tier: ProposedChange['tier'];
522
- readonly change: ProposedChange['change'];
523
- readonly patternRefs: readonly PatternRef[];
524
- readonly hypothesis: string;
525
- readonly proposerRuleId: string;
364
+ function parseEvidence(raw: unknown): { judgmentIds?: string[] } | string {
365
+ if (raw === undefined) return {};
366
+ const evidence = obj(raw);
367
+ const ids = evidence?.judgmentIds;
368
+ const valid =
369
+ evidence !== undefined &&
370
+ Object.keys(evidence).every((k) => k === 'judgmentIds') &&
371
+ (ids === undefined || (Array.isArray(ids) && ids.every((id) => nonEmpty(id))));
372
+ if (!valid) return '`evidence` is { judgmentIds?: string[] }';
373
+ return ids === undefined ? {} : { judgmentIds: ids as string[] };
526
374
  }
527
375
 
528
- function parseDraftBody(body: unknown): Parsed<DraftPayload> {
529
- if (body === null || typeof body !== 'object') {
530
- return { kind: 'err', message: 'Request body must be an object' };
531
- }
532
- const b = body as Record<string, unknown>;
533
- const agentId = b.agentId;
534
- if (typeof agentId !== 'string' || agentId.length === 0) {
535
- return { kind: 'err', message: 'Field `agentId` must be a non-empty string' };
536
- }
537
- const agentVersion = b.agentVersion;
538
- if (typeof agentVersion !== 'string' || agentVersion.length === 0) {
539
- return { kind: 'err', message: 'Field `agentVersion` must be a non-empty string' };
540
- }
541
- const tier = b.tier;
542
- if (typeof tier !== 'string' || !PROPOSAL_TIERS.has(tier as ProposedChange['tier'])) {
543
- return {
544
- kind: 'err',
545
- message: 'Field `tier` must be one of "prompt", "retrieval", "tool-config"',
546
- };
547
- }
548
- const change = b.change;
549
- if (change === null || typeof change !== 'object') {
550
- return { kind: 'err', message: 'Field `change` must be an object' };
376
+ function parseCreateBody(b: Record<string, unknown>): CreateBody | string {
377
+ if (!nonEmpty(b.agentId)) return '`agentId` is required';
378
+ if (!nonEmpty(b.fromVersion)) {
379
+ return '`fromVersion` is required: the agent version the change applies to';
551
380
  }
552
- const patternRefs = b.patternRefs;
553
- if (!Array.isArray(patternRefs)) {
554
- return { kind: 'err', message: 'Field `patternRefs` must be an array' };
555
- }
556
- const hypothesis = b.hypothesis;
557
- if (typeof hypothesis !== 'string' || hypothesis.length === 0) {
558
- return { kind: 'err', message: 'Field `hypothesis` must be a non-empty string' };
559
- }
560
- const proposerRuleId = b.proposerRuleId;
561
- if (typeof proposerRuleId !== 'string' || proposerRuleId.length === 0) {
562
- return { kind: 'err', message: 'Field `proposerRuleId` must be a non-empty string' };
381
+ const scope = parseLiveScopeBody(b.scope);
382
+ if (scope.kind === 'err') return scope.message;
383
+ if (!TIERS.includes(b.tier as ProposalTier)) return `\`tier\` must be one of ${TIERS.join(', ')}`;
384
+ const tier = b.tier as ProposalTier;
385
+ const change = obj(b.change);
386
+ if (change === undefined) return '`change` must be an object: { blockId, content }';
387
+ if (!nonEmpty(change.blockId)) return '`change.blockId` is required';
388
+ const contentProblem = parseContent(tier, change.content);
389
+ if (contentProblem !== undefined) return contentProblem;
390
+ if (!nonEmpty(b.hypothesis)) {
391
+ return '`hypothesis` is required: what the change should improve, and why';
563
392
  }
393
+ const evidence = parseEvidence(b.evidence);
394
+ if (typeof evidence === 'string') return evidence;
564
395
  return {
565
- kind: 'ok',
566
- value: {
567
- agentId: agentId as AgentId,
568
- agentVersion,
569
- tier: tier as ProposedChange['tier'],
570
- change: change as ProposedChange['change'],
571
- patternRefs: patternRefs as readonly PatternRef[],
572
- hypothesis,
573
- proposerRuleId,
574
- },
396
+ agentId: b.agentId,
397
+ fromVersion: b.fromVersion,
398
+ scope: scope.scope,
399
+ tier,
400
+ blockId: change.blockId,
401
+ content: change.content as ProposedChange['change']['content'],
402
+ hypothesis: b.hypothesis,
403
+ ...evidence,
575
404
  };
576
405
  }
577
406
 
578
- interface DryRunPayload {
579
- readonly datasetId: string;
580
- readonly datasetVersion: string;
581
- readonly criterion: PassCriterion;
407
+ interface EvaluateBody {
408
+ readonly suiteId: string;
409
+ readonly objective: ProposalObjective;
410
+ readonly comparison?: EvalComparison;
582
411
  }
583
412
 
584
- function parseDryRunBody(body: unknown): Parsed<DryRunPayload> {
585
- if (body === null || typeof body !== 'object') {
586
- return { kind: 'err', message: 'Request body must be an object' };
587
- }
588
- const b = body as Record<string, unknown>;
589
- const datasetId = b.datasetId;
590
- if (typeof datasetId !== 'string' || datasetId.length === 0) {
591
- return { kind: 'err', message: 'Field `datasetId` must be a non-empty string' };
413
+ function parseEvaluateBody(b: Record<string, unknown>): EvaluateBody | string {
414
+ if (!nonEmpty(b.suiteId)) {
415
+ return '`suiteId` is required: the test set (a judged eval suite) to compare on';
592
416
  }
593
- const datasetVersion = b.datasetVersion;
594
- if (typeof datasetVersion !== 'string' || datasetVersion.length === 0) {
595
- return { kind: 'err', message: 'Field `datasetVersion` must be a non-empty string' };
417
+ if (b.objective !== undefined && !OBJECTIVES.includes(b.objective as ProposalObjective)) {
418
+ return `\`objective\` must be one of ${OBJECTIVES.join(', ')}`;
596
419
  }
597
- const rawCriterion = b.criterion;
598
- if (rawCriterion === null || typeof rawCriterion !== 'object') {
599
- return { kind: 'err', message: 'Field `criterion` must be an object' };
600
- }
601
- const cr = rawCriterion as Record<string, unknown>;
602
- if (cr.kind === 'min-pass-rate') {
603
- const minPassRate = cr.minPassRate;
604
- if (typeof minPassRate !== 'number' || !Number.isFinite(minPassRate)) {
605
- return {
606
- kind: 'err',
607
- message: 'Field `criterion.minPassRate` must be a finite number for kind "min-pass-rate"',
608
- };
609
- }
610
- return {
611
- kind: 'ok',
612
- value: { datasetId, datasetVersion, criterion: { kind: 'min-pass-rate', minPassRate } },
613
- };
614
- }
615
- if (cr.kind === 'strict-improvement') {
616
- const baseline = cr.baselinePassRate;
617
- const minDelta = cr.minDelta;
618
- if (typeof baseline !== 'number' || !Number.isFinite(baseline)) {
619
- return {
620
- kind: 'err',
621
- message:
622
- 'Field `criterion.baselinePassRate` must be a finite number for kind "strict-improvement"',
623
- };
624
- }
625
- if (typeof minDelta !== 'number' || !Number.isFinite(minDelta)) {
626
- return {
627
- kind: 'err',
628
- message: 'Field `criterion.minDelta` must be a finite number for kind "strict-improvement"',
629
- };
630
- }
631
- return {
632
- kind: 'ok',
633
- value: {
634
- datasetId,
635
- datasetVersion,
636
- criterion: { kind: 'strict-improvement', baselinePassRate: baseline, minDelta },
637
- },
638
- };
420
+ const extra = Object.keys(b).filter(
421
+ (k) =>
422
+ !['suiteId', 'objective', 'reads', 'repetitions', 'k', 'classWeights', 'sample'].includes(k),
423
+ );
424
+ if (extra.length > 0) {
425
+ return `\`${extra[0]}\` isn't a field of an evaluation: { suiteId, objective?, reads?, repetitions?, k?, classWeights?, sample? }`;
639
426
  }
427
+ const comparison = parseComparison(b);
428
+ if (comparison.kind === 'err') return comparison.message;
640
429
  return {
641
- kind: 'err',
642
- message: 'Field `criterion.kind` must be "min-pass-rate" or "strict-improvement"',
430
+ suiteId: b.suiteId,
431
+ objective: (b.objective as ProposalObjective | undefined) ?? 'weightedYesShare',
432
+ ...(comparison.value !== undefined && { comparison: comparison.value }),
643
433
  };
644
434
  }
645
435
 
646
- interface SubmitReviewPayload {
647
- readonly requiredRole?: 'standard' | 'senior' | 'admin';
648
- readonly expiresAt?: Timestamp;
649
- }
650
-
651
- function parseSubmitReviewBody(body: unknown): Parsed<SubmitReviewPayload> {
652
- if (body === null || typeof body !== 'object') {
653
- return { kind: 'err', message: 'Request body must be an object' };
654
- }
655
- const b = body as Record<string, unknown>;
656
- const out: {
657
- requiredRole?: 'standard' | 'senior' | 'admin';
658
- expiresAt?: Timestamp;
659
- } = {};
660
- const requiredRole = b.requiredRole;
661
- if (requiredRole !== undefined) {
662
- if (
663
- typeof requiredRole !== 'string' ||
664
- !REVIEWER_ROLES.has(requiredRole as 'standard' | 'senior' | 'admin')
665
- ) {
666
- return {
436
+ function optionalText(
437
+ body: Record<string, unknown>,
438
+ field: string,
439
+ ): { kind: 'ok'; value?: string } | { kind: 'err'; message: string } {
440
+ const v = body[field];
441
+ if (v === undefined) return { kind: 'ok' };
442
+ return nonEmpty(v)
443
+ ? { kind: 'ok', value: v }
444
+ : {
667
445
  kind: 'err',
668
- message: 'Field `requiredRole` must be one of "standard", "senior", "admin"',
446
+ message: `\`${field}\` must be a non-empty string (at most ${MAX_TEXT} characters)`,
669
447
  };
670
- }
671
- out.requiredRole = requiredRole as 'standard' | 'senior' | 'admin';
672
- }
673
- const expiresAt = b.expiresAt;
674
- if (expiresAt !== undefined) {
675
- if (typeof expiresAt !== 'string' || Number.isNaN(new Date(expiresAt).getTime())) {
676
- return { kind: 'err', message: 'Field `expiresAt` must be an ISO date string' };
677
- }
678
- out.expiresAt = expiresAt as Timestamp;
679
- }
680
- return { kind: 'ok', value: out };
681
448
  }
682
449
 
683
- interface ApplyPayload {
684
- readonly newVersion?: string;
450
+ /** The body as an object; `{}` when empty, `undefined` when it isn't a JSON object. */
451
+ async function readBody(c: Ctx): Promise<Record<string, unknown> | undefined> {
452
+ const text = await c.req.text();
453
+ if (text.trim() === '') return {};
454
+ try {
455
+ return obj(JSON.parse(text));
456
+ } catch {
457
+ return undefined;
458
+ }
685
459
  }
686
460
 
687
- function parseApplyBody(body: unknown): Parsed<ApplyPayload> {
688
- if (body === null || typeof body !== 'object') {
689
- return { kind: 'err', message: 'Request body must be an object' };
690
- }
691
- const b = body as Record<string, unknown>;
692
- const newVersion = b.newVersion;
693
- if (newVersion !== undefined) {
694
- if (typeof newVersion !== 'string' || newVersion.length === 0) {
695
- return {
696
- kind: 'err',
697
- message: 'Field `newVersion` must be a non-empty string when present',
698
- };
699
- }
700
- return { kind: 'ok', value: { newVersion } };
701
- }
702
- return { kind: 'ok', value: {} };
461
+ // ---------- helpers ----------
462
+
463
+ function fail(
464
+ c: Ctx,
465
+ code: string,
466
+ error: { code: string; message: string } & Record<string, unknown>,
467
+ ) {
468
+ c.status(statusFor(code) as never);
469
+ return c.json(toWireError(error, c.get('requestId')));
703
470
  }
704
471
 
705
- interface ReasonPayload {
706
- readonly reason: string;
472
+ function badInput(c: Ctx, message: string) {
473
+ return fail(c, 'bad-input', { code: 'bad-input', message });
707
474
  }
708
475
 
709
- function parseReasonBody(body: unknown): Parsed<ReasonPayload> {
710
- if (body === null || typeof body !== 'object') {
711
- return { kind: 'err', message: 'Request body must be an object' };
712
- }
713
- const b = body as Record<string, unknown>;
714
- const reason = b.reason;
715
- if (typeof reason !== 'string' || reason.length === 0) {
716
- return { kind: 'err', message: 'Field `reason` must be a non-empty string' };
717
- }
718
- return { kind: 'ok', value: { reason } };
476
+ function failWith(c: Ctx, error: ProposalServiceError) {
477
+ return fail(c, error.code, error);
719
478
  }