@topy-ai/maggie 0.7.48 → 0.7.49

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 (49) hide show
  1. package/README-zh-TW.md +2 -2
  2. package/README.md +31 -4
  3. package/bin/maggie.js +17 -4
  4. package/bundled-contracts/google-integrations/capability-report-v2.schema.json +2 -1
  5. package/bundled-contracts/maggie-content/email-localization-v1.schema.json +89 -0
  6. package/bundled-contracts/maggie-content/lifecycle-email-v1.schema.json +76 -0
  7. package/bundled-contracts/maggie-deployment/readiness-v1.schema.json +6 -1
  8. package/bundled-contracts/maggie-ops/marketing-run-v1.schema.json +89 -0
  9. package/bundled-contracts/maggie-ops/notification-catalogue-example-v1.json +941 -0
  10. package/bundled-contracts/maggie-ops/notification-catalogue-v1.schema.json +273 -0
  11. package/bundled-contracts/maggie-service-booking/fact-provenance-v1.schema.json +128 -0
  12. package/bundled-contracts/maggie-service-booking/import-stages-v1.schema.json +191 -0
  13. package/bundled-references/google-integrations-runbook.md +54 -0
  14. package/bundled-skills/maggie-content-localization/SKILL.md +17 -1
  15. package/bundled-skills/maggie-content-localization/references/email-and-lifecycle.md +83 -0
  16. package/bundled-skills/maggie-deployment/SKILL.md +89 -7
  17. package/bundled-skills/maggie-deployment/references/vps.md +38 -6
  18. package/bundled-skills/maggie-design/SKILL.md +55 -0
  19. package/bundled-skills/maggie-feedback/SKILL.md +29 -0
  20. package/bundled-skills/maggie-ops/SKILL.md +37 -0
  21. package/bundled-skills/maggie-ops/references/notifications-operations.md +154 -0
  22. package/bundled-skills/maggie-service-booking/SKILL.md +59 -0
  23. package/bundled-skills/maggie-social-share/SKILL.md +19 -2
  24. package/bundled-skills/maggie-social-share/references/marketing-run.md +57 -0
  25. package/bundled-skills/maggie-social-share/references/queue-and-connections.md +51 -0
  26. package/bundled-tools/clis/maggie_deployment.py +58 -17
  27. package/bundled-tools/clis/maggie_deployment_auth_canary.py +216 -0
  28. package/bundled-tools/clis/maggie_deployment_collect.py +225 -0
  29. package/bundled-tools/clis/maggie_deployment_readiness.py +53 -3
  30. package/bundled-tools/clis/maggie_design.py +8 -0
  31. package/bundled-tools/clis/maggie_design_verify.py +359 -0
  32. package/bundled-tools/clis/maggie_docs.py +82 -4
  33. package/bundled-tools/clis/maggie_email_localization.py +382 -0
  34. package/bundled-tools/clis/maggie_feedback.py +9 -2
  35. package/bundled-tools/clis/maggie_icon_glyph_check.py +382 -0
  36. package/bundled-tools/clis/maggie_icon_release_gate.py +28 -1
  37. package/bundled-tools/clis/maggie_localization.py +5 -0
  38. package/bundled-tools/clis/maggie_marketing_run.py +272 -0
  39. package/bundled-tools/clis/maggie_notifications.py +528 -0
  40. package/bundled-tools/clis/maggie_ops.py +87 -0
  41. package/bundled-tools/clis/maggie_schedule.py +12 -2
  42. package/bundled-tools/clis/maggie_service_import.py +520 -0
  43. package/bundled-tools/clis/maggie_social_queue.py +104 -0
  44. package/bundled-tools/clis/maggie_social_share.py +6 -2
  45. package/bundled-tools/runtime/google_ads.py +306 -0
  46. package/bundled-tools/runtime/google_capabilities.py +18 -2
  47. package/bundled-tools/runtime/google_remediation.py +152 -0
  48. package/package.json +1 -1
  49. package/references/google-integrations-runbook.md +54 -0
