@topy-ai/maggie 0.7.47 → 0.7.49

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.
Files changed (91) hide show
  1. package/README-zh-TW.md +2 -2
  2. package/README.md +76 -15
  3. package/bin/maggie.js +23 -6
  4. package/bundled-contracts/google-integrations/capability-report-v2.schema.json +2 -1
  5. package/bundled-contracts/maggie-content/email-localization-v1.schema.json +89 -0
  6. package/bundled-contracts/maggie-content/lifecycle-email-v1.schema.json +76 -0
  7. package/bundled-contracts/maggie-deployment/readiness-v1.schema.json +6 -1
  8. package/bundled-contracts/maggie-design/palette-roles-v1.schema.json +47 -0
  9. package/bundled-contracts/maggie-marketplace/template-content-v1.schema.json +103 -0
  10. package/bundled-contracts/maggie-ops/marketing-run-v1.schema.json +89 -0
  11. package/bundled-contracts/maggie-ops/notification-catalogue-example-v1.json +941 -0
  12. package/bundled-contracts/maggie-ops/notification-catalogue-v1.schema.json +273 -0
  13. package/bundled-contracts/maggie-service-booking/fact-provenance-v1.schema.json +128 -0
  14. package/bundled-contracts/maggie-service-booking/import-stages-v1.schema.json +191 -0
  15. package/bundled-contracts/maggiedash/booking-browser-evidence-v1.json +21 -0
  16. package/bundled-contracts/maggiedash/booking-browser-evidence-v2.json +191 -0
  17. package/bundled-contracts/maggiedash/booking-email-templates-v1.json +3 -1
  18. package/bundled-contracts/maggiedash/booking-voucher-lifecycle-v1.json +85 -0
  19. package/bundled-contracts/maggiedash/execution-board.json +4629 -1553
  20. package/bundled-contracts/maggiedash/stripe-booking-capabilities-v1.json +1 -0
  21. package/bundled-marketplace/README.md +17 -0
  22. package/bundled-marketplace/catalog.json +56 -7
  23. package/bundled-references/browser-inspection.md +25 -0
  24. package/bundled-references/google-integrations-runbook.md +54 -0
  25. package/bundled-references/maggiedash-booking/CURRENT-STATE.md +128 -10
  26. package/bundled-references/maggiedash-booking/EXECUTION-BOARD.json +4629 -1553
  27. package/bundled-references/maggiedash-booking/HOST-ADAPTER.md +5 -0
  28. package/bundled-references/maggiedash-booking/ORAWELLNESS-INTEGRATION-AUDIT.md +64 -21
  29. package/bundled-references/maggiedash-booking/PRD.md +9 -1
  30. package/bundled-references/maggiedash-booking/PROGRESS.md +244 -2399
  31. package/bundled-references/maggiedash-booking/QA-TEST-PLAN.md +37 -6
  32. package/bundled-references/maggiedash-booking/README.md +23 -6
  33. package/bundled-references/maggiedash-booking/RUNTIME-OPERATIONS.md +64 -8
  34. package/bundled-references/maggiedash-booking/SKILLS-AND-CLI.md +11 -2
  35. package/bundled-references/maggiedash-booking/STRIPE-INTEGRATION.md +66 -2
  36. package/bundled-references/maggiedash-booking/TASK-RUNBOOK.md +1 -1
  37. package/bundled-references/maggiedash-booking/TASKS.md +44 -3
  38. package/bundled-references/maggiedash-booking/VOUCHERS.md +47 -0
  39. package/bundled-references/provider-adapter-contract.md +33 -0
  40. package/bundled-skills/maggie-booking/SKILL.md +8 -0
  41. package/bundled-skills/maggie-clone-to-template/SKILL.md +26 -0
  42. package/bundled-skills/maggie-content-localization/SKILL.md +17 -1
  43. package/bundled-skills/maggie-content-localization/references/email-and-lifecycle.md +83 -0
  44. package/bundled-skills/maggie-deployment/SKILL.md +141 -7
  45. package/bundled-skills/maggie-deployment/references/vps.md +74 -1
  46. package/bundled-skills/maggie-design/SKILL.md +154 -0
  47. package/bundled-skills/maggie-feedback/SKILL.md +67 -1
  48. package/bundled-skills/maggie-marketplace/SKILL.md +91 -0
  49. package/bundled-skills/maggie-ops/SKILL.md +37 -0
  50. package/bundled-skills/maggie-ops/references/notifications-operations.md +154 -0
  51. package/bundled-skills/maggie-qa-workflow/SKILL.md +41 -1
  52. package/bundled-skills/maggie-service-booking/SKILL.md +82 -0
  53. package/bundled-skills/maggie-social-share/SKILL.md +56 -7
  54. package/bundled-skills/maggie-social-share/references/marketing-run.md +57 -0
  55. package/bundled-skills/maggie-social-share/references/queue-and-connections.md +152 -0
  56. package/bundled-skills/maggie-template/SKILL.md +57 -3
  57. package/bundled-skills/maggie-template/references/template-content-v1.example.json +34 -0
  58. package/bundled-tools/clis/maggie_booking.py +199 -26
  59. package/bundled-tools/clis/maggie_booking_vouchers.py +185 -0
  60. package/bundled-tools/clis/maggie_clone_to_template.py +14 -0
  61. package/bundled-tools/clis/maggie_deployment.py +307 -18
  62. package/bundled-tools/clis/maggie_deployment_auth_canary.py +216 -0
  63. package/bundled-tools/clis/maggie_deployment_collect.py +225 -0
  64. package/bundled-tools/clis/maggie_deployment_readiness.py +53 -3
  65. package/bundled-tools/clis/maggie_design.py +17 -0
  66. package/bundled-tools/clis/maggie_design_lint.py +277 -0
  67. package/bundled-tools/clis/maggie_design_verify.py +359 -0
  68. package/bundled-tools/clis/maggie_docs.py +82 -4
  69. package/bundled-tools/clis/maggie_email_localization.py +382 -0
  70. package/bundled-tools/clis/maggie_feedback.py +115 -5
  71. package/bundled-tools/clis/maggie_icon_glyph_check.py +382 -0
  72. package/bundled-tools/clis/maggie_icon_release_gate.py +28 -1
  73. package/bundled-tools/clis/maggie_localization.py +5 -0
  74. package/bundled-tools/clis/maggie_marketing_run.py +272 -0
  75. package/bundled-tools/clis/maggie_marketplace.py +199 -5
  76. package/bundled-tools/clis/maggie_notifications.py +528 -0
  77. package/bundled-tools/clis/maggie_ops.py +87 -0
  78. package/bundled-tools/clis/maggie_palette.py +280 -0
  79. package/bundled-tools/clis/maggie_qa_workflow.py +156 -1
  80. package/bundled-tools/clis/maggie_schedule.py +12 -2
  81. package/bundled-tools/clis/maggie_service_import.py +520 -0
  82. package/bundled-tools/clis/maggie_social_queue.py +442 -0
  83. package/bundled-tools/clis/maggie_social_share.py +46 -9
  84. package/bundled-tools/clis/maggie_template_package.py +1141 -0
  85. package/bundled-tools/runtime/google_ads.py +306 -0
  86. package/bundled-tools/runtime/google_capabilities.py +18 -2
  87. package/bundled-tools/runtime/google_remediation.py +152 -0
  88. package/package.json +1 -1
  89. package/references/browser-inspection.md +25 -0
  90. package/references/google-integrations-runbook.md +54 -0
  91. package/references/provider-adapter-contract.md +33 -0
