@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.
- package/README.md +49 -5
- package/README.zh-TW.md +12 -2
- package/bin/maggie.js +10 -0
- package/bundled-contracts/maggiedash/README.md +19 -0
- package/bundled-contracts/maggiedash/activity-log-v1.json +27 -0
- package/bundled-contracts/maggiedash/dashboard-runtime-v1.json +33 -0
- package/bundled-contracts/maggiedash/navigation-v1.json +29 -0
- package/bundled-references/universal-booking-adapter.md +36 -1
- package/bundled-skills/maggie-dash/SKILL.md +22 -1
- package/bundled-skills/maggie-deployment/SKILL.md +9 -1
- package/bundled-skills/maggie-feedback/SKILL.md +5 -0
- package/bundled-skills/maggie-qa-workflow/SKILL.md +11 -0
- package/bundled-skills/maggie-service-booking/SKILL.md +19 -1
- package/bundled-tools/clis/maggie_dash.py +26 -5
- package/bundled-tools/clis/maggie_docs.py +151 -0
- package/bundled-tools/clis/maggie_feedback.py +17 -5
- package/bundled-tools/clis/maggie_qa_workflow.py +2 -0
- package/bundled-tools/clis/maggie_release.py +88 -2
- package/bundled-tools/clis/maggie_service_booking.py +42 -3
- package/bundled-tools/runtime/booking_capabilities.py +137 -0
- package/bundled-tools/runtime/maggie_api_contract.py +51 -0
- package/bundled-tools/runtime/maggie_dash_runtime.py +58 -0
- package/bundled-tools/runtime/maggie_schema_audit.py +72 -0
- package/bundled-tools/runtime/service_variants.py +48 -7
- package/package.json +1 -1
- package/references/universal-booking-adapter.md +36 -1
|
@@ -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":
|
|
84
|
-
"hreflang": {locale:
|
|
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
|
-
|
|
132
|
-
|
|
133
|
-
|
|
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
|
@@ -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
|
-
|
|
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
|
+
```
|