@@ -0,0 +1,83 @@
1
+ # Localized email and consent-aware lifecycle email
2
+
3
+ Email is a localized content surface. Treat every template × locale × market
4
+ as a reviewable variant, and keep transactional/security messages separate
5
+ from marketing consent. Both commands below are read-only: they never call a
6
+ provider and never send.
7
+
8
+ ## Localized templates (`email-matrix`)
9
+
10
+ Describe templates in a `maggie-email-localization.v1` manifest (schema:
11
+ `contracts/maggie-content/email-localization-v1.schema.json`, shipped as
12
+ `bundled-contracts/maggie-content/` in the npm package).
13
+
14
+ Locale precedence is fixed: **user preference > market > account > fallback**.
15
+ A send path must pass the resolved recipient locale. Two patterns fail:
16
+
17
+ - a hard-coded locale (e.g. a fulfilment path that always renders `en`);
18
+ - `Accept-Language` as the only source for fulfilment or transactional email.
19
+ For other paths it is only a hint and produces a warning.
20
+
21
+ Declare each send path under `callSites` with its `localeSource` so the check
22
+ covers code paths, not only templates.
23
+
24
+ The check fails when:
25
+
26
+ - a required locale or its subject/HTML/plain text is missing;
27
+ - a translated field is identical to the source text (untranslated);
28
+ - variables differ from the source field, are undeclared, or differ between
29
+ the HTML and plain-text bodies;
30
+ - a CTA route is not a site path or `https` URL, uses a market whose prefix it
31
+ does not carry, targets a market that does not serve that locale, or changes
32
+ the template route's parameters.
33
+
34
+ It warns about glossary terms not rendered as their approved translation,
35
+ CTA routes missing from a body, unused variables, and unapproved variants.
36
+ A variant is publishable only when its `approval` has `status: approved` and a
37
+ `reviewer`; `--require-approved` turns that into a failure for release gates.
38
+
39
+ ```bash
40
+ maggie localization email-matrix emails/manifest.json \
41
+ --preview-dir .maggie/localization/email-preview
42
+ maggie localization email-matrix emails/manifest.json --require-approved
43
+ ```
44
+
45
+ `--preview-dir` writes one `.html` and one `.txt` file per template × locale,
46
+ filled from the template's synthetic `fixtures` (HTML-escaped) and labelled as
47
+ a preview. Never put customer data in fixtures.
48
+
49
+ ## Lifecycle and marketing email (`lifecycle`)
50
+
51
+ A `maggie-lifecycle-email.v1` plan (schema:
52
+ `contracts/maggie-content/lifecycle-email-v1.schema.json`) runs through fixed
53
+ stages: plan → segment → consent-check → preview → approve → schedule → send →
54
+ report. `defaults.send` must be `disabled`: nothing is sent until approved.
55
+
56
+ Start from the library, which drafts every kind with nothing sendable:
57
+
58
+ ```bash
59
+ maggie localization lifecycle --library > emails/lifecycle.json
60
+ maggie localization lifecycle emails/lifecycle.json
61
+ ```
62
+
63
+ Library kinds: opt-in confirmation, welcome, digest, recommendation, gift /
64
+ voucher / waitlist follow-up, merchant lead digest, campaign announcement,
65
+ re-engagement.
66
+
67
+ Rules the validator enforces:
68
+
69
+ - Marketing templates need `consent.basis` (opt-in, double-opt-in,
70
+ soft-opt-in, or legitimate-interest-b2b) and a preference-centre
71
+ `consent.category`, an unsubscribe path, a preference-centre path, and
72
+ attribution fields (source/UTM) so outcomes can be measured.
73
+ - Every template has a frequency cap (`maxPerWindow` + ISO 8601 `window`).
74
+ - The opt-in confirmation is sent before consent exists, so it must be
75
+ transactional and carry no promotional content.
76
+ - `sendable: true` requires an approval with `status: approved`, `reviewer`
77
+ and `approvedAt` — on the template and on the campaign.
78
+ - Campaigns using marketing templates require consent, must honour
79
+ suppression, must deduplicate contacts, and need a schedule before they are
80
+ sendable. Declare aggregate report metrics; never report per-recipient data.
81
+
82
+ The host owns consent records, segments, suppression lists, delivery and
83
+ reporting. Maggie validates the plan; it does not send or store addresses.
@@ -2,7 +2,7 @@
2
2
  name: maggie-deployment
3
3
  description: Deploy and operate Maggie blog projects with Cloudflare Workers as the default target, while preserving an adapter boundary for VPS, GCP, and AWS.
4
4
  metadata:
5
- version: 1.5.0
5
+ version: 1.6.0
6
6
  ---
7
7
 
8
8
  # Maggie Deployment
@@ -88,6 +88,74 @@ not fabricate browser, rendered, or deployment evidence and does not deploy
88
88
  from this command. See
89
89
  [`readiness-v1.schema.json`](../../bundled-contracts/maggie-deployment/readiness-v1.schema.json).
90
90
 