@@ -0,0 +1,216 @@
1
+ #!/usr/bin/env python3
2
+ """Run declared, role-scoped authenticated API canaries without storing credentials or bodies.
3
+
4
+ Public route canaries cannot see authenticated failures such as database
5
+ permission errors, schema mismatches, or 403s on account and business APIs.
6
+ This command executes a reviewed probe plan with credentials read only from
7
+ environment variable *names*, and records status codes and contract checks
8
+ only: never cookies, tokens, headers, response bodies, or personal data.
9
+ """
10
+
11
+ from __future__ import annotations
12
+
13
+ import argparse
14
+ import json
15
+ import os
16
+ import re
17
+ import sys
18
+ import urllib.error
19
+ import urllib.request
20
+ from datetime import datetime, timezone
21
+ from pathlib import Path
22
+ from urllib.parse import urlparse
23
+
24
+ PLAN_SCHEMA = "maggie-auth-canary-plan.v1"
25
+ EVIDENCE_SCHEMA = "maggie-deployment-auth-canary.v1"
26
+ MUTATING = {"POST", "PUT", "PATCH", "DELETE"}
27
+ ALLOWED_METHODS = {"GET", "HEAD", "OPTIONS"} | MUTATING
28
+ LOCAL_HOSTS = {"localhost", "127.0.0.1", "::1"}
29
+ ENV_NAME = re.compile(r"^[A-Z][A-Z0-9_]{1,63}$")
30
+ SECRET_VALUE = re.compile(r"(?:Bearer\s+\S{8,}|eyJ[A-Za-z0-9_-]{10,}\.|sk[-_](?:live|test)[-_]|session=|sid=|[A-Fa-f0-9]{32,})")
31
+
32
+
33
+ class PlanError(ValueError):
34
+ pass
35
+
36
+
37
+ def classify(status: int | None) -> str:
38
+ if status is None:
39
+ return "network"
40
+ if status in (401, 403):
41
+ return "authorization"
42
+ if status >= 500:
43
+ return "server"
44
+ if status == 404:
45
+ return "route"
46
+ return "contract"
47
+
48
+
49
+ def contains_literal_secret(value: object) -> bool:
50
+ if isinstance(value, dict):
51
+ return any(contains_literal_secret(item) for item in value.values())
52
+ if isinstance(value, list):
53
+ return any(contains_literal_secret(item) for item in value)
54
+ return isinstance(value, str) and bool(SECRET_VALUE.search(value))
55
+
56
+
57
+ def validate_plan(plan: object) -> dict:
58
+ """Return the normalized plan or raise PlanError for an unsafe/invalid plan."""
59
+ if not isinstance(plan, dict) or plan.get("schemaVersion") != PLAN_SCHEMA:
60
+ raise PlanError(f"plan schemaVersion must be {PLAN_SCHEMA}")
61
+ if contains_literal_secret(plan):
62
+ raise PlanError("plan contains a literal credential; reference environment variable names only")
63
+ base = str(plan.get("baseUrl") or "")
64
+ parsed = urlparse(base)
65
+ if parsed.scheme not in {"http", "https"} or not parsed.hostname or parsed.query or parsed.fragment:
66
+ raise PlanError("baseUrl must be an http(s) origin without query or fragment")
67
+ if parsed.scheme == "http" and parsed.hostname not in LOCAL_HOSTS:
68
+ raise PlanError("baseUrl must use https outside localhost")
69
+ roles = plan.get("roles")
70
+ if not isinstance(roles, dict) or not roles:
71
+ raise PlanError("roles must declare at least one role")
72
+ for name, role in roles.items():
73
+ if not isinstance(role, dict):
74
+ raise PlanError(f"role {name} must be an object")
75
+ keys = [key for key in ("tokenEnv", "cookieEnv") if role.get(key)]
76
+ if len(keys) != 1:
77
+ raise PlanError(f"role {name} must declare exactly one of tokenEnv or cookieEnv")
78
+ if not ENV_NAME.fullmatch(str(role[keys[0]])):
79
+ raise PlanError(f"role {name} {keys[0]} must be an environment variable name")
80
+ unknown = set(role) - {"tokenEnv", "cookieEnv", "scheme", "description"}
81
+ if unknown:
82
+ raise PlanError(f"role {name} has unsupported fields: {', '.join(sorted(unknown))}")
83
+ probes = plan.get("probes")
84
+ if not isinstance(probes, list) or not probes:
85
+ raise PlanError("probes must be a non-empty list")
86
+ seen = set()
87
+ for probe in probes:
88
+ if not isinstance(probe, dict):
89
+ raise PlanError("each probe must be an object")
90
+ probe_id = str(probe.get("id") or "")
91
+ if not re.fullmatch(r"[a-z0-9][a-z0-9._-]{0,63}", probe_id) or probe_id in seen:
92
+ raise PlanError(f"probe id is missing, invalid, or duplicated: {probe_id or '<empty>'}")
93
+ seen.add(probe_id)
94
+ if probe.get("role") not in roles:
95
+ raise PlanError(f"probe {probe_id} references an undeclared role")
96
+ method = str(probe.get("method", "GET")).upper()
97
+ if method not in ALLOWED_METHODS:
98
+ raise PlanError(f"probe {probe_id} method is unsupported")
99
+ path = str(probe.get("path") or "")
100
+ if not path.startswith("/") or "//" in path or "?" in path or "#" in path:
101
+ raise PlanError(f"probe {probe_id} path must be an absolute path without query or fragment")
102
+ expect = probe.get("expectStatus", [200])
103
+ if not isinstance(expect, list) or not expect or not all(isinstance(code, int) and 100 <= code <= 599 for code in expect):
104
+ raise PlanError(f"probe {probe_id} expectStatus must be a list of HTTP status codes")
105
+ if method in MUTATING and probe.get("fixture") != "disposable":
106
+ raise PlanError(f"probe {probe_id} mutates state and must declare fixture: disposable")
107
+ keys = probe.get("expectJsonKeys", [])
108
+ if not isinstance(keys, list) or not all(isinstance(key, str) and key for key in keys):
109
+ raise PlanError(f"probe {probe_id} expectJsonKeys must be a list of key names")
110
+ return plan
111
+
112
+
113
+ def credential_headers(role: dict) -> dict[str, str] | None:
114
+ if role.get("tokenEnv"):
115
+ value = os.environ.get(role["tokenEnv"], "")
116
+ return {"Authorization": f"{role.get('scheme', 'Bearer')} {value}"} if value else None
117
+ value = os.environ.get(role["cookieEnv"], "")
118
+ return {"Cookie": value} if value else None
119
+
120
+
121
+ def run_probe(base: str, probe: dict, role: dict, timeout: float) -> dict:
122
+ method = str(probe.get("method", "GET")).upper()
123
+ record = {"id": probe["id"], "role": probe["role"], "method": method, "path": probe["path"], "expectStatus": probe.get("expectStatus", [200])}
124
+ if probe.get("fixture"):
125
+ record["fixture"] = probe["fixture"]
126
+ headers = credential_headers(role)
127
+ if headers is None:
128
+ record.update({"state": "inconclusive", "reason": "credential environment variable is not set"})
129
+ return record
130
+ headers["User-Agent"] = "maggie-deployment-auth-canary/1.0"
131
+ headers["Accept"] = "application/json"
132
+ data = None
133
+ if method in MUTATING:
134
+ headers["Content-Type"] = "application/json"
135
+ data = json.dumps(probe.get("body", {})).encode("utf-8")
136
+ request = urllib.request.Request(base.rstrip("/") + probe["path"], data=data, headers=headers, method=method)
137
+ status: int | None = None
138
+ body = b""
139
+ try:
140
+ with urllib.request.urlopen(request, timeout=timeout) as response:
141
+ status = response.status
142
+ body = response.read(1_000_000)
143
+ except urllib.error.HTTPError as error:
144
+ status = error.code
145
+ except (urllib.error.URLError, OSError) as error:
146
+ record.update({"state": "failed", "status": None, "failureClass": "network", "reason": type(error).__name__})
147
+ return record
148
+ record["status"] = status
149
+ errors = []
150
+ if status not in record["expectStatus"]:
151
+ errors.append(f"status {status} not in {record['expectStatus']}")
152
+ keys = probe.get("expectJsonKeys", [])
153
+ if keys and not errors:
154
+ try:
155
+ payload = json.loads(body.decode("utf-8") or "null")
156
+ except (UnicodeDecodeError, json.JSONDecodeError):
157
+ payload = None
158
+ if not isinstance(payload, dict):
159
+ errors.append("response is not a JSON object")
160
+ else:
161
+ missing = [key for key in keys if key not in payload]
162
+ record["contractKeys"] = {"expected": len(keys), "present": len(keys) - len(missing)}
163
+ if missing:
164
+ errors.append("missing contract keys: " + ", ".join(missing))
165
+ if errors:
166
+ record.update({"state": "failed", "failureClass": classify(status) if status not in record["expectStatus"] else "contract", "reason": "; ".join(errors)})
167
+ else:
168
+ record["state"] = "passed"
169
+ return record
170
+
171
+
172
+ def auth_canary(plan: dict, allow_mutations: bool, timeout: float = 15.0) -> dict:
173
+ plan = validate_plan(plan)
174
+ results = []
175
+ for probe in plan["probes"]:
176
+ method = str(probe.get("method", "GET")).upper()
177
+ if method in MUTATING and not allow_mutations:
178
+ results.append({"id": probe["id"], "role": probe["role"], "method": method, "path": probe["path"], "fixture": probe.get("fixture"), "state": "blocked", "reason": "mutating probe requires --allow-mutations"})
179
+ continue
180
+ results.append(run_probe(plan["baseUrl"], probe, plan["roles"][probe["role"]], timeout))
181
+ states = {item["state"] for item in results}
182
+ status = "failed" if "failed" in states else "inconclusive" if states & {"inconclusive", "blocked"} else "passed"
183
+ return {
184
+ "schemaVersion": EVIDENCE_SCHEMA,
185
+ "generatedAt": datetime.now(timezone.utc).isoformat(),
186
+ "origin": urlparse(plan["baseUrl"]).netloc,
187
+ "status": status,
188
+ "passed": status == "passed",
189
+ "roles": sorted(plan["roles"]),
190
+ "probes": results,
191
+ "privacy": "status codes and contract-key counts only; no credentials, headers, bodies, or personal data",
192
+ }
193
+
194
+
195
+ def main() -> int:
196
+ parser = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter)
197
+ parser.add_argument("--plan", type=Path, required=True, help=f"{PLAN_SCHEMA} probe plan")
198
+ parser.add_argument("--allow-mutations", action="store_true", help="run probes that mutate disposable fixtures")
199
+ parser.add_argument("--timeout", type=float, default=15.0)
200
+ parser.add_argument("--output", type=Path)
201
+ args = parser.parse_args()
202
+ try:
203
+ report = auth_canary(json.loads(args.plan.read_text(encoding="utf-8")), args.allow_mutations, args.timeout)
204
+ except (OSError, json.JSONDecodeError, PlanError) as error:
205
+ print(f"BLOCKED: maggie deployment auth-canary: {error}", file=sys.stderr)
206
+ return 2
207
+ encoded = json.dumps(report, indent=2, ensure_ascii=False) + "\n"
208
+ if args.output:
209
+ args.output.parent.mkdir(parents=True, exist_ok=True)
210
+ args.output.write_text(encoded, encoding="utf-8")
211
+ print(encoded, end="")
212
+ return 0 if report["passed"] else 1
213
+
214
+
215
+ if __name__ == "__main__":
216
+ raise SystemExit(main())
@@ -0,0 +1,225 @@
1
+ #!/usr/bin/env python3
2
+ """Collect standard host release evidence and emit one reproducible readiness report.
3
+
4
+ `maggie deployment readiness` validates evidence files but does not produce
5
+ them, so each host used to hand-roll browser, SSH, systemd, and retention
6
+ scripts. This collector runs the standard adapters declared in one reviewed
7
+ config (edge/rendered canary, host health, rollback consistency, remote
8
+ release identity, optional authenticated API canary), writes each adapter's
9
+ evidence, and summarizes everything with the existing
10
+ `maggie-deployment-readiness.v1` report. It never deploys, never uses a
11
+ shell, and never records credentials or response bodies.
12
+ """
13
+
14
+ from __future__ import annotations
15
+
16
+ import argparse
17
+ import hashlib
18
+ import importlib.util
19
+ import json
20
+ import re
21
+ import subprocess
22
+ import sys
23
+ import urllib.error
24
+ import urllib.request
25
+ from datetime import datetime, timezone
26
+ from pathlib import Path
27
+ from typing import Any
28
+ from urllib.parse import urlparse
29
+
30
+ CONFIG_SCHEMA = "maggie-evidence-collector.v1"
31
+ HERE = Path(__file__).resolve().parent
32
+ STANDARD_EVIDENCE = {
33
+ "unitRegression": ".maggie/verification/unit-regression.json",
34
+ "packageSmoke": ".maggie/verification/package-smoke.json",
35
+ "browserEvidence": ".maggie/verification/browser-evidence.json",
36
+ "deploymentPreflight": ".maggie/release-preflight.json",
37
+ }
38
+ SECRET_ARG = re.compile(r"(?:password|passwd|token|secret|apikey|api_key)=|Bearer\s|sk[-_](?:live|test)[-_]|eyJ[A-Za-z0-9_-]{10,}\.", re.IGNORECASE)
39
+ RELEASE_ID = re.compile(r"^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$")
40
+
41
+
42
+ def load_module(name: str):
43
+ spec = importlib.util.spec_from_file_location(name, HERE / f"{name}.py")
44
+ assert spec and spec.loader
45
+ module = importlib.util.module_from_spec(spec)
46
+ spec.loader.exec_module(module)
47
+ return module
48
+
49
+
50
+ def now() -> str:
51
+ return datetime.now(timezone.utc).isoformat()
52
+
53
+
54
+ def resolve(project: Path, value: str | Path) -> Path:
55
+ path = Path(value).expanduser()
56
+ return path if path.is_absolute() else project / path
57
+
58
+
59
+ def write(path: Path, payload: dict) -> None:
60
+ path.parent.mkdir(parents=True, exist_ok=True)
61
+ path.write_text(json.dumps(payload, indent=2, ensure_ascii=False) + "\n", encoding="utf-8")
62
+
63
+
64
+ def check_argv(argv: object, label: str) -> list[str]:
65
+ if not isinstance(argv, list) or not argv or not all(isinstance(item, str) and item for item in argv):
66
+ raise ValueError(f"{label} command must be a non-empty argv list (no shell string)")
67
+ if any(SECRET_ARG.search(item) for item in argv):
68
+ raise ValueError(f"{label} command contains a literal credential; use host configuration or SSH keys")
69
+ return argv
70
+
71
+
72
+ def http_status(url: str, timeout: float) -> int | None:
73
+ request = urllib.request.Request(url, headers={"User-Agent": "maggie-deployment-collect/1.0"}, method="GET")
74
+ try:
75
+ with urllib.request.urlopen(request, timeout=timeout) as response:
76
+ return response.status
77
+ except urllib.error.HTTPError as error:
78
+ return error.code
79
+ except (urllib.error.URLError, OSError):
80
+ return None
81
+
82
+
83
+ def collect_health(config: dict, timeout: float) -> dict:
84
+ urls = config.get("urls")
85
+ if not isinstance(urls, list) or not urls:
86
+ raise ValueError("health adapter needs a non-empty urls list")
87
+ expect = config.get("expectStatus", [200])
88
+ checks = []
89
+ for url in urls:
90
+ parsed = urlparse(str(url))
91
+ if parsed.scheme not in {"http", "https"} or parsed.query:
92
+ raise ValueError("health URLs must be http(s) without query strings")
93
+ status = http_status(str(url), timeout)
94
+ checks.append({"url": f"{parsed.scheme}://{parsed.netloc}{parsed.path or '/'}", "status": status, "passed": status in expect})
95
+ return {"schemaVersion": "maggie-deployment-health.v1", "generatedAt": now(), "checks": checks, "passed": all(item["passed"] for item in checks)}
96
+
97
+
98
+ def collect_release_identity(config: dict, release: str, timeout: float) -> tuple[dict, dict]:
99
+ run: dict[str, Any] = {}
100
+ observed = ""
101
+ if config.get("command"):
102
+ argv = check_argv(config["command"], "releaseIdentity")
103
+ completed = subprocess.run(argv, capture_output=True, text=True, timeout=timeout, check=False)
104
+ run = {"argv": argv, "exitCode": completed.returncode}
105
+ raw = completed.stdout.strip().splitlines()[-1].strip() if completed.returncode == 0 and completed.stdout.strip() else ""
106
+ # A `readlink -f <current>` path resolves to .../releases/<release-id>.
107
+ observed = raw.rstrip("/").rsplit("/", 1)[-1] if raw else ""
108
+ elif config.get("url"):
109
+ url = str(config["url"])
110
+ field = str(config.get("field") or "release")
111
+ request = urllib.request.Request(url, headers={"User-Agent": "maggie-deployment-collect/1.0", "Accept": "application/json"})
112
+ try:
113
+ with urllib.request.urlopen(request, timeout=timeout) as response:
114
+ payload = json.loads(response.read(200_000).decode("utf-8"))
115
+ observed = str(payload.get(field, "")) if isinstance(payload, dict) else ""
116
+ run = {"url": url, "field": field}
117
+ except (urllib.error.URLError, OSError, ValueError) as error:
118
+ run = {"url": url, "field": field, "error": type(error).__name__}
119
+ else:
120
+ raise ValueError("releaseIdentity adapter needs command (argv) or url")
121
+ if observed and not RELEASE_ID.fullmatch(observed):
122
+ observed = "<unrecognized>"
123
+ evidence = {"schemaVersion": "maggie-release-identity.v1", "generatedAt": now(), "expected": release, "observed": observed, "passed": bool(observed) and observed == release}
124
+ return evidence, run
125
+
126
+
127
+ def collect_rollback(project: Path, config: dict) -> dict:
128
+ deployment = load_module("maggie_deployment")
129
+ evidence_path = resolve(project, config.get("evidence", "docs/deployment-rollback-smoke.json"))
130
+ keep = None
131
+ if config.get("plan"):
132
+ plan = json.loads(resolve(project, config["plan"]).read_text(encoding="utf-8"))
133
+ keep = (plan.get("retention") or {}).get("keep")
134
+ try:
135
+ result = deployment.validate_rollback_evidence(json.loads(evidence_path.read_text(encoding="utf-8")), keep)
136
+ except (OSError, json.JSONDecodeError) as error:
137
+ result = {"passed": False, "errors": [f"rollback evidence unavailable: {type(error).__name__}"]}
138
+ return {"schemaVersion": "maggie-deployment-rollback-check.v1", "generatedAt": now(), "retentionKeep": keep, "rollbackAvailable": None if keep is None else keep >= 2, "passed": result["passed"], "errors": result["errors"]}
139
+
140
+
141
+ def collect_canary(project: Path, config: dict, destination: Path, timeout: float) -> dict:
142
+ argv = [sys.executable, str(HERE / "maggie_deployment_canary.py"), "--project", str(project), "--output", str(destination)]
143
+ for asset in config.get("assets", []):
144
+ argv += ["--asset", str(asset)]
145
+ if config.get("renderReport"):
146
+ argv += ["--render-report", str(resolve(project, config["renderReport"]))]
147
+ completed = subprocess.run(argv, capture_output=True, text=True, timeout=timeout, check=False)
148
+ return {"adapter": "maggie_deployment_canary.py", "exitCode": completed.returncode}
149
+
150
+
151
+ def collect(project: Path, config: dict, evidence_dir: Path, timeout: float = 60.0) -> dict:
152
+ if config.get("schemaVersion") != CONFIG_SCHEMA:
153
+ raise ValueError(f"collector config schemaVersion must be {CONFIG_SCHEMA}")
154
+ release = str(config.get("release") or "")
155
+ if not RELEASE_ID.fullmatch(release):
156
+ raise ValueError("collector config needs the expected release id")
157
+ adapters = config.get("adapters") or {}
158
+ unknown = set(adapters) - {"canary", "health", "rollback", "releaseIdentity", "authCanary"}
159
+ if unknown:
160
+ raise ValueError("unsupported adapters: " + ", ".join(sorted(unknown)))
161
+ readiness = load_module("maggie_deployment_readiness")
162
+ paths = {name: resolve(project, (config.get("evidence") or {}).get(name, default)) for name, default in STANDARD_EVIDENCE.items()}
163
+ runs: dict[str, Any] = {}
164
+ canary_path = evidence_dir / "rendered-canary.json"
165
+ if "canary" in adapters:
166
+ runs["canary"] = collect_canary(project, adapters["canary"], canary_path, timeout)
167
+ paths["renderedCanary"] = canary_path
168
+ else:
169
+ paths["renderedCanary"] = resolve(project, (config.get("evidence") or {}).get("renderedCanary", ".maggie/deployment-canary.json"))
170
+ if "health" in adapters:
171
+ write(evidence_dir / "host-health.json", collect_health(adapters["health"], timeout))
172
+ paths["hostHealth"] = evidence_dir / "host-health.json"
173
+ runs["health"] = {"urls": len(adapters["health"].get("urls", []))}
174
+ if "rollback" in adapters:
175
+ write(evidence_dir / "rollback-check.json", collect_rollback(project, adapters["rollback"]))
176
+ paths["rollbackEvidence"] = evidence_dir / "rollback-check.json"
177
+ runs["rollback"] = {"validator": "validate_rollback_evidence"}
178
+ if "releaseIdentity" in adapters:
179
+ evidence, run = collect_release_identity(adapters["releaseIdentity"], release, timeout)
180
+ write(evidence_dir / "release-identity.json", evidence)
181
+ paths["releaseIdentity"] = evidence_dir / "release-identity.json"
182
+ runs["releaseIdentity"] = run
183
+ if "authCanary" in adapters:
184
+ auth = load_module("maggie_deployment_auth_canary")
185
+ spec = adapters["authCanary"]
186
+ plan = json.loads(resolve(project, spec["plan"]).read_text(encoding="utf-8"))
187
+ write(evidence_dir / "auth-canary.json", auth.auth_canary(plan, bool(spec.get("allowMutations")), timeout))
188
+ paths["authenticatedCanary"] = evidence_dir / "auth-canary.json"
189
+ runs["authCanary"] = {"plan": str(spec["plan"]), "allowMutations": bool(spec.get("allowMutations"))}
190
+ report = readiness.readiness_report(project, paths)
191
+ canonical = json.dumps(config, sort_keys=True, ensure_ascii=False).encode("utf-8")
192
+ report["collector"] = {
193
+ "schemaVersion": CONFIG_SCHEMA,
194
+ "configSha256": hashlib.sha256(canonical).hexdigest(),
195
+ "release": release,
196
+ "evidenceDir": readiness.display_path(evidence_dir, project),
197
+ "adapters": runs,
198
+ "mutation": "not executed",
199
+ }
200
+ return report
201
+
202
+
203
+ def main() -> int:
204
+ parser = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter)
205
+ parser.add_argument("--project", type=Path, default=Path.cwd())
206
+ parser.add_argument("--config", type=Path, default=Path(".maggie/deployment/collector.json"), help=f"{CONFIG_SCHEMA} config")
207
+ parser.add_argument("--evidence-dir", type=Path, default=Path(".maggie/deployment/collected"))
208
+ parser.add_argument("--timeout", type=float, default=60.0)
209
+ parser.add_argument("--output", type=Path, help="readiness report path (default: <evidence-dir>/readiness.json)")
210
+ args = parser.parse_args()
211
+ project = args.project.resolve()
212
+ evidence_dir = resolve(project, args.evidence_dir)
213
+ try:
214
+ config = json.loads(resolve(project, args.config).read_text(encoding="utf-8"))
215
+ report = collect(project, config, evidence_dir, args.timeout)
216
+ except (OSError, ValueError, KeyError, subprocess.SubprocessError) as error:
217
+ print(f"BLOCKED: maggie deployment collect: {error}", file=sys.stderr)
218
+ return 2
219
+ write(resolve(project, args.output) if args.output else evidence_dir / "readiness.json", report)
220
+ print(json.dumps(report, indent=2, ensure_ascii=False))
221
+ return 0 if report["status"] == "passed" else 1
222
+
223
+
224
+ if __name__ == "__main__":
225
+ raise SystemExit(main())
@@ -92,6 +92,39 @@ def validate_runtime_parity(payload: dict[str, Any]) -> tuple[bool, str]:
92
92
  return True, "worker/browser runtime parity passed"
