@topy-ai/maggie 0.7.0 → 0.7.2

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 +35 -9
  2. package/bin/maggie.js +6 -6
  3. package/bundled-references/maggiedash-dashboard-ui.md +28 -0
  4. package/bundled-skills/maggie-blog/SKILL.md +12 -0
  5. package/bundled-skills/maggie-content-localization/SKILL.md +9 -0
  6. package/bundled-skills/maggie-dash/SKILL.md +45 -2
  7. package/bundled-skills/maggie-deployment/SKILL.md +17 -0
  8. package/bundled-skills/maggie-ops/SKILL.md +27 -0
  9. package/bundled-skills/maggie-seo-geo/SKILL.md +21 -1
  10. package/bundled-skills/maggie-service-booking/SKILL.md +6 -0
  11. package/bundled-templates/maggiedash/README.md +15 -0
  12. package/bundled-templates/maggiedash/dashboard-ui-contract.json +31 -0
  13. package/bundled-tools/clis/maggie.py +29 -4
  14. package/bundled-tools/clis/maggie_analytics.py +14 -1
  15. package/bundled-tools/clis/maggie_blog.py +6 -0
  16. package/bundled-tools/clis/maggie_dash.py +179 -0
  17. package/bundled-tools/clis/maggie_deployment.py +43 -0
  18. package/bundled-tools/clis/maggie_feedback.py +16 -1
  19. package/bundled-tools/clis/maggie_ops.py +21 -1
  20. package/bundled-tools/clis/maggie_service_booking.py +6 -3
  21. package/bundled-tools/clis/maggie_sitemap.py +16 -2
  22. package/bundled-tools/runtime/analytics_traffic.py +30 -0
  23. package/bundled-tools/runtime/content_localization.py +63 -1
  24. package/bundled-tools/runtime/dependency_lock.py +43 -0
  25. package/bundled-tools/runtime/integration_state.py +17 -0
  26. package/bundled-tools/runtime/maggie_dash_store.py +150 -6
  27. package/bundled-tools/runtime/maggie_dash_ui.py +60 -0
  28. package/bundled-tools/runtime/maggie_sitemap.py +50 -4
  29. package/bundled-tools/runtime/route_imports.py +51 -0
  30. package/bundled-tools/runtime/seed_evidence.py +25 -0
  31. package/package.json +1 -1
  32. package/references/maggiedash-dashboard-ui.md +28 -0
package/README.md CHANGED
@@ -14,9 +14,9 @@ persistent project memory.
14
14
 
15
15
  ## Install
16
16
 
17
- Working-tree additions (not yet released): `site-audit --crawl --save-baseline
18
- FILE --reviewer NAME` records a reviewed site contract; `site-audit --crawl
19
- --baseline FILE` fails on URL, metadata, HTML structure or copy changes.
17
+ `site-audit --crawl --save-baseline FILE --reviewer NAME` records a reviewed
18
+ site contract; `site-audit --crawl --baseline FILE` fails on URL, metadata,
19
+ HTML structure or copy changes.
20
20
  Complete sitemap coverage is required and existing baselines cannot be
21
21
  overwritten. This does not verify browser layout or source-only changes.
22
22
 
@@ -66,8 +66,9 @@ Maggie in this order:
66
66
  ```bash
67
67
  python3 tools/clis/maggie.py analyze . --json --save
68
68
  python3 tools/clis/maggie.py bootstrap interview .
69
- python3 tools/clis/maggie_dash.py init --project . --confirm
70
- python3 tools/clis/maggie_dash.py migrate --project . --confirm
69
+ maggie dash install --project . --confirm
70
+ maggie dash init --project . --confirm
71
+ maggie dash migrate --project . --confirm
71
72
  ```
72
73
 
73
74
  The normal daily loop is:
@@ -88,7 +89,7 @@ The CLI provides the installer plus durable workflow commands:
88
89
  maggie init | install | update | remove | list | doctor
89
90
  maggie cleanup --project . [--confirm]
90
91
  maggie bootstrap interview | phase ...
91
- maggie dash init | status | migrate
92
+ maggie dash install | init | status | migrate | cms ...
92
93
  maggie dash transition ... # explicit content approval transition
93
94
  maggie dash variant ... # service variant create/review/preview/publish
94
95
  maggie clone ... # authorized homepage capture
@@ -101,7 +102,7 @@ maggie localization ... # plan, validate, review, publish, stale
101
102
  maggie service ... # import, sync, generate, validate
102
103
  maggie seo performance ... # sampled PageSpeed/CWV report and baseline
103
104
  maggie seo images ... # inventory, variants, confirmation, validate
104
- maggie seo sitemap ... # typed plan, validate, apply, rollback
105
+ maggie seo sitemap ... # typed/semantic plan, agent-files, apply, rollback
105
106
  maggie deployment | migration | release | analytics | schedule
106
107
  maggie deployment canary --asset URL=SHA256 --render-report report.json
107
108
  maggie design icon-inventory --source-dir src --runtime assets/icons.css
@@ -190,21 +191,46 @@ artifact schemas.
190
191
  Recommended upgrade sequence for the current release:
191
192
 
