mixle.substrate package

Typed, provenanced, scoped storage for local knowledge and artifacts.

The substrate stores raw data, documents, model artifacts, harvested traces, ontology triples, simulation outputs, and context packets as SubstrateItem objects. Each item carries a kind, provenance, access scope, tags, links, and a retrievable text surface.

class Substrate(root=None, *, secret_policy='redact')[source]

Bases: object

A local shard of the knowledge substrate: a filesystem-backed store with typed retrieval.

put / get / remove / all manage items; search retrieves the k most relevant items for a query, filtered by kind and scope, ranking text items semantically (a learned embedding over the current text corpus) and everything else lexically. save / load persist the shard as one items.jsonl under root.

Immutability contract: every item that crosses the store boundary is a defensive copy. put copies the object you hand it before storing it; get / all (and therefore search) return copies of what is stored. Mutating an object you obtained from – or are about to pass to – this store never affects it: the store’s own state, and everything derived from it (the semantic index), is independent of what any caller does with its own reference afterward. The only way to change a stored item is put() (replace wholesale) or update() (change named fields); both revalidate the result and correctly invalidate the semantic index, exactly like a fresh write.

Scope isolation contract: search(..., scope=X) ranks using an embedding index fit ONLY on scope X’s own text items (see reindex()) – a scoped query’s results, and the model that produced them, never depend on any other scope’s content, so the presence or content of items in a scope you cannot pass never leaks through a scope you can. scope=None (the default) is the one deliberate exception: it asks for every item across every scope, so its index is fit over everything, same as asking for all of it directly through all()/get() would show you.

Secret-handling contract (MXR-080-0262): every write – put(), and update(), which routes through it – scans item.text, item.payload (recursively), and item.tags for known secret shapes (mixle.substrate.security) using exactly the surface _lexical_score() serializes for search, BEFORE anything reaches self._items or either index. This is unconditional: there is no path into the store that skips it, so the redact-before-store guard safe_text() always offered is no longer opt-in. secret_policy (set at construction, or overridden per call) picks what happens when a secret is found: "redact" (the default) masks it in place so only the masked form is ever stored, embedded, or served; "reject" raises SecretPolicyError and stores nothing.

Parameters:
  • root (str | None)

  • secret_policy (SecretPolicy)

put(item, *, secret_policy=None)[source]

Add or replace an item; returns its id and schedules semantic-index rebuilds for text items.

Stores a defensive copy of item (see the class docstring’s immutability contract) – the caller’s own object is never aliased into the store, so mutating it after this call has no effect. Dirty whenever this put could change what _text_items() (the embedding index’s real inclusion rule – ANY kind with a truthy .text, not just a fixed subset) would return: the new item itself carries text, OR it replaces an item that used to. That second half matters as much as the first – clearing an existing item’s text (or replacing it with a no-text item) shrinks the indexed corpus exactly as much as adding text grows it, and without it the stale embedding for the old text would keep matching queries after the text is gone.

Secret-handling (MXR-080-0262, see the class docstring’s secret-handling contract): before anything is stored or indexed, this scans item.text, item.payload (recursively), and item.tags – the same surface _lexical_score() serializes for search – for known secret shapes, and acts per secret_policy (this call’s override, else self.secret_policy, default "redact"): "redact" masks detected secrets in place before storing; "reject" raises SecretPolicyError and stores nothing. This is unconditional – there is no way to reach self._items that skips it.

Parameters:
  • item (SubstrateItem)

  • secret_policy (Literal['redact', 'reject'] | None)

Return type:

str

add(kind, text='', **kw)[source]

Convenience: build a SubstrateItem and put() it.

Parameters:
Return type:

str

get(item_id)[source]

Return a defensive copy of the item stored as item_id, or None when it is absent.

Mutating the returned object does not change what is stored – call update() (or put() a full replacement) to make a change land, so index invalidation stays correct.

Parameters:

item_id (str)

Return type:

SubstrateItem | None

remove(item_id)[source]

Remove an item by id and return whether anything was deleted.

Parameters:

item_id (str)

Return type:

bool

all(*, kind=None, scope=None)[source]

Return defensive copies of stored items, optionally filtered by kind and scope.

As with get(), mutating a returned item never affects the store; use update().

Parameters:
  • kind (str | None)

  • scope (str | None)

Return type:

list[SubstrateItem]

update(item_id, *, secret_policy=None, **fields)[source]

Change named fields on the stored item item_id, the supported way to edit in place.

Mutating an object returned by get()/all() never reaches the store – both return defensive copies, so the store’s own state (and anything derived from it, like the semantic index) is untouched by that. update is how a change actually lands: it rebuilds the stored item with fields applied, re-runs SubstrateItem’s own validation (__post_init__, e.g. rejecting an unknown kind) via dataclasses.replace(), and routes the result back through put()’s storage path so the same dirty/index-invalidation bookkeeping AND the same secret-handling guard (see the class docstring’s secret-handling contract; secret_policy here overrides self.secret_policy for this call only) fire as they would for any other write – a change that affects what _text_items() covers (new text, cleared text, a different scope for a text item) correctly schedules a reindex instead of leaving the semantic index silently stale, and a change that introduces a secret into any field is masked (or rejected) exactly like a fresh put() would.

Returns a defensive copy of the item as actually stored (post-redaction, consistent with get()/all() – NOT the raw pre-policy fields-applied object).

Raises:
  • KeyError – item_id is not stored.

  • ValueError – fields tries to change id – update changes an item’s content, not its identity; put a new item and remove the old one for that.

  • SecretPolicyError – secret_policy="reject" (or self.secret_policy is) and the update introduces a secret anywhere in the surface – nothing is changed.

Parameters:
  • item_id (str)

  • secret_policy (Literal['redact', 'reject'] | None)

  • fields (Any)

Return type:

SubstrateItem

reindex()[source]

(Re)fit one embedding index PER visibility domain. Idempotent, lazy-called.

MXR-080-0237: a single embedder fit over every scope’s text pooled together makes a scoped query’s transform and ranking depend on inaccessible scopes’ content – a membership side channel (whether some distinctive text exists in a scope you can never read is inferable from how it shifts your OWN scoped query’s scores). Fixed by fitting one embedder per distinct scope value present, each ONLY on that scope’s own text items, plus one unrestricted index (key None) fit over everything for callers that pass scope=None – that caller is asking for every scope by construction, so pooling there asserts no isolation boundary to violate. search() picks the index matching its own scope argument, so a scope’s results and the model that produced them never see a single byte of another scope’s text.

Return type:

None

search(query, k=5, *, kind=None, scope=None)[source]

The k most relevant items to query as (item, score), filtered by kind/scope.

Text-bearing items rank by cosine similarity in the learned embedding space fit for THIS scope alone (see reindex()); when there are too few same-scope items to learn one (or for a non-text query), ranking falls back to a lexical token overlap. Structured items with no text always rank lexically over their serialized payload + tags.

k must be an exact, non-negative int (MXR-080-0236, see _require_count()): “return up to k results,” never a negative-indexing slice trick. Checked before reindex() (a potentially expensive rebuild) ever runs, so an invalid k fails fast. retrieve()’s diversified and flat merge paths, and top(), all validate through this same function, so a result count means exactly the same thing – and an invalid one is rejected exactly the same way – at every entry point across the retrieval surface.

Parameters:
Return type:

list[tuple[SubstrateItem, float]]

save(root=None)[source]

Persist the shard to {root}/items.jsonl (one item per line), atomically.

The full snapshot is written to a sibling temp file in target first, fsynced, then published with a single os.replace – mirrors mixle.task.artifact._atomic_json_dump() and mixle.system.registry.Registry._write_index()’s temp-file-plus-os.replace convention. A plain open(path, "w") truncates items.jsonl BEFORE the new content is written, so any failure partway through this method (a crash, a disk-full write, a non-serializable item further down the store) would destroy the last good shard instead of just failing the save; staging the new snapshot off to the side first means the write is all-or-nothing and the previous items.jsonl is untouched by a failed attempt.

Parameters:

root (str | None)

Return type:

str

load(root=None)[source]

Load items from {root}/items.jsonl into this shard, all-or-nothing.

Every row is parsed into a private, freshly-built dict first; only once the ENTIRE file has parsed and validated cleanly does that dict become self._items. A malformed or invalid row anywhere in the file raises – naming the file, the 1-based line number, and a preview of the offending row – and leaves whatever this shard held before the call completely untouched: never a partial store holding only the rows that happened to precede the bad one.

Parameters:

root (str | None)

Return type:

None

class SubstrateItem(kind, text='', payload=<factory>, provenance=<factory>, scope='local', tags=<factory>, links=<factory>, derived_from=<factory>, id=<factory>, created_at=<factory>)[source]

Bases: object

One typed, provenanced, scoped item in the substrate.

Edge typing (MXR-080-0261): links and derived_from are both lists of item ids, but they are NOT interchangeable. links is the generic, untyped KG-relation surface – “related to”, “mentions”, “co-occurs with”, anything kg_rag/multihop want to associate two items by. derived_from is narrower and load-bearing: it is the ONLY place genuine provenance/ancestry (“this item was derived from that one”) is recorded, and it is the only field verify_lineage() ever traverses. Before this field existed, lineage verification walked links itself, so a merely-related entity was indistinguishable from a true derivation parent – putting a citation-worthy relation into links silently made it certifiable ancestry. Keeping the two lists separate (rather than tagging each entry with a type) is a deliberate minimal-diff choice: every existing reader of links (kg_rag, multihop, freshness, context, governance, spaces) keeps reading exactly the same untyped list it always has, with no change to its shape or meaning.

Parameters:
to_json()[source]

Return this item as a JSON-serializable dictionary.

Return type:

dict[str, Any]

classmethod from_json(d)[source]

Build a substrate item from its serialized dictionary form.

Parameters:

d (dict[str, Any])

Return type:

SubstrateItem

ingest_documents(substrate, docs, *, source='documents', scope='local', indices=None)[source]

Add text passages to the substrate as kind="text" items. Returns an IngestReceipt (the new/revised item ids, plus any per-doc failures – see its docstring; behaves exactly like the historical bare list[str] for every existing caller).

Each doc is a string, or a {"text": ..., "tags": [...], "payload": {...}} dict for metadata. Anything else is reported on .failed rather than crashing the whole batch (MXR-080-0269).

Stable, revisioned identity and integrity (MXR-080-0268): each doc’s id is derived from (source, its position) – indices[i] when the caller supplies one (so ingest_file() can pass through a .jsonl file’s true line numbers even after splitting mixed content into docs/records), else the plain enumerate position. Re-ingesting the SAME source reuses the SAME ids, turning a repeat call into a revision of the existing records (see _put_revisioned()) rather than a fresh random-id duplicate every time. When source names a real, readable file, every doc’s provenance also carries that file’s full, algorithm-labelled content_hash (the exact digest check_freshness() later compares against) – source is often just a label with nothing to hash (e.g. the "documents" default), so no content_hash is recorded for those.

Parameters:
Return type:

IngestReceipt