91
+ ### Collect host evidence
92
+
93
+ Instead of hand-written SSH, systemd, and retention scripts per host, declare
94
+ the standard adapters once in `.maggie/deployment/collector.json`
95
+ (`maggie-evidence-collector.v1`) and let one command run them and emit the
96
+ same `maggie-deployment-readiness.v1` report:
97
+
98
+ ```json
99
+ {
100
+ "schemaVersion": "maggie-evidence-collector.v1",
101
+ "release": "2026-10-10-a",
102
+ "adapters": {
103
+ "canary": {"assets": ["https://example.test/_astro/app.js=<sha256>"], "renderReport": ".maggie/rendered-canary.json"},
104
+ "health": {"urls": ["https://example.test/robots.txt", "https://example.test/sitemap.xml"]},
105
+ "rollback": {"evidence": "docs/deployment-rollback-smoke.json", "plan": ".maggie/deployment/vps/deployment-plan.json"},
106
+ "releaseIdentity": {"command": ["ssh", "-o", "BatchMode=yes", "maggie-deploy@host", "readlink -f /var/www/example/current"]},
107
+ "authCanary": {"plan": ".maggie/deployment/auth-canary-plan.json"}
108
+ }
109
+ }
110
+ ```
111
+
112
+ ```bash
113
+ maggie deployment collect --project . --config .maggie/deployment/collector.json
114
+ ```
115
+
116
+ Adapters write their evidence under `.maggie/deployment/collected/` and the
117
+ readiness report adds the optional `hostHealth`, `releaseIdentity`,
118
+ `rollbackEvidence`, and `authenticatedCanary` slots next to the five required
119
+ slots. `releaseIdentity` proves the host's `current` release (from an argv
120
+ command such as `readlink -f`, or a `{"url", "field"}` version endpoint)
121
+ equals the expected `release`. Commands are argv lists, never shell strings,
122
+ and are rejected if they contain a literal credential. The report records the
123
+ config SHA-256, the release, and per-adapter run metadata so it can be
124
+ reproduced; it never deploys.
125
+
126
+ ### Authenticated API canaries
127
+
128
+ Public canaries cannot see failures on signed-in account or business APIs
129
+ (database permission errors, schema mismatches, 403s). Declare role-scoped
130
+ probes in a `maggie-auth-canary-plan.v1` plan:
131
+
132
+ ```json
133
+ {
134
+ "schemaVersion": "maggie-auth-canary-plan.v1",
135
+ "baseUrl": "https://example.test",
136
+ "roles": {"owner": {"tokenEnv": "CANARY_OWNER_TOKEN"}, "staff": {"cookieEnv": "CANARY_STAFF_COOKIE"}},
137
+ "probes": [
138
+ {"id": "account", "role": "owner", "path": "/api/account", "expectJsonKeys": ["id", "role"]},
139
+ {"id": "business", "role": "staff", "path": "/api/business", "expectStatus": [200]},
140
+ {"id": "fixture-create", "role": "owner", "method": "POST", "path": "/api/canary-fixtures", "fixture": "disposable", "expectStatus": [201]}
141
+ ]
142
+ }
143
+ ```
144
+
145
+ ```bash
146
+ CANARY_OWNER_TOKEN=... maggie deployment auth-canary \
147
+ --plan .maggie/deployment/auth-canary-plan.json --output .maggie/deployment/auth-canary.json
148
+ ```
149
+
150
+ Credentials come only from the named environment variables of dedicated
151
+ canary accounts; a plan containing a literal token or cookie is rejected.
152
+ Mutating probes must target a `disposable` fixture and run only with
153
+ `--allow-mutations`. Evidence (`maggie-deployment-auth-canary.v1`) records
154
+ probe id, role, method, path, status, and contract-key counts, classifying
155
+ failures as `authorization`, `server`, `route`, `contract`, or `network`;
156
+ it never stores headers, cookies, tokens, response bodies, or personal data.
157
+ A missing credential makes the probe `inconclusive`, not passed.
158
+
91
159
  Classify host/runtime failures before a release can move:
92
160
 
93
161
  ```bash
@@ -207,7 +275,7 @@ The output contains only the immutable release/current layout, systemd unit,
207
275
  Nginx reverse-proxy config, health commands, rollback command, and an ordered
208
276
  reviewable release runner. The runner keeps migrations before the symlink flip,
209
277
  carries `.agents`/`.claude` state, restarts before verification, and prunes to
210
- the current release plus one rollback candidate. It never
278
+ the plan's retention policy (default: current plus one rollback candidate). It never
211
279
  contains credentials and never SSHs, changes DNS, restarts systemd or deploys.
212
280
 
213
281
  VPS plans use a dedicated deployer account by default:
@@ -225,9 +293,23 @@ when they are unmarked or stale; use `--check-runner PATH` for a runner stored
225
293
  elsewhere. Regenerate from the reviewed VPS plan, inspect the diff, and replace
226
294
  the checked-in runner only after review.
227
295
 
228
- VPS plans include a bounded retention policy: keep two immutable releases,
229
- preserve the `current` target and rollback target, and review prune candidates
230
- before any operator executes cleanup. Generate a read-only candidate report:
296
+ VPS plans carry one explicit retention policy, `--keep-releases N` (1-5,
297
+ default 2). The same value drives the plan's prune command, the generated
298
+ runner's prune step, `--retention-plan`, preflight, and rollback evidence, so
299
+ Maggie never advertises a rollback target the host deletes:
300
+
301
+ - `N >= 2` keeps the `current` target plus the release it replaced; the plan's
302
+ `commands.rollback` switches back to it.
303
+ - `N = 1` keeps only the active release. The plan reports
304
+ `retention.rollbackAvailable: false` and `commands.rollback: null`, the
305
+ runner prints `rollback target: unavailable`, and production rollback
306
+ evidence must declare `codeRollback: "unavailable"` with a `recoveryPlan`
307
+ (redeploy the previous commit as a new release and restore its data
308
+ checkpoint). Preflight's `rollback_target_retained` check fails when a plan
309
+ keeps one release but still advertises a previous-release rollback.
310
+
311
+ Review prune candidates before any operator executes cleanup. Generate a
312
+ read-only candidate report:
231
313
 
232
314
  ```bash
233
315
  maggie tool maggie_deployment.py --retention-plan \
