@topy-ai/maggie 0.7.23 → 0.7.24

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -87,7 +87,9 @@ maggie qa assertion-audit --project . \
87
87
 
88
88
  For a dialog or drawer, include a browser-runtime `scroll-owner-count` check
89
89
  with `expected: 1`. This catches the fixed-shell plus inner-content double
90
- scrollbar that static class checks can miss.
90
+ scrollbar that static class checks can miss. For icon controls, use
91
+ `icon-rendered` with a visible glyph, positive width/height, and an accessible
92
+ label; this catches a missing runtime glyph that build/typecheck cannot see.
91
93
 
92
94
  Dashboard and documentation audits are provider-neutral and keep host data
93
95
  behind explicit evidence files:
@@ -214,7 +216,7 @@ maggie memory ... # confirmed preferences and lessons
214
216
  maggie feedback ... # redact, preview, submit, list
215
217
  maggie qa ... # scenario browser QA, fix/retest, release gate
216
218
  maggie localization ... # plan, validate, review, publish, stale
217
- maggie service ... # import, sync/report, catalogue, lifecycle, validate
219
+ maggie service ... # import, sync/report, catalogue, lifecycle, validate
218
220
  maggie seo performance ... # sampled PageSpeed/CWV report and baseline
219
221
  maggie seo images ... # inventory, variants, confirmation, validate
220
222
  maggie seo sitemap ... # typed/semantic plan, agent-files, apply, rollback
@@ -224,6 +226,7 @@ maggie seo head-tags ... # rendered-shell head metadata drift audit
224
226
  maggie ops favicon-check ... # served favicon behaviour check
225
227
  maggie deployment | migration | release | analytics | schedule
226
228
  maggie deployment readiness --project PATH
229
+ maggie deployment parity --project PATH --evidence FILE --output FILE
227
230
  maggie migration identity --identity-file FILE [--expected-file FILE]
228
231
  maggie deployment canary --asset URL=SHA256 --render-report report.json
229
232
  maggie design icon-inventory --source-dir src --runtime assets/icons.css
@@ -298,6 +301,11 @@ maggie design icon-inventory --project . --source-dir src \
298
301
  --runtime public/assets/icons.css --output docs/icon-inventory.json
299
302
  ```
300
303
 
304
+ The inventory covers compound selectors such as `.ph.ph-arrow-square-out`
305
+ and is only static name coverage. Pair changed icon controls with a
306
+ browser-runtime `icon-rendered` assertion so the host adapter proves that the
307
+ glyph is visible, has positive dimensions, and has an accessible label.
308
+
301
309
  After deployment, compare an immutable asset fingerprint and validate the
302
310
  browser adapter's rendered report:
303
311
 
@@ -312,8 +320,23 @@ The canary report requires a screenshot and zero console errors, missing
312
320
  assets, and visual placeholders for every route. It records safe cache headers
313
321
  only and never stores response bodies, cookies, or credentials.
314
322
 
323
+ When workers, migrations, browser drivers, import jobs, or route APIs are part
324
+ of the deployment, validate their sanitized parity evidence before the canary:
325
+
326
+ ```bash
327
+ maggie deployment parity --project . \
328
+ --evidence .maggie/verification/runtime-parity-input.json \
329
+ --output .maggie/verification/runtime-parity.json
330
+ ```
331
+
332
+ The parity contract checks required worker configuration, migration
333
+ privileges, route API status, stale resident processes, source-job IDs, and
334
+ browser/driver compatibility. Add the resulting report to readiness with
335
+ `--runtime-parity`; a failed report blocks the gate.
336
+
315
337
  Unit regression does not establish runtime release readiness. Produce unit
316
- evidence and then validate all five release evidence slots:
338
+ evidence and then validate all required release evidence slots; add
339
+ `--runtime-parity` when the deployment uses the worker/browser surfaces above:
317
340
 
318
341
  ```bash
319
342
  python3 tools/tests/run_regression.py \
@@ -323,13 +346,27 @@ maggie deployment readiness --project . \
323
346
  --browser-report .maggie/verification/browser-evidence.json \
324
347
  --rendered-canary .maggie/deployment-canary.json \
325
348
  --deployment-preflight .maggie/release-preflight.json \
349
+ --runtime-parity .maggie/verification/runtime-parity.json \
326
350
  --output .maggie/deployment-readiness.json
327
351
  ```
328
352
 
329
353
  The readiness report follows `maggie-deployment-readiness.v1`. It reports unit
330
354
  regression, package smoke, host browser evidence, rendered canary, and
331
355
  deployment preflight separately. Missing host adapter evidence is
332
- `inconclusive`; only five passing slots produce `passed`.
356
+ `inconclusive`; only all configured slots produce `passed`.
357
+
358
+ For service-booking hosts, validate independent import, local-search, and
359
+ website-research jobs before public generation:
360
+
361
+ ```bash
362
+ maggie service orchestration-validate --project . \
363
+ --manifest .maggie/booking/onboarding-orchestration.json \
364
+ --output docs/service-onboarding-orchestration.json
365
+ ```
366
+
367
+ The contract requires unique idempotency keys, bounded retry history, pollable
368
+ status references, and disjoint field ownership so one onboarding workflow
369
+ cannot overwrite another.
333
370
 
334
371
  ### Localization and analytics release gates
335
372
 
@@ -356,8 +393,8 @@ artifact schemas.
356
393
  Recommended upgrade sequence for the current release:
357
394
 
358
395
  ```bash
359
- npx @topy-ai/maggie@0.7.23 update --project . --force
360
- npx @topy-ai/maggie@0.7.23 cleanup --project .
396
+ npx @topy-ai/maggie@0.7.24 update --project . --force
397
+ npx @topy-ai/maggie@0.7.24 cleanup --project .
361
398
  ```
362
399
 
363
400
  Maintainers should pass npm credentials through the repository helper, never
@@ -367,7 +404,9 @@ as a command-line argument:
367
404
  node scripts/publish-npm.mjs --maggie-env-file ../.env
368
405
  ```
