@topy-ai/maggie 0.7.16 → 0.7.18

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (32) hide show
  1. package/README.md +52 -9
  2. package/README.zh-TW.md +10 -2
  3. package/bin/maggie.js +5 -4
  4. package/bundled-references/universal-booking-adapter.md +36 -1
  5. package/bundled-skills/maggie-blog/SKILL.md +11 -1
  6. package/bundled-skills/maggie-dash/SKILL.md +5 -0
  7. package/bundled-skills/maggie-deployment/SKILL.md +9 -1
  8. package/bundled-skills/maggie-feedback/SKILL.md +5 -0
  9. package/bundled-skills/maggie-ops/SKILL.md +8 -0
  10. package/bundled-skills/maggie-qa-workflow/SKILL.md +11 -0
  11. package/bundled-skills/maggie-seo-geo/SKILL.md +29 -1
  12. package/bundled-skills/maggie-service-booking/SKILL.md +19 -1
  13. package/bundled-tools/clis/maggie.py +1 -1
  14. package/bundled-tools/clis/maggie_blog.py +12 -2
  15. package/bundled-tools/clis/maggie_feedback.py +17 -5
  16. package/bundled-tools/clis/maggie_head_tags.py +35 -0
  17. package/bundled-tools/clis/maggie_indexnow.py +6 -3
  18. package/bundled-tools/clis/maggie_ops.py +12 -0
  19. package/bundled-tools/clis/maggie_qa_workflow.py +2 -0
  20. package/bundled-tools/clis/maggie_release.py +88 -2
  21. package/bundled-tools/clis/maggie_service_booking.py +42 -3
  22. package/bundled-tools/clis/maggie_social_cards.py +45 -0
  23. package/bundled-tools/clis/site_audit.py +2 -2
  24. package/bundled-tools/runtime/booking_capabilities.py +137 -0
  25. package/bundled-tools/runtime/maggie_blog.py +85 -0
  26. package/bundled-tools/runtime/maggie_favicon.py +86 -0
  27. package/bundled-tools/runtime/maggie_head_tags.py +101 -0
  28. package/bundled-tools/runtime/maggie_indexnow.py +50 -0
  29. package/bundled-tools/runtime/maggie_social_cards.py +165 -0
  30. package/bundled-tools/runtime/service_variants.py +48 -7
  31. package/package.json +1 -1
  32. package/references/universal-booking-adapter.md +36 -1
package/README.md CHANGED
@@ -80,9 +80,30 @@ The current package also includes reusable safeguards from the latest feedback
80
80
  review: `maggie dash api-contract` checks declared request and 2xx response
81
81
  shapes; `maggie blog check-gate`/`approve` enforces review before publish;
82
82
  sitemap validation flags suspiciously uniform `lastmod` dates; `maggie seo
83
- indexnow` plans only changed same-origin URLs and holds back URLs accepted in
84
- the last 24 hours; and dash inventory separates renderer kind from public page
85
- kind while reporting source coverage for code-rendered routes.
83
+ indexnow` plans only changed same-origin URLs, verifies the deployed key route
84
+ with an optional negative control, and holds back URLs accepted in the last 24
85
+ hours. `maggie seo social-cards` checks per-page OG image dimensions and format;
86
+ `maggie seo head-tags` reports metadata drift across rendered shells;
87
+ `maggie ops favicon-check` verifies the served `/favicon.ico` and declared icon.
88
+ Dash inventory separates renderer kind from public page kind while reporting
89
+ source coverage for code-rendered routes.
90
+
91
+ Service booking providers can be checked with a machine-readable,
92
+ fixture-backed capability matrix:
93
+
94
+ ```bash
95
+ maggie service capability-audit --project . \
96
+ --catalogue .maggie/booking/services.json \
97
+ --capabilities-file .maggie/booking/provider-capabilities.json \
98
+ --fixture .maggie/booking/fixtures/provider.json
99
+ ```
100
+
101
+ The declaration records variant mode (`native`, `derived-offers`,
102
+ `single-fallback`, or `blocked`), price semantics, and stable-ID confidence.
103
+ The audit also records observed counts and fixture evidence. Localized service
104
+ variants use market/locale-aware canonical routes and reciprocal hreflang.
105
+ Release preflight consumes the latest matching scenario QA run when one is
106
+ configured and blocks incomplete evidence.
86
107
 
87
108
  For Google integrations, validate a redacted provider matrix before reporting
88
109
  access. The command fails closed on unknown scopes, missing Ads prerequisites,
@@ -141,11 +162,14 @@ maggie memory ... # confirmed preferences and lessons
141
162
  maggie feedback ... # redact, preview, submit, list
142
163
  maggie qa ... # scenario browser QA, fix/retest, release gate
143
164
  maggie localization ... # plan, validate, review, publish, stale
144
- maggie service ... # import, sync, generate, validate
165
+ maggie service ... # import, sync, capability audit, validate
145
166
  maggie seo performance ... # sampled PageSpeed/CWV report and baseline
146
167
  maggie seo images ... # inventory, variants, confirmation, validate
147
168
  maggie seo sitemap ... # typed/semantic plan, agent-files, apply, rollback
148
- maggie seo indexnow ... # changed URLs, key check, retry-safe 24h guard
169
+ maggie seo indexnow ... # changed URLs, deployed key check, 24h guard
170
+ maggie seo social-cards ... # per-page og:image format/dimension audit
171
+ maggie seo head-tags ... # rendered-shell head metadata drift audit
172
+ maggie ops favicon-check ... # served favicon behaviour check
149
173
  maggie deployment | migration | release | analytics | schedule
150
174
  maggie migration identity --identity-file FILE [--expected-file FILE]
151
175
  maggie deployment canary --asset URL=SHA256 --render-report report.json
@@ -199,6 +223,19 @@ Read the [performance PRD](https://github.com/TOPY-AI-LTD/ai-cmo-skills/blob/mai
199
223
  and [image/sitemap PRD](https://github.com/TOPY-AI-LTD/ai-cmo-skills/blob/main/docs/image-sitemap-structure-prd.md)
200
224
  for adapter, privacy, apply, rollback, and release gates.
201
225
 
226
+ For a database-backed blog, `maggie blog check-gate` accepts a sanitized host
227
+ settings adapter so review policy can be checked without copying database
228
+ credentials into a local store:
229
+
230
+ ```bash
231
+ maggie blog check-gate --project . \
232
+ --adapter-command '["node", "scripts/read-blog-settings.mjs"]'
233
+ ```
234
+
235
+ The adapter is an argv array, receives a versioned request on stdin, and must
236
+ print only the three review-policy fields. Provider stderr is suppressed and a
237
+ missing local store without an adapter fails closed.
238
+
202
239
  ### Design and deployment release gates
203
240
 
204
241
  Check source-to-runtime icon coverage before shipping a design:
@@ -247,8 +284,8 @@ artifact schemas.
247
284
  Recommended upgrade sequence for the current release:
248
285
 
249
286
  ```bash
