@psiclawops/hypermem 0.5.0 → 0.5.2

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.
Files changed (163) hide show
  1. package/ARCHITECTURE.md +12 -3
  2. package/README.md +30 -6
  3. package/bin/hypermem-status.mjs +166 -0
  4. package/dist/background-indexer.d.ts +132 -0
  5. package/dist/background-indexer.d.ts.map +1 -0
  6. package/dist/background-indexer.js +1044 -0
  7. package/dist/cache.d.ts +110 -0
  8. package/dist/cache.d.ts.map +1 -0
  9. package/dist/cache.js +495 -0
  10. package/dist/compaction-fence.d.ts +89 -0
  11. package/dist/compaction-fence.d.ts.map +1 -0
  12. package/dist/compaction-fence.js +153 -0
  13. package/dist/compositor.d.ts +226 -0
  14. package/dist/compositor.d.ts.map +1 -0
  15. package/dist/compositor.js +2558 -0
  16. package/dist/content-type-classifier.d.ts +41 -0
  17. package/dist/content-type-classifier.d.ts.map +1 -0
  18. package/dist/content-type-classifier.js +181 -0
  19. package/dist/cross-agent.d.ts +62 -0
  20. package/dist/cross-agent.d.ts.map +1 -0
  21. package/dist/cross-agent.js +259 -0
  22. package/dist/db.d.ts +131 -0
  23. package/dist/db.d.ts.map +1 -0
  24. package/dist/db.js +402 -0
  25. package/dist/desired-state-store.d.ts +100 -0
  26. package/dist/desired-state-store.d.ts.map +1 -0
  27. package/dist/desired-state-store.js +222 -0
  28. package/dist/doc-chunk-store.d.ts +140 -0
  29. package/dist/doc-chunk-store.d.ts.map +1 -0
  30. package/dist/doc-chunk-store.js +391 -0
  31. package/dist/doc-chunker.d.ts +99 -0
  32. package/dist/doc-chunker.d.ts.map +1 -0
  33. package/dist/doc-chunker.js +324 -0
  34. package/dist/dreaming-promoter.d.ts +86 -0
  35. package/dist/dreaming-promoter.d.ts.map +1 -0
  36. package/dist/dreaming-promoter.js +381 -0
  37. package/dist/episode-store.d.ts +49 -0
  38. package/dist/episode-store.d.ts.map +1 -0
  39. package/dist/episode-store.js +135 -0
  40. package/dist/fact-store.d.ts +75 -0
  41. package/dist/fact-store.d.ts.map +1 -0
  42. package/dist/fact-store.js +236 -0
  43. package/dist/fleet-store.d.ts +144 -0
  44. package/dist/fleet-store.d.ts.map +1 -0
  45. package/dist/fleet-store.js +276 -0
  46. package/dist/fos-mod.d.ts +178 -0
  47. package/dist/fos-mod.d.ts.map +1 -0
  48. package/dist/fos-mod.js +416 -0
  49. package/dist/hybrid-retrieval.d.ts +64 -0
  50. package/dist/hybrid-retrieval.d.ts.map +1 -0
  51. package/dist/hybrid-retrieval.js +344 -0
  52. package/dist/image-eviction.d.ts +49 -0
  53. package/dist/image-eviction.d.ts.map +1 -0
  54. package/dist/image-eviction.js +251 -0
  55. package/dist/index.d.ts +650 -0
  56. package/dist/index.d.ts.map +1 -0
  57. package/dist/index.js +1072 -0
  58. package/dist/keystone-scorer.d.ts +51 -0
  59. package/dist/keystone-scorer.d.ts.map +1 -0
  60. package/dist/keystone-scorer.js +52 -0
  61. package/dist/knowledge-graph.d.ts +110 -0
  62. package/dist/knowledge-graph.d.ts.map +1 -0
  63. package/dist/knowledge-graph.js +305 -0
  64. package/dist/knowledge-lint.d.ts +29 -0
  65. package/dist/knowledge-lint.d.ts.map +1 -0
  66. package/dist/knowledge-lint.js +116 -0
  67. package/dist/knowledge-store.d.ts +72 -0
  68. package/dist/knowledge-store.d.ts.map +1 -0
  69. package/dist/knowledge-store.js +247 -0
  70. package/dist/library-schema.d.ts +22 -0
  71. package/dist/library-schema.d.ts.map +1 -0
  72. package/dist/library-schema.js +1038 -0
  73. package/dist/message-store.d.ts +89 -0
  74. package/dist/message-store.d.ts.map +1 -0
  75. package/dist/message-store.js +323 -0
  76. package/dist/metrics-dashboard.d.ts +114 -0
  77. package/dist/metrics-dashboard.d.ts.map +1 -0
  78. package/dist/metrics-dashboard.js +260 -0
  79. package/dist/obsidian-exporter.d.ts +57 -0
  80. package/dist/obsidian-exporter.d.ts.map +1 -0
  81. package/dist/obsidian-exporter.js +274 -0
  82. package/dist/obsidian-watcher.d.ts +147 -0
  83. package/dist/obsidian-watcher.d.ts.map +1 -0
  84. package/dist/obsidian-watcher.js +403 -0
  85. package/dist/open-domain.d.ts +46 -0
  86. package/dist/open-domain.d.ts.map +1 -0
  87. package/dist/open-domain.js +125 -0
  88. package/dist/preference-store.d.ts +54 -0
  89. package/dist/preference-store.d.ts.map +1 -0
  90. package/dist/preference-store.js +109 -0
  91. package/dist/preservation-gate.d.ts +82 -0
  92. package/dist/preservation-gate.d.ts.map +1 -0
  93. package/dist/preservation-gate.js +150 -0
  94. package/dist/proactive-pass.d.ts +63 -0
  95. package/dist/proactive-pass.d.ts.map +1 -0
  96. package/dist/proactive-pass.js +239 -0
  97. package/dist/profiles.d.ts +44 -0
  98. package/dist/profiles.d.ts.map +1 -0
  99. package/dist/profiles.js +227 -0
  100. package/dist/provider-translator.d.ts +50 -0
  101. package/dist/provider-translator.d.ts.map +1 -0
  102. package/dist/provider-translator.js +403 -0
  103. package/dist/rate-limiter.d.ts +76 -0
  104. package/dist/rate-limiter.d.ts.map +1 -0
  105. package/dist/rate-limiter.js +179 -0
  106. package/dist/repair-tool-pairs.d.ts +38 -0
  107. package/dist/repair-tool-pairs.d.ts.map +1 -0
  108. package/dist/repair-tool-pairs.js +138 -0
  109. package/dist/retrieval-policy.d.ts +51 -0
  110. package/dist/retrieval-policy.d.ts.map +1 -0
  111. package/dist/retrieval-policy.js +77 -0
  112. package/dist/schema.d.ts +15 -0
  113. package/dist/schema.d.ts.map +1 -0
  114. package/dist/schema.js +229 -0
  115. package/dist/secret-scanner.d.ts +51 -0
  116. package/dist/secret-scanner.d.ts.map +1 -0
  117. package/dist/secret-scanner.js +248 -0
  118. package/dist/seed.d.ts +108 -0
  119. package/dist/seed.d.ts.map +1 -0
  120. package/dist/seed.js +177 -0
  121. package/dist/session-flusher.d.ts +53 -0
  122. package/dist/session-flusher.d.ts.map +1 -0
  123. package/dist/session-flusher.js +69 -0
  124. package/dist/session-topic-map.d.ts +41 -0
  125. package/dist/session-topic-map.d.ts.map +1 -0
  126. package/dist/session-topic-map.js +77 -0
  127. package/dist/spawn-context.d.ts +54 -0
  128. package/dist/spawn-context.d.ts.map +1 -0
  129. package/dist/spawn-context.js +159 -0
  130. package/dist/system-store.d.ts +73 -0
  131. package/dist/system-store.d.ts.map +1 -0
  132. package/dist/system-store.js +182 -0
  133. package/dist/temporal-store.d.ts +80 -0
  134. package/dist/temporal-store.d.ts.map +1 -0
  135. package/dist/temporal-store.js +149 -0
  136. package/dist/topic-detector.d.ts +35 -0
  137. package/dist/topic-detector.d.ts.map +1 -0
  138. package/dist/topic-detector.js +249 -0
  139. package/dist/topic-store.d.ts +45 -0
  140. package/dist/topic-store.d.ts.map +1 -0
  141. package/dist/topic-store.js +136 -0
  142. package/dist/topic-synthesizer.d.ts +51 -0
  143. package/dist/topic-synthesizer.d.ts.map +1 -0
  144. package/dist/topic-synthesizer.js +315 -0
  145. package/dist/trigger-registry.d.ts +63 -0
  146. package/dist/trigger-registry.d.ts.map +1 -0
  147. package/dist/trigger-registry.js +163 -0
  148. package/dist/types.d.ts +537 -0
  149. package/dist/types.d.ts.map +1 -0
  150. package/dist/types.js +9 -0
  151. package/dist/vector-store.d.ts +170 -0
  152. package/dist/vector-store.d.ts.map +1 -0
  153. package/dist/vector-store.js +677 -0
  154. package/dist/version.d.ts +34 -0
  155. package/dist/version.d.ts.map +1 -0
  156. package/dist/version.js +34 -0
  157. package/dist/wiki-page-emitter.d.ts +65 -0
  158. package/dist/wiki-page-emitter.d.ts.map +1 -0
  159. package/dist/wiki-page-emitter.js +258 -0
  160. package/dist/work-store.d.ts +112 -0
  161. package/dist/work-store.d.ts.map +1 -0
  162. package/dist/work-store.js +273 -0
  163. package/package.json +4 -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