369
406
 
370
- The 0.7.23 workflow adds the single-scroll-owner MaggieDash modal primitive,
407
+ The 0.7.24 workflow adds browser-runtime `icon-rendered` assertions, sanitized
408
+ worker/browser parity preflight evidence, and the provider-neutral asynchronous
409
+ service-onboarding contract. The 0.7.23 workflow adds the single-scroll-owner MaggieDash modal primitive,
371
410
  server-runtime-only auth/provider configuration guidance, best-effort optional
372
411
  identity-mirror semantics, and `scroll-owner-count` browser QA evidence. The
373
412
  0.7.22 workflow adds provider-catalogue authority checks, field/variant
package/README.zh-TW.md CHANGED
@@ -8,7 +8,7 @@ Codex、Claude Code 與相容的 coding agents。
8
8
  ## 安裝
9
9
 
10
10
  ```bash
11
- npx @topy-ai/maggie@0.7.23 init --agent all
11
+ npx @topy-ai/maggie@0.7.24 init --agent all
12
12
  npx @topy-ai/maggie doctor --project .
13
13
  ```
14
14
 
@@ -25,6 +25,8 @@ maggie doctor --project . --require-bootstrap --strict
25
25
  deployment、memory、feedback 和 MaggieDash。內容先 draft/review,外部寫入、
26
26
  publish 與 production deployment 需要明確確認。
27
27
 
28
+ 0.7.24 加入 `icon-rendered` browser QA assertion、worker/browser deployment parity
29
+ preflight,以及 import/search/research 的非同步 service onboarding contract。
28
30
  0.7.23 加入 dashboard dialog 單一 scroll owner、server runtime-only auth/provider
29
31
  設定規則、optional identity mirror 的 best-effort 邊界,以及 QA 的
30
32
  `scroll-owner-count` browser evidence。0.7.22 加入 provider catalogue authority check、field/variant sync report、
package/bin/maggie.js CHANGED
@@ -108,10 +108,12 @@ Usage:
108
108
  maggie service validate --project PATH
109
109
  maggie service inspect --project PATH
110
110
  maggie service status --project PATH
111
+ maggie service orchestration-validate --project PATH --manifest FILE
111
112
  maggie service convert-page <page-path> --project PATH
112
113
  maggie service match-pages --project PATH --pages-dir src/pages
113
114
  maggie deployment --project PATH --target vps-with-cloudflare-dns
114
115
  maggie deployment readiness --project PATH
116
+ maggie deployment parity --project PATH --evidence FILE --output FILE
115
117
  maggie deployment canary --project PATH --asset URL=SHA256 --render-report report.json --output docs/deployment-canary.json
116
118
  maggie migration --project PATH --environment staging
117
119
  maggie migration identity --identity-file FILE [--expected-file FILE]
@@ -301,7 +303,8 @@ function marketplace(args) {
301
303
 
302
304
  function service(args) {
303
305
  const root = projectRoot(args);
304
- const script = join(root, "tools", "clis", "maggie_service_booking.py");
306
+ const scriptName = args[0] === "orchestration-validate" ? "maggie_service_onboarding.py" : "maggie_service_booking.py";
307
+ const script = join(root, "tools", "clis", scriptName);
305
308
  if (!existsSync(script)) throw new Error(`service booking CLI is missing: ${script}`);
306
309
  const result = spawnSync("python3", [script, ...args], { stdio: "inherit", cwd: root, env: { ...process.env, MAGGIE_VERSION: PACKAGE_VERSION } });
307
310
  if (result.error) throw result.error;
@@ -413,6 +416,7 @@ try {
413
416
  else if (command === "ops") workflowCli("maggie_ops.py", args);
414
417
  else if (command === "deployment" && args[0] === "canary") workflowCli("maggie_deployment_canary.py", args.slice(1));
415
418
  else if (command === "deployment" && args[0] === "readiness") workflowCli("maggie_deployment_readiness.py", args.slice(1));
419
+ else if (command === "deployment" && args[0] === "parity") workflowCli("maggie_deployment_parity.py", args.slice(1));
416
420
  else if (command === "deployment") workflowCli("maggie_deployment.py", args);
417
421
  else if (command === "migration") workflowCli("maggie_migration.py", args);
418
422
  else if (command === "schedule") workflowCli("maggie_schedule.py", args);
@@ -16,7 +16,8 @@
16
16
  "packageSmoke": {"$ref": "#/$defs/check"},
17
17
  "browserEvidence": {"$ref": "#/$defs/check"},
18
18
  "renderedCanary": {"$ref": "#/$defs/check"},
19
- "deploymentPreflight": {"$ref": "#/$defs/check"}
19
+ "deploymentPreflight": {"$ref": "#/$defs/check"},
20
+ "runtimeParity": {"$ref": "#/$defs/check"}
20
21
  }
21
22
  }
22
23
  },