250
- npx @topy-ai/maggie@0.7.16 update --project . --force
251
- npx @topy-ai/maggie@0.7.16 cleanup --project .
287
+ npx @topy-ai/maggie@0.7.18 update --project . --force
288
+ npx @topy-ai/maggie@0.7.18 cleanup --project .
252
289
  ```
253
290
 
254
291
  Maintainers should pass npm credentials through the repository helper, never
@@ -258,8 +295,14 @@ as a command-line argument:
258
295
  node scripts/publish-npm.mjs --maggie-env-file ../.env
259
296
  ```
260
297
 
261
- The 0.7.16 workflow tightens the review-gate exit code and adds retry-path
262
- regression coverage. The 0.7.15 workflow added API contracts, blog review gates, sitemap freshness,
298
+ The 0.7.18 workflow adds staged/untracked changed-surface detection, scenario
299
+ QA release integration, market/locale-aware service variant routes and
300
+ reciprocal hreflang, provider capability matrices with fixture evidence, and
301
+ feedback fixed-proof/path-privacy gates. The 0.7.17 workflow adds sanitized database-backed blog gate adapters, deployed
302
+ IndexNow key verification, social-card and cross-shell head-tag audits, required
303
+ Open Graph image coverage, and behavioural favicon verification. The 0.7.16
304
+ workflow tightens the review-gate exit code and adds retry-path regression
305
+ coverage. The 0.7.15 workflow added API contracts, blog review gates, sitemap freshness,
263
306
  change-driven IndexNow, and page-kind/source-coverage inventory. It also keeps
264
307
  the general `maggie-qa-workflow` skill and `maggie qa`
265
308
  CLI for scenario manifests, secret-free browser evidence metadata, test/fix/
package/README.zh-TW.md CHANGED
@@ -8,7 +8,7 @@ Codex、Claude Code 與相容的 coding agents。
8
8
  ## 安裝
9
9
 
10
10
  ```bash
11
- npx @topy-ai/maggie@0.7.16 init --agent all
11
+ npx @topy-ai/maggie@0.7.18 init --agent all
12
12
  npx @topy-ai/maggie doctor --project .
13
13
  ```
14
14
 
@@ -25,7 +25,9 @@ maggie doctor --project . --require-bootstrap --strict
25
25
  deployment、memory、feedback 和 MaggieDash。內容先 draft/review,外部寫入、
26
26
  publish 與 production deployment 需要明確確認。
27
27
 
28
- 0.7.16 補強 review gate 的 CI exit code 與 IndexNow retry regression;
28
+ 0.7.18 加入 staged/untracked changed-surface release gate、QA run 與 release
29
+ 整合、market/locale-aware service variant route、provider capability matrix
30
+ fixture evidence,以及 feedback fixed-proof/path privacy gate。0.7.16 補強 review gate 的 CI exit code 與 IndexNow retry regression;
29
31
  0.7.15 也加入 API schema contract、blog review gate、sitemap freshness
30
32
  warning、change-driven IndexNow 與 page-kind/source-coverage inventory。
31
33
 
@@ -59,6 +61,12 @@ maggie migration identity --identity-file .maggie/db-identity.json \
59
61
  maggie qa start --project . --scenario-file .maggie/scenario-manifest.json \
60
62
  --environment local --base-url http://localhost:4321
61
63
  maggie qa summary --project . --run <run-id>
