@topy-ai/maggie 0.7.40 → 0.7.42

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 (41) hide show
  1. package/README-zh-TW.md +34 -4
  2. package/README.md +41 -1
  3. package/bin/maggie.js +24 -5
  4. package/bundled-contracts/maggie-clone/interaction-state-v1.schema.json +26 -0
  5. package/bundled-contracts/maggie-content/provenance-v1.schema.json +20 -0
  6. package/bundled-contracts/maggie-design/brand-kit-v1.schema.json +18 -0
  7. package/bundled-contracts/maggie-design/browser-interactions-v1.schema.json +27 -0
  8. package/bundled-contracts/maggie-design/style-editing-v1.schema.json +35 -0
  9. package/bundled-contracts/maggie-media/image-generation-policy-v1.json +28 -0
  10. package/bundled-contracts/maggie-media/video-generation-policy-v1.json +40 -0
  11. package/bundled-contracts/maggie-media/video-job-v1.schema.json +20 -0
  12. package/bundled-contracts/maggie-media/video-playback-evidence-v1.schema.json +15 -0
  13. package/bundled-contracts/maggie-media/video-provider-evidence-v1.schema.json +25 -0
  14. package/bundled-contracts/maggie-ops/npm11-preflight-v1.schema.json +17 -0
  15. package/bundled-contracts/maggie-scaffold/host-scaffold-v1.schema.json +25 -0
  16. package/bundled-contracts/maggie-seo/gsc-readiness-v1.schema.json +19 -0
  17. package/bundled-contracts/maggie-service-booking/delivery-provider-default-v1.json +8 -0
  18. package/bundled-contracts/maggie-service-booking/delivery-provider-v1.schema.json +16 -0
  19. package/bundled-contracts/maggiedash/browser-session-v1.schema.json +18 -0
  20. package/bundled-contracts/maggiedash/content-overrides-v1.schema.json +19 -0
  21. package/bundled-contracts/maggiedash/public-session-cache-v1.schema.json +17 -0
  22. package/bundled-references/browser-inspection.md +21 -0
  23. package/bundled-skills/maggie-blog/SKILL.md +12 -0
  24. package/bundled-skills/maggie-blog-bootstrap/SKILL.md +23 -0
  25. package/bundled-skills/maggie-booking/SKILL.md +15 -0
  26. package/bundled-skills/maggie-clone/SKILL.md +13 -0
  27. package/bundled-skills/maggie-deployment/SKILL.md +6 -0
  28. package/bundled-skills/maggie-design/SKILL.md +29 -3
  29. package/bundled-skills/maggie-ops/SKILL.md +12 -0
  30. package/bundled-skills/maggie-seo-geo/SKILL.md +33 -0
  31. package/bundled-tools/clis/maggie_analytics.py +43 -1
  32. package/bundled-tools/clis/maggie_browser_audit.py +99 -3
  33. package/bundled-tools/clis/maggie_clone.py +46 -1
  34. package/bundled-tools/clis/maggie_contracts.py +111 -0
  35. package/bundled-tools/clis/maggie_design.py +55 -0
  36. package/bundled-tools/clis/maggie_workflows.py +439 -0
  37. package/bundled-tools/clis/site_audit.py +28 -1
  38. package/bundled-tools/integrations/analytics.md +14 -0
  39. package/bundled-tools/runtime/site_baseline.py +3 -0
  40. package/package.json +1 -1
  41. package/references/browser-inspection.md +21 -0
@@ -237,6 +237,105 @@ def validate_capabilities(args: argparse.Namespace) -> int:
237
237
  return emit_and_exit(result("maggie-host-capabilities.v1", errors, enabled=sum(1 for item in endpoints if isinstance(item, dict) and item.get("enabled") is True), endpointCount=len(endpoints)), Path(args.output) if args.output else None)
238
238
 
239
239
 
