Skip to content

feat(offline): design source-scoped offline media contracts (tr-92a) - #181

Draft
jm2 wants to merge 2 commits into
mainfrom
polecat/tr-92a
Draft

feat(offline): design source-scoped offline media contracts (tr-92a)#181
jm2 wants to merge 2 commits into
mainfrom
polecat/tr-92a

Conversation

@jm2

@jm2 jm2 commented Jul 26, 2026

Copy link
Copy Markdown
Owner

Draft — contract correction required

This PR is the design-first record for the source-scoped offline-media work in
docs/task.md, P3.1. The current exact head
998305926d7782071177d4c282f3a47fa1661d3d must not merge because its
normative contract cannot yet be implemented safely against Tributary's current
identity, HTTP, and persistence model.

Exact-head review found these blockers:

  • Cached playback is still conditioned on live session/generation authority,
    while disconnect behavior is described both as retiring and preserving the
    row. That defeats the core offline-use contract. Durable saved-source/cache
    authority must be separated from transient adapter authority.
  • An opaque remote TrackId can contain path separators, so it cannot be
    interpolated directly into a cache path.
  • The proposed resume path truncates the destination prefix and has no stable
    ETag/Last-Modified validator or If-Range contract, allowing revision
    splicing instead of a valid resume.
  • Cross-filesystem copy/sync/delete is described as atomic, and publication by
    rename occurs before integrity verification.
  • Mandatory server-advertised SHA-256 has no defined source for protocols that
    do not provide one.
  • Disconnect, refresh, predecessor retention, and Denied authority semantics
    contradict one another.
  • The proposed foreign keys target the local-only tracks table or an
    in-memory source registry; neither can represent durable remote media/jobs
    across restart.
  • Dependent work already uses a different offline_capability surface, so the
    contracts must be reconciled before releasing it.
  • Quota order contradicts itself, filesystem deletion is incorrectly described
    as atomic with a database transaction, and the proposed worker buffers an
    unbounded whole-media Vec<u8> instead of a streamed body.

The retained PR should be corrected to define an encoded/collision-safe cache
identity, validator-bound resume, verify-before-publish semantics, truthful
cross-filesystem durability, obtainable integrity evidence, a single-valued
live-versus-durable authority model, streamed bounded I/O, durable
remote-media/download-job/reconciliation tables, and truthful quota/eviction
semantics matching the actual repository and adapters.

This is documentation-only, but green compilation/CI is not evidence that a
normative design is coherent. Dependents remain blocked until a corrected exact
head passes independent source/design review.

Issue: tr-92a

@claude

claude Bot commented Jul 26, 2026

Copy link
Copy Markdown

Claude finished @jm2's task in 3m 49s —— View job


Review of PR #181 — offline media design contract

  • Gather context (read diff, files, CLAUDE.md)
  • Understand the request (code review of docs-only PR)
  • Review docs/offline-media.md for correctness, consistency, security implications
  • Review docs/task.md change
  • Post review feedback

This is a docs-only change (docs/offline-media.md, +479, new; docs/task.md, +2/-1). Since there's no runtime code, the review focused on internal consistency, accuracy of cross-references, and whether the contract is implementable as written.

What holds up well:

  • Cross-references resolve correctly: task.md#p31--offline-remote-media, #credential-boundary, and all sibling docs (source-scoped-playlists.md, subsonic-playlist-sync.md, architecture/source-lifecycle.md, lastfm-scrobbling.md, task-remediation-2026-07.md) exist with matching content.
  • The PR Compose Last.fm application owner #165 reference (Last.fm application owner) and the TRIBUTARY_* naming (matches existing usage in src/panic_reporting.rs, src/lastfm/client.rs, etc.) both check out against the actual repo state.
  • The "reversible like migration 13" claim matches m20260719_000013_source_scoped_playlist_entries.rs, which does wrap schema changes in a transaction that rolls back and preserves the original error on failure.
  • docs/task.md diff is minimal and correctly adds the offline-media.md link to the P3.1 checkbox without altering its unchecked state, consistent with the design-first delivery-boundary claims in the PR body.
  • Referenced code symbols (MediaBackend trait, ServerPlaylist, chromecast_output.rs, http_security.rs) all exist in src/.

Two internal inconsistencies filed as inline comments:

  1. Misattributed "verbatim" quote (docs/offline-media.md:101-102) — "no token in MPD/Chromecast/AirPlay ticket" is cited as a verbatim P1.4 rule, but that string doesn't appear anywhere in task-remediation-2026-07.md. The MPD/Chromecast/AirPlay credential-free ticket behavior is actually P1.6's contribution ("Stop handing backend credentials to receivers"), not P1.4's (exact-origin redirects). The same P1.4-only attribution recurs at lines 72, 264, and 416 for content that appears to be P1.6's.
  2. Undefined Denied error variant (docs/offline-media.md:128-130, repeated at :386) — offline_snapshot() is typed -> Result<Option<OfflineSnapshot>, OfflineError> and the doc says sources return Err(Denied) to refuse, but the OfflineError variant list enumerated at lines 180-184 has no Denied variant (it has a distinct LicenceDenied for a different concept). As written, this isn't constructible against the declared type.