93
93
 
94
94
 
95
+ def validate_host_health(payload: dict[str, Any]) -> tuple[bool, str]:
96
+ if payload.get("schemaVersion") != "maggie-deployment-health.v1":
97
+ return False, "host health evidence schemaVersion is unsupported"
98
+ checks = payload.get("checks")
99
+ if payload.get("passed") is not True or not isinstance(checks, list) or not checks:
100
+ return False, "host health evidence must contain passing checks"
101
+ return True, "host health checks passed"
102
+
103
+
104
+ def validate_release_identity(payload: dict[str, Any]) -> tuple[bool, str]:
105
+ if payload.get("schemaVersion") != "maggie-release-identity.v1":
106
+ return False, "release identity evidence schemaVersion is unsupported"
107
+ if payload.get("passed") is not True or not payload.get("expected") or payload.get("observed") != payload.get("expected"):
108
+ return False, "remote release identity does not match the expected release"
109
+ return True, "remote release identity matches"
110
+
111
+
112
+ def validate_rollback_check(payload: dict[str, Any]) -> tuple[bool, str]:
113
+ if payload.get("schemaVersion") != "maggie-deployment-rollback-check.v1":
114
+ return False, "rollback check schemaVersion is unsupported"
115
+ if payload.get("passed") is not True:
116
+ return False, "rollback evidence is inconsistent with the retention policy or not passed"
117
+ return True, "rollback evidence is consistent with the retention policy"
118
+
119
+
120
+ def validate_auth_canary(payload: dict[str, Any]) -> tuple[bool, str]:
121
+ if payload.get("schemaVersion") != "maggie-deployment-auth-canary.v1":
122
+ return False, "authenticated canary schemaVersion is unsupported"
123
+ if payload.get("status") != "passed" or payload.get("passed") is not True:
124
+ return False, "authenticated API canary did not pass"
125
+ return True, "authenticated API canary passed"
126
+
127
+
95
128
  VALIDATORS: dict[str, Callable[[dict[str, Any]], tuple[bool, str]]] = {
96
129
  "unitRegression": validate_unit,
97
130
  "packageSmoke": validate_package,
@@ -99,7 +132,12 @@ VALIDATORS: dict[str, Callable[[dict[str, Any]], tuple[bool, str]]] = {
99
132
  "renderedCanary": validate_canary,
100
133
  "deploymentPreflight": validate_preflight,
101
134
  "runtimeParity": validate_runtime_parity,
135
+ "hostHealth": validate_host_health,
136
+ "releaseIdentity": validate_release_identity,
137
+ "rollbackEvidence": validate_rollback_check,
138
+ "authenticatedCanary": validate_auth_canary,
102
139
  }
140
+ OPTIONAL_CHECKS = ("runtimeParity", "hostHealth", "releaseIdentity", "rollbackEvidence", "authenticatedCanary")
103
141
 
104
142
 
105
143
  def inspect_check(name: str, path: Path, project: Path) -> dict[str, Any]:
@@ -120,7 +158,7 @@ def inspect_check(name: str, path: Path, project: Path) -> dict[str, Any]:
120
158
  return result
121
159
 
122
160
 
123
- def readiness(project: Path, paths: dict[str, Path], output: Path | None = None) -> int:
161
+ def readiness_report(project: Path, paths: dict[str, Path]) -> dict[str, Any]:
124
162
  checks = {name: inspect_check(name, path, project) for name, path in paths.items()}
125
163
  states = {item["state"] for item in checks.values()}
126
164
  status = "failed" if "failed" in states else "inconclusive" if "inconclusive" in states else "passed"
@@ -132,6 +170,12 @@ def readiness(project: Path, paths: dict[str, Path], output: Path | None = None)
132
170
  "checks": checks,
133
171
  "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",
134
172
  }
