@topy-ai/maggie 0.7.20 → 0.7.22

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.
@@ -0,0 +1,158 @@
1
+ #!/usr/bin/env python3
2
+ """Summarize release evidence without confusing tests with runtime readiness."""
3
+
4
+ from __future__ import annotations
5
+
6
+ import argparse
7
+ import json
8
+ import sys
9
+ from datetime import datetime, timezone
10
+ from pathlib import Path
11
+ from typing import Any, Callable
12
+
13
+
14
+ CHECKS = (
15
+ ("unitRegression", "unit_report"),
16
+ ("packageSmoke", "package_report"),
17
+ ("browserEvidence", "browser_report"),
18
+ ("renderedCanary", "rendered_canary"),
19
+ ("deploymentPreflight", "deployment_preflight"),
20
+ )
21
+
22
+
23
+ def display_path(path: Path, project: Path) -> str:
24
+ try:
25
+ return str(path.resolve().relative_to(project.resolve()))
26
+ except ValueError:
27
+ return path.name
28
+
29
+
30
+ def validate_unit(payload: dict[str, Any]) -> tuple[bool, str]:
31
+ if payload.get("schemaVersion") != "maggie-regression.v1":
32
+ return False, "unit evidence schemaVersion is unsupported"
33
+ if payload.get("status") != "passed" or not isinstance(payload.get("suites"), int) or payload["suites"] < 1:
34
+ return False, "unit evidence must report a passing suite count"
35
+ if payload.get("skipped", 0) != 0:
36
+ return False, "unit evidence contains skipped suites"
37
+ return True, "dependency-free regression suites passed"
38
+
39
+
40
+ def validate_package(payload: dict[str, Any]) -> tuple[bool, str]:
41
+ if payload.get("schemaVersion") != "maggie-package-smoke.v1":
42
+ return False, "package evidence schemaVersion is unsupported"
43
+ checks = payload.get("checks")
44
+ if payload.get("passed") is not True or not isinstance(checks, list) or not checks:
45
+ return False, "package evidence must contain passing smoke checks"
46
+ return True, "package smoke checks passed"
47
+
48
+
49
+ def validate_browser(payload: dict[str, Any]) -> tuple[bool, str]:
50
+ if payload.get("schemaVersion") != "maggie-browser-evidence.v1":
51
+ return False, "browser evidence schemaVersion is unsupported"
52
+ routes = payload.get("routes")
53
+ if payload.get("passed") is not True or not isinstance(routes, list) or not routes:
54
+ return False, "browser evidence must contain a passing non-empty route set"
55
+ for route in routes:
56
+ if not isinstance(route, dict) or not route.get("route") or not route.get("screenshot"):
57
+ return False, "browser evidence routes need a route and screenshot"
58
+ if route.get("consoleErrors") or route.get("networkErrors") or route.get("missingAssets"):
59
+ return False, "browser evidence contains runtime errors"
60
+ return True, "host browser evidence passed"
61
+
62
+
63
+ def validate_canary(payload: dict[str, Any]) -> tuple[bool, str]:
64
+ if payload.get("schemaVersion") != "maggie-deployment-canary.v1":
65
+ return False, "rendered canary schemaVersion is unsupported"
66
+ if payload.get("status") != "pass":
67
+ return False, "rendered canary did not pass"
68
+ rendered = payload.get("rendered")
69
+ if not isinstance(rendered, dict) or not isinstance(rendered.get("report"), dict):
70
+ return False, "rendered canary is missing browser-rendered report evidence"
71
+ return True, "edge and rendered canary passed"
72
+
73
+
74
+ def validate_preflight(payload: dict[str, Any]) -> tuple[bool, str]:
75
+ if payload.get("schemaVersion") not in {"1.0", "maggie-release-preflight.v1"}:
76
+ return False, "deployment preflight schemaVersion is unsupported"
77
+ if payload.get("passed") is not True:
78
+ return False, "deployment preflight contains a failed gate"
79
+ gates = payload.get("gates")
80
+ if not isinstance(gates, list) or not gates:
81
+ return False, "deployment preflight has no gate results"
82
+ return True, "deployment preflight gates passed"
83
+
84
+
85
+ VALIDATORS: dict[str, Callable[[dict[str, Any]], tuple[bool, str]]] = {
86
+ "unitRegression": validate_unit,
87
+ "packageSmoke": validate_package,
88
+ "browserEvidence": validate_browser,
89
+ "renderedCanary": validate_canary,
90
+ "deploymentPreflight": validate_preflight,
91
+ }
92
+
93
+
94
+ def inspect_check(name: str, path: Path, project: Path) -> dict[str, Any]:
95
+ result: dict[str, Any] = {"state": "inconclusive", "configured": path.is_file(), "path": display_path(path, project)}
96
+ if not path.is_file():
97
+ result["reason"] = "required adapter evidence is unavailable"
98
+ return result
99
+ try:
100
+ payload = json.loads(path.read_text(encoding="utf-8"))
101
+ except (OSError, json.JSONDecodeError) as error:
102
+ result.update({"state": "failed", "reason": f"evidence is not valid JSON: {error}"})
103
+ return result
104
+ if not isinstance(payload, dict):
105
+ result.update({"state": "failed", "reason": "evidence root must be an object"})
106
+ return result
107
+ valid, reason = VALIDATORS[name](payload)
108
+ result.update({"state": "passed" if valid else "failed", "reason": reason})
109
+ return result
110
+
111
+
112
+ def readiness(project: Path, paths: dict[str, Path], output: Path | None = None) -> int:
113
+ checks = {name: inspect_check(name, path, project) for name, path in paths.items()}
114
+ states = {item["state"] for item in checks.values()}
115
+ status = "failed" if "failed" in states else "inconclusive" if "inconclusive" in states else "passed"
116
+ report = {
117
+ "schemaVersion": "maggie-deployment-readiness.v1",
118
+ "generatedAt": datetime.now(timezone.utc).isoformat(),
119
+ "project": display_path(project, project),
120
+ "status": status,
121
+ "checks": checks,
122
+ "nextAction": "resolve failed evidence before release" if status == "failed" else "collect the missing host adapter evidence" if status == "inconclusive" else "all declared readiness evidence passed",
123
+ }
124
+ if output:
125
+ destination = output if output.is_absolute() else project / output
126
+ destination.parent.mkdir(parents=True, exist_ok=True)
127
+ destination.write_text(json.dumps(report, indent=2, ensure_ascii=False) + "\n", encoding="utf-8")
128
+ report["output"] = display_path(destination, project)
129
+ print(json.dumps(report, indent=2, ensure_ascii=False))
130
+ return 0 if status == "passed" else 1
131
+
132
+
133
+ def main() -> int:
134
+ parser = argparse.ArgumentParser(description=__doc__)
135
+ parser.add_argument("--project", type=Path, default=Path.cwd())
136
+ parser.add_argument("--unit-report", type=Path, default=Path(".maggie/verification/unit-regression.json"))
137
+ parser.add_argument("--package-report", type=Path, default=Path(".maggie/verification/package-smoke.json"))
138
+ parser.add_argument("--browser-report", type=Path, default=Path(".maggie/verification/browser-evidence.json"))
139
+ parser.add_argument("--rendered-canary", type=Path, default=Path(".maggie/deployment-canary.json"))
140
+ parser.add_argument("--deployment-preflight", type=Path, default=Path(".maggie/release-preflight.json"))
141
+ parser.add_argument("--output", type=Path)
142
+ args = parser.parse_args()
143
+ project = args.project.resolve()
144
+ if not project.is_dir():
145
+ parser.error(f"project directory does not exist: {project}")
146
+ paths = {
147
+ name: (getattr(args, option).resolve() if getattr(args, option).is_absolute() else project / getattr(args, option))
148
+ for name, option in CHECKS
149
+ }
150
+ try:
151
+ return readiness(project, paths, args.output)
152
+ except (OSError, ValueError) as error:
153
+ print(f"BLOCKED: maggie deployment readiness: {error}", file=sys.stderr)
154
+ return 2
155
+
156
+
157
+ if __name__ == "__main__":
158
+ raise SystemExit(main())
@@ -12,7 +12,7 @@ from datetime import datetime, timezone
12
12
  from pathlib import Path
