@topy-ai/maggie 0.7.27 → 0.7.28

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
@@ -60,7 +60,12 @@ Maggie keeps the existing project foundation and asks for decisions before
60
60
  shared routes, analytics, or publishing boundaries change. The current
61
61
  package ships 19 installable skills and a local-first MaggieDash foundation.
62
62
 
63
- The current release is `0.7.27`. It adds a provider-neutral MaggieDash host
63
+ The current release is `0.7.28`. It adds a versioned Gemini model policy with
64
+ an explicit fallback and provenance, bundled-CLI version discovery, and a
65
+ bundled-first dispatcher that avoids stale project wrappers. It also adds
66
+ privacy-safe feedback batch review/duplicate detection, marketplace
67
+ enrichment evidence, bounded booking worker/resolver reports, and read-only
68
+ migration preflight contracts. The release retains the provider-neutral MaggieDash host
64
69
  adapter contract, idempotent PostgreSQL starter schema, type-only backend
65
70
  boundary, runtime endpoint conformance evidence, safe quota/error summaries,
66
71
  stale marketplace readiness revalidation, explicit VPS port/origin planning,
@@ -206,6 +211,7 @@ redacted and explicit; it is never promoted to active memory automatically.
206
211
  The CLI provides the installer plus durable workflow commands:
207
212
 
208
213
  ```text
214
+ maggie --version | version
209
215
  maggie init | install | update | remove | list | doctor
210
216
  maggie cleanup --project . [--confirm]
211
217
  maggie bootstrap interview | phase ...
@@ -226,6 +232,7 @@ maggie clone-to-template ... # URL → validated marketplace template
226
232
  maggie marketplace ... # catalog and on-demand template workflow
227
233
  maggie memory ... # confirmed preferences and lessons
228
234
  maggie feedback ... # redact, preview, batch, submit, list
235
+ maggie feedback batch-review ... # aggregate bounded drafts and detect duplicates
229
236
  maggie qa ... # scenario browser QA, fix/retest, release gate
230
237
  maggie localization ... # plan, validate, review, publish, stale
231
238
  maggie service ... # import, sync/report, catalogue, lifecycle, validate
@@ -240,9 +247,13 @@ maggie deployment | migration | release | analytics | schedule
240
247
  maggie deployment readiness --project PATH
241
248
  maggie deployment parity --project PATH --evidence FILE --output FILE
242
249
  maggie migration identity --identity-file FILE [--expected-file FILE]
250
+ maggie migration preflight --evidence FILE [--output FILE]
243
251
  maggie deployment canary --asset URL=SHA256 --render-report report.json
244
252
  maggie design icon-inventory --source-dir src --runtime assets/icons.css
245
253
  maggie api lifecycle | site-audit | ops audit
254
+ maggie marketplace enrichment-report --project PATH --input FILE [--output FILE]
255
+ maggie service worker-health --project PATH --evidence FILE [--output FILE]
256
+ maggie service resolver-audit --project PATH --evidence FILE [--output FILE]
246
257
  ```
247
258
 
248
259
  Agent content writes must declare the exact host origin that minted the
@@ -405,8 +416,8 @@ artifact schemas.
405
416
  Recommended upgrade sequence for the current release:
406
417
 
407
418
  ```bash
408
- npx @topy-ai/maggie@0.7.27 update --project . --force
409
- npx @topy-ai/maggie@0.7.27 cleanup --project .
419
+ npx @topy-ai/maggie@0.7.28 update --project . --force
420
+ npx @topy-ai/maggie@0.7.28 cleanup --project .
410
421
  ```
411
422
 
412
423
  Maintainers should pass npm credentials through the repository helper, never
@@ -577,6 +588,34 @@ maggie feedback batch --project . --batch-file ./feedback-batch.json
577
588
 
578
589
  The batch command never submits automatically or writes active memory.
579
590
 
591
+ Aggregate a correlated batch before reviewing individual drafts:
592
+
593
+ ```bash
594
+ maggie feedback batch-review --project . --batch-id run-review-001
595
+ ```
596
+
597
+ The `maggie-feedback-batch-review.v1` report retains bounded observations,
598
+ missing-index and duplicate checks, and privacy flags. It never contains raw
599
+ responses, local paths, or secrets.
600
+
601
+ The same evidence-first pattern is available for the other maintainer checks:
602
+
603
+ ```bash
604
+ maggie marketplace enrichment-report --project . \
605
+ --input .maggie/marketplace/enrichment-input.json
606
+ maggie service worker-health --project . \
607
+ --evidence .maggie/booking/worker-health-input.json
608
+ maggie service resolver-audit --project . \
609
+ --evidence .maggie/booking/resolver-input.json
610
+ maggie migration preflight \
611
+ --evidence .maggie/migration/preflight-input.json
612
+ ```
613
+
614
+ Each command validates a versioned, bounded evidence contract and writes only
615
+ sanitized status data. The deployment parity report also records the active
616
+ release revision, runtime configuration hash, migration version, worker
617
+ revision, asset fingerprints, and rollback target.
618
+
580
619
  Submission is never implicit. Project paths, source files, logs, and secrets
581
620
  are excluded by default. The hosted endpoint stores the normalized feedback in
582
621
  NoBlox for maintainer review; it does not automatically create a GitHub Issue.
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.26 init --agent all
11
+ npx @topy-ai/maggie@0.7.28 init --agent all
12
12
  npx @topy-ai/maggie doctor --project .
13
13
  ```
14
14
 
@@ -25,7 +25,9 @@ maggie doctor --project . --require-bootstrap --strict
25
25
  deployment、memory、feedback 和 MaggieDash。內容先 draft/review,外部寫入、
26
26
  publish 與 production deployment 需要明確確認。
27
27
 
28
- 目前 release 是 `0.7.26`,修正 top-level feedback help 並包含 host-owned mobile app surface contract、signed-in
28
+ 目前 release 是 `0.7.28`,加入 versioned Gemini model policy、明確 fallback 與 provenance、bundled-first CLI
29
+ dispatch、`maggie --version`、feedback batch review 聚合與重複偵測、marketplace enrichment evidence、booking
30
+ worker/resolver evidence,以及 read-only migration preflight。它也包含 host-owned mobile app surface contract、signed-in
29
31
  camera-state QA、直接 Astro route resolution、correlated feedback batch,以及
30
32
  runtime package version capture。
31
33
 
package/bin/maggie.js CHANGED
@@ -51,6 +51,7 @@ function usage() {
51
51
  console.log(`Maggie Skills installer
52
52
 
53
53
  Usage:
54
+ maggie --version | version
54
55
  maggie init [--project PATH] [--agent auto|codex|claude|all] [--skills LIST]
55
56
  maggie install [SKILL ...] [--project PATH] [--agent auto|codex|claude|all]
56
57
  maggie update [SKILL ...] [--project PATH] [--agent auto|codex|claude|all] [--force]
@@ -117,6 +118,7 @@ Usage:
117
118
  maggie deployment readiness --project PATH
118
119
  maggie deployment parity --project PATH --evidence FILE --output FILE
119
120
  maggie deployment canary --project PATH --asset URL=SHA256 --render-report report.json --output docs/deployment-canary.json
121
+ maggie migration preflight --evidence FILE --output FILE
120
122
  maggie migration --project PATH --environment staging
121
123
  maggie migration identity --identity-file FILE [--expected-file FILE]
122
124
  maggie schedule PATH/.maggie/schedule.json --project PATH
@@ -126,7 +128,7 @@ Usage:
126
128
  maggie memory <init|list|search|context|add|record-error|transition|export> --project PATH
127
129
  maggie localization <extract|plan|generate|preview|validate|review|publish|stale|glossary> [options]
128
130
  maggie seo performance|images|sitemap|indexnow|social-cards|head-tags [options] (sitemap supports strict validate and agent-files)
129
- maggie feedback <collect|preview|batch|submit|list> [options]
131
+ maggie feedback <collect|preview|batch|batch-review|submit|list> [options]
130
132
  maggie qa <start|record|summary|export|assertion-audit> [options]
