@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.
- package/PURPOSE.md +642 -128
- package/README.md +10 -8
- package/dist/accepted-tail.d.ts +26 -0
- package/dist/accepted-tail.js +37 -0
- package/dist/accepted-tail.js.map +1 -0
- package/dist/agent-execution.d.ts +16 -1
- package/dist/agent-execution.js +13 -5
- package/dist/agent-execution.js.map +1 -1
- package/dist/apply-cleanup.d.ts +28 -0
- package/dist/apply-cleanup.js +166 -0
- package/dist/apply-cleanup.js.map +1 -0
- package/dist/apply-recovery.d.ts +47 -0
- package/dist/apply-recovery.js +99 -0
- package/dist/apply-recovery.js.map +1 -0
- package/dist/apply-rows.d.ts +72 -0
- package/dist/apply-rows.js +150 -0
- package/dist/apply-rows.js.map +1 -0
- package/dist/apply-saves.d.ts +85 -0
- package/dist/apply-saves.js +238 -0
- package/dist/apply-saves.js.map +1 -0
- package/dist/boot.js +2 -1
- package/dist/boot.js.map +1 -1
- package/dist/capture-work.d.ts +8 -0
- package/dist/capture-work.js +67 -0
- package/dist/capture-work.js.map +1 -0
- package/dist/cargo-group.d.ts +45 -0
- package/dist/cargo-group.js +88 -0
- package/dist/cargo-group.js.map +1 -0
- package/dist/channel-session.d.ts +4 -2
- package/dist/channel-session.js +4 -3
- package/dist/channel-session.js.map +1 -1
- package/dist/chat-composition.d.ts +2 -0
- package/dist/chat-composition.js +1 -0
- package/dist/chat-composition.js.map +1 -1
- package/dist/cli.js +20 -1
- package/dist/cli.js.map +1 -1
- package/dist/construct-apply-host.d.ts +4 -2
- package/dist/construct-apply-host.js +8 -3
- package/dist/construct-apply-host.js.map +1 -1
- package/dist/construct-door.js +1 -1
- package/dist/construct-door.js.map +1 -1
- package/dist/construct-ledger.d.ts +6 -1
- package/dist/construct-ledger.js +21 -33
- package/dist/construct-ledger.js.map +1 -1
- package/dist/content-cache-collection.d.ts +18 -0
- package/dist/content-cache-collection.js +99 -0
- package/dist/content-cache-collection.js.map +1 -0
- package/dist/content-cache-host.d.ts +83 -17
- package/dist/content-cache-host.js +335 -95
- package/dist/content-cache-host.js.map +1 -1
- package/dist/content-cache-namespace.d.ts +59 -0
- package/dist/content-cache-namespace.js +210 -0
- package/dist/content-cache-namespace.js.map +1 -0
- package/dist/content-cache-ownership.d.ts +13 -0
- package/dist/content-cache-ownership.js +75 -0
- package/dist/content-cache-ownership.js.map +1 -0
- package/dist/content-cache-retention.d.ts +23 -0
- package/dist/content-cache-retention.js +195 -0
- package/dist/content-cache-retention.js.map +1 -0
- package/dist/content-capture-pool.d.ts +287 -0
- package/dist/content-capture-pool.js +461 -0
- package/dist/content-capture-pool.js.map +1 -0
- package/dist/content-capture-worker.d.ts +8 -0
- package/dist/content-capture-worker.js +109 -0
- package/dist/content-capture-worker.js.map +1 -0
- package/dist/content-clock.d.ts +42 -0
- package/dist/content-clock.js +166 -0
- package/dist/content-clock.js.map +1 -0
- package/dist/content-direct.d.ts +2 -0
- package/dist/content-direct.js +12 -0
- package/dist/content-direct.js.map +1 -1
- package/dist/content-failure.d.ts +18 -0
- package/dist/content-failure.js +38 -0
- package/dist/content-failure.js.map +1 -0
- package/dist/content-stream-thread.d.ts +60 -0
- package/dist/content-stream-thread.js +95 -0
- package/dist/content-stream-thread.js.map +1 -0
- package/dist/content-stream-worker.d.ts +7 -0
- package/dist/content-stream-worker.js +52 -0
- package/dist/content-stream-worker.js.map +1 -0
- package/dist/declaration-absorption.d.ts +37 -0
- package/dist/declaration-absorption.js +119 -0
- package/dist/declaration-absorption.js.map +1 -0
- package/dist/declaration-chain.d.ts +36 -0
- package/dist/declaration-chain.js +96 -0
- package/dist/declaration-chain.js.map +1 -0
- package/dist/declaration-commit.d.ts +31 -0
- package/dist/declaration-commit.js +49 -0
- package/dist/declaration-commit.js.map +1 -0
- package/dist/declaration-hold.d.ts +22 -0
- package/dist/declaration-hold.js +24 -0
- package/dist/declaration-hold.js.map +1 -0
- package/dist/declaration-journal.d.ts +62 -0
- package/dist/declaration-journal.js +99 -0
- package/dist/declaration-journal.js.map +1 -0
- package/dist/declaration-save.d.ts +31 -0
- package/dist/declaration-save.js +134 -0
- package/dist/declaration-save.js.map +1 -0
- package/dist/detection/accepted-result.d.ts +19 -1
- package/dist/detection/accepted-result.js +36 -5
- package/dist/detection/accepted-result.js.map +1 -1
- package/dist/detection/admission-save.d.ts +55 -0
- package/dist/detection/admission-save.js +48 -0
- package/dist/detection/admission-save.js.map +1 -0
- package/dist/detection/admission-units.d.ts +66 -0
- package/dist/detection/admission-units.js +322 -0
- package/dist/detection/admission-units.js.map +1 -0
- package/dist/detection/alias-discovery.d.ts +12 -0
- package/dist/detection/alias-discovery.js +80 -0
- package/dist/detection/alias-discovery.js.map +1 -0
- package/dist/detection/alias-examination.d.ts +30 -0
- package/dist/detection/alias-examination.js +49 -0
- package/dist/detection/alias-examination.js.map +1 -0
- package/dist/detection/apply-ownership.d.ts +81 -0
- package/dist/detection/apply-ownership.js +232 -0
- package/dist/detection/apply-ownership.js.map +1 -0
- package/dist/detection/basis-advanced.d.ts +11 -0
- package/dist/detection/basis-advanced.js +26 -0
- package/dist/detection/basis-advanced.js.map +1 -0
- package/dist/detection/capture-outcome.d.ts +16 -0
- package/dist/detection/capture-outcome.js +92 -0
- package/dist/detection/capture-outcome.js.map +1 -0
- package/dist/detection/container-membership.d.ts +32 -0
- package/dist/detection/container-membership.js +49 -0
- package/dist/detection/container-membership.js.map +1 -0
- package/dist/detection/detect-cancelled.d.ts +6 -0
- package/dist/detection/detect-cancelled.js +10 -0
- package/dist/detection/detect-cancelled.js.map +1 -0
- package/dist/detection/directory-proof.d.ts +47 -0
- package/dist/detection/directory-proof.js +77 -0
- package/dist/detection/directory-proof.js.map +1 -0
- package/dist/detection/disposition-rows.d.ts +18 -0
- package/dist/detection/disposition-rows.js +29 -0
- package/dist/detection/disposition-rows.js.map +1 -0
- package/dist/detection/dispositions.d.ts +130 -0
- package/dist/detection/dispositions.js +248 -0
- package/dist/detection/dispositions.js.map +1 -0
- package/dist/detection/git-notice.d.ts +88 -0
- package/dist/detection/git-notice.js +444 -0
- package/dist/detection/git-notice.js.map +1 -0
- package/dist/detection/ground-proof.d.ts +130 -0
- package/dist/detection/ground-proof.js +274 -0
- package/dist/detection/ground-proof.js.map +1 -0
- package/dist/detection/ground-reader.d.ts +29 -0
- package/dist/detection/ground-reader.js +102 -0
- package/dist/detection/ground-reader.js.map +1 -0
- package/dist/detection/held-territory.d.ts +51 -0
- package/dist/detection/held-territory.js +205 -0
- package/dist/detection/held-territory.js.map +1 -0
- package/dist/detection/identity-discovery.d.ts +45 -0
- package/dist/detection/identity-discovery.js +287 -0
- package/dist/detection/identity-discovery.js.map +1 -0
- package/dist/detection/maintenance-sweep.d.ts +108 -0
- package/dist/detection/maintenance-sweep.js +325 -0
- package/dist/detection/maintenance-sweep.js.map +1 -0
- package/dist/detection/name-reach.d.ts +12 -0
- package/dist/detection/name-reach.js +28 -0
- package/dist/detection/name-reach.js.map +1 -0
- package/dist/detection/notebook-basis.d.ts +5 -7
- package/dist/detection/notebook-basis.js +6 -7
- package/dist/detection/notebook-basis.js.map +1 -1
- package/dist/detection/owed-selection.d.ts +51 -0
- package/dist/detection/owed-selection.js +169 -0
- package/dist/detection/owed-selection.js.map +1 -0
- package/dist/detection/portable.d.ts +34 -34
- package/dist/detection/portable.js +67 -93
- package/dist/detection/portable.js.map +1 -1
- package/dist/detection/reconciliation-coverage.d.ts +7 -0
- package/dist/detection/reconciliation-coverage.js +16 -0
- package/dist/detection/reconciliation-coverage.js.map +1 -0
- package/dist/detection/repository-publication.d.ts +20 -0
- package/dist/detection/repository-publication.js +79 -0
- package/dist/detection/repository-publication.js.map +1 -0
- package/dist/detection/repository-routes.d.ts +43 -0
- package/dist/detection/repository-routes.js +97 -0
- package/dist/detection/repository-routes.js.map +1 -0
- package/dist/detection/root-examination.d.ts +94 -0
- package/dist/detection/root-examination.js +523 -0
- package/dist/detection/root-examination.js.map +1 -0
- package/dist/detection/runtime.d.ts +131 -64
- package/dist/detection/runtime.js +808 -1103
- package/dist/detection/runtime.js.map +1 -1
- package/dist/detection/scoped-identity-port.d.ts +34 -0
- package/dist/detection/scoped-identity-port.js +81 -0
- package/dist/detection/scoped-identity-port.js.map +1 -0
- package/dist/detection/seal-law.d.ts +66 -0
- package/dist/detection/seal-law.js +116 -0
- package/dist/detection/seal-law.js.map +1 -0
- package/dist/detection/sharing-policy.d.ts +11 -0
- package/dist/detection/sharing-policy.js +32 -0
- package/dist/detection/sharing-policy.js.map +1 -0
- package/dist/detection/thread-database.d.ts +4 -0
- package/dist/detection/thread-database.js +30 -0
- package/dist/detection/thread-database.js.map +1 -0
- package/dist/detection/unreadable.d.ts +11 -0
- package/dist/detection/unreadable.js +9 -0
- package/dist/detection/unreadable.js.map +1 -0
- package/dist/detection/verified-content.d.ts +6 -0
- package/dist/detection/verified-content.js +12 -0
- package/dist/detection/verified-content.js.map +1 -0
- package/dist/detection/walk-law.d.ts +34 -0
- package/dist/detection/walk-law.js +55 -0
- package/dist/detection/walk-law.js.map +1 -0
- package/dist/development/fly-guest.js +2 -1
- package/dist/development/fly-guest.js.map +1 -1
- package/dist/entity-apply-host.d.ts +29 -13
- package/dist/entity-apply-host.js +1426 -461
- package/dist/entity-apply-host.js.map +1 -1
- package/dist/entity-apply-store.d.ts +77 -30
- package/dist/entity-apply-store.js +221 -516
- package/dist/entity-apply-store.js.map +1 -1
- package/dist/entity-record-observability.js +2 -0
- package/dist/entity-record-observability.js.map +1 -1
- package/dist/entity-record-saves.d.ts +20 -0
- package/dist/entity-record-saves.js +91 -0
- package/dist/entity-record-saves.js.map +1 -0
- package/dist/entity-record-store.d.ts +15 -7
- package/dist/entity-record-store.js +57 -117
- package/dist/entity-record-store.js.map +1 -1
- package/dist/files-register-host.d.ts +22 -3
- package/dist/files-register-host.js +70 -31
- package/dist/files-register-host.js.map +1 -1
- package/dist/filesystem.js +3 -2
- package/dist/filesystem.js.map +1 -1
- package/dist/git-binding.d.ts +65 -0
- package/dist/git-binding.js +220 -0
- package/dist/git-binding.js.map +1 -0
- package/dist/git-capture.d.ts +39 -0
- package/dist/git-capture.js +86 -0
- package/dist/git-capture.js.map +1 -0
- package/dist/git-card.d.ts +12 -0
- package/dist/git-card.js +123 -0
- package/dist/git-card.js.map +1 -0
- package/dist/git-checkpoint-construction.d.ts +41 -0
- package/dist/git-checkpoint-construction.js +96 -0
- package/dist/git-checkpoint-construction.js.map +1 -0
- package/dist/git-checkpoint-preparation.d.ts +37 -0
- package/dist/git-checkpoint-preparation.js +145 -0
- package/dist/git-checkpoint-preparation.js.map +1 -0
- package/dist/git-command.d.ts +61 -8
- package/dist/git-command.js +217 -63
- package/dist/git-command.js.map +1 -1
- package/dist/git-configuration.d.ts +40 -0
- package/dist/git-configuration.js +217 -0
- package/dist/git-configuration.js.map +1 -0
- package/dist/git-evidence.d.ts +61 -0
- package/dist/git-evidence.js +181 -0
- package/dist/git-evidence.js.map +1 -0
- package/dist/git-index-entries.d.ts +15 -0
- package/dist/git-index-entries.js +44 -0
- package/dist/git-index-entries.js.map +1 -0
- package/dist/git-index-format.d.ts +35 -0
- package/dist/git-index-format.js +293 -0
- package/dist/git-index-format.js.map +1 -0
- package/dist/git-index-witness.d.ts +33 -0
- package/dist/git-index-witness.js +69 -0
- package/dist/git-index-witness.js.map +1 -0
- package/dist/git-literal-host.d.ts +82 -0
- package/dist/git-literal-host.js +172 -0
- package/dist/git-literal-host.js.map +1 -0
- package/dist/git-literal-read.d.ts +88 -0
- package/dist/git-literal-read.js +170 -0
- package/dist/git-literal-read.js.map +1 -0
- package/dist/git-literal-replay.d.ts +33 -0
- package/dist/git-literal-replay.js +268 -0
- package/dist/git-literal-replay.js.map +1 -0
- package/dist/git-native-file.d.ts +42 -0
- package/dist/git-native-file.js +145 -0
- package/dist/git-native-file.js.map +1 -0
- package/dist/git-native-index.d.ts +21 -0
- package/dist/git-native-index.js +44 -0
- package/dist/git-native-index.js.map +1 -0
- package/dist/git-operation-host.d.ts +48 -0
- package/dist/git-operation-host.js +127 -0
- package/dist/git-operation-host.js.map +1 -0
- package/dist/git-private-index.d.ts +11 -0
- package/dist/git-private-index.js +42 -0
- package/dist/git-private-index.js.map +1 -0
- package/dist/git-private-repository.d.ts +4 -0
- package/dist/git-private-repository.js +69 -0
- package/dist/git-private-repository.js.map +1 -0
- package/dist/git-private-view.d.ts +39 -0
- package/dist/git-private-view.js +162 -0
- package/dist/git-private-view.js.map +1 -0
- package/dist/git-ref-store.d.ts +12 -0
- package/dist/git-ref-store.js +68 -0
- package/dist/git-ref-store.js.map +1 -0
- package/dist/git-registration-host.d.ts +20 -30
- package/dist/git-registration-host.js +86 -249
- package/dist/git-registration-host.js.map +1 -1
- package/dist/git-repository-host.d.ts +148 -22
- package/dist/git-repository-host.js +633 -650
- package/dist/git-repository-host.js.map +1 -1
- package/dist/git-worktree-host.d.ts +2 -0
- package/dist/git-worktree-host.js +16 -0
- package/dist/git-worktree-host.js.map +1 -0
- package/dist/graph-publication.d.ts +75 -0
- package/dist/graph-publication.js +203 -0
- package/dist/graph-publication.js.map +1 -0
- package/dist/ground-basis.d.ts +242 -0
- package/dist/ground-basis.js +1023 -0
- package/dist/ground-basis.js.map +1 -0
- package/dist/ground-checkpoint-worker.d.ts +1 -0
- package/dist/ground-checkpoint-worker.js +84 -0
- package/dist/ground-checkpoint-worker.js.map +1 -0
- package/dist/ground-checkpoints.d.ts +16 -0
- package/dist/ground-checkpoints.js +45 -0
- package/dist/ground-checkpoints.js.map +1 -0
- package/dist/ground-coordination.d.ts +107 -3
- package/dist/ground-coordination.js +249 -24
- package/dist/ground-coordination.js.map +1 -1
- package/dist/ground-database.d.ts +42 -0
- package/dist/ground-database.js +290 -0
- package/dist/ground-database.js.map +1 -0
- package/dist/ground-differences.d.ts +23 -0
- package/dist/ground-differences.js +47 -0
- package/dist/ground-differences.js.map +1 -0
- package/dist/ground-holders.d.ts +4 -0
- package/dist/ground-holders.js +15 -0
- package/dist/ground-holders.js.map +1 -0
- package/dist/ground-initialization-worker.d.ts +1 -0
- package/dist/ground-initialization-worker.js +20 -0
- package/dist/ground-initialization-worker.js.map +1 -0
- package/dist/ground-initialization.d.ts +35 -0
- package/dist/ground-initialization.js +102 -0
- package/dist/ground-initialization.js.map +1 -0
- package/dist/ground-install-saves.d.ts +63 -0
- package/dist/ground-install-saves.js +139 -0
- package/dist/ground-install-saves.js.map +1 -0
- package/dist/ground-inventory-schema.d.ts +18 -0
- package/dist/ground-inventory-schema.js +168 -0
- package/dist/ground-inventory-schema.js.map +1 -0
- package/dist/ground-profile.d.ts +7 -0
- package/dist/ground-profile.js +52 -0
- package/dist/ground-profile.js.map +1 -0
- package/dist/ground-root-index.d.ts +2 -1
- package/dist/ground-root-index.js +16 -14
- package/dist/ground-root-index.js.map +1 -1
- package/dist/ground-row.d.ts +62 -0
- package/dist/ground-row.js +70 -0
- package/dist/ground-row.js.map +1 -0
- package/dist/ground-save-ops.d.ts +44 -0
- package/dist/ground-save-ops.js +41 -0
- package/dist/ground-save-ops.js.map +1 -0
- package/dist/ground-saver-worker.d.ts +1 -0
- package/dist/ground-saver-worker.js +156 -0
- package/dist/ground-saver-worker.js.map +1 -0
- package/dist/ground-saver.d.ts +75 -0
- package/dist/ground-saver.js +216 -0
- package/dist/ground-saver.js.map +1 -0
- package/dist/ground-store.d.ts +84 -0
- package/dist/ground-store.js +473 -0
- package/dist/ground-store.js.map +1 -0
- package/dist/ground-territory.d.ts +130 -0
- package/dist/ground-territory.js +511 -0
- package/dist/ground-territory.js.map +1 -0
- package/dist/index.d.ts +1 -1
- package/dist/index.js.map +1 -1
- package/dist/installation/ownership.d.ts +0 -2
- package/dist/installation/ownership.js +3 -15
- package/dist/installation/ownership.js.map +1 -1
- package/dist/machine-host.d.ts +3 -2
- package/dist/machine-host.js +28 -16
- package/dist/machine-host.js.map +1 -1
- package/dist/materialized-graph.d.ts +7 -7
- package/dist/materialized-graph.js +11 -23
- package/dist/materialized-graph.js.map +1 -1
- package/dist/module-thread.d.ts +9 -0
- package/dist/module-thread.js +10 -0
- package/dist/module-thread.js.map +1 -0
- package/dist/native/fsevents-stream.node +0 -0
- package/dist/physical-path-witness.d.ts +64 -0
- package/dist/physical-path-witness.js +168 -0
- package/dist/physical-path-witness.js.map +1 -0
- package/dist/register-upload-fusion.d.ts +5 -4
- package/dist/register-upload-fusion.js +9 -6
- package/dist/register-upload-fusion.js.map +1 -1
- package/dist/registry-graph.d.ts +3 -2
- package/dist/registry-graph.js +9 -6
- package/dist/registry-graph.js.map +1 -1
- package/dist/registry-page-cache.d.ts +5 -2
- package/dist/registry-page-cache.js.map +1 -1
- package/dist/repository-identity-steps.d.ts +5 -0
- package/dist/repository-identity-steps.js +33 -0
- package/dist/repository-identity-steps.js.map +1 -0
- package/dist/runtime-node-flags.d.ts +10 -0
- package/dist/runtime-node-flags.js +11 -0
- package/dist/runtime-node-flags.js.map +1 -0
- package/dist/service/adapter.d.ts +1 -1
- package/dist/service/adapter.js +1 -1
- package/dist/service/adapter.js.map +1 -1
- package/dist/service/cli.js +16 -20
- package/dist/service/cli.js.map +1 -1
- package/dist/service/control.d.ts +2 -2
- package/dist/service/keepalive.d.ts +4 -11
- package/dist/service/keepalive.js +65 -118
- package/dist/service/keepalive.js.map +1 -1
- package/dist/service/policy.d.ts +36 -0
- package/dist/service/policy.js +25 -0
- package/dist/service/policy.js.map +1 -0
- package/dist/service/portable.d.ts +1 -1
- package/dist/service/portable.js +1 -1
- package/dist/service/portable.js.map +1 -1
- package/dist/service/recovery.d.ts +10 -9
- package/dist/service/recovery.js +29 -19
- package/dist/service/recovery.js.map +1 -1
- package/dist/service/windows-action.d.ts +5 -3
- package/dist/service/windows-action.js +6 -9
- package/dist/service/windows-action.js.map +1 -1
- package/dist/service/windows-task.js +5 -5
- package/dist/service/windows-task.js.map +1 -1
- package/dist/sharing-admission.d.ts +17 -0
- package/dist/sharing-admission.js +128 -0
- package/dist/sharing-admission.js.map +1 -0
- package/dist/snapshot-delivery-baseline.d.ts +12 -3
- package/dist/snapshot-delivery-baseline.js +41 -42
- package/dist/snapshot-delivery-baseline.js.map +1 -1
- package/dist/source-proof.d.ts +36 -0
- package/dist/source-proof.js +70 -0
- package/dist/source-proof.js.map +1 -0
- package/dist/source-signature.d.ts +56 -0
- package/dist/source-signature.js +56 -0
- package/dist/source-signature.js.map +1 -0
- package/dist/source-witness.d.ts +15 -0
- package/dist/source-witness.js +29 -0
- package/dist/source-witness.js.map +1 -0
- package/dist/sqlite-durability.d.ts +31 -0
- package/dist/sqlite-durability.js +28 -0
- package/dist/sqlite-durability.js.map +1 -0
- package/dist/sqlite.d.ts +8 -2
- package/dist/sqlite.js +27 -4
- package/dist/sqlite.js.map +1 -1
- package/dist/sync-address.d.ts +19 -0
- package/dist/sync-address.js +52 -0
- package/dist/sync-address.js.map +1 -0
- package/dist/sync-setting.d.ts +15 -0
- package/dist/sync-setting.js +13 -0
- package/dist/sync-setting.js.map +1 -1
- package/dist/turns.d.ts +42 -0
- package/dist/turns.js +112 -0
- package/dist/turns.js.map +1 -0
- package/dist/updates/bootstrap.d.ts +53 -7
- package/dist/updates/bootstrap.js +224 -137
- package/dist/updates/bootstrap.js.map +1 -1
- package/dist/updates/controller.d.ts +8 -0
- package/dist/updates/controller.js +27 -3
- package/dist/updates/controller.js.map +1 -1
- package/dist/updates/store.d.ts +3 -0
- package/dist/updates/store.js +12 -0
- package/dist/updates/store.js.map +1 -1
- package/dist/user-ground-host.d.ts +364 -69
- package/dist/user-ground-host.js +5231 -3366
- package/dist/user-ground-host.js.map +1 -1
- package/dist/watching/fsevents.d.ts +52 -1
- package/dist/watching/fsevents.js +60 -25
- package/dist/watching/fsevents.js.map +1 -1
- package/dist/watching/host.d.ts +129 -5
- package/dist/watching/host.js +433 -80
- package/dist/watching/host.js.map +1 -1
- package/dist/watching/index.d.ts +1 -1
- package/dist/wire-binary.d.ts +3 -2
- package/dist/wire-binary.js +10 -8
- package/dist/wire-binary.js.map +1 -1
- package/dist/wire-client.d.ts +3 -4
- package/dist/wire-client.js +18 -44
- package/dist/wire-client.js.map +1 -1
- package/dist/wire-outbox.d.ts +34 -0
- package/dist/wire-outbox.js +127 -0
- package/dist/wire-outbox.js.map +1 -0
- package/dist/workspace-host.d.ts +4 -0
- package/dist/workspace-host.js +12 -1
- package/dist/workspace-host.js.map +1 -1
- package/examinations/fixtures/private-wire-authority.js +133 -10
- package/npm-shrinkwrap.json +43 -56
- 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,
|
|
6
|
-
import {
|
|
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 {
|
|
9
|
-
import {
|
|
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.
|
|
43
|
+
return plan.kind === "structure" ? plan.resultArtifact : null;
|
|
24
44
|
};
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
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
|
-
#
|
|
144
|
-
#
|
|
145
|
+
#pendingOutbox;
|
|
146
|
+
#repositoriesUnderRoot;
|
|
145
147
|
#rowsByParent;
|
|
146
|
-
#
|
|
147
|
-
#
|
|
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
|
|
155
|
-
*
|
|
156
|
-
*
|
|
157
|
-
*
|
|
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
|
|
161
|
-
this
|
|
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) =>
|
|
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.#
|
|
180
|
-
this.#
|
|
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.#
|
|
183
|
-
|
|
184
|
-
this.#
|
|
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
|
-
/**
|
|
213
|
-
*
|
|
214
|
-
|
|
215
|
-
|
|
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
|
-
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
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
|
-
|
|
252
|
-
|
|
253
|
-
|
|
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
|
-
|
|
256
|
-
|
|
257
|
-
|
|
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
|
|
262
|
-
|
|
263
|
-
|
|
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
|
-
|
|
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.
|
|
344
|
+
else if (plan.kind === "structure" && plan.resultArtifact) {
|
|
283
345
|
prepared = {
|
|
284
|
-
artifact: await this.#sealedArtifact(plan.
|
|
285
|
-
?? await this.#readContent(plan.
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
414
|
-
|
|
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
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
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
|
|
467
|
-
//
|
|
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
|
-
|
|
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
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
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
|
-
|
|
498
|
-
|
|
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.#
|
|
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
|
-
|
|
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.#
|
|
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
|
|
554
|
-
|
|
555
|
-
|
|
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
|
-
&&
|
|
563
|
-
this.#
|
|
747
|
+
&& tryLstat(context.current.absolutePath)) {
|
|
748
|
+
this.#moveToVacantAddress(context.current.absolutePath, context.targetPath);
|
|
564
749
|
}
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
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
|
-
|
|
570
|
-
|
|
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
|
-
|
|
769
|
+
this.#installArtifact(intent, context, plan.target, artifact);
|
|
574
770
|
}
|
|
575
771
|
}
|
|
576
|
-
|
|
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.#
|
|
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
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
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
|
-
|
|
704
|
-
|
|
705
|
-
|
|
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
|
|
737
|
-
if (!
|
|
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
|
-
|
|
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
|
|
761
|
-
*
|
|
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
|
|
777
|
-
|
|
778
|
-
|
|
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
|
-
|
|
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
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
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
|
-
|
|
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
|
|
909
|
-
|
|
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(
|
|
913
|
-
|
|
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
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
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
|
-
#
|
|
1809
|
+
#moveToVacantAddress(source, destination) {
|
|
968
1810
|
ensureUserDir(dirname(destination));
|
|
969
1811
|
if (tryLstat(destination)) {
|
|
970
|
-
|
|
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
|
|
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
|
-
|
|
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 (
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
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(
|
|
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:
|
|
1888
|
+
stateId: layout.stateId,
|
|
1027
1889
|
transportVersion: record.transportVersion,
|
|
1028
1890
|
card: layout.card,
|
|
1029
1891
|
identityHash: layout.identityHash,
|
|
1030
1892
|
};
|
|
1031
|
-
|
|
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
|
|
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 =
|
|
1914
|
+
const stats = receipt.stats;
|
|
1047
1915
|
if (target.status === "active" && !stats)
|
|
1048
|
-
throw new Error(`entity ${target.uuid}
|
|
1049
|
-
const
|
|
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
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
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
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
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
|
-
|
|
1073
|
-
|
|
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
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
const
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
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
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
...row,
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1254
|
-
|
|
1255
|
-
|
|
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.
|
|
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
|
-
/**
|
|
1286
|
-
*
|
|
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
|
-
|
|
1289
|
-
|
|
1290
|
-
|
|
1291
|
-
|
|
1292
|
-
|
|
1293
|
-
|
|
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
|
-
|
|
1310
|
-
|
|
1311
|
-
|
|
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
|
|
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
|
-
/**
|
|
1329
|
-
*
|
|
1330
|
-
|
|
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
|
|
1336
|
-
const
|
|
1337
|
-
held.
|
|
1338
|
-
|
|
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
|
-
|
|
1341
|
-
|
|
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
|
-
|
|
1344
|
-
return
|
|
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
|