phronomy 0.26.0 → 0.28.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.mutant.yml +9 -9
- data/CHANGELOG.md +92 -0
- data/CONTRIBUTING.md +8 -8
- data/README.md +13 -3
- data/VERIFY.sh +27 -27
- data/benchmark/bench_agent_invoke.rb +26 -47
- data/benchmark/bench_context_assembler.rb +17 -15
- data/benchmark/bench_regression.rb +1 -1
- data/benchmark/bench_tool_schema.rb +1 -1
- data/docs/architecture/agent-chat-and-state-ownership.md +153 -0
- data/docs/architecture/agent-configuration-and-tool-binding.md +139 -0
- data/docs/architecture/agent-context.md +4 -2
- data/docs/architecture/agent-transition-ownership.md +13 -0
- data/docs/architecture/backend-groups.md +152 -0
- data/docs/architecture/before-llm-input.md +9 -3
- data/docs/architecture/context-management.md +31 -1
- data/docs/architecture/context-preparation-steps.md +90 -0
- data/docs/architecture/entry-action-and-team-wording.md +72 -0
- data/docs/architecture/execution-abstraction-review.md +88 -0
- data/docs/architecture/execution-boundaries.md +28 -0
- data/docs/architecture/execution-metadata-and-values.md +86 -0
- data/docs/architecture/generator-verifier-ownership.md +111 -0
- data/docs/architecture/multi-agent-handoff.md +11 -4
- data/docs/architecture/persistence-refactoring-plan.md +50 -0
- data/docs/architecture/persistence.md +45 -4
- data/docs/architecture/r8-unit1.md +104 -0
- data/docs/architecture/r8-unit10.md +72 -0
- data/docs/architecture/r8-unit11.md +71 -0
- data/docs/architecture/r8-unit12.md +67 -0
- data/docs/architecture/r8-unit13.md +61 -0
- data/docs/architecture/r8-unit14.md +72 -0
- data/docs/architecture/r8-unit15.md +65 -0
- data/docs/architecture/r8-unit16.md +54 -0
- data/docs/architecture/r8-unit17.md +73 -0
- data/docs/architecture/r8-unit18.md +92 -0
- data/docs/architecture/r8-unit19.md +61 -0
- data/docs/architecture/r8-unit2.md +55 -0
- data/docs/architecture/r8-unit20.md +51 -0
- data/docs/architecture/r8-unit21.md +79 -0
- data/docs/architecture/r8-unit22.md +74 -0
- data/docs/architecture/r8-unit23.md +81 -0
- data/docs/architecture/r8-unit3.md +130 -0
- data/docs/architecture/r8-unit4.md +79 -0
- data/docs/architecture/r8-unit5.md +115 -0
- data/docs/architecture/r8-unit6.md +71 -0
- data/docs/architecture/r8-unit7.md +70 -0
- data/docs/architecture/r8-unit8.md +78 -0
- data/docs/architecture/r8-unit9.md +73 -0
- data/docs/architecture/refactoring-closure.md +343 -0
- data/docs/architecture/remaining-refactoring-plan.md +390 -0
- data/docs/architecture/rubyllm-2-token-ownership.md +82 -0
- data/docs/architecture/storage-execution-boundaries.md +54 -0
- data/docs/architecture/tool-schema-recording-gap.md +56 -0
- data/docs/architecture/tracing.md +2 -2
- data/docs/architecture/workflow-terminal-ownership-design.md +104 -0
- data/docs/architecture.md +222 -0
- data/docs/decisions/014-unified-persistence-durable-state.md +11 -0
- data/docs/decisions/015-tool-public-facade-and-rbs-boundary.md +3 -3
- data/docs/decisions/024-event-loop-single-writer-agent-runtime.md +5 -1
- data/docs/decisions/025-process-local-agent-ownership-and-runtime-admission.md +13 -3
- data/docs/decisions/026-workflow-runtime-admission-and-durable-terminal-barrier.md +5 -1
- data/docs/decisions/027-llm-adapter-provider-boundary.md +2 -0
- data/docs/decisions/030-agent-handoff-domain-and-durable-responsibility.md +7 -0
- data/docs/decisions/031-durable-multi-agent-coordination.md +7 -0
- data/docs/decisions/032-storage-backend-composition.md +85 -0
- data/docs/decisions/033-domain-persistence-ownership.md +79 -0
- data/docs/decisions/034-handoff-runner-coordination-ownership.md +78 -0
- data/docs/decisions/035-tool-executor-capability-ownership.md +68 -0
- data/docs/decisions/036-context-contract-ownership.md +86 -0
- data/docs/decisions/037-common-definition-ownership.md +62 -0
- data/docs/decisions/038-responsibility-based-source-layout.md +145 -0
- data/docs/decisions/039-runtime-configuration-lifecycle-ownership.md +82 -0
- data/docs/decisions/040-configuration-default-composition.md +105 -0
- data/docs/decisions/041-feature-owned-identity-registries.md +100 -0
- data/docs/decisions/042-feature-owned-execution-state.md +106 -0
- data/docs/decisions/043-storage-execution-constraint-notifications.md +75 -0
- data/docs/decisions/044-agent-default-and-one-shot-composition.md +106 -0
- data/docs/decisions/045-worker-input-restriction-ownership.md +104 -0
- data/docs/decisions/046-agent-responsibility-layout-and-shared-records.md +110 -0
- data/docs/decisions/047-recovered-execution-continuation-contract.md +116 -0
- data/docs/decisions/048-dispatch-preparation-worker-ownership.md +120 -0
- data/docs/decisions/049-initial-preparation-worker-ownership.md +111 -0
- data/docs/decisions/050-approval-resume-snapshot-and-commit-ownership.md +98 -0
- data/docs/decisions/051-execution-outcome-worker-ownership.md +153 -0
- data/docs/decisions/052-tool-invocation-restoration-ownership.md +92 -0
- data/docs/decisions/053-shared-state-coordination-ownership.md +71 -0
- data/docs/decisions/054-workflow-terminal-save-single-owner.md +76 -0
- data/docs/decisions/055-terminal-observer-failure-settlement.md +60 -0
- data/docs/decisions/056-workflow-terminal-policy-ownership.md +87 -0
- data/docs/decisions/057-storage-transaction-boundaries.md +73 -0
- data/docs/decisions/058-neutral-storage-primitives.md +83 -0
- data/docs/decisions/059-backend-contracts-and-async-clients.md +74 -0
- data/docs/decisions/060-runtime-settings-and-application-configuration.md +86 -0
- data/docs/decisions/061-persistence-failure-contracts.md +89 -0
- data/docs/decisions/062-content-store-service.md +52 -0
- data/docs/decisions/063-execution-contracts-and-services.md +88 -0
- data/docs/decisions/064-agent-execution-and-multi-agent-responsibility.md +36 -0
- data/docs/decisions/065-vector-and-embeddings-common-operations.md +38 -0
- data/docs/decisions/066-llm-and-tool-operation-contracts.md +20 -0
- data/docs/decisions/067-agent-progress-and-engine-connection.md +29 -0
- data/docs/decisions/068-agent-tool-child-progress-and-tool-operation.md +36 -0
- data/docs/decisions/README.md +52 -6
- data/docs/design/durable-semantic-coordination/IMPLEMENTATION_DESIGN_V2.md +6 -0
- data/docs/design/durable-semantic-coordination/IMPLEMENTATION_REPORT.md +6 -0
- data/docs/execution-boundaries-preview.md +45 -0
- data/docs/features.md +16 -5
- data/docs/getting-started.md +11 -8
- data/docs/migrations/0.15.md +5 -0
- data/docs/migrations/0.22.md +2 -2
- data/docs/migrations/durable-semantic-coordination-v2.md +8 -0
- data/docs/migrations/handoff-runner-multi-agent.md +37 -0
- data/docs/migrations/neutral-storage-spi.md +43 -0
- data/docs/migrations/parallel-tool-chat-removal.md +49 -0
- data/docs/migrations/persistence-failure-contracts.md +67 -0
- data/docs/migrations/r8-unit10.md +36 -0
- data/docs/migrations/r8-unit11.md +33 -0
- data/docs/migrations/r8-unit12.md +31 -0
- data/docs/migrations/r8-unit13.md +36 -0
- data/docs/migrations/r8-unit14.md +31 -0
- data/docs/migrations/r8-unit15.md +31 -0
- data/docs/migrations/r8-unit16.md +26 -0
- data/docs/migrations/r8-unit17.md +28 -0
- data/docs/migrations/r8-unit18.md +29 -0
- data/docs/migrations/r8-unit19.md +28 -0
- data/docs/migrations/r8-unit20.md +24 -0
- data/docs/migrations/r8-unit21.md +28 -0
- data/docs/migrations/r8-unit22.md +29 -0
- data/docs/migrations/r8-unit23.md +20 -0
- data/docs/migrations/r8-unit4.md +67 -0
- data/docs/migrations/r8-unit5.md +75 -0
- data/docs/migrations/r8-unit6.md +109 -0
- data/docs/migrations/r8-unit7.md +31 -0
- data/docs/migrations/r8-unit8.md +33 -0
- data/docs/migrations/r8-unit9.md +28 -0
- data/docs/migrations/shared-state-multi-agent.md +44 -0
- data/docs/migrations/storage-async-client.md +103 -0
- data/docs/migrations/storage-backend-composition.md +117 -0
- data/docs/migrations/storage-transaction-boundaries.md +74 -0
- data/docs/migrations/vector-async-clients.md +110 -0
- data/docs/persistence-backends.md +190 -215
- data/docs/runtime-and-concurrency.md +142 -32
- data/lib/phronomy/agent/admission.rb +78 -0
- data/lib/phronomy/{agent_already_exists_error.rb → agent/agent_already_exists_error.rb} +2 -0
- data/lib/phronomy/{agent_busy_error.rb → agent/agent_busy_error.rb} +2 -0
- data/lib/phronomy/{agent_purged_error.rb → agent/agent_purged_error.rb} +2 -0
- data/lib/phronomy/agent/api/agent.rb +17 -0
- data/lib/phronomy/{execution_rehydration_required_error.rb → agent/api/execution_rehydration_required_error.rb} +2 -0
- data/lib/phronomy/agent/async_event_api.rb +38 -33
- data/lib/phronomy/agent/base.rb +106 -294
- data/lib/phronomy/agent/before_llm_input.rb +64 -0
- data/lib/phronomy/{agent.rb → agent/composition/run_once.rb} +4 -13
- data/lib/phronomy/agent/context_candidate_resolver.rb +2 -2
- data/lib/phronomy/agent/context_importer.rb +2 -2
- data/lib/phronomy/agent/{context_assembler.rb → context_preparation.rb} +157 -288
- data/lib/phronomy/agent/control_request.rb +21 -0
- data/lib/phronomy/agent/{agent_execution.rb → execution/agent_execution.rb} +8 -4
- data/lib/phronomy/agent/{agent_invocation.rb → execution/agent_invocation.rb} +32 -63
- data/lib/phronomy/agent/execution/approval_resume_commit.rb +108 -0
- data/lib/phronomy/agent/execution/dispatch_preparation.rb +195 -0
- data/lib/phronomy/agent/{exact_execution.rb → execution/exact_execution.rb} +43 -23
- data/lib/phronomy/agent/{execution_cancellation.rb → execution/execution_cancellation.rb} +8 -5
- data/lib/phronomy/agent/execution/execution_coordinator.rb +1744 -0
- data/lib/phronomy/agent/execution/execution_failure.rb +30 -0
- data/lib/phronomy/agent/execution/execution_metadata.rb +54 -0
- data/lib/phronomy/agent/execution/execution_outcome_committer.rb +288 -0
- data/lib/phronomy/agent/execution/execution_registry.rb +463 -0
- data/lib/phronomy/agent/execution/execution_session_runner.rb +113 -0
- data/lib/phronomy/agent/execution/initial_preparation.rb +323 -0
- data/lib/phronomy/agent/execution/invocation_actions.rb +368 -0
- data/lib/phronomy/agent/execution/invocation_transitions.rb +124 -0
- data/lib/phronomy/agent/execution/runtime_record_encoder.rb +210 -0
- data/lib/phronomy/agent/execution_change.rb +161 -0
- data/lib/phronomy/agent/execution_environment.rb +36 -0
- data/lib/phronomy/agent/execution_extension_state.rb +21 -0
- data/lib/phronomy/agent/execution_observation.rb +35 -0
- data/lib/phronomy/agent/filterable.rb +182 -0
- data/lib/phronomy/agent/{handoff_context.rb → handoff/transfer_context.rb} +6 -6
- data/lib/phronomy/agent/{handoff_projection.rb → handoff/transfer_projection.rb} +14 -33
- data/lib/phronomy/agent/handoff_error.rb +7 -0
- data/lib/phronomy/agent/{journal_projection.rb → journal/journal_projection.rb} +4 -0
- data/lib/phronomy/agent/{journal_record.rb → journal/journal_record.rb} +3 -3
- data/lib/phronomy/agent/{llm_call_record.rb → journal/llm_call_record.rb} +2 -2
- data/lib/phronomy/agent/knowledge_item.rb +13 -0
- data/lib/phronomy/agent/{agent_root.rb → lifecycle/agent_root.rb} +6 -2
- data/lib/phronomy/agent/lifecycle/default_persistence.rb +29 -0
- data/lib/phronomy/{engine/runtime/agent_ownership_registry.rb → agent/lifecycle/ownership_registry.rb} +24 -15
- data/lib/phronomy/agent/persistence/agent_repository.rb +69 -0
- data/lib/phronomy/agent/persistence/cancellation_repository.rb +51 -0
- data/lib/phronomy/agent/persistence/codec.rb +338 -0
- data/lib/phronomy/agent/persistence/execution_repository.rb +124 -0
- data/lib/phronomy/agent/persistence/journal_repository.rb +63 -0
- data/lib/phronomy/agent/persistence/records.rb +33 -0
- data/lib/phronomy/agent/persistence/retention_repository.rb +70 -0
- data/lib/phronomy/agent/persistence/storage_schema.rb +29 -0
- data/lib/phronomy/agent/persistence/watermark.rb +29 -0
- data/lib/phronomy/agent/recovery/invocation_restorer.rb +124 -0
- data/lib/phronomy/agent/{recovery_coordinator → recovery/recovery_coordinator}/continuation.rb +28 -51
- data/lib/phronomy/agent/{recovery_coordinator → recovery/recovery_coordinator}/installation.rb +50 -55
- data/lib/phronomy/agent/{recovery_coordinator → recovery/recovery_coordinator}/resolution.rb +38 -43
- data/lib/phronomy/agent/{recovery_coordinator.rb → recovery/recovery_coordinator.rb} +26 -23
- data/lib/phronomy/agent/recovery/recovery_rules.rb +119 -0
- data/lib/phronomy/agent/recovery/recovery_support.rb +224 -0
- data/lib/phronomy/agent/reserved_execution.rb +16 -0
- data/lib/phronomy/agent/retention.rb +17 -0
- data/lib/phronomy/agent/runtime_binding/engine_environment.rb +60 -0
- data/lib/phronomy/agent/runtime_binding/engine_session_builder.rb +34 -0
- data/lib/phronomy/agent/runtime_binding/phase_machine_builder.rb +64 -0
- data/lib/phronomy/agent/runtime_binding/tool_session_builder.rb +34 -0
- data/lib/phronomy/agent/{ruby_llm_materializer.rb → runtime_input.rb} +19 -39
- data/lib/phronomy/agent/saved_context_reader.rb +55 -0
- data/lib/phronomy/agent/settings.rb +47 -0
- data/lib/phronomy/agent/state_writer.rb +166 -0
- data/lib/phronomy/agent/store.rb +253 -0
- data/lib/phronomy/{stream_callback_error.rb → agent/stream_callback_error.rb} +2 -0
- data/lib/phronomy/agent/{approval_evaluation_request.rb → tool_execution/approval_evaluation_request.rb} +1 -12
- data/lib/phronomy/agent/{tool_approval_request.rb → tool_execution/tool_approval_request.rb} +1 -10
- data/lib/phronomy/agent/tool_execution/tool_binding.rb +30 -0
- data/lib/phronomy/agent/{tool_invocation.rb → tool_execution/tool_invocation.rb} +114 -248
- data/lib/phronomy/agent/tool_execution/tool_invocation_actions.rb +107 -0
- data/lib/phronomy/agent/tool_execution/tool_invocation_transitions.rb +70 -0
- data/lib/phronomy/common/configuration_error.rb +7 -0
- data/lib/phronomy/common/error.rb +5 -0
- data/lib/phronomy/{agent → common/values}/immutable.rb +9 -1
- data/lib/phronomy/common/values/serializable.rb +32 -0
- data/lib/phronomy/configuration/runtime_settings.rb +49 -0
- data/lib/phronomy/content_store/storage_schema.rb +12 -0
- data/lib/phronomy/content_store/stored_contents.rb +44 -0
- data/lib/phronomy/context/assembly.rb +202 -0
- data/lib/phronomy/{context_budget_exceeded_error.rb → context/budget_exceeded_error.rb} +5 -1
- data/lib/phronomy/context/candidate.rb +51 -0
- data/lib/phronomy/context/constraint.rb +47 -0
- data/lib/phronomy/{agent → context}/context_plan.rb +2 -2
- data/lib/phronomy/{agent → context}/context_plan_validator.rb +3 -3
- data/lib/phronomy/{agent → context}/context_policy.rb +1 -1
- data/lib/phronomy/{agent → context}/context_policy_input.rb +7 -7
- data/lib/phronomy/{agent → context}/context_policy_input_builder.rb +1 -1
- data/lib/phronomy/context/default_policy.rb +140 -0
- data/lib/phronomy/context/final_budget_validator.rb +33 -0
- data/lib/phronomy/{agent → context}/llm_input_build_context.rb +2 -2
- data/lib/phronomy/{agent → context}/llm_input_manifest.rb +21 -21
- data/lib/phronomy/{agent → context}/llm_input_patch.rb +3 -3
- data/lib/phronomy/context/manifest_error.rb +10 -0
- data/lib/phronomy/context/prompt_template.rb +98 -0
- data/lib/phronomy/{vector_store → documents}/loader/base.rb +1 -1
- data/lib/phronomy/{vector_store → documents}/loader/csv_loader.rb +2 -2
- data/lib/phronomy/{vector_store → documents}/loader/markdown_loader.rb +3 -3
- data/lib/phronomy/{vector_store → documents}/loader/plain_text_loader.rb +2 -2
- data/lib/phronomy/{vector_store → documents}/splitter/base.rb +1 -1
- data/lib/phronomy/{vector_store → documents}/splitter/fixed_size_splitter.rb +2 -2
- data/lib/phronomy/{vector_store → documents}/splitter/recursive_splitter.rb +2 -2
- data/lib/phronomy/embeddings/async/async_client.rb +44 -0
- data/lib/phronomy/embeddings/backends/ruby_llm_embeddings.rb +48 -0
- data/lib/phronomy/embeddings/base.rb +43 -0
- data/lib/phronomy/embeddings/invalid_result_error.rb +11 -0
- data/lib/phronomy/embeddings/transport_error.rb +11 -0
- data/lib/phronomy/engine/concurrency/offload_pool.rb +39 -20
- data/lib/phronomy/engine/concurrency/pool_registry.rb +47 -12
- data/lib/phronomy/{diagnostics.rb → engine/diagnostics.rb} +1 -1
- data/lib/phronomy/engine/event_loop.rb +164 -630
- data/lib/phronomy/engine/execution_binding.rb +23 -0
- data/lib/phronomy/engine/execution_receiver_binding.rb +43 -0
- data/lib/phronomy/engine/fsm_protocol.rb +9 -0
- data/lib/phronomy/engine/fsm_session.rb +37 -30
- data/lib/phronomy/engine/metrics.rb +28 -0
- data/lib/phronomy/engine/runtime/timer_queue.rb +1 -1
- data/lib/phronomy/engine/runtime.rb +104 -95
- data/lib/phronomy/engine/terminal_decision.rb +7 -0
- data/lib/phronomy/execution/async_operation.rb +41 -0
- data/lib/phronomy/execution/backpressure_error.rb +7 -0
- data/lib/phronomy/{blocking.rb → execution/blocking.rb} +2 -2
- data/lib/phronomy/execution/cancellation_error.rb +7 -0
- data/lib/phronomy/{engine → execution}/concurrency/cancellation_token.rb +5 -1
- data/lib/phronomy/{engine → execution}/concurrency/deadline.rb +0 -17
- data/lib/phronomy/{engine → execution}/concurrency/physical_completion_task.rb +1 -1
- data/lib/phronomy/{engine → execution}/concurrency/result_collector.rb +1 -1
- data/lib/phronomy/{engine → execution}/concurrency/result_composition.rb +33 -12
- data/lib/phronomy/{engine/concurrency/subscriptions.rb → execution/concurrency/result_subscriptions.rb} +14 -18
- data/lib/phronomy/execution/concurrency/subscriptions.rb +25 -0
- data/lib/phronomy/execution/concurrency/worker_input_restricted.rb +14 -0
- data/lib/phronomy/execution/event_loop_reentrancy_error.rb +8 -0
- data/lib/phronomy/{execution.rb → execution/execution.rb} +57 -30
- data/lib/phronomy/{execution_cancellation_error.rb → execution/execution_cancellation_error.rb} +2 -0
- data/lib/phronomy/execution/execution_receiver.rb +78 -0
- data/lib/phronomy/execution/execution_scope_state.rb +35 -0
- data/lib/phronomy/{execution_timeout_error.rb → execution/execution_timeout_error.rb} +2 -0
- data/lib/phronomy/{invalid_async_entry_action_error.rb → execution/invalid_async_entry_action_error.rb} +2 -0
- data/lib/phronomy/{invalid_async_transition_action_error.rb → execution/invalid_async_transition_action_error.rb} +2 -0
- data/lib/phronomy/{invalid_async_workflow_action_error.rb → execution/invalid_async_workflow_action_error.rb} +2 -0
- data/lib/phronomy/{invocation_context.rb → execution/invocation_context.rb} +0 -15
- data/lib/phronomy/execution/invocation_controls.rb +73 -0
- data/lib/phronomy/execution/pool_shutdown_error.rb +7 -0
- data/lib/phronomy/execution/recursion_limit_error.rb +7 -0
- data/lib/phronomy/execution/runnable.rb +24 -0
- data/lib/phronomy/execution/runtime_shutdown_error.rb +7 -0
- data/lib/phronomy/execution/runtime_shutdown_reentrancy_error.rb +7 -0
- data/lib/phronomy/execution/scheduler_reentrancy_error.rb +9 -0
- data/lib/phronomy/{engine → execution}/task_result.rb +7 -3
- data/lib/phronomy/execution/timeout_error.rb +7 -0
- data/lib/phronomy/execution/wait_policy.rb +24 -0
- data/lib/phronomy/filter/filter_block_error.rb +14 -0
- data/lib/phronomy/generation/generator_verifier/agent_operation.rb +65 -0
- data/lib/phronomy/generation/generator_verifier/agent_result_receiver.rb +89 -0
- data/lib/phronomy/generation/generator_verifier/default_parser.rb +27 -0
- data/lib/phronomy/generation/generator_verifier/pipeline_state.rb +57 -0
- data/lib/phronomy/generation/generator_verifier/workflow_builder.rb +112 -0
- data/lib/phronomy/generation/generator_verifier.rb +118 -0
- data/lib/phronomy/generation/low_confidence_error.rb +14 -0
- data/lib/phronomy/llm_adapter/async/async_client.rb +47 -0
- data/lib/phronomy/llm_adapter/authentication_error.rb +9 -0
- data/lib/phronomy/llm_adapter/backends/ruby_llm.rb +183 -0
- data/lib/phronomy/llm_adapter/base.rb +49 -68
- data/lib/phronomy/llm_adapter/context_length_error.rb +9 -0
- data/lib/phronomy/llm_adapter/invalid_result_error.rb +10 -0
- data/lib/phronomy/llm_adapter/message.rb +62 -0
- data/lib/phronomy/llm_adapter/rate_limit_error.rb +9 -0
- data/lib/phronomy/llm_adapter/request.rb +79 -0
- data/lib/phronomy/llm_adapter/response.rb +36 -0
- data/lib/phronomy/llm_adapter/stream_chunk.rb +18 -0
- data/lib/phronomy/llm_adapter/token_usage.rb +64 -0
- data/lib/phronomy/llm_adapter/transport_error.rb +9 -0
- data/lib/phronomy/llm_adapter.rb +3 -16
- data/lib/phronomy/llm_context_window/token_budget.rb +6 -7
- data/lib/phronomy/multi_agent/admission_registry.rb +15 -2
- data/lib/phronomy/multi_agent/durable_subagent_coordinator.rb +108 -84
- data/lib/phronomy/multi_agent/execution_environment.rb +29 -0
- data/lib/phronomy/{agent → multi_agent}/handoff.rb +2 -2
- data/lib/phronomy/{agent → multi_agent}/handoff_capability_factory.rb +21 -2
- data/lib/phronomy/multi_agent/handoff_participant.rb +79 -0
- data/lib/phronomy/{agent → multi_agent}/handoff_policy.rb +1 -1
- data/lib/phronomy/multi_agent/handoff_runner.rb +156 -0
- data/lib/phronomy/{agent → multi_agent}/handoff_state.rb +2 -2
- data/lib/phronomy/multi_agent/orchestrator.rb +61 -75
- data/lib/phronomy/multi_agent/persistence/codec.rb +75 -0
- data/lib/phronomy/multi_agent/persistence/handoff_state_repository.rb +77 -0
- data/lib/phronomy/multi_agent/persistence/records.rb +27 -0
- data/lib/phronomy/multi_agent/persistence/storage_schema.rb +21 -0
- data/lib/phronomy/multi_agent/persistence/team_execution_repository.rb +124 -0
- data/lib/phronomy/multi_agent/persistence/team_repository.rb +69 -0
- data/lib/phronomy/multi_agent/reserved_child_admission.rb +88 -0
- data/lib/phronomy/multi_agent/runtime_binding/engine_environment.rb +35 -0
- data/lib/phronomy/{agent → multi_agent}/shared_state.rb +56 -39
- data/lib/phronomy/multi_agent/store.rb +156 -0
- data/lib/phronomy/multi_agent/team_coordinator.rb +87 -75
- data/lib/phronomy/multi_agent/team_execution.rb +1 -1
- data/lib/phronomy/{engine/runtime → multi_agent}/team_ownership_registry.rb +5 -5
- data/lib/phronomy/multi_agent/team_root.rb +1 -1
- data/lib/phronomy/output_parser/parse_error.rb +7 -0
- data/lib/phronomy/persistence/content_repository.rb +26 -0
- data/lib/phronomy/persistence/errors.rb +16 -0
- data/lib/phronomy/persistence/migration/initial_format_migration.rb +21 -21
- data/lib/phronomy/persistence/persistence.rb +23 -0
- data/lib/phronomy/persistence/save_outcome.rb +24 -0
- data/lib/phronomy/persistence/snapshot_comparison.rb +81 -0
- data/lib/phronomy/persistence/storage_boundary.rb +32 -0
- data/lib/phronomy/persistence/transaction.rb +86 -0
- data/lib/phronomy/persistence_composition/storage_schema.rb +29 -0
- data/lib/phronomy/persistence_composition/stores.rb +34 -0
- data/lib/phronomy/persistence_composition/unit4_migration.rb +276 -0
- data/lib/phronomy/runtime_composition/agent_defaults.rb +11 -0
- data/lib/phronomy/runtime_composition/configuration.rb +146 -0
- data/lib/phronomy/runtime_composition/configuration_defaults.rb +9 -0
- data/lib/phronomy/runtime_composition/evaluation_defaults.rb +7 -0
- data/lib/phronomy/runtime_composition/execution_defaults.rb +5 -0
- data/lib/phronomy/runtime_composition/generation_defaults.rb +6 -0
- data/lib/phronomy/runtime_composition/global_configuration.rb +38 -0
- data/lib/phronomy/runtime_composition/global_runtime.rb +19 -0
- data/lib/phronomy/runtime_composition/multi_agent_defaults.rb +9 -0
- data/lib/phronomy/runtime_composition/workflow_defaults.rb +4 -0
- data/lib/phronomy/storage/async/async_client.rb +60 -0
- data/lib/phronomy/storage/backend.rb +107 -0
- data/lib/phronomy/storage/backends/in_memory.rb +157 -0
- data/lib/phronomy/storage/blob_conflict_error.rb +10 -0
- data/lib/phronomy/storage/blobs.rb +31 -0
- data/lib/phronomy/storage/condition.rb +25 -0
- data/lib/phronomy/storage/condition_failed_error.rb +16 -0
- data/lib/phronomy/storage/conflict_error.rb +9 -0
- data/lib/phronomy/{persistence → storage}/durable_record.rb +12 -12
- data/lib/phronomy/storage/entry.rb +33 -0
- data/lib/phronomy/storage/guard_ref.rb +13 -0
- data/lib/phronomy/storage/not_found_error.rb +9 -0
- data/lib/phronomy/storage/record_codec.rb +177 -0
- data/lib/phronomy/storage/records.rb +61 -0
- data/lib/phronomy/storage/resource.rb +126 -0
- data/lib/phronomy/storage/scope.rb +25 -0
- data/lib/phronomy/storage/serialization_error.rb +9 -0
- data/lib/phronomy/storage/streams.rb +44 -0
- data/lib/phronomy/storage/transaction_error.rb +10 -0
- data/lib/phronomy/storage/unique_constraint_error.rb +17 -0
- data/lib/phronomy/storage/unsupported_backend_error.rb +9 -0
- data/lib/phronomy/storage/validation.rb +53 -0
- data/lib/phronomy/storage/view.rb +131 -0
- data/lib/phronomy/testing/eval/metrics.rb +1 -1
- data/lib/phronomy/testing/eval/scorer/llm_judge.rb +19 -3
- data/lib/phronomy/testing/persistence_contract/a_content_store.rb +1 -1
- data/lib/phronomy/testing/persistence_contract/a_journal_repository.rb +2 -2
- data/lib/phronomy/testing/persistence_contract/a_persistence_backend.rb +54 -48
- data/lib/phronomy/testing/persistence_contract/a_workflow_state_repository.rb +1 -1
- data/lib/phronomy/testing/persistence_contract/an_agent_repository.rb +1 -1
- data/lib/phronomy/testing/persistence_contract/an_execution_repository.rb +5 -5
- data/lib/phronomy/testing/persistence_contract/coordination_repositories.rb +52 -46
- data/lib/phronomy/testing/persistence_contract/neutral_storage_primitives.rb +263 -0
- data/lib/phronomy/testing/persistence_contract/storage_transaction_boundaries.rb +124 -0
- data/lib/phronomy/testing/persistence_contract.rb +4 -0
- data/lib/phronomy/tool/authorization.rb +77 -0
- data/lib/phronomy/tool/base.rb +549 -9
- data/lib/phronomy/tool/call_request.rb +40 -0
- data/lib/phronomy/{agent/tool_definition_set.rb → tool/definition_set.rb} +16 -12
- data/lib/phronomy/tool/operation.rb +88 -0
- data/lib/phronomy/tool/schema.rb +214 -0
- data/lib/phronomy/tool/settings.rb +31 -0
- data/lib/phronomy/tool/tool_error.rb +7 -0
- data/lib/phronomy/{agent → tool}/tool_executor.rb +10 -7
- data/lib/phronomy/tools/agent.rb +22 -90
- data/lib/phronomy/tools/mcp.rb +21 -14
- data/lib/phronomy/tools/vector_search.rb +4 -4
- data/lib/phronomy/tracing/automatic.rb +11 -6
- data/lib/phronomy/tracing/base.rb +1 -1
- data/lib/phronomy/tracing/observation.rb +37 -0
- data/lib/phronomy/tracing/settings.rb +36 -0
- data/lib/phronomy/vector_store/{async_backend.rb → async/async_client.rb} +35 -29
- data/lib/phronomy/vector_store/{in_memory.rb → backends/in_memory.rb} +12 -10
- data/lib/phronomy/vector_store/{pgvector.rb → backends/pgvector.rb} +44 -16
- data/lib/phronomy/vector_store/{redis_search.rb → backends/redis_search.rb} +74 -37
- data/lib/phronomy/vector_store/base.rb +118 -49
- data/lib/phronomy/vector_store/invalid_result_error.rb +12 -0
- data/lib/phronomy/vector_store.rb +2 -17
- data/lib/phronomy/version.rb +1 -1
- data/lib/phronomy/workflow/completion.rb +11 -0
- data/lib/phronomy/{workflow.rb → workflow/execution/workflow.rb} +10 -8
- data/lib/phronomy/workflow/execution/workflow_action_rules.rb +67 -0
- data/lib/phronomy/{workflow_context.rb → workflow/execution/workflow_context.rb} +5 -1
- data/lib/phronomy/workflow/execution/workflow_context_ownership_error.rb +7 -0
- data/lib/phronomy/workflow/execution/workflow_execution_environment.rb +27 -0
- data/lib/phronomy/workflow/execution/workflow_execution_registry.rb +186 -0
- data/lib/phronomy/{workflow_runner.rb → workflow/execution/workflow_runner.rb} +145 -115
- data/lib/phronomy/workflow/execution/workflow_settings.rb +28 -0
- data/lib/phronomy/workflow/persistence/codec.rb +153 -0
- data/lib/phronomy/workflow/persistence/state_repository.rb +63 -0
- data/lib/phronomy/workflow/persistence/storage_schema.rb +13 -0
- data/lib/phronomy/workflow/runtime_binding/workflow_engine_environment.rb +38 -0
- data/lib/phronomy/workflow/runtime_binding/workflow_phase_machine_builder.rb +156 -0
- data/lib/phronomy/workflow/runtime_binding/workflow_terminal_policy.rb +40 -0
- data/lib/phronomy.rb +97 -98
- data/scripts/api_snapshot.rb +36 -16
- data/scripts/storage_spi_snapshot.rb +36 -0
- data/sig/_private/phronomy/agent/settings.rbs +19 -0
- data/sig/_private/phronomy/configuration/runtime_settings.rbs +20 -0
- data/sig/_private/phronomy/llm_adapter/async/async_client.rbs +10 -0
- data/sig/_private/phronomy/multi_agent/store.rbs +8 -0
- data/sig/_private/phronomy/persistence/storage_boundary.rbs +7 -0
- data/sig/_private/phronomy/tool/settings.rbs +13 -0
- data/sig/_private/phronomy/tracing/settings.rbs +15 -0
- data/sig/_private/phronomy/workflow/execution/workflow_settings.rbs +13 -0
- data/sig/phronomy/agent/admission.rbs +19 -0
- data/sig/phronomy/agent/execution_contract.rbs +132 -0
- data/sig/phronomy/agent/execution_environment.rbs +20 -0
- data/sig/phronomy/agent/store.rbs +73 -0
- data/sig/phronomy/agent.rbs +14 -139
- data/sig/phronomy/common.rbs +7 -0
- data/sig/phronomy/content_store/contracts.rbs +13 -0
- data/sig/phronomy/context.rbs +152 -0
- data/sig/phronomy/embeddings/async/async_client.rbs +9 -0
- data/sig/phronomy/embeddings/base.rbs +17 -0
- data/sig/phronomy/engine/concurrency/timer_bindings.rbs +22 -0
- data/sig/phronomy/engine/control_bindings.rbs +11 -0
- data/sig/phronomy/engine/control_protocols.rbs +23 -0
- data/sig/phronomy/engine/execution_binding.rbs +6 -0
- data/sig/phronomy/engine/execution_receiver.rbs +15 -0
- data/sig/phronomy/engine/observability.rbs +13 -0
- data/sig/phronomy/execution/async_operation.rbs +7 -0
- data/sig/phronomy/execution/blocking.rbs +5 -0
- data/sig/phronomy/execution/concurrency/cancellation_token.rbs +13 -0
- data/sig/phronomy/execution/concurrency/deadline.rbs +10 -0
- data/sig/phronomy/execution/concurrency/result_composition.rbs +7 -0
- data/sig/phronomy/execution/control_protocols.rbs +18 -0
- data/sig/phronomy/execution/errors.rbs +7 -0
- data/sig/phronomy/execution/execution.rbs +41 -0
- data/sig/phronomy/execution/execution_receiver.rbs +40 -0
- data/sig/phronomy/execution/execution_scope_state.rbs +9 -0
- data/sig/phronomy/execution/invocation_context.rbs +18 -0
- data/sig/phronomy/execution/invocation_controls.rbs +13 -0
- data/sig/phronomy/execution/task_result.rbs +29 -0
- data/sig/phronomy/extensions.rbs +0 -21
- data/sig/phronomy/handoff.rbs +12 -4
- data/sig/phronomy/llm_adapter/backends/ruby_llm.rbs +6 -0
- data/sig/phronomy/llm_adapter/base.rbs +14 -0
- data/sig/phronomy/llm_adapter/usage.rbs +14 -0
- data/sig/phronomy/llm_adapter/values.rbs +43 -0
- data/sig/phronomy/llm_context_window.rbs +10 -0
- data/sig/phronomy/multi_agent/reserved_child_admission.rbs +8 -0
- data/sig/phronomy/multi_agent/store.rbs +54 -0
- data/sig/phronomy/multi_agent.rbs +24 -3
- data/sig/phronomy/persistence/errors.rbs +18 -0
- data/sig/phronomy/persistence/migration.rbs +13 -0
- data/sig/phronomy/persistence/transaction.rbs +19 -0
- data/sig/phronomy/persistence.rbs +3 -185
- data/sig/phronomy/persistence_composition.rbs +19 -0
- data/sig/phronomy/runtime_composition/configuration.rbs +36 -0
- data/sig/phronomy/storage/async/async_client.rbs +8 -0
- data/sig/phronomy/storage/backends/in_memory.rbs +9 -0
- data/sig/phronomy/storage/contracts.rbs +160 -0
- data/sig/phronomy/tool.rbs +58 -5
- data/sig/phronomy/tracing/observation.rbs +7 -0
- data/sig/phronomy/vector_store/async/async_client.rbs +13 -0
- data/sig/phronomy/vector_store/base.rbs +23 -0
- data/sig/phronomy/workflow/execution/workflow_execution_environment.rbs +14 -0
- data/sig/phronomy/workflow/runtime_binding/workflow_terminal_policy.rbs +7 -0
- data/sig/phronomy/workflow/store.rbs +8 -0
- data/sig/phronomy/workflow.rbs +1 -1
- data/sig/phronomy.rbs +0 -49
- metadata +462 -130
- data/lib/phronomy/agent/agent_invocation_session_builder.rb +0 -584
- data/lib/phronomy/agent/concerns/before_llm_input.rb +0 -66
- data/lib/phronomy/agent/concerns/error_translation.rb +0 -45
- data/lib/phronomy/agent/concerns/filterable.rb +0 -184
- data/lib/phronomy/agent/context/capability/base.rb +0 -507
- data/lib/phronomy/agent/context/instruction/prompt_template.rb +0 -102
- data/lib/phronomy/agent/context_parts/validators/final_budget_validator.rb +0 -37
- data/lib/phronomy/agent/context_policies/default.rb +0 -142
- data/lib/phronomy/agent/execution_coordinator.rb +0 -3151
- data/lib/phronomy/agent/handoff_execution_coordinator.rb +0 -143
- data/lib/phronomy/agent/handoff_request.rb +0 -45
- data/lib/phronomy/agent/handoff_runner.rb +0 -178
- data/lib/phronomy/agent/phase_machine_builder.rb +0 -152
- data/lib/phronomy/agent/provider_call_outcome.rb +0 -106
- data/lib/phronomy/agent/recovery_support.rb +0 -504
- data/lib/phronomy/agent/selection/candidate.rb +0 -53
- data/lib/phronomy/agent/selection/constraint.rb +0 -49
- data/lib/phronomy/agent/token_budget_resolver.rb +0 -70
- data/lib/phronomy/agent/tool_call_intercepted.rb +0 -29
- data/lib/phronomy/agent/tool_invocation_session_builder.rb +0 -276
- data/lib/phronomy/configuration.rb +0 -59
- data/lib/phronomy/engine/concurrency/cancellation_scope.rb +0 -124
- data/lib/phronomy/engine/concurrency/operation_binding.rb +0 -43
- data/lib/phronomy/generator_verifier.rb +0 -369
- data/lib/phronomy/invalid_context_budget_configuration_error.rb +0 -8
- data/lib/phronomy/llm_adapter/ruby_llm.rb +0 -47
- data/lib/phronomy/metrics.rb +0 -26
- data/lib/phronomy/multi_agent/parallel_tool_chat.rb +0 -116
- data/lib/phronomy/persistence/durable_codec.rb +0 -706
- data/lib/phronomy/persistence/in_memory.rb +0 -690
- data/lib/phronomy/persistence/repository_facades.rb +0 -535
- data/lib/phronomy/persistence.rb +0 -276
- data/lib/phronomy/recovery.rb +0 -186
- data/lib/phronomy/ruby_llm_patches.rb +0 -24
- data/lib/phronomy/runnable.rb +0 -54
- data/lib/phronomy/token_usage.rb +0 -85
- data/lib/phronomy/vector_store/embeddings/base.rb +0 -44
- data/lib/phronomy/vector_store/embeddings/ruby_llm_embeddings.rb +0 -47
- data/lib/phronomy/workflow/phase_machine_builder.rb +0 -205
- data/lib/phronomy/workflow_recovery.rb +0 -123
- data/sig/phronomy/llm_adapter.rbs +0 -11
- data/sig/phronomy/runtime.rbs +0 -76
- /data/lib/phronomy/agent/{llm_operation_result.rb → execution/llm_operation_result.rb} +0 -0
- /data/lib/phronomy/{canonical_json.rb → common/canonical_json.rb} +0 -0
- /data/lib/phronomy/{event.rb → engine/event.rb} +0 -0
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
module Phronomy
|
|
4
|
-
|
|
4
|
+
module Storage
|
|
5
5
|
# Immutable logical record exchanged across the Persistence Backend SPI.
|
|
6
6
|
#
|
|
7
7
|
# Phronomy owns +record_type+, +format_version+, and the semantic meaning of
|
|
@@ -28,20 +28,20 @@ module Phronomy
|
|
|
28
28
|
# @api public
|
|
29
29
|
def initialize(record_type: MISSING, format_version: MISSING, payload: MISSING)
|
|
30
30
|
if record_type.equal?(MISSING)
|
|
31
|
-
raise Phronomy::
|
|
31
|
+
raise Phronomy::Storage::SerializationError, "durable record_type is missing"
|
|
32
32
|
end
|
|
33
33
|
if format_version.equal?(MISSING)
|
|
34
|
-
raise Phronomy::
|
|
34
|
+
raise Phronomy::Storage::SerializationError, "durable format_version is missing"
|
|
35
35
|
end
|
|
36
36
|
if payload.equal?(MISSING)
|
|
37
|
-
raise Phronomy::
|
|
37
|
+
raise Phronomy::Storage::SerializationError, "durable payload is missing"
|
|
38
38
|
end
|
|
39
39
|
unless record_type.is_a?(String)
|
|
40
|
-
raise Phronomy::
|
|
40
|
+
raise Phronomy::Storage::SerializationError,
|
|
41
41
|
"durable record_type must be a String"
|
|
42
42
|
end
|
|
43
43
|
unless format_version.is_a?(String)
|
|
44
|
-
raise Phronomy::
|
|
44
|
+
raise Phronomy::Storage::SerializationError,
|
|
45
45
|
"durable format_version must be a String"
|
|
46
46
|
end
|
|
47
47
|
|
|
@@ -51,10 +51,10 @@ module Phronomy
|
|
|
51
51
|
validate_payload!(payload)
|
|
52
52
|
@payload = immutable_copy(payload)
|
|
53
53
|
freeze
|
|
54
|
-
rescue Phronomy::
|
|
54
|
+
rescue Phronomy::Storage::SerializationError
|
|
55
55
|
raise
|
|
56
56
|
rescue => error
|
|
57
|
-
raise Phronomy::
|
|
57
|
+
raise Phronomy::Storage::SerializationError, error.message
|
|
58
58
|
end
|
|
59
59
|
|
|
60
60
|
# Returns an independent immutable record with the same logical value.
|
|
@@ -75,26 +75,26 @@ module Phronomy
|
|
|
75
75
|
|
|
76
76
|
def validate_header!
|
|
77
77
|
if record_type.empty?
|
|
78
|
-
raise Phronomy::
|
|
78
|
+
raise Phronomy::Storage::SerializationError,
|
|
79
79
|
"durable record_type must not be empty"
|
|
80
80
|
end
|
|
81
81
|
|
|
82
82
|
return if FORMAT_VERSION_PATTERN.match?(format_version)
|
|
83
83
|
|
|
84
|
-
raise Phronomy::
|
|
84
|
+
raise Phronomy::Storage::SerializationError,
|
|
85
85
|
"invalid durable format_version: #{format_version.inspect}"
|
|
86
86
|
end
|
|
87
87
|
|
|
88
88
|
def validate_payload!(payload)
|
|
89
89
|
unless payload.is_a?(Hash)
|
|
90
|
-
raise Phronomy::
|
|
90
|
+
raise Phronomy::Storage::SerializationError,
|
|
91
91
|
"durable payload must be a Hash"
|
|
92
92
|
end
|
|
93
93
|
|
|
94
94
|
Phronomy::CanonicalJSON.dump(payload)
|
|
95
95
|
true
|
|
96
96
|
rescue ArgumentError => error
|
|
97
|
-
raise Phronomy::
|
|
97
|
+
raise Phronomy::Storage::SerializationError,
|
|
98
98
|
"durable payload is not canonical JSON compatible: #{error.message}"
|
|
99
99
|
end
|
|
100
100
|
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Phronomy
|
|
4
|
+
module Storage
|
|
5
|
+
# Values crossing the driver boundary never expose mutable storage state.
|
|
6
|
+
# @api public
|
|
7
|
+
module Entry
|
|
8
|
+
Record = Data.define(:key, :revision, :attributes, :record) do
|
|
9
|
+
def initialize(key:, revision:, attributes:, record:)
|
|
10
|
+
super(key: Validation.key(key), revision: Validation.revision(revision),
|
|
11
|
+
attributes: Validation.attributes(attributes), record: Validation.record(record))
|
|
12
|
+
end
|
|
13
|
+
end
|
|
14
|
+
Append = Data.define(:id, :record) do
|
|
15
|
+
def initialize(id:, record:)
|
|
16
|
+
super(id: Validation.key(id), record: Validation.record(record))
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
Stream = Data.define(:position, :id, :record) do
|
|
20
|
+
def initialize(position:, id:, record:)
|
|
21
|
+
super(position: Validation.revision(position), id: Validation.key(id), record: Validation.record(record))
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
Blob = Data.define(:key, :bytes, :attributes) do
|
|
25
|
+
def initialize(key:, bytes:, attributes:)
|
|
26
|
+
raise ArgumentError, "blob bytes must be a String" unless bytes.is_a?(String)
|
|
27
|
+
super(key: Validation.key(key), bytes: bytes.dup.force_encoding(Encoding::BINARY).freeze,
|
|
28
|
+
attributes: Validation.attributes(attributes))
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Phronomy
|
|
4
|
+
module Storage
|
|
5
|
+
# A stable existing record used as a transaction lock anchor.
|
|
6
|
+
# @api public
|
|
7
|
+
GuardRef = Data.define(:resource, :key) do
|
|
8
|
+
def initialize(resource:, key:)
|
|
9
|
+
super(resource: Resource.normalize_reference(resource), key: Validation.key(key))
|
|
10
|
+
end
|
|
11
|
+
end
|
|
12
|
+
end
|
|
13
|
+
end
|
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Phronomy
|
|
4
|
+
module Storage
|
|
5
|
+
# Shared record-envelope and scalar validation; domain schemas live with their domains.
|
|
6
|
+
# @api private
|
|
7
|
+
module RecordCodec
|
|
8
|
+
private
|
|
9
|
+
|
|
10
|
+
def current_payload!(record, record_type:, format_version:, keys:, label:)
|
|
11
|
+
unless record.is_a?(Phronomy::Storage::DurableRecord)
|
|
12
|
+
raise Phronomy::Storage::SerializationError,
|
|
13
|
+
"backend returned #{record.class}; expected Storage::DurableRecord"
|
|
14
|
+
end
|
|
15
|
+
unless record.record_type == record_type
|
|
16
|
+
raise Phronomy::Storage::SerializationError,
|
|
17
|
+
"durable record type mismatch: expected #{record_type.inspect}, " \
|
|
18
|
+
"got #{record.record_type.inspect}"
|
|
19
|
+
end
|
|
20
|
+
unless record.format_version == format_version
|
|
21
|
+
raise Phronomy::Storage::SerializationError,
|
|
22
|
+
"unsupported #{record_type} format version: #{record.format_version.inspect}; " \
|
|
23
|
+
"current version is #{format_version.inspect}"
|
|
24
|
+
end
|
|
25
|
+
validate_exact_keys!(record.payload, keys, label: label)
|
|
26
|
+
record.payload
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def validate_allowed_keys!(hash, required_keys:, optional_keys:, label:)
|
|
30
|
+
unless hash.is_a?(Hash)
|
|
31
|
+
raise Phronomy::Storage::SerializationError, "#{label} must be a Hash"
|
|
32
|
+
end
|
|
33
|
+
unless hash.keys.all? { |key| key.is_a?(String) }
|
|
34
|
+
raise Phronomy::Storage::SerializationError,
|
|
35
|
+
"#{label} keys must all be String"
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
actual = hash.keys.sort
|
|
39
|
+
missing = required_keys.sort - actual
|
|
40
|
+
unknown = actual - (required_keys + optional_keys).sort
|
|
41
|
+
return hash if missing.empty? && unknown.empty?
|
|
42
|
+
|
|
43
|
+
details = []
|
|
44
|
+
details << "missing=#{missing.inspect}" unless missing.empty?
|
|
45
|
+
details << "unknown=#{unknown.inspect}" unless unknown.empty?
|
|
46
|
+
raise Phronomy::Storage::SerializationError,
|
|
47
|
+
"#{label} schema mismatch (#{details.join(", ")})"
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def validate_exact_keys!(hash, expected_keys, label:)
|
|
51
|
+
unless hash.is_a?(Hash)
|
|
52
|
+
raise Phronomy::Storage::SerializationError, "#{label} must be a Hash"
|
|
53
|
+
end
|
|
54
|
+
unless hash.keys.all? { |key| key.is_a?(String) }
|
|
55
|
+
raise Phronomy::Storage::SerializationError,
|
|
56
|
+
"#{label} keys must all be String"
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
actual = hash.keys.sort
|
|
60
|
+
expected = expected_keys.sort
|
|
61
|
+
return hash if actual == expected
|
|
62
|
+
|
|
63
|
+
missing = expected - actual
|
|
64
|
+
unknown = actual - expected
|
|
65
|
+
details = []
|
|
66
|
+
details << "missing=#{missing.inspect}" unless missing.empty?
|
|
67
|
+
details << "unknown=#{unknown.inspect}" unless unknown.empty?
|
|
68
|
+
raise Phronomy::Storage::SerializationError,
|
|
69
|
+
"#{label} schema mismatch (#{details.join(", ")})"
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def top_level_string_keys(value, label:)
|
|
73
|
+
unless value.is_a?(Hash)
|
|
74
|
+
raise Phronomy::Storage::SerializationError, "#{label} must be a Hash"
|
|
75
|
+
end
|
|
76
|
+
value.each_with_object({}) do |(key, child), result|
|
|
77
|
+
unless key.is_a?(String) || key.is_a?(Symbol)
|
|
78
|
+
raise Phronomy::Storage::SerializationError,
|
|
79
|
+
"#{label} key must be String or Symbol, got #{key.class}"
|
|
80
|
+
end
|
|
81
|
+
string_key = key.to_s
|
|
82
|
+
if result.key?(string_key)
|
|
83
|
+
raise Phronomy::Storage::SerializationError,
|
|
84
|
+
"#{label} contains duplicate key after normalization: #{string_key.inspect}"
|
|
85
|
+
end
|
|
86
|
+
result[string_key] = child
|
|
87
|
+
end
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
def require_nonempty_string!(hash, key, label:)
|
|
91
|
+
value = hash.fetch(key)
|
|
92
|
+
return value if value.is_a?(String) && !value.empty?
|
|
93
|
+
|
|
94
|
+
raise Phronomy::Storage::SerializationError,
|
|
95
|
+
"#{label} #{key} must be a non-empty String"
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
def require_optional_string!(hash, key, label:)
|
|
99
|
+
value = hash.fetch(key)
|
|
100
|
+
return value if value.nil? || value.is_a?(String)
|
|
101
|
+
|
|
102
|
+
raise Phronomy::Storage::SerializationError,
|
|
103
|
+
"#{label} #{key} must be a String or nil"
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
def require_integer!(hash, key, label:)
|
|
107
|
+
value = hash.fetch(key)
|
|
108
|
+
return value if value.is_a?(Integer)
|
|
109
|
+
|
|
110
|
+
raise Phronomy::Storage::SerializationError,
|
|
111
|
+
"#{label} #{key} must be an Integer"
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
def require_positive_integer!(hash, key, label:)
|
|
115
|
+
value = hash.fetch(key)
|
|
116
|
+
return value if value.is_a?(Integer) && value.positive?
|
|
117
|
+
|
|
118
|
+
raise Phronomy::Storage::SerializationError,
|
|
119
|
+
"#{label} #{key} must be a positive Integer"
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
def require_nonnegative_integer!(hash, key, label:)
|
|
123
|
+
value = hash.fetch(key)
|
|
124
|
+
return value if value.is_a?(Integer) && value >= 0
|
|
125
|
+
|
|
126
|
+
raise Phronomy::Storage::SerializationError,
|
|
127
|
+
"#{label} #{key} must be a non-negative Integer"
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
def require_boolean!(hash, key, label:)
|
|
131
|
+
value = hash.fetch(key)
|
|
132
|
+
return value if boolean?(value)
|
|
133
|
+
|
|
134
|
+
raise Phronomy::Storage::SerializationError,
|
|
135
|
+
"#{label} #{key} must be true or false"
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
def require_enum_string!(hash, key, allowed, label:)
|
|
139
|
+
value = hash.fetch(key)
|
|
140
|
+
return value if value.is_a?(String) && allowed.include?(value)
|
|
141
|
+
|
|
142
|
+
raise Phronomy::Storage::SerializationError,
|
|
143
|
+
"#{label} #{key} must be one of #{allowed.inspect}"
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
def require_canonical_hash!(hash, key, label:)
|
|
147
|
+
value = hash.fetch(key)
|
|
148
|
+
unless value.is_a?(Hash)
|
|
149
|
+
raise Phronomy::Storage::SerializationError,
|
|
150
|
+
"#{label} #{key} must be a Hash"
|
|
151
|
+
end
|
|
152
|
+
Phronomy::CanonicalJSON.dump(value)
|
|
153
|
+
value
|
|
154
|
+
rescue ArgumentError => error
|
|
155
|
+
raise Phronomy::Storage::SerializationError,
|
|
156
|
+
"#{label} #{key} is not canonical JSON compatible: #{error.message}"
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
def boolean?(value)
|
|
160
|
+
value.equal?(true) || value.equal?(false)
|
|
161
|
+
end
|
|
162
|
+
|
|
163
|
+
def build_record(record_type, format_version, payload)
|
|
164
|
+
Phronomy::Storage::DurableRecord.new(
|
|
165
|
+
record_type: record_type,
|
|
166
|
+
format_version: format_version,
|
|
167
|
+
payload: payload
|
|
168
|
+
)
|
|
169
|
+
end
|
|
170
|
+
|
|
171
|
+
def serialization_error(prefix, error)
|
|
172
|
+
raise Phronomy::Storage::SerializationError,
|
|
173
|
+
"#{prefix}: #{error.class}: #{error.message}"
|
|
174
|
+
end
|
|
175
|
+
end
|
|
176
|
+
end
|
|
177
|
+
end
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Phronomy
|
|
4
|
+
module Storage
|
|
5
|
+
# Compare-and-write records and declared equality indexes.
|
|
6
|
+
# @api public
|
|
7
|
+
class Records
|
|
8
|
+
UNCHECKED = Object.new.freeze
|
|
9
|
+
attr_reader :resource
|
|
10
|
+
def initialize(view, resource) = (@view, @resource = view, resource)
|
|
11
|
+
|
|
12
|
+
# @api public
|
|
13
|
+
def insert(key:, revision:, attributes:, record:)
|
|
14
|
+
@view.validate!
|
|
15
|
+
entry = Entry::Record.new(key: key, revision: revision, attributes: resource.validate_attributes(attributes), record: record)
|
|
16
|
+
@view.perform(:insert_record, resource, entry: entry)
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
# @api public
|
|
20
|
+
def read(key)
|
|
21
|
+
@view.validate!
|
|
22
|
+
@view.perform(:read_record, resource, key: Validation.key(key))
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
# @api public
|
|
26
|
+
def fetch(key)
|
|
27
|
+
read(key) || raise(NotFoundError, "record not found: #{resource.id}/#{key}")
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
# @api public
|
|
31
|
+
def replace(key:, expected_revision:, next_revision:, attributes:, record:, expected_attributes: {})
|
|
32
|
+
@view.validate!
|
|
33
|
+
expected = Validation.revision(expected_revision)
|
|
34
|
+
entry = Entry::Record.new(key: key, revision: next_revision, attributes: resource.validate_attributes(attributes), record: record)
|
|
35
|
+
raise ConflictError, "replace must advance revision exactly once" unless next_revision == expected + 1
|
|
36
|
+
expected_values = resource.validate_attributes(expected_attributes, partial: true)
|
|
37
|
+
@view.perform(:replace_record, resource, entry: entry, expected_revision: expected, expected_attributes: expected_values)
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
# @api public
|
|
41
|
+
def delete(key:, expected_revision: UNCHECKED)
|
|
42
|
+
@view.validate!
|
|
43
|
+
expected = expected_revision.equal?(UNCHECKED) ? UNCHECKED : Validation.revision(expected_revision)
|
|
44
|
+
@view.perform(:delete_record, resource, key: Validation.key(key), expected_revision: expected)
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
# @api public
|
|
48
|
+
def scan(index:, equals:, after: nil, limit: nil)
|
|
49
|
+
@view.validate!
|
|
50
|
+
values = resource.index_values(index, equals)
|
|
51
|
+
@view.perform(:scan_records, resource, equals: values, after: after.nil? ? nil : Validation.key(after), limit: Validation.limit(limit))
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
# @api public
|
|
55
|
+
def delete_matching(index:, equals:)
|
|
56
|
+
@view.validate!
|
|
57
|
+
@view.perform(:delete_matching_records, resource, equals: resource.index_values(index, equals))
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
end
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Phronomy
|
|
4
|
+
module Storage
|
|
5
|
+
# Immutable logical schema. Physical drivers receive a separate mapping.
|
|
6
|
+
# @api public
|
|
7
|
+
class Resource
|
|
8
|
+
TYPES = [:string, :integer, :boolean, :nullable_string, :nullable_integer, :nullable_boolean].freeze
|
|
9
|
+
attr_reader :id, :kind, :attributes, :immutable_attributes, :indexes, :unique, :guard
|
|
10
|
+
|
|
11
|
+
# Normalize a schema object or ID before constructing guards and conditions.
|
|
12
|
+
# @api private
|
|
13
|
+
def self.normalize_reference(value)
|
|
14
|
+
value.is_a?(Resource) ? value : Validation.key(value)
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
# @api public
|
|
18
|
+
def initialize(id:, kind:, attributes: {}, immutable_attributes: [], indexes: {}, unique: [], guard: nil)
|
|
19
|
+
@id = Validation.key(id)
|
|
20
|
+
@kind = kind
|
|
21
|
+
@attributes = Validation.immutable(attributes)
|
|
22
|
+
@immutable_attributes = Validation.immutable(immutable_attributes)
|
|
23
|
+
@indexes = Validation.immutable(indexes)
|
|
24
|
+
@unique = Validation.immutable(unique)
|
|
25
|
+
@guard = Validation.immutable(guard)
|
|
26
|
+
validate_shape!
|
|
27
|
+
validate_indexes!
|
|
28
|
+
validate_guard!
|
|
29
|
+
validate_unique!
|
|
30
|
+
freeze
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
# @api public
|
|
34
|
+
def validate_attributes(values, partial: false)
|
|
35
|
+
valid = values.is_a?(Hash) && (values.keys - attributes.keys).empty? && (partial || values.keys.sort == attributes.keys.sort)
|
|
36
|
+
unless valid
|
|
37
|
+
raise ArgumentError, "attributes do not match #{id}"
|
|
38
|
+
end
|
|
39
|
+
values = values.to_h do |name, value|
|
|
40
|
+
type = attributes.fetch(name)
|
|
41
|
+
if type.to_s.start_with?("nullable_") && value.nil?
|
|
42
|
+
[name, nil]
|
|
43
|
+
else
|
|
44
|
+
[name, typed_value(name, type, value)]
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
if guard && values.key?(guard[:via])
|
|
48
|
+
values[guard[:via]] = Validation.key(values.fetch(guard[:via]))
|
|
49
|
+
end
|
|
50
|
+
Validation.attributes(values)
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
# @api public
|
|
54
|
+
def index_values(index, equals)
|
|
55
|
+
fields = indexes.fetch(index) { raise ArgumentError, "unknown index: #{index}" }
|
|
56
|
+
raise ArgumentError, "index attributes must match exactly" unless equals.is_a?(Hash) && equals.keys.sort == fields.sort
|
|
57
|
+
validate_attributes(equals, partial: true)
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
private
|
|
61
|
+
|
|
62
|
+
def validate_shape!
|
|
63
|
+
raise ArgumentError, "unsupported resource kind" unless [:records, :streams, :blobs].include?(kind)
|
|
64
|
+
unless attributes.is_a?(Hash) && attributes.keys.all? { |key| key.is_a?(Symbol) } && attributes.values.all? { |type| TYPES.include?(type) }
|
|
65
|
+
raise ArgumentError, "invalid attribute declarations"
|
|
66
|
+
end
|
|
67
|
+
unless field_list?(immutable_attributes, empty: true) && indexes.is_a?(Hash) && unique.is_a?(Array)
|
|
68
|
+
raise ArgumentError, "invalid resource declarations"
|
|
69
|
+
end
|
|
70
|
+
if kind != :records && (!immutable_attributes.empty? || !indexes.empty? || !unique.empty?)
|
|
71
|
+
raise ArgumentError, "only records declare indexes, uniqueness and immutable attributes"
|
|
72
|
+
end
|
|
73
|
+
raise ArgumentError, "streams have no indexed attributes" if kind == :streams && !attributes.empty?
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
def validate_indexes!
|
|
77
|
+
unless indexes.all? { |name, fields| name.is_a?(Symbol) && field_list?(fields) }
|
|
78
|
+
raise ArgumentError, "indexes need names and known distinct fields"
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
def validate_unique!
|
|
83
|
+
names = unique.map do |constraint|
|
|
84
|
+
unless constraint.is_a?(Hash) && constraint.keys.sort == [:fields, :name, :where] &&
|
|
85
|
+
constraint[:name].is_a?(Symbol) && field_list?(constraint[:fields])
|
|
86
|
+
raise ArgumentError, "invalid unique constraint"
|
|
87
|
+
end
|
|
88
|
+
validate_attributes(constraint.fetch(:where), partial: true)
|
|
89
|
+
constraint[:name]
|
|
90
|
+
end
|
|
91
|
+
raise ArgumentError, "unique constraints need distinct names" unless names.uniq == names
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
def validate_guard!
|
|
95
|
+
return unless guard
|
|
96
|
+
unless guard.is_a?(Hash) && guard.keys.sort == [:resource, :via] && guard[:resource].is_a?(String)
|
|
97
|
+
raise ArgumentError, "invalid guard declaration"
|
|
98
|
+
end
|
|
99
|
+
Validation.key(guard[:resource])
|
|
100
|
+
allowed = case kind
|
|
101
|
+
when :records then [:key, *attributes.select { |_, type| type == :string }.keys]
|
|
102
|
+
when :streams then [:stream]
|
|
103
|
+
else []
|
|
104
|
+
end
|
|
105
|
+
raise ArgumentError, "invalid guard key" unless allowed.include?(guard[:via])
|
|
106
|
+
if kind == :records && guard[:via] != :key && !immutable_attributes.include?(guard[:via])
|
|
107
|
+
raise ArgumentError, "guard attributes must be immutable"
|
|
108
|
+
end
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
def field_list?(values, empty: false)
|
|
112
|
+
values.is_a?(Array) && (empty || !values.empty?) && values.uniq == values && (values - attributes.keys).empty?
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
def typed_value(name, type, value)
|
|
116
|
+
valid = case type.to_s.delete_prefix("nullable_")
|
|
117
|
+
when "string" then value.is_a?(String) && value.valid_encoding? && !value.include?("\0")
|
|
118
|
+
when "integer" then value.is_a?(Integer)
|
|
119
|
+
when "boolean" then value.equal?(true) || value.equal?(false)
|
|
120
|
+
end
|
|
121
|
+
raise ArgumentError, "invalid #{id}.#{name}" unless valid
|
|
122
|
+
value.is_a?(String) ? value.encode(Encoding::UTF_8) : value
|
|
123
|
+
end
|
|
124
|
+
end
|
|
125
|
+
end
|
|
126
|
+
end
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Phronomy
|
|
4
|
+
module Storage
|
|
5
|
+
# One synchronous transaction lifetime, independent of physical rollback.
|
|
6
|
+
# @api private
|
|
7
|
+
class Scope
|
|
8
|
+
def initialize
|
|
9
|
+
@thread = Thread.current
|
|
10
|
+
@active = true
|
|
11
|
+
@failed = false
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def check!
|
|
15
|
+
raise TransactionError, "transaction belongs to another execution context" unless @thread.equal?(Thread.current)
|
|
16
|
+
raise TransactionError, "transaction scope is closed" unless @active
|
|
17
|
+
raise TransactionError, "transaction scope has failed; leave it before continuing" if @failed
|
|
18
|
+
true
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def fail! = @failed = true
|
|
22
|
+
def close! = @active = false
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Phronomy
|
|
4
|
+
module Storage
|
|
5
|
+
# Atomic ordered append with a per-stream head and entry identity.
|
|
6
|
+
# @api public
|
|
7
|
+
class Streams
|
|
8
|
+
attr_reader :resource
|
|
9
|
+
def initialize(view, resource) = (@view, @resource = view, resource)
|
|
10
|
+
|
|
11
|
+
# @api public
|
|
12
|
+
def append(stream:, expected_head:, entries:)
|
|
13
|
+
@view.validate!
|
|
14
|
+
key = Validation.key(stream)
|
|
15
|
+
expected = Validation.revision(expected_head)
|
|
16
|
+
raise ArgumentError, "entries must be an Array" unless entries.is_a?(Array)
|
|
17
|
+
values = entries.map do |entry|
|
|
18
|
+
raise SerializationError, "expected Entry::Append" unless entry.is_a?(Entry::Append)
|
|
19
|
+
Entry::Append.new(id: entry.id, record: entry.record)
|
|
20
|
+
end
|
|
21
|
+
raise ConflictError, "duplicate stream entry identity" unless values.map(&:id).uniq.length == values.length
|
|
22
|
+
@view.perform(:append_stream, resource, stream: key, expected_head: expected, entries: values.freeze)
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
# @api public
|
|
26
|
+
def read(stream:, after: 0, limit: nil)
|
|
27
|
+
@view.validate!
|
|
28
|
+
@view.perform(:read_stream, resource, stream: Validation.key(stream), after: Validation.revision(after), limit: Validation.limit(limit))
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
# @api public
|
|
32
|
+
def head(stream:)
|
|
33
|
+
@view.validate!
|
|
34
|
+
@view.perform(:stream_head, resource, stream: Validation.key(stream))
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
# @api public
|
|
38
|
+
def delete(stream:)
|
|
39
|
+
@view.validate!
|
|
40
|
+
@view.perform(:delete_stream, resource, stream: Validation.key(stream))
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
end
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Phronomy
|
|
4
|
+
module Storage
|
|
5
|
+
# A neutral storage contract failure.
|
|
6
|
+
# @api public
|
|
7
|
+
class UniqueConstraintError < ConflictError
|
|
8
|
+
attr_reader :resource, :constraint
|
|
9
|
+
# @api public
|
|
10
|
+
def initialize(resource:, constraint:)
|
|
11
|
+
@resource = resource.id
|
|
12
|
+
@constraint = constraint
|
|
13
|
+
super("unique constraint #{resource.id}.#{constraint} failed")
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
end
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Phronomy
|
|
4
|
+
module Storage
|
|
5
|
+
# Input validation shared by storage handles; no physical work happens here.
|
|
6
|
+
# @api private
|
|
7
|
+
module Validation
|
|
8
|
+
module_function
|
|
9
|
+
|
|
10
|
+
def key(value)
|
|
11
|
+
raise ArgumentError, "key must be a non-empty String" unless value.is_a?(String) && !value.empty?
|
|
12
|
+
result = value.encode(Encoding::UTF_8)
|
|
13
|
+
raise ArgumentError, "key must be valid UTF-8" unless result.valid_encoding? && !result.include?("\0")
|
|
14
|
+
result.dup.freeze
|
|
15
|
+
rescue EncodingError
|
|
16
|
+
raise ArgumentError, "key must be valid UTF-8"
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def revision(value)
|
|
20
|
+
raise ArgumentError, "revision must be a non-negative Integer" unless value.is_a?(Integer) && value >= 0
|
|
21
|
+
value
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def limit(value)
|
|
25
|
+
return nil if value.nil?
|
|
26
|
+
raise ArgumentError, "limit must be a positive Integer" unless value.is_a?(Integer) && value.positive?
|
|
27
|
+
value
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def record(value)
|
|
31
|
+
raise SerializationError, "expected Storage::DurableRecord" unless value.is_a?(DurableRecord)
|
|
32
|
+
value.copy
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def attributes(value)
|
|
36
|
+
unless value.is_a?(Hash) && value.all? { |name, item| name.is_a?(Symbol) && [String, Integer, TrueClass, FalseClass, NilClass].any? { |type| item.is_a?(type) } }
|
|
37
|
+
raise ArgumentError, "attributes must contain named scalar values"
|
|
38
|
+
end
|
|
39
|
+
immutable(value)
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def immutable(value)
|
|
43
|
+
case value
|
|
44
|
+
when Hash then value.to_h { |k, v| [immutable(k), immutable(v)] }.freeze
|
|
45
|
+
when Array then value.map { |v| immutable(v) }.freeze
|
|
46
|
+
when String then value.dup.freeze
|
|
47
|
+
when Symbol, Integer, TrueClass, FalseClass, NilClass then value
|
|
48
|
+
else raise ArgumentError, "declarations must contain immutable scalar values"
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
end
|