feat(truth): schema_version 2 read path, v2 only

Replaces the v1 shape rather than accepting both. `anneal_sec` and the
top-level `sample_fps` are deleted, not zeroed; `extraction` and `cut`
blocks arrive; `scenes` become objects carrying belief and route, so a
window records how far to trust it instead of being a bare float pair.

`TruthSchema.IsSupported` is the single gate and is applied on all four
read paths — sidecar, managed store load, managed PUT, and converted
manifest. Previously only the controller checked, so the version the
plugin claimed to require and the one it would actually parse were free
to drift. Rejections name the file and the version found, so an item that
looks empty is distinguishable from one that was refused.

`ManifestConverter` carries belief, route and both provenance blocks
through: dropping them would silently downgrade every fetched manifest
against a locally extracted one.

TRACES: JR-002, JR-003 | SR-003
This commit is contained in:
2026-07-31 16:23:58 +02:00
parent 596566813c
commit 19aecee646
15 changed files with 583 additions and 46 deletions
@@ -83,12 +83,36 @@ public sealed class TruthDataService : ITruthDataService
using var stream = File.OpenRead(truthPath);
var truth = await JsonSerializer.DeserializeAsync<TruthFile>(stream, JsonOptions, cancellationToken)
.ConfigureAwait(false);
if (!TruthSchema.IsSupported(truth))
{
// Named, not silent. A stale v1 sidecar makes an item look
// un-extracted, and re-extracting a library that has already
// been processed is the most expensive mistake this plugin can
// cause a user to make. The log line is what distinguishes the
// two states.
_logger.LogWarning(
"JRay: ignoring truth file {TruthPath} — schema_version {Found}, expected {Expected}. Re-extract this item.",
truthPath,
truth?.SchemaVersion ?? 0,
TruthSchema.SupportedVersion);
_cache[itemId] = new CacheEntry(null, DateTime.UtcNow);
return null;
}
_cache[itemId] = new CacheEntry(truth, DateTime.UtcNow);
return truth;
}
catch (Exception ex) when (ex is IOException or JsonException)
{
_logger.LogWarning(ex, "JRay: failed to read truth file {TruthPath}", truthPath);
// A v1 file also lands here rather than above: `scenes` was a float
// pair in v1 and is an object in v2, so it fails to deserialise
// before the version can be inspected. Both routes must therefore
// name the file, which is why the message below says the same thing.
_logger.LogWarning(
ex,
"JRay: failed to read truth file {TruthPath}. If this is a v1 file, re-extract the item — v1 is no longer read.",
truthPath);
return null;
}
}