@topy-ai/maggie 0.7.22 → 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
@@ -85,6 +85,12 @@ maggie qa assertion-audit --project . \
85
85
  --output docs/qa-assertion-audit.json
86
86
  ```
87
87
 
88
+ For a dialog or drawer, include a browser-runtime `scroll-owner-count` check
89
+ with `expected: 1`. This catches the fixed-shell plus inner-content double
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.
93
+
88
94
  Dashboard and documentation audits are provider-neutral and keep host data
89
95
  behind explicit evidence files:
90
96
 
@@ -210,7 +216,7 @@ maggie memory ... # confirmed preferences and lessons
210
216
  maggie feedback ... # redact, preview, submit, list
211
217
  maggie qa ... # scenario browser QA, fix/retest, release gate
212
218
  maggie localization ... # plan, validate, review, publish, stale
213
- maggie service ... # import, sync/report, catalogue, lifecycle, validate
219
+ maggie service ... # import, sync/report, catalogue, lifecycle, validate
214
220
  maggie seo performance ... # sampled PageSpeed/CWV report and baseline
215
221
  maggie seo images ... # inventory, variants, confirmation, validate
216
222
  maggie seo sitemap ... # typed/semantic plan, agent-files, apply, rollback
@@ -220,6 +226,7 @@ maggie seo head-tags ... # rendered-shell head metadata drift audit
220
226
  maggie ops favicon-check ... # served favicon behaviour check
221
227
  maggie deployment | migration | release | analytics | schedule
222
228
  maggie deployment readiness --project PATH
229
+ maggie deployment parity --project PATH --evidence FILE --output FILE
223
230
  maggie migration identity --identity-file FILE [--expected-file FILE]
224
231
  maggie deployment canary --asset URL=SHA256 --render-report report.json
225
232
  maggie design icon-inventory --source-dir src --runtime assets/icons.css
@@ -294,6 +301,11 @@ maggie design icon-inventory --project . --source-dir src \
294
301
  --runtime public/assets/icons.css --output docs/icon-inventory.json
295
302
  ```
296
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
+
297
309
  After deployment, compare an immutable asset fingerprint and validate the
298
310
  browser adapter's rendered report:
299
311
 
@@ -308,8 +320,23 @@ The canary report requires a screenshot and zero console errors, missing
308
320
  assets, and visual placeholders for every route. It records safe cache headers
309
321
  only and never stores response bodies, cookies, or credentials.
310
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
+
311
337
  Unit regression does not establish runtime release readiness. Produce unit
312
- 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:
313
340
 
314
341
  ```bash
315
342
  python3 tools/tests/run_regression.py \
@@ -319,13 +346,27 @@ maggie deployment readiness --project . \
319
346
  --browser-report .maggie/verification/browser-evidence.json \
320
347
  --rendered-canary .maggie/deployment-canary.json \
321
348
  --deployment-preflight .maggie/release-preflight.json \
349
+ --runtime-parity .maggie/verification/runtime-parity.json \
322
350
  --output .maggie/deployment-readiness.json
323
351
  ```
324
352
 
325
353
  The readiness report follows `maggie-deployment-readiness.v1`. It reports unit
326
354
  regression, package smoke, host browser evidence, rendered canary, and
327
355
  deployment preflight separately. Missing host adapter evidence is
328
- `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.
329
370
 
330
371
  ### Localization and analytics release gates
331
372
 
@@ -352,8 +393,8 @@ artifact schemas.
352
393
  Recommended upgrade sequence for the current release:
353
394
 
354
395
  ```bash
355
- npx @topy-ai/maggie@0.7.22 update --project . --force
356
- npx @topy-ai/maggie@0.7.22 cleanup --project .
396
+ npx @topy-ai/maggie@0.7.24 update --project . --force
397
+ npx @topy-ai/maggie@0.7.24 cleanup --project .
357
398
  ```
358
399
 
359
400
  Maintainers should pass npm credentials through the repository helper, never
@@ -363,7 +404,12 @@ as a command-line argument:
363
404
  node scripts/publish-npm.mjs --maggie-env-file ../.env
364
405
  ```