Minor / non-blocking observation: The eviction policy in "Cancellation, quota, and eviction" (docs/offline-media.md:344-346) — "newest-first within source, oldest-first across sources" — is an unusual ordering (evicting a source's newest downloads before its oldest). It reads intentional rather than accidental, but may be worth a one-line rationale so implementers don't second-guess it.

No security, performance, or testing concerns apply — this slice deliberately introduces no schema, runtime code, or credential-handling implementation (correctly documented as such in "Status and delivery boundary"), so those review dimensions don't have surface area here yet.

@coderabbitai

coderabbitai Bot commented Jul 26, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

The PR adds a design contract for source-scoped offline media, covering identity, capabilities, downloads, storage, licensing, reconciliation, UI behavior, migration, validation, and compatibility. It also links the contract from the task checklist.

Changes

Offline media contract

Layer / File(s) Summary
Identity, capability, and download contract
docs/offline-media.md
Defines source-scoped identity, adapter capability gating, credential isolation, resumable jobs, cache layout, and atomic integrity-checked storage.
Licensing, reconciliation, and operational behavior
docs/offline-media.md
Specifies licensing states, immutable snapshot refreshes, cancellation, quota and eviction rules, UI visibility, and failure outcomes.
Migration, validation, and compatibility
docs/offline-media.md, docs/task.md
Documents the offline cache migration, validation strategy, deferred scope, compatibility behavior, and checklist reference update.

Estimated code review effort: 2 (Simple) | ~10 minutes

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarizes the main change: a design-first offline media contract for source-scoped media support.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch polecat/tr-92a

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

@codacy-production codacy-production Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Pull Request Overview

This PR establishes the normative design contract for source-scoped offline media. While the design-first approach is sound, the current documentation contains high-risk security flaws and logical inconsistencies that must be resolved before this serves as a reference for implementation. Most notably, using un-normalized TrackIds in file paths poses a path traversal risk, and the mandatory SHA-256 integrity requirement is incompatible with several target backends (Subsonic, Plex, Jellyfin).

Codacy analysis indicates the PR is not up to standards, primarily due to systemic markdown linting violations (MD004). These style issues, along with the identified design gaps in eviction and cache retirement policies, should be addressed to provide a stable foundation for the upcoming download engine.

About this PR

  • This PR is a documentation-only design contract. While this aligns with the design-first requirement, ensure that the functional implementation following this PR strictly validates these boundaries, as no automated validation of these contracts is possible within this documentation-only slice.

Test suggestions

  • Verify identity mapping uses (SourceId, TrackId) without minting new types
  • Verify resumable download job correctly handles partial bytes and range requests
  • Verify atomic storage sequence: temp write -> fsync -> rename -> integrity check
  • Verify credential boundary: ensure no tokens/passwords persist in cache metadata or logs
  • Verify redirect policy enforcement for authenticated download clients
  • Verify license revocation retires the cache row while preserving the file
  • Verify oldest-source-first eviction policy when quota is exceeded
Prompt proposal for missing tests
Consider implementing these tests if applicable:
1. Verify identity mapping uses (SourceId, TrackId) without minting new types
2. Verify resumable download job correctly handles partial bytes and range requests
3. Verify atomic storage sequence: temp write -> fsync -> rename -> integrity check
4. Verify credential boundary: ensure no tokens/passwords persist in cache metadata or logs
5. Verify redirect policy enforcement for authenticated download clients
6. Verify license revocation retires the cache row while preserving the file
7. Verify oldest-source-first eviction policy when quota is exceeded

TIP Improve review quality by adding custom instructions
TIP How was this review? Give us feedback

Comment thread docs/offline-media.md

The cache is split by exact `SourceId`, never by backend string or base URL:

- `<cache_root>/<source_id_hex>/<track_id_within_source>/`

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🔴 HIGH RISK

Using an opaque and un-normalized track_id directly in a filesystem path is a security and stability risk. Since TrackId is provided by remote sources, it could contain path traversal characters (e.g., .., /) or characters invalid on specific operating systems. Specify that the track_id must be hashed (e.g. SHA-256) before being used as a directory name on disk to ensure filesystem safety and cross-platform compatibility.

Comment thread docs/offline-media.md
| Plex | `GET /library/parts/<partId>` authenticated through the exact-origin proxy; uses `X-Plex-Token` only inside the proxy boundary. | Identical. | Same. |
| DAAP | `DAAP.song` request, authenticated through the DAAP protocol-specific lane already retired to the source lifecycle. | Identical. | DAAP connection still has exactly-once logout; cache rows must retire on disconnect. |
| Radio-Browser | Disallowed. | — | Streams are public and not licensable for offline by default; deny hard. |
| Built-in local | Disallowed. | — | Local files are already local; the cache is the filesystem. |

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🟡 MEDIUM RISK

There is a contradiction in the design regarding the lifecycle of DAAP cache rows. This line states DAAP cache rows must retire on disconnect, but line 282 states that session revocation retires only the in-flight lease and NOT the cache row. These requirements should be reconciled.

Comment thread docs/offline-media.md
the job must verify the rename is single-step or fall back to copy + sync
+ delete, never a partial overwrite.
5. **Post-write integrity check.** The job's `current_sha256` is re-computed
from the bytes on disk; only equality with the server-advertised digest

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🟡 MEDIUM RISK

Most target backends (Subsonic, Jellyfin, Plex) do not typically provide SHA-256 digests for media streams in their HTTP responses. If the contract mandates a match against a 'server-advertised digest' as a terminal requirement for playability, these sources will be unable to satisfy the contract. Update the 'Integrity' and 'Atomic storage' sections to allow for optional integrity verification (e.g. falling back to size-only check) when a SHA-256 digest is not advertised.

Comment thread docs/offline-media.md

## See also

- [`task.md`](task.md) — P3.1 implementation record and overall backlog.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚪ LOW RISK

Use the '+' marker for this list item to maintain consistency with the project's markdownlint configuration.

See Issue in Codacy

Comment thread docs/offline-media.md

Failure at any step:

- Temp reservation: the previous temp is unlinked, no cache row created.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚪ LOW RISK

Update the list marker to use '+' instead of '-' to satisfy the project's markdownlint (MD004) configuration. Example:

Suggested change
- Temp reservation: the previous temp is unlinked, no cache row created.
+ Temp reservation: the previous temp is unlinked, no cache row created.

See Issue in Codacy

@codacy-production

Copy link
Copy Markdown

Not up to standards ⛔

🔴 Issues 11 minor

Alerts:
⚠ 11 issues (≤ 0 issues of at least minor severity)

Results:
11 new issues

Category Results
CodeStyle 11 minor

View in Codacy

AI Reviewer: first review requested successfully. AI can make mistakes. Always validate suggestions.

Run reviewer

TIP This summary will be updated as you push new changes.

Comment thread docs/offline-media.md
Comment on lines +99 to +102
The credential-boundary section is normative and may not be weakened by an
implementation slice. Any slice that would persist a credential to make a
download work is a bug. This matches `task-remediation-2026-07.md` P1.4's
"no token in MPD/Chromecast/AirPlay ticket" rule verbatim.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Citation doesn't check out. The quoted phrase "no token in MPD/Chromecast/AirPlay ticket" is attributed to task-remediation-2026-07.md P1.4 "verbatim," but that exact string doesn't appear anywhere in task-remediation-2026-07.md. Looking at that file, P1.4 ("Enforce exact-origin authenticated redirects") covers redirect-matrix/Referer/HTTPS-downgrade behavior; the credential-free MPD/Chromecast/AirPlay ticket behavior this line describes is actually documented under P1.6 ("Stop handing backend credentials to receivers", task-remediation-2026-07.md:633).

Same misattribution recurs elsewhere in this doc — lines 72, 264, and 416 all cite "P1.4" for behavior (credential-free GTK rows, receiver ticket boundary) that P1.6 actually established. Worth either fixing the citations to point at P1.6 where appropriate, or dropping the "verbatim" framing since no such literal sentence exists in the cited file.

[Fix this →](https://claude.ai/code?q=In%20docs%2Foffline-media.md%2C%20the%20phrase%20%22no%20token%20in%20MPD%2FChromecast%2FAirPlay%20ticket%22%20at%20line%20101-102%20is%20quoted%20as%20verbatim%20from%20task-remediation-2026-07.md%20P1.4%2C%20but%20that%20string%20doesn%27t%20exist%20in%20that%20file%20-%20the%20MPD%2FChromecast%2FAirPlay%20credential-free%20ticket%20behavior%20is%20actually%20documented%20under%20P1.6%20(%22Stop%20handing%20backend%20credentials%20to%20receivers%22%2C%20around%20line%20633).%20Fix%20this%20citation%20and%20check%20other%20P1.4%20citations%20at%20lines%2072%2C%20264%2C%20and%20416%20for%20the%20same%20misattribution%20-%20some%20of%20that%20content%20belongs%20to%20P1.6%2C%20not%20P1.4.&repo=jm2/tributary

Comment thread docs/offline-media.md
Comment on lines +127 to +130
1. The registry's `MediaBackend` trait is extended with a default-deny
`offline_snapshot() -> Result<Option<OfflineSnapshot>, OfflineError>`
adapter. `None` and `Err(Denied)` are distinct; `None` means the source has
not declared, `Denied` means it explicitly refuses.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Denied isn't a defined OfflineError variant. This adapter is typed as offline_snapshot() -> Result<Option<OfflineSnapshot>, OfflineError>, and the text says a source returns Err(Denied) to explicitly refuse. But the OfflineError enum is later enumerated (lines 180–184) as exactly Network, AuthExpired, LeaseRevoked, IntegrityMismatch, LicenceDenied, QuotaExceeded, StorageUnavailable, UnsupportedSource — no Denied variant appears. The failure-modes table at line 386 repeats Err(Denied) the same way.

As written, an implementer following this contract literally can't construct Err(Denied) against the declared OfflineError type. Either Denied needs to be added to the enumerated variant list, or the two Err(Denied) call sites should be reconciled with an existing variant (e.g. is this meant to be distinct from LicenceDenied, which is a separate concept in the Licensing section?).

[Fix this →](https://claude.ai/code?q=In%20docs%2Foffline-media.md%2C%20lines%20128-130%20and%20386%20reference%20%60Err(Denied)%60%20as%20returned%20from%20%60offline_snapshot()%20-%3E%20Result%3COption%3COfflineSnapshot%3E%2C%20OfflineError%3E%60%2C%20but%20the%20%60OfflineError%60%20enum%20variant%20list%20at%20lines%20180-184%20(Network%2C%20AuthExpired%2C%20LeaseRevoked%2C%20IntegrityMismatch%2C%20LicenceDenied%2C%20QuotaExceeded%2C%20StorageUnavailable%2C%20UnsupportedSource)%20has%20no%20%60Denied%60%20variant.%20Reconcile%20this%20by%20either%20adding%20%60Denied%60%20to%20the%20enumerated%20list%20or%20clarifying%20its%20relationship%20to%20the%20existing%20%60LicenceDenied%60%20variant.&repo=jm2/tributary

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 10

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@docs/offline-media.md`:
- Around line 207-213: Update the cross-filesystem fallback described in the
“Atomic rename” step to copy into a temporary file on the destination
filesystem, fsync that temporary file, atomically rename it to the final cache
path, and delete the source only after the destination rename succeeds. Preserve
the existing single-step rename path and prevent direct writes to the final
destination.
- Around line 457-460: Update the offline subsystem retirement guidance to
define how on-disk cache files are handled before dropping the offline tables,
indexes, and triggers: specify that cache files are unlinked or reconciled while
their metadata remains available, or explicitly document intentional retention
and ownership. Keep the migration’s single-transaction behavior clear.
- Around line 227-230: The cache refresh flow must keep the predecessor snapshot
available until the replacement is fully committed, rather than exposing only
the live endpoint during steps 1–5. Update the lookup and reconciliation rules
described in the offline-media contract so valid offline items continue
resolving through the previous snapshot until step 5 succeeds, while preserving
the prohibition on exposing partial or half-promoted cache rows.
- Around line 451-455: Update the offline compatibility description near the
OfflineSnapshot::None discussion: replace the byte-level database identity claim
with the invariant that databases have the same logical application data and
contain no offline cache rows. Keep the statements about runtime machinery and
opt-out semantics aligned with this logical-data definition.
- Around line 119-120: Update the offline media documentation’s TrackId path
layout to define a collision-safe, path-safe filesystem encoding for opaque IDs,
such as an explicit reversible encoding, while retaining the original TrackId
only as metadata. Clarify that the download engine must not place the raw ID
directly in paths or parse, normalize, or hash it, and apply the same definition
to the additional TrackId layout described later in the document.
- Line 252: Resolve the conflicting DAAP disconnect lifecycle by choosing a
single rule for committed cache rows versus in-flight leases, then apply it
consistently across the capability matrix, credential/session revocation
section, logout behavior, failure table, and related tests in
docs/offline-media.md. Update the DAAP-specific statements and assertions so
disconnect and revocation semantics match the selected rule.
- Around line 173-176: The offline media documentation must distinguish
fresh-download and resume file-opening modes: preserve the committed prefix
during resumption without truncating or reopening in append mode, validate the
existing temporary-file prefix byte-for-byte, and seek to the committed offset
before writing the next range. Update both the resumption section and the
corresponding later guidance around the storage contract.
- Around line 342-346: Align the normative eviction policy and the
validation/regression expectation to one ordering: oldest-cache-first across
sources, with newest-first eviction within each source. Update the contradictory
newest-source-first wording wherever it appears, including the additional
referenced occurrence, without changing the documented quota behavior.
- Around line 26-29: Clarify the documentation’s contract for Denied entries:
state explicitly whether Denied is only an ephemeral admission/job state or
whether it creates a stored catalogue row, and make the central rule and
licence-table wording consistent with that decision. Update the related
references to Denied so they describe the same persistence and playability
behavior.
- Around line 214-216: Update the “Post-write integrity check” section to
identify the exact trusted digest source for each adapter, such as the relevant
response header or manifest field, and state that downloads are rejected when
the server-provided digest is missing. Ensure current_sha256 is compared only
with that explicitly trusted value.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: deaf82cb-4765-45a4-a749-faf688bcf168

📥 Commits

Reviewing files that changed from the base of the PR and between 6afb427 and 126125c.

📒 Files selected for processing (2)
  • docs/offline-media.md
  • docs/task.md

Comment thread docs/offline-media.md
Comment thread docs/offline-media.md
Comment on lines +119 to +120
3. A track ID is opaque and bounded exactly as it is in `task.md:78-83`. The
download engine does not parse, normalise, or hash a `TrackId`.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🔒 Security & Privacy | 🟠 Major | 🏗️ Heavy lift

Define a path-safe encoding for TrackId.

TrackId is opaque and must not be parsed, normalized, or hashed, but the layout places it in a filesystem path. IDs containing separators, traversal components, or filesystem-invalid characters could escape the source directory or fail on disk. Define a collision-safe filesystem representation and retain the original ID only as metadata.

Also applies to: 234-240

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@docs/offline-media.md` around lines 119 - 120, Update the offline media
documentation’s TrackId path layout to define a collision-safe, path-safe
filesystem encoding for opaque IDs, such as an explicit reversible encoding,
while retaining the original TrackId only as metadata. Clarify that the download
engine must not place the raw ID directly in paths or parse, normalize, or hash
it, and apply the same definition to the additional TrackId layout described
later in the document.

Comment thread docs/offline-media.md
Comment on lines +173 to +176
2. **Resumption is exact and bounded.** A retried range request uses the
`Range` bytes the previous job last committed. Out-of-order or duplicate
ranges are rejected; ranges past `Content-Length` are rejected; the previous
temporary file is reused only after byte-level equality check.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🗄️ Data Integrity & Integration | 🟠 Major | 🏗️ Heavy lift

Do not truncate the temp file when resuming.

The job contract reuses the previously committed prefix, but the storage contract mandates truncate(true) and forbids reopening for append. Following the latter destroys the prefix and invalidates current_bytes. Specify separate fresh-download and resume open modes, including prefix validation and seeking before the next range.

Also applies to: 203-206

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@docs/offline-media.md` around lines 173 - 176, The offline media
documentation must distinguish fresh-download and resume file-opening modes:
preserve the committed prefix during resumption without truncating or reopening
in append mode, validate the existing temporary-file prefix byte-for-byte, and
seek to the committed offset before writing the next range. Update both the
resumption section and the corresponding later guidance around the storage
contract.

Comment thread docs/offline-media.md
Comment thread docs/offline-media.md
Comment on lines +214 to +216
5. **Post-write integrity check.** The job's `current_sha256` is re-computed
from the bytes on disk; only equality with the server-advertised digest
enables the next step.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Inspect the relevant doc section and related digest terminology.
git ls-files docs/offline-media.md
echo "----"
cat -n docs/offline-media.md | sed -n '180,250p'
echo "---- SEARCH current_sha256 / digest terms ----"
rg -n "current_sha256|server-advertised digest|digest|sha256|checksum|manifest|header" docs offline-media.md . -g '!**/node_modules/**'

Repository: jm2/tributary

Length of output: 50370


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Narrow search to likely source/docs files for the digest contract.
rg -n "current_sha256|server-advertised digest|digest|sha256|checksum|manifest|header" . \
  -g '!**/node_modules/**' -g '!**/dist/**' -g '!**/build/**' -g '!**/.git/**'

Repository: jm2/tributary

Length of output: 50370


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# If the document references adapters or download flows, inspect nearby sections too.
rg -n "adapter|download|integrity check|current_sha256|digest" docs/offline-media.md

Repository: jm2/tributary

Length of output: 5249


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Map the doc structure first if the file is small enough.
wc -l docs/offline-media.md
ast-grep outline docs/offline-media.md --view expanded

Repository: jm2/tributary

Length of output: 213


🏁 Script executed:

#!/bin/bash
set -euo pipefail
wc -l docs/offline-media.md
cat -n docs/offline-media.md | sed -n '180,250p'
rg -n "current_sha256|server-advertised digest|digest|sha256|checksum|manifest|header|adapter|download|integrity check" docs . -g '!**/node_modules/**' -g '!**/.git/**' -g '!**/dist/**' -g '!**/build/**'

Repository: jm2/tributary

Length of output: 50370


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Read the precise sections that define the download contract and adapter matrix.
cat -n docs/offline-media.md | sed -n '60,90p'
echo "----"
cat -n docs/offline-media.md | sed -n '149,170p'
echo "----"
cat -n docs/offline-media.md | sed -n '214,260p'
echo "----"
cat -n docs/offline-media.md | sed -n '372,405p'

Repository: jm2/tributary

Length of output: 9174


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Find any explicit missing-digest handling in the offline-media spec.
rg -n "missing|absent|without one|reject downloads|digest source|trusted digest|advertised digest|current_sha256" docs/offline-media.md README.md CHANGELOG.md docs/task.md docs/task-remediation-2026-07.md

Repository: jm2/tributary

Length of output: 43895


Define the digest source and missing-digest behavior. current_sha256 still compares against a “server-advertised digest,” but this contract never names the header/manifest field or says whether missing digests fail closed. Spell out the trusted field per adapter, or reject downloads without one. docs/offline-media.md:214-216

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@docs/offline-media.md` around lines 214 - 216, Update the “Post-write
integrity check” section to identify the exact trusted digest source for each
adapter, such as the relevant response header or manifest field, and state that
downloads are rejected when the server-provided digest is missing. Ensure
current_sha256 is compared only with that explicitly trusted value.

Comment thread docs/offline-media.md
Comment on lines +227 to +230
A half-promoted cache row that points at a missing or partial file is a bug
that the contract forbids; downstream layers must never observe it. The
`tracks` row remains untouched until step 5 succeeds, and the lookup path
between step 1 and step 5 returns the live endpoint only.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Keep the predecessor available until refresh commits.

The lookup rule returns only the live endpoint during steps 1–5, while reconciliation requires the previous snapshot to remain available until the replacement is committed. A refresh can therefore make a valid offline item unavailable before the new snapshot is ready.

Also applies to: 322-324

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@docs/offline-media.md` around lines 227 - 230, The cache refresh flow must
keep the predecessor snapshot available until the replacement is fully
committed, rather than exposing only the live endpoint during steps 1–5. Update
the lookup and reconciliation rules described in the offline-media contract so
valid offline items continue resolving through the previous snapshot until step
5 succeeds, while preserving the prohibition on exposing partial or
half-promoted cache rows.

Comment thread docs/offline-media.md
| Subsonic | `GET .../download?view=...&id=<trackId>` authenticated through the exact-origin proxy. | Per-source byte total bounded at the source-adapter-declared cap; offline rows are still capped by the per-track quota. | Bearer URL handling per `task-remediation-2026-07.md` P1.6 — only the proxy ticket ever reaches GTK. |
| Jellyfin | `GET /Items/<id>/Download` authenticated through the exact-origin proxy. | Identical. | Same. |
| Plex | `GET /library/parts/<partId>` authenticated through the exact-origin proxy; uses `X-Plex-Token` only inside the proxy boundary. | Identical. | Same. |
| DAAP | `DAAP.song` request, authenticated through the DAAP protocol-specific lane already retired to the source lifecycle. | Identical. | DAAP connection still has exactly-once logout; cache rows must retire on disconnect. |

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🗄️ Data Integrity & Integration | 🟠 Major | 🏗️ Heavy lift

Resolve DAAP disconnect semantics.

The capability matrix says cache rows retire on DAAP disconnect, while the credential section says session revocation retires only the in-flight lease and not the committed cache row. Choose one lifecycle rule and align the matrix, logout behavior, failure table, and tests.

Also applies to: 281-283

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@docs/offline-media.md` at line 252, Resolve the conflicting DAAP disconnect
lifecycle by choosing a single rule for committed cache rows versus in-flight
leases, then apply it consistently across the capability matrix,
credential/session revocation section, logout behavior, failure table, and
related tests in docs/offline-media.md. Update the DAAP-specific statements and
assertions so disconnect and revocation semantics match the selected rule.

Comment thread docs/offline-media.md
Comment on lines +342 to +346
1. **Quota is global.** The application has one offline quota expressed in
bytes. Sub-limits per source are advisory only at admission time.
2. **Eviction is newest-first within source, oldest-first across sources.**
When the quota is exceeded, eviction walks sources in oldest-cache-first
order and within a source newest-first.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Use one eviction ordering.

The policy specifies oldest-cache-first across sources, but the validation table says newest-source-first eviction. These produce different deletion results; make the normative policy and regression expectation identical.

Also applies to: 421-421

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@docs/offline-media.md` around lines 342 - 346, Align the normative eviction
policy and the validation/regression expectation to one ordering:
oldest-cache-first across sources, with newest-first eviction within each
source. Update the contradictory newest-source-first wording wherever it
appears, including the additional referenced occurrence, without changing the
documented quota behavior.

Comment thread docs/offline-media.md
Comment on lines +451 to +455
Until an offline-capable source opts in for the first time, none of the offline
machinery is exercised at runtime. A database that has never had an offline
cache row is identical at the byte level to a database without the migration.
A source that opts out keeps the same `OfflineSnapshot::None` semantics that
the new adapter implies.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🗄️ Data Integrity & Integration | 🟡 Minor | ⚡ Quick win

Replace the byte-level compatibility claim.

Adding an offline table necessarily changes database bytes even when it is empty. The invariant should be “same logical application data and no offline rows,” otherwise this statement cannot be true or tested.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@docs/offline-media.md` around lines 451 - 455, Update the offline
compatibility description near the OfflineSnapshot::None discussion: replace the
byte-level database identity claim with the invariant that databases have the
same logical application data and contain no offline cache rows. Keep the
statements about runtime machinery and opt-out semantics aligned with this
logical-data definition.

Comment thread docs/offline-media.md
Comment on lines +457 to +460
When the project eventually retires the offline subsystem, the migration is
reversed by a follower migration that drops the offline tables, indexes, and
triggers in one transaction; no live production path depends on the offline
machinery existing.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🗄️ Data Integrity & Integration | 🟠 Major | 🏗️ Heavy lift

Define cache-file cleanup before dropping offline tables.

Dropping the metadata tables without unlinking or reconciling on-disk cache files leaves unreachable media consuming storage. Specify cleanup ordering, or explicitly document intentional retention and ownership.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@docs/offline-media.md` around lines 457 - 460, Update the offline subsystem
retirement guidance to define how on-disk cache files are handled before
dropping the offline tables, indexes, and triggers: specify that cache files are
unlinked or reconciled while their metadata remains available, or explicitly
document intentional retention and ownership. Keep the migration’s
single-transaction behavior clear.

jm2 and others added 2 commits July 27, 2026 12:12
Adds the design-first contract required by task.md:21-22 before the P3.1
issue #11 implementation lands. The document fixes the persistent
source-scoped identity shape, the authenticated/resumable download job
model, atomic temp-then-rename storage, the per-source capability matrix,
the credential boundary (matching P1.4 exact-origin / no-Referer policy
and the 2026-07-13 authenticated redirect policy), the licensing row
types, and the reconciliation rules. It deliberately introduces no
schema, migration, runtime worker, GTK widget, or HTTP client; those
follow implementation slices after this contract is accepted.

The design reuses the source-scoped (SourceId, TrackId) identity shape
already established in architecture/source-lifecycle.md and
source-scoped-playlists.md, so the offline cache does not introduce a
parallel identity kind. The download engine is not a SourceKind; it is a
state change for an existing source-owned media row.

Links the design document into the P3.1 checkbox in task.md and keeps
that checkbox unchecked — the design is a fleet-completable artifact but
the implementation record remains open until the download/cache engine
and storage UI land.
…view)

Review-blocking fixes for tr-92a:

- docs/offline-media.md:170-172: drop mol-polecat-work application-owner
  reference; point at the Last.fm application owner composed in #165 as the
  reference shape for the offline-job supervisor.
- docs/offline-media.md:285-287: replace GC_* build-time prefix with
  TRIBUTARY_* (the real project prefix used by src/lastfm/client.rs for
  TRIBUTARY_LASTFM_API_KEY / TRIBUTARY_SHARED_SECRET).
- Qualify every P1.4/P1.5/P1.6/P2.3 anchor to task-remediation-2026-07.md
  (these resolved against task.md which has different P-codes for the same
  labels) and add task-remediation-2026-07.md to the See also list with the
  P-code map.
- Fix the 'Path 2.3' typo at line 326 to 'P2.3'.

The contract itself is unchanged; only surface vocabulary and citation
disambiguation. Docs-only diff; no Rust changes.
@jm2
jm2 force-pushed the polecat/tr-92a branch from 126125c to 9983059 Compare July 27, 2026 16:12

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@docs/offline-media.md`:
- Around line 127-135: Resolve the offline_snapshot() unsupported-source
contract consistently: choose either None or Err(Denied) for Radio-Browser and
other disallowed adapters, then update the adapter rules, failure table, and
related tests to match that choice while preserving the documented distinction
between undeclared and explicitly refused capabilities.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 329df48b-ce1a-4281-bcff-be482626a0a3

📥 Commits

Reviewing files that changed from the base of the PR and between 126125c and 9983059.

📒 Files selected for processing (2)
  • docs/offline-media.md
  • docs/task.md
🚧 Files skipped from review as they are similar to previous changes (1)
  • docs/task.md

Comment thread docs/offline-media.md
Comment on lines +127 to +135
1. The registry's `MediaBackend` trait is extended with a default-deny
`offline_snapshot() -> Result<Option<OfflineSnapshot>, OfflineError>`
adapter. `None` and `Err(Denied)` are distinct; `None` means the source has
not declared, `Denied` means it explicitly refuses.
2. Only the same authenticated-backends that opt into live `ServerPlaylist`
reads (Subsonic, Jellyfin, Plex, DAAP) may opt in here. Radio-Browser,
removable, external-file, and the built-in local source must return
`None`. A local file is already local; a removable volume is lifecycle-bound
but not credentialed; an external file is one-shot.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Align unsupported-source return semantics.

The capability contract requires Radio-Browser and other disallowed adapters to return None, but the failure table says Radio-Browser returns Err(Denied). Since the document explicitly distinguishes these outcomes, choose one behavior and update the adapter rules, failure table, and tests consistently.

Also applies to: 386-387

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@docs/offline-media.md` around lines 127 - 135, Resolve the offline_snapshot()
unsupported-source contract consistently: choose either None or Err(Denied) for
Radio-Browser and other disallowed adapters, then update the adapter rules,
failure table, and related tests to match that choice while preserving the
documented distinction between undeclared and explicitly refused capabilities.

@claude

claude Bot commented Jul 27, 2026

Copy link
Copy Markdown

Claude finished @jm2's task in 3m 13s —— View job


Review of PR #181 — re-review after commit 9983059

  • Gather context (diff since last review, CLAUDE.md)
  • Check whether prior review findings were actually resolved by commit 9983059
  • Review the newest diff (958723a...9983059) for new issues
  • Post final review feedback

What commit 9983059 actually fixed (matches the PR body's "Resumed from rejection" note): dropped the mol-polecat-work reference in favor of the real Last.fm application-owner PR (#165), replaced GC_* with TRIBUTARY_*, added a filename qualifier (task-remediation-2026-07.md) to every bare P-code citation, added that file to "See also," and fixed the "Path 2.3" → "P2.3" typo. This is a narrow, correctly-scoped fix for exactly what it claims.

Two prior findings are marked "✅ Addressed" by CodeRabbit but are not actually fixed. I checked: the Denied-persistence contradiction (lines 26-29, 295-299) and the cross-filesystem atomic-rename fallback (lines 210-213) are byte-identical to the versions CodeRabbit originally flagged. CodeRabbit's tracker cites the range "commits 5f9dd50 to 9983059" — I inspected 5f9dd50 directly and it's an unrelated commit (test: await Last.fm failure publication (tr-d3f), touching only src/lastfm/production.rs on main) that never touches docs/offline-media.md. The "addressed" annotation appears to be tracking the base branch moving forward between review passes, not a real fix in this PR — left inline comments re-opening both.

The P1.4/P1.6 misattribution I raised last time is only half-fixed. Line 101-102 now cites task-remediation-2026-07.md P1.4 for the "no token in MPD/Chromecast/AirPlay ticket" claim — same milestone number as before, just with the filename added. I read the source file directly this time: task-remediation-2026-07.md:652 (P1.6, "Output boundary") is what actually establishes credential-free tickets for MPD/Chromecast/AirPlay/local GStreamer; P1.4 (:606-622) is the exact-origin/redirect policy and doesn't mention MPD or Chromecast at all. Left an inline comment with the citation.

Everything else CodeRabbit and Codacy already flagged remains open (this commit didn't touch it, and the PR body doesn't claim otherwise): Err(Denied) isn't a defined OfflineError variant (128-130, 386); Radio-Browser's None vs. Err(Denied) contradiction (133-135 vs. 386, re-flagged by CodeRabbit's own 2026-07-27 pass on this same commit); the temp-file truncate-on-resume contradiction (173-176 vs. 203-206); undefined digest source for integrity checks (214-216); the DAAP disconnect contradiction (252 vs. 281-283, cache row retires on disconnect and survives revocation); the eviction-ordering contradiction between the normative policy and the validation table (344-346 vs. 421); the byte-level database-identity claim (451-455); and TrackId-in-path-safety (119-120, 234-240). None of these are new findings — flagging so the open count is accurate against what's actually in the diff versus what the bot trackers claim.

Docs-only change; no runtime/security/test surface beyond what's already covered above.

Comment thread docs/offline-media.md
Comment on lines +101 to +102
download work is a bug. This matches `task-remediation-2026-07.md` P1.4's
"no token in MPD/Chromecast/AirPlay ticket" rule verbatim.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Still misattributed to P1.4 — this is P1.6's territory. Commit 9983059 added the task-remediation-2026-07.md filename qualifier here but didn't correct the milestone number. I checked the cited file directly: task-remediation-2026-07.md:652 (P1.6, "Output boundary" row) states "Chromecast, MPD, local GStreamer, and AirPlay exchange it for their existing opaque, receiver-reachable tickets; none can fall back to the clean endpoint or serialized credential state." — that's the actual source of the "no token in MPD/Chromecast/AirPlay ticket" claim. P1.4 (task-remediation-2026-07.md:606-622) is the exact-origin/redirect policy; it does touch local/AirPlay routing, but the credential-free-ticket guarantee for all four receivers (including MPD/Chromecast, which P1.4 doesn't mention at all) is P1.6's contribution.

This is the same issue flagged in the original review (also recurring at lines 264 and 416) — worth fixing for real this time rather than just adding the filename.

[Fix this →](https://claude.ai/code?q=In%20docs%2Foffline-media.md%2C%20line%20101-102%20still%20attributes%20the%20%22no%20token%20in%20MPD%2FChromecast%2FAirPlay%20ticket%22%20rule%20to%20P1.4%2C%20but%20per%20task-remediation-2026-07.md%3A652%20(P1.6%2C%20%22Output%20boundary%22%20row)%20that%20guarantee%20for%20MPD%2FChromecast%2FAirPlay%20receiver%20tickets%20is%20P1.6%27s%2C%20not%20P1.4%27s.%20Change%20the%20citation%20to%20P1.6%20here%2C%20and%20check%20the%20same%20pattern%20at%20lines%20264%20and%20416%20which%20may%20need%20the%20same%20correction.&repo=jm2/tributary

Comment thread docs/offline-media.md
Comment on lines +295 to +297
| OperationalLicence | Meaning | Visible to GTK | Persistent |
|---|---|---|---|
| `Denied` | Default. The cached row exists structurally but cannot become playable. | "Offline unavailable" for that source. | No |

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

CodeRabbit's tracker marked this "✅ Addressed in commits 5f9dd50 to 9983059," but the text is unchangedDenied still says "The cached row exists structurally" while Persistent is marked No in the same row, and the central-rules text at lines 26-29 is byte-identical to the prior review. I checked commit 5f9dd50 directly: it's test: await Last.fm failure publication (tr-d3f), touching only src/lastfm/production.rs on main — it never touches this file. The "addressed" marker looks like it's tracking the base branch moving forward between reviews, not an actual fix in this PR. This finding (and the atomic-rename one at lines 210-213, marked the same way) is still open.

[Fix this →](https://claude.ai/code?q=In%20docs%2Foffline-media.md%2C%20lines%2026-29%20and%20295-299%2C%20the%20Denied%20licence%20state%20is%20described%20as%20a%20%22cached%20row%20exists%20structurally%22%20while%20the%20table%20marks%20Persistent%3ANo%20for%20the%20same%20state%20-%20this%20contradiction%20was%20flagged%20by%20CodeRabbit%20and%20incorrectly%20marked%20addressed%20(the%20cited%20fix%20commit%205f9dd50%20doesn%27t%20touch%20this%20file).%20Clarify%20whether%20Denied%20is%20an%20ephemeral%20admission-time%20state%20or%20a%20stored%20non-playable%20row%2C%20and%20make%20both%20passages%20consistent.&repo=jm2/tributary

@jm2
jm2 marked this pull request as draft July 27, 2026 17:24
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.

1 participant