192
193
  ```bash
193
- npx @topy-ai/maggie@0.7.0 update --project . --force
194
- npx @topy-ai/maggie@0.7.0 cleanup --project .
194
+ npx @topy-ai/maggie@0.7.2 update --project . --force
195
+ npx @topy-ai/maggie@0.7.2 cleanup --project .
195
196
  ```
196
197
 
198
+ Maintainers should pass npm credentials through the repository helper, never
199
+ as a command-line argument:
200
+
201
+ ```bash
202
+ node scripts/publish-npm.mjs --maggie-env-file ../.env
203
+ ```
204
+
205
+ The 0.7.2 workflow adds the installable MaggieDash admin distribution and
206
+ audited CMS operations (`cms revisions`,
207
+ `trash`, `restore`, `schedule`, `duplicate`, `redirect`, and signed
208
+ `preview`), import-authoritative service matching, shared translation indexes,
209
+ sanitized seed manifests, lockfile/analytics traffic checks, semantic sitemap
210
+ validation, locale-aware `llms.txt`/`sitemap.md`/`insights.md` generation,
211
+ explicit integration states, and deployment Origin/infrastructure/data
212
+ rollback gates.
213
+
197
214
  ## MaggieDash lifecycle
198
215
 
199
216
  For a new project, establish the local content and approval foundation before
200
217
  connecting providers or publishing:
201
218
 
202
219
  ```bash
220
+ maggie dash install --project . --confirm
203
221
  maggie dash init --project . --confirm
204
222
  maggie dash status --project .
205
223
  maggie doctor --project .
206
224
  ```
207
225
 
226
+ `maggie dash install` installs the first-party private MaggieDash dashboard
227
+ source into `./_maggie/admin`, preserving local files unless `--force` is used.
228
+ It records the resolved source revision in `.maggie/dash-install.json`. During
229
+ bootstrap this is enabled by default; use `--dash-source /path/to/MaggieDash`
230
+ for local development or a configured private Git URL. The host project keeps
231
+ ownership of routes, email/password auth, database, provider credentials, and
232
+ the `/api/maggie/*` adapter.
233
+
208
234
  Content remains draft-first and external writes remain explicit. Use the
209
235
  project-local memory workflow for confirmed preferences and reusable lessons;
210
236
  feedback drafts are never promoted to active memory automatically.
package/bin/maggie.js CHANGED
@@ -65,7 +65,7 @@ Usage:
65
65
  maggie list
66
66
  maggie doctor [--project PATH]
67
67
  maggie bootstrap interview [project]
68
- maggie dash init|status|migrate|transition|variant --project PATH [options]
68
+ maggie dash init|install|status|migrate|transition|variant|cms --project PATH [options]
69
69
  maggie dash status --project PATH
70
70
  maggie dash migrate --project PATH --confirm
71
71
  maggie content FILE --source PROVIDER --project PATH --confirm
@@ -92,7 +92,7 @@ Usage:
92
92
  maggie design author --project PATH --route /about --purpose TEXT --audience TEXT --confirm
93
93
  maggie auth reference --project PATH --confirm
94
94
  maggie auth check --project PATH [--production]
95
- maggie blog init|inspect|ingest|validate|publish|sitemap|settings|rollback --project PATH
95
+ maggie blog init|inspect|ingest|validate|publish|sitemap|settings|rollback|integration-state
96
96
  maggie design status <job-id>
97
97
  maggie service import <provider-url> --project PATH
98
98
  maggie service sync <provider-url> --project PATH
@@ -106,18 +106,18 @@ Usage:
106
106
  maggie deployment canary --project PATH --asset URL=SHA256 --render-report report.json --output docs/deployment-canary.json
107
107
  maggie migration --project PATH --environment staging
108
108
  maggie schedule PATH/.maggie/schedule.json --project PATH
109
- maggie analytics --project PATH --environment staging
109
+ maggie analytics [traffic-audit|release-gate] --project PATH --environment staging
110
110
  maggie release PATH --environment staging --target vps-with-cloudflare-dns
111
111
  maggie api lifecycle --project PATH [--execute --allow-quota]
112
112
  maggie memory <init|list|search|context|add|record-error|transition|export> --project PATH
113
113
  maggie localization <extract|plan|generate|preview|validate|review|publish|stale|glossary> [options]
114
- maggie seo performance|images|sitemap [options]
114
+ maggie seo performance|images|sitemap [options] (sitemap supports strict validate and agent-files)
115
115
  maggie feedback <collect|preview|submit|list> [options]
116
116
  maggie site-audit URL [--crawl] [--languages en-GB,es-MX,ja-JP] [--check-hreflang]
117
117
  maggie site-audit URL --crawl --save-baseline FILE --reviewer NAME
118
118
  maggie site-audit URL --crawl --baseline FILE
119
119
  maggie browser-audit URL --browse PATH --output DIR --required SELECTOR [--sticky SELECTOR]
120
- maggie ops audit --project PATH
120
+ maggie ops audit|preflight|verify|lockfiles|seed-manifest --project PATH
121
121
  maggie ops preflight --project PATH --write
122
122
 
123
123
  Examples:
