How to trace and audit a fact#

This guide shows you how to start from a fact you already hold (its assertion id or AssertionRecord), read the provenance recorded on it, and inspect which value the runtime chose for that entity field cell, including when several active assertions compete.

Prerequisites#

  • A FactGraph (alias SDKStore) with a registered schema and at least one written field assertion.
  • The assertion id of the fact, or an AssertionRecord for it. Field writes return the assertion id: fg.fields.set(...) and fg.fields.add(...) both return a str asrt_id. If you only have the entity and field, see step 1 for how to recover the id.

The examples use this schema and a single written cell:

from factgraph.sdk import Entity, FactGraph, Field, Identity

class User(Entity):
    user_id: str = Identity()
    name: str = Field()
    tag: list[str] = Field()

fg = FactGraph.create(schema_classes=[User])
ref = fg.entities.ref(User, user_id="u-1")
name_asrt = fg.fields.set(User.name, ref, "Alice", meta={"source": "seed"})

Steps#

  1. Get the AssertionRecord for the fact. If you held onto the asrt_id, look it up directly:

    record = fg.assertions.by_id(name_asrt)

    fg.assertions.by_id(asrt_id) returns an AssertionRecord, or None if no claim has that id. If instead you only have the entity reference and the Field, recover the active records first and take their asrt_id:

    record = fg.assertions.where(field=User.name, e_ref=ref).first()

    fg.assertions.where(...) filters active records by field=, e_ref=, value=, value_tag=, and _meta=. See reference/three-layer-api.md for the full filter surface.

  2. Read the record’s value and provenance. An AssertionRecord is a frozen dataclass with asrt_id, value, value_tag, is_active, entity_type, field_name, pred_id, e_ref, and meta:

    record.value        # "Alice"
    record.is_active     # True until a revocation supersedes it
    record.pred_id       # predicate id for User.name
    record.e_ref         # the entity reference this cell belongs to

    The meta attribute is an AssertionMeta dataclass carrying the typed provenance pulled from the ledger’s meta rows: source, trace_id, ingested_at, approved_by, note, derived_rule_id, derived_rule_version, candidate_id, candidate_key, candidate_kind, and raw. Read the typed fields when present, and fall back to meta.raw (a dict of every recorded meta key) for keys without a dedicated attribute:

    record.meta.source            # "seed"
    record.meta.derived_rule_id    # set when a rule derived the fact, else None
    record.meta.raw                # {"source": "seed", ...} every recorded meta key

    For a fact written by a rule rather than asserted by hand, meta.derived_rule_id and meta.derived_rule_version carry the deriving rule’s identity when the writer recorded them. See reference/three-layer-api.md for the AssertionMeta field list and evaluate-a-rule-and-read-the-proof.md for the evidence side of derivation.

  3. Inspect the chosen value at the cell with fg.audit.explain:

    explanation = fg.audit.explain(record)

    fg.audit.explain(target) accepts either the AssertionRecord or its asrt_id string. It returns a dict describing the predicate/entity cell the assertion sits in:

    • pred_id: the predicate id of the cell.
    • e_ref: the entity reference.
    • active_claims: a list of rows for the currently active claims at that cell, each row a dict with asrt_id, args, and meta.
    • chosen_asrt_id: the asrt_id the policy selected for the cell, or None.
    • asrt_id: the asrt_id of the target you passed.
    • chosen: True when your target is the chosen assertion, False otherwise.

    To verify: explanation["chosen"] is True and explanation["chosen_asrt_id"] == record.asrt_id for a cell with a single active write.

  4. If you need only the competing ids at a cell, or you want to start from the entity and field rather than an assertion, use fg.audit.conflicts:

    conflicts = fg.audit.conflicts(record)
    # or, by entity reference and field name:
    conflicts = fg.audit.conflicts((ref, "name"))
    # or by entity reference and Field descriptor:
    conflicts = fg.audit.conflicts((ref, User.name))

    fg.audit.conflicts(target) accepts an AssertionRecord, an asrt_id string, or a two-tuple of (entity, field) where entity is an entity reference string (or an EntitySnapshot) and field is a Field descriptor or a field name string. It returns a dict with:

    • pred_id and e_ref: the cell.
    • active_asrt_ids: the list of all active assertion ids competing at the cell.
    • chosen_asrt_id: the selected asrt_id, or None.

    When active_asrt_ids holds more than one id, the cell has competing active writes and chosen_asrt_id tells you which one wins. To verify your fact is among them: record.asrt_id in conflicts["active_asrt_ids"].

    To resolve a competing write, retract the losing assertion by id with fg.assertions.retract(asrt_id); see write-and-read-facts.md.

See also: reference/audit.md for the full fg.audit and fg.meta surface, reference/three-layer-api.md for AssertionRecord and AssertionMeta, and About auditability for why the ledger records provenance this way.

comments powered by Disqus