@@ -0,0 +1,38 @@
1
+ {
2
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
3
+ "$id": "https://maggie.noblox.app/contracts/deployment-runtime-parity-v1.json",
4
+ "title": "Maggie deployment worker and browser runtime parity evidence",
5
+ "type": "object",
6
+ "required": ["schemaVersion", "environment", "passed", "worker", "browser"],
7
+ "properties": {
8
+ "schemaVersion": {"const": "maggie-deployment-runtime-parity.v1"},
9
+ "environment": {"enum": ["development", "staging", "production"]},
10
+ "passed": {"const": true},
11
+ "worker": {
12
+ "type": "object",
13
+ "required": ["config", "migration", "routeApis", "residentProcesses", "sourceJobs"],
14
+ "properties": {
15
+ "config": {"$ref": "#/$defs/config"},
16
+ "migration": {"type": "object", "required": ["privilegeCheck"], "properties": {"privilegeCheck": {"$ref": "#/$defs/passed"}}},
17
+ "routeApis": {"type": "array", "minItems": 1, "items": {"$ref": "#/$defs/route"}},
18
+ "residentProcesses": {"$ref": "#/$defs/processes"},
19
+ "sourceJobs": {"type": "array", "items": {"$ref": "#/$defs/job"}}
20
+ },
21
+ "additionalProperties": true
22
+ },
23
+ "browser": {
24
+ "type": "object",
25
+ "required": ["browserVersion", "driverVersion", "compatible"],
26
+ "properties": {"browserVersion": {"type": "string", "minLength": 1}, "driverVersion": {"type": "string", "minLength": 1}, "compatible": {"const": true}},
27
+ "additionalProperties": true
28
+ }
29
+ },
30
+ "$defs": {
31
+ "passed": {"type": "object", "required": ["passed"], "properties": {"passed": {"const": true}}, "additionalProperties": true},
32
+ "config": {"type": "object", "required": ["required", "missing", "passed"], "properties": {"required": {"type": "array", "minItems": 1, "items": {"type": "string", "minLength": 1}}, "missing": {"type": "array", "maxItems": 0}, "passed": {"const": true}}, "additionalProperties": true},
33
+ "route": {"type": "object", "required": ["method", "path", "status", "passed"], "properties": {"method": {"type": "string"}, "path": {"type": "string", "pattern": "^/"}, "status": {"type": "integer", "minimum": 200, "maximum": 399}, "passed": {"const": true}}, "additionalProperties": true},
34
+ "processes": {"type": "object", "required": ["checked", "unexpected", "stale", "passed"], "properties": {"checked": {"const": true}, "unexpected": {"type": "array", "maxItems": 0}, "stale": {"type": "array", "maxItems": 0}, "passed": {"const": true}}, "additionalProperties": true},
35
+ "job": {"type": "object", "required": ["jobId", "passed"], "properties": {"jobId": {"type": "string", "minLength": 1}, "passed": {"const": true}}, "additionalProperties": true}
36
+ },
37
+ "additionalProperties": true
38
+ }
@@ -0,0 +1,34 @@
1
+ {
2
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
3
+ "$id": "https://maggie.noblox.app/contracts/service-onboarding-orchestration-v1.json",
4
+ "title": "Maggie provider-neutral asynchronous service onboarding orchestration",
5
+ "type": "object",
6
+ "required": ["schemaVersion", "workflows", "fieldOwnership"],
7
+ "properties": {
8
+ "schemaVersion": {"const": "maggie-service-onboarding-orchestration.v1"},
9
+ "workflows": {"type": "array", "minItems": 3, "maxItems": 3, "items": {"$ref": "#/$defs/workflow"}},
10
+ "fieldOwnership": {"type": "object", "minProperties": 1, "additionalProperties": {"enum": ["import", "search", "research"]}}
11
+ },
12
+ "$defs": {
13
+ "workflow": {
14
+ "type": "object",
15
+ "required": ["name", "jobId", "idempotencyKey", "state", "history", "attempt", "maxAttempts", "owner", "statusRef", "nextAction", "writes"],
16
+ "properties": {
17
+ "name": {"enum": ["import", "search", "research"]},
18
+ "jobId": {"type": "string", "minLength": 1},
19
+ "idempotencyKey": {"type": "string", "minLength": 1},
20
+ "state": {"enum": ["queued", "running", "succeeded", "failed", "blocked", "cancelled"]},
21
+ "history": {"type": "array", "minItems": 1, "items": {"type": "object", "required": ["state", "at"], "properties": {"state": {"type": "string"}, "at": {"type": "string"}}}},
22
+ "attempt": {"type": "integer", "minimum": 1},
23
+ "maxAttempts": {"type": "integer", "minimum": 1},
24
+ "owner": {"type": "string", "minLength": 1},
25
+ "statusRef": {"type": "string", "minLength": 1},
26
+ "nextAction": {"type": "string", "minLength": 1},
27
+ "writes": {"type": "array", "minItems": 1, "items": {"type": "string", "minLength": 1}},
28
+ "errorCode": {"type": ["string", "null"]}
29
+ },
30
+ "additionalProperties": true
31
+ }
32
+ },
33
+ "additionalProperties": true
34
+ }
@@ -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.3.0
5
+ version: 1.4.0
6
6
  ---
7
7
 
8
8
  # Maggie Deployment
@@ -37,12 +37,33 @@ visitor-facing files. The canary must include screenshots, zero console or
37
37
  network errors, and zero placeholder matches. Query-driven routes belong in
38
38
  behavior/API checks, not static byte baselines.
39
39
 
40
+ ## Worker/browser runtime parity
41
+
42
+ Before a canary when the release depends on workers, migrations, browser
43
+ automation, import jobs, or route APIs, collect sanitized host-adapter evidence
44
+ and validate it before traffic moves:
45
+
46
+ ```bash
47
+ maggie deployment parity --project . \
48
+ --evidence .maggie/verification/runtime-parity-input.json \
49
+ --output .maggie/verification/runtime-parity.json
50
+ ```
51
+
52
+ The `maggie-deployment-runtime-parity.v1` contract checks required worker
53
+ configuration and missing bindings, migration privileges, route API status,
54
+ stale/unexpected resident import processes, non-empty source-job IDs, and
55
+ browser/driver compatibility. It is provider-neutral: the host adapter runs
56
+ the environment checks and supplies only safe facts; Maggie validates the
57
+ evidence and never receives credentials, cookies, or response bodies. A failed
58
+ parity report blocks the readiness command and therefore the canary.
59
+
40
60
  ## Release readiness evidence
41
61
 
42
62
  Unit regression is necessary but does not prove runtime release readiness. The
43
- readiness command keeps five evidence slots separate: dependency-free unit
44
- regression, package smoke, host browser evidence, rendered canary, and
45
- deployment preflight:
63
+ readiness command keeps five required evidence slots separate: dependency-free
64
+ unit regression, package smoke, host browser evidence, rendered canary, and
65
+ deployment preflight. Add the optional `runtimeParity` slot whenever the
66
+ release uses the worker/browser surfaces described above:
46
67
 
