@psiclawops/hypermem 0.5.0 → 0.5.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/background-indexer.d.ts +132 -0
- package/dist/background-indexer.d.ts.map +1 -0
- package/dist/background-indexer.js +1044 -0
- package/dist/cache.d.ts +110 -0
- package/dist/cache.d.ts.map +1 -0
- package/dist/cache.js +495 -0
- package/dist/compaction-fence.d.ts +89 -0
- package/dist/compaction-fence.d.ts.map +1 -0
- package/dist/compaction-fence.js +153 -0
- package/dist/compositor.d.ts +226 -0
- package/dist/compositor.d.ts.map +1 -0
- package/dist/compositor.js +2558 -0
- package/dist/content-type-classifier.d.ts +41 -0
- package/dist/content-type-classifier.d.ts.map +1 -0
- package/dist/content-type-classifier.js +181 -0
- package/dist/cross-agent.d.ts +62 -0
- package/dist/cross-agent.d.ts.map +1 -0
- package/dist/cross-agent.js +259 -0
- package/dist/db.d.ts +131 -0
- package/dist/db.d.ts.map +1 -0
- package/dist/db.js +402 -0
- package/dist/desired-state-store.d.ts +100 -0
- package/dist/desired-state-store.d.ts.map +1 -0
- package/dist/desired-state-store.js +222 -0
- package/dist/doc-chunk-store.d.ts +140 -0
- package/dist/doc-chunk-store.d.ts.map +1 -0
- package/dist/doc-chunk-store.js +391 -0
- package/dist/doc-chunker.d.ts +99 -0
- package/dist/doc-chunker.d.ts.map +1 -0
- package/dist/doc-chunker.js +324 -0
- package/dist/dreaming-promoter.d.ts +86 -0
- package/dist/dreaming-promoter.d.ts.map +1 -0
- package/dist/dreaming-promoter.js +381 -0
- package/dist/episode-store.d.ts +49 -0
- package/dist/episode-store.d.ts.map +1 -0
- package/dist/episode-store.js +135 -0
- package/dist/fact-store.d.ts +75 -0
- package/dist/fact-store.d.ts.map +1 -0
- package/dist/fact-store.js +236 -0
- package/dist/fleet-store.d.ts +144 -0
- package/dist/fleet-store.d.ts.map +1 -0
- package/dist/fleet-store.js +276 -0
- package/dist/fos-mod.d.ts +178 -0
- package/dist/fos-mod.d.ts.map +1 -0
- package/dist/fos-mod.js +416 -0
- package/dist/hybrid-retrieval.d.ts +64 -0
- package/dist/hybrid-retrieval.d.ts.map +1 -0
- package/dist/hybrid-retrieval.js +344 -0
- package/dist/image-eviction.d.ts +49 -0
- package/dist/image-eviction.d.ts.map +1 -0
- package/dist/image-eviction.js +251 -0
- package/dist/index.d.ts +650 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +1072 -0
- package/dist/keystone-scorer.d.ts +51 -0
- package/dist/keystone-scorer.d.ts.map +1 -0
- package/dist/keystone-scorer.js +52 -0
- package/dist/knowledge-graph.d.ts +110 -0
- package/dist/knowledge-graph.d.ts.map +1 -0
- package/dist/knowledge-graph.js +305 -0
- package/dist/knowledge-lint.d.ts +29 -0
- package/dist/knowledge-lint.d.ts.map +1 -0
- package/dist/knowledge-lint.js +116 -0
- package/dist/knowledge-store.d.ts +72 -0
- package/dist/knowledge-store.d.ts.map +1 -0
- package/dist/knowledge-store.js +247 -0
- package/dist/library-schema.d.ts +22 -0
- package/dist/library-schema.d.ts.map +1 -0
- package/dist/library-schema.js +1038 -0
- package/dist/message-store.d.ts +89 -0
- package/dist/message-store.d.ts.map +1 -0
- package/dist/message-store.js +323 -0
- package/dist/metrics-dashboard.d.ts +114 -0
- package/dist/metrics-dashboard.d.ts.map +1 -0
- package/dist/metrics-dashboard.js +260 -0
- package/dist/obsidian-exporter.d.ts +57 -0
- package/dist/obsidian-exporter.d.ts.map +1 -0
- package/dist/obsidian-exporter.js +274 -0
- package/dist/obsidian-watcher.d.ts +147 -0
- package/dist/obsidian-watcher.d.ts.map +1 -0
- package/dist/obsidian-watcher.js +403 -0
- package/dist/open-domain.d.ts +46 -0
- package/dist/open-domain.d.ts.map +1 -0
- package/dist/open-domain.js +125 -0
- package/dist/preference-store.d.ts +54 -0
- package/dist/preference-store.d.ts.map +1 -0
- package/dist/preference-store.js +109 -0
- package/dist/preservation-gate.d.ts +82 -0
- package/dist/preservation-gate.d.ts.map +1 -0
- package/dist/preservation-gate.js +150 -0
- package/dist/proactive-pass.d.ts +63 -0
- package/dist/proactive-pass.d.ts.map +1 -0
- package/dist/proactive-pass.js +239 -0
- package/dist/profiles.d.ts +44 -0
- package/dist/profiles.d.ts.map +1 -0
- package/dist/profiles.js +227 -0
- package/dist/provider-translator.d.ts +50 -0
- package/dist/provider-translator.d.ts.map +1 -0
- package/dist/provider-translator.js +403 -0
- package/dist/rate-limiter.d.ts +76 -0
- package/dist/rate-limiter.d.ts.map +1 -0
- package/dist/rate-limiter.js +179 -0
- package/dist/repair-tool-pairs.d.ts +38 -0
- package/dist/repair-tool-pairs.d.ts.map +1 -0
- package/dist/repair-tool-pairs.js +138 -0
- package/dist/retrieval-policy.d.ts +51 -0
- package/dist/retrieval-policy.d.ts.map +1 -0
- package/dist/retrieval-policy.js +77 -0
- package/dist/schema.d.ts +15 -0
- package/dist/schema.d.ts.map +1 -0
- package/dist/schema.js +229 -0
- package/dist/secret-scanner.d.ts +51 -0
- package/dist/secret-scanner.d.ts.map +1 -0
- package/dist/secret-scanner.js +248 -0
- package/dist/seed.d.ts +108 -0
- package/dist/seed.d.ts.map +1 -0
- package/dist/seed.js +177 -0
- package/dist/session-flusher.d.ts +53 -0
- package/dist/session-flusher.d.ts.map +1 -0
- package/dist/session-flusher.js +69 -0
- package/dist/session-topic-map.d.ts +41 -0
- package/dist/session-topic-map.d.ts.map +1 -0
- package/dist/session-topic-map.js +77 -0
- package/dist/spawn-context.d.ts +54 -0
- package/dist/spawn-context.d.ts.map +1 -0
- package/dist/spawn-context.js +159 -0
- package/dist/system-store.d.ts +73 -0
- package/dist/system-store.d.ts.map +1 -0
- package/dist/system-store.js +182 -0
- package/dist/temporal-store.d.ts +80 -0
- package/dist/temporal-store.d.ts.map +1 -0
- package/dist/temporal-store.js +149 -0
- package/dist/topic-detector.d.ts +35 -0
- package/dist/topic-detector.d.ts.map +1 -0
- package/dist/topic-detector.js +249 -0
- package/dist/topic-store.d.ts +45 -0
- package/dist/topic-store.d.ts.map +1 -0
- package/dist/topic-store.js +136 -0
- package/dist/topic-synthesizer.d.ts +51 -0
- package/dist/topic-synthesizer.d.ts.map +1 -0
- package/dist/topic-synthesizer.js +315 -0
- package/dist/trigger-registry.d.ts +63 -0
- package/dist/trigger-registry.d.ts.map +1 -0
- package/dist/trigger-registry.js +163 -0
- package/dist/types.d.ts +533 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +9 -0
- package/dist/vector-store.d.ts +170 -0
- package/dist/vector-store.d.ts.map +1 -0
- package/dist/vector-store.js +677 -0
- package/dist/version.d.ts +34 -0
- package/dist/version.d.ts.map +1 -0
- package/dist/version.js +34 -0
- package/dist/wiki-page-emitter.d.ts +65 -0
- package/dist/wiki-page-emitter.d.ts.map +1 -0
- package/dist/wiki-page-emitter.js +258 -0
- package/dist/work-store.d.ts +112 -0
- package/dist/work-store.d.ts.map +1 -0
- package/dist/work-store.js +273 -0
- package/package.json +1 -1
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* hypermem Keystone Scorer
|
|
3
|
+
*
|
|
4
|
+
* Scores message candidates for inclusion in the Keystone History Slot (P2.1).
|
|
5
|
+
* A "keystone" is an older, high-signal message that provides critical context
|
|
6
|
+
* for the current conversation — decisions, specs, discoveries that happened
|
|
7
|
+
* before the recent history window.
|
|
8
|
+
*
|
|
9
|
+
* Scoring formula (weights sum to 1.0):
|
|
10
|
+
* - episodeSignificance × 0.5 (was this message linked to a significant episode?)
|
|
11
|
+
* - ftsRelevance × 0.3 (is it semantically relevant to the current prompt?)
|
|
12
|
+
* - recencyFactor × 0.2 (how recent is it, relative to maxAgeHours?)
|
|
13
|
+
*
|
|
14
|
+
* Content-type bonus: messages classified as 'decision' or 'spec' get +0.1,
|
|
15
|
+
* capped at 1.0. These are the highest-value signals for context recall.
|
|
16
|
+
*/
|
|
17
|
+
export interface KeystoneCandidate {
|
|
18
|
+
messageId: number;
|
|
19
|
+
messageIndex: number;
|
|
20
|
+
role: string;
|
|
21
|
+
content: string;
|
|
22
|
+
timestamp: string;
|
|
23
|
+
/** Significance from the episodes table (NULL if no episode was linked). */
|
|
24
|
+
episodeSignificance: number | null;
|
|
25
|
+
/** FTS5 BM25 relevance rank, already normalized to [0, 1]. */
|
|
26
|
+
ftsRank: number;
|
|
27
|
+
/** Age in hours from now. */
|
|
28
|
+
ageHours: number;
|
|
29
|
+
}
|
|
30
|
+
export interface ScoredKeystone extends KeystoneCandidate {
|
|
31
|
+
score: number;
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Score a single keystone candidate.
|
|
35
|
+
*
|
|
36
|
+
* @param candidate - The message candidate with its signals
|
|
37
|
+
* @param maxAgeHours - The age ceiling for the recency factor (messages older
|
|
38
|
+
* than this get recencyFactor = 0, but are not excluded — they can still score
|
|
39
|
+
* via significance + ftsRelevance).
|
|
40
|
+
* @returns Score in [0.0, 1.0]
|
|
41
|
+
*/
|
|
42
|
+
export declare function scoreKeystone(candidate: KeystoneCandidate, maxAgeHours: number): number;
|
|
43
|
+
/**
|
|
44
|
+
* Score an array of candidates and sort by score descending.
|
|
45
|
+
*
|
|
46
|
+
* @param candidates - Raw candidates from the DB query
|
|
47
|
+
* @param maxAgeHours - Age ceiling for recency scoring (e.g. 720 = 30 days)
|
|
48
|
+
* @returns Candidates sorted by score DESC with score field attached
|
|
49
|
+
*/
|
|
50
|
+
export declare function rankKeystones(candidates: KeystoneCandidate[], maxAgeHours: number): ScoredKeystone[];
|
|
51
|
+
//# sourceMappingURL=keystone-scorer.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"keystone-scorer.d.ts","sourceRoot":"","sources":["../src/keystone-scorer.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AAMH,MAAM,WAAW,iBAAiB;IAChC,SAAS,EAAE,MAAM,CAAC;IAClB,YAAY,EAAE,MAAM,CAAC;IACrB,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,MAAM,CAAC;IAChB,SAAS,EAAE,MAAM,CAAC;IAClB,4EAA4E;IAC5E,mBAAmB,EAAE,MAAM,GAAG,IAAI,CAAC;IACnC,8DAA8D;IAC9D,OAAO,EAAE,MAAM,CAAC;IAChB,6BAA6B;IAC7B,QAAQ,EAAE,MAAM,CAAC;CAClB;AAED,MAAM,WAAW,cAAe,SAAQ,iBAAiB;IACvD,KAAK,EAAE,MAAM,CAAC;CACf;AAID;;;;;;;;GAQG;AACH,wBAAgB,aAAa,CAC3B,SAAS,EAAE,iBAAiB,EAC5B,WAAW,EAAE,MAAM,GAClB,MAAM,CAcR;AAED;;;;;;GAMG;AACH,wBAAgB,aAAa,CAC3B,UAAU,EAAE,iBAAiB,EAAE,EAC/B,WAAW,EAAE,MAAM,GAClB,cAAc,EAAE,CAIlB"}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* hypermem Keystone Scorer
|
|
3
|
+
*
|
|
4
|
+
* Scores message candidates for inclusion in the Keystone History Slot (P2.1).
|
|
5
|
+
* A "keystone" is an older, high-signal message that provides critical context
|
|
6
|
+
* for the current conversation — decisions, specs, discoveries that happened
|
|
7
|
+
* before the recent history window.
|
|
8
|
+
*
|
|
9
|
+
* Scoring formula (weights sum to 1.0):
|
|
10
|
+
* - episodeSignificance × 0.5 (was this message linked to a significant episode?)
|
|
11
|
+
* - ftsRelevance × 0.3 (is it semantically relevant to the current prompt?)
|
|
12
|
+
* - recencyFactor × 0.2 (how recent is it, relative to maxAgeHours?)
|
|
13
|
+
*
|
|
14
|
+
* Content-type bonus: messages classified as 'decision' or 'spec' get +0.1,
|
|
15
|
+
* capped at 1.0. These are the highest-value signals for context recall.
|
|
16
|
+
*/
|
|
17
|
+
import { classifyContentType } from './content-type-classifier.js';
|
|
18
|
+
// ─── Scorer ──────────────────────────────────────────────────────
|
|
19
|
+
/**
|
|
20
|
+
* Score a single keystone candidate.
|
|
21
|
+
*
|
|
22
|
+
* @param candidate - The message candidate with its signals
|
|
23
|
+
* @param maxAgeHours - The age ceiling for the recency factor (messages older
|
|
24
|
+
* than this get recencyFactor = 0, but are not excluded — they can still score
|
|
25
|
+
* via significance + ftsRelevance).
|
|
26
|
+
* @returns Score in [0.0, 1.0]
|
|
27
|
+
*/
|
|
28
|
+
export function scoreKeystone(candidate, maxAgeHours) {
|
|
29
|
+
const significance = candidate.episodeSignificance ?? 0.3;
|
|
30
|
+
const ftsRelevance = Math.min(1.0, Math.max(0, candidate.ftsRank));
|
|
31
|
+
const recencyFactor = Math.max(0, 1.0 - (candidate.ageHours / maxAgeHours));
|
|
32
|
+
let score = (significance * 0.5) + (ftsRelevance * 0.3) + (recencyFactor * 0.2);
|
|
33
|
+
// Content-type bonus: decisions and specs get +0.1 (capped at 1.0)
|
|
34
|
+
const contentType = classifyContentType(candidate.content);
|
|
35
|
+
if (contentType.type === 'decision' || contentType.type === 'spec') {
|
|
36
|
+
score = Math.min(1.0, score + 0.1);
|
|
37
|
+
}
|
|
38
|
+
return score;
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Score an array of candidates and sort by score descending.
|
|
42
|
+
*
|
|
43
|
+
* @param candidates - Raw candidates from the DB query
|
|
44
|
+
* @param maxAgeHours - Age ceiling for recency scoring (e.g. 720 = 30 days)
|
|
45
|
+
* @returns Candidates sorted by score DESC with score field attached
|
|
46
|
+
*/
|
|
47
|
+
export function rankKeystones(candidates, maxAgeHours) {
|
|
48
|
+
return candidates
|
|
49
|
+
.map(c => ({ ...c, score: scoreKeystone(c, maxAgeHours) }))
|
|
50
|
+
.sort((a, b) => b.score - a.score);
|
|
51
|
+
}
|
|
52
|
+
//# sourceMappingURL=keystone-scorer.js.map
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* hypermem Knowledge Graph
|
|
3
|
+
*
|
|
4
|
+
* DAG traversal over knowledge_links in library.db.
|
|
5
|
+
* Links connect entities across collections:
|
|
6
|
+
* - fact ↔ fact (supersedes, contradicts, supports)
|
|
7
|
+
* - fact ↔ knowledge (references, derived_from)
|
|
8
|
+
* - knowledge ↔ knowledge (depends_on, extends)
|
|
9
|
+
* - topic ↔ fact (covers)
|
|
10
|
+
* - agent ↔ fact (authored_by)
|
|
11
|
+
*
|
|
12
|
+
* Traversal is bounded (max depth, max results) to prevent runaway queries.
|
|
13
|
+
*/
|
|
14
|
+
import type { DatabaseSync } from 'node:sqlite';
|
|
15
|
+
export type EntityType = 'fact' | 'knowledge' | 'topic' | 'episode' | 'agent' | 'preference';
|
|
16
|
+
export interface KnowledgeLink {
|
|
17
|
+
id: number;
|
|
18
|
+
fromType: EntityType;
|
|
19
|
+
fromId: number;
|
|
20
|
+
toType: EntityType;
|
|
21
|
+
toId: number;
|
|
22
|
+
linkType: string;
|
|
23
|
+
createdAt: string;
|
|
24
|
+
}
|
|
25
|
+
export interface GraphNode {
|
|
26
|
+
type: EntityType;
|
|
27
|
+
id: number;
|
|
28
|
+
depth: number;
|
|
29
|
+
linkType: string;
|
|
30
|
+
direction: 'outbound' | 'inbound';
|
|
31
|
+
}
|
|
32
|
+
export interface TraversalResult {
|
|
33
|
+
nodes: GraphNode[];
|
|
34
|
+
edges: KnowledgeLink[];
|
|
35
|
+
truncated: boolean;
|
|
36
|
+
}
|
|
37
|
+
export declare class KnowledgeGraph {
|
|
38
|
+
private readonly db;
|
|
39
|
+
constructor(db: DatabaseSync);
|
|
40
|
+
/**
|
|
41
|
+
* Create a directed link between two entities.
|
|
42
|
+
* Idempotent — unique constraint on (from_type, from_id, to_type, to_id, link_type).
|
|
43
|
+
*/
|
|
44
|
+
addLink(fromType: EntityType, fromId: number, toType: EntityType, toId: number, linkType: string): KnowledgeLink;
|
|
45
|
+
/**
|
|
46
|
+
* Remove a specific link.
|
|
47
|
+
*/
|
|
48
|
+
removeLink(fromType: EntityType, fromId: number, toType: EntityType, toId: number, linkType: string): boolean;
|
|
49
|
+
/**
|
|
50
|
+
* Remove all links involving an entity (both directions).
|
|
51
|
+
*/
|
|
52
|
+
removeEntityLinks(type: EntityType, id: number): number;
|
|
53
|
+
/**
|
|
54
|
+
* Get outbound links from an entity.
|
|
55
|
+
*/
|
|
56
|
+
getOutbound(type: EntityType, id: number, linkType?: string): KnowledgeLink[];
|
|
57
|
+
/**
|
|
58
|
+
* Get inbound links to an entity.
|
|
59
|
+
*/
|
|
60
|
+
getInbound(type: EntityType, id: number, linkType?: string): KnowledgeLink[];
|
|
61
|
+
/**
|
|
62
|
+
* Get all links for an entity (both directions).
|
|
63
|
+
*/
|
|
64
|
+
getLinks(type: EntityType, id: number): KnowledgeLink[];
|
|
65
|
+
/**
|
|
66
|
+
* Breadth-first traversal from a starting entity.
|
|
67
|
+
* Follows links in both directions up to maxDepth.
|
|
68
|
+
*
|
|
69
|
+
* @param startType - Entity type to start from
|
|
70
|
+
* @param startId - Entity ID to start from
|
|
71
|
+
* @param opts - Traversal options
|
|
72
|
+
* @returns Discovered nodes and edges
|
|
73
|
+
*/
|
|
74
|
+
traverse(startType: EntityType, startId: number, opts?: {
|
|
75
|
+
maxDepth?: number;
|
|
76
|
+
maxResults?: number;
|
|
77
|
+
linkTypes?: string[];
|
|
78
|
+
direction?: 'outbound' | 'inbound' | 'both';
|
|
79
|
+
targetTypes?: EntityType[];
|
|
80
|
+
}): TraversalResult;
|
|
81
|
+
/**
|
|
82
|
+
* Find the shortest path between two entities.
|
|
83
|
+
* Uses BFS — returns null if no path exists within maxDepth.
|
|
84
|
+
*/
|
|
85
|
+
findPath(fromType: EntityType, fromId: number, toType: EntityType, toId: number, maxDepth?: number): GraphNode[] | null;
|
|
86
|
+
/**
|
|
87
|
+
* Get the most connected entities (highest degree).
|
|
88
|
+
*/
|
|
89
|
+
getMostConnected(opts?: {
|
|
90
|
+
type?: EntityType;
|
|
91
|
+
limit?: number;
|
|
92
|
+
}): Array<{
|
|
93
|
+
type: EntityType;
|
|
94
|
+
id: number;
|
|
95
|
+
degree: number;
|
|
96
|
+
}>;
|
|
97
|
+
/**
|
|
98
|
+
* Count links by type.
|
|
99
|
+
*/
|
|
100
|
+
getLinkStats(): Array<{
|
|
101
|
+
linkType: string;
|
|
102
|
+
count: number;
|
|
103
|
+
}>;
|
|
104
|
+
/**
|
|
105
|
+
* Total link count.
|
|
106
|
+
*/
|
|
107
|
+
getTotalLinks(): number;
|
|
108
|
+
private rowToLink;
|
|
109
|
+
}
|
|
110
|
+
//# sourceMappingURL=knowledge-graph.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"knowledge-graph.d.ts","sourceRoot":"","sources":["../src/knowledge-graph.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,305 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* hypermem Knowledge Graph
|
|
3
|
+
*
|
|
4
|
+
* DAG traversal over knowledge_links in library.db.
|
|
5
|
+
* Links connect entities across collections:
|
|
6
|
+
* - fact ↔ fact (supersedes, contradicts, supports)
|
|
7
|
+
* - fact ↔ knowledge (references, derived_from)
|
|
8
|
+
* - knowledge ↔ knowledge (depends_on, extends)
|
|
9
|
+
* - topic ↔ fact (covers)
|
|
10
|
+
* - agent ↔ fact (authored_by)
|
|
11
|
+
*
|
|
12
|
+
* Traversal is bounded (max depth, max results) to prevent runaway queries.
|
|
13
|
+
*/
|
|
14
|
+
function nowIso() {
|
|
15
|
+
return new Date().toISOString();
|
|
16
|
+
}
|
|
17
|
+
export class KnowledgeGraph {
|
|
18
|
+
db;
|
|
19
|
+
constructor(db) {
|
|
20
|
+
this.db = db;
|
|
21
|
+
}
|
|
22
|
+
// ─── Link Management ───────────────────────────────────────
|
|
23
|
+
/**
|
|
24
|
+
* Create a directed link between two entities.
|
|
25
|
+
* Idempotent — unique constraint on (from_type, from_id, to_type, to_id, link_type).
|
|
26
|
+
*/
|
|
27
|
+
addLink(fromType, fromId, toType, toId, linkType) {
|
|
28
|
+
const now = nowIso();
|
|
29
|
+
this.db.prepare(`
|
|
30
|
+
INSERT OR IGNORE INTO knowledge_links (from_type, from_id, to_type, to_id, link_type, created_at)
|
|
31
|
+
VALUES (?, ?, ?, ?, ?, ?)
|
|
32
|
+
`).run(fromType, fromId, toType, toId, linkType, now);
|
|
33
|
+
const row = this.db.prepare(`
|
|
34
|
+
SELECT * FROM knowledge_links
|
|
35
|
+
WHERE from_type = ? AND from_id = ? AND to_type = ? AND to_id = ? AND link_type = ?
|
|
36
|
+
`).get(fromType, fromId, toType, toId, linkType);
|
|
37
|
+
return this.rowToLink(row);
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Remove a specific link.
|
|
41
|
+
*/
|
|
42
|
+
removeLink(fromType, fromId, toType, toId, linkType) {
|
|
43
|
+
const result = this.db.prepare(`
|
|
44
|
+
DELETE FROM knowledge_links
|
|
45
|
+
WHERE from_type = ? AND from_id = ? AND to_type = ? AND to_id = ? AND link_type = ?
|
|
46
|
+
`).run(fromType, fromId, toType, toId, linkType);
|
|
47
|
+
return result.changes > 0;
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Remove all links involving an entity (both directions).
|
|
51
|
+
*/
|
|
52
|
+
removeEntityLinks(type, id) {
|
|
53
|
+
const r1 = this.db.prepare('DELETE FROM knowledge_links WHERE from_type = ? AND from_id = ?').run(type, id);
|
|
54
|
+
const r2 = this.db.prepare('DELETE FROM knowledge_links WHERE to_type = ? AND to_id = ?').run(type, id);
|
|
55
|
+
return r1.changes + r2.changes;
|
|
56
|
+
}
|
|
57
|
+
// ─── Direct Queries ────────────────────────────────────────
|
|
58
|
+
/**
|
|
59
|
+
* Get outbound links from an entity.
|
|
60
|
+
*/
|
|
61
|
+
getOutbound(type, id, linkType) {
|
|
62
|
+
if (linkType) {
|
|
63
|
+
return this.db.prepare('SELECT * FROM knowledge_links WHERE from_type = ? AND from_id = ? AND link_type = ?').all(type, id, linkType).map(r => this.rowToLink(r));
|
|
64
|
+
}
|
|
65
|
+
return this.db.prepare('SELECT * FROM knowledge_links WHERE from_type = ? AND from_id = ?').all(type, id).map(r => this.rowToLink(r));
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* Get inbound links to an entity.
|
|
69
|
+
*/
|
|
70
|
+
getInbound(type, id, linkType) {
|
|
71
|
+
if (linkType) {
|
|
72
|
+
return this.db.prepare('SELECT * FROM knowledge_links WHERE to_type = ? AND to_id = ? AND link_type = ?').all(type, id, linkType).map(r => this.rowToLink(r));
|
|
73
|
+
}
|
|
74
|
+
return this.db.prepare('SELECT * FROM knowledge_links WHERE to_type = ? AND to_id = ?').all(type, id).map(r => this.rowToLink(r));
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* Get all links for an entity (both directions).
|
|
78
|
+
*/
|
|
79
|
+
getLinks(type, id) {
|
|
80
|
+
return this.db.prepare(`
|
|
81
|
+
SELECT * FROM knowledge_links
|
|
82
|
+
WHERE (from_type = ? AND from_id = ?) OR (to_type = ? AND to_id = ?)
|
|
83
|
+
ORDER BY created_at DESC
|
|
84
|
+
`).all(type, id, type, id).map(r => this.rowToLink(r));
|
|
85
|
+
}
|
|
86
|
+
// ─── Traversal ─────────────────────────────────────────────
|
|
87
|
+
/**
|
|
88
|
+
* Breadth-first traversal from a starting entity.
|
|
89
|
+
* Follows links in both directions up to maxDepth.
|
|
90
|
+
*
|
|
91
|
+
* @param startType - Entity type to start from
|
|
92
|
+
* @param startId - Entity ID to start from
|
|
93
|
+
* @param opts - Traversal options
|
|
94
|
+
* @returns Discovered nodes and edges
|
|
95
|
+
*/
|
|
96
|
+
traverse(startType, startId, opts) {
|
|
97
|
+
const maxDepth = opts?.maxDepth ?? 3;
|
|
98
|
+
const maxResults = opts?.maxResults ?? 50;
|
|
99
|
+
const direction = opts?.direction ?? 'both';
|
|
100
|
+
const linkTypes = opts?.linkTypes;
|
|
101
|
+
const targetTypes = opts?.targetTypes;
|
|
102
|
+
const visited = new Set();
|
|
103
|
+
const nodes = [];
|
|
104
|
+
const edges = [];
|
|
105
|
+
let truncated = false;
|
|
106
|
+
// BFS queue: [type, id, depth]
|
|
107
|
+
const queue = [[startType, startId, 0]];
|
|
108
|
+
visited.add(`${startType}:${startId}`);
|
|
109
|
+
while (queue.length > 0 && nodes.length < maxResults) {
|
|
110
|
+
const [currentType, currentId, depth] = queue.shift();
|
|
111
|
+
if (depth >= maxDepth) {
|
|
112
|
+
if (depth > maxDepth)
|
|
113
|
+
truncated = true;
|
|
114
|
+
continue;
|
|
115
|
+
}
|
|
116
|
+
// Get neighbors
|
|
117
|
+
const neighbors = [];
|
|
118
|
+
if (direction === 'outbound' || direction === 'both') {
|
|
119
|
+
const outbound = linkTypes
|
|
120
|
+
? this.getOutbound(currentType, currentId).filter(l => linkTypes.includes(l.linkType))
|
|
121
|
+
: this.getOutbound(currentType, currentId);
|
|
122
|
+
for (const link of outbound) {
|
|
123
|
+
const nodeType = link.toType;
|
|
124
|
+
if (targetTypes && !targetTypes.includes(nodeType))
|
|
125
|
+
continue;
|
|
126
|
+
neighbors.push({
|
|
127
|
+
node: {
|
|
128
|
+
type: nodeType,
|
|
129
|
+
id: link.toId,
|
|
130
|
+
depth: depth + 1,
|
|
131
|
+
linkType: link.linkType,
|
|
132
|
+
direction: 'outbound',
|
|
133
|
+
},
|
|
134
|
+
link,
|
|
135
|
+
});
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
if (direction === 'inbound' || direction === 'both') {
|
|
139
|
+
const inbound = linkTypes
|
|
140
|
+
? this.getInbound(currentType, currentId).filter(l => linkTypes.includes(l.linkType))
|
|
141
|
+
: this.getInbound(currentType, currentId);
|
|
142
|
+
for (const link of inbound) {
|
|
143
|
+
const nodeType = link.fromType;
|
|
144
|
+
if (targetTypes && !targetTypes.includes(nodeType))
|
|
145
|
+
continue;
|
|
146
|
+
neighbors.push({
|
|
147
|
+
node: {
|
|
148
|
+
type: nodeType,
|
|
149
|
+
id: link.fromId,
|
|
150
|
+
depth: depth + 1,
|
|
151
|
+
linkType: link.linkType,
|
|
152
|
+
direction: 'inbound',
|
|
153
|
+
},
|
|
154
|
+
link,
|
|
155
|
+
});
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
for (const { node, link } of neighbors) {
|
|
159
|
+
const key = `${node.type}:${node.id}`;
|
|
160
|
+
if (visited.has(key))
|
|
161
|
+
continue;
|
|
162
|
+
visited.add(key);
|
|
163
|
+
if (nodes.length >= maxResults) {
|
|
164
|
+
truncated = true;
|
|
165
|
+
break;
|
|
166
|
+
}
|
|
167
|
+
nodes.push(node);
|
|
168
|
+
edges.push(link);
|
|
169
|
+
queue.push([node.type, node.id, node.depth]);
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
if (queue.length > 0)
|
|
173
|
+
truncated = true;
|
|
174
|
+
return { nodes, edges, truncated };
|
|
175
|
+
}
|
|
176
|
+
/**
|
|
177
|
+
* Find the shortest path between two entities.
|
|
178
|
+
* Uses BFS — returns null if no path exists within maxDepth.
|
|
179
|
+
*/
|
|
180
|
+
findPath(fromType, fromId, toType, toId, maxDepth = 5) {
|
|
181
|
+
const visited = new Map();
|
|
182
|
+
const startKey = `${fromType}:${fromId}`;
|
|
183
|
+
const endKey = `${toType}:${toId}`;
|
|
184
|
+
visited.set(startKey, {
|
|
185
|
+
parent: null,
|
|
186
|
+
node: { type: fromType, id: fromId, depth: 0, linkType: 'start', direction: 'outbound' },
|
|
187
|
+
});
|
|
188
|
+
const queue = [[fromType, fromId, 0]];
|
|
189
|
+
while (queue.length > 0) {
|
|
190
|
+
const [currentType, currentId, depth] = queue.shift();
|
|
191
|
+
const currentKey = `${currentType}:${currentId}`;
|
|
192
|
+
if (currentKey === endKey) {
|
|
193
|
+
// Reconstruct path
|
|
194
|
+
const path = [];
|
|
195
|
+
let key = endKey;
|
|
196
|
+
while (key) {
|
|
197
|
+
const entry = visited.get(key);
|
|
198
|
+
if (!entry)
|
|
199
|
+
break;
|
|
200
|
+
path.unshift(entry.node);
|
|
201
|
+
key = entry.parent;
|
|
202
|
+
}
|
|
203
|
+
return path;
|
|
204
|
+
}
|
|
205
|
+
if (depth >= maxDepth)
|
|
206
|
+
continue;
|
|
207
|
+
// Expand in both directions
|
|
208
|
+
const allLinks = this.getLinks(currentType, currentId);
|
|
209
|
+
for (const link of allLinks) {
|
|
210
|
+
let nextType;
|
|
211
|
+
let nextId;
|
|
212
|
+
let dir;
|
|
213
|
+
if (link.fromType === currentType && link.fromId === currentId) {
|
|
214
|
+
nextType = link.toType;
|
|
215
|
+
nextId = link.toId;
|
|
216
|
+
dir = 'outbound';
|
|
217
|
+
}
|
|
218
|
+
else {
|
|
219
|
+
nextType = link.fromType;
|
|
220
|
+
nextId = link.fromId;
|
|
221
|
+
dir = 'inbound';
|
|
222
|
+
}
|
|
223
|
+
const nextKey = `${nextType}:${nextId}`;
|
|
224
|
+
if (visited.has(nextKey))
|
|
225
|
+
continue;
|
|
226
|
+
const node = {
|
|
227
|
+
type: nextType,
|
|
228
|
+
id: nextId,
|
|
229
|
+
depth: depth + 1,
|
|
230
|
+
linkType: link.linkType,
|
|
231
|
+
direction: dir,
|
|
232
|
+
};
|
|
233
|
+
visited.set(nextKey, { parent: currentKey, node });
|
|
234
|
+
queue.push([nextType, nextId, depth + 1]);
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
return null; // No path found
|
|
238
|
+
}
|
|
239
|
+
// ─── Analytics ─────────────────────────────────────────────
|
|
240
|
+
/**
|
|
241
|
+
* Get the most connected entities (highest degree).
|
|
242
|
+
*/
|
|
243
|
+
getMostConnected(opts) {
|
|
244
|
+
const limit = opts?.limit ?? 10;
|
|
245
|
+
const query = opts?.type
|
|
246
|
+
? `
|
|
247
|
+
SELECT type, id, COUNT(*) as degree FROM (
|
|
248
|
+
SELECT from_type as type, from_id as id FROM knowledge_links WHERE from_type = ?
|
|
249
|
+
UNION ALL
|
|
250
|
+
SELECT to_type as type, to_id as id FROM knowledge_links WHERE to_type = ?
|
|
251
|
+
)
|
|
252
|
+
GROUP BY type, id
|
|
253
|
+
ORDER BY degree DESC
|
|
254
|
+
LIMIT ?
|
|
255
|
+
`
|
|
256
|
+
: `
|
|
257
|
+
SELECT type, id, COUNT(*) as degree FROM (
|
|
258
|
+
SELECT from_type as type, from_id as id FROM knowledge_links
|
|
259
|
+
UNION ALL
|
|
260
|
+
SELECT to_type as type, to_id as id FROM knowledge_links
|
|
261
|
+
)
|
|
262
|
+
GROUP BY type, id
|
|
263
|
+
ORDER BY degree DESC
|
|
264
|
+
LIMIT ?
|
|
265
|
+
`;
|
|
266
|
+
const rows = opts?.type
|
|
267
|
+
? this.db.prepare(query).all(opts.type, opts.type, limit)
|
|
268
|
+
: this.db.prepare(query).all(limit);
|
|
269
|
+
return rows.map(r => ({
|
|
270
|
+
type: r.type,
|
|
271
|
+
id: r.id,
|
|
272
|
+
degree: r.degree,
|
|
273
|
+
}));
|
|
274
|
+
}
|
|
275
|
+
/**
|
|
276
|
+
* Count links by type.
|
|
277
|
+
*/
|
|
278
|
+
getLinkStats() {
|
|
279
|
+
const rows = this.db.prepare('SELECT link_type, COUNT(*) as count FROM knowledge_links GROUP BY link_type ORDER BY count DESC').all();
|
|
280
|
+
return rows.map(r => ({
|
|
281
|
+
linkType: r.link_type,
|
|
282
|
+
count: r.count,
|
|
283
|
+
}));
|
|
284
|
+
}
|
|
285
|
+
/**
|
|
286
|
+
* Total link count.
|
|
287
|
+
*/
|
|
288
|
+
getTotalLinks() {
|
|
289
|
+
const row = this.db.prepare('SELECT COUNT(*) as count FROM knowledge_links').get();
|
|
290
|
+
return row.count;
|
|
291
|
+
}
|
|
292
|
+
// ─── Helpers ───────────────────────────────────────────────
|
|
293
|
+
rowToLink(row) {
|
|
294
|
+
return {
|
|
295
|
+
id: row.id,
|
|
296
|
+
fromType: row.from_type,
|
|
297
|
+
fromId: row.from_id,
|
|
298
|
+
toType: row.to_type,
|
|
299
|
+
toId: row.to_id,
|
|
300
|
+
linkType: row.link_type,
|
|
301
|
+
createdAt: row.created_at,
|
|
302
|
+
};
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
//# sourceMappingURL=knowledge-graph.js.map
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Knowledge Lint
|
|
3
|
+
*
|
|
4
|
+
* Health checks for the knowledge table:
|
|
5
|
+
* 1. Stale syntheses — decay confidence on old topic-synthesis entries
|
|
6
|
+
* 2. Orphan topics — topics with too few messages, stale > 48h
|
|
7
|
+
* 3. Coverage gaps — topics with many messages but no synthesis
|
|
8
|
+
*/
|
|
9
|
+
import type { DatabaseSync } from 'node:sqlite';
|
|
10
|
+
export interface LintResult {
|
|
11
|
+
staleDecayed: number;
|
|
12
|
+
orphansFound: number;
|
|
13
|
+
coverageGaps: string[];
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* Run lint checks on the knowledge table.
|
|
17
|
+
*
|
|
18
|
+
* 1. Stale syntheses: topic-synthesis entries where the source topic's
|
|
19
|
+
* updated_at is older than LINT_STALE_DAYS and there are no new messages.
|
|
20
|
+
* Marks these with confidence = 0.3.
|
|
21
|
+
*
|
|
22
|
+
* 2. Orphan topics: topics with message_count < 3 and updated_at older than 48h.
|
|
23
|
+
* Logged but not synthesized.
|
|
24
|
+
*
|
|
25
|
+
* 3. Coverage gaps: topics with message_count >= 20 but no corresponding
|
|
26
|
+
* knowledge entry (domain='topic-synthesis', key=topic.name).
|
|
27
|
+
*/
|
|
28
|
+
export declare function lintKnowledge(libraryDb: DatabaseSync): LintResult;
|
|
29
|
+
//# sourceMappingURL=knowledge-lint.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"knowledge-lint.d.ts","sourceRoot":"","sources":["../src/knowledge-lint.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAKhD,MAAM,WAAW,UAAU;IACzB,YAAY,EAAE,MAAM,CAAC;IACrB,YAAY,EAAE,MAAM,CAAC;IACrB,YAAY,EAAE,MAAM,EAAE,CAAC;CACxB;AAID;;;;;;;;;;;;GAYG;AACH,wBAAgB,aAAa,CAAC,SAAS,EAAE,YAAY,GAAG,UAAU,CAuGjE"}
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Knowledge Lint
|
|
3
|
+
*
|
|
4
|
+
* Health checks for the knowledge table:
|
|
5
|
+
* 1. Stale syntheses — decay confidence on old topic-synthesis entries
|
|
6
|
+
* 2. Orphan topics — topics with too few messages, stale > 48h
|
|
7
|
+
* 3. Coverage gaps — topics with many messages but no synthesis
|
|
8
|
+
*/
|
|
9
|
+
import { LINT_STALE_DAYS } from './topic-synthesizer.js';
|
|
10
|
+
// ─── lintKnowledge ──────────────────────────────────────────────
|
|
11
|
+
/**
|
|
12
|
+
* Run lint checks on the knowledge table.
|
|
13
|
+
*
|
|
14
|
+
* 1. Stale syntheses: topic-synthesis entries where the source topic's
|
|
15
|
+
* updated_at is older than LINT_STALE_DAYS and there are no new messages.
|
|
16
|
+
* Marks these with confidence = 0.3.
|
|
17
|
+
*
|
|
18
|
+
* 2. Orphan topics: topics with message_count < 3 and updated_at older than 48h.
|
|
19
|
+
* Logged but not synthesized.
|
|
20
|
+
*
|
|
21
|
+
* 3. Coverage gaps: topics with message_count >= 20 but no corresponding
|
|
22
|
+
* knowledge entry (domain='topic-synthesis', key=topic.name).
|
|
23
|
+
*/
|
|
24
|
+
export function lintKnowledge(libraryDb) {
|
|
25
|
+
const result = {
|
|
26
|
+
staleDecayed: 0,
|
|
27
|
+
orphansFound: 0,
|
|
28
|
+
coverageGaps: [],
|
|
29
|
+
};
|
|
30
|
+
// ── 1. Stale syntheses ─────────────────────────────────────────
|
|
31
|
+
// Find topic-synthesis knowledge entries whose source topic hasn't been
|
|
32
|
+
// updated in LINT_STALE_DAYS days.
|
|
33
|
+
try {
|
|
34
|
+
const staleSyntheses = libraryDb.prepare(`
|
|
35
|
+
SELECT k.id, k.source_ref, k.agent_id, k.key
|
|
36
|
+
FROM knowledge k
|
|
37
|
+
WHERE k.domain = 'topic-synthesis'
|
|
38
|
+
AND k.superseded_by IS NULL
|
|
39
|
+
AND k.updated_at < datetime('now', '-${LINT_STALE_DAYS} days')
|
|
40
|
+
`).all();
|
|
41
|
+
for (const entry of staleSyntheses) {
|
|
42
|
+
// Extract topic id from source_ref: "topic:<id>" or "topic:<id>:mc:<count>"
|
|
43
|
+
if (!entry.source_ref)
|
|
44
|
+
continue;
|
|
45
|
+
const match = entry.source_ref.match(/^topic:(\d+)/);
|
|
46
|
+
if (!match)
|
|
47
|
+
continue;
|
|
48
|
+
const topicId = parseInt(match[1], 10);
|
|
49
|
+
// Check if topic exists and is stale (no recent updates)
|
|
50
|
+
let topicRow;
|
|
51
|
+
try {
|
|
52
|
+
topicRow = libraryDb.prepare('SELECT updated_at, message_count FROM topics WHERE id = ?').get(topicId);
|
|
53
|
+
}
|
|
54
|
+
catch {
|
|
55
|
+
continue;
|
|
56
|
+
}
|
|
57
|
+
if (!topicRow)
|
|
58
|
+
continue;
|
|
59
|
+
// Check if topic is stale (updated_at older than LINT_STALE_DAYS)
|
|
60
|
+
const topicAge = libraryDb.prepare(`
|
|
61
|
+
SELECT CASE WHEN datetime(?) < datetime('now', '-${LINT_STALE_DAYS} days') THEN 1 ELSE 0 END AS is_stale
|
|
62
|
+
`).get(topicRow.updated_at);
|
|
63
|
+
if (topicAge.is_stale) {
|
|
64
|
+
// Decay confidence to 0.3
|
|
65
|
+
libraryDb.prepare('UPDATE knowledge SET confidence = 0.3, updated_at = datetime(\'now\') WHERE id = ?').run(entry.id);
|
|
66
|
+
result.staleDecayed++;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
catch {
|
|
71
|
+
// Non-fatal — continue with other checks
|
|
72
|
+
}
|
|
73
|
+
// ── 2. Orphan topics ───────────────────────────────────────────
|
|
74
|
+
// Topics with message_count < 3 and updated_at > 48h ago
|
|
75
|
+
try {
|
|
76
|
+
const orphans = libraryDb.prepare(`
|
|
77
|
+
SELECT id, name, message_count, updated_at FROM topics
|
|
78
|
+
WHERE message_count < 3
|
|
79
|
+
AND updated_at < datetime('now', '-48 hours')
|
|
80
|
+
`).all();
|
|
81
|
+
result.orphansFound = orphans.length;
|
|
82
|
+
if (orphans.length > 0) {
|
|
83
|
+
console.log(`[lint] ${orphans.length} orphan topic(s) found (< 3 messages, stale > 48h): ${orphans.map(o => o.name).join(', ')}`);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
catch {
|
|
87
|
+
// Non-fatal
|
|
88
|
+
}
|
|
89
|
+
// ── 3. Coverage gaps ───────────────────────────────────────────
|
|
90
|
+
// Topics with >= 20 messages but no synthesis in knowledge table
|
|
91
|
+
try {
|
|
92
|
+
const bigTopics = libraryDb.prepare(`
|
|
93
|
+
SELECT t.name, t.agent_id
|
|
94
|
+
FROM topics t
|
|
95
|
+
WHERE t.message_count >= 20
|
|
96
|
+
`).all();
|
|
97
|
+
for (const topic of bigTopics) {
|
|
98
|
+
const synthesis = libraryDb.prepare(`
|
|
99
|
+
SELECT id FROM knowledge
|
|
100
|
+
WHERE agent_id = ?
|
|
101
|
+
AND domain = 'topic-synthesis'
|
|
102
|
+
AND key = ?
|
|
103
|
+
AND superseded_by IS NULL
|
|
104
|
+
LIMIT 1
|
|
105
|
+
`).get(topic.agent_id, topic.name);
|
|
106
|
+
if (!synthesis) {
|
|
107
|
+
result.coverageGaps.push(topic.name);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
catch {
|
|
112
|
+
// Non-fatal
|
|
113
|
+
}
|
|
114
|
+
return result;
|
|
115
|
+
}
|
|
116
|
+
//# sourceMappingURL=knowledge-lint.js.map
|