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:
| Pair | Tracks | Closes when |
|---|---|---|
valid_from / valid_to | Real-world event time | Fact stops being true |
created_at / expired_at | System ingest time | Fact 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 page | Canonical |
|---|---|
valid_from | valid_from |
valid_to | valid_to |
created_at | tx_from |
expired_at | tx_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 likedocument: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:
- Group hits by entity
id. - The first occurrence (highest precedence) wins.
- Lower-precedence hits get appended to the winning hit's
dissenting_layers(ComposedHit.dissenting_layers) so the UI can surface them transparently. valid_from/valid_toare preserved on every hit so a downstreamas_ofreconstruction is lossless.