ace-assign 0.70.0 → 0.73.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.ace-defaults/assign/catalog/steps/auto-merge.step.yml +8 -8
- data/.ace-defaults/assign/catalog/steps/create-pr.step.yml +1 -1
- data/.ace-defaults/assign/catalog/steps/mark-pr-ready.step.yml +1 -1
- data/.ace-defaults/assign/config.yml +0 -13
- data/.ace-defaults/assign/presets/fix-bug.yml +1 -1
- data/.ace-defaults/assign/presets/work-on-task-ace-development.yml +1 -1
- data/.ace-defaults/assign/presets/work-on-task-auto-merge.yml +1 -1
- data/.ace-defaults/assign/presets/work-on-task.yml +1 -1
- data/CHANGELOG.md +3 -2099
- data/README.md +58 -31
- data/docs/exit-codes.md +36 -44
- data/docs/getting-started.md +9 -4
- data/docs/usage.md +129 -522
- data/handbook/skills/as-assign-drive/SKILL.md +1 -1
- data/handbook/workflow-instructions/assign/drive.wf.md +47 -118
- data/handbook/workflow-instructions/assign/prepare.wf.md +1 -1
- data/handbook/workflow-instructions/assign/verify-test-suite.wf.md +8 -2
- data/lib/ace/assign/cli/commands/assignment_target.rb +2 -15
- data/lib/ace/assign/cli/commands/create.rb +1 -8
- data/lib/ace/assign/cli/commands/fail.rb +0 -6
- data/lib/ace/assign/cli/commands/finish.rb +2 -23
- data/lib/ace/assign/cli/commands/fork_session.rb +0 -1
- data/lib/ace/assign/cli/commands/select.rb +0 -1
- data/lib/ace/assign/cli/commands/start.rb +0 -2
- data/lib/ace/assign/cli/commands/status.rb +1 -31
- data/lib/ace/assign/cli/commands/step.rb +1 -3
- data/lib/ace/assign/cli.rb +1 -85
- data/lib/ace/assign/errors.rb +8 -80
- data/lib/ace/assign/models/assignment.rb +5 -28
- data/lib/ace/assign/molecules/assignment_manager.rb +0 -26
- data/lib/ace/assign/molecules/fork_session_launcher.rb +4 -12
- data/lib/ace/assign/molecules/lifecycle_exclusion.rb +15 -112
- data/lib/ace/assign/molecules/runtime_control_surface_runner.rb +2 -1
- data/lib/ace/assign/organisms/assignment_executor.rb +0 -6
- data/lib/ace/assign/organisms/task_assignment_creator.rb +3 -8
- data/lib/ace/assign/version.rb +1 -1
- data/lib/ace/assign.rb +0 -6
- metadata +4 -161
- data/.ace-defaults/assign/presets/close-delivery.yml +0 -57
- data/.ace-defaults/assign/presets/deliver-release.yml +0 -60
- data/.ace-defaults/assign/presets/integrate-batch.yml +0 -72
- data/.ace-defaults/assign/presets/supervise-delivery.yml +0 -69
- data/docs/protected-authority.md +0 -282
- data/exe/ace-scope-ready +0 -55
- data/lib/ace/assign/atoms/assignment_scope.rb +0 -58
- data/lib/ace/assign/atoms/attempt_state_machine.rb +0 -84
- data/lib/ace/assign/atoms/delivery_parameters.rb +0 -19
- data/lib/ace/assign/atoms/evidence_digest.rb +0 -78
- data/lib/ace/assign/atoms/fixed_merge_schemas.rb +0 -398
- data/lib/ace/assign/authority/assignment_definition.rb +0 -35
- data/lib/ace/assign/authority/assignment_inventory.rb +0 -290
- data/lib/ace/assign/authority/campaign_consumer_policy.rb +0 -100
- data/lib/ace/assign/authority/campaign_execution.rb +0 -119
- data/lib/ace/assign/authority/campaign_round_transfer.rb +0 -82
- data/lib/ace/assign/authority/candidate_transfer.rb +0 -327
- data/lib/ace/assign/authority/cleanup_result_protection.rb +0 -53
- data/lib/ace/assign/authority/client.rb +0 -257
- data/lib/ace/assign/authority/deployment.rb +0 -627
- data/lib/ace/assign/authority/deployment_history.rb +0 -146
- data/lib/ace/assign/authority/endcap.rb +0 -455
- data/lib/ace/assign/authority/endcap_attempts.rb +0 -274
- data/lib/ace/assign/authority/endcap_campaign_results.rb +0 -232
- data/lib/ace/assign/authority/endcap_campaign_rounds.rb +0 -266
- data/lib/ace/assign/authority/endcap_contexts.rb +0 -23
- data/lib/ace/assign/authority/endcap_inboxes.rb +0 -37
- data/lib/ace/assign/authority/endcap_prepared_work.rb +0 -80
- data/lib/ace/assign/authority/endcap_publication.rb +0 -207
- data/lib/ace/assign/authority/endcap_results.rb +0 -398
- data/lib/ace/assign/authority/endcap_review_requests.rb +0 -183
- data/lib/ace/assign/authority/endcap_reviews.rb +0 -130
- data/lib/ace/assign/authority/endcap_service_settlement.rb +0 -152
- data/lib/ace/assign/authority/endcap_services.rb +0 -598
- data/lib/ace/assign/authority/evidence_transfer.rb +0 -67
- data/lib/ace/assign/authority/execution_scope_observation.rb +0 -903
- data/lib/ace/assign/authority/inbox_context_original.rb +0 -113
- data/lib/ace/assign/authority/launch_attempt_consumers.rb +0 -51
- data/lib/ace/assign/authority/launch_campaign_registration.rb +0 -214
- data/lib/ace/assign/authority/launch_control_channel.rb +0 -313
- data/lib/ace/assign/authority/launch_control_exclusions.rb +0 -127
- data/lib/ace/assign/authority/launch_driver.rb +0 -393
- data/lib/ace/assign/authority/launch_input_inhibition.rb +0 -132
- data/lib/ace/assign/authority/launch_lifecycle.rb +0 -997
- data/lib/ace/assign/authority/launch_prepared_work.rb +0 -122
- data/lib/ace/assign/authority/launch_review.rb +0 -91
- data/lib/ace/assign/authority/launch_scope_abort.rb +0 -81
- data/lib/ace/assign/authority/launch_scope_admission.rb +0 -215
- data/lib/ace/assign/authority/launch_scope_close.rb +0 -146
- data/lib/ace/assign/authority/launch_scope_parent.rb +0 -44
- data/lib/ace/assign/authority/launch_scope_release.rb +0 -859
- data/lib/ace/assign/authority/launch_steering.rb +0 -353
- data/lib/ace/assign/authority/launch_stop.rb +0 -254
- data/lib/ace/assign/authority/maintenance_workspace_retirement.rb +0 -229
- data/lib/ace/assign/authority/ordinary_attempt_start.rb +0 -137
- data/lib/ace/assign/authority/ordinary_client.rb +0 -70
- data/lib/ace/assign/authority/ordinary_completion.rb +0 -94
- data/lib/ace/assign/authority/ordinary_effect_settlement.rb +0 -118
- data/lib/ace/assign/authority/ordinary_lifecycle.rb +0 -191
- data/lib/ace/assign/authority/ordinary_peer.rb +0 -54
- data/lib/ace/assign/authority/ordinary_progress.rb +0 -162
- data/lib/ace/assign/authority/ordinary_queue.rb +0 -115
- data/lib/ace/assign/authority/ordinary_results.rb +0 -196
- data/lib/ace/assign/authority/ordinary_server.rb +0 -75
- data/lib/ace/assign/authority/original_inbox_selection.rb +0 -41
- data/lib/ace/assign/authority/posix_acl.rb +0 -59
- data/lib/ace/assign/authority/prepared_input.rb +0 -130
- data/lib/ace/assign/authority/prepared_queue.rb +0 -185
- data/lib/ace/assign/authority/prepared_task_context.rb +0 -116
- data/lib/ace/assign/authority/prepared_work.rb +0 -304
- data/lib/ace/assign/authority/prepared_worker.rb +0 -69
- data/lib/ace/assign/authority/private_directory.rb +0 -33
- data/lib/ace/assign/authority/protected_assignment_context.rb +0 -226
- data/lib/ace/assign/authority/readiness_hook.rb +0 -120
- data/lib/ace/assign/authority/receipt_transfer.rb +0 -48
- data/lib/ace/assign/authority/router.rb +0 -81
- data/lib/ace/assign/authority/server.rb +0 -131
- data/lib/ace/assign/authority/service_cleanup_dispatch.rb +0 -88
- data/lib/ace/assign/authority/service_cleanup_evidence.rb +0 -180
- data/lib/ace/assign/authority/service_delivery_evidence.rb +0 -101
- data/lib/ace/assign/authority/service_evidence.rb +0 -286
- data/lib/ace/assign/authority/service_publication_evidence.rb +0 -110
- data/lib/ace/assign/authority/socket_server.rb +0 -255
- data/lib/ace/assign/authority/task_context_entry.rb +0 -32
- data/lib/ace/assign/authority/transfer_codec.rb +0 -187
- data/lib/ace/assign/cli/commands/attempt/base.rb +0 -84
- data/lib/ace/assign/cli/commands/attempt/evidence.rb +0 -28
- data/lib/ace/assign/cli/commands/attempt/finish.rb +0 -51
- data/lib/ace/assign/cli/commands/attempt/reconcile.rb +0 -49
- data/lib/ace/assign/cli/commands/attempt/start.rb +0 -45
- data/lib/ace/assign/cli/commands/attempt/status.rb +0 -54
- data/lib/ace/assign/cli/commands/authority/inbox_context_principal.rb +0 -25
- data/lib/ace/assign/cli/commands/authority/inbox_context_selection.rb +0 -28
- data/lib/ace/assign/cli/commands/authority/launch.rb +0 -97
- data/lib/ace/assign/cli/commands/authority/serve.rb +0 -31
- data/lib/ace/assign/cli/commands/authority/status.rb +0 -26
- data/lib/ace/assign/cli/commands/authority/task_context.rb +0 -31
- data/lib/ace/assign/cli/commands/authority/task_context_principal.rb +0 -25
- data/lib/ace/assign/cli/commands/authority/task_context_selection.rb +0 -30
- data/lib/ace/assign/cli/commands/authority/terminate.rb +0 -29
- data/lib/ace/assign/cli/commands/authority/worker.rb +0 -23
- data/lib/ace/assign/cli/commands/campaign_export_result.rb +0 -55
- data/lib/ace/assign/cli/commands/campaign_record_round.rb +0 -57
- data/lib/ace/assign/cli/commands/delivery.rb +0 -107
- data/lib/ace/assign/cli/commands/inbox_bind.rb +0 -34
- data/lib/ace/assign/cli/commands/protected_submission.rb +0 -61
- data/lib/ace/assign/cli/commands/resume.rb +0 -38
- data/lib/ace/assign/cli/commands/submit_candidate.rb +0 -34
- data/lib/ace/assign/cli/commands/submit_result.rb +0 -61
- data/lib/ace/assign/models/attempt.rb +0 -178
- data/lib/ace/assign/models/attempt_binding.rb +0 -116
- data/lib/ace/assign/models/evidence_event.rb +0 -86
- data/lib/ace/assign/models/execution_receipt.rb +0 -139
- data/lib/ace/assign/molecules/attempt_reconciler.rb +0 -127
- data/lib/ace/assign/molecules/attempt_store.rb +0 -201
- data/lib/ace/assign/molecules/canonical_attempt_state.rb +0 -151
- data/lib/ace/assign/molecules/canonical_evidence.rb +0 -153
- data/lib/ace/assign/molecules/canonical_read_commands.rb +0 -84
- data/lib/ace/assign/molecules/canonical_read_snapshot.rb +0 -313
- data/lib/ace/assign/molecules/evidence_calculator.rb +0 -237
- data/lib/ace/assign/molecules/evidence_journal.rb +0 -1912
- data/lib/ace/assign/molecules/execution_identity_resolver.rb +0 -137
- data/lib/ace/assign/molecules/execution_scope_lineage.rb +0 -345
- data/lib/ace/assign/molecules/fixed_effect_reference.rb +0 -31
- data/lib/ace/assign/molecules/installed_journal_read.rb +0 -143
- data/lib/ace/assign/molecules/journal_input_inhibition.rb +0 -78
- data/lib/ace/assign/molecules/journal_mutation.rb +0 -273
- data/lib/ace/assign/molecules/journal_prompt_mutation.rb +0 -241
- data/lib/ace/assign/molecules/ordinary_merge_cli.rb +0 -68
- data/lib/ace/assign/molecules/ordinary_merge_contract.rb +0 -410
- data/lib/ace/assign/molecules/original_launch_binding.rb +0 -37
- data/lib/ace/assign/molecules/proposal_journal.rb +0 -179
- data/lib/ace/assign/molecules/protected_control_exclusion.rb +0 -303
- data/lib/ace/assign/molecules/protected_workspace_exclusion.rb +0 -330
- data/lib/ace/assign/molecules/protected_workspace_provisioner.rb +0 -164
- data/lib/ace/assign/molecules/receipt_verifier.rb +0 -314
- data/lib/ace/assign/molecules/terminal_scope_receipt.rb +0 -88
- data/lib/ace/assign/organisms/attempt_coordinator.rb +0 -1337
- data/lib/ace/assign/organisms/delivery_coordinator.rb +0 -342
- data/lib/ace/assign/organisms/ordinary_merge_consumer.rb +0 -365
- data/lib/ace/assign/organisms/prepared_work_builder.rb +0 -194
- data/lib/ace/assign/organisms/protected_delivery_coordinator.rb +0 -100
|
@@ -1,84 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
module Ace
|
|
4
|
-
module Assign
|
|
5
|
-
module Molecules
|
|
6
|
-
# Closed bounded read dialect shared by isolated protected snapshots and
|
|
7
|
-
# trusted installed journals. Neither boundary can dispatch a Git write.
|
|
8
|
-
module CanonicalReadCommands
|
|
9
|
-
BYTE_LIMIT = 256 * 1_048_576
|
|
10
|
-
OID = /\A[0-9a-f]{40}\z/
|
|
11
|
-
|
|
12
|
-
attr_reader :commit
|
|
13
|
-
|
|
14
|
-
# EvidenceJournal uses this only as its existing read command boundary.
|
|
15
|
-
def call(argv, stdin_data: "", output_limit: BYTE_LIMIT)
|
|
16
|
-
refuse!("read_outside_snapshot") unless @view && @commit
|
|
17
|
-
unless read_command?(argv, stdin_data)
|
|
18
|
-
refuse!("write_or_unsupported_command")
|
|
19
|
-
end
|
|
20
|
-
result = read_result(argv, stdin_data: stdin_data, limit: [output_limit, BYTE_LIMIT].min)
|
|
21
|
-
[result.stdout.to_s.strip, result.stderr.to_s.strip, result.status]
|
|
22
|
-
end
|
|
23
|
-
|
|
24
|
-
def blob_batch(entries, output_limit:)
|
|
25
|
-
refuse!("read_outside_snapshot") unless @view && @commit
|
|
26
|
-
unless entries.is_a?(Array) && entries.all? { |oid| oid.is_a?(String) && oid.match?(OID) }
|
|
27
|
-
refuse!("unsupported_command")
|
|
28
|
-
end
|
|
29
|
-
result = run!(["cat-file", "--batch"], stdin_data: entries.join("\n") + "\n",
|
|
30
|
-
limit: [output_limit, BYTE_LIMIT].min)
|
|
31
|
-
result.stdout.to_s.b
|
|
32
|
-
end
|
|
33
|
-
|
|
34
|
-
def history_diff(commits, paths, output_limit:)
|
|
35
|
-
refuse!("read_outside_snapshot") unless @view && @commit
|
|
36
|
-
unless commits.is_a?(Array) && commits.size.between?(1, 64) && commits.uniq.size == commits.size &&
|
|
37
|
-
commits.all? { |commit| commit.is_a?(String) && commit.match?(OID) } &&
|
|
38
|
-
paths.is_a?(Array) && !paths.empty? && paths.uniq.size == paths.size &&
|
|
39
|
-
(paths == ["execution/"] || paths.all? { |path| path.is_a?(String) && path.match?(%r{\Aexecution/[a-zA-Z0-9_.:-]+/events/\z}) }) &&
|
|
40
|
-
output_limit.is_a?(Integer) && output_limit.positive?
|
|
41
|
-
refuse!("unsupported_command")
|
|
42
|
-
end
|
|
43
|
-
result = run!(%w[diff-tree --stdin --always --root -r --raw -z --no-renames --no-ext-diff
|
|
44
|
-
--no-textconv --no-abbrev --] + paths, stdin_data: commits.join("\n") + "\n",
|
|
45
|
-
limit: [output_limit, BYTE_LIMIT].min)
|
|
46
|
-
result.stdout.to_s.b
|
|
47
|
-
end
|
|
48
|
-
|
|
49
|
-
def blob_sizes(oids)
|
|
50
|
-
refuse!("read_outside_snapshot") unless @view && @commit
|
|
51
|
-
unless oids.is_a?(Array) && oids.size.between?(1, 64) && oids.all? { |oid| oid.is_a?(String) && oid.match?(OID) }
|
|
52
|
-
refuse!("unsupported_command")
|
|
53
|
-
end
|
|
54
|
-
run!(%w[cat-file --batch-check], stdin_data: oids.join("\n") + "\n", limit: oids.size * 128).stdout.to_s.b
|
|
55
|
-
end
|
|
56
|
-
|
|
57
|
-
def blob(path, commit:, max_bytes:)
|
|
58
|
-
refuse!("read_outside_snapshot") unless @view && @commit
|
|
59
|
-
unless commit.is_a?(String) && commit.match?(OID) && max_bytes.is_a?(Integer) && max_bytes.positive?
|
|
60
|
-
refuse!("unsupported_command")
|
|
61
|
-
end
|
|
62
|
-
result = run!(["cat-file", "blob", "#{commit}:#{path}"], limit: [max_bytes, BYTE_LIMIT].min)
|
|
63
|
-
result.stdout.to_s.b
|
|
64
|
-
end
|
|
65
|
-
|
|
66
|
-
private
|
|
67
|
-
|
|
68
|
-
def read_result(argv, **options)
|
|
69
|
-
run!(argv, **options)
|
|
70
|
-
end
|
|
71
|
-
|
|
72
|
-
def read_command?(argv, stdin)
|
|
73
|
-
return false unless argv.is_a?(Array) && argv.all? { |item| item.is_a?(String) && !item.include?("\0") } && stdin.empty?
|
|
74
|
-
allowed = {"rev-parse" => %w[--verify --quiet --git-dir], "cat-file" => %w[-t -s blob],
|
|
75
|
-
"rev-list" => %w[--first-parent --parents], "ls-tree" => %w[-r -l -d --name-only --],
|
|
76
|
-
"show" => [], "log" => %w[--diff-filter=A -1 --format=%H --]}
|
|
77
|
-
flags = allowed[argv.first]
|
|
78
|
-
flags && argv.drop(1).all? { |item| !item.start_with?("-") || flags.include?(item) || item == "--max-count=100001" }
|
|
79
|
-
end
|
|
80
|
-
|
|
81
|
-
end
|
|
82
|
-
end
|
|
83
|
-
end
|
|
84
|
-
end
|
|
@@ -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
|