@@ -290,7 +290,7 @@ function service(args) {
290
290
  const root = projectRoot(args);
291
291
  const script = join(root, "tools", "clis", "maggie_service_booking.py");
292
292
  if (!existsSync(script)) throw new Error(`service booking CLI is missing: ${script}`);
293
- const result = spawnSync("python3", [script, ...args], { stdio: "inherit", cwd: root });
293
+ const result = spawnSync("python3", [script, ...args], { stdio: "inherit", cwd: root, env: { ...process.env, MAGGIE_VERSION: PACKAGE_VERSION } });
294
294
  if (result.error) throw result.error;
295
295
  process.exitCode = result.status ?? 1;
296
296
  }
@@ -0,0 +1,28 @@
1
+ # MaggieDash dashboard UI contract
2
+
3
+ MaggieDash workspaces use the lightweight contract in
4
+ `templates/maggiedash/dashboard-ui-contract.json`. It is provider-neutral and
5
+ describes the dashboard chrome, not a framework-specific component library.
6
+
7
+ The reference is the users workspace: one `WorkspaceBar`, one sidebar/content
8
+ navigation relationship, and one `ContentTabs` row. A route must not render a
9
+ second horizontal navigation, duplicate its sidebar, or hide duplicate markup
10
+ with CSS. `WorkspaceCard` owns card framing; page routes own content.
11
+
12
+ `ContentTabs` accepts only the props it renders (`tabs`, `active`, `actions`,
13
+ and `children`). It does not accept a page `title` or `description`. If a page
14
+ needs a heading or description, render a dedicated page header or use
15
+ `WorkspaceBar` explicitly. This keeps component APIs honest and prevents
16
+ dead-copy drift.
17
+
18
+ Validate a host implementation with:
19
+
20
+ ```bash
21
+ maggie dash ui validate --contract templates/maggiedash/dashboard-ui-contract.json \
22
+ --source src/components/WorkspaceBar.tsx \
23
+ --source src/components/WorkspaceCard.tsx \
24
+ --source src/components/ContentTabs.tsx
25
+ ```
26
+
27
+ The check is static evidence. It does not replace an authenticated browser
28
+ review of the rendered dashboard at desktop and mobile sizes.
@@ -60,3 +60,15 @@ maggie design init --project . --surface blog \
60
60
 
61
61
  The blog skill supplies route/data semantics; `maggie-design` supplies the
62
62
  host-native components and responsive visual review.
63
+
64
+ Optional Search Console/AI visibility integrations must report state rather
65
+ than returning an ambiguous empty success payload. Use:
66
+
67
+ ```bash
68
+ maggie blog integration-state # not-configured
69
+ maggie blog integration-state --configured --consent-required # awaiting-consent
70
+ maggie blog integration-state --configured --authorized # ready
71
+ ```
72
+
73
+ Provider adapters should preserve the same `not-configured`,
74
+ `awaiting-consent`, `awaiting-authorization`, `ready`, and `error` semantics.
@@ -116,3 +116,12 @@ styles, SVG, and dynamic expressions. Supplying source and render reports to
116
116
  remains backward-compatible without those artifacts. See
117
117
  [`docs/localization-extraction-render-prd.md`](../../docs/localization-extraction-render-prd.md)
118
118
  for the artifact contract and limitations.
119
+
120
+ Use the shared translation index and route predicates from
121
+ `tools/runtime/content_localization.py`. A host must not keep a second wording
122
+ dictionary beside its translation registry: conflicting `(contentId, locale)`
123
+ records fail closed. Use `is_translated_path()` for both exact routes and
124
+ prefix routes, and do not let locale middleware capture root `.txt`, `.md`,
125
+ `.xml`, or `.json` assets. If a framework rewrites a localized request,
126
+ dedupe instrumentation with `rewrite_once(request_key, seen)` so one request
127
+ does not count twice.
@@ -5,8 +5,30 @@ description: Manage the MaggieDash project foundation, local content store, and
5
5
 
6
6
  # MaggieDash
7
7
 
8
- Use the provider-neutral MaggieDash contracts and CLI for project setup and
9
- content operations.
8
+ Use the provider-neutral MaggieDash contracts and CLI for project setup,
9
+ installation of the first-party admin workspace, and content operations.
10
+
11
+ ## Install the admin workspace
12
+
13
+ During bootstrap, the recommended decision is to install MaggieDash into
14
+ `./_maggie/admin`. The dashboard source lives in the private first-party
15
+ `TOPY-AI-LTD/MaggieDash` repository during the MVP phase; Git/gh credentials
16
+ must already be configured for the project owner.
17
+
18
+ ```bash
19
+ maggie dash install --project . --confirm
20
+ ```
21
+
22
+ The command is idempotent, preserves existing files unless `--force` is
23
+ explicitly supplied, and records the resolved revision in
24
+ `.maggie/dash-install.json`. It never accepts or stores a GitHub token as a
25
+ CLI argument. For local development or a pinned checkout, use
26
+ `--source /path/to/MaggieDash` or `--source URL --ref REF`.
27
+
28
+ The installed dashboard owns the React workspace UI. The host project still
29
+ owns the framework route, email/password session middleware, database, media
30
+ storage, provider credentials, and `/api/maggie/*` adapter endpoints. Read the
31
+ MaggieDash host adapter contract before adding a new framework adapter.
10
32
 
11
33
  ## Workflow
