@guanlin4924/devflow-pi 0.1.19 → 0.1.21
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/README.md +19 -0
- package/package.json +3 -3
- package/plugins/devflow-core/extensions/devflow-core.ts +152 -3
- package/plugins/devflow-core/skills/workflow-orchestrator/scripts/acceptance_test_runner.py +394 -0
- package/plugins/devflow-core/skills/workflow-orchestrator/scripts/acceptance_test_suite.rb +86 -0
- package/plugins/devflow-core/skills/workflow-orchestrator/scripts/advance_state.rb +192 -1
- package/plugins/devflow-core/skills/workflow-orchestrator/scripts/api_test_matrix.py +829 -0
- package/plugins/devflow-core/skills/workflow-orchestrator/scripts/execution_contract.rb +7 -5
- package/plugins/devflow-core/skills/workflow-orchestrator/scripts/init_state.rb +11 -1
- package/plugins/devflow-core/skills/workflow-orchestrator/scripts/inspect_state.rb +5 -1
- package/plugins/devflow-core/skills/workflow-orchestrator/scripts/machine_evidence.rb +39 -3
- package/plugins/devflow-core/skills/workflow-orchestrator/scripts/requirement_graph.rb +309 -23
- package/plugins/devflow-core/skills/workflow-orchestrator/scripts/tdd_evidence.py +311 -0
- package/plugins/devflow-core/skills/workflow-orchestrator/scripts/test_command_policy.py +53 -0
- package/plugins/devflow-core/skills/workflow-orchestrator/scripts/test_task_binding_check.py +254 -0
- package/plugins/devflow-core/skills/workflow-orchestrator/templates/feature-development.yaml +109 -4
- package/plugins/devflow-core/skills/workflow-orchestrator/templates/uat-remediation.yaml +21 -1
- package/plugins/devflow-core/skills/workflow-project-context/SKILL.md +17 -0
- package/plugins/devflow-design/skills/workflow-api-contract/SKILL.md +38 -0
- package/plugins/devflow-implementation/agents/implementation-worker.md +3 -0
- package/plugins/devflow-implementation/skills/workflow-change-plan/SKILL.md +7 -0
- package/plugins/devflow-implementation/skills/workflow-change-plan/references/output-template.md +2 -0
- package/plugins/devflow-implementation/skills/workflow-execute-phase/SKILL.md +3 -3
- package/plugins/devflow-quality/skills/workflow-acceptance-test-design/SKILL.md +63 -0
- package/plugins/devflow-quality/skills/workflow-acceptance-test-design/agents/openai.yaml +4 -0
- package/plugins/devflow-quality/skills/workflow-generate-mainflow-cases/SKILL.md +7 -6
- package/plugins/devflow-quality/skills/workflow-generate-mainflow-cases/references/intermediate-format.md +3 -0
- package/plugins/devflow-quality/skills/workflow-generate-mainflow-cases/scripts/compile_dsl.py +49 -3
- package/plugins/devflow-quality/skills/workflow-run-acceptance-tests/SKILL.md +48 -0
- package/plugins/devflow-quality/skills/workflow-run-acceptance-tests/agents/openai.yaml +4 -0
- package/plugins/devflow-quality/skills/workflow-verify-work/SKILL.md +9 -3
- package/src/constants.ts +1 -1
- package/src/doctor.ts +6 -2
- package/src/project-context.ts +11 -1
- package/src/runtime.ts +7 -0
- package/src/usage-stats.ts +3 -3
|
@@ -12,12 +12,14 @@ require_relative "requirement_graph"
|
|
|
12
12
|
|
|
13
13
|
POLICY = {
|
|
14
14
|
"requirement-baseline" => { "checkers" => %w[requirement-graph-check slice-independence-check acceptance-coverage-check change-impact-check], "agents" => [], "budget" => 0, "allowed_graph_delta" => %w[requirement rule boundary slice acceptance] },
|
|
15
|
-
"api-contract" => { "checkers" => %w[requirement-graph-check contract-coverage-check change-impact-check], "agents" => [], "budget" => 0, "allowed_graph_delta" => %w[api] },
|
|
15
|
+
"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] },
|
|
16
16
|
"architecture-feasibility" => { "checkers" => %w[requirement-graph-check change-impact-check], "agents" => [], "budget" => 0, "allowed_graph_delta" => %w[workload_shape workload_constraint decision risk] },
|
|
17
17
|
"integration-verify" => { "checkers" => %w[requirement-graph-check slice-independence-check contract-coverage-check change-impact-check], "agents" => ["verification-runner"], "budget" => 1, "allowed_graph_delta" => %w[requirement risk decision] },
|
|
18
|
-
"
|
|
19
|
-
"
|
|
20
|
-
"
|
|
18
|
+
"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] },
|
|
19
|
+
"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] },
|
|
20
|
+
"execute" => { "checkers" => %w[tdd-evidence-check change-impact-check], "agents" => ["implementation-worker"], "budget" => 1, "allowed_graph_delta" => %w[requirement rule boundary behavior] },
|
|
21
|
+
"run-acceptance-tests" => { "checkers" => %w[api-test-matrix-check test-coverage-check acceptance-test-results-check change-impact-check], "agents" => ["verification-runner"], "budget" => 1, "allowed_graph_delta" => [] },
|
|
22
|
+
"verify" => { "checkers" => %w[acceptance-coverage-check acceptance-test-results-check change-impact-check], "agents" => ["verification-runner"], "budget" => 1, "allowed_graph_delta" => %w[acceptance] }
|
|
21
23
|
}.freeze
|
|
22
24
|
|
|
23
25
|
options = { feature: nil, step: nil, expected_state_revision: nil, expected_graph_revision: nil, write: false, json: false }
|
|
@@ -67,7 +69,7 @@ begin
|
|
|
67
69
|
"state_revision" => state["revision"], "graph_revision" => graph["revision"], "workflow_path" => workflow_relative,
|
|
68
70
|
"workflow_fingerprint" => Digest::SHA256.hexdigest(workflow_raw), "manifest_id" => "devflow-core-execution-policy-v2",
|
|
69
71
|
"allowed_paths" => paths, "allowed_agents" => policy["agents"], "checker_ids" => policy["checkers"],
|
|
70
|
-
"allowed_tools" => %w[read grep find glob lsp search write edit apply_patch bash shell exec_command task],
|
|
72
|
+
"allowed_tools" => %w[read grep find glob lsp search write edit apply_patch bash shell exec_command task devflow_api_test_matrix devflow_tdd_evidence devflow_run_acceptance_tests],
|
|
71
73
|
"tool_budget" => policy["budget"], "acceptance" => Array(graph["nodes"]).select { |node| node["kind"] == "acceptance" }.map { |node| node["id"] },
|
|
72
74
|
"allowed_graph_delta" => Array(policy["allowed_graph_delta"]),
|
|
73
75
|
"on_unknown" => "block_and_escalate"
|
|
@@ -87,7 +87,17 @@ fallback_pending_steps = [
|
|
|
87
87
|
]
|
|
88
88
|
pending_steps = if File.exist?(workflow_path)
|
|
89
89
|
workflow = YAML.safe_load(File.read(workflow_path), permitted_classes: [], aliases: false)
|
|
90
|
-
|
|
90
|
+
# The template is authored in readable sections, so physical
|
|
91
|
+
# YAML position is not a scheduling contract. A step with an
|
|
92
|
+
# explicit order must initialize ahead of later ordered
|
|
93
|
+
# steps even when it was inserted lower in the document.
|
|
94
|
+
Array(workflow["steps"])
|
|
95
|
+
.each_with_index
|
|
96
|
+
.sort_by do |(step, index)|
|
|
97
|
+
order = step["order"]
|
|
98
|
+
[order.is_a?(Integer) ? 0 : 1, order.is_a?(Integer) ? order : index, index]
|
|
99
|
+
end
|
|
100
|
+
.map { |(step, _index)| step.fetch("id") }
|
|
91
101
|
else
|
|
92
102
|
fallback_pending_steps
|
|
93
103
|
end
|
|
@@ -148,7 +148,11 @@ end
|
|
|
148
148
|
|
|
149
149
|
def next_pending_step_id(state, pending_ids, step_by_id, completed_ids)
|
|
150
150
|
track = active_track(state)
|
|
151
|
-
|
|
151
|
+
unless track
|
|
152
|
+
return pending_ids.find do |step_id|
|
|
153
|
+
step_by_id[step_id] && dependencies_satisfied?(step_by_id[step_id], completed_ids)
|
|
154
|
+
end
|
|
155
|
+
end
|
|
152
156
|
|
|
153
157
|
allowed = track_allowed_steps(track)
|
|
154
158
|
pending_ids.find do |step_id|
|
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
# and canonical graph. Agent text is never used to decide completion.
|
|
6
6
|
require "digest"
|
|
7
7
|
require "json"
|
|
8
|
+
require "open3"
|
|
8
9
|
require "optparse"
|
|
9
10
|
require "yaml"
|
|
10
11
|
require_relative "control_io"
|
|
@@ -37,14 +38,46 @@ def configured_checkers(workflow, step_id)
|
|
|
37
38
|
return Array(explicit) if explicit
|
|
38
39
|
case step_id
|
|
39
40
|
when "requirement-baseline" then %w[requirement-graph-check slice-independence-check acceptance-coverage-check change-impact-check]
|
|
40
|
-
when "api-contract" then %w[requirement-graph-check contract-coverage-check change-impact-check]
|
|
41
|
+
when "api-contract" then %w[requirement-graph-check contract-coverage-check api-test-schema-check change-impact-check]
|
|
41
42
|
when "architecture-feasibility" then %w[requirement-graph-check change-impact-check]
|
|
42
|
-
when "integration-verify"
|
|
43
|
-
when "
|
|
43
|
+
when "integration-verify" then %w[requirement-graph-check slice-independence-check contract-coverage-check change-impact-check]
|
|
44
|
+
when "change-plan" then %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]
|
|
45
|
+
when "acceptance-test-design" then %w[requirement-graph-check contract-coverage-check acceptance-coverage-check api-test-matrix-check test-coverage-check test-obligation-check change-impact-check]
|
|
46
|
+
when "run-acceptance-tests" then %w[api-test-matrix-check test-coverage-check acceptance-test-results-check change-impact-check]
|
|
47
|
+
when "verify" then %w[acceptance-coverage-check acceptance-test-results-check change-impact-check]
|
|
48
|
+
when "execute" then %w[tdd-evidence-check change-impact-check]
|
|
44
49
|
else []
|
|
45
50
|
end
|
|
46
51
|
end
|
|
47
52
|
|
|
53
|
+
def supplemental_checker(root, feature, id, step_id)
|
|
54
|
+
command = case id
|
|
55
|
+
when "test-task-binding-check"
|
|
56
|
+
stage = step_id == "change-plan" ? "candidate" : "frozen"
|
|
57
|
+
[File.join(__dir__, "test_task_binding_check.py"), "--feature", feature, "--stage", stage, "--json"]
|
|
58
|
+
when "tdd-evidence-check"
|
|
59
|
+
[File.join(__dir__, "tdd_evidence.py"), "validate", "--feature", feature, "--json"]
|
|
60
|
+
when "acceptance-test-results-check"
|
|
61
|
+
[File.join(__dir__, "acceptance_test_runner.py"), "validate-results", "--feature", feature, "--json"]
|
|
62
|
+
when "api-test-matrix-check"
|
|
63
|
+
[File.join(__dir__, "api_test_matrix.py"), "validate", "--feature", feature, "--json"]
|
|
64
|
+
when "api-test-schema-check"
|
|
65
|
+
[File.join(__dir__, "api_test_matrix.py"), "check", "--feature", feature, "--json"]
|
|
66
|
+
end
|
|
67
|
+
return nil unless command
|
|
68
|
+
|
|
69
|
+
stdout, stderr, status = Open3.capture3("python3", *command, chdir: root)
|
|
70
|
+
payload = JSON.parse(stdout)
|
|
71
|
+
issues = payload.is_a?(Hash) && payload["errors"].is_a?(Array) ? payload["errors"].map(&:to_s) : ["test-task-binding checker returned invalid JSON"]
|
|
72
|
+
passed = status.success? && payload.is_a?(Hash) && payload["ok"] == true
|
|
73
|
+
{ "id" => id, "origin" => "core_fixed_checker", "exit_code" => passed ? 0 : 1, "passed" => passed,
|
|
74
|
+
"issues" => passed ? [] : (issues.empty? ? [stderr.strip.empty? ? "#{id} failed" : stderr.strip] : issues),
|
|
75
|
+
"counters" => { "issue_count" => passed ? 0 : issues.length } }
|
|
76
|
+
rescue JSON::ParserError => error
|
|
77
|
+
{ "id" => id, "origin" => "core_fixed_checker", "exit_code" => 1, "passed" => false,
|
|
78
|
+
"issues" => ["#{id} returned invalid JSON: #{error.message}"], "counters" => { "issue_count" => 1 } }
|
|
79
|
+
end
|
|
80
|
+
|
|
48
81
|
root = Dir.pwd
|
|
49
82
|
lock = DevflowControlIO.acquire_project_lock(root)
|
|
50
83
|
begin
|
|
@@ -78,6 +111,9 @@ begin
|
|
|
78
111
|
gate = workflow.dig("governance", "requirement_graph_gate").to_s
|
|
79
112
|
enforced = gate == "enforced"
|
|
80
113
|
checker_results = selected.map do |id|
|
|
114
|
+
supplemental = supplemental_checker(root, options[:feature], id, options[:step])
|
|
115
|
+
next supplemental if supplemental
|
|
116
|
+
|
|
81
117
|
issues = checkers.fetch(id, ["unknown fixed checker: #{id}"])
|
|
82
118
|
{ "id" => id, "origin" => "core_fixed_checker", "exit_code" => issues.empty? ? 0 : 1, "passed" => issues.empty?, "issues" => issues,
|
|
83
119
|
"counters" => { "issue_count" => issues.length } }
|
|
@@ -13,19 +13,24 @@ require_relative "control_io"
|
|
|
13
13
|
require_relative "architecture_constraints"
|
|
14
14
|
|
|
15
15
|
module RequirementGraph
|
|
16
|
-
#
|
|
16
|
+
# v4 adds derived TC verification nodes. Existing v1-v3
|
|
17
17
|
# graphs are migrated in memory and re-written only by this Core writer.
|
|
18
|
-
SCHEMA_VERSION =
|
|
19
|
-
SUPPORTED_SCHEMA_VERSIONS = [1, 2, SCHEMA_VERSION].freeze
|
|
18
|
+
SCHEMA_VERSION = 4
|
|
19
|
+
SUPPORTED_SCHEMA_VERSIONS = [1, 2, 3, SCHEMA_VERSION].freeze
|
|
20
20
|
ROOT = ".planning/requirements"
|
|
21
21
|
KINDS = {
|
|
22
22
|
"source" => "SRC", "code" => "CODE", "term" => "TERM", "object" => "OBJECT",
|
|
23
23
|
"behavior" => "BEH", "requirement" => "REQ", "rule" => "BR", "boundary" => "BC",
|
|
24
24
|
"slice" => "SL", "risk" => "RISK", "decision" => "DECISION", "acceptance" => "AC",
|
|
25
|
-
"api" => "API", "workload_shape" => "WSH", "workload_constraint" => "WLC"
|
|
25
|
+
"api" => "API", "workload_shape" => "WSH", "workload_constraint" => "WLC", "test_case" => "TC"
|
|
26
26
|
}.freeze
|
|
27
|
-
VIEW_NAMES = %w[evidence definitions behavior-graph requirement-graph slices acceptance traceability].freeze
|
|
27
|
+
VIEW_NAMES = %w[evidence definitions behavior-graph requirement-graph slices acceptance tests traceability].freeze
|
|
28
28
|
FEATURE_INVALID = /[\\\/\x00-\x1F\x7F]/
|
|
29
|
+
# Delta data never writes a node's top-level id/status/revision: Core builds
|
|
30
|
+
# that wrapper itself. Reserving these words recursively made ordinary API
|
|
31
|
+
# bodies such as {"id": ...} or {"status": ...} impossible to describe;
|
|
32
|
+
# reject only root data keys, while nested contract/request payloads remain
|
|
33
|
+
# factual business data.
|
|
29
34
|
RESERVED_KEYS = %w[id status revision passed confirmed graph_revision stale_fingerprint severity risk_level priority p0 high_risk].freeze
|
|
30
35
|
|
|
31
36
|
module_function
|
|
@@ -85,12 +90,7 @@ module RequirementGraph
|
|
|
85
90
|
end
|
|
86
91
|
|
|
87
92
|
def contains_reserved_key?(value)
|
|
88
|
-
|
|
89
|
-
when Hash
|
|
90
|
-
value.any? { |key, child| RESERVED_KEYS.include?(key.to_s) || contains_reserved_key?(child) }
|
|
91
|
-
when Array then value.any? { |child| contains_reserved_key?(child) }
|
|
92
|
-
else false
|
|
93
|
-
end
|
|
93
|
+
value.is_a?(Hash) && value.keys.any? { |key| RESERVED_KEYS.include?(key.to_s) }
|
|
94
94
|
end
|
|
95
95
|
|
|
96
96
|
def normalize_data(value)
|
|
@@ -111,6 +111,24 @@ module RequirementGraph
|
|
|
111
111
|
graph["nodes"].map { |node| node["id"] if node.is_a?(Hash) && node["id"].is_a?(String) }.compact.to_set
|
|
112
112
|
end
|
|
113
113
|
|
|
114
|
+
# The API test compiler deliberately fingerprints only the API contracts.
|
|
115
|
+
# Importing its derived TC nodes advances the graph revision, but must not
|
|
116
|
+
# make the compiler believe the source contract itself changed.
|
|
117
|
+
def api_contract_fingerprint(graph)
|
|
118
|
+
source = Array(graph["nodes"]).select { |node| node.is_a?(Hash) && node["kind"] == "api" }.map do |node|
|
|
119
|
+
{ "id" => node["id"], "data" => node["data"] }
|
|
120
|
+
end
|
|
121
|
+
Digest::SHA256.hexdigest(JSON.generate(deep_sort(source)))
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
def deep_sort(value)
|
|
125
|
+
case value
|
|
126
|
+
when Hash then value.keys.sort.to_h { |key| [key, deep_sort(value[key])] }
|
|
127
|
+
when Array then value.map { |item| deep_sort(item) }
|
|
128
|
+
else value
|
|
129
|
+
end
|
|
130
|
+
end
|
|
131
|
+
|
|
114
132
|
def set_node_fingerprint!(node)
|
|
115
133
|
node["fingerprint"] = fingerprint(node.reject { |key, _| key == "fingerprint" })
|
|
116
134
|
end
|
|
@@ -158,7 +176,7 @@ module RequirementGraph
|
|
|
158
176
|
raise ArgumentError, "node kind #{kind} is not allowed by the execution contract" if allowed_kinds && !allowed_kinds.include?(kind)
|
|
159
177
|
data = normalize_data(delta["data"])
|
|
160
178
|
node = { "id" => next_id(graph, prefix), "kind" => kind, "status" => "candidate", "data" => data }
|
|
161
|
-
candidate_errors = workload_node_errors(graph, node) + architecture_decision_errors(graph, node) + architecture_risk_errors(graph, node)
|
|
179
|
+
candidate_errors = workload_node_errors(graph, node) + architecture_decision_errors(graph, node) + architecture_risk_errors(graph, node) + test_case_errors(graph, node)
|
|
162
180
|
raise ArgumentError, candidate_errors.join("; ") if candidate_errors.any?
|
|
163
181
|
set_node_fingerprint!(node)
|
|
164
182
|
graph["nodes"] << node
|
|
@@ -175,12 +193,12 @@ module RequirementGraph
|
|
|
175
193
|
id = delta["id"]
|
|
176
194
|
node = Array(graph["nodes"]).find { |entry| entry.is_a?(Hash) && entry["id"] == id }
|
|
177
195
|
raise ArgumentError, "update requires a known node id" unless node
|
|
178
|
-
if (%w[workload_shape workload_constraint].include?(node["kind"]) || architecture_control_node?(node)) && node["status"] == "confirmed"
|
|
179
|
-
raise ArgumentError, "confirmed workload nodes and architecture decisions are immutable; add a new candidate and relink consumers"
|
|
196
|
+
if (%w[workload_shape workload_constraint test_case].include?(node["kind"]) || architecture_control_node?(node)) && node["status"] == "confirmed"
|
|
197
|
+
raise ArgumentError, "confirmed workload nodes and architecture decisions are immutable; confirmed test cases are also immutable; add a new candidate and relink consumers"
|
|
180
198
|
end
|
|
181
199
|
data = normalize_data(delta["data"])
|
|
182
200
|
node["data"] = node.fetch("data", {}).merge(data)
|
|
183
|
-
candidate_errors = workload_node_errors(graph, node) + architecture_decision_errors(graph, node) + architecture_risk_errors(graph, node)
|
|
201
|
+
candidate_errors = workload_node_errors(graph, node) + architecture_decision_errors(graph, node) + architecture_risk_errors(graph, node) + test_case_errors(graph, node)
|
|
184
202
|
raise ArgumentError, candidate_errors.join("; ") if candidate_errors.any?
|
|
185
203
|
set_node_fingerprint!(node)
|
|
186
204
|
clear_stale_for!(graph, id)
|
|
@@ -199,6 +217,7 @@ module RequirementGraph
|
|
|
199
217
|
node = Array(graph["nodes"]).find { |entry| entry.is_a?(Hash) && entry["id"] == node_id }
|
|
200
218
|
raise ArgumentError, "confirm requires a known node id" unless node
|
|
201
219
|
raise ArgumentError, "node is already confirmed" if node["status"] == "confirmed"
|
|
220
|
+
raise ArgumentError, "test cases must be frozen through acceptance_test_suite.rb" if node["kind"] == "test_case"
|
|
202
221
|
unless evidence.is_a?(Hash) && (evidence["cp_record"] || evidence["evidence_ref"])
|
|
203
222
|
raise ArgumentError, "confirm requires cp_record or evidence_ref"
|
|
204
223
|
end
|
|
@@ -252,6 +271,215 @@ module RequirementGraph
|
|
|
252
271
|
end
|
|
253
272
|
end
|
|
254
273
|
|
|
274
|
+
TEST_CASE_TYPES = %w[api page permission boundary resilience migration performance].freeze
|
|
275
|
+
TEST_AUTOMATION_MODES = %w[automated manual blocked].freeze
|
|
276
|
+
|
|
277
|
+
# TC nodes are derived verification intent. They deliberately store only
|
|
278
|
+
# externally observable setup, operation and oracle; implementation paths,
|
|
279
|
+
# selectors and agent summaries belong to later execution bindings.
|
|
280
|
+
def test_case_errors(graph, node, require_edges: false, require_confirmed_inputs: false)
|
|
281
|
+
return [] unless node["kind"] == "test_case"
|
|
282
|
+
|
|
283
|
+
data = node["data"].is_a?(Hash) ? node["data"] : {}
|
|
284
|
+
errors = []
|
|
285
|
+
%w[acceptance_refs requirement_refs test_type target preconditions steps oracle automation_mode].each do |field|
|
|
286
|
+
errors << "#{node["id"]}: missing #{field}" unless present?(data[field])
|
|
287
|
+
end
|
|
288
|
+
errors << "#{node["id"]}: acceptance_refs must be a non-empty array" unless data["acceptance_refs"].is_a?(Array) && !data["acceptance_refs"].empty?
|
|
289
|
+
errors << "#{node["id"]}: requirement_refs must be a non-empty array" unless data["requirement_refs"].is_a?(Array) && !data["requirement_refs"].empty?
|
|
290
|
+
errors << "#{node["id"]}: preconditions must be a non-empty array" unless data["preconditions"].is_a?(Array) && !data["preconditions"].empty?
|
|
291
|
+
errors << "#{node["id"]}: steps must be a non-empty array" unless data["steps"].is_a?(Array) && !data["steps"].empty?
|
|
292
|
+
errors << "#{node["id"]}: invalid test_type" unless TEST_CASE_TYPES.include?(data["test_type"])
|
|
293
|
+
errors << "#{node["id"]}: invalid automation_mode" unless TEST_AUTOMATION_MODES.include?(data["automation_mode"])
|
|
294
|
+
if data.key?("test_obligation_refs")
|
|
295
|
+
errors << "#{node["id"]}: test_obligation_refs must be a non-empty string array when present" unless data["test_obligation_refs"].is_a?(Array) && !data["test_obligation_refs"].empty? && data["test_obligation_refs"].all? { |ref| ref.is_a?(String) && ref.match?(/\AOBL-[A-Z0-9-]+\z/) }
|
|
296
|
+
end
|
|
297
|
+
if data.key?("generated_case_key")
|
|
298
|
+
errors << "#{node["id"]}: generated_case_key is invalid" unless data["generated_case_key"].is_a?(String) && data["generated_case_key"].match?(/\AATC-[A-Z0-9-]+\z/)
|
|
299
|
+
errors << "#{node["id"]}: generated API TC requires test_obligation_refs" unless data["test_obligation_refs"].is_a?(Array) && !data["test_obligation_refs"].empty?
|
|
300
|
+
errors << "#{node["id"]}: generated API TC requires a non-empty api_refs" unless data["api_refs"].is_a?(Array) && !data["api_refs"].empty?
|
|
301
|
+
execution = data["execution"]
|
|
302
|
+
unless execution.is_a?(Hash) && execution["protocol"] == "http" && execution["request"].is_a?(Hash) && execution["expect"].is_a?(Hash) && execution.dig("request", "method").is_a?(String) && execution.dig("request", "path").is_a?(String) && execution.dig("expect", "http_status").is_a?(Integer)
|
|
303
|
+
errors << "#{node["id"]}: generated API TC requires an HTTP execution definition"
|
|
304
|
+
end
|
|
305
|
+
end
|
|
306
|
+
if data["automation_mode"] == "manual"
|
|
307
|
+
%w[manual_reason owner].each { |field| errors << "#{node["id"]}: manual tests require #{field}" unless present?(data[field]) }
|
|
308
|
+
elsif data["automation_mode"] == "blocked"
|
|
309
|
+
%w[blocked_by owner].each { |field| errors << "#{node["id"]}: blocked tests require #{field}" unless present?(data[field]) }
|
|
310
|
+
end
|
|
311
|
+
|
|
312
|
+
Array(data["acceptance_refs"]).each do |ref|
|
|
313
|
+
errors << "#{node["id"]}: acceptance_ref #{ref} must reference AC" unless refs_known?(graph, [ref], %w[AC])
|
|
314
|
+
errors << "#{node["id"]}: acceptance_ref #{ref} must link with verified_by" if require_edges && !incoming_edge?(graph, ref, node["id"], "verified_by")
|
|
315
|
+
errors << "#{node["id"]}: acceptance_ref #{ref} must be confirmed" if require_confirmed_inputs && !confirmed?(graph, ref)
|
|
316
|
+
end
|
|
317
|
+
Array(data["requirement_refs"]).each do |ref|
|
|
318
|
+
errors << "#{node["id"]}: requirement_ref #{ref} must reference REQ/BR/BC" unless refs_known?(graph, [ref], %w[REQ BR BC])
|
|
319
|
+
errors << "#{node["id"]}: requirement_ref #{ref} must link with verified_by" if require_edges && !incoming_edge?(graph, ref, node["id"], "verified_by")
|
|
320
|
+
errors << "#{node["id"]}: requirement_ref #{ref} must be confirmed" if require_confirmed_inputs && !confirmed?(graph, ref)
|
|
321
|
+
end
|
|
322
|
+
{
|
|
323
|
+
"api_refs" => %w[API],
|
|
324
|
+
"workload_refs" => %w[WSH WLC],
|
|
325
|
+
"risk_refs" => %w[RISK]
|
|
326
|
+
}.each do |field, prefixes|
|
|
327
|
+
next unless data.key?(field)
|
|
328
|
+
|
|
329
|
+
errors << "#{node["id"]}: #{field} must be a non-empty array when present" unless data[field].is_a?(Array) && !data[field].empty?
|
|
330
|
+
Array(data[field]).each do |ref|
|
|
331
|
+
label = field.sub(/s\z/, "")
|
|
332
|
+
errors << "#{node["id"]}: #{label} #{ref} references an invalid graph node" unless refs_known?(graph, [ref], prefixes)
|
|
333
|
+
errors << "#{node["id"]}: #{label} #{ref} must link with verified_by" if require_edges && !incoming_edge?(graph, ref, node["id"], "verified_by")
|
|
334
|
+
errors << "#{node["id"]}: #{label} #{ref} must be confirmed" if require_confirmed_inputs && !confirmed?(graph, ref)
|
|
335
|
+
end
|
|
336
|
+
end
|
|
337
|
+
errors.uniq
|
|
338
|
+
end
|
|
339
|
+
|
|
340
|
+
def frozen_test_case?(graph, node)
|
|
341
|
+
node["kind"] == "test_case" && node["status"] == "confirmed" && !Array(graph["stale"]).any? { |entry| entry.is_a?(Hash) && entry["node_id"] == node["id"] }
|
|
342
|
+
end
|
|
343
|
+
|
|
344
|
+
# CP-3 is the single freeze point. Keep it here instead of in a Skill so the
|
|
345
|
+
# graph revision, TC statuses, views and audit entry commit atomically under
|
|
346
|
+
# the caller's control-plane lock.
|
|
347
|
+
def freeze_test_cases!(root, graph, cp_record)
|
|
348
|
+
cases = Array(graph["nodes"]).select { |node| node.is_a?(Hash) && node["kind"] == "test_case" }
|
|
349
|
+
return { "ok" => false, "reason" => "test_suite_empty", "message" => "cannot freeze an empty TC suite" } if cases.empty?
|
|
350
|
+
return { "ok" => false, "reason" => "test_suite_already_frozen", "message" => "confirmed test cases require a new candidate suite" } if cases.any? { |node| node["status"] == "confirmed" }
|
|
351
|
+
return { "ok" => false, "reason" => "test_suite_graph_stale", "message" => "cannot freeze TC suite while the requirement graph has stale downstream nodes" } if Array(graph["stale"]).any?
|
|
352
|
+
|
|
353
|
+
checkers = checker_results(graph)
|
|
354
|
+
errors = checkers.fetch("test-coverage-check", []).dup
|
|
355
|
+
errors.concat(checkers.fetch("test-obligation-check", []))
|
|
356
|
+
cases.each { |node| errors.concat(test_case_errors(graph, node, require_edges: true, require_confirmed_inputs: true)) }
|
|
357
|
+
return { "ok" => false, "reason" => "test_suite_incomplete", "message" => errors.uniq.join("; "), "errors" => errors.uniq } if errors.any?
|
|
358
|
+
|
|
359
|
+
cases.each do |node|
|
|
360
|
+
node["status"] = "confirmed"
|
|
361
|
+
node["confirmation"] = { "origin" => "core_acceptance_test_suite", "cp_record" => cp_record }
|
|
362
|
+
set_node_fingerprint!(node)
|
|
363
|
+
clear_stale_for!(graph, node["id"])
|
|
364
|
+
end
|
|
365
|
+
graph["revision"] += 1
|
|
366
|
+
write_views!(root, graph, stale: false)
|
|
367
|
+
path, = graph_path(root, graph["feature"])
|
|
368
|
+
append_audit!(root, graph, { "event" => "test_suite_freeze", "node_ids" => cases.map { |node| node["id"] }, "cp_record" => cp_record })
|
|
369
|
+
DevflowControlIO.atomic_write(path, JSON.pretty_generate(graph) + "\n")
|
|
370
|
+
{ "ok" => true, "path" => path.sub("#{File.realpath(root)}/", ""), "graph_revision" => graph["revision"],
|
|
371
|
+
"frozen_case_ids" => cases.map { |node| node["id"] }, "tests_view" => graph.dig("views", "tests") }
|
|
372
|
+
end
|
|
373
|
+
|
|
374
|
+
# The matrix compiler is Core-owned and deterministic; this importer is the
|
|
375
|
+
# only path which may turn its complete generated case list into graph TC
|
|
376
|
+
# candidates. Keeping the batch write here prevents a Skill from omitting
|
|
377
|
+
# the 37th boundary case while still preserving the normal graph audit and
|
|
378
|
+
# ID allocation rules.
|
|
379
|
+
def import_api_test_matrix!(root, graph)
|
|
380
|
+
matrix_path = File.join(feature_dir(root, graph["feature"]), "api-test-matrix.json")
|
|
381
|
+
raise ArgumentError, "API test matrix is missing; compile it before import" unless File.file?(matrix_path)
|
|
382
|
+
matrix = JSON.parse(File.read(matrix_path))
|
|
383
|
+
raise ArgumentError, "API test matrix identity is invalid" unless matrix["schema_version"] == 1 && matrix["origin"] == "core_api_test_matrix" && matrix["feature"] == graph["feature"]
|
|
384
|
+
expected_source = { "api_contract_fingerprint" => api_contract_fingerprint(graph), "api_ids" => Array(graph["nodes"]).select { |node| node.is_a?(Hash) && node["kind"] == "api" }.map { |node| node["id"] } }
|
|
385
|
+
unless matrix["source"] == expected_source
|
|
386
|
+
raise ArgumentError, "API test matrix is stale for the current API contract (expected #{expected_source["api_contract_fingerprint"]}, got #{matrix.dig("source", "api_contract_fingerprint")})"
|
|
387
|
+
end
|
|
388
|
+
raise ArgumentError, "API test matrix contains unresolved contract gaps" unless Array(matrix["errors"]).empty?
|
|
389
|
+
|
|
390
|
+
generated_cases = matrix["cases"]
|
|
391
|
+
raise ArgumentError, "API test matrix cases must be an array" unless generated_cases.is_a?(Array)
|
|
392
|
+
existing = Array(graph["nodes"]).select { |node| node.is_a?(Hash) && node["kind"] == "test_case" && node.dig("data", "generated_case_key").is_a?(String) }.to_h { |node| [node.dig("data", "generated_case_key"), node] }
|
|
393
|
+
keys = generated_cases.map { |item| item.is_a?(Hash) ? item["key"] : nil }
|
|
394
|
+
raise ArgumentError, "API test matrix case keys must be unique non-empty strings" unless keys.all? { |key| key.is_a?(String) && key.match?(/\AATC-[A-Z0-9-]+\z/) } && keys.uniq.length == keys.length
|
|
395
|
+
|
|
396
|
+
imported = []
|
|
397
|
+
generated_cases.each do |item|
|
|
398
|
+
candidate_data = item["test_case"]
|
|
399
|
+
raise ArgumentError, "#{item["key"]}: test_case must be an object" unless candidate_data.is_a?(Hash)
|
|
400
|
+
raise ArgumentError, "#{item["key"]}: test_case generated_case_key mismatch" unless candidate_data["generated_case_key"] == item["key"]
|
|
401
|
+
raise ArgumentError, "#{item["key"]}: test_case obligation refs mismatch" unless candidate_data["test_obligation_refs"] == item["obligation_refs"]
|
|
402
|
+
node = existing[item["key"]]
|
|
403
|
+
if node
|
|
404
|
+
comparable = node.dig("data", "execution") == candidate_data["execution"] && node.dig("data", "test_obligation_refs") == candidate_data["test_obligation_refs"]
|
|
405
|
+
raise ArgumentError, "#{item["key"]}: existing generated TC does not match matrix" unless comparable
|
|
406
|
+
else
|
|
407
|
+
applied = proposal(graph, { "op" => "add", "kind" => "test_case", "data" => candidate_data }, ["test_case"])
|
|
408
|
+
node = applied.fetch("node")
|
|
409
|
+
refs = Array(candidate_data["acceptance_refs"]) + Array(candidate_data["requirement_refs"]) + Array(candidate_data["api_refs"])
|
|
410
|
+
refs.each do |ref|
|
|
411
|
+
edge = { "from" => ref, "to" => node["id"], "relation" => "verified_by" }
|
|
412
|
+
graph["edges"] << edge unless graph["edges"].include?(edge)
|
|
413
|
+
end
|
|
414
|
+
errors = test_case_errors(graph, node, require_edges: true, require_confirmed_inputs: true)
|
|
415
|
+
raise ArgumentError, errors.join("; ") if errors.any?
|
|
416
|
+
existing[item["key"]] = node
|
|
417
|
+
imported << node["id"]
|
|
418
|
+
end
|
|
419
|
+
item["tc_id"] = node["id"]
|
|
420
|
+
end
|
|
421
|
+
|
|
422
|
+
if imported.any?
|
|
423
|
+
graph["revision"] += 1
|
|
424
|
+
stale = Array(graph["stale"]).any?
|
|
425
|
+
write_views!(root, graph, stale: stale)
|
|
426
|
+
append_audit!(root, graph, { "event" => "api_test_matrix_import", "node_ids" => imported, "case_set_fingerprint" => matrix["case_set_fingerprint"] })
|
|
427
|
+
path, = graph_path(root, graph["feature"])
|
|
428
|
+
DevflowControlIO.atomic_write(path, JSON.pretty_generate(graph) + "\n")
|
|
429
|
+
end
|
|
430
|
+
matrix["imported_graph_revision"] = graph["revision"]
|
|
431
|
+
DevflowControlIO.atomic_write(matrix_path, JSON.pretty_generate(matrix) + "\n")
|
|
432
|
+
template_path = File.join(root, ".planning", "features", graph["feature"], "api-acceptance-bindings.template.json")
|
|
433
|
+
template = {
|
|
434
|
+
"schema_version" => 1, "origin" => "core_api_test_matrix_template", "feature" => graph["feature"],
|
|
435
|
+
"case_set_fingerprint" => matrix["case_set_fingerprint"],
|
|
436
|
+
"cases" => generated_cases.map do |item|
|
|
437
|
+
{ "tc_id" => item["tc_id"], "mode" => "automated", "adapter" => { "kind" => "http", "base_url_env" => "DEVFLOW_TEST_BASE_URL", "headers_env" => { "Authorization" => "DEVFLOW_TEST_AUTHORIZATION" } } }
|
|
438
|
+
end
|
|
439
|
+
}
|
|
440
|
+
DevflowControlIO.atomic_write(template_path, JSON.pretty_generate(template) + "\n")
|
|
441
|
+
{ "ok" => true, "imported_case_ids" => imported, "test_case_ids" => generated_cases.map { |item| item["tc_id"] }, "graph_revision" => graph["revision"], "case_set_fingerprint" => matrix["case_set_fingerprint"] }
|
|
442
|
+
end
|
|
443
|
+
|
|
444
|
+
# CP-API, not the API-contract Skill, turns a complete candidate contract
|
|
445
|
+
# into confirmed graph fact. Downstream TC imports therefore never need to
|
|
446
|
+
# trust an agent-written `cp_record` as proof that the shared contract was
|
|
447
|
+
# reviewed by the user.
|
|
448
|
+
def confirm_api_contracts!(root, graph, cp_record)
|
|
449
|
+
apis = Array(graph["nodes"]).select { |node| node.is_a?(Hash) && node["kind"] == "api" }
|
|
450
|
+
return { "ok" => true, "confirmed_api_ids" => [], "graph_revision" => graph["revision"], "skipped" => "no_api_nodes" } if apis.empty?
|
|
451
|
+
|
|
452
|
+
errors = checker_results(graph).fetch("contract-coverage-check", []).dup
|
|
453
|
+
apis.each do |node|
|
|
454
|
+
Array(node.dig("data", "requirement_refs")).each do |ref|
|
|
455
|
+
errors << "#{node["id"]}: requirement_ref #{ref} must be confirmed before CP-API" unless confirmed?(graph, ref)
|
|
456
|
+
end
|
|
457
|
+
behavior_ref = node.dig("data", "behavior_ref")
|
|
458
|
+
errors << "#{node["id"]}: behavior_ref #{behavior_ref} must be confirmed before CP-API" unless confirmed?(graph, behavior_ref)
|
|
459
|
+
end
|
|
460
|
+
return { "ok" => false, "reason" => "api_contract_incomplete", "errors" => errors.uniq } if errors.any?
|
|
461
|
+
|
|
462
|
+
confirmed = []
|
|
463
|
+
apis.each do |node|
|
|
464
|
+
next if node["status"] == "confirmed"
|
|
465
|
+
|
|
466
|
+
node["status"] = "confirmed"
|
|
467
|
+
node["confirmation"] = { "origin" => "core_api_contract_checkpoint", "cp_record" => cp_record }
|
|
468
|
+
set_node_fingerprint!(node)
|
|
469
|
+
clear_stale_for!(graph, node["id"])
|
|
470
|
+
confirmed << node["id"]
|
|
471
|
+
end
|
|
472
|
+
if confirmed.any?
|
|
473
|
+
graph["revision"] += 1
|
|
474
|
+
stale = Array(graph["stale"]).any?
|
|
475
|
+
write_views!(root, graph, stale: stale)
|
|
476
|
+
path, = graph_path(root, graph["feature"])
|
|
477
|
+
append_audit!(root, graph, { "event" => "api_contract_confirm", "node_ids" => confirmed, "cp_record" => cp_record })
|
|
478
|
+
DevflowControlIO.atomic_write(path, JSON.pretty_generate(graph) + "\n")
|
|
479
|
+
end
|
|
480
|
+
{ "ok" => true, "confirmed_api_ids" => confirmed, "graph_revision" => graph["revision"] }
|
|
481
|
+
end
|
|
482
|
+
|
|
255
483
|
# WSH/WLC are graph-owned facts, not a second unbounded document schema.
|
|
256
484
|
# The candidate validator owns field rules; this layer adds only the facts
|
|
257
485
|
# it can prove: graph references, traceability edges, and confirmation state.
|
|
@@ -400,7 +628,7 @@ module RequirementGraph
|
|
|
400
628
|
def checker_results(graph)
|
|
401
629
|
results = {
|
|
402
630
|
"requirement-graph-check" => [], "slice-independence-check" => [], "contract-coverage-check" => [],
|
|
403
|
-
"change-impact-check" => [], "acceptance-coverage-check" => [], "risk-derivation-check" => []
|
|
631
|
+
"change-impact-check" => [], "acceptance-coverage-check" => [], "test-coverage-check" => [], "test-obligation-check" => [], "risk-derivation-check" => []
|
|
404
632
|
}
|
|
405
633
|
Array(graph["nodes"]).each do |node|
|
|
406
634
|
next unless node.is_a?(Hash)
|
|
@@ -446,6 +674,45 @@ module RequirementGraph
|
|
|
446
674
|
missing = %w[business contract resilience] - types
|
|
447
675
|
results["acceptance-coverage-check"] << "#{node["id"]}: high-risk acceptance missing #{missing.join(", ")}" if missing.any?
|
|
448
676
|
end
|
|
677
|
+
when "test_case"
|
|
678
|
+
results["test-coverage-check"].concat(
|
|
679
|
+
test_case_errors(graph, node, require_edges: true, require_confirmed_inputs: node["status"] == "confirmed")
|
|
680
|
+
)
|
|
681
|
+
end
|
|
682
|
+
end
|
|
683
|
+
Array(graph["nodes"]).select { |node| node.is_a?(Hash) && node["kind"] == "acceptance" }.each do |acceptance|
|
|
684
|
+
covered = Array(graph["nodes"]).any? do |candidate|
|
|
685
|
+
candidate.is_a?(Hash) && candidate["kind"] == "test_case" && Array(candidate.dig("data", "acceptance_refs")).include?(acceptance["id"])
|
|
686
|
+
end
|
|
687
|
+
results["test-coverage-check"] << "#{acceptance["id"]}: missing TC coverage" unless covered
|
|
688
|
+
end
|
|
689
|
+
test_cases = Array(graph["nodes"]).select { |node| node.is_a?(Hash) && node["kind"] == "test_case" }
|
|
690
|
+
covered_by = lambda do |test_type, field, ref|
|
|
691
|
+
test_cases.any? do |test_case|
|
|
692
|
+
test_case.dig("data", "test_type") == test_type && Array(test_case.dig("data", field)).include?(ref)
|
|
693
|
+
end
|
|
694
|
+
end
|
|
695
|
+
Array(graph["nodes"]).each do |node|
|
|
696
|
+
next unless node.is_a?(Hash)
|
|
697
|
+
|
|
698
|
+
case node["kind"]
|
|
699
|
+
when "api"
|
|
700
|
+
results["test-obligation-check"] << "#{node["id"]}: API change requires an api TC with api_refs" unless covered_by.call("api", "api_refs", node["id"])
|
|
701
|
+
when "boundary"
|
|
702
|
+
results["test-obligation-check"] << "#{node["id"]}: boundary requires a boundary TC with requirement_refs" unless covered_by.call("boundary", "requirement_refs", node["id"])
|
|
703
|
+
when "workload_shape", "workload_constraint"
|
|
704
|
+
results["test-obligation-check"] << "#{node["id"]}: workload constraint requires a performance TC with workload_refs" unless covered_by.call("performance", "workload_refs", node["id"])
|
|
705
|
+
if node["kind"] == "workload_shape" && node.dig("data", "facts", "data_effect") == "migration"
|
|
706
|
+
results["test-obligation-check"] << "#{node["id"]}: migration workload requires a migration TC with workload_refs" unless covered_by.call("migration", "workload_refs", node["id"])
|
|
707
|
+
end
|
|
708
|
+
when "risk"
|
|
709
|
+
if architecture_option_risk?(node)
|
|
710
|
+
results["test-obligation-check"] << "#{node["id"]}: architecture risk requires a resilience TC with risk_refs" unless covered_by.call("resilience", "risk_refs", node["id"])
|
|
711
|
+
end
|
|
712
|
+
when "requirement"
|
|
713
|
+
if high_risk_by_structure?(node)
|
|
714
|
+
results["test-obligation-check"] << "#{node["id"]}: cross-domain requirement requires a permission TC with requirement_refs" unless covered_by.call("permission", "requirement_refs", node["id"])
|
|
715
|
+
end
|
|
449
716
|
end
|
|
450
717
|
end
|
|
451
718
|
stale = Array(graph["stale"])
|
|
@@ -477,7 +744,10 @@ module RequirementGraph
|
|
|
477
744
|
Array(graph["edges"]).each do |edge|
|
|
478
745
|
errors << "invalid edge" unless edge.is_a?(Hash) && known.include?(edge["from"]) && known.include?(edge["to"]) && edge["relation"].is_a?(String)
|
|
479
746
|
end
|
|
480
|
-
|
|
747
|
+
# Test coverage becomes enforced only at acceptance-test-design and later
|
|
748
|
+
# workflow steps. Baseline construction itself must remain valid before
|
|
749
|
+
# any derived TC candidates exist.
|
|
750
|
+
checker_results(graph).reject { |id, _| %w[test-coverage-check test-obligation-check].include?(id) }.each_value { |issues| errors.concat(issues) }
|
|
481
751
|
Array(graph["nodes"]).each do |node|
|
|
482
752
|
next unless node.is_a?(Hash) && high_risk_by_structure?(node)
|
|
483
753
|
errors << "#{node["id"]}: derived high-risk node must be confirmed and provable" unless node["status"] == "confirmed"
|
|
@@ -490,14 +760,21 @@ module RequirementGraph
|
|
|
490
760
|
when "evidence" then %w[source code]
|
|
491
761
|
when "definitions" then %w[term object rule boundary]
|
|
492
762
|
when "behavior-graph" then %w[behavior]
|
|
493
|
-
when "requirement-graph", "traceability" then %w[requirement rule boundary api workload_shape workload_constraint]
|
|
763
|
+
when "requirement-graph", "traceability" then %w[requirement rule boundary api workload_shape workload_constraint test_case]
|
|
494
764
|
when "slices" then %w[slice]
|
|
495
765
|
when "acceptance" then %w[acceptance risk decision]
|
|
766
|
+
when "tests" then %w[test_case]
|
|
496
767
|
else []
|
|
497
768
|
end
|
|
498
769
|
nodes = Array(graph["nodes"]).select { |node| kinds.include?(node["kind"]) }
|
|
499
|
-
{ "schema_version" => SCHEMA_VERSION, "origin" => "core_requirement_graph", "feature" => graph["feature"], "graph_revision" => graph["revision"],
|
|
500
|
-
|
|
770
|
+
payload = { "schema_version" => SCHEMA_VERSION, "origin" => "core_requirement_graph", "feature" => graph["feature"], "graph_revision" => graph["revision"],
|
|
771
|
+
"view" => name, "nodes" => nodes, "edges" => Array(graph["edges"]).select { |edge| nodes.any? { |node| node["id"] == edge["from"] || node["id"] == edge["to"] } } }
|
|
772
|
+
if name == "tests"
|
|
773
|
+
test_cases = nodes.select { |node| node["kind"] == "test_case" }
|
|
774
|
+
payload["frozen"] = !test_cases.empty? && test_cases.all? { |node| frozen_test_case?(graph, node) }
|
|
775
|
+
payload["frozen_graph_revision"] = graph["revision"] if payload["frozen"]
|
|
776
|
+
end
|
|
777
|
+
payload
|
|
501
778
|
end
|
|
502
779
|
|
|
503
780
|
def write_views!(root, graph, stale: false)
|
|
@@ -559,7 +836,7 @@ if __FILE__ == $PROGRAM_NAME
|
|
|
559
836
|
options = { feature: nil, expected_revision: nil, delta_json: nil, slice_id: nil, node_id: nil, evidence_json: nil, allowed_kinds: nil, confirmed_interactively: false, write: false, read: false, json: false }
|
|
560
837
|
command = ARGV.shift
|
|
561
838
|
OptionParser.new do |opts|
|
|
562
|
-
|
|
839
|
+
opts.banner = "Usage: requirement_graph.rb propose|import-test-cases|inspect|validate|context --feature NAME [options]"
|
|
563
840
|
opts.on("--feature NAME") { |value| options[:feature] = value }
|
|
564
841
|
opts.on("--expected-revision N", Integer) { |value| options[:expected_revision] = value }
|
|
565
842
|
opts.on("--delta-json JSON") { |value| options[:delta_json] = value }
|
|
@@ -573,7 +850,7 @@ if __FILE__ == $PROGRAM_NAME
|
|
|
573
850
|
opts.on("--json") { options[:json] = true }
|
|
574
851
|
end.parse!
|
|
575
852
|
root = Dir.pwd
|
|
576
|
-
lock = %w[propose context confirm].include?(command) && options[:write] ? DevflowControlIO.acquire_project_lock(root) : nil
|
|
853
|
+
lock = %w[propose import-test-cases context confirm].include?(command) && options[:write] ? DevflowControlIO.acquire_project_lock(root) : nil
|
|
577
854
|
begin
|
|
578
855
|
graph, path, relative = RequirementGraph.load_graph(root, options[:feature])
|
|
579
856
|
result = case command
|
|
@@ -617,6 +894,15 @@ if __FILE__ == $PROGRAM_NAME
|
|
|
617
894
|
DevflowControlIO.atomic_write(path, JSON.pretty_generate(graph) + "\n")
|
|
618
895
|
{ "ok" => true, "path" => relative, "revision" => graph["revision"], "applied" => applied, "validation_errors" => RequirementGraph.validate(graph), "checkers" => RequirementGraph.checker_results(graph), "views" => graph["views"] }
|
|
619
896
|
end
|
|
897
|
+
when "import-test-cases"
|
|
898
|
+
raise ArgumentError, "--write is required" unless options[:write]
|
|
899
|
+
raise ArgumentError, "--expected-revision is required" if options[:expected_revision].nil?
|
|
900
|
+
if graph["revision"] != options[:expected_revision]
|
|
901
|
+
{ "ok" => false, "reason" => "stale_graph_revision", "expected_revision" => options[:expected_revision], "actual_revision" => graph["revision"] }
|
|
902
|
+
else
|
|
903
|
+
imported = RequirementGraph.import_api_test_matrix!(root, graph)
|
|
904
|
+
{ "ok" => true, "path" => relative, "revision" => graph["revision"], "imported" => imported, "validation_errors" => RequirementGraph.validate(graph), "checkers" => RequirementGraph.checker_results(graph), "views" => graph["views"] }
|
|
905
|
+
end
|
|
620
906
|
when "confirm"
|
|
621
907
|
raise ArgumentError, "--write is required" unless options[:write]
|
|
622
908
|
raise ArgumentError, "--node-id is required" unless options[:node_id]
|
|
@@ -635,7 +921,7 @@ if __FILE__ == $PROGRAM_NAME
|
|
|
635
921
|
{ "ok" => true, "path" => relative, "revision" => graph["revision"], "applied" => applied, "validation_errors" => RequirementGraph.validate(graph), "checkers" => RequirementGraph.checker_results(graph), "views" => graph["views"] }
|
|
636
922
|
end
|
|
637
923
|
else
|
|
638
|
-
{ "ok" => false, "reason" => "invalid_command", "message" => "command must be propose, inspect, validate, context, or confirm" }
|
|
924
|
+
{ "ok" => false, "reason" => "invalid_command", "message" => "command must be propose, import-test-cases, inspect, validate, context, or confirm" }
|
|
639
925
|
end
|
|
640
926
|
rescue ArgumentError, JSON::ParserError => error
|
|
641
927
|
result = { "ok" => false, "reason" => "invalid_requirement_graph_input", "message" => error.message }
|