@topy-ai/maggie 0.6.8 → 0.7.0

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 (30) hide show
  1. package/README.md +39 -2
  2. package/README.zh-TW.md +1 -1
  3. package/bin/maggie.js +6 -2
  4. package/bundled-references/blog-translation-ingestion.md +52 -0
  5. package/bundled-skills/maggie-blog/SKILL.md +16 -0
  6. package/bundled-skills/maggie-content-localization/SKILL.md +41 -1
  7. package/bundled-skills/maggie-dash/SKILL.md +31 -0
  8. package/bundled-skills/maggie-deployment/SKILL.md +1 -1
  9. package/bundled-skills/maggie-design/SKILL.md +12 -1
  10. package/bundled-skills/maggie-memory/SKILL.md +7 -0
  11. package/bundled-skills/maggie-seo-geo/SKILL.md +75 -1
  12. package/bundled-skills/maggie-service-booking/SKILL.md +11 -1
  13. package/bundled-tools/clis/maggie_blog.py +13 -1
  14. package/bundled-tools/clis/maggie_browser_audit.py +78 -0
  15. package/bundled-tools/clis/maggie_dash.py +47 -0
  16. package/bundled-tools/clis/maggie_design.py +12 -0
  17. package/bundled-tools/clis/maggie_localization.py +42 -0
  18. package/bundled-tools/clis/maggie_memory.py +4 -1
  19. package/bundled-tools/clis/maggie_service_booking.py +3 -3
  20. package/bundled-tools/clis/site_audit.py +103 -8
  21. package/bundled-tools/runtime/browser_behavior.py +35 -0
  22. package/bundled-tools/runtime/browser_geometry.js +31 -0
  23. package/bundled-tools/runtime/localization_runner.py +113 -0
  24. package/bundled-tools/runtime/maggie_blog.py +66 -1
  25. package/bundled-tools/runtime/maggie_memory.py +7 -2
  26. package/bundled-tools/runtime/maggie_sitemap.py +16 -3
  27. package/bundled-tools/runtime/service_variants.py +152 -0
  28. package/bundled-tools/runtime/site_baseline.py +60 -0
  29. package/package.json +1 -1
  30. package/references/blog-translation-ingestion.md +52 -0
package/README.md CHANGED
@@ -14,6 +14,16 @@ 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.
20
+ Complete sitemap coverage is required and existing baselines cannot be
21
+ overwritten. This does not verify browser layout or source-only changes.
22
+
23
+ `localization generate` invokes a trusted project-supplied provider process
24
+ with explicit `--confirm`, resumable draft batches and no automatic publishing.
25
+ See the installed localization skill for the adapter protocol and examples.
26
+
17
27
  ```bash
18
28
  npx @topy-ai/maggie init --agent codex
19
29
  ```
@@ -80,6 +90,7 @@ maggie cleanup --project . [--confirm]
80
90
  maggie bootstrap interview | phase ...
81
91
  maggie dash init | status | migrate
82
92
  maggie dash transition ... # explicit content approval transition
93
+ maggie dash variant ... # service variant create/review/preview/publish
83
94
  maggie clone ... # authorized homepage capture
84
95
  maggie design ... # authorized interior-page design
85
96
  maggie clone-to-template ... # URL → validated marketplace template
@@ -179,8 +190,8 @@ artifact schemas.
179
190
  Recommended upgrade sequence for the current release:
180
191
 
181
192
  ```bash
182
- npx @topy-ai/maggie@0.6.8 update --project . --force
183
- npx @topy-ai/maggie@0.6.8 cleanup --project .
193
+ npx @topy-ai/maggie@0.7.0 update --project . --force
194
+ npx @topy-ai/maggie@0.7.0 cleanup --project .
184
195
  ```
185
196
 
186
197
  ## MaggieDash lifecycle
@@ -419,6 +430,32 @@ Blog imports are idempotent and draft-first. Published slugs remain stable;
419
430
  public feeds exclude drafts, and `maggie blog rollback --confirm` restores the
420
431
  latest local content backup.
421
432
 
