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-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..b5aedfc --- /dev/null +++ b/.agents/skills/apple-app-marketing-site/references/landing-page-recipe.md @@ -0,0 +1,131 @@ +# Homepage recipe — anatomy, real screenshots, verification + +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) + +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), `.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, 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` + (`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 + 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` (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 +`#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 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 + `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 (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 + `#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..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 @@ -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, 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 +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/.agents/skills/apple-design-system/SKILL.md b/.agents/skills/apple-design-system/SKILL.md index 5bb1dba..6823be6 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 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. +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/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 ffdf9fb..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 @@ -19,8 +19,20 @@ 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 | +| “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`, `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 +skill IDs and discovery): [design-motion-glass-routing.md](design-motion-glass-routing.md). + ## Description / discovery levers (done or next) 1. **Done in this change:** Strengthen the master skill `description` so bootstrap 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..8f18986 --- /dev/null +++ b/.agents/skills/apple-development-foundation/references/design-motion-glass-routing.md @@ -0,0 +1,62 @@ +# 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 | 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). + +## Liquid Glass procedure + +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 + +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 depth discovery (IDs only — not catalog entries) + +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: + +| 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/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/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 b6e6e60..a528b81 100644 --- a/.agents/skills/codex-bootstrap/assets/consumer-AGENTS.md.template +++ b/.agents/skills/codex-bootstrap/assets/consumer-AGENTS.md.template @@ -25,6 +25,78 @@ 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 +journeys. This may be Maestro, XCUITest, Appium, or another configured tool. +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`) + +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 UI E2E **is** in use, document the deterministic app contract before +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: `` +- 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 + `` +- 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 +- 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 +- 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, + keep human-readable accessibility labels, and document any native-control + exception; otherwise `n/a` + ## Apple verification policy Fill these before relying on fallback behavior. Shell access or an installed @@ -64,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/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..40ff312 100644 --- a/.agents/skills/swift-testing-verification/SKILL.md +++ b/.agents/skills/swift-testing-verification/SKILL.md @@ -36,3 +36,18 @@ 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. + +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/.agents/skills/swiftui-component-author/SKILL.md b/.agents/skills/swiftui-component-author/SKILL.md index 607ca5f..9bdda16 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. 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. ## Stop conditions @@ -27,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/SKILL.md b/.agents/skills/swiftui-tab-navigation/SKILL.md index 3531816..7c6ef2e 100644 --- a/.agents/skills/swiftui-tab-navigation/SKILL.md +++ b/.agents/skills/swiftui-tab-navigation/SKILL.md @@ -10,16 +10,16 @@ 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. +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..274da16 --- /dev/null +++ b/.agents/skills/swiftui-tab-navigation/references/liquid-glass-chrome.md @@ -0,0 +1,38 @@ +# Liquid Glass chrome (restraint) + +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. +- 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 + +- 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. +- 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/.gitignore b/.gitignore index ea95eb7..b611b26 100644 --- a/.gitignore +++ b/.gitignore @@ -1,15 +1,21 @@ .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/ dist/ __pycache__/ *.py[cod] + +# Local TinyFish research outputs (not product guidance) +tinyfish-results/ diff --git a/AGENTS.md b/AGENTS.md index 11d291d..cce9a79 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -31,10 +31,13 @@ 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. +- 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 - Skill or manifest changes: `./Scripts/verify-skills.sh` (and `./Scripts/test-install-skills.sh` if installer behavior changes) 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/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 25c54ba..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": [ @@ -1129,6 +1147,35 @@ "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 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", + "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 + } + } } ], "should_not_use": [ @@ -1464,6 +1511,68 @@ "report_residual_risk": true } } + }, + { + "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", + "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", + "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/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/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). 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/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/Scripts/test-consumer-guidance.sh b/Scripts/test-consumer-guidance.sh index 961509f..6579f8c 100755 --- a/Scripts/test-consumer-guidance.sh +++ b/Scripts/test-consumer-guidance.sh @@ -42,6 +42,45 @@ 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" +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 27c55aa..c8a6810 100755 --- a/Scripts/validate-skill-evaluations.py +++ b/Scripts/validate-skill-evaluations.py @@ -54,6 +54,8 @@ "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 = { "ordinary-routing-no-audit": { @@ -79,6 +81,49 @@ "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", + }, +} +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", @@ -102,11 +147,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'}; " @@ -121,8 +172,56 @@ 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, 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 + ): + 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 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"]) + 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: - 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") @@ -152,6 +251,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( @@ -185,9 +286,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, @@ -226,8 +346,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) @@ -241,6 +362,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) @@ -277,6 +409,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/Templates/AppMarketingSite/CHECKLIST.md b/Templates/AppMarketingSite/CHECKLIST.md index 1e0ae11..35832a7 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 and 390px (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..e829753 --- /dev/null +++ b/Templates/AppMarketingSite/LANDING_RECIPE.md @@ -0,0 +1,61 @@ +# Landing page recipe + +Self-contained homepage recipe — no external example repo required. +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, 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, 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. +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 (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. + +## 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 and 390px (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 diff --git a/Templates/LiquidOrbLoader/DotMatrixLoader.swift b/Templates/LiquidOrbLoader/DotMatrixLoader.swift new file mode 100644 index 0000000..5386b8d --- /dev/null +++ b/Templates/LiquidOrbLoader/DotMatrixLoader.swift @@ -0,0 +1,488 @@ +import SwiftUI + +// MARK: - Drop-in template (portable) +// +// Source of truth: Harborlight DotMatrixLoader + FeatureLoadingView + InlineLoadingRow. +// 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. +// + +/// 3×3 dot-matrix loading animation. +/// +/// 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 (primary Harborlight 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) + ] + + let center: Center + var dotSize: CGFloat + var spacing: CGFloat + var period: TimeInterval + var tint: Color + var idleTint: Color + + @Environment(\.accessibilityReduceMotion) private var reduceMotion + + init( + center: Center = .orb, + dotSize: CGFloat = 12, + spacing: CGFloat = 10, + period: TimeInterval = 1.8, + tint: Color = .accentColor, + idleTint: Color = Color.secondary + ) { + self.center = center + self.dotSize = dotSize + self.spacing = spacing + self.period = period + self.tint = tint + self.idleTint = idleTint + } + + /// 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, 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 (fits ~52pt compact posters). + static var micro: DotMatrixLoader { + 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. + private var step: CGFloat { + switch center { + case .orb: max(dotSize + spacing, orbSize * 0.82) + default: dotSize + spacing + } + } + + /// Scales with `dotSize` so `.feature` / `.compact` / `.micro` stay proportional. + private var orbSize: CGFloat { + switch center { + case .orb: isMicroOrb ? max(dotSize * 4.5, 18) : max(dotSize * 5.2, 24) + default: dotSize + } + } + + private var ringDotSize: CGFloat { + switch center { + case .orb: dotSize * 0.82 + default: dotSize + } + } + + 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 * 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: timelineInterval, 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, economy: isMicroOrb) + } + } + + 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 used by `FeatureStateView` and the developer preview. +struct FeatureLoadingView: View { + var title: String = "Loading" + var detail: String = "Just a moment." + var compact: Bool = false + + 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(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 + /// 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 * (economy ? Self.microLayoutScale : 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) + // 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 * (economy ? 1.85 : 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.30 * field.glow + 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))) + + 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( + 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.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.12 + ) + ) + + // Very soft rim + context.stroke( + blob, + 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 * 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, + 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: 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 { + var path = Path() + let steps = economy ? 64 : 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)) + // Bake squash into the path (avoids scaleEffect clipping). + let point = CGPoint( + 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) + } else { + path.addLine(to: point) + } + } + path.closeSubpath() + return path + } +} + +/// Continuous liquid field — every channel is a sum of sines, so nothing holds. +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(detail: "Refreshing your library.") +} + +#Preview("Orb mark") { + 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 new file mode 100644 index 0000000..bdb5699 --- /dev/null +++ b/Templates/LiquidOrbLoader/README.md @@ -0,0 +1,145 @@ +# Liquid orb loading mark (DotMatrixLoader) + +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, per-surface loading copy, micro economy path, loader outside `ScrollView` (incl. Services + release sheets). + +## What you get + +| Piece | Role | +| --- | --- | +| `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 (~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 | + +## When to use + +| 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: 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. Map tokens if you have a design system: + + ```swift + DotMatrixLoader( + center: .orb, + tint: AppColor.accent, // default: Color.accentColor + idleTint: AppColor.secondaryLabel // default: Color.secondary + ) + ``` + +3. Use the right density: + + ```swift + // Full-screen feature loading + FeatureLoadingView(title: "Loading", detail: "Refreshing your library.") + + // 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. Wire full-screen `FeatureState` loading with **surface-specific** copy: + + ```swift + switch state { + case .loading: + 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. + Same rule for **Services first load** and **release sheets**—not only tab roots. + + ```swift + // Preferred structure + FeatureStateView(state: state, retry: load) { content in + ScrollView { /* content only */ } + } + + // Avoid: ScrollView { FeatureStateView { … } } // loader collapses + // Avoid: ScrollView { if isLoading { FeatureLoadingView() } … } + ``` + +6. Accessibility: + - Mark: `accessibilityLabel("Loading")`, not an image. + - 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 → “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`; pauses under Reduce Motion. | +| 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) + +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 wiring patterns + +| Surface | Pattern | +| --- | --- | +| 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 + +- [ ] 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 + +## 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. +- Use this for determinate percent complete—keep `ProgressView(value:)`. diff --git a/archive/Website/README.md b/archive/Website/README.md index ad4f616..2faaa2a 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. From the `archive/` directory: ```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). 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/skills/skill-evaluation.mdx b/docs/skills/skill-evaluation.mdx index 9cf3709..b065361 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 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 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 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/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/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/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 65c3754..0b2ad52 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 | +| 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. @@ -77,6 +78,78 @@ 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 +journeys. This may be Maestro, XCUITest, Appium, or another configured tool. +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`) + +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 UI E2E **is** in use, document the deterministic app contract before +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: `` +- 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 + `` +- 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 +- 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 +- 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, + keep human-readable accessibility labels, and document any native-control + exception; otherwise `n/a` + ## Apple verification policy Fill these before relying on fallback behavior. Shell access or an installed @@ -116,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 da2f730..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** @@ -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", ], }); - diff --git a/docs/workflow/multi-service-ios-app.mdx b/docs/workflow/multi-service-ios-app.mdx new file mode 100644 index 0000000..923d0dd --- /dev/null +++ b/docs/workflow/multi-service-ios-app.mdx @@ -0,0 +1,230 @@ +--- +title: Multi-service iOS app patterns +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 (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 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; teams set phase boundaries. + +## 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, 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:** 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 + +```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** — `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 service 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 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, 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 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: + 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) + +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. +- 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 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. + +## Cross-service joins + +| Do | Don’t | +| --- | --- | +| 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 each status or badge | Require one service’s credentials to show another service’s private state | + +## 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 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. + +## 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 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. + +## 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 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 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) — project-local 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 one service +- Storing API keys next to base URLs in UserDefaults +- 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 application-neutral. Application-specific severity lists and +paste prompts live in the active workspace’s `Documentation/`.* diff --git a/docs/workflow/task-recipes.mdx b/docs/workflow/task-recipes.mdx index b4e2a35..9b04a39 100644 --- a/docs/workflow/task-recipes.mdx +++ b/docs/workflow/task-recipes.mdx @@ -36,17 +36,47 @@ 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/` | Read each skill’s `SKILL.md` for checklists. Bootstrap chain context: [Bootstrap](/skills/bootstrap). +### Liquid orb loading mark (full-surface + inline) + +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 | `Templates/LiquidOrbLoader/` — `README.md` (wiring + anti-box checklist) + drop-in `DotMatrixLoader.swift` | +| 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; 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 | +| 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 | Step | Action |