240
+ def validate_style_editing(args: argparse.Namespace) -> int:
241
+ """Validate a closed, host-rendered style editing contract."""
242
+ manifest = read_json(Path(args.manifest))
243
+ errors: list[str] = []
244
+ if manifest.get("schemaVersion") != "maggie-style-editing.v1":
245
+ errors.append("manifest schemaVersion must be maggie-style-editing.v1")
246
+ properties = manifest.get("properties")
247
+ if not isinstance(properties, list) or not properties:
248
+ errors.append("properties must be a non-empty array")
249
+ properties = []
250
+ allowed: set[str] = set()
251
+ patterns: dict[str, str] = {}
252
+ dangerous = re.compile(r"(?:;|url\s*\(|expression\s*\(|<|>|javascript:)", re.I)
253
+ for item in properties:
254
+ if not isinstance(item, dict):
255
+ errors.append("each style property must be an object")
256
+ continue
257
+ name = str(item.get("name") or "")
258
+ pattern = str(item.get("pattern") or "")
259
+ if not re.fullmatch(r"[A-Za-z][A-Za-z0-9-]*", name):
260
+ errors.append(f"invalid style property name: {name or 'unknown'}")
261
+ if name in allowed:
262
+ errors.append(f"duplicate style property: {name}")
263
+ allowed.add(name)
264
+ if not pattern:
265
+ errors.append(f"style property {name or 'unknown'} needs a value pattern")
266
+ else:
267
+ try:
268
+ re.compile(pattern)
269
+ patterns[name] = pattern
270
+ except re.error:
271
+ errors.append(f"style property {name or 'unknown'} has an invalid value pattern")
272
+ for template in manifest.get("templates") or []:
273
+ if not isinstance(template, dict) or not str(template.get("id") or ""):
274
+ errors.append("each style template needs an id")
275
+ continue
276
+ for name, value in (template.get("styles") or {}).items():
277
+ if name not in allowed:
278
+ errors.append(f"style template {template['id']} uses undeclared property: {name}")
279
+ continue
280
+ if not isinstance(value, str) or dangerous.search(value) or not re.fullmatch(patterns[name], value):
281
+ errors.append(f"style template {template['id']} has an unsafe value for {name}")
282
+ return emit_and_exit(result("maggie-style-editing.v1", errors, properties=sorted(allowed), templates=len(manifest.get("templates") or [])), Path(args.output) if args.output else None)
283
+
284
+
285
+ def validate_content_overrides(args: argparse.Namespace) -> int:
286
+ manifest = read_json(Path(args.manifest))
287
+ errors: list[str] = []
288
+ if manifest.get("schemaVersion") != "maggie-content-overrides.v1":
289
+ errors.append("manifest schemaVersion must be maggie-content-overrides.v1")
290
+ expected = {
291
+ "readAtRequestTime": True,
292
+ "sourceLiteralIsFallback": True,
293
+ "writesCreateRevision": True,
294
+ "conflictRequiresSourceRevision": True,
295
+ "orphanPolicy": "report-and-never-publish",
296
+ }
297
+ for key, value in expected.items():
298
+ if manifest.get(key) != value:
299
+ errors.append(f"{key} must be {value!r}")
300
+ if not isinstance(manifest.get("protectedFields"), list) or not manifest.get("protectedFields"):
301
+ errors.append("protectedFields must list factual fields that cannot be overridden")
302
+ return emit_and_exit(result("maggie-content-overrides.v1", errors, protectedFields=manifest.get("protectedFields", [])), Path(args.output) if args.output else None)
303
+
304
+
305
+ def validate_session_contract(args: argparse.Namespace) -> int:
306
+ manifest = read_json(Path(args.manifest))
307
+ errors: list[str] = []
308
+ if manifest.get("schemaVersion") != "maggie-browser-session.v1":
309
+ errors.append("manifest schemaVersion must be maggie-browser-session.v1")
310
+ if manifest.get("mintingBoundary") != "host-adapter":
311
+ errors.append("mintingBoundary must be host-adapter")
312
+ if manifest.get("passwordHandling") != "never-exposed":
313
+ errors.append("passwordHandling must be never-exposed")
314
+ ttl = manifest.get("ttlMinutes")
315
+ if not isinstance(ttl, int) or not 1 <= ttl <= 240:
316
+ errors.append("ttlMinutes must be between 1 and 240")
317
+ for key in ("mintRoute", "revokeRoute"):
318
+ if not str(manifest.get(key) or "").startswith("/"):
319
+ errors.append(f"{key} must be a relative host route")
320
+ return emit_and_exit(result("maggie-browser-session.v1", errors, ttlMinutes=ttl), Path(args.output) if args.output else None)
321
+
322
+
323
+ def validate_cache_boundary(args: argparse.Namespace) -> int:
324
+ evidence = read_json(Path(args.evidence))
325
+ errors: list[str] = []
326
+ if evidence.get("schemaVersion") != "maggie-public-session-cache.v1":
327
+ errors.append("evidence schemaVersion must be maggie-public-session-cache.v1")
328
+ if evidence.get("publicHtmlVariesBySession") is not False:
329
+ errors.append("publicHtmlVariesBySession must be false")
330
+ if evidence.get("sessionCookieHttpOnly") is not True:
331
+ errors.append("sessionCookieHttpOnly must be true")
332
+ if evidence.get("identityHintCookieReadable") is not True:
333
+ errors.append("identityHintCookieReadable must be true")
334
+ if evidence.get("identityRequestWhenHintAbsent") is not False:
335
+ errors.append("identityRequestWhenHintAbsent must be false")
336
+ return emit_and_exit(result("maggie-public-session-cache.v1", errors), Path(args.output) if args.output else None)
337
+
338
+
240
339
  def main() -> int:
