feat(search): commercial-parity features, privacy hardening, and cross-layer fixes#103
Merged
Merged
Conversation
…s-layer fixes Closes the gap to major commercial engines while tightening the privacy guarantees, in three strands: New search features (all on-device, no new third parties): - Instant answers: calculator (2+2, sqrt(9)*3, pi*2), unit conversions (10 km to miles, 72 f to c), number-base conversions (0xff in decimal), and percentages (15% of 80), rendered as an answer card on the served page and in-app. Pure local computation; date/phone-shaped input is guarded so it never mis-fires. - DuckDuckGo-style !bangs (!w, !gh, !yt, !osm, ...) resolved from a curated on-device table; the terms never enter the metasearch fan-out, and unknown tags (e.g. !important) are never hijacked. - Search-as-you-type on the served pages: an ARIA-listbox autocomplete dropdown over the existing /suggest endpoint with debounce, stale-fetch aborts, and full keyboard support; '/' focuses the search box. - Results meta line now shows result count and elapsed time; a favicon is served; the topbar/sort/did-you-mean controls carry the active vertical and sort instead of silently resetting them. Privacy hardening: - The Wikipedia summary thumbnail is re-served through a new loopback /img proxy (SSRF-scoped to upload.wikimedia.org, image-only, size- capped) so the browser never fetches from Wikimedia directly - that fetch leaked the user's IP plus the searched entity via the filename. CSP tightens from img-src https: to img-src 'self' data: and gains connect-src 'self'. - Engine adapters now pin one User-Agent per logical search (a 429 retry no longer switches identities mid-conversation), the UA pool is refreshed, and per-host politeness spacing is process-wide and thread-safe (it was per-search and racy, i.e. never effective). - WikiSummaryProvider reads are size-capped like every other fetch. - OpenSearch descriptor uses the request's Host for network-mode visitors so browser integration works off-device (without embedding the access token). Correctness and data-safety fixes: - Bootstrap metadata (the only copy of the wrapped DEK) is written atomically (temp + fsync + rename), and a present-but-corrupt file now fails CLOSED instead of silently re-keying and destroying all encrypted data. - Whole-call HTTP timeout so a trickling upstream can no longer hold a search open for minutes past the engine deadline. - Vertical scoping no longer poisons engines without site:/OR syntax (Wikipedia/Marginalia/Mwmbl now receive the clean query; constraints stay locally enforced), and upstream spelling corrections no longer leak or duplicate the vertical's site: clause. - Dedup key folds http/https and mobile-host variants of the same page; merged results backfill a blank title; domain rules resolve most-specific-first; slash dates with day>12 parse day-first; before:/after: accept single-digit months/days; DuckDuckGo's direct- href variant is parsed instead of yielding zero results; media links encode spaces as %20 (paths); spell-corrector auto-search threshold raised so brand names are not silently rewritten. - History suggest escapes LIKE wildcards. - App: fresh ViewModels per create() (Settings previously went dead after one back-out and history recording silently stopped), cancelled searches no longer flash an error state, onboarding survives rotation and handles system back, battery-exemption cards refresh on resume, the widget deeplink no longer re-fires on rotation, the notification prompt defers to onboarding, engine toggles are transactional, update banner dismissal is saveable, APK staging is off the main thread, all startActivity calls are guarded, result cards show the source host, and the search field auto-focuses when opened idle. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Qsx1eDT79eSgUwfxUaYS34
- ThumbnailProxy.kt's KDoc contained a literal `image/*`; Kotlin block
comments nest, so the '/*' inside the comment opened a nested comment
that never closed and ktlint failed to parse the file ('61:1 Unclosed
comment'). Reworded to 'image content types'. A context-aware sweep of
every changed file found no other nested-comment instance.
- Bangs table: the max-line-length suppression did not cover the
argument-list-wrapping rule that also fires past the margin; suppress
both (one bang per line beats wrapped URL templates).
- Import order: engine.aggregate imports sort before engine.bang in
SearchServer.kt; server.SearchServer before server.ThumbnailProxy in
SearchMobService.kt.
- UnitConverter: blank line before the local label() declaration.
Verified with the standalone ktlint CLI pinned to the ruleset that
matches CI (1.0.1 + repo .editorconfig reports zero findings on files
that are green on main): all changed files now lint clean, and the
241-test engine suite still passes.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Qsx1eDT79eSgUwfxUaYS34
ErikChevalier
added a commit
that referenced
this pull request
Jul 11, 2026
Version bump and changelog for the 26.07.03 release: on-device instant answers, !bangs, search-as-you-type on the served pages, Wikipedia thumbnail privacy proxy, engine User-Agent pinning, atomic bootstrap metadata writes, and a broad set of ranking, server, and app-UI fixes (PR #103), plus the androidx.sqlite 2.7.0 bump. Co-authored-by: FlintWave <flintwave@tuta.com> 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.
What & why
This change closes much of the everyday-experience gap to major commercial engines while tightening the privacy guarantees, and fixes a broad set of verified bugs found in a full four-layer audit (engine, server/web UI, app UI, data/crypto).
New search features — all computed on-device, no new third parties:
2+2,sqrt(9)*3), unit conversions (10 km to miles,72 f to c), number-base conversions (0xff in decimal), and percentages (15% of 80), shown as an answer card on the served page and in-app. Date/phone-shaped input (2020-2021,555-1234) is guarded so the card never mis-fires.!w,!gh,!yt,!osm, …): jump straight to a site's own search, resolved from a curated on-device table. The terms never enter the metasearch fan-out; unknown tags (!important css) are never hijacked./suggestendpoint (debounced, aborts stale fetches, same-origin only)./focuses the search box.Privacy hardening:
/imgproxy (scoped toupload.wikimedia.org, image-only, size-capped) — previously the browser fetched it from Wikimedia directly, leaking the user's IP plus the searched entity via the image filename. CSP tightens fromimg-src https:toimg-src 'self' data:(plusconnect-src 'self'for the suggestions dropdown).WikiSummaryProviderbody reads are size-capped like every other fetch; the OpenSearch descriptor uses the request Host for network-mode visitors (without embedding the access token) so browser integration works off-device.Correctness / data-safety fixes (highlights):
callTimeout) so a byte-trickling upstream can no longer hold a search open for minutes.site:/ORsyntax (Wikipedia/Marginalia/Mwmbl get the clean query; constraints stay locally enforced); upstream spelling corrections no longer leak or double the vertical'ssite:clause.after:2024-3-1works, DuckDuckGo's direct-href A/B variant parses instead of yielding zero results, and the spell-corrector's auto-search threshold is raised so brand names ("spotify") are never silently rewritten ("spotty").create()(Settings previously went permanently dead after one back-out, silently stopping history recording), cancelled searches no longer flash a raw error, onboarding survives rotation + handles system back + routes "Open privacy settings" to Settings, battery-exemption cards refresh on resume, widget deeplink no longer re-fires on rotation, notification prompt defers to onboarding, engine toggles are transactional (no lost writes), update-banner dismissal survives rotation, APK staging is off the main thread, allstartActivitycalls are guarded, result cards show the source host, and the search field auto-focuses when opened idle.Known items deliberately deferred (documented for follow-up): true upstream pagination, streamed/early page render, vault-lock races around DataStore caching, and loopback app-to-app access control.
Relates to OpenSpec change:
add-relevance-ranking(plus touches onadd-result-paging,add-engine-status-visibility)Checklist
feat//fix/branch offmain./gradlew ktlintCheck lint test assembleDebugis green locally — no Android SDK in this environment; the pure-JVM engine/data suites (241 + 19 tests) were compiled and run green against Kotlin 2.4.0 locally, CI validates the rest!gbang is an explicit user-chosen redirect, not scraping)openspec validate <name> --strict) and tasks are checked off — no new OpenSpec change authored for this cross-cutting fix/parity sweep🤖 Generated with Claude Code
https://claude.ai/code/session_01Qsx1eDT79eSgUwfxUaYS34
Generated by Claude Code