@intentic/sandbox-contract 1.311.0 → 1.313.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 (444) hide show
  1. package/README.md +32 -4
  2. package/dist/chores/chores.js +6 -6
  3. package/dist/chores/chores.js.map +1 -1
  4. package/dist/chores/index.d.ts +0 -1
  5. package/dist/chores/index.d.ts.map +1 -1
  6. package/dist/chores/index.js +0 -1
  7. package/dist/chores/index.js.map +1 -1
  8. package/dist/chores/prompt.d.ts.map +1 -1
  9. package/dist/chores/prompt.js +3 -2
  10. package/dist/chores/prompt.js.map +1 -1
  11. package/dist/contracts/accounts.contract.d.ts +69 -0
  12. package/dist/contracts/accounts.contract.d.ts.map +1 -1
  13. package/dist/contracts/agent.contract.d.ts +436 -0
  14. package/dist/contracts/agent.contract.d.ts.map +1 -1
  15. package/dist/contracts/agent.contract.js +11 -1
  16. package/dist/contracts/agent.contract.js.map +1 -1
  17. package/dist/contracts/agents.contract.d.ts +718 -90
  18. package/dist/contracts/agents.contract.d.ts.map +1 -1
  19. package/dist/contracts/agents.contract.js +30 -2
  20. package/dist/contracts/agents.contract.js.map +1 -1
  21. package/dist/contracts/capabilities.contract.js +1 -1
  22. package/dist/contracts/capabilities.contract.js.map +1 -1
  23. package/dist/contracts/device.contract.d.ts +20 -4
  24. package/dist/contracts/device.contract.d.ts.map +1 -1
  25. package/dist/contracts/device.contract.js +1 -1
  26. package/dist/contracts/device.contract.js.map +1 -1
  27. package/dist/contracts/extensions.contract.d.ts +13 -0
  28. package/dist/contracts/extensions.contract.d.ts.map +1 -1
  29. package/dist/contracts/git.contract.d.ts +2 -0
  30. package/dist/contracts/git.contract.d.ts.map +1 -1
  31. package/dist/contracts/offload.contract.d.ts +107 -0
  32. package/dist/contracts/offload.contract.d.ts.map +1 -0
  33. package/dist/contracts/offload.contract.js +53 -0
  34. package/dist/contracts/offload.contract.js.map +1 -0
  35. package/dist/contracts/personas.contract.d.ts +2 -0
  36. package/dist/contracts/personas.contract.d.ts.map +1 -1
  37. package/dist/contracts/ports.contract.d.ts +5 -0
  38. package/dist/contracts/ports.contract.d.ts.map +1 -1
  39. package/dist/contracts/runner.contract.d.ts +204 -0
  40. package/dist/contracts/runner.contract.d.ts.map +1 -1
  41. package/dist/contracts/runner.contract.js +3 -1
  42. package/dist/contracts/runner.contract.js.map +1 -1
  43. package/dist/contracts/secrets.contract.d.ts.map +1 -1
  44. package/dist/contracts/secrets.contract.js +1 -0
  45. package/dist/contracts/secrets.contract.js.map +1 -1
  46. package/dist/contracts/sessions.contract.d.ts +59 -0
  47. package/dist/contracts/sessions.contract.d.ts.map +1 -1
  48. package/dist/contracts/settings.contract.d.ts +20 -8
  49. package/dist/contracts/settings.contract.d.ts.map +1 -1
  50. package/dist/contracts/settings.contract.js +2 -2
  51. package/dist/contracts/settings.contract.js.map +1 -1
  52. package/dist/contracts/system.contract.d.ts +230 -15
  53. package/dist/contracts/system.contract.d.ts.map +1 -1
  54. package/dist/contracts/translator.contract.d.ts +88 -0
  55. package/dist/contracts/translator.contract.d.ts.map +1 -1
  56. package/dist/contracts/workspace.contract.d.ts +217 -0
  57. package/dist/contracts/workspace.contract.d.ts.map +1 -1
  58. package/dist/contracts/workspace.contract.js +40 -3
  59. package/dist/contracts/workspace.contract.js.map +1 -1
  60. package/dist/documents/conversions.d.ts +81 -0
  61. package/dist/documents/conversions.d.ts.map +1 -0
  62. package/dist/documents/conversions.js +213 -0
  63. package/dist/documents/conversions.js.map +1 -0
  64. package/dist/documents/index.d.ts +4 -0
  65. package/dist/documents/index.d.ts.map +1 -0
  66. package/dist/documents/index.js +4 -0
  67. package/dist/documents/index.js.map +1 -0
  68. package/dist/documents/passthrough.d.ts +13 -0
  69. package/dist/documents/passthrough.d.ts.map +1 -0
  70. package/dist/documents/passthrough.js +101 -0
  71. package/dist/documents/passthrough.js.map +1 -0
  72. package/dist/documents/read.d.ts +24 -0
  73. package/dist/documents/read.d.ts.map +1 -0
  74. package/dist/documents/read.js +81 -0
  75. package/dist/documents/read.js.map +1 -0
  76. package/dist/events/agent-events.d.ts +235 -0
  77. package/dist/events/agent-events.d.ts.map +1 -1
  78. package/dist/events/agent-events.js +3 -0
  79. package/dist/events/agent-events.js.map +1 -1
  80. package/dist/events/child-run.d.ts +9 -0
  81. package/dist/events/child-run.d.ts.map +1 -0
  82. package/dist/events/child-run.js +41 -0
  83. package/dist/events/child-run.js.map +1 -0
  84. package/dist/events/errands.d.ts +5 -0
  85. package/dist/events/errands.d.ts.map +1 -0
  86. package/dist/events/errands.js +19 -0
  87. package/dist/events/errands.js.map +1 -0
  88. package/dist/events/land-breakage.d.ts +3 -0
  89. package/dist/events/land-breakage.d.ts.map +1 -1
  90. package/dist/events/land-breakage.js +3 -0
  91. package/dist/events/land-breakage.js.map +1 -1
  92. package/dist/events/land-conflict.d.ts +3 -0
  93. package/dist/events/land-conflict.d.ts.map +1 -0
  94. package/dist/events/land-conflict.js +4 -0
  95. package/dist/events/land-conflict.js.map +1 -0
  96. package/dist/events/requests.d.ts +152 -0
  97. package/dist/events/requests.d.ts.map +1 -1
  98. package/dist/events/requests.js +26 -1
  99. package/dist/events/requests.js.map +1 -1
  100. package/dist/events/system-events.d.ts +74 -12
  101. package/dist/events/system-events.d.ts.map +1 -1
  102. package/dist/events/system-events.js +3 -1
  103. package/dist/events/system-events.js.map +1 -1
  104. package/dist/events/transcript.d.ts +387 -0
  105. package/dist/events/transcript.d.ts.map +1 -1
  106. package/dist/events/transcript.js +3 -0
  107. package/dist/events/transcript.js.map +1 -1
  108. package/dist/events/verify-nudge.d.ts.map +1 -1
  109. package/dist/events/verify-nudge.js.map +1 -1
  110. package/dist/front/browser-wire.d.ts +6 -0
  111. package/dist/front/browser-wire.d.ts.map +1 -0
  112. package/dist/front/browser-wire.js +4 -0
  113. package/dist/front/browser-wire.js.map +1 -0
  114. package/dist/front/front-wire.d.ts +6 -0
  115. package/dist/front/front-wire.d.ts.map +1 -0
  116. package/dist/front/front-wire.js +5 -0
  117. package/dist/front/front-wire.js.map +1 -0
  118. package/dist/front/generated/browser-wire.d.ts +19 -0
  119. package/dist/front/generated/browser-wire.d.ts.map +1 -0
  120. package/dist/front/generated/browser-wire.js +2 -0
  121. package/dist/front/generated/browser-wire.js.map +1 -0
  122. package/dist/front/generated/wire.d.ts +70 -1
  123. package/dist/front/generated/wire.d.ts.map +1 -1
  124. package/dist/ids/conversation-ids.d.ts +3 -0
  125. package/dist/ids/conversation-ids.d.ts.map +1 -1
  126. package/dist/ids/conversation-ids.js +4 -2
  127. package/dist/ids/conversation-ids.js.map +1 -1
  128. package/dist/ids/session-names.d.ts +3 -0
  129. package/dist/ids/session-names.d.ts.map +1 -1
  130. package/dist/ids/session-names.js +3 -0
  131. package/dist/ids/session-names.js.map +1 -1
  132. package/dist/index.d.ts +2052 -190
  133. package/dist/index.d.ts.map +1 -1
  134. package/dist/index.js +15 -1
  135. package/dist/index.js.map +1 -1
  136. package/dist/models/agent-catalog.d.ts.map +1 -1
  137. package/dist/models/agent-catalog.js +4 -6
  138. package/dist/models/agent-catalog.js.map +1 -1
  139. package/dist/models/agent-runtimes.d.ts +2 -1
  140. package/dist/models/agent-runtimes.d.ts.map +1 -1
  141. package/dist/models/agent-runtimes.js +7 -1
  142. package/dist/models/agent-runtimes.js.map +1 -1
  143. package/dist/models/plan-pools.d.ts +22 -1
  144. package/dist/models/plan-pools.d.ts.map +1 -1
  145. package/dist/models/plan-pools.js +86 -0
  146. package/dist/models/plan-pools.js.map +1 -1
  147. package/dist/policy/request-status.d.ts +1 -1
  148. package/dist/policy/request-status.d.ts.map +1 -1
  149. package/dist/policy/request-status.js +4 -11
  150. package/dist/policy/request-status.js.map +1 -1
  151. package/dist/policy/reserved-servers.d.ts.map +1 -1
  152. package/dist/policy/reserved-servers.js +2 -1
  153. package/dist/policy/reserved-servers.js.map +1 -1
  154. package/dist/policy/turned-away.d.ts.map +1 -1
  155. package/dist/policy/turned-away.js +1 -0
  156. package/dist/policy/turned-away.js.map +1 -1
  157. package/dist/protocol/container-requirements.d.ts +0 -1
  158. package/dist/protocol/container-requirements.d.ts.map +1 -1
  159. package/dist/protocol/container-requirements.js +1 -2
  160. package/dist/protocol/container-requirements.js.map +1 -1
  161. package/dist/protocol/edge-verdict.d.ts +2 -1
  162. package/dist/protocol/edge-verdict.d.ts.map +1 -1
  163. package/dist/protocol/edge-verdict.js +1 -1
  164. package/dist/protocol/edge-verdict.js.map +1 -1
  165. package/dist/protocol/ingress-contract.d.ts +1 -12
  166. package/dist/protocol/ingress-contract.d.ts.map +1 -1
  167. package/dist/protocol/ingress-contract.js +1 -1
  168. package/dist/protocol/ingress-contract.js.map +1 -1
  169. package/dist/protocol/raw-routes.d.ts +36 -26
  170. package/dist/protocol/raw-routes.d.ts.map +1 -1
  171. package/dist/protocol/raw-routes.js +17 -23
  172. package/dist/protocol/raw-routes.js.map +1 -1
  173. package/dist/protocol/route-meta.d.ts +22 -0
  174. package/dist/protocol/route-meta.d.ts.map +1 -1
  175. package/dist/protocol/route-meta.js +16 -0
  176. package/dist/protocol/route-meta.js.map +1 -1
  177. package/dist/protocol/runner-protocol.d.ts +34 -0
  178. package/dist/protocol/runner-protocol.d.ts.map +1 -1
  179. package/dist/protocol/runner-protocol.js +33 -0
  180. package/dist/protocol/runner-protocol.js.map +1 -1
  181. package/dist/protocol/tunnel-bulk.d.ts +2 -0
  182. package/dist/protocol/tunnel-bulk.d.ts.map +1 -0
  183. package/dist/protocol/tunnel-bulk.js +3 -0
  184. package/dist/protocol/tunnel-bulk.js.map +1 -0
  185. package/dist/protocol/x402.d.ts +17 -0
  186. package/dist/protocol/x402.d.ts.map +1 -0
  187. package/dist/protocol/x402.js +37 -0
  188. package/dist/protocol/x402.js.map +1 -0
  189. package/dist/schemas/activity.d.ts.map +1 -1
  190. package/dist/schemas/activity.js.map +1 -1
  191. package/dist/schemas/agent.d.ts +25 -0
  192. package/dist/schemas/agent.d.ts.map +1 -1
  193. package/dist/schemas/agent.js +14 -2
  194. package/dist/schemas/agent.js.map +1 -1
  195. package/dist/schemas/agents.d.ts +90 -18
  196. package/dist/schemas/agents.d.ts.map +1 -1
  197. package/dist/schemas/agents.js +49 -5
  198. package/dist/schemas/agents.js.map +1 -1
  199. package/dist/schemas/automations.d.ts +23 -6
  200. package/dist/schemas/automations.d.ts.map +1 -1
  201. package/dist/schemas/automations.js.map +1 -1
  202. package/dist/schemas/devices.d.ts +134 -7
  203. package/dist/schemas/devices.d.ts.map +1 -1
  204. package/dist/schemas/devices.js +43 -18
  205. package/dist/schemas/devices.js.map +1 -1
  206. package/dist/schemas/extension-updates.d.ts +26 -0
  207. package/dist/schemas/extension-updates.d.ts.map +1 -1
  208. package/dist/schemas/extension-updates.js +4 -0
  209. package/dist/schemas/extension-updates.js.map +1 -1
  210. package/dist/schemas/git/git.d.ts +19 -0
  211. package/dist/schemas/git/git.d.ts.map +1 -1
  212. package/dist/schemas/git/git.js +1 -0
  213. package/dist/schemas/git/git.js.map +1 -1
  214. package/dist/schemas/hosts.d.ts +35 -3
  215. package/dist/schemas/hosts.d.ts.map +1 -1
  216. package/dist/schemas/hosts.js +22 -6
  217. package/dist/schemas/hosts.js.map +1 -1
  218. package/dist/schemas/keep-warm.d.ts +2 -12
  219. package/dist/schemas/keep-warm.d.ts.map +1 -1
  220. package/dist/schemas/keep-warm.js +8 -10
  221. package/dist/schemas/keep-warm.js.map +1 -1
  222. package/dist/schemas/metrics.d.ts +29 -0
  223. package/dist/schemas/metrics.d.ts.map +1 -1
  224. package/dist/schemas/metrics.js +19 -3
  225. package/dist/schemas/metrics.js.map +1 -1
  226. package/dist/schemas/offload.d.ts +62 -0
  227. package/dist/schemas/offload.d.ts.map +1 -0
  228. package/dist/schemas/offload.js +23 -0
  229. package/dist/schemas/offload.js.map +1 -0
  230. package/dist/schemas/personas.d.ts +3 -0
  231. package/dist/schemas/personas.d.ts.map +1 -1
  232. package/dist/schemas/personas.js +12 -3
  233. package/dist/schemas/personas.js.map +1 -1
  234. package/dist/schemas/ports.d.ts +10 -0
  235. package/dist/schemas/ports.d.ts.map +1 -1
  236. package/dist/schemas/ports.js +8 -0
  237. package/dist/schemas/ports.js.map +1 -1
  238. package/dist/schemas/providers/plan-limits.d.ts +169 -0
  239. package/dist/schemas/providers/plan-limits.d.ts.map +1 -1
  240. package/dist/schemas/providers/plan-limits.js +54 -1
  241. package/dist/schemas/providers/plan-limits.js.map +1 -1
  242. package/dist/schemas/providers/provider-oauth.d.ts +69 -0
  243. package/dist/schemas/providers/provider-oauth.d.ts.map +1 -1
  244. package/dist/schemas/providers/provider-oauth.js +6 -1
  245. package/dist/schemas/providers/provider-oauth.js.map +1 -1
  246. package/dist/schemas/settings-history.d.ts +55 -0
  247. package/dist/schemas/settings-history.d.ts.map +1 -0
  248. package/dist/schemas/settings-history.js +73 -0
  249. package/dist/schemas/settings-history.js.map +1 -0
  250. package/dist/schemas/settings.d.ts +188 -4
  251. package/dist/schemas/settings.d.ts.map +1 -1
  252. package/dist/schemas/settings.js +41 -26
  253. package/dist/schemas/settings.js.map +1 -1
  254. package/dist/schemas/speaker.d.ts +36 -0
  255. package/dist/schemas/speaker.d.ts.map +1 -0
  256. package/dist/schemas/speaker.js +62 -0
  257. package/dist/schemas/speaker.js.map +1 -0
  258. package/dist/schemas/state-plan.d.ts +46 -0
  259. package/dist/schemas/state-plan.d.ts.map +1 -0
  260. package/dist/schemas/state-plan.js +31 -0
  261. package/dist/schemas/state-plan.js.map +1 -0
  262. package/dist/schemas/system.d.ts +64 -0
  263. package/dist/schemas/system.d.ts.map +1 -1
  264. package/dist/schemas/system.js +12 -1
  265. package/dist/schemas/system.js.map +1 -1
  266. package/dist/schemas/workspace/mainline.d.ts +513 -0
  267. package/dist/schemas/workspace/mainline.d.ts.map +1 -0
  268. package/dist/schemas/workspace/mainline.js +182 -0
  269. package/dist/schemas/workspace/mainline.js.map +1 -0
  270. package/dist/schemas/workspace/workspace-tree.d.ts +23 -0
  271. package/dist/schemas/workspace/workspace-tree.d.ts.map +1 -1
  272. package/dist/schemas/workspace/workspace-tree.js +34 -0
  273. package/dist/schemas/workspace/workspace-tree.js.map +1 -1
  274. package/dist/state/contract-lock.d.ts +1 -0
  275. package/dist/state/contract-lock.d.ts.map +1 -1
  276. package/dist/state/contract-lock.js +47 -1
  277. package/dist/state/contract-lock.js.map +1 -1
  278. package/dist/state/definition.d.ts +44 -20
  279. package/dist/state/definition.d.ts.map +1 -1
  280. package/dist/state/definition.js.map +1 -1
  281. package/dist/state/fix-stance.d.ts +1 -1
  282. package/dist/state/fix-stance.d.ts.map +1 -1
  283. package/dist/state/fix-stance.js +2 -1
  284. package/dist/state/fix-stance.js.map +1 -1
  285. package/dist/state/history-state.d.ts.map +1 -1
  286. package/dist/state/history-state.js +7 -0
  287. package/dist/state/history-state.js.map +1 -1
  288. package/dist/state/landed-commit.d.ts +1 -0
  289. package/dist/state/landed-commit.d.ts.map +1 -1
  290. package/dist/state/landed-commit.js +2 -0
  291. package/dist/state/landed-commit.js.map +1 -1
  292. package/dist/state/red-streak.d.ts +7 -0
  293. package/dist/state/red-streak.d.ts.map +1 -0
  294. package/dist/state/red-streak.js +11 -0
  295. package/dist/state/red-streak.js.map +1 -0
  296. package/dist/state/runtime-state.d.ts +3 -0
  297. package/dist/state/runtime-state.d.ts.map +1 -1
  298. package/dist/state/runtime-state.js +1 -0
  299. package/dist/state/runtime-state.js.map +1 -1
  300. package/dist/state/workspace-state.d.ts +23 -1
  301. package/dist/state/workspace-state.d.ts.map +1 -1
  302. package/dist/state/workspace-state.js +33 -3
  303. package/dist/state/workspace-state.js.map +1 -1
  304. package/dist/text/shell-regions.d.ts +1 -0
  305. package/dist/text/shell-regions.d.ts.map +1 -1
  306. package/dist/text/shell-regions.js +1 -1
  307. package/dist/text/shell-regions.js.map +1 -1
  308. package/dist/text/transcript-fold.d.ts +1 -0
  309. package/dist/text/transcript-fold.d.ts.map +1 -1
  310. package/dist/text/transcript-fold.js +24 -8
  311. package/dist/text/transcript-fold.js.map +1 -1
  312. package/package.json +42 -42
  313. package/src/chores/chores.ts +6 -6
  314. package/src/chores/index.ts +0 -1
  315. package/src/chores/prompt.ts +3 -2
  316. package/src/contracts/agent.contract.ts +14 -0
  317. package/src/contracts/agents.contract.ts +36 -1
  318. package/src/contracts/capabilities.contract.ts +2 -2
  319. package/src/contracts/device.contract.ts +5 -3
  320. package/src/contracts/offload.contract.ts +60 -0
  321. package/src/contracts/runner.contract.ts +7 -1
  322. package/src/contracts/secrets.contract.ts +2 -0
  323. package/src/contracts/settings.contract.ts +2 -1
  324. package/src/contracts/workspace.contract.ts +54 -2
  325. package/src/documents/conversions.ts +402 -0
  326. package/src/documents/index.ts +7 -0
  327. package/src/documents/passthrough.ts +139 -0
  328. package/src/documents/read.ts +114 -0
  329. package/src/events/agent-events.ts +5 -0
  330. package/src/events/child-run.test.ts +81 -0
  331. package/src/events/child-run.ts +73 -0
  332. package/src/events/errands.test.ts +22 -0
  333. package/src/events/errands.ts +27 -0
  334. package/src/events/land-breakage.ts +12 -0
  335. package/src/events/land-conflict.ts +12 -0
  336. package/src/events/requests.ts +40 -1
  337. package/src/events/system-events.ts +9 -3
  338. package/src/events/transcript.ts +8 -0
  339. package/src/events/verify-nudge.ts +4 -3
  340. package/src/front/browser-wire.ts +20 -0
  341. package/src/front/front-wire.ts +15 -0
  342. package/src/front/generated/browser-wire.json +138 -0
  343. package/src/front/generated/browser-wire.ts +16 -0
  344. package/src/front/generated/front-wire.json +793 -0
  345. package/src/front/generated/tunnel.json +38 -0
  346. package/src/front/generated/wire.ts +37 -9
  347. package/src/front/wire-manifests.test.ts +61 -0
  348. package/src/ids/conversation-ids.ts +13 -5
  349. package/src/ids/session-names.ts +7 -0
  350. package/src/index.ts +15 -2
  351. package/src/models/agent-catalog.test.ts +6 -4
  352. package/src/models/agent-catalog.ts +4 -6
  353. package/src/models/agent-runtimes.ts +22 -7
  354. package/src/models/capability-ledger.test.ts +2 -0
  355. package/src/models/plan-pools.test.ts +121 -2
  356. package/src/models/plan-pools.ts +152 -2
  357. package/src/policy/request-status.ts +4 -12
  358. package/src/policy/reserved-servers.test.ts +6 -0
  359. package/src/policy/reserved-servers.ts +13 -10
  360. package/src/policy/turned-away.ts +3 -0
  361. package/src/protocol/container-requirements.test.ts +2 -3
  362. package/src/protocol/container-requirements.ts +1 -7
  363. package/src/protocol/edge-verdict.ts +13 -11
  364. package/src/protocol/ingress-contract.fixture.json +90 -0
  365. package/src/protocol/ingress-contract.test.ts +18 -26
  366. package/src/protocol/ingress-contract.ts +10 -25
  367. package/src/protocol/raw-routes.ts +22 -26
  368. package/src/protocol/route-meta.ts +39 -0
  369. package/src/protocol/runner-protocol.ts +53 -0
  370. package/src/protocol/tunnel-bulk.test.ts +37 -0
  371. package/src/protocol/tunnel-bulk.ts +7 -0
  372. package/src/protocol/x402.test.ts +29 -0
  373. package/src/protocol/x402.ts +61 -0
  374. package/src/schemas/activity.ts +3 -1
  375. package/src/schemas/agent.ts +28 -3
  376. package/src/schemas/agents.ts +83 -8
  377. package/src/schemas/automations.ts +4 -2
  378. package/src/schemas/devices.ts +106 -39
  379. package/src/schemas/extension-updates.ts +6 -0
  380. package/src/schemas/git/git.ts +5 -0
  381. package/src/schemas/hosts.ts +64 -13
  382. package/src/schemas/ic-shape-args.test.ts +64 -0
  383. package/src/schemas/keep-warm.test.ts +6 -13
  384. package/src/schemas/keep-warm.ts +15 -20
  385. package/src/schemas/metrics.ts +28 -3
  386. package/src/schemas/offload.ts +44 -0
  387. package/src/schemas/personas.ts +18 -3
  388. package/src/schemas/ports.ts +10 -0
  389. package/src/schemas/providers/plan-limits.ts +84 -1
  390. package/src/schemas/providers/provider-oauth.ts +10 -1
  391. package/src/schemas/settings-history.ts +108 -0
  392. package/src/schemas/settings.test.ts +54 -1
  393. package/src/schemas/settings.ts +88 -49
  394. package/src/schemas/speaker.ts +89 -0
  395. package/src/schemas/state-plan.test.ts +69 -0
  396. package/src/schemas/state-plan.ts +47 -0
  397. package/src/schemas/system.ts +24 -3
  398. package/src/schemas/workspace/mainline.test.ts +28 -0
  399. package/src/schemas/workspace/mainline.ts +268 -0
  400. package/src/schemas/workspace/workspace-tree.ts +47 -0
  401. package/src/state/contract-lock.test.ts +65 -4
  402. package/src/state/contract-lock.ts +70 -2
  403. package/src/state/definition.ts +3 -2
  404. package/src/state/fix-stance.test.ts +8 -0
  405. package/src/state/fix-stance.ts +3 -3
  406. package/src/state/history-state.ts +19 -0
  407. package/src/state/landed-commit.test.ts +12 -1
  408. package/src/state/landed-commit.ts +5 -0
  409. package/src/state/red-streak.test.ts +28 -0
  410. package/src/state/red-streak.ts +28 -0
  411. package/src/state/runtime-state.ts +4 -0
  412. package/src/state/workspace-state.test.ts +7 -1
  413. package/src/state/workspace-state.ts +45 -3
  414. package/src/text/shell-regions.test.ts +38 -0
  415. package/src/text/shell-regions.ts +3 -3
  416. package/src/text/transcript-fold.test.ts +46 -0
  417. package/src/text/transcript-fold.ts +31 -9
  418. package/dist/chores/fix-deps.d.ts +0 -10
  419. package/dist/chores/fix-deps.d.ts.map +0 -1
  420. package/dist/chores/fix-deps.js +0 -18
  421. package/dist/chores/fix-deps.js.map +0 -1
  422. package/dist/protocol/ingress-protocol.d.ts +0 -30
  423. package/dist/protocol/ingress-protocol.d.ts.map +0 -1
  424. package/dist/protocol/ingress-protocol.js +0 -300
  425. package/dist/protocol/ingress-protocol.js.map +0 -1
  426. package/dist/protocol/terminal-protocol.d.ts +0 -18
  427. package/dist/protocol/terminal-protocol.d.ts.map +0 -1
  428. package/dist/protocol/terminal-protocol.js +0 -2
  429. package/dist/protocol/terminal-protocol.js.map +0 -1
  430. package/dist/protocol/tunnel-heartbeat.d.ts +0 -19
  431. package/dist/protocol/tunnel-heartbeat.d.ts.map +0 -1
  432. package/dist/protocol/tunnel-heartbeat.js +0 -41
  433. package/dist/protocol/tunnel-heartbeat.js.map +0 -1
  434. package/dist/protocol/upgrade-relay.d.ts +0 -4
  435. package/dist/protocol/upgrade-relay.d.ts.map +0 -1
  436. package/dist/protocol/upgrade-relay.js +0 -25
  437. package/dist/protocol/upgrade-relay.js.map +0 -1
  438. package/src/chores/fix-deps.ts +0 -22
  439. package/src/protocol/ingress-protocol.test.ts +0 -579
  440. package/src/protocol/ingress-protocol.ts +0 -469
  441. package/src/protocol/terminal-protocol.ts +0 -13
  442. package/src/protocol/tunnel-heartbeat.test.ts +0 -75
  443. package/src/protocol/tunnel-heartbeat.ts +0 -67
  444. package/src/protocol/upgrade-relay.ts +0 -28