131
133
  maggie site-audit URL [--crawl] [--access-log FILE] [--require-sitemap-request] [--languages en-GB,es-MX,ja-JP] [--check-hreflang]
132
134
  maggie site-audit URL --crawl --save-baseline FILE --reviewer NAME
@@ -294,7 +296,7 @@ function list() {
294
296
 
295
297
  function marketplace(args) {
296
298
  const root = projectRoot(args);
297
- const script = join(root, "tools", "clis", "maggie_marketplace.py");
299
+ const script = toolScript("maggie_marketplace.py", root);
298
300
  if (!existsSync(script)) throw new Error(`marketplace CLI is missing: ${script}`);
299
301
  const command = args[0] || "list";
300
302
  const forwarded = args.slice(1);
@@ -306,7 +308,7 @@ function marketplace(args) {
306
308
  function service(args) {
307
309
  const root = projectRoot(args);
308
310
  const scriptName = args[0] === "orchestration-validate" ? "maggie_service_onboarding.py" : "maggie_service_booking.py";
309
- const script = join(root, "tools", "clis", scriptName);
311
+ const script = toolScript(scriptName, root);
310
312
  if (!existsSync(script)) throw new Error(`service booking CLI is missing: ${script}`);
311
313
  const result = spawnSync("python3", [script, ...args], { stdio: "inherit", cwd: root, env: { ...process.env, MAGGIE_VERSION: PACKAGE_VERSION } });
312
314
  if (result.error) throw result.error;
@@ -327,7 +329,7 @@ function seo(args) {
327
329
 
328
330
  function workflowCli(name, args) {
329
331
  const root = projectRoot(args);
330
- const script = join(root, "tools", "clis", name);
332
+ const script = toolScript(name, root);
331
333
  if (!existsSync(script)) throw new Error(`${name} is missing: ${script}`);
332
334
  const env = name === "maggie_feedback.py" ? { ...process.env, MAGGIE_VERSION: PACKAGE_VERSION } : process.env;
333
335
  const result = spawnSync("python3", [script, ...args], { stdio: "inherit", cwd: root, env });
@@ -335,6 +337,15 @@ function workflowCli(name, args) {
335
337
  process.exitCode = result.status ?? 1;
336
338
  }
337
339
 
340
+ function toolScript(name, root) {
341
+ const bundled = join(TOOLS_ROOT, "clis", name);
342
+ return existsSync(bundled) ? bundled : join(root, "tools", "clis", name);
343
+ }
344
+
345
+ function version() {
346
+ console.log(PACKAGE_VERSION);
347
+ }
348
+
338
349
  function doctor(args) {
339
350
  const root = projectRoot(args);
340
351
  const checks = [
@@ -404,7 +415,8 @@ function cleanup(args) {
404
415
 
405
416
  const [command = "help", ...args] = process.argv.slice(2);
406
417
  try {
407
- if (["help", "--help", "-h"].includes(command)) usage();
418
+ if (["--version", "-v", "version"].includes(command)) version();
419
+ else if (["help", "--help", "-h"].includes(command)) usage();
408
420
  else if (command === "list") list();
409
421
  else if (command === "marketplace") marketplace(args);
410
422
  else if (command === "clone") workflowCli("maggie_clone.py", args);
@@ -3,17 +3,35 @@
3
3
  "$id": "https://maggie.noblox.app/contracts/deployment-runtime-parity-v1.json",
4
4
  "title": "Maggie deployment worker and browser runtime parity evidence",
5
5
  "type": "object",
6
- "required": ["schemaVersion", "environment", "passed", "worker", "browser"],
6
+ "required": ["schemaVersion", "environment", "passed", "release", "worker", "browser"],
7
7
  "properties": {
8
8
  "schemaVersion": {"const": "maggie-deployment-runtime-parity.v1"},
9
9
  "environment": {"enum": ["development", "staging", "production"]},
10
10
  "passed": {"const": true},
11
+ "release": {
12
+ "type": "object",
13
+ "required": ["activeRevision", "expectedRevision", "rollbackRevision", "environment", "runtimeConfigHash", "expectedConfigHash", "migrationVersion", "expectedMigrationVersion", "workerRevision", "assetVersions"],
14
+ "properties": {
15
+ "activeRevision": {"type": "string", "minLength": 1},
16
+ "expectedRevision": {"type": "string", "minLength": 1},
17
+ "rollbackRevision": {"type": "string", "minLength": 1},
18
+ "environment": {"enum": ["development", "staging", "production"]},
19
+ "runtimeConfigHash": {"type": "string", "pattern": "^[0-9a-f]{64}$"},
20
+ "expectedConfigHash": {"type": "string", "pattern": "^[0-9a-f]{64}$"},
21
+ "migrationVersion": {"type": "string", "minLength": 1},
22
+ "expectedMigrationVersion": {"type": "string", "minLength": 1},
23
+ "workerRevision": {"type": "string", "minLength": 1},
24
+ "assetVersions": {"type": "array", "minItems": 1, "items": {"type": "object", "required": ["name", "sha256"], "properties": {"name": {"type": "string"}, "sha256": {"type": "string", "pattern": "^[0-9a-f]{64}$"}}, "additionalProperties": false}}
25
+ },
26
+ "additionalProperties": true
27
+ },
11
28
  "worker": {
12
29
  "type": "object",
13
30
  "required": ["config", "migration", "routeApis", "residentProcesses", "sourceJobs"],
14
31
  "properties": {
15
32
  "config": {"$ref": "#/$defs/config"},
16
33
  "migration": {"type": "object", "required": ["privilegeCheck"], "properties": {"privilegeCheck": {"$ref": "#/$defs/passed"}}},
34
+ "revision": {"type": "string", "minLength": 1},
17
35
  "routeApis": {"type": "array", "minItems": 1, "items": {"$ref": "#/$defs/route"}},
18
36
  "residentProcesses": {"$ref": "#/$defs/processes"},
19
37
  "sourceJobs": {"type": "array", "items": {"$ref": "#/$defs/job"}}
@@ -30,7 +48,7 @@
30
48
  "$defs": {
31
49
  "passed": {"type": "object", "required": ["passed"], "properties": {"passed": {"const": true}}, "additionalProperties": true},
32
50
  "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},
51
+ "route": {"type": "object", "required": ["method", "path", "status", "passed", "releaseRevision"], "properties": {"method": {"type": "string"}, "path": {"type": "string", "pattern": "^/"}, "status": {"type": "integer", "minimum": 200, "maximum": 399}, "passed": {"const": true}, "releaseRevision": {"type": "string", "minLength": 1}}, "additionalProperties": true},
34
52
  "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
53
  "job": {"type": "object", "required": ["jobId", "passed"], "properties": {"jobId": {"type": "string", "minLength": 1}, "passed": {"const": true}}, "additionalProperties": true}
36
54
  },
@@ -0,0 +1,17 @@
1
+ {
2
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
3
+ "$id": "https://maggie.noblox.app/contracts/maggie-seo/model-policy-v1.schema.json",
4
+ "title": "Maggie Gemini model policy v1",
5
+ "type": "object",
6
+ "required": ["policyVersion", "model", "fallbackModel", "promptVersion", "schemaVersion", "locale", "sourceRevision"],
7
+ "properties": {
8
+ "policyVersion": {"const": "maggie-gemini-model-policy.v1"},
9
+ "model": {"type": "string", "pattern": "^[A-Za-z0-9][A-Za-z0-9._-]{1,99}$"},
10
+ "fallbackModel": {"type": "string", "pattern": "^[A-Za-z0-9][A-Za-z0-9._-]{1,99}$"},
11
+ "promptVersion": {"type": "string", "minLength": 1},
12
+ "schemaVersion": {"type": "string", "minLength": 1},
13
+ "locale": {"type": ["string", "null"]},
14
+ "sourceRevision": {"type": ["string", "null"]}
15
+ },
16
+ "additionalProperties": false
17
+ }
@@ -0,0 +1,17 @@
1
+ {
2
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
3
+ "$id": "https://maggie.noblox.app/contracts/resolver-v1.schema.json",
4
+ "title": "Maggie booking URL resolver outcomes v1",
5
+ "type": "object",
6
+ "required": ["schemaVersion", "status", "passed", "errors", "counts", "observations", "privacy"],
7
+ "properties": {
8
+ "schemaVersion": {"const": "maggie-booking-resolver.v1"},
9
+ "status": {"enum": ["passed", "failed"]},
10
+ "passed": {"type": "boolean"},
11
+ "errors": {"type": "array", "items": {"type": "string"}},
12
+ "counts": {"type": "object", "additionalProperties": {"type": "integer", "minimum": 0}},
13
+ "observations": {"type": "array", "maxItems": 200, "items": {"type": "object"}},
14
+ "privacy": {"type": "object", "required": ["rawResponsesIncluded", "credentialsIncluded"]}
15
+ },
16
+ "additionalProperties": false
17
+ }
@@ -0,0 +1,17 @@
1
+ {
2
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
3
+ "$id": "https://maggie.noblox.app/contracts/worker-health-v1.schema.json",
4
+ "title": "Maggie service booking worker health v1",
5
+ "type": "object",
6
+ "required": ["schemaVersion", "environment", "workerId", "status", "passed", "errors", "checks"],
7
+ "properties": {
8
+ "schemaVersion": {"const": "maggie-booking-worker-health.v1"},
9
+ "environment": {"enum": ["development", "staging", "production"]},
10
+ "workerId": {"type": "string", "minLength": 1},
11
+ "status": {"enum": ["passed", "failed"]},
12
+ "passed": {"type": "boolean"},
13
+ "errors": {"type": "array", "items": {"type": "string"}},
14
+ "checks": {"type": "object", "additionalProperties": {"type": "boolean"}}
15
+ },
16
+ "additionalProperties": false
17
+ }
@@ -0,0 +1,16 @@
1
+ {
2
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
3
+ "$id": "https://maggiedash.noblox.app/contracts/migration-preflight-v1.schema.json",
4
+ "title": "MaggieDash Migration Preflight v1",
5
+ "type": "object",
6
+ "required": ["schemaVersion", "environment", "status", "passed", "errors", "checks", "mutation"],
7
+ "properties": {
8
+ "schemaVersion": {"const": "maggie-migration-preflight.v1"},
9
+ "environment": {"enum": ["development", "staging", "production"]},
10
+ "status": {"enum": ["passed", "failed"]}, "passed": {"type": "boolean"},
11
+ "errors": {"type": "array", "items": {"type": "string"}},
12
+ "checks": {"type": "object", "additionalProperties": {"type": "boolean"}},
13
+ "mutation": {"const": "not executed"}
14
+ },
15
+ "additionalProperties": false
16
+ }
@@ -5,6 +5,18 @@ description: Manage the MaggieDash project foundation, local content store, and
5
5
 
6
6
  # MaggieDash
7
7
 
8
+ Run the read-only migration preflight before any schema write:
9
+
10
+ ```bash
11
+ maggie migration preflight \
12
+ --evidence .maggie/migration/preflight-input.json \
13
+ --output .maggie/migration/preflight.json
14
+ ```
15
+
16
+ It checks database privileges, required columns, unique constraints used by
17
+ `ON CONFLICT`, idempotency, and version advancement. Evidence is secret-free
18
+ and the command never executes a migration; a failed report blocks the write.
19
+
8
20
  Use the provider-neutral MaggieDash contracts and CLI for project setup,
9
21
  installation of the first-party admin workspace, and content operations.
10
22
 
@@ -52,10 +52,14 @@ maggie deployment parity --project . \
52
52
  The `maggie-deployment-runtime-parity.v1` contract checks required worker
53
53
  configuration and missing bindings, migration privileges, route API status,
54
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.
55
+ browser/driver compatibility. It also requires the active release revision to
56
+ match the expected revision, an equal runtime configuration hash, the expected
57
+ migration and worker revisions, non-empty asset fingerprints, route release
58
+ revisions, and a valid rollback target. It is provider-neutral: the host
59
+ adapter runs the environment checks and supplies only safe facts; Maggie
60
+ validates the evidence and never receives credentials, cookies, or response
61
+ bodies. A failed parity report blocks the readiness command and therefore the
62
+ canary.
59
63
 
60
64
  ## Release readiness evidence
61
65
 
@@ -55,9 +55,19 @@ The batch file contains 1–50 items and may provide shared `skill`, `runId`,
55
55
  `phase`, `priority`, and `affectedCli` values. An item can override those
56
56
  fields and supplies the normal `type`, `summary`, `expected`, `actual`,
57
57
  `errorFingerprint`, reproduction, resolution, validation, and screenshot
58
- metadata fields. Each generated draft stores only the safe batch ID, zero-based
59
- index, and bounded size. The batch command still creates local drafts only;
60
- review each draft and submit explicitly.
58
+ metadata fields. It may also provide bounded `routeIds` and
59
+ `validationEvidence` identifiers. Each generated draft stores only the safe
60
+ batch ID, zero-based index, and bounded size. The batch command still creates
61
+ local drafts only.
62
+
63
+ Aggregate a batch before manual review:
64
+
65
+ maggie feedback batch-review --project . --batch-id run-review-001
66
+
67
+ This writes `.maggie/feedback/batches/<batch-id>-review.json` under the
68
+ `maggie-feedback-batch-review.v1` contract. It reports missing indexes and
69
+ duplicate fingerprints/summaries, while retaining only redacted observations;
70
+ review the aggregate and each draft before submitting.
61
71
 
62
72
  Example:
63
73
 
@@ -7,6 +7,22 @@ metadata:
7
7
 
8
8
  # Maggie Marketplace
9
9
 
10
+ ## Enrichment evidence
11
+
12
+ Candidate discovery must preserve enough structured evidence to debug a
13
+ missing Google candidate, crawl, audit, snapshot, or SEO backfill without
14
+ storing provider HTML or credentials:
15
+
16
+ ```bash
17
+ maggie marketplace enrichment-report --project . \
18
+ --input .maggie/marketplace/enrichment-input.json
19
+ ```
20
+
21
+ The output follows `maggie-marketplace-enrichment.v1`, records safe status and
22
+ error codes for each stage, and makes retry eligibility explicit. Null/error
23
+ reasons remain visible for a maintainer; raw response bodies, local paths, and
24
+ secrets are excluded.
25
+
10
26
  ## Automatic memory hook
11
27
 
12
28
  Follow [Maggie Memory Hook](../../references/memory-hook.md) at invocation and completion.
@@ -7,6 +7,17 @@ metadata:
7
7
 
8
8
  # Maggie SEO and GEO
9
9
 
10
+ ## Versioned Gemini policy
11
+
12
+ AI-assisted SEO, localization, and content generation must resolve models
13
+ through one explicit policy: `maggie-gemini-model-policy.v1`. Pin the primary
14
+ model, declare a separate fallback, and record the policy version, exact
15
+ prompt/schema versions, locale, and source revision with generated evidence.
16
+ Only model availability failures (retired model, quota, timeout, or provider
17
+ 5xx) may roll forward to the declared fallback; authentication failures and
18
+ invalid output remain errors. Do not scatter retired model defaults across
19
+ routes.
20
+
10
21
  ## Static audit and sitemap evidence
11
22
 
12
23
  Run `maggie site-audit https://example.com --crawl --json` for sitemap-listed
@@ -7,6 +7,25 @@ metadata:
7
7
 
8
8
  # Maggie Service Booking
9
9
 
10
+ ## Bounded worker and resolver evidence
11
+
12
+ Before enabling provider crawling or URL resolution, validate sanitized host
13
+ evidence. These checks keep queue depth, leases, concurrency, timeouts,
14
+ retries, cancellation cleanup, and worker health explicit:
15
+
16
+ ```bash
17
+ maggie service worker-health --project . \
18
+ --evidence .maggie/booking/worker-health-input.json
19
+ maggie service resolver-audit --project . \
20
+ --evidence .maggie/booking/resolver-input.json
21
+ ```
22
+
23
+ The worker report uses `maggie-booking-worker-health.v1`. Resolver outcomes are
24
+ limited to `resolved`, `resolved-no-menu`, `not-found`, `blocked`, `timeout`,
25
+ and `unavailable`; resolved outcomes require a public canonical HTTPS URL and
26
+ safe evidence IDs. Raw provider responses, credentials, logs, and request
27
+ bodies are never copied into the reports.
28
+
10
29
  Route matching evidence must come from the route source's actual import
11
30
  statements. The shared resolver in `tools/runtime/route_imports.py` resolves
12
31
  relative imports and records a component fingerprint; it never selects a
@@ -14,6 +14,7 @@ from typing import Any
14
14
  SCHEMA = "maggie-deployment-runtime-parity.v1"
15
15
  ENVIRONMENTS = {"development", "staging", "production"}
16
16
  SAFE_ID = re.compile(r"^[A-Za-z0-9][A-Za-z0-9._:-]{0,119}$")
17
+ SHA256 = re.compile(r"^[0-9a-f]{64}$")
17
18
 
18
19
 
19
20
  def load(path: Path) -> dict[str, Any]:
@@ -42,6 +43,38 @@ def validate(payload: dict[str, Any]) -> list[str]:
42
43
  if payload.get("passed") is not True:
43
44
  errors.append("parity evidence is not marked passed")
44
45
 
46
+ release = payload.get("release")
47
+ if not isinstance(release, dict):
48
+ errors.append("release evidence must be an object")
49
+ release = {}
50
+ for field in ("activeRevision", "expectedRevision", "rollbackRevision"):
51
+ if not isinstance(release.get(field), str) or not SAFE_ID.fullmatch(release[field]):
52
+ errors.append(f"release.{field} must be a safe identifier")
53
+ if release.get("activeRevision") != release.get("expectedRevision"):
54
+ errors.append("active release revision does not match expected revision")
55
+ if release.get("rollbackRevision") == release.get("activeRevision"):
56
+ errors.append("rollback revision must differ from the active revision")
57
+ if release.get("environment") != payload.get("environment"):
58
+ errors.append("release environment does not match parity environment")
59
+ for field in ("runtimeConfigHash", "expectedConfigHash"):
60
+ if not isinstance(release.get(field), str) or not SHA256.fullmatch(release[field]):
61
+ errors.append(f"release.{field} must be a SHA-256 hex digest")
62
+ if release.get("runtimeConfigHash") != release.get("expectedConfigHash"):
63
+ errors.append("active runtime configuration does not match expected configuration")
64
+ for field in ("migrationVersion", "expectedMigrationVersion", "workerRevision"):
65
+ if not isinstance(release.get(field), str) or not SAFE_ID.fullmatch(release[field]):
66
+ errors.append(f"release.{field} must be a safe identifier")
67
+ if release.get("migrationVersion") != release.get("expectedMigrationVersion"):
68
+ errors.append("active migration version does not match expected migration version")
69
+ assets = release.get("assetVersions")
70
+ if not isinstance(assets, list) or not assets:
71
+ errors.append("release.assetVersions must contain at least one asset hash")
72
+ for index, asset in enumerate(assets if isinstance(assets, list) else []):
73
+ if not isinstance(asset, dict) or not SAFE_ID.fullmatch(str(asset.get("name") or "")):
74
+ errors.append(f"release.assetVersions[{index}].name must be a safe identifier")
75
+ if not isinstance(asset, dict) or not SHA256.fullmatch(str(asset.get("sha256") or "")):
76
+ errors.append(f"release.assetVersions[{index}].sha256 must be a SHA-256 hex digest")
77
+
45
78
  worker = payload.get("worker")
46
79
  if not isinstance(worker, dict):
47
80
  errors.append("worker evidence must be an object")
@@ -58,6 +91,8 @@ def validate(payload: dict[str, Any]) -> list[str]:
58
91
  errors.append("worker.config has missing requirements")
59
92
  if config.get("passed") is not True:
60
93
  errors.append("worker.config did not pass")
94
+ if worker.get("revision") != release.get("activeRevision"):
95
+ errors.append("worker revision does not match the active release")
61
96
 
62
97
  migration = worker.get("migration")
63
98
  privilege = migration.get("privilegeCheck") if isinstance(migration, dict) else None
@@ -84,6 +119,8 @@ def validate(payload: dict[str, Any]) -> list[str]:
84
119
  errors.append(f"{prefix}.status must be a successful HTTP status")
85
120
  if route.get("passed") is not True:
86
121
  errors.append(f"{prefix} did not pass")
122
+ if route.get("releaseRevision") != release.get("activeRevision"):
123
+ errors.append(f"{prefix}.releaseRevision does not match the active release")
87
124
 
88
125
  processes = worker.get("residentProcesses")
89
126
  if not isinstance(processes, dict):
@@ -135,6 +172,11 @@ def parity(evidence: Path, output: Path) -> int:
135
172
  "status": "passed" if not errors else "failed",
136
173
  "passed": not errors,
137
174
  "checks": {
175
+ "activeRelease": "validated",
176
+ "runtimeConfiguration": "validated",
177
+ "migrationVersion": "validated",
178
+ "assetVersions": "validated",
179
+ "rollbackTarget": "validated",
138
180
  "workerConfig": "validated",
139
181
  "migrationPrivileges": "validated",
140
182
  "routeApis": "validated",
@@ -28,6 +28,10 @@ LOCAL_PATH_RE = re.compile(
28
28
  )
29
29
  VERSION_RE = re.compile(r"\d+\.\d+\.\d+(?:[-+][A-Za-z0-9.-]+)?")
30
30
  SAFE_BATCH_ID_RE = re.compile(r"^[A-Za-z0-9][A-Za-z0-9._:-]{0,159}$")
31
+ SAFE_ROUTE_RE = re.compile(r"^/[A-Za-z0-9._~!$&'()*+,;=:@%/?#-]{1,240}$")
32
+ SAFE_EVIDENCE_RE = re.compile(r"^[A-Za-z0-9][A-Za-z0-9._:/=-]{0,159}$")
33
+ MAX_ROUTE_IDS = 20
34
+ MAX_VALIDATION_EVIDENCE = 20
31
35
  ACKNOWLEDGEMENT_KEYS = ("status", "feedbackId", "requestId")
32
36
  REPO_ROOT = Path(__file__).resolve().parents[2]
33
37
 
@@ -107,6 +111,29 @@ def load_json(path: Path) -> dict:
107
111
  return value
108
112
 
109
113
 
114
+ def safe_route_ids(values: object) -> list[str]:
115
+ """Keep bounded route identifiers, never request bodies or local paths."""
116
+ if not isinstance(values, list):
117
+ return []
118
+ result = []
119
+ for value in values[:MAX_ROUTE_IDS]:
120
+ item = safe_text(value)
121
+ if SAFE_ROUTE_RE.fullmatch(item):
122
+ result.append(item)
123
+ return sorted(set(result))
124
+
125
+
126
+ def safe_validation_evidence(values: object) -> list[str]:
127
+ if not isinstance(values, list):
128
+ return []
129
+ result = []
130
+ for value in values[:MAX_VALIDATION_EVIDENCE]:
131
+ item = safe_text(value)
132
+ if SAFE_EVIDENCE_RE.fullmatch(item):
133
+ result.append(item)
134
+ return sorted(set(result))
135
+
136
+
110
137
  def collect(args: argparse.Namespace) -> int:
111
138
  project = Path(args.project).resolve()
112
139
  report = load_json(Path(args.run_report)) if args.run_report else {}
@@ -132,6 +159,8 @@ def collect(args: argparse.Namespace) -> int:
132
159
  if priority not in {"low", "normal", "high", "critical"}:
133
160
  raise ValueError("priority must be low, normal, high, or critical")
134
161
  affected_cli = safe_text(getattr(args, "affected_cli", ""))
162
+ route_ids = safe_route_ids(getattr(args, "route_ids", []))
163
+ validation_evidence = safe_validation_evidence(getattr(args, "validation_evidence", []))
135
164
  context = {"projectFingerprint": project_fingerprint(project)}
136
165
  if args.allow_project_context and args.context_note:
137
166
  context["note"] = safe_text(args.context_note)
@@ -156,6 +185,8 @@ def collect(args: argparse.Namespace) -> int:
156
185
  "validation": validation,
157
186
  "priority": priority,
158
187
  "affectedCli": affected_cli,
188
+ "routeIds": route_ids,
189
+ "validationEvidence": validation_evidence,
159
190
  "attachments": [attachment(value) for value in args.screenshot],
160
191
  "environment": {"os": platform.system().lower(), "python": platform.python_version()},
161
192
  "privacy": {"secretsRedacted": True, "projectContextAllowed": bool(args.allow_project_context)},
@@ -264,6 +295,8 @@ def collect_batch(args: argparse.Namespace) -> int:
264
295
  "phase": safe_text(manifest.get("phase")),
265
296
  "priority": safe_text(manifest.get("priority") or "normal"),
266
297
  "affected_cli": safe_text(manifest.get("affectedCli")),
298
+ "route_ids": safe_route_ids(manifest.get("routeIds", [])),
299
+ "validation_evidence": safe_validation_evidence(manifest.get("validationEvidence", [])),
267
300
  }
268
301
  paths = []
269
302
  for index, item in enumerate(items):
@@ -292,14 +325,82 @@ def collect_batch(args: argparse.Namespace) -> int:
292
325
  "batch_size": len(items),
293
326
  "priority": item.get("priority") or shared["priority"],
294
327
  "affected_cli": item.get("affectedCli") or shared["affected_cli"],
328
+ "route_ids": item.get("routeIds", shared["route_ids"]),
329
+ "validation_evidence": item.get("validationEvidence", shared["validation_evidence"]),
295
330
  }
296
331
  if not isinstance(values["reproduce"], list): values["reproduce"] = []
297
332
  if not isinstance(values["screenshot"], list): values["screenshot"] = []
333
+ values["route_ids"] = safe_route_ids(values["route_ids"])
334
+ values["validation_evidence"] = safe_validation_evidence(values["validation_evidence"])
298
335
  paths.append(collect(argparse.Namespace(**values)))
299
336
  print(json.dumps({"batchId": batch_id, "count": len(paths), "status": "drafts-created"}, ensure_ascii=False))
300
337
  return 0
301
338
 
302
339
 
340
+ def batch_review(args: argparse.Namespace) -> int:
341
+ """Aggregate one bounded batch without exposing raw payloads or paths."""
342
+ project = Path(args.project).resolve()
343
+ batch_id = safe_text(args.batch_id)
344
+ if not SAFE_BATCH_ID_RE.fullmatch(batch_id):
345
+ raise ValueError("batch ID is invalid")
346
+ records = []
347
+ expected_sizes = []
348
+ for path in sorted((project / ".maggie" / "feedback").glob("*.json")):
349
+ try:
350
+ data = read_feedback(path)
351
+ except (OSError, ValueError, json.JSONDecodeError):
352
+ continue
353
+ batch = data.get("batch") if isinstance(data.get("batch"), dict) else {}
354
+ if batch.get("batchId") != batch_id:
355
+ continue
356
+ if isinstance(batch.get("size"), int):
357
+ expected_sizes.append(batch["size"])
358
+ records.append({
359
+ "feedbackId": safe_text(data.get("feedbackId")),
360
+ "index": batch.get("index"),
361
+ "type": safe_text(data.get("type")),
362
+ "skill": safe_text(data.get("skill")),
363
+ "phase": safe_text(data.get("phase")),
364
+ "summary": safe_text(data.get("summary")),
365
+ "expected": safe_text(data.get("expected")),
366
+ "actual": safe_text(data.get("actual")),
367
+ "errorFingerprint": safe_text(data.get("errorFingerprint")),
368
+ "fixed": data.get("fixed") is True,
369
+ "resolution": safe_text(data.get("resolution")),
370
+ "validation": safe_text(data.get("validation")),
371
+ "routeIds": safe_route_ids(data.get("routeIds", [])),
372
+ "validationEvidence": safe_validation_evidence(data.get("validationEvidence", [])),
373
+ })
374
+ records.sort(key=lambda item: (item["index"] is None, item["index"] if isinstance(item["index"], int) else 0, item["feedbackId"]))
375
+ expected_count = max(expected_sizes, default=len(records))
376
+ seen_indexes = {item["index"] for item in records if isinstance(item["index"], int)}
377
+ duplicate_groups = {}
378
+ for item in records:
379
+ key = item["errorFingerprint"] or re.sub(r"\s+", " ", item["summary"].lower()).strip()
380
+ if key:
381
+ duplicate_groups.setdefault(key, []).append(item["feedbackId"])
382
+ duplicates = [{"key": safe_text(key), "feedbackIds": ids} for key, ids in duplicate_groups.items() if len(ids) > 1]
383
+ output = Path(args.output) if args.output else project / ".maggie" / "feedback" / "batches" / f"{batch_id}-review.json"
384
+ if not output.is_absolute(): output = project / output
385
+ report = {
386
+ "schemaVersion": "maggie-feedback-batch-review.v1",
387
+ "batchId": batch_id,
388
+ "status": "passed" if records and not duplicates and len(records) == expected_count else "needs-review",
389
+ "expectedCount": expected_count,
390
+ "draftCount": len(records),
391
+ "missingIndexes": sorted(set(range(expected_count)) - seen_indexes),
392
+ "skills": sorted({item["skill"] for item in records if item["skill"]}),
393
+ "phases": sorted({item["phase"] for item in records if item["phase"]}),
394
+ "observations": records,
395
+ "duplicates": duplicates,
396
+ "privacy": {"rawPayloadsIncluded": False, "localPathsIncluded": False, "secretsRedacted": True},
397
+ }
398
+ output.parent.mkdir(parents=True, exist_ok=True)
399
+ output.write_text(json.dumps(report, indent=2, ensure_ascii=False) + "\n", encoding="utf-8")
400
+ print(json.dumps({"status": report["status"], "batchId": batch_id, "draftCount": len(records), "expectedCount": expected_count, "duplicates": len(duplicates), "report": str(output.resolve())}, ensure_ascii=False))
401
+ return 0 if report["status"] == "passed" else 1
402
+
403
+
303
404
  def main() -> int:
304
405
  parser = argparse.ArgumentParser(description=__doc__)
305
406
  parser.add_argument("--project", default=".")
@@ -329,9 +430,15 @@ def main() -> int:
329
430
  collect_parser.add_argument("--batch-size", type=int)
330
431
  collect_parser.add_argument("--priority", choices=("low", "normal", "high", "critical"), default="normal")
331
432
  collect_parser.add_argument("--affected-cli", default="")
433
+ collect_parser.add_argument("--route-id", dest="route_ids", action="append", default=[])
434
+ collect_parser.add_argument("--validation-evidence", dest="validation_evidence", action="append", default=[])
332
435
  batch_parser = sub.add_parser("batch")
333
436
  batch_parser.add_argument("--project", default=argparse.SUPPRESS)
334
437
  batch_parser.add_argument("--batch-file", required=True)
438
+ review_parser = sub.add_parser("batch-review")
439
+ review_parser.add_argument("--project", default=argparse.SUPPRESS)
440
+ review_parser.add_argument("--batch-id", required=True)
441
+ review_parser.add_argument("--output")
335
442
  preview_parser = sub.add_parser("preview")
336
443
  preview_parser.add_argument("feedback")
337
444
  preview_parser.add_argument("--format", choices=("json", "markdown"), default="json")
@@ -344,6 +451,7 @@ def main() -> int:
344
451
  try:
345
452
  if args.command == "collect": return collect(args)
346
453
  if args.command == "batch": return collect_batch(args)
454
+ if args.command == "batch-review": return batch_review(args)
347
455
  if args.command == "preview": return preview(args)
348
456
  if args.command == "submit": return submit(args)
349
457
  return list_feedback(args)
@@ -216,7 +216,15 @@ def plan_job(args: argparse.Namespace) -> int:
216
216
  "translationGroupId": identity.get("translationGroupId", f"tg-{content_id}"),
217
217
  "sourceRevision": args.source_revision or (source_artifact or {}).get("sourceRevision") or content.get("sourceRevision", "unknown"),
218
218
  "status": "draft",
219
- "translation": {"translationStatus": "draft", "isIndexable": False},
219
+ "translation": {
220
+ "translationStatus": "draft", "isIndexable": False,
221
+ "provenance": {
222
+ "operation": args.operation, "sourceRevision": args.source_revision or (source_artifact or {}).get("sourceRevision") or content.get("sourceRevision", "unknown"),
223
+ "sourceLanguage": args.source_lang, "targetLanguage": args.target_lang,
224
+ "targetLocale": args.locale, "market": args.market,
225
+ "evidenceIds": [f"content:{content_id}"], "createdAt": now(),
226
+ },
227
+ },
220
228
  "createdAt": now(),
221
229
  }
222
230
  if source_artifact:
@@ -246,6 +254,15 @@ def validate_job(path: Path, source_path: Path | None = None, render_path: Path
246
254
  errors.append("targetLocale must be a supported tag matching targetLanguage")
247
255
  translation = job.get("translation", {})
248
256
  generation = job.get("generationContract", {})
257
+ provenance = translation.get("provenance", {}) if isinstance(translation, dict) else {}
258
+ if not isinstance(provenance, dict):
259
+ errors.append("translation.provenance must be an object")
260
+ provenance = {}
261
+ for field in ("sourceLanguage", "targetLanguage", "targetLocale", "market"):
262
+ if provenance.get(field) != job.get({"sourceLanguage": "sourceLanguage", "targetLanguage": "targetLanguage", "targetLocale": "targetLocale", "market": "market"}[field]):
263
+ errors.append(f"translation.provenance.{field} does not match the job")
264
+ if provenance.get("sourceRevision") != job.get("sourceRevision"):
265
+ errors.append("translation.provenance.sourceRevision does not match the job")
249
266
  if generation.get("mode") and generation.get("mode") != job.get("operation"):
250
267
  errors.append("generationContract mode must match operation")
251
268
  if job.get("operation") == "rewrite" and not generation.get("allowStructuralRewrite"):
@@ -2061,6 +2061,47 @@ def apply_template(args: argparse.Namespace) -> int:
2061
2061
  return 0
2062
2062
 
2063
2063
 
2064
+ def enrichment_report(args: argparse.Namespace) -> int:
2065
+ """Normalize privacy-safe enrichment evidence for a marketplace candidate."""
2066
+ try:
2067
+ payload = read_json(args.input.resolve())
2068
+ except (OSError, ValueError, json.JSONDecodeError) as error:
2069
+ print(json.dumps({"status": "failed", "errors": [f"cannot read enrichment evidence: {error}"]})); return 1
2070
+ errors = []
2071
+ if payload.get("schemaVersion") != "maggie-marketplace-enrichment-input.v1": errors.append("schemaVersion is unsupported")
2072
+ candidate = payload.get("candidate") if isinstance(payload.get("candidate"), dict) else {}
2073
+ candidate_id = str(candidate.get("candidateId") or "")
2074
+ if not ID_PATTERN.fullmatch(candidate_id): errors.append("candidate.candidateId must be marketplace-safe")
2075
+ crawl = payload.get("crawl") if isinstance(payload.get("crawl"), dict) else {}
2076
+ audit = payload.get("audit") if isinstance(payload.get("audit"), dict) else {}
2077
+ snapshot = payload.get("snapshot") if isinstance(payload.get("snapshot"), dict) else {}
2078
+ for field, item in (("crawl", crawl), ("audit", audit), ("snapshot", snapshot)):
2079
+ if not item: errors.append(f"{field} evidence is required")
2080
+ if item.get("status") not in {"passed", "failed", "not-found", "blocked", "unavailable"}: errors.append(f"{field}.status is unsupported")
2081
+ if item.get("error") and not re.fullmatch(r"[a-z0-9][a-z0-9._:-]{0,119}", str(item["error"])): errors.append(f"{field}.error must be a safe error code")
2082
+ fields = payload.get("rawAuditFields", {})
2083
+ if not isinstance(fields, dict): errors.append("rawAuditFields must be an object")
2084
+ safe_fields = {}
2085
+ for key, value in (fields.items() if isinstance(fields, dict) else []):
2086
+ if re.fullmatch(r"[A-Za-z][A-Za-z0-9_.:-]{0,79}", str(key)) and not re.search(r"path|secret|token|password|cookie|header|body|html", str(key), re.I) and isinstance(value, (str, int, float, bool)) and not (isinstance(value, str) and re.search(r"(?:^|/)(?:home|Users|tmp|var|private)/", value)):
2087
+ safe_fields[str(key)] = str(value)[:240] if isinstance(value, str) else value
2088
+ backfill = payload.get("seoBackfill") if isinstance(payload.get("seoBackfill"), dict) else {}
2089
+ report = {
2090
+ "schemaVersion": "maggie-marketplace-enrichment.v1", "candidateId": candidate_id,
2091
+ "status": "passed" if not errors else "failed", "passed": not errors, "errors": errors,
2092
+ "candidate": {"source": str(candidate.get("source") or "")[:120], "market": str(candidate.get("market") or "global")[:20], "locale": str(candidate.get("locale") or "en")[:20]},
2093
+ "crawl": {"status": crawl.get("status"), "pages": crawl.get("pages", 0) if isinstance(crawl.get("pages", 0), int) else 0, "error": crawl.get("error")},
2094
+ "audit": {"status": audit.get("status"), "fields": safe_fields, "error": audit.get("error")},
2095
+ "snapshot": {"status": snapshot.get("status"), "revision": str(snapshot.get("revision") or "")[:120], "error": snapshot.get("error")},
2096
+ "seoBackfill": {"status": backfill.get("status"), "updated": backfill.get("updated", 0) if isinstance(backfill.get("updated", 0), int) else 0, "error": backfill.get("error")},
2097
+ "retry": {"allowed": bool(errors), "reason": ";".join(errors)[:240] if errors else None},
2098
+ "privacy": {"rawHtmlIncluded": False, "responseBodiesIncluded": False, "localPathsIncluded": False, "secretsRedacted": True},
2099
+ }
2100
+ output = args.output.resolve() if args.output else args.project.resolve() / "docs" / "marketplace-enrichment.json"
2101
+ output.parent.mkdir(parents=True, exist_ok=True); output.write_text(json.dumps(report, indent=2, ensure_ascii=False) + "\n", encoding="utf-8")
2102
+ print(json.dumps({"status": report["status"], "candidateId": candidate_id, "report": str(output), "retry": report["retry"]}, ensure_ascii=False)); return 0 if not errors else 1
2103
+
2104
+
2064
2105
  def main() -> int:
2065
2106
  parser = argparse.ArgumentParser(description=__doc__)
2066
2107
  parser.add_argument("--marketplace", type=Path, default=Path("marketplace"))
@@ -2130,6 +2171,11 @@ def main() -> int:
2130
2171
  apply.add_argument("--project", type=Path, default=Path.cwd())
2131
2172
  apply.add_argument("--allow-draft", action="store_true")
2132
2173
  apply.set_defaults(func=apply_template)
2174
+ enrichment = sub.add_parser("enrichment-report", help="normalize safe candidate crawl and audit evidence")
2175
+ enrichment.add_argument("--project", type=Path, default=Path.cwd())
2176
+ enrichment.add_argument("--input", type=Path, required=True)
2177
+ enrichment.add_argument("--output", type=Path)
2178
+ enrichment.set_defaults(func=enrichment_report)
2133
2179
  args = parser.parse_args()
2134
2180
  try:
2135
2181
  return int(args.func(args) or 0)
@@ -56,6 +56,8 @@ def validate_project(project: Path, environment: str) -> dict:
56
56
 
57
57
 
58
58
  IDENTITY_SCHEMA = "maggie-database-identity.v1"
59
+ PREFLIGHT_SCHEMA = "maggie-migration-preflight.v1"
60
+ SAFE_NAME = re.compile(r"^[A-Za-z][A-Za-z0-9_.:-]{0,119}$")
59
61
 
60
62
 
61
63
  def _identity_fingerprint(identity: dict) -> str:
@@ -119,7 +121,48 @@ def identity_main(argv: list[str]) -> int:
119
121
  return 0 if result["passed"] else 1
120
122
 
121
123
 
124
+ def preflight_main(argv: list[str]) -> int:
125
+ parser = argparse.ArgumentParser(description="Validate secret-free migration privilege, column, and constraint evidence.")
126
+ parser.add_argument("--evidence", required=True)
127
+ parser.add_argument("--output")
128
+ args = parser.parse_args(argv)
129
+ errors: list[str] = []
130
+ try:
131
+ payload = json.loads(Path(args.evidence).read_text(encoding="utf-8"))
132
+ except (OSError, json.JSONDecodeError) as error:
133
+ payload = {}; errors.append(f"cannot read migration preflight evidence: {error}")
134
+ if not isinstance(payload, dict): payload = {}; errors.append("preflight evidence must be an object")
135
+ if payload.get("schemaVersion") != PREFLIGHT_SCHEMA: errors.append(f"schemaVersion must be {PREFLIGHT_SCHEMA}")
136
+ if payload.get("environment") not in {"development", "staging", "production"}: errors.append("environment is invalid")
137
+ privileges = payload.get("privileges") if isinstance(payload.get("privileges"), dict) else {}
138
+ for field in ("canCreateTable", "canAlterTable", "canCreateIndex", "passed"):
139
+ if privileges.get(field) is not True: errors.append(f"privileges.{field} must be true")
140
+ columns = payload.get("requiredColumns")
141
+ if not isinstance(columns, list) or not columns: errors.append("requiredColumns must not be empty")
142
+ for index, item in enumerate(columns if isinstance(columns, list) else []):
143
+ if not isinstance(item, dict) or not SAFE_NAME.fullmatch(str(item.get("table") or "")) or not SAFE_NAME.fullmatch(str(item.get("column") or "")) or item.get("present") is not True:
144
+ errors.append(f"requiredColumns[{index}] must identify a present table column")
145
+ constraints = payload.get("uniqueConstraints")
146
+ if not isinstance(constraints, list) or not constraints: errors.append("uniqueConstraints must not be empty")
147
+ for index, item in enumerate(constraints if isinstance(constraints, list) else []):
148
+ if not isinstance(item, dict) or not SAFE_NAME.fullmatch(str(item.get("table") or "")) or not isinstance(item.get("columns"), list) or not item["columns"] or any(not SAFE_NAME.fullmatch(str(value)) for value in item["columns"]) or item.get("present") is not True:
149
+ errors.append(f"uniqueConstraints[{index}] must identify a present unique constraint")
150
+ idempotency = payload.get("idempotency") if isinstance(payload.get("idempotency"), dict) else {}
151
+ if idempotency.get("safe") is not True or idempotency.get("onConflictHandled") is not True: errors.append("idempotency must be safe and handle conflicts")
152
+ migration = payload.get("migration") if isinstance(payload.get("migration"), dict) else {}
153
+ for field in ("currentVersion", "targetVersion"):
154
+ if not SAFE_NAME.fullmatch(str(migration.get(field) or "")): errors.append(f"migration.{field} must be a safe identifier")
155
+ if migration.get("currentVersion") == migration.get("targetVersion"): errors.append("migration targetVersion must advance")
156
+ if migration.get("passed") is not True: errors.append("migration evidence did not pass")
157
+ result = {"schemaVersion": PREFLIGHT_SCHEMA, "environment": payload.get("environment"), "status": "passed" if not errors else "failed", "passed": not errors, "errors": errors, "checks": {"privileges": True, "requiredColumns": True, "uniqueConstraints": True, "idempotency": True, "migration": True}, "mutation": "not executed"}
158
+ if args.output:
159
+ output = Path(args.output); output.parent.mkdir(parents=True, exist_ok=True); output.write_text(json.dumps(result, indent=2) + "\n", encoding="utf-8")
160
+ print(json.dumps(result, indent=2)); return 0 if not errors else 1
161
+
162
+
122
163
  def main() -> int:
164
+ if sys.argv[1:2] == ["preflight"]:
165
+ return preflight_main(sys.argv[2:])
123
166
  if sys.argv[1:2] == ["identity"]:
124
167
  return identity_main(sys.argv[2:])
125
168
  parser = argparse.ArgumentParser(description=__doc__)
@@ -7,7 +7,7 @@ import xml.etree.ElementTree as ET
7
7
  from datetime import datetime, timezone
8
8
  from html.parser import HTMLParser
9
9
  from pathlib import Path
10
- from urllib.parse import quote, urljoin, urlsplit
10
+ from urllib.parse import quote, urljoin, urlsplit, urlparse
11
11
  from urllib.request import Request, urlopen
12
12
 
13
13
  sys.path.insert(0, str(Path(__file__).resolve().parents[1] / "runtime"))
@@ -1148,6 +1148,85 @@ def cmd_variant_render_audit(args):
1148
1148
  result={"schemaVersion":"1.0","generatedAt":NOW(),"environment":args.environment,"status":"passed" if not errors else "failed","variants":results,"errors":errors,"passed":not errors}
1149
1149
  out=project/"docs"/"service-variant-render-audit.json"; out.parent.mkdir(parents=True,exist_ok=True); out.write_text(json.dumps(result,indent=2)+"\n",encoding="utf-8")
1150
1150
  print(json.dumps({**result,"report":str(out)},indent=2)); return 0 if not errors else 1
1151
+ SAFE_WORKER_ID = re.compile(r"^[A-Za-z0-9][A-Za-z0-9._:-]{0,119}$")
1152
+ SAFE_EVIDENCE_ID = re.compile(r"^[A-Za-z0-9][A-Za-z0-9._:/=-]{0,159}$")
1153
+ RESOLVER_OUTCOMES = {"resolved", "resolved-no-menu", "not-found", "blocked", "timeout", "unavailable"}
1154
+
1155
+
1156
+ def cmd_worker_health(args):
1157
+ """Validate bounded queue, lease, retry, timeout, and cancellation evidence."""
1158
+ try:
1159
+ payload = json.loads(Path(args.evidence).resolve().read_text(encoding="utf-8"))
1160
+ except (OSError, ValueError, json.JSONDecodeError) as exc:
1161
+ print(json.dumps({"status": "failed", "errors": [f"cannot read worker evidence: {exc}"]}, indent=2))
1162
+ return 1
1163
+ errors = []
1164
+ if payload.get("schemaVersion") != "maggie-booking-worker-health-input.v1": errors.append("schemaVersion is unsupported")
1165
+ if payload.get("environment") not in {"development", "staging", "production"}: errors.append("environment is invalid")
1166
+ worker_id = str(payload.get("workerId") or "")
1167
+ if not SAFE_WORKER_ID.fullmatch(worker_id): errors.append("workerId must be a safe identifier")
1168
+ queue = payload.get("queue") if isinstance(payload.get("queue"), dict) else {}
1169
+ for field in ("maxDepth", "maxConcurrency"):
1170
+ if not isinstance(queue.get(field), int) or isinstance(queue.get(field), bool) or queue[field] < 1: errors.append(f"queue.{field} must be a positive integer")
1171
+ if queue.get("bounded") is not True: errors.append("queue must be bounded")
1172
+ leases = payload.get("leases") if isinstance(payload.get("leases"), dict) else {}
1173
+ if leases.get("enabled") is not True or not isinstance(leases.get("ttlSeconds"), int) or leases.get("ttlSeconds", 0) < 1: errors.append("leases must have a positive TTL")
1174
+ if leases.get("staleReclaimed") is not True: errors.append("stale leases must be reclaimed")
1175
+ timeouts = payload.get("timeouts") if isinstance(payload.get("timeouts"), dict) else {}
1176
+ for field in ("jobSeconds", "providerSeconds"):
1177
+ if not isinstance(timeouts.get(field), int) or isinstance(timeouts.get(field), bool) or timeouts[field] < 1: errors.append(f"timeouts.{field} must be positive")
1178
+ retry = payload.get("retry") if isinstance(payload.get("retry"), dict) else {}
1179
+ if not isinstance(retry.get("maxAttempts"), int) or isinstance(retry.get("maxAttempts"), bool) or retry["maxAttempts"] < 0: errors.append("retry.maxAttempts must be non-negative")
1180
+ if retry.get("backoff") not in {"fixed", "exponential", "exponential-jitter"}: errors.append("retry.backoff must be an approved strategy")
1181
+ cancellation = payload.get("cancellation") if isinstance(payload.get("cancellation"), dict) else {}
1182
+ if cancellation.get("supported") is not True or cancellation.get("cleanupVerified") is not True: errors.append("cancellation cleanup is not verified")
1183
+ health = payload.get("health") if isinstance(payload.get("health"), dict) else {}
1184
+ if not isinstance(health.get("path"), str) or not health["path"].startswith("/") or health.get("passed") is not True: errors.append("health endpoint did not pass")
1185
+ if payload.get("orphanedJobs", 0) != 0: errors.append("orphanedJobs must be zero")
1186
+ report = {"schemaVersion": "maggie-booking-worker-health.v1", "environment": payload.get("environment"), "workerId": worker_id, "status": "passed" if not errors else "failed", "passed": not errors, "errors": errors, "checks": {"boundedQueue": True, "leases": True, "timeouts": True, "retryBackoff": True, "cancellationCleanup": True, "healthEndpoint": True}}
1187
+ project = root(args)
1188
+ output = Path(args.output) if args.output else project / "docs/booking-worker-health.json"
1189
+ if not output.is_absolute(): output = project / output
1190
+ output = output.resolve()
1191
+ output.parent.mkdir(parents=True, exist_ok=True); output.write_text(json.dumps(report, indent=2) + "\n", encoding="utf-8")
1192
+ print(json.dumps({"status": report["status"], "report": str(output), "errors": errors}, indent=2)); return 0 if not errors else 1
1193
+
1194
+
1195
+ def cmd_resolver_audit(args):
1196
+ """Classify booking URL outcomes using safe canonical URL evidence."""
1197
+ try:
1198
+ payload = json.loads(Path(args.evidence).resolve().read_text(encoding="utf-8"))
1199
+ except (OSError, ValueError, json.JSONDecodeError) as exc:
1200
+ print(json.dumps({"status": "failed", "errors": [f"cannot read resolver evidence: {exc}"]}, indent=2)); return 1
1201
+ errors = []
1202
+ if payload.get("schemaVersion") != "maggie-booking-resolver-input.v1": errors.append("schemaVersion is unsupported")
1203
+ observations = payload.get("observations")
1204
+ if not isinstance(observations, list) or not observations or len(observations) > 200: errors.append("observations must contain 1–200 items")
1205
+ safe = []
1206
+ for index, item in enumerate(observations if isinstance(observations, list) else []):
1207
+ prefix = f"observations[{index}]"
1208
+ if not isinstance(item, dict): errors.append(f"{prefix} must be an object"); continue
1209
+ service_id = str(item.get("serviceId") or ""); outcome = item.get("outcome")
1210
+ if not SAFE_WORKER_ID.fullmatch(service_id): errors.append(f"{prefix}.serviceId must be a safe identifier")
1211
+ if outcome not in RESOLVER_OUTCOMES: errors.append(f"{prefix}.outcome is unsupported")
1212
+ canonical = item.get("canonicalUrl")
1213
+ if outcome in {"resolved", "resolved-no-menu"}:
1214
+ parsed = urlparse(str(canonical or ""))
1215
+ if parsed.scheme != "https" or not parsed.netloc or parsed.username or parsed.password: errors.append(f"{prefix}.canonicalUrl must be a public HTTPS URL")
1216
+ elif canonical: errors.append(f"{prefix}.canonicalUrl is only allowed for resolved outcomes")
1217
+ evidence = item.get("evidenceIds")
1218
+ if not isinstance(evidence, list) or not evidence or any(not SAFE_EVIDENCE_ID.fullmatch(str(value)) for value in evidence[:20]): errors.append(f"{prefix}.evidenceIds must contain safe identifiers")
1219
+ if not isinstance(item.get("durationMs"), int) or isinstance(item.get("durationMs"), bool) or item["durationMs"] < 0: errors.append(f"{prefix}.durationMs must be non-negative")
1220
+ safe.append({"serviceId": service_id, "outcome": outcome, "canonicalUrl": canonical if outcome in {"resolved", "resolved-no-menu"} else None, "evidenceIds": [str(value) for value in evidence[:20]] if isinstance(evidence, list) else [], "durationMs": item.get("durationMs")})
1221
+ counts = {outcome: sum(item.get("outcome") == outcome for item in safe) for outcome in sorted(RESOLVER_OUTCOMES)}
1222
+ report = {"schemaVersion": "maggie-booking-resolver.v1", "status": "passed" if not errors else "failed", "passed": not errors, "errors": errors, "counts": counts, "observations": safe, "privacy": {"rawResponsesIncluded": False, "credentialsIncluded": False}}
1223
+ project = root(args)
1224
+ output = Path(args.output) if args.output else project / "docs/booking-resolver-audit.json"
1225
+ if not output.is_absolute(): output = project / output
1226
+ output = output.resolve(); output.parent.mkdir(parents=True, exist_ok=True); output.write_text(json.dumps(report, indent=2) + "\n", encoding="utf-8")
1227
+ print(json.dumps({"status": report["status"], "report": str(output), "counts": counts, "errors": errors}, indent=2)); return 0 if not errors else 1
1228
+
1229
+
1151
1230
  def main():
1152
1231
  p=argparse.ArgumentParser(); sub=p.add_subparsers(dest="command",required=True)
1153
1232
  for name in ("import","sync","run"):
@@ -1173,6 +1252,8 @@ def main():
1173
1252
  q=sub.add_parser("variant-audit"); q.add_argument("--project",default="."); q.add_argument("--variants",required=True,help="draft service-page variant JSON"); q.add_argument("--as-of",help="ISO timestamp for expiry checks"); q.add_argument("--environment",choices=("development","staging","production"),default="staging")
1174
1253
  q=sub.add_parser("category-visual-audit"); q.add_argument("--project",default="."); q.add_argument("--screenshot-dir",required=True); q.add_argument("--min-bytes",type=int,default=5000); q.add_argument("--environment",choices=("development","staging","production"),default="staging")
1175
1254
  q=sub.add_parser("variant-render-audit"); q.add_argument("--project",default="."); q.add_argument("--variants",required=True); q.add_argument("--rendered-dir",required=True); q.add_argument("--environment",choices=("development","staging","production"),default="staging")
1255
+ q=sub.add_parser("worker-health"); q.add_argument("--project",default="."); q.add_argument("--evidence",required=True); q.add_argument("--output")
1256
+ q=sub.add_parser("resolver-audit"); q.add_argument("--project",default="."); q.add_argument("--evidence",required=True); q.add_argument("--output")
1176
1257
  for name in ("validate","inspect","status"):
1177
1258
  q=sub.add_parser(name); q.add_argument("--project",default=".")
1178
1259
  a=p.parse_args(); project=root(a)
@@ -1198,6 +1279,8 @@ def main():
1198
1279
  if a.command=="variant-audit": return cmd_variant_audit(a)
1199
1280
  if a.command=="category-visual-audit": return cmd_category_visual_audit(a)
1200
1281
  if a.command=="variant-render-audit": return cmd_variant_render_audit(a)
1282
+ if a.command=="worker-health": return cmd_worker_health(a)
1283
+ if a.command=="resolver-audit": return cmd_resolver_audit(a)
1201
1284
  if a.command=="inspect": return cmd_inspect(a)
1202
1285
  if a.command=="status": return cmd_status(a)
1203
1286
  data=load(project) if path(project).exists() else {}; errors=validate(data); print(json.dumps({"status":"passed" if not errors else "failed","errors":errors,"services":len(data.get("services",[]))},indent=2)); return 0 if not errors else 1
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@topy-ai/maggie",
3
- "version": "0.7.27",
3
+ "version": "0.7.28",
4
4
  "description": "Install and manage Maggie Skills for AI coding agents",
5
5
  "license": "MIT",
6
6
  "type": "module",