433
+ In the working tree, opt-in blog translation uses `autoTranslateEnabled` and
434
+ `translationLocales` in `.maggie/blog/settings.json`. Process or retry persisted
435
+ posts without a new pull:
436
+
437
+ ```bash
438
+ maggie blog translate-pending --project . \
439
+ --adapter-command '["python3", "scripts/translation-provider.py"]' --confirm
440
+ ```
441
+
442
+ The project supplies the trusted provider script. Completed locale/revision
443
+ tasks are skipped; failed work returns nonzero. Title, excerpt, body, topic
444
+ labels and supplied image alt text remain draft/non-indexable. Run one worker
445
+ per project; existing host HTTP schedulers require separate integration.
446
+
447
+ Browser behavior validation uses the shared gstack browser:
448
+
449
+ ```bash
450
+ maggie browser-audit https://example.com \
451
+ --browse "$HOME/.codex/skills/gstack/browse/dist/browse" \
452
+ --output .maggie/browser-audit --required main --sticky header \
453
+ --viewport 390x844 --viewport 768x1024
454
+ ```
455
+
456
+ It captures scroll/geometry samples and screenshots, then fails on hidden
457
+ required elements, horizontal overflow or invalid sticky positioning.
458
+
422
459
  List every installed skill and command:
423
460
 
424
461
  ```bash
package/README.zh-TW.md CHANGED
@@ -8,7 +8,7 @@ Codex、Claude Code 與相容的 coding agents。
8
8
  ## 安裝
9
9
 
10
10
  ```bash
11
- npx @topy-ai/maggie@0.6.8 init --agent all
11
+ npx @topy-ai/maggie@0.6.9 init --agent all
12
12
  npx @topy-ai/maggie doctor --project .
