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.
139 lines
3.9 KiB
TypeScript
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, ' ');
|
|
}
|