Skip to main content
Version: v0.9.0a2
Spec

Spec-01-Fact-Model

9 min readSpec contributor · SDK authorFact tuple foundations

What this page is

Rendered compatibility entry point for Spec-01-Fact-Model. The fact tuple, value types, scopes, HLC, identity, federation-trust fields.

Authoritative source: spec/stigmem-spec-v0.9.0a1.md

Section body

The maintained component spec is Spec-01-Fact-Model; legacy §2 anchors are retained for existing links.

Revisions before pre-reset draft: the pre-reset spec-draft, pre-reset draft, v1.0

From stigmem-spec-the pre-reset spec-draft.md:

Every piece of knowledge in Stigmem is an atomic fact:

(entity, relation, value, source, timestamp, hlc, confidence, scope)
FieldTypeDescription
entityURI (see §2.5, §2.6)What this fact is about. Formal: stigmem://company.example/user/alice. Informal (deprecated): user:alice. Stored in canonical normalized form (§2.6).
relationstring (namespaced predicate)What kind of statement this is. Examples: memory:role, roadmap:status, preference:timezone.
valueFactValue (see §2.1)The asserted value.
sourceURI (see §2.5, §2.6)Who asserted the fact. Examples: stigmem://company.example/agent/assistant, stigmem://company.example/user/alice. Stored in canonical normalized form (§2.6).
timestampISO 8601 UTC datetimeWall-clock time when the fact was asserted. Set by the node at write time; clients may suggest.
hlcHLC string (see §2.4)Hybrid Logical Clock timestamp. Causality-preserving; required for federation.
valid_untilISO 8601 UTC datetime or nullOptional. If set, the fact is expired after this time.
confidencefloat in [0.0, 1.0]Asserting party's confidence. 1.0 = certain, 0.5 = uncertain, 0.0 = retracted.
scopeFactScope (see §2.2)Visibility / federation boundary.

A fact is immutable once written. Updates are new facts. The latest fact for a given (entity, relation, scope) triple wins unless contradiction policy applies (see §3.3).

From stigmem-spec-pre-reset draft.md:

Stable sections §2.1–§2.6 unchanged from the pre-reset spec. The following fields are added to the fact record.

From stigmem-spec-v1.0.md:

Stable sections §2.1–§2.6 unchanged from the pre-reset spec. The following fields are added to the fact record.

§2.1 FactValue

A FactValue is a discriminated union that constrains what a fact can assert. The type tag forces consumers to handle each variant explicitly — there is no "any" escape hatch — so that queries, indexing, and synthesis can operate on typed data without runtime introspection.

FactValue =
| { type: "string", v: string } // short identifier or label (≤1 KB recommended)
| { type: "text", v: string } // unbounded narrative; markdown allowed; ≤64 KB inline; use ref for larger
| { type: "number", v: number }
| { type: "boolean", v: boolean }
| { type: "datetime", v: ISO8601 }
| { type: "ref", v: URI } // pointer to another entity or external content
| { type: "null" } // explicit "unknown / not applicable"

The string vs text distinction exists because short labels and multi-paragraph narratives have different indexing characteristics.

Nodes index string values for exact-match queries; text values feed the embedding pipeline (§20.2) for semantic recall. The ref type creates typed edges in the knowledge graph — the recall pipeline (§20.1) traverses ref values during k-hop expansion.

text size guidance (v0.4): Inline text values SHOULD be ≤ 64 KB. For larger payloads, assert a ref fact pointing to external storage and keep the text value as a summary. Nodes MAY reject text values above their configured limit; they MUST return HTTP 413 if they do.

§2.2 FactScope

Scope is the visibility fence that determines which facts leave a node during federation. The four levels form a strict hierarchy from most private to most shareable.

FactScope =
| "local" // visible only within this node, never federated
| "team" // visible within a logical team boundary (node-defined)
| "company" // visible within the owning company node
| "public" // federatable to any peer that has a handshake with this node

Nodes MUST NOT federate local or team facts without explicit operator override. company-scoped facts are only federated when the active PeerDeclaration explicitly includes "company" in allowed_scopes (see §6.1).

§2.3 Reification (N-ary relationships)

The fact tuple is binary — one entity, one relation, one value — but real-world knowledge often involves three or more parties. Reification handles this by minting a synthetic entity that represents the relationship itself, then attaching the participants as facts about that entity.

