From 3396aab0c0d0cb8e68a9557d7a66ddf876f942d3 Mon Sep 17 00:00:00 2001 From: brbndon Date: Mon, 27 Jul 2026 09:27:00 -0700 Subject: [PATCH 01/21] chore(oss): polish public storefront for sharing - Fill MIT copyright holder; add SECURITY.md and cut changelog 0.2.0 - Clarify archive boundary and mark Website README as superseded - Ignore .swiftpm/.cursor; set GitHub About outside this commit --- .gitignore | 3 +++ ARCHIVE.md | 6 +----- CHANGELOG.md | 6 ++++-- CONTRIBUTING.md | 2 +- LICENSE | 2 +- SECURITY.md | 11 +++++++++++ archive/Website/README.md | 40 +++++++++------------------------------ 7 files changed, 30 insertions(+), 40 deletions(-) create mode 100644 SECURITY.md diff --git a/.gitignore b/.gitignore index ea95eb7..857b0e6 100644 --- a/.gitignore +++ b/.gitignore @@ -1,12 +1,15 @@ .DS_Store .build/ +.swiftpm/ DerivedData/ *.xcuserstate xcuserdata/ .xcodebuildmcp/ +.cursor/ archive/Website/node_modules/ archive/Website/dist/ archive/Website/.vite/ +archive/Website/.vercel/ node_modules/ .blume/ .blume-verify/ diff --git a/ARCHIVE.md b/ARCHIVE.md index 181e5c8..09bf064 100644 --- a/ARCHIVE.md +++ b/ARCHIVE.md @@ -25,8 +25,4 @@ The repository's former Swift package, examples, apps, website, historical packa ## Agent guidance -Do **not** add modules, expand public APIs, refactor package code, or "improve" archived targets unless the user explicitly asks. Skills in this repo apply to **consumer apps and packages in the user's active workspace**, not to landing code in `archive/`. See [`archive/README.md`](archive/README.md) for the preserved layout. - -## History note - -Commit `2c9f948` repurpose also included unrelated LoggingKit/AppShellKit test additions. Those diffs were left in place; they are not part of the skills/MCP product story. +Do **not** add modules, expand public APIs, refactor package code, or "improve" archived targets unless the user explicitly asks. Skills in this repo apply to **consumer apps and packages in the user's active workspace**, not to landing code in `archive/`. See [`archive/README.md`](archive/README.md) for the preserved layout. Archived sources are frozen as moved for history; they are not part of the live skills/MCP product. diff --git a/CHANGELOG.md b/CHANGELOG.md index b283740..0263628 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,8 +1,10 @@ # Changelog -## Unreleased +## 0.2.0 — 2026-07-27 -- Repurposed repo as skills + MCP reference; Swift package archived (see `ARCHIVE.md`). +- Skills + MCP reference is the primary product; Swift package, demos, and former website live under `archive/` (see `ARCHIVE.md`). +- Agent playbook docs site (Blume) with GitHub Pages deploy. +- Installer, skill verification, and behavioral evaluation fixtures. ## 0.1.0 — 2026-07-10 diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index cc40b82..e6ae08c 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -7,4 +7,4 @@ This repo's product is **skills + MCP references**, not the archived Swift packa - Agent playbook: update Blume content under `docs/` when the skill/MCP workflow changes; run `npm run build` and `npm run validate` from the repo root - Archived package: see `ARCHIVE.md` — do not expand `archive/Sources/` or public APIs unless explicitly requested -See `AGENTS.md` and `docs/skills/skill-authoring-guide.mdx` for durable rules. +See `AGENTS.md` and `docs/skills/skill-authoring-guide.mdx` for durable rules. Report security issues privately per `SECURITY.md`. diff --git a/LICENSE b/LICENSE index 14fac91..61b1e85 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ MIT License -Copyright (c) 2026 +Copyright (c) 2026 brbndon Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/SECURITY.md b/SECURITY.md new file mode 100644 index 0000000..d09ded5 --- /dev/null +++ b/SECURITY.md @@ -0,0 +1,11 @@ +# Security Policy + +## Supported material + +This repository’s maintained surface is Codex skills, install/verify scripts, and agent documentation. The Swift package and demos under `archive/` are historical and not maintained as a product. + +## Reporting a vulnerability + +Please report security issues privately via [GitHub Security Advisories](https://github.com/brbndon/AppleDevelopmentFoundation/security/advisories/new). + +Do not open a public issue for sensitive reports. Include enough detail to reproduce the problem (affected path or script, host assumptions, and impact). Reports about third-party tools referenced here (for example XcodeBuildMCP or Maestro) should go to those projects unless the issue is in this repository’s guidance or wrappers. diff --git a/archive/Website/README.md b/archive/Website/README.md index ad4f616..4109b1e 100644 --- a/archive/Website/README.md +++ b/archive/Website/README.md @@ -1,45 +1,23 @@ -# Foundation website +# Archived website (superseded) -This is an isolated React + Tailwind SPA powered by Vite that introduces the **skills + MCP** product of this repository. The site deliberately lives outside the Swift package targets and does not change package source, products, or module dependencies. +Historical Vite + React marketing SPA preserved for reference only. It is **not** the live product site and should not be deployed. -Authoritative entry points in the repository: +**Live docs:** [https://brbndon.github.io/AppleDevelopmentFoundation/](https://brbndon.github.io/AppleDevelopmentFoundation/) (Blume site under [`docs/`](../../docs/) at the repository root). + +Authoritative entry points: - [README.md](../../README.md) — install skills and skill inventory - [MCP.md](../../MCP.md) — XcodeBuildMCP setup, tools, and copy-paste prompts - [.agents/skills/](../../.agents/skills/) — reusable Codex skills (the live product) -- [ARCHIVE.md](../../ARCHIVE.md) — archived Swift package, examples, and historical docs - -The website includes a clearly labeled archived-package reference section. Do not treat that material as active integration guidance. +- [ARCHIVE.md](../../ARCHIVE.md) — archive boundary -## Install and preview locally +## Local preview (archive work only) -From the `archive/` directory, install the website-only dependencies once: +Only when explicitly working on this archived material: ```bash npm ci --prefix Website -``` - -Start the Vite development server with HMR: - -```bash npm run dev --prefix Website ``` -Open . - -## Build and preview production output - -```bash -npm run build --prefix Website -npm run preview --prefix Website -``` - -The production preview runs on . The build output is written to `Website/dist/`. - -## Deploy - -Publish the contents of `Website/dist/` as the document root on any static host, including GitHub Pages, Cloudflare Pages, Netlify, or a simple web server. No backend, runtime service, environment variables, or external assets are required. - -Run `./Scripts/verify-website.sh` after `npm ci --prefix Website` and before publishing to validate the source anchors and production build. - -The website dependencies are intentionally isolated from the Swift package. To remove the frontend tooling, delete `Website/package.json`, `Website/package-lock.json`, `Website/vite.config.js`, and `Website/src/`, then restore the static `index.html` and `script.js` entry arrangement. +Open . Production build output lands in `Website/dist/` (gitignored). From 6564a8e3850b234e2875ce0908d77d347fac6746 Mon Sep 17 00:00:00 2001 From: brbndon Date: Mon, 27 Jul 2026 21:44:25 -0700 Subject: [PATCH 02/21] docs(archive): restore website preview working directory - Say preview commands must run from archive/ with --prefix Website --- archive/Website/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/archive/Website/README.md b/archive/Website/README.md index 4109b1e..2faaa2a 100644 --- a/archive/Website/README.md +++ b/archive/Website/README.md @@ -13,7 +13,7 @@ Authoritative entry points: ## Local preview (archive work only) -Only when explicitly working on this archived material: +Only when explicitly working on this archived material. From the `archive/` directory: ```bash npm ci --prefix Website From 16fbe54dfb0f19051680e17a83adbd64e233958f Mon Sep 17 00:00:00 2001 From: brbndon Date: Wed, 29 Jul 2026 08:51:54 -0700 Subject: [PATCH 03/21] docs(e2e): add consumer testing contract guidance - Document deterministic Maestro app, simulator, state, fixture, artifact, and accessibility contracts.\n- Add active onboarding first-run and permission guidance.\n- Verified with verify-skills, npm validate, and consumer guidance tests. --- .../assets/consumer-AGENTS.md.template | 32 +++++++++++++++++++ docs/tools/maestro.mdx | 8 +++++ docs/workflow/agents-md-template.mdx | 32 +++++++++++++++++++ 3 files changed, 72 insertions(+) diff --git a/.agents/skills/codex-bootstrap/assets/consumer-AGENTS.md.template b/.agents/skills/codex-bootstrap/assets/consumer-AGENTS.md.template index b6e6e60..7146974 100644 --- a/.agents/skills/codex-bootstrap/assets/consumer-AGENTS.md.template +++ b/.agents/skills/codex-bootstrap/assets/consumer-AGENTS.md.template @@ -25,6 +25,38 @@ this contract for files in their scope. - Targeted tests: `` - Full tests: `` +## E2E test contract + +Document the deterministic app contract before relying on Maestro journeys. Keep +secrets out of this file, flow files, fixtures, and artifacts. + +- App identifier (bundle ID): `` +- `.maestro/` location and naming: ``; name + flows `.yaml` and reusable subflows `.yaml` using + `` +- Exact simulator/configuration: simulator ID ``, scheme + ``, and build configuration ``; use the same + destination and configuration for XcodeBuildMCP, Maestro MCP, and CLI runs +- Launch arguments/environment: `` and + ``; inject secrets locally or in CI + rather than committing them +- Reset and seed strategy: `` and + ``; state what persists across relaunch and when app + data, keychain, permissions, or backend state must be reset +- Onboarding state: ``; specify how tests enter + each state and whether onboarding is required, conditional, or a dedicated + subflow +- Fixtures and test accounts: `` and + ``; define ownership, cleanup, and + account reset behavior +- Deep links: `` +- Artifacts: ``; preserve + failure artifacts while redacting secrets and private user data +- Accessibility-ID conventions: use stable + `.` selectors on actionable elements, + keep human-readable accessibility labels, and document any native-control + exception + ## Apple verification policy Fill these before relying on fallback behavior. Shell access or an installed diff --git a/docs/tools/maestro.mdx b/docs/tools/maestro.mdx index 32564e7..0e5a34e 100644 --- a/docs/tools/maestro.mdx +++ b/docs/tools/maestro.mdx @@ -47,6 +47,14 @@ If Maestro or its MCP server is unavailable, **stop and report** the exact block 7. On failure: preserve debug output, screenshots, hierarchy, video when useful; classify; reproduce once; fix; re-run the same focused flow. 8. Run a complete suite only when the user explicitly requests it or a documented release/CI gate requires it. +## Onboarding E2E guidance + +- Make first-run state explicit: document whether each flow starts from a fresh install, reset data, or an already-completed onboarding state. +- Define reset and persistence behavior. Reset app data, keychain, permissions, and backend fixtures only through the project’s agreed strategy; verify relaunch behavior instead of assuming state is cleared. +- Handle system permissions conditionally because a prompt may appear only on first run or after a reset. Keep permission setup deterministic and never depend on a prior simulator state. +- If onboarding may appear in a feature flow, use a conditional check to complete it; do not hide an unexpected onboarding screen with arbitrary sleeps. +- Keep the complete first-run journey in a dedicated onboarding flow or reusable subflow under `.maestro/`, then invoke it from other flows only when their contract requires it. + ## Authoring rules (distilled from the skill) - Prefer stable `testID` / accessibility selectors and explicit assertions. diff --git a/docs/workflow/agents-md-template.mdx b/docs/workflow/agents-md-template.mdx index 65c3754..387d28d 100644 --- a/docs/workflow/agents-md-template.mdx +++ b/docs/workflow/agents-md-template.mdx @@ -77,6 +77,38 @@ this contract for files in their scope. - Targeted tests: `` - Full tests: `` +## E2E test contract + +Document the deterministic app contract before relying on Maestro journeys. Keep +secrets out of this file, flow files, fixtures, and artifacts. + +- App identifier (bundle ID): `` +- `.maestro/` location and naming: ``; name + flows `.yaml` and reusable subflows `.yaml` using + `` +- Exact simulator/configuration: simulator ID ``, scheme + ``, and build configuration ``; use the same + destination and configuration for XcodeBuildMCP, Maestro MCP, and CLI runs +- Launch arguments/environment: `` and + ``; inject secrets locally or in CI + rather than committing them +- Reset and seed strategy: `` and + ``; state what persists across relaunch and when app + data, keychain, permissions, or backend state must be reset +- Onboarding state: ``; specify how tests enter + each state and whether onboarding is required, conditional, or a dedicated + subflow +- Fixtures and test accounts: `` and + ``; define ownership, cleanup, and + account reset behavior +- Deep links: `` +- Artifacts: ``; preserve + failure artifacts while redacting secrets and private user data +- Accessibility-ID conventions: use stable + `.` selectors on actionable elements, + keep human-readable accessibility labels, and document any native-control + exception + ## Apple verification policy Fill these before relying on fallback behavior. Shell access or an installed From 20cf213cfd3c5622a98063fd76d4f6040c7bb0c6 Mon Sep 17 00:00:00 2001 From: brbndon Date: Wed, 29 Jul 2026 11:47:03 -0700 Subject: [PATCH 04/21] docs(skills): refine Apple design craft, glass, and motion defaults Close foundation skill gaps from the design-guidance audit without expanding archive docs or promoting host-local skills into the install catalog. - apple-design-system: polish defaults (opaque content, glass restraint, ~44pt targets, avoid thin UI weights) - swiftui-tab-navigation: selective bar tinting, system-materials verification, and a thin liquid-glass-chrome restraint reference - swiftui-component-author: visual craft plus large Dynamic Type layout (scroll by default, primary text growth) and frequency-gated motion defaults (critically damped, touch-down press, source-anchored presentation, no input lock during transitions) - apple-accessibility-review: align Dynamic Type inspection with author policy - apple-platform-planner: name 2026 HIG principles as audit language - competing-macos-skills-plan: route deep motion and glass APIs to host-local skills after foundation restraint rules Verified with ./Scripts/verify-skills.sh. --- .../apple-accessibility-review/SKILL.md | 2 +- .agents/skills/apple-design-system/SKILL.md | 9 +++++---- .../references/competing-macos-skills-plan.md | 4 ++++ .../skills/apple-platform-planner/SKILL.md | 2 +- .../skills/swiftui-component-author/SKILL.md | 9 +++++---- .../skills/swiftui-tab-navigation/SKILL.md | 6 +++--- .../references/liquid-glass-chrome.md | 19 +++++++++++++++++++ 7 files changed, 38 insertions(+), 13 deletions(-) create mode 100644 .agents/skills/swiftui-tab-navigation/references/liquid-glass-chrome.md diff --git a/.agents/skills/apple-accessibility-review/SKILL.md b/.agents/skills/apple-accessibility-review/SKILL.md index 4ef531d..4fbf784 100644 --- a/.agents/skills/apple-accessibility-review/SKILL.md +++ b/.agents/skills/apple-accessibility-review/SKILL.md @@ -12,7 +12,7 @@ Review **reusable SwiftUI** in the **consumer workspace** for accessibility and Work top-down through the component or shared UI under review: 1. **VoiceOver** — meaningful labels, traits, values, hints only when needed; correct grouping/combine; decorative images hidden; custom controls expose actions. -2. **Dynamic Type** — layouts reflow; no truncated essential text at large sizes; avoid fixed heights that clip content. +2. **Dynamic Type** — layouts reflow; scroll by default; primary text grows first; no truncated essential text at large sizes; avoid fixed heights that clip content. 3. **Focus and keyboard** — logical order on platforms with keyboard/focus (especially macOS); no keyboard traps; actionable elements reachable. 4. **Touch and hit targets** — controls remain usable; spacing does not rely on tiny hit areas alone. 5. **Color and contrast** — text and essential icons remain readable; Increased Contrast considered when the project supports it. diff --git a/.agents/skills/apple-design-system/SKILL.md b/.agents/skills/apple-design-system/SKILL.md index 5bb1dba..110c10e 100644 --- a/.agents/skills/apple-design-system/SKILL.md +++ b/.agents/skills/apple-design-system/SKILL.md @@ -13,10 +13,11 @@ Change **shared** semantic tokens or design-system components in the **consumer 2. **Semantic tokens before chrome.** Prefer roles (`background`, `label`, `accent`, `danger`, spacing scale, type styles) over raw hex or one-off sizes. Map tokens to light/dark (and increased contrast when the project supports it). 3. **Native first.** Prefer system materials, typography, and controls. Custom tokens should wrap or complement platform defaults, not fight them. 4. **Appearance and motion.** New visual tokens must remain readable in light and dark appearance. Prefer Reduce Motion–safe defaults; avoid decorative motion that is the only affordance of meaning. -5. **Differentiate without color alone.** Status and state must not rely only on hue when color is the sole signal. -6. **Layering.** Tokens feed shared components; app screens consume components/tokens. Do not hardcode magic numbers for shared UI when a token should exist. -7. **Document usage.** Name tokens for role, not for a single marketing campaign or temporary experiment. Note deprecations when replacing tokens. -8. **Verify.** Add or update focused tests/examples or a gallery surface when the project has one. Build the affected target. Accessibility audits of screens belong to `apple-accessibility-review`, not this skill alone. +5. **Polish defaults.** Content layer stays solid/opaque; reserve glass for system navigation chrome (not content lists, cards, or tables). Use brand/accent for primary actions and content highlights, not full custom chrome. Shared interactive targets must remain usable at ~44×44 pt. Prefer system text styles and Regular/Medium/Semibold/Bold; avoid Ultralight/Thin for UI copy. +6. **Differentiate without color alone.** Status and state must not rely only on hue when color is the sole signal. +7. **Layering.** Tokens feed shared components; app screens consume components/tokens. Do not hardcode magic numbers for shared UI when a token should exist. +8. **Document usage.** Name tokens for role, not for a single marketing campaign or temporary experiment. Note deprecations when replacing tokens. +9. **Verify.** Add or update focused tests/examples or a gallery surface when the project has one. Build the affected target. Accessibility audits of screens belong to `apple-accessibility-review`, not this skill alone. ## Stop conditions diff --git a/.agents/skills/apple-development-foundation/references/competing-macos-skills-plan.md b/.agents/skills/apple-development-foundation/references/competing-macos-skills-plan.md index ffdf9fb..111789e 100644 --- a/.agents/skills/apple-development-foundation/references/competing-macos-skills-plan.md +++ b/.agents/skills/apple-development-foundation/references/competing-macos-skills-plan.md @@ -19,8 +19,12 @@ catalog’s bootstrap → design-system → verification chain. | Feature planning (no implement) | `$apple-development-foundation` → `$apple-platform-planner` | Review skills only if asked | | Ordinary feature/component in an existing consumer app | Child skill directly (`swiftui-component-author`, …) | `$swift-testing-verification` | | Pure HIG / visual critique with no foundation workflow | External design skill is fine | Do not invent bootstrap | +| Gesture polish / fluid motion depth | Foundation first (native chrome, a11y, craft defaults) | Host-local `apple-design` only if still needed | +| iOS 26 Liquid Glass **API** patterns | Foundation “system chrome first” (`swiftui-tab-navigation` + `liquid-glass-chrome`) | Host-local `liquid-glass-design` only after restraint rules | | Foundation skill inventory / installer audit | `$apple-development-foundation` (audit path) | Foundation verify scripts only | +Host-local skills such as `apple-design` and `liquid-glass-design` are **not** in this catalog and are not installed by `./Scripts/install-skills.sh` unless later promoted. + ## Description / discovery levers (done or next) 1. **Done in this change:** Strengthen the master skill `description` so bootstrap diff --git a/.agents/skills/apple-platform-planner/SKILL.md b/.agents/skills/apple-platform-planner/SKILL.md index 1469706..8ccd78d 100644 --- a/.agents/skills/apple-platform-planner/SKILL.md +++ b/.agents/skills/apple-platform-planner/SKILL.md @@ -13,7 +13,7 @@ Produce a **bounded implementation plan** for an Apple-platform feature in the * 2. **Clarify the slice.** Restate the user goal, platforms in scope, and out-of-scope items. Stop and ask if platforms, consumer path, or success criteria are materially ambiguous. 3. **Separate shared vs platform-specific.** Identify logic that can stay shared versus presentation, navigation, or lifecycle differences that need `ios-macos-platform-adaptation` later. 4. **Name ownership.** For each major state or dependency, say who owns it (view, environment, observable model, actor, package) and how it is injected. Flag any proposed view model with an ownership or testability reason. -5. **Surface constraints early.** Note accessibility, privacy/permissions, entitlements, offline/network, and concurrency risks that affect design — not only polish. +5. **Surface constraints early.** Note accessibility, privacy/permissions, entitlements, offline/network, and concurrency risks that affect design — not only polish. When relevant, plan against Apple’s 2026 HIG design principles (Purpose, Agency, Responsibility, Familiarity, Flexibility, Simplicity, Craft, Delight) — see [Design principles](https://developer.apple.com/design/human-interface-guidelines/design-principles). Use them as named audit language only; do not expand into an essay. 6. **Plan verification.** List the smallest build/test/UI checks (XcodeBuildMCP / unit / Maestro) that will prove the feature. Prefer one exact simulator/device strategy when iOS is in scope. 7. **Sequence work.** Ordered implementation slices small enough to land independently. Recommend skill chain for the build phase (for example design system → component author → platform adaptation → reviews → testing). 8. **Risks and stops.** Call out unknowns, dependency needs that require authorization, and decisions that need the user. diff --git a/.agents/skills/swiftui-component-author/SKILL.md b/.agents/skills/swiftui-component-author/SKILL.md index 607ca5f..de3bcb6 100644 --- a/.agents/skills/swiftui-component-author/SKILL.md +++ b/.agents/skills/swiftui-component-author/SKILL.md @@ -13,10 +13,11 @@ Create or refactor a **reusable** SwiftUI component in the **consumer workspace* 2. **Inspect first.** Read existing design-system tokens, sibling components, and repository UI conventions. Prefer extending an existing component over forking a near-duplicate. 3. **Prefer native controls.** Use system controls and semantic configuration. Avoid custom chrome that reimplements buttons, lists, or navigation unless the product requires it and accessibility parity is planned. 4. **Own state explicitly.** Prefer initializer parameters and environment injection. Use native observation for shared app state. Do not introduce a view model without a state-ownership or testability reason written in the handoff. -5. **Accessibility baseline (required).** Support Dynamic Type, VoiceOver labels/traits/grouping, keyboard and focus order where the platform has them, sufficient contrast, Reduce Motion, Differentiate Without Color, and a descriptive label for every icon-only control. Touch targets must remain usable. -6. **Keep the public API small.** Expose only configuration the consumers need. Document defaults and non-obvious parameters. Prefer semantic style enums over raw colors/fonts when tokens exist. -7. **Ship a preview or example** that exercises primary configurations and at least one accessibility-sensitive case (for example large Dynamic Type or an empty state). -8. **Test what the component owns.** Focused tests for configuration logic, state transitions, and public API contracts. Do not claim VoiceOver or visual polish passed without evidence — list manual checks. +5. **Accessibility baseline (required).** Support Dynamic Type, VoiceOver labels/traits/grouping, keyboard and focus order where the platform has them, sufficient contrast, Reduce Motion, Differentiate Without Color, and a descriptive label for every icon-only control. Touch targets must remain usable (~44×44 pt). At large Dynamic Type sizes, allow scrolling by default and prioritize primary text growth so essential content is not clipped. +6. **Visual craft.** Match SF Symbol weight and scale to adjacent text. Prefer frequency-gated, interruptible motion: critically damped defaults; bounce only after momentum; press feedback on touch-down; source-anchored sheets/menus when origin matters; never lock input during transitions. Prefer system transitions; avoid multi-second decorative choreography and motion that is the only affordance of meaning. Icon-only controls need a label and a usable hit area. +7. **Keep the public API small.** Expose only configuration the consumers need. Document defaults and non-obvious parameters. Prefer semantic style enums over raw colors/fonts when tokens exist. +8. **Ship a preview or example** that exercises primary configurations and at least one accessibility-sensitive case (for example large Dynamic Type or an empty state). +9. **Test what the component owns.** Focused tests for configuration logic, state transitions, and public API contracts. Do not claim VoiceOver or visual polish passed without evidence — list manual checks. ## Stop conditions diff --git a/.agents/skills/swiftui-tab-navigation/SKILL.md b/.agents/skills/swiftui-tab-navigation/SKILL.md index 3531816..5f8970c 100644 --- a/.agents/skills/swiftui-tab-navigation/SKILL.md +++ b/.agents/skills/swiftui-tab-navigation/SKILL.md @@ -13,13 +13,13 @@ Build primary navigation from native SwiftUI containers in the **consumer worksp 2. Prefer the system tab container. On supported iOS versions, native tab chrome adopts the current system appearance, including Liquid Glass on iOS 26. Do not imitate it with `glassEffect`, materials, overlays, or a custom safe-area bar. 3. Keep loading, onboarding, unrecoverable failures, and other pre-content routing outside the tab shell. Give each tab its own `NavigationStack` when it owns drill-down navigation. 4. Add typed selection state only when programmatic tab changes, restoration, inspection destinations, or deep links require it. Preserve existing direct inspection and deep-link routes. -5. Use concise text plus SF Symbols in each tab label. Keep selection semantics and interaction native; apply only semantic app tinting. +5. Use concise text plus SF Symbols in each tab label. Keep selection semantics and interaction native; apply only semantic app tinting (one primary accent, not every bar item). 6. If iOS and macOS need genuinely different primary-navigation conventions, invoke `ios-macos-platform-adaptation`; do not force an iOS tab bar onto a macOS app that calls for a sidebar, windows, or commands. -7. Read [references/native-tab-shell.md](references/native-tab-shell.md) when implementing the shell or updating UI automation. +7. Read [references/liquid-glass-chrome.md](references/liquid-glass-chrome.md) for Liquid Glass do/don’t (system chrome first; Regular/Clear; no glass-on-glass or glass-as-content). Read [references/native-tab-shell.md](references/native-tab-shell.md) when implementing the shell or updating UI automation. ## Verification -Use `swift-testing-verification` and XcodeBuildMCP with one exact target. Build and run, capture a screenshot or hierarchy on the newest supported runtime, exercise every tab and nested back path, and verify any minimum-OS fallback the project supports. Run affected XCTest or Maestro journeys after selector changes. +Use `swift-testing-verification` and XcodeBuildMCP with one exact target. Build and run, capture a screenshot or hierarchy on the newest supported runtime, exercise every tab and nested back path, and verify any minimum-OS fallback the project supports. When chrome changes, confirm no custom bar/sheet background fights system materials. Run affected XCTest or Maestro journeys after selector changes. Inspect the runtime hierarchy before choosing UI-test selectors. SwiftUI creates the actual tab buttons; an accessibility identifier placed inside `.tabItem` may not propagate to those generated controls. Prefer an exposed stable identifier when present, otherwise use the visible accessibility label and scope XCTest queries to `tabBars`. diff --git a/.agents/skills/swiftui-tab-navigation/references/liquid-glass-chrome.md b/.agents/skills/swiftui-tab-navigation/references/liquid-glass-chrome.md new file mode 100644 index 0000000..69a9541 --- /dev/null +++ b/.agents/skills/swiftui-tab-navigation/references/liquid-glass-chrome.md @@ -0,0 +1,19 @@ +# Liquid Glass chrome (restraint) + +System navigation chrome owns Liquid Glass. Prefer native `TabView`, toolbars, navigation bars, and sheets. + +## Do + +- Put glass on the **navigation / control** layer only. +- Prefer **system** materials and containers; let the OS adopt Liquid Glass without imitation. +- Default material: **Regular**. Use **Clear** only over media, with dimming and bold/legible foreground. +- Custom floating controls in the navigation plane: `glassEffect` **inside** `GlassEffectContainer` only when system chrome cannot express the control. +- Tint **one** primary action (or semantic accent), not every bar item. + +## Don’t + +- Do not apply glass to list/table/card **content** rows or large content surfaces. +- Do not stack glass-on-glass. +- Do not mix Regular and Clear in the same surface hierarchy without a clear media reason. +- Do not fake system tab/nav bars with custom `glassEffect`, materials, overlays, or safe-area bars. +- Do not keep older custom bar/sheet **opaque backgrounds** that fight scroll-edge / system materials — remove them when adopting system chrome. From 21007cbfe4eeb3c86392b43a297cc564d2cb43aa Mon Sep 17 00:00:00 2001 From: brbndon Date: Wed, 29 Jul 2026 11:49:43 -0700 Subject: [PATCH 05/21] chore: ignore local TinyFish research outputs Keep audit/research dumps under tinyfish-results/ out of version control so skill refinements stay product-focused. --- .gitignore | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.gitignore b/.gitignore index 857b0e6..b611b26 100644 --- a/.gitignore +++ b/.gitignore @@ -16,3 +16,6 @@ node_modules/ dist/ __pycache__/ *.py[cod] + +# Local TinyFish research outputs (not product guidance) +tinyfish-results/ From f02999aba731beb2c21344f054a9da7cfd3276f0 Mon Sep 17 00:00:00 2001 From: brbndon Date: Wed, 29 Jul 2026 12:13:38 -0700 Subject: [PATCH 06/21] docs(skills): route design motion/glass and close craft activation gaps MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Always-on feature-screen polish; keyboard/high-frequency never-animate; path symmetry + motion verification - Add design-motion-glass routing reference and wire host-depth rows without catalog promotion - Liquid Glass Don’t: materialize-not-fade and steady-state content/glass separation --- .../apple-development-foundation/SKILL.md | 3 ++ .../references/competing-macos-skills-plan.md | 10 +++- .../references/design-motion-glass-routing.md | 52 +++++++++++++++++++ .../skills/swiftui-component-author/SKILL.md | 4 +- .../references/liquid-glass-chrome.md | 2 + AGENTS.md | 2 + 6 files changed, 70 insertions(+), 3 deletions(-) create mode 100644 .agents/skills/apple-development-foundation/references/design-motion-glass-routing.md diff --git a/.agents/skills/apple-development-foundation/SKILL.md b/.agents/skills/apple-development-foundation/SKILL.md index fb2eea6..75bb1a4 100644 --- a/.agents/skills/apple-development-foundation/SKILL.md +++ b/.agents/skills/apple-development-foundation/SKILL.md @@ -39,3 +39,6 @@ override their exclusions, install global files, or claim automatic activation. For host-local skills that compete with this catalog (generic macOS development guides, design-only skills, and similar), see [references/competing-macos-skills-plan.md](references/competing-macos-skills-plan.md). +For Liquid Glass, animation polish, and motion-audit routing (foundation first, +host depth only if needed), see +[references/design-motion-glass-routing.md](references/design-motion-glass-routing.md). diff --git a/.agents/skills/apple-development-foundation/references/competing-macos-skills-plan.md b/.agents/skills/apple-development-foundation/references/competing-macos-skills-plan.md index 111789e..51ac48c 100644 --- a/.agents/skills/apple-development-foundation/references/competing-macos-skills-plan.md +++ b/.agents/skills/apple-development-foundation/references/competing-macos-skills-plan.md @@ -21,9 +21,17 @@ catalog’s bootstrap → design-system → verification chain. | Pure HIG / visual critique with no foundation workflow | External design skill is fine | Do not invent bootstrap | | Gesture polish / fluid motion depth | Foundation first (native chrome, a11y, craft defaults) | Host-local `apple-design` only if still needed | | iOS 26 Liquid Glass **API** patterns | Foundation “system chrome first” (`swiftui-tab-navigation` + `liquid-glass-chrome`) | Host-local `liquid-glass-design` only after restraint rules | +| “What should animate?” / opportunity map | Foundation craft first (`swiftui-component-author` frequency gate) | Host-local `find-animation-opportunities` | +| Audit / improve motion across the app | Foundation craft + a11y baselines | Host-local `improve-animations` (read-only plans) | | Foundation skill inventory / installer audit | `$apple-development-foundation` (audit path) | Foundation verify scripts only | -Host-local skills such as `apple-design` and `liquid-glass-design` are **not** in this catalog and are not installed by `./Scripts/install-skills.sh` unless later promoted. +Host-local skills such as `apple-design`, `liquid-glass-design`, +`find-animation-opportunities`, `improve-animations`, and +`animation-vocabulary` are **not** in this catalog and are not installed by +`./Scripts/install-skills.sh` unless later promoted. + +Procedure detail (Liquid Glass steps, animation delete-first refactor, host +paths): [design-motion-glass-routing.md](design-motion-glass-routing.md). ## Description / discovery levers (done or next) diff --git a/.agents/skills/apple-development-foundation/references/design-motion-glass-routing.md b/.agents/skills/apple-development-foundation/references/design-motion-glass-routing.md new file mode 100644 index 0000000..05a6677 --- /dev/null +++ b/.agents/skills/apple-development-foundation/references/design-motion-glass-routing.md @@ -0,0 +1,52 @@ +# Design, motion, and Liquid Glass routing + +Foundation-first routing for polish, Liquid Glass, and animation work. Host skills are **depth only** — never installable catalog replacements. + +## Hard rule + +For new UI, never shortlist host design/motion skills instead of: + +- `swiftui-tab-navigation` (+ `liquid-glass-chrome.md`) +- `apple-design-system` +- `swiftui-component-author` +- `apple-accessibility-review` + +Always-on craft for feature screens also lives in root `Agents.md` / `AGENTS.md`. + +## Intent matrix + +| Intent | Foundation first | Then | Host depth only if needed | +| --- | --- | --- | --- | +| New app / major skeleton | `$apple-development-foundation` → `$codex-bootstrap` → design-system, tabs, a11y | Chained children | — | +| Liquid Glass / materials | System chrome + `swiftui-tab-navigation` + `liquid-glass-chrome.md` | Custom `glassEffect` only after restraint | `~/.agents/skills/liquid-glass-design/SKILL.md` | +| Animation polish on a screen/component | `swiftui-component-author` craft defaults + a11y Reduce Motion | Delete high-frequency motion first | `~/.agents/skills/apple-design/SKILL.md` (gesture physics) | +| “What should animate?” | Craft defaults + frequency gate (no keyboard / high-frequency paths) | — | `~/.agents/skills/find-animation-opportunities/SKILL.md` | +| Whole-app motion audit | Craft + a11y baselines applied | — | `~/.agents/skills/improve-animations/SKILL.md` (read-only plans) | +| Motion naming / vocabulary | Prefer plain craft language in foundation skills | — | `~/.agents/skills/animation-vocabulary/SKILL.md` | + +Also see [competing-macos-skills-plan.md](competing-macos-skills-plan.md). + +## Liquid Glass procedure + +1. Prefer **system** navigation chrome (`TabView`, toolbars, navigation bars, sheets) — do not imitate with custom glass. +2. Read `swiftui-tab-navigation/references/liquid-glass-chrome.md` and apply Do/Don’t restraint. +3. Use custom `glassEffect` / `GlassEffectContainer` **only** when system chrome cannot express a navigation-plane control, and only after restraint. +4. Host `liquid-glass-design` last — API depth after foundation restraint, never instead of it. + +## Animation refactor steps + +1. **Inventory** custom transitions and decorative motion on the touched surfaces. +2. **Delete high-frequency first** — keyboard-initiated paths, command palettes, shortcuts, rapid focus moves stay unanimated. +3. **Apply craft defaults** from `swiftui-component-author` (critically damped; bounce only after momentum; touch-down press; source-anchored; interruptible; no input lock) and honor Reduce Motion with non-motion feedback. +4. **Keep spatial origin** when presentation origin matters; prefer system transitions over multi-second choreography. +5. **Stop**, or go host-depth: gesture physics (velocity handoff, projection, rubber-banding) → `apple-design`; whole-app opportunity/audit process → `find-animation-opportunities` / `improve-animations` (read-only plans). Do not import glossaries, spring tables, or duration budgets into foundation skills. + +## Host pointers (paths only — not catalog entries) + +These are **not** installed by `./Scripts/install-skills.sh` and are not in `manifest.json`: + +- `~/.agents/skills/apple-design/SKILL.md` +- `~/.agents/skills/find-animation-opportunities/SKILL.md` +- `~/.agents/skills/improve-animations/SKILL.md` +- `~/.agents/skills/animation-vocabulary/SKILL.md` +- `~/.agents/skills/liquid-glass-design/SKILL.md` diff --git a/.agents/skills/swiftui-component-author/SKILL.md b/.agents/skills/swiftui-component-author/SKILL.md index de3bcb6..9bdda16 100644 --- a/.agents/skills/swiftui-component-author/SKILL.md +++ b/.agents/skills/swiftui-component-author/SKILL.md @@ -14,7 +14,7 @@ Create or refactor a **reusable** SwiftUI component in the **consumer workspace* 3. **Prefer native controls.** Use system controls and semantic configuration. Avoid custom chrome that reimplements buttons, lists, or navigation unless the product requires it and accessibility parity is planned. 4. **Own state explicitly.** Prefer initializer parameters and environment injection. Use native observation for shared app state. Do not introduce a view model without a state-ownership or testability reason written in the handoff. 5. **Accessibility baseline (required).** Support Dynamic Type, VoiceOver labels/traits/grouping, keyboard and focus order where the platform has them, sufficient contrast, Reduce Motion, Differentiate Without Color, and a descriptive label for every icon-only control. Touch targets must remain usable (~44×44 pt). At large Dynamic Type sizes, allow scrolling by default and prioritize primary text growth so essential content is not clipped. -6. **Visual craft.** Match SF Symbol weight and scale to adjacent text. Prefer frequency-gated, interruptible motion: critically damped defaults; bounce only after momentum; press feedback on touch-down; source-anchored sheets/menus when origin matters; never lock input during transitions. Prefer system transitions; avoid multi-second decorative choreography and motion that is the only affordance of meaning. Icon-only controls need a label and a usable hit area. +6. **Visual craft.** Match SF Symbol weight and scale to adjacent text. Prefer frequency-gated, interruptible motion: critically damped defaults; bounce only after momentum; press feedback on touch-down; source-anchored sheets/menus when origin matters; never lock input during transitions. Never animate keyboard-initiated or other high-frequency paths (command palettes, shortcuts, rapid focus moves). Custom enter/exit transitions should use the same path. Prefer system transitions; avoid multi-second decorative choreography and motion that is the only affordance of meaning. Icon-only controls need a label and a usable hit area. 7. **Keep the public API small.** Expose only configuration the consumers need. Document defaults and non-obvious parameters. Prefer semantic style enums over raw colors/fonts when tokens exist. 8. **Ship a preview or example** that exercises primary configurations and at least one accessibility-sensitive case (for example large Dynamic Type or an empty state). 9. **Test what the component owns.** Focused tests for configuration logic, state transitions, and public API contracts. Do not claim VoiceOver or visual polish passed without evidence — list manual checks. @@ -28,6 +28,6 @@ Create or refactor a **reusable** SwiftUI component in the **consumer workspace* ## Verification -Use `swift-testing-verification` and XcodeBuildMCP for the affected consumer target. Build the component’s host target; run focused tests. When the component is shared UI, follow with `apple-accessibility-review` before calling reusable work complete. Report residual risk for untested platforms and manual-only a11y checks. +Use `swift-testing-verification` and XcodeBuildMCP for the affected consumer target. Build the component’s host target; run focused tests. When the component is shared UI, follow with `apple-accessibility-review` before calling reusable work complete. If custom motion was added, confirm high-frequency/keyboard paths stay unanimated and Reduce Motion still provides non-motion feedback. Report residual risk for untested platforms and manual-only a11y checks. Inputs: reusable component requirement and consumers. Output: a small documented component, self-contained preview/example, focused tests, and relevant build result. For planning-only requests, provide the component design and verification plan without implementation. diff --git a/.agents/skills/swiftui-tab-navigation/references/liquid-glass-chrome.md b/.agents/skills/swiftui-tab-navigation/references/liquid-glass-chrome.md index 69a9541..21b5fa0 100644 --- a/.agents/skills/swiftui-tab-navigation/references/liquid-glass-chrome.md +++ b/.agents/skills/swiftui-tab-navigation/references/liquid-glass-chrome.md @@ -17,3 +17,5 @@ System navigation chrome owns Liquid Glass. Prefer native `TabView`, toolbars, n - Do not mix Regular and Clear in the same surface hierarchy without a clear media reason. - Do not fake system tab/nav bars with custom `glassEffect`, materials, overlays, or safe-area bars. - Do not keep older custom bar/sheet **opaque backgrounds** that fight scroll-edge / system materials — remove them when adopting system chrome. +- Do not treat glass appearance as a plain opacity fade when customizing materials; prefer system materialization behavior. +- In steady states, avoid large content intersections under glass chrome (rely on system scroll-edge effects; don’t pin opaque blockers that kill blur). diff --git a/AGENTS.md b/AGENTS.md index 11d291d..56237b9 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -35,6 +35,8 @@ Use Swift 6, SwiftUI, SwiftData where appropriate, native observation, initializ All SwiftUI components must support Dynamic Type, VoiceOver, keyboard access, contrast, Reduce Motion, Differentiate Without Color, and a descriptive label for an icon-only control. Validate imports before reading them. Never log credentials, tokens, private content, raw imports, complete sensitive paths, or security-scoped URLs. +- For any SwiftUI UI (including feature screens, not only shared components): keep content opaque; reserve glass for system navigation chrome; ~44×44 pt targets; system text styles (no Ultralight/Thin for UI); selective accent; frequency-gated motion (no animation on high-frequency or keyboard paths). Defer deep gesture physics to host-local motion skills. + ## Verification - Skill or manifest changes: `./Scripts/verify-skills.sh` (and `./Scripts/test-install-skills.sh` if installer behavior changes) From 1b8279932c5f9aeb3c0e82db038fe8ae8e41fe6c Mon Sep 17 00:00:00 2001 From: brbndon Date: Wed, 29 Jul 2026 18:16:51 -0700 Subject: [PATCH 07/21] docs(skills): fix host skill paths and optional Maestro contract - Resolve host-depth skills by ID under the active Codex discovery path instead of hard-coded ~/.agents/skills pointers - Gate the consumer E2E/Maestro section with not configured/n/a for non-iOS and non-Maestro consumers --- .../references/competing-macos-skills-plan.md | 2 +- .../references/design-motion-glass-routing.md | 29 +++++---- .../assets/consumer-AGENTS.md.template | 63 +++++++++++-------- docs/workflow/agents-md-template.mdx | 62 +++++++++++------- 4 files changed, 94 insertions(+), 62 deletions(-) diff --git a/.agents/skills/apple-development-foundation/references/competing-macos-skills-plan.md b/.agents/skills/apple-development-foundation/references/competing-macos-skills-plan.md index 51ac48c..f6db86f 100644 --- a/.agents/skills/apple-development-foundation/references/competing-macos-skills-plan.md +++ b/.agents/skills/apple-development-foundation/references/competing-macos-skills-plan.md @@ -31,7 +31,7 @@ Host-local skills such as `apple-design`, `liquid-glass-design`, `./Scripts/install-skills.sh` unless later promoted. Procedure detail (Liquid Glass steps, animation delete-first refactor, host -paths): [design-motion-glass-routing.md](design-motion-glass-routing.md). +skill IDs and discovery): [design-motion-glass-routing.md](design-motion-glass-routing.md). ## Description / discovery levers (done or next) diff --git a/.agents/skills/apple-development-foundation/references/design-motion-glass-routing.md b/.agents/skills/apple-development-foundation/references/design-motion-glass-routing.md index 05a6677..bd4a77e 100644 --- a/.agents/skills/apple-development-foundation/references/design-motion-glass-routing.md +++ b/.agents/skills/apple-development-foundation/references/design-motion-glass-routing.md @@ -18,11 +18,11 @@ Always-on craft for feature screens also lives in root `Agents.md` / `AGENTS.md` | Intent | Foundation first | Then | Host depth only if needed | | --- | --- | --- | --- | | New app / major skeleton | `$apple-development-foundation` → `$codex-bootstrap` → design-system, tabs, a11y | Chained children | — | -| Liquid Glass / materials | System chrome + `swiftui-tab-navigation` + `liquid-glass-chrome.md` | Custom `glassEffect` only after restraint | `~/.agents/skills/liquid-glass-design/SKILL.md` | -| Animation polish on a screen/component | `swiftui-component-author` craft defaults + a11y Reduce Motion | Delete high-frequency motion first | `~/.agents/skills/apple-design/SKILL.md` (gesture physics) | -| “What should animate?” | Craft defaults + frequency gate (no keyboard / high-frequency paths) | — | `~/.agents/skills/find-animation-opportunities/SKILL.md` | -| Whole-app motion audit | Craft + a11y baselines applied | — | `~/.agents/skills/improve-animations/SKILL.md` (read-only plans) | -| Motion naming / vocabulary | Prefer plain craft language in foundation skills | — | `~/.agents/skills/animation-vocabulary/SKILL.md` | +| Liquid Glass / materials | System chrome + `swiftui-tab-navigation` + `liquid-glass-chrome.md` | Custom `glassEffect` only after restraint | Host skill ID `liquid-glass-design` | +| Animation polish on a screen/component | `swiftui-component-author` craft defaults + a11y Reduce Motion | Delete high-frequency motion first | Host skill ID `apple-design` (gesture physics) | +| “What should animate?” | Craft defaults + frequency gate (no keyboard / high-frequency paths) | — | Host skill ID `find-animation-opportunities` | +| Whole-app motion audit | Craft + a11y baselines applied | — | Host skill ID `improve-animations` (read-only plans) | +| Motion naming / vocabulary | Prefer plain craft language in foundation skills | — | Host skill ID `animation-vocabulary` | Also see [competing-macos-skills-plan.md](competing-macos-skills-plan.md). @@ -41,12 +41,17 @@ Also see [competing-macos-skills-plan.md](competing-macos-skills-plan.md). 4. **Keep spatial origin** when presentation origin matters; prefer system transitions over multi-second choreography. 5. **Stop**, or go host-depth: gesture physics (velocity handoff, projection, rubber-banding) → `apple-design`; whole-app opportunity/audit process → `find-animation-opportunities` / `improve-animations` (read-only plans). Do not import glossaries, spring tables, or duration budgets into foundation skills. -## Host pointers (paths only — not catalog entries) +## Host depth discovery (IDs only — not catalog entries) -These are **not** installed by `./Scripts/install-skills.sh` and are not in `manifest.json`: +These skill IDs are **not** installed by `./Scripts/install-skills.sh` and are not in `manifest.json`. Resolve them through the **active host’s discovery path**, not a hard-coded third-party directory: -- `~/.agents/skills/apple-design/SKILL.md` -- `~/.agents/skills/find-animation-opportunities/SKILL.md` -- `~/.agents/skills/improve-animations/SKILL.md` -- `~/.agents/skills/animation-vocabulary/SKILL.md` -- `~/.agents/skills/liquid-glass-design/SKILL.md` +| Skill ID | Typical depth role | +| --- | --- | +| `apple-design` | Gesture physics, fluid motion depth | +| `find-animation-opportunities` | Opportunity map for missing motion | +| `improve-animations` | Read-only whole-app motion audit plans | +| `animation-vocabulary` | Motion naming / vocabulary | +| `liquid-glass-design` | Liquid Glass API patterns after foundation restraint | + +**Codex (supported):** discover under `${CODEX_HOME:-$HOME/.codex}/skills//` (or invoke `$skill-id` when the host surfaces installed skills). +**Other hosts:** use that host’s documented skill root; do not assume `~/.agents/skills` or any path outside the verified Codex contract. If the skill is not discovered, stay on foundation craft and report the missing host skill instead of inventing a path. diff --git a/.agents/skills/codex-bootstrap/assets/consumer-AGENTS.md.template b/.agents/skills/codex-bootstrap/assets/consumer-AGENTS.md.template index 7146974..6db3c61 100644 --- a/.agents/skills/codex-bootstrap/assets/consumer-AGENTS.md.template +++ b/.agents/skills/codex-bootstrap/assets/consumer-AGENTS.md.template @@ -27,35 +27,48 @@ this contract for files in their scope. ## E2E test contract -Document the deterministic app contract before relying on Maestro journeys. Keep -secrets out of this file, flow files, fixtures, and artifacts. - -- App identifier (bundle ID): `` -- `.maestro/` location and naming: ``; name - flows `.yaml` and reusable subflows `.yaml` using - `` -- Exact simulator/configuration: simulator ID ``, scheme - ``, and build configuration ``; use the same +Optional. Fill this section only when the project uses Maestro (or another UI +E2E tool) for iOS simulator journeys. For macOS-only apps, visionOS/watchOS +targets, Swift packages, or repos without UI E2E, set: + +- Maestro / UI E2E: `not configured` (or `n/a`) + +and leave the remaining fields as `n/a`. Do not invent a simulator ID, `.maestro/` +layout, or onboarding flow when E2E is not in scope. Agents must not treat an +unconfigured contract as an iOS Maestro destination. + +When Maestro/iOS E2E **is** in use, document the deterministic app contract +before relying on journeys. Keep secrets out of this file, flow files, fixtures, +and artifacts. + +- Maestro / UI E2E: `` +- App identifier (bundle ID): `` +- `.maestro/` location and naming: ``; + name flows `.yaml` and reusable subflows `.yaml` using + `` +- Exact simulator/configuration: simulator ID + ``, scheme ``, and build + configuration ``; when configured, use the same destination and configuration for XcodeBuildMCP, Maestro MCP, and CLI runs -- Launch arguments/environment: `` and - ``; inject secrets locally or in CI - rather than committing them -- Reset and seed strategy: `` and - ``; state what persists across relaunch and when app - data, keychain, permissions, or backend state must be reset -- Onboarding state: ``; specify how tests enter - each state and whether onboarding is required, conditional, or a dedicated - subflow -- Fixtures and test accounts: `` and - ``; define ownership, cleanup, and +- Launch arguments/environment: `` and + ``; inject secrets locally or + in CI rather than committing them +- Reset and seed strategy: `` and + ``; state what persists across relaunch and + when app data, keychain, permissions, or backend state must be reset +- Onboarding state: ``; specify how tests + enter each state and whether onboarding is required, conditional, or a + dedicated subflow +- Fixtures and test accounts: `` and + ``; define ownership, cleanup, and account reset behavior -- Deep links: `` -- Artifacts: ``; preserve - failure artifacts while redacting secrets and private user data -- Accessibility-ID conventions: use stable +- Deep links: `` +- Artifacts: ``; + preserve failure artifacts while redacting secrets and private user data +- Accessibility-ID conventions: when E2E is configured, use stable `.` selectors on actionable elements, keep human-readable accessibility labels, and document any native-control - exception + exception; otherwise `n/a` ## Apple verification policy diff --git a/docs/workflow/agents-md-template.mdx b/docs/workflow/agents-md-template.mdx index 387d28d..b247efb 100644 --- a/docs/workflow/agents-md-template.mdx +++ b/docs/workflow/agents-md-template.mdx @@ -33,6 +33,7 @@ Fill every `<…>` placeholder from checked-in project configuration. Do not inv | Destinations | exact `simulatorId`; macOS arch or `n/a` | | Formatter / linter / checks | repo-native commands or `not configured` | | Targeted and full tests | repo-native or MCP test scope | +| E2E / Maestro contract | `configured` with real values, or `not configured` / `n/a` for non-iOS or non-Maestro repos | | Apple verification policy | XcodeBuildMCP CLI and raw `xcodebuild`/`xcrun`/`simctl`: keep `require-approval` unless you authorize `allowed` or `denied` | Scoped `AGENTS.md` files in subdirectories refine this root contract for their trees; keep them when present. @@ -79,35 +80,48 @@ this contract for files in their scope. ## E2E test contract -Document the deterministic app contract before relying on Maestro journeys. Keep -secrets out of this file, flow files, fixtures, and artifacts. +Optional. Fill this section only when the project uses Maestro (or another UI +E2E tool) for iOS simulator journeys. For macOS-only apps, visionOS/watchOS +targets, Swift packages, or repos without UI E2E, set: -- App identifier (bundle ID): `` -- `.maestro/` location and naming: ``; name - flows `.yaml` and reusable subflows `.yaml` using - `` -- Exact simulator/configuration: simulator ID ``, scheme - ``, and build configuration ``; use the same +- Maestro / UI E2E: `not configured` (or `n/a`) + +and leave the remaining fields as `n/a`. Do not invent a simulator ID, `.maestro/` +layout, or onboarding flow when E2E is not in scope. Agents must not treat an +unconfigured contract as an iOS Maestro destination. + +When Maestro/iOS E2E **is** in use, document the deterministic app contract +before relying on journeys. Keep secrets out of this file, flow files, fixtures, +and artifacts. + +- Maestro / UI E2E: `` +- App identifier (bundle ID): `` +- `.maestro/` location and naming: ``; + name flows `.yaml` and reusable subflows `.yaml` using + `` +- Exact simulator/configuration: simulator ID + ``, scheme ``, and build + configuration ``; when configured, use the same destination and configuration for XcodeBuildMCP, Maestro MCP, and CLI runs -- Launch arguments/environment: `` and - ``; inject secrets locally or in CI - rather than committing them -- Reset and seed strategy: `` and - ``; state what persists across relaunch and when app - data, keychain, permissions, or backend state must be reset -- Onboarding state: ``; specify how tests enter - each state and whether onboarding is required, conditional, or a dedicated - subflow -- Fixtures and test accounts: `` and - ``; define ownership, cleanup, and +- Launch arguments/environment: `` and + ``; inject secrets locally or + in CI rather than committing them +- Reset and seed strategy: `` and + ``; state what persists across relaunch and + when app data, keychain, permissions, or backend state must be reset +- Onboarding state: ``; specify how tests + enter each state and whether onboarding is required, conditional, or a + dedicated subflow +- Fixtures and test accounts: `` and + ``; define ownership, cleanup, and account reset behavior -- Deep links: `` -- Artifacts: ``; preserve - failure artifacts while redacting secrets and private user data -- Accessibility-ID conventions: use stable +- Deep links: `` +- Artifacts: ``; + preserve failure artifacts while redacting secrets and private user data +- Accessibility-ID conventions: when E2E is configured, use stable `.` selectors on actionable elements, keep human-readable accessibility labels, and document any native-control - exception + exception; otherwise `n/a` ## Apple verification policy From 18b3670b9cfb8a392f417d42706a3f899bd010dc Mon Sep 17 00:00:00 2001 From: brbndon Date: Wed, 29 Jul 2026 18:57:00 -0700 Subject: [PATCH 08/21] docs(verification): add iOS UI evidence contract - require risk-based screenshot and hierarchy evidence for UI changes\n- provide reusable consumer guidance for deterministic inspection --- .agents/skills/codex-bootstrap/SKILL.md | 15 ++++++++--- .../assets/consumer-AGENTS.md.template | 26 +++++++++++++++---- .../references/bootstrap-checklist.md | 4 ++- .../swift-testing-verification/SKILL.md | 13 ++++++++++ MCP.md | 2 +- 5 files changed, 49 insertions(+), 11 deletions(-) diff --git a/.agents/skills/codex-bootstrap/SKILL.md b/.agents/skills/codex-bootstrap/SKILL.md index 3819a21..8a27ad8 100644 --- a/.agents/skills/codex-bootstrap/SKILL.md +++ b/.agents/skills/codex-bootstrap/SKILL.md @@ -37,8 +37,11 @@ Before creating project files: the file; the non-dry-run form refuses conflicts by default. 4. Customize project/workspace, scheme, configuration, platforms, deployment versions, exact simulator or `.xcodebuildmcp/config.yaml`, repository-native - formatter/linter/test commands, and **Apple verification policy** knobs from - inspected consumer configuration and team preference. The knobs are + formatter/linter/test commands, the **UI inspection contract** for user-visible + iOS/iPadOS work, and **Apple verification policy** knobs from inspected consumer + configuration and team preference. The inspection contract records deterministic + routes or states, safe UI-only launch arguments, a small risk-based evidence + matrix, and artifact handling. The policy knobs are XcodeBuildMCP CLI fallback and repository-native raw `xcodebuild` / `xcrun` / `simctl`, each `require-approval` | `allowed` | `denied`. Template default is `require-approval` for both — keep that unless the user authorizes a different @@ -95,8 +98,12 @@ Use XcodeBuildMCP (see repo `MCP.md`) — not raw `xcodebuild`/`simctl`. Enable `build_run_sim` for launch; before tests, wait for same-project `xcodebuild` / `xctest` / test-runner processes; run `test_sim` without a redundant preceding build and default to - `extraArgs: ["-parallel-testing-enabled", "NO"]`; optional `screenshot` / - `snapshot_ui` (ui-automation workflow; iOS simulator only) + `extraArgs: ["-parallel-testing-enabled", "NO"]`. For user-visible + iOS/iPadOS work, run the consumer's small risk-based evidence matrix: a + primary route by default, plus only affected sheet, appearance, or Dynamic + Type scenarios. For each selected scenario, capture a `screenshot`, inspect + the rendered image, then capture `snapshot_ui` (ui-automation workflow; + iOS simulator only) - **macOS:** use the `macos` workflow — prefer `build_run_macos` for a launch smoke; run `test_macos` when a test target exists (required verification when tests are present). If neither launch smoke nor `test_macos` runs, diff --git a/.agents/skills/codex-bootstrap/assets/consumer-AGENTS.md.template b/.agents/skills/codex-bootstrap/assets/consumer-AGENTS.md.template index 6db3c61..1eb126a 100644 --- a/.agents/skills/codex-bootstrap/assets/consumer-AGENTS.md.template +++ b/.agents/skills/codex-bootstrap/assets/consumer-AGENTS.md.template @@ -25,6 +25,22 @@ this contract for files in their scope. - Targeted tests: `` - Full tests: `` +## UI inspection contract + +For user-visible iOS/iPadOS UI, fill this section even when UI E2E is not +configured. For a project without iOS/iPadOS UI, set each value to `n/a`. + +- Deterministic inspection routes or states: + `` +- UI-only launch arguments/environment (do not include secrets): + `` +- Risk-based evidence matrix: `` +- Evidence procedure: for every selected scenario, use XcodeBuildMCP to capture + a screenshot, visually inspect it, and capture `snapshot_ui`; record manual-only + checks separately +- Artifacts: ``; + preserve failure artifacts while redacting secrets and private user data + ## E2E test contract Optional. Fill this section only when the project uses Maestro (or another UI @@ -50,9 +66,9 @@ and artifacts. ``, scheme ``, and build configuration ``; when configured, use the same destination and configuration for XcodeBuildMCP, Maestro MCP, and CLI runs -- Launch arguments/environment: `` and - ``; inject secrets locally or - in CI rather than committing them +- E2E-only launch arguments/environment beyond the UI inspection contract: + `` and ``; + inject secrets locally or in CI rather than committing them - Reset and seed strategy: `` and ``; state what persists across relaunch and when app data, keychain, permissions, or backend state must be reset @@ -63,8 +79,8 @@ and artifacts. ``; define ownership, cleanup, and account reset behavior - Deep links: `` -- Artifacts: ``; - preserve failure artifacts while redacting secrets and private user data +- E2E artifacts: use the UI inspection contract location and preserve failure + artifacts while redacting secrets and private user data - Accessibility-ID conventions: when E2E is configured, use stable `.` selectors on actionable elements, keep human-readable accessibility labels, and document any native-control diff --git a/.agents/skills/codex-bootstrap/references/bootstrap-checklist.md b/.agents/skills/codex-bootstrap/references/bootstrap-checklist.md index 0c63603..a9c7cc5 100644 --- a/.agents/skills/codex-bootstrap/references/bootstrap-checklist.md +++ b/.agents/skills/codex-bootstrap/references/bootstrap-checklist.md @@ -53,7 +53,9 @@ Use during and after `$codex-bootstrap` in the **consumer workspace**. `test_macos` when tests exist; if neither runs, at least `build_macos` — not `test_sim` / ui-automation - [ ] Tests run serially with parallel testing disabled by default (if present) -- [ ] Optional iOS-only: screenshot or view hierarchy confirms launch +- [ ] User-visible iOS/iPadOS work: inspection contract defines the smallest + risk-based matrix; every selected scenario has a screenshot that was + visually inspected and a `snapshot_ui` capture - [ ] macOS: no screenshot/hierarchy required (ui-automation is iOS-only); if no tests yet, `test_macos` skip and residual risk reported only after `build_run_macos` or `build_macos` succeeds diff --git a/.agents/skills/swift-testing-verification/SKILL.md b/.agents/skills/swift-testing-verification/SKILL.md index d0e59d9..e843ce0 100644 --- a/.agents/skills/swift-testing-verification/SKILL.md +++ b/.agents/skills/swift-testing-verification/SKILL.md @@ -36,3 +36,16 @@ Never infer authorization from shell access or installed binaries. Preserve the same target context and test scope in an authorized fallback. Handoff must include the project/workspace, scheme, configuration, exact destination, commands or tools, outcomes, skipped checks, and residual risk. + +## User-interface evidence + +For a user-visible iOS/iPadOS change, use the consumer's UI inspection contract +after a launch smoke. Choose the smallest risk-based evidence matrix: the primary +affected route by default, plus an affected sheet, appearance variant, or large +Dynamic Type scenario only when that risk changed. For each selected scenario, +capture a screenshot, visually inspect the rendered image, and capture +`snapshot_ui` to verify semantic controls, labels, values, and scrollability. + +Do not apply this matrix to non-UI changes. Do not repeat discovery, stop/launch, +or pre-fix captures after the cause is understood; retain post-fix evidence and +any artifact needed to explain a failure. Report manual-only checks separately. diff --git a/MCP.md b/MCP.md index b541269..92b92b0 100644 --- a/MCP.md +++ b/MCP.md @@ -81,7 +81,7 @@ After `$codex-bootstrap` completes initial structure: 1. `session_show_defaults` — confirm project/workspace, scheme, configuration, and platform destination 2. **iOS:** `build_run_sim` for launch smoke; **macOS:** `build_run_macos` (requires `macos` workflow). If macOS launch smoke and tests are not run, at least `build_macos` 3. Run unit tests if present (`test_sim` / `test_macos`) with parallel testing disabled by default -4. Optional iOS-only: capture a screenshot or view hierarchy (`ui-automation`). macOS has no XcodeBuildMCP UI automation — rely on `test_macos`; if tests are absent, still require `build_run_macos` or at least `build_macos` (do not declare complete on residual risk alone) +4. For user-visible iOS/iPadOS work, run the consumer's small risk-based inspection matrix: capture and visually inspect a screenshot plus `snapshot_ui` for each selected scenario. For non-UI iOS work, screenshot/hierarchy capture remains optional. macOS has no XcodeBuildMCP UI automation — rely on `test_macos`; if tests are absent, still require `build_run_macos` or at least `build_macos` (do not declare complete on residual risk alone) 5. Apply review skills (`swift-concurrency-review`, `apple-security-privacy-review`, `apple-accessibility-review`) before shipping shared components Keep bootstraps focused on reusable skills and clean architecture in the **consumer workspace**. Do not rebuild the archived package in this repo unless asked — see [ARCHIVE.md](ARCHIVE.md). From cdfe7dfc44ff045e3ae19c67138ed29e7c8f8b73 Mon Sep 17 00:00:00 2001 From: brbndon Date: Thu, 30 Jul 2026 10:03:06 -0700 Subject: [PATCH 09/21] docs(foundation): document system-first Liquid Glass guidance - make system navigation chrome the default interpretation of Liquid Glass\n- propagate opaque content and restrained custom-glass rules across foundation skills and workflows\n- verify the skill catalog and static docs build --- .agents/skills/apple-design-system/SKILL.md | 2 +- .../references/design-motion-glass-routing.md | 13 +- .../skills/swiftui-tab-navigation/SKILL.md | 2 +- .../references/liquid-glass-chrome.md | 21 +- AGENTS.md | 1 + docs/workflow/multi-service-ios-app.mdx | 201 ++++++++++++++++++ docs/workflow/task-recipes.mdx | 11 + 7 files changed, 243 insertions(+), 8 deletions(-) create mode 100644 docs/workflow/multi-service-ios-app.mdx diff --git a/.agents/skills/apple-design-system/SKILL.md b/.agents/skills/apple-design-system/SKILL.md index 110c10e..6823be6 100644 --- a/.agents/skills/apple-design-system/SKILL.md +++ b/.agents/skills/apple-design-system/SKILL.md @@ -13,7 +13,7 @@ Change **shared** semantic tokens or design-system components in the **consumer 2. **Semantic tokens before chrome.** Prefer roles (`background`, `label`, `accent`, `danger`, spacing scale, type styles) over raw hex or one-off sizes. Map tokens to light/dark (and increased contrast when the project supports it). 3. **Native first.** Prefer system materials, typography, and controls. Custom tokens should wrap or complement platform defaults, not fight them. 4. **Appearance and motion.** New visual tokens must remain readable in light and dark appearance. Prefer Reduce Motion–safe defaults; avoid decorative motion that is the only affordance of meaning. -5. **Polish defaults.** Content layer stays solid/opaque; reserve glass for system navigation chrome (not content lists, cards, or tables). Use brand/accent for primary actions and content highlights, not full custom chrome. Shared interactive targets must remain usable at ~44×44 pt. Prefer system text styles and Regular/Medium/Semibold/Bold; avoid Ultralight/Thin for UI copy. +5. **Polish defaults.** Content layer stays solid/opaque; reserve Liquid Glass for system navigation/control chrome (not content lists, cards, or tables). A Liquid Glass request means adopting native `TabView`, navigation bars, toolbars, and sheets before considering custom effects; never rebuild that chrome with materials, overlays, or safe-area bars. Use brand/accent for primary actions and content highlights, not full custom chrome. Shared interactive targets must remain usable at ~44×44 pt. Prefer system text styles and Regular/Medium/Semibold/Bold; avoid Ultralight/Thin for UI copy. 6. **Differentiate without color alone.** Status and state must not rely only on hue when color is the sole signal. 7. **Layering.** Tokens feed shared components; app screens consume components/tokens. Do not hardcode magic numbers for shared UI when a token should exist. 8. **Document usage.** Name tokens for role, not for a single marketing campaign or temporary experiment. Note deprecations when replacing tokens. diff --git a/.agents/skills/apple-development-foundation/references/design-motion-glass-routing.md b/.agents/skills/apple-development-foundation/references/design-motion-glass-routing.md index bd4a77e..8f18986 100644 --- a/.agents/skills/apple-development-foundation/references/design-motion-glass-routing.md +++ b/.agents/skills/apple-development-foundation/references/design-motion-glass-routing.md @@ -28,10 +28,15 @@ Also see [competing-macos-skills-plan.md](competing-macos-skills-plan.md). ## Liquid Glass procedure -1. Prefer **system** navigation chrome (`TabView`, toolbars, navigation bars, sheets) — do not imitate with custom glass. -2. Read `swiftui-tab-navigation/references/liquid-glass-chrome.md` and apply Do/Don’t restraint. -3. Use custom `glassEffect` / `GlassEffectContainer` **only** when system chrome cannot express a navigation-plane control, and only after restraint. -4. Host `liquid-glass-design` last — API depth after foundation restraint, never instead of it. +When a user asks for Liquid Glass, interpret that as a request for the platform's +navigation and control appearance, not as a request to decorate the whole app. + +1. Prefer **system** navigation chrome (`TabView`, toolbars, navigation bars, sheets) — do not imitate it with custom glass. +2. Keep the content plane opaque: lists, tables, cards, forms, and feature surfaces are content, not chrome. +3. Read [`liquid-glass-chrome.md`](../../swiftui-tab-navigation/references/liquid-glass-chrome.md) and apply its Do/Don’t restraint. +4. Use custom `glassEffect` / `GlassEffectContainer` **only** when system chrome cannot express a navigation-plane control, and only after restraint. +5. Preserve native accessibility, hit targets, interaction, safe-area behavior, and the supported older-OS fallback. +6. Host `liquid-glass-design` last — API depth after foundation restraint, never instead of it. ## Animation refactor steps diff --git a/.agents/skills/swiftui-tab-navigation/SKILL.md b/.agents/skills/swiftui-tab-navigation/SKILL.md index 5f8970c..7c6ef2e 100644 --- a/.agents/skills/swiftui-tab-navigation/SKILL.md +++ b/.agents/skills/swiftui-tab-navigation/SKILL.md @@ -10,7 +10,7 @@ Build primary navigation from native SwiftUI containers in the **consumer worksp ## Workflow 1. Confirm that the destinations are persistent peers. Use `TabView` for peer app destinations; keep transient actions, onboarding steps, and drill-down destinations out of the tab bar. -2. Prefer the system tab container. On supported iOS versions, native tab chrome adopts the current system appearance, including Liquid Glass on iOS 26. Do not imitate it with `glassEffect`, materials, overlays, or a custom safe-area bar. +2. Prefer the system tab container. On supported iOS versions, native tab chrome adopts the current system appearance, including Liquid Glass on iOS 26. Treat a Liquid Glass request as native chrome adoption; do not imitate it with `glassEffect`, materials, overlays, or a custom safe-area bar. 3. Keep loading, onboarding, unrecoverable failures, and other pre-content routing outside the tab shell. Give each tab its own `NavigationStack` when it owns drill-down navigation. 4. Add typed selection state only when programmatic tab changes, restoration, inspection destinations, or deep links require it. Preserve existing direct inspection and deep-link routes. 5. Use concise text plus SF Symbols in each tab label. Keep selection semantics and interaction native; apply only semantic app tinting (one primary accent, not every bar item). diff --git a/.agents/skills/swiftui-tab-navigation/references/liquid-glass-chrome.md b/.agents/skills/swiftui-tab-navigation/references/liquid-glass-chrome.md index 21b5fa0..274da16 100644 --- a/.agents/skills/swiftui-tab-navigation/references/liquid-glass-chrome.md +++ b/.agents/skills/swiftui-tab-navigation/references/liquid-glass-chrome.md @@ -1,13 +1,30 @@ # Liquid Glass chrome (restraint) -System navigation chrome owns Liquid Glass. Prefer native `TabView`, toolbars, navigation bars, and sheets. +Liquid Glass is a system-managed material for navigation and controls, not an +app-wide surface style. When a user requests Liquid Glass, preserve the +platform's layer hierarchy: + +1. **System chrome:** use native `TabView`, navigation bars, toolbars, and + sheets. On iOS 26 and later, these containers adopt the current system + appearance automatically; let the OS own their shape, material, selection, + safe-area behavior, and interaction. +2. **Content surfaces:** keep lists, tables, cards, forms, and feature content + opaque. Glass should not be used to make ordinary content look like + navigation chrome. +3. **Custom navigation controls:** only when a system container cannot express + the control, use `glassEffect` in the navigation plane. Coordinate multiple + glass elements with `GlassEffectContainer`; do not use custom glass to fake a + tab bar or navigation bar. +4. **Fallbacks and accessibility:** preserve native behavior on older supported + OS versions, Dynamic Type, contrast, VoiceOver, keyboard/pointer access, + Reduce Motion, Differentiate Without Color, and usable hit targets. ## Do - Put glass on the **navigation / control** layer only. - Prefer **system** materials and containers; let the OS adopt Liquid Glass without imitation. - Default material: **Regular**. Use **Clear** only over media, with dimming and bold/legible foreground. -- Custom floating controls in the navigation plane: `glassEffect` **inside** `GlassEffectContainer` only when system chrome cannot express the control. +- Use `.interactive()` only for controls that actually respond to touch or pointer interaction. - Tint **one** primary action (or semantic accent), not every bar item. ## Don’t diff --git a/AGENTS.md b/AGENTS.md index 56237b9..0717df6 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -36,6 +36,7 @@ Use Swift 6, SwiftUI, SwiftData where appropriate, native observation, initializ All SwiftUI components must support Dynamic Type, VoiceOver, keyboard access, contrast, Reduce Motion, Differentiate Without Color, and a descriptive label for an icon-only control. Validate imports before reading them. Never log credentials, tokens, private content, raw imports, complete sensitive paths, or security-scoped URLs. - For any SwiftUI UI (including feature screens, not only shared components): keep content opaque; reserve glass for system navigation chrome; ~44×44 pt targets; system text styles (no Ultralight/Thin for UI); selective accent; frequency-gated motion (no animation on high-frequency or keyboard paths). Defer deep gesture physics to host-local motion skills. +- For Liquid Glass requests, interpret “Liquid Glass” as adopting the OS-owned navigation/control appearance: use native `TabView`, navigation bars, toolbars, and sheets; never imitate system chrome with custom materials or safe-area bars. Use custom `glassEffect` only for a navigation-plane control the system cannot express, with a `GlassEffectContainer` when multiple glass elements must coordinate, and preserve accessibility plus older-OS fallback behavior. ## Verification diff --git a/docs/workflow/multi-service-ios-app.mdx b/docs/workflow/multi-service-ios-app.mdx new file mode 100644 index 0000000..2069b74 --- /dev/null +++ b/docs/workflow/multi-service-ios-app.mdx @@ -0,0 +1,201 @@ +--- +title: Multi-service iOS app patterns +description: Reusable architecture and agent-workflow patterns for LAN/API mesh apps (Keychain, FeatureState, phased integrations, inspection routes)—distilled from production consumer apps such as Harborlight. +--- + +# Multi-service iOS app patterns + +Patterns for **device → many independent HTTP APIs** apps (home-lab dashboards, ops clients, multi-vendor read-only mesh). Neutral and reusable: no product branding, no private hosts, no secrets. + +**Provenance:** distilled from Harborlight-style Phase 2–3 work (Sonarr / Radarr / Plex / qBittorrent-class peers). Apply in the **consumer workspace**, not this foundation’s `archive/`. + +## When to use + +- App talks to **2+ services**, each with its own base URL and credential. +- Prefer **direct device→service** (no hosted BFF) for first release. +- Secrets must stay **off disk-in-UserDefaults** and out of fixtures/logs. +- Agents will implement integrations and UI; humans set product phase boundaries. + +## Phase slice (product) + +Ship capability in **read-only slices** before mutations: + +| Phase | Goal | Avoid | +| --- | --- | --- | +| Foundation | Registry, Keychain refs, transport, probe, one list surface | Writes, search, delete | +| Enrichment | Detail, calendar, artwork, status aggregation | Title-only “joins” across services | +| Download client | Separate client for transfer UI | Using the wrong service for media availability | +| Mutations | Pause/resume, monitor toggles, destructive ops | Shipping controls before read state is trustworthy | + +**Rule:** media-associated “downloading” state often belongs to the **orchestrator** API (e.g. *arr queue), not the torrent client. Full transfer lists belong to the download client. Document the join key early (hash / id) and ban title matching. + +## Architecture sketch + +```text +AppDependencies (@Observable / environment) + ├─ ServiceRegistry (non-secret config → Preferences) + ├─ KeychainStore (secrets by CredentialReference) + ├─ HTTPTransport (mockable; scheme policy) + ├─ ConnectionResolver (local/remote preference + probe) + └─ Repository (aggregate FeatureState per feature) + +ServiceClientFactory → per-kind actor clients (DTO in, domain out) +``` + +### Domain boundary + +1. **Config** — kind, display name, endpoints (scheme/host/port/path), preference, opaque credential reference. +2. **Clients** — private DTOs, auth headers/cookies, path construction including optional `urlBase` / base path. +3. **Domain models** — `Movie`, `TVShow`, `DownloadItem`, `ServiceStatus`, … only types UI should see. +4. **Repository** — fan-out enabled services, merge by **stable ids**, map errors to recovery. + +Do not leak raw JSON or vendor field names into SwiftUI views. + +## Secrets and credentials + +| Store | What | +| --- | --- | +| Preferences / registry | Non-secret config, credential **reference** (UUID account id) | +| Keychain | API keys, tokens, username/password | +| Memory only | Session cookies / SID (re-login on expiry) | +| Never | Secrets in fixtures, launch args, screenshots, logs, sample data | + +**Connection Test:** probe with an **ephemeral** Keychain account so Test never overwrites the live secret on edit. Empty credential field on Save **keeps** the existing Keychain value. + +**Auth mapping:** HTTP 401/403 → unauthorized; missing secret → missing credential; transport failure → unavailable. UI recovery: Retry vs Open Settings / re-enter credentials. + +Never log tokens, passwords, SID, `apikey`, or full private URLs. + +## Transport policy + +- Prefer **HTTPS** for remote endpoints. +- Allow **user-explicit plain HTTP** only where product requires LAN (document which endpoint labels allow it). +- ATS: narrow exception (`NSAllowsLocalNetworking` or equivalent)—**not** `NSAllowsArbitraryLoads`. +- **Never** disable certificate validation to “make it work.” +- Timeouts, cancellation, and a mockable `HTTPTransport` protocol are mandatory for tests. + +## FeatureState (feature-level loading) + +Use a small enum for every remote feature surface: + +```text +loading | content(value, refreshedAt) | empty | stale(value) | unavailable(recovery) | unauthorized | disabled +``` + +- Prefer **one status row per service** (probe OK + content fail must not double-emit connected + failed). +- Partial multi-service success: show connected peers + failed peers without blanking the whole app when any sibling works. +- Recovery actions are explicit: retry, update credentials, configure endpoint, enable service. + +## Artwork + +- Prefer absolute **CDN / remote** URLs when the API provides them. +- Relative media paths need **base URL + auth** (header preferred; query key only if URL-based loaders force it). +- Never log authenticated image URLs. +- List rows use **fixed compact frames** + clip; do not let aspect-ratio posters overflow adjacent cards. +- Cache bytes when scroll causes probe-per-image cost. + +## Cross-service joins + +| Do | Don’t | +| --- | --- | +| Join on vendor ids / hashes (case-insensitive when required) | Match by title string | +| Keep private join fields off ordinary UI | Surface raw hashes as primary labels | +| Document which service owns which badge | Require download-client credentials to show orchestrator queue | + +## System UI first: Liquid Glass + +Liquid Glass is a **navigation/control treatment**, not an app-wide background +style. This rule applies to every SwiftUI app; it is especially important in a +multi-service app, where dense lists, status cards, and partial-failure states +must remain readable. + +1. **Use system chrome first.** Prefer native `TabView`, navigation bars, + toolbars, and sheets. On supported OS versions, let the system provide the + current Liquid Glass appearance, including its material, shape, selection, + safe-area behavior, and interaction. +2. **Keep the content plane opaque.** Lists, tables, cards, forms, and feature + surfaces should remain solid/opaque. Do not apply glass to content merely + because the app uses Liquid Glass in its navigation chrome. +3. **Add custom glass only for a real navigation-plane gap.** A floating action + or control may use `glassEffect` only when a native container cannot express + it; use `GlassEffectContainer` for coordinated glass elements. Never rebuild + a tab bar or navigation bar with materials, overlays, or a custom safe-area + bar. +4. **Preserve the platform contract.** Keep controls around ~44×44 pt, use + system text styles, support Dynamic Type and VoiceOver, maintain contrast and + Differentiate Without Color, and respect Reduce Motion. Keep the native + fallback on older supported OS versions instead of imitating a newer glass + appearance. + +The short version: when a future app request says “use Liquid Glass,” start by +adopting system navigation chrome. Reach for custom glass only after confirming +that system UI cannot express the needed navigation-plane control. + +## Agent handoff shape + +Every multi-step integration handoff should include: + +1. **Do not redo** — already-fixed items (regression surface only). +2. **Severity / priority bands** — P0 trust → P1 next client → P2 auth → P3 peers. +3. **File map** — exact paths. +4. **API truth doc** — prefer a fact-checked agent guide over marketing product specs for endpoints. +5. **Non-goals** — no push, no secrets in fixtures, no write APIs this phase. +6. **Paste prompt** — zero-context instruction for a fresh agent. +7. **Verification gate** — scheme, `test_sim` parallel off, inspection screenshots when UI changes. + +## Inspection contract (deterministic UI) + +For agent-driven UI verification: + +- Launch arguments such as `--inspection ` for loading / empty / unauthorized / primary tabs. +- Optional `--light-appearance`, accessibility Dynamic Type flags. +- For each touched user-visible route: screenshot → **read the image** → `snapshot_ui` hierarchy check. +- Keep inspection routes covered by unit tests when adding critical surfaces. + +## Testing + +| Layer | Practice | +| --- | --- | +| Transport | Stub / path-routing mock; assert scheme policy and status mapping | +| Clients | Decode synthetic fixtures only (`fixture.invalid`, fake ids) | +| Repository | Multi-service partial failure and status dedupe | +| Secrets skim | Grep fixtures for token/apikey/password/private IPs before commit | + +XcodeBuildMCP: `session_show_defaults` once per session; `test_sim` with **parallel testing disabled** for scheme stability; `build_run_sim` for smoke when UI changes. + +## New service client checklist + +1. `ServiceKind` case + registry form fields (correct credential shape). +2. Actor client conforming to shared `ServiceClient` (`probe`). +3. Private request helper: Keychain load + auth headers/cookies + base path. +4. DTO → domain mapping. +5. Register in factory. +6. Fixtures + decode/auth tests. +7. Icon asset (template or brand mark per product policy). +8. No live network in CI. + +## App Store / private LAN note + +Private LAN tools may ship brand marks for personal use; **public App Store** may require trademark care and clearer privacy copy (local network usage, no account backend). Call that out in the product plan—not in fixtures. + +## Related foundation pages + +- [Session workflow](/workflow) — full agent loop +- [Task recipes](/workflow/task-recipes) — scenario shortcuts +- [AGENTS.md template](/workflow/agents-md-template) — consumer always-on rules +- [XcodeBuildMCP](/tools/xcodebuildmcp) — verification ladder +- [Security defaults](/) — never log secrets (see always-on `AGENTS.md` in this repo) + +## Anti-patterns + +- Global TLS disable or arbitrary ATS loads “for Plex” +- Storing API keys next to base URLs in UserDefaults +- Sample Downloads data that looks live after real library data exists +- Title-based matching between download client and library +- Re-researching vendor APIs when a fact-checked agent guide exists +- Expanding this foundation’s `archive/Sources` for app work +- One monocommit of P0–P3 without intermediate green tests + +--- + +*Keep this page product-neutral. Harborlight-specific severity lists and paste prompts live in that app’s `Documentation/`.* diff --git a/docs/workflow/task-recipes.mdx b/docs/workflow/task-recipes.mdx index b4e2a35..b9a3099 100644 --- a/docs/workflow/task-recipes.mdx +++ b/docs/workflow/task-recipes.mdx @@ -36,11 +36,22 @@ Details: [Bootstrap](/skills/bootstrap). | Verify | `$swift-testing-verification` + MCP build/test; reviews if shared UI / async / sensitive paths | | Do not | Plan-for-planning on one-off screens; skip a11y on shared components | +### Multi-service LAN / API mesh integration (read-only phase) + +| Step | Action | +| --- | --- | +| Skills | Security/privacy review mindset (`$apple-security-privacy-review` when touching Keychain/logging); concurrency review if sessions/actors grow; testing verification after clients land | +| Patterns | [Multi-service iOS app patterns](/workflow/multi-service-ios-app) — phase slice, Keychain refs, transport policy, FeatureState, joins, handoff shape | +| Tools | XcodeBuildMCP `test_sim` (parallel off); fixtures only—no live secrets | +| Verify | Decode/auth tests; secrets skim; optional inspection screenshots for UI | +| Do not | Disable TLS validation; title-only cross-service matching; write APIs before read path is solid; invent endpoints when a fact-checked agent guide exists | + ### Author reusable UI (tabs, tokens, components) | Step | Action | | --- | --- | | Skills | `$swiftui-tab-navigation` when the app needs **app-level** peer destinations; `$apple-design-system` for tokens/appearance; `$swiftui-component-author` for reusable components on that system; `$ios-macos-platform-adaptation` only when platforms diverge | +| Liquid Glass | Treat the request as system chrome adoption first: native tabs, navigation bars, toolbars, and sheets; keep content opaque; use custom glass only for a navigation-plane control the system cannot express | | Tools | XcodeBuildMCP for build/preview targets | | Verify | MCP build; `$apple-accessibility-review` on shared UI; `$swift-testing-verification` when logic or public component APIs change | | Do not | Use tab skill for page-style `TabView` content or segmented controls; put branding-specific tokens into foundation skills; land components in this repo’s `archive/Sources/` | From 77aafb24ea7eb96ebc31cfeceddc990a4e362cc0 Mon Sep 17 00:00:00 2001 From: brbndon Date: Thu, 30 Jul 2026 12:36:43 -0700 Subject: [PATCH 10/21] fix(bootstrap): make consumer UI E2E contract tool-neutral - allow Maestro, XCUITest, Appium, or another configured UI E2E runner\n- make all E2E fields n/a when the consumer has no UI E2E\n- keep the public AGENTS template mirror synchronized --- .../assets/consumer-AGENTS.md.template | 38 ++++++++------- docs/workflow/agents-md-template.mdx | 48 ++++++++++--------- 2 files changed, 47 insertions(+), 39 deletions(-) diff --git a/.agents/skills/codex-bootstrap/assets/consumer-AGENTS.md.template b/.agents/skills/codex-bootstrap/assets/consumer-AGENTS.md.template index 1eb126a..78cac2e 100644 --- a/.agents/skills/codex-bootstrap/assets/consumer-AGENTS.md.template +++ b/.agents/skills/codex-bootstrap/assets/consumer-AGENTS.md.template @@ -41,31 +41,35 @@ configured. For a project without iOS/iPadOS UI, set each value to `n/a`. - Artifacts: ``; preserve failure artifacts while redacting secrets and private user data -## E2E test contract +## UI E2E test contract -Optional. Fill this section only when the project uses Maestro (or another UI -E2E tool) for iOS simulator journeys. For macOS-only apps, visionOS/watchOS -targets, Swift packages, or repos without UI E2E, set: +Optional. Fill this section only when the project uses a UI E2E tool for user +journeys. This may be Maestro, XCUITest, Appium, or another configured tool. +For a consumer without UI E2E—whether it is macOS-only, visionOS/watchOS, a +Swift package, or another target—set: -- Maestro / UI E2E: `not configured` (or `n/a`) +- UI E2E tool: `not configured` (or `n/a`) -and leave the remaining fields as `n/a`. Do not invent a simulator ID, `.maestro/` -layout, or onboarding flow when E2E is not in scope. Agents must not treat an -unconfigured contract as an iOS Maestro destination. +and leave the remaining fields as `n/a`. Do not invent a destination, +tool-specific test directory, or onboarding flow when E2E is not in scope. +Agents must not treat an unconfigured contract as an executable UI E2E +destination. -When Maestro/iOS E2E **is** in use, document the deterministic app contract -before relying on journeys. Keep secrets out of this file, flow files, fixtures, -and artifacts. +When UI E2E **is** in use, document the deterministic app contract before +relying on journeys. Keep secrets out of this file, flow files, fixtures, and +artifacts. -- Maestro / UI E2E: `` +- UI E2E tool: `` - App identifier (bundle ID): `` -- `.maestro/` location and naming: ``; - name flows `.yaml` and reusable subflows `.yaml` using +- Test location and naming: ``; + document journeys/tests `` and reusable + subflows/helpers `` using `` -- Exact simulator/configuration: simulator ID - ``, scheme ``, and build +- Exact destination/configuration: destination ID + ``, scheme ``, and build configuration ``; when configured, use the same - destination and configuration for XcodeBuildMCP, Maestro MCP, and CLI runs + destination and configuration for XcodeBuildMCP and the configured UI E2E + runner - E2E-only launch arguments/environment beyond the UI inspection contract: `` and ``; inject secrets locally or in CI rather than committing them diff --git a/docs/workflow/agents-md-template.mdx b/docs/workflow/agents-md-template.mdx index b247efb..ebc7bd4 100644 --- a/docs/workflow/agents-md-template.mdx +++ b/docs/workflow/agents-md-template.mdx @@ -78,34 +78,38 @@ this contract for files in their scope. - Targeted tests: `` - Full tests: `` -## E2E test contract +## UI E2E test contract -Optional. Fill this section only when the project uses Maestro (or another UI -E2E tool) for iOS simulator journeys. For macOS-only apps, visionOS/watchOS -targets, Swift packages, or repos without UI E2E, set: +Optional. Fill this section only when the project uses a UI E2E tool for user +journeys. This may be Maestro, XCUITest, Appium, or another configured tool. +For a consumer without UI E2E—whether it is macOS-only, visionOS/watchOS, a +Swift package, or another target—set: -- Maestro / UI E2E: `not configured` (or `n/a`) +- UI E2E tool: `not configured` (or `n/a`) -and leave the remaining fields as `n/a`. Do not invent a simulator ID, `.maestro/` -layout, or onboarding flow when E2E is not in scope. Agents must not treat an -unconfigured contract as an iOS Maestro destination. +and leave the remaining fields as `n/a`. Do not invent a destination, +tool-specific test directory, or onboarding flow when E2E is not in scope. +Agents must not treat an unconfigured contract as an executable UI E2E +destination. -When Maestro/iOS E2E **is** in use, document the deterministic app contract -before relying on journeys. Keep secrets out of this file, flow files, fixtures, -and artifacts. +When UI E2E **is** in use, document the deterministic app contract before +relying on journeys. Keep secrets out of this file, flow files, fixtures, and +artifacts. -- Maestro / UI E2E: `` +- UI E2E tool: `` - App identifier (bundle ID): `` -- `.maestro/` location and naming: ``; - name flows `.yaml` and reusable subflows `.yaml` using +- Test location and naming: ``; + document journeys/tests `` and reusable + subflows/helpers `` using `` -- Exact simulator/configuration: simulator ID - ``, scheme ``, and build +- Exact destination/configuration: destination ID + ``, scheme ``, and build configuration ``; when configured, use the same - destination and configuration for XcodeBuildMCP, Maestro MCP, and CLI runs -- Launch arguments/environment: `` and - ``; inject secrets locally or - in CI rather than committing them + destination and configuration for XcodeBuildMCP and the configured UI E2E + runner +- E2E-only launch arguments/environment beyond the UI inspection contract: + `` and ``; + inject secrets locally or in CI rather than committing them - Reset and seed strategy: `` and ``; state what persists across relaunch and when app data, keychain, permissions, or backend state must be reset @@ -116,8 +120,8 @@ and artifacts. ``; define ownership, cleanup, and account reset behavior - Deep links: `` -- Artifacts: ``; - preserve failure artifacts while redacting secrets and private user data +- E2E artifacts: use the UI inspection contract location and preserve failure + artifacts while redacting secrets and private user data - Accessibility-ID conventions: when E2E is configured, use stable `.` selectors on actionable elements, keep human-readable accessibility labels, and document any native-control From 8a755e5d247e9a46d55fe991a27a7cedf71aac44 Mon Sep 17 00:00:00 2001 From: brbndon Date: Thu, 30 Jul 2026 14:53:29 -0700 Subject: [PATCH 11/21] fix(workflow): sync AGENTS template inspection contract - include the UI inspection contract in the website copy-paste template\n- describe configured UI E2E tools without classifying non-Maestro runners as unconfigured --- docs/workflow/agents-md-template.mdx | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/docs/workflow/agents-md-template.mdx b/docs/workflow/agents-md-template.mdx index ebc7bd4..b3f3a9a 100644 --- a/docs/workflow/agents-md-template.mdx +++ b/docs/workflow/agents-md-template.mdx @@ -33,7 +33,7 @@ Fill every `<…>` placeholder from checked-in project configuration. Do not inv | Destinations | exact `simulatorId`; macOS arch or `n/a` | | Formatter / linter / checks | repo-native commands or `not configured` | | Targeted and full tests | repo-native or MCP test scope | -| E2E / Maestro contract | `configured` with real values, or `not configured` / `n/a` for non-iOS or non-Maestro repos | +| UI E2E contract | `configured` with the selected tool and real values, or `not configured` / `n/a` when UI E2E is not in scope | | Apple verification policy | XcodeBuildMCP CLI and raw `xcodebuild`/`xcrun`/`simctl`: keep `require-approval` unless you authorize `allowed` or `denied` | Scoped `AGENTS.md` files in subdirectories refine this root contract for their trees; keep them when present. @@ -78,6 +78,22 @@ this contract for files in their scope. - Targeted tests: `` - Full tests: `` +## UI inspection contract + +For user-visible iOS/iPadOS UI, fill this section even when UI E2E is not +configured. For a project without iOS/iPadOS UI, set each value to `n/a`. + +- Deterministic inspection routes or states: + `` +- UI-only launch arguments/environment (do not include secrets): + `` +- Risk-based evidence matrix: `` +- Evidence procedure: for every selected scenario, use XcodeBuildMCP to capture + a screenshot, visually inspect it, and capture `snapshot_ui`; record manual-only + checks separately +- Artifacts: ``; + preserve failure artifacts while redacting secrets and private user data + ## UI E2E test contract Optional. Fill this section only when the project uses a UI E2E tool for user From 57e7746393d5156e13ec6b8c826287c3c445a0da Mon Sep 17 00:00:00 2001 From: brbndon Date: Thu, 30 Jul 2026 15:15:43 -0700 Subject: [PATCH 12/21] docs(quality): neutralize playbook and cover UI evidence boundaries - remove consumer and vendor-specific assumptions from the multi-service workflow\n- add UI-positive and non-UI verification fixtures with enforced boundary coverage\n- update evaluation documentation for the expanded framework boundaries --- Evaluations/skill-routing.json | 37 +++++++++++++++++++ Scripts/validate-skill-evaluations.py | 5 +++ docs/skills/skill-evaluation.mdx | 6 ++-- docs/workflow/multi-service-ios-app.mdx | 48 +++++++++++++------------ 4 files changed, 70 insertions(+), 26 deletions(-) diff --git a/Evaluations/skill-routing.json b/Evaluations/skill-routing.json index 25c54ba..cc241c4 100644 --- a/Evaluations/skill-routing.json +++ b/Evaluations/skill-routing.json @@ -1129,6 +1129,24 @@ "report_residual_risk": true } } + }, + { + "id": "testing-use-ios-ui-evidence-contract", + "prompt": "Verify a completed user-visible iOS/iPadOS UI change with the consumer's UI inspection contract: run a launch smoke, capture and visually inspect a screenshot for the primary affected route, then capture snapshot_ui and report any manual-only checks.", + "expected": { + "skill_ids": [ + "swift-testing-verification" + ], + "workspace_classification": "consumer-workspace", + "implementation_authorized": false, + "audit_expected": false, + "verification_category": "apple-ui-e2e", + "stop_condition": "none", + "handoff": { + "report_exact_checks": true, + "report_residual_risk": true + } + } } ], "should_not_use": [ @@ -1464,6 +1482,25 @@ "report_residual_risk": true } } + }, + { + "boundary_id": "non-ui-verification-no-ui-evidence", + "id": "boundary-non-ui-verification-no-ui-evidence", + "prompt": "Verify a completed non-UI networking change in a consumer app. Run focused tests and report exact outcomes; do not apply the UI inspection contract or capture screenshot/snapshot_ui evidence.", + "expected": { + "skill_ids": [ + "swift-testing-verification" + ], + "workspace_classification": "consumer-workspace", + "implementation_authorized": false, + "audit_expected": false, + "verification_category": "apple-code", + "stop_condition": "none", + "handoff": { + "report_exact_checks": true, + "report_residual_risk": true + } + } } ] } diff --git a/Scripts/validate-skill-evaluations.py b/Scripts/validate-skill-evaluations.py index 27c55aa..25a327f 100755 --- a/Scripts/validate-skill-evaluations.py +++ b/Scripts/validate-skill-evaluations.py @@ -54,6 +54,7 @@ "planning-only-no-implementation", "installer-conflicts-preserved", "handoff-exact-checks-and-risk", + "non-ui-verification-no-ui-evidence", } BOUNDARY_REQUIREMENTS = { "ordinary-routing-no-audit": { @@ -79,6 +80,10 @@ "verification_category": "installer-behavior", "stop_condition": "preserve-conflict", }, + "non-ui-verification-no-ui-evidence": { + "verification_category": "apple-code", + "stop_condition": "none", + }, } EXPECTED_FIELDS = { "skill_ids", diff --git a/docs/skills/skill-evaluation.mdx b/docs/skills/skill-evaluation.mdx index 9cf3709..4eb3afb 100644 --- a/docs/skills/skill-evaluation.mdx +++ b/docs/skills/skill-evaluation.mdx @@ -26,16 +26,16 @@ The scope below describes where a skill performs its work. The manifest's `repos | `swift-concurrency-review` | “Audit these actors for Sendable errors.”; “Review cancellation in this image loader.” | “Change button colors.”; “Design synchronous routing APIs.” | Overlaps testing verification; use `$swift-concurrency-review` for isolation/task review. | Consumer workspace | | `apple-accessibility-review` | “Audit this reusable SwiftUI control for VoiceOver.”; “Check macOS keyboard access in this component.” | “Review a file parser.”; “Choose spacing token names.” | Overlaps component/design work; use `$apple-accessibility-review` for accessibility findings. | Consumer workspace | | `apple-security-privacy-review` | “Review this import flow for path traversal.”; “Audit logging privacy.” | “Polish this empty state.”; “Rename a generic protocol.” | Overlaps concurrency/file implementation; use `$apple-security-privacy-review` for sensitive-data boundaries. | Consumer workspace | -| `swift-testing-verification` | “Run the right checks after this public SwiftUI API change.”; “Verify this SwiftData fix.” | “Plan test coverage before coding.”; “Write a feature plan.” | Overlaps all implementation skills; use `$swift-testing-verification` after a change. | Consumer workspace | +| `swift-testing-verification` | “Run the right checks after this public SwiftUI API change.”; “Verify a user-visible iOS/iPadOS change with its UI inspection contract.” | “Plan test coverage before coding.”; “Write a feature plan.” | Overlaps all implementation skills; use `$swift-testing-verification` after a change. | Consumer workspace | | `maestro-apple-app-testing` | “Author a Maestro regression flow for onboarding.”; “Debug this flaky iOS UI test.” | “Implement the onboarding screen.”; “Run standalone unit tests.” | Overlaps testing verification; use `$maestro-apple-app-testing` for end-to-end UI workflows. | Consumer workspace | | `codex-skill-maintainer` | “Review these local SKILL.md files.”; “Split an ambiguous repository skill.” | “Implement app validation.”; “Install skills into my global Codex directory.” | Overlaps all skills only at metadata level; use `$codex-skill-maintainer` for `.agents/skills` work. | Foundation repository | | `codex-bootstrap` | “Bootstrap a new consumer iOS SwiftUI app.”; “Set up a shared iOS/macOS app skeleton.” | “Build one settings screen.”; “Expand this repo’s archived package.” | Overlaps planning and component author; use `$codex-bootstrap` for consumer-project setup and chaining. | Consumer workspace | ## Framework boundaries -The fixture set also requires cases proving that ordinary routing does not imply a foundation audit, consumer work does not target archived foundation code, review-only and planning-only prompts do not authorize edits, unavailable XcodeBuildMCP follows the capability policy, installer conflicts are preserved, and verification handoff reports exact checks plus residual risk. +The fixture set also requires cases proving that ordinary routing does not imply a foundation audit, consumer work does not target archived foundation code, review-only and planning-only prompts do not authorize edits, unavailable XcodeBuildMCP follows the capability policy, installer conflicts are preserved, user-visible iOS/iPadOS verification uses its UI evidence contract, non-UI verification does not, and verification handoff reports exact checks plus residual risk. -The current evaluation covers all 15 manifest skills and 7 required framework boundaries. The human-readable table above is a routing summary; the JSON fixtures are the validation source of truth. The principal remaining ambiguity is intentional composition: the master may shortlist tab navigation, component author, accessibility review, and verification in sequence. +The current evaluation covers all manifest skills and 8 required framework boundaries. The human-readable table above is a routing summary; the JSON fixtures are the validation source of truth. The principal remaining ambiguity is intentional composition: the master may shortlist tab navigation, component author, accessibility review, and verification in sequence. ## What still requires model or manual evaluation diff --git a/docs/workflow/multi-service-ios-app.mdx b/docs/workflow/multi-service-ios-app.mdx index 2069b74..c815c54 100644 --- a/docs/workflow/multi-service-ios-app.mdx +++ b/docs/workflow/multi-service-ios-app.mdx @@ -1,13 +1,13 @@ --- title: Multi-service iOS app patterns -description: Reusable architecture and agent-workflow patterns for LAN/API mesh apps (Keychain, FeatureState, phased integrations, inspection routes)—distilled from production consumer apps such as Harborlight. +description: Reusable architecture and agent-workflow patterns for device-to-many HTTP API apps (Keychain, FeatureState, phased integrations, inspection routes). --- # Multi-service iOS app patterns -Patterns for **device → many independent HTTP APIs** apps (home-lab dashboards, ops clients, multi-vendor read-only mesh). Neutral and reusable: no product branding, no private hosts, no secrets. +Patterns for **device → many independent HTTP APIs** apps (dashboards, admin clients, multi-service workflows). Neutral and reusable: no product branding, no private hosts, and no secrets. -**Provenance:** distilled from Harborlight-style Phase 2–3 work (Sonarr / Radarr / Plex / qBittorrent-class peers). Apply in the **consumer workspace**, not this foundation’s `archive/`. +Distilled from production multi-service consumer apps with independent service peers. Apply in the **consumer workspace**, not this foundation’s `archive/`. ## When to use @@ -16,18 +16,18 @@ Patterns for **device → many independent HTTP APIs** apps (home-lab dashboards - Secrets must stay **off disk-in-UserDefaults** and out of fixtures/logs. - Agents will implement integrations and UI; humans set product phase boundaries. -## Phase slice (product) +## Phase slice Ship capability in **read-only slices** before mutations: | Phase | Goal | Avoid | | --- | --- | --- | | Foundation | Registry, Keychain refs, transport, probe, one list surface | Writes, search, delete | -| Enrichment | Detail, calendar, artwork, status aggregation | Title-only “joins” across services | -| Download client | Separate client for transfer UI | Using the wrong service for media availability | +| Enrichment | Detail, related resources, remote assets, status aggregation | Display-name “joins” across services | +| Transfer client | Separate client for transfer UI | Using the wrong service for resource availability | | Mutations | Pause/resume, monitor toggles, destructive ops | Shipping controls before read state is trustworthy | -**Rule:** media-associated “downloading” state often belongs to the **orchestrator** API (e.g. *arr queue), not the torrent client. Full transfer lists belong to the download client. Document the join key early (hash / id) and ban title matching. +**Rule:** resource-associated “in progress” state often belongs to the **orchestrator** API, not the transfer client. Full transfer lists belong to the transfer client. Document the join key early (stable id / hash) and ban display-name matching. ## Architecture sketch @@ -46,10 +46,10 @@ ServiceClientFactory → per-kind actor clients (DTO in, domain out) 1. **Config** — kind, display name, endpoints (scheme/host/port/path), preference, opaque credential reference. 2. **Clients** — private DTOs, auth headers/cookies, path construction including optional `urlBase` / base path. -3. **Domain models** — `Movie`, `TVShow`, `DownloadItem`, `ServiceStatus`, … only types UI should see. +3. **Domain models** — `Resource`, `CollectionItem`, `TransferItem`, `ServiceStatus`, … only types UI should see. 4. **Repository** — fan-out enabled services, merge by **stable ids**, map errors to recovery. -Do not leak raw JSON or vendor field names into SwiftUI views. +Do not leak raw JSON or service field names into SwiftUI views. ## Secrets and credentials @@ -86,21 +86,21 @@ loading | content(value, refreshedAt) | empty | stale(value) | unavailable(recov - Partial multi-service success: show connected peers + failed peers without blanking the whole app when any sibling works. - Recovery actions are explicit: retry, update credentials, configure endpoint, enable service. -## Artwork +## Remote assets - Prefer absolute **CDN / remote** URLs when the API provides them. -- Relative media paths need **base URL + auth** (header preferred; query key only if URL-based loaders force it). -- Never log authenticated image URLs. -- List rows use **fixed compact frames** + clip; do not let aspect-ratio posters overflow adjacent cards. +- Relative asset paths need **base URL + auth** (header preferred; query key only if URL-based loaders force it). +- Never log authenticated asset URLs. +- List rows use **fixed compact frames** + clip; do not let variable-aspect-ratio assets overflow adjacent cards. - Cache bytes when scroll causes probe-per-image cost. ## Cross-service joins | Do | Don’t | | --- | --- | -| Join on vendor ids / hashes (case-insensitive when required) | Match by title string | +| Join on service ids / hashes (case-insensitive when required) | Match by display name | | Keep private join fields off ordinary UI | Surface raw hashes as primary labels | -| Document which service owns which badge | Require download-client credentials to show orchestrator queue | +| Document which service owns each status or badge | Require one service’s credentials to show another service’s private state | ## System UI first: Liquid Glass @@ -171,12 +171,14 @@ XcodeBuildMCP: `session_show_defaults` once per session; `test_sim` with **paral 4. DTO → domain mapping. 5. Register in factory. 6. Fixtures + decode/auth tests. -7. Icon asset (template or brand mark per product policy). +7. Icon asset (template or app-provided mark per product policy). 8. No live network in CI. -## App Store / private LAN note +## Distribution and privacy note -Private LAN tools may ship brand marks for personal use; **public App Store** may require trademark care and clearer privacy copy (local network usage, no account backend). Call that out in the product plan—not in fixtures. +If this pattern ships publicly, document endpoint security, local-network usage, +and data handling in the product and privacy plan. Keep product-specific policy +out of fixtures. ## Related foundation pages @@ -188,14 +190,14 @@ Private LAN tools may ship brand marks for personal use; **public App Store** ma ## Anti-patterns -- Global TLS disable or arbitrary ATS loads “for Plex” +- Global TLS disable or arbitrary ATS loads for one service - Storing API keys next to base URLs in UserDefaults -- Sample Downloads data that looks live after real library data exists -- Title-based matching between download client and library -- Re-researching vendor APIs when a fact-checked agent guide exists +- Sample records that look live after real service data exists +- Display-name matching between services +- Re-researching service APIs when a fact-checked agent guide exists - Expanding this foundation’s `archive/Sources` for app work - One monocommit of P0–P3 without intermediate green tests --- -*Keep this page product-neutral. Harborlight-specific severity lists and paste prompts live in that app’s `Documentation/`.* +*Keep this page product-neutral. App-specific severity lists and paste prompts live in the consumer app’s `Documentation/`.* From 34c98f437524cf79e1f25a9d693c5aba95923c88 Mon Sep 17 00:00:00 2001 From: brbndon Date: Thu, 30 Jul 2026 15:49:56 -0700 Subject: [PATCH 13/21] fix(audit): close reusable guidance findings - enforce UI and non-UI verification evidence boundaries\n- harden neutral multi-service security guidance\n- keep the website AGENTS template synchronized --- .../swift-testing-verification/SKILL.md | 8 +- Evaluations/README.md | 6 + Evaluations/schema.json | 39 ++++++- Evaluations/skill-routing.json | 54 +++++++++ Scripts/test-consumer-guidance.sh | 19 +++ Scripts/validate-skill-evaluations.py | 110 +++++++++++++++++- docs/skills/skill-evaluation.mdx | 4 +- docs/workflow/multi-service-ios-app.mdx | 40 +++++-- 8 files changed, 260 insertions(+), 20 deletions(-) diff --git a/.agents/skills/swift-testing-verification/SKILL.md b/.agents/skills/swift-testing-verification/SKILL.md index e843ce0..40ff312 100644 --- a/.agents/skills/swift-testing-verification/SKILL.md +++ b/.agents/skills/swift-testing-verification/SKILL.md @@ -46,6 +46,8 @@ Dynamic Type scenario only when that risk changed. For each selected scenario, capture a screenshot, visually inspect the rendered image, and capture `snapshot_ui` to verify semantic controls, labels, values, and scrollability. -Do not apply this matrix to non-UI changes. Do not repeat discovery, stop/launch, -or pre-fix captures after the cause is understood; retain post-fix evidence and -any artifact needed to explain a failure. Report manual-only checks separately. +For a non-UI change, run focused code tests only; do not require a launch smoke, +screenshots, visual inspection, or `snapshot_ui`. Do not repeat discovery, +stop/launch, or pre-fix captures after the cause is understood; retain post-fix +evidence and any artifact needed to explain a failure. Report manual-only checks +separately for user-visible changes. diff --git a/Evaluations/README.md b/Evaluations/README.md index 12a0714..4e5d73b 100644 --- a/Evaluations/README.md +++ b/Evaluations/README.md @@ -15,6 +15,12 @@ fixtures. Every prompt records: - the required verification category and stop condition; and - whether handoff must report exact checks and residual risk. +UI-verification boundaries additionally encode the required and forbidden +evidence actions. A user-visible iOS/iPadOS change requires its inspection +contract (launch smoke, screenshot and visual inspection, `snapshot_ui`, and +manual-only checks); a non-UI change requires focused code tests only and +forbids that UI evidence. + An empty `skill_ids` array means that no skill in this repository's catalog is appropriate for that prompt; it is still an explicit routing expectation. diff --git a/Evaluations/schema.json b/Evaluations/schema.json index 9d8024d..e2e3f0c 100644 --- a/Evaluations/schema.json +++ b/Evaluations/schema.json @@ -22,7 +22,7 @@ }, "framework_boundaries": { "type": "array", - "minItems": 7, + "minItems": 9, "items": { "$ref": "#/$defs/boundaryCase" } } }, @@ -126,6 +126,43 @@ "report_exact_checks": { "type": "boolean" }, "report_residual_risk": { "type": "boolean" } } + }, + "ui_evidence": { + "type": "object", + "required": ["scope", "required_actions", "forbidden_actions"], + "additionalProperties": false, + "properties": { + "scope": { + "enum": ["ui-inspection-contract", "focused-code-tests-only"] + }, + "required_actions": { + "type": "array", + "uniqueItems": true, + "items": { + "enum": [ + "focused-code-tests", + "launch-smoke", + "screenshot", + "visual-inspection", + "snapshot-ui", + "manual-only-checks" + ] + } + }, + "forbidden_actions": { + "type": "array", + "uniqueItems": true, + "items": { + "enum": [ + "launch-smoke", + "screenshot", + "visual-inspection", + "snapshot-ui", + "manual-only-checks" + ] + } + } + } } } }, diff --git a/Evaluations/skill-routing.json b/Evaluations/skill-routing.json index cc241c4..faf8954 100644 --- a/Evaluations/skill-routing.json +++ b/Evaluations/skill-routing.json @@ -1142,6 +1142,17 @@ "audit_expected": false, "verification_category": "apple-ui-e2e", "stop_condition": "none", + "ui_evidence": { + "scope": "ui-inspection-contract", + "required_actions": [ + "launch-smoke", + "screenshot", + "visual-inspection", + "snapshot-ui", + "manual-only-checks" + ], + "forbidden_actions": [] + }, "handoff": { "report_exact_checks": true, "report_residual_risk": true @@ -1483,6 +1494,36 @@ } } }, + { + "boundary_id": "user-visible-ui-verification-evidence", + "id": "boundary-user-visible-ui-verification-evidence", + "prompt": "Verify a completed user-visible iOS/iPadOS change. Use the consumer UI inspection contract: run a launch smoke, capture and visually inspect a screenshot for the affected route, capture snapshot_ui, and report manual-only checks.", + "expected": { + "skill_ids": [ + "swift-testing-verification" + ], + "workspace_classification": "consumer-workspace", + "implementation_authorized": false, + "audit_expected": false, + "verification_category": "apple-ui-e2e", + "stop_condition": "none", + "ui_evidence": { + "scope": "ui-inspection-contract", + "required_actions": [ + "launch-smoke", + "screenshot", + "visual-inspection", + "snapshot-ui", + "manual-only-checks" + ], + "forbidden_actions": [] + }, + "handoff": { + "report_exact_checks": true, + "report_residual_risk": true + } + } + }, { "boundary_id": "non-ui-verification-no-ui-evidence", "id": "boundary-non-ui-verification-no-ui-evidence", @@ -1496,6 +1537,19 @@ "audit_expected": false, "verification_category": "apple-code", "stop_condition": "none", + "ui_evidence": { + "scope": "focused-code-tests-only", + "required_actions": [ + "focused-code-tests" + ], + "forbidden_actions": [ + "launch-smoke", + "screenshot", + "visual-inspection", + "snapshot-ui", + "manual-only-checks" + ] + }, "handoff": { "report_exact_checks": true, "report_residual_risk": true diff --git a/Scripts/test-consumer-guidance.sh b/Scripts/test-consumer-guidance.sh index 961509f..1c3ba0f 100755 --- a/Scripts/test-consumer-guidance.sh +++ b/Scripts/test-consumer-guidance.sh @@ -42,6 +42,25 @@ assert_apple_verification_policy() { assert_apple_verification_policy "$template" +# The website's copyable template must remain an exact mirror of the canonical +# bootstrap asset. Extract only its markdown fence, not incidental shell fences. +website_template="$scratch/website-consumer-AGENTS.md" +if ! awk ' + $0 == "```markdown" { + if (found || inside) exit 1 + found = 1 + inside = 1 + next + } + inside && $0 == "```" { inside = 0; exit } + inside { print } + END { if (found != 1 || inside) exit 1 } +' "$root/docs/workflow/agents-md-template.mdx" > "$website_template"; then + fail "could not extract the website AGENTS.md template" +fi +cmp -s "$template" "$website_template" \ + || fail "website AGENTS.md template differs from the canonical asset" + consumer="$scratch/Consumer App With Spaces" mkdir -p "$consumer" consumer="$(cd "$consumer" && pwd -P)" diff --git a/Scripts/validate-skill-evaluations.py b/Scripts/validate-skill-evaluations.py index 25a327f..39676e4 100755 --- a/Scripts/validate-skill-evaluations.py +++ b/Scripts/validate-skill-evaluations.py @@ -54,6 +54,7 @@ "planning-only-no-implementation", "installer-conflicts-preserved", "handoff-exact-checks-and-risk", + "user-visible-ui-verification-evidence", "non-ui-verification-no-ui-evidence", } BOUNDARY_REQUIREMENTS = { @@ -85,6 +86,41 @@ "stop_condition": "none", }, } +UI_EVIDENCE_ACTIONS = { + "focused-code-tests", + "launch-smoke", + "screenshot", + "visual-inspection", + "snapshot-ui", + "manual-only-checks", +} +UI_EVIDENCE_SCOPES = {"ui-inspection-contract", "focused-code-tests-only"} +UI_EVIDENCE_FIELDS = {"scope", "required_actions", "forbidden_actions"} +UI_EVIDENCE_FORBIDDEN_ACTIONS = UI_EVIDENCE_ACTIONS - {"focused-code-tests"} +UI_EVIDENCE_REQUIREMENTS = { + "user-visible-ui-verification-evidence": { + "scope": "ui-inspection-contract", + "required_actions": [ + "launch-smoke", + "screenshot", + "visual-inspection", + "snapshot-ui", + "manual-only-checks", + ], + "forbidden_actions": [], + }, + "non-ui-verification-no-ui-evidence": { + "scope": "focused-code-tests-only", + "required_actions": ["focused-code-tests"], + "forbidden_actions": [ + "launch-smoke", + "screenshot", + "visual-inspection", + "snapshot-ui", + "manual-only-checks", + ], + }, +} EXPECTED_FIELDS = { "skill_ids", "workspace_classification", @@ -107,11 +143,17 @@ def load_json(path: Path, label: str) -> Any: raise EvaluationError(f"cannot read {label} {path}: {error}") from error -def require_object(value: Any, location: str, fields: set[str]) -> dict[str, Any]: +def require_object( + value: Any, + location: str, + fields: set[str], + optional_fields: set[str] | None = None, +) -> dict[str, Any]: if not isinstance(value, dict): raise EvaluationError(f"{location} must be an object") + optional_fields = optional_fields or set() missing = sorted(fields - value.keys()) - extra = sorted(value.keys() - fields) + extra = sorted(value.keys() - fields - optional_fields) if missing or extra: raise EvaluationError( f"{location} fields differ; missing: {', '.join(missing) or 'none'}; " @@ -126,8 +168,28 @@ def require_identifier(value: Any, location: str) -> str: return value +def validate_ui_evidence(value: Any, location: str) -> None: + evidence = require_object( + value, + location, + UI_EVIDENCE_FIELDS, + ) + if evidence["scope"] not in UI_EVIDENCE_SCOPES: + raise EvaluationError(f"{location}.scope is invalid") + for field in ("required_actions", "forbidden_actions"): + actions = evidence[field] + if not isinstance(actions, list) or not all( + isinstance(action, str) for action in actions + ): + raise EvaluationError(f"{location}.{field} must be a string array") + if len(actions) != len(set(actions)): + raise EvaluationError(f"{location}.{field} must be a unique array") + if not all(action in UI_EVIDENCE_ACTIONS for action in actions): + raise EvaluationError(f"{location}.{field} contains an invalid action") + + def validate_expectation(value: Any, location: str, skill_ids: set[str]) -> None: - expectation = require_object(value, location, EXPECTED_FIELDS) + expectation = require_object(value, location, EXPECTED_FIELDS, {"ui_evidence"}) expected_skills = expectation["skill_ids"] if not isinstance(expected_skills, list): raise EvaluationError(f"{location}.skill_ids must be an array") @@ -157,6 +219,8 @@ def validate_expectation(value: Any, location: str, skill_ids: set[str]) -> None for field, field_value in handoff.items(): if not isinstance(field_value, bool): raise EvaluationError(f"{location}.handoff.{field} must be boolean") + if "ui_evidence" in expectation: + validate_ui_evidence(expectation["ui_evidence"], f"{location}.ui_evidence") def validate_prompt_case( @@ -190,9 +254,28 @@ def validate(fixtures_path: Path) -> tuple[int, int]: raise EvaluationError("Evaluations/schema.json does not document schema version 1") if schema_properties.get("activation_claim", {}).get("const") != ACTIVATION_CLAIM: raise EvaluationError("Evaluations/schema.json activation limitation has drifted") + if schema_properties.get("framework_boundaries", {}).get("minItems") != len( + REQUIRED_BOUNDARIES + ): + raise EvaluationError("Evaluations/schema.json framework boundary count has drifted") if set(schema_expectation.get("required", [])) != EXPECTED_FIELDS: raise EvaluationError("Evaluations/schema.json expected fields have drifted") schema_expectation_properties = schema_expectation.get("properties", {}) + schema_ui_evidence = schema_expectation_properties.get("ui_evidence", {}) + if set(schema_ui_evidence.get("required", [])) != UI_EVIDENCE_FIELDS: + raise EvaluationError("Evaluations/schema.json UI evidence fields have drifted") + schema_ui_evidence_properties = schema_ui_evidence.get("properties", {}) + if set(schema_ui_evidence_properties.get("scope", {}).get("enum", [])) != UI_EVIDENCE_SCOPES: + raise EvaluationError("Evaluations/schema.json UI evidence scopes have drifted") + for field, allowed_actions in { + "required_actions": UI_EVIDENCE_ACTIONS, + "forbidden_actions": UI_EVIDENCE_FORBIDDEN_ACTIONS, + }.items(): + documented_actions = schema_ui_evidence_properties.get(field, {}).get("items", {}).get( + "enum", [] + ) + if set(documented_actions) != allowed_actions: + raise EvaluationError(f"Evaluations/schema.json {field} values have drifted") documented_enums = { "workspace_classification": WORKSPACES, "verification_category": VERIFICATION_CATEGORIES, @@ -231,8 +314,9 @@ def validate(fixtures_path: Path) -> tuple[int, int]: covered_skills.add(skill_id) for kind in ("should_use", "should_not_use"): cases = entry[kind] - if not isinstance(cases, list) or len(cases) < 2: - raise EvaluationError(f"{location}.{kind} must contain at least two cases") + minimum = 3 if skill_id == "swift-testing-verification" and kind == "should_use" else 2 + if not isinstance(cases, list) or len(cases) < minimum: + raise EvaluationError(f"{location}.{kind} must contain at least {minimum} cases") for case_index, case_value in enumerate(cases): case_location = f"{location}.{kind}[{case_index}]" case = validate_prompt_case(case_value, case_location, skill_ids, seen_case_ids) @@ -246,6 +330,17 @@ def validate(fixtures_path: Path) -> tuple[int, int]: f"{case_location} must exclude its subject skill {skill_id}" ) prompt_count += 1 + if skill_id == "swift-testing-verification": + expected_evidence = UI_EVIDENCE_REQUIREMENTS[ + "user-visible-ui-verification-evidence" + ] + if not any( + case["expected"].get("ui_evidence") == expected_evidence + for case in entry["should_use"] + ): + raise EvaluationError( + f"{location}.should_use must cover the user-visible UI inspection contract" + ) missing_skills = sorted(skill_ids - covered_skills) extra_skills = sorted(covered_skills - skill_ids) @@ -282,6 +377,11 @@ def validate(fixtures_path: Path) -> tuple[int, int]: raise EvaluationError( f"{location}.expected.handoff must require exact checks and residual risk" ) + expected_ui_evidence = UI_EVIDENCE_REQUIREMENTS.get(boundary_id) + if expected_ui_evidence and expectation.get("ui_evidence") != expected_ui_evidence: + raise EvaluationError( + f"{location}.expected.ui_evidence must define the required UI evidence contract" + ) prompt_count += 1 missing_boundaries = sorted(REQUIRED_BOUNDARIES - seen_boundaries) if missing_boundaries: diff --git a/docs/skills/skill-evaluation.mdx b/docs/skills/skill-evaluation.mdx index 4eb3afb..94f4211 100644 --- a/docs/skills/skill-evaluation.mdx +++ b/docs/skills/skill-evaluation.mdx @@ -33,9 +33,9 @@ The scope below describes where a skill performs its work. The manifest's `repos ## Framework boundaries -The fixture set also requires cases proving that ordinary routing does not imply a foundation audit, consumer work does not target archived foundation code, review-only and planning-only prompts do not authorize edits, unavailable XcodeBuildMCP follows the capability policy, installer conflicts are preserved, user-visible iOS/iPadOS verification uses its UI evidence contract, non-UI verification does not, and verification handoff reports exact checks plus residual risk. +The fixture set also requires cases proving that ordinary routing does not imply a foundation audit, consumer work does not target archived foundation code, review-only and planning-only prompts do not authorize edits, unavailable XcodeBuildMCP follows the capability policy, installer conflicts are preserved, user-visible iOS/iPadOS verification requires launch smoke, screenshot visual inspection, `snapshot_ui`, and manual-only checks, non-UI verification uses focused code tests only and forbids that UI evidence, and verification handoff reports exact checks plus residual risk. -The current evaluation covers all manifest skills and 8 required framework boundaries. The human-readable table above is a routing summary; the JSON fixtures are the validation source of truth. The principal remaining ambiguity is intentional composition: the master may shortlist tab navigation, component author, accessibility review, and verification in sequence. +The current evaluation covers all manifest skills and 9 required framework boundaries. The Swift verification skill has at least three positive fixtures, including the UI-inspection contract case. The human-readable table above is a routing summary; the JSON fixtures are the validation source of truth. The principal remaining ambiguity is intentional composition: the master may shortlist tab navigation, component author, accessibility review, and verification in sequence. ## What still requires model or manual evaluation diff --git a/docs/workflow/multi-service-ios-app.mdx b/docs/workflow/multi-service-ios-app.mdx index c815c54..2a47c70 100644 --- a/docs/workflow/multi-service-ios-app.mdx +++ b/docs/workflow/multi-service-ios-app.mdx @@ -7,12 +7,13 @@ description: Reusable architecture and agent-workflow patterns for device-to-man Patterns for **device → many independent HTTP APIs** apps (dashboards, admin clients, multi-service workflows). Neutral and reusable: no product branding, no private hosts, and no secrets. -Distilled from production multi-service consumer apps with independent service peers. Apply in the **consumer workspace**, not this foundation’s `archive/`. +Designed for dashboards, service clients, and independent service peers. Apply in +the **consumer workspace**, not this foundation’s `archive/`. ## When to use - App talks to **2+ services**, each with its own base URL and credential. -- Prefer **direct device→service** (no hosted BFF) for first release. +- Prefer **direct device→service** connections for the first release. - Secrets must stay **off disk-in-UserDefaults** and out of fixtures/logs. - Agents will implement integrations and UI; humans set product phase boundaries. @@ -57,22 +58,40 @@ Do not leak raw JSON or service field names into SwiftUI views. | --- | --- | | Preferences / registry | Non-secret config, credential **reference** (UUID account id) | | Keychain | API keys, tokens, username/password | -| Memory only | Session cookies / SID (re-login on expiry) | +| Memory only | Session credentials (re-authenticate on expiry) | | Never | Secrets in fixtures, launch args, screenshots, logs, sample data | **Connection Test:** probe with an **ephemeral** Keychain account so Test never overwrites the live secret on edit. Empty credential field on Save **keeps** the existing Keychain value. **Auth mapping:** HTTP 401/403 → unauthorized; missing secret → missing credential; transport failure → unavailable. UI recovery: Retry vs Open Settings / re-enter credentials. -Never log tokens, passwords, SID, `apikey`, or full private URLs. +Never log tokens, passwords, session credentials, or endpoint URLs containing +sensitive data. + +### Credential lifecycle + +- Rotate credentials by writing the replacement only after it has been validated; + preserve the prior value until the replacement succeeds. +- Deleting a credential or removing an account must remove its Keychain item, + clear in-memory sessions, cancel in-flight work, and purge account-scoped + caches and persisted endpoint configuration. +- Never place credentials in query strings, logs, screenshots, fixtures, sample + data, or handoff text. Use request headers or secure session storage instead. ## Transport policy - Prefer **HTTPS** for remote endpoints. - Allow **user-explicit plain HTTP** only where product requires LAN (document which endpoint labels allow it). - ATS: narrow exception (`NSAllowsLocalNetworking` or equivalent)—**not** `NSAllowsArbitraryLoads`. -- **Never** disable certificate validation to “make it work.” -- Timeouts, cancellation, and a mockable `HTTPTransport` protocol are mandatory for tests. +- **Never** disable certificate validation to “make it work.” +- Normalize endpoints with URL components before storing or requesting them: + trim input, reject malformed URLs, user-info credentials, fragments, and + unsupported schemes, and enforce the configured scheme allowlist before and + after redirects. +- Timeouts, cancellation, retry/backoff, and a mockable `HTTPTransport` protocol + are mandatory for tests. Document timeouts by request class; cancel work when + its feature leaves scope; retry only idempotent transient failures with capped + exponential backoff and jitter, never authentication or validation failures. ## FeatureState (feature-level loading) @@ -85,11 +104,14 @@ loading | content(value, refreshedAt) | empty | stale(value) | unavailable(recov - Prefer **one status row per service** (probe OK + content fail must not double-emit connected + failed). - Partial multi-service success: show connected peers + failed peers without blanking the whole app when any sibling works. - Recovery actions are explicit: retry, update credentials, configure endpoint, enable service. +- Show stale values with their refresh time and recovery action. Define cache + expiration per resource, avoid treating expired cache as fresh content, and + purge account-scoped cache on credential deletion or account removal. ## Remote assets -- Prefer absolute **CDN / remote** URLs when the API provides them. -- Relative asset paths need **base URL + auth** (header preferred; query key only if URL-based loaders force it). +- Prefer absolute remote URLs when the API provides them. +- Relative asset paths need a normalized base URL and header-based authorization. - Never log authenticated asset URLs. - List rows use **fixed compact frames** + clip; do not let variable-aspect-ratio assets overflow adjacent cards. - Cache bytes when scroll causes probe-per-image cost. @@ -159,7 +181,7 @@ For agent-driven UI verification: | Transport | Stub / path-routing mock; assert scheme policy and status mapping | | Clients | Decode synthetic fixtures only (`fixture.invalid`, fake ids) | | Repository | Multi-service partial failure and status dedupe | -| Secrets skim | Grep fixtures for token/apikey/password/private IPs before commit | +| Secrets skim | Grep fixtures for credentials, tokens, and passwords before commit | XcodeBuildMCP: `session_show_defaults` once per session; `test_sim` with **parallel testing disabled** for scheme stability; `build_run_sim` for smoke when UI changes. From 7977ec028f65b33fbd82c0975daf365173768551 Mon Sep 17 00:00:00 2001 From: brbndon Date: Thu, 30 Jul 2026 17:50:32 -0700 Subject: [PATCH 14/21] docs(workflow): expose multi-service app patterns --- docs/workflow/index.mdx | 4 ++++ docs/workflow/meta.ts | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/docs/workflow/index.mdx b/docs/workflow/index.mdx index da2f730..1136545 100644 --- a/docs/workflow/index.mdx +++ b/docs/workflow/index.mdx @@ -46,6 +46,10 @@ A reliable agent session follows a fixed loop. Skip steps only when the task is - Active project/workspace, scheme, configuration, exact simulator/device, what ran, and residual risk; report blocked when no authorized verification path exists. - Which skills were applied and which to run next. +## Multi-service / LAN API mesh apps + +For apps that talk to several independent HTTP APIs (Keychain secrets, local/remote endpoints, phased read-only clients, `FeatureState`, inspection routes), follow the reusable patterns in [Multi-service iOS app patterns](/workflow/multi-service-ios-app). Keep product-specific severity lists and paste prompts in the consumer app’s `Documentation/`. + ## Do not | Anti-pattern | Instead | diff --git a/docs/workflow/meta.ts b/docs/workflow/meta.ts index 3b92d0c..ad2227b 100644 --- a/docs/workflow/meta.ts +++ b/docs/workflow/meta.ts @@ -10,6 +10,6 @@ export default defineMeta({ "skill-routing", "consumer-workspace", "agents-md-template", + "multi-service-ios-app", ], }); - From 2c657e5b835480d5e4311754703bf0282db33bf2 Mon Sep 17 00:00:00 2001 From: brbndon Date: Thu, 30 Jul 2026 19:22:19 -0700 Subject: [PATCH 15/21] fix(audit): close reusable guidance findings - neutralize the multi-service playbook and harden reusable security guidance\n- make UI E2E contracts tool-neutral and keep website copy synchronized\n- enforce positive and boundary UI verification evidence in fixtures --- .../assets/consumer-AGENTS.md.template | 29 ++++++++++------- Evaluations/skill-routing.json | 2 +- Scripts/test-consumer-guidance.sh | 20 ++++++++++++ Scripts/validate-skill-evaluations.py | 29 +++++++++++++++++ docs/skills/skill-evaluation.mdx | 2 +- docs/workflow/agents-md-template.mdx | 31 ++++++++++++------- docs/workflow/multi-service-ios-app.mdx | 25 +++++++++------ 7 files changed, 103 insertions(+), 35 deletions(-) diff --git a/.agents/skills/codex-bootstrap/assets/consumer-AGENTS.md.template b/.agents/skills/codex-bootstrap/assets/consumer-AGENTS.md.template index 78cac2e..3e25c16 100644 --- a/.agents/skills/codex-bootstrap/assets/consumer-AGENTS.md.template +++ b/.agents/skills/codex-bootstrap/assets/consumer-AGENTS.md.template @@ -45,7 +45,10 @@ configured. For a project without iOS/iPadOS UI, set each value to `n/a`. Optional. Fill this section only when the project uses a UI E2E tool for user journeys. This may be Maestro, XCUITest, Appium, or another configured tool. -For a consumer without UI E2E—whether it is macOS-only, visionOS/watchOS, a +The contract is either `configured with the selected tool and real values` or +`not configured / n/a when UI E2E is not in scope`. + +For a project without UI E2E—whether it is macOS-only, visionOS/watchOS, a Swift package, or another target—set: - UI E2E tool: `not configured` (or `n/a`) @@ -56,20 +59,25 @@ Agents must not treat an unconfigured contract as an executable UI E2E destination. When UI E2E **is** in use, document the deterministic app contract before -relying on journeys. Keep secrets out of this file, flow files, fixtures, and -artifacts. +relying on journeys. Use fields that match the selected runner; do not assume a +specific directory, protocol, device type, identifier format, or onboarding +mechanism. Keep secrets out of this file, flow files, fixtures, and artifacts. - UI E2E tool: `` -- App identifier (bundle ID): `` +- UI E2E commands: `` +- Application identifier (bundle ID, package ID, or other runner identifier): + `` - Test location and naming: ``; document journeys/tests `` and reusable subflows/helpers `` using `` -- Exact destination/configuration: destination ID - ``, scheme ``, and build - configuration ``; when configured, use the same - destination and configuration for XcodeBuildMCP and the configured UI E2E - runner +- Deterministic routes or states/deep links: `` +- Risk-based evidence matrix: ``; align it with the + UI inspection contract when the selected runner supports the same scenarios +- Exact destination/configuration when required by the selected runner: + ``, scheme ``, and build + configuration ``; reuse project-defined values across + build, launch, and E2E commands when applicable - E2E-only launch arguments/environment beyond the UI inspection contract: `` and ``; inject secrets locally or in CI rather than committing them @@ -82,8 +90,7 @@ artifacts. - Fixtures and test accounts: `` and ``; define ownership, cleanup, and account reset behavior -- Deep links: `` -- E2E artifacts: use the UI inspection contract location and preserve failure +- E2E artifact location: ``; preserve failure artifacts while redacting secrets and private user data - Accessibility-ID conventions: when E2E is configured, use stable `.` selectors on actionable elements, diff --git a/Evaluations/skill-routing.json b/Evaluations/skill-routing.json index faf8954..e97c8bb 100644 --- a/Evaluations/skill-routing.json +++ b/Evaluations/skill-routing.json @@ -1132,7 +1132,7 @@ }, { "id": "testing-use-ios-ui-evidence-contract", - "prompt": "Verify a completed user-visible iOS/iPadOS UI change with the consumer's UI inspection contract: run a launch smoke, capture and visually inspect a screenshot for the primary affected route, then capture snapshot_ui and report any manual-only checks.", + "prompt": "Verify a completed user-visible iOS/iPadOS UI change with the consumer's UI inspection contract: run a launch smoke, capture and visually inspect a screenshot for the affected route, then capture snapshot_ui and report any manual-only checks.", "expected": { "skill_ids": [ "swift-testing-verification" diff --git a/Scripts/test-consumer-guidance.sh b/Scripts/test-consumer-guidance.sh index 1c3ba0f..6579f8c 100755 --- a/Scripts/test-consumer-guidance.sh +++ b/Scripts/test-consumer-guidance.sh @@ -42,6 +42,26 @@ assert_apple_verification_policy() { assert_apple_verification_policy "$template" +assert_tool_neutral_e2e_contract() { + local file="$1" + grep -q 'configured with the selected tool and real values' "$file" \ + || fail "missing selected-tool configuration guidance in $file" + grep -q 'not configured / n/a when UI E2E is not in scope' "$file" \ + || fail "missing explicit out-of-scope UI E2E form in $file" + grep -q 'UI E2E commands:' "$file" \ + || fail "missing configurable UI E2E commands field in $file" + grep -q 'Deterministic routes or states/deep links:' "$file" \ + || fail "missing configurable deterministic route field in $file" + grep -q 'Risk-based evidence matrix:' "$file" \ + || fail "missing configurable evidence matrix field in $file" + grep -q 'E2E artifact location:' "$file" \ + || fail "missing configurable E2E artifact location field in $file" + ! grep -q '\.maestro/\|Maestro MCP' "$file" \ + || fail "canonical E2E contract prescribes Maestro-specific setup in $file" +} + +assert_tool_neutral_e2e_contract "$template" + # The website's copyable template must remain an exact mirror of the canonical # bootstrap asset. Extract only its markdown fence, not incidental shell fences. website_template="$scratch/website-consumer-AGENTS.md" diff --git a/Scripts/validate-skill-evaluations.py b/Scripts/validate-skill-evaluations.py index 39676e4..d300ff5 100755 --- a/Scripts/validate-skill-evaluations.py +++ b/Scripts/validate-skill-evaluations.py @@ -81,6 +81,10 @@ "verification_category": "installer-behavior", "stop_condition": "preserve-conflict", }, + "user-visible-ui-verification-evidence": { + "verification_category": "apple-ui-e2e", + "stop_condition": "none", + }, "non-ui-verification-no-ui-evidence": { "verification_category": "apple-code", "stop_condition": "none", @@ -186,6 +190,31 @@ def validate_ui_evidence(value: Any, location: str) -> None: raise EvaluationError(f"{location}.{field} must be a unique array") if not all(action in UI_EVIDENCE_ACTIONS for action in actions): raise EvaluationError(f"{location}.{field} contains an invalid action") + required = set(evidence["required_actions"]) + forbidden = set(evidence["forbidden_actions"]) + if required & forbidden: + raise EvaluationError(f"{location} cannot require and forbid the same action") + if evidence["scope"] == "ui-inspection-contract": + missing = sorted( + { + "launch-smoke", + "screenshot", + "visual-inspection", + "snapshot-ui", + "manual-only-checks", + } + - required + ) + if missing: + raise EvaluationError( + f"{location} is missing UI inspection actions: {', '.join(missing)}" + ) + elif evidence["scope"] == "focused-code-tests-only": + if required != {"focused-code-tests"} or forbidden != UI_EVIDENCE_FORBIDDEN_ACTIONS: + raise EvaluationError( + f"{location} focused-code-tests-only must require focused code tests " + "and forbid all UI evidence actions" + ) def validate_expectation(value: Any, location: str, skill_ids: set[str]) -> None: diff --git a/docs/skills/skill-evaluation.mdx b/docs/skills/skill-evaluation.mdx index 94f4211..b065361 100644 --- a/docs/skills/skill-evaluation.mdx +++ b/docs/skills/skill-evaluation.mdx @@ -33,7 +33,7 @@ The scope below describes where a skill performs its work. The manifest's `repos ## Framework boundaries -The fixture set also requires cases proving that ordinary routing does not imply a foundation audit, consumer work does not target archived foundation code, review-only and planning-only prompts do not authorize edits, unavailable XcodeBuildMCP follows the capability policy, installer conflicts are preserved, user-visible iOS/iPadOS verification requires launch smoke, screenshot visual inspection, `snapshot_ui`, and manual-only checks, non-UI verification uses focused code tests only and forbids that UI evidence, and verification handoff reports exact checks plus residual risk. +The fixture set also requires cases proving that ordinary routing does not imply a foundation audit, consumer work does not target archived foundation code, review-only and planning-only prompts do not authorize edits, unavailable XcodeBuildMCP follows the capability policy, installer conflicts are preserved, user-visible iOS/iPadOS verification requires launch smoke, screenshot visual inspection, `snapshot_ui`, and manual-only checks, non-UI verification uses focused code tests only and forbids that UI evidence, and verification handoff reports exact checks plus residual risk. The positive and boundary cases encode the required actions and forbidden actions explicitly. The current evaluation covers all manifest skills and 9 required framework boundaries. The Swift verification skill has at least three positive fixtures, including the UI-inspection contract case. The human-readable table above is a routing summary; the JSON fixtures are the validation source of truth. The principal remaining ambiguity is intentional composition: the master may shortlist tab navigation, component author, accessibility review, and verification in sequence. diff --git a/docs/workflow/agents-md-template.mdx b/docs/workflow/agents-md-template.mdx index b3f3a9a..16ef07b 100644 --- a/docs/workflow/agents-md-template.mdx +++ b/docs/workflow/agents-md-template.mdx @@ -33,7 +33,7 @@ Fill every `<…>` placeholder from checked-in project configuration. Do not inv | Destinations | exact `simulatorId`; macOS arch or `n/a` | | Formatter / linter / checks | repo-native commands or `not configured` | | Targeted and full tests | repo-native or MCP test scope | -| UI E2E contract | `configured` with the selected tool and real values, or `not configured` / `n/a` when UI E2E is not in scope | +| UI E2E contract | `configured with the selected tool and real values` or `not configured / n/a when UI E2E is not in scope` | | Apple verification policy | XcodeBuildMCP CLI and raw `xcodebuild`/`xcrun`/`simctl`: keep `require-approval` unless you authorize `allowed` or `denied` | Scoped `AGENTS.md` files in subdirectories refine this root contract for their trees; keep them when present. @@ -98,7 +98,10 @@ configured. For a project without iOS/iPadOS UI, set each value to `n/a`. Optional. Fill this section only when the project uses a UI E2E tool for user journeys. This may be Maestro, XCUITest, Appium, or another configured tool. -For a consumer without UI E2E—whether it is macOS-only, visionOS/watchOS, a +The contract is either `configured with the selected tool and real values` or +`not configured / n/a when UI E2E is not in scope`. + +For a project without UI E2E—whether it is macOS-only, visionOS/watchOS, a Swift package, or another target—set: - UI E2E tool: `not configured` (or `n/a`) @@ -109,20 +112,25 @@ Agents must not treat an unconfigured contract as an executable UI E2E destination. When UI E2E **is** in use, document the deterministic app contract before -relying on journeys. Keep secrets out of this file, flow files, fixtures, and -artifacts. +relying on journeys. Use fields that match the selected runner; do not assume a +specific directory, protocol, device type, identifier format, or onboarding +mechanism. Keep secrets out of this file, flow files, fixtures, and artifacts. - UI E2E tool: `` -- App identifier (bundle ID): `` +- UI E2E commands: `` +- Application identifier (bundle ID, package ID, or other runner identifier): + `` - Test location and naming: ``; document journeys/tests `` and reusable subflows/helpers `` using `` -- Exact destination/configuration: destination ID - ``, scheme ``, and build - configuration ``; when configured, use the same - destination and configuration for XcodeBuildMCP and the configured UI E2E - runner +- Deterministic routes or states/deep links: `` +- Risk-based evidence matrix: ``; align it with the + UI inspection contract when the selected runner supports the same scenarios +- Exact destination/configuration when required by the selected runner: + ``, scheme ``, and build + configuration ``; reuse project-defined values across + build, launch, and E2E commands when applicable - E2E-only launch arguments/environment beyond the UI inspection contract: `` and ``; inject secrets locally or in CI rather than committing them @@ -135,8 +143,7 @@ artifacts. - Fixtures and test accounts: `` and ``; define ownership, cleanup, and account reset behavior -- Deep links: `` -- E2E artifacts: use the UI inspection contract location and preserve failure +- E2E artifact location: ``; preserve failure artifacts while redacting secrets and private user data - Accessibility-ID conventions: when E2E is configured, use stable `.` selectors on actionable elements, diff --git a/docs/workflow/multi-service-ios-app.mdx b/docs/workflow/multi-service-ios-app.mdx index 2a47c70..1d3ef92 100644 --- a/docs/workflow/multi-service-ios-app.mdx +++ b/docs/workflow/multi-service-ios-app.mdx @@ -5,17 +5,19 @@ description: Reusable architecture and agent-workflow patterns for device-to-man # Multi-service iOS app patterns -Patterns for **device → many independent HTTP APIs** apps (dashboards, admin clients, multi-service workflows). Neutral and reusable: no product branding, no private hosts, and no secrets. +Patterns for **device → many independent HTTP APIs** apps (dashboards, service +clients, and multi-service workflows). Keep examples neutral and reusable: +no branded or environment-specific references, and no secrets. Designed for dashboards, service clients, and independent service peers. Apply in -the **consumer workspace**, not this foundation’s `archive/`. +the active application workspace, not this foundation’s `archive/`. ## When to use - App talks to **2+ services**, each with its own base URL and credential. - Prefer **direct device→service** connections for the first release. - Secrets must stay **off disk-in-UserDefaults** and out of fixtures/logs. -- Agents will implement integrations and UI; humans set product phase boundaries. +- Agents will implement integrations and UI; teams set phase boundaries. ## Phase slice @@ -81,7 +83,8 @@ sensitive data. ## Transport policy - Prefer **HTTPS** for remote endpoints. -- Allow **user-explicit plain HTTP** only where product requires LAN (document which endpoint labels allow it). +- Allow **user-explicit plain HTTP** only where a documented local-network + requirement exists (document which endpoint labels allow it). - ATS: narrow exception (`NSAllowsLocalNetworking` or equivalent)—**not** `NSAllowsArbitraryLoads`. - **Never** disable certificate validation to “make it work.” - Normalize endpoints with URL components before storing or requesting them: @@ -160,7 +163,8 @@ Every multi-step integration handoff should include: 1. **Do not redo** — already-fixed items (regression surface only). 2. **Severity / priority bands** — P0 trust → P1 next client → P2 auth → P3 peers. 3. **File map** — exact paths. -4. **API truth doc** — prefer a fact-checked agent guide over marketing product specs for endpoints. +4. **API truth doc** — prefer a fact-checked agent guide over marketing copy or + unstable public specs for endpoints. 5. **Non-goals** — no push, no secrets in fixtures, no write APIs this phase. 6. **Paste prompt** — zero-context instruction for a fresh agent. 7. **Verification gate** — scheme, `test_sim` parallel off, inspection screenshots when UI changes. @@ -193,20 +197,20 @@ XcodeBuildMCP: `session_show_defaults` once per session; `test_sim` with **paral 4. DTO → domain mapping. 5. Register in factory. 6. Fixtures + decode/auth tests. -7. Icon asset (template or app-provided mark per product policy). +7. Icon asset (template or app-provided mark per application policy). 8. No live network in CI. ## Distribution and privacy note If this pattern ships publicly, document endpoint security, local-network usage, -and data handling in the product and privacy plan. Keep product-specific policy -out of fixtures. +and data handling in the application and privacy plan. Keep +application-specific policy out of fixtures. ## Related foundation pages - [Session workflow](/workflow) — full agent loop - [Task recipes](/workflow/task-recipes) — scenario shortcuts -- [AGENTS.md template](/workflow/agents-md-template) — consumer always-on rules +- [AGENTS.md template](/workflow/agents-md-template) — project-local always-on rules - [XcodeBuildMCP](/tools/xcodebuildmcp) — verification ladder - [Security defaults](/) — never log secrets (see always-on `AGENTS.md` in this repo) @@ -222,4 +226,5 @@ out of fixtures. --- -*Keep this page product-neutral. App-specific severity lists and paste prompts live in the consumer app’s `Documentation/`.* +*Keep this page application-neutral. Application-specific severity lists and +paste prompts live in the active workspace’s `Documentation/`.* From fa021f6164b2be8a19ab9bcfbd82c522bad5e814 Mon Sep 17 00:00:00 2001 From: brbndon Date: Mon, 3 Aug 2026 21:25:59 -0700 Subject: [PATCH 16/21] docs(templates): add LiquidOrbLoader portable loading mark - Drop-in DotMatrixLoader + FeatureLoadingView template - Design rationale and integration checklist - Task recipe for full-surface liquid orb loading --- .../LiquidOrbLoader/DotMatrixLoader.swift | 387 ++++++++++++++++++ Templates/LiquidOrbLoader/README.md | 87 ++++ docs/workflow/task-recipes.mdx | 16 + 3 files changed, 490 insertions(+) create mode 100644 Templates/LiquidOrbLoader/DotMatrixLoader.swift create mode 100644 Templates/LiquidOrbLoader/README.md diff --git a/Templates/LiquidOrbLoader/DotMatrixLoader.swift b/Templates/LiquidOrbLoader/DotMatrixLoader.swift new file mode 100644 index 0000000..ba28f66 --- /dev/null +++ b/Templates/LiquidOrbLoader/DotMatrixLoader.swift @@ -0,0 +1,387 @@ +import SwiftUI + +// MARK: - Drop-in template (portable) +// +// Source of truth for the pattern: Harborlight DotMatrixLoader + FeatureLoadingView. +// Defaults use system colors so this file has no app-specific design-system dependency. +// Map `tint` / `idleTint` to your tokens when integrating. + +/// 3×3 dot-matrix loading animation. +/// +/// Eight dots sweep a smooth highlight around the ring. The orb center is a +/// continuous liquid-glass morph driven by overlapping harmonics (no keyframe holds). +public struct DotMatrixLoader: View { + public enum Center { + case plain + case pulse + case symbol(String) + case emoji(String) + /// Continuous liquid-glass orb (recommended full-surface loading mark). + case orb + } + + private static let ring: [SIMD2] = [ + .init(-1, -1), .init(0, -1), .init(1, -1), + .init(1, 0), + .init(1, 1), .init(0, 1), .init(-1, 1), + .init(-1, 0) + ] + + public let center: Center + public var dotSize: CGFloat + public var spacing: CGFloat + public var period: TimeInterval + public var tint: Color + public var idleTint: Color + + @Environment(\.accessibilityReduceMotion) private var reduceMotion + + public init( + center: Center = .pulse, + dotSize: CGFloat = 12, + spacing: CGFloat = 10, + period: TimeInterval = 1.8, + tint: Color = .accentColor, + idleTint: Color = .secondary + ) { + self.center = center + self.dotSize = dotSize + self.spacing = spacing + self.period = period + self.tint = tint + self.idleTint = idleTint + } + + /// Full-surface loading mark (generous air around the orb). + public static var feature: DotMatrixLoader { + DotMatrixLoader(center: .orb, dotSize: 11, spacing: 28, period: 2.8) + } + + /// Compact mark for cards and inline chrome. + public static var compact: DotMatrixLoader { + DotMatrixLoader(center: .orb, dotSize: 8, spacing: 16, period: 2.8) + } + + private var step: CGFloat { + switch center { + case .orb: max(dotSize + spacing, orbSize * 0.82) + default: dotSize + spacing + } + } + + private var orbSize: CGFloat { + switch center { + case .orb: max(dotSize * 4.4, 58) + default: dotSize + } + } + + private var ringDotSize: CGFloat { + switch center { + case .orb: dotSize * 0.82 + default: dotSize + } + } + + private var bounds: CGFloat { + let ringExtent = step * 2 + ringDotSize + switch center { + case .orb: return max(ringExtent, orbSize * 1.35) + default: return ringExtent + } + } + + public var body: some View { + TimelineView(.animation(minimumInterval: 1 / 60, paused: reduceMotion)) { context in + let time = reduceMotion ? 0 : context.date.timeIntervalSinceReferenceDate / period + ZStack { + ForEach(Array(Self.ring.enumerated()), id: \.offset) { index, coord in + ringDot(at: coord, index: index, time: time) + } + centerDot(time: time) + } + .frame(width: bounds, height: bounds) + .accessibilityLabel("Loading") + .accessibilityRemoveTraits(.isImage) + } + } + + private func ringDot(at coord: SIMD2, index: Int, time: Double) -> some View { + let intensity = wave(angle: (Double(index) / Double(Self.ring.count)) * 2 * .pi, time: time) + let size = ringDotSize + return Circle() + .fill(tint) + .frame(width: size, height: size) + .scaleEffect(0.82 + 0.32 * intensity) + .opacity(0.28 + 0.72 * intensity) + .offset(x: CGFloat(coord.x) * step, y: CGFloat(coord.y) * step) + } + + @ViewBuilder + private func centerDot(time: Double) -> some View { + let breath = 1 + 0.28 * (0.5 + 0.5 * cos(2 * .pi * time + .pi)) + let tilt = sin(2 * .pi * time) * 10 + switch center { + case .plain: + Circle() + .fill(idleTint) + .frame(width: dotSize, height: dotSize) + .scaleEffect(1 + 0.15 * (breath - 1)) + case .pulse: + Circle() + .fill(tint) + .frame(width: dotSize, height: dotSize) + .scaleEffect(breath) + case .symbol(let name): + Image(systemName: name) + .font(.system(size: dotSize * 1.15, weight: .semibold)) + .foregroundStyle(tint) + .scaleEffect(breath) + .rotationEffect(.degrees(tilt)) + case .emoji(let character): + Text(character) + .font(.system(size: dotSize * 1.35)) + .scaleEffect(breath) + .rotationEffect(.degrees(tilt)) + case .orb: + LiquidOrbMorph(time: time, size: orbSize) + } + } + + private func wave(angle: Double, time: Double) -> Double { + let cursor = time * 2 * .pi + let delta = min(abs(angle - cursor), 2 * .pi - abs(angle - cursor)) + return 0.5 + 0.5 * cos(delta) + } +} + +// MARK: - Feature loading surface + +/// Full-surface loading chrome: liquid orb + title + detail. +public struct FeatureLoadingView: View { + public var title: String + public var detail: String + public var compact: Bool + + public init( + title: String = "Loading", + detail: String = "Refreshing…", + compact: Bool = false + ) { + self.title = title + self.detail = detail + self.compact = compact + } + + public var body: some View { + VStack(spacing: 24) { + if compact { + DotMatrixLoader.compact + } else { + DotMatrixLoader.feature + } + VStack(spacing: 10) { + Text(title) + .font(.title3.weight(.semibold)) + Text(detail) + .font(.subheadline) + .foregroundStyle(.secondary) + .multilineTextAlignment(.center) + } + } + .frame(maxWidth: .infinity, maxHeight: .infinity) + .accessibilityElement(children: .combine) + .accessibilityLabel("\(title). \(detail)") + } +} + +// MARK: - Continuous liquid orb + +private struct LiquidOrbMorph: View { + let time: Double + let size: CGFloat + + private var canvasSize: CGFloat { size * 1.55 } + + var body: some View { + let field = LiquidField.sample(at: time) + Canvas { context, canvasSize in + let center = CGPoint(x: canvasSize.width / 2, y: canvasSize.height / 2) + let logical = min(canvasSize.width, canvasSize.height) / 1.55 + let baseR = logical * 0.36 * field.scale + let blob = softBlobPath(center: center, baseRadius: baseR, field: field) + + var glow = context + glow.opacity = 0.28 + 0.36 * field.glow + glow.addFilter(.blur(radius: baseR * (0.55 + 0.15 * field.glow))) + glow.fill(blob, with: .color(Color(red: 0.40, green: 0.72, blue: 1.0))) + + var bloom = context + bloom.opacity = 0.16 + 0.20 * field.glow + bloom.addFilter(.blur(radius: baseR * 0.85)) + bloom.fill(blob, with: .color(Color(red: 0.55, green: 0.85, blue: 1.0))) + + context.fill( + blob, + with: .radialGradient( + Gradient(stops: [ + .init(color: Color(red: 0.97, green: 0.99, blue: 1.0).opacity(0.96), location: 0), + .init(color: Color(red: 0.62, green: 0.86, blue: 1.0).opacity(0.78), location: 0.34), + .init(color: Color(red: 0.36, green: 0.62, blue: 0.98).opacity(0.52), location: 0.74), + .init(color: Color(red: 0.28, green: 0.50, blue: 0.96).opacity(0.22), location: 1) + ]), + center: center, + startRadius: 0, + endRadius: baseR * 1.5 + ) + ) + + context.stroke( + blob, + with: .linearGradient( + Gradient(colors: [ + Color.white.opacity(0.65), + Color.white.opacity(0.12), + Color(red: 0.55, green: 0.82, blue: 1.0).opacity(0.40) + ]), + startPoint: CGPoint(x: center.x - baseR, y: center.y - baseR), + endPoint: CGPoint(x: center.x + baseR, y: center.y + baseR) + ), + lineWidth: max(0.9, baseR * 0.05) + ) + + let hx = center.x + CGFloat(cos(field.highlightAngle)) * baseR * 0.24 + let hy = center.y + CGFloat(sin(field.highlightAngle)) * baseR * 0.18 + let highlightR = baseR * (0.30 + 0.08 * field.glow) + let highlight = Path(ellipseIn: CGRect( + x: hx - highlightR, + y: hy - highlightR * 0.7, + width: highlightR * 2, + height: highlightR * 1.4 + )) + context.blendMode = .plusLighter + context.fill( + highlight, + with: .radialGradient( + Gradient(colors: [ + Color.white.opacity(0.50 + 0.28 * field.glow), + Color.white.opacity(0.06), + .clear + ]), + center: CGPoint(x: hx, y: hy), + startRadius: 0, + endRadius: highlightR * 1.55 + ) + ) + + let coreR = baseR * (0.18 + 0.12 * field.glow) + let core = Path(ellipseIn: CGRect( + x: center.x - coreR, + y: center.y - coreR, + width: coreR * 2, + height: coreR * 2 + )) + context.fill( + core, + with: .radialGradient( + Gradient(colors: [ + Color.white.opacity(0.90), + Color(red: 0.70, green: 0.92, blue: 1.0).opacity(0.38), + .clear + ]), + center: center, + startRadius: 0, + endRadius: coreR * 1.7 + ) + ) + } + .frame(width: canvasSize, height: canvasSize) + .scaleEffect(x: field.squashX, y: field.squashY) + .frame(width: size, height: size) + } + + private func softBlobPath(center: CGPoint, baseRadius: CGFloat, field: LiquidField) -> Path { + var path = Path() + let steps = 160 + for i in 0...steps { + let t = Double(i) / Double(steps) + let theta = t * 2 * Double.pi + let r = baseRadius * CGFloat(field.radius(at: theta)) + let point = CGPoint( + x: center.x + r * CGFloat(cos(theta)), + y: center.y + r * CGFloat(sin(theta)) + ) + if i == 0 { + path.move(to: point) + } else { + path.addLine(to: point) + } + } + path.closeSubpath() + return path + } +} + +private struct LiquidField { + var lobe: Double + var lobePhase: Double + var clover: Double + var wobble: Double + var wobblePhase: Double + var scale: Double + var glow: Double + var squashX: CGFloat + var squashY: CGFloat + var highlightAngle: Double + + func radius(at theta: Double) -> Double { + let peanut = lobe * cos(2 * theta + lobePhase) + let petals = clover * cos(4 * theta + lobePhase * 0.55) + let ripple = wobble * cos(3 * theta + wobblePhase) + let shimmer = 0.035 * cos(5 * theta - lobePhase * 1.3) + let fine = 0.018 * cos(7 * theta + wobblePhase * 0.6) + return max(0.32, 1 + peanut + petals + ripple + shimmer + fine) + } + + static func sample(at time: Double) -> LiquidField { + let ω = time * 2 * Double.pi + let lobePhase = ω * 0.55 + 0.28 * sin(ω * 0.45) + 0.08 * sin(ω * 1.1) + let elongate = 0.5 + 0.5 * cos(ω * 0.9 + 0.2) + let puffy = 0.5 + 0.5 * sin(ω * 0.9 + 0.9) + let lobe = 0.18 + 0.22 * elongate - 0.06 * puffy + let clover = 0.05 + 0.18 * puffy * puffy + 0.04 * sin(ω * 1.4) + let wobble = 0.05 + 0.07 * (0.5 + 0.5 * sin(ω * 1.35 + 0.5)) + let wobblePhase = ω * 1.85 + 0.3 * sin(ω * 0.6) + let scale = 1.0 + + 0.08 * sin(ω * 0.85 + 0.4) + + 0.045 * sin(ω * 1.7 + 1.2) + + 0.02 * sin(ω * 2.6) + let glow = 0.40 + 0.34 * puffy + 0.14 * sin(ω * 1.6 + 0.7) + 0.08 * sin(ω * 0.5) + let stretch = 0.05 * cos(ω * 0.9) + 0.02 * sin(ω * 1.8) + let squashX = CGFloat(1 + stretch * cos(lobePhase)) + let squashY = CGFloat(1 + stretch * sin(lobePhase)) + let highlightAngle = ω * 0.9 + 0.55 * sin(ω * 0.65) + + return LiquidField( + lobe: lobe, + lobePhase: lobePhase, + clover: clover, + wobble: wobble, + wobblePhase: wobblePhase, + scale: scale, + glow: min(1, max(0, glow)), + squashX: squashX, + squashY: squashY, + highlightAngle: highlightAngle + ) + } +} + +#Preview("Feature loading") { + FeatureLoadingView() +} + +#Preview("Orb mark") { + DotMatrixLoader.feature + .padding() +} diff --git a/Templates/LiquidOrbLoader/README.md b/Templates/LiquidOrbLoader/README.md new file mode 100644 index 0000000..fdeeab2 --- /dev/null +++ b/Templates/LiquidOrbLoader/README.md @@ -0,0 +1,87 @@ +# Liquid orb loading mark (DotMatrixLoader) + +Portable SwiftUI loading chrome proven in **Harborlight** (iOS 26). Use this when you want a full-surface loading state that feels continuous and liquid, not a discrete spinner or keyframed “pose” loop. + +## What you get + +| Piece | Role | +| --- | --- | +| `DotMatrixLoader` | 3×3 ring with a continuous highlight sweep; center is a liquid-glass **orb** (or plain / pulse / symbol / emoji) | +| `FeatureLoadingView` | Full-surface “Loading” + detail copy above the mark | +| Continuous harmonics | Orb shape never holds still — REST → MERGE → REBOUND → RELAX emerge from sines, not keyframes | +| Reduce Motion | `TimelineView` pauses; orb freezes on the first frame | + +## When to use + +- **Yes:** Screen-level `FeatureState.loading`, empty-shell first fetch, developer loading previews. +- **No:** Determinate progress (use `ProgressView(value:)`), poster tile placeholders, tiny toolbar busy indicators, high-frequency keyboard paths. + +Frequency note (Emil / Apple design): loading chrome is occasional → motion is appropriate. Keep Reduce Motion freezes. + +## Drop into a consumer app + +1. Copy `DotMatrixLoader.swift` into the app target (e.g. `Components/`). +2. Wire design tokens if you have them: + + ```swift + // Optional: map to your design system + DotMatrixLoader( + center: .orb, + tint: AppColor.accent, // default: Color.accentColor + idleTint: AppColor.secondaryLabel // default: Color.secondary + ) + ``` + +3. Use the presets: + + ```swift + // Full-screen feature loading + FeatureLoadingView(title: "Loading", detail: "Refreshing your library.") + + // Or the mark alone + DotMatrixLoader.feature // generous spacing, hero orb + DotMatrixLoader.compact // cards / inline + ``` + +4. Replace stock loading surfaces: + + ```swift + switch state { + case .loading: + FeatureLoadingView() + // ... + } + ``` + +5. Accessibility: + - Mark: `accessibilityLabel("Loading")`, not an image. + - Surface: combined label `"\(title). \(detail)"` + a stable `accessibilityIdentifier` if you use inspection routes. + +## Design rationale (keep these) + +| Choice | Why | +| --- | --- | +| Harmonics, not keyframes | Smoothstep keyframes zero velocity at holds → feels “stop then go”. Sines never stop. | +| Padded canvas (~1.55×) | Glow and stretch must not clip to a hard square. | +| Ring step from orb size | Prevents the liquid body from overlapping ring dots. | +| No brand glyph in the orb | Glyphs read as a frozen logo on a morphing blob; pure liquid reads cleaner. | +| `TimelineView(.animation)` | No `Timer`, no lifecycle dance; pauses under Reduce Motion. | + +## Proven source + +- App: Harborlight (`Harborlight/Components/DotMatrixLoader.swift`) +- Primary consumers: `FeatureStateView` loading case, Services first load, Settings developer preview + +## Verification checklist + +- [ ] Reduce Motion freezes the mark +- [ ] Light and dark: orb remains readable on ambient background +- [ ] Ring dots do not collide with the orb at peak scale +- [ ] VoiceOver announces loading without treating the canvas as a photo +- [ ] Inspection / screenshot route if the app has deterministic loading states + +## Do not + +- Land this into AppleDevelopmentFoundation `archive/Sources/` unless you are explicitly expanding that package. +- Animate keyboard-triggered chrome with this mark. +- Put credentials or private hostnames on the loading detail string. diff --git a/docs/workflow/task-recipes.mdx b/docs/workflow/task-recipes.mdx index b9a3099..42ea196 100644 --- a/docs/workflow/task-recipes.mdx +++ b/docs/workflow/task-recipes.mdx @@ -58,6 +58,22 @@ Details: [Bootstrap](/skills/bootstrap). Read each skill’s `SKILL.md` for checklists. Bootstrap chain context: [Bootstrap](/skills/bootstrap). +### Liquid orb full-surface loading mark + +Proven pattern (Harborlight). Continuous 3×3 ring + liquid-glass **orb** center (no brand glyph). Prefer harmonics over keyframe holds so the mark never “pauses.” + +| Step | Action | +| --- | --- | +| Template | Copy from repo `Templates/LiquidOrbLoader/` (`README.md` + drop-in `DotMatrixLoader.swift`) | +| Integrate | Drop into consumer `Components/`; map `tint` / spacing to app tokens; use `FeatureLoadingView` for screen-level `loading`, `DotMatrixLoader.compact` for cards | +| Replace | Stock `ContentUnavailableView` / `ProgressView()` **full-screen** loading only — keep determinate `ProgressView(value:)` and poster-tile spinners | +| A11y | Reduce Motion freezes via `TimelineView`; label `"Loading"` on the mark; combined title+detail on the surface | +| Tools | XcodeBuildMCP build + optional inspection screenshot of the loading route | +| Verify | Light/dark contrast; no ring/orb overlap at peak scale; Reduce Motion freeze; VoiceOver not treating canvas as an image | +| Do not | Animate keyboard chrome; put private hostnames in detail copy; expand foundation `archive/Sources/` for this drop-in | + +Proven consumer wiring: Harborlight `FeatureStateView` + Services first load + Settings developer preview. + ### Fix a bug | Step | Action | From e5564e98cabff663e8ffe18793151aeeb8483dbf Mon Sep 17 00:00:00 2001 From: brbndon Date: Mon, 3 Aug 2026 21:45:02 -0700 Subject: [PATCH 17/21] docs(templates): sync LiquidOrbLoader with soft-glow Harborlight UI - Update README: presets, InlineLoadingRow, ScrollView layout, anti-box checklist - Refresh drop-in Swift from current Harborlight implementation - Expand task recipe for full-surface and inline loading patterns --- README.md | 2 +- .../LiquidOrbLoader/DotMatrixLoader.swift | 216 ++++++++++++------ Templates/LiquidOrbLoader/README.md | 100 ++++++-- docs/workflow/task-recipes.mdx | 24 +- 4 files changed, 236 insertions(+), 106 deletions(-) diff --git a/README.md b/README.md index 8c98090..78ea95e 100644 --- a/README.md +++ b/README.md @@ -37,7 +37,7 @@ See [Agent host support](docs/reference/host-support.mdx). | `.agents/skills/` | Live reusable skills | | `Scripts/` | Live catalog, installer, consumer-guidance, and verification tools | | `docs/`, `MCP.md` | Live agent playbook and command reference | -| `Templates/` | Skill and consumer-project templates (includes `AppMarketingSite/`) | +| `Templates/` | Skill and consumer-project templates (includes `AppMarketingSite/`, `LiquidOrbLoader/`) | | `archive/` | Preserved exploratory package, demos, old website, historical documentation, and package tooling | ## Install skills diff --git a/Templates/LiquidOrbLoader/DotMatrixLoader.swift b/Templates/LiquidOrbLoader/DotMatrixLoader.swift index ba28f66..72181db 100644 --- a/Templates/LiquidOrbLoader/DotMatrixLoader.swift +++ b/Templates/LiquidOrbLoader/DotMatrixLoader.swift @@ -2,21 +2,23 @@ import SwiftUI // MARK: - Drop-in template (portable) // -// Source of truth for the pattern: Harborlight DotMatrixLoader + FeatureLoadingView. -// Defaults use system colors so this file has no app-specific design-system dependency. -// Map `tint` / `idleTint` to your tokens when integrating. +// Source of truth: Harborlight DotMatrixLoader + FeatureLoadingView + InlineLoadingRow. +// Soft-glow orb (no hard square clip). Defaults use system colors — map tint to app tokens. +// See Templates/LiquidOrbLoader/README.md for layout, anti-box checklist, and wiring. +// /// 3×3 dot-matrix loading animation. /// -/// Eight dots sweep a smooth highlight around the ring. The orb center is a -/// continuous liquid-glass morph driven by overlapping harmonics (no keyframe holds). -public struct DotMatrixLoader: View { - public enum Center { +/// Eight dots sweep a smooth highlight around the ring while the center plays +/// a distinct role. The ring highlight is a continuous cosine wave; the orb +/// center is a continuous liquid-glass morph (no discrete keyframe holds). +struct DotMatrixLoader: View { + enum Center { case plain case pulse case symbol(String) case emoji(String) - /// Continuous liquid-glass orb (recommended full-surface loading mark). + /// Continuous liquid-glass orb (primary Harborlight loading mark). case orb } @@ -27,16 +29,16 @@ public struct DotMatrixLoader: View { .init(-1, 0) ] - public let center: Center - public var dotSize: CGFloat - public var spacing: CGFloat - public var period: TimeInterval - public var tint: Color - public var idleTint: Color + let center: Center + var dotSize: CGFloat + var spacing: CGFloat + var period: TimeInterval + var tint: Color + var idleTint: Color @Environment(\.accessibilityReduceMotion) private var reduceMotion - public init( + init( center: Center = .pulse, dotSize: CGFloat = 12, spacing: CGFloat = 10, @@ -52,16 +54,22 @@ public struct DotMatrixLoader: View { self.idleTint = idleTint } - /// Full-surface loading mark (generous air around the orb). - public static var feature: DotMatrixLoader { + /// Recommended full-surface loading mark (generous air around the orb). + static var feature: DotMatrixLoader { DotMatrixLoader(center: .orb, dotSize: 11, spacing: 28, period: 2.8) } - /// Compact mark for cards and inline chrome. - public static var compact: DotMatrixLoader { - DotMatrixLoader(center: .orb, dotSize: 8, spacing: 16, period: 2.8) + /// Compact mark for cards, forms, and detail sections. + static var compact: DotMatrixLoader { + DotMatrixLoader(center: .orb, dotSize: 7, spacing: 14, period: 2.8) + } + + /// Tiny mark for poster tiles and dense chrome. + static var micro: DotMatrixLoader { + DotMatrixLoader(center: .orb, dotSize: 5, spacing: 9, period: 2.8) } + /// Ring step. Orb mode pushes the ring out so the liquid mark has room. private var step: CGFloat { switch center { case .orb: max(dotSize + spacing, orbSize * 0.82) @@ -69,9 +77,10 @@ public struct DotMatrixLoader: View { } } + /// Scales with `dotSize` so `.feature` / `.compact` / `.micro` stay proportional. private var orbSize: CGFloat { switch center { - case .orb: max(dotSize * 4.4, 58) + case .orb: max(dotSize * 5.2, 24) default: dotSize } } @@ -86,12 +95,13 @@ public struct DotMatrixLoader: View { private var bounds: CGFloat { let ringExtent = step * 2 + ringDotSize switch center { - case .orb: return max(ringExtent, orbSize * 1.35) + // Canvas is oversized for soft glow falloff — must not clip to a square. + case .orb: return max(ringExtent, orbSize * LiquidOrbMorph.layoutScale) default: return ringExtent } } - public var body: some View { + var body: some View { TimelineView(.animation(minimumInterval: 1 / 60, paused: reduceMotion)) { context in let time = reduceMotion ? 0 : context.date.timeIntervalSinceReferenceDate / period ZStack { @@ -157,23 +167,13 @@ public struct DotMatrixLoader: View { // MARK: - Feature loading surface -/// Full-surface loading chrome: liquid orb + title + detail. -public struct FeatureLoadingView: View { - public var title: String - public var detail: String - public var compact: Bool - - public init( - title: String = "Loading", - detail: String = "Refreshing…", - compact: Bool = false - ) { - self.title = title - self.detail = detail - self.compact = compact - } +/// Full-surface loading chrome used by `FeatureStateView` and the developer preview. +struct FeatureLoadingView: View { + var title: String = "Loading" + var detail: String = "Refreshing your services." + var compact: Bool = false - public var body: some View { + var body: some View { VStack(spacing: 24) { if compact { DotMatrixLoader.compact @@ -185,73 +185,124 @@ public struct FeatureLoadingView: View { .font(.title3.weight(.semibold)) Text(detail) .font(.subheadline) - .foregroundStyle(.secondary) + .foregroundStyle(Color.secondary) .multilineTextAlignment(.center) } } .frame(maxWidth: .infinity, maxHeight: .infinity) .accessibilityElement(children: .combine) .accessibilityLabel("\(title). \(detail)") + .accessibilityIdentifier("feature.loading") + } +} + +/// Inline loading row for forms, detail panels, and banners (compact orb + message). +struct InlineLoadingRow: View { + var message: String + var micro: Bool = false + + var body: some View { + HStack(spacing: 16) { + if micro { + DotMatrixLoader.micro + } else { + DotMatrixLoader.compact + } + Text(message) + .font(.subheadline.weight(.medium)) + .foregroundStyle(Color.secondary) + .fixedSize(horizontal: false, vertical: true) + Spacer(minLength: 0) + } + .accessibilityElement(children: .combine) + .accessibilityLabel(message) } } // MARK: - Continuous liquid orb +/// Always-moving liquid-glass orb driven by overlapping harmonics. +/// +/// Glow is drawn on an oversized canvas and never forced through a tight +/// square frame (that was the hard box edge). Stretch is baked into the path +/// instead of `scaleEffect`, which also clips. private struct LiquidOrbMorph: View { let time: Double let size: CGFloat - private var canvasSize: CGFloat { size * 1.55 } + /// Layout size / visual body size — room for blur falloff past the orb edge. + static let layoutScale: CGFloat = 2.6 + + private var canvasSize: CGFloat { size * Self.layoutScale } var body: some View { let field = LiquidField.sample(at: time) + let side = canvasSize Canvas { context, canvasSize in let center = CGPoint(x: canvasSize.width / 2, y: canvasSize.height / 2) - let logical = min(canvasSize.width, canvasSize.height) / 1.55 - let baseR = logical * 0.36 * field.scale + // Body radius relative to the *logical* orb, not the padded canvas. + let baseR = size * 0.36 * field.scale let blob = softBlobPath(center: center, baseRadius: baseR, field: field) + // Soft ambient halo — large radial fill that fades to clear (no hard edge). + let haloR = baseR * 2.25 + let halo = Path(ellipseIn: CGRect( + x: center.x - haloR, + y: center.y - haloR, + width: haloR * 2, + height: haloR * 2 + )) + context.fill( + halo, + with: .radialGradient( + Gradient(stops: [ + .init(color: Color(red: 0.45, green: 0.75, blue: 1.0).opacity(0.20 + 0.16 * field.glow), location: 0), + .init(color: Color(red: 0.40, green: 0.70, blue: 1.0).opacity(0.08), location: 0.42), + .init(color: .clear, location: 1) + ]), + center: center, + startRadius: 0, + endRadius: haloR + ) + ) + + // Blurred body glow — kept moderate so it dies out before the canvas edge. var glow = context - glow.opacity = 0.28 + 0.36 * field.glow - glow.addFilter(.blur(radius: baseR * (0.55 + 0.15 * field.glow))) - glow.fill(blob, with: .color(Color(red: 0.40, green: 0.72, blue: 1.0))) + glow.opacity = 0.28 + 0.30 * field.glow + glow.addFilter(.blur(radius: baseR * 0.55)) + glow.fill(blob, with: .color(Color(red: 0.42, green: 0.74, blue: 1.0))) var bloom = context - bloom.opacity = 0.16 + 0.20 * field.glow - bloom.addFilter(.blur(radius: baseR * 0.85)) - bloom.fill(blob, with: .color(Color(red: 0.55, green: 0.85, blue: 1.0))) + bloom.opacity = 0.12 + 0.16 * field.glow + bloom.addFilter(.blur(radius: baseR * 0.9)) + bloom.fill(blob, with: .color(Color(red: 0.55, green: 0.86, blue: 1.0))) + // Liquid body — gradient ends transparent so the rim doesn't print a hard cut. context.fill( blob, with: .radialGradient( Gradient(stops: [ .init(color: Color(red: 0.97, green: 0.99, blue: 1.0).opacity(0.96), location: 0), - .init(color: Color(red: 0.62, green: 0.86, blue: 1.0).opacity(0.78), location: 0.34), - .init(color: Color(red: 0.36, green: 0.62, blue: 0.98).opacity(0.52), location: 0.74), - .init(color: Color(red: 0.28, green: 0.50, blue: 0.96).opacity(0.22), location: 1) + .init(color: Color(red: 0.62, green: 0.86, blue: 1.0).opacity(0.78), location: 0.32), + .init(color: Color(red: 0.36, green: 0.62, blue: 0.98).opacity(0.45), location: 0.66), + .init(color: Color(red: 0.30, green: 0.55, blue: 0.96).opacity(0.10), location: 0.88), + .init(color: .clear, location: 1) ]), center: center, startRadius: 0, - endRadius: baseR * 1.5 + endRadius: baseR * 1.12 ) ) + // Very soft rim context.stroke( blob, - with: .linearGradient( - Gradient(colors: [ - Color.white.opacity(0.65), - Color.white.opacity(0.12), - Color(red: 0.55, green: 0.82, blue: 1.0).opacity(0.40) - ]), - startPoint: CGPoint(x: center.x - baseR, y: center.y - baseR), - endPoint: CGPoint(x: center.x + baseR, y: center.y + baseR) - ), - lineWidth: max(0.9, baseR * 0.05) + with: .color(Color.white.opacity(0.22 + 0.14 * field.glow)), + lineWidth: max(0.7, baseR * 0.035) ) - let hx = center.x + CGFloat(cos(field.highlightAngle)) * baseR * 0.24 - let hy = center.y + CGFloat(sin(field.highlightAngle)) * baseR * 0.18 + let hx = center.x + CGFloat(cos(field.highlightAngle)) * baseR * 0.24 * field.squashX + let hy = center.y + CGFloat(sin(field.highlightAngle)) * baseR * 0.18 * field.squashY let highlightR = baseR * (0.30 + 0.08 * field.glow) let highlight = Path(ellipseIn: CGRect( x: hx - highlightR, @@ -295,9 +346,23 @@ private struct LiquidOrbMorph: View { ) ) } - .frame(width: canvasSize, height: canvasSize) - .scaleEffect(x: field.squashX, y: field.squashY) - .frame(width: size, height: size) + .frame(width: side, height: side) + // Feather any residual canvas-edge hardness so glow never reads as a box. + .mask( + RadialGradient( + colors: [ + .white, + .white, + .white.opacity(0.85), + .white.opacity(0.35), + .clear + ], + center: .center, + startRadius: 0, + endRadius: side * 0.50 + ) + ) + .allowsHitTesting(false) } private func softBlobPath(center: CGPoint, baseRadius: CGFloat, field: LiquidField) -> Path { @@ -307,9 +372,10 @@ private struct LiquidOrbMorph: View { let t = Double(i) / Double(steps) let theta = t * 2 * Double.pi let r = baseRadius * CGFloat(field.radius(at: theta)) + // Bake squash into the path (avoids scaleEffect clipping). let point = CGPoint( - x: center.x + r * CGFloat(cos(theta)), - y: center.y + r * CGFloat(sin(theta)) + x: center.x + r * CGFloat(cos(theta)) * field.squashX, + y: center.y + r * CGFloat(sin(theta)) * field.squashY ) if i == 0 { path.move(to: point) @@ -322,6 +388,7 @@ private struct LiquidOrbMorph: View { } } +/// Continuous liquid field — every channel is a sum of sines, so nothing holds. private struct LiquidField { var lobe: Double var lobePhase: Double @@ -385,3 +452,12 @@ private struct LiquidField { DotMatrixLoader.feature .padding() } + +#Preview("Compact + inline") { + VStack(spacing: 24) { + DotMatrixLoader.compact + InlineLoadingRow(message: "Searching…") + InlineLoadingRow(message: "Loading posters…", micro: true) + } + .padding() +} diff --git a/Templates/LiquidOrbLoader/README.md b/Templates/LiquidOrbLoader/README.md index fdeeab2..5ac17fb 100644 --- a/Templates/LiquidOrbLoader/README.md +++ b/Templates/LiquidOrbLoader/README.md @@ -1,82 +1,133 @@ # Liquid orb loading mark (DotMatrixLoader) -Portable SwiftUI loading chrome proven in **Harborlight** (iOS 26). Use this when you want a full-surface loading state that feels continuous and liquid, not a discrete spinner or keyframed “pose” loop. +Portable SwiftUI loading chrome proven in **Harborlight** (iOS 26). Use this for **indeterminate** full-surface and inline loading that feels continuous and liquid—not a discrete spinner or keyframed “pose” loop. + +**Proven consumer:** Harborlight · `Harborlight/Components/DotMatrixLoader.swift` +**Last synced:** soft-glow orb (no hard square clip), no brand glyph, app-wide wiring patterns. ## What you get | Piece | Role | | --- | --- | -| `DotMatrixLoader` | 3×3 ring with a continuous highlight sweep; center is a liquid-glass **orb** (or plain / pulse / symbol / emoji) | -| `FeatureLoadingView` | Full-surface “Loading” + detail copy above the mark | -| Continuous harmonics | Orb shape never holds still — REST → MERGE → REBOUND → RELAX emerge from sines, not keyframes | +| `DotMatrixLoader` | 3×3 ring with a continuous highlight sweep; center cases: plain / pulse / symbol / emoji / **orb** | +| `DotMatrixLoader.feature` | Hero full-surface mark (generous ring air) | +| `DotMatrixLoader.compact` | Cards, forms, detail sections | +| `DotMatrixLoader.micro` | Poster tiles, dense banners | +| `FeatureLoadingView` | Full-surface title + detail + hero orb | +| `InlineLoadingRow` | Compact/micro orb + message for forms and panels | +| Continuous harmonics | REST → MERGE → REBOUND → RELAX emerge from sines—not keyframe holds | | Reduce Motion | `TimelineView` pauses; orb freezes on the first frame | ## When to use -- **Yes:** Screen-level `FeatureState.loading`, empty-shell first fetch, developer loading previews. -- **No:** Determinate progress (use `ProgressView(value:)`), poster tile placeholders, tiny toolbar busy indicators, high-frequency keyboard paths. +| Use | Avoid | +| --- | --- | +| Screen-level `FeatureState.loading` | Determinate progress → `ProgressView(value:)` | +| Empty-shell first fetch | High-frequency or keyboard-triggered chrome | +| Form “Searching…” / “Loading defaults…” | Putting private hostnames in detail copy | +| Detail panels (“Loading episodes…”) | Landing this in foundation `archive/Sources/` unless asked | +| Poster tile busy state (`.micro`) | | -Frequency note (Emil / Apple design): loading chrome is occasional → motion is appropriate. Keep Reduce Motion freezes. +Frequency note: loading chrome is occasional → motion is appropriate. Always honor Reduce Motion. ## Drop into a consumer app 1. Copy `DotMatrixLoader.swift` into the app target (e.g. `Components/`). -2. Wire design tokens if you have them: +2. Map tokens if you have a design system: ```swift - // Optional: map to your design system DotMatrixLoader( center: .orb, - tint: AppColor.accent, // default: Color.accentColor + tint: AppColor.accent, // default: Color.accentColor idleTint: AppColor.secondaryLabel // default: Color.secondary ) ``` -3. Use the presets: +3. Use the right density: ```swift // Full-screen feature loading FeatureLoadingView(title: "Loading", detail: "Refreshing your library.") - // Or the mark alone - DotMatrixLoader.feature // generous spacing, hero orb - DotMatrixLoader.compact // cards / inline + // Mark alone + DotMatrixLoader.feature // hero + DotMatrixLoader.compact // forms / cards + DotMatrixLoader.micro // posters / dense chrome + + // Inline row + InlineLoadingRow(message: "Searching…") + InlineLoadingRow(message: "Loading posters…", micro: true) ``` -4. Replace stock loading surfaces: +4. Wire full-screen `FeatureState` loading: ```swift switch state { case .loading: FeatureLoadingView() + .frame(maxWidth: .infinity, minHeight: 360) // ... } ``` -5. Accessibility: +5. **Layout critical:** put `FeatureStateView` (or the loading branch) **outside** `ScrollView`. + If the loader sits inside a scroll view, `maxHeight: .infinity` collapses and the mark looks wrong. + + ```swift + // Preferred structure + FeatureStateView(state: state, retry: load) { content in + ScrollView { /* content only */ } + } + + // Avoid: ScrollView { FeatureStateView { … } } // loader collapses + ``` + +6. Accessibility: - Mark: `accessibilityLabel("Loading")`, not an image. - - Surface: combined label `"\(title). \(detail)"` + a stable `accessibilityIdentifier` if you use inspection routes. + - Surface: combined `"\(title). \(detail)"` + a stable `accessibilityIdentifier` if you use inspection routes. ## Design rationale (keep these) | Choice | Why | | --- | --- | -| Harmonics, not keyframes | Smoothstep keyframes zero velocity at holds → feels “stop then go”. Sines never stop. | -| Padded canvas (~1.55×) | Glow and stretch must not clip to a hard square. | -| Ring step from orb size | Prevents the liquid body from overlapping ring dots. | +| Harmonics, not keyframes | Smoothstep keyframes zero velocity at holds → “stop then go”. Sines never stop. | +| Oversized canvas (`layoutScale` ≈ 2.6) | Blur/glow needs room past the body; tight frames print a **hard square**. | +| No `scaleEffect` + tight secondary frame | That combo was the visible “box” around the orb. Bake squash into the polar path. | +| Radial mask + transparent gradient stops | Feathers residual canvas-edge hardness; body gradient ends in `.clear`. | +| Ring step from orb size | Liquid body must not overlap ring dots. | | No brand glyph in the orb | Glyphs read as a frozen logo on a morphing blob; pure liquid reads cleaner. | -| `TimelineView(.animation)` | No `Timer`, no lifecycle dance; pauses under Reduce Motion. | +| `TimelineView(.animation)` | No `Timer`; pauses under Reduce Motion. | +| Presets scale with `dotSize` | `.feature` / `.compact` / `.micro` stay proportional (orb size ≈ `dotSize * 5.2`). | -## Proven source +### Soft-glow anti-box checklist (when re-tuning visuals) -- App: Harborlight (`Harborlight/Components/DotMatrixLoader.swift`) -- Primary consumers: `FeatureStateView` loading case, Services first load, Settings developer preview +1. Canvas side ≥ body × **2.6** (or more). +2. Never apply `.frame(small)` **after** `scaleEffect` on the orb. +3. Body radial gradient last stop: **clear**. +4. Optional: mask the canvas with a soft radial white→clear gradient. +5. Parent bounds must include `orbSize * layoutScale` so the ring layout does not clip the glow. + +## Proven Harborlight wiring + +| Surface | Pattern | +| --- | --- | +| Home / Library / Calendar / Downloads | `FeatureStateView` → `FeatureLoadingView` (loader outside `ScrollView`) | +| Services first load | `FeatureLoadingView(detail: "Checking configured services…")` | +| Settings developer preview | Sheet with `FeatureLoadingView` (~3s auto-dismiss) | +| Release sheets | Via `FeatureStateView` | +| Poster tiles | `DotMatrixLoader.micro` | +| Poster progress banner | `InlineLoadingRow(..., micro: true)` | +| Add media search / defaults | `InlineLoadingRow` in form sections | +| Detail: episodes / *arr submit / qBit check | `InlineLoadingRow` | +| Transfer / download / completion bars | Keep **determinate** `ProgressView(value:)` | ## Verification checklist - [ ] Reduce Motion freezes the mark - [ ] Light and dark: orb remains readable on ambient background +- [ ] **No hard rectangular box** around the glow (screenshot at peak glow) - [ ] Ring dots do not collide with the orb at peak scale +- [ ] Full-screen loader fills the viewport (not collapsed in a `ScrollView`) - [ ] VoiceOver announces loading without treating the canvas as a photo - [ ] Inspection / screenshot route if the app has deterministic loading states @@ -85,3 +136,4 @@ Frequency note (Emil / Apple design): loading chrome is occasional → motion is - Land this into AppleDevelopmentFoundation `archive/Sources/` unless you are explicitly expanding that package. - Animate keyboard-triggered chrome with this mark. - Put credentials or private hostnames on the loading detail string. +- Use this for determinate percent complete—keep `ProgressView(value:)`. diff --git a/docs/workflow/task-recipes.mdx b/docs/workflow/task-recipes.mdx index 42ea196..4160444 100644 --- a/docs/workflow/task-recipes.mdx +++ b/docs/workflow/task-recipes.mdx @@ -58,21 +58,23 @@ Details: [Bootstrap](/skills/bootstrap). Read each skill’s `SKILL.md` for checklists. Bootstrap chain context: [Bootstrap](/skills/bootstrap). -### Liquid orb full-surface loading mark +### Liquid orb loading mark (full-surface + inline) -Proven pattern (Harborlight). Continuous 3×3 ring + liquid-glass **orb** center (no brand glyph). Prefer harmonics over keyframe holds so the mark never “pauses.” +Proven pattern (Harborlight). Continuous 3×3 ring + liquid-glass **orb** (no brand glyph). Harmonics, not keyframe holds. Soft glow must **not** clip to a hard square. | Step | Action | | --- | --- | -| Template | Copy from repo `Templates/LiquidOrbLoader/` (`README.md` + drop-in `DotMatrixLoader.swift`) | -| Integrate | Drop into consumer `Components/`; map `tint` / spacing to app tokens; use `FeatureLoadingView` for screen-level `loading`, `DotMatrixLoader.compact` for cards | -| Replace | Stock `ContentUnavailableView` / `ProgressView()` **full-screen** loading only — keep determinate `ProgressView(value:)` and poster-tile spinners | -| A11y | Reduce Motion freezes via `TimelineView`; label `"Loading"` on the mark; combined title+detail on the surface | -| Tools | XcodeBuildMCP build + optional inspection screenshot of the loading route | -| Verify | Light/dark contrast; no ring/orb overlap at peak scale; Reduce Motion freeze; VoiceOver not treating canvas as an image | -| Do not | Animate keyboard chrome; put private hostnames in detail copy; expand foundation `archive/Sources/` for this drop-in | - -Proven consumer wiring: Harborlight `FeatureStateView` + Services first load + Settings developer preview. +| Template | `Templates/LiquidOrbLoader/` — `README.md` (wiring + anti-box checklist) + drop-in `DotMatrixLoader.swift` | +| Full-screen | `FeatureLoadingView` for `FeatureState.loading`; put loader **outside** `ScrollView` so it fills the viewport | +| Inline | `InlineLoadingRow` / `DotMatrixLoader.compact` / `.micro` for forms, detail panels, poster tiles | +| Replace | Indeterminate `ProgressView()` busy chrome only — keep determinate `ProgressView(value:)` | +| Glow | Oversized canvas (`layoutScale` ≈ 2.6); bake squash into path; no `scaleEffect` + tight frame; radial mask + transparent gradient stops | +| A11y | Reduce Motion freezes via `TimelineView`; mark label `"Loading"`; surface combines title+detail | +| Tools | XcodeBuildMCP build + optional loading inspection screenshot | +| Verify | No rectangular glow box; light/dark; ring/orb clearance; Reduce Motion freeze; loader not collapsed in scroll | +| Do not | Keyboard chrome; private hostnames in detail; expand foundation `archive/Sources/` for this drop-in | + +Proven consumer: Harborlight Home/Library/Calendar/Downloads/Services + forms/details/posters. Full notes: `Templates/LiquidOrbLoader/README.md`. ### Fix a bug From 9f59eeb34e05732c14d630397897d12bc52a0107 Mon Sep 17 00:00:00 2001 From: brbndon Date: Tue, 4 Aug 2026 12:10:06 -0700 Subject: [PATCH 18/21] docs(templates): sync LiquidOrbLoader with audit fixes - Micro economy path and tile-safe sizing in drop-in Swift - Document per-surface loading copy and ScrollView rule for Services/release sheets - Task recipe: micro cost + screen-specific detail --- .../LiquidOrbLoader/DotMatrixLoader.swift | 63 +++++++++++++------ Templates/LiquidOrbLoader/README.md | 28 +++++---- docs/workflow/task-recipes.mdx | 5 +- 3 files changed, 64 insertions(+), 32 deletions(-) diff --git a/Templates/LiquidOrbLoader/DotMatrixLoader.swift b/Templates/LiquidOrbLoader/DotMatrixLoader.swift index 72181db..5386b8d 100644 --- a/Templates/LiquidOrbLoader/DotMatrixLoader.swift +++ b/Templates/LiquidOrbLoader/DotMatrixLoader.swift @@ -3,7 +3,8 @@ import SwiftUI // MARK: - Drop-in template (portable) // // Source of truth: Harborlight DotMatrixLoader + FeatureLoadingView + InlineLoadingRow. -// Soft-glow orb (no hard square clip). Defaults use system colors — map tint to app tokens. +// Soft-glow orb (no hard square clip). Micro uses economy draw (30 Hz, fewer path steps). +// Defaults use system colors — map tint to app tokens. // See Templates/LiquidOrbLoader/README.md for layout, anti-box checklist, and wiring. // @@ -39,12 +40,12 @@ struct DotMatrixLoader: View { @Environment(\.accessibilityReduceMotion) private var reduceMotion init( - center: Center = .pulse, + center: Center = .orb, dotSize: CGFloat = 12, spacing: CGFloat = 10, period: TimeInterval = 1.8, tint: Color = .accentColor, - idleTint: Color = .secondary + idleTint: Color = Color.secondary ) { self.center = center self.dotSize = dotSize @@ -64,9 +65,15 @@ struct DotMatrixLoader: View { DotMatrixLoader(center: .orb, dotSize: 7, spacing: 14, period: 2.8) } - /// Tiny mark for poster tiles and dense chrome. + /// Tiny mark for poster tiles and dense chrome (fits ~52pt compact posters). static var micro: DotMatrixLoader { - DotMatrixLoader(center: .orb, dotSize: 5, spacing: 9, period: 2.8) + DotMatrixLoader(center: .orb, dotSize: 4, spacing: 6, period: 2.8) + } + + /// True for dense tile marks — cheaper timeline + simpler orb draw. + private var isMicroOrb: Bool { + if case .orb = center { return dotSize <= 4.5 } + return false } /// Ring step. Orb mode pushes the ring out so the liquid mark has room. @@ -80,7 +87,7 @@ struct DotMatrixLoader: View { /// Scales with `dotSize` so `.feature` / `.compact` / `.micro` stay proportional. private var orbSize: CGFloat { switch center { - case .orb: max(dotSize * 5.2, 24) + case .orb: isMicroOrb ? max(dotSize * 4.5, 18) : max(dotSize * 5.2, 24) default: dotSize } } @@ -92,17 +99,26 @@ struct DotMatrixLoader: View { } } + private var layoutScale: CGFloat { + isMicroOrb ? LiquidOrbMorph.microLayoutScale : LiquidOrbMorph.layoutScale + } + private var bounds: CGFloat { let ringExtent = step * 2 + ringDotSize switch center { // Canvas is oversized for soft glow falloff — must not clip to a square. - case .orb: return max(ringExtent, orbSize * LiquidOrbMorph.layoutScale) + case .orb: return max(ringExtent, orbSize * layoutScale) default: return ringExtent } } + private var timelineInterval: TimeInterval { + // Many micros can appear during progressive poster load; 30 Hz is enough there. + isMicroOrb ? 1 / 30 : 1 / 60 + } + var body: some View { - TimelineView(.animation(minimumInterval: 1 / 60, paused: reduceMotion)) { context in + TimelineView(.animation(minimumInterval: timelineInterval, paused: reduceMotion)) { context in let time = reduceMotion ? 0 : context.date.timeIntervalSinceReferenceDate / period ZStack { ForEach(Array(Self.ring.enumerated()), id: \.offset) { index, coord in @@ -154,7 +170,7 @@ struct DotMatrixLoader: View { .scaleEffect(breath) .rotationEffect(.degrees(tilt)) case .orb: - LiquidOrbMorph(time: time, size: orbSize) + LiquidOrbMorph(time: time, size: orbSize, economy: isMicroOrb) } } @@ -170,7 +186,7 @@ struct DotMatrixLoader: View { /// Full-surface loading chrome used by `FeatureStateView` and the developer preview. struct FeatureLoadingView: View { var title: String = "Loading" - var detail: String = "Refreshing your services." + var detail: String = "Just a moment." var compact: Bool = false var body: some View { @@ -229,11 +245,17 @@ struct InlineLoadingRow: View { private struct LiquidOrbMorph: View { let time: Double let size: CGFloat + /// Fewer path samples + single blur for dense tile marks (multi-poster load). + var economy: Bool = false /// Layout size / visual body size — room for blur falloff past the orb edge. static let layoutScale: CGFloat = 2.6 + /// Tighter pad for micro so the mark fits compact poster tiles (~52pt). + static let microLayoutScale: CGFloat = 2.0 - private var canvasSize: CGFloat { size * Self.layoutScale } + private var canvasSize: CGFloat { + size * (economy ? Self.microLayoutScale : Self.layoutScale) + } var body: some View { let field = LiquidField.sample(at: time) @@ -245,7 +267,7 @@ private struct LiquidOrbMorph: View { let blob = softBlobPath(center: center, baseRadius: baseR, field: field) // Soft ambient halo — large radial fill that fades to clear (no hard edge). - let haloR = baseR * 2.25 + let haloR = baseR * (economy ? 1.85 : 2.25) let halo = Path(ellipseIn: CGRect( x: center.x - haloR, y: center.y - haloR, @@ -269,13 +291,15 @@ private struct LiquidOrbMorph: View { // Blurred body glow — kept moderate so it dies out before the canvas edge. var glow = context glow.opacity = 0.28 + 0.30 * field.glow - glow.addFilter(.blur(radius: baseR * 0.55)) + glow.addFilter(.blur(radius: baseR * (economy ? 0.40 : 0.55))) glow.fill(blob, with: .color(Color(red: 0.42, green: 0.74, blue: 1.0))) - var bloom = context - bloom.opacity = 0.12 + 0.16 * field.glow - bloom.addFilter(.blur(radius: baseR * 0.9)) - bloom.fill(blob, with: .color(Color(red: 0.55, green: 0.86, blue: 1.0))) + if !economy { + var bloom = context + bloom.opacity = 0.12 + 0.16 * field.glow + bloom.addFilter(.blur(radius: baseR * 0.9)) + bloom.fill(blob, with: .color(Color(red: 0.55, green: 0.86, blue: 1.0))) + } // Liquid body — gradient ends transparent so the rim doesn't print a hard cut. context.fill( @@ -367,7 +391,7 @@ private struct LiquidOrbMorph: View { private func softBlobPath(center: CGPoint, baseRadius: CGFloat, field: LiquidField) -> Path { var path = Path() - let steps = 160 + let steps = economy ? 64 : 160 for i in 0...steps { let t = Double(i) / Double(steps) let theta = t * 2 * Double.pi @@ -444,8 +468,9 @@ private struct LiquidField { } } + #Preview("Feature loading") { - FeatureLoadingView() + FeatureLoadingView(detail: "Refreshing your library.") } #Preview("Orb mark") { diff --git a/Templates/LiquidOrbLoader/README.md b/Templates/LiquidOrbLoader/README.md index 5ac17fb..2526264 100644 --- a/Templates/LiquidOrbLoader/README.md +++ b/Templates/LiquidOrbLoader/README.md @@ -3,7 +3,7 @@ Portable SwiftUI loading chrome proven in **Harborlight** (iOS 26). Use this for **indeterminate** full-surface and inline loading that feels continuous and liquid—not a discrete spinner or keyframed “pose” loop. **Proven consumer:** Harborlight · `Harborlight/Components/DotMatrixLoader.swift` -**Last synced:** soft-glow orb (no hard square clip), no brand glyph, app-wide wiring patterns. +**Last synced:** soft-glow orb, per-surface loading copy, micro economy path, loader outside `ScrollView` (incl. Services + release sheets). ## What you get @@ -12,8 +12,8 @@ Portable SwiftUI loading chrome proven in **Harborlight** (iOS 26). Use this for | `DotMatrixLoader` | 3×3 ring with a continuous highlight sweep; center cases: plain / pulse / symbol / emoji / **orb** | | `DotMatrixLoader.feature` | Hero full-surface mark (generous ring air) | | `DotMatrixLoader.compact` | Cards, forms, detail sections | -| `DotMatrixLoader.micro` | Poster tiles, dense banners | -| `FeatureLoadingView` | Full-surface title + detail + hero orb | +| `DotMatrixLoader.micro` | Poster tiles (~52pt-safe), dense banners; 30 Hz + lighter Canvas | +| `FeatureLoadingView` | Full-surface title + detail + hero orb (pass **screen-specific** detail) | | `InlineLoadingRow` | Compact/micro orb + message for forms and panels | | Continuous harmonics | REST → MERGE → REBOUND → RELAX emerge from sines—not keyframe holds | | Reduce Motion | `TimelineView` pauses; orb freezes on the first frame | @@ -59,19 +59,23 @@ Frequency note: loading chrome is occasional → motion is appropriate. Always h InlineLoadingRow(message: "Loading posters…", micro: true) ``` -4. Wire full-screen `FeatureState` loading: +4. Wire full-screen `FeatureState` loading with **surface-specific** copy: ```swift switch state { case .loading: - FeatureLoadingView() + FeatureLoadingView(title: "Loading", detail: "Refreshing your library.") .frame(maxWidth: .infinity, minHeight: 360) // ... } + + // If you wrap FeatureState in a reusable view, pass loadingDetail per screen + // (do not reuse a generic “services” string on Library/Calendar/Downloads). ``` 5. **Layout critical:** put `FeatureStateView` (or the loading branch) **outside** `ScrollView`. - If the loader sits inside a scroll view, `maxHeight: .infinity` collapses and the mark looks wrong. + If the loader sits inside a scroll view, `maxHeight: .infinity` collapses and the mark looks wrong. + Same rule for **Services first load** and **release sheets**—not only tab roots. ```swift // Preferred structure @@ -80,6 +84,7 @@ Frequency note: loading chrome is occasional → motion is appropriate. Always h } // Avoid: ScrollView { FeatureStateView { … } } // loader collapses + // Avoid: ScrollView { if isLoading { FeatureLoadingView() } … } ``` 6. Accessibility: @@ -97,7 +102,8 @@ Frequency note: loading chrome is occasional → motion is appropriate. Always h | Ring step from orb size | Liquid body must not overlap ring dots. | | No brand glyph in the orb | Glyphs read as a frozen logo on a morphing blob; pure liquid reads cleaner. | | `TimelineView(.animation)` | No `Timer`; pauses under Reduce Motion. | -| Presets scale with `dotSize` | `.feature` / `.compact` / `.micro` stay proportional (orb size ≈ `dotSize * 5.2`). | +| Presets scale with `dotSize` | `.feature` / `.compact` proportional (orb ≈ `dotSize * 5.2`); `.micro` tighter (≈4.5×, layoutScale 2.0) for compact posters. | +| Micro economy path | 30 Hz timeline, 64 path steps, single blur — safe when many tiles load at once. | ### Soft-glow anti-box checklist (when re-tuning visuals) @@ -111,11 +117,11 @@ Frequency note: loading chrome is occasional → motion is appropriate. Always h | Surface | Pattern | | --- | --- | -| Home / Library / Calendar / Downloads | `FeatureStateView` → `FeatureLoadingView` (loader outside `ScrollView`) | -| Services first load | `FeatureLoadingView(detail: "Checking configured services…")` | +| Home / Library / Calendar / Downloads | `FeatureStateView` → `FeatureLoadingView` with **per-screen** `loadingDetail` (loader outside `ScrollView`) | +| Services first load | `FeatureLoadingView` **outside** `ScrollView` (`detail: "Checking configured services…"`) | | Settings developer preview | Sheet with `FeatureLoadingView` (~3s auto-dismiss) | -| Release sheets | Via `FeatureStateView` | -| Poster tiles | `DotMatrixLoader.micro` | +| Release sheets | `FeatureStateView` **outside** `ScrollView` (`loadingDetail: "Looking up releases."`) | +| Poster tiles | `DotMatrixLoader.micro` (economy path) | | Poster progress banner | `InlineLoadingRow(..., micro: true)` | | Add media search / defaults | `InlineLoadingRow` in form sections | | Detail: episodes / *arr submit / qBit check | `InlineLoadingRow` | diff --git a/docs/workflow/task-recipes.mdx b/docs/workflow/task-recipes.mdx index 4160444..9b04a39 100644 --- a/docs/workflow/task-recipes.mdx +++ b/docs/workflow/task-recipes.mdx @@ -65,10 +65,11 @@ Proven pattern (Harborlight). Continuous 3×3 ring + liquid-glass **orb** (no br | Step | Action | | --- | --- | | Template | `Templates/LiquidOrbLoader/` — `README.md` (wiring + anti-box checklist) + drop-in `DotMatrixLoader.swift` | -| Full-screen | `FeatureLoadingView` for `FeatureState.loading`; put loader **outside** `ScrollView` so it fills the viewport | +| Full-screen | `FeatureLoadingView` for `FeatureState.loading` with **screen-specific** detail; put loader **outside** `ScrollView` (tabs, Services, release sheets) | | Inline | `InlineLoadingRow` / `DotMatrixLoader.compact` / `.micro` for forms, detail panels, poster tiles | | Replace | Indeterminate `ProgressView()` busy chrome only — keep determinate `ProgressView(value:)` | -| Glow | Oversized canvas (`layoutScale` ≈ 2.6); bake squash into path; no `scaleEffect` + tight frame; radial mask + transparent gradient stops | +| Glow | Oversized canvas (`layoutScale` ≈ 2.6; micro ≈ 2.0); bake squash into path; no `scaleEffect` + tight frame; radial mask + transparent gradient stops | +| Micro cost | Economy path (30 Hz, fewer path steps, single blur) when many tiles can load together | | A11y | Reduce Motion freezes via `TimelineView`; mark label `"Loading"`; surface combines title+detail | | Tools | XcodeBuildMCP build + optional loading inspection screenshot | | Verify | No rectangular glow box; light/dark; ring/orb clearance; Reduce Motion freeze; loader not collapsed in scroll | From 5f23dbb04d3c5ddf960cc73e0e0bd9deeb7fad8b Mon Sep 17 00:00:00 2001 From: brbndon Date: Wed, 5 Aug 2026 01:26:13 -0700 Subject: [PATCH 19/21] docs(web-marketing): document proven landing page recipe - Add landing-page-recipe.md to apple-app-marketing-site: homepage anatomy (screens, benefits, social proof, pricing, FAQ), honesty conventions, real-screenshot capture via XcodeBuildMCP + Maestro, and the mandatory link/form/width verification pass - Update structure-and-pages.md homepage sections and SKILL.md implementation order to the proven anatomy - Add LANDING_RECIPE.md to the AppMarketingSite template pack; update README table, STRUCTURE composition, and CHECKLIST items Verification: ./Scripts/verify-skills.sh (16 skills, 77 fixtures) and generate-skill-catalog.py --check both pass. --- .../skills/apple-app-marketing-site/SKILL.md | 7 +- .../references/landing-page-recipe.md | 128 ++++++++++++++++++ .../references/structure-and-pages.md | 18 ++- Templates/AppMarketingSite/CHECKLIST.md | 4 + Templates/AppMarketingSite/LANDING_RECIPE.md | 62 +++++++++ Templates/AppMarketingSite/README.md | 1 + Templates/AppMarketingSite/STRUCTURE.md | 2 +- 7 files changed, 212 insertions(+), 10 deletions(-) create mode 100644 .agents/skills/apple-app-marketing-site/references/landing-page-recipe.md create mode 100644 Templates/AppMarketingSite/LANDING_RECIPE.md diff --git a/.agents/skills/apple-app-marketing-site/SKILL.md b/.agents/skills/apple-app-marketing-site/SKILL.md index d3a0f04..57b9654 100644 --- a/.agents/skills/apple-app-marketing-site/SKILL.md +++ b/.agents/skills/apple-app-marketing-site/SKILL.md @@ -76,14 +76,15 @@ Full token tables, component inventory, and page patterns: - [references/retheme-guide.md](references/retheme-guide.md) - [references/structure-and-pages.md](references/structure-and-pages.md) +- [references/landing-page-recipe.md](references/landing-page-recipe.md) — proven homepage anatomy (screens, benefits, social proof, pricing, FAQ), real-screenshot capture, and the mandatory link/form/width verification pass -Human-facing template pack (copy into consumer projects): repository path `Templates/AppMarketingSite/` (`README.md`, `RETHEME.md`, `STRUCTURE.md`, `CHECKLIST.md`, `TOKEN_REFERENCE.css`). +Human-facing template pack (copy into consumer projects): repository path `Templates/AppMarketingSite/` (`README.md`, `RETHEME.md`, `STRUCTURE.md`, `CHECKLIST.md`, `LANDING_RECIPE.md`, `TOKEN_REFERENCE.css`). ## Implementation order 1. Scaffold Astro + Tailwind in `web/`; configure `site` + trailing slashes. 2. Tokens + `BaseLayout` / `Header` / `Footer` (static). -3. Homepage (hero, mockup, trust, story, feature, privacy band). +3. Homepage (hero, trust, screenshot gallery, benefits, clarity visual, social proof, pricing, FAQ, final CTA band — anatomy in `references/landing-page-recipe.md`). 4. Secondary pages with shared `PageIntro` + `LegalProse`. 5. Minimal islands only where needed. 6. `npm run build`; responsive pass; Vercel config. @@ -94,7 +95,7 @@ Human-facing template pack (copy into consumer projects): repository path `Templ cd web && npm run build ``` -Confirm routes emit under `dist/` with trailing-slash directories, header/footer present **without** client JS, and no third-party script tags. Optional: `npm run preview` and check mobile hero + mockup. +Confirm routes emit under `dist/` with trailing-slash directories, header/footer present **without** client JS, and no third-party script tags. Then run the full verification pass (every link, forms, desktop + mobile widths, a11y spot-checks) described in `references/landing-page-recipe.md`. ## Reference implementation diff --git a/.agents/skills/apple-app-marketing-site/references/landing-page-recipe.md b/.agents/skills/apple-app-marketing-site/references/landing-page-recipe.md new file mode 100644 index 0000000..8673ee0 --- /dev/null +++ b/.agents/skills/apple-app-marketing-site/references/landing-page-recipe.md @@ -0,0 +1,128 @@ +# Homepage recipe — anatomy, real screenshots, verification + +The proven homepage pattern from **Group Trip Money** (`GroupTripMoney/web/`). +Everything is data-driven: structure, tokens, and workflow stay identical +across products; only frontmatter content changes. Read the example repo +before building; keep this reference as the spec. + +## Homepage anatomy (section order) + +All copy lives in frontmatter `as const` arrays at the top of +`src/pages/index.astro` (`screens`, `benefits`, `testimonialPlaceholders`, +`pricingPoints`, `faqs`) so the next product swap only edits data. + +1. **Hero** — eyebrow, `h1.display-title` (two short lines, e.g. "Plan + together. Split fairly."), `.lede`, CTA trio, CSS device mockup right. + CTA trio: status pill (`span.btn.btn-coming-soon`, `role="status"`, + `aria-describedby` → microcopy note), `a.btn.btn-secondary[href="#screens"]` + "See it in action", text link to `/help/`. +2. **Trust strip** — thin divider + centered row of three privacy/product + promises that match the app's real behavior. +3. **Screenshot gallery** (`id="screens"`) — exactly 4 real captures + (Overview, Expenses, Itinerary, Trips). Mobile: horizontal + `snap-x snap-mandatory` scroll, `w-[72vw] max-w-[16.5rem]` cards; `sm+`: + `grid sm:grid-cols-2 lg:grid-cols-4`. Each item: `figure` → rounded frame + (`rounded-[1.4rem] ring-1 ring-black/10 shadow-soft`) → `img` + (`width="640" height="1392"`, `loading="lazy"`, descriptive `alt`) → + `figcaption` (bold title + muted caption). Never CSS illustrations here — + only captures of the real app. +4. **Benefits** — full-bleed `#efeff1` band, `ul.grid sm:grid-cols-2 + lg:grid-cols-3` of 6 cards (`rounded-[1.25rem] border border-line + bg-surface p-6 shadow-soft`) with uppercase kicker, `h3`, muted body. + One benefit per real capability. +5. **Clarity / product visual** — two-column grid; calm diagram card + (flat cards, `shadow-soft`, no stickers/gradients) + copy + short bullet + list with 5px ink dot markers. +6. **Social proof placeholders** — full-bleed band, `md:grid-cols-3` quote + cards: `blockquote` in curly quotes + `footer` with 36px initials avatar + (soft pastel bg), name, context line. Pre-launch: array is + `testimonialPlaceholders` with a `// PLACEHOLDER` comment — never present + invented people as real customers. +7. **Pricing** — centered; one card (`max-w-md`, `rounded-[1.5rem]`, + `p-8 md:p-10`) with app name + "Free" (or real price), border-t bullet + list with ink dots, full-width coming-soon status pill, microcopy. Only + state pricing facts the product really has. +8. **FAQ** — `max-w-3xl border-t border-line` list of 5 native + `details.faq-item` rows (`border-b border-line`, `+`/`−` circle + indicators), plus a muted line linking `/help/`. Answers describe the + app's real behavior (accounts? money movement? storage? launch timing). +9. **Final CTA / privacy band** — dark `bg-night rounded-[1.5rem]` panel: + eyebrow + `h2` ("Your next trip will thank you."), body combining privacy + posture + launch status (`#a1a1a6`), right column with `a.btn.btn-on-dark` + → `#screens` and underline link → `/privacy/`. + +Section rhythm: page canvas (`#f5f5f7`) alternating with full-bleed +`#efeff1` bands; `py-24 md:py-32` section padding; `site-shell` +(`min(1120px, calc(100% - 2.5rem))`). + +## Honesty conventions + +- Pre-launch CTAs are **status controls**, not fake buttons: `role="status"` + + `aria-describedby` + microcopy ("The App Store listing is not available + yet."). No dead App Store links. +- Social proof is placeholder data until real users exist, marked in code. +- Every claim traces to something the app actually does — read the app + source first; never invent features, pricing, or privacy posture. + +## Capturing real screenshots + +Fixture mode first: launch the app with its fixture launch argument (e.g. +`-useFixtureData` seeding an in-memory container) via XcodeBuildMCP +(`build_run_sim` with `launchArgs`). Then drive navigation with a throwaway +Maestro flow in `/tmp` — never the repo's committed `.maestro/` suite: + +- Do **not** include `launchApp` in the flow: Maestro restarts the app and + its `arguments:` map does not reproduce `-useFixtureData` on iOS, so the + app relaunches without fixtures. Drive the already-running app. +- `assertVisible` only text guaranteed on screen (below-fold assertions + fail); scroll first (`swipe: {direction: UP}`) for below-fold shots. +- Tap rows/buttons by text; tap tab bars by percentage points + (Overview 17% / Itinerary 50% / Expenses 83% at 94% height). Toolbar + buttons like "New trip" exist only on the library screen. +- Maestro terminates the app between runs: one flow covering every screen, + or relaunch with fixtures before each flow. +- Assert distinctive text on every screen before screenshotting; names on + `takeScreenshot` map to content. + +Downscale to 640px wide into `web/public/screenshots/`: +`sips --resampleWidth 640 shot.png --out web/public/screenshots/name.png` +(optionally `xcrun simctl io booted screenshot /tmp/x.png` for full-res). +Delete unused captures; register each in the `screens` array. + +## Verification pass (mandatory) + +1. `cd web && npm run build` — all routes emit, no errors. +2. `npm run preview -- --port 4321`; every route and asset returns 200 + (`/`, `/help/`, `/support/`, `/privacy/`, `/terms/`, screenshots, favicon). +3. Browser automation (agent-browser CLI or equivalent): desktop 1440×900 — + no console/page errors; no horizontal overflow + (`document.documentElement.scrollWidth <= innerWidth`); **every link** + clicked (scroll the element into view first — below-fold clicks miss), + URLs/hashes confirmed; support form: empty submit shows per-field errors + and focuses the first invalid field, valid submit reveals the toast and a + correctly built `mailto:` href; help search filters with result count and + empty state; FAQ `
` toggles; skip link (Tab → Enter → `#main`); + `img` naturalWidth > 0 with non-empty `alt`; heading order h1→h2→h3 + without jumps; `role="status"` pills present. +4. Mobile 390×844: overflow still false (gallery scrolls internally); menu + toggle sets `aria-expanded` and a nav link closes it. +5. Full-page screenshots at both widths; if the model cannot view images, + pixel-sample the PNGs (canvas `#f5f5f7`, bands `#efeff1`, night + `#1d1d1f`, white cards) to confirm sections render. +6. Re-sweep all routes for console errors; commit only intended files + (revert incidental lockfile churn). + +Regenerate screenshots whenever the app UI changes — stale captures are +worse than none. + +## Per-project swap table + +| What changes | Where | +| --- | --- | +| Product name, domain, support email | `astro.config.mjs`, `BaseLayout` meta, `Header`/`Footer`, `SupportForm` address | +| Screenshots | recapture from THIS app | +| Reviews / social proof | `testimonialPlaceholders` array | +| Hero, benefits, FAQ, pricing copy | frontmatter arrays on `index.astro` | +| Privacy / trust claims | `TrustStrip`, final band — match real posture | +| Phone mockup content | `PhoneMockup.astro` (illustrative) | +| Favicon / brand mark | `public/favicon.*`, `BrandMark.astro` | diff --git a/.agents/skills/apple-app-marketing-site/references/structure-and-pages.md b/.agents/skills/apple-app-marketing-site/references/structure-and-pages.md index 8ed179e..a12483b 100644 --- a/.agents/skills/apple-app-marketing-site/references/structure-and-pages.md +++ b/.agents/skills/apple-app-marketing-site/references/structure-and-pages.md @@ -47,13 +47,19 @@ export default defineConfig({ ## Homepage sections (order) -1. **Hero** — eyebrow, display title, lede, coming-soon (or App Store) CTA, secondary text link, device mockup +1. **Hero** — eyebrow, display title, lede, CTA trio (coming-soon status pill, "See it in action" → `#screens`, help link), device mockup 2. **Trust strip** — three short privacy / product promises -3. **Story / journey** — three numbered beats (before / during / after) -4. **Clarity / feature** — diagram or simple visual + bullets -5. **Privacy band** — inverted night panel + link to privacy policy - -Keep copy product-specific; keep rhythm and component reuse. +3. **Screenshot gallery** — 4 real app captures (Overview, Expenses, Itinerary, Trips); snap-scroll on mobile, 4-col grid on desktop +4. **Benefits** — six cards (splitting, budgets, settlements, itinerary, offline, trust) +5. **Clarity / feature** — diagram or simple visual + bullets +6. **Social proof** — quote cards; placeholder data until real users exist +7. **Pricing** — single centered card; only real pricing facts +8. **FAQ** — native `
` rows + help link +9. **Final CTA / privacy band** — inverted night panel, privacy link, `#screens` CTA + +All copy lives in frontmatter `as const` arrays (screens, benefits, +testimonialPlaceholders, pricingPoints, faqs); keep structure and rhythm, +swap only product data. Full spec: `references/landing-page-recipe.md`. ## Secondary pages diff --git a/Templates/AppMarketingSite/CHECKLIST.md b/Templates/AppMarketingSite/CHECKLIST.md index 1e0ae11..9182e4e 100644 --- a/Templates/AppMarketingSite/CHECKLIST.md +++ b/Templates/AppMarketingSite/CHECKLIST.md @@ -7,6 +7,10 @@ - [ ] Support email works - [ ] Help deep links (`#ids`) match support suggestion slugs - [ ] No placeholder lorem +- [ ] Screenshots are real captures of the current build, 640px wide, with descriptive alt text +- [ ] Placeholder testimonials marked in code (pre-launch) +- [ ] Every link and form tested at desktop (1440px) and mobile (390px) +- [ ] No horizontal page overflow at 320px (gallery scrolls internally) ## Visual / UX diff --git a/Templates/AppMarketingSite/LANDING_RECIPE.md b/Templates/AppMarketingSite/LANDING_RECIPE.md new file mode 100644 index 0000000..2ce353f --- /dev/null +++ b/Templates/AppMarketingSite/LANDING_RECIPE.md @@ -0,0 +1,62 @@ +# Landing page recipe + +The proven homepage pattern from the Group Trip Money marketing site. +Structure, tokens, and workflow stay identical across products — only the +frontmatter content changes. + +## Homepage anatomy (build in this order) + +All copy lives in `as const` arrays at the top of `src/pages/index.astro` +(`screens`, `benefits`, `testimonialPlaceholders`, `pricingPoints`, `faqs`). +The next product is a data swap, not a redesign. + +1. **Hero** — eyebrow, display title (two short lines), lede, CTA trio, CSS + device mockup. CTAs: "Coming soon" status pill (`role="status"` + + microcopy note), "See it in action" → `#screens`, "Explore Help Center". +2. **Trust strip** — three privacy/product promises that match the real app. +3. **Screenshot gallery** (`id="screens"`) — 4 real captures (Overview, + Expenses, Itinerary, Trips). Mobile: horizontal snap-scroll, ~72vw cards; + desktop: 4-column grid. Every image needs `alt`, `width`/`height`, lazy + loading. Real screenshots only — the CSS mockup belongs in the hero. +4. **Benefits** — 6 cards on a light band: splitting, budgets, settlements, + itinerary, offline, trust. One card per real capability. +5. **Clarity / product visual** — calm diagram card + copy + short bullets. +6. **Social proof** — quote cards (initials avatar, name, context). Before + launch these are placeholders, marked in code — never invented customers. +7. **Pricing** — one centered card; only real pricing facts ("Free" works + for local-first apps). Full-width status pill when pre-launch. +8. **FAQ** — 5 native `
` rows + link to the Help Center. Answers + describe real behavior: accounts, money movement, storage, launch timing. +9. **Final CTA / privacy band** — dark rounded panel: privacy posture + + launch status, "See it in action" and "Read privacy details" links. + +Section rhythm: `py-24 md:py-32`, canvas `#f5f5f7` alternating with full-bleed +`#efeff1` bands, `site-shell` container. + +## Honesty rules + +- Pre-launch CTAs are status pills with microcopy, never fake App Store + buttons or dead links. +- Every claim traces to something the app actually does — read the app + source first. +- Placeholder testimonials are labeled `PLACEHOLDER` in the frontmatter. + +## Real screenshots (not mockups) + +1. Launch the app with its fixture argument (e.g. `-useFixtureData`) via + XcodeBuildMCP `build_run_sim`. +2. Drive navigation with a throwaway Maestro flow in `/tmp` — do NOT + `launchApp` inside it (restarts the app without fixtures). Tap by text; + tab bars by percentage points (Overview 17% / Itinerary 50% / Expenses + 83% at 94% height). Assert on-screen text before each `takeScreenshot`. +3. Downscale to 640px wide into `web/public/screenshots/`: + `sips --resampleWidth 640 in.png --out web/public/screenshots/name.png`. +4. Regenerate whenever the app UI changes. + +## Ship checklist (additions to CHECKLIST.md) + +- [ ] Screenshots are real captures of the current build, 640px wide, with descriptive alt text +- [ ] Every link and form tested at desktop (1440px) and mobile (390px) +- [ ] No horizontal page overflow at 320px (gallery scrolls internally) +- [ ] Placeholder testimonials marked in code (pre-launch) +- [ ] Full verification pass ran: build → preview → 200s → links/forms → widths → error sweep diff --git a/Templates/AppMarketingSite/README.md b/Templates/AppMarketingSite/README.md index 1f1956c..ab3f11e 100644 --- a/Templates/AppMarketingSite/README.md +++ b/Templates/AppMarketingSite/README.md @@ -11,6 +11,7 @@ This pack documents the polish language proven on consumer apps (light-first, mo | [README.md](./README.md) | This overview | | [RETHEME.md](./RETHEME.md) | Colors, fonts, assets, strings — edit checklist | | [STRUCTURE.md](./STRUCTURE.md) | Folder layout, components, routes | +| [LANDING_RECIPE.md](./LANDING_RECIPE.md) | Homepage anatomy, real-screenshot capture, ship checklist | | [CHECKLIST.md](./CHECKLIST.md) | Ship checklist (a11y, perf, Vercel, content) | | [TOKEN_REFERENCE.css](./TOKEN_REFERENCE.css) | Copy-paste `@theme` starter | diff --git a/Templates/AppMarketingSite/STRUCTURE.md b/Templates/AppMarketingSite/STRUCTURE.md index f23ee83..5396407 100644 --- a/Templates/AppMarketingSite/STRUCTURE.md +++ b/Templates/AppMarketingSite/STRUCTURE.md @@ -55,7 +55,7 @@ Use `trailingSlash: "always"` and directory build format so URLs stay `/help/`-s ## Page composition -**Home:** Hero + device → Trust → Story beats → Feature visual → Privacy band +**Home:** Hero + device → Trust → Screenshot gallery → Benefits → Feature visual → Social proof → Pricing → FAQ → Final CTA band (full spec: `LANDING_RECIPE.md`) **Help:** PageIntro + search → category grids → FAQ details From 66160eabd63e6f636e82257e9865f4c80c57ed9b Mon Sep 17 00:00:00 2001 From: brbndon Date: Wed, 5 Aug 2026 10:28:03 -0700 Subject: [PATCH 20/21] docs(web-marketing): fix screenshot height and unify overflow check widths - Document the actual capture height (640x1391) instead of 1392 - State the mobile overflow check consistently as 320px and 390px across the skill reference and the template pack Verification: ./Scripts/verify-skills.sh passes (16 skills, 77 fixtures). --- .../references/landing-page-recipe.md | 5 +++-- Templates/AppMarketingSite/CHECKLIST.md | 2 +- Templates/AppMarketingSite/LANDING_RECIPE.md | 2 +- 3 files changed, 5 insertions(+), 4 deletions(-) diff --git a/.agents/skills/apple-app-marketing-site/references/landing-page-recipe.md b/.agents/skills/apple-app-marketing-site/references/landing-page-recipe.md index 8673ee0..076bfa5 100644 --- a/.agents/skills/apple-app-marketing-site/references/landing-page-recipe.md +++ b/.agents/skills/apple-app-marketing-site/references/landing-page-recipe.md @@ -23,7 +23,7 @@ All copy lives in frontmatter `as const` arrays at the top of `snap-x snap-mandatory` scroll, `w-[72vw] max-w-[16.5rem]` cards; `sm+`: `grid sm:grid-cols-2 lg:grid-cols-4`. Each item: `figure` → rounded frame (`rounded-[1.4rem] ring-1 ring-black/10 shadow-soft`) → `img` - (`width="640" height="1392"`, `loading="lazy"`, descriptive `alt`) → + (`width="640" height="1391"`, `loading="lazy"`, descriptive `alt`) → `figcaption` (bold title + muted caption). Never CSS illustrations here — only captures of the real app. 4. **Benefits** — full-bleed `#efeff1` band, `ul.grid sm:grid-cols-2 @@ -104,7 +104,8 @@ Delete unused captures; register each in the `screens` array. empty state; FAQ `
` toggles; skip link (Tab → Enter → `#main`); `img` naturalWidth > 0 with non-empty `alt`; heading order h1→h2→h3 without jumps; `role="status"` pills present. -4. Mobile 390×844: overflow still false (gallery scrolls internally); menu +4. Mobile 390×844 (plus the 320px minimum width): overflow still false + (gallery scrolls internally); menu toggle sets `aria-expanded` and a nav link closes it. 5. Full-page screenshots at both widths; if the model cannot view images, pixel-sample the PNGs (canvas `#f5f5f7`, bands `#efeff1`, night diff --git a/Templates/AppMarketingSite/CHECKLIST.md b/Templates/AppMarketingSite/CHECKLIST.md index 9182e4e..35832a7 100644 --- a/Templates/AppMarketingSite/CHECKLIST.md +++ b/Templates/AppMarketingSite/CHECKLIST.md @@ -10,7 +10,7 @@ - [ ] Screenshots are real captures of the current build, 640px wide, with descriptive alt text - [ ] Placeholder testimonials marked in code (pre-launch) - [ ] Every link and form tested at desktop (1440px) and mobile (390px) -- [ ] No horizontal page overflow at 320px (gallery scrolls internally) +- [ ] No horizontal page overflow at 320px and 390px (gallery scrolls internally) ## Visual / UX diff --git a/Templates/AppMarketingSite/LANDING_RECIPE.md b/Templates/AppMarketingSite/LANDING_RECIPE.md index 2ce353f..b7eb5b0 100644 --- a/Templates/AppMarketingSite/LANDING_RECIPE.md +++ b/Templates/AppMarketingSite/LANDING_RECIPE.md @@ -57,6 +57,6 @@ Section rhythm: `py-24 md:py-32`, canvas `#f5f5f7` alternating with full-bleed - [ ] Screenshots are real captures of the current build, 640px wide, with descriptive alt text - [ ] Every link and form tested at desktop (1440px) and mobile (390px) -- [ ] No horizontal page overflow at 320px (gallery scrolls internally) +- [ ] No horizontal page overflow at 320px and 390px (gallery scrolls internally) - [ ] Placeholder testimonials marked in code (pre-launch) - [ ] Full verification pass ran: build → preview → 200s → links/forms → widths → error sweep From 7786a22a1862d4700ac1cb5aae7af1319b619067 Mon Sep 17 00:00:00 2001 From: brbndon Date: Wed, 5 Aug 2026 11:17:13 -0700 Subject: [PATCH 21/21] docs(design-skills): address PR review on validator, recipe, template - validate ui_evidence.forbidden_actions against the schema enum so a schema-invalid fixture cannot pass verification - keep the homepage recipe self-contained; GroupTripMoney example is optional and illustrative only, never the source of truth - replace Harborlight-specific wiring surfaces (media search, *arr, qBit) with neutral surface categories in the LiquidOrbLoader template - carry the same neutralization into structure-and-pages and the consumer LANDING_RECIPE template --- .../references/landing-page-recipe.md | 28 ++++++++++--------- .../references/structure-and-pages.md | 4 +-- .../assets/consumer-AGENTS.md.template | 4 +-- AGENTS.md | 2 +- Evaluations/skill-routing.json | 28 +++++++++++++++---- Scripts/validate-skill-evaluations.py | 7 +++-- Templates/AppMarketingSite/LANDING_RECIPE.md | 13 ++++----- Templates/LiquidOrbLoader/README.md | 18 ++++++------ docs/index.mdx | 2 +- docs/skills/skill-authoring-guide.mdx | 2 +- docs/tools/index.mdx | 2 +- docs/tools/xcodebuildmcp.mdx | 12 +++++--- docs/workflow/agents-md-template.mdx | 4 +-- docs/workflow/index.mdx | 2 +- docs/workflow/multi-service-ios-app.mdx | 2 +- 15 files changed, 78 insertions(+), 52 deletions(-) diff --git a/.agents/skills/apple-app-marketing-site/references/landing-page-recipe.md b/.agents/skills/apple-app-marketing-site/references/landing-page-recipe.md index 076bfa5..b5aedfc 100644 --- a/.agents/skills/apple-app-marketing-site/references/landing-page-recipe.md +++ b/.agents/skills/apple-app-marketing-site/references/landing-page-recipe.md @@ -1,9 +1,10 @@ # Homepage recipe — anatomy, real screenshots, verification -The proven homepage pattern from **Group Trip Money** (`GroupTripMoney/web/`). -Everything is data-driven: structure, tokens, and workflow stay identical -across products; only frontmatter content changes. Read the example repo -before building; keep this reference as the spec. +This reference is the complete homepage spec — self-contained, with no +external checkout required before building. Structure, tokens, and workflow +stay identical across products; only frontmatter content changes. A consumer +example may exist at `GroupTripMoney/web/` where that checkout is available; +it is optional and illustrative only, never the source of truth. ## Homepage anatomy (section order) @@ -11,15 +12,15 @@ All copy lives in frontmatter `as const` arrays at the top of `src/pages/index.astro` (`screens`, `benefits`, `testimonialPlaceholders`, `pricingPoints`, `faqs`) so the next product swap only edits data. -1. **Hero** — eyebrow, `h1.display-title` (two short lines, e.g. "Plan - together. Split fairly."), `.lede`, CTA trio, CSS device mockup right. +1. **Hero** — eyebrow, `h1.display-title` (two short lines), `.lede`, CTA + trio, CSS device mockup right. CTA trio: status pill (`span.btn.btn-coming-soon`, `role="status"`, `aria-describedby` → microcopy note), `a.btn.btn-secondary[href="#screens"]` "See it in action", text link to `/help/`. 2. **Trust strip** — thin divider + centered row of three privacy/product promises that match the app's real behavior. -3. **Screenshot gallery** (`id="screens"`) — exactly 4 real captures - (Overview, Expenses, Itinerary, Trips). Mobile: horizontal +3. **Screenshot gallery** (`id="screens"`) — exactly 4 real captures, one + per main app screen. Mobile: horizontal `snap-x snap-mandatory` scroll, `w-[72vw] max-w-[16.5rem]` cards; `sm+`: `grid sm:grid-cols-2 lg:grid-cols-4`. Each item: `figure` → rounded frame (`rounded-[1.4rem] ring-1 ring-black/10 shadow-soft`) → `img` @@ -47,8 +48,8 @@ All copy lives in frontmatter `as const` arrays at the top of indicators), plus a muted line linking `/help/`. Answers describe the app's real behavior (accounts? money movement? storage? launch timing). 9. **Final CTA / privacy band** — dark `bg-night rounded-[1.5rem]` panel: - eyebrow + `h2` ("Your next trip will thank you."), body combining privacy - posture + launch status (`#a1a1a6`), right column with `a.btn.btn-on-dark` + eyebrow + `h2` (product promise), body combining privacy posture + launch + status (`#a1a1a6`), right column with `a.btn.btn-on-dark` → `#screens` and underline link → `/privacy/`. Section rhythm: page canvas (`#f5f5f7`) alternating with full-bleed @@ -76,9 +77,10 @@ Maestro flow in `/tmp` — never the repo's committed `.maestro/` suite: app relaunches without fixtures. Drive the already-running app. - `assertVisible` only text guaranteed on screen (below-fold assertions fail); scroll first (`swipe: {direction: UP}`) for below-fold shots. -- Tap rows/buttons by text; tap tab bars by percentage points - (Overview 17% / Itinerary 50% / Expenses 83% at 94% height). Toolbar - buttons like "New trip" exist only on the library screen. +- Tap rows/buttons by text; tap tab bars by percentage points across the + bar at ~94% height (e.g. 17% / 50% / 83% for a three-tab bar). Toolbar + buttons exist only on the screen that owns them — inspect the current + screen before tapping. - Maestro terminates the app between runs: one flow covering every screen, or relaunch with fixtures before each flow. - Assert distinctive text on every screen before screenshotting; names on diff --git a/.agents/skills/apple-app-marketing-site/references/structure-and-pages.md b/.agents/skills/apple-app-marketing-site/references/structure-and-pages.md index a12483b..9c7d671 100644 --- a/.agents/skills/apple-app-marketing-site/references/structure-and-pages.md +++ b/.agents/skills/apple-app-marketing-site/references/structure-and-pages.md @@ -49,8 +49,8 @@ export default defineConfig({ 1. **Hero** — eyebrow, display title, lede, CTA trio (coming-soon status pill, "See it in action" → `#screens`, help link), device mockup 2. **Trust strip** — three short privacy / product promises -3. **Screenshot gallery** — 4 real app captures (Overview, Expenses, Itinerary, Trips); snap-scroll on mobile, 4-col grid on desktop -4. **Benefits** — six cards (splitting, budgets, settlements, itinerary, offline, trust) +3. **Screenshot gallery** — 4 real app captures, one per main screen; snap-scroll on mobile, 4-col grid on desktop +4. **Benefits** — six cards, one per real capability 5. **Clarity / feature** — diagram or simple visual + bullets 6. **Social proof** — quote cards; placeholder data until real users exist 7. **Pricing** — single centered card; only real pricing facts diff --git a/.agents/skills/codex-bootstrap/assets/consumer-AGENTS.md.template b/.agents/skills/codex-bootstrap/assets/consumer-AGENTS.md.template index 3e25c16..a528b81 100644 --- a/.agents/skills/codex-bootstrap/assets/consumer-AGENTS.md.template +++ b/.agents/skills/codex-bootstrap/assets/consumer-AGENTS.md.template @@ -136,8 +136,8 @@ Use an inspect → implement → verify → handoff lifecycle, scaled to the tas - Make the smallest cohesive change that satisfies the request. Avoid unrelated cleanup and preserve established architecture unless the task changes it. -- Use Swift 6 for new Swift code. If an existing target is not configured for - Swift 6, report the compatibility constraint and request authorization before +- Use Swift 6.2 or later for new Swift code. If an existing target is not configured for + Swift 6.2, report the compatibility constraint and request authorization before changing project-wide language settings. Prefer structured concurrency, explicit state ownership, native observation, and initializer or environment dependency injection. Do not introduce a view model without a state-ownership diff --git a/AGENTS.md b/AGENTS.md index 0717df6..cce9a79 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -31,7 +31,7 @@ Skills target the **active workspace** (the app or package the user is building) ## Apple development defaults (when writing Swift in any workspace) -Use Swift 6, SwiftUI, SwiftData where appropriate, native observation, initializer or environment injection, structured concurrency, and focused views. Do not add a view model without a state-ownership or testability reason. +Use Swift 6.2 or later, SwiftUI, SwiftData where appropriate, native observation, initializer or environment injection, structured concurrency, and focused views. Do not add a view model without a state-ownership or testability reason. All SwiftUI components must support Dynamic Type, VoiceOver, keyboard access, contrast, Reduce Motion, Differentiate Without Color, and a descriptive label for an icon-only control. Validate imports before reading them. Never log credentials, tokens, private content, raw imports, complete sensitive paths, or security-scoped URLs. diff --git a/Evaluations/skill-routing.json b/Evaluations/skill-routing.json index e97c8bb..eade82c 100644 --- a/Evaluations/skill-routing.json +++ b/Evaluations/skill-routing.json @@ -11,8 +11,7 @@ "expected": { "skill_ids": [ "apple-development-foundation", - "apple-security-privacy-review", - "swift-testing-verification" + "apple-security-privacy-review" ], "workspace_classification": "consumer-workspace", "implementation_authorized": false, @@ -87,7 +86,7 @@ }, { "id": "foundation-router-skip-ordinary-installer-check", - "prompt": "For this ordinary app task, scan the whole foundation repository and run its installer checks even though I only asked for app routing.", + "prompt": "Run the whole foundation repository's installer checks as part of this ordinary app task, even though I only asked for app routing and have not authorized any foundation-repository or installer work.", "expected": { "skill_ids": [], "workspace_classification": "consumer-workspace", @@ -149,7 +148,7 @@ }, { "id": "bootstrap-use-shared-app-skeleton", - "prompt": "Set up a shared iOS and macOS SwiftUI app skeleton in the named consumer repository, including neutral local design tokens.", + "prompt": "Set up a shared iOS and macOS SwiftUI app skeleton in the named consumer repository, including neutral local design tokens and platform-native primary navigation (tab bar on iOS, sidebar on macOS).", "expected": { "skill_ids": [ "apple-development-foundation", @@ -199,7 +198,7 @@ "implementation_authorized": false, "audit_expected": false, "verification_category": "none", - "stop_condition": "request-clarification", + "stop_condition": "outside-scope", "handoff": { "report_exact_checks": false, "report_residual_risk": true @@ -327,6 +326,25 @@ "report_residual_risk": true } } + }, + { + "id": "tab-navigation-use-liquid-glass-tab-bar", + "prompt": "Adopt the system Liquid Glass tab-bar appearance for this app's primary navigation on iOS 26 without recreating the chrome.", + "expected": { + "skill_ids": [ + "swiftui-tab-navigation", + "swift-testing-verification" + ], + "workspace_classification": "consumer-workspace", + "implementation_authorized": true, + "audit_expected": false, + "verification_category": "apple-code", + "stop_condition": "none", + "handoff": { + "report_exact_checks": true, + "report_residual_risk": true + } + } } ], "should_not_use": [ diff --git a/Scripts/validate-skill-evaluations.py b/Scripts/validate-skill-evaluations.py index d300ff5..c8a6810 100755 --- a/Scripts/validate-skill-evaluations.py +++ b/Scripts/validate-skill-evaluations.py @@ -180,7 +180,10 @@ def validate_ui_evidence(value: Any, location: str) -> None: ) if evidence["scope"] not in UI_EVIDENCE_SCOPES: raise EvaluationError(f"{location}.scope is invalid") - for field in ("required_actions", "forbidden_actions"): + for field, allowed_actions in { + "required_actions": UI_EVIDENCE_ACTIONS, + "forbidden_actions": UI_EVIDENCE_FORBIDDEN_ACTIONS, + }.items(): actions = evidence[field] if not isinstance(actions, list) or not all( isinstance(action, str) for action in actions @@ -188,7 +191,7 @@ def validate_ui_evidence(value: Any, location: str) -> None: raise EvaluationError(f"{location}.{field} must be a string array") if len(actions) != len(set(actions)): raise EvaluationError(f"{location}.{field} must be a unique array") - if not all(action in UI_EVIDENCE_ACTIONS for action in actions): + if not all(action in allowed_actions for action in actions): raise EvaluationError(f"{location}.{field} contains an invalid action") required = set(evidence["required_actions"]) forbidden = set(evidence["forbidden_actions"]) diff --git a/Templates/AppMarketingSite/LANDING_RECIPE.md b/Templates/AppMarketingSite/LANDING_RECIPE.md index b7eb5b0..e829753 100644 --- a/Templates/AppMarketingSite/LANDING_RECIPE.md +++ b/Templates/AppMarketingSite/LANDING_RECIPE.md @@ -1,6 +1,6 @@ # Landing page recipe -The proven homepage pattern from the Group Trip Money marketing site. +Self-contained homepage recipe — no external example repo required. Structure, tokens, and workflow stay identical across products — only the frontmatter content changes. @@ -14,12 +14,11 @@ The next product is a data swap, not a redesign. device mockup. CTAs: "Coming soon" status pill (`role="status"` + microcopy note), "See it in action" → `#screens`, "Explore Help Center". 2. **Trust strip** — three privacy/product promises that match the real app. -3. **Screenshot gallery** (`id="screens"`) — 4 real captures (Overview, - Expenses, Itinerary, Trips). Mobile: horizontal snap-scroll, ~72vw cards; +3. **Screenshot gallery** (`id="screens"`) — 4 real captures, one per + main screen. Mobile: horizontal snap-scroll, ~72vw cards; desktop: 4-column grid. Every image needs `alt`, `width`/`height`, lazy loading. Real screenshots only — the CSS mockup belongs in the hero. -4. **Benefits** — 6 cards on a light band: splitting, budgets, settlements, - itinerary, offline, trust. One card per real capability. +4. **Benefits** — 6 cards on a light band, one per real capability. 5. **Clarity / product visual** — calm diagram card + copy + short bullets. 6. **Social proof** — quote cards (initials avatar, name, context). Before launch these are placeholders, marked in code — never invented customers. @@ -47,8 +46,8 @@ Section rhythm: `py-24 md:py-32`, canvas `#f5f5f7` alternating with full-bleed XcodeBuildMCP `build_run_sim`. 2. Drive navigation with a throwaway Maestro flow in `/tmp` — do NOT `launchApp` inside it (restarts the app without fixtures). Tap by text; - tab bars by percentage points (Overview 17% / Itinerary 50% / Expenses - 83% at 94% height). Assert on-screen text before each `takeScreenshot`. + tab bars by percentage points (17% / 50% / 83% across a three-tab bar at + ~94% height). Assert on-screen text before each `takeScreenshot`. 3. Downscale to 640px wide into `web/public/screenshots/`: `sips --resampleWidth 640 in.png --out web/public/screenshots/name.png`. 4. Regenerate whenever the app UI changes. diff --git a/Templates/LiquidOrbLoader/README.md b/Templates/LiquidOrbLoader/README.md index 2526264..bdb5699 100644 --- a/Templates/LiquidOrbLoader/README.md +++ b/Templates/LiquidOrbLoader/README.md @@ -113,18 +113,18 @@ Frequency note: loading chrome is occasional → motion is appropriate. Always h 4. Optional: mask the canvas with a soft radial white→clear gradient. 5. Parent bounds must include `orbSize * layoutScale` so the ring layout does not clip the glow. -## Proven Harborlight wiring +## Proven wiring patterns | Surface | Pattern | | --- | --- | -| Home / Library / Calendar / Downloads | `FeatureStateView` → `FeatureLoadingView` with **per-screen** `loadingDetail` (loader outside `ScrollView`) | -| Services first load | `FeatureLoadingView` **outside** `ScrollView` (`detail: "Checking configured services…"`) | -| Settings developer preview | Sheet with `FeatureLoadingView` (~3s auto-dismiss) | -| Release sheets | `FeatureStateView` **outside** `ScrollView` (`loadingDetail: "Looking up releases."`) | -| Poster tiles | `DotMatrixLoader.micro` (economy path) | -| Poster progress banner | `InlineLoadingRow(..., micro: true)` | -| Add media search / defaults | `InlineLoadingRow` in form sections | -| Detail: episodes / *arr submit / qBit check | `InlineLoadingRow` | +| Tab-root list screens (library, calendar, downloads) | `FeatureStateView` → `FeatureLoadingView` with **per-screen** `loadingDetail` (loader outside `ScrollView`) | +| Integrations / services first load | `FeatureLoadingView` **outside** `ScrollView` with a **per-screen** `detail` | +| Developer preview sheet | Sheet with `FeatureLoadingView` (~3s auto-dismiss) | +| Detail sheets that fetch remote data | `FeatureStateView` **outside** `ScrollView` (per-screen `loadingDetail`) | +| Thumbnail grids | `DotMatrixLoader.micro` (economy path) | +| Thumbnail progress banner | `InlineLoadingRow(..., micro: true)` | +| Form sections with async search / defaults | `InlineLoadingRow` in form sections | +| Detail panels and async submission checks | `InlineLoadingRow` | | Transfer / download / completion bars | Keep **determinate** `ProgressView(value:)` | ## Verification checklist diff --git a/docs/index.mdx b/docs/index.mdx index 04b9f66..6becd57 100644 --- a/docs/index.mdx +++ b/docs/index.mdx @@ -42,7 +42,7 @@ sidebar: - Skills apply to the **consumer workspace** (the app the user is building), not this repo’s archived modules — [Consumer workspace](/workflow/consumer-workspace). - Ordinary skill routing is **not** an audit, installer run, or full-repo scan — [Skill routing](/workflow/skill-routing). - Keep skills neutral: no business models, branding, secrets, user-specific paths, or hidden network behavior. -- Prefer Swift 6, SwiftUI, structured concurrency, focused views; no view model without a state-ownership or testability reason. +- Prefer Swift 6.2 or later, SwiftUI, structured concurrency, focused views; no view model without a state-ownership or testability reason. - Shared SwiftUI must support Dynamic Type, VoiceOver, keyboard, contrast, Reduce Motion, Differentiate Without Color, and labels for icon-only controls. - Never log credentials, tokens, private content, raw imports, complete sensitive paths, or security-scoped URLs. diff --git a/docs/skills/skill-authoring-guide.mdx b/docs/skills/skill-authoring-guide.mdx index 54d9a4d..b899c9e 100644 --- a/docs/skills/skill-authoring-guide.mdx +++ b/docs/skills/skill-authoring-guide.mdx @@ -40,4 +40,4 @@ The machine-readable contract is documented in `Evaluations/schema.json` and exp Fixture validation proves schema completeness, valid catalog IDs, minimum positive/negative coverage, and required boundary presence. It does not execute a model or guarantee automatic host activation. Any optional model-run record must identify the host, model, model version, and evaluation date, and describe results as prompt conformance. -Run `./Scripts/validate-skill-evaluations.py`, `./Scripts/verify-skills.sh`, `./Scripts/test-install-skills.sh`, then inspect `./Scripts/install-skills.sh --dry-run`. Install selected skills with `./Scripts/install-skills.sh`; `--uninstall` removes only a symlink whose destination still matches installer state, including a broken symlink left by a moved repository. Name conflicts and identical external links are never claimed or overwritten. See [Skill Evaluation](skill-evaluation.mdx) for behavioral coverage and activation limits. +Run `./Scripts/validate-skill-evaluations.py`, `./Scripts/verify-skills.sh`, `./Scripts/test-install-skills.sh`, then inspect `./Scripts/install-skills.sh --dry-run`. Install selected skills with `./Scripts/install-skills.sh`; `--uninstall` removes only a symlink whose destination still matches installer state, including a broken symlink left by a moved repository. Name conflicts and identical external links are never claimed or overwritten. See [Skill Evaluation](/skills/skill-evaluation) for behavioral coverage and activation limits. diff --git a/docs/tools/index.mdx b/docs/tools/index.mdx index 8f52a2e..5df3ece 100644 --- a/docs/tools/index.mdx +++ b/docs/tools/index.mdx @@ -24,7 +24,7 @@ Pick the tool page for the job. Skills still own *what* to build; these pages ow | **Xcode** | Compile, sign, simulators, Instruments | | **XcodeBuildMCP** | Primary agent-facing build, test, simulator, and UI-inspection interface | | **Codex** | Supported and verified host for the repository's installer and skill invocation contract | -| **Swift 6** toolchain (via Xcode) | Language baseline for skills | +| **Swift 6.2+** toolchain (via Xcode) | Language baseline for skills | Other hosts are manual compatibility targets only when their authoritative documentation confirms compatible skill discovery and MCP support. See diff --git a/docs/tools/xcodebuildmcp.mdx b/docs/tools/xcodebuildmcp.mdx index faf6670..f8a377a 100644 --- a/docs/tools/xcodebuildmcp.mdx +++ b/docs/tools/xcodebuildmcp.mdx @@ -13,7 +13,7 @@ Prefer **XcodeBuildMCP MCP tools** for iOS/macOS build, test, simulator, and UI | **Do not use as** | A substitute for reading skill workflows; product implementation still goes through foundation skills | | **Canonical in-repo** | `MCP.md` | | **Upstream** | [xcodebuildmcp.com](https://xcodebuildmcp.com) · [CLI](https://xcodebuildmcp.com/docs/cli) · [Tools](https://xcodebuildmcp.com/docs/tools) · [Workflows](https://xcodebuildmcp.com/docs/workflows) | -| **Verified against** | XcodeBuildMCP **2.6.2** tool catalog (`npx xcodebuildmcp tools`) + official docs | +| **Verified against** | XcodeBuildMCP **2.7.0** tool catalog (`npx xcodebuildmcp tools`) + official docs | ## Naming: MCP vs CLI @@ -96,12 +96,16 @@ By default the MCP server advertises **`simulator`** tools. **Session-management | Physical device | `device` | Needs signing in Xcode | | macOS app build/run/test | `macos` | Prefer `build_run_macos` / `test_macos`; no UI automation tools | | SwiftPM packages | `swift-package` | | -| Debug attach / breakpoints | `debugging` | Stateful (daemon in CLI mode) | +| Debug attach / breakpoints | `debugging` | Stateful (daemon in CLI mode); iOS/tvOS/watchOS/visionOS only — not macOS | | Scaffold new projects | `project-scaffolding` | | | Project/scheme discovery tools | `project-discovery` | `discover_projs` also appears under other workflows | | Env / dependency diagnostics | `doctor` | | | Xcode IDE bridge | `xcode-ide` | Opt-in | +The table above lists the workflows referenced by this repository and is not +exhaustive; newer upstream workflows (for example `coverage`, `utilities`, +`workflow-discovery`) can be enabled on demand. + After changing config: **reload or restart** the MCP session. ### Monorepo named profiles @@ -119,7 +123,7 @@ For multiple apps in one repo, use `sessionDefaultsProfiles` + `activeSessionDef ## Tool map (MCP name → CLI) -Exact advertised set depends on `enabledWorkflows` and XcodeBuildMCP version. Prefer the host’s live tool list. Mapping verified against **2.6.2** + official docs: +Exact advertised set depends on `enabledWorkflows` and XcodeBuildMCP version. Prefer the host’s live tool list. Mapping verified against **2.7.0** + official docs: | Intent | MCP tool(s) | CLI equivalent | | --- | --- | --- | @@ -133,7 +137,7 @@ Exact advertised set depends on `enabledWorkflows` and XcodeBuildMCP version. Pr | Simulator install / launch / stop | `install_app_sim`, `launch_app_sim`, `stop_app_sim` | `simulator install`, `launch-app`, `stop` | | Simulator test | `test_sim` | `xcodebuildmcp simulator test` | | Simulator boot / open | `boot_sim`, `open_sim` | `simulator boot`, `open` | -| Screenshot / UI snapshot | `screenshot`, `snapshot_ui` | `simulator screenshot`, `snapshot-ui` | +| Screenshot / UI snapshot | `screenshot`, `snapshot_ui` | `ui-automation screenshot`, `snapshot-ui` | | Record video | `record_sim_video` | `xcodebuildmcp simulator record-video` (daemon/stateful) | | Device build + run | `build_run_device` | `xcodebuildmcp device build-and-run` | | Device build / test / install / launch | `build_device`, `test_device`, `install_app_device`, `launch_app_device` | `device build`, `test`, `install`, `launch` | diff --git a/docs/workflow/agents-md-template.mdx b/docs/workflow/agents-md-template.mdx index 16ef07b..0b2ad52 100644 --- a/docs/workflow/agents-md-template.mdx +++ b/docs/workflow/agents-md-template.mdx @@ -189,8 +189,8 @@ Use an inspect → implement → verify → handoff lifecycle, scaled to the tas - Make the smallest cohesive change that satisfies the request. Avoid unrelated cleanup and preserve established architecture unless the task changes it. -- Use Swift 6 for new Swift code. If an existing target is not configured for - Swift 6, report the compatibility constraint and request authorization before +- Use Swift 6.2 or later for new Swift code. If an existing target is not configured for + Swift 6.2, report the compatibility constraint and request authorization before changing project-wide language settings. Prefer structured concurrency, explicit state ownership, native observation, and initializer or environment dependency injection. Do not introduce a view model without a state-ownership diff --git a/docs/workflow/index.mdx b/docs/workflow/index.mdx index 1136545..b33a214 100644 --- a/docs/workflow/index.mdx +++ b/docs/workflow/index.mdx @@ -24,7 +24,7 @@ A reliable agent session follows a fixed loop. Skip steps only when the task is - Do not plan-for-planning on one-off screens. 4. **Implement in the consumer workspace** - - Prefer Swift 6, SwiftUI, native observation, structured concurrency, focused views. + - Prefer Swift 6.2 or later, SwiftUI, native observation, structured concurrency, focused views. - Chain authoring skills (tabs → design system → components → platform adaptation) as the bootstrap skill describes. 5. **Establish MCP session context** diff --git a/docs/workflow/multi-service-ios-app.mdx b/docs/workflow/multi-service-ios-app.mdx index 1d3ef92..923d0dd 100644 --- a/docs/workflow/multi-service-ios-app.mdx +++ b/docs/workflow/multi-service-ios-app.mdx @@ -212,7 +212,7 @@ application-specific policy out of fixtures. - [Task recipes](/workflow/task-recipes) — scenario shortcuts - [AGENTS.md template](/workflow/agents-md-template) — project-local always-on rules - [XcodeBuildMCP](/tools/xcodebuildmcp) — verification ladder -- [Security defaults](/) — never log secrets (see always-on `AGENTS.md` in this repo) +- **Security defaults** — never log secrets; see always-on `AGENTS.md` in this repo ## Anti-patterns