@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.
Files changed (95) hide show
  1. package/README-zh-TW.md +82 -6
  2. package/README.md +99 -7
  3. package/bin/maggie.js +30 -3
  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/css-utility-evidence-v1.schema.json +10 -0
  9. package/bundled-contracts/maggie-design/dashboard-surface-v1.schema.json +11 -0
  10. package/bundled-contracts/maggie-design/progressive-enhancement-v1.schema.json +12 -0
  11. package/bundled-contracts/maggie-design/sample-surface-v1.schema.json +13 -0
  12. package/bundled-contracts/maggie-design/style-editing-v1.schema.json +35 -0
  13. package/bundled-contracts/maggie-media/image-generation-policy-v1.json +28 -0
  14. package/bundled-contracts/maggie-media/video-generation-policy-v1.json +40 -0
  15. package/bundled-contracts/maggie-media/video-job-v1.schema.json +20 -0
  16. package/bundled-contracts/maggie-media/video-playback-evidence-v1.schema.json +15 -0
  17. package/bundled-contracts/maggie-ops/npm11-preflight-v1.schema.json +17 -0
  18. package/bundled-contracts/maggie-scaffold/host-scaffold-v1.schema.json +25 -0
  19. package/bundled-contracts/maggie-seo/gsc-readiness-v1.schema.json +19 -0
  20. package/bundled-contracts/maggie-seo/privacy-origin-evidence-v1.schema.json +11 -0
  21. package/bundled-contracts/maggie-service-booking/delivery-provider-default-v1.json +8 -0
  22. package/bundled-contracts/maggie-service-booking/delivery-provider-v1.schema.json +16 -0
  23. package/bundled-contracts/maggiedash/booking-access-v1.json +5 -4
  24. package/bundled-contracts/maggiedash/booking-customer-surface-v1.json +12 -1
  25. package/bundled-contracts/maggiedash/booking-email-templates-v1.json +2 -0
  26. package/bundled-contracts/maggiedash/booking-host-adapter-v1.json +14 -2
  27. package/bundled-contracts/maggiedash/booking-runtime.v1.json +41 -0
  28. package/bundled-contracts/maggiedash/browser-session-v1.schema.json +18 -0
  29. package/bundled-contracts/maggiedash/content-overrides-v1.schema.json +19 -0
  30. package/bundled-contracts/maggiedash/execution-board.json +526 -26
  31. package/bundled-contracts/maggiedash/host-capabilities-v1.schema.json +10 -0
  32. package/bundled-contracts/maggiedash/public-session-cache-v1.schema.json +17 -0
  33. package/bundled-contracts/maggiedash/site-structure-v1.schema.json +13 -0
  34. package/bundled-references/browser-inspection.md +21 -0
  35. package/bundled-references/maggiedash-booking/ARCHITECTURE.md +218 -0
  36. package/bundled-references/maggiedash-booking/CURRENT-STATE.md +92 -0
  37. package/bundled-references/maggiedash-booking/DATA-FLOW.md +143 -0
  38. package/bundled-references/maggiedash-booking/DATA-MODEL.md +367 -0
  39. package/bundled-references/maggiedash-booking/DECISIONS.md +94 -0
  40. package/bundled-references/maggiedash-booking/EXECUTION-BOARD.json +2387 -0
  41. package/bundled-references/maggiedash-booking/HOST-ADAPTER.md +314 -0
  42. package/bundled-references/maggiedash-booking/ORAWELLNESS-INTEGRATION-AUDIT.md +227 -0
  43. package/bundled-references/maggiedash-booking/PAYMENT-GATEWAY.md +267 -0
  44. package/bundled-references/maggiedash-booking/PRD.md +228 -0
  45. package/bundled-references/maggiedash-booking/PROGRESS.md +2434 -0
  46. package/bundled-references/maggiedash-booking/QA-TEST-PLAN.md +235 -0
  47. package/bundled-references/maggiedash-booking/README.md +271 -0
  48. package/bundled-references/maggiedash-booking/RUNTIME-OPERATIONS.md +152 -0
  49. package/bundled-references/maggiedash-booking/SECURITY-COMPLIANCE.md +158 -0
  50. package/bundled-references/maggiedash-booking/SKILLS-AND-CLI.md +542 -0
  51. package/bundled-references/maggiedash-booking/STRIPE-INTEGRATION.md +129 -0
  52. package/bundled-references/maggiedash-booking/TASK-RUNBOOK.md +107 -0
  53. package/bundled-references/maggiedash-booking/TASKS.md +137 -0
  54. package/bundled-references/maggiedash-booking/USER-JOURNEYS.md +224 -0
  55. package/bundled-references/maggiedash-booking/diagrams/booking-dfd.excalidraw +1 -0
  56. package/bundled-references/maggiedash-booking/diagrams/booking-dfd.mmd +16 -0
  57. package/bundled-references/maggiedash-booking/diagrams/booking-dfd.png +0 -0
  58. package/bundled-references/maggiedash-booking/diagrams/booking-dfd.svg +1 -0
  59. package/bundled-references/maggiedash-booking/diagrams/booking-state-machine.mmd +19 -0
  60. package/bundled-references/maggiedash-booking/diagrams/booking-state-machine.png +0 -0
  61. package/bundled-references/maggiedash-booking/diagrams/booking-state-machine.svg +1 -0
  62. package/bundled-references/maggiedash-booking/diagrams/manager-journey.excalidraw +1 -0
  63. package/bundled-references/maggiedash-booking/diagrams/manager-journey.mmd +11 -0
  64. package/bundled-references/maggiedash-booking/diagrams/manager-journey.png +0 -0
  65. package/bundled-references/maggiedash-booking/diagrams/manager-journey.svg +1 -0
  66. package/bundled-references/maggiedash-booking/diagrams/payment-sequence.mmd +20 -0
  67. package/bundled-references/maggiedash-booking/diagrams/payment-sequence.png +0 -0
  68. package/bundled-references/maggiedash-booking/diagrams/payment-sequence.svg +1 -0
  69. package/bundled-references/maggiedash-booking/diagrams/system-context.excalidraw +1 -0
  70. package/bundled-references/maggiedash-booking/diagrams/system-context.mmd +10 -0
  71. package/bundled-references/maggiedash-booking/diagrams/system-context.png +0 -0
  72. package/bundled-references/maggiedash-booking/diagrams/system-context.svg +1 -0
  73. package/bundled-skills/maggie-blog/SKILL.md +12 -0
  74. package/bundled-skills/maggie-blog-bootstrap/SKILL.md +39 -0
  75. package/bundled-skills/maggie-booking/SKILL.md +112 -18
  76. package/bundled-skills/maggie-clone/SKILL.md +13 -0
  77. package/bundled-skills/maggie-deployment/SKILL.md +6 -0
  78. package/bundled-skills/maggie-design/SKILL.md +64 -3
  79. package/bundled-skills/maggie-ops/SKILL.md +12 -0
  80. package/bundled-skills/maggie-seo-geo/SKILL.md +45 -0
  81. package/bundled-skills/maggie-service-booking/SKILL.md +14 -0
  82. package/bundled-tools/clis/maggie_analytics.py +43 -1
  83. package/bundled-tools/clis/maggie_booking.py +161 -26
  84. package/bundled-tools/clis/maggie_browser_audit.py +99 -3
  85. package/bundled-tools/clis/maggie_clone.py +46 -1
  86. package/bundled-tools/clis/maggie_contracts.py +393 -0
  87. package/bundled-tools/clis/maggie_dash.py +22 -6
  88. package/bundled-tools/clis/maggie_design.py +84 -12
  89. package/bundled-tools/clis/maggie_service_booking.py +50 -1
  90. package/bundled-tools/clis/maggie_workflows.py +387 -0
  91. package/bundled-tools/clis/site_audit.py +77 -1
  92. package/bundled-tools/integrations/analytics.md +14 -0
  93. package/bundled-tools/runtime/site_baseline.py +3 -0
  94. package/package.json +1 -1
  95. package/references/browser-inspection.md +21 -0