12
34
 
@@ -74,3 +96,24 @@ not part of this skill.
74
96
  Before and after a meaningful run, load and record confirmed project
75
97
  preferences or repaired pitfalls with
76
98
  [the shared memory hook](../../references/memory-hook.md).
99
+
100
+ CMS operations are explicit and auditable:
101
+
102
+ ```bash
103
+ maggie dash cms revisions --project . --project-id local-project --document-id <id> --confirm
104
+ maggie dash cms trash --project . --project-id local-project --document-id <id> --reason "remove from editor" --confirm
105
+ maggie dash cms restore --project . --project-id local-project --document-id <id> --reason "restore" --confirm
106
+ maggie dash cms schedule --project . --project-id local-project --document-id <id> \
107
+ --publish-at 2026-09-20T10:00:00Z --reason "approved release" --confirm
108
+ maggie dash cms duplicate --project . --project-id local-project --document-id <id> \
109
+ --new-id <new-id> --new-slug <new-slug> --reason "create draft" --confirm
110
+ maggie dash cms redirect --project . --project-id local-project --from-path /old --to-path /new \
111
+ --reason "canonical slug change" --confirm
112
+ maggie dash cms preview --project . --project-id local-project --document-id <id> \
113
+ --secret "$MAGGIE_PREVIEW_SECRET" --confirm
114
+ ```
115
+
116
+ Content writes create immutable revision snapshots. Trash is reversible and
117
+ does not destroy the document. Scheduling is accepted only for approved
118
+ content and requires a timezone. Preview tokens are short-lived HMAC-signed
119
+ tokens; never place the secret in source control or generated reports.
@@ -210,3 +210,20 @@ Follow the shared [Maggie Decision Loop](../../references/decision-loop.md) for
210
210
  6. Require explicit final confirmation before any mutation or external write.
211
211
 
212
212
  If a decision is not relevant, record it as `skipped` with a reason. Do not silently assume a missing choice, and do not treat an existing output as permission to skip required work.
213
+
214
+ For scheduled POST jobs, validate the request contract before installation:
215
+
216
+ ```bash
217
+ maggie deployment --validate-request --method POST \
218
+ --origin https://example.test --has-auth
219
+ maggie deployment --verify-infra --service fresha \
220
+ --units-dir .maggie/deployment/schedules
221
+ ```
222
+
223
+ The deployment scheduler must send an explicit Origin and configured auth
224
+ contract; a missing Origin must not be mistaken for an application auth
225
+ failure. Infrastructure verification accepts a declared service/timer pair or
226
+ both units being enabled in systemd. Data-dependent releases additionally
227
+ require `rollback.backupId` and `rollback.restoreCommand` in
228
+ `.maggie/deployment/data-release.json`, because switching code alone does not
229
+ restore incompatible data.
@@ -7,6 +7,19 @@ metadata:
7
7
 
8
8
  # Maggie Ops
9
9
 
10
+ Before release, run the lockfile guard when a project has more than one package
11
+ manager:
12
+
13
+ ```bash
14
+ maggie ops lockfiles --project .
15
+ ```
16
+
17
+ It compares direct package and optional dependency names in `package.json`
18
+ with `package-lock.json`, reports the presence of `pnpm-lock.yaml`, and fails
19
+ when npm CI cannot install a declared package. It does not rewrite either
20
+ lockfile; regenerate and commit both through the project's chosen package
21
+ manager workflow.
22
+
10
23
  ## Automatic memory hook
11
24
 
12
25
  Follow [Maggie Memory Hook](../../references/memory-hook.md) at invocation and completion.
@@ -195,3 +208,17 @@ Follow the shared [Maggie Decision Loop](../../references/decision-loop.md) for
195
208
  6. Require explicit final confirmation before any mutation or external write.
196
209
 
197
210
  If a decision is not relevant, record it as `skipped` with a reason. Do not silently assume a missing choice, and do not treat an existing output as permission to skip required work.
211
+
212
+ Before data-dependent checks, validate an explicit sanitized fixture manifest:
213
+
214
+ ```bash
215
+ maggie ops seed-manifest --project . --manifest .maggie/seed-manifest.json
216
+ maggie ops lockfiles --project .
217
+ ```
218
+
219
+ The seed manifest must be opt-in, sanitized, and contain non-empty fixtures;
220
+ an empty dev database is not evidence that an operational check passed.
221
+ Known Maggie/toolchain traffic can be measured without polluting first-party
222
+ analytics using `maggie analytics traffic-audit --events events.json`. The
223
+ classifier excludes only explicit tool/test markers and never infers identity
224
+ from IP or private fields.
@@ -33,6 +33,11 @@ lastmod, optional JSON object mapping locale tags to alternate URLs, such as
33
33
  It emits `lastmod` and `xhtml:link rel="alternate"`. Supply truthful content
34
34
  change dates; omit unknown dates. Alternates must currently use the approved
35
35
  origin. Reciprocal locale coverage and date semantics require separate review.
36
+ `lastmod` must come from a recorded content-change event or source revision;
37
+ never derive it from `updated_at`, pull time, sync time, or deployment time.
38
+ If the change date is unknown, omit `lastmod`. An empty content-type does not
39
+ need a sitemap chunk in the sitemap index; serving an empty endpoint and
40
+ advertising it are separate decisions.
36
41
 
