Skip to content

FCL-M1: stabilize camera core - #2

Merged
PNHD merged 5 commits into
feat/focelle-betafrom
task/FCL-M1-camera-core-stabilization
Aug 1, 2026
Merged

FCL-M1: stabilize camera core#2
PNHD merged 5 commits into
feat/focelle-betafrom
task/FCL-M1-camera-core-stabilization

Conversation

@PNHD

@PNHD PNHD commented Jul 30, 2026

Copy link
Copy Markdown
Owner

CI-only evidence for immutable commit b381c53. No merge requested.

PNHD and others added 5 commits July 30, 2026 23:10
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>
@PNHD
PNHD merged commit f2b20ed into feat/focelle-beta Aug 1, 2026
3 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant