Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
64 changes: 56 additions & 8 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,12 @@
| SwiftFormat | 0.60.1 | `.mise.toml` |
| Swift PM | 6.2 | `Package.swift` (`swift-tools-version`) |

**Libraries** (**StuffCore**, **LifecycleKit**, **JournalKit**, **LogKit**, **LogViewerUI**, **PeriscopeCore**, **PeriscopeUI**, **PeriscopeTools**, **TestHostSupport**, **RegionKit**, **WhereCore**, **WhereUI**) are defined in the root [`Package.swift`](Package.swift) — local package for libraries, Tuist for apps and test bundles.

Tuist manifests live at the repo root ([`Project.swift`](Project.swift), [`Tuist.swift`](Tuist.swift)). `Project.swift` references `Package.local(path: .relativeToRoot("."))` and declares the **Where** app, **StuffTestHost**, and unit-test targets that depend on package products.
Library targets live in the root [`Package.swift`](Package.swift) (one local
package); apps, app extensions, and test bundles are Tuist targets in
[`Project.swift`](Project.swift) (plus [`Tuist.swift`](Tuist.swift)), which
references the package via `Package.local(path: .relativeToRoot("."))`. The
two manifests are the authoritative target catalog — it is deliberately not
duplicated here.

Run `./ide` (or `./ide -i` to also install dependencies) to regenerate the
Xcode project, install external agent skills, and point Git at `.githooks/`.
Expand Down Expand Up @@ -53,8 +56,10 @@ by `./sync-agents`.

## Targets