37
42
  ## Freeze and compare a reviewed site
38
43
 
@@ -122,7 +127,7 @@ maggie seo sitemap rollback --backup-manifest .maggie-sitemap-backups/<plan>/bac
122
127
 
123
128
  Only confirmed image variants may enter `srcset`; `apply` requires an explicit
124
129
  confirmation and host adapter. Sitemap plans keep content types separate,
125
- emit chunk 1 for empty enabled types, enforce absolute same-origin URLs, and
130
+ omit empty chunks from the sitemap index, enforce absolute same-origin URLs, and
126
131
  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)
127
132
  for adapter and rollback rules.
128
133
 
@@ -207,3 +212,18 @@ Follow the shared [Maggie Decision Loop](../../references/decision-loop.md) for
207
212
  6. Require explicit final confirmation before any mutation or external write.
208
213
 
209
214
  If a decision is not relevant, record it as `skipped` with a reason. Do not silently assume a missing choice, and do not treat an existing output as permission to skip required work.
215
+
216
+ Use semantic validation when route evidence is available:
217
+
218
+ ```bash
219
+ maggie seo sitemap validate --plan .maggie/sitemap-plan.json --strict-semantic
220
+ maggie seo sitemap agent-files --origin https://example.test \
221
+ --routes-file .maggie/routes.tsv --locale fr-FR --output-dir public/fr
222
+ ```
223
+
224
+ Strict validation checks searchable content evidence, indexability, self
225
+ canonical ownership and truthful `lastmod` provenance. `lastmod` may only be
226
+ backed by a content-change/source-revision event; operational sync, pull,
227
+ deploy, or `updated_at` timestamps are rejected. The agent-file command emits
228
+ locale-aware `llms.txt`, `sitemap.md`, and `insights.md` from the same route
229
+ inventory; non-indexable routes are omitted.
@@ -7,6 +7,12 @@ metadata:
7
7
 
8
8
  # Maggie Service Booking
9
9
 
10
+ Route matching evidence must come from the route source's actual import
11
+ statements. The shared resolver in `tools/runtime/route_imports.py` resolves
12
+ relative imports and records a component fingerprint; it never selects a
13
+ same-basename sibling as a fallback. Missing imports are evidence requiring
14
+ review, not permission to guess.
15
+
10
16
  ## Reviewed page relationships
11
17
 
12
18
  Reviewed page selection preserves existing `supporting` relations, including
@@ -7,3 +7,18 @@ own framework conventions for rendering.
7
7
  The package includes only these lightweight contracts. Large marketplace
8
8
  previews and template media remain separately distributable through the
9
9
  marketplace repository.
10
+
11
+ Dashboard routes should adopt `dashboard-ui-contract.json` and validate their
12
+ actual host components with `maggie dash ui validate`. The contract provides
13
+ the shared workspace chrome and rejects components that declare dead props.
14
+
15
+ The first-party dashboard distribution is installed separately into
16
+ `./_maggie/admin`:
17
+
18
+ ```bash
19
+ maggie dash install --project . --confirm
20
+ ```
21
+
22
+ It records the source revision at `.maggie/dash-install.json`; host routes,
23
+ email/password authentication, and `/api/maggie/*` adapters remain owned by the
24
+ website project.
@@ -0,0 +1,31 @@
1
+ {
2
+ "schemaVersion": "maggiedash-dashboard-ui.v1",
3
+ "shell": {
4
+ "reference": "users-workspace",
5
+ "navigation": "sidebar-plus-content-tabs",
6
+ "rules": [
7
+ "render one workspace bar per route",
8
+ "render one content tab bar per route",
9
+ "do not duplicate sidebar navigation in page content",
10
+ "do not hide duplicate navigation with CSS"
11
+ ]
12
+ },
13
+ "components": [
14
+ {
15
+ "name": "WorkspaceBar",
16
+ "props": ["workspaceName", "activeSection", "actions", "children"],
17
+ "forbiddenProps": []
18
+ },
19
+ {
20
+ "name": "WorkspaceCard",
21
+ "props": ["title", "children"],
22
+ "forbiddenProps": []
23
+ },
24
+ {
25
+ "name": "ContentTabs",
26
+ "props": ["tabs", "active", "actions", "children"],
27
+ "forbiddenProps": ["title", "description"]
28
+ }
29
+ ],
30
+ "accessibility": ["active tab exposes aria-selected", "actions have accessible names", "workspace navigation has a landmark"]
31
+ }
@@ -8,6 +8,7 @@ import hashlib
8
8
  import json
9
9
  import os
10
10
  import re
11
+ import subprocess
11
12
  import sys
12
13
  import time
13
14
  from datetime import datetime, timezone
@@ -561,6 +562,11 @@ def command_bootstrap_phase(args: argparse.Namespace) -> int:
561
562
  if analysis.get("maggiedash", {}).get("status") not in {"Detected", "Planned"}:
562
563
  print("GATE_BLOCKED: MaggieDash project contract is unavailable.", file=sys.stderr)
563
564
  return 2