13
13
 
14
14
  sys.path.insert(0, str(Path(__file__).resolve().parents[1] / "runtime"))
15
- from content_localization import LANGUAGES, MARKETS, OPERATIONS, parse_locale, protected_field_changes, stale, valid_locale, validate_translation
15
+ from content_localization import LANGUAGES, MARKETS, OPERATIONS, parse_locale, protected_field_changes, stale, valid_locale, validate_operation_quality, validate_translation
16
16
  from localization_runner import generate, process_adapter
17
17
 
18
18
 
@@ -250,6 +250,8 @@ def validate_job(path: Path, source_path: Path | None = None, render_path: Path
250
250
  errors.append("generationContract mode must match operation")
251
251
  if job.get("operation") == "rewrite" and not generation.get("allowStructuralRewrite"):
252
252
  errors.append("rewrite requires structural rewrite permission")
253
+ if job.get("operation") == "polish" and job.get("sourceLanguage") != job.get("targetLanguage"):
254
+ errors.append("polish must keep sourceLanguage and targetLanguage identical")
253
255
  errors.extend(validate_translation({
254
256
  "contentId": job.get("contentId"), "lang": job.get("targetLanguage"),
255
257
  "locale": job.get("targetLocale"), "translationGroupId": job.get("translationGroupId", f"tg-{job.get('contentId')}"),
@@ -260,6 +262,8 @@ def validate_job(path: Path, source_path: Path | None = None, render_path: Path
260
262
  })
261
263
  }, expected_lang=job.get("targetLanguage")))
