forgexa-cli 1.19.0__tar.gz → 1.20.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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: forgexa-cli
3
- Version: 1.19.0
3
+ Version: 1.20.0
4
4
  Summary: Forgexa CLI — command-line client and AI agent runtime for the Forgexa platform
5
5
  Author-email: Jason Sun <dev.winds@gmail.com>
6
6
  License-Expression: MIT
@@ -1,2 +1,2 @@
1
1
  """forgexa-cli — Forgexa command-line client."""
2
- __version__ = "1.19.0"
2
+ __version__ = "1.20.0"
@@ -555,7 +555,7 @@ except (ImportError, ModuleNotFoundError):
555
555
  # DAEMON_VERSION is the protocol/logic version of the daemon code.
556
556
  # Kept in sync with pyproject.toml version via bump-version.sh.
557
557
  # CLIENT_TYPE identifies which packaging/distribution this daemon runs in.
558
- DAEMON_VERSION = "1.19.0"
558
+ DAEMON_VERSION = "1.20.0"
559
559
 
560
560
 
561
561
  def _detect_client_type() -> str:
@@ -5495,6 +5495,13 @@ class ProcessManager:
5495
5495
  pass
5496
5496
  if data.get("duration_ms") and "duration_seconds" not in metrics:
5497
5497
  metrics["duration_seconds"] = data["duration_ms"] / 1000.0
5498
+ # Turn usage (Claude CLI result event) — baseline telemetry for
5499
+ # node-type turn budgeting (see test-script quality audit M0).
5500
+ if data.get("num_turns") and "num_turns" not in metrics:
5501
+ try:
5502
+ metrics["num_turns"] = int(data["num_turns"])
5503
+ except (TypeError, ValueError):
5504
+ pass
5498
5505
 
5499
5506
  # ── 3. Prefer "result" type line — most authoritative ──
5500
5507
  for data in reversed(parsed):
@@ -6697,6 +6704,146 @@ class ServerConnection:
6697
6704
  # ── Main Daemon ──
6698
6705
 
6699
6706
 
