ace-assign 0.71.0 → 0.73.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (197) hide show
  1. checksums.yaml +4 -4
  2. data/.ace-defaults/assign/catalog/steps/auto-merge.step.yml +8 -8
  3. data/.ace-defaults/assign/catalog/steps/create-pr.step.yml +1 -1
  4. data/.ace-defaults/assign/catalog/steps/mark-pr-ready.step.yml +1 -1
  5. data/.ace-defaults/assign/config.yml +0 -13
  6. data/.ace-defaults/assign/presets/fix-bug.yml +1 -1
  7. data/.ace-defaults/assign/presets/work-on-task-ace-development.yml +1 -1
  8. data/.ace-defaults/assign/presets/work-on-task-auto-merge.yml +1 -1
  9. data/.ace-defaults/assign/presets/work-on-task.yml +1 -1
  10. data/CHANGELOG.md +5 -2116
  11. data/README.md +58 -31
  12. data/docs/exit-codes.md +36 -44
  13. data/docs/getting-started.md +9 -4
  14. data/docs/usage.md +129 -522
  15. data/handbook/skills/as-assign-drive/SKILL.md +1 -4
  16. data/handbook/workflow-instructions/assign/drive.wf.md +47 -195
  17. data/handbook/workflow-instructions/assign/prepare.wf.md +1 -1
  18. data/handbook/workflow-instructions/assign/verify-test-suite.wf.md +8 -2
  19. data/lib/ace/assign/cli/commands/assignment_target.rb +2 -15
  20. data/lib/ace/assign/cli/commands/create.rb +1 -8
  21. data/lib/ace/assign/cli/commands/fail.rb +0 -6
  22. data/lib/ace/assign/cli/commands/finish.rb +2 -23
  23. data/lib/ace/assign/cli/commands/fork_session.rb +0 -1
  24. data/lib/ace/assign/cli/commands/select.rb +0 -1
  25. data/lib/ace/assign/cli/commands/start.rb +0 -2
  26. data/lib/ace/assign/cli/commands/status.rb +1 -31
  27. data/lib/ace/assign/cli/commands/step.rb +1 -3
  28. data/lib/ace/assign/cli.rb +1 -85
  29. data/lib/ace/assign/errors.rb +8 -80
  30. data/lib/ace/assign/models/assignment.rb +5 -28
  31. data/lib/ace/assign/molecules/assignment_manager.rb +0 -26
  32. data/lib/ace/assign/molecules/fork_session_launcher.rb +4 -12
  33. data/lib/ace/assign/molecules/lifecycle_exclusion.rb +15 -112
  34. data/lib/ace/assign/molecules/runtime_control_surface_runner.rb +6 -2
  35. data/lib/ace/assign/molecules/skill_assign_source_resolver.rb +9 -0
  36. data/lib/ace/assign/organisms/assignment_executor.rb +0 -6
  37. data/lib/ace/assign/organisms/task_assignment_creator.rb +3 -8
  38. data/lib/ace/assign/version.rb +1 -1
  39. data/lib/ace/assign.rb +0 -6
  40. metadata +22 -176
  41. data/.ace-defaults/assign/presets/close-delivery.yml +0 -57
  42. data/.ace-defaults/assign/presets/deliver-release.yml +0 -60
  43. data/.ace-defaults/assign/presets/integrate-batch.yml +0 -72
  44. data/.ace-defaults/assign/presets/supervise-delivery.yml +0 -69
  45. data/docs/protected-authority.md +0 -282
  46. data/exe/ace-scope-ready +0 -55
  47. data/lib/ace/assign/atoms/assignment_scope.rb +0 -58
  48. data/lib/ace/assign/atoms/attempt_state_machine.rb +0 -84
  49. data/lib/ace/assign/atoms/candidate_limits.rb +0 -20
  50. data/lib/ace/assign/atoms/delivery_parameters.rb +0 -19
  51. data/lib/ace/assign/atoms/evidence_digest.rb +0 -78
  52. data/lib/ace/assign/atoms/fixed_merge_schemas.rb +0 -398
  53. data/lib/ace/assign/atoms/git_blob_batch.rb +0 -59
  54. data/lib/ace/assign/authority/assignment_definition.rb +0 -35
  55. data/lib/ace/assign/authority/assignment_inventory.rb +0 -290
  56. data/lib/ace/assign/authority/campaign_consumer_policy.rb +0 -100
  57. data/lib/ace/assign/authority/campaign_execution.rb +0 -119
  58. data/lib/ace/assign/authority/campaign_round_transfer.rb +0 -82
  59. data/lib/ace/assign/authority/candidate_blob_batch.rb +0 -55
  60. data/lib/ace/assign/authority/candidate_head_comparison.rb +0 -139
  61. data/lib/ace/assign/authority/candidate_transfer.rb +0 -357
  62. data/lib/ace/assign/authority/cleanup_result_protection.rb +0 -53
  63. data/lib/ace/assign/authority/client.rb +0 -257
  64. data/lib/ace/assign/authority/command_execution.rb +0 -180
  65. data/lib/ace/assign/authority/deployment.rb +0 -627
  66. data/lib/ace/assign/authority/deployment_history.rb +0 -146
  67. data/lib/ace/assign/authority/endcap.rb +0 -457
  68. data/lib/ace/assign/authority/endcap_attempts.rb +0 -274
  69. data/lib/ace/assign/authority/endcap_campaign_results.rb +0 -232
  70. data/lib/ace/assign/authority/endcap_campaign_rounds.rb +0 -266
  71. data/lib/ace/assign/authority/endcap_contexts.rb +0 -23
  72. data/lib/ace/assign/authority/endcap_inboxes.rb +0 -37
  73. data/lib/ace/assign/authority/endcap_prepared_work.rb +0 -80
  74. data/lib/ace/assign/authority/endcap_publication.rb +0 -41
  75. data/lib/ace/assign/authority/endcap_results.rb +0 -398
  76. data/lib/ace/assign/authority/endcap_review_requests.rb +0 -183
  77. data/lib/ace/assign/authority/endcap_reviews.rb +0 -130
  78. data/lib/ace/assign/authority/endcap_service_context.rb +0 -38
  79. data/lib/ace/assign/authority/endcap_service_settlement.rb +0 -152
  80. data/lib/ace/assign/authority/evidence_transfer.rb +0 -67
  81. data/lib/ace/assign/authority/execution_scope_observation.rb +0 -903
  82. data/lib/ace/assign/authority/fixed_publication_lifecycle.rb +0 -176
  83. data/lib/ace/assign/authority/fixed_service_lifecycle.rb +0 -594
  84. data/lib/ace/assign/authority/inbox_context_original.rb +0 -113
  85. data/lib/ace/assign/authority/launch_attempt_consumers.rb +0 -51
  86. data/lib/ace/assign/authority/launch_campaign_registration.rb +0 -214
  87. data/lib/ace/assign/authority/launch_control_channel.rb +0 -313
  88. data/lib/ace/assign/authority/launch_control_exclusions.rb +0 -127
  89. data/lib/ace/assign/authority/launch_driver.rb +0 -393
  90. data/lib/ace/assign/authority/launch_input_inhibition.rb +0 -132
  91. data/lib/ace/assign/authority/launch_lifecycle.rb +0 -997
  92. data/lib/ace/assign/authority/launch_prepared_work.rb +0 -122
  93. data/lib/ace/assign/authority/launch_review.rb +0 -91
  94. data/lib/ace/assign/authority/launch_scope_abort.rb +0 -81
  95. data/lib/ace/assign/authority/launch_scope_admission.rb +0 -215
  96. data/lib/ace/assign/authority/launch_scope_close.rb +0 -146
  97. data/lib/ace/assign/authority/launch_scope_parent.rb +0 -44
  98. data/lib/ace/assign/authority/launch_scope_release.rb +0 -859
  99. data/lib/ace/assign/authority/launch_steering.rb +0 -353
  100. data/lib/ace/assign/authority/launch_stop.rb +0 -254
  101. data/lib/ace/assign/authority/maintenance_workspace_retirement.rb +0 -229
  102. data/lib/ace/assign/authority/ordinary_attempt_start.rb +0 -137
  103. data/lib/ace/assign/authority/ordinary_client.rb +0 -70
  104. data/lib/ace/assign/authority/ordinary_completion.rb +0 -104
  105. data/lib/ace/assign/authority/ordinary_effect_settlement.rb +0 -118
  106. data/lib/ace/assign/authority/ordinary_lifecycle.rb +0 -227
  107. data/lib/ace/assign/authority/ordinary_merge_evidence.rb +0 -81
  108. data/lib/ace/assign/authority/ordinary_peer.rb +0 -54
  109. data/lib/ace/assign/authority/ordinary_progress.rb +0 -162
  110. data/lib/ace/assign/authority/ordinary_queue.rb +0 -115
  111. data/lib/ace/assign/authority/ordinary_release_artifacts.rb +0 -143
  112. data/lib/ace/assign/authority/ordinary_results.rb +0 -206
  113. data/lib/ace/assign/authority/ordinary_review_evidence.rb +0 -180
  114. data/lib/ace/assign/authority/ordinary_reviews.rb +0 -214
  115. data/lib/ace/assign/authority/ordinary_server.rb +0 -75
  116. data/lib/ace/assign/authority/ordinary_supervisor.rb +0 -99
  117. data/lib/ace/assign/authority/original_inbox_selection.rb +0 -41
  118. data/lib/ace/assign/authority/posix_acl.rb +0 -59
  119. data/lib/ace/assign/authority/prepared_input.rb +0 -130
  120. data/lib/ace/assign/authority/prepared_queue.rb +0 -185
  121. data/lib/ace/assign/authority/prepared_task_context.rb +0 -116
  122. data/lib/ace/assign/authority/prepared_work.rb +0 -309
  123. data/lib/ace/assign/authority/prepared_worker.rb +0 -69
  124. data/lib/ace/assign/authority/private_directory.rb +0 -33
  125. data/lib/ace/assign/authority/protected_assignment_context.rb +0 -226
  126. data/lib/ace/assign/authority/readiness_hook.rb +0 -120
  127. data/lib/ace/assign/authority/receipt_transfer.rb +0 -48
  128. data/lib/ace/assign/authority/release_artifact_manifest.rb +0 -91
  129. data/lib/ace/assign/authority/retained_input.rb +0 -53
  130. data/lib/ace/assign/authority/router.rb +0 -81
  131. data/lib/ace/assign/authority/server.rb +0 -131
  132. data/lib/ace/assign/authority/service_cleanup_dispatch.rb +0 -88
  133. data/lib/ace/assign/authority/service_cleanup_evidence.rb +0 -180
  134. data/lib/ace/assign/authority/service_delivery_evidence.rb +0 -101
  135. data/lib/ace/assign/authority/service_evidence.rb +0 -286
  136. data/lib/ace/assign/authority/service_publication_evidence.rb +0 -110
  137. data/lib/ace/assign/authority/socket_server.rb +0 -255
  138. data/lib/ace/assign/authority/task_context_entry.rb +0 -32
  139. data/lib/ace/assign/authority/transfer_codec.rb +0 -189
  140. data/lib/ace/assign/cli/commands/attempt/base.rb +0 -84
  141. data/lib/ace/assign/cli/commands/attempt/evidence.rb +0 -28
  142. data/lib/ace/assign/cli/commands/attempt/finish.rb +0 -51
  143. data/lib/ace/assign/cli/commands/attempt/reconcile.rb +0 -49
  144. data/lib/ace/assign/cli/commands/attempt/start.rb +0 -45
  145. data/lib/ace/assign/cli/commands/attempt/status.rb +0 -54
  146. data/lib/ace/assign/cli/commands/authority/inbox_context_principal.rb +0 -25
  147. data/lib/ace/assign/cli/commands/authority/inbox_context_selection.rb +0 -28
  148. data/lib/ace/assign/cli/commands/authority/launch.rb +0 -97
  149. data/lib/ace/assign/cli/commands/authority/serve.rb +0 -31
  150. data/lib/ace/assign/cli/commands/authority/status.rb +0 -26
  151. data/lib/ace/assign/cli/commands/authority/task_context.rb +0 -31
  152. data/lib/ace/assign/cli/commands/authority/task_context_principal.rb +0 -25
  153. data/lib/ace/assign/cli/commands/authority/task_context_selection.rb +0 -30
  154. data/lib/ace/assign/cli/commands/authority/terminate.rb +0 -29
  155. data/lib/ace/assign/cli/commands/authority/worker.rb +0 -23
  156. data/lib/ace/assign/cli/commands/campaign_export_result.rb +0 -55
  157. data/lib/ace/assign/cli/commands/campaign_record_round.rb +0 -57
  158. data/lib/ace/assign/cli/commands/delivery.rb +0 -107
  159. data/lib/ace/assign/cli/commands/inbox_bind.rb +0 -34
  160. data/lib/ace/assign/cli/commands/protected_submission.rb +0 -61
  161. data/lib/ace/assign/cli/commands/resume.rb +0 -38
  162. data/lib/ace/assign/cli/commands/submit_candidate.rb +0 -34
  163. data/lib/ace/assign/cli/commands/submit_result.rb +0 -61
  164. data/lib/ace/assign/models/attempt.rb +0 -178
  165. data/lib/ace/assign/models/attempt_binding.rb +0 -116
  166. data/lib/ace/assign/models/evidence_event.rb +0 -86
  167. data/lib/ace/assign/models/execution_receipt.rb +0 -139
  168. data/lib/ace/assign/molecules/attempt_reconciler.rb +0 -127
  169. data/lib/ace/assign/molecules/attempt_store.rb +0 -201
  170. data/lib/ace/assign/molecules/canonical_attempt_state.rb +0 -151
  171. data/lib/ace/assign/molecules/canonical_evidence.rb +0 -170
  172. data/lib/ace/assign/molecules/canonical_read_commands.rb +0 -84
  173. data/lib/ace/assign/molecules/canonical_read_snapshot.rb +0 -313
  174. data/lib/ace/assign/molecules/evidence_calculator.rb +0 -237
  175. data/lib/ace/assign/molecules/evidence_journal.rb +0 -1899
  176. data/lib/ace/assign/molecules/execution_identity_resolver.rb +0 -137
  177. data/lib/ace/assign/molecules/execution_scope_lineage.rb +0 -345
  178. data/lib/ace/assign/molecules/fixed_effect_reference.rb +0 -31
  179. data/lib/ace/assign/molecules/installed_journal_read.rb +0 -143
  180. data/lib/ace/assign/molecules/journal_input_inhibition.rb +0 -78
  181. data/lib/ace/assign/molecules/journal_mutation.rb +0 -274
  182. data/lib/ace/assign/molecules/journal_prompt_mutation.rb +0 -241
  183. data/lib/ace/assign/molecules/ordinary_merge_cli.rb +0 -68
  184. data/lib/ace/assign/molecules/ordinary_merge_contract.rb +0 -410
  185. data/lib/ace/assign/molecules/original_launch_binding.rb +0 -37
  186. data/lib/ace/assign/molecules/proposal_journal.rb +0 -179
  187. data/lib/ace/assign/molecules/protected_control_exclusion.rb +0 -303
  188. data/lib/ace/assign/molecules/protected_workspace_exclusion.rb +0 -330
  189. data/lib/ace/assign/molecules/protected_workspace_provisioner.rb +0 -164
  190. data/lib/ace/assign/molecules/receipt_verifier.rb +0 -320
  191. data/lib/ace/assign/molecules/terminal_scope_receipt.rb +0 -88
  192. data/lib/ace/assign/organisms/attempt_coordinator.rb +0 -1374
  193. data/lib/ace/assign/organisms/delivery_coordinator.rb +0 -342
  194. data/lib/ace/assign/organisms/ordinary_driver.rb +0 -310
  195. data/lib/ace/assign/organisms/ordinary_merge_consumer.rb +0 -365
  196. data/lib/ace/assign/organisms/prepared_work_builder.rb +0 -194
  197. data/lib/ace/assign/organisms/protected_delivery_coordinator.rb +0 -100