241
340
  parser = argparse.ArgumentParser(prog="maggie contracts")
242
341
  sub = parser.add_subparsers(dest="command", required=True)
@@ -270,6 +369,18 @@ def main() -> int:
270
369
  capabilities = sub.add_parser("capabilities-validate")
271
370
  capabilities.add_argument("--manifest", required=True); capabilities.add_argument("--output")
272
371
  capabilities.set_defaults(func=validate_capabilities)
372
+ style = sub.add_parser("style-check")
373
+ style.add_argument("--manifest", required=True); style.add_argument("--output")
374
+ style.set_defaults(func=validate_style_editing)
375
+ overrides = sub.add_parser("content-overrides-check")
376
+ overrides.add_argument("--manifest", required=True); overrides.add_argument("--output")
377
+ overrides.set_defaults(func=validate_content_overrides)
378
+ session = sub.add_parser("session-check")
379
+ session.add_argument("--manifest", required=True); session.add_argument("--output")
380
+ session.set_defaults(func=validate_session_contract)
381
+ cache = sub.add_parser("cache-boundary-check")
382
+ cache.add_argument("--evidence", required=True); cache.add_argument("--output")
383
+ cache.set_defaults(func=validate_cache_boundary)
273
384
  args = parser.parse_args()
274
385
  try:
275
386
  return args.func(args)
@@ -10,6 +10,7 @@ from __future__ import annotations
10
10
 
11
11
  import argparse
12
12
  import hashlib
13
+ import html as html_lib
13
14
  import json
14
15
  import re
15
16
  import shutil
@@ -82,6 +83,50 @@ def rendered_asset_preflight(template: Path) -> list[str]:
82
83
  return errors
83
84
 
84
85
 