6707
+ def _categorize_validation_issue(issue: str) -> str:
6708
+ """Map a free-text validation issue to a stable category for telemetry.
6709
+
6710
+ Baseline data for the test-script quality initiative (M0): lets us rank
6711
+ validation failure modes without parsing free text. Order matters —
6712
+ specific patterns are checked before generic ones.
6713
+ """
6714
+ text = issue.lower()
6715
+ if "script_content" in text:
6716
+ return "empty_script_content"
6717
+ if "script_path" in text and "does not exist" in text:
6718
+ return "missing_script_file"
6719
+ if "not valid json" in text:
6720
+ return "invalid_json"
6721
+ if "e2e" in text and "test case" in text:
6722
+ return "missing_e2e_case"
6723
+ if "coverage" in text:
6724
+ return "coverage_gap"
6725
+ if "scope" in text and "violation" in text:
6726
+ return "scope_violation"
6727
+ if "missing" in text or "not found" in text:
6728
+ return "missing_file"
6729
+ if "is empty" in text:
6730
+ return "empty_file"
6731
+ return "other"
6732
+
6733
+
6734
+ def _extract_testing_artifacts(workspace_path: Path, output_dir: str) -> dict | None:
6735
+ """Summarize test-cases.json artifact counts for telemetry (best effort).
6736
+
6737
+ Returns None when the file is missing/unparseable; never raises.
6738
+ """
6739
+ try:
6740
+ tc_path = workspace_path / output_dir / "test-cases.json"
6741
+ if not output_dir or not tc_path.exists():
6742
+ return None
6743
+ data = json.loads(tc_path.read_text(encoding="utf-8"))
6744
+ cases = data.get("test_cases") or []
6745
+ by_type: dict[str, int] = {}
6746
+ scripted = 0
6747
+ e2e_scripted = 0
6748
+ for case in cases:
6749
+ if not isinstance(case, dict):
6750
+ continue
6751
+ case_type = (case.get("test_type") or "unknown").lower()
6752
+ by_type[case_type] = by_type.get(case_type, 0) + 1
6753
+ if (case.get("script_content") or "").strip():
6754
+ scripted += 1
6755
+ if case_type == "e2e":
6756
+ e2e_scripted += 1
6757
+ return {
6758
+ "total_cases": len(cases),
6759
+ "by_type": by_type,
6760
+ "scripted_cases": scripted,
6761
+ "e2e_scripted": e2e_scripted,
6762
+ }
6763
+ except Exception:
6764
+ logger.debug("testing artifact telemetry extraction failed", exc_info=True)
6765
+ return None
6766
+
6767
+
6768
+ def _validate_test_evidence(
6769
+ base: Path, workspace_path: Path, e2e_case_ids: list[str], max_anchor_checks: int = 50
6770
+ ) -> list[str]:
6771
+ """Validate the M1 evidence bundle (test-evidence.json) for e2e test cases.
6772
+
6773
+ Deterministic, zero-environment checks: file exists and parses, locators/
6774
+ routes are present, every e2e case has a case_evidence entry, and each
6775
+ evidence anchor is a literal string that actually appears in its declared
6776
+ source_file (rejects invented selectors/routes). Items explicitly marked
6777
+ runtime_evidence_required are exempt from anchor checks by design.
6778
+ """
6779
+ issues: list[str] = []
6780
+ ev_path = base / "test-evidence.json"
6781
+ if not ev_path.exists():
6782
+ return [
6783
+ "test-evidence.json not found. It is MANDATORY when test-intent.json "
6784
+ "has e2e intents: record every route/selector used by E2E scripts with "
6785
+ "source_file + anchor (a literal string copied from that file), plus "
6786
+ "case_evidence mapping each e2e test case to its route/locator refs."
6787
+ ]
6788
+ try:
6789
+ ev = json.loads(ev_path.read_text(encoding="utf-8"))
6790
+ except (json.JSONDecodeError, UnicodeDecodeError) as e:
6791
+ return [f"test-evidence.json is not valid JSON: {e}"]
6792
+
6793
+ locators = ev.get("locators") or []
6794
+ routes = ev.get("routes") or []
6795
+ if not locators and not routes:
6796
+ issues.append(
6797
+ "test-evidence.json has no 'locators' or 'routes' entries — E2E scripts "
6798
+ "must ground their selectors/routes in source-file evidence."
6799
+ )
6800
+
6801
+ case_evidence = ev.get("case_evidence") or []
6802
+ covered_ids = {
6803
+ str(entry.get("test_case_id"))
6804
+ for entry in case_evidence
6805
+ if isinstance(entry, dict) and entry.get("test_case_id")
6806
+ }
6807
+ missing_cases = [cid for cid in e2e_case_ids if cid not in covered_ids]
6808
+ if missing_cases:
6809
+ issues.append(
6810
+ f"test-evidence.json missing case_evidence for e2e test cases: "
6811
+ f"{missing_cases[:10]}. Each e2e test case needs a case_evidence entry "
6812
+ f"with route_refs/locator_refs."
6813
+ )
6814
+
6815
+ checked = 0
6816
+ for item in list(locators) + list(routes):
6817
+ if checked >= max_anchor_checks:
6818
+ break
6819
+ if not isinstance(item, dict) or item.get("runtime_evidence_required"):
6820
+ continue
6821
+ checked += 1
6822
+ label = item.get("selector") or item.get("path") or item.get("id") or "?"
6823
+ src = (item.get("source_file") or "").strip()
6824
+ anchor = (item.get("anchor") or "").strip()
6825
+ if not src or not anchor:
6826
+ issues.append(
6827
+ f"evidence item '{label}' missing 'source_file' or 'anchor' — "
6828
+ f"anchor must be a literal string copied from source_file."
6829
+ )
6830
+ continue
6831
+ fpath = workspace_path / src
6832
+ if not fpath.exists():
6833
+ issues.append(f"evidence source_file not found: '{src}' (for '{label}')")
6834
+ continue
6835
+ try:
6836
+ content = fpath.read_text(encoding="utf-8", errors="replace")
6837
+ except OSError:
6838
+ continue
6839
+ if anchor not in content:
6840
+ issues.append(
6841
+ f"evidence anchor not found in '{src}': '{anchor[:80]}' (for '{label}'). "
6842
+ f"Copy the exact literal from the source file — do not paraphrase."
6843
+ )
6844
+ return issues
6845
+
6846
+
6700
6847
  class RuntimeDaemon:
