ace-assign 0.67.0 → 0.68.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.
@@ -0,0 +1,365 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "tempfile"
4
+ require_relative "../errors"
5
+ require_relative "../molecules/ordinary_merge_contract"
6
+ require_relative "../molecules/ordinary_merge_cli"
7
+
8
+ module Ace
9
+ module Assign
10
+ module Organisms
11
+ # Ordinary project-overseer merge consumer (qkb.1).
12
+ #
13
+ # Records the exact immutable integration request as attempt evidence
14
+ # before dispatch, invokes the fixed producer through `ace-lab service
15
+ # request/status --mode ordinary-merge` with the closed selectors,
16
+ # persists the receiver-owned opaque reference, restores it after a
17
+ # queue-owner restart, and accepts only the exact terminal receipt.
18
+ #
19
+ # ace-assign attempt/evidence state is the sole queue/effect record:
20
+ # no caller-local journal, receipt ledger or second engine exists here.
21
+ # Lost acknowledgments, requester/receiver restarts, unavailable
22
+ # receivers and uncertain original results reconcile only by the
23
+ # recorded binding and reference, performing at most one merge. An
24
+ # uncertain original result is never resubmitted; only a proven
25
+ # pre-effect refusal closes an intent for a new reviewed request.
26
+ # Read-only status polls never dispatch, repair or refresh anything,
27
+ # and no `refused` state exists as status — refusal is request-time
28
+ # only.
29
+ class OrdinaryMergeConsumer
30
+ INTENT_STAGE = "ordinary-merge-intent"
31
+ ACK_STAGE = "ordinary-merge-ack"
32
+ RESULT_STAGE = "ordinary-merge-result"
33
+
34
+ def initialize(repo_root:, journal: nil, coordinator: nil, identity_resolver: nil, producer: nil)
35
+ @repo_root = repo_root
36
+ @journal = journal || Molecules::EvidenceJournal.new(repo_root: repo_root)
37
+ @coordinator = coordinator || AttemptCoordinator.new(repo_root: repo_root, journal: @journal)
38
+ @identity_resolver = identity_resolver || Molecules::ExecutionIdentityResolver.new
39
+ @producer = producer || Molecules::OrdinaryMergeCli.new
40
+ end
41
+
42
+ # Records the immutable binding and dispatches one admission request.
43
+ # Re-invocation with the identical recorded binding reconciles by the
44
+ # same request ID (an identical digest returns the original
45
+ # acknowledgment); any different binding while an effect is unresolved
46
+ # is forbidden resubmission.
47
+ def dispatch(assignment_id:, attempt_id:, project:, queue_entry:, input:, authorization:, request_id:)
48
+ identity = trusted_identity!
49
+ binding = identity_binding!(assignment_id, attempt_id)
50
+ attempt = authoritative_attempt!(binding)
51
+ raise AttemptErrors::ReceiptRejected, "Ordinary merge attempt is no longer active" unless attempt.active?
52
+ input_document = validate_input!(input)
53
+ selectors = validated_selectors(project: project, queue_entry: queue_entry,
54
+ authorization: authorization, request_id: request_id)
55
+ digest = Atoms::FixedMergeSchemas.canonical_request_digest(
56
+ project: selectors[:project], assignment: binding.fetch("assignment_id"),
57
+ attempt: binding.fetch("attempt_id"), queue_entry: selectors[:queue_entry],
58
+ authorization: selectors[:authorization], request_id: selectors[:request_id],
59
+ input: input_document
60
+ )
61
+ recorded = merge_effects(binding)
62
+ if (intent = recorded[:intent])
63
+ unless identical_binding?(intent.fetch("payload"), selectors, digest)
64
+ raise AttemptErrors::Conflict,
65
+ "Unresolved ordinary merge binding requires read-only status reconciliation; resubmission is forbidden"
66
+ end
67
+ return admit(binding, identity, selectors, input_document, digest, record_intent: false)
68
+ end
69
+ raise AttemptErrors::InvalidState, "Ordinary merge is already terminal for this attempt" if recorded[:receipt]
70
+ admit(binding, identity, selectors, input_document, digest, record_intent: true)
71
+ rescue Molecules::OrdinaryMergeCli::UnavailableError
72
+ raise AttemptErrors::EvidenceUnavailable,
73
+ "ordinary merge receiver is unavailable before admission; reconciliation by the recorded binding is required, resubmission is forbidden"
74
+ rescue ArgumentError
75
+ raise AttemptErrors::ReceiptRejected, "ordinary merge request violates the closed contract"
76
+ end
77
+
78
+ # Read-only observation. Recovers the binding and opaque reference
79
+ # from canonical attempt evidence, polls the producer status command,
80
+ # and accepts only the exact terminal receipt. Once a terminal
81
+ # receipt is accepted it is returned without further producer calls.
82
+ def poll(assignment_id:, attempt_id:)
83
+ trusted_identity!
84
+ binding = identity_binding!(assignment_id, attempt_id)
85
+ authoritative_attempt!(binding)
86
+ recorded = merge_effects(binding)
87
+ return terminal_result(recorded[:receipt].fetch("payload")) if recorded[:receipt]
88
+ unless recorded[:intent]
89
+ raise AttemptErrors::NotFound,
90
+ "No ordinary merge request is recorded or unresolved for this attempt"
91
+ end
92
+ intent = recorded[:intent].fetch("payload")
93
+ selectors = intent_selectors(intent)
94
+ input_document = intent.fetch("input")
95
+ digest = rederive_recorded_digest(binding, intent)
96
+ reference = recorded[:ack]&.fetch("payload")&.fetch("reference")
97
+ return recover_admission(binding, selectors, input_document, digest) unless reference
98
+ observe(binding, selectors, reference, input_document, digest)
99
+ rescue Molecules::OrdinaryMergeCli::UnavailableError
100
+ raise AttemptErrors::EvidenceUnavailable,
101
+ "ordinary merge status is unavailable; read-only reconciliation by the recorded reference is required"
102
+ end
103
+
104
+ private
105
+
106
+ def trusted_identity!
107
+ identity = @identity_resolver.resolve
108
+ raise AttemptErrors::UnauthorizedIdentity, "Only a trusted coordinator may consume ordinary merge" unless
109
+ @identity_resolver.trusted?(identity)
110
+ identity
111
+ end
112
+
113
+ def identity_binding!(assignment_id, attempt_id)
114
+ [assignment_id, attempt_id].each do |value|
115
+ raise ArgumentError, "ordinary merge assignment/attempt must match the closed token grammar" unless
116
+ value.is_a?(String) && value.match?(Molecules::JournalMutation::ID)
117
+ end
118
+ {"assignment_id" => assignment_id, "attempt_id" => attempt_id}
119
+ end
120
+
121
+ def authoritative_attempt!(binding)
122
+ attempt = @coordinator.authoritative_attempt(binding)
123
+ raise AttemptErrors::NotFound, "Ordinary merge attempt not found" unless attempt
124
+ raise AttemptErrors::ReceiptRejected, "Ordinary merge requires a managed attempt" unless attempt.managed?
125
+ attempt
126
+ end
127
+
128
+ def validate_input!(input)
129
+ Molecules::OrdinaryMergeContract.validate_request_input!(input)
130
+ end
131
+
132
+ def validated_selectors(project:, queue_entry:, authorization:, request_id:)
133
+ {"project" => project, "queue_entry" => queue_entry, "request_id" => request_id}.each do |label, value|
134
+ raise ArgumentError, "ordinary merge #{label} must match the closed token grammar" unless
135
+ value.is_a?(String) && value.match?(Molecules::JournalMutation::ID)
136
+ end
137
+ unless authorization.is_a?(String) && authorization.length.between?(1, 256)
138
+ raise ArgumentError, "ordinary merge authorization reference must be a non-empty string of at most 256 characters"
139
+ end
140
+ {project: project, queue_entry: queue_entry, authorization: authorization, request_id: request_id}
141
+ end
142
+
143
+ def intent_selectors(intent)
144
+ {project: intent.fetch("project"), queue_entry: intent.fetch("queue_entry"),
145
+ authorization: intent.fetch("authorization"), request_id: intent.fetch("request_id")}
146
+ end
147
+
148
+ def identical_binding?(intent, selectors, digest)
149
+ intent_selectors(intent) == selectors && intent.fetch("input_digest") == digest
150
+ end
151
+
152
+ # The canonical request digest (spec ruling r4) is the SHARED
153
+ # Atoms::FixedMergeSchemas.canonical_request_digest implementation —
154
+ # this consumer deliberately keeps no local digest computation.
155
+
156
+ # Re-derives the identical digest from the recorded binding and
157
+ # verifies it against the stored value before any echo validation.
158
+ def rederive_recorded_digest(binding, intent)
159
+ selectors = intent_selectors(intent)
160
+ derived = Atoms::FixedMergeSchemas.canonical_request_digest(
161
+ project: selectors[:project], assignment: binding.fetch("assignment_id"),
162
+ attempt: binding.fetch("attempt_id"), queue_entry: selectors[:queue_entry],
163
+ authorization: selectors[:authorization], request_id: selectors[:request_id],
164
+ input: intent.fetch("input")
165
+ )
166
+ unless derived == intent.fetch("input_digest")
167
+ raise AttemptErrors::ReceiptRejected,
168
+ "recorded ordinary merge binding digest does not re-derive from the recorded binding"
169
+ end
170
+ derived
171
+ end
172
+
173
+ # The recorded ordinary-merge effects for one attempt: the open
174
+ # intent (if any), its acknowledgment reference and an accepted
175
+ # terminal receipt. Intents settle only through a result event for
176
+ # the same request ID.
177
+ def merge_effects(binding)
178
+ events = @journal.read_events(binding.fetch("assignment_id")).select do |event|
179
+ event["type"] == "delivery" && event["attempt_id"] == binding.fetch("attempt_id") &&
180
+ event.dig("payload", "stage").to_s.start_with?("ordinary-merge-")
181
+ end
182
+ intents = events.select { |event| event.dig("payload", "stage") == INTENT_STAGE }
183
+ settled = events.select { |event| event.dig("payload", "stage") == RESULT_STAGE }
184
+ .map { |event| event.dig("payload", "request_id") }
185
+ open = intents.reject { |event| settled.include?(event.dig("payload", "request_id")) }
186
+ raise AttemptErrors::Conflict, "Multiple unresolved ordinary merge bindings require reconciliation" if open.length > 1
187
+ intent = open.last
188
+ ack = intent && events.reverse.find do |event|
189
+ event.dig("payload", "stage") == ACK_STAGE &&
190
+ event.dig("payload", "request_id") == intent.dig("payload", "request_id")
191
+ end
192
+ receipt = events.reverse.find do |event|
193
+ event.dig("payload", "stage") == RESULT_STAGE &&
194
+ %w[succeeded failed].include?(event.dig("payload", "terminal_status"))
195
+ end
196
+ {intent: intent, ack: ack, receipt: receipt}
197
+ end
198
+
199
+ def admit(binding, identity, selectors, input_document, digest, record_intent:)
200
+ record_event(binding, identity, intent_payload(selectors, input_document, digest)) if record_intent
201
+ branch = admission_branch(request_admission(binding, selectors, input_document))
202
+ if branch[:status] == "ok"
203
+ ack = begin
204
+ Molecules::OrdinaryMergeContract.validate_ack!(branch[:payload],
205
+ request_id: selectors[:request_id], input_digest: digest)
206
+ rescue ArgumentError
207
+ raise AttemptErrors::EvidenceUnavailable,
208
+ "ordinary merge acknowledgment is not the maintained contract; admission stays unconfirmed"
209
+ end
210
+ record_event(binding, identity, {"stage" => ACK_STAGE, "reference" => ack.fetch("reference"),
211
+ "request_id" => ack.fetch("request_id"), "input_digest" => ack.fetch("input_digest")})
212
+ return {state: "accepted", reference: ack.fetch("reference"),
213
+ request_id: ack.fetch("request_id"), input_digest: ack.fetch("input_digest")}
214
+ end
215
+ handle_refusal(binding, identity, branch[:payload], expected_digest: digest)
216
+ end
217
+
218
+ # Closed outer envelope for admission responses: exactly {status,
219
+ # data} for ok and exactly {status, error} for error, both objects
220
+ # rejecting unknown fields. Any other outer status or shape —
221
+ # including a refusal attached to a non-error envelope such as
222
+ # pending — is malformed, leaves admission unconfirmed, and is never
223
+ # persisted as a proven refusal.
224
+ def admission_branch(envelope)
225
+ Molecules::OrdinaryMergeContract.validate_envelope_branch!(envelope)
226
+ rescue ArgumentError
227
+ raise AttemptErrors::EvidenceUnavailable,
228
+ "ordinary merge response is not the maintained envelope; admission stays unconfirmed"
229
+ end
230
+
231
+ def request_admission(binding, selectors, input_document)
232
+ input_path = nil
233
+ Tempfile.create("ace-assign-ordinary-merge-input") do |file|
234
+ file.write(Atoms::EvidenceDigest.canonical_json(input_document))
235
+ file.flush
236
+ input_path = file.path
237
+ return @producer.request(project: selectors[:project], assignment: binding.fetch("assignment_id"),
238
+ attempt: binding.fetch("attempt_id"), queue_entry: selectors[:queue_entry],
239
+ input_path: input_path, authorization: selectors[:authorization],
240
+ request_id: selectors[:request_id])
241
+ end
242
+ end
243
+
244
+ def handle_refusal(binding, identity, error, expected_digest:)
245
+ refusal = begin
246
+ Molecules::OrdinaryMergeContract.validate_refusal!(error)
247
+ rescue ArgumentError
248
+ raise AttemptErrors::EvidenceUnavailable,
249
+ "ordinary merge refusal is not the maintained contract; admission stays unconfirmed"
250
+ end
251
+ echo = refusal.fetch("input_digest")
252
+ if echo && echo != expected_digest
253
+ raise AttemptErrors::EvidenceUnavailable,
254
+ "ordinary merge refusal echoes a different input digest; admission stays unconfirmed"
255
+ end
256
+ if refusal.fetch("code") == Molecules::OrdinaryMergeContract::UNAVAILABLE_CODE
257
+ raise AttemptErrors::EvidenceUnavailable,
258
+ "ordinary merge receiver refused as unavailable before admission; this is a blocker, never a proven refusal"
259
+ end
260
+ record_event(binding, identity, {"stage" => RESULT_STAGE, "outcome" => "refused_no_effect",
261
+ "code" => refusal.fetch("code"), "message" => refusal.fetch("message"),
262
+ "request_id" => refusal.fetch("request_id"), "input_digest" => refusal.fetch("input_digest")})
263
+ {state: "refused", code: refusal.fetch("code"), message: refusal.fetch("message")}
264
+ end
265
+
266
+ # Lost acknowledgment after durable admission: reconcile by the
267
+ # identical recorded binding. A request ID bound to the identical
268
+ # digest returns the original acknowledgment and reference.
269
+ def recover_admission(binding, selectors, input_document, digest)
270
+ identity = trusted_identity!
271
+ result = admit(binding, identity, selectors, input_document, digest, record_intent: false)
272
+ return result if result.is_a?(Hash) && result[:state] == "refused"
273
+ unless result.is_a?(Hash) && result[:state] == "accepted"
274
+ raise AttemptErrors::EvidenceUnavailable, "ordinary merge acknowledgment is still unavailable"
275
+ end
276
+ observe(binding, selectors, result.fetch(:reference), input_document, digest)
277
+ end
278
+
279
+ def observe(binding, selectors, reference, input_document, digest)
280
+ envelope = @producer.status(reference: reference, request_id: selectors[:request_id],
281
+ project: selectors[:project], assignment: binding.fetch("assignment_id"),
282
+ attempt: binding.fetch("attempt_id"), queue_entry: selectors[:queue_entry],
283
+ input_digest: digest)
284
+ branch = status_branch(envelope)
285
+ if branch[:status] == "error"
286
+ begin
287
+ validated = Molecules::OrdinaryMergeContract.validate_refusal!(branch[:payload],
288
+ schema: Molecules::OrdinaryMergeContract::STATUS_ERROR_SCHEMA)
289
+ rescue ArgumentError
290
+ raise AttemptErrors::EvidenceUnavailable, "ordinary merge status error is not the maintained contract"
291
+ end
292
+ echo = validated.fetch("input_digest")
293
+ if echo && echo != digest
294
+ raise AttemptErrors::EvidenceUnavailable,
295
+ "ordinary merge status error echoes a different input digest; never a durable state"
296
+ end
297
+ raise AttemptErrors::EvidenceUnavailable,
298
+ "ordinary merge status command error (#{validated.fetch("code")}); a command error is never a durable state or a refusal"
299
+ end
300
+ observed = begin
301
+ Molecules::OrdinaryMergeContract.validate_status!(envelope, reference: reference,
302
+ request_id: selectors[:request_id], input_digest: digest)
303
+ rescue ArgumentError
304
+ raise AttemptErrors::ReceiptRejected, "ordinary merge status or receipt differs from the closed contract"
305
+ end
306
+ return {state: observed[:state]} unless observed[:receipt]
307
+ accept_receipt(binding, selectors, reference, input_document, digest, observed[:receipt])
308
+ end
309
+
310
+ # Closed outer envelope for read-only status responses. A malformed
311
+ # shape — including a missing branch object — is a classified caller-
312
+ # side blocker, never a raw internal error and never a durable state.
313
+ def status_branch(envelope)
314
+ Molecules::OrdinaryMergeContract.validate_envelope_branch!(envelope)
315
+ rescue ArgumentError
316
+ raise AttemptErrors::EvidenceUnavailable,
317
+ "ordinary merge status response is not the maintained envelope"
318
+ end
319
+
320
+ def accept_receipt(binding, selectors, reference, input_document, digest, receipt)
321
+ receipt_binding = {reference: reference, request_id: selectors[:request_id], input_digest: digest,
322
+ request: {project: selectors[:project], assignment: binding.fetch("assignment_id"),
323
+ attempt: binding.fetch("attempt_id"), queue_entry: selectors[:queue_entry],
324
+ authorization: selectors[:authorization], input: input_document}}
325
+ accepted = begin
326
+ Molecules::OrdinaryMergeContract.validate_receipt!(receipt, binding: receipt_binding)
327
+ rescue ArgumentError
328
+ raise AttemptErrors::ReceiptRejected, "terminal receipt differs from the recorded binding or the closed schema"
329
+ end
330
+ payload = {"stage" => RESULT_STAGE, "terminal_status" => accepted.fetch("terminal_status"),
331
+ "outcome" => accepted.fetch("effect").fetch("outcome"), "reference" => reference,
332
+ "request_id" => selectors[:request_id], "input_digest" => digest, "receipt" => accepted,
333
+ "synchronization_required" => accepted.fetch("effect").fetch("outcome") == "already_merged"}
334
+ record_event(binding, trusted_identity!, payload)
335
+ terminal_result(payload)
336
+ end
337
+
338
+ def terminal_result(payload)
339
+ {state: payload.fetch("terminal_status"), receipt: payload.fetch("receipt"),
340
+ synchronization_required: payload.fetch("synchronization_required")}
341
+ end
342
+
343
+ def intent_payload(selectors, input_document, digest)
344
+ {"stage" => INTENT_STAGE, "project" => selectors[:project], "queue_entry" => selectors[:queue_entry],
345
+ "operation" => "merge", "authorization" => selectors[:authorization],
346
+ "request_id" => selectors[:request_id], "input" => input_document, "input_digest" => digest}
347
+ end
348
+
349
+ def record_event(binding, identity, payload)
350
+ producer = {"actor" => identity.actor, "role" => identity.role, "runtime" => identity.runtime}
351
+ @journal.record(**binding.transform_keys(&:to_sym), type: "delivery",
352
+ payload: payload.merge("producer" => producer),
353
+ guard: -> {
354
+ live = @coordinator.authoritative_attempt(binding)
355
+ raise AttemptErrors::ReceiptRejected, "Ordinary merge attempt is no longer active" unless
356
+ live&.active? && live.managed?
357
+ })
358
+ rescue SystemCallError
359
+ raise AttemptErrors::EvidenceUnavailable,
360
+ "ordinary merge evidence record is unavailable; reconcile by the recorded binding"
361
+ end
362
+ end
363
+ end
364
+ end
365
+ end
@@ -2,6 +2,6 @@
2
2
 
