@amalgm/shell 0.1.164-preview.36072132286 → 0.1.164-preview.36809057014

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 (475) hide show
  1. package/PURPOSE.md +642 -128
  2. package/README.md +10 -8
  3. package/dist/accepted-tail.d.ts +26 -0
  4. package/dist/accepted-tail.js +37 -0
  5. package/dist/accepted-tail.js.map +1 -0
  6. package/dist/agent-execution.d.ts +16 -1
  7. package/dist/agent-execution.js +13 -5
  8. package/dist/agent-execution.js.map +1 -1
  9. package/dist/apply-cleanup.d.ts +28 -0
  10. package/dist/apply-cleanup.js +166 -0
  11. package/dist/apply-cleanup.js.map +1 -0
  12. package/dist/apply-recovery.d.ts +47 -0
  13. package/dist/apply-recovery.js +99 -0
  14. package/dist/apply-recovery.js.map +1 -0
  15. package/dist/apply-rows.d.ts +72 -0
  16. package/dist/apply-rows.js +150 -0
  17. package/dist/apply-rows.js.map +1 -0
  18. package/dist/apply-saves.d.ts +85 -0
  19. package/dist/apply-saves.js +238 -0
  20. package/dist/apply-saves.js.map +1 -0
  21. package/dist/boot.js +2 -1
  22. package/dist/boot.js.map +1 -1
  23. package/dist/capture-work.d.ts +8 -0
  24. package/dist/capture-work.js +67 -0
  25. package/dist/capture-work.js.map +1 -0
  26. package/dist/cargo-group.d.ts +45 -0
  27. package/dist/cargo-group.js +88 -0
  28. package/dist/cargo-group.js.map +1 -0
  29. package/dist/channel-session.d.ts +4 -2
  30. package/dist/channel-session.js +4 -3
  31. package/dist/channel-session.js.map +1 -1
  32. package/dist/chat-composition.d.ts +2 -0
  33. package/dist/chat-composition.js +1 -0
  34. package/dist/chat-composition.js.map +1 -1
  35. package/dist/cli.js +20 -1
  36. package/dist/cli.js.map +1 -1
  37. package/dist/construct-apply-host.d.ts +4 -2
  38. package/dist/construct-apply-host.js +8 -3
  39. package/dist/construct-apply-host.js.map +1 -1
  40. package/dist/construct-door.js +1 -1
  41. package/dist/construct-door.js.map +1 -1
  42. package/dist/construct-ledger.d.ts +6 -1
  43. package/dist/construct-ledger.js +21 -33
  44. package/dist/construct-ledger.js.map +1 -1
  45. package/dist/content-cache-collection.d.ts +18 -0
  46. package/dist/content-cache-collection.js +99 -0
  47. package/dist/content-cache-collection.js.map +1 -0
  48. package/dist/content-cache-host.d.ts +83 -17
  49. package/dist/content-cache-host.js +335 -95
  50. package/dist/content-cache-host.js.map +1 -1
  51. package/dist/content-cache-namespace.d.ts +59 -0
  52. package/dist/content-cache-namespace.js +210 -0
  53. package/dist/content-cache-namespace.js.map +1 -0
  54. package/dist/content-cache-ownership.d.ts +13 -0
  55. package/dist/content-cache-ownership.js +75 -0
  56. package/dist/content-cache-ownership.js.map +1 -0
  57. package/dist/content-cache-retention.d.ts +23 -0
  58. package/dist/content-cache-retention.js +195 -0
  59. package/dist/content-cache-retention.js.map +1 -0
  60. package/dist/content-capture-pool.d.ts +287 -0
  61. package/dist/content-capture-pool.js +461 -0
  62. package/dist/content-capture-pool.js.map +1 -0
  63. package/dist/content-capture-worker.d.ts +8 -0
  64. package/dist/content-capture-worker.js +109 -0
  65. package/dist/content-capture-worker.js.map +1 -0
  66. package/dist/content-clock.d.ts +42 -0
  67. package/dist/content-clock.js +166 -0
  68. package/dist/content-clock.js.map +1 -0
  69. package/dist/content-direct.d.ts +2 -0
  70. package/dist/content-direct.js +12 -0
  71. package/dist/content-direct.js.map +1 -1
  72. package/dist/content-failure.d.ts +18 -0
  73. package/dist/content-failure.js +38 -0
  74. package/dist/content-failure.js.map +1 -0
  75. package/dist/content-stream-thread.d.ts +60 -0
  76. package/dist/content-stream-thread.js +95 -0
  77. package/dist/content-stream-thread.js.map +1 -0
  78. package/dist/content-stream-worker.d.ts +7 -0
  79. package/dist/content-stream-worker.js +52 -0
  80. package/dist/content-stream-worker.js.map +1 -0
  81. package/dist/declaration-absorption.d.ts +37 -0
  82. package/dist/declaration-absorption.js +119 -0
  83. package/dist/declaration-absorption.js.map +1 -0
  84. package/dist/declaration-chain.d.ts +36 -0
  85. package/dist/declaration-chain.js +96 -0
  86. package/dist/declaration-chain.js.map +1 -0
  87. package/dist/declaration-commit.d.ts +31 -0
  88. package/dist/declaration-commit.js +49 -0
  89. package/dist/declaration-commit.js.map +1 -0
  90. package/dist/declaration-hold.d.ts +22 -0
  91. package/dist/declaration-hold.js +24 -0
  92. package/dist/declaration-hold.js.map +1 -0
  93. package/dist/declaration-journal.d.ts +62 -0
  94. package/dist/declaration-journal.js +99 -0
  95. package/dist/declaration-journal.js.map +1 -0
  96. package/dist/declaration-save.d.ts +31 -0
  97. package/dist/declaration-save.js +134 -0
  98. package/dist/declaration-save.js.map +1 -0
  99. package/dist/detection/accepted-result.d.ts +19 -1
  100. package/dist/detection/accepted-result.js +36 -5
  101. package/dist/detection/accepted-result.js.map +1 -1
  102. package/dist/detection/admission-save.d.ts +55 -0
  103. package/dist/detection/admission-save.js +48 -0
  104. package/dist/detection/admission-save.js.map +1 -0
  105. package/dist/detection/admission-units.d.ts +66 -0
  106. package/dist/detection/admission-units.js +322 -0
  107. package/dist/detection/admission-units.js.map +1 -0
  108. package/dist/detection/alias-discovery.d.ts +12 -0
  109. package/dist/detection/alias-discovery.js +80 -0
  110. package/dist/detection/alias-discovery.js.map +1 -0
  111. package/dist/detection/alias-examination.d.ts +30 -0
  112. package/dist/detection/alias-examination.js +49 -0
  113. package/dist/detection/alias-examination.js.map +1 -0
  114. package/dist/detection/apply-ownership.d.ts +81 -0
  115. package/dist/detection/apply-ownership.js +232 -0
  116. package/dist/detection/apply-ownership.js.map +1 -0
  117. package/dist/detection/basis-advanced.d.ts +11 -0
  118. package/dist/detection/basis-advanced.js +26 -0
  119. package/dist/detection/basis-advanced.js.map +1 -0
  120. package/dist/detection/capture-outcome.d.ts +16 -0
  121. package/dist/detection/capture-outcome.js +92 -0
  122. package/dist/detection/capture-outcome.js.map +1 -0
  123. package/dist/detection/container-membership.d.ts +32 -0
  124. package/dist/detection/container-membership.js +49 -0
  125. package/dist/detection/container-membership.js.map +1 -0
  126. package/dist/detection/detect-cancelled.d.ts +6 -0
  127. package/dist/detection/detect-cancelled.js +10 -0
  128. package/dist/detection/detect-cancelled.js.map +1 -0
  129. package/dist/detection/directory-proof.d.ts +47 -0
  130. package/dist/detection/directory-proof.js +77 -0
  131. package/dist/detection/directory-proof.js.map +1 -0
  132. package/dist/detection/disposition-rows.d.ts +18 -0
  133. package/dist/detection/disposition-rows.js +29 -0
  134. package/dist/detection/disposition-rows.js.map +1 -0
  135. package/dist/detection/dispositions.d.ts +130 -0
  136. package/dist/detection/dispositions.js +248 -0
  137. package/dist/detection/dispositions.js.map +1 -0
  138. package/dist/detection/git-notice.d.ts +88 -0
  139. package/dist/detection/git-notice.js +444 -0
  140. package/dist/detection/git-notice.js.map +1 -0
  141. package/dist/detection/ground-proof.d.ts +130 -0
  142. package/dist/detection/ground-proof.js +274 -0
  143. package/dist/detection/ground-proof.js.map +1 -0
  144. package/dist/detection/ground-reader.d.ts +29 -0
  145. package/dist/detection/ground-reader.js +102 -0
  146. package/dist/detection/ground-reader.js.map +1 -0
  147. package/dist/detection/held-territory.d.ts +51 -0
  148. package/dist/detection/held-territory.js +205 -0
  149. package/dist/detection/held-territory.js.map +1 -0
  150. package/dist/detection/identity-discovery.d.ts +45 -0
  151. package/dist/detection/identity-discovery.js +287 -0
  152. package/dist/detection/identity-discovery.js.map +1 -0
  153. package/dist/detection/maintenance-sweep.d.ts +108 -0
  154. package/dist/detection/maintenance-sweep.js +325 -0
  155. package/dist/detection/maintenance-sweep.js.map +1 -0
  156. package/dist/detection/name-reach.d.ts +12 -0
  157. package/dist/detection/name-reach.js +28 -0
  158. package/dist/detection/name-reach.js.map +1 -0
  159. package/dist/detection/notebook-basis.d.ts +5 -7
  160. package/dist/detection/notebook-basis.js +6 -7
  161. package/dist/detection/notebook-basis.js.map +1 -1
  162. package/dist/detection/owed-selection.d.ts +51 -0
  163. package/dist/detection/owed-selection.js +169 -0
  164. package/dist/detection/owed-selection.js.map +1 -0
  165. package/dist/detection/portable.d.ts +34 -34
  166. package/dist/detection/portable.js +67 -93
  167. package/dist/detection/portable.js.map +1 -1
  168. package/dist/detection/reconciliation-coverage.d.ts +7 -0
  169. package/dist/detection/reconciliation-coverage.js +16 -0
  170. package/dist/detection/reconciliation-coverage.js.map +1 -0
  171. package/dist/detection/repository-publication.d.ts +20 -0
  172. package/dist/detection/repository-publication.js +79 -0
  173. package/dist/detection/repository-publication.js.map +1 -0
  174. package/dist/detection/repository-routes.d.ts +43 -0
  175. package/dist/detection/repository-routes.js +97 -0
  176. package/dist/detection/repository-routes.js.map +1 -0
  177. package/dist/detection/root-examination.d.ts +94 -0
  178. package/dist/detection/root-examination.js +523 -0
  179. package/dist/detection/root-examination.js.map +1 -0
  180. package/dist/detection/runtime.d.ts +131 -64
  181. package/dist/detection/runtime.js +808 -1103
  182. package/dist/detection/runtime.js.map +1 -1
  183. package/dist/detection/scoped-identity-port.d.ts +34 -0
  184. package/dist/detection/scoped-identity-port.js +81 -0
  185. package/dist/detection/scoped-identity-port.js.map +1 -0
  186. package/dist/detection/seal-law.d.ts +66 -0
  187. package/dist/detection/seal-law.js +116 -0
  188. package/dist/detection/seal-law.js.map +1 -0
  189. package/dist/detection/sharing-policy.d.ts +11 -0
  190. package/dist/detection/sharing-policy.js +32 -0
  191. package/dist/detection/sharing-policy.js.map +1 -0
  192. package/dist/detection/thread-database.d.ts +4 -0
  193. package/dist/detection/thread-database.js +30 -0
  194. package/dist/detection/thread-database.js.map +1 -0
  195. package/dist/detection/unreadable.d.ts +11 -0
  196. package/dist/detection/unreadable.js +9 -0
  197. package/dist/detection/unreadable.js.map +1 -0
  198. package/dist/detection/verified-content.d.ts +6 -0
  199. package/dist/detection/verified-content.js +12 -0
  200. package/dist/detection/verified-content.js.map +1 -0
  201. package/dist/detection/walk-law.d.ts +34 -0
  202. package/dist/detection/walk-law.js +55 -0
  203. package/dist/detection/walk-law.js.map +1 -0
  204. package/dist/development/fly-guest.js +2 -1
  205. package/dist/development/fly-guest.js.map +1 -1
  206. package/dist/entity-apply-host.d.ts +29 -13
  207. package/dist/entity-apply-host.js +1426 -461
  208. package/dist/entity-apply-host.js.map +1 -1
  209. package/dist/entity-apply-store.d.ts +77 -30
  210. package/dist/entity-apply-store.js +221 -516
  211. package/dist/entity-apply-store.js.map +1 -1
  212. package/dist/entity-record-observability.js +2 -0
  213. package/dist/entity-record-observability.js.map +1 -1
  214. package/dist/entity-record-saves.d.ts +20 -0
  215. package/dist/entity-record-saves.js +91 -0
  216. package/dist/entity-record-saves.js.map +1 -0
  217. package/dist/entity-record-store.d.ts +15 -7
  218. package/dist/entity-record-store.js +57 -117
  219. package/dist/entity-record-store.js.map +1 -1
  220. package/dist/files-register-host.d.ts +22 -3
  221. package/dist/files-register-host.js +70 -31
  222. package/dist/files-register-host.js.map +1 -1
  223. package/dist/filesystem.js +3 -2
  224. package/dist/filesystem.js.map +1 -1
  225. package/dist/git-binding.d.ts +65 -0
  226. package/dist/git-binding.js +220 -0
  227. package/dist/git-binding.js.map +1 -0
  228. package/dist/git-capture.d.ts +39 -0
  229. package/dist/git-capture.js +86 -0
  230. package/dist/git-capture.js.map +1 -0
  231. package/dist/git-card.d.ts +12 -0
  232. package/dist/git-card.js +123 -0
  233. package/dist/git-card.js.map +1 -0
  234. package/dist/git-checkpoint-construction.d.ts +41 -0
  235. package/dist/git-checkpoint-construction.js +96 -0
  236. package/dist/git-checkpoint-construction.js.map +1 -0
  237. package/dist/git-checkpoint-preparation.d.ts +37 -0
  238. package/dist/git-checkpoint-preparation.js +145 -0
  239. package/dist/git-checkpoint-preparation.js.map +1 -0
  240. package/dist/git-command.d.ts +61 -8
  241. package/dist/git-command.js +217 -63
  242. package/dist/git-command.js.map +1 -1
  243. package/dist/git-configuration.d.ts +40 -0
  244. package/dist/git-configuration.js +217 -0
  245. package/dist/git-configuration.js.map +1 -0
  246. package/dist/git-evidence.d.ts +61 -0
  247. package/dist/git-evidence.js +181 -0
  248. package/dist/git-evidence.js.map +1 -0
  249. package/dist/git-index-entries.d.ts +15 -0
  250. package/dist/git-index-entries.js +44 -0
  251. package/dist/git-index-entries.js.map +1 -0
  252. package/dist/git-index-format.d.ts +35 -0
  253. package/dist/git-index-format.js +293 -0
  254. package/dist/git-index-format.js.map +1 -0
  255. package/dist/git-index-witness.d.ts +33 -0
  256. package/dist/git-index-witness.js +69 -0
  257. package/dist/git-index-witness.js.map +1 -0
  258. package/dist/git-literal-host.d.ts +82 -0
  259. package/dist/git-literal-host.js +172 -0
  260. package/dist/git-literal-host.js.map +1 -0
  261. package/dist/git-literal-read.d.ts +88 -0
  262. package/dist/git-literal-read.js +170 -0
  263. package/dist/git-literal-read.js.map +1 -0
  264. package/dist/git-literal-replay.d.ts +33 -0
  265. package/dist/git-literal-replay.js +268 -0
  266. package/dist/git-literal-replay.js.map +1 -0
  267. package/dist/git-native-file.d.ts +42 -0
  268. package/dist/git-native-file.js +145 -0
  269. package/dist/git-native-file.js.map +1 -0
  270. package/dist/git-native-index.d.ts +21 -0
  271. package/dist/git-native-index.js +44 -0
  272. package/dist/git-native-index.js.map +1 -0
  273. package/dist/git-operation-host.d.ts +48 -0
  274. package/dist/git-operation-host.js +127 -0
  275. package/dist/git-operation-host.js.map +1 -0
  276. package/dist/git-private-index.d.ts +11 -0
  277. package/dist/git-private-index.js +42 -0
  278. package/dist/git-private-index.js.map +1 -0
  279. package/dist/git-private-repository.d.ts +4 -0
  280. package/dist/git-private-repository.js +69 -0
  281. package/dist/git-private-repository.js.map +1 -0
  282. package/dist/git-private-view.d.ts +39 -0
  283. package/dist/git-private-view.js +162 -0
  284. package/dist/git-private-view.js.map +1 -0
  285. package/dist/git-ref-store.d.ts +12 -0
  286. package/dist/git-ref-store.js +68 -0
  287. package/dist/git-ref-store.js.map +1 -0
  288. package/dist/git-registration-host.d.ts +20 -30
  289. package/dist/git-registration-host.js +86 -249
  290. package/dist/git-registration-host.js.map +1 -1
  291. package/dist/git-repository-host.d.ts +148 -22
  292. package/dist/git-repository-host.js +633 -650
  293. package/dist/git-repository-host.js.map +1 -1
  294. package/dist/git-worktree-host.d.ts +2 -0
  295. package/dist/git-worktree-host.js +16 -0
  296. package/dist/git-worktree-host.js.map +1 -0
  297. package/dist/graph-publication.d.ts +75 -0
  298. package/dist/graph-publication.js +203 -0
  299. package/dist/graph-publication.js.map +1 -0
  300. package/dist/ground-basis.d.ts +242 -0
  301. package/dist/ground-basis.js +1023 -0
  302. package/dist/ground-basis.js.map +1 -0
  303. package/dist/ground-checkpoint-worker.d.ts +1 -0
  304. package/dist/ground-checkpoint-worker.js +84 -0
  305. package/dist/ground-checkpoint-worker.js.map +1 -0
  306. package/dist/ground-checkpoints.d.ts +16 -0
  307. package/dist/ground-checkpoints.js +45 -0
  308. package/dist/ground-checkpoints.js.map +1 -0
  309. package/dist/ground-coordination.d.ts +107 -3
  310. package/dist/ground-coordination.js +249 -24
  311. package/dist/ground-coordination.js.map +1 -1
  312. package/dist/ground-database.d.ts +42 -0
  313. package/dist/ground-database.js +290 -0
  314. package/dist/ground-database.js.map +1 -0
  315. package/dist/ground-differences.d.ts +23 -0
  316. package/dist/ground-differences.js +47 -0
  317. package/dist/ground-differences.js.map +1 -0
  318. package/dist/ground-holders.d.ts +4 -0
  319. package/dist/ground-holders.js +15 -0
  320. package/dist/ground-holders.js.map +1 -0
  321. package/dist/ground-initialization-worker.d.ts +1 -0
  322. package/dist/ground-initialization-worker.js +20 -0
  323. package/dist/ground-initialization-worker.js.map +1 -0
  324. package/dist/ground-initialization.d.ts +35 -0
  325. package/dist/ground-initialization.js +102 -0
  326. package/dist/ground-initialization.js.map +1 -0
  327. package/dist/ground-install-saves.d.ts +63 -0
  328. package/dist/ground-install-saves.js +139 -0
  329. package/dist/ground-install-saves.js.map +1 -0
  330. package/dist/ground-inventory-schema.d.ts +18 -0
  331. package/dist/ground-inventory-schema.js +168 -0
  332. package/dist/ground-inventory-schema.js.map +1 -0
  333. package/dist/ground-profile.d.ts +7 -0
  334. package/dist/ground-profile.js +52 -0
  335. package/dist/ground-profile.js.map +1 -0
  336. package/dist/ground-root-index.d.ts +2 -1
  337. package/dist/ground-root-index.js +16 -14
  338. package/dist/ground-root-index.js.map +1 -1
  339. package/dist/ground-row.d.ts +62 -0
  340. package/dist/ground-row.js +70 -0
  341. package/dist/ground-row.js.map +1 -0
  342. package/dist/ground-save-ops.d.ts +44 -0
  343. package/dist/ground-save-ops.js +41 -0
  344. package/dist/ground-save-ops.js.map +1 -0
  345. package/dist/ground-saver-worker.d.ts +1 -0
  346. package/dist/ground-saver-worker.js +156 -0
  347. package/dist/ground-saver-worker.js.map +1 -0
  348. package/dist/ground-saver.d.ts +75 -0
  349. package/dist/ground-saver.js +216 -0
  350. package/dist/ground-saver.js.map +1 -0
  351. package/dist/ground-store.d.ts +84 -0
  352. package/dist/ground-store.js +473 -0
  353. package/dist/ground-store.js.map +1 -0
  354. package/dist/ground-territory.d.ts +130 -0
  355. package/dist/ground-territory.js +511 -0
  356. package/dist/ground-territory.js.map +1 -0
  357. package/dist/index.d.ts +1 -1
  358. package/dist/index.js.map +1 -1
  359. package/dist/installation/ownership.d.ts +0 -2
  360. package/dist/installation/ownership.js +3 -15
  361. package/dist/installation/ownership.js.map +1 -1
  362. package/dist/machine-host.d.ts +3 -2
  363. package/dist/machine-host.js +28 -16
  364. package/dist/machine-host.js.map +1 -1
  365. package/dist/materialized-graph.d.ts +7 -7
  366. package/dist/materialized-graph.js +11 -23
  367. package/dist/materialized-graph.js.map +1 -1
  368. package/dist/module-thread.d.ts +9 -0
  369. package/dist/module-thread.js +10 -0
  370. package/dist/module-thread.js.map +1 -0
  371. package/dist/native/fsevents-stream.node +0 -0
  372. package/dist/physical-path-witness.d.ts +64 -0
  373. package/dist/physical-path-witness.js +168 -0
  374. package/dist/physical-path-witness.js.map +1 -0
  375. package/dist/register-upload-fusion.d.ts +5 -4
  376. package/dist/register-upload-fusion.js +9 -6
  377. package/dist/register-upload-fusion.js.map +1 -1
  378. package/dist/registry-graph.d.ts +3 -2
  379. package/dist/registry-graph.js +9 -6
  380. package/dist/registry-graph.js.map +1 -1
  381. package/dist/registry-page-cache.d.ts +5 -2
  382. package/dist/registry-page-cache.js.map +1 -1
  383. package/dist/repository-identity-steps.d.ts +5 -0
  384. package/dist/repository-identity-steps.js +33 -0
  385. package/dist/repository-identity-steps.js.map +1 -0
  386. package/dist/runtime-node-flags.d.ts +10 -0
  387. package/dist/runtime-node-flags.js +11 -0
  388. package/dist/runtime-node-flags.js.map +1 -0
  389. package/dist/service/adapter.d.ts +1 -1
  390. package/dist/service/adapter.js +1 -1
  391. package/dist/service/adapter.js.map +1 -1
  392. package/dist/service/cli.js +16 -20
  393. package/dist/service/cli.js.map +1 -1
  394. package/dist/service/control.d.ts +2 -2
  395. package/dist/service/keepalive.d.ts +4 -11
  396. package/dist/service/keepalive.js +65 -118
  397. package/dist/service/keepalive.js.map +1 -1
  398. package/dist/service/policy.d.ts +36 -0
  399. package/dist/service/policy.js +25 -0
  400. package/dist/service/policy.js.map +1 -0
  401. package/dist/service/portable.d.ts +1 -1
  402. package/dist/service/portable.js +1 -1
  403. package/dist/service/portable.js.map +1 -1
  404. package/dist/service/recovery.d.ts +10 -9
  405. package/dist/service/recovery.js +29 -19
  406. package/dist/service/recovery.js.map +1 -1
  407. package/dist/service/windows-action.d.ts +5 -3
  408. package/dist/service/windows-action.js +6 -9
  409. package/dist/service/windows-action.js.map +1 -1
  410. package/dist/service/windows-task.js +5 -5
  411. package/dist/service/windows-task.js.map +1 -1
  412. package/dist/sharing-admission.d.ts +17 -0
  413. package/dist/sharing-admission.js +128 -0
  414. package/dist/sharing-admission.js.map +1 -0
  415. package/dist/snapshot-delivery-baseline.d.ts +12 -3
  416. package/dist/snapshot-delivery-baseline.js +41 -42
  417. package/dist/snapshot-delivery-baseline.js.map +1 -1
  418. package/dist/source-proof.d.ts +36 -0
  419. package/dist/source-proof.js +70 -0
  420. package/dist/source-proof.js.map +1 -0
  421. package/dist/source-signature.d.ts +56 -0
  422. package/dist/source-signature.js +56 -0
  423. package/dist/source-signature.js.map +1 -0
  424. package/dist/source-witness.d.ts +15 -0
  425. package/dist/source-witness.js +29 -0
  426. package/dist/source-witness.js.map +1 -0
  427. package/dist/sqlite-durability.d.ts +31 -0
  428. package/dist/sqlite-durability.js +28 -0
  429. package/dist/sqlite-durability.js.map +1 -0
  430. package/dist/sqlite.d.ts +8 -2
  431. package/dist/sqlite.js +27 -4
  432. package/dist/sqlite.js.map +1 -1
  433. package/dist/sync-address.d.ts +19 -0
  434. package/dist/sync-address.js +52 -0
  435. package/dist/sync-address.js.map +1 -0
  436. package/dist/sync-setting.d.ts +15 -0
  437. package/dist/sync-setting.js +13 -0
  438. package/dist/sync-setting.js.map +1 -1
  439. package/dist/turns.d.ts +42 -0
  440. package/dist/turns.js +112 -0
  441. package/dist/turns.js.map +1 -0
  442. package/dist/updates/bootstrap.d.ts +53 -7
  443. package/dist/updates/bootstrap.js +224 -137
  444. package/dist/updates/bootstrap.js.map +1 -1
  445. package/dist/updates/controller.d.ts +8 -0
  446. package/dist/updates/controller.js +27 -3
  447. package/dist/updates/controller.js.map +1 -1
  448. package/dist/updates/store.d.ts +3 -0
  449. package/dist/updates/store.js +12 -0
  450. package/dist/updates/store.js.map +1 -1
  451. package/dist/user-ground-host.d.ts +364 -69
  452. package/dist/user-ground-host.js +5231 -3366
  453. package/dist/user-ground-host.js.map +1 -1
  454. package/dist/watching/fsevents.d.ts +52 -1
  455. package/dist/watching/fsevents.js +60 -25
  456. package/dist/watching/fsevents.js.map +1 -1
  457. package/dist/watching/host.d.ts +129 -5
  458. package/dist/watching/host.js +433 -80
  459. package/dist/watching/host.js.map +1 -1
  460. package/dist/watching/index.d.ts +1 -1
  461. package/dist/wire-binary.d.ts +3 -2
  462. package/dist/wire-binary.js +10 -8
  463. package/dist/wire-binary.js.map +1 -1
  464. package/dist/wire-client.d.ts +3 -4
  465. package/dist/wire-client.js +18 -44
  466. package/dist/wire-client.js.map +1 -1
  467. package/dist/wire-outbox.d.ts +34 -0
  468. package/dist/wire-outbox.js +127 -0
  469. package/dist/wire-outbox.js.map +1 -0
  470. package/dist/workspace-host.d.ts +4 -0
  471. package/dist/workspace-host.js +12 -1
  472. package/dist/workspace-host.js.map +1 -1
  473. package/examinations/fixtures/private-wire-authority.js +133 -10
  474. package/npm-shrinkwrap.json +43 -56
  475. package/package.json +26 -13
