Commit Graph
13 Commits
Author SHA1 Message Date
dtourolle 6c0c80f20b feat(deb): package the server, and ask the questions that fail silently
CI / fmt, clippy, test (pull_request) Successful in 1m48s
CI / advisories and licences (pull_request) Successful in 42s
CI / static musl binary (pull_request) Successful in 1m46s
CI / debian package (pull_request) Failing after 3h10m28s
DR-015, DR-016. §8 already shipped a static binary and an optional
container; this adds the third form, and it packages the SAME binary the
musl job proved static rather than building its own. Two builds of the
same commit could diverge, and the whole point of that assertion is that
the artifact an operator installs is the one that was checked.

Built with dpkg-deb from an explicit staging tree rather than cargo-deb.
debconf's `config` script and `templates` live in the control archive
next to the maintainer scripts, and controlling that archive directly
beats discovering what a wrapper will copy into it. dpkg-dev is on every
Debian builder, so this adds no build dependency.

Why debconf at all: two settings fail SILENTLY when unset. Without
JRAY_TMDB_API_KEY every upload stays `pending` and is never listed;
without JRAY_TRUSTED_PROXIES the X-Forwarded-For header is ignored, so
every client shares one rate-limit bucket and every abuse report points
at the proxy. Both leave a server that works and is quietly doing the
wrong thing — the worst thing to leave to a README nobody reads.

Three properties, each a way packaging usually goes wrong:

The generated config is NOT a dpkg conffile. It is written from the
debconf answers, so shipping it as one would make dpkg prompt on every
upgrade about changes the package itself had made.

Hand edits survive. postinst rewrites only the keys debconf manages;
comments, ordering and any other setting are left alone.

A blank key on reconfigure keeps the existing one. Otherwise pressing
Enter through a dpkg-reconfigure would unpublish every future upload.

The seeding guard is worth its comment, because the obvious version is
wrong twice over. `config` seeds unanswered questions from the env file
so a reconfigure shows what is actually in force. Seeding
unconditionally overwrites a preseed — debconf-set-selections marks what
it sets as seen — so every unattended install would quietly reconfigure
itself back to whatever was on disk. Guarding on an empty value does not
work either: server-id and bind carry template Defaults, so db_get
returns "localhost" for a question nobody answered. The test is the
`seen` flag, which is the actual question being asked.

Purge keeps the database, knowingly departing from the expectation that
purge removes everything. Manifests are the output of real CV compute on
media the operator may no longer have, and §8 says federation is
explicitly not a backup. Destroying that during an `apt purge` is not a
trade worth making for tidiness; postrm names the path instead.

The nginx example is documentation, not installed configuration. The
proxy usually runs on a different host from the server, so a file
dropped into this machine's nginx would be in the wrong place — and §8
leaves the edge to the operator deliberately.

Verified by running it, not by reading it: a full lifecycle in a
bookworm container — build, preseeded install, mode-600 env file, key
absent from debconf's database afterwards, `systemd-analyze verify` on
the unit, the installed binary answering /health and /ready, reconfigure
preserving both the key and an unmanaged setting, and purge leaving the
database. It failed on the seeding bug above the first time, which is
why that guard exists. CI runs the same checks against every build.

TRACES: DR-015, DR-016 | PR-004
2026-09-06 09:58:47 +02:00
dtourolle b4cf0c0fbf Merge PR #2: licence the code GPLv3 and contributed manifests CC0
CI / fmt, clippy, test (push) Successful in 1m17s
CI / advisories and licences (push) Successful in 32s
CI / static musl binary (push) Successful in 1m36s
2026-09-05 11:51:03 +00:00
dtourolle 8d9aafb4d4 Merge PR #1: drop a redundant borrow clippy now rejects
CI / fmt, clippy, test (push) Successful in 1m21s
CI / advisories and licences (push) Successful in 40s
CI / static musl binary (push) Successful in 2m16s
2026-09-05 11:50:23 +00:00
dtourolleandClaude Opus 5 830b4b452f fix: drop a redundant borrow clippy now rejects
CI / fmt, clippy, test (pull_request) Successful in 5m29s
CI / advisories and licences (pull_request) Successful in 36s
CI / static musl binary (pull_request) Successful in 7m14s
error: redundant reference in `assert_eq!` argument
      --> tests/injection.rs:633:13
      = note: `-D clippy::useless-borrows-in-formatting` implied by `-D warnings`

No code changed to cause this. ci.yml installs whatever rustup calls
stable and the tree pins no toolchain, so the gate moved under the
repository: `useless_borrows_in_formatting` fires on a line that has been
there since the injection tests were written, and `RUSTFLAGS: -D
warnings` turns it into a hard failure.