64
+
65
+ # Provider variant capability matrix
66
+ maggie service capability-audit --project . \
67
+ --catalogue .maggie/booking/services.json \
68
+ --capabilities-file .maggie/booking/provider-capabilities.json \
69
+ --fixture .maggie/booking/fixtures/provider.json
62
70
  ```
63
71
 
64
72
  完整中文說明、19 個 skills 清單和 roadmap:
package/bin/maggie.js CHANGED
@@ -98,6 +98,7 @@ Usage:
98
98
  maggie service import <provider-url> --project PATH
99
99
  maggie service sync <provider-url> --project PATH
100
100
  maggie service generate --project PATH
101
+ maggie service capability-audit --project PATH --catalogue FILE --capabilities-file FILE --fixture FILE
101
102
  maggie service validate --project PATH
102
103
  maggie service inspect --project PATH
103
104
  maggie service status --project PATH
@@ -113,7 +114,7 @@ Usage:
113
114
  maggie api lifecycle --project PATH [--execute --allow-quota]
114
115
  maggie memory <init|list|search|context|add|record-error|transition|export> --project PATH
115
116
  maggie localization <extract|plan|generate|preview|validate|review|publish|stale|glossary> [options]
116
- maggie seo performance|images|sitemap|indexnow [options] (sitemap supports strict validate and agent-files)
117
+ maggie seo performance|images|sitemap|indexnow|social-cards|head-tags [options] (sitemap supports strict validate and agent-files)
117
118
  maggie feedback <collect|preview|submit|list> [options]
118
119
  maggie qa <start|record|summary|export> [options]
119
120
  maggie site-audit URL [--crawl] [--access-log FILE] [--require-sitemap-request] [--languages en-GB,es-MX,ja-JP] [--check-hreflang]
@@ -121,7 +122,7 @@ Usage:
121
122
  maggie site-audit URL --crawl --baseline FILE
122
123
  maggie browser-audit URL --browse PATH --output DIR --required SELECTOR [--sticky SELECTOR]
123
124
  maggie verification coverage --contract FILE --evidence FILE
124
- maggie ops audit|preflight|verify|lockfiles|seed-manifest|google-capabilities --project PATH
125
+ maggie ops audit|preflight|verify|lockfiles|seed-manifest|google-capabilities|favicon-check --project PATH
125
126
  maggie ops google-capabilities --project PATH --report FILE [--output FILE]
126
127
  maggie ops preflight --project PATH --write
127
128
 
@@ -302,8 +303,8 @@ function service(args) {
302
303
 
303
304
  function seo(args) {
304
305
  const command = args[0];
305
- const scripts = { performance: "maggie_performance.py", images: "maggie_images.py", sitemap: "maggie_sitemap.py", indexnow: "maggie_indexnow.py" };
306
- if (!scripts[command]) throw new Error("seo command must be performance, images, sitemap, or indexnow");
306
+ const scripts = { performance: "maggie_performance.py", images: "maggie_images.py", sitemap: "maggie_sitemap.py", indexnow: "maggie_indexnow.py", "social-cards": "maggie_social_cards.py", "head-tags": "maggie_head_tags.py" };
307
+ if (!scripts[command]) throw new Error("seo command must be performance, images, sitemap, indexnow, social-cards, or head-tags");
307
308
  workflowCli(scripts[command], args.slice(1));
308
309
  }
309
310
 
@@ -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
+ ```
@@ -38,6 +38,9 @@ maggie blog init --project . --base-path /our-blogs --confirm
38
38
  maggie blog ingest --project . --source local --input content/posts.json --confirm
39
39
  maggie blog validate --project .
40
40
  maggie blog check-gate --project .
41
+ # Database-backed hosts can supply a sanitized policy through an adapter.
42
+ maggie blog check-gate --project . \
43
+ --adapter-command '["node", "scripts/read-blog-settings.mjs"]'
41
44
  maggie blog approve --project . --slug example-post --actor reviewer --reason "reviewed" --confirm
42
45
  maggie blog publish --project . --slug example-post --actor owner --reason "approved" --confirm
43
46
  maggie blog sitemap --project .
@@ -51,7 +54,14 @@ explicit `approve` transition before `publish`. Stable `contentId` is the
51
54
  ingest identity and a published slug must not change during a rewrite.
52
55
  Search/sort views are not indexable; drafts never appear in public routes, RSS,
53
56
  or sitemap output. Provider keys remain server-side. Public publication and
54
- migrations always require explicit confirmation.
57
+ migrations always require explicit confirmation.
58
+
59
+ The adapter runs as an argv array without a shell, receives a versioned request
60
+ on stdin, and must print only an object containing
61
+ `defaultPostStatus`/`default_post_status`, `requireReview`/`require_review`,
62
+ and `autoPublishEnabled`/`auto_publish`. Provider stderr is suppressed. Use
63
+ `--settings-file` for a reviewed sanitized snapshot. A missing local store with
64
+ neither input fails closed.
55
65
 
56
66
  To initialize native front-end pages from the approved local UI guideline,
57
67
  run `maggie-design`:
@@ -219,6 +219,11 @@ ordinary, and blog routes, plus `sourceCoverage`. A host adapter must include
219
219
  code-rendered routes in the input or declare coverage incomplete; a zero
220
220
  database-row count is not evidence that no public routes exist.
221
221
 