13
13
  ```
14
14
 
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 --project PATH --confirm
68
+ maggie dash init|status|migrate|transition|variant --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
@@ -110,10 +110,13 @@ Usage:
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
- maggie localization <plan|preview|validate|review|publish|stale|glossary> [options]
113
+ maggie localization <extract|plan|generate|preview|validate|review|publish|stale|glossary> [options]
114
114
  maggie seo performance|images|sitemap [options]
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
+ maggie site-audit URL --crawl --save-baseline FILE --reviewer NAME
118
+ maggie site-audit URL --crawl --baseline FILE
119
+ maggie browser-audit URL --browse PATH --output DIR --required SELECTOR [--sticky SELECTOR]
117
120
  maggie ops audit --project PATH
118
121
  maggie ops preflight --project PATH --write
119
122
 
@@ -390,6 +393,7 @@ try {
390
393
  else if (command === "localization") workflowCli("maggie_localization.py", args);
391
394
  else if (command === "feedback") workflowCli("maggie_feedback.py", args);
392
395
  else if (command === "site-audit") workflowCli("site_audit.py", args);
396
+ else if (command === "browser-audit") workflowCli("maggie_browser_audit.py", args);
393
397
  else if (command === "dash") workflowCli("maggie_dash.py", args);
394
398
  else if (command === "content") workflowCli("maggie_content.py", args);
395
399
  else if (command === "init" || command === "install") install(args);
@@ -0,0 +1,52 @@
1
+ # Blog translation ingestion contract
2
+
3
+ ## Entry points and ownership
4
+
5
+ List every ingest entry point before enabling automatic translation: manual
6
+ CLI, admin HTTP action, scheduled tick, webhook and backfill. Each must use
7
+ the same post-persistence scheduling function. Record the caller and source
8
+ revision in the run evidence. A hook in a standalone script is insufficient
9
+ evidence for a scheduled HTTP path.
10
+
11
+ The packaged Python BlogStore reconciles durable translation tasks after source
12
+ persistence when `autoTranslateEnabled` is true and `translationLocales` contains
13
+ supported locale tags. The same reconciliation runs before translation retries,
14
+ so an interruption between source persistence and scheduling is recoverable.
15
+ Tasks are under `.maggie/blog/translations/`; content changes create new task
16
+ identities, and only current identities are processed. Old drafts are retained
17
+ for inspection, not eligible for automatic publication.
18
+
19
+ Run `maggie blog translate-pending --project . --adapter-command
20
+ '["python3", "scripts/translation-provider.py"]' --confirm` to generate drafts
21
+ from saved sources without re-pulling. The project supplies a trusted adapter
22
+ using the localization provider protocol. It receives title, excerpt, body,
23
+ topic labels and supplied image alt text. Completed tasks are skipped; failures
24
+ return partial/nonzero and store only an error category. Use one worker per
25
+ project. Host database/API schedulers must explicitly integrate this boundary;
26
+ installing the skill does not patch an existing host ingestion implementation.
27
+
28
+ ## Durable work
29
+
30
+ After source persistence, enqueue translation work using an idempotency key
31
+ of project, content ID, source revision, target locale and operation. The
32
+ source write and scheduling intent must commit together or use a reconciliation
33
+ pass that detects missing intents. A repeat pull must retry pending/failed
34
+ translation without purchasing another upstream pull or duplicating completed
35
+ work. A changed source revision invalidates earlier translation work.
36
+
37
+ Track pending, running, succeeded and failed states with attempt count and
38
+ safe error category. A run with failed required translations is partial, not
39
+ completed. Missing credentials or provider availability must be visible;
40
+ never print credentials or provider payloads in the report. Keep translated
41
+ content draft and non-indexable until its validation and review pass.
42
+
43
+ ## Acceptance evidence
44
+
45
+ Exercise the actual CLI and scheduled HTTP entry points against fixture
46
+ providers. With translation enabled, both must schedule identical work for
47
+ the same source revision. With translation disabled, neither schedules work.
48
+ Test provider failure, process interruption, retry without re-pull, duplicate
49
+ delivery, changed source revision, multiple target locales, media alt text and
50
+ topic labels. Assert persisted target output and queue state, not merely the
51
+ presence of a translate function name. Browser preview must show the target
52
+ locale before publication is claimed complete.
@@ -11,6 +11,22 @@ project lessons, never raw provider credentials or temporary content facts.
11
11
 
12
12
  # Maggie Blog
13
13
 
14
+ ## Automatic translation across ingest paths
15
+
16
+ Follow the [translation ingestion contract](../../references/blog-translation-ingestion.md)
17
+ when adding host auto-translation. Inventory manual CLI, admin API and scheduler
18
+ entry points and connect each to the same durable translation scheduling step.
19
+ Verify the actual scheduled path with a fixture provider and failure/retry
20
+ tests. An enabled setting or successful manual run does not prove scheduled
21
+ translation works. Packaged BlogStore reconciles translation tasks when settings
22
+ enable `autoTranslateEnabled` with supported `translationLocales`. Run
23
+ `maggie blog translate-pending --project . --adapter-command
24
+ '["python3", "scripts/translation-provider.py"]' --confirm` with a trusted
25
+ project-supplied adapter to process saved posts without re-pulling. Completed
26
+ locale/revision tasks are skipped; failed tasks retry from checkpoints. Run one
27
+ worker per project. Outputs stay draft and non-indexable. Existing host HTTP
28
+ and scheduler integrations still need separate implementation and tests.
29
+
14
30
  Use the stable CLI to initialize a local blog contract, ingest versioned local
15
31
  content, validate lifecycle invariants, publish with an actor and reason, and
16
32
  generate route/feed artifacts. Read the host framework and database contract
@@ -2,11 +2,51 @@
2
2
  name: maggie-content-localization
3
3
  description: Manage translation, polish, rewrite, market localization, review, stale detection, and publishing for pages, guides, posts, services, products, and categories.
4
4
  metadata:
5
- version: 1.2.0
5
+ version: 1.3.0
6
6
  ---
7
7
 
8
8
  # Maggie Content Localization
9
9
 
10
+ ## Generation responsibility and operation contract
11
+
12
+ `plan` records `generationContract.mode`, `preserveMeaning`,
13
+ `allowStructuralRewrite`, and `marketAdaptation`. Rewrite permits structural
14
+ changes; translate and polish preserve meaning; localise enables market
15
+ adaptation. A host generation adapter or the authoring agent must apply these
16
+ instructions when producing copy. Planning does not invoke a model or generate
17
+ translations. Validation currently checks mode consistency and rewrite
18
+ permission, not whether prose was actually restructured. Review the produced
19
+ copy against the requested operation before approval.
20
+
21
+ ### Resumable draft generation
22
+
23
+ After extracting source strings and planning a job with `--source`, run:
24
+
25
+ ```bash
26
+ maggie localization generate .maggie/localization/<job>.json \
27
+ --project . --source .maggie/localization/source.json \
28
+ --output .maggie/localization/generation/<job>.json \
29
+ --adapter-command '["python3", "scripts/translation-provider.py"]' --confirm
30
+ ```
31
+
32
+ The adapter script is project-supplied, not bundled. Only execute a trusted
33
+ adapter: it receives source text and may call a paid external provider.
34
+ The JSON argv array executes without a shell, relative to `--project`.
35
+ Its stdin is a `maggie-provider-request.v1` object with `strings` and `contract`
36
+ (job identity, source revision, languages, locale, market and operation).
37
+ Its stdout must contain only a JSON object mapping every supplied string ID
38
+ to nonempty translated text. Exit nonzero on provider failure; provider logs
39
+ are suppressed from CLI errors to avoid exposing content or credentials.
40
+
41
+ `--character-budget` defaults to 8000 source characters per batch (a single
42
+ larger string remains intact); `--timeout` defaults to 120 seconds per call.
43
+ Malformed output splits batches until individual strings; provider failures
44
+ save completed batches for retry. Rerun with the same output to resume; changed
45
+ source or operation requires a new output. Run only one process per output.
46
+ The output is a draft checkpoint, not an approved translation: this command
47
+ does not edit project files, update the job, publish, or prove semantic quality.
48
+ Host ingestion integration and operation-quality verification remain under #28.
49
+
10
50
  Localization job filenames are derived from content identity, but content IDs
11
51
  are data, not paths. The CLI sanitizes separators and adds a short identity
12
52
  hash when needed, so IDs such as `site.example/static-pages` always produce a
@@ -33,6 +33,37 @@ Valid transitions and statuses are owned by the MaggieDash storage contract;
33
33
  do not edit the database directly. A transition is an auditable state change,
34
34
  not a publish shortcut.
35
35
 
36
+ ## Service variants
37
+
38
+ Service variants use `.maggie/service-variants.json` so location, event and
39
+ holiday pages retain service identity and canonical ownership:
40
+
41
+ ```bash
42
+ maggie dash variant create --project . --service-id massage \
43
+ --variant-id massage-london --variant-type location --locale en-GB --market uk \
44
+ --slug location/london --title "Massage in London" \
45
+ --facts '[{"key":"availability","value":"Weekdays"}]' \
46
+ --source-revision source-1 --confirm
47
+ maggie dash variant review --project . --variant-id massage-london \
48
+ --actor editor --reason "facts checked" --confirm
49
+ maggie dash variant preview --project . --variant-id massage-london \
50
+ --actor reviewer --reason "rendered QA" --preview-url http://localhost/preview --confirm
51
+ maggie dash variant publish --project . --variant-id massage-london \
52
+ --actor owner --reason "approved" --confirm
53
+ ```
54
+
55
+ The lifecycle is draft → review → approved → preview/published. Records carry
56
+ locale, market, source revision, provenance, canonical relationship, cluster
57
+ links, hreflang, facts and layout family. Translated variants cannot publish
58
+ before their canonical source. Slugs use `location/<slug>`, `event/<slug>`, or
59
+ `holiday/<slug>`; collisions fail. `variant slug-plan` creates a reviewed
60
+ redirect with owner approval before an indexed URL changes. `variant validate`
61
+ checks facts, canonical relation, source similarity and layout metadata.
62
+ With `--render-report report.json`, it additionally requires schema
63
+ `maggie-service-variant-render.v1`, a passing report, and matching layout
64
+ family, locale and canonical URL. Metadata validation alone is not a rendering
65
+ claim.
66
+
36
67
  All mutating commands require `--confirm`. The default development adapter is
37
68
  SQLite; production adapters must satisfy the same versioned contracts.
38
69
 
@@ -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.2.0
5
+ version: 1.3.0
6
6
  ---
7
7
 
8
8
  # Maggie Deployment
@@ -2,11 +2,22 @@
2
2
  name: maggie-design
3
3
  description: Design authorized interior pages, review rendered responsive layouts, or explicitly rebrand a packaged homepage/template. Use rebrand only with a named source brand and target brand.
4
4
  metadata:
5
- version: 1.3.0
5
+ version: 1.4.0
6
6
  ---
7
7
 
8
8
  # Maggie Design
9
9
 
10
+ ## In-place route evidence
11
+
12
+ `maggie design in-place --project . --route /example` resolves local source
13
+ files and falls back to a discovered `[...slug]`/`[[...slug]]` file. That
14
+ fallback is currently a source candidate, not proof that the concrete URL is
15
+ served. Verify the route's HTTP response, content identity and framework route
16
+ mapping before editing a shared catch-all. Next app-router catch-alls whose
17
+ file is named `page.tsx` are not covered by this filename fallback. Capture
18
+ responsive and interaction evidence separately; a plan in phase `ready` is
19
+ not a successful browser test.
20
+
10
21
  ## Source-to-runtime icon inventory
11
22
 
12
23
  Before release, inventory icon names in source and compare them with the
@@ -38,6 +38,13 @@ truth, content state, or audit log.
38
38
 
39
39
  ## Commands
40
40
 
41
+ `add` defaults to `candidate` and reports that the item is excluded from
42
+ `search` and `context`. Inspect it with `maggie memory list --status candidate
43
+ --project .`; after review, run `maggie memory transition --project . <kind>
44
+ <id> active`. Explicit status listing supports query and skill filters while
45
+ preserving project scope. Expired active items remain inspectable through
46
+ explicit status listing but are excluded from normal context.
47
+
41
48
  ```bash
