From 03319c1fe48ba1ef7edf218f412761088de954db Mon Sep 17 00:00:00 2001 From: rrader2890 Date: Wed, 19 Aug 2026 07:54:42 -0400 Subject: [PATCH] fix(graph): model the edge read routes' actual hydrated shape MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `listEdges`, `traverse`, and `getEntity().edges` do not return the raw `memory_edge` row. They return the server's `GraphTraversalEdge`: `subject` and `object` are fully hydrated entities rather than ids, plus a `hop` counter — and there is no `subjectId`, `objectId`, or `brainId` on the wire at all. Caught by running the Rust port against production, where strict decoding failed with `missing field 'subjectId'`. TypeScript's structural typing hid it — the call compiled and then handed back objects whose declared fields were all undefined at runtime. Drops the raw-row `MemoryEdge` type rather than keeping it alongside. No read route returns that shape, and a type nothing returns is a trap. `hop` is 0 from `listEdges` (no seed) and 1-indexed from `traverse`. Verified live: NVIDIA CORP -[reported_metric]-> Cost of Revenue, decoded through the corrected type. --- src/index.ts | 2 +- src/resources/graph.ts | 10 +++---- src/types/graph.ts | 61 +++++++++++++++++++++--------------------- 3 files changed, 37 insertions(+), 36 deletions(-) diff --git a/src/index.ts b/src/index.ts index 5cdca7f..43aafa3 100644 --- a/src/index.ts +++ b/src/index.ts @@ -257,7 +257,7 @@ export type { export type { MemoryEntity, MemoryEntityType, - MemoryEdge, + GraphTraversalEdge, GraphStats, ListEntitiesParams, ListEdgesParams, diff --git a/src/resources/graph.ts b/src/resources/graph.ts index 7aeb44b..630b4fd 100644 --- a/src/resources/graph.ts +++ b/src/resources/graph.ts @@ -5,7 +5,7 @@ import type { GraphStats, ListEdgesParams, ListEntitiesParams, - MemoryEdge, + GraphTraversalEdge, MemoryEntity, TraverseParams, } from '../types/graph.js' @@ -65,8 +65,8 @@ export class GraphResource { * Every currently-valid edge. Use for rendering a whole small graph; for a * large one, seed from an entity and `traverse` instead. */ - async listEdges(params?: ListEdgesParams, options?: RequestOptions): Promise { - return this.http.get( + async listEdges(params?: ListEdgesParams, options?: RequestOptions): Promise { + return this.http.get( '/admin/memory/graph/edges', params as Record, options, @@ -84,8 +84,8 @@ export class GraphResource { entityId: string, params?: TraverseParams, options?: RequestOptions, - ): Promise { - return this.http.post( + ): Promise { + return this.http.post( '/admin/memory/graph/traverse', { entityId, ...params }, options, diff --git a/src/types/graph.ts b/src/types/graph.ts index 4673387..ea8c74b 100644 --- a/src/types/graph.ts +++ b/src/types/graph.ts @@ -6,12 +6,10 @@ * reach a fact no single memory states outright ("who does Sarah report to?" * answered from `sarah -[member_of]-> team` + `team -[led_by]-> priya`). * - * Both records are bi-temporal, and the two time axes mean different things: - * - `validFrom` / `validTo` — when the fact was TRUE in the world. - * - `expiredAt` (edges) — when the graph stopped BELIEVING it, because a - * contradicting edge superseded it. - * A fact that was true last year and a fact we were wrong about are not the - * same thing, and collapsing them loses the audit trail. + * Entities and edges are bi-temporal: `validFrom` / `validTo` say when the fact + * was TRUE in the world, which is not the same as when we believed it. The read + * routes return only currently-believed edges, so a row superseded by a + * contradicting one simply stops appearing rather than coming back flagged. */ import type { MemoryScope } from './memory.js' @@ -26,8 +24,8 @@ export interface MemoryEntity { projectId: string | null /** * The brain that first created this entity. Entities dedupe per project, so - * this is provenance, NOT an isolation key — use `MemoryEdge.brainId` for - * brain-scoped graph work. + * this is provenance, NOT an isolation key — brain-scoped graph work filters + * on the edge's `brainId`, which the read routes do server-side. */ brainId: string | null locationId: string | null @@ -46,36 +44,39 @@ export interface MemoryEntity { supersededById: string | null } -export interface MemoryEdge { +/** + * An edge as the READ routes return it — hydrated, not the raw `memory_edge` + * row. `subject` and `object` are resolved entities rather than ids, and `hop` + * says how far from the seed the walk found it. + * + * This is the server's `GraphTraversalEdge`, returned by `listEdges`, + * `traverse`, and the `edges` of `getEntity`. The raw row shape (with + * `subjectId` / `objectId` / `brainId`) is not exposed by any read route, so it + * is deliberately not modelled here — a type nothing returns is a trap. + */ +export interface GraphTraversalEdge { + /** Edge primary key — needed for invalidation. */ id: string - created: string - updated: string - platformId: string - projectId: string | null - /** Per-brain KG isolation — edges are written fresh per brain, so this one - * IS the enforceable key for brain-scoped walks. Null on legacy edges. */ - brainId: string | null - locationId: string | null - chatbotId: string | null - chatIdentityId: string | null - scope: MemoryScope - /** Entity id this edge starts from. */ - subjectId: string + /** The entity this edge starts from, hydrated. */ + subject: MemoryEntity /** The relationship — `works_at`, `owns`, `located_in`, ... */ predicate: string - /** Entity id, when the object is itself an entity. */ - objectId: string | null + /** The target entity, hydrated. `null` when `objectLiteral` carries the value. */ + object: MemoryEntity | null /** Literal value, when the object is not an entity (a date, a price, "v1.2.3"). */ objectLiteral: string | null /** Confidence, 0..1. */ weight: number - /** The memory this edge was extracted from. */ - sourceMemoryId: string | null - metadata: Record | null validFrom: string + /** Null while the edge is still valid. */ validTo: string | null - /** Set when a contradicting edge superseded this one. Null = still believed. */ - expiredAt?: string | null + /** The memory this edge was extracted from. */ + sourceMemoryId: string | null + /** + * Distance from the seed entity on a `traverse` — 1 for a direct neighbour. + * `listEdges` has no seed, so every edge comes back with `hop: 0`. + */ + hop: number } export interface ListEntitiesParams { @@ -126,5 +127,5 @@ export interface GraphStats { /** An entity plus its 1-hop neighbourhood. */ export interface EntityWithEdges { entity: MemoryEntity | null - edges: MemoryEdge[] + edges: GraphTraversalEdge[] }