@soleri/core 2.11.0 → 7.0.0

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 (255) hide show
  1. package/data/flows/build.flow.yaml +128 -0
  2. package/data/flows/deliver.flow.yaml +110 -0
  3. package/data/flows/design.flow.yaml +108 -0
  4. package/data/flows/enhance.flow.yaml +90 -0
  5. package/data/flows/explore.flow.yaml +84 -0
  6. package/data/flows/fix.flow.yaml +90 -0
  7. package/data/flows/plan.flow.yaml +87 -0
  8. package/data/flows/review.flow.yaml +90 -0
  9. package/dist/brain/brain.d.ts.map +1 -1
  10. package/dist/brain/brain.js +10 -0
  11. package/dist/brain/brain.js.map +1 -1
  12. package/dist/brain/intelligence.d.ts.map +1 -1
  13. package/dist/brain/intelligence.js +16 -2
  14. package/dist/brain/intelligence.js.map +1 -1
  15. package/dist/capabilities/chain-mapping.d.ts +21 -0
  16. package/dist/capabilities/chain-mapping.d.ts.map +1 -0
  17. package/dist/capabilities/chain-mapping.js +86 -0
  18. package/dist/capabilities/chain-mapping.js.map +1 -0
  19. package/dist/capabilities/index.d.ts +10 -0
  20. package/dist/capabilities/index.d.ts.map +1 -0
  21. package/dist/capabilities/index.js +8 -0
  22. package/dist/capabilities/index.js.map +1 -0
  23. package/dist/capabilities/registry.d.ts +95 -0
  24. package/dist/capabilities/registry.d.ts.map +1 -0
  25. package/dist/capabilities/registry.js +227 -0
  26. package/dist/capabilities/registry.js.map +1 -0
  27. package/dist/capabilities/types.d.ts +106 -0
  28. package/dist/capabilities/types.d.ts.map +1 -0
  29. package/dist/capabilities/types.js +12 -0
  30. package/dist/capabilities/types.js.map +1 -0
  31. package/dist/control/intent-router.d.ts.map +1 -1
  32. package/dist/control/intent-router.js +58 -2
  33. package/dist/control/intent-router.js.map +1 -1
  34. package/dist/domain-packs/index.d.ts +8 -0
  35. package/dist/domain-packs/index.d.ts.map +1 -0
  36. package/dist/domain-packs/index.js +8 -0
  37. package/dist/domain-packs/index.js.map +1 -0
  38. package/dist/domain-packs/inject-rules.d.ts +24 -0
  39. package/dist/domain-packs/inject-rules.d.ts.map +1 -0
  40. package/dist/domain-packs/inject-rules.js +65 -0
  41. package/dist/domain-packs/inject-rules.js.map +1 -0
  42. package/dist/domain-packs/knowledge-installer.d.ts +27 -0
  43. package/dist/domain-packs/knowledge-installer.d.ts.map +1 -0
  44. package/dist/domain-packs/knowledge-installer.js +89 -0
  45. package/dist/domain-packs/knowledge-installer.js.map +1 -0
  46. package/dist/domain-packs/loader.d.ts +28 -0
  47. package/dist/domain-packs/loader.d.ts.map +1 -0
  48. package/dist/domain-packs/loader.js +105 -0
  49. package/dist/domain-packs/loader.js.map +1 -0
  50. package/dist/domain-packs/pack-runtime.d.ts +80 -0
  51. package/dist/domain-packs/pack-runtime.d.ts.map +1 -0
  52. package/dist/domain-packs/pack-runtime.js +36 -0
  53. package/dist/domain-packs/pack-runtime.js.map +1 -0
  54. package/dist/domain-packs/skills-installer.d.ts +21 -0
  55. package/dist/domain-packs/skills-installer.d.ts.map +1 -0
  56. package/dist/domain-packs/skills-installer.js +38 -0
  57. package/dist/domain-packs/skills-installer.js.map +1 -0
  58. package/dist/domain-packs/token-resolver.d.ts +37 -0
  59. package/dist/domain-packs/token-resolver.d.ts.map +1 -0
  60. package/dist/domain-packs/token-resolver.js +109 -0
  61. package/dist/domain-packs/token-resolver.js.map +1 -0
  62. package/dist/domain-packs/types.d.ts +91 -0
  63. package/dist/domain-packs/types.d.ts.map +1 -0
  64. package/dist/domain-packs/types.js +122 -0
  65. package/dist/domain-packs/types.js.map +1 -0
  66. package/dist/engine/bin/soleri-engine.d.ts +12 -0
  67. package/dist/engine/bin/soleri-engine.d.ts.map +1 -0
  68. package/dist/engine/bin/soleri-engine.js +183 -0
  69. package/dist/engine/bin/soleri-engine.js.map +1 -0
  70. package/dist/engine/core-ops.d.ts +27 -0
  71. package/dist/engine/core-ops.d.ts.map +1 -0
  72. package/dist/engine/core-ops.js +159 -0
  73. package/dist/engine/core-ops.js.map +1 -0
  74. package/dist/engine/index.d.ts +19 -0
  75. package/dist/engine/index.d.ts.map +1 -0
  76. package/dist/engine/index.js +17 -0
  77. package/dist/engine/index.js.map +1 -0
  78. package/dist/engine/register-engine.d.ts +54 -0
  79. package/dist/engine/register-engine.d.ts.map +1 -0
  80. package/dist/engine/register-engine.js +270 -0
  81. package/dist/engine/register-engine.js.map +1 -0
  82. package/dist/engine/test-helpers.d.ts +30 -0
  83. package/dist/engine/test-helpers.d.ts.map +1 -0
  84. package/dist/engine/test-helpers.js +59 -0
  85. package/dist/engine/test-helpers.js.map +1 -0
  86. package/dist/flows/context-router.d.ts +39 -0
  87. package/dist/flows/context-router.d.ts.map +1 -0
  88. package/dist/flows/context-router.js +206 -0
  89. package/dist/flows/context-router.js.map +1 -0
  90. package/dist/flows/dispatch-registry.d.ts +24 -0
  91. package/dist/flows/dispatch-registry.d.ts.map +1 -0
  92. package/dist/flows/dispatch-registry.js +70 -0
  93. package/dist/flows/dispatch-registry.js.map +1 -0
  94. package/dist/flows/epilogue.d.ts +24 -0
  95. package/dist/flows/epilogue.d.ts.map +1 -0
  96. package/dist/flows/epilogue.js +52 -0
  97. package/dist/flows/epilogue.js.map +1 -0
  98. package/dist/flows/executor.d.ts +25 -0
  99. package/dist/flows/executor.d.ts.map +1 -0
  100. package/dist/flows/executor.js +153 -0
  101. package/dist/flows/executor.js.map +1 -0
  102. package/dist/flows/gate-evaluator.d.ts +26 -0
  103. package/dist/flows/gate-evaluator.d.ts.map +1 -0
  104. package/dist/flows/gate-evaluator.js +162 -0
  105. package/dist/flows/gate-evaluator.js.map +1 -0
  106. package/dist/flows/index.d.ts +14 -0
  107. package/dist/flows/index.d.ts.map +1 -0
  108. package/dist/flows/index.js +20 -0
  109. package/dist/flows/index.js.map +1 -0
  110. package/dist/flows/loader.d.ts +17 -0
  111. package/dist/flows/loader.d.ts.map +1 -0
  112. package/dist/flows/loader.js +61 -0
  113. package/dist/flows/loader.js.map +1 -0
  114. package/dist/flows/plan-builder.d.ts +40 -0
  115. package/dist/flows/plan-builder.d.ts.map +1 -0
  116. package/dist/flows/plan-builder.js +213 -0
  117. package/dist/flows/plan-builder.js.map +1 -0
  118. package/dist/flows/probes.d.ts +11 -0
  119. package/dist/flows/probes.d.ts.map +1 -0
  120. package/dist/flows/probes.js +62 -0
  121. package/dist/flows/probes.js.map +1 -0
  122. package/dist/flows/types.d.ts +950 -0
  123. package/dist/flows/types.d.ts.map +1 -0
  124. package/dist/flows/types.js +105 -0
  125. package/dist/flows/types.js.map +1 -0
  126. package/dist/index.d.ts +11 -1
  127. package/dist/index.d.ts.map +1 -1
  128. package/dist/index.js +10 -1
  129. package/dist/index.js.map +1 -1
  130. package/dist/intelligence/loader.d.ts +19 -0
  131. package/dist/intelligence/loader.d.ts.map +1 -1
  132. package/dist/intelligence/loader.js +86 -5
  133. package/dist/intelligence/loader.js.map +1 -1
  134. package/dist/intelligence/types.d.ts +1 -0
  135. package/dist/intelligence/types.d.ts.map +1 -1
  136. package/dist/packs/types.d.ts +58 -19
  137. package/dist/packs/types.d.ts.map +1 -1
  138. package/dist/packs/types.js +14 -0
  139. package/dist/packs/types.js.map +1 -1
  140. package/dist/playbooks/generic/onboarding.d.ts +9 -0
  141. package/dist/playbooks/generic/onboarding.d.ts.map +1 -0
  142. package/dist/playbooks/generic/onboarding.js +74 -0
  143. package/dist/playbooks/generic/onboarding.js.map +1 -0
  144. package/dist/playbooks/playbook-registry.d.ts.map +1 -1
  145. package/dist/playbooks/playbook-registry.js +2 -0
  146. package/dist/playbooks/playbook-registry.js.map +1 -1
  147. package/dist/runtime/admin-extra-ops.d.ts.map +1 -1
  148. package/dist/runtime/admin-extra-ops.js +15 -9
  149. package/dist/runtime/admin-extra-ops.js.map +1 -1
  150. package/dist/runtime/admin-ops.js +4 -4
  151. package/dist/runtime/admin-ops.js.map +1 -1
  152. package/dist/runtime/capture-ops.d.ts.map +1 -1
  153. package/dist/runtime/capture-ops.js +33 -1
  154. package/dist/runtime/capture-ops.js.map +1 -1
  155. package/dist/runtime/domain-ops.d.ts +21 -5
  156. package/dist/runtime/domain-ops.d.ts.map +1 -1
  157. package/dist/runtime/domain-ops.js +85 -8
  158. package/dist/runtime/domain-ops.js.map +1 -1
  159. package/dist/runtime/facades/cognee-facade.d.ts.map +1 -1
  160. package/dist/runtime/facades/cognee-facade.js +3 -1
  161. package/dist/runtime/facades/cognee-facade.js.map +1 -1
  162. package/dist/runtime/facades/index.d.ts.map +1 -1
  163. package/dist/runtime/facades/index.js +10 -6
  164. package/dist/runtime/facades/index.js.map +1 -1
  165. package/dist/runtime/facades/vault-facade.d.ts.map +1 -1
  166. package/dist/runtime/facades/vault-facade.js +2 -0
  167. package/dist/runtime/facades/vault-facade.js.map +1 -1
  168. package/dist/runtime/orchestrate-ops.d.ts +8 -7
  169. package/dist/runtime/orchestrate-ops.d.ts.map +1 -1
  170. package/dist/runtime/orchestrate-ops.js +227 -58
  171. package/dist/runtime/orchestrate-ops.js.map +1 -1
  172. package/dist/runtime/runtime.d.ts.map +1 -1
  173. package/dist/runtime/runtime.js +23 -17
  174. package/dist/runtime/runtime.js.map +1 -1
  175. package/dist/runtime/types.d.ts +6 -2
  176. package/dist/runtime/types.d.ts.map +1 -1
  177. package/dist/runtime/vault-linking-ops.d.ts +13 -0
  178. package/dist/runtime/vault-linking-ops.d.ts.map +1 -0
  179. package/dist/runtime/vault-linking-ops.js +367 -0
  180. package/dist/runtime/vault-linking-ops.js.map +1 -0
  181. package/dist/vault/linking.d.ts +46 -0
  182. package/dist/vault/linking.d.ts.map +1 -0
  183. package/dist/vault/linking.js +275 -0
  184. package/dist/vault/linking.js.map +1 -0
  185. package/dist/vault/vault-types.d.ts +37 -0
  186. package/dist/vault/vault-types.d.ts.map +1 -1
  187. package/dist/vault/vault.d.ts +12 -0
  188. package/dist/vault/vault.d.ts.map +1 -1
  189. package/dist/vault/vault.js +85 -6
  190. package/dist/vault/vault.js.map +1 -1
  191. package/package.json +4 -1
  192. package/src/__tests__/admin-extra-ops.test.ts +1 -1
  193. package/src/__tests__/admin-ops.test.ts +2 -1
  194. package/src/__tests__/cognee-client-gaps.test.ts +470 -0
  195. package/src/__tests__/cognee-hybrid-search.test.ts +478 -0
  196. package/src/__tests__/cognee-sync-manager-deep.test.ts +630 -0
  197. package/src/__tests__/cognee-sync-manager.test.ts +1 -0
  198. package/src/__tests__/core-ops.test.ts +9 -61
  199. package/src/__tests__/domain-packs.test.ts +421 -0
  200. package/src/__tests__/flows.test.ts +604 -0
  201. package/src/__tests__/playbook-registry.test.ts +2 -2
  202. package/src/__tests__/playbook-seeder.test.ts +8 -8
  203. package/src/__tests__/playbook.test.ts +5 -5
  204. package/src/__tests__/token-resolver.test.ts +79 -0
  205. package/src/brain/brain.ts +12 -0
  206. package/src/brain/intelligence.ts +21 -2
  207. package/src/capabilities/chain-mapping.ts +93 -0
  208. package/src/capabilities/index.ts +21 -0
  209. package/src/capabilities/registry.ts +290 -0
  210. package/src/capabilities/types.ts +143 -0
  211. package/src/control/intent-router.ts +46 -2
  212. package/src/domain-packs/index.ts +27 -0
  213. package/src/domain-packs/inject-rules.ts +74 -0
  214. package/src/domain-packs/knowledge-installer.ts +116 -0
  215. package/src/domain-packs/loader.ts +124 -0
  216. package/src/domain-packs/pack-runtime.ts +99 -0
  217. package/src/domain-packs/skills-installer.ts +56 -0
  218. package/src/domain-packs/token-resolver.ts +126 -0
  219. package/src/domain-packs/types.ts +229 -0
  220. package/src/engine/__tests__/register-engine.test.ts +104 -0
  221. package/src/engine/bin/soleri-engine.ts +217 -0
  222. package/src/engine/core-ops.ts +178 -0
  223. package/src/engine/index.ts +19 -0
  224. package/src/engine/register-engine.ts +385 -0
  225. package/src/engine/test-helpers.ts +83 -0
  226. package/src/flows/context-router.ts +257 -0
  227. package/src/flows/dispatch-registry.ts +80 -0
  228. package/src/flows/epilogue.ts +65 -0
  229. package/src/flows/executor.ts +182 -0
  230. package/src/flows/gate-evaluator.ts +171 -0
  231. package/src/flows/index.ts +52 -0
  232. package/src/flows/loader.ts +63 -0
  233. package/src/flows/plan-builder.ts +250 -0
  234. package/src/flows/probes.ts +70 -0
  235. package/src/flows/types.ts +217 -0
  236. package/src/index.ts +68 -1
  237. package/src/intelligence/loader.ts +96 -5
  238. package/src/intelligence/types.ts +1 -0
  239. package/src/packs/types.ts +19 -0
  240. package/src/playbooks/generic/onboarding.ts +79 -0
  241. package/src/playbooks/playbook-registry.ts +2 -0
  242. package/src/runtime/admin-extra-ops.ts +14 -8
  243. package/src/runtime/admin-ops.ts +4 -4
  244. package/src/runtime/capture-ops.ts +40 -1
  245. package/src/runtime/domain-ops.ts +92 -7
  246. package/src/runtime/facades/cognee-facade.ts +3 -1
  247. package/src/runtime/facades/index.ts +12 -6
  248. package/src/runtime/facades/vault-facade.ts +2 -0
  249. package/src/runtime/orchestrate-ops.ts +271 -62
  250. package/src/runtime/runtime.ts +27 -18
  251. package/src/runtime/types.ts +6 -2
  252. package/src/runtime/vault-linking-ops.ts +454 -0
  253. package/src/vault/linking.ts +333 -0
  254. package/src/vault/vault-types.ts +46 -0
  255. package/src/vault/vault.ts +94 -7
