ace-assign 0.71.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 (195) 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 -2119
  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/organisms/assignment_executor.rb +0 -6
  35. data/lib/ace/assign/organisms/task_assignment_creator.rb +3 -8
  36. data/lib/ace/assign/version.rb +1 -1
  37. data/lib/ace/assign.rb +0 -6
  38. metadata +4 -176
  39. data/.ace-defaults/assign/presets/close-delivery.yml +0 -57
  40. data/.ace-defaults/assign/presets/deliver-release.yml +0 -60
  41. data/.ace-defaults/assign/presets/integrate-batch.yml +0 -72
  42. data/.ace-defaults/assign/presets/supervise-delivery.yml +0 -69
  43. data/docs/protected-authority.md +0 -282
  44. data/exe/ace-scope-ready +0 -55
  45. data/lib/ace/assign/atoms/assignment_scope.rb +0 -58
  46. data/lib/ace/assign/atoms/attempt_state_machine.rb +0 -84
  47. data/lib/ace/assign/atoms/candidate_limits.rb +0 -20
  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/atoms/git_blob_batch.rb +0 -59
  52. data/lib/ace/assign/authority/assignment_definition.rb +0 -35
  53. data/lib/ace/assign/authority/assignment_inventory.rb +0 -290
  54. data/lib/ace/assign/authority/campaign_consumer_policy.rb +0 -100
  55. data/lib/ace/assign/authority/campaign_execution.rb +0 -119
  56. data/lib/ace/assign/authority/campaign_round_transfer.rb +0 -82
  57. data/lib/ace/assign/authority/candidate_blob_batch.rb +0 -55
  58. data/lib/ace/assign/authority/candidate_head_comparison.rb +0 -139
  59. data/lib/ace/assign/authority/candidate_transfer.rb +0 -357
  60. data/lib/ace/assign/authority/cleanup_result_protection.rb +0 -53
  61. data/lib/ace/assign/authority/client.rb +0 -257
  62. data/lib/ace/assign/authority/command_execution.rb +0 -180
  63. data/lib/ace/assign/authority/deployment.rb +0 -627
  64. data/lib/ace/assign/authority/deployment_history.rb +0 -146
  65. data/lib/ace/assign/authority/endcap.rb +0 -457
  66. data/lib/ace/assign/authority/endcap_attempts.rb +0 -274
  67. data/lib/ace/assign/authority/endcap_campaign_results.rb +0 -232
  68. data/lib/ace/assign/authority/endcap_campaign_rounds.rb +0 -266
  69. data/lib/ace/assign/authority/endcap_contexts.rb +0 -23
  70. data/lib/ace/assign/authority/endcap_inboxes.rb +0 -37
  71. data/lib/ace/assign/authority/endcap_prepared_work.rb +0 -80
  72. data/lib/ace/assign/authority/endcap_publication.rb +0 -41
  73. data/lib/ace/assign/authority/endcap_results.rb +0 -398
  74. data/lib/ace/assign/authority/endcap_review_requests.rb +0 -183
  75. data/lib/ace/assign/authority/endcap_reviews.rb +0 -130
  76. data/lib/ace/assign/authority/endcap_service_context.rb +0 -38
  77. data/lib/ace/assign/authority/endcap_service_settlement.rb +0 -152
  78. data/lib/ace/assign/authority/evidence_transfer.rb +0 -67
  79. data/lib/ace/assign/authority/execution_scope_observation.rb +0 -903
  80. data/lib/ace/assign/authority/fixed_publication_lifecycle.rb +0 -176
  81. data/lib/ace/assign/authority/fixed_service_lifecycle.rb +0 -594
  82. data/lib/ace/assign/authority/inbox_context_original.rb +0 -113
  83. data/lib/ace/assign/authority/launch_attempt_consumers.rb +0 -51
  84. data/lib/ace/assign/authority/launch_campaign_registration.rb +0 -214
  85. data/lib/ace/assign/authority/launch_control_channel.rb +0 -313
  86. data/lib/ace/assign/authority/launch_control_exclusions.rb +0 -127
  87. data/lib/ace/assign/authority/launch_driver.rb +0 -393
  88. data/lib/ace/assign/authority/launch_input_inhibition.rb +0 -132
  89. data/lib/ace/assign/authority/launch_lifecycle.rb +0 -997
  90. data/lib/ace/assign/authority/launch_prepared_work.rb +0 -122
  91. data/lib/ace/assign/authority/launch_review.rb +0 -91
  92. data/lib/ace/assign/authority/launch_scope_abort.rb +0 -81
  93. data/lib/ace/assign/authority/launch_scope_admission.rb +0 -215
  94. data/lib/ace/assign/authority/launch_scope_close.rb +0 -146
  95. data/lib/ace/assign/authority/launch_scope_parent.rb +0 -44
  96. data/lib/ace/assign/authority/launch_scope_release.rb +0 -859
  97. data/lib/ace/assign/authority/launch_steering.rb +0 -353
  98. data/lib/ace/assign/authority/launch_stop.rb +0 -254
  99. data/lib/ace/assign/authority/maintenance_workspace_retirement.rb +0 -229
  100. data/lib/ace/assign/authority/ordinary_attempt_start.rb +0 -137
  101. data/lib/ace/assign/authority/ordinary_client.rb +0 -70
  102. data/lib/ace/assign/authority/ordinary_completion.rb +0 -104
  103. data/lib/ace/assign/authority/ordinary_effect_settlement.rb +0 -118
  104. data/lib/ace/assign/authority/ordinary_lifecycle.rb +0 -227
  105. data/lib/ace/assign/authority/ordinary_merge_evidence.rb +0 -81
  106. data/lib/ace/assign/authority/ordinary_peer.rb +0 -54
  107. data/lib/ace/assign/authority/ordinary_progress.rb +0 -162
  108. data/lib/ace/assign/authority/ordinary_queue.rb +0 -115
  109. data/lib/ace/assign/authority/ordinary_release_artifacts.rb +0 -143
  110. data/lib/ace/assign/authority/ordinary_results.rb +0 -206
  111. data/lib/ace/assign/authority/ordinary_review_evidence.rb +0 -180
  112. data/lib/ace/assign/authority/ordinary_reviews.rb +0 -214
  113. data/lib/ace/assign/authority/ordinary_server.rb +0 -75
  114. data/lib/ace/assign/authority/ordinary_supervisor.rb +0 -99
  115. data/lib/ace/assign/authority/original_inbox_selection.rb +0 -41
  116. data/lib/ace/assign/authority/posix_acl.rb +0 -59
  117. data/lib/ace/assign/authority/prepared_input.rb +0 -130
  118. data/lib/ace/assign/authority/prepared_queue.rb +0 -185
  119. data/lib/ace/assign/authority/prepared_task_context.rb +0 -116
  120. data/lib/ace/assign/authority/prepared_work.rb +0 -309
  121. data/lib/ace/assign/authority/prepared_worker.rb +0 -69
  122. data/lib/ace/assign/authority/private_directory.rb +0 -33
  123. data/lib/ace/assign/authority/protected_assignment_context.rb +0 -226
  124. data/lib/ace/assign/authority/readiness_hook.rb +0 -120
  125. data/lib/ace/assign/authority/receipt_transfer.rb +0 -48
  126. data/lib/ace/assign/authority/release_artifact_manifest.rb +0 -91
  127. data/lib/ace/assign/authority/retained_input.rb +0 -53
  128. data/lib/ace/assign/authority/router.rb +0 -81
  129. data/lib/ace/assign/authority/server.rb +0 -131
  130. data/lib/ace/assign/authority/service_cleanup_dispatch.rb +0 -88
  131. data/lib/ace/assign/authority/service_cleanup_evidence.rb +0 -180
  132. data/lib/ace/assign/authority/service_delivery_evidence.rb +0 -101
  133. data/lib/ace/assign/authority/service_evidence.rb +0 -286
  134. data/lib/ace/assign/authority/service_publication_evidence.rb +0 -110
  135. data/lib/ace/assign/authority/socket_server.rb +0 -255
  136. data/lib/ace/assign/authority/task_context_entry.rb +0 -32
  137. data/lib/ace/assign/authority/transfer_codec.rb +0 -189
  138. data/lib/ace/assign/cli/commands/attempt/base.rb +0 -84
  139. data/lib/ace/assign/cli/commands/attempt/evidence.rb +0 -28
  140. data/lib/ace/assign/cli/commands/attempt/finish.rb +0 -51
  141. data/lib/ace/assign/cli/commands/attempt/reconcile.rb +0 -49
  142. data/lib/ace/assign/cli/commands/attempt/start.rb +0 -45
  143. data/lib/ace/assign/cli/commands/attempt/status.rb +0 -54
  144. data/lib/ace/assign/cli/commands/authority/inbox_context_principal.rb +0 -25
  145. data/lib/ace/assign/cli/commands/authority/inbox_context_selection.rb +0 -28
  146. data/lib/ace/assign/cli/commands/authority/launch.rb +0 -97
  147. data/lib/ace/assign/cli/commands/authority/serve.rb +0 -31
  148. data/lib/ace/assign/cli/commands/authority/status.rb +0 -26
  149. data/lib/ace/assign/cli/commands/authority/task_context.rb +0 -31
  150. data/lib/ace/assign/cli/commands/authority/task_context_principal.rb +0 -25
  151. data/lib/ace/assign/cli/commands/authority/task_context_selection.rb +0 -30
  152. data/lib/ace/assign/cli/commands/authority/terminate.rb +0 -29
  153. data/lib/ace/assign/cli/commands/authority/worker.rb +0 -23
  154. data/lib/ace/assign/cli/commands/campaign_export_result.rb +0 -55
  155. data/lib/ace/assign/cli/commands/campaign_record_round.rb +0 -57
  156. data/lib/ace/assign/cli/commands/delivery.rb +0 -107
  157. data/lib/ace/assign/cli/commands/inbox_bind.rb +0 -34
  158. data/lib/ace/assign/cli/commands/protected_submission.rb +0 -61
  159. data/lib/ace/assign/cli/commands/resume.rb +0 -38
  160. data/lib/ace/assign/cli/commands/submit_candidate.rb +0 -34
  161. data/lib/ace/assign/cli/commands/submit_result.rb +0 -61
  162. data/lib/ace/assign/models/attempt.rb +0 -178
  163. data/lib/ace/assign/models/attempt_binding.rb +0 -116
  164. data/lib/ace/assign/models/evidence_event.rb +0 -86
  165. data/lib/ace/assign/models/execution_receipt.rb +0 -139
  166. data/lib/ace/assign/molecules/attempt_reconciler.rb +0 -127
  167. data/lib/ace/assign/molecules/attempt_store.rb +0 -201
  168. data/lib/ace/assign/molecules/canonical_attempt_state.rb +0 -151
  169. data/lib/ace/assign/molecules/canonical_evidence.rb +0 -170
  170. data/lib/ace/assign/molecules/canonical_read_commands.rb +0 -84
  171. data/lib/ace/assign/molecules/canonical_read_snapshot.rb +0 -313
  172. data/lib/ace/assign/molecules/evidence_calculator.rb +0 -237
  173. data/lib/ace/assign/molecules/evidence_journal.rb +0 -1899
  174. data/lib/ace/assign/molecules/execution_identity_resolver.rb +0 -137
  175. data/lib/ace/assign/molecules/execution_scope_lineage.rb +0 -345
  176. data/lib/ace/assign/molecules/fixed_effect_reference.rb +0 -31
  177. data/lib/ace/assign/molecules/installed_journal_read.rb +0 -143
  178. data/lib/ace/assign/molecules/journal_input_inhibition.rb +0 -78
  179. data/lib/ace/assign/molecules/journal_mutation.rb +0 -274
  180. data/lib/ace/assign/molecules/journal_prompt_mutation.rb +0 -241
  181. data/lib/ace/assign/molecules/ordinary_merge_cli.rb +0 -68
  182. data/lib/ace/assign/molecules/ordinary_merge_contract.rb +0 -410
  183. data/lib/ace/assign/molecules/original_launch_binding.rb +0 -37
  184. data/lib/ace/assign/molecules/proposal_journal.rb +0 -179
  185. data/lib/ace/assign/molecules/protected_control_exclusion.rb +0 -303
  186. data/lib/ace/assign/molecules/protected_workspace_exclusion.rb +0 -330
  187. data/lib/ace/assign/molecules/protected_workspace_provisioner.rb +0 -164
  188. data/lib/ace/assign/molecules/receipt_verifier.rb +0 -320
  189. data/lib/ace/assign/molecules/terminal_scope_receipt.rb +0 -88
  190. data/lib/ace/assign/organisms/attempt_coordinator.rb +0 -1374
  191. data/lib/ace/assign/organisms/delivery_coordinator.rb +0 -342
  192. data/lib/ace/assign/organisms/ordinary_driver.rb +0 -310
  193. data/lib/ace/assign/organisms/ordinary_merge_consumer.rb +0 -365
  194. data/lib/ace/assign/organisms/prepared_work_builder.rb +0 -194
  195. 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