222
+ When a host renders multiple shells, export a normalized head manifest and run
223
+ `maggie seo head-tags audit`. This is the reusable boundary for detecting icon
224
+ MIME drift, OG-image fallback drift, and verification-tag coverage; MaggieDash
225
+ does not infer these values from a route filename or from one sample page.
226
+
222
227
  The catalogue declares purpose, usage, placement, repeatability and layout
223
228
  limits, renderer-owned examples, and the shape of repeated entries. A repeat
224
229
  may contain an object (`title`, `body`, `href`, and so on), not just a count;
@@ -32,7 +32,8 @@ response bodies in it.
32
32
 
33
33
  When a release changes a visitor-facing HTML, CSS, component, or asset surface,
34
34
  the release preflight automatically requires both a passing icon inventory and
35
- rendered canary evidence. The canary must include screenshots, zero console or
35
+ rendered canary evidence. It considers unstaged, staged, and untracked
36
+ visitor-facing files. The canary must include screenshots, zero console or
36
37
  network errors, and zero placeholder matches. Query-driven routes belong in
37
38
  behavior/API checks, not static byte baselines.
38
39
 
@@ -161,6 +162,13 @@ python3 tools/clis/maggie_release.py /path/to/project \
161
162
  --base-url https://staging.example.com
162
163
  ```
163
164
 
165
+ If `.maggie/scenario-manifest.json` or `.maggie/qa-runs/` exists, the same
166
+ preflight consumes the latest matching `maggie qa` run for the requested
167
+ environment (and `--base-url`, when supplied). It blocks when a scenario is
168
+ not passed, has no browser-adapter evidence, or the matching run is missing.
169
+ Projects without scenario QA report `not-configured`; Maggie does not run the
170
+ browser itself.
171
+
164
172
  This aggregates deployment, migration, provider-health, schedule, analytics,
165
173
  service-fact, editorial approval, durable SEO/route/category evidence,
166
174
  MaggieDash schema compatibility, and live runtime security-header checks. It writes
@@ -34,6 +34,11 @@ The draft is written to `.maggie/feedback/`. It contains the Maggie version,
34
34
  skill, run ID, phase, error fingerprint, expected/actual result, reproduction
35
35
  steps, resolution, validation, and metadata-only screenshot references.
36
36
 
37
+ If a draft is marked fixed with `--fixed` (or the supplied run report says
38
+ `fixed`), both `--resolution` and `--validation` are required. Feedback text
39
+ also scrubs common POSIX/home/temp and Windows absolute paths by default while
40
+ preserving public URLs and route paths.
41
+
37
42
  ## Submit explicitly
38
43
 
39
44
  The CLI never submits automatically. After reviewing the draft:
@@ -42,6 +42,8 @@ python3 tools/clis/maggie_ops.py --project . audit
42
42
  python3 tools/clis/maggie_ops.py --project . preflight --write
43
43
  python3 tools/clis/maggie_ops.py --project . status
44
44
  python3 tools/clis/maggie_ops.py --project . verify
45
+ python3 tools/clis/maggie_ops.py --project . favicon-check \
46
+ --origin https://example.com
45
47
  python3 tools/clis/maggie_ops.py --project . record sitemap-match \
46
48
  --dry-run --quota-impact quota --idempotency-key match-2026-08-29-001
47
49
  ```
@@ -52,6 +54,12 @@ python3 tools/clis/maggie_ops.py --project . record sitemap-match \
52
54
  approved operation but does not call a remote provider; provider mutations
53
55
  remain behind the server-side API adapter and its execute gate.
54
56
 
57
+ `favicon-check` is a deployed behavioural check. It fetches `/favicon.ico`
58
+ and the declared `<link rel="icon">` (or an explicit `--declared-url`), then
59
+ requires HTTP 200, a readable square image, and an engine-supported ICO, PNG,
60
+ or GIF response. A logo-shaped source filename is only an unverified source
61
+ candidate; it is never evidence that the public favicon route works.
62
+
55
63
  If the user does not name a mode, inspect the project and propose the smallest
56
64
  mode that satisfies the request. Do not rebuild the public blog or change its
57
65
  framework just to add Ops.
@@ -69,6 +69,11 @@ maggie qa record --project . --run local-qa-001 \
69
69
  the test failed, a fix. After a fix, retest the original scenario and at least
70
70
  one adjacent scenario affected by the same surface.
71
71
 
72
+ A passing `test` or `retest` requires at least one `--evidence` reference from
73
+ the host browser adapter. The adapter owns console, network, authentication,
74
+ URL, viewport, and screenshot capture; this skill stores only a relative path,
75
+ URL reference, and hash when a local file exists.
76
+
72
77
  ## Gate and release evidence
73
78
 
74
79
  The run gate is `fail` when any scenario fails, `blocked` when there is no
@@ -88,6 +93,12 @@ Before calling a release Pass, also run the relevant build, accessibility,
88
93
  SEO, media, API, and deployment-canary checks. A green build, HTTP 200, source
89
94
  class, or one guest smoke is not browser QA evidence.
90
95
 
96
+ `maggie release` consumes the latest matching run from `.maggie/qa-runs/` when
97
+ the project has a scenario manifest or QA run directory. A matching run must
98
+ have a passed summary, passing scenarios, a passing final test/retest event,
99
+ and browser evidence for every scenario. No manifest or run reports an
100
+ explicit `not-configured` state.
101
+
91
102
  ## Privacy and feedback