42
49
  maggie memory init --project .
43
50
  maggie memory context --project . --skill maggie-clone
@@ -2,11 +2,85 @@
2
2
  name: maggie-seo-geo
3
3
  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.
4
4
  metadata:
5
- version: 1.0.0
5
+ version: 1.1.0
6
6
  ---
7
7
 
8
8
  # Maggie SEO and GEO
9
9
 
10
+ ## Static audit and sitemap evidence
11
+
12
+ Run `maggie site-audit https://example.com --crawl --json` for sitemap-listed
13
+ pages. Homepage checks expose `robots_directive` and `robots_conflict`.
14
+ Crawl entries expose `robots` (no restrictive meta found) and
15
+ `robots_conflict` (a contradiction exists). Missing robots meta is allowed;
16
+ noindex/none/nofollow or contradictory index/follow directives fail the gate.
17
+ Crawled pages also inspect X-Robots-Tag headers, including bot-prefixed noindex.
18
+ Static audit success
19
+ does not establish Google indexing, scroll behavior, responsive visibility,
20
+ or translation quality. Record those as unverified without separate evidence.
21
+
22
+ Crawl reports include `summary.byLocaleTemplate` with total, passed, failed
23
+ and failed URLs per declared locale and template. Regions/scripts remain
24
+ distinct (for example en-GB versus en-US). A template is identified only when
25
+ the page has exactly one distinct `data-template` value; absent or ambiguous
26
+ markup is `unknown`, not an inferred route family. Static report evidence marks
27
+ `rendered` and `behavioral` as `not_run`; these groups do not prove responsive
28
+ or scroll behavior. Fetch failures remain visible in the unknown group.
29
+
30
+ Sitemap planning accepts TSV columns: content type, absolute URL, optional
31
+ lastmod, optional JSON object mapping locale tags to alternate URLs, such as
32
+ `{"en-GB":"https://example.com/post","zh-Hant":"https://example.com/zh/post"}`.
33
+ It emits `lastmod` and `xhtml:link rel="alternate"`. Supply truthful content
34
+ change dates; omit unknown dates. Alternates must currently use the approved
35
+ origin. Reciprocal locale coverage and date semantics require separate review.
36
+
37
+ ## Freeze and compare a reviewed site
38
+
39
+ ```bash
40
+ maggie site-audit https://example.com --crawl --json \
41
+ --save-baseline docs/seo-baseline-v1.json --reviewer maintainer
42
+ maggie site-audit https://example.com --crawl --json \
43
+ --baseline docs/seo-baseline-v1.json --output .maggie/seo-comparison.json
44
+ ```
45
+
46
+ Only save after reviewing the launched site. Creation requires a passing,
47
+ complete sitemap crawl and never overwrites an existing file. Comparison exits
48
+ nonzero for added/removed URLs, changed metadata, response directives/redirect
49
+ targets, structured data, images, DOM structure or text. Text and structure
50
+ hashes catch served translation reversions and template swaps even when section
51
+ counts match. Reports list changed fields, not raw copy.
52
+
53
+ A crawl exceeding `--max-pages` fails instead of silently sampling; raise the
54
+ limit to cover the sitemap. Oversized responses fail instead of truncating.
55
+ For intentional changes, review the diff and create a new versioned baseline
56
+ with a reviewer. Commit the approved change and new contract together; never
57
+ automatically replace the baseline following failure. Dynamic dates, class
58
+ names and copy can produce legitimate differences requiring review.
59
+
60
+ This covers server-rendered sitemap pages, not CSS rendering, JavaScript-only
61
+ content, database translation keys or browser interactions. A source key change
62
+ is detected here only when it changes served content. Baselines contain site
63
+ metadata; do not publish private project contracts without permission.
64
+
65
+ ## Browser behavior evidence
66
+
67
+ For a local or deployed page, run the browser audit with the shared gstack
68
+ `browse` binary:
69
+
70
+ ```bash
71
+ maggie browser-audit https://example.com --browse "$HOME/.codex/skills/gstack/browse/dist/browse" \
72
+ --output .maggie/browser-audit --required "main" \
73
+ --required "nav" --sticky "header" --viewport 390x844 --viewport 768x1024
74
+ ```
75
+
76
+ The command captures three real scroll positions per viewport, DOM geometry,
77
+ horizontal overflow, required-element visibility, sticky/fixed top-inset
78
+ behavior and a screenshot. It exits nonzero for missing/hidden elements, mixed
79
+ viewport samples, no scroll advance, overflow or sticky geometry failure. It
80
+ does not claim keyboard accessibility, network asset correctness or semantic
81
+ content quality unless those checks run separately. Use the installed browser
82
+ path explicitly when Bun is not in PATH.
83
+
10
84
  ## Automatic memory hook
