@guanlin4924/devflow-pi 0.1.85 → 0.1.88
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 +4 -0
- package/package.json +1 -1
- package/plugins/devflow-core/extensions/devflow-core.ts +211 -19
- package/plugins/devflow-core/skills/workflow-orchestrator/scripts/acceptance_test_runner.py +15 -0
- package/plugins/devflow-core/skills/workflow-orchestrator/scripts/advance_state.rb +49 -3
- package/plugins/devflow-core/skills/workflow-orchestrator/scripts/checker_adapter.rb +83 -0
- package/plugins/devflow-core/skills/workflow-orchestrator/scripts/checker_registry.rb +19 -0
- package/plugins/devflow-core/skills/workflow-orchestrator/scripts/checkpoint_meta.rb +46 -0
- package/plugins/devflow-core/skills/workflow-orchestrator/scripts/execution_contract.rb +10 -1
- package/plugins/devflow-core/skills/workflow-orchestrator/scripts/gate_runtime.rb +32 -0
- package/plugins/devflow-core/skills/workflow-orchestrator/scripts/init_state.rb +44 -41
- package/plugins/devflow-core/skills/workflow-orchestrator/scripts/init_workflow.rb +5 -4
- package/plugins/devflow-core/skills/workflow-orchestrator/scripts/inspect_state.rb +15 -4
- package/plugins/devflow-core/skills/workflow-orchestrator/scripts/machine_evidence.rb +15 -9
- package/plugins/devflow-core/skills/workflow-orchestrator/scripts/review_preflight.py +238 -0
- package/plugins/devflow-core/skills/workflow-orchestrator/scripts/review_run.py +234 -32
- package/plugins/devflow-core/skills/workflow-orchestrator/scripts/validate_state.rb +63 -11
- package/plugins/devflow-core/skills/workflow-orchestrator/scripts/workflow_gate_contract.rb +5 -1
- package/plugins/devflow-core/skills/workflow-orchestrator/templates/feature-development.yaml +40 -31
- package/plugins/devflow-core/skills/workflow-pipeline/SKILL.md +11 -5
- package/plugins/devflow-core/skills/workflow-pipeline/references/pipeline-flow.md +6 -6
- package/plugins/devflow-implementation/skills/workflow-change-plan/SKILL.md +16 -1
- package/plugins/devflow-implementation/skills/workflow-change-plan/references/output-template.md +3 -1
- package/plugins/devflow-quality/agents/code-reviewer.md +7 -1
- package/plugins/devflow-quality/skills/code-review/SKILL.md +10 -0
- package/plugins/devflow-quality/skills/workflow-gen-test-guide/SKILL.md +37 -0
- package/plugins/devflow-quality/skills/workflow-gen-test-guide/legacy/SKILL.md +327 -0
- package/plugins/devflow-quality/skills/workflow-gen-test-guide/legacy/assets/output-template.md +213 -0
- package/plugins/devflow-quality/skills/workflow-gen-test-guide/legacy/references/bug-rules.md +9 -0
- package/plugins/devflow-quality/skills/workflow-gen-test-guide/legacy/references/coverage-checklist.md +63 -0
- package/plugins/devflow-quality/skills/workflow-gen-test-guide/legacy/references/exception-checklist.md +37 -0
- package/plugins/devflow-quality/skills/workflow-gen-test-guide/legacy/references/test-type-slots/api-test.md +58 -0
- package/plugins/devflow-quality/skills/workflow-gen-test-guide/legacy/references/test-type-slots/boundary-test.md +151 -0
- package/plugins/devflow-quality/skills/workflow-gen-test-guide/legacy/references/test-type-slots/common-test.md +49 -0
- package/plugins/devflow-quality/skills/workflow-gen-test-guide/legacy/references/test-type-slots/compatibility-test.md +105 -0
- package/plugins/devflow-quality/skills/workflow-gen-test-guide/legacy/references/test-type-slots/data-test.md +67 -0
- package/plugins/devflow-quality/skills/workflow-gen-test-guide/legacy/references/test-type-slots/exception-test.md +109 -0
- package/plugins/devflow-quality/skills/workflow-gen-test-guide/legacy/references/test-type-slots/function-test.md +65 -0
- package/plugins/devflow-quality/skills/workflow-gen-test-guide/legacy/references/test-type-slots/integration-test.md +36 -0
- package/plugins/devflow-quality/skills/workflow-gen-test-guide/legacy/references/test-type-slots/performance-test.md +100 -0
- package/plugins/devflow-quality/skills/workflow-gen-test-guide/legacy/references/test-type-slots/scenario-test.md +63 -0
- package/plugins/devflow-quality/skills/workflow-gen-test-guide/legacy/references/test-type-slots/security-test.md +55 -0
- package/plugins/devflow-quality/skills/workflow-gen-test-guide/legacy/references/test-type-slots/ui-test.md +49 -0
- package/plugins/devflow-quality/skills/workflow-gen-test-guide/legacy/references/test-type-slots/usability-test.md +46 -0
- package/plugins/devflow-quality/skills/workflow-gen-test-guide/legacy/references/writing-guide.md +111 -0
- package/src/constants.ts +1 -1
|
@@ -0,0 +1,238 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Read-only Core review preflight. Never runs an AI review or changes project files."""
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import argparse
|
|
6
|
+
import hashlib
|
|
7
|
+
import json
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
import subprocess
|
|
10
|
+
import sys
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def sha256(path: Path) -> str:
|
|
14
|
+
return hashlib.sha256(path.read_bytes()).hexdigest()
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def within(root: Path, relative: str) -> Path:
|
|
18
|
+
if not isinstance(relative, str) or not relative or Path(relative).is_absolute() or "\x00" in relative:
|
|
19
|
+
raise ValueError("invalid relative path")
|
|
20
|
+
path = (root / relative).resolve()
|
|
21
|
+
if not path.is_relative_to(root):
|
|
22
|
+
raise ValueError("path escapes project")
|
|
23
|
+
return path
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def load_workflow(path: Path) -> dict:
|
|
27
|
+
command = ["ruby", "-ryaml", "-rjson", "-e",
|
|
28
|
+
"puts JSON.generate(YAML.safe_load(File.read(ARGV.fetch(0)), permitted_classes: [], aliases: false))",
|
|
29
|
+
str(path)]
|
|
30
|
+
result = subprocess.run(command, text=True, capture_output=True, timeout=15, check=False)
|
|
31
|
+
if result.returncode:
|
|
32
|
+
raise ValueError(f"workflow cannot be read: {result.stderr.strip()}")
|
|
33
|
+
return json.loads(result.stdout)
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def load_state(path: Path) -> dict:
|
|
37
|
+
command = ["ruby", "-ryaml", "-rjson", "-e",
|
|
38
|
+
"front = File.read(ARGV.fetch(0)).match(/\\A---\\n(.*?)\\n---/m); "
|
|
39
|
+
"abort 'STATE front matter missing' unless front; "
|
|
40
|
+
"puts JSON.generate(YAML.safe_load(front[1], permitted_classes: [], aliases: false))",
|
|
41
|
+
str(path)]
|
|
42
|
+
result = subprocess.run(command, text=True, capture_output=True, timeout=15, check=False)
|
|
43
|
+
if result.returncode:
|
|
44
|
+
raise ValueError(f"STATE cannot be read: {result.stderr.strip()}")
|
|
45
|
+
return json.loads(result.stdout)
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def preflight(root: Path, feature: str, step: str, contract_path: str,
|
|
49
|
+
obligations: list[dict], review_agent: str | None = None,
|
|
50
|
+
review_contract_path: str | None = None, stage: str = "completion") -> dict:
|
|
51
|
+
root = root.resolve()
|
|
52
|
+
evidence, issues = [], []
|
|
53
|
+
if stage not in {"entry", "completion"}:
|
|
54
|
+
return {"status": "human_confirmation", "scope": "snapshot_preflight_only_not_gate_approval",
|
|
55
|
+
"evidence": [], "issues": [{"code": "INVALID_STAGE"}]}
|
|
56
|
+
try:
|
|
57
|
+
frozen_path = within(root, contract_path)
|
|
58
|
+
frozen = json.loads(frozen_path.read_text(encoding="utf-8"))
|
|
59
|
+
command = ["ruby", str(Path(__file__).with_name("execution_contract.rb")),
|
|
60
|
+
"inspect", "--feature", feature, "--step", step, "--json"]
|
|
61
|
+
run = subprocess.run(command, cwd=root, text=True, capture_output=True, timeout=15, check=False)
|
|
62
|
+
current_result = json.loads(run.stdout)
|
|
63
|
+
if run.returncode or not current_result.get("ok"):
|
|
64
|
+
return {"status": "human_confirmation", "evidence": evidence,
|
|
65
|
+
"issues": [{"code": "CURRENT_TRUTH_UNAVAILABLE", "detail": current_result.get("message", run.stderr)}]}
|
|
66
|
+
current = current_result["contract"]
|
|
67
|
+
evidence.extend([
|
|
68
|
+
{"kind": "state_graph", "state_revision": current["state_revision"],
|
|
69
|
+
"graph_revision": current["graph_revision"]},
|
|
70
|
+
{"kind": "contract", "path": contract_path, "sha256": sha256(frozen_path),
|
|
71
|
+
"contract_id": frozen.get("contract_id")},
|
|
72
|
+
])
|
|
73
|
+
if frozen != current:
|
|
74
|
+
return {"status": "human_confirmation", "evidence": evidence,
|
|
75
|
+
"issues": [{"code": "STALE_EXECUTION_CONTRACT",
|
|
76
|
+
"detail": "frozen contract differs from current Core snapshot; recreate and retry"}]}
|
|
77
|
+
if frozen.get("feature") != feature or frozen.get("step") != step:
|
|
78
|
+
issues.append({"code": "CONTRACT_TARGET_MISMATCH", "detail": "contract feature/step mismatch"})
|
|
79
|
+
workflow_path = within(root, current["workflow_path"])
|
|
80
|
+
if sha256(workflow_path) != current["workflow_fingerprint"]:
|
|
81
|
+
return {"status": "human_confirmation", "evidence": evidence,
|
|
82
|
+
"issues": [{"code": "STALE_WORKFLOW_SNAPSHOT"}]}
|
|
83
|
+
workflow = load_workflow(workflow_path)
|
|
84
|
+
state = load_state(within(root, ".planning/STATE.md"))
|
|
85
|
+
if state.get("revision") != current["state_revision"] or state.get("current_feature") != feature or state.get("current_step") != step:
|
|
86
|
+
return {"status": "human_confirmation", "evidence": evidence,
|
|
87
|
+
"issues": [{"code": "STALE_STATE_SNAPSHOT"}]}
|
|
88
|
+
step_definition = next((item for item in workflow.get("steps", [])
|
|
89
|
+
if isinstance(item, dict) and item.get("id") == step), None)
|
|
90
|
+
if step_definition is None:
|
|
91
|
+
raise ValueError("current workflow step unavailable")
|
|
92
|
+
render = lambda value: value.replace("{feature}", feature).replace("{phase}", str(state["current_phase"]))
|
|
93
|
+
derived = []
|
|
94
|
+
for raw in step_definition.get("inputs", []):
|
|
95
|
+
if isinstance(raw, str):
|
|
96
|
+
relative, required = raw, True
|
|
97
|
+
elif isinstance(raw, dict) and isinstance(raw.get("path"), str):
|
|
98
|
+
relative = raw["path"]
|
|
99
|
+
required = (raw.get("required", True) and raw.get("required_at_entry", True)) if stage == "entry" else raw.get("required_at_completion", raw.get("required", True))
|
|
100
|
+
else:
|
|
101
|
+
issues.append({"code": "INVALID_WORKFLOW_INPUT", "detail": "unrecognized input obligation"})
|
|
102
|
+
continue
|
|
103
|
+
if required and relative != ".planning/execution-contract.md":
|
|
104
|
+
derived.append({"path": render(relative), "producer": "existing"})
|
|
105
|
+
for item in step_definition.get("outputs", []):
|
|
106
|
+
if isinstance(item, dict) and item.get("required") is True and isinstance(item.get("path"), str):
|
|
107
|
+
derived.append({"path": render(item["path"]), "producer": "step"})
|
|
108
|
+
meta = step_definition.get("meta_output")
|
|
109
|
+
if isinstance(meta, dict) and isinstance(meta.get("path"), str):
|
|
110
|
+
derived.append({"path": render(meta["path"]), "producer": "step"})
|
|
111
|
+
evidence.append({"kind": "workflow", "path": current["workflow_path"],
|
|
112
|
+
"sha256": current["workflow_fingerprint"],
|
|
113
|
+
"required_obligation_count": len(derived),
|
|
114
|
+
"checker_ids": current.get("checker_ids", [])})
|
|
115
|
+
if step == "code-review":
|
|
116
|
+
graph_relative = f".planning/requirements/{feature}/governance.json"
|
|
117
|
+
graph_path = within(root, graph_relative)
|
|
118
|
+
if not graph_path.is_file():
|
|
119
|
+
issues.append({"code": "CONFIRMED_EXPECTATIONS_UNAVAILABLE", "path": graph_relative})
|
|
120
|
+
else:
|
|
121
|
+
graph = json.loads(graph_path.read_text(encoding="utf-8"))
|
|
122
|
+
stale = {entry.get("node_id") for entry in graph.get("stale", []) if isinstance(entry, dict)}
|
|
123
|
+
relevant = [node for node in graph.get("nodes", [])
|
|
124
|
+
if isinstance(node, dict) and node.get("kind") in {"requirement", "rule", "boundary", "acceptance"}]
|
|
125
|
+
unconfirmed = [node.get("id", "unknown") for node in relevant
|
|
126
|
+
if node.get("status") != "confirmed" or node.get("id") in stale
|
|
127
|
+
or not isinstance(node.get("confirmation"), dict)
|
|
128
|
+
or node["confirmation"].get("origin") != "core"
|
|
129
|
+
or not node["confirmation"].get("evidence")]
|
|
130
|
+
if graph.get("feature") != feature or graph.get("revision") != current["graph_revision"] or not relevant:
|
|
131
|
+
issues.append({"code": "CONFIRMED_EXPECTATIONS_UNAVAILABLE", "path": graph_relative})
|
|
132
|
+
elif unconfirmed:
|
|
133
|
+
issues.append({"code": "UNCONFIRMED_EXPECTATIONS", "path": graph_relative,
|
|
134
|
+
"node_ids": unconfirmed,
|
|
135
|
+
"detail": "Core review scope is not available; confirm or scope these nodes before task dispatch"})
|
|
136
|
+
else:
|
|
137
|
+
evidence.append({"kind": "confirmed_expectations", "path": graph_relative,
|
|
138
|
+
"sha256": sha256(graph_path), "graph_revision": graph["revision"],
|
|
139
|
+
"node_ids": [node["id"] for node in relevant]})
|
|
140
|
+
if not derived and not current.get("checker_ids"):
|
|
141
|
+
issues.append({"code": "NO_REVIEW_OBLIGATIONS", "detail": "Core workflow defines no review obligations"})
|
|
142
|
+
supplied = {item.get("path"): item for item in obligations if isinstance(item, dict)}
|
|
143
|
+
for item in derived:
|
|
144
|
+
supplement = supplied.get(item["path"], {})
|
|
145
|
+
if supplement.get("producer") not in (None, item["producer"]):
|
|
146
|
+
issues.append({"code": "OBLIGATION_PRODUCER_MISMATCH", "path": item["path"]})
|
|
147
|
+
item.update({key: value for key, value in supplement.items() if key == "sha256"})
|
|
148
|
+
obligations = derived + [item for item in obligations if not isinstance(item, dict) or item.get("path") not in {d["path"] for d in derived}]
|
|
149
|
+
allowed = set(current.get("allowed_paths", []))
|
|
150
|
+
for index, obligation in enumerate(obligations):
|
|
151
|
+
if not isinstance(obligation, dict) or not isinstance(obligation.get("path"), str):
|
|
152
|
+
issues.append({"code": "INVALID_OBLIGATION", "index": index})
|
|
153
|
+
continue
|
|
154
|
+
relative = obligation["path"]
|
|
155
|
+
try:
|
|
156
|
+
path = within(root, relative)
|
|
157
|
+
except ValueError as error:
|
|
158
|
+
issues.append({"code": "INVALID_OBLIGATION_PATH", "index": index, "detail": str(error)})
|
|
159
|
+
continue
|
|
160
|
+
producer = obligation.get("producer")
|
|
161
|
+
if producer == "existing":
|
|
162
|
+
if not path.is_file():
|
|
163
|
+
issues.append({"code": "MISSING_INPUT_FACT", "index": index, "path": relative})
|
|
164
|
+
elif obligation.get("sha256") is not None and obligation.get("sha256") != sha256(path):
|
|
165
|
+
issues.append({"code": "STALE_INPUT_FACT", "index": index, "path": relative})
|
|
166
|
+
else:
|
|
167
|
+
evidence.append({"kind": "input", "path": relative, "sha256": sha256(path)})
|
|
168
|
+
elif producer == "step":
|
|
169
|
+
if relative not in allowed:
|
|
170
|
+
issues.append({"code": "UNREACHABLE_OBLIGATION", "index": index, "path": relative,
|
|
171
|
+
"detail": "step contract does not allow producing this path"})
|
|
172
|
+
else:
|
|
173
|
+
evidence.append({"kind": "producer", "path": relative, "step": step,
|
|
174
|
+
"contract_id": current["contract_id"]})
|
|
175
|
+
if path.is_file():
|
|
176
|
+
evidence.append({"kind": "output_snapshot", "path": relative, "sha256": sha256(path)})
|
|
177
|
+
elif stage == "completion":
|
|
178
|
+
issues.append({"code": "MISSING_REVIEW_OUTPUT", "index": index, "path": relative})
|
|
179
|
+
else:
|
|
180
|
+
issues.append({"code": "UNKNOWN_PRODUCER", "index": index, "path": relative})
|
|
181
|
+
if step == "code-review" and review_agent not in (None, "devflow-code-reviewer"):
|
|
182
|
+
issues.append({"code": "REVIEW_AGENT_MISMATCH", "agent": review_agent,
|
|
183
|
+
"detail": "code-review requires the independent devflow-code-reviewer"})
|
|
184
|
+
if step == "code-review":
|
|
185
|
+
review_agent = "devflow-code-reviewer"
|
|
186
|
+
if review_agent:
|
|
187
|
+
reviewer_contract = current
|
|
188
|
+
if review_contract_path:
|
|
189
|
+
review_path = within(root, review_contract_path)
|
|
190
|
+
reviewer_contract = json.loads(review_path.read_text(encoding="utf-8"))
|
|
191
|
+
evidence.append({"kind": "review_contract", "path": review_contract_path,
|
|
192
|
+
"sha256": sha256(review_path)})
|
|
193
|
+
if reviewer_contract.get("feature") != feature or reviewer_contract.get("graph_revision") != current.get("graph_revision"):
|
|
194
|
+
issues.append({"code": "STALE_REVIEW_CONTRACT"})
|
|
195
|
+
# A separate step cannot be inspected while STATE points at the execution
|
|
196
|
+
# step. A supplied JSON file alone is not Core authority.
|
|
197
|
+
issues.append({"code": "REVIEW_CONTRACT_UNVERIFIED",
|
|
198
|
+
"detail": "separate review contract needs a Core-verified step transition"})
|
|
199
|
+
if review_agent not in reviewer_contract.get("allowed_agents", []) or reviewer_contract.get("tool_budget", 0) < 1:
|
|
200
|
+
issues.append({"code": "REVIEW_AGENT_UNREACHABLE", "agent": review_agent,
|
|
201
|
+
"detail": "review agent is not allowed or task budget is insufficient"})
|
|
202
|
+
except (OSError, ValueError, KeyError, TypeError, subprocess.TimeoutExpired) as error:
|
|
203
|
+
return {"status": "human_confirmation", "evidence": evidence,
|
|
204
|
+
"issues": [{"code": "PREFLIGHT_INPUT_UNAVAILABLE", "detail": str(error)}]}
|
|
205
|
+
conflict_codes = {"CONTRACT_TARGET_MISMATCH", "UNREACHABLE_OBLIGATION", "REVIEW_AGENT_MISMATCH",
|
|
206
|
+
"REVIEW_AGENT_UNREACHABLE", "STALE_REVIEW_CONTRACT"}
|
|
207
|
+
status = ("core_contract_conflict" if any(issue["code"] in conflict_codes for issue in issues)
|
|
208
|
+
else "human_confirmation" if issues else "ready")
|
|
209
|
+
return {"status": status, "scope": "snapshot_preflight_only_not_gate_approval",
|
|
210
|
+
"evidence": evidence, "issues": issues}
|
|
211
|
+
|
|
212
|
+
|
|
213
|
+
def main() -> int:
|
|
214
|
+
parser = argparse.ArgumentParser(description=__doc__)
|
|
215
|
+
parser.add_argument("--root", type=Path, default=Path.cwd())
|
|
216
|
+
parser.add_argument("--feature", required=True)
|
|
217
|
+
parser.add_argument("--step", required=True)
|
|
218
|
+
parser.add_argument("--contract", required=True, help="project-relative frozen contract JSON")
|
|
219
|
+
parser.add_argument("--obligations", required=True, help="JSON array: {path, producer: existing|step, sha256?}")
|
|
220
|
+
parser.add_argument("--review-agent")
|
|
221
|
+
parser.add_argument("--review-contract", help="separate project-relative reviewer contract JSON")
|
|
222
|
+
parser.add_argument("--stage", choices=["entry", "completion"], default="completion")
|
|
223
|
+
args = parser.parse_args()
|
|
224
|
+
try:
|
|
225
|
+
obligations = json.loads(args.obligations)
|
|
226
|
+
if not isinstance(obligations, list):
|
|
227
|
+
raise ValueError("obligations must be an array")
|
|
228
|
+
result = preflight(args.root.resolve(), args.feature, args.step, args.contract,
|
|
229
|
+
obligations, args.review_agent, args.review_contract, args.stage)
|
|
230
|
+
except ValueError as error:
|
|
231
|
+
result = {"status": "human_confirmation", "evidence": [],
|
|
232
|
+
"issues": [{"code": "INVALID_INPUT", "detail": str(error)}]}
|
|
233
|
+
print(json.dumps(result, ensure_ascii=False, sort_keys=True))
|
|
234
|
+
return 0 if result["status"] == "ready" else 2
|
|
235
|
+
|
|
236
|
+
|
|
237
|
+
if __name__ == "__main__":
|
|
238
|
+
sys.exit(main())
|
|
@@ -7,6 +7,7 @@ pretend that a source excerpt alone proves runtime behaviour.
|
|
|
7
7
|
from __future__ import annotations
|
|
8
8
|
|
|
9
9
|
import hashlib
|
|
10
|
+
import hmac
|
|
10
11
|
import json
|
|
11
12
|
import os
|
|
12
13
|
from pathlib import Path
|
|
@@ -36,6 +37,29 @@ def canonical(value: Any) -> str:
|
|
|
36
37
|
return json.dumps(value, ensure_ascii=False, sort_keys=True, separators=(",", ":"))
|
|
37
38
|
|
|
38
39
|
|
|
40
|
+
def verified_review_binding(request: dict) -> tuple[bool, str]:
|
|
41
|
+
"""A completed reviewer task, not the caller, owns the submitted report."""
|
|
42
|
+
binding = request.get("core_review_binding")
|
|
43
|
+
secret = os.environ.get("DEVFLOW_REVIEW_BINDING_KEY", "")
|
|
44
|
+
if not isinstance(binding, dict) or not secret:
|
|
45
|
+
return False, "missing Core-bound independent reviewer task"
|
|
46
|
+
fields = ("agent", "feature", "contract_id", "task_call_id", "report_sha256", "signature")
|
|
47
|
+
if any(not isinstance(binding.get(field), str) or not binding[field] for field in fields):
|
|
48
|
+
return False, "incomplete Core reviewer task binding"
|
|
49
|
+
target = request.get("target")
|
|
50
|
+
if binding["agent"] != "devflow-code-reviewer" or not isinstance(target, dict) or binding["feature"] != target.get("feature"):
|
|
51
|
+
return False, "reviewer identity or feature does not match the report"
|
|
52
|
+
unsigned_report = {key: value for key, value in request.items() if key != "core_review_binding"}
|
|
53
|
+
report_hash = digest(canonical(unsigned_report).encode())
|
|
54
|
+
if not hmac.compare_digest(report_hash, binding["report_sha256"]):
|
|
55
|
+
return False, "review report differs from the completed reviewer task"
|
|
56
|
+
unsigned_binding = {key: binding[key] for key in fields if key != "signature"}
|
|
57
|
+
expected = hmac.new(secret.encode(), canonical(unsigned_binding).encode(), hashlib.sha256).hexdigest()
|
|
58
|
+
if not hmac.compare_digest(expected, binding["signature"]):
|
|
59
|
+
return False, "invalid Core reviewer task signature"
|
|
60
|
+
return True, ""
|
|
61
|
+
|
|
62
|
+
|
|
39
63
|
def safe_file(root: Path, relative: str) -> Path:
|
|
40
64
|
if not isinstance(relative, str) or not relative or Path(relative).is_absolute() or "\x00" in relative:
|
|
41
65
|
raise ValueError("path must be a nonempty project-relative path")
|
|
@@ -136,6 +160,10 @@ def db_open(root: Path) -> sqlite3.Connection:
|
|
|
136
160
|
id INTEGER PRIMARY KEY, chain_id TEXT, status TEXT NOT NULL, detail_json TEXT NOT NULL,
|
|
137
161
|
created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP
|
|
138
162
|
);
|
|
163
|
+
CREATE TABLE IF NOT EXISTS reviewer_tasks (
|
|
164
|
+
task_call_id TEXT PRIMARY KEY, chain_id TEXT NOT NULL, round INTEGER NOT NULL,
|
|
165
|
+
report_sha256 TEXT NOT NULL
|
|
166
|
+
);
|
|
139
167
|
""")
|
|
140
168
|
return db
|
|
141
169
|
|
|
@@ -221,6 +249,35 @@ def verify_core_result(root: Path, feature: str, evidence: Any, expected_status:
|
|
|
221
249
|
return False, f"unreadable Core acceptance result: {exc}"
|
|
222
250
|
|
|
223
251
|
|
|
252
|
+
def verify_core_impact(root: Path, feature: str, evidence: dict, impact: str, trigger: str) -> tuple[bool, str]:
|
|
253
|
+
"""A failed TC alone does not establish the reviewer's causal/impact label."""
|
|
254
|
+
try:
|
|
255
|
+
suite = read_json(safe_file(root, f".planning/requirements/{feature}/tests.json"))
|
|
256
|
+
node = next((n for n in suite.get("nodes", []) if isinstance(n, dict) and n.get("id") == evidence.get("tc_id")), None)
|
|
257
|
+
data = node.get("data", {}) if node else {}
|
|
258
|
+
if data.get("review_impact") == impact and data.get("review_trigger") == trigger:
|
|
259
|
+
return True, ""
|
|
260
|
+
return False, "Core frozen TC does not confirm the claimed failure trigger and business impact"
|
|
261
|
+
except (OSError, ValueError, TypeError, json.JSONDecodeError) as exc:
|
|
262
|
+
return False, f"Core frozen impact cannot be verified: {exc}"
|
|
263
|
+
|
|
264
|
+
|
|
265
|
+
def declared_change_paths(root: Path, feature: str) -> tuple[set[str], str, str]:
|
|
266
|
+
relative = f".planning/features/{feature}/变更清单.md"
|
|
267
|
+
source = safe_file(root, relative)
|
|
268
|
+
declarations = re.findall(r"^\s*-\s*\*\*文件\*\*\s*[::]\s*(.+)$", source.read_text(encoding="utf-8"), re.MULTILINE)
|
|
269
|
+
paths = set()
|
|
270
|
+
for declaration in declarations:
|
|
271
|
+
matches = re.findall(r"`([^`]+)`", declaration)
|
|
272
|
+
if len(matches) != 1 or declaration.strip() != f"`{matches[0]}`":
|
|
273
|
+
raise ValueError("ambiguous change-list file declaration")
|
|
274
|
+
safe_file(root, matches[0])
|
|
275
|
+
paths.add(matches[0])
|
|
276
|
+
if not paths:
|
|
277
|
+
raise ValueError("change list has no unambiguous source file declarations")
|
|
278
|
+
return paths, relative, digest(source.read_bytes())
|
|
279
|
+
|
|
280
|
+
|
|
224
281
|
def verify_coverage(root: Path, feature: str, required: list[str], coverage_evidence: Any) -> tuple[set[str], list[str]]:
|
|
225
282
|
if not isinstance(coverage_evidence, dict):
|
|
226
283
|
return set(), ["Each review surface and scenario needs a distinct Core TC binding"]
|
|
@@ -241,7 +298,10 @@ def verify_coverage(root: Path, feature: str, required: list[str], coverage_evid
|
|
|
241
298
|
suite = read_json(safe_file(root, f".planning/requirements/{feature}/tests.json"))
|
|
242
299
|
node = next((n for n in suite.get("nodes", []) if isinstance(n, dict) and n.get("id") == ref["tc_id"]), None)
|
|
243
300
|
types = {"scenario:normal": {"api", "page"}, "scenario:unauthorized": {"permission"}, "scenario:failure_rollback": {"resilience", "boundary"}, "scenario:compatibility": {"migration"}}
|
|
244
|
-
|
|
301
|
+
data = node.get("data", {}) if node else {}
|
|
302
|
+
explicit = tag in data.get("review_coverage_tags", []) if isinstance(data.get("review_coverage_tags"), list) else False
|
|
303
|
+
typed = data.get("test_type") in types.get(tag, set())
|
|
304
|
+
if not node or node.get("kind") != "test_case" or node.get("status") != "confirmed" or not (explicit or typed):
|
|
245
305
|
issues.append(f"{tag}: frozen TC type cannot prove this coverage obligation; request human confirmation")
|
|
246
306
|
continue
|
|
247
307
|
covered.add(tag)
|
|
@@ -256,10 +316,139 @@ def suite_has_failures(root: Path, feature: str) -> bool:
|
|
|
256
316
|
return True
|
|
257
317
|
|
|
258
318
|
|
|
319
|
+
def gate_status(root: Path, feature: str, contract_id: str | None = None) -> dict:
|
|
320
|
+
"""Read only the Core ledger; prose reports never confer approval."""
|
|
321
|
+
root = root.resolve(strict=True)
|
|
322
|
+
if not contract_id:
|
|
323
|
+
return {"status": "human_confirmation", "reason": "Current Core review contract identity is required"}
|
|
324
|
+
try:
|
|
325
|
+
contract = read_json(safe_file(root, f".planning/contracts/{feature}/{contract_id}.json"))
|
|
326
|
+
if contract.get("origin") != "core_execution_contract" or contract.get("step") != "code-review" or contract.get("feature") != feature or contract.get("contract_id") != contract_id:
|
|
327
|
+
return {"status": "core_contract_conflict", "reason": "Review contract identity is not a frozen Core code-review contract"}
|
|
328
|
+
sys.path.insert(0, str(Path(__file__).resolve().parent))
|
|
329
|
+
from review_preflight import load_state
|
|
330
|
+
state = load_state(safe_file(root, ".planning/STATE.md"))
|
|
331
|
+
graph = read_json(safe_file(root, f".planning/requirements/{feature}/governance.json"))
|
|
332
|
+
workflow = safe_file(root, contract["workflow_path"])
|
|
333
|
+
if (state.get("current_feature") != feature or state.get("current_step") != "code-review"
|
|
334
|
+
or state.get("revision") != contract.get("state_revision")
|
|
335
|
+
or graph.get("revision") != contract.get("graph_revision")
|
|
336
|
+
or digest(workflow.read_bytes()) != contract.get("workflow_fingerprint")):
|
|
337
|
+
return {"status": "stale_evidence", "reason": "Review contract differs from current Core state, graph, or workflow"}
|
|
338
|
+
except (OSError, ValueError, KeyError, TypeError, ImportError, json.JSONDecodeError) as exc:
|
|
339
|
+
return {"status": "human_confirmation", "reason": f"Current Core review contract cannot be verified: {exc}"}
|
|
340
|
+
database = root / ".planning" / "review-runs" / "review-runs.sqlite"
|
|
341
|
+
if not database.is_file() or database.is_symlink():
|
|
342
|
+
return {"status": "human_confirmation", "reason": "No Core review round exists"}
|
|
343
|
+
db = sqlite3.connect(f"file:{database}?mode=ro", uri=True)
|
|
344
|
+
try:
|
|
345
|
+
rows = db.execute("SELECT result_json FROM rounds ORDER BY created_at DESC, rowid DESC").fetchall()
|
|
346
|
+
finally:
|
|
347
|
+
db.close()
|
|
348
|
+
for (raw,) in rows:
|
|
349
|
+
result = json.loads(raw)
|
|
350
|
+
task = result.get("reviewer_task", {})
|
|
351
|
+
if task.get("feature") != feature:
|
|
352
|
+
continue
|
|
353
|
+
if task.get("contract_id") != contract_id:
|
|
354
|
+
continue
|
|
355
|
+
report_path = next((item for item in contract.get("allowed_paths", []) if isinstance(item, str) and item.endswith("/REVIEW.md")), None)
|
|
356
|
+
if not report_path:
|
|
357
|
+
return {"status": "core_contract_conflict", "reason": "Core contract has no REVIEW.md output"}
|
|
358
|
+
try:
|
|
359
|
+
if safe_file(root, report_path).read_text(encoding="utf-8") != review_markdown(result):
|
|
360
|
+
return {"status": "core_contract_conflict", "reason": "REVIEW.md differs from Core review round"}
|
|
361
|
+
except (OSError, ValueError):
|
|
362
|
+
return {"status": "core_contract_conflict", "reason": "Core-derived REVIEW.md is missing"}
|
|
363
|
+
if task.get("agent") != "devflow-code-reviewer":
|
|
364
|
+
return {"status": "core_contract_conflict", "reason": "Review round lacks independent reviewer"}
|
|
365
|
+
fixed = result.get("fixed_versions", {})
|
|
366
|
+
index_generation = fixed.get("code_index_generation")
|
|
367
|
+
if index_generation is not None:
|
|
368
|
+
try:
|
|
369
|
+
snapshot = read_json(safe_file(root, ".planning/code-index/SNAPSHOT.json"))
|
|
370
|
+
if snapshot.get("generation") != index_generation:
|
|
371
|
+
return {"status": "stale_evidence", "reason": "Code-index generation changed after review", "chain_id": result.get("chain_id")}
|
|
372
|
+
except (OSError, ValueError, json.JSONDecodeError):
|
|
373
|
+
return {"status": "stale_evidence", "reason": "Code-index snapshot unavailable after review", "chain_id": result.get("chain_id")}
|
|
374
|
+
for relative, expected in fixed.get("source", {}).items():
|
|
375
|
+
try:
|
|
376
|
+
if digest(safe_file(root, relative).read_bytes()) != expected:
|
|
377
|
+
return {"status": "stale_evidence", "reason": f"Source changed after review: {relative}", "chain_id": result.get("chain_id")}
|
|
378
|
+
except (OSError, ValueError):
|
|
379
|
+
return {"status": "stale_evidence", "reason": f"Source unavailable after review: {relative}", "chain_id": result.get("chain_id")}
|
|
380
|
+
for authority in fixed.get("authority", []):
|
|
381
|
+
try:
|
|
382
|
+
if digest(safe_file(root, authority["authority_path"]).read_bytes()) != authority["authority_sha256"]:
|
|
383
|
+
return {"status": "stale_evidence", "reason": "Authority changed after review", "chain_id": result.get("chain_id")}
|
|
384
|
+
except (OSError, ValueError, KeyError):
|
|
385
|
+
return {"status": "stale_evidence", "reason": "Authority unavailable after review", "chain_id": result.get("chain_id")}
|
|
386
|
+
for relative, expected in fixed.get("core_results", {}).items():
|
|
387
|
+
try:
|
|
388
|
+
if digest(safe_file(root, relative).read_bytes()) != expected:
|
|
389
|
+
return {"status": "stale_evidence", "reason": f"Core test evidence changed after review: {relative}", "chain_id": result.get("chain_id")}
|
|
390
|
+
except (OSError, ValueError):
|
|
391
|
+
return {"status": "stale_evidence", "reason": f"Core test evidence unavailable: {relative}", "chain_id": result.get("chain_id")}
|
|
392
|
+
plan = fixed.get("change_plan", {})
|
|
393
|
+
try:
|
|
394
|
+
if not plan or digest(safe_file(root, plan["path"]).read_bytes()) != plan["sha256"]:
|
|
395
|
+
return {"status": "stale_evidence", "reason": "Core change-list scope changed after review", "chain_id": result.get("chain_id")}
|
|
396
|
+
except (OSError, ValueError, KeyError, TypeError):
|
|
397
|
+
return {"status": "stale_evidence", "reason": "Core change-list scope unavailable", "chain_id": result.get("chain_id")}
|
|
398
|
+
status = result.get("status")
|
|
399
|
+
verdict = "pass" if status == "clean" and result.get("automatic_release_allowed") else ("terminate_task" if status == "terminated_p0" else "repair")
|
|
400
|
+
return {"status": verdict, "chain_id": result.get("chain_id"), "round": result.get("round"), "evidence_chain": {"reviewer_task": task, "fixed_versions": fixed, "coverage": result.get("coverage")}, "open_findings": [f for f in result.get("findings", []) if f.get("state") != "resolved"], "next_action": "advance" if verdict == "pass" else ("stop_current_task" if verdict == "terminate_task" else "repair_and_re_review")}
|
|
401
|
+
return {"status": "human_confirmation", "reason": "No Core review round exists for this feature and contract"}
|
|
402
|
+
|
|
403
|
+
|
|
404
|
+
def review_markdown(result: dict) -> str:
|
|
405
|
+
lines = ["# Code Review", "", "> Core-derived view; the review ledger is authoritative.", "", f"- Chain: `{result['chain_id']}`", f"- Round: {result['round']}", f"- Decision: `{result['status']}`", f"- Reviewer task: `{result['reviewer_task']['task_call_id']}`", "", "## Open findings", ""]
|
|
406
|
+
for finding in result.get("findings", []):
|
|
407
|
+
if finding.get("state") != "resolved":
|
|
408
|
+
source = finding.get("source", {})
|
|
409
|
+
lines.append(f"- `{finding['finding_id']}` {finding['severity']}: {source.get('path')}:{source.get('line')} — {finding.get('actual', '')}")
|
|
410
|
+
if lines[-1] == "":
|
|
411
|
+
lines.append("- None")
|
|
412
|
+
lines.extend(["", "## Next action", "", "Advance to the next workflow step." if result.get("status") == "clean" else "Repair findings and request a new independent review.", ""])
|
|
413
|
+
return "\n".join(lines)
|
|
414
|
+
|
|
415
|
+
|
|
416
|
+
def render_review(root: Path, feature: str, contract_id: str) -> dict:
|
|
417
|
+
root = root.resolve(strict=True)
|
|
418
|
+
contract = read_json(safe_file(root, f".planning/contracts/{feature}/{contract_id}.json"))
|
|
419
|
+
path = next((item for item in contract.get("allowed_paths", []) if isinstance(item, str) and item.endswith("/REVIEW.md")), None)
|
|
420
|
+
if not path:
|
|
421
|
+
return {"status": "core_contract_conflict", "reason": "Core contract has no REVIEW.md output"}
|
|
422
|
+
database = root / ".planning/review-runs/review-runs.sqlite"
|
|
423
|
+
if not database.is_file():
|
|
424
|
+
return {"status": "human_confirmation", "reason": "No Core review round exists"}
|
|
425
|
+
db = sqlite3.connect(f"file:{database}?mode=ro", uri=True)
|
|
426
|
+
try:
|
|
427
|
+
rows = db.execute("SELECT result_json FROM rounds ORDER BY created_at DESC, rowid DESC").fetchall()
|
|
428
|
+
finally:
|
|
429
|
+
db.close()
|
|
430
|
+
for (raw,) in rows:
|
|
431
|
+
result = json.loads(raw)
|
|
432
|
+
task = result.get("reviewer_task", {})
|
|
433
|
+
if task.get("feature") == feature and task.get("contract_id") == contract_id:
|
|
434
|
+
destination = root / path
|
|
435
|
+
if destination.is_symlink() or not destination.resolve().is_relative_to(root):
|
|
436
|
+
return {"status": "core_contract_conflict", "reason": "Unsafe review output path"}
|
|
437
|
+
destination.parent.mkdir(parents=True, exist_ok=True)
|
|
438
|
+
temporary = destination.with_name(destination.name + ".core-tmp")
|
|
439
|
+
if temporary.exists() or temporary.is_symlink():
|
|
440
|
+
return {"status": "core_contract_conflict", "reason": "Review output temporary path is occupied"}
|
|
441
|
+
temporary.write_text(review_markdown(result), encoding="utf-8")
|
|
442
|
+
os.replace(temporary, destination)
|
|
443
|
+
return {"status": "rendered", "path": path, "chain_id": result["chain_id"], "round": result["round"]}
|
|
444
|
+
return {"status": "human_confirmation", "reason": "No Core review round exists for this contract"}
|
|
445
|
+
|
|
446
|
+
|
|
259
447
|
def run(root: Path, request: dict) -> dict:
|
|
260
448
|
root = root.resolve(strict=True)
|
|
261
449
|
if not isinstance(request, dict):
|
|
262
450
|
raise ValueError("request must be an object")
|
|
451
|
+
bound, binding_issue = verified_review_binding(request)
|
|
263
452
|
if "round" in request or "findings" in request or "severity" in request:
|
|
264
453
|
raise ValueError("Core assigns rounds and findings; caller-supplied authority fields are forbidden")
|
|
265
454
|
target = request.get("target")
|
|
@@ -275,6 +464,9 @@ def run(root: Path, request: dict) -> dict:
|
|
|
275
464
|
raise ValueError("review_plan.questions must be nonempty")
|
|
276
465
|
if not all(isinstance(q, str) and q.strip() for q in plan["questions"]):
|
|
277
466
|
raise ValueError("review questions must be strings")
|
|
467
|
+
review_questions = request.get("review_questions", [])
|
|
468
|
+
if not isinstance(review_questions, list) or not all(isinstance(q, str) and q.strip() for q in review_questions):
|
|
469
|
+
raise ValueError("review_questions must be a list of nonempty strings")
|
|
278
470
|
surfaces, scenarios = plan.get("surfaces"), plan.get("required_scenarios")
|
|
279
471
|
if not isinstance(surfaces, list) or not set(surfaces) <= SURFACES or not isinstance(scenarios, list) or not set(scenarios) <= SCENARIOS:
|
|
280
472
|
raise ValueError("review plan uses an unsupported surface or scenario")
|
|
@@ -284,6 +476,14 @@ def run(root: Path, request: dict) -> dict:
|
|
|
284
476
|
if not isinstance(expectations, list):
|
|
285
477
|
raise ValueError("expectations must be a list")
|
|
286
478
|
accepted, questions = validate_authority(root, target["feature"], expectations)
|
|
479
|
+
try:
|
|
480
|
+
declared_paths, change_plan_path, change_plan_hash = declared_change_paths(root, target["feature"])
|
|
481
|
+
missing_paths = declared_paths - set(paths)
|
|
482
|
+
if missing_paths:
|
|
483
|
+
questions.append("Review target omits change-list files: " + ", ".join(sorted(missing_paths)))
|
|
484
|
+
except (OSError, ValueError, UnicodeError) as exc:
|
|
485
|
+
declared_paths, change_plan_path, change_plan_hash = set(), None, None
|
|
486
|
+
questions.append(f"Core change-list review scope cannot be verified: {exc}")
|
|
287
487
|
chain_id = request.get("chain_id")
|
|
288
488
|
if chain_id is not None and (not isinstance(chain_id, str) or not chain_id.startswith("rev_")):
|
|
289
489
|
raise ValueError("invalid chain_id")
|
|
@@ -311,11 +511,14 @@ def run(root: Path, request: dict) -> dict:
|
|
|
311
511
|
raise ValueError("three completed review rounds exhausted")
|
|
312
512
|
else:
|
|
313
513
|
round_no = 1
|
|
314
|
-
if questions:
|
|
315
|
-
result = response("awaiting_human_confirmation", chain_id, round_no, questions=questions)
|
|
514
|
+
if questions or review_questions:
|
|
515
|
+
result = response("awaiting_human_confirmation", chain_id, round_no, questions=[*questions, *review_questions])
|
|
316
516
|
db.execute("INSERT INTO events(chain_id,status,detail_json) VALUES(?,?,?)", (chain_id, result["status"], canonical(result)))
|
|
317
517
|
db.commit()
|
|
318
518
|
return result
|
|
519
|
+
binding = request.get("core_review_binding") if bound else None
|
|
520
|
+
if bound and db.execute("SELECT 1 FROM reviewer_tasks WHERE task_call_id=?", (binding["task_call_id"],)).fetchone():
|
|
521
|
+
raise ValueError("reviewer task was already consumed by another review round")
|
|
319
522
|
if not chain_id:
|
|
320
523
|
# A new ID cannot erase an unresolved P0 for the same target and authority.
|
|
321
524
|
for old_id, raw in db.execute("SELECT c.chain_id,r.result_json FROM chains c JOIN rounds r ON r.chain_id=c.chain_id AND r.round=(SELECT MAX(round) FROM rounds WHERE chain_id=c.chain_id) WHERE c.target_key=? AND c.authority_key=?", (target_key, authority_key)):
|
|
@@ -370,6 +573,10 @@ def run(root: Path, request: dict) -> dict:
|
|
|
370
573
|
valid, reason = verify_core_result(root, target["feature"], item.get("runtime_evidence"), "failed", authority_node_id)
|
|
371
574
|
if not valid:
|
|
372
575
|
evidence_issues.append(f"{evidence['path']}:{evidence['line']}: {reason}")
|
|
576
|
+
else:
|
|
577
|
+
causal, reason = verify_core_impact(root, target["feature"], item["runtime_evidence"], item["impact"], item.get("trigger"))
|
|
578
|
+
if not causal:
|
|
579
|
+
evidence_issues.append(f"{evidence['path']}:{evidence['line']}: {reason}; human confirmation required")
|
|
373
580
|
fingerprint = digest(canonical([item["expectation_id"], evidence["path"], evidence["line"], item["impact"], item.get("trigger")]).encode())[:20]
|
|
374
581
|
findings.append({"finding_id": "F-" + fingerprint, "expectation_id": item["expectation_id"], "severity": IMPACTS[item["impact"]], "severity_source": "reviewer_claim", "impact": item["impact"], "source": evidence, "actual": item.get("actual"), "trigger": item.get("trigger"), "reproduction": item.get("reproduction"), "improvement": item.get("improvement"), "confidence": "source_and_core_tc_verified_causality_unverified" if item.get("runtime_evidence") else "source_verified_inference", "epistemic_status": "reviewer_interpretation", "verified_boundaries": ["approved_core_graph_expectation", "source_line_and_hash", "core_tc_identity_and_status"] if item.get("runtime_evidence") else ["approved_core_graph_expectation", "source_line_and_hash"]})
|
|
375
582
|
gaps = sorted(set(required) - covered)
|
|
@@ -390,13 +597,12 @@ def run(root: Path, request: dict) -> dict:
|
|
|
390
597
|
evidence_issues.append(f"{old_id}: source unchanged; cannot resolve by removing a report observation")
|
|
391
598
|
else:
|
|
392
599
|
regression = request.get("regression_evidence", {}).get(old_id)
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
evidence_issues.append(f"{old_id}: missing verified regression evidence")
|
|
600
|
+
authority_node_id = next((e["node_id"] for e in accepted if e["id"] == old.get("expectation_id")), None)
|
|
601
|
+
valid, reason = verify_core_result(root, target["feature"], regression, "passed", authority_node_id)
|
|
602
|
+
if not valid:
|
|
603
|
+
evidence_issues.append(f"{old_id}: missing Core passed regression TC: {reason}")
|
|
604
|
+
else:
|
|
605
|
+
findings.append({**old, "state": "resolved"})
|
|
400
606
|
for rel, start_hash in versions.items():
|
|
401
607
|
if digest(safe_file(root, rel).read_bytes()) != start_hash:
|
|
402
608
|
result = response("stale_evidence", chain_id, round_no, questions=[f"Source changed during review: {rel}"])
|
|
@@ -424,33 +630,24 @@ def run(root: Path, request: dict) -> dict:
|
|
|
424
630
|
return result
|
|
425
631
|
open_findings = [f for f in findings if f["state"] != "resolved"]
|
|
426
632
|
summary = {p: sum(f["severity"] == p for f in open_findings) for p in ("P0", "P1", "P2", "P3")}
|
|
427
|
-
if
|
|
428
|
-
|
|
429
|
-
# causal story or impact category is true. Until Core has a
|
|
430
|
-
# verifiable independent-review attestation, these are proposals,
|
|
431
|
-
# not completed rounds that can terminate a task after three tries.
|
|
432
|
-
result = response(
|
|
433
|
-
"awaiting_human_confirmation", chain_id, round_no,
|
|
434
|
-
questions=["Independent reviewer identity and business causality/impact require confirmation"],
|
|
435
|
-
proposed_findings=open_findings,
|
|
436
|
-
proposed_risk_summary=summary,
|
|
437
|
-
proposed_improvement_plan=[{"finding_id": f["finding_id"], "severity": f["severity"], "action": f["improvement"]} for f in open_findings],
|
|
438
|
-
coverage={"covered": sorted(covered), "gaps": gaps},
|
|
439
|
-
)
|
|
633
|
+
if not bound:
|
|
634
|
+
result = response("awaiting_human_confirmation", chain_id, round_no, questions=[binding_issue], proposed_findings=open_findings, proposed_risk_summary=summary, coverage={"covered": sorted(covered), "gaps": gaps})
|
|
440
635
|
db.execute("INSERT INTO events(chain_id,status,detail_json) VALUES(?,?,?)", (chain_id, result["status"], canonical(result)))
|
|
441
636
|
db.commit()
|
|
442
637
|
return result
|
|
443
|
-
if not
|
|
638
|
+
if not open_findings:
|
|
444
639
|
reasons = [*evidence_issues]
|
|
445
640
|
if suite_has_failures(root, target["feature"]):
|
|
446
641
|
reasons.append("The same Core acceptance run contains failed or blocked TCs; clean cannot be inferred from selected passed cases")
|
|
447
|
-
reasons
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
status = "terminated_p0" if round_no == 3 and summary["P0"] else ("findings" if
|
|
453
|
-
|
|
642
|
+
if reasons:
|
|
643
|
+
result = response("awaiting_human_confirmation", chain_id, round_no, questions=reasons, coverage={"covered": sorted(covered), "gaps": gaps})
|
|
644
|
+
db.execute("INSERT INTO events(chain_id,status,detail_json) VALUES(?,?,?)", (chain_id, result["status"], canonical(result)))
|
|
645
|
+
db.commit()
|
|
646
|
+
return result
|
|
647
|
+
status = "terminated_p0" if round_no == 3 and summary["P0"] else ("findings" if open_findings else "clean")
|
|
648
|
+
core_results = {rel: digest(safe_file(root, rel).read_bytes()) for rel in (f".planning/requirements/{target['feature']}/tests.json", f".planning/test-runs/{target['feature']}/acceptance-results.json")}
|
|
649
|
+
result = response(status, chain_id, round_no, fixed_versions={"source": versions, "authority": accepted, "core_results": core_results, "change_plan": {"path": change_plan_path, "sha256": change_plan_hash, "declared_paths": sorted(declared_paths)}, "code_index_generation": index_discovery.get("generation")}, index_discovery=index_discovery, coverage={"covered": sorted(covered), "gaps": gaps, "index_gap": index_discovery.get("reason") if index_discovery["status"] != "located" else None}, findings=findings, risk_summary=summary, improvement_plan=[{"finding_id": f["finding_id"], "severity": f["severity"], "action": f["improvement"]} for f in sorted(open_findings, key=lambda x: x["severity"])], all_p0_resolved=summary["P0"] == 0, severity_source="independent_reviewer_core_evidence_bound", semantic_causality_verified=True, automatic_release_allowed=status == "clean", termination_scope="current_task_only" if status == "terminated_p0" else None, reviewer_task={key: binding[key] for key in ("agent", "feature", "contract_id", "task_call_id", "report_sha256")}, history=[{"round": r, "status": json.loads(raw)["status"]} for r, raw in db.execute("SELECT round,result_json FROM rounds WHERE chain_id=? ORDER BY round", (chain_id,))])
|
|
650
|
+
db.execute("INSERT INTO reviewer_tasks(task_call_id,chain_id,round,report_sha256) VALUES(?,?,?,?)", (binding["task_call_id"], chain_id, round_no, binding["report_sha256"]))
|
|
454
651
|
db.execute("INSERT INTO rounds(chain_id,round,result_json) VALUES(?,?,?)", (chain_id, round_no, canonical(result)))
|
|
455
652
|
if status == "terminated_p0":
|
|
456
653
|
db.execute("UPDATE chains SET terminal=? WHERE chain_id=?", (status, chain_id))
|
|
@@ -466,7 +663,12 @@ def run(root: Path, request: dict) -> dict:
|
|
|
466
663
|
if __name__ == "__main__":
|
|
467
664
|
try:
|
|
468
665
|
project = Path(sys.argv[1] if len(sys.argv) > 1 else os.getcwd())
|
|
469
|
-
|
|
666
|
+
if len(sys.argv) > 3 and sys.argv[2] == "--gate":
|
|
667
|
+
print(canonical(gate_status(project.resolve(strict=True), sys.argv[3], sys.argv[4] if len(sys.argv) > 4 else None)))
|
|
668
|
+
elif len(sys.argv) > 4 and sys.argv[2] == "--render":
|
|
669
|
+
print(canonical(render_review(project, sys.argv[3], sys.argv[4])))
|
|
670
|
+
else:
|
|
671
|
+
print(canonical(run(project, json.load(sys.stdin))))
|
|
470
672
|
except (ValueError, OSError, json.JSONDecodeError, sqlite3.Error, StaleEvidence) as error:
|
|
471
673
|
print(canonical({"status": "error", "error": str(error)}))
|
|
472
674
|
sys.exit(2)
|