mixle.substrate.freshness module

Freshness checks for substrate knowledge items.

Models drift; so does knowledge. An item can go stale three ways, each independently checkable:

  • moved data – the item references a file (payload.ref / provenance.source/path) that no longer exists, or whose content hash no longer matches the one recorded at ingest;

  • superseded – a newer item shares the same lineage parent or declares supersedes on this one, so this item is no longer the current version of its knowledge;

  • aged out – older than the caller’s max_age_s policy for its kind (a soft signal: age alone is a review trigger, not proof of wrongness – the finding says so).

check_freshness() audits one item and returns a three-state FreshnessState verdict (MXR-080-0267, mirrors LineageState/MXR-080-0260/0261) rather than a bare boolean: FRESH (every check ran, nothing fired), STALE (a confirmed signal fired), or UNVERIFIABLE (a referenced path exists and carries a recorded ingest-time hash, but its CURRENT content could not be read to check against that hash – a directory, a permission error, or any other read failure). Before this fix, an unreadable path’s hash comparison was silently skipped as “no signal,” so a directory with a deliberately wrong recorded hash was reported fresh; unreadable content is now always UNVERIFIABLE, never silently promoted to FRESH. content_hash() records a full, algorithm-labelled digest ("sha256:<64-hex>"), not a bare hex string truncated to 128 bits with no algorithm on it. now and max_age_s are validated (finite; max_age_s also non-negative) before use, and a NaN or future-dated item.created_at is itself a named STALE signal rather than a silently clamped age_s=0.0.

freshness_report() sweeps a store. These checks help monitoring workflows surface stale citations, unverifiable artifacts, and moved data before they affect downstream answers.

class FreshnessState(*values)[source]

Bases: StrEnum

The three-state verdict of one item’s freshness check (MXR-080-0267) – a closed vocabulary, the same discipline LineageState applies to lineage verification and MergeStrategy applies to merge strategies. Never a bare boolean: a bare fresh=True cannot distinguish “every check ran, nothing fired” from “couldn’t check some of it, so I don’t actually know” – exactly the fail-open overclaim this finding fixes.

FRESH: every staleness check ran to completion and no signal fired.

STALE: at least one CONFIRMED signal fired – moved, changed, superseded, aged out past policy, or an invalid (NaN / future-dated) recorded timestamp.

UNVERIFIABLE: a referenced path exists and an ingest-time hash was recorded for it, but the CURRENT content could not be read to compare against that hash (a directory, a permission error, or any other read failure). Never silently promoted to FRESH just because nothing contradicted the recorded hash – there was no comparison to contradict it WITH.

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]

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

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]