565
+ bootstrap = load_state(root) or {}
566
+ decisions = bootstrap.get("decisions", {})
567
+ if decisions.get("maggiedash_admin") and not (root / STATE_DIR / "dash-install.json").exists():
568
+ print("GATE_BLOCKED: MaggieDash admin is enabled but not installed; run `maggie dash install --project . --confirm`.", file=sys.stderr)
569
+ return 2
564
570
  phase["status"] = "passed"
565
571
  phase["validation"] = args.validation
566
572
  phase["validated_at"] = utc_now()
@@ -589,7 +595,7 @@ BOOTSTRAP_QUESTIONS = [
589
595
  ("content_source", "Where should posts be stored?", ["preserve", "local-content", "database", "api-pull"]),
590
596
  ("posts_per_page", "How many posts should appear on each blog page?", ["9", "12", "6", "custom"]),
591
597
  ("grid_columns", "How many columns should the default post grid use?", ["3", "2", "4", "custom"]),
592
- ("ops_dashboard", "Should the project add the private Maggie Ops dashboard after bootstrap?", ["disabled", "enabled"]),
598
+ ("maggiedash_admin", "Should bootstrap install the private MaggieDash admin workspace into ./_maggie/admin?", ["enabled", "disabled"]),
593
599
  ]
594
600
 
595
601
 
@@ -604,7 +610,7 @@ def question_default(key: str, analysis: dict) -> str:
604
610
  detected = analysis.get(key, {}).get("value") if isinstance(analysis.get(key), dict) else None
605
611
  if key == "posts_per_page": return "9"
606
612
  if key == "grid_columns": return "3"
607
- if key == "ops_dashboard": return "disabled"
613
+ if key == "maggiedash_admin": return "enabled"
608
614
  return detected or {"ui_system": "preserve-or-tailwind", "icon_set": "phosphor", "font": "preserve-or-system", "database": "preserve-or-sqlite"}.get(key, "detected")
609
615
 
610
616
 
@@ -641,6 +647,8 @@ def command_bootstrap_interview(args: argparse.Namespace) -> int:
641
647
  raise RuntimeError(f"invalid answers file: {args.answers_file}") from error
642
648
  if not isinstance(loaded, dict): raise RuntimeError("answers file must contain a JSON object")
643
649
  answers.update({str(key): str(value) for key, value in loaded.items()})
650
+ if "maggiedash_admin" not in answers and "ops_dashboard" in answers:
651
+ answers["maggiedash_admin"] = answers["ops_dashboard"]
644
652
  for key, question, choices in BOOTSTRAP_QUESTIONS:
645
653
  value = answers.get(key, question_default(key, analysis))
646
654
  if not args.non_interactive and key not in answers:
@@ -665,8 +673,10 @@ def command_bootstrap_interview(args: argparse.Namespace) -> int:
665
673
  project=str(root), confirm=["foundation", "experience", "data", "publishing"],
666
674
  framework=answers["framework"], language=answers["language"], content_language=answers["content_language"],
667
675
  ui_system=answers["ui_system"], icon_set=answers["icon_set"], font=answers["font"], database=answers["database"], content_source=answers["content_source"],
668
- posts_per_page=int(answers["posts_per_page"]), grid_columns=int(answers["grid_columns"]), ops_dashboard=answers["ops_dashboard"] == "enabled",
676
+ posts_per_page=int(answers["posts_per_page"]), grid_columns=int(answers["grid_columns"]),
677
+ ops_dashboard=answers["maggiedash_admin"] == "enabled", maggiedash_admin=answers["maggiedash_admin"] == "enabled",
669
678
  site_domain=answers["site_domain"], api_key_present=answers["api_key_present"], foundation_source=answers["foundation_source"], maggiedash_path=None,
679
+ dash_source=getattr(args, "dash_source", None),
670
680
  )
671
681
  return command_complete(complete_args)
672
682
 
@@ -685,6 +695,17 @@ def command_complete(args: argparse.Namespace) -> int:
685
695
  if foundation_analysis.get("maggiedash", {}).get("status") != "Detected":
686
696
  print("FOUNDATION_REQUIRED: use-existing-maggiedash was selected but no MaggieDash contract was detected.", file=sys.stderr)
687
697
  return 2
698
+ install_admin = bool(getattr(args, "maggiedash_admin", False))
699
+ if install_admin:
700
+ root.mkdir(parents=True, exist_ok=True)
701
+ command = [sys.executable, str(Path(__file__).with_name("maggie_dash.py")), "install", "--project", str(root), "--confirm"]
702
+ dash_source = getattr(args, "dash_source", None)
703
+ if dash_source:
704
+ command.extend(["--source", dash_source])
705
+ result = subprocess.run(command, cwd=root, check=False)
706
+ if result.returncode:
707
+ print("MAGGIEDASH_INSTALL_FAILED: bootstrap was not completed; resolve the private MaggieDash source and retry.", file=sys.stderr)
708
+ return result.returncode
688
709
  state = {
689
710
  "schema_version": "1.1",
690
711
  "status": "completed",
@@ -707,6 +728,7 @@ def command_complete(args: argparse.Namespace) -> int:
707
728
  "posts_per_page": args.posts_per_page,
708
729
  "grid_columns": args.grid_columns,
709
730
  "ops_dashboard": args.ops_dashboard,
731
+ "maggiedash_admin": install_admin,
710
732
  },