11
85
 
12
86
  Follow [Maggie Memory Hook](../../references/memory-hook.md) at invocation and completion.
@@ -2,11 +2,21 @@
2
2
  name: maggie-service-booking
3
3
  description: Import, synchronise, validate, and design SPA service pages from a booking provider such as Fresha. Use for service catalogues, treatment variants, prices, durations, booking links, payment links, and booking-aware page generation.
4
4
  metadata:
5
- version: 1.0.0
5
+ version: 1.1.0
6
6
  ---
7
7
 
8
8
  # Maggie Service Booking
9
9
 
10
+ ## Reviewed page relationships
11
+
12
+ Reviewed page selection preserves existing `supporting` relations, including
13
+ routes absent from the current source scan. It does not automatically create
14
+ supporting relations for all suggested candidates. Inspect each service's
15
+ `pages` after selection and validate the rendered links. The current selection
16
+ path writes `canonical` for selected pages; multiple selections can violate
17
+ the one-canonical invariant. Resolve roles and run validation before publishing.
18
+ Variant role assignment and layout/URL conventions remain tracked in issue #27.
19
+
10
20
  ## Automatic memory hook
11
21
 
12
22
  Follow [Maggie Memory Hook](../../references/memory-hook.md) at invocation and completion.
@@ -10,11 +10,18 @@ from pathlib import Path
10
10
 