The borrow was never doing anything -- `{:?}` formats a String and a
&String identically -- so this is the lint being right, not a workaround
for it.

Verified against the toolchain CI actually resolved to (1.97.1, not the
1.85 in Cargo.toml's rust-version), all four gates the workflow runs:
fmt clean, clippy --all-targets --all-features clean with no other lint,
208 tests passing, and cargo deny reporting advisories/bans/licenses/
sources ok.

Worth separating from this fix: the failure was invisible until it
happened because the toolchain floats. Nothing here pins it, so the next
stable release can turn this repository red without a commit. A
rust-toolchain.toml would close that, at the cost of not seeing new lints
until it is bumped -- a policy call, not a bug fix, so it is not in here.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-03 20:53:49 +02:00
dtourolle 7eb5c175af docs: tag the untagged verifications, and record the UR-007 conflict
CI / fmt, clippy, test (pull_request) Failing after 54s
CI / static musl binary (pull_request) Skipped
CI / advisories and licences (pull_request) Successful in 39s
Five tests and the schema constant implemented requirements without
carrying a tag, so those requirements read as uncovered when they were
not. No behaviour changes here — every edit is a comment.

Also documents `static` as a real tier rather than an exemption: it is
already in `ci_executable_tiers` and its checks run in CI, and it exists
because DR-007's single binary and DR-012's licence policy are properties
of the build that a unit test could only assert as theatre.

The UR-007 note records a cross-repo status conflict rather than
resolving it. `jRay` JR-025 is `Done` and claims UR-007, but the fetch
path that would exercise it (`jRay` JR-031) is still `Planned`. Either
JR-025 is scoped to selection alone or it over-claims; until that is
settled neither register should be trusted for UR-007 coverage.

The gate reports 0 orphan tags and 19/19 UR coverage.

TRACES: DR-001, DR-014, UR-015, UR-016, UR-018 | SR-003, SR-004
2026-07-31 16:26:10 +02:00
dtourolleandClaude Opus 5 4afa36e7a2 feat(licence): contributed manifests are CC0 1.0
Contributed manifests were in no declared condition at all, which left §9a
replication with no grant flowing through it: peers mirror each other's
catalogues wholesale, and every hop of that was unlicensed.

CC0 rather than a share-alike licence, because a share-alike works by asserting
a right in the data and then conditioning its use. The position in
docs/legal-posture.md §3 is that presence timings are facts rather than
protectable expression — asserting copyright in them in order to license them
would contradict that argument in the same repository, and that contradiction is
worth more to an opponent than the licence is worth to us. CC0 also waives the
sui generis database right by name, closing the EU-specific residual exposure
from the contributor's side.

The grant is taken at token issuance, and that is not incidental. There are no
accounts, so there is no sign-up to attach terms to, and a manifest arrives over
POST /manifests with no channel to negotiate over. Acquiring the contribute
capability is the only moment a grant can be made, so POST /tokens now returns
the licence and its terms alongside the token — a licence the server publishes
but never delivers is one no contributor agreed to.

The test pins the scope limit as well as the identifier. Bounding the grant to
the manifest is the half that can fail silently: a reworded term reading onto
the underlying work would purport to grant what no contributor can.

Also records the settled code-licence position across all four repositories in
the legal posture, correcting an earlier claim there that the plugin and
extraction repos declared nothing. Both already carried LICENSE files.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

TRACES: UR-019 | PR-006
2026-07-31 10:43:43 +02:00
dtourolleandClaude Opus 5 d5dd8113ef chore: add the GPLv3 LICENSE file Cargo.toml already declared
`license = "GPL-3.0-or-later"` has been in the manifest since the start, and
cargo-deny's allow-list carries it annotated "This crate's own licence" — but
no LICENSE file existed, so the declaration pointed at nothing.

Text copied from the jRay plugin's LICENSE after verifying it is byte-identical
to the FSF's canonical gpl-3.0.txt, rather than transcribed. 674 lines where a
single altered word changes the terms is not a file to write by hand.

No requirement trailer: this serves none, it completes an existing declaration.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-31 10:43:32 +02:00
dtourolleandClaude Opus 5 0ff1018bcc Withdraw the file-hash tier; document the legal posture
CI / static musl binary (push) Has been skipped
CI / fmt, clippy, test (push) Failing after 2m0s
CI / advisories and licences (push) Successful in 27s
Removes `cut.video_hash` and the `exact` match tier on legal grounds. The
OpenSubtitles hash was the strongest technical signal available — it identifies
a specific file, so it cannot produce a false positive — and that is exactly
the problem.

Every tier must be a claim about a *cut*, never about a copy. A TMDB id
discloses "some copy of this film", which is what a library catalogue
discloses. A file hash discloses "this exact release": it made a read endpoint
into a release-level oracle, and made an instance's database a mapping from
file fingerprints to the instances holding them. That is a far more specific
disclosure than PR-005 permits, and a dataset no volunteer operator should be
asked to hold. The audio signature is the replacement: derived from content, it
identifies the cut rather than the copy, so two encodes of the same edit agree.

The field is deleted rather than kept as a vestigial null, on the same
reasoning §2 applied to `anneal_sec` — a key naming a signal the format no
longer has is actively misleading — so an upload carrying one is now an
unknown-field 400, with a test asserting it.

**Every content_id changes**, including for manifests that never carried a
hash, because the canonical `cut` object lost a key. The golden vector is
regenerated and re-verified against an independent Python implementation; the
plugin and extraction repos must adopt the new value or federation
deduplication silently breaks. Free now, pre-release; not free later.

Adds docs/legal-posture.md, the operator-facing half of what §5a asks for:
what an instance holds exhaustively, what it structurally cannot do, and how
that sits against the intermediary-liability regimes that plausibly apply.

208 tests. Coverage 25/32.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

TRACES: UR-011 | SR-004, PR-005
2026-07-31 09:52:03 +02:00
dtourolleandClaude Opus 5 545c7d92a2 Federation: replicate content, re-derive judgement (UR-008)
CI / fmt, clippy, test (push) Failing after 1m20s
CI / static musl binary (push) Has been skipped
CI / advisories and licences (push) Successful in 25s
Implements §9a. The replication surface is four reads and no writes: a change
feed, fetch by content_id, a batch have, and a human-facing peer directory —
plus a capabilities endpoint carrying the accepted envelope versions, which
lets a client discover a schema mismatch in one request instead of a 400 per
manifest across a library sweep.

Pull, never push: a pulling server chooses what it ingests and when. Push would
let any peer inject work into the validation queue — the same abuse surface as
anonymous upload, at higher volume.

Nothing inherits a peer's judgement. A pulled manifest runs the full §6 stage 1
and 2 validation and this server's own cast check, and the fetched body must
hash to the content_id that was asked for — the check that stops an
intermediary or a misbehaving peer substituting content under a trusted id.
A peer's retraction flags for review rather than delisting, because
auto-delisting would hand every peer a remote delete primitive; only the opt-in
per-peer abuse channel delists, because a takedown propagating at the speed of
manual review is the wrong failure mode for that one case.

A test caught a real bug in the first cut: the feed cursor was a ULID, and
ULIDs are only monotonic *between* milliseconds — two generated in the same
millisecond carry independent random components and can sort opposite to write
order. A peer resuming from `seq > cursor` would then silently skip an entry:
replication losing manifests with no error anywhere. The cursor is now an
AUTOINCREMENT integer, and the test asserts strict monotonicity rather than
merely sortedness.

Peer administration is deliberately not an API. §9a requires that a peering
exist only because an operator typed a URL, so nothing a remote server returns
can establish or widen one; there_is_no_endpoint_that_creates_a_peering asserts
that absence rather than trusting it.

212 tests. Coverage 25/32 (78%).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

TRACES: UR-008 | PR-006
2026-07-31 09:28:32 +02:00
dtourolleandClaude Opus 5 88c7264094 Ship the SR-003 schema bump: jmanifest_version 2
CI / fmt, clippy, test (push) Failing after 1m22s
CI / static musl binary (push) Has been skipped
CI / advisories and licences (push) Successful in 26s
Moves the exchange envelope to version 2 in lockstep with the truth file's
schema_version, per SR-003's requirement that breaking changes be batched and
ship together rather than piecemeal. The plugin had already moved to
schema_version 2; the server declaring 1 while accepting the new fields
defeated the point of having a version at all.

Flag day, not dual-accept (JR-003): version 1 is now rejected outright. All
three components are pre-release, and a v1 read path would be the one nobody
exercises, so it is the one that would rot while being dragged through every
later change to the reader. A pipeline still emitting v1 is incompatible until
updated — stated plainly rather than papered over with a shim nobody tests.

scenes become objects carrying belief and route (extraction AR-017) instead of
float pairs. Belief is bounded to [0, 1] rather than merely stored: §5a's
Threat 1 argument rests on every accepted value being bounded, and an unbounded
float is a 64-bit channel however harmless it looks. route is a closed enum, so
an invented value cannot be stored.

UR-018 is the requirement with the trap in it, and the reason content_id.rs is
untouched by this commit: belief is a producer-side estimate that may
legitimately differ between pipeline versions for identical timings, so
including it in the canonical form would give two servers different ids for the
same content — the exact failure mode §9a quantises centiseconds to avoid,
reintroduced one field along. It replicates as an attribute, exactly as
audio_signature does. The golden vector still passes unchanged, which is the
evidence rather than the claim.

191 tests. UR-015..018 move from Planned to Done; coverage 24/32 (75%).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

TRACES: UR-014, UR-015, UR-016, UR-017, UR-018 | SR-003
2026-07-31 09:13:14 +02:00
dtourolleandClaude Opus 5 c73f417d45 Traceability: move per-repo settings into traceability.toml
CI / fmt, clippy, test (push) Failing after 1m22s
CI / static musl binary (push) Has been skipped
CI / advisories and licences (push) Successful in 25s
The shared extractor now takes its per-repo taxonomy from a config file rather
than CLI flags, so the wrapper shrinks to the one thing it alone knows: the
repo root, which the vendored gate cannot infer because its own default
resolves inside the submodule.

Coverage unchanged at 23/32; the gate reports the scan as '26 (.rs under src,
tests)', which is the config being read rather than defaults being guessed.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-30 18:56:13 +02:00
dtourolleandClaude Opus 5 a1e789a6fe Traceability: vendor the shared gate, annotate the source
CI / fmt, clippy, test (push) Failing after 1m21s
CI / static musl binary (push) Has been skipped
CI / advisories and licences (push) Successful in 24s
Adds jray-project as a submodule at scripts/vendor/jray-project, so this repo
runs the same extractor as every other component rather than its own copy, and
gains the system spec that defines the PR/SR requirements its register traces
up to.

