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:
-
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.
-
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.
-
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
| Verb | Purpose |
|---|
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
| Verb | Purpose |
|---|
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
| Verb | Purpose |
|---|
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.
Plan #6 shipped zero user-visible regression across all six waves:
| Point | Local recall p50 | Server1 recall p50 |
|---|
| Pre-Plan-6 baseline | 197 ms | 305 ms |
| Post-Wave-A | 197 ms | — |
| Post-Wave-B | 189 ms | — |
| Post-Wave-C | 186 ms | — |
| Post-Wave-D | 187 ms | 315 ms (+3.4%) |
| Post-Wave-E | 185 ms | 308 ms (+1.1%) |
| Post-Wave-F | 188 ms | 304 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 #5 —
retros/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:
eaad16e → 4c66446 → ef3f502 → bd52dcf →
d33b75b → df8e50e.
Last modified on June 7, 2026