aidp 0.24.0 → 0.26.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 (58) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +72 -7
  3. data/lib/aidp/analyze/error_handler.rb +11 -0
  4. data/lib/aidp/auto_update/bundler_adapter.rb +66 -0
  5. data/lib/aidp/auto_update/checkpoint.rb +178 -0
  6. data/lib/aidp/auto_update/checkpoint_store.rb +182 -0
  7. data/lib/aidp/auto_update/coordinator.rb +204 -0
  8. data/lib/aidp/auto_update/errors.rb +17 -0
  9. data/lib/aidp/auto_update/failure_tracker.rb +162 -0
  10. data/lib/aidp/auto_update/rubygems_api_adapter.rb +95 -0
  11. data/lib/aidp/auto_update/update_check.rb +106 -0
  12. data/lib/aidp/auto_update/update_logger.rb +143 -0
  13. data/lib/aidp/auto_update/update_policy.rb +109 -0
  14. data/lib/aidp/auto_update/version_detector.rb +144 -0
  15. data/lib/aidp/auto_update.rb +52 -0
  16. data/lib/aidp/cli.rb +165 -1
  17. data/lib/aidp/execute/work_loop_runner.rb +225 -55
  18. data/lib/aidp/harness/config_loader.rb +20 -11
  19. data/lib/aidp/harness/config_schema.rb +80 -8
  20. data/lib/aidp/harness/configuration.rb +73 -2
  21. data/lib/aidp/harness/filter_strategy.rb +45 -0
  22. data/lib/aidp/harness/generic_filter_strategy.rb +63 -0
  23. data/lib/aidp/harness/output_filter.rb +136 -0
  24. data/lib/aidp/harness/provider_factory.rb +2 -0
  25. data/lib/aidp/harness/provider_manager.rb +18 -3
  26. data/lib/aidp/harness/rspec_filter_strategy.rb +82 -0
  27. data/lib/aidp/harness/test_runner.rb +165 -27
  28. data/lib/aidp/harness/ui/enhanced_tui.rb +4 -1
  29. data/lib/aidp/logger.rb +35 -5
  30. data/lib/aidp/message_display.rb +56 -2
  31. data/lib/aidp/prompt_optimization/style_guide_indexer.rb +3 -1
  32. data/lib/aidp/provider_manager.rb +2 -0
  33. data/lib/aidp/providers/kilocode.rb +202 -0
  34. data/lib/aidp/safe_directory.rb +10 -3
  35. data/lib/aidp/setup/provider_registry.rb +15 -0
  36. data/lib/aidp/setup/wizard.rb +12 -4
  37. data/lib/aidp/skills/composer.rb +4 -0
  38. data/lib/aidp/skills/loader.rb +3 -1
  39. data/lib/aidp/storage/csv_storage.rb +9 -3
  40. data/lib/aidp/storage/file_manager.rb +8 -2
  41. data/lib/aidp/storage/json_storage.rb +9 -3
  42. data/lib/aidp/version.rb +1 -1
  43. data/lib/aidp/watch/build_processor.rb +106 -17
  44. data/lib/aidp/watch/change_request_processor.rb +659 -0
  45. data/lib/aidp/watch/ci_fix_processor.rb +448 -0
  46. data/lib/aidp/watch/plan_processor.rb +81 -8
  47. data/lib/aidp/watch/repository_client.rb +465 -20
  48. data/lib/aidp/watch/review_processor.rb +266 -0
  49. data/lib/aidp/watch/reviewers/base_reviewer.rb +164 -0
  50. data/lib/aidp/watch/reviewers/performance_reviewer.rb +65 -0
  51. data/lib/aidp/watch/reviewers/security_reviewer.rb +65 -0
  52. data/lib/aidp/watch/reviewers/senior_dev_reviewer.rb +33 -0
  53. data/lib/aidp/watch/runner.rb +222 -0
  54. data/lib/aidp/watch/state_store.rb +99 -1
  55. data/lib/aidp/workstream_executor.rb +5 -2
  56. data/lib/aidp.rb +5 -0
  57. data/templates/aidp.yml.example +53 -0
  58. metadata +25 -1
