@topy-ai/maggie 0.7.24 → 0.7.26
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 +21 -4
- package/README.zh-TW.md +5 -1
- package/bin/maggie.js +5 -2
- package/bundled-contracts/maggie-design/mobile-app-surface-v1.schema.json +99 -0
- package/bundled-skills/maggie-design/SKILL.md +29 -2
- package/bundled-skills/maggie-feedback/SKILL.md +36 -1
- package/bundled-tools/clis/maggie_design.py +134 -1
- package/bundled-tools/clis/maggie_feedback.py +100 -2
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -60,6 +60,11 @@ 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.26`. It corrects the top-level feedback help and
|
|
64
|
+
includes the host-owned mobile app surface
|
|
65
|
+
contract (`maggie design app-init`/`app-validate`), direct Astro route
|
|
66
|
+
resolution, correlated feedback batches, and runtime package-version capture.
|
|
67
|
+
|
|
63
68
|
For repeatable browser QA, create a project-owned scenario manifest and record
|
|
64
69
|
the test, fix, and retest lifecycle:
|
|
65
70
|
|
|
@@ -213,7 +218,7 @@ maggie design ... # authorized interior-page design
|
|
|
213
218
|
maggie clone-to-template ... # URL → validated marketplace template
|
|
214
219
|
maggie marketplace ... # catalog and on-demand template workflow
|
|
215
220
|
maggie memory ... # confirmed preferences and lessons
|
|
216
|
-
maggie feedback ... # redact, preview, submit, list
|
|
221
|
+
maggie feedback ... # redact, preview, batch, submit, list
|
|
217
222
|
maggie qa ... # scenario browser QA, fix/retest, release gate
|
|
218
223
|
maggie localization ... # plan, validate, review, publish, stale
|
|
219
224
|
maggie service ... # import, sync/report, catalogue, lifecycle, validate
|
|
@@ -393,8 +398,8 @@ artifact schemas.
|
|
|
393
398
|
Recommended upgrade sequence for the current release:
|
|
394
399
|
|
|
395
400
|
```bash
|
|
396
|
-
npx @topy-ai/maggie@0.7.
|
|
397
|
-
npx @topy-ai/maggie@0.7.
|
|
401
|
+
npx @topy-ai/maggie@0.7.26 update --project . --force
|
|
402
|
+
npx @topy-ai/maggie@0.7.26 cleanup --project .
|
|
398
403
|
```
|
|
399
404
|
|
|
400
405
|
Maintainers should pass npm credentials through the repository helper, never
|
|
@@ -404,7 +409,10 @@ as a command-line argument:
|
|
|
404
409
|
node scripts/publish-npm.mjs --maggie-env-file ../.env
|
|
405
410
|
```
|
|
406
411
|
|
|
407
|
-
The 0.7.
|
|
412
|
+
The 0.7.25 workflow adds host-owned mobile app surface plans with signed-in
|
|
413
|
+
camera-state QA, direct `.astro` route resolution, correlated feedback batch
|
|
414
|
+
drafts, and runtime package-version precedence. The 0.7.24 workflow adds
|
|
415
|
+
browser-runtime `icon-rendered` assertions, sanitized
|
|
408
416
|
worker/browser parity preflight evidence, and the provider-neutral asynchronous
|
|
409
417
|
service-onboarding contract. The 0.7.23 workflow adds the single-scroll-owner MaggieDash modal primitive,
|
|
410
418
|
server-runtime-only auth/provider configuration guidance, best-effort optional
|
|
@@ -553,6 +561,15 @@ maggie feedback submit .maggie/feedback/<feedback-id>.json \
|
|
|
553
561
|
--endpoint https://feedback.noblox.app/api/feedback --confirm
|
|
554
562
|
```
|
|
555
563
|
|
|
564
|
+
For related observations, create 1–50 local drafts with shared correlation
|
|
565
|
+
metadata and review them before submitting:
|
|
566
|
+
|
|
567
|
+
```bash
|
|
568
|
+
maggie feedback batch --project . --batch-file ./feedback-batch.json
|
|
569
|
+
```
|
|
570
|
+
|
|
571
|
+
The batch command never submits automatically or writes active memory.
|
|
572
|
+
|
|
556
573
|
Submission is never implicit. Project paths, source files, logs, and secrets
|
|
557
574
|
are excluded by default. The hosted endpoint stores the normalized feedback in
|
|
558
575
|
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.
|
|
11
|
+
npx @topy-ai/maggie@0.7.26 init --agent all
|
|
12
12
|
npx @topy-ai/maggie doctor --project .
|
|
13
13
|
```
|
|
14
14
|
|
|
@@ -25,6 +25,10 @@ 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
|
|
29
|
+
camera-state QA、直接 Astro route resolution、correlated feedback batch,以及
|
|
30
|
+
runtime package version capture。
|
|
31
|
+
|
|
28
32
|
0.7.24 加入 `icon-rendered` browser QA assertion、worker/browser deployment parity
|
|
29
33
|
preflight,以及 import/search/research 的非同步 service onboarding contract。
|
|
30
34
|
0.7.23 加入 dashboard dialog 單一 scroll owner、server runtime-only auth/provider
|
package/bin/maggie.js
CHANGED
|
@@ -88,6 +88,8 @@ Usage:
|
|
|
88
88
|
maggie design reference-ui --project PATH --reference PATH --surface blog,service --confirm
|
|
89
89
|
maggie design init --project PATH --surface blog|service --reference-run PATH --confirm
|
|
90
90
|
maggie design validate-ui --project PATH --plan PATH --rendered-dir PATH --confirm
|
|
91
|
+
maggie design app-init --project PATH --route /app/valuation --auth-mode email-password --confirm
|
|
92
|
+
maggie design app-validate --project PATH --plan PATH --runtime-evidence FILE --rendered-dir PATH --confirm
|
|
91
93
|
maggie design icon-inventory --project PATH --source-dir src --runtime assets/icons.css --output docs/icon-inventory.json
|
|
92
94
|
maggie design author --project PATH --route /about --purpose TEXT --audience TEXT --confirm
|
|
93
95
|
maggie design in-place --project PATH --route /pricing [--content-source PATH]
|
|
@@ -124,7 +126,7 @@ Usage:
|
|
|
124
126
|
maggie memory <init|list|search|context|add|record-error|transition|export> --project PATH
|
|
125
127
|
maggie localization <extract|plan|generate|preview|validate|review|publish|stale|glossary> [options]
|
|
126
128
|
maggie seo performance|images|sitemap|indexnow|social-cards|head-tags [options] (sitemap supports strict validate and agent-files)
|
|
127
|
-
maggie feedback <collect|preview|submit|list> [options]
|
|
129
|
+
maggie feedback <collect|preview|batch|submit|list> [options]
|
|
128
130
|
maggie qa <start|record|summary|export|assertion-audit> [options]
|
|
129
131
|
maggie site-audit URL [--crawl] [--access-log FILE] [--require-sitemap-request] [--languages en-GB,es-MX,ja-JP] [--check-hreflang]
|
|
130
132
|
maggie site-audit URL --crawl --save-baseline FILE --reviewer NAME
|
|
@@ -327,7 +329,8 @@ function workflowCli(name, args) {
|
|
|
327
329
|
const root = projectRoot(args);
|
|
328
330
|
const script = join(root, "tools", "clis", name);
|
|
329
331
|
if (!existsSync(script)) throw new Error(`${name} is missing: ${script}`);
|
|
330
|
-
const
|
|
332
|
+
const env = name === "maggie_feedback.py" ? { ...process.env, MAGGIE_VERSION: PACKAGE_VERSION } : process.env;
|
|
333
|
+
const result = spawnSync("python3", [script, ...args], { stdio: "inherit", cwd: root, env });
|
|
331
334
|
if (result.error) throw result.error;
|
|
332
335
|
process.exitCode = result.status ?? 1;
|
|
333
336
|
}
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
3
|
+
"$id": "https://github.com/TOPY-AI-LTD/ai-cmo-skills/blob/main/contracts/maggie-design/mobile-app-surface-v1.schema.json",
|
|
4
|
+
"title": "Maggie mobile app surface contract",
|
|
5
|
+
"type": "object",
|
|
6
|
+
"required": [
|
|
7
|
+
"schemaVersion",
|
|
8
|
+
"workflow",
|
|
9
|
+
"mode",
|
|
10
|
+
"route",
|
|
11
|
+
"auth",
|
|
12
|
+
"shell",
|
|
13
|
+
"camera",
|
|
14
|
+
"viewports",
|
|
15
|
+
"requiredSteps",
|
|
16
|
+
"publicWrite"
|
|
17
|
+
],
|
|
18
|
+
"properties": {
|
|
19
|
+
"schemaVersion": {
|
|
20
|
+
"const": "maggie-mobile-app-surface.v1"
|
|
21
|
+
},
|
|
22
|
+
"workflow": {
|
|
23
|
+
"const": "maggie-design"
|
|
24
|
+
},
|
|
25
|
+
"mode": {
|
|
26
|
+
"const": "app-view-init"
|
|
27
|
+
},
|
|
28
|
+
"route": {
|
|
29
|
+
"type": "string",
|
|
30
|
+
"pattern": "^/"
|
|
31
|
+
},
|
|
32
|
+
"auth": {
|
|
33
|
+
"type": "object",
|
|
34
|
+
"required": ["mode", "required", "credentialPolicy"],
|
|
35
|
+
"properties": {
|
|
36
|
+
"mode": {
|
|
37
|
+
"enum": ["email-password", "existing-session", "none"]
|
|
38
|
+
},
|
|
39
|
+
"required": {
|
|
40
|
+
"type": "boolean"
|
|
41
|
+
},
|
|
42
|
+
"credentialPolicy": {
|
|
43
|
+
"const": "use-host-auth-without-recording-credentials"
|
|
44
|
+
}
|
|
45
|
+
},
|
|
46
|
+
"additionalProperties": true
|
|
47
|
+
},
|
|
48
|
+
"shell": {
|
|
49
|
+
"type": "object",
|
|
50
|
+
"required": ["fixedHeader", "fixedFooter", "oneScrollOwner"],
|
|
51
|
+
"properties": {
|
|
52
|
+
"fixedHeader": {"const": true},
|
|
53
|
+
"fixedFooter": {"const": true},
|
|
54
|
+
"oneScrollOwner": {"const": true}
|
|
55
|
+
},
|
|
56
|
+
"additionalProperties": true
|
|
57
|
+
},
|
|
58
|
+
"camera": {
|
|
59
|
+
"type": "object",
|
|
60
|
+
"required": ["requiredStates", "controls", "fallback"],
|
|
61
|
+
"properties": {
|
|
62
|
+
"requiredStates": {
|
|
63
|
+
"type": "array",
|
|
64
|
+
"contains": {"const": "permission"}
|
|
65
|
+
},
|
|
66
|
+
"controls": {
|
|
67
|
+
"type": "array",
|
|
68
|
+
"minItems": 4
|
|
69
|
+
},
|
|
70
|
+
"fallback": {
|
|
71
|
+
"type": "string",
|
|
72
|
+
"minLength": 1
|
|
73
|
+
}
|
|
74
|
+
},
|
|
75
|
+
"additionalProperties": true
|
|
76
|
+
},
|
|
77
|
+
"viewports": {
|
|
78
|
+
"type": "object",
|
|
79
|
+
"required": ["desktop", "tablet", "mobile"],
|
|
80
|
+
"additionalProperties": {
|
|
81
|
+
"type": "array",
|
|
82
|
+
"prefixItems": [
|
|
83
|
+
{"type": "integer", "minimum": 1},
|
|
84
|
+
{"type": "integer", "minimum": 1}
|
|
85
|
+
],
|
|
86
|
+
"minItems": 2,
|
|
87
|
+
"maxItems": 2
|
|
88
|
+
}
|
|
89
|
+
},
|
|
90
|
+
"requiredSteps": {
|
|
91
|
+
"type": "array",
|
|
92
|
+
"contains": {"const": "signed-in-browser-qa"}
|
|
93
|
+
},
|
|
94
|
+
"publicWrite": {
|
|
95
|
+
"const": false
|
|
96
|
+
}
|
|
97
|
+
},
|
|
98
|
+
"additionalProperties": true
|
|
99
|
+
}
|
|
@@ -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.6.0
|
|
6
6
|
---
|
|
7
7
|
|
|
8
8
|
# Maggie Design
|
|
@@ -10,7 +10,7 @@ metadata:
|
|
|
10
10
|
## In-place route evidence
|
|
11
11
|
|
|
12
12
|
`maggie design in-place --project . --route /example` resolves local source
|
|
13
|
-
files and falls back to a discovered `[...slug]`/`[[...slug]]` file. A catch-all
|
|
13
|
+
files, including direct `src/pages/<route>.astro` files, and falls back to a discovered `[...slug]`/`[[...slug]]` file. A catch-all
|
|
14
14
|
is recorded as `resolution: unproven` unless `--content-source` names the
|
|
15
15
|
actual DB/content resolver. A source candidate is not proof that the concrete
|
|
16
16
|
URL is served: verify the HTTP response, content identity, and framework route
|
|
@@ -202,6 +202,33 @@ semantics from `maggie-blog`. For service UI, preserve provider identity,
|
|
|
202
202
|
variants, booking URLs, duplicate canonical/noindex decisions, and service
|
|
203
203
|
sitemap semantics from `maggie-service-booking`.
|
|
204
204
|
|
|
205
|
+
## Mobile app surfaces
|
|
206
|
+
|
|
207
|
+
Use the app surface contract for camera, scan, inventory, valuation, or other
|
|
208
|
+
authenticated app views. It is a general host contract, not a replacement for
|
|
209
|
+
the project's auth or camera implementation:
|
|
210
|
+
|
|
211
|
+
maggie design app-init --project . --route /app/valuation \
|
|
212
|
+
--auth-mode email-password --confirm
|
|
213
|
+
|
|
214
|
+
The initializer records the fixed header/footer, one-scroll-owner rule,
|
|
215
|
+
camera permission/ready/gallery/scan/unavailable states, decision-critical UI,
|
|
216
|
+
and desktop/tablet/mobile evidence requirements. It never collects credentials
|
|
217
|
+
or copies private project data. Use `existing-session` only when the host
|
|
218
|
+
already supplies a safe signed-in session; use `none` for public app surfaces.
|
|
219
|
+
|
|
220
|
+
After implementation, validate sanitized runtime evidence and screenshots:
|
|
221
|
+
|
|
222
|
+
maggie design app-validate --project . \
|
|
223
|
+
--plan .maggie/design/app-view/app-<id>/plan.json \
|
|
224
|
+
--runtime-evidence ./qa/mobile-app-evidence.json \
|
|
225
|
+
--rendered-dir ./qa/screenshots --confirm
|
|
226
|
+
|
|
227
|
+
The evidence must prove the auth state, fixed shell, exactly one scroll owner,
|
|
228
|
+
all required camera states, and valid PNGs at 390px, 768px, and desktop
|
|
229
|
+
widths. The machine-readable contract is
|
|
230
|
+
[mobile-app-surface-v1.schema.json](../../bundled-contracts/maggie-design/mobile-app-surface-v1.schema.json).
|
|
231
|
+
|
|
205
232
|
## Explicit homepage rebrand mode
|
|
206
233
|
|
|
207
234
|
The homepage `review` mode only compares screenshots. It does not rebrand a
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
name: maggie-feedback
|
|
3
3
|
description: Collect, review, and explicitly submit privacy-safe feedback about a Maggie skill run, workflow result, bug, or feature request.
|
|
4
4
|
metadata:
|
|
5
|
-
version: 1.
|
|
5
|
+
version: 1.1.0
|
|
6
6
|
---
|
|
7
7
|
|
|
8
8
|
# Maggie Feedback
|
|
@@ -39,6 +39,41 @@ If a draft is marked fixed with `--fixed` (or the supplied run report says
|
|
|
39
39
|
also scrubs common POSIX/home/temp and Windows absolute paths by default while
|
|
40
40
|
preserving public URLs and route paths.
|
|
41
41
|
|
|
42
|
+
The CLI records the executing Maggie package version when it is available from
|
|
43
|
+
the runtime environment or `node_modules/@topy-ai/maggie/package.json`. A
|
|
44
|
+
stale `.maggie/install.json` is only a fallback; conflicting runtime package
|
|
45
|
+
versions fail closed as `unknown`.
|
|
46
|
+
|
|
47
|
+
## Correlated feedback batches
|
|
48
|
+
|
|
49
|
+
When several observations belong to one workflow run, create bounded drafts
|
|
50
|
+
with one shared correlation ID instead of losing their relationship:
|
|
51
|
+
|
|
52
|
+
maggie feedback batch --project . --batch-file ./feedback-batch.json
|
|
53
|
+
|
|
54
|
+
The batch file contains 1–50 items and may provide shared `skill`, `runId`,
|
|
55
|
+
`phase`, `priority`, and `affectedCli` values. An item can override those
|
|
56
|
+
fields and supplies the normal `type`, `summary`, `expected`, `actual`,
|
|
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.
|
|
61
|
+
|
|
62
|
+
Example:
|
|
63
|
+
|
|
64
|
+
{
|
|
65
|
+
"batchId": "run-review-001",
|
|
66
|
+
"skill": "maggie-design",
|
|
67
|
+
"runId": "design-001",
|
|
68
|
+
"phase": "post-release-review",
|
|
69
|
+
"priority": "normal",
|
|
70
|
+
"affectedCli": "design",
|
|
71
|
+
"items": [
|
|
72
|
+
{"type": "bug", "summary": "Route evidence is incomplete", "expected": "Direct route resolves", "actual": "Fallback was used"},
|
|
73
|
+
{"type": "feature", "summary": "Need an app-surface QA contract", "expected": "Reusable evidence plan", "actual": "Only public-page rules exist"}
|
|
74
|
+
]
|
|
75
|
+
}
|
|
76
|
+
|
|
42
77
|
## Submit explicitly
|
|
43
78
|
|
|
44
79
|
The CLI never submits automatically. After reviewing the draft:
|
|
@@ -157,6 +157,114 @@ def validate_content_ui(project: Path, plan_path: Path, rendered_dir: Path | Non
|
|
|
157
157
|
output = plan_path.parent / "validation.json"; output.write_text(json.dumps(result, indent=2, ensure_ascii=False) + "\n", encoding="utf-8"); print(json.dumps(result, indent=2, ensure_ascii=False)); return 0 if not errors else 1
|
|
158
158
|
|
|
159
159
|
|
|
160
|
+
def app_surface_init(project: Path, route: str, auth_mode: str, force: bool, confirm: bool) -> int:
|
|
161
|
+
"""Create a host-owned mobile app surface contract and QA manifest."""
|
|
162
|
+
project = project.resolve()
|
|
163
|
+
if not route.startswith("/") or "?" in route or "#" in route:
|
|
164
|
+
raise ValueError("route must be a path beginning with / and must not contain a query or hash")
|
|
165
|
+
if auth_mode not in {"email-password", "existing-session", "none"}:
|
|
166
|
+
raise ValueError("auth mode must be email-password, existing-session, or none")
|
|
167
|
+
contract = require_design_contract(project)
|
|
168
|
+
relative = route.strip("/")
|
|
169
|
+
candidates = [
|
|
170
|
+
project / relative,
|
|
171
|
+
project / "src" / "pages" / relative,
|
|
172
|
+
project / "src" / "pages" / f"{relative}.astro",
|
|
173
|
+
project / "src" / "pages" / f"{relative}.html",
|
|
174
|
+
project / "src" / "app" / relative,
|
|
175
|
+
project / "src" / "app" / relative / "page.tsx",
|
|
176
|
+
]
|
|
177
|
+
source = next((path for path in candidates if path.is_file()), None)
|
|
178
|
+
digest = hashlib.sha256((str(project) + "\n" + route + "\n" + auth_mode).encode()).hexdigest()[:12]
|
|
179
|
+
output_dir = project / ".maggie" / "design" / "app-view" / f"app-{digest}"
|
|
180
|
+
if output_dir.exists() and not force:
|
|
181
|
+
print((output_dir / "plan.json").read_text(encoding="utf-8"), end="")
|
|
182
|
+
return 0
|
|
183
|
+
output_dir.mkdir(parents=True, exist_ok=True)
|
|
184
|
+
viewports = {"desktop": REVIEW_VIEWPORTS["desktop"], "tablet": REVIEW_VIEWPORTS["tablet"], "mobile": REVIEW_VIEWPORTS["mobile"]}
|
|
185
|
+
plan = {
|
|
186
|
+
"schemaVersion": "maggie-mobile-app-surface.v1",
|
|
187
|
+
"id": f"app-{digest}",
|
|
188
|
+
"workflow": "maggie-design",
|
|
189
|
+
"mode": "app-view-init",
|
|
190
|
+
"phase": "ready",
|
|
191
|
+
"route": route,
|
|
192
|
+
"routeSource": str(source) if source else None,
|
|
193
|
+
"designContract": contract,
|
|
194
|
+
"auth": {"mode": auth_mode, "required": auth_mode != "none", "credentialPolicy": "use-host-auth-without-recording-credentials"},
|
|
195
|
+
"shell": {"fixedHeader": True, "fixedFooter": True, "oneScrollOwner": True, "cameraMayUseFullScreenViewport": True, "hostShellSourceOfTruth": "existing homepage shell"},
|
|
196
|
+
"camera": {"requiredStates": ["permission", "ready", "gallery", "scan", "unavailable"], "controls": ["close", "gallery", "shutter", "mode"], "fallback": "show an actionable unavailable state; never leave a black screen"},
|
|
197
|
+
"decisionCriticalUi": ["card normalization", "graded-price fallback", "grade-9-to-10 economics", "scan-to-inventory or valuation transition"],
|
|
198
|
+
"viewports": viewports,
|
|
199
|
+
"requiredSteps": ["inspect-host-shell", "define-auth-gate", "define-camera-states", "capture-desktop-tablet-mobile", "signed-in-browser-qa", "accessibility-check", "route-validation", "explicit-approval"],
|
|
200
|
+
"provenance": {"hostOwnsAuthAndCamera": True, "designContractReused": True, "privateDataCopied": False},
|
|
201
|
+
"publicWrite": False,
|
|
202
|
+
"createdAt": datetime.now(timezone.utc).isoformat(),
|
|
203
|
+
}
|
|
204
|
+
evidence = {
|
|
205
|
+
"schemaVersion": "maggie-mobile-app-evidence.v1",
|
|
206
|
+
"route": route,
|
|
207
|
+
"auth": plan["auth"],
|
|
208
|
+
"shell": plan["shell"],
|
|
209
|
+
"camera": plan["camera"],
|
|
210
|
+
"viewports": {name: {"screenshot": f"{name}.png", "captured": False} for name in viewports},
|
|
211
|
+
"status": "pending",
|
|
212
|
+
}
|
|
213
|
+
qa = {"schemaVersion": "maggie-mobile-app-qa.v1", "route": route, "auth": plan["auth"], "scenarios": [
|
|
214
|
+
{"id": "shell-scroll", "viewport": "mobile", "assertions": ["fixed header and footer remain usable", "exactly one scroll owner"]},
|
|
215
|
+
{"id": "camera-permission", "viewport": "mobile", "assertions": ["permission state is explained", "denied permission shows fallback"]},
|
|
216
|
+
{"id": "camera-scan", "viewport": "mobile", "assertions": ["gallery/shutter/mode/close controls are reachable", "scan result can enter inventory or valuation"]},
|
|
217
|
+
{"id": "valuation-auth", "viewport": "desktop", "assertions": ["signed-in route does not redirect unexpectedly", "grade economics and fallback price remain legible"]},
|
|
218
|
+
]}
|
|
219
|
+
(output_dir / "plan.json").write_text(json.dumps(plan, indent=2, ensure_ascii=False) + "\n", encoding="utf-8")
|
|
220
|
+
(output_dir / "qa-scenarios.json").write_text(json.dumps(qa, indent=2, ensure_ascii=False) + "\n", encoding="utf-8")
|
|
221
|
+
(output_dir / "validation.json").write_text(json.dumps(evidence, indent=2, ensure_ascii=False) + "\n", encoding="utf-8")
|
|
222
|
+
(output_dir / "screenshots").mkdir(exist_ok=True)
|
|
223
|
+
print(json.dumps({"plan": str(output_dir / "plan.json"), "qaScenarios": str(output_dir / "qa-scenarios.json"), "surface": "app", "phase": "ready", "publicWrite": False}, indent=2, ensure_ascii=False))
|
|
224
|
+
return 0
|
|
225
|
+
|
|
226
|
+
|
|
227
|
+
def validate_app_surface(project: Path, plan_path: Path, rendered_dir: Path | None, runtime_evidence: Path | None, confirm: bool) -> int:
|
|
228
|
+
"""Validate signed-in app evidence and responsive screenshots."""
|
|
229
|
+
project = project.resolve(); plan_path = plan_path.resolve()
|
|
230
|
+
if not plan_path.exists(): raise ValueError(f"app surface plan not found: {plan_path}")
|
|
231
|
+
plan = json.loads(plan_path.read_text(encoding="utf-8")); errors = []
|
|
232
|
+
if plan.get("workflow") != "maggie-design" or plan.get("mode") != "app-view-init": errors.append("invalid mobile app surface plan")
|
|
233
|
+
if plan.get("publicWrite") is not False: errors.append("publicWrite must remain false before approval")
|
|
234
|
+
if not plan.get("shell", {}).get("oneScrollOwner"): errors.append("app surface must define one scroll owner")
|
|
235
|
+
if not plan.get("auth", {}).get("credentialPolicy"): errors.append("app surface must define a credential policy")
|
|
236
|
+
try: require_design_contract(project)
|
|
237
|
+
except ValueError as error: errors.append(str(error))
|
|
238
|
+
evidence = {}
|
|
239
|
+
evidence_path = runtime_evidence.resolve() if runtime_evidence else plan_path.parent / "runtime-evidence.json"
|
|
240
|
+
if evidence_path.is_file():
|
|
241
|
+
try: evidence = json.loads(evidence_path.read_text(encoding="utf-8"))
|
|
242
|
+
except json.JSONDecodeError: errors.append("runtime evidence is not valid JSON")
|
|
243
|
+
else:
|
|
244
|
+
errors.append("runtime evidence file is required")
|
|
245
|
+
if evidence:
|
|
246
|
+
if evidence.get("schemaVersion") != "maggie-mobile-app-evidence.v1": errors.append("runtime evidence schemaVersion is invalid")
|
|
247
|
+
if evidence.get("route") != plan.get("route"): errors.append("runtime evidence route does not match plan")
|
|
248
|
+
auth = evidence.get("auth", {})
|
|
249
|
+
if plan.get("auth", {}).get("required") and auth.get("signedIn") is not True: errors.append("signed-in evidence is required for this app route")
|
|
250
|
+
shell = evidence.get("shell", {})
|
|
251
|
+
if shell.get("fixedHeader") is not True or shell.get("fixedFooter") is not True or shell.get("scrollOwnerCount") != 1: errors.append("runtime evidence must prove fixed shell and one scroll owner")
|
|
252
|
+
camera = evidence.get("camera", {})
|
|
253
|
+
camera_states = set(camera.get("states", [])) if isinstance(camera, dict) else set()
|
|
254
|
+
if not set(plan.get("camera", {}).get("requiredStates", [])).issubset(camera_states): errors.append("runtime evidence is missing a required camera state")
|
|
255
|
+
screenshot_status = {}
|
|
256
|
+
for name in ("desktop", "tablet", "mobile"):
|
|
257
|
+
path = (rendered_dir / f"{name}.png") if rendered_dir else plan_path.parent / "screenshots" / f"{name}.png"
|
|
258
|
+
valid = path.is_file() and path.read_bytes()[:8] == b"\x89PNG\r\n\x1a\n"
|
|
259
|
+
screenshot_status[name] = {"path": str(path), "validPng": valid}
|
|
260
|
+
if not valid: errors.append(f"missing valid {name} screenshot")
|
|
261
|
+
result = {"schemaVersion": "maggie-mobile-app-validation.v1", "plan": str(plan_path), "route": plan.get("route"), "screenshots": screenshot_status, "runtimeEvidence": str(evidence_path), "checks": {"fixedShell": not any("fixed shell" in error for error in errors), "signedInRoute": not any("signed-in" in error for error in errors), "cameraStates": not any("camera state" in error for error in errors), "publicWriteBlocked": plan.get("publicWrite") is False}, "status": "passed" if not errors else "failed", "errors": errors, "validatedAt": datetime.now(timezone.utc).isoformat()}
|
|
262
|
+
if confirm:
|
|
263
|
+
plan_path.parent.joinpath("validation.json").write_text(json.dumps(result, indent=2, ensure_ascii=False) + "\n", encoding="utf-8")
|
|
264
|
+
print(json.dumps(result, indent=2, ensure_ascii=False))
|
|
265
|
+
return 0 if not errors else 1
|
|
266
|
+
|
|
267
|
+
|
|
160
268
|
def rebrand_template(args: argparse.Namespace) -> int:
|
|
161
269
|
"""Apply an explicit brand identity to a packaged homepage template."""
|
|
162
270
|
template = args.template.resolve()
|
|
@@ -547,6 +655,7 @@ def in_place_job(project: Path, routes: list[str], force: bool = False, content_
|
|
|
547
655
|
project / relative,
|
|
548
656
|
project / "src" / "pages" / relative,
|
|
549
657
|
project / "src" / "app" / relative,
|
|
658
|
+
project / "src" / "pages" / f"{relative}.astro",
|
|
550
659
|
project / "src" / "pages" / f"{relative}.html",
|
|
551
660
|
project / "src" / "app" / relative / "page.tsx",
|
|
552
661
|
]
|
|
@@ -680,7 +789,7 @@ commands:
|
|
|
680
789
|
status show a design job and its per-step progress
|
|
681
790
|
resume restart a failed design workflow
|
|
682
791
|
author plan an original first-party page
|
|
683
|
-
reference-ui, init, validate-ui, rebrand, review
|
|
792
|
+
reference-ui, init, validate-ui, app-init, app-validate, rebrand, review
|
|
684
793
|
""")
|
|
685
794
|
return 0
|
|
686
795
|
if len(sys.argv) > 1 and sys.argv[1] == "reference-ui":
|
|
@@ -719,6 +828,30 @@ commands:
|
|
|
719
828
|
return validate_content_ui(args.project, args.plan, args.rendered_dir, args.confirm)
|
|
720
829
|
except (OSError, ValueError, json.JSONDecodeError) as error:
|
|
721
830
|
print(f"BLOCKED: maggie-design validate-ui: {error}", file=sys.stderr); return 1
|
|
831
|
+
if len(sys.argv) > 1 and sys.argv[1] == "app-init":
|
|
832
|
+
command = argparse.ArgumentParser(description="Create a mobile app surface contract with signed-in QA scenarios.")
|
|
833
|
+
command.add_argument("--project", type=Path, default=Path.cwd())
|
|
834
|
+
command.add_argument("--route", required=True)
|
|
835
|
+
command.add_argument("--auth-mode", choices=["email-password", "existing-session", "none"], default="email-password")
|
|
836
|
+
command.add_argument("--force", action="store_true")
|
|
837
|
+
command.add_argument("--confirm", action="store_true")
|
|
838
|
+
args = command.parse_args(sys.argv[2:])
|
|
839
|
+
try:
|
|
840
|
+
return app_surface_init(args.project, args.route, args.auth_mode, args.force, args.confirm)
|
|
841
|
+
except (OSError, ValueError, json.JSONDecodeError) as error:
|
|
842
|
+
print(f"BLOCKED: maggie-design app-init: {error}", file=sys.stderr); return 1
|
|
843
|
+
if len(sys.argv) > 1 and sys.argv[1] == "app-validate":
|
|
844
|
+
command = argparse.ArgumentParser(description="Validate mobile app runtime evidence and responsive screenshots.")
|
|
845
|
+
command.add_argument("--project", type=Path, default=Path.cwd())
|
|
846
|
+
command.add_argument("--plan", type=Path, required=True)
|
|
847
|
+
command.add_argument("--rendered-dir", type=Path)
|
|
848
|
+
command.add_argument("--runtime-evidence", type=Path)
|
|
849
|
+
command.add_argument("--confirm", action="store_true")
|
|
850
|
+
args = command.parse_args(sys.argv[2:])
|
|
851
|
+
try:
|
|
852
|
+
return validate_app_surface(args.project, args.plan, args.rendered_dir, args.runtime_evidence, args.confirm)
|
|
853
|
+
except (OSError, ValueError, json.JSONDecodeError) as error:
|
|
854
|
+
print(f"BLOCKED: maggie-design app-validate: {error}", file=sys.stderr); return 1
|
|
722
855
|
if len(sys.argv) > 1 and sys.argv[1] == "rebrand":
|
|
723
856
|
rebrand = argparse.ArgumentParser(description="Apply an explicit brand identity to a packaged marketplace template.")
|
|
724
857
|
rebrand.add_argument("--template", type=Path, required=True)
|
|
@@ -27,6 +27,7 @@ LOCAL_PATH_RE = re.compile(
|
|
|
27
27
|
r"\\\\[^\\/\s,;:)\]}]+(?:[\\/]+[^\\/\s,;:)\]}]+)+)"
|
|
28
28
|
)
|
|
29
29
|
VERSION_RE = re.compile(r"\d+\.\d+\.\d+(?:[-+][A-Za-z0-9.-]+)?")
|
|
30
|
+
SAFE_BATCH_ID_RE = re.compile(r"^[A-Za-z0-9][A-Za-z0-9._:-]{0,159}$")
|
|
30
31
|
ACKNOWLEDGEMENT_KEYS = ("status", "feedbackId", "requestId")
|
|
31
32
|
REPO_ROOT = Path(__file__).resolve().parents[2]
|
|
32
33
|
|
|
@@ -45,10 +46,25 @@ def project_fingerprint(project: Path) -> str:
|
|
|
45
46
|
|
|
46
47
|
|
|
47
48
|
def installed_version(project: Path) -> str:
|
|
48
|
-
"""Resolve the exact runtime version
|
|
49
|
+
"""Resolve the exact runtime version, preferring the running package."""
|
|
49
50
|
if os.environ.get("MAGGIE_VERSION"):
|
|
50
51
|
value = safe_text(os.environ["MAGGIE_VERSION"])
|
|
51
52
|
return value if VERSION_RE.fullmatch(value) else "unknown"
|
|
53
|
+
|
|
54
|
+
# A consumer project can retain an old .maggie/install.json after npm
|
|
55
|
+
# upgrade. The package actually being executed is stronger evidence.
|
|
56
|
+
package_versions = []
|
|
57
|
+
for parent in (project.resolve(), *project.resolve().parents):
|
|
58
|
+
package_path = parent / "node_modules" / "@topy-ai" / "maggie" / "package.json"
|
|
59
|
+
try:
|
|
60
|
+
value = json.loads(package_path.read_text(encoding="utf-8")).get("version", "")
|
|
61
|
+
if VERSION_RE.fullmatch(str(value)):
|
|
62
|
+
package_versions.append(str(value))
|
|
63
|
+
except (OSError, ValueError, json.JSONDecodeError, AttributeError):
|
|
64
|
+
continue
|
|
65
|
+
if package_versions:
|
|
66
|
+
return package_versions[0] if len(set(package_versions)) == 1 else "unknown"
|
|
67
|
+
|
|
52
68
|
install_path = project / ".maggie" / "install.json"
|
|
53
69
|
try:
|
|
54
70
|
value = json.loads(install_path.read_text(encoding="utf-8")).get("version", "")
|
|
@@ -102,6 +118,20 @@ def collect(args: argparse.Namespace) -> int:
|
|
|
102
118
|
validation = safe_text(args.validation or report.get("validation"))
|
|
103
119
|
if fixed and (not resolution or not validation):
|
|
104
120
|
raise ValueError("fixed feedback requires non-empty --resolution and --validation")
|
|
121
|
+
batch_id = safe_text(getattr(args, "batch_id", ""))
|
|
122
|
+
batch_index = getattr(args, "batch_index", None)
|
|
123
|
+
batch_size = getattr(args, "batch_size", None)
|
|
124
|
+
if batch_id:
|
|
125
|
+
if not SAFE_BATCH_ID_RE.fullmatch(batch_id):
|
|
126
|
+
raise ValueError("batch ID is invalid")
|
|
127
|
+
if not isinstance(batch_index, int) or isinstance(batch_index, bool) or batch_index < 0:
|
|
128
|
+
raise ValueError("batch index is required when batch ID is provided")
|
|
129
|
+
if not isinstance(batch_size, int) or isinstance(batch_size, bool) or batch_size < 1 or batch_index >= batch_size:
|
|
130
|
+
raise ValueError("batch size must be positive and greater than batch index")
|
|
131
|
+
priority = safe_text(getattr(args, "priority", "") or "normal").lower()
|
|
132
|
+
if priority not in {"low", "normal", "high", "critical"}:
|
|
133
|
+
raise ValueError("priority must be low, normal, high, or critical")
|
|
134
|
+
affected_cli = safe_text(getattr(args, "affected_cli", ""))
|
|
105
135
|
context = {"projectFingerprint": project_fingerprint(project)}
|
|
106
136
|
if args.allow_project_context and args.context_note:
|
|
107
137
|
context["note"] = safe_text(args.context_note)
|
|
@@ -124,12 +154,16 @@ def collect(args: argparse.Namespace) -> int:
|
|
|
124
154
|
"fixed": fixed,
|
|
125
155
|
"resolution": resolution,
|
|
126
156
|
"validation": validation,
|
|
157
|
+
"priority": priority,
|
|
158
|
+
"affectedCli": affected_cli,
|
|
127
159
|
"attachments": [attachment(value) for value in args.screenshot],
|
|
128
160
|
"environment": {"os": platform.system().lower(), "python": platform.python_version()},
|
|
129
161
|
"privacy": {"secretsRedacted": True, "projectContextAllowed": bool(args.allow_project_context)},
|
|
130
162
|
"projectContext": context if args.allow_project_context else None,
|
|
131
163
|
"submission": {"status": "draft"},
|
|
132
164
|
}
|
|
165
|
+
if batch_id:
|
|
166
|
+
data["batch"] = {"batchId": batch_id, "index": batch_index, "size": batch_size}
|
|
133
167
|
directory = project / ".maggie" / "feedback"
|
|
134
168
|
directory.mkdir(parents=True, exist_ok=True)
|
|
135
169
|
path = directory / f"{data['feedbackId']}.json"
|
|
@@ -144,7 +178,11 @@ def collect(args: argparse.Namespace) -> int:
|
|
|
144
178
|
|
|
145
179
|
|
|
146
180
|
def to_markdown(data: dict) -> str:
|
|
147
|
-
|
|
181
|
+
batch = data.get("batch") or {}
|
|
182
|
+
lines = [f"# Maggie feedback: {data.get('summary') or data.get('type')}", "", "<!-- Generated by maggie feedback. Secrets and local paths are omitted. -->", "", "## Run", "", f"- Skill: `{data.get('skill')}`", f"- Maggie version: `{data.get('maggieVersion')}`", f"- Run ID: `{data.get('runId') or 'not provided'}`", f"- Phase: `{data.get('phase') or 'not provided'}`", f"- Status: `{data.get('status')}`", f"- Priority: `{data.get('priority') or 'normal'}`", f"- Affected CLI: `{data.get('affectedCli') or 'not provided'}`", f"- Error fingerprint: `{data.get('errorFingerprint') or 'not provided'}`"]
|
|
183
|
+
if batch:
|
|
184
|
+
lines.append(f"- Batch: `{batch.get('batchId')}` ({batch.get('index', 0) + 1}/{batch.get('size')})")
|
|
185
|
+
lines += ["", "## Report", "", f"**Summary:** {data.get('summary') or 'Not provided'}", "", f"**Expected:** {data.get('expected') or 'Not provided'}", "", f"**Actual:** {data.get('actual') or 'Not provided'}", ""]
|
|
148
186
|
if data.get("stepsToReproduce"):
|
|
149
187
|
lines += ["**Steps to reproduce:**", ""] + [f"{index}. {step}" for index, step in enumerate(data["stepsToReproduce"], 1)] + [""]
|
|
150
188
|
lines += [f"**Fixed:** {'yes' if data.get('fixed') else 'no'}", f"**Resolution:** {data.get('resolution') or 'Not provided'}", f"**Validation:** {data.get('validation') or 'Not provided'}", "", "## Privacy", "", f"- Project context allowed: `{bool(data.get('privacy', {}).get('projectContextAllowed'))}`", f"- Attachments: `{len(data.get('attachments', []))}` metadata-only reference(s)", ""]
|
|
@@ -211,6 +249,57 @@ def list_feedback(args: argparse.Namespace) -> int:
|
|
|
211
249
|
return 0
|
|
212
250
|
|
|
213
251
|
|
|
252
|
+
def collect_batch(args: argparse.Namespace) -> int:
|
|
253
|
+
"""Create one privacy-safe draft per item while retaining shared batch context."""
|
|
254
|
+
manifest = load_json(Path(args.batch_file).expanduser().resolve())
|
|
255
|
+
batch_id = safe_text(manifest.get("batchId"))
|
|
256
|
+
if not SAFE_BATCH_ID_RE.fullmatch(batch_id):
|
|
257
|
+
raise ValueError("batch file requires a safe batchId")
|
|
258
|
+
items = manifest.get("items")
|
|
259
|
+
if not isinstance(items, list) or not items or len(items) > 50:
|
|
260
|
+
raise ValueError("batch file requires between 1 and 50 items")
|
|
261
|
+
shared = {
|
|
262
|
+
"skill": safe_text(manifest.get("skill")),
|
|
263
|
+
"run_id": safe_text(manifest.get("runId")),
|
|
264
|
+
"phase": safe_text(manifest.get("phase")),
|
|
265
|
+
"priority": safe_text(manifest.get("priority") or "normal"),
|
|
266
|
+
"affected_cli": safe_text(manifest.get("affectedCli")),
|
|
267
|
+
}
|
|
268
|
+
paths = []
|
|
269
|
+
for index, item in enumerate(items):
|
|
270
|
+
if not isinstance(item, dict):
|
|
271
|
+
raise ValueError(f"items[{index}] must be an object")
|
|
272
|
+
values = {
|
|
273
|
+
"project": args.project,
|
|
274
|
+
"run_report": None,
|
|
275
|
+
"skill": safe_text(item.get("skill") or shared["skill"]),
|
|
276
|
+
"run_id": safe_text(item.get("runId") or shared["run_id"]),
|
|
277
|
+
"type": item.get("type", "bug"),
|
|
278
|
+
"phase": safe_text(item.get("phase") or shared["phase"]),
|
|
279
|
+
"summary": item.get("summary", ""),
|
|
280
|
+
"expected": item.get("expected", ""),
|
|
281
|
+
"actual": item.get("actual", ""),
|
|
282
|
+
"error_fingerprint": item.get("errorFingerprint", ""),
|
|
283
|
+
"reproduce": item.get("stepsToReproduce", []),
|
|
284
|
+
"fixed": item.get("fixed") is True,
|
|
285
|
+
"resolution": item.get("resolution", ""),
|
|
286
|
+
"validation": item.get("validation", ""),
|
|
287
|
+
"screenshot": item.get("screenshots", []),
|
|
288
|
+
"allow_project_context": False,
|
|
289
|
+
"context_note": "",
|
|
290
|
+
"batch_id": batch_id,
|
|
291
|
+
"batch_index": index,
|
|
292
|
+
"batch_size": len(items),
|
|
293
|
+
"priority": item.get("priority") or shared["priority"],
|
|
294
|
+
"affected_cli": item.get("affectedCli") or shared["affected_cli"],
|
|
295
|
+
}
|
|
296
|
+
if not isinstance(values["reproduce"], list): values["reproduce"] = []
|
|
297
|
+
if not isinstance(values["screenshot"], list): values["screenshot"] = []
|
|
298
|
+
paths.append(collect(argparse.Namespace(**values)))
|
|
299
|
+
print(json.dumps({"batchId": batch_id, "count": len(paths), "status": "drafts-created"}, ensure_ascii=False))
|
|
300
|
+
return 0
|
|
301
|
+
|
|
302
|
+
|
|
214
303
|
def main() -> int:
|
|
215
304
|
parser = argparse.ArgumentParser(description=__doc__)
|
|
216
305
|
parser.add_argument("--project", default=".")
|
|
@@ -235,6 +324,14 @@ def main() -> int:
|
|
|
235
324
|
collect_parser.add_argument("--screenshot", action="append", default=[])
|
|
236
325
|
collect_parser.add_argument("--allow-project-context", action="store_true")
|
|
237
326
|
collect_parser.add_argument("--context-note", default="")
|
|
327
|
+
collect_parser.add_argument("--batch-id", default="")
|
|
328
|
+
collect_parser.add_argument("--batch-index", type=int)
|
|
329
|
+
collect_parser.add_argument("--batch-size", type=int)
|
|
330
|
+
collect_parser.add_argument("--priority", choices=("low", "normal", "high", "critical"), default="normal")
|
|
331
|
+
collect_parser.add_argument("--affected-cli", default="")
|
|
332
|
+
batch_parser = sub.add_parser("batch")
|
|
333
|
+
batch_parser.add_argument("--project", default=argparse.SUPPRESS)
|
|
334
|
+
batch_parser.add_argument("--batch-file", required=True)
|
|
238
335
|
preview_parser = sub.add_parser("preview")
|
|
239
336
|
preview_parser.add_argument("feedback")
|
|
240
337
|
preview_parser.add_argument("--format", choices=("json", "markdown"), default="json")
|
|
@@ -246,6 +343,7 @@ def main() -> int:
|
|
|
246
343
|
args = parser.parse_args()
|
|
247
344
|
try:
|
|
248
345
|
if args.command == "collect": return collect(args)
|
|
346
|
+
if args.command == "batch": return collect_batch(args)
|
|
249
347
|
if args.command == "preview": return preview(args)
|
|
250
348
|
if args.command == "submit": return submit(args)
|
|
251
349
|
return list_feedback(args)
|