Epistemic OS Hardening (2.20.0) — the detailed capability catalog¶
Purpose of this page. This is the code-verified, line-anchored catalog of every capability the "Epistemic OS Hardening" program (Phase 0 → Phase 3 + the "exceed" tracks) shipped into
epistemic-graph, culminating in 2.20.0. It exists so a future static audit (the kind that spawned this program — see theCHANGELOG.md## [2.20.0]entry for the release-note summary anddocs/capabilities.mdfor the operation-by-operation parity matrix) can read what is actually true of the shipped code without re-deriving it from source. Where the program's original intent and the shipped reality diverge, this page documents the shipped reality — every claim below was checked againstcrates/andsrc/at the commit this page was written against (Cargo.tomlversion = "2.20.0"), not against the design doc or the CHANGELOG prose.How this page relates to the other capability docs (read this before citing any of them):
docs/capabilities.generated.mdis the machine-checked ledger — one row per wireMethod, generated bycargo run -p eg-capabilities --bin gen_ledgerfrom the exhaustivepolicy()match incrates/eg-capabilities/src/lib.rs. It is authoritative for mutates / durability-domain / authz-action / idempotent / audited / emits-CDC / txn-participation facts, per method.docs/capabilities.mdis the hand-maintained, per-surface parity matrix (SQL / SPARQL / Cypher / GraphQL / vector / …) — coarser-grained, answers "which surface supports which operation", not per-method security semantics.- This page is the per-capability, program-narrative catalog: for each thing the hardening program built, what it is, where it lives, whether it is default-on or opt-in (and the exact feature name/env var), how to exercise it, and its honest limitations. It goes deeper than the CHANGELOG's release-note prose and cross-links into the other two rather than duplicating their tables.
- The forward-looking companion is
plans/epistemic-os-evolution-roadmap-2026-07-11.md— a workspace-level file (outside this repo, so not a clickable link from this site) — where the architecture goes next (unifyingKnowledgeBatchas the end-to-end currency, closing the placement/TMS control loops, proving scale). This page is the "where we landed" half; that file is the "where we go" half.
Verification discipline for this page: every code anchor below (file path, item name, env var, feature name, exact count) was grep/read-confirmed in this worktree. Where the program's own tracker or the CHANGELOG stated an approximate number (e.g. "~80 routed methods"), this page states the exact number found in code today and notes the discrepancy. Anything I could not independently confirm is called out explicitly in Honest gaps and unverifiable claims rather than asserted.
Phase 0 — Trustworthy core¶
Phase 0 (shipped in 2.19.0, with one closeout item — the mutation-gateway rollout — completed in 2.20.0) makes the engine's own mutation/durability/security machinery internally self-consistent and machine-checked, rather than relying on hand-maintained classifiers agreeing with each other by convention.
The generated capability ledger (eg-capabilities)¶
- What it is: an exhaustive, compiler-enforced
MethodPolicyfor every variant of the wire-protocolMethodenum (crates/eg-types/src/protocol.rs). The cratecrates/eg-capabilities(workspace member,crates/eg-capabilities/src/lib.rs) declares:Becausepub struct MethodPolicy { pub mutates: bool, pub durability_domain: DurabilityDomain, // GraphRedb | SeriesRedb | KvRedb | JobsRedb | BlobRedb | Outbox | None pub authz_action: &'static str, // "<domain>:<verb>" scope string pub idempotent: bool, pub audited: bool, pub emits_cdc: bool, pub txn_participation: TxnParticipation, // Atomic | Saga | Snapshot | None } pub fn policy(m: &Method) -> MethodPolicy { match m { /* one arm per variant, no wildcard */ } }policy()has no wildcard arm, adding aMethodvariant without adding its policy arm is a compile error ("non-exhaustive patterns") — that failure is the guarantee (CONCEPT:EG-P0-1). - Exact count (verified):
crates/eg-capabilities/tests/consistency.rsassertsseen.len() == expectedwhereexpectedis 343 Method variants in the base build, 344 when the opt-injobsfeature addsMethod::AnalyticsJob. (The program tracker's earlier "337 Methods" figure predates the X-1/EPI-P3-3 facade wiring, which added 3 more —ExplainEvidence/CausalEstimate/RankByProvenance.) - Default-on or opt-in: the crate is a workspace member but is not linked
independently — it is pulled in only via the facade's
serverfeature (server = ["dep:tokio", "dep:clap", "dep:tracing-subscriber", "dep:async-trait", "dep:eg-capabilities"],Cargo.toml), because the one consumer (the mutation gateway, below) is server-only. - How to exercise it:
cargo run -p eg-capabilities --bin gen_ledgerregeneratesdocs/capabilities.generated.mdfrom the livepolicy()match (crates/eg-capabilities/src/bin/gen_ledger.rs). - Honest limitation:
authz_action,idempotent, andtxn_participationare judgment calls — the crate's own doc comment onMethodPolicysays so plainly: there is no pre-existing classifier in the codebase to cross-check them against, unlikemutates/durability_domain(cross-checked againstaccess.rs/wal.rs) andaudited/emits_cdc(cross-checked againstaudit.rs/cdc.rs).
The MutationPlan commit gateway (src/server/mutation.rs)¶
- What it is: the single place a routed mutation's authz + durable write + CDC
emission happen together, driven by
eg_capabilities::policy()rather than a re-hardcoded classifier.MutationPlanis populated FROM the ledger;commit_mutation(and, for the runtime-conditional family,commit_conditional_mutation/commit_conditional_mutation_async) is the one call site. - Exact rollout state (verified in code, not just the CHANGELOG's "~80"):
GATEWAY_ROUTED: &[&str]— 86 method names (grew from the initial 7:AddNode/RemoveNode/AddEdge/RemoveEdge/CreateSummaryNode/Consolidate/Reinforce, through the plain graph-core CRUD family, the broker/stream family (Outboxdomain,feature = "broker"), both runtime-conditional families (Mine*/GraphLearn*viacommit_conditional_mutation), and — the final L11 batch —Sql/CypherQuery/GraphQlviacommit_conditional_mutation_asyncplusAddTriples/RemoveTriples/DropNamedGraphat the RDF dispatch site).JUSTIFIED_NA: &[(&str, &str)]— 57 entries, each a(method, reason)pair: a different commit protocol (Txn*/Commit/RollbackOCC family), a non-graph- scoped store (Blob*/Kv*/TsAppend— each self-routes to its own durability domain beforedispatch_graph_op), a cluster-wide/cross-shard admin action (Reshard/CatalogAssign/…), a registry-lifecycle op (CreateGraph/DeleteGraph), or a process-global registry (RbacAdmin/channels/CDC triggers/matviews).OPEN_NOT_JUSTIFIED: &[(&str, &str)] = &[]— machine-checked empty via the testmutation::tests::gateway_routed_set_matches_mutating_policy_surface: every mutating method (per the ledger) must be in exactly one ofGATEWAY_ROUTEDorJUSTIFIED_NA, or the test fails. A future mutating method that is routable but unrouted is therefore a hard test failure, not a silent gap.RunRuleswas found, during this rollout, to be a policy correction, not a route:handle_run_rulesreasons over an off-lock snapshot and returns inferred triples with no writeback (unlike its siblingRunDatalogReasoning, which materializes in-place) — the ledger'smutates: trueguess for it was wrong and was corrected rather than the method being routed.- What the gateway does NOT reimplement: the tamper-evident hash-chain audit log
(
src/audit.rs+redb_store::append_audit_entry) stays inside the durable-commit path (PersistenceBackend::record/record_durable) — the gateway delegates to it rather than risking a second, diverging chain. - Coalescer interaction (L18, closed): step 4 of
commit_mutationroutes a coalescable routed mutation (AddNode/RemoveNode/AddEdge/RemoveEdge) through the sameWriteCoalescerRegistry::writer_forpath the legacy dispatch shell used, so the hot-path structural writes still batch — this was a real scale regression (found by the full--libsuite, not a filtered subset) that was fixed by re-entering the coalescer from inside the gateway rather than bypassing it. - Default-on or opt-in: not feature-gated separately — it is part of the
serverbuild (every server binary links it); there is no way to disable the gateway short of building withoutserver. - Exercise it:
cargo test -p epistemic-graph --lib mutation::(in-crate); the cross-check test builds a realRedbBackendand reads the audit chain back.
WAL durability closure (src/wal.rs)¶
- What it is:
pub fn is_durable_mutation(m: &Method) -> bool— the classifier that decides whether a mutating method's effect is WAL-logged (surviveskill -9). EG-P0-3 added durability arms forAddEmbedding(unconditional) and four writeback-conditional mining methods:MineSequence/MineForecast(any writeback),MineText(onlylda/nmf, nevertfidf),MineSubgraph(onlygspan) — matchingaccess.rs::requires_write's exact conditions byte-for-byte. - Exact remaining gap (verified against
docs/capabilities.generated.md, not just restated): 9 methods, each individually documented as recomputable / bulk-restore-only / a CRDT merge (not an ordinary silent-data-loss risk), but still an open WAL gap for a crash between checkpoints:FromMsgpack,ClearLedger,ApplyLedger,CompactNodesByType,RunDatalogReasoning,Reconcile,ApplyMutation,ApplyMultisigMutation,IcvConfigure. (Down from 18/23 before EG-P0-3 + theRunRulespolicy correction.) - Real bug found+fixed along the way (EG-P0-3): WAL replay for
AddEmbeddingmustmark_dirty/re-apply the writeback, or the node stays hidden post-restart — a genuine post-restart visibility bug, not a hypothetical. - Default-on: part of the
serverbuild; not separately feature-gated.
Canonical time-series store (EG-P0-4)¶
- What it is: unifies two previously-separate redb files (
graph.redbandseries.redb) that could not share aWriteTransaction(exclusive-lock conflict). The fix is not a single-file merge — it is a documented two-hop write with a reconciliation pass closing the gap: - Graph + vector + blob-ref + measurement (+ lowered axiom/CONSTRUCT/plan-writeback)
land in one
graph.redbWriteTransaction(redb_store.rs::commit_crossmodal) — that set is the true cross-modal ACID boundary. - The measurement is then replayed into the served
series.redb(state.tsdb_store) immediately after step 1 commits, soTsRange/TsAsofJoin/TsWindow/TsGapFill/UQLOp::TsScansee it post-commit and post-restart. - This replay is a separate, non-atomic redb write on a different file: a crash
strictly between the two commits leaves the measurement durable in
graph.redbbut not yet reflected inseries.redb. L16 closes this window:RedbBackend:: reconcile_time_seriesruns once at boot, scans every shard'sgraph.redbSERIES tables, and replays intoseries.redbany series whose point count hasn't converged — an exact multiset diff, so it is idempotent and never duplicates a point. - Process note (found during the ledger-closeout wave): the Phase-0 integration pass initially missed merging this workstream's branch to main (additive, so build gates passed regardless of the miss) — it was caught later by the L16 agent and restored. Worth remembering when auditing "is X really on main": additive changes can pass every gate while silently absent.
- Default-on: part of the
redb-backed durable server; not a separate feature flag.
The opt-in v1 signed request envelope (src/server/auth.rs)¶
- What it is: two authentication modes coexist:
- v0 (legacy, default) —
compute_auth_token/verify_auth: a plain HMAC-SHA256 hex digest. Every existing client still speaks this, and it remains the default. - v1 (opt-in,
eg1.-prefixed token) — binds version/audience/tenant/principal/ graph/method/body-hash/timestamp/nonce/idempotency into one canonical byte layout (eg_types::protocol::build_envelope_v1_bytes, pure data, no crypto dep), HMAC'd by both the signer (eg-plan'sRemoteEngineSource::auth_token_v1) and the verifier (verify_envelope_v1, constant-time MAC compare) from the same function. - Exact env vars (verified in code):
EPISTEMIC_GRAPH_REQUIRE_SIGNED— read once viarequire_signed_envelope()(src/server/auth.rs:96). Defaultfalse: a v0 request is accepted with a warning log unless this is set, at which pointverify_request's v0 branch (verify_legacy_request) hard-rejects it.EPISTEMIC_GRAPH_ENVELOPE_SKEW_SECS— the allowed clock-skew window for a v1 envelope'stimestamp; also doubles as the replay-nonce cache TTL (src/server/auth.rs:113).- Request routing:
verify_request(src/server/auth.rs:324) detects v0 vs v1 by theeg1.prefix heuristic on the wireauth_tokenfield — a legacy hex digest is never mistaken for a v1 envelope or vice versa. - Federation note (open follow-up, L12): a v1
auth_token_v1signer exists on the federation client side but is not yet wired intofetch_uql/fetch_cypher— federation calls still use v0-shaped auth today. - Default-on or opt-in: opt-in — a fresh deployment is byte-for-byte unchanged
from every pre-EG-P0-5 deployment until
EPISTEMIC_GRAPH_REQUIRE_SIGNED=1is set.
Default-deny RLS (crates/eg-core/src/isolation.rs) — WS-1b (2026-07-12): flipped secure-by-default¶
- What it is:
IsolationLayercarries arls_default_deny: boolfield (crates/eg-core/src/isolation.rs). The struct-level builder default (IsolationLayer::new()) staysfalse(the permissive posture: an unowned, undecodable, or untagged-legacy row stays visible to all agents) — that is the bare library default for a caller (tests/harnesses/the embedded in-process API) that constructs anIsolationLayerdirectly with no env resolution of its own. Setting ittrue(set_rls_default_deny/with_rls_default_deny) switchescan_see_rowto the strict posture: such a row is denied unless explicitly_visibility: "public"or_owner-tagged. - Exact env var:
EPISTEMIC_GRAPH_RLS_DEFAULT_DENY, resolved once at server startup (src/main.rs) via the pure, unit-testedeg_core::isolation::resolve_rls_default_denyand wired into theIsolationLayerthe server constructs. - Default-on or opt-in: default-on (strict) as of WS-1b, opt-out available. The
env var's own default flipped from permissive to strict: an unset
EPISTEMIC_GRAPH_RLS_DEFAULT_DENYnow resolves totrue(secure-by-default) for a fresh/greenfield deployment, per this repo's no-back-compat policy (there is no external caller pinned to the old posture). A deployment that still wants the permissive/back-compat posture opts out explicitly withEPISTEMIC_GRAPH_RLS_DEFAULT_DENY=0(false/no/offalso accepted, case-insensitive); any other value — including a typo — resolves to strict (fails closed). Migration note for an existing deployment upgrading past this change: legacy unowned/untagged rows become invisible to non-Systemagents until backfilled with an_owneror an explicit_visibility: "public"tag, or until the deployment sets the env var to0to keep the old permissive posture.
Exhaustive audit + admin scopes (EG-P0-6)¶
- 8 broker/stream mutating ops previously classified as read in
access.rs(StreamDeclare/Publish/Trim/CommitOffset,PublishConfirmed,BrokerAck/NackTag,PublishIdempotent) were a real security gap (L10) — a read-only caller could invoke them — and are now correctly classified as writes. - Admin scoping is driven off the ledger's
authz_actionfield, checked once indispatch_inner— covering every existing admin method and, by construction, every future one the ledger declares. - A 64-method exhaustive audit surface backs
audit.rs's coverage.
Phase 1 — Universal modality & work contracts¶
eg-modality runtime registry + the 12-point TCK¶
- The trait (
crates/eg-modality/src/contract.rs):ModalityContracthas 4 core methods every implementer must answer (storage_kind,to_rowset,txn_stage,cdc_topic), 4 default-empty methods overridden only where meaningful (provenance,evidence,policy_labels,analytics_ops), and — added by EG-P1-1 — 4 more default-"unsupported" hooks that make the TCK genuinely 12-for-12 (ingest_report,storage_stats,backup_selfcheck,recovery_selfcheck), plus one honesty escape hatch,tck_not_applicable(point) -> Option<&'static str>, for a point that genuinely does not apply to a given modality's nature (distinct from "not yet implemented"). - The 12 TCK points (
crates/eg-modality/src/tck.rs,TckPoint::ALL):SchemaAndIds,IngestStreaming,CodecUnsupportedFormat,StorageIndexStats,TypedQueryOperators,TxnOrSagaOutbox,CdcDeleteRetentionGc,TenantRowRegionPolicy,ProvenanceEvidenceLineage,BackupRestoreMigrateRecover,SingleNodeFailure,InteropWorkloadSmoke. Each point resolves to one of three honestTckStatusvalues:Pass,NotApplicable(reason)(counts as first-class — not a gap), orNotImplemented(reason)— there is deliberately no silent-skip / default-pass status. - The registry:
crate::register_modality/registered_modalities()(crates/eg-modality/src/registry.rs) is aOnceLock<Mutex<Vec<ModalityDescriptor>>>— a deliberate choice overlinkme/inventory(neither is a workspace dependency anywhere; both would be the first proc-macro dependencyeg-modalitypulls in, which is meant to stay the thinnest possible seam). Important nuance, verified in the registry module's own doc comment:register_modality()is called from inside the#[cfg(test)]conformance-test module themodality_conformance_tests!macro generates — soregistered_modalities()only populates when that crate's own test binary actually runs its conformance suite. This is a per-process, test-time registry, not a live inventory a running server process can query ("Cross-process caveat" in the crate's own README). A future whole-binary auto-discovery seam (revisitinglinkme) is explicitly left as a follow-on, not built. - The macro:
modality_conformance_tests!($T)(exported fromeg-modality) generates the full battery per implementer: round-trip losslessness, rollback symmetry, provenance-family non-panic,cdc_topicwell-formedness, a malformed-payload decode-as-Errcheck,analytics_ops()well-formedness, and the TCK-report-generation - registration test. Invoked inside each crate's own
#[cfg(feature = "contract")]module (opt-in, default off — a conformance/testing seam, not a serving feature). - Exact adoption count (grep-verified, not just restated from the CHANGELOG's "16"):
19 crates currently invoke
modality_conformance_tests!(each behind its owncontractfeature):eg-core,eg-ann,eg-geo,eg-tensor,eg-stream,eg-rdf,eg-epistemic,eg-text,eg-shacl,eg-shex,eg-lake,eg-kvcache,eg-numeric,eg-image,eg-audio,eg-video,eg-document,eg-compute, andeg-modalityitself (which defines the macro and dogfoods it). Every one of these crates' owncontract = ["dep:eg-modality", …]feature is off by default.
Arrow-backed KnowledgeBatch (eg-plan/knowledge_batch.rs)¶
- What it is: a third
RowSetprojection (alongside the row-orientedRowSetand the graph-nativeKnowledgeSet) laying results out as a real ArrowRecordBatch(crates/eg-plan/src/knowledge_batch.rs). Columns (arrow_schema()/to_record_batch()/from_record_batch(), round-trip tested):id(Utf8),kind(Utf8), onescore_<name>(Float32) per named score,confidence(Float64),evidence_kind(Utf8 — a filterable summary spanning all 11EvidenceSpanvariants, see X-1 below), bitemporalvalid_from/valid_until/tx_from/tx_to(Int64), plus list-typed provenance/policy/evidence-ref/contradiction/proof/transformation/alternative-id columns and a lazyblob_handle(Utf8) +has_payload(Boolean). - Reserved epistemic columns — populated, not stub (L22 closure):
contradiction_ids(symmetric, viaAuxEdgeIndex),proof_ids(viaexplain_belief), andtransformation_ids(viaGENERATED_BY→:Activityedges) are genuinely populated on the write-back path across 18 mining families pluseg-jobs. Remaining gap: noALTERNATIVE_TOproducer yet (the column is read-wired but nothing writes it), and the calibration column is wired but always null (no calibrated producer feeds it yet). - Default-on or opt-in: feature
knowledge-batch = ["query", "dep:arrow"](crates/eg-plan/Cargo.toml:235) — opt-in, not folded intofull. Afullbuild links no Arrow via this path (Arrow only entersfulltransitively through other features likelake, which is itself opt-in — see below). RowSet/KnowledgeSetthemselves are unchanged by this addition — it is a parallel, opt-in third projection, not a replacement.
eg-document / eg-image / eg-audio / eg-video + eg-alignment (EG-P1-3)¶
- What it is: five new leaf crates —
eg-document(pages → layout-blocks → tables → spans typed model,decoder.rs/header.rs/document.rs/contract.rs),eg-image/eg-audio/eg-video(header-parse + region/segment/shot evidence types,runtime.rs/header.rs/{image,audio,video}.rs/contract.rseach), andeg-alignment(the sharedEvidenceResolvertrait,crates/eg-alignment/src/ resolver.rs:47, + agraph.rsalignment graph). Each of the four data-model crates implementsModalityContract(unit-tested, registered in the EG-P1-1 TCK via its owncontractfeature). - Honest status — verified, matches the CHANGELOG's own framing: none of these
four is folded into any serving tier (pi/node/cluster/full) — they compile,
unit-test, and register into the TCK, but there is no
Method/dispatch path that a deployed server exposes for them today. This is a capability-discovery/testing seam, explicitly distinct from every ✅ row elsewhere indocs/capabilities.md. - The one facade-reachable piece:
CasEvidenceResolver(src/server/blob/cas_resolver.rs:52, implementseg_alignment::EvidenceResolver). Gated by thealignmentfeature (alignment = ["blob", "dep:eg-alignment", "dep:eg-modality"], Cargo.toml — off by default, not folded into any tier). It resolves aDocumentSpan/TableCellRangelocus to a real UTF-8 excerpt read off the engine's own blob CAS (ChunkStore/stream_blob_get, ablob_refproperty looked up in aGraphViewsnapshot); every other locus kind (image/audio/video/code/trace — no in-tree codec to crop/slice pixels or samples with) resolves to a real CAS-digest reference, never a fabricated excerpt. - A second, parallel resolver exists inside
eg-epistemicitself, crate-internal, gated by the facadeevidence-graphfeature (notalignment) — see X-1 below. The two are not unified; there are genuinely two resolution paths for the sameEvidenceSpanshape today (alignment's blob-CAS-backed one, andevidence-graph'seg-epistemic-internal one) — this is exactly the "one evidence spine" gap the evolution roadmap's Seam 2 calls out as future work, not something this program closed.
Persistent index pushdown into the served planner (EG-P1-4)¶
- What it is: the served planner now binds directly to the maintained persistent
BM25 text index and the live SemanticStore (vector index) via a
ServedTextIndexdowncast seam overIndexManager, instead of rebuilding/cloning a snapshot copy of the whole index on everyUnifiedQuery/NlQueryrequest. - Adaptive re-optimization: cardinality-based cost re-optimization now runs
automatically mid-execution, gated by the same
EPISTEMIC_GRAPH_COST_OPTkill-switch (0→ identity/off) that already governed plan-time optimization. - Streaming cursor:
ChunkedKnowledgeCursoris a real chunk-at-a-time cursor, closing the prior positional stub (L23). - Documented backlog — NOT done in this pass:
- Vector was the one index type wired in this specific EG-P1-4 pass; spatial/R-tree
index pushdown landed separately, in a later closeout batch (L37 — see the "GIS
/ Spatial" section of
docs/capabilities.md,Op::SpatialScan/SpatialSource+GraphSpatialIndexincremental pushdown, confirmed shipped). ExplainPlan/ExplainProvenance/ExplainPolicydiagnostics still cloneSemanticStoreper call (not on the fast path this pass optimized).- The mining
RankTextleg still falls back to the snapshot text index. - The opt-in
par-runtimedriver does not run adaptive-reopt yet. - SQL cancellation is not wired end-to-end from the wire protocol in this pass (a
separate
CancelRequest/EPISTEMIC_GRAPH_SQL_REQUEST_TIMEOUT_MSmechanism was added in a later closeout — L36).
Phase 2 — Distributed planes¶
PlacementCatalog (src/raft/placement.rs)¶
- What it is: an epoch'd catalog (
PlacementCatalogstruct,src/raft/ placement.rs:217) tracking which(group, epoch)a graph — or a split partition-key range (split_tenant_key,PartitionStateenum) — currently belongs to. Online split/merge/move runs a prepare-then-fenced-cutover sequence:MultiRaft::placement_fence_cutover(src/raft/multi.rs:724) bumps the epoch so any caller still presenting a pre-cutover epoch is redirected, never served stale data. The catalog takes priority over the hash-ring router for any graph with an explicit placement entry (an unpinned graph still falls back to the ring). - Wire surface:
Method::PlacementRouteis always in the enum (pure serde, present in every build perdocs/capabilities.generated.md); the real answer needs theraft/clusterfeature plus a liveMultiRaftcluster, otherwise it returns a well-formed{"explicit": false}JSON — not an error. - Default-on or opt-in: gated
raft/cluster(opt-in layer stacked onfull, perdocs/architecture/tiers.md).
Multi-group production startup + cross-shard read fan-out (DIST-P2-2)¶
- Env var:
EPISTEMIC_GRAPH_RAFT_GROUPS(src/raft/config.rs:90,parse_groupsat:126), default1— unset/empty/"0"all collapse to the single-group path, byte-for-byte unchanged from before this workstream. - Cross-shard reads:
CrossShardReader::read_cross_shard(src/raft/xread.rs) fans a read across every Raft group a query's graphs resolve to (each leg routed throughPlacementCatalogahead of the ring, mirroring the write side) and merges the result; a single-group read is correctly never flagged cross-shard. - Proven by live harnesses (not just unit tests):
src/raft/xread_harness.rs,src/raft/xshard_harness.rs,src/raft/placement_harness.rs(a one-node/two-group live setup).
Lazy graph lifecycle + bounded hot-context cache (DIST-P2-3)¶
EPISTEMIC_GRAPH_LAZY_STARTUP(src/main.rs:1305) swaps the eagerload_allboot recovery for a catalog-only scan — a graph's identity is known at boot, but its node/edge data does not hydrate until first access (BackendGraphMaterializer::materialize,src/server/persistence/read_through.rs:98, implementingeg_core::registry::GraphMaterializer).EPISTEMIC_GRAPH_MAX_RESIDENT_GRAPHS(src/server/persistence/cold_offload.rs:193), default0= unbounded — caps how many graphs are simultaneously RESIDENT, evicting the coldest by last-access recency through the same durability-gated cold-offload hibernate path R6 already used.__commons__is never evicted (src/server/persistence/cold_offload.rs:133,161,222).- L38 "paged adjacency" — ✅ CLOSED (surpass-6mo WS-3, 2026-07-13). Was: "lazy"
meant when a graph's data loads (deferred past boot to first access), not
partial/paged loading —
BackendGraphMaterializer::materializecalledread_graph_material_blocking, which returned the whole graph's material in one shot, so first access to a lazily-opened graph still fully rehydrated it into memory. Now:eg_core::registry::GraphMaterializer::materialize_page(a seam that already existed, unit-tested in isolation, but was never called by the facade/ server) is wired end to end: EPISTEMIC_GRAPH_LAZY_OPEN_PAGE_SIZE(src/server/persistence/cold_offload.rs::lazy_open_page_size) — a positive value switchescold_offload::lazy_openfromGraphRegistry::open_lazy(full rehydrate) toGraphRegistry::open_lazy_paged: the graph is resident/ queryable after just ONE bounded page, with the rest paged in by a spawned background task (page_in_remaining) — a not-yet-paged node still resolves via the pre-existing per-node read-through in the interim.RedbBackend::read_graph_material_page_blocking(src/server/persistence/redb_backend.rs) overrides the trait's default (full-fetch-then-slice) with a genuinely SOURCE-bounded scan —redb_store::read_graph_dump_pageskips/takes a bounded window of the per-graphnodes/edgestable range directly, never collecting the whole graph's rows into memory first.BackendGraphMaterializer::materialize_page(src/server/persistence/read_through.rs) routes to it.- Tests:
cold_offload::admission_tests::open_lazy_paged_materializes_one_page_then_page_in_completes_deterministically(proves the first page is STRICTLY SMALLER than the full graph — the literal "no full rehydrate" assertion — then drivespage_into completion) +paged_lazy_open_wiring_is_resident_immediately_then_completes_in_background(thelazy_open/background-task wiring, over a real redb backend). - Known residual (documented, not load-bearing): a graph deleted and
recreated under the identical name WHILE a background page-in is still in
flight could replay a stale-cursor page against the new incarnation — closing
that fully needs a generation/epoch stamp on
GraphEntry, tracked as a follow-up, not part of L38 itself. - Default-on or opt-in: all three env vars (
EPISTEMIC_GRAPH_LAZY_STARTUP,EPISTEMIC_GRAPH_MAX_RESIDENT_GRAPHS,EPISTEMIC_GRAPH_LAZY_OPEN_PAGE_SIZE) default to the pre-existing eager/unbounded/full-rehydrate behavior; a small deployment is unaffected unless it opts in.
Durable analytics-job plane (eg-jobs, INT-P2-1)¶
- What it is:
Method::AnalyticsJob { op }(async submit/status/cancel/resume,src/server/handlers/jobs.rs) over a redb-backedAnalyticsJobstate machine (crates/eg-jobs/src/model.rs):pub struct AnalyticsJob { pub job_id: JobId, pub input_snapshot: InputSnapshotHandle, // pinned OCC version — re-readable via AS OF, never a row copy pub policy: JobPolicy, pub algo: AlgoVersion, // full algorithm/params/code-version lineage pub retry: RetryPolicy, pub state: JobState, pub cancel_requested: bool, pub created_at_ms: i64, pub updated_at_ms: i64, }result_ref()is a deterministic function of(input_snapshot, algo), notjob_id— two jobs (an original run and a retry under a freshjob_id) that ran the same algorithm+params over the same snapshot converge on the sameresult_ref, the basis for idempotent result-commit. The result-commit path writes the same:Claim/:Evidenceconventioneg-epistemicreads (see Phase 3). - Default-on or opt-in: feature
(
Cargo.toml) — off by default, deliberately not folded intofull(the same posture asalignment/eg-document: a narrow capability-discovery surface, not yet a serving-tier feature).
Arrow dataset-handle seam for external heavy compute (INT-P2-2)¶
- A
KnowledgeBatch-backed handle a job or foreign-compute leg can hand off over Arrow IPC without re-serializing through the wire protocol row-by-row. Featuredataset-handle = ["query", "server", "blob", "dep:arrow"]— opt-in, not infull. Follow-up not done here (L39): a real data-science-mcp Arrow client (pull IPC→compute→sign→POST result) and a resumable producer-side cursor are still open.
WAL → lake materialization + Iceberg-REST catalog + OpenLineage (INT-P2-3, lake)¶
- What it is:
LakeManagermaterialize/compact/delete runs emit a real OpenLineageRunEvent(src/server/lake/lineage.rs) — job/run/input-dataset/output-dataset with schema/datasource/output-statistics facets plus an engine-specific LSN/Iceberg-snapshot custom facet (schema URLhttps://openlineage.io/spec/1-0-5/OpenLineage.json#/$defs/RunEvent). - Optional push: env var
EPISTEMIC_GRAPH_OPENLINEAGE_URL(OPENLINEAGE_URL_ENVconstant,src/server/lake/lineage.rs:36) — unset means a silent no-op; lineage export never blocks or fails a materialization run. - Default-on or opt-in:
lake = ["server", "blob", "tsdb", "dep:eg-lake", "eg-lake/lake", "dep:ureq"]— opt-in, not infull(pulls arrow/parquet + delta/iceberg deps, kept out of the default build's dependency footprint). The Iceberg v2 Avro manifest/manifest-list writer (crates/eg-lake/src/iceberg_avro.rs, pure-Rustapache-avro) is real (per-column stats for predicate pushdown), not a stub.
Phase 3 — Epistemic differentiation (eg-epistemic)¶
eg-epistemic (crates/eg-epistemic/, ~5,760 lines across model.rs, propagate.rs,
tms.rs, recompute.rs, causal.rs, ranking.rs, redact.rs, query.rs,
evidence.rs, adapter.rs, contract.rs) is the epistemic layer. Claims/Evidence/
Sources are ordinary type-tagged graph nodes — no new persistence — and
Support/Contradict/Attack are ordinary edges; the base epistemic feature
(epistemic = ["query", "dep:eg-epistemic", "eg-types/epistemic", "dep:eg-plan",
"eg-plan/epistemic", "dep:eg-modality"], Cargo.toml) is folded into full (verified:
full = [… , "epistemic"] is the last entry in the full feature list) — so a standard
build always links the base epistemic substrate. Every deeper Phase-3 feature below
(epistemic-tms, epistemic-redaction, epistemic-causal, evidence-graph) was
originally layered ON TOP as its own opt-in feature, NOT folded into full. WS-1b
(2026-07-12) folded the LIGHT ones — epistemic-redaction and evidence-graph — into
full (neither pulls a new heavy dependency nor runs expensive recompute; see the
WS-1b note in Cargo.toml above the full definition). epistemic-tms and
epistemic-causal remain deliberately opt-in (HEAVY: NP-hard-in-the-worst-case Dung
argumentation search / genuine Pearl do-calculus computation) — a default full build
must never pay for or run their recompute paths.
Claim/Evidence/Source/BeliefState + confidence propagation¶
- Base
epistemicfeature (shipped 2.16.0, restated here as the foundation Phase 3 builds on).eg_epistemic::propagate_confidence(crates/eg-epistemic/src/ propagate.rs) does cycle-guarded Bayesian-conjugate confidence propagation over support/contradiction/attack edges.Method::ExplainBeliefreturns the full justification tree.
Paraconsistent TMS + Dung argumentation (tms.rs)¶
- What it is:
crates/eg-epistemic/src/tms.rs(687 lines) implements Dung abstract-argumentation semantics over aBeliefGraph:grounded_extension,preferred_extensions,stable_extensions,is_skeptically_accepted/is_credulously_accepted, plusretract(RetractionResult) for dependency-directed retraction. Paraconsistent means a contradiction is contained to the arguments actually in tension, never an explosion into "everything is now unbelieved." - Default-on or opt-in: gated
epistemic-tms(epistemic-tms = ["epistemic", "eg-epistemic/epistemic-tms"]) — opt-in, not infull.
Dependency-driven recompute + the CDC hook (recompute.rs + src/server/tms_hook.rs)¶
- The recompute engine (
crates/eg-epistemic/src/recompute.rs):TruthMaintenance—register(id, depends_on, …),on_change(&ChangeEvent) -> BTreeSet<String>(which ids just wentStale),status_of,stale(),dependents_of,recompute(re-derives a stale id toFresh/Retracted), andregister_from_provenance(tm, view, derived_id)— the intended real-world registration path, reading a node's:DerivedFrom/:GeneratedByedges (EPI-P3-1's lineage) to auto-populate dependencies rather than requiring a caller to hand-list them (L45 closure). - The live CDC hook (
src/server/tms_hook.rs, 192 lines, read in full for this page): a process-globalOnceLock<Mutex<TruthMaintenance>>singleton (global_index()) — chosen explicitly over adding aServerStatefield, to avoid touching the ~40 test-fixtureServerState { .. }literals across the tree.change_event_for_method(method: &Method) -> Option<ChangeEvent>maps only two unambiguous cases:RemoveNode/RemoveEdge→ChangeEvent::Deleted,CompareAndSetNodeFields→ChangeEvent::Updated.notify(method)looks up the event and callson_changeon the global index, returning every materialization id that was staled. - Deliberately narrow — verified directly from the module's own docs and code:
- A plain
AddNodeis not mapped — this seam has no pre-image capture (unlikecrate::server::cdc::emit_for_method, which disambiguates create-vs-update by reading the pre-image), so folding it in would be semantically wrong without that plumbing. PolicyChanged/ModelRetired/OntologyEvolvedare not on any wireMethodyet — there is nothing to map them from.- Nothing calls
TruthMaintenance::registerfrom this hook — the index starts empty every process;on_changehas nothing to stale until a caller registers materializations.register_from_provenance(above) is the intended future source, but the hook itself does not invoke it automatically on every commit. - The staled-id set is dropped after logging — no scheduler, served query method,
or metric currently consumes
notify's return value in production traffic; this hook proves the wiring works (its own two unit tests register a materialization inline and observe the transition), but nothing downstream acts on staleness yet. - The index is in-memory only, resets on restart — no persistence layer exists for it anywhere in the crate today.
- Default-on or opt-in: gated
epistemic-tms(same feature as the TMS above, since the hook iseg-epistemic-dependent); the module does not exist at all in a build without that feature.
Calibrated causal reasoning (causal.rs) — engine-native, partially wire-exposed¶
- What it is:
crates/eg-epistemic/src/causal.rs(632 lines) — a genuine linear-Gaussian structural causal model (CausalGraph,StructuralEquation) with real Pearl do-calculus: intervene(...)(causal.rs:232) — graph surgery: cuts incoming edges to the intervened variable, does not just condition on it. This is the operationally meaningful distinction betweendo(X=x)andobserve(X=x).observe(...)(:262) — conditional, backward-inference-aware.counterfactual(...)(:342) — abduction/action/prediction (Pearl's three-step counterfactual recipe).- Every query returns a
CausalEstimate(:73) with a calibrated credible interval, not a point estimate. - Wire exposure — verified precisely, matches the CHANGELOG's own correction:
Method::CausalEstimatewires onlyintervene(the do-calculus operation) onto the facade, via the handler atsrc/server/handlers/query.rs(causal_estimate_wire).observeandcounterfactualremain crate-internal — exercised only bycargo test -p eg-epistemic --features epistemic-causal, not reachable over any wireMethodtoday. This is an open, documented follow-on, not an oversight the CHANGELOG hid. - Feature gating (two layers, both opt-in, verified in
Cargo.toml): eg-epistemic's ownepistemic-causalfeature gates the crate-internal implementation.- The facade feature
epistemic-causal = ["epistemic", "eg-epistemic/epistemic-causal"]turns on the wireMethod::CausalEstimate/Method::RankByProvenancehandler arms (#[cfg(feature = "epistemic-causal")]insrc/server/handlers/query.rs). Neither is infull.
Provenance-aware retrieval ranking (ranking.rs)¶
- What it is:
crates/eg-epistemic/src/ranking.rs—rank(candidates: &[RetrievalCandidate], weights: RankWeights) -> Vec<RankedResult>, scoring byevidence_quality(reliability, corroboration, calibration precision, freshness) in addition to similarity, not similarity alone. - Wire exposure:
Method::RankByProvenance(handlerrank_by_provenance_wire,src/server/handlers/query.rs), a pure function over request-carried candidates — no graph snapshot needed. - Default-on or opt-in: same facade
epistemic-causalfeature asCausalEstimateabove (both landed together in EPI-P3-3) — opt-in, not infull.
Policy-aware proof redaction (redact.rs, Method::ExplainBelief disclosure_level)¶
- What it is:
crates/eg-epistemic/src/redact.rs(614 lines) definesDisclosureLevel—Full<Skeleton<ExistenceOnly(a total ordering by how much is hidden) — andExistenceSignal(Supported/Contradicted/Uncertain, the coarse "is this believed at all" signal surfaced even atExistenceOnly, deliberately never a raw float confidence).explain_belief_redacted/explain_belief_redacted_capped(:238/:299) reuse the exact sameRowVisibility/can_see_rowcheck (crates/eg-core/src/isolation.rs) every other RLS-aware read path enforces — masking (never silently dropping) an evidence node the caller's RLS context cannot see. - The dual-arm handler (
src/server/handlers/query.rs, both verified by direct read): #[cfg(feature = "epistemic-redaction")]arm:disclosure_level: Nonetakes the byte-for-byte classicexplain_beliefpath;Some(cap)routes throughexplain_belief_redacted_wireunder the caller's own RLS actor.#[cfg(all(feature = "epistemic", not(feature = "epistemic-redaction")))]arm:disclosure_level: Some(_)returns an explicit error ("requires the epistemic-redaction feature, not enabled in this build") rather than silently ignoring the parameter and returning an unredacted tree — the one behavior that would actually be dangerous.- Default-on or opt-in:
epistemic-redaction = ["epistemic", "security", "eg-epistemic/epistemic-redaction"]— requiressecuritytoo (for the sharedIsolationLayer); infullsince WS-1b (2026-07-12; bothsecurityandepistemicare already infull, so this was a zero-new-dependency fold-in).
The bitemporal epistemic_status capstone (query.rs, EPI-P3-5)¶
- What it is:
crates/eg-epistemic/src/query.rs(740 lines) implements the why/why-not/what-changed/what-would-invalidate acceptance query family over the bitemporalAsOfaxis, layered on the paraconsistent TMS. Two wire methods: Method::EpistemicStatus { node_id }— the acceptance capstone: "is this still believed, as of when, and why" (handlerepistemic_status_wire).Method::WhatChanged { tx_from, tx_to }— a whole-graph bitemporal diff between two transaction times; the one facetEpistemicStatusdoes not subsume (handlerwhat_changed_wire).- Default-on or opt-in: both gated
epistemic-tms(same feature as the TMS/CDC hook above) — opt-in, not infull.
Exceed tracks¶
X-1: multimodal evidence graph spine¶
EvidenceSpan— verified to be exactly 11 located-evidence locus kinds (crates/eg-modality/src/evidence.rs, read in full for this page):DocumentSpan(character range in a text document),TableCellRange(rectangular cell range),ImageRegion(pixel-space rectangle),PageBox(visual region on one page of a paged document — distinct fromDocumentSpan's character range),AudioSegment(millisecond time range),VideoShot(shot-boundary time range),VideoFrameRange(frame-index range — distinct fromVideoShot's wall-clock range),MetricWindow(time window on a named metric series),RowVersion(a SQL row's identity + exact transaction/version stamp),CodeSymbol(named symbol by line range),TraceSpan(distributed-tracing span). This confirms the CHANGELOG's "11" figure and corrects an earlier in-flight program note that said "9" (the last two,PageBoxand one other, landed in the same final push).- The citation resolver:
evidence_citations/resolve_locus/justification_citationsincrates/eg-epistemic/src/evidence.rs(387 lines) — walks the same support/contradiction/attackBeliefGraphtopologyExplainBeliefwalks and returns every transitively-cited node's locatedEvidenceSpanplus itsAssetOccurrence/Blobidentity chain. - Wire exposure:
Method::ExplainEvidence { node_id }(handlerexplain_evidence_wire,src/server/handlers/query.rs). - Feature gating — two independent, non-unified resolvers, verified precisely:
- The wire
Method::ExplainEvidencevariant itself is gated only by the baseepistemicfeature (already infull). - The handler arm that actually answers it is gated
#[cfg(feature = "evidence-graph")](evidence-graph = ["epistemic", "eg-epistemic/evidence-graph"]— infullsince WS-1b, 2026-07-12); a build that explicitly disables it (--no-default-featureswithout re-adding it) falls through to the not-built catch-all. - This is a separate resolver from
alignment'sCasEvidenceResolver(Phase 1, above) — sameEvidenceSpanshape, two different implementations, reachable via two different opt-in features (evidence-graphvsalignment). Neither this program nor 2.20.0 unifies them; the evolution roadmap's Seam 2 ("one evidence spine") names this exact gap as the next step.
X-6: reversible intelligence, via the TMS recompute engine¶
- Realized entirely by the truth-maintenance + live CDC hook covered under Phase 3
above: a retraction propagates
Stalealong real dependency edges (never across an unrelated contradiction — the paraconsistency property is preserved), andTruthMaintenance::recomputere-derives a stale id toFreshorRetracted— this is genuinely reversible, dependency-directed derived-knowledge maintenance, not just forward-append. Its honest limitations are exactly those documented under "Dependency- driven recompute" above (nothing callsregisterautomatically; staled ids are currently dropped after logging, not acted on).
Feature-gating map¶
The default build is full (default = ["graph", "algorithms", "metrics", "full"],
Cargo.toml) — there is no separate pi/node tier (see
docs/architecture/tiers.md). The table below covers only the features this
program introduced or wired; consult that page for the pre-existing tier map.
| Feature | In full? |
What it turns on | Depends on / implies |
|---|---|---|---|
epistemic |
yes | Claim/Evidence/Source/BeliefState, confidence propagation, base ExplainBelief |
query, dep:eg-epistemic, dep:eg-modality |
epistemic-tms |
no (HEAVY, opt-in) | Paraconsistent TMS + Dung argumentation, live CDC hook (tms_hook.rs), EpistemicStatus/WhatChanged |
epistemic, eg-epistemic/epistemic-tms |
epistemic-redaction |
yes (WS-1b) | ExplainBelief's disclosure_level (policy-aware proof redaction) |
epistemic, security, eg-epistemic/epistemic-redaction |
epistemic-causal |
no (HEAVY, opt-in) | CausalEstimate (do-intervene only), RankByProvenance |
epistemic, eg-epistemic/epistemic-causal |
evidence-graph |
yes (WS-1b) | ExplainEvidence citation resolver (crate-internal, eg-epistemic-side) |
epistemic, eg-epistemic/evidence-graph |
alignment |
no | CasEvidenceResolver (blob-CAS-backed, facade-reachable) |
blob, dep:eg-alignment, dep:eg-modality |
knowledge-batch |
no | Arrow-backed KnowledgeBatch RowSet projection (eg-plan) |
query, dep:arrow |
contract (per-crate) |
no | ModalityContract conformance-test battery (19 crates each have their own) |
dep:eg-modality (+ crate-specific extras, e.g. eg-rdf also needs owl/sparql) |
jobs |
no | Method::AnalyticsJob durable analytics-job plane (eg-jobs) |
server, mining, epistemic, dep:eg-jobs |
dataset-handle |
no | Arrow IPC dataset-handle seam for external compute | query, server, blob, dep:arrow |
lake |
no | Parquet/Delta/Iceberg-REST materialization + OpenLineage | server, blob, tsdb, dep:eg-lake |
lake-rest |
no | Iceberg-REST catalog endpoint on top of lake |
lake |
raft / cluster |
no (opt-in layer) | PlacementCatalog, multi-group Raft, lazy lifecycle's cross-shard leg |
server, redb, dep:openraft |
Small-footprint invariant, verified (not just asserted): none of the opt-in features
above is in default/full. crates/eg-plan/src/knowledge_batch.rs's knowledge-batch
feature only pulls Arrow via dep:arrow when explicitly requested; full gets Arrow
only if lake is also explicitly requested (it is not part of full). A cargo tree
on the default build links no new heavy dependency class from this program beyond what
the pre-existing full build already carried (DataFusion, Tantivy, redb) — the workspace
[features] block's own comments assert this at each opt-in feature's definition site
(e.g. numeric's comment: "verified: cargo tree --features pi links no eg-numeric/
faer/ndarray" — predates this program but the same discipline was followed for every
feature added here).
Honest gaps and unverifiable claims in this pass¶
Everything above was confirmed by reading the named file/line. A few things this page does not assert because they could not be independently confirmed in this pass (scope: static read of the worktree, no running server, no benchmark run):
- Whether
cargo tree --features full(or--all-features) actually shows zero new default deps beyond pre-2.19.0 — the feature-definition comments inCargo.tomlassert this at each site, and the pattern (every new feature requires an explicitdep:opt-in) is structurally sound, but this page did not runcargo treeitself to independently re-verify the claim end-to-end. - The SCALE-P2-1 soak/chaos harness's actual pass/fail history (referenced by the
evolution roadmap as "not yet run at the sustained 1M-resident scale") — this page
confirms the harness code exists (
src/raft/harness/) but did not attempt to run it; whether the 24-72h soak was ever executed is out of scope for a static read and is explicitly named as open work by the roadmap itself (Seam 5). - Runtime behavior of
EPISTEMIC_GRAPH_OPENLINEAGE_URLpush against a real OpenLineage collector — the code path (src/server/lake/lineage.rs) was read but not exercised against a live endpoint. - Whether the
eg-capabilitiescrate's 343/344 Method-variant count will still be exact by the time this page is read — it is enforced by a compile-time exhaustive match plus a test asserting the literal count, so it is a strong invariant as of this commit, but the count itself is not a permanently-fixed constant across future releases.
Nothing above was inflated to look more complete than the code shows, and nothing
confirmed in code was left out to look more conservative than it should. Where the
CHANGELOG/tracker used an approximate figure (~80 routed methods, 16 TCK-adopting
crates), this page states the exact, current, code-verified number instead and flags the
discrepancy rather than silently overwriting or silently repeating the approximation.