@@ -236,8 +318,8 @@ maggie tool maggie_deployment.py --retention-plan \
236
318
  --keep-releases 2 --output .maggie/deployment/retention-plan.json
237
319
  ```
238
320
 
239
- The CLI never deletes release directories and never permits fewer than two
240
- retained releases.
321
+ The CLI never deletes release directories; only the reviewed runner prunes,
322
+ and only after health checks pass.
241
323
 
242
324
  ### Multi-host secret parity
243
325
 
@@ -121,6 +121,29 @@ before staging migration and require a successful restore test before a
121
121
  production release. Keep the backup artifact and migration version in the
122
122
  handover record; never put a database URL or password in the manifest.
123
123
 
124
+ ### Running migrations as the database owner
125
+
126
+ `sudo -u postgres psql -f /home/ubuntu/site/releases/<release>/db/073.sql`
127
+ fails with `Permission denied` when the release lives under a deploy user's
128
+ home: the `postgres` account cannot traverse that path. Do not widen
129
+ filesystem permissions. Stream the file from a root-readable read into the
130
+ owner-level `psql` instead (the generated plan's `commands.migrate_as_db_owner`):
131
+
132
+ ```bash
133
+ sudo cat /home/ubuntu/site/releases/<release>/db/073.sql \
134
+ | sudo -u postgres psql -v ON_ERROR_STOP=1 --single-transaction -d <database> -f -
135
+ ```
136
+
137
+ If a file path is required, copy it to a private temporary file readable only
138
+ by the owner and remove it afterwards:
139
+
140
+ ```bash
141
+ tmp=$(sudo -u postgres mktemp) && sudo cp <migration.sql> "$tmp" \
142
+ && sudo chown postgres: "$tmp" && sudo chmod 600 "$tmp" \
143
+ && sudo -u postgres psql -v ON_ERROR_STOP=1 -d <database> -f "$tmp"; \
144
+ sudo rm -f "$tmp"
145
+ ```
146
+
124
147
  ## Release workflow
125
148
 
126
149
  ```text
@@ -134,11 +157,20 @@ robots, sitemap, or private-route correctness.
134
157
 
135
158
  ### Release retention
136
159
 
137
- Keep exactly the `current` release plus one previous release as the rollback
138
- target. Prune after every deploy, and only after the new release passed its
139
- health checks: an unhealthy release must still have its predecessor on disk.
140
- The generated release runner does this as its final step; it never removes the
141
- release `current` points at or the release it replaced. Without pruning,
160
+ The retention policy is one explicit number, `--keep-releases N` (default 2),
161
+ shared by the plan, the runner, `--retention-plan`, preflight, and rollback
162
+ evidence. The default keeps the `current` release plus one previous release as
163
+ the rollback target. `N = 1` is allowed for hosts that intentionally keep only
164
+ the active release, but then no on-disk rollback exists: the plan reports
165
+ `rollbackAvailable: false`, the runner says so after pruning, and rollback
166
+ evidence must name a recovery plan (redeploy the previous commit, restore the
167
+ data checkpoint) instead of a previous release.
168
+
169
+ Prune after every deploy, and only after the new release passed its health
170
+ checks: an unhealthy release must still have its predecessor on disk. The
171
+ generated release runner does this as its final step; it never removes the
172
+ release `current` points at, and with `N >= 2` it also keeps the release it
173
+ replaced. Without pruning,
142
174
  `releases/` grows by one full `node_modules` tree per deploy until the disk
143
175
  fills. Use `--retention-plan` for a read-only list of prune candidates on hosts
144
176
  whose runner predates this step, and review it before deleting anything.
@@ -180,7 +212,7 @@ canonical URLs, booking links, and protected Ops/API routes. Check both apex
180
212
  and `www` when configured. Record HTTP status, commit, and release.
181
213
 
182
214
  Rollback switches `current` to the previous immutable release and restarts the
183
- service. If migrations are not backward-compatible, stop before deployment and
215
+ service, when the retention policy keeps one. If migrations are not backward-compatible, stop before deployment and
184
216
  provide a tested restore or database rollback plan. The first production
185
217
  release may declare rollback as not applicable only with `releaseKind: "first"`,
186
218
  a non-empty `notApplicableReason`, and `RELEASE_KIND=first`; upgrades still
@@ -84,6 +84,61 @@ a successful `assetStatus` (2xx), and a non-empty accessible name. This
84
84
  gate is independent from typecheck/build and is read-only apart from its
85
85
  explicit output.
86
86
 