(entity="stigmem:rel:abc123", relation="rel:subject", value={type:"ref", v:"stigmem://company.example/company/a"})
(entity="stigmem:rel:abc123", relation="rel:object", value={type:"ref", v:"stigmem://company.example/company/b"})
(entity="stigmem:rel:abc123", relation="rel:type", value={type:"string", v:"policy:board-approval"})

rel:subject, rel:object, and rel:type are reserved in the rel: namespace (see §9). The graph traversal engine (§20.1) follows ref values out of reified entities the same way it follows any other ref, so reified relationships participate naturally in k-hop recall.

§2.4 Hybrid Logical Clock (HLC) — pre-reset

Wall-clock timestamps alone cannot establish causality in a distributed system because clocks drift. A pure logical clock (Lamport-style) preserves causality but loses correlation with real time. Stigmem uses a Hybrid Logical Clock that combines both.

Every node maintains a single HLC value:

HLC = wall_ms || counter

Format: "{wall_ms_utc}.{counter}" — e.g. "1746230400000.003".

The string encoding uses a dot separator so that lexicographic string comparison produces correct causal ordering without parsing. The wall_ms component is zero-padded to 13 digits (sufficient until year 2286); the counter component is zero-padded to 3 digits per node (overflow creates a new millisecond bucket).

Advance rules:

Trigger
wall_ms rule
Counter rule
Local write
max(now_ms, last_hlc_ms)
Increment if wall_ms unchanged.
Receiving federated fact
max(now_ms, received_hlc_ms)
Increment counter.

Causal ordering: Two facts a, b are causally ordered iff a.hlc < b.hlc. Equal HLCs on different nodes indicate concurrent writes; standard contradiction policy (§3.3) applies.

Wire encoding: hlc is included in all fact responses and replication payloads. Clients that do not understand HLC MAY ignore the field; nodes MUST store and propagate it.

§2.5 Entity URI scheme — pre-reset normative

pre-reset open question §8.1 resolved.

The entity URI scheme is now normative.

Formal URI scheme

stigmem://{authority}/{type}/{id}
Component
Examples
Description
authority
company.example
Hostname of the Stigmem node that owns this entity namespace.
type
user, agent, project, issue
Entity type slug (lowercase, no spaces).
id
alice, cto, EG-42
Opaque stable identifier for the entity.

Examples:

stigmem://company.example/user/alice

stigmem://company.example/agent/cto

stigmem://company.example/issue/EG-42

stigmem://node.acme/decision/use-sqlite

Deprecation of informal URIs

Informal URIs (colon-separated shorthand such as user:alice, agent:cto) are deprecated as of pre-reset.

Actor
Requirement
Behavior
Nodes
accept + warn
MUST accept informal URIs without rejecting (backward compatibility). MUST emit a deprecation warning to stderr when storing a fact whose entity or source field does not match the stigmem:// scheme. MUST NOT auto-rewrite informal URIs to formal URIs on ingest.
Adapters
use formal
SHOULD use formal URIs for all new fact assertions. MUST NOT emit informal URIs in new code targeting pre-reset or later.

Collision rationale.

Informal URIs are inherently ambiguous once federation is active. user:alice on node A and user:alice on node B may refer to different people. The formal scheme binds the authority to the URI, preventing silent identity collisions across federated nodes.

pre-reset note: All components of the formal URI are normalized to lowercase on ingest (§2.6). stigmem://company.example/issue/EG-42 is stored as stigmem://company.example/issue/eg-42.

§2.6 Entity naming rules — pre-reset normative

This section defines canonical entity naming rules and the strict normalizer contract. The goal is to prevent silent entity fragmentation: multiple facts about the same real-world entity using different URI representations that create disconnected entity nodes in the store.

pre-reset scope: The strict normalizer addresses case-based and whitespace-based fragmentation deterministically. Full alias resolution (e.g. user:aliceuser:a.smith) is deferred to the pre-reset design-partner window fuzzy resolver.

§2.6.1 The fragmentation problem

Before strict normalization, the following assertions create separate entities for the same project:

entity="project/eg-18" (informal, slash separator, lowercase)
entity="project/EG-18" (informal, slash separator, uppercase)
entity="stigmem://company.example/project/eg-18" (formal, lowercase id)
entity="stigmem://company.example/project/EG-18" (formal, uppercase id)

