@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.
- package/README-zh-TW.md +34 -4
- package/README.md +41 -1
- package/bin/maggie.js +24 -5
- 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/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-media/video-provider-evidence-v1.schema.json +25 -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-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/browser-session-v1.schema.json +18 -0
- package/bundled-contracts/maggiedash/content-overrides-v1.schema.json +19 -0
- package/bundled-contracts/maggiedash/public-session-cache-v1.schema.json +17 -0
- package/bundled-references/browser-inspection.md +21 -0
- package/bundled-skills/maggie-blog/SKILL.md +12 -0
- package/bundled-skills/maggie-blog-bootstrap/SKILL.md +23 -0
- package/bundled-skills/maggie-booking/SKILL.md +15 -0
- 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 +29 -3
- package/bundled-skills/maggie-ops/SKILL.md +12 -0
- package/bundled-skills/maggie-seo-geo/SKILL.md +33 -0
- package/bundled-tools/clis/maggie_analytics.py +43 -1
- 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 +111 -0
- package/bundled-tools/clis/maggie_design.py +55 -0
- package/bundled-tools/clis/maggie_workflows.py +439 -0
- package/bundled-tools/clis/site_audit.py +28 -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
|
@@ -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())
|