@topy-ai/maggie 0.7.51 → 0.7.52

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-zh-TW.md CHANGED
@@ -29,7 +29,7 @@ npx @topy-ai/maggie init --scope project --agent all
29
29
  npx @topy-ai/maggie doctor --scope project --project .
30
30
  ```
31
31
 
32
- 目前 npm 版本為 `0.7.51`,完成 batch 28:17 條 hosted feedback,來自一個跨平台產品開發 session(同一套 Expo 程式碼支援 web、iOS、Android,加上具備 MCP 與 ChatGPT/Claude app 的 AI 助理、產品自己的 CLI/MCP 套件、第三方 skills catalog 以及多租戶 assistant bridge host)。`maggie design app-validate` 改讀每個 viewport 的 evidence(scroll owner、固定 header/footer),只有 `app-init --pane list-detail:1024:2` 宣告的多欄版面才允許多個 scroll owner,並以 PNG 實際尺寸對照 plan viewport;`app-init --capabilities none|camera,…`(或 `--camera none`)讓非相機頁面不必填相機狀態,`--pattern` 加入新的 app UI patterns 驗收規則。新增 `maggie design app-collect`:在同一個瀏覽器分頁逐一以真實尺寸渲染已登入的 app 路由、略過 React Native Web 的包裝層計算 scroll owner、證明固定 shell,並寫出 `runtime-evidence.json` 與截圖(可 `--cdp-url` 接上已登入的 Chrome,或用專用 QA profile,`--read-only` 阻擋寫入)。新增 `maggie qa app-preflight`:偵測以 `CI=1` 或從錯誤資料夾啟動的 Metro/Expo dev server,並對會輪替的 refresh token 與連到 production API 的本機 server 發出警告。新增 `maggie-mobile-app` skill 與 `maggie mobile preflight|plan`(EAS build profile、TestFlight / Play 內部測試、EAS Update channel 與 fingerprint runtime policy、上架審核規則、建置前成本提示;`--auto-submit` 缺 `ascAppId` 或 `appVersion` policy 配上 remote build number 時在花費額度前就失敗)。新增 `maggie-assistant-tools` skill 與 `maggie assistant`(單一 tool registry、風險等級與 gate、與 model description 分開的使用者文案、由風險推導 MCP annotations、自動產生「助理能做與不能做」頁面、audit 檢查與簽章一次性 approval token 參考實作)。新增 `maggie product release-check|smoke|mcp-check|review-status`(產品 npm CLI/MCP 套件 metadata、`server.json`/`mcpName`、打包檔案機密掃描、安裝後 binary smoke、OAuth protected-resource metadata、ChatGPT/Claude/商店審核追蹤)、`maggie skills scaffold|validate|package|smoke`(第三方 skills catalog 打包成分類 plugin、all-in-one plugin 與 Claude Code `marketplace.json`)、`maggie deployment bridge-plan|bridge-collect-script|bridge-check`(多租戶 bridge host);`maggie feedback session-summary` 支援多個 repo 與一般句子形式的 commit。MaggieDash 0.2.12 從 ORA host 上游通用修正。前一版 `0.7.50`:`maggie update` 改為 idempotent,內容已與套件相同的檔案不再改寫(保留時間戳),摘要分別列出 changed/unchanged/preserved 數量,只有真的保留了本機修改時才顯示 preserved 提示。前一版 `0.7.49` 完成 batch 27:33 條 2026-09-09 至 09-16 建立但從未提交的本機 feedback drafts,已逐條對照目前程式碼(其中兩條已被既有功能滿足)。新增離線 `maggie ops notifications`(通知 catalogue 合約、consent/retention/durable lead delivery 規則、provider template drift、只檢查變數名稱的 Resend preflight、queue health 門檻與 operator alerts、dry-run retention plan);`maggie localization email-matrix` 與 `maggie localization lifecycle`(locale 優先順序、未翻譯/缺漏 locale、變數與 HTML/text 一致、market CTA route、consent-aware 行銷 email 預設不發送);Deployment 的 `--keep-releases` 統一 retention policy、`auth-canary`、資料庫 owner 無法讀取 release 路徑時的 streamed migration、`collect` 單一 readiness report;Google failure remediation(`maggie ops google-diagnose`)與唯讀 `google-ads-preflight`/`google-ads-keyword-ideas`;Social Share `reconcile`(timeout 後不重複發文)、`maggie-marketing-run-v1` 合約、命名排程層與只管制對外發布的 regulated-content review;`maggie design icon-glyph-check`、`maggie design verify`、`maggie service import-stages-validate`/`import-readiness`/`fact-provenance`;以及 `--release-metadata` 版本一致性檢查、`maggie docs rollout-status` 與 stdin batch 輸入。前一版 `0.7.48` 處理 2026-10-10 的 27 條 hosted feedback(batch 26):Social Share 新增 `adapters`(每個 provider 一組 canonical OAuth scope,LinkedIn 不同 scope 會撤銷舊 token;共用 OAuth client 警告)、`connections`(撤銷或無法解密的 token 標為 `needs_reconnect`,偵測會過期的簽名頭像 URL)、`queue`(slot-based 排程:not-before、最新優先、campaign 輪流與每日上限、DST-safe)與 `campaigns`(idea stage Start gate);Deployment 新增 hash-only `secret-fingerprint`/`secret-parity`、`env-isolation`(本機 `.env` 指向 production database 時警告)、release retention 與 worker-before-enqueue 發布順序;QA 在未明確同意時阻擋對 production API 的寫入情境,並以 `maggie qa cors-preflight` 只用 OPTIONS 檢查每個 HTTP method;Design 新增 `pressable-check`、`layer-check`(Tailwind v4 unlayered CSS)、onboarding wizard/picker 模式與 `maggie-palette-roles-v1` palette generate/validate;Marketplace 加入 screenshot 完整性與像素預算、從 CSS 量測 palette、asset provenance、中性 `displayName`(全部模板已補齊)、template studio、`maggie-template-content-v1` 合約與圖片生成成本/無文字審查 gate;Booking 新增唯讀 Stripe live setup runbook 與 gift voucher lifecycle 合約(`maggie booking voucher-contract --ledger`);Feedback 新增 `improvement` 類型與 `maggie feedback session-summary`。MaggieDash 0.2.11 會保留尚未部署 template 的 outbox email 而不是 dead-letter,並從 ORA host 上游通用修正。本版也包含 Booking 十個 email events 合約與 CLI validator 同步及安全輸出的 `arrivalNote`。前一版 `0.7.47` 處理 2026-09-26 五條新 feedback:project-scope
32
+ 目前 npm 版本為 `0.7.52`:加入三項 mobile lane 後續修正——`maggie mobile preflight` 會在 non-interactive `eas update` script 缺少 `--environment` 時失敗,並在 build 後修改 `eas.json` 可能改變 fingerprint(OTA 更新會無聲地送不到該 build)時警告;`maggie mobile plan` 一律帶 `--environment`,每次 update 前先跑 `eas fingerprint:compare`;新增 `maggie design safe-area-check`,標出 iOS modal route 與 React Native `<Modal>` 內的 `<SafeAreaView>`(可能量到 0 的 top inset)。前一版 `0.7.51` 完成 batch 28:17 條 hosted feedback,來自一個跨平台產品開發 session(同一套 Expo 程式碼支援 web、iOS、Android,加上具備 MCP 與 ChatGPT/Claude app 的 AI 助理、產品自己的 CLI/MCP 套件、第三方 skills catalog 以及多租戶 assistant bridge host)。`maggie design app-validate` 改讀每個 viewport 的 evidence(scroll owner、固定 header/footer),只有 `app-init --pane list-detail:1024:2` 宣告的多欄版面才允許多個 scroll owner,並以 PNG 實際尺寸對照 plan viewport;`app-init --capabilities none|camera,…`(或 `--camera none`)讓非相機頁面不必填相機狀態,`--pattern` 加入新的 app UI patterns 驗收規則。新增 `maggie design app-collect`:在同一個瀏覽器分頁逐一以真實尺寸渲染已登入的 app 路由、略過 React Native Web 的包裝層計算 scroll owner、證明固定 shell,並寫出 `runtime-evidence.json` 與截圖(可 `--cdp-url` 接上已登入的 Chrome,或用專用 QA profile,`--read-only` 阻擋寫入)。新增 `maggie qa app-preflight`:偵測以 `CI=1` 或從錯誤資料夾啟動的 Metro/Expo dev server,並對會輪替的 refresh token 與連到 production API 的本機 server 發出警告。新增 `maggie-mobile-app` skill 與 `maggie mobile preflight|plan`(EAS build profile、TestFlight / Play 內部測試、EAS Update channel 與 fingerprint runtime policy、上架審核規則、建置前成本提示;`--auto-submit` 缺 `ascAppId` 或 `appVersion` policy 配上 remote build number 時在花費額度前就失敗)。新增 `maggie-assistant-tools` skill 與 `maggie assistant`(單一 tool registry、風險等級與 gate、與 model description 分開的使用者文案、由風險推導 MCP annotations、自動產生「助理能做與不能做」頁面、audit 檢查與簽章一次性 approval token 參考實作)。新增 `maggie product release-check|smoke|mcp-check|review-status`(產品 npm CLI/MCP 套件 metadata、`server.json`/`mcpName`、打包檔案機密掃描、安裝後 binary smoke、OAuth protected-resource metadata、ChatGPT/Claude/商店審核追蹤)、`maggie skills scaffold|validate|package|smoke`(第三方 skills catalog 打包成分類 plugin、all-in-one plugin 與 Claude Code `marketplace.json`)、`maggie deployment bridge-plan|bridge-collect-script|bridge-check`(多租戶 bridge host);`maggie feedback session-summary` 支援多個 repo 與一般句子形式的 commit。MaggieDash 0.2.12 從 ORA host 上游通用修正。前一版 `0.7.50`:`maggie update` 改為 idempotent,內容已與套件相同的檔案不再改寫(保留時間戳),摘要分別列出 changed/unchanged/preserved 數量,只有真的保留了本機修改時才顯示 preserved 提示。前一版 `0.7.49` 完成 batch 27:33 條 2026-09-09 至 09-16 建立但從未提交的本機 feedback drafts,已逐條對照目前程式碼(其中兩條已被既有功能滿足)。新增離線 `maggie ops notifications`(通知 catalogue 合約、consent/retention/durable lead delivery 規則、provider template drift、只檢查變數名稱的 Resend preflight、queue health 門檻與 operator alerts、dry-run retention plan);`maggie localization email-matrix` 與 `maggie localization lifecycle`(locale 優先順序、未翻譯/缺漏 locale、變數與 HTML/text 一致、market CTA route、consent-aware 行銷 email 預設不發送);Deployment 的 `--keep-releases` 統一 retention policy、`auth-canary`、資料庫 owner 無法讀取 release 路徑時的 streamed migration、`collect` 單一 readiness report;Google failure remediation(`maggie ops google-diagnose`)與唯讀 `google-ads-preflight`/`google-ads-keyword-ideas`;Social Share `reconcile`(timeout 後不重複發文)、`maggie-marketing-run-v1` 合約、命名排程層與只管制對外發布的 regulated-content review;`maggie design icon-glyph-check`、`maggie design verify`、`maggie service import-stages-validate`/`import-readiness`/`fact-provenance`;以及 `--release-metadata` 版本一致性檢查、`maggie docs rollout-status` 與 stdin batch 輸入。前一版 `0.7.48` 處理 2026-10-10 的 27 條 hosted feedback(batch 26):Social Share 新增 `adapters`(每個 provider 一組 canonical OAuth scope,LinkedIn 不同 scope 會撤銷舊 token;共用 OAuth client 警告)、`connections`(撤銷或無法解密的 token 標為 `needs_reconnect`,偵測會過期的簽名頭像 URL)、`queue`(slot-based 排程:not-before、最新優先、campaign 輪流與每日上限、DST-safe)與 `campaigns`(idea stage Start gate);Deployment 新增 hash-only `secret-fingerprint`/`secret-parity`、`env-isolation`(本機 `.env` 指向 production database 時警告)、release retention 與 worker-before-enqueue 發布順序;QA 在未明確同意時阻擋對 production API 的寫入情境,並以 `maggie qa cors-preflight` 只用 OPTIONS 檢查每個 HTTP method;Design 新增 `pressable-check`、`layer-check`(Tailwind v4 unlayered CSS)、onboarding wizard/picker 模式與 `maggie-palette-roles-v1` palette generate/validate;Marketplace 加入 screenshot 完整性與像素預算、從 CSS 量測 palette、asset provenance、中性 `displayName`(全部模板已補齊)、template studio、`maggie-template-content-v1` 合約與圖片生成成本/無文字審查 gate;Booking 新增唯讀 Stripe live setup runbook 與 gift voucher lifecycle 合約(`maggie booking voucher-contract --ledger`);Feedback 新增 `improvement` 類型與 `maggie feedback session-summary`。MaggieDash 0.2.11 會保留尚未部署 template 的 outbox email 而不是 dead-letter,並從 ORA host 上游通用修正。本版也包含 Booking 十個 email events 合約與 CLI validator 同步及安全輸出的 `arrivalNote`。前一版 `0.7.47` 處理 2026-09-26 五條新 feedback:project-scope
33
33
  `maggie init` 會建立本地 memory store;context 預設只讀取本 project scope;memory hook
34
34
  在 local Python adapter 不存在時 fallback 到已安裝 CLI;重複 init 保留既有檔案與時間戳;
35
35
  `maggie feedback list` 的 `--project` 可放在子命令前或後。Generated VPS runner 現在記錄
@@ -113,7 +113,7 @@ structure drift 或重複 treatment product 時 fail closed。
113
113
  deployment、memory、feedback 和 MaggieDash。內容先 draft/review,外部寫入、
114
114
  publish 與 production deployment 需要明確確認。
115
115
 
116
- 目前 npm 版本為 `0.7.51`,包含 batch 28 產品 app、assistant tools、mobile、product release、skills catalog 與 bridge host 功能、batch 26、27 feedback 修正與 idempotent `maggie update`與 Booking email-template 合約/CLI validator 同步。本版完成 2026-09-24 booking feedback batch:Calendar grid/free-slot entry 與 host modal focus-restoration evidence。本版新增 Starter marketplace provisioning plan;本版完成 feedback batches 22、23:加入 declarative browser interaction evidence、design markup/style/content-key gate、compiler source anchoring、entity double-escape protection、`update --help` read-only、noindex hypothetical indexability、可過期 baseline acknowledgement、DOM-only link extraction regression,以及 MaggieDash 0.2.10 的 content override、短期 browser session、CDN-safe public-session cache contracts。authentication 和 isolated dev server 仍由 host 負責。`maggie-booking` workflow、傳統 email/password、Stripe、Resend、public booking flow 及既有安全邊界維持不變;provider-bound video generation 與 transactional delivery 仍記錄在 issue #39,前一批延後功能仍在 issue #38,不會直接成為 active memory。0.7.33 記錄並審查一筆更正回饋;確認 0.7.32 已正確處理 batch index,沒有修改已驗證的 shared behavior。0.7.32 加入 feedback 一基索引支援、MaggieDash panel 的 source/freshness/error evidence、Consent Mode 與實際 write/no-op reconciliation、SEO baseline recapture、sitemap origin rebasing、content-diff scope、migration ledger reconciliation、deployment credential preflight、release provenance/runtime preflight、icon release gate、opt-in blog auto-publish gate,以及 least-privilege VPS deployer。也修復 top-level `maggie feedback` dispatcher parity。0.7.31 加入 update 差異摘要、atomic installer copy、subset install manifest merge,以及 inventory drift 時 doctor non-zero。0.7.30 補上 feedback tracking issue closeout gate 與 batch 14/15 audit ledger。0.7.29 修正 npm package landing page 使用英文 README;0.7.28 加入 versioned Gemini model policy、明確 fallback 與 provenance、bundled-first CLI
116
+ 目前 npm 版本為 `0.7.52`,包含 batch 28 產品 app、assistant tools、mobile、product release、skills catalog 與 bridge host 功能、batch 26、27 feedback 修正與 idempotent `maggie update`與 Booking email-template 合約/CLI validator 同步。本版完成 2026-09-24 booking feedback batch:Calendar grid/free-slot entry 與 host modal focus-restoration evidence。本版新增 Starter marketplace provisioning plan;本版完成 feedback batches 22、23:加入 declarative browser interaction evidence、design markup/style/content-key gate、compiler source anchoring、entity double-escape protection、`update --help` read-only、noindex hypothetical indexability、可過期 baseline acknowledgement、DOM-only link extraction regression,以及 MaggieDash 0.2.10 的 content override、短期 browser session、CDN-safe public-session cache contracts。authentication 和 isolated dev server 仍由 host 負責。`maggie-booking` workflow、傳統 email/password、Stripe、Resend、public booking flow 及既有安全邊界維持不變;provider-bound video generation 與 transactional delivery 仍記錄在 issue #39,前一批延後功能仍在 issue #38,不會直接成為 active memory。0.7.33 記錄並審查一筆更正回饋;確認 0.7.32 已正確處理 batch index,沒有修改已驗證的 shared behavior。0.7.32 加入 feedback 一基索引支援、MaggieDash panel 的 source/freshness/error evidence、Consent Mode 與實際 write/no-op reconciliation、SEO baseline recapture、sitemap origin rebasing、content-diff scope、migration ledger reconciliation、deployment credential preflight、release provenance/runtime preflight、icon release gate、opt-in blog auto-publish gate,以及 least-privilege VPS deployer。也修復 top-level `maggie feedback` dispatcher parity。0.7.31 加入 update 差異摘要、atomic installer copy、subset install manifest merge,以及 inventory drift 時 doctor non-zero。0.7.30 補上 feedback tracking issue closeout gate 與 batch 14/15 audit ledger。0.7.29 修正 npm package landing page 使用英文 README;0.7.28 加入 versioned Gemini model policy、明確 fallback 與 provenance、bundled-first CLI
117
117
  dispatch、`maggie --version`、feedback batch review 聚合與重複偵測、marketplace enrichment evidence、booking
118
118
  worker/resolver evidence,以及 read-only migration preflight。它也包含 host-owned mobile app surface contract、signed-in
119
119
  camera-state QA、直接 Astro route resolution、correlated feedback batch,以及
package/README.md CHANGED
@@ -89,7 +89,16 @@ Maggie keeps the existing project foundation and asks for decisions before
89
89
  shared routes, analytics, or publishing boundaries change. The current
90
90
  package ships 22 installable skills and a local-first MaggieDash foundation.
91
91
 
92
- This package is `0.7.51`. It closes batch 28: 17 hosted feedback
92
+ This package is `0.7.52`. It adds three follow-ups to the mobile
93
+ lane that arrived during the 0.7.51 release: `maggie mobile preflight` fails
94
+ on non-interactive `eas update` scripts without `--environment` and warns
95
+ when `eas.json` can change the fingerprint after a build (OTA updates would
96
+ silently stop reaching it); `maggie mobile plan` always sets `--environment`
97
+ and gates every update on `eas fingerprint:compare`; and the new
98
+ `maggie design safe-area-check` flags `<SafeAreaView>` inside iOS modal routes
99
+ and React Native `<Modal>`, where it can measure a 0 top inset.
100
+
101
+ The 0.7.51 release closed batch 28: 17 hosted feedback
93
102
  records from a cross-platform product session (one Expo codebase for web,
94
103
  iOS and Android, an assistant with MCP and ChatGPT/Claude app surfaces,
95
104
  product CLI/MCP packages, a third-party skills catalog, and a multi-tenant
@@ -778,8 +787,8 @@ artifact schemas.
778
787
  Recommended upgrade sequence for the current release:
779
788
 
780
789
  ```bash