@@ -1,16 +1,36 @@
1
+ import { assertApplyCleanupUnchanged, describeApplyCleanup, finishApplyCleanup } from "./apply-cleanup.js";
2
+ import { contentCacheOwnership } from "./content-cache-ownership.js";
3
+ import { ApplyOwnershipSnapshot, applyRecoveryFootprint, ownedObject, ownershipWait, sqliteApplyOwnership } from "./detection/apply-ownership.js";
4
+ import { applyControlDirectory, applyPreimageRoot, applyRecoveryRemainder, readApplyRecovery, writeApplyRecovery, } from "./apply-recovery.js";
5
+ import { planWorkingReveal, RepositoryGroundObstructed } from "./git-literal-replay.js";
6
+ import { save } from "./ground-saver.js";
7
+ import { groundStore } from "./ground-store.js";
8
+ import { GROUND_ROW_PROJECTION, portableGroundRecord as portable } from "./ground-row.js";
1
9
  /** Node filesystem, immutable-cache, Git, and Verify effects for Live Apply. */
2
10
  import { createHash } from "node:crypto";
3
11
  import { chmodSync, closeSync, copyFileSync, existsSync, fsyncSync, linkSync, lstatSync, openSync, readFileSync, readdirSync, readlinkSync, renameSync, rmSync, symlinkSync, } from "node:fs";
4
12
  import { basename, dirname, isAbsolute, join, relative, sep } from "node:path";
5
- import { applyRepositoryIdentity, buildEntityApplyEffects, createUserGroundEnrollmentPolicy, indexRepositoryTerritory, materializedSlotNames, portableFileMode, repositoryIdentityHash, } from "@amalgm/live";
6
- import { openFileDatabase } from "./sqlite.js";
13
+ import { applyRepositoryIdentity, canonicalVersion, buildEntityApplyEffects, indexRepositoryTerritory, materializedSlotNames, portableFileMode, repositoryIdentityHash, repositoryPayloadVersion, repositoryBoundaryDependencies, repositorySharesWorktreeStorage, trashRemovedEntities, } from "@amalgm/live";
14
+ import { openGroundConnection } from "./ground-database.js";
7
15
  import { ensurePrivateDir, ensureUserDir } from "./filesystem.js";
8
- import { applyRepositoryFiles, hasGitMarker, inspectRepositoryTransportFile, markRepositoryReveal, nestedRepositoryPaths, observeRepository, repositoryObservationMatches, readCachedRepositoryIdentity, sealCachedRepositoryIdentity, repositoryRevealInProgress, } from "./git-repository-host.js";
9
- import { hashContentFile, sealCapturedArtifact } from "./content-cache-host.js";
16
+ import { unsupportedGitOperation, UnsupportedGitOperationError } from "./git-operation-host.js";
17
+ import { inspectGitWorktreeStorage } from "./git-worktree-host.js";
18
+ import { reflogKeys, repositoryGroundCones } from "./git-binding.js";
19
+ import { PhysicalPathWitnesses } from "./physical-path-witness.js";
20
+ import { observeLiteralPath } from "./git-literal-host.js";
21
+ import { leafPayload, payloadFormat } from "./git-literal-read.js";
22
+ import { applyRepositoryFiles, hasGitMarker, inspectRepositoryTransportFile, judgeRepository, markRepositoryReveal, nestedRepositoryPaths, readRepository, repositoryObservationMatches, readCachedRepositoryIdentity, sealCachedRepositoryIdentity, repositoryRevealInProgress, } from "./git-repository-host.js";
23
+ import { CaptureSourceChangedError, hashContentFile } from "./content-cache-host.js";
24
+ import { physicalKey, sourceFingerprint } from "./source-signature.js";
25
+ import { leafBoundary, stampContentClock } from "./content-clock.js";
26
+ import { sealContent } from "./content-capture-pool.js";
10
27
  import { constructApplyAdapters, } from "./construct-apply-host.js";
11
28
  import { ConstructLedger } from "./construct-ledger.js";
29
+ import { settledCaptureOutcome } from "./detection/capture-outcome.js";
12
30
  import { projectMaterializedGraph } from "./materialized-graph.js";
13
31
  import { EntityApplySqliteStore } from "./entity-apply-store.js";
32
+ import { admissionProof, closePhysically, closeSource, sourceProofs, witnessProof } from "./source-proof.js";
33
+ import { awaitTurn } from "./turns.js";
14
34
  /** A structural repository record carries no cargo because its immutable
15
35
  * transport head did not change. Repair still belongs to the repository
16
36
  * adapter: the target head names the exact parcel to reopen when this machine
@@ -20,32 +40,13 @@ const repositoryArtifact = (plan) => {
20
40
  return null;
21
41
  if (plan.kind === "repo.git")
22
42
  return plan.artifact;
23
- return plan.kind === "structure" ? plan.repairArtifact : null;
43
+ return plan.kind === "structure" ? plan.resultArtifact : null;
24
44
  };
25
- /** Detect's settled view and the accepted registry share one ground row
26
- * shape; the notebook answers first wherever both are consulted. */
27
- const groundSelect = (table) => `
28
- SELECT resource_id AS resourceId, root_uuid AS rootUUID,
29
- uuid, type, parent_uuid AS parentUUID, name, status, version,
30
- payload_version AS payloadVersion, transport_version AS transportVersion,
31
- relative_path AS relativePath, absolute_path AS absolutePath,
32
- device_number AS deviceNumber, inode,
33
- byte_size AS byteSize, modified_time_ms AS modifiedTimeMs,
34
- changed_time_ms AS changedTimeMs, filesystem_mode AS filesystemMode,
35
- content_verified_at_ms AS contentVerifiedAtMs
36
- FROM ${table}`;
45
+ const groundSelect = (table) => `SELECT ${GROUND_ROW_PROJECTION} FROM ${table}`;
46
+ /** A record carrying only these facets changed what its entity holds, never
47
+ * which (parent, name) slot it occupies or its lifecycle. */
48
+ const SLOT_KEEPING_FACETS = new Set(["content", "mode"]);
37
49
  const sha256Hex = (input) => createHash("sha256").update(input).digest("hex");