ingest_artifacts(substrate, registry_root, *, scope='local')[source]

Index every deployed artifact under registry_root (dirs containing a manifest.json). Returns an IngestReceipt (behaves exactly like the historical bare list[str]).

The item’s text surface is a human summary of the manifest (kind, io, meta); its payload REFERENCES the artifact directory ({"ref": path}) rather than copying it – unchanged, since other readers (e.g. mixle.reason.receipt.decision_receipt()) depend on ref naming the directory, not the manifest file – and provenance carries the manifest’s lineage fields when present.

Integrity, identity, and validation (MXR-080-0268/0269): provenance also carries the manifest FILE’s full, algorithm-labelled content_hash (content_hash()) and its own manifest_path, for anyone who wants to verify it directly – note that because payload["ref"] (a directory) is what check_freshness() actually re-hashes, an artifact item’s freshness verdict is UNVERIFIABLE by construction (a directory cannot be read as file bytes), which is the documented, intentional behavior of that check, not a gap in this one; recording the hash here is still correct and useful (forensic verification, and a future reader can compare it against a fresh hash of the same manifest file by hand). Each artifact’s id is derived from its directory path (see _stable_id()), so re-ingesting the SAME artifact directory revises the existing record instead of duplicating it. A manifest that fails to read, fails to parse, or parses to valid JSON of the wrong shape (anything other than a JSON object) is reported on .failed – checked BEFORE any .get() is called on it and BEFORE anything is stored for it – rather than raising and aborting every artifact discovered after it (MXR-080-0269).

Parameters:
  • substrate (Substrate)

  • registry_root (str)

  • scope (str)

Return type:

IngestReceipt

ingest_traces(substrate, jsonl_path, *, source=None, scope='local')[source]

Index a harvested .jsonl of {"input": ..., "answer"/"label"/"call": ...} rows as traces. Returns an IngestReceipt (behaves exactly like the historical bare list[str]).

Integrity, identity, and validation (MXR-080-0268/0269): every row’s provenance carries the source file’s full, algorithm-labelled content_hash (one read of the whole file up front, reused for every row – all rows from the same file share the same recorded hash, since a byte anywhere in the file invalidates every row’s “as ingested” guarantee equally). Each row’s id is derived from (source, its line number), so re-ingesting the SAME file revises the existing per-row records instead of duplicating them. A line that fails to parse, or parses to valid JSON that is not a JSON object (the documented row shape requires .get()-able fields), is reported on .failed – checked BEFORE any .get() is called on it – rather than raising and aborting every row after it in the file (MXR-080-0269, the exact “trace code assumes every row is a mapping” bug).

Parameters:
  • substrate (Substrate)

  • jsonl_path (str)

  • source (str | None)

  • scope (str)

Return type:

IngestReceipt

ingest_file(substrate, path, *, kind=None, source=None, scope='local')[source]

Ingest a data file into the substrate. Format inferred from the extension unless kind forces it. Returns an IngestReceipt (behaves exactly like the historical bare list[str]).

.txt/.md -> one text item per non-empty line; .jsonl -> one item per JSON line (a string / {"text": ...} becomes a text item, any other object a record item); .csv -> one record item per row keyed by the header. source defaults to the file path, so every resulting item picks up that file’s content_hash and a stable, revisioned identity (see ingest_documents()/ingest_records()) automatically.

A malformed .jsonl line is reported on the returned receipt’s .failed (naming its 1-based line number) rather than silently dropped (MXR-080-0268) or allowed to abort every line after it (MXR-080-0269); the .jsonl branch preserves each row’s TRUE original line number as its stable identity even after splitting mixed content into a docs sub-batch and a records sub-batch (via IngestReceipt.merge(), which – unlike plain list concatenation – keeps both sub-batches’ .failed entries instead of silently dropping one side’s).

Parameters:
  • substrate (Substrate)

  • path (str)

  • kind (str | None)

  • source (str | None)

  • scope (str)

Return type:

IngestReceipt

ingest_records(substrate, records, *, source='records', scope='local', text_fields=None, indices=None)[source]

Add a sequence of records (dicts or tuples) to the substrate as kind="record" items. Returns an IngestReceipt (behaves exactly like the historical bare list[str]).

Each record’s payload is stored structured; its retrievable text surface is the text_fields (for dict records) joined, else the whole serialized record – so records are searchable by content.

Stable, revisioned identity and integrity (MXR-080-0268), mirroring ingest_documents(): each record’s id is derived from (source, its position) – indices[i] when supplied (see ingest_file()), else the plain position – so re-ingesting the SAME source revises the existing records instead of duplicating them, and a real, readable source file’s content_hash is recorded on every record’s provenance. One record’s storage failure (e.g. a rejected secret under secret_policy="reject") is reported on .failed rather than aborting the rest of the batch.

Parameters:
Return type:

IngestReceipt

class ContextPacket(task, items=<factory>, scores=<factory>, budget=<factory>, n_candidates=0, texts=<factory>, compressed=False)[source]

Bases: object

A budgeted, provenanced view of the substrate assembled for one target + task.

texts holds the text actually used per item – the full item surface, or (when the packet was compressed) an extractive summary that keeps only the query-relevant sentences. preservation receipts how much of each item’s query-relevant content survived, so compression is measured, not trusted.

Parameters:
render(*, header=True)[source]

The assembled context string the target consumes (respecting the budget shape).

Parameters:

header (bool)

Return type:

str

property used_chars: int

The TRUE size of this packet’s default (header-included) rendering – always derived from the actual render() output, never an independently-settable estimate that can silently drift from what a receiver actually gets (MXR-080-0239, MXR-080-0240).

preservation()[source]

Per item, the fraction of the task’s query terms retained in the used text (1.0 = all kept).

The receipt for compression: a value near 1.0 means the summary kept what the query cares about; a low value flags an item whose relevant content was squeezed out.

Return type:

list[float]

property compression_ratio: float

Used chars / full chars over the selected items (1.0 = uncompressed).

provenance()[source]

Where every included piece came from – ids, kinds, sources, relevance scores.

Return type:

list[dict[str, Any]]

as_dict()[source]

Return a JSON-serializable context-packet summary.

Return type:

dict[str, Any]

to_knowledge_dict(*, id, project_id, target_kind, target_id=None, expected_output_schema=None, factuality=None)[source]

Return a plain dict shaped like mixle_knowledge.contracts.ContextPacket.

The exported fields cover id, project_id, task, target_kind, target_id, token and byte budgets, evidence item identifiers, constraints, citations, expected_output_schema, and payload. Constructing a validated pydantic object is the receiving package’s responsibility; core mixle intentionally keeps this as a dependency-free dictionary so platform contract packages can depend on core rather than the reverse.

When factuality is a FactualityReceipt, it is included in payload["factuality"] so receivers can inspect grounding metadata before trusting the packet.

Parameters:
  • id (str)

  • project_id (str)

  • target_kind (str)

  • target_id (str | None)

  • expected_output_schema (dict[str, Any] | None)

  • factuality (Any)

Return type:

dict[str, Any]

to_knowledge_bundle_dict(*, id, project_id, target_kind, target_id=None, expected_output_schema=None, gaps=None, required_capability_ids=None, handoff_policy=None, continuation=None)[source]

Return a plain dict shaped like mixle_knowledge.contracts.KnowledgeBundle (IC-13, M0b).

Unlike to_knowledge_dict() (kept, unchanged, as a deprecated compatibility view – M1c must never use it as canonical state), this bridge carries each selected item’s OWN canonical structured payload/artifact_ref/relations (substrate_item_to_knowledge_dict()) instead of flattening everything into one rendered text string: a graph stays a graph, a typed table stays a typed table, an image keeps its artifact ref. self.render() and the compression receipt are only a disposable legacy view, kept under renderings["legacy_text"]; they may be recomputed or dropped at will without touching any item’s canonical payload or hash.

As with substrate_item_to_knowledge_dict(), core never imports mixle_knowledge – the caller validates the result (e.g. with KnowledgeBundle.model_validate).

Parameters:
Return type:

dict[str, Any]

class ContextBudget(max_chars=2000, max_items=20, shape='passages')[source]

Bases: object

What a target can take – the DeviceSpec of context. shape hints the rendering style.

Parameters:
  • max_chars (int)

  • max_items (int)

  • shape (str)

class ReceiverProfile(name, max_chars=2000, max_items=20, shape='passages', compress=False)[source]

Bases: object

A named receiver’s capacity – what assemble_for_receivers() budgets and shapes for it.

A frontier LM and a local student are not the same target: the LM affords a large, prose-shaped context; the student needs a small, feature-shaped one. ReceiverProfile names that difference so it is set once per receiver, not re-derived ad hoc at every call site.

Parameters:
to_budget()[source]

Convert this receiver profile to a context budget.

Return type:

ContextBudget

assemble_context(substrate, task, *, budget=None, kind=None, scope=None, compress=False, telemetry=None)[source]

Assemble the best-affordable ContextPacket for task from substrate.

Retrieves relevant items (Substrate.search()), then packs them in descending relevance until the character budget or item cap is reached. The packet’s default rendering – header, separators, provenance tags, and all – is GUARANTEED to fit budget.max_chars: every candidate is checked against its actual rendered length before being kept, not an estimate that can silently diverge from what ContextPacket.render() later returns (MXR-080-0239). When even the single most relevant item does not fit whole, its text is shrunk instead of dropped – extractively with compress=True, by hard truncation otherwise – so a small-but-feasible budget still yields something, exactly as before, except the result now genuinely fits rather than silently overflowing.

budget.max_chars must be enough to hold at least the empty rendering for task (the header alone); if it cannot, this raises immediately rather than doing retrieval work for a budget no selection could ever satisfy.

With compress=True, items too large to fit whole are extractively summarized to their query-relevant sentences instead of dropped; packet.preservation() reports what was kept. Emits a context event when telemetry is supplied.

Parameters:
  • substrate (Substrate)

  • task (str)

  • budget (ContextBudget | None)

  • kind (str | None)

  • scope (str | None)

  • compress (bool)

  • telemetry (Any)

Return type:

ContextPacket

assemble_for_receivers(substrate, task, receivers, *, kind=None, scope=None, telemetry=None)[source]

Assemble ONE task-conditioned ContextPacket per named receiver – the concrete receiver-conditioned compression path.

Two receivers reading the same substrate for the same task get genuinely different renderings: budget, shape, and, via compress, which sentences survive. The result is not the same blob truncated to fit each consumer.

packets = assemble_for_receivers(substrate, task, [
    ReceiverProfile("frontier_llm", max_chars=2000, shape="passages"),
    ReceiverProfile("local_student", max_chars=200, shape="features", compress=True),
])
packets["frontier_llm"].render(), packets["local_student"].render()

receivers must have unique, non-empty names – the result is keyed by name, so a duplicate would otherwise silently discard an earlier receiver’s packet AFTER its retrieval, compression, and telemetry work has already run (MXR-080-0241). Validated upfront, before any of that work starts.