781
- npx @topy-ai/maggie@0.7.51 update --scope project --project . --force
782
- npx @topy-ai/maggie@0.7.51 cleanup --scope project --project .
790
+ npx @topy-ai/maggie@0.7.52 update --scope project --project . --force
791
+ npx @topy-ai/maggie@0.7.52 cleanup --scope project --project .
783
792
  ```
784
793
 
785
794
  Maintainers should pass npm credentials through the repository helper, never
package/bin/maggie.js CHANGED
@@ -99,7 +99,7 @@ Usage:
99
99
  maggie dash structure-validate --model FILE --evidence FILE
100
100
  maggie design progressive-check|css-check|css-cascade-check|style-check --evidence FILE
101
101
  maggie design dash-init|dash-validate|sample-init|sample-validate [options]
102
- maggie design pressable-check|layer-check --project PATH
102
+ maggie design pressable-check|layer-check|safe-area-check --project PATH
103
103
  maggie design palette generate --canvas HEX --text HEX --accent HEX | palette validate --palette FILE
104
104
  maggie seo privacy-check --evidence FILE
105
105
  maggie content FILE --source PROVIDER --project PATH --confirm
@@ -370,6 +370,19 @@ when `fullPage: true`). A top-level `shell.scrollOwnerCount` is ignored with a
370
370
  warning, and a signature-only or scaled screenshot fails. The machine-readable
371
371
  contract is
372
372
  [mobile-app-surface-v1.schema.json](../../bundled-contracts/maggie-design/mobile-app-surface-v1.schema.json).
373
+ Native iOS modals need one more check. `<SafeAreaView>` inside an Expo Router
374
+ modal route (`presentation: "transparentModal"`, `"modal"`, …) or a React
375
+ Native `<Modal>` can measure a 0 top inset on iOS, putting the header and
376
+ Close under the notch or Dynamic Island; web screenshots at 390/768/1440
377
+ cannot show it:
378
+
379
+ maggie design safe-area-check --project apps/app
380
+
381
+ It flags those uses; pad with `useSafeAreaInsets()` from the root
382
+ `SafeAreaProvider` instead (`// maggie-allow-safe-area-view` marks a reviewed
383
+ exception). `app-init` adds an `ios-safe-area` scenario that needs a notched
384
+ device or simulator screenshot.
385
+
373
386
  Keep `maggie design pressable-check` in the same loop for React Native Web