87
+ ### Rendered glyph proof
88
+
89
+ Collect that browser evidence with the shared collector instead of a
90
+ hand-written report. It renders each route at mobile, tablet, and desktop in
91
+ headless Chrome — through a local GET-only proxy for a running host
92
+ (`--base-url`) or directly from a static build (`--dist`) — and records, per
93
+ representative icon, whether its icon font reached `document.fonts` status
94
+ `loaded`, whether the glyph paints ink that differs from the browser fallback
95
+ font (a missing glyph falls back or paints blank), whether runtime CSS hides
96
+ it (`display`, `visibility`, `opacity`, zero size, or a hidden pseudo-element),
97
+ its font family, accessible name, and the served font asset status:
98
+
99
+ ```bash
100
+ maggie design icon-glyph-check --base-url http://localhost:4321 \
101
+ --route / --route /contact --family ph=Phosphor \
102
+ --output .maggie/icon-browser-evidence.json
103
+ ```
104
+
105
+ `--family PREFIX=FAMILY` asserts the expected visual family for a class
106
+ prefix. Evidence from this collector makes `icon-release-gate` also require
107
+ `fontLoaded`, `glyphPainted`, matching family, and an icon at every declared
108
+ viewport; hand-written evidence still passes the original checks but the gate
109
+ reports `glyphIdentityProven: false` with a warning. The collector forwards GET
110
+ requests only and never stores page text beyond icon names and labels.
111
+
112
+ ## Design job verification
113
+
114
+ `design step` records evidence one step at a time. `design verify` runs every
115
+ step that can be proven automatically against a host adapter and records it
116
+ with durable evidence under `.maggie/design-evidence/<job>/`:
117
+
118
+ ```bash
119
+ # section-scoped job: snapshot before editing, then verify after
120
+ maggie design verify section-<id> --project . --base-url http://localhost:4321 --stage before
121
+ maggie design verify section-<id> --project . --base-url http://localhost:4321
122
+
123
+ # in-place or author job against a static build, including the host build
124
+ maggie design verify in-place-<id> --project . --dist dist --build-command "npm run build"
125
+ ```
126
+
127
+ Automated steps: `inspect-route`/`inspect-shell`/`inspect-section` (2xx HTML,
128
+ shell landmarks, target section present), `declare-content-source` (no
129
+ unproven catch-all resolution), `capture-before`/`capture-after` (per-section
130
+ structure hashes from `<section id>` or `data-section-id`),
131
+ `assert-unchanged-rest`, `capture-responsive-screenshots` (390/768/1440 PNGs
132
+ with hashes), `accessibility-check` (lang, one h1, alt text, named controls,
133
+ labelled fields, `<main>`), `route-validation` (2xx, title, h1, no template
134
+ placeholders, no accidental `noindex`), and `build-check` only with an explicit
135
+ `--build-command`. A failed check leaves its step pending with `lastCheck`
136
+ evidence. `apply-design-contract`, `implement-*`, `visual-review`, and
137
+ `approval` are never completed by `verify`; record them with `design step` and
138
+ real implementer or reviewer evidence, so a job cannot reach `done` without
139
+ evidence. Client-rendered sections need host-adapter evidence through
140
+ `design step` when the server HTML does not contain them.
141
+
87
142
  ## Hero video playback contract
88
143
 
89
144
  Hero video is an enhancement, not the only way to see the first-band message.
@@ -70,6 +70,15 @@ with one shared correlation ID instead of losing their relationship:
70
70
 
71
71
  maggie feedback batch --project . --batch-file ./feedback-batch.json
72
72
 
73
+ For long cross-feature reviews, prefer the batch file over repeated `collect`
74
+ calls: JSON strings carry multiline text, backticks, and command names without
75
+ shell escaping. `--batch-file -` reads the same JSON from stdin, so a quoted
76
+ heredoc (`<<'EOF'`) never interpolates command substitutions:
77
+
78
+ maggie feedback batch --project . --batch-file - <<'EOF'
79
+ {"batchId": "review-2026-10", "skill": "maggie-deployment", "items": [...]}
80
+ EOF
81
+
73
82
  The batch file contains 1–50 items and may provide shared `skill`, `runId`,
74
83
  `phase`, `priority`, and `affectedCli` values. An item can override those
75
84
  fields and supplies the normal `type`, `summary`, `expected`, `actual`,
@@ -155,6 +164,26 @@ skill or CLI, and replace every `REVIEW:` field (expected, actual, validation).
155
164
  placeholders. Then continue with `batch`, `batch-review`, and explicit
156
165
  `submit` as usual.
157
166
 
167
+ ## Check release and rollout claims before reporting "fixed"
168
+
169
+ A fixed report is only as good as the release state it points to. Two
170
+ read-only checks catch the drift that previous reports had to find by hand:
171
+
172
+ python3 scripts/validate_docs_freshness.py --release-metadata
173
+ maggie docs rollout-status --project .
174
+
175
+ The first (Maggie source repository) fails when `VERSION`, the CLI package
176
+ version, the newest `CHANGELOG.md` heading, and the README current-version text
177
+ disagree, so a published version cannot ship without its release notes. The
178
+ second scans `docs/` for rollout item IDs (default pattern `ABC-12`, override
179
+ with `--id-pattern`) and fails when one document says an item is complete while
180
+ another says it is pending or in progress. When `docs/rollout-status.json`
181
+ (`maggie-rollout-status.v1`: `items[]` with `id`, `status` of `planned`,
182
+ `in_progress`, `blocked`, or `complete`, and `evidence` for complete items)
183
+ exists, it is canonical and every document must agree with it. Mark superseded
184
+ documents with `<!-- documentation-status: historical-snapshot -->` instead of
185
+ leaving contradictory claims current.
186
+
158
187
  ## Submit explicitly
