pi-daddy 0.22.0 → 0.23.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 (362) hide show
  1. package/CHANGELOG.md +30 -0
  2. package/contracts/daily-view/v1/README.md +114 -0
  3. package/contracts/daily-view/v1/fixtures/view.json +525 -0
  4. package/contracts/daily-view/v1/fixtures/view.txt +75 -0
  5. package/contracts/daily-view/v1/fixtures/work.jsonl +17 -0
  6. package/contracts/daily-view/v1/p03/README.md +26 -0
  7. package/contracts/daily-view/v1/p03/fixtures/retained-executions.json +241 -0
  8. package/contracts/daily-view/v1/p03/projection.schema.json +194 -0
  9. package/contracts/daily-view/v1/p03/provenance.json +21 -0
  10. package/contracts/dashboard-host/v1/README.md +176 -0
  11. package/contracts/debrief/v1/README.md +146 -0
  12. package/contracts/debrief/v1/intervention/README.md +32 -0
  13. package/contracts/debrief/v1/intervention/fixtures/artifact-3ee11136beb864f518252e5024dae9197421fbf97b6e981d3243c782b5c44bd0.txt +1 -0
  14. package/contracts/debrief/v1/intervention/fixtures/artifact-58905d872dd3c54a9705f67b05fa82edbad7af6476ae8993faf1a7b9fadfb905.txt +1 -0
  15. package/contracts/debrief/v1/intervention/fixtures/assessment.json +29 -0
  16. package/contracts/debrief/v1/intervention/fixtures/blind-view.json +20 -0
  17. package/contracts/debrief/v1/intervention/fixtures/choice.json +7 -0
  18. package/contracts/debrief/v1/intervention/fixtures/manifest.json +57 -0
  19. package/contracts/debrief/v1/intervention/fixtures/reveal.json +39 -0
  20. package/contracts/debrief/v1/provenance.json +89 -0
  21. package/contracts/debrief/v1/work-capture/README.md +38 -0
  22. package/contracts/debrief/v1/work-capture/fixtures/batch.json +17 -0
  23. package/contracts/debrief/v1/work-capture/fixtures/cases.json +184 -0
  24. package/contracts/debrief/v1/work-capture/fixtures/review.json +184 -0
  25. package/contracts/debrief/v1/work-capture/review-request.schema.json +41 -0
  26. package/contracts/debrief/v1/work-capture/work-case.schema.json +190 -0
  27. package/contracts/debrief/v2/README.md +71 -0
  28. package/contracts/debrief/v2/input-pin.json +66 -0
  29. package/contracts/debrief/v2/intervention-upstream.md +40 -0
  30. package/contracts/debrief/v2/work-signals-upstream.md +22 -0
  31. package/contracts/dispatch-control/v1/README.md +109 -0
  32. package/contracts/dispatch-control/v1/request.schema.json +19 -0
  33. package/contracts/effect-profile/v1/README.md +124 -0
  34. package/contracts/execution-retention/v1/README.md +109 -0
  35. package/contracts/execution-retention/v2/README.md +148 -0
  36. package/contracts/execution-retention/v2/fixtures/interrupted-native-bytes.json +91 -0
  37. package/contracts/execution-retention/v2/fixtures/missing-native-session.json +91 -0
  38. package/contracts/execution-retention/v2/fixtures/native-file-unknown-branch.json +91 -0
  39. package/contracts/execution-retention/v2/fixtures/native-id-without-bytes.json +91 -0
  40. package/contracts/execution-retention/v2/fixtures/native-live-branch.json +90 -0
  41. package/contracts/execution-retention/v2/fixtures/replaced-native-session.json +92 -0
  42. package/contracts/execution-retention/v2/fixtures/result-82c620cdfeb8ca6b858cf653d7f5e2eedca5315d6244216d60e8b36f03223d6a.bin +1 -0
  43. package/contracts/execution-retention/v2/fixtures/session-2ec1f134bc739984434f7fca689e1c3d1c39fce2dcadae709e9b6faedfcc4ac7.bin +3 -0
  44. package/contracts/execution-retention/v2/fixtures/session-9bdb4bd16a901da31580acf056bda525225259e15ec409e0410480109011772f.bin +3 -0
  45. package/contracts/execution-retention/v2/manifest.schema.json +858 -0
  46. package/contracts/experiment/v1/README.md +164 -0
  47. package/contracts/factory-order/v1/README.md +151 -0
  48. package/contracts/factory-order/v1/adoption-pin.json +8 -0
  49. package/contracts/intent-control/v1/README.md +135 -0
  50. package/contracts/intent-control/v1/request.schema.json +1096 -0
  51. package/contracts/intent-control/v2/README.md +34 -0
  52. package/contracts/intent-control/v2/request.schema.json +1070 -0
  53. package/contracts/ledger/v4/README.md +241 -0
  54. package/contracts/ledger/v4/fixtures/layout-options.json +428 -0
  55. package/contracts/ledger/v4/fixtures/work-acceptance.json +57 -0
  56. package/contracts/ledger/v4/fixtures/work-occurrence.json +44 -0
  57. package/contracts/ledger/v4/fixtures/work-revision.json +26 -0
  58. package/contracts/ledger/v4/fixtures/work-snapshot.json +73 -0
  59. package/contracts/ledger/v4/ledger-event.schema.json +162 -0
  60. package/contracts/ordinary-control/v1/README.md +59 -0
  61. package/contracts/producer-ipc/v1/README.md +137 -0
  62. package/dist/check-runner.d.ts +5 -0
  63. package/dist/check-runner.d.ts.map +1 -1
  64. package/dist/check-runner.js +16 -2
  65. package/dist/check-runner.js.map +1 -1
  66. package/dist/control-journal.d.ts +20 -0
  67. package/dist/control-journal.d.ts.map +1 -0
  68. package/dist/control-journal.js +73 -0
  69. package/dist/control-journal.js.map +1 -0
  70. package/dist/daily-view-input.d.ts +44 -0
  71. package/dist/daily-view-input.d.ts.map +1 -0
  72. package/dist/daily-view-input.js +68 -0
  73. package/dist/daily-view-input.js.map +1 -0
  74. package/dist/daily-view-render.d.ts +4 -0
  75. package/dist/daily-view-render.d.ts.map +1 -0
  76. package/dist/daily-view-render.js +41 -0
  77. package/dist/daily-view-render.js.map +1 -0
  78. package/dist/daily-view.d.ts +74 -0
  79. package/dist/daily-view.d.ts.map +1 -0
  80. package/dist/daily-view.js +170 -0
  81. package/dist/daily-view.js.map +1 -0
  82. package/dist/dashboard-cli.d.ts +26 -1
  83. package/dist/dashboard-cli.d.ts.map +1 -1
  84. package/dist/dashboard-cli.js +121 -3
  85. package/dist/dashboard-cli.js.map +1 -1
  86. package/dist/dashboard-harness.d.ts +19 -0
  87. package/dist/dashboard-harness.d.ts.map +1 -0
  88. package/dist/dashboard-harness.js +75 -0
  89. package/dist/dashboard-harness.js.map +1 -0
  90. package/dist/dashboard-herdr.d.ts.map +1 -1
  91. package/dist/dashboard-herdr.js +7 -2
  92. package/dist/dashboard-herdr.js.map +1 -1
  93. package/dist/dashboard-host-contract.d.ts +111 -0
  94. package/dist/dashboard-host-contract.d.ts.map +1 -0
  95. package/dist/dashboard-host-contract.js +14 -0
  96. package/dist/dashboard-host-contract.js.map +1 -0
  97. package/dist/dashboard-host-transport.d.ts +15 -0
  98. package/dist/dashboard-host-transport.d.ts.map +1 -0
  99. package/dist/dashboard-host-transport.js +122 -0
  100. package/dist/dashboard-host-transport.js.map +1 -0
  101. package/dist/dashboard-host.d.ts +149 -0
  102. package/dist/dashboard-host.d.ts.map +1 -0
  103. package/dist/dashboard-host.js +329 -0
  104. package/dist/dashboard-host.js.map +1 -0
  105. package/dist/dashboard-observation.d.ts +28 -0
  106. package/dist/dashboard-observation.d.ts.map +1 -0
  107. package/dist/dashboard-observation.js +86 -0
  108. package/dist/dashboard-observation.js.map +1 -0
  109. package/dist/debrief-contract.d.ts +65 -0
  110. package/dist/debrief-contract.d.ts.map +1 -0
  111. package/dist/debrief-contract.js +151 -0
  112. package/dist/debrief-contract.js.map +1 -0
  113. package/dist/debrief-fixture.d.ts +16 -0
  114. package/dist/debrief-fixture.d.ts.map +1 -0
  115. package/dist/debrief-fixture.js +41 -0
  116. package/dist/debrief-fixture.js.map +1 -0
  117. package/dist/debrief-host.d.ts +59 -0
  118. package/dist/debrief-host.d.ts.map +1 -0
  119. package/dist/debrief-host.js +49 -0
  120. package/dist/debrief-host.js.map +1 -0
  121. package/dist/debrief-render.d.ts +5 -0
  122. package/dist/debrief-render.d.ts.map +1 -0
  123. package/dist/debrief-render.js +65 -0
  124. package/dist/debrief-render.js.map +1 -0
  125. package/dist/debrief.d.ts +90 -0
  126. package/dist/debrief.d.ts.map +1 -0
  127. package/dist/debrief.js +263 -0
  128. package/dist/debrief.js.map +1 -0
  129. package/dist/delegate-types.d.ts +2 -0
  130. package/dist/delegate-types.d.ts.map +1 -1
  131. package/dist/delegate-types.js.map +1 -1
  132. package/dist/delegate.d.ts.map +1 -1
  133. package/dist/delegate.js +1 -0
  134. package/dist/delegate.js.map +1 -1
  135. package/dist/dispatch-control.d.ts +45 -0
  136. package/dist/dispatch-control.d.ts.map +1 -0
  137. package/dist/dispatch-control.js +76 -0
  138. package/dist/dispatch-control.js.map +1 -0
  139. package/dist/effect-profile-runtime.d.ts +40 -0
  140. package/dist/effect-profile-runtime.d.ts.map +1 -0
  141. package/dist/effect-profile-runtime.js +51 -0
  142. package/dist/effect-profile-runtime.js.map +1 -0
  143. package/dist/effect-profile.d.ts +40 -0
  144. package/dist/effect-profile.d.ts.map +1 -0
  145. package/dist/effect-profile.js +152 -0
  146. package/dist/effect-profile.js.map +1 -0
  147. package/dist/execution-retention.d.ts +90 -0
  148. package/dist/execution-retention.d.ts.map +1 -0
  149. package/dist/execution-retention.js +247 -0
  150. package/dist/execution-retention.js.map +1 -0
  151. package/dist/experiment-contract.d.ts +56 -0
  152. package/dist/experiment-contract.d.ts.map +1 -0
  153. package/dist/experiment-contract.js +80 -0
  154. package/dist/experiment-contract.js.map +1 -0
  155. package/dist/experiment-state.d.ts +22 -0
  156. package/dist/experiment-state.d.ts.map +1 -0
  157. package/dist/experiment-state.js +79 -0
  158. package/dist/experiment-state.js.map +1 -0
  159. package/dist/experiment-store.d.ts +28 -0
  160. package/dist/experiment-store.d.ts.map +1 -0
  161. package/dist/experiment-store.js +130 -0
  162. package/dist/experiment-store.js.map +1 -0
  163. package/dist/experiment.d.ts +65 -0
  164. package/dist/experiment.d.ts.map +1 -0
  165. package/dist/experiment.js +357 -0
  166. package/dist/experiment.js.map +1 -0
  167. package/dist/factory-contract.d.ts +79 -0
  168. package/dist/factory-contract.d.ts.map +1 -0
  169. package/dist/factory-contract.js +89 -0
  170. package/dist/factory-contract.js.map +1 -0
  171. package/dist/factory-migration.d.ts +20 -0
  172. package/dist/factory-migration.d.ts.map +1 -0
  173. package/dist/factory-migration.js +55 -0
  174. package/dist/factory-migration.js.map +1 -0
  175. package/dist/factory-order.d.ts +848 -0
  176. package/dist/factory-order.d.ts.map +1 -0
  177. package/dist/factory-order.js +83 -0
  178. package/dist/factory-order.js.map +1 -0
  179. package/dist/factory-registry.d.ts +121 -0
  180. package/dist/factory-registry.d.ts.map +1 -0
  181. package/dist/factory-registry.js +133 -0
  182. package/dist/factory-registry.js.map +1 -0
  183. package/dist/file-lock.d.ts +8 -2
  184. package/dist/file-lock.d.ts.map +1 -1
  185. package/dist/file-lock.js +78 -19
  186. package/dist/file-lock.js.map +1 -1
  187. package/dist/herdr-poll.d.ts +8 -0
  188. package/dist/herdr-poll.d.ts.map +1 -1
  189. package/dist/herdr-poll.js +23 -0
  190. package/dist/herdr-poll.js.map +1 -1
  191. package/dist/index.d.ts +20 -0
  192. package/dist/index.d.ts.map +1 -1
  193. package/dist/index.js +20 -0
  194. package/dist/index.js.map +1 -1
  195. package/dist/intent-application.d.ts +17 -0
  196. package/dist/intent-application.d.ts.map +1 -0
  197. package/dist/intent-application.js +113 -0
  198. package/dist/intent-application.js.map +1 -0
  199. package/dist/intent-control.d.ts +201 -0
  200. package/dist/intent-control.d.ts.map +1 -0
  201. package/dist/intent-control.js +110 -0
  202. package/dist/intent-control.js.map +1 -0
  203. package/dist/intent-scheduling.d.ts +9 -0
  204. package/dist/intent-scheduling.d.ts.map +1 -0
  205. package/dist/intent-scheduling.js +15 -0
  206. package/dist/intent-scheduling.js.map +1 -0
  207. package/dist/ledger-append.d.ts +15 -0
  208. package/dist/ledger-append.d.ts.map +1 -0
  209. package/dist/ledger-append.js +94 -0
  210. package/dist/ledger-append.js.map +1 -0
  211. package/dist/ledger.d.ts +11 -0
  212. package/dist/ledger.d.ts.map +1 -1
  213. package/dist/ledger.js +3 -7
  214. package/dist/ledger.js.map +1 -1
  215. package/dist/native-session-target.d.ts +9 -0
  216. package/dist/native-session-target.d.ts.map +1 -0
  217. package/dist/native-session-target.js +29 -0
  218. package/dist/native-session-target.js.map +1 -0
  219. package/dist/native-session.d.ts +59 -0
  220. package/dist/native-session.d.ts.map +1 -0
  221. package/dist/native-session.js +154 -0
  222. package/dist/native-session.js.map +1 -0
  223. package/dist/order-schedule.d.ts +37 -0
  224. package/dist/order-schedule.d.ts.map +1 -0
  225. package/dist/order-schedule.js +81 -0
  226. package/dist/order-schedule.js.map +1 -0
  227. package/dist/ordinary-children.d.ts +52 -0
  228. package/dist/ordinary-children.d.ts.map +1 -0
  229. package/dist/ordinary-children.js +93 -0
  230. package/dist/ordinary-children.js.map +1 -0
  231. package/dist/producer-ipc-contract.d.ts +29 -0
  232. package/dist/producer-ipc-contract.d.ts.map +1 -0
  233. package/dist/producer-ipc-contract.js +29 -0
  234. package/dist/producer-ipc-contract.js.map +1 -0
  235. package/dist/producer-ipc.d.ts +55 -0
  236. package/dist/producer-ipc.d.ts.map +1 -0
  237. package/dist/producer-ipc.js +143 -0
  238. package/dist/producer-ipc.js.map +1 -0
  239. package/dist/resource-budget.d.ts +334 -0
  240. package/dist/resource-budget.d.ts.map +1 -0
  241. package/dist/resource-budget.js +415 -0
  242. package/dist/resource-budget.js.map +1 -0
  243. package/dist/retention-contract.d.ts +73 -0
  244. package/dist/retention-contract.d.ts.map +1 -0
  245. package/dist/retention-contract.js +104 -0
  246. package/dist/retention-contract.js.map +1 -0
  247. package/dist/retention-json.d.ts +3 -0
  248. package/dist/retention-json.d.ts.map +1 -0
  249. package/dist/retention-json.js +32 -0
  250. package/dist/retention-json.js.map +1 -0
  251. package/dist/run-child.d.ts +4 -0
  252. package/dist/run-child.d.ts.map +1 -1
  253. package/dist/run-child.js +16 -2
  254. package/dist/run-child.js.map +1 -1
  255. package/dist/run-herdr.d.ts +6 -0
  256. package/dist/run-herdr.d.ts.map +1 -1
  257. package/dist/run-herdr.js +19 -6
  258. package/dist/run-herdr.js.map +1 -1
  259. package/dist/vendor/adoption.d.ts +129 -0
  260. package/dist/vendor/adoption.d.ts.map +1 -0
  261. package/dist/vendor/adoption.js +102 -0
  262. package/dist/vendor/adoption.js.map +1 -0
  263. package/dist/work-ledger-destination.d.ts +20 -0
  264. package/dist/work-ledger-destination.d.ts.map +1 -0
  265. package/dist/work-ledger-destination.js +131 -0
  266. package/dist/work-ledger-destination.js.map +1 -0
  267. package/dist/work-ledger-json.d.ts +25 -0
  268. package/dist/work-ledger-json.d.ts.map +1 -0
  269. package/dist/work-ledger-json.js +267 -0
  270. package/dist/work-ledger-json.js.map +1 -0
  271. package/dist/work-ledger-occurrences.d.ts +227 -0
  272. package/dist/work-ledger-occurrences.d.ts.map +1 -0
  273. package/dist/work-ledger-occurrences.js +115 -0
  274. package/dist/work-ledger-occurrences.js.map +1 -0
  275. package/dist/work-ledger-projection.d.ts +295 -0
  276. package/dist/work-ledger-projection.d.ts.map +1 -0
  277. package/dist/work-ledger-projection.js +237 -0
  278. package/dist/work-ledger-projection.js.map +1 -0
  279. package/dist/work-ledger-snapshot.d.ts +29 -0
  280. package/dist/work-ledger-snapshot.d.ts.map +1 -0
  281. package/dist/work-ledger-snapshot.js +267 -0
  282. package/dist/work-ledger-snapshot.js.map +1 -0
  283. package/dist/work-ledger-types.d.ts +261 -0
  284. package/dist/work-ledger-types.d.ts.map +1 -0
  285. package/dist/work-ledger-types.js +13 -0
  286. package/dist/work-ledger-types.js.map +1 -0
  287. package/dist/work-ledger-validation.d.ts +14 -0
  288. package/dist/work-ledger-validation.d.ts.map +1 -0
  289. package/dist/work-ledger-validation.js +226 -0
  290. package/dist/work-ledger-validation.js.map +1 -0
  291. package/dist/work-ledger.d.ts +43 -0
  292. package/dist/work-ledger.d.ts.map +1 -0
  293. package/dist/work-ledger.js +225 -0
  294. package/dist/work-ledger.js.map +1 -0
  295. package/extensions/delegate-chain.ts +1 -0
  296. package/extensions/delegation-native.ts +12 -0
  297. package/extensions/delegation.ts +4 -3
  298. package/extensions/execute-child.ts +34 -4
  299. package/extensions/grants.ts +2 -0
  300. package/extensions/ordinary-runtime.ts +14 -0
  301. package/extensions/run-delegation.ts +7 -7
  302. package/extensions/session.ts +3 -1
  303. package/package.json +78 -4
  304. package/src/check-runner.ts +20 -2
  305. package/src/control-journal.ts +26 -0
  306. package/src/daily-view-input.ts +59 -0
  307. package/src/daily-view-render.ts +44 -0
  308. package/src/daily-view.ts +152 -0
  309. package/src/dashboard-cli.ts +103 -4
  310. package/src/dashboard-harness.ts +28 -0
  311. package/src/dashboard-herdr.ts +7 -2
  312. package/src/dashboard-host-contract.ts +42 -0
  313. package/src/dashboard-host-transport.ts +38 -0
  314. package/src/dashboard-host.ts +168 -0
  315. package/src/dashboard-observation.ts +44 -0
  316. package/src/debrief-contract.ts +127 -0
  317. package/src/debrief-fixture.ts +29 -0
  318. package/src/debrief-host.ts +53 -0
  319. package/src/debrief-render.ts +49 -0
  320. package/src/debrief.ts +198 -0
  321. package/src/delegate-types.ts +2 -0
  322. package/src/delegate.ts +1 -0
  323. package/src/dispatch-control.ts +81 -0
  324. package/src/effect-profile-runtime.ts +46 -0
  325. package/src/effect-profile.ts +129 -0
  326. package/src/execution-retention.ts +230 -0
  327. package/src/experiment-contract.ts +66 -0
  328. package/src/experiment-state.ts +44 -0
  329. package/src/experiment-store.ts +90 -0
  330. package/src/experiment.ts +231 -0
  331. package/src/factory-contract.ts +47 -0
  332. package/src/factory-migration.ts +29 -0
  333. package/src/factory-order.ts +56 -0
  334. package/src/factory-registry.ts +60 -0
  335. package/src/file-lock.ts +47 -11
  336. package/src/herdr-poll.ts +18 -0
  337. package/src/index.ts +32 -0
  338. package/src/intent-application.ts +90 -0
  339. package/src/intent-control.ts +107 -0
  340. package/src/intent-scheduling.ts +14 -0
  341. package/src/ledger-append.ts +72 -0
  342. package/src/ledger.ts +3 -9
  343. package/src/native-session-target.ts +23 -0
  344. package/src/native-session.ts +139 -0
  345. package/src/order-schedule.ts +42 -0
  346. package/src/ordinary-children.ts +59 -0
  347. package/src/producer-ipc-contract.ts +26 -0
  348. package/src/producer-ipc.ts +107 -0
  349. package/src/resource-budget.ts +349 -0
  350. package/src/retention-contract.ts +84 -0
  351. package/src/retention-json.ts +20 -0
  352. package/src/run-child.ts +13 -2
  353. package/src/run-herdr.ts +11 -7
  354. package/src/vendor/adoption.ts +109 -0
  355. package/src/work-ledger-destination.ts +113 -0
  356. package/src/work-ledger-json.ts +228 -0
  357. package/src/work-ledger-occurrences.ts +99 -0
  358. package/src/work-ledger-projection.ts +262 -0
  359. package/src/work-ledger-snapshot.ts +246 -0
  360. package/src/work-ledger-types.ts +229 -0
  361. package/src/work-ledger-validation.ts +174 -0
  362. package/src/work-ledger.ts +186 -0
