@topy-ai/maggie 0.7.13 → 0.7.15

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 CHANGED
@@ -58,7 +58,31 @@ Then invoke the installed skills from your coding agent, for example:
58
58
 
59
59
  Maggie keeps the existing project foundation and asks for decisions before
60
60
  shared routes, analytics, or publishing boundaries change. The current
61
- package ships 18 installable skills and a local-first MaggieDash foundation.
61
+ package ships 19 installable skills and a local-first MaggieDash foundation.
62
+
63
+ For repeatable browser QA, create a project-owned scenario manifest and record
64
+ the test, fix, and retest lifecycle:
65
+
66
+ ```bash
67
+ maggie qa start --project . \
68
+ --scenario-file .maggie/scenario-manifest.json \
69
+ --environment local --base-url http://localhost:4321
70
+ maggie qa record --project . --run <run-id> \
71
+ --scenario HOME-001 --phase test --status pass \
72
+ --summary "Homepage smoke passed" --evidence .maggie/qa/home.png
73
+ maggie qa summary --project . --run <run-id>
74
+ ```
75
+
76
+ The QA workflow stores secret-free run state under `.maggie/qa-runs/` and
77
+ keeps project-specific scenarios and evidence outside the npm package.
78
+
79
+ The current package also includes reusable safeguards from the latest feedback
80
+ review: `maggie dash api-contract` checks declared request and 2xx response
81
+ shapes; `maggie blog check-gate`/`approve` enforces review before publish;
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.
62
86
 
63
87
  For Google integrations, validate a redacted provider matrix before reporting
64
88
  access. The command fails closed on unknown scopes, missing Ads prerequisites,
@@ -105,7 +129,8 @@ maggie dash install | init | status | migrate | cms ...
105
129
  maggie dash transition ... # explicit content approval transition
106
130
  maggie dash variant ... # service variant create/review/preview/publish
107
131
  maggie dash sections ... # field fan-out, locale, binding, media, copy, identity keys
108
- maggie dash inventory ... # disjoint published-page inventory
132
+ maggie dash inventory ... # disjoint renderer/page-kind inventory + coverage
133
+ maggie dash api-contract ... # declared API body and 2xx response contract
109
134
  maggie agent-content write ... # host-authorized, origin-bound content bridge
110
135
  maggie verification coverage ... # changed surface/locale evidence gate
111
136
  maggie clone ... # authorized homepage capture
@@ -114,11 +139,13 @@ maggie clone-to-template ... # URL → validated marketplace template
114
139
  maggie marketplace ... # catalog and on-demand template workflow
115
140
  maggie memory ... # confirmed preferences and lessons
116
141
  maggie feedback ... # redact, preview, submit, list
142
+ maggie qa ... # scenario browser QA, fix/retest, release gate
117
143
  maggie localization ... # plan, validate, review, publish, stale
118
144
  maggie service ... # import, sync, generate, validate
119
145
  maggie seo performance ... # sampled PageSpeed/CWV report and baseline
120
146
  maggie seo images ... # inventory, variants, confirmation, validate
121
147
  maggie seo sitemap ... # typed/semantic plan, agent-files, apply, rollback
148
+ maggie seo indexnow ... # changed URLs, key check, retry-safe 24h guard
122
149
  maggie deployment | migration | release | analytics | schedule
123
150
  maggie migration identity --identity-file FILE [--expected-file FILE]
124
151
  maggie deployment canary --asset URL=SHA256 --render-report report.json
@@ -220,8 +247,8 @@ artifact schemas.
220
247
  Recommended upgrade sequence for the current release:
221
248
 
222
249
  ```bash
223
- npx @topy-ai/maggie@0.7.13 update --project . --force
224
- npx @topy-ai/maggie@0.7.13 cleanup --project .
250
+ npx @topy-ai/maggie@0.7.15 update --project . --force
251
+ npx @topy-ai/maggie@0.7.15 cleanup --project .
225
252
  ```
226
253
 
227
254
  Maintainers should pass npm credentials through the repository helper, never
@@ -231,7 +258,12 @@ as a command-line argument:
231
258
  node scripts/publish-npm.mjs --maggie-env-file ../.env