159
188
 
160
189
  The CLI never submits automatically. After reviewing the draft:
@@ -96,10 +96,47 @@ prints these safe identifiers and scopes before any separate provider adapter
96
96
  is allowed to perform a write. A v1 report is rejected because it cannot prove
97
97
  that the browser/session is on the intended Google account and property.
98
98
 
99
+ When a check fails, add an optional `failure` (`code`, `reason`, `httpStatus`;
100
+ never a token or payload) to the row. The validator maps it to one failed
101
+ prerequisite and one safe next action under `remediation`. For a single error,
102
+ `maggie ops google-diagnose --code SERVICE_DISABLED --http-status 403` prints the
103
+ same mapping.
104
+
105
+ For Google Ads, run the read-only adapter before any keyword research:
106
+
107
+ ```bash
108
+ maggie ops google-ads-preflight --customer-id 123-456-7890 \
109
+ [--login-customer-id 999-888-7777] [--probe-keyword-planner]
110
+ maggie ops google-ads-keyword-ideas --customer-id 123-456-7890 \
111
+ --keyword "day spa" --language 1000 --geo-target 2826 --currency GBP
112
+ ```
113
+
114
+ Credentials come from `GOOGLE_ADS_ACCESS_TOKEN` and
115
+ `GOOGLE_ADS_DEVELOPER_TOKEN` (names overridable); values are never printed.
116
+ The adapter only allows `listAccessibleCustomers`, `googleAds:search`, and
117
+ `generateKeywordIdeas`, so it cannot mutate Ads state.
118
+
99
119
  The result separates `read`, `report`, `edit`, and `publish` for each
100
120
  provider/resource. Write and publish remain `not_tested` until the operator
101
121
  explicitly confirms the exact mutation and a successful read-back is captured.
102
122
 
123
+ ## Website notifications
124
+
125
+ For website email, in-app, and SMS operations, follow
126
+ [Website notification operations](references/notifications-operations.md):
127
+ a catalogue with consent, retention, and delivery rules, template drift
128
+ against a paginated provider export, a Resend preflight by variable name
129
+ only, aggregate queue health with operator alerts, and a dry-run retention
130
+ plan. All `maggie ops notifications` commands are offline and read-only, and
131
+ none of them sends a message.
132
+
133
+ ```bash
134
+ maggie ops notifications catalogue validate --project . --catalogue docs/notifications/catalogue.json
135
+ maggie ops notifications templates drift --project . --catalogue docs/notifications/catalogue.json --local src/emails --provider-export exports/page-1.json
136
+ maggie ops notifications preflight --project . --env-file .env.production --catalogue docs/notifications/catalogue.json
137
+ maggie ops notifications health --project . --snapshot .maggie/notifications-health.json
138
+ ```
139
+
103
140
  ## Required preflight
104
141
 
105
142
  1. Inspect `.maggie/analysis.json` and `.maggie/bootstrap-state.json`.