6701
6848
  """Main Daemon orchestrator — runs on the host machine.
6702
6849
 
@@ -8457,6 +8604,7 @@ class RuntimeDaemon:
8457
8604
  # Not gated by _requires_structured_artifacts so that bugfix with e2e
8458
8605
  # intents gets the same E2E chain validation as feature/improvement.
8459
8606
  _e2e_intent_ids: set[str] = set()
8607
+ _e2e_case_ids: list[str] = []
8460
8608
  _analysis_dir = (
8461
8609
  _input.get("analysis_output_dir")
8462
8610
  or (_input.get("context") or {}).get("analysis_output_dir")
@@ -8493,6 +8641,13 @@ class RuntimeDaemon:
8493
8641
  try:
8494
8642
  tc_data = _json.loads(tc_path.read_text(encoding="utf-8"))
8495
8643
  cases = tc_data.get("test_cases", [])
8644
+ _e2e_case_ids = [
8645
+ str(_tc.get("id"))
8646
+ for _tc in cases
8647
+ if isinstance(_tc, dict)
8648
+ and (_tc.get("test_type") or "").lower() == "e2e"
8649
+ and _tc.get("id")
8650
+ ]
8496
8651
  if not cases:
8497
8652
  issues.append("test-cases.json exists but contains no test cases")
8498
8653
  else:
@@ -8580,6 +8735,15 @@ class RuntimeDaemon:
8580
8735
  elif _requires_structured_artifacts or _requires_e2e_artifacts:
8581
8736
  issues.append(f"coverage-matrix.json not found in {doc_dir or 'workspace root'}")
8582
8737
 
8738
+ # --- test-evidence.json validation (M1 evidence bundle) ---
8739
+ # Required whenever e2e intents exist: every route/selector
8740
+ # used by E2E scripts must be grounded in source-file evidence
8741
+ # (anchor literals are verified against the declared files).
8742
+ if _requires_e2e_artifacts:
8743
+ issues.extend(
8744
+ _validate_test_evidence(base, workspace_path, _e2e_case_ids)
8745
+ )
8746
+
8583
8747
  # --- test-report.md validation ---
8584
8748
  report_path = base / "test-report.md"
8585
8749
  if not report_path.exists():
@@ -8897,6 +9061,9 @@ class RuntimeDaemon:
8897
9061
  )
8898
9062
  issues = self._validate_outputs(workspace_path, task, result)
8899
9063
  if not issues:
9064
+ self._record_testing_telemetry(
9065
+ workspace_path, task, result, retries_used=attempt
9066
+ )
8900
9067
  return result # All validations passed
8901
9068
 
8902
9069
  issues_text = "\n".join(f"- {iss}" for iss in issues)
@@ -9031,8 +9198,38 @@ class RuntimeDaemon:
9031
9198
  len(remaining), max_retries, task.task_id,
9032
9199
  )
9033
9200
  result.metrics["validation_issues"] = remaining
9201
+ self._record_testing_telemetry(workspace_path, task, result, retries_used=max_retries)
9034
9202
  return result
9035
9203
 
9204
+ def _record_testing_telemetry(
9205
+ self, workspace_path: Path, task: TaskInfo, result: TaskResult, retries_used: int
9206
+ ) -> None:
9207
+ """Attach validation/testing telemetry to result.metrics (best effort).
9208
+
9209
+ M0 baseline for the test-script quality initiative: validation retry
9210
+ usage, stable failure categories, and testing artifact counts. These
9211
+ travel with the completion payload (metrics are kept in full by the
9212
+ backend) so baselines can be built without parsing free text.
9213
+ Never raises.
9214
+ """
9215
+ try:
9216
+ result.metrics["validation_retries_used"] = retries_used
9217
+ issues = result.metrics.get("validation_issues") or []
9218
+ if issues:
9219
+ result.metrics["validation_issue_categories"] = sorted(
9220
+ {_categorize_validation_issue(str(issue)) for issue in issues}
9221
+ )
9222
+ if task.node_type != "testing":
9223
+ return
9224
+ input_data = task.input_data or {}
9225
+ context = input_data.get("context") or {}
9226
+ output_dir = input_data.get("output_dir") or context.get("output_dir") or ""
9227
+ artifacts = _extract_testing_artifacts(workspace_path, output_dir)
9228
+ if artifacts:
9229
+ result.metrics["testing_artifacts"] = artifacts
9230
+ except Exception:
9231
+ logger.debug("testing telemetry recording failed", exc_info=True)
9232
+
9036
9233
  async def _collect_analysis_artifacts(
9037
9234
  self, workspace_path: Path, task: TaskInfo, result: TaskResult
9038
9235
  ) -> None:
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: forgexa-cli
3
- Version: 1.19.0
3
+ Version: 1.20.0
4
4
  Summary: Forgexa CLI — command-line client and AI agent runtime for the Forgexa platform
5
5
  Author-email: Jason Sun <dev.winds@gmail.com>
6
6
  License-Expression: MIT
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "forgexa-cli"
3
- version = "1.19.0"
3
+ version = "1.20.0"
4
4
  description = "Forgexa CLI — command-line client and AI agent runtime for the Forgexa platform"
5
5
  requires-python = ">=3.9"
6
6
  license = "MIT"
File without changes
File without changes