@@ -0,0 +1,333 @@
1
+ /**
2
+ * LinkManager — Zettelkasten bidirectional linking for vault entries.
3
+ *
4
+ * Provides typed links between entries (supports, contradicts, extends, sequences),
5
+ * backlink traversal, graph walking, orphan detection, and link suggestions via FTS5.
6
+ *
7
+ * Ported from Salvador MCP with improvements:
8
+ * - Uses PersistenceProvider (not raw SQLite) for backend abstraction
9
+ * - Uses FTS5 for suggest_links (Salvador used TF-IDF cosine similarity)
10
+ * - Graceful degradation — all methods return empty on table-not-found
11
+ */
12
+
13
+ import type { PersistenceProvider } from '../persistence/types.js';
14
+ import type {
15
+ VaultLink,
16
+ VaultLinkRow,
17
+ LinkType,
18
+ LinkedEntry,
19
+ LinkSuggestion,
20
+ } from './vault-types.js';
21
+
22
+ export class LinkManager {
23
+ private initialized = false;
24
+
25
+ constructor(private provider: PersistenceProvider) {
26
+ this.ensureTable();
27
+ }
28
+
29
+ // ===========================================================================
30
+ // SCHEMA
31
+ // ===========================================================================
32
+
33
+ private ensureTable(): void {
34
+ if (this.initialized) return;
35
+ try {
36
+ this.provider.execSql(`
37
+ CREATE TABLE IF NOT EXISTS vault_links (
38
+ source_id TEXT NOT NULL REFERENCES entries(id) ON DELETE CASCADE,
39
+ target_id TEXT NOT NULL REFERENCES entries(id) ON DELETE CASCADE,
40
+ link_type TEXT NOT NULL CHECK(link_type IN ('supports', 'contradicts', 'extends', 'sequences')),
41
+ note TEXT,
42
+ created_at INTEGER NOT NULL,
43
+ PRIMARY KEY (source_id, target_id)
44
+ );
45
+ CREATE INDEX IF NOT EXISTS idx_links_target ON vault_links(target_id);
46
+ CREATE INDEX IF NOT EXISTS idx_links_type ON vault_links(link_type);
47
+ `);
48
+ this.initialized = true;
49
+ } catch {
50
+ // Table may already exist or DB may be read-only — degrade gracefully
51
+ }
52
+ }
53
+
54
+ // ===========================================================================
55
+ // CRUD
56
+ // ===========================================================================
57
+
58
+ /** Create a typed link between two entries. */
59
+ addLink(sourceId: string, targetId: string, linkType: LinkType, note?: string): void {
60
+ this.provider.run(
61
+ `INSERT OR REPLACE INTO vault_links (source_id, target_id, link_type, note, created_at)
62
+ VALUES (?, ?, ?, ?, ?)`,
63
+ [sourceId, targetId, linkType, note ?? null, Date.now()],
64
+ );
65
+ }
66
+
67
+ /** Remove a link between two entries. */
68
+ removeLink(sourceId: string, targetId: string): void {
69
+ this.provider.run('DELETE FROM vault_links WHERE source_id = ? AND target_id = ?', [
70
+ sourceId,
71
+ targetId,
72
+ ]);
73
+ }
74
+
75
+ /** Get all outgoing links FROM an entry. */
76
+ getLinks(entryId: string): VaultLink[] {
77
+ try {
78
+ const rows = this.provider.all<VaultLinkRow>(
79
+ 'SELECT * FROM vault_links WHERE source_id = ?',
80
+ [entryId],
81
+ );
82
+ return rows.map(rowToVaultLink);
83
+ } catch {
84
+ return [];
85
+ }
86
+ }
87
+
88
+ /** Get all incoming links TO an entry (backlinks). */
89
+ getBacklinks(entryId: string): VaultLink[] {
90
+ try {
91
+ const rows = this.provider.all<VaultLinkRow>(
92
+ 'SELECT * FROM vault_links WHERE target_id = ?',
93
+ [entryId],
94
+ );
95
+ return rows.map(rowToVaultLink);
96
+ } catch {
97
+ return [];
98
+ }
99
+ }
100
+
101
+ /** Get total link count (outgoing + incoming). */
102
+ getLinkCount(entryId: string): number {
103
+ try {
104
+ const row = this.provider.get<{ count: number }>(
105
+ 'SELECT COUNT(*) as count FROM vault_links WHERE source_id = ? OR target_id = ?',
106
+ [entryId, entryId],
107
+ );
108
+ return row?.count ?? 0;
109
+ } catch {
110
+ return 0;
111
+ }
112
+ }
113
+
114
+ // ===========================================================================
115
+ // GRAPH TRAVERSAL
116
+ // ===========================================================================
117
+
118
+ /**
119
+ * Walk the link graph from a starting entry up to `depth` hops.
120
+ * Returns all connected entries with link metadata.
121
+ * BFS — walks both outgoing and incoming links (undirected).
122
+ */
123
+ traverse(entryId: string, depth: number = 2): LinkedEntry[] {
124
+ const visited = new Set<string>([entryId]);
125
+ const result: LinkedEntry[] = [];
126
+ let frontier = [entryId];
127
+
128
+ for (let d = 0; d < depth && frontier.length > 0; d++) {
129
+ const nextFrontier: string[] = [];
130
+
131
+ for (const currentId of frontier) {
132
+ // Outgoing
133
+ for (const link of this.getLinks(currentId)) {
134
+ if (!visited.has(link.targetId)) {
135
+ visited.add(link.targetId);
136
+ nextFrontier.push(link.targetId);
137
+ const entry = this.getEntryMeta(link.targetId);
138
+ if (entry) {
139
+ result.push({
140
+ ...entry,
141
+ linkType: link.linkType,
142
+ linkDirection: 'outgoing',
143
+ linkNote: link.note,
144
+ });
145
+ }
146
+ }
147
+ }
148
+ // Incoming
149
+ for (const link of this.getBacklinks(currentId)) {
150
+ if (!visited.has(link.sourceId)) {
151
+ visited.add(link.sourceId);
152
+ nextFrontier.push(link.sourceId);
153
+ const entry = this.getEntryMeta(link.sourceId);
154
+ if (entry) {
155
+ result.push({
156
+ ...entry,
157
+ linkType: link.linkType,
158
+ linkDirection: 'incoming',
159
+ linkNote: link.note,
160
+ });
161
+ }
162
+ }
163
+ }
164
+ }
165
+
166
+ frontier = nextFrontier;
167
+ }
168
+
169
+ return result;
170
+ }
171
+
172
+ // ===========================================================================
173
+ // ORPHAN DETECTION
174
+ // ===========================================================================
175
+
176
+ /** Find entries with zero links. */
177
+ getOrphans(
178
+ limit: number = 50,
179
+ ): Array<{ id: string; title: string; type: string; domain: string }> {
180
+ try {
181
+ return this.provider.all<{ id: string; title: string; type: string; domain: string }>(
182
+ `SELECT id, title, type, domain FROM entries
183
+ WHERE id NOT IN (SELECT source_id FROM vault_links)
184
+ AND id NOT IN (SELECT target_id FROM vault_links)
185
+ ORDER BY updated_at DESC LIMIT ?`,
186
+ [limit],
187
+ );
188
+ } catch {
189
+ return [];
190
+ }
191
+ }
192
+
193
+ // ===========================================================================
194
+ // LINK SUGGESTIONS (FTS5-powered — improvement over Salvador's TF-IDF)
195
+ // ===========================================================================
196
+
197
+ /** Find semantically similar entries as link candidates using FTS5. */
198
+ suggestLinks(entryId: string, limit: number = 5): LinkSuggestion[] {
199
+ try {
200
+ // Get the entry to build a search query
201
+ const entry = this.provider.get<{
202
+ title: string;
203
+ description: string;
204
+ type: string;
205
+ tags: string;
206
+ }>('SELECT title, description, type, tags FROM entries WHERE id = ?', [entryId]);
207
+ if (!entry) return [];
208
+
209
+ // Build FTS query from entry content — extract significant keywords only.
210
+ // FTS5 MATCH chokes on long raw text; use top keywords joined with OR.
211
+ const rawWords = `${entry.title} ${entry.description}`
212
+ .replace(/[^\w\s]/g, ' ')
213
+ .toLowerCase()
214
+ .split(/\s+/)
215
+ .filter((w) => w.length > 2);
216
+ // Deduplicate and take top 10 most significant words (skip common stop words)
217
+ const stopWords = new Set([
218
+ 'the',
219
+ 'and',
220
+ 'for',
221
+ 'with',
222
+ 'from',
223
+ 'this',
224
+ 'that',
225
+ 'are',
226
+ 'was',
227
+ 'not',
228
+ 'but',
229
+ 'have',
230
+ 'has',
231
+ 'use',
232
+ 'can',
233
+ 'will',
234
+ 'all',
235
+ 'each',
236
+ 'than',
237
+ 'its',
238
+ 'more',
239
+ 'when',
240
+ 'into',
241
+ 'also',
242
+ 'any',
243
+ 'may',
244
+ 'only',
245
+ 'should',
246
+ 'which',
247
+ ]);
248
+ const unique = [...new Set(rawWords)].filter((w) => !stopWords.has(w));
249
+ const keywords = unique.slice(0, 10);
250
+ if (keywords.length === 0) return [];
251
+ const queryTerms = keywords.join(' OR ');
252
+
253
+ // FTS5 match with BM25 ranking
254
+ const matches = this.provider.all<{
255
+ id: string;
256
+ title: string;
257
+ type: string;
258
+ domain: string;
259
+ rank: number;
260
+ }>(
261
+ `SELECT e.id, e.title, e.type, e.domain, rank
262
+ FROM entries_fts fts
263
+ JOIN entries e ON e.rowid = fts.rowid
264
+ WHERE entries_fts MATCH ?
265
+ ORDER BY rank
266
+ LIMIT ?`,
267
+ [queryTerms, limit + 5],
268
+ );
269
+
270
+ // Filter out self and already-linked entries
271
+ const existingLinks = new Set([
272
+ ...this.getLinks(entryId).map((l) => l.targetId),
273
+ ...this.getBacklinks(entryId).map((l) => l.sourceId),
274
+ ]);
275
+
276
+ return matches
277
+ .filter((m) => m.id !== entryId && !existingLinks.has(m.id))
278
+ .slice(0, limit)
279
+ .map((m) => {
280
+ const suggestedType = inferLinkType(entry.type, m.type);
281
+ return {
282
+ entryId: m.id,
283
+ title: m.title,
284
+ type: m.type,
285
+ score: Math.abs(m.rank), // FTS5 rank is negative (lower = better)
286
+ suggestedType,
287
+ reason: `${m.type} in ${m.domain}`,
288
+ };
289
+ });
290
+ } catch {
291
+ return [];
292
+ }
293
+ }
294
+
295
+ // ===========================================================================
296
+ // PRIVATE
297
+ // ===========================================================================
298
+
299
+ private getEntryMeta(
300
+ entryId: string,
301
+ ): Omit<LinkedEntry, 'linkType' | 'linkDirection' | 'linkNote'> | null {
302
+ try {
303
+ const row = this.provider.get<{ id: string; title: string; type: string; domain: string }>(
304
+ 'SELECT id, title, type, domain FROM entries WHERE id = ?',
305
+ [entryId],
306
+ );
307
+ return row ?? null;
308
+ } catch {
309
+ return null;
310
+ }
311
+ }
312
+ }
313
+
314
+ // =============================================================================
315
+ // HELPERS
316
+ // =============================================================================
317
+
318
+ function rowToVaultLink(row: VaultLinkRow): VaultLink {
319
+ return {
320
+ sourceId: row.source_id,
321
+ targetId: row.target_id,
322
+ linkType: row.link_type as LinkType,
323
+ note: row.note ?? undefined,
324
+ createdAt: row.created_at,
325
+ };
326
+ }
327
+
328
+ function inferLinkType(sourceType: string, targetType: string): LinkType {
329
+ if (sourceType === 'pattern' && targetType === 'anti-pattern') return 'contradicts';
330
+ if (sourceType === 'anti-pattern' && targetType === 'pattern') return 'contradicts';
331
+ if (targetType === 'rule') return 'supports';
332
+ return 'extends';
333
+ }
@@ -48,3 +48,49 @@ export interface VaultTierInfo {
48
48
  connected: boolean;
49
49
  entryCount: number;
50
50
  }
