Spotlight: index body correctly (textContent + keywords) and lock exposure to the levels iOS actually delivers#141
Merged
Merged
Conversation
Body text was only in contentDescription (the result SUBTITLE field). The type label (in title/displayName) matched, but interior body words did not — owner-reported: "reminder" found Takes, "Considus" (mid-note) did not, at full-text exposure. Set attributes.textContent — Spotlight's dedicated full-text field — from the SAME exposure-gated source as contentDescription. This adds NO new exposure: nil at None/Type-only, first line at first-line, full body at full-text — identical to today, just placed where search reads it word-by-word. Both fields share one `body` value so they can't diverge. Tests: the Type-only privacy guard (textContent nil) already existed and still holds; added positive coverage that textContent carries the body at first-line and full-text and equals contentDescription. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…dex diagnostics A sideloaded dev build has no App Store receipt, so refreshEntitlements() derives .lapsed — which wipes the Spotlight index and blocks re-indexing, making on-device Spotlight validation impossible (this is why the textContent fix couldn't be tested). This DEBUG-only Settings button pins status to .subscribed via the existing test hook and rebuilds the index at the current exposure. Compiled out of Release/TestFlight. Also adds a content-free reindex breadcrumb (.lifecycle, export-only): count + exposure level + status, never any Take text or id. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Force-subscribed alone did not surface body results, so the swallowed indexSearchableItems error is now surfaced: the debug button shows, in an alert, CSSearchableIndex.isIndexingAvailable(), how many items were submitted, the exposure level, and the exact OS error (content-free — counts + error text only). This measures the actual failure instead of guessing. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
After indexing measured healthy (available=true, 11/11 items, no error) but body words still don't surface in global Spotlight, add a DEBUG probe: type a word, run a live CSSearchQuery against title / contentDescription / textContent separately, and report the per-field match COUNTS. Isolates 'the index can't match this word' from 'global Spotlight won't surface body matches'. Counts only; no item content leaves the device. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…faces
Decisive on-device measurement (2026-07-24): an in-app CSSearchQuery matched a
body word in contentDescription AND textContent (desc=1, text=1), yet the
global home-screen Spotlight surfaced nothing — while title matches ("reminder")
did surface. iOS's global search ranks/surfaces title + keywords, NOT
contentDescription/textContent, for third-party items. So neither the
contentDescription nor the textContent change could ever fix GLOBAL search.
Fix: also populate keywords (deduped, lowercased, >=2-char body word tokens)
from the SAME exposure-gated source — nil at None/Type-only, first-line tokens
at first-line, full-body tokens at full-text. No new exposure; capped at 100
tokens so a long Take can't produce a runaway array.
Tests: the Type-only privacy guard already asserts keywords nil (now protects
this field too); added tokenisation + exposure-gating + interior-word coverage.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ody text Owner decision 2026-07-24. Global Spotlight on iOS 17+ surfaces only title/displayName matches for third-party items (Apple FB17330079/FB17408320, unresolved; proven on-device: a body word matched contentDescription, textContent AND keywords via in-app CSSearchQuery yet never appeared in home-screen search). The two body-indexing levels therefore put decrypted text in the OS index in exchange for search results iOS cannot deliver. - Settings menu now renders the two body levels greyed + non-selectable (explicit Buttons; a Picker cannot disable individual options), with the caption explaining why. - SpotlightExposure.isSelectable gates the offer; SpotlightExposure.current and the view binding clamp a previously persisted body level to .type. - One-time migration in AppModel.init: rewrite the stored choice, wipe the OS index immediately (works while locked), and rebuild type-label items on the first unlock so un-edited Takes do not vanish from title search. - The indexing pipeline keeps full body support (textContent + keywords land in the index and match via CSSearchQuery) so re-enabling when Apple fixes surfacing is a one-line isSelectable flip. - DEBUG diagnostics from the hunt (force-entitled button, per-field query probe, debugReport) are removed; the content-free reindex breadcrumb stays. Tests: SpotlightExposureLockTests pins the offered set, the clamp, and the fallback; existing core SpotlightIndexerTests still green. Co-Authored-By: Claude Fable 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.
Summary
The on-device Spotlight investigation, end to end. Squash-merge — the intermediate DEBUG-probe commits add then remove diagnostic scaffolding.
Root cause (measured, not guessed): iOS 17+ global Spotlight surfaces only
title/displayNamematches for third-party items. Proven on-device: a body word matchedcontentDescription,textContentANDkeywordsvia in-appCSSearchQuery(desc=1 text=1 kw=1), yet home-screen search showed nothing — while title matches ("reminder") surfaced. Known open Apple issue (FB17330079/FB17408320, no workaround).What ships:
textContentand tokenisedkeywordsalongsidecontentDescription— all from ONE exposure-gated source (nil at None/Type-only). Correct per the API contract and future-proof for Apple's fix; verified present and matchable in the live index.isSelectableflip.Tests
SpotlightExposureLockTests(new): offered set, clamp, fallback.SpotlightIndexerTests: textContent/keywords population, tokenisation, exposure gating; the Type-only privacy guard (all body fields nil) held throughout.🤖 Generated with Claude Code