All four refer to the same project. Without normalization, queries for any one form miss the others entirely, and contradiction detection never fires for facts that should conflict.

§2.6.2 Canonical form

The canonical form is the lowercase form of the URI with surrounding whitespace trimmed and internal whitespace in the id component collapsed to hyphens.

For formal URIs (stigmem://authority/type/id):

Component
Rule
Notes
authority
lowercase + trim
Trim surrounding whitespace.
type
lowercase + trim
Trim surrounding whitespace.
id
lowercase + trim + collapse whitespace
Collapse internal whitespace runs to a single hyphen.

For informal URIs (any non-stigmem:// form):

Lowercase entire string

Trim surrounding whitespace; collapse internal whitespace to hyphens.

Format preserved

Informal stays informal — not converted to formal.

Honors §2.5 anti-rewrite

Lowercasing the informal form is not the same as expanding it to the formal scheme.

§2.6.3 Strict normalizer — normative algorithm

Reference implementation at stigmem/node/src/stigmem_node/entity_normalizer.py:

import re

_FORMAL_URI_RE = re.compile(r"^stigmem://([^/]+)/([^/]+)/(.+)$")
_WHITESPACE_RE = re.compile(r"\s+")

class NormalizationError(ValueError):
pass

def normalize_entity_uri(raw: str) -> str:
"""Return the canonical form of an entity URI string.

Raises NormalizationError on empty or whitespace-only input.
"""
if not raw or not raw.strip():
raise NormalizationError("entity URI must not be empty")

stripped = raw.strip()
m = _FORMAL_URI_RE.match(stripped)
if m:
authority = m.group(1).strip().lower()
type_slug = m.group(2).strip().lower()
id_part = _WHITESPACE_RE.sub("-", m.group(3).strip().lower())
if not authority or not type_slug or not id_part:
raise NormalizationError(
f"normalization produced empty component in formal URI: {raw!r}"
)
return f"stigmem://{authority}/{type_slug}/{id_part}"

# Informal URI: lowercase and collapse whitespace; format preserved
return _WHITESPACE_RE.sub("-", stripped.lower())

Invariants the normalizer MUST satisfy:

Deterministic

Identical inputs always produce identical outputs.

Idempotent

normalize(normalize(x)) == normalize(x) for all valid inputs.

Total on valid inputs

Every non-empty string produces exactly one output; invalid inputs raise NormalizationError.

What the strict normalizer does NOT do:

Alias resolution

user:aliceuser:a.smith — pre-reset design-partner fuzzy resolver.

Existence validation

Against the fact store.

Semantic similarity

Matching.

Informal → formal conversion

§2.5 prohibits silent auto-rewrite.

§2.6.4 Ingest-path contract

Nodes MUST apply the strict normalizer to the entity and source fields of every incoming fact before persistence:

  1. If normalize_entity_uri returns a canonical URI, store the canonical form.
  2. If the input was an informal URI (does not match stigmem://), also emit a deprecation warning to stderr as specified in §2.5.
  3. If normalize_entity_uri raises NormalizationError, reject the fact with HTTP 400 { "error": "invalid_entity_uri", "detail": "<NormalizationError message>" }.

Why normalize at ingest (not query).

Query-time normalization would require every consumer to carry normalization logic and would leave non-canonical data permanently in the store. Ingest normalization ensures the stored form is always canonical; all queries use exact string matching on the canonical form, keeping query performance O(1) on indexed lookups.

Retraction and contradiction compatibility: Ingest normalization is safe for retractions (§5.4) and contradiction detection (§3.3). If a retraction and the original fact both normalize to the same canonical entity, they match correctly.

§2.6.5 Query-time backward compatibility

For nodes upgrading to pre-reset, query parameters are also normalized before matching:

GET /v1/facts?entity=<raw>&...

The node MUST apply normalize_entity_uri to the entity and source query parameters before executing the database query. This allows clients holding references to pre-normalization forms to still retrieve existing facts written after pre-reset is deployed.

For pre-pre-reset facts stored with non-canonical URIs, the alias table (§2.6.6, migration 003) is the recommended migration path.

§2.6.6 Migration guide for existing facts

Facts stored before pre-reset strict normalization was deployed may use informal URIs or non-canonical formal URIs. Because facts are immutable (§2), they cannot be rewritten in place.

Option
When to use
How it works
A · Alias table
recommended for production
Migration 003 adds an entity_aliases table that maps known informal/legacy URIs to their canonical equivalents (see §10). Populate by scanning the facts table for non-canonical values. At query time, the node joins against this table to find pre-pre-reset facts via canonical queries.
B · Re-assertion sweep
smaller nodes / clean migration windows
For each fact with a non-canonical entity URI: (1) Assert a new fact with the canonical entity and same (relation, value, scope, confidence), provenance source="system:stigmem:migration". (2) Retract the original fact by asserting confidence=0.0. Originals retained with confidence=0.0 for audit.

Phased rollout recommendation:

Phase
Window
Action
pre-reset deploy
T+0
Enable strict normalizer on ingest. Query normalization enabled.
Scan + alias
T+2 weeks
Scan facts table; populate alias table for any non-canonical existing facts.
Re-assertion sweep
T+4 weeks
For nodes with < 10k facts; otherwise maintain alias table.
Alias removal
pre-reset spec target
Remove alias table read path; all facts use canonical URIs.

§2.7 Garden field

An optional garden_id field on a fact associates it with a Memory Garden (§17).

FactRecord (the pre-reset spec extension):
...all the pre-reset spec fields...
garden_id: URI | null // stigmem://authority/garden/{slug}; null = no garden
attested: boolean | null // source attestation result (§18); null = not applicable

garden_id invariant. When garden_id is set:

  1. The garden MUST exist on the local node.
  2. The writing principal MUST hold writer or admin role in the garden.
  3. The fact's scope MUST equal the garden's declared scope.
  4. Garden-tagged facts are subject to garden ACL at read time (§17.3).

garden_id on federation. Garden membership is node-local. Facts with garden_id set MUST NOT be replicated to peers. Nodes MUST silently drop garden_id from federated facts they receive.

attested semantics:

Value
Status
Meaning
true
verified
Node verified that source equals the caller's authenticated entity_uri.
false
mismatch
Source/identity mismatch detected; fact accepted in warn or off mode.
null
not applicable
Auth disabled, federation ingest, or system fact.
Revisions before v1.0: pre-reset draft

From stigmem-spec-pre-reset draft.md:

2.7 Garden Field — the pre-reset spec

An optional garden_id field on a fact associates it with a Memory Garden (§17).

FactRecord (the pre-reset spec extension):
...all the pre-reset spec fields...
garden_id: URI | null // the pre-reset spec: stigmem://authority/garden/{slug}; null = no garden
attested: boolean | null // the pre-reset spec: source attestation result (§18); null = not applicable

garden_id invariant: When garden_id is set:

  1. The garden MUST exist on the local node.
  2. The writing principal MUST hold writer or admin role in the garden.
  3. The fact's scope MUST equal the garden's declared scope.
  4. Garden-tagged facts are subject to garden ACL at read time (§17.3).

garden_id on federation: Garden membership is node-local. Facts with garden_id set MUST NOT be replicated to peers. Nodes MUST silently drop garden_id from federated facts they receive (so cross-node garden membership doesn't accidentally leak or create ghost associations).

attested semantics:

ValueMeaning
trueNode verified that source equals the caller's authenticated entity_uri.
falseSource/identity mismatch detected; fact accepted in warn or off mode.
nullAttestation not applicable: auth disabled, federation ingest, or system fact.

§2.8 Federation trust fields

Three optional fields extend the fact record to carry provenance, attestation evidence, and source-trust information.

FactRecord (v0.9.0a1):
...all canonical FactRecord fields...
derived_from: [FactHash] | null // provenance: hashes of facts this derives from (§19.6)
attestation_chain: [Signature] | null // ordered attestation signatures (§19.6)
source_trust: float | null // cached source-trust score at write time (§19.4); null = not computed
Field
Type
Semantics
derived_from
[FactHash] | null
Hex-encoded SHA-256 hashes of antecedent facts. Ordered by logical derivation; first entry is most direct.
attestation_chain
[Signature] | null
Base64url-encoded Ed25519 signatures from org manifest keys. Ordered from innermost to outermost signer; empty array equivalent to null.
source_trust
float [0.0, 1.0] | null
Cached source-trust score at write time (§19.4). Nodes SHOULD populate when computation enabled. Nodes MUST NOT reject facts with a low value at write time; the value is informational. Recomputed at recall time — MUST NOT be relied upon as final from the stored record; stored value is a snapshot for audit.