@@ -16,6 +16,13 @@ import {
16
16
  WorkspaceGraphSchema,
17
17
  WorkspaceSyncSchema,
18
18
  } from "../schemas/workspace/workspace-repos.js";
19
+ import {
20
+ MainlinePushDismissResultSchema,
21
+ MainlinePushDismissSchema,
22
+ MainlinePushRecheckResultSchema,
23
+ MainlinePushRecheckSchema,
24
+ MainlineStatusSchema,
25
+ } from "../schemas/workspace/mainline.js";
19
26
  import { WorkspaceSearchQuerySchema, WorkspaceSearchResultSchema } from "../schemas/workspace/workspace-search.js";
20
27
  import { WorkspaceInstallResultSchema, WorkspaceInstallSchema, WorkspaceSetupSchema } from "../schemas/workspace/workspace-setup.js";
21
28
  import {
@@ -25,8 +32,11 @@ import {
25
32
  SidecarStatusSchema,
26
33
  WorkspaceDerivedQuerySchema,
27
34
  WorkspaceDerivedSchema,
35
+ WorkspaceDeletedSchema,
28
36
  WorkspaceDirSchema,
29
37
  WorkspaceExtractSchema,
38
+ WorkspaceRestoredSchema,
39
+ WorkspaceRestoreSchema,
30
40
  WorkspaceFileQuerySchema,
31
41
  WorkspaceFileReadQuerySchema,
32
42
  WorkspaceFileSchema,
@@ -191,11 +201,22 @@ export const workspaceContract = {
191
201
  path: "/workspace/entry",
192
202
  summary: "Delete a file or folder",
193
203
  description:
194
- "Removes one entry and everything under it. The path travels in the body rather than the address, the same as every other write in this group.",
204
+ "Removes one entry and everything under it. It goes to the trash rather than being erased, and the answer carries the id that brings it back through the restore call for a day. The path travels in the body rather than the address, the same as every other write in this group.",
195
205
  })
196
206
  .meta({ floor: "writer" })
197
207
  .input(WorkspaceFileQuerySchema)
198
- .output(OkSchema),
208
+ .output(WorkspaceDeletedSchema),
209
+ restore: procedure
210
+ .route({
211
+ method: "POST",
212
+ path: "/workspace/restore",
213
+ summary: "Bring back something deleted",
214
+ description:
215
+ "Puts an entry a delete sent to the trash back where it was, recreating the folders above it. Nothing is written over: when something new holds the name, it comes back beside it and the answer says where. Trash older than a day is gone.",
216
+ })
217
+ .meta({ floor: "writer" })
218
+ .input(WorkspaceRestoreSchema)
219
+ .output(WorkspaceRestoredSchema),
199
220
  move: procedure
200
221
  .route({
201
222
  method: "POST",
@@ -247,6 +268,37 @@ export const workspaceContract = {
247
268
  })
248
269
  .input(WorkspaceInstallSchema)
249
270
  .output(WorkspaceInstallResultSchema),
271
+ // Nothing is checked inside a turn; this is the check that runs instead, over the main tree after work lands.
272
+ mainline: procedure
273
+ .route({
274
+ method: "GET",
275
+ path: "/workspace/mainline",
276
+ summary: "The checks that run after work lands",
277
+ description:
278
+ "What the main tree's own check is measuring right now, which landed work waits for the next run, what the last run in each project said, and what became of a red one: sent back to the conversation that landed it, handed to a fresh conversation, or waiting on one still working. Nothing here ever holds a land, a commit or a push.",
279
+ })
280
+ .output(MainlineStatusSchema),
281
+ // What a push check found and let through waits here until measured gone or dismissed; these are the two hands on it.
282
+ mainlinePushDismiss: procedure
283
+ .route({
284
+ method: "POST",
285
+ path: "/workspace/mainline/push/dismiss",
286
+ summary: "Set aside what a push left behind",
287
+ description:
288
+ "Marks findings a push check let through as not to be fixed, so they stop counting against the project, or opens dismissed ones again. Nothing is changed in the code.",
289
+ })
290
+ .input(MainlinePushDismissSchema)
291
+ .output(MainlinePushDismissResultSchema),
292
+ mainlinePushRecheck: procedure
293
+ .route({
294
+ method: "POST",
295
+ path: "/workspace/mainline/push/recheck",
296
+ summary: "Measure what a push left behind again",
297
+ description:
298
+ "Runs the project's own push measurement over its main tree now and resolves every open finding it no longer prints. A few seconds; nothing is pushed.",
299
+ })
300
+ .input(MainlinePushRecheckSchema)
301
+ .output(MainlinePushRecheckResultSchema),
250
302
  repos: procedure
251
303
  .route({
252
304
  method: "GET",
@@ -0,0 +1,402 @@
1
+ // A stored document's history as data: each conversion is a guarded, pure rewrite of the raw JSON a store reads, run
2
+ // before the schema on every read. The guard makes a conversion a no-op on a document already past it, so running the
3
+ // whole list over a document from any era converges on today's shape without the document carrying a version, and
4
+ // every door old bytes arrive through (an update, a bundle, a git revert, a history restore) is covered by the read.
5
+ // Two rules keep that sound, and the vocabulary below only offers moves that obey them: a change of meaning ships as a
6
+ // rename, never as a new reading of the same key; and a retired key is never reused.
7
+
8
+ // The only thing a conversion rewrites: one JSON object, never mutated, replaced along the branch that changed.
9
+ export type JsonObject = Record<string, unknown>;
10
+
11
+ export const isJsonObject = (value: unknown): value is JsonObject => typeof value === "object" && value !== null && !Array.isArray(value);
12
+
13
+ // What one conversion did at one place in a document, for the ledger and the pre-flight plan. `at` is the path inside
14
+ // the document ("" is the document itself, "[3]" its fourth entry, "trigger" a nested object).
15
+ export interface ConversionChange {
16
+ readonly conversion: string;
17
+ readonly at: string;
18
+ readonly detail?: string;
19
+ }
20
+
21
+ interface ConversionBase<Kind extends string> {
22
+ readonly kind: Kind;
23
+ // One line a person reads in a plan or the ledger: "renames agentRunModel to agentRunModels".
24
+ readonly describe: string;
25
+ }
26
+
27
+ export interface RenameConversion<From extends string = string, To extends string = string> extends ConversionBase<"rename"> {
28
+ readonly from: From;
29
+ readonly to: To;
30
+ }
31
+
32
+ export interface DropConversion<Key extends string = string> extends ConversionBase<"drop"> {
33
+ readonly key: Key;
34
+ }
35
+
36
+ export interface RetypeConversion<Key extends string = string, In = unknown, Out = unknown> extends ConversionBase<"retype"> {
37
+ readonly key: Key;
38
+ // True only for the OLD representation; a converted value must fail it, or the conversion would run twice.
39
+ readonly guard: (value: unknown) => value is In;
40
+ // Method syntax on purpose: the guard proves `In` at runtime, so a union of conversions may hold any input type.
41
+ convert(value: In): Out;
42
+ }
43
+
44
+ export interface MapValueConversion<Key extends string = string, Mapping extends Readonly<Record<string, unknown>> = Readonly<Record<string, unknown>>>
45
+ extends ConversionBase<"mapValue"> {
46
+ readonly key: Key;
47
+ readonly mapping: Mapping;
48
+ }
49
+
50
+ export interface PinDefaultConversion<Key extends string = string, Value = unknown> extends ConversionBase<"pinDefault"> {
51
+ readonly key: Key;
52
+ readonly value: Value;
53
+ }
54
+
55
+ export interface TransformConversion<In extends JsonObject = JsonObject, Out extends JsonObject = JsonObject> extends ConversionBase<"transform"> {
56
+ // True only for the old shape; the output must fail it.
57
+ readonly applies: (value: JsonObject) => value is In;
58
+ // Method syntax on purpose, as for retype.
59
+ convert(value: In): Out;
60
+ }
61
+
62
+ // Several keys become one (three optional fields folded into a ladder): the old keys go, the new one takes the place of
63
+ // the first of them.
64
+ export interface FoldConversion<From extends string = string, Into extends string = string, Out = unknown> extends ConversionBase<"fold"> {
65
+ readonly from: readonly From[];
66
+ readonly into: Into;
67
+ // True only for the old shape; the folded output must fail it.
68
+ readonly applies: (value: JsonObject) => boolean;
69
+ // Method syntax on purpose, as for retype. `whole` is the object before folding, for a value derived from elsewhere.
70
+ convert(fields: Readonly<Partial<Record<From, unknown>>>, whole: JsonObject): Out;
71
+ }
72
+
73
+ // Entries of a list this version withdrew (a capability kind that no longer exists): removed, each one recorded. Only
74
+ // for entries nothing can convert; one with a successor is converted instead (mapValue, fold).
75
+ export interface RetireEntriesConversion<Retired = unknown> extends ConversionBase<"retireEntries"> {
76
+ readonly isRetired: (entry: unknown) => entry is Retired;
77
+ }
78
+
79
+ export interface AtConversion<Path extends string = string, Inner = unknown> extends ConversionBase<"at"> {
80
+ // Dotted keys; `*` is every element of an array or every value of an object.
81
+ readonly path: Path;
82
+ readonly inner: Inner;
83
+ }
84
+
85
+ export type Conversion =
86
+ | RenameConversion
87
+ | DropConversion
88
+ | RetypeConversion
89
+ | MapValueConversion
90
+ | PinDefaultConversion
91
+ | TransformConversion
92
+ | FoldConversion
93
+ | RetireEntriesConversion
94
+ | AtConversion<string, Conversion>;
95
+
96
+ // Where a document's conversions apply: the document itself, each entry of a top-level array, or each value of a
97
+ // top-level object keyed by id.
98
+ export type Granularity = "object" | "entries" | "record";
99
+
100
+ // The value a conversion leaves in a change record, short enough for a log line.
101
+ const glimpse = (value: unknown): string => {
102
+ // String() over the result, since JSON.stringify answers undefined for a value JSON cannot spell.
103
+ const spelled = String(JSON.stringify(value));
104
+ return spelled.length > 80 ? `${spelled.slice(0, 77)}…` : spelled;
105
+ };
106
+
107
+ // Keeps key order: a renamed key takes its predecessor's place, so a tracked file's diff shows one line changing.
108
+ const renamedKey = (target: JsonObject, from: string, to: string): JsonObject =>
109
+ Object.fromEntries(Object.entries(target).map(([key, value]) => [key === from ? to : key, value]));
110
+
111
+ const withoutKey = (target: JsonObject, key: string): JsonObject => Object.fromEntries(Object.entries(target).filter(([name]) => name !== key));
112
+
113
+ export const rename = <const From extends string, const To extends string>(from: From, to: To): RenameConversion<From, To> => ({
114
+ kind: "rename",
115
+ describe: `renames ${from} to ${to}`,
116
+ from,
117
+ to,
118
+ });
119
+
120
+ export const drop = <const Key extends string>(key: Key): DropConversion<Key> => ({ kind: "drop", describe: `drops ${key}`, key });
121
+
122
+ export const retype = <const Key extends string, In, Out>(
123
+ key: Key,
124
+ guard: (value: unknown) => value is In,
125
+ convert: (value: In) => Out,
126
+ describe = `converts ${key} to its new form`,
127
+ ): RetypeConversion<Key, In, Out> => ({ kind: "retype", describe, key, guard, convert });
128
+
129
+ // A mapped value that is itself a key of the mapping would be mapped again on the next read.
130
+ export const mapValue = <const Key extends string, const Mapping extends Readonly<Record<string, unknown>>>(
131
+ key: Key,
132
+ mapping: Mapping,
133
+ ): MapValueConversion<Key, Mapping> => {
134
+ const cyclic = Object.values(mapping).filter((mapped): mapped is string => typeof mapped === "string" && Object.hasOwn(mapping, mapped));
135
+ if (cyclic.length > 0) {
136
+ throw new Error(`mapValue(${key}) maps onto its own keys (${cyclic.join(", ")}), so it would not settle`);
137
+ }
138
+ return { kind: "mapValue", describe: `converts ${key} from its old values`, key, mapping };
139
+ };
140
+
141
+ // For a changed schema default: an existing file that never set the key keeps the old behavior, a new one gets the new.
142
+ export const pinDefault = <const Key extends string, const Value>(key: Key, value: Value): PinDefaultConversion<Key, Value> => ({
143
+ kind: "pinDefault",
144
+ describe: `keeps the earlier default for ${key}`,
145
+ key,
146
+ value,
147
+ });
148
+
149
+ export const transform = <In extends JsonObject, Out extends JsonObject>(
150
+ describe: string,
151
+ applies: (value: JsonObject) => value is In,
152
+ convert: (value: In) => Out,
153
+ ): TransformConversion<In, Out> => ({ kind: "transform", describe, applies, convert });
154
+
155
+ export const fold = <const From extends string, const Into extends string, Out>(
156
+ describe: string,
157
+ shape: {
158
+ readonly from: readonly From[];
159
+ readonly into: Into;
160
+ readonly applies: (value: JsonObject) => boolean;
161
+ readonly convert: (fields: Readonly<Partial<Record<From, unknown>>>, whole: JsonObject) => Out;
162
+ },
163
+ ): FoldConversion<From, Into, Out> => ({ kind: "fold", describe, from: shape.from, into: shape.into, applies: shape.applies, convert: shape.convert });
164
+
165
+ // Every key a schema once had and no longer has, dropped: retired, not from the future, so passthrough stops carrying it
166
+ // and no later schema may reuse the name. A tuple, so the type-level replay sees each key.
167
+ export const dropAll = <const Keys extends readonly string[]>(keys: Keys): { readonly [I in keyof Keys]: DropConversion<Keys[I] & string> } =>
168
+ keys.map((key) => drop(key)) as unknown as { readonly [I in keyof Keys]: DropConversion<Keys[I] & string> };
169
+
170
+ type Nested<Path extends string, History extends readonly Conversion[], Entries extends boolean> = {
171
+ readonly [I in keyof History]: History[I] extends RetireEntriesConversion
172
+ ? AtConversion<Path, History[I]>
173
+ : AtConversion<Entries extends true ? `${Path}.*` : Path, History[I]>;
174
+ };
175
+
176
+ // One document's history applied where another document carries it (a definition's `settings` table, a bundle's
177
+ // embedded definition): one declaration, every carrier. `entries` for a carried list, whose entries each convert and
178
+ // whose retired entries leave the list.
179
+ export const nested = <const Path extends string, const History extends readonly Conversion[], const Entries extends boolean = false>(
180
+ path: Path,
181
+ history: History,
182
+ entries?: Entries,
183
+ ): Nested<Path, History, Entries> =>
184
+ history.map((conversion) => at(conversion.kind === "retireEntries" || entries !== true ? path : `${path}.*`, conversion)) as unknown as Nested<
185
+ Path,
186
+ History,
187
+ Entries
188
+ >;
189
+
190
+ export const retireEntries = <Retired>(describe: string, isRetired: (entry: unknown) => entry is Retired): RetireEntriesConversion<Retired> => ({
191
+ kind: "retireEntries",
192
+ describe,
193
+ isRetired,
194
+ });
195
+
196
+ export const at = <const Path extends string, const Inner extends Conversion>(path: Path, inner: Inner): AtConversion<Path, Inner> => ({
197
+ kind: "at",
198
+ describe: `${inner.describe} under ${path}`,
199
+ path,
200
+ inner,
201
+ });
202
+
203
+ type Report = (change: ConversionChange) => void;
204
+
205
+ // One conversion over one object: the same object back, by reference, when its guard says it does not apply.
206
+ const applyToObject = (conversion: Conversion, target: JsonObject, where: string, report: Report): JsonObject => {
207
+ switch (conversion.kind) {
208
+ case "rename": {
209
+ if (!Object.hasOwn(target, conversion.from)) {
210
+ return target;
211
+ }
212
+ if (!Object.hasOwn(target, conversion.to)) {
213
+ report({ conversion: conversion.describe, at: where });
214
+ return renamedKey(target, conversion.from, conversion.to);
215
+ }
216
+ // Both names holding the same value (a file a daemon before 2026-09-25 wrote under both during a rename's
217
+ // grace window, or one edited by hand): nothing to report, nothing for the file to lose.
218
+ if (JSON.stringify(target[conversion.from]) === JSON.stringify(target[conversion.to])) {
219
+ return withoutKey(target, conversion.from);
220
+ }
221
+ // Both names present and different (an old key back through git, or a rolled-back build's edit):
222
+ // the current name wins, and what the older one held is recorded rather than lost without a word.
223
+ report({ conversion: conversion.describe, at: where, detail: `kept ${conversion.to}; ${conversion.from} held ${glimpse(target[conversion.from])}` });
224
+ return withoutKey(target, conversion.from);
225
+ }
226
+ case "drop": {
227
+ if (!Object.hasOwn(target, conversion.key)) {
228
+ return target;
229
+ }
230
+ report({ conversion: conversion.describe, at: where, detail: `it held ${glimpse(target[conversion.key])}` });
231
+ return withoutKey(target, conversion.key);
232
+ }
233
+ case "retype": {
234
+ const value = target[conversion.key];
235
+ if (!Object.hasOwn(target, conversion.key) || !conversion.guard(value)) {
236
+ return target;
237
+ }
238
+ report({ conversion: conversion.describe, at: where });
239
+ return { ...target, [conversion.key]: conversion.convert(value) };
240
+ }
241
+ case "mapValue": {
242
+ const value = target[conversion.key];
243
+ if (typeof value !== "string" || !Object.hasOwn(conversion.mapping, value)) {
244
+ return target;
245
+ }
246
+ report({ conversion: conversion.describe, at: where, detail: `${glimpse(value)} became ${glimpse(conversion.mapping[value])}` });
247
+ return { ...target, [conversion.key]: conversion.mapping[value] };
248
+ }
249
+ case "pinDefault": {
250
+ if (Object.hasOwn(target, conversion.key)) {
251
+ return target;
252
+ }
253
+ report({ conversion: conversion.describe, at: where });
254
+ return { ...target, [conversion.key]: conversion.value };
255
+ }
256
+ case "transform": {
257
+ if (!conversion.applies(target)) {
258
+ return target;
259
+ }
260
+ report({ conversion: conversion.describe, at: where });
261
+ return conversion.convert(target);
262
+ }
263
+ case "fold":
264
+ return applyFold(conversion, target, where, report);
265
+ case "retireEntries":
266
+ // An object is not a list; retiring acts on the list holding it (applyOne, applyAt).
267
+ return target;
268
+ case "at":
269
+ return applyAt(conversion.inner, conversion.path.split("."), target, where, report) as JsonObject;
270
+ }
271
+ };
272
+
273
+ const applyFold = (conversion: FoldConversion, target: JsonObject, where: string, report: Report): JsonObject => {
274
+ if (!conversion.applies(target)) {
275
+ return target;
276
+ }
277
+ const folded = new Set<string>(conversion.from);
278
+ const fields = Object.fromEntries(Object.entries(target).filter(([key]) => folded.has(key)));
279
+ const entries = Object.entries(target);
280
+ const first = entries.findIndex(([key]) => folded.has(key));
281
+ const kept = entries.filter(([key]) => !folded.has(key) && key !== conversion.into);
282
+ kept.splice(first === -1 ? kept.length : Math.min(first, kept.length), 0, [conversion.into, conversion.convert(fields, target)]);
283
+ report({ conversion: conversion.describe, at: where });
284
+ return Object.fromEntries(kept);
285
+ };
286
+
287
+ // Removes a list's withdrawn entries, by index for an array and by key for an object keyed by id.
288
+ const retireFrom = (conversion: RetireEntriesConversion, container: unknown, where: string, report: Report): unknown => {
289
+ const retired = (entry: unknown, place: string): boolean => {
290
+ if (!conversion.isRetired(entry)) {
291
+ return false;
292
+ }
293
+ report({ conversion: conversion.describe, at: place, detail: glimpse(entry) });
294
+ return true;
295
+ };
296
+ if (Array.isArray(container)) {
297
+ const kept = container.filter((entry, index) => !retired(entry, joinWhere(where, `[${index}]`)));
298
+ return kept.length === container.length ? container : kept;
299
+ }
300
+ if (!isJsonObject(container)) {
301
+ return container;
302
+ }
303
+ const kept = Object.entries(container).filter(([key, entry]) => !retired(entry, joinWhere(where, key)));
304
+ return kept.length === Object.keys(container).length ? container : Object.fromEntries(kept);
305
+ };
306
+
307
+ const joinWhere = (where: string, segment: string): string => (segment.startsWith("[") || where === "" ? `${where}${segment}` : `${where}.${segment}`);
308
+
309
+ // Every element of an array or every value of an object, rebuilt only where one of them changed.
310
+ const applyToEach = (visit: (value: unknown, where: string) => unknown, container: unknown, where: string): unknown => {
311
+ if (Array.isArray(container)) {
312
+ const next = container.map((value, index) => visit(value, joinWhere(where, `[${index}]`)));
313
+ return next.some((value, index) => value !== container[index]) ? next : container;
314
+ }
315
+ if (!isJsonObject(container)) {
316
+ return container;
317
+ }
318
+ const entries = Object.entries(container);
319
+ const next = entries.map(([key, value]) => [key, visit(value, joinWhere(where, key))] as const);
320
+ return next.some(([, value], index) => value !== entries[index]?.[1]) ? Object.fromEntries(next) : container;
321
+ };
322
+
323
+ const applyAt = (inner: Conversion, segments: readonly string[], value: unknown, where: string, report: Report): unknown => {
324
+ const [head, ...rest] = segments;
325
+ if (head === undefined) {
326
+ if (inner.kind === "retireEntries") {
327
+ return retireFrom(inner, value, where, report);
328
+ }
329
+ return isJsonObject(value) ? applyToObject(inner, value, where, report) : value;
330
+ }
331
+ if (head === "*") {
332
+ return applyToEach((element, elementWhere) => applyAt(inner, rest, element, elementWhere, report), value, where);
333
+ }
334
+ if (!isJsonObject(value) || !Object.hasOwn(value, head)) {
335
+ return value;
336
+ }
337
+ const child = value[head];
338
+ const next = applyAt(inner, rest, child, joinWhere(where, head), report);
339
+ return next === child ? value : { ...value, [head]: next };
340
+ };
341
+
342
+ // One conversion over a whole document, at its granularity.
343
+ const applyOne = (conversion: Conversion, granularity: Granularity, value: unknown, report: Report): unknown => {
344
+ if (conversion.kind === "retireEntries" && granularity !== "object") {
345
+ return retireFrom(conversion, value, "", report);
346
+ }
347
+ if (granularity === "object") {
348
+ return isJsonObject(value) ? applyToObject(conversion, value, "", report) : value;
349
+ }
350
+ const container = granularity === "entries" ? Array.isArray(value) : isJsonObject(value);
351
+ return container ? applyToEach((entry, where) => (isJsonObject(entry) ? applyToObject(conversion, entry, where, report) : entry), value, "") : value;
352
+ };
353
+
354
+ export class ConversionError extends Error {
355
+ readonly conversion: string;
356
+
357
+ constructor(conversion: string, cause: unknown) {
358
+ super(`conversion "${conversion}" failed: ${cause instanceof Error ? cause.message : String(cause)}`, { cause });
359
+ this.name = "ConversionError";
360
+ this.conversion = conversion;
361
+ }
362
+ }
363
+
364
+ // Every conversion in declaration order; a throw is named after the conversion that threw, not the last that reported.
365
+ const applyAll = (conversions: readonly Conversion[], granularity: Granularity, raw: unknown, report: Report): unknown => {
366
+ let value = raw;
367
+ for (const conversion of conversions) {
368
+ try {
369
+ value = applyOne(conversion, granularity, value, report);
370
+ } catch (error) {
371
+ throw new ConversionError(conversion.describe, error);
372
+ }
373
+ }
374
+ return value;
375
+ };
376
+
377
+ export interface Converted {
378
+ readonly value: unknown;
379
+ readonly changes: readonly ConversionChange[];
380
+ }
381
+
382
+ // `checkSettles` runs the list a second time over its own output and fails a conversion that is not a no-op there: what
383
+ // a development build turns on, so the first read that reaches such a conversion fails instead of every read after it
384
+ // quietly rewriting the file.
385
+ export const convertDocument = (conversions: readonly Conversion[], granularity: Granularity, raw: unknown, checkSettles = false): Converted => {
386
+ if (conversions.length === 0) {
387
+ return { value: raw, changes: [] };
388
+ }
389
+ const changes: ConversionChange[] = [];
390
+ const value = applyAll(conversions, granularity, raw, (change) => changes.push(change));
391
+ if (checkSettles && changes.length > 0) {
392
+ const again: ConversionChange[] = [];
393
+ const settled = applyAll(conversions, granularity, value, (change) => again.push(change));
394
+ if (settled !== value || again.length > 0) {
395
+ throw new ConversionError(again[0]?.conversion ?? "unknown", new Error("it changed its own output, so it would run on every read"));
396
+ }
397
+ }
398
+ return { value, changes };
399
+ };
400
+
401
+ // A document's conversions as one-line descriptions, for a plan that has nothing to convert but still names the chain.
402
+ export const describeConversions = (conversions: readonly Conversion[]): string[] => conversions.map((conversion) => conversion.describe);
@@ -0,0 +1,7 @@
1
+ // Stored-document evolution shared by everything that keeps a file across versions: the daemon's stores, and an
2
+ // extension's own files through `sandboxDocument` (@intentic/extension-api). The vocabulary of guarded, pure
3
+ // conversions, the passthrough that keeps what a build does not know on its writes, and the one read (read.ts) that
4
+ // puts the two together.
5
+ export * from "./conversions.js";
6
+ export * from "./passthrough.js";
7
+ export * from "./read.js";
@@ -0,0 +1,139 @@
1
+ import { isJsonObject, type JsonObject } from "./conversions.js";
2
+
3
+ // Carries into a write what the file held that this build's parse dropped: keys a newer build added, and entries this
4
+ // build cannot read. Loose schemas already let an older build READ a newer file; without this, its next write would
5
+ // quietly delete everything it did not understand, so a rollback cost the newer version's data on the first save.
6
+ // What counts as "unknown" is exactly what parse stripped, so a key this build knows and the change removed stays gone,
7
+ // and a key a conversion retired (drop, rename) was removed before parse and is never carried.
8
+
9
+ // A key whose value names which arm of a union an object is: an object that switched arms carries nothing across, since
10
+ // the old arm's extra keys mean nothing on the new one.
11
+ const DISCRIMINATORS = ["kind", "type"] as const;
12
+
13
+ const sameArm = (parsed: JsonObject, updated: JsonObject): boolean =>
14
+ DISCRIMINATORS.every((key) => typeof parsed[key] !== "string" || parsed[key] === updated[key]);
15
+
16
+ // Which keys name an entry, tried in order: most entries carry `id`, a few (workflow runs) their own.
17
+ export type IdKeys = readonly string[];
18
+ const DEFAULT_ID_KEYS: IdKeys = ["id"];
19
+
20
+ const carryObject = (raw: JsonObject, parsed: JsonObject, updated: JsonObject, idKeys: IdKeys): JsonObject => {
21
+ if (!sameArm(parsed, updated)) {
22
+ return updated;
23
+ }
24
+ let result: JsonObject | undefined;
25
+ for (const [key, value] of Object.entries(raw)) {
26
+ if (!Object.hasOwn(updated, key)) {
27
+ // Stripped by parse and not set by the change: a key from a newer build (or a typo, which stays reported).
28
+ if (!Object.hasOwn(parsed, key)) {
29
+ result ??= { ...updated };
30
+ result[key] = value;
31
+ }
32
+ continue;
33
+ }
34
+ if (Object.hasOwn(parsed, key)) {
35
+ const carried = carryUnknown(value, parsed[key], updated[key], idKeys);
36
+ if (carried !== updated[key]) {
37
+ result ??= { ...updated };
38
+ result[key] = carried;
39
+ }
40
+ }
41
+ }
42
+ return result ?? updated;
43
+ };
44
+
45
+ const idOf = (value: unknown, idKeys: IdKeys): string | undefined => {
46
+ if (!isJsonObject(value)) {
47
+ return undefined;
48
+ }
49
+ const id = idKeys.map((key) => value[key]).find((candidate) => typeof candidate === "string");
50
+ return typeof id === "string" ? id : undefined;
51
+ };
52
+
53
+ // Pairs each written element with the raw element it came from: by reference when the change kept the parsed object,
54
+ // by id when it rebuilt it. Only a parse that kept every element lines raw up with parsed by position.
55
+ const carryArray = (raw: readonly unknown[], parsed: readonly unknown[], updated: readonly unknown[], idKeys: IdKeys): readonly unknown[] => {
56
+ if (raw.length !== parsed.length) {
57
+ return updated;
58
+ }
59
+ const rawOfParsed = new Map<unknown, unknown>();
60
+ const indexOfId = new Map<string, number>();
61
+ parsed.forEach((element, index) => {
62
+ if (typeof element === "object" && element !== null) {
63
+ rawOfParsed.set(element, raw[index]);
64
+ }
65
+ const id = idOf(element, idKeys);
66
+ if (id !== undefined) {
67
+ indexOfId.set(id, index);
68
+ }
69
+ });
70
+ let result: unknown[] | undefined;
71
+ updated.forEach((element, index) => {
72
+ const byId = indexOfId.get(idOf(element, idKeys) ?? "");
73
+ const [source, before] = rawOfParsed.has(element) ? [rawOfParsed.get(element), element] : byId === undefined ? [undefined, undefined] : [raw[byId], parsed[byId]];
74
+ if (source === undefined) {
75
+ return;
76
+ }
77
+ const carried = carryUnknown(source, before, element, idKeys);
78
+ if (carried !== element) {
79
+ result ??= [...updated];
80
+ result[index] = carried;
81
+ }
82
+ });
83
+ return result ?? updated;
84
+ };
85
+
86
+ // `raw` is the file as read (after conversions), `parsed` what the schema made of it, `updated` what the change returns.
87
+ // Answers `updated` itself, by reference, when nothing needs carrying.
88
+ export const carryUnknown = (raw: unknown, parsed: unknown, updated: unknown, idKeys: IdKeys = DEFAULT_ID_KEYS): unknown => {
89
+ if (isJsonObject(raw) && isJsonObject(parsed) && isJsonObject(updated)) {
90
+ return carryObject(raw, parsed, updated, idKeys);
91
+ }
92
+ if (Array.isArray(raw) && Array.isArray(parsed) && Array.isArray(updated)) {
93
+ return carryArray(raw, parsed, updated, idKeys);
94
+ }
95
+ return updated;
96
+ };
97
+
98
+ // An entry list read one entry at a time: the entries this build can use, the raw entries lined up with them, and the
99
+ // ones it could not read, each at the index it held.
100
+ export interface EntryRead<E> {
101
+ readonly entries: E[];
102
+ readonly aligned: readonly unknown[];
103
+ readonly quarantined: readonly { readonly index: number; readonly entry: unknown }[];
104
+ }
105
+
106
+ export const readEntries = <E>(raw: readonly unknown[], parseEntry: (entry: unknown) => E | undefined): EntryRead<E> => {
107
+ const entries: E[] = [];
108
+ const aligned: unknown[] = [];
109
+ const quarantined: { index: number; entry: unknown }[] = [];
110
+ raw.forEach((entry, index) => {
111
+ const parsed = parseEntry(entry);
112
+ if (parsed === undefined) {
113
+ quarantined.push({ index, entry });
114
+ return;
115
+ }
116
+ entries.push(parsed);
117
+ aligned.push(entry);
118
+ });
119
+ return { entries, aligned, quarantined };
120
+ };
121
+
122
+ // Puts the entries this build could not read back into what it writes, near where they were; one the write now holds a
123
+ // same-id entry for was replaced on purpose and stays out.
124
+ export const reemitQuarantined = (
125
+ updated: readonly unknown[],
126
+ quarantined: EntryRead<unknown>["quarantined"],
127
+ idKeys: IdKeys = DEFAULT_ID_KEYS,
128
+ ): readonly unknown[] => {
129
+ const written = new Set(updated.map((entry) => idOf(entry, idKeys)).filter((id) => id !== undefined));
130
+ const kept = quarantined.filter(({ entry }) => !written.has(idOf(entry, idKeys) ?? "\u0000"));
131
+ if (kept.length === 0) {
132
+ return updated;
133
+ }
134
+ const result = [...updated];
135
+ for (const { index, entry } of kept) {
136
+ result.splice(Math.min(index, result.length), 0, entry);
137
+ }
138
+ return result;
139
+ };