@@ -8,6 +8,7 @@ import hashlib
8
8
  import json
9
9
  import re
10
10
  import sys
11
+ from datetime import datetime, timedelta, timezone
11
12
  from pathlib import Path
12
13
  from urllib.parse import urljoin, urlparse, urlunparse
13
14
  from html.parser import HTMLParser
@@ -69,9 +70,20 @@ class PageParser(HTMLParser):
69
70
  self.paragraphs = []
70
71
  self.links = []
71
72
  self.fragment_ids = set()
73
+ self.primary_navigation_links = []
74
+ self._header_depth = 0
75
+ self._nav_depth = 0
76
+ self._primary_nav_depth = 0
72
77
 
73
78
  def handle_starttag(self, tag, attrs):
74
79
  data = dict(attrs)
80
+ if tag == "header":
81
+ self._header_depth += 1
82
+ if tag == "nav":
83
+ self._nav_depth += 1
84
+ label = f"{data.get('aria-label', '')} {data.get('id', '')} {data.get('class', '')}".lower()
85
+ if self._header_depth or any(token in label for token in ("primary", "main", "navigation")):
86
+ self._primary_nav_depth += 1
75
87
  if data.get("id"):
76
88
  self.fragment_ids.add(data["id"])
77
89
  if tag == "a" and data.get("name"):
