Skip to content

Epistemic tiers and confidence

Not all intelligence is created equal. A verified CISA advisory carries more weight than an anonymous forum post, which carries more weight than an LLM's inference. ZettelForge tracks this difference through two complementary fields on every MemoryNote: an epistemic tier and a confidence score.

Epistemic tiers (A / B / C)

Every note stored in ZettelForge carries a tier field in its metadata:

Tier Label Meaning Typical sources
A Authoritative Verified intelligence from trusted sources CISA advisories, MITRE ATT&CK, vendor-confirmed CVEs, court documents
B Operational Working knowledge with moderate confidence, useful for day-to-day analysis Threat reports from CrowdStrike/Mandiant, internal incident notes, peer-reviewed analysis
C Support Low-confidence, inferred, or speculative intelligence Analyst notes flagged for review, notes with high evolution counts

The default value in Metadata is "B". However, notes created through the NoteConstructor.construct() path — which remember() uses — are explicitly assigned tier="A" at ingestion time. You can change this afterward:

from zettelforge import MemoryManager

mm = MemoryManager()
note, status = mm.remember(
    "APT28 exploited CVE-2024-1234 in a campaign targeting NATO logistics.",
    source_type="report",
)
# note.metadata.tier is "A" after construction

# Downgrade if you have doubts about the source:
note.metadata.tier = "B"
mm.store.rewrite_note(note)

# Upgrade to authoritative after independent verification:
note.metadata.tier = "A"
mm.store.rewrite_note(note)

Tier is just a string — the system enforces no hard constraints beyond what you document for your team. The value is only meaningful in the context of your retrieval and synthesis calls.

Confidence scores (0.0 to 1.0)

Every MemoryNote carries two confidence-related fields:

Field Type Default Meaning
metadata.confidence float 1.0 Note quality score. Decays on each evolution event. Drives should_flag_for_review().
metadata.stix_confidence int -1 STIX 0–100 confidence scale. Stored in SQLite. -1 means unset. Set explicitly when ingesting STIX-sourced intelligence.

Both fields live on the note, not on the knowledge graph edge. KG edges between entities do not carry a guaranteed confidence attribute in the OSS schema.

Confidence decay on evolution

Every time a note is superseded or evolved, increment_evolution() fires:

def increment_evolution(self, evolved_by_note_id: str | None = None):
    self.metadata.evolution_count += 1
    if evolved_by_note_id:
        self.evolved_by.append(evolved_by_note_id)
    self.updated_at = datetime.now().isoformat()
    # Confidence decay: evolved notes lose confidence
    self.metadata.confidence = min(self.metadata.confidence, 0.95)

confidence is capped at 0.95 after the first evolution. It does not decay below that automatically — you manage further decay by calling mm.remember(..., evolve=True) repeatedly as new, conflicting intelligence arrives. A note that has been substantially revised many times naturally accumulates a lower confidence over time.

Flagging for human review

def should_flag_for_review(self) -> bool:
    return self.metadata.confidence < 0.5 or self.metadata.evolution_count > 5

A note is flagged when its confidence drops below 0.5 or when it has been evolved more than 5 times (at evolution_count of 6 or higher). The memory_defense layer checks this flag and surfaces the note in governance reports.

How tiers affect synthesis

The synthesize() method accepts a tier_filter parameter. When you call synthesize() without specifying it, the synthesis generator uses all three tiers by default (["A", "B", "C"]):

# Default — all tiers contribute:
result = mm.synthesize("APT28 capabilities")

# Operational quality and above only (excludes low-confidence notes):
result = mm.synthesize("APT28 capabilities", tier_filter=["A", "B"])

# Authoritative sources only:
result = mm.synthesize("APT28 capabilities", tier_filter=["A"])

The default ["A", "B", "C"] means all notes contribute to synthesis regardless of tier unless you restrict it. If you need to exclude inferred or low-confidence notes from synthesis, pass tier_filter=["A", "B"] explicitly.

Configuration note

config.retrieval.tier_filter (default ["A", "B"]) is used by the CrewAI integration retriever, not by direct mm.synthesize() calls. If you rely on direct API calls, control tier filtering at the call site.

The Diamond Model connection

CTI practitioners will recognize this confidence model as complementary to the Diamond Model of Intrusion Analysis. Where the Diamond Model maps relationships between adversary, capability, infrastructure, and victim, ZettelForge's epistemic tiers address the meta-question: how much do you trust each piece of intelligence?

A Diamond Model analysis might state "Adversary X uses Capability Y targeting Victim Z." In ZettelForge, this is stored as notes describing each relationship, each carrying an independent confidence score and tier classification. The adversary–capability note might be tier="A" with confidence=0.9 (observed in multiple ISAC reports), while the adversary–victim attribution note might be tier="B" with confidence=0.4 (single unconfirmed report). When you synthesize, those scores influence which sources contribute and with what weight.

For analysts who need STIX-formatted confidence values, set metadata.stix_confidence (0–100) on notes that carry formal STIX provenance. The field is stored in SQLite alongside the float confidence and can be included in STIX export workflows.

Confidence on ThreatRecall.ai SaaS

ThreatRecall.ai extends the confidence model with typed confidence attributes on knowledge graph relationships, enabling queries like "show me all relationships attributed to APT28 with confidence above 0.7." This is not part of the ZettelForge OSS schema, where confidence lives on notes. If your workflow needs per-relationship confidence at scale, see the ThreatRecall.ai documentation.