@@ -0,0 +1,204 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "version_detector"
4
+ require_relative "checkpoint_store"
5
+ require_relative "update_logger"
6
+ require_relative "failure_tracker"
7
+ require_relative "update_policy"
8
+ require_relative "errors"
9
+
10
+ module Aidp
11
+ module AutoUpdate
12
+ # Facade for orchestrating the complete auto-update workflow
13
+ class Coordinator
14
+ attr_reader :policy, :version_detector, :checkpoint_store, :update_logger, :failure_tracker
15
+
16
+ def initialize(
17
+ policy:,
18
+ version_detector: nil,
19
+ checkpoint_store: nil,
20
+ update_logger: nil,
21
+ failure_tracker: nil,
22
+ project_dir: Dir.pwd
23
+ )
24
+ @policy = policy
25
+ @project_dir = project_dir
26
+
27
+ # Use provided instances or create defaults
28
+ @version_detector = version_detector || VersionDetector.new(policy: policy)
29
+ @checkpoint_store = checkpoint_store || CheckpointStore.new(project_dir: project_dir)
30
+ @update_logger = update_logger || UpdateLogger.new(project_dir: project_dir)
31
+ @failure_tracker = failure_tracker || FailureTracker.new(
32
+ project_dir: project_dir,
33
+ max_failures: policy.max_consecutive_failures
34
+ )
35
+ end
36
+
37
+ # Create coordinator from configuration
38
+ # @param config [Hash] Auto-update configuration from aidp.yml
39
+ # @param project_dir [String] Project root directory
40
+ # @return [Coordinator]
41
+ def self.from_config(config, project_dir: Dir.pwd)
42
+ policy = UpdatePolicy.from_config(config)
43
+ new(policy: policy, project_dir: project_dir)
44
+ end
45
+
46
+ # Check if update is available and allowed
47
+ # @return [UpdateCheck] Update check result
48
+ def check_for_update
49
+ return UpdateCheck.unavailable unless @policy.enabled
50
+
51
+ update_check = @version_detector.check_for_update
52
+ @update_logger.log_check(update_check)
53
+ update_check
54
+ rescue => e
55
+ Aidp.log_error("auto_update_coordinator", "check_failed",
56
+ error: e.message)
57
+ UpdateCheck.failed(e.message)
58
+ end
59
+
60
+ # Initiate update process (checkpoint + exit with code 75)
61
+ # @param current_state [Hash] Current application state (from Watch::Runner)
62
+ # @return [void] (exits process with code 75)
63
+ # @raise [UpdateError] If updates are disabled or preconditions not met
64
+ # @raise [UpdateLoopError] If too many consecutive failures
65
+ def initiate_update(current_state)
66
+ raise UpdateError, "Updates disabled by configuration" unless @policy.enabled
67
+
68
+ # Check for restart loops
69
+ if @failure_tracker.too_many_failures?
70
+ @update_logger.log_restart_loop(@failure_tracker.failure_count)
71
+ raise UpdateLoopError, "Too many consecutive update failures (#{@failure_tracker.failure_count}/#{@policy.max_consecutive_failures})"
72
+ end
73
+
74
+ # Verify supervisor is configured
75
+ unless @policy.supervised?
76
+ raise UpdateError, "No supervisor configured. Set auto_update.supervisor in aidp.yml"
77
+ end
78
+
79
+ # Get latest version to record in checkpoint
80
+ update_check = check_for_update
81
+
82
+ unless update_check.should_update?
83
+ Aidp.log_info("auto_update_coordinator", "no_update_needed",
84
+ reason: update_check.policy_reason)
85
+ return
86
+ end
87
+
88
+ # Create checkpoint from current state
89
+ checkpoint = build_checkpoint(current_state, update_check.available_version)
90
+
91
+ # Save checkpoint
92
+ unless @checkpoint_store.save_checkpoint(checkpoint)
93
+ raise UpdateError, "Failed to save checkpoint"
94
+ end
95
+
96
+ # Log update initiation
97
+ @update_logger.log_update_initiated(checkpoint, target_version: update_check.available_version)
98
+
99
+ Aidp.log_info("auto_update_coordinator", "exiting_for_update",
100
+ from_version: update_check.current_version,
101
+ to_version: update_check.available_version,
102
+ checkpoint_id: checkpoint.checkpoint_id)
103
+
104
+ # Exit with special code 75 to signal supervisor to update
105
+ exit(75)
106
+ rescue UpdateError, UpdateLoopError
107
+ # Re-raise domain errors
108
+ raise
109
+ rescue => e
110
+ @failure_tracker.record_failure
111
+ @update_logger.log_failure(e.message)
112
+ Aidp.log_error("auto_update_coordinator", "initiate_failed",
113
+ error: e.message)
114
+ raise UpdateError, "Update initiation failed: #{e.message}"
115
+ end
116
+
117
+ # Restore from checkpoint after update
118
+ # @return [Checkpoint, nil] Restored checkpoint or nil
119
+ def restore_from_checkpoint
120
+ checkpoint = @checkpoint_store.latest_checkpoint
121
+ return nil unless checkpoint
122
+
123
+ Aidp.log_info("auto_update_coordinator", "restoring_checkpoint",
124
+ id: checkpoint.checkpoint_id,
125
+ created_at: checkpoint.created_at.iso8601)
126
+
127
+ # Validate checkpoint
128
+ unless checkpoint.valid?
129
+ Aidp.log_error("auto_update_coordinator", "invalid_checkpoint",
130
+ id: checkpoint.checkpoint_id,
131
+ reason: "Checksum validation failed")
132
+ @failure_tracker.record_failure
133
+ @update_logger.log_failure("Invalid checkpoint checksum", checkpoint_id: checkpoint.checkpoint_id)
134
+ return nil
135
+ end
136
+
137
+ # Check version compatibility
138
+ unless checkpoint.compatible_version?
139
+ Aidp.log_warn("auto_update_coordinator", "incompatible_version",
140
+ checkpoint_version: checkpoint.aidp_version,
141
+ current_version: Aidp::VERSION)
142
+ @failure_tracker.record_failure
143
+ @update_logger.log_failure(
144
+ "Incompatible version: checkpoint from #{checkpoint.aidp_version}, current #{Aidp::VERSION}",
145
+ checkpoint_id: checkpoint.checkpoint_id
146
+ )
147
+ return nil
148
+ end
149
+
150
+ # Log successful restoration
151
+ @update_logger.log_restore(checkpoint)
152
+ @update_logger.log_success(
153
+ from_version: checkpoint.aidp_version,
154
+ to_version: Aidp::VERSION
155
+ )
156
+
157
+ # Reset failure tracker on success
158
+ @failure_tracker.reset_on_success
159
+
160
+ # Delete checkpoint after successful restore
161
+ @checkpoint_store.delete_checkpoint(checkpoint.checkpoint_id)
162
+
163
+ checkpoint
164
+ rescue => e
165
+ @failure_tracker.record_failure
166
+ @update_logger.log_failure("Checkpoint restore failed: #{e.message}")
167
+ Aidp.log_error("auto_update_coordinator", "restore_failed",
168
+ error: e.message)
169
+ nil
170
+ end
171
+
172
+ # Get status summary for CLI display
173
+ # @return [Hash] Status information
174
+ def status
175
+ update_check = check_for_update
176
+
177
+ {
178
+ enabled: @policy.enabled,
179
+ policy: @policy.policy,
180
+ supervisor: @policy.supervisor,
181
+ current_version: Aidp::VERSION,
182
+ available_version: update_check.available_version,
183
+ update_available: update_check.update_available,
184
+ update_allowed: update_check.update_allowed,
185
+ policy_reason: update_check.policy_reason,
186
+ failure_tracker: @failure_tracker.status,
187
+ recent_updates: @update_logger.recent_entries(limit: 5)
188
+ }
189
+ end
190
+
191
+ private
192
+
193
+ def build_checkpoint(current_state, target_version)
194
+ Checkpoint.new(
195
+ mode: current_state[:mode] || "watch",
196
+ watch_state: current_state[:watch_state]
197
+ ).tap do |cp|
198
+ # Store target version in metadata for logging
199
+ cp.instance_variable_set(:@target_version, target_version)
200
+ end
201
+ end
202
+ end
203
+ end
204
+ end
@@ -0,0 +1,17 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Aidp
4
+ module AutoUpdate
5
+ # Base error for all auto-update errors
6
+ class UpdateError < StandardError; end
7
+
8
+ # Error raised when too many consecutive update failures detected
9
+ class UpdateLoopError < UpdateError; end
10
+
11
+ # Error raised when checkpoint is invalid or corrupted
12
+ class CheckpointError < UpdateError; end
13
+
14
+ # Error raised when version policy prevents update
15
+ class VersionPolicyError < UpdateError; end
16
+ end
17
+ end
@@ -0,0 +1,162 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "fileutils"
4
+ require "json"
5
+ require "time"
6
+ require_relative "../safe_directory"
7
+
8
+ module Aidp
9
+ module AutoUpdate
10
+ # Service for tracking update failures to prevent restart loops
11
+ class FailureTracker
12
+ include Aidp::SafeDirectory
13
+
14
+ attr_reader :state_file, :max_failures
15
+
16
+ def initialize(project_dir: Dir.pwd, max_failures: 3)
17
+ @project_dir = project_dir
18
+ state_dir = File.join(project_dir, ".aidp")
19
+ actual_dir = safe_mkdir_p(state_dir, component_name: "FailureTracker")
20
+ @state_file = File.join(actual_dir, "auto_update_failures.json")
21
+ @max_failures = max_failures
22
+ @state = load_state
23
+ end
24
+
25
+ # Record a failure
26
+ def record_failure
27
+ @state[:failures] << {
28
+ timestamp: Time.now.utc.iso8601,
29
+ version: Aidp::VERSION
30
+ }
31
+
32
+ # Keep only recent failures (last hour)
33
+ @state[:failures].select! { |f|
34
+ Time.parse(f[:timestamp]) > Time.now - 3600
35
+ }
36
+
37
+ save_state
38
+
39
+ Aidp.log_warn("failure_tracker", "failure_recorded",
40
+ total_failures: @state[:failures].size,
41
+ max_failures: @max_failures)
42
+ rescue => e
43
+ Aidp.log_error("failure_tracker", "record_failure_failed",
44
+ error: e.message)
45
+ end
46
+
47
+ # Check if too many consecutive failures have occurred
48
+ # @return [Boolean]
49
+ def too_many_failures?
50
+ failure_count = @state[:failures].size
51
+ is_looping = failure_count >= @max_failures
52
+
53
+ if is_looping
54
+ Aidp.log_error("failure_tracker", "restart_loop_detected",
55
+ failure_count: failure_count,
56
+ max_failures: @max_failures)
57
+ end
58
+
59
+ is_looping
60
+ end
61
+
62
+ # Reset failure count after successful operation
63
+ def reset_on_success
64
+ previous_failures = @state[:failures].size
65
+
66
+ @state[:failures] = []
67
+ @state[:last_success] = Time.now.utc.iso8601
68
+ @state[:last_success_version] = Aidp::VERSION
69
+
70
+ save_state
71
+
72
+ Aidp.log_info("failure_tracker", "reset_on_success",
73
+ previous_failures: previous_failures,
74
+ version: Aidp::VERSION)
75
+ rescue => e
76
+ Aidp.log_error("failure_tracker", "reset_failed",
77
+ error: e.message)
78
+ end
79
+
80
+ # Get current failure count
81
+ # @return [Integer]
82
+ def failure_count
83
+ @state[:failures].size
84
+ end
85
+
86
+ # Get time since last success
87
+ # @return [Integer, nil] Seconds since last success, or nil if never successful
88
+ def time_since_last_success
89
+ return nil unless @state[:last_success]
90
+
91
+ Time.now - Time.parse(@state[:last_success])
92
+ rescue => e
93
+ Aidp.log_error("failure_tracker", "time_calculation_failed",
94
+ error: e.message)
95
+ nil
96
+ end
97
+
98
+ # Get all failure timestamps
99
+ # @return [Array<Time>]
100
+ def failure_timestamps
101
+ @state[:failures].map { |f| Time.parse(f[:timestamp]) }
102
+ rescue => e
103
+ Aidp.log_error("failure_tracker", "timestamp_parsing_failed",
104
+ error: e.message)
105
+ []
106
+ end
107
+
108
+ # Manually reset failures (for CLI command or recovery)
109
+ def force_reset
110
+ Aidp.log_warn("failure_tracker", "manual_reset_triggered",
111
+ previous_failures: @state[:failures].size)
112
+
113
+ @state[:failures] = []
114
+ save_state
115
+ end
116
+
117
+ # Get state summary for status display
118
+ # @return [Hash]
119
+ def status
120
+ {
121
+ failure_count: failure_count,
122
+ max_failures: @max_failures,
123
+ too_many_failures: too_many_failures?,
124
+ last_success: @state[:last_success],
125
+ last_success_version: @state[:last_success_version],
126
+ recent_failures: @state[:failures]
127
+ }
128
+ end
129
+
130
+ private
131
+
132
+ def load_state
133
+ return default_state unless File.exist?(@state_file)
134
+
135
+ JSON.parse(File.read(@state_file), symbolize_names: true)
136
+ rescue JSON::ParserError => e
137
+ Aidp.log_warn("failure_tracker", "state_file_corrupted",
138
+ error: e.message)
139
+ default_state
140
+ rescue => e
141
+ Aidp.log_warn("failure_tracker", "load_state_failed",
142
+ error: e.message)
143
+ default_state
144
+ end
145
+
146
+ def save_state
147
+ File.write(@state_file, JSON.pretty_generate(@state))
148
+ rescue => e
149
+ Aidp.log_error("failure_tracker", "save_state_failed",
150
+ error: e.message)
151
+ end
152
+
153
+ def default_state
154
+ {
155
+ failures: [],
156
+ last_success: nil,
157
+ last_success_version: nil
158
+ }
159
+ end
160
+ end
161
+ end
162
+ end
@@ -0,0 +1,95 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "net/http"
4
+ require "json"
5
+ require "uri"
6
+
7
+ module Aidp
8
+ module AutoUpdate
9
+ # Adapter for querying gem versions via RubyGems API (fallback)
10
+ class RubyGemsAPIAdapter
11
+ RUBYGEMS_API_BASE = "https://rubygems.org/api/v1"
12
+ TIMEOUT_SECONDS = 5
13
+
14
+ # Get the latest version of a gem from RubyGems API
15
+ # @param gem_name [String] Name of the gem
16
+ # @param allow_prerelease [Boolean] Whether to allow prerelease versions
17
+ # @return [Gem::Version, nil] Latest version or nil if unavailable
18
+ def latest_version_for(gem_name, allow_prerelease: false)
19
+ Aidp.log_debug("rubygems_api", "checking_gem_version",
20
+ gem: gem_name,
21
+ allow_prerelease: allow_prerelease)
22
+
23
+ uri = URI.parse("#{RUBYGEMS_API_BASE}/gems/#{gem_name}.json")
24
+ response = fetch_with_timeout(uri)
25
+
26
+ return nil unless response.is_a?(Net::HTTPSuccess)
27
+
28
+ data = JSON.parse(response.body)
29
+ version_string = data["version"]
30
+
31
+ if version_string
32
+ version = Gem::Version.new(version_string)
33
+
34
+ # Filter out prerelease if not allowed
35
+ if !allow_prerelease && version.prerelease?
36
+ Aidp.log_debug("rubygems_api", "skipping_prerelease",
37
+ gem: gem_name,
38
+ version: version_string)
39
+ return nil
40
+ end
41
+
42
+ Aidp.log_debug("rubygems_api", "found_version",
43
+ gem: gem_name,
44
+ version: version_string)
45
+ version
46
+ else
47
+ Aidp.log_debug("rubygems_api", "no_version_in_response",
48
+ gem: gem_name)
49
+ nil
50
+ end
51
+ rescue JSON::ParserError => e
52
+ Aidp.log_error("rubygems_api", "json_parse_failed",
53
+ gem: gem_name,
54
+ error: e.message)
55
+ nil
56
+ rescue ArgumentError => e
57
+ Aidp.log_error("rubygems_api", "invalid_version",
58
+ gem: gem_name,
59
+ error: e.message)
60
+ nil
61
+ rescue => e
62
+ Aidp.log_error("rubygems_api", "api_request_failed",
63
+ gem: gem_name,
64
+ error: e.message)
65
+ nil
66
+ end
67
+
68
+ private
69
+
70
+ def fetch_with_timeout(uri)
71
+ Net::HTTP.start(
72
+ uri.host,
73
+ uri.port,
74
+ use_ssl: uri.scheme == "https",
75
+ open_timeout: TIMEOUT_SECONDS,
76
+ read_timeout: TIMEOUT_SECONDS
77
+ ) do |http|
78
+ request = Net::HTTP::Get.new(uri)
79
+ request["User-Agent"] = "Aidp/#{Aidp::VERSION}"
80
+ http.request(request)
81
+ end
82
+ rescue Timeout::Error, Errno::ETIMEDOUT
83
+ Aidp.log_warn("rubygems_api", "request_timeout",
84
+ uri: uri.to_s,
85
+ timeout: TIMEOUT_SECONDS)
86
+ nil
87
+ rescue SocketError, Errno::ECONNREFUSED => e
88
+ Aidp.log_warn("rubygems_api", "connection_failed",
89
+ uri: uri.to_s,
90
+ error: e.message)
91
+ nil
92
+ end
93
+ end
94
+ end
95
+ end
@@ -0,0 +1,106 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "time"
4
+
5
+ module Aidp
6
+ module AutoUpdate
7
+ # Value object representing the result of an update check
8
+ class UpdateCheck
9
+ attr_reader :current_version, :available_version, :update_available,
10
+ :update_allowed, :policy_reason, :checked_at, :error
11
+
12
+ def initialize(
13
+ current_version:,
14
+ available_version:,
15
+ update_available:,
16
+ update_allowed:,
17
+ policy_reason: nil,
18
+ checked_at: Time.now,
19
+ error: nil
20
+ )
21
+ @current_version = current_version
22
+ @available_version = available_version
23
+ @update_available = update_available
24
+ @update_allowed = update_allowed
25
+ @policy_reason = policy_reason
26
+ @checked_at = checked_at
27
+ @error = error
28
+ end
29
+
30
+ # Create a failed update check
31
+ # @param error_message [String] Error message
32
+ # @param current_version [String] Current version
33
+ # @return [UpdateCheck]
34
+ def self.failed(error_message, current_version: Aidp::VERSION)
35
+ new(
36
+ current_version: current_version,
37
+ available_version: current_version,
38
+ update_available: false,
39
+ update_allowed: false,
40
+ policy_reason: "Update check failed",
41
+ error: error_message
42
+ )
43
+ end
44
+
45
+ # Create an unavailable update check (service temporarily unavailable)
46
+ # @param current_version [String] Current version
47
+ # @return [UpdateCheck]
48
+ def self.unavailable(current_version: Aidp::VERSION)
49
+ new(
50
+ current_version: current_version,
51
+ available_version: current_version,
52
+ update_available: false,
53
+ update_allowed: false,
54
+ policy_reason: "Update service temporarily unavailable"
55
+ )
56
+ end
57
+
58
+ # Check if update check was successful
59
+ # @return [Boolean]
60
+ def success?
61
+ @error.nil?
62
+ end
63
+
64
+ # Check if update check failed
65
+ # @return [Boolean]
66
+ def failed?
67
+ !success?
68
+ end
69
+
70
+ # Check if update should be performed
71
+ # @return [Boolean]
72
+ def should_update?
73
+ success? && @update_available && @update_allowed
74
+ end
75
+
76
+ # Convert to hash for serialization
77
+ # @return [Hash]
78
+ def to_h
79
+ {
80
+ current_version: @current_version,
81
+ available_version: @available_version,
82
+ update_available: @update_available,
83
+ update_allowed: @update_allowed,
84
+ policy_reason: @policy_reason,
85
+ checked_at: @checked_at.utc.iso8601,
86
+ error: @error
87
+ }
88
+ end
89
+
90
+ # Create from hash
91
+ # @param hash [Hash] Serialized update check
92
+ # @return [UpdateCheck]
93
+ def self.from_h(hash)
94
+ new(
95
+ current_version: hash[:current_version] || hash["current_version"],
96
+ available_version: hash[:available_version] || hash["available_version"],
97
+ update_available: hash[:update_available] || hash["update_available"],
98
+ update_allowed: hash[:update_allowed] || hash["update_allowed"],
99
+ policy_reason: hash[:policy_reason] || hash["policy_reason"],
100
+ checked_at: Time.parse(hash[:checked_at] || hash["checked_at"]),
101
+ error: hash[:error] || hash["error"]
102
+ )
103
+ end
104
+ end
105
+ end
106
+ end