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,313 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require "digest"
4
- require "fileutils"
5
- require "tmpdir"
6
- require "ace/herdr/molecules/bounded_process"
7
- require_relative "evidence_journal"
8
- require_relative "canonical_read_commands"
9
- require_relative "../authority/private_directory"
10
-
11
- module Ace
12
- module Assign
13
- module Molecules
14
- # Isolates read-only Git from an installed authority's live configuration.
15
- # The copied view never publishes evidence or mutates the source journal.
16
- class CanonicalReadSnapshot
17
- include CanonicalReadCommands
18
- class Unavailable < AttemptErrors::EvidenceUnavailable; end
19
- BYTE_LIMIT = 256 * 1_048_576
20
- ENTRY_LIMIT = 4096
21
- OID = /\A[0-9a-f]{40}\z/
22
- ENVIRONMENT = {"PATH" => "/usr/bin:/bin", "LC_ALL" => "C",
23
- "GIT_CONFIG_NOSYSTEM" => "1", "GIT_CONFIG_SYSTEM" => "/dev/null",
24
- "GIT_CONFIG_GLOBAL" => "/dev/null", "GIT_TERMINAL_PROMPT" => "0",
25
- "GIT_ALLOW_PROTOCOL" => "", "GIT_NO_REPLACE_OBJECTS" => "1",
26
- "GIT_NO_LAZY_FETCH" => "1", "GIT_OPTIONAL_LOCKS" => "0"}.freeze
27
-
28
- # These selections come from installed source composition, not frames.
29
- def initialize(repo_root:, checkout_root:, ref:, private_root:, git_executable: "/usr/bin/git")
30
- paths = [repo_root, checkout_root, private_root, git_executable]
31
- unless paths.all? { |path| path.is_a?(String) && path.start_with?("/") &&
32
- !path.include?("\0") && File.expand_path(path) == path } &&
33
- ref.is_a?(String) && ref.match?(/\Arefs\/[a-zA-Z0-9_-]+(?:\/[a-zA-Z0-9_-]+)+\z/)
34
- raise ArgumentError, "canonical snapshot source selection is malformed"
35
- end
36
- @repo_root, @checkout_root, @private_root, @git = paths.map { |path| path.dup.freeze }
37
- @ref = ref.dup.freeze
38
- @verification = Mutex.new
39
- end
40
-
41
- def with(deadline:)
42
- refuse!("snapshot_busy") unless @verification.try_lock
43
- begin
44
- with_snapshot(deadline: deadline) { |snapshot| yield snapshot }
45
- ensure
46
- @verification.unlock
47
- end
48
- end
49
-
50
- def with_snapshot(deadline:)
51
- validate_deadline!(deadline)
52
- verify_private_root!
53
- private_stat = directory!(@private_root)
54
- unless private_stat.uid == Process.uid && (private_stat.mode & 0o7777) == 0o700
55
- refuse!("private_storage_unprotected")
56
- end
57
- Dir.mktmpdir("canonical-read-", @private_root) do |view|
58
- verify_private_root!
59
- refuse!("private_storage_unprotected") unless directory_identity(File.lstat(@private_root)) == directory_identity(private_stat)
60
- @view, @deadline = view, deadline
61
- @held, @directories, @inventory, @inventory_files, @metadata_files = [], [], [], [], []
62
- @bytes, @entries = 0, 0
63
- git_dir = File.join(@repo_root, ".git")
64
- directory!(git_dir)
65
- %w[commondir reftable shallow].each do |name|
66
- refuse!("unsupported_source") if exists?(File.join(git_dir, name))
67
- end
68
- lock, original_lock = held_file!(File.join(@checkout_root, ".evidence.lock"), limit: 128)
69
- begin
70
- acquire!(lock)
71
- begin
72
- source_config!(git_dir)
73
- @commit = select_ref!(git_dir)
74
- objects = File.join(git_dir, "objects")
75
- inventory = inventory!(objects)
76
- inventory.each { |relative, handle| copy!(relative, handle) }
77
- verify_held!
78
- refuse!("source_changed") unless inventory_names!(objects) == @inventory &&
79
- select_ref!(git_dir) == @commit && identity(File.lstat(lock.path)) == original_lock
80
- verify_held!
81
- ensure
82
- lock.flock(File::LOCK_UN)
83
- end
84
- end
85
- prepare_view!
86
- run!(%w[fsck --full --strict --no-reflogs], limit: 65_536)
87
- yield self
88
- ensure
89
- @held&.each { |handle| handle.close unless handle.closed? }
90
- @held = @directories = @inventory = @inventory_files = @metadata_files = @view = @deadline = @commit = nil
91
- end
92
- rescue Timeout::Error
93
- refuse!("resource_limit")
94
- rescue IOError, SystemCallError, Herdr::Molecules::BoundedProcess::PostLaunchError
95
- refuse!("source_unavailable")
96
- end
97
- private :with_snapshot
98
-
99
- private
100
-
101
- def refuse!(reason)
102
- raise Unavailable, "canonical read snapshot unavailable: #{reason}"
103
- end
104
-
105
- def validate_deadline!(deadline)
106
- unless deadline.is_a?(Numeric) && deadline.finite? && deadline > Process.clock_gettime(Process::CLOCK_MONOTONIC)
107
- refuse!("resource_limit")
108
- end
109
- end
110
-
111
- def remaining
112
- value = @deadline - Process.clock_gettime(Process::CLOCK_MONOTONIC)
113
- refuse!("resource_limit") unless value.positive?
114
- value
115
- end
116
-
117
- def acquire!(lock)
118
- until lock.flock(File::LOCK_SH | File::LOCK_NB)
119
- sleep [remaining, 0.005].min
120
- end
121
- end
122
-
123
- def identity(stat)
124
- [stat.dev, stat.ino, stat.mode, stat.uid, stat.gid, stat.size, stat.mtime, stat.ctime]
125
- end
126
-
127
- def verify_private_root!
128
- Authority::PrivateDirectory.verify!(@private_root)
129
- rescue AttemptErrors::ReceiptRejected
130
- refuse!("private_storage_unprotected")
131
- end
132
-
133
- def directory_identity(stat)
134
- [stat.dev, stat.ino, stat.mode, stat.uid, stat.gid]
135
- end
136
-
137
- def directory!(path)
138
- stat = File.lstat(path)
139
- refuse!("unsupported_source") unless stat.directory? && !stat.symlink?
140
- stat
141
- end
142
-
143
- def exists?(path)
144
- File.lstat(path)
145
- true
146
- rescue Errno::ENOENT
147
- false
148
- end
149
-
150
- def held_file!(path, limit:)
151
- remaining
152
- parent = File.dirname(path)
153
- until parent == "/"
154
- stat = directory!(parent)
155
- @directories << [parent, directory_identity(stat)]
156
- parent = File.dirname(parent)
157
- end
158
- handle = File.open(path, File::RDONLY | File::NOFOLLOW | File::NONBLOCK)
159
- @held << handle
160
- stat = handle.stat
161
- refuse!("unsupported_source") unless stat.file?
162
- refuse!("resource_limit") if stat.size > limit
163
- [handle, identity(stat)]
164
- end
165
-
166
- def select_ref!(git_dir)
167
- loose = File.join(git_dir, @ref)
168
- if exists?(loose)
169
- handle, before = held_file!(loose, limit: 128)
170
- bytes = handle.read(129)
171
- @metadata_files << [loose, handle, before]
172
- refuse!("source_changed") unless identity(handle.stat) == before
173
- refuse!("unsupported_ref") unless bytes.match?(/\A[0-9a-f]{40}\n?\z/)
174
- return bytes.strip
175
- end
176
- packed = File.join(git_dir, "packed-refs")
177
- handle, before = held_file!(packed, limit: 65_536)
178
- bytes = handle.read(65_537)
179
- @metadata_files << [packed, handle, before]
180
- refuse!("source_changed") unless identity(handle.stat) == before
181
- entries = {}
182
- bytes.lines.each do |line|
183
- next if line.start_with?("#") || line.match?(/\A\^[0-9a-f]{40}\n?\z/)
184
- match = /\A([0-9a-f]{40}) (refs\/[^\s\0]+)\n?\z/.match(line)
185
- refuse!("unsupported_ref") unless match && !entries.key?(match[2])
186
- entries[match[2]] = match[1]
187
- end
188
- entries.fetch(@ref) { refuse!("unsupported_ref") }
189
- end
190
-
191
- def source_config!(git_dir)
192
- path = File.join(git_dir, "config")
193
- handle, before = held_file!(path, limit: 65_536)
194
- bytes = handle.read(65_537)
195
- @metadata_files << [path, handle, before]
196
- refuse!("source_changed") unless identity(handle.stat) == before
197
- # Standalone file parsing does not select the target repository,
198
- # follow includes or execute aliases/hooks/helpers. No source config
199
- # is installed into the private administrative view.
200
- result = run!(["config", "--no-includes", "--null", "--list", "--file", "/dev/stdin"],
201
- stdin_data: bytes, limit: 65_536)
202
- versions = []
203
- result.stdout.split("\0").each do |entry|
204
- key, value = entry.split("\n", 2)
205
- refuse!("unsupported_source") if key.start_with?("extensions.") || key == "core.worktree"
206
- versions << value if key == "core.repositoryformatversion"
207
- end
208
- refuse!("unsupported_source") unless versions.empty? || versions == ["0"]
209
- end
210
-
211
- def inventory_names!(objects)
212
- names = []
213
- walk = lambda do |path, relative|
214
- remaining
215
- stat = File.lstat(path)
216
- names << [relative, stat.directory? ? "directory" : "file"]
217
- refuse!("resource_limit") if names.size > ENTRY_LIMIT
218
- refuse!("unsupported_source") if stat.symlink? || !(stat.directory? || stat.file?)
219
- if stat.directory?
220
- unless relative.empty? || %w[pack info].include?(relative) || relative.match?(/\A[0-9a-f]{2}\z/)
221
- refuse!("unsupported_source")
222
- end
223
- children = []
224
- Dir.each_child(path) do |name|
225
- remaining
226
- children << name
227
- refuse!("resource_limit") if children.size + names.size > ENTRY_LIMIT
228
- end
229
- children.sort.each { |name| walk.call(File.join(path, name), relative.empty? ? name : "#{relative}/#{name}") }
230
- end
231
- end
232
- walk.call(objects, "")
233
- names
234
- end
235
-
236
- def inventory!(objects)
237
- @inventory = inventory_names!(objects)
238
- files = @inventory.select { |_relative, kind| kind == "file" }.map(&:first)
239
- packs = files.grep(/\Apack\/pack-[0-9a-f]{40}\.pack\z/)
240
- unless packs.all? { |pack| files.include?(pack.sub(/\.pack\z/, ".idx")) } &&
241
- files.grep(/\Apack\/pack-[0-9a-f]{40}\.(?:idx|rev)\z/).all? { |file| packs.include?(file.sub(/\.(?:idx|rev)\z/, ".pack")) }
242
- refuse!("unsupported_source")
243
- end
244
- @inventory.filter_map do |relative, kind|
245
- path = relative.empty? ? objects : File.join(objects, relative)
246
- if kind == "directory"
247
- unless relative.empty? || relative == "pack" || relative == "info" || relative.match?(/\A[0-9a-f]{2}\z/)
248
- refuse!("unsupported_source")
249
- end
250
- @directories << [path, directory_identity(File.lstat(path))]
251
- next
252
- end
253
- # These ordinary Git indexes/retention hints do not redirect
254
- # storage. The clean view never reads them; only object bytes and
255
- # pack indexes are copied. Redirect/lazy-fetch metadata refuses.
256
- next if relative.match?(/\Ainfo\/(?:packs|commit-graph)\z/) ||
257
- relative.match?(/\Apack\/pack-[0-9a-f]{40}\.(?:bitmap|keep)\z/)
258
- unless relative.match?(/\A[0-9a-f]{2}\/[0-9a-f]{38}\z/) ||
259
- relative.match?(/\Apack\/pack-[0-9a-f]{40}\.(?:pack|idx|rev)\z/)
260
- refuse!("unsupported_source")
261
- end
262
- handle, before = held_file!(path, limit: BYTE_LIMIT)
263
- @bytes += handle.stat.size
264
- refuse!("resource_limit") if @bytes > BYTE_LIMIT
265
- @inventory_files << [path, handle, before]
266
- [relative, handle]
267
- end
268
- end
269
-
270
- def copy!(relative, source)
271
- destination = File.join(@view, "objects", relative)
272
- FileUtils.mkdir_p(File.dirname(destination), mode: 0o700)
273
- File.open(destination, File::WRONLY | File::CREAT | File::EXCL, 0o600) do |target|
274
- copied = 0
275
- expected = source.stat.size
276
- while (chunk = source.read(65_536))
277
- remaining
278
- copied += chunk.bytesize
279
- refuse!("source_changed") if copied > expected
280
- target.write(chunk)
281
- end
282
- refuse!("source_changed") unless copied == expected
283
- end
284
- end
285
-
286
- def verify_held!
287
- @directories.each { |path, before| refuse!("source_changed") unless directory_identity(File.lstat(path)) == before }
288
- (@inventory_files + @metadata_files).each do |path, handle, before|
289
- refuse!("source_changed") unless identity(handle.stat) == before && identity(File.lstat(path)) == before
290
- end
291
- end
292
-
293
- def prepare_view!
294
- FileUtils.mkdir_p(File.join(@view, "objects"), mode: 0o700)
295
- FileUtils.mkdir_p(File.join(@view, "refs"), mode: 0o700)
296
- File.binwrite(File.join(@view, "HEAD"), "#{@commit}\n")
297
- File.binwrite(File.join(@view, "config"), "[core]\nrepositoryformatversion = 0\nbare = true\n")
298
- ref = File.join(@view, @ref)
299
- FileUtils.mkdir_p(File.dirname(ref), mode: 0o700)
300
- File.binwrite(ref, "#{@commit}\n")
301
- end
302
-
303
- def run!(argv, stdin_data: "", limit:)
304
- result = Herdr::Molecules::BoundedProcess.call([@git, "--git-dir=#{@view}", "--no-pager", *argv],
305
- environment: ENVIRONMENT, chdir: @view, stdin_data: stdin_data,
306
- timeout_s: remaining, output_limit: limit, stderr_limit: 65_536)
307
- refuse!(result.oversized ? "resource_limit" : "git_unavailable") unless result.status.success? && !result.oversized
308
- result
309
- end
310
- end
311
- end
312
- end
313
- end
@@ -1,237 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require "digest"
4
- require "open3"
5
- require "ace/support/fs"
6
-
7
- module Ace
8
- module Assign
9
- module Molecules
10
- # Computes delivery evidence for an assignment from accepted,
11
- # current-head execution evidence.
12
- #
13
- # Decisions are derived exclusively from accepted receipts recorded in
14
- # the evidence journal (managed attempts) or the durable attempt store
15
- # (taskless attempts). Report files on disk, exit codes, and prose never
16
- # establish review or merge authorization, and no feedback state is
17
- # hardcoded: an attempt that may have completed without a durable
18
- # receipt yields `uncertain`, never success.
19
- class EvidenceCalculator
20
- def self.calculate(pr_number: nil, auto_merge: false, assignment_id: nil)
21
- new.calculate(pr_number: pr_number, auto_merge: auto_merge, assignment_id: assignment_id)
22
- end
23
-
24
- # @param cache_base [String, nil] Assignment cache base (default config)
25
- # @param repo_root [String, nil] Candidate repository root (default project root)
26
- # @param journal [EvidenceJournal, nil] Evidence journal (default built lazily)
27
- def initialize(cache_base: nil, repo_root: nil, journal: nil)
28
- @cache_base = cache_base
29
- @repo_root = repo_root || Ace::Support::Fs::Molecules::ProjectRootFinder.find_or_current
30
- @journal = journal
31
- end
32
-
33
- def calculate(pr_number: nil, auto_merge: false, assignment_id: nil)
34
- head, tree, changed_scope_digest = git_facts
35
- assignment = find_assignment(assignment_id)
36
- attempts = assignment ? attempts_for(assignment) : []
37
- receipts = assignment ? accepted_receipts(assignment) : []
38
-
39
- review_receipt = receipt_currency(receipts, "review", head)
40
- release_receipt = receipt_currency(receipts, "release", head)
41
- unresolved_effects = collect_unresolved_effects(attempts)
42
- if assignment&.managed?
43
- requests = journal.service_requests(assignment.id)
44
- # A rejected effect stays unresolved when its claim had been
45
- # dispatched (authorization consumed): only attributable evidence
46
- # that no effect occurred settles it, never the bare rejection.
47
- unresolved_effects.concat(requests
48
- .select { |request| unresolved_service_state?(request) }
49
- .map { |request| "#{request["request_id"]}:#{request["operation"]}:#{request["state"]}" })
50
- # Settled requests keep their authorization only while their
51
- # receipt evidence remains intact in the candidate repository:
52
- # a deleted or modified artifact drops the outcome back to
53
- # unresolved instead of trusting an unverifiable receipt.
54
- unresolved_effects.concat(requests
55
- .select { |request| request["state"] == "succeeded" || request["state"] == "failed-settled" }
56
- .reject { |request| evidence_intact?(request) }
57
- .map { |request| "#{request["request_id"]}:#{request["operation"]}:evidence-unavailable" })
58
- unresolved_effects.uniq!
59
- end
60
- feedback_state = derive_feedback_state(attempts, receipts, head, unresolved_effects)
61
-
62
- # Authorization requires managed evidence, settled work (no active
63
- # attempts), no unresolved effects, and a current independent
64
- # review verdict. Taskless assignments never authorize merges.
65
- merge_decision = if auto_merge && assignment&.managed? &&
66
- review_receipt == "current" && unresolved_effects.empty? && attempts.none?(&:active?)
67
- "authorized"
68
- else
69
- "approval-required"
70
- end
71
-
72
- latest = attempts.first
73
- active = attempts.find(&:active?)
74
- evidence = {
75
- pr: pr_number || "none",
76
- head: head,
77
- tree: tree,
78
- changed_scope_digest: changed_scope_digest,
79
- review_receipt: review_receipt,
80
- release_receipt: release_receipt,
81
- feedback_state: feedback_state,
82
- merge_decision: merge_decision,
83
- decision_digest: nil,
84
- attempt: latest ? attempt_summary(latest, active: active) : nil,
85
- base_head: latest&.binding&.base_head,
86
- candidate_head: latest&.candidate_head,
87
- evidence_git_ref: evidence_git_ref(assignment),
88
- journal_commit: assignment&.managed? ? journal.ref_value : latest&.journal_commit,
89
- unresolved_effects: unresolved_effects
90
- }
91
- evidence[:decision_digest] = Digest::SHA256.hexdigest(
92
- Atoms::EvidenceDigest.canonical_json(evidence)
93
- )
94
- evidence
95
- end
96
-
97
- private
98
-
99
- # Accepted, uncertain, and failed effects are unresolved by
100
- # definition; a rejected effect stays unresolved when its claim was
101
- # dispatched (authorization consumed), because the effect may have
102
- # happened.
103
- def unresolved_service_state?(request)
104
- %w[accepted uncertain failed].include?(request["state"]) ||
105
- (request["state"] == "rejected" && request["consumed"] != false)
106
- end
107
-
108
- # Every receipt evidence artifact must still exist in the candidate
109
- # repository with its recorded digest; evidence lives in the working
110
- # tree, so its integrity is re-checked on every calculation.
111
- def evidence_intact?(request)
112
- receipt = request["receipt"]
113
- return false unless receipt.is_a?(Hash)
114
- repo_root = File.realpath(@repo_root)
115
- Array(receipt["evidence"]).all? do |item|
116
- path = File.expand_path(item["ref"].to_s, repo_root)
117
- real = begin
118
- File.realpath(path)
119
- rescue Errno::ENOENT, Errno::EACCES, Errno::ELOOP
120
- next false
121
- end
122
- begin
123
- real.start_with?(repo_root + File::SEPARATOR) &&
124
- Digest::SHA256.file(real).hexdigest == item["sha256"]
125
- rescue Errno::EACCES, Errno::ELOOP
126
- false
127
- end
128
- end
129
- end
130
-
131
- def git_facts
132
- head, _s = Open3.capture2("git", "rev-parse", "HEAD", chdir: @repo_root, stdin_data: "")
133
- tree, _s = Open3.capture2("git", "rev-parse", "HEAD^{tree}", chdir: @repo_root, stdin_data: "")
134
- diff_output, _s = Open3.capture2("git", "diff", "HEAD", chdir: @repo_root, stdin_data: "")
135
-
136
- [head.to_s.strip, tree.to_s.strip, Digest::SHA256.hexdigest(diff_output.to_s)]
137
- end
138
-
139
- def find_assignment(assignment_id)
140
- manager = Molecules::AssignmentManager.new(cache_base: @cache_base)
141
- assignment_id ? manager.load(assignment_id) : manager.find_active
142
- end
143
-
144
- def store
145
- @store ||= Molecules::AssignmentManager.new(cache_base: @cache_base).attempt_store
146
- end
147
-
148
- def journal
149
- @journal ||= Molecules::EvidenceJournal.new(repo_root: @repo_root)
150
- end
151
-
152
- # Attempt state for evidence decisions. Managed assignments treat the
153
- # journal as authoritative: ALL journal-derived attempts (including
154
- # terminal states) replace local records with the same ID, so a crash
155
- # between journaling and the local save cannot leave stale open state.
156
- def attempts_for(assignment)
157
- attempts = store.list(assignment.id)
158
- return attempts unless assignment.managed?
159
-
160
- journal_by_id = {}
161
- journal.derived_attempts(assignment.id).each { |attempt| journal_by_id[attempt.attempt_id] = attempt }
162
- merged = attempts.map { |attempt| journal_by_id.delete(attempt.attempt_id) || attempt }
163
- merged + journal_by_id.values
164
- end
165
-
166
- # Accepted receipts for evidence currency. Managed assignments use
167
- # journal receipts only — the journal is the authority; local records
168
- # are disposable projections.
169
- def accepted_receipts(assignment)
170
- if assignment.managed?
171
- return journal.accepted_receipts(assignment.id).uniq { |receipt| receipt["digest"] }
172
- end
173
-
174
- store.list(assignment.id).flat_map(&:accepted_receipts)
175
- .uniq { |receipt| receipt["digest"] }
176
- end
177
-
178
- # Currency against the current head. Review approval requires an
179
- # executed independent reviewer verdict; receipts were verified at
180
- # acceptance and the independence guard is re-checked here defensively
181
- # (a review receipt without a reviewer verdict is not review evidence
182
- # at all).
183
- def receipt_currency(receipts, operation, head)
184
- succeeded = receipts.select do |receipt|
185
- next false unless receipt["operation"] == operation && receipt["verdict"] == "succeeded"
186
- next false if operation == "review" && receipt.dig("review", "reviewer", "actor").nil?
187
-
188
- true
189
- end
190
-
191
- current = succeeded.any? { |receipt| receipt["head"] == head }
192
-
193
- return "missing" if succeeded.empty?
194
- return "current" if current
195
-
196
- "stale"
197
- end
198
-
199
- def collect_unresolved_effects(attempts)
200
- attempts.flat_map { |attempt| attempt.unresolved_effects.map { |effect| effect["operation"] } }
201
- .concat(attempts.select(&:uncertain?).map { |attempt| "#{attempt.attempt_id}:uncertain" })
202
- .uniq
203
- end
204
-
205
- # Feedback is derived: uncertain evidence wins, active attempts keep
206
- # the loop open, accepted current-head evidence closes it.
207
- def derive_feedback_state(attempts, receipts, head, unresolved_effects)
208
- return "unknown" if attempts.empty? && receipts.empty?
209
-
210
- return "uncertain" if unresolved_effects.any?
211
-
212
- return "terminal" if attempts.none?(&:active?) && receipts.any? do |receipt|
213
- receipt["verdict"] == "succeeded" && receipt["head"] == head
214
- end
215
-
216
- "open"
217
- end
218
-
219
- def attempt_summary(attempt, active: nil)
220
- {
221
- "attempt_id" => attempt.attempt_id,
222
- "state" => attempt.state,
223
- "scope" => attempt.binding.scope,
224
- "recovery_mode" => attempt.recovery_mode,
225
- "active" => active ? active.attempt_id == attempt.attempt_id : false
226
- }
227
- end
228
-
229
- def evidence_git_ref(assignment)
230
- return nil unless assignment&.managed?
231
-
232
- journal.ref
233
- end
234
- end
235
- end
236
- end
237
- end