3
3
  module Ace
4
4
  module Assign
5
- VERSION = "0.67.0"
5
+ VERSION = "0.68.0"
6
6
  end
7
7
  end
data/lib/ace/assign.rb CHANGED
@@ -98,4 +98,7 @@ module Ace
98
98
  end
99
99
 
100
100
  require_relative "assign/atoms/delivery_parameters"
101
+ require_relative "assign/molecules/ordinary_merge_contract"
102
+ require_relative "assign/molecules/ordinary_merge_cli"
103
+ require_relative "assign/organisms/ordinary_merge_consumer"
101
104
  require_relative "assign/organisms/delivery_coordinator"
metadata CHANGED
@@ -1,13 +1,13 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: ace-assign
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.67.0
4
+ version: 0.68.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Michal Czyz
8
8
  bindir: exe
9
9
  cert_chain: []
10
- date: 2026-10-12 00:00:00.000000000 Z
10
+ date: 2026-10-14 00:00:00.000000000 Z
11
11
  dependencies:
12
12
  - !ruby/object:Gem::Dependency
13
13
  name: ace-llm-providers-cli
@@ -409,6 +409,7 @@ files:
409
409
  - lib/ace/assign/atoms/composition_rules.rb
410
410
  - lib/ace/assign/atoms/delivery_parameters.rb
