{"version":3,"file":"kg-entity-BSW7ra8i.mjs","names":[],"sources":["../src/memory/kg.ts","../src/memory/kg-entity.ts"],"sourcesContent":["/**\n * Temporal Knowledge Graph — kg_triples CRUD layer.\n *\n * Uses the Postgres connection pool from the storage backend.\n * Triples are time-scoped: valid_from/valid_to enable point-in-time queries.\n * Invalidation sets valid_to = NOW() instead of deleting rows.\n */\n\nimport type { Pool } from \"pg\";\n\n// ---------------------------------------------------------------------------\n// Types\n// ---------------------------------------------------------------------------\n\nexport interface KgTriple {\n  id: number;\n  subject: string;\n  predicate: string;\n  object: string;\n  project_id?: number;\n  source_session?: string;\n  valid_from: Date;\n  valid_to?: Date;\n  confidence: \"EXTRACTED\" | \"INFERRED\" | \"AMBIGUOUS\";\n  created_at: Date;\n}\n\nexport interface KgAddParams {\n  subject: string;\n  predicate: string;\n  object: string;\n  project_id?: number;\n  source_session?: string;\n  confidence?: \"EXTRACTED\" | \"INFERRED\" | \"AMBIGUOUS\";\n}\n\nexport interface KgQueryParams {\n  subject?: string;\n  predicate?: string;\n  object?: string;\n  project_id?: number;\n  as_of?: Date;\n  include_invalidated?: boolean;\n}\n\nexport interface KgContradiction {\n  subject: string;\n  predicate: string;\n  objects: string[];\n}\n\n// ---------------------------------------------------------------------------\n// Helpers\n// ---------------------------------------------------------------------------\n\nfunction rowToTriple(row: Record<string, unknown>): KgTriple {\n  return {\n    id: row.id as number,\n    subject: row.subject as string,\n    predicate: row.predicate as string,\n    object: row.object as string,\n    project_id: row.project_id as number | undefined,\n    source_session: row.source_session as string | undefined,\n    valid_from: new Date(row.valid_from as string),\n    valid_to: row.valid_to ? new Date(row.valid_to as string) : undefined,\n    confidence: row.confidence as \"EXTRACTED\" | \"INFERRED\" | \"AMBIGUOUS\",\n    created_at: new Date(row.created_at as string),\n  };\n}\n\n// ---------------------------------------------------------------------------\n// Core operations\n// ---------------------------------------------------------------------------\n\n/**\n * Add a new triple to the knowledge graph.\n * Returns the inserted triple.\n */\nexport async function kgAdd(pool: Pool, params: KgAddParams): Promise<KgTriple> {\n  const confidence = params.confidence ?? \"EXTRACTED\";\n  const result = await pool.query<Record<string, unknown>>(\n    `INSERT INTO kg_triples\n       (subject, predicate, object, project_id, source_session, confidence)\n     VALUES ($1, $2, $3, $4, $5, $6)\n     RETURNING *`,\n    [\n      params.subject,\n      params.predicate,\n      params.object,\n      params.project_id ?? null,\n      params.source_session ?? null,\n      confidence,\n    ]\n  );\n  return rowToTriple(result.rows[0]);\n}\n\n/**\n * Query triples by subject, predicate, object, and/or project.\n * Supports point-in-time queries via as_of.\n * By default only returns currently-valid triples (valid_to IS NULL).\n */\nexport async function kgQuery(pool: Pool, params: KgQueryParams): Promise<KgTriple[]> {\n  const conditions: string[] = [];\n  const values: unknown[] = [];\n  let idx = 1;\n\n  if (params.subject !== undefined) {\n    conditions.push(`subject = $${idx++}`);\n    values.push(params.subject);\n  }\n  if (params.predicate !== undefined) {\n    conditions.push(`predicate = $${idx++}`);\n    values.push(params.predicate);\n  }\n  if (params.object !== undefined) {\n    conditions.push(`object = $${idx++}`);\n    values.push(params.object);\n  }\n  if (params.project_id !== undefined) {\n    conditions.push(`project_id = $${idx++}`);\n    values.push(params.project_id);\n  }\n\n  if (params.as_of !== undefined) {\n    // Valid at the given timestamp: started before or at as_of, and not yet ended\n    conditions.push(`valid_from <= $${idx++}`);\n    values.push(params.as_of);\n    conditions.push(`(valid_to IS NULL OR valid_to > $${idx++})`);\n    values.push(params.as_of);\n  } else if (!params.include_invalidated) {\n    // Default: only currently-valid (no valid_to set)\n    conditions.push(`valid_to IS NULL`);\n  }\n\n  const where = conditions.length > 0 ? `WHERE ${conditions.join(\" AND \")}` : \"\";\n  const result = await pool.query<Record<string, unknown>>(\n    `SELECT * FROM kg_triples ${where} ORDER BY valid_from DESC`,\n    values\n  );\n  return result.rows.map(rowToTriple);\n}\n\n/**\n * Invalidate a triple by setting valid_to = NOW().\n * Does not delete the row — preserves history.\n */\nexport async function kgInvalidate(pool: Pool, tripleId: number): Promise<void> {\n  await pool.query(\n    `UPDATE kg_triples SET valid_to = NOW() WHERE id = $1 AND valid_to IS NULL`,\n    [tripleId]\n  );\n}\n\n/**\n * Find contradictions: cases where the same (subject, predicate) pair has\n * multiple currently-valid objects.\n */\nexport async function kgContradictions(\n  pool: Pool,\n  subject: string\n): Promise<KgContradiction[]> {\n  const result = await pool.query<{ subject: string; predicate: string; objects: string[] }>(\n    `SELECT subject, predicate, array_agg(object ORDER BY object) AS objects\n     FROM kg_triples\n     WHERE subject = $1\n       AND valid_to IS NULL\n     GROUP BY subject, predicate\n     HAVING COUNT(*) > 1`,\n    [subject]\n  );\n  return result.rows.map((row) => ({\n    subject: row.subject,\n    predicate: row.predicate,\n    objects: row.objects,\n  }));\n}\n","/**\n * kg-entity.ts — Entity content-addressing with multi-tenant support.\n *\n * Provides UUID5-style deterministic content hashes for KG entities and edges,\n * ensuring that the same entity name always maps to the same ID within a tenant.\n * This enables idempotent upserts and stable foreign keys for kg_triples.\n *\n * Multi-tenant support: each tenant namespace gets its own entity ID space.\n * The default tenant is \"default\" for single-user deployments.\n */\n\nimport { createHash } from \"node:crypto\";\nimport type { Database } from \"better-sqlite3\";\n\n// ---------------------------------------------------------------------------\n// Types\n// ---------------------------------------------------------------------------\n\nexport interface KgEntity {\n  entity_id: string;\n  tenant_id: string;\n  name: string;\n  type: string;\n  description?: string;\n  first_seen?: number;\n  last_seen?: number;\n  mention_count: number;\n  feedback_weight: number;\n}\n\nexport interface KgEntityUpsertParams {\n  name: string;\n  type?: string;\n  description?: string;\n  tenantId?: string;\n}\n\n// ---------------------------------------------------------------------------\n// Content addressing\n// ---------------------------------------------------------------------------\n\n/**\n * Generate a deterministic entity ID (UUID5-style) for a given name and tenant.\n *\n * The ID is a hex digest derived from \"tenant_id:name\" so the same entity\n * always receives the same ID within a tenant namespace.\n *\n * @param name      Entity name (case-preserved)\n * @param tenantId  Tenant namespace (default: \"default\")\n */\nexport function entityContentId(name: string, tenantId = \"default\"): string {\n  return createHash(\"sha256\")\n    .update(`entity:${tenantId}:${name}`)\n    .digest(\"hex\")\n    .slice(0, 32); // 128-bit hex string — UUID5-compatible length\n}\n\n/**\n * Generate a deterministic edge ID for a (source, relation, target) triple\n * within a tenant namespace.\n *\n * @param source    Source entity name\n * @param relation  Relation/predicate verb phrase\n * @param target    Target entity name\n * @param tenantId  Tenant namespace (default: \"default\")\n */\nexport function edgeContentId(\n  source: string,\n  relation: string,\n  target: string,\n  tenantId = \"default\"\n): string {\n  return createHash(\"sha256\")\n    .update(`edge:${tenantId}:${source}:${relation}:${target}`)\n    .digest(\"hex\")\n    .slice(0, 32);\n}\n\n// ---------------------------------------------------------------------------\n// SQLite entity upsert (for federation.db)\n// ---------------------------------------------------------------------------\n\n/**\n * Upsert a KG entity in the federation SQLite database.\n *\n * If the entity already exists for this tenant:\n *   - Updates last_seen to now\n *   - Increments mention_count\n *   - Updates description if provided (overwrites older description)\n *\n * Returns the entity_id for use as a foreign key in kg_triples.\n */\nexport function upsertKgEntity(\n  db: Database,\n  params: KgEntityUpsertParams\n): string {\n  const tenantId = params.tenantId ?? \"default\";\n  const entityId = entityContentId(params.name, tenantId);\n  const now = Date.now();\n\n  db.prepare(`\n    INSERT INTO kg_entities\n      (entity_id, tenant_id, name, type, description, first_seen, last_seen, mention_count, feedback_weight)\n    VALUES\n      (?, ?, ?, ?, ?, ?, ?, 1, 0.5)\n    ON CONFLICT(entity_id) DO UPDATE SET\n      last_seen     = excluded.last_seen,\n      mention_count = mention_count + 1,\n      description   = COALESCE(excluded.description, description),\n      type          = CASE WHEN excluded.type != 'unknown' THEN excluded.type ELSE type END\n  `).run(\n    entityId,\n    tenantId,\n    params.name,\n    params.type ?? \"unknown\",\n    params.description ?? null,\n    now,\n    now\n  );\n\n  return entityId;\n}\n\n/**\n * Look up a KG entity by name within a tenant.\n * Returns null if the entity does not exist.\n */\nexport function findKgEntity(\n  db: Database,\n  name: string,\n  tenantId = \"default\"\n): KgEntity | null {\n  const entityId = entityContentId(name, tenantId);\n  const row = db.prepare(\n    \"SELECT * FROM kg_entities WHERE entity_id = ? AND tenant_id = ?\"\n  ).get(entityId, tenantId) as KgEntity | undefined;\n  return row ?? null;\n}\n\n/**\n * List KG entities for a tenant, optionally filtered by type.\n *\n * @param db        Federation SQLite database\n * @param tenantId  Tenant namespace (default: \"default\")\n * @param type      Optional entity type filter\n * @param limit     Maximum entities to return (default: 100)\n */\nexport function listKgEntities(\n  db: Database,\n  tenantId = \"default\",\n  type?: string,\n  limit = 100\n): KgEntity[] {\n  if (type) {\n    return db.prepare(\n      \"SELECT * FROM kg_entities WHERE tenant_id = ? AND type = ? ORDER BY mention_count DESC LIMIT ?\"\n    ).all(tenantId, type, limit) as KgEntity[];\n  }\n  return db.prepare(\n    \"SELECT * FROM kg_entities WHERE tenant_id = ? ORDER BY mention_count DESC LIMIT ?\"\n  ).all(tenantId, limit) as KgEntity[];\n}\n\n// ---------------------------------------------------------------------------\n// Feedback weight update (MR2 — EMA)\n// ---------------------------------------------------------------------------\n\n/**\n * Apply an EMA (Exponential Moving Average) feedback update to an entity's weight.\n *\n * EMA formula: new_weight = old_weight + alpha * (target - old_weight)\n *\n * @param db            Federation SQLite database\n * @param entityId      Entity ID to update\n * @param normalizedRating  Rating normalized to [0, 1] (e.g., rating/5 for 1-5 scale)\n * @param alpha         EMA learning rate (default: 0.1)\n */\nexport function updateEntityFeedbackWeight(\n  db: Database,\n  entityId: string,\n  normalizedRating: number,\n  alpha = 0.1\n): void {\n  const row = db.prepare(\n    \"SELECT feedback_weight FROM kg_entities WHERE entity_id = ?\"\n  ).get(entityId) as { feedback_weight: number } | undefined;\n\n  if (!row) return;\n\n  const newWeight = row.feedback_weight + alpha * (normalizedRating - row.feedback_weight);\n  db.prepare(\n    \"UPDATE kg_entities SET feedback_weight = ? WHERE entity_id = ?\"\n  ).run(newWeight, entityId);\n}\n"],"mappings":"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"}