@guanlin4924/devflow-pi 0.1.12 → 0.1.14

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.
@@ -4,6 +4,8 @@
4
4
  require "json"
5
5
  require "optparse"
6
6
  require "yaml"
7
+ require_relative "control_io"
8
+ require_relative "phase_ownership"
7
9
 
8
10
  module WorkflowStateValidator
9
11
  REQUIRED_STATE_FIELDS = %w[
@@ -18,12 +20,18 @@ module WorkflowStateValidator
18
20
 
19
21
  TERMINAL_ARTIFACT_STATUSES = %w[approved completed passed].freeze
20
22
  NON_TERMINAL_ARTIFACT_STATUSES = %w[planned generated pending_rerun stale failed].freeze
23
+ TERMINAL_WORKFLOW_STATUSES = %w[completed failed cancelled].freeze
21
24
 
22
25
  module_function
23
26
 
24
- def validate(root:, state_path:, state_override: nil, workflow_override: nil, artifact_registry_override: nil, strict_artifact_status: true, strict_registry_revision: true)
27
+ def validate(root:, state_path:, state_override: nil, workflow_override: nil, artifact_registry_override: nil, strict_artifact_status: true, strict_registry_revision: true, phase_owner_override: nil)
25
28
  root = File.expand_path(root)
26
- state_path = File.expand_path(state_path, root)
29
+ canonical_state_path, = DevflowControlIO.resolve_control_path(root, ".planning/STATE.md", :state)
30
+ unless File.expand_path(state_path, root) == canonical_state_path
31
+ issues = [issue("error", "invalid_control_path", "STATE.md path must be .planning/STATE.md")]
32
+ return result(false, state_path, nil, nil, issues)
33
+ end
34
+ state_path = canonical_state_path
27
35
  issues = []
28
36
 
29
37
  state = state_override || load_state(state_path, issues)
@@ -34,8 +42,14 @@ module WorkflowStateValidator
34
42
  end
35
43
  return result(false, state_path, nil, nil, issues) if issues.any? { |entry| entry["severity"] == "error" }
36
44
 
45
+ ownership = PhaseOwnership.validate_state(root: root, state: state, owner_override: phase_owner_override)
46
+ unless ownership["ok"]
47
+ issues << issue("error", ownership["reason"], ownership["message"], ownership.reject { |key, _| %w[ok reason message].include?(key) })
48
+ return result(false, state_path, nil, nil, issues)
49
+ end
50
+
37
51
  workflow_path = state["workflow_config"] || File.join(".planning", "workflows", "#{state.fetch("current_flow")}.yaml")
38
- abs_workflow_path = File.expand_path(workflow_path, root)
52
+ abs_workflow_path, workflow_path = DevflowControlIO.resolve_control_path(root, workflow_path, :workflow)
39
53
  workflow = workflow_override || load_yaml(abs_workflow_path, issues, "workflow_config")
40
54
  return result(false, state_path, workflow_path, nil, issues) unless workflow
41
55
 
@@ -103,7 +117,7 @@ module WorkflowStateValidator
103
117
 
104
118
  registry = registry_override
105
119
  unless registry
106
- abs_registry_path = File.expand_path(registry_path, root)
120
+ abs_registry_path, = DevflowControlIO.resolve_control_path(root, registry_path, :registry)
107
121
  unless File.exist?(abs_registry_path)
108
122
  issues << issue("error", "missing_artifact_registry", "Artifact registry does not exist", path: registry_path)
109
123
  return {}
@@ -121,7 +135,7 @@ module WorkflowStateValidator
121
135
  )
122
136
  end
123
137
  Array(registry["artifacts"]).to_h { |artifact| [artifact.fetch("path"), artifact] }
124
- rescue ArgumentError, TypeError, Psych::SyntaxError => e
138
+ rescue ArgumentError, TypeError, Psych::SyntaxError, DevflowControlIO::InvalidControlPath => e
125
139
  issues << issue("error", "invalid_artifact_registry", e.message, path: registry_path)
126
140
  {}
127
141
  end
@@ -145,8 +159,10 @@ module WorkflowStateValidator
145
159
  unknown.each do |step_id|
146
160
  issues << issue("error", "unknown_tracked_step", "Tracked step is not defined in the active workflow", step: step_id)
147
161
  end
148
- missing.each do |step_id|
149
- issues << issue("error", "missing_tracked_step", "Workflow step is missing from both completed_steps and pending_steps", step: step_id)
162
+ unless %w[failed cancelled].include?(state["status"])
163
+ missing.each do |step_id|
164
+ issues << issue("error", "missing_tracked_step", "Workflow step is missing from both completed_steps and pending_steps", step: step_id)
165
+ end
150
166
  end
151
167
 
152
168
  current_step = state["current_step"]
@@ -162,6 +178,10 @@ module WorkflowStateValidator
162
178
  missing_completed.each do |step_id|
163
179
  issues << issue("error", "completed_status_missing_step", "Workflow status is completed but a workflow step is not completed", step: step_id)
164
180
  end
181
+ elsif %w[failed cancelled].include?(state["status"])
182
+ if pending_ids.any?
183
+ issues << issue("error", "terminal_with_pending_steps", "Terminal workflow status must not retain pending steps", status: state["status"])
184
+ end
165
185
  elsif pending_ids.empty?
166
186
  issues << issue("error", "non_completed_without_pending_steps", "Workflow has no pending steps but status is not completed")
167
187
  end
@@ -589,7 +609,12 @@ if __FILE__ == $PROGRAM_NAME
589
609
  end.parse!
590
610
 
591
611
  root = Dir.pwd
592
- state_path = File.expand_path(options[:state], root)
612
+ begin
613
+ state_path, = DevflowControlIO.resolve_control_path(root, options[:state], :state)
614
+ rescue DevflowControlIO::InvalidControlPath => error
615
+ puts JSON.pretty_generate({ "ok" => false, "errors" => [{ "code" => "invalid_control_path", "message" => error.message }] })
616
+ exit 1
617
+ end
593
618
  validation = WorkflowStateValidator.validate(
594
619
  root: root,
595
620
  state_path: state_path,