@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.
- package/data/flows/build.flow.yaml +128 -0
- package/data/flows/deliver.flow.yaml +110 -0
- package/data/flows/design.flow.yaml +108 -0
- package/data/flows/enhance.flow.yaml +90 -0
- package/data/flows/explore.flow.yaml +84 -0
- package/data/flows/fix.flow.yaml +90 -0
- package/data/flows/plan.flow.yaml +87 -0
- package/data/flows/review.flow.yaml +90 -0
- package/dist/brain/brain.d.ts.map +1 -1
- package/dist/brain/brain.js +10 -0
- package/dist/brain/brain.js.map +1 -1
- package/dist/brain/intelligence.d.ts.map +1 -1
- package/dist/brain/intelligence.js +16 -2
- package/dist/brain/intelligence.js.map +1 -1
- package/dist/capabilities/chain-mapping.d.ts +21 -0
- package/dist/capabilities/chain-mapping.d.ts.map +1 -0
- package/dist/capabilities/chain-mapping.js +86 -0
- package/dist/capabilities/chain-mapping.js.map +1 -0
- package/dist/capabilities/index.d.ts +10 -0
- package/dist/capabilities/index.d.ts.map +1 -0
- package/dist/capabilities/index.js +8 -0
- package/dist/capabilities/index.js.map +1 -0
- package/dist/capabilities/registry.d.ts +95 -0
- package/dist/capabilities/registry.d.ts.map +1 -0
- package/dist/capabilities/registry.js +227 -0
- package/dist/capabilities/registry.js.map +1 -0
- package/dist/capabilities/types.d.ts +106 -0
- package/dist/capabilities/types.d.ts.map +1 -0
- package/dist/capabilities/types.js +12 -0
- package/dist/capabilities/types.js.map +1 -0
- package/dist/control/intent-router.d.ts.map +1 -1
- package/dist/control/intent-router.js +58 -2
- package/dist/control/intent-router.js.map +1 -1
- package/dist/domain-packs/index.d.ts +8 -0
- package/dist/domain-packs/index.d.ts.map +1 -0
- package/dist/domain-packs/index.js +8 -0
- package/dist/domain-packs/index.js.map +1 -0
- package/dist/domain-packs/inject-rules.d.ts +24 -0
- package/dist/domain-packs/inject-rules.d.ts.map +1 -0
- package/dist/domain-packs/inject-rules.js +65 -0
- package/dist/domain-packs/inject-rules.js.map +1 -0
- package/dist/domain-packs/knowledge-installer.d.ts +27 -0
- package/dist/domain-packs/knowledge-installer.d.ts.map +1 -0
- package/dist/domain-packs/knowledge-installer.js +89 -0
- package/dist/domain-packs/knowledge-installer.js.map +1 -0
- package/dist/domain-packs/loader.d.ts +28 -0
- package/dist/domain-packs/loader.d.ts.map +1 -0
- package/dist/domain-packs/loader.js +105 -0
- package/dist/domain-packs/loader.js.map +1 -0
- package/dist/domain-packs/pack-runtime.d.ts +80 -0
- package/dist/domain-packs/pack-runtime.d.ts.map +1 -0
- package/dist/domain-packs/pack-runtime.js +36 -0
- package/dist/domain-packs/pack-runtime.js.map +1 -0
- package/dist/domain-packs/skills-installer.d.ts +21 -0
- package/dist/domain-packs/skills-installer.d.ts.map +1 -0
- package/dist/domain-packs/skills-installer.js +38 -0
- package/dist/domain-packs/skills-installer.js.map +1 -0
- package/dist/domain-packs/token-resolver.d.ts +37 -0
- package/dist/domain-packs/token-resolver.d.ts.map +1 -0
- package/dist/domain-packs/token-resolver.js +109 -0
- package/dist/domain-packs/token-resolver.js.map +1 -0
- package/dist/domain-packs/types.d.ts +91 -0
- package/dist/domain-packs/types.d.ts.map +1 -0
- package/dist/domain-packs/types.js +122 -0
- package/dist/domain-packs/types.js.map +1 -0
- package/dist/engine/bin/soleri-engine.d.ts +12 -0
- package/dist/engine/bin/soleri-engine.d.ts.map +1 -0
- package/dist/engine/bin/soleri-engine.js +183 -0
- package/dist/engine/bin/soleri-engine.js.map +1 -0
- package/dist/engine/core-ops.d.ts +27 -0
- package/dist/engine/core-ops.d.ts.map +1 -0
- package/dist/engine/core-ops.js +159 -0
- package/dist/engine/core-ops.js.map +1 -0
- package/dist/engine/index.d.ts +19 -0
- package/dist/engine/index.d.ts.map +1 -0
- package/dist/engine/index.js +17 -0
- package/dist/engine/index.js.map +1 -0
- package/dist/engine/register-engine.d.ts +54 -0
- package/dist/engine/register-engine.d.ts.map +1 -0
- package/dist/engine/register-engine.js +270 -0
- package/dist/engine/register-engine.js.map +1 -0
- package/dist/engine/test-helpers.d.ts +30 -0
- package/dist/engine/test-helpers.d.ts.map +1 -0
- package/dist/engine/test-helpers.js +59 -0
- package/dist/engine/test-helpers.js.map +1 -0
- package/dist/flows/context-router.d.ts +39 -0
- package/dist/flows/context-router.d.ts.map +1 -0
- package/dist/flows/context-router.js +206 -0
- package/dist/flows/context-router.js.map +1 -0
- package/dist/flows/dispatch-registry.d.ts +24 -0
- package/dist/flows/dispatch-registry.d.ts.map +1 -0
- package/dist/flows/dispatch-registry.js +70 -0
- package/dist/flows/dispatch-registry.js.map +1 -0
- package/dist/flows/epilogue.d.ts +24 -0
- package/dist/flows/epilogue.d.ts.map +1 -0
- package/dist/flows/epilogue.js +52 -0
- package/dist/flows/epilogue.js.map +1 -0
- package/dist/flows/executor.d.ts +25 -0
- package/dist/flows/executor.d.ts.map +1 -0
- package/dist/flows/executor.js +153 -0
- package/dist/flows/executor.js.map +1 -0
- package/dist/flows/gate-evaluator.d.ts +26 -0
- package/dist/flows/gate-evaluator.d.ts.map +1 -0
- package/dist/flows/gate-evaluator.js +162 -0
- package/dist/flows/gate-evaluator.js.map +1 -0
- package/dist/flows/index.d.ts +14 -0
- package/dist/flows/index.d.ts.map +1 -0
- package/dist/flows/index.js +20 -0
- package/dist/flows/index.js.map +1 -0
- package/dist/flows/loader.d.ts +17 -0
- package/dist/flows/loader.d.ts.map +1 -0
- package/dist/flows/loader.js +61 -0
- package/dist/flows/loader.js.map +1 -0
- package/dist/flows/plan-builder.d.ts +40 -0
- package/dist/flows/plan-builder.d.ts.map +1 -0
- package/dist/flows/plan-builder.js +213 -0
- package/dist/flows/plan-builder.js.map +1 -0
- package/dist/flows/probes.d.ts +11 -0
- package/dist/flows/probes.d.ts.map +1 -0
- package/dist/flows/probes.js +62 -0
- package/dist/flows/probes.js.map +1 -0
- package/dist/flows/types.d.ts +950 -0
- package/dist/flows/types.d.ts.map +1 -0
- package/dist/flows/types.js +105 -0
- package/dist/flows/types.js.map +1 -0
- package/dist/index.d.ts +11 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +10 -1
- package/dist/index.js.map +1 -1
- package/dist/intelligence/loader.d.ts +19 -0
- package/dist/intelligence/loader.d.ts.map +1 -1
- package/dist/intelligence/loader.js +86 -5
- package/dist/intelligence/loader.js.map +1 -1
- package/dist/intelligence/types.d.ts +1 -0
- package/dist/intelligence/types.d.ts.map +1 -1
- package/dist/packs/types.d.ts +58 -19
- package/dist/packs/types.d.ts.map +1 -1
- package/dist/packs/types.js +14 -0
- package/dist/packs/types.js.map +1 -1
- package/dist/playbooks/generic/onboarding.d.ts +9 -0
- package/dist/playbooks/generic/onboarding.d.ts.map +1 -0
- package/dist/playbooks/generic/onboarding.js +74 -0
- package/dist/playbooks/generic/onboarding.js.map +1 -0
- package/dist/playbooks/playbook-registry.d.ts.map +1 -1
- package/dist/playbooks/playbook-registry.js +2 -0
- package/dist/playbooks/playbook-registry.js.map +1 -1
- package/dist/runtime/admin-extra-ops.d.ts.map +1 -1
- package/dist/runtime/admin-extra-ops.js +15 -9
- package/dist/runtime/admin-extra-ops.js.map +1 -1
- package/dist/runtime/admin-ops.js +4 -4
- package/dist/runtime/admin-ops.js.map +1 -1
- package/dist/runtime/capture-ops.d.ts.map +1 -1
- package/dist/runtime/capture-ops.js +33 -1
- package/dist/runtime/capture-ops.js.map +1 -1
- package/dist/runtime/domain-ops.d.ts +21 -5
- package/dist/runtime/domain-ops.d.ts.map +1 -1
- package/dist/runtime/domain-ops.js +85 -8
- package/dist/runtime/domain-ops.js.map +1 -1
- package/dist/runtime/facades/cognee-facade.d.ts.map +1 -1
- package/dist/runtime/facades/cognee-facade.js +3 -1
- package/dist/runtime/facades/cognee-facade.js.map +1 -1
- package/dist/runtime/facades/index.d.ts.map +1 -1
- package/dist/runtime/facades/index.js +10 -6
- package/dist/runtime/facades/index.js.map +1 -1
- package/dist/runtime/facades/vault-facade.d.ts.map +1 -1
- package/dist/runtime/facades/vault-facade.js +2 -0
- package/dist/runtime/facades/vault-facade.js.map +1 -1
- package/dist/runtime/orchestrate-ops.d.ts +8 -7
- package/dist/runtime/orchestrate-ops.d.ts.map +1 -1
- package/dist/runtime/orchestrate-ops.js +227 -58
- package/dist/runtime/orchestrate-ops.js.map +1 -1
- package/dist/runtime/runtime.d.ts.map +1 -1
- package/dist/runtime/runtime.js +23 -17
- package/dist/runtime/runtime.js.map +1 -1
- package/dist/runtime/types.d.ts +6 -2
- package/dist/runtime/types.d.ts.map +1 -1
- package/dist/runtime/vault-linking-ops.d.ts +13 -0
- package/dist/runtime/vault-linking-ops.d.ts.map +1 -0
- package/dist/runtime/vault-linking-ops.js +367 -0
- package/dist/runtime/vault-linking-ops.js.map +1 -0
- package/dist/vault/linking.d.ts +46 -0
- package/dist/vault/linking.d.ts.map +1 -0
- package/dist/vault/linking.js +275 -0
- package/dist/vault/linking.js.map +1 -0
- package/dist/vault/vault-types.d.ts +37 -0
- package/dist/vault/vault-types.d.ts.map +1 -1
- package/dist/vault/vault.d.ts +12 -0
- package/dist/vault/vault.d.ts.map +1 -1
- package/dist/vault/vault.js +85 -6
- package/dist/vault/vault.js.map +1 -1
- package/package.json +4 -1
- package/src/__tests__/admin-extra-ops.test.ts +1 -1
- package/src/__tests__/admin-ops.test.ts +2 -1
- package/src/__tests__/cognee-client-gaps.test.ts +470 -0
- package/src/__tests__/cognee-hybrid-search.test.ts +478 -0
- package/src/__tests__/cognee-sync-manager-deep.test.ts +630 -0
- package/src/__tests__/cognee-sync-manager.test.ts +1 -0
- package/src/__tests__/core-ops.test.ts +9 -61
- package/src/__tests__/domain-packs.test.ts +421 -0
- package/src/__tests__/flows.test.ts +604 -0
- package/src/__tests__/playbook-registry.test.ts +2 -2
- package/src/__tests__/playbook-seeder.test.ts +8 -8
- package/src/__tests__/playbook.test.ts +5 -5
- package/src/__tests__/token-resolver.test.ts +79 -0
- package/src/brain/brain.ts +12 -0
- package/src/brain/intelligence.ts +21 -2
- package/src/capabilities/chain-mapping.ts +93 -0
- package/src/capabilities/index.ts +21 -0
- package/src/capabilities/registry.ts +290 -0
- package/src/capabilities/types.ts +143 -0
- package/src/control/intent-router.ts +46 -2
- package/src/domain-packs/index.ts +27 -0
- package/src/domain-packs/inject-rules.ts +74 -0
- package/src/domain-packs/knowledge-installer.ts +116 -0
- package/src/domain-packs/loader.ts +124 -0
- package/src/domain-packs/pack-runtime.ts +99 -0
- package/src/domain-packs/skills-installer.ts +56 -0
- package/src/domain-packs/token-resolver.ts +126 -0
- package/src/domain-packs/types.ts +229 -0
- package/src/engine/__tests__/register-engine.test.ts +104 -0
- package/src/engine/bin/soleri-engine.ts +217 -0
- package/src/engine/core-ops.ts +178 -0
- package/src/engine/index.ts +19 -0
- package/src/engine/register-engine.ts +385 -0
- package/src/engine/test-helpers.ts +83 -0
- package/src/flows/context-router.ts +257 -0
- package/src/flows/dispatch-registry.ts +80 -0
- package/src/flows/epilogue.ts +65 -0
- package/src/flows/executor.ts +182 -0
- package/src/flows/gate-evaluator.ts +171 -0
- package/src/flows/index.ts +52 -0
- package/src/flows/loader.ts +63 -0
- package/src/flows/plan-builder.ts +250 -0
- package/src/flows/probes.ts +70 -0
- package/src/flows/types.ts +217 -0
- package/src/index.ts +68 -1
- package/src/intelligence/loader.ts +96 -5
- package/src/intelligence/types.ts +1 -0
- package/src/packs/types.ts +19 -0
- package/src/playbooks/generic/onboarding.ts +79 -0
- package/src/playbooks/playbook-registry.ts +2 -0
- package/src/runtime/admin-extra-ops.ts +14 -8
- package/src/runtime/admin-ops.ts +4 -4
- package/src/runtime/capture-ops.ts +40 -1
- package/src/runtime/domain-ops.ts +92 -7
- package/src/runtime/facades/cognee-facade.ts +3 -1
- package/src/runtime/facades/index.ts +12 -6
- package/src/runtime/facades/vault-facade.ts +2 -0
- package/src/runtime/orchestrate-ops.ts +271 -62
- package/src/runtime/runtime.ts +27 -18
- package/src/runtime/types.ts +6 -2
- package/src/runtime/vault-linking-ops.ts +454 -0
- package/src/vault/linking.ts +333 -0
- package/src/vault/vault-types.ts +46 -0
- 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
|
+
}
|
package/src/vault/vault-types.ts
CHANGED
|
@@ -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
|
+
}
|
package/src/vault/vault.ts
CHANGED
|
@@ -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.*,
|
|
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
|
-
|
|
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
|
-
|
|
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 {
|