prizmkit 1.1.124 → 1.1.126
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/bundled/VERSION.json +3 -3
- package/bundled/adapters/claude/command-adapter.js +4 -6
- package/bundled/dev-pipeline/prizmkit_runtime/reset.py +49 -3
- package/bundled/dev-pipeline/prizmkit_runtime/runners.py +75 -14
- package/bundled/dev-pipeline/prizmkit_runtime/runtime_helper.py +37 -0
- package/bundled/dev-pipeline/prizmkit_runtime/task_checkout.py +267 -0
- package/bundled/dev-pipeline/scripts/generate-recovery-prompt.py +4 -4
- package/bundled/dev-pipeline/templates/bootstrap-prompt.md +1 -1
- package/bundled/dev-pipeline/templates/bootstrap-tier3.md +5 -16
- package/bundled/dev-pipeline/templates/bugfix-bootstrap-prompt.md +6 -14
- package/bundled/dev-pipeline/templates/refactor-bootstrap-prompt.md +8 -4
- package/bundled/dev-pipeline/templates/sections/bugfix-phase-review.md +9 -8
- package/bundled/dev-pipeline/templates/sections/phase-review-agent.md +12 -15
- package/bundled/dev-pipeline/templates/sections/phase-review-full.md +11 -16
- package/bundled/dev-pipeline/templates/sections/refactor-phase-review.md +9 -8
- package/bundled/dev-pipeline/tests/test_generate_bootstrap_prompt.py +8 -4
- package/bundled/dev-pipeline/tests/test_generate_bugfix_prompt.py +1 -1
- package/bundled/dev-pipeline/tests/test_generate_refactor_prompt.py +1 -1
- package/bundled/dev-pipeline/tests/test_python_runner_parity.py +206 -3
- package/bundled/dev-pipeline/tests/test_runtime_helper.py +36 -0
- package/bundled/dev-pipeline/tests/test_unified_cli.py +21 -12
- package/bundled/skills/_metadata.json +4 -4
- package/bundled/skills/prizmkit-code-review/SKILL.md +130 -103
- package/bundled/skills/prizmkit-code-review/assets/reviewer-role.json +3 -5
- package/bundled/skills/prizmkit-code-review/references/review-report-template.md +89 -38
- package/bundled/skills/prizmkit-code-review/references/reviewer-agent-prompt.md +44 -62
- package/bundled/skills/prizmkit-code-review/scripts/render_review_report.py +249 -143
- package/package.json +1 -1
- package/bundled/skills/prizmkit-code-review/references/reviewer-execution-protocol.md +0 -179
- package/bundled/skills/prizmkit-code-review/references/reviewer-workspace-protocol.md +0 -118
- package/bundled/skills/prizmkit-code-review/scripts/check_loop.py +0 -380
- package/bundled/skills/prizmkit-code-review/scripts/workspace_snapshot.py +0 -222
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Reviewer Prompt Contract
|
|
2
2
|
|
|
3
|
-
Use this complete template for every Reviewer execution
|
|
3
|
+
Use this complete template for every Reviewer execution. Fill the documented placeholders with current-round context. Do not include previous-round findings, verdicts, filtering decisions, rejection reasons, or repair explanations.
|
|
4
4
|
|
|
5
5
|
```text
|
|
6
6
|
You are the sole independent read-only Reviewer for this round of `/prizmkit-code-review`.
|
|
@@ -9,61 +9,60 @@ You are the sole independent read-only Reviewer for this round of `/prizmkit-cod
|
|
|
9
9
|
|
|
10
10
|
Perform the complete review yourself.
|
|
11
11
|
|
|
12
|
-
Do not create, schedule, resume, continue, request, or coordinate any separate downstream execution unit. Do not re-enter orchestration,
|
|
12
|
+
Do not create, schedule, resume, continue, request, or coordinate any separate downstream execution unit. Do not re-enter orchestration, invoke a skill, delegate review work, or ask the Main Agent to create a helper.
|
|
13
13
|
|
|
14
|
-
Do not modify, create, delete, rename, stage, or commit workspace files.
|
|
14
|
+
Do not modify, create, delete, rename, stage, or commit workspace files. Use only read-only inspection capabilities.
|
|
15
15
|
|
|
16
|
-
If you cannot complete the review
|
|
16
|
+
If you cannot complete the review in this execution context, return `BLOCKED` with the missing capability or context. Do not delegate and do not return a partial `PASS`.
|
|
17
17
|
|
|
18
18
|
## Independent Input Boundary
|
|
19
19
|
|
|
20
|
-
Review only the current state supplied below. No
|
|
20
|
+
Review only the current state supplied below. No previous-round findings, verdicts, filtering decisions, rejection reasons, repair descriptions, or Reviewer reasoning are part of your input.
|
|
21
21
|
|
|
22
|
-
##
|
|
22
|
+
## Goals and Acceptance Criteria
|
|
23
23
|
|
|
24
|
-
{goals and acceptance criteria from spec.md}
|
|
24
|
+
{goals and acceptance criteria from spec.md, or "No spec artifact found; infer the requested behavior from the user goal and current change."}
|
|
25
25
|
|
|
26
|
-
## Plan
|
|
26
|
+
## Plan and Intended Change
|
|
27
27
|
|
|
28
|
-
{architecture decisions and
|
|
28
|
+
{relevant architecture decisions and completed tasks from plan.md, or a concise current task description}
|
|
29
29
|
|
|
30
30
|
## Applicable Rules and Contracts
|
|
31
31
|
|
|
32
32
|
{relevant Prizm docs, project rules, public and internal contracts, or "No additional project rules identified."}
|
|
33
33
|
|
|
34
|
-
##
|
|
34
|
+
## Test Evidence
|
|
35
35
|
|
|
36
|
-
{current test
|
|
36
|
+
{current scoped test evidence, or "No scoped test report found."}
|
|
37
37
|
|
|
38
|
-
##
|
|
38
|
+
## Current Change
|
|
39
39
|
|
|
40
|
-
|
|
40
|
+
- Repository and Reviewer-visible checkout: {path or platform-visible location}
|
|
41
|
+
- Base revision: {base revision when known}
|
|
42
|
+
- Changed paths and states: {staged, unstaged, untracked, deleted, and renamed inventory}
|
|
43
|
+
- Review access: {active checkout, or forced worktree used only for baseline and surrounding context}
|
|
41
44
|
|
|
42
|
-
##
|
|
45
|
+
## Supplied Git Diff
|
|
43
46
|
|
|
44
|
-
|
|
47
|
+
### Staged
|
|
45
48
|
|
|
46
|
-
|
|
49
|
+
{complete staged diff, or "No staged changes."}
|
|
47
50
|
|
|
48
|
-
|
|
51
|
+
### Unstaged
|
|
49
52
|
|
|
50
|
-
|
|
53
|
+
{complete unstaged diff, or "No unstaged tracked changes."}
|
|
51
54
|
|
|
52
|
-
|
|
55
|
+
## Supplied Untracked Files
|
|
53
56
|
|
|
54
|
-
|
|
55
|
-
2. Staged, unstaged, untracked, deleted, and renamed changes are preserved.
|
|
56
|
-
3. Content and snapshot identities match.
|
|
57
|
-
4. Supporting caller or dependent context is distinguishable from changed content.
|
|
58
|
-
5. No unexplained changed path, stale content, mixed-round content, or unverifiable entry exists.
|
|
57
|
+
{path and full content of every untracked file, or "No untracked files."}
|
|
59
58
|
|
|
60
|
-
|
|
59
|
+
Treat the supplied diffs, path states, and untracked file contents as the authoritative current change. Use read-only file inspection for targeted surrounding context, callers, dependents, contracts, and tests. Do not depend on shell or Git execution being available.
|
|
61
60
|
|
|
62
|
-
If
|
|
61
|
+
If a platform-forced worktree does not contain the active checkout's dirty or untracked changes, use the worktree only for baseline and surrounding-context reads. You are not required to calculate hashes or prove equivalence between physical checkouts. If an essential changed file or diff is missing from the supplied input, return `BLOCKED` and identify exactly what is missing.
|
|
63
62
|
|
|
64
|
-
## Complete Current-
|
|
63
|
+
## Complete Current-Change Review
|
|
65
64
|
|
|
66
|
-
Review the complete current change
|
|
65
|
+
Review the complete current change independently. Do not limit inspection to one file or one suspected issue.
|
|
67
66
|
|
|
68
67
|
Evaluate applicable dimensions:
|
|
69
68
|
|
|
@@ -79,60 +78,43 @@ Evaluate applicable dimensions:
|
|
|
79
78
|
10. Test adequacy and evidence compatibility
|
|
80
79
|
11. Project-rule compliance
|
|
81
80
|
|
|
82
|
-
Use both
|
|
81
|
+
Use both directions in this review:
|
|
83
82
|
|
|
84
83
|
- Code-forward: changed code → affected symbols → callers and dependents → concrete failure scenarios
|
|
85
84
|
- Requirement-backward: requirements and contracts → required behavior → implementation path → side effects → test evidence
|
|
86
85
|
|
|
87
|
-
Read unchanged callers, dependents,
|
|
86
|
+
Read unchanged callers, dependents, shared patterns, or tests when needed. Prefer targeted read-only inspection. Do not perform a broad repository scan without a concrete cross-cutting reason.
|
|
88
87
|
|
|
89
|
-
##
|
|
88
|
+
## Finding Standard
|
|
90
89
|
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
Use existing valid test evidence when it answers the question. Prefer the narrowest verification that can establish or refute a concrete issue. Do not rerun an equivalent check when relevant code and inputs have not changed.
|
|
94
|
-
|
|
95
|
-
Avoid broad builds, tests, lint, or type checks as exploratory defaults. A broad command remains valid when a concrete cross-cutting risk genuinely requires it; state why narrower evidence is insufficient.
|
|
96
|
-
|
|
97
|
-
Non-normative examples:
|
|
98
|
-
|
|
99
|
-
- A repository-wide test or build without a concrete reason is often redundant with the formal Test Gate.
|
|
100
|
-
- A focused module or test-case invocation is usually better when investigating one failure scenario.
|
|
101
|
-
|
|
102
|
-
These examples do not prohibit command names. The governing rule is targeted, non-duplicative evidence gathering.
|
|
103
|
-
|
|
104
|
-
## Finding Coverage
|
|
105
|
-
|
|
106
|
-
Report every concrete issue you find, including uncertain or low-severity issues. Do not filter for importance or confidence; the Main Agent performs downstream filtering. Exclude pure style or naming preferences that cannot cause incorrect behavior, contract drift, regression, security weakness, or misleading evidence.
|
|
90
|
+
Report every concrete defect you find, including low-severity issues. Exclude pure style preferences and speculative concerns that lack a concrete wrong behavior, contract violation, regression path, security weakness, data-integrity risk, or misleading test result.
|
|
107
91
|
|
|
108
92
|
## Output Contract
|
|
109
93
|
|
|
110
94
|
Respond with exactly this structure:
|
|
111
95
|
|
|
112
|
-
### Result: PASS |
|
|
96
|
+
### Result: PASS | FINDINGS | BLOCKED
|
|
113
97
|
|
|
114
|
-
###
|
|
115
|
-
- Status: COMPLIANT | BLOCKED
|
|
116
|
-
- Descendant execution units created: 0
|
|
117
|
-
- Delegation depth: 0
|
|
98
|
+
### Findings
|
|
118
99
|
|
|
119
|
-
|
|
120
|
-
- Status: VERIFIED | FAILED
|
|
121
|
-
- Identity: <snapshot identity or mismatch summary>
|
|
100
|
+
If the result is `PASS`, write:
|
|
122
101
|
|
|
123
|
-
|
|
124
|
-
(If PASS, write "No issues found." If WORKSPACE_MISMATCH or REVIEW_INFRASTRUCTURE_BLOCKED, include only blocker evidence and required action.)
|
|
102
|
+
No issues found.
|
|
125
103
|
|
|
126
|
-
|
|
104
|
+
If the result is `BLOCKED`, write only the missing capability or essential context and the required action.
|
|
105
|
+
|
|
106
|
+
If the result is `FINDINGS`, repeat this block for every concrete issue:
|
|
107
|
+
|
|
108
|
+
#### Finding N: <short title>
|
|
127
109
|
- Severity: high | medium | low
|
|
128
|
-
- Confidence: high | medium | low
|
|
129
|
-
- Dimension: requirement-alignment | defect | completeness | error-handling | security | data-integrity | concurrency | contract | compatibility | test-quality | rules-compliance
|
|
130
110
|
- Location: filepath:line (or "project-level")
|
|
131
111
|
- Problem: concrete wrong behavior and why it matters
|
|
132
112
|
- Failure scenario: inputs or state that produce the wrong result
|
|
133
|
-
-
|
|
113
|
+
- Reasoning: concise evidence connecting the current code to the failure
|
|
114
|
+
- Suggested repair: smallest safe repair approach
|
|
134
115
|
- Verification: how to prove the repair
|
|
135
116
|
|
|
136
117
|
### Summary
|
|
137
|
-
|
|
118
|
+
|
|
119
|
+
One or two sentences describing the current change and result. Do not expose private reasoning traces.
|
|
138
120
|
```
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env python3
|
|
2
|
-
"""
|
|
2
|
+
"""Initialize and append validated sections to review-report.md."""
|
|
3
3
|
|
|
4
4
|
from __future__ import annotations
|
|
5
5
|
|
|
@@ -9,180 +9,286 @@ import sys
|
|
|
9
9
|
from pathlib import Path
|
|
10
10
|
from typing import Any
|
|
11
11
|
|
|
12
|
-
VERDICTS = {"PASS", "NEEDS_FIXES"}
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
"
|
|
18
|
-
"
|
|
19
|
-
"
|
|
20
|
-
"
|
|
21
|
-
"
|
|
22
|
-
"WORKSPACE_MISMATCH",
|
|
23
|
-
}
|
|
24
|
-
REVIEW_MODES = {
|
|
25
|
-
"independent-reviewer-verified-snapshot",
|
|
26
|
-
"not-run-no-changes",
|
|
12
|
+
VERDICTS = {"PASS", "NEEDS_FIXES", "BLOCKED"}
|
|
13
|
+
RISKS = {"low", "medium", "high"}
|
|
14
|
+
EVENTS = {
|
|
15
|
+
"risk-assessment",
|
|
16
|
+
"main-review-round",
|
|
17
|
+
"repair-verification",
|
|
18
|
+
"reviewer-running",
|
|
19
|
+
"reviewer-result",
|
|
20
|
+
"infrastructure-failure",
|
|
21
|
+
"final-verification",
|
|
27
22
|
}
|
|
23
|
+
FINAL_HEADING = "## Final Result"
|
|
28
24
|
|
|
29
25
|
|
|
30
26
|
class ReportStateError(ValueError):
|
|
31
|
-
"""Raised when
|
|
27
|
+
"""Raised when an operation would produce an invalid report lifecycle."""
|
|
32
28
|
|
|
33
29
|
|
|
34
|
-
def
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
30
|
+
def _require_object(data: Any) -> dict[str, Any]:
|
|
31
|
+
if not isinstance(data, dict):
|
|
32
|
+
raise ReportStateError("input must be a JSON object")
|
|
33
|
+
return data
|
|
34
|
+
|
|
39
35
|
|
|
36
|
+
def _require_text(data: dict[str, Any], name: str) -> str:
|
|
37
|
+
value = data.get(name)
|
|
38
|
+
if not isinstance(value, str) or not value.strip():
|
|
39
|
+
raise ReportStateError(f"{name} must be a non-empty string")
|
|
40
|
+
return value.strip()
|
|
40
41
|
|
|
41
|
-
|
|
42
|
-
|
|
42
|
+
|
|
43
|
+
def _require_count(data: dict[str, Any], name: str, *, maximum: int | None = None) -> int:
|
|
44
|
+
value = data.get(name)
|
|
43
45
|
if not isinstance(value, int) or isinstance(value, bool) or value < 0:
|
|
44
46
|
raise ReportStateError(f"{name} must be a non-negative integer")
|
|
47
|
+
if maximum is not None and value > maximum:
|
|
48
|
+
raise ReportStateError(f"{name} cannot exceed {maximum}")
|
|
45
49
|
return value
|
|
46
50
|
|
|
47
51
|
|
|
48
|
-
def
|
|
49
|
-
|
|
50
|
-
if not
|
|
51
|
-
raise ReportStateError("
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
if
|
|
67
|
-
raise ReportStateError("
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
""
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
"",
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
)
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
52
|
+
def _require_risk(data: dict[str, Any]) -> str:
|
|
53
|
+
risk = data.get("risk")
|
|
54
|
+
if risk not in RISKS:
|
|
55
|
+
raise ReportStateError(f"risk has invalid value: {risk!r}")
|
|
56
|
+
return risk
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def _report_text(path: Path) -> str:
|
|
60
|
+
try:
|
|
61
|
+
return path.read_text(encoding="utf-8")
|
|
62
|
+
except FileNotFoundError as error:
|
|
63
|
+
raise ReportStateError("report must be initialized before append") from error
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def _ensure_appendable(path: Path) -> None:
|
|
67
|
+
text = _report_text(path)
|
|
68
|
+
if "## Status: IN_PROGRESS" not in text:
|
|
69
|
+
raise ReportStateError("report is not an initialized in-progress execution")
|
|
70
|
+
if FINAL_HEADING in text:
|
|
71
|
+
raise ReportStateError("cannot append after Final Result")
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def _append_section(path: Path, section: str) -> None:
|
|
75
|
+
with path.open("a", encoding="utf-8") as report:
|
|
76
|
+
report.write(section.rstrip() + "\n\n")
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def initialize_report(path: Path) -> None:
|
|
80
|
+
"""Start a new execution and remove stale prior execution content."""
|
|
81
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
82
|
+
path.write_text("# Review Report\n\n## Status: IN_PROGRESS\n\n", encoding="utf-8")
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def _render_event(data: dict[str, Any]) -> str:
|
|
86
|
+
event = data.get("event")
|
|
87
|
+
if event not in EVENTS:
|
|
88
|
+
raise ReportStateError(f"event has invalid value: {event!r}")
|
|
89
|
+
|
|
90
|
+
if event == "risk-assessment":
|
|
91
|
+
risk = _require_risk(data)
|
|
92
|
+
reasons = data.get("reasons")
|
|
93
|
+
if not isinstance(reasons, list) or not reasons or not all(
|
|
94
|
+
isinstance(reason, str) and reason.strip() for reason in reasons
|
|
95
|
+
):
|
|
96
|
+
raise ReportStateError("reasons must be a non-empty string array")
|
|
97
|
+
limit = {"low": 0, "medium": 1, "high": 2}[risk]
|
|
98
|
+
lines = [
|
|
99
|
+
"## Risk Assessment",
|
|
100
|
+
"",
|
|
101
|
+
f"- Risk: {risk}",
|
|
102
|
+
f"- Independent Reviewer Limit: {limit}",
|
|
103
|
+
"- Reasons:",
|
|
104
|
+
]
|
|
105
|
+
lines.extend(f" - {reason.strip()}" for reason in reasons)
|
|
106
|
+
return "\n".join(lines)
|
|
107
|
+
|
|
108
|
+
if event == "main-review-round":
|
|
109
|
+
round_number = _require_count(data, "round", maximum=10)
|
|
110
|
+
if round_number < 1:
|
|
111
|
+
raise ReportStateError("round must be at least 1")
|
|
112
|
+
findings = _require_count(data, "findings")
|
|
113
|
+
accepted = _require_count(data, "accepted")
|
|
114
|
+
rejected = _require_count(data, "rejected")
|
|
115
|
+
if accepted + rejected != findings:
|
|
116
|
+
raise ReportStateError("accepted + rejected must equal findings")
|
|
117
|
+
return "\n".join(
|
|
118
|
+
[
|
|
119
|
+
f"## Main Review Round {round_number}",
|
|
120
|
+
"",
|
|
121
|
+
"- Status: COMPLETED",
|
|
122
|
+
f"- Findings: {findings}",
|
|
123
|
+
f"- Accepted: {accepted}",
|
|
124
|
+
f"- Rejected: {rejected}",
|
|
125
|
+
f"- Next: {_require_text(data, 'next')}",
|
|
126
|
+
]
|
|
127
|
+
)
|
|
128
|
+
|
|
129
|
+
if event == "repair-verification":
|
|
130
|
+
source = _require_text(data, "source")
|
|
131
|
+
if source not in {"main", "reviewer-1", "reviewer-2"}:
|
|
132
|
+
raise ReportStateError(f"source has invalid value: {source!r}")
|
|
133
|
+
return "\n".join(
|
|
134
|
+
[
|
|
135
|
+
"## Repair Verification",
|
|
136
|
+
"",
|
|
137
|
+
f"- Source: {source}",
|
|
138
|
+
f"- Fixed Findings: {_require_count(data, 'fixed_findings')}",
|
|
139
|
+
f"- Verification: {_require_text(data, 'verification')}",
|
|
140
|
+
f"- Next: {_require_text(data, 'next')}",
|
|
141
|
+
]
|
|
142
|
+
)
|
|
143
|
+
|
|
144
|
+
if event == "reviewer-running":
|
|
145
|
+
reviewer = _require_count(data, "reviewer", maximum=2)
|
|
146
|
+
if reviewer < 1:
|
|
147
|
+
raise ReportStateError("reviewer must be 1 or 2")
|
|
148
|
+
risk = _require_risk(data)
|
|
149
|
+
if risk == "low" or (risk == "medium" and reviewer == 2):
|
|
150
|
+
raise ReportStateError("reviewer exceeds the semantic risk limit")
|
|
151
|
+
attempt = data.get("attempt", 1)
|
|
152
|
+
if attempt not in {1, 2}:
|
|
153
|
+
raise ReportStateError("attempt must be 1 or 2")
|
|
154
|
+
return "\n".join(
|
|
150
155
|
[
|
|
151
|
-
f"
|
|
156
|
+
f"## Independent Reviewer {reviewer}",
|
|
152
157
|
"",
|
|
153
|
-
|
|
154
|
-
f"-
|
|
155
|
-
f"-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
158
|
+
"- Status: RUNNING",
|
|
159
|
+
f"- Risk Trigger: {risk}",
|
|
160
|
+
f"- Attempt: {attempt}",
|
|
161
|
+
]
|
|
162
|
+
)
|
|
163
|
+
|
|
164
|
+
if event == "reviewer-result":
|
|
165
|
+
reviewer = _require_count(data, "reviewer", maximum=2)
|
|
166
|
+
if reviewer < 1:
|
|
167
|
+
raise ReportStateError("reviewer must be 1 or 2")
|
|
168
|
+
result = data.get("result")
|
|
169
|
+
if result not in {"PASS", "FINDINGS", "BLOCKED"}:
|
|
170
|
+
raise ReportStateError(f"result has invalid value: {result!r}")
|
|
171
|
+
findings = _require_count(data, "findings")
|
|
172
|
+
accepted = _require_count(data, "accepted")
|
|
173
|
+
rejected = _require_count(data, "rejected")
|
|
174
|
+
accepted_high = _require_count(data, "accepted_high")
|
|
175
|
+
if accepted + rejected != findings or accepted_high > accepted:
|
|
176
|
+
raise ReportStateError("reviewer finding counts are inconsistent")
|
|
177
|
+
status = "BLOCKED" if result == "BLOCKED" else "COMPLETED"
|
|
178
|
+
return "\n".join(
|
|
179
|
+
[
|
|
180
|
+
f"## Independent Reviewer {reviewer} Result",
|
|
181
|
+
"",
|
|
182
|
+
f"- Status: {status}",
|
|
183
|
+
f"- Result: {result}",
|
|
184
|
+
f"- Findings: {findings}",
|
|
185
|
+
f"- Accepted: {accepted}",
|
|
186
|
+
f"- Rejected: {rejected}",
|
|
187
|
+
f"- Accepted High: {accepted_high}",
|
|
188
|
+
f"- Next: {_require_text(data, 'next')}",
|
|
189
|
+
]
|
|
190
|
+
)
|
|
191
|
+
|
|
192
|
+
if event == "infrastructure-failure":
|
|
193
|
+
reviewer = _require_count(data, "reviewer", maximum=2)
|
|
194
|
+
attempt = _require_count(data, "attempt", maximum=2)
|
|
195
|
+
if reviewer < 1 or attempt < 1:
|
|
196
|
+
raise ReportStateError("reviewer and attempt must be at least 1")
|
|
197
|
+
return "\n".join(
|
|
198
|
+
[
|
|
199
|
+
f"## Independent Reviewer {reviewer} Infrastructure Failure",
|
|
159
200
|
"",
|
|
201
|
+
f"- Attempt: {attempt}",
|
|
202
|
+
f"- Evidence: {_require_text(data, 'evidence')}",
|
|
203
|
+
f"- Next: {_require_text(data, 'next')}",
|
|
160
204
|
]
|
|
161
205
|
)
|
|
162
206
|
|
|
163
|
-
|
|
164
|
-
|
|
207
|
+
return "\n".join(
|
|
208
|
+
[
|
|
209
|
+
"## Final Verification",
|
|
210
|
+
"",
|
|
211
|
+
f"- Status: {_require_text(data, 'status')}",
|
|
212
|
+
f"- Evidence: {_require_text(data, 'evidence')}",
|
|
213
|
+
]
|
|
214
|
+
)
|
|
215
|
+
|
|
216
|
+
|
|
217
|
+
def append_event(path: Path, data: Any) -> None:
|
|
218
|
+
"""Append one validated progress event to an active execution."""
|
|
219
|
+
_ensure_appendable(path)
|
|
220
|
+
_append_section(path, _render_event(_require_object(data)))
|
|
221
|
+
|
|
222
|
+
|
|
223
|
+
def append_final_result(path: Path, data: Any) -> None:
|
|
224
|
+
"""Append the sole terminal result for the current execution."""
|
|
225
|
+
_ensure_appendable(path)
|
|
226
|
+
valid = _require_object(data)
|
|
227
|
+
verdict = valid.get("verdict")
|
|
228
|
+
if verdict not in VERDICTS:
|
|
229
|
+
raise ReportStateError(f"verdict has invalid value: {verdict!r}")
|
|
230
|
+
risk = _require_risk(valid)
|
|
231
|
+
main_rounds = _require_count(valid, "main_review_rounds", maximum=10)
|
|
232
|
+
reviewers = _require_count(valid, "independent_reviewers_started", maximum=2)
|
|
233
|
+
if reviewers > {"low": 0, "medium": 1, "high": 2}[risk]:
|
|
234
|
+
raise ReportStateError("independent_reviewers_started exceeds risk limit")
|
|
235
|
+
accepted = _require_count(valid, "accepted_findings")
|
|
236
|
+
fixed = _require_count(valid, "fixed_findings")
|
|
237
|
+
rejected = _require_count(valid, "rejected_findings")
|
|
238
|
+
unresolved = _require_count(valid, "unresolved_findings")
|
|
239
|
+
if verdict == "PASS" and unresolved:
|
|
240
|
+
raise ReportStateError("PASS cannot contain unresolved findings")
|
|
241
|
+
if verdict == "NEEDS_FIXES" and not unresolved:
|
|
242
|
+
raise ReportStateError("NEEDS_FIXES requires unresolved findings")
|
|
243
|
+
|
|
244
|
+
section = "\n".join(
|
|
245
|
+
[
|
|
246
|
+
FINAL_HEADING,
|
|
247
|
+
"",
|
|
248
|
+
f"- Verdict: {verdict}",
|
|
249
|
+
f"- Risk: {risk}",
|
|
250
|
+
f"- Main Review Rounds: {main_rounds}",
|
|
251
|
+
f"- Independent Reviewers Started: {reviewers}",
|
|
252
|
+
f"- Accepted Findings: {accepted}",
|
|
253
|
+
f"- Fixed Findings: {fixed}",
|
|
254
|
+
f"- Rejected Findings: {rejected}",
|
|
255
|
+
f"- Unresolved Findings: {unresolved}",
|
|
256
|
+
f"- Summary: {_require_text(valid, 'summary')}",
|
|
257
|
+
]
|
|
258
|
+
)
|
|
259
|
+
_append_section(path, section)
|
|
260
|
+
|
|
261
|
+
|
|
262
|
+
def _read_json_input() -> Any:
|
|
263
|
+
try:
|
|
264
|
+
return json.loads(sys.stdin.read())
|
|
265
|
+
except json.JSONDecodeError as error:
|
|
266
|
+
raise ReportStateError(f"invalid JSON input: {error}") from error
|
|
165
267
|
|
|
166
268
|
|
|
167
269
|
def parse_args() -> argparse.Namespace:
|
|
168
270
|
parser = argparse.ArgumentParser(description=__doc__)
|
|
169
|
-
parser.
|
|
170
|
-
|
|
271
|
+
subparsers = parser.add_subparsers(dest="command", required=True)
|
|
272
|
+
for command in ("init", "append", "finalize"):
|
|
273
|
+
subparser = subparsers.add_parser(command)
|
|
274
|
+
subparser.add_argument("report", type=Path)
|
|
171
275
|
return parser.parse_args()
|
|
172
276
|
|
|
173
277
|
|
|
174
278
|
def main() -> int:
|
|
175
279
|
args = parse_args()
|
|
176
280
|
try:
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
args.
|
|
180
|
-
|
|
181
|
-
|
|
281
|
+
if args.command == "init":
|
|
282
|
+
initialize_report(args.report)
|
|
283
|
+
elif args.command == "append":
|
|
284
|
+
append_event(args.report, _read_json_input())
|
|
285
|
+
else:
|
|
286
|
+
append_final_result(args.report, _read_json_input())
|
|
287
|
+
except (OSError, ReportStateError, KeyError) as error:
|
|
182
288
|
print(json.dumps({"error": str(error)}, sort_keys=True))
|
|
183
289
|
return 1
|
|
184
290
|
return 0
|
|
185
291
|
|
|
186
292
|
|
|
187
293
|
if __name__ == "__main__":
|
|
188
|
-
|
|
294
|
+
raise SystemExit(main())
|