38
- const portable = (row) => ({
39
- uuid: row.uuid,
40
- type: row.type,
41
- parentUUID: row.parentUUID,
42
- name: row.name,
43
- status: row.status,
44
- version: row.version,
45
- payloadVersion: row.payloadVersion,
46
- transportVersion: row.transportVersion,
47
- fileMode: portableFileMode(row.type, row.filesystemMode),
48
- });
49
50
  function fsyncFile(path) {
50
51
  // Windows FlushFileBuffers requires write access to a regular file.
51
52
  const fd = openSync(path, process.platform === "win32" ? "r+" : "r");
@@ -98,9 +99,21 @@ function fsyncAddressParents(addresses) {
98
99
  for (const directory of directories)
99
100
  fsyncDirectory(directory);
100
101
  }
102
+ /** What a container owns as a repository: its nearest-repository territory
103
+ * with the container itself counted as the owner, whichever type it holds
104
+ * now. A container becoming a repository replaces exactly this ground. */
105
+ const repositoryTerritory = (rows, container) => indexRepositoryTerritory(container.type === "repo.git" ? rows
106
+ : rows.map(row => row.uuid === container.uuid ? { ...row, type: "repo.git" } : row))
107
+ .ownedBy(container.uuid);
108
+ const identityEntry = (container, row) => ({
109
+ path: relative(container.absolutePath, row.absolutePath).split(sep).join("/"),
110
+ uuid: row.uuid, type: row.type, payloadVersion: row.type === "folder" ? null : row.payloadVersion,
111
+ });
112
+ /** One repository reading serves every identity with the same boundaries. */
113
+ const readingAddress = (path, boundaries) => [path, ...boundaries].join("\0");
101
114
  const tryLstat = (path) => {
102
115
  try {
103
- return lstatSync(path);
116
+ return lstatSync(path, { bigint: true });
104
117
  }
105
118
  catch (error) {
106
119
  const code = error.code;
@@ -109,14 +122,6 @@ const tryLstat = (path) => {
109
122
  throw error;
110
123
  }
111
124
  };
112
- const exactRecord = (left, right) => left.uuid === right.uuid
113
- && left.type === right.type
114
- && left.parentUUID === right.parentUUID
115
- && left.name === right.name
116
- && left.status === right.status
117
- && left.version === right.version
118
- && left.payloadVersion === right.payloadVersion
119
- && left.transportVersion === right.transportVersion;
120
125
  const pathInside = (root, candidate) => {
121
126
  const path = relative(root, candidate);
122
127
  return path === "" || (!isAbsolute(path) && path !== ".." && !path.startsWith(`..${sep}`));
@@ -128,62 +133,73 @@ export class EntityApplyHost {
128
133
  /** The construct records this computer holds; constructs have no ground. */
129
134
  constructs;
130
135
  #database;
131
- #userRoot;
132
136
  #cacheDir;
133
137
  #bindingDir;
134
138
  #readContent;
135
- #captureLocal;
136
139
  #acquireGround;
137
140
  #onStage;
138
141
  #now;
139
142
  #rowByUuid;
140
143
  #entityByUuid;
141
- #rowAtPath;
142
144
  #entityAtPath;
143
- #rowsUnderRoot;
144
- #notebookUnderRoot;
145
+ #pendingOutbox;
146
+ #repositoriesUnderRoot;
145
147
  #rowsByParent;
146
- #entitiesByParent;
147
- #relocateNotebookRow;
148
- #relocateEntityRow;
149
- #refreshNotebookCtime;
150
- #refreshEntityCtime;
148
+ #physicalByParent;
149
+ #slotSharer;
151
150
  #onLawRelocationStage;
152
151
  #prepared = new Map();
153
152
  #groundReleases = new Map();
154
- /** Repository moments one intent has already read, keyed by intent then by
155
- * address + nested-repository boundary. A guard reads each repository once;
156
- * the held ground keeps that reading true through reveal, and the reveal's
157
- * own proof is the reading Verify consumes. Released with the ground. */
153
+ /** Repository readings keyed by intent, then by address and nested
154
+ * boundaries. Each guard reads afresh; the reveal's readback is the reading
155
+ * Verify consumes once it closes again after Verify's content clock, since
156
+ * external editors remain independent of Amalgm's ground lock. */
158
157
  #observations = new Map();
159
158
  constructor(options) {
160
- this.#database = openFileDatabase(options.databaseFile);
161
- this.constructs = new ConstructLedger(this.#database, options.now);
162
- this.#userRoot = options.userRoot;
159
+ // Read-only: the file's saver thread makes every write this host posts.
160
+ this.#database = openGroundConnection(options.databaseFile);
163
161
  this.store = new EntityApplySqliteStore(this.#database, {
164
162
  now: options.now,
165
- onIntentFinished: (intent) => this.#releaseGround(intent),
163
+ onIntentFinished: (intent) => {
164
+ this.#releaseGround(intent);
165
+ const control = this.store.recoveryLocation(intent);
166
+ const recovery = control === null ? null : readApplyRecovery(control, intent);
167
+ if (control !== null && !recovery)
168
+ throw new Error("Terminal Apply lost its ownership receipt before wake");
169
+ options.onAttributionReleased?.(recovery ? applyRecoveryFootprint(recovery)
170
+ : { cones: [], uuids: [intent.entityId] });
171
+ },
172
+ onAttemptFailed: (intent) => {
173
+ this.#observations.delete(this.#key(intent));
174
+ this.#releaseGround(intent);
175
+ },
176
+ onOwnPublicationResolved: (target) => this.#onStage?.({
177
+ entityId: target.entityId,
178
+ throughSequence: target.globalSequence,
179
+ type: target.change.result.type,
180
+ phase: null,
181
+ stage: "own-publication-resolved",
182
+ at: this.#now(),
183
+ }),
166
184
  isLocallyEligible: (target) => this.#isLocallyEligible(target),
185
+ holdsOwnPlacement: (target, settled) => this.#holdsOwnPlacement(target, settled),
167
186
  });
187
+ this.constructs = new ConstructLedger(this.#database, options.now);
168
188
  this.#cacheDir = options.cacheDir;
169
189
  this.#bindingDir = options.bindingDir;
170
190
  this.#readContent = options.readContent;
171
- this.#captureLocal = options.captureLocal;
172
191
  this.#acquireGround = options.acquireGround;
173
192
  this.#onStage = options.onStage;
174
193
  this.#now = options.now ?? Date.now;
175
194
  this.#rowByUuid = this.#database.prepare(`${groundSelect("detection_notebook")} WHERE uuid = ? LIMIT 1`);
176
195
  this.#entityByUuid = this.#database.prepare(`${groundSelect("entities")} WHERE uuid = ? LIMIT 1`);
177
- this.#rowAtPath = this.#database.prepare(`${groundSelect("detection_notebook")} WHERE absolute_path = ? AND status = 'active' LIMIT 1`);
178
196
  this.#entityAtPath = this.#database.prepare(`${groundSelect("entities")} WHERE absolute_path = ? AND status = 'active' LIMIT 1`);
179
- this.#rowsUnderRoot = this.#database.prepare(`${groundSelect("entities")} WHERE root_uuid = ? ORDER BY relative_path`);
180
- this.#notebookUnderRoot = this.#database.prepare(`${groundSelect("detection_notebook")} WHERE root_uuid = ? ORDER BY relative_path`);
197
+ this.#pendingOutbox = this.#database.prepare("SELECT DISTINCT entity_id FROM record_outbox WHERE authority_id = ? AND status = 'pending'");
198
+ this.#repositoriesUnderRoot = this.#database.prepare(`${groundSelect("entities")} WHERE root_uuid = ? AND type = 'repo.git' AND status = 'active' ORDER BY relative_path`);
181
199
  this.#rowsByParent = this.#database.prepare(`${groundSelect("detection_notebook")} WHERE parent_uuid = ?`);
182
- this.#entitiesByParent = this.#database.prepare(`${groundSelect("entities")} WHERE parent_uuid = ?`);
183
- this.#relocateNotebookRow = this.#database.prepare("UPDATE detection_notebook SET relative_path = ?, absolute_path = ? WHERE uuid = ?");
184
- this.#relocateEntityRow = this.#database.prepare("UPDATE entities SET relative_path = ?, absolute_path = ? WHERE uuid = ?");
185
- this.#refreshNotebookCtime = this.#database.prepare("UPDATE detection_notebook SET changed_time_ms = ? WHERE uuid = ?");
186
- this.#refreshEntityCtime = this.#database.prepare("UPDATE entities SET changed_time_ms = ? WHERE uuid = ?");
200
+ this.#physicalByParent = this.#database.prepare(`SELECT o.uuid AS uuid, o.absolute_path AS absolutePath FROM detection_notebook b
201
+ JOIN entities o ON o.uuid = b.uuid WHERE b.parent_uuid = ?`);
202
+ this.#slotSharer = this.#database.prepare(`SELECT 1 FROM detection_notebook WHERE parent_uuid = ? AND name = ? AND uuid <> ? AND status = 'active' LIMIT 1`);
187
203
  this.#onLawRelocationStage = options.onLawRelocationStage;
188
204
  // Every type in Live's alphabet binds both sockets or the registry
189
205
  // refuses to build. Filesystem entities reread their native ground;
@@ -199,43 +215,47 @@ export class EntityApplyHost {
199
215
  // the only local fact that it holds one at all.
200
216
  holds: (entityId) => this.constructs.holds(entityId),
201
217
  remember: (record) => this.constructs.remember(record),
218
+ verified: (intent, record) => this.store.stageConstructVerification(intent, record),
219
+ prepareRetirement: (intent, purpose) => this.store.stageRetirement(intent, purpose, { contract: "amalgm.apply-no-ground@1" }),
202
220
  }),
203
221
  ];
204
- this.effects = buildEntityApplyEffects(adapters, options.captureLocal);
222
+ this.effects = buildEntityApplyEffects(adapters, options.captureLocal, async (retirement) => {
223
+ const cleanup = this.store.cleanupDescription(retirement);
224
+ if (cleanup.contract === "amalgm.apply-no-ground@1")
225
+ return;
226
+ await contentCacheOwnership(this.#cacheDir).using(async () => finishApplyCleanup(cleanup));
227
+ });
205
228
  }
206
229
  close() {
207
- for (const release of this.#groundReleases.values())
230
+ for (const { release } of this.#groundReleases.values())
208
231
  release();
209
232
  this.#groundReleases.clear();
210
233
  this.store.close();
211
234
  }
212
- /** Adopt Inbox history already represented by exact verified notebook
213
- * ground before Live starts new Apply lanes. An interrupted prior process
214
- * may have left private intent state; the verified baseline makes that
215
- * preparation obsolete, so remove it alongside the durable intent. */
216
- adoptVerifiedMaterialization(rootUUID = null) {
217
- const adopted = this.store.adoptVerifiedMaterialization(rootUUID);
235
+ /** The installed snapshot watermark settles history, not a new disk claim.
236
+ * Startup can retire only untouched private preparation. */
237
+ async adoptSnapshotHistory() {
238
+ const adopted = await this.store.adoptSnapshotHistory(intent => this.#canRetireUnstartedIntent(intent));
218
239
  for (const intent of adopted.supersededIntents) {
219
240
  this.#prepared.delete(this.#key(intent));
220
241
  this.#observations.delete(this.#key(intent));
221
242
  const current = this.#current(intent.entityId);
222
243
  if (current) {
223
- const context = this.#context(portable(current));
224
- rmSync(this.#controlDirectory(intent, context), { recursive: true, force: true });
244
+ rmSync(this.#controlDirectory(intent, this.#anchors(portable(current))), { recursive: true, force: true });
225
245
  }
226
246
  this.#releaseGround(intent);
227
247
  }
228
- return adopted.settledRecords;
248
+ return adopted.resolvedEntities;
229
249
  }
230
250
  #adapter(type) {
231
251
  const apply = Object.freeze({
232
- prepare: (intent, plan) => this.#prepare(intent, plan),
233
- guard: (intent, plan) => this.#guard(intent, plan),
234
- reveal: (intent, plan) => this.#reveal(intent, plan),
235
- discard: (intent, plan) => this.#discard(intent, plan),
252
+ prepare: (intent, plan) => contentCacheOwnership(this.#cacheDir).using(() => this.#prepare(intent, plan)),
253
+ guard: (intent, plan) => contentCacheOwnership(this.#cacheDir).using(() => this.#guard(intent, plan)),
254
+ reveal: (intent, plan) => contentCacheOwnership(this.#cacheDir).using(() => this.#reveal(intent, plan)),
255
+ prepareRetirement: (intent, plan, purpose) => contentCacheOwnership(this.#cacheDir).using(() => this.#prepareRetirement(intent, plan, purpose)),
236
256
  });
237
257
  const verify = Object.freeze({
238
- verify: (intent, plan) => this.#verify(intent, plan),
258
+ verify: (intent, plan) => contentCacheOwnership(this.#cacheDir).using(() => this.#verify(intent, plan)),
239
259
  });
240
260
  return Object.freeze({
241
261
  name: `${type}-apply`,
@@ -243,24 +263,57 @@ export class EntityApplyHost {
243
263
  rails: Object.freeze({ apply, verify }),
244
264
  });
245
265
  }
246
- /** The portable Inbox contains cloud truth for every entity. Shell alone
247
- * decides whether that accepted entity belongs on this machine. External
248
- * selected roots are fully materialized; core-ground descendants obey the
249
- * portable `.amalgmignore` policy before any Apply intent or disk write. */
266
+ observedDependencies(entityIds, observations) {
267
+ return entityIds.filter((uuid) => {
268
+ const rows = [this.#entityByUuid.get(uuid), this.#rowByUuid.get(uuid)];
269
+ return rows.some(row => row !== undefined && observations.some(({ directory, suspicion }) => (suspicion.paths === null ? [directory] : suspicion.paths.map(path => join(directory, path)))
270
+ .some(path => pathInside(path, row.absolutePath) || pathInside(row.absolutePath, path))));
271
+ });
272
+ }
273
+ /** Where an examination of these entities must read: each one's O and B
274
+ * placement, so an entity that vanished starts from its original address. */
275
+ examinationAddresses(entityIds) {
276
+ return [...new Set(entityIds.flatMap(uuid => [this.#entityByUuid.get(uuid), this.#rowByUuid.get(uuid)]
277
+ .flatMap(row => row ? [row.absolutePath] : [])))];
278
+ }
279
+ /** Selected materialization roots decide where accepted results belong:
280
+ * a result is local while its own row or its parent's is materialized here.
281
+ * Existing local work whose new parent is still arriving is local and
282
+ * reaches the coordinator's dependency guard. Ignore rules govern local
283
+ * Detect only; they cannot hide cloud truth from Apply or turn an existing
284
+ * selected descendant into a cloud-only entity. Placement is not asked. */
250
285
  #isLocallyEligible(target) {
251
- const context = this.#context(target);
252
- const boundary = context.current ?? context.parent;
253
- if (!boundary)
286
+ return this.#current(target.uuid) !== null
287
+ || (target.parentUUID !== null && this.#current(target.parentUUID) !== null);
288
+ }
289
+ /** Own ground needs no replay only while it sits at its derived address and
290
+ * the slot it left or entered moves no sibling. A resolution whose settled
291
+ * records change only content or mode left and entered no slot, so it
292
+ * proves only its own address; a sibling's placement belongs to the records
293
+ * that change it. Any other resolution, or an own address that is not a
294
+ * lone occupant's, derives the parent's complete slot projection. An
295
+ * unplaced parent is not held placement; the ordinary Apply path owns that
296
+ * wait. */
297
+ #holdsOwnPlacement(target, settled) {
298
+ const parent = this.#current(target.parentUUID ?? target.uuid);
299
+ if (!parent)
254
300
  return false;
255
- const root = this.#current(boundary.rootUUID);
256
- if (!root || root.absolutePath !== this.#userRoot)
257
- return true;
258
- const relativePath = relative(this.#userRoot, context.targetPath).split("\\").join("/");
259
- if (!relativePath)
301
+ if (target.parentUUID !== null && target.status === "active"
302
+ && settled.every(record => record.change.facets.every(facet => SLOT_KEEPING_FACETS.has(facet)))
303
+ && this.#holdsLoneSlot(target, parent))
260
304
  return true;
261
- const ignore = join(this.#userRoot, ".amalgmignore");
262
- const policy = createUserGroundEnrollmentPolicy(existsSync(ignore) ? readFileSync(ignore, "utf8") : "");
263
- return policy(relativePath);
305
+ const context = this.#context(target);
306
+ return context.beforeRelocations.length === 0 && context.afterRelocations.length === 0
307
+ && (target.status !== "active" || context.current?.absolutePath === context.targetPath);
308
+ }
309
+ /** The target's own ground sits at its portable name under its placed
310
+ * parent, and no other active identity shares that portable slot, so its
311
+ * derived address is that name. */
312
+ #holdsLoneSlot(target, parent) {
313
+ const current = this.#current(target.uuid);
314
+ return current !== null && current.parentUUID === target.parentUUID && current.name === target.name
315
+ && current.absolutePath === join(parent.absolutePath, target.name)
316
+ && this.#slotSharer.get(target.parentUUID, target.name, target.uuid) === undefined;
264
317
  }
265
318
  async #prepare(intent, plan) {
266
319
  await this.#ensurePrepared(intent, plan);
@@ -274,15 +327,24 @@ export class EntityApplyHost {
274
327
  let prepared = {};
275
328
  const repository = repositoryArtifact(plan);
276
329
  if (repository) {
277
- prepared = { repository: await this.#repositoryChain(repository) };
330
+ const verified = await this.#repositoryChain(repository);
331
+ const latest = await inspectRepositoryTransportFile(verified.chain.at(-1).file);
332
+ if (repositoryPayloadVersion(latest, sha256Hex) !== plan.target.payloadVersion
333
+ || (plan.kind === "repo.git" && (latest.stateId !== plan.stateId
334
+ || latest.cardId !== plan.cardId || latest.checkpointId !== plan.checkpointId
335
+ || latest.identityHash !== plan.identityHash
336
+ || latest.parentTransportVersion !== plan.parentTransportVersion))) {
337
+ throw new Error("repository package does not describe the accepted complete result");
338
+ }
339
+ prepared = { repository: verified };
278
340
  }
279
341
  else if (plan.kind === "file.text" || plan.kind === "file.binary" || plan.kind === "link") {
280
342
  prepared = { artifact: await this.#prepareArtifact(plan) };
281
343
  }
282
- else if (plan.kind === "structure" && plan.repairArtifact) {
344
+ else if (plan.kind === "structure" && plan.resultArtifact) {
283
345
  prepared = {
284
- artifact: await this.#sealedArtifact(plan.repairArtifact.contentHash)
285
- ?? await this.#readContent(plan.repairArtifact),
346
+ artifact: await this.#sealedArtifact(plan.resultArtifact.contentHash)
347
+ ?? await this.#readContent(plan.resultArtifact),
286
348
  };
287
349
  }
288
350
  this.#prepared.set(key, prepared);
@@ -299,7 +361,7 @@ export class EntityApplyHost {
299
361
  if (sealed)
300
362
  return sealed;
301
363
  if (plan.kind === "link") {
302
- sealCapturedArtifact(this.#cacheDir, plan.artifact, Buffer.from(plan.targetText, "utf8"));
364
+ await sealContent(this.#cacheDir, plan.artifact, Buffer.from(plan.targetText, "utf8"), "apply");
303
365
  const literal = await this.#sealedArtifact(plan.artifact.contentHash);
304
366
  if (literal)
305
367
  return literal;
@@ -319,7 +381,7 @@ export class EntityApplyHost {
319
381
  return null;
320
382
  if (await hashContentFile(file) !== contentHash)
321
383
  return null;
322
- return { file, bytes: stat.size, contentHash };
384
+ return { file, bytes: Number(stat.size), contentHash };
323
385
  }
324
386
  async #spliceFromBase(artifact, strategy) {
325
387
  let base;
@@ -341,10 +403,27 @@ export class EntityApplyHost {
341
403
  ]);
342
404
  if (sha256Hex(result) !== artifact.contentHash)
343
405
  return null;
344
- sealCapturedArtifact(this.#cacheDir, artifact, result);
406
+ await sealContent(this.#cacheDir, artifact, result, "apply");
345
407
  return this.#sealedArtifact(artifact.contentHash);
346
408
  }
409
+ /** Only a final safe/equal guard retains ground through reveal and Verify. */
347
410
  async #guard(intent, plan) {
411
+ let outcome;
412
+ try {
413
+ // Resolved promises do not service sockets or Watch callbacks. Let the
414
+ // host run before collecting the proof that authorizes this decision.
415
+ await awaitTurn();
416
+ outcome = await this.#inspectGround(intent, plan);
417
+ return outcome;
418
+ }
419
+ finally {
420
+ if (intent.phase === "declared"
421
+ || (outcome?.kind !== "safe" && outcome?.kind !== "already-target")) {
422
+ this.#releaseGround(intent);
423
+ }
424
+ }
425
+ }
426
+ async #inspectGround(intent, plan) {
348
427
  // The declared guard is an optimistic preflight. Any concurrent
349
428
  // local write is caught by the second guard after private preparation, so
350
429
  // this first pass must never queue cargo behind Detect, Register, or Add.
@@ -356,6 +435,13 @@ export class EntityApplyHost {
356
435
  const finalGuard = intent.phase !== "declared";
357
436
  this.#observations.delete(this.#key(intent));
358
437
  try {
438
+ const recoveryReplan = this.#recoveryReplan(intent, plan);
439
+ if (recoveryReplan)
440
+ return recoveryReplan;
441
+ if (plan.target.parentUUID !== null && !this.#current(plan.target.parentUUID)) {
442
+ return { kind: "dependency", reason: "The accepted parent has not materialized yet.",
443
+ waitFor: { producer: "apply", entityIds: [plan.target.parentUUID] } };
444
+ }
359
445
  if (finalGuard) {
360
446
  this.#emit(intent, plan, "waiting-for-ground");
361
447
  await this.#holdGround(intent, plan);
@@ -364,22 +450,66 @@ export class EntityApplyHost {
364
450
  else {
365
451
  this.#emit(intent, plan, "preflight-started");
366
452
  }
367
- const context = this.#context(plan.target);
453
+ const unavailableRoot = this.#unavailableRoot(plan.target);
454
+ if (unavailableRoot) {
455
+ return unavailableRoot;
456
+ }
457
+ const context = this.#context(plan.target, intent);
458
+ const ownership = this.#foreignOwnership(intent, this.#bindingRoot(plan.target), this.applyAddresses(intent, plan));
459
+ if (ownership.waiting)
460
+ return ownership.waiting;
368
461
  const current = context.current;
462
+ // A receipt must preserve the source association the guard proved. An
463
+ // equal-content atomic save can still replace the object being moved.
464
+ for (const row of [current, ...context.beforeRelocations.map(move => this.#current(move.row.uuid)),
465
+ ...context.afterRelocations.map(move => this.#current(move.row.uuid))]) {
466
+ const stat = row && tryLstat(row.absolutePath);
467
+ if (stat && physicalKey(stat) !== row.physicalKey) {
468
+ return { kind: "local-change", reason: "Detect must refresh the source's physical association." };
469
+ }
470
+ }
471
+ if (![context.parent, current?.parentUUID ? this.#current(current.parentUUID) : null]
472
+ .every(row => row === null || row === undefined || this.#directoryAncestorsMatch(row))) {
473
+ return { kind: "unsettled-ground", reason: "Detect must reconcile a replaced parent directory." };
474
+ }
369
475
  let repositoryTargetMatches = false;
370
476
  let repositoryRelocationsPending = false;
371
- if (plan.target.type === "repo.git") {
477
+ let repositoryIdentityMatches = true;
478
+ if (plan.target.type === "repo.git" || current?.type === "repo.git") {
479
+ for (const path of new Set([context.targetPath, current?.absolutePath].filter((path) => path !== undefined))) {
480
+ if (!hasGitMarker(path))
481
+ continue;
482
+ const storage = await inspectGitWorktreeStorage(path);
483
+ const unsupported = unsupportedGitOperation(storage.privateDirectory);
484
+ if (unsupported) {
485
+ return { kind: "dependency", reason: new UnsupportedGitOperationError(unsupported).message,
486
+ waitFor: { producer: "capability", entityIds: [intent.entityId] } };
487
+ }
488
+ if (repositorySharesWorktreeStorage(storage)) {
489
+ return { kind: "dependency",
490
+ reason: "This checkout shares Git storage with another worktree. Apply requires an independent checkout.",
491
+ waitFor: { producer: "capability", entityIds: [intent.entityId] },
492
+ };
493
+ }
494
+ }
495
+ }
496
+ if (plan.target.type === "repo.git" && plan.target.status === "active") {
372
497
  const repository = (await this.#ensurePrepared(intent, plan)).repository;
373
498
  if (!repository)
374
499
  throw new Error("repository Apply has no prepared transport identity");
500
+ const dependencies = this.#repositoryBoundaries(context, repository.identity);
501
+ if (dependencies.length > 0) {
502
+ return { kind: "dependency",
503
+ reason: "The enclosing result removes an independently owned repository. Its own accepted move or type change must settle first.",
504
+ waitFor: { producer: "apply", entityIds: dependencies.map(({ uuid }) => uuid) },
505
+ };
506
+ }
375
507
  // A repository reveal rewrites its target in place. Once one has
376
508
  // started, the repository is private Apply ground in whatever state
377
509
  // an interruption left it: nothing can match it, Detect refuses to
378
- // read it, and the only way forward is a reveal that resumes there.
510
+ // read it, and the only way forward is the reveal that resumes there.
379
511
  if (repositoryRevealInProgress(context.targetPath)
380
512
  || (current && repositoryRevealInProgress(current.absolutePath))) {
381
- if (!finalGuard)
382
- this.#releaseGround(intent);
383
513
  return { kind: "safe" };
384
514
  }
385
515
  const groups = this.#repositoryRelocations(context, repository.identity);
@@ -405,47 +535,80 @@ export class EntityApplyHost {
405
535
  }
406
536
  repositoryRelocationsPending ||= sourcePresent;
407
537
  }
538
+ repositoryIdentityMatches = this.#repositoryIdentityMatches(intent, context.targetPath, repository.identity);
408
539
  repositoryTargetMatches = current !== null
409
540
  && await this.#matches(intent, plan.target, context.targetPath, plan);
410
- if (repositoryTargetMatches && repositoryRelocationsPending)
411
- return { kind: "safe" };
412
541
  }
413
- if (current
414
- && exactRecord(portable(current), plan.target)
542
+ const recoveryReplanAfterPreparation = this.#recoveryReplan(intent, plan);
543
+ if (recoveryReplanAfterPreparation)
544
+ return recoveryReplanAfterPreparation;
545
+ // Identity comes from O, never a historical sharing address or byte
546
+ // equality. A same-identity complete target can be adopted even when B
547
+ // lags, provided no other physical effect remains. Fresh Verify still
548
+ // owns acknowledgment and recovery retirement.
549
+ const physical = this.#entityByUuid.get(intent.entityId);
550
+ const targetStats = tryLstat(context.targetPath);
551
+ const targetIdentity = physical?.status === "active"
552
+ && physical.absolutePath === context.targetPath
553
+ && targetStats !== null && physicalKey(targetStats) === physical.physicalKey;
554
+ if (targetIdentity
555
+ && repositoryIdentityMatches
556
+ && !repositoryRelocationsPending
415
557
  && context.beforeRelocations.length === 0
416
558
  && context.afterRelocations.length === 0
417
559
  && (repositoryTargetMatches
418
- || await this.#matches(intent, plan.target, context.targetPath, plan))) {
560
+ || await this.#matches(intent, plan.target, context.targetPath, plan))
561
+ && this.#physicalDirectoriesMatch(physical)) {
419
562
  return { kind: "already-target" };
420
563
  }
421
- // A rail transition can change an entity's registered payload witness
422
- // without changing its bytes: clean repo leaves use Git object ids,
423
- // while the ordinary file rail uses SHA-256. At the same durable UUID
424
- // and address, exact target bytes prove that no write is required; Verify
425
- // can advance the row directly. Equal bytes at a different address are
426
- // deliberately not identity evidence.
427
- if (current?.absolutePath === context.targetPath
428
- && context.beforeRelocations.length === 0
429
- && context.afterRelocations.length === 0
430
- && await this.#matches(intent, plan.target, context.targetPath, plan)) {
431
- return { kind: "already-target" };
564
+ const localObservation = settledCaptureOutcome(this.#database, {
565
+ authorityId: intent.authorityId, entityId: intent.entityId,
566
+ addresses: this.applyAddresses(intent, plan),
567
+ });
568
+ if (localObservation.kind === "local-only") {
569
+ return { kind: "local-change", reason: localObservation.reason };
570
+ }
571
+ if (localObservation.kind === "retry")
572
+ return localObservation;
573
+ // Trash, or a leaf replacing a container, displaces the whole directory.
574
+ // A rename carries its subtree, and collisions relocate intact, so this
575
+ // is the only container an effect displaces. Local remainder below it
576
+ // has no copy anywhere else: the effect waits for Detect to see that
577
+ // ground leave or become shared.
578
+ if (current && this.#isContainer(current) && tryLstat(current.absolutePath)
579
+ && (plan.target.status !== "active" || !["workspace", "folder", "repo.git"].includes(plan.target.type))) {
580
+ const named = this.#displacedRemainder(current, localObservation.remainder);
581
+ if (named.length > 0) {
582
+ return { kind: "dependency", waitFor: { producer: "detect", entityIds: named },
583
+ blocker: { code: "local-ground-would-be-displaced",
584
+ effect: plan.target.status === "active" ? "replace-container" : "trash" },
585
+ reason: `${current.absolutePath} holds local ground sharing never held; removing it would destroy that ground.` };
586
+ }
587
+ const publishing = this.#pendingPublication(intent.authorityId, current);
588
+ if (publishing.length > 0) {
589
+ return { kind: "dependency", waitFor: { producer: "send", entityIds: publishing },
590
+ reason: `${current.absolutePath} holds local results not yet sequenced; removing it would hide them.` };
591
+ }
592
+ }
593
+ const relocations = [...context.beforeRelocations, ...context.afterRelocations];
594
+ const vacated = current && (plan.target.status !== "active" || current.absolutePath !== context.targetPath)
595
+ ? [current.absolutePath] : [];
596
+ if (relocations.length > 0 && !await this.#lawRelocationsReady(intent, plan, relocations, vacated)) {
597
+ return { kind: "local-change", reason: "A required sibling relocation has unproved ground." };
598
+ }
599
+ if (plan.target.status !== "active" && !current) {
600
+ if (!await this.#resolvedAbsence(intent, plan, context))
601
+ return { kind: "local-change" };
602
+ return relocations.length === 0 ? { kind: "already-target" } : { kind: "safe" };
432
603
  }
433
604
  if (!current) {
434
605
  if (!tryLstat(context.targetPath)) {
435
- if (intent.phase === "declared")
436
- this.#releaseGround(intent);
437
606
  return { kind: "safe" };
438
607
  }
439
- if (await this.#matches(intent, plan.target, context.targetPath, plan)) {
440
- return { kind: "already-target" };
441
- }
442
608
  if (context.collisionEligible
443
609
  && await this.#lawRelocationsReady(intent, plan, context.beforeRelocations)) {
444
- if (intent.phase === "declared")
445
- this.#releaseGround(intent);
446
610
  return { kind: "safe" };
447
611
  }
448
- this.#releaseGround(intent);
449
612
  return this.#heldDestination(intent, context.targetPath);
450
613
  }
451
614
  // A move/rename may never erase a different thing already occupying the
@@ -453,49 +616,50 @@ export class EntityApplyHost {
453
616
  if (current.absolutePath !== context.targetPath && tryLstat(context.targetPath)) {
454
617
  if (context.collisionEligible
455
618
  && await this.#lawRelocationsReady(intent, plan, context.beforeRelocations)) {
456
- if (intent.phase === "declared")
457
- this.#releaseGround(intent);
458
619
  return { kind: "safe" };
459
620
  }
460
- this.#releaseGround(intent);
461
621
  return this.#heldDestination(intent, context.targetPath);
462
622
  }
623
+ // An unfinished reveal displaced this ground itself: its target was
624
+ // never visible, so resuming cannot overwrite anything local. Once the
625
+ // reveal finished, absence is visible-ground evidence like any other.
626
+ const displaced = this.#displacedPath(intent, context);
627
+ const recovery = this.#boundRecovery(intent);
628
+ if (intent.phase === "prepared"
629
+ && !tryLstat(current.absolutePath)
630
+ && tryLstat(displaced)
631
+ && recovery?.source
632
+ && await this.#matchesPreimage(intent, plan, recovery.source, displaced)) {
633
+ return { kind: "safe" };
634
+ }
463
635
  // Absence at one address can be a move. While the entity is still
464
636
  // materialized, Detect must reconcile its root before repair may destroy
465
637
  // that physical continuity. Release our cone before asking Detect to
466
- // acquire it. A proved absence removes only the materialized row; the
467
- // retained notebook then permits reconstruction on the next guard.
638
+ // acquire it. A proved removal records Trash before this older result may
639
+ // proceed; Apply's own displaced ground remains repair evidence.
468
640
  if (plan.target.status === "active"
469
641
  && !tryLstat(current.absolutePath)
470
642
  && !tryLstat(context.targetPath)) {
471
643
  if (this.#entityByUuid.get(current.uuid)) {
472
- this.#releaseGround(intent);
473
- await this.#captureLocal(intent, plan, intent.phase === "revealed" ? "after-reveal" : "before-reveal");
474
- return { kind: "retry", reason: "missing ground was reconciled before repair" };
644
+ return { kind: "unsettled-ground", reason: "Detect must distinguish a move from local absence." };
475
645
  }
476
- if (intent.phase === "declared")
477
- this.#releaseGround(intent);
478
- return { kind: "safe" };
479
- }
480
- if (await this.#matches(intent, portable(current), current.absolutePath, plan)) {
481
- if (intent.phase === "declared")
482
- this.#releaseGround(intent);
483
646
  return { kind: "safe" };
484
647
  }
485
- const displaced = this.#displacedPath(intent, context);
486
- if (!tryLstat(current.absolutePath)
487
- && tryLstat(displaced)
488
- && await this.#matches(intent, portable(current), displaced, plan)) {
489
- if (intent.phase === "declared")
490
- this.#releaseGround(intent);
648
+ // A repository's O also holds unshared remainder; its ground is safe
649
+ // when it still holds the sharing basis plus only accounted remainder.
650
+ const shared = current.type === "repo.git" ? this.#rowByUuid.get(current.uuid) : undefined;
651
+ if (await this.#matches(intent, portable(shared?.status === "active" ? shared : current), current.absolutePath, plan)
652
+ && this.#physicalDirectoriesMatch(current)) {
491
653
  return { kind: "safe" };
492
654
  }
493
- this.#releaseGround(intent);
494
655
  return { kind: "local-change" };
495
656
  }
496
657
  catch (error) {
497
- this.#releaseGround(intent);
498
- return { kind: "retry", reason: error instanceof Error ? error.message : String(error) };
658
+ if (error instanceof UnsupportedGitOperationError) {
659
+ return { kind: "dependency", reason: error.message,
660
+ waitFor: { producer: "capability", entityIds: [intent.entityId] } };
661
+ }
662
+ throw error;
499
663
  }
500
664
  finally {
501
665
  if (!finalGuard) {
@@ -513,8 +677,7 @@ export class EntityApplyHost {
513
677
  * Record can say so.
514
678
  */
515
679
  #heldDestination(intent, targetPath) {
516
- const occupant = this.#rowAtPath.get(targetPath)
517
- ?? this.#entityAtPath.get(targetPath);
680
+ const occupant = this.#entityAtPath.get(targetPath);
518
681
  if (!occupant || occupant.uuid === intent.entityId)
519
682
  return { kind: "local-change" };
520
683
  return {
@@ -525,12 +688,34 @@ export class EntityApplyHost {
525
688
  async #reveal(intent, plan) {
526
689
  this.#emit(intent, plan, "reveal-started");
527
690
  try {
528
- const context = this.#context(plan.target);
691
+ const context = this.#context(plan.target, intent);
529
692
  const prepared = await this.#ensurePrepared(intent, plan);
530
- this.#relocateLawSiblings(context, context.beforeRelocations);
693
+ // Plan repository removals before anything durable: an obstruction
694
+ // leaves neither a receipt nor an effect behind.
695
+ const kept = plan.target.type === "repo.git" && plan.kind !== "lifecycle" ? await this.#revealRemainder(intent, context) : [];
696
+ let removals = { leaves: [], directories: [] };
697
+ if (plan.target.type === "repo.git" && plan.kind !== "lifecycle" && context.current && tryLstat(context.current.absolutePath)) {
698
+ if (!prepared.repository)
699
+ throw new Error("repository Apply has no prepared transport chain");
700
+ try {
701
+ removals = planWorkingReveal(context.current.absolutePath, prepared.repository.identity, await this.#sharedIdentity(plan.target.uuid), new Set(kept.map(entry => entry.path)));
702
+ }
703
+ catch (error) {
704
+ if (!(error instanceof RepositoryGroundObstructed))
705
+ throw error;
706
+ this.#releaseGround(intent);
707
+ return this.#obstructedRepository(context.current, error);
708
+ }
709
+ }
710
+ const waiting = await this.#retainPreimages(intent, plan, context, kept);
711
+ if (waiting) {
712
+ this.#releaseGround(intent);
713
+ return waiting;
714
+ }
715
+ await this.#relocateLawSiblings(context, context.beforeRelocations);
531
716
  if (plan.kind === "lifecycle") {
532
717
  this.#removeWithRecovery(intent, context);
533
- this.#relocateAfterTarget(plan.target, context);
718
+ await this.#relocateAfterTarget(plan.target, context);
534
719
  return;
535
720
  }
536
721
  if (plan.target.type === "repo.git") {
@@ -544,47 +729,47 @@ export class EntityApplyHost {
544
729
  markRepositoryReveal(context.current.absolutePath);
545
730
  }
546
731
  if (context.current && context.current.absolutePath !== context.targetPath
547
- && !context.currentShared
548
732
  && tryLstat(context.current.absolutePath)) {
549
- this.#moveWithRecovery(intent, context.current.absolutePath, context.targetPath, context);
733
+ this.#moveToVacantAddress(context.current.absolutePath, context.targetPath);
550
734
  }
551
735
  ensureUserDir(context.targetPath);
552
- const applied = await applyRepositoryFiles(context.targetPath, repository.chain);
553
- // The reveal proof is the reading Verify consumes: the same held
554
- // ground, the same moment, no second capture.
555
- this.#observe(intent, context.targetPath, applied.identity, { stateId: applied.stateId, card: applied.card });
736
+ const applied = await applyRepositoryFiles(context.targetPath, repository.chain, { kind: "working", removals, remainder: new Set(kept.map(entry => entry.path)) });
737
+ // The reveal's readback is the reading Verify inherits.
738
+ this.#observations.set(this.#key(intent), new Map([[
739
+ readingAddress(context.targetPath, applied.reading.boundaries), applied.reading
740
+ ]]));
556
741
  this.#moveRepositoryRelocations(intent, context, repository.identity);
557
- this.#relocateAfterTarget(plan.target, context);
742
+ await this.#relocateAfterTarget(plan.target, context);
558
743
  return;
559
744
  }
560
745
  if (plan.kind === "structure") {
561
746
  if (context.current && context.current.absolutePath !== context.targetPath
562
- && !context.currentShared) {
563
- this.#moveWithRecovery(intent, context.current.absolutePath, context.targetPath, context);
747
+ && tryLstat(context.current.absolutePath)) {
748
+ this.#moveToVacantAddress(context.current.absolutePath, context.targetPath);
564
749
  }
565
- else if (!tryLstat(context.targetPath)) {
566
- if (plan.target.type === "workspace" || plan.target.type === "folder") {
567
- ensureUserDir(context.targetPath);
750
+ if (plan.target.type === "workspace" || plan.target.type === "folder") {
751
+ ensureUserDir(context.targetPath);
752
+ if (hasGitMarker(context.targetPath)) {
753
+ rmSync(join(context.targetPath, ".git"), { recursive: true, force: true });
568
754
  }
569
- else if (prepared.artifact) {
570
- this.#installArtifact(intent, context, plan.target, prepared.artifact);
755
+ }
756
+ else if (!await this.#matches(intent, plan.target, context.targetPath, plan)) {
757
+ // No new sender cargo does not mean this receiver has the bytes.
758
+ // A later rename or mode result can legitimately select a payload
759
+ // different from the receiver's preceding accepted result.
760
+ const artifact = prepared.artifact;
761
+ if (!artifact)
762
+ throw new Error("structural leaf result has no immutable artifact");
763
+ if (plan.target.type === "link") {
764
+ const targetText = new TextDecoder("utf-8", { fatal: true })
765
+ .decode(readFileSync(artifact.file));
766
+ this.#installLink(intent, context, targetText);
571
767
  }
572
768
  else {
573
- throw new Error("structural leaf repair has no immutable artifact");
769
+ this.#installArtifact(intent, context, plan.target, artifact);
574
770
  }
575
771
  }
576
- if ((plan.target.type === "workspace" || plan.target.type === "folder")
577
- && hasGitMarker(context.targetPath)) {
578
- rmSync(join(context.targetPath, ".git"), { recursive: true, force: true });
579
- }
580
- if (plan.target.type === "file.text" || plan.target.type === "file.binary") {
581
- if (plan.target.fileMode === null) {
582
- throw new Error(`ordinary file ${plan.target.uuid} has no portable mode`);
583
- }
584
- chmodSync(context.targetPath, plan.target.fileMode);
585
- fsyncFile(context.targetPath);
586
- }
587
- this.#relocateAfterTarget(plan.target, context);
772
+ await this.#relocateAfterTarget(plan.target, context);
588
773
  return;
589
774
  }
590
775
  // Identity owns the address transition. A content or link result may
@@ -593,25 +778,24 @@ export class EntityApplyHost {
593
778
  // installing at the new address would leave the old address behind for
594
779
  // Detect to misidentify as a new local entity and echo back to cloud.
595
780
  if (context.current && context.current.absolutePath !== context.targetPath
596
- && !context.currentShared
597
781
  && tryLstat(context.current.absolutePath)) {
598
- this.#moveWithRecovery(intent, context.current.absolutePath, context.targetPath, context);
782
+ this.#moveToVacantAddress(context.current.absolutePath, context.targetPath);
599
783
  }
600
784
  if (plan.kind === "reference") {
601
785
  this.#installLink(intent, context, relative(dirname(context.targetPath), join(this.#bindingDir, plan.targetEntityId)));
602
- this.#relocateAfterTarget(plan.target, context);
786
+ await this.#relocateAfterTarget(plan.target, context);
603
787
  return;
604
788
  }
605
789
  if (plan.kind === "link") {
606
790
  this.#installLink(intent, context, plan.targetText);
607
- this.#relocateAfterTarget(plan.target, context);
791
+ await this.#relocateAfterTarget(plan.target, context);
608
792
  return;
609
793
  }
610
794
  const artifact = prepared.artifact;
611
795
  if (!artifact)
612
796
  throw new Error(`${plan.kind} Apply has no prepared immutable artifact`);
613
797
  this.#installArtifact(intent, context, plan.target, artifact);
614
- this.#relocateAfterTarget(plan.target, context);
798
+ await this.#relocateAfterTarget(plan.target, context);
615
799
  }
616
800
  catch (error) {
617
801
  this.#releaseGround(intent);
@@ -624,27 +808,33 @@ export class EntityApplyHost {
624
808
  async #verify(intent, plan) {
625
809
  this.#emit(intent, plan, "verify-started");
626
810
  try {
811
+ const recoveryReplan = this.#recoveryReplan(intent, plan);
812
+ if (recoveryReplan) {
813
+ this.#releaseGround(intent);
814
+ return recoveryReplan;
815
+ }
816
+ // Yield before the fresh physical proof, never between a final guard
817
+ // and reveal. Ground ownership and the durable intent remain in force.
818
+ await awaitTurn();
819
+ await this.#holdGround(intent, plan);
627
820
  // Entity repair is valid only inside the same materialized root that
628
821
  // owns the accepted rows. Check that boundary before path planning:
629
822
  // planning may inspect siblings, and must never recreate or traverse a
630
823
  // missing/replaced workspace piecemeal.
631
- const current = this.#current(plan.target.uuid);
632
- const parent = plan.target.parentUUID ? this.#current(plan.target.parentUUID) : null;
633
- const anchor = current ?? parent;
634
- if (anchor) {
635
- const root = this.#root(anchor);
636
- const rootGround = tryLstat(root.absolutePath);
637
- if (!rootGround
638
- || rootGround.dev !== root.deviceNumber
639
- || rootGround.ino !== root.inode) {
640
- this.#releaseGround(intent);
641
- return {
642
- kind: "retry",
643
- reason: `materialized root ${root.uuid} is missing or replaced; explicit root repair is required`,
644
- };
645
- }
824
+ const unavailableRoot = this.#unavailableRoot(plan.target);
825
+ if (unavailableRoot) {
826
+ this.#releaseGround(intent);
827
+ return unavailableRoot;
828
+ }
829
+ const prepared = plan.target.type === "repo.git" && plan.target.status === "active"
830
+ ? (await this.#ensurePrepared(intent, plan)).repository : null;
831
+ const basis = this.#verificationBasis(plan.target, prepared?.identity ?? []);
832
+ const context = this.#context(plan.target, intent);
833
+ const ownership = this.#foreignOwnership(intent, this.#bindingRoot(plan.target), this.applyAddresses(intent, plan));
834
+ if (ownership.waiting) {
835
+ this.#releaseGround(intent);
836
+ return ownership.waiting;
646
837
  }
647
- const context = this.#context(plan.target);
648
838
  if (await this.#restoreRacedLocal(intent, plan, context)) {
649
839
  this.#releaseGround(intent);
650
840
  return { kind: "local-change" };
@@ -653,7 +843,17 @@ export class EntityApplyHost {
653
843
  this.#releaseGround(intent);
654
844
  return { kind: "missing-target" };
655
845
  }
656
- if (!await this.#matches(intent, plan.target, context.targetPath, plan)) {
846
+ const anchor = context.current ?? context.parent;
847
+ if (!anchor)
848
+ throw new Error(`entity ${intent.entityId} has no materialized root`);
849
+ // Every read Verify proves follows its root's content clock.
850
+ const { boundary: clock } = await stampContentClock(this.#database.name, this.#root(anchor).absolutePath);
851
+ await this.#closeInherited(intent);
852
+ let receipt;
853
+ const targetMatches = plan.target.status === "active"
854
+ ? await this.#matches(intent, plan.target, context.targetPath, plan, context.targetPath, proof => { receipt = proof; })
855
+ : await this.#resolvedAbsence(intent, plan, context, proof => { receipt = proof; });
856
+ if (!targetMatches || context.beforeRelocations.length > 0 || context.afterRelocations.length > 0) {
657
857
  this.#releaseGround(intent);
658
858
  return { kind: "local-change" };
659
859
  }
@@ -666,12 +866,18 @@ export class EntityApplyHost {
666
866
  fsyncFile(context.targetPath);
667
867
  }
668
868
  fsyncAddressParents(this.applyAddresses(intent, plan));
869
+ const verification = receipt.proof;
870
+ const proof = { semantic: () => verification.semantic(), witnesses: [],
871
+ admission: () => { ownership.basis.assertCurrent(); closePhysically(verification); } };
669
872
  if (plan.target.type === "repo.git" && plan.target.status === "active") {
670
- const repository = await this.#repositoryRows(intent, plan, context);
671
- this.store.stageVerification(intent, repository.rows, repository.removals);
873
+ const repository = await this.#repositoryRows(intent, plan, context, receipt, clock);
874
+ this.store.stageVerification(intent, repository, basis, proof);
672
875
  }
673
876
  else {
674
- this.store.stageVerification(intent, [this.#verifiedRow(plan.target, context)]);
877
+ const row = this.#verifiedRow(plan.target, context, receipt, clock);
878
+ const ground = this.#containerGround(plan.target, context, row);
879
+ closePhysically(proof);
880
+ this.store.stageVerification(intent, ground, basis, proof);
675
881
  }
676
882
  return { kind: "match" };
677
883
  }
@@ -683,26 +889,493 @@ export class EntityApplyHost {
683
889
  this.#emit(intent, plan, "verify-finished");
684
890
  }
685
891
  }
686
- async #discard(intent, plan) {
892
+ /** Original O/B evidence is captured before any verification output reads
893
+ * it. Only this target's subtrees, manifest UUIDs and parent namespaces enter. */
894
+ #verificationBasis(target, identity) {
895
+ return groundStore(this.#database).readSnapshot(reader => {
896
+ const parents = new Set(target.parentUUID ? [target.parentUUID] : []);
897
+ for (const inventory of ["entities", "detection_notebook"]) {
898
+ const read = inventory === "entities" ? reader.physical : reader.sharing;
899
+ for (const uuid of [target.uuid, ...identity.map(entry => entry.uuid)]) {
900
+ const row = read(uuid);
901
+ if (row?.parentUUID)
902
+ parents.add(row.parentUUID);
903
+ }
904
+ for (const row of reader.subtree(inventory, target.uuid)) {
905
+ if (row.parentUUID)
906
+ parents.add(row.parentUUID);
907
+ if (["workspace", "folder"].includes(row.type))
908
+ parents.add(row.uuid);
909
+ }
910
+ }
911
+ // Accepted new parents also occur in the manifest before they have rows.
912
+ for (const entry of identity)
913
+ if (entry.type === "folder")
914
+ parents.add(entry.uuid);
915
+ if (["workspace", "folder"].includes(target.type))
916
+ parents.add(target.uuid);
917
+ for (const uuid of parents) {
918
+ reader.physical(uuid);
919
+ reader.sharing(uuid);
920
+ reader.children("entities", uuid);
921
+ reader.children("detection_notebook", uuid);
922
+ }
923
+ }).basis;
924
+ }
925
+ /** An inactive identity may share its former logical name with a surviving
926
+ * sibling. Verify the removed identity, never emptiness of that old name. */
927
+ async #resolvedAbsence(intent, plan, context, remember) {
928
+ const witnesses = new PhysicalPathWitnesses();
929
+ let occupantProof;
930
+ // The occupant's receipt closed completely inside its own match.
931
+ const retain = () => {
932
+ const own = witnessProof([witnesses.seal()]);
933
+ const proof = occupantProof ? sourceProofs([own, occupantProof.proof]) : own;
934
+ closePhysically(proof);
935
+ remember?.({ stats: null, proof });
936
+ return true;
937
+ };
938
+ if (context.current) {
939
+ const current = tryLstat(context.current.absolutePath);
940
+ if (current)
941
+ return false;
942
+ witnesses.remember(context.current.absolutePath, current);
943
+ const recovery = this.#boundRecovery(intent);
944
+ if (!recovery?.source || !tryLstat(this.#displacedPath(intent, context)))
945
+ return false;
946
+ if (!await this.#matchesPreimage(intent, plan, recovery.source, this.#displacedPath(intent, context)))
947
+ return false;
948
+ }
949
+ const stats = tryLstat(context.targetPath);
950
+ witnesses.remember(context.targetPath, stats);
951
+ if (!stats)
952
+ return retain();
953
+ const occupant = this.#entityAtPath.get(context.targetPath);
954
+ return occupant !== undefined && occupant.uuid !== intent.entityId
955
+ && occupant.physicalKey === physicalKey(stats)
956
+ && await this.#matches(intent, portable(occupant), context.targetPath, plan, context.targetPath, proof => { occupantProof = proof; }) && retain();
957
+ }
958
+ #canRetireUnstartedIntent(intent) {
959
+ if (this.store.recoveryLocation(intent) !== null)
960
+ return false;
961
+ const current = this.#current(intent.entityId);
962
+ if (!current)
963
+ return false;
964
+ const control = this.#controlDirectory(intent, this.#anchors(portable(current)));
965
+ if (repositoryRevealInProgress(current.absolutePath))
966
+ return false;
967
+ // Existing effects must visit the normal guarded, asynchronous retirement
968
+ // path. Startup adoption is allowed to dispose private preparation only.
969
+ return !["displaced", "applied", "relocated"]
970
+ .some(name => tryLstat(join(control, name)) !== null);
971
+ }
972
+ #preimage(path) {
973
+ // O supplies last-proven identity and content. Reading disk here would
974
+ // incorrectly bless an edit made after the guard as a disposable preimage.
975
+ const prefix = `${path}${sep}`;
976
+ const rows = this.#database.prepare(`${groundSelect("entities")}
977
+ WHERE status = 'active' AND (absolute_path = ? OR (absolute_path >= ? AND absolute_path < ?))
978
+ ORDER BY absolute_path`).all(path, prefix, `${path}${String.fromCharCode(sep.charCodeAt(0) + 1)}`);
979
+ const root = rows.find(row => row.absolutePath === path), stat = tryLstat(path);
980
+ if (!root || !stat || physicalKey(stat) !== root.physicalKey) {
981
+ throw new Error("Apply preimage source association changed after its guard");
982
+ }
983
+ return { path, rows: rows.map(row => ({
984
+ path: relative(path, row.absolutePath).split(sep).join("/"), record: portable(row),
985
+ physical: row.absolutePath === path || this.#isContainer(row) ? row.physicalKey : null,
986
+ })) };
987
+ }
988
+ async #retainPreimages(intent, plan, context, remainder) {
989
+ const control = this.#controlDirectory(intent, context);
990
+ const existing = this.#boundRecovery(intent);
991
+ const bind = (recovery) => {
992
+ const { cones, uuids } = applyRecoveryFootprint(recovery);
993
+ return this.store.bindRecoveryLocation(intent, control, this.#ownershipScope(intent, recovery.binding.path, cones, uuids));
994
+ };
995
+ if (existing) {
996
+ if (existing.targetPath !== context.targetPath)
997
+ throw new Error("Apply recovery target address changed; explicit recovery is required");
998
+ if (!this.#receiptAllowsPlan(existing, context, this.#prepared.get(this.#key(intent))?.repository?.identity)) {
999
+ throw new Error("Apply recovery cannot authorize additional effects");
1000
+ }
1001
+ return bind(existing);
1002
+ }
1003
+ const repository = this.#prepared.get(this.#key(intent))?.repository;
1004
+ const anchor = context.current ?? context.parent;
1005
+ if (!anchor)
1006
+ throw new Error("Apply recovery lacks a bound root");
1007
+ const root = this.#root(anchor);
1008
+ const recovery = {
1009
+ contract: "amalgm.apply-recovery@6", intent, target: plan.target, targetPath: context.targetPath,
1010
+ binding: { path: root.absolutePath, physical: root.physicalKey },
1011
+ moves: [...context.beforeRelocations, ...context.afterRelocations].map(move => {
1012
+ const physical = this.#current(move.row.uuid);
1013
+ if (!physical)
1014
+ throw new Error("Apply collision move has no physical identity");
1015
+ return { uuid: physical.uuid, source: this.#preimage(move.source), destination: move.destination };
1016
+ }),
1017
+ source: context.current && tryLstat(context.current.absolutePath)
1018
+ ? this.#preimage(context.current.absolutePath) : null,
1019
+ relocated: repository ? this.#repositoryRelocations(context, repository.identity).map(group => ({
1020
+ uuid: group.sourceRoot.uuid, source: this.#preimage(group.sourceRoot.absolutePath),
1021
+ })) : [],
1022
+ remainder: remainder.map(entry => entry.path),
1023
+ };
1024
+ writeApplyRecovery(control, recovery);
1025
+ fsyncFile(join(control, "preimage.json"));
1026
+ fsyncDirectory(control);
1027
+ fsyncDirectory(dirname(control));
1028
+ return bind(recovery);
1029
+ }
1030
+ /** Durable ownership outlives a transient ground lease. Reuse the same
1031
+ * scope predicate as Detect; only this exact attempt may use its effects. */
1032
+ #foreignOwnership(intent, root, addresses, uuids = []) {
1033
+ const ownership = new ApplyOwnershipSnapshot(sqliteApplyOwnership(this.#database), intent);
1034
+ const owners = ownership.use(this.#ownershipScope(intent, root, addresses, uuids));
1035
+ return { waiting: ownershipWait(owners, this.store.recoveryLocation(intent) !== null), basis: ownership.seal() };
1036
+ }
1037
+ /** Every address an Apply touches lies in its binding: the root its
1038
+ * target or that target's parent is materialized in. */
1039
+ #bindingRoot(target) {
1040
+ const { current, parent } = this.#anchors(target);
1041
+ return this.#root((current ?? parent)).absolutePath;
1042
+ }
1043
+ #ownershipScope(intent, root, addresses, uuids) {
1044
+ const physical = addresses.flatMap(path => {
1045
+ const stat = tryLstat(path);
1046
+ return stat ? [ownedObject(root, physicalKey(stat))] : [];
1047
+ });
1048
+ return { subtrees: addresses, uuids: new Set([intent.entityId, ...uuids]), physical: new Set(physical) };
1049
+ }
1050
+ #boundRecovery(intent) {
1051
+ const control = this.store.recoveryLocation(intent);
1052
+ if (control === null)
1053
+ return null;
1054
+ const recovery = readApplyRecovery(control, intent);
1055
+ if (!recovery)
1056
+ throw new Error("Open Apply intent lost its recovery receipt");
1057
+ return recovery;
1058
+ }
1059
+ #receiptAllowsPlan(recovery, context, identity) {
1060
+ if (recovery.targetPath !== context.targetPath)
1061
+ return false;
1062
+ if (![...context.beforeRelocations, ...context.afterRelocations].every(move => recovery.moves.some(original => original.uuid === move.row.uuid && original.source.path === move.source && original.destination === move.destination)))
1063
+ return false;
1064
+ return !identity || this.#repositoryRelocations(context, identity).every(group => recovery.relocated.some(original => original.uuid === group.sourceRoot.uuid && original.source.path === group.sourceRoot.absolutePath));
1065
+ }
1066
+ /** An old effect owner must be handled before ordinary current-path or
1067
+ * parent checks can hand its own intermediate placement to Detect. Its
1068
+ * receipt lives inside the materialized root, so a missing root is root
1069
+ * recovery, never a lost receipt. */
1070
+ #recoveryReplan(intent, plan) {
1071
+ const control = this.store.recoveryLocation(intent);
1072
+ if (control === null)
1073
+ return null;
1074
+ const unavailableRoot = this.#unavailableRoot(plan.target);
1075
+ if (unavailableRoot)
1076
+ return unavailableRoot;
1077
+ const recovery = this.#boundRecovery(intent);
1078
+ if (!recovery)
1079
+ throw new Error("Open Apply intent lost its recovery receipt");
1080
+ const replan = { kind: "replan", reason: "Original Apply effects require a coherent cancellation before replanning." };
1081
+ if (plan.target.parentUUID !== null && !this.#current(plan.target.parentUUID))
1082
+ return replan;
1083
+ for (const move of recovery.moves) {
1084
+ const current = this.#current(move.uuid), source = tryLstat(move.source.path);
1085
+ const anchor = applyPreimageRoot(move.source).physical;
1086
+ if (current?.absolutePath === move.destination && source && physicalKey(source) === anchor)
1087
+ return replan;
1088
+ }
1089
+ return this.#receiptAllowsPlan(recovery, this.#context(plan.target, intent), this.#prepared.get(this.#key(intent))?.repository?.identity) ? null : replan;
1090
+ }
1091
+ async #recoveryPlacementRepair(intent, plan, recovery) {
1092
+ const original = groundStore(this.#database).readSnapshot(reader => {
1093
+ const moves = [...recovery.moves];
1094
+ const primary = reader.physical(intent.entityId);
1095
+ if (recovery.source && recovery.source.path !== recovery.targetPath && primary
1096
+ && JSON.stringify(portable(primary)) === JSON.stringify(applyPreimageRoot(recovery.source).record)) {
1097
+ moves.unshift({ uuid: intent.entityId, source: recovery.source, destination: recovery.targetPath });
1098
+ }
1099
+ const rows = new Map(), roots = new Map();
1100
+ for (const move of moves) {
1101
+ const current = reader.physical(move.uuid);
1102
+ rows.set(move.uuid, current ? reader.subtree("entities", move.uuid) : []);
1103
+ if (current) {
1104
+ const root = reader.physical(current.rootUUID);
1105
+ if (!root)
1106
+ throw new Error("Apply placement lost its original binding");
1107
+ roots.set(move.uuid, root);
1108
+ for (const path of [move.source.path, move.destination])
1109
+ reader.atPath("entities", current.rootUUID, relative(root.absolutePath, path).split(sep).join("/"));
1110
+ }
1111
+ }
1112
+ return { moves, rows, roots };
1113
+ });
1114
+ const repaired = [], witnesses = new PhysicalPathWitnesses();
1115
+ const preimages = [];
1116
+ for (const move of original.value.moves) {
1117
+ const rows = original.value.rows.get(move.uuid), current = rows.find(row => row.uuid === move.uuid);
1118
+ if (!current || ![move.source.path, move.destination].includes(current.absolutePath)) {
1119
+ throw new Error("Apply recovery lost its recorded placement participant");
1120
+ }
1121
+ const source = tryLstat(move.source.path), destination = tryLstat(move.destination);
1122
+ const anchor = applyPreimageRoot(move.source).physical;
1123
+ const atSource = source !== null && physicalKey(source) === anchor;
1124
+ const atDestination = destination !== null && physicalKey(destination) === anchor;
1125
+ if (atSource === atDestination)
1126
+ throw new Error("Apply recovery identity has ambiguous placement");
1127
+ const actual = atSource ? move.source.path : move.destination;
1128
+ const root = original.value.roots.get(move.uuid);
1129
+ if (!pathInside(root.absolutePath, actual))
1130
+ throw new Error("Apply placement crosses an unproved root binding");
1131
+ const validateRoot = () => {
1132
+ const stat = tryLstat(root.absolutePath);
1133
+ if (!stat?.isDirectory() || stat.isSymbolicLink() || physicalKey(stat) !== root.physicalKey) {
1134
+ throw new Error("Apply placement root changed");
1135
+ }
1136
+ };
1137
+ validateRoot();
1138
+ preimages.push(admissionProof(validateRoot));
1139
+ if (!await this.#matchesPreimage(intent, plan, move.source, actual, proof => preimages.push(proof))) {
1140
+ throw new Error("Apply recovery placement preimage changed");
1141
+ }
1142
+ const before = new Map(move.source.rows.map(row => [row.record.uuid, row]));
1143
+ if (rows.length !== before.size)
1144
+ throw new Error("Apply recovery placement membership changed");
1145
+ for (const row of rows) {
1146
+ const member = before.get(row.uuid);
1147
+ if (!member || JSON.stringify(portable(row)) !== JSON.stringify(member.record)
1148
+ || row.absolutePath !== join(current.absolutePath, ...member.path.split("/"))) {
1149
+ throw new Error("Apply recovery cannot rewrite newer physical attribution");
1150
+ }
1151
+ const absolutePath = join(actual, ...member.path.split("/"));
1152
+ witnesses.remember(absolutePath, tryLstat(absolutePath));
1153
+ if (current.absolutePath !== actual)
1154
+ repaired.push({ ...row, absolutePath,
1155
+ relativePath: relative(root.absolutePath, absolutePath).split(sep).join("/") });
1156
+ }
1157
+ // The contradictory recorded root slot must remain unchanged until
1158
+ // the cancellation transaction accepts this original placement proof.
1159
+ witnesses.remember(current.absolutePath, tryLstat(current.absolutePath));
1160
+ witnesses.remember(move.source.path, source);
1161
+ witnesses.remember(move.destination, destination);
1162
+ }
1163
+ return { rows: repaired, basis: original.basis,
1164
+ proof: sourceProofs([...preimages, witnessProof([witnesses.seal()])]) };
1165
+ }
1166
+ async #matchesPreimage(intent, plan, preimage, physicalRoot, remember) {
1167
+ const expected = new Map(preimage.rows.map(row => [row.path, row]));
1168
+ const proofs = [];
1169
+ const visit = async (path, relativePath) => {
1170
+ const row = expected.get(relativePath);
1171
+ if (!row || !await this.#matches(intent, row.record, path, plan, join(preimage.path, ...relativePath.split("/")), receipt => proofs.push(receipt.proof)))
1172
+ return false;
1173
+ const stat = tryLstat(path);
1174
+ if (["workspace", "folder", "repo.git"].includes(row.record.type) && (!stat?.isDirectory() || stat.isSymbolicLink()
1175
+ || physicalKey(stat) !== row.physical))
1176
+ return false;
1177
+ expected.delete(relativePath);
1178
+ if (stat?.isDirectory() && !stat.isSymbolicLink()) {
1179
+ for (const name of readdirSync(path)) {
1180
+ // Machine control and Git storage have their own ownership. Git's
1181
+ // complete portable state was checked by its adapter above.
1182
+ if (name === ".amalgm" || (name === ".git" && row.record.type === "repo.git"))
1183
+ continue;
1184
+ if (!await visit(join(path, name), relativePath ? `${relativePath}/${name}` : name))
1185
+ return false;
1186
+ }
1187
+ }
1188
+ return true;
1189
+ };
1190
+ if (!await visit(physicalRoot, "") || expected.size !== 0)
1191
+ return false;
1192
+ // A preimage match is a claim: its complete proof closes before it holds.
1193
+ const proof = sourceProofs(proofs);
1194
+ await closeSource(proof);
1195
+ remember?.(proof);
1196
+ return true;
1197
+ }
1198
+ async #proveRecoveryRetirement(intent, plan, context, purpose, repaired) {
1199
+ const control = this.#controlDirectory(intent, context);
1200
+ if ([context.targetPath, context.current?.absolutePath].some(path => path && repositoryRevealInProgress(path))) {
1201
+ throw new Error("Interrupted repository effects cannot be retired");
1202
+ }
1203
+ const recovery = this.#boundRecovery(intent);
1204
+ if (this.store.recoveryLocation(intent) !== null && !recovery)
1205
+ throw new Error("Open Apply intent lost its recovery receipt");
1206
+ const witnesses = new PhysicalPathWitnesses();
1207
+ witnesses.remember(control, tryLstat(control));
1208
+ if (!tryLstat(control))
1209
+ return { proof: witnessProof([witnesses.seal()]), attribution: null };
1210
+ const basis = groundStore(this.#database).readSnapshot(reader => {
1211
+ for (const uuid of new Set([intent.entityId, ...(recovery?.moves.map(move => move.uuid) ?? []),
1212
+ ...(recovery?.relocated.map(move => move.uuid) ?? [])]))
1213
+ reader.subtree("entities", uuid);
1214
+ }).basis;
1215
+ const proofs = [];
1216
+ const retain = (receipt) => { proofs.push(receipt.proof); };
1217
+ const corrected = new Map(repaired?.map(row => [row.uuid, row]));
1218
+ const placementProved = purpose.kind === "cancelled" && repaired !== undefined;
1219
+ // Cancellation already has the complete original-placement proof. Keep
1220
+ // that same closing validator instead of rereading every moved subtree.
1221
+ for (const move of placementProved ? [] : recovery?.moves ?? []) {
1222
+ const current = corrected.get(move.uuid) ?? this.#current(move.uuid);
1223
+ const allowed = purpose.kind === "materialized" ? [move.destination] : [move.source.path, move.destination];
1224
+ const physical = current && tryLstat(current.absolutePath);
1225
+ const anchor = applyPreimageRoot(move.source).physical;
1226
+ if (!current || !allowed.includes(current.absolutePath) || !physical
1227
+ || physicalKey(physical) !== anchor
1228
+ || !await this.#matchesPreimage(intent, plan, move.source, current.absolutePath, proof => proofs.push(proof))) {
1229
+ throw new Error("Apply collision relocation has an unresolved identity placement");
1230
+ }
1231
+ }
1232
+ if (recovery?.source && recovery.source.path !== recovery.targetPath) {
1233
+ const source = recovery.source, oldSlot = tryLstat(source.path);
1234
+ witnesses.remember(source.path, oldSlot);
1235
+ const primary = corrected.get(intent.entityId) ?? this.#current(intent.entityId);
1236
+ if (purpose.kind === "materialized") {
1237
+ if (!await this.#matches(intent, recovery.target, recovery.targetPath, plan, recovery.targetPath, retain)) {
1238
+ throw new Error("Apply primary movement lacks its accepted target");
1239
+ }
1240
+ }
1241
+ else if (!(placementProved && primary
1242
+ && JSON.stringify(portable(primary)) === JSON.stringify(applyPreimageRoot(source).record))
1243
+ && (!primary || ![source.path, recovery.targetPath].includes(primary.absolutePath)
1244
+ || (primary.absolutePath === recovery.targetPath && primary.version !== recovery.target.version)
1245
+ || !await this.#matches(intent, portable(primary), primary.absolutePath, plan, primary.absolutePath, retain))) {
1246
+ throw new Error("Apply primary movement has unresolved physical attribution");
1247
+ }
1248
+ if (oldSlot && (purpose.kind === "materialized" || primary?.absolutePath !== source.path)
1249
+ && !recovery.moves.some(move => {
1250
+ const anchor = applyPreimageRoot(move.source).physical;
1251
+ return move.destination === source.path && anchor === physicalKey(oldSlot)
1252
+ && (corrected.get(move.uuid) ?? this.#current(move.uuid))?.absolutePath === source.path;
1253
+ }))
1254
+ throw new Error("Apply primary source has an unexplained occupant");
1255
+ }
1256
+ const displaced = join(control, "displaced"), applied = join(control, "applied"), relocated = join(control, "relocated");
1257
+ if (tryLstat(displaced) && (!recovery?.source
1258
+ || !await this.#matchesPreimage(intent, plan, recovery.source, displaced, proof => proofs.push(proof)))) {
1259
+ throw new Error("Apply recovery retains an unexplained displaced version");
1260
+ }
1261
+ if (tryLstat(applied) && (!recovery || !await this.#matches(intent, recovery.target, applied, plan, recovery.targetPath, retain))) {
1262
+ throw new Error("Apply recovery retains an unexplained applied version");
1263
+ }
1264
+ if (tryLstat(relocated)) {
1265
+ const sources = new Map(recovery?.relocated.map(row => [row.uuid, row.source]) ?? []);
1266
+ for (const uuid of readdirSync(relocated)) {
1267
+ const source = sources.get(uuid);
1268
+ if (!source || !await this.#matchesPreimage(intent, plan, source, join(relocated, uuid), proof => proofs.push(proof))) {
1269
+ throw new Error("Apply recovery retains an unexplained relocated version");
1270
+ }
1271
+ }
1272
+ }
1273
+ if (readdirSync(control).some(name => !["preimage.json", "prepared", "displaced", "applied", "relocated"].includes(name))) {
1274
+ throw new Error("Apply control contains an unrecognized recovery obligation");
1275
+ }
1276
+ return { proof: sourceProofs([witnessProof([witnesses.seal()]), ...proofs]), attribution: basis };
1277
+ }
1278
+ async #prepareRetirement(intent, plan, purpose) {
687
1279
  try {
688
- this.#prepared.delete(this.#key(intent));
1280
+ const heldControl = this.store.recoveryLocation(intent);
1281
+ if (purpose.kind === "cancelled" && heldControl !== null) {
1282
+ const recovery = readApplyRecovery(heldControl, intent);
1283
+ if (!recovery)
1284
+ throw new Error("Open Apply intent lost its recovery receipt");
1285
+ // An old obligation cannot depend on the new target's parent or Git
1286
+ // package becoming available. Its immutable receipt supplies scope.
1287
+ this.#releaseGround(intent);
1288
+ const addresses = [...new Set([heldControl, recovery.targetPath,
1289
+ ...(recovery.source ? [recovery.source.path] : []),
1290
+ ...recovery.moves.flatMap(move => [move.source.path, move.destination]),
1291
+ ...recovery.relocated.map(move => move.source.path)])];
1292
+ const release = await this.#acquireGround(addresses);
1293
+ this.#groundReleases.set(this.#key(intent), { addresses, release });
1294
+ const ownership = this.#foreignOwnership(intent, recovery.binding.path, addresses, applyRecoveryFootprint(recovery).uuids);
1295
+ if (ownership.waiting) {
1296
+ this.#releaseGround(intent);
1297
+ return ownership.waiting;
1298
+ }
1299
+ const binding = tryLstat(recovery.binding.path);
1300
+ if (!binding?.isDirectory() || binding.isSymbolicLink()
1301
+ || physicalKey(binding) !== recovery.binding.physical) {
1302
+ throw new Error("Apply recovery root is missing or replaced");
1303
+ }
1304
+ const repair = await this.#recoveryPlacementRepair(intent, plan, recovery);
1305
+ const cleanup = await describeApplyCleanup(recovery.binding, heldControl);
1306
+ const context = { current: this.#current(intent.entityId), parent: null,
1307
+ targetPath: recovery.targetPath, targetName: basename(recovery.targetPath),
1308
+ beforeRelocations: [], afterRelocations: [], collisionEligible: false };
1309
+ const retirement = await this.#proveRecoveryRetirement(intent, plan, context, purpose, repair.rows);
1310
+ closePhysically(repair.proof);
1311
+ assertApplyCleanupUnchanged(cleanup);
1312
+ this.store.stageRetirement(intent, purpose, cleanup, { repair, proof: this.#cleanupProof(ownership, retirement.proof, cleanup), attribution: retirement.attribution });
1313
+ this.#prepared.delete(this.#key(intent));
1314
+ return;
1315
+ }
1316
+ await this.#holdGround(intent, plan);
1317
+ const ownership = this.#foreignOwnership(intent, this.#bindingRoot(plan.target), this.applyAddresses(intent, plan));
1318
+ if (ownership.waiting) {
1319
+ this.#releaseGround(intent);
1320
+ return ownership.waiting;
1321
+ }
689
1322
  this.#observations.delete(this.#key(intent));
690
- const context = this.#context(plan.target);
691
- rmSync(this.#controlDirectory(intent, {
692
- current: context.current,
693
- parent: context.parent,
694
- }), { recursive: true, force: true });
1323
+ const context = this.#context(plan.target, intent);
1324
+ const control = this.#controlDirectory(intent, context);
1325
+ const recovery = this.#boundRecovery(intent);
1326
+ const anchor = context.current ?? context.parent;
1327
+ if (!anchor)
1328
+ throw new Error("Apply retirement has no root binding");
1329
+ const root = this.#root(anchor);
1330
+ const binding = recovery?.binding ?? { path: root.absolutePath, physical: root.physicalKey };
1331
+ const cleanup = await describeApplyCleanup(binding, control);
1332
+ const retirement = await this.#proveRecoveryRetirement(intent, plan, context, purpose);
1333
+ assertApplyCleanupUnchanged(cleanup);
1334
+ this.store.stageRetirement(intent, purpose, cleanup, { proof: this.#cleanupProof(ownership, retirement.proof, cleanup), attribution: retirement.attribution });
1335
+ this.#prepared.delete(this.#key(intent));
695
1336
  }
696
1337
  catch (error) {
697
1338
  this.#releaseGround(intent);
698
1339
  throw error;
699
1340
  }
700
1341
  }
1342
+ /** Retirement re-asks its sources before the terminal save; as it posts,
1343
+ * ownership, native witnesses and the cleanup description close. */
1344
+ #cleanupProof(ownership, retirement, cleanup) {
1345
+ return { semantic: () => retirement.semantic(), witnesses: [],
1346
+ admission: () => { ownership.basis.assertCurrent(); closePhysically(retirement); assertApplyCleanupUnchanged(cleanup); } };
1347
+ }
701
1348
  async #holdGround(intent, plan) {
1349
+ // Immutable identity determines repository relocation cones. Load it
1350
+ // before acquiring ground, including after restart with an empty cache.
1351
+ if (plan.target.type === "repo.git" && plan.target.status === "active") {
1352
+ await this.#ensurePrepared(intent, plan);
1353
+ }
702
1354
  const key = this.#key(intent);
703
- if (this.#groundReleases.has(key))
704
- return;
705
- this.#groundReleases.set(key, await this.#acquireGround(this.applyAddresses(intent, plan)));
1355
+ for (let attempt = 0; attempt < 3; attempt += 1) {
1356
+ const addresses = this.#groundCones(intent, plan);
1357
+ const held = this.#groundReleases.get(key);
1358
+ if (held && addresses.every(address => held.addresses.some(cone => pathInside(cone, address))))
1359
+ return;
1360
+ this.#releaseGround(intent);
1361
+ // A repository's ref updates append to its reflogs, which may have or
1362
+ // later gain links anywhere; the grant declares them before any effect.
1363
+ const reflogs = plan.target.type === "repo.git"
1364
+ ? () => this.applyAddresses(intent, plan).flatMap(reflogKeys) : undefined;
1365
+ const release = await this.#acquireGround(addresses, reflogs);
1366
+ this.#groundReleases.set(key, { addresses, release });
1367
+ // Context may have changed while waiting. No effects may use cones
1368
+ // discovered after acquisition without releasing and proving them again.
1369
+ if (this.#groundCones(intent, plan).every(address => addresses.some(cone => pathInside(cone, address))))
1370
+ return;
1371
+ }
1372
+ this.#releaseGround(intent);
1373
+ throw new Error("Apply ground scope changed repeatedly while acquiring ownership");
1374
+ }
1375
+ /** The held ground: every address plus Git storage a repository at that
1376
+ * address keeps outside its checkout, which the same effect writes. */
1377
+ #groundCones(intent, plan) {
1378
+ return [...new Set(this.applyAddresses(intent, plan).flatMap(address => repositoryGroundCones(address)))];
706
1379
  }
707
1380
  /**
708
1381
  * The visible addresses one Apply may read or replace: the target address,
@@ -712,10 +1385,25 @@ export class EntityApplyHost {
712
1385
  * free for Detect and other Apply work.
713
1386
  */
714
1387
  applyAddresses(intent, plan) {
715
- const context = this.#context(plan.target);
1388
+ const context = this.#context(plan.target, intent);
716
1389
  const addresses = new Set([context.targetPath]);
1390
+ const physical = this.#entityByUuid.get(intent.entityId);
1391
+ if (physical)
1392
+ addresses.add(physical.absolutePath);
717
1393
  if (context.current)
718
1394
  addresses.add(context.current.absolutePath);
1395
+ const recovery = this.#boundRecovery(intent);
1396
+ if (recovery) {
1397
+ addresses.add(recovery.targetPath);
1398
+ if (recovery.source)
1399
+ addresses.add(recovery.source.path);
1400
+ for (const row of recovery.relocated)
1401
+ addresses.add(row.source.path);
1402
+ for (const move of recovery.moves) {
1403
+ addresses.add(move.source.path);
1404
+ addresses.add(move.destination);
1405
+ }
1406
+ }
719
1407
  for (const relocation of [...context.beforeRelocations, ...context.afterRelocations]) {
720
1408
  addresses.add(relocation.source);
721
1409
  addresses.add(relocation.destination);
@@ -730,23 +1418,48 @@ export class EntityApplyHost {
730
1418
  }
731
1419
  return [...addresses];
732
1420
  }
1421
+ /** Called only after the requesting Watch cutoff settles. Outbox protection
1422
+ * remains the rail's independent check; local-only observations never fake it. */
1423
+ captureOutcome(intent, plan, observedAddresses) {
1424
+ const outcome = settledCaptureOutcome(this.#database, {
1425
+ authorityId: intent.authorityId, entityId: intent.entityId,
1426
+ addresses: [...observedAddresses, ...this.applyAddresses(intent, plan)],
1427
+ });
1428
+ return outcome.kind === "settled" ? { kind: "settled" } : outcome;
1429
+ }
733
1430
  #releaseGround(intent) {
734
1431
  const key = this.#key(intent);
735
1432
  this.#observations.delete(key);
736
- const release = this.#groundReleases.get(key);
737
- if (!release)
1433
+ const held = this.#groundReleases.get(key);
1434
+ if (!held)
738
1435
  return;
739
1436
  this.#groundReleases.delete(key);
740
- release();
1437
+ held.release();
741
1438
  }
742
1439
  /** A collision relocation is safe only when every source still proves its
743
1440
  * own portable identity and every destination is either free or another
744
1441
  * relocation source. The final reveal then performs the whole finite chain
745
1442
  * while holding all source and destination cones. */
746
- async #lawRelocationsReady(intent, plan, relocations) {
1443
+ async #lawRelocationsReady(intent, plan, relocations, vacated = []) {
747
1444
  if (relocations.length === 0)
748
1445
  return false;
749
- const sources = new Set(relocations.map(({ source }) => source));
1446
+ // The executor only moves into a vacant slot. Reject a closed dependency
1447
+ // cycle before changing either rows or disk; retrying it cannot free a slot.
1448
+ const next = new Map(relocations.map(move => [move.source, move.destination]));
1449
+ const checked = new Set();
1450
+ for (const source of next.keys()) {
1451
+ const chain = new Set();
1452
+ let current = source;
1453
+ while (current !== undefined && next.has(current) && !checked.has(current)) {
1454
+ if (chain.has(current))
1455
+ throw new Error("Apply derived-address plan contains a closed relocation cycle");
1456
+ chain.add(current);
1457
+ current = next.get(current);
1458
+ }
1459
+ for (const path of chain)
1460
+ checked.add(path);
1461
+ }
1462
+ const sources = new Set([...vacated, ...relocations.map(({ source }) => source)]);
750
1463
  for (const { row, source, destination } of relocations) {
751
1464
  if (tryLstat(destination) && !sources.has(destination))
752
1465
  return false;
@@ -757,9 +1470,9 @@ export class EntityApplyHost {
757
1470
  }
758
1471
  /** Relocate a derived-name chain without ever overwriting ground. Each move
759
1472
  * whose destination is free commits row addresses first, then renames disk.
760
- * A crash between those facts is repaired by Detect from the same portable
761
- * identity; the derived suffix can never become a rename mutation. */
762
- #relocateLawSiblings(_context, relocations) {
1473
+ * A crash between those facts belongs to this Apply receipt. Recovery
1474
+ * reconciles its placement before Detect may attribute another change. */
1475
+ async #relocateLawSiblings(_context, relocations) {
763
1476
  const pending = [...relocations];
764
1477
  while (pending.length > 0) {
765
1478
  const sources = new Set(pending.map(({ source }) => source));
@@ -773,64 +1486,32 @@ export class EntityApplyHost {
773
1486
  const { row, source, destination } = relocation;
774
1487
  if (!tryLstat(source))
775
1488
  continue;
776
- const destinationRelative = relative(this.#root(row).absolutePath, destination)
777
- .split(sep).join("/");
778
- const descendants = (rows) => rows.filter((candidate) => pathInside(source, candidate.absolutePath));
779
- const entityRows = descendants(this.#rowsUnderRoot.all(row.rootUUID));
780
- const notebookRows = descendants(this.#notebookUnderRoot.all(row.rootUUID));
781
- this.#database.transaction(() => {
782
- for (const candidate of entityRows) {
783
- this.#relocateEntityRow.run(destinationRelative + candidate.relativePath.slice(row.relativePath.length), destination + candidate.absolutePath.slice(source.length), candidate.uuid);
784
- }
785
- for (const candidate of notebookRows) {
786
- this.#relocateNotebookRow.run(destinationRelative + candidate.relativePath.slice(row.relativePath.length), destination + candidate.absolutePath.slice(source.length), candidate.uuid);
787
- }
788
- })();
1489
+ const relativePath = relative(this.#root(row).absolutePath, destination).split(sep).join("/");
1490
+ await save(this.#database.name, "apply.relocateRows", { uuid: row.uuid,
1491
+ destination: { absolutePath: destination, relativePath } });
789
1492
  this.#onLawRelocationStage?.("rows-committed", row.uuid, destination);
790
1493
  renameSync(source, destination);
791
1494
  this.#onLawRelocationStage?.("ground-moved", row.uuid, destination);
792
- const moved = lstatSync(destination);
793
- this.#refreshEntityCtime.run(moved.ctimeMs, row.uuid);
794
- this.#refreshNotebookCtime.run(moved.ctimeMs, row.uuid);
1495
+ await save(this.#database.name, "apply.renameSignatures", { uuid: row.uuid, ctimeNs: lstatSync(destination, { bigint: true }).ctimeNs });
795
1496
  }
796
1497
  }
797
1498
  /** Reveal has already moved or removed the target ground. Release its old
798
1499
  * SQLite address before a sibling takes that slot; final Verify will write
799
1500
  * the complete accepted row. The durable Apply intent owns any crash in
800
1501
  * this short interval. */
801
- #relocateAfterTarget(target, context) {
1502
+ async #relocateAfterTarget(target, context) {
802
1503
  if (context.afterRelocations.length === 0)
803
1504
  return;
804
1505
  const current = context.current;
805
1506
  if (current) {
806
- const entityRows = this.#rowsUnderRoot.all(current.rootUUID)
807
- .filter((row) => pathInside(current.absolutePath, row.absolutePath));
808
- const notebookRows = this.#notebookUnderRoot.all(current.rootUUID)
809
- .filter((row) => pathInside(current.absolutePath, row.absolutePath));
810
- if (target.status === "active") {
811
- const destinationRelative = relative(this.#root(current).absolutePath, context.targetPath)
812
- .split(sep).join("/");
813
- this.#database.transaction(() => {
814
- for (const row of entityRows) {
815
- this.#relocateEntityRow.run(destinationRelative + row.relativePath.slice(current.relativePath.length), context.targetPath + row.absolutePath.slice(current.absolutePath.length), row.uuid);
816
- }
817
- for (const row of notebookRows) {
818
- this.#relocateNotebookRow.run(destinationRelative + row.relativePath.slice(current.relativePath.length), context.targetPath + row.absolutePath.slice(current.absolutePath.length), row.uuid);
819
- }
820
- })();
821
- }
822
- else {
823
- const removeEntity = this.#database.prepare("DELETE FROM entities WHERE uuid = ?");
824
- const removeNotebook = this.#database.prepare("DELETE FROM detection_notebook WHERE uuid = ?");
825
- this.#database.transaction(() => {
826
- for (const row of entityRows)
827
- removeEntity.run(row.uuid);
828
- for (const row of notebookRows)
829
- removeNotebook.run(row.uuid);
830
- })();
831
- }
1507
+ const root = target.status === "active" ? this.#root(context.parent ?? current) : null;
1508
+ await save(this.#database.name, "apply.relocateRows", { uuid: current.uuid, destination: root && {
1509
+ absolutePath: context.targetPath,
1510
+ relativePath: relative(root.absolutePath, context.targetPath).split(sep).join("/"),
1511
+ root: { uuid: root.uuid, resourceId: root.resourceId },
1512
+ } });
832
1513
  }
833
- this.#relocateLawSiblings(context, context.afterRelocations);
1514
+ await this.#relocateLawSiblings(context, context.afterRelocations);
834
1515
  }
835
1516
  #emit(intent, plan, stage) {
836
1517
  this.#onStage?.({
@@ -842,6 +1523,151 @@ export class EntityApplyHost {
842
1523
  at: this.#now(),
843
1524
  });
844
1525
  }
1526
+ /** The identity frontier the sharing basis holds where a repository
1527
+ * reveals: B's sealed repository result, or, while B still holds that
1528
+ * ground as a container, each shared member the repository would own.
1529
+ * Never a later query of O. */
1530
+ async #sharedIdentity(uuid) {
1531
+ const shared = this.#rowByUuid.get(uuid);
1532
+ if (shared?.status !== "active" || !this.#isContainer(shared))
1533
+ return [];
1534
+ if (shared.type !== "repo.git") {
1535
+ const rows = groundStore(this.#database).rowsUnder("detection_notebook", shared.rootUUID, shared.relativePath);
1536
+ return repositoryTerritory(rows, shared).filter(row => row.status === "active")
1537
+ .map(row => identityEntry(shared, row));
1538
+ }
1539
+ const identity = shared.transportVersion ? await readCachedRepositoryIdentity(this.#cacheDir, shared.transportVersion) : null;
1540
+ if (!identity)
1541
+ throw new Error("repository sharing basis has no sealed identity frontier");
1542
+ return identity.entries;
1543
+ }
1544
+ /** Local ground a repository, or a container becoming one, holds beyond
1545
+ * an expected identity that sharing never held. Anything else beyond the
1546
+ * identity is unaccounted: null. */
1547
+ #remainder(repository, expected) {
1548
+ const known = new Set(expected.map(entry => entry.uuid));
1549
+ const rows = groundStore(this.#database).rowsUnder("entities", repository.rootUUID, repository.relativePath);
1550
+ const extras = repositoryTerritory(rows, repository)
1551
+ .filter(row => row.status === "active" && !known.has(row.uuid));
1552
+ if (extras.length === 0)
1553
+ return [];
1554
+ if (extras.some(row => this.#rowByUuid.get(row.uuid) !== undefined))
1555
+ return null;
1556
+ return extras.map(row => identityEntry(repository, row));
1557
+ }
1558
+ /** The extras one repository comparison may account for: once effects are
1559
+ * authorized, exactly the receipt's frozen allowance; before, only the
1560
+ * local remainder the committed inventories name. */
1561
+ #allowance(intent, identity) {
1562
+ const recovery = this.#boundRecovery(intent);
1563
+ if (recovery)
1564
+ return applyRecoveryRemainder(recovery);
1565
+ const repository = this.#current(intent.entityId);
1566
+ return repository?.type === "repo.git" ? this.#remainder(repository, identity) : [];
1567
+ }
1568
+ /** The remainder a reveal keeps, relative to the sharing basis it replaces. */
1569
+ async #revealRemainder(intent, context) {
1570
+ const recovery = this.#boundRecovery(intent);
1571
+ if (recovery)
1572
+ return applyRecoveryRemainder(recovery);
1573
+ if (!context.current || !this.#isContainer(context.current) || !tryLstat(context.current.absolutePath))
1574
+ return [];
1575
+ const remainder = this.#remainder(context.current, await this.#sharedIdentity(context.current.uuid));
1576
+ if (!remainder)
1577
+ throw new Error("repository ground changed after its final guard; Detect must examine it again");
1578
+ return remainder;
1579
+ }
1580
+ #repositoryIdentityMatches(intent, path, identity) {
1581
+ const repository = this.#entityAtPath.get(path);
1582
+ if (!repository || repository.type !== "repo.git")
1583
+ return false;
1584
+ const remainder = this.#allowance(intent, identity);
1585
+ if (!remainder)
1586
+ return false;
1587
+ const kept = new Set(remainder.map(entry => entry.uuid));
1588
+ const rows = groundStore(this.#database).rowsUnder("entities", repository.rootUUID, repository.relativePath);
1589
+ const territory = indexRepositoryTerritory(rows);
1590
+ const actual = territory.ownedBy(repository.uuid).filter(row => row.status === "active" && !kept.has(row.uuid));
1591
+ const expected = new Map(identity.map(row => [row.uuid, row]));
1592
+ return actual.length === expected.size && actual.every(row => {
1593
+ const member = expected.get(row.uuid);
1594
+ return member?.type === row.type && row.absolutePath === join(path, ...member.path.split("/"))
1595
+ && (!this.#isContainer(row) || this.#samePhysicalDirectory(row));
1596
+ });
1597
+ }
1598
+ /** The rows a displacement wait names: each topmost remainder row inside
1599
+ * the container and every O row from it up to the container. The answer
1600
+ * depends only on those rows, so a change to any of them may release it. */
1601
+ #displacedRemainder(container, remainder) {
1602
+ const inside = remainder.filter(row => row.uuid !== container.uuid
1603
+ && pathInside(container.absolutePath, row.absolutePath));
1604
+ const held = new Set(inside.map(row => row.uuid));
1605
+ const named = new Set();
1606
+ for (const row of inside) {
1607
+ if (row.parentUUID !== null && held.has(row.parentUUID))
1608
+ continue;
1609
+ for (let at = row; at && !named.has(at.uuid); at = at.uuid === container.uuid || at.parentUUID === null ? null : this.#current(at.parentUUID)) {
1610
+ named.add(at.uuid);
1611
+ }
1612
+ }
1613
+ return [...named];
1614
+ }
1615
+ /** Descendants whose local result has no authority sequence yet. Cached
1616
+ * bytes keep the promise deliverable but do not order it before the
1617
+ * incoming removal; once Send sequences them the guard runs again. */
1618
+ #pendingPublication(authorityId, container) {
1619
+ return this.#pendingOutbox.all(authorityId).flatMap(({ entity_id: uuid }) => {
1620
+ const row = uuid === container.uuid ? null : this.#current(uuid);
1621
+ return row && pathInside(container.absolutePath, row.absolutePath) ? [uuid] : [];
1622
+ });
1623
+ }
1624
+ /** Ground the accepted repository would remove or overwrite. Each observed
1625
+ * obstruction is named with its chain; the repository itself is always
1626
+ * named, so the examination that classifies ground Detect has not yet
1627
+ * seen inside it is what releases the wait. */
1628
+ #obstructedRepository(repository, error) {
1629
+ const observed = error.paths.flatMap(path => {
1630
+ const row = this.#entityAtPath.get(join(repository.absolutePath, path));
1631
+ return row ? [row] : [];
1632
+ });
1633
+ return { kind: "dependency", reason: error.message,
1634
+ waitFor: { producer: "detect", entityIds: [...new Set([repository.uuid, ...this.#displacedRemainder(repository, observed)])] },
1635
+ blocker: { code: "local-ground-would-be-displaced", effect: "repository-reveal" } };
1636
+ }
1637
+ #isContainer(row) {
1638
+ return row.type === "workspace" || row.type === "folder" || row.type === "repo.git";
1639
+ }
1640
+ #samePhysicalDirectory(row) {
1641
+ const physical = tryLstat(row.absolutePath);
1642
+ return physical !== null && physical.isDirectory() && !physical.isSymbolicLink()
1643
+ && physicalKey(physical) === row.physicalKey;
1644
+ }
1645
+ #directoryAncestorsMatch(row) {
1646
+ const seen = new Set();
1647
+ let current = row;
1648
+ while (current) {
1649
+ if (seen.has(current.uuid))
1650
+ throw new Error("Apply directory ancestry has a cycle");
1651
+ seen.add(current.uuid);
1652
+ if (this.#isContainer(current) && !this.#samePhysicalDirectory(current))
1653
+ return false;
1654
+ if (current.uuid === current.rootUUID)
1655
+ return true;
1656
+ current = current.parentUUID === null ? null : this.#current(current.parentUUID);
1657
+ }
1658
+ return false;
1659
+ }
1660
+ /** Byte equality cannot adopt a replaced populated directory. Leaves may
1661
+ * have a new inode after an ordinary atomic save; containers retain their
1662
+ * proven physical identity until Detect or this intent accounts for a move. */
1663
+ #physicalDirectoriesMatch(row) {
1664
+ if (!this.#directoryAncestorsMatch(row))
1665
+ return false;
1666
+ if (!this.#isContainer(row))
1667
+ return true;
1668
+ return groundStore(this.#database).rowsUnder("entities", row.rootUUID, row.relativePath)
1669
+ .every(child => child.status !== "active" || !this.#isContainer(child) || this.#samePhysicalDirectory(child));
1670
+ }
845
1671
  #repositoryRelocations(context, identity) {
846
1672
  const relocations = identity.flatMap((entry) => {
847
1673
  const current = this.#current(entry.uuid);
@@ -903,16 +1729,27 @@ export class EntityApplyHost {
903
1729
  const displaced = this.#displacedPath(intent, context);
904
1730
  if (!tryLstat(displaced))
905
1731
  return false;
906
- if (await this.#matches(intent, portable(current), displaced, plan))
1732
+ const control = this.#controlDirectory(intent, context);
1733
+ const recovery = this.#boundRecovery(intent);
1734
+ if (!recovery?.source)
1735
+ throw new Error("Displaced Apply ground has no immutable preimage");
1736
+ if (recovery.targetPath !== context.targetPath)
1737
+ throw new Error("Apply recovery target address changed; explicit recovery is required");
1738
+ if (await this.#matchesPreimage(intent, plan, recovery.source, displaced))
907
1739
  return false;
908
- const applied = join(this.#controlDirectory(intent, context), "applied");
909
- rmSync(applied, { recursive: true, force: true });
1740
+ const source = recovery.source.path;
1741
+ const applied = join(control, "applied");
1742
+ // The retained local version can return only without erasing another
1743
+ // unaccounted-for version. Ambiguity keeps all ground and this intent.
1744
+ if (tryLstat(applied)
1745
+ || (source !== context.targetPath && tryLstat(source))
1746
+ || (tryLstat(context.targetPath) && !await this.#matches(intent, plan.target, context.targetPath, plan))) {
1747
+ throw new Error("Apply recovery has multiple local versions; explicit recovery is required");
1748
+ }
910
1749
  if (tryLstat(context.targetPath))
911
1750
  renameSync(context.targetPath, applied);
912
- ensureUserDir(dirname(current.absolutePath));
913
- if (tryLstat(current.absolutePath))
914
- rmSync(current.absolutePath, { recursive: true, force: true });
915
- renameSync(displaced, current.absolutePath);
1751
+ ensureUserDir(dirname(source));
1752
+ renameSync(displaced, source);
916
1753
  return true;
917
1754
  }
918
1755
  #installArtifact(intent, context, target, artifact) {
@@ -944,11 +1781,16 @@ export class EntityApplyHost {
944
1781
  const displaced = this.#displacedPath(intent, context);
945
1782
  const current = tryLstat(context.targetPath);
946
1783
  if (current) {
947
- rmSync(displaced, { recursive: true, force: true });
948
- // A regular file keeps its displaced bytes through a hard link, so the
949
- // reveal is one atomic rename and no reader ever sees the path absent.
950
- // Other kinds (links, directories) cannot be linked and keep two renames.
951
- if (current.isFile())
1784
+ const held = tryLstat(displaced);
1785
+ if (held) {
1786
+ // A stopped replacement may already have linked its exact live file.
1787
+ // Reuse that same backup without erasing either version. The immutable
1788
+ // receipt and Verify still catch a save racing either attempt.
1789
+ if (!current.isFile() || !held.isFile() || physicalKey(current) !== physicalKey(held)) {
1790
+ throw new Error("Apply replacement still owns a different displaced preimage");
1791
+ }
1792
+ }
1793
+ else if (current.isFile())
952
1794
  linkSync(context.targetPath, displaced);
953
1795
  else
954
1796
  renameSync(context.targetPath, displaced);
@@ -964,45 +1806,62 @@ export class EntityApplyHost {
964
1806
  }
965
1807
  fsyncDirectory(dirname(context.targetPath));
966
1808
  }
967
- #moveWithRecovery(intent, source, destination, context) {
1809
+ #moveToVacantAddress(source, destination) {
968
1810
  ensureUserDir(dirname(destination));
969
1811
  if (tryLstat(destination)) {
970
- const displaced = this.#displacedPath(intent, context);
971
- rmSync(displaced, { recursive: true, force: true });
972
- renameSync(destination, displaced);
1812
+ throw new Error("Apply move destination is still occupied");
973
1813
  }
974
1814
  renameSync(source, destination);
975
1815
  }
976
1816
  #removeWithRecovery(intent, context) {
977
- const source = context.current?.absolutePath ?? context.targetPath;
978
- if (!tryLstat(source))
1817
+ const source = context.current?.absolutePath;
1818
+ if (!source || context.current?.status !== "active" || !tryLstat(source))
979
1819
  return;
980
1820
  const displaced = this.#displacedPath(intent, context);
981
- rmSync(displaced, { recursive: true, force: true });
1821
+ if (tryLstat(displaced))
1822
+ throw new Error("Apply removal still owns a displaced preimage");
982
1823
  ensurePrivateDir(dirname(displaced));
983
1824
  renameSync(source, displaced);
984
1825
  }
985
- async #matches(intent, record, path, plan) {
1826
+ async #matches(intent, record, path, plan, originalPath = path, remember) {
986
1827
  const stats = tryLstat(path);
1828
+ // A match is a claim, so its proof closes before it returns. A repository
1829
+ // observation already closed at #observe with nothing awaited since; it
1830
+ // joins the retained receipt without a second Git answer here.
1831
+ const retain = async (actual, proof, closed) => {
1832
+ await closeSource(proof);
1833
+ remember?.({ stats: actual ? Object.freeze({ dev: actual.dev, ino: actual.ino, mode: actual.mode, nlink: actual.nlink,
1834
+ size: actual.size, mtimeNs: actual.mtimeNs, ctimeNs: actual.ctimeNs }) : null,
1835
+ proof: closed ? sourceProofs([proof, closed]) : proof });
1836
+ return true;
1837
+ };
1838
+ const witnesses = new PhysicalPathWitnesses();
1839
+ witnesses.remember(path, stats);
1840
+ const pathProof = witnessProof([witnesses.seal()]);
987
1841
  if (record.status !== "active")
988
1842
  return stats === null;
989
1843
  if (!stats)
990
1844
  return false;
991
- if (record.type === "file.text" || record.type === "file.binary") {
992
- return stats.isFile() && !stats.isSymbolicLink()
993
- && record.payloadVersion !== null
994
- && record.fileMode === portableFileMode(record.type, stats.mode)
995
- && await this.#hashFile(path) === record.payloadVersion;
996
- }
997
- if (record.type === "link") {
998
- return stats.isSymbolicLink()
999
- && record.payloadVersion !== null
1000
- && sha256Hex(Buffer.from(readlinkSync(path), "utf8")) === record.payloadVersion;
1845
+ if (["file.text", "file.binary", "link"].includes(record.type)) {
1846
+ if (!record.payloadVersion)
1847
+ return false;
1848
+ const format = payloadFormat(record.payloadVersion);
1849
+ if (format !== "sha1" && format !== "sha256")
1850
+ return false;
1851
+ const { observation, witness } = await observeLiteralPath(dirname(path), basename(path), format, "apply");
1852
+ if (observation.kind !== "leaf" || observation.leaf.type !== record.type)
1853
+ return false;
1854
+ const leaf = observation.leaf;
1855
+ const payload = leafPayload(record.payloadVersion, format, leaf);
1856
+ return payload === record.payloadVersion && (record.type === "link"
1857
+ || record.fileMode === portableFileMode(record.type, Number(leaf.stats.mode)))
1858
+ && await retain(leaf.stats, witnessProof([witness]));
1001
1859
  }
1002
1860
  if (record.type === "reference") {
1003
1861
  if (!stats.isSymbolicLink() || !record.payloadVersion)
1004
1862
  return false;
1005
- return readlinkSync(path) === relative(dirname(path), join(this.#bindingDir, record.payloadVersion));
1863
+ return readlinkSync(path) === relative(dirname(originalPath), join(this.#bindingDir, record.payloadVersion))
1864
+ && await retain(stats, pathProof);
1006
1865
  }
1007
1866
  if (record.type === "repo.git") {
1008
1867
  if (!stats.isDirectory() || !hasGitMarker(path)
@@ -1015,20 +1874,29 @@ export class EntityApplyHost {
1015
1874
  const file = join(this.#cacheDir, `${record.transportVersion}.bin`);
1016
1875
  if (!existsSync(file))
1017
1876
  return false;
1018
- const layout = inspectRepositoryTransportFile(file);
1877
+ const layout = await inspectRepositoryTransportFile(file);
1878
+ if (repositoryPayloadVersion(layout, sha256Hex) !== record.payloadVersion) {
1879
+ throw new Error("repository transport does not describe the complete accepted payload");
1880
+ }
1019
1881
  const artifact = repositoryArtifact(plan);
1020
1882
  const identity = artifact && record.transportVersion === artifact.contentHash
1021
1883
  ? (await this.#ensurePrepared(intent, plan)).repository?.identity
1022
- : readCachedRepositoryIdentity(this.#cacheDir, record.transportVersion);
1884
+ : (await readCachedRepositoryIdentity(this.#cacheDir, record.transportVersion))?.entries;
1023
1885
  if (!identity)
1024
1886
  throw new Error("repository Apply has no prepared transport identity");
1025
1887
  const prior = {
1026
- stateId: record.payloadVersion,
1888
+ stateId: layout.stateId,
1027
1889
  transportVersion: record.transportVersion,
1028
1890
  card: layout.card,
1029
1891
  identityHash: layout.identityHash,
1030
1892
  };
1031
- return repositoryObservationMatches(this.#observe(intent, path, identity), identity, prior);
1893
+ // Only this Apply's own repository may hold accounted remainder.
1894
+ const remainder = record.uuid === intent.entityId ? this.#allowance(intent, identity) : [];
1895
+ if (!remainder)
1896
+ return false;
1897
+ const observed = await this.#observe(intent, path, identity, remainder);
1898
+ return repositoryObservationMatches(observed, identity, prior)
1899
+ && await retain(stats, pathProof, observed.proof);
1032
1900
  }
1033
1901
  if (!stats.isDirectory() || stats.isSymbolicLink())
1034
1902
  return false;
@@ -1037,16 +1905,17 @@ export class EntityApplyHost {
1037
1905
  // Containers do not ship or overwrite a child list. Their physical Apply
1038
1906
  // proof is only the accepted address/type/lifecycle result; independently
1039
1907
  // ordered child UUIDs establish membership as they materialize.
1040
- return true;
1908
+ return retain(stats, pathProof);
1041
1909
  }
1042
- #verifiedRow(target, context) {
1043
- const prior = context.current ?? context.parent;
1910
+ #verifiedRow(target, context, receipt, clock) {
1911
+ const prior = target.status === "active" ? context.parent ?? context.current : context.current ?? context.parent;
1044
1912
  if (!prior)
1045
1913
  throw new Error(`entity ${target.uuid} has no local materialization boundary`);
1046
- const stats = tryLstat(context.targetPath);
1914
+ const stats = receipt.stats;
1047
1915
  if (target.status === "active" && !stats)
1048
- throw new Error(`entity ${target.uuid} vanished before Verify`);
1049
- const evidence = stats ?? prior;
1916
+ throw new Error(`entity ${target.uuid} has no retained Verify observation`);
1917
+ const fingerprint = stats ? sourceFingerprint(stats) : { physicalKey: prior.physicalKey,
1918
+ sourceSignature: prior.sourceSignature, filesystemMode: prior.filesystemMode };
1050
1919
  return {
1051
1920
  resourceId: prior.resourceId,
1052
1921
  rootUUID: prior.rootUUID,
@@ -1055,103 +1924,166 @@ export class EntityApplyHost {
1055
1924
  ? prior.relativePath
1056
1925
  : [context.parent?.relativePath, context.targetName].filter(Boolean).join("/"),
1057
1926
  absolutePath: context.targetPath,
1058
- deviceNumber: "dev" in evidence ? evidence.dev : evidence.deviceNumber,
1059
- inode: "ino" in evidence ? evidence.ino : evidence.inode,
1060
- byteSize: "size" in evidence ? evidence.size : evidence.byteSize,
1061
- modifiedTimeMs: "mtimeMs" in evidence ? evidence.mtimeMs : evidence.modifiedTimeMs,
1062
- changedTimeMs: "ctimeMs" in evidence ? evidence.ctimeMs : evidence.changedTimeMs,
1063
- filesystemMode: "mode" in evidence ? evidence.mode : evidence.filesystemMode,
1064
- contentVerifiedAtMs: ["file.text", "file.binary", "link", "reference"]
1065
- .includes(target.type) ? Date.now() : null,
1927
+ ...fingerprint,
1928
+ contentVerifiedNs: stats && ["file.text", "file.binary", "link", "reference"]
1929
+ .includes(target.type) ? leafBoundary(clock, stats) : null,
1066
1930
  };
1067
1931
  }
1068
- async #repositoryRows(intent, plan, context) {
1069
- if (plan.target.type !== "repo.git") {
1070
- throw new Error("repository Verify received a non-repository target");
1932
+ /** Address changes preserve each inventory's own portable values. Verifying
1933
+ * a container does not admit the contents of independent descendants. */
1934
+ #rebaseRows(rows, prior, target) {
1935
+ return rows.filter(row => row.uuid !== prior.uuid).map(row => ({
1936
+ ...row, record: portable(row), rootUUID: target.rootUUID, resourceId: target.resourceId,
1937
+ relativePath: target.relativePath + row.relativePath.slice(prior.relativePath.length),
1938
+ absolutePath: target.absolutePath + row.absolutePath.slice(prior.absolutePath.length),
1939
+ }));
1940
+ }
1941
+ #containerGround(target, context, root) {
1942
+ const ground = {
1943
+ observed: [root], shared: [root], removedObserved: [], removedShared: [],
1944
+ };
1945
+ for (const [inventory, output] of [["entities", "observed"], ["detection_notebook", "shared"]]) {
1946
+ const prior = inventory === "entities" ? context.current : this.#rowByUuid.get(target.uuid);
1947
+ if (!prior)
1948
+ continue;
1949
+ if (target.status !== "active") {
1950
+ if (inventory === "entities") {
1951
+ for (const row of groundStore(this.#database).rowsUnder(inventory, prior.rootUUID, prior.relativePath)) {
1952
+ ground.removedObserved.push(row.uuid);
1953
+ }
1954
+ }
1955
+ }
1956
+ else if (prior.absolutePath !== root.absolutePath
1957
+ && (prior.type === "folder" || prior.type === "workspace" || prior.type === "repo.git")) {
1958
+ const rebased = this.#rebaseRows(groundStore(this.#database)
1959
+ .rowsUnder(inventory, prior.rootUUID, prior.relativePath), prior, root);
1960
+ for (const row of rebased)
1961
+ ground[output].push(row);
1962
+ }
1071
1963
  }
1072
- const prepared = await this.#ensurePrepared(intent, plan);
1073
- const repository = prepared.repository;
1964
+ return ground;
1965
+ }
1966
+ async #repositoryRows(intent, plan, context, receipt, clock) {
1967
+ if (plan.target.type !== "repo.git")
1968
+ throw new Error("repository Verify received a non-repository target");
1969
+ const repository = (await this.#ensurePrepared(intent, plan)).repository;
1074
1970
  if (!repository)
1075
1971
  throw new Error("repository Verify has no transport identity");
1076
- const root = this.#verifiedRow(plan.target, context);
1077
- const existing = this.#rowsUnderRoot.all(root.rootUUID);
1078
- const repositoryPrefix = root.relativePath;
1079
- const identityByPath = new Map(repository.identity.map((entry) => [entry.path, entry]));
1080
- const nestedRepositoryPaths = repository.identity
1081
- .filter((entry) => entry.type === "repo.git")
1082
- .map((entry) => entry.path);
1083
- const localRepositoryPath = (row) => {
1084
- if (row.uuid === plan.target.uuid)
1085
- return "";
1086
- if (!repositoryPrefix)
1087
- return row.relativePath;
1088
- return row.relativePath.startsWith(`${repositoryPrefix}/`)
1089
- ? row.relativePath.slice(repositoryPrefix.length + 1)
1090
- : null;
1972
+ if (this.#repositoryBoundaries(context, repository.identity).length > 0) {
1973
+ throw new Error("repository projection cannot retire independently owned repository identities");
1974
+ }
1975
+ const allowance = this.#allowance(intent, repository.identity);
1976
+ if (!allowance)
1977
+ throw new Error("repository Verify has no accounted remainder");
1978
+ const proof = await this.#observe(intent, context.targetPath, repository.identity, allowance);
1979
+ if (!proof.identityMatches || proof.identityHash !== repositoryIdentityHash(repository.identity, sha256Hex)) {
1980
+ throw new Error("repository Verify physical members differ from accepted identity");
1981
+ }
1982
+ const root = this.#verifiedRow(plan.target, context, receipt, clock);
1983
+ // The O row Verify writes keeps the Git configuration stamp that routes
1984
+ // this repository's own Git notices to it alone.
1985
+ const pair = proof.pair(plan.target.uuid, context.targetPath);
1986
+ if (pair.physicalKey !== root.physicalKey || pair.filesystemMode !== root.filesystemMode) {
1987
+ throw new Error("repository Verify observed a different directory than its receipt");
1988
+ }
1989
+ const authorized = new Set([plan.target.uuid, ...repository.identity.map(row => row.uuid)]);
1990
+ // O is the complete observation: remainder still present keeps its
1991
+ // identity and fresh evidence; remainder now absent leaves O.
1992
+ const kept = new Map(proof.remainder.map(observed => [observed.entry.uuid, observed]));
1993
+ const previousRoot = this.#current(plan.target.uuid);
1994
+ const keptRows = [...kept.values()].map(({ entry, stats }) => {
1995
+ const row = this.#current(entry.uuid);
1996
+ if (!row || !previousRoot)
1997
+ throw new Error("repository remainder lost its observed row");
1998
+ const [placed] = this.#rebaseRows([row], previousRoot, root);
1999
+ // The observation already proved the remainder kept its recorded type.
2000
+ const type = placed.record.type;
2001
+ const fields = { ...placed.record, payloadVersion: type === "folder" ? null : entry.payloadVersion ?? null,
2002
+ fileMode: portableFileMode(type, Number(stats.mode)) };
2003
+ return { ...placed, record: { ...fields, version: canonicalVersion(fields, sha256Hex) }, ...sourceFingerprint(stats),
2004
+ contentVerifiedNs: type === "folder" ? placed.contentVerifiedNs : leafBoundary(clock, stats) };
2005
+ });
2006
+ const observedRoot = (record) => {
2007
+ if (proof.observedPayload === record.payloadVersion)
2008
+ return record;
2009
+ const fields = { ...record, payloadVersion: proof.observedPayload, transportVersion: null };
2010
+ return { ...fields, version: canonicalVersion(fields, sha256Hex) };
1091
2011
  };
1092
- const removals = existing.filter((row) => {
1093
- const path = localRepositoryPath(row);
1094
- if (path === null || path === "")
1095
- return false;
1096
- if (nestedRepositoryPaths.some((nested) => path.startsWith(`${nested}/`)))
1097
- return false;
1098
- return identityByPath.get(path)?.uuid !== row.uuid;
1099
- }).map((row) => row.uuid);
1100
- const removed = new Set(removals);
1101
- const base = existing.filter((row) => !removed.has(row.uuid)).map((row) => ({
1102
- record: row.uuid === plan.target.uuid ? plan.target : portable(row),
1103
- relativePath: row.uuid === plan.target.uuid ? root.relativePath : row.relativePath,
1104
- absolutePath: row.uuid === plan.target.uuid ? root.absolutePath : row.absolutePath,
1105
- deviceNumber: row.uuid === plan.target.uuid ? root.deviceNumber : row.deviceNumber,
1106
- inode: row.uuid === plan.target.uuid ? root.inode : row.inode,
1107
- byteSize: row.uuid === plan.target.uuid ? root.byteSize ?? 0 : row.byteSize ?? 0,
1108
- modifiedTimeMs: row.uuid === plan.target.uuid ? root.modifiedTimeMs ?? 0 : row.modifiedTimeMs ?? 0,
1109
- changedTimeMs: row.uuid === plan.target.uuid ? root.changedTimeMs ?? 0 : row.changedTimeMs ?? 0,
1110
- filesystemMode: row.uuid === plan.target.uuid ? root.filesystemMode ?? 0 : row.filesystemMode ?? 0,
1111
- contentVerifiedAtMs: row.uuid === plan.target.uuid
1112
- ? root.contentVerifiedAtMs : row.contentVerifiedAtMs,
1113
- }));
1114
- if (!base.some((row) => row.record.uuid === plan.target.uuid))
1115
- base.push({
1116
- record: root.record,
1117
- relativePath: root.relativePath,
1118
- absolutePath: root.absolutePath,
1119
- deviceNumber: root.deviceNumber,
1120
- inode: root.inode,
1121
- byteSize: root.byteSize ?? 0,
1122
- modifiedTimeMs: root.modifiedTimeMs ?? 0,
1123
- changedTimeMs: root.changedTimeMs ?? 0,
1124
- filesystemMode: root.filesystemMode ?? 0,
1125
- contentVerifiedAtMs: root.contentVerifiedAtMs,
1126
- });
1127
- const rows = projectMaterializedGraph(base, [{
1128
- record: plan.target,
1129
- identity: repository.identity,
1130
- }], sha256Hex, { existingRows: "prior-materialization" }).map((row) => ({
1131
- resourceId: root.resourceId,
1132
- rootUUID: root.rootUUID,
1133
- ...row,
1134
- }));
1135
- const projectedIdentity = indexRepositoryTerritory(rows.map((row) => ({
1136
- ...row,
1137
- uuid: row.record.uuid,
1138
- type: row.record.type,
1139
- parentUUID: row.record.parentUUID,
1140
- }))).ownedBy(plan.target.uuid)
1141
- .map((row) => ({
1142
- path: root.relativePath
1143
- ? row.relativePath.slice(root.relativePath.length + 1)
1144
- : row.relativePath,
1145
- uuid: row.record.uuid,
1146
- type: row.record.type,
2012
+ const nested = repository.identity.filter(entry => entry.type === "repo.git");
2013
+ const atPath = new Map([["", root.record.uuid], ...repository.identity.map(entry => [entry.path, entry.uuid])]);
2014
+ const materialized = (row) => ({ ...row, filesystemMode: row.filesystemMode ?? 0 });
2015
+ const inventory = (table) => {
2016
+ const get = (uuid) => (table === "entities" ? this.#entityByUuid : this.#rowByUuid).get(uuid);
2017
+ const previous = get(root.record.uuid);
2018
+ const existing = previous ? groundStore(this.#database).rowsUnder(table, previous.rootUUID, previous.relativePath) : [];
2019
+ const absent = indexRepositoryTerritory(existing).ownedBy(root.record.uuid)
2020
+ .filter(row => !authorized.has(row.uuid) && !(table === "entities" && kept.has(row.uuid)));
2021
+ // A complete repository result retires ordinary member placement. It
2022
+ // cannot erase a nested repository's independently accepted history or
2023
+ // any recoverable member history from the sharing inventory.
2024
+ const removed = table === "entities" ? absent.map(row => row.uuid) : [];
2025
+ const trashed = table === "detection_notebook" ? trashRemovedEntities(existing.map(portable), new Set(absent.filter(row => row.type !== "repo.git").map(row => row.uuid)), sha256Hex) : new Map();
2026
+ const retained = table === "detection_notebook" ? absent.map(row => ({
2027
+ ...row, record: trashed.get(row.uuid) ?? portable(row),
2028
+ })) : [];
2029
+ // Only the repository and its independent boundary spine are context.
2030
+ // Unrelated workspace rows never enter this verification output.
2031
+ const base = [materialized(root)];
2032
+ const descendants = [];
2033
+ for (const entry of nested) {
2034
+ const prior = get(entry.uuid);
2035
+ if (!prior || prior.type !== "repo.git")
2036
+ throw new Error(`repository boundary ${entry.uuid} has no ${table} state`);
2037
+ const parentPath = dirname(entry.path) === "." ? "" : dirname(entry.path).split(sep).join("/");
2038
+ const parentUUID = atPath.get(parentPath);
2039
+ if (!parentUUID)
2040
+ throw new Error("repository boundary has no accepted parent");
2041
+ const fields = { ...portable(prior), parentUUID, name: basename(entry.path) };
2042
+ const record = { ...fields, version: canonicalVersion(fields, sha256Hex) };
2043
+ const boundary = { ...prior, record, rootUUID: root.rootUUID, resourceId: root.resourceId,
2044
+ relativePath: [root.relativePath, entry.path].filter(Boolean).join("/"),
2045
+ absolutePath: join(root.absolutePath, ...entry.path.split("/")),
2046
+ };
2047
+ base.push(materialized(boundary));
2048
+ const rebased = this.#rebaseRows(groundStore(this.#database)
2049
+ .rowsUnder(table, prior.rootUUID, prior.relativePath), prior, boundary);
2050
+ for (const row of rebased)
2051
+ descendants.push(row);
2052
+ }
2053
+ const rows = projectMaterializedGraph(base, [{ record: plan.target, identity: repository.identity }], sha256Hex, { existingRows: "prior-materialization", contentBoundary: clock }).map(row => ({ ...row,
2054
+ resourceId: root.resourceId, rootUUID: root.rootUUID,
2055
+ ...(table === "entities" && row.record.uuid === root.record.uuid ? { record: observedRoot(row.record),
2056
+ gitConfigurationObservation: pair.gitConfigurationObservation, repositoryObservation: pair } : {}),
2057
+ }));
2058
+ return { rows: [...rows, ...descendants, ...retained, ...(table === "entities" ? keptRows : [])], removed };
2059
+ };
2060
+ const observed = inventory("entities"), shared = inventory("detection_notebook");
2061
+ const projectedIdentity = indexRepositoryTerritory(observed.rows.filter(row => !kept.has(row.record.uuid)).map(row => ({ ...row,
2062
+ uuid: row.record.uuid, type: row.record.type, parentUUID: row.record.parentUUID,
2063
+ }))).ownedBy(plan.target.uuid).map(row => ({
2064
+ path: root.relativePath ? row.relativePath.slice(root.relativePath.length + 1) : row.relativePath,
2065
+ uuid: row.record.uuid, type: row.record.type,
1147
2066
  payloadVersion: row.record.type === "repo.git" ? null : row.record.payloadVersion,
1148
- }))
1149
- .sort((left, right) => left.path.localeCompare(right.path));
1150
- if (repositoryIdentityHash(projectedIdentity, sha256Hex)
1151
- !== repositoryIdentityHash(repository.identity, sha256Hex)) {
2067
+ })).sort((left, right) => left.path.localeCompare(right.path));
2068
+ if (repositoryIdentityHash(projectedIdentity, sha256Hex) !== repositoryIdentityHash(repository.identity, sha256Hex)) {
1152
2069
  throw new Error("repository Apply projection does not reproduce its transport identity map");
1153
2070
  }
1154
- return { rows, removals };
2071
+ // #observe closed this observation; only local rows were read since.
2072
+ closePhysically(proof.proof);
2073
+ return { observed: observed.rows, shared: shared.rows,
2074
+ removedObserved: observed.removed, removedShared: shared.removed };
2075
+ }
2076
+ #repositoryBoundaries(context, identity) {
2077
+ const root = context.current ?? context.parent;
2078
+ if (!root)
2079
+ return [];
2080
+ const repositoryPath = context.current?.relativePath;
2081
+ if (repositoryPath === undefined)
2082
+ return [];
2083
+ const rows = this.#repositoriesUnderRoot.all(root.rootUUID);
2084
+ const prefix = repositoryPath ? `${repositoryPath}/` : "";
2085
+ return repositoryBoundaryDependencies(rows.filter((row) => row.uuid !== root.uuid && row.type === "repo.git" && row.status === "active"
2086
+ && row.relativePath.startsWith(prefix)).map((row) => ({ uuid: row.uuid, path: row.relativePath.slice(prefix.length) })), identity);
1155
2087
  }
1156
2088
  async #repositoryChain(artifact) {
1157
2089
  const reversed = [];
@@ -1165,42 +2097,47 @@ export class EntityApplyHost {
1165
2097
  if (local.contentHash !== version)
1166
2098
  throw new Error("repository Apply cache returned wrong transport");
1167
2099
  reversed.push({ file: local.file, transportVersion: version });
1168
- version = inspectRepositoryTransportFile(local.file).parentTransportVersion;
2100
+ version = (await inspectRepositoryTransportFile(local.file)).parentTransportVersion;
1169
2101
  }
1170
2102
  const chain = reversed.reverse();
1171
2103
  let identity = null;
1172
2104
  for (const sealed of chain) {
1173
- identity = applyRepositoryIdentity(identity, inspectRepositoryTransportFile(sealed.file), sha256Hex);
2105
+ identity = applyRepositoryIdentity(identity, await inspectRepositoryTransportFile(sealed.file), sha256Hex);
1174
2106
  }
1175
2107
  const result = identity ?? [];
1176
2108
  const latestFile = chain.at(-1)?.file;
1177
2109
  if (!latestFile)
1178
2110
  throw new Error("repository Apply transport chain is empty");
1179
- const latest = inspectRepositoryTransportFile(latestFile);
1180
- sealCachedRepositoryIdentity(this.#cacheDir, artifact.contentHash, result, latest.identityHash);
2111
+ const latest = await inspectRepositoryTransportFile(latestFile);
2112
+ await sealCachedRepositoryIdentity(this.#cacheDir, artifact.contentHash, { entries: result, identityHash: latest.identityHash }, latest.identityHash);
1181
2113
  return { chain, identity: result };
1182
2114
  }
1183
- #context(target) {
2115
+ /** The rows that place the target: its own and its parent's. A child waits
2116
+ * for its parent; a root is placed by its machine binding. */
2117
+ #anchors(target) {
1184
2118
  const current = this.#current(target.uuid);
1185
2119
  const parent = target.parentUUID ? this.#current(target.parentUUID) : null;
1186
2120
  if (target.parentUUID !== null && !parent) {
1187
2121
  throw new Error(`entity ${target.uuid} waits for materialized parent ${target.parentUUID}`);
1188
2122
  }
2123
+ if (!parent && !current)
2124
+ throw new Error(`root entity ${target.uuid} has no machine binding`);
2125
+ return { current, parent };
2126
+ }
2127
+ #context(target, intent) {
2128
+ const { current, parent } = this.#anchors(target);
1189
2129
  if (!parent) {
1190
- const targetPath = current?.absolutePath;
1191
- if (!targetPath)
1192
- throw new Error(`root entity ${target.uuid} has no machine binding`);
1193
2130
  return {
1194
2131
  current,
1195
2132
  parent,
1196
- targetPath,
2133
+ targetPath: current.absolutePath,
1197
2134
  targetName: target.name,
1198
2135
  beforeRelocations: [],
1199
2136
  afterRelocations: [],
1200
- currentShared: false,
1201
2137
  collisionEligible: false,
1202
2138
  };
1203
2139
  }
2140
+ const recovery = intent ? this.#boundRecovery(intent) : null;
1204
2141
  const finalByParent = new Map();
1205
2142
  const affectedParents = new Map();
1206
2143
  affectedParents.set(parent.uuid, parent);
@@ -1242,6 +2179,7 @@ export class EntityApplyHost {
1242
2179
  const parentRow = affectedParents.get(parentUUID);
1243
2180
  if (!parentRow)
1244
2181
  continue;
2182
+ const sources = this.#physicalSources(parentUUID);
1245
2183
  for (const row of siblings) {
1246
2184
  if (row.uuid === target.uuid)
1247
2185
  continue;
@@ -1250,18 +2188,23 @@ export class EntityApplyHost {
1250
2188
  && row.name === target.name)
1251
2189
  continue;
1252
2190
  const destination = join(parentRow.absolutePath, derivedByUuid.get(row.uuid) ?? row.name);
1253
- const source = destination === row.absolutePath && !tryLstat(destination)
1254
- ? this.#physicalSiblingPath(parentRow.absolutePath, row)
1255
- : row.absolutePath;
2191
+ let source = sources.get(row.uuid) ?? null;
2192
+ if (source === null)
2193
+ continue;
2194
+ // A sibling recorded at its destination is already there, unless this
2195
+ // attempt's recovery moved it away and the move's source still holds it.
2196
+ const move = source === destination ? recovery?.moves.find(move => move.uuid === row.uuid) : undefined;
2197
+ if (move && !tryLstat(destination)) {
2198
+ const original = tryLstat(move.source.path);
2199
+ source = original && physicalKey(original) === applyPreimageRoot(move.source).physical ? move.source.path : null;
2200
+ }
1256
2201
  if (source === null || source === destination)
1257
2202
  continue;
1258
2203
  const relocation = { row, source, destination };
1259
- const leavesOldSlot = current !== null
2204
+ const leavesOldSlot = target.status !== "active" || (current !== null
1260
2205
  && row.parentUUID === current.parentUUID
1261
2206
  && row.name === current.name
1262
- && (target.status !== "active"
1263
- || target.parentUUID !== current.parentUUID
1264
- || target.name !== current.name);
2207
+ && (target.parentUUID !== current.parentUUID || target.name !== current.name));
1265
2208
  (leavesOldSlot ? afterRelocations : beforeRelocations).push(relocation);
1266
2209
  }
1267
2210
  }
@@ -1272,9 +2215,6 @@ export class EntityApplyHost {
1272
2215
  targetName,
1273
2216
  beforeRelocations,
1274
2217
  afterRelocations,
1275
- currentShared: current !== null
1276
- && [...finalByParent.values()].flat()
1277
- .some((row) => row.uuid !== current.uuid && row.absolutePath === current.absolutePath),
1278
2218
  // A new identity can arrive concurrently in an already-held portable
1279
2219
  // slot. An existing identity renaming or moving into another identity's
1280
2220
  // slot is not concurrency; mutation-time law forbids it and Apply keeps
@@ -1282,33 +2222,39 @@ export class EntityApplyHost {
1282
2222
  collisionEligible,
1283
2223
  };
1284
2224
  }
1285
- /** Active children across accepted and Detect truth; the notebook row wins
1286
- * for a UUID exactly as #current does. */
2225
+ /** B determines admitted portable slots. O alone supplies any physical
2226
+ * source; a retained B address cannot claim a new occupant at that path. */
1287
2227
  #activeSiblings(parentUUID) {
1288
- const rows = new Map();
1289
- for (const row of this.#entitiesByParent.all(parentUUID)) {
1290
- rows.set(row.uuid, row);
1291
- }
1292
- for (const row of this.#rowsByParent.all(parentUUID)) {
1293
- rows.set(row.uuid, row);
1294
- }
1295
- return [...rows.values()].filter((row) => row.status === "active");
1296
- }
1297
- /** Rows are committed before a derived-address rename. After a crash, the
1298
- * row therefore names the destination while the same inode can still stand
1299
- * at the old sibling address. Recover that source without guessing by name. */
1300
- #physicalSiblingPath(parent, row) {
1301
- const matches = readdirSync(parent, { withFileTypes: true }).flatMap((entry) => {
1302
- const path = join(parent, entry.name);
1303
- const stats = tryLstat(path);
1304
- return stats && stats.dev === row.deviceNumber && stats.ino === row.inode ? [path] : [];
1305
- });
1306
- return matches.length === 1 ? matches[0] : null;
2228
+ return this.#rowsByParent.all(parentUUID)
2229
+ .filter(row => row.status === "active");
2230
+ }
2231
+ /** Where O holds each of those slots' identities, in one read. */
2232
+ #physicalSources(parentUUID) {
2233
+ return new Map(this.#physicalByParent.all(parentUUID)
2234
+ .map(({ uuid, absolutePath }) => [uuid, absolutePath]));
1307
2235
  }
1308
2236
  #current(entityId) {
1309
- return this.#rowByUuid.get(entityId)
1310
- ?? this.#entityByUuid.get(entityId)
1311
- ?? null;
2237
+ const row = this.#entityByUuid.get(entityId);
2238
+ return row?.status === "active" ? row : null;
2239
+ }
2240
+ /** Entity Apply cannot reconstruct a missing workspace one child at a time. */
2241
+ #unavailableRoot(target) {
2242
+ const anchors = [this.#current(target.uuid), target.parentUUID ? this.#current(target.parentUUID) : null];
2243
+ const checked = new Set();
2244
+ for (const anchor of anchors) {
2245
+ if (!anchor)
2246
+ continue;
2247
+ const root = this.#root(anchor);
2248
+ if (checked.has(root.uuid))
2249
+ continue;
2250
+ checked.add(root.uuid);
2251
+ const ground = tryLstat(root.absolutePath);
2252
+ if (ground && physicalKey(ground) === root.physicalKey)
2253
+ continue;
2254
+ return { kind: "dependency", reason: `Materialized root ${root.uuid} is missing or replaced; explicit root repair is required.`,
2255
+ waitFor: { producer: "capability", entityIds: [root.uuid] } };
2256
+ }
2257
+ return null;
1312
2258
  }
1313
2259
  #root(current) {
1314
2260
  const root = this.#current(current.rootUUID);
@@ -1317,31 +2263,50 @@ export class EntityApplyHost {
1317
2263
  return root;
1318
2264
  }
1319
2265
  #controlDirectory(intent, context) {
2266
+ const existing = this.store.recoveryLocation(intent);
2267
+ if (existing !== null)
2268
+ return existing;
1320
2269
  const anchor = context.current ?? context.parent;
1321
2270
  if (!anchor)
1322
2271
  throw new Error(`entity ${intent.entityId} has no Apply control root`);
1323
- return join(this.#root(anchor).absolutePath, ".amalgm", "apply", `${intent.entityId}-${intent.throughSequence}`);
2272
+ return applyControlDirectory(this.#root(anchor).absolutePath, intent);
1324
2273
  }
1325
2274
  #displacedPath(intent, context) {
1326
2275
  return join(this.#controlDirectory(intent, context), "displaced");
1327
2276
  }
1328
- /** Read one repository moment for this intent, or return the moment it
1329
- * already holds for the same address and nested-repository boundary. A
1330
- * supplied observation (the reveal proof) replaces whatever was held. */
1331
- #observe(intent, path, identity, proof) {
2277
+ /** Each decision reads a repository once and judges every record it
2278
+ * compares from that one closed reading. */
2279
+ async #observe(intent, path, identity, remainder) {
1332
2280
  const key = this.#key(intent);
1333
2281
  const held = this.#observations.get(key) ?? new Map();
1334
2282
  this.#observations.set(key, held);
1335
- const address = [path, ...nestedRepositoryPaths(identity)].join("\0");
1336
- const observation = proof ?? held.get(address) ?? observeRepository(path, identity);
1337
- held.set(address, observation);
1338
- return observation;
2283
+ const boundaries = nestedRepositoryPaths(identity);
2284
+ const address = readingAddress(path, boundaries);
2285
+ let reading = held.get(address);
2286
+ if (!reading) {
2287
+ reading = await readRepository(path, boundaries, "apply");
2288
+ held.set(address, reading);
2289
+ }
2290
+ return judgeRepository(reading, identity, remainder);
1339
2291
  }
1340
- #key(intent) {
1341
- return `${intent.entityId}:${intent.throughSequence}`;
2292
+ /** A decision trusts an inherited reading only after closing it once more.
2293
+ * One that no longer closes has passed; the decision reads anew. */
2294
+ async #closeInherited(intent) {
2295
+ const held = this.#observations.get(this.#key(intent));
2296
+ for (const [address, reading] of held ?? []) {
2297
+ try {
2298
+ await closeSource(reading.proof);
2299
+ }
2300
+ catch (error) {
2301
+ if (!(error instanceof CaptureSourceChangedError))
2302
+ throw error;
2303
+ held.delete(address);
2304
+ }
2305
+ }
1342
2306
  }
1343
- async #hashFile(file) {
1344
- return hashContentFile(file);
2307
+ #key(intent) {
2308
+ return JSON.stringify([intent.attemptId, intent.authorityId, intent.entityId, intent.fromSequence,
2309
+ intent.throughSequence, intent.targetMutationId, intent.targetState, intent.targetType, intent.createdAt]);
1345
2310
  }
1346
2311
  }
1347
2312
  //# sourceMappingURL=entity-apply-host.js.map