> ## Documentation Index
> Fetch the complete documentation index at: https://prism.ntecdev.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# TRIGRAPH

# 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

```mermaid theme={null}
flowchart TB
  subgraph canonical["Canonical — Identity & Type Ontology"]
    direction LR
    type["(:Type)"] -.->|INSTANCE_OF| entity["(:Entity)"]
  end
  subgraph semantic["Semantic — Similarity & Embeddings"]
    direction LR
    emb["(:Embedding)"] -.->|SIMILAR_TO| emb2["(:Embedding)"]
  end
  subgraph temporal["Temporal — State Transitions Over Time"]
    direction LR
    state1["(:EntityState)<br/>valid_from…valid_until"] -.->|SUPERSEDED_BY| state2["(:EntityState)<br/>valid_from=now"]
  end

  entity -.->|"has current state"| state1
  entity -.->|"has embedding"| emb

  classDef canonicalLayer fill:#92400e,stroke:#78350f,color:#ffffff
  classDef semanticLayer fill:#1e40af,stroke:#1e3a8a,color:#ffffff
  classDef temporalLayer fill:#166534,stroke:#14532d,color:#ffffff
  classDef item fill:#1e293b,stroke:#475569,color:#e2e8f0,stroke-width:1px
  class canonical canonicalLayer
  class semantic semanticLayer
  class temporal temporalLayer
  class type,entity,emb,emb2,state1,state2 item
```

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:

```json theme={null}
{
  "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

| 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.

## 9. Performance

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.

## 10. Related Artifacts

* **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`.
