Skip to main content

Prism Tri-Graph Knowledge Representation

Architectural overview of the Canonical / Semantic / Temporal graph layers introduced by SPEC-020 (Plan #6, shipped 2026-04-19/20). Audience: external collaborators + future agents working on Prism.

1. Why

Before the tri-graph, Prism’s Neo4j graph was a single-layer (:Memory)-[:MENTIONS]->(:Concept) co-occurrence index. That structure:
  • Conflated identity with current-state (a project “is” its name, so renaming breaks identity).
  • Conflated aliases (surface-form variants: “PrismGR” = old name for “Prism”) with references (typed relationships: ADR-19 FORMALIZES SPEC-019).
  • Had no notion of time — no way to ask “what was true as of last month” or distinguish a stale fact from a current one.
The observable failure: semantic_recall("PrismGR installation") returned historical memories with the same confidence as current-state memories, with no way to tell which was which. Frank’s directive: “close to perfection, perform like a bat out of hell.” That’s a precision target the old graph couldn’t hit structurally. SPEC-020 adopts Frank’s Self-Discovering Ontology (SDO) principle from a prior domain-discovery project: three distinct reasoning modes require three distinct graph structures, and conflating them corrupts each.

2. The Three Layers

Each layer answers a different question:
  • Canonical“what is this and what kind of thing is it?”
  • Semantic“what is this similar to?”
  • Temporal“what did this look like at time T?”
Conflating the three is what the pre-SPEC-020 graph did, and it’s what the failure case in §1 comes from. Separating them gives each operation a structure that matches its question.

2.1 Canonical — Identity + Type Ontology

Stable facts about what kinds of things exist and who each thing is. Nodes:
  • (:Type {zone, name, ptype, immutable_props, mutable_props}) — the type ontology. System-zone types (Project, Persona, Session) apply to every Prism project. Project-zone types depend on the ptype (application declares ADR, SPEC, Plan, TODO, Delta, Journal, Retro, Component, EnvVar, Location, Mode).
  • (:Entity {uuid, type_name, tenant_id, namespace, created_at, source_memory_id}) — identity registry. Carries only metadata. Domain properties (name, title, status, etc.) live on :EntityState, not here.
Edges (Canonical only):
  • (:Entity)-[:INSTANCE_OF]->(:Type) — every entity has exactly one.
Invariants:
  • :Entity.uuid unique per (tenant_id, namespace).
  • :Entity MUST NOT carry any property outside the whitelist (enforced at the application layer; Neo4j Community can’t enforce this directly).

2.2 Semantic — Aliases + Typed References

Meaning. Which surface forms refer to the same entity, and how distinct entities relate. Edges:
  • (:Entity)-[:ALIAS_OF {surface_form, authority_tier, source_memory_id}]->(:Entity) — the target is the preferred/canonical form. Directional: retrieval always resolves toward the preferred form. “PrismGR” aliases to the Prism Project entity.
  • (:Entity)-[:REFERENCES|:FORMALIZES|:DEPENDS_ON|:SPECIFIES|...]->(:Entity) — typed cross-entity relationships. The allowed set is declared by the Canonical layer: (:Type)-[:CAN_REFERENCE]->(:Type) edges constrain which type pairs can participate.
Authority tiers (per Q2 resolution):
  • Tier 1: human-direct (Frank files via prism_alias). Writes directly.
  • Tier 2: agent-with-artifact (an ADR’s content implies an alias; the agent files it as part of authoring). Writes directly.
  • Tier 3: pattern extractor (ambient memory scan detects a likely alias). Lands in alias_candidates queue for review.

2.3 Temporal — State Versions + Events

Change. How an entity’s mutable state evolves and what caused each change. Nodes:
  • (:EntityState {uuid, entity_uuid, valid_from, valid_until, commit_status, source_memory_id, <domain props>}) — one state per entity per time interval. Domain props (name, title, status, etc.) live here. valid_until=NULL means “still current.”
Edges:
  • (:Entity)-[:HAS_STATE]->(:EntityState) — entity to each of its state versions.
  • (:EntityState)-[:SUPERSEDED_BY {event_type, at, cause_memory_id, cause_commit, event_description, props_drift}]->(:EntityState) — state transitions as first-class events. event_type describes the nature (“rename”, “status_transition”, “mode_change”, “wip_sealed”), cause_memory_id pins the supersession to a memory row for provenance.
Invariants (application-layer-enforced):
  • At most one :EntityState per entity with valid_until=NULL and commit_status='committed' (the sealed current state).
  • At most one :EntityState per entity with valid_until=NULL and commit_status='wip' (the WIP current state per v1.3).
  • New state’s valid_from equals prior state’s valid_until — no gaps, no overlaps.
  • Supersession chain is acyclic.

3. Computed Current-State (not stored)

Per Q1 resolution, the current state of an entity is computed by walking :HAS_STATE filtered by valid_until IS NULL, not stored as a direct :HAS_CURRENT_STATE edge. Rationale: the invariant “at most one sealed current state” is enforced by the data, not by careful code maintenance. An index on (entity_uuid, valid_until) makes the lookup O(1).

4. WIP (Work-In-Progress) States per v1.3

A conversational/authorial state for exploration that hasn’t crystallized yet. An agent noticing intent signals (“let’s noodle on the cypher shape”, “draft a response”, extended open-ended reasoning without convergence) SHOULD call prism_wip proactively — asymmetric cost:
  • Missing an intent signal loses exploration.
  • False-positive WIP seals cheaply when the work converges.
Verb: prism_wip(entity_uuid, props, source={session_intent|working_tree|explicit}) — creates or updates the single WIP state for the entity. Sealed + WIP coexist; retrieval returns the WIP as current (with tag [wip]) when prefer_wip=True. Sealing: prism_seal(wip_state_uuid, sealing_event, props_at_seal) closes the WIP, opens a sealed state, and creates a :SUPERSEDED_BY {event_type="wip_sealed", props_drift: bool} edge. props_drift=true means the final sealed props differed from the in-flight WIP props — useful for auditing where reasoning changed the final decision. Stale-WIP detection: WIP states with touched_at older than 30 days surface in prism_start.rules_reminders with category stale_wip.

5. Retrieval — Three-Phase Annotation

semantic_recall gains an optional as_of: datetime | None = None parameter plus two response fields per hit:
{
  "id": "…",
  "content": "…",
  "score": 0.032,
  "state_flag": "historical",           // 'current' | 'historical' | 'wip' | null
  "state_summary": "[historical: current Project is Prism, this reflects PrismGR]"
}
The pipeline:
  1. Phase 1 — Query entity resolution. Scan query text for entity mentions via two paths: :ALIAS_OF surface-form matching (walks the transitive chain to the canonical entity) and state-prop direct matching (hits entities whose current state’s name/title/pid/fqdn appears in the query). Produces a set of anchor entity UUIDs.
  2. Phase 2 — Temporal resolution. For each anchor, fetch the state valid at as_of (or current, WIP-preferred per v1.3 flag). Include full state history and alias surface_forms for annotation context.
  3. Phase 3 — Memory recall + truth-anchor annotation. Run the 4-leg RRF (vector + lexical + graph + temporal) and post-process each hit: match its content against the anchor’s identifier surface forms (longest match wins). Tag the hit current / historical / wip accordingly. Alias surface forms are historical by construction (an alias exists because the form is non-canonical). The graph leg traverses the Semantic-layer typed edges (:REFERENCES and the subtypes :IMPLEMENTS / :EXTENDS / :RELATES_TO / :SUPERSEDES / :DEPENDS_ON / :SPECIFIES / :FORMALIZES / :RESOLVES / :DOCUMENTS) when the query mentions SPEC-N or ADR-N tokens, and falls back to the legacy :MENTIONS co-occurrence path otherwise. The temporal leg scores each candidate by exp(-age_days / half_life) with half_life=180d — a default-on recency-decay re-weight over the existing fused candidate set.
Fail-soft: when Neo4j is unavailable or no tri-graph anchors match the query, hits pass through with state_flag=None. Existing callers see no behavior change.

6. New MCP Verbs

Canonical / Types / Entities

VerbPurpose
prism_list_types()List the 14 seeded :Type nodes
prism_entity(pid, type_name, initial_props)Create entity + initial state atomically
prism_list_entities(pid, type_name?, limit)List entities with current state

Semantic / Aliases / References / Review Queue

VerbPurpose
prism_alias(pid, surface_form, canonical_entity_uuid, authority_tier)Q2 three-tier alias authoring
prism_resolve_alias(pid, surface_form)Walk :ALIAS_OF* to canonical
prism_reference(pid, source, target, reference_type)Typed cross-entity edge, :CAN_REFERENCE-constrained
prism_list_references(pid, source?, target?, type?)List reference edges
prism_list_fact_candidates(pid, status_filter?)Review queue — pattern-extracted state changes
prism_review_fact_candidate(pid, candidate_id, new_status)Promote / reject
prism_list_alias_candidates(pid)Tier-3 alias queue
prism_review_alias_candidate(pid, candidate_id, new_status)Promote / reject

Temporal / State Changes / WIP

VerbPurpose
prism_fact(pid, entity_uuid, predicate, value)Single-prop state transition
prism_transition(pid, entity_uuid, props, event_type)Multi-prop ACID transition (Q3)
prism_wip(pid, entity_uuid, props, source)Create/update WIP state (v1.3)
prism_seal(pid, wip_state_uuid, props_at_seal)Close WIP, open sealed, flag props_drift
prism_state_as_of(pid, entity_uuid, as_of, prefer_wip)Point-in-time state lookup

Retrieval

semantic_recall gains as_of: str | None = None + prefer_wip: bool = True.

7. How Plan #6 Shipped

Six waves, all non-breaking, all deployed on server1:
  • Wave A — Foundation: Postgres review-queue tables + Neo4j schema constraints / indexes. Nothing reads or writes new schema yet.
  • Wave B — Canonical: Type seeding from JSON data source, prism_entity verb, invariant validation.
  • Wave C — Semantic + Temporal: aliases, references, state transitions, WIP/seal. The biggest wave.
  • Wave D — Retrieval: as_of parameter, three-phase pipeline, truth-anchor annotation. Fail-soft so existing callers are unaffected.
  • Wave E — Migration: host-side mcp/trigraph_migrate.py seeds every PID-PGR01 artifact as a tri-graph entity with valid_from=row.created_at. Also registers the known “PrismGR” / “prismgr” aliases (tier 1) and extracts :REFERENCES edges from ADR/SPEC/Plan bodies. Idempotent.
  • Wave F — Validation: complete 25-test smoke battery, stale-WIP detection surfaces in prism_start.rules_reminders, live demo per SPEC-020 §13 passing on server1.

8. What’s Deferred

  • Phase 8 — drop old schema: remove :Memory / :Concept / :MENTIONS labels and their associated code. Gated on one release-cycle of Phase 7 green.
Deliberate deferral with a named gate, not scope creep.

8.1 Phase 5 graph-leg rewire — shipped 2026-05-03

The graph leg now traverses the Semantic layer for entity-anchored queries (PR #102 6771f46, Plan #8 Phase 4): when the query mentions SPEC-N or ADR-N tokens it 1-hop bidirectional-walks :REFERENCES + :IMPLEMENTS + :EXTENDS + :RELATES_TO + :SUPERSEDES + :DEPENDS_ON + :SPECIFIES + :FORMALIZES + :RESOLVES + :DOCUMENTS edges and sums per-target edge counts. Queries without entity anchors fall back to the legacy :MENTIONS co-occurrence path so existing callers see no behavior change. Edge auto-extraction (PR #101 c9bb9b4) populates the typed edges from spec / ADR / plan / retro / journal markdown headings on every write. The temporal leg activated as a fourth RRF fuse leg the same day (PR #103 99561fb, Plan #8 Phase 5): exp(-age_days / half_life) with half_life=180d re-weights the existing fused candidate set rather than adding new candidates, so recall composition is unchanged but recent-but-relevant memories rank higher on historical-context queries.

9. Performance

Plan #6 shipped zero user-visible regression across all six waves:
PointLocal recall p50Server1 recall p50
Pre-Plan-6 baseline197 ms305 ms
Post-Wave-A197 ms
Post-Wave-B189 ms
Post-Wave-C186 ms
Post-Wave-D187 ms315 ms (+3.4%)
Post-Wave-E185 ms308 ms (+1.1%)
Post-Wave-F188 ms304 ms (0%)
Annotation overhead on recall is ~0 when no tri-graph anchors match the query (the most common case pre-Wave-E). Post-Wave-E, server1 carries 199 entities and annotation adds one Cypher round-trip (~5ms) per call.
  • SPEC-020 — frozen architecture spec (filed as memory deltas 56e279ac v1.0 + 4c910dd8 v1.2 + 0ddd894b v1.3, Q1-Q6 resolutions 5cae8334, formally re-filed via prism_spec as fbb9cb51-e535-43b1-903d-af56f4e9aac2 on 2026-04-21).
  • SPEC-021 (Ring 0) — builds on the tri-graph + ring chain with a bootstrap-enforcement layer that sits outside Ring 1 BIOS. The tri-graph’s identity-stability guarantee (a project UUID survives renames) is what lets Ring 0’s L4 hygiene layer safely rewrite cached summaries when a project renames or advances phase — the identity the summary refers to is stable even if the label changes. See SPEC-021 §4.4 for the wrap-time update payload shape.
  • ADR-23 — formal “Adopt tri-graph knowledge representation” ADR. Promotes the original tri-graph acceptance memory (delta 966082d0, initially filed as “ADR-22” in-memory) to the formal ADR table.
  • Plan #6 — the six-wave execution plan (filed in plans table). The six-wave pattern — old schema and new schema coexist through every intermediate wave, never a hard cutover — graduated from this plan to a global methodology lesson via retro-005: any architectural refactor that touches a queryable surface should use a coexistence-not-replacement rollout when the old surface has live consumers. Applies to SPEC-024’s retirement migration and any future schema-shape changes.
  • Retro #5retros/retro-005-plan-6-spec-020-tri-graph-knowledge-representation. Source of the six-wave-coexistence global lesson; also captures the authority-tier model for agent-written aliases (tier 1 human-direct, tier 2 agent-with-artifact, tier 3 pattern-extractor-candidate).
  • Commits: eaad16e4c66446ef3f502bd52dcfd33b75bdf8e50e.
Last modified on June 7, 2026