Skip to content

fix(resources): complete offline manifests beyond 100 search hits - #437

Merged
mattrace-gloo merged 4 commits into
mainfrom
mrace/fix/431-complete-resource-manifest-clean
Oct 22, 2026
Merged

mattrace-gloo merged 4 commits into
mainfrom
mrace/fix/431-complete-resource-manifest-clean

Conversation

@mattrace-gloo

@mattrace-gloo mattrace-gloo commented Oct 21, 2026 •

Copy link
Copy Markdown
Contributor

TLDR

Prepare Offline manifests no longer re-slice a successfully enumerated Aquifer search back to 100 items per configuration. When bounded pagination collects every matching resource (including counts above 100), the response stays truncated: false with full hydration and provider-reported metadata. When the 20-page search ceiling stops collection without provider proof of completeness, truncated stays true so clients cannot treat a partial catalogue as complete. This enumeration fix does not make missing asset sizes known: the observed underestimate is tracked separately in fluent-mobile#756, with the API size contract under #273.

Details

Refs #431
Related mobile acceptance: fluent-mobile#736 (do not close until Android QA on a deployed revision)

  • Base: origin/main @ 4bccdc1
  • Root cause: getPrepareOfflineManifest set truncated and slice(0, 100) after searchAllResources had already paginated (100/page, ≤20 pages).
  • Fix: return { items, complete } from bounded Aquifer enumeration; hydrate all collected items; set truncated only when complete === false.
  • Builds on landed #433: manifest hydration uses the 'fail' policy so any getResource failure fails closed (502) rather than silently dropping items.
  • No public pagination/continuation protocol change; no arbitrary hydration-cap increase; no unbounded load.
  • No overlap with held image caption/license builder work (now landed as #435 on this merge-base).
  • #424 modularization does not touch translation-resources (compared file list). #411 only adds Bible-text 404 httpStatus on Aquifer errors — no intentional interaction.
  • No deploy in this PR.

Type of change

  • Bug fix
  • New feature
  • Breaking change
  • Docs / chore

Technical changes

  • src/lib/services/aquifer/aquifer.client.ts — searchAllResources returns { items, complete }; export AQUIFER_MAX_SEARCH_PAGES
  • src/lib/services/aquifer/aquifer.types.ts — AquiferResourceSearchEnumeration
  • src/domains/translation-resources/translation-resources.service.ts — remove per-config 100 slice; hydrate full enumeration; truncated from !complete; warn on incomplete range/image searches
  • src/domains/translation-resources/translation-resources.types.ts — OpenAPI truncated description updated
  • Focused route + Aquifer client regressions (synthetic mocks, not live payloads)

Standards exceptions

  • FA-L7 (vi.mock('@/db') in unit tests): translation-resources.manifest-completeness.test.ts carries a new-file suppression for the same auth-chain activeOrg @/db mock already grandfathered in sibling route-test files. FA-L11 forced the split out of translation-resources.test.ts; net no-restricted-syntax count across those files is flat (test.ts 5→4, new file +1). Declared here rather than regenerating the baseline to absorb a new violation. Follow-up: mock the middleware/service seam so the new file can drop the suppression.

Testing

  • Focused Vitest (--configLoader=runner): 67 passed across translation-resources + aquifer.client suites (includes >100 complete / page-ceiling incomplete cases, plus images-range warn assertion)
  • prettier --check on touched files: pass
  • eslint on touched domain/aquifer paths: pass
  • tsc --noEmit: pass
  • npm run deadcode / knip: blocked locally — knip@5.88.1 not installed in shared node_modules; stock script invokes bare npx (not run; no install)
  • test:db / test:live: skipped (no isolated DB / no live Aquifer credentials)
  • scripts/check-docs-structure.sh: fails on pre-existing docs/.DS_Store (unrelated; not modified here)
  • No live API load, no dependency acquisition, no deploy

How to verify

  1. Unit: from repo root, ./node_modules/.bin/vitest run --configLoader=runner src/domains/translation-resources src/lib/services/aquifer/aquifer.client.test.ts — expect pass including #431 >100 complete and page-ceiling incomplete cases.
  2. After deploy (out of scope): Android Prepare Offline for a single assigned chapter that previously emitted truncated_manifest (e.g. project 12) — expect a non-truncated catalogue with all enumerated metadata; record runtime revision against mobile #736 before closing either issue. Size completeness and unknown-byte presentation remain separate verification under fluent-mobile#756 / Expose Aquifer-backed Translation Resources API for mobile (TN / TQ / Images & Maps) #273.

Follow-ups

Delivery note: source-identical replacement for #436 solely to remove an automatically added coauthor trailer. The tested source tree is unchanged; no force-push, merge or deployment.

Summary by CodeRabbit

  • Bug Fixes
    • Translation-resource catalogs can now include more than 100 resources per configuration.
    • Catalogs are marked as truncated when resource enumeration is incomplete, rather than based on a per-configuration item limit.
    • Range searches return collected results when enumeration is incomplete and log a warning; manifest requests continue to fail when required resource hydration fails.

Stop re-slicing bounded Aquifer search results at 100 and report truncated
only when provider pagination metadata cannot prove enumeration finished.
@mattrace-gloo mattrace-gloo self-assigned this Oct 21, 2026
@coderabbitai

coderabbitai Bot commented Oct 21, 2026 •

Copy link
Copy Markdown
Contributor

Review in Change Stack →

Warning

Review limit reached

You've used all free OSS reviews for now. Wait for the free limit to reset to keep reviewing this public repository.

Next included review available in 59 minutes.

Check out review usage here.

View limit details

Limit details: You’ve used the included review currently available.

Learn how review limits work.

Review configuration:

⚙️ Run configuration
  • Source mode: Auto
  • Repository: Unavailable
  • Branch: Unavailable
  • File: No configuration file from this repository applied
  • Commit: Unavailable
  • Layers: defaults
  • Review profile: CHILL
  • Plan: Advanced
  • Run ID: 74166642-d470-4596-babd-9551305ff221
📥 Commits

Reviewing files that changed from the base of the PR and between d14adcd and 10d26f0.

📒 Files selected for processing: 1
  • src/domains/translation-resources/translation-resources.range.test.ts

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration
  • Source mode: Auto
  • Repository: Unavailable
  • Branch: Unavailable
  • File: No configuration file from this repository applied
  • Commit: Unavailable
  • Layers: defaults
  • Review profile: CHILL
  • Plan: Advanced
  • Run ID: ade6b9af-97b0-4eb5-918a-7962a317a5bd
📥 Commits

Reviewing files that changed from the base of the PR and between 4bccdc1 and d14adcd.

📒 Files selected for processing: 11
  • eslint-suppressions.json
  • src/domains/translation-resources/translation-resources.license.test.ts
  • src/domains/translation-resources/translation-resources.manifest-completeness.test.ts
  • src/domains/translation-resources/translation-resources.range.test.ts
  • src/domains/translation-resources/translation-resources.service.ts
  • src/domains/translation-resources/translation-resources.test-fixtures.ts
  • src/domains/translation-resources/translation-resources.test.ts
  • src/domains/translation-resources/translation-resources.types.ts
  • src/lib/services/aquifer/aquifer.client.test.ts
  • src/lib/services/aquifer/aquifer.client.ts
  • src/lib/services/aquifer/aquifer.types.ts

Included review availability: This review used your included allowance. Your plan provides up to 1 included review per hour; 0 remain after this review.

📝 Walkthrough

Aquifer searches now return collected resources with a completeness status. Translation-resource services use the collected items for range and image results, and use the status to determine manifest truncation. Tests cover pagination limits and manifest behavior.

Changes: Translation-resource enumeration

Layer / File(s) Summary
Aquifer search enumeration contract and pagination
src/lib/services/aquifer/aquifer.types.ts, src/lib/services/aquifer/aquifer.client.ts, src/lib/services/aquifer/aquifer.client.test.ts
searchAllResources returns items and a complete flag. The exported page ceiling is 20. Tests cover completion signals and reaching the ceiling.
Resource search consumers
src/domains/translation-resources/translation-resources.service.ts, src/domains/translation-resources/translation-resources.test-fixtures.ts, src/domains/translation-resources/translation-resources.range.test.ts, src/domains/translation-resources/translation-resources.license.test.ts, src/domains/translation-resources/translation-resources.test.ts
Range and image searches use the enumerated items and log incomplete searches. Test fixtures and search mocks use the enumeration result shape.
Manifest completeness and response tests
src/domains/translation-resources/translation-resources.service.ts, src/domains/translation-resources/translation-resources.types.ts, src/domains/translation-resources/translation-resources.manifest-completeness.test.ts, src/domains/translation-resources/translation-resources.test.ts, eslint-suppressions.json
Manifest truncation follows the enumeration completeness flag. Added tests cover manifests with more than 100 resources, incomplete enumeration, hydration and search failures, response content, byte totals, and invalid chapter ranges. The former manifest tests were removed from translation-resources.test.ts.

Priority: ➖ Normal

Estimated code review effort: 3 (Moderate) | ~25 minutes

Change: Bug fix · Severity of issue fixed: Medium

Possibly related issues

Suggested reviewers: kaseywright

Merge Risk: ⚪ Minimal · up to d14ad

The change appears mergeable after normal checks. Outstanding Android verification and asset-size work remain separate follow-ups.

Pre-merge checks: Passed 4 · Failed 1

Failed Failed checks: 1 warning

Check name Status Explanation Resolution
Linked Issues check Warning Issue #431 requires a complete manifest with accurate resource sizes. The PR implements bounded enumeration, removes the per-configuration 100-item slice, propagates complete, and adds tests for 101… Implement or integrate the API size contract required by #431/#273 for manifest assets. Add focused tests that verify accurate sizes for the affected media resources. Keep truncated and failure behavior fail-closed when size data is not a…
Passed Passed checks: 4 passed
Check name Status Explanation
Description Check Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check Passed The title clearly and concisely describes the main change: allowing offline manifests to include more than 100 Aquifer search hits.
Out of Scope Changes check Passed The changes support issue #431. Aquifer pagination metadata, shared result-type updates, range-caller migrations, warnings, manifest tests, and lint-suppression updates are required by or support the …
Docstring Coverage Passed Docstring coverage is 83.33% which is sufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 6 functions across 10 files. (1 skipped: 1 …
Full details: Out of Scope Changes check

Explanation

The changes support issue #431. Aquifer pagination metadata, shared result-type updates, range-caller migrations, warnings, manifest tests, and lint-suppression updates are required by or support the bounded enumeration change. No unrelated product behavior is demonstrated.

Full details: Docstring Coverage

Explanation

Docstring coverage is 83.33% which is sufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 6 functions across 10 files. (1 skipped: 1 unsupported.)

Full details: Linked Issues check

Explanation

Issue #431 requires a complete manifest with accurate resource sizes. The PR implements bounded enumeration, removes the per-configuration 100-item slice, propagates complete, and adds tests for 101 resources and the 20-page ceiling. However, buildManifestItem still uses asset?.size ?? 0 for media items, and the PR states that missing asset sizes remain unresolved under #273. This leaves a direct coding requirement unmet. The Android QA requirement is manual verification and is not used for this coding verdict.

Resolution

Implement or integrate the API size contract required by #431/#273 for manifest assets. Add focused tests that verify accurate sizes for the affected media resources. Keep truncated and failure behavior fail-closed when size data is not available.

✨ Finishing Touches
📝 Generate docstrings
  • Commit to this branch
  • Create a new PR
🧪 Generate unit tests (beta)
  • Commit to this branch
  • Create a new PR
  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@kaseywright kaseywright left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reviewed against origin/main @ c8d4cb6 (single commit 931f3e6). Verified locally: the
focused suites pass 61/61, tsc --noEmit, prettier --check, eslint on touched paths are
clean, and knip flags neither new export. Core fix is right — the 100-slice is gone,
complete→truncated wiring is correct, and the ceiling semantics are codified in tests.
A few things below, ordered by weight.

Should address

  1. truncated OpenAPI description overstates hydration — translation-resources.types.ts:315-317.
    "False when every matching resource was collected and hydrated" isn't true today:
    mapWithConcurrency skips failed getResource calls, so 1-of-N hydrate failure yields
    truncated:false with a required item silently missing — the same "partial looks
    complete" failure class #431 exists to kill. Deferring strict-fail to #433 is a fine
    scope split, but the doc shouldn't claim a guarantee the code doesn't provide. Suggest
    softening to "collected" (or noting hydrate failures may still drop items) until #433
    lands.

  2. Range-route callers discard complete — translation-resources.service.ts:271-282
    (searchVerseRangeCollection) and :326-337 (getTranslationImagesForRange). Both do
    search.data.items and drop complete, so a >2000-hit verse range still returns a
    silently partial items array — no flag, no warn, and unchanged OpenAPI descriptions.
    Pre-existing behavior, but the diff introduces the signal specifically so partial sets
    can't pass as exhaustive, then drops it in two of three callers. Minimum: a
    logger.warn with collection/range context (also satisfies FA-R8); alternatively a
    comment documenting that the exhaustion guard is intentionally manifest-only.

  3. No server-side signal on !complete — service.ts:463-465. truncated:true tells
    the client, but nothing logs which config hit the 20-page ceiling. If this ever fires in
    prod we'll want logger.warn({ resourceCollectionCode, bookCode, startChapter, endChapter }) here — debugging "some config truncated" with no record will be rough.

Judgement calls / nits

  • aquifer.client.ts:35 — const MAX_SEARCH_PAGES = AQUIFER_MAX_SEARCH_PAGES; is a
    leftover alias used once at :253, and the docstring at :242 still names
    MAX_SEARCH_PAGES. Use the export directly.
  • aquifer.client.ts:260-261 — page.data.items.length === 0 || is subsumed by
    < limit.
  • Provider-contradiction edge — searchAllResources breaks complete=true on a
    short/empty page even when allItems.length < totalItemCount (codified by
    aquifer.client.test.ts:294-316: total 500, 1 item → complete). Trusting the page signal
    is defensible pagination semantics; worth a comment acknowledging that count-vs-page
    disagreement resolves to "complete".
  • Wasted hydration when !complete — the manifest hydrates the entire partial set
    (up to 2000 items × getResource) even though the flag means the primary client rejects
    the response. Consider skipping hydration when !search.data.complete if no other
    consumer needs the partial items.
  • Serial config loop — service.ts:447: five independent config searches awaited
    serially (~100 sequential upstream calls worst case). Pre-existing; if it's intentional
    (Aquifer rate limits) it deserves a comment per FA-R10.
  • Minor (pre-existing): the manifest path doesn't dedupe hits — the range path does via
    dedupeSearchHitsById. Duplicated provider items → duplicate aquiferContentId entries.

Spec check (#431)

The bounded-enumeration approach satisfies the issue's core asks: no re-slicing, exhaustion
can't pass as complete on the manifest route, and there's focused >100 + ceiling coverage.
Deferred ACs (accurate sizes → #756/#273, strict hydrate → #433, Android QA → post-deploy)
are reasonable splits — just keep #431 open until the mobile verification lands.

Resolve conflicts with #432 fail-closed manifest hydrate and #435 image
metadata. Address #437 review: soften truncated OpenAPI copy, warn on
incomplete Aquifer searches (manifest + range), and drop the local
MAX_SEARCH_PAGES alias / redundant empty-page check.
Record a true merge with main (prior tip already carried the resolved
tree + #437 review fixes). Keep HEAD resolutions for translation-resources
conflicts so #431 enumeration, #432 fail-closed hydrate, and incomplete-
search warns remain intact.
@mattrace-gloo

Copy link
Copy Markdown
Contributor Author

Temporarily closing to refresh GitHub's stuck head SHA (branch tip is d14adcd; PR still showed 931f3e6).

@mattrace-buildmidwestern

Copy link
Copy Markdown

@kaseywright Thanks for the review — addressed on d14adcd (merge of latest main + review fixes; prior tip be268dc carried the tree, d14adcd is the true merge commit).

Conflicts with main

Merged origin/main @ 4bccdc1. Resolved translation-resources conflicts by keeping #431 enumeration ({ items, complete } / no 100-slice) and taking main’s #432 fail-closed manifest hydrate (mapWithConcurrency(..., 'fail')) plus #435 image caption/attribution/license wiring. Focused suites: 66 passed.

Should-address items

  1. truncated OpenAPI copy — softened to “collected”; notes that hydrate failures may still drop items (or 502 on the fail-closed manifest path) and are not covered by this flag.
  2. Range callers discarding complete — logger.warn with collection/range context in searchVerseRangeCollection and getTranslationImagesForRange (still return partial items; no new client flag).
  3. !complete on manifest path — logger.warn with resourceCollectionCode / resourceType / resourceName / book / chapters / collectedCount when marking truncated.

Nits taken

  • Removed MAX_SEARCH_PAGES alias; loop uses AQUIFER_MAX_SEARCH_PAGES.
  • Dropped redundant items.length === 0 check (subsumed by < limit).
  • Comment on count-vs-page disagreement → complete=true.

Deferred (judgement calls)

  • Skip hydration when !complete (still hydrate partial set).
  • Parallelize serial MANIFEST_SEARCH_CONFIGS loop / FA-R10 rate-limit comment.
  • Manifest-path dedupe via dedupeSearchHitsById (range path only today).

Also split over-max-lines manifest cases into translation-resources.manifest-completeness.test.ts (covers #431 + #432 + warn assertion).

@kaseywright kaseywright left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Re-reviewed at d14adcd (merge-base now 4bccdc1). Nice round — everything from my last
pass is addressed: truncated doc is honest about hydrate failures, all three
searchAllResources consumers warn on !complete with useful context, the alias/redundant
condition/provider-disagreement items are all resolved, and pulling in #433's 'fail'
policy makes manifest hydration fail-closed (502 on any getResource failure) — which
genuinely closes the "partial looks complete" hole rather than just documenting it.
Re-verified locally: 66/66 focused tests pass, tsc/eslint/prettier clean.

Two things left:

  1. New-file FA-L7 suppression. translation-resources.manifest-completeness.test.ts:25
    does vi.mock('@/db') and eslint-suppressions.json gained a fresh entry for the new
    file. The standards doc says the baselines "only ever shrink" and aren't regenerated to
    absorb a new violation. I get the bind — the sibling route-test files carry the same
    grandfathered mock (the auth chain's activeOrg read needs it) and FA-L11 forced the
    split — but a new file starting life with a suppression is exactly what that rule is
    against. Options: mock the seam differently (e.g. the middleware/service layer rather
    than @/db), or declare it as a Standards exceptions note in the PR body per the
    pre-PR checklist. Net count across files is flat (test.ts 5→4) so this is closer to
    bookkeeping than a real regression — but worth a deliberate answer.

  2. PR body is stale. Still cites base c8d4cb6 (branch now merges 4bccdc1), claims
    "61 passed" (now 66), and frames #433 as an unmerged dependency ("strict-fail remains
    #433") when it has landed and this PR builds on its 'fail' mode. A refresh would make
    the merge/rebase story clearer for the next reviewer.

Nit, for completeness: the images-range warn (service.ts:354-368) has no assertion —
only the notes-range twin is covered. Symmetric code, low risk.

Mirror the notes-range coverage for the images verse-range page-ceiling warn.

@mattrace-gloo mattrace-gloo left a comment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the re-review at d14adcd — addressed the two remaining items:

  1. FA-L7 / new-file suppression — declared under Standards exceptions in the PR body. Same auth-chain @/db mock as the sibling route tests; FA-L11 forced the split; net no-restricted-syntax count is flat (test.ts 5→4). Follow-up noted to mock the middleware/service seam and drop the suppression later.

  2. PR body refresh — merge-base 4bccdc1, focused suite 67 (was 66; +1 images-range warn), and #433 framed as landed/'fail' mode this PR builds on (not an unmerged dependency).

Nit: added the images-range warn assertion (mirrors notes-range) in 10d26f0.

@kaseywright kaseywright left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

All items resolved — approving.

Verified at 10d26f0: the images-range warn assertion covers the last gap, the FA-L7
new-file @/db mock is declared under Standards exceptions with a seam-mock follow-up,
and the PR body now reflects the real base (4bccdc1), the 67-test count, and #433 as a
landed dependency. Re-ran the focused suites: 67/67 pass.

Spec side (#431): every API-implementable acceptance criterion is met — no re-slicing,
exhaustion can't pass as complete on any consumer (flag on manifest, warns on range
routes), focused >100 + ceiling coverage, and fail-closed hydration via #433's 'fail'
mode. Remaining items are tracked follow-ups (sizes → #756/#273, Android QA → mobile #736),
so #431 stays open until the deployed-revision verification lands.

@mattrace-gloo
mattrace-gloo merged commit 29f24d1 into main Oct 22, 2026
6 checks passed
@github-actions
github-actions Bot deleted the mrace/fix/431-complete-resource-manifest-clean branch October 22, 2026 23:57
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.

Prepare Offline manifest truncates resources above 100 per configuration without client continuation

3 participants