47
68
  ```bash
48
69
  python3 tools/tests/run_regression.py \
@@ -52,12 +73,13 @@ maggie deployment readiness --project . \
52
73
  --browser-report .maggie/verification/browser-evidence.json \
53
74
  --rendered-canary .maggie/deployment-canary.json \
54
75
  --deployment-preflight .maggie/release-preflight.json \
76
+ --runtime-parity .maggie/verification/runtime-parity.json \
55
77
  --output .maggie/deployment-readiness.json
56
78
  ```
57
79
 
58
80
  The report follows `maggie-deployment-readiness.v1`. A failed evidence file
59
81
  returns `failed`; a missing or unavailable host/browser adapter returns
60
- `inconclusive`; only five passing evidence slots return `passed`. Maggie does
82
+ `inconclusive`; only all configured evidence slots return `passed`. Maggie does
61
83
  not fabricate browser, rendered, or deployment evidence and does not deploy
62
84
  from this command. See
63
85
  [`readiness-v1.schema.json`](../../bundled-contracts/maggie-deployment/readiness-v1.schema.json).
@@ -2,7 +2,7 @@
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.4.0
5
+ version: 1.5.0
6
6
  ---
7
7
 
8
8
  # Maggie Design
@@ -55,9 +55,18 @@ Use `missing`, `unknown`, and `coverage` from the versioned
55
55
  name map is reported as evidence only; it must not be treated as proof that a
56
56
  glyph exists. Fix the source token or add the runtime definition before
57
57
  shipping. The inventory compares a scoped runtime icon family (`ph-*`, `fa-*`,
58
- or `lucide-*`), normalizes family prefixes, and ignores inline SVG IDs and font
59
- filename noise. Do not paste private source, URLs with credentials, or user
60
- data into the report.
58
+ or `lucide-*`), including compound CSS selectors such as
59
+ `.ph.ph-arrow-square-out`, normalizes family prefixes, and ignores inline SVG
60
+ IDs and font filename noise. Do not paste private source, URLs with
61
+ credentials, or user data into the report.
62
+
63
+ Inventory is static name coverage, not proof that the browser painted a glyph.
64
+ For every new or changed icon control, pair it with the QA `icon-rendered`
65
+ assertion: the host browser adapter must record a visible glyph, positive
66
+ rendered width and height, and an accessible label. If the runtime font/map
67
+ cannot render the icon, add an approved runtime definition or inline SVG
68
+ fallback and rerun both checks. Never release a source token solely because
69
+ typecheck or build succeeded.
61
70
 
62
71
  ## Automatic memory hook
63
72
 
@@ -2,7 +2,7 @@
2
2
  name: maggie-qa-workflow
3
3
  description: Run scenario-based browser QA with explicit evidence, fix/retest lifecycle, and release-gate decisions for web projects.
4
4
  metadata:
5
- version: 1.1.0
5
+ version: 1.2.0
6
6
  ---
7
7
 
8
8
  # Maggie QA Workflow
@@ -91,10 +91,14 @@ HTTP status evidence, a screenshot/reference, and boolean results for runtime
91
91
  checks such as `text-present`, `text-absent`, `meta`, `link-absent`, or
92
92
  `redirect`. For dialogs and drawers, `scroll-owner-count` records the browser
93
93
  runtime's computed number of intentional scroll owners and must use
94
- `expected: 1`. Source/class/markup-only checks are rejected. The audit stores
95
- assertion IDs and safe pass/fail metadata, never response bodies, credentials,
96
- or cookies. A passing assertion audit complements browser evidence; it does
97
- not replace the host adapter's actual HTTP and screenshot capture.
94
+ `expected: 1`. For an icon control, `icon-rendered` records browser-runtime
95
+ evidence with `actual.visible: true`, positive `actual.width` and
96
+ `actual.height`, and `actual.accessibleLabel: true`; a blank glyph, zero-sized
97
+ glyph, or unlabelled control fails the audit even when build/typecheck passes.
98
+ Source/class/markup-only checks are rejected. The audit stores assertion IDs
99
+ and safe pass/fail metadata, never response bodies, credentials, or cookies. A
100
+ passing assertion audit complements browser evidence; it does not replace the
101
+ host adapter's actual HTTP and screenshot capture.
98
102
 
99
103
  ## Gate and release evidence
100
104
 
@@ -2,7 +2,7 @@
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.2.0
5
+ version: 1.3.0
6
6
  ---
7
7
 
8
8
  # Maggie Service Booking
@@ -136,6 +136,13 @@ python3 tools/clis/maggie_service_booking.py retirement-audit \
136
136
  --project . \
137
137
  --catalogue .maggie/booking/services.json \
138
138
  --evidence .maggie/booking/retirement-evidence.json