711
733
  }
712
734
  path = state_path(root)
@@ -714,7 +736,7 @@ def command_complete(args: argparse.Namespace) -> int:
714
736
  path.write_text(json.dumps(state, indent=2, ensure_ascii=False) + "\n", encoding="utf-8")
715
737
  contracts = {
716
738
  "project.json": {"schema_version": "1.0", "framework": args.framework, "language": args.language, "database": args.database, "content_source": args.content_source, "site_domain": getattr(args, "site_domain", "none"), "context_source": state["decisions"]["context_source"], "foundation_source": foundation_source},
717
- "maggiedash.json": {"schema_version": "1.0", "source": foundation_source, "path": getattr(args, "maggiedash_path", None), "scaffold_command": "maggie dash init --project ." if foundation_source == "create-maggiedash" else None, "capabilities": ["content", "admin", "auth", "media", "plugins", "api", "mcp"], "status": "detected" if foundation_source == "use-existing-maggiedash" else "planned" if foundation_source == "create-maggiedash" else "not-selected"},
739
+ "maggiedash.json": {"schema_version": "1.0", "source": foundation_source, "path": getattr(args, "maggiedash_path", None), "scaffold_command": "maggie dash init --project ." if foundation_source == "create-maggiedash" else None, "admin_install": {"enabled": install_admin, "target": "./_maggie/admin", "state": ".maggie/dash-install.json" if install_admin else None}, "capabilities": ["content", "admin", "auth", "media", "plugins", "api", "mcp"], "status": "detected" if foundation_source == "use-existing-maggiedash" else "planned" if foundation_source == "create-maggiedash" else "not-selected"},
718
740
  "decisions.json": state["decisions"],
719
741
  "schema.json": {"post_required": ["id", "slug", "title", "content", "status", "canonicalUrl", "publishedAt", "updatedAt", "author"], "public_status": "published", "posts_per_page": args.posts_per_page, "grid_columns": args.grid_columns, "grid_default": f"{args.grid_columns}x{args.posts_per_page // args.grid_columns}"},
720
742
  "routes.json": {"public": ["/about", "/blog", "/blog/page/[page]", "/blog/[slug]", "/topics", "/topics/[slug]", "/authors/[slug]", "/sitemap.xml", "/robots.txt"], "ops": ["/ops", "/ops/posts", "/ops/posts/new", "/ops/posts/[id]/edit", "/ops/posts/[id]/preview", "/ops/topics", "/ops/sitemap", "/ops/reports", "/ops/settings/site", "/ops/settings/integrations", "/ops/operations", "/ops/wordpress", "/api/ops/summary", "/api/ops/posts", "/api/ops/topics", "/api/ops/bulk", "/api/ops/agency", "/api/ops/entities", "/api/ops/migrations/import", "/api/ops/migrations/[id]/resume", "/api/ops/wordpress/migration-plan", "/api/ops/pull/project-context", "/api/ops/pull/sync", "/api/ops/pull/sync-updates", "/api/ops/sitemap/matching-history", "/api/ops/sitemap/match", "/api/ops/sitemap/auto-detect", "/api/ops/rewrite/queue", "/api/ops/rewrite/history", "/api/ops/rewrite/policy", "/api/ops/content-tracking/report-state", "/api/ops/content-tracking/report-state/batch", "/api/ops/reports", "/api/ops/playbooks/[id]/run"]},
@@ -903,6 +925,7 @@ def parser() -> argparse.ArgumentParser:
903
925
  interview.add_argument("--answers-file", help="JSON answers for headless/CI use")
904
926
  interview.add_argument("--non-interactive", action="store_true", help="do not prompt; requires --answers-file and --confirm")
905
927
  interview.add_argument("--confirm", action="store_true", help="apply the reviewed proposal")
928
+ interview.add_argument("--dash-source", help="local checkout or Git URL for the MaggieDash admin source")
906
929
  interview.set_defaults(func=command_bootstrap_interview)
907
930
  phase = bootstrap_sub.add_parser("phase", help="start/pass/fail one ordered workflow phase")
908
931
  phase.add_argument("action", choices=["status", "start", "pass", "fail"])
@@ -915,11 +938,13 @@ def parser() -> argparse.ArgumentParser:
915
938
  complete.add_argument("--api-key-present", choices=["yes", "no"], default="no")
916
939
  complete.add_argument("--foundation-source", choices=["use-existing-maggiedash", "create-maggiedash", "preserve-existing-project"], default="preserve-existing-project")
917
940
  complete.add_argument("--maggiedash-path", default=None, help="path to an existing MaggieDash project contract")
941
+ complete.add_argument("--dash-source", help="local checkout or Git URL for the MaggieDash admin source")
918
942
  for name, default in (("framework", "detected"), ("language", "detected"), ("content-language", "confirmed"), ("ui-system", "preserve-or-tailwind"), ("icon-set", "phosphor"), ("font", "preserve-or-system"), ("database", "preserve-or-sqlite"), ("content-source", "confirmed")):
919
943
  complete.add_argument("--" + name, dest=name.replace("-", "_"), default=default)
920
944
  complete.add_argument("--posts-per-page", type=int, default=9)
921
945
  complete.add_argument("--grid-columns", type=int, default=3)
922
946
  complete.add_argument("--ops-dashboard", action=argparse.BooleanOptionalAction, default=False)
947
+ complete.add_argument("--maggiedash-admin", action=argparse.BooleanOptionalAction, default=False)
923
948
  complete.set_defaults(func=command_complete)
924
949
  generate = sub.add_parser("generate", help="generate stable contract or fixture files")
925
950
  generate.add_argument("target", choices=["contract", "fixture", "post", "topic", "author", "migration", "seo", "ops-page"])
@@ -10,6 +10,9 @@ import re
10
10
  import subprocess
11
11
  from pathlib import Path
12
12
  from urllib.parse import urlsplit
13
+ import sys
14
+ sys.path.insert(0, str(Path(__file__).resolve().parents[1] / "runtime"))
15
+ from analytics_traffic import audit_events # noqa: E402
13
16
 
14
17
 
15
18
  GA4_ID = re.compile(r"^G-[A-Z0-9]+$", re.I)
@@ -107,7 +110,7 @@ def release_gate(args: argparse.Namespace) -> int:
107
110
 
108
111
  def main() -> int:
109
112
  parser = argparse.ArgumentParser()
110
- parser.add_argument("command", nargs="?", choices=("release-gate",))
113
+ parser.add_argument("command", nargs="?", choices=("release-gate", "traffic-audit"))
111
114
  parser.add_argument("--project", default=".")
112
115
  parser.add_argument("--environment", choices=("development", "staging", "production"), default="staging")
113
116
  parser.add_argument("--env-file", help="optional env file; values are never printed")
@@ -117,12 +120,22 @@ def main() -> int:
117
120
  parser.add_argument("--network-report", help="redacted network evidence for release-gate")
118
121
  parser.add_argument("--provider-report", help="read-only provider evidence for release-gate")
119
122
  parser.add_argument("--smoke-report", help="production smoke evidence for release-gate")
123
+ parser.add_argument("--events", help="redacted JSON array of analytics events for traffic-audit")
120
124
  args = parser.parse_args()
121
125
  if args.command == "release-gate":
122
126
  required = ("contract", "render_report", "network_report", "provider_report", "smoke_report")
123
127
  if any(not getattr(args, name) for name in required):
124
128
  parser.error("release-gate requires --contract, --render-report, --network-report, --provider-report, and --smoke-report")
125
129
  return release_gate(args)
130
+ if args.command == "traffic-audit":
131
+ if not args.events:
132
+ parser.error("traffic-audit requires --events")
133
+ value = json.loads(Path(args.events).read_text(encoding="utf-8"))
134
+ if not isinstance(value, list):
135
+ parser.error("--events must contain a JSON array")
136
+ result = audit_events(value)
137
+ print(json.dumps(result, indent=2, ensure_ascii=False))
138
+ return 0
126
139
  project = Path(args.project).resolve()
127
140
  env = parse_env(Path(args.env_file).resolve()) if args.env_file else parse_env(project / ".env.example")
128
141
  env.update({key: value for key, value in os.environ.items() if key in {"PUBLIC_GA4_MEASUREMENT_ID", "PUBLIC_ANALYTICS_ENABLED", "PUBLIC_ANALYTICS_CONSENT_REQUIRED", "GSC_SITE_URL", "GSC_VERIFICATION_TOKEN"}})
@@ -11,6 +11,7 @@ from pathlib import Path
11
11
  sys.path.insert(0, str(Path(__file__).resolve().parents[1] / "runtime"))
12
12
  from maggie_blog import BlogStore # noqa: E402
13
13
  from localization_runner import process_adapter
14
+ from integration_state import integration_state # noqa: E402
14
15
 
15
16
 
16
17
  def main() -> int:
@@ -29,8 +30,13 @@ def main() -> int:
29
30
  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")
30
31
  sitemap = sub.add_parser("sitemap"); sitemap.add_argument("--project", type=Path, default=Path.cwd())
31
32
  settings = sub.add_parser("settings"); settings.add_argument("--project", type=Path, default=Path.cwd())
33
+ state = sub.add_parser("integration-state"); state.add_argument("--configured", action="store_true"); state.add_argument("--consent-required", action="store_true"); state.add_argument("--consent", action="store_true"); state.add_argument("--authorized", action="store_true"); state.add_argument("--error")
32
34
  rollback = sub.add_parser("rollback"); rollback.add_argument("--project", type=Path, default=Path.cwd()); rollback.add_argument("--backup"); rollback.add_argument("--confirm", action="store_true")
33
35
  args = parser.parse_args()
36
+ if args.command == "integration-state":
37
+ result = integration_state(configured=args.configured, consent_required=args.consent_required, consent=args.consent, authorized=args.authorized, error=args.error)
38
+ print(json.dumps(result, indent=2, ensure_ascii=False))
39
+ return 0 if result["status"] != "error" else 1
34
40
  store = BlogStore(args.project.resolve())
35
41
  if args.command in {"init", "ingest", "publish", "rollback", "translate-pending"} and not args.confirm:
36
42
  print("CONFIRMATION_REQUIRED: rerun with --confirm", file=sys.stderr); return 2