365
406
 
366
- The 0.7.22 workflow adds provider-catalogue authority checks, field/variant
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,
410
+ server-runtime-only auth/provider configuration guidance, best-effort optional
411
+ identity-mirror semantics, and `scroll-owner-count` browser QA evidence. The
412
+ 0.7.22 workflow adds provider-catalogue authority checks, field/variant
367
413
  sync reports, separate supply/display states, site-owned slug proposals,
368
414
  withdrawal endings, retirement evidence audits, and runtime QA assertion lint.
369
415
  The 0.7.21 workflow adds operation-specific localization quality checks,
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.22 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,7 +25,11 @@ maggie doctor --project . --require-bootstrap --strict
25
25
  deployment、memory、feedback 和 MaggieDash。內容先 draft/review,外部寫入、
26
26
  publish 與 production deployment 需要明確確認。
27
27
 
28
- 0.7.22 加入 provider catalogue authority check、field/variant sync report、
28
+ 0.7.24 加入 `icon-rendered` browser QA assertion、worker/browser deployment parity
29
+ preflight,以及 import/search/research 的非同步 service onboarding contract。
30
+ 0.7.23 加入 dashboard dialog 單一 scroll owner、server runtime-only auth/provider
31
+ 設定規則、optional identity mirror 的 best-effort 邊界,以及 QA 的
32
+ `scroll-owner-count` browser evidence。0.7.22 加入 provider catalogue authority check、field/variant sync report、
29
33
  supply/display lifecycle state、site-owned slug proposal、withdrawal ending、
30
34
  retirement evidence audit,以及 runtime QA assertion lint。
31
35
  0.7.21 增加 localization polish/rewrite 的 operation-specific quality checks、
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
+ }
@@ -19,6 +19,17 @@ and passwordless login are not supported by this contract.
19
19
  - Every project request must check membership and role after session
20
20
  validation; a valid account alone does not grant project access.
21
21
 
22
+ ## Runtime configuration boundary
23
+
24
+ The host adapter reads database, session, and external identity-provider
25
+ settings from server runtime configuration. Private settings must not come
26
+ from browser/public environment variables, dashboard props, or client bundles.
27
+ The primary provider session is the source of truth for the login response.
28
+ An optional local identity/profile mirror may run afterward, but missing mirror
29
+ tables or temporary mirror failures must not reject an otherwise valid session.
30
+ Hosts that require the mirror must declare that requirement in their adapter
31
+ contract and test its failure path separately.
32
+
22
33
  ## Local fixture adapter
23
34
 
24
35
  `tools/runtime/maggie_auth.py` provides a dependency-free SQLite fixture. It
@@ -29,6 +29,10 @@ in environment variables and are never copied into reports.
29
29
  - Auth is traditional email/password with verified email, Argon2id production
30
30
  hashing, server-side sessions, project membership, and audit events. Passkey,
31
31
  WebAuthn, and passwordless login are not supported.
32
+ - Auth and provider configuration are server-runtime-only. An optional local
33
+ identity/profile mirror runs after the primary session and cannot turn a
34
+ successful provider login into a generic failure unless the host contract
35
+ explicitly declares that mirror required.
32
36
  - API Pull, booking, analytics, email, and deployment providers expose health,
33
37
  normalized errors, and explicit configuration schemas.
34
38
 
@@ -2,7 +2,7 @@
2
2
  name: maggie-auth-reference
3
3
  description: Generate and validate a provider-neutral traditional email/password auth reference with secure server sessions and production security gates.
4
4
  metadata:
5
- version: 1.0.0
5
+ version: 1.1.0
6
6
  ---
7
7
 
8
8
  Before and after a meaningful auth-reference run, follow the shared
@@ -26,3 +26,16 @@ HttpOnly/SameSite server-side sessions, generic failed-login responses,
26
26
  throttling, lockout, revocation, and an explicit owner setup gate. Secrets,