173
+ return report
174
+
175
+
176
+ def readiness(project: Path, paths: dict[str, Path], output: Path | None = None) -> int:
177
+ report = readiness_report(project, paths)
178
+ status = report["status"]
135
179
  if output:
136
180
  destination = output if output.is_absolute() else project / output
137
181
  destination.parent.mkdir(parents=True, exist_ok=True)
@@ -150,6 +194,10 @@ def main() -> int:
150
194
  parser.add_argument("--rendered-canary", type=Path, default=Path(".maggie/deployment-canary.json"))
151
195
  parser.add_argument("--deployment-preflight", type=Path, default=Path(".maggie/release-preflight.json"))
152
196
  parser.add_argument("--runtime-parity", type=Path, help="validated worker/browser parity report from maggie deployment parity")
197
+ parser.add_argument("--host-health", type=Path, help="maggie-deployment-health.v1 evidence (e.g. from maggie deployment collect)")
198
+ parser.add_argument("--release-identity", type=Path, help="maggie-release-identity.v1 evidence")
199
+ parser.add_argument("--rollback-check", type=Path, help="maggie-deployment-rollback-check.v1 evidence")
200
+ parser.add_argument("--auth-canary", type=Path, help="maggie-deployment-auth-canary.v1 evidence")
153
201
  parser.add_argument("--output", type=Path)