11
11
  sys.path.insert(0, str(Path(__file__).resolve().parents[1] / "runtime"))
12
12
  from maggie_blog import BlogStore # noqa: E402
13
+ from localization_runner import process_adapter
13
14
 
14
15
 
15
16
  def main() -> int:
16
17
  parser = argparse.ArgumentParser(prog="maggie blog")
17
18
  sub = parser.add_subparsers(dest="command", required=True)
19
+ translate = sub.add_parser("translate-pending")
20
+ translate.add_argument("--project", type=Path, default=Path.cwd())
21
+ translate.add_argument("--adapter-command", required=True, help="trusted provider JSON argv array")
22
+ translate.add_argument("--timeout", type=int, default=120)
23
+ translate.add_argument("--character-budget", type=int, default=8000)
24
+ translate.add_argument("--confirm", action="store_true")
18
25
  init = sub.add_parser("init"); init.add_argument("--project", type=Path, default=Path.cwd()); init.add_argument("--base-path", default="/our-blogs"); init.add_argument("--posts-per-page", type=int, default=9); init.add_argument("--confirm", action="store_true")
19
26
  inspect = sub.add_parser("inspect"); inspect.add_argument("--project", type=Path, default=Path.cwd())
20
27
  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")
@@ -25,10 +32,13 @@ def main() -> int:
25
32
  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")
26
33
  args = parser.parse_args()
27
34
  store = BlogStore(args.project.resolve())
28
- if args.command in {"init", "ingest", "publish", "rollback"} and not args.confirm:
35
+ if args.command in {"init", "ingest", "publish", "rollback", "translate-pending"} and not args.confirm:
29
36
  print("CONFIRMATION_REQUIRED: rerun with --confirm", file=sys.stderr); return 2
30
37
  try:
31
38
  if args.command == "init": result = store.init(args.base_path, args.posts_per_page)
39
+ elif args.command == "translate-pending":
40
+ adapter = process_adapter(json.loads(args.adapter_command), args.project, args.timeout)
41
+ result = store.translate_pending(adapter, args.character_budget)
32
42
  elif args.command == "inspect": result = {"settings": store.settings(), "posts": store.posts(), "runs": json.loads(store.runs_path.read_text(encoding="utf-8")) if store.runs_path.exists() else []}
33
43
  elif args.command == "ingest":
34
44
  payload = json.loads(args.input.read_text(encoding="utf-8"));
@@ -42,6 +52,8 @@ def main() -> int:
42
52
  except (OSError, ValueError, json.JSONDecodeError) as error:
43
53
  print(f"maggie-blog: {error}", file=sys.stderr); return 1
44
54
  print(json.dumps(result, indent=2, ensure_ascii=False))
55
+ if args.command in {"ingest", "translate-pending"} and result.get("status") == "partial":
56
+ return 1
45
57
  return 0 if args.command != "validate" or result.get("valid") else 1
46
58
 
47
59
 