27
27
  hashes, and session tokens must never be emitted into browser responses,
28
28
  logs, or project context.
29
+
30
+ ## Runtime provider boundary
31
+
32
+ Read database, session, and external identity-provider settings inside the
33
+ server route from the deployment platform's runtime configuration. Do not use
34
+ browser/public environment variables for private settings, and do not pass
35
+ provider credentials through dashboard props or client bundles.
36
+
37
+ If a host also mirrors an authenticated identity into a local user/profile
38
+ table, run that mirror after the primary provider session succeeds. A missing
39
+ optional table or temporary mirror failure must be a safe, redacted warning and
40
+ must not reject a valid primary session. Make the mirror blocking only when the
41
+ host has explicitly documented it as part of the authentication contract.
@@ -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
 
@@ -303,6 +312,9 @@ shell is reconciled. Record:
303
312
  responsive changes, comparing every value with the homepage tokens;
304
313
  - hover, focus, open, loading, empty, error, scroll, tab, and reduced-motion
305
314
  behavior;
315
+ - each modal or drawer has one intentional scroll owner. Lock the page/app
316
+ shell while it is open, contain the backdrop, and let only the content area
317
+ scroll when the dialog is taller than the viewport;
306
318
  - required assets and their reuse/licensing notes;
307
319
  - page metadata and whether the page is a marketing route, conversion route,
308
320
  or actual blog content.
@@ -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.0.0
5
+ version: 1.2.0
6
6
  ---
7
7
 
8
8
  # Maggie QA Workflow
@@ -89,10 +89,16 @@ maggie qa assertion-audit --project . \
89
89
  The `maggie.qa-assertions.v1` manifest requires a route, `transport: "http"`,
90
90
  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
- `redirect`. Source/class/markup-only checks are rejected. The audit stores
93
- assertion IDs and safe pass/fail metadata, never response bodies, credentials,
94
- or cookies. A passing assertion audit complements browser evidence; it does
95
- not replace the host adapter's actual HTTP and screenshot capture.
92
+ `redirect`. For dialogs and drawers, `scroll-owner-count` records the browser
93
+ runtime's computed number of intentional scroll owners and must use
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.
96
102
 
97
103
  ## Gate and release evidence
98
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"}
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,;]+"
@@ -377,6 +377,26 @@ def assertion_audit(args: argparse.Namespace) -> int:
377
377
  item_errors.append(f"checks[{check_index}].passed must be boolean evidence")
378
378
  if check_type == "http-status" and isinstance(evidence_data.get("httpStatus"), int) and check.get("actual") != evidence_data["httpStatus"]:
379
379
  item_errors.append(f"checks[{check_index}] actual status does not match evidence.httpStatus")
380
+ if check_type == "scroll-owner-count":
381
+ if not isinstance(check.get("actual"), int) or isinstance(check.get("actual"), bool):
382
+ item_errors.append(f"checks[{check_index}].actual must be an integer scroll-owner count")
383
+ if check.get("expected") != 1:
384
+ item_errors.append(f"checks[{check_index}].expected must be 1 scroll owner")
385
+ if isinstance(check.get("actual"), int) and not isinstance(check.get("actual"), bool) and check.get("actual") != 1:
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")
380
400
  if any(isinstance(check, dict) and check.get("passed") is False for check in checks):
381
401
  item_errors.append("one or more runtime checks failed")
382
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.22",
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",
@@ -29,6 +29,10 @@ in environment variables and are never copied into reports.
29
29
  - Auth is traditional email/password with verified email, Argon2id production
30
30
  hashing, server-side sessions, project membership, and audit events. Passkey,
31
31
  WebAuthn, and passwordless login are not supported.
32
+ - Auth and provider configuration are server-runtime-only. An optional local
33
+ identity/profile mirror runs after the primary session and cannot turn a
34
+ successful provider login into a generic failure unless the host contract
35
+ explicitly declares that mirror required.
32
36
  - API Pull, booking, analytics, email, and deployment providers expose health,
33
37
  normalized errors, and explicit configuration schemas.
34
38