@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 +52 -6
- package/README.zh-TW.md +6 -2
- package/bin/maggie.js +5 -1
- package/bundled-contracts/maggie-deployment/readiness-v1.schema.json +2 -1
- package/bundled-contracts/maggie-deployment/runtime-parity-v1.schema.json +38 -0
- package/bundled-contracts/maggie-service-booking/onboarding-orchestration-v1.schema.json +34 -0
- package/bundled-contracts/maggiedash/auth-model.md +11 -0
- package/bundled-references/maggiedash-integration.md +4 -0
- package/bundled-skills/maggie-auth-reference/SKILL.md +14 -1
- package/bundled-skills/maggie-deployment/SKILL.md +27 -5
- package/bundled-skills/maggie-design/SKILL.md +16 -4
- package/bundled-skills/maggie-qa-workflow/SKILL.md +11 -5
- package/bundled-skills/maggie-service-booking/SKILL.md +35 -1
- package/bundled-tools/clis/maggie_deployment_parity.py +170 -0
- package/bundled-tools/clis/maggie_deployment_readiness.py +14 -0
- package/bundled-tools/clis/maggie_qa_workflow.py +21 -1
- package/bundled-tools/clis/maggie_service_onboarding.py +186 -0
- package/package.json +1 -1
- package/references/maggiedash-integration.md +4 -0
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 ...
|
|
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
|
|
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
|
|
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.
|
|
356
|
-
npx @topy-ai/maggie@0.7.
|
|
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.
|
|
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.
|
|
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.
|
|
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
|
|
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.
|
|
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.
|
|
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
|
|
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
|
|
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.
|
|
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-*`),
|
|
59
|
-
|
|
60
|
-
|
|
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.
|
|
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`.
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
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.
|
|
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
|
@@ -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
|
|