@smthrs/engine-store 0.0.0-stage → 1.0.0-rc.3
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/CHANGELOG.md +247 -0
- package/LICENSE +21 -0
- package/README.md +146 -2
- package/dist/cjs/ArtifactGc.d.ts +235 -0
- package/dist/cjs/ArtifactGc.d.ts.map +1 -0
- package/dist/cjs/ArtifactGc.js +183 -0
- package/dist/cjs/ArtifactGc.js.map +7 -0
- package/dist/cjs/ArtifactSync.d.ts +164 -0
- package/dist/cjs/ArtifactSync.d.ts.map +1 -0
- package/dist/cjs/ArtifactSync.js +144 -0
- package/dist/cjs/ArtifactSync.js.map +7 -0
- package/dist/cjs/CacheSync.d.ts +112 -0
- package/dist/cjs/CacheSync.d.ts.map +1 -0
- package/dist/cjs/CacheSync.js +64 -0
- package/dist/cjs/CacheSync.js.map +7 -0
- package/dist/cjs/DeferredClockFold.d.ts +354 -0
- package/dist/cjs/DeferredClockFold.d.ts.map +1 -0
- package/dist/cjs/DeferredClockFold.js +335 -0
- package/dist/cjs/DeferredClockFold.js.map +7 -0
- package/dist/cjs/DisasterRecovery.d.ts +431 -0
- package/dist/cjs/DisasterRecovery.d.ts.map +1 -0
- package/dist/cjs/DisasterRecovery.js +453 -0
- package/dist/cjs/DisasterRecovery.js.map +7 -0
- package/dist/cjs/DurableEngineState.d.ts +776 -0
- package/dist/cjs/DurableEngineState.d.ts.map +1 -0
- package/dist/cjs/DurableEngineState.js +1536 -0
- package/dist/cjs/DurableEngineState.js.map +7 -0
- package/dist/cjs/EngineStore.d.ts +158 -0
- package/dist/cjs/EngineStore.d.ts.map +1 -0
- package/dist/cjs/EngineStore.js +398 -0
- package/dist/cjs/EngineStore.js.map +7 -0
- package/dist/cjs/EngineStoreMetrics.d.ts +276 -0
- package/dist/cjs/EngineStoreMetrics.d.ts.map +1 -0
- package/dist/cjs/EngineStoreMetrics.js +135 -0
- package/dist/cjs/EngineStoreMetrics.js.map +7 -0
- package/dist/cjs/Errors.d.ts +22 -0
- package/dist/cjs/Errors.d.ts.map +1 -0
- package/dist/cjs/Errors.js +39 -0
- package/dist/cjs/Errors.js.map +7 -0
- package/dist/cjs/EventTypes.d.ts +27 -0
- package/dist/cjs/EventTypes.d.ts.map +1 -0
- package/dist/cjs/EventTypes.js +37 -0
- package/dist/cjs/EventTypes.js.map +7 -0
- package/dist/cjs/ExecutionFacts.d.ts +59 -0
- package/dist/cjs/ExecutionFacts.d.ts.map +1 -0
- package/dist/cjs/ExecutionFacts.js +123 -0
- package/dist/cjs/ExecutionFacts.js.map +7 -0
- package/dist/cjs/ExecutionSnapshot.d.ts +117 -0
- package/dist/cjs/ExecutionSnapshot.d.ts.map +1 -0
- package/dist/cjs/ExecutionSnapshot.js +111 -0
- package/dist/cjs/ExecutionSnapshot.js.map +7 -0
- package/dist/cjs/Inconsistency.d.ts +154 -0
- package/dist/cjs/Inconsistency.d.ts.map +1 -0
- package/dist/cjs/Inconsistency.js +135 -0
- package/dist/cjs/Inconsistency.js.map +7 -0
- package/dist/cjs/Migrations.d.ts +56 -0
- package/dist/cjs/Migrations.d.ts.map +1 -0
- package/dist/cjs/Migrations.js +78 -0
- package/dist/cjs/Migrations.js.map +7 -0
- package/dist/cjs/OwnerIdentity.d.ts +91 -0
- package/dist/cjs/OwnerIdentity.d.ts.map +1 -0
- package/dist/cjs/OwnerIdentity.js +65 -0
- package/dist/cjs/OwnerIdentity.js.map +7 -0
- package/dist/cjs/PlanInputStore.d.ts +110 -0
- package/dist/cjs/PlanInputStore.d.ts.map +1 -0
- package/dist/cjs/PlanInputStore.js +224 -0
- package/dist/cjs/PlanInputStore.js.map +7 -0
- package/dist/cjs/PlanMergeStore.d.ts +113 -0
- package/dist/cjs/PlanMergeStore.d.ts.map +1 -0
- package/dist/cjs/PlanMergeStore.js +255 -0
- package/dist/cjs/PlanMergeStore.js.map +7 -0
- package/dist/cjs/PlanScheduler.d.ts +363 -0
- package/dist/cjs/PlanScheduler.d.ts.map +1 -0
- package/dist/cjs/PlanScheduler.js +1090 -0
- package/dist/cjs/PlanScheduler.js.map +7 -0
- package/dist/cjs/Reconciliation.d.ts +175 -0
- package/dist/cjs/Reconciliation.d.ts.map +1 -0
- package/dist/cjs/Reconciliation.js +119 -0
- package/dist/cjs/Reconciliation.js.map +7 -0
- package/dist/cjs/ReplayOnly.d.ts +91 -0
- package/dist/cjs/ReplayOnly.d.ts.map +1 -0
- package/dist/cjs/ReplayOnly.js +52 -0
- package/dist/cjs/ReplayOnly.js.map +7 -0
- package/dist/cjs/Retention.d.ts +113 -0
- package/dist/cjs/Retention.d.ts.map +1 -0
- package/dist/cjs/Retention.js +99 -0
- package/dist/cjs/Retention.js.map +7 -0
- package/dist/cjs/RunCatalogRead.d.ts +17 -0
- package/dist/cjs/RunCatalogRead.d.ts.map +1 -0
- package/dist/cjs/RunCatalogRead.js +33 -0
- package/dist/cjs/RunCatalogRead.js.map +7 -0
- package/dist/cjs/RunChangeFeed.d.ts +75 -0
- package/dist/cjs/RunChangeFeed.d.ts.map +1 -0
- package/dist/cjs/RunChangeFeed.js +96 -0
- package/dist/cjs/RunChangeFeed.js.map +7 -0
- package/dist/cjs/RunState.d.ts +116 -0
- package/dist/cjs/RunState.d.ts.map +1 -0
- package/dist/cjs/RunState.js +76 -0
- package/dist/cjs/RunState.js.map +7 -0
- package/dist/cjs/Selection.d.ts +495 -0
- package/dist/cjs/Selection.d.ts.map +1 -0
- package/dist/cjs/Selection.js +288 -0
- package/dist/cjs/Selection.js.map +7 -0
- package/dist/cjs/SelectionStore.d.ts +141 -0
- package/dist/cjs/SelectionStore.d.ts.map +1 -0
- package/dist/cjs/SelectionStore.js +229 -0
- package/dist/cjs/SelectionStore.js.map +7 -0
- package/dist/cjs/StepBoundary.d.ts +501 -0
- package/dist/cjs/StepBoundary.d.ts.map +1 -0
- package/dist/cjs/StepBoundary.js +619 -0
- package/dist/cjs/StepBoundary.js.map +7 -0
- package/dist/cjs/StepCacheFold.d.ts +148 -0
- package/dist/cjs/StepCacheFold.d.ts.map +1 -0
- package/dist/cjs/StepCacheFold.js +131 -0
- package/dist/cjs/StepCacheFold.js.map +7 -0
- package/dist/cjs/StepSandbox.d.ts +78 -0
- package/dist/cjs/StepSandbox.d.ts.map +1 -0
- package/dist/cjs/StepSandbox.js +75 -0
- package/dist/cjs/StepSandbox.js.map +7 -0
- package/dist/cjs/WakeBus.d.ts +116 -0
- package/dist/cjs/WakeBus.d.ts.map +1 -0
- package/dist/cjs/WakeBus.js +89 -0
- package/dist/cjs/WakeBus.js.map +7 -0
- package/dist/cjs/WorkspaceSandbox.d.ts +619 -0
- package/dist/cjs/WorkspaceSandbox.d.ts.map +1 -0
- package/dist/cjs/WorkspaceSandbox.js +711 -0
- package/dist/cjs/WorkspaceSandbox.js.map +7 -0
- package/dist/cjs/index.d.ts +157 -0
- package/dist/cjs/index.d.ts.map +1 -0
- package/dist/cjs/index.js +95 -0
- package/dist/cjs/index.js.map +7 -0
- package/dist/cjs/internal/ActionPersistence.d.ts +276 -0
- package/dist/cjs/internal/ActionPersistence.d.ts.map +1 -0
- package/dist/cjs/internal/ActionPersistence.js +1395 -0
- package/dist/cjs/internal/ActionPersistence.js.map +7 -0
- package/dist/cjs/internal/ArtifactRoots.d.ts +38 -0
- package/dist/cjs/internal/ArtifactRoots.d.ts.map +1 -0
- package/dist/cjs/internal/ArtifactRoots.js +89 -0
- package/dist/cjs/internal/ArtifactRoots.js.map +7 -0
- package/dist/cjs/internal/AttemptAdmission.d.ts +53 -0
- package/dist/cjs/internal/AttemptAdmission.d.ts.map +1 -0
- package/dist/cjs/internal/AttemptAdmission.js +52 -0
- package/dist/cjs/internal/AttemptAdmission.js.map +7 -0
- package/dist/cjs/internal/AttemptLifecycle.d.ts +245 -0
- package/dist/cjs/internal/AttemptLifecycle.d.ts.map +1 -0
- package/dist/cjs/internal/AttemptLifecycle.js +112 -0
- package/dist/cjs/internal/AttemptLifecycle.js.map +7 -0
- package/dist/cjs/internal/AttemptProbe.d.ts +53 -0
- package/dist/cjs/internal/AttemptProbe.d.ts.map +1 -0
- package/dist/cjs/internal/AttemptProbe.js +66 -0
- package/dist/cjs/internal/AttemptProbe.js.map +7 -0
- package/dist/cjs/internal/CacheAdmission.d.ts +79 -0
- package/dist/cjs/internal/CacheAdmission.d.ts.map +1 -0
- package/dist/cjs/internal/CacheAdmission.js +71 -0
- package/dist/cjs/internal/CacheAdmission.js.map +7 -0
- package/dist/cjs/internal/CacheAgeHistory.d.ts +27 -0
- package/dist/cjs/internal/CacheAgeHistory.d.ts.map +1 -0
- package/dist/cjs/internal/CacheAgeHistory.js +61 -0
- package/dist/cjs/internal/CacheAgeHistory.js.map +7 -0
- package/dist/cjs/internal/CacheAgeVerdicts.d.ts +15 -0
- package/dist/cjs/internal/CacheAgeVerdicts.d.ts.map +1 -0
- package/dist/cjs/internal/CacheAgeVerdicts.js +73 -0
- package/dist/cjs/internal/CacheAgeVerdicts.js.map +7 -0
- package/dist/cjs/internal/CacheOutputPolicy.d.ts +24 -0
- package/dist/cjs/internal/CacheOutputPolicy.d.ts.map +1 -0
- package/dist/cjs/internal/CacheOutputPolicy.js +75 -0
- package/dist/cjs/internal/CacheOutputPolicy.js.map +7 -0
- package/dist/cjs/internal/CachePublication.d.ts +97 -0
- package/dist/cjs/internal/CachePublication.d.ts.map +1 -0
- package/dist/cjs/internal/CachePublication.js +80 -0
- package/dist/cjs/internal/CachePublication.js.map +7 -0
- package/dist/cjs/internal/CallFacts.d.ts +15 -0
- package/dist/cjs/internal/CallFacts.d.ts.map +1 -0
- package/dist/cjs/internal/CallFacts.js +97 -0
- package/dist/cjs/internal/CallFacts.js.map +7 -0
- package/dist/cjs/internal/CopiedRecord.d.ts +61 -0
- package/dist/cjs/internal/CopiedRecord.d.ts.map +1 -0
- package/dist/cjs/internal/CopiedRecord.js +134 -0
- package/dist/cjs/internal/CopiedRecord.js.map +7 -0
- package/dist/cjs/internal/DeferredPersistence.d.ts +108 -0
- package/dist/cjs/internal/DeferredPersistence.d.ts.map +1 -0
- package/dist/cjs/internal/DeferredPersistence.js +270 -0
- package/dist/cjs/internal/DeferredPersistence.js.map +7 -0
- package/dist/cjs/internal/EffectRecords.d.ts +92 -0
- package/dist/cjs/internal/EffectRecords.d.ts.map +1 -0
- package/dist/cjs/internal/EffectRecords.js +63 -0
- package/dist/cjs/internal/EffectRecords.js.map +7 -0
- package/dist/cjs/internal/EngineJj.d.ts +30 -0
- package/dist/cjs/internal/EngineJj.d.ts.map +1 -0
- package/dist/cjs/internal/EngineJj.js +42 -0
- package/dist/cjs/internal/EngineJj.js.map +7 -0
- package/dist/cjs/internal/EngineStateSchema.d.ts +68 -0
- package/dist/cjs/internal/EngineStateSchema.d.ts.map +1 -0
- package/dist/cjs/internal/EngineStateSchema.js +58 -0
- package/dist/cjs/internal/EngineStateSchema.js.map +7 -0
- package/dist/cjs/internal/ExecutionSnapshotRead.d.ts +162 -0
- package/dist/cjs/internal/ExecutionSnapshotRead.d.ts.map +1 -0
- package/dist/cjs/internal/ExecutionSnapshotRead.js +199 -0
- package/dist/cjs/internal/ExecutionSnapshotRead.js.map +7 -0
- package/dist/cjs/internal/ExecutionSnapshotRelations.d.ts +39 -0
- package/dist/cjs/internal/ExecutionSnapshotRelations.d.ts.map +1 -0
- package/dist/cjs/internal/ExecutionSnapshotRelations.js +105 -0
- package/dist/cjs/internal/ExecutionSnapshotRelations.js.map +7 -0
- package/dist/cjs/internal/ExitEncoding.d.ts +180 -0
- package/dist/cjs/internal/ExitEncoding.d.ts.map +1 -0
- package/dist/cjs/internal/ExitEncoding.js +236 -0
- package/dist/cjs/internal/ExitEncoding.js.map +7 -0
- package/dist/cjs/internal/FileBoundarySnapshot.d.ts +14 -0
- package/dist/cjs/internal/FileBoundarySnapshot.d.ts.map +1 -0
- package/dist/cjs/internal/FileBoundarySnapshot.js +51 -0
- package/dist/cjs/internal/FileBoundarySnapshot.js.map +7 -0
- package/dist/cjs/internal/FileEnumeration.d.ts +127 -0
- package/dist/cjs/internal/FileEnumeration.d.ts.map +1 -0
- package/dist/cjs/internal/FileEnumeration.js +194 -0
- package/dist/cjs/internal/FileEnumeration.js.map +7 -0
- package/dist/cjs/internal/HostReflection.d.ts +80 -0
- package/dist/cjs/internal/HostReflection.d.ts.map +1 -0
- package/dist/cjs/internal/HostReflection.js +43 -0
- package/dist/cjs/internal/HostReflection.js.map +7 -0
- package/dist/cjs/internal/JournalRecords.d.ts +325 -0
- package/dist/cjs/internal/JournalRecords.d.ts.map +1 -0
- package/dist/cjs/internal/JournalRecords.js +122 -0
- package/dist/cjs/internal/JournalRecords.js.map +7 -0
- package/dist/cjs/internal/NodeJournal.d.ts +157 -0
- package/dist/cjs/internal/NodeJournal.d.ts.map +1 -0
- package/dist/cjs/internal/NodeJournal.js +192 -0
- package/dist/cjs/internal/NodeJournal.js.map +7 -0
- package/dist/cjs/internal/Ordering.d.ts +14 -0
- package/dist/cjs/internal/Ordering.d.ts.map +1 -0
- package/dist/cjs/internal/Ordering.js +25 -0
- package/dist/cjs/internal/Ordering.js.map +7 -0
- package/dist/cjs/internal/PlanInputs.d.ts +14 -0
- package/dist/cjs/internal/PlanInputs.d.ts.map +1 -0
- package/dist/cjs/internal/PlanInputs.js +78 -0
- package/dist/cjs/internal/PlanInputs.js.map +7 -0
- package/dist/cjs/internal/PlanMerges.d.ts +178 -0
- package/dist/cjs/internal/PlanMerges.d.ts.map +1 -0
- package/dist/cjs/internal/PlanMerges.js +125 -0
- package/dist/cjs/internal/PlanMerges.js.map +7 -0
- package/dist/cjs/internal/ProvenanceSlot.d.ts +30 -0
- package/dist/cjs/internal/ProvenanceSlot.d.ts.map +1 -0
- package/dist/cjs/internal/ProvenanceSlot.js +53 -0
- package/dist/cjs/internal/ProvenanceSlot.js.map +7 -0
- package/dist/cjs/internal/ResultEnvelope.d.ts +88 -0
- package/dist/cjs/internal/ResultEnvelope.d.ts.map +1 -0
- package/dist/cjs/internal/ResultEnvelope.js +90 -0
- package/dist/cjs/internal/ResultEnvelope.js.map +7 -0
- package/dist/cjs/internal/RetentionOps.d.ts +414 -0
- package/dist/cjs/internal/RetentionOps.d.ts.map +1 -0
- package/dist/cjs/internal/RetentionOps.js +354 -0
- package/dist/cjs/internal/RetentionOps.js.map +7 -0
- package/dist/cjs/internal/RunCatalogOps.d.ts +118 -0
- package/dist/cjs/internal/RunCatalogOps.d.ts.map +1 -0
- package/dist/cjs/internal/RunCatalogOps.js +85 -0
- package/dist/cjs/internal/RunCatalogOps.js.map +7 -0
- package/dist/cjs/internal/RunCoordinator.d.ts +78 -0
- package/dist/cjs/internal/RunCoordinator.d.ts.map +1 -0
- package/dist/cjs/internal/RunCoordinator.js +123 -0
- package/dist/cjs/internal/RunCoordinator.js.map +7 -0
- package/dist/cjs/internal/RunDriver.d.ts +192 -0
- package/dist/cjs/internal/RunDriver.d.ts.map +1 -0
- package/dist/cjs/internal/RunDriver.js +1584 -0
- package/dist/cjs/internal/RunDriver.js.map +7 -0
- package/dist/cjs/internal/RunLifecycle.d.ts +22 -0
- package/dist/cjs/internal/RunLifecycle.d.ts.map +1 -0
- package/dist/cjs/internal/RunLifecycle.js +63 -0
- package/dist/cjs/internal/RunLifecycle.js.map +7 -0
- package/dist/cjs/internal/RunListing.d.ts +78 -0
- package/dist/cjs/internal/RunListing.d.ts.map +1 -0
- package/dist/cjs/internal/RunListing.js +171 -0
- package/dist/cjs/internal/RunListing.js.map +7 -0
- package/dist/cjs/internal/RunListingError.d.ts +35 -0
- package/dist/cjs/internal/RunListingError.d.ts.map +1 -0
- package/dist/cjs/internal/RunListingError.js +47 -0
- package/dist/cjs/internal/RunListingError.js.map +7 -0
- package/dist/cjs/internal/RuntimeGraph.d.ts +73 -0
- package/dist/cjs/internal/RuntimeGraph.d.ts.map +1 -0
- package/dist/cjs/internal/RuntimeGraph.js +180 -0
- package/dist/cjs/internal/RuntimeGraph.js.map +7 -0
- package/dist/cjs/internal/SandboxedExecution.d.ts +80 -0
- package/dist/cjs/internal/SandboxedExecution.d.ts.map +1 -0
- package/dist/cjs/internal/SandboxedExecution.js +104 -0
- package/dist/cjs/internal/SandboxedExecution.js.map +7 -0
- package/dist/cjs/internal/StateTransaction.d.ts +44 -0
- package/dist/cjs/internal/StateTransaction.d.ts.map +1 -0
- package/dist/cjs/internal/StateTransaction.js +52 -0
- package/dist/cjs/internal/StateTransaction.js.map +7 -0
- package/dist/cjs/internal/StepFacts.d.ts +14 -0
- package/dist/cjs/internal/StepFacts.d.ts.map +1 -0
- package/dist/cjs/internal/StepFacts.js +58 -0
- package/dist/cjs/internal/StepFacts.js.map +7 -0
- package/dist/cjs/internal/TypedEvents.d.ts +14 -0
- package/dist/cjs/internal/TypedEvents.d.ts.map +1 -0
- package/dist/cjs/internal/TypedEvents.js +37 -0
- package/dist/cjs/internal/TypedEvents.js.map +7 -0
- package/dist/cjs/migrations/0001_initial.d.ts +17 -0
- package/dist/cjs/migrations/0001_initial.d.ts.map +1 -0
- package/dist/cjs/migrations/0001_initial.js +61 -0
- package/dist/cjs/migrations/0001_initial.js.map +7 -0
- package/dist/cjs/migrations/0002_selection_store.d.ts +15 -0
- package/dist/cjs/migrations/0002_selection_store.d.ts.map +1 -0
- package/dist/cjs/migrations/0002_selection_store.js +56 -0
- package/dist/cjs/migrations/0002_selection_store.js.map +7 -0
- package/dist/cjs/migrations/0003_plan_inputs.d.ts +13 -0
- package/dist/cjs/migrations/0003_plan_inputs.d.ts.map +1 -0
- package/dist/cjs/migrations/0003_plan_inputs.js +90 -0
- package/dist/cjs/migrations/0003_plan_inputs.js.map +7 -0
- package/dist/cjs/migrations/0004_plan_environment.d.ts +14 -0
- package/dist/cjs/migrations/0004_plan_environment.d.ts.map +1 -0
- package/dist/cjs/migrations/0004_plan_environment.js +56 -0
- package/dist/cjs/migrations/0004_plan_environment.js.map +7 -0
- package/dist/cjs/migrations/0005_plan_merges.d.ts +14 -0
- package/dist/cjs/migrations/0005_plan_merges.d.ts.map +1 -0
- package/dist/cjs/migrations/0005_plan_merges.js +82 -0
- package/dist/cjs/migrations/0005_plan_merges.js.map +7 -0
- package/dist/cjs/migrations/0006_execution_listing.d.ts +15 -0
- package/dist/cjs/migrations/0006_execution_listing.d.ts.map +1 -0
- package/dist/cjs/migrations/0006_execution_listing.js +101 -0
- package/dist/cjs/migrations/0006_execution_listing.js.map +7 -0
- package/dist/cjs/migrations/0007_run_parent_sequence.d.ts +15 -0
- package/dist/cjs/migrations/0007_run_parent_sequence.d.ts.map +1 -0
- package/dist/cjs/migrations/0007_run_parent_sequence.js +40 -0
- package/dist/cjs/migrations/0007_run_parent_sequence.js.map +7 -0
- package/dist/cjs/migrations/0008_deferred_consumption.d.ts +16 -0
- package/dist/cjs/migrations/0008_deferred_consumption.d.ts.map +1 -0
- package/dist/cjs/migrations/0008_deferred_consumption.js +45 -0
- package/dist/cjs/migrations/0008_deferred_consumption.js.map +7 -0
- package/dist/cjs/migrations/0009_deferred_clock_facts.d.ts +24 -0
- package/dist/cjs/migrations/0009_deferred_clock_facts.d.ts.map +1 -0
- package/dist/cjs/migrations/0009_deferred_clock_facts.js +141 -0
- package/dist/cjs/migrations/0009_deferred_clock_facts.js.map +7 -0
- package/dist/cjs/migrations/0010_pending_cancellation.d.ts +15 -0
- package/dist/cjs/migrations/0010_pending_cancellation.d.ts.map +1 -0
- package/dist/cjs/migrations/0010_pending_cancellation.js +41 -0
- package/dist/cjs/migrations/0010_pending_cancellation.js.map +7 -0
- package/dist/cjs/package.json +1 -0
- package/dist/cjs/test/TestStores.d.ts +89 -0
- package/dist/cjs/test/TestStores.d.ts.map +1 -0
- package/dist/cjs/test/TestStores.js +98 -0
- package/dist/cjs/test/TestStores.js.map +7 -0
- package/dist/esm/ArtifactGc.d.ts +235 -0
- package/dist/esm/ArtifactGc.d.ts.map +1 -0
- package/dist/esm/ArtifactGc.js +268 -0
- package/dist/esm/ArtifactGc.js.map +1 -0
- package/dist/esm/ArtifactSync.d.ts +164 -0
- package/dist/esm/ArtifactSync.d.ts.map +1 -0
- package/dist/esm/ArtifactSync.js +176 -0
- package/dist/esm/ArtifactSync.js.map +1 -0
- package/dist/esm/CacheSync.d.ts +112 -0
- package/dist/esm/CacheSync.d.ts.map +1 -0
- package/dist/esm/CacheSync.js +94 -0
- package/dist/esm/CacheSync.js.map +1 -0
- package/dist/esm/DeferredClockFold.d.ts +354 -0
- package/dist/esm/DeferredClockFold.d.ts.map +1 -0
- package/dist/esm/DeferredClockFold.js +472 -0
- package/dist/esm/DeferredClockFold.js.map +1 -0
- package/dist/esm/DisasterRecovery.d.ts +431 -0
- package/dist/esm/DisasterRecovery.d.ts.map +1 -0
- package/dist/esm/DisasterRecovery.js +523 -0
- package/dist/esm/DisasterRecovery.js.map +1 -0
- package/dist/esm/DurableEngineState.d.ts +776 -0
- package/dist/esm/DurableEngineState.d.ts.map +1 -0
- package/dist/esm/DurableEngineState.js +1648 -0
- package/dist/esm/DurableEngineState.js.map +1 -0
- package/dist/esm/EngineStore.d.ts +158 -0
- package/dist/esm/EngineStore.d.ts.map +1 -0
- package/dist/esm/EngineStore.js +403 -0
- package/dist/esm/EngineStore.js.map +1 -0
- package/dist/esm/EngineStoreMetrics.d.ts +276 -0
- package/dist/esm/EngineStoreMetrics.d.ts.map +1 -0
- package/dist/esm/EngineStoreMetrics.js +260 -0
- package/dist/esm/EngineStoreMetrics.js.map +1 -0
- package/dist/esm/Errors.d.ts +22 -0
- package/dist/esm/Errors.d.ts.map +1 -0
- package/dist/esm/Errors.js +22 -0
- package/dist/esm/Errors.js.map +1 -0
- package/dist/esm/EventTypes.d.ts +27 -0
- package/dist/esm/EventTypes.d.ts.map +1 -0
- package/dist/esm/EventTypes.js +27 -0
- package/dist/esm/EventTypes.js.map +1 -0
- package/dist/esm/ExecutionFacts.d.ts +59 -0
- package/dist/esm/ExecutionFacts.d.ts.map +1 -0
- package/dist/esm/ExecutionFacts.js +96 -0
- package/dist/esm/ExecutionFacts.js.map +1 -0
- package/dist/esm/ExecutionSnapshot.d.ts +117 -0
- package/dist/esm/ExecutionSnapshot.d.ts.map +1 -0
- package/dist/esm/ExecutionSnapshot.js +95 -0
- package/dist/esm/ExecutionSnapshot.js.map +1 -0
- package/dist/esm/Inconsistency.d.ts +154 -0
- package/dist/esm/Inconsistency.d.ts.map +1 -0
- package/dist/esm/Inconsistency.js +133 -0
- package/dist/esm/Inconsistency.js.map +1 -0
- package/dist/esm/Migrations.d.ts +56 -0
- package/dist/esm/Migrations.d.ts.map +1 -0
- package/dist/esm/Migrations.js +91 -0
- package/dist/esm/Migrations.js.map +1 -0
- package/dist/esm/OwnerIdentity.d.ts +91 -0
- package/dist/esm/OwnerIdentity.d.ts.map +1 -0
- package/dist/esm/OwnerIdentity.js +99 -0
- package/dist/esm/OwnerIdentity.js.map +1 -0
- package/dist/esm/PlanInputStore.d.ts +110 -0
- package/dist/esm/PlanInputStore.d.ts.map +1 -0
- package/dist/esm/PlanInputStore.js +207 -0
- package/dist/esm/PlanInputStore.js.map +1 -0
- package/dist/esm/PlanMergeStore.d.ts +113 -0
- package/dist/esm/PlanMergeStore.d.ts.map +1 -0
- package/dist/esm/PlanMergeStore.js +244 -0
- package/dist/esm/PlanMergeStore.js.map +1 -0
- package/dist/esm/PlanScheduler.d.ts +363 -0
- package/dist/esm/PlanScheduler.d.ts.map +1 -0
- package/dist/esm/PlanScheduler.js +1321 -0
- package/dist/esm/PlanScheduler.js.map +1 -0
- package/dist/esm/Reconciliation.d.ts +175 -0
- package/dist/esm/Reconciliation.d.ts.map +1 -0
- package/dist/esm/Reconciliation.js +175 -0
- package/dist/esm/Reconciliation.js.map +1 -0
- package/dist/esm/ReplayOnly.d.ts +91 -0
- package/dist/esm/ReplayOnly.d.ts.map +1 -0
- package/dist/esm/ReplayOnly.js +59 -0
- package/dist/esm/ReplayOnly.js.map +1 -0
- package/dist/esm/Retention.d.ts +113 -0
- package/dist/esm/Retention.d.ts.map +1 -0
- package/dist/esm/Retention.js +139 -0
- package/dist/esm/Retention.js.map +1 -0
- package/dist/esm/RunCatalogRead.d.ts +17 -0
- package/dist/esm/RunCatalogRead.d.ts.map +1 -0
- package/dist/esm/RunCatalogRead.js +17 -0
- package/dist/esm/RunCatalogRead.js.map +1 -0
- package/dist/esm/RunChangeFeed.d.ts +75 -0
- package/dist/esm/RunChangeFeed.d.ts.map +1 -0
- package/dist/esm/RunChangeFeed.js +81 -0
- package/dist/esm/RunChangeFeed.js.map +1 -0
- package/dist/esm/RunState.d.ts +116 -0
- package/dist/esm/RunState.d.ts.map +1 -0
- package/dist/esm/RunState.js +109 -0
- package/dist/esm/RunState.js.map +1 -0
- package/dist/esm/Selection.d.ts +495 -0
- package/dist/esm/Selection.d.ts.map +1 -0
- package/dist/esm/Selection.js +482 -0
- package/dist/esm/Selection.js.map +1 -0
- package/dist/esm/SelectionStore.d.ts +141 -0
- package/dist/esm/SelectionStore.d.ts.map +1 -0
- package/dist/esm/SelectionStore.js +215 -0
- package/dist/esm/SelectionStore.js.map +1 -0
- package/dist/esm/StepBoundary.d.ts +501 -0
- package/dist/esm/StepBoundary.d.ts.map +1 -0
- package/dist/esm/StepBoundary.js +919 -0
- package/dist/esm/StepBoundary.js.map +1 -0
- package/dist/esm/StepCacheFold.d.ts +148 -0
- package/dist/esm/StepCacheFold.d.ts.map +1 -0
- package/dist/esm/StepCacheFold.js +201 -0
- package/dist/esm/StepCacheFold.js.map +1 -0
- package/dist/esm/StepSandbox.d.ts +78 -0
- package/dist/esm/StepSandbox.d.ts.map +1 -0
- package/dist/esm/StepSandbox.js +70 -0
- package/dist/esm/StepSandbox.js.map +1 -0
- package/dist/esm/WakeBus.d.ts +116 -0
- package/dist/esm/WakeBus.d.ts.map +1 -0
- package/dist/esm/WakeBus.js +121 -0
- package/dist/esm/WakeBus.js.map +1 -0
- package/dist/esm/WorkspaceSandbox.d.ts +619 -0
- package/dist/esm/WorkspaceSandbox.d.ts.map +1 -0
- package/dist/esm/WorkspaceSandbox.js +985 -0
- package/dist/esm/WorkspaceSandbox.js.map +1 -0
- package/dist/esm/index.d.ts +157 -0
- package/dist/esm/index.d.ts.map +1 -0
- package/dist/esm/index.js +157 -0
- package/dist/esm/index.js.map +1 -0
- package/dist/esm/internal/ActionPersistence.d.ts +276 -0
- package/dist/esm/internal/ActionPersistence.d.ts.map +1 -0
- package/dist/esm/internal/ActionPersistence.js +2306 -0
- package/dist/esm/internal/ActionPersistence.js.map +1 -0
- package/dist/esm/internal/ArtifactRoots.d.ts +38 -0
- package/dist/esm/internal/ArtifactRoots.d.ts.map +1 -0
- package/dist/esm/internal/ArtifactRoots.js +76 -0
- package/dist/esm/internal/ArtifactRoots.js.map +1 -0
- package/dist/esm/internal/AttemptAdmission.d.ts +53 -0
- package/dist/esm/internal/AttemptAdmission.d.ts.map +1 -0
- package/dist/esm/internal/AttemptAdmission.js +52 -0
- package/dist/esm/internal/AttemptAdmission.js.map +1 -0
- package/dist/esm/internal/AttemptLifecycle.d.ts +245 -0
- package/dist/esm/internal/AttemptLifecycle.d.ts.map +1 -0
- package/dist/esm/internal/AttemptLifecycle.js +104 -0
- package/dist/esm/internal/AttemptLifecycle.js.map +1 -0
- package/dist/esm/internal/AttemptProbe.d.ts +53 -0
- package/dist/esm/internal/AttemptProbe.d.ts.map +1 -0
- package/dist/esm/internal/AttemptProbe.js +83 -0
- package/dist/esm/internal/AttemptProbe.js.map +1 -0
- package/dist/esm/internal/CacheAdmission.d.ts +79 -0
- package/dist/esm/internal/CacheAdmission.d.ts.map +1 -0
- package/dist/esm/internal/CacheAdmission.js +72 -0
- package/dist/esm/internal/CacheAdmission.js.map +1 -0
- package/dist/esm/internal/CacheAgeHistory.d.ts +27 -0
- package/dist/esm/internal/CacheAgeHistory.d.ts.map +1 -0
- package/dist/esm/internal/CacheAgeHistory.js +49 -0
- package/dist/esm/internal/CacheAgeHistory.js.map +1 -0
- package/dist/esm/internal/CacheAgeVerdicts.d.ts +15 -0
- package/dist/esm/internal/CacheAgeVerdicts.d.ts.map +1 -0
- package/dist/esm/internal/CacheAgeVerdicts.js +54 -0
- package/dist/esm/internal/CacheAgeVerdicts.js.map +1 -0
- package/dist/esm/internal/CacheOutputPolicy.d.ts +24 -0
- package/dist/esm/internal/CacheOutputPolicy.d.ts.map +1 -0
- package/dist/esm/internal/CacheOutputPolicy.js +73 -0
- package/dist/esm/internal/CacheOutputPolicy.js.map +1 -0
- package/dist/esm/internal/CachePublication.d.ts +97 -0
- package/dist/esm/internal/CachePublication.d.ts.map +1 -0
- package/dist/esm/internal/CachePublication.js +116 -0
- package/dist/esm/internal/CachePublication.js.map +1 -0
- package/dist/esm/internal/CallFacts.d.ts +15 -0
- package/dist/esm/internal/CallFacts.d.ts.map +1 -0
- package/dist/esm/internal/CallFacts.js +83 -0
- package/dist/esm/internal/CallFacts.js.map +1 -0
- package/dist/esm/internal/CopiedRecord.d.ts +61 -0
- package/dist/esm/internal/CopiedRecord.d.ts.map +1 -0
- package/dist/esm/internal/CopiedRecord.js +165 -0
- package/dist/esm/internal/CopiedRecord.js.map +1 -0
- package/dist/esm/internal/DeferredPersistence.d.ts +108 -0
- package/dist/esm/internal/DeferredPersistence.d.ts.map +1 -0
- package/dist/esm/internal/DeferredPersistence.js +240 -0
- package/dist/esm/internal/DeferredPersistence.js.map +1 -0
- package/dist/esm/internal/EffectRecords.d.ts +92 -0
- package/dist/esm/internal/EffectRecords.d.ts.map +1 -0
- package/dist/esm/internal/EffectRecords.js +94 -0
- package/dist/esm/internal/EffectRecords.js.map +1 -0
- package/dist/esm/internal/EngineJj.d.ts +30 -0
- package/dist/esm/internal/EngineJj.d.ts.map +1 -0
- package/dist/esm/internal/EngineJj.js +29 -0
- package/dist/esm/internal/EngineJj.js.map +1 -0
- package/dist/esm/internal/EngineStateSchema.d.ts +68 -0
- package/dist/esm/internal/EngineStateSchema.d.ts.map +1 -0
- package/dist/esm/internal/EngineStateSchema.js +53 -0
- package/dist/esm/internal/EngineStateSchema.js.map +1 -0
- package/dist/esm/internal/ExecutionSnapshotRead.d.ts +162 -0
- package/dist/esm/internal/ExecutionSnapshotRead.d.ts.map +1 -0
- package/dist/esm/internal/ExecutionSnapshotRead.js +218 -0
- package/dist/esm/internal/ExecutionSnapshotRead.js.map +1 -0
- package/dist/esm/internal/ExecutionSnapshotRelations.d.ts +39 -0
- package/dist/esm/internal/ExecutionSnapshotRelations.d.ts.map +1 -0
- package/dist/esm/internal/ExecutionSnapshotRelations.js +81 -0
- package/dist/esm/internal/ExecutionSnapshotRelations.js.map +1 -0
- package/dist/esm/internal/ExitEncoding.d.ts +180 -0
- package/dist/esm/internal/ExitEncoding.d.ts.map +1 -0
- package/dist/esm/internal/ExitEncoding.js +361 -0
- package/dist/esm/internal/ExitEncoding.js.map +1 -0
- package/dist/esm/internal/FileBoundarySnapshot.d.ts +14 -0
- package/dist/esm/internal/FileBoundarySnapshot.d.ts.map +1 -0
- package/dist/esm/internal/FileBoundarySnapshot.js +29 -0
- package/dist/esm/internal/FileBoundarySnapshot.js.map +1 -0
- package/dist/esm/internal/FileEnumeration.d.ts +127 -0
- package/dist/esm/internal/FileEnumeration.d.ts.map +1 -0
- package/dist/esm/internal/FileEnumeration.js +236 -0
- package/dist/esm/internal/FileEnumeration.js.map +1 -0
- package/dist/esm/internal/HostReflection.d.ts +80 -0
- package/dist/esm/internal/HostReflection.d.ts.map +1 -0
- package/dist/esm/internal/HostReflection.js +98 -0
- package/dist/esm/internal/HostReflection.js.map +1 -0
- package/dist/esm/internal/JournalRecords.d.ts +325 -0
- package/dist/esm/internal/JournalRecords.d.ts.map +1 -0
- package/dist/esm/internal/JournalRecords.js +306 -0
- package/dist/esm/internal/JournalRecords.js.map +1 -0
- package/dist/esm/internal/NodeJournal.d.ts +157 -0
- package/dist/esm/internal/NodeJournal.d.ts.map +1 -0
- package/dist/esm/internal/NodeJournal.js +298 -0
- package/dist/esm/internal/NodeJournal.js.map +1 -0
- package/dist/esm/internal/Ordering.d.ts +14 -0
- package/dist/esm/internal/Ordering.d.ts.map +1 -0
- package/dist/esm/internal/Ordering.js +14 -0
- package/dist/esm/internal/Ordering.js.map +1 -0
- package/dist/esm/internal/PlanInputs.d.ts +14 -0
- package/dist/esm/internal/PlanInputs.d.ts.map +1 -0
- package/dist/esm/internal/PlanInputs.js +57 -0
- package/dist/esm/internal/PlanInputs.js.map +1 -0
- package/dist/esm/internal/PlanMerges.d.ts +178 -0
- package/dist/esm/internal/PlanMerges.d.ts.map +1 -0
- package/dist/esm/internal/PlanMerges.js +113 -0
- package/dist/esm/internal/PlanMerges.js.map +1 -0
- package/dist/esm/internal/ProvenanceSlot.d.ts +30 -0
- package/dist/esm/internal/ProvenanceSlot.d.ts.map +1 -0
- package/dist/esm/internal/ProvenanceSlot.js +39 -0
- package/dist/esm/internal/ProvenanceSlot.js.map +1 -0
- package/dist/esm/internal/ResultEnvelope.d.ts +88 -0
- package/dist/esm/internal/ResultEnvelope.d.ts.map +1 -0
- package/dist/esm/internal/ResultEnvelope.js +83 -0
- package/dist/esm/internal/ResultEnvelope.js.map +1 -0
- package/dist/esm/internal/RetentionOps.d.ts +414 -0
- package/dist/esm/internal/RetentionOps.d.ts.map +1 -0
- package/dist/esm/internal/RetentionOps.js +576 -0
- package/dist/esm/internal/RetentionOps.js.map +1 -0
- package/dist/esm/internal/RunCatalogOps.d.ts +118 -0
- package/dist/esm/internal/RunCatalogOps.d.ts.map +1 -0
- package/dist/esm/internal/RunCatalogOps.js +120 -0
- package/dist/esm/internal/RunCatalogOps.js.map +1 -0
- package/dist/esm/internal/RunCoordinator.d.ts +78 -0
- package/dist/esm/internal/RunCoordinator.d.ts.map +1 -0
- package/dist/esm/internal/RunCoordinator.js +115 -0
- package/dist/esm/internal/RunCoordinator.js.map +1 -0
- package/dist/esm/internal/RunDriver.d.ts +192 -0
- package/dist/esm/internal/RunDriver.d.ts.map +1 -0
- package/dist/esm/internal/RunDriver.js +2334 -0
- package/dist/esm/internal/RunDriver.js.map +1 -0
- package/dist/esm/internal/RunLifecycle.d.ts +22 -0
- package/dist/esm/internal/RunLifecycle.d.ts.map +1 -0
- package/dist/esm/internal/RunLifecycle.js +49 -0
- package/dist/esm/internal/RunLifecycle.js.map +1 -0
- package/dist/esm/internal/RunListing.d.ts +78 -0
- package/dist/esm/internal/RunListing.d.ts.map +1 -0
- package/dist/esm/internal/RunListing.js +146 -0
- package/dist/esm/internal/RunListing.js.map +1 -0
- package/dist/esm/internal/RunListingError.d.ts +35 -0
- package/dist/esm/internal/RunListingError.d.ts.map +1 -0
- package/dist/esm/internal/RunListingError.js +27 -0
- package/dist/esm/internal/RunListingError.js.map +1 -0
- package/dist/esm/internal/RuntimeGraph.d.ts +73 -0
- package/dist/esm/internal/RuntimeGraph.d.ts.map +1 -0
- package/dist/esm/internal/RuntimeGraph.js +182 -0
- package/dist/esm/internal/RuntimeGraph.js.map +1 -0
- package/dist/esm/internal/SandboxedExecution.d.ts +80 -0
- package/dist/esm/internal/SandboxedExecution.d.ts.map +1 -0
- package/dist/esm/internal/SandboxedExecution.js +107 -0
- package/dist/esm/internal/SandboxedExecution.js.map +1 -0
- package/dist/esm/internal/StateTransaction.d.ts +44 -0
- package/dist/esm/internal/StateTransaction.d.ts.map +1 -0
- package/dist/esm/internal/StateTransaction.js +54 -0
- package/dist/esm/internal/StateTransaction.js.map +1 -0
- package/dist/esm/internal/StepFacts.d.ts +14 -0
- package/dist/esm/internal/StepFacts.d.ts.map +1 -0
- package/dist/esm/internal/StepFacts.js +35 -0
- package/dist/esm/internal/StepFacts.js.map +1 -0
- package/dist/esm/internal/TypedEvents.d.ts +14 -0
- package/dist/esm/internal/TypedEvents.d.ts.map +1 -0
- package/dist/esm/internal/TypedEvents.js +14 -0
- package/dist/esm/internal/TypedEvents.js.map +1 -0
- package/dist/esm/migrations/0001_initial.d.ts +17 -0
- package/dist/esm/migrations/0001_initial.d.ts.map +1 -0
- package/dist/esm/migrations/0001_initial.js +41 -0
- package/dist/esm/migrations/0001_initial.js.map +1 -0
- package/dist/esm/migrations/0002_selection_store.d.ts +15 -0
- package/dist/esm/migrations/0002_selection_store.d.ts.map +1 -0
- package/dist/esm/migrations/0002_selection_store.js +34 -0
- package/dist/esm/migrations/0002_selection_store.js.map +1 -0
- package/dist/esm/migrations/0003_plan_inputs.d.ts +13 -0
- package/dist/esm/migrations/0003_plan_inputs.d.ts.map +1 -0
- package/dist/esm/migrations/0003_plan_inputs.js +70 -0
- package/dist/esm/migrations/0003_plan_inputs.js.map +1 -0
- package/dist/esm/migrations/0004_plan_environment.d.ts +14 -0
- package/dist/esm/migrations/0004_plan_environment.d.ts.map +1 -0
- package/dist/esm/migrations/0004_plan_environment.js +33 -0
- package/dist/esm/migrations/0004_plan_environment.js.map +1 -0
- package/dist/esm/migrations/0005_plan_merges.d.ts +14 -0
- package/dist/esm/migrations/0005_plan_merges.d.ts.map +1 -0
- package/dist/esm/migrations/0005_plan_merges.js +59 -0
- package/dist/esm/migrations/0005_plan_merges.js.map +1 -0
- package/dist/esm/migrations/0006_execution_listing.d.ts +15 -0
- package/dist/esm/migrations/0006_execution_listing.d.ts.map +1 -0
- package/dist/esm/migrations/0006_execution_listing.js +81 -0
- package/dist/esm/migrations/0006_execution_listing.js.map +1 -0
- package/dist/esm/migrations/0007_run_parent_sequence.d.ts +15 -0
- package/dist/esm/migrations/0007_run_parent_sequence.d.ts.map +1 -0
- package/dist/esm/migrations/0007_run_parent_sequence.js +18 -0
- package/dist/esm/migrations/0007_run_parent_sequence.js.map +1 -0
- package/dist/esm/migrations/0008_deferred_consumption.d.ts +16 -0
- package/dist/esm/migrations/0008_deferred_consumption.d.ts.map +1 -0
- package/dist/esm/migrations/0008_deferred_consumption.js +24 -0
- package/dist/esm/migrations/0008_deferred_consumption.js.map +1 -0
- package/dist/esm/migrations/0009_deferred_clock_facts.d.ts +24 -0
- package/dist/esm/migrations/0009_deferred_clock_facts.d.ts.map +1 -0
- package/dist/esm/migrations/0009_deferred_clock_facts.js +127 -0
- package/dist/esm/migrations/0009_deferred_clock_facts.js.map +1 -0
- package/dist/esm/migrations/0010_pending_cancellation.d.ts +15 -0
- package/dist/esm/migrations/0010_pending_cancellation.d.ts.map +1 -0
- package/dist/esm/migrations/0010_pending_cancellation.js +19 -0
- package/dist/esm/migrations/0010_pending_cancellation.js.map +1 -0
- package/dist/esm/test/TestStores.d.ts +89 -0
- package/dist/esm/test/TestStores.d.ts.map +1 -0
- package/dist/esm/test/TestStores.js +103 -0
- package/dist/esm/test/TestStores.js.map +1 -0
- package/docs/README.md +113 -0
- package/docs/api.md +1494 -0
- package/docs/concepts/attempts-and-replay.md +149 -0
- package/docs/concepts/cache-admission.md +265 -0
- package/docs/concepts/call-facts.md +86 -0
- package/docs/concepts/durable-waits.md +175 -0
- package/docs/concepts/execution-facts.md +90 -0
- package/docs/concepts/ownership-and-fencing.md +128 -0
- package/docs/concepts/step-boundaries.md +148 -0
- package/docs/concepts/workspace-transactions.md +199 -0
- package/docs/event-types.md +14 -0
- package/docs/guides/back-up-and-restore.md +164 -0
- package/docs/guides/collect-unreferenced-artifacts.md +128 -0
- package/docs/guides/compose-a-durable-engine.md +146 -0
- package/docs/guides/coordinate-two-processes.md +120 -0
- package/docs/guides/defer-work-with-selection.md +171 -0
- package/docs/guides/delete-old-run-history.md +154 -0
- package/docs/guides/drive-a-plan.md +314 -0
- package/docs/guides/observe-engine-metrics.md +98 -0
- package/docs/guides/observe-executions.md +187 -0
- package/docs/guides/reclaim-runs-from-a-dead-host.md +133 -0
- package/docs/guides/share-a-cache-across-machines.md +134 -0
- package/docs/guides/testing.md +123 -0
- package/docs/installation.md +102 -0
- package/docs/quickstart.md +215 -0
- package/docs/troubleshooting.md +347 -0
- package/package.json +438 -3
- package/src/ArtifactGc.ts +408 -0
- package/src/ArtifactSync.ts +278 -0
- package/src/CacheSync.ts +131 -0
- package/src/DeferredClockFold.ts +586 -0
- package/src/DisasterRecovery.ts +888 -0
- package/src/DurableEngineState.ts +2656 -0
- package/src/EngineStore.ts +656 -0
- package/src/EngineStoreMetrics.ts +331 -0
- package/src/Errors.ts +28 -0
- package/src/EventTypes.ts +27 -0
- package/src/ExecutionFacts.ts +125 -0
- package/src/ExecutionSnapshot.ts +186 -0
- package/src/Inconsistency.ts +253 -0
- package/src/Migrations.ts +95 -0
- package/src/OwnerIdentity.ts +125 -0
- package/src/PlanInputStore.ts +292 -0
- package/src/PlanMergeStore.ts +330 -0
- package/src/PlanScheduler.ts +1806 -0
- package/src/Reconciliation.ts +224 -0
- package/src/ReplayOnly.ts +95 -0
- package/src/Retention.ts +214 -0
- package/src/RunCatalogRead.ts +27 -0
- package/src/RunChangeFeed.ts +125 -0
- package/src/RunState.ts +137 -0
- package/src/Selection.ts +683 -0
- package/src/SelectionStore.ts +353 -0
- package/src/StepBoundary.ts +1165 -0
- package/src/StepCacheFold.ts +244 -0
- package/src/StepSandbox.ts +110 -0
- package/src/WakeBus.ts +167 -0
- package/src/WorkspaceSandbox.ts +1604 -0
- package/src/index.ts +185 -0
- package/src/internal/ActionPersistence.ts +2687 -0
- package/src/internal/ArtifactRoots.ts +92 -0
- package/src/internal/AttemptAdmission.ts +76 -0
- package/src/internal/AttemptLifecycle.ts +163 -0
- package/src/internal/AttemptProbe.ts +90 -0
- package/src/internal/CacheAdmission.ts +133 -0
- package/src/internal/CacheAgeHistory.ts +58 -0
- package/src/internal/CacheAgeVerdicts.ts +61 -0
- package/src/internal/CacheOutputPolicy.ts +82 -0
- package/src/internal/CachePublication.ts +153 -0
- package/src/internal/CallFacts.ts +87 -0
- package/src/internal/CopiedRecord.ts +201 -0
- package/src/internal/DeferredPersistence.ts +459 -0
- package/src/internal/EffectRecords.ts +136 -0
- package/src/internal/EngineJj.ts +30 -0
- package/src/internal/EngineStateSchema.ts +92 -0
- package/src/internal/ExecutionSnapshotRead.ts +298 -0
- package/src/internal/ExecutionSnapshotRelations.ts +121 -0
- package/src/internal/ExitEncoding.ts +453 -0
- package/src/internal/FileBoundarySnapshot.ts +39 -0
- package/src/internal/FileEnumeration.ts +334 -0
- package/src/internal/HostReflection.ts +128 -0
- package/src/internal/JournalRecords.ts +396 -0
- package/src/internal/NodeJournal.ts +340 -0
- package/src/internal/Ordering.ts +14 -0
- package/src/internal/PlanInputs.ts +62 -0
- package/src/internal/PlanMerges.ts +123 -0
- package/src/internal/ProvenanceSlot.ts +52 -0
- package/src/internal/ResultEnvelope.ts +118 -0
- package/src/internal/RetentionOps.ts +865 -0
- package/src/internal/RunCatalogOps.ts +174 -0
- package/src/internal/RunCoordinator.ts +215 -0
- package/src/internal/RunDriver.ts +3152 -0
- package/src/internal/RunLifecycle.ts +53 -0
- package/src/internal/RunListing.ts +210 -0
- package/src/internal/RunListingError.ts +37 -0
- package/src/internal/RuntimeGraph.ts +253 -0
- package/src/internal/SandboxedExecution.ts +165 -0
- package/src/internal/StateTransaction.ts +71 -0
- package/src/internal/StepFacts.ts +36 -0
- package/src/internal/TypedEvents.ts +29 -0
- package/src/migrations/0001_initial.ts +51 -0
- package/src/migrations/0002_selection_store.ts +38 -0
- package/src/migrations/0003_plan_inputs.ts +75 -0
- package/src/migrations/0004_plan_environment.ts +34 -0
- package/src/migrations/0005_plan_merges.ts +60 -0
- package/src/migrations/0006_execution_listing.ts +86 -0
- package/src/migrations/0007_run_parent_sequence.ts +19 -0
- package/src/migrations/0008_deferred_consumption.ts +27 -0
- package/src/migrations/0009_deferred_clock_facts.ts +163 -0
- package/src/migrations/0010_pending_cancellation.ts +20 -0
- package/src/test/TestStores.ts +145 -0
|
@@ -0,0 +1,1604 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Functional workspace transactions for action execution.
|
|
3
|
+
*
|
|
4
|
+
* An action body normally mutates the host tree in place, which leaves the
|
|
5
|
+
* engine two bad options: trust the declaration, or diff the whole repository
|
|
6
|
+
* after the fact. This module takes the third one. The body runs against an
|
|
7
|
+
* **isolated workspace** seeded with exactly its declared read set; its writes
|
|
8
|
+
* are a value — a whole-tree diff — that reaches the host only through an
|
|
9
|
+
* explicit {@link Service.materialize} step with preconditions and rollback.
|
|
10
|
+
*
|
|
11
|
+
* Two consequences follow, and they are the reason this exists:
|
|
12
|
+
*
|
|
13
|
+
* - **Whole-tree write observation is structural.** The transaction *is* the
|
|
14
|
+
* tree, so "did this body write outside its declared write set" is a map
|
|
15
|
+
* comparison, not an inference. `StepBoundary`'s filesystem layer can only
|
|
16
|
+
* re-measure paths it was told about, which is why it never claims
|
|
17
|
+
* `wholeTreeWritesVerified` and why nothing it settled could enter the
|
|
18
|
+
* cross-run cache.
|
|
19
|
+
* - **The host is untouched until copy-back.** An execution whose observations
|
|
20
|
+
* contradict its declaration is {@link Invalidated}, and an invalidated
|
|
21
|
+
* result exposes provenance and violations *only* — never the candidate
|
|
22
|
+
* output, files, or queued effects.
|
|
23
|
+
*
|
|
24
|
+
* This is a **deterministic transaction model, not a security boundary** — the
|
|
25
|
+
* same caveat the proof of concept carried. A body that reaches the host
|
|
26
|
+
* through a service this module does not seed (a spawned native process, an
|
|
27
|
+
* undecorated socket) is outside the transaction. Actually denying that
|
|
28
|
+
* ambient access requires a machine boundary such as the providers in
|
|
29
|
+
* `packages/smithers/flows/sandbox/docs/api.md`. The one host
|
|
30
|
+
* write this module *does* perform is confined, however: copy-back resolves
|
|
31
|
+
* symlinks before it lands a byte and refuses any target whose canonical
|
|
32
|
+
* location escapes the workspace root.
|
|
33
|
+
*
|
|
34
|
+
* Settled bundles reach the host without a human review gate. Governing
|
|
35
|
+
* design: `docs/concepts/workspace-transactions.md`.
|
|
36
|
+
*
|
|
37
|
+
* @since 0.1.0
|
|
38
|
+
*/
|
|
39
|
+
|
|
40
|
+
import * as ArtifactStore from "@smthrs/artifacts/ArtifactStore"
|
|
41
|
+
import * as FileLease from "@smthrs/artifacts/FileLease"
|
|
42
|
+
import { Sha256 } from "@smthrs/crypto"
|
|
43
|
+
import type { FileBoundary } from "@smthrs/flow/FileBoundary"
|
|
44
|
+
import * as KernelFileSystem from "@smthrs/kernel/FileSystem"
|
|
45
|
+
import { Workspace as KernelWorkspace } from "@smthrs/kernel/Workspace"
|
|
46
|
+
import { DerivedKey } from "@smthrs/keys"
|
|
47
|
+
import * as FileSet from "@smthrs/plan/FileSet"
|
|
48
|
+
import * as Cause from "effect/Cause"
|
|
49
|
+
import * as Context from "effect/Context"
|
|
50
|
+
import type * as Crypto from "effect/Crypto"
|
|
51
|
+
import * as Effect from "effect/Effect"
|
|
52
|
+
import * as Exit from "effect/Exit"
|
|
53
|
+
import * as FileSystem from "effect/FileSystem"
|
|
54
|
+
import * as Layer from "effect/Layer"
|
|
55
|
+
import * as Metric from "effect/Metric"
|
|
56
|
+
import * as Path from "effect/Path"
|
|
57
|
+
import * as PlatformError from "effect/PlatformError"
|
|
58
|
+
import * as RcMap from "effect/RcMap"
|
|
59
|
+
import * as Ref from "effect/Ref"
|
|
60
|
+
import * as Result from "effect/Result"
|
|
61
|
+
import * as Schema from "effect/Schema"
|
|
62
|
+
import * as Scope from "effect/Scope"
|
|
63
|
+
import * as Semaphore from "effect/Semaphore"
|
|
64
|
+
import * as EngineStoreMetrics from "./EngineStoreMetrics.ts"
|
|
65
|
+
import * as FileBoundarySnapshot from "./internal/FileBoundarySnapshot.ts"
|
|
66
|
+
import * as FileEnumeration from "./internal/FileEnumeration.ts"
|
|
67
|
+
import { compareText } from "./internal/Ordering.ts"
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* A protocol-neutral resource named in execution provenance.
|
|
71
|
+
*
|
|
72
|
+
* `kind` leaves room for later database, object-store, or network resources;
|
|
73
|
+
* this implementation records filesystem resources with `kind: "file"`.
|
|
74
|
+
*
|
|
75
|
+
* @category models
|
|
76
|
+
* @since 0.1.0
|
|
77
|
+
*/
|
|
78
|
+
export interface Resource {
|
|
79
|
+
readonly kind: string
|
|
80
|
+
readonly id: string
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* An input value actually consulted by an execution.
|
|
85
|
+
*
|
|
86
|
+
* @category models
|
|
87
|
+
* @since 0.1.0
|
|
88
|
+
*/
|
|
89
|
+
export interface InputObservation {
|
|
90
|
+
readonly resource: Resource
|
|
91
|
+
readonly digest: string
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* An output value actually produced by an execution.
|
|
96
|
+
*
|
|
97
|
+
* @category models
|
|
98
|
+
* @since 0.1.0
|
|
99
|
+
*/
|
|
100
|
+
export interface OutputObservation {
|
|
101
|
+
readonly resource: Resource
|
|
102
|
+
readonly operation: "write" | "remove"
|
|
103
|
+
readonly digest: string | undefined
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Observed data dependencies and products of one isolated execution.
|
|
108
|
+
*
|
|
109
|
+
* @category models
|
|
110
|
+
* @since 0.1.0
|
|
111
|
+
*/
|
|
112
|
+
export interface Provenance {
|
|
113
|
+
readonly baseRevision: string
|
|
114
|
+
readonly inputs: ReadonlyArray<InputObservation>
|
|
115
|
+
readonly outputs: ReadonlyArray<OutputObservation>
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* One functional filesystem transformation.
|
|
120
|
+
*
|
|
121
|
+
* `beforeDigest` is a materialization precondition, never a hint: copy-back is
|
|
122
|
+
* a compare-and-set against it. `afterDigest` is absent for a removal;
|
|
123
|
+
* otherwise it addresses the file's complete post-state. `after` carries those
|
|
124
|
+
* bytes inline when they fit the host's inline bound, and is absent when they
|
|
125
|
+
* were spilled to the content-addressed artifact store under `afterDigest`.
|
|
126
|
+
*
|
|
127
|
+
* @category models
|
|
128
|
+
* @since 0.1.0
|
|
129
|
+
*/
|
|
130
|
+
export interface FileChange {
|
|
131
|
+
readonly path: string
|
|
132
|
+
readonly beforeDigest: string | undefined
|
|
133
|
+
readonly afterDigest: string | undefined
|
|
134
|
+
readonly after?: Uint8Array | undefined
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* An external effect requested by an execution but deliberately **not**
|
|
139
|
+
* dispatched inside its speculative transaction.
|
|
140
|
+
*
|
|
141
|
+
* A sandbox that dispatched these would have already sent the message when its
|
|
142
|
+
* result turned out to be invalid, or would send it twice on a conflict retry.
|
|
143
|
+
* Delivery and idempotency belong to a dispatch stage that runs after
|
|
144
|
+
* copy-back settles; materializing file changes dispatches nothing.
|
|
145
|
+
*
|
|
146
|
+
* @category models
|
|
147
|
+
* @since 0.1.0
|
|
148
|
+
*/
|
|
149
|
+
export interface QueuedEffect {
|
|
150
|
+
readonly protocol: string
|
|
151
|
+
readonly idempotencyKey: string
|
|
152
|
+
readonly payload: unknown
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* The generic functional result of one isolated execution.
|
|
157
|
+
*
|
|
158
|
+
* `output` is the body's ordinary return value and stays schema-typed — the
|
|
159
|
+
* engine persists it as the action's recorded result, so it must be
|
|
160
|
+
* JSON-encodable. It is deliberately not bytes: file products travel in
|
|
161
|
+
* {@link FileChange}, which is content-addressed and can spill to the artifact
|
|
162
|
+
* store, and folding them into the output would defeat both.
|
|
163
|
+
*
|
|
164
|
+
* @category models
|
|
165
|
+
* @since 0.1.0
|
|
166
|
+
*/
|
|
167
|
+
export interface WorkflowResult<out Output = unknown> {
|
|
168
|
+
readonly output: Output
|
|
169
|
+
readonly files: ReadonlyArray<FileChange>
|
|
170
|
+
readonly provenance: Provenance
|
|
171
|
+
readonly effects: ReadonlyArray<QueuedEffect>
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* A filesystem and effect-outbox view scoped to one workspace transaction.
|
|
176
|
+
*
|
|
177
|
+
* A body receives this service only while executing inside
|
|
178
|
+
* {@link Service.execute}; every call mutates the isolated transaction and
|
|
179
|
+
* never the host. It sits beside — and over the same transaction as — the
|
|
180
|
+
* re-rooted Effect `FileSystem` the sandbox also seeds, so a body written
|
|
181
|
+
* against either surface is isolated.
|
|
182
|
+
*
|
|
183
|
+
* @category services
|
|
184
|
+
* @since 0.1.0
|
|
185
|
+
*/
|
|
186
|
+
export interface Workspace {
|
|
187
|
+
readonly readFile: (path: string) => Effect.Effect<Uint8Array, WorkspaceError>
|
|
188
|
+
readonly writeFile: (path: string, content: Uint8Array) => Effect.Effect<void, WorkspaceError>
|
|
189
|
+
readonly removeFile: (path: string) => Effect.Effect<void, WorkspaceError>
|
|
190
|
+
readonly queueEffect: (effect: QueuedEffect) => Effect.Effect<void>
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
/**
|
|
194
|
+
* Context service made available only inside a workspace transaction.
|
|
195
|
+
*
|
|
196
|
+
* @category services
|
|
197
|
+
* @since 0.1.0
|
|
198
|
+
*/
|
|
199
|
+
export const Workspace: Context.Service<Workspace, Workspace> = Context.Service<Workspace>(
|
|
200
|
+
"@smthrs/engine-store/WorkspaceSandbox/Workspace"
|
|
201
|
+
)
|
|
202
|
+
|
|
203
|
+
/**
|
|
204
|
+
* The declaration and body submitted to a workspace sandbox.
|
|
205
|
+
*
|
|
206
|
+
* `descriptor` is the same {@link FileBoundary} the step key was derived from
|
|
207
|
+
* and `StepBoundary` prepares against — an optimistic prediction the sandbox
|
|
208
|
+
* validates against what the body actually did. The proof of concept carried
|
|
209
|
+
* `Effects.Declaration` plus `KeyMaterial` here; on this side the declaration
|
|
210
|
+
* vocabulary is `FileBoundary` and step identity is already the engine's, so
|
|
211
|
+
* `cacheKey` is only an opaque memo address the caller may omit.
|
|
212
|
+
*
|
|
213
|
+
* @category models
|
|
214
|
+
* @since 0.1.0
|
|
215
|
+
*/
|
|
216
|
+
export interface Execution<out Output, out Error> {
|
|
217
|
+
readonly descriptor: FileBoundary
|
|
218
|
+
readonly cacheKey?: string | undefined
|
|
219
|
+
readonly workflow: Effect.Effect<Output, Error, Workspace | FileSystem.FileSystem>
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
/**
|
|
223
|
+
* Why observed execution contradicted its declaration.
|
|
224
|
+
*
|
|
225
|
+
* @category models
|
|
226
|
+
* @since 0.1.0
|
|
227
|
+
*/
|
|
228
|
+
export interface DeclarationViolation {
|
|
229
|
+
readonly kind: "undeclared-read" | "undeclared-write"
|
|
230
|
+
readonly resource: Resource
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
/**
|
|
234
|
+
* Memo outcome of an execution.
|
|
235
|
+
*
|
|
236
|
+
* The memo is a **run-local accelerator**, not the cross-run cache: the engine
|
|
237
|
+
* owns that through `CacheStore` and the step key. `disabled` is what a caller
|
|
238
|
+
* that supplied no `cacheKey` gets, and is the engine's ordinary case.
|
|
239
|
+
*
|
|
240
|
+
* A `hit` hands back the same {@link WorkflowResult} the miss produced rather
|
|
241
|
+
* than a copy of it — the proof of concept deep-cloned, but cloning the output
|
|
242
|
+
* needs `structuredClone`, and this port's output is not constrained to
|
|
243
|
+
* `Schema.Json`. The transaction itself is still isolated (its reads and writes
|
|
244
|
+
* copy bytes in both directions); what a caller must not do is mutate a
|
|
245
|
+
* returned result in place and then ask for it again.
|
|
246
|
+
*
|
|
247
|
+
* @category models
|
|
248
|
+
* @since 0.1.0
|
|
249
|
+
*/
|
|
250
|
+
export type CacheOutcome =
|
|
251
|
+
| { readonly status: "disabled" }
|
|
252
|
+
| { readonly status: "miss"; readonly key: string }
|
|
253
|
+
| { readonly status: "hit"; readonly key: string }
|
|
254
|
+
|
|
255
|
+
/**
|
|
256
|
+
* An execution whose observations matched its declaration.
|
|
257
|
+
*
|
|
258
|
+
* Only this variant is accepted by {@link Service.materialize}, so invalidated
|
|
259
|
+
* speculative changes cannot be applied by accident.
|
|
260
|
+
*
|
|
261
|
+
* @category models
|
|
262
|
+
* @since 0.1.0
|
|
263
|
+
*/
|
|
264
|
+
export interface Accepted<out Output = unknown> {
|
|
265
|
+
readonly _tag: "Accepted"
|
|
266
|
+
readonly result: WorkflowResult<Output>
|
|
267
|
+
readonly cache: CacheOutcome
|
|
268
|
+
/**
|
|
269
|
+
* Deviations the declaration did not predict. Empty in hard mode — that
|
|
270
|
+
* execution is {@link Invalidated} instead — and, in expected mode, the
|
|
271
|
+
* whole-tree evidence behind the deviation the engine journals.
|
|
272
|
+
*
|
|
273
|
+
* The proof of concept had no expected mode and so had no field here. It is
|
|
274
|
+
* carried on the result rather than recomputed by the caller because only
|
|
275
|
+
* the sandbox knows the root the declaration and the observations have to be
|
|
276
|
+
* compared in.
|
|
277
|
+
*/
|
|
278
|
+
readonly violations: ReadonlyArray<DeclarationViolation>
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
/**
|
|
282
|
+
* An execution discarded because its observations contradicted its
|
|
283
|
+
* declaration.
|
|
284
|
+
*
|
|
285
|
+
* The candidate output, file changes, and queued effects are intentionally
|
|
286
|
+
* absent from this shape — there is no accessor that leaks them. Provenance
|
|
287
|
+
* and violations remain so the engine can journal an honest reason and, in
|
|
288
|
+
* expected mode, re-plan.
|
|
289
|
+
*
|
|
290
|
+
* @category models
|
|
291
|
+
* @since 0.1.0
|
|
292
|
+
*/
|
|
293
|
+
export interface Invalidated {
|
|
294
|
+
readonly _tag: "Invalidated"
|
|
295
|
+
readonly provenance: Provenance
|
|
296
|
+
readonly violations: ReadonlyArray<DeclarationViolation>
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
/**
|
|
300
|
+
* The result of validating one speculative execution.
|
|
301
|
+
*
|
|
302
|
+
* @category models
|
|
303
|
+
* @since 0.1.0
|
|
304
|
+
*/
|
|
305
|
+
export type ExecutionResult<Output = unknown> = Accepted<Output> | Invalidated
|
|
306
|
+
|
|
307
|
+
/**
|
|
308
|
+
* Stable workspace sandbox failure codes.
|
|
309
|
+
*
|
|
310
|
+
* @category schemas
|
|
311
|
+
* @since 0.1.0
|
|
312
|
+
*/
|
|
313
|
+
export const WorkspaceErrorCode = Schema.Literals([
|
|
314
|
+
"invalid_path",
|
|
315
|
+
"not_found",
|
|
316
|
+
"host_unavailable",
|
|
317
|
+
"path_escapes_workspace",
|
|
318
|
+
"commit_lock_timeout"
|
|
319
|
+
])
|
|
320
|
+
|
|
321
|
+
/**
|
|
322
|
+
* Stable workspace sandbox failure codes.
|
|
323
|
+
*
|
|
324
|
+
* @category models
|
|
325
|
+
* @since 0.1.0
|
|
326
|
+
*/
|
|
327
|
+
export type WorkspaceErrorCode = typeof WorkspaceErrorCode.Type
|
|
328
|
+
|
|
329
|
+
/**
|
|
330
|
+
* A body could not execute inside — or its result could not be moved through —
|
|
331
|
+
* an isolated workspace.
|
|
332
|
+
*
|
|
333
|
+
* `cause` carries the refusing host or artifact-store failure whole, so a
|
|
334
|
+
* debugger sees the original error instead of a flattened message
|
|
335
|
+
* (`PlatformError`'s own convention).
|
|
336
|
+
*
|
|
337
|
+
* @category errors
|
|
338
|
+
* @since 0.1.0
|
|
339
|
+
*/
|
|
340
|
+
export class WorkspaceError extends Schema.TaggedError<WorkspaceError>()(
|
|
341
|
+
"@smthrs/engine-store/WorkspaceError",
|
|
342
|
+
{
|
|
343
|
+
code: WorkspaceErrorCode,
|
|
344
|
+
message: Schema.String,
|
|
345
|
+
cause: Schema.optional(Schema.Defect())
|
|
346
|
+
}
|
|
347
|
+
) {}
|
|
348
|
+
|
|
349
|
+
/**
|
|
350
|
+
* Copy-back would overwrite host state that changed since the transaction's
|
|
351
|
+
* base snapshot was taken.
|
|
352
|
+
*
|
|
353
|
+
* The engine answers this by retrying the attempt from a fresh base a bounded
|
|
354
|
+
* number of times. This local retry is rebase-shaped. The worktree-lane
|
|
355
|
+
* surface was removed; this error does not recreate it.
|
|
356
|
+
*
|
|
357
|
+
* @category errors
|
|
358
|
+
* @since 0.1.0
|
|
359
|
+
*/
|
|
360
|
+
export class MaterializationConflict extends Schema.TaggedError<MaterializationConflict>()(
|
|
361
|
+
"@smthrs/engine-store/MaterializationConflict",
|
|
362
|
+
{
|
|
363
|
+
paths: Schema.Array(
|
|
364
|
+
Schema.NonEmptyString.check(Schema.isMaxLength(4_096))
|
|
365
|
+
).check(Schema.isMaxLength(1_024)),
|
|
366
|
+
message: Schema.NonEmptyString.check(Schema.isMaxLength(16_384))
|
|
367
|
+
}
|
|
368
|
+
) {}
|
|
369
|
+
|
|
370
|
+
/**
|
|
371
|
+
* Whether a failure is a materialization conflict in live or durable form.
|
|
372
|
+
* Persisted failures retain the schema tag but not the class prototype, so
|
|
373
|
+
* replay classification must recognize both representations.
|
|
374
|
+
*
|
|
375
|
+
* @category guards
|
|
376
|
+
* @since 0.1.0
|
|
377
|
+
*/
|
|
378
|
+
export const isMaterializationConflict = (error: unknown): boolean => {
|
|
379
|
+
if (typeof error !== "object" || error === null) return false
|
|
380
|
+
try {
|
|
381
|
+
if (error instanceof MaterializationConflict) return true
|
|
382
|
+
const own = (name: string): unknown => {
|
|
383
|
+
const descriptor = Object.getOwnPropertyDescriptor(error, name)
|
|
384
|
+
return descriptor !== undefined && descriptor.enumerable && "value" in descriptor
|
|
385
|
+
? descriptor.value
|
|
386
|
+
: undefined
|
|
387
|
+
}
|
|
388
|
+
const paths = own("paths")
|
|
389
|
+
const message = own("message")
|
|
390
|
+
if (
|
|
391
|
+
own("_tag") !== MaterializationConflict.identifier ||
|
|
392
|
+
typeof message !== "string" ||
|
|
393
|
+
message.length === 0 ||
|
|
394
|
+
message.length > 16_384 ||
|
|
395
|
+
!Array.isArray(paths) ||
|
|
396
|
+
Object.getPrototypeOf(paths) !== Array.prototype ||
|
|
397
|
+
paths.length > 1_024
|
|
398
|
+
) return false
|
|
399
|
+
for (let index = 0; index < paths.length; index++) {
|
|
400
|
+
const descriptor = Object.getOwnPropertyDescriptor(paths, String(index))
|
|
401
|
+
if (
|
|
402
|
+
descriptor === undefined ||
|
|
403
|
+
!("value" in descriptor) ||
|
|
404
|
+
!descriptor.enumerable ||
|
|
405
|
+
typeof descriptor.value !== "string" ||
|
|
406
|
+
descriptor.value.length === 0 ||
|
|
407
|
+
descriptor.value.length > 4_096
|
|
408
|
+
) return false
|
|
409
|
+
}
|
|
410
|
+
if (
|
|
411
|
+
Reflect.ownKeys(paths).some((key) =>
|
|
412
|
+
key !== "length" &&
|
|
413
|
+
(typeof key !== "string" || !/^(0|[1-9][0-9]*)$/.test(key) || Number(key) >= paths.length)
|
|
414
|
+
)
|
|
415
|
+
) return false
|
|
416
|
+
return Reflect.ownKeys(error).every((key) => key === "_tag" || key === "paths" || key === "message")
|
|
417
|
+
} catch {
|
|
418
|
+
return false
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
/**
|
|
423
|
+
* The two-phase workspace transaction service.
|
|
424
|
+
*
|
|
425
|
+
* `execute` is speculative and never touches the host. `materialize` is the
|
|
426
|
+
* only host write. See its filesystem failure limits below.
|
|
427
|
+
*
|
|
428
|
+
* @category models
|
|
429
|
+
* @since 0.1.0
|
|
430
|
+
*/
|
|
431
|
+
export interface Service {
|
|
432
|
+
readonly execute: <Output, Error>(
|
|
433
|
+
execution: Execution<Output, Error>
|
|
434
|
+
) => Effect.Effect<ExecutionResult<Output>, Error | WorkspaceError, Crypto.Crypto>
|
|
435
|
+
/**
|
|
436
|
+
* Checks all copy-back preconditions before applying any file change.
|
|
437
|
+
* The filesystem host serializes cooperating commits through confinement,
|
|
438
|
+
* preflight, apply, and rollback. On apply failure it attempts restoration
|
|
439
|
+
* from an in-memory pre-image journal; this is not crash-atomic. A failed
|
|
440
|
+
* rollback returns a compound cause containing both the apply failure and
|
|
441
|
+
* a `WorkspaceError` carrying the rollback cause. The caller must reconcile
|
|
442
|
+
* host files after a crash or failed rollback before resuming work.
|
|
443
|
+
*
|
|
444
|
+
* Filesystem coordination reserves `.smithers-workspace-lock` under the
|
|
445
|
+
* root, and copy-back refuses the `.flows` engine state directory plus any
|
|
446
|
+
* `FileSystemOptions.reservedPaths`, whatever the write set or mode. The
|
|
447
|
+
* lock is an `@smthrs/artifacts/FileLease`: a killed holder's lock is
|
|
448
|
+
* reclaimed once its heartbeat is 60 seconds old, and a commit that cannot
|
|
449
|
+
* take it within 2 minutes fails with `commit_lock_timeout` before changing
|
|
450
|
+
* anything. Writers that ignore the lock are outside this guarantee.
|
|
451
|
+
*/
|
|
452
|
+
readonly materialize: <Output>(
|
|
453
|
+
accepted: Accepted<Output>
|
|
454
|
+
) => Effect.Effect<void, MaterializationConflict | WorkspaceError, Crypto.Crypto>
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
/**
|
|
458
|
+
* Context service for the selected workspace sandbox implementation.
|
|
459
|
+
*
|
|
460
|
+
* @category services
|
|
461
|
+
* @since 0.1.0
|
|
462
|
+
*/
|
|
463
|
+
export const WorkspaceSandbox: Context.Service<Service, Service> = Context.Service<Service>(
|
|
464
|
+
"@smthrs/engine-store/WorkspaceSandbox"
|
|
465
|
+
)
|
|
466
|
+
|
|
467
|
+
/**
|
|
468
|
+
* Constructs a workspace sandbox from an implementation.
|
|
469
|
+
*
|
|
470
|
+
* @category constructors
|
|
471
|
+
* @since 0.1.0
|
|
472
|
+
*/
|
|
473
|
+
export const make = (service: Service): Service => WorkspaceSandbox.of(service)
|
|
474
|
+
|
|
475
|
+
/**
|
|
476
|
+
* Provides a workspace sandbox implementation.
|
|
477
|
+
*
|
|
478
|
+
* @category layers
|
|
479
|
+
* @since 0.1.0
|
|
480
|
+
*/
|
|
481
|
+
export const layer = (service: Service): Layer.Layer<Service> => Layer.succeed(WorkspaceSandbox, make(service))
|
|
482
|
+
|
|
483
|
+
/**
|
|
484
|
+
* Delivery for the effects a transaction queued but refused to send.
|
|
485
|
+
*
|
|
486
|
+
* The engine runs this **after** copy-back settles, never inside the
|
|
487
|
+
* transaction: an effect dispatched speculatively has already reached the
|
|
488
|
+
* world when its execution turns out to be invalid, and has reached it twice
|
|
489
|
+
* when copy-back loses a race and the body re-runs. Deduplication is by
|
|
490
|
+
* {@link QueuedEffect.idempotencyKey}, which is the same key
|
|
491
|
+
* `docs/concepts/workspace-transactions.md` requires before an irreversible
|
|
492
|
+
* effect may be retried at all.
|
|
493
|
+
*
|
|
494
|
+
* @category services
|
|
495
|
+
* @since 0.1.0
|
|
496
|
+
*/
|
|
497
|
+
export interface Dispatcher {
|
|
498
|
+
readonly dispatch: (effect: QueuedEffect) => Effect.Effect<void>
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
/**
|
|
502
|
+
* Context service for the queued-effect dispatch stage. Optional: with no
|
|
503
|
+
* dispatcher provided the engine journals what a transaction queued and sends
|
|
504
|
+
* nothing.
|
|
505
|
+
*
|
|
506
|
+
* @category services
|
|
507
|
+
* @since 0.1.0
|
|
508
|
+
*/
|
|
509
|
+
export const EffectDispatcher: Context.Service<Dispatcher, Dispatcher> = Context.Service<Dispatcher>(
|
|
510
|
+
"@smthrs/engine-store/WorkspaceSandbox/EffectDispatcher"
|
|
511
|
+
)
|
|
512
|
+
|
|
513
|
+
/**
|
|
514
|
+
* Provides a queued-effect dispatch stage.
|
|
515
|
+
*
|
|
516
|
+
* @category layers
|
|
517
|
+
* @since 0.1.0
|
|
518
|
+
*/
|
|
519
|
+
export const layerDispatcher = (dispatcher: Dispatcher): Layer.Layer<Dispatcher> =>
|
|
520
|
+
Layer.succeed(EffectDispatcher, EffectDispatcher.of(dispatcher))
|
|
521
|
+
|
|
522
|
+
// -----------------------------------------------------------------------------
|
|
523
|
+
// path vocabulary
|
|
524
|
+
// -----------------------------------------------------------------------------
|
|
525
|
+
|
|
526
|
+
const decoder = new TextDecoder()
|
|
527
|
+
const encoder = new TextEncoder()
|
|
528
|
+
|
|
529
|
+
const invalidPath = (path: string): WorkspaceError =>
|
|
530
|
+
new WorkspaceError({
|
|
531
|
+
code: "invalid_path",
|
|
532
|
+
message: `Workspace paths must be relative paths inside the workspace without '..': ${path}`
|
|
533
|
+
})
|
|
534
|
+
|
|
535
|
+
/**
|
|
536
|
+
* Normalizes a caller path to a workspace-relative key.
|
|
537
|
+
*
|
|
538
|
+
* Absolute paths are accepted only under `root`, which is how a body that
|
|
539
|
+
* resolved `workspace.root` itself still lands inside the transaction; every
|
|
540
|
+
* other absolute path, and anything traversing upward, is refused rather than
|
|
541
|
+
* silently confined.
|
|
542
|
+
*/
|
|
543
|
+
const normalizePath = (root: string, path: string, allowRoot = false): Result.Result<string, WorkspaceError> => {
|
|
544
|
+
const slashed = path.replaceAll("\\", "/")
|
|
545
|
+
const slashedRoot = root.replaceAll("\\", "/")
|
|
546
|
+
const rooted = slashed === slashedRoot
|
|
547
|
+
? ""
|
|
548
|
+
: slashedRoot !== "" && slashed.startsWith(`${slashedRoot}/`)
|
|
549
|
+
? slashed.slice(slashedRoot.length + 1)
|
|
550
|
+
: slashed
|
|
551
|
+
const parts = rooted.split("/")
|
|
552
|
+
if (rooted.startsWith("/") || parts.some((part) => part === "..")) return Result.fail(invalidPath(path))
|
|
553
|
+
const compact = parts.filter((part) => part !== "" && part !== ".").join("/")
|
|
554
|
+
return compact === "" && !allowRoot ? Result.fail(invalidPath(path)) : Result.succeed(compact)
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
/**
|
|
558
|
+
* Whether a declared write-set entry covers an observed path. A plain string
|
|
559
|
+
* is a literal path, even when it contains `*` or `**`, exactly as
|
|
560
|
+
* `StepBoundary` captures and `FileSet.overlaps` compares it; wildcard
|
|
561
|
+
* declarations are written as a Glob.
|
|
562
|
+
*/
|
|
563
|
+
const covers = (entry: FileSet.Entry, path: string): boolean =>
|
|
564
|
+
typeof entry === "string"
|
|
565
|
+
? entry === path
|
|
566
|
+
: entry._tag === "TreeArtifact"
|
|
567
|
+
? path === entry.path || path.startsWith(`${entry.path}/`)
|
|
568
|
+
: FileSet.matchesGlob(entry, path)
|
|
569
|
+
|
|
570
|
+
const resource = (path: string): Resource => ({ kind: "file", id: path })
|
|
571
|
+
|
|
572
|
+
/**
|
|
573
|
+
* Restates a declaration in the transaction's own path vocabulary.
|
|
574
|
+
*
|
|
575
|
+
* A caller may declare `out/result.txt` or the absolute path the same file has
|
|
576
|
+
* on the host — the kernel-decorated `FileSystem` accepts both, so the
|
|
577
|
+
* declaration does too. Observed paths are always workspace-relative, so the
|
|
578
|
+
* declaration has to be brought to the same footing before anything is
|
|
579
|
+
* compared against it, or an absolute declaration would cover nothing and
|
|
580
|
+
* every write would read as undeclared. An entry that cannot be named inside
|
|
581
|
+
* the workspace is kept verbatim: it covers nothing, which is the honest
|
|
582
|
+
* answer for a declaration pointing outside the tree.
|
|
583
|
+
*/
|
|
584
|
+
const workspaceRelative = (root: string, descriptor: FileBoundary): FileBoundary => {
|
|
585
|
+
const relative = (path: string): string => {
|
|
586
|
+
const normalized = normalizePath(root, path)
|
|
587
|
+
return Result.isFailure(normalized) ? path : normalized.success
|
|
588
|
+
}
|
|
589
|
+
return {
|
|
590
|
+
readSet: descriptor.readSet.map((entry) =>
|
|
591
|
+
FileSet.isGlob(entry)
|
|
592
|
+
? {
|
|
593
|
+
...entry,
|
|
594
|
+
include: [relative(entry.include[0]), ...entry.include.slice(1).map(relative)],
|
|
595
|
+
...(entry.exclude === undefined ? {} : { exclude: entry.exclude.map(relative) })
|
|
596
|
+
}
|
|
597
|
+
: { ...entry, path: relative(entry.path) }
|
|
598
|
+
),
|
|
599
|
+
writeSet: descriptor.writeSet.map((entry) =>
|
|
600
|
+
typeof entry === "string"
|
|
601
|
+
? relative(entry)
|
|
602
|
+
: entry._tag === "TreeArtifact"
|
|
603
|
+
? { ...entry, path: relative(entry.path) }
|
|
604
|
+
: {
|
|
605
|
+
...entry,
|
|
606
|
+
include: [relative(entry.include[0]), ...entry.include.slice(1).map(relative)],
|
|
607
|
+
...(entry.exclude === undefined ? {} : { exclude: entry.exclude.map(relative) })
|
|
608
|
+
}
|
|
609
|
+
),
|
|
610
|
+
...(descriptor.removes === undefined ? {} : { removes: descriptor.removes.map(relative) }),
|
|
611
|
+
boundaryMode: descriptor.boundaryMode
|
|
612
|
+
}
|
|
613
|
+
}
|
|
614
|
+
|
|
615
|
+
const digestOf = (bytes: Uint8Array): Effect.Effect<string, never, Crypto.Crypto> =>
|
|
616
|
+
Schema.decodeUnknownEffect(Sha256)(bytes).pipe(Effect.orDie)
|
|
617
|
+
|
|
618
|
+
const parentDirectory = (path: string): string | undefined => {
|
|
619
|
+
const index = path.lastIndexOf("/")
|
|
620
|
+
return index <= 0 ? undefined : path.slice(0, index)
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
// -----------------------------------------------------------------------------
|
|
624
|
+
// the host seam
|
|
625
|
+
// -----------------------------------------------------------------------------
|
|
626
|
+
|
|
627
|
+
/**
|
|
628
|
+
* Everything a workspace sandbox needs from the world it isolates bodies from.
|
|
629
|
+
*
|
|
630
|
+
* Splitting it out is what lets one transaction implementation serve both an
|
|
631
|
+
* in-memory host (tests, browsers, and the conformance suite) and a real
|
|
632
|
+
* filesystem host: the transaction, the diff, the violation check, and the
|
|
633
|
+
* provenance are identical, and only *where the base comes from* and *where
|
|
634
|
+
* copy-back lands* differ.
|
|
635
|
+
*
|
|
636
|
+
* @category models
|
|
637
|
+
* @since 0.1.0
|
|
638
|
+
*/
|
|
639
|
+
export interface Host {
|
|
640
|
+
/**
|
|
641
|
+
* The transaction's base. A host that can seed exactly the declared read set
|
|
642
|
+
* does so — a body then cannot read an undeclared file because it is not
|
|
643
|
+
* there, which is the strong tier of `Effect Taxonomy.md`. A host that hands
|
|
644
|
+
* back a wider tree gets the same guarantees post hoc, through the
|
|
645
|
+
* undeclared-read check.
|
|
646
|
+
*/
|
|
647
|
+
readonly snapshot: (
|
|
648
|
+
descriptor: FileBoundary
|
|
649
|
+
) => Effect.Effect<ReadonlyMap<string, Uint8Array>, WorkspaceError, Crypto.Crypto>
|
|
650
|
+
/**
|
|
651
|
+
* The host's current digest for a path the transaction never observed, or
|
|
652
|
+
* `undefined` when the host holds nothing there.
|
|
653
|
+
*
|
|
654
|
+
* A host that seeds only the declared read set does not know what else is on
|
|
655
|
+
* disk, and "absent from the seed" is emphatically **not** "absent from the
|
|
656
|
+
* host": a body writing a declared output it never declared as a read is the
|
|
657
|
+
* ordinary case, and that file usually already exists from a previous run.
|
|
658
|
+
* Treating the seed as the whole world made every such copy-back a
|
|
659
|
+
* {@link MaterializationConflict} the engine could only rebase into the same
|
|
660
|
+
* refusal, and made an identical rewrite look like a change.
|
|
661
|
+
*
|
|
662
|
+
* The digest this returns is therefore the change's `beforeDigest`. A blind
|
|
663
|
+
* write cannot carry a compare-and-set over the body's whole execution — the
|
|
664
|
+
* body never read the prior content, so there is nothing it could have
|
|
665
|
+
* depended on — but copy-back still refuses if the path moves again between
|
|
666
|
+
* the diff and the commit.
|
|
667
|
+
*/
|
|
668
|
+
readonly baseline: (
|
|
669
|
+
path: string
|
|
670
|
+
) => Effect.Effect<string | undefined, WorkspaceError, Crypto.Crypto>
|
|
671
|
+
/**
|
|
672
|
+
* Decides whether a produced file's bytes travel inline in its
|
|
673
|
+
* {@link FileChange} or by content address. Returning `undefined` means the
|
|
674
|
+
* bytes were retained under the change's `afterDigest` and must be fetched
|
|
675
|
+
* back at copy-back.
|
|
676
|
+
*/
|
|
677
|
+
readonly retain: (
|
|
678
|
+
bytes: Uint8Array
|
|
679
|
+
) => Effect.Effect<Uint8Array | undefined, WorkspaceError, Crypto.Crypto>
|
|
680
|
+
/**
|
|
681
|
+
* Applies the diff as a compare-and-set on every `beforeDigest`. Hosts must
|
|
682
|
+
* serialize preflight through apply and rollback with competing commits.
|
|
683
|
+
* Check every precondition before changing files; on apply failure attempt
|
|
684
|
+
* to restore every touched path and preserve both causes if rollback fails.
|
|
685
|
+
* Crash recovery and host reconciliation remain the caller's responsibility.
|
|
686
|
+
*/
|
|
687
|
+
readonly commit: (
|
|
688
|
+
changes: ReadonlyArray<FileChange>
|
|
689
|
+
) => Effect.Effect<void, MaterializationConflict | WorkspaceError, Crypto.Crypto>
|
|
690
|
+
/** The absolute root absolute host paths are interpreted against. */
|
|
691
|
+
readonly root: string
|
|
692
|
+
}
|
|
693
|
+
|
|
694
|
+
// -----------------------------------------------------------------------------
|
|
695
|
+
// the transaction
|
|
696
|
+
// -----------------------------------------------------------------------------
|
|
697
|
+
|
|
698
|
+
/**
|
|
699
|
+
* Reads are recorded as bytes and hashed once, after the body returns.
|
|
700
|
+
* Digesting inside `readFile` would put `Crypto` in the error-free signature
|
|
701
|
+
* Effect's own `FileSystem` fixes, and a body must see the tag's real shape.
|
|
702
|
+
*/
|
|
703
|
+
interface Trace {
|
|
704
|
+
readonly inputs: Array<{ readonly path: string; readonly content: Uint8Array }>
|
|
705
|
+
readonly attemptedReads: Array<{ readonly path: string; readonly produced: boolean }>
|
|
706
|
+
readonly effects: Array<QueuedEffect>
|
|
707
|
+
}
|
|
708
|
+
|
|
709
|
+
const notFound = (path: string): WorkspaceError =>
|
|
710
|
+
new WorkspaceError({ code: "not_found", message: `Workspace file does not exist: ${path}` })
|
|
711
|
+
|
|
712
|
+
/**
|
|
713
|
+
* The transaction surfaces: the {@link Workspace} tag and a re-rooted Effect
|
|
714
|
+
* `FileSystem`, both over one path-keyed map.
|
|
715
|
+
*
|
|
716
|
+
* The `FileSystem` is built from `makeNoop` on purpose. Only the operations
|
|
717
|
+
* listed here are meaningful over a functional map, and a body reaching for
|
|
718
|
+
* one that is not — a temp file, a watch, a stream — gets the platform's own
|
|
719
|
+
* refusal rather than a plausible lie. Widening the surface is a deliberate
|
|
720
|
+
* change to this list, never a silent default.
|
|
721
|
+
*/
|
|
722
|
+
const transaction = (base: ReadonlyMap<string, Uint8Array>, trace: Trace, root: string) => {
|
|
723
|
+
const files = new Map(base)
|
|
724
|
+
const produced = new Set<string>()
|
|
725
|
+
const resolvePath = (path: string, allowRoot = false): Effect.Effect<string, WorkspaceError> => {
|
|
726
|
+
const normalized = normalizePath(root, path, allowRoot)
|
|
727
|
+
return Result.isFailure(normalized) ? Effect.fail(normalized.failure) : Effect.succeed(normalized.success)
|
|
728
|
+
}
|
|
729
|
+
const readBytes = Effect.fn("WorkspaceSandbox.read")(function*(path: string) {
|
|
730
|
+
const key = yield* resolvePath(path)
|
|
731
|
+
trace.attemptedReads.push({ path: key, produced: produced.has(key) })
|
|
732
|
+
const content = files.get(key)
|
|
733
|
+
if (content === undefined) return yield* Effect.fail(notFound(key))
|
|
734
|
+
trace.inputs.push({ path: key, content })
|
|
735
|
+
return content.slice()
|
|
736
|
+
})
|
|
737
|
+
const writeBytes = Effect.fn("WorkspaceSandbox.write")(function*(path: string, content: Uint8Array) {
|
|
738
|
+
const key = yield* resolvePath(path)
|
|
739
|
+
produced.add(key)
|
|
740
|
+
files.set(key, content.slice())
|
|
741
|
+
})
|
|
742
|
+
const removePath = Effect.fn("WorkspaceSandbox.remove")(function*(path: string) {
|
|
743
|
+
const key = yield* resolvePath(path)
|
|
744
|
+
produced.add(key)
|
|
745
|
+
files.delete(key)
|
|
746
|
+
})
|
|
747
|
+
const workspace = Workspace.of({
|
|
748
|
+
readFile: (path) => readBytes(path),
|
|
749
|
+
writeFile: (path, content) => writeBytes(path, content),
|
|
750
|
+
removeFile: (path) => removePath(path),
|
|
751
|
+
queueEffect: (effect) => Effect.sync(() => trace.effects.push(effect))
|
|
752
|
+
})
|
|
753
|
+
const refuse = (method: string, path: string) => (error: WorkspaceError) =>
|
|
754
|
+
error.code === "not_found"
|
|
755
|
+
? PlatformError.systemError({
|
|
756
|
+
_tag: "NotFound",
|
|
757
|
+
module: "FileSystem",
|
|
758
|
+
method,
|
|
759
|
+
description: error.message,
|
|
760
|
+
pathOrDescriptor: path
|
|
761
|
+
})
|
|
762
|
+
: PlatformError.badArgument({ module: "FileSystem", method, description: error.message })
|
|
763
|
+
const fileSystem = FileSystem.makeNoop({
|
|
764
|
+
// `exists` and `readDirectory` trace every PRESENCE they reveal: under a
|
|
765
|
+
// whole-tree-seeding host the map holds undeclared world state, and a
|
|
766
|
+
// probe that observes it is a read the key never folded. Absence stays
|
|
767
|
+
// untraced — under the declared-set host an undeclared path is absent by
|
|
768
|
+
// construction, the Bazel forest's own deterministic answer.
|
|
769
|
+
exists: (path) =>
|
|
770
|
+
resolvePath(path, true).pipe(
|
|
771
|
+
Effect.map((key) => {
|
|
772
|
+
if (key === "") return true
|
|
773
|
+
if (files.has(key)) {
|
|
774
|
+
trace.attemptedReads.push({ path: key, produced: produced.has(key) })
|
|
775
|
+
return true
|
|
776
|
+
}
|
|
777
|
+
const prefix = `${key}/`
|
|
778
|
+
let present = false
|
|
779
|
+
for (const candidate of files.keys()) {
|
|
780
|
+
if (!candidate.startsWith(prefix)) continue
|
|
781
|
+
trace.attemptedReads.push({ path: candidate, produced: produced.has(candidate) })
|
|
782
|
+
present = true
|
|
783
|
+
}
|
|
784
|
+
return present
|
|
785
|
+
}),
|
|
786
|
+
// A path the transaction cannot even name does not exist in it; that
|
|
787
|
+
// is an answer, not a host failure.
|
|
788
|
+
Effect.catch(() => Effect.succeed(false))
|
|
789
|
+
),
|
|
790
|
+
readFile: (path) => readBytes(path).pipe(Effect.mapError(refuse("readFile", path))),
|
|
791
|
+
readFileString: (path) =>
|
|
792
|
+
readBytes(path).pipe(
|
|
793
|
+
Effect.map((bytes) => decoder.decode(bytes)),
|
|
794
|
+
Effect.mapError(refuse("readFileString", path))
|
|
795
|
+
),
|
|
796
|
+
writeFile: (path, data) => writeBytes(path, data).pipe(Effect.mapError(refuse("writeFile", path))),
|
|
797
|
+
writeFileString: (path, data) =>
|
|
798
|
+
writeBytes(path, encoder.encode(data)).pipe(Effect.mapError(refuse("writeFileString", path))),
|
|
799
|
+
remove: (path) => removePath(path).pipe(Effect.mapError(refuse("remove", path))),
|
|
800
|
+
// Directories are implicit in a path-keyed map: creating one is a
|
|
801
|
+
// successful no-op so a body that mirrors ordinary filesystem discipline
|
|
802
|
+
// works unchanged, and listing one is a prefix scan.
|
|
803
|
+
makeDirectory: () => Effect.void,
|
|
804
|
+
readDirectory: (path) =>
|
|
805
|
+
resolvePath(path, true).pipe(
|
|
806
|
+
Effect.map((key) => {
|
|
807
|
+
const prefix = key === "" ? "" : `${key}/`
|
|
808
|
+
const entries = new Set<string>()
|
|
809
|
+
for (const candidate of files.keys()) {
|
|
810
|
+
if (!candidate.startsWith(prefix)) continue
|
|
811
|
+
// A listing reveals each file it names, so each is a traced read.
|
|
812
|
+
trace.attemptedReads.push({ path: candidate, produced: produced.has(candidate) })
|
|
813
|
+
entries.add(candidate.slice(prefix.length).split("/")[0]!)
|
|
814
|
+
}
|
|
815
|
+
return [...entries].sort(compareText)
|
|
816
|
+
}),
|
|
817
|
+
Effect.mapError(refuse("readDirectory", path))
|
|
818
|
+
)
|
|
819
|
+
})
|
|
820
|
+
return { files, workspace, fileSystem }
|
|
821
|
+
}
|
|
822
|
+
|
|
823
|
+
const changes = Effect.fn("WorkspaceSandbox.changes")(function*(
|
|
824
|
+
base: ReadonlyMap<string, Uint8Array>,
|
|
825
|
+
current: ReadonlyMap<string, Uint8Array>,
|
|
826
|
+
host: Host
|
|
827
|
+
) {
|
|
828
|
+
const paths = [...new Set([...base.keys(), ...current.keys()])].sort(compareText)
|
|
829
|
+
const result: Array<FileChange> = []
|
|
830
|
+
for (const path of paths) {
|
|
831
|
+
const before = base.get(path)
|
|
832
|
+
const after = current.get(path)
|
|
833
|
+
// A path the transaction never observed is not a path the host lacks: ask
|
|
834
|
+
// the host what is there before claiming the change creates it.
|
|
835
|
+
const beforeDigest = before === undefined ? yield* host.baseline(path) : yield* digestOf(before)
|
|
836
|
+
const afterDigest = after === undefined ? undefined : yield* digestOf(after)
|
|
837
|
+
if (beforeDigest === afterDigest) continue
|
|
838
|
+
result.push({
|
|
839
|
+
path,
|
|
840
|
+
beforeDigest,
|
|
841
|
+
afterDigest,
|
|
842
|
+
...(after === undefined ? {} : { after: yield* host.retain(after) })
|
|
843
|
+
})
|
|
844
|
+
}
|
|
845
|
+
return result
|
|
846
|
+
})
|
|
847
|
+
|
|
848
|
+
/**
|
|
849
|
+
* The compare-and-set every copy-back runs before it writes a single byte,
|
|
850
|
+
* capturing each target's pre-image while it looks.
|
|
851
|
+
*
|
|
852
|
+
* Both hosts share it, so "all-or-nothing" cannot drift between the in-memory
|
|
853
|
+
* conformance implementation and the filesystem one: a bundle whose base moved
|
|
854
|
+
* is refused whole, and the tree is left exactly as it was found. The captured
|
|
855
|
+
* pre-images are the rollback journal the filesystem host restores from when a
|
|
856
|
+
* mid-apply refusal would otherwise strand a half-written tree.
|
|
857
|
+
*/
|
|
858
|
+
const preflight = Effect.fn("WorkspaceSandbox.preflight")(function*(
|
|
859
|
+
changes: ReadonlyArray<FileChange>,
|
|
860
|
+
current: (path: string) => Effect.Effect<Uint8Array | undefined, WorkspaceError, Crypto.Crypto>
|
|
861
|
+
) {
|
|
862
|
+
const conflicting: Array<string> = []
|
|
863
|
+
const before = new Map<string, Uint8Array | undefined>()
|
|
864
|
+
for (const change of changes) {
|
|
865
|
+
const content = yield* current(change.path)
|
|
866
|
+
before.set(change.path, content)
|
|
867
|
+
const digest = content === undefined ? undefined : yield* digestOf(content)
|
|
868
|
+
conflicting.push(...(digest === change.beforeDigest ? [] : [change.path]))
|
|
869
|
+
}
|
|
870
|
+
const conflict = conflicting.length === 0 ? undefined : new MaterializationConflict({
|
|
871
|
+
paths: conflicting,
|
|
872
|
+
message: "Workspace state changed after the transaction's base snapshot was taken"
|
|
873
|
+
})
|
|
874
|
+
return { conflict, before }
|
|
875
|
+
})
|
|
876
|
+
|
|
877
|
+
const revisionOf = Effect.fn("WorkspaceSandbox.revision")(function*(base: ReadonlyMap<string, Uint8Array>) {
|
|
878
|
+
const entries: Array<{ readonly path: string; readonly digest: string }> = []
|
|
879
|
+
for (const [path, content] of [...base].sort((left, right) => compareText(left[0], right[0]))) {
|
|
880
|
+
entries.push({ path, digest: yield* digestOf(content) })
|
|
881
|
+
}
|
|
882
|
+
return yield* Schema.decodeUnknownEffect(DerivedKey)({ kind: "workspace-revision", entries }).pipe(Effect.orDie)
|
|
883
|
+
})
|
|
884
|
+
|
|
885
|
+
/**
|
|
886
|
+
* Everything the declaration failed to predict, deduplicated.
|
|
887
|
+
*
|
|
888
|
+
* Reads and writes are checked against the declaration's exact paths and patterns; writes
|
|
889
|
+
* against the declared write set's patterns. A read of a file the body itself
|
|
890
|
+
* produced is not undeclared — the transaction, not the host, supplied it.
|
|
891
|
+
*
|
|
892
|
+
* @category accessors
|
|
893
|
+
* @since 0.1.0
|
|
894
|
+
*/
|
|
895
|
+
export const violations = (
|
|
896
|
+
descriptor: FileBoundary,
|
|
897
|
+
base: ReadonlyMap<string, Uint8Array>,
|
|
898
|
+
provenance: Provenance
|
|
899
|
+
): ReadonlyArray<DeclarationViolation> => {
|
|
900
|
+
const declaredReads = descriptor.readSet
|
|
901
|
+
const observed: Array<DeclarationViolation> = [
|
|
902
|
+
...provenance.inputs
|
|
903
|
+
.filter((input) =>
|
|
904
|
+
base.has(input.resource.id) &&
|
|
905
|
+
!declaredReads.some((entry) =>
|
|
906
|
+
FileSet.isGlob(entry) ? FileSet.matchesGlob(entry, input.resource.id) : entry.path === input.resource.id
|
|
907
|
+
)
|
|
908
|
+
)
|
|
909
|
+
.map((input): DeclarationViolation => ({ kind: "undeclared-read", resource: input.resource })),
|
|
910
|
+
...provenance.outputs
|
|
911
|
+
.filter((output) =>
|
|
912
|
+
![...descriptor.writeSet, ...descriptor.removes ?? []].some((entry) => covers(entry, output.resource.id))
|
|
913
|
+
)
|
|
914
|
+
.map((output): DeclarationViolation => ({ kind: "undeclared-write", resource: output.resource }))
|
|
915
|
+
]
|
|
916
|
+
return [
|
|
917
|
+
...new Map(observed.map((violation) => [`${violation.kind}:${violation.resource.id}`, violation])).values()
|
|
918
|
+
]
|
|
919
|
+
}
|
|
920
|
+
|
|
921
|
+
/**
|
|
922
|
+
* Builds a workspace sandbox over a {@link Host}.
|
|
923
|
+
*
|
|
924
|
+
* @category constructors
|
|
925
|
+
* @since 0.1.0
|
|
926
|
+
*/
|
|
927
|
+
export const makeHosted = (host: Host): Service => {
|
|
928
|
+
const memo = new Map<
|
|
929
|
+
string,
|
|
930
|
+
{ readonly result: WorkflowResult<never>; readonly violations: ReadonlyArray<DeclarationViolation> }
|
|
931
|
+
>()
|
|
932
|
+
/**
|
|
933
|
+
* The speculative half, untraced: the `execute` span below wraps it, so
|
|
934
|
+
* the transaction body reports under one span with its metrics.
|
|
935
|
+
*/
|
|
936
|
+
const executeBody = Effect.fnUntraced(function*<Output, Error>(
|
|
937
|
+
execution: Execution<Output, Error>
|
|
938
|
+
) {
|
|
939
|
+
const boundary = FileBoundarySnapshot.make(execution.descriptor)
|
|
940
|
+
const key = execution.cacheKey
|
|
941
|
+
if (key !== undefined) {
|
|
942
|
+
const hit = memo.get(key)
|
|
943
|
+
if (hit !== undefined) {
|
|
944
|
+
return {
|
|
945
|
+
_tag: "Accepted" as const,
|
|
946
|
+
result: hit.result as unknown as WorkflowResult<Output>,
|
|
947
|
+
violations: hit.violations,
|
|
948
|
+
cache: { status: "hit" as const, key }
|
|
949
|
+
}
|
|
950
|
+
}
|
|
951
|
+
}
|
|
952
|
+
const captured = yield* host.snapshot(boundary)
|
|
953
|
+
// The host owns its returned buffers. Preserve the instant represented by
|
|
954
|
+
// snapshot() even if that host or another fiber mutates them while the
|
|
955
|
+
// workflow is suspended.
|
|
956
|
+
const base = new Map([...captured].map(([path, content]) => [path, content.slice()]))
|
|
957
|
+
const trace: Trace = { inputs: [], attemptedReads: [], effects: [] }
|
|
958
|
+
const isolated = transaction(base, trace, host.root)
|
|
959
|
+
const outcome = yield* execution.workflow.pipe(
|
|
960
|
+
Effect.provideService(Workspace, isolated.workspace),
|
|
961
|
+
Effect.provideService(FileSystem.FileSystem, isolated.fileSystem),
|
|
962
|
+
Effect.exit
|
|
963
|
+
)
|
|
964
|
+
const inputs: Array<InputObservation> = []
|
|
965
|
+
for (const input of trace.inputs) {
|
|
966
|
+
inputs.push({ resource: resource(input.path), digest: yield* digestOf(input.content) })
|
|
967
|
+
}
|
|
968
|
+
const descriptor = workspaceRelative(host.root, boundary)
|
|
969
|
+
const undeclaredReads = trace.attemptedReads.filter((attempt) =>
|
|
970
|
+
!attempt.produced &&
|
|
971
|
+
!descriptor.readSet.some((entry) =>
|
|
972
|
+
FileSet.isGlob(entry) ? FileSet.matchesGlob(entry, attempt.path) : entry.path === attempt.path
|
|
973
|
+
)
|
|
974
|
+
)
|
|
975
|
+
if (Exit.isFailure(outcome)) {
|
|
976
|
+
// Hard mode only, mirroring the success path below: in expected mode
|
|
977
|
+
// an undeclared read is a deviation the engine journals, and a body's
|
|
978
|
+
// own typed failure must surface as itself — converting it into an
|
|
979
|
+
// isolation verdict would both harden a soft boundary and mask the
|
|
980
|
+
// error the retry policy classifies on.
|
|
981
|
+
if (undeclaredReads.length > 0 && boundary.boundaryMode === "hard") {
|
|
982
|
+
return {
|
|
983
|
+
_tag: "Invalidated" as const,
|
|
984
|
+
provenance: {
|
|
985
|
+
baseRevision: yield* revisionOf(base),
|
|
986
|
+
inputs,
|
|
987
|
+
outputs: []
|
|
988
|
+
},
|
|
989
|
+
violations: [
|
|
990
|
+
...new Map(undeclaredReads.map((attempt) => [attempt.path, {
|
|
991
|
+
kind: "undeclared-read" as const,
|
|
992
|
+
resource: resource(attempt.path)
|
|
993
|
+
}])).values()
|
|
994
|
+
]
|
|
995
|
+
}
|
|
996
|
+
}
|
|
997
|
+
return yield* Effect.failCause(outcome.cause)
|
|
998
|
+
}
|
|
999
|
+
const output = outcome.value
|
|
1000
|
+
const files = yield* changes(base, isolated.files, host)
|
|
1001
|
+
const provenance: Provenance = {
|
|
1002
|
+
baseRevision: yield* revisionOf(base),
|
|
1003
|
+
inputs,
|
|
1004
|
+
outputs: files.map((change) => ({
|
|
1005
|
+
resource: resource(change.path),
|
|
1006
|
+
operation: change.afterDigest === undefined ? "remove" as const : "write" as const,
|
|
1007
|
+
digest: change.afterDigest
|
|
1008
|
+
}))
|
|
1009
|
+
}
|
|
1010
|
+
const invalid = [
|
|
1011
|
+
...new Map([
|
|
1012
|
+
...violations(descriptor, base, provenance),
|
|
1013
|
+
...undeclaredReads.map((attempt) => ({
|
|
1014
|
+
kind: "undeclared-read" as const,
|
|
1015
|
+
resource: resource(attempt.path)
|
|
1016
|
+
}))
|
|
1017
|
+
].map((violation) => [`${violation.kind}:${violation.resource.id}`, violation])).values()
|
|
1018
|
+
]
|
|
1019
|
+
// Hard mode discards; expected mode admits the result and leaves the
|
|
1020
|
+
// deviation for the engine to journal and reconcile
|
|
1021
|
+
// (`Effect Taxonomy.md`, "Expected sets — the soft mode"). Either way
|
|
1022
|
+
// the host has not been touched.
|
|
1023
|
+
if (invalid.length > 0 && boundary.boundaryMode === "hard") {
|
|
1024
|
+
return { _tag: "Invalidated" as const, provenance, violations: invalid }
|
|
1025
|
+
}
|
|
1026
|
+
const result: WorkflowResult<Output> = { output, files, provenance, effects: trace.effects }
|
|
1027
|
+
if (key !== undefined) {
|
|
1028
|
+
memo.set(key, { result: result as unknown as WorkflowResult<never>, violations: invalid })
|
|
1029
|
+
}
|
|
1030
|
+
return {
|
|
1031
|
+
_tag: "Accepted" as const,
|
|
1032
|
+
result,
|
|
1033
|
+
violations: invalid,
|
|
1034
|
+
cache: key === undefined ? { status: "disabled" as const } : { status: "miss" as const, key }
|
|
1035
|
+
}
|
|
1036
|
+
})
|
|
1037
|
+
return make({
|
|
1038
|
+
execute: Effect.fn("WorkspaceSandbox.execute")(
|
|
1039
|
+
function*<Output, Error>(
|
|
1040
|
+
execution: Execution<Output, Error>
|
|
1041
|
+
) {
|
|
1042
|
+
yield* Effect.annotateCurrentSpan({ boundaryMode: execution.descriptor.boundaryMode })
|
|
1043
|
+
return yield* executeBody(execution)
|
|
1044
|
+
},
|
|
1045
|
+
(effect) =>
|
|
1046
|
+
EngineStoreMetrics.observe({
|
|
1047
|
+
timer: EngineStoreMetrics.sandboxExecutionDuration,
|
|
1048
|
+
counter: EngineStoreMetrics.sandboxExecution
|
|
1049
|
+
})(effect)
|
|
1050
|
+
),
|
|
1051
|
+
materialize: Effect.fn("WorkspaceSandbox.materialize")(
|
|
1052
|
+
function*<Output>(accepted: Accepted<Output>) {
|
|
1053
|
+
yield* Effect.annotateCurrentSpan({ changes: accepted.result.files.length })
|
|
1054
|
+
// Nothing to copy back needs no commit: a body that changed nothing
|
|
1055
|
+
// (a sealed read) must not need write authority over the root.
|
|
1056
|
+
if (accepted.result.files.length === 0) return
|
|
1057
|
+
yield* host.commit(accepted.result.files)
|
|
1058
|
+
},
|
|
1059
|
+
(effect) =>
|
|
1060
|
+
EngineStoreMetrics.observe({
|
|
1061
|
+
timer: EngineStoreMetrics.materializationDuration,
|
|
1062
|
+
counter: EngineStoreMetrics.materialization
|
|
1063
|
+
})(effect)
|
|
1064
|
+
)
|
|
1065
|
+
})
|
|
1066
|
+
}
|
|
1067
|
+
|
|
1068
|
+
// -----------------------------------------------------------------------------
|
|
1069
|
+
// the in-memory host
|
|
1070
|
+
// -----------------------------------------------------------------------------
|
|
1071
|
+
|
|
1072
|
+
/**
|
|
1073
|
+
* Initial file contents accepted by {@link makeMemory}.
|
|
1074
|
+
*
|
|
1075
|
+
* @category models
|
|
1076
|
+
* @since 0.1.0
|
|
1077
|
+
*/
|
|
1078
|
+
export type InitialFiles = Readonly<Record<string, string | Uint8Array>>
|
|
1079
|
+
|
|
1080
|
+
/**
|
|
1081
|
+
* A host file snapshot exposed by the deterministic in-memory implementation.
|
|
1082
|
+
*
|
|
1083
|
+
* @category models
|
|
1084
|
+
* @since 0.1.0
|
|
1085
|
+
*/
|
|
1086
|
+
export interface HostFile {
|
|
1087
|
+
readonly path: string
|
|
1088
|
+
readonly content: Uint8Array
|
|
1089
|
+
}
|
|
1090
|
+
|
|
1091
|
+
/**
|
|
1092
|
+
* The deterministic in-memory sandbox plus read-only test observations.
|
|
1093
|
+
*
|
|
1094
|
+
* @category models
|
|
1095
|
+
* @since 0.1.0
|
|
1096
|
+
*/
|
|
1097
|
+
export interface MemorySandbox {
|
|
1098
|
+
readonly service: Service
|
|
1099
|
+
readonly files: Effect.Effect<ReadonlyArray<HostFile>>
|
|
1100
|
+
}
|
|
1101
|
+
|
|
1102
|
+
/**
|
|
1103
|
+
* Creates a deterministic in-memory workspace sandbox.
|
|
1104
|
+
*
|
|
1105
|
+
* The returned `files` effect observes *host* state, not a running
|
|
1106
|
+
* transaction: it changes only through `service.materialize`. This host hands
|
|
1107
|
+
* the transaction its whole tree rather than seeding the declared read set, so
|
|
1108
|
+
* an undeclared read is observable — which is exactly what makes it the
|
|
1109
|
+
* conformance implementation for the bare tier of `Effect Taxonomy.md`.
|
|
1110
|
+
*
|
|
1111
|
+
* @category constructors
|
|
1112
|
+
* @since 0.1.0
|
|
1113
|
+
*/
|
|
1114
|
+
export const makeMemory = (
|
|
1115
|
+
initialFiles: InitialFiles = {}
|
|
1116
|
+
): Effect.Effect<MemorySandbox, WorkspaceError, Crypto.Crypto> =>
|
|
1117
|
+
Effect.gen(function*() {
|
|
1118
|
+
const entries: Array<readonly [string, Uint8Array]> = []
|
|
1119
|
+
for (const [path, content] of Object.entries(initialFiles)) {
|
|
1120
|
+
const normalized = normalizePath("", path)
|
|
1121
|
+
if (Result.isFailure(normalized)) return yield* Effect.fail(normalized.failure)
|
|
1122
|
+
entries.push([normalized.success, typeof content === "string" ? encoder.encode(content) : content.slice()])
|
|
1123
|
+
}
|
|
1124
|
+
const host = yield* Ref.make<ReadonlyMap<string, Uint8Array>>(new Map(entries))
|
|
1125
|
+
const service = makeHosted({
|
|
1126
|
+
root: "",
|
|
1127
|
+
snapshot: () => Ref.get(host),
|
|
1128
|
+
// This host seeds the transaction with the WHOLE tree, so a path the
|
|
1129
|
+
// transaction did not observe is one the host genuinely did not hold:
|
|
1130
|
+
// `undefined` is the honest snapshot-time answer, and a file that
|
|
1131
|
+
// appeared underneath the transaction is caught by copy-back's
|
|
1132
|
+
// compare-and-set rather than clobbered.
|
|
1133
|
+
baseline: () => Effect.succeed(undefined),
|
|
1134
|
+
retain: (bytes) => Effect.succeed(bytes.slice()),
|
|
1135
|
+
commit: Effect.fn("WorkspaceSandbox.commit")(function*(changes) {
|
|
1136
|
+
yield* Effect.annotateCurrentSpan({ changes: changes.length })
|
|
1137
|
+
const current = yield* Ref.get(host)
|
|
1138
|
+
const { conflict } = yield* preflight(changes, (path) => Effect.succeed(current.get(path)))
|
|
1139
|
+
if (conflict !== undefined) {
|
|
1140
|
+
yield* Metric.update(EngineStoreMetrics.materializationConflicts, 1)
|
|
1141
|
+
return yield* Effect.fail(conflict)
|
|
1142
|
+
}
|
|
1143
|
+
const next = new Map(current)
|
|
1144
|
+
for (const change of changes) {
|
|
1145
|
+
if (change.afterDigest === undefined) next.delete(change.path)
|
|
1146
|
+
else next.set(change.path, change.after!.slice())
|
|
1147
|
+
}
|
|
1148
|
+
yield* Ref.set(host, next)
|
|
1149
|
+
})
|
|
1150
|
+
})
|
|
1151
|
+
return {
|
|
1152
|
+
service,
|
|
1153
|
+
files: Ref.get(host).pipe(
|
|
1154
|
+
Effect.map((current) =>
|
|
1155
|
+
[...current]
|
|
1156
|
+
.map(([path, content]) => ({ path, content }))
|
|
1157
|
+
.sort((left, right) => compareText(left.path, right.path))
|
|
1158
|
+
)
|
|
1159
|
+
)
|
|
1160
|
+
}
|
|
1161
|
+
})
|
|
1162
|
+
|
|
1163
|
+
// -----------------------------------------------------------------------------
|
|
1164
|
+
// the filesystem host
|
|
1165
|
+
// -----------------------------------------------------------------------------
|
|
1166
|
+
|
|
1167
|
+
/**
|
|
1168
|
+
* Filesystem workspace sandbox options.
|
|
1169
|
+
*
|
|
1170
|
+
* @category models
|
|
1171
|
+
* @since 0.1.0
|
|
1172
|
+
*/
|
|
1173
|
+
export interface FileSystemOptions {
|
|
1174
|
+
/**
|
|
1175
|
+
* The largest produced file (in bytes) carried inline in its
|
|
1176
|
+
* {@link FileChange}. Anything larger is retained in the content-addressed
|
|
1177
|
+
* artifact store and fetched back at copy-back. Defaults to 1 MiB, matching
|
|
1178
|
+
* `StepBoundary`'s evidence bound.
|
|
1179
|
+
*/
|
|
1180
|
+
readonly maxInlineBytes?: number | undefined
|
|
1181
|
+
/**
|
|
1182
|
+
* Root-relative paths copy-back refuses to write or remove, in addition to
|
|
1183
|
+
* the always-reserved `.smithers-workspace-lock` and `.flows` directories.
|
|
1184
|
+
* Name the engine database, its `-wal`/`-shm` siblings, and the artifact
|
|
1185
|
+
* directory here when they live under the root outside `.flows`. A
|
|
1186
|
+
* reserved directory also reserves every path beneath it.
|
|
1187
|
+
*/
|
|
1188
|
+
readonly reservedPaths?: ReadonlyArray<string> | undefined
|
|
1189
|
+
}
|
|
1190
|
+
|
|
1191
|
+
// Shared across separately constructed sandboxes. Entries live only while a
|
|
1192
|
+
// commit is running or waiting, so short-lived workspace roots do not leak.
|
|
1193
|
+
const CommitLocks = Context.Service<RcMap.RcMap<string, Semaphore.Semaphore>>(
|
|
1194
|
+
"@smthrs/engine-store/WorkspaceSandbox/CommitLocks"
|
|
1195
|
+
)
|
|
1196
|
+
const locksLayer = Layer.effect(CommitLocks, RcMap.make({ lookup: (_root: string) => Semaphore.make(1) }))
|
|
1197
|
+
// The memo map shares the layer across active operations, including operations
|
|
1198
|
+
// from different store instances. Their scopes release the final owner.
|
|
1199
|
+
const lockOwners = Layer.makeMemoMapUnsafe()
|
|
1200
|
+
|
|
1201
|
+
const coordinateCommit = <A, E, R>(root: string, write: Effect.Effect<A, E, R>): Effect.Effect<A, E, R> =>
|
|
1202
|
+
Effect.scoped(Effect.gen(function*() {
|
|
1203
|
+
const services = yield* Layer.buildWithMemoMap(locksLayer, lockOwners, yield* Scope.Scope)
|
|
1204
|
+
const lock = yield* RcMap.get(Context.get(services, CommitLocks), root)
|
|
1205
|
+
return yield* lock.withPermit(write)
|
|
1206
|
+
}))
|
|
1207
|
+
|
|
1208
|
+
const commitLockName = ".smithers-workspace-lock"
|
|
1209
|
+
// The conventional engine state directory: the SQLite store, its WAL/SHM
|
|
1210
|
+
// siblings, and the artifact objects. A step body must never replace them.
|
|
1211
|
+
const engineStateName = ".flows"
|
|
1212
|
+
|
|
1213
|
+
const defaultMaxInlineBytes = 1024 * 1024
|
|
1214
|
+
|
|
1215
|
+
const hostFailure = (cause: unknown): WorkspaceError =>
|
|
1216
|
+
new WorkspaceError({
|
|
1217
|
+
code: "host_unavailable",
|
|
1218
|
+
message: "the host filesystem could not serve the workspace transaction",
|
|
1219
|
+
cause
|
|
1220
|
+
})
|
|
1221
|
+
|
|
1222
|
+
const artifactFailure = (cause: { readonly message: string }): WorkspaceError =>
|
|
1223
|
+
new WorkspaceError({
|
|
1224
|
+
code: "host_unavailable",
|
|
1225
|
+
message: `the artifact store could not serve the workspace transaction: ${cause.message}`,
|
|
1226
|
+
cause
|
|
1227
|
+
})
|
|
1228
|
+
|
|
1229
|
+
/**
|
|
1230
|
+
* The errno names the descriptor-relative host reports when it refuses to
|
|
1231
|
+
* resolve a path inside its pinned root: a symlink at the final component
|
|
1232
|
+
* (`ELOOP`); a symlink at an intermediate component (`ELOOP` on Linux,
|
|
1233
|
+
* `ENOTDIR` on Darwin, which also covers a file standing where a directory
|
|
1234
|
+
* must be); or `..` traversal, a path outside the root, a hard-linked file, or
|
|
1235
|
+
* a root whose identity changed (`EPERM`). Each means the change does not
|
|
1236
|
+
* name a workspace entry reachable without following a link.
|
|
1237
|
+
*/
|
|
1238
|
+
const confinementCodes: ReadonlySet<unknown> = new Set(["ELOOP", "ENOTDIR", "EPERM"])
|
|
1239
|
+
|
|
1240
|
+
const refusedByConfinement = (error: PlatformError.PlatformError): boolean =>
|
|
1241
|
+
error.reason._tag !== "BadArgument" &&
|
|
1242
|
+
typeof error.reason.cause === "object" && error.reason.cause !== null &&
|
|
1243
|
+
confinementCodes.has((error.reason.cause as { readonly code?: unknown }).code)
|
|
1244
|
+
|
|
1245
|
+
/** Maps one copy-back host failure onto the sandbox's typed error codes. */
|
|
1246
|
+
const copyBackFailure = (path: string) => (error: PlatformError.PlatformError): WorkspaceError =>
|
|
1247
|
+
refusedByConfinement(error)
|
|
1248
|
+
? new WorkspaceError({
|
|
1249
|
+
code: "path_escapes_workspace",
|
|
1250
|
+
message:
|
|
1251
|
+
`materializing ${path} was refused: it does not name a workspace entry reachable without following a link (${error.message})`,
|
|
1252
|
+
cause: error
|
|
1253
|
+
})
|
|
1254
|
+
: hostFailure(error)
|
|
1255
|
+
|
|
1256
|
+
/**
|
|
1257
|
+
* Maps a commit-lock failure: a missed acquisition deadline is
|
|
1258
|
+
* `commit_lock_timeout`, so a caller can tell "another writer holds the root"
|
|
1259
|
+
* from "the host refused".
|
|
1260
|
+
*/
|
|
1261
|
+
const lockFailure = (cause: unknown): WorkspaceError =>
|
|
1262
|
+
Cause.isTimeoutError(cause)
|
|
1263
|
+
? new WorkspaceError({
|
|
1264
|
+
code: "commit_lock_timeout",
|
|
1265
|
+
message:
|
|
1266
|
+
`another writer held ${commitLockName} for the whole acquisition deadline; a lock whose owner stopped is reclaimed once its heartbeat is ${FileLease.defaultStaleAfterMs} ms old`,
|
|
1267
|
+
cause
|
|
1268
|
+
})
|
|
1269
|
+
// `FileLease` hands every other refusal over as the host's `PlatformError`.
|
|
1270
|
+
: copyBackFailure(commitLockName)(cause as PlatformError.PlatformError)
|
|
1271
|
+
|
|
1272
|
+
// Over-approximates the spellings a case-insensitive, normalization-insensitive
|
|
1273
|
+
// volume equates: `.FLOWS`, `.flow\u017f`, and `.\ufb02ows` all open `.flows` on
|
|
1274
|
+
// default APFS. Folding more than a volume does only refuses more names.
|
|
1275
|
+
const foldPath = (path: string): string => path.normalize("NFKC").toUpperCase().toLowerCase().normalize("NFKC")
|
|
1276
|
+
|
|
1277
|
+
// NTFS with 8.3 name creation on (the system volume default) also opens a
|
|
1278
|
+
// long name through its generated short name: `.flows` through `FLOWS~1`, or
|
|
1279
|
+
// `FL3A2B~1` once the numbered tails collide. Linux reaches such a volume
|
|
1280
|
+
// through WSL's `/mnt/c` and macOS through SMB, so the shape is refused on
|
|
1281
|
+
// every host. The pattern is broader than the generator: any `~` and digit
|
|
1282
|
+
// tail on a base of at most eight characters, with an extension of at most
|
|
1283
|
+
// three.
|
|
1284
|
+
const shortNameAlias = /^[^.~]{0,7}~\d{1,7}(\.[^.]{0,3})?$/u
|
|
1285
|
+
const isShortNameAlias = (segment: string): boolean => {
|
|
1286
|
+
// `split` always yields a first element, possibly empty.
|
|
1287
|
+
const base = segment.split(".")[0]!
|
|
1288
|
+
return base.length <= 8 && shortNameAlias.test(segment)
|
|
1289
|
+
}
|
|
1290
|
+
|
|
1291
|
+
/**
|
|
1292
|
+
* Builds the filesystem-backed workspace sandbox.
|
|
1293
|
+
*
|
|
1294
|
+
* **Copy-in, not overlay.** The transaction is seeded with exactly the
|
|
1295
|
+
* declared read set and nothing else
|
|
1296
|
+
* (`docs/concepts/workspace-transactions.md`). Bazel uses the same strategy, which
|
|
1297
|
+
* is why whole-tree write observation is structural here. Re-rooting a
|
|
1298
|
+
* body onto a bare host tree instead would leave every undeclared file
|
|
1299
|
+
* readable and reduce enforcement to the taxonomy's weakest, post-hoc
|
|
1300
|
+
* detection-by-diff tier; and the OS-level overlay that would fix that is not
|
|
1301
|
+
* reachable through Effect's `FileSystem` tag, so it could not run in a
|
|
1302
|
+
* browser. Seeding costs a copy of the declared reads and buys both.
|
|
1303
|
+
*
|
|
1304
|
+
* **Copy-back is confined and journaled.** Every copy-back host call (the
|
|
1305
|
+
* lock, preflight, apply, and rollback) goes through
|
|
1306
|
+
* `@smthrs/kernel/FileSystem`'s `confined` view: one descriptor-relative,
|
|
1307
|
+
* no-follow request against the workspace root pinned when the commit starts.
|
|
1308
|
+
* A symlink anywhere on a change's path, a hard-linked file, or a root
|
|
1309
|
+
* replaced mid-commit is refused with `path_escapes_workspace`, never
|
|
1310
|
+
* followed, and no check-then-write window exists for a concurrent process to
|
|
1311
|
+
* swap a link into. A copy-back through a symlink that stays inside the root
|
|
1312
|
+
* is refused too: the body writes the referent path itself. `fs` must
|
|
1313
|
+
* therefore carry the kernel's descriptor-relative executor (Node and Bun
|
|
1314
|
+
* hosts use `@smthrs/platform-node`'s `AtomicFileSystem`) or a
|
|
1315
|
+
* whole-filesystem isolation attestation (`withIsolatedFileSystem`, for an
|
|
1316
|
+
* in-memory volume); a plain path-based host is refused at copy-back with
|
|
1317
|
+
* `host_unavailable`. Snapshot reads use `fs` as given. A root-keyed semaphore and
|
|
1318
|
+
* the `.smithers-workspace-lock` lease file (`@smthrs/artifacts/FileLease`)
|
|
1319
|
+
* serialize cooperating callers, including separate processes. Preconditions run before file
|
|
1320
|
+
* changes, and the apply loop keeps each target's pre-image for in-process
|
|
1321
|
+
* rollback. A crash or rollback failure can leave partial changes; see
|
|
1322
|
+
* {@link Service.materialize} for the caller's reconciliation obligations.
|
|
1323
|
+
* Hosts must implement exclusive `wx` file creation, `stat` with an mtime,
|
|
1324
|
+
* `utimes`, `rename`, and `remove` for the reserved lock. Copy-back
|
|
1325
|
+
* also refuses the `.flows` engine state directory and any
|
|
1326
|
+
* {@link FileSystemOptions.reservedPaths}, so no write set or boundary mode
|
|
1327
|
+
* lets a step body replace the engine database or its artifact blobs.
|
|
1328
|
+
*
|
|
1329
|
+
* @category constructors
|
|
1330
|
+
* @since 0.1.0
|
|
1331
|
+
*/
|
|
1332
|
+
export const makeFileSystem = (
|
|
1333
|
+
fs: FileSystem.FileSystem,
|
|
1334
|
+
platformPath: Path.Path,
|
|
1335
|
+
artifacts: ArtifactStore.Service,
|
|
1336
|
+
workspaceRoot: string,
|
|
1337
|
+
options: FileSystemOptions = {}
|
|
1338
|
+
): Service => {
|
|
1339
|
+
const maxInlineBytes = options.maxInlineBytes ?? defaultMaxInlineBytes
|
|
1340
|
+
const root = workspaceRoot.replaceAll(/\/+$/g, "")
|
|
1341
|
+
const reserved = [
|
|
1342
|
+
commitLockName,
|
|
1343
|
+
engineStateName,
|
|
1344
|
+
...(options.reservedPaths ?? []).map((path) => path.replace(/^(\.\/)+/, "").replaceAll(/\/+$/g, ""))
|
|
1345
|
+
].filter((path) => path !== "" && path !== ".").map((name) => ({ name, segments: foldPath(name).split("/") }))
|
|
1346
|
+
// Why the root-relative `path` is refused, if it targets or lies beneath a
|
|
1347
|
+
// reserved entry. The confined host refuses every symlink on a path, so the
|
|
1348
|
+
// remaining aliases are spellings the volume itself equates: case,
|
|
1349
|
+
// compatibility, and normalization variants on APFS and NTFS, and NTFS 8.3
|
|
1350
|
+
// short names. Comparing folded segments, and refusing a short-name shape
|
|
1351
|
+
// wherever a reserved segment stands, refuses them on every volume,
|
|
1352
|
+
// case-sensitive ones included.
|
|
1353
|
+
const reservedAt = (path: string): string | undefined => {
|
|
1354
|
+
const segments = foldPath(path).split("/")
|
|
1355
|
+
const within = reserved.filter((entry) => entry.segments.length <= segments.length)
|
|
1356
|
+
const exact = within.find((entry) => entry.segments.every((segment, index) => segments[index] === segment))
|
|
1357
|
+
if (exact !== undefined) return `the workspace path ${exact.name} is reserved`
|
|
1358
|
+
const alias = within.find((entry) =>
|
|
1359
|
+
// `within` holds only entries no longer than the path, so every index is present.
|
|
1360
|
+
entry.segments.every((segment, index) => segments[index] === segment || isShortNameAlias(segments[index]!))
|
|
1361
|
+
)
|
|
1362
|
+
if (alias === undefined) return undefined
|
|
1363
|
+
const spelling = path.split("/").slice(0, alias.segments.length).join("/")
|
|
1364
|
+
return `the workspace path ${spelling} may be a short-name alias of a reserved path`
|
|
1365
|
+
}
|
|
1366
|
+
const hostPath = (path: string) => root === "" ? path : `${root}/${path}`
|
|
1367
|
+
// One host call, not two: the read reports an absent path itself. On the
|
|
1368
|
+
// confined host
|
|
1369
|
+
// the `exists` probe that used to precede every read was a second CPython
|
|
1370
|
+
// fork (`@smthrs/platform-node/AtomicFileSystem`) for an answer the read
|
|
1371
|
+
// was about to give.
|
|
1372
|
+
const readFrom = (
|
|
1373
|
+
host: FileSystem.FileSystem,
|
|
1374
|
+
path: string,
|
|
1375
|
+
onError: (error: PlatformError.PlatformError) => WorkspaceError
|
|
1376
|
+
) =>
|
|
1377
|
+
host.readFile(path).pipe(
|
|
1378
|
+
Effect.catchReason("PlatformError", "NotFound", () => Effect.succeed(undefined)),
|
|
1379
|
+
Effect.mapError(onError)
|
|
1380
|
+
)
|
|
1381
|
+
const readIfPresent = Effect.fn("WorkspaceSandbox.readIfPresent")(function*(path: string) {
|
|
1382
|
+
return yield* readFrom(fs, path, hostFailure)
|
|
1383
|
+
})
|
|
1384
|
+
const canonicalRoot = fs.realPath(root === "" ? "." : root).pipe(
|
|
1385
|
+
Effect.catchReason("PlatformError", "NotFound", () => Effect.succeed(undefined)),
|
|
1386
|
+
Effect.mapError(hostFailure)
|
|
1387
|
+
)
|
|
1388
|
+
const withCommitLock = <E, R>(
|
|
1389
|
+
changes: ReadonlyArray<FileChange>,
|
|
1390
|
+
effect: (host: FileSystem.FileSystem) => Effect.Effect<void, E, R>
|
|
1391
|
+
) =>
|
|
1392
|
+
Effect.gen(function*() {
|
|
1393
|
+
for (const change of changes) {
|
|
1394
|
+
const hit = reservedAt(change.path)
|
|
1395
|
+
if (hit !== undefined) return yield* Effect.fail(hostFailure(hit))
|
|
1396
|
+
}
|
|
1397
|
+
yield* fs.makeDirectory(root === "" ? "." : root, { recursive: true }).pipe(Effect.mapError(hostFailure))
|
|
1398
|
+
// Pinned once per commit, after the root exists: every lock, preflight,
|
|
1399
|
+
// apply, and rollback call below resolves against this root identity.
|
|
1400
|
+
const host = yield* KernelFileSystem.confined(fs, root).pipe(
|
|
1401
|
+
Effect.provideService(Path.Path, platformPath),
|
|
1402
|
+
Effect.mapError(hostFailure)
|
|
1403
|
+
)
|
|
1404
|
+
const canonical = yield* canonicalRoot
|
|
1405
|
+
// The confined view refuses `utimes`, which the lease heartbeat needs.
|
|
1406
|
+
// The heartbeat touches the lock only after a confined, no-follow read
|
|
1407
|
+
// found this call's own token there, so the one path-based call it
|
|
1408
|
+
// makes can at worst refresh an mtime.
|
|
1409
|
+
const leaseHost: FileSystem.FileSystem = { ...host, utimes: fs.utimes }
|
|
1410
|
+
return yield* coordinateCommit(
|
|
1411
|
+
canonical ?? root,
|
|
1412
|
+
FileLease.withLease(leaseHost, hostPath(commitLockName), effect(host), lockFailure, {
|
|
1413
|
+
label: "Workspace commit lock",
|
|
1414
|
+
annotations: { root: canonical ?? root }
|
|
1415
|
+
})
|
|
1416
|
+
)
|
|
1417
|
+
})
|
|
1418
|
+
const commitWith = Effect.fn("WorkspaceSandbox.commit")(function*(
|
|
1419
|
+
host: FileSystem.FileSystem,
|
|
1420
|
+
changes: ReadonlyArray<FileChange>
|
|
1421
|
+
) {
|
|
1422
|
+
yield* Effect.annotateCurrentSpan({ changes: changes.length })
|
|
1423
|
+
// The advisory lock and process semaphore cover preflight, apply, and
|
|
1424
|
+
// rollback. Preconditions run only after both are acquired.
|
|
1425
|
+
const { before, conflict } = yield* preflight(
|
|
1426
|
+
changes,
|
|
1427
|
+
(path) => readFrom(host, hostPath(path), copyBackFailure(path))
|
|
1428
|
+
)
|
|
1429
|
+
if (conflict !== undefined) {
|
|
1430
|
+
yield* Metric.update(EngineStoreMetrics.materializationConflicts, 1)
|
|
1431
|
+
return yield* Effect.fail(conflict)
|
|
1432
|
+
}
|
|
1433
|
+
const resolved = new Map<string, Uint8Array>()
|
|
1434
|
+
for (const change of changes) {
|
|
1435
|
+
if (change.afterDigest === undefined) continue
|
|
1436
|
+
const bytes = change.after ?? (yield* artifacts.get(`${change.afterDigest}`).pipe(
|
|
1437
|
+
Effect.mapError((error) =>
|
|
1438
|
+
error._tag === "@smthrs/artifacts/ArtifactStoreError"
|
|
1439
|
+
? artifactFailure(error)
|
|
1440
|
+
: new WorkspaceError({
|
|
1441
|
+
code: "not_found",
|
|
1442
|
+
message: `the retained bytes for ${change.path} are unavailable: ${error.message}`,
|
|
1443
|
+
cause: error
|
|
1444
|
+
})
|
|
1445
|
+
)
|
|
1446
|
+
))
|
|
1447
|
+
resolved.set(change.path, bytes)
|
|
1448
|
+
}
|
|
1449
|
+
const applied: Array<FileChange> = []
|
|
1450
|
+
const apply = Effect.gen(function*() {
|
|
1451
|
+
for (const change of changes) {
|
|
1452
|
+
// Journal the change before touching its target: a write that
|
|
1453
|
+
// fails halfway may still have mutated the file it was writing.
|
|
1454
|
+
applied.push(change)
|
|
1455
|
+
const target = hostPath(change.path)
|
|
1456
|
+
if (change.afterDigest === undefined) {
|
|
1457
|
+
yield* host.remove(target).pipe(Effect.mapError(copyBackFailure(change.path)))
|
|
1458
|
+
continue
|
|
1459
|
+
}
|
|
1460
|
+
const parent = parentDirectory(target)
|
|
1461
|
+
if (parent !== undefined) {
|
|
1462
|
+
yield* host.makeDirectory(parent, { recursive: true }).pipe(
|
|
1463
|
+
Effect.mapError(copyBackFailure(change.path))
|
|
1464
|
+
)
|
|
1465
|
+
}
|
|
1466
|
+
yield* host.writeFile(target, resolved.get(change.path)!).pipe(
|
|
1467
|
+
Effect.mapError(copyBackFailure(change.path))
|
|
1468
|
+
)
|
|
1469
|
+
}
|
|
1470
|
+
})
|
|
1471
|
+
const rollback = Effect.gen(function*() {
|
|
1472
|
+
for (const change of [...applied].reverse()) {
|
|
1473
|
+
const target = hostPath(change.path)
|
|
1474
|
+
const previous = before.get(change.path)
|
|
1475
|
+
if (previous === undefined) {
|
|
1476
|
+
yield* host.remove(target, { force: true }).pipe(Effect.mapError(copyBackFailure(change.path)))
|
|
1477
|
+
} else {
|
|
1478
|
+
yield* host.writeFile(target, previous).pipe(Effect.mapError(copyBackFailure(change.path)))
|
|
1479
|
+
}
|
|
1480
|
+
}
|
|
1481
|
+
// Keep empty parent directories. Effect's portable FileSystem has no
|
|
1482
|
+
// atomic, non-recursive rmdir; recursively deleting a directory after
|
|
1483
|
+
// a separate emptiness check could erase a concurrent writer's file.
|
|
1484
|
+
})
|
|
1485
|
+
yield* apply.pipe(
|
|
1486
|
+
Effect.catchCause((cause) =>
|
|
1487
|
+
Effect.gen(function*() {
|
|
1488
|
+
const restored = yield* Effect.exit(rollback)
|
|
1489
|
+
if (Exit.isSuccess(restored)) return yield* Effect.failCause(cause)
|
|
1490
|
+
const reasons = restored.cause.reasons
|
|
1491
|
+
.filter(Cause.isFailReason)
|
|
1492
|
+
.map((reason) => reason.error.message)
|
|
1493
|
+
// Both failures travel: the apply cause that opened the window and
|
|
1494
|
+
// the rollback cause that could not close it, composed the way
|
|
1495
|
+
// `acquireUseRelease` composes a failed use with a failed release.
|
|
1496
|
+
return yield* Effect.failCause(
|
|
1497
|
+
Cause.combine(
|
|
1498
|
+
cause,
|
|
1499
|
+
Cause.fail(
|
|
1500
|
+
new WorkspaceError({
|
|
1501
|
+
code: "host_unavailable",
|
|
1502
|
+
message: `copy-back failed mid-apply and rollback could not restore the workspace: ${
|
|
1503
|
+
reasons.join("; ")
|
|
1504
|
+
}`,
|
|
1505
|
+
cause: restored.cause
|
|
1506
|
+
})
|
|
1507
|
+
)
|
|
1508
|
+
)
|
|
1509
|
+
)
|
|
1510
|
+
})
|
|
1511
|
+
),
|
|
1512
|
+
// Interruption inside the apply window is a mid-sequence abort the
|
|
1513
|
+
// journal exists to prevent; the window is bounded local work, so it
|
|
1514
|
+
// closes before the fiber answers the interrupt.
|
|
1515
|
+
Effect.uninterruptible
|
|
1516
|
+
)
|
|
1517
|
+
})
|
|
1518
|
+
return makeHosted({
|
|
1519
|
+
root,
|
|
1520
|
+
snapshot: Effect.fn("WorkspaceSandbox.snapshot")(function*(descriptor) {
|
|
1521
|
+
yield* Effect.annotateCurrentSpan({
|
|
1522
|
+
reads: descriptor.readSet.length,
|
|
1523
|
+
boundaryMode: descriptor.boundaryMode
|
|
1524
|
+
})
|
|
1525
|
+
const base = new Map<string, Uint8Array>()
|
|
1526
|
+
for (const entry of descriptor.readSet) {
|
|
1527
|
+
// Through `FileEnumeration`, never the host `glob`: host results skip
|
|
1528
|
+
// dotfiles, so a declared read glob covering one would seed a sandbox
|
|
1529
|
+
// that silently hides it from the body.
|
|
1530
|
+
const paths = FileSet.isGlob(entry)
|
|
1531
|
+
? yield* FileEnumeration.expandGlob(fs, entry, {
|
|
1532
|
+
resolve: (path) => path === "" ? (root === "" ? "." : root) : hostPath(path)
|
|
1533
|
+
}).pipe(Effect.mapError(hostFailure))
|
|
1534
|
+
: [entry.path]
|
|
1535
|
+
for (const path of paths) {
|
|
1536
|
+
const normalized = normalizePath(root, path)
|
|
1537
|
+
if (Result.isFailure(normalized)) return yield* Effect.fail(normalized.failure)
|
|
1538
|
+
const content = yield* readIfPresent(hostPath(normalized.success))
|
|
1539
|
+
// A declared read that does not exist is seeded as absent, not as an
|
|
1540
|
+
// error: `StepBoundary.prepare` already records the mismatch as the
|
|
1541
|
+
// evidence that refuses the cache hit.
|
|
1542
|
+
if (content !== undefined) base.set(normalized.success, content)
|
|
1543
|
+
}
|
|
1544
|
+
}
|
|
1545
|
+
for (const path of descriptor.removes ?? []) {
|
|
1546
|
+
const normalized = normalizePath(root, path)
|
|
1547
|
+
/* v8 ignore next -- `FileBoundary.removes` is `FileSet.Pattern`-checked, so a decoded boundary cannot carry an upward or absolute removal; the branch guards hand-built descriptors */
|
|
1548
|
+
if (Result.isFailure(normalized)) return yield* Effect.fail(normalized.failure)
|
|
1549
|
+
const content = yield* readIfPresent(hostPath(normalized.success))
|
|
1550
|
+
if (content !== undefined) base.set(normalized.success, content)
|
|
1551
|
+
}
|
|
1552
|
+
return base
|
|
1553
|
+
}),
|
|
1554
|
+
baseline: Effect.fn("WorkspaceSandbox.baseline")(function*(path) {
|
|
1555
|
+
const content = yield* readIfPresent(hostPath(path))
|
|
1556
|
+
return content === undefined ? undefined : yield* digestOf(content)
|
|
1557
|
+
}),
|
|
1558
|
+
// The bytes now live at the change's `afterDigest`; the change itself
|
|
1559
|
+
// carries only the reference, exactly as oversized `StepBoundary`
|
|
1560
|
+
// evidence does.
|
|
1561
|
+
retain: (bytes) =>
|
|
1562
|
+
bytes.length <= maxInlineBytes
|
|
1563
|
+
? Effect.succeed(bytes)
|
|
1564
|
+
: artifacts.put(bytes).pipe(Effect.mapError(artifactFailure), Effect.as(undefined)),
|
|
1565
|
+
commit: (changes) => withCommitLock(changes, (host) => commitWith(host, changes))
|
|
1566
|
+
})
|
|
1567
|
+
}
|
|
1568
|
+
|
|
1569
|
+
/**
|
|
1570
|
+
* Provides the filesystem-backed workspace sandbox.
|
|
1571
|
+
*
|
|
1572
|
+
* Host access arrives through Effect's `FileSystem` tag — the same tag the
|
|
1573
|
+
* capability kernel decorates in place — and blob retention through
|
|
1574
|
+
* `@smthrs/artifacts`, so the same sandbox runs over a purely local store or a
|
|
1575
|
+
* local-plus-shared composition without knowing which it got. The workspace
|
|
1576
|
+
* root arrives through the kernel's `Workspace` service, so absolute paths a
|
|
1577
|
+
* body resolved for itself still land inside the transaction.
|
|
1578
|
+
*
|
|
1579
|
+
* The layer refuses to build over a host that {@link makeFileSystem} could
|
|
1580
|
+
* not confine (a plain path-based `FileSystem`), failing with
|
|
1581
|
+
* `host_unavailable`, so a composition that could never copy back safely is
|
|
1582
|
+
* rejected at startup rather than at its first commit.
|
|
1583
|
+
*
|
|
1584
|
+
* @category layers
|
|
1585
|
+
* @since 0.1.0
|
|
1586
|
+
*/
|
|
1587
|
+
export const layerFileSystem = (
|
|
1588
|
+
options: FileSystemOptions = {}
|
|
1589
|
+
): Layer.Layer<
|
|
1590
|
+
Service,
|
|
1591
|
+
WorkspaceError,
|
|
1592
|
+
FileSystem.FileSystem | Path.Path | ArtifactStore.ArtifactStore | KernelWorkspace
|
|
1593
|
+
> =>
|
|
1594
|
+
Layer.effect(
|
|
1595
|
+
WorkspaceSandbox,
|
|
1596
|
+
Effect.gen(function*() {
|
|
1597
|
+
const fs = yield* FileSystem.FileSystem
|
|
1598
|
+
const platformPath = yield* Path.Path
|
|
1599
|
+
const artifacts = yield* ArtifactStore.ArtifactStore
|
|
1600
|
+
const workspace = yield* KernelWorkspace
|
|
1601
|
+
yield* KernelFileSystem.requireConfinable(fs, workspace.root).pipe(Effect.mapError(hostFailure))
|
|
1602
|
+
return makeFileSystem(fs, platformPath, artifacts, workspace.root, options)
|
|
1603
|
+
})
|
|
1604
|
+
)
|