@guanlin4924/devflow-pi 0.1.83 → 0.1.85

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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@guanlin4924/devflow-pi",
3
- "version": "0.1.83",
3
+ "version": "0.1.85",
4
4
  "type": "module",
5
5
  "description": "Devflow 专项研发 Pi,基于固定版本 OMP Runtime 组装研发状态机、代码智能、设计、实现与质量门禁。",
6
6
  "license": "UNLICENSED",
@@ -968,7 +968,9 @@ export function workflowResumeDisposition(snapshot: Record<string, unknown>): Wo
968
968
 
969
969
  const feature = typeof snapshot.current_feature === "string" ? snapshot.current_feature : "";
970
970
  const revision = snapshot.revision;
971
- const selectedStep = compactStep(snapshot.next_step) ?? compactStep(snapshot.current_step);
971
+ const selectedStep = snapshot.action === "run_parallel_window"
972
+ ? compactStep(snapshot.next_step) ?? compactStep(snapshot.current_step)
973
+ : compactStep(snapshot.current_step) ?? compactStep(snapshot.next_step);
972
974
  const stepId = typeof selectedStep?.id === "string" ? selectedStep.id : "";
973
975
  if (!feature || !stepId || typeof revision !== "number" || !Number.isInteger(revision)) return "idle";
974
976
 
@@ -985,7 +987,9 @@ export function workflowResumeDisposition(snapshot: Record<string, unknown>): Wo
985
987
  }
986
988
 
