Skip to main content

Bi-temporal PermissionedDataPoint

Every node and every edge in the KB carries the same envelope:

class TemporalRange(BaseModel):
valid_from: datetime # event time start
valid_to: Optional[datetime] # event time end (None = still true)
created_at: datetime # system time start
expired_at: Optional[datetime] # system time end (None = active)

class PermissionedDataPoint(BaseModel):
id: UUID
type: str = "PermissionedDataPoint"
temporal: TemporalRange
acl: ACL # owner + role-based + ABAC + ReBAC anchors
provenance: Provenance # dataset_id + data_id + extractor chain
layer: LayerMembership # PRIVATE / HIERARCHICAL / MARKETPLACE / GLOBAL
index_fields: list[str] # which fields feed the vector embedding
properties: dict[str, Any]

Two timelines​

Following the Graphiti / Zep four-timestamp model:

PairTracksCloses when
valid_from / valid_toReal-world event timeFact stops being true
created_at / expired_atSystem ingest timeFact is logically invalidated

A contradicted edge closes valid_to (and optionally expired_at + invalidated_by_edge_id) — it is never deleted. This preserves the timeline for as_of=<past_ts> queries.

Naming: this page's created_at / expired_at is the transaction axis​

The canonical platform vocabulary is valid_from / valid_to / tx_from / tx_to, defined once in alphaswarm_core.graph.envelope — see the Graph Data Pillar. The KB's TemporalRange above predates that decision and spells the transaction axis in the Graphiti style:

This pageCanonical
valid_fromvalid_from
valid_tovalid_to
created_attx_from
expired_attx_to

Both name the same two axes; only the transaction-axis spelling differs. The KB's SOKG projection records (SourceAssertion / DocumentAssertion) already emit the canonical names, so what crosses the boundary into the graph service is canonical — the Graphiti-style names survive only inside the KB's own envelope and its Graphiti adapter, which is exactly where legacy vocabulary is permitted to live.

alphaswarm_core.graph.envelope ships LEGACY_FIELD_ALIASES and canonicalize_envelope() for adapters that need to translate a payload.

Interval semantics​

Intervals are half-open, [from, to): a fact is valid at t when valid_from <= t < valid_to. Back-to-back facts therefore never both match the boundary instant — exactly one is visible at any point in time.

Provenance chain​

Provenance carries dataset_id + data_id + the extractor chain (["spacy", "gliner", "llm"]) + the pipeline run id. When a tenant requests targeted forgetting (GDPR / CCPA), KBRuntime.forget locates rows by dataset/data id and closes their validity window.

ACL envelope​

The ACL block carries:

  • owner_principal_id + owner_tenant_id (RBAC anchor).
  • roles_read / roles_write / roles_delete (RBAC).
  • abac_tags (ABAC — region, classification, time-of-day, ...).
  • rebac_anchor_ids (OpenFGA tuple keys like document:abc#viewer).
  • deny_principal_ids (explicit denial list).

DefaultPermissionResolver (in kb-permissions.md) fuses all four into a single per-request AccessBitmap.

Bi-temporal merge in the composer​

DefaultLayerComposer.compose_recall collects hits across layers (private > hierarchical > marketplace > global), then applies the precedence-aware bi-temporal merger:

  1. Group hits by entity id.
  2. The first occurrence (highest precedence) wins.
  3. Lower-precedence hits get appended to the winning hit's dissenting_layers (ComposedHit.dissenting_layers) so the UI can surface them transparently.
  4. valid_from/valid_to are preserved on every hit so a downstream as_of reconstruction is lossless.