Build Focelle iOS beta - #1
Draft
PNHD wants to merge 93 commits into
Draft
Conversation
Sideloadly re-signs the unsigned CI IPA with a free personal team, which rewrites the bundle id to com.pnhd.focelle.<TeamID> and drops the iCloud entitlement. CKContainer(identifier:) then calls os_crash instead of throwing, so PresetStore.sync()'s do/catch could not stop it and the app died at launch with EXC_BREAKPOINT on the cooperative pool. Device crash report 6EC151FD-D545-4975-BC30-24374A444967 confirms the frames: CloudKit -> PresetSync.database() -> PresetSync.fetch() -> PresetStore.sync(). FocelleCloudKitContainer now comes from FOCELLE_CLOUDKIT_CONTAINER, which only the signed TestFlight archive fills. Unsigned builds leave it empty, take the existing iCloudNotConfigured path, and keep local presets. iCloud support itself is unchanged for properly provisioned builds. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Print embedded frameworks, load commands, rpaths, entitlements and the launch-critical Info.plist keys for each unsigned device build. Secret values are reported as lengths only because this repository is public. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Run 54 was cancelled at the screenshot step after 10.6 minutes. Run 52 needed 6.8, so the old cap left no headroom for runner variance. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Run 55 measurements: boot 119s, simulator build+test 393s, device IPA 132s, screenshot 34s. Two of those were pure waste. The boot no longer blocks. simctl boot returns as soon as the boot starts, so the Debug build now runs during it and bootstatus only has to confirm what already finished. That reclaims most of the 119s. The simulator test and the device package share no build products, so they are now separate jobs on separate runners instead of running back to back. Wall clock becomes the longer of the two rather than their sum, and total billed minutes drop because no work was added. Both jobs share one -clonedSourcePackagesDirPath so GoogleMobileAds is resolved into a cacheable directory instead of being downloaded twice into two DerivedData trees. Artifacts are now focelle-ios-tests and focelle-ios-ipa; upload-artifact rejects the same name from parallel jobs. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Run 56 measured build-for-testing at 448s plus test-without-building at 69s, against 393s for the single combined invocation in run 55. The split made xcodebuild plan the build twice for no benefit. Reverting to one pass keeps the two changes that did work: the device package now runs as its own parallel job off the critical path (116s), and the simulator boot no longer blocks (119s down to 19s plus a 14s confirm). The SwiftPM cache missed on run 56 because that run created it. Run 57 is the first that can hit it. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Pressing the shutter or a volume button killed the app on device. Three crash reports (CB556D85, 40C2ED7D, and the 20:19:22 one) all show the same frames: capturePhotoWithSettings:delegate: raising an ObjC exception from CameraSession.capture() on com.pnhd.focelle.camera, then abort. capture() set photoQualityPrioritization to .quality, but nothing ever raised photoOutput.maxPhotoQualityPrioritization above its .balanced default. AVCapturePhotoOutput rejects a settings value above that ceiling with NSInvalidArgumentException, which Swift cannot catch. configureCapabilities now raises the ceiling while the session is being configured, and capture() reads the ceiling back instead of assuming one. maxPhotoDimensions is bounded against the output for the same reason: it is the other documented throw in that call, and a stale value after a format change would crash the same way. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Schema 1 required instructionVi, instructionEn, poseVi and poseEn on every plan, so each added language meant three more strings per plan and nine more per response. Chinese, Japanese and Korean would have taken it to thirty strings a tap. Schema 2 keeps one instruction and one pose, written in the language the client asked for. The request now carries a BCP-47 tag instead of a vi/en switch, so a new language is a translation job rather than a schema change. The tag reaches the provider prompt, so the backend accepts only letters plus one hyphen. Two tests cover that boundary and one covers the client tag it is given. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Following the guidance on a real device did not work: walking closer made it switch to "move left", and standing near a threshold made it alternate between "move closer" and "step back". Three causes, all in the proposal step. The target teleported. targetX was 0.5 above width 0.42 and a third below it, so walking in moved the target a sixth of the frame in one step and produced a fresh sideways instruction. It also flipped between the left and right third the moment the subject crossed centre. The target now eases toward centre across widths 0.30 to 0.50, and a chosen third is kept until the subject is well past the middle. Every threshold was single-valued, so a subject resting on one oscillated. Each goal now has a wider entry threshold and a narrower exit threshold. The update step switched after two differing frames, roughly 1.4 seconds at the current detection rate. A goal is now held until its exit threshold clears it, the subject is lost, or four seconds pass. A goal that has become wrong still yields at once, since a stale instruction is worse than a new one. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Foundation may report a likely script for a locale that never spelled one out, so en_US could produce "en-Latn" and ja_JP "ja-Jpan". Only Chinese needs its script to pick the right characters, so only Chinese carries one now and everything else sends a bare language code. The test no longer asserts an exact string for every locale either. It checks each tag against the same pattern backend/src/analyze.ts enforces, which is the property that actually matters, and pins exact values only where the script is genuinely part of the answer. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The strip showed names only, so choosing a look meant guessing. Doka solves this with a bundled sample photo per filter, which is a large part of why that app is 163 MB. Each look is now rendered onto the current camera frame instead, at 96 points, refreshed every two seconds on the capture queue. The strip previews the scene actually being shot rather than someone else's photo, and the app still ships no image assets for it. User presets get the same treatment, which also makes them visible: they were already appended after the twelve originals but looked identical to them as plain text. The strip also gains horizontal padding. Chips previously sat flush against the screen edge, so the last one read as clipped rather than scrollable. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The spec asks for charcoal chrome, ivory text and a warm amber accent, and rules out a generic purple-blue AI gradient. The guidance overlay drew its target ring and target frame with an angular cyan to orange to pink sweep, lit them with a cyan shadow, and used cyan for the aim dot and the ring hint. Every one of those is now the amber accent. The three sliders rendered in the iOS system blue for the same reason: nothing tinted them. The zoom row showed a static "1x" beside the live value, which read as the same number printed twice whenever zoom sat at minimum. The left end of the track already means 1x, so the static label is gone and the live value now carries the accent once zoom leaves minimum. The instruction bubble sat at a fixed 16 percent from the top and landed on the subject whenever the subject reached into that band. It now drops below the subject in that case. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Missed in the previous pass; the target frame still cast a cyan glow. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Give the analyzer an explicit resume path: any real session transition from stopped to running (Settings dismissal, interruption recovery, or a fresh launch) now clears analysisInFlight, bumps the analysis generation, resets the tracker/stabilizer/guidance engine, and republishes nil measurement/guidance so the next frame produces a clean detection. switchCamera() now shares the same reset path instead of duplicating it. The stale-generation guard in the video delegate is extracted into CameraSession.shouldAcceptAnalysis so it's independently testable. Added DEBUG-only os.Logger diagnostics at the lifecycle/analysis checkpoints needed for a physical retest, and regression tests covering: guidance preference defaults/persists independent of other settings, stale vs current generation acceptance, and a resume clearing in-flight analysis, bumping generation, and clearing published guidance. Also records FCL-002 as physically completed and FCL-003 as implemented and pending physical verification in docs/project-status.md. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Address independent review findings against 76b11e1: - Wire the Settings sheet's real onDismiss into a new CameraSession.refreshLocalGuidanceAfterSettings(), instead of only logging. It works on the still-running session (no stop/restart, no scenePhase assumption), invalidates in-flight/stale analysis, resets the tracker/stabilizer/guidance engine, and clears stale measurement/guidance so the next frame republishes them. Camera settings (zoom, exposure, ratio, filter, flash, timer) and guidanceEnabled are untouched. - Fix stale-completion ordering: acceptAnalysisCompletion() now validates the request generation before clearing analysisInFlight, so a stale completion (from before a reset) can no longer clear the flag a newer, still-in-flight request owns. - Reconcile docs/project-status.md's Next Gate section, which still called FCL-002 inactive after FCL-002 had already been recorded complete; FCL-003 is now the one documented active gate, still not physically verified. - Tests: exercise refreshLocalGuidanceAfterSettings() itself (the same method Settings' onDismiss calls) for state invalidation and idempotency across repeated calls, and a new stale-vs-current completion ordering test proving a stale result can't clear a newer request's in-flight flag. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
FCL-004. Physical evidence on iPhone 17e showed the toolbar claiming "24" while the saved photo was ~12 MP, and selecting "48 MP" changed nothing observable — the UI was showing a hardcoded label disconnected from what AVFoundation could actually deliver, and CameraSession.resolution could be set directly from the toolbar without updating the persisted AppSettings.maximumResolution, so the two could drift apart independently. Single source of truth: AppSettings.requestedResolution (a CameraResolution, replacing the old maximumResolution Bool) is now the only persisted selection. CameraSession.resolution is private(set); the toolbar and Settings both write through settings.requestedResolution, and CameraView mirrors it into the camera via the one entry point, setRequestedResolution(_:). Truthful three-way split: ResolvedResolution captures what was requested, what the active device/format actually resolves to (PhotoDimensions), and whether/why that's a downgrade (unsupportedByActiveFormat vs outputLimited) — computed by the pure, testable resolve() rule whenever capabilities or the requested mode change. The toolbar label, the two menu choices, and a non-modal inline "lower than requested" indicator are all driven by this instead of a fixed "24"/"48". CaptureResolutionRecord adds a privacy-safe (dimensions and enum labels only) record of requested/resolved/ saved dimensions after every capture, decoded from the saved bytes via pixelDimensions(of:) without touching pixel content. Confirmed the live filter/aspect-ratio pipeline does not silently reduce output size beyond a deliberate ratio crop (FilterRenderer.renderedData only crops and re-encodes; it never scales), so it's not a downgrade source and was left untouched. No zoom/exposure change: setRequestedResolution and refreshLocalGuidanceAfterSettings never touch either. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Addresses four independent-review findings against 2b3af0c, without amending it or the two cherry-picked FCL-003 commits. 1. Compile blocker: FocelleTests/SmokeTests.swift still set the deleted AppSettings.maximumResolution; switched to requestedResolution. 2. Legacy migration: AppSettings now reads an existing requestedResolution key first, otherwise maps a legacy maximumResolution Bool (true -> .maximum, false -> .standard) via the pure, testable migratedResolution(newRawValue:legacyValue:), persists the mapped value immediately, and drops the legacy key once it's been carried across. A new-format value already present is never overwritten, and repeated instantiation is idempotent. 3. Requested tier survives capability changes: configureCapabilities() no longer forces `resolution` to .standard when the active camera lacks a distinct maximum tier — that line was mutating the requested tier from inside CameraSession itself, bypassing setRequestedResolution(_:) and contradicting AppSettings.requestedResolution as sole intent. Only resolvedResolution's dimensions/downgradeReason reflect the camera's current limits; switching back to a capable camera resolves to maximum again with no further Settings/toolbar action. 4. One immutable capture snapshot: capture() now takes a single CaptureResolutionSnapshot (CameraSession.currentCaptureSnapshot(for:)) on `queue` at the moment of capture, computed from queue-owned standardDimensions/maximumDimensions/outputLimitDimensions (now PhotoDimensions, replacing the old CMVideoDimensions/PhotoDimensions dual state) plus a capabilityGeneration bumped on every configureCapabilities() call. That one snapshot configures AVCapturePhotoSettings.maxPhotoDimensions and later becomes CaptureResolutionRecord — they can no longer be independently re-derived and disagree, and a stale generation can't leak into a later capture. Also corrects docs/project-status.md, which still described FCL-004 as unreviewed and its "changeable only through setRequestedResolution(_:)" claim as unconditionally true — neither was accurate until this batch. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Implements the approved Focelle iOS beta scope in
docs/focelle-spec.mdanddocs/focelle-plan.md.Included
VNTrackObjectRequestupdates between detections, thermal throttling, group bounds, persistent long-press selection, and stale-result protection when switching camerasVerified
d7e8dc4: backend, strict Swift formatting, iPhone 17e simulator boot, iOS build, 29/29 tests, app launch, onboarding screenshot, xcresult, logs, and artifact upload all passedRelease gates
iCloud.com.pnhd.focellePNHD/focelle-iosback to private after releaseSee
docs/completion-audit.md,docs/ui/doka-flow-analysis.md, anddocs/testflight-checklist.mdfor requirement-by-requirement evidence.