374
387
  apps: together they caught nested pressables and shell regressions during
375
388
  repeated sheet and modal changes.
@@ -58,6 +58,11 @@ with `--config-json` when the config is dynamic), `eas.json` and
58
58
  future binary would accept any JS update, including one that needs a new
59
59
  native module. Use one top-level `{"policy": "fingerprint"}`.
60
60
 
61
+ It also fails when a `package.json` script runs `eas update --non-interactive`
62
+ without `--environment` (EAS refuses it), and warns when an update script has
63
+ no `eas fingerprint:compare` gate or when `eas.json` is not in
64
+ `.fingerprintignore` under the fingerprint policy.
65
+
61
66
  It warns about missing export-compliance (`usesNonExemptEncryption`), an
62
67
  unpinned `cli.version`, per-platform-only runtime versions (what
63
68
  `eas update:configure` writes) and tab-indented `app.json` that
@@ -88,10 +93,22 @@ prints the exact commands with the cost notice and runs nothing:
88
93
  → TestFlight (iOS) or the Play internal track (Android). Add testers in
89
94
  App Store Connect / Play Console; internal testing needs no review.
90
95
  2. Without `--auto-submit`: `npx eas-cli submit -p ios --profile production --latest`.
91
- 3. JS-only fixes: `npx eas-cli update --channel production --platform ios --message "…"`.
92
- A native change (new module, permission, SDK upgrade) needs a new build;
93
- the fingerprint policy stops an incompatible update from reaching old
94
- binaries.
96
+ 3. JS-only fixes: first `npx eas-cli fingerprint:compare --build-id <installed build>`
97
+ — it must report no difference — then
98
+ `npx eas-cli update --channel production --environment production --platform ios --non-interactive --message "…"`.
99
+ `--environment` is mandatory in non-interactive mode. A native change (new
100
+ module, permission, SDK upgrade) needs a new build; the fingerprint policy
101
+ stops an incompatible update from reaching old binaries.
102
+
103
+ ### The fingerprint includes `eas.json`
104
+
105
+ Editing `eas.json` after a build — even only adding `submit.ios.ascAppId` —
106
+ changes the local fingerprint. An update published from that tree targets a
107
+ runtime no installed build has, and EAS reports no error. Run
108
+ `fingerprint:compare` against the target build before every update; if only
109
+ `eas.json` differs, publish from the build-time `eas.json` (restore it, publish,
110
+ restore the current file) and add `eas.json` to `.fingerprintignore` with the
111
+ next native build so it stops affecting the runtime.
95
112
 