86
+ def source_anchor_start(source: str, tag: str, reported_offset: int) -> int:
87
+ """Resolve an Astro compiler offset to the opening bracket of an element."""
88
+ if not re.fullmatch(r"[A-Za-z][A-Za-z0-9:-]*", tag):
89
+ raise ValueError("tag name is invalid")
90
+ bound = max(0, min(len(source), int(reported_offset) + 1))
91
+ start = source.rfind("<" + tag, 0, bound)
92
+ if start < 0:
93
+ raise ValueError(f"opening tag not found near compiler offset: {tag}")
94
+ return start
95
+
96
+
97
+ def decode_content_text(value: str) -> str:
98
+ """Decode markup entities once and refuse unknown/double-escaped entities."""
99
+ decoded = html_lib.unescape(value)
100
+ if re.search(r"&(?:[A-Za-z][A-Za-z0-9]+|#\d+|#x[0-9A-Fa-f]+);", decoded):
101
+ raise ValueError("text contains an unknown or double-escaped HTML entity")
102
+ return decoded
103
+
104
+
105
+ def markup_check(html_path: Path, require_keys: bool) -> int:
106
+ source = html_path.read_text(encoding="utf-8")
107
+ errors: list[str] = []
108
+ if re.search(r"\bstyle\s*=\s*([\"'])\s*\1", source, re.I):
109
+ errors.append("empty style attributes must be omitted")
110
+ keys = re.findall(r"\bdata-maggie-content-key\s*=\s*[\"']([^\"']+)[\"']", source, re.I)
111
+ duplicates = sorted({key for key in keys if keys.count(key) > 1})
112
+ if duplicates:
113
+ errors.append("duplicate content keys: " + ", ".join(duplicates))
114
+ if require_keys:
115
+ for match in re.finditer(r"<(p|h[1-6]|li|button|a|span)\b([^>]*)>([^<>]+)</\1>", source, re.I | re.S):
116
+ attrs, text = match.group(2), " ".join(match.group(3).split())
117
+ if text and not re.search(r"\bdata-maggie-content-key\s*=", attrs, re.I):
118
+ errors.append(f"editable text node lacks data-maggie-content-key: {match.group(1)}")
119
+ try:
120
+ decode_content_text(match.group(3))
121
+ except ValueError as error:
122
+ errors.append(str(error))
123
+ result = {"schemaVersion": "maggie-design-markup.v1", "passed": not errors,
124
+ "errors": errors, "contentKeyCount": len(keys), "requireKeys": require_keys,
125
+ "source": str(html_path.resolve())}
126
+ print(json.dumps(result, indent=2, ensure_ascii=False))
127
+ return 0 if not errors else 1
128
+
129
+
85
130
  def _surface_list(value: str) -> list[str]:
86
131
  surfaces = [item.strip().lower() for item in value.split(",") if item.strip()]
87
132
  invalid = sorted(set(surfaces) - set(REFERENCE_SURFACES))
@@ -808,6 +853,7 @@ commands:
808
853
  status show a design job and its per-step progress
809
854
  resume restart a failed design workflow
810
855
  author plan an original first-party page
856
+ markup-check validate optional style attributes, content keys, and entities
811
857
  reference-ui, init, validate-ui, app-init, app-validate, rebrand, review
812
858
  """)
813
859
  return 0
@@ -823,6 +869,15 @@ commands:
823
869
  return reference_ui(args.project, args.reference, _surface_list(args.surface), args.screenshots_dir, args.confirm)
824
870
  except (OSError, ValueError, json.JSONDecodeError) as error:
825
871
  print(f"BLOCKED: maggie-design reference-ui: {error}", file=sys.stderr); return 1
872
+ if len(sys.argv) > 1 and sys.argv[1] == "markup-check":
873
+ command = argparse.ArgumentParser(description="Validate generated markup before it is made editable.")
874
+ command.add_argument("--html", type=Path, required=True)
875
+ command.add_argument("--require-content-keys", action="store_true")
876
+ args = command.parse_args(sys.argv[2:])
877
+ try:
878
+ return markup_check(args.html, args.require_content_keys)
879
+ except (OSError, ValueError) as error:
880
+ print(f"BLOCKED: maggie-design markup-check: {error}", file=sys.stderr); return 1
826
881
  if len(sys.argv) > 1 and sys.argv[1] == "init":
827
882
  command = argparse.ArgumentParser(description="Initialize a native blog or service UI plan from a sanitized reference manifest.")
828
883
  command.add_argument("--project", type=Path, default=Path.cwd())