✨ feat: static versioning via gradle.properties#6
Merged
Conversation
vitofico
added a commit
that referenced
this pull request
May 15, 2026
* 🚧 chore: seed gradle.properties with current version values * ✨ feat: read app version from gradle.properties instead of git describe * 🔥 chore: remove buildSrc/ — no longer needed with static versioning * 👷 ci: bump+tag gradle.properties on main, read static on PR * 👷 ci: drop QUIRE_VERSION_FALLBACK from release job, checkout the freshly-pushed tag * 👷 ci(codeql): drop QUIRE_VERSION_FALLBACK and fetch-tags * 📝 docs: rewrite release.md for static gradle.properties versioning
vitofico
added a commit
that referenced
this pull request
May 21, 2026
…le (pr-γ) PR-γ of Bundle 3 (Reader Profile) — adds the user-visible Reader Profile surface on Android. New "Insights" entry on the Library overflow menu opens LibraryInsightsScreen, which renders five sealed UI states (Disabled / Empty / Loading / Loaded / Error) over GET /ai/v1/profile and POST /ai/v1/profile/refresh. Highlights: * AiClient gains refreshProfile() (200 → ReaderProfileResponseDto, 409 ai_not_opted_in, 429 quota, 5xx model failure) and deleteProfile() (idempotent 204 — wired by PR-δ from Settings). * AiRepository.refreshProfile drains the sync orchestrator + library uploader best-effort before calling the server (Lock #6 NTH #9; corrections.md PR-γ Required #6). Preflight failures log via the PreflightOutcome callback and the refresh proceeds regardless. * Decoupling via two fun interfaces (ProfilePreflightSync / ProfilePreflightLibrary) so :data:ai never depends on :data:sync or :data:library. * AiConfig.progressSupported (Lock #5) consumed to drive Disabled.ProgressUnsupported; the server already advertises the field. * Local input-fingerprint approximation per Lock #12 (mirrors PR-β's server recipe; uses ReaderStats.booksWithThemesCount per Lock #15 / CC-8, NOT topThemes.size). Mismatch renders a low-emphasis "Profile may be out of date" hint (architect Finding #2). * In-library recommendations resolve via DocumentRepository.findByIdentity → /reader/{id}; discovery recommendations expose an explicit "Open in browser" button gated on https:// (architect Finding #4), wrapped in runCatching for ActivityNotFoundException. * Coverage meter uses booksWithThemesCount/totalBooks (NTH #12, Lock #15). * AppNavGraph wires composable("library/insights"); AppContainer adds LibraryInsightsViewModelFactory + a StateFlow<Boolean> over AiPreferences.aiEnabled. Tests cover the disabled/empty/loaded/error state transitions, the local fingerprint recipe (match + mismatch + null cases), and best-effort preflight semantics (sync→library→refresh order, network still fires when preflight throws). All :app:testDebugUnitTest and :data:ai:testDebugUnitTest pass.
vitofico
added a commit
that referenced
this pull request
May 21, 2026
…-β + PR-γ + PR-δ) (#34) * ✨ feat(server): reader_profiles substrate + abandoned_at + ReaderStats v1 + GET /ai/v1/profile Lands the server half of pr-α (Bundle 3 / Reader Profile substrate): * Migrations - progress_002: add `progress.abandoned_at` + check constraint `finished_at IS NULL OR abandoned_at IS NULL` + partial index on `(document_pk) WHERE abandoned_at IS NOT NULL`. - ai_005: new `reader_profiles` table keyed on `(tenant_id, subject)` with `payload JSONB`, `schema_version`, `model_id`, `prompt_version`, `input_fingerprint VARCHAR(16)` (Lock #12 / coordinator §3.6). * ORM - `Progress.abandoned_at` + `ck_progress_abandoned_xor_finished`. - New `ReaderProfile` model — user-scoped, NOT shared cache. * Pydantic - `AuthorCount`, `ReaderStats` (with `books_with_themes_count: int = 0` per Lock #15 / CC-8), `BookRec` (full §3.4 provenance shape with safe defaults), `ReaderProfilePayload` (schema_version=1), and the `ReaderProfileResponse` envelope. * `_compute_reader_stats` (`quire_server.core.ai.service`) - Deterministic per-user stats: total/finished/in-progress/abandoned counts, `finish_rate_by_theme` (top-10), `most_read_authors` (top-5). - Mirrors PR9's `pick_priority = case(...)` pattern exactly (coordinator §3.4 REJECT (e)). - pr-α emits `books_with_themes_count = 0`; pr-β replaces with the real count. * Terminal-state invariant - `push_progress` clears the opposite terminal flag on a flip; preserves `percent` on the abandon transition. - `pull_progress` drops `abandoned_at` (finished wins) for corrupt legacy rows and emits a structured warning. * `GET /ai/v1/profile` - Cache-only read. No opt-in gate (opted-out users can still read their last generation). 404 when no row exists. Refresh / LLM generation lives in pr-β. * Tests (server/tests/integration/test_reader_profile.py + adjusted test_health / test_migrate_script / test_readyz_migration_state for the new ai_005 + progress_002 heads). * ✨ feat(android): abandoned_at progress field, MIGRATION_7_8, profile fetch + abandon DAO Lands the Android half of pr-α (Bundle 3 / Reader Profile substrate): * Room - DB version 7 -> 8 + MIGRATION_7_8 adds nullable `abandonedAt INTEGER` column to `progress`. - `ProgressEntity.abandonedAt: Long?` (Long? maps to nullable INTEGER). - `ProgressDao.markAbandoned` / `unmarkAbandoned` bump BOTH `updatedAt` AND `localUpdatedAt` to `now`. This is load-bearing: Android pushes `progress.updatedAt` as `client_updated_at`, and the server's LWW guard only accepts the row when `client_updated_at` is strictly newer. Setting only `localUpdatedAt` would make the row "dirty" locally but the server would silently reject the abandon. - `markAbandoned` clears `finishedAt`; `unmarkAbandoned` leaves `percent` untouched. (Abandoning at 60% remembers 60%.) * Core / sync DTO - `Progress.abandonedAt` on the domain model so the push path can read it through ProgressRepository.dirty(). - `ProgressItemDto.abandonedAt` (snake_case wire name). - `SyncOrchestrator`: push serializes `abandonedAt` as ISO-8601; pull defensively drops `abandonedAt` if `finishedAt` is also set (finished wins) — mirrors the server's defensive read. * AI - `AiClient.fetchProfile()` -> `GET /ai/v1/profile`. Maps 404 to null. - `AiRepository.fetchProfile()`, `markAbandoned()`, `unmarkAbandoned()`. Constructor gains an optional `progressDao` so existing test sites that only stub `insightDao` keep compiling; AppContainer wires the real DAO. - DTOs for `ReaderProfileResponse`, `ReaderProfilePayload`, `ReaderStats` (including `booksWithThemesCount` per Lock #15 / CC-8), `BookRec` (full coordinator §3.4 shape), `AuthorCount`. - `source_type`, `owned_state`, `confidence` declared as `String` (not enums) so a future server bump doesn't crash existing clients. * Tests - MigrationTest: `migrate 7 to 8 adds abandonedAt column`, `migrate 6 to 8 chains via book_insights and abandonedAt` (the Lock #8 chained-migration test). - ProgressDaoTest: mark/unmark abandon semantics + LWW timestamp correctness (BOTH updatedAt AND localUpdatedAt bumped) + dirty() pickup. - ProgressDtosTest: round-trip + null-omission for `abandonedAt`. - SyncOrchestratorTest: defensive-pull rule when server emits both terminal flags. - AiRepositoryProfileTest: fetchProfile 404/200, mark/unmark delegation through FakeProgressDao. * ✨ feat(server): refresh_profile orchestrator + POST /ai/v1/profile/refresh (pr-β) Stitches the AI building blocks into a per-user reader-profile orchestrator gated on AI_ENABLED + PROGRESS_ENABLED + opt-in: - ai_006 migration: ai_generation_log.book_insight_id becomes nullable, new `kind` discriminator with CHECK admitting 'insight'/'profile'/'promote' (Lock #11 amendment), partial index for profile-row lookups, and ai_usage_daily.profile_count for the 3/day cap. - refresh_profile(): per-user singleflight lock, low-data short-circuit with weight=0 (runs even at quota cap), daily-cap enforcement, global semaphore around the LLM call, deterministic candidate maps for in-library + OpenLibrary-discovery recommendations, owner-exclusion belt-and-suspenders, 16-hex-char input_fingerprint, kind='profile' audit rows per waiter (including collapsed singleflight waiters). - PR-ζ wire-up (Lock #11 amendment): promote handler now writes a kind='promote' DB row alongside the retained stdout audit line. - Retriever.author_bibliography(): 30d positive cache, 24h negative cache, 429 with Retry-After capped at 6h, 5xx → stale-if-error via new allow_stale=True read path. - _compute_reader_stats now populates books_with_themes_count (Lock #15). - ConfigResponse gains progress_supported so AI-only deploys can suppress the Android reader-profile UI (coordinator §3.5). - READER_PROFILE_PROMPT + READER_PROFILE_PROMPT_VERSION='1' bumped independently of the per-book PROMPT_VERSION. - Tests: existing audit-log / migration / promote / health tests updated for the ai_006 schema (kind required, head moves ai_005 → ai_006). Integration test_reader_profile passes all 11 cases. - docs/sync-api.md: full POST /profile/refresh section + progress_supported on GET /ai/v1/config. 426 tests pass. * ✨ feat(android): Library Insights screen + refreshProfile/deleteProfile (pr-γ) PR-γ of Bundle 3 (Reader Profile) — adds the user-visible Reader Profile surface on Android. New "Insights" entry on the Library overflow menu opens LibraryInsightsScreen, which renders five sealed UI states (Disabled / Empty / Loading / Loaded / Error) over GET /ai/v1/profile and POST /ai/v1/profile/refresh. Highlights: * AiClient gains refreshProfile() (200 → ReaderProfileResponseDto, 409 ai_not_opted_in, 429 quota, 5xx model failure) and deleteProfile() (idempotent 204 — wired by PR-δ from Settings). * AiRepository.refreshProfile drains the sync orchestrator + library uploader best-effort before calling the server (Lock #6 NTH #9; corrections.md PR-γ Required #6). Preflight failures log via the PreflightOutcome callback and the refresh proceeds regardless. * Decoupling via two fun interfaces (ProfilePreflightSync / ProfilePreflightLibrary) so :data:ai never depends on :data:sync or :data:library. * AiConfig.progressSupported (Lock #5) consumed to drive Disabled.ProgressUnsupported; the server already advertises the field. * Local input-fingerprint approximation per Lock #12 (mirrors PR-β's server recipe; uses ReaderStats.booksWithThemesCount per Lock #15 / CC-8, NOT topThemes.size). Mismatch renders a low-emphasis "Profile may be out of date" hint (architect Finding #2). * In-library recommendations resolve via DocumentRepository.findByIdentity → /reader/{id}; discovery recommendations expose an explicit "Open in browser" button gated on https:// (architect Finding #4), wrapped in runCatching for ActivityNotFoundException. * Coverage meter uses booksWithThemesCount/totalBooks (NTH #12, Lock #15). * AppNavGraph wires composable("library/insights"); AppContainer adds LibraryInsightsViewModelFactory + a StateFlow<Boolean> over AiPreferences.aiEnabled. Tests cover the disabled/empty/loaded/error state transitions, the local fingerprint recipe (match + mismatch + null cases), and best-effort preflight semantics (sync→library→refresh order, network still fires when preflight throws). All :app:testDebugUnitTest and :data:ai:testDebugUnitTest pass. * ✨ feat(pr-δ): DELETE /ai/v1/profile + abandoned UX + delete-profile button Server: - DELETE /ai/v1/profile (idempotent, 204) gated on ai_enabled + opt-in - 5 integration tests covering happy path, idempotency, tenant isolation, not-opted-in (409), and ai_disabled (503) Android: - LibraryPreferences struct (sort + showAbandoned), persisted via the same SharedPreferences file (Lock #17); migration is read-only — old key 'library_sort' keeps working. - Library home gains a 'Show abandoned' toggle; abandoned books are hidden by default. - Settings: 'Delete reader profile' button calls AiRepository.deleteProfile() with snackbar feedback (success / failure) and a re-entrancy guard. - ProgressRepository: expose abandonedAt for the library filter. * ✨ fix(server): POST /profile/refresh returns the ReaderProfileResponse envelope `POST /ai/v1/profile/refresh` used to return the raw `ReaderProfilePayload` while the Android client decodes the wrapped `ReaderProfileResponseDto` envelope — every real refresh would have failed client-side deserialization. The bug was masked in `AiRepositoryRefreshTest` because the fixture body already used the envelope shape. Refresh now returns the same envelope as `GET /profile` so the server is internally consistent and the Android client decodes one DTO for both endpoints. Added an integration test that exercises the low-data short-circuit path (no LLM call required) and asserts the envelope top-level fields are present. * ✨ fix: canonicalize reader profile fingerprint as epoch millis Server hashed `datetime.isoformat()` (UTC → `+00:00`) while Android hashed `Instant.toString()` (UTC → `Z`). The two representations diverge for the same instant, so the fingerprint compare reported spurious staleness on every refresh and the "Profile may be out of date" banner would have been permanently on. Both ends now serialize `latest_progress_updated_at` as epoch milliseconds (or the literal `"none"` when absent) before hashing. Docstrings on both sides call out the contract so a future change on either side cannot drift again. Existing VM fingerprint test updated to the canonical token and a second test covers the no-progress path. * ✨ feat(android): surface ai_suggested_recommendations in Library Insights The server `ReaderProfilePayload` carries an `ai_suggested_recommendations` list alongside the in-library and discovery buckets, but the Android DTO didn't declare the field and the screen never rendered the bucket — so AI-only suggestions (no candidate match, no Open Library URL) were silently dropped from the UI. Added the field to `ReaderProfilePayloadDto` (defaulted, so older server builds round-trip cleanly) and added a "Suggested by AI" LazyRow section between Discovery and the footer. `BookRecCard` already handles `source_type == "ai_suggested"` by showing title/author/rationale with no link affordance, so no card-level changes were needed.
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
Replaces the runtime
git describeversion derivation with staticVERSION_NAME/VERSION_CODEingradle.properties, bumped + committed + tagged by CI on every push tomain.Why
fdroid checkupdatesauto-update detection — fdroidserver can now statically parse the version from source.buildSrc/(Version parser + tests) and theQUIRE_VERSION_FALLBACKenv var plumbing.Out of scope
Test plan
chore/static-versioningbranch CI:android-cibuild job green;releasejob skipped; APK manifest reports2026.05.08.43.mainCI auto-bumpsgradle.properties, creates av2026.05.09.<run>tag, publishes a Release.Could not derive versionerrors.