diff --git a/SPEC.md b/SPEC.md index 939b12d..df32021 100644 --- a/SPEC.md +++ b/SPEC.md @@ -360,6 +360,50 @@ tail. The result is ~1.7 KB per manifest — negligible against a ~4.8 KiB manifest. +##### Normative v1 parameters + +**The six steps above are not sufficient to reproduce a byte stream.** Each +choice below was underspecified and is now pinned; two independent +implementations that differ on any one of them produce signatures that never +match, which silently defeats the entire mechanism. + +| Parameter | v1 value | +|---|---| +| Hann window | **Periodic** (not symmetric) | +| Band value | **Mean of linear magnitudes** in the band — not sum, not max, and taken before the log | +| Peak tie-break | Lowest band index wins | +| Energy class | `log10(frame band-energy / upper-median frame energy)`, quantised at **−0.6 / −0.2 / +0.2** | +| Byte layout | `(band << 2) | class` — 5-bit band, 2-bit class | +| Base64 | Standard alphabet, with padding | +| Frame count | Whole frames only. Over 1 323 000 samples this yields **1288** frames, not "~1290" | + +The energy class is normalised against the **upper-median frame energy** rather +than an absolute level, which is what makes it invariant to gain and to trim +differences between releases. The thresholds straddle the median rather than +sitting on it, so a frame near the centre of the distribution does not flip class +under small perturbations. + +##### Conformance fixture + +A golden fixture is the authoritative tiebreak, because prose cannot pin +floating-point behaviour: +`scene-actor-extraction/tests/fixtures/audio/jray_audio_v1_golden.json`. + +It carries the expected signature, the decoded-window PCM checksum, the full +32-entry band→FFT-bin table, and the parameter contract — **an implementation can +be written from that file alone.** The PCM checksum is asserted separately from +the signature so a codec-level divergence is distinguishable from a DSP one. + +**Implementations should compute the FFT themselves** (radix-2, double +precision) rather than depending on a library whose version could change the +numerics. Verified decision margins on the fixture are 1.3% between the two +strongest bands and 3.6e-3 in log10 to an energy-class edge — many orders above +double-precision noise, so any two correct implementations agree. + +Measured on that fixture: the peak-band sequence survives a stereo/44.1 kHz round +trip and AAC 128 kbit/s re-encoding **exactly** (score 1.00), which is the +codec-robustness this design claims. + This is deliberately a **peak-bin** signature rather than a full spectrum: peaks survive lossy re-encoding, loudness normalisation and channel-layout differences, whereas absolute magnitudes do not. It follows the same principle diff --git a/docs/traceability.md b/docs/traceability.md index 1ae5904..dc6116b 100644 --- a/docs/traceability.md +++ b/docs/traceability.md @@ -1,11 +1,11 @@ # Requirements traceability matrix - + -**Generated:** 2026-07-30T16:25:39+00:00 +**Generated:** 2026-07-30T16:55:59+00:00 -Denominators are read from [`requirements.md`](requirements.md) at run time, never hardcoded. Coverage counts a requirement only when it is tagged in source **and** has a verification tier this CI host can execute — CI is an Intel N100 with no discrete GPU. +Denominators are read from [`requirements.md`](requirements.md) at run time, never hardcoded. Coverage counts a requirement only when it is tagged in source **and** has a verification tier this repo's CI host can execute (`T1, T2, static`). ## Summary @@ -18,7 +18,7 @@ Denominators are read from [`requirements.md`](requirements.md) at run time, nev | Requirements covered | 23 | | **Coverage** | **71.9%** (23/32) | | Coverage of CI-executable scope | 71.9% (23/32) | -| Tagged but unexecuted in CI (T4/GPU) | 0 | +| Tagged but unexecuted in CI | 0 | | Orphan tags | 0 | ### By type @@ -33,7 +33,7 @@ Denominators are read from [`requirements.md`](requirements.md) at run time, nev ## Not executable in CI -CI runs on an Intel N100 with no discrete GPU. These requirements have no verification tier that can run here, so a tag on them is evidence of *intent*, not of verification. They are never counted as covered. +These requirements have no verification tier this repo's CI host can run, so a tag on them is evidence of *intent*, not of verification. They are never counted as covered. _None._ @@ -51,7 +51,7 @@ _None._ ## Recorded exceptions -Deliberate, documented departures from an invariant (`EXCEPTION: AR-nnn `). Reported separately and never counted as coverage — an exception is a decision to be reviewed, not evidence a requirement is met. +Deliberate, documented departures from an invariant (`EXCEPTION: XX-nnn `). Reported separately and never counted as coverage — an exception is a decision to be reviewed, not evidence a requirement is met. _None._ @@ -98,9 +98,9 @@ _None._ **Locations:** 3 -- [`src/api/fetch.rs:267`](../src/api/fetch.rs#L267) — `Unknown` +- [`src/api/fetch.rs:267`](../src/api/fetch.rs#L267) — `pub fn reconstruct(` - [`src/db/repo.rs:337`](../src/db/repo.rs#L337) — `pub fn insert_manifest(tx: &Transaction<'_>, m: &NewManifest<'_>) -> anyhow::Result<()>` -- [`src/db/repo.rs:405`](../src/db/repo.rs#L405) — `Unknown` +- [`src/db/repo.rs:405`](../src/db/repo.rs#L405) — `pub fn actors_for_manifest(` ### DR-003 @@ -124,7 +124,7 @@ _None._ **Locations:** 1 -- [`src/ratelimit.rs:76`](../src/ratelimit.rs#L76) — `struct Window` +- [`src/ratelimit.rs:76`](../src/ratelimit.rs#L76) — `impl Default for RateLimiter` ### DR-008 @@ -149,8 +149,8 @@ _None._ **Locations:** 3 -- [`src/content_id.rs:52`](../src/content_id.rs#L52) — `Unknown` -- [`src/content_id.rs:129`](../src/content_id.rs#L129) — `fn push_opt_num(s: &mut String, v: Option)` +- [`src/content_id.rs:52`](../src/content_id.rs#L52) — `pub fn canonical_json(` +- [`src/content_id.rs:129`](../src/content_id.rs#L129) — `pub fn content_id(` - [`src/validate.rs:78`](../src/validate.rs#L78) — `pub fn to_centiseconds(secs: f64) -> i64` ### DR-013 @@ -173,27 +173,27 @@ _None._ **Locations:** 1 -- [`src/api/exists.rs:74`](../src/api/exists.rs#L74) — `Unknown` +- [`src/api/exists.rs:74`](../src/api/exists.rs#L74) — `pub async fn exists_batch(` ### PR-006 **Locations:** 5 -- [`src/api/fetch.rs:128`](../src/api/fetch.rs#L128) — `Unknown` -- [`src/api/upload.rs:29`](../src/api/upload.rs#L29) — `Unknown` -- [`src/api/upload.rs:101`](../src/api/upload.rs#L101) — `Unknown` -- [`src/ingest.rs:50`](../src/ingest.rs#L50) — `Unknown` +- [`src/api/fetch.rs:128`](../src/api/fetch.rs#L128) — `pub async fn get_series(` +- [`src/api/upload.rs:29`](../src/api/upload.rs#L29) — `pub async fn post_manifest(` +- [`src/api/upload.rs:101`](../src/api/upload.rs#L101) — `pub async fn post_bundle(` +- [`src/ingest.rs:50`](../src/ingest.rs#L50) — `pub fn persist(` - [`src/validate.rs:540`](../src/validate.rs#L540) — `pub fn validate_bundle_envelope(b: &SeriesBundle) -> VResult<()>` ### SR-001 **Locations:** 7 -- [`src/api/exists.rs:61`](../src/api/exists.rs#L61) — `Unknown` -- [`src/api/exists.rs:74`](../src/api/exists.rs#L74) — `Unknown` -- [`src/api/fetch.rs:267`](../src/api/fetch.rs#L267) — `Unknown` +- [`src/api/exists.rs:61`](../src/api/exists.rs#L61) — `pub async fn exists(` +- [`src/api/exists.rs:74`](../src/api/exists.rs#L74) — `pub async fn exists_batch(` +- [`src/api/fetch.rs:267`](../src/api/fetch.rs#L267) — `pub fn reconstruct(` - [`src/castcheck.rs:82`](../src/castcheck.rs#L82) — `pub fn evaluate(submitted: &[SubmittedActor], credits: &[CastMember]) -> CastCheckOutcome` -- [`src/db/repo.rs:405`](../src/db/repo.rs#L405) — `Unknown` +- [`src/db/repo.rs:405`](../src/db/repo.rs#L405) — `pub fn actors_for_manifest(` - [`src/matching.rs:52`](../src/matching.rs#L52) — `pub fn match_cut(client: &ClientCut, stored: &StoredCut) -> Option` - [`src/model.rs:179`](../src/model.rs#L179) — `Unknown` @@ -201,7 +201,7 @@ _None._ **Locations:** 3 -- [`src/api/fetch.rs:267`](../src/api/fetch.rs#L267) — `Unknown` +- [`src/api/fetch.rs:267`](../src/api/fetch.rs#L267) — `pub fn reconstruct(` - [`src/model.rs:179`](../src/model.rs#L179) — `Unknown` - [`src/validate.rs:486`](../src/validate.rs#L486) — `fn validate_scenes(idx: usize, a: &Actor, runtime_sec: f64) -> VResult>` @@ -210,8 +210,8 @@ _None._ **Locations:** 8 - [`src/api/json.rs:100`](../src/api/json.rs#L100) — `fn require_utf8(bytes: &[u8]) -> Result<&str, ApiError>` -- [`src/content_id.rs:52`](../src/content_id.rs#L52) — `Unknown` -- [`src/content_id.rs:129`](../src/content_id.rs#L129) — `fn push_opt_num(s: &mut String, v: Option)` +- [`src/content_id.rs:52`](../src/content_id.rs#L52) — `pub fn canonical_json(` +- [`src/content_id.rs:129`](../src/content_id.rs#L129) — `pub fn content_id(` - [`src/error.rs:8`](../src/error.rs#L8) — `Unknown` - [`src/model.rs:198`](../src/model.rs#L198) — `Unknown` - [`src/validate.rs:78`](../src/validate.rs#L78) — `pub fn to_centiseconds(secs: f64) -> i64` @@ -222,9 +222,9 @@ _None._ **Locations:** 16 -- [`src/api/report.rs:56`](../src/api/report.rs#L56) — `Unknown` -- [`src/api/upload.rs:29`](../src/api/upload.rs#L29) — `Unknown` -- [`src/api/upload.rs:223`](../src/api/upload.rs#L223) — `Unknown` +- [`src/api/report.rs:56`](../src/api/report.rs#L56) — `pub async fn post_report(` +- [`src/api/upload.rs:29`](../src/api/upload.rs#L29) — `pub async fn post_manifest(` +- [`src/api/upload.rs:223`](../src/api/upload.rs#L223) — `pub async fn post_token(` - [`src/app.rs:26`](../src/app.rs#L26) — `pub fn router(state: AppState) -> Router` - [`src/auth.rs:22`](../src/auth.rs#L22) — `pub fn hash_token(token: &str) -> String` - [`src/auth.rs:76`](../src/auth.rs#L76) — `pub fn client_ip(headers: &HeaderMap, peer: Option, trusted_proxies: &[IpAddr]) -…` @@ -233,7 +233,7 @@ _None._ - [`src/db/repo.rs:337`](../src/db/repo.rs#L337) — `pub fn insert_manifest(tx: &Transaction<'_>, m: &NewManifest<'_>) -> anyhow::Result<()>` - [`src/model.rs:156`](../src/model.rs#L156) — `Unknown` - [`src/model.rs:198`](../src/model.rs#L198) — `Unknown` -- [`src/ratelimit.rs:76`](../src/ratelimit.rs#L76) — `struct Window` +- [`src/ratelimit.rs:76`](../src/ratelimit.rs#L76) — `impl Default for RateLimiter` - [`src/validate.rs:112`](../src/validate.rs#L112) — `fn is_allowed_text_char(c: char) -> bool` - [`src/validate.rs:203`](../src/validate.rs#L203) — `pub fn validate_manifest(mut m: Jmanifest) -> VResult` - [`src/validate.rs:354`](../src/validate.rs#L354) — `pub fn validate_audio_signature(sig: &str, runtime_sec: f64) -> VResult<()>` @@ -244,28 +244,28 @@ _None._ **Locations:** 2 - [`src/db/repo.rs:337`](../src/db/repo.rs#L337) — `pub fn insert_manifest(tx: &Transaction<'_>, m: &NewManifest<'_>) -> anyhow::Result<()>` -- [`src/ingest.rs:50`](../src/ingest.rs#L50) — `Unknown` +- [`src/ingest.rs:50`](../src/ingest.rs#L50) — `pub fn persist(` ### UR-001 **Locations:** 3 -- [`src/api/exists.rs:61`](../src/api/exists.rs#L61) — `Unknown` -- [`src/api/exists.rs:74`](../src/api/exists.rs#L74) — `Unknown` +- [`src/api/exists.rs:61`](../src/api/exists.rs#L61) — `pub async fn exists(` +- [`src/api/exists.rs:74`](../src/api/exists.rs#L74) — `pub async fn exists_batch(` - [`src/matching.rs:52`](../src/matching.rs#L52) — `pub fn match_cut(client: &ClientCut, stored: &StoredCut) -> Option` ### UR-002 **Locations:** 2 -- [`src/api/upload.rs:29`](../src/api/upload.rs#L29) — `Unknown` -- [`src/ingest.rs:50`](../src/ingest.rs#L50) — `Unknown` +- [`src/api/upload.rs:29`](../src/api/upload.rs#L29) — `pub async fn post_manifest(` +- [`src/ingest.rs:50`](../src/ingest.rs#L50) — `pub fn persist(` ### UR-003 **Locations:** 6 -- [`src/api/upload.rs:29`](../src/api/upload.rs#L29) — `Unknown` +- [`src/api/upload.rs:29`](../src/api/upload.rs#L29) — `pub async fn post_manifest(` - [`src/castcheck.rs:82`](../src/castcheck.rs#L82) — `pub fn evaluate(submitted: &[SubmittedActor], credits: &[CastMember]) -> CastCheckOutcome` - [`src/model.rs:156`](../src/model.rs#L156) — `Unknown` - [`src/model.rs:198`](../src/model.rs#L198) — `Unknown` @@ -277,14 +277,14 @@ _None._ **Locations:** 2 - [`src/auth.rs:76`](../src/auth.rs#L76) — `pub fn client_ip(headers: &HeaderMap, peer: Option, trusted_proxies: &[IpAddr]) -…` -- [`src/ratelimit.rs:76`](../src/ratelimit.rs#L76) — `struct Window` +- [`src/ratelimit.rs:76`](../src/ratelimit.rs#L76) — `impl Default for RateLimiter` ### UR-005 **Locations:** 6 -- [`src/api/report.rs:56`](../src/api/report.rs#L56) — `Unknown` -- [`src/api/upload.rs:223`](../src/api/upload.rs#L223) — `Unknown` +- [`src/api/report.rs:56`](../src/api/report.rs#L56) — `pub async fn post_report(` +- [`src/api/upload.rs:223`](../src/api/upload.rs#L223) — `pub async fn post_token(` - [`src/auth.rs:22`](../src/auth.rs#L22) — `pub fn hash_token(token: &str) -> String` - [`src/castcheck.rs:82`](../src/castcheck.rs#L82) — `pub fn evaluate(submitted: &[SubmittedActor], credits: &[CastMember]) -> CastCheckOutcome` - [`src/castcheck.rs:215`](../src/castcheck.rs#L215) — `pub fn category_guard_violation(matched: &[MatchedActor], title_is_adult: bool) -> Option…` @@ -294,15 +294,15 @@ _None._ **Locations:** 3 -- [`src/api/fetch.rs:128`](../src/api/fetch.rs#L128) — `Unknown` -- [`src/api/upload.rs:101`](../src/api/upload.rs#L101) — `Unknown` +- [`src/api/fetch.rs:128`](../src/api/fetch.rs#L128) — `pub async fn get_series(` +- [`src/api/upload.rs:101`](../src/api/upload.rs#L101) — `pub async fn post_bundle(` - [`src/validate.rs:540`](../src/validate.rs#L540) — `pub fn validate_bundle_envelope(b: &SeriesBundle) -> VResult<()>` ### UR-007 **Locations:** 1 -- [`src/api/exists.rs:74`](../src/api/exists.rs#L74) — `Unknown` +- [`src/api/exists.rs:74`](../src/api/exists.rs#L74) — `pub async fn exists_batch(` ### UR-009 @@ -314,9 +314,9 @@ _None._ **Locations:** 4 -- [`src/api/fetch.rs:267`](../src/api/fetch.rs#L267) — `Unknown` +- [`src/api/fetch.rs:267`](../src/api/fetch.rs#L267) — `pub fn reconstruct(` - [`src/castcheck.rs:82`](../src/castcheck.rs#L82) — `pub fn evaluate(submitted: &[SubmittedActor], credits: &[CastMember]) -> CastCheckOutcome` -- [`src/db/repo.rs:405`](../src/db/repo.rs#L405) — `Unknown` +- [`src/db/repo.rs:405`](../src/db/repo.rs#L405) — `pub fn actors_for_manifest(` - [`src/model.rs:179`](../src/model.rs#L179) — `Unknown` ### UR-011 @@ -333,13 +333,13 @@ _None._ **Locations:** 2 - [`src/db/repo.rs:337`](../src/db/repo.rs#L337) — `pub fn insert_manifest(tx: &Transaction<'_>, m: &NewManifest<'_>) -> anyhow::Result<()>` -- [`src/ingest.rs:50`](../src/ingest.rs#L50) — `Unknown` +- [`src/ingest.rs:50`](../src/ingest.rs#L50) — `pub fn persist(` ### UR-013 **Locations:** 3 -- [`src/api/fetch.rs:267`](../src/api/fetch.rs#L267) — `Unknown` +- [`src/api/fetch.rs:267`](../src/api/fetch.rs#L267) — `pub fn reconstruct(` - [`src/model.rs:179`](../src/model.rs#L179) — `Unknown` - [`src/validate.rs:486`](../src/validate.rs#L486) — `fn validate_scenes(idx: usize, a: &Actor, runtime_sec: f64) -> VResult>` diff --git a/scripts/traceability-gate.sh b/scripts/traceability-gate.sh index 6961ecd..8386bfe 100755 --- a/scripts/traceability-gate.sh +++ b/scripts/traceability-gate.sh @@ -3,8 +3,9 @@ # Requirement traceability gate for JRay-public-server. # # A thin wrapper over the shared implementation in the `jray-project` submodule. -# Everything repo-specific lives here; the tooling itself is identical across all -# three components, so a fix to the gate benefits every repo rather than one. +# Everything repo-specific lives in `traceability.toml` at the repo root, so this +# script only has to point the shared gate at the right tree — and a fix to the +# gate benefits every component rather than one. # # scripts/traceability-gate.sh # @@ -12,8 +13,8 @@ # # git submodule update --init --recursive # -# Environment (passed through to the shared gate): -# MIN_COVERAGE minimum overall coverage percent +# Environment (passed through): +# MIN_COVERAGE override the threshold in traceability.toml # ALLOW_ORPHANS set to 1 to report orphan tags without failing set -eu @@ -28,31 +29,8 @@ if [ ! -f "$VENDOR/scripts/traceability/traceability-gate.sh" ]; then exit 2 fi -# Why each override is needed, since omitting any of them fails silently in a -# way that looks like "no work done" rather than "misconfigured": -# -# REPO_ROOT the shared gate defaults to two levels above itself, which is -# inside the submodule once vendored. -# TYPES this repo's register uses UR/DR. The default is the extraction -# pipeline's AR/DP/IR/GR/VR, under which the register parses to -# ZERO requirements. -# SUFFIXES this repo is Rust. The default is C++/Python, under which the -# source tree scans to ZERO files. -# SYSTEM_SPEC the PR/SR requirements live in the project home, which is the -# submodule itself — so it ships with the tool that reads it. -# -# MIN_COVERAGE stays 0 until the TRACES annotation pass lands. That does not -# make the gate toothless: orphan tags, a >100% ratio, a register parsing to -# nothing and an empty source scan are all hard failures from day one. Raise it -# as tags land, and treat every raise as a ratchet. -REPO_ROOT="$REPO_ROOT" \ -REQUIREMENTS="$REPO_ROOT/docs/requirements.md" \ -SYSTEM_SPEC="$VENDOR/SPEC.md" \ -TYPES="UR,DR" \ -SUFFIXES=".rs" \ -SCAN_ROOTS="src,tests" \ -MIN_COVERAGE="${MIN_COVERAGE:-0}" \ -ALLOW_ORPHANS="${ALLOW_ORPHANS:-0}" \ -TRACES_JSON="${TRACES_JSON:-$REPO_ROOT/traces-report.json}" \ -TRACES_MD="${TRACES_MD:-$REPO_ROOT/docs/traceability.md}" \ -exec sh "$VENDOR/scripts/traceability/traceability-gate.sh" +# REPO_ROOT is the one thing the shared gate cannot infer: its own default is +# two levels above itself, which is inside the submodule once vendored. The +# taxonomy, languages and thresholds all come from traceability.toml, which the +# extractor discovers from this root. +REPO_ROOT="$REPO_ROOT" exec sh "$VENDOR/scripts/traceability/traceability-gate.sh" "$@" diff --git a/scripts/vendor/jray-project b/scripts/vendor/jray-project index 041961c..17106f3 160000 --- a/scripts/vendor/jray-project +++ b/scripts/vendor/jray-project @@ -1 +1 @@ -Subproject commit 041961c8c63bf37fa220dfa1f39858ca9830a84e +Subproject commit 17106f337074753698454ccaf9c739cb6d0b4d79 diff --git a/traceability.toml b/traceability.toml new file mode 100644 index 0000000..012c9ab --- /dev/null +++ b/traceability.toml @@ -0,0 +1,33 @@ +# Traceability configuration for JRay-public-server. +# +# Read by the shared extractor in the jray-project submodule, which is the same +# implementation every JRay component uses. Everything repo-specific lives here +# rather than in the tool; run `extract_traces.py --print-example-config` for +# the annotated schema. +# +# This file's directory is taken as the repo root, so the gate works from any +# subdirectory. + +# The prefixes this repo's register defines. Nothing else enters the fraction: +# UT/IT are evidence for requirements, PR/SR belong to the system spec. +requirement_types = ["UR", "DR"] + +languages = ["rust"] + +source_roots = ["src", "tests"] + +# Every test here runs on any machine in seconds — no GPU, no fixtures, no +# external services. Unlike the extraction pipeline, this repo has no tier that +# cannot execute in CI, so nothing is ever counted as tagged-but-unverified. +ci_executable_tiers = ["T1", "T2", "static"] + +# 0 until the remaining work lands: UR-007 is plugin-side, UR-008 is federation, +# and UR-015..018 are the pending SR-003 schema bump. This is not a gate that +# cannot fail — orphan tags, a >100% ratio, a register that parses to nothing +# and an empty source scan are all hard failures already. Ratchet this up as +# tags land; never reset it down. +min_coverage = 0.0 + +# The system spec owning PR/SR, vendored as a submodule so it ships with the +# tool that reads it. +system_spec = "scripts/vendor/jray-project/SPEC.md"