wyrm-mcp 7.3.2 → 7.3.3

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 (184) hide show
  1. package/README.md +8 -14
  2. package/dist/activation.js +59 -1
  3. package/dist/agent-daemon.js +281 -4
  4. package/dist/agent-loop.js +332 -7
  5. package/dist/analytics.js +236 -13
  6. package/dist/attribution.js +49 -1
  7. package/dist/audit.js +457 -2
  8. package/dist/auto-capture.js +138 -3
  9. package/dist/auto-orchestrator.js +325 -1
  10. package/dist/autoconfig.d.ts +50 -0
  11. package/dist/autoconfig.d.ts.map +1 -1
  12. package/dist/autoconfig.js +1115 -39
  13. package/dist/autoconfig.js.map +1 -1
  14. package/dist/buddy-runner.js +109 -1
  15. package/dist/buddy.js +564 -14
  16. package/dist/build-flags.js +15 -1
  17. package/dist/capabilities.js +183 -3
  18. package/dist/capture.js +56 -1
  19. package/dist/causality.js +148 -8
  20. package/dist/cli.js +281 -20
  21. package/dist/cloud/cli.js +541 -5
  22. package/dist/cloud/client.js +221 -1
  23. package/dist/cloud/crypto.js +85 -1
  24. package/dist/cloud/machine-id.js +113 -2
  25. package/dist/cloud/recovery.js +60 -1
  26. package/dist/cloud/sync-engine.js +543 -7
  27. package/dist/cloud-backup.js +579 -5
  28. package/dist/cloud-profile.js +138 -1
  29. package/dist/cloud-sync-entrypoint.js +47 -1
  30. package/dist/cloud-sync.js +309 -2
  31. package/dist/connectors/bridge-source.d.ts +46 -0
  32. package/dist/connectors/bridge-source.d.ts.map +1 -0
  33. package/dist/connectors/bridge-source.js +77 -0
  34. package/dist/connectors/bridge-source.js.map +1 -0
  35. package/dist/connectors/index.d.ts +24 -0
  36. package/dist/connectors/index.d.ts.map +1 -0
  37. package/dist/connectors/index.js +69 -0
  38. package/dist/connectors/index.js.map +1 -0
  39. package/dist/connectors/ingest.d.ts +16 -0
  40. package/dist/connectors/ingest.d.ts.map +1 -0
  41. package/dist/connectors/ingest.js +116 -0
  42. package/dist/connectors/ingest.js.map +1 -0
  43. package/dist/connectors/types.d.ts +99 -0
  44. package/dist/connectors/types.d.ts.map +1 -0
  45. package/dist/connectors/types.js +17 -0
  46. package/dist/connectors/types.js.map +1 -0
  47. package/dist/constellation.js +168 -12
  48. package/dist/content-signature.js +45 -1
  49. package/dist/context-build-budgeted.js +144 -4
  50. package/dist/context-ranking.js +69 -1
  51. package/dist/crypto.js +179 -1
  52. package/dist/daemon-write-endpoint.js +290 -1
  53. package/dist/daemon-writer.js +406 -2
  54. package/dist/database.js +1278 -53
  55. package/dist/deprecations.js +162 -2
  56. package/dist/design.js +141 -13
  57. package/dist/event-replication.js +112 -1
  58. package/dist/events-sse.js +43 -7
  59. package/dist/events.js +238 -6
  60. package/dist/failure-patterns.d.ts +107 -0
  61. package/dist/failure-patterns.d.ts.map +1 -1
  62. package/dist/failure-patterns.js +924 -43
  63. package/dist/failure-patterns.js.map +1 -1
  64. package/dist/federation.js +236 -12
  65. package/dist/goals.js +101 -13
  66. package/dist/golden.js +355 -3
  67. package/dist/handlers/agent.js +165 -4
  68. package/dist/handlers/alias-adapters.js +129 -1
  69. package/dist/handlers/aliases.js +171 -1
  70. package/dist/handlers/audit.js +87 -1
  71. package/dist/handlers/boundary.js +221 -1
  72. package/dist/handlers/capture.js +1114 -73
  73. package/dist/handlers/causality.js +119 -9
  74. package/dist/handlers/cloud.js +382 -85
  75. package/dist/handlers/companion.js +459 -28
  76. package/dist/handlers/datalake.js +187 -7
  77. package/dist/handlers/dispatch-context.js +22 -0
  78. package/dist/handlers/entity.js +256 -25
  79. package/dist/handlers/events.js +335 -16
  80. package/dist/handlers/failure.d.ts.map +1 -1
  81. package/dist/handlers/failure.js +408 -13
  82. package/dist/handlers/failure.js.map +1 -1
  83. package/dist/handlers/goals.js +296 -4
  84. package/dist/handlers/intelligence.js +681 -126
  85. package/dist/handlers/invoicing.js +70 -1
  86. package/dist/handlers/mcpclient.js +137 -6
  87. package/dist/handlers/orchestration.js +125 -40
  88. package/dist/handlers/output-schemas.js +24 -1
  89. package/dist/handlers/presence.js +99 -3
  90. package/dist/handlers/project.js +182 -28
  91. package/dist/handlers/prompts.js +157 -6
  92. package/dist/handlers/quest.js +224 -4
  93. package/dist/handlers/recall.js +237 -13
  94. package/dist/handlers/registry.js +167 -1
  95. package/dist/handlers/resources.js +288 -1
  96. package/dist/handlers/review.js +74 -11
  97. package/dist/handlers/run.js +498 -16
  98. package/dist/handlers/search.js +338 -15
  99. package/dist/handlers/session.js +643 -31
  100. package/dist/handlers/share.js +184 -8
  101. package/dist/handlers/shims.js +464 -1
  102. package/dist/handlers/skill.js +449 -67
  103. package/dist/handlers/survivors.js +120 -1
  104. package/dist/handlers/symbols.js +109 -8
  105. package/dist/handlers/syncops.js +302 -4
  106. package/dist/handlers/types.js +27 -1
  107. package/dist/harvest.js +191 -5
  108. package/dist/hours.js +156 -7
  109. package/dist/http-auth.js +321 -3
  110. package/dist/http-fast.js +1302 -22
  111. package/dist/icons.js +47 -1
  112. package/dist/importers.js +268 -1
  113. package/dist/index.js +840 -2
  114. package/dist/indexer.js +145 -4
  115. package/dist/intelligence.js +261 -31
  116. package/dist/internal-dispatch.js +212 -3
  117. package/dist/keyset.js +110 -1
  118. package/dist/knowledge-graph.js +176 -12
  119. package/dist/license.js +441 -2
  120. package/dist/logger.js +199 -2
  121. package/dist/maintenance.js +148 -2
  122. package/dist/mcp-client.js +262 -6
  123. package/dist/memory-artifacts.js +596 -32
  124. package/dist/migrate-prompt.js +124 -2
  125. package/dist/migrations.d.ts.map +1 -1
  126. package/dist/migrations.js +799 -42
  127. package/dist/migrations.js.map +1 -1
  128. package/dist/performance.js +228 -1
  129. package/dist/presence.js +140 -11
  130. package/dist/priority-embed.js +164 -5
  131. package/dist/providers/embedding-provider.js +196 -1
  132. package/dist/readonly-gate.js +29 -1
  133. package/dist/receipt.js +43 -1
  134. package/dist/rehydration.js +157 -9
  135. package/dist/reindex.js +88 -1
  136. package/dist/render-target.js +544 -21
  137. package/dist/render.js +280 -4
  138. package/dist/repl-guard.js +173 -1
  139. package/dist/replication-daemon-entrypoint.js +31 -1
  140. package/dist/replication-daemon.js +262 -2
  141. package/dist/rerank.js +142 -1
  142. package/dist/resilience.js +591 -1
  143. package/dist/reverse-bridge.js +360 -5
  144. package/dist/security.js +244 -1
  145. package/dist/session-seen.js +51 -3
  146. package/dist/setup.js +260 -1
  147. package/dist/skill-author.js +168 -5
  148. package/dist/spec-kit.js +191 -1
  149. package/dist/sqlite-busy.js +154 -1
  150. package/dist/statusline.js +315 -11
  151. package/dist/sub-agent.js +262 -13
  152. package/dist/summarizer.js +139 -13
  153. package/dist/symbols.js +283 -7
  154. package/dist/sync.js +359 -5
  155. package/dist/tasks-dispatch.js +84 -1
  156. package/dist/tasks.js +282 -1
  157. package/dist/token-budget.js +143 -1
  158. package/dist/tool-analytics.js +129 -7
  159. package/dist/tool-annotations.js +365 -1
  160. package/dist/tool-manifest-v2.json +1 -1
  161. package/dist/tool-manifest.json +1 -1
  162. package/dist/tool-profiles.js +75 -1
  163. package/dist/trace-harvest.js +244 -6
  164. package/dist/types.js +30 -1
  165. package/dist/ui-dashboard.js +50 -41
  166. package/dist/ulid.js +81 -1
  167. package/dist/usage-tracker.js +66 -1
  168. package/dist/validate.js +129 -1
  169. package/dist/vault.js +534 -1
  170. package/dist/vector-init.js +67 -1
  171. package/dist/vectors.js +184 -3
  172. package/dist/version-check.js +136 -4
  173. package/dist/visibility.js +155 -19
  174. package/dist/wyrm-cli.js +2845 -101
  175. package/dist/wyrm-cli.js.map +1 -1
  176. package/dist/wyrm-guard.d.ts.map +1 -1
  177. package/dist/wyrm-guard.js +475 -14
  178. package/dist/wyrm-guard.js.map +1 -1
  179. package/dist/wyrm-loop.js +150 -3
  180. package/dist/wyrm-manifest.json +1 -1
  181. package/dist/wyrm-statusline-daemon.js +11 -1
  182. package/dist/wyrm-statusline.js +56 -4
  183. package/dist/wyrm-ui.js +77 -9
  184. package/package.json +1 -1