@@ -0,0 +1,154 @@
1
+ # Website notification operations
2
+
3
+ A provider-neutral operating contract for website email, in-app, and SMS
4
+ notifications: what each message is, which consent it needs, how templates
5
+ stay in sync with the provider, how delivery is kept durable, how the queue is
6
+ watched, and how notification data expires. Every command below is offline and
7
+ read-only. None of them call a provider API, send a message, or print secret
8
+ values or recipient addresses.
9
+
10
+ ## 1. Catalogue every message
11
+
12
+ Keep one machine-readable catalogue in the project (for example
13
+ `docs/notifications/catalogue.json`) using
14
+ `contracts/maggie-ops/notification-catalogue-v1.schema.json`. The packaged
15
+ example `contracts/maggie-ops/notification-catalogue-example-v1.json` covers the
16
+ seven sections a website normally needs:
17
+
18
+ | Section | Typical entries |
19
+ |---|---|
20
+ | `identity-security` | email verification, password reset (often provider-owned), sign-in alert, credential/2FA/passkey change, account deletion |
21
+ | `account` | welcome, onboarding reminder, preference update, saved-item digest |
22
+ | `business` | listing/claim status, verification, team invite/approval, import or sync result, publish/domain readiness |
23
+ | `billing` | checkout/subscription active, payment failed, receipt (often provider-owned), plan change, cancellation, trial ending |
24
+ | `support` | contact/waitlist receipt, team copy of a contact, case reply/closure, feedback request |
25
+ | `marketing` | opt-in confirmation, newsletter welcome, content digest, recommendation, gift/voucher interest follow-up, merchant lead digest, re-engagement |
26
+ | `deliverability-ops` | bounce/complaint, suppression, worker failure, dead-letter growth, template drift, sender-domain health, queue health |
27
+
28
+ Each entry records trigger, audience, category (`transactional`, `support`,
29
+ `marketing`), consent boundary, locales and markets, variables, CTA route,
30
+ idempotency-key template, retention days, provider, channels, delivery mode
31
+ (`outbox`, `direct`, `provider`), queue, and status (`implemented`,
32
+ `provider-owned`, `reserved`, `missing`). The status column turns the
33
+ catalogue into a planning asset: `missing` lists what the site still lacks.
34
+
35
+ ```bash
36
+ maggie ops notifications catalogue validate --project . --catalogue docs/notifications/catalogue.json
37
+ ```
38
+
39
+ The validator enforces the cross-field rules a schema cannot:
40
+
41
+ - Marketing messages require `consent: opt-in-required`, `unsubscribe: true`
42
+ (preference-centre and unsubscribe links), and the `marketing` queue.
43
+ - `deliverability-ops` alerts use the `operations` queue. An alert about a
44
+ failing provider or queue must not travel through that same queue.
45
+ - External-recipient lead email (`lead: true`: contact, waitlist, gift or
46
+ voucher interest) must use durable `outbox` delivery. A direct send that
47
+ only logs a failure can silently lose a business lead.
48
+ - CTA routes are site paths or `https://` URLs with a fixed host.
49
+
50
+ ## 2. Durable delivery for lead email
51
+
52
+ Contact, waitlist, and interest messages are acquisition signals. Route them
53
+ through the same outbox as transactional mail: an idempotency key per
54
+ submission, lease-based claiming, bounded retries, a dead-letter state with
55
+ recorded-reason replay, and provider webhook reconciliation. The visitor's
56
+ request succeeds once the outbox row is committed; delivery failure is an
57
+ operations alert, not a visitor-facing error.
58
+
59
+ ## 3. Template lifecycle and drift
60
+
61
+ Template work moves through these stages, each producing reviewable evidence:
62
+
63
+ 1. **Discover/scaffold**: derive the template matrix (alias × locale) from the
64
+ catalogue's `implemented` email entries.
65
+ 2. **Preview**: render HTML and plain text from fixtures only, never live
66
+ customer data.
67
+ 3. **Lint**: every placeholder is a declared variable; CTA routes resolve.
68
+ 4. **Localize**: every catalogue locale exists in HTML and plain text, with
69
+ the same variables (see `maggie-content-localization`).
70
+ 5. **Approve**: record who approved which template version.
71
+ 6. **Sync dry run**: compute the provider diff without writing.
72
+ 7. **Publish**: apply the reviewed diff with an explicit confirmation.
73
+ 8. **Rollback**: keep the previous provider version ID to restore.
74
+ 9. **Verify**: re-run drift; any real send uses a provider test mode or an
75
+ allow-listed internal recipient, behind an explicit confirmation.
76
+
77
+ Before every deploy, compare the catalogue, local templates, and a provider
78
+ export. Export **every** page of the provider's template list to JSON first
79
+ (list endpoints paginate; a single call with a page limit can miss templates):
80
+
81
+ ```bash
82
+ maggie ops notifications templates drift --project . \
83
+ --catalogue docs/notifications/catalogue.json \
84
+ --local src/emails/templates \
85
+ --provider-export exports/templates-page-1.json \
86
+ --provider-export exports/templates-page-2.json
87
+ ```
88
+
89
+ `--local` accepts a directory of `<alias>.<locale>.<ext>` files (placeholders
90
+ `{{ name }}` are extracted) or a `maggie-notification-templates.v1` manifest.
91
+ The report lists missing local/provider templates, extra provider or local
92
+ aliases, locale gaps, undeclared or unused variables, and local/provider
93
+ version drift. It exits non-zero on drift, and when the last export page still
94
+ reports `has_more: true`. A runtime fallback to locally rendered email protects
95
+ delivery but hides drift, so treat drift as a deploy blocker.
96
+
97
+ ## 4. Provider preflight (Resend)
98
+
99
+ ```bash
100
+ maggie ops notifications preflight --project . --env-file .env.production \
101
+ --catalogue docs/notifications/catalogue.json
102
+ ```
103
+
104
+ Checks, by variable name only: API key, sender (`RESEND_EMAIL`, `RESEND_FROM`,
105
+ or `RESEND_SENDER`), worker secret, support recipients, webhook secret, and
106
+ the template map. Template IDs are not printed. Only the map's alias keys are
107
+ compared with implemented Resend catalogue entries. Add project-specific names
108
+ with `--require NAME`. Presence is not proof of delivery: after deploy, the
109
+ release still needs sender-domain verification, webhook reachability, timer
110
+ health, and an authenticated synthetic delivery recorded as redacted evidence.
111
+
112
+ ## 5. Queue health and alerts
113
+
114
+ Have the host worker or a read-only SQL job write an aggregate snapshot
115
+ (`maggie-notification-queue-snapshot.v1`) with counts (`pending`,
116
+ `processing`, `deadLetter`, `awaitingWebhook`), `oldestPendingAt`,
117
+ `expiredLeases`, `lastWebhookAt`, `lastSuccessfulRunAt`, optional `rates`
118
+ (`bounce`, `complaint`), and `senderDomainVerified`. Snapshots must not contain
119
+ addresses; the check rejects them if they do.
120
+
121
+ ```bash
122
+ maggie ops notifications health --project . --snapshot .maggie/notifications-health.json \
123
+ --max-oldest-pending-minutes 30 --max-dead-letter 0
124
+ maggie ops notifications alerts
125
+ ```
126
+
127
+ `health` exits non-zero when any threshold is exceeded, so it can run from a
128
+ systemd timer or CI after the worker. A timer can be active while the worker
129
+ never succeeds, so `lastSuccessfulRunAt` is required. `alerts` prints the
130
+ operator alert templates (severity, owner, recovery action, escalation). Send
131
+ alerts through an operations channel that is independent of the notification
132
+ queue and provider being monitored.
133
+
134
+ ## 6. Privacy, retention, and suppression
135
+
136
+ The catalogue's `retention` block sets days for `webhook-payload`,
137
+ `delivery-record`, `rendered-snapshot`, and `template-variables`. Store
138
+ minimized webhook fields (event type, provider message ID, status, time), not
139
+ the full raw payload. Keep suppression entries as a hashed address with
140
+ reason and time. Recovery from suppression (for example, a fixed mailbox)
141
+ needs an explicit operator action with a recorded reason.
142
+
143
+ Plan purges from aggregate counts only:
144
+
145
+ ```bash
146
+ maggie ops notifications retention plan --project . \
147
+ --catalogue docs/notifications/catalogue.json \
148
+ --inventory .maggie/notifications-inventory.json
149
+ ```
150
+
151
+ The plan is always a dry run. Rows tied to an open support case or legal hold
152
+ are reported as held. Apply purges through the host's reviewed job or
153
+ migration, and handle account deletion by purging that user's delivery
154
+ records and snapshots and keeping only the hashed suppression entry.
@@ -231,6 +231,65 @@ The contract is documented in
231
231
  It is provider-neutral and does not accept credentials, provider response
