forgexa-cli 1.76.3__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.3 → forgexa_cli-1.77.0}/PKG-INFO +1 -1
- {forgexa_cli-1.76.3 → forgexa_cli-1.77.0}/forgexa_cli/__init__.py +1 -1
- {forgexa_cli-1.76.3 → forgexa_cli-1.77.0}/forgexa_cli/agent_core.py +77 -0
- {forgexa_cli-1.76.3 → forgexa_cli-1.77.0}/forgexa_cli/daemon.py +34 -7
- {forgexa_cli-1.76.3 → forgexa_cli-1.77.0}/forgexa_cli/runtime_evidence.py +80 -5
- {forgexa_cli-1.76.3 → forgexa_cli-1.77.0}/forgexa_cli.egg-info/PKG-INFO +1 -1
- {forgexa_cli-1.76.3 → forgexa_cli-1.77.0}/pyproject.toml +1 -1
- {forgexa_cli-1.76.3 → forgexa_cli-1.77.0}/README.md +0 -0
- {forgexa_cli-1.76.3 → forgexa_cli-1.77.0}/forgexa_cli/_build_config.py +0 -0
- {forgexa_cli-1.76.3 → forgexa_cli-1.77.0}/forgexa_cli/_local_bind.py +0 -0
- {forgexa_cli-1.76.3 → forgexa_cli-1.77.0}/forgexa_cli/autoupgrade.py +0 -0
- {forgexa_cli-1.76.3 → forgexa_cli-1.77.0}/forgexa_cli/aws_evidence.py +0 -0
- {forgexa_cli-1.76.3 → forgexa_cli-1.77.0}/forgexa_cli/aws_logs_evidence.py +0 -0
- {forgexa_cli-1.76.3 → forgexa_cli-1.77.0}/forgexa_cli/main.py +0 -0
- {forgexa_cli-1.76.3 → forgexa_cli-1.77.0}/forgexa_cli/py.typed +0 -0
- {forgexa_cli-1.76.3 → forgexa_cli-1.77.0}/forgexa_cli/zcode_protocol.py +0 -0
- {forgexa_cli-1.76.3 → forgexa_cli-1.77.0}/forgexa_cli.egg-info/SOURCES.txt +0 -0
- {forgexa_cli-1.76.3 → forgexa_cli-1.77.0}/forgexa_cli.egg-info/dependency_links.txt +0 -0
- {forgexa_cli-1.76.3 → forgexa_cli-1.77.0}/forgexa_cli.egg-info/entry_points.txt +0 -0
- {forgexa_cli-1.76.3 → forgexa_cli-1.77.0}/forgexa_cli.egg-info/requires.txt +0 -0
- {forgexa_cli-1.76.3 → forgexa_cli-1.77.0}/forgexa_cli.egg-info/top_level.txt +0 -0
- {forgexa_cli-1.76.3 → forgexa_cli-1.77.0}/setup.cfg +0 -0
- {forgexa_cli-1.76.3 → forgexa_cli-1.77.0}/tests/test_auth_and_runtime_commands.py +0 -0
- {forgexa_cli-1.76.3 → forgexa_cli-1.77.0}/tests/test_autoupgrade.py +0 -0
- {forgexa_cli-1.76.3 → forgexa_cli-1.77.0}/tests/test_check_command.py +0 -0
- {forgexa_cli-1.76.3 → forgexa_cli-1.77.0}/tests/test_expiry_warnings_and_revoke.py +0 -0
- {forgexa_cli-1.76.3 → forgexa_cli-1.77.0}/tests/test_local_bind_commands.py +0 -0
- {forgexa_cli-1.76.3 → forgexa_cli-1.77.0}/tests/test_runtime_credentials.py +0 -0
- {forgexa_cli-1.76.3 → forgexa_cli-1.77.0}/tests/test_session_credentials.py +0 -0
- {forgexa_cli-1.76.3 → forgexa_cli-1.77.0}/tests/test_silent_install.py +0 -0
- {forgexa_cli-1.76.3 → 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,61 @@ 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
|
+
|
|
106
161
|
def assess_implementation_audit(contract: dict, report: object) -> dict:
|
|
107
162
|
"""Validate implementation coverage independently of later runtime acceptance."""
|
|
108
163
|
issues: list[str] = []
|
|
@@ -111,6 +166,7 @@ def assess_implementation_audit(contract: dict, report: object) -> dict:
|
|
|
111
166
|
"audit_id": contract.get("audit_id"), "report_valid": False,
|
|
112
167
|
"node_complete": False, "status": "incomplete", "issues": issues,
|
|
113
168
|
"findings": findings, "followup_task_ids": [], "report": report,
|
|
169
|
+
"revision_requests": [],
|
|
114
170
|
}
|
|
115
171
|
if (
|
|
116
172
|
not isinstance(contract.get("audit_id"), str) or not contract["audit_id"]
|
|
@@ -156,6 +212,12 @@ def assess_implementation_audit(contract: dict, report: object) -> dict:
|
|
|
156
212
|
else:
|
|
157
213
|
index = row.get("index")
|
|
158
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
|
|
159
221
|
if (
|
|
160
222
|
type(index) is not int or not 1 <= index <= criterion_count or index in checked_criteria
|
|
161
223
|
or not isinstance(related, list) or not related
|
|
@@ -177,9 +239,22 @@ def assess_implementation_audit(contract: dict, report: object) -> dict:
|
|
|
177
239
|
):
|
|
178
240
|
issues.append("Implementation gap paths must be repository-relative")
|
|
179
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)
|
|
180
254
|
findings.extend({
|
|
181
255
|
"task_id": task_id, "status": status, "description": description,
|
|
182
256
|
"files": paths, "evidence": evidence,
|
|
257
|
+
**({"plan_revision": revision} if revision is not None else {}),
|
|
183
258
|
} for task_id in related)
|
|
184
259
|
required = expected if contract.get("final_verification") else {subject}
|
|
185
260
|
if required - checked.keys():
|
|
@@ -193,6 +268,8 @@ def assess_implementation_audit(contract: dict, report: object) -> dict:
|
|
|
193
268
|
)
|
|
194
269
|
result["followup_task_ids"] = sorted({finding["task_id"] for finding in findings})
|
|
195
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"
|
|
196
273
|
return result
|
|
197
274
|
|
|
198
275
|
|
|
@@ -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
|
},
|
|
@@ -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
|
)
|
|
@@ -13866,8 +13881,7 @@ class RuntimeDaemon:
|
|
|
13866
13881
|
audit_contract = input_data.get("implementation_audit_contract")
|
|
13867
13882
|
if isinstance(audit_contract, dict):
|
|
13868
13883
|
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
|
|
13884
|
+
audit_report = agent_core.extract_implementation_audit(_daemon_extract_agent_output(agent_id, result.stdout))
|
|
13871
13885
|
result.observations = [
|
|
13872
13886
|
observation for observation in result.observations
|
|
13873
13887
|
if observation.get("type") != "implementation_audit"
|
|
@@ -13875,7 +13889,12 @@ class RuntimeDaemon:
|
|
|
13875
13889
|
if isinstance(audit_report, dict):
|
|
13876
13890
|
result.observations.append({"type": "implementation_audit", "data": audit_report})
|
|
13877
13891
|
audit_result = agent_core.assess_implementation_audit(audit_contract, audit_report)
|
|
13878
|
-
if
|
|
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"]:
|
|
13879
13898
|
issues.extend(audit_result["issues"] or ["The selected approved task still has implementation gaps"])
|
|
13880
13899
|
|
|
13881
13900
|
if node_type == "coding" or (
|
|
@@ -14415,7 +14434,11 @@ class RuntimeDaemon:
|
|
|
14415
14434
|
)
|
|
14416
14435
|
except (_json.JSONDecodeError, UnicodeDecodeError) as e:
|
|
14417
14436
|
issues.append(f"coverage-matrix.json is not valid JSON: {e}")
|
|
14418
|
-
elif
|
|
14437
|
+
elif (
|
|
14438
|
+
_requires_structured_artifacts
|
|
14439
|
+
or _requires_e2e_artifacts
|
|
14440
|
+
or _input.get("deferred_acceptance_verification") is not None
|
|
14441
|
+
):
|
|
14419
14442
|
issues.append(f"coverage-matrix.json not found in {doc_dir or 'workspace root'}")
|
|
14420
14443
|
|
|
14421
14444
|
# --- test-evidence.json validation (M1 evidence bundle) ---
|
|
@@ -15840,6 +15863,8 @@ class RuntimeDaemon:
|
|
|
15840
15863
|
preserved_paths=preserved_paths,
|
|
15841
15864
|
)
|
|
15842
15865
|
issues = self._validate_outputs(workspace_path, task, result)
|
|
15866
|
+
if result.failure_code == "implementation_plan_revision_required":
|
|
15867
|
+
return result
|
|
15843
15868
|
if require_source_inventory:
|
|
15844
15869
|
issues.extend(inventory_issues)
|
|
15845
15870
|
if not issues:
|
|
@@ -15967,6 +15992,8 @@ class RuntimeDaemon:
|
|
|
15967
15992
|
preserved_paths=preserved_paths,
|
|
15968
15993
|
)
|
|
15969
15994
|
remaining = self._validate_outputs(workspace_path, task, result)
|
|
15995
|
+
if result.failure_code == "implementation_plan_revision_required":
|
|
15996
|
+
return result
|
|
15970
15997
|
if require_source_inventory:
|
|
15971
15998
|
remaining.extend(inventory_issues)
|
|
15972
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"
|
|
@@ -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
|