@@ -0,0 +1,231 @@
1
+ import { randomUUID } from "node:crypto";
2
+ import { join } from "node:path";
3
+ import { MAX_CHILDREN_PER_CALL } from "./fanout.ts";
4
+ import { openResourceBudget, resourceBindingDigest, type ExperimentBudgetBinding, type BudgetSnapshot } from "./resource-budget.ts";
5
+ import { isDigestProfile, reserveDigestProfileBatch, type DigestProfile, type ReservedDigestRun } from "./effect-profile.ts";
6
+ import { readIntentWork, resolveIntent } from "./intent-application.ts";
7
+ import { freezeWork } from "./work-ledger-json.ts";
8
+ import { byteHash, cloneExperiment, experimentAuthority, experimentCancellation, experimentCancellationDigest, experimentCharter, experimentCharterDigest,
9
+ type ExperimentAuthority, type ExperimentCancellation, type ExperimentCharter } from "./experiment-contract.ts";
10
+ import { createExperimentStore, experimentBindingDigest, experimentStore, ownedDirectory, readExperimentFile, writeExperimentFile, type ExperimentBinding } from "./experiment-store.ts";
11
+ import { evaluateOrder, factoryDecision, factoryDecisionDigest, type FactoryDecision } from "./order-schedule.ts";
12
+ import { replayExperiment, type VariantRecord } from "./experiment-state.ts";
13
+ export { experimentCharterDigest, experimentCancellationDigest, parseExperimentCharter, type ExperimentCharter, type ExperimentAuthority, type ExperimentCancellation } from "./experiment-contract.ts";
14
+ export { experimentBindingDigest, type ExperimentBinding } from "./experiment-store.ts";
15
+ export interface ExperimentView {
16
+ version: "experiment-view-v1"; experimentId: string; mode: ExperimentCharter["mode"]; variants: readonly VariantRecord[];
17
+ budget: Readonly<BudgetSnapshot> | null; cancellations: readonly { request: ExperimentCancellation; outcome: "requested-or-unknown" | "observed-cancelled" | "finished-without-cancel" }[];
18
+ control: "not-assessed" | "failed" | "unknown"; diagnostics: readonly string[]; acceptance: "not-assessed"; configuration: { model: null; effort: null; skills: null }; freshness: "snapshot-unknown";
19
+ }
20
+ function approved(a: ExperimentAuthority | null, b: ExperimentBudgetBinding, c: ExperimentCharter) {
21
+ if (!a || a.authorityDigest !== b.authorityDigest || !a.charterDigests.includes(experimentCharterDigest(c))) throw new Error("independent exact experiment authority unavailable");
22
+ }
23
+ async function common(c: ExperimentCharter, bytes: Uint8Array) {
24
+ if (bytes.length !== c.common.bytes || byteHash(bytes) !== c.common.sha256) throw new Error("common input drift");
25
+ if (c.common.work) {
26
+ const text = await readIntentWork(c.common.work);
27
+ if (byteHash(text) !== c.common.workTextDigest) throw new Error("common P01 work drift");
28
+ resolveIntent(text, c.common.work.selection, c.common.work.priorities);
29
+ }
30
+ }
31
+ export async function createExperiment(input: { directory: string; budget: ExperimentBudgetBinding; charter: ExperimentCharter; bytes: Uint8Array; authority: ExperimentAuthority | null }): Promise<ExperimentBinding> {
32
+ if (!(input.bytes instanceof Uint8Array) || input.bytes.length > 16384) throw new TypeError("bounded common bytes required");
33
+ const c = experimentCharter(input.charter), budget = openResourceBudget(input.budget).binding, a = experimentAuthority(input.authority), bytes = Buffer.from(input.bytes), directory = input.directory;
34
+ if (budget.version !== "4.0" || resourceBindingDigest(budget) !== c.budgetDigest) throw new Error("explicit experiment budget required");
35
+ approved(a, budget, c); await common(c, bytes); await openResourceBudget(budget).inspect();
36
+ return createExperimentStore(directory, budget, c, bytes);
37
+ }
38
+ export interface ExperimentRun { readonly primary: Promise<VariantRecord>; readonly completion: Promise<ExperimentView>; readonly boundary: Promise<ExperimentView>; readonly started: readonly Promise<"spawned" | "settled-without-spawn">[] }
39
+ const controllers = new WeakSet<object>();
40
+ export const isExperimentController = (value: unknown): value is ReturnType<typeof openExperiment> => typeof value === "object" && value !== null && controllers.has(value);
41
+ /** A separate owned controller, not delegate_all and not a model-facing launch/callback loader. */
42
+ export function openExperiment(input: ExperimentBinding, hostAuthority: ExperimentAuthority | null) {
43
+ const store = experimentStore(input), b = store.binding, c = b.charter, a = experimentAuthority(hostAuthority), owner = randomUUID(), digest = experimentBindingDigest(b);
44
+ const budget = openResourceBudget(b.budget), handles = new Map<string, AbortController>();
45
+ let live: ExperimentRun | null = null, starting = false, wake: (() => void) | null = null, epoch = 0;
46
+ let fault = false, failureAcknowledged: boolean | null = null;
47
+ const notify = () => { epoch++; wake?.(); wake = null; };
48
+ const artifact = (id: string) => join(b.directory, "variant-" + byteHash(id), "result.json");
49
+ const load = async () => {
50
+ const data = await store.read(), state = replayExperiment(c, data.events);
51
+ if (state.cancellations.some(r => r.bindingDigest !== digest)) throw new Error("cancellation binding mismatch");
52
+ return state;
53
+ };
54
+ const append = (event: unknown) => store.transaction(async (events, write) => { replayExperiment(c, [...events, cloneExperiment(event) as Record<string, unknown>]); await write(event); });
55
+ const unknown = (): ExperimentView => ({ version: "experiment-view-v1", experimentId: c.experimentId, mode: c.mode,
56
+ variants: c.variants.map(v => ({ variantId: v.variantId, kind: v.kind, operation: v.operation, executionId: v.executionId, state: "unknown", artifactDigest: null, spawned: false })), budget: null, cancellations: [], control: "unknown", diagnostics: ["controller-or-storage-unknown", ...(fault ? ["controller-failure-observed"] : [])], acceptance: "not-assessed", configuration: { model: null, effort: null, skills: null }, freshness: "snapshot-unknown" });
57
+ const inspect = async (control = false): Promise<ExperimentView> => {
58
+ const state = await load(), diagnostics: string[] = [], accounting = await (control ? budget.controlSnapshot() : budget.inspect());
59
+ if (state.controllerFailed) diagnostics.push("controller-failure-record-observed");
60
+ if (fault) diagnostics.push("controller-failure-observed");
61
+ if (failureAcknowledged === false) diagnostics.push("controller-failure-recording-unacknowledged");
62
+ for (const v of state.variants) {
63
+ if (v.artifactDigest) {
64
+ try { await ownedDirectory(join(b.directory, "variant-" + byteHash(v.executionId))); if (byteHash(await readExperimentFile(artifact(v.executionId), 16384)) !== v.artifactDigest) throw new Error("artifact mismatch"); }
65
+ catch { diagnostics.push("artifact-unavailable:" + v.executionId); v.state = "unknown"; v.artifactDigest = null; }
66
+ } else if (state.claim && (!handles.has(v.executionId) || state.claim.owner !== owner)) v.state = "unknown";
67
+ }
68
+ return freezeWork({ version: "experiment-view-v1", experimentId: c.experimentId, mode: c.mode, variants: state.variants, budget: accounting,
69
+ cancellations: state.cancellations.map(request => { const v = state.variants.find(v => v.executionId === request.executionId)!; return { request,
70
+ outcome: v.state === "cancelled" ? "observed-cancelled" : v.artifactDigest ? "finished-without-cancel" : "requested-or-unknown" }; }),
71
+ control: state.controllerFailed || fault ? "failed" : "not-assessed", diagnostics, acceptance: "not-assessed", configuration: { model: null, effort: null, skills: null }, freshness: "snapshot-unknown" }) as ExperimentView;
72
+ };
73
+ const computeOrder = async (control = false) => {
74
+ if (!c.order) throw new Error("not an order controller");
75
+ const view = await inspect(control), state = await load(), digests = new Map<string,string|null>();
76
+ for (const v of view.variants) if (v.artifactDigest) {
77
+ try { const bytes=await readExperimentFile(artifact(v.executionId),16384); if(byteHash(bytes)!==v.artifactDigest)throw new Error("changed artifact");const result=JSON.parse(bytes.toString());
78
+ if(result.attemptId!==v.executionId)throw new Error("wrong execution artifact"); digests.set(v.executionId, typeof result.digest==="string"&&result.output?.code===0&&!result.output.aborted&&!result.output.timedOut&&!result.output.spawnError?result.digest:null);
79
+ } catch { /* frozen view: use a detached unknown row below */ digests.set(v.executionId,"unavailable"); }
80
+ }
81
+ const variants=view.variants.map(v=>digests.get(v.executionId)==="unavailable"?{...v,state:"unknown" as const}:v);
82
+ return { view, nodes:evaluateOrder(c.order,variants,digests,state.decisions).map(n => view.control === "not-assessed" ? n : {...n, action:"stakeholder" as const}), superseded:state.superseded };
83
+ };
84
+ const orderView = () => computeOrder(false);
85
+ const api = Object.freeze({ binding: b, inspect: () => inspect(false), reconcile: () => inspect(false), orderView,
86
+ async decideOrder(input: FactoryDecision, authority: { id: string; digests: readonly string[] } | null) {
87
+ const request=factoryDecision(input); approved(a,b.budget,c);
88
+ if(!authority||authority.id!==request.authorityId||!authority.digests.includes(factoryDecisionDigest(request))||request.bindingDigest!==digest)throw new Error("independent decision authority required");
89
+ const fresh=await store.transaction(async(events,write)=>{
90
+ const state=replayExperiment(c,events),old=state.decisions.find(d=>d.requestId===request.requestId);
91
+ if(old){if(factoryDecisionDigest(old)!==factoryDecisionDigest(request))throw new Error("conflicting decision ID");return false;}
92
+ await common(c,await readExperimentFile(join(b.directory,"common.bin"),16384));
93
+ const row=(await computeOrder(true)).nodes.find(n=>n.nodeId===request.nodeId);
94
+ if(!row||row.state!=="decision-required"||row.evidenceDigest!==request.evidenceDigest)throw new Error("stale decision or unavailable evidence");
95
+ replayExperiment(c,[...events,{type:"order-decision",request}]);await write({type:"order-decision",request});return true;
96
+ });if(fresh)notify();return orderView();
97
+ },
98
+ async readArtifact(executionId: string): Promise<Uint8Array> {
99
+ const row = (await load()).variants.find(v => v.executionId === executionId);
100
+ if (!row?.artifactDigest) throw new Error("exact byte-backed artifact unavailable");
101
+ await ownedDirectory(join(b.directory, "variant-" + byteHash(executionId)));
102
+ const bytes = await readExperimentFile(artifact(executionId), 16384);
103
+ if (byteHash(bytes) !== row.artifactDigest) throw new Error("artifact digest mismatch");
104
+ return Uint8Array.from(bytes);
105
+ },
106
+ async cancel(input: ExperimentCancellation, hostAuthority?: ExperimentAuthority | null): Promise<ExperimentView> {
107
+ const request = experimentCancellation(input), cancellationAuthority = hostAuthority === undefined ? a : experimentAuthority(hostAuthority);
108
+ approved(cancellationAuthority, b.budget, c);
109
+ if (request.bindingDigest !== digest || !c.variants.some(v => v.executionId === request.executionId) || !cancellationAuthority!.cancellationDigests.includes(experimentCancellationDigest(request))) throw new Error("independent exact cancellation authority unavailable");
110
+ const fresh = await store.transaction(async (events, write) => {
111
+ const state = replayExperiment(c, events), old = state.cancellations.find(r => r.requestId === request.requestId);
112
+ if (old) { if (experimentCancellationDigest(old) !== experimentCancellationDigest(request)) throw new Error("conflicting cancellation ID"); return false; }
113
+ replayExperiment(c, [...events, { type: "cancel", request }]); await write({ type: "cancel", request }); return true;
114
+ });
115
+ if (fresh) handles.get(request.executionId)?.abort("operator-cancellation"); // Only an original live handle; no PID/idle/pane lookup.
116
+ // Cancellation is a control operation: serialize its readback with controller appends
117
+ // and original budget settlement. Public inspect/reconcile remain unlocked, fallible reads.
118
+ return store.transaction(async () => inspect(true));
119
+ },
120
+ async start(profile: DigestProfile): Promise<ExperimentRun> {
121
+ approved(a, b.budget, c);
122
+ if (!isDigestProfile(profile) || profile.bindingDigest !== c.budgetDigest) throw new Error("profile budget mismatch");
123
+ if (live) return live;
124
+ if (starting) throw new Error("controller start pending; inspect, do not retry effects"); starting = true;
125
+ let reserved: readonly ReservedDigestRun[] = [];
126
+ try {
127
+ const bytes = await readExperimentFile(join(b.directory, "common.bin"), 16384); await common(c, bytes);
128
+ const claimed = await store.transaction(async (events, write) => {
129
+ const state = replayExperiment(c, events); if (state.superseded) throw new Error("order migrated; no old dispatch"); if (state.claim) return false;
130
+ await write({ type: "claim", owner, deadlineAt: Date.now() + c.deadlineMs }); return true;
131
+ });
132
+ if (!claimed) {
133
+ const view = await inspect(); return Object.freeze({ primary: Promise.resolve(view.variants[0]), completion: Promise.resolve(view), boundary: Promise.resolve(view), started: [] });
134
+ }
135
+ reserved = await reserveDigestProfileBatch(profile, c.variants.map(v => ({ attempt: { attemptId: v.executionId, orderId: c.orderId, experimentId: c.experimentId, kind: v.kind, parentAttemptId: v.parentExecutionId }, bytes: Buffer.concat([bytes, Buffer.from(v.suffixBase64, "base64")]), operation: v.operation })));
136
+ await append({ type: "admitted" });
137
+ const state = await load(), used = new Set<number>();
138
+ let resolvePrimary!: (value: VariantRecord) => void;
139
+ const primary = { promise: new Promise<VariantRecord>(resolve => { resolvePrimary = resolve; }), resolve: (value: VariantRecord) => resolvePrimary(value) };
140
+ for (const v of c.variants) { const handle = new AbortController(); handles.set(v.executionId, handle); if (state.cancellations.some(r => r.executionId === v.executionId)) handle.abort("operator-cancellation"); }
141
+ // Close the read-to-handle-install race without adding a control/observation transport.
142
+ for (const request of (await load()).cancellations) handles.get(request.executionId)?.abort("operator-cancellation");
143
+ const abort = (reason = "controller-failure") => { handles.forEach(h => h.abort(reason)); notify(); };
144
+ const timer = setTimeout(() => abort("deadline"), Math.max(1, state.claim!.deadlineAt - Date.now()));
145
+ const settled = c.variants.map(() => {
146
+ let resolve!: (success: boolean) => void;
147
+ return { promise: new Promise<boolean>(done => { resolve = done; }), done: (success: boolean) => resolve(success) };
148
+ });
149
+ const one = async (i: number) => {
150
+ const v = c.variants[i]; let success = false, launched = false, primaryResult: VariantRecord | null = null;
151
+ try {
152
+ if (!c.order && v.kind === "retry" && await settled[c.variants.findIndex(p => p.executionId === v.parentExecutionId)].promise) handles.get(v.executionId)!.abort("retry-not-needed");
153
+ await append({ type: "dispatch", executionId: v.executionId });
154
+ used.add(i);
155
+ launched = true;
156
+ const running = reserved[i].run(handles.get(v.executionId)!.signal);
157
+ // Consume both promises even on bookkeeping failure. No untracked rejection or worker.
158
+ const observed = reserved[i].started.then(async value => { if (value === "spawned") await append({ type: "spawn", executionId: v.executionId }); });
159
+ const [result, observation] = await Promise.allSettled([running, observed]);
160
+ if (result.status === "rejected" || observation.status === "rejected") throw new Error("runtime/accounting/observation unknown");
161
+ const data = Buffer.from(JSON.stringify(result.value) + "\n"), out = result.value.output;
162
+ const cancelled = out.aborted && (Boolean(out.signal) || await reserved[i].started === "settled-without-spawn");
163
+ await ownedDirectory(join(b.directory, "variant-" + byteHash(v.executionId))); await writeExperimentFile(artifact(v.executionId), data);
164
+ const state = out.timedOut || cancelled && handles.get(v.executionId)?.signal.reason === "deadline" ? "timed-out" : cancelled ? "cancelled" : result.value.digest ? "completed" : "failed";
165
+ // Original worker/artifact outcome, NOT a successful controller acknowledgement.
166
+ primaryResult = {variantId:v.variantId,kind:v.kind,operation:v.operation,executionId:v.executionId,state,artifactDigest:byteHash(data),spawned:await reserved[i].started === "spawned"};
167
+ await append({ type: "result", executionId: v.executionId, artifactDigest: byteHash(data), state });
168
+ success = Boolean(result.value.digest);
169
+ } catch {
170
+ fault = true; abort();
171
+ // Failed dispatch acknowledgement still consumes the ORIGINAL unused permit without
172
+ // spawning and resolves started, including when the control journal stays locked.
173
+ if (!launched) { used.add(i); await reserved[i].run(AbortSignal.abort()).catch(() => {}); }
174
+ }
175
+ finally {
176
+ settled[i].done(success); handles.delete(v.executionId);
177
+ if (i === 0) primary.resolve(primaryResult ? freezeWork(primaryResult) : unknown().variants[0]);
178
+ }
179
+ };
180
+ let resolveBoundary!: (view: ExperimentView) => void;
181
+ const boundary = new Promise<ExperimentView>(resolve => { resolveBoundary = resolve; });
182
+ const orderRunning = new Set<Promise<void>>();
183
+ const completion = (async () => {
184
+ try {
185
+ if(c.order) {
186
+ const running=orderRunning;
187
+ while(true) {
188
+ const before=epoch; await common(c,await readExperimentFile(join(b.directory,"common.bin"),16384));
189
+ const status=await store.transaction(async()=>computeOrder(true));
190
+ if(fault || Date.now()>=state.claim!.deadlineAt) { abort("deadline"); await Promise.all(running); break; }
191
+ const eligible=status.nodes.filter(n=>n.action==="dispatch").map(n=>c.variants.findIndex(v=>v.executionId===n.executionId)).filter(i=>!used.has(i));
192
+ for(const i of eligible.slice(0,MAX_CHILDREN_PER_CALL-running.size)) {
193
+ used.add(i); const task=one(i); running.add(task); void task.then(()=>{running.delete(task);notify();},()=>{running.delete(task);notify();});
194
+ }
195
+ if(running.size){await Promise.race(running);continue;}
196
+ if(!status.nodes.some(n=>n.state==="decision-required"))break;
197
+ resolveBoundary(status.view);
198
+ if(epoch!==before)continue;
199
+ await new Promise<void>(resolve=>{wake=resolve;});
200
+ }
201
+ } else for (let i = 0; i < c.variants.length; i += MAX_CHILDREN_PER_CALL) {
202
+ await common(c, await readExperimentFile(join(b.directory, "common.bin"), 16384));
203
+ await Promise.all(c.variants.slice(i, i + MAX_CHILDREN_PER_CALL).map((_, n) => one(i + n)));
204
+ if (fault) break;
205
+ }
206
+ } catch { fault = true; abort(); }
207
+ finally {
208
+ clearTimeout(timer); abort(); await Promise.allSettled(orderRunning);
209
+ for (let i = 0; i < reserved.length; i++) if (!used.has(i)) {
210
+ if(c.order) { handles.get(c.variants[i].executionId)?.abort("unused-order-reservation"); await one(i); }
211
+ else { used.add(i); await reserved[i].run(AbortSignal.abort()).catch(() => { fault = true; }); }
212
+ }
213
+ handles.clear();
214
+ if (fault) {
215
+ try { await append({ type: "controller-unknown" }); failureAcknowledged = true; }
216
+ catch { failureAcknowledged = false; } // No fabricated durable acknowledgement.
217
+ }
218
+ }
219
+ const view = await inspect().catch(unknown); primary.resolve(view.variants[0]); resolveBoundary(view); return view;
220
+ })();
221
+ live = Object.freeze({ primary: primary.promise, completion, boundary: c.order ? boundary : completion, started: Object.freeze(reserved.map(r => r.started)) }); return live;
222
+ } catch (error) {
223
+ fault = true;
224
+ for (const run of reserved) await run.run(AbortSignal.abort()).catch(() => {});
225
+ const state = await load().catch(() => null); if (state?.claim?.owner === owner) await append({ type: "controller-unknown" }).catch(() => {});
226
+ throw error;
227
+ } finally { starting = false; }
228
+ },
229
+ });
230
+ controllers.add(api); return api;
231
+ }
@@ -0,0 +1,47 @@
1
+ import { cloneExperiment, closed, experimentHash, experimentCharter, byteHash, type ExperimentCharter } from "./experiment-contract.ts";
2
+ import { resourceBindingDigest, type ExperimentBudgetBinding } from "./resource-budget.ts";
3
+ import { workIntentBinding, readIntentWork, resolveIntent } from "./intent-application.ts";
4
+ import type { WorkIntentBinding } from "./intent-control.ts";
5
+ import type { WorkFrozen, RevisionRef } from "./work-ledger-types.ts";
6
+ import type { AdoptionAuthority, AdoptionFacts, AdoptionBinding, AdoptionReceipt } from "./vendor/adoption.ts";
7
+ import { parseWorkJson } from "./work-ledger-json.ts";
8
+ import { orderSchedule } from "./order-schedule.ts";
9
+ export { factoryDecisionDigest, type FactoryDecision } from "./order-schedule.ts";
10
+ export interface FixedPolicy {version:"fixed-policy-v1";suffixBase64:string;acceptancePolicyDigest:string;grants:readonly string[];effects:readonly "fixed-digest"[];model:null;effort:null;skills:readonly string[]}
11
+ export const fixedPolicyDigest=(p:FixedPolicy)=>experimentHash(fixedPolicy(p));
12
+ const hex=(x:unknown)=>typeof x==="string"&&/^[a-f0-9]{64}$/.test(x);
13
+ export function fixedPolicy(value:FixedPolicy):FixedPolicy{const p=cloneExperiment(value);closed(p,["version","suffixBase64","acceptancePolicyDigest","grants","effects","model","effort","skills"]);
14
+ if(p.version!=="fixed-policy-v1"||!hex(p.acceptancePolicyDigest)||typeof p.suffixBase64!=="string"||p.suffixBase64.length>2048||Buffer.from(p.suffixBase64,"base64").toString("base64")!==p.suffixBase64||JSON.stringify(p.grants)!=="[]"||JSON.stringify(p.effects)!=='["fixed-digest"]'||p.model!==null||p.effort!==null||JSON.stringify(p.skills)!=="[]")throw new Error("unsupported policy or grant expansion");return p;}
15
+ export interface FactoryOrderCharter {version:"factory-order-v1";orderId:string;directory:string;budget:ExperimentBudgetBinding;work:WorkFrozen<WorkIntentBinding>;workTextDigest:string;scopeDigest:string;commonBase64:string;deadlineMs:number;pin:{revision:number;candidateDigest:string};nodes:readonly {nodeId:string;obligation:RevisionRef;dependencies:readonly string[];attempts:readonly {executionId:string;suffixBase64:string;operation:"digest"|"hold"}[];expectedDigest:string;decision:{decisionId:string;authorityId:string}|null}[]}
16
+ /** Digest of the entire bounded proposal; validation at application is separate. */
17
+ export const factoryOrderDigest=(c:FactoryOrderCharter)=>experimentHash(c);
18
+ export interface FactoryAuthority {id:string;orderDigests:readonly string[];decisionDigests:readonly string[];cancellationDigests?:readonly string[];activationDigests:readonly string[];migrationDigests:readonly string[];adoption:AdoptionAuthority|null;facts:readonly {bindingId:string;facts:AdoptionFacts}[]}
19
+ export function factoryAuthority(value:FactoryAuthority|null):FactoryAuthority|null{if(value===null)return null;const a=cloneExperiment(value);closed(a,["id","orderDigests","decisionDigests","activationDigests","migrationDigests","adoption","facts",...(Object.hasOwn(a,"cancellationDigests")?["cancellationDigests"]:[])]);
20
+ if(typeof a.id!=="string"||!/^[a-zA-Z0-9:_-]{1,128}$/.test(a.id)||[a.orderDigests,a.decisionDigests,a.activationDigests,a.migrationDigests,...(Object.hasOwn(a,"cancellationDigests")?[a.cancellationDigests!]:[])].some(xs=>!Array.isArray(xs)||xs.length>128||xs.some(x=>!hex(x)))||!Array.isArray(a.facts)||a.facts.length>128)throw new Error("invalid independent factory authority");
21
+ const ids=new Set<string>();for(const f of a.facts as FactoryAuthority["facts"]){closed(f,["bindingId","facts"]);if(!hex(f.bindingId)||ids.has(f.bindingId))throw new Error("ambiguous independent facts");ids.add(f.bindingId);}
22
+ if(a.adoption!==null){closed(a.adoption,["id","adoptions","rollbacks"]);if(a.adoption.id!==a.id)throw new Error("independent authority identity mismatch");}
23
+ return a;}
24
+ export function requireFactoryAuthority(a:FactoryAuthority|null,id:string){if(!a||a.id!==id)throw new Error("independent factory authority required");return a;}
25
+ export interface ActivationRequest {version:"factory-activation-v1";requestId:string;expectedRevision:number;expectedCandidateDigest:string;candidate:FixedPolicy;binding:AdoptionBinding;receipt:AdoptionReceipt}
26
+ export const activationRequestDigest=(r:ActivationRequest)=>experimentHash(r);
27
+ export function factoryCharter(value:FactoryOrderCharter):FactoryOrderCharter{
28
+ const c=cloneExperiment(value);closed(c,["version","orderId","directory","budget","work","workTextDigest","scopeDigest","commonBase64","deadlineMs","pin","nodes"]);closed(c.pin,["revision","candidateDigest"]);
29
+ if(c.version!=="factory-order-v1"||!/^[a-zA-Z0-9:_-]{1,128}$/.test(c.orderId)||typeof c.directory!=="string"||!hex(c.workTextDigest)||!hex(c.scopeDigest)||!hex(c.pin.candidateDigest)||!Number.isSafeInteger(c.pin.revision)||c.pin.revision<0||typeof c.commonBase64!=="string"||c.commonBase64.length>22000||Buffer.from(c.commonBase64,"base64").toString("base64")!==c.commonBase64||Buffer.byteLength(JSON.stringify(c))>48000||c.budget.version!=="4.0")throw new Error("invalid bounded factory charter");
30
+ c.work=workIntentBinding(JSON.parse(JSON.stringify(c.work)));if(c.scopeDigest!==experimentHash(c.work.selection.snapshot))throw new Error("changed scope");
31
+ if(!Array.isArray(c.nodes)||!c.nodes.length||c.nodes.length>16)throw new Error("bounded nodes required");
32
+ for(const n of c.nodes as FactoryOrderCharter["nodes"]){closed(n,["nodeId","obligation","dependencies","attempts","expectedDigest","decision"]);if(!Array.isArray(n.attempts)||!n.attempts.length||n.attempts.length>3)throw new Error("bounded recovery required");for(const a of n.attempts){closed(a,["executionId","suffixBase64","operation"]);if(typeof a.suffixBase64!=="string"||a.suffixBase64.length>22000||Buffer.from(a.suffixBase64,"base64").toString("base64")!==a.suffixBase64)throw new Error("invalid attempt byte suffix");}}
33
+ orderSchedule({version:"order-schedule-v1",policyDigest:c.pin.candidateDigest,nodes:(c.nodes as FactoryOrderCharter["nodes"]).map(n=>({nodeId:n.nodeId,dependencies:n.dependencies,executions:n.attempts.map(a=>a.executionId),expectedDigest:n.expectedDigest,decision:n.decision}))},(c.nodes as FactoryOrderCharter["nodes"]).flatMap(n=>n.attempts.map(a=>a.executionId)));
34
+ return c;
35
+ }
36
+ export const parseFactoryOrder = (text:string) => factoryCharter(parseWorkJson(text) as unknown as FactoryOrderCharter);
37
+ export async function compileFactoryOrder(value:FactoryOrderCharter,candidate:FixedPolicy):Promise<ExperimentCharter>{
38
+ const c=factoryCharter(value),p=fixedPolicy(candidate),text=await readIntentWork(c.work);
39
+ if(byteHash(text)!==c.workTextDigest||fixedPolicyDigest(p)!==c.pin.candidateDigest)throw new Error("work or candidate drift");
40
+ const intent=resolveIntent(text,c.work.selection,c.work.priorities),refs=intent.snapshot!.bindings.map(b=>experimentHash(b.obligation)).sort();
41
+ if(JSON.stringify(refs)!==JSON.stringify(c.nodes.map(n=>experimentHash(n.obligation)).sort()))throw new Error("order must cover exact selected obligations");
42
+ for(const n of c.nodes){const rev=intent.revisions.find(r=>experimentHash({kind:r.kind,id:r.id,revision:r.revision,digest:r.digest})===experimentHash(n.obligation));if(!rev||!rev.permittedEffects.includes("read"))throw new Error("unsupported obligation effects");for(const dep of rev.dependencies.filter(d=>d.kind==="obligation")){const parent=c.nodes.find(m=>experimentHash(m.obligation)===experimentHash(dep));if(!parent||!n.dependencies.includes(parent.nodeId))throw new Error("required intent dependency omitted");}}
43
+ const bytes=Buffer.from(c.commonBase64,"base64");
44
+ return experimentCharter({version:"fixed-experiment-v2",experimentId:c.orderId,orderId:c.orderId,budgetDigest:resourceBindingDigest(c.budget),profile:"linux-bwrap-digest-v1",common:{bytes:bytes.length,sha256:byteHash(bytes),work:c.work,workTextDigest:c.workTextDigest},mode:"bounded-waves",deadlineMs:c.deadlineMs,
45
+ variants:c.nodes.flatMap(n=>n.attempts.map((a,i)=>({variantId:a.executionId,executionId:a.executionId,kind:i?"retry" as const:"primary" as const,parentExecutionId:i?n.attempts[i-1].executionId:null,suffixBase64:Buffer.concat([Buffer.from(p.suffixBase64,"base64"),Buffer.from(a.suffixBase64,"base64")]).toString("base64"),operation:a.operation,configuration:{model:null,effort:null,skills:null}}))),
46
+ order:{version:"order-schedule-v1",policyDigest:p.acceptancePolicyDigest,nodes:c.nodes.map(n=>({nodeId:n.nodeId,dependencies:n.dependencies,executions:n.attempts.map(a=>a.executionId),expectedDigest:n.expectedDigest,decision:n.decision}))}});
47
+ }
@@ -0,0 +1,29 @@
1
+ import { cloneExperiment, closed, experimentHash } from "./experiment-contract.ts";
2
+ import { experimentStore } from "./experiment-store.ts";
3
+ import { replayExperiment } from "./experiment-state.ts";
4
+ import { factoryAuthority, factoryCharter, factoryOrderDigest, requireFactoryAuthority, type FactoryAuthority, type FactoryOrderCharter } from "./factory-contract.ts";
5
+ import { factoryRegistryStore, replayRegistry, type FactoryRegistryBinding } from "./factory-registry.ts";
6
+ import { createFactoryOrder } from "./factory-order.ts";
7
+ export interface FactoryMigration {version:"factory-migration-v1";requestId:string;sourceOrderId:string;successor:FactoryOrderCharter}
8
+ export const factoryMigrationDigest=(request:FactoryMigration)=>experimentHash(request);
9
+ /** Only unstarted controllers can be superseded. An original claim is a refusal, never an idle/PID guess. */
10
+ export async function migrateFactoryOrder(inputRegistry:FactoryRegistryBinding,input:FactoryMigration,host:FactoryAuthority|null){
11
+ const b=factoryRegistryStore(inputRegistry).binding;
12
+ const r=cloneExperiment(input),a=requireFactoryAuthority(factoryAuthority(host),b.initial.authorityId);closed(r,["version","requestId","sourceOrderId","successor"]);
13
+ if(r.version!=="factory-migration-v1"||!/^[a-zA-Z0-9:_-]{1,128}$/.test(r.requestId)||r.sourceOrderId===r.successor.orderId||!a.migrationDigests.includes(factoryMigrationDigest(r)))throw new Error("independent migration authority required");
14
+ r.successor=factoryCharter(r.successor);if(!a.orderDigests.includes(factoryOrderDigest(r.successor)))throw new Error("successor order authority required");
15
+ const store=factoryRegistryStore(b);let fresh=false;
16
+ await store.transaction(async(events,append)=>{const s=replayRegistry(b,events),old=s.changes.get(r.requestId);if(old){if(old!==factoryMigrationDigest(r))throw new Error("conflicting migration ID");return;}
17
+ const source=s.orders.get(r.sourceOrderId);if(!source?.binding||!a.orderDigests.includes(factoryOrderDigest(source.charter)))throw new Error("source order authority unavailable");
18
+ if(r.successor.scopeDigest!==b.initial.scopeDigest||r.successor.pin.revision!==s.revision||r.successor.pin.candidateDigest!==s.candidateDigest||s.orders.has(r.successor.orderId))throw new Error("stale migration target");
19
+ const e={type:"migration-request",requestId:r.requestId,sourceOrderId:r.sourceOrderId,targetOrderId:r.successor.orderId,requestDigest:factoryMigrationDigest(r)};replayRegistry(b,[...events,e]);await append(e);fresh=true;
20
+ });
21
+ if(fresh){const s=replayRegistry(b,(await store.read()).events),source=s.orders.get(r.sourceOrderId)!,binding=source.binding!,old=experimentStore(binding);
22
+ const applied=await old.transaction(async(events,append)=>{const state=replayExperiment(binding.charter,events);if(state.claim||state.superseded)return false;
23
+ const e={type:"order-migrated",requestId:r.requestId,successorDigest:factoryOrderDigest(r.successor)};replayExperiment(binding.charter,[...events,e]);await append(e);return true;});
24
+ if(applied)await createFactoryOrder(b,r.successor,a);
25
+ await store.transaction(async(events,append)=>{const e={type:"migration-receipt",requestId:r.requestId,application:applied?"applied":"refused-active",renewedObligations:applied?source.charter.nodes.map(n=>n.obligation):[]};replayRegistry(b,[...events,e]);await append(e);});
26
+ }
27
+ const m=replayRegistry(b,(await store.read()).events).migrations.get(r.requestId)!;
28
+ return {...m,requestId:r.requestId,grantExpansion:false,acceptanceTransfer:"none" as const};
29
+ }
@@ -0,0 +1,56 @@
1
+ import { byteHash, cloneExperiment, experimentCharterDigest, experimentHash } from "./experiment-contract.ts";
2
+ import { createExperiment, openExperiment, type ExperimentView } from "./experiment.ts";
3
+ import { experimentBindingDigest, experimentStore } from "./experiment-store.ts";
4
+ import { readIntentWork } from "./intent-application.ts";
5
+ import { freezeWork } from "./work-ledger-json.ts";
6
+ import { compileFactoryOrder, factoryAuthority, factoryCharter, factoryOrderDigest, fixedPolicyDigest, requireFactoryAuthority, type FactoryOrderCharter, type FactoryAuthority } from "./factory-contract.ts";
7
+ import { factoryRegistryStore, replayRegistry, validateActivePolicy, type FactoryRegistryBinding, type PinnedOrder } from "./factory-registry.ts";
8
+ import type { ExperimentCancellation } from "./experiment-contract.ts";
9
+ import type { FactoryDecision } from "./order-schedule.ts";
10
+ import type { DigestProfile } from "./effect-profile.ts";
11
+ export { migrateFactoryOrder, factoryMigrationDigest, type FactoryMigration } from "./factory-migration.ts";
12
+ export { createFactoryRegistry, openFactoryRegistry, type FactoryRegistryBinding } from "./factory-registry.ts";
13
+ export { parseFactoryOrder, factoryOrderDigest, fixedPolicyDigest, factoryDecisionDigest, activationRequestDigest, type FactoryOrderCharter, type FactoryAuthority, type FixedPolicy, type ActivationRequest } from "./factory-contract.ts";
14
+ function lowerAuthority(p:PinnedOrder,a:FactoryAuthority|null){return a?.orderDigests.includes(factoryOrderDigest(p.charter))?{authorityDigest:p.charter.budget.authorityDigest,charterDigests:[p.compiledDigest],cancellationDigests:a.cancellationDigests??[]}:null;}
15
+ export async function createFactoryOrder(inputRegistry:FactoryRegistryBinding,input:FactoryOrderCharter,host:FactoryAuthority|null){
16
+ const registry=factoryRegistryStore(inputRegistry).binding;
17
+ const c=factoryCharter(input),a=requireFactoryAuthority(factoryAuthority(host),registry.initial.authorityId),store=factoryRegistryStore(registry);
18
+ if(!a.orderDigests.includes(factoryOrderDigest(c)))throw new Error("independent order authority required");
19
+ let pinned!:PinnedOrder,existing=false;
20
+ await store.transaction(async(events,append)=>{
21
+ const state=replayRegistry(registry,events),old=state.orders.get(c.orderId);
22
+ if(old){if(factoryOrderDigest(old.charter)!==factoryOrderDigest(c))throw new Error("conflicting order identity");pinned=old;existing=true;return;}
23
+ if(c.scopeDigest!==registry.initial.scopeDigest)throw new Error("changed registry scope");
24
+ if(c.pin.revision!==state.revision||c.pin.candidateDigest!==state.candidateDigest)throw new Error("stale order policy pin");validateActivePolicy(state,a);
25
+ const compiled=await compileFactoryOrder(c,state.candidate),event={type:"order-pin",charter:c,candidate:state.candidate,compiledDigest:experimentCharterDigest(compiled),adoptionId:state.activation?.receipt.id??null};
26
+ replayRegistry(registry,[...events,event]);await append(event);pinned={charter:c,candidate:state.candidate,compiledDigest:event.compiledDigest,adoptionId:event.adoptionId,binding:null};
27
+ });
28
+ if(existing){if(!pinned.binding)throw new Error("order materialization pending-or-unknown; no retry effects");await experimentStore(pinned.binding).read();return pinned.binding;}
29
+ const charter=await compileFactoryOrder(c,pinned.candidate);
30
+ const binding=await createExperiment({directory:c.directory,budget:c.budget,charter,bytes:Buffer.from(c.commonBase64,"base64"),authority:lowerAuthority(pinned,a)});
31
+ await store.transaction(async(events,append)=>{const event={type:"order-materialized",orderId:c.orderId,binding};replayRegistry(registry,[...events,event]);await append(event);});return binding;
32
+ }
33
+ export async function openFactoryOrder(inputRegistry:FactoryRegistryBinding,orderId:string,host:FactoryAuthority|null){
34
+ const registry=factoryRegistryStore(inputRegistry).binding;
35
+ const a=factoryAuthority(host),store=factoryRegistryStore(registry),state=replayRegistry(registry,(await store.read()).events),p=state.orders.get(orderId);
36
+ if(!p?.binding)throw new Error("order materialization missing or unknown");
37
+ const binding=p.binding;await experimentStore(binding).read();
38
+ if(experimentCharterDigest(binding.charter)!==p.compiledDigest)throw new Error("compiled order identity mismatch");
39
+ const admitted=a?.id===registry.initial.authorityId?lowerAuthority(p,a):null,controller=openExperiment(binding,admitted);
40
+ const inspect=async()=>{
41
+ const result=await controller.orderView();let scope:"pinned"|"changed-or-unavailable"="pinned";
42
+ try{if(byteHash(await readIntentWork(p.charter.work))!==p.charter.workTextDigest)scope="changed-or-unavailable";}catch{scope="changed-or-unavailable";}
43
+ const nodes=result.superseded?result.nodes.map(n=>({...n,state:"superseded" as const,action:"none" as const})):scope==="pinned"?result.nodes:result.nodes.map(n=>({...n,state:"unknown" as const,action:"stakeholder" as const}));
44
+ return freezeWork({version:"factory-order-view-v1",orderId,scope,policyPin:{revision:p.charter.pin.revision,candidateDigest:fixedPolicyDigest(p.candidate),adoptionId:p.adoptionId},orderDigest:factoryOrderDigest(p.charter),nodes:nodes.map(n=>({...n,obligation:p.charter.nodes.find(o=>o.nodeId===n.nodeId)!.obligation})),resources:result.view.budget,control:result.view.control,diagnostics:result.view.diagnostics,superseded:result.superseded,dispatchAuthorized:Boolean(admitted)&&result.view.control==="not-assessed",modelTransport:"not-used-fixed-profile",acceptance:"not-assessed",freshness:"snapshot-unknown"});
45
+ };
46
+ const settledView = async (completed: ExperimentView) => { const view = await inspect().catch(() => freezeWork({version:"factory-order-view-v1",orderId,scope:"changed-or-unavailable",policyPin:{revision:p.charter.pin.revision,candidateDigest:fixedPolicyDigest(p.candidate),adoptionId:p.adoptionId},orderDigest:factoryOrderDigest(p.charter),nodes:p.charter.nodes.map(n=>({nodeId:n.nodeId,obligation:n.obligation,state:"unknown",action:"stakeholder",executionId:null,evidenceDigest:null})),resources:null,superseded:null,dispatchAuthorized:false,modelTransport:"not-used-fixed-profile",acceptance:"not-assessed",freshness:"snapshot-unknown",control:"unknown"}));
47
+ return completed.control === "not-assessed" ? view : freezeWork({...view,control:completed.control,diagnostics:completed.diagnostics,dispatchAuthorized:false,nodes:view.nodes.map(n=>({...n,action:"stakeholder" as const}))});
48
+ };
49
+ return Object.freeze({bindingDigest:experimentBindingDigest(binding),binding,pin:freezeWork({revision:p.charter.pin.revision,candidateDigest:fixedPolicyDigest(p.candidate),adoptionId:p.adoptionId}),inspect,reconcile:inspect,
50
+ async advance(profile:DigestProfile){if(!admitted)throw new Error("independent order authority required");const run=await controller.start(profile);return Object.freeze({boundary:run.boundary.then(settledView),completion:run.completion.then(settledView),started:run.started});},
51
+ async decide(request:FactoryDecision,host:FactoryAuthority|null){const current=requireFactoryAuthority(factoryAuthority(host),registry.initial.authorityId);if(!current.orderDigests.includes(factoryOrderDigest(p.charter)))throw new Error("order authority required");await controller.decideOrder(request,{id:current.id,digests:current.decisionDigests});return inspect();},
52
+ async cancel(request:ExperimentCancellation,host:FactoryAuthority|null){const current=requireFactoryAuthority(factoryAuthority(host),registry.initial.authorityId);await controller.cancel(request,lowerAuthority(p,current));return inspect();},
53
+ readArtifact:controller.readArtifact,
54
+ });
55
+ }
56
+ export type FactoryOrderView=Awaited<ReturnType<Awaited<ReturnType<typeof openFactoryOrder>>["inspect"]>>;
@@ -0,0 +1,60 @@
1
+ import { createControlJournal, controlJournal, type ControlBinding } from "./control-journal.ts";
2
+ import { cloneExperiment, closed, experimentHash, experimentCharterDigest } from "./experiment-contract.ts";
3
+ import { validateExperimentBinding, type ExperimentBinding } from "./experiment-store.ts";
4
+ import { fixedPolicy, fixedPolicyDigest, factoryCharter, factoryOrderDigest, factoryAuthority, requireFactoryAuthority, activationRequestDigest, type FixedPolicy, type FactoryAuthority, type FactoryOrderCharter, type ActivationRequest } from "./factory-contract.ts";
5
+ import { validateAdoptionReceipt, authorizeRollback, buildRollbackRequest, buildAdoptionBinding, type RollbackRequest } from "./vendor/adoption.ts";
6
+ export interface RegistryInitial {version:"factory-registry-v1";authorityId:string;scopeDigest:string;baseline:FixedPolicy}
7
+ export type FactoryRegistryBinding=ControlBinding<RegistryInitial>;
8
+ export interface PinnedOrder {charter:FactoryOrderCharter;candidate:FixedPolicy;compiledDigest:string;adoptionId:string|null;binding:ExperimentBinding|null}
9
+ export function registryInitial(value:RegistryInitial){const v=cloneExperiment(value);closed(v,["version","authorityId","scopeDigest","baseline"]);if(v.version!=="factory-registry-v1"||!/^[a-zA-Z0-9:_-]{1,128}$/.test(v.authorityId)||!/^[a-f0-9]{64}$/.test(v.scopeDigest))throw new Error("invalid registry initial binding");v.baseline=fixedPolicy(v.baseline);return v;}
10
+ export function replayRegistry(b:FactoryRegistryBinding,events:Record<string,unknown>[]){
11
+ const initial=registryInitial(b.initial);let revision=0,candidate=initial.baseline,activation:ActivationRequest|null=null;
12
+ const changes=new Map<string,string>(),orders=new Map<string,PinnedOrder>(),migrations=new Map<string,{digest:string;sourceOrderId:string;targetOrderId:string;application:string}>();
13
+ const candidates=new Map([[fixedPolicyDigest(candidate),candidate]]), lineage:(ActivationRequest|null)[]=[];
14
+ for(const e of events){
15
+ if(e.type==="activate"){
16
+ closed(e,["type","request"]);const r=cloneExperiment(e.request) as ActivationRequest;closed(r,["version","requestId","expectedRevision","expectedCandidateDigest","candidate","binding","receipt"]);
17
+ const {id,version,...input}=r.binding;
18
+ if(r.version!=="factory-activation-v1"||typeof r.requestId!=="string"||!/^[a-zA-Z0-9:_-]{1,128}$/.test(r.requestId)||version!=="adoption-binding-v1"||buildAdoptionBinding(input).id!==id||r.expectedRevision!==revision||r.expectedCandidateDigest!==fixedPolicyDigest(candidate)||r.binding.scopeDigest!==initial.scopeDigest||r.binding.rollbackCandidateDigest!==fixedPolicyDigest(candidate)||r.binding.candidateDigest!==fixedPolicyDigest(r.candidate)||r.binding.assessmentPolicyDigest!==r.candidate.acceptancePolicyDigest||changes.has(r.requestId))throw new Error("invalid activation sequence");
19
+ changes.set(r.requestId,activationRequestDigest(r));lineage.push(activation);candidate=fixedPolicy(r.candidate);candidates.set(fixedPolicyDigest(candidate),candidate);activation=r;revision++;
20
+ }else if(e.type==="rollback"){
21
+ closed(e,["type","request","fromRevision"]);const r=e.request as RollbackRequest;
22
+ if(!activation||e.fromRevision!==revision||changes.has(r.id)||buildRollbackRequest(activation.receipt,r.reason,[...r.evidence],r.expiresAt).id!==r.id||r.restoreCandidateDigest!==activation.binding.rollbackCandidateDigest)throw new Error("invalid rollback sequence");
23
+ const restored=candidates.get(r.restoreCandidateDigest);if(!restored)throw new Error("rollback bytes not registered");changes.set(r.id,experimentHash(r));candidate=restored;if(!lineage.length)throw new Error("missing rollback lineage");activation=lineage.pop()!;
24
+ if((activation?.binding.candidateDigest??fixedPolicyDigest(initial.baseline))!==r.restoreCandidateDigest)throw new Error("rollback lineage mismatch");revision++;
25
+ }else if(e.type==="order-pin"){
26
+ closed(e,["type","charter","candidate","compiledDigest","adoptionId"]);const charter=factoryCharter(e.charter as FactoryOrderCharter),policy=fixedPolicy(e.candidate as FixedPolicy);
27
+ if(orders.size>=32||orders.has(charter.orderId)||charter.scopeDigest!==initial.scopeDigest||charter.pin.revision!==revision||charter.pin.candidateDigest!==fixedPolicyDigest(candidate)||fixedPolicyDigest(policy)!==fixedPolicyDigest(candidate)||e.adoptionId!==(activation?.receipt.id??null)||typeof e.compiledDigest!=="string"||!/^[a-f0-9]{64}$/.test(e.compiledDigest))throw new Error("invalid order pin sequence");
28
+ orders.set(charter.orderId,{charter,candidate:policy,compiledDigest:e.compiledDigest,adoptionId:e.adoptionId as string|null,binding:null});
29
+ }else if(e.type==="order-materialized"){
30
+ closed(e,["type","orderId","binding"]);const p=orders.get(String(e.orderId)),binding=validateExperimentBinding(e.binding as ExperimentBinding);
31
+ if(!p||p.binding||binding.directory!==p.charter.directory||experimentCharterDigest(binding.charter)!==p.compiledDigest)throw new Error("invalid order materialization");p.binding=binding;
32
+ }else if(e.type==="migration-request"){
33
+ closed(e,["type","requestId","sourceOrderId","targetOrderId","requestDigest"]);
34
+ if(typeof e.requestId!=="string"||changes.has(e.requestId)||!orders.has(String(e.sourceOrderId))||e.sourceOrderId===e.targetOrderId||typeof e.requestDigest!=="string"||!/^[a-f0-9]{64}$/.test(e.requestDigest))throw new Error("invalid migration request");
35
+ changes.set(e.requestId,e.requestDigest);migrations.set(e.requestId,{digest:e.requestDigest,sourceOrderId:String(e.sourceOrderId),targetOrderId:String(e.targetOrderId),application:"pending-or-unknown"});
36
+ }else if(e.type==="migration-receipt"){
37
+ closed(e,["type","requestId","application","renewedObligations"]);const m=migrations.get(String(e.requestId));
38
+ if(!m||m.application!=="pending-or-unknown"||!["applied","refused-active"].includes(String(e.application))||!Array.isArray(e.renewedObligations)||e.renewedObligations.length>16||e.application==="applied"&&!orders.get(m.targetOrderId)?.binding)throw new Error("invalid migration receipt");m.application=String(e.application);
39
+ }else throw new Error("unknown factory control event");
40
+ }
41
+ return{revision,candidate,candidateDigest:fixedPolicyDigest(candidate),activation,changes,orders,migrations};
42
+ }
43
+ export const factoryRegistryStore=(b:FactoryRegistryBinding)=>{registryInitial(b.initial);return controlJournal(b);};
44
+ export async function createFactoryRegistry(input:{directory:string;authorityId:string;scopeDigest:string;baseline:FixedPolicy}):Promise<FactoryRegistryBinding>{const {directory,...rest}=cloneExperiment(input);return createControlJournal(directory,registryInitial({version:"factory-registry-v1",...rest}));}
45
+ export function validateActivePolicy(state:ReturnType<typeof replayRegistry>,a:FactoryAuthority){if(state.activation){const r=state.activation,facts=a.facts.find(f=>f.bindingId===r.binding.id)?.facts;if(!facts)throw new Error("current independent eligibility facts required");validateAdoptionReceipt(r.receipt,r.binding,a.adoption,facts,Date.now());}}
46
+ export function openFactoryRegistry(input:FactoryRegistryBinding){const store=factoryRegistryStore(input),b=store.binding;
47
+ const inspect=async()=>{const s=replayRegistry(b,(await store.read()).events);return{version:"factory-registry-view-v1" as const,revision:s.revision,candidateDigest:s.candidateDigest,candidate:cloneExperiment(s.candidate),scopeDigest:b.initial.scopeDigest,migrations:[...s.migrations].map(([requestId,m])=>({requestId,...m})),orders:[...s.orders].map(([orderId,p])=>({orderId,orderDigest:factoryOrderDigest(p.charter),candidateDigest:fixedPolicyDigest(p.candidate),revision:p.charter.pin.revision,materialization:p.binding?"recorded":"pending-or-unknown"})),acceptance:"not-assessed" as const};};
48
+ return{inspect,async activate(input:ActivationRequest,host:FactoryAuthority|null){const r=cloneExperiment(input),a=requireFactoryAuthority(factoryAuthority(host),b.initial.authorityId);closed(r,["version","requestId","expectedRevision","expectedCandidateDigest","candidate","binding","receipt"]);
49
+ if(!a.activationDigests.includes(activationRequestDigest(r)))throw new Error("independent activation authority required");
50
+ await store.transaction(async(events,append)=>{const s=replayRegistry(b,events),old=s.changes.get(r.requestId);if(old){if(old!==activationRequestDigest(r))throw new Error("conflicting activation ID");return;}
51
+ if(r.expectedRevision!==s.revision||r.expectedCandidateDigest!==s.candidateDigest)throw new Error("stale activation");
52
+ if(r.binding.scopeDigest!==b.initial.scopeDigest||r.binding.candidateDigest!==fixedPolicyDigest(r.candidate)||r.binding.assessmentPolicyDigest!==r.candidate.acceptancePolicyDigest||r.binding.rollbackCandidateDigest!==s.candidateDigest)throw new Error("activation scope/candidate/policy mismatch");
53
+ const facts=a.facts.find(f=>f.bindingId===r.binding.id)?.facts;if(!facts)throw new Error("independent eligibility facts required");validateAdoptionReceipt(r.receipt,r.binding,a.adoption,facts,Date.now());
54
+ replayRegistry(b,[...events,{type:"activate",request:r}]);await append({type:"activate",request:r});});return inspect();
55
+ },async rollback(input:RollbackRequest,host:FactoryAuthority|null){const r=cloneExperiment(input),a=requireFactoryAuthority(factoryAuthority(host),b.initial.authorityId);closed(r,["version","id","adoptionId","scopeDigest","restoreCandidateDigest","reason","evidence","expiresAt"]);
56
+ await store.transaction(async(events,append)=>{const s=replayRegistry(b,events),old=s.changes.get(r.id);if(old){if(old!==experimentHash(r)||!a.adoption?.rollbacks.includes(r.id))throw new Error("rollback authority/identity mismatch");return;}
57
+ if(!s.activation)throw new Error("stale rollback");authorizeRollback(s.activation.receipt,r,a.adoption,Date.now(),{adoptionId:s.activation.receipt.id,candidateDigest:s.candidateDigest,scopeDigest:b.initial.scopeDigest});
58
+ const event={type:"rollback",request:r,fromRevision:s.revision};replayRegistry(b,[...events,event]);await append(event);});return{...await inspect(),application:"applied" as const,requestId:r.id,grantExpansion:false};
59
+ }};
60
+ }
package/src/file-lock.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  /**
2
- * One cross-process file lock, used by both files this package writes.
2
+ * One cross-process file lock, shared by producer ledgers and the approvals store.
3
3
  *
4
4
  * **Extracted rather than copied (R-49).** The ledger has had this lock since fan-out made a second writer
5
5
  * possible; the approvals store had an unlocked read-modify-write, so session 1 could load, session 2 could
@@ -36,8 +36,25 @@ export class LockTimeoutError extends Error {
36
36
  }
37
37
  }
38
38
 
39
+ export interface FileLockOptions { readonly staleRecovery?: "age" | "disabled" }
40
+ function lockPolicy(options: FileLockOptions | undefined): "age" | "disabled" {
41
+ if (options === undefined) return "age";
42
+ const invalid = (): never => { throw new TypeError("invalid file lock options"); };
43
+ if (options === null || typeof options !== "object" || Array.isArray(options) ||
44
+ ![Object.prototype, null].includes(Object.getPrototypeOf(options))) return invalid();
45
+ const keys = Reflect.ownKeys(options);
46
+ if (keys.some(key => key !== "staleRecovery")) return invalid();
47
+ if (!keys.length) return "age";
48
+ const field = Object.getOwnPropertyDescriptor(options, "staleRecovery")!;
49
+ if (!Object.hasOwn(field, "value") || !field.enumerable || !["age", "disabled"].includes(field.value)) return invalid();
50
+ return field.value;
51
+ }
52
+
39
53
  /**
40
54
  * Run `work` while holding an exclusive lock beside `path`.
55
+ * Explicit disabled recovery never reclaims by age/liveness. V4 selects it internally; an orphan
56
+ * requires separately authorized quiescent recovery, not a timer or automatic operator action.
57
+ * The age-based behavior described below remains the default for legacy callers.
41
58
  *
42
59
  * **Why a lock at all.** `O_APPEND` is atomic for one write to a regular file on a POSIX filesystem, and the
43
60
  * guarantee does **not** hold on drvfs (`/mnt/c` under WSL2) or NFS — which is exactly where this project
@@ -59,7 +76,8 @@ export class LockTimeoutError extends Error {
59
76
  * The timeout is short *on purpose*: work refused because a file was busy is recoverable and loud, while
60
77
  * work that hangs waiting for a lock is neither.
61
78
  */