Parameters:
  • substrate (Substrate)

  • task (str)

  • receivers (Sequence[ReceiverProfile])

  • kind (str | None)

  • scope (str | None)

  • telemetry (Any)

Return type:

dict[str, ContextPacket]

compress_text(text, task, max_chars)[source]

Extractive, torch-free summary of text keeping the sentences most relevant to task, within max_chars (the standalone compressor used by assemble_context() with compress=True). max_chars must be a positive int – raises otherwise; the returned string’s length is always <= max_chars (MXR-080-0239).

Parameters:
Return type:

str

retrieve(substrate, query, *, k=8, kinds=None, weights=None, diversify=True, scope=None, telemetry=None)[source]

Plan a cross-kind retrieval for query (see module docstring).

Parameters:
  • k (int) – total items to return. Must be an exact, non-negative int (MXR-080-0236, see _require_count()) – bool and fractional values are rejected, and a negative k raises rather than silently slicing from the end. Validated once, up front, before any per-kind search runs, and used identically by both the diversified and flat merge paths below so a given k means the same thing regardless of diversify.

  • kinds (list[str] | None) – restrict to these substrate kinds (default: every kind present).

  • weights (dict[str, float] | None) – per-kind score multipliers (e.g. {"artifact": 1.2} to favor deployable models). Every weight must be finite and non-negative (MXR-080-0248) – a negative weight would silently reverse that kind’s relevance ordering, and a NaN/infinite weight would destabilize the merged order; both are rejected up front, before any score is multiplied.

  • diversify (bool) – when True (default), interleave the top hits of each kind so the result spans modalities; when False, take a flat merged top-k (whichever kind scores highest wins).

  • scope (str | None) – restrict to a team/access scope.

  • substrate (Substrate)

  • query (str)

  • telemetry (Any)

Return type:

Retrieval

class Retrieval(query, items=None, scores=None)[source]

Bases: object

A planned, cross-kind retrieval result: items in merged relevance order, grouped by kind.

Construction contract (MXR-080-0248): built from aligned, validated (item, score) pairs (see RetrievedHit) – not independently-settable parallel lists. items/scores stay available as read-only derived views for every existing reader (r.items, r.scores, zip(r.items, r.scores), r.scores[0]); what changed is that a Retrieval can no longer be constructed into a state where they disagree on length, or where a score is NaN/inf.

Parameters:
property items: list[SubstrateItem]

Retrieved items in merged relevance order (a read-only derived view of hits).

property scores: list[float]

1 with items (a read-only derived view of hits).

Type:

Scores aligned 1

by_kind()[source]

Group retrieved items by substrate kind.

Return type:

dict[str, list[SubstrateItem]]

kinds()[source]

Return the sorted substrate kinds present in the result.

Return type:

list[str]

top(n)[source]

Return the top n retrieved items.

n shares Substrate.search()’s exact-non-negative-integer contract (MXR-080-0236) – no bool, no fractional value, no negative-indexing trick.

Parameters:

n (int)

Return type:

list[SubstrateItem]

provenance()[source]

Return compact provenance records for retrieved items.

Return type:

list[dict[str, Any]]

to_context(task=None, **assemble_kw)[source]

Budget THIS retrieval’s own ordered, weighted hits into a ContextPacket (MXR-080-0249).

Previously this copied self.items into a throwaway Substrate and called assemble_context(), which ran ITS OWN fresh, unweighted search over that tiny shard – discarding this retrieval’s cross-kind weights, diversification order, and scores entirely, and substituting whatever a brand-new lexical/cosine fit over just these few items happened to produce instead. A weighted retrieval that had ranked an artifact above a text item (scores 50 and 1) came back from that fresh search reordered text-before-artifact, rescored 1 and 0.5 – the weighting had silently vanished.

This instead packs self.hits – already in this retrieval’s own decided order, at their own decided scores – directly against the budget, with the same greedy, fit-checked packing assemble_context() itself uses (built on the same public ContextPacket/ContextBudget/compress_text surface, so the fit guarantee – the default rendering is always checked against its TRUE length, never an estimate – holds here too), but never re-scoring, re-ordering, or re-searching.

Accepts the same budget/compress/telemetry keywords assemble_context did. kind/scope are no longer accepted: there is no fresh search left for them to filter – restrict kinds=/scope= on the original retrieve() call instead.

Parameters:
  • task (str | None)

  • assemble_kw (Any)

Return type:

Any

eig_retrieve(substrate, belief, evidence_fn, *, k=8, kind=None, scope=None, seed=None)[source]

Greedily pick up to k substrate items by expected posterior-entropy reduction against belief.