987
989
  function workflowDirective(snapshot: Record<string, unknown>, source: "start" | "approval", requirementDraft?: string): string {
988
- const selectedStep = compactStep(snapshot.next_step) ?? compactStep(snapshot.current_step);
990
+ const selectedStep = snapshot.action === "run_parallel_window"
991
+ ? compactStep(snapshot.next_step) ?? compactStep(snapshot.current_step)
992
+ : compactStep(snapshot.current_step) ?? compactStep(snapshot.next_step);
989
993
  const envelope = {
990
994
  source,
991
995
  action: snapshot.action,
@@ -15,6 +15,12 @@ POLICY = {
15
15
  "requirement-baseline" => { "checkers" => %w[requirement-graph-check business-closure-check slice-independence-check acceptance-coverage-check change-impact-check], "agents" => [], "budget" => 0, "allowed_graph_delta" => %w[requirement rule boundary slice acceptance] },
16
16
  "api-contract" => { "checkers" => %w[requirement-graph-check contract-coverage-check api-test-schema-check change-impact-check], "agents" => [], "budget" => 0, "allowed_graph_delta" => %w[api] },
17
17
  "architecture-feasibility" => { "checkers" => %w[requirement-graph-check change-impact-check], "agents" => [], "budget" => 0, "allowed_graph_delta" => %w[workload_shape workload_constraint decision risk metric read_model] },
18
+ "backend-tech-spec" => { "checkers" => %w[requirement-graph-check change-impact-check], "agents" => ["implementation-worker"], "budget" => 1, "allowed_graph_delta" => %w[decision risk] },
19
+ "api-call-guide" => { "checkers" => %w[requirement-graph-check contract-coverage-check], "agents" => ["implementation-worker"], "budget" => 1, "allowed_graph_delta" => %w[api decision] },
20
+ "ui-spec" => { "checkers" => %w[requirement-graph-check change-impact-check], "agents" => ["implementation-worker"], "budget" => 1, "allowed_graph_delta" => %w[decision risk] },
21
+ "feature-breakdown" => { "checkers" => %w[requirement-graph-check], "agents" => ["implementation-worker"], "budget" => 1, "allowed_graph_delta" => %w[requirement rule] },
22
+ "frontend-tech-spec" => { "checkers" => %w[requirement-graph-check change-impact-check], "agents" => ["implementation-worker"], "budget" => 1, "allowed_graph_delta" => %w[decision risk] },
23
+ "api-supplement" => { "checkers" => %w[requirement-graph-check contract-coverage-check], "agents" => ["implementation-worker"], "budget" => 1, "allowed_graph_delta" => %w[api decision] },
18
24
  "integration-verify" => { "checkers" => %w[requirement-graph-check slice-independence-check contract-coverage-check change-impact-check cross-track-consistency-check business-claim-reference-check business-cross-model-check], "agents" => ["verification-runner"], "budget" => 1, "allowed_graph_delta" => %w[requirement risk decision] },
19
25
  "acceptance-test-design" => { "checkers" => %w[requirement-graph-check contract-coverage-check acceptance-coverage-check api-test-matrix-check test-coverage-check test-obligation-check change-impact-check], "agents" => [], "budget" => 0, "allowed_graph_delta" => %w[test_case] },
20
26
  "change-plan" => { "checkers" => %w[requirement-graph-check slice-independence-check contract-coverage-check api-test-matrix-check test-coverage-check test-obligation-check test-task-binding-check change-impact-check], "agents" => [], "budget" => 0, "allowed_graph_delta" => %w[acceptance] },
@@ -45,6 +51,27 @@ def interpolate(value, state)
45
51
  value.gsub("{feature}", state.fetch("current_feature").to_s).gsub("{phase}", state.fetch("current_phase").to_s)
46
52
  end
47
53
 
54
+ def render_execution_contract_markdown(contract)
55
+ lines = [
56
+ "# Execution Contract",
57
+ "",
58
+ "> Core-generated derived view. The immutable contract JSON under `.planning/contracts/` is authoritative.",
59
+ "",
60
+ "- Contract ID: `#{contract.fetch("contract_id")}`",
61
+ "- Feature: `#{contract.fetch("feature")}`",
62
+ "- Step: `#{contract.fetch("step")}`",
63
+ "- STATE revision: `#{contract.fetch("state_revision")}`",
64
+ "- Requirement graph revision: `#{contract.fetch("graph_revision")}`",
65
+ "- Frozen hash: `#{contract.fetch("frozen_hash")}`",
66
+ "",
67
+ "## Allowed paths",
68
+ "",
69
+ ]
70
+ lines.concat(Array(contract["allowed_paths"]).map { |path| "- `#{path}`" })
71
+ lines.concat(["", "## Allowed agents", "", *Array(contract["allowed_agents"]).map { |agent| "- `#{agent}`" }, "", "## Checkers", "", *Array(contract["checker_ids"]).map { |checker| "- `#{checker}`" }, "", "## Allowed tools", "", *Array(contract["allowed_tools"]).map { |tool| "- `#{tool}`" }, ""])
72
+ lines.join("\n")
73
+ end
74
+
48
75
  root = Dir.pwd
49
76
  lock = %w[create inspect].include?(command) ? DevflowControlIO.acquire_project_lock(root) : nil
50
77
  begin
@@ -92,6 +119,7 @@ begin
92
119
  raise ArgumentError, "--write is required" unless options[:write]
93
120
  destination = File.join(root, ".planning/contracts", options[:feature], "#{contract_id}.json")
94
121
  DevflowControlIO.atomic_write(destination, JSON.pretty_generate(frozen) + "\n")
122
+ DevflowControlIO.atomic_write(File.join(root, ".planning/execution-contract.md"), render_execution_contract_markdown(frozen) + "\n")
95
123
  audit = File.join(root, ".planning/contracts", options[:feature], "control-events.jsonl")
96
124
  existing = File.exist?(audit) ? File.read(audit) : ""
97
125
  DevflowControlIO.atomic_write(audit, existing + JSON.generate({ "event" => "execution_contract_frozen", "contract_id" => contract_id, "state_revision" => state["revision"], "graph_revision" => graph["revision"], "frozen_hash" => frozen["frozen_hash"] }) + "\n")
@@ -212,6 +212,10 @@ def track_boundary(state, pending_ids, completed_ids)
212
212
  end
213
213
 
214
214
  def active_parallel_window(workflow, state, root, step_by_id, completed_ids, pending_ids, artifact_by_path)
215
+ # A failed/unsupported parallel dispatch may be explicitly converted to the
216
+ # declared serial fallback. Keep the join topology in the workflow, but do
217
+ # not re-enter the same parallel window on every inspection.
218
+ return nil if state["parallel_execution_mode"].to_s == "serial_fallback"
215
219
  Array(workflow["parallel_windows"]).each do |window|
216
220
  after_step = window["after_step"]
217
221
  branch_map = window["branches"].is_a?(Hash) ? window["branches"] : {}
@@ -206,7 +206,7 @@ if $PROGRAM_NAME == __FILE__
206
206
  begin
207
207
  options = { action: nil, feature: nil, state_revision: nil, run_revision: nil, branch_id: nil, status: nil, workflow_window: nil, branches: [], state_path: ".planning/STATE.md", json: false }
208
208
  OptionParser.new do |opts|
209
- opts.on("--action ACTION", %w[inspect prepare update retry rebase]) { |value| options[:action] = value }
209
+ opts.on("--action ACTION", %w[inspect prepare update retry rebase serial-fallback]) { |value| options[:action] = value }
210
210
  opts.on("--feature FEATURE") { |value| options[:feature] = value }
211
211
  opts.on("--state-revision REV", Integer) { |value| options[:state_revision] = value }
212
212
  opts.on("--run-revision REV", Integer) { |value| options[:run_revision] = value }
@@ -214,6 +214,8 @@ if $PROGRAM_NAME == __FILE__
214
214
  opts.on("--status STATUS") { |value| options[:status] = value }
215
215
  opts.on("--workflow-window NAME") { |value| options[:workflow_window] = value }
216
216
  opts.on("--state PATH") { |value| options[:state_path] = value }
217
+ opts.on("--step STEP") { |value| options[:serial_step] = value }
218
+ opts.on("--expected-state-revision REV", Integer) { |value| options[:expected_state_revision] = value }
217
219
  opts.on("--branch STEP:TRACK", "Register a branch as workflow_step:track") do |value|
218
220
  workflow_step, track = value.split(":", 2)
219
221
  raise ArgumentError, "--branch must be WORKFLOW_STEP:TRACK" if workflow_step.to_s.empty? || track.to_s.empty?
@@ -233,7 +235,55 @@ if $PROGRAM_NAME == __FILE__
233
235
  ParallelRun.update_branch(root: Dir.pwd, feature: options[:feature], branch_id: options[:branch_id], expected_revision: options[:run_revision], status: options[:status])
234
236
  when "retry"
235
237
  ParallelRun.retry_branch(root: Dir.pwd, feature: options[:feature], branch_id: options[:branch_id], expected_revision: options[:run_revision])
236
- when "rebase"
238
+ when "serial-fallback"
239
+ root = Dir.pwd
240
+ feature = ParallelRun.safe_feature(options[:feature])
241
+ lock = DevflowControlIO.acquire_project_lock(root)
242
+ begin
243
+ record = ParallelRun.load(root, feature) || raise(ArgumentError, "parallel run not found")
244
+ state_path, = DevflowControlIO.resolve_control_path(root, options[:state_path], :state)
245
+ state_text = File.read(state_path)
246
+ front = state_text.match(/\A---\n(.*?)\n---\n/m)&.[](1)
247
+ raise ArgumentError, "STATE.md has no YAML front matter" unless front
248
+ state = YAML.safe_load(front, permitted_classes: [], aliases: false)
249
+ raise ArgumentError, "STATE feature mismatch" unless state["current_feature"].to_s == feature
250
+ raise ArgumentError, "STATE does not reference this parallel run" unless state["parallel_run_ref"].to_s == record["run_id"].to_s
251
+ actual_revision = Integer(state.fetch("revision"))
252
+ if options[:expected_state_revision] && actual_revision != options[:expected_state_revision]
253
+ raise ArgumentError, "STATE revision conflict"
254
+ end
255
+ branches = Array(record["branches"])
256
+ raise ArgumentError, "serial fallback is only safe before any branch succeeds or runs" if branches.any? { |branch| %w[running succeeded].include?(branch["status"].to_s) }
257
+ workflow_path, = DevflowControlIO.resolve_control_path(root, state.fetch("workflow_config"), :workflow)
258
+ workflow = YAML.safe_load(File.read(workflow_path), permitted_classes: [], aliases: false)
259
+ policy = Array(workflow["parallel_windows"]).find { |window| window.is_a?(Hash) && window["id"].to_s == record["workflow_window"].to_s }&.dig("execution_policy", "serial_fallback_order")
260
+ order = Array(policy).map(&:to_s)
261
+ selected = options[:serial_step].to_s
262
+ selected = order.find do |step_id|
263
+ branch = branches.find { |item| item["workflow_step"].to_s == step_id }
264
+ branch && branch["status"].to_s == "pending"
265
+ end if selected.empty?
266
+ raise ArgumentError, "serial fallback has no pending branch" if selected.to_s.empty?
267
+ raise ArgumentError, "serial fallback step is not a pending branch" unless branches.any? { |branch| branch["workflow_step"].to_s == selected && branch["status"].to_s == "pending" }
268
+
269
+ state.delete("parallel_run_ref")
270
+ state["parallel_execution_mode"] = "serial_fallback"
271
+ state["current_step"] = selected
272
+ state["revision"] = actual_revision + 1
273
+ state["state_updated_at"] = Date.today.to_s
274
+ body = state_text.sub(/\A---\n.*?\n---\n/m, "---\n#{state.to_yaml.sub(/\A---\n/, "")}---\n")
275
+ DevflowControlIO.atomic_write(state_path, body)
276
+ record["status"] = "abandoned_serial_fallback"
277
+ record["abandoned_at"] = Time.now.utc.iso8601
278
+ record["abandon_reason"] = "no child Agent lifecycle started; converted to declared serial_fallback_order"
279
+ record["serial_fallback_step"] = selected
280
+ record["updated_at"] = Time.now.utc.iso8601
281
+ ParallelRun.write(root, feature, record)
282
+ result = { "ok" => true, "state_revision" => state["revision"], "serial_step" => selected, "run" => record }
283
+ ensure
284
+ DevflowControlIO.release_lock(lock)
285
+ end
286
+ when "rebase"
237
287
  raise ArgumentError, "--state-revision is required" if options[:state_revision].nil?
238
288
  ParallelRun.rebase_state_revision(root: Dir.pwd, feature: options[:feature], state_revision: options[:state_revision])
239
289
  .then { |run| run ? { "ok" => true, "run" => run } : { "ok" => false, "reason" => "parallel_run_not_found" } }
@@ -175,7 +175,13 @@ unless %w[prepared claimed consumed superseded failed].include?(status)
175
175
  return nil
176
176
  end
177
177
 
178
- YAML.safe_load(File.read(path), permitted_classes: [], aliases: false)
178
+ raw = File.read(path)
179
+ if code == "workflow_config"
180
+ WorkflowGateContract.duplicate_yaml_keys(raw).each do |key_path|
181
+ issues << issue("error", "duplicate_workflow_yaml_key", "Workflow contains a duplicate YAML mapping key", path: key_path)
182
+ end
183
+ end
184
+ YAML.safe_load(raw, permitted_classes: [], aliases: false)
179
185
  rescue Psych::SyntaxError => e
180
186
  issues << issue("error", "invalid_#{code}", e.message, path: path)
181
187
  nil
@@ -14,6 +14,31 @@ module WorkflowGateContract
14
14
 
15
15
  module_function
16
16
 
17
+ # Psych silently keeps the last value for duplicate mapping keys. Detect
18
+ # duplicates before loading a workflow so a checker declaration can never be
19
+ # overwritten without a gate failure.
20
+ def duplicate_yaml_keys(yaml)
21
+ duplicates = []
22
+ walk = lambda do |node, path|
23
+ return unless node
24
+ if node.is_a?(Psych::Nodes::Mapping)
25
+ seen = {}
26
+ children = node.children
27
+ children.each_slice(2) do |key_node, value_node|
28
+ key = key_node.respond_to?(:value) ? key_node.value.to_s : key_node.to_s
29
+ key_path = path.empty? ? key : "#{path}.#{key}"
30
+ duplicates << key_path if seen.key?(key)
31
+ seen[key] = true
32
+ walk.call(value_node, key_path)
33
+ end
34
+ elsif node.respond_to?(:children)
35
+ Array(node.children).each { |child| walk.call(child, path) }
36
+ end
37
+ end
38
+ Psych.parse_stream(yaml).children.each { |document| walk.call(document.root, "") }
39
+ duplicates.uniq
40
+ end
41
+
17
42
  def validate(workflow)
18
43
  errors = []
19
44
  unless workflow.is_a?(Hash)
@@ -139,7 +164,11 @@ if $PROGRAM_NAME == __FILE__
139
164
  opts.on("--json") { options[:json] = true }
140
165
  end.parse!
141
166
  raise ArgumentError, "--workflow is required" unless options[:workflow]
142
- result = WorkflowGateContract.validate(YAML.safe_load(File.read(options[:workflow]), permitted_classes: [], aliases: false))
167
+ raw = File.read(options[:workflow])
168
+ duplicate_errors = WorkflowGateContract.duplicate_yaml_keys(raw).map do |path|
169
+ WorkflowGateContract.error("DUPLICATE_YAML_KEY", "workflow contains a duplicate YAML mapping key", path: path)
170
+ end
171
+ result = duplicate_errors + WorkflowGateContract.validate(YAML.safe_load(raw, permitted_classes: [], aliases: false))
143
172
  puts(options[:json] ? JSON.generate(result) : YAML.dump(result))
144
173
  rescue StandardError => e
145
174
  warn e.message
@@ -70,10 +70,7 @@ governance:
70
70
  - api-test-matrix-check
71
71
  - test-coverage-check
72
72
  - acceptance-test-results-check
73
- - change-impact-check
74
- run-acceptance-tests:
75
73
  - acceptance-coverage-check
76
- - acceptance-test-results-check
77
74
  - change-impact-check
78
75
 
79
76
  artifact_registry:
package/src/constants.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  export const PRODUCT_NAME = "Devflow Pi";
2
- export const PRODUCT_VERSION = "0.1.83";
2
+ export const PRODUCT_VERSION = "0.1.85";
3
3
  export const PROJECT_SCHEMA_VERSION = 1;
4
4
  export const OMP_RUNTIME_VERSION = "17.2.7";
5
5