96
113
  Show the plan's `costNotice` and wait for confirmation before step 1.
97
114
 
@@ -356,6 +356,7 @@ def app_surface_init(project: Path, route: str, auth_mode: str, force: bool, con
356
356
  for name in capabilities:
357
357
  if name != "camera":
358
358
  scenarios.append({"id": f"{name}-permission", "viewport": "mobile", "assertions": [f"{name} permission is explained before the system prompt", "denied permission leaves an actionable fallback"]})
359
+ scenarios.append({"id": "ios-safe-area", "viewport": "notched iPhone (device or simulator)", "assertions": ["header and Close sit below the status bar / Dynamic Island in every modal and sheet", "insets come from useSafeAreaInsets(), not SafeAreaView inside a modal (`maggie design safe-area-check`)", "web screenshots cannot prove this; record a device screenshot"]})
359
360
  for pattern in patterns:
360
361
  scenarios.append({"id": f"pattern-{pattern}", "viewport": "all", "assertions": APP_PATTERN_ACCEPTANCE[pattern]})
361
362
  if auth_mode != "none":
@@ -1003,6 +1004,7 @@ commands:
1003
1004
  markup-check validate optional style attributes, content keys, and entities
1004
1005
  pressable-check flag pressables/buttons nested in another pressable (JSX/TSX)
1005
1006
  layer-check flag unlayered CSS that overrides Tailwind v4 utilities
1007
+ safe-area-check flag SafeAreaView inside iOS modal routes / RN Modal
1006
1008
  palette generate or validate a semantic palette role contract
1007
1009
  verify run automatable design-job steps and record evidence
1008
1010
  icon-glyph-check render icons and prove font load and glyph paint
@@ -1010,7 +1012,7 @@ commands:
1010
1012
  reference-ui, init, validate-ui, app-init, app-validate, rebrand, review
1011
1013
  """)
1012
1014
  return 0
1013
- if len(sys.argv) > 1 and sys.argv[1] in {"pressable-check", "layer-check"}:
1015
+ if len(sys.argv) > 1 and sys.argv[1] in {"pressable-check", "layer-check", "safe-area-check"}:
1014
1016
  from maggie_design_lint import main as lint_main
1015
1017
  return lint_main(sys.argv[1:])
1016
1018
  if len(sys.argv) > 1 and sys.argv[1] == "verify":
@@ -12,6 +12,13 @@ Tailwind v4 emits utilities inside ``@layer utilities``; any unlayered rule
12
12
  wins over every layer regardless of specificity, so an unlayered
13
13
  ``h1 { font-family }`` beats ``font-*`` and ``.ph { display: inline-block }``
14
14
  beats ``hidden`` and ``group-open:inline``.
15
+
16
+ ``safe-area-check`` flags ``<SafeAreaView>`` used as the inset source inside an
17
+ Expo Router modal route (``presentation: "transparentModal"``, ``"modal"``,
18
+ ``"fullScreenModal"``, ...) or a React Native ``<Modal>``. On iOS those
19
+ surfaces can measure a 0 top inset, so the header and Close button sit under
20
+ the notch or Dynamic Island; web screenshots cannot show it. Read insets with
21
+ ``useSafeAreaInsets()`` from the root ``SafeAreaProvider`` instead.
15
22
  """
16
23
 
17
24
  from __future__ import annotations
@@ -249,6 +256,61 @@ def layer_check(project: Path, paths: list[Path], force_v4: bool) -> dict[str, o
249
256
  return {"schemaVersion": "maggie-design-layer-check.v1", "passed": not errors, "status": "checked", "checkedFiles": len(files), "errors": len(errors), "warnings": len(findings) - len(errors), "findings": findings}
250
257
 
251
258
 
259
+ MODAL_PRESENTATIONS = ("transparentModal", "containedTransparentModal", "modal", "containedModal", "fullScreenModal", "formSheet")
260
+ SAFE_AREA_ALLOW = "maggie-allow-safe-area-view"
261
+ SCREEN_TAG = re.compile(r"<(?:Stack|Tabs|Drawer)\.Screen\b(?:[^<>]|=>|\{[^{}]*(?:\{[^{}]*\}[^{}]*)*\})*?/?>", re.S)
262
+ PRESENTATION = re.compile(r"presentation\s*:\s*[\"'](" + "|".join(MODAL_PRESENTATIONS) + r")[\"']")
263
+
264
+
265
+ def _route_file(layout: Path, name: str) -> Path | None:
266
+ for candidate in (layout.parent / f"{name}.tsx", layout.parent / f"{name}.jsx", layout.parent / name / "index.tsx", layout.parent / name / "index.jsx"):
267
+ if candidate.is_file():
268
+ return candidate
269
+ return None
270
+
271
+
272
+ def _line(source: str, index: int) -> int:
273
+ return source.count("\n", 0, index) + 1
274
+
275
+
276
+ def safe_area_check(project: Path, paths: list[Path]) -> dict[str, object]:
277
+ project = project.resolve()
278
+ files = iter_files(paths or [project], (".tsx", ".jsx"))
279
+ modal_routes: dict[Path, str] = {}
280
+ for path in files:
281
+ source = _strip_jsx_comments(path.read_text(encoding="utf-8", errors="replace"))
282
+ for tag in SCREEN_TAG.finditer(source):
283
+ presentation = PRESENTATION.search(tag.group(0))
284
+ if not presentation:
285
+ continue
286
+ name = re.search(r"\bname\s*=\s*[\"']([^\"']+)[\"']", tag.group(0))
287
+ target = _route_file(path, name.group(1)) if name and path.stem == "_layout" else (path if not name else None)
288
+ if target:
289
+ modal_routes[target.resolve()] = presentation.group(1)
290
+ findings: list[dict[str, object]] = []
291
+ for path in files:
292
+ raw = path.read_text(encoding="utf-8", errors="replace")
293
+ if SAFE_AREA_ALLOW in raw or "SafeAreaView" not in raw:
294
+ continue
295
+ source = _strip_jsx_comments(raw)
296
+ display = _display(path, project)
297
+ presentation = modal_routes.get(path.resolve())
298
+ if presentation:
299
+ match = re.search(r"<SafeAreaView\b", source)
300
+ if match:
301
+ findings.append({"file": display, "line": _line(source, match.start()), "kind": "modal-route", "presentation": presentation, "message": f"route presented as {presentation} uses <SafeAreaView>; on iOS it can measure a 0 top inset, so pad with useSafeAreaInsets() instead"})
302
+ continue
303
+ if re.search(r"import\s*\{[^}]*\bModal\b[^}]*\}\s*from\s*[\"']react-native[\"']", source):
304
+ for modal in re.finditer(r"<Modal\b", source):
305
+ end = source.find("</Modal>", modal.end())
306
+ inner = source[modal.end(): end if end >= 0 else len(source)]
307
+ safe = re.search(r"<SafeAreaView\b", inner)
308
+ if safe:
309
+ findings.append({"file": display, "line": _line(source, modal.end() + safe.start()), "kind": "rn-modal", "message": "<SafeAreaView> inside a React Native <Modal> can report a 0 top inset on iOS; use useSafeAreaInsets() padding"})
310
+ return {"schemaVersion": "maggie-design-safe-area-check.v1", "passed": not findings, "checkedFiles": len(files), "modalRoutes": sorted(_display(path, project) for path in modal_routes), "findings": findings,
311
+ "recommendation": "Wrap the app once in SafeAreaProvider; in modals read const insets = useSafeAreaInsets() and pad the header/footer. Verify on a notched iPhone (or simulator) — web screenshots cannot show the inset."}
312
+
313
+
252
314
  def main(argv: list[str] | None = None) -> int:
253
315
  parser = argparse.ArgumentParser(prog="maggie design", description="Static design checks that run before browser QA.")
254
316
  sub = parser.add_subparsers(dest="command", required=True)
@@ -260,9 +322,14 @@ def main(argv: list[str] | None = None) -> int:
260
322
  layers.add_argument("--project", type=Path, default=Path.cwd())
261
323
  layers.add_argument("--css", type=Path, action="append", default=[], help="CSS file or directory to scan; defaults to the project")
262
324
  layers.add_argument("--tailwind-v4", action="store_true", help="check even when no @import \"tailwindcss\" entry is found")
325
+ safe = sub.add_parser("safe-area-check", help="flag SafeAreaView inside Expo Router modal routes and React Native Modal (.tsx/.jsx)")
326
+ safe.add_argument("--project", type=Path, default=Path.cwd())
327
+ safe.add_argument("--path", type=Path, action="append", default=[], help="file or directory to scan; defaults to the project")
263
328
  args = parser.parse_args(argv)
264
329
  try:
265
- if args.command == "pressable-check":
330
+ if args.command == "safe-area-check":
331
+ report = safe_area_check(args.project, args.path)
332
+ elif args.command == "pressable-check":
266
333
  report = pressable_check(args.project, args.path, args.pressable)
267
334
  else:
268
335
  report = layer_check(args.project, args.css, args.tailwind_v4)
@@ -68,7 +68,7 @@ def runtime_policy(expo: dict, platform: str) -> tuple[object, str]:
68
68
  return expo.get("runtimeVersion"), "expo.runtimeVersion"
69
69
 
70
70
 
71
- def check_profile(expo: dict, eas: dict, package: dict, profile: str, platforms: list[str], auto_submit: bool, raw_app_json: str | None) -> tuple[list[str], list[str], dict]:
71
+ def check_profile(expo: dict, eas: dict, package: dict, profile: str, platforms: list[str], auto_submit: bool, raw_app_json: str | None, app_dir_ignore_text: str = "") -> tuple[list[str], list[str], dict]:
72
72
  errors: list[str] = []
73
73
  warnings: list[str] = []
74
74
  facts: dict = {"profile": profile, "platforms": platforms}
@@ -140,6 +140,17 @@ def check_profile(expo: dict, eas: dict, package: dict, profile: str, platforms:
140
140
  warnings.append(f"android: submit.{profile}.android.serviceAccountKeyPath is not set; provide it or EXPO_ANDROID_SERVICE_ACCOUNT_KEY_PATH, and keep the key file out of git")
141
141
  if android.get("track", "internal") not in {"internal", "alpha", "beta", "production"}:
142
142
  errors.append("android: submit track must be internal, alpha, beta or production")
143
+ for script_name, command in (package.get("scripts", {}) or {}).items():
144
+ text = str(command)
145
+ if re.search(r"\beas(?:-cli)?\s+update\b(?!:)", text):
146
+ if "--non-interactive" in text and "--environment" not in text:
147
+ errors.append(f"package.json script {script_name!r} runs `eas update --non-interactive` without --environment; EAS refuses it ('The --environment flag must be set'). Add --environment matching the channel")
148
+ if "fingerprint:compare" not in text and uses_updates:
149
+ warnings.append(f"package.json script {script_name!r} publishes an update without `eas fingerprint:compare --build-id <build>`; a fingerprint mismatch sends the update to a runtime no installed build has")
150
+ if uses_updates and any(isinstance(runtime_policy(expo, p)[0], dict) and runtime_policy(expo, p)[0].get("policy") == "fingerprint" for p in platforms):
151
+ ignore = app_dir_ignore_text
152
+ if "eas.json" not in ignore.split():
153
+ warnings.append("runtime policy fingerprint includes eas.json: editing eas.json after a build (for example adding ascAppId) changes the fingerprint and silently stops OTA updates reaching that build. Add eas.json to .fingerprintignore with the next native build, and run `eas fingerprint:compare --build-id <build>` before every update")
143
154
  if raw_app_json and "\t" in raw_app_json:
144
155
  warnings.append("app.json is tab-indented; `eas update:configure` rewrites it with spaces (a large diff) — prefer editing runtimeVersion and updates by hand")
145
156
  return errors, warnings, facts
@@ -154,7 +165,8 @@ def preflight(project: Path, app_dir: Path | None, config_json: Path | None, pro
154
165
  raise ValueError("eas.json not found; run `npx eas-cli build:configure` once, then review the file")
155
166
  eas = _load(eas_path)
156
167
  package = _load(app / "package.json") if (app / "package.json").is_file() else {}
157
- errors, warnings, facts = check_profile(expo, eas, package, profile, platforms, auto_submit, raw)
168
+ ignore = (app / ".fingerprintignore").read_text(encoding="utf-8") if (app / ".fingerprintignore").is_file() else ""
169
+ errors, warnings, facts = check_profile(expo, eas, package, profile, platforms, auto_submit, raw, ignore)
158
170
  return {
159
171
  "schemaVersion": SCHEMA,
160
172
  "appDir": str(app.relative_to(project)) if app.is_relative_to(project) else ".",
@@ -173,7 +185,8 @@ def cost_notice(platforms: list[str], profile: str) -> str:
173
185
  "build minutes even if a later step fails. EAS Update publishes are counted against monthly update users. Confirm before running.")
174
186
 
175
187
 
176
- def plan_commands(profile: str, platforms: list[str], auto_submit: bool, channel: str | None) -> list[dict]:
188
+ def plan_commands(profile: str, platforms: list[str], auto_submit: bool, channel: str | None, environment: str | None = None) -> list[dict]:
189
+ environment = environment or (profile if profile in {"development", "preview", "production"} else "production")
177
190
  platform_arg = "all" if len(platforms) == 2 else platforms[0]
178
191
  commands = [{"step": "preflight", "command": f"maggie mobile preflight --profile {profile} --platform {platform_arg}" + (" --auto-submit" if auto_submit else ""), "spends": False}]
179
192
  build = f"npx eas-cli build -p {platform_arg} --profile {profile} --non-interactive" + (" --auto-submit" if auto_submit else "")
@@ -182,8 +195,8 @@ def plan_commands(profile: str, platforms: list[str], auto_submit: bool, channel
182
195
  for platform in platforms:
183
196
  commands.append({"step": f"submit-{platform}", "command": f"npx eas-cli submit -p {platform} --profile {profile} --latest --non-interactive", "spends": False})
184
197
  if channel:
185
- environment = f" --environment {profile}" if profile in {"development", "preview", "production"} else ""
186
- commands.append({"step": "ota-update", "command": f"npx eas-cli update --channel {channel}{environment} --platform {platform_arg} --message \"<what changed>\" --non-interactive", "spends": True, "note": "only JS/asset changes; a native change needs a new build (the fingerprint policy enforces this)"})
198
+ commands.append({"step": "fingerprint-gate", "command": "npx eas-cli fingerprint:compare --build-id <installed build id>", "spends": False, "note": "must report no difference; editing eas.json, native config or dependencies after the build changes the fingerprint and the update would reach no installed build"})
199
+ commands.append({"step": "ota-update", "command": f"npx eas-cli update --channel {channel} --environment {environment} --platform {platform_arg} --message \"<what changed>\" --non-interactive", "spends": True, "note": "only JS/asset changes; a native change needs a new build (the fingerprint policy enforces this)"})
187
200
  return commands
188
201
 
189
202
 
@@ -198,6 +211,7 @@ def main(argv: list[str] | None = None) -> int:
198
211
  command.add_argument("--profile", default="production")
199
212
  command.add_argument("--platform", choices=("ios", "android", "all"), default="all")
200
213
  command.add_argument("--auto-submit", action="store_true", help="the build will run with --auto-submit --non-interactive")
214
+ command.add_argument("--environment", help="EAS environment for updates (default: the profile when it is development/preview/production)")
201
215
  command.add_argument("--output", type=Path)
202
216
  args = parser.parse_args(argv)
203
217
  platforms = list(PLATFORMS) if args.platform == "all" else [args.platform]
@@ -207,7 +221,7 @@ def main(argv: list[str] | None = None) -> int:
207
221
  print(f"BLOCKED: maggie mobile {args.command}: {error}", file=sys.stderr)
208
222
  return 2
209
223
  if args.command == "plan":
210
- report = {"schemaVersion": "maggie-mobile-release-plan.v1", "preflight": {key: report[key] for key in ("status", "errors", "warnings")}, "commands": plan_commands(args.profile, platforms, args.auto_submit, report["facts"].get("channel") if report["facts"].get("easUpdate") else None), "costNotice": report["costNotice"], "executed": False}
224
+ report = {"schemaVersion": "maggie-mobile-release-plan.v1", "preflight": {key: report[key] for key in ("status", "errors", "warnings")}, "commands": plan_commands(args.profile, platforms, args.auto_submit, report["facts"].get("channel") if report["facts"].get("easUpdate") else None, args.environment), "costNotice": report["costNotice"], "executed": False}
211
225
  text = json.dumps(report, indent=2, ensure_ascii=False)
212
226
  if args.output:
213
227
  args.output.parent.mkdir(parents=True, exist_ok=True)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@topy-ai/maggie",
3
- "version": "0.7.51",
3
+ "version": "0.7.52",
4
4
  "description": "Install and manage Maggie Skills for AI coding agents",
5
5
  "license": "MIT",
6
6
  "type": "module",