learning_ai_common_plat/packages/palace/src/kg.ts
saravanakumardb1 d1c6cf47c8 feat(palace): add @bytelyst/palace shared package — MemPalace primitives (91 tests)
New shared package: packages/palace/ (@bytelyst/palace)

Modules:
- types.ts — BasePalaceWingDoc, RoomDoc, MemoryDoc, TunnelDoc, KGTripleDoc, DiaryDoc
- halls.ts — HallType union, HALL_PRESETS (notelett/mindlyst/coding), hallFromLabel()
- cosine.ts — cosineSimilarity(), topKByCosine(), normalizeVector()
- dedup.ts — isContentDuplicate(), isExactDuplicate(), findClosestMatch()
- decay.ts — computeDecayedRelevance(), boostRelevance()
- extraction.ts — buildExtractionPrompt(), parseExtractionResponse(), regexFallbackExtraction()
- kg.ts — findContradictions(), mergeTriples(), isTripleCurrent()
- wakeup.ts — buildWakeUpLayers(), truncateToTokenBudget(), WAKEUP_PRESETS
- config.ts — palaceConfigSchema (Zod)

7 test files, 91 tests passing.
Consumed by NoteLett, MindLyst, and future palace-enabled products.
2026-04-10 00:57:00 -07:00

139 lines
3.9 KiB
TypeScript

/**
* Knowledge graph helpers for palace triple management.
*
* Triples are (subject, predicate, object) with temporal validity.
* This module provides pure functions for contradiction detection,
* merging, and currency checking.
*/
/**
* A lightweight triple for comparison (does not require full doc fields).
*/
export interface TripleInput {
subject: string;
predicate: string;
object: string;
validFrom: string;
validTo?: string;
confidence?: number;
}
/**
* Find contradictions between existing triples and incoming ones.
*
* A contradiction exists when:
* - Same subject + predicate, different object
* - Both are currently valid (no validTo or validTo in the future)
*
* @returns Array of { existing, incoming } contradiction pairs
*/
export function findContradictions(
existing: TripleInput[],
incoming: TripleInput[],
asOf: Date = new Date()
): Array<{ existing: TripleInput; incoming: TripleInput }> {
const contradictions: Array<{ existing: TripleInput; incoming: TripleInput }> = [];
for (const inc of incoming) {
for (const ext of existing) {
if (
normalizeEntity(ext.subject) === normalizeEntity(inc.subject) &&
normalizeEntity(ext.predicate) === normalizeEntity(inc.predicate) &&
normalizeEntity(ext.object) !== normalizeEntity(inc.object) &&
isTripleCurrent(ext, asOf) &&
isTripleCurrent(inc, asOf)
) {
contradictions.push({ existing: ext, incoming: inc });
}
}
}
return contradictions;
}
/**
* Merge incoming triples into existing set.
*
* - If an incoming triple contradicts an existing one, the existing triple
* is invalidated (validTo set) and the incoming one is kept.
* - If an incoming triple is a duplicate (same S/P/O), it is skipped.
* - Otherwise, the incoming triple is added.
*
* @returns { merged, invalidated, added, skipped } counts
*/
export function mergeTriples(
existing: TripleInput[],
incoming: TripleInput[],
asOf: Date = new Date()
): {
merged: TripleInput[];
invalidated: TripleInput[];
added: TripleInput[];
skipped: TripleInput[];
} {
const invalidated: TripleInput[] = [];
const added: TripleInput[] = [];
const skipped: TripleInput[] = [];
const merged = [...existing];
for (const inc of incoming) {
// Check for exact duplicate
const isDuplicate = merged.some(
ext =>
normalizeEntity(ext.subject) === normalizeEntity(inc.subject) &&
normalizeEntity(ext.predicate) === normalizeEntity(inc.predicate) &&
normalizeEntity(ext.object) === normalizeEntity(inc.object) &&
isTripleCurrent(ext, asOf)
);
if (isDuplicate) {
skipped.push(inc);
continue;
}
// Check for contradiction
const contradictIdx = merged.findIndex(
ext =>
normalizeEntity(ext.subject) === normalizeEntity(inc.subject) &&
normalizeEntity(ext.predicate) === normalizeEntity(inc.predicate) &&
normalizeEntity(ext.object) !== normalizeEntity(inc.object) &&
isTripleCurrent(ext, asOf)
);
if (contradictIdx >= 0) {
// Invalidate the old triple
const old = merged[contradictIdx];
merged[contradictIdx] = { ...old, validTo: asOf.toISOString() };
invalidated.push(old);
}
merged.push(inc);
added.push(inc);
}
return { merged, invalidated, added, skipped };
}
/**
* Check if a triple is currently valid.
*
* @param triple - The triple to check
* @param asOf - Reference time (default: now)
* @returns true if the triple has no validTo or validTo is in the future
*/
export function isTripleCurrent(
triple: Pick<TripleInput, 'validTo'>,
asOf: Date = new Date()
): boolean {
if (!triple.validTo) return true;
return new Date(triple.validTo).getTime() > asOf.getTime();
}
/**
* Normalize an entity string for comparison (lowercase, trim, collapse whitespace).
*/
function normalizeEntity(s: string): string {
return s.toLowerCase().trim().replace(/\s+/g, ' ');
}