92
103
 
93
104
  Do not put passwords, tokens, cookies, full private URLs, personal data,
@@ -181,13 +181,23 @@ maggie seo sitemap rollback --backup-manifest .maggie-sitemap-backups/<plan>/bac
181
181
  --public-dir public --confirm
182
182
 
183
183
  # Change-driven IndexNow: only pass URLs whose rendered content changed.
184
- maggie seo indexnow key-check --public-dir public --key-file <key>.txt --key <key>
184
+ maggie seo indexnow key-check --public-dir public --key-file <key>.txt --key <key> \
185
+ --key-url https://example.com/<key>.txt \
186
+ --wrong-key-url https://example.com/not-a-key.txt
185
187
  maggie seo indexnow plan --origin https://example.com \
186
188
  --changed-urls-file .maggie/changed-urls.json \
187
189
  --state-file .maggie/indexnow-state.json --key <key> \
188
190
  --output .maggie/indexnow-plan.json
189
191
  maggie seo indexnow submit --plan .maggie/indexnow-plan.json \
190
192
  --state-file .maggie/indexnow-state.json --confirm
193
+
194
+ # Resolve each page's og:image and inspect its image contract.
195
+ maggie seo social-cards audit --urls-file .maggie/public-urls.json \
196
+ --output .maggie/social-cards.json
197
+
198
+ # Compare normalized head metadata emitted by every rendered shell.
199
+ maggie seo head-tags audit --pages-file .maggie/rendered-head.json \
200
+ --output .maggie/head-tags.json
191
201
  ```
192
202
 
193
203
  Only confirmed image variants may enter `srcset`; `apply` requires an explicit
@@ -203,6 +213,24 @@ records accepted state only after HTTP 200/202. A 403/429/5xx or network error
203
213
  is retryable and must never make the editor save fail; do not retry unchanged
204
214
  URLs in a loop.
205
215
 
216
+ The complete IndexNow key gate checks both the local public file and the
217
+ deployed URL body. Add a wrong-key URL as a negative control when the host
218
+ route is expected to return 404; response bodies are never printed.
219
+
220
+ Social-card auditing uses a `300x157` minimum for a large summary card and
221
+ accepts JPEG, PNG, GIF, and ICO responses. It reports missing, unreachable,
222
+ undersized, unreadable, or unsupported images and warns when one source is
223
+ used for at least 75% of a sample of three or more pages. Page-specific
224
+ first-band images remain an editorial choice and should be declared by the
225
+ host manifest.
226
+
227
+ Head-tag auditing consumes a host-produced JSON manifest with `url`, `shell`,
228
+ and normalized `tags`. It compares structural declarations for icons,
229
+ `og:image`, image MIME type, verification presence, robots, and Twitter cards;
230
+ route-specific title and canonical values are intentionally excluded from
231
+ shell drift. It reports disagreements instead of guessing which shell is
232
+ correct.
233
+
206
234
  For a deterministic technical smoke check, run:
207
235
 
208
236
  ```bash
@@ -21,7 +21,8 @@ supporting relations for all suggested candidates. Inspect each service's
21
21
  `pages` after selection and validate the rendered links. The current selection
22
22
  path writes `canonical` for selected pages; multiple selections can violate
23
23
  the one-canonical invariant. Resolve roles and run validation before publishing.
24
- Variant role assignment and layout/URL conventions remain tracked in issue #27.
24
+ Variant role assignment and layout/URL conventions are enforced by the shared
25
+ market/locale route and reciprocal hreflang contract.
25
26
 
26
27
  ## Automatic memory hook
27
28
 
@@ -107,6 +108,14 @@ python3 tools/clis/maggie_service_booking.py category-audit \
107
108
  --project . --rendered-dir /tmp/category-rendered
108
109
 
109
110
  python3 tools/clis/maggie_service_booking.py category-context --project .