232
232
  bodies, or arbitrary filesystem paths in the poll reference.
233
233
 
234
+ ### Long-running import stages, readiness, and fact provenance
235
+
236
+ Inside the `import` workflow, a provider import runs four stages: `source`,
237
+ then `enrichment` and `research` in parallel, then `verification`. Validate a
238
+ redacted snapshot with:
239
+
240
+ ```bash
241
+ maggie service import-stages-validate --project . \
242
+ --manifest .maggie/booking/import-stages.json --output docs/import-stages.json
243
+ ```
244
+
245
+ The `maggie-service-import-stages.v1` contract
246
+ (`contracts/maggie-service-booking/import-stages-v1.schema.json`) requires a
247
+ stage key per stage prefixed with the import ID and reused on retry, bounded
248
+ attempts, `{done, total}` progress (a succeeded stage is complete), and a
249
+ `heartbeatAt` for running stages. A running stage without a heartbeat for more
250
+ than `staleAfterSeconds` makes the import `stale` with an explicit
251
+ `nextAction`. Parallel stages may complete in any order, but a stage may not
252
+ start before its dependencies finish, and a downstream result built from a
253
+ superseded `source.resultRevision` must be marked `resultDiscarded` instead of
254
+ applied. Cancellation must reach every stage; results completed after
255
+ `cancellation.requestedAt` are discarded. The declared import state must match
256
+ the state derived from its stages (`partial` when retries are exhausted after
257
+ some stages succeeded).
258
+
259
+ Before queuing a long-running provider job, run the worker readiness preflight
260
+ on the worker host. It fails — and the job must not be queued — when a Python
261
+ module is not importable, a browser or matching WebDriver is missing, a crawler
262
+ setting is absent (checked by environment variable name; values are never
263
+ printed), the runtime is not UTF-8, or a country label cannot be normalized to
264
+ ISO 3166 (locale display names and mis-decoded text such as `Königreich` are
265
+ repaired first). A rate-limit probe is a single HEAD request and runs only when
266
+ requested:
267
+
268
+ ```bash
269
+ maggie service import-readiness --project . --python-module selenium \
270
+ --browser --webdriver --env MAPS_CRAWLER_KEY --country "United Kingdom" \
271
+ --output docs/import-readiness.json
272
+ ```
273
+
274
+ Imported business facts keep field-level provenance in
275
+ `maggie-service-fact-provenance.v1`
276
+ (`contracts/maggie-service-booking/fact-provenance-v1.schema.json`). A
277
+ user-entered value is authoritative and always wins; provider values carry a
278
+ source, confidence, and reference. Providers that agree corroborate a value;
279
+ providers that disagree above `minConfidence` produce a `needs-review` conflict
280
+ with the policy's proposed choice (`fieldPrecedence` can, for example, prefer
281
+ the website for `websiteUrl`). A provider error is recorded under
282
+ `unavailableSources` and never blocks other providers or fields:
283
+
284
+ ```bash
285
+ maggie service fact-provenance --project . \
286
+ --facts .maggie/booking/fact-provenance.json --output docs/fact-merge.json
287
+ ```
288
+
289
+ The command exits 1 while conflicts need review; the host review UI reads the
290
+ `maggie-service-fact-merge.v1` artifact and records the reviewer's choice as a
291
+ user-entered value.
292
+
234
293
  `convert-page` imports a normal page as a draft service without inventing
235
294
  booking facts. `match-pages` reads filesystem pages plus `docs/pages.json` and
236
295
  `docs/page-content.json`, writes candidate evidence to both `.maggie/booking`