@topy-ai/maggie 0.7.17 → 0.7.19

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.
@@ -0,0 +1,58 @@
1
+ """Validate runtime evidence that a dashboard screen actually mounted and ran."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from collections.abc import Mapping
6
+ from typing import Any
7
+ from urllib.parse import urlparse
8
+
9
+
10
+ SCHEMA = "maggiedash-dashboard-runtime.v1"
11
+
12
+
13
+ def validate_runtime_evidence(evidence: object) -> dict[str, Any]:
14
+ errors: list[str] = []
15
+ if not isinstance(evidence, Mapping):
16
+ return {"schemaVersion": SCHEMA, "passed": False, "errors": ["evidence must be an object"]}
17
+ if evidence.get("schemaVersion") != SCHEMA:
18
+ errors.append(f"schemaVersion must be {SCHEMA}")
19
+ for field in ("screen", "route"):
20
+ if not isinstance(evidence.get(field), str) or not evidence[field].strip():
21
+ errors.append(f"{field} is required")
22
+ if evidence.get("mounted") is not True:
23
+ errors.append("mounted must be true")
24
+ checks = evidence.get("checks")
25
+ if not isinstance(checks, list) or not checks:
26
+ errors.append("checks must contain at least one executed check")
27
+ checks = []
28
+ check_results: list[dict[str, Any]] = []
29
+ for index, check in enumerate(checks):
30
+ if not isinstance(check, Mapping) or not isinstance(check.get("name"), str) or not check["name"].strip():
31
+ errors.append(f"checks[{index}] needs a name")
32
+ continue
33
+ if not isinstance(check.get("passed"), bool):
34
+ errors.append(f"checks[{index}].passed must be boolean")
35
+ elif check["passed"] is False:
36
+ errors.append(f"runtime check failed: {check['name']}")
37
+ check_results.append({"name": check["name"], "passed": check.get("passed")})
38
+ calls = evidence.get("dataCalls", [])
39
+ if not isinstance(calls, list):
40
+ errors.append("dataCalls must be a list")
41
+ calls = []
42
+ call_results: list[dict[str, Any]] = []
43
+ for index, call in enumerate(calls):
44
+ if not isinstance(call, Mapping):
45
+ errors.append(f"dataCalls[{index}] must be an object")
46
+ continue
47
+ url = str(call.get("url") or "")
48
+ parsed = urlparse(url)
49
+ if not url or parsed.username or parsed.password or parsed.scheme not in {"", "http", "https"}:
50
+ errors.append(f"dataCalls[{index}].url must be a relative or credential-free HTTP URL")
51
+ if not isinstance(call.get("method", "GET"), str):
52
+ errors.append(f"dataCalls[{index}].method must be a string")
53
+ if not isinstance(call.get("ok"), bool):
54
+ errors.append(f"dataCalls[{index}].ok must be boolean")
55
+ elif call["ok"] is False:
56
+ errors.append(f"data call failed: {url or 'unknown'}")
57
+ call_results.append({"url": url, "method": call.get("method", "GET"), "status": call.get("status"), "ok": call.get("ok")})
58
+ return {"schemaVersion": SCHEMA, "passed": not errors, "errors": errors, "screen": evidence.get("screen"), "route": evidence.get("route"), "mounted": evidence.get("mounted"), "checks": check_results, "dataCalls": call_results}
@@ -0,0 +1,72 @@
1
+ """Validate host-supplied table/reader evidence without connecting to a database."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from collections.abc import Mapping
6
+ from typing import Any
7
+
8
+
9
+ SCHEMA = "maggie-schema-inventory.v1"
10
+
11
+
12
+ def audit_schema_inventory(inventory: object, *, fail_on_unread: bool = False) -> dict[str, Any]:
13
+ errors: list[str] = []
14
+ warnings: list[str] = []
15
+ if not isinstance(inventory, Mapping):
16
+ return {"schemaVersion": SCHEMA, "passed": False, "errors": ["inventory must be an object"], "tables": []}
17
+ if inventory.get("schemaVersion") != SCHEMA:
18
+ errors.append(f"schemaVersion must be {SCHEMA}")
19
+ tables = inventory.get("tables")
20
+ if not isinstance(tables, list):
21
+ errors.append("tables must be a list")
22
+ tables = []
23
+ seen: set[str] = set()
24
+ normalized: list[dict[str, Any]] = []
25
+ unread: list[dict[str, Any]] = []
26
+ unknown: list[str] = []
27
+ for index, table in enumerate(tables):
28
+ prefix = f"tables[{index}]"
29
+ if not isinstance(table, Mapping):
30
+ errors.append(f"{prefix} must be an object")
31
+ continue
32
+ name = str(table.get("name") or "").strip()
33
+ if not name:
34
+ errors.append(f"{prefix}.name is required")
35
+ continue
36
+ if name in seen:
37
+ errors.append(f"duplicate table: {name}")
38
+ seen.add(name)
39
+ row_count = table.get("rowCount")
40
+ if not isinstance(row_count, int) or isinstance(row_count, bool) or row_count < 0:
41
+ errors.append(f"{prefix}.rowCount must be a non-negative integer")
42
+ row_count = None
43
+ readers = table.get("readers")
44
+ if readers is None:
45
+ unknown.append(name)
46
+ warnings.append(f"{name} has no reader evidence")
47
+ reader_list: list[str] = []
48
+ reader_status = "unknown"
49
+ elif not isinstance(readers, list) or any(not isinstance(reader, str) or not reader.strip() for reader in readers):
50
+ errors.append(f"{prefix}.readers must be a list of non-empty strings")
51
+ reader_list = []
52
+ reader_status = "invalid"
53
+ else:
54
+ reader_list = sorted({reader.strip() for reader in readers})
55
+ reader_status = "read" if reader_list else "unread"
56
+ record = {"name": name, "rowCount": row_count, "readers": reader_list, "readerStatus": reader_status}
57
+ normalized.append(record)
58
+ if reader_status == "unread":
59
+ unread.append(record)
60
+ if fail_on_unread and unread:
61
+ errors.append(f"{len(unread)} declared table(s) have no reader evidence")
62
+ return {
63
+ "schemaVersion": SCHEMA,
64
+ "passed": not errors,
65
+ "errors": errors,
66
+ "warnings": warnings,
67
+ "tables": normalized,
68
+ "unreadTables": unread,
69
+ "unknownReaderEvidence": unknown,
70
+ "tableCount": len(normalized),
71
+ "unreadCount": len(unread),
72
+ }
@@ -35,6 +35,16 @@ def variant_slug(kind: str, *, location: str = "", event: str = "", holiday: str
35
35
  return f"{kind}/{slug_part(value)}"
36
36
 
37
37
 
38
+ def locale_segment(value: str) -> str:
39
+ """Return the stable lowercase URL segment for a BCP 47-like locale."""
40
+ return slug_part(value.replace("_", "-"))
41
+
42
+
43
+ def variant_canonical_url(slug: str, locale: str, market: str) -> str:
44
+ """Build a collision-safe, market/language-aware service URL."""
45
+ return f"/services/{slug_part(market)}/{locale_segment(locale)}/{slug.strip('/')}/"
46
+
47
+
38
48
  def similarity(source: str, variant: str) -> float:
39
49
  left, right = set(re.findall(r"[a-z0-9]+", source.lower())), set(re.findall(r"[a-z0-9]+", variant.lower()))
40
50
  return 1.0 if not left and not right else len(left & right) / max(1, len(left | right))
@@ -62,6 +72,20 @@ class ServiceVariantStore:
62
72
  raise ValueError("service variant not found")
63
73
  return found
64
74
 
75
+ def _cluster(self, canonical_variant_id: str) -> list[dict]:
76
+ return [
77
+ item for item in self.data["variants"]
78
+ if item.get("id") == canonical_variant_id or item.get("canonicalVariantId") == canonical_variant_id
79
+ ]
80
+
81
+ def _refresh_hreflang(self, canonical_variant_id: str) -> None:
82
+ """Keep published locale links reciprocal and limited to published peers."""
83
+ cluster = self._cluster(canonical_variant_id)
84
+ published = [item for item in cluster if item.get("status") == "published"]
85
+ published_links = {item["locale"]: item["canonicalUrl"] for item in published}
86
+ for item in cluster:
87
+ item["hreflang"] = published_links if item in published else {item["locale"]: item["canonicalUrl"]}
88
+
65
89
  def create(self, *, service_id: str, variant_id: str, variant_type: str, locale: str, market: str,
66
90
  slug: str, title: str, facts: list[dict], source_revision: str, canonical_variant_id: str | None = None,
67
91
  cluster_links: list[str] | None = None, layout_family: str = "service-default", actor: str = "cli") -> dict:
@@ -73,15 +97,19 @@ class ServiceVariantStore:
73
97
  raise ValueError("variant requires meaningful keyed facts")
74
98
  if any(item["id"] == variant_id for item in self.data["variants"]):
75
99
  raise ValueError("variant ID already exists")
76
- if any(item["slug"] == slug and item["locale"] == locale for item in self.data["variants"]):
77
- raise ValueError("variant slug and locale collision")
78
100
  if canonical_variant_id and not any(item["id"] == canonical_variant_id for item in self.data["variants"]):
79
101
  raise ValueError("canonical variant does not exist")
102
+ canonical_url = variant_canonical_url(slug, locale, market)
103
+ if any(item.get("canonicalUrl") == canonical_url for item in self.data["variants"]):
104
+ raise ValueError("variant market, locale and slug route collision")
105
+ family = canonical_variant_id or variant_id
106
+ if any(item.get("canonicalVariantId") == family and item.get("locale") == locale for item in self.data["variants"]):
107
+ raise ValueError("variant locale collision in translation cluster")
80
108
  item = {"id": variant_id, "serviceId": service_id, "variantType": variant_type, "locale": locale,
81
109
  "market": market, "slug": slug, "title": title, "facts": facts,
82
110
  "clusterLinks": sorted(set(cluster_links or [])), "layoutFamily": layout_family,
83
- "canonicalVariantId": canonical_variant_id or variant_id, "canonicalUrl": "/services/" + slug + "/",
84
- "hreflang": {locale: "/services/" + slug + "/"}, "sourceRevision": source_revision,
111
+ "canonicalVariantId": family, "canonicalUrl": canonical_url,
112
+ "hreflang": {locale: canonical_url}, "sourceRevision": source_revision,
85
113
  "provenance": {"operation": "create", "actor": actor, "sourceRevision": source_revision},
86
114
  "status": "draft", "createdAt": now(), "updatedAt": now()}
87
115
  self.data["variants"].append(item); self._event("variant.created", variant_id, actor, "draft created"); self._save()
@@ -120,6 +148,7 @@ class ServiceVariantStore:
120
148
  raise ValueError("canonical source must be published before a translated variant")
121
149
  old = item["status"]; item["status"] = target; item["updatedAt"] = now()
122
150
  item.setdefault("approval", []).append({"from": old, "to": target, "actor": actor, "reason": reason, "at": now()})
151
+ self._refresh_hreflang(item["canonicalVariantId"])
123
152
  self._event("variant." + target, variant_id, actor, reason); self._save(); return item
124
153
 
125
154
  def plan_slug_change(self, variant_id: str, new_slug: str, actor: str) -> dict:
@@ -128,17 +157,29 @@ class ServiceVariantStore:
128
157
  raise ValueError("published URL changes require owner approval")
129
158
  if not new_slug.startswith(item["variantType"] + "/") or not SLUG_WORD.fullmatch(new_slug.split("/", 1)[-1]):
130
159
  raise ValueError("new slug violates variant grammar")
131
- if any(v["slug"] == new_slug and v["locale"] == item["locale"] for v in self.data["variants"] if v["id"] != variant_id):
132
- raise ValueError("new slug collides with another variant")
133
- redirect = {"from": item["canonicalUrl"], "to": "/services/" + new_slug + "/", "ownerApproval": actor, "status": "planned"}
160
+ new_url = variant_canonical_url(new_slug, item["locale"], item["market"])
161
+ if any(v.get("canonicalUrl") == new_url for v in self.data["variants"] if v["id"] != variant_id):
162
+ raise ValueError("new market, locale and slug route collides with another variant")
163
+ redirect = {"from": item["canonicalUrl"], "to": new_url, "ownerApproval": actor, "status": "planned"}
134
164
  self.data["redirects"].append(redirect); self._save(); return redirect
135
165
 
136
166
  def validate(self, variant_id: str, source_text: str | None = None, render_report: dict | None = None) -> dict:
137
167
  item = self._find(variant_id); errors = []
168
+ expected_url = variant_canonical_url(item["slug"], item["locale"], item["market"])
169
+ if item.get("canonicalUrl") != expected_url:
170
+ errors.append("canonical URL must include market, locale and variant slug")
138
171
  if not item["clusterLinks"] and item["variantType"] != "location": errors.append("cluster links required")
139
172
  if source_text is not None and similarity(source_text, item["title"] + " " + " ".join(str(f["value"]) for f in item["facts"])) < 0.1:
140
173
  errors.append("variant content has no meaningful similarity to source")
141
174
  if item["canonicalVariantId"] != item["id"] and item["canonicalVariantId"] not in {v["id"] for v in self.data["variants"]}: errors.append("canonical relation missing")
175
+ if item.get("status") == "published":
176
+ expected_hreflang = {
177
+ peer["locale"]: peer["canonicalUrl"]
178
+ for peer in self._cluster(item["canonicalVariantId"])
179
+ if peer.get("status") == "published"
180
+ }
181
+ if item.get("hreflang") != expected_hreflang:
182
+ errors.append("published hreflang links must be reciprocal published counterparts")
142
183
  if render_report is not None:
143
184
  if render_report.get("schemaVersion") != "maggie-service-variant-render.v1" or render_report.get("passed") is not True:
144
185
  errors.append("render report is not passed")
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@topy-ai/maggie",
3
- "version": "0.7.17",
3
+ "version": "0.7.19",
4
4
  "description": "Install and manage Maggie Skills for AI coding agents",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -25,8 +25,10 @@ when a title changes.
25
25
  "title": "Signature Scalp Ritual",
26
26
  "description": "Provider-supplied factual description.",
27
27
  "category": {"level1": "Head Spa", "level2": "Scalp Treatments"},
28
- "variants": [{
28
+ "variants": [{
29
29
  "id": "service-123:60",
30
+ "idSource": "provider",
31
+ "variantSource": "native",
30
32
  "title": "60 minutes",
31
33
  "durationMinutes": 60,
32
34
  "price": {"amountMinor": 9500, "currency": "GBP", "display": "£95.00"}
@@ -147,3 +149,36 @@ Provider adapters may add `raw` evidence in a private local snapshot, but
147
149
  public page generation consumes only the canonical fields above. A sync must
148
150
  preserve removed records as `archived` with `removedAt` and must output a
149
151
  change report.
152
+
153
+ ## Provider variant capability matrix
154
+
155
+ Before service pages are published, each provider declares its variant
156
+ behavior in a project-owned JSON file. The declaration is validated against a
157
+ sanitized fixture and emits `maggie-provider-variant-capabilities.v1`:
158
+
159
+ ```json
160
+ {
161
+ "schemaVersion": "maggie-provider-capabilities.v1",
162
+ "providers": [{
163
+ "provider": "fresha",
164
+ "variantMode": "native",
165
+ "priceSemantics": "minor-unit-and-display",
166
+ "stableIdConfidence": "high"
167
+ }]
168
+ }
169
+ ```
170
+
171
+ `variantMode` is one of `native`, `derived-offers`, `single-fallback`, or
172
+ `blocked`. `priceSemantics` records whether structured minor units and display
173
+ prices are available. `stableIdConfidence` must be justified by provider or
174
+ derived ID evidence. The audit records service/variant counts, observed source
175
+ types, fixture name and hash, and validation errors without copying fixture
176
+ rows or provider credentials. Run it with:
177
+
178
+ ```bash
179
+ maggie service capability-audit \
180
+ --project . \
181
+ --catalogue .maggie/booking/services.json \
182
+ --capabilities-file .maggie/booking/provider-capabilities.json \
183
+ --fixture .maggie/booking/fixtures/provider.json
184
+ ```