@@ -0,0 +1,78 @@
1
+ #!/usr/bin/env python3
2
+ """Capture real viewport/scroll evidence using an installed gstack browse CLI."""
3
+ import argparse
4
+ import json
5
+ import subprocess
6
+ import sys
7
+ from pathlib import Path
8
+ from urllib.parse import urlparse
9
+
10
+ RUNTIME = Path(__file__).resolve().parents[1] / "runtime"
11
+ sys.path.insert(0, str(RUNTIME))
12
+ from browser_behavior import validate_samples
13
+ from localization_runner import checkpoint
14
+
15
+
16
+ def audit(args):
17
+ if urlparse(args.url).scheme not in {"http", "https", "file"}:
18
+ raise ValueError("URL must use http, https or file")
19
+ if not args.required:
20
+ raise ValueError("at least one --required selector is necessary")
21
+ output = args.output.resolve()
22
+ output.mkdir(parents=True, exist_ok=True)
23
+ def call(*command):
24
+ result = subprocess.run([str(args.browse), *command], capture_output=True, text=True, timeout=45)
25
+ if result.returncode:
26
+ raise ValueError("browser command failed: " + command[0])
27
+ return result.stdout
28
+ report = {"schemaVersion": "maggie-browser-audit.v1", "url": args.url,
29
+ "passed": False, "viewports": [], "evidence": "browser-captured"}
30
+ try:
31
+ for index, viewport in enumerate(args.viewport or ["390x844", "768x1024", "1440x900"]):
32
+ width, height = [int(value) for value in viewport.split("x")]
33
+ if min(width, height) < 100:
34
+ raise ValueError("viewport dimensions must be at least 100")
35
+ call("viewport", viewport)
36
+ call("goto", args.url)
37
+ selectors = list(dict.fromkeys(args.required + args.sticky))
38
+ call("js", "window.__maggieAuditSelectors=" + json.dumps(selectors))
39
+ samples = []
40
+ for step, fraction in enumerate([0, 0.5, 0.9]):
41
+ call("js", f"window.scrollTo(0, (document.documentElement.scrollHeight-innerHeight)*{fraction})")
42
+ # Two animation frames allow layout/scroll handlers to settle.
43
+ call("js", "new Promise(resolve => requestAnimationFrame(() => requestAnimationFrame(() => resolve(true))))")
44
+ sample = output / f"viewport-{index}-sample-{step}.json"
45
+ call("eval", str(RUNTIME / "browser_geometry.js"), "--out", str(sample), "--raw")
46
+ samples.append(json.loads(sample.read_text()))
47
+ screenshot = output / f"viewport-{index}.png"
48
+ call("screenshot", "--viewport", str(screenshot))
49
+ if not screenshot.is_file():
50
+ raise ValueError("browser did not create screenshot")
51
+ finding = validate_samples(samples, args.required, args.sticky)
52
+ report["viewports"].append({"viewport": viewport, "samples": samples,
53
+ "screenshot": str(screenshot), **finding})
54
+ report["passed"] = all(item["passed"] for item in report["viewports"])
55
+ finally:
56
+ checkpoint(output / "report.json", report)
57
+ print(json.dumps(report, indent=2))
58
+ return 0 if report["passed"] else 1
59
+
60
+
61
+ def main():
62
+ parser = argparse.ArgumentParser(description=__doc__)
63
+ parser.add_argument("url")
64
+ parser.add_argument("--browse", type=Path, required=True)
65
+ parser.add_argument("--output", type=Path, required=True, help="new directory for evidence")
66
+ parser.add_argument("--viewport", action="append")
67
+ parser.add_argument("--required", action="append", default=[])
68
+ parser.add_argument("--sticky", action="append", default=[])
69
+ args = parser.parse_args()
70
+ try:
71
+ return audit(args)
72
+ except (ValueError, OSError, subprocess.TimeoutExpired) as error:
73
+ print(f"browser-audit: {error}", file=sys.stderr)
74
+ return 1
75
+
76
+
77
+ if __name__ == "__main__":
78
+ raise SystemExit(main())
@@ -16,6 +16,7 @@ from pathlib import Path
16
16
 
17
17
  sys.path.insert(0, str(Path(__file__).resolve().parents[1] / "runtime"))
18
18
  from maggie_dash_store import MaggieDashStore # noqa: E402
19
+ from service_variants import ServiceVariantStore # noqa: E402
19
20
 
20
21
 
21
22
  def project_root(args: argparse.Namespace) -> Path:
@@ -92,6 +93,40 @@ def command_transition(args: argparse.Namespace) -> int:
92
93
  store.close()
93
94
 
94
95
 
