forgexa-cli 1.76.3__tar.gz → 1.77.1__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.3 → forgexa_cli-1.77.1}/PKG-INFO +1 -1
- {forgexa_cli-1.76.3 → forgexa_cli-1.77.1}/forgexa_cli/__init__.py +1 -1
- {forgexa_cli-1.76.3 → forgexa_cli-1.77.1}/forgexa_cli/agent_core.py +94 -0
- {forgexa_cli-1.76.3 → forgexa_cli-1.77.1}/forgexa_cli/daemon.py +45 -12
- {forgexa_cli-1.76.3 → forgexa_cli-1.77.1}/forgexa_cli/runtime_evidence.py +80 -5
- {forgexa_cli-1.76.3 → forgexa_cli-1.77.1}/forgexa_cli.egg-info/PKG-INFO +1 -1
- {forgexa_cli-1.76.3 → forgexa_cli-1.77.1}/pyproject.toml +1 -1
- {forgexa_cli-1.76.3 → forgexa_cli-1.77.1}/README.md +0 -0
- {forgexa_cli-1.76.3 → forgexa_cli-1.77.1}/forgexa_cli/_build_config.py +0 -0
- {forgexa_cli-1.76.3 → forgexa_cli-1.77.1}/forgexa_cli/_local_bind.py +0 -0
- {forgexa_cli-1.76.3 → forgexa_cli-1.77.1}/forgexa_cli/autoupgrade.py +0 -0
- {forgexa_cli-1.76.3 → forgexa_cli-1.77.1}/forgexa_cli/aws_evidence.py +0 -0
- {forgexa_cli-1.76.3 → forgexa_cli-1.77.1}/forgexa_cli/aws_logs_evidence.py +0 -0
- {forgexa_cli-1.76.3 → forgexa_cli-1.77.1}/forgexa_cli/main.py +0 -0
- {forgexa_cli-1.76.3 → forgexa_cli-1.77.1}/forgexa_cli/py.typed +0 -0
- {forgexa_cli-1.76.3 → forgexa_cli-1.77.1}/forgexa_cli/zcode_protocol.py +0 -0
- {forgexa_cli-1.76.3 → forgexa_cli-1.77.1}/forgexa_cli.egg-info/SOURCES.txt +0 -0
- {forgexa_cli-1.76.3 → forgexa_cli-1.77.1}/forgexa_cli.egg-info/dependency_links.txt +0 -0
- {forgexa_cli-1.76.3 → forgexa_cli-1.77.1}/forgexa_cli.egg-info/entry_points.txt +0 -0
- {forgexa_cli-1.76.3 → forgexa_cli-1.77.1}/forgexa_cli.egg-info/requires.txt +0 -0
- {forgexa_cli-1.76.3 → forgexa_cli-1.77.1}/forgexa_cli.egg-info/top_level.txt +0 -0
- {forgexa_cli-1.76.3 → forgexa_cli-1.77.1}/setup.cfg +0 -0
- {forgexa_cli-1.76.3 → forgexa_cli-1.77.1}/tests/test_auth_and_runtime_commands.py +0 -0
- {forgexa_cli-1.76.3 → forgexa_cli-1.77.1}/tests/test_autoupgrade.py +0 -0
- {forgexa_cli-1.76.3 → forgexa_cli-1.77.1}/tests/test_check_command.py +0 -0
- {forgexa_cli-1.76.3 → forgexa_cli-1.77.1}/tests/test_expiry_warnings_and_revoke.py +0 -0
- {forgexa_cli-1.76.3 → forgexa_cli-1.77.1}/tests/test_local_bind_commands.py +0 -0
- {forgexa_cli-1.76.3 → forgexa_cli-1.77.1}/tests/test_runtime_credentials.py +0 -0
- {forgexa_cli-1.76.3 → forgexa_cli-1.77.1}/tests/test_session_credentials.py +0 -0
- {forgexa_cli-1.76.3 → forgexa_cli-1.77.1}/tests/test_silent_install.py +0 -0
- {forgexa_cli-1.76.3 → forgexa_cli-1.77.1}/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.1"
|
|
@@ -67,6 +67,15 @@ except ImportError:
|
|
|
67
67
|
logger = logging.getLogger("agent_core")
|
|
68
68
|
|
|
69
69
|
|
|
70
|
+
def is_implementation_testing_task(node_type: str, input_data: dict) -> bool:
|
|
71
|
+
"""Retain the testing responsibility of a phase-remediation wrapper."""
|
|
72
|
+
fix_context = input_data.get("fix_context")
|
|
73
|
+
return node_type == "testing" or (
|
|
74
|
+
node_type == "fix" and input_data.get("task") == "phase_remediation_fix"
|
|
75
|
+
and isinstance(fix_context, dict) and fix_context.get("failed_node_type") == "testing"
|
|
76
|
+
)
|
|
77
|
+
|
|
78
|
+
|
|
70
79
|
def missing_planned_task_files(task_input: dict, changed_files: list[str]) -> list[str]:
|
|
71
80
|
"""Find planned files without delivery evidence, including grouped sub-tasks."""
|
|
72
81
|
def normalize_path(value: object) -> str | None:
|
|
@@ -103,6 +112,61 @@ def missing_planned_task_files(task_input: dict, changed_files: list[str]) -> li
|
|
|
103
112
|
)
|
|
104
113
|
|
|
105
114
|
|
|
115
|
+
def extract_implementation_audit(text: str) -> dict | None:
|
|
116
|
+
"""Read the last complete audit envelope from mixed assistant output."""
|
|
117
|
+
decoder = json.JSONDecoder()
|
|
118
|
+
offset = 0
|
|
119
|
+
report = None
|
|
120
|
+
report_end = 0
|
|
121
|
+
while offset < len(text):
|
|
122
|
+
start = text.find("{", offset)
|
|
123
|
+
if start < 0:
|
|
124
|
+
break
|
|
125
|
+
try:
|
|
126
|
+
value, end = decoder.raw_decode(text, start)
|
|
127
|
+
except json.JSONDecodeError:
|
|
128
|
+
offset = start + 1
|
|
129
|
+
continue
|
|
130
|
+
if isinstance(value, dict) and "implementation_audit" in value:
|
|
131
|
+
report = value["implementation_audit"] if isinstance(value["implementation_audit"], dict) else None
|
|
132
|
+
report_end = end
|
|
133
|
+
offset = end
|
|
134
|
+
if '"implementation_audit"' in text[report_end:]:
|
|
135
|
+
return None
|
|
136
|
+
return report
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
def redact_implementation_audit(contract: dict, report: object) -> dict | None:
|
|
140
|
+
"""Keep contract-owned status and hashed evidence without local free text."""
|
|
141
|
+
from hashlib import sha256
|
|
142
|
+
|
|
143
|
+
if not assess_implementation_audit(contract, report)["report_valid"]:
|
|
144
|
+
return None
|
|
145
|
+
summary = {"audit_id": contract["audit_id"], "task_checks": [], "acceptance_checks": []}
|
|
146
|
+
for key in ("task_checks", "acceptance_checks"):
|
|
147
|
+
for row in report[key]:
|
|
148
|
+
evidence = []
|
|
149
|
+
for reference in row["evidence"][:20]:
|
|
150
|
+
digest = reference.removeprefix("local-evidence-sha256:")
|
|
151
|
+
evidence.append(reference if digest != reference and len(digest) == 64
|
|
152
|
+
and all(character in "0123456789abcdef" for character in digest)
|
|
153
|
+
else "local-evidence-sha256:" + sha256(reference.encode()).hexdigest())
|
|
154
|
+
check = {"status": row["status"], "evidence": evidence}
|
|
155
|
+
if key == "task_checks":
|
|
156
|
+
check["task_id"] = row["task_id"]
|
|
157
|
+
else:
|
|
158
|
+
check.update(index=row["index"], task_ids=row["task_ids"])
|
|
159
|
+
if row["status"] != "covered":
|
|
160
|
+
check.update(description="Local implementation issue remains unresolved; inspect local task evidence.", files=[])
|
|
161
|
+
if row.get("plan_revision"):
|
|
162
|
+
check["plan_revision"] = {
|
|
163
|
+
"reason": "Local evidence conflicts with the approved plan",
|
|
164
|
+
"proposal": "Inspect the source in the requirement workspace and revise analysis and tasks together",
|
|
165
|
+
}
|
|
166
|
+
summary[key].append(check)
|
|
167
|
+
return summary
|
|
168
|
+
|
|
169
|
+
|
|
106
170
|
def assess_implementation_audit(contract: dict, report: object) -> dict:
|
|
107
171
|
"""Validate implementation coverage independently of later runtime acceptance."""
|
|
108
172
|
issues: list[str] = []
|
|
@@ -111,6 +175,7 @@ def assess_implementation_audit(contract: dict, report: object) -> dict:
|
|
|
111
175
|
"audit_id": contract.get("audit_id"), "report_valid": False,
|
|
112
176
|
"node_complete": False, "status": "incomplete", "issues": issues,
|
|
113
177
|
"findings": findings, "followup_task_ids": [], "report": report,
|
|
178
|
+
"revision_requests": [], "blocking_reasons": [],
|
|
114
179
|
}
|
|
115
180
|
if (
|
|
116
181
|
not isinstance(contract.get("audit_id"), str) or not contract["audit_id"]
|
|
@@ -156,6 +221,12 @@ def assess_implementation_audit(contract: dict, report: object) -> dict:
|
|
|
156
221
|
else:
|
|
157
222
|
index = row.get("index")
|
|
158
223
|
related = row.get("task_ids")
|
|
224
|
+
if type(index) is int and not 1 <= index <= criterion_count:
|
|
225
|
+
issues.append(
|
|
226
|
+
f"Acceptance index {index} is outside the frozen baseline 1-{criterion_count}; "
|
|
227
|
+
"report the baseline conflict instead of dropping or renumbering requirements"
|
|
228
|
+
)
|
|
229
|
+
continue
|
|
159
230
|
if (
|
|
160
231
|
type(index) is not int or not 1 <= index <= criterion_count or index in checked_criteria
|
|
161
232
|
or not isinstance(related, list) or not related
|
|
@@ -164,6 +235,10 @@ def assess_implementation_audit(contract: dict, report: object) -> dict:
|
|
|
164
235
|
issues.append("Implementation audit has invalid acceptance criterion or task references")
|
|
165
236
|
continue
|
|
166
237
|
checked_criteria.add(index)
|
|
238
|
+
if len(related) > 1:
|
|
239
|
+
unresolved_contributors = [task_id for task_id in related if checked.get(task_id) != "covered"]
|
|
240
|
+
if unresolved_contributors:
|
|
241
|
+
related = unresolved_contributors
|
|
167
242
|
if status != "covered":
|
|
168
243
|
description = row.get("description")
|
|
169
244
|
paths = row.get("files") or []
|
|
@@ -177,9 +252,22 @@ def assess_implementation_audit(contract: dict, report: object) -> dict:
|
|
|
177
252
|
):
|
|
178
253
|
issues.append("Implementation gap paths must be repository-relative")
|
|
179
254
|
continue
|
|
255
|
+
revision = row.get("plan_revision")
|
|
256
|
+
if revision is not None:
|
|
257
|
+
if not isinstance(revision, dict) or any(
|
|
258
|
+
not isinstance(revision.get(key), str) or not revision[key].strip()
|
|
259
|
+
for key in ("reason", "proposal")
|
|
260
|
+
):
|
|
261
|
+
issues.append("Plan revision requests require evidence, reason and a concrete proposal")
|
|
262
|
+
continue
|
|
263
|
+
result["revision_requests"].extend({
|
|
264
|
+
"task_id": task_id, "reason": revision["reason"], "proposal": revision["proposal"],
|
|
265
|
+
"evidence": evidence, "files": paths,
|
|
266
|
+
} for task_id in related)
|
|
180
267
|
findings.extend({
|
|
181
268
|
"task_id": task_id, "status": status, "description": description,
|
|
182
269
|
"files": paths, "evidence": evidence,
|
|
270
|
+
**({"plan_revision": revision} if revision is not None else {}),
|
|
183
271
|
} for task_id in related)
|
|
184
272
|
required = expected if contract.get("final_verification") else {subject}
|
|
185
273
|
if required - checked.keys():
|
|
@@ -191,8 +279,14 @@ def assess_implementation_audit(contract: dict, report: object) -> dict:
|
|
|
191
279
|
not issues and checked.get(subject) == "covered"
|
|
192
280
|
and not any(finding["task_id"] == subject for finding in findings)
|
|
193
281
|
)
|
|
282
|
+
result["blocking_reasons"] = list(dict.fromkeys(
|
|
283
|
+
f"Approved task '{subject}' is {finding['status']}: {finding['description']}"
|
|
284
|
+
for finding in findings if finding["task_id"] == subject
|
|
285
|
+
))
|
|
194
286
|
result["followup_task_ids"] = sorted({finding["task_id"] for finding in findings})
|
|
195
287
|
result["status"] = "complete" if not issues and not findings else "incomplete"
|
|
288
|
+
if result["report_valid"] and result["revision_requests"]:
|
|
289
|
+
result["status"] = "plan_revision_required"
|
|
196
290
|
return result
|
|
197
291
|
|
|
198
292
|
|
|
@@ -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.1"
|
|
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
|
},
|
|
@@ -13410,8 +13424,9 @@ class RuntimeDaemon:
|
|
|
13410
13424
|
task.task_id, 94, "acceptance_pending_final_qa",
|
|
13411
13425
|
execution_attempt_id=task.execution_attempt_id,
|
|
13412
13426
|
output_lines=[
|
|
13413
|
-
"[daemon]
|
|
13414
|
-
"post-deployment
|
|
13427
|
+
"[daemon] Verification assets are prepared; acceptance remains pending "
|
|
13428
|
+
"post-deployment automated or human verification: "
|
|
13429
|
+
+ ", ".join(acceptance["criteria"]),
|
|
13415
13430
|
"[daemon] No browser acceptance or release approval is claimed.",
|
|
13416
13431
|
],
|
|
13417
13432
|
)
|
|
@@ -13860,14 +13875,15 @@ class RuntimeDaemon:
|
|
|
13860
13875
|
issues: list[str] = []
|
|
13861
13876
|
node_type = task.node_type
|
|
13862
13877
|
input_data = task.input_data or {}
|
|
13878
|
+
if agent_core.is_implementation_testing_task(node_type, input_data):
|
|
13879
|
+
node_type = "testing"
|
|
13863
13880
|
req_type = input_data.get("requirement_type", "feature")
|
|
13864
13881
|
verification_work_item = bool(input_data.get("verification_work_item"))
|
|
13865
13882
|
|
|
13866
13883
|
audit_contract = input_data.get("implementation_audit_contract")
|
|
13867
13884
|
if isinstance(audit_contract, dict):
|
|
13868
13885
|
agent_id = str(result.metrics.get("actual_agent") or getattr(task, "agent_type", "") or "")
|
|
13869
|
-
|
|
13870
|
-
audit_report = parsed.get("implementation_audit") if isinstance(parsed, dict) else None
|
|
13886
|
+
audit_report = agent_core.extract_implementation_audit(_daemon_extract_agent_output(agent_id, result.stdout))
|
|
13871
13887
|
result.observations = [
|
|
13872
13888
|
observation for observation in result.observations
|
|
13873
13889
|
if observation.get("type") != "implementation_audit"
|
|
@@ -13875,8 +13891,15 @@ class RuntimeDaemon:
|
|
|
13875
13891
|
if isinstance(audit_report, dict):
|
|
13876
13892
|
result.observations.append({"type": "implementation_audit", "data": audit_report})
|
|
13877
13893
|
audit_result = agent_core.assess_implementation_audit(audit_contract, audit_report)
|
|
13878
|
-
if
|
|
13879
|
-
|
|
13894
|
+
if audit_result["report_valid"] and audit_result["revision_requests"]:
|
|
13895
|
+
result.status = "failed"
|
|
13896
|
+
result.failure_code = "implementation_plan_revision_required"
|
|
13897
|
+
result.error = "Approved analysis and task plan require revision before implementation can continue"
|
|
13898
|
+
issues.append(result.error)
|
|
13899
|
+
elif not audit_result["report_valid"] or not audit_result["node_complete"]:
|
|
13900
|
+
issues.extend(audit_result["issues"] or audit_result["blocking_reasons"] or [
|
|
13901
|
+
"The selected approved task still has implementation gaps",
|
|
13902
|
+
])
|
|
13880
13903
|
|
|
13881
13904
|
if node_type == "coding" or (
|
|
13882
13905
|
node_type == "fix" and input_data.get("task") == "phase_remediation_fix"
|
|
@@ -14415,7 +14438,11 @@ class RuntimeDaemon:
|
|
|
14415
14438
|
)
|
|
14416
14439
|
except (_json.JSONDecodeError, UnicodeDecodeError) as e:
|
|
14417
14440
|
issues.append(f"coverage-matrix.json is not valid JSON: {e}")
|
|
14418
|
-
elif
|
|
14441
|
+
elif (
|
|
14442
|
+
_requires_structured_artifacts
|
|
14443
|
+
or _requires_e2e_artifacts
|
|
14444
|
+
or _input.get("deferred_acceptance_verification") is not None
|
|
14445
|
+
):
|
|
14419
14446
|
issues.append(f"coverage-matrix.json not found in {doc_dir or 'workspace root'}")
|
|
14420
14447
|
|
|
14421
14448
|
# --- test-evidence.json validation (M1 evidence bundle) ---
|
|
@@ -15840,6 +15867,8 @@ class RuntimeDaemon:
|
|
|
15840
15867
|
preserved_paths=preserved_paths,
|
|
15841
15868
|
)
|
|
15842
15869
|
issues = self._validate_outputs(workspace_path, task, result)
|
|
15870
|
+
if result.failure_code == "implementation_plan_revision_required":
|
|
15871
|
+
return result
|
|
15843
15872
|
if require_source_inventory:
|
|
15844
15873
|
issues.extend(inventory_issues)
|
|
15845
15874
|
if not issues:
|
|
@@ -15855,7 +15884,7 @@ class RuntimeDaemon:
|
|
|
15855
15884
|
])
|
|
15856
15885
|
|
|
15857
15886
|
if (
|
|
15858
|
-
task.node_type
|
|
15887
|
+
agent_core.is_implementation_testing_task(task.node_type, task.input_data or {})
|
|
15859
15888
|
and all(
|
|
15860
15889
|
issue.startswith("Uncovered acceptance criteria in coverage-matrix.json:")
|
|
15861
15890
|
for issue in issues
|
|
@@ -15967,6 +15996,8 @@ class RuntimeDaemon:
|
|
|
15967
15996
|
preserved_paths=preserved_paths,
|
|
15968
15997
|
)
|
|
15969
15998
|
remaining = self._validate_outputs(workspace_path, task, result)
|
|
15999
|
+
if result.failure_code == "implementation_plan_revision_required":
|
|
16000
|
+
return result
|
|
15970
16001
|
if require_source_inventory:
|
|
15971
16002
|
remaining.extend(inventory_issues)
|
|
15972
16003
|
if remaining:
|
|
@@ -15976,7 +16007,7 @@ class RuntimeDaemon:
|
|
|
15976
16007
|
*[f"[daemon] {issue}" for issue in remaining],
|
|
15977
16008
|
])
|
|
15978
16009
|
if (
|
|
15979
|
-
task.node_type
|
|
16010
|
+
agent_core.is_implementation_testing_task(task.node_type, task.input_data or {})
|
|
15980
16011
|
and all(
|
|
15981
16012
|
issue.startswith("Uncovered acceptance criteria in coverage-matrix.json:")
|
|
15982
16013
|
for issue in remaining
|
|
@@ -16135,7 +16166,7 @@ class RuntimeDaemon:
|
|
|
16135
16166
|
result.metrics["validation_issue_categories"] = sorted(
|
|
16136
16167
|
{_categorize_validation_issue(str(issue)) for issue in issues}
|
|
16137
16168
|
)
|
|
16138
|
-
if task.node_type
|
|
16169
|
+
if not agent_core.is_implementation_testing_task(task.node_type, task.input_data or {}):
|
|
16139
16170
|
return
|
|
16140
16171
|
input_data = task.input_data or {}
|
|
16141
16172
|
context = input_data.get("context") or {}
|
|
@@ -17589,7 +17620,9 @@ class RuntimeDaemon:
|
|
|
17589
17620
|
before_sha: str,
|
|
17590
17621
|
) -> None:
|
|
17591
17622
|
"""Bind generated testing artifacts to the committed testing attempt."""
|
|
17592
|
-
if task.workspace_resolution != "cloud_worktree" or
|
|
17623
|
+
if task.workspace_resolution != "cloud_worktree" or not agent_core.is_implementation_testing_task(
|
|
17624
|
+
task.node_type, task.input_data or {},
|
|
17625
|
+
):
|
|
17593
17626
|
return
|
|
17594
17627
|
|
|
17595
17628
|
after_sha = str((result.git or {}).get("commit_sha") or "").strip()
|
|
@@ -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"
|
|
@@ -802,12 +803,18 @@ def validate_deferred_acceptance_criteria(
|
|
|
802
803
|
or not isinstance(policy, dict)
|
|
803
804
|
or policy.get("enforcement") != "enforce"
|
|
804
805
|
or not isinstance(e2e_policy, dict)
|
|
805
|
-
or contract.get("version")
|
|
806
|
+
or type(contract.get("version")) is not int
|
|
807
|
+
or contract["version"] not in (1, DEFERRED_ACCEPTANCE_VERIFICATION_PROTOCOL_VERSION)
|
|
806
808
|
or contract.get("execution_phase") != "verification_final"
|
|
807
809
|
or not isinstance(snapshot.get("fingerprint"), str)
|
|
808
810
|
or contract.get("qa_test_level_policy_fingerprint") != snapshot["fingerprint"]
|
|
809
|
-
or
|
|
810
|
-
|
|
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
|
+
)
|
|
811
818
|
):
|
|
812
819
|
return set(), ["Final QA acceptance contract does not match the frozen testing policy"]
|
|
813
820
|
|
|
@@ -829,6 +836,8 @@ def validate_deferred_acceptance_criteria(
|
|
|
829
836
|
return set(), ["coverage-matrix.json does not match the frozen acceptance criteria"]
|
|
830
837
|
|
|
831
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"]
|
|
832
841
|
prepared_intents = {
|
|
833
842
|
case.get("intent_id") or case.get("test_intent_id")
|
|
834
843
|
for case in cases
|
|
@@ -852,8 +861,74 @@ def validate_deferred_acceptance_criteria(
|
|
|
852
861
|
or any(not isinstance(value, str) or not value for value in intent_ids)
|
|
853
862
|
):
|
|
854
863
|
return set(), ["Final QA acceptance contract has an invalid test intent mapping"]
|
|
855
|
-
if
|
|
856
|
-
|
|
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
|
+
]
|
|
857
932
|
eligible.add(identifier)
|
|
858
933
|
return {
|
|
859
934
|
item["id"] for item, identifier in zip(criteria, identifiers)
|
|
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
|