232
259
  ```
233
260
 
234
- The 0.7.13 workflow adds field-aware section fan-out and locale coverage,
261
+ The 0.7.15 workflow adds API contracts, blog review gates, sitemap freshness,
262
+ change-driven IndexNow, and page-kind/source-coverage inventory. It also keeps
263
+ the general `maggie-qa-workflow` skill and `maggie qa`
264
+ CLI for scenario manifests, secret-free browser evidence metadata, test/fix/
265
+ retest lifecycle, adjacent regression checks, and explicit release gates. The
266
+ 0.7.13 workflow adds field-aware section fan-out and locale coverage,
235
267
  sibling-copy/media checks, disjoint page inventory, binding validation,
236
268
  idempotency and database-target identity gates, full W3C sitemap lastmod
237
269
  validation, and the accepted `X-Robots-Tag: noindex` response contract. The
@@ -484,6 +516,7 @@ python3 tools/clis/maggie_design.py rebrand \
484
516
  | `maggie-memory` | Persist confirmed preferences, conventions, lessons, and errors |
485
517
  | `maggie-content-localization` | Manage locale-aware translation, review, provenance, stale state, and publication gates |
486
518
  | `maggie-feedback` | Collect redacted feedback drafts and explicitly submit them to the NoBlox feedback endpoint |
519
+ | `maggie-qa-workflow` | Run scenario-based browser QA with evidence, fix/retest lifecycle, and release gates |
487
520
  | `maggie-auth-reference` | Generate and validate traditional email/password auth with secure server-side sessions |
488
521
  | `maggie-blog` | Run a provider-neutral blog lifecycle with stable identity, topics, feeds, settings, and rollback |
489
522
 
@@ -528,12 +561,13 @@ repairs with `maggie dash sections validate`, `fanout-validate`,
528
561
  `maggie dash inventory` to classify published pages once into disjoint kinds.
529
562
  See the [quality contract examples](https://github.com/TOPY-AI-LTD/ai-cmo-skills/blob/main/contracts/maggiedash/quality-contracts.md).
530
563
 
531
- The package includes all 18 installable skills: `maggie-blog-bootstrap`,
564
+ The package includes all 19 installable skills: `maggie-blog-bootstrap`,
532
565
  `maggie-dash`, `maggie-clone`, `maggie-clone-to-template`, `maggie-marketplace`,
533
566
  `maggie-template`, `maggie-design`, `maggie-ops`, `maggie-deployment`,
534
567
  `maggie-project-context`, `maggie-seo-geo`, `maggie-social-share`,
535
568
  `maggie-service-booking`, `maggie-memory`, `maggie-content-localization`,
536
- `maggie-feedback`, `maggie-auth-reference`, and `maggie-blog`. Use the stable
569
+ `maggie-feedback`, `maggie-qa-workflow`, `maggie-auth-reference`, and
570
+ `maggie-blog`. Use the stable
537
571
  commands below after installation:
538
572
 
539
573
  ```bash
@@ -543,6 +577,8 @@ maggie design author --project . --route /about --purpose "Explain our approach"
543
577
  maggie blog init --project . --confirm
544
578
  maggie blog ingest --project . --source local --input content/posts.json --confirm
545
579
  maggie blog validate --project .
580
+ maggie blog check-gate --project .
581
+ maggie blog approve --project . --slug example-post --actor reviewer --reason "reviewed" --confirm
546
582
  maggie blog sitemap --project .
547
583
  ```
548
584
 
package/README.zh-TW.md CHANGED
@@ -2,13 +2,13 @@
2
2
 
3
3
  [English README](README.md) · [完整繁體中文 README](https://github.com/TOPY-AI-LTD/ai-cmo-skills/blob/main/README.zh-TW.md)
4
4
 
5
- `@topy-ai/maggie` 提供 18 個可安裝的 AI website workflow skills,支援
5
+ `@topy-ai/maggie` 提供 19 個可安裝的 AI website workflow skills,支援
6
6
  Codex、Claude Code 與相容的 coding agents。
7
7
 
8
8
  ## 安裝
9
9
 
10
10
  ```bash
11
- npx @topy-ai/maggie@0.7.13 init --agent all
11
+ npx @topy-ai/maggie@0.7.15 init --agent all
12
12
  npx @topy-ai/maggie doctor --project .