@@ -106,6 +118,8 @@ class PageParser(HTMLParser):
106
118
  self._jsonld = []
107
119
  elif tag == "a" and data.get("href"):
108
120
  self.anchors += 1
121
+ if self._primary_nav_depth:
122
+ self.primary_navigation_links.append(data.get("href", ""))
109
123
  elif tag == "img":
110
124
  self.images.append({"src": data.get("src", ""), "alt": data.get("alt")})
111
125
 
@@ -132,6 +146,12 @@ class PageParser(HTMLParser):
132
146
  except json.JSONDecodeError:
133
147
  self.jsonld_values.append(None)
134
148
  self._jsonld = None
149
+ if tag == "nav":
150
+ self._nav_depth = max(0, self._nav_depth - 1)
151
+ if self._primary_nav_depth:
152
+ self._primary_nav_depth -= 1
153
+ elif tag == "header":
154
+ self._header_depth = max(0, self._header_depth - 1)
135
155
 
136
156
  def handle_data(self, data):
137
157
  if not self.hidden_depth and data.strip():
@@ -157,6 +177,20 @@ def fetch(url: str, evidence: dict | None = None) -> tuple[int, str, str]:
157
177
  return response.status, response.headers.get_content_type(), body
158
178
 
159
179
 
180
+ def indexability_if_indexed(page: PageParser, robots: list[str]) -> dict:
181
+ """Explain what a noindex page would fail if it became indexable."""
182
+ tokens = [token.strip().rsplit(":", 1)[-1] for directive in robots for token in directive.split(",")]
183
+ blocked = sorted(set(tokens) & {"noindex", "none"})
184
+ checks = {
185
+ "title": bool(page.title),
186
+ "description": bool(page.meta.get("description")),
187
+ "canonical": bool(page.canonical) and urlparse(page.canonical).fragment == "",
188
+ "headings": bool(page.headings) and page.h1 == 1,
189
+ "jsonld": page.jsonld > 0 and all(item is not None for item in page.jsonld_values),
190
+ }
191
+ return {"applicable": bool(blocked), "blockedBy": blocked, "wouldPass": all(checks.values()) if blocked else None, "checks": checks if blocked else {}}
192
+
193
+
160
194
  def audit_page(url: str, html: str, status: int, content_type: str, expected_languages: set[str] | None = None, response_evidence: dict | None = None) -> dict:
161
195
  page = PageParser()
162
196
  page.feed(html)