@@ -1,180 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require "ace/runtime/molecules/protected_artifact_set"
4
- require_relative "cleanup_result_protection"
5
-
6
- module Ace
7
- module Assign
8
- module Authority
9
- # The existing service evidence owner authenticates this complete pair.
10
- # Root-private archives remain outside the authority evidence graph.
11
- module ServiceCleanupEvidence
12
- ROOT_SELECTION_SCHEMA = "ace.protected-workspace-prune-root-selection/v1"
13
- ROOT_RECEIPT_SCHEMA = "ace.protected-workspace-prune-receipt/v1"
14
- ROOT_SELECTION_FIELDS = %w[schema request_id input_digest operation_owner_binding_digest receipt_ref].freeze
15
- ROOT_INSPECTION_SELECTION_SCHEMA = "ace.protected-workspace-prune-inspection-selection/v1"
16
- ROOT_INSPECTION_SELECTION_FIELDS = %w[schema request_id input_digest operation_owner_binding_digest inspection_ref].freeze
17
- ROOT_RECEIPT_FIELDS = %w[schema request_id input_digest maintenance target publication canonical_snapshots preservation removal].freeze
18
-
19
- # Pre-import decoding reuses the collection validator. Only the lower
20
- # writer's pending collection can authenticate protected storage and
21
- # accept a terminal update.
22
- def cleanup_inspection_inputs!(contents, record, challenge)
23
- unless record["operation"] == "prune-preserved-workspace" && contents.is_a?(Array) && contents.size == 2
24
- raise AttemptErrors::ReceiptRejected, "cleanup inspection requires its complete pair"
25
- end
26
- cleanup_documents!(contents, record, no_effect: true, challenge: challenge)
27
- true
28
- rescue KeyError, TypeError, JSON::ParserError, JSON::GeneratorError
29
- raise AttemptErrors::ReceiptRejected, "cleanup inspection pair is unavailable"
30
- end
31
-
32
- private
33
-
34
- def cleanup_collection!(references, record, state, pending)
35
- unless record["operation"] == "prune-preserved-workspace" && %w[succeeded failed-settled].include?(state) && references.size == 2
36
- raise AttemptErrors::ReceiptRejected, "cleanup completion requires its complete original evidence pair"
37
- end
38
- no_effect = state == "failed-settled"
39
- challenge = no_effect ? challenge!(record, pending: pending) : nil
40
- expected = no_effect ? context_for_challenge(record, challenge) : context(record)
41
- proof = cleanup_dispatch_context!(record, commit: pending && pending[:commit] || @journal.ref_value, pending: pending)
42
- contents = references.map do |reference|
43
- if pending && pending[:pending_events]
44
- @canonical.read_pending(reference, **expected, **pending.slice(:current_events, :pending_events, :blobs, :commit))
45
- else
46
- @canonical.read(reference, **expected, commit: pending && pending[:commit] || @journal.ref_value)
47
- end
48
- end
49
- reference, operation_bytes = cleanup_documents!(contents, record, no_effect: no_effect,
50
- challenge: challenge)
51
- path = reference.fetch("path")
52
- # The lower writer supplies its pending import transaction. Historical
53
- # consumers have only a pinned commit and never reopen mutable storage.
54
- if pending && pending[:pending_events]
55
- selected_artifacts = @cleanup_artifacts || Ace::Runtime::Molecules::ProtectedArtifactSet.new(
56
- protection: CleanupResultProtection.new(result_path: path, authority_gid: Process.egid))
57
- selected_artifacts.with do |artifacts|
58
- unless artifacts.read!(reference) == operation_bytes
59
- raise AttemptErrors::ReceiptRejected, "cleanup retained result bytes differ"
60
- end
61
- artifacts.verify_unchanged!
62
- end
63
- end
64
- if (view = service_read_view(pending))
65
- view.projection = settlement_projection(record, pending, challenge, proof)
66
- end
67
- contents.freeze
68
- rescue KeyError, TypeError, JSON::ParserError, JSON::GeneratorError, Ace::Runtime::RuntimeUnavailableError
69
- raise AttemptErrors::ReceiptRejected, "cleanup original evidence is unavailable"
70
- end
71
-
72
- def cleanup_documents!(contents, record, no_effect:, challenge:)
73
- documents = contents.map { |bytes| cleanup_json!(bytes) }
74
- selection_schema = no_effect ? ROOT_INSPECTION_SELECTION_SCHEMA : ROOT_SELECTION_SCHEMA
75
- selection_fields = no_effect ? ROOT_INSPECTION_SELECTION_FIELDS : ROOT_SELECTION_FIELDS
76
- selection_index = documents.each_index.select { |index| documents[index]["schema"] == selection_schema }
77
- receipt_index = documents.each_index.select do |index|
78
- no_effect ? documents[index].keys.sort == ServiceEvidence::INSPECTION_FIELDS.sort : documents[index]["schema"] == ROOT_RECEIPT_SCHEMA
79
- end
80
- unless selection_index.one? && receipt_index.one?
81
- raise AttemptErrors::ReceiptRejected, "cleanup original evidence selection is ambiguous"
82
- end
83
- selection = documents.fetch(selection_index.first)
84
- receipt = documents.fetch(receipt_index.first)
85
- closed_cleanup!(selection, selection_fields)
86
- closed_cleanup!(receipt, no_effect ? ServiceEvidence::INSPECTION_FIELDS : ROOT_RECEIPT_FIELDS)
87
- unless [selection, receipt].all? { |value| value["request_id"] == record.fetch("request_id") && value["input_digest"] == record.fetch("input_digest") } &&
88
- selection["operation_owner_binding_digest"] == Atoms::EvidenceDigest.digest(record.fetch("operation_owner_binding"))
89
- raise AttemptErrors::ReceiptRejected, "cleanup original owner or request differs"
90
- end
91
- reference = selection.fetch(no_effect ? "inspection_ref" : "receipt_ref")
92
- closed_cleanup!(reference, %w[path bytes sha256])
93
- path = reference.fetch("path")
94
- operation_bytes = contents.fetch(receipt_index.first)
95
- unless path.is_a?(String) && path.bytesize <= 4096 && path.valid_encoding? && !path.include?("\0") &&
96
- path.start_with?("/") && File.expand_path(path) == path &&
97
- reference["bytes"].is_a?(Integer) && reference["bytes"].between?(1, 65_536) &&
98
- reference["bytes"] == operation_bytes.bytesize && reference["sha256"] == Digest::SHA256.hexdigest(operation_bytes)
99
- raise AttemptErrors::ReceiptRejected, "cleanup retained result reference differs"
100
- end
101
- if no_effect
102
- inspection_projection!(receipt, record, challenge)
103
- else
104
- cleanup_receipt_projection!(receipt, record)
105
- end
106
- [reference, operation_bytes]
107
- end
108
-
109
- def cleanup_json!(bytes)
110
- unless bytes.is_a?(String) && bytes.bytesize.between?(1, 65_536)
111
- raise AttemptErrors::ReceiptRejected, "cleanup evidence exceeds bounds"
112
- end
113
- text = bytes.dup.force_encoding(Encoding::UTF_8)
114
- raise AttemptErrors::ReceiptRejected, "cleanup evidence is invalid UTF8" unless text.valid_encoding? && !text.include?("\0")
115
- value = JSON.parse(text, create_additions: false, max_nesting: 16, allow_duplicate_key: false, allow_comments: false)
116
- raise AttemptErrors::ReceiptRejected, "cleanup evidence is not an object" unless value.is_a?(Hash)
117
- value
118
- end
119
-
120
- def closed_cleanup!(value, fields)
121
- unless value.is_a?(Hash) && value.keys.sort == fields.sort
122
- raise AttemptErrors::ReceiptRejected, "cleanup evidence fields differ"
123
- end
124
- end
125
-
126
- def cleanup_receipt_projection!(receipt, record)
127
- preservation = receipt.fetch("preservation")
128
- closed_cleanup!(preservation, %w[head branch destinations manifest_sha256 inventory_sha256 file_count total_bytes private_manifest_sha256 archives_sha256])
129
- unless preservation["file_count"].is_a?(Integer) && preservation["file_count"].between?(0, 4096) &&
130
- preservation["total_bytes"].is_a?(Integer) && preservation["total_bytes"].between?(0, 256 * 1024 * 1024) &&
131
- %w[manifest_sha256 inventory_sha256 private_manifest_sha256 archives_sha256].all? { |key| cleanup_sha?(preservation[key]) } &&
132
- preservation.values_at("manifest_sha256", "inventory_sha256", "private_manifest_sha256").uniq.one?
133
- raise AttemptErrors::ReceiptRejected, "cleanup preservation observation differs"
134
- end
135
- input = receipt.slice("maintenance", "target", "publication").merge("schema" => "ace.protected-workspace-prune/v1",
136
- "preservation" => preservation.slice("head", "branch", "destinations", "manifest_sha256"))
137
- unless Atoms::EvidenceDigest.digest(input) == record.fetch("input_digest") &&
138
- receipt.fetch("maintenance") == record.slice("project_id", "mapping_id", "assignment_id", "attempt_id")
139
- raise AttemptErrors::ReceiptRejected, "cleanup original input differs"
140
- end
141
- snapshots = receipt.fetch("canonical_snapshots")
142
- unless snapshots.is_a?(Array) && snapshots.size.between?(1, 256)
143
- raise AttemptErrors::ReceiptRejected, "cleanup canonical snapshots exceed bounds"
144
- end
145
- snapshots.each do |snapshot|
146
- closed_cleanup!(snapshot, %w[mapping_id journal_commit binding_event_digest release_event_digest proof_event_digest])
147
- unless snapshot["mapping_id"].is_a?(String) && snapshot["mapping_id"].match?(Molecules::JournalMutation::ID) &&
148
- snapshot["journal_commit"].is_a?(String) && snapshot["journal_commit"].match?(/\A(?:[0-9a-f]{40}|[0-9a-f]{64})\z/) &&
149
- %w[binding_event_digest release_event_digest proof_event_digest].all? { |key| cleanup_sha?(snapshot[key]) }
150
- raise AttemptErrors::ReceiptRejected, "cleanup canonical snapshot differs"
151
- end
152
- end
153
- ids = snapshots.map { |snapshot| snapshot.fetch("mapping_id") }
154
- raise AttemptErrors::ReceiptRejected, "cleanup canonical snapshots are not uniquely ordered" unless ids == ids.uniq.sort
155
- removal = receipt.fetch("removal")
156
- closed_cleanup!(removal, %w[original captured outcome fence_digest])
157
- unless removal["outcome"] == "removed" && cleanup_sha?(removal["fence_digest"])
158
- raise AttemptErrors::ReceiptRejected, "cleanup has no positive removal observation"
159
- end
160
- %w[original captured].each do |key|
161
- object = removal.fetch(key)
162
- closed_cleanup!(object, %w[device inode worktree_admin_id head branch])
163
- unless %w[device inode].all? { |field| object[field].is_a?(Integer) && object[field].positive? } &&
164
- object["worktree_admin_id"].is_a?(String) && object["worktree_admin_id"].bytesize.between?(1, 200) &&
165
- object["worktree_admin_id"].match?(/\A[a-zA-Z0-9_.-]+\z/) &&
166
- object["head"] == preservation["head"] && object["branch"] == preservation["branch"]
167
- raise AttemptErrors::ReceiptRejected, "cleanup captured object differs"
168
- end
169
- end
170
- unless removal.fetch("original") == removal.fetch("captured")
171
- raise AttemptErrors::ReceiptRejected, "cleanup replaced object cannot attest removal"
172
- end
173
- true
174
- end
175
-
176
- def cleanup_sha?(value) = value.is_a?(String) && value.match?(/\A[0-9a-f]{64}\z/)
177
- end
178
- end
179
- end
180
- end
@@ -1,101 +0,0 @@
1
- # frozen_string_literal: true
2
- require "ace/git/atoms/service_merge_evidence"
3
- require "ace/git/atoms/service_pr_evidence"
4
- require_relative "service_evidence"
5
-
6
- module Ace
7
- module Assign
8
- module Authority
9
- # The existing service completion/import transaction owns delivery writes.
10
- class ServiceDeliveryEvidence
11
- def initialize(journal:)
12
- @journal = journal
13
- end
14
-
15
- def completion_event(record:, receipt:, completion_digest:, artifacts:)
16
- unless artifacts.length == 1
17
- raise AttemptErrors::ReceiptRejected, "merge completion requires its one fixed evidence artifact"
18
- end
19
- proof = validate_artifact!(record, artifacts.first)
20
- {type: "delivery", payload: payload(record, receipt, completion_digest, proof)}
21
- end
22
-
23
- def verified(record:, events:, commit:)
24
- unless %w[create update ready merge].include?(record["operation"]) && record["state"] == "succeeded" &&
25
- record.fetch("receipt").fetch("evidence").length == 1
26
- raise AttemptErrors::EvidenceUnavailable, "canonical merge completion is unavailable"
27
- end
28
- receipt = record.fetch("receipt")
29
- reference = receipt.fetch("evidence").first
30
- bytes = Molecules::CanonicalEvidence.new(journal: @journal).read(reference,
31
- **ServiceEvidence.new(journal: @journal).context(record), commit: commit)
32
- expected = payload(record, receipt, record.fetch("completion_digest"), validate_artifact!(record, bytes))
33
- results = events.select { |event| event["type"] == "delivery" && event.dig("payload", "service_request_id") == record.fetch("request_id") }
34
- unless results.one? && Atoms::EvidenceDigest.digest(results.first.fetch("payload")) == Atoms::EvidenceDigest.digest(expected)
35
- raise AttemptErrors::EvidenceUnavailable, "canonical merge delivery result differs"
36
- end
37
- transitions = events.select { |event| event["type"] == "service_transition" &&
38
- event.dig("payload", "request_id") == record.fetch("request_id") && event.dig("payload", "state") == "succeeded" &&
39
- event.dig("payload", "record_digest") == Atoms::EvidenceDigest.digest(record) &&
40
- event.dig("payload", "receipt_digest") == Atoms::EvidenceDigest.digest(receipt) }
41
- imports = events.select { |event| event["type"] == "evidence_import" &&
42
- "evidence/imports/#{event.dig('payload', 'artifact_id')}" == reference.fetch("ref") }
43
- unless transitions.one? && imports.one?
44
- raise AttemptErrors::EvidenceUnavailable, "canonical merge completion introduction differs"
45
- end
46
- # The first completion's authority mutation immediately follows its
47
- # terminal transition. Later identical completion observations may
48
- # have their own mutation IDs; they cannot replace this introduction.
49
- completion = events[events.index(transitions.first) + 1]
50
- unless completion && completion["type"] == "authority_mutation" &&
51
- completion.dig("payload", "operation") == "complete_service" &&
52
- completion.dig("payload", "data", "request_id") == record.fetch("request_id")
53
- raise AttemptErrors::EvidenceUnavailable, "original merge completion mutation differs"
54
- end
55
- selected = [results.first, transitions.first, completion, imports.first]
56
- introductions = @journal.event_commits!(assignment_id: record.fetch("assignment_id"),
57
- event_digests: selected.map { |event| event.fetch("digest") }, commit: commit)
58
- unless introductions.values.uniq.length == 1
59
- raise AttemptErrors::EvidenceUnavailable, "merge delivery result was not atomically imported and completed"
60
- end
61
- results.first
62
- rescue KeyError, TypeError, ArgumentError, AttemptErrors::ReceiptRejected
63
- raise AttemptErrors::EvidenceUnavailable, "canonical merge delivery evidence is unverifiable"
64
- end
65
-
66
- private
67
-
68
- def validate_artifact!(record, bytes)
69
- options = {request_id: record.fetch("request_id"), input_digest: record.fetch("input_digest"),
70
- target: record.fetch("target"), head: record.fetch("candidate_head")}
71
- proof = if record.fetch("operation") == "merge"
72
- Ace::Git::Atoms::ServiceMergeEvidence.validate(bytes, **options)
73
- else
74
- Ace::Git::Atoms::ServicePrEvidence.validate(bytes, operation: record.fetch("operation"), **options)
75
- end
76
- unless Atoms::EvidenceDigest.digest(proof.fetch(:input)) == record.fetch("input_digest")
77
- raise AttemptErrors::ReceiptRejected, "merge evidence does not reconstruct the originally accepted input"
78
- end
79
- proof
80
- rescue ArgumentError, KeyError
81
- raise AttemptErrors::ReceiptRejected, "merge evidence original input or outcome differs"
82
- end
83
-
84
- def payload(record, receipt, completion_digest, proof)
85
- self.class.payload(record, receipt, completion_digest, proof)
86
- end
87
-
88
- # Pure result shape shared with the read-only worker consumer. This
89
- # function creates no evidence or authorization on its own.
90
- def self.payload(record, receipt, completion_digest, proof)
91
- {"stage" => "result", "operation" => record.fetch("operation"), "head" => record.fetch("candidate_head"), "outcome" => "succeeded",
92
- "intent_digest" => nil, "intent_attempt_id" => nil,
93
- "pr" => proof.fetch(:pr).slice("number", "url", "head_sha", "draft", "state", "merge_commit_sha"),
94
- "service_request_id" => record.fetch("request_id"), "service_completion_digest" => completion_digest,
95
- "service_receipt_digest" => Atoms::EvidenceDigest.digest(receipt), "service_evidence" => receipt.fetch("evidence"),
96
- "producer" => {"actor" => "service:#{record.fetch('executor_uid')}", "role" => "service", "runtime" => "herdr"}}
97
- end
98
- end
99
- end
100
- end
101
- end
@@ -1,286 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require_relative "../molecules/evidence_journal"
4
- require_relative "../molecules/canonical_evidence"
5
- require_relative "service_cleanup_evidence"
6
- require_relative "service_cleanup_dispatch"
7
- require_relative "service_publication_evidence"
8
- require_relative "../molecules/execution_scope_lineage"
9
-
10
- module Ace
11
- module Assign
12
- module Authority
13
- # One source-owned context for initial and retained service evidence.
14
- # The journal supplies the record and transient pending view; callers
15
- # never supply an alternate reader, artifact path or trusted flag.
16
- class ServiceEvidence
17
- include ServiceCleanupEvidence
18
- include ServiceCleanupDispatch
19
- include ServicePublicationEvidence
20
- FIELDS = (Molecules::EvidenceJournal::TERMINAL_BINDING_FIELDS +
21
- %w[dispatch_ticket_id claim_binding candidate_generation claim_generation policy_digest]).freeze
22
- CHALLENGE_FIELDS = %w[version request_id input_digest claim_binding dispatch_ticket_id no_effect_challenge
23
- challenge_generation failure_event_digest failure_generation].freeze
24
- INSPECTION_FIELDS = %w[version request_id input_digest claim_binding no_effect_challenge challenge_generation
25
- challenge_event_digest failure_event_digest failure_generation target dispatch_phase effect_absent
26
- handler_terminated writers_absent].freeze
27
-
28
- def initialize(journal:, cleanup_artifacts: nil)
29
- @journal = journal
30
- @canonical = Molecules::CanonicalEvidence.new(journal: journal)
31
- @cleanup_artifacts = cleanup_artifacts
32
- end
33
-
34
- # Executor-only projection from the same authenticated original record;
35
- # selectors never come from the receiver or a caller-supplied target.
36
- def settlement_context(record, commit: @journal.ref_value, read_view: nil)
37
- pending = {commit: commit, service_read_view: read_view}
38
- context(record, pending: pending)
39
- challenge = challenge!(record, pending: pending, current: false) if
40
- %w[no_effect_challenge challenge_generation challenge_event_digest].any? { |key| record.key?(key) }
41
- proof = cleanup_dispatch_context!(record, commit: commit, pending: pending) if
42
- record.values_at("operation", "dispatch_phase") == ["prune-preserved-workspace", "dispatch_started"]
43
- settlement_projection(record, pending, challenge, proof)
44
- rescue KeyError, TypeError
45
- raise AttemptErrors::EvidenceUnavailable, "original service settlement context is incomplete"
46
- end
47
-
48
- def context(record, no_effect: false, pending: nil)
49
- binding = FIELDS.to_h { |key| [key, record.fetch(key)] }
50
- if record.fetch("operation") == "prune-preserved-workspace" && record["dispatch_phase"] == "dispatch_started"
51
- binding["operation_owner_binding"] = record.fetch("operation_owner_binding")
52
- executor = record.fetch("executor_process_binding")
53
- boot = record.fetch("worker_process_binding").fetch("started_at").split(":").fetch(1)
54
- Molecules::ExecutionScopeLineage.validate_process_identity!(executor, boot_id: boot)
55
- raise AttemptErrors::EvidenceUnavailable, "original cleanup executor differs" unless executor.fetch("uid") == record.fetch("executor_uid")
56
- binding["executor_process_binding"] = executor
57
- end
58
- if record["operation"] == "publish" && record["dispatch_phase"] != "issued"
59
- executor = record.fetch("executor_process_binding")
60
- boot = record.fetch("worker_process_binding").fetch("started_at").split(":").fetch(1)
61
- Molecules::ExecutionScopeLineage.validate_process_identity!(executor, boot_id: boot)
62
- raise AttemptErrors::EvidenceUnavailable, "original publication executor differs" unless executor.fetch("uid") == record.fetch("executor_uid")
63
- binding["executor_process_binding"] = executor
64
- end
65
- unless %w[claim_binding policy_digest].all? { |key| binding[key].is_a?(String) && binding[key].match?(/\A[0-9a-f]{64}\z/) } &&
66
- %w[candidate_generation claim_generation].all? { |key| binding[key].is_a?(Integer) && binding[key].positive? } &&
67
- binding["dispatch_ticket_id"].is_a?(String) && binding["dispatch_ticket_id"].match?(Molecules::JournalMutation::ID)
68
- raise AttemptErrors::EvidenceUnavailable, "canonical dispatch binding is invalid"
69
- end
70
- if no_effect
71
- return context_for_challenge(record, challenge!(record, pending: pending))
72
- end
73
- {kind: "service", project_id: record.fetch("project_id"), assignment_id: record.fetch("assignment_id"),
74
- attempt_id: record.fetch("attempt_id"), peer_uid: record.fetch("executor_uid"), binding: binding,
75
- request_id_or_event_id: record.fetch("request_id"), generation: record.fetch("claim_generation")}
76
- rescue KeyError
77
- raise AttemptErrors::EvidenceUnavailable, "canonical service evidence binding is incomplete"
78
- end
79
-
80
- # Exact original challenge and failure selected from the same immutable
81
- # canonical prefix as its import. Pending events are the journal owner's
82
- # held CAS plan, never caller-supplied alternative history.
83
- def challenge!(record, pending: nil, current: true)
84
- context(record)
85
- selector = record.slice("no_effect_challenge", "challenge_generation", "challenge_event_digest")
86
- unless selector.keys.sort == %w[no_effect_challenge challenge_generation challenge_event_digest].sort &&
87
- selector["no_effect_challenge"].is_a?(String) && selector["no_effect_challenge"].match?(/\A[0-9a-f]{32}\z/) &&
88
- selector["challenge_generation"].is_a?(Integer) && selector["challenge_generation"].positive? &&
89
- selector["challenge_event_digest"].is_a?(String) && selector["challenge_event_digest"].match?(/\A[0-9a-f]{64}\z/)
90
- raise AttemptErrors::EvidenceUnavailable, "canonical challenge selector is invalid"
91
- end
92
- commit = pending && pending[:commit] || @journal.ref_value
93
- events = if pending && pending[:pending_events]
94
- pending.fetch(:current_events) + pending.fetch(:pending_events)
95
- else
96
- service_read_events(record, pending)
97
- end
98
- unless Models::EvidenceEvent.chain_valid?(events)
99
- raise AttemptErrors::EvidenceUnavailable, "canonical challenge chain is corrupt"
100
- end
101
- selected = events.select { |event| event["digest"] == record.fetch("challenge_event_digest") }
102
- unless selected.size == 1 && selected.first["type"] == "service_no_effect_challenge"
103
- raise AttemptErrors::EvidenceUnavailable, "canonical challenge event is missing"
104
- end
105
- challenge = selected.first
106
- payload = challenge.fetch("payload")
107
- before = events.take_while { |event| event["digest"] != challenge.fetch("digest") }
108
- failures = events.select do |event|
109
- %w[service_claim service_transition].include?(event["type"]) &&
110
- event.dig("payload", "request_id") == record.fetch("request_id") &&
111
- %w[uncertain failed].include?(event.dig("payload", "state"))
112
- end
113
- failure = before.reverse.find { |event| failures.include?(event) }
114
- failure_prefix = before.take_while { |event| event["digest"] != failure&.fetch("digest") }
115
- generation = failure_prefix.count { |event| event["type"] == "authority_mutation" } + 1
116
- unless payload.is_a?(Hash) && payload.keys.sort == CHALLENGE_FIELDS.sort &&
117
- payload["version"].is_a?(Integer) && payload["version"] == 1 &&
118
- %w[challenge_generation failure_generation].all? { |key| payload[key].is_a?(Integer) && payload[key].positive? } &&
119
- payload["no_effect_challenge"].is_a?(String) && payload["no_effect_challenge"].match?(/\A[0-9a-f]{32}\z/) &&
120
- %w[request_id input_digest claim_binding dispatch_ticket_id no_effect_challenge challenge_generation].all? { |key| payload[key] == record[key] } &&
121
- failure && (!current || failures.last == failure) && before.any? { |event| event["digest"] == failure.fetch("digest") } &&
122
- payload["failure_event_digest"] == failure.fetch("digest") && payload["failure_generation"] == generation &&
123
- payload["challenge_generation"] == before.count { |event| event["type"] == "authority_mutation" } + 1
124
- raise AttemptErrors::EvidenceUnavailable, "canonical challenge or current failure binding differs"
125
- end
126
- position = events.index(challenge)
127
- if pending && pending[:pending_events]&.any? { |event| event["digest"] == challenge.fetch("digest") }
128
- unless pending[:operation] == "claim_service_settlement"
129
- raise AttemptErrors::EvidenceUnavailable, "pending challenge has no fixed authority owner"
130
- end
131
- else
132
- update, accepted = events.values_at(position + 1, position + 2)
133
- expected_params = record.slice("mapping_id", "assignment_id", "attempt_id", "request_id", "candidate_generation").merge(
134
- "head" => record.fetch("candidate_head"), "expected_generation" => payload.fetch("challenge_generation") - 1)
135
- unless update && update["type"] == "service_challenge" && update.dig("payload", "request_id") == record.fetch("request_id") &&
136
- accepted && accepted["type"] == "authority_mutation" && accepted.dig("payload", "operation") == "claim_service_settlement" &&
137
- accepted.dig("payload", "assignment_id") == record.fetch("assignment_id") && accepted.dig("payload", "attempt_id") == record.fetch("attempt_id") &&
138
- accepted.dig("payload", "parameters_digest") == Atoms::EvidenceDigest.digest(expected_params) &&
139
- accepted.dig("payload", "data", "generation").is_a?(Integer) &&
140
- accepted.dig("payload", "data", "generation") == payload.fetch("challenge_generation") &&
141
- accepted.dig("payload", "data", "reconciliation_challenge", "challenge_generation").is_a?(Integer) &&
142
- accepted.dig("payload", "data", "request_id") == record.fetch("request_id") &&
143
- accepted.dig("payload", "data", "reconciliation_challenge") == record.slice("no_effect_challenge", "challenge_generation", "challenge_event_digest")
144
- raise AttemptErrors::EvidenceUnavailable, "canonical challenge lacks its accepted authority mutation"
145
- end
146
- commits = service_read_introductions(record,
147
- [challenge.fetch("digest"), failure.fetch("digest"), update.fetch("digest"), accepted.fetch("digest")], pending)
148
- unless commits.fetch(challenge.fetch("digest")) == commits.fetch(update.fetch("digest")) &&
149
- commits.fetch(challenge.fetch("digest")) == commits.fetch(accepted.fetch("digest"))
150
- raise AttemptErrors::EvidenceUnavailable, "challenge record and authority acceptance were not atomic"
151
- end
152
- original = @journal.service_request(record.fetch("request_id"), commit: commits.fetch(challenge.fetch("digest")))
153
- unless original && %w[uncertain failed].include?(original["state"]) &&
154
- update.dig("payload", "record_digest") == Atoms::EvidenceDigest.digest(original) &&
155
- (FIELDS + %w[mapping_id service_id caller_uid authorization worker_process_binding launch_ticket reservation_generation dispatch_phase]).all? { |key| original[key] == record[key] } &&
156
- original.slice("no_effect_challenge", "challenge_generation", "challenge_event_digest") ==
157
- record.slice("no_effect_challenge", "challenge_generation", "challenge_event_digest")
158
- raise AttemptErrors::EvidenceUnavailable, "challenge accepted record differs from original binding"
159
- end
160
- end
161
- challenge
162
- rescue KeyError, TypeError, NoMethodError
163
- raise AttemptErrors::EvidenceUnavailable, "canonical challenge binding is incomplete"
164
- end
165
-
166
- def inspection!(content, record, challenge)
167
- unless content.is_a?(String) && content.bytesize <= Molecules::CanonicalEvidence::MAX_ARTIFACT_BYTES
168
- raise AttemptErrors::EvidenceUnavailable, "no-effect inspection exceeds artifact bounds"
169
- end
170
- text = content.dup.force_encoding(Encoding::UTF_8)
171
- raise AttemptErrors::EvidenceUnavailable, "no-effect inspection is invalid UTF8" unless text.valid_encoding?
172
- lines = text.lines.filter_map { |line| line.delete_prefix("ace-service-no-effect ").chomp if line.start_with?("ace-service-no-effect ") }
173
- raise AttemptErrors::EvidenceUnavailable, "no-effect inspection is missing or repeated" unless lines.size == 1
174
- inspection = JSON.parse(lines.first, create_additions: false, max_nesting: 8,
175
- allow_duplicate_key: false, allow_comments: false)
176
- inspection_projection!(inspection, record, challenge)
177
- rescue JSON::ParserError, KeyError, TypeError
178
- raise AttemptErrors::EvidenceUnavailable, "no-effect inspection is malformed"
179
- end
180
-
181
- def inspection_projection!(inspection, record, challenge)
182
- expected = challenge.fetch("payload").slice("request_id", "input_digest", "claim_binding", "no_effect_challenge",
183
- "challenge_generation", "failure_event_digest", "failure_generation").merge("version" => 1,
184
- "challenge_event_digest" => challenge.fetch("digest"), "target" => record.fetch("target"),
185
- "dispatch_phase" => record.fetch("dispatch_phase"), "effect_absent" => true,
186
- "handler_terminated" => true, "writers_absent" => true)
187
- unless inspection.is_a?(Hash) && inspection.keys.sort == INSPECTION_FIELDS.sort &&
188
- %w[version challenge_generation failure_generation].all? { |key| inspection[key].is_a?(Integer) } &&
189
- inspection == expected
190
- raise AttemptErrors::EvidenceUnavailable, "no-effect inspection differs from original challenge"
191
- end
192
- true
193
- rescue KeyError, TypeError
194
- raise AttemptErrors::EvidenceUnavailable, "no-effect inspection is malformed"
195
- end
196
-
197
- def call(reference, record, state, pending)
198
- if reference.is_a?(Array)
199
- return publication_collection!(reference, record, state, pending) if record["operation"] == "publish"
200
- return cleanup_collection!(reference, record, state, pending)
201
- end
202
- no_effect = state == "failed-settled"
203
- expected = context(record, no_effect: no_effect, pending: pending)
204
- bytes = if pending && pending[:pending_events]
205
- @canonical.read_pending(reference, **expected, **pending.slice(:current_events, :pending_events, :blobs, :commit))
206
- else
207
- @canonical.read(reference, **expected, commit: pending && pending[:commit] || @journal.ref_value)
208
- end
209
- if no_effect
210
- challenge = challenge!(record, pending: pending)
211
- inspection!(bytes, record, challenge)
212
- end
213
- bytes
214
- end
215
-
216
- private
217
-
218
- def settlement_projection(record, pending, challenge, proof)
219
- selected = nil
220
- if challenge
221
- latest_failure = service_read_events(record, pending).reverse.find do |event|
222
- event["attempt_id"] == record.fetch("attempt_id") &&
223
- %w[service_claim service_transition].include?(event["type"]) &&
224
- event.dig("payload", "request_id") == record.fetch("request_id") &&
225
- %w[uncertain failed].include?(event.dig("payload", "state"))
226
- end
227
- if latest_failure && latest_failure.fetch("digest") == challenge.dig("payload", "failure_event_digest")
228
- selected = challenge.fetch("payload").slice("request_id", "input_digest", "claim_binding",
229
- "no_effect_challenge", "challenge_generation", "failure_event_digest", "failure_generation")
230
- .merge("challenge_event_digest" => challenge.fetch("digest"))
231
- end
232
- end
233
- execution = record.slice("mapping_id", "candidate_generation", "claim_binding", "dispatch_phase")
234
- .merge("head" => record.fetch("candidate_head"))
235
- if record["operation"] == "prune-preserved-workspace" && record["dispatch_phase"] == "dispatch_started"
236
- execution.merge!(record.slice("operation_owner_binding", "executor_process_binding"))
237
- execution.merge!(proof.slice("request_event_digest", "dispatch_event_digest"))
238
- end
239
- value = {"version" => 1, "request" => record.slice(*Molecules::EvidenceJournal::TERMINAL_BINDING_FIELDS),
240
- "execution" => execution, "challenge" => selected}
241
- freeze_projection(JSON.parse(JSON.generate(value)))
242
- end
243
-
244
- def context_for_challenge(record, selection)
245
- expected = context(record)
246
- binding = expected.fetch(:binding)
247
- binding.merge!(record.slice("no_effect_challenge", "challenge_generation", "challenge_event_digest"))
248
- binding.merge!(selection.fetch("payload").slice("failure_event_digest", "failure_generation"))
249
- binding["dispatch_phase"] = record.fetch("dispatch_phase")
250
- expected
251
- end
252
-
253
- def service_read_view(pending)
254
- view = pending && pending[:service_read_view]
255
- return nil unless view
256
- unless view.is_a?(Molecules::EvidenceJournal::ServiceReadView) && view.journal.equal?(@journal) &&
257
- view.commit == pending.fetch(:commit) && view.inventory.fetch("commit") == view.commit
258
- raise AttemptErrors::EvidenceUnavailable, "service read view differs from its selected owner"
259
- end
260
- view
261
- end
262
-
263
- def service_read_events(record, pending)
264
- view = service_read_view(pending)
265
- selected = view ? view.inventory.fetch("events").fetch(record.fetch("assignment_id")) :
266
- @journal.read_events(record.fetch("assignment_id"), commit: pending && pending[:commit] || @journal.ref_value)
267
- selected.select { |event| event.fetch("attempt_id") == record.fetch("attempt_id") }
268
- end
269
-
270
- def service_read_introductions(record, selectors, pending)
271
- view = service_read_view(pending)
272
- return @journal.event_commits!(assignment_id: record.fetch("assignment_id"), event_digests: selectors,
273
- commit: pending && pending[:commit] || @journal.ref_value) unless view
274
- facts = view.inventory.fetch("introductions").fetch(record.fetch("assignment_id"))
275
- selectors.to_h { |selector| [selector, facts.fetch(selector)] }.freeze
276
- end
277
-
278
- def freeze_projection(value)
279
- value.each_value { |child| freeze_projection(child) } if value.is_a?(Hash)
280
- value.each { |child| freeze_projection(child) } if value.is_a?(Array)
281
- value.freeze
282
- end
283
- end
284
- end
285
- end
286
- end