@topy-ai/maggie 0.7.38 → 0.7.41
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.
- package/README-zh-TW.md +82 -6
- package/README.md +99 -7
- package/bin/maggie.js +30 -3
- package/bundled-contracts/maggie-clone/interaction-state-v1.schema.json +26 -0
- package/bundled-contracts/maggie-content/provenance-v1.schema.json +20 -0
- package/bundled-contracts/maggie-design/brand-kit-v1.schema.json +18 -0
- package/bundled-contracts/maggie-design/browser-interactions-v1.schema.json +27 -0
- package/bundled-contracts/maggie-design/css-utility-evidence-v1.schema.json +10 -0
- package/bundled-contracts/maggie-design/dashboard-surface-v1.schema.json +11 -0
- package/bundled-contracts/maggie-design/progressive-enhancement-v1.schema.json +12 -0
- package/bundled-contracts/maggie-design/sample-surface-v1.schema.json +13 -0
- package/bundled-contracts/maggie-design/style-editing-v1.schema.json +35 -0
- package/bundled-contracts/maggie-media/image-generation-policy-v1.json +28 -0
- package/bundled-contracts/maggie-media/video-generation-policy-v1.json +40 -0
- package/bundled-contracts/maggie-media/video-job-v1.schema.json +20 -0
- package/bundled-contracts/maggie-media/video-playback-evidence-v1.schema.json +15 -0
- package/bundled-contracts/maggie-ops/npm11-preflight-v1.schema.json +17 -0
- package/bundled-contracts/maggie-scaffold/host-scaffold-v1.schema.json +25 -0
- package/bundled-contracts/maggie-seo/gsc-readiness-v1.schema.json +19 -0
- package/bundled-contracts/maggie-seo/privacy-origin-evidence-v1.schema.json +11 -0
- package/bundled-contracts/maggie-service-booking/delivery-provider-default-v1.json +8 -0
- package/bundled-contracts/maggie-service-booking/delivery-provider-v1.schema.json +16 -0
- package/bundled-contracts/maggiedash/booking-access-v1.json +5 -4
- package/bundled-contracts/maggiedash/booking-customer-surface-v1.json +12 -1
- package/bundled-contracts/maggiedash/booking-email-templates-v1.json +2 -0
- package/bundled-contracts/maggiedash/booking-host-adapter-v1.json +14 -2
- package/bundled-contracts/maggiedash/booking-runtime.v1.json +41 -0
- package/bundled-contracts/maggiedash/browser-session-v1.schema.json +18 -0
- package/bundled-contracts/maggiedash/content-overrides-v1.schema.json +19 -0
- package/bundled-contracts/maggiedash/execution-board.json +526 -26
- package/bundled-contracts/maggiedash/host-capabilities-v1.schema.json +10 -0
- package/bundled-contracts/maggiedash/public-session-cache-v1.schema.json +17 -0
- package/bundled-contracts/maggiedash/site-structure-v1.schema.json +13 -0
- package/bundled-references/browser-inspection.md +21 -0
- package/bundled-references/maggiedash-booking/ARCHITECTURE.md +218 -0
- package/bundled-references/maggiedash-booking/CURRENT-STATE.md +92 -0
- package/bundled-references/maggiedash-booking/DATA-FLOW.md +143 -0
- package/bundled-references/maggiedash-booking/DATA-MODEL.md +367 -0
- package/bundled-references/maggiedash-booking/DECISIONS.md +94 -0
- package/bundled-references/maggiedash-booking/EXECUTION-BOARD.json +2387 -0
- package/bundled-references/maggiedash-booking/HOST-ADAPTER.md +314 -0
- package/bundled-references/maggiedash-booking/ORAWELLNESS-INTEGRATION-AUDIT.md +227 -0
- package/bundled-references/maggiedash-booking/PAYMENT-GATEWAY.md +267 -0
- package/bundled-references/maggiedash-booking/PRD.md +228 -0
- package/bundled-references/maggiedash-booking/PROGRESS.md +2434 -0
- package/bundled-references/maggiedash-booking/QA-TEST-PLAN.md +235 -0
- package/bundled-references/maggiedash-booking/README.md +271 -0
- package/bundled-references/maggiedash-booking/RUNTIME-OPERATIONS.md +152 -0
- package/bundled-references/maggiedash-booking/SECURITY-COMPLIANCE.md +158 -0
- package/bundled-references/maggiedash-booking/SKILLS-AND-CLI.md +542 -0
- package/bundled-references/maggiedash-booking/STRIPE-INTEGRATION.md +129 -0
- package/bundled-references/maggiedash-booking/TASK-RUNBOOK.md +107 -0
- package/bundled-references/maggiedash-booking/TASKS.md +137 -0
- package/bundled-references/maggiedash-booking/USER-JOURNEYS.md +224 -0
- package/bundled-references/maggiedash-booking/diagrams/booking-dfd.excalidraw +1 -0
- package/bundled-references/maggiedash-booking/diagrams/booking-dfd.mmd +16 -0
- package/bundled-references/maggiedash-booking/diagrams/booking-dfd.png +0 -0
- package/bundled-references/maggiedash-booking/diagrams/booking-dfd.svg +1 -0
- package/bundled-references/maggiedash-booking/diagrams/booking-state-machine.mmd +19 -0
- package/bundled-references/maggiedash-booking/diagrams/booking-state-machine.png +0 -0
- package/bundled-references/maggiedash-booking/diagrams/booking-state-machine.svg +1 -0
- package/bundled-references/maggiedash-booking/diagrams/manager-journey.excalidraw +1 -0
- package/bundled-references/maggiedash-booking/diagrams/manager-journey.mmd +11 -0
- package/bundled-references/maggiedash-booking/diagrams/manager-journey.png +0 -0
- package/bundled-references/maggiedash-booking/diagrams/manager-journey.svg +1 -0
- package/bundled-references/maggiedash-booking/diagrams/payment-sequence.mmd +20 -0
- package/bundled-references/maggiedash-booking/diagrams/payment-sequence.png +0 -0
- package/bundled-references/maggiedash-booking/diagrams/payment-sequence.svg +1 -0
- package/bundled-references/maggiedash-booking/diagrams/system-context.excalidraw +1 -0
- package/bundled-references/maggiedash-booking/diagrams/system-context.mmd +10 -0
- package/bundled-references/maggiedash-booking/diagrams/system-context.png +0 -0
- package/bundled-references/maggiedash-booking/diagrams/system-context.svg +1 -0
- package/bundled-skills/maggie-blog/SKILL.md +12 -0
- package/bundled-skills/maggie-blog-bootstrap/SKILL.md +39 -0
- package/bundled-skills/maggie-booking/SKILL.md +112 -18
- package/bundled-skills/maggie-clone/SKILL.md +13 -0
- package/bundled-skills/maggie-deployment/SKILL.md +6 -0
- package/bundled-skills/maggie-design/SKILL.md +64 -3
- package/bundled-skills/maggie-ops/SKILL.md +12 -0
- package/bundled-skills/maggie-seo-geo/SKILL.md +45 -0
- package/bundled-skills/maggie-service-booking/SKILL.md +14 -0
- package/bundled-tools/clis/maggie_analytics.py +43 -1
- package/bundled-tools/clis/maggie_booking.py +161 -26
- package/bundled-tools/clis/maggie_browser_audit.py +99 -3
- package/bundled-tools/clis/maggie_clone.py +46 -1
- package/bundled-tools/clis/maggie_contracts.py +393 -0
- package/bundled-tools/clis/maggie_dash.py +22 -6
- package/bundled-tools/clis/maggie_design.py +84 -12
- package/bundled-tools/clis/maggie_service_booking.py +50 -1
- package/bundled-tools/clis/maggie_workflows.py +387 -0
- package/bundled-tools/clis/site_audit.py +77 -1
- package/bundled-tools/integrations/analytics.md +14 -0
- package/bundled-tools/runtime/site_baseline.py +3 -0
- package/package.json +1 -1
- package/references/browser-inspection.md +21 -0
|
@@ -222,7 +222,24 @@ def make_record(provider, source, pid, name, description, variants, booking, lin
|
|
|
222
222
|
if not booking:
|
|
223
223
|
for href,_ in links:
|
|
224
224
|
if any(x in href.lower() for x in ("book","appointment","checkout")): booking=urljoin(source,href); break
|
|
225
|
-
|
|
225
|
+
record = {"id":f"{provider}:{pid}","provider":provider,"providerServiceId":pid,"slug":slug(name),"title":name,"description":description.strip(),"sourceUrl":source,"category":{"level1":"Uncategorised","level2":None},"variants":variants,"bookingUrl":booking,"paymentUrl":None,"status":"active","supplyState":"live","displayState":"published","firstSeenAt":NOW(),"lastSeenAt":NOW()}
|
|
226
|
+
# A treatment is one catalogue entry. Duration/price choices belong to
|
|
227
|
+
# variants so the same treatment is not presented as four products.
|
|
228
|
+
durations = [int(item["durationMinutes"]) for item in variants if isinstance(item, dict) and isinstance(item.get("durationMinutes"), (int, float)) and item["durationMinutes"] > 0]
|
|
229
|
+
priced = [item.get("price", {}) for item in variants if isinstance(item, dict) and isinstance(item.get("price"), dict) and isinstance(item.get("price", {}).get("amountMinor"), int)]
|
|
230
|
+
currencies = {item.get("currency") for item in priced if item.get("currency")}
|
|
231
|
+
from_price = None
|
|
232
|
+
if priced and len(currencies) == 1:
|
|
233
|
+
lowest = min(priced, key=lambda item: item["amountMinor"])
|
|
234
|
+
from_price = {"amountMinor": lowest["amountMinor"], "currency": lowest["currency"]}
|
|
235
|
+
record["treatmentGroup"] = {
|
|
236
|
+
"id": record["id"],
|
|
237
|
+
"title": name,
|
|
238
|
+
"variantCount": len(variants),
|
|
239
|
+
"durationRange": {"minMinutes": min(durations), "maxMinutes": max(durations)} if durations else None,
|
|
240
|
+
"fromPrice": from_price,
|
|
241
|
+
}
|
|
242
|
+
return record
|
|
226
243
|
|
|
227
244
|
def root(args): return Path(args.project).resolve()
|
|
228
245
|
def path(project): return project/".maggie"/"booking"/"services.json"
|
|
@@ -1263,6 +1280,36 @@ def cmd_resolver_audit(args):
|
|
|
1263
1280
|
print(json.dumps({"status": report["status"], "report": str(output), "counts": counts, "errors": errors}, indent=2)); return 0 if not errors else 1
|
|
1264
1281
|
|
|
1265
1282
|
|
|
1283
|
+
def cmd_treatment_group_audit(args):
|
|
1284
|
+
"""Require one treatment record with explicit duration/price variants."""
|
|
1285
|
+
data = load(root(args))
|
|
1286
|
+
errors = []
|
|
1287
|
+
groups = {}
|
|
1288
|
+
for service in data.get("services", []):
|
|
1289
|
+
group = service.get("treatmentGroup") if isinstance(service.get("treatmentGroup"), dict) else {}
|
|
1290
|
+
group_id = str(group.get("id") or "")
|
|
1291
|
+
if not group_id:
|
|
1292
|
+
errors.append(f"{service.get('id', 'unknown')}: missing treatmentGroup.id")
|
|
1293
|
+
continue
|
|
1294
|
+
if group_id in groups:
|
|
1295
|
+
errors.append(f"duplicate treatment group: {group_id}")
|
|
1296
|
+
groups[group_id] = service.get("id")
|
|
1297
|
+
variants = service.get("variants") if isinstance(service.get("variants"), list) else []
|
|
1298
|
+
if not variants:
|
|
1299
|
+
errors.append(f"{service.get('id', 'unknown')}: treatment group has no variants")
|
|
1300
|
+
if group.get("variantCount") != len(variants):
|
|
1301
|
+
errors.append(f"{service.get('id', 'unknown')}: treatmentGroup.variantCount is stale")
|
|
1302
|
+
durations = [item.get("durationMinutes") for item in variants if isinstance(item, dict) and isinstance(item.get("durationMinutes"), (int, float))]
|
|
1303
|
+
duration_range = group.get("durationRange")
|
|
1304
|
+
if durations and (not isinstance(duration_range, dict) or duration_range.get("minMinutes") != min(durations) or duration_range.get("maxMinutes") != max(durations)):
|
|
1305
|
+
errors.append(f"{service.get('id', 'unknown')}: treatmentGroup.durationRange is stale")
|
|
1306
|
+
report = {"schemaVersion": "maggie-treatment-groups.v1", "passed": not errors, "errors": errors, "treatments": len(groups), "variants": sum(len(item.get("variants", [])) for item in data.get("services", []))}
|
|
1307
|
+
if args.output:
|
|
1308
|
+
output = (root(args) / args.output).resolve() if not Path(args.output).is_absolute() else Path(args.output)
|
|
1309
|
+
output.parent.mkdir(parents=True, exist_ok=True); output.write_text(json.dumps(report, indent=2, ensure_ascii=False) + "\n", encoding="utf-8")
|
|
1310
|
+
print(json.dumps(report, indent=2, ensure_ascii=False))
|
|
1311
|
+
return 0 if report["passed"] else 1
|
|
1312
|
+
|
|
1266
1313
|
def main():
|
|
1267
1314
|
p=argparse.ArgumentParser(); sub=p.add_subparsers(dest="command",required=True)
|
|
1268
1315
|
for name in ("import","sync","run"):
|
|
@@ -1279,6 +1326,7 @@ def main():
|
|
|
1279
1326
|
q=sub.add_parser("category-hash"); q.add_argument("--project",default="."); q.add_argument("--copy-data",required=True); q.add_argument("--apply",action="store_true",help="backfill deterministic provenance hashes in this generated artifact")
|
|
1280
1327
|
q=sub.add_parser("fact-audit"); q.add_argument("--project",default="."); q.add_argument("--backfill-source",action="store_true",help="copy the catalogue sourceUrl into records that have no sourceUrl"); q.add_argument("--overrides",help="JSON of human-approved, evidenced descriptions for provider gaps"); q.add_argument("--apply-overrides",action="store_true",help="apply only approved fact overrides to the draft catalogue")
|
|
1281
1328
|
q=sub.add_parser("catalogue-check"); q.add_argument("source"); q.add_argument("--project",default="."); q.add_argument("--provider",default="fresha",choices=["fresha"]); q.add_argument("--treatment",action="append",required=True,help="provider treatment name; repeat for multiple checks")
|
|
1329
|
+
q=sub.add_parser("treatment-group-audit", help="ensure each treatment is one entry with duration/price variants"); q.add_argument("--project",default="."); q.add_argument("--output")
|
|
1282
1330
|
q=sub.add_parser("retirement-audit"); q.add_argument("--project",default="."); q.add_argument("--catalogue",default=".maggie/booking/services.json"); q.add_argument("--evidence",required=True,help="sanitized runtime retirement evidence JSON"); q.add_argument("--output",default="docs/service-retirement-audit.json")
|
|
1283
1331
|
q=sub.add_parser("capability-audit", help="validate provider variant declarations and fixture evidence"); q.add_argument("--project",default="."); q.add_argument("--provider"); q.add_argument("--catalogue",default=".maggie/booking/services.json"); q.add_argument("--capabilities-file",default=".maggie/booking/provider-capabilities.json"); q.add_argument("--fixture",help="sanitized provider fixture JSON"); q.add_argument("--output")
|
|
1284
1332
|
q=sub.add_parser("category-context"); q.add_argument("--project",default="."); q.add_argument("--output")
|
|
@@ -1306,6 +1354,7 @@ def main():
|
|
|
1306
1354
|
if a.command=="category-hash": return cmd_category_hash(a)
|
|
1307
1355
|
if a.command=="fact-audit": return cmd_fact_audit(a)
|
|
1308
1356
|
if a.command=="catalogue-check": return cmd_catalogue_check(a)
|
|
1357
|
+
if a.command=="treatment-group-audit": return cmd_treatment_group_audit(a)
|
|
1309
1358
|
if a.command=="retirement-audit": return cmd_retirement_audit(a)
|
|
1310
1359
|
if a.command=="capability-audit": return cmd_capability_audit(a)
|
|
1311
1360
|
if a.command=="category-context": return cmd_category_context(a)
|
|
@@ -0,0 +1,387 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Stable, read-only policy and evidence gates for deferred Maggie workflows."""
|
|
3
|
+
|
|
4
|
+
from __future__ import annotations
|
|
5
|
+
|
|
6
|
+
import argparse
|
|
7
|
+
import hashlib
|
|
8
|
+
import html
|
|
9
|
+
import json
|
|
10
|
+
import shutil
|
|
11
|
+
import subprocess
|
|
12
|
+
import tempfile
|
|
13
|
+
from datetime import datetime, timezone
|
|
14
|
+
from pathlib import Path
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
ROOT = Path(__file__).resolve().parents[2]
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def contract_root() -> Path:
|
|
21
|
+
tool_path = Path(__file__).resolve()
|
|
22
|
+
bundled_tools = next((path for path in tool_path.parents if path.name == "bundled-tools"), None)
|
|
23
|
+
if bundled_tools:
|
|
24
|
+
return bundled_tools.parent / "bundled-contracts"
|
|
25
|
+
return ROOT / "contracts"
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def now() -> str:
|
|
29
|
+
return datetime.now(timezone.utc).isoformat().replace("+00:00", "Z")
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def load(path: Path) -> dict:
|
|
33
|
+
value = json.loads(path.read_text(encoding="utf-8"))
|
|
34
|
+
if not isinstance(value, dict):
|
|
35
|
+
raise ValueError(f"{path} must contain a JSON object")
|
|
36
|
+
return value
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def emit(value: dict, output: str | None = None) -> int:
|
|
40
|
+
if output:
|
|
41
|
+
target = Path(output).expanduser().resolve()
|
|
42
|
+
target.parent.mkdir(parents=True, exist_ok=True)
|
|
43
|
+
target.write_text(json.dumps(value, indent=2, ensure_ascii=False) + "\n", encoding="utf-8")
|
|
44
|
+
print(json.dumps(value, indent=2, ensure_ascii=False))
|
|
45
|
+
return 0 if value.get("passed") is True else 1
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def validate_media(args: argparse.Namespace) -> int:
|
|
49
|
+
policy = load(Path(args.policy).expanduser().resolve())
|
|
50
|
+
kind = args.kind
|
|
51
|
+
expected = f"maggie-{kind}-generation-policy.v1"
|
|
52
|
+
errors: list[str] = []
|
|
53
|
+
if policy.get("schemaVersion") != expected:
|
|
54
|
+
errors.append(f"schemaVersion must be {expected}")
|
|
55
|
+
if policy.get("provider") != "google-gemini":
|
|
56
|
+
errors.append("provider must be google-gemini")
|
|
57
|
+
if not str(policy.get("model", "")).startswith("gemini-"):
|
|
58
|
+
errors.append("model must be a Gemini model id")
|
|
59
|
+
if not str(policy.get("fallbackModel", "")).startswith(("gemini-", "veo-")):
|
|
60
|
+
errors.append("fallbackModel must be a Gemini or Veo model id")
|
|
61
|
+
if policy.get("endpointEnv") != "GEMINI_API_ENDPOINT" or policy.get("apiKeyEnv") != "GEMINI_API_KEY":
|
|
62
|
+
errors.append("Gemini endpoint and key must remain host environment names")
|
|
63
|
+
rights = policy.get("rights") if isinstance(policy.get("rights"), dict) else {}
|
|
64
|
+
moderation = policy.get("moderation") if isinstance(policy.get("moderation"), dict) else {}
|
|
65
|
+
for key in ("inputRightsRequired", "outputReviewRequired"):
|
|
66
|
+
if rights.get(key) is not True:
|
|
67
|
+
errors.append(f"rights.{key} must be true")
|
|
68
|
+
for key in ("preflightRequired", "postGenerationReviewRequired"):
|
|
69
|
+
if moderation.get(key) is not True:
|
|
70
|
+
errors.append(f"moderation.{key} must be true")
|
|
71
|
+
if kind == "image":
|
|
72
|
+
outputs = policy.get("outputs") if isinstance(policy.get("outputs"), dict) else {}
|
|
73
|
+
if not outputs.get("allowedMimeTypes") or not outputs.get("allowedSizes"):
|
|
74
|
+
errors.append("image outputs must declare mime types and sizes")
|
|
75
|
+
else:
|
|
76
|
+
cost = policy.get("costPolicy") if isinstance(policy.get("costPolicy"), dict) else {}
|
|
77
|
+
storage = policy.get("storage") if isinstance(policy.get("storage"), dict) else {}
|
|
78
|
+
if cost.get("budgetRequired") is not True or cost.get("duplicateChargeProtection") != "idempotency-key":
|
|
79
|
+
errors.append("video cost policy must require a budget and idempotency protection")
|
|
80
|
+
if storage.get("adapter") != "host-owned-object-storage":
|
|
81
|
+
errors.append("video storage must remain host-owned-object-storage")
|
|
82
|
+
provenance = policy.get("provenance")
|
|
83
|
+
if not isinstance(provenance, list) or not {"model", "sourceRevision", "createdAt"}.issubset(provenance):
|
|
84
|
+
errors.append("policy provenance must include model, sourceRevision, and createdAt")
|
|
85
|
+
return emit({"schemaVersion": expected, "passed": not errors, "kind": kind, "provider": policy.get("provider"), "model": policy.get("model"), "fallbackModel": policy.get("fallbackModel"), "errors": errors, "credentialsPrinted": False}, args.output)
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def validate_video_job(args: argparse.Namespace) -> int:
|
|
89
|
+
job = load(Path(args.job).expanduser().resolve())
|
|
90
|
+
errors: list[str] = []
|
|
91
|
+
if job.get("schemaVersion") != "maggie-video-job.v1":
|
|
92
|
+
errors.append("schemaVersion must be maggie-video-job.v1")
|
|
93
|
+
for key in ("jobId", "idempotencyKey", "sourceRevision", "provenance"):
|
|
94
|
+
if not job.get(key):
|
|
95
|
+
errors.append(f"{key} is required")
|
|
96
|
+
state = job.get("state")
|
|
97
|
+
attempt = job.get("attempt")
|
|
98
|
+
if state == "retrying" and (not isinstance(attempt, int) or attempt < 1 or attempt > 3):
|
|
99
|
+
errors.append("retrying jobs need attempt between 1 and 3")
|
|
100
|
+
if state == "dead-letter" and job.get("errorClass") not in {"provider-permanent", "moderation", "rights", "validation", "cost-limit", "unknown"}:
|
|
101
|
+
errors.append("dead-letter jobs need a terminal errorClass")
|
|
102
|
+
if state == "completed" and not isinstance(job.get("storage"), dict):
|
|
103
|
+
errors.append("completed jobs need host storage evidence")
|
|
104
|
+
return emit({"schemaVersion": "maggie-video-job.v1", "passed": not errors, "jobId": job.get("jobId"), "state": state, "errors": errors, "rawProviderPayloadIncluded": False}, args.output)
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def git_value(project: Path, *args: str) -> str:
|
|
108
|
+
result = subprocess.run(["git", "-C", str(project), *args], capture_output=True, text=True, check=False)
|
|
109
|
+
if result.returncode:
|
|
110
|
+
raise ValueError(result.stderr.strip() or "not a git worktree")
|
|
111
|
+
return result.stdout.strip()
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def capture_provenance(args: argparse.Namespace) -> int:
|
|
115
|
+
project = Path(args.project).expanduser().resolve()
|
|
116
|
+
source = Path(args.source).expanduser().resolve() if args.source else project
|
|
117
|
+
try:
|
|
118
|
+
relative = source.relative_to(project).as_posix() or "."
|
|
119
|
+
except ValueError as error:
|
|
120
|
+
raise ValueError("--source must be inside --project") from error
|
|
121
|
+
if any(part in {".env", ".git", "node_modules"} for part in source.parts):
|
|
122
|
+
raise ValueError("source cannot be a secret, git metadata, or dependency path")
|
|
123
|
+
commit = git_value(project, "rev-parse", "HEAD")
|
|
124
|
+
branch = git_value(project, "branch", "--show-current") or "detached"
|
|
125
|
+
dirty = bool(git_value(project, "status", "--porcelain", "--", relative))
|
|
126
|
+
tracked = subprocess.run(["git", "-C", str(project), "ls-files", "--error-unmatch", relative], capture_output=True, text=True, check=False).returncode == 0
|
|
127
|
+
value = {"schemaVersion": "maggie-content-provenance.v1", "commit": commit, "branch": branch, "dirty": dirty, "sourceRevision": commit, "sourcePath": relative, "authoringBoundary": args.boundary, "tracked": tracked, "staleWritePolicy": "reject-unless-source-revision-matches", "capturedAt": now()}
|
|
128
|
+
# capturedAt is useful in a report but not part of the contract artifact.
|
|
129
|
+
return emit({**value, "passed": True}, args.output)
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
FRAMEWORK_PACKAGES = {
|
|
133
|
+
"astro": ["astro"],
|
|
134
|
+
"nextjs": ["next", "react", "react-dom"],
|
|
135
|
+
"sveltekit": ["@sveltejs/kit"],
|
|
136
|
+
"nuxt": ["nuxt"],
|
|
137
|
+
"vite-react": ["vite", "react", "react-dom"],
|
|
138
|
+
"vite-vue": ["vite", "vue"],
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
NPM_SUPPORT_MATRIX = [
|
|
142
|
+
{"packageManager": "npm", "supportedVersions": ["11.x"], "installCommand": "npm install", "lockfile": "package-lock-v3", "lifecyclePolicy": "ignore-scripts-smoke-first"},
|
|
143
|
+
{"packageManager": "pnpm", "supportedVersions": ["9.x", "10.x"], "installCommand": "pnpm install", "lockfile": "pnpm-lock.yaml", "lifecyclePolicy": "ignore-scripts-smoke-first"},
|
|
144
|
+
{"packageManager": "yarn", "supportedVersions": ["1.x", "4.x"], "installCommand": "yarn install", "lockfile": "yarn.lock", "lifecyclePolicy": "ignore-scripts-smoke-first"},
|
|
145
|
+
{"packageManager": "bun", "supportedVersions": ["1.x"], "installCommand": "bun install", "lockfile": "bun.lock", "lifecyclePolicy": "ignore-scripts-smoke-first"},
|
|
146
|
+
]
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
def scaffold_files(framework: str, language: str) -> dict[str, str]:
|
|
150
|
+
typed = language == "typescript"
|
|
151
|
+
if framework == "astro":
|
|
152
|
+
return {
|
|
153
|
+
"astro.config.mjs": "import { defineConfig } from 'astro/config';\n\nexport default defineConfig();\n",
|
|
154
|
+
f"src/pages/index.{'astro'}": "---\nconst title = 'Maggie project';\n---\n<html lang=\"en\">\n <head><meta charset=\"utf-8\" /><meta name=\"viewport\" content=\"width=device-width\" /><title>{title}</title></head>\n <body><main><h1>{title}</h1><p>Generated by Maggie.</p></main></body>\n</html>\n",
|
|
155
|
+
}
|
|
156
|
+
if framework == "nextjs":
|
|
157
|
+
extension = "tsx" if typed else "jsx"
|
|
158
|
+
return {
|
|
159
|
+
f"app/layout.{extension}": "export default function RootLayout({ children }) { return <html lang=\"en\"><body>{children}</body></html>; }\n",
|
|
160
|
+
f"app/page.{extension}": "export default function Home() { return <main><h1>Maggie project</h1><p>Generated by Maggie.</p></main>; }\n",
|
|
161
|
+
"next.config.mjs": "/** @type {import('next').NextConfig} */\nconst nextConfig = {};\nexport default nextConfig;\n",
|
|
162
|
+
}
|
|
163
|
+
if framework == "sveltekit":
|
|
164
|
+
return {
|
|
165
|
+
"src/routes/+page.svelte": "<svelte:head><title>Maggie project</title></svelte:head>\n<main><h1>Maggie project</h1><p>Generated by Maggie.</p></main>\n",
|
|
166
|
+
"svelte.config.js": "import adapter from '@sveltejs/adapter-auto';\nexport default { kit: { adapter: adapter() } };\n",
|
|
167
|
+
"vite.config.js": "import { sveltekit } from '@sveltejs/kit/vite';\nimport { defineConfig } from 'vite';\nexport default defineConfig({ plugins: [sveltekit()] });\n",
|
|
168
|
+
}
|
|
169
|
+
if framework == "nuxt":
|
|
170
|
+
extension = "ts" if typed else "js"
|
|
171
|
+
return {
|
|
172
|
+
f"pages/index.vue": "<template><main><h1>Maggie project</h1><p>Generated by Maggie.</p></main></template>\n",
|
|
173
|
+
f"nuxt.config.{extension}": "export default defineNuxtConfig({});\n",
|
|
174
|
+
}
|
|
175
|
+
if framework == "vite-react":
|
|
176
|
+
extension = "tsx" if typed else "jsx"
|
|
177
|
+
return {
|
|
178
|
+
"index.html": "<div id=\"root\"></div><script type=\"module\" src=\"/src/main.%s\"></script>\n" % extension,
|
|
179
|
+
f"src/main.{extension}": "import React from 'react';\nimport { createRoot } from 'react-dom/client';\nimport App from './App';\nimport './style.css';\ncreateRoot(document.getElementById('root')).render(<React.StrictMode><App /></React.StrictMode>);\n",
|
|
180
|
+
f"src/App.{extension}": "export default function App() { return <main><h1>Maggie project</h1><p>Generated by Maggie.</p></main>; }\n",
|
|
181
|
+
"src/style.css": "body { margin: 0; font-family: system-ui, sans-serif; } main { padding: 3rem; }\n",
|
|
182
|
+
}
|
|
183
|
+
if framework == "vite-vue":
|
|
184
|
+
return {
|
|
185
|
+
"index.html": "<div id=\"app\"></div><script type=\"module\" src=\"/src/main.js\"></script>\n",
|
|
186
|
+
"src/main.js": "import { createApp } from 'vue';\nimport App from './App.vue';\nimport './style.css';\ncreateApp(App).mount('#app');\n",
|
|
187
|
+
"src/App.vue": "<template><main><h1>Maggie project</h1><p>Generated by Maggie.</p></main></template>\n",
|
|
188
|
+
"src/style.css": "body { margin: 0; font-family: system-ui, sans-serif; } main { padding: 3rem; }\n",
|
|
189
|
+
}
|
|
190
|
+
raise ValueError(f"unsupported scaffold framework: {framework}")
|
|
191
|
+
|
|
192
|
+
|
|
193
|
+
def package_manifest(project: Path, framework: str, language: str, package_manager: str, packages: list[str]) -> dict:
|
|
194
|
+
extension = "ts" if language == "typescript" else "js"
|
|
195
|
+
scripts = {"dev": "astro dev", "build": "astro build", "preview": "astro preview"} if framework == "astro" else {
|
|
196
|
+
"dev": "next dev", "build": "next build", "start": "next start"} if framework == "nextjs" else {
|
|
197
|
+
"dev": "vite dev", "build": "vite build", "preview": "vite preview"}
|
|
198
|
+
if framework == "sveltekit": scripts = {"dev": "vite dev", "build": "vite build", "preview": "vite preview"}
|
|
199
|
+
if framework == "nuxt": scripts = {"dev": "nuxt dev", "build": "nuxt build", "preview": "nuxt preview"}
|
|
200
|
+
dev_dependencies = {package: "latest" for package in packages}
|
|
201
|
+
return {"name": project.name.lower().replace("_", "-") or "maggie-project", "private": True, "type": "module", "scripts": scripts, "packageManager": package_manager, "maggie": {"framework": framework, "language": language, "entryExtension": extension}, "devDependencies": dev_dependencies}
|
|
202
|
+
|
|
203
|
+
|
|
204
|
+
def scaffold(args: argparse.Namespace) -> int:
|
|
205
|
+
project = Path(args.project).expanduser().resolve()
|
|
206
|
+
packages = FRAMEWORK_PACKAGES[args.framework]
|
|
207
|
+
generated = scaffold_files(args.framework, args.language)
|
|
208
|
+
manifest = {"schemaVersion": "maggie-host-scaffold.v1", "framework": args.framework, "language": args.language, "packageManager": args.package_manager, "installPolicy": {"requiresConfirm": True, "secretsByDefault": "never", "migrationsByDefault": "never", "installDependencies": bool(args.install_dependencies)}, "hostHandoff": {"generatedFiles": [".maggie/scaffold/manifest.json", *sorted(generated), "package.json"], "rollbackPlan": "remove generated files only and revert only the explicit dependency diff", "nextCommands": [f"maggie doctor --project {project}", "maggie ops preflight --project ."]}, "packages": packages, "createdAt": now()}
|
|
209
|
+
errors: list[str] = []
|
|
210
|
+
if args.install_dependencies and not args.confirm:
|
|
211
|
+
errors.append("--install-dependencies requires --confirm")
|
|
212
|
+
if args.write:
|
|
213
|
+
if errors:
|
|
214
|
+
return emit({"schemaVersion": "maggie-host-scaffold.v1", "passed": False, "errors": errors, "mutation": "not executed"}, args.output)
|
|
215
|
+
existing = [str(project / relative) for relative in [*generated, "package.json"] if (project / relative).exists()]
|
|
216
|
+
if existing and not args.force:
|
|
217
|
+
errors.append(f"refusing to overwrite existing scaffold files; use --force: {', '.join(existing[:5])}")
|
|
218
|
+
return emit({**manifest, "passed": False, "mutation": "not executed", "errors": errors}, args.output)
|
|
219
|
+
project.mkdir(parents=True, exist_ok=True)
|
|
220
|
+
package_path = project / "package.json"
|
|
221
|
+
package_path.write_text(json.dumps(package_manifest(project, args.framework, args.language, args.package_manager, packages), indent=2) + "\n", encoding="utf-8")
|
|
222
|
+
for relative, content in generated.items():
|
|
223
|
+
target = project / relative
|
|
224
|
+
target.parent.mkdir(parents=True, exist_ok=True)
|
|
225
|
+
target.write_text(content, encoding="utf-8")
|
|
226
|
+
target = project / ".maggie" / "scaffold" / "manifest.json"
|
|
227
|
+
target.parent.mkdir(parents=True, exist_ok=True)
|
|
228
|
+
target.write_text(json.dumps(manifest, indent=2, ensure_ascii=False) + "\n", encoding="utf-8")
|
|
229
|
+
if args.install_dependencies:
|
|
230
|
+
install_cmd = [args.package_manager, "add" if args.package_manager in {"pnpm", "yarn", "bun"} else "install", *packages]
|
|
231
|
+
if args.package_manager == "npm":
|
|
232
|
+
install_cmd = ["npm", "install", *packages]
|
|
233
|
+
subprocess.run(install_cmd, cwd=project, check=True)
|
|
234
|
+
return emit({**manifest, "passed": not errors, "mutation": "executed" if args.write else "plan-only", "errors": errors}, args.output)
|
|
235
|
+
|
|
236
|
+
|
|
237
|
+
def validate_brand(args: argparse.Namespace) -> int:
|
|
238
|
+
kit = load(Path(args.manifest).expanduser().resolve())
|
|
239
|
+
errors: list[str] = []
|
|
240
|
+
required = {"schemaVersion": "maggie-brand-kit.v1", "designBoundary": "DESIGN.md-and-host-tokens-are-source-of-truth"}
|
|
241
|
+
for key, expected in required.items():
|
|
242
|
+
if kit.get(key) != expected:
|
|
243
|
+
errors.append(f"{key} must be {expected}")
|
|
244
|
+
for key in ("designTokens", "logo", "typography", "icons", "assetManifest", "preview"):
|
|
245
|
+
if not kit.get(key):
|
|
246
|
+
errors.append(f"{key} is required")
|
|
247
|
+
if isinstance(kit.get("icons"), dict) and kit["icons"].get("policy") not in {"library-first", "host-defined"}:
|
|
248
|
+
errors.append("icons.policy must declare library-first or host-defined")
|
|
249
|
+
return emit({"schemaVersion": "maggie-brand-kit.v1", "passed": not errors, "errors": errors, "designTokensMutated": False}, args.output)
|
|
250
|
+
|
|
251
|
+
|
|
252
|
+
def token_values(design_path: Path, token_path: Path | None) -> dict:
|
|
253
|
+
if token_path:
|
|
254
|
+
return load(token_path)
|
|
255
|
+
content = design_path.read_text(encoding="utf-8")
|
|
256
|
+
found = {name: value.strip() for name, value in __import__("re").findall(r"(--[A-Za-z0-9_-]+)\s*:\s*([^;\n]+)", content)}
|
|
257
|
+
return found or {"source": str(design_path), "contentHash": hashlib.sha256(content.encode()).hexdigest()}
|
|
258
|
+
|
|
259
|
+
|
|
260
|
+
def brand_kit(args: argparse.Namespace) -> int:
|
|
261
|
+
project = Path(args.project).expanduser().resolve()
|
|
262
|
+
design_path = Path(args.design).expanduser().resolve() if args.design else project / "DESIGN.md"
|
|
263
|
+
token_path = Path(args.tokens).expanduser().resolve() if args.tokens else None
|
|
264
|
+
errors: list[str] = []
|
|
265
|
+
if not design_path.is_file(): errors.append(f"DESIGN.md is missing: {design_path}")
|
|
266
|
+
if token_path and not token_path.is_file(): errors.append(f"tokens file is missing: {token_path}")
|
|
267
|
+
if errors: return emit({"schemaVersion": "maggie-brand-kit.v1", "passed": False, "errors": errors, "designTokensMutated": False}, args.output)
|
|
268
|
+
design_tokens = token_values(design_path, token_path)
|
|
269
|
+
assets: list[str] = []
|
|
270
|
+
if args.assets:
|
|
271
|
+
asset_root = Path(args.assets).expanduser().resolve()
|
|
272
|
+
if not asset_root.is_dir(): errors.append(f"asset directory is missing: {asset_root}")
|
|
273
|
+
else: assets = [str(path.relative_to(project)) if path.is_relative_to(project) else str(path) for path in sorted(asset_root.rglob("*")) if path.is_file()]
|
|
274
|
+
kit = {"schemaVersion": "maggie-brand-kit.v1", "designTokens": design_tokens, "logo": {"primary": args.logo or "host-defined", "variants": ["primary", "monochrome"]}, "typography": {"families": [args.font or "host-defined"], "weights": [400, 500, 600, 700]}, "icons": {"library": args.icon_library, "policy": "library-first"}, "assetManifest": ".maggie/brand-kit/assets.json", "preview": ".maggie/brand-kit/preview.html", "designBoundary": "DESIGN.md-and-host-tokens-are-source-of-truth", "sources": {"design": str(design_path), "tokens": str(token_path) if token_path else None}}
|
|
275
|
+
if args.write:
|
|
276
|
+
if not args.confirm: errors.append("--write requires --confirm")
|
|
277
|
+
if not errors:
|
|
278
|
+
target = project / ".maggie" / "brand-kit"
|
|
279
|
+
target.mkdir(parents=True, exist_ok=True)
|
|
280
|
+
(target / "brand-kit.json").write_text(json.dumps(kit, indent=2, ensure_ascii=False) + "\n", encoding="utf-8")
|
|
281
|
+
(target / "assets.json").write_text(json.dumps({"schemaVersion": "maggie-brand-assets.v1", "assets": assets, "redacted": True}, indent=2, ensure_ascii=False) + "\n", encoding="utf-8")
|
|
282
|
+
swatches = "".join(f"<li><code>{html.escape(str(key))}</code><span style=\"background:{html.escape(str(value))}\"></span><small>{html.escape(str(value))}</small></li>" for key, value in design_tokens.items() if isinstance(value, (str, int, float)))
|
|
283
|
+
preview = "<!doctype html><meta charset=\"utf-8\"><title>Maggie brand kit preview</title><style>body{font:16px system-ui,sans-serif;max-width:60rem;margin:2rem auto;padding:0 1rem;color:#172033}ul{list-style:none;padding:0}li{display:flex;gap:.75rem;align-items:center;padding:.5rem 0;border-bottom:1px solid #ddd}li span{width:2rem;height:2rem;border:1px solid #aaa;border-radius:.35rem}small{color:#5b6472}</style><main><h1>Maggie brand kit</h1><p>Read-only preview generated from the approved DESIGN source.</p><h2>Design tokens</h2><ul>" + swatches + f"</ul><p>Logo: <code>{html.escape(str(kit['logo']['primary']))}</code></p><p>Icon library: <code>{html.escape(args.icon_library)}</code></p><p>Assets listed: {len(assets)}</p></main>\n"
|
|
284
|
+
(target / "preview.html").write_text(preview, encoding="utf-8")
|
|
285
|
+
return emit({**kit, "passed": not errors, "mutation": "executed" if args.write and not errors else "plan-only", "errors": errors, "designTokensMutated": False}, args.output)
|
|
286
|
+
|
|
287
|
+
|
|
288
|
+
def npm11(args: argparse.Namespace) -> int:
|
|
289
|
+
project = Path(args.project).expanduser().resolve()
|
|
290
|
+
package_path = project / "package.json"
|
|
291
|
+
errors: list[str] = []
|
|
292
|
+
package: dict = {}
|
|
293
|
+
if not package_path.is_file():
|
|
294
|
+
errors.append("package.json is missing")
|
|
295
|
+
else:
|
|
296
|
+
package = load(package_path)
|
|
297
|
+
engines = package.get("engines") if isinstance(package.get("engines"), dict) else {}
|
|
298
|
+
scripts = package.get("scripts") if isinstance(package.get("scripts"), dict) else {}
|
|
299
|
+
node_version = subprocess.run(["node", "--version"], capture_output=True, text=True, check=False).stdout.strip()
|
|
300
|
+
npm_version = subprocess.run(["npm", "--version"], capture_output=True, text=True, check=False).stdout.strip()
|
|
301
|
+
try:
|
|
302
|
+
npm_major = int(npm_version.split(".", 1)[0])
|
|
303
|
+
except (ValueError, IndexError):
|
|
304
|
+
npm_major = None
|
|
305
|
+
required_node = str(engines.get("node") or ">=18")
|
|
306
|
+
lifecycle_scripts = sorted(set(scripts).intersection({"install", "preinstall", "postinstall"}))
|
|
307
|
+
if lifecycle_scripts and not (args.smoke and args.confirm):
|
|
308
|
+
errors.append("install lifecycle scripts require an explicit audited release decision")
|
|
309
|
+
if npm_major is not None and npm_major < 11:
|
|
310
|
+
errors.append("npm 11 is required for this preflight")
|
|
311
|
+
smoke = {"requested": bool(args.smoke), "ignoreScripts": True, "passed": not errors, "command": None, "stderr": None}
|
|
312
|
+
if args.smoke:
|
|
313
|
+
if not args.confirm: errors.append("--smoke requires --confirm because it performs an isolated dependency install")
|
|
314
|
+
elif not package_path.is_file(): errors.append("--smoke requires package.json")
|
|
315
|
+
else:
|
|
316
|
+
with tempfile.TemporaryDirectory(prefix="maggie-npm11-") as temporary:
|
|
317
|
+
sandbox = Path(temporary)
|
|
318
|
+
shutil.copy2(package_path, sandbox / "package.json")
|
|
319
|
+
if (project / "package-lock.json").is_file(): shutil.copy2(project / "package-lock.json", sandbox / "package-lock.json")
|
|
320
|
+
command = ["npm", "install", "--ignore-scripts", "--no-audit", "--no-fund", "--package-lock=false"]
|
|
321
|
+
proc = subprocess.run(command, cwd=sandbox, capture_output=True, text=True, check=False, timeout=300)
|
|
322
|
+
smoke.update({"command": "npm install --ignore-scripts --no-audit --no-fund --package-lock=false", "exitCode": proc.returncode, "stderr": proc.stderr[-500:] if proc.stderr else None, "passed": proc.returncode == 0})
|
|
323
|
+
if proc.returncode: errors.append("isolated npm install smoke failed")
|
|
324
|
+
result = {"schemaVersion": "maggie-npm11-preflight.v1", "supportMatrix": NPM_SUPPORT_MATRIX, "node": {"supportedMajor": required_node, "observedMajor": node_version}, "npm": {"supportedMajor": 11, "observedMajor": npm_version}, "installScriptPolicy": "ignore-scripts-smoke-first", "lockfilePolicy": "package-lock-v3" if (project / "package-lock.json").exists() else "host-package-manager", "smoke": smoke, "scripts": sorted(scripts), "lifecycleScripts": lifecycle_scripts, "errors": errors, "passed": not errors}
|
|
325
|
+
return emit(result, args.output)
|
|
326
|
+
|
|
327
|
+
|
|
328
|
+
def validate_booking_delivery(args: argparse.Namespace) -> int:
|
|
329
|
+
policy = load(Path(args.policy).expanduser().resolve())
|
|
330
|
+
errors: list[str] = []
|
|
331
|
+
if policy.get("schemaVersion") != "maggie-booking-delivery-provider.v1":
|
|
332
|
+
errors.append("schemaVersion must be maggie-booking-delivery-provider.v1")
|
|
333
|
+
if policy.get("confirmationRule") != "provider-acknowledged-only":
|
|
334
|
+
errors.append("confirmationRule must be provider-acknowledged-only")
|
|
335
|
+
email = policy.get("email") if isinstance(policy.get("email"), dict) else {}
|
|
336
|
+
sms = policy.get("sms") if isinstance(policy.get("sms"), dict) else {}
|
|
337
|
+
if email.get("provider") not in {"resend", "provider-neutral"} or email.get("channel") != "email":
|
|
338
|
+
errors.append("email must use Resend or provider-neutral email")
|
|
339
|
+
if sms.get("provider") not in {"twilio", "provider-neutral"} or sms.get("channel") != "sms":
|
|
340
|
+
errors.append("sms must use Twilio or provider-neutral SMS")
|
|
341
|
+
retry = policy.get("retryPolicy") if isinstance(policy.get("retryPolicy"), dict) else {}
|
|
342
|
+
if retry.get("idempotency") != "booking-event-and-channel" or not isinstance(retry.get("maxAttempts"), int):
|
|
343
|
+
errors.append("retry policy must be idempotent and bounded")
|
|
344
|
+
dead = policy.get("deadLetterPolicy") if isinstance(policy.get("deadLetterPolicy"), dict) else {}
|
|
345
|
+
if dead.get("afterMaxAttempts") != "dead-letter" or dead.get("replayRequiresExplicitAction") is not True:
|
|
346
|
+
errors.append("dead-letter policy must require explicit replay")
|
|
347
|
+
return emit({"schemaVersion": "maggie-booking-delivery-provider.v1", "passed": not errors, "emailProvider": email.get("provider"), "smsProvider": sms.get("provider"), "errors": errors, "credentialsPrinted": False}, args.output)
|
|
348
|
+
|
|
349
|
+
|
|
350
|
+
def validate_playback(args: argparse.Namespace) -> int:
|
|
351
|
+
evidence = load(Path(args.evidence).expanduser().resolve())
|
|
352
|
+
errors: list[str] = []
|
|
353
|
+
if evidence.get("schemaVersion") != "maggie-video-playback-evidence.v1": errors.append("schemaVersion must be maggie-video-playback-evidence.v1")
|
|
354
|
+
if not evidence.get("sourceRevision") or not evidence.get("jobId"): errors.append("jobId and sourceRevision are required")
|
|
355
|
+
viewports = evidence.get("viewports") if isinstance(evidence.get("viewports"), list) else []
|
|
356
|
+
required = {"desktop", "tablet", "mobile"}
|
|
357
|
+
if not required.issubset({item.get("viewport") for item in viewports if isinstance(item, dict)}): errors.append("desktop, tablet, and mobile playback evidence are required")
|
|
358
|
+
for item in viewports:
|
|
359
|
+
if not isinstance(item, dict) or item.get("status") != "passed": errors.append("every viewport playback check must pass")
|
|
360
|
+
if isinstance(item, dict) and not item.get("posterVisible"): errors.append("posterVisible is required for each viewport")
|
|
361
|
+
if evidence.get("rawProviderPayloadIncluded") is not False: errors.append("raw provider payloads must not be included")
|
|
362
|
+
return emit({"schemaVersion": "maggie-video-playback-evidence.v1", "passed": not errors, "jobId": evidence.get("jobId"), "viewports": [item.get("viewport") for item in viewports if isinstance(item, dict)], "errors": errors, "rawProviderPayloadIncluded": False}, args.output)
|
|
363
|
+
|
|
364
|
+
|
|
365
|
+
def main() -> int:
|
|
366
|
+
parser = argparse.ArgumentParser(prog="maggie workflows")
|
|
367
|
+
sub = parser.add_subparsers(dest="command", required=True)
|
|
368
|
+
media = sub.add_parser("media"); media_sub = media.add_subparsers(dest="media_command", required=True)
|
|
369
|
+
for kind, relative in (("image", "maggie-media/image-generation-policy-v1.json"), ("video", "maggie-media/video-generation-policy-v1.json")):
|
|
370
|
+
command = media_sub.add_parser(f"{kind}-policy"); command.add_argument("--policy", default=str(contract_root() / relative)); command.add_argument("--output"); command.set_defaults(func=validate_media, kind=kind)
|
|
371
|
+
job = media_sub.add_parser("video-job"); job.add_argument("--job", required=True); job.add_argument("--output"); job.set_defaults(func=validate_video_job)
|
|
372
|
+
provenance = sub.add_parser("provenance"); provenance.add_argument("--project", default="."); provenance.add_argument("--source"); provenance.add_argument("--boundary", choices=["git-source", "host-runtime", "generated-artifact"], default="git-source"); provenance.add_argument("--output"); provenance.set_defaults(func=capture_provenance)
|
|
373
|
+
scaffold_parser = sub.add_parser("scaffold"); scaffold_parser.add_argument("--project", default="."); scaffold_parser.add_argument("--framework", choices=sorted(FRAMEWORK_PACKAGES), required=True); scaffold_parser.add_argument("--language", choices=["typescript", "javascript"], default="typescript"); scaffold_parser.add_argument("--package-manager", choices=["npm", "pnpm", "yarn", "bun"], default="npm"); scaffold_parser.add_argument("--install-dependencies", action="store_true"); scaffold_parser.add_argument("--write", action="store_true"); scaffold_parser.add_argument("--confirm", action="store_true"); scaffold_parser.add_argument("--force", action="store_true"); scaffold_parser.add_argument("--output"); scaffold_parser.set_defaults(func=scaffold)
|
|
374
|
+
brand = sub.add_parser("brand-kit"); brand.add_argument("--manifest"); brand.add_argument("--project", default="."); brand.add_argument("--design"); brand.add_argument("--tokens"); brand.add_argument("--logo"); brand.add_argument("--font"); brand.add_argument("--icon-library", default="host-icon-library"); brand.add_argument("--assets"); brand.add_argument("--write", action="store_true"); brand.add_argument("--confirm", action="store_true"); brand.add_argument("--output"); brand.set_defaults(func=lambda args: validate_brand(args) if args.manifest else brand_kit(args))
|
|
375
|
+
npm = sub.add_parser("npm11"); npm.add_argument("--project", default="."); npm.add_argument("--smoke", action="store_true"); npm.add_argument("--confirm", action="store_true"); npm.add_argument("--output"); npm.set_defaults(func=npm11)
|
|
376
|
+
booking = sub.add_parser("booking-delivery"); booking.add_argument("--policy", default=str(contract_root() / "maggie-service-booking/delivery-provider-default-v1.json")); booking.add_argument("--output"); booking.set_defaults(func=validate_booking_delivery)
|
|
377
|
+
playback = sub.add_parser("video-playback"); playback.add_argument("--evidence", required=True); playback.add_argument("--output"); playback.set_defaults(func=validate_playback)
|
|
378
|
+
args = parser.parse_args()
|
|
379
|
+
try:
|
|
380
|
+
return args.func(args)
|
|
381
|
+
except (OSError, ValueError, json.JSONDecodeError, subprocess.CalledProcessError) as error:
|
|
382
|
+
print(f"BLOCKED: maggie workflow: {error}", file=__import__("sys").stderr)
|
|
383
|
+
return 1
|
|
384
|
+
|
|
385
|
+
|
|
386
|
+
if __name__ == "__main__":
|
|
387
|
+
raise SystemExit(main())
|