scripts/traceability-gate.sh is a thin wrapper holding only what is specific to
this repo: UR/DR prefixes, .rs sources, and REPO_ROOT — which the shared gate
cannot infer once vendored, since its default resolves to the submodule itself.
Each override fails silently in a way that looks like "no work done" rather
than "misconfigured", so the wrapper documents why each is needed.

Annotates 35 units with TRACES tags, on the code that decides rather than every
helper it calls. Coverage is 23/32 (71.9%) with no orphan tags. The nine
untraced are genuinely unimplemented: UR-007 is plugin-side, UR-008 is
federation, and UR-015..018 are the pending SR-003 schema bump.

The gate caught a real error in the first pass: several tags separated IDs of
different types with commas. A comma joins IDs within one type; a pipe
separates types. Fixed, and the diagnostics are now clean.

MIN_COVERAGE stays 0 deliberately. The gate still fails on orphan tags, a >100%
ratio, a register parsing to nothing, or an empty source scan — raise the
threshold as a ratchet once the remaining work lands.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-30 18:27:16 +02:00
dtourolleandClaude Opus 5 a848750a65 Initial implementation: core vertical slice
CI / fmt, clippy, test (push) Failing after 2m46s
CI / static musl binary (push) Has been skipped
CI / advisories and licences (push) Successful in 4m22s
Implements the core of SPEC.md — the manifest exchange, less audio-tier
matching (§3) and federation (§9a), both of which the spec sequences as
later work.