13
13
  ```
14
14
 
@@ -25,6 +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.15 也加入 API schema contract、blog review gate、sitemap freshness
29
+ warning、change-driven IndexNow 與 page-kind/source-coverage inventory。
30
+
28
31
  MaggieDash 也提供穩定 section identity、可重用 section arrangement、機器可讀
29
32
  section registry、短期 agent content bridge,以及 translation out-of-band write
30
33
  後的 restart gate。`maggie doctor` 會比較 install manifest 與磁碟上的實際 skills。
@@ -50,8 +53,13 @@ maggie dash inventory --pages-file .maggie/published-pages.json
50
53
  # Migration target identity (never prints or stores a database URL)
51
54
  maggie migration identity --identity-file .maggie/db-identity.json \
52
55
  --expected-file .maggie/service-db-identity.json
56
+
57
+ # Scenario browser QA
58
+ maggie qa start --project . --scenario-file .maggie/scenario-manifest.json \
59
+ --environment local --base-url http://localhost:4321
60
+ maggie qa summary --project . --run <run-id>
53
61
  ```
54
62
 
55
- 完整中文說明、18 個 skills 清單和 roadmap:
63
+ 完整中文說明、19 個 skills 清單和 roadmap:
56
64
  [繁體中文 README](https://github.com/TOPY-AI-LTD/ai-cmo-skills/blob/main/README.zh-TW.md)
57
65
  · [Roadmap](https://github.com/TOPY-AI-LTD/ai-cmo-skills/blob/main/ROADMAP.md)
package/bin/maggie.js CHANGED
@@ -59,7 +59,8 @@ Usage:
59
59
  maggie bootstrap interview [project]
60
60
  maggie dash init|install|status|migrate|transition|variant|cms --project PATH [options]
61
61
  maggie dash sections <validate|prompt|keys|remap-translations|fanout-validate|locale-validate|variant-copy-validate|media-validate|bindings-validate|idempotency-validate|reconcile> [options]
62
- maggie dash inventory --pages-file FILE
62
+ maggie dash inventory --pages-file FILE [--require-page-kinds] [--require-source-coverage]
63
+ maggie dash api-contract --spec FILE
63
64
  maggie dash components-audit --bindings-file FILE --sections-file FILE --pages-file FILE
64
65
  maggie dash status --project PATH
65
66
  maggie dash migrate --project PATH --confirm
@@ -92,7 +93,7 @@ Usage:
92
93
  maggie design step <job-id> --step NAME --evidence FILE
93
94
  maggie auth reference --project PATH --confirm
94
95
  maggie auth check --project PATH [--production]
95
- maggie blog init|inspect|ingest|validate|publish|sitemap|settings|rollback|integration-state
96
+ maggie blog init|inspect|ingest|validate|check-gate|approve|publish|sitemap|settings|rollback|integration-state
96
97
  maggie design status <job-id>
97
98
  maggie service import <provider-url> --project PATH
98
99
  maggie service sync <provider-url> --project PATH
@@ -112,8 +113,9 @@ Usage:
112
113
  maggie api lifecycle --project PATH [--execute --allow-quota]
113
114
  maggie memory <init|list|search|context|add|record-error|transition|export> --project PATH
114
115
  maggie localization <extract|plan|generate|preview|validate|review|publish|stale|glossary> [options]
115
- maggie seo performance|images|sitemap [options] (sitemap supports strict validate and agent-files)
116
+ maggie seo performance|images|sitemap|indexnow [options] (sitemap supports strict validate and agent-files)
116
117
  maggie feedback <collect|preview|submit|list> [options]
118
+ maggie qa <start|record|summary|export> [options]
117
119
  maggie site-audit URL [--crawl] [--access-log FILE] [--require-sitemap-request] [--languages en-GB,es-MX,ja-JP] [--check-hreflang]
118
120
  maggie site-audit URL --crawl --save-baseline FILE --reviewer NAME
119
121
  maggie site-audit URL --crawl --baseline FILE
@@ -300,8 +302,8 @@ function service(args) {
300
302
 
301
303
  function seo(args) {
302
304
  const command = args[0];
303
- const scripts = { performance: "maggie_performance.py", images: "maggie_images.py", sitemap: "maggie_sitemap.py" };
304
- if (!scripts[command]) throw new Error("seo command must be performance, images, or sitemap");
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");
305
307
  workflowCli(scripts[command], args.slice(1));
306
308
  }
307
309
 
@@ -406,6 +408,7 @@ try {
406
408
  else if (command === "memory") workflowCli("maggie_memory.py", args);
407
409
  else if (command === "localization") workflowCli("maggie_localization.py", args);
408
410
  else if (command === "feedback") workflowCli("maggie_feedback.py", args);
411
+ else if (command === "qa") workflowCli("maggie_qa_workflow.py", args);
409
412
  else if (command === "site-audit") workflowCli("site_audit.py", args);
410
413
  else if (command === "browser-audit") workflowCli("maggie_browser_audit.py", args);
411
414
  else if (command === "verification") workflowCli("maggie_verification.py", args);
@@ -20,6 +20,7 @@ Skills are the agent-facing workflows. They compose with the tools in
20
20
  | `maggie-memory` | Persist confirmed preferences, project conventions, lessons, and error history across skill runs | local `.maggie/memory/` state |
21
21
  | `maggie-content-localization` | Plan, validate, review, publish, and age localized content with market, locale, provenance, and translation safeguards | localization contract, locale CLI |
22
22
  | `maggie-feedback` | Collect, redact, review, and explicitly submit feedback from skill runs | feedback CLI, hosted endpoint, GitHub Issue Forms |
23
+ | `maggie-qa-workflow` | Run scenario-based browser QA with evidence, fix/retest lifecycle, and release gates | browser adapter, QA workflow CLI, scenario manifest |
23
24
 
24
25
  Read only the selected skill and its linked references for a task. Do not load
25
26
  all skills as one undifferentiated prompt.
@@ -58,6 +58,10 @@
58
58
  "name": "maggie-project-context",
59
59
  "description": "Sync a site's safe AI CMO Project, selected brand voice, site settings, and CTA context into a local generated context file. Use when connecting a vibe-coded blog or existing website to AI CMO, refreshing brand context, or diagnosing missing CTA/project data."
60
60
  },
61
+ {
62
+ "name": "maggie-qa-workflow",
63
+ "description": "Run scenario-based browser QA with explicit evidence, fix/retest lifecycle, and release-gate decisions for web projects."
64
+ },
61
65
  {
62
66
  "name": "maggie-seo-geo",
63
67
  "description": "Plan, audit, create, rewrite, and measure content for AI CMO's paid SEO and GEO workflow. Use for topic opportunities, AI visibility, technical SEO, extractable article structure, sitemap-based rewrites, GSC readback, or SEO/GEO client reports."
@@ -27,7 +27,7 @@ locale/revision tasks are skipped; failed tasks retry from checkpoints. Run one
27
27
  worker per project. Outputs stay draft and non-indexable. Existing host HTTP
28
28
  and scheduler integrations still need separate implementation and tests.
29
29
 
30
- Use the stable CLI to initialize a local blog contract, ingest versioned local
30
+ Use the stable CLI to initialize a local blog contract, ingest versioned local
31
31
  content, validate lifecycle invariants, publish with an actor and reason, and
32
32
  generate route/feed artifacts. Read the host framework and database contract
33
33
  before adding public routes. The host project owns rendering, persistence
@@ -35,19 +35,23 @@ credentials, and deployment; this skill owns normalized blog semantics.
35
35
 
36
36
  ```bash
37
37
  maggie blog init --project . --base-path /our-blogs --confirm
38
- maggie blog ingest --project . --source local --input content/posts.json --confirm
39
- maggie blog validate --project .
40
- maggie blog publish --project . --slug example-post --actor owner --reason "approved" --confirm
38
+ maggie blog ingest --project . --source local --input content/posts.json --confirm
39
+ maggie blog validate --project .
40
+ maggie blog check-gate --project .
41
+ maggie blog approve --project . --slug example-post --actor reviewer --reason "reviewed" --confirm
42
+ maggie blog publish --project . --slug example-post --actor owner --reason "approved" --confirm
41
43
  maggie blog sitemap --project .
42
44
  maggie blog settings --project .
43
45
  maggie blog rollback --project . --confirm
44
46
  ```
45
47
 
46
- Posts are draft-first. Stable `contentId` is the ingest identity and a
47
- published slug must not change during a rewrite. Search/sort views are not
48
- indexable; drafts never appear in public routes, RSS, or sitemap output.
49
- Provider keys remain server-side. Public publication and migrations always
50
- require explicit confirmation.
48
+ Posts are draft-first. `maggie blog check-gate` reports the publication policy;
49
+ the default requires `requireReview=true`, disables auto-publish, and forces an
50
+ explicit `approve` transition before `publish`. Stable `contentId` is the
51
+ ingest identity and a published slug must not change during a rewrite.
52
+ Search/sort views are not indexable; drafts never appear in public routes, RSS,
53
+ or sitemap output. Provider keys remain server-side. Public publication and
54
+ migrations always require explicit confirmation.
51
55
 
52
56
  To initialize native front-end pages from the approved local UI guideline,
53
57
  run `maggie-design`:
@@ -200,10 +200,25 @@ maggie dash sections bindings-validate --sections-file .maggie/sections.json \
200
200
  --references-file .maggie/reference-inventory.json
201
201
  maggie dash sections idempotency-validate --contract-file .maggie/reconcile-contract.json
202
202
 
203
- # Classify every published page exactly once:
204
- maggie dash inventory --pages-file .maggie/published-pages.json
203
+ # Validate API body/response shapes before a client writes against an endpoint:
204
+ maggie dash api-contract --spec .maggie/openapi.json
205
+
206
+ # Classify every published page exactly once. `pageKind` describes the public
207
+ # page family; `kind`/`rendererKind` describes how it is rendered. Keep these
208
+ # axes separate and require explicit source coverage when database rows do not
209
+ # contain code-rendered routes:
210
+ maggie dash inventory --pages-file .maggie/published-pages.json \
211
+ --require-page-kinds --require-source-coverage
205
212
  ```
206
213
 
214
+ The API contract checker fails closed when a body or 2xx response schema is
215
+ missing. Intentional empty bodies must be explicit (`x-maggie-empty-request-body`
216
+ or `x-maggie-empty-response`) so a consumer does not guess field names.
217
+ Inventory reports `pageKindCounts` for service, variant, category hub,
218
+ ordinary, and blog routes, plus `sourceCoverage`. A host adapter must include
219
+ code-rendered routes in the input or declare coverage incomplete; a zero
220
+ database-row count is not evidence that no public routes exist.
221
+
207
222
  The catalogue declares purpose, usage, placement, repeatability and layout
208
223
  limits, renderer-owned examples, and the shape of repeated entries. A repeat
209
224
  may contain an object (`title`, `body`, `href`, and so on), not just a count;
@@ -0,0 +1,102 @@
1
+ ---
2
+ name: maggie-qa-workflow
3
+ description: Run scenario-based browser QA with explicit evidence, fix/retest lifecycle, and release-gate decisions for web projects.
4
+ metadata:
5
+ version: 1.0.0
6
+ ---
7
+
8
+ # Maggie QA Workflow
9
+
10
+ Use this skill when a web project needs repeatable user-scenario testing that
11
+ connects browser evidence to a fix, a same-scenario retest, adjacent regression
12
+ checks, and an explicit Pass/Blocked/Fail release decision. It is framework
13
+ neutral and does not replace the project's browser-control or test tools.
14
+ Follow the shared [memory hook](../../references/memory-hook.md) before and
15
+ after the run; memory and feedback remain bounded by the privacy rules below.
16
+
17
+ ## Prepare a scenario manifest
18
+
19
+ Create `.maggie/scenario-manifest.json` or pass another JSON file with a
20
+ non-empty `scenarios` array. Each scenario needs a unique `id`; useful fields
21
+ include `title`, `group`, `priority`, `routes`, `persona`, `auth`, `browser`,
22
+ and `viewport`. Keep project-specific scenarios in the project; do not ship
23
+ them in the Maggie package.
24
+
25
+ ## Start and record a run
26
+
27
+ The CLI stores only secret-free run state under `.maggie/qa-runs/`. Screenshots
28
+ and logs stay in the project and are represented by relative paths and hashes.
29
+ Use a real browser adapter for visible interaction, computed layout,
30
+ console/network evidence, final URLs, and response status:
31
+
32
+ ```bash
33
+ maggie qa start --project . \
34
+ --scenario-file .maggie/scenario-manifest.json \
35
+ --run-id local-qa-001 --environment local \
36
+ --base-url http://localhost:4321 --browser chrome \
37
+ --commit "$(git rev-parse --short HEAD)"
38
+
39
+ maggie qa record --project . --run local-qa-001 \
40
+ --scenario HOME-001 --phase test --status pass \
41
+ --summary "Homepage navigation and consent passed" \
42
+ --evidence .maggie/qa-runs/local-qa-001/home.png
43
+ ```
44
+
45
+ For a failure, record expected behavior, actual behavior, and a stable error
46
+ fingerprint. A failed scenario cannot be silently changed to Pass:
47
+
48
+ ```bash
49
+ maggie qa record --project . --run local-qa-001 \
50
+ --scenario SEARCH-001 --phase test --status fail \
51
+ --summary "Search dialog did not open" \
52
+ --expected "Selecting a result opens its detail page" \
53
+ --actual "The click leaves the dialog open" \
54
+ --error-fingerprint search-dialog-click-stale \
55
+ --evidence .maggie/qa-runs/local-qa-001/search-console.txt
56
+
57
+ maggie qa record --project . --run local-qa-001 \
58
+ --scenario SEARCH-001 --phase fix \
59
+ --resolution "Guarded the dialog transition after the selected result is resolved" \
60
+ --validation "focused browser check and project build passed"
61
+
62
+ maggie qa record --project . --run local-qa-001 \
63
+ --scenario SEARCH-001 --phase retest --status pass \
64
+ --summary "Search result opens the detail page" \
65
+ --evidence .maggie/qa-runs/local-qa-001/search-retest.png
66
+ ```
67
+
68
+ `fix` must follow a recorded failure. `retest` must follow a test and, when
69
+ the test failed, a fix. After a fix, retest the original scenario and at least
70
+ one adjacent scenario affected by the same surface.
71
+
72
+ ## Gate and release evidence
73
+
74
+ The run gate is `fail` when any scenario fails, `blocked` when there is no
75
+ failure but pending/blocked/inconclusive scenarios remain, and `pass` only
76
+ when every scenario passes. Missing credentials, unsafe fixtures, a real
77
+ mobile viewport, a payment sandbox, or a fault-injection environment are
78
+ explicit blockers; never guess around them.
79
+
80
+ ```bash
81
+ maggie qa summary --project . --run local-qa-001
82
+ maggie qa summary --project . --run local-qa-001 --format markdown
83
+ maggie qa export --project . --run local-qa-001 \
84
+ --output docs/qa-runs/local-qa-001.md
85
+ ```
86
+
87
+ Before calling a release Pass, also run the relevant build, accessibility,
88
+ SEO, media, API, and deployment-canary checks. A green build, HTTP 200, source
89
+ class, or one guest smoke is not browser QA evidence.
90
+
91
+ ## Privacy and feedback
92
+
93
+ Do not put passwords, tokens, cookies, full private URLs, personal data,
94
+ provider response bodies, or secrets in run state, screenshots, or exported
95
+ reports. When the same workflow defect is reusable across projects, create a
96
+ privacy-safe draft through `maggie-feedback`; submitting it remains an explicit
97
+ user-confirmed action. Do not promote a project-specific preference directly
98
+ to active memory.
99
+
100
+ The implementation is `tools/clis/maggie_qa_workflow.py`; it is intentionally
101
+ small enough to run without a browser dependency and delegates browser
102
+ interaction to the host agent/browser capability.
@@ -59,6 +59,11 @@ If the change date is unknown, omit `lastmod`. An empty content-type does not
59
59
  need a sitemap chunk in the sitemap index; serving an empty endpoint and
60
60
  advertising it are separate decisions.
61
61
 
62
+ Validation also reports a freshness-distribution warning when a large sample
63
+ collapses onto one date (especially today). Treat that as a provenance review,
64
+ not as a reason to rewrite dates: verify the content-change event and omit
65
+ unknown dates. Strict semantic validation promotes the warning to a failure.
66
+
62
67
  Generated sitemap XML uses the conventional readable shape by default: one
63
68
  `<url>`/`<sitemap>` entry per block, UTF-8 XML, and date-only `lastmod` evidence
64
69
  rendered as a full UTC W3C datetime. The plan also exposes the response
@@ -174,6 +179,15 @@ maggie seo sitemap validate --plan docs/sitemap-plan.json
174
179
  # After an approved host adapter apply, rollback uses its exact backup manifest.
175
180
  maggie seo sitemap rollback --backup-manifest .maggie-sitemap-backups/<plan>/backup-manifest.json \
176
181
  --public-dir public --confirm
182
+
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>
185
+ maggie seo indexnow plan --origin https://example.com \
186
+ --changed-urls-file .maggie/changed-urls.json \
187
+ --state-file .maggie/indexnow-state.json --key <key> \
188
+ --output .maggie/indexnow-plan.json
189
+ maggie seo indexnow submit --plan .maggie/indexnow-plan.json \
190
+ --state-file .maggie/indexnow-state.json --confirm
177
191
  ```
178
192
 
179
193
  Only confirmed image variants may enter `srcset`; `apply` requires an explicit
@@ -182,6 +196,13 @@ omit empty chunks from the sitemap index, enforce absolute same-origin URLs, and
182
196
  record redirects for removed sitemap files. Read the [image and sitemap PRD](https://github.com/TOPY-AI-LTD/ai-cmo-skills/blob/main/docs/image-sitemap-structure-prd.md)
183
197
  for adapter and rollback rules.
184
198
 
199
+ IndexNow is opt-in and complements, rather than replaces, the sitemap. The
200
+ host supplies a changed-URL set and a public verification-key file. The plan
201
+ holds back URLs accepted within 24 hours, deduplicates same-origin URLs, and
202
+ records accepted state only after HTTP 200/202. A 403/429/5xx or network error
203
+ is retryable and must never make the editor save fail; do not retry unchanged
204
+ URLs in a loop.
205
+
185
206
  For a deterministic technical smoke check, run:
186
207
 
187
208
  ```bash
@@ -27,6 +27,8 @@ 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())
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")
30
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")
31
33
  sitemap = sub.add_parser("sitemap"); sitemap.add_argument("--project", type=Path, default=Path.cwd())
32
34
  settings = sub.add_parser("settings"); settings.add_argument("--project", type=Path, default=Path.cwd())
@@ -38,7 +40,7 @@ def main() -> int:
38
40
  print(json.dumps(result, indent=2, ensure_ascii=False))
39
41
  return 0 if result["status"] != "error" else 1
40
42
  store = BlogStore(args.project.resolve())
41
- if args.command in {"init", "ingest", "publish", "rollback", "translate-pending"} and not args.confirm:
43
+ if args.command in {"init", "ingest", "publish", "approve", "rollback", "translate-pending"} and not args.confirm:
42
44
  print("CONFIRMATION_REQUIRED: rerun with --confirm", file=sys.stderr); return 2
43
45
  try:
44
46
  if args.command == "init": result = store.init(args.base_path, args.posts_per_page)
@@ -51,6 +53,8 @@ def main() -> int:
51
53
  if not isinstance(payload, list): raise ValueError("local input must be a JSON array")
52
54
  result = store.ingest(payload, args.source)
53
55
  elif args.command == "validate": result = store.validate()
56
+ elif args.command == "check-gate": result = store.review_gate(store.settings())
57
+ elif args.command == "approve": result = store.approve(args.slug, args.actor, args.reason)
54
58
  elif args.command == "publish": result = store.publish(args.slug, args.actor, args.reason)
55
59
  elif args.command == "settings": result = store.settings()
56
60
  elif args.command == "rollback": result = store.rollback(args.backup)
@@ -25,6 +25,7 @@ from service_variants import ServiceVariantStore # noqa: E402
25
25
  from maggie_dash_ui import load_and_validate # noqa: E402
26
26
  from maggie_sections import catalogue, remap_translations, section_id_migration, validate_registry, copy_notes, validate_values, validate_fanout, reconcile_fields, validate_locale_coverage # noqa: E402
27
27
  from maggie_quality import validate_variant_copy, validate_media_uniqueness, classify_inventory, validate_bindings, validate_reconcile_contract # noqa: E402
28
+ from maggie_api_contract import validate_api_contract # noqa: E402
28
29
  from route_imports import classify_bindings # noqa: E402
29
30
 
30
31
 
@@ -422,7 +423,14 @@ def command_components_audit(args: argparse.Namespace) -> int:
422
423
  def command_inventory(args: argparse.Namespace) -> int:
423
424
  """Classify each published page once, with no broad predicate overlap."""
424
425
  value = json.loads(Path(args.pages_file).resolve().read_text(encoding="utf-8"))
425
- result = classify_inventory(value)
426
+ result = classify_inventory(value, require_page_kinds=args.require_page_kinds, require_source_coverage=args.require_source_coverage)
427
+ emit(result)
428
+ return 0 if result["passed"] else 1
429
+
430
+
431
+ def command_api_contract(args: argparse.Namespace) -> int:
432
+ value = json.loads(Path(args.spec).resolve().read_text(encoding="utf-8"))
433
+ result = validate_api_contract(value)
426
434
  emit(result)
427
435
  return 0 if result["passed"] else 1
428
436
 
@@ -572,7 +580,12 @@ def parser() -> argparse.ArgumentParser:
572
580
  components.set_defaults(func=command_components_audit)
573
581
  inventory = sub.add_parser("inventory", help="classify published pages into disjoint band/code-rendered kinds")
574
582
  inventory.add_argument("--pages-file", required=True, help="JSON page inventory")
583
+ inventory.add_argument("--require-page-kinds", action="store_true", help="fail when a published page has no pageKind")
584
+ inventory.add_argument("--require-source-coverage", action="store_true", help="fail unless sourceCoverage.complete is true")
575
585
  inventory.set_defaults(func=command_inventory)
586
+ api_contract = sub.add_parser("api-contract", help="validate declared request and success-response schemas")
587
+ api_contract.add_argument("--spec", required=True, help="OpenAPI-like JSON document")
588
+ api_contract.set_defaults(func=command_api_contract)
576
589
  variant = sub.add_parser("variant", help="manage service variant lifecycle")
577
590
  variant_sub = variant.add_subparsers(dest="variant_command", required=True)
578
591
  create = variant_sub.add_parser("create"); create.add_argument("--project", default="."); create.add_argument("--service-id", required=True); create.add_argument("--variant-id", required=True); create.add_argument("--variant-type", required=True); create.add_argument("--locale", required=True); create.add_argument("--market", required=True); create.add_argument("--slug", required=True); create.add_argument("--title", required=True); create.add_argument("--facts", required=True); create.add_argument("--source-revision", required=True); create.add_argument("--canonical-variant-id"); create.add_argument("--cluster-link", action="append", default=[]); create.add_argument("--layout-family", default="service-default"); create.add_argument("--confirm", action="store_true")
@@ -0,0 +1,60 @@
1
+ #!/usr/bin/env python3
2
+ """Plan and submit change-driven IndexNow notifications."""
3
+
4
+ from __future__ import annotations
5
+
6
+ import argparse
7
+ import json
8
+ from pathlib import Path
9
+ import sys
10
+
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
13
+
14
+
15
+ def read_json(path: Path, default: object) -> object:
16
+ if not path.exists():
17
+ return default
18
+ return json.loads(path.read_text(encoding="utf-8"))
19
+
20
+
21
+ def write_json(path: Path, value: object) -> None:
22
+ path.parent.mkdir(parents=True, exist_ok=True)
23
+ path.write_text(json.dumps(value, indent=2, ensure_ascii=False) + "\n", encoding="utf-8")
24
+
25
+
26
+ def main() -> int:
27
+ parser = argparse.ArgumentParser(prog="maggie seo indexnow")
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)
31
+ plan = sub.add_parser("plan", help="hold back recently accepted URLs and create a submission plan")
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
+ submit = sub.add_parser("submit", help="submit an approved plan; provider failure never updates accepted state")
34
+ submit.add_argument("--plan", required=True); submit.add_argument("--state-file", required=True); submit.add_argument("--endpoint", default="https://api.indexnow.org/indexnow"); submit.add_argument("--confirm", action="store_true")
35
+ args = parser.parse_args()
36
+ try:
37
+ if args.command == "key-check":
38
+ result = check_key_file(Path(args.public_dir).resolve(), args.key_file, args.key)
39
+ print(json.dumps(result, indent=2)); return 0 if result["status"] == "pass" else 1
40
+ if args.command == "plan":
41
+ changed = read_json(Path(args.changed_urls_file), [])
42
+ urls = changed.get("urls", []) if isinstance(changed, dict) else changed
43
+ if not isinstance(urls, list): raise ValueError("changed-urls-file must be a JSON array or {\"urls\": []}")
44
+ result = plan_indexnow(args.origin, urls, read_json(Path(args.state_file), {}), key=args.key, key_location=args.key_location, guard_hours=args.guard_hours, now=args.now)
45
+ write_json(Path(args.output), result)
46
+ print(json.dumps({"status": "planned", "eligible": len(result["eligible"]), "heldBack": len(result["heldBack"]), "rejected": len(result["rejected"]), "output": str(Path(args.output).resolve())}, indent=2)); return 0
47
+ if not args.confirm: raise ValueError("submit requires --confirm")
48
+ plan_value = read_json(Path(args.plan), {})
49
+ if not isinstance(plan_value, dict) or plan_value.get("schemaVersion") != "maggie-indexnow.v1": raise ValueError("invalid IndexNow plan")
50
+ state_path = Path(args.state_file); state = read_json(state_path, {})
51
+ if not isinstance(state, dict): state = {}
52
+ result = submit_plan(plan_value, state, args.endpoint)
53
+ if result["status"] == "accepted": write_json(state_path, state)
54
+ print(json.dumps(result, indent=2)); return 0 if result["status"] in {"accepted", "no-op"} else 1
55
+ except (OSError, ValueError, TypeError, json.JSONDecodeError) as error:
56
+ print(f"maggie-indexnow: {error}", file=sys.stderr); return 1
57
+
58
+
59
+ if __name__ == "__main__":
60
+ raise SystemExit(main())