111
+
112
+ # Declare provider behavior, then validate it against the imported catalogue
113
+ # and a sanitized fixture before publishing service pages.
114
+ python3 tools/clis/maggie_service_booking.py capability-audit \
115
+ --project . \
116
+ --catalogue .maggie/booking/services.json \
117
+ --capabilities-file .maggie/booking/provider-capabilities.json \
118
+ --fixture .maggie/booking/fixtures/provider.json
110
119
  ```
111
120
 
112
121
  `import` creates the first catalogue. `sync` compares the newly imported
@@ -265,6 +274,15 @@ amounts silently. Include `Service` and `Offer` JSON-LD only from validated
265
274
  catalogue fields. Do not emit `Review`, `AggregateRating`, or medical claims
266
275
  unless verified source data is present.
267
276
 
277
+ Localized and market-specific variants use
278
+ `/services/<market>/<locale>/<variant-slug>/` as their canonical route. A
279
+ translation never reuses the source route. Published variants expose
280
+ reciprocal hreflang links only to published counterparts. Before generation,
281
+ each provider must declare `variantMode` (`native`, `derived-offers`,
282
+ `single-fallback`, or `blocked`), `priceSemantics`, and `stableIdConfidence`
283
+ in a machine-readable capability file. The capability audit requires a
284
+ sanitized fixture and fails closed on missing or contradictory evidence.
285
+
268
286
  ## AI category/page copy contract
269
287
 
270
288
  Read [`references/ai-service-copy-contract.md`](../../references/ai-service-copy-contract.md)
@@ -206,7 +206,7 @@ def detect(root: Path) -> dict:
206
206
  asset_contract = {
207
207
  "redirects": found("present", "redirects file") if any(name in names for name in {"_redirects", "redirects.json", "redirects.csv"}) else missing("no redirects manifest detected"),
208
208
  "security_headers": found("present", name) if (name := next((name for name in {"_headers", "headers.json", "vercel.json", "netlify.toml"} if name in names), None)) else missing("no deployment/header policy detected"),
209
- "favicon_or_brand": found("present", path) if (path := next((path for path in implementation_paths if any(token in path.lower() for token in {"favicon", "logo", "brand"}) and Path(path).suffix in {".svg", ".png", ".ico", ".webp"}), None)) else missing("no favicon or brand asset detected"),
209
+ "favicon_or_brand": {"status": "Unverified", "value": "source asset candidate found; verify the served icon with `maggie ops favicon-check`", "evidence": [path]} if (path := next((path for path in implementation_paths if any(token in path.lower() for token in {"favicon", "logo", "brand"}) and Path(path).suffix in {".svg", ".png", ".ico", ".webp"}), None)) else missing("no favicon or brand asset detected"),
210
210
  "social_image": found("present", path) if (path := next((path for path in implementation_paths if any(token in path.lower() for token in {"og", "social", "twitter"}) and Path(path).suffix in {".jpg", ".jpeg", ".png", ".webp"}), None)) else missing("no social image asset detected"),
211
211
  }
212
212
  content_model = {
@@ -9,7 +9,7 @@ import sys
9
9
  from pathlib import Path
10
10
 
11
11
  sys.path.insert(0, str(Path(__file__).resolve().parents[1] / "runtime"))
12
- from maggie_blog import BlogStore # noqa: E402
12
+ from maggie_blog import BlogStore, review_settings_from_adapter # noqa: E402
13
13
  from localization_runner import process_adapter
14
14
  from integration_state import integration_state # noqa: E402
15
15
 
@@ -27,7 +27,7 @@ def main() -> int:
27
27
  inspect = sub.add_parser("inspect"); inspect.add_argument("--project", type=Path, default=Path.cwd())
28
28
  ingest = sub.add_parser("ingest"); ingest.add_argument("--project", type=Path, default=Path.cwd()); ingest.add_argument("--input", type=Path, required=True); ingest.add_argument("--source", default="local"); ingest.add_argument("--confirm", action="store_true")
29
29
  validate = sub.add_parser("validate"); validate.add_argument("--project", type=Path, default=Path.cwd())
30
- gate = sub.add_parser("check-gate", help="report whether generated content requires review before publication"); gate.add_argument("--project", type=Path, default=Path.cwd())
30
+ gate = sub.add_parser("check-gate", help="report whether generated content requires review before publication"); gate.add_argument("--project", type=Path, default=Path.cwd()); gate.add_argument("--settings-file", type=Path, help="sanitized host settings JSON"); gate.add_argument("--adapter-command", help="trusted provider JSON argv array that prints sanitized settings JSON"); gate.add_argument("--timeout", type=int, default=120)
31
31
  approve = sub.add_parser("approve"); approve.add_argument("--project", type=Path, default=Path.cwd()); approve.add_argument("--slug", required=True); approve.add_argument("--actor", required=True); approve.add_argument("--reason", required=True); approve.add_argument("--confirm", action="store_true")
32
32
  publish = sub.add_parser("publish"); publish.add_argument("--project", type=Path, default=Path.cwd()); publish.add_argument("--slug", required=True); publish.add_argument("--actor", required=True); publish.add_argument("--reason", required=True); publish.add_argument("--confirm", action="store_true")
33
33
  sitemap = sub.add_parser("sitemap"); sitemap.add_argument("--project", type=Path, default=Path.cwd())
@@ -39,6 +39,16 @@ def main() -> int:
39
39
  result = integration_state(configured=args.configured, consent_required=args.consent_required, consent=args.consent, authorized=args.authorized, error=args.error)
40
40
  print(json.dumps(result, indent=2, ensure_ascii=False))
41
41
  return 0 if result["status"] != "error" else 1
42
+ if args.command == "check-gate" and (args.settings_file or args.adapter_command):
43
+ try:
44
+ adapter_command = json.loads(args.adapter_command) if args.adapter_command else None
45
+ settings, source = review_settings_from_adapter(args.project.resolve(), args.settings_file.resolve() if args.settings_file else None, adapter_command, args.timeout)
46
+ result = BlogStore.review_gate(settings)
47
+ result["source"] = source
48
+ except (OSError, ValueError, json.JSONDecodeError) as error:
49
+ print(f"maggie-blog: {error}", file=sys.stderr); return 1
50
+ print(json.dumps(result, indent=2, ensure_ascii=False))
51
+ return 0 if result.get("passed") else 1
42
52
  store = BlogStore(args.project.resolve())
43
53
  if args.command in {"init", "ingest", "publish", "approve", "rollback", "translate-pending"} and not args.confirm:
44
54
  print("CONFIRMATION_REQUIRED: rerun with --confirm", file=sys.stderr); return 2
@@ -20,6 +20,12 @@ from urllib.request import Request, urlopen
20
20
 
21
21
  DEFAULT_ENDPOINT = "https://feedback.noblox.app/api/feedback"
22
22
  SECRET_RE = re.compile(r"(?i)(bearer\s+|(?:api[_-]?key|token|secret|password|npm_config[^=]*|authorization)\s*[=:]\s*)[^\s,;]+")
23
+ LOCAL_PATH_RE = re.compile(
24
+ r"(?<![A-Za-z0-9])(?:~/(?:[^\s,;:)\]}]+/)*[^\s,;:)\]}]+|"
25
+ r"/(?:home|Users|private|tmp|var|mnt|opt|root|srv|workspace|workspaces)(?:/[^\s,;:)\]}]+)+|"
26
+ r"[A-Za-z]:[\\/]+(?:[^\\/\s,;:)\]}]+[\\/]+)*[^\\/\s,;:)\]}]+|"
27
+ r"\\\\[^\\/\s,;:)\]}]+(?:[\\/]+[^\\/\s,;:)\]}]+)+)"
28
+ )
23
29
  VERSION_RE = re.compile(r"\d+\.\d+\.\d+(?:[-+][A-Za-z0-9.-]+)?")
24
30
  ACKNOWLEDGEMENT_KEYS = ("status", "feedbackId", "requestId")
25
31
  REPO_ROOT = Path(__file__).resolve().parents[2]
@@ -30,7 +36,8 @@ def now() -> str:
30
36
 
31
37
 
32
38
  def safe_text(value: object) -> str:
33
- return SECRET_RE.sub(r"\1[REDACTED]", str(value or "")).strip()
39
+ value = SECRET_RE.sub(r"\1[REDACTED]", str(value or ""))
40
+ return LOCAL_PATH_RE.sub("[LOCAL_PATH_REDACTED]", value).strip()
34
41
 
35
42
 
36
43
  def project_fingerprint(project: Path) -> str:
@@ -90,6 +97,11 @@ def collect(args: argparse.Namespace) -> int:
90
97
  skill = args.skill or report.get("skill") or report.get("workflow") or "unknown"
91
98
  run_id = args.run_id or report.get("runId") or report.get("jobId") or ""
92
99
  actual = args.actual or report.get("actual") or report.get("error") or report.get("message") or ""
100
+ fixed = bool(args.fixed or report.get("fixed") is True or report.get("status") == "fixed")
101
+ resolution = safe_text(args.resolution or report.get("resolution"))
102
+ validation = safe_text(args.validation or report.get("validation"))
103
+ if fixed and (not resolution or not validation):
104
+ raise ValueError("fixed feedback requires non-empty --resolution and --validation")
93
105
  context = {"projectFingerprint": project_fingerprint(project)}
94
106
  if args.allow_project_context and args.context_note:
95
107
  context["note"] = safe_text(args.context_note)
@@ -103,15 +115,15 @@ def collect(args: argparse.Namespace) -> int:
103
115
  "skill": skill,
104
116
  "runId": run_id,
105
117
  "phase": args.phase or report.get("phase") or "",
106
- "status": report.get("status") or ("fixed" if args.fixed else "reported"),
118
+ "status": report.get("status") or ("fixed" if fixed else "reported"),
107
119
  "summary": safe_text(args.summary),
108
120
  "expected": safe_text(args.expected),
109
121
  "actual": safe_text(actual),
110
122
  "errorFingerprint": safe_text(args.error_fingerprint or report.get("errorFingerprint") or ""),
111
123
  "stepsToReproduce": [safe_text(step) for step in args.reproduce],
112
- "fixed": bool(args.fixed),
113
- "resolution": safe_text(args.resolution),
114
- "validation": safe_text(args.validation),
124
+ "fixed": fixed,
125
+ "resolution": resolution,
126
+ "validation": validation,
115
127
  "attachments": [attachment(value) for value in args.screenshot],
116
128
  "environment": {"os": platform.system().lower(), "python": platform.python_version()},
117
129
  "privacy": {"secretsRedacted": True, "projectContextAllowed": bool(args.allow_project_context)},
@@ -0,0 +1,35 @@
1
+ #!/usr/bin/env python3
2
+ """Compare normalized rendered head tags across site shells."""
3
+
4
+ from __future__ import annotations
5
+
6
+ import argparse
7
+ import json
8
+ import sys
9
+ from pathlib import Path
10
+
11
+ sys.path.insert(0, str(Path(__file__).resolve().parents[1] / "runtime"))
12
+ from maggie_head_tags import audit_head_tags, read_pages # noqa: E402
13
+
14
+
15
+ def main() -> int:
16
+ parser = argparse.ArgumentParser(prog="maggie seo head-tags")
17
+ sub = parser.add_subparsers(dest="command", required=True)
18
+ audit = sub.add_parser("audit", help="report head-tag disagreements across rendered shells")
19
+ audit.add_argument("--pages-file", type=Path, required=True, help="JSON page manifest with url, shell, and tags")
20
+ audit.add_argument("--output")
21
+ args = parser.parse_args()
22
+ try:
23
+ result = audit_head_tags(read_pages(args.pages_file))
24
+ if args.output:
25
+ output = Path(args.output).resolve(); output.parent.mkdir(parents=True, exist_ok=True)
26
+ output.write_text(json.dumps(result, indent=2, ensure_ascii=False) + "\n", encoding="utf-8")
27
+ print(json.dumps(result, indent=2, ensure_ascii=False))
28
+ return 0 if result["passed"] else 1
29
+ except (OSError, ValueError, json.JSONDecodeError) as error:
30
+ print(f"maggie-head-tags: {error}", file=sys.stderr)
31
+ return 1
32
+
33
+
34
+ if __name__ == "__main__":
35
+ raise SystemExit(main())
@@ -9,7 +9,7 @@ from pathlib import Path
9
9
  import sys
10
10
 
11
11
  sys.path.insert(0, str(Path(__file__).resolve().parents[1] / "runtime"))
12
- from maggie_indexnow import check_key_file, plan_indexnow, submit_plan # noqa: E402
12
+ from maggie_indexnow import check_key_file, check_key_http, plan_indexnow, submit_plan # noqa: E402
13
13
 
14
14
 
15
15
  def read_json(path: Path, default: object) -> object:
@@ -26,8 +26,8 @@ def write_json(path: Path, value: object) -> None:
26
26
  def main() -> int:
27
27
  parser = argparse.ArgumentParser(prog="maggie seo indexnow")
28
28
  sub = parser.add_subparsers(dest="command", required=True)
29
- key_check = sub.add_parser("key-check", help="verify the public ownership key file")
30
- key_check.add_argument("--public-dir", required=True); key_check.add_argument("--key-file", required=True); key_check.add_argument("--key", required=True)
29
+ key_check = sub.add_parser("key-check", help="verify local and deployed public ownership key evidence")
30
+ key_check.add_argument("--public-dir", required=True); key_check.add_argument("--key-file", required=True); key_check.add_argument("--key", required=True); key_check.add_argument("--key-url", help="deployed key URL to fetch"); key_check.add_argument("--wrong-key-url", help="negative-control URL expected to return HTTP 404"); key_check.add_argument("--timeout", type=int, default=15)
31
31
  plan = sub.add_parser("plan", help="hold back recently accepted URLs and create a submission plan")
32
32
  plan.add_argument("--origin", required=True); plan.add_argument("--changed-urls-file", required=True); plan.add_argument("--state-file", required=True); plan.add_argument("--key", required=True); plan.add_argument("--key-location"); plan.add_argument("--guard-hours", type=int, default=24); plan.add_argument("--now"); plan.add_argument("--output", required=True)
33
33
  submit = sub.add_parser("submit", help="submit an approved plan; provider failure never updates accepted state")
@@ -36,6 +36,9 @@ def main() -> int:
36
36
  try:
37
37
  if args.command == "key-check":
38
38
  result = check_key_file(Path(args.public_dir).resolve(), args.key_file, args.key)
39
+ if args.key_url:
40
+ result["http"] = check_key_http(args.key_url, args.key, args.wrong_key_url, args.timeout)
41
+ if result["http"]["status"] != "pass": result["status"] = "fail"
39
42
  print(json.dumps(result, indent=2)); return 0 if result["status"] == "pass" else 1
40
43
  if args.command == "plan":
41
44
  changed = read_json(Path(args.changed_urls_file), [])
@@ -15,6 +15,7 @@ from seed_evidence import validate_manifest
15
15
  from agent_runtime import preflight as agent_preflight
16
16
  from restart_gate import validate as validate_restart_gate
17
17
  from google_capabilities import validate_report as validate_google_capability_report
18
+ from maggie_favicon import check_favicon
18
19
 
19
20
 
20
21
  REQUIRED_ROUTES = (
@@ -181,6 +182,12 @@ def command_google_capabilities(args: argparse.Namespace) -> int:
181
182
  return 0 if result["passed"] else 1
182
183
 
183
184
 
185
+ def command_favicon_check(args: argparse.Namespace) -> int:
186
+ result = check_favicon(args.origin, args.declared_url, args.timeout)
187
+ print(json.dumps(result, indent=2, ensure_ascii=False))
188
+ return 0 if result["passed"] else 1
189
+
190
+
184
191
  def main() -> int:
185
192
  parser = argparse.ArgumentParser(description=__doc__)
186
193
  parser.add_argument("--project", default=".")
@@ -203,6 +210,11 @@ def main() -> int:
203
210
  google.add_argument("--report", required=True)
204
211
  google.add_argument("--output")
205
212
  google.set_defaults(func=command_google_capabilities)
213
+ favicon = sub.add_parser("favicon-check", help="fetch /favicon.ico and the declared icon to verify behaviour")
214
+ favicon.add_argument("--origin", required=True)
215
+ favicon.add_argument("--declared-url")
216
+ favicon.add_argument("--timeout", type=int, default=15)
217
+ favicon.set_defaults(func=command_favicon_check)
206
218
  record = sub.add_parser("record", help="record an explicitly authorised operation")
207
219
  record.add_argument("operation", choices=("sync", "sitemap-match", "rewrite-queue", "rewrite-approve", "publish", "migration"))
208
220
  record.add_argument("--dry-run", action="store_true")