411
411
  - lib/ace/assign/atoms/evidence_digest.rb
412
+ - lib/ace/assign/atoms/fixed_merge_schemas.rb
412
413
  - lib/ace/assign/atoms/number_generator.rb
413
414
  - lib/ace/assign/atoms/preset_expander.rb
414
415
  - lib/ace/assign/atoms/preset_loader.rb
@@ -537,11 +538,14 @@ files:
537
538
  - lib/ace/assign/molecules/evidence_journal.rb
538
539
  - lib/ace/assign/molecules/execution_identity_resolver.rb
539
540
  - lib/ace/assign/molecules/execution_scope_lineage.rb
541
+ - lib/ace/assign/molecules/fixed_effect_reference.rb
540
542
  - lib/ace/assign/molecules/fork_session_launcher.rb
541
543
  - lib/ace/assign/molecules/journal_input_inhibition.rb
542
544
  - lib/ace/assign/molecules/journal_mutation.rb
543
545
  - lib/ace/assign/molecules/journal_prompt_mutation.rb
544
546
  - lib/ace/assign/molecules/lifecycle_exclusion.rb
547
+ - lib/ace/assign/molecules/ordinary_merge_cli.rb
548
+ - lib/ace/assign/molecules/ordinary_merge_contract.rb
545
549
  - lib/ace/assign/molecules/original_launch_binding.rb
546
550
  - lib/ace/assign/molecules/preset_inferrer.rb
547
551
  - lib/ace/assign/molecules/proposal_journal.rb
@@ -558,6 +562,7 @@ files:
558
562
  - lib/ace/assign/organisms/assignment_executor.rb
559
563
  - lib/ace/assign/organisms/attempt_coordinator.rb
560
564
  - lib/ace/assign/organisms/delivery_coordinator.rb
565
+ - lib/ace/assign/organisms/ordinary_merge_consumer.rb
561
566
  - lib/ace/assign/organisms/prepared_work_builder.rb
562
567
  - lib/ace/assign/organisms/protected_delivery_coordinator.rb
563
568
  - lib/ace/assign/organisms/task_assignment_creator.rb