96
+ def variant_store(args: argparse.Namespace) -> ServiceVariantStore:
97
+ return ServiceVariantStore(project_root(args) / ".maggie" / "service-variants.json")
98
+
99
+
100
+ def command_variant(args: argparse.Namespace) -> int:
101
+ require_confirm(args)
102
+ store = variant_store(args)
103
+ if args.variant_command == "create":
104
+ result = store.create(service_id=args.service_id, variant_id=args.variant_id,
105
+ variant_type=args.variant_type, locale=args.locale, market=args.market,
106
+ slug=args.slug, title=args.title, facts=json.loads(args.facts),
107
+ source_revision=args.source_revision, canonical_variant_id=args.canonical_variant_id,
108
+ cluster_links=args.cluster_link, layout_family=args.layout_family)
109
+ elif args.variant_command == "edit":
110
+ result = store.edit(args.variant_id, title=args.title, facts=json.loads(args.facts),
111
+ source_revision=args.source_revision, actor=args.actor, reason=args.reason)
112
+ elif args.variant_command == "translate":
113
+ result = store.translate(args.source_variant_id, variant_id=args.variant_id,
114
+ locale=args.locale, market=args.market, title=args.title, facts=json.loads(args.facts),
115
+ source_revision=args.source_revision, actor=args.actor)
116
+ elif args.variant_command in {"review", "preview", "publish", "archive"}:
117
+ target = {"review": "review", "preview": "preview", "publish": "published", "archive": "archived"}[args.variant_command]
118
+ result = store.transition(args.variant_id, target, args.actor, args.reason, preview_url=getattr(args, "preview_url", None))
119
+ elif args.variant_command == "validate":
120
+ render_report = json.loads(Path(args.render_report).read_text(encoding="utf-8")) if args.render_report else None
121
+ result = store.validate(args.variant_id, args.source_text, render_report)
122
+ elif args.variant_command == "slug-plan":
123
+ result = store.plan_slug_change(args.variant_id, args.new_slug, args.actor)
124
+ else:
125
+ result = [item for item in store.data["variants"] if not args.variant_id or item["id"] == args.variant_id]
126
+ emit(result)
127
+ return 0 if not isinstance(result, dict) or result.get("passed", True) else 1
128
+
129
+
95
130
  def parser() -> argparse.ArgumentParser:
96
131
  parser = argparse.ArgumentParser(prog="maggie dash", description=__doc__)
97
132
  sub = parser.add_subparsers(dest="command", required=True)
@@ -120,6 +155,18 @@ def parser() -> argparse.ArgumentParser:
120
155
  transition.add_argument("--reason", required=True)
121
156
  transition.add_argument("--confirm", action="store_true")
122
157
  transition.set_defaults(func=command_transition)
158
+ variant = sub.add_parser("variant", help="manage service variant lifecycle")
159
+ variant_sub = variant.add_subparsers(dest="variant_command", required=True)
160
+ 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")
161
+ edit = variant_sub.add_parser("edit"); edit.add_argument("--project", default="."); edit.add_argument("--variant-id", required=True); edit.add_argument("--title", required=True); edit.add_argument("--facts", required=True); edit.add_argument("--source-revision", required=True); edit.add_argument("--actor", required=True); edit.add_argument("--reason", required=True); edit.add_argument("--confirm", action="store_true")
162
+ translate = variant_sub.add_parser("translate"); translate.add_argument("--project", default="."); translate.add_argument("--source-variant-id", required=True); translate.add_argument("--variant-id", required=True); translate.add_argument("--locale", required=True); translate.add_argument("--market", required=True); translate.add_argument("--title", required=True); translate.add_argument("--facts", required=True); translate.add_argument("--source-revision", required=True); translate.add_argument("--actor", required=True); translate.add_argument("--confirm", action="store_true")
163
+ for name in ("review", "preview", "publish", "archive"):
164
+ command = variant_sub.add_parser(name); command.add_argument("--project", default="."); command.add_argument("--variant-id", required=True); command.add_argument("--actor", required=True); command.add_argument("--reason", required=True); command.add_argument("--confirm", action="store_true")
165
+ if name == "preview": command.add_argument("--preview-url", required=True)
166
+ validate = variant_sub.add_parser("validate"); validate.add_argument("--project", default="."); validate.add_argument("--variant-id", required=True); validate.add_argument("--source-text"); validate.add_argument("--render-report"); validate.add_argument("--confirm", action="store_true")
167
+ plan_slug = variant_sub.add_parser("slug-plan"); plan_slug.add_argument("--project", default="."); plan_slug.add_argument("--variant-id", required=True); plan_slug.add_argument("--new-slug", required=True); plan_slug.add_argument("--actor", required=True); plan_slug.add_argument("--confirm", action="store_true")
168
+ inspect = variant_sub.add_parser("inspect"); inspect.add_argument("--project", default="."); inspect.add_argument("--variant-id"); inspect.add_argument("--confirm", action="store_true")
169
+ variant.set_defaults(func=command_variant)
123
170
  return parser
124
171
 
125
172