@topy-ai/maggie 0.3.0 → 0.5.1
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 +20 -2
- package/bin/maggie.js +12 -2
- package/bundled-contracts/maggie-content-ui/README.md +6 -0
- package/bundled-contracts/maggie-content-ui/plan-v1.schema.json +19 -0
- package/bundled-contracts/maggie-content-ui/reference-manifest-v1.schema.json +15 -0
- package/bundled-skills/maggie-blog/SKILL.md +12 -0
- package/bundled-skills/maggie-design/SKILL.md +51 -0
- package/bundled-skills/maggie-seo-geo/SKILL.md +4 -1
- package/bundled-skills/maggie-service-booking/SKILL.md +13 -0
- package/bundled-tools/clis/maggie_design.py +137 -0
- package/bundled-tools/clis/site_audit.py +13 -6
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -80,8 +80,8 @@ with `--confirm`, removes only known retired EmDash artifacts. It preserves
|
|
|
80
80
|
Recommended upgrade sequence for the current release:
|
|
81
81
|
|
|
82
82
|
```bash
|
|
83
|
-
npx @topy-ai/maggie@0.
|
|
84
|
-
npx @topy-ai/maggie@0.
|
|
83
|
+
npx @topy-ai/maggie@0.5.1 update --project . --force
|
|
84
|
+
npx @topy-ai/maggie@0.5.1 cleanup --project .
|
|
85
85
|
```
|
|
86
86
|
|
|
87
87
|
## MaggieDash lifecycle
|
|
@@ -267,6 +267,24 @@ python3 tools/clis/maggie_design.py rebrand \
|
|
|
267
267
|
| `maggie-auth-reference` | Generate and validate traditional email/password auth with secure server-side sessions |
|
|
268
268
|
| `maggie-blog` | Run a provider-neutral blog lifecycle with stable identity, topics, feeds, settings, and rollback |
|
|
269
269
|
|
|
270
|
+
`maggie-design` can initialize native blog and service UI plans from a local,
|
|
271
|
+
read-only structural reference:
|
|
272
|
+
|
|
273
|
+
```bash
|
|
274
|
+
maggie design reference-ui --project . --reference /path/to/reference \
|
|
275
|
+
--surface blog,service --confirm
|
|
276
|
+
maggie design init --project . --surface blog \
|
|
277
|
+
--reference-run .maggie/design/content-ui/reference-<id> --confirm
|
|
278
|
+
maggie design init --project . --surface service \
|
|
279
|
+
--reference-run .maggie/design/content-ui/reference-<id> --confirm
|
|
280
|
+
maggie design validate-ui --project . \
|
|
281
|
+
--plan .maggie/design/content-ui/blog/init-<id>/plan.json \
|
|
282
|
+
--rendered-dir .maggie/design/content-ui/blog/init-<id>/screenshots --confirm
|
|
283
|
+
```
|
|
284
|
+
|
|
285
|
+
This reuses the host project's homepage shell and `DESIGN.md`; it does not
|
|
286
|
+
copy reference branding, source code, private data, or provider facts.
|
|
287
|
+
|
|
270
288
|
The remaining installable skills are `maggie-blog-bootstrap`, `maggie-dash`,
|
|
271
289
|
`maggie-clone`, `maggie-clone-to-template`, `maggie-marketplace`,
|
|
272
290
|
`maggie-template`, `maggie-design`, `maggie-ops`, `maggie-deployment`,
|
package/bin/maggie.js
CHANGED
|
@@ -85,6 +85,9 @@ Usage:
|
|
|
85
85
|
maggie clone run <homepage-url> --run-id <id>
|
|
86
86
|
maggie clone status --run-id <id>
|
|
87
87
|
maggie design run <target-url> --clone-run <id>
|
|
88
|
+
maggie design reference-ui --project PATH --reference PATH --surface blog,service --confirm
|
|
89
|
+
maggie design init --project PATH --surface blog|service --reference-run PATH --confirm
|
|
90
|
+
maggie design validate-ui --project PATH --plan PATH --rendered-dir PATH --confirm
|
|
88
91
|
maggie design author --project PATH --route /about --purpose TEXT --audience TEXT --confirm
|
|
89
92
|
maggie auth reference --project PATH --confirm
|
|
90
93
|
maggie auth check --project PATH [--production]
|
|
@@ -205,7 +208,11 @@ function install(args) {
|
|
|
205
208
|
const tools = join(root, "tools");
|
|
206
209
|
if (existsSync(TOOLS_ROOT)) {
|
|
207
210
|
for (const group of ["clis", "runtime", "integrations"]) {
|
|
208
|
-
if (existsSync(join(TOOLS_ROOT, group)))
|
|
211
|
+
if (existsSync(join(TOOLS_ROOT, group))) {
|
|
212
|
+
const target = join(tools, group);
|
|
213
|
+
if (existsSync(target)) syncTree(join(TOOLS_ROOT, group), target, force);
|
|
214
|
+
else copyIfMissing(join(TOOLS_ROOT, group), target, force);
|
|
215
|
+
}
|
|
209
216
|
}
|
|
210
217
|
}
|
|
211
218
|
if (existsSync(join(DESIGN_ROOT, "SPA-DESIGN.md"))) {
|
|
@@ -241,7 +248,10 @@ function update(args) {
|
|
|
241
248
|
if (existsSync(REFERENCES_ROOT) && existsSync(join(agentRoot, "references"))) updated += syncTree(REFERENCES_ROOT, join(agentRoot, "references"), force);
|
|
242
249
|
}
|
|
243
250
|
const tools = join(root, "tools");
|
|
244
|
-
if (existsSync(TOOLS_ROOT)) for (const group of ["clis", "integrations"]) if (existsSync(join(
|
|
251
|
+
if (existsSync(TOOLS_ROOT)) for (const group of ["clis", "runtime", "integrations"]) if (existsSync(join(TOOLS_ROOT, group))) {
|
|
252
|
+
const target = join(tools, group);
|
|
253
|
+
if (existsSync(target)) updated += syncTree(join(TOOLS_ROOT, group), target, force);
|
|
254
|
+
}
|
|
245
255
|
if (existsSync(join(DESIGN_ROOT, "SPA-DESIGN.md")) && existsSync(join(root, ".maggie", "design-reference"))) {
|
|
246
256
|
updated += syncTree(DESIGN_ROOT, join(root, ".maggie", "design-reference"), force);
|
|
247
257
|
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
# Maggie Content UI Contracts
|
|
2
|
+
|
|
3
|
+
These contracts describe sanitized structural evidence captured from a local
|
|
4
|
+
UI reference and the native implementation plan consumed by `maggie-design`.
|
|
5
|
+
They intentionally exclude source-brand content, private data, credentials,
|
|
6
|
+
raw assets, and copied source code.
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
3
|
+
"$id": "maggie-content-ui-plan.v1",
|
|
4
|
+
"type": "object",
|
|
5
|
+
"required": ["schemaVersion", "id", "workflow", "mode", "surface", "phase", "shell", "components", "routes", "dataAdapter", "publicWrite"],
|
|
6
|
+
"properties": {
|
|
7
|
+
"schemaVersion": { "const": "maggie-content-ui-plan.v1" },
|
|
8
|
+
"id": { "type": "string", "pattern": "^init-[a-f0-9]+$" },
|
|
9
|
+
"workflow": { "const": "maggie-design" },
|
|
10
|
+
"mode": { "const": "content-ui-init" },
|
|
11
|
+
"surface": { "enum": ["blog", "service"] },
|
|
12
|
+
"phase": { "const": "ready" },
|
|
13
|
+
"shell": { "type": "object", "required": ["sourceOfTruth", "reuse", "replaceAllowed"] },
|
|
14
|
+
"components": { "type": "array", "minItems": 1 },
|
|
15
|
+
"routes": { "type": "array", "minItems": 1 },
|
|
16
|
+
"dataAdapter": { "enum": ["maggie-blog", "maggie-service-booking"] },
|
|
17
|
+
"publicWrite": { "const": false }
|
|
18
|
+
}
|
|
19
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
3
|
+
"$id": "maggie-content-ui-reference.v1",
|
|
4
|
+
"type": "object",
|
|
5
|
+
"required": ["schemaVersion", "id", "workflow", "mode", "surfaces", "sourcePolicy", "privateSourceCopied"],
|
|
6
|
+
"properties": {
|
|
7
|
+
"schemaVersion": { "const": "maggie-content-ui-reference.v1" },
|
|
8
|
+
"id": { "type": "string", "pattern": "^reference-[a-f0-9]+$" },
|
|
9
|
+
"workflow": { "const": "maggie-design" },
|
|
10
|
+
"mode": { "const": "reference-ui" },
|
|
11
|
+
"surfaces": { "type": "object", "minProperties": 1 },
|
|
12
|
+
"sourcePolicy": { "const": "read-only-structural-guideline" },
|
|
13
|
+
"privateSourceCopied": { "const": false }
|
|
14
|
+
}
|
|
15
|
+
}
|
|
@@ -32,3 +32,15 @@ published slug must not change during a rewrite. Search/sort views are not
|
|
|
32
32
|
indexable; drafts never appear in public routes, RSS, or sitemap output.
|
|
33
33
|
Provider keys remain server-side. Public publication and migrations always
|
|
34
34
|
require explicit confirmation.
|
|
35
|
+
|
|
36
|
+
To initialize native front-end pages from the approved local UI guideline,
|
|
37
|
+
run `maggie-design`:
|
|
38
|
+
|
|
39
|
+
```bash
|
|
40
|
+
maggie design init --project . --surface blog \
|
|
41
|
+
--reference-run .maggie/design/content-ui/reference-<id> \
|
|
42
|
+
--base-path /our-blogs --confirm
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
The blog skill supplies route/data semantics; `maggie-design` supplies the
|
|
46
|
+
host-native components and responsive visual review.
|
|
@@ -90,6 +90,57 @@ unconfirmed writes. Generated copy and media must be marked authored rather
|
|
|
90
90
|
than source-observed. Implementation must still capture responsive screenshots
|
|
91
91
|
and run accessibility/build validation before publication.
|
|
92
92
|
|
|
93
|
+
## Content UI initialization
|
|
94
|
+
|
|
95
|
+
Use content UI initialization when `maggie-blog` or
|
|
96
|
+
`maggie-service-booking` needs native front-end layouts based on a local UI
|
|
97
|
+
reference. The reference is structural evidence only. It must not replace the
|
|
98
|
+
project's `DESIGN.md`, homepage shell, brand, copy, assets, or provider facts.
|
|
99
|
+
|
|
100
|
+
First record a sanitized, read-only reference manifest:
|
|
101
|
+
|
|
102
|
+
```bash
|
|
103
|
+
maggie design reference-ui \
|
|
104
|
+
--project . \
|
|
105
|
+
--reference /home/balalior/Dev/clients/spachevychase.org \
|
|
106
|
+
--surface blog,service --confirm
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
Then initialize one surface at a time from that manifest:
|
|
110
|
+
|
|
111
|
+
```bash
|
|
112
|
+
maggie design init --project . --surface blog \
|
|
113
|
+
--reference-run .maggie/design/content-ui/reference-<id> \
|
|
114
|
+
--base-path /our-blogs --confirm
|
|
115
|
+
|
|
116
|
+
maggie design init --project . --surface service \
|
|
117
|
+
--reference-run .maggie/design/content-ui/reference-<id> \
|
|
118
|
+
--route-pattern '/services/[slug]/' --confirm
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
The initializer creates a reviewable component/route/responsive/provenance
|
|
122
|
+
plan under `.maggie/design/content-ui/`. It does not write public components
|
|
123
|
+
or publish routes. The implementation phase must call the matching data skill,
|
|
124
|
+
reuse the homepage shell, capture desktop/tablet/mobile screenshots, and pass
|
|
125
|
+
accessibility, metadata, build, and visual review gates before approval.
|
|
126
|
+
|
|
127
|
+
Validate the rendered plan before approval:
|
|
128
|
+
|
|
129
|
+
```bash
|
|
130
|
+
maggie design validate-ui --project . \
|
|
131
|
+
--plan .maggie/design/content-ui/blog/init-<id>/plan.json \
|
|
132
|
+
--rendered-dir .maggie/design/content-ui/blog/init-<id>/screenshots \
|
|
133
|
+
--confirm
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
This gate requires valid PNG evidence for desktop, tablet, and mobile and keeps
|
|
137
|
+
`publicWrite: false` until a separate approval step.
|
|
138
|
+
|
|
139
|
+
For blog UI, preserve archive, topic, post/slug, pagination, RSS, and sitemap
|
|
140
|
+
semantics from `maggie-blog`. For service UI, preserve provider identity,
|
|
141
|
+
variants, booking URLs, duplicate canonical/noindex decisions, and service
|
|
142
|
+
sitemap semantics from `maggie-service-booking`.
|
|
143
|
+
|
|
93
144
|
## Explicit homepage rebrand mode
|
|
94
145
|
|
|
95
146
|
The homepage `review` mode only compares screenshots. It does not rebrand a
|
|
@@ -30,7 +30,10 @@ interpretation, and content creation remain agent-reviewed decisions.
|
|
|
30
30
|
With `--crawl`, it resolves sitemap indexes and audits every listed HTML route
|
|
31
31
|
for HTTP 200, one H1, title/description, locale, canonical, Open Graph/Twitter
|
|
32
32
|
metadata, valid entity JSON-LD, and image alt text. Non-HTML endpoints such as
|
|
33
|
-
RSS must not be included in the HTML page sitemap.
|
|
33
|
+
RSS must not be included in the HTML page sitemap. Sitemap `<loc>` values must
|
|
34
|
+
be absolute HTTP(S) URLs; the audit reports relative values as a failure even
|
|
35
|
+
when it can resolve them for continued crawling, so one malformed entry cannot
|
|
36
|
+
hide a protocol violation.
|
|
34
37
|
Use `--output <project>/docs/site-audit.json` to persist the evidence used by
|
|
35
38
|
the release review.
|
|
36
39
|
|
|
@@ -207,6 +207,19 @@ and `DESIGN.md`. Use the homepage header, footer, fonts, tokens, navigation,
|
|
|
207
207
|
analytics boundary, accessibility behaviour, and responsive breakpoints.
|
|
208
208
|
Only the service content region varies.
|
|
209
209
|
|
|
210
|
+
When initializing a new service UI from the approved reference guideline, use
|
|
211
|
+
the shared `maggie-design` initializer:
|
|
212
|
+
|
|
213
|
+
```bash
|
|
214
|
+
maggie design init --project . --surface service \
|
|
215
|
+
--reference-run .maggie/design/content-ui/reference-<id> \
|
|
216
|
+
--route-pattern '/services/[slug]/' --confirm
|
|
217
|
+
```
|
|
218
|
+
|
|
219
|
+
This creates a reviewable native component/route plan. It does not copy the
|
|
220
|
+
reference project's brand, source code, assets, or provider facts, and it does
|
|
221
|
+
not publish service pages without the existing service fact/copy gates.
|
|
222
|
+
|
|
210
223
|
Each service page must expose the service title, category breadcrumbs, factual
|
|
211
224
|
description, all active variants, duration, currency-formatted price, booking
|
|
212
225
|
CTA, and payment CTA only if available. Preserve the provider booking URL as
|
|
@@ -34,6 +34,32 @@ DESIGN_HEADINGS = (
|
|
|
34
34
|
|
|
35
35
|
REVIEW_VIEWPORTS = {"desktop": (1440, 900), "tablet": (768, 900), "mobile": (390, 844)}
|
|
36
36
|
|
|
37
|
+
REFERENCE_SURFACES = {
|
|
38
|
+
"blog": {
|
|
39
|
+
"files": [
|
|
40
|
+
"src/layouts/BlogLayout.astro", "src/components/blog/BlogArchive.astro",
|
|
41
|
+
"src/components/blog/PostCard.astro", "src/components/blog/Pagination.astro",
|
|
42
|
+
"src/components/blog/PostSidebar.astro", "src/pages/our-blogs/index.astro",
|
|
43
|
+
"src/pages/our-blogs/[slug].astro", "src/pages/our-blogs/topic/[slug].astro",
|
|
44
|
+
"src/pages/rss.xml.ts", "src/pages/sitemap.xml.ts",
|
|
45
|
+
],
|
|
46
|
+
"components": ["BlogLayout", "BlogArchive", "PostCard", "Pagination", "PostSidebar", "ShareMenu"],
|
|
47
|
+
"routes": ["{base}/", "{base}/page/{n}/", "{base}/{slug}/", "{base}/topic/{slug}/", "{base}/topic/{slug}/page/{n}/", "/rss.xml", "/sitemap.xml"],
|
|
48
|
+
"patterns": ["compact image-backed archive hero", "URL-backed search/topic/sort controls", "responsive post grid", "sticky article sidebar", "related articles", "crawlable topic links"],
|
|
49
|
+
},
|
|
50
|
+
"service": {
|
|
51
|
+
"files": [
|
|
52
|
+
"src/pages/services/index.astro", "src/pages/services/[slug].astro",
|
|
53
|
+
"src/components/ServicePage.astro", "src/components/CategoryServiceHub.astro",
|
|
54
|
+
"src/components/SupportingServicePage.astro", "src/components/ServiceContextNarrative.astro",
|
|
55
|
+
"src/pages/sitemap-services.xml.ts", "src/pages/sitemap-service-categories.xml.ts",
|
|
56
|
+
],
|
|
57
|
+
"components": ["ServiceIndex", "CategoryServiceHub", "ServicePage", "ServiceFacts", "RelatedServices", "ServiceFaq"],
|
|
58
|
+
"routes": ["/services/", "/services/{slug}/", "/service-categories/{slug}/", "/sitemap-services.xml", "/sitemap-service-categories.xml"],
|
|
59
|
+
"patterns": ["category breadcrumb hero", "variant facts and booking card", "service narrative with image", "related treatment grid", "FAQ accordion", "category navigation"],
|
|
60
|
+
},
|
|
61
|
+
}
|
|
62
|
+
|
|
37
63
|
|
|
38
64
|
def rendered_asset_preflight(template: Path) -> list[str]:
|
|
39
65
|
"""Catch package CSS failures before visual comparison is trusted."""
|
|
@@ -56,6 +82,81 @@ def rendered_asset_preflight(template: Path) -> list[str]:
|
|
|
56
82
|
return errors
|
|
57
83
|
|
|
58
84
|
|
|
85
|
+
def _surface_list(value: str) -> list[str]:
|
|
86
|
+
surfaces = [item.strip().lower() for item in value.split(",") if item.strip()]
|
|
87
|
+
invalid = sorted(set(surfaces) - set(REFERENCE_SURFACES))
|
|
88
|
+
if invalid or not surfaces:
|
|
89
|
+
raise ValueError("surface must contain blog and/or service")
|
|
90
|
+
return list(dict.fromkeys(surfaces))
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def reference_ui(project: Path, reference: Path, surfaces: list[str], screenshots_dir: Path | None, confirm: bool) -> int:
|
|
94
|
+
"""Record sanitized structural evidence from a read-only reference project."""
|
|
95
|
+
project = project.resolve(); reference = reference.resolve()
|
|
96
|
+
if not reference.is_dir(): raise ValueError(f"reference project not found: {reference}")
|
|
97
|
+
if not confirm: print("CONFIRMATION_REQUIRED: rerun with --confirm", file=sys.stderr); return 2
|
|
98
|
+
missing = {surface: [item for item in REFERENCE_SURFACES[surface]["files"] if not (reference / item).exists()] for surface in surfaces}
|
|
99
|
+
missing = {key: value for key, value in missing.items() if value}
|
|
100
|
+
if missing: raise ValueError("reference files missing: " + json.dumps(missing, ensure_ascii=False))
|
|
101
|
+
digest = hashlib.sha256((str(reference) + "\n" + ",".join(surfaces)).encode()).hexdigest()[:12]
|
|
102
|
+
output_dir = project / ".maggie" / "design" / "content-ui" / f"reference-{digest}"; output_dir.mkdir(parents=True, exist_ok=True)
|
|
103
|
+
evidence = {}
|
|
104
|
+
for surface in surfaces:
|
|
105
|
+
config = REFERENCE_SURFACES[surface]
|
|
106
|
+
evidence[surface] = {"sourceFiles": config["files"], "components": config["components"], "routes": config["routes"], "patterns": config["patterns"], "observations": {pattern: True for pattern in config["patterns"]}}
|
|
107
|
+
screenshots = {name: "pending" for name in REVIEW_VIEWPORTS}
|
|
108
|
+
if screenshots_dir:
|
|
109
|
+
screenshots = {name: str((screenshots_dir / f"{name}.png").resolve()) if (screenshots_dir / f"{name}.png").is_file() else "missing" for name in REVIEW_VIEWPORTS}
|
|
110
|
+
manifest = {"schemaVersion": "maggie-content-ui-reference.v1", "id": f"reference-{digest}", "workflow": "maggie-design", "mode": "reference-ui", "referenceProject": reference.name, "surfaces": evidence, "screenshots": screenshots, "sourcePolicy": "read-only-structural-guideline", "privateSourceCopied": False, "createdAt": datetime.now(timezone.utc).isoformat()}
|
|
111
|
+
path = output_dir / "reference-manifest.json"; path.write_text(json.dumps(manifest, indent=2, ensure_ascii=False) + "\n", encoding="utf-8")
|
|
112
|
+
print(json.dumps({"manifest": str(path), "id": manifest["id"], "surfaces": surfaces, "screenshotStatus": screenshots}, indent=2, ensure_ascii=False)); return 0
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def init_content_ui(project: Path, surface: str, reference_run: Path, base_path: str, route_pattern: str, confirm: bool) -> int:
|
|
116
|
+
"""Create the reviewable plan that Maggie Design will implement natively."""
|
|
117
|
+
project = project.resolve(); surface = surface.strip().lower()
|
|
118
|
+
if surface not in REFERENCE_SURFACES: raise ValueError("surface must be blog or service")
|
|
119
|
+
if not confirm: print("CONFIRMATION_REQUIRED: rerun with --confirm", file=sys.stderr); return 2
|
|
120
|
+
contract = require_design_contract(project)
|
|
121
|
+
manifest_path = reference_run.resolve()
|
|
122
|
+
if manifest_path.is_dir(): manifest_path /= "reference-manifest.json"
|
|
123
|
+
if not manifest_path.exists(): raise ValueError(f"reference manifest required: {manifest_path}")
|
|
124
|
+
manifest = json.loads(manifest_path.read_text(encoding="utf-8"))
|
|
125
|
+
if surface not in manifest.get("surfaces", {}): raise ValueError(f"reference manifest does not contain surface: {surface}")
|
|
126
|
+
digest = hashlib.sha256((str(project) + "\n" + surface + "\n" + str(manifest_path)).encode()).hexdigest()[:12]
|
|
127
|
+
output_dir = project / ".maggie" / "design" / "content-ui" / surface / f"init-{digest}"; output_dir.mkdir(parents=True, exist_ok=True)
|
|
128
|
+
config = REFERENCE_SURFACES[surface]
|
|
129
|
+
routes = config["routes"]
|
|
130
|
+
if surface == "blog": routes = [route.replace("{base}", base_path.rstrip("/") or "/our-blogs") for route in routes]
|
|
131
|
+
elif route_pattern: routes = [route_pattern, "/sitemap-services.xml", "/sitemap-service-categories.xml"]
|
|
132
|
+
plan = {"schemaVersion": "maggie-content-ui-plan.v1", "id": f"init-{digest}", "workflow": "maggie-design", "mode": "content-ui-init", "surface": surface, "phase": "ready", "referenceManifest": str(manifest_path), "designContract": contract, "shell": {"sourceOfTruth": "existing homepage shell", "reuse": ["header", "footer", "tokens", "typography", "icons", "breakpoints", "accessibility", "analytics"], "replaceAllowed": False}, "components": config["components"], "routes": routes, "dataAdapter": "maggie-blog" if surface == "blog" else "maggie-service-booking", "provenance": {"referenceObserved": True, "projectPreserved": True, "agentAuthored": True, "providerFact": surface == "service"}, "requiredSteps": ["inspect-host-shell", "create-native-components", "bind-data-adapter", "capture-desktop-tablet-mobile", "accessibility-check", "metadata-and-route-check", "build-check", "explicit-approval"], "publicWrite": False, "createdAt": datetime.now(timezone.utc).isoformat()}
|
|
133
|
+
files = {"reference-manifest.json": manifest, "shell-reuse.json": plan["shell"], "component-plan.json": {"components": config["components"], "patterns": config["patterns"]}, "route-plan.json": {"routes": routes, "dataAdapter": plan["dataAdapter"]}, "responsive-plan.json": {"viewports": REVIEW_VIEWPORTS, "mobileStickySidebar": False if surface == "blog" else None}, "provenance.json": plan["provenance"], "validation.json": {"status": "pending", "required": plan["requiredSteps"]}}
|
|
134
|
+
for filename, value in files.items(): (output_dir / filename).write_text(json.dumps(value, indent=2, ensure_ascii=False) + "\n", encoding="utf-8")
|
|
135
|
+
(output_dir / "screenshots").mkdir(exist_ok=True)
|
|
136
|
+
(output_dir / "plan.json").write_text(json.dumps(plan, indent=2, ensure_ascii=False) + "\n", encoding="utf-8")
|
|
137
|
+
print(json.dumps({"plan": str(output_dir / "plan.json"), "surface": surface, "phase": "ready", "publicWrite": False}, indent=2)); return 0
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
def validate_content_ui(project: Path, plan_path: Path, rendered_dir: Path | None, confirm: bool) -> int:
|
|
141
|
+
"""Validate a generated content UI plan before host publication."""
|
|
142
|
+
project = project.resolve(); plan_path = plan_path.resolve()
|
|
143
|
+
if not plan_path.exists(): raise ValueError(f"content UI plan not found: {plan_path}")
|
|
144
|
+
plan = json.loads(plan_path.read_text(encoding="utf-8")); errors = []
|
|
145
|
+
if plan.get("workflow") != "maggie-design" or plan.get("mode") != "content-ui-init": errors.append("invalid design workflow plan")
|
|
146
|
+
if plan.get("publicWrite") is not False: errors.append("publicWrite must remain false before approval")
|
|
147
|
+
try: require_design_contract(project)
|
|
148
|
+
except ValueError as error: errors.append(str(error))
|
|
149
|
+
screenshot_status = {}
|
|
150
|
+
for name in REVIEW_VIEWPORTS:
|
|
151
|
+
path = (rendered_dir / f"{name}.png") if rendered_dir else plan_path.parent / "screenshots" / f"{name}.png"
|
|
152
|
+
valid = path.is_file() and path.read_bytes()[:8] == b"\x89PNG\r\n\x1a\n"
|
|
153
|
+
screenshot_status[name] = {"path": str(path), "validPng": valid}
|
|
154
|
+
if not valid: errors.append(f"missing valid {name} screenshot")
|
|
155
|
+
result = {"schemaVersion": "maggie-content-ui-validation.v1", "plan": str(plan_path), "surface": plan.get("surface"), "screenshots": screenshot_status, "checks": {"shellReuse": plan.get("shell", {}).get("replaceAllowed") is False, "publicWriteBlocked": plan.get("publicWrite") is False, "designContract": not any("DESIGN.md" in error for error in errors)}, "status": "passed" if not errors else "failed", "errors": errors, "validatedAt": datetime.now(timezone.utc).isoformat()}
|
|
156
|
+
if not confirm: print(json.dumps(result, indent=2, ensure_ascii=False)); return 0 if not errors else 1
|
|
157
|
+
output = plan_path.parent / "validation.json"; output.write_text(json.dumps(result, indent=2, ensure_ascii=False) + "\n", encoding="utf-8"); print(json.dumps(result, indent=2, ensure_ascii=False)); return 0 if not errors else 1
|
|
158
|
+
|
|
159
|
+
|
|
59
160
|
def rebrand_template(args: argparse.Namespace) -> int:
|
|
60
161
|
"""Apply an explicit brand identity to a packaged homepage template."""
|
|
61
162
|
template = args.template.resolve()
|
|
@@ -479,6 +580,42 @@ def author_job(project: Path, route: str, purpose: str, audience: str, brief_fil
|
|
|
479
580
|
|
|
480
581
|
|
|
481
582
|
def main() -> int:
|
|
583
|
+
if len(sys.argv) > 1 and sys.argv[1] == "reference-ui":
|
|
584
|
+
command = argparse.ArgumentParser(description="Record sanitized blog/service UI evidence from a read-only reference project.")
|
|
585
|
+
command.add_argument("--project", type=Path, default=Path.cwd())
|
|
586
|
+
command.add_argument("--reference", type=Path, required=True)
|
|
587
|
+
command.add_argument("--surface", default="blog,service")
|
|
588
|
+
command.add_argument("--screenshots-dir", type=Path)
|
|
589
|
+
command.add_argument("--confirm", action="store_true")
|
|
590
|
+
args = command.parse_args(sys.argv[2:])
|
|
591
|
+
try:
|
|
592
|
+
return reference_ui(args.project, args.reference, _surface_list(args.surface), args.screenshots_dir, args.confirm)
|
|
593
|
+
except (OSError, ValueError, json.JSONDecodeError) as error:
|
|
594
|
+
print(f"BLOCKED: maggie-design reference-ui: {error}", file=sys.stderr); return 1
|
|
595
|
+
if len(sys.argv) > 1 and sys.argv[1] == "init":
|
|
596
|
+
command = argparse.ArgumentParser(description="Initialize a native blog or service UI plan from a sanitized reference manifest.")
|
|
597
|
+
command.add_argument("--project", type=Path, default=Path.cwd())
|
|
598
|
+
command.add_argument("--surface", required=True, choices=["blog", "service"])
|
|
599
|
+
command.add_argument("--reference-run", type=Path, required=True)
|
|
600
|
+
command.add_argument("--base-path", default="/our-blogs")
|
|
601
|
+
command.add_argument("--route-pattern", default="/services/[slug]/")
|
|
602
|
+
command.add_argument("--confirm", action="store_true")
|
|
603
|
+
args = command.parse_args(sys.argv[2:])
|
|
604
|
+
try:
|
|
605
|
+
return init_content_ui(args.project, args.surface, args.reference_run, args.base_path, args.route_pattern, args.confirm)
|
|
606
|
+
except (OSError, ValueError, json.JSONDecodeError) as error:
|
|
607
|
+
print(f"BLOCKED: maggie-design init: {error}", file=sys.stderr); return 1
|
|
608
|
+
if len(sys.argv) > 1 and sys.argv[1] == "validate-ui":
|
|
609
|
+
command = argparse.ArgumentParser(description="Validate content UI screenshots and design/public-write gates.")
|
|
610
|
+
command.add_argument("--project", type=Path, default=Path.cwd())
|
|
611
|
+
command.add_argument("--plan", type=Path, required=True)
|
|
612
|
+
command.add_argument("--rendered-dir", type=Path)
|
|
613
|
+
command.add_argument("--confirm", action="store_true")
|
|
614
|
+
args = command.parse_args(sys.argv[2:])
|
|
615
|
+
try:
|
|
616
|
+
return validate_content_ui(args.project, args.plan, args.rendered_dir, args.confirm)
|
|
617
|
+
except (OSError, ValueError, json.JSONDecodeError) as error:
|
|
618
|
+
print(f"BLOCKED: maggie-design validate-ui: {error}", file=sys.stderr); return 1
|
|
482
619
|
if len(sys.argv) > 1 and sys.argv[1] == "rebrand":
|
|
483
620
|
rebrand = argparse.ArgumentParser(description="Apply an explicit brand identity to a packaged marketplace template.")
|
|
484
621
|
rebrand.add_argument("--template", type=Path, required=True)
|
|
@@ -134,10 +134,11 @@ def hreflang_check(url: str, page: PageParser, expected: set[str]) -> dict:
|
|
|
134
134
|
return {"ok": not missing and not reciprocal_errors, "missing": missing, "reciprocal_errors": reciprocal_errors, "links": links}
|
|
135
135
|
|
|
136
136
|
|
|
137
|
-
def sitemap_urls(base: str) -> list[str]:
|
|
138
|
-
"""Resolve a sitemap index and return
|
|
137
|
+
def sitemap_urls(base: str) -> tuple[list[str], list[dict[str, str]]]:
|
|
138
|
+
"""Resolve a sitemap index and return page URLs plus raw loc violations."""
|
|
139
139
|
pending = [urljoin(base + "/", "sitemap.xml")]
|
|
140
140
|
pages = []
|
|
141
|
+
violations = []
|
|
141
142
|
seen = set()
|
|
142
143
|
while pending:
|
|
143
144
|
sitemap_url = pending.pop(0)
|
|
@@ -145,12 +146,16 @@ def sitemap_urls(base: str) -> list[str]:
|
|
|
145
146
|
continue
|
|
146
147
|
seen.add(sitemap_url)
|
|
147
148
|
_, _, body = fetch(sitemap_url)
|
|
148
|
-
|
|
149
|
+
raw_locs = [value.strip() for value in re.findall(r"<loc>\s*(.*?)\s*</loc>", body, re.I | re.S)]
|
|
150
|
+
for value in raw_locs:
|
|
151
|
+
if not re.match(r"^https?://[^\s]+$", value, re.I):
|
|
152
|
+
violations.append({"sitemap": sitemap_url, "loc": value, "reason": "sitemap loc must be an absolute HTTP(S) URL"})
|
|
153
|
+
locs = [urljoin(base + "/", value) for value in raw_locs]
|
|
149
154
|
if re.search(r"<sitemapindex\b", body, re.I):
|
|
150
155
|
pending.extend(locs)
|
|
151
156
|
else:
|
|
152
157
|
pages.extend(locs)
|
|
153
|
-
return list(dict.fromkeys(pages))
|
|
158
|
+
return list(dict.fromkeys(pages)), violations
|
|
154
159
|
|
|
155
160
|
|
|
156
161
|
def main() -> int:
|
|
@@ -215,7 +220,8 @@ def main() -> int:
|
|
|
215
220
|
crawl = {"enabled": args.crawl, "passed": True, "pages": []}
|
|
216
221
|
if args.crawl:
|
|
217
222
|
try:
|
|
218
|
-
urls = sitemap_urls(base)
|
|
223
|
+
urls, sitemap_violations = sitemap_urls(base)
|
|
224
|
+
urls = urls[: args.max_pages]
|
|
219
225
|
for page_url in urls:
|
|
220
226
|
try:
|
|
221
227
|
status, content_type, body = fetch(page_url)
|
|
@@ -232,7 +238,8 @@ def main() -> int:
|
|
|
232
238
|
except Exception as exc:
|
|
233
239
|
page_checks = {"url": page_url, "passed": False, "error": type(exc).__name__}
|
|
234
240
|
crawl["pages"].append(page_checks)
|
|
235
|
-
crawl["
|
|
241
|
+
crawl["sitemap_loc_violations"] = sitemap_violations
|
|
242
|
+
crawl["passed"] = not sitemap_violations and bool(urls) and len(crawl["pages"]) == len(urls) and all(item["passed"] for item in crawl["pages"])
|
|
236
243
|
crawl["url_count"] = len(urls)
|
|
237
244
|
except Exception as exc:
|
|
238
245
|
crawl = {"enabled": True, "passed": False, "error": type(exc).__name__, "pages": []}
|