ace-assign 0.70.0 → 0.73.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.
Files changed (181) 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 +3 -2099
  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 -1
  16. data/handbook/workflow-instructions/assign/drive.wf.md +47 -118
  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 +2 -1
  35. data/lib/ace/assign/organisms/assignment_executor.rb +0 -6
  36. data/lib/ace/assign/organisms/task_assignment_creator.rb +3 -8
  37. data/lib/ace/assign/version.rb +1 -1
  38. data/lib/ace/assign.rb +0 -6
  39. metadata +4 -161
  40. data/.ace-defaults/assign/presets/close-delivery.yml +0 -57
  41. data/.ace-defaults/assign/presets/deliver-release.yml +0 -60
  42. data/.ace-defaults/assign/presets/integrate-batch.yml +0 -72
  43. data/.ace-defaults/assign/presets/supervise-delivery.yml +0 -69
  44. data/docs/protected-authority.md +0 -282
  45. data/exe/ace-scope-ready +0 -55
  46. data/lib/ace/assign/atoms/assignment_scope.rb +0 -58
  47. data/lib/ace/assign/atoms/attempt_state_machine.rb +0 -84
  48. data/lib/ace/assign/atoms/delivery_parameters.rb +0 -19
  49. data/lib/ace/assign/atoms/evidence_digest.rb +0 -78
  50. data/lib/ace/assign/atoms/fixed_merge_schemas.rb +0 -398
  51. data/lib/ace/assign/authority/assignment_definition.rb +0 -35
  52. data/lib/ace/assign/authority/assignment_inventory.rb +0 -290
  53. data/lib/ace/assign/authority/campaign_consumer_policy.rb +0 -100
  54. data/lib/ace/assign/authority/campaign_execution.rb +0 -119
  55. data/lib/ace/assign/authority/campaign_round_transfer.rb +0 -82
  56. data/lib/ace/assign/authority/candidate_transfer.rb +0 -327
  57. data/lib/ace/assign/authority/cleanup_result_protection.rb +0 -53
  58. data/lib/ace/assign/authority/client.rb +0 -257
  59. data/lib/ace/assign/authority/deployment.rb +0 -627
  60. data/lib/ace/assign/authority/deployment_history.rb +0 -146
  61. data/lib/ace/assign/authority/endcap.rb +0 -455
  62. data/lib/ace/assign/authority/endcap_attempts.rb +0 -274
  63. data/lib/ace/assign/authority/endcap_campaign_results.rb +0 -232
  64. data/lib/ace/assign/authority/endcap_campaign_rounds.rb +0 -266
  65. data/lib/ace/assign/authority/endcap_contexts.rb +0 -23
  66. data/lib/ace/assign/authority/endcap_inboxes.rb +0 -37
  67. data/lib/ace/assign/authority/endcap_prepared_work.rb +0 -80
  68. data/lib/ace/assign/authority/endcap_publication.rb +0 -207
  69. data/lib/ace/assign/authority/endcap_results.rb +0 -398
  70. data/lib/ace/assign/authority/endcap_review_requests.rb +0 -183
  71. data/lib/ace/assign/authority/endcap_reviews.rb +0 -130
  72. data/lib/ace/assign/authority/endcap_service_settlement.rb +0 -152
  73. data/lib/ace/assign/authority/endcap_services.rb +0 -598
  74. data/lib/ace/assign/authority/evidence_transfer.rb +0 -67
  75. data/lib/ace/assign/authority/execution_scope_observation.rb +0 -903
  76. data/lib/ace/assign/authority/inbox_context_original.rb +0 -113
  77. data/lib/ace/assign/authority/launch_attempt_consumers.rb +0 -51
  78. data/lib/ace/assign/authority/launch_campaign_registration.rb +0 -214
  79. data/lib/ace/assign/authority/launch_control_channel.rb +0 -313
  80. data/lib/ace/assign/authority/launch_control_exclusions.rb +0 -127
  81. data/lib/ace/assign/authority/launch_driver.rb +0 -393
  82. data/lib/ace/assign/authority/launch_input_inhibition.rb +0 -132
  83. data/lib/ace/assign/authority/launch_lifecycle.rb +0 -997
  84. data/lib/ace/assign/authority/launch_prepared_work.rb +0 -122
  85. data/lib/ace/assign/authority/launch_review.rb +0 -91
  86. data/lib/ace/assign/authority/launch_scope_abort.rb +0 -81
  87. data/lib/ace/assign/authority/launch_scope_admission.rb +0 -215
  88. data/lib/ace/assign/authority/launch_scope_close.rb +0 -146
  89. data/lib/ace/assign/authority/launch_scope_parent.rb +0 -44
  90. data/lib/ace/assign/authority/launch_scope_release.rb +0 -859
  91. data/lib/ace/assign/authority/launch_steering.rb +0 -353
  92. data/lib/ace/assign/authority/launch_stop.rb +0 -254
  93. data/lib/ace/assign/authority/maintenance_workspace_retirement.rb +0 -229
  94. data/lib/ace/assign/authority/ordinary_attempt_start.rb +0 -137
  95. data/lib/ace/assign/authority/ordinary_client.rb +0 -70
  96. data/lib/ace/assign/authority/ordinary_completion.rb +0 -94
  97. data/lib/ace/assign/authority/ordinary_effect_settlement.rb +0 -118
  98. data/lib/ace/assign/authority/ordinary_lifecycle.rb +0 -191
  99. data/lib/ace/assign/authority/ordinary_peer.rb +0 -54
  100. data/lib/ace/assign/authority/ordinary_progress.rb +0 -162
  101. data/lib/ace/assign/authority/ordinary_queue.rb +0 -115
  102. data/lib/ace/assign/authority/ordinary_results.rb +0 -196
  103. data/lib/ace/assign/authority/ordinary_server.rb +0 -75
  104. data/lib/ace/assign/authority/original_inbox_selection.rb +0 -41
  105. data/lib/ace/assign/authority/posix_acl.rb +0 -59
  106. data/lib/ace/assign/authority/prepared_input.rb +0 -130
  107. data/lib/ace/assign/authority/prepared_queue.rb +0 -185
  108. data/lib/ace/assign/authority/prepared_task_context.rb +0 -116
  109. data/lib/ace/assign/authority/prepared_work.rb +0 -304
  110. data/lib/ace/assign/authority/prepared_worker.rb +0 -69
  111. data/lib/ace/assign/authority/private_directory.rb +0 -33
  112. data/lib/ace/assign/authority/protected_assignment_context.rb +0 -226
  113. data/lib/ace/assign/authority/readiness_hook.rb +0 -120
  114. data/lib/ace/assign/authority/receipt_transfer.rb +0 -48
  115. data/lib/ace/assign/authority/router.rb +0 -81
  116. data/lib/ace/assign/authority/server.rb +0 -131
  117. data/lib/ace/assign/authority/service_cleanup_dispatch.rb +0 -88
  118. data/lib/ace/assign/authority/service_cleanup_evidence.rb +0 -180
  119. data/lib/ace/assign/authority/service_delivery_evidence.rb +0 -101
  120. data/lib/ace/assign/authority/service_evidence.rb +0 -286
  121. data/lib/ace/assign/authority/service_publication_evidence.rb +0 -110
  122. data/lib/ace/assign/authority/socket_server.rb +0 -255
  123. data/lib/ace/assign/authority/task_context_entry.rb +0 -32
  124. data/lib/ace/assign/authority/transfer_codec.rb +0 -187
  125. data/lib/ace/assign/cli/commands/attempt/base.rb +0 -84
  126. data/lib/ace/assign/cli/commands/attempt/evidence.rb +0 -28
  127. data/lib/ace/assign/cli/commands/attempt/finish.rb +0 -51
  128. data/lib/ace/assign/cli/commands/attempt/reconcile.rb +0 -49
  129. data/lib/ace/assign/cli/commands/attempt/start.rb +0 -45
  130. data/lib/ace/assign/cli/commands/attempt/status.rb +0 -54
  131. data/lib/ace/assign/cli/commands/authority/inbox_context_principal.rb +0 -25
  132. data/lib/ace/assign/cli/commands/authority/inbox_context_selection.rb +0 -28
  133. data/lib/ace/assign/cli/commands/authority/launch.rb +0 -97
  134. data/lib/ace/assign/cli/commands/authority/serve.rb +0 -31
  135. data/lib/ace/assign/cli/commands/authority/status.rb +0 -26
  136. data/lib/ace/assign/cli/commands/authority/task_context.rb +0 -31
  137. data/lib/ace/assign/cli/commands/authority/task_context_principal.rb +0 -25
  138. data/lib/ace/assign/cli/commands/authority/task_context_selection.rb +0 -30
  139. data/lib/ace/assign/cli/commands/authority/terminate.rb +0 -29
  140. data/lib/ace/assign/cli/commands/authority/worker.rb +0 -23
  141. data/lib/ace/assign/cli/commands/campaign_export_result.rb +0 -55
  142. data/lib/ace/assign/cli/commands/campaign_record_round.rb +0 -57
  143. data/lib/ace/assign/cli/commands/delivery.rb +0 -107
  144. data/lib/ace/assign/cli/commands/inbox_bind.rb +0 -34
  145. data/lib/ace/assign/cli/commands/protected_submission.rb +0 -61
  146. data/lib/ace/assign/cli/commands/resume.rb +0 -38
  147. data/lib/ace/assign/cli/commands/submit_candidate.rb +0 -34
  148. data/lib/ace/assign/cli/commands/submit_result.rb +0 -61
  149. data/lib/ace/assign/models/attempt.rb +0 -178
  150. data/lib/ace/assign/models/attempt_binding.rb +0 -116
  151. data/lib/ace/assign/models/evidence_event.rb +0 -86
  152. data/lib/ace/assign/models/execution_receipt.rb +0 -139
  153. data/lib/ace/assign/molecules/attempt_reconciler.rb +0 -127
  154. data/lib/ace/assign/molecules/attempt_store.rb +0 -201
  155. data/lib/ace/assign/molecules/canonical_attempt_state.rb +0 -151
  156. data/lib/ace/assign/molecules/canonical_evidence.rb +0 -153
  157. data/lib/ace/assign/molecules/canonical_read_commands.rb +0 -84
  158. data/lib/ace/assign/molecules/canonical_read_snapshot.rb +0 -313
  159. data/lib/ace/assign/molecules/evidence_calculator.rb +0 -237
  160. data/lib/ace/assign/molecules/evidence_journal.rb +0 -1912
  161. data/lib/ace/assign/molecules/execution_identity_resolver.rb +0 -137
  162. data/lib/ace/assign/molecules/execution_scope_lineage.rb +0 -345
  163. data/lib/ace/assign/molecules/fixed_effect_reference.rb +0 -31
  164. data/lib/ace/assign/molecules/installed_journal_read.rb +0 -143
  165. data/lib/ace/assign/molecules/journal_input_inhibition.rb +0 -78
  166. data/lib/ace/assign/molecules/journal_mutation.rb +0 -273
  167. data/lib/ace/assign/molecules/journal_prompt_mutation.rb +0 -241
  168. data/lib/ace/assign/molecules/ordinary_merge_cli.rb +0 -68
  169. data/lib/ace/assign/molecules/ordinary_merge_contract.rb +0 -410
  170. data/lib/ace/assign/molecules/original_launch_binding.rb +0 -37
  171. data/lib/ace/assign/molecules/proposal_journal.rb +0 -179
  172. data/lib/ace/assign/molecules/protected_control_exclusion.rb +0 -303
  173. data/lib/ace/assign/molecules/protected_workspace_exclusion.rb +0 -330
  174. data/lib/ace/assign/molecules/protected_workspace_provisioner.rb +0 -164
  175. data/lib/ace/assign/molecules/receipt_verifier.rb +0 -314
  176. data/lib/ace/assign/molecules/terminal_scope_receipt.rb +0 -88
  177. data/lib/ace/assign/organisms/attempt_coordinator.rb +0 -1337
  178. data/lib/ace/assign/organisms/delivery_coordinator.rb +0 -342
  179. data/lib/ace/assign/organisms/ordinary_merge_consumer.rb +0 -365
  180. data/lib/ace/assign/organisms/prepared_work_builder.rb +0 -194
  181. data/lib/ace/assign/organisms/protected_delivery_coordinator.rb +0 -100