- §2 Jmanifest format and series bundles
- §3 cut matching: exact / runtime / loose tiers
- §4 API, less POST /manifests/search
- §5 rate limiting; §5a trust model, anonymous bearer tokens
- §6 upload validation, all four stages
- §7 relational storage, no JSON blob on the write path
- §8 Rust + Axum + SQLite, single serialized writer, in-process job queue
- §9a content addressing, computed on upload

Reconciled against the system spec:

- anneal_sec removed, withdrawn upstream by AR-012/AR-013. Presence follows
  track extent, so a track survives its own gaps and there is nothing to
  anneal. Its successor extinction_sec and the new gallery_scope are accepted
  and stored; scope enters the §7 ranking. A manifest still carrying
  anneal_sec is a hard 400, not silently ignored — it came from a pipeline
  whose window semantics differ from what this server assumes.
- Audio signature: media under 120 s now emits no signature at all, matching
  scene-actor-extraction IR-007. The earlier §3 draft allowed a shortened
  window under 150 s, which was the weaker rule — a caller-varying length is
  the property SR-004 forbids.
- UR IDs regularised to UR-nnn; docs/requirements.md registers 32
  requirements, each tracing to an SR-nnn or PR-nnn.

189 tests: unit, end-to-end through the real router, and an injection suite
covering SQL, JSON, header and Unicode payloads. Writing that suite found two
real gaps, both fixed here: compatibility homoglyphs passed the §5a character
class, and a one-frame audio signature was accepted on a feature-length item.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-30 18:14:02 +02:00