51
+
52
+ // =============================================================================
53
+ // ZETTELKASTEN LINKS
54
+ // =============================================================================
55
+
56
+ /** Typed relationship between two vault entries. */
57
+ export type LinkType = 'supports' | 'contradicts' | 'extends' | 'sequences';
58
+
59
+ /** A directional, typed link between two vault entries. */
60
+ export interface VaultLink {
61
+ sourceId: string;
62
+ targetId: string;
63
+ linkType: LinkType;
64
+ note?: string;
65
+ createdAt: number;
66
+ }
67
+
68
+ /** Raw SQLite row from vault_links table. */
69
+ export interface VaultLinkRow {
70
+ source_id: string;
71
+ target_id: string;
72
+ link_type: string;
73
+ note: string | null;
74
+ created_at: number;
75
+ }
76
+
77
+ /** Entry enriched with link context for graph traversal results. */
78
+ export interface LinkedEntry {
79
+ id: string;
80
+ title: string;
81
+ type: string;
82
+ domain: string;
83
+ linkType: LinkType;
84
+ linkDirection: 'outgoing' | 'incoming';
85
+ linkNote?: string;
86
+ }
87
+
88
+ /** Suggested link candidate from semantic/FTS similarity. */
89
+ export interface LinkSuggestion {
90
+ entryId: string;
91
+ title: string;
92
+ type: string;
93
+ score: number;
94
+ suggestedType: LinkType;
95
+ reason: string;
96
+ }
@@ -178,6 +178,7 @@ export class Vault {
178
178
  `);
179
179
  this.migrateBrainSchema();
180
180
  this.migrateTemporalSchema();
181
+ this.migrateOriginColumn();
181
182
  this.migrateContentHash();
182
183
  this.migrateTierColumn();
183
184
  }
@@ -206,6 +207,17 @@ export class Vault {
206
207
  }
207
208
  }
208
209
 
210
+ private migrateOriginColumn(): void {
211
+ try {
212
+ this.provider.run(
213
+ "ALTER TABLE entries ADD COLUMN origin TEXT NOT NULL DEFAULT 'user' CHECK(origin IN ('agent', 'pack', 'user'))",
214
+ );
215
+ } catch {
216
+ // Column already exists
217
+ }
218
+ this.provider.execSql('CREATE INDEX IF NOT EXISTS idx_entries_origin ON entries(origin)');
219
+ }
220
+
209
221
  private migrateContentHash(): void {
210
222
  try {
211
223
  this.provider.run('ALTER TABLE entries ADD COLUMN content_hash TEXT');
@@ -293,12 +305,12 @@ export class Vault {
293
305
 
294
306
  seed(entries: IntelligenceEntry[]): number {
295
307
  const sql = `
296
- INSERT INTO entries (id,type,domain,title,severity,description,context,example,counter_example,why,tags,applies_to,valid_from,valid_until,content_hash,tier)
297
- VALUES (@id,@type,@domain,@title,@severity,@description,@context,@example,@counterExample,@why,@tags,@appliesTo,@validFrom,@validUntil,@contentHash,@tier)
308
+ INSERT INTO entries (id,type,domain,title,severity,description,context,example,counter_example,why,tags,applies_to,valid_from,valid_until,content_hash,tier,origin)
309
+ VALUES (@id,@type,@domain,@title,@severity,@description,@context,@example,@counterExample,@why,@tags,@appliesTo,@validFrom,@validUntil,@contentHash,@tier,@origin)
298
310
  ON CONFLICT(id) DO UPDATE SET type=excluded.type,domain=excluded.domain,title=excluded.title,severity=excluded.severity,
299
311
  description=excluded.description,context=excluded.context,example=excluded.example,counter_example=excluded.counter_example,
300
312
  why=excluded.why,tags=excluded.tags,applies_to=excluded.applies_to,valid_from=excluded.valid_from,valid_until=excluded.valid_until,
301
- content_hash=excluded.content_hash,tier=excluded.tier,updated_at=unixepoch()
313
+ content_hash=excluded.content_hash,tier=excluded.tier,origin=excluded.origin,updated_at=unixepoch()
302
314
  `;
303
315
  return this.provider.transaction(() => {
304
316
  let count = 0;
@@ -320,6 +332,7 @@ export class Vault {
320
332
  validUntil: entry.validUntil ?? null,
321
333
  contentHash: computeContentHash(entry),
322
334
  tier: entry.tier ?? 'agent',
335
+ origin: entry.origin ?? 'agent',
323
336
  });
324
337
  count++;
325
338
  if (this.syncManager) {
@@ -330,6 +343,24 @@ export class Vault {
330
343
  });
331
344
  }
332
345
 
346
+ /**
347
+ * Install a knowledge pack — seeds entries with origin:'pack' and content-hash dedup.
348
+ * Packs are installable domain knowledge (UX laws, design tokens, clean code rules).
349
+ * Unlike seed(), this forces origin:'pack' regardless of what the entry says.
350
+ */
351
+ installPack(entries: IntelligenceEntry[]): { installed: number; skipped: number } {
352
+ let installed = 0;
353
+ let skipped = 0;
354
+ // Tag all entries with origin:'pack' and seed — seed() handles its own transaction
355
+ const tagged = entries.map((e) => ({ ...e, origin: 'pack' as const }));
356
+ const results = this.seedDedup(tagged);
357
+ for (const r of results) {
358
+ if (r.action === 'inserted') installed++;
359
+ else skipped++;
360
+ }
361
+ return { installed, skipped };
362
+ }
363
+
333
364
  /**
334
365
  * Seed entries with content-hash dedup. Returns per-entry results.
335
366
  * Unlike seed(), skips entries whose content already exists in the vault.
@@ -360,6 +391,7 @@ export class Vault {
360
391
  domain?: string;
361
392
  type?: string;
362
393
  severity?: string;
394
+ origin?: 'agent' | 'pack' | 'user';
363
395
  limit?: number;
364
396
  includeExpired?: boolean;
365
397
  },
@@ -379,6 +411,10 @@ export class Vault {
379
411
  filters.push('e.severity = @severity');
380
412
  fp.severity = options.severity;
381
413
  }
414
+ if (options?.origin) {
415
+ filters.push('e.origin = @origin');
416
+ fp.origin = options.origin;
417
+ }
382
418
  if (!options?.includeExpired) {
383
419
  const now = Math.floor(Date.now() / 1000);
384
420
  filters.push('(e.valid_until IS NULL OR e.valid_until > @now)');
@@ -386,14 +422,30 @@ export class Vault {
386
422
  fp.now = now;
387
423
  }
388
424
  const wc = filters.length > 0 ? `AND ${filters.join(' AND ')}` : '';
425
+
426
+ // Build FTS5 query: use OR between terms for broader matching,
427
+ // with title column boosted 3x for relevance ranking.
428
+ // FTS5 BM25 with default AND degrades with more entries because
429
+ // fewer documents match ALL terms simultaneously.
430
+ const ftsQuery = buildFtsQuery(query);
431
+
389
432
  try {
390
433
  const rows = this.provider.all<Record<string, unknown>>(
391
- `SELECT e.*, -rank as score FROM entries_fts fts JOIN entries e ON e.rowid = fts.rowid WHERE entries_fts MATCH @query ${wc} ORDER BY score DESC LIMIT @limit`,
392
- { query, limit, ...fp },
434
+ `SELECT e.*, bm25(entries_fts, 5.0, 10.0, 3.0, 1.0, 2.0) as score FROM entries_fts fts JOIN entries e ON e.rowid = fts.rowid WHERE entries_fts MATCH @query ${wc} ORDER BY score ASC LIMIT @limit`,
435
+ { query: ftsQuery, limit, ...fp },
393
436
  );
394
437
  return rows.map(rowToSearchResult);
395
438
  } catch {
396
- return [];
439
+ // Fallback: try original query if FTS5 syntax fails
440
+ try {
441
+ const rows = this.provider.all<Record<string, unknown>>(
442
+ `SELECT e.*, -rank as score FROM entries_fts fts JOIN entries e ON e.rowid = fts.rowid WHERE entries_fts MATCH @query ${wc} ORDER BY score DESC LIMIT @limit`,
443
+ { query, limit, ...fp },
444
+ );
445
+ return rows.map(rowToSearchResult);
446
+ } catch {
447
+ return [];
448
+ }
397
449
  }
398
450
  }
399
451
 
@@ -408,6 +460,7 @@ export class Vault {
408
460
  domain?: string;
409
461
  type?: string;
410
462
  severity?: string;
463
+ origin?: 'agent' | 'pack' | 'user';
411
464
  tags?: string[];
412
465
  limit?: number;
413
466
  offset?: number;
@@ -427,6 +480,10 @@ export class Vault {
427
480
  filters.push('severity = @severity');
428
481
  params.severity = options.severity;
429
482
  }
483
+ if (options?.origin) {
484
+ filters.push('origin = @origin');
485
+ params.origin = options.origin;
486
+ }
430
487
  if (options?.tags?.length) {
431
488
  const c = options.tags.map((t, i) => {
432
489
  params[`tag${i}`] = `%"${t}"%`;
@@ -1133,13 +1190,43 @@ function rowToEntry(row: Record<string, unknown>): IntelligenceEntry {
1133
1190
  tags: JSON.parse((row.tags as string) || '[]'),
1134
1191
  appliesTo: JSON.parse((row.applies_to as string) || '[]'),
1135
1192
  tier: (row.tier as IntelligenceEntry['tier']) ?? undefined,
1193
+ origin: (row.origin as IntelligenceEntry['origin']) ?? undefined,
1136
1194
  validFrom: (row.valid_from as number) ?? undefined,
1137
1195
  validUntil: (row.valid_until as number) ?? undefined,
1138
1196
  };
1139
1197
  }
1140
1198
 
1141
1199
  function rowToSearchResult(row: Record<string, unknown>): SearchResult {
1142
- return { entry: rowToEntry(row), score: row.score as number };
1200
+ // bm25() returns negative scores (lower = better), normalize to positive
1201
+ const rawScore = row.score as number;
1202
+ const score = rawScore < 0 ? -rawScore : rawScore;
1203
+ return { entry: rowToEntry(row), score };
1204
+ }
1205
+
1206
+ /**
1207
+ * Build an FTS5 query from natural language input.
1208
+ *
1209
+ * Converts "React render performance memo" to:
1210
+ * {title}: (react OR render OR performance OR memo) OR (react OR render OR performance OR memo)
1211
+ *
1212
+ * Uses OR matching (not AND) so results include partial matches.
1213
+ * FTS5 BM25 ranks documents with more matching terms higher.
1214
+ * Title column is boosted via bm25() weights in the SQL query.
1215
+ */
1216
+ function buildFtsQuery(query: string): string {
1217
+ const terms = query
1218
+ .toLowerCase()
1219
+ .split(/\s+/)
1220
+ .filter((t) => t.length >= 2)
1221
+ .map((t) => t.replace(/[^a-z0-9]/g, ''))
1222
+ .filter(Boolean);
1223
+
1224
+ if (terms.length === 0) return query;
1225
+ if (terms.length === 1) return terms[0];
1226
+
1227
+ // Use OR to match any term — BM25 ranks by how many terms match
1228
+ const orTerms = terms.join(' OR ');
1229
+ return orTerms;
1143
1230
  }
1144
1231
 
1145
1232
  function rowToMemory(row: Record<string, unknown>): Memory {