evidence_fn(item) turns a candidate item into whatever belief.update(...) expects (e.g. a per-hypothesis log-likelihood vector for a CategoricalBelief`), OR – when the item’s true evidence is not yet known – an EvidenceOutcomes describing what it could turn out to be. Each round, every remaining candidate is scored by the expected entropy reduction current.entropy() - E[updated.entropy()] (MXR-080-0250; the exact realized reduction in the common deterministic-evidence case, since that is the same formula’s single-outcome degenerate case); the best-scoring item is taken – but only if its gain clears zero, otherwise eig_retrieve() stops rather than hand back a pick that would increase uncertainty (or add none) dressed as evidence – the running belief moves to its post-update state, and scoring repeats against the shrunk pool, so an item whose evidence is redundant with an already-picked item scores near zero on its next look, the direct fix for similarity retrieval pulling in near-duplicates.

belief itself is never mutated: every hypothetical update runs against a fresh clone (MXR-080-0251), so a candidate that turns out not to be chosen – or a BeliefState realization whose update() does not honor the documented “returns a new belief” contract – can never contaminate scoring for the rest of the round. evidence_fn should raise ValueError to signal “no usable evidence for this item” (mirroring what belief.update() itself raises for incompatible evidence); that is the only exception type treated as a skip – every skip is recorded, with its reason, on the returned EigRetrieval rather than silently dropped, and any other exception is a genuine bug and propagates. seed (an int or numpy.random.RandomState) is used only when a chosen candidate carries genuine outcome uncertainty, to draw which outcome is treated as realized for the next round’s baseline.

Returned as an EigRetrieval (query is a fixed marker, not a text query) so it composes with the same to_context/by_kind surface as cosine retrieval, plus .skipped and .stop_reason diagnostics cosine retrieval has no equivalent of.

Parameters:
Return type:

EigRetrieval

class EigRetrieval(query, items=None, scores=None, *, skipped=None, stop_reason=None)[source]

Bases: Retrieval

A Retrieval from eig_retrieve(), plus the diagnostics a similarity retrieval has no use for: which candidates scoring skipped (and why), and whether the run stopped early because no remaining candidate cleared the admissible-gain bar (MXR-080-0250/0251).

Deliberately a plain subclass with its own __init__ rather than a second @dataclass layer: Retrieval itself is @dataclass(init=False) with a hand-written constructor that builds its validated hits tuple from items/scores (MXR-080-0248, e.g. rejecting a length mismatch or a non-finite score) – re-declaring EigRetrieval as its own @dataclass would let dataclass field inheritance synthesize a DIFFERENT __init__ over the raw field list instead (query, hits=(), skipped=..., stop_reason=...), silently bypassing that validation. Delegating to Retrieval.__init__ via super() keeps construction going through the one real validation path regardless of how Retrieval represents itself internally.

Parameters:
  • query (str)

  • items (list[SubstrateItem] | None)

  • scores (list[float] | None)

  • skipped (list[SkippedCandidate] | None)

  • stop_reason (str | None)

class EvidenceOutcome(probability, evidence)[source]

Bases: object

One possible realization of a candidate’s evidence, weighted by its probability of occurring.

Parameters:
class EvidenceOutcomes(outcomes)[source]

Bases: object

A candidate’s predictive distribution over what its evidence could turn out to be (MXR-080-0250).

Return this from evidence_fn instead of a bare evidence value when a candidate’s true evidence is NOT yet known at scoring time – e.g. a not-yet-run experiment or a diagnostic whose result is uncertain until performed. outcomes is the finite set of what it could resolve to, each paired with its probability under the current belief’s own predictive distribution; eig_retrieve() scores the candidate by the true expectation, H[belief] - E[H[belief | outcome]], over this set, rather than the entropy change from a single realized update. Validated eagerly, the same discipline CategoricalBelief applies to its own probabilities: every probability must be finite and non-negative, and they must sum to 1 (within floating-point tolerance) – a caller that gets this wrong finds out immediately, at the point they built the outcome set, not several stack frames away inside a confusing entropy computation.

Parameters:

outcomes (Sequence[EvidenceOutcome])

class SkippedCandidate(item, reason)[source]

Bases: object

A substrate item scoring could not use, and why (MXR-080-0251) – surfaced, never just dropped.

Parameters:
  • item (SubstrateItem)

  • reason (str)

multihop(substrate, query, *, max_hops=2, seeds=3, branch=2, max_items=12, min_score=0.0, scope=None, telemetry=None)[source]

Chain typed hops from query across the substrate, recording the evidence path (see docstring).

Parameters:
  • max_hops (int) – how many hops out from the seeds to expand.

  • seeds (int) – how many top matches to start from (depth 0).

  • branch (int) – how many neighbors to expand per frontier item per hop.

  • max_items (int) – overall cap on the chain size.

  • min_score (float) – relevance floor for SEED and CONTENT hops – a match must score strictly above it to enter the chain (LINK hops are explicit edges and always followed). Keeps a fuzzy retriever from chaining on near-zero-similarity noise; raise it for a dense embedder.

  • scope (str | None) – restrict to a team/access scope.

  • substrate (Substrate)

  • query (str)

  • telemetry (Any)

Raises:
  • TypeError – max_hops/seeds/branch/max_items is not a plain, exact int (bools and fractional floats included).

  • ValueError – any of them is negative.

Return type:

HopChain

class HopChain(query, steps=<factory>)[source]

Bases: object

A multi-hop retrieval result: the items found and the evidence PATH to each.

Parameters:
  • query (str)

  • steps (list[HopStep])

property items: list[SubstrateItem]

Retrieved items in hop-chain order.

by_depth()[source]

Group retrieved items by hop depth.

Return type:

dict[int, list[SubstrateItem]]

max_depth()[source]

Return the deepest hop reached by the chain.

Return type:

int

path_to(item_id)[source]

The evidence chain from a seed to item_id – the trace the reasoner cites.

Parameters:

item_id (str)

Return type:

list[SubstrateItem]

provenance()[source]

Return compact provenance records for every hop step.

Return type:

list[dict[str, Any]]

to_context(task=None, **assemble_kw)[source]

Assemble the hop-chain items into a context packet.

Parameters:
  • task (str | None)

  • assemble_kw (Any)

Return type:

Any

class HopStep(item, depth, via, parent_id, score=0.0)[source]

Bases: object

One item in the chain plus how it was reached: the provenance of a retrieval decision.

Parameters:
  • item (SubstrateItem)

  • depth (int)

  • via (str)

  • parent_id (str | None)

  • score (float)

answer_from_substrate(substrate, question, answerer, *, budget=None, hops=1, min_evidence=1, min_confidence=0.1, compress=True, scope=None, telemetry=None)[source]

Answer question from substrate via answerer, or abstain when evidence is too thin.

Parameters:
  • answerer (Callable[[str, str], str]) – (question, context_text) -> answer_str – any model/rule; called only when there is enough evidence above the confidence floor (so a weak retrieval never fabricates).

  • budget (ContextBudget | None) – the context budget handed to the answerer (default 2000 chars).

  • hops (int) – 1 = single-shot retrieve(); >1 = multihop() chaining that many hops.

  • min_evidence (int) – minimum evidence items the answerer must actually receive to attempt an answer – checked against packet.items (what survives context budgeting), never the pre-budget retrieved/hop-chain count. A small budget can drop most retrieved items before the answerer ever sees them; gating on the pre-budget count would let a caller believe far more evidence was used than the answerer actually got (MXR-080-0257).

  • min_confidence (float) – retrieval-strength floor below which it abstains rather than guess.

  • compress (bool) – compress the context to fit more sources under budget.

  • scope (str | None) – restrict to a team/access scope.

  • substrate (Substrate)

  • question (str)

  • telemetry (Any)

Return type:

Answer

class Answer(question, answer, abstained, confidence, context, note='', evidence=<factory>)[source]

Bases: object

A cited answer or abstention with the evidence it rests on and a confidence.

Parameters:
  • question (str)

  • answer (str | None)

  • abstained (bool)

  • confidence (float)

  • context (ContextPacket)

  • note (str)

  • evidence (list[SubstrateItem])

citations()[source]

Where the answer’s evidence came from – the provenance the answer must be checkable against.

Return type:

list[dict[str, Any]]

as_dict()[source]

Return a JSON-serializable answer with citations and confidence.

Return type:

dict[str, Any]

measure_flywheel(sub, questions, answer_fn, assimilate_batch, *, k=5, min_credence=None, reset_answer_fn=None, trials=1)[source]

Measure answer_fn against questions before and after assimilate_batch(sub) adds a batch of calibrated beliefs (returning the ids it touched), with a THIRD measurement that isolates exactly the batch’s own contribution – the attribution control. answer_fn and every other piece of the system stay fixed throughout: only the store’s content (and what it makes retrievable) changes.

Withheld isolation is delta-level, not belief-level (MXR-080-0246): assimilate_batch may UPDATE a belief that already existed – and was already visible to the before pass – rather than create one from nothing. An id it returns that was already present pre-batch is rolled back, for the duration of the withheld measurement only, to exactly its pre-batch snapshot (its pre-existing evidence, unmodified); an id that did not exist pre-batch is excluded from retrieval outright, same as before, since there is no pre-batch state for it to roll back to. Either way sub is restored to its true post-assimilation content before this function returns – the rollback is an internal, temporary device for taking one measurement, never an observable side effect of calling this function.

answer_fn must be a pure function of (question, context), OR reset_answer_fn must rewind whatever internal state it has to an identical starting point (MXR-080-0247): when given, it is called immediately before every individual measurement – every trial of every one of the three passes – so a stateful evaluator (a cache, a call counter, a seeded sampler) cannot drift across before/after/withheld and masquerade as a store-content effect. trials (requires reset_answer_fn when greater than 1 – without a reset, repeated calls just accumulate whatever state answer_fn carries between them rather than sampling independent replicates) repeats each pass that many times and keeps every individual rate alongside the mean, turning the attribution check into a paired comparison across matched replicates instead of one unrepeated scalar.

Parameters:
Return type:

FlywheelReport

class FlywheelReport(before, after, withheld, attribution_confirmed)[source]

Bases: object

Before/after/withheld flywheel measurements with an attribution check.

Parameters:
  • before (FlywheelMeasurement)

  • after (FlywheelMeasurement)

  • withheld (FlywheelMeasurement)

  • attribution_confirmed (bool)

class FlywheelMeasurement(solve_rate, grounded_fraction, trial_solve_rates=<factory>, trial_grounded_fractions=<factory>)[source]

Bases: object

Held-out answer quality and grounding rate for one flywheel measurement.

trial_solve_rates / trial_grounded_fractions carry one entry per replicate trial (MXR-080-0247): a 1-tuple equal to solve_rate / grounded_fraction for the default trials=1, longer when measure_flywheel() is asked to repeat each pass. Exposed raw – rather than collapsed into one particular spread statistic – so a caller can compute whatever uncertainty measure (stderr, a confidence interval, a paired significance test) actually fits its own answer_fn, instead of this module asserting one test is correct for an arbitrary evaluator.

Parameters:
class QAItem(question, answer)[source]

Bases: object

One held-out question: answer_fn is judged correct on it if it produces answer from the retrieved context alone.

Parameters:
investigate(question, actions, answerer, *, budget_cost=None, min_evidence=1, min_confidence=0.15, target_confidence=None, max_actions=None, scorer=None, telemetry=None)[source]

Answer question by firing evidence-acquiring actions under a cost budget, or abstain.

Actions are ordered by scorer (default score_action(), EIG-per-cost) and fired highest-first. The loop stops early once it holds at least min_evidence fragments and confidence clears target_confidence (default: min_confidence). It also stops at the cost budget or max_actions. scorer can be replaced with a learned acquisition policy such as mixle.inference.learn_action_policy(). The answerer is called only when the evidence clears the bar. The returned Investigation carries the ordered action trace as provenance, and each fired action can emit telemetry for later policy learning. Each action’s score is evaluated exactly once – up front, cached, and reused for both the ordering and the fire/skip decision – so a stateful or stochastic scorer can never produce a fired order inconsistent with the score recorded on its Step (MXR-080-0256).

Every action’s cost is validated (finite, non-negative) up front, before any ranking or accounting runs (MXR-080-0254). An action that raises during run() is recorded as an explicit failed Step (empty fragments, failed=True, the exception on error) instead of being silently coerced into an empty success; an answerer that raises is likewise recorded on the returned Investigation (abstained=True, failed=True, error set) instead of propagating with no investigation record at all (MXR-080-0256).

Parameters:
Return type:

Investigation

class Investigation(question, answer, abstained, confidence, steps=<factory>, note='', factuality=None, proposal=None, failed=False, error='')[source]

Bases: object

A cited answer (or abstention) plus the sequence of actions that acquired its evidence.

Parameters:
property evidence: list[str]

Flatten all evidence fragments collected by the investigation.

property spent: float

Total action cost spent by the investigation.

trace()[source]

The actions taken, in order – the provenance the answer must be checkable against.

Each entry’s failed/error distinguish “ran, found nothing” from “raised” – collapsing those into the same bare n_fragments: 0 is exactly how an action failure used to disappear from the trace (MXR-080-0256).

Return type:

list[dict[str, Any]]

as_dict()[source]

Return a JSON-serializable investigation summary.

Return type:

dict[str, Any]

class Action(name, kind, run, cost=1.0, description='', base_score=0.0, earned_relevance=None)[source]

Bases: object

One evidence-acquiring move: run it on a question, get back evidence fragments, at a cost.

Parameters:
class Step(action, kind, fragments, cost, score, relevance=0.0, failed=False, error='')[source]

Bases: object

A fired action and what it yielded – the audit trail behind an investigated answer.

Parameters:
score_action(action, question)[source]

EIG-per-cost proxy: lexical relevance of the action to the question, divided by its cost.

This heuristic can be replaced with a learned or calibrated expected-information-gain estimate. Retrieval-style actions carry a base_score floor because retrieval is always at least weakly informative.

Zero-cost actions have nothing to divide by: a free, on-topic action has no cost tradeoff to weigh against anything else, so it scores +inf and sorts first; a free but wholly irrelevant action (zero numerator) scores 0.0, same as any other non-informative action, rather than becoming an unbounded favorite. Action.__post_init__ already guarantees cost is finite and non-negative, so these are the only two cases – no clamped division ever runs on a negative or non-finite value (MXR-080-0254).

Parameters:
  • action (Action)

  • question (str)

Return type:

float

relevance_of(action, question)[source]

How on-topic an action is for a question (lexical overlap + its base floor), ignoring cost.

This is a PROSPECTIVE estimate – “how promising does this mounted capability look” – used both as score_action’s routing prior and, elsewhere, to rank untried acquisition strategies (see mixle.scientist.Scientist.propose()). It is deliberately NOT what confidence is earned from once an action has actually run; see _relevance_for().

Parameters:
  • action (Action)

  • question (str)

Return type:

float

action_features(action, question)[source]

The features a learned acquisition policy keys on: the action’s kind, cost, and query overlap.

Parameters:
  • action (Action)

  • question (str)

Return type:

dict[str, Any]

retrieve_action(substrate, *, name='retrieve', k=6, scope=None, cost=1.0, min_score=0.0)[source]

A retrieve action over a Substrate (the always-available floor action).

min_score filters out weak matches: a small embedder returns a result for every query, so a positive floor keeps genuinely-irrelevant items from becoming false evidence. It defaults to 0.0, but the comparison is a strict > regardless of the floor chosen: an exact-zero score means no overlap at all, which is never usable evidence, so it is always excluded – not admitted just because the caller left the default floor at its lowest setting (MXR-080-0255). A small positive min_score makes retrieval conservative still, on a noisy index where near-zero (but not exactly zero) scores are also unreliable.

Confidence is earned separately from ordering: base_score gives retrieval a routing prior for score_action() (“always at least weakly worth trying”), but the fired Step’s confidence-bearing relevance is the TRUE top calibrated score among the results that survived the floor (0.0 when nothing did) – never the fixed routing prior. A store with nothing relevant now reports low or zero confidence instead of borrowing retrieval’s routing floor as if it were support (MXR-080-0255).

Parameters:
Return type:

Action

compute_action(skill, *, name=None, cost=1.0, description=None)[source]

A COMPUTE action that runs a Skill and reports its result.

Parameters:
Return type:

Action

simulate_action(simulator, field_index, scenario, *, name=None, cost=2.0, description='')[source]

A SIMULATE action that runs a what-if scenario and reports the simulated outcome mean.

Parameters:
Return type:

Action

create_action(build, *, name='create', cost=4.0, description='', report=None)[source]

A CREATE action that builds a model / dataset on demand and reports what it made.

build(question) -> artifact fits/synthesizes something (e.g. via mixle.inference.create() or mixle.inference.synthesize()); report renders the artifact to an evidence fragment (default: a certificate/guarantee summary when present, else repr). Creation is the most expensive action, so it defaults to cost=4 – the reasoner reaches for it only when cheaper retrieve/compute/simulate actions cannot answer.

Parameters:
Return type:

Action

delegate_action(delegate, *, name='delegate', cost=8.0, description='', priced=True)[source]

A DELEGATE action that hands the question to an external worker (pool job / remote tool / agent).

delegate(question) -> answer is any priced external capability. This is the reasoner’s most expensive move (default cost=8) and the escalation of last resort: it fires only when nothing local clears the bar, honoring the 99%-local topology. priced=True records that the call incurs real spend (the pool/interop layers own the actual budget-reject + confirm rails).

Parameters:
Return type:

Action

class Reasoner(answerer, *, substrate=None, skills=None, actions=None, budget_cost=None, min_confidence=0.15, retrieve_min_score=0.0, scorer=None, telemetry=None)[source]

Bases: object

A configured reasoner: a knowledge store + skills + actions, asked questions through one method.

Parameters:
  • answerer (Callable[[str, str], str])

  • substrate (Substrate | None)

  • skills (Any)

  • actions (list[Action] | None)

  • budget_cost (float | None)

  • min_confidence (float)

  • retrieve_min_score (float)

  • scorer (Callable[[Action, str], float] | None)

  • telemetry (Any)

property actions: list[Action]

The current action space (retrieve + one compute per skill + attached simulators/creators).

add_action(action)[source]

Attach an extra action (a simulator, creator, or delegate); returns self for chaining.

Parameters:

action (Action)

Return type:

Reasoner

use_policy(scorer)[source]

Route by a learned acquisition policy instead of the lexical prior. Chainable.

Parameters:

scorer (Callable[[Action, str], float])

Return type:

Reasoner

ask(question, *, verify=False, actions=None, **overrides)[source]

Answer question over the configured action space, or abstain. overrides pass through to investigate() (e.g. budget_cost, min_confidence, target_confidence, max_actions).

actions, when given, is used for THIS call only, instead of self._actions – e.g. a caller-owned immutable view such as Harness’s per-request whitelist snapshot. It never reads or mutates self._actions, so passing it cannot corrupt the reasoner’s own configured action list (or any other in-flight call). Default None keeps prior behavior: the reasoner’s own action list.

With verify=True and a substrate configured, the (non-abstained) answer is run back through check_factuality() and the FactualityReceipt is attached to Investigation.factuality – the reasoner grounds its own answer’s claims and reports which it can cite. It does not suppress the answer; the receipt is there for the caller to gate on.

Parameters:
  • question (str)

  • verify (bool)

  • actions (list[Action] | tuple[Action, ...] | None)

  • overrides (Any)

Return type:

Investigation

check_factuality(substrate, answer, *, extract=None, corroborates=None, min_score=0.2, k=4, scope=None)[source]

Ground each claim of answer against substrate, returning a FactualityReceipt.

Parameters:
  • extract (Callable[[str], list[str]] | None) – answer -> [claim, ...] (default mixle.reason.llm.sentence_claims()).

  • corroborates (Callable[[str, str], Corroboration | bool] | None) – (evidence_text, claim) -> Corroboration classifying retrieved evidence against a claim as SUPPORTED / CONTRADICTED / UNVERIFIED (default: lexical candidacy gated by a negation/polarity check – see _default_corroborates(); pass a real NLI/entailment model for stronger grounding). A plain bool is still accepted for compatibility: True maps to SUPPORTED, False to UNVERIFIED.

  • min_score (float) – retrieval-score floor; evidence below it doesn’t count (guards low-signal embedder noise).

  • k (int) – evidence items retrieved per claim.

  • scope (str | None) – restrict retrieval to a team/access scope.

  • substrate (Substrate)

  • answer (str)

Return type:

FactualityReceipt

class FactualityReceipt(answer, verdicts=<factory>)[source]

Bases: object

A per-claim grounding of an answer against the substrate – the receipt behind ‘is this true?’.

Parameters:
  • answer (str)

  • verdicts (list[ClaimVerdict])

property grounded_fraction: float | None

Fraction of extracted claims supported by substrate evidence.

None when the answer had no extractable claims at all (empty, unparseable, or evasive) – there is nothing that was checked, so this is UNKNOWN rather than a vacuous 1.0.

unsupported()[source]

The claims the substrate could not corroborate – exactly what to flag or retract.

Return type:

list[ClaimVerdict]

contradicted()[source]

Claims where retrieved evidence actively disagreed – worse than merely uncited.

Return type:

list[ClaimVerdict]

is_grounded(threshold=1.0)[source]

True iff the grounded fraction meets threshold (default 1.0: every claim must be cited).

Fails closed when there is nothing to assess: an answer with no extractable claims is never reported as “grounded”, regardless of threshold – a full-grounding gate must not wave through silence just because a vacuous fraction happens to clear the bar.

Parameters:

threshold (float)

Return type:

bool

as_dict()[source]

Return a JSON-serializable factuality receipt.

Return type:

dict[str, Any]

class ClaimVerdict(claim, supported, score, citations=<factory>, contradicted=False, contradictions=<factory>)[source]

Bases: object

One claim from an answer, with what the substrate said about it and the evidence either way.

supported is True only when some qualifying evidence was judged Corroboration.SUPPORTED (entailment-aware – see _default_corroborates() – never bare lexical overlap). contradicted is True when some qualifying evidence was judged Corroboration.CONTRADICTED – a stronger and distinct signal from merely being uncited: the substrate does not just fail to back this claim, something in it disagrees. The two are not mutually exclusive: conflicting evidence for the same claim (one item backs it, another disputes it) sets both.

Parameters:
class Space(substrate, team, *, shared=(PUBLIC,), policy=None)[source]

Bases: object

A team’s scoped view over a shared substrate: its own items plus what has been shared to it.

team doubles as this space’s authorization principal (MXR-080-0264): its home scope, per AccessPolicy, is the scope of the same name. Construction validates every entry of shared against policy – PUBLIC always passes; anything else, including another team’s scope, needs an explicit grant, or construction raises AccessDeniedError rather than silently trusting the caller-supplied tuple. Reads re-check live (see scopes), so revoking a grant after construction takes effect immediately rather than requiring a new Space.

Parameters:
  • substrate (Substrate)

  • team (str)

  • shared (tuple[str, ...])

  • policy (AccessPolicy | None)

property scopes: set[str]

its own scope plus whichever of shared the policy currently authorizes – re-checked live (not just once at construction), so a grant revoked later stops being visible immediately rather than requiring a new Space.

Type:

Scopes actually visible to this team right now

all(*, kind=None)[source]

Every visible item (optionally of one kind) – never another team’s private knowledge.

Parameters:

kind (str | None)

Return type:

list[SubstrateItem]

add(*, scope=None, **kw)[source]

Add an item to this team’s own scope by default (pass scope=PUBLIC to share immediately, or any other scope this principal holds a write grant for). MXR-080-0264: no longer writes into an arbitrary scope – in particular another team’s – on the caller’s say-so alone.

Parameters:
Return type:

str

retrieve(query, *, k=8, **kw)[source]

Retrieve over exactly the team’s visible set (own scope ∪ shared), with cross-kind diversity.

Parameters:
Return type:

Any

publish(ids, *, to=PUBLIC, by=None)[source]

Share this team’s items into a common scope (audited). Only own-scope items are publishable.

by (default: self.team) is also the authorization principal for this call (MXR-080-0264): overriding it to a different principal requires that principal to hold its own grants on self.policy, since Space cannot vouch for an identity other than the team it represents.

Parameters:
Return type:

list[str]

publish(substrate, ids, *, to=PUBLIC, by, from_scope=None, policy=None)[source]

Share items into a common scope, authorized (MXR-080-0264).

Re-scopes each item in ids to to and records published_by / published_from in its provenance. Returns the ids actually published; missing ids, ids whose scope doesn’t match from_scope (an additional, OPTIONAL caller assertion), and ids by is not authorized to move are all skipped rather than aborting the whole batch.

by must be a non-empty, authenticated principal, and policy (an AccessPolicy; a fresh, all-deny-but-home-scope-and-PUBLIC one when omitted) must authorize it to WRITE to – checked once, up front, for the whole call – AND to READ each item’s ACTUAL stored scope, checked per item. That second check is what closes the hole from_scope alone never did: it fires even when from_scope is omitted entirely, so a known item id from a scope by cannot read is never movable just by leaving the filter off.

Every published item’s pre-publish state is persisted as an immutable, content-addressed revision (MXR-080-0265) before it is overwritten; version_history entries carry that revision’s digest, recoverable with revision() – so “a re-published item never silently overwrites its predecessor, the prior state is always recoverable” is an enforced property, not just a claim.

Parameters:
  • substrate (Substrate)

  • ids (list[str])

  • to (str)

  • by (str)

  • from_scope (str | None)

  • policy (AccessPolicy | None)

Return type:

list[str]

visible_scopes(team, *, shared=(PUBLIC,))[source]

The scopes a team may read: its own id plus the shared scopes (never another team’s private one).

A pure helper – it does not itself enforce anything. Space is the enforcement point: it validates shared against an AccessPolicy before this ever runs.

Parameters:
Return type:

set[str]

merge_versions(substrate, keep_id, other_id, *, by=None, prefer=MergeStrategy.LATEST, policy=None, confirm=False)[source]

Reconcile two versions of the same knowledge into one, keeping full lineage (no silent loss, P2).

Merges other_id into keep_id: unions tags and links, keeps the text/payload of whichever prefer (a MergeStrategy) selects, bumps the surviving item’s version, records BOTH parents in the history, and removes the merged-away item. Returns the surviving id, or None if either id is missing (checked first, before any authorization – a merge naming a nonexistent id is a no-op, not a permission question).

Authorized common lineage, required (MXR-080-0266): by must be a non-empty, authenticated principal, and policy (an AccessPolicy; a fresh, all-deny-but-home-scope-and-PUBLIC one when omitted) must authorize it to WRITE both keep’s and other’s current scopes – a caller can no longer reconcile, and thereby delete, another principal’s item merely by knowing its id. keep and other must also share the same kind: merging e.g. a text item into an image item is never a legitimate reconciliation of the same knowledge, so this is a hard reject with no override. This is deliberately not a full lineage-graph check (no links/common-ancestor requirement): P1’s own cross-team-fork use case reconciles items that were never formally linked to each other; kind agreement plus dual-scope write authorization is the minimum coherent gate that rejects genuinely unrelated items (different kind, unauthorized principal) without breaking that legitimate use case.

prefer (unrecognized values now raise ValueError instead of silently behaving like "keep") and both items’ pre-merge states are persisted as immutable, content-addressed revisions (MXR-080-0265) before keep is overwritten and other is deleted – recoverable with revision() even though other no longer exists as a live item. The delete is real and permanent as a LIVE item, so it requires an explicit confirm=True acknowledgment; without it this raises ValueError rather than silently performing the destructive step.

Parameters:
  • substrate (Substrate)

  • keep_id (str)

  • other_id (str)

  • by (str | None)

  • prefer (MergeStrategy | str)

  • policy (AccessPolicy | None)

  • confirm (bool)

Return type:

str | None

class MergeStrategy(*values)[source]

Bases: StrEnum

Which side’s text/payload survives a merge – a closed, validated vocabulary (MXR-080-0266): an unrecognized strategy now raises instead of silently behaving like KEEP.

history(substrate, item_id)[source]

The full publish/merge history of an item: every version with who changed it, from where, and to where, plus (MXR-080-0265/0266) a "revision" (publish entries) or parents[]["revision"] (merge entries) content-hash digest that revision() resolves to the complete immutable prior item state – not just this bookkeeping.

Parameters:
  • substrate (Substrate)

  • item_id (str)

Return type:

list[dict[str, Any]]

revision(substrate, digest)[source]

The complete immutable snapshot recorded under content hash digest (MXR-080-0265), or None if no such revision was ever persisted. Returns the full prior SubstrateItem state as a JSON-compatible dict (kind, text, payload, provenance, tags, links, scope, created_at) – not just the who/from/to bookkeeping version_history alone would give you. The returned dict is independent of the stored snapshot (a deep copy), so a caller mutating it can never corrupt the persisted revision.

Parameters:
  • substrate (Substrate)

  • digest (str)

Return type:

dict[str, Any] | None

version_of(item)[source]

The share version of an item (0 if never published) – a monotonic counter bumped by each publish.

Parameters:

item (Any)

Return type:

int

class AccessPolicy(_read=<factory>, _write=<factory>)[source]

Bases: object

The centralized access-policy decision point (MXR-080-0264): the one place that decides whether a principal may read or write a scope.

A principal always has read+write access to the scope sharing its own name (Space is constructed with team doubling as that team’s principal id, so a team’s own scope is always its home scope) and to PUBLIC, the explicit sharing commons – this module’s whole design is that nothing else crosses a boundary until someone deliberately publishes into a common scope. Every OTHER scope, in particular another team’s private scope, is denied until explicitly granted with grant_read() / grant_write() / grant(). Space, publish(), and merge_versions() all consult an AccessPolicy instead of trusting caller-supplied scope parameters (shared=, to=, from_scope=) as though they were already-validated permissions – closing the concrete leak where a team-A space configured with shared=("team-b",) simply read team-B’s private items with no check at all.

Parameters:
grant_read(principal, scope)[source]

Grant principal read access to scope beyond its home scope and PUBLIC (chainable).

Parameters:
Return type:

AccessPolicy

grant_write(principal, scope)[source]

Grant principal write access to scope beyond its home scope and PUBLIC (chainable).

Parameters:
Return type:

AccessPolicy

grant(principal, scope)[source]

Grant principal both read and write access to scope (chainable).

Parameters:
Return type:

AccessPolicy

revoke(principal, scope)[source]

Revoke both read and write grants of scope from principal (chainable).

A principal’s home scope and PUBLIC are intrinsic, not grant-dependent, so this can never revoke those – only a scope previously given via grant_read()/grant_write().

Parameters:
Return type:

AccessPolicy

can_read(principal, scope)[source]

Whether principal may read items in scope.

Parameters:
Return type:

bool

can_write(principal, scope)[source]

Whether principal may write (add or publish) items into scope.

Parameters:
Return type:

bool

require_read(principal, scope)[source]

Raise AccessDeniedError unless principal may read scope.

Parameters:
Return type:

None

require_write(principal, scope)[source]

Raise AccessDeniedError unless principal may write scope.

Parameters:
Return type:

None

exception AccessDeniedError[source]

Bases: PermissionError

Raised when a principal is not authorized for the scope it tried to read, write, or publish.

verify_lineage(substrate, item_id, *, max_depth=20, scope=None, policy=None)[source]

Walk item_id’s ancestry via derived_from ONLY, returning a three-state verdict (cycle-safe).

Traverses exclusively derived_from (MXR-080-0261) – never the generic, untyped links KG edges (see SubstrateItem) – so a merely-related entity can no longer be certified as a derivation ancestor just by existing.

scope, when given, doubles as the authorization principal (the same convention belief uses for the identical problem): every ancestor, at every hop, is resolved through policy (a fresh, home-scope-plus-PUBLIC-only AccessPolicy when omitted). An ancestor outside that authorized view can neither ground an INTACT verdict nor be silently skipped as though absent-therefore-fine – it is named in LineageReport.unverified. An ancestor id that does not exist at all and one that exists but is private to a scope scope cannot read are DELIBERATELY indistinguishable here: both land in unverified, never dangling – telling them apart would let a caller use their own scoped audits as an existence oracle for another scope’s content, the same side channel MXR-080-0237 closed for semantic search and MXR-080-0244 closed for belief evidence. scope=None (the default) is the one deliberate exception, matching Substrate.all()/Substrate.search()’s own convention: an unrestricted caller has no boundary to hide behind, so a missing ancestor is unambiguously reportable as LineageState.BROKEN (dangling) – the legacy behavior.

max_depth bounds pathological chains, but reaching it is no longer silent (MXR-080-0260): a node at the cap that still has its OWN unexplored derived_from entries is named in LineageReport.truncated and the verdict is LineageState.UNVERIFIED, never INTACT – an unvisited tail is never certified. A node at the cap with no further edges (a genuine leaf) is not truncated.

A back-edge into an ancestor already on the CURRENT root-to-node path is a genuine cycle (an item cannot legitimately derive, even transitively, from itself) and is named in LineageReport.cycles, forcing BROKEN. Reaching an already-fully-resolved ancestor via a DIFFERENT branch (a shared ancestor / diamond) is ordinary DAG structure, not a cycle, and is not re-walked.

Precedence when a walk has more than one kind of finding: BROKEN (a confirmed defect) outranks UNVERIFIED (an open question) outranks INTACT – the legacy “one break anywhere fails the whole chain” discipline, extended to the two new states. A missing or unauthorized ROOT item is handled the same way: BROKEN when unrestricted (scope=None), UNVERIFIED when scoped.

Parameters:
  • substrate (Substrate)

  • item_id (str)

  • max_depth (int)

  • scope (str | None)

  • policy (AccessPolicy | None)

Return type:

LineageReport

audit_substrate(substrate, *, scope=None, policy=None)[source]

A knowledge-integrity sweep: how many items are intact/broken/unverified, every one named.

MXR-080-0261: scope/policy are threaded into every verify_lineage() call, so a scoped audit (scope not None) can only ever certify a chain using ancestors that SAME scope is authorized to read. Previously this filtered its own item list by scope but then handed each id to an unscoped verify_lineage, which walked every scope regardless – a scoped audit could be, and was, validated by (or leak the internal integrity of) items it had no business seeing at all.

Returns {n_items, n_intact, n_broken, n_unverified, broken: [...], unverified: [...]}. broken names every item with a confirmed structural defect (dangling edge and/or cycle) within its authorized visibility; unverified names every item whose chain could not be fully checked (depth-cap truncation and/or an ancestor outside scope’s authorized visibility) – kept separate from broken because “could not verify” is never the same claim as “confirmed defective.”

Parameters:
  • substrate (Substrate)

  • scope (str | None)

  • policy (AccessPolicy | None)

Return type:

dict[str, Any]

class LineageReport(item_id, state, n_links, dangling=<factory>, cycles=<factory>, unverified=<factory>, truncated=<factory>, depth=0, visited=0)[source]

Bases: object

Whether an item’s provenance chain resolves end to end – and exactly why, if it doesn’t.

state is the headline three-state verdict (see LineageState); every other field names precisely which ancestor ids drove it, so a caller can always see WHY, not just THAT.

Parameters:
property intact: bool

True iff state is LineageState.INTACT.

Never itself the source of truth – state is (MXR-080-0260/0261): a bare boolean cannot distinguish a confirmed break from an open question, which is exactly what these findings fix. Kept only so a caller that wants a single coarse check still gets a CORRECT one: both BROKEN and UNVERIFIED now read False here, where the pre-fix code could read True for a truncated or cross-scope chain it had never actually verified.

Type:

Read-only convenience view

as_dict()[source]

Return a JSON-serializable lineage report.

Return type:

dict[str, Any]

class LineageState(*values)[source]

Bases: StrEnum

The three-state verdict of a lineage verification (MXR-080-0260/0261) – a closed vocabulary, the same discipline MergeStrategy applies to merge strategies. Never a bare boolean: a bare intact cannot distinguish “checked everything, it’s fine” from “stopped looking partway and don’t actually know” – exactly the overclaim these findings fix.

INTACT: the full chain was traversed – every ancestor authorized and resolved, no cycle, no

depth-cap truncation, no dangling edge. The only state that certifies anything.

BROKEN: a confirmed structural defect within the verified region – a dangling edge (an ancestor

id that resolves to nothing, where the caller had legitimate visibility to know that for a fact) or a cycle (an ancestor that is, directly or transitively, its own ancestor).

UNVERIFIED: the chain could not be fully checked – max_depth was reached with further,

unexplored edges beyond it, or an ancestor lies outside the caller’s authorized scope and could be neither confirmed present nor absent. Never silently promoted to INTACT.

class Governance(_approvers=<factory>, _grants=<factory>)[source]

Bases: object

Who may approve promotions into which scope – the org-governance ACL.

approvers is the live, queryable ACL; grants is its append-only audit trail (who was granted, into which scope, and who authorized it). Both are READ-ONLY views: they were public mutable containers, so authority could be granted straight into the dict with no authorization check and no audit entry, and the trail recording that could itself be edited (MXR-080-1883). grant() is the only way in, and it both checks and records.

A scope with no approvers yet is unowned: the first grant into it may come from anyone, so an approver set can be bootstrapped from nothing. That remains a genuine hole – anyone may claim an unowned scope – and it is now marked bootstrap: True in the audit trail rather than being indistinguishable from an authorized grant. Rooting the bootstrap in a declared authority is outstanding. Once a scope has at least one approver, only an existing approver of THAT scope may grant further ones into it – grant() raises GovernanceAuthorizationError for anyone else.

Parameters:
property approvers: Mapping[str, frozenset[str]]

The live ACL, as a read-only view.

This was a public mutable dict, so governance.approvers.setdefault("secret", set()).add(...) granted approval authority over a scope with no authorization check and no entry in the audit trail that exists to record exactly that (MXR-080-1883). grant() is the only way in, and it both checks and records.

property grants: tuple[Mapping[str, Any], ...]

The append-only grant audit trail, as a read-only view.

An audit trail a caller can edit or truncate is not one; it was a public list.

may_approve(who, scope)[source]

Return whether who is allowed to approve (or reject) promotion into scope.

Parameters:
Return type:

bool

grant(who, scope, *, by)[source]

Add who as an approver for scope (chainable) – audited, and gated once owned.

by must already be an approver of scope, unless scope currently has none at all (the bootstrap case: someone has to seed the first approver). Raises GovernanceAuthorizationError otherwise.

Parameters:
Return type:

Governance

exception GovernanceAuthorizationError[source]

Bases: PermissionError

Raised when an actor attempts a governance transition they are not authorized to make.

propose(substrate, ids, *, to, by)[source]

Mark items as pending promotion to scope to; they are not yet visible there. Returns the ids.

Each call appends a NEW entry to the item’s append-only proposal_history – it never overwrites an existing entry, so a prior decision’s audit record (who rejected or approved it, and why) always survives, even when the item is proposed again later. An item that already has a PENDING proposal is skipped (left out of the returned ids): resolve that in-flight review with approve() or reject() first, rather than silently replacing it with a second one.

Parameters:
Return type:

list[str]

approve(substrate, item_id, *, by, governance, to=None)[source]

Promote a pending item into its proposed scope – IFF by may approve for that scope (the gate).

On success the item is published into the target scope (via P1 publish(), so it inherits the versioned/audited share) and a new APPROVED entry is appended to its proposal_history naming the approver – the PENDING entry it decides is left untouched, never overwritten. Returns False (no change, not a security event) if the item has no pending proposal. Raises GovernanceAuthorizationError if by is not an approver authorized for the target scope; no mutation happens in that case.

Parameters:
  • substrate (Substrate)

  • item_id (str)

  • by (str)

  • governance (Governance)

  • to (str | None)

Return type:

bool

authorization_decision(substrate, item_id, *, capability_id, version, digest=None)[source]

Adapt a completed legacy scope proposal to the shared authorization contract.

Pending or missing proposals have no decision and raise ValueError. This adapter keeps the substrate API compatible while allowing orchestrators to exchange one authorization representation with other Mixle projects.

Parameters:
  • substrate (Substrate)

  • item_id (str)

  • capability_id (str)

  • version (str)

  • digest (str | None)

Return type:

AuthorizationDecision

reject(substrate, item_id, *, by, governance, reason='')[source]

Refuse a pending promotion – the item stays in its origin scope; the refusal is recorded.

by must be either the proposal’s own proposer (a self-withdrawal) or an actor governance authorizes to approve into the proposed scope (a governance refusal). Anyone else raises GovernanceAuthorizationError – an unrelated third party can no longer reject someone else’s proposal. A new REJECTED entry is appended to the item’s proposal_history; the PENDING entry it decides is left untouched, never overwritten. Returns False (no change, not a security event) if the item has no pending proposal to refuse.

Parameters:
  • substrate (Substrate)

  • item_id (str)

  • by (str)

  • governance (Governance)

  • reason (str)

Return type:

bool

pending(substrate, *, to=None)[source]

Items awaiting approval (optionally only those proposed to scope to).

Parameters:
  • substrate (Substrate)

  • to (str | None)

Return type:

list[SubstrateItem]

detect_secrets(text)[source]

Scan text for well-known secret shapes; return a SecretScan naming each finding.

Parameters:

text (str)

Return type:

SecretScan

redact_secrets(text, *, mask='[REDACTED:{rule}]', keep_prefix=0)[source]

Return text with every detected secret replaced by a rule-labelled mask (destructive to secrets).

keep_prefix leaves that many leading characters of the secret visible (0 = fully masked) so a reader can still tell which credential it was without recovering it.

Applied to a FIXED POINT, because one pass did not deliver what this docstring promises (MXR-080-1882). A rule may match only the credential and leave its assignment context in place: token=ghp_... masks to token=[REDACTED:github_token], which still matches the sensitive_assignment rule, so the “redacted” text was still detected as carrying a secret. That only became visible once the store began re-scanning what it was about to write; before that, the single pass simply returned text nobody checked again.

Iteration is bounded and monotone in practice – each pass replaces at least one match with a mask that is not itself that rule’s shape – but the bound is explicit rather than assumed, and a text that has not converged is refused instead of returned dirty.

Parameters:
Return type:

str

safe_text(text)[source]

Redact-before-store guard: mask any secrets so they are never indexed or served.

Parameters:

text (str)

Return type:

str

scan_item(item)[source]

Scan a substrate item’s full indexed/serialized surface (text + payload + tags) for secrets.

Parameters:

item (Any)

Return type:

SecretScan

scan_substrate(substrate, *, scope=None)[source]

Sweep a substrate for leaked secrets and report which stored items triggered rules.

Returns {n_items, n_dirty, dirty: [{item_id, rules}]} for compatibility with existing callers; entries in dirty are the items that matched one or more secret-detection rules.

Parameters:
  • substrate (Any)

  • scope (str | None)

Return type:

dict[str, Any]

class SecretScan(findings=<factory>)[source]

Bases: object

The result of scanning a text: whether anything leaked and every finding.

Parameters:

findings (list[SecretFinding])

property clean: bool

Whether the scan found no secrets.

rules()[source]

Return the sorted names of triggered secret-detection rules.

Return type:

list[str]

as_dict()[source]

Return a JSON-serializable scan summary.

Return type:

dict[str, Any]

class SecretFinding(rule, start, end, preview)[source]

Bases: object

One detected secret: which rule matched, where, and a safe preview (the value stays masked).

Parameters:
class ExternalModel(generate, *, calibration_prompts=None, equivalent=None, max_entropy=None, alpha=0.1, samples=8)[source]

Bases: object

An external generate callable wrapped so each answer carries semantic-entropy UQ.

Parameters:
  • generate (Callable[[Any], Any]) – prompt -> answer (an external agent / LLM / remote tool). Called multiple times per query to measure how much its meaning varies (the uncertainty signal).

  • calibration_prompts (Any) – example prompts whose (1-alpha) semantic-entropy quantile becomes the “too uncertain” cutoff – a calibration policy. Required unless max_entropy is given directly: with neither (or with calibration prompts too few/degenerate to yield a finite quantile), no calibration policy exists at all, and every answer’s state is Confidence.UNCALIBRATED (MXR-080-0270) – never silently treated as confident just because its entropy happened to be finite.

  • equivalent (Callable[[Any, Any], bool] | None) – (a, b) -> bool meaning-equivalence for clustering samples (default: exact match).

  • max_entropy (float | None) – an explicit uncertainty cutoff, overriding the calibrated one. Must be finite and non-negative – entropy is never negative, so a non-finite or negative cutoff is always a caller error, not a valid (if strict) policy.

  • samples (int) – how many draws to sample per answer() call. The first draw becomes the returned answer; entropy is measured over the same batch it came from (MXR-080-0270).

  • alpha (float)

property max_entropy: float | None

Semantic-entropy cutoff used to decide confidence, or None if no calibration policy is configured (MXR-080-0270).

None whenever neither calibration_prompts nor an explicit max_entropy produced a finite cutoff – callers must fail closed on None, never treat a missing cutoff as “anything finite is confident” the way an unguarded entropy <= inf silently would.

answer(prompt)[source]

Sample the external model; the first draw is the returned answer (MXR-080-0270).

Every draw – the one returned and every other sample used to measure entropy – comes from the same batch, so the confidence verdict genuinely describes the returned text’s own semantic position in the cluster, rather than a separately generated call that was never actually compared to anything. Fails closed to Confidence.UNCALIBRATED when no calibration policy (calibration_prompts or an explicit max_entropy) is configured, however low the measured entropy happens to be.

Parameters:

prompt (Any)

Return type:

ExternalAnswer

confident(prompt)[source]

Whether the external model is calibrated-confident on prompt.

Fails closed (False) when no calibration policy is configured (MXR-080-0270) – an uncalibrated model is never reported confident, regardless of measured entropy.

Parameters:

prompt (Any)

Return type:

bool

class ExternalAnswer(prompt, answer, entropy, state)[source]

Bases: object

An external model’s answer plus its self-measured uncertainty (semantic entropy).

answer is itself one of the samples measured for entropy (MXR-080-0270), drawn from the same batch rather than a separate, unassessed call – so state genuinely describes the returned text’s own membership in the measured cluster.

Parameters:
  • prompt (Any)

  • answer (Any)

  • entropy (float)

  • state (Confidence)

property confident: bool

True iff state is Confidence.CONFIDENT.

Never itself the source of truth – state is (MXR-080-0270): both Confidence.UNCERTAIN and Confidence.UNCALIBRATED read False here, so an uncalibrated model can no longer read as confident just because its entropy happened to be finite.

Type:

Read-only convenience view

property calibrated: bool

True unless state is Confidence.UNCALIBRATED.

external_action(model, *, name='external', cost=8.0, description='', trust_uncertain=False)[source]

A reasoner delegate action backed by a UQ-wrapped external model (see module docstring).

By default (trust_uncertain=False) the action contributes evidence only when the external model’s answer is Confidence.CONFIDENT; an UNCERTAIN or UNCALIBRATED answer yields no fragment, so the reasoner treats it as no answer rather than a guess (MXR-080-0270: a never-calibrated model is withheld exactly like a genuinely self-contradicting one – both are “we cannot vouch for this”, not “confident”). The fragment carries the model’s entropy and verdict so the trace records how sure the external source was, and why. Cost defaults high – external calls are the escalation of last resort.

Parameters:
  • model (ExternalModel)

  • name (str)

  • cost (float)

  • description (str)

  • trust_uncertain (bool)

Return type:

Any

kg_action(triples, *, ontology=None, types=None, name='kg', cost=1.0, description='', k=8)[source]

A reasoner RETRIEVE action over a knowledge graph (typed facts, not passages).

Contributes one fragment per fact (head relation tail); nothing links -> no evidence, so the reasoner falls through honestly instead of forcing a match. Relevance comes from the action’s description plus the KG’s own entity inventory (queries naming a known entity score).

triples is materialized exactly once, here, into an immutable, arity-validated structure (MXR-080-0253, see _materialize_triples()) – both the inventory built below and every later _run call read from that same materialized data, so a one-shot triples generator can no longer be exhausted by the inventory pass and come up empty when the action actually runs. k shares retrieve_triples()’s exact-non-negative-integer contract, checked up front so a bad k fails at construction rather than silently inside a fired action.

Parameters:
Return type:

Any

link_entities(question, entities)[source]

The entity-linking leaf: which KG entities does the question mention?

Matches each entity’s normalized name as a token subsequence of the question, longest name first so multi-word entities win over their substrings. Returns the linked entities in match order.

Parameters:
  • question (str)

  • entities (Any)

Return type:

list[str]

retrieve_triples(triples, question, *, ontology=None, types=None, k=8)[source]

Typed KG retrieval: link the question’s entities, return the (schema-valid) facts about them.

Returns {entities, facts, rejected} – facts are the triples touching a linked entity (head or tail), at most k; when an ontology (+ entity types) is supplied, schema-violating triples are excluded and reported under rejected with named reasons, so an unvalidated store cannot inject a type-invalid fact as evidence.

k must be an exact, non-negative int (MXR-080-0253, see _require_count()) – a negative k used to fall through to ordinary Python slicing (hits[:-1] silently drops just the last hit) instead of being rejected. triples is materialized once, validated, into an immutable structure before any lookup runs (MXR-080-0253, see _materialize_triples()) – safe no matter how many times triples is a one-shot iterable and no matter how many times this function is called with it.

Parameters:
Return type:

dict[str, Any]

check_freshness(substrate, item_id, *, max_age_s=None, now=None)[source]

Audit one item for the staleness signals (see module docstring), returning a three-state FreshnessState verdict (MXR-080-0267) – never a bare boolean. Missing item -> stale.

now (default: the current wall clock, injectable for deterministic tests) and max_age_s are caller-supplied ARGUMENTS, validated up front: NaN or infinite (and, for max_age_s, negative) values are always a caller bug, so an invalid one raises ValueError immediately rather than being silently clamped or ignored. By contrast, a NaN or future-dated item.created_at is substrate DATA being audited, exactly like a moved file or a changed hash: it is named in the result (state FreshnessState.STALE) rather than raised, so one corrupted record can never abort a freshness_report() sweep over everything else.

Precedence when more than one kind of finding applies to the same item (mirrors verify_lineage()’s BROKEN-outranks-UNVERIFIED rule): FreshnessState.STALE (a confirmed signal) outranks FreshnessState.UNVERIFIABLE (an open question) outranks FreshnessState.FRESH.

Parameters:
  • substrate (Substrate)

  • item_id (str)

  • max_age_s (float | None)

  • now (float | None)

Return type:

Freshness

freshness_report(substrate, *, max_age_s=None, scope=None, now=None)[source]

Sweep a store for stale/unverifiable knowledge (MXR-080-0267 three-state; mirrors audit_substrate()’s broken/unverified split): {n_items, n_fresh, n_stale, n_unverifiable, stale: [...], unverifiable: [...]} – the monitor feed. stale names every item with a confirmed signal (moved/changed/superseded/aged/invalid timestamp); unverifiable names every item whose referenced content exists and carries a recorded hash but could not be read to check against it – kept separate from stale because “could not verify” is never the same claim as “confirmed defective.”

Parameters:
  • substrate (Substrate)

  • max_age_s (float | None)

  • scope (str | None)

  • now (float | None)

Return type:

dict[str, Any]

content_hash(path)[source]

The full, algorithm-labelled digest of a file’s bytes ("sha256:<64-hex>"), or None if unreadable – record this verbatim at ingest, and compare it verbatim at audit time.

MXR-080-0267: previously a bare hex string truncated to the first 32 characters (128 bits), with no algorithm recorded – collision resistance silently weaker than sha256’s own, and no way for a future reader to know what algorithm produced a recorded value (or to add a second scheme without guessing which one an old value used). The label makes the digest self-describing and algorithm-agile; the digest itself is never truncated.

Parameters:

path (str)

Return type:

str | None

class Freshness(item_id, state, signals=<factory>, age_s=0.0)[source]

Bases: object

One item’s freshness verdict: a three-state state, never a bare boolean (MXR-080-0267; mirrors LineageReport). Every signal – confirmed or unverifiable – is named in signals.

Parameters:
property fresh: bool

True iff state is FreshnessState.FRESH.

Never itself the source of truth – state is (MXR-080-0267): both FreshnessState.STALE and FreshnessState.UNVERIFIABLE read False here, where the pre-fix code could read True for content it had never actually been able to check against its recorded hash – a directory (or any other unreadable path) with a deliberately wrong recorded hash was reported fresh.

Type:

Read-only convenience view

as_dict()[source]

Return a JSON-serializable freshness verdict.

Return type:

dict[str, Any]

class Harness(reasoner, *, name, description='', validate=None, allowed_kinds=None, escalate=None, min_confidence=0.15, on_result=None)[source]

Bases: object

Schema + whitelist + guardrails + escalation around a reasoner (see module docstring).

Parameters:
  • reasoner (Reasoner) – the configured Reasoner (answerer + substrate + skills + actions).

  • description (str) – identity, used by the registry.

  • validate (Callable[[str], str | None] | None) – (request) -> None | str – return an error string to REFUSE the request before any model runs (the input schema, as a callable so any validator plugs in).

  • allowed_kinds (tuple[str, ...] | None) – action kinds this harness may fire (whitelist; None = all).

  • escalate (Callable[[str, Any], str] | None) – (request, result) -> str – called on abstention with the REDACTED request and a redacted Investigation; its return is the escalated answer handed back (e.g. a ticket id). None = abstentions surface as ‘escalated’ with no handler note.

  • min_confidence (float) – the answer bar (passed through to ask).

  • on_result (Callable[[HarnessResult], None] | None) – optional UI hook, called with every HarnessResult (fire-and-forget).

  • name (str)

  • description

Action-view immutability: reasoner._actions is a shared, mutable structure – other Harness instances built from the same reasoner, or a later reasoner.add_action() call, can change it at any time. So that one harness’s whitelist can never be corrupted by another harness’s construction, or bypassed by a later add_action(), a Harness never mutates reasoner._actions and never re-reads reasoner.actions after __init__: it snapshots its own immutable action view – the whitelist-filtered (or, with allowed_kinds=None, full) action list, each action wrapped to redact its own evidence – exactly once, at construction time, and every request the harness ever serves uses only that view. Attach every action a harness should be able to use to the reasoner BEFORE constructing that harness; reasoner.add_action() calls (or another harness’s construction) after that point affect only the shared reasoner and any harnesses built afterward, never one already built. The frozen view is available for inspection as harness.actions.

property actions: tuple[Action, ...]

This harness’s own immutable, redaction-wrapped action view (frozen at construction).

handle(request)[source]

Run one request through every gate: schema -> guardrails -> reasoner -> escalation.

Parameters:

request (str)

Return type:

HarnessResult

class HarnessResult(status, answer=None, reason='', investigation=None, redactions=0)[source]

Bases: object

One request’s outcome: which gate decided (refused/answered/escalated), and the evidence.

Parameters:
  • status (str)

  • answer (str | None)

  • reason (str)

  • investigation (Any)

  • redactions (int)

as_dict()[source]

Return a JSON-serializable harness result.

Return type:

dict[str, Any]

support_triage_harness(substrate, answerer, *, escalate=None, max_chars=2000)[source]

Support triage: retrieve-only over the team’s knowledge, refuse empty/oversized requests, escalate anything the knowledge base cannot support – the canonical ‘never guess at a customer’.

Parameters:
Return type:

Harness

monitoring_harness(reasoner, *, escalate=None)[source]

Monitoring/alerting: compute + simulate allowed (run checks, what-ifs), no delegation out.

Parameters:
Return type:

Harness

register_harness(substrate, harness, *, scope='local')[source]

Index a harness on the substrate as a scoped artifact – discoverable and shareable (P-scoped).

Parameters:
  • substrate (Substrate)

  • harness (Harness)

  • scope (str)

Return type:

str

find_harnesses(substrate, query='', *, scope=None)[source]

Discover registered harnesses (optionally by query / scope). Returns their manifests.

Parameters:
  • substrate (Substrate)

  • query (str)

  • scope (str | None)

Return type:

list[dict[str, Any]]

harvest_knowledge(model_output, *, source, extract=None)[source]

Split model_output into atomic claims, each stamped with source (which model produced it, its confidence, etc). Default extraction is sentence-level (mixle.reason.llm.sentence_claims()); pass extract for a different atomic-proposition splitter.

Parameters:
Return type:

list[Claim]

assimilate(sub, claim, evidence, *, scope='local', policy=None)[source]

Bayesian-ish update of the belief in claim from evidence – never a binary write.

Finds or creates the belief item (keyed on normalized claim text) and appends each evidence entry ({"source_id", "tier", "direction": "+"/"-", "weight"}); tier must be one of _TIER_STRENGTH ("model_assertion" plus mixle.doe.oracle.VERIFIABILITY_TIERS), and direction, when given, must be "+" or "-" (MXR-080-0242).

Verification (MXR-080-0243): a tier above MODEL_ASSERTION only earns its claimed strength when source_id resolves to a real substrate item through an authorized view of scope (see _resolve()); an unresolved, fabricated, or omitted source_id is recorded as unverified and contributes at MODEL_ASSERTION-strength regardless of what it claims.

Anti-laundering (MXR-080-0244): an entry whose source_id resolves back to THIS belief (a cycle) or to another belief item with no independent (non-model-assertion, verified) support of its own is stored with an effective weight of zero – it cannot move the credence, though it stays in the trail for audit. Resolution – both the verification check above and the laundering check – goes through policy (a fresh, home-scope-plus-PUBLIC-only AccessPolicy when omitted), never a raw global lookup, so citing another scope’s private belief is indistinguishable from citing something that does not exist at all: neither the credence it produces nor anything else observable here can be used to learn whether that other scope’s belief exists or is grounded.

Parameters:
Return type:

BeliefItem

retract(sub, source_id, *, scope=None)[source]

Remove every evidence entry citing source_id, recomputing credence, and CASCADE: if that removal causes a belief to lose its only independent support, also strip citations of THAT belief from whatever cited it, recursively. Returns every belief item touched by the cascade.

Parameters:
  • sub (Substrate)

  • source_id (str)

  • scope (str | None)

Return type:

list[BeliefItem]

retrieve_beliefs(sub, query, *, k=8, min_credence=None, scope=None)[source]

Beliefs relevant to query, optionally thresholded on min_credence and re-ranked by relevance * credence – so a caller can weight by, or hard-filter on, how much the store actually believes each item (never a “fact” vs “non-fact” partition, only credence).

MXR-080-0245: the belief tag and min_credence are filtered over the FULL "record"-kind candidate domain, not a fixed-size prefetch – a store where ordinary (non-belief) records or low-credence beliefs rank higher by raw relevance can no longer crowd out qualifying beliefs that rank lower but still exist. sub.search still does the actual relevance scoring (this module never touches core.py’s ranking); the fix is asking it for every "record"-kind candidate in scope rather than an arbitrary 3*k cut of them.

Parameters:
  • sub (Substrate)

  • query (str)

  • k (int)

  • min_credence (float | None)

  • scope (str | None)

Return type:

list[BeliefItem]

credence_from_history(evidence_history)[source]

The credence implied by an evidence history alone – a pure function, so replaying a belief’s stored evidence_history through this always reproduces its current credence exactly.

An entry’s claimed tier only counts at its claimed strength when EvidenceEntry.verified is True (MXR-080-0243); an unverified claim of a strong tier contributes at MODEL_ASSERTION-strength instead, and does not count toward has_real_support (so it cannot, by itself, lift a belief past MODEL_ASSERTION_CAP) – otherwise merely CLAIMING a strong tier would escape the cap even though its actual numeric contribution never did.

Parameters:

evidence_history (Sequence[EvidenceEntry])

Return type:

float

class BeliefItem(id, claim, credence, evidence_history=<factory>, scope='local')[source]

Bases: object

A claim’s current credence plus the full evidence trail that produced it.

Parameters:
  • id (str)

  • claim (Claim)

  • credence (float)

  • evidence_history (list[EvidenceEntry])

  • scope (str)

class Claim(text, produced_by=<factory>, quantity=None)[source]

Bases: object

One atomic proposition (or typed quantity) pulled from a model’s output.

Parameters:
class EvidenceEntry(source_id, tier, direction='+', weight=1.0, time=<factory>, verified=True)[source]

Bases: object

One piece of evidence that moved a belief’s credence, in the order it was applied.

verified records whether source_id resolved to something real, through an authorized scoped view, at the moment this entry was assimilated (MXR-080-0243/0244) – computed once, by assimilate(), and carried on the entry itself so credence_from_history() can replay it without ever touching the substrate again. Direct construction (tests, deserializing an already-decided entry) defaults to True, matching direction/weight/time’s own “already-legitimate input” defaults; assimilate() is the untrusted-input boundary and always computes and passes this explicitly rather than relying on the default.

Parameters:

Submodules