@topy-ai/maggie 0.7.26 → 0.7.28
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 +50 -4
- package/README.zh-TW.md +4 -2
- package/bin/maggie.js +17 -5
- package/bundled-contracts/maggie-deployment/runtime-parity-v1.schema.json +20 -2
- package/bundled-contracts/maggie-seo/model-policy-v1.schema.json +17 -0
- package/bundled-contracts/maggie-service-booking/resolver-v1.schema.json +17 -0
- package/bundled-contracts/maggie-service-booking/worker-health-v1.schema.json +17 -0
- package/bundled-contracts/maggiedash/README.md +5 -0
- package/bundled-contracts/maggiedash/migration-preflight-v1.schema.json +16 -0
- package/bundled-references/blog-translation-ingestion.md +7 -0
- package/bundled-references/maggiedash-integration.md +19 -0
- package/bundled-skills/maggie-blog-bootstrap/SKILL.md +21 -0
- package/bundled-skills/maggie-dash/SKILL.md +34 -0
- package/bundled-skills/maggie-deployment/SKILL.md +22 -7
- package/bundled-skills/maggie-deployment/references/vps.md +12 -2
- package/bundled-skills/maggie-feedback/SKILL.md +13 -3
- package/bundled-skills/maggie-marketplace/SKILL.md +20 -1
- package/bundled-skills/maggie-seo-geo/SKILL.md +11 -0
- package/bundled-skills/maggie-service-booking/SKILL.md +19 -0
- package/bundled-tools/clis/maggie.py +29 -1
- package/bundled-tools/clis/maggie_dash.py +108 -0
- package/bundled-tools/clis/maggie_deployment.py +43 -4
- package/bundled-tools/clis/maggie_deployment_parity.py +42 -0
- package/bundled-tools/clis/maggie_feedback.py +108 -0
- package/bundled-tools/clis/maggie_localization.py +18 -1
- package/bundled-tools/clis/maggie_marketplace.py +58 -4
- package/bundled-tools/clis/maggie_migration.py +43 -0
- package/bundled-tools/clis/maggie_service_booking.py +84 -1
- package/bundled-tools/integrations/maggie-api-pull.md +8 -4
- package/bundled-tools/runtime/maggie_dash_store.py +20 -0
- package/package.json +1 -1
- package/references/blog-translation-ingestion.md +7 -0
- package/references/maggiedash-integration.md +19 -0
|
@@ -25,7 +25,7 @@ DEFAULT_SAMPLE_ID = "medical-spa-landing-001"
|
|
|
25
25
|
PUBLISHED_STATUSES = {"published", "ready"}
|
|
26
26
|
PHOSPHOR_BRAND_ICONS = {"ph-instagram-logo", "ph-facebook-logo", "ph-x-logo", "ph-linkedin-logo", "ph-youtube-logo", "ph-tiktok-logo"}
|
|
27
27
|
PHOSPHOR_CDN = "https://cdn.jsdelivr.net/npm/@phosphor-icons/web@2.1.1/src/regular/style.css"
|
|
28
|
-
JOB_PHASES = ("CREATED", "IMPORTED", "WAITING_FOR_ASSETS", "COMPLETED", "VALIDATED", "READY", "FAILED")
|
|
28
|
+
JOB_PHASES = ("CREATED", "IMPORTED", "WAITING_FOR_ASSETS", "COMPLETED", "VALIDATED", "READY", "STALE", "FAILED")
|
|
29
29
|
|
|
30
30
|
|
|
31
31
|
class MetadataParser(HTMLParser):
|
|
@@ -1738,8 +1738,16 @@ def run_marketplace_job(args: argparse.Namespace) -> int:
|
|
|
1738
1738
|
if existing_path.exists() and not args.force:
|
|
1739
1739
|
existing = load_job(root, args.id)
|
|
1740
1740
|
if existing.get("phase") == "READY":
|
|
1741
|
-
|
|
1742
|
-
|
|
1741
|
+
destination = template_path(root, args.id)
|
|
1742
|
+
# READY is a claim, not a permanent exemption from newer
|
|
1743
|
+
# validators. Re-check it on every reuse and make a stale package
|
|
1744
|
+
# explicitly resumable when the contract has moved on.
|
|
1745
|
+
if validate_template(destination, args.sample.resolve() if args.sample else None) == 0:
|
|
1746
|
+
print(f"marketplace job already ready: {args.id}")
|
|
1747
|
+
return 0
|
|
1748
|
+
set_job_phase(root, existing, "STALE", staleReason="current validator rejected the recorded READY package")
|
|
1749
|
+
print(f"STALE: marketplace job {args.id} must be resumed or rebuilt")
|
|
1750
|
+
return 1
|
|
1743
1751
|
args = args_from_job(existing)
|
|
1744
1752
|
job = load_job(root, args.id) if existing_path.exists() else {
|
|
1745
1753
|
"id": args.id,
|
|
@@ -1823,7 +1831,7 @@ def resume_marketplace_job(args: argparse.Namespace) -> int:
|
|
|
1823
1831
|
print(f"marketplace job already ready: {args.job_id}")
|
|
1824
1832
|
return 0
|
|
1825
1833
|
resumed = args_from_job(job)
|
|
1826
|
-
if job.get("phase")
|
|
1834
|
+
if job.get("phase") in {"FAILED", "STALE"}:
|
|
1827
1835
|
destination = template_path(root, args.job_id)
|
|
1828
1836
|
# A post-import failure must resume from the completion checkpoint;
|
|
1829
1837
|
# importing again would erase generated assets and task evidence.
|
|
@@ -2053,6 +2061,47 @@ def apply_template(args: argparse.Namespace) -> int:
|
|
|
2053
2061
|
return 0
|
|
2054
2062
|
|
|
2055
2063
|
|
|
2064
|
+
def enrichment_report(args: argparse.Namespace) -> int:
|
|
2065
|
+
"""Normalize privacy-safe enrichment evidence for a marketplace candidate."""
|
|
2066
|
+
try:
|
|
2067
|
+
payload = read_json(args.input.resolve())
|
|
2068
|
+
except (OSError, ValueError, json.JSONDecodeError) as error:
|
|
2069
|
+
print(json.dumps({"status": "failed", "errors": [f"cannot read enrichment evidence: {error}"]})); return 1
|
|
2070
|
+
errors = []
|
|
2071
|
+
if payload.get("schemaVersion") != "maggie-marketplace-enrichment-input.v1": errors.append("schemaVersion is unsupported")
|
|
2072
|
+
candidate = payload.get("candidate") if isinstance(payload.get("candidate"), dict) else {}
|
|
2073
|
+
candidate_id = str(candidate.get("candidateId") or "")
|
|
2074
|
+
if not ID_PATTERN.fullmatch(candidate_id): errors.append("candidate.candidateId must be marketplace-safe")
|
|
2075
|
+
crawl = payload.get("crawl") if isinstance(payload.get("crawl"), dict) else {}
|
|
2076
|
+
audit = payload.get("audit") if isinstance(payload.get("audit"), dict) else {}
|
|
2077
|
+
snapshot = payload.get("snapshot") if isinstance(payload.get("snapshot"), dict) else {}
|
|
2078
|
+
for field, item in (("crawl", crawl), ("audit", audit), ("snapshot", snapshot)):
|
|
2079
|
+
if not item: errors.append(f"{field} evidence is required")
|
|
2080
|
+
if item.get("status") not in {"passed", "failed", "not-found", "blocked", "unavailable"}: errors.append(f"{field}.status is unsupported")
|
|
2081
|
+
if item.get("error") and not re.fullmatch(r"[a-z0-9][a-z0-9._:-]{0,119}", str(item["error"])): errors.append(f"{field}.error must be a safe error code")
|
|
2082
|
+
fields = payload.get("rawAuditFields", {})
|
|
2083
|
+
if not isinstance(fields, dict): errors.append("rawAuditFields must be an object")
|
|
2084
|
+
safe_fields = {}
|
|
2085
|
+
for key, value in (fields.items() if isinstance(fields, dict) else []):
|
|
2086
|
+
if re.fullmatch(r"[A-Za-z][A-Za-z0-9_.:-]{0,79}", str(key)) and not re.search(r"path|secret|token|password|cookie|header|body|html", str(key), re.I) and isinstance(value, (str, int, float, bool)) and not (isinstance(value, str) and re.search(r"(?:^|/)(?:home|Users|tmp|var|private)/", value)):
|
|
2087
|
+
safe_fields[str(key)] = str(value)[:240] if isinstance(value, str) else value
|
|
2088
|
+
backfill = payload.get("seoBackfill") if isinstance(payload.get("seoBackfill"), dict) else {}
|
|
2089
|
+
report = {
|
|
2090
|
+
"schemaVersion": "maggie-marketplace-enrichment.v1", "candidateId": candidate_id,
|
|
2091
|
+
"status": "passed" if not errors else "failed", "passed": not errors, "errors": errors,
|
|
2092
|
+
"candidate": {"source": str(candidate.get("source") or "")[:120], "market": str(candidate.get("market") or "global")[:20], "locale": str(candidate.get("locale") or "en")[:20]},
|
|
2093
|
+
"crawl": {"status": crawl.get("status"), "pages": crawl.get("pages", 0) if isinstance(crawl.get("pages", 0), int) else 0, "error": crawl.get("error")},
|
|
2094
|
+
"audit": {"status": audit.get("status"), "fields": safe_fields, "error": audit.get("error")},
|
|
2095
|
+
"snapshot": {"status": snapshot.get("status"), "revision": str(snapshot.get("revision") or "")[:120], "error": snapshot.get("error")},
|
|
2096
|
+
"seoBackfill": {"status": backfill.get("status"), "updated": backfill.get("updated", 0) if isinstance(backfill.get("updated", 0), int) else 0, "error": backfill.get("error")},
|
|
2097
|
+
"retry": {"allowed": bool(errors), "reason": ";".join(errors)[:240] if errors else None},
|
|
2098
|
+
"privacy": {"rawHtmlIncluded": False, "responseBodiesIncluded": False, "localPathsIncluded": False, "secretsRedacted": True},
|
|
2099
|
+
}
|
|
2100
|
+
output = args.output.resolve() if args.output else args.project.resolve() / "docs" / "marketplace-enrichment.json"
|
|
2101
|
+
output.parent.mkdir(parents=True, exist_ok=True); output.write_text(json.dumps(report, indent=2, ensure_ascii=False) + "\n", encoding="utf-8")
|
|
2102
|
+
print(json.dumps({"status": report["status"], "candidateId": candidate_id, "report": str(output), "retry": report["retry"]}, ensure_ascii=False)); return 0 if not errors else 1
|
|
2103
|
+
|
|
2104
|
+
|
|
2056
2105
|
def main() -> int:
|
|
2057
2106
|
parser = argparse.ArgumentParser(description=__doc__)
|
|
2058
2107
|
parser.add_argument("--marketplace", type=Path, default=Path("marketplace"))
|
|
@@ -2122,6 +2171,11 @@ def main() -> int:
|
|
|
2122
2171
|
apply.add_argument("--project", type=Path, default=Path.cwd())
|
|
2123
2172
|
apply.add_argument("--allow-draft", action="store_true")
|
|
2124
2173
|
apply.set_defaults(func=apply_template)
|
|
2174
|
+
enrichment = sub.add_parser("enrichment-report", help="normalize safe candidate crawl and audit evidence")
|
|
2175
|
+
enrichment.add_argument("--project", type=Path, default=Path.cwd())
|
|
2176
|
+
enrichment.add_argument("--input", type=Path, required=True)
|
|
2177
|
+
enrichment.add_argument("--output", type=Path)
|
|
2178
|
+
enrichment.set_defaults(func=enrichment_report)
|
|
2125
2179
|
args = parser.parse_args()
|
|
2126
2180
|
try:
|
|
2127
2181
|
return int(args.func(args) or 0)
|
|
@@ -56,6 +56,8 @@ def validate_project(project: Path, environment: str) -> dict:
|
|
|
56
56
|
|
|
57
57
|
|
|
58
58
|
IDENTITY_SCHEMA = "maggie-database-identity.v1"
|
|
59
|
+
PREFLIGHT_SCHEMA = "maggie-migration-preflight.v1"
|
|
60
|
+
SAFE_NAME = re.compile(r"^[A-Za-z][A-Za-z0-9_.:-]{0,119}$")
|
|
59
61
|
|
|
60
62
|
|
|
61
63
|
def _identity_fingerprint(identity: dict) -> str:
|
|
@@ -119,7 +121,48 @@ def identity_main(argv: list[str]) -> int:
|
|
|
119
121
|
return 0 if result["passed"] else 1
|
|
120
122
|
|
|
121
123
|
|
|
124
|
+
def preflight_main(argv: list[str]) -> int:
|
|
125
|
+
parser = argparse.ArgumentParser(description="Validate secret-free migration privilege, column, and constraint evidence.")
|
|
126
|
+
parser.add_argument("--evidence", required=True)
|
|
127
|
+
parser.add_argument("--output")
|
|
128
|
+
args = parser.parse_args(argv)
|
|
129
|
+
errors: list[str] = []
|
|
130
|
+
try:
|
|
131
|
+
payload = json.loads(Path(args.evidence).read_text(encoding="utf-8"))
|
|
132
|
+
except (OSError, json.JSONDecodeError) as error:
|
|
133
|
+
payload = {}; errors.append(f"cannot read migration preflight evidence: {error}")
|
|
134
|
+
if not isinstance(payload, dict): payload = {}; errors.append("preflight evidence must be an object")
|
|
135
|
+
if payload.get("schemaVersion") != PREFLIGHT_SCHEMA: errors.append(f"schemaVersion must be {PREFLIGHT_SCHEMA}")
|
|
136
|
+
if payload.get("environment") not in {"development", "staging", "production"}: errors.append("environment is invalid")
|
|
137
|
+
privileges = payload.get("privileges") if isinstance(payload.get("privileges"), dict) else {}
|
|
138
|
+
for field in ("canCreateTable", "canAlterTable", "canCreateIndex", "passed"):
|
|
139
|
+
if privileges.get(field) is not True: errors.append(f"privileges.{field} must be true")
|
|
140
|
+
columns = payload.get("requiredColumns")
|
|
141
|
+
if not isinstance(columns, list) or not columns: errors.append("requiredColumns must not be empty")
|
|
142
|
+
for index, item in enumerate(columns if isinstance(columns, list) else []):
|
|
143
|
+
if not isinstance(item, dict) or not SAFE_NAME.fullmatch(str(item.get("table") or "")) or not SAFE_NAME.fullmatch(str(item.get("column") or "")) or item.get("present") is not True:
|
|
144
|
+
errors.append(f"requiredColumns[{index}] must identify a present table column")
|
|
145
|
+
constraints = payload.get("uniqueConstraints")
|
|
146
|
+
if not isinstance(constraints, list) or not constraints: errors.append("uniqueConstraints must not be empty")
|
|
147
|
+
for index, item in enumerate(constraints if isinstance(constraints, list) else []):
|
|
148
|
+
if not isinstance(item, dict) or not SAFE_NAME.fullmatch(str(item.get("table") or "")) or not isinstance(item.get("columns"), list) or not item["columns"] or any(not SAFE_NAME.fullmatch(str(value)) for value in item["columns"]) or item.get("present") is not True:
|
|
149
|
+
errors.append(f"uniqueConstraints[{index}] must identify a present unique constraint")
|
|
150
|
+
idempotency = payload.get("idempotency") if isinstance(payload.get("idempotency"), dict) else {}
|
|
151
|
+
if idempotency.get("safe") is not True or idempotency.get("onConflictHandled") is not True: errors.append("idempotency must be safe and handle conflicts")
|
|
152
|
+
migration = payload.get("migration") if isinstance(payload.get("migration"), dict) else {}
|
|
153
|
+
for field in ("currentVersion", "targetVersion"):
|
|
154
|
+
if not SAFE_NAME.fullmatch(str(migration.get(field) or "")): errors.append(f"migration.{field} must be a safe identifier")
|
|
155
|
+
if migration.get("currentVersion") == migration.get("targetVersion"): errors.append("migration targetVersion must advance")
|
|
156
|
+
if migration.get("passed") is not True: errors.append("migration evidence did not pass")
|
|
157
|
+
result = {"schemaVersion": PREFLIGHT_SCHEMA, "environment": payload.get("environment"), "status": "passed" if not errors else "failed", "passed": not errors, "errors": errors, "checks": {"privileges": True, "requiredColumns": True, "uniqueConstraints": True, "idempotency": True, "migration": True}, "mutation": "not executed"}
|
|
158
|
+
if args.output:
|
|
159
|
+
output = Path(args.output); output.parent.mkdir(parents=True, exist_ok=True); output.write_text(json.dumps(result, indent=2) + "\n", encoding="utf-8")
|
|
160
|
+
print(json.dumps(result, indent=2)); return 0 if not errors else 1
|
|
161
|
+
|
|
162
|
+
|
|
122
163
|
def main() -> int:
|
|
164
|
+
if sys.argv[1:2] == ["preflight"]:
|
|
165
|
+
return preflight_main(sys.argv[2:])
|
|
123
166
|
if sys.argv[1:2] == ["identity"]:
|
|
124
167
|
return identity_main(sys.argv[2:])
|
|
125
168
|
parser = argparse.ArgumentParser(description=__doc__)
|
|
@@ -7,7 +7,7 @@ import xml.etree.ElementTree as ET
|
|
|
7
7
|
from datetime import datetime, timezone
|
|
8
8
|
from html.parser import HTMLParser
|
|
9
9
|
from pathlib import Path
|
|
10
|
-
from urllib.parse import quote, urljoin, urlsplit
|
|
10
|
+
from urllib.parse import quote, urljoin, urlsplit, urlparse
|
|
11
11
|
from urllib.request import Request, urlopen
|
|
12
12
|
|
|
13
13
|
sys.path.insert(0, str(Path(__file__).resolve().parents[1] / "runtime"))
|
|
@@ -1148,6 +1148,85 @@ def cmd_variant_render_audit(args):
|
|
|
1148
1148
|
result={"schemaVersion":"1.0","generatedAt":NOW(),"environment":args.environment,"status":"passed" if not errors else "failed","variants":results,"errors":errors,"passed":not errors}
|
|
1149
1149
|
out=project/"docs"/"service-variant-render-audit.json"; out.parent.mkdir(parents=True,exist_ok=True); out.write_text(json.dumps(result,indent=2)+"\n",encoding="utf-8")
|
|
1150
1150
|
print(json.dumps({**result,"report":str(out)},indent=2)); return 0 if not errors else 1
|
|
1151
|
+
SAFE_WORKER_ID = re.compile(r"^[A-Za-z0-9][A-Za-z0-9._:-]{0,119}$")
|
|
1152
|
+
SAFE_EVIDENCE_ID = re.compile(r"^[A-Za-z0-9][A-Za-z0-9._:/=-]{0,159}$")
|
|
1153
|
+
RESOLVER_OUTCOMES = {"resolved", "resolved-no-menu", "not-found", "blocked", "timeout", "unavailable"}
|
|
1154
|
+
|
|
1155
|
+
|
|
1156
|
+
def cmd_worker_health(args):
|
|
1157
|
+
"""Validate bounded queue, lease, retry, timeout, and cancellation evidence."""
|
|
1158
|
+
try:
|
|
1159
|
+
payload = json.loads(Path(args.evidence).resolve().read_text(encoding="utf-8"))
|
|
1160
|
+
except (OSError, ValueError, json.JSONDecodeError) as exc:
|
|
1161
|
+
print(json.dumps({"status": "failed", "errors": [f"cannot read worker evidence: {exc}"]}, indent=2))
|
|
1162
|
+
return 1
|
|
1163
|
+
errors = []
|
|
1164
|
+
if payload.get("schemaVersion") != "maggie-booking-worker-health-input.v1": errors.append("schemaVersion is unsupported")
|
|
1165
|
+
if payload.get("environment") not in {"development", "staging", "production"}: errors.append("environment is invalid")
|
|
1166
|
+
worker_id = str(payload.get("workerId") or "")
|
|
1167
|
+
if not SAFE_WORKER_ID.fullmatch(worker_id): errors.append("workerId must be a safe identifier")
|
|
1168
|
+
queue = payload.get("queue") if isinstance(payload.get("queue"), dict) else {}
|
|
1169
|
+
for field in ("maxDepth", "maxConcurrency"):
|
|
1170
|
+
if not isinstance(queue.get(field), int) or isinstance(queue.get(field), bool) or queue[field] < 1: errors.append(f"queue.{field} must be a positive integer")
|
|
1171
|
+
if queue.get("bounded") is not True: errors.append("queue must be bounded")
|
|
1172
|
+
leases = payload.get("leases") if isinstance(payload.get("leases"), dict) else {}
|
|
1173
|
+
if leases.get("enabled") is not True or not isinstance(leases.get("ttlSeconds"), int) or leases.get("ttlSeconds", 0) < 1: errors.append("leases must have a positive TTL")
|
|
1174
|
+
if leases.get("staleReclaimed") is not True: errors.append("stale leases must be reclaimed")
|
|
1175
|
+
timeouts = payload.get("timeouts") if isinstance(payload.get("timeouts"), dict) else {}
|
|
1176
|
+
for field in ("jobSeconds", "providerSeconds"):
|
|
1177
|
+
if not isinstance(timeouts.get(field), int) or isinstance(timeouts.get(field), bool) or timeouts[field] < 1: errors.append(f"timeouts.{field} must be positive")
|
|
1178
|
+
retry = payload.get("retry") if isinstance(payload.get("retry"), dict) else {}
|
|
1179
|
+
if not isinstance(retry.get("maxAttempts"), int) or isinstance(retry.get("maxAttempts"), bool) or retry["maxAttempts"] < 0: errors.append("retry.maxAttempts must be non-negative")
|
|
1180
|
+
if retry.get("backoff") not in {"fixed", "exponential", "exponential-jitter"}: errors.append("retry.backoff must be an approved strategy")
|
|
1181
|
+
cancellation = payload.get("cancellation") if isinstance(payload.get("cancellation"), dict) else {}
|
|
1182
|
+
if cancellation.get("supported") is not True or cancellation.get("cleanupVerified") is not True: errors.append("cancellation cleanup is not verified")
|
|
1183
|
+
health = payload.get("health") if isinstance(payload.get("health"), dict) else {}
|
|
1184
|
+
if not isinstance(health.get("path"), str) or not health["path"].startswith("/") or health.get("passed") is not True: errors.append("health endpoint did not pass")
|
|
1185
|
+
if payload.get("orphanedJobs", 0) != 0: errors.append("orphanedJobs must be zero")
|
|
1186
|
+
report = {"schemaVersion": "maggie-booking-worker-health.v1", "environment": payload.get("environment"), "workerId": worker_id, "status": "passed" if not errors else "failed", "passed": not errors, "errors": errors, "checks": {"boundedQueue": True, "leases": True, "timeouts": True, "retryBackoff": True, "cancellationCleanup": True, "healthEndpoint": True}}
|
|
1187
|
+
project = root(args)
|
|
1188
|
+
output = Path(args.output) if args.output else project / "docs/booking-worker-health.json"
|
|
1189
|
+
if not output.is_absolute(): output = project / output
|
|
1190
|
+
output = output.resolve()
|
|
1191
|
+
output.parent.mkdir(parents=True, exist_ok=True); output.write_text(json.dumps(report, indent=2) + "\n", encoding="utf-8")
|
|
1192
|
+
print(json.dumps({"status": report["status"], "report": str(output), "errors": errors}, indent=2)); return 0 if not errors else 1
|
|
1193
|
+
|
|
1194
|
+
|
|
1195
|
+
def cmd_resolver_audit(args):
|
|
1196
|
+
"""Classify booking URL outcomes using safe canonical URL evidence."""
|
|
1197
|
+
try:
|
|
1198
|
+
payload = json.loads(Path(args.evidence).resolve().read_text(encoding="utf-8"))
|
|
1199
|
+
except (OSError, ValueError, json.JSONDecodeError) as exc:
|
|
1200
|
+
print(json.dumps({"status": "failed", "errors": [f"cannot read resolver evidence: {exc}"]}, indent=2)); return 1
|
|
1201
|
+
errors = []
|
|
1202
|
+
if payload.get("schemaVersion") != "maggie-booking-resolver-input.v1": errors.append("schemaVersion is unsupported")
|
|
1203
|
+
observations = payload.get("observations")
|
|
1204
|
+
if not isinstance(observations, list) or not observations or len(observations) > 200: errors.append("observations must contain 1–200 items")
|
|
1205
|
+
safe = []
|
|
1206
|
+
for index, item in enumerate(observations if isinstance(observations, list) else []):
|
|
1207
|
+
prefix = f"observations[{index}]"
|
|
1208
|
+
if not isinstance(item, dict): errors.append(f"{prefix} must be an object"); continue
|
|
1209
|
+
service_id = str(item.get("serviceId") or ""); outcome = item.get("outcome")
|
|
1210
|
+
if not SAFE_WORKER_ID.fullmatch(service_id): errors.append(f"{prefix}.serviceId must be a safe identifier")
|
|
1211
|
+
if outcome not in RESOLVER_OUTCOMES: errors.append(f"{prefix}.outcome is unsupported")
|
|
1212
|
+
canonical = item.get("canonicalUrl")
|
|
1213
|
+
if outcome in {"resolved", "resolved-no-menu"}:
|
|
1214
|
+
parsed = urlparse(str(canonical or ""))
|
|
1215
|
+
if parsed.scheme != "https" or not parsed.netloc or parsed.username or parsed.password: errors.append(f"{prefix}.canonicalUrl must be a public HTTPS URL")
|
|
1216
|
+
elif canonical: errors.append(f"{prefix}.canonicalUrl is only allowed for resolved outcomes")
|
|
1217
|
+
evidence = item.get("evidenceIds")
|
|
1218
|
+
if not isinstance(evidence, list) or not evidence or any(not SAFE_EVIDENCE_ID.fullmatch(str(value)) for value in evidence[:20]): errors.append(f"{prefix}.evidenceIds must contain safe identifiers")
|
|
1219
|
+
if not isinstance(item.get("durationMs"), int) or isinstance(item.get("durationMs"), bool) or item["durationMs"] < 0: errors.append(f"{prefix}.durationMs must be non-negative")
|
|
1220
|
+
safe.append({"serviceId": service_id, "outcome": outcome, "canonicalUrl": canonical if outcome in {"resolved", "resolved-no-menu"} else None, "evidenceIds": [str(value) for value in evidence[:20]] if isinstance(evidence, list) else [], "durationMs": item.get("durationMs")})
|
|
1221
|
+
counts = {outcome: sum(item.get("outcome") == outcome for item in safe) for outcome in sorted(RESOLVER_OUTCOMES)}
|
|
1222
|
+
report = {"schemaVersion": "maggie-booking-resolver.v1", "status": "passed" if not errors else "failed", "passed": not errors, "errors": errors, "counts": counts, "observations": safe, "privacy": {"rawResponsesIncluded": False, "credentialsIncluded": False}}
|
|
1223
|
+
project = root(args)
|
|
1224
|
+
output = Path(args.output) if args.output else project / "docs/booking-resolver-audit.json"
|
|
1225
|
+
if not output.is_absolute(): output = project / output
|
|
1226
|
+
output = output.resolve(); output.parent.mkdir(parents=True, exist_ok=True); output.write_text(json.dumps(report, indent=2) + "\n", encoding="utf-8")
|
|
1227
|
+
print(json.dumps({"status": report["status"], "report": str(output), "counts": counts, "errors": errors}, indent=2)); return 0 if not errors else 1
|
|
1228
|
+
|
|
1229
|
+
|
|
1151
1230
|
def main():
|
|
1152
1231
|
p=argparse.ArgumentParser(); sub=p.add_subparsers(dest="command",required=True)
|
|
1153
1232
|
for name in ("import","sync","run"):
|
|
@@ -1173,6 +1252,8 @@ def main():
|
|
|
1173
1252
|
q=sub.add_parser("variant-audit"); q.add_argument("--project",default="."); q.add_argument("--variants",required=True,help="draft service-page variant JSON"); q.add_argument("--as-of",help="ISO timestamp for expiry checks"); q.add_argument("--environment",choices=("development","staging","production"),default="staging")
|
|
1174
1253
|
q=sub.add_parser("category-visual-audit"); q.add_argument("--project",default="."); q.add_argument("--screenshot-dir",required=True); q.add_argument("--min-bytes",type=int,default=5000); q.add_argument("--environment",choices=("development","staging","production"),default="staging")
|
|
1175
1254
|
q=sub.add_parser("variant-render-audit"); q.add_argument("--project",default="."); q.add_argument("--variants",required=True); q.add_argument("--rendered-dir",required=True); q.add_argument("--environment",choices=("development","staging","production"),default="staging")
|
|
1255
|
+
q=sub.add_parser("worker-health"); q.add_argument("--project",default="."); q.add_argument("--evidence",required=True); q.add_argument("--output")
|
|
1256
|
+
q=sub.add_parser("resolver-audit"); q.add_argument("--project",default="."); q.add_argument("--evidence",required=True); q.add_argument("--output")
|
|
1176
1257
|
for name in ("validate","inspect","status"):
|
|
1177
1258
|
q=sub.add_parser(name); q.add_argument("--project",default=".")
|
|
1178
1259
|
a=p.parse_args(); project=root(a)
|
|
@@ -1198,6 +1279,8 @@ def main():
|
|
|
1198
1279
|
if a.command=="variant-audit": return cmd_variant_audit(a)
|
|
1199
1280
|
if a.command=="category-visual-audit": return cmd_category_visual_audit(a)
|
|
1200
1281
|
if a.command=="variant-render-audit": return cmd_variant_render_audit(a)
|
|
1282
|
+
if a.command=="worker-health": return cmd_worker_health(a)
|
|
1283
|
+
if a.command=="resolver-audit": return cmd_resolver_audit(a)
|
|
1201
1284
|
if a.command=="inspect": return cmd_inspect(a)
|
|
1202
1285
|
if a.command=="status": return cmd_status(a)
|
|
1203
1286
|
data=load(project) if path(project).exists() else {}; errors=validate(data); print(json.dumps({"status":"passed" if not errors else "failed","errors":errors,"services":len(data.get("services",[]))},indent=2)); return 0 if not errors else 1
|
|
@@ -23,8 +23,11 @@ feedback payloads, or public repositories.
|
|
|
23
23
|
|
|
24
24
|
`/history` rejects invalid pagination with `422`; do not request more than 50
|
|
25
25
|
records per page. Empty `/posts` or `/updates` responses are normal and mean
|
|
26
|
-
there is currently no queued item.
|
|
27
|
-
|
|
26
|
+
there is currently no queued item. Quota responses must expose the effective
|
|
27
|
+
delivery allowance: when `quota_exhausted` is true, report the relevant daily
|
|
28
|
+
remaining value as zero and preserve a bounded provider `message`/error
|
|
29
|
+
summary. The public OpenAPI contract does not include `/whoami`; resolve
|
|
30
|
+
configuration from the API key and project context instead.
|
|
28
31
|
|
|
29
32
|
Returned content may expose provider-specific fields such as `content`,
|
|
30
33
|
`content_markdown`, `content_html`, `content_portable_text`, `body`, or `html`.
|
|
@@ -56,8 +59,9 @@ python3 tools/clis/maggie.py api lifecycle --project . \
|
|
|
56
59
|
The first command is always dry-run. The executing command persists key-scoped
|
|
57
60
|
context, remote IDs, versions, slugs, canonicals, and hashes under
|
|
58
61
|
`.maggie/ai-cmo-lifecycle.json`. Raw provider responses are not copied into
|
|
59
|
-
the checkpoint
|
|
60
|
-
|
|
62
|
+
the checkpoint; only bounded messages, effective quota fields, and safe scalar
|
|
63
|
+
outcomes are retained. Pulled content is projected as draft-only state; no
|
|
64
|
+
lifecycle step publishes local content.
|
|
61
65
|
|
|
62
66
|
To prepare and explicitly import the projection into MaggieDash drafts:
|
|
63
67
|
|
|
@@ -108,6 +108,26 @@ class MaggieDashStore:
|
|
|
108
108
|
|
|
109
109
|
def init_project(self, project_id: str, name: str, adapter: str = "maggiedash-sqlite", locale: str = "en-GB") -> dict[str, Any]:
|
|
110
110
|
timestamp = now()
|
|
111
|
+
# The default CLI identity is a provisional row. Once the operator
|
|
112
|
+
# supplies a real project id, converge that row instead of leaving an
|
|
113
|
+
# orphan project behind in the local store.
|
|
114
|
+
if project_id != "local-project":
|
|
115
|
+
has_target = self.connection.execute("SELECT 1 FROM maggiedash_projects WHERE id=?", (project_id,)).fetchone()
|
|
116
|
+
has_default = self.connection.execute("SELECT 1 FROM maggiedash_projects WHERE id=?", ("local-project",)).fetchone()
|
|
117
|
+
if not has_target and has_default:
|
|
118
|
+
# Create the destination first so SQLite's foreign-key checks
|
|
119
|
+
# remain enabled while project-scoped rows are moved.
|
|
120
|
+
self.connection.execute(
|
|
121
|
+
"INSERT INTO maggiedash_projects (id,name,adapter,locale,status,created_at,updated_at) "
|
|
122
|
+
"SELECT ?,name,adapter,locale,status,created_at,updated_at FROM maggiedash_projects WHERE id=?",
|
|
123
|
+
(project_id, "local-project"),
|
|
124
|
+
)
|
|
125
|
+
existing_tables = {row[0] for row in self.connection.execute("SELECT name FROM sqlite_master WHERE type='table'")}
|
|
126
|
+
for table in ("maggiedash_memberships", "maggiedash_documents", "maggiedash_redirects", "maggiedash_audit_events", "maggiedash_section_presets"):
|
|
127
|
+
if table not in existing_tables:
|
|
128
|
+
continue
|
|
129
|
+
self.connection.execute(f"UPDATE {table} SET project_id=? WHERE project_id=?", (project_id, "local-project"))
|
|
130
|
+
self.connection.execute("DELETE FROM maggiedash_projects WHERE id=?", ("local-project",))
|
|
111
131
|
self.connection.execute(
|
|
112
132
|
"INSERT INTO maggiedash_projects (id,name,adapter,locale,created_at,updated_at) VALUES (?,?,?,?,?,?) "
|
|
113
133
|
"ON CONFLICT(id) DO UPDATE SET name=excluded.name, adapter=excluded.adapter, locale=excluded.locale, updated_at=excluded.updated_at",
|
package/package.json
CHANGED
|
@@ -25,6 +25,13 @@ return partial/nonzero and store only an error category. Use one worker per
|
|
|
25
25
|
project. Host database/API schedulers must explicitly integrate this boundary;
|
|
26
26
|
installing the skill does not patch an existing host ingestion implementation.
|
|
27
27
|
|
|
28
|
+
When a host imports JavaScript helpers from strict TypeScript, ship an adjacent
|
|
29
|
+
`.d.mts` declaration for every `.mjs` module that is part of the public
|
|
30
|
+
translation/ingest boundary. Keep the declaration's nullable/error outcome
|
|
31
|
+
types aligned with the runtime and include it in the typecheck fixture. This
|
|
32
|
+
prevents a successful runtime helper from becoming an implicit `any` or a
|
|
33
|
+
nullability mismatch in the scheduled path.
|
|
34
|
+
|
|
28
35
|
## Durable work
|
|
29
36
|
|
|
30
37
|
After source persistence, enqueue translation work using an idempotency key
|
|
@@ -5,6 +5,14 @@ approvals, jobs, audit, API, and MCP boundaries. External systems are adapters;
|
|
|
5
5
|
they never become the local source of truth without an explicit migration and
|
|
6
6
|
rollback decision.
|
|
7
7
|
|
|
8
|
+
The first-party distribution installs the React admin under `./_maggie/admin`
|
|
9
|
+
and portable host starters under `maggiedash/`: an idempotent PostgreSQL
|
|
10
|
+
schema, type-only adapter boundary, and the screen-level
|
|
11
|
+
`maggiedash-host-adapter.v2` contract. The schema is a starting migration for
|
|
12
|
+
the host's normal migration system; it must not drop legacy tables or receive
|
|
13
|
+
unbound project IDs. The type file is intentionally not a fake database
|
|
14
|
+
implementation.
|
|
15
|
+
|
|
8
16
|
## Foundation
|
|
9
17
|
|
|
10
18
|
Initialize and migrate the local project contract before content or provider
|
|
@@ -35,6 +43,17 @@ in environment variables and are never copied into reports.
|
|
|
35
43
|
explicitly declares that mirror required.
|
|
36
44
|
- API Pull, booking, analytics, email, and deployment providers expose health,
|
|
37
45
|
normalized errors, and explicit configuration schemas.
|
|
46
|
+
- Dashboard hosts must expose the exact method, route, response content type,
|
|
47
|
+
and required fields declared by `host-adapter-v2`. Create sanitized runtime
|
|
48
|
+
observations and validate them with `maggie dash conformance`; a static
|
|
49
|
+
component scan is not evidence that a screen works. Astro hosts that exclude
|
|
50
|
+
underscore filesystem segments must rewrite `/_maggie/section-preview` once
|
|
51
|
+
to the internal preview route without disabling origin checks.
|
|
52
|
+
|
|
53
|
+
For API Pull, retain only bounded provider messages and effective quota values
|
|
54
|
+
in lifecycle state. A quota-exhausted response means daily delivery remaining
|
|
55
|
+
is zero even if an older allowance field says otherwise; raw provider bodies
|
|
56
|
+
never enter `.maggie` reports.
|
|
38
57
|
|
|
39
58
|
## Ordered workflow
|
|
40
59
|
|