- **Package products** ([`Package.swift`](Package.swift)) — **StuffCore** ([`Shared/StuffCore/Sources/`](Shared/StuffCore/Sources/)), **LifecycleKit** ([`Shared/LifecycleKit/Sources/`](Shared/LifecycleKit/Sources/)), **JournalKit** ([`Shared/JournalKit/Sources/`](Shared/JournalKit/Sources/), the generic append-only crash-durable journal), **LogKit** ([`Shared/LogKit/Sources/`](Shared/LogKit/Sources/), the logging facade), **LogViewerUI** ([`Shared/LogViewerUI/Sources/`](Shared/LogViewerUI/Sources/), the generic SwiftUI log viewer), **PeriscopeCore** / **PeriscopeUI** / **PeriscopeTools** ([`Shared/Periscope/`](Shared/Periscope/), the typed hierarchical observability framework — core + SwiftData store, SwiftUI environment integration, and on-device exploration tooling), **SwiftDataInspector** ([`Shared/SwiftDataInspector/Sources/`](Shared/SwiftDataInspector/Sources/), the generic SwiftData browser), and **TestHostSupport** ([`Shared/TestHostSupport/Sources/`](Shared/TestHostSupport/Sources/), the shared UIKit hosting + run-loop helpers for hosted test bundles) under [`Shared/`](Shared/); the **Broadway** design-system libraries **BroadwayCore** / **BroadwayUI** under [`Shared/Broadway/`](Shared/Broadway/) (see [`Shared/Broadway/AGENTS.md`](Shared/Broadway/AGENTS.md)); **RegionKit** ([`Where/RegionKit/Sources/`](Where/RegionKit/Sources/), the geometry + GeoJSON + region-lookup engine WhereCore builds on) / **WhereCore** / **WhereUI** under [`Where/`](Where/).
- **Tuist targets** ([`Project.swift`](Project.swift)) — **Where** app ([`Where/Where/`](Where/Where/)) and its embedded app extensions **WhereWidgets** ([`Where/WhereWidgets/`](Where/WhereWidgets/), home/lock-screen widgets) and **WhereShareExtension** ([`Where/WhereShareExtension/`](Where/WhereShareExtension/), a Share-sheet action that saves shared content as evidence into the App Group store), **RegionViewer** ([`Where/RegionViewer/`](Where/RegionViewer/), a thin standalone **Mac Catalyst** host for the WhereUI region-map developer tool — the only target with a `.macCatalyst` destination), **BroadwayCatalog** ([`Shared/Broadway/BroadwayCatalog/`](Shared/Broadway/BroadwayCatalog/), the Broadway component showcase app), **StuffTestHost** ([`Shared/StuffTestHost/`](Shared/StuffTestHost/)), **WhereTests** (app tests, no host), and hosted **\*Tests** bundles (**StuffCoreTests**, **LifecycleKitTests**, **JournalKitTests**, **LogKitTests**, **LogViewerUITests**, **PeriscopeCoreTests**, **PeriscopeUITests**, **PeriscopeToolsTests**, **SwiftDataInspectorTests**, **RegionKitTests**, **WhereCoreTests**, **WhereUITests**, **BroadwayCoreTests**, **BroadwayUITests**, **BroadwayCatalogTests**) that depend on **StuffTestHost** + **TestHostSupport** + the relevant package product (most via the shared `unitTests` helper).
- For the current list of library products, apps, extensions, and test
bundles, read [`Package.swift`](Package.swift) and
[`Project.swift`](Project.swift); each module's own `README.md` /
`AGENTS.md` says what it is and how it may be used.
- Add SPM library targets in `Package.swift` and wire apps/tests in `Project.swift` (see existing `unitTests` helper). A new module also ships a root `README.md` and `AGENTS.md` — see [Per-module docs](#per-module-docs).
- **CI scheme**: CI runs the explicit shared **Stuff-iOS-Tests** scheme (all test bundles) rather than the autogenerated `Stuff-Workspace` scheme. New test bundles must be added to the `Stuff-iOS-Tests` scheme in `Project.swift` or CI won't run them.
- **Never double-link a package product into a test bundle that already gets it through a dynamic-framework dependency.** Xcode's default SPM integration (how `Package.local` is wired here) embeds a product's code into *every* image that links it. **WhereUI** is a dynamic framework that statically embeds its own dependencies (WhereCore, BroadwayCore/BroadwayUI, LifecycleKit, LogViewerUI, SwiftDataInspector, …), so a `*Tests` bundle that depends on **WhereUI** *and* re-lists one of those in `extraPackageProducts` ends up with a **second copy** of it. When the shared **StuffTestHost** loads several `.xctest` bundles into one process, that leaves duplicate **type metadata** for the module, and any *type-keyed runtime lookup that crosses the WhereUI boundary* — SwiftUI `EnvironmentKey`s, `UITraitBridgedEnvironmentKey` bridging, the type-keyed `BTraits`/`BThemes`/`BStylesheets` containers — silently resolves against the wrong copy and returns the default (the writer stores under one copy's key *type*, the reader looks it up under another's). It only reproduces in the **full multi-bundle scheme** (not isolated `tuist test WhereUITests` runs) and is papered over by newer Xcode linkers, so it is brutal to diagnose (it cost ~2 hours once). **Depend on such products only transitively via `WhereUI`; keep them out of `extraPackageProducts`.** `WhereStylesheetTests.resolvesTraitAwareTokensFromTheBroadwayRoot` is the regression guard — it exercises a `\.stylesheet` (→ `\.bContext`) read across the boundary and fails if a duplicate copy returns.
Expand All @@ -67,7 +72,9 @@ by `./sync-agents`.

## Directory layout

Shared code and the shared iOS test host live under **`Shared/`**. Feature apps and their modules (e.g. **Where**) live under a top-level folder per feature (e.g. **`Where/`**). The imported **Broadway** design-system modules live together under **`Shared/Broadway/`** (each module keeps its own `Sources/`, `Tests/`, `README.md`, and `AGENTS.md`).
Every module — shared ones under `Shared/`, feature ones under a top-level
folder per feature (e.g. `Where/`) — follows this skeleton, and a new module
must too:

```
Shared/<TargetName>/
Expand Down Expand Up @@ -123,6 +130,10 @@ the generated (gitignored) `CLAUDE.md` is produced next to it.
behind `@_spi(Testing)` (with `#if DEBUG` when release must not ship them);
tests inject small values (e.g. a retry-queue size of 20) instead of
hardcoding production limits.
- **Test doubles conform to the production protocol.** Model a seam as a
protocol the real and fake both conform to (`LocationSource` /
`ScriptedLocationSource`) — never an enum switch inside a production type
that branches to fake behavior.
- State machines with many branches (launch runners, lifecycle drives) benefit
from **seeded fuzz/adversarial tests** that replay failures exactly.
- **Non-obvious types get a brief doc comment** on the type — detectors,
Expand All @@ -134,6 +145,11 @@ the generated (gitignored) `CLAUDE.md` is produced next to it.
one field or that escapes a single function — tuples are fine as
ad-hoc inline returns but should not appear in property types,
collection element types, or public API.
- **Group large flat types into sub-structs and child types.** When a type
grows a long flat property list (e.g. a config with a cluster of watchdog
knobs) or a file accretes several behavioral areas, group related properties
into nested structs and split responsibilities into focused child types —
don't let one god-type keep growing.
- Identifiers/keys are `Hashable` (ideally a typed enum) or `AnyHashable`, not
raw `String`s — a typed token can't silently typo into a new, untracked id,
and any `Hashable` converts to `AnyHashable` implicitly at the call site.
Expand Down Expand Up @@ -193,6 +209,19 @@ the generated (gitignored) `CLAUDE.md` is produced next to it.
- **`didSet` must skip work when the value is unchanged.** When the stored
type is `Equatable`, guard `oldValue != newValue` before invalidation,
logging, or other side effects — reassigning the same value should be a no-op.
- **Prefer synthesized `Codable`; keep persisted formats rename-safe.**
Anything persisted (journals, backups, stored preferences) must survive
Swift-side renames — synthesized coding of an enum with associated values
freezes the *case names* into the wire format, so renaming a case silently
breaks old data. A hand-written conformance needs a load-bearing reason,
documented on the conformance (see `LogJournalEntry`); simple structs of
primitives should just use the synthesized one (see `CalendarDay`).
- **Retain notification-observer tokens; every `start` has a `stop`.** A
block-based `addObserver(forName:)` observation stays alive in the center
whether or not you keep the token — dropping it makes the observation
unremovable and immortalizes everything the block captured. Store the token
(see `AmbientObserverTokens`) and give any `start…`-style observation API a
paired `stop()` that removes it.
- **Testing-only APIs use `@_spi(Testing)`.** Hooks meant exclusively for unit
tests or previews (direct store mutation, failure injection, queue
introspection, etc.) are marked `@_spi(Testing)`; wrap in `#if DEBUG` when
Expand Down Expand Up @@ -268,8 +297,20 @@ mark `in_progress`, implement, run local checks, commit, mark `completed`.
- Pure-groundwork steps (no behavior change) still get their own commit; say so
in the body.
- Name the plan step each commit closes (the to-do title is fine).
- Don't push until the user asks — unless the plan says otherwise, or the
request clearly implies it (e.g. "open a PR", "ship it").
- Pushing follows the [GitHub](#github) rules: a branch with an open PR gets
every commit pushed as it lands; otherwise don't push until the user asks —
unless the plan says otherwise, or the request clearly implies it
(e.g. "open a PR", "ship it").

## GitHub

- Use the `gh` CLI for **all** GitHub interaction — PRs, issues, checks,
releases, review comments — not raw API calls or the web UI.
- Open PRs in **ready-for-review** mode, not draft.
- **Keep open PRs current.** When the current branch has an open PR, push each
local commit as it lands so the PR never goes stale. A branch without a PR
still waits for the user before pushing (see [Working on
plans](#working-on-plans)).

## Working on PR feedback

Expand All @@ -280,6 +321,13 @@ editing and pushing, not on understanding. Once the user points you at feedback
(names comments, says "address these", etc.), that's your go-ahead — do the
work end to end without re-asking per comment.

- **When a commit resolves an issue a comment called out, reply to that
comment** saying so — name the commit and what changed — using the AI-agent
prefix from [Posting on the user's behalf](#posting-on-the-users-behalf).
- **Deferred feedback gets filed, never dropped.** If a comment is deliberately
not addressed, record it somewhere durable (the module's `TODOs.md` or the
review-tracking file) and reply linking where it's tracked.

## Debugging build/test/CI failures

**Check `git status` and recent history first — before analyzing the error.** A
Expand Down
11 changes: 0 additions & 11 deletions Shared/Periscope/PeriscopeCore/AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,17 +8,6 @@ pipeline, ambient event sources, and the SwiftData store. See
This file complements the root [`AGENTS.md`](../../../AGENTS.md), which owns
the build system, formatting, and global conventions. Read that first.

## Layout

`Sources/` groups by concern: `Events/` (the event vocabulary — levels,
tags, attachments), `Loggers/` (`Log<Event>` and the scope tree),
`Context/` (task-local and per-instance context derivation), `Spans/`
(timing and exits), `Pipeline/` (the `Periscope` system, records, sinks),
`Store/` (SwiftData persistence and queries), `Journal/` (the crash
journal's log-layer envelope and writer, over
[`JournalKit`](../../JournalKit)), and `Ambient/` (environmental event
sources). Tests stay flat, named 1:1 with their source files.

## Scope & dependencies

- **Foundation + os + SwiftData + Network + JournalKit only** (plus the
Expand Down
Loading
Loading