@@ -1,68 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require "json"
4
- require "open3"
5
- require_relative "../errors"
6
-
7
- module Ace
8
- module Assign
9
- module Molecules
10
- # Invokes the maintained ordinary fixed merge producer through the
11
- # installed `ace-lab service request/status --mode ordinary-merge`
12
- # CLI. The argv is closed: no shell, no caller-selected executable,
13
- # socket, evidence path or token selector beyond the published request
14
- # form. Output is bounded and parsed as one strict JSON envelope.
15
- #
16
- # Any local execution, framing or parse failure raises
17
- # UnavailableError: a caller-side blocker that never proves refusal
18
- # and never authorizes resubmission.
19
- class OrdinaryMergeCli
20
- MAX_OUTPUT_BYTES = 16 * 1024
21
-
22
- # Raised when no maintained producer response is obtainable before
23
- # the envelope could be observed. Maps to the closed
24
- # `ordinary_merge_unavailable` caller-side blocker.
25
- class UnavailableError < Ace::Assign::AttemptErrors::EvidenceUnavailable
26
- def initialize(message = "ordinary merge producer response unavailable")
27
- super
28
- end
29
- end
30
-
31
- def initialize(binary: "ace-lab")
32
- @binary = binary
33
- end
34
-
35
- def request(project:, assignment:, attempt:, queue_entry:, input_path:, authorization:, request_id:)
36
- run(["service", "request", "--mode", "ordinary-merge",
37
- "--project", project, "--assignment", assignment, "--attempt", attempt,
38
- "--queue-entry", queue_entry, "--operation", "merge", "--input", input_path,
39
- "--authorization", authorization, "--request-id", request_id])
40
- end
41
-
42
- def status(reference:, request_id:, project:, assignment:, attempt:, queue_entry:, input_digest:)
43
- run(["service", "status", "--mode", "ordinary-merge", "--reference", reference,
44
- "--request", request_id, "--project", project, "--assignment", assignment,
45
- "--attempt", attempt, "--queue-entry", queue_entry,
46
- "--input-digest", input_digest, "--format", "json"])
47
- end
48
-
49
- private
50
-
51
- def run(arguments)
52
- output, = Open3.capture3(@binary, *arguments)
53
- raise UnavailableError, "ordinary merge producer output is missing or exceeds its bound" unless
54
- output.is_a?(String) && !output.empty? && output.bytesize <= MAX_OUTPUT_BYTES
55
- envelope = JSON.parse(output, max_nesting: 16, create_additions: false,
56
- allow_duplicate_key: false, allow_comments: false, allow_nan: false)
57
- unless envelope.is_a?(Hash) && envelope["status"].is_a?(String) &&
58
- (envelope.keys - %w[status data error]).empty?
59
- raise UnavailableError, "ordinary merge producer output is not a maintained envelope"
60
- end
61
- envelope
62
- rescue JSON::ParserError, SystemCallError
63
- raise UnavailableError, "ordinary merge producer response unavailable"
64
- end
65
- end
66
- end
67
- end
68
- end
@@ -1,410 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require "time"
4
- require "uri"
5
- require "ace/git/atoms/delivery_parameters"
6
- require_relative "../atoms/evidence_digest"
7
- require_relative "../atoms/fixed_merge_schemas"
8
-
9
- module Ace
10
- module Assign
11
- module Molecules
12
- # Closed wire contract for the ordinary fixed merge producer
13
- # (`8x3.t.xz9.4`) consumed by the qkb.1 ordinary merge consumer through
14
- # `ace-lab service request/status --mode ordinary-merge`.
15
- #
16
- # The consumer invokes and validates the maintained envelopes without
17
- # translation or flattening. Every validator rejects unknown fields at
18
- # every nesting level and enforces the published patterns, formats and
19
- # semantic joins. The terminal receipt stays embedded: request
20
- # selectors live only under the single `request` object, and succeeded
21
- # (merged/already_merged) versus proven-no-effect failed are mutually
22
- # exclusive.
23
- module OrdinaryMergeContract
24
- REQUEST_SCHEMA = "ace.lab.service.ordinary-merge-request/v1"
25
- ACK_SCHEMA = "ace.lab.service.ordinary-merge-ack/v1"
26
- REFUSAL_SCHEMA = "ace.lab.service.ordinary-merge-refusal/v1"
27
- STATUS_SCHEMA = "ace.lab.service.ordinary-merge-status/v1"
28
- STATUS_ERROR_SCHEMA = "ace.lab.service.ordinary-merge-status-error/v1"
29
- RECEIPT_SCHEMA = "ace.assign.fixed-effect-receipt/v1"
30
- REVIEW_SCHEMA = "ace.review.acceptance-reference/v1"
31
- TEST_SCHEMA = "ace.test.execution-reference/v1"
32
-
33
- REFUSAL_CODES = %w[
34
- ordinary_merge_invalid_request
35
- ordinary_merge_unauthenticated
36
- ordinary_merge_forbidden
37
- ordinary_merge_selection_mismatch
38
- ordinary_merge_authorization_invalid
39
- ordinary_merge_provenance_mismatch
40
- ordinary_merge_target_changed
41
- ordinary_merge_evidence_invalid
42
- ordinary_merge_request_conflict
43
- ordinary_merge_unavailable
44
- ].freeze
45
-
46
- # A caller-side blocker: never a proven refusal, never authorization
47
- # for resubmission. Every other code is an authenticated receiver
48
- # refusal that proves no effect began.
49
- UNAVAILABLE_CODE = "ordinary_merge_unavailable"
50
- PROVEN_NO_EFFECT_CODES = (REFUSAL_CODES - [UNAVAILABLE_CODE]).freeze
51
-
52
- STATES = %w[pending uncertain succeeded failed].freeze
53
- TERMINAL_STATES = %w[succeeded failed].freeze
54
- OUTCOMES = %w[merged already_merged failed_no_effect].freeze
55
-
56
- HEX64 = /\A[0-9a-f]{64}\z/
57
- SHA40 = /\A[0-9a-f]{40}\z/
58
- FORGE_NAME = /\A[A-Za-z0-9][A-Za-z0-9._:-]{0,127}\z/
59
- DATETIME = /\A\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d+)?(?:Z|[+-]\d{2}:\d{2})\z/
60
-
61
- REQUEST_KEYS = %w[schema forge pr_url head_repository head_ref candidate_head
62
- base_repository base_ref expected_base_head method review_evidence test_evidence].freeze
63
- REVIEW_KEYS = %w[schema reference reviewer verdict reviewed_head recorded_at record_sha256].freeze
64
- TEST_KEYS = %w[schema reference candidate_head command result recorded_at receipt_sha256].freeze
65
- ACK_KEYS = %w[schema type request_id input_digest reference].freeze
66
- REFUSAL_KEYS = %w[schema code message request_id input_digest].freeze
67
- STATUS_KEYS = %w[schema state reference request_id input_digest receipt].freeze
68
- RECEIPT_KEYS = %w[schema reference request_id input_digest terminal_status request
69
- producer effect remote_observation].freeze
70
- RECEIPT_REQUEST_KEYS = %w[project assignment attempt queue_entry operation authorization input].freeze
71
- PRODUCER_KEYS = %w[receiver_id principal configuration_sha256].freeze
72
- EFFECT_KEYS = %w[started_at finished_at outcome failure_code no_effect_evidence].freeze
73
- OBSERVATION_KEYS = %w[forge_instance pr_identity_url target_repository_identity target_ref_observed
74
- merged_candidate_head admission_target_head resulting_target_head merge_commit
75
- checkout_id worktree_id branch].freeze
76
-
77
- class << self
78
- # Validates and returns the closed ordinary-merge request input.
79
- # Both packages enforce `format` and reject unknown fields at every
80
- # nesting level; evidence heads must equal the candidate head.
81
- def validate_request_input!(document)
82
- exact_object!(document, REQUEST_KEYS, "request input")
83
- const!(document.fetch("schema"), REQUEST_SCHEMA, "request input schema")
84
- pattern!(document.fetch("forge"), FORGE_NAME, "forge")
85
- url!(document.fetch("pr_url"))
86
- bounded!(document.fetch("head_repository"), 1024, "head_repository")
87
- bounded!(document.fetch("head_ref"), 255, "head_ref")
88
- pattern!(document.fetch("candidate_head"), SHA40, "candidate_head")
89
- bounded!(document.fetch("base_repository"), 1024, "base_repository")
90
- bounded!(document.fetch("base_ref"), 255, "base_ref")
91
- pattern!(document.fetch("expected_base_head"), SHA40, "expected_base_head")
92
- const!(document.fetch("method"), "merge", "method")
93
- review = document.fetch("review_evidence")
94
- exact_object!(review, REVIEW_KEYS, "review_evidence")
95
- const!(review.fetch("schema"), REVIEW_SCHEMA, "review_evidence schema")
96
- bounded!(review.fetch("reference"), 256, "review_evidence.reference")
97
- bounded!(review.fetch("reviewer"), 128, "review_evidence.reviewer")
98
- const!(review.fetch("verdict"), "approved", "review_evidence.verdict")
99
- pattern!(review.fetch("reviewed_head"), SHA40, "review_evidence.reviewed_head")
100
- datetime!(review.fetch("recorded_at"), "review_evidence.recorded_at")
101
- pattern!(review.fetch("record_sha256"), HEX64, "review_evidence.record_sha256")
102
- tests = document.fetch("test_evidence")
103
- raise ArgumentError, "request input test_evidence must be a non-empty array" unless tests.is_a?(Array) && !tests.empty?
104
- seen = []
105
- tests.each_with_index do |item, index|
106
- exact_object!(item, TEST_KEYS, "test_evidence[#{index}]")
107
- const!(item.fetch("schema"), TEST_SCHEMA, "test_evidence[#{index}] schema")
108
- bounded!(item.fetch("reference"), 256, "test_evidence[#{index}].reference")
109
- pattern!(item.fetch("candidate_head"), SHA40, "test_evidence[#{index}].candidate_head")
110
- bounded!(item.fetch("command"), 2048, "test_evidence[#{index}].command")
111
- const!(item.fetch("result"), "passed", "test_evidence[#{index}].result")
112
- datetime!(item.fetch("recorded_at"), "test_evidence[#{index}].recorded_at")
113
- pattern!(item.fetch("receipt_sha256"), HEX64, "test_evidence[#{index}].receipt_sha256")
114
- entry = Atoms::EvidenceDigest.digest(item)
115
- raise ArgumentError, "request input test_evidence entries must be unique" if seen.include?(entry)
116
- seen << entry
117
- end
118
- unless review.fetch("reviewed_head") == document.fetch("candidate_head") &&
119
- tests.all? { |item| item.fetch("candidate_head") == document.fetch("candidate_head") }
120
- raise ArgumentError, "request input evidence heads must equal the candidate head"
121
- end
122
- document
123
- end
124
-
125
- # Canonical lowercase SHA-256 input digest of a validated document.
126
- # Canonical request digest — spec ruling r4: the shared producer/
127
- # consumer implementation lives in Atoms::FixedMergeSchemas
128
- # (canonical_request_digest); this contract deliberately keeps no
129
- # local digest computation so both packages can never diverge.
130
-
131
- # Admission acknowledgment: exact keys, exact selector echo, opaque
132
- # reference with no parseable structure implied.
133
- def validate_ack!(data, request_id:, input_digest:)
134
- exact_object!(data, ACK_KEYS, "acknowledgment")
135
- const!(data.fetch("schema"), ACK_SCHEMA, "acknowledgment schema")
136
- const!(data.fetch("type"), "accepted", "acknowledgment type")
137
- selector!(data.fetch("request_id"), "acknowledgment request_id")
138
- pattern!(data.fetch("input_digest"), HEX64, "acknowledgment input_digest")
139
- selector!(data.fetch("reference"), "acknowledgment reference")
140
- unless data.fetch("request_id") == request_id && data.fetch("input_digest") == input_digest
141
- raise ArgumentError, "acknowledgment selectors differ from the recorded binding"
142
- end
143
- data
144
- end
145
-
146
- # Closed outer response envelope: status "ok" carries exactly the
147
- # data object; status "error" carries exactly the error object. The
148
- # outer objects reject unknown fields; the returned branch payload
149
- # is validated by the inner contract validators. Any other outer
150
- # status or shape is malformed. Returns {status:, payload:}.
151
- def validate_envelope_branch!(envelope)
152
- unless envelope.is_a?(Hash) && %w[ok error].include?(envelope["status"])
153
- raise ArgumentError, "response envelope status must be ok or error"
154
- end
155
- if envelope["status"] == "ok"
156
- exact_object!(envelope, %w[status data], "response envelope")
157
- payload = envelope.fetch("data")
158
- else
159
- exact_object!(envelope, %w[status error], "response envelope")
160
- payload = envelope.fetch("error")
161
- end
162
- raise ArgumentError, "response envelope branch must be a JSON object" unless payload.is_a?(Hash)
163
- {status: envelope["status"], payload: payload}
164
- end
165
-
166
- # Pre-admission refusal envelope. Refusal is request-time only; it
167
- # never carries a reference and is never a read-only status value.
168
- def validate_refusal!(error, schema: REFUSAL_SCHEMA)
169
- exact_object!(error, REFUSAL_KEYS, "refusal")
170
- const!(error.fetch("schema"), schema, "refusal schema")
171
- unless REFUSAL_CODES.include?(error.fetch("code"))
172
- raise ArgumentError, "refusal code is outside the closed set"
173
- end
174
- message = error.fetch("message")
175
- unless message.is_a?(String) && !message.empty? && message.length <= 512
176
- raise ArgumentError, "refusal message must be a sanitized non-empty string of at most 512 characters"
177
- end
178
- selector!(error.fetch("request_id"), "refusal request_id")
179
- digest = error.fetch("input_digest")
180
- valid_digest = digest.nil? || (digest.is_a?(String) && digest.match?(HEX64))
181
- unless valid_digest
182
- raise ArgumentError, "refusal input_digest must be a lowercase SHA-256 string or null"
183
- end
184
- error
185
- end
186
-
187
- # Read-only status success envelope. Reports only pending,
188
- # uncertain, succeeded or failed; succeeded/failed carry the one
189
- # embedded terminal receipt, pending/uncertain a null receipt.
190
- def validate_status!(envelope, reference:, request_id:, input_digest:)
191
- exact_object!(envelope, %w[status data], "status envelope")
192
- const!(envelope.fetch("status"), "ok", "status envelope")
193
- data = envelope.fetch("data")
194
- exact_object!(data, STATUS_KEYS, "status data")
195
- const!(data.fetch("schema"), STATUS_SCHEMA, "status schema")
196
- unless STATES.include?(data.fetch("state"))
197
- raise ArgumentError, "status state is outside the closed model"
198
- end
199
- selector!(data.fetch("reference"), "status reference")
200
- selector!(data.fetch("request_id"), "status request_id")
201
- pattern!(data.fetch("input_digest"), HEX64, "status input_digest")
202
- unless data.fetch("reference") == reference && data.fetch("request_id") == request_id &&
203
- data.fetch("input_digest") == input_digest
204
- raise ArgumentError, "status identifiers differ from the recorded binding"
205
- end
206
- receipt = data.fetch("receipt")
207
- if TERMINAL_STATES.include?(data.fetch("state"))
208
- raise ArgumentError, "terminal status requires the embedded receipt" unless receipt.is_a?(Hash)
209
- validated = validate_receipt!(receipt)
210
- raise ArgumentError, "receipt terminal_status differs from the status state" unless validated.fetch("terminal_status") == data.fetch("state")
211
- else
212
- raise ArgumentError, "pending/uncertain status must carry a null receipt" unless receipt.nil?
213
- validated = nil
214
- end
215
- {state: data.fetch("state"), receipt: validated}
216
- end
217
-
218
- # Terminal receipt: embedded request, never flattened. When
219
- # `binding` is given, every command selector and the complete input
220
- # document must equal the recorded binding; success additionally
221
- # joins the remote observation back to the embedded request.
222
- def validate_receipt!(receipt, binding: nil)
223
- exact_object!(receipt, RECEIPT_KEYS, "terminal receipt")
224
- const!(receipt.fetch("schema"), RECEIPT_SCHEMA, "receipt schema")
225
- selector!(receipt.fetch("reference"), "receipt reference")
226
- selector!(receipt.fetch("request_id"), "receipt request_id")
227
- pattern!(receipt.fetch("input_digest"), HEX64, "receipt input_digest")
228
- unless %w[succeeded failed].include?(receipt.fetch("terminal_status"))
229
- raise ArgumentError, "receipt terminal_status is outside the closed set"
230
- end
231
- request = receipt.fetch("request")
232
- exact_object!(request, RECEIPT_REQUEST_KEYS, "receipt request")
233
- bounded!(request.fetch("project"), 128, "receipt request.project")
234
- bounded!(request.fetch("assignment"), 128, "receipt request.assignment")
235
- bounded!(request.fetch("attempt"), 128, "receipt request.attempt")
236
- bounded!(request.fetch("queue_entry"), 128, "receipt request.queue_entry")
237
- const!(request.fetch("operation"), "merge", "receipt request.operation")
238
- selector!(request.fetch("authorization"), "receipt request.authorization")
239
- input = validate_request_input!(request.fetch("input"))
240
- producer = receipt.fetch("producer")
241
- exact_object!(producer, PRODUCER_KEYS, "receipt producer")
242
- bounded!(producer.fetch("receiver_id"), 128, "receipt producer.receiver_id")
243
- bounded!(producer.fetch("principal"), 256, "receipt producer.principal")
244
- pattern!(producer.fetch("configuration_sha256"), HEX64, "receipt producer.configuration_sha256")
245
- unless Atoms::FixedMergeSchemas.canonical_request_digest(
246
- project: request.fetch("project"), assignment: request.fetch("assignment"),
247
- attempt: request.fetch("attempt"), queue_entry: request.fetch("queue_entry"),
248
- authorization: request.fetch("authorization"), request_id: receipt.fetch("request_id"),
249
- input: input
250
- ) == receipt.fetch("input_digest")
251
- raise ArgumentError, "receipt input_digest is not the canonical digest of the embedded binding"
252
- end
253
- validate_effect!(receipt.fetch("effect"))
254
- validate_observation!(receipt.fetch("remote_observation"), input)
255
- enforce_exclusivity!(receipt)
256
- join_observation!(receipt, input)
257
- join_binding!(receipt, binding, input) if binding
258
- receipt
259
- end
260
-
261
- private
262
-
263
- def validate_effect!(effect)
264
- exact_object!(effect, EFFECT_KEYS, "receipt effect")
265
- started_at = effect.fetch("started_at")
266
- datetime!(started_at, "receipt effect.started_at") unless started_at.nil?
267
- datetime!(effect.fetch("finished_at"), "receipt effect.finished_at")
268
- unless OUTCOMES.include?(effect.fetch("outcome"))
269
- raise ArgumentError, "receipt effect.outcome is outside the closed set"
270
- end
271
- failure_code = effect.fetch("failure_code")
272
- unless failure_code.nil? || failure_code == "merge_failed_no_effect"
273
- raise ArgumentError, "receipt effect.failure_code must be null or merge_failed_no_effect"
274
- end
275
- evidence = effect.fetch("no_effect_evidence")
276
- valid_evidence = evidence.nil? || (evidence.is_a?(Hash) &&
277
- evidence.keys.sort == %w[reference sha256].sort &&
278
- bounded?(evidence["reference"], 256) &&
279
- evidence["sha256"].is_a?(String) && evidence["sha256"].match?(HEX64))
280
- unless valid_evidence
281
- raise ArgumentError, "receipt effect.no_effect_evidence must be null or a bounded reference/sha256 object"
282
- end
283
- end
284
-
285
- def validate_observation!(observation, input)
286
- return if observation.nil?
287
- exact_object!(observation, OBSERVATION_KEYS, "receipt remote_observation")
288
- pattern!(observation.fetch("forge_instance"), FORGE_NAME, "remote_observation.forge_instance")
289
- url!(observation.fetch("pr_identity_url"), "remote_observation.pr_identity_url")
290
- bounded!(observation.fetch("target_repository_identity"), 1024, "remote_observation.target_repository_identity")
291
- bounded!(observation.fetch("target_ref_observed"), 255, "remote_observation.target_ref_observed")
292
- %w[merged_candidate_head admission_target_head resulting_target_head merge_commit].each do |key|
293
- pattern!(observation.fetch(key), SHA40, "remote_observation.#{key}")
294
- end
295
- bounded!(observation.fetch("checkout_id"), 256, "remote_observation.checkout_id")
296
- bounded!(observation.fetch("worktree_id"), 256, "remote_observation.worktree_id")
297
- bounded!(observation.fetch("branch"), 255, "remote_observation.branch")
298
- end
299
-
300
- # Succeeded (merged/already_merged) versus proven-no-effect failed
301
- # are mutually exclusive; failure is terminal only with immutable
302
- # no-effect evidence.
303
- def enforce_exclusivity!(receipt)
304
- effect = receipt.fetch("effect")
305
- case receipt.fetch("terminal_status")
306
- when "succeeded"
307
- unless %w[merged already_merged].include?(effect.fetch("outcome")) &&
308
- effect.fetch("failure_code").nil? && effect.fetch("no_effect_evidence").nil? &&
309
- effect.fetch("started_at").is_a?(String) && receipt.fetch("remote_observation").is_a?(Hash)
310
- raise ArgumentError, "succeeded receipt must describe a verified remote outcome without failure evidence"
311
- end
312
- when "failed"
313
- unless effect.fetch("outcome") == "failed_no_effect" &&
314
- effect.fetch("failure_code") == "merge_failed_no_effect" &&
315
- effect.fetch("no_effect_evidence").is_a?(Hash) && receipt.fetch("remote_observation").nil?
316
- raise ArgumentError, "failed receipt must prove no effect and carry no remote observation"
317
- end
318
- end
319
- end
320
-
321
- # `remote_observation` uses distinct observation keys whose values
322
- # are joined back to the embedded request; the fixed merge method
323
- # additionally requires merge_commit == resulting_target_head.
324
- def join_observation!(receipt, input)
325
- observation = receipt.fetch("remote_observation")
326
- return if observation.nil?
327
- unless observation.fetch("forge_instance") == input.fetch("forge") &&
328
- observation.fetch("pr_identity_url") == input.fetch("pr_url") &&
329
- observation.fetch("target_repository_identity") == input.fetch("base_repository") &&
330
- observation.fetch("target_ref_observed") == input.fetch("base_ref") &&
331
- observation.fetch("merged_candidate_head") == input.fetch("candidate_head") &&
332
- observation.fetch("admission_target_head") == input.fetch("expected_base_head") &&
333
- observation.fetch("merge_commit") == observation.fetch("resulting_target_head")
334
- raise ArgumentError, "remote observation does not join back to the embedded request"
335
- end
336
- end
337
-
338
- def join_binding!(receipt, binding, input)
339
- request = receipt.fetch("request")
340
- expected = binding.fetch(:request)
341
- unless receipt.fetch("reference") == binding.fetch(:reference) &&
342
- receipt.fetch("request_id") == binding.fetch(:request_id) &&
343
- receipt.fetch("input_digest") == binding.fetch(:input_digest) &&
344
- request.fetch("project") == expected.fetch(:project) &&
345
- request.fetch("assignment") == expected.fetch(:assignment) &&
346
- request.fetch("attempt") == expected.fetch(:attempt) &&
347
- request.fetch("queue_entry") == expected.fetch(:queue_entry) &&
348
- request.fetch("authorization") == expected.fetch(:authorization) &&
349
- input == expected.fetch(:input)
350
- raise ArgumentError, "embedded request selectors differ from the recorded binding"
351
- end
352
- end
353
-
354
- def exact_object!(value, keys, label)
355
- raise ArgumentError, "#{label} must be a JSON object" unless value.is_a?(Hash)
356
- unless value.keys.sort == keys.sort
357
- raise ArgumentError, "#{label} must have exactly the closed field set"
358
- end
359
- end
360
-
361
- def const!(value, expected, label)
362
- return value if value == expected
363
- raise ArgumentError, "#{label} must be #{expected.inspect}"
364
- end
365
-
366
- def pattern!(value, regex, label)
367
- return value if value.is_a?(String) && value.match?(regex)
368
- raise ArgumentError, "#{label} does not match the closed pattern"
369
- end
370
-
371
- def bounded?(value, max)
372
- value.is_a?(String) && !value.empty? && value.length <= max
373
- end
374
-
375
- def bounded!(value, max, label)
376
- return value if bounded?(value, max)
377
- raise ArgumentError, "#{label} must be a non-empty string of at most #{max} characters"
378
- end
379
-
380
- def selector!(value, label)
381
- bounded!(value, 256, label)
382
- end
383
-
384
- def datetime!(value, label)
385
- unless value.is_a?(String) && value.match?(DATETIME) &&
386
- Time.iso8601(value)
387
- raise ArgumentError, "#{label} must be an RFC 3339 date-time"
388
- end
389
- rescue ArgumentError
390
- raise ArgumentError, "#{label} must be an RFC 3339 date-time"
391
- end
392
-
393
- # `format: uri` plus the maintained neutral PR URL validation: the
394
- # normalized URL produced by the selected forge adapter.
395
- def url!(value, label = "pr_url")
396
- unless value.is_a?(String) && !value.empty? && value.length <= 2048 &&
397
- !value.match?(/\s/) && URI.parse(value).absolute? &&
398
- %w[http https].include?(URI.parse(value).scheme)
399
- raise ArgumentError, "#{label} must be an absolute http(s) URI"
400
- end
401
- Ace::Git::Atoms::DeliveryParameters.validate_url!(value)
402
- value
403
- rescue URI::InvalidURIError, Ace::Git::Error
404
- raise ArgumentError, "#{label} must be a normalized forge PR URL"
405
- end
406
- end
407
- end
408
- end
409
- end
410
- end
@@ -1,37 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require "ace/herdr/molecules/guarded_native_origin"
4
- require_relative "../models/evidence_event"
5
-
6
- module Ace
7
- module Assign
8
- module Molecules
9
- # Exact accepted launch record; this reader grants no native effect.
10
- module OriginalLaunchBinding
11
- def self.verify!(events:, state:, params:)
12
- unless state.is_a?(Hash) && state["guarded_origin"].is_a?(Hash) && state["process_binding"].is_a?(Hash)
13
- raise AttemptErrors::EvidenceUnavailable, "Original guarded record is unavailable"
14
- end
15
- unless Models::EvidenceEvent.chain_valid?(events)
16
- raise AttemptErrors::EvidenceUnavailable, "Original guarded record chain is corrupt"
17
- end
18
- records = events.select { |event| event["type"] == "authority_mutation" && event.dig("payload", "operation") == "record_launch" }
19
- raise AttemptErrors::EvidenceUnavailable, "Original guarded record is ambiguous or absent" unless records.size == 1
20
- record = records.first
21
- data = record.fetch("payload").fetch("data")
22
- unless data.is_a?(Hash) && record["attempt_id"] == params.fetch("attempt_id") &&
23
- %w[mapping_id assignment_id attempt_id].all? { |key| data[key] == params.fetch(key) } &&
24
- data["process_binding"] == state.fetch("process_binding") && data["guarded_origin"] == state.fetch("guarded_origin")
25
- raise AttemptErrors::EvidenceUnavailable, "Original guarded record differs from accepted launch"
26
- end
27
- binding = data.fetch("process_binding")
28
- origin = Ace::Herdr::Molecules::GuardedNativeOrigin.verify!(data.fetch("guarded_origin"),
29
- terminal_id: binding.fetch("terminal_id"), child: binding.fetch("process_identity"))
30
- {"binding_digest" => record.fetch("digest").dup.freeze, "origin" => origin}.freeze
31
- rescue KeyError, TypeError, Ace::Runtime::RuntimeUnavailableError
32
- raise AttemptErrors::EvidenceUnavailable, "Original guarded record is malformed"
33
- end
34
- end
35
- end
36
- end
37
- end