262
264
  errors.extend(f"protected field changed without approval: {field}" for field in protected_field_changes(job.get("content", {}), translation))
265
+ quality_errors, quality = validate_operation_quality(job.get("content", {}), translation, job.get("operation", ""), job.get("review"))
266
+ errors.extend(quality_errors)
263
267
  if source_path:
264
268
  errors.extend(validate_source(source_path, job.get("sourceRevision")))
265
269
  source = load(source_path)
@@ -269,7 +273,7 @@ def validate_job(path: Path, source_path: Path | None = None, render_path: Path
269
273
  errors.append("translation sourceStringIds must link every extracted source string")
270
274
  if render_path:
271
275
  errors.extend(validate_render_report(render_path))
272
- return {"passed": not errors, "errors": errors, "jobId": job.get("jobId"), "status": job.get("status")}
276
+ return {"passed": not errors, "errors": errors, "jobId": job.get("jobId"), "status": job.get("status"), "quality": quality}
273
277
 
274
278
 
275
279
  def preview(path: Path) -> int:
@@ -22,6 +22,9 @@ from urllib.parse import urlsplit, urlunsplit
22
22
  DEFAULT_SCENARIOS = Path(".maggie") / "scenario-manifest.json"
23
23
  VALID_STATUSES = {"pending", "pass", "fail", "blocked", "inconclusive"}
24
24
  VALID_PHASES = {"test", "fix", "retest"}
25
+ ASSERTION_SCHEMA = "maggie.qa-assertions.v1"
26
+ RUNTIME_ASSERTION_TYPES = {"http-status", "final-url", "text-present", "text-absent", "meta", "link-absent", "redirect"}
27
+ SOURCE_ASSERTION_TYPES = {"class-present", "class-absent", "markup-present", "markup-absent", "source-text"}
25
28
  SECRET_RE = re.compile(
26
29
  r"(?i)(bearer\s+|(?:api[_-]?key|token|secret|password|authorization|cookie)\s*[=:]\s*)[^\s,;]+"
27
30
  )
@@ -314,6 +317,86 @@ def export_run(args: argparse.Namespace) -> int:
314
317
  return 0
315
318
 
316
319
 
320
+ def assertion_audit(args: argparse.Namespace) -> int:
321
+ """Ensure QA assertions test the served requirement, not edited source markup."""
322
+ project = project_root(args.project)
323
+ manifest_path = project_root(args.assertions) if Path(args.assertions).is_absolute() else project / args.assertions
324
+ manifest = load_json(manifest_path)
325
+ errors = []
326
+ records = manifest.get("assertions")
327
+ if manifest.get("schemaVersion") != ASSERTION_SCHEMA:
328
+ errors.append(f"manifest schemaVersion must be {ASSERTION_SCHEMA}")
329
+ if not isinstance(records, list) or not records:
330
+ errors.append("assertions must be a non-empty array")
331
+ records = []
332
+ seen = set()
333
+ audited = []
334
+ for index, item in enumerate(records):
335
+ item_errors = []
336
+ if not isinstance(item, dict):
337
+ audited.append({"id": f"assertion-{index + 1}", "passed": False, "errors": ["assertion must be an object"]})
338
+ continue
339
+ identifier = safe_text(item.get("id"), 120) or f"assertion-{index + 1}"
340
+ if identifier in seen:
341
+ item_errors.append("assertion id is duplicated")
342
+ seen.add(identifier)
343
+ if not safe_text(item.get("requirement"), 1000):
344
+ item_errors.append("requirement is required")
345
+ route = safe_text(item.get("route"), 500)
346
+ if not route or not route.startswith("/"):
347
+ item_errors.append("route must be a path on the tested site")
348
+ if item.get("transport") != "http":
349
+ item_errors.append("transport must be http runtime evidence")
350
+ evidence_data = item.get("evidence")
351
+ if not isinstance(evidence_data, dict):
352
+ item_errors.append("evidence must be an object")
353
+ evidence_data = {}
354
+ if not safe_text(evidence_data.get("screenshot"), 500):
355
+ item_errors.append("evidence.screenshot is required")
356
+ if not isinstance(evidence_data.get("httpStatus"), int) or isinstance(evidence_data.get("httpStatus"), bool):
357
+ item_errors.append("evidence.httpStatus must be an integer")
358
+ checks = item.get("checks")
359
+ if not isinstance(checks, list) or not checks:
360
+ item_errors.append("checks must be a non-empty array")
361
+ checks = []
362
+ if not any(isinstance(check, dict) and check.get("type") == "http-status" for check in checks):
363
+ item_errors.append("at least one http-status check is required")
364
+ for check_index, check in enumerate(checks):
365
+ if not isinstance(check, dict):
366
+ item_errors.append(f"checks[{check_index}] must be an object")
367
+ continue
368
+ check_type = check.get("type")
369
+ surface = check.get("surface")
370
+ if check_type in SOURCE_ASSERTION_TYPES or surface == "source":
371
+ item_errors.append(f"checks[{check_index}] is source/markup-only; assert the served route instead")
372
+ if check_type not in RUNTIME_ASSERTION_TYPES:
373
+ item_errors.append(f"checks[{check_index}] has an unsupported runtime assertion type")
374
+ if surface not in {"http", "browser-runtime"}:
375
+ item_errors.append(f"checks[{check_index}] must identify http or browser-runtime evidence")
376
+ if not isinstance(check.get("passed"), bool):
377
+ item_errors.append(f"checks[{check_index}].passed must be boolean evidence")
378
+ if check_type == "http-status" and isinstance(evidence_data.get("httpStatus"), int) and check.get("actual") != evidence_data["httpStatus"]:
379
+ item_errors.append(f"checks[{check_index}] actual status does not match evidence.httpStatus")
380
+ if any(isinstance(check, dict) and check.get("passed") is False for check in checks):
381
+ item_errors.append("one or more runtime checks failed")
382
+ audited.append({"id": identifier, "passed": not item_errors, "errors": item_errors})
383
+ passed = not errors and all(item.get("passed") for item in audited) and bool(audited)
384
+ report = {
385
+ "schemaVersion": "maggie.qa-assertion-audit.v1",
386
+ "assertionSchema": ASSERTION_SCHEMA,
387
+ "checked": len(audited),
388
+ "assertions": audited,
389
+ "errors": errors,
390
+ "passed": passed,
391
+ }
392
+ output = Path(args.output).expanduser()
393
+ if not output.is_absolute():
394
+ output = project / output
395
+ write_json(output, report)
396
+ print(json.dumps({"status": "passed" if passed else "failed", "report": str(output.resolve()), "checked": len(audited), "errors": errors}, ensure_ascii=False, indent=2))
397
+ return 0 if passed else 1
398
+
399
+
317
400
  def parser() -> argparse.ArgumentParser:
318
401
  root = argparse.ArgumentParser(description=__doc__)
319
402
  root.add_argument("--project", default=".")
@@ -350,6 +433,10 @@ def parser() -> argparse.ArgumentParser:
350
433
  command.add_argument("--run", required=True)
351
434
  if name == "summary": command.add_argument("--format", choices=("json", "markdown"), default="json")
352
435
  else: command.add_argument("--output", required=True)
436
+ assertion_parser = commands.add_parser("assertion-audit")
437
+ assertion_parser.add_argument("--project", default=argparse.SUPPRESS)
438
+ assertion_parser.add_argument("--assertions", required=True, help="runtime assertion manifest JSON")
439
+ assertion_parser.add_argument("--output", default=".maggie/qa-assertion-audit.json")
353
440
  return root
354
441
 
355
442
 
@@ -359,6 +446,7 @@ def main() -> int:
359
446
  if args.command == "start": return start(args)
360
447
  if args.command == "record": return record(args)
361
448
  if args.command == "summary": return summary(args)
449
+ if args.command == "assertion-audit": return assertion_audit(args)
362
450
  return export_run(args)
363
451
  except (OSError, ValueError, json.JSONDecodeError) as error:
364
452
  print(f"QA workflow error: {safe_text(error)}", file=sys.stderr)