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.
- 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 -2119
- 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 -4
- data/handbook/workflow-instructions/assign/drive.wf.md +47 -195
- 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/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 -176
- 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/candidate_limits.rb +0 -20
- 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/atoms/git_blob_batch.rb +0 -59
- 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_blob_batch.rb +0 -55
- data/lib/ace/assign/authority/candidate_head_comparison.rb +0 -139
- data/lib/ace/assign/authority/candidate_transfer.rb +0 -357
- 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/command_execution.rb +0 -180
- 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 -457
- 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 -41
- 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_context.rb +0 -38
- data/lib/ace/assign/authority/endcap_service_settlement.rb +0 -152
- 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/fixed_publication_lifecycle.rb +0 -176
- data/lib/ace/assign/authority/fixed_service_lifecycle.rb +0 -594
- 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 -104
- data/lib/ace/assign/authority/ordinary_effect_settlement.rb +0 -118
- data/lib/ace/assign/authority/ordinary_lifecycle.rb +0 -227
- data/lib/ace/assign/authority/ordinary_merge_evidence.rb +0 -81
- 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_release_artifacts.rb +0 -143
- data/lib/ace/assign/authority/ordinary_results.rb +0 -206
- data/lib/ace/assign/authority/ordinary_review_evidence.rb +0 -180
- data/lib/ace/assign/authority/ordinary_reviews.rb +0 -214
- data/lib/ace/assign/authority/ordinary_server.rb +0 -75
- data/lib/ace/assign/authority/ordinary_supervisor.rb +0 -99
- 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 -309
- 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/release_artifact_manifest.rb +0 -91
- data/lib/ace/assign/authority/retained_input.rb +0 -53
- 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 -189
- 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 -170
- 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 -1899
- 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 -274
- 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 -320
- data/lib/ace/assign/molecules/terminal_scope_receipt.rb +0 -88
- data/lib/ace/assign/organisms/attempt_coordinator.rb +0 -1374
- data/lib/ace/assign/organisms/delivery_coordinator.rb +0 -342
- data/lib/ace/assign/organisms/ordinary_driver.rb +0 -310
- 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,116 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
module Ace
|
|
4
|
-
module Assign
|
|
5
|
-
module Models
|
|
6
|
-
# Immutable start-time facts for an assignment attempt.
|
|
7
|
-
#
|
|
8
|
-
# A binding freezes exactly what an attempt owns: the immutable attempt
|
|
9
|
-
# ID, the assignment and step/subtree scope, the project, the
|
|
10
|
-
# authenticated actor/role and runtime identity resolved at the
|
|
11
|
-
# execution boundary, the source task ID, the base head captured once at
|
|
12
|
-
# start, and the configured evidence ref. Bindings are never mutated;
|
|
13
|
-
# changes require a new attempt.
|
|
14
|
-
class AttemptBinding
|
|
15
|
-
attr_reader :attempt_id, :assignment_id, :scope, :project_id, :task_id,
|
|
16
|
-
:actor, :role, :runtime, :base_head, :evidence_git_ref, :created_at
|
|
17
|
-
|
|
18
|
-
# @param attempt_id [String] Immutable attempt ID (ace-b36ts)
|
|
19
|
-
# @param assignment_id [String] Owning assignment ID
|
|
20
|
-
# @param scope [String] Canonical step/subtree scope
|
|
21
|
-
# @param project_id [String] Project the attempt executes in
|
|
22
|
-
# @param actor [String] Authenticated actor from the execution boundary
|
|
23
|
-
# @param role [String] Trusted role (coordinator, service, worker)
|
|
24
|
-
# @param runtime [String] Runtime identity of the executing boundary
|
|
25
|
-
# @param base_head [String] Git HEAD captured exactly once at start
|
|
26
|
-
# @param task_id [String, nil] Source task ID (nil for taskless assignments)
|
|
27
|
-
# @param evidence_git_ref [String, nil] Evidence ref for managed attempts
|
|
28
|
-
# @param created_at [Time] Binding creation time
|
|
29
|
-
def initialize(attempt_id:, assignment_id:, scope:, project_id:, actor:, role:, runtime:, base_head:,
|
|
30
|
-
task_id: nil, evidence_git_ref: nil, created_at:)
|
|
31
|
-
@attempt_id = attempt_id.freeze
|
|
32
|
-
@assignment_id = assignment_id.freeze
|
|
33
|
-
@scope = scope.freeze
|
|
34
|
-
@project_id = project_id.freeze
|
|
35
|
-
@task_id = task_id&.freeze
|
|
36
|
-
@actor = actor.freeze
|
|
37
|
-
@role = role.freeze
|
|
38
|
-
@runtime = runtime.freeze
|
|
39
|
-
@base_head = base_head.freeze
|
|
40
|
-
@evidence_git_ref = evidence_git_ref&.freeze
|
|
41
|
-
@created_at = created_at
|
|
42
|
-
end
|
|
43
|
-
|
|
44
|
-
# @return [Boolean] True when the attempt is bound to a source task and
|
|
45
|
-
# eligible for managed evidence-ref delivery
|
|
46
|
-
def managed?
|
|
47
|
-
!task_id.nil? && !task_id.to_s.strip.empty?
|
|
48
|
-
end
|
|
49
|
-
|
|
50
|
-
# Recovery mode implied by task attachment: git-backed journaling for
|
|
51
|
-
# task-attached attempts, disposable local state otherwise.
|
|
52
|
-
#
|
|
53
|
-
# @return [String] "git" or "local_only"
|
|
54
|
-
def recovery_mode
|
|
55
|
-
managed? ? "git" : "local_only"
|
|
56
|
-
end
|
|
57
|
-
|
|
58
|
-
# Identity facts repeated by receipts must match the binding exactly.
|
|
59
|
-
#
|
|
60
|
-
# @param other [AttemptBinding] Binding to compare
|
|
61
|
-
# @return [Boolean] True if both bindings own the same attempt identity
|
|
62
|
-
def same_identity?(other)
|
|
63
|
-
attempt_id == other.attempt_id &&
|
|
64
|
-
assignment_id == other.assignment_id &&
|
|
65
|
-
scope == other.scope &&
|
|
66
|
-
project_id == other.project_id
|
|
67
|
-
end
|
|
68
|
-
|
|
69
|
-
# Convert to a hash for canonical serialization.
|
|
70
|
-
# @return [Hash] Binding data with string keys
|
|
71
|
-
def to_h
|
|
72
|
-
{
|
|
73
|
-
"attempt_id" => attempt_id,
|
|
74
|
-
"assignment_id" => assignment_id,
|
|
75
|
-
"scope" => scope,
|
|
76
|
-
"project_id" => project_id,
|
|
77
|
-
"task_id" => task_id,
|
|
78
|
-
"actor" => actor,
|
|
79
|
-
"role" => role,
|
|
80
|
-
"runtime" => runtime,
|
|
81
|
-
"base_head" => base_head,
|
|
82
|
-
"evidence_git_ref" => evidence_git_ref,
|
|
83
|
-
"created_at" => created_at.iso8601
|
|
84
|
-
}
|
|
85
|
-
end
|
|
86
|
-
|
|
87
|
-
# Rebuild a binding from serialized data.
|
|
88
|
-
#
|
|
89
|
-
# @param data [Hash] Serialized binding (string keys)
|
|
90
|
-
# @return [AttemptBinding] Rebuilt binding
|
|
91
|
-
def self.from_h(data)
|
|
92
|
-
new(
|
|
93
|
-
attempt_id: data["attempt_id"],
|
|
94
|
-
assignment_id: data["assignment_id"],
|
|
95
|
-
scope: data["scope"],
|
|
96
|
-
project_id: data["project_id"],
|
|
97
|
-
task_id: data["task_id"],
|
|
98
|
-
actor: data["actor"],
|
|
99
|
-
role: data["role"],
|
|
100
|
-
runtime: data["runtime"],
|
|
101
|
-
base_head: data["base_head"],
|
|
102
|
-
evidence_git_ref: data["evidence_git_ref"],
|
|
103
|
-
created_at: parse_time(data["created_at"])
|
|
104
|
-
)
|
|
105
|
-
end
|
|
106
|
-
|
|
107
|
-
def self.parse_time(value)
|
|
108
|
-
return value if value.is_a?(Time)
|
|
109
|
-
require "time"
|
|
110
|
-
Time.parse(value)
|
|
111
|
-
end
|
|
112
|
-
private_class_method :parse_time
|
|
113
|
-
end
|
|
114
|
-
end
|
|
115
|
-
end
|
|
116
|
-
end
|
|
@@ -1,86 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
require_relative "../atoms/evidence_digest"
|
|
4
|
-
|
|
5
|
-
module Ace
|
|
6
|
-
module Assign
|
|
7
|
-
module Models
|
|
8
|
-
# Append-only accepted evidence event.
|
|
9
|
-
#
|
|
10
|
-
# Evidence events are the journal trail of an attempt: intent,
|
|
11
|
-
# process start, accepted receipts, state transitions, candidate
|
|
12
|
-
# invalidations, and reconciliations. Each event chains to the digest
|
|
13
|
-
# of its predecessor for the attempt, so accepted history cannot be
|
|
14
|
-
# reordered or silently rewritten. Events carry bounded, non-secret
|
|
15
|
-
# metadata only.
|
|
16
|
-
module EvidenceEvent
|
|
17
|
-
TYPES = %w[
|
|
18
|
-
intent process_start receipt_accepted transition attempt_stopped
|
|
19
|
-
candidate_invalidated reconciliation
|
|
20
|
-
service_claim service_transition service_challenge service_no_effect_challenge service_publication_challenge service_publication_issuing recovery_observation inbox_binding delivery
|
|
21
|
-
authority_mutation evidence_import proposal_state result_submitted prompt_issued prompt_outcome prompt_completion_observed input_inhibited
|
|
22
|
-
scope_provisioning scope_bound scope_native_bound scope_child_bound scope_sealed scope_closed_no_writers
|
|
23
|
-
merge_admission merge_transition
|
|
24
|
-
].freeze
|
|
25
|
-
|
|
26
|
-
# Build an event chained to a previous digest.
|
|
27
|
-
#
|
|
28
|
-
# @param type [String] Event type (see TYPES)
|
|
29
|
-
# @param attempt_id [String] Owning attempt ID
|
|
30
|
-
# @param payload [Hash] Bounded, non-secret event metadata
|
|
31
|
-
# @param previous_digest [String, nil] Digest of the previous event (chain)
|
|
32
|
-
# @param recorded_at [Time] Event creation time
|
|
33
|
-
# @return [Hash] Serializable event with type, payload, digests, timestamp
|
|
34
|
-
# @raise [ArgumentError] for unknown event types
|
|
35
|
-
def self.build(type:, attempt_id:, payload:, previous_digest: nil, recorded_at: Time.now.utc)
|
|
36
|
-
raise ArgumentError, "Unknown evidence event type: #{type}" unless TYPES.include?(type.to_s)
|
|
37
|
-
|
|
38
|
-
recorded_at = recorded_at.utc
|
|
39
|
-
body = {
|
|
40
|
-
"type" => type.to_s,
|
|
41
|
-
"attempt_id" => attempt_id,
|
|
42
|
-
"payload" => payload,
|
|
43
|
-
"previous_digest" => previous_digest,
|
|
44
|
-
"recorded_at" => recorded_at.iso8601
|
|
45
|
-
}
|
|
46
|
-
body.merge("digest" => Atoms::EvidenceDigest.digest(body))
|
|
47
|
-
end
|
|
48
|
-
|
|
49
|
-
# Validate that a stored event is intact: its digest must match a
|
|
50
|
-
# recomputation over its own body.
|
|
51
|
-
#
|
|
52
|
-
# @param event [Hash] Stored event
|
|
53
|
-
# @return [Boolean] True if the event digest verifies
|
|
54
|
-
def self.valid?(event)
|
|
55
|
-
stored = event["digest"]
|
|
56
|
-
return false if stored.nil?
|
|
57
|
-
|
|
58
|
-
body = {
|
|
59
|
-
"type" => event["type"],
|
|
60
|
-
"attempt_id" => event["attempt_id"],
|
|
61
|
-
"payload" => event["payload"],
|
|
62
|
-
"previous_digest" => event["previous_digest"],
|
|
63
|
-
"recorded_at" => event["recorded_at"]
|
|
64
|
-
}
|
|
65
|
-
Atoms::EvidenceDigest.digest(body) == stored
|
|
66
|
-
end
|
|
67
|
-
|
|
68
|
-
# Validate a whole chain: every event verifies and links to its
|
|
69
|
-
# predecessor.
|
|
70
|
-
#
|
|
71
|
-
# @param events [Array<Hash>] Ordered event list
|
|
72
|
-
# @return [Boolean] True if the chain is intact
|
|
73
|
-
def self.chain_valid?(events)
|
|
74
|
-
previous = nil
|
|
75
|
-
events.all? do |event|
|
|
76
|
-
return false unless valid?(event)
|
|
77
|
-
return false unless event["previous_digest"] == previous
|
|
78
|
-
|
|
79
|
-
previous = event["digest"]
|
|
80
|
-
true
|
|
81
|
-
end
|
|
82
|
-
end
|
|
83
|
-
end
|
|
84
|
-
end
|
|
85
|
-
end
|
|
86
|
-
end
|
|
@@ -1,139 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
require_relative "../atoms/evidence_digest"
|
|
4
|
-
|
|
5
|
-
module Ace
|
|
6
|
-
module Assign
|
|
7
|
-
module Models
|
|
8
|
-
# Structured, non-secret execution receipt.
|
|
9
|
-
#
|
|
10
|
-
# A receipt is the only accepted proof that an attempted operation
|
|
11
|
-
# completed: it names the attributed producer, the attempted operation,
|
|
12
|
-
# the actual tested/reviewed head, the scope, a verdict, the required
|
|
13
|
-
# artifacts with SHA-256 digests, and executed checks. Receipts never
|
|
14
|
-
# carry credentials, terminal bytes, or receipt-file payloads; journal
|
|
15
|
-
# entries keep metadata and digests only.
|
|
16
|
-
class ExecutionReceipt
|
|
17
|
-
VERDICTS = %w[succeeded failed].freeze
|
|
18
|
-
|
|
19
|
-
# Field names rejected outright in receipt payloads: accepted evidence
|
|
20
|
-
# is structured metadata, never raw process output or credentials.
|
|
21
|
-
FORBIDDEN_FIELDS = %w[
|
|
22
|
-
stdout stderr terminal_output log logs credentials secret secrets
|
|
23
|
-
token password api_key private_key env environment
|
|
24
|
-
].freeze
|
|
25
|
-
|
|
26
|
-
attr_reader :attempt_id, :assignment_id, :project_id, :scope, :operation,
|
|
27
|
-
:producer, :head, :verdict, :artifacts, :checks, :review, :campaign, :recorded_at, :digest
|
|
28
|
-
|
|
29
|
-
# @param attempt_id [String] Attempt the receipt belongs to
|
|
30
|
-
# @param assignment_id [String] Owning assignment ID
|
|
31
|
-
# @param project_id [String] Project the operation ran in
|
|
32
|
-
# @param scope [String] Canonical step/subtree scope
|
|
33
|
-
# @param operation [String] Attempted operation name
|
|
34
|
-
# @param producer [Hash] Attributed producer (actor, role, runtime)
|
|
35
|
-
# @param head [String] Actual tested/reviewed head
|
|
36
|
-
# @param verdict [String] "succeeded" or "failed"
|
|
37
|
-
# @param artifacts [Array<Hash>] Artifacts with path + sha256
|
|
38
|
-
# @param checks [Array<Hash>] Executed checks with name + verdict
|
|
39
|
-
# @param review [Hash, nil] Reviewer verdict for review receipts
|
|
40
|
-
# @param recorded_at [Time] Receipt creation time
|
|
41
|
-
# @param digest [String, nil] Canonical digest (computed when nil)
|
|
42
|
-
def initialize(attempt_id:, assignment_id:, project_id:, scope:, operation:, producer:, head:, verdict:,
|
|
43
|
-
artifacts: [], checks: [], review: nil, campaign: nil, recorded_at:, digest: nil)
|
|
44
|
-
@attempt_id = attempt_id
|
|
45
|
-
@assignment_id = assignment_id
|
|
46
|
-
@project_id = project_id
|
|
47
|
-
@scope = scope
|
|
48
|
-
@operation = operation
|
|
49
|
-
@producer = producer
|
|
50
|
-
@head = head
|
|
51
|
-
@verdict = verdict
|
|
52
|
-
@artifacts = artifacts
|
|
53
|
-
@checks = checks
|
|
54
|
-
@review = review
|
|
55
|
-
@campaign = campaign
|
|
56
|
-
@recorded_at = recorded_at
|
|
57
|
-
@digest = digest || Atoms::EvidenceDigest.digest(digest_payload)
|
|
58
|
-
end
|
|
59
|
-
|
|
60
|
-
# Canonical payload covered by the receipt digest (excludes the digest
|
|
61
|
-
# itself and the record timestamp, which is assigned at acceptance).
|
|
62
|
-
#
|
|
63
|
-
# @return [Hash] Digest payload
|
|
64
|
-
def digest_payload
|
|
65
|
-
payload = {
|
|
66
|
-
"attempt_id" => attempt_id,
|
|
67
|
-
"assignment_id" => assignment_id,
|
|
68
|
-
"project_id" => project_id,
|
|
69
|
-
"scope" => scope,
|
|
70
|
-
"operation" => operation,
|
|
71
|
-
"producer" => producer,
|
|
72
|
-
"head" => head,
|
|
73
|
-
"verdict" => verdict,
|
|
74
|
-
"artifacts" => artifacts,
|
|
75
|
-
"checks" => checks,
|
|
76
|
-
"review" => review
|
|
77
|
-
}
|
|
78
|
-
payload["campaign"] = campaign if campaign
|
|
79
|
-
payload
|
|
80
|
-
end
|
|
81
|
-
|
|
82
|
-
# Convert to a hash for serialization.
|
|
83
|
-
# @return [Hash] Receipt data with string keys
|
|
84
|
-
def to_h
|
|
85
|
-
digest_payload.merge(
|
|
86
|
-
"recorded_at" => recorded_at.iso8601,
|
|
87
|
-
"digest" => digest
|
|
88
|
-
)
|
|
89
|
-
end
|
|
90
|
-
|
|
91
|
-
# Rebuild a receipt from serialized or submitted data.
|
|
92
|
-
#
|
|
93
|
-
# @param data [Hash] Receipt data (string keys); missing digest is computed
|
|
94
|
-
# @return [ExecutionReceipt] Receipt instance
|
|
95
|
-
def self.from_h(data)
|
|
96
|
-
new(
|
|
97
|
-
attempt_id: data["attempt_id"],
|
|
98
|
-
assignment_id: data["assignment_id"],
|
|
99
|
-
project_id: data["project_id"],
|
|
100
|
-
scope: data["scope"],
|
|
101
|
-
operation: data["operation"],
|
|
102
|
-
producer: data["producer"] || {},
|
|
103
|
-
head: data["head"],
|
|
104
|
-
verdict: data["verdict"],
|
|
105
|
-
artifacts: data["artifacts"] || [],
|
|
106
|
-
checks: data["checks"] || [],
|
|
107
|
-
review: data["review"],
|
|
108
|
-
campaign: data["campaign"],
|
|
109
|
-
recorded_at: parse_time(data["recorded_at"]) || Time.now.utc,
|
|
110
|
-
digest: data["digest"]
|
|
111
|
-
)
|
|
112
|
-
end
|
|
113
|
-
|
|
114
|
-
# Reject payloads that carry forbidden fields anywhere in the tree.
|
|
115
|
-
#
|
|
116
|
-
# @param data [Hash] Raw submitted receipt data
|
|
117
|
-
# @return [Boolean] True if a forbidden field is present
|
|
118
|
-
def self.forbidden_field?(data)
|
|
119
|
-
case data
|
|
120
|
-
when Hash
|
|
121
|
-
data.any? { |key, value| FORBIDDEN_FIELDS.include?(key.to_s) || forbidden_field?(value) }
|
|
122
|
-
when Array
|
|
123
|
-
data.any? { |item| forbidden_field?(item) }
|
|
124
|
-
else
|
|
125
|
-
false
|
|
126
|
-
end
|
|
127
|
-
end
|
|
128
|
-
|
|
129
|
-
def self.parse_time(value)
|
|
130
|
-
return nil if value.nil?
|
|
131
|
-
return value if value.is_a?(Time)
|
|
132
|
-
require "time"
|
|
133
|
-
Time.parse(value)
|
|
134
|
-
end
|
|
135
|
-
private_class_method :parse_time
|
|
136
|
-
end
|
|
137
|
-
end
|
|
138
|
-
end
|
|
139
|
-
end
|
|
@@ -1,127 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
require_relative "canonical_attempt_state"
|
|
3
|
-
|
|
4
|
-
require "json"
|
|
5
|
-
|
|
6
|
-
module Ace
|
|
7
|
-
module Assign
|
|
8
|
-
module Molecules
|
|
9
|
-
# Conservative classification and resolution of interrupted attempts.
|
|
10
|
-
#
|
|
11
|
-
# Classification of a `running` attempt found after a lost session:
|
|
12
|
-
# - no recorded process start -> `uncertain` (launch may be unbound)
|
|
13
|
-
# - recorded process still verifiably live -> keep `running`
|
|
14
|
-
# - otherwise -> `uncertain` (the operation may have completed without
|
|
15
|
-
# a durable receipt)
|
|
16
|
-
#
|
|
17
|
-
# Reconciliation resolves an `uncertain` attempt only against verified
|
|
18
|
-
# receipts whose producer runtime matches the recorded execution
|
|
19
|
-
# boundary. It never replays operations: merge, publish, and deploy
|
|
20
|
-
# effects are never retried automatically, and an uncertain attempt
|
|
21
|
-
# without a receipt stays uncertain.
|
|
22
|
-
class AttemptReconciler
|
|
23
|
-
# The protected boundary supplies authenticated original scope facts
|
|
24
|
-
# and exact kernel identities; never a runtime discovered by pane name.
|
|
25
|
-
def self.protected_observation(kernel:, binding:, observer:, lineage:)
|
|
26
|
-
return {"liveness" => "unknown"} if lineage.sealed?
|
|
27
|
-
kernel.live!(binding.fetch("process_identity"))
|
|
28
|
-
kernel.live!(binding.fetch("native_origin").fetch("server_identity"))
|
|
29
|
-
observation = observer.observe(lineage)
|
|
30
|
-
unless observation.is_a?(Hash) && observation["populated"].is_a?(Integer) && observation["populated"].positive?
|
|
31
|
-
return {"liveness" => "unknown"}
|
|
32
|
-
end
|
|
33
|
-
{"liveness" => "live", "identity" => binding.fetch("process_identity"), "runtime_binding" => binding}
|
|
34
|
-
rescue AttemptErrors::EvidenceUnavailable, Ace::Runtime::RuntimeUnavailableError
|
|
35
|
-
{"liveness" => "unknown"}
|
|
36
|
-
end
|
|
37
|
-
|
|
38
|
-
# @param journal [EvidenceJournal, nil] Managed evidence journal
|
|
39
|
-
# @param verifier [ReceiptVerifier, nil] Receipt verifier (coordinator supplies on use)
|
|
40
|
-
def initialize(journal: nil, verifier: nil, observer: nil, runtime_resolver: Ace::Runtime)
|
|
41
|
-
@journal = journal
|
|
42
|
-
@verifier = verifier
|
|
43
|
-
@observer = observer || Ace::Runtime::Molecules::ProcessIdentity.new
|
|
44
|
-
@runtime_resolver = runtime_resolver
|
|
45
|
-
end
|
|
46
|
-
|
|
47
|
-
# Classify a running attempt after an interruption.
|
|
48
|
-
#
|
|
49
|
-
# @param attempt [Models::Attempt] Running attempt
|
|
50
|
-
# @return [Symbol] :stopped, :live, or :uncertain
|
|
51
|
-
def classify(attempt)
|
|
52
|
-
events = events_for(attempt)
|
|
53
|
-
if events.any? { |event| event["type"] == "attempt_stopped" } && CanonicalAttemptState.derive(events) == "stopped"
|
|
54
|
-
return :stopped
|
|
55
|
-
end
|
|
56
|
-
return :live if process_live?(attempt)
|
|
57
|
-
:uncertain
|
|
58
|
-
end
|
|
59
|
-
|
|
60
|
-
# @param attempt [Models::Attempt] Attempt to inspect
|
|
61
|
-
# @return [Boolean] True when a process start was recorded
|
|
62
|
-
def process_started?(attempt)
|
|
63
|
-
!start_event(attempt).nil?
|
|
64
|
-
end
|
|
65
|
-
|
|
66
|
-
# @param attempt [Models::Attempt] Attempt to inspect
|
|
67
|
-
# @return [Boolean] True when the recorded process is verifiably alive
|
|
68
|
-
def process_live?(attempt)
|
|
69
|
-
observation(attempt)["liveness"] == "live"
|
|
70
|
-
end
|
|
71
|
-
|
|
72
|
-
def observation(attempt)
|
|
73
|
-
payload = start_event(attempt)&.dig("payload")
|
|
74
|
-
observation = @observer.observe(payload&.dig("process_identity"))
|
|
75
|
-
binding = payload&.dig("runtime_binding")
|
|
76
|
-
return observation unless binding && observation["liveness"] == "live"
|
|
77
|
-
|
|
78
|
-
live = @runtime_resolver.resolve(binding["runtime"]).process_binding(
|
|
79
|
-
pane: binding["pane"], caller_pid: binding.dig("process_identity", "pid"))
|
|
80
|
-
return observation if binding == live
|
|
81
|
-
|
|
82
|
-
observation.merge("liveness" => "unknown", "reason" => "native process/session binding changed")
|
|
83
|
-
rescue Ace::Runtime::Error
|
|
84
|
-
{"liveness" => "unknown", "observed_at" => Time.now.utc.iso8601,
|
|
85
|
-
"reason" => "native process/session observation unavailable"}
|
|
86
|
-
end
|
|
87
|
-
|
|
88
|
-
def events(attempt)
|
|
89
|
-
events_for(attempt)
|
|
90
|
-
end
|
|
91
|
-
|
|
92
|
-
# Runtime recorded at the attempt's process start — the boundary any
|
|
93
|
-
# reconciling receipt must attribute to.
|
|
94
|
-
#
|
|
95
|
-
# @param attempt [Models::Attempt] Attempt to inspect
|
|
96
|
-
# @return [String, nil] Recorded runtime identity
|
|
97
|
-
def recorded_runtime(attempt)
|
|
98
|
-
start_event(attempt)&.dig("payload", "runtime")
|
|
99
|
-
end
|
|
100
|
-
|
|
101
|
-
def recorded_binding(attempt)
|
|
102
|
-
binding = start_event(attempt)&.dig("payload", "runtime_binding")
|
|
103
|
-
JSON.parse(JSON.generate(binding)) if binding.is_a?(Hash)
|
|
104
|
-
end
|
|
105
|
-
|
|
106
|
-
private
|
|
107
|
-
|
|
108
|
-
def start_event(attempt)
|
|
109
|
-
events_for(attempt).reverse.find do |event|
|
|
110
|
-
event["type"] == "process_start" && event["attempt_id"] == attempt.attempt_id
|
|
111
|
-
end
|
|
112
|
-
end
|
|
113
|
-
|
|
114
|
-
def events_for(attempt)
|
|
115
|
-
events = if attempt.managed?
|
|
116
|
-
(@journal || Molecules::EvidenceJournal.new(repo_root: Dir.pwd))
|
|
117
|
-
.read_events(attempt.binding.assignment_id)
|
|
118
|
-
else
|
|
119
|
-
attempt.events
|
|
120
|
-
end
|
|
121
|
-
# Only this attempt's events define its execution boundary.
|
|
122
|
-
events.select { |event| event["attempt_id"] == attempt.attempt_id }
|
|
123
|
-
end
|
|
124
|
-
end
|
|
125
|
-
end
|
|
126
|
-
end
|
|
127
|
-
end
|
|
@@ -1,201 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
require "fileutils"
|
|
4
|
-
require "json"
|
|
5
|
-
|
|
6
|
-
module Ace
|
|
7
|
-
module Assign
|
|
8
|
-
module Molecules
|
|
9
|
-
# Durable local attempt state: the single authority for attempt records
|
|
10
|
-
# and active subtree ownership.
|
|
11
|
-
#
|
|
12
|
-
# Layout under the assignment cache directory (`.ace-local/assign` by
|
|
13
|
-
# default, which remains disposable session/projection data):
|
|
14
|
-
# - `<assignment-id>/attempts/records/<attempt-id>.json` — full attempt
|
|
15
|
-
# record, written atomically (temp file + rename).
|
|
16
|
-
# - `<assignment-id>/attempts/active/<ownership-key>.json` — pointer to
|
|
17
|
-
# the one active attempt owning an assignment + normalized subtree.
|
|
18
|
-
# - `<assignment-id>/attempts/.lock` — flock serialization for
|
|
19
|
-
# claim/release and record transitions.
|
|
20
|
-
#
|
|
21
|
-
# Only one active attempt owns a subtree; claim replaces the pointer
|
|
22
|
-
# only for the identical binding, and release clears it only when it
|
|
23
|
-
# still references the given attempt.
|
|
24
|
-
class AttemptStore
|
|
25
|
-
# @param cache_base [String] Base cache directory
|
|
26
|
-
def initialize(cache_base: nil)
|
|
27
|
-
@cache_base = cache_base || Ace::Assign.cache_dir
|
|
28
|
-
end
|
|
29
|
-
|
|
30
|
-
# @param assignment_id [String] Assignment ID
|
|
31
|
-
# @return [String] Attempts directory for the assignment
|
|
32
|
-
def attempts_dir(assignment_id)
|
|
33
|
-
File.join(@cache_base, assignment_id, "attempts")
|
|
34
|
-
end
|
|
35
|
-
|
|
36
|
-
# Serialize a block under the assignment's exclusive attempt lock.
|
|
37
|
-
#
|
|
38
|
-
# @param assignment_id [String] Assignment ID
|
|
39
|
-
# @yield Block executed while holding the lock
|
|
40
|
-
def with_lock(assignment_id)
|
|
41
|
-
FileUtils.mkdir_p(attempts_dir(assignment_id))
|
|
42
|
-
File.open(lock_path(assignment_id), File::RDWR | File::CREAT) do |lock|
|
|
43
|
-
lock.flock(File::LOCK_EX)
|
|
44
|
-
begin
|
|
45
|
-
yield
|
|
46
|
-
ensure
|
|
47
|
-
lock.flock(File::LOCK_UN)
|
|
48
|
-
end
|
|
49
|
-
end
|
|
50
|
-
end
|
|
51
|
-
|
|
52
|
-
# Persist an attempt record atomically.
|
|
53
|
-
#
|
|
54
|
-
# @param attempt [Models::Attempt] Attempt to persist
|
|
55
|
-
def save(attempt)
|
|
56
|
-
FileUtils.mkdir_p(records_dir(attempt.binding.assignment_id))
|
|
57
|
-
atomic_write(
|
|
58
|
-
record_path(attempt.binding.assignment_id, attempt.attempt_id),
|
|
59
|
-
JSON.pretty_generate(attempt.to_h)
|
|
60
|
-
)
|
|
61
|
-
end
|
|
62
|
-
|
|
63
|
-
# Load an attempt record.
|
|
64
|
-
#
|
|
65
|
-
# @param assignment_id [String] Assignment ID
|
|
66
|
-
# @param attempt_id [String] Attempt ID
|
|
67
|
-
# @return [Models::Attempt, nil] Attempt or nil when absent/corrupt
|
|
68
|
-
def load(assignment_id, attempt_id)
|
|
69
|
-
path = record_path(assignment_id, attempt_id)
|
|
70
|
-
return nil unless File.exist?(path)
|
|
71
|
-
|
|
72
|
-
Models::Attempt.from_h(JSON.parse(File.read(path)))
|
|
73
|
-
rescue JSON::ParserError, KeyError, ArgumentError, TypeError => e
|
|
74
|
-
warn "ace-assign: corrupt attempt record #{path}: #{e.message}" if Ace::Assign.debug?
|
|
75
|
-
nil
|
|
76
|
-
end
|
|
77
|
-
|
|
78
|
-
# The active attempt owning an assignment + scope, if any.
|
|
79
|
-
#
|
|
80
|
-
# @param assignment_id [String] Assignment ID
|
|
81
|
-
# @param scope [String] Step/subtree scope
|
|
82
|
-
# @return [Models::Attempt, nil] Active attempt or nil
|
|
83
|
-
def active(assignment_id, scope)
|
|
84
|
-
pointer = active_path(assignment_id, scope)
|
|
85
|
-
return nil unless File.exist?(pointer)
|
|
86
|
-
|
|
87
|
-
entry = JSON.parse(File.read(pointer))
|
|
88
|
-
attempt = load(assignment_id, entry["attempt_id"])
|
|
89
|
-
return nil unless attempt&.active?
|
|
90
|
-
|
|
91
|
-
attempt
|
|
92
|
-
rescue JSON::ParserError, TypeError
|
|
93
|
-
nil
|
|
94
|
-
end
|
|
95
|
-
|
|
96
|
-
# Claim subtree ownership for an attempt. Callers must hold
|
|
97
|
-
# {with_lock} for read-modify-write atomicity.
|
|
98
|
-
#
|
|
99
|
-
# @param assignment_id [String] Assignment ID
|
|
100
|
-
# @param scope [String] Canonical scope
|
|
101
|
-
# @param attempt [Models::Attempt] Active attempt claiming the scope
|
|
102
|
-
def claim(assignment_id, scope, attempt)
|
|
103
|
-
FileUtils.mkdir_p(active_dir(assignment_id))
|
|
104
|
-
atomic_write(
|
|
105
|
-
active_path(assignment_id, scope),
|
|
106
|
-
JSON.generate({"attempt_id" => attempt.attempt_id})
|
|
107
|
-
)
|
|
108
|
-
end
|
|
109
|
-
|
|
110
|
-
# Release subtree ownership when it still references the attempt.
|
|
111
|
-
#
|
|
112
|
-
# @param assignment_id [String] Assignment ID
|
|
113
|
-
# @param scope [String] Canonical scope
|
|
114
|
-
# @param attempt_id [String] Attempt releasing ownership
|
|
115
|
-
def release(assignment_id, scope, attempt_id)
|
|
116
|
-
pointer = active_path(assignment_id, scope)
|
|
117
|
-
return unless File.exist?(pointer)
|
|
118
|
-
|
|
119
|
-
entry = JSON.parse(File.read(pointer))
|
|
120
|
-
File.delete(pointer) if entry["attempt_id"] == attempt_id
|
|
121
|
-
rescue JSON::ParserError, TypeError
|
|
122
|
-
nil
|
|
123
|
-
end
|
|
124
|
-
|
|
125
|
-
# Atomically reserve an attempt ID across all assignments
|
|
126
|
-
# (mkdir-based EEXIST check). Returns false when already taken.
|
|
127
|
-
#
|
|
128
|
-
# @param attempt_id [String] Candidate ID
|
|
129
|
-
# @return [Boolean] True when newly reserved
|
|
130
|
-
def reserve_id(attempt_id)
|
|
131
|
-
registry = File.join(@cache_base, ".attempt-ids")
|
|
132
|
-
FileUtils.mkdir_p(registry)
|
|
133
|
-
Dir.mkdir(File.join(registry, attempt_id))
|
|
134
|
-
true
|
|
135
|
-
rescue Errno::EEXIST
|
|
136
|
-
false
|
|
137
|
-
end
|
|
138
|
-
|
|
139
|
-
# Locate an attempt record by ID across all assignments (used by
|
|
140
|
-
# reconcile, which receives only the attempt ID).
|
|
141
|
-
#
|
|
142
|
-
# @param attempt_id [String] Attempt ID
|
|
143
|
-
# @return [Models::Attempt, nil] Attempt or nil
|
|
144
|
-
def find(attempt_id)
|
|
145
|
-
path = Dir.glob(File.join(@cache_base, "*", "attempts", "records", "#{attempt_id}.json")).first
|
|
146
|
-
return nil unless path
|
|
147
|
-
|
|
148
|
-
assignment_id = File.basename(File.dirname(File.dirname(File.dirname(path))))
|
|
149
|
-
load(assignment_id, attempt_id)
|
|
150
|
-
end
|
|
151
|
-
|
|
152
|
-
# All attempt records for an assignment, newest first. Ties (records
|
|
153
|
-
# serialized within the same second) break deterministically by
|
|
154
|
-
# attempt ID.
|
|
155
|
-
#
|
|
156
|
-
# @param assignment_id [String] Assignment ID
|
|
157
|
-
# @return [Array<Models::Attempt>] Persisted attempts
|
|
158
|
-
def list(assignment_id)
|
|
159
|
-
Dir.glob(File.join(records_dir(assignment_id), "*.json"))
|
|
160
|
-
.map { |path| load(assignment_id, File.basename(path, ".json")) }
|
|
161
|
-
.compact
|
|
162
|
-
.sort_by { |attempt| [attempt.updated_at.utc, attempt.attempt_id] }
|
|
163
|
-
.reverse
|
|
164
|
-
end
|
|
165
|
-
|
|
166
|
-
private
|
|
167
|
-
|
|
168
|
-
def records_dir(assignment_id)
|
|
169
|
-
File.join(attempts_dir(assignment_id), "records")
|
|
170
|
-
end
|
|
171
|
-
|
|
172
|
-
def active_dir(assignment_id)
|
|
173
|
-
File.join(attempts_dir(assignment_id), "active")
|
|
174
|
-
end
|
|
175
|
-
|
|
176
|
-
def lock_path(assignment_id)
|
|
177
|
-
File.join(attempts_dir(assignment_id), ".lock")
|
|
178
|
-
end
|
|
179
|
-
|
|
180
|
-
def record_path(assignment_id, attempt_id)
|
|
181
|
-
File.join(records_dir(assignment_id), "#{attempt_id}.json")
|
|
182
|
-
end
|
|
183
|
-
|
|
184
|
-
def active_path(assignment_id, scope)
|
|
185
|
-
File.join(active_dir(assignment_id), Atoms::AssignmentScope.ownership_key(assignment_id, scope))
|
|
186
|
-
end
|
|
187
|
-
|
|
188
|
-
# Atomic same-filesystem write: temp file plus rename.
|
|
189
|
-
def atomic_write(path, content)
|
|
190
|
-
dir = File.dirname(path)
|
|
191
|
-
FileUtils.mkdir_p(dir)
|
|
192
|
-
temp = File.join(dir, ".tmp-#{Process.pid}-#{rand(1_000_000)}")
|
|
193
|
-
File.write(temp, content)
|
|
194
|
-
File.rename(temp, path)
|
|
195
|
-
ensure
|
|
196
|
-
File.delete(temp) if temp && File.exist?(temp)
|
|
197
|
-
end
|
|
198
|
-
end
|
|
199
|
-
end
|
|
200
|
-
end
|
|
201
|
-
end
|