139
+
140
+ # Validate independent, pollable onboarding jobs before provider research or
141
+ # public service generation. The manifest contains no credentials or payloads.
142
+ maggie service orchestration-validate \
143
+ --project . \
144
+ --manifest .maggie/booking/onboarding-orchestration.json \
145
+ --output docs/service-onboarding-orchestration.json
139
146
  ```
140
147
 
141
148
  `import` creates the first catalogue. `sync` compares the newly imported
@@ -159,6 +166,33 @@ evidence artifact and validates the selected ending (`pending`, `redirect`,
159
166
  `tombstone`, or `gone`), HTTP status, booking suppression, unavailable/noindex
160
167
  signals, and sitemap exclusion. It never stores response bodies or decides a
161
168
  redirect destination for the host.
169
+
170
+ ## Asynchronous onboarding contract
171
+
172
+ Provider onboarding is three independent state machines: `import` owns the
173
+ provider catalogue, `search` owns local discovery results, and `research` owns
174
+ website research. Each job must have a unique `jobId`, unique
175
+ `idempotencyKey`, bounded `attempt`/`maxAttempts`, a current state and history,
176
+ an owner, a pollable `statusRef`, and an explicit `nextAction`. State history
177
+ is validated so retries cannot silently become successes. `fieldOwnership`
178
+ ensures that one workflow cannot overwrite another workflow's fields; the host
179
+ adapter remains responsible for actual persistence and queue execution.
180
+
181
+ Validate the redacted orchestration snapshot before provider research and
182
+ again before public generation:
183
+
184
+ ```bash
185
+ maggie service orchestration-validate \
186
+ --project . \
187
+ --manifest .maggie/booking/onboarding-orchestration.json \
188
+ --output docs/service-onboarding-orchestration.json
189
+ ```
190
+
191
+ The contract is documented in
192
+ [`onboarding-orchestration-v1.schema.json`](../../bundled-contracts/maggie-service-booking/onboarding-orchestration-v1.schema.json).
193
+ It is provider-neutral and does not accept credentials, provider response
194
+ bodies, or arbitrary filesystem paths in the poll reference.
195
+
162
196
  `convert-page` imports a normal page as a draft service without inventing
163
197
  booking facts. `match-pages` reads filesystem pages plus `docs/pages.json` and
164
198
  `docs/page-content.json`, writes candidate evidence to both `.maggie/booking`
@@ -0,0 +1,170 @@
1
+ #!/usr/bin/env python3
2
+ """Validate provider-neutral worker/browser runtime parity evidence before canary traffic."""
3
+
4
+ from __future__ import annotations
5
+
6
+ import argparse
7
+ import json
8
+ import re
9
+ import sys
10
+ from pathlib import Path
11
+ from typing import Any
12
+
13
+
14
+ SCHEMA = "maggie-deployment-runtime-parity.v1"
15
+ ENVIRONMENTS = {"development", "staging", "production"}
16
+ SAFE_ID = re.compile(r"^[A-Za-z0-9][A-Za-z0-9._:-]{0,119}$")
17
+
18
+
19
+ def load(path: Path) -> dict[str, Any]:
20
+ try:
21
+ value = json.loads(path.read_text(encoding="utf-8"))
22
+ except (OSError, json.JSONDecodeError) as error:
23
+ raise ValueError(f"cannot read parity evidence: {error}") from error
24
+ if not isinstance(value, dict):
25
+ raise ValueError("parity evidence root must be an object")
26
+ return value
27
+
28
+
29
+ def non_empty_strings(value: object, field: str, errors: list[str]) -> list[str]:
30
+ if not isinstance(value, list) or any(not isinstance(item, str) or not item.strip() for item in value):
31
+ errors.append(f"{field} must be an array of non-empty strings")
32
+ return []
33
+ return value
34
+
35
+
36
+ def validate(payload: dict[str, Any]) -> list[str]:
37
+ errors: list[str] = []
38
+ if payload.get("schemaVersion") != SCHEMA:
39
+ errors.append(f"schemaVersion must be {SCHEMA}")
40
+ if payload.get("environment") not in ENVIRONMENTS:
41
+ errors.append("environment must be development, staging, or production")
42
+ if payload.get("passed") is not True:
43
+ errors.append("parity evidence is not marked passed")
44
+
45
+ worker = payload.get("worker")
46
+ if not isinstance(worker, dict):
47
+ errors.append("worker evidence must be an object")
48
+ worker = {}
49
+ config = worker.get("config")
50
+ if not isinstance(config, dict):
51
+ errors.append("worker.config evidence must be an object")
52
+ config = {}
53
+ required = non_empty_strings(config.get("required"), "worker.config.required", errors)
54
+ missing = non_empty_strings(config.get("missing"), "worker.config.missing", errors)
55
+ if not required:
56
+ errors.append("worker.config.required must declare at least one requirement")
57
+ if missing:
58
+ errors.append("worker.config has missing requirements")
59
+ if config.get("passed") is not True:
60
+ errors.append("worker.config did not pass")
61
+
62
+ migration = worker.get("migration")
63
+ privilege = migration.get("privilegeCheck") if isinstance(migration, dict) else None
64
+ if not isinstance(privilege, dict) or privilege.get("passed") is not True:
65
+ errors.append("worker.migration.privilegeCheck did not pass")
66
+
67
+ routes = worker.get("routeApis")
68
+ if not isinstance(routes, list) or not routes:
69
+ errors.append("worker.routeApis must contain at least one route check")
70
+ routes = []
71
+ for index, route in enumerate(routes):
72
+ prefix = f"worker.routeApis[{index}]"
73
+ if not isinstance(route, dict):
74
+ errors.append(f"{prefix} must be an object")
75
+ continue
76
+ method = route.get("method")
77
+ path = route.get("path")
78
+ status = route.get("status")
79
+ if not isinstance(method, str) or not re.fullmatch(r"[A-Z]{3,10}", method):
80
+ errors.append(f"{prefix}.method must be an uppercase HTTP method")
81
+ if not isinstance(path, str) or not path.startswith("/") or "?" in path or "#" in path:
82
+ errors.append(f"{prefix}.path must be a local route path without query or fragment")
83
+ if not isinstance(status, int) or isinstance(status, bool) or not 200 <= status < 400:
84
+ errors.append(f"{prefix}.status must be a successful HTTP status")
85
+ if route.get("passed") is not True:
86
+ errors.append(f"{prefix} did not pass")
87
+
88
+ processes = worker.get("residentProcesses")
89
+ if not isinstance(processes, dict):
90
+ errors.append("worker.residentProcesses evidence must be an object")
91
+ processes = {}
92
+ for field in ("unexpected", "stale"):
93
+ values = non_empty_strings(processes.get(field), f"worker.residentProcesses.{field}", errors)
94
+ if values:
95
+ errors.append(f"worker.residentProcesses contains {field} processes")
96
+ if processes.get("checked") is not True or processes.get("passed") is not True:
97
+ errors.append("worker.residentProcesses did not pass")
98
+
99
+ jobs = worker.get("sourceJobs")
100
+ if not isinstance(jobs, list):
101
+ errors.append("worker.sourceJobs must be an array")
102
+ jobs = []
103
+ for index, job in enumerate(jobs):
104
+ prefix = f"worker.sourceJobs[{index}]"
105
+ if not isinstance(job, dict):
106
+ errors.append(f"{prefix} must be an object")
107
+ continue
108
+ job_id = job.get("jobId")
109
+ if not isinstance(job_id, str) or not SAFE_ID.fullmatch(job_id):
110
+ errors.append(f"{prefix}.jobId must be a non-empty safe identifier")
111
+ if job.get("passed") is not True:
112
+ errors.append(f"{prefix} did not pass")
113
+
114
+ browser = payload.get("browser")
115
+ if not isinstance(browser, dict):
116
+ errors.append("browser evidence must be an object")
117
+ browser = {}
118
+ for field in ("browserVersion", "driverVersion"):
119
+ if not isinstance(browser.get(field), str) or not browser[field].strip():
120
+ errors.append(f"browser.{field} is required")
121
+ if browser.get("compatible") is not True:
122
+ errors.append("browser and driver versions are not compatible")
123
+ return errors
124
+
125
+
126
+ def parity(evidence: Path, output: Path) -> int:
127
+ try:
128
+ payload = load(evidence)
129
+ errors = validate(payload)
130
+ except ValueError as error:
131
+ errors = [str(error)]
132
+ report = {
133
+ "schemaVersion": SCHEMA,
134
+ "environment": payload.get("environment") if "payload" in locals() else None,
135
+ "status": "passed" if not errors else "failed",
136
+ "passed": not errors,
137
+ "checks": {
138
+ "workerConfig": "validated",
139
+ "migrationPrivileges": "validated",
140
+ "routeApis": "validated",
141
+ "residentProcesses": "validated",
142
+ "sourceJobIds": "validated",
143
+ "browserDriverCompatibility": "validated",
144
+ },
145
+ "errors": errors,
146
+ }
147
+ output.parent.mkdir(parents=True, exist_ok=True)
148
+ output.write_text(json.dumps(report, indent=2, ensure_ascii=False) + "\n", encoding="utf-8")
149
+ print(json.dumps({"status": report["status"], "report": str(output.resolve()), "errors": errors}, indent=2, ensure_ascii=False))
150
+ return 0 if not errors else 1
151
+
152
+
153
+ def main() -> int:
154
+ parser = argparse.ArgumentParser(description=__doc__)
155
+ parser.add_argument("--project", type=Path, default=Path.cwd())
156
+ parser.add_argument("--evidence", type=Path, required=True, help="sanitized host-adapter parity evidence JSON")
157
+ parser.add_argument("--output", type=Path, default=Path(".maggie/deployment-runtime-parity.json"))
158
+ args = parser.parse_args()
159
+ project = args.project.resolve()
160
+ evidence = args.evidence if args.evidence.is_absolute() else project / args.evidence
161
+ output = args.output if args.output.is_absolute() else project / args.output
162
+ try:
163
+ return parity(evidence, output)
164
+ except OSError as error:
165
+ print(f"BLOCKED: maggie deployment parity: {error}", file=sys.stderr)
166
+ return 2
167
+
168
+
169
+ if __name__ == "__main__":
170
+ raise SystemExit(main())
@@ -82,12 +82,23 @@ def validate_preflight(payload: dict[str, Any]) -> tuple[bool, str]:
82
82
  return True, "deployment preflight gates passed"
83
83
 
84
84
 
85
+ def validate_runtime_parity(payload: dict[str, Any]) -> tuple[bool, str]:
86
+ if payload.get("schemaVersion") != "maggie-deployment-runtime-parity.v1":
87
+ return False, "runtime parity evidence schemaVersion is unsupported"
88
+ if payload.get("status") != "passed" or payload.get("passed") is not True:
89
+ return False, "worker/browser runtime parity did not pass"
90
+ if payload.get("errors") not in ([], None):
91
+ return False, "runtime parity evidence contains errors"
92
+ return True, "worker/browser runtime parity passed"
93
+
94
+
85
95
  VALIDATORS: dict[str, Callable[[dict[str, Any]], tuple[bool, str]]] = {
86
96
  "unitRegression": validate_unit,
87
97
  "packageSmoke": validate_package,
88
98
  "browserEvidence": validate_browser,
89
99
  "renderedCanary": validate_canary,
90
100
  "deploymentPreflight": validate_preflight,
101
+ "runtimeParity": validate_runtime_parity,
91
102
  }
92
103
 
93
104
 
@@ -138,6 +149,7 @@ def main() -> int:
138
149
  parser.add_argument("--browser-report", type=Path, default=Path(".maggie/verification/browser-evidence.json"))
139
150
  parser.add_argument("--rendered-canary", type=Path, default=Path(".maggie/deployment-canary.json"))
140
151
  parser.add_argument("--deployment-preflight", type=Path, default=Path(".maggie/release-preflight.json"))
152
+ parser.add_argument("--runtime-parity", type=Path, help="validated worker/browser parity report from maggie deployment parity")
141
153
  parser.add_argument("--output", type=Path)
142
154
  args = parser.parse_args()
143
155
  project = args.project.resolve()
@@ -147,6 +159,8 @@ def main() -> int:
147
159
  name: (getattr(args, option).resolve() if getattr(args, option).is_absolute() else project / getattr(args, option))
148
160
  for name, option in CHECKS
149
161
  }
162
+ if args.runtime_parity:
163
+ paths["runtimeParity"] = args.runtime_parity.resolve() if args.runtime_parity.is_absolute() else project / args.runtime_parity
150
164
  try:
151
165
  return readiness(project, paths, args.output)
152
166
  except (OSError, ValueError) as error:
@@ -23,7 +23,7 @@ DEFAULT_SCENARIOS = Path(".maggie") / "scenario-manifest.json"
23
23
  VALID_STATUSES = {"pending", "pass", "fail", "blocked", "inconclusive"}
24
24
  VALID_PHASES = {"test", "fix", "retest"}
25
25
  ASSERTION_SCHEMA = "maggie.qa-assertions.v1"
26
- RUNTIME_ASSERTION_TYPES = {"http-status", "final-url", "text-present", "text-absent", "meta", "link-absent", "redirect", "scroll-owner-count"}
26
+ RUNTIME_ASSERTION_TYPES = {"http-status", "final-url", "text-present", "text-absent", "meta", "link-absent", "redirect", "scroll-owner-count", "icon-rendered"}
27
27
  SOURCE_ASSERTION_TYPES = {"class-present", "class-absent", "markup-present", "markup-absent", "source-text"}
28
28
  SECRET_RE = re.compile(
29
29
  r"(?i)(bearer\s+|(?:api[_-]?key|token|secret|password|authorization|cookie)\s*[=:]\s*)[^\s,;]+"
@@ -384,6 +384,19 @@ def assertion_audit(args: argparse.Namespace) -> int:
384
384
  item_errors.append(f"checks[{check_index}].expected must be 1 scroll owner")
385
385
  if isinstance(check.get("actual"), int) and not isinstance(check.get("actual"), bool) and check.get("actual") != 1:
386
386
  item_errors.append(f"checks[{check_index}] found more or fewer than one scroll owner")
387
+ if check_type == "icon-rendered":
388
+ actual = check.get("actual")
389
+ if not isinstance(actual, dict):
390
+ item_errors.append(f"checks[{check_index}].actual must be an icon runtime measurement object")
391
+ else:
392
+ if actual.get("visible") is not True:
393
+ item_errors.append(f"checks[{check_index}] did not find a visible icon glyph")
394
+ for dimension in ("width", "height"):
395
+ value = actual.get(dimension)
396
+ if not isinstance(value, (int, float)) or isinstance(value, bool) or value <= 0:
397
+ item_errors.append(f"checks[{check_index}].actual.{dimension} must be a positive rendered dimension")
398
+ if actual.get("accessibleLabel") is not True:
399
+ item_errors.append(f"checks[{check_index}] icon control is missing an accessible label")
387
400
  if any(isinstance(check, dict) and check.get("passed") is False for check in checks):
388
401
  item_errors.append("one or more runtime checks failed")
389
402
  audited.append({"id": identifier, "passed": not item_errors, "errors": item_errors})
@@ -0,0 +1,186 @@
1
+ #!/usr/bin/env python3
2
+ """Validate provider-neutral asynchronous service onboarding orchestration evidence."""
3
+
4
+ from __future__ import annotations
5
+
6
+ import argparse
7
+ import json
8
+ import re
9
+ import sys
10
+ from pathlib import Path
11
+ from typing import Any
12
+
13
+
14
+ SCHEMA = "maggie-service-onboarding-orchestration.v1"
15
+ WORKFLOWS = {"import", "search", "research"}
16
+ STATES = {"queued", "running", "succeeded", "failed", "blocked", "cancelled"}
17
+ TRANSITIONS = {
18
+ "queued": {"queued", "running", "blocked", "cancelled"},
19
+ "running": {"running", "succeeded", "failed", "blocked", "cancelled"},
20
+ "failed": {"failed", "queued", "running", "blocked", "cancelled"},
21
+ "blocked": {"blocked", "queued", "cancelled"},
22
+ "succeeded": {"succeeded"},
23
+ "cancelled": {"cancelled"},
24
+ }
25
+ SAFE_ID = re.compile(r"^[A-Za-z0-9][A-Za-z0-9._:-]{0,119}$")
26
+ SAFE_ERROR = re.compile(r"^[a-z0-9][a-z0-9._-]{0,79}$")
27
+
28
+
29
+ def load(path: Path) -> dict[str, Any]:
30
+ try:
31
+ value = json.loads(path.read_text(encoding="utf-8"))
32
+ except (OSError, json.JSONDecodeError) as error:
33
+ raise ValueError(f"cannot read orchestration manifest: {error}") from error
34
+ if not isinstance(value, dict):
35
+ raise ValueError("orchestration manifest root must be an object")
36
+ return value
37
+
38
+
39
+ def safe_status_ref(value: object) -> bool:
40
+ if not isinstance(value, str) or not value.strip() or "?" in value or "#" in value or "://" in value:
41
+ return False
42
+ if value.startswith("/"):
43
+ return True
44
+ return value.startswith(".") and ".." not in value
45
+
46
+
47
+ def validate(payload: dict[str, Any]) -> list[str]:
48
+ errors: list[str] = []
49
+ if payload.get("schemaVersion") != SCHEMA:
50
+ errors.append(f"schemaVersion must be {SCHEMA}")
51
+ workflows = payload.get("workflows")
52
+ if not isinstance(workflows, list):
53
+ errors.append("workflows must be an array")
54
+ workflows = []
55
+ seen_names: set[str] = set()
56
+ seen_jobs: set[str] = set()
57
+ seen_keys: set[str] = set()
58
+ claimed_fields: dict[str, str] = {}
59
+ for index, workflow in enumerate(workflows):
60
+ prefix = f"workflows[{index}]"
61
+ if not isinstance(workflow, dict):
62
+ errors.append(f"{prefix} must be an object")
63
+ continue
64
+ name = workflow.get("name")
65
+ if name not in WORKFLOWS:
66
+ errors.append(f"{prefix}.name must be import, search, or research")
67
+ elif name in seen_names:
68
+ errors.append(f"{prefix}.name is duplicated")
69
+ else:
70
+ seen_names.add(name)
71
+ for field, seen, pattern in (("jobId", seen_jobs, SAFE_ID), ("idempotencyKey", seen_keys, SAFE_ID)):
72
+ value = workflow.get(field)
73
+ if not isinstance(value, str) or not pattern.fullmatch(value):
74
+ errors.append(f"{prefix}.{field} must be a non-empty safe identifier")
75
+ elif value in seen:
76
+ errors.append(f"{prefix}.{field} is duplicated")
77
+ else:
78
+ seen.add(value)
79
+ state = workflow.get("state")
80
+ if state not in STATES:
81
+ errors.append(f"{prefix}.state is unsupported")
82
+ if not isinstance(workflow.get("owner"), str) or not workflow["owner"].strip():
83
+ errors.append(f"{prefix}.owner is required")
84
+ if not safe_status_ref(workflow.get("statusRef")):
85
+ errors.append(f"{prefix}.statusRef must be a pollable local route or relative artifact reference")
86
+ attempt = workflow.get("attempt")
87
+ max_attempts = workflow.get("maxAttempts")
88
+ if not isinstance(attempt, int) or isinstance(attempt, bool) or attempt < 1:
89
+ errors.append(f"{prefix}.attempt must be a positive integer")
90
+ if not isinstance(max_attempts, int) or isinstance(max_attempts, bool) or max_attempts < 1:
91
+ errors.append(f"{prefix}.maxAttempts must be a positive integer")
92
+ if isinstance(attempt, int) and isinstance(max_attempts, int) and attempt > max_attempts:
93
+ errors.append(f"{prefix}.attempt cannot exceed maxAttempts")
94
+ next_action = workflow.get("nextAction")
95
+ if not isinstance(next_action, str) or not next_action.strip():
96
+ errors.append(f"{prefix}.nextAction is required for polling or recovery")
97
+
98
+ writes = workflow.get("writes")
99
+ if not isinstance(writes, list) or not writes or any(not isinstance(field, str) or not field.strip() for field in writes):
100
+ errors.append(f"{prefix}.writes must contain owned field paths")
101
+ writes = []
102
+ for field in writes:
103
+ if field in claimed_fields:
104
+ errors.append(f"field {field!r} is claimed by more than one workflow")
105
+ else:
106
+ claimed_fields[field] = name if isinstance(name, str) else "unknown"
107
+
108
+ history = workflow.get("history")
109
+ if not isinstance(history, list) or not history:
110
+ errors.append(f"{prefix}.history must contain the state transition history")
111
+ history = []
112
+ previous = None
113
+ for history_index, event in enumerate(history):
114
+ event_prefix = f"{prefix}.history[{history_index}]"
115
+ if not isinstance(event, dict) or event.get("state") not in STATES:
116
+ errors.append(f"{event_prefix}.state is unsupported")
117
+ continue
118
+ if not isinstance(event.get("at"), str) or not event["at"].strip():
119
+ errors.append(f"{event_prefix}.at is required")
120
+ if previous is not None and event["state"] not in TRANSITIONS[previous]:
121
+ errors.append(f"{event_prefix} is an invalid state transition")
122
+ previous = event["state"]
123
+ if state in STATES and history and isinstance(history[-1], dict) and history[-1].get("state") != state:
124
+ errors.append(f"{prefix}.history must end at the current state")
125
+ if state == "failed":
126
+ error_code = workflow.get("errorCode")
127
+ if not isinstance(error_code, str) or not SAFE_ERROR.fullmatch(error_code):
128
+ errors.append(f"{prefix}.errorCode is required for a failed workflow")
129
+
130
+ if seen_names != WORKFLOWS:
131
+ errors.append("workflows must contain exactly one import, search, and research state machine")
132
+ ownership = payload.get("fieldOwnership")
133
+ if not isinstance(ownership, dict):
134
+ errors.append("fieldOwnership must map each written field to one workflow")
135
+ ownership = {}
136
+ for field, owner in ownership.items():
137
+ if not isinstance(field, str) or not field.strip() or owner not in WORKFLOWS:
138
+ errors.append("fieldOwnership contains an invalid field owner")
139
+ for field, owner in claimed_fields.items():
140
+ if ownership.get(field) != owner:
141
+ errors.append(f"fieldOwnership does not assign {field!r} to its writing workflow")
142
+ return errors
143
+
144
+
145
+ def orchestration(manifest: Path, output: Path) -> int:
146
+ try:
147
+ payload = load(manifest)
148
+ errors = validate(payload)
149
+ except ValueError as error:
150
+ errors = [str(error)]
151
+ payload = {}
152
+ workflows = payload.get("workflows") if isinstance(payload.get("workflows"), list) else []
153
+ report = {
154
+ "schemaVersion": SCHEMA,
155
+ "status": "passed" if not errors else "failed",
156
+ "passed": not errors,
157
+ "workflowCount": len(workflows),
158
+ "stateMachines": sorted(
159
+ {item.get("name") for item in workflows if isinstance(item, dict) and isinstance(item.get("name"), str)}
160
+ ),
161
+ "errors": errors,
162
+ }
163
+ output.parent.mkdir(parents=True, exist_ok=True)
164
+ output.write_text(json.dumps(report, indent=2, ensure_ascii=False) + "\n", encoding="utf-8")
165
+ print(json.dumps({"status": report["status"], "report": str(output.resolve()), "errors": errors}, indent=2, ensure_ascii=False))
166
+ return 0 if not errors else 1
167
+
168
+
169
+ def main() -> int:
170
+ parser = argparse.ArgumentParser(description=__doc__)
171
+ parser.add_argument("--project", type=Path, default=Path.cwd())
172
+ parser.add_argument("--manifest", type=Path, required=True, help="sanitized orchestration contract JSON")
173
+ parser.add_argument("--output", type=Path, default=Path("docs/service-onboarding-orchestration.json"))
174
+ args = parser.parse_args()
175
+ project = args.project.resolve()
176
+ manifest = args.manifest if args.manifest.is_absolute() else project / args.manifest
177
+ output = args.output if args.output.is_absolute() else project / args.output
178
+ try:
179
+ return orchestration(manifest, output)
180
+ except OSError as error:
181
+ print(f"BLOCKED: maggie service orchestration-validate: {error}", file=sys.stderr)
182
+ return 2
183
+
184
+
185
+ if __name__ == "__main__":
186
+ raise SystemExit(main())
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@topy-ai/maggie",
3
- "version": "0.7.23",
3
+ "version": "0.7.24",
4
4
  "description": "Install and manage Maggie Skills for AI coding agents",
5
5
  "license": "MIT",
6
6
  "type": "module",