62
- export async function withFileLock<T>(path: string, label: string, work: () => Promise<T>): Promise<T> {
79
+ export async function withFileLock<T>(path: string, label: string, work: () => Promise<T>, options?: FileLockOptions): Promise<T> {
80
+ const staleRecovery = lockPolicy(options); // Detached once, before I/O or any wait.
63
81
  const lockPath = `${path}.lock`;
64
82
  const deadline = Date.now() + LOCK_TIMEOUT_MS;
65
83
 
@@ -98,7 +116,7 @@ export async function withFileLock<T>(path: string, label: string, work: () => P
98
116
 
99
117
  // Someone else holds it. Break it only if it is old enough to be abandoned — and only the exact file
100
118
  // we judged, so a lock created in the gap survives.
101
- try {
119
+ if (staleRecovery === "age") try {
102
120
  const held = await stat(lockPath);
103
121
  if (Date.now() - held.mtimeMs > STALE_LOCK_MS) {
104
122
  const abandoned = await readFile(lockPath, "utf8").catch(() => undefined);
@@ -113,17 +131,34 @@ export async function withFileLock<T>(path: string, label: string, work: () => P
113
131
  continue;
114
132
  }
115
133
 
134
+ let bodyFailed = false;
116
135
  try {
117
136
  return await work();
118
- } finally {
119
- await handle.close().catch(() => undefined);
120
- // Only if it is still OURS. A lock broken out from under us belongs to somebody else now, and
121
- // deleting it is what turned one lost race into a cascade.
122
- await removeIfOurs(lockPath, token);
137
+ } catch (error) { bodyFailed = true; throw error; }
138
+ finally {
139
+ if (staleRecovery === "disabled") {
140
+ try { await releaseDisabledLock(handle, lockPath, token); }
141
+ catch (error) { if (!bodyFailed) throw error; } // Preserve even a falsy primary rejection.
142
+ } else {
143
+ await handle.close().catch(() => undefined);
144
+ // Legacy age policy remains best effort; never remove a demonstrably different token.
145
+ await removeIfOurs(lockPath, token);
146
+ }
123
147
  }
124
148
  }
125
149
  }
126
150
 
151
+ /** Disabled ownership cannot silently manufacture an indefinite orphan after a successful body.
152
+ * Try token-checked removal even if close failed; report the first failure after both attempts.
153
+ * Appended bytes are not rolled back, and a primary body error takes precedence at the caller. */
154
+ async function releaseDisabledLock(handle: Awaited<ReturnType<typeof open>>, lockPath: string, token: string): Promise<void> {
155
+ let failure: { error: unknown } | null = null;
156
+ try { await handle.close(); } catch (error) { failure = { error }; }
157
+ try { await removeIfOurs(lockPath, token, true); }
158
+ catch (error) { failure ??= { error }; }
159
+ if (failure) throw failure.error;
160
+ }
161
+
127
162
  /**
128
163
  * Delete the lock only if it still holds `token`.
129
164
  *
@@ -131,12 +166,13 @@ export async function withFileLock<T>(path: string, label: string, work: () => P
131
166
  * "the whole of `work()`" to "between a read and an unlink", and it removes the *systematic* break entirely:
132
167
  * a process can no longer delete a lock it demonstrably never owned.
133
168
  */
134
- async function removeIfOurs(lockPath: string, token: string): Promise<void> {
169
+ async function removeIfOurs(lockPath: string, token: string, strict = false): Promise<void> {
135
170
  try {
136
171
  const held = await readFile(lockPath, "utf8");
137
172
  if (held.trim() !== token) return;
138
173
  await rm(lockPath, { force: true });
139
- } catch {
140
- /* already gone, or unreadable — either way this process is not the one that should force it */
174
+ } catch (error) {
175
+ if (strict) throw error; // No ownership inference or force-delete after a failed read.
176
+ /* Legacy: already gone, or unreadable — this process must not force removal. */
141
177
  }
142
178
  }