Getting KiFinder ready for the App Store meant turning on the App Sandbox, which I wrote about last time as mostly paperwork. It was not. After the sandbox went on, a scan would finish and find nothing: no faces detected, no photos scored, an empty grid where my daughters' matches used to be. Everything still looked like it was working, which is the worst kind of broken.
The cause was one hardcoded path. KiFinder's face model runs through CoreML, which compiles the model into a temporary directory before it can run, and I had pointed that directory at /private/tmp/kifinder-coreml, outside the app's sandbox container. Before the sandbox, that was fine. After, the directory was not writable, the CoreML session failed to build, and because the failure was swallowed by a try?, every embedding call quietly returned nil and the scan produced a manifest with zero faces. It had worked right up until the sandbox landed, and the on-device smoke test that would have caught it had been deferred. The fix was small, deriving the scratch directory from the system temp dir so it lands inside the container, but I only trusted it after a throwaway live-engine test over my own kept photos showed embeddings going from zero before to five and six after. The lesson I keep relearning: the check you defer is exactly the one that would have caught the regression.
The rest of the stretch was making the engine calmer and more legible, all through Sparra. Re-scoring the whole album after every keep or skip was spinning the fans, so that moved behind a manual "Find new matches" button instead of running on every decision. And the re-scoring itself, which used to be a scalar cosine loop over every face against every reference, became a single batched matrix multiply per album. The one I am most curious about is Face Space, a 2D scatter of the active person's embeddings projected down to two principal components: the reference and kept faces, the skip-taught negatives, and the review candidates, all plotted so I can see why a photo matches and spot a reference that is dragging the rest off course. It is gated behind an internal flag and not yet validated on device, more a way to see what the matcher sees than a shipped feature. But being able to look at the embedding space instead of just trusting the scores feels like the right direction.