forgexa-cli 1.76.2__tar.gz → 1.77.0__tar.gz
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.
- {forgexa_cli-1.76.2 → forgexa_cli-1.77.0}/PKG-INFO +1 -1
- {forgexa_cli-1.76.2 → forgexa_cli-1.77.0}/forgexa_cli/__init__.py +1 -1
- {forgexa_cli-1.76.2 → forgexa_cli-1.77.0}/forgexa_cli/agent_core.py +170 -0
- {forgexa_cli-1.76.2 → forgexa_cli-1.77.0}/forgexa_cli/daemon.py +76 -4
- {forgexa_cli-1.76.2 → forgexa_cli-1.77.0}/forgexa_cli/runtime_evidence.py +155 -0
- {forgexa_cli-1.76.2 → forgexa_cli-1.77.0}/forgexa_cli.egg-info/PKG-INFO +1 -1
- {forgexa_cli-1.76.2 → forgexa_cli-1.77.0}/pyproject.toml +1 -1
- {forgexa_cli-1.76.2 → forgexa_cli-1.77.0}/README.md +0 -0
- {forgexa_cli-1.76.2 → forgexa_cli-1.77.0}/forgexa_cli/_build_config.py +0 -0
- {forgexa_cli-1.76.2 → forgexa_cli-1.77.0}/forgexa_cli/_local_bind.py +0 -0
- {forgexa_cli-1.76.2 → forgexa_cli-1.77.0}/forgexa_cli/autoupgrade.py +0 -0
- {forgexa_cli-1.76.2 → forgexa_cli-1.77.0}/forgexa_cli/aws_evidence.py +0 -0
- {forgexa_cli-1.76.2 → forgexa_cli-1.77.0}/forgexa_cli/aws_logs_evidence.py +0 -0
- {forgexa_cli-1.76.2 → forgexa_cli-1.77.0}/forgexa_cli/main.py +0 -0
- {forgexa_cli-1.76.2 → forgexa_cli-1.77.0}/forgexa_cli/py.typed +0 -0
- {forgexa_cli-1.76.2 → forgexa_cli-1.77.0}/forgexa_cli/zcode_protocol.py +0 -0
- {forgexa_cli-1.76.2 → forgexa_cli-1.77.0}/forgexa_cli.egg-info/SOURCES.txt +0 -0
- {forgexa_cli-1.76.2 → forgexa_cli-1.77.0}/forgexa_cli.egg-info/dependency_links.txt +0 -0
- {forgexa_cli-1.76.2 → forgexa_cli-1.77.0}/forgexa_cli.egg-info/entry_points.txt +0 -0
- {forgexa_cli-1.76.2 → forgexa_cli-1.77.0}/forgexa_cli.egg-info/requires.txt +0 -0
- {forgexa_cli-1.76.2 → forgexa_cli-1.77.0}/forgexa_cli.egg-info/top_level.txt +0 -0
- {forgexa_cli-1.76.2 → forgexa_cli-1.77.0}/setup.cfg +0 -0
- {forgexa_cli-1.76.2 → forgexa_cli-1.77.0}/tests/test_auth_and_runtime_commands.py +0 -0
- {forgexa_cli-1.76.2 → forgexa_cli-1.77.0}/tests/test_autoupgrade.py +0 -0
- {forgexa_cli-1.76.2 → forgexa_cli-1.77.0}/tests/test_check_command.py +0 -0
- {forgexa_cli-1.76.2 → forgexa_cli-1.77.0}/tests/test_expiry_warnings_and_revoke.py +0 -0
- {forgexa_cli-1.76.2 → forgexa_cli-1.77.0}/tests/test_local_bind_commands.py +0 -0
- {forgexa_cli-1.76.2 → forgexa_cli-1.77.0}/tests/test_runtime_credentials.py +0 -0
- {forgexa_cli-1.76.2 → forgexa_cli-1.77.0}/tests/test_session_credentials.py +0 -0
- {forgexa_cli-1.76.2 → forgexa_cli-1.77.0}/tests/test_silent_install.py +0 -0
- {forgexa_cli-1.76.2 → forgexa_cli-1.77.0}/tests/test_upgrade_observability.py +0 -0
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
"""forgexa-cli — Forgexa command-line client."""
|
|
2
|
-
__version__ = "1.
|
|
2
|
+
__version__ = "1.77.0"
|
|
@@ -103,6 +103,176 @@ def missing_planned_task_files(task_input: dict, changed_files: list[str]) -> li
|
|
|
103
103
|
)
|
|
104
104
|
|
|
105
105
|
|
|
106
|
+
def extract_implementation_audit(text: str) -> dict | None:
|
|
107
|
+
"""Read the last complete audit envelope from mixed assistant output."""
|
|
108
|
+
decoder = json.JSONDecoder()
|
|
109
|
+
offset = 0
|
|
110
|
+
report = None
|
|
111
|
+
report_end = 0
|
|
112
|
+
while offset < len(text):
|
|
113
|
+
start = text.find("{", offset)
|
|
114
|
+
if start < 0:
|
|
115
|
+
break
|
|
116
|
+
try:
|
|
117
|
+
value, end = decoder.raw_decode(text, start)
|
|
118
|
+
except json.JSONDecodeError:
|
|
119
|
+
offset = start + 1
|
|
120
|
+
continue
|
|
121
|
+
if isinstance(value, dict) and "implementation_audit" in value:
|
|
122
|
+
report = value["implementation_audit"] if isinstance(value["implementation_audit"], dict) else None
|
|
123
|
+
report_end = end
|
|
124
|
+
offset = end
|
|
125
|
+
if '"implementation_audit"' in text[report_end:]:
|
|
126
|
+
return None
|
|
127
|
+
return report
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
def redact_implementation_audit(contract: dict, report: object) -> dict | None:
|
|
131
|
+
"""Keep contract-owned status and hashed evidence without local free text."""
|
|
132
|
+
from hashlib import sha256
|
|
133
|
+
|
|
134
|
+
if not assess_implementation_audit(contract, report)["report_valid"]:
|
|
135
|
+
return None
|
|
136
|
+
summary = {"audit_id": contract["audit_id"], "task_checks": [], "acceptance_checks": []}
|
|
137
|
+
for key in ("task_checks", "acceptance_checks"):
|
|
138
|
+
for row in report[key]:
|
|
139
|
+
evidence = []
|
|
140
|
+
for reference in row["evidence"][:20]:
|
|
141
|
+
digest = reference.removeprefix("local-evidence-sha256:")
|
|
142
|
+
evidence.append(reference if digest != reference and len(digest) == 64
|
|
143
|
+
and all(character in "0123456789abcdef" for character in digest)
|
|
144
|
+
else "local-evidence-sha256:" + sha256(reference.encode()).hexdigest())
|
|
145
|
+
check = {"status": row["status"], "evidence": evidence}
|
|
146
|
+
if key == "task_checks":
|
|
147
|
+
check["task_id"] = row["task_id"]
|
|
148
|
+
else:
|
|
149
|
+
check.update(index=row["index"], task_ids=row["task_ids"])
|
|
150
|
+
if row["status"] != "covered":
|
|
151
|
+
check.update(description="Local implementation issue remains unresolved; inspect local task evidence.", files=[])
|
|
152
|
+
if row.get("plan_revision"):
|
|
153
|
+
check["plan_revision"] = {
|
|
154
|
+
"reason": "Local evidence conflicts with the approved plan",
|
|
155
|
+
"proposal": "Inspect the source in the requirement workspace and revise analysis and tasks together",
|
|
156
|
+
}
|
|
157
|
+
summary[key].append(check)
|
|
158
|
+
return summary
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
def assess_implementation_audit(contract: dict, report: object) -> dict:
|
|
162
|
+
"""Validate implementation coverage independently of later runtime acceptance."""
|
|
163
|
+
issues: list[str] = []
|
|
164
|
+
findings: list[dict] = []
|
|
165
|
+
result = {
|
|
166
|
+
"audit_id": contract.get("audit_id"), "report_valid": False,
|
|
167
|
+
"node_complete": False, "status": "incomplete", "issues": issues,
|
|
168
|
+
"findings": findings, "followup_task_ids": [], "report": report,
|
|
169
|
+
"revision_requests": [],
|
|
170
|
+
}
|
|
171
|
+
if (
|
|
172
|
+
not isinstance(contract.get("audit_id"), str) or not contract["audit_id"]
|
|
173
|
+
or not isinstance(report, dict) or report.get("audit_id") != contract["audit_id"]
|
|
174
|
+
):
|
|
175
|
+
issues.append("Implementation audit is missing or belongs to another remediation task")
|
|
176
|
+
return result
|
|
177
|
+
if set(report) - {"audit_id", "task_checks", "acceptance_checks"}:
|
|
178
|
+
issues.append("Implementation audit contains unsupported fields")
|
|
179
|
+
expected = {task["id"] for task in contract.get("tasks") or []}
|
|
180
|
+
subject = contract.get("subject_task_id")
|
|
181
|
+
if not expected or subject not in expected:
|
|
182
|
+
issues.append("Implementation audit has no approved task baseline")
|
|
183
|
+
task_checks = report.get("task_checks")
|
|
184
|
+
acceptance_checks = report.get("acceptance_checks")
|
|
185
|
+
if not isinstance(task_checks, list) or not isinstance(acceptance_checks, list):
|
|
186
|
+
issues.append("Implementation audit requires task_checks and acceptance_checks lists")
|
|
187
|
+
return result
|
|
188
|
+
checked: dict[str, str] = {}
|
|
189
|
+
checked_criteria: set[int] = set()
|
|
190
|
+
criterion_count = len(contract.get("acceptance_criteria") or [])
|
|
191
|
+
for kind, rows in (("task", task_checks), ("acceptance", acceptance_checks)):
|
|
192
|
+
for row in rows:
|
|
193
|
+
if not isinstance(row, dict):
|
|
194
|
+
issues.append(f"Invalid implementation {kind} check")
|
|
195
|
+
continue
|
|
196
|
+
evidence = row.get("evidence")
|
|
197
|
+
status = row.get("status")
|
|
198
|
+
if (
|
|
199
|
+
not isinstance(status, str) or status not in {"covered", "gap", "blocked", "unverified"}
|
|
200
|
+
or not isinstance(evidence, list) or not evidence
|
|
201
|
+
or any(not isinstance(item, str) or not item.strip() for item in evidence)
|
|
202
|
+
):
|
|
203
|
+
issues.append(f"Implementation {kind} check requires an explicit status and concrete evidence")
|
|
204
|
+
continue
|
|
205
|
+
if kind == "task":
|
|
206
|
+
task_id = row.get("task_id")
|
|
207
|
+
if not isinstance(task_id, str) or task_id not in expected or task_id in checked:
|
|
208
|
+
issues.append("Implementation audit has unknown or duplicate task IDs")
|
|
209
|
+
continue
|
|
210
|
+
checked[task_id] = status
|
|
211
|
+
related = [task_id]
|
|
212
|
+
else:
|
|
213
|
+
index = row.get("index")
|
|
214
|
+
related = row.get("task_ids")
|
|
215
|
+
if type(index) is int and not 1 <= index <= criterion_count:
|
|
216
|
+
issues.append(
|
|
217
|
+
f"Acceptance index {index} is outside the frozen baseline 1-{criterion_count}; "
|
|
218
|
+
"report the baseline conflict instead of dropping or renumbering requirements"
|
|
219
|
+
)
|
|
220
|
+
continue
|
|
221
|
+
if (
|
|
222
|
+
type(index) is not int or not 1 <= index <= criterion_count or index in checked_criteria
|
|
223
|
+
or not isinstance(related, list) or not related
|
|
224
|
+
or any(not isinstance(task_id, str) or task_id not in expected for task_id in related)
|
|
225
|
+
):
|
|
226
|
+
issues.append("Implementation audit has invalid acceptance criterion or task references")
|
|
227
|
+
continue
|
|
228
|
+
checked_criteria.add(index)
|
|
229
|
+
if status != "covered":
|
|
230
|
+
description = row.get("description")
|
|
231
|
+
paths = row.get("files") or []
|
|
232
|
+
if not isinstance(description, str) or not description.strip() or not isinstance(paths, list):
|
|
233
|
+
issues.append("Implementation gaps require an actionable description and file list")
|
|
234
|
+
continue
|
|
235
|
+
if any(
|
|
236
|
+
not isinstance(path, str) or not path.strip()
|
|
237
|
+
or PurePosixPath(path).is_absolute() or ".." in PurePosixPath(path).parts
|
|
238
|
+
for path in paths
|
|
239
|
+
):
|
|
240
|
+
issues.append("Implementation gap paths must be repository-relative")
|
|
241
|
+
continue
|
|
242
|
+
revision = row.get("plan_revision")
|
|
243
|
+
if revision is not None:
|
|
244
|
+
if not isinstance(revision, dict) or any(
|
|
245
|
+
not isinstance(revision.get(key), str) or not revision[key].strip()
|
|
246
|
+
for key in ("reason", "proposal")
|
|
247
|
+
):
|
|
248
|
+
issues.append("Plan revision requests require evidence, reason and a concrete proposal")
|
|
249
|
+
continue
|
|
250
|
+
result["revision_requests"].extend({
|
|
251
|
+
"task_id": task_id, "reason": revision["reason"], "proposal": revision["proposal"],
|
|
252
|
+
"evidence": evidence, "files": paths,
|
|
253
|
+
} for task_id in related)
|
|
254
|
+
findings.extend({
|
|
255
|
+
"task_id": task_id, "status": status, "description": description,
|
|
256
|
+
"files": paths, "evidence": evidence,
|
|
257
|
+
**({"plan_revision": revision} if revision is not None else {}),
|
|
258
|
+
} for task_id in related)
|
|
259
|
+
required = expected if contract.get("final_verification") else {subject}
|
|
260
|
+
if required - checked.keys():
|
|
261
|
+
issues.append("Implementation audit omitted required approved tasks")
|
|
262
|
+
if contract.get("final_verification") and len(checked_criteria) != criterion_count:
|
|
263
|
+
issues.append("Implementation audit omitted acceptance criteria")
|
|
264
|
+
result["report_valid"] = not issues
|
|
265
|
+
result["node_complete"] = (
|
|
266
|
+
not issues and checked.get(subject) == "covered"
|
|
267
|
+
and not any(finding["task_id"] == subject for finding in findings)
|
|
268
|
+
)
|
|
269
|
+
result["followup_task_ids"] = sorted({finding["task_id"] for finding in findings})
|
|
270
|
+
result["status"] = "complete" if not issues and not findings else "incomplete"
|
|
271
|
+
if result["report_valid"] and result["revision_requests"]:
|
|
272
|
+
result["status"] = "plan_revision_required"
|
|
273
|
+
return result
|
|
274
|
+
|
|
275
|
+
|
|
106
276
|
_WORKSPACE_INSTRUCTION_PATHS = (
|
|
107
277
|
"AGENTS.md",
|
|
108
278
|
"CLAUDE.md",
|
|
@@ -1234,7 +1234,7 @@ except (ImportError, ModuleNotFoundError):
|
|
|
1234
1234
|
# DAEMON_VERSION is the protocol/logic version of the daemon code.
|
|
1235
1235
|
# Kept in sync with pyproject.toml version via bump-version.sh.
|
|
1236
1236
|
# CLIENT_TYPE identifies which packaging/distribution this daemon runs in.
|
|
1237
|
-
DAEMON_VERSION = "1.
|
|
1237
|
+
DAEMON_VERSION = "1.77.0"
|
|
1238
1238
|
|
|
1239
1239
|
|
|
1240
1240
|
def _detect_client_type() -> str:
|
|
@@ -2641,6 +2641,14 @@ def _read_local_bind_shared_output_snapshot(task_id: str) -> dict | None:
|
|
|
2641
2641
|
|
|
2642
2642
|
def _prepare_local_bind_completion(task: "TaskInfo", result: "TaskResult") -> None:
|
|
2643
2643
|
"""Attach the automatic private snapshot before stripping normal local output."""
|
|
2644
|
+
result.metrics.pop("implementation_audit", None)
|
|
2645
|
+
audit_contract = (task.input_data or {}).get("implementation_audit_contract")
|
|
2646
|
+
if isinstance(audit_contract, dict):
|
|
2647
|
+
audit_report = next((item.get("data") for item in reversed(result.observations)
|
|
2648
|
+
if item.get("type") == "implementation_audit"), None)
|
|
2649
|
+
summary = agent_core.redact_implementation_audit(audit_contract, audit_report)
|
|
2650
|
+
if summary is not None:
|
|
2651
|
+
result.metrics["implementation_audit"] = summary
|
|
2644
2652
|
if _MANAGED_WSL_POLICY is not None:
|
|
2645
2653
|
result.shared_local_output = None
|
|
2646
2654
|
if _local_bind_output_sharing_enabled(task.local_workspace_binding_id):
|
|
@@ -9904,6 +9912,9 @@ class ServerConnection:
|
|
|
9904
9912
|
"local_fresh_branch_reset": {
|
|
9905
9913
|
"protocol_version": _LOCAL_FRESH_BRANCH_RESET_PROTOCOL_VERSION,
|
|
9906
9914
|
},
|
|
9915
|
+
"acceptance_verification": {
|
|
9916
|
+
"protocol_version": runtime_evidence.DEFERRED_ACCEPTANCE_VERIFICATION_PROTOCOL_VERSION,
|
|
9917
|
+
},
|
|
9907
9918
|
"task_attachment_staging": {"protocol_version": 1, "runtime_proxy": True, "image_agents": ["claude"]},
|
|
9908
9919
|
},
|
|
9909
9920
|
"server_url": self.server_url,
|
|
@@ -10324,6 +10335,9 @@ class ServerConnection:
|
|
|
10324
10335
|
"local_fresh_branch_reset": {
|
|
10325
10336
|
"protocol_version": _LOCAL_FRESH_BRANCH_RESET_PROTOCOL_VERSION,
|
|
10326
10337
|
},
|
|
10338
|
+
"acceptance_verification": {
|
|
10339
|
+
"protocol_version": runtime_evidence.DEFERRED_ACCEPTANCE_VERIFICATION_PROTOCOL_VERSION,
|
|
10340
|
+
},
|
|
10327
10341
|
"local_evidence": {"protocol_version": 1, "structured_summary": True},
|
|
10328
10342
|
"task_attachment_staging": {"protocol_version": 1, "runtime_proxy": True, "image_agents": ["claude"]},
|
|
10329
10343
|
},
|
|
@@ -13404,6 +13418,18 @@ class RuntimeDaemon:
|
|
|
13404
13418
|
result.git = {}
|
|
13405
13419
|
|
|
13406
13420
|
if result.status == "success":
|
|
13421
|
+
acceptance = result.metrics.get("acceptance_verification") or {}
|
|
13422
|
+
if acceptance.get("status") == "pending_final_qa":
|
|
13423
|
+
await reporter.report_progress(
|
|
13424
|
+
task.task_id, 94, "acceptance_pending_final_qa",
|
|
13425
|
+
execution_attempt_id=task.execution_attempt_id,
|
|
13426
|
+
output_lines=[
|
|
13427
|
+
"[daemon] Verification assets are prepared; acceptance remains pending "
|
|
13428
|
+
"post-deployment automated or human verification: "
|
|
13429
|
+
+ ", ".join(acceptance["criteria"]),
|
|
13430
|
+
"[daemon] No browser acceptance or release approval is claimed.",
|
|
13431
|
+
],
|
|
13432
|
+
)
|
|
13407
13433
|
if is_local_bind:
|
|
13408
13434
|
# Restricted commit/push adapter for the user's real
|
|
13409
13435
|
# directory: never `git add -A` sensitive paths, never
|
|
@@ -13852,6 +13878,25 @@ class RuntimeDaemon:
|
|
|
13852
13878
|
req_type = input_data.get("requirement_type", "feature")
|
|
13853
13879
|
verification_work_item = bool(input_data.get("verification_work_item"))
|
|
13854
13880
|
|
|
13881
|
+
audit_contract = input_data.get("implementation_audit_contract")
|
|
13882
|
+
if isinstance(audit_contract, dict):
|
|
13883
|
+
agent_id = str(result.metrics.get("actual_agent") or getattr(task, "agent_type", "") or "")
|
|
13884
|
+
audit_report = agent_core.extract_implementation_audit(_daemon_extract_agent_output(agent_id, result.stdout))
|
|
13885
|
+
result.observations = [
|
|
13886
|
+
observation for observation in result.observations
|
|
13887
|
+
if observation.get("type") != "implementation_audit"
|
|
13888
|
+
]
|
|
13889
|
+
if isinstance(audit_report, dict):
|
|
13890
|
+
result.observations.append({"type": "implementation_audit", "data": audit_report})
|
|
13891
|
+
audit_result = agent_core.assess_implementation_audit(audit_contract, audit_report)
|
|
13892
|
+
if audit_result["report_valid"] and audit_result["revision_requests"]:
|
|
13893
|
+
result.status = "failed"
|
|
13894
|
+
result.failure_code = "implementation_plan_revision_required"
|
|
13895
|
+
result.error = "Approved analysis and task plan require revision before implementation can continue"
|
|
13896
|
+
issues.append(result.error)
|
|
13897
|
+
elif not audit_result["report_valid"] or not audit_result["node_complete"]:
|
|
13898
|
+
issues.extend(audit_result["issues"] or ["The selected approved task still has implementation gaps"])
|
|
13899
|
+
|
|
13855
13900
|
if node_type == "coding" or (
|
|
13856
13901
|
node_type == "fix" and input_data.get("task") == "phase_remediation_fix"
|
|
13857
13902
|
):
|
|
@@ -14319,6 +14364,7 @@ class RuntimeDaemon:
|
|
|
14319
14364
|
))
|
|
14320
14365
|
|
|
14321
14366
|
# --- coverage-matrix.json validation ---
|
|
14367
|
+
result.metrics.pop("acceptance_verification", None)
|
|
14322
14368
|
if req_type == "task":
|
|
14323
14369
|
result.metrics.pop("acceptance_criteria_manual_review_required", None)
|
|
14324
14370
|
cm_path = base / "coverage-matrix.json"
|
|
@@ -14361,16 +14407,38 @@ class RuntimeDaemon:
|
|
|
14361
14407
|
"coverage_matrix_invalid"
|
|
14362
14408
|
)
|
|
14363
14409
|
else:
|
|
14364
|
-
|
|
14410
|
+
pending, contract_issues = (
|
|
14411
|
+
runtime_evidence.validate_deferred_acceptance_criteria(
|
|
14412
|
+
_input, cm_data, _tc_data,
|
|
14413
|
+
)
|
|
14414
|
+
)
|
|
14415
|
+
issues.extend(contract_issues)
|
|
14416
|
+
if pending:
|
|
14417
|
+
contract = _input["deferred_acceptance_verification"]
|
|
14418
|
+
result.metrics["acceptance_verification"] = {
|
|
14419
|
+
"status": "pending_final_qa",
|
|
14420
|
+
"analysis_revision_id": contract["analysis_revision_id"],
|
|
14421
|
+
"criteria": [ac["id"] for ac in ac_list if ac["id"] in pending],
|
|
14422
|
+
}
|
|
14423
|
+
uncovered = [
|
|
14424
|
+
ac for ac in ac_list
|
|
14425
|
+
if ac["status"] != "covered" and ac["id"] not in pending
|
|
14426
|
+
]
|
|
14365
14427
|
if uncovered:
|
|
14366
|
-
ids = ", ".join(ac["id"] for ac in uncovered[:
|
|
14428
|
+
ids = ", ".join(ac["id"] for ac in uncovered[:20])
|
|
14429
|
+
if len(uncovered) > 20:
|
|
14430
|
+
ids += f" (+{len(uncovered) - 20} more)"
|
|
14367
14431
|
issues.append(
|
|
14368
14432
|
"Uncovered acceptance criteria in "
|
|
14369
14433
|
f"coverage-matrix.json: {ids}"
|
|
14370
14434
|
)
|
|
14371
14435
|
except (_json.JSONDecodeError, UnicodeDecodeError) as e:
|
|
14372
14436
|
issues.append(f"coverage-matrix.json is not valid JSON: {e}")
|
|
14373
|
-
elif
|
|
14437
|
+
elif (
|
|
14438
|
+
_requires_structured_artifacts
|
|
14439
|
+
or _requires_e2e_artifacts
|
|
14440
|
+
or _input.get("deferred_acceptance_verification") is not None
|
|
14441
|
+
):
|
|
14374
14442
|
issues.append(f"coverage-matrix.json not found in {doc_dir or 'workspace root'}")
|
|
14375
14443
|
|
|
14376
14444
|
# --- test-evidence.json validation (M1 evidence bundle) ---
|
|
@@ -15795,6 +15863,8 @@ class RuntimeDaemon:
|
|
|
15795
15863
|
preserved_paths=preserved_paths,
|
|
15796
15864
|
)
|
|
15797
15865
|
issues = self._validate_outputs(workspace_path, task, result)
|
|
15866
|
+
if result.failure_code == "implementation_plan_revision_required":
|
|
15867
|
+
return result
|
|
15798
15868
|
if require_source_inventory:
|
|
15799
15869
|
issues.extend(inventory_issues)
|
|
15800
15870
|
if not issues:
|
|
@@ -15922,6 +15992,8 @@ class RuntimeDaemon:
|
|
|
15922
15992
|
preserved_paths=preserved_paths,
|
|
15923
15993
|
)
|
|
15924
15994
|
remaining = self._validate_outputs(workspace_path, task, result)
|
|
15995
|
+
if result.failure_code == "implementation_plan_revision_required":
|
|
15996
|
+
return result
|
|
15925
15997
|
if require_source_inventory:
|
|
15926
15998
|
remaining.extend(inventory_issues)
|
|
15927
15999
|
if remaining:
|
|
@@ -15,6 +15,7 @@ from pathlib import Path
|
|
|
15
15
|
from typing import Any
|
|
16
16
|
|
|
17
17
|
RUNTIME_EVIDENCE_PLAN_VERSION = 1
|
|
18
|
+
DEFERRED_ACCEPTANCE_VERIFICATION_PROTOCOL_VERSION = 2
|
|
18
19
|
TEST_COVERAGE_CHECK_KIND = "test_coverage"
|
|
19
20
|
STATIC_CHECK_KIND = "static_check"
|
|
20
21
|
TYPE_CHECK_KIND = "type_check"
|
|
@@ -783,6 +784,160 @@ def parse_acceptance_criteria_coverage_artifacts(
|
|
|
783
784
|
}
|
|
784
785
|
|
|
785
786
|
|
|
787
|
+
def validate_deferred_acceptance_criteria(
|
|
788
|
+
input_data: dict[str, Any],
|
|
789
|
+
coverage_matrix: dict[str, Any],
|
|
790
|
+
test_cases: dict[str, Any] | None,
|
|
791
|
+
) -> tuple[set[str], list[str]]:
|
|
792
|
+
"""Validate a server-owned final-QA handoff without claiming test success."""
|
|
793
|
+
contract = input_data.get("deferred_acceptance_verification")
|
|
794
|
+
if contract is None:
|
|
795
|
+
return set(), []
|
|
796
|
+
snapshot = input_data.get("qa_test_level_policy_snapshot") or {}
|
|
797
|
+
policy = snapshot.get("policy") if isinstance(snapshot, dict) else None
|
|
798
|
+
levels = policy.get("levels") if isinstance(policy, dict) else None
|
|
799
|
+
e2e_policy = levels.get("e2e") if isinstance(levels, dict) else None
|
|
800
|
+
if (
|
|
801
|
+
not isinstance(contract, dict)
|
|
802
|
+
or not isinstance(snapshot, dict)
|
|
803
|
+
or not isinstance(policy, dict)
|
|
804
|
+
or policy.get("enforcement") != "enforce"
|
|
805
|
+
or not isinstance(e2e_policy, dict)
|
|
806
|
+
or type(contract.get("version")) is not int
|
|
807
|
+
or contract["version"] not in (1, DEFERRED_ACCEPTANCE_VERIFICATION_PROTOCOL_VERSION)
|
|
808
|
+
or contract.get("execution_phase") != "verification_final"
|
|
809
|
+
or not isinstance(snapshot.get("fingerprint"), str)
|
|
810
|
+
or contract.get("qa_test_level_policy_fingerprint") != snapshot["fingerprint"]
|
|
811
|
+
or (
|
|
812
|
+
contract["version"] == 1
|
|
813
|
+
and (
|
|
814
|
+
e2e_policy.get("enabled") is not True
|
|
815
|
+
or e2e_policy.get("workflow_auto_run") is not True
|
|
816
|
+
)
|
|
817
|
+
)
|
|
818
|
+
):
|
|
819
|
+
return set(), ["Final QA acceptance contract does not match the frozen testing policy"]
|
|
820
|
+
|
|
821
|
+
raw_expected = contract.get("acceptance_criteria_ids")
|
|
822
|
+
raw_deferred = contract.get("criteria")
|
|
823
|
+
if (
|
|
824
|
+
not isinstance(raw_expected, list)
|
|
825
|
+
or not raw_expected
|
|
826
|
+
or not isinstance(raw_deferred, list)
|
|
827
|
+
or not raw_deferred
|
|
828
|
+
):
|
|
829
|
+
return set(), ["Final QA acceptance contract has no frozen acceptance criteria"]
|
|
830
|
+
expected = {_canonical_acceptance_criteria_reference(value) for value in raw_expected}
|
|
831
|
+
criteria = coverage_matrix["acceptance_criteria"]
|
|
832
|
+
identifiers = [_canonical_acceptance_criteria_reference(item["id"]) for item in criteria]
|
|
833
|
+
if None in expected or None in identifiers or len(set(identifiers)) != len(identifiers):
|
|
834
|
+
return set(), ["coverage-matrix.json has invalid or duplicate acceptance criterion IDs"]
|
|
835
|
+
if set(identifiers) != expected:
|
|
836
|
+
return set(), ["coverage-matrix.json does not match the frozen acceptance criteria"]
|
|
837
|
+
|
|
838
|
+
cases = (test_cases or {}).get("test_cases") or []
|
|
839
|
+
if not isinstance(cases, list):
|
|
840
|
+
return set(), ["Final QA acceptance requires a test_cases array"]
|
|
841
|
+
prepared_intents = {
|
|
842
|
+
case.get("intent_id") or case.get("test_intent_id")
|
|
843
|
+
for case in cases
|
|
844
|
+
if isinstance(case, dict)
|
|
845
|
+
and case.get("test_type") == "e2e"
|
|
846
|
+
and isinstance(case.get("intent_id") or case.get("test_intent_id"), str)
|
|
847
|
+
and case.get("id")
|
|
848
|
+
and case.get("script_path")
|
|
849
|
+
and case.get("script_content")
|
|
850
|
+
}
|
|
851
|
+
eligible = set()
|
|
852
|
+
for item in raw_deferred:
|
|
853
|
+
if not isinstance(item, dict):
|
|
854
|
+
return set(), ["Final QA acceptance contract has an invalid criterion mapping"]
|
|
855
|
+
identifier = _canonical_acceptance_criteria_reference(item.get("id"))
|
|
856
|
+
intent_ids = item.get("test_intent_ids")
|
|
857
|
+
if (
|
|
858
|
+
identifier not in expected
|
|
859
|
+
or not isinstance(intent_ids, list)
|
|
860
|
+
or not intent_ids
|
|
861
|
+
or any(not isinstance(value, str) or not value for value in intent_ids)
|
|
862
|
+
):
|
|
863
|
+
return set(), ["Final QA acceptance contract has an invalid test intent mapping"]
|
|
864
|
+
if contract["version"] == 1:
|
|
865
|
+
if not set(intent_ids).issubset(prepared_intents):
|
|
866
|
+
return set(), [f"Final QA acceptance criterion {identifier} has no prepared E2E test case"]
|
|
867
|
+
else:
|
|
868
|
+
requirements = item.get("test_intents")
|
|
869
|
+
if (
|
|
870
|
+
not isinstance(requirements, list)
|
|
871
|
+
or len(requirements) != len(intent_ids)
|
|
872
|
+
or any(not isinstance(requirement, dict) for requirement in requirements)
|
|
873
|
+
or any(
|
|
874
|
+
not isinstance(requirement.get("id"), str)
|
|
875
|
+
for requirement in requirements
|
|
876
|
+
)
|
|
877
|
+
or len(set(intent_ids)) != len(intent_ids)
|
|
878
|
+
or {requirement.get("id") for requirement in requirements} != set(intent_ids)
|
|
879
|
+
):
|
|
880
|
+
return set(), ["Final QA acceptance contract has invalid verification obligations"]
|
|
881
|
+
for requirement in requirements:
|
|
882
|
+
target = requirement.get("automation_target")
|
|
883
|
+
expected_refs = requirement.get("acceptance_criteria_refs")
|
|
884
|
+
if (
|
|
885
|
+
not isinstance(expected_refs, list)
|
|
886
|
+
or any(not isinstance(reference, str) for reference in expected_refs)
|
|
887
|
+
or identifier not in expected_refs
|
|
888
|
+
or not set(expected_refs).issubset(expected)
|
|
889
|
+
):
|
|
890
|
+
return set(), ["Final QA acceptance contract has invalid verification references"]
|
|
891
|
+
if target == "e2e":
|
|
892
|
+
prepared = (
|
|
893
|
+
e2e_policy.get("enabled") is True
|
|
894
|
+
and e2e_policy.get("workflow_auto_run") is True
|
|
895
|
+
and requirement["id"] in prepared_intents
|
|
896
|
+
)
|
|
897
|
+
elif target == "manual":
|
|
898
|
+
reason = requirement.get("manual_reason_code")
|
|
899
|
+
if not isinstance(reason, str) or not reason.strip():
|
|
900
|
+
return set(), ["Final QA acceptance contract has invalid manual obligations"]
|
|
901
|
+
prepared = any(
|
|
902
|
+
isinstance(case, dict)
|
|
903
|
+
and isinstance(case.get("id"), str)
|
|
904
|
+
and bool(case["id"].strip())
|
|
905
|
+
and (case.get("intent_id") or case.get("test_intent_id")) == requirement["id"]
|
|
906
|
+
and case.get("test_type") == "manual"
|
|
907
|
+
and case.get("manual_reason_code") == reason
|
|
908
|
+
and isinstance(case.get("acceptance_criteria_refs"), list)
|
|
909
|
+
and {
|
|
910
|
+
_canonical_acceptance_criteria_reference(reference)
|
|
911
|
+
for reference in case["acceptance_criteria_refs"]
|
|
912
|
+
} == set(expected_refs)
|
|
913
|
+
and isinstance(case.get("steps"), list)
|
|
914
|
+
and bool(case["steps"])
|
|
915
|
+
and all(
|
|
916
|
+
isinstance(step, dict)
|
|
917
|
+
and isinstance(step.get("action"), str)
|
|
918
|
+
and bool(step["action"].strip())
|
|
919
|
+
for step in case["steps"]
|
|
920
|
+
)
|
|
921
|
+
and not case.get("script_path")
|
|
922
|
+
and not case.get("script_content")
|
|
923
|
+
for case in cases
|
|
924
|
+
)
|
|
925
|
+
else:
|
|
926
|
+
return set(), ["Final QA acceptance contract has an unsupported verification target"]
|
|
927
|
+
if not prepared:
|
|
928
|
+
return set(), [
|
|
929
|
+
f"Final QA acceptance criterion {identifier} has no prepared "
|
|
930
|
+
f"{target} test case for intent {requirement['id']}"
|
|
931
|
+
]
|
|
932
|
+
eligible.add(identifier)
|
|
933
|
+
return {
|
|
934
|
+
item["id"] for item, identifier in zip(criteria, identifiers)
|
|
935
|
+
if identifier in eligible
|
|
936
|
+
and item["status"] == "unverifiable"
|
|
937
|
+
and item.get("covered_by", []) == []
|
|
938
|
+
}, []
|
|
939
|
+
|
|
940
|
+
|
|
786
941
|
async def run_acceptance_criteria_coverage_evidence(
|
|
787
942
|
workspace_path: str | Path,
|
|
788
943
|
check: object,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|