Shadow Brain · small apps track · architecture design

LookFor Architecture

The structural views of the app: what it talks to, what it's made of, where the bits live, and how a cold start and a search actually unfold. Every diagram is rendered from the PlantUML sources in docs/architecture/, which describe the shipped code — the numbers in them are the real constants from the source.

C4 · level 1

Context — the app and the outside world

The widest view, and the shortest list you'll ever see on a C4 diagram: the user, the app, and the only two external systems LookFor ever touches — GitHub Releases, contacted once to download the ML models on first run, and Apple's on-device speech recogniser, which despite being an OS service never sends audio off the phone. There is no backend, no account system, no analytics endpoint. This is the architecture-level proof behind the App Store privacy label "Data Not Collected".

C4 context diagram: the user interacts with the LookFor iOS app; the app downloads models from GitHub Releases on first run and uses Apple's on-device speech recognizer. No other external systems.
System context. Everything else on this page lives inside that one app box.full size ↗ · c4-context.puml

C4 · level 2

Containers — five things inside the app

One level down, the app splits into five containers with hard boundaries between them:

① the React Native / JS app (Expo Router screens plus the finder orchestration — all the decisions); ② VisionCamera, which owns the camera session and hands frames to native code; ③ the lookfor-ml native module (Swift + Core ML — all the inference); ④ the Core ML model files on disk in the app's Documents directory, downloaded on first run; and ⑤ the SQLite database for settings, recent queries, and the model manifest.

The load-bearing boundary is between ① and ③: camera pixels flow only Camera → native module, and only 512-float embeddings and small box arrays cross into JavaScript. Nothing image-sized ever crosses the JS bridge — that single rule is what makes a 4–8 Hz loop possible in a React Native app.

C4 container diagram: the React Native app, VisionCamera, the lookfor-ml Swift/Core ML native module, Core ML model files on disk, and the SQLite database, with the relationships between them.
Container view. Pixels stay on the native side of the line; embeddings and boxes cross it.full size ↗ · c4-container.puml

C4 · level 3

Components — inside the finder

The RN/JS container opened up into its real modules. The two loops from the pipeline page appear here as code: the guide loop (rotatingCells planning crops, guideScorer scoring them, GuideAggregator smoothing into warmth) and the confirm loop (YoloeDetector + re-rank). Around them sit the state machine that owns the phase, the single FeedbackController that owns everything sensory, the overlay views, voice input, and the model downloader. Arrows show who calls whom — note that overlays and feedback only consume state-machine output; nothing reaches back into the scoring path.

C4 component diagram of the finder subsystem: guide loop components (rotatingCells, guideScorer, GuideAggregator), confirm loop (YoloeDetector), state machine, feedback controller, overlays, voice input, and model downloader with their call relationships.
Component view of the finder subsystem — the modules under src/finder, src/ml, src/feedback, src/overlay.full size ↗ · c4-component.puml

data shapes

Entities — everything persisted or passed around

The app's whole data model fits on one diagram. Persisted in SQLite: Settings and StoredQuery (the last-10 recents). On disk beside the models: ManifestEntry, which drives the download/verify loop. The rest are the finder's runtime types — FinderParams, GuideParams, GuideOutput, Box, Transition, Phase, Direction, Cue — shown with their real default values. One subtlety the diagram calls out explicitly: the confirm loop's clipScore is the calibrated softmax probability (pTarget), not the raw CLIP cosine (clipCos) — mixing those up is the classic way to misread the thresholds.

Entity diagram: Settings and StoredQuery persisted in SQLite, ManifestEntry for the model manifest, and the finder runtime types FinderParams, GuideParams, GuideOutput, Box, Transition, Phase, Direction, and Cue with default values.
Persisted and runtime data shapes, defaults included.full size ↗ · entities.puml

deployment

System — where the bits live and move

The physical view: on the iPhone, the app process, the Neural Engine it dispatches to, the Documents directory holding the downloaded .mlpackages, and the SQLite file. Off the phone: the GitHub repo and its Releases (the model CDN), the EAS build-and-submit pipeline, and App Store distribution. The diagram also shows the dev-only loop — a Mac running http.server over models/release-assets/ so a development build can pull models over local Wi-Fi without waiting on a GitHub release.

Deployment diagram: iPhone with app process, Neural Engine, Documents directory and SQLite; GitHub repo and Releases; EAS build and submit pipeline; App Store distribution; and the dev-only Mac HTTP server flow.
Deployment view, including the dev-only Mac model server.full size ↗ · system.puml

sequence · cold start

Cold start — from tap to live camera

What happens between tapping the icon and seeing the viewfinder. The firstRunDone gate decides whether onboarding runs (three honest-copy cards, then the camera permission). ensureModels walks the manifest entry by entry — download if missing, SHA-256 verify, re-download on mismatch — and then the three native models (image tower, text tower, detector) load in parallel before the camera goes live. On later launches the whole left half of the diagram collapses to a manifest check.

Sequence diagram of cold start: firstRunDone gate, onboarding cards and camera permission, ensureModels download-and-verify loop per manifest entry, then parallel loading of the image tower, text tower, and detector before the camera starts.
Cold-start sequence: onboarding gate → download/verify loop → parallel model load → camera live.full size ↗ · seq-init.puml

sequence · a search

A full search — the pipeline as a timeline

The same story the pipeline page tells stage by stage, drawn as actual call order: query entry (typed or voice) → text encoding → the per-frame guide loop (rotating-cell scoring → warmth EMA → hysteresis) → once warm, the confirm loop (native YOLOE detect → CLIP re-rank → the 3-of-5 vote) → found and lost feedback → the user resolving with Keep looking or Done. This is the diagram to read next to src/finder/finderStep.ts — the lifelines map one-to-one onto the modules in the component view above.

Sequence diagram of a full search: query entry, text encoding, the repeating guide loop with rotating-cell scoring and warmth EMA, the confirm loop with YOLOE detection, CLIP re-rank and 3-of-5 vote, then found and lost feedback and user resolution.
The search sequence — guide loop repeating until warm, confirm loop until the vote passes.full size ↗ · seq-finding.puml

ground truth

The constants these diagrams bake in

Every threshold shown above is a real constant in the source, not an illustration. The convention in the repo: if one of these changes, the diagrams are updated in the same PR.

ConstantValueMeaningDefined in
warmOn0.45warmth needed to enter warm (wakes the confirm loop)stateMachine.ts
warmOff0.35warmth below which warm falls back to searching (hysteresis gap)stateMachine.ts
foundScore0.55calibrated re-rank probability a box must beatstateMachine.ts
foundK / foundN3 / 5frames the vote requires before declaring foundstateMachine.ts
lostAfterMs2000box absence that flips found → loststateMachine.ts
temperature0.05softmax τ over [target, negatives] cosinesreplay.ts
cosineFloor0.2absolute cosine below which warmth is suppressed (empty-scene guard)replay.ts
emaAlpha0.3EMA weight for the warmth signalreplay.ts
directionFloor0.15minimum cell score before a direction cue is offeredreplay.ts

From DEFAULT_FINDER_PARAMS in src/finder/stateMachine.ts and DEFAULT_GUIDE_PARAMS in src/finder/replay.ts. The interactive demos on the pipeline page run these same values.