154
202
  args = parser.parse_args()
155
203
  project = args.project.resolve()
@@ -159,8 +207,10 @@ def main() -> int:
159
207
  name: (getattr(args, option).resolve() if getattr(args, option).is_absolute() else project / getattr(args, option))
160
208
  for name, option in CHECKS
161
209
  }
162
- if args.runtime_parity:
163
- paths["runtimeParity"] = args.runtime_parity.resolve() if args.runtime_parity.is_absolute() else project / args.runtime_parity
210
+ for name, option in (("runtimeParity", "runtime_parity"), ("hostHealth", "host_health"), ("releaseIdentity", "release_identity"), ("rollbackEvidence", "rollback_check"), ("authenticatedCanary", "auth_canary")):
211
+ value = getattr(args, option)
212
+ if value:
213
+ paths[name] = value.resolve() if value.is_absolute() else project / value
164
214
  try:
165
215
  return readiness(project, paths, args.output)
166
216
  except (OSError, ValueError) as error:
@@ -854,9 +854,26 @@ commands:
854
854
  resume restart a failed design workflow
855
855
  author plan an original first-party page
856
856
  markup-check validate optional style attributes, content keys, and entities
857
+ pressable-check flag pressables/buttons nested in another pressable (JSX/TSX)
858
+ layer-check flag unlayered CSS that overrides Tailwind v4 utilities
859
+ palette generate or validate a semantic palette role contract
860
+ verify run automatable design-job steps and record evidence
861
+ icon-glyph-check render icons and prove font load and glyph paint
857
862
  reference-ui, init, validate-ui, app-init, app-validate, rebrand, review
858
863
  """)
859
864
  return 0
865
+ if len(sys.argv) > 1 and sys.argv[1] in {"pressable-check", "layer-check"}:
866
+ from maggie_design_lint import main as lint_main
867
+ return lint_main(sys.argv[1:])
868
+ if len(sys.argv) > 1 and sys.argv[1] == "verify":
869
+ from maggie_design_verify import main as verify_main
870
+ return verify_main(sys.argv[2:])
871
+ if len(sys.argv) > 1 and sys.argv[1] == "icon-glyph-check":
872
+ from maggie_icon_glyph_check import main as glyph_main
873
+ return glyph_main(sys.argv[2:])
874
+ if len(sys.argv) > 1 and sys.argv[1] == "palette":
875
+ from maggie_palette import main as palette_main
876
+ return palette_main(sys.argv[2:])
860
877
  if len(sys.argv) > 1 and sys.argv[1] == "reference-ui":
861
878
  command = argparse.ArgumentParser(description="Record sanitized blog/service UI evidence from a read-only reference project.")
862
879
  command.add_argument("--project", type=Path, default=Path.cwd())