@@ -1,8 +1,217 @@
1
- import{sanitizeFtsQuery as k}from"./security.js";import{classifyCapture as _}from"./capture.js";function q(p){const{db:r,failures:i,symbols:a,rehydration:m,causality:u,presence:f,federation:j,memory:c,groundTruths:y}=p;return async(d,s)=>{try{switch(d){case"wyrm_search":{const e=String(s.query??s.q??"").slice(0,200);if(!e)return{ok:!1,error:"query required"};const t=k(e);return{ok:!0,result:r.getDatabase().prepare(`
1
+ /**
2
+ * Internal tool dispatcher for the AgentLoop (v7 F4 T036 — extracted from the
3
+ * index.ts monolith). A curated subset of read-mostly + bounded-write tools the
4
+ * autonomous OODA loop is allowed to call; the whitelist itself is enforced in
5
+ * agent-loop.ts (SAFE_INTERNAL_TOOLS). Lifted VERBATIM out of index.ts behind a
6
+ * factory that captures the same subsystem singletons it always closed over —
7
+ * the single-quoted `case` labels here are what tests/tool-surface-integrity.ts
8
+ * scans to keep the whitelist and the dispatcher in lockstep.
9
+ *
10
+ * @copyright 2026 Ghost Protocol (Pvt) Ltd.
11
+ * @license Proprietary — (c) 2026 Ghost Protocol (Pvt) Ltd. All rights reserved. See LICENSE.
12
+ */
13
+ import { sanitizeFtsQuery } from "./security.js";
14
+ import { classifyCapture } from "./capture.js";
15
+ export function makeInternalDispatch(deps) {
16
+ const { db, failures, symbols, rehydration, causality, presence, federation, memory, groundTruths } = deps;
17
+ const internalDispatch = async (name, args) => {
18
+ try {
19
+ switch (name) {
20
+ case 'wyrm_search': {
21
+ const q = String(args.query ?? args.q ?? '').slice(0, 200);
22
+ if (!q)
23
+ return { ok: false, error: 'query required' };
24
+ const sanitized = sanitizeFtsQuery(q);
25
+ const rows = db.getDatabase().prepare(`
2
26
  SELECT s.id, s.date, s.summary FROM sessions s
3
27
  JOIN sessions_fts fts ON fts.rowid = s.id
4
28
  WHERE sessions_fts MATCH ? LIMIT 10
5
- `).all(t)}}case"wyrm_all_quests":return{ok:!0,result:r.getAllPendingQuests().slice(0,30)};case"wyrm_truth_get":{const e=s.projectPath,t=e?r.getProject(e):null;return{ok:!0,result:t?r.getDatabase().prepare(`
29
+ `).all(sanitized);
30
+ return { ok: true, result: rows };
31
+ }
32
+ case 'wyrm_all_quests': {
33
+ return { ok: true, result: db.getAllPendingQuests().slice(0, 30) };
34
+ }
35
+ case 'wyrm_truth_get': {
36
+ const projPath = args.projectPath;
37
+ const proj = projPath ? db.getProject(projPath) : null;
38
+ const rows = proj
39
+ ? db.getDatabase().prepare(`
6
40
  SELECT category, key, value, rationale FROM ground_truths
7
41
  WHERE project_id = ? AND is_current = 1 ORDER BY confidence DESC LIMIT 30
8
- `).all(t.id):[]}}case"wyrm_failure_check":{const e=s,t=e.projectPath?r.getProject(e.projectPath):null;return{ok:!0,result:i.check(e.scope,e.target,e.description,t?.id??null)}}case"wyrm_failure_list":{const e=s,t=e.projectPath?r.getProject(e.projectPath):null;return{ok:!0,result:i.list(t?.id??null,Math.min(50,e.limit??20))}}case"wyrm_failure_record":{const e=s,t=e.projectPath?r.getProject(e.projectPath):null;return{ok:!0,result:i.record({project_id:t?.id??null,scope:e.scope,target:e.target,description:e.description,why_failed:e.why_failed,severity:e.severity})}}case"wyrm_symbol_search":{const e=s;if(!e.symbol)return{ok:!1,error:"symbol required"};const t=e.projectPath?r.getProject(e.projectPath):null;return{ok:!0,result:a.search(e.symbol,{projectId:t?.id,kind:e.kind,language:e.language,exact:e.exact,limit:Math.min(50,e.limit??20)})}}case"wyrm_symbol_callers":{const e=s,t=e.projectPath?r.getProject(e.projectPath):null;return{ok:!0,result:a.callers(e.symbol,t?.id).slice(0,30)}}case"wyrm_symbol_stats":{const e=s,t=e.projectPath?r.getProject(e.projectPath):null;return{ok:!0,result:a.stats(t?.id)}}case"wyrm_session_rehydrate":{const e=s,t=m.rehydrate(e.session_id);return{ok:t!=null,result:t,error:t?void 0:"session not found"}}case"wyrm_decision_upstream":{const e=s;return{ok:!0,result:u.upstreamOf(e.kind,e.id)}}case"wyrm_decision_downstream":{const e=s;return{ok:!0,result:u.downstreamOf(e.kind,e.id)}}case"wyrm_decided_because":{const e=s,t=r.getProject(e.projectPath);return t?{ok:!0,result:u.link({project_id:t.id,...e})}:{ok:!1,error:"project not found"}}case"wyrm_presence_list":{const e=s,t=e.projectPath?r.getProject(e.projectPath):null;return{ok:!0,result:f.liveAgents(t?.id??null)}}case"wyrm_sync_conflicts":return{ok:!0,result:j.unresolvedConflicts(20)};case"wyrm_quest_add":{const e=s,t=r.getProject(e.projectPath);if(!t)return{ok:!1,error:"project not found"};const o=r.addQuest(t.id,e.title,e.description,e.priority||"medium",e.tags);return{ok:!0,result:{id:o.id,title:o.title}}}case"wyrm_quest_complete":{const e=s;return{ok:!0,result:{id:r.updateQuest(e.questId,"completed").id,status:"completed"}}}case"wyrm_project_context":{const e=s,t=r.getProject(e.projectPath);return t?{ok:!0,result:{project:t.name,quests:r.getAllPendingQuests().filter(n=>n.project_id===t.id).slice(0,10),last_session:r.getDatabase().prepare("SELECT id, date, summary FROM sessions WHERE project_id = ? ORDER BY date DESC LIMIT 1").get(t.id)}}:{ok:!1,error:"project not found"}}case"wyrm_global_context":{const e=s,t=r.getAllProjects(e.maxProjects??20).map(n=>({name:n.name,stack:n.stack??null,stats:r.getProjectStats(n.id)})),o={globalContext:r.getAllGlobalContext(),projects:t};return e.includeQuests&&(o.quests=r.getAllPendingQuests().slice(0,20)),{ok:!0,result:o}}case"wyrm_recall":{const e=s,t=e.projectPath?r.getProject(e.projectPath):null;return t?e.query?{ok:!0,result:c.recall(t.id,e.query,{kind:e.kind,limit:Math.min(20,e.limit??10),minConfidence:e.minConfidence}).map(n=>({id:n.artifact.id,kind:n.artifact.kind,problem:n.artifact.problem,validated_fix:n.artifact.validated_fix,relevance:n.relevance_score}))}:{ok:!1,error:"query required"}:{ok:!1,error:"project not found"}}case"wyrm_remember":{const e=s,t=e.projectPath?r.getProject(e.projectPath):null;if(!t)return{ok:!1,error:"project not found"};if(!e.problem)return{ok:!1,error:"problem required"};const o=c.add(t.id,{kind:e.kind,problem:e.problem,constraints:e.constraints,validatedFix:e.validatedFix,whyItWorked:e.whyItWorked,outcome:e.outcome,tags:e.tags,confidence:e.confidence,sourceSessionId:e.sourceSessionId});return{ok:!0,result:{id:o.id,kind:o.kind}}}case"wyrm_distill":{const e=s,t=e.projectPath?r.getProject(e.projectPath):null;if(!t)return{ok:!1,error:"project not found"};if(!e.candidates?.length)return{ok:!1,error:"candidates required"};const o=e.candidates.map(n=>c.add(t.id,{kind:n.kind,problem:n.title,validatedFix:n.content,tags:n.tags??[],confidence:n.confidence??.7,needsReview:1}).id);return{ok:!0,result:{queued:o.length,ids:o}}}case"wyrm_capture":{const e=s;if(!e.content)return{ok:!1,error:"content required"};let t=_(e.content);if(e.mode&&e.mode!=="auto"){const l={quest:"quest",truth:"decision",memory:"pattern"};t={type:e.mode,subtype:l[e.mode]??e.mode,confidence:100,reasoning:`mode:${e.mode}`}}const o=e.project_id??null;return t.type==="quest"?o===null?{ok:!1,error:"project_id required for quest"}:{ok:!0,result:{type:"quest",id:r.addQuest(o,e.content.slice(0,200),"","medium",e.tags?.join(",")).id}}:t.type==="truth"?o===null?{ok:!1,error:"project_id required for truth"}:e.mode==="truth"||t.confidence>=100?{ok:!0,result:{type:"truth",id:y.set(o,{category:"decision",key:e.content.slice(0,60),value:e.content}).id}}:{ok:!0,result:{type:"memory_review",id:c.add(o,{kind:"pattern",problem:e.content,tags:e.tags??[],confidence:t.confidence/100,needsReview:1}).id}}:o===null?{ok:!1,error:"project_id required for memory"}:{ok:!0,result:{type:"memory",id:c.add(o,{kind:"pattern",problem:e.content,tags:e.tags??[],confidence:(t.confidence??70)/100,needsReview:1}).id}}}default:return{ok:!1,error:`Internal tool '${d}' not implemented in dispatcher`}}}catch(e){return{ok:!1,error:e.message}}}}export{q as makeInternalDispatch};
42
+ `).all(proj.id)
43
+ : [];
44
+ return { ok: true, result: rows };
45
+ }
46
+ case 'wyrm_failure_check': {
47
+ const a = args;
48
+ const proj = a.projectPath ? db.getProject(a.projectPath) : null;
49
+ return { ok: true, result: failures.check(a.scope, a.target, a.description, proj?.id ?? null) };
50
+ }
51
+ case 'wyrm_failure_list': {
52
+ const a = args;
53
+ const proj = a.projectPath ? db.getProject(a.projectPath) : null;
54
+ return { ok: true, result: failures.list(proj?.id ?? null, Math.min(50, a.limit ?? 20)) };
55
+ }
56
+ case 'wyrm_failure_record': {
57
+ const a = args;
58
+ const proj = a.projectPath ? db.getProject(a.projectPath) : null;
59
+ return { ok: true, result: failures.record({ project_id: proj?.id ?? null, scope: a.scope, target: a.target, description: a.description, why_failed: a.why_failed, severity: a.severity }) };
60
+ }
61
+ case 'wyrm_symbol_search': {
62
+ const a = args;
63
+ if (!a.symbol)
64
+ return { ok: false, error: 'symbol required' };
65
+ const proj = a.projectPath ? db.getProject(a.projectPath) : null;
66
+ return { ok: true, result: symbols.search(a.symbol, { projectId: proj?.id, kind: a.kind, language: a.language, exact: a.exact, limit: Math.min(50, a.limit ?? 20) }) };
67
+ }
68
+ case 'wyrm_symbol_callers': {
69
+ const a = args;
70
+ const proj = a.projectPath ? db.getProject(a.projectPath) : null;
71
+ return { ok: true, result: symbols.callers(a.symbol, proj?.id).slice(0, 30) };
72
+ }
73
+ case 'wyrm_symbol_stats': {
74
+ const a = args;
75
+ const proj = a.projectPath ? db.getProject(a.projectPath) : null;
76
+ return { ok: true, result: symbols.stats(proj?.id) };
77
+ }
78
+ case 'wyrm_session_rehydrate': {
79
+ const a = args;
80
+ const brief = rehydration.rehydrate(a.session_id);
81
+ return { ok: brief != null, result: brief, error: brief ? undefined : 'session not found' };
82
+ }
83
+ case 'wyrm_decision_upstream': {
84
+ const a = args;
85
+ return { ok: true, result: causality.upstreamOf(a.kind, a.id) };
86
+ }
87
+ case 'wyrm_decision_downstream': {
88
+ const a = args;
89
+ return { ok: true, result: causality.downstreamOf(a.kind, a.id) };
90
+ }
91
+ case 'wyrm_decided_because': {
92
+ const a = args;
93
+ const proj = db.getProject(a.projectPath);
94
+ if (!proj)
95
+ return { ok: false, error: 'project not found' };
96
+ return { ok: true, result: causality.link({ project_id: proj.id, ...a }) };
97
+ }
98
+ case 'wyrm_presence_list': {
99
+ const a = args;
100
+ const proj = a.projectPath ? db.getProject(a.projectPath) : null;
101
+ return { ok: true, result: presence.liveAgents(proj?.id ?? null) };
102
+ }
103
+ case 'wyrm_sync_conflicts': {
104
+ return { ok: true, result: federation.unresolvedConflicts(20) };
105
+ }
106
+ case 'wyrm_quest_add': {
107
+ const a = args;
108
+ const proj = db.getProject(a.projectPath);
109
+ if (!proj)
110
+ return { ok: false, error: 'project not found' };
111
+ const q = db.addQuest(proj.id, a.title, a.description, a.priority || 'medium', a.tags);
112
+ return { ok: true, result: { id: q.id, title: q.title } };
113
+ }
114
+ case 'wyrm_quest_complete': {
115
+ const a = args;
116
+ const q = db.updateQuest(a.questId, 'completed');
117
+ return { ok: true, result: { id: q.id, status: 'completed' } };
118
+ }
119
+ case 'wyrm_project_context': {
120
+ const a = args;
121
+ const proj = db.getProject(a.projectPath);
122
+ if (!proj)
123
+ return { ok: false, error: 'project not found' };
124
+ // Summary: open quests + latest session
125
+ const ctx = {
126
+ project: proj.name,
127
+ quests: db.getAllPendingQuests().filter(q => q.project_id === proj.id).slice(0, 10),
128
+ last_session: db.getDatabase().prepare('SELECT id, date, summary FROM sessions WHERE project_id = ? ORDER BY date DESC LIMIT 1').get(proj.id),
129
+ };
130
+ return { ok: true, result: ctx };
131
+ }
132
+ // ── Memory ops the OODA loop is whitelisted for (SAFE_INTERNAL_TOOLS).
133
+ // These were promised by the whitelist but had no dispatch case until 6.8.1,
134
+ // so the loop silently failed on them. Kept in lockstep by
135
+ // tests/tool-surface-integrity.test.ts.
136
+ case 'wyrm_global_context': {
137
+ const a = args;
138
+ const projects = db.getAllProjects(a.maxProjects ?? 20).map((p) => ({
139
+ name: p.name, stack: p.stack ?? null, stats: db.getProjectStats(p.id),
140
+ }));
141
+ const result = { globalContext: db.getAllGlobalContext(), projects };
142
+ if (a.includeQuests)
143
+ result.quests = db.getAllPendingQuests().slice(0, 20);
144
+ return { ok: true, result };
145
+ }
146
+ case 'wyrm_recall': {
147
+ const a = args;
148
+ const proj = a.projectPath ? db.getProject(a.projectPath) : null;
149
+ if (!proj)
150
+ return { ok: false, error: 'project not found' };
151
+ if (!a.query)
152
+ return { ok: false, error: 'query required' };
153
+ const results = memory.recall(proj.id, a.query, { kind: a.kind, limit: Math.min(20, a.limit ?? 10), minConfidence: a.minConfidence });
154
+ return { ok: true, result: results.map((r) => ({ id: r.artifact.id, kind: r.artifact.kind, problem: r.artifact.problem, validated_fix: r.artifact.validated_fix, relevance: r.relevance_score })) };
155
+ }
156
+ case 'wyrm_remember': {
157
+ const a = args;
158
+ const proj = a.projectPath ? db.getProject(a.projectPath) : null;
159
+ if (!proj)
160
+ return { ok: false, error: 'project not found' };
161
+ if (!a.problem)
162
+ return { ok: false, error: 'problem required' };
163
+ const art = memory.add(proj.id, { kind: a.kind, problem: a.problem, constraints: a.constraints, validatedFix: a.validatedFix, whyItWorked: a.whyItWorked, outcome: a.outcome, tags: a.tags, confidence: a.confidence, sourceSessionId: a.sourceSessionId });
164
+ return { ok: true, result: { id: art.id, kind: art.kind } };
165
+ }
166
+ case 'wyrm_distill': {
167
+ const a = args;
168
+ const proj = a.projectPath ? db.getProject(a.projectPath) : null;
169
+ if (!proj)
170
+ return { ok: false, error: 'project not found' };
171
+ if (!a.candidates?.length)
172
+ return { ok: false, error: 'candidates required' };
173
+ const ids = a.candidates.map((c) => memory.add(proj.id, { kind: c.kind, problem: c.title, validatedFix: c.content, tags: c.tags ?? [], confidence: c.confidence ?? 0.7, needsReview: 1 }).id);
174
+ return { ok: true, result: { queued: ids.length, ids } };
175
+ }
176
+ case 'wyrm_capture': {
177
+ const a = args;
178
+ if (!a.content)
179
+ return { ok: false, error: 'content required' };
180
+ let cls = classifyCapture(a.content);
181
+ if (a.mode && a.mode !== 'auto') {
182
+ const sub = { quest: 'quest', truth: 'decision', memory: 'pattern' };
183
+ cls = { type: a.mode, subtype: sub[a.mode] ?? a.mode, confidence: 100, reasoning: `mode:${a.mode}` };
184
+ }
185
+ const pid = a.project_id ?? null;
186
+ if (cls.type === 'quest') {
187
+ if (pid === null)
188
+ return { ok: false, error: 'project_id required for quest' };
189
+ const q = db.addQuest(pid, a.content.slice(0, 200), '', 'medium', a.tags?.join(','));
190
+ return { ok: true, result: { type: 'quest', id: q.id } };
191
+ }
192
+ if (cls.type === 'truth') {
193
+ if (pid === null)
194
+ return { ok: false, error: 'project_id required for truth' };
195
+ if (a.mode === 'truth' || cls.confidence >= 100) {
196
+ const t = groundTruths.set(pid, { category: 'decision', key: a.content.slice(0, 60), value: a.content });
197
+ return { ok: true, result: { type: 'truth', id: t.id } };
198
+ }
199
+ const art = memory.add(pid, { kind: 'pattern', problem: a.content, tags: a.tags ?? [], confidence: cls.confidence / 100, needsReview: 1 });
200
+ return { ok: true, result: { type: 'memory_review', id: art.id } };
201
+ }
202
+ if (pid === null)
203
+ return { ok: false, error: 'project_id required for memory' };
204
+ const art = memory.add(pid, { kind: 'pattern', problem: a.content, tags: a.tags ?? [], confidence: (cls.confidence ?? 70) / 100, needsReview: 1 });
205
+ return { ok: true, result: { type: 'memory', id: art.id } };
206
+ }
207
+ default:
208
+ return { ok: false, error: `Internal tool '${name}' not implemented in dispatcher` };
209
+ }
210
+ }
211
+ catch (err) {
212
+ return { ok: false, error: err.message };
213
+ }
214
+ };
215
+ return internalDispatch;
216
+ }
217
+ //# sourceMappingURL=internal-dispatch.js.map
package/dist/keyset.js CHANGED
@@ -1 +1,110 @@
1
- const a=50,m=200;function S(t,n={}){const r=n.defaultSize??50,e=n.maxSize??200;return t==null||!Number.isFinite(t)||t<=0?Math.min(r,e):Math.min(Math.floor(t),e)}function c(t){const n=String(t.sortKey),r=`${n.length}.${n}:${t.id}`;return Buffer.from(r,"utf8").toString("base64url")}function g(t){if(typeof t!="string"||t.length===0)return null;let n;try{n=Buffer.from(t,"base64url").toString("utf8")}catch{return null}const r=n.indexOf(".");if(r<=0)return null;const e=n.slice(0,r);if(!/^\d{1,9}$/.test(e))return null;const l=Number(e),u=n.slice(r+1);if(u.length<l+1)return null;const s=u.slice(0,l);if(u.charAt(l)!==":")return null;const o=u.slice(l+1);if(!/^\d{1,15}$/.test(o))return null;const i=Number(o);return!Number.isSafeInteger(i)||i<0?null:{sortKey:s,id:i}}function d(t,n,r){if(t.length<=n)return{items:t};const e=t.slice(0,n),l=e[e.length-1];return{items:e,nextCursor:c(r(l))}}export{a as DEFAULT_PAGE_SIZE,m as MAX_PAGE_SIZE,d as buildPage,S as clampPageSize,g as decodeCursor,c as encodeCursor};
1
+ /**
2
+ * Keyset (cursor) pagination — the v7 F4 T035 context-economy primitive.
3
+ *
4
+ * THE MOVE: a big listing returns a PAGE plus an opaque `nextCursor`, not the
5
+ * whole table. The cursor is a COMPOSITE `(sortKey, id)` — never a bare
6
+ * sortKey. This is deliberate: the wyrm-cloud sync keyset bug (fixed at
7
+ * cloud/client.ts:299 with a `"<updated_at>:<id>"` composite) is the
8
+ * cautionary tale — a bare `sortKey` cursor SILENTLY DROPS every row that
9
+ * shares the boundary row's sort value (e.g. three artifacts with identical
10
+ * `confidence` straddling a page edge). The id tiebreak makes the keyset
11
+ * total-ordered, so `(sortKey, id) > (lastSortKey, lastId)` resumes EXACTLY
12
+ * after the last emitted row, dropping nothing and repeating nothing.
13
+ *
14
+ * The cursor is OPAQUE on the wire: a base64url-encoded `"<sortKey>:<id>"`
15
+ * string. Callers MUST treat it as a token (round-trip it verbatim); they
16
+ * never parse it. A malformed/garbage cursor decodes to `null` and the caller
17
+ * degrades to "start from the first page" (never throws on the wire — a
18
+ * resource-less or buggy client can't be wedged by a bad token).
19
+ *
20
+ * Article III: pure, deterministic, no network, no clock, no LLM. Same rows in
21
+ * ⇒ same page + same cursor out. Article VII: hard default + max caps so an
22
+ * unbounded `limit` can never be requested; the encode/decode is total (no
23
+ * throw on adversarial input).
24
+ *
25
+ * @copyright 2026 Ghost Protocol (Pvt) Ltd.
26
+ * @license Proprietary — (c) 2026 Ghost Protocol (Pvt) Ltd. All rights reserved. See LICENSE.
27
+ */
28
+ /** Default page size when a caller passes no `limit`. */
29
+ export const DEFAULT_PAGE_SIZE = 50;
30
+ /** Absolute ceiling on a single page — a caller can never exceed this. */
31
+ export const MAX_PAGE_SIZE = 200;
32
+ /**
33
+ * Clamp a requested limit into `[1, MAX_PAGE_SIZE]`, defaulting to
34
+ * `DEFAULT_PAGE_SIZE` when absent/NaN/≤0. The hard cap holds regardless of
35
+ * what the caller asks for (Article VII: bounded by construction).
36
+ */
37
+ export function clampPageSize(requested, opts = {}) {
38
+ const def = opts.defaultSize ?? DEFAULT_PAGE_SIZE;
39
+ const max = opts.maxSize ?? MAX_PAGE_SIZE;
40
+ if (requested == null || !Number.isFinite(requested) || requested <= 0)
41
+ return Math.min(def, max);
42
+ return Math.min(Math.floor(requested), max);
43
+ }
44
+ /**
45
+ * Encode a composite `(sortKey, id)` anchor into an opaque base64url token.
46
+ * The sortKey is length-prefixed (`<len>.<sortKey>:<id>`) so a sortKey that
47
+ * itself contains a `:` (e.g. an ISO timestamp `2026-06-13T10:00:00`) round-
48
+ * trips losslessly — decode reads exactly `<len>` chars for the sortKey.
49
+ */
50
+ export function encodeCursor(cursor) {
51
+ const sk = String(cursor.sortKey);
52
+ const raw = `${sk.length}.${sk}:${cursor.id}`;
53
+ return Buffer.from(raw, 'utf8').toString('base64url');
54
+ }
55
+ /**
56
+ * Decode an opaque cursor token back into `(sortKey, id)`. Returns `null` for
57
+ * any malformed/garbage/empty token — the caller degrades to the first page
58
+ * rather than throwing on the wire (Article VII: total on adversarial input).
59
+ */
60
+ export function decodeCursor(token) {
61
+ if (typeof token !== 'string' || token.length === 0)
62
+ return null;
63
+ let raw;
64
+ try {
65
+ raw = Buffer.from(token, 'base64url').toString('utf8');
66
+ }
67
+ catch {
68
+ return null;
69
+ }
70
+ // Grammar: <len>.<sortKey(len chars)>:<id>
71
+ const dot = raw.indexOf('.');
72
+ if (dot <= 0)
73
+ return null;
74
+ const lenStr = raw.slice(0, dot);
75
+ if (!/^\d{1,9}$/.test(lenStr))
76
+ return null;
77
+ const len = Number(lenStr);
78
+ const after = raw.slice(dot + 1);
79
+ if (after.length < len + 1)
80
+ return null;
81
+ const sortKey = after.slice(0, len);
82
+ const sep = after.charAt(len);
83
+ if (sep !== ':')
84
+ return null;
85
+ const idStr = after.slice(len + 1);
86
+ if (!/^\d{1,15}$/.test(idStr))
87
+ return null;
88
+ const id = Number(idStr);
89
+ if (!Number.isSafeInteger(id) || id < 0)
90
+ return null;
91
+ return { sortKey, id };
92
+ }
93
+ /**
94
+ * Build a page from an OVER-FETCHED row array. Call the underlying query with
95
+ * `limit = pageSize + 1`; pass the result here. If `pageSize + 1` rows came
96
+ * back there is a further page: slice to `pageSize` and mint a `nextCursor`
97
+ * from the LAST emitted row via `anchorOf`. The over-fetch-by-one is how we
98
+ * know "is there more" without a second COUNT query.
99
+ *
100
+ * `anchorOf` must return the SAME `(sortKey, id)` the query's WHERE/ORDER use,
101
+ * or pages will skip/repeat. Keep them in lockstep at the call site.
102
+ */
103
+ export function buildPage(rows, pageSize, anchorOf) {
104
+ if (rows.length <= pageSize)
105
+ return { items: rows };
106
+ const items = rows.slice(0, pageSize);
107
+ const last = items[items.length - 1];
108
+ return { items, nextCursor: encodeCursor(anchorOf(last)) };
109
+ }
110
+ //# sourceMappingURL=keyset.js.map
@@ -1,31 +1,139 @@
1
- const p=5,_=200;class l{db;constructor(t){this.db=t}addEntity(t,e,i,n,r){const s=this.db.prepare(`
1
+ /**
2
+ * Wyrm Knowledge Graph — Entity & Relationship Management
3
+ *
4
+ * @copyright 2026 Ghost Protocol (Pvt) Ltd.
5
+ * @license Proprietary — (c) 2026 Ghost Protocol (Pvt) Ltd. All rights reserved. See LICENSE.
6
+ *
7
+ * Manual-first knowledge graph with provenance tracking:
8
+ * - Named entities with types and metadata
9
+ * - Typed, directed relationships between entities
10
+ * - Aliases for deduplication
11
+ * - Graph traversal via recursive CTEs (neighborhood, paths)
12
+ * - FTS5 search on entity names
13
+ */
14
+ const MAX_DEPTH = 5;
15
+ const MAX_RESULTS = 200;
16
+ /**
17
+ * Knowledge graph operations backed by SQLite.
18
+ */
19
+ export class KnowledgeGraph {
20
+ db;
21
+ constructor(db) {
22
+ this.db = db;
23
+ }
24
+ // ==================== ENTITIES ====================
25
+ addEntity(projectId, name, type, metadata, createdBy) {
26
+ const result = this.db.prepare(`
2
27
  INSERT INTO entities (project_id, name, type, metadata, created_by)
3
28
  VALUES (?, ?, ?, ?, ?)
4
- `).run(t,e.trim(),i.trim(),n??null,r??"local");return this.db.prepare("SELECT * FROM entities WHERE id = ?").get(s.lastInsertRowid)}getEntity(t){return this.db.prepare("SELECT * FROM entities WHERE id = ?").get(t)??null}findEntity(t,e,i){return i?this.db.prepare("SELECT * FROM entities WHERE project_id = ? AND name = ? AND type = ?").get(t,e,i)??null:this.db.prepare("SELECT * FROM entities WHERE project_id = ? AND name = ?").get(t,e)??null}updateEntity(t,e){return this.getEntity(t)?(this.db.prepare(`
29
+ `).run(projectId, name.trim(), type.trim(), metadata ?? null, createdBy ?? 'local');
30
+ return this.db.prepare('SELECT * FROM entities WHERE id = ?').get(result.lastInsertRowid);
31
+ }
32
+ getEntity(id) {
33
+ return this.db.prepare('SELECT * FROM entities WHERE id = ?').get(id) ?? null;
34
+ }
35
+ findEntity(projectId, name, type) {
36
+ if (type) {
37
+ return this.db.prepare('SELECT * FROM entities WHERE project_id = ? AND name = ? AND type = ?').get(projectId, name, type) ?? null;
38
+ }
39
+ return this.db.prepare('SELECT * FROM entities WHERE project_id = ? AND name = ?').get(projectId, name) ?? null;
40
+ }
41
+ updateEntity(id, updates) {
42
+ const entity = this.getEntity(id);
43
+ if (!entity)
44
+ return null;
45
+ this.db.prepare(`
5
46
  UPDATE entities SET
6
47
  name = COALESCE(?, name),
7
48
  type = COALESCE(?, type),
8
49
  metadata = COALESCE(?, metadata),
9
50
  updated_at = datetime('now')
10
51
  WHERE id = ?
11
- `).run(e.name??null,e.type??null,e.metadata??null,t),this.getEntity(t)):null}deleteEntity(t){return this.db.prepare("DELETE FROM entities WHERE id = ?").run(t).changes>0}listEntities(t,e){let i="SELECT * FROM entities WHERE project_id = ?";const n=[t];return e?.type&&(i+=" AND type = ?",n.push(e.type)),i+=" ORDER BY name ASC",i+=" LIMIT ? OFFSET ?",n.push(e?.limit??50,e?.offset??0),this.db.prepare(i).all(...n)}searchEntities(t,e,i=20){try{const n=this.db.prepare(`
52
+ `).run(updates.name ?? null, updates.type ?? null, updates.metadata ?? null, id);
53
+ return this.getEntity(id);
54
+ }
55
+ deleteEntity(id) {
56
+ const result = this.db.prepare('DELETE FROM entities WHERE id = ?').run(id);
57
+ return result.changes > 0;
58
+ }
59
+ listEntities(projectId, options) {
60
+ let sql = 'SELECT * FROM entities WHERE project_id = ?';
61
+ const params = [projectId];
62
+ if (options?.type) {
63
+ sql += ' AND type = ?';
64
+ params.push(options.type);
65
+ }
66
+ sql += ' ORDER BY name ASC';
67
+ sql += ` LIMIT ? OFFSET ?`;
68
+ params.push(options?.limit ?? 50, options?.offset ?? 0);
69
+ return this.db.prepare(sql).all(...params);
70
+ }
71
+ searchEntities(projectId, query, limit = 20) {
72
+ // Try FTS first, fall back to LIKE
73
+ try {
74
+ const ftsResults = this.db.prepare(`
12
75
  SELECT e.* FROM entities e
13
76
  JOIN entities_fts f ON f.rowid = e.id
14
77
  WHERE f.entities_fts MATCH ? AND e.project_id = ?
15
78
  LIMIT ?
16
- `).all(e,t,i);if(n.length>0)return n}catch{}return this.db.prepare(`
79
+ `).all(query, projectId, limit);
80
+ if (ftsResults.length > 0)
81
+ return ftsResults;
82
+ }
83
+ catch {
84
+ // FTS query syntax error — fall back to LIKE
85
+ }
86
+ return this.db.prepare(`
17
87
  SELECT * FROM entities WHERE project_id = ? AND (name LIKE ? OR type LIKE ?)
18
88
  ORDER BY name ASC LIMIT ?
19
- `).all(t,`%${e}%`,`%${e}%`,i)}addAlias(t,e){this.db.prepare("INSERT OR IGNORE INTO entity_aliases (entity_id, alias) VALUES (?, ?)").run(t,e.trim())}getAliases(t){return this.db.prepare("SELECT alias FROM entity_aliases WHERE entity_id = ?").all(t).map(i=>i.alias)}findByAlias(t,e){return this.db.prepare(`
89
+ `).all(projectId, `%${query}%`, `%${query}%`, limit);
90
+ }
91
+ // ==================== ALIASES ====================
92
+ addAlias(entityId, alias) {
93
+ this.db.prepare('INSERT OR IGNORE INTO entity_aliases (entity_id, alias) VALUES (?, ?)').run(entityId, alias.trim());
94
+ }
95
+ getAliases(entityId) {
96
+ const rows = this.db.prepare('SELECT alias FROM entity_aliases WHERE entity_id = ?').all(entityId);
97
+ return rows.map(r => r.alias);
98
+ }
99
+ findByAlias(projectId, alias) {
100
+ return this.db.prepare(`
20
101
  SELECT e.* FROM entities e
21
102
  JOIN entity_aliases a ON a.entity_id = e.id
22
103
  WHERE e.project_id = ? AND a.alias = ?
23
- `).get(t,e)??null}addRelationship(t,e,i,n,r){const s=this.db.prepare(`
104
+ `).get(projectId, alias) ?? null;
105
+ }
106
+ // ==================== RELATIONSHIPS ====================
107
+ addRelationship(projectId, sourceId, targetId, type, options) {
108
+ const result = this.db.prepare(`
24
109
  INSERT INTO relationships (
25
110
  project_id, source_entity_id, target_entity_id, relationship_type,
26
111
  weight, confidence, source_memory, extraction_method, metadata, created_by
27
112
  ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
28
- `).run(t,e,i,n.trim(),r?.weight??1,r?.confidence??1,r?.sourceMemory??null,r?.extractionMethod??"manual",r?.metadata??null,r?.createdBy??"local");return this.db.prepare("SELECT * FROM relationships WHERE id = ?").get(s.lastInsertRowid)}getRelationships(t,e="both"){switch(e){case"outgoing":return this.db.prepare("SELECT * FROM relationships WHERE source_entity_id = ?").all(t);case"incoming":return this.db.prepare("SELECT * FROM relationships WHERE target_entity_id = ?").all(t);case"both":return this.db.prepare("SELECT * FROM relationships WHERE source_entity_id = ? OR target_entity_id = ?").all(t,t)}}deleteRelationship(t){return this.db.prepare("DELETE FROM relationships WHERE id = ?").run(t).changes>0}getNeighborhood(t,e=2){const i=Math.min(e,5),n=this.db.prepare(`
113
+ `).run(projectId, sourceId, targetId, type.trim(), options?.weight ?? 1.0, options?.confidence ?? 1.0, options?.sourceMemory ?? null, options?.extractionMethod ?? 'manual', options?.metadata ?? null, options?.createdBy ?? 'local');
114
+ return this.db.prepare('SELECT * FROM relationships WHERE id = ?').get(result.lastInsertRowid);
115
+ }
116
+ getRelationships(entityId, direction = 'both') {
117
+ switch (direction) {
118
+ case 'outgoing':
119
+ return this.db.prepare('SELECT * FROM relationships WHERE source_entity_id = ?').all(entityId);
120
+ case 'incoming':
121
+ return this.db.prepare('SELECT * FROM relationships WHERE target_entity_id = ?').all(entityId);
122
+ case 'both':
123
+ return this.db.prepare('SELECT * FROM relationships WHERE source_entity_id = ? OR target_entity_id = ?').all(entityId, entityId);
124
+ }
125
+ }
126
+ deleteRelationship(id) {
127
+ return this.db.prepare('DELETE FROM relationships WHERE id = ?').run(id).changes > 0;
128
+ }
129
+ // ==================== GRAPH QUERIES ====================
130
+ /**
131
+ * Get neighborhood — all nodes within N hops of a given entity.
132
+ * Uses recursive CTE with cycle detection.
133
+ */
134
+ getNeighborhood(entityId, maxDepth = 2) {
135
+ const depth = Math.min(maxDepth, MAX_DEPTH);
136
+ const nodes = this.db.prepare(`
29
137
  WITH RECURSIVE neighbors(id, depth, visited) AS (
30
138
  SELECT ?, 0, ',' || ? || ','
31
139
  UNION ALL
@@ -44,16 +152,31 @@ const p=5,_=200;class l{db;constructor(t){this.db=t}addEntity(t,e,i,n,r){const s
44
152
  GROUP BY e.id
45
153
  ORDER BY depth ASC
46
154
  LIMIT ?
47
- `).all(t,t,i,200),r=n.map(a=>a.id);if(r.length===0)return{nodes:[],edges:[]};const s=r.map(()=>"?").join(","),E=this.db.prepare(`
155
+ `).all(entityId, entityId, depth, MAX_RESULTS);
156
+ // Get edges between all discovered nodes
157
+ const nodeIds = nodes.map(n => n.id);
158
+ if (nodeIds.length === 0)
159
+ return { nodes: [], edges: [] };
160
+ const placeholders = nodeIds.map(() => '?').join(',');
161
+ const edges = this.db.prepare(`
48
162
  SELECT r.source_entity_id as source_id, s.name as source_name,
49
163
  r.target_entity_id as target_id, t.name as target_name,
50
164
  r.relationship_type, r.weight
51
165
  FROM relationships r
52
166
  JOIN entities s ON s.id = r.source_entity_id
53
167
  JOIN entities t ON t.id = r.target_entity_id
54
- WHERE r.source_entity_id IN (${s})
55
- AND r.target_entity_id IN (${s})
56
- `).all(...r,...r);return{nodes:n,edges:E}}findPath(t,e,i=5){const n=Math.min(i,5),r=this.db.prepare(`
168
+ WHERE r.source_entity_id IN (${placeholders})
169
+ AND r.target_entity_id IN (${placeholders})
170
+ `).all(...nodeIds, ...nodeIds);
171
+ return { nodes, edges };
172
+ }
173
+ /**
174
+ * Find path between two entities using BFS with cycle detection.
175
+ * Returns the shortest path as a list of entity IDs, or null if no path exists.
176
+ */
177
+ findPath(sourceId, targetId, maxDepth = 5) {
178
+ const depth = Math.min(maxDepth, MAX_DEPTH);
179
+ const rows = this.db.prepare(`
57
180
  WITH RECURSIVE path_finder(id, path, depth) AS (
58
181
  SELECT ?, CAST(? AS TEXT), 0
59
182
  UNION ALL
@@ -68,4 +191,45 @@ const p=5,_=200;class l{db;constructor(t){this.db=t}addEntity(t,e,i,n,r){const s
68
191
  AND INSTR(',' || pf.path || ',', ',' || CASE WHEN r.source_entity_id = pf.id THEN r.target_entity_id ELSE r.source_entity_id END || ',') = 0
69
192
  )
70
193
  SELECT path FROM path_finder WHERE id = ? LIMIT 1
71
- `).all(t,t,n,e);return r.length===0?null:r[0].path.split(",").map(Number)}mergeEntities(t,e){const i=this.getEntity(t),n=this.getEntity(e);return!i||!n?null:(this.db.transaction(()=>{this.addAlias(e,i.name),this.db.prepare("UPDATE entity_aliases SET entity_id = ? WHERE entity_id = ?").run(e,t),this.db.prepare("UPDATE relationships SET source_entity_id = ? WHERE source_entity_id = ?").run(e,t),this.db.prepare("UPDATE relationships SET target_entity_id = ? WHERE target_entity_id = ?").run(e,t),this.db.prepare("DELETE FROM relationships WHERE source_entity_id = target_entity_id").run(),this.deleteEntity(t)})(),this.getEntity(e))}getStats(t){const e=this.db.prepare("SELECT COUNT(*) as c FROM entities WHERE project_id = ?").get(t).c,i=this.db.prepare("SELECT COUNT(*) as c FROM relationships WHERE project_id = ?").get(t).c,n=this.db.prepare("SELECT type, COUNT(*) as cnt FROM entities WHERE project_id = ? GROUP BY type").all(t),r={};for(const s of n)r[s.type]=s.cnt;return{entities:e,relationships:i,entityTypes:r}}}export{l as KnowledgeGraph};
194
+ `).all(sourceId, sourceId, depth, targetId);
195
+ if (rows.length === 0)
196
+ return null;
197
+ return rows[0].path.split(',').map(Number);
198
+ }
199
+ /**
200
+ * Merge two entities: moves all relationships and aliases from source to target,
201
+ * adds source name as alias on target, then deletes source.
202
+ */
203
+ mergeEntities(sourceId, targetId) {
204
+ const source = this.getEntity(sourceId);
205
+ const target = this.getEntity(targetId);
206
+ if (!source || !target)
207
+ return null;
208
+ const merge = this.db.transaction(() => {
209
+ // Add source name as alias on target
210
+ this.addAlias(targetId, source.name);
211
+ // Move source's aliases to target
212
+ this.db.prepare('UPDATE entity_aliases SET entity_id = ? WHERE entity_id = ?').run(targetId, sourceId);
213
+ // Redirect relationships from source to target
214
+ this.db.prepare('UPDATE relationships SET source_entity_id = ? WHERE source_entity_id = ?').run(targetId, sourceId);
215
+ this.db.prepare('UPDATE relationships SET target_entity_id = ? WHERE target_entity_id = ?').run(targetId, sourceId);
216
+ // Remove self-loops created by merge
217
+ this.db.prepare('DELETE FROM relationships WHERE source_entity_id = target_entity_id').run();
218
+ // Delete source entity
219
+ this.deleteEntity(sourceId);
220
+ });
221
+ merge();
222
+ return this.getEntity(targetId);
223
+ }
224
+ // ==================== STATS ====================
225
+ getStats(projectId) {
226
+ const entities = this.db.prepare('SELECT COUNT(*) as c FROM entities WHERE project_id = ?').get(projectId).c;
227
+ const relationships = this.db.prepare('SELECT COUNT(*) as c FROM relationships WHERE project_id = ?').get(projectId).c;
228
+ const typeRows = this.db.prepare('SELECT type, COUNT(*) as cnt FROM entities WHERE project_id = ? GROUP BY type').all(projectId);
229
+ const entityTypes = {};
230
+ for (const r of typeRows)
231
+ entityTypes[r.type] = r.cnt;
232
+ return { entities, relationships, entityTypes };
233
+ }
234
+ }
235
+ //# sourceMappingURL=knowledge-graph.js.map