Evidence-Spine Convergence (Seam 2)¶
Concepts: AU-KG.identity.evidence-spine-convergence (this doc) ·
AU-KG.identity.asset-occurrence (AU-P1-4, media_store.py's existing
Blob/Rendition/AssetOccurrence identity chain) · EG-X1 (epistemic-graph's
multimodal evidence-graph spine + citation resolver,
crates/eg-epistemic/src/evidence.rs, feature evidence-graph).
The gap this closes¶
Before this change there were two parallel evidence chains for the same
governed eg_modality::EvidenceLocus shape:
- AU stored that some bytes occurred — a
:SourceObject -> :AssetOccurrence -> :Blobidentity chain (AU-P1-4) — but had no way to say where inside those bytes a claim's evidence sat. - epistemic-graph's own evidence-graph (EG-X1) already resolved a located
EvidenceLocus(anEvidenceAddressof kindPageRegion/ImageRegion/AudioRange/…, wrapped withid/subject/policy_ref/derivation_ref) off an:Evidencenode'sevidence_locus/occurrence_id/blob_refproperties viaMethod::ExplainEvidence/eg_epistemic::evidence_citations— but nothing ever wrote an AU-produced occurrence into that shape.
Correction (this pass): the first version of this seam wrote the property
under the WRONG key with the WRONG shape — evidence_span holding an
informal, externally-tagged {"PageBox": {...}}-style dict (missing the
governed id/subject/policy_ref/derivation_ref envelope entirely and
using field names that don't match eg_modality::EvidenceAddress, e.g.
PageBox instead of PageRegion, DocumentSpan instead of CharacterRange).
Every MediaStore.store_*_evidence write therefore reached the graph but was
silently unresolvable by decode_locus/evidence_citations/
Method::ExplainEvidence (which read ONLY evidence_locus, confirmed by
direct grep of the engine source and by crates/eg-epistemic/tests/
x1_au_occurrence_chain.rs, the engine-side contract test this seam always
claimed to mirror) — despite every MediaStore unit test passing, because
those tests asserted on the same wrong key/shape the code wrote, never
cross-checked against the engine's real decoder. Every producer below now
writes the correct evidence_locus property in the exact shape
x1_au_occurrence_chain.rs proves the engine decodes (see
MediaStore._governed_locus/_opaque_hex).
A citation resolved through AU and a citation resolved through EG were answering two different questions from two different graphs of record. Seam 2 converges them: one write path, one resolver.
What changed¶
MediaStore.store_document_page_evidence (agent_utilities/knowledge_graph/
memory/media_store.py) is a new, opt-in method — nothing about
store_media/store_rendition changed, and a caller that never calls it writes
nothing extra. When a caller HAS a document page + bounding box for the bytes
it's storing, this method:
- Stores the bytes via the existing
store_media(AU-P1-4's:AssetOccurrence -> :Blobchain, unchanged). - Writes/reuses a
:SourceObjectnode for the owning document (sourceobject:<document_id>, upserted once) plus a structuralhasOccurrenceedge to the new occurrence. - Writes an
:Evidencenode carrying a governedeg_modality::EvidenceLocus(aPageRegionEvidenceAddress—{page, x, y, width, height}— wrapped withid/subject/policy_ref/derivation_ref, the exact{"id", "subject", "address", "policy_ref", "derivation_ref"}shapeeg_epistemic::BeliefGraph::from_graph_viewdecodes off the node'sevidence_locusproperty — seeMediaStore._governed_locus) plusoccurrence_id/blob_ref— the SAME identity-chain conventioneg_epistemic::evidencedocuments — and a structuralextractedFromedge back to the occurrence. - When a
claim_idis given, links the evidence to it with arelationship_type: "SUPPORTS"edge — the SAME conventioneg_epistemic's own claim materialization (src/server/handlers/mining.rs::materialize_claim) writes, soevidence_citations's support/contradiction/attack walk recognizes it with no engine-side change.
No second resolver, no new engine write endpoint. The generic
AddNode/AddEdge RPCs (client.nodes.add/client.edges.add) the rest of
MediaStore already uses are enough to produce the exact property/edge
shape the engine's real decoder expects — reading citations back always goes
through epistemic-graph's own Method::ExplainEvidence, never a second,
AU-side implementation of the same resolution logic.
flowchart LR
subgraph AU["agent-utilities (Python)"]
SM["MediaStore.store_media()\n(unchanged, AU-P1-4)"]
SD["MediaStore.store_document_page_evidence()\n(NEW, opt-in)"]
SD --> SM
end
SM -->|AddNode/AddEdge| SO[":SourceObject"]
SM -->|AddNode/AddEdge, hasBlob| OC[":AssetOccurrence"]
OC --> BL[":Blob"]
SD -->|hasOccurrence| SO
SO --> OC
SD -->|AddNode: evidence_locus/occurrence_id/blob_ref| EV[":Evidence"]
SD -->|extractedFrom| OC
SD -->|"SUPPORTS (relationship_type)"| CL[":Claim"]
EV --> CL
subgraph EG["epistemic-graph engine (Rust)"]
BGV["BeliefGraph::from_graph_view()"]
EC["evidence_citations() /\nMethod::ExplainEvidence"]
BGV --> EC
end
EV -.decoded by.-> BGV
CL -.decoded by.-> BGV
Proof (the vertical slice)¶
One modality, end-to-end: document page-box (EvidenceAddress::PageRegion).
- AU half (no live engine needed):
tests/unit/knowledge_graph/ test_media_store_evidence_spine.pyprovesstore_document_page_evidencewrites the exact node/edge shape — a governedevidence_locus/occurrence_id/blob_refon the:Evidencenode (test_governed_locus_matches_the_engine_ contractpins_governed_locus's output against hardcoded literals — not by comparing the helper to itself — so a regression in the helper can't pass silently), structuralhasOccurrence/extractedFrom/hasBlobedges, and therelationship_type: "SUPPORTS"edge when aclaim_idis given. - EG half (epistemic-graph repo):
crates/eg-epistemic/tests/ x1_au_occurrence_chain.rsmirrors those EXACT literal values (page=4, x=72.0, y=120.5, width=400.0, height=18.0) into a realGraphView, decodes them through the REALBeliefGraph::from_graph_view, and assertsevidence_citations/resolve_locusreturn the exactPageRegionlocus + occurrence/blob identity — the same acceptance shape EG-X1's ownx1_evidence_chain.rsestablished for a hand-built fixture, now keyed off AU's actual write shape.crates/eg-epistemic/tests/ x2_au_locus_kinds_chain.rsextends the same proof toTableCellRange/CodeSymbol/RowVersion/MetricWindow/TraceSpan(W3.3).
Together the two prove the round trip without requiring a live server built
with the (opt-in, non-default) evidence-graph Cargo feature in this repo's
test harness — see "What remains" below.
All eleven loci now wired (AU half) — Seam 2 completion¶
eg_modality::EvidenceAddress defines eleven located-locus variants (plus a
twelfth, Point/Spatial, added to the engine after this seam's scope was
set — see the W3.3 note at the end of this doc). The page-region slice above
proved the pattern for PageRegion; the SAME pattern now extends to the
remaining ten via a shared private skeleton, MediaStore._store_located_evidence
(agent_utilities/knowledge_graph/memory/media_store.py) — the generalized
form of store_document_page_evidence's steps 2-4 (upsert :SourceObject,
write the :Evidence node with a governed evidence_locus/occurrence_id/
blob_ref, the extractedFrom edge, and the SUPPORTS edge when claim_id
is given), parameterized over about_id (whichever locus field identifies the
owning artifact) and locus_kind/address (the Rust EvidenceAddress
variant's own tag + fields — built into the full governed shape by
MediaStore._governed_locus; caller-supplied identifiers that the Rust side
types as an opaque reference, e.g. CodeSymbol's revision_ref/symbol_ref
or RowVersion's row_ref, are hashed into a deterministic opaque token by
MediaStore._opaque_hex rather than carried verbatim). Each public wrapper
below returns the (identically-shaped) EvidenceLocus dataclass and is opt-in
exactly like store_document_page_evidence — nothing about store_media/
store_rendition changes.
Locus (eg_modality::EvidenceAddress) |
MediaStore method |
Producer wiring |
|---|---|---|
PageBox |
store_document_page_evidence |
Wired 2026-07-22 (pass 2) — readers_office.read_pptx (agent_utilities/knowledge_graph/extraction/readers_office.py), one locus per slide, boxed to the deck's real slide_width/slide_height. The PDF text path (extraction/pdf.py) was surveyed and genuinely has no computed-and-discarded per-page box to wire: it deliberately isolates parsing in a killable, engine-less subprocess and joins ALL pages into one flat string, by design, for security isolation — no page boundary or box survives the pipe, so wiring it will require new computation, not just plumbing. W4.6 (2026-07-24) adds a SECOND, sidecar-delegated PDF producer — agent_utilities/media/pdf_sidecar.py::ingest_pdf_via_sidecar hands the PDF to a governed fleet OCR sidecar (default stirlingpdf-mcp; see agent_utilities/media/sidecar_contract.py) and writes one PageBox locus per returned page — a DIFFERENT, additive producer from extraction/pdf.py's deliberately page-boundary-free subprocess, reached via the new graph_media_sidecar MCP tool rather than the passive ingestion funnel. |
DocumentSpan |
store_document_span_evidence |
Wired (pass 1, 2026-07-22) — IngestionEngine._extract_facts_into_graph (agent_utilities/knowledge_graph/ingestion/engine.py), one locus per persisted fact whose evidence_span is a real substring of the window it was extracted from. W4.6 (2026-07-24) adds a second producer: pdf_sidecar.py, one locus per page covering that page's full sidecar-extracted text (the same "whole known extent" volume discipline TableCellRange/PageBox already use, not per-line/per-fact). |
TableCellRange |
store_table_cell_evidence |
Wired (pass 1, 2026-07-22) — readers_office.read_xlsx (agent_utilities/knowledge_graph/extraction/readers_office.py), one locus per worksheet covering its full used range. |
ImageRegion |
store_image_region_evidence |
Wired (pass 1, 2026-07-22), RapidOCR branch only — readers_media._ocr_with_rapidocr (agent_utilities/knowledge_graph/extraction/readers_media.py). The pytesseract branch (_ocr_with_pytesseract) never computes a box at all (image_to_string has no box output), so there is nothing to wire there without adding new computation — left unwired by design. W4.6 (2026-07-24) adds two more producers, both sidecar-delegated (agent_utilities/media/, CONCEPT:AU-KG.ingest.media-sidecar-delegation): pdf_sidecar.py writes one locus per OCR word/line box a PDF sidecar returns (image_id="<document_id>:page<n>", mirroring RapidOCR's own per-line granularity); image_sidecar.py writes one locus per detected region in a JPEG (a NEW modality — JPEG decode never had an AU producer before this wave). Both are fail-closed gated by the sidecar's declared SidecarCapability.produces (sidecar_contract.assert_capable) — a provider not declared for ImageRegion (e.g. the pdf_documents/paperless-ngx-mcp alternate) writes none, never a guess. |
AudioSegment |
store_audio_segment_evidence |
Wired (pre-existing, confirmed 2026-07-21) — messaging/router.py. GOC-07 adds a second producer: agent_utilities/media/audio_sidecar.py::ingest_audio_via_sidecar hands the recording to a governed fleet transcription sidecar (default audio-transcriber-mcp) and writes one locus per returned segment. |
MetricWindow |
store_metric_window_evidence |
Wired 2026-07-22 (pass 2) — new observability/gateway_health.py, driven from GatewayMetricsMiddleware's already-computed per-request duration (observability/gateway_metrics.py). Bounded/claim-driven-equivalent by design: request durations distill into ONE HealthTrendBuffer window (5 min), and a write fires only when health.detect_anomaly actually flags that window against the gateway's own rolling baseline — never per request. This SAME wiring is also the first live caller anywhere in AU of observability.health's anomaly kernel + health_ingest.ingest_health_anomaly, which pass 1 found was itself a fully-built, unwired kernel. |
CodeSymbol |
store_code_symbol_evidence |
Wired 2026-07-22 (pass 2) — research/candidate_insight.py's register_claim_materialization (the ONE shared seam every real, floor-cleared :Claim from every finding family already passes through), via new _persist_code_symbol_evidence. Bounded/claim-driven: fires only when a claim's own source_ids resolve to a real, engine-stored :Code/:Test node — never per AST symbol on ingestion. data is the real text of the symbol's known start line, read back from the source file on disk (no fabricated end_line — the stored node doesn't carry one, and recovering it will need a second, language-specific re-parse this module deliberately avoids). |
TraceSpan |
store_trace_span_evidence |
Wired 2026-07-22 (pass 2) — same register_claim_materialization seam, via new _persist_trace_span_evidence. Bounded/claim-driven: fires only when a claim's source_ids resolve to a real, engine-stored span/generation node (the shape an ops-causal root-cause finding's causal path produces, since it runs from an ingested Trace/Generation through agent/tool/model/service/deploy) — never per span/generation event on KGTraceBackend.record_event's hot path. data is the node's own already-stored properties, serialized. |
RowVersion |
store_row_version_evidence |
Wired 2026-07-22 (pass 2), opt-in — DatabaseConnector.poll() (protocols/source_connectors/connectors/database.py), via new _persist_row_version_evidence. Requires TWO real, non-fabricated facts: a new optional table config field (the connector wraps an arbitrary SELECT, so this is NEVER inferred from the query text — only written when the operator who authored the query explicitly configures it) AND updated_field's value parsing as a genuine integer (a real incrementing-id/revision watermark; an ISO-timestamp watermark, the other documented updated_field use case, cleanly no-ops — no version is invented). With table unset (the default) or a non-numeric watermark, this locus stays unwired for that source, honestly, rather than guessing. |
VideoShot |
store_video_shot_evidence |
Unwired 2026-07-22 through W4.6 (see "What will be required" below — no in-AU video-decode capability existed). GOC-07 (2026-08-16) wires it via the sidecar-delegate path (not an in-AU decode capability — that gap is unchanged): agent_utilities/media/video_sidecar.py::ingest_video_via_sidecar hands the video to a governed fleet sidecar (default data-science-mcp) and writes one locus per returned shot boundary. |
VideoFrameRange |
store_video_frame_range_evidence |
Unwired 2026-07-22 through W4.6 (see "What will be required" below — no in-AU video-decode capability existed). GOC-07 (2026-08-16) wires it via the same sidecar-delegate path: one locus per returned keyframe (a decoded-frame index range, kept distinct from VideoShot's wall-clock range). |
Proof: tests/unit/knowledge_graph/test_media_store_evidence_spine.py's
test_store_locus_evidence_writes_the_full_identity_chain /
test_store_locus_evidence_links_supports_edge_when_claim_given are
parametrized over all ten new loci (LOCUS_CASES), each asserting the exact
governed evidence_locus (built via the independently-pinned
_governed_locus) plus occurrence_id/blob_ref and the structural
hasOccurrence/extractedFrom/hasBlob edges, mirroring the page-region
test's approach node-for-node.
Producer wiring — 2026-07-22 pass (three more closed)¶
The seam-closure audit (reports/seam-closure-audit-2026-07-22.md) re-counted
this survey against live callers and found only 2 of 11 loci (PageBox,
AudioSegment) had one — the other nine had a tested MediaStore method with
the underlying data computed and discarded right next to it. This pass wired
three of the nine (DocumentSpan, TableCellRange, ImageRegion), following
AudioSegment's own producer (messaging/router.py) as the template — a
best-effort side effect at the exact point the data was already being thrown
away, reached via knowledge_graph.memory.native_ingest.media_store() (the
same ambient-engine accessor native_ingest.py's other typed writers already
use, so no new context plumbing was needed to reach a bound MediaStore):
DocumentSpan—IngestionEngine._extract_facts_into_graph(knowledge_graph/ingestion/engine.py) now locates each persisted fact'sevidence_spanback into the SAME windowextract_factsmined it from viastr.find(a real offset lookup, not a new computation) and writes one locus per fact.persist_facts()itself is untouched — the evidence write sits alongside it, exactly like_persist_audio_segment_evidencesits alongsidestore_mediain the router.TableCellRange—readers_office.read_xlsxnow writes ONE locus per worksheet spanning its real used range (row_end/col_endfrom the same row/column extent_format_rowswas already iterating over) rather than one per cell/row — a large sheet will otherwise mean thousands of writes for a single ingest pass, an unreasonable multiple of the sheet's own read. The.csv/.tsvreader (owned byextraction/readers.py, notreaders_office.py) was left unwired this pass — same shape, not yet done.ImageRegion—readers_media._ocr_with_rapidocrnow writes one locus per recognised text line, the quadrilateral box RapidOCR already returns collapsed to the axis-aligned rectangle the locus expects (min/max over the same points, no new detection). Scoped to the RapidOCR branch only —_ocr_with_pytesseract'simage_to_stringcall never computes a box at all, so there is nothing to wire there without adding new computation (fabricating one will violate this seam's own "no invented data" charter).
Producer wiring — 2026-07-22 pass 2 (five more closed; 10/11 total)¶
Pass 2 closed every locus pass 1 left unwired except the two with no real
underlying capability in AU (VideoShot/VideoFrameRange — see below), and
also fixed PageBox itself (found to have zero live callers despite being
this seam's original template).
PageBox—readers_office.read_pptxnow writes one locus per slide, boxed toPresentation.slide_width/slide_height(the deck's real EMU dimensions, the same "whole known extent" conventionTableCellRange's producer established) — not a per-shape box, whichpython-pptxdoesn't expose without walking every shape's own layout individually.MetricWindow— closed alongside a SEPARATE pre-existing gap pass 1 found:observability/health.py's anomaly kernel (compute_baseline/detect_anomaly/HealthTrendBuffer) andhealth_ingest. ingest_health_anomalyhad zero live AU callers anywhere — a fully-built, unwired kernel. Newobservability/gateway_health.pywires BOTH gaps with ONE real, already-computed signal:GatewayMetricsMiddlewarealready measures per-request latency for its Prometheus histogram (gateway_metrics.py) and now also feeds it into aHealthTrendBuffer. Request durations distill into one 5-minute trend window (never a per-request write — the same volume discipline the audit asked for); whendetect_anomalyflags a window against the gateway's own rolling baseline,ingest_health_anomalywrites the:HealthAnomalynode ANDstore_metric_window_evidencewrites the triggering window itself (data = json.dumps(trend)) as its evidence,metric="gateway: request_duration_seconds",start_ms/end_msfrom the buffer's own real min/max sample timestamps (a new additivestart_at/end_atonHealthTrendBuffer._flush, not fabricated — genuinely the first/last sample time in that flush).CodeSymbol/TraceSpan— pass 1 declined both for the same reason: their candidate producers (enrichment/extractors/code_test.py's AST mapper;harness/trace_backend.py::KGTraceBackend.record_event) sit on genuinely HOT paths (every symbol across the fleet; every span/generation in the harness) with noclaim_id, and named the fix as "a claim actually cites this symbol/span" — a trigger that didn't exist as a call site yet. Pass 2 built it:research/candidate_insight.py'sregister_claim_materializationis THE one shared seam every real, floor-cleared:Claimfrom every finding family (association rule / anomaly / predicted edge / sequential pattern / ops-causal root cause / placement proposal) already passes through on persist. Two new functions hook it:_persist_code_symbol_evidenceresolves each of a claim'ssource_idsagainst the engine's stored:Code/:Testnodes (the samecode:<file_path>::<name>/test:<file_path>::<name>id conventioncode_test.pymints) and, on a hit, writes aCodeSymbollocus whosedatais the real text of the symbol's known start line read back from disk (no fabricatedend_line— the stored node doesn't track one, and a second language-specific re-parse to recover it is exactly the kind of new computation this module's own docstring says the Rust engine, not Python, should own);_persist_trace_span_evidenceresolves eachsource_idagainst aSpanNode/GenerationNode(matched by the node's ownnode_typeproperty, since span/generation ids carry no distinguishing prefix) and writes aTraceSpanlocus whosedatais the node's own already-stored properties, serialized. Both are genuinely reachable: an ops-causal root-cause finding's causal path (enrichment/ ops_causal_graph.py) runs FROM an ingested Trace/Generation THROUGH agent/tool/model/service/deploy, and agraph_learn-minedPredictedEdgefinding can legitimately connect two code-symbol nodes — sosource_idsciting either kind is a real, not hypothetical, case. A claim citing neither kind writes nothing extra, exactly preserving every existing caller's behavior.RowVersion—DatabaseConnector.poll()genuinely lacked both required fields, confirmed by re-reading the connector in full:tableis unknowable in general (the connector wraps an arbitrarySELECT, potentially a join/view/aggregate — inferring one from the query text risks misattributing a multi-table read, worse than not writing at all), andupdated_fieldis documented as EITHER a timestamp OR an incrementing id, so it is not reliably aversionint. Pass 2's remedy is "plumb through what's real, never invent": a new optionaltable: str = ""config field lets the OPERATOR — who alone knows what their ownqueryreads — supply it explicitly (unset by default, so nothing is guessed);versionisint(updated_field's value), which succeeds only when that column genuinely is numeric (a real, already-fetched value, not a new computation) and cleanly no-ops for a timestamp. Withtableunset, the locus stays unwired for that source — an honest, config-gated closure rather than a forced one.
Still not wired, re-surveyed this pass:
VideoShot/VideoFrameRange— unchanged, re-confirmed: no shot-detection or frame-accurate video-processing path exists anywhere in AU (grepforffmpeg/cv2/VideoFileClip/moviepy/scenedetectacrossagent_utilities/returns nothing outside generated protocol schemas);tools/media_tools.py'sgenerate_video()only generates video, it does not analyze one;harness/optimization_backend.py's_evidence_address(modality="video")is a SYNTHETIC placeholder locus generator for the training harness's ephemeral program-example rows ({"start_frame": 0, "end_frame": 0}unconditionally) — not a real extractor, and not something to repurpose as one. What building this will actually require: a new video-decode dependency (opencv-python/PyAV/ffmpegsubprocess) plus either a shot-boundary-detection algorithm (histogram/perceptual-hash frame-diffing, or a vendored detector likePySceneDetect) forVideoShot, or exact frame-accurate seeking forVideoFrameRange— genuinely new capability, not a wiring gap, and explicitly out of scope for this seam-closure pass per the task's own instruction not to build a video pipeline. W4.6 (2026-07-24) declares — but does not implement — the target contract:agent_utilities/media/ sidecar_contract.py'sSIDECAR_CAPABILITIES['video']names the future producer (adata-science-mcpkeyframe/shot-boundary sidecar) and its wire shape, so the capability that's missing is now a documented contract stub, not an open question — see the section below andplans/_archive/au-eg-program/issue-register.md's W4.6 entry (a workspace-level planning archive outside this repo, not this repo's ownreports/). GOC-07 (2026-08-16) wires the adapter (agent_utilities/media/video_sidecar.py) against this same declared contract — see the updated table rows above and the "GOC-07" section below. This still does NOT add an in-AU video-decode capability; the fleet sidecar'svideo_keyframes/extract_keyframesaction itself remains not-yet-live (proven with the fleet call mocked at the seam, same posture PDF/JPEG shipped under in W4.6).
The governed media-sidecar pattern (W4.6, CONCEPT:AU-KG.ingest.media-sidecar-delegation)¶
reports/wave4/ADR-media-sidecar.md (HG-7) drew the engine's permanent
boundary: heavy media decode (OCR, JPEG, MP3/AAC, H.264/VP9, Whisper) never
runs in-engine or in AU's own Python process — it runs in a governed fleet
delegate agent, reached the same way graph_mine_deep already reaches
data-science-mcp for torch-dependent mining (CONCEPT:AU-KG.mining.dsm-forecast-delegation). W4.6
standardizes that shape into ONE reusable component,
agent_utilities/media/sidecar_delegate.py::delegate_extract, so every
modality adapter (pdf_sidecar.py, image_sidecar.py, audio_sidecar.py,
video_sidecar.py — the last two landed in GOC-07) shares one
fleet-call/decode/provenance loop instead of reimplementing it:
flowchart LR
subgraph AU["agent-utilities (Python)"]
MCP["graph_media_sidecar MCP/REST tool"]
ADAPT["pdf_sidecar.py / image_sidecar.py\n(modality write-back mapping)"]
DEL["sidecar_delegate.delegate_extract\n(ONE reusable fleet-call loop)"]
CAP["sidecar_contract.py\nfail-closed capability manifest"]
MS["MediaStore\n(EXISTING ArtifactBundle/EvidenceLocus API)"]
MCP --> ADAPT --> DEL
DEL --> CAP
ADAPT --> MS
end
DEL -- "call_tool_once\n(digest + media_type + artifact_b64)" --> SC["Fleet sidecar\n(stirlingpdf-mcp / data-science-mcp / ...)"]
SC -- "decoded pages/regions/pHash/embeddings" --> DEL
MS --> EV[":Evidence loci\n(PageBox/DocumentSpan/ImageRegion)"]
ADAPT -- "record_media_sidecar_claim" --> CL[":Claim\n(confidence=1.0, is_verified=True)"]
EV -- "SUPPORTS" --> CL
DEL -- "record_media_sidecar_activity" --> ACT[":PROVENANCE_ACTIVITY"]
subgraph EG["epistemic-graph engine (Rust) — unchanged"]
BGV["BeliefGraph::from_graph_view()"]
end
EV -.decoded by.-> BGV
CL -.decoded by.-> BGV
Governance closes the loop the same way the page-box seam above does — no
second resolver, no new engine write endpoint: delegate_extract records
ONE PROV-O :PROVENANCE_ACTIVITY node per fleet call
(lineage.record_media_sidecar_activity), the adapter records ONE
directly-checked :Claim per artifact processed
(lineage.record_media_sidecar_claim — confidence=1.0/is_checked=True,
following record_connector_sync_claim's established convention, NOT the
governed mining-flywheel lifecycle reserved for inferred findings), and
every per-locus store_<locus>_evidence write-back links that claim via
claim_id, so evidence_citations's existing SUPPORTS-walk makes a
sidecar-produced locus challengeable through the standard why/why-not
machinery with zero engine-side change. The capability manifest
(sidecar_contract.SIDECAR_CAPABILITIES) is a fail-closed table of which
sidecar may produce which locus kind — assert_capable/is_capable gate
every write-back, so a provider not declared for a locus kind (e.g. the
paperless-ngx-mcp PDF alternate, which has no word-box detail) writes none
rather than guessing.
Shipped W4.6: PDF (via stirlingpdf-mcp, default) and JPEG (via
data-science-mcp) — see the table above and
plans/_archive/au-eg-program/issue-register.md's W4.6 entry (workspace-level
planning archive, outside this repo) for the exact locus/producer mapping.
Shipped GOC-07 (2026-08-16): audio (via
audio-transcriber-mcp, default) and video (via data-science-mcp),
completing all four modalities sidecar_contract.SIDECAR_CAPABILITIES
declares — STUB_MODALITIES is now empty. All four sidecar tool actions
(ocr_pdf, analyze_image, transcribe_segments, extract_keyframes)
remain not-yet-live in the fleet as of this wave; every adapter is proven
the same way — AU-side contract + write-back with the fleet call mocked at
the seam, per the ADR's own testing directive.
What remains for full convergence¶
- All 12 loci now have both a tested
MediaStoremethod and a live AU producer call (GOC-07 closed the last two,VideoShot/VideoFrameRange, via the sidecar-delegate path). What remains is fleet-side, not AU-side: none of the four sidecar tool actions (stirlingpdf-mcp'socr_pdf,data-science-mcp'sanalyze_imageandvideo_keyframes,audio-transcriber-mcp'stranscribe_media) are live yet — each adapter's own conformance test proves the AU-side contract + write-back with the fleet call mocked at the seam, per the ADR's testing directive, but no locus of any of the four sidecar-delegated modalities has actually round-tripped through a real fleet service. - No live-engine round-trip test in AU's own suite.
evidence-graphis an opt-in, non-default Cargo feature (not folded into any tier, includingfull/default) — AU's shared ephemeral-engine test fixture (tests/_test_engine.py,tiny_engine/engine_graph) does not build or probe for it, so there is nopytest.mark.enginetest in AU assertingclient.query.explain_evidence(...)against a REAL running server for this seam yet. The EG-side Rust test is the closest thing to that proof today (it runs the actual engine decode/resolve code, just not over the wire). Standing up a dedicatedevidence-graph-featured test engine (or foldingevidence-graphinto a test-only tier) is the natural follow-up. - Historical note (since resolved): this bullet used to flag
tests/_test_engine.py'sBUILD_TIER = "pi-max"fallback build path as stale drift relative to epistemic-graph's own tier retirement (EG-371:pi/pi-max/nodeno longer exist as Cargo features —fullisdefaultnow, and the engine-selection axis at the deployment-config layer isengine_topology, not a tier). That fallback path has since been removed entirely —resolve_engine_binary()now resolves only an explicitEPISTEMIC_GRAPH_TEST_BINARYoverride or the mandatoryepistemic-graph[full]wheel binary, with no build-tier axis at all. Nothing here blocks the live-engine test above.
W3.3 correction — the wire contract itself, and a twelfth locus kind¶
The bug. Every producer this doc describes above was, until this pass,
writing to the WRONG property (evidence_span) in an informal, non-governed
shape — see the "Correction" note under "The gap this closes". The engine
never reads evidence_span; decode_locus/BeliefGraph::from_graph_view
read evidence_locus exclusively (grep-confirmed against
crates/eg-epistemic/src/{evidence.rs,adapter.rs} — zero matches for
evidence_span anywhere in the engine). So despite this doc's own prior claim
of a proven round trip, zero of the "10/11 wired" loci were actually citable
through Method::ExplainEvidence — every MediaStore unit test passed only
because it asserted on the same wrong key/shape the code wrote, never against
the engine's real decoder. MediaStore._governed_locus/_opaque_hex now
build the correct governed shape; test_governed_locus_matches_the_engine_
contract pins it against hardcoded literals (not the helper compared to
itself) so this class of bug cannot recur silently.
A twelfth EvidenceAddress kind. The engine's EvidenceAddress enum
(crates/eg-modality/src/artifact.rs) now has 12 variants, not 11: Point
(mapped to eg-program's ProgramModality::Spatial) was added after this
seam's 11-locus scope was set and after the audit this program's W3.3 task
cites. No AU producer targets Point — out of scope for W3.3 (a spatial/geo
locus, not one of the named table/code/row/metric/trace kinds) — logged as
entry W3.3-2 in plans/_archive/au-eg-program/issue-register.md
(workspace-level planning archive, outside this repo) as a scope note, not a
defect.
Media-decode loci deferred to W4.6. PageRegion/ImageRegion/
AudioRange already have live AU producers (readers_office/readers_media/
messaging router) predating this pass; VideoTimeRange/FrameRange were
unwired at the time of this pass (no video-decode capability in AU, as this
doc's "Still not wired" section above documented) and paired with the W4.6
media-sidecar program rather than being wired here — GOC-07 later wired
both via that program's sidecar-delegate path (see above).