@@ -165,6 +199,7 @@ def audit_page(url: str, html: str, status: int, content_type: str, expected_lan
165
199
  robots = page.robots_directives + [value.lower() for value in response_evidence["xRobotsTag"]]
166
200
  robots_tokens = [token.strip() for directive in robots for token in directive.split(",")]
167
201
  robots_tokens = [token.rsplit(":", 1)[-1].strip() for token in robots_tokens]
202
+ hypothetical = indexability_if_indexed(page, robots)
168
203
  return {
169
204
  "url": url,
170
205
  "status": status,
@@ -193,6 +228,7 @@ def audit_page(url: str, html: str, status: int, content_type: str, expected_lan
193
228
  "lang": page.lang, "hreflang": page.hreflang, "robots": robots,
194
229
  "jsonld": page.jsonld_values, "images": page.images,
195
230
  "fragmentIds": sorted(page.fragment_ids),
231
+ "primaryNavigationLinks": page.primary_navigation_links,
196
232
  "structureHash": hashlib.sha256(json.dumps(stable_structure(page.structure), sort_keys=True).encode()).hexdigest(),
197
233
  "textHash": hashlib.sha256(" ".join(page.visible_text).encode()).hexdigest(),
198
234
  },
@@ -215,9 +251,38 @@ def audit_page(url: str, html: str, status: int, content_type: str, expected_lan
215
251
  "robots": not robots or not any(token in {"noindex", "none", "nofollow"} for token in robots_tokens),
216
252
  "robots_directives": robots,
217
253
  "robots_conflict": len({token for token in robots_tokens if token in {"index", "noindex", "follow", "nofollow", "none"}} & {"index", "noindex"}) > 1 or len({token for token in robots_tokens if token in {"follow", "nofollow", "none"}} & {"follow", "nofollow"}) > 1,
254
+ "primary_navigation_links": page.primary_navigation_links,
255
+ "indexabilityIfIndexed": hypothetical,
218
256
  }
219
257
 
220
258
 
259
+ def primary_navigation_check(base: str, page: PageParser, fetcher=fetch) -> dict:
260
+ """Ensure links presented as primary navigation are indexable destinations."""
261
+ origin = urlparse(base).netloc
262
+ routes = []
263
+ for href in page.primary_navigation_links:
264
+ target = urljoin(base + "/", href)
265
+ parsed = urlparse(target)
266
+ if parsed.scheme not in {"http", "https"} or parsed.netloc != origin:
267
+ continue
268
+ target = urlunparse((parsed.scheme, parsed.netloc, parsed.path or "/", "", "", ""))
269
+ if target not in routes:
270
+ routes.append(target)
271
+ violations = []
272
+ for target in routes:
273
+ try:
274
+ status, content_type, body = fetcher(target)
275
+ destination = PageParser(); destination.feed(body)
276
+ directives = [item.lower() for item in destination.robots_directives]
277
+ tokens = [token.strip().rsplit(":", 1)[-1] for directive in directives for token in directive.split(",")]
278
+ blocked = sorted(set(tokens) & {"noindex", "nofollow", "none"})
279
+ if blocked:
280
+ violations.append({"url": target, "status": status, "directives": directives, "blocked": blocked})
281
+ except Exception as exc:
282
+ violations.append({"url": target, "error": type(exc).__name__})
283
+ return {"ok": not violations, "links": routes, "violations": violations}
284
+
285
+
221
286
  def summarize_crawl(pages: list[dict]) -> dict:
222
287
  """Count captured evidence, preserving regional/script locale distinctions."""
223
288
  groups = {}
@@ -400,6 +465,12 @@ def parse_baseline_reasons(values: list[str], drift_urls: set[str], reason_all:
400
465
  return reasons
401
466
 
402
467
 
468
+ def baseline_ack_expiry(days: int) -> str:
469
+ if not 1 <= days <= 3650:
470
+ raise ValueError("baseline acknowledgement TTL must be between 1 and 3650 days")
471
+ return (datetime.now(timezone.utc) + timedelta(days=days)).isoformat().replace("+00:00", "Z")
472
+
473
+
403
474
  def main() -> int:
404
475
  parser = argparse.ArgumentParser()
405
476
  parser.add_argument("url")
@@ -423,6 +494,7 @@ def main() -> int:
423
494
  parser.add_argument("--baseline-id", help="required stable ID for --recapture-baseline")
424
495
  parser.add_argument("--reason", action="append", default=[], metavar="URL=REASON", help="reviewer-approved reason for one drifting URL; repeat for every drift")
425
496
  parser.add_argument("--reason-all", help="reviewer-approved reason applied to every drifting URL during baseline recapture")
497
+ parser.add_argument("--reason-ttl-days", type=int, default=30, help="expiry for a reviewed baseline drift acknowledgement (default: 30)")
426
498
  parser.add_argument("--reviewer", help="required for --save-baseline")
427
499
  args = parser.parse_args()
428
500
  if args.max_pages < 1:
@@ -455,7 +527,8 @@ def main() -> int:
455
527
  base = args.url.rstrip("/")
456
528
  checks = {}
457
529
  try:
458
- status, content_type, html = fetch(base)
530
+ homepage_response = {}
531
+ status, content_type, html = fetch(base, homepage_response)
459
532
  page = PageParser()
460
533
  page.feed(html)
461
534
  checks["homepage"] = {"ok": status == 200 and content_type == "text/html", "status": status, "content_type": content_type}
@@ -470,9 +543,11 @@ def main() -> int:
470
543
  checks["jsonld"] = {"ok": page.jsonld > 0 and all(item is not None for item in page.jsonld_values), "count": page.jsonld}
471
544
  checks["entity_jsonld"] = {"ok": any(isinstance(item, dict) and item.get("@type") and (item.get("url") or item.get("@id")) for item in page.jsonld_values), "count": page.jsonld}
472
545
  checks["crawlable_links"] = {"ok": page.anchors > 0, "count": page.anchors}
546
+ checks["primary_navigation_indexability"] = primary_navigation_check(base, page)
473
547
  robots_tokens = [token.strip() for directive in page.robots_directives for token in directive.split(",")]
474
548
  checks["robots_directive"] = {"ok": not page.robots_directives or not any(token in {"noindex", "none", "nofollow"} for token in robots_tokens), "directives": page.robots_directives}
475
549
  checks["robots_conflict"] = {"ok": not (len({token for token in robots_tokens if token in {"index", "noindex"}}) > 1 or len({token for token in robots_tokens if token in {"follow", "nofollow"}}) > 1), "directives": page.robots_directives}
550
+ checks["indexability_if_indexed"] = {"ok": True, **indexability_if_indexed(page, page.robots_directives + [value.lower() for value in homepage_response.get("xRobotsTag", [])])}
476
551
  if args.check_hreflang or args.check_translation_completeness:
477
552
  checks["hreflang"] = hreflang_check(base, page, expected_languages) if args.check_hreflang else {"ok": True, "links": page.hreflang}
478
553
  if args.check_translation_completeness and expected_languages:
@@ -565,6 +640,7 @@ def main() -> int:
565
640
  baseline_id=args.baseline_id,
566
641
  supersedes=previous_baseline.get("baselineId") or site_baseline.baseline_fingerprint(previous_baseline),
567
642
  change_reasons=reasons,
643
+ approval_expires_at=baseline_ack_expiry(args.reason_ttl_days),
568
644
  )
569
645
  site_baseline.save(args.recapture_baseline, recaptured)
570
646
  result["baselineRecapture"] = {
@@ -60,6 +60,20 @@ GSC data is read-only in this starter. Any query, page, or indexing report must
60
60
  show its date window and property so an agent does not confuse an empty result
61
61
  with a failed connection.
62
62
 
63
+ The GSC readiness evidence contract is explicit and can be checked before the
64
+ combined release gate:
65
+
66
+ ```bash
67
+ maggie analytics gsc-readiness \
68
+ --gsc-evidence .maggie/gsc-readiness.json
69
+ ```
70
+
71
+ The evidence must cover property/verification, canonical origin, robots,
72
+ sitemap, read-only authorization, query/readback, and production smoke. Use
73
+ `--gsc-evidence ... --require-gsc` with `maggie analytics release-gate` to make
74
+ those checks blocking. The command does not authorize a property or mutate
75
+ Search Console.
76
+
63
77
  Run the deterministic configuration gate before enabling production tracking:
64
78
 
65
79
  ```bash
@@ -21,6 +21,7 @@ def snapshot(
21
21
  baseline_id: str | None = None,
22
22
  supersedes: str | None = None,
23
23
  change_reasons: dict[str, str] | None = None,
24
+ approval_expires_at: str | None = None,
24
25
  ) -> dict:
25
26
  if not reviewer.strip():
26
27
  raise ValueError("baseline requires a reviewer")
@@ -58,6 +59,8 @@ def snapshot(
58
59
  result["supersedes"] = supersedes
59
60
  if change_reasons:
60
61
  result["approvedChangeReasons"] = {key: change_reasons[key] for key in sorted(change_reasons)}
62
+ if approval_expires_at:
63
+ result["approval"] = {"type": "reviewed-drift-acknowledgement", "expiresAt": approval_expires_at}
61
64
  return result
62
65
 
63
66
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@topy-ai/maggie",
3
- "version": "0.7.38",
3
+ "version": "0.7.41",
4
4
  "description": "Install and manage Maggie Skills for AI coding agents",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -27,6 +27,27 @@ interaction states and triggers
27
27
  responsive differences
28
28
  ```
29
29
 
30
+ Behavioral checks may be declared instead of hidden in a bespoke script. The
31
+ manifest is safe to commit because input values are not copied into evidence:
32
+
33
+ ```json
34
+ {
35
+ "schemaVersion": "maggie-browser-interactions.v1",
36
+ "steps": [
37
+ {"id": "open", "action": "click", "selector": "[data-menu]"},
38
+ {"id": "visible", "action": "assert-visible", "selector": "[data-drawer]"},
39
+ {"id": "styles", "action": "assert-style", "selector": "[data-drawer]", "property": "display", "value": "block"},
40
+ {"id": "privacy", "action": "assert-no-request", "origin": "third-party.example"}
41
+ ]
42
+ }
43
+ ```
44
+
45
+ Run it with `maggie browser-audit ... --interactions .maggie/interactions.json`.
46
+ Supported actions cover click, fill, type, select, press, wait, visibility and
47
+ state assertions, computed-style assertions, and new-request assertions. The
48
+ browser adapter owns the session and the audit records only step IDs and
49
+ redacted outcomes.
50
+
30
51
  If a target requires login, a bot challenge, a consent interaction, or a
31
52
  private browser profile, stop at that boundary and ask the user to provide
32
53
  authorized access. Do not bypass access controls or record cookies in project