@rahmantech/knl-view 0.0.0-stage → 0.2.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/LICENSE +201 -0
- package/README.md +483 -2
- package/bin/knl-view.js +1320 -0
- package/dist/assets/abnfDiagram-WEYOIA3C-hoSbsMOz.js +1 -0
- package/dist/assets/arc-g87Np6E5.js +1 -0
- package/dist/assets/architecture-WOLXFQ4H-DOIq-0IA.js +1 -0
- package/dist/assets/architectureDiagram-ZHVYKT53-Dxwe8n7m.js +36 -0
- package/dist/assets/array-BifhSqXX.js +1 -0
- package/dist/assets/blockDiagram-RJAE7QA3-BnGng59Z.js +140 -0
- package/dist/assets/c4Diagram-XDPPPE54-C5LjqIIo.js +38 -0
- package/dist/assets/channel-BVWkRFBw.js +1 -0
- package/dist/assets/chunk-2E7WHVMD-BJ1dyGLq.js +88 -0
- package/dist/assets/chunk-2Q5K7J3B-C1jixKkw.js +1 -0
- package/dist/assets/chunk-4EMNKR2T-DXF02Sgw.js +1 -0
- package/dist/assets/chunk-55XCU76K-CM1akAbu.js +1 -0
- package/dist/assets/chunk-5VM5RSS4-ZNzvKenW.js +15 -0
- package/dist/assets/chunk-7S6XSTFO-BOUjqldL.js +2 -0
- package/dist/assets/chunk-AQDTSKZB-C4isdNLA.js +72 -0
- package/dist/assets/chunk-DSNLSHJZ-CbOTkSVM.js +129 -0
- package/dist/assets/chunk-DUW6YSOI-Dgx8z5s3.js +1 -0
- package/dist/assets/chunk-HAGFOTED-CRfnketR.js +62 -0
- package/dist/assets/chunk-HNE4Q36P-rxlpGUU8.js +1 -0
- package/dist/assets/chunk-J5ZVWO5B-oDCd-gWO.js +1 -0
- package/dist/assets/chunk-JWPE2WC7-DVXcaiue.js +1 -0
- package/dist/assets/chunk-NGNAAXSQ-C2Wy9Xmz.js +136 -0
- package/dist/assets/chunk-NQOKRVIF-BOYCgTmV.js +213 -0
- package/dist/assets/chunk-UHW7PFBP-Cnogp1Fb.js +1 -0
- package/dist/assets/chunk-VQ5W62QI-Cu1WY5Uo.js +1 -0
- package/dist/assets/chunk-WQQL4CDK-BJc3FPE2.js +10 -0
- package/dist/assets/chunk-XXDRQBXY-BlPj4H7s.js +1 -0
- package/dist/assets/chunk-Y2CYZVJY-DsF7k-Jl.js +1 -0
- package/dist/assets/chunk-YZO67HFG-V85H31x4.js +1 -0
- package/dist/assets/chunk-ZAUX2LTX-C3XKXW09.js +1 -0
- package/dist/assets/chunk-ZCHMQSOR-B0mmpEgW.js +2 -0
- package/dist/assets/classDiagram-v2-4MYWR3UA-oMd7YaHP.js +217 -0
- package/dist/assets/cose-bilkent-JH36ORCC-J0kSVsDf.js +1 -0
- package/dist/assets/cynefin-EF2NZ3EQ-DK2TNhel.js +1 -0
- package/dist/assets/cynefinDiagram-WF24UDRP-DDvKgieA.js +62 -0
- package/dist/assets/cytoscape.esm-BegBNzOV.js +321 -0
- package/dist/assets/dagre-NK4GIZVX-Ca2Fe-i-.js +4 -0
- package/dist/assets/defaultLocale-BFoDCU3G.js +1 -0
- package/dist/assets/diagram-CBK3MWTC-L6dmeQ2V.js +24 -0
- package/dist/assets/diagram-I5XIIV2D-DhG8R-v5.js +24 -0
- package/dist/assets/diagram-ODIL2UCW-BpYYbYBT.js +30 -0
- package/dist/assets/diagram-PFBEJUXE-DKQpHSsC.js +3 -0
- package/dist/assets/diagram-RJA3I4VV-DP7Dsnya.js +41 -0
- package/dist/assets/diagram-TYUNFSNR-CfSFewUS.js +200 -0
- package/dist/assets/dist-CQNjnrld.js +1 -0
- package/dist/assets/ebnfDiagram-V4P3TTPT-DE3MZoJO.js +1 -0
- package/dist/assets/elk-QONTBCTQ-p_aAQPHU.js +27 -0
- package/dist/assets/erDiagram-KYQIGQAA-DZO3vQrk.js +99 -0
- package/dist/assets/eventmodeling-K75KTNOO-BII9EPl5.js +1 -0
- package/dist/assets/flowDiagram-IUGLLWBU-D9uPno39.js +1 -0
- package/dist/assets/ganttDiagram-R7GPNTJY-BKiH3v2r.js +292 -0
- package/dist/assets/gitGraph-VSP46ZUC-oHL6cr73.js +1 -0
- package/dist/assets/gitGraphDiagram-PDYL7XHY-CqPVIEeo.js +106 -0
- package/dist/assets/graphlib-DS17s2tU.js +1 -0
- package/dist/assets/index-CSpirV16.js +76 -0
- package/dist/assets/index-DLgMsUs2.css +1 -0
- package/dist/assets/info-OHQRW6UA-Drrbnhpd.js +1 -0
- package/dist/assets/infoDiagram-MVDRKAX5-DnQQFMAd.js +2 -0
- package/dist/assets/init-C-OQMol4.js +1 -0
- package/dist/assets/ishikawaDiagram-HPEHV2GE-DIpQlxRh.js +70 -0
- package/dist/assets/journeyDiagram-BOSHW7O7-EBR9I5h1.js +139 -0
- package/dist/assets/kanban-definition-FNSA4HC5-CdvNsjik.js +89 -0
- package/dist/assets/katex-B_JcIwqj.js +257 -0
- package/dist/assets/line-CDLzgxDc.js +1 -0
- package/dist/assets/linear-DfXKql9u.js +1 -0
- package/dist/assets/mermaid-parser.core-DnXGphT2.js +7 -0
- package/dist/assets/mermaid.core-h6fiCaSZ.js +44 -0
- package/dist/assets/mindmap-definition-6OY57UG3-BiNfio-j.js +98 -0
- package/dist/assets/ordinal-BDEzSJ7C.js +1 -0
- package/dist/assets/packet-JDAUHWVQ-D76byv1S.js +1 -0
- package/dist/assets/path-fybaL0A-.js +1 -0
- package/dist/assets/pegDiagram-ECIUJNBX-Drs8Pxdt.js +1 -0
- package/dist/assets/pie-XZMESJXO-D_83z1lM.js +1 -0
- package/dist/assets/pieDiagram-GNMPY74T-wvzIphSf.js +39 -0
- package/dist/assets/quadrantDiagram-D57K4MC3-B7EfMRWb.js +7 -0
- package/dist/assets/radar-ABXABTNO-Bs6dVdAR.js +1 -0
- package/dist/assets/railroad-I3PHUGI6-lKaN4miw.js +1 -0
- package/dist/assets/railroad-abnf-LNEFI6M7-Bhh16b7Z.js +1 -0
- package/dist/assets/railroad-ebnf-DWYD2UWJ-m6ybW-Y_.js +1 -0
- package/dist/assets/railroad-peg-JR7BVNR7-DNn72e7r.js +1 -0
- package/dist/assets/railroadDiagram-Y72NRBL7-DNeop1iZ.js +1 -0
- package/dist/assets/requirementDiagram-EQYSW5RA-BVYZ2XYJ.js +84 -0
- package/dist/assets/rough.esm-Dy-Kn_BL.js +1 -0
- package/dist/assets/sankeyDiagram-SY24JHO5-B2MP51on.js +40 -0
- package/dist/assets/sequenceDiagram-BLEHNHQU-BUvxstXG.js +169 -0
- package/dist/assets/sizeCapture-INFHLROL-3uHXrFqA.js +1 -0
- package/dist/assets/src-BrR0lqlG.js +1 -0
- package/dist/assets/stateDiagram-v2-A6IESKWZ-Ba84OGKE.js +291 -0
- package/dist/assets/swimlanes-B3ENRKFQ-BMSl2PNA.js +1 -0
- package/dist/assets/swimlanesDiagram-AYQRV4AK-M1dmvq7m.js +8 -0
- package/dist/assets/timeline-definition-JXYKGLGB-DOdW-dxq.js +120 -0
- package/dist/assets/treeView-D4JQ5SDB-DZ9KQyn5.js +1 -0
- package/dist/assets/treemap-SAJKECNS-WEC5wtDV.js +1 -0
- package/dist/assets/usecaseDiagram-WPOLBNNX-cldTztFL.js +328 -0
- package/dist/assets/vennDiagram-Q3Y3IDCW-c41KySoY.js +34 -0
- package/dist/assets/wardley-7MLQ67FV-Bf9_aSSZ.js +1 -0
- package/dist/assets/wardleyDiagram-IVAIDKWA-BeKEvEBG.js +78 -0
- package/dist/assets/xychartDiagram-4URNHDU4-tNkAxTxG.js +7 -0
- package/dist/index.html +22 -0
- package/package.json +50 -4
- package/server/config.js +389 -0
- package/server/consensus.js +180 -0
- package/server/graph/anchordb_client.js +253 -0
- package/server/graph/model.js +157 -0
- package/server/graph/parser.js +327 -0
- package/server/graph/sources.js +801 -0
- package/server/graph/syntax.js +437 -0
- package/server/index.js +742 -0
- package/server/install.js +437 -0
- package/server/map.js +126 -0
- package/server/models_registry.js +109 -0
- package/server/scaffold.js +466 -0
|
@@ -0,0 +1,253 @@
|
|
|
1
|
+
import { connectWire } from '@rahmantech/anchorwire'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Optional bridge module to the AnchorDB backend via the AnchorWire protocol
|
|
5
|
+
* (wire://). Follows the KISS principle: active only when configured, with
|
|
6
|
+
* graceful fallback.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Open an AnchorWire client connection.
|
|
11
|
+
* @param {string|object} urlOrOptions - Example: 'wire://127.0.0.1:7448?model=knowledges'
|
|
12
|
+
*/
|
|
13
|
+
export async function createAnchorWireClient(urlOrOptions) {
|
|
14
|
+
try {
|
|
15
|
+
const client = await connectWire(urlOrOptions)
|
|
16
|
+
return client
|
|
17
|
+
} catch (err) {
|
|
18
|
+
const target = typeof urlOrOptions === 'string'
|
|
19
|
+
? urlOrOptions
|
|
20
|
+
: `${urlOrOptions?.host || '?'}:${urlOrOptions?.port || '?'}`
|
|
21
|
+
throw new Error(`Failed to connect to AnchorDB (${target}): ${err.message}`)
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Stable key for one edge, for diffing and the mirror: "from\x00type\x00to".
|
|
27
|
+
* NUL is used because it cannot appear in healthy markdown IDs.
|
|
28
|
+
*/
|
|
29
|
+
export function edgeKey(from, type, to) {
|
|
30
|
+
return `${from}\u0000${type}\u0000${to}`
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Collect the set of unique edges from a DirectedGraph. Duplicate edges in
|
|
35
|
+
* markdown (one decision writing supersedes twice, say) only need to be sent
|
|
36
|
+
* to the server once.
|
|
37
|
+
* @param {import('./model.js').DirectedGraph} graph
|
|
38
|
+
* @returns {string[]} sorted edge keys
|
|
39
|
+
*/
|
|
40
|
+
export function edgesOfGraph(graph) {
|
|
41
|
+
const set = new Set()
|
|
42
|
+
for (const edge of graph?.edges ?? []) {
|
|
43
|
+
if (edge?.from && edge?.type && edge?.to) set.add(edgeKey(edge.from, edge.type, edge.to))
|
|
44
|
+
}
|
|
45
|
+
return Array.from(set).sort()
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Diff two edge sets: what must be sent as link and unlink.
|
|
50
|
+
* @param {string[]} desired - edges that should exist (the current local graph)
|
|
51
|
+
* @param {string[]|null} previous - edges per the last sync mirror;
|
|
52
|
+
* null means no mirror (the server is assumed empty → full link)
|
|
53
|
+
* @returns {{toLink: string[], toUnlink: string[]}}
|
|
54
|
+
*/
|
|
55
|
+
export function diffEdges(desired, previous) {
|
|
56
|
+
const now = new Set(desired)
|
|
57
|
+
if (!previous) return { toLink: [...now], toUnlink: [] }
|
|
58
|
+
const before = new Set(previous)
|
|
59
|
+
return {
|
|
60
|
+
toLink: desired.filter((k) => !before.has(k)),
|
|
61
|
+
toUnlink: previous.filter((k) => !now.has(k))
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function isEdgeNotFound(err) {
|
|
66
|
+
return /edge not found/i.test(err?.message || '')
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Cheap check that the server still recognizes one sample edge from the
|
|
71
|
+
* mirror. If the sample edge does not show up in causalChain, the server is
|
|
72
|
+
* considered different (reinstalled, wiped, model reset) and the mirror must
|
|
73
|
+
* be discarded so the sync becomes a full link. Link is an upsert, so a full
|
|
74
|
+
* link is always safe; what is dangerous is trusting a mirror against an
|
|
75
|
+
* empty server.
|
|
76
|
+
*/
|
|
77
|
+
export async function mirrorStillValid(client, previousEdges) {
|
|
78
|
+
if (!Array.isArray(previousEdges) || previousEdges.length === 0) return Boolean(previousEdges)
|
|
79
|
+
const [from, type, to] = previousEdges[0].split('\u0000')
|
|
80
|
+
try {
|
|
81
|
+
const results = await client.causalChain([from], type, 5)
|
|
82
|
+
return results.some((item) => item?.word === to)
|
|
83
|
+
} catch {
|
|
84
|
+
return false
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Sync the graph to AnchorDB INCREMENTALLY: only new edges are linked, edges
|
|
90
|
+
* missing from markdown are unlinked, the rest are left alone. The server
|
|
91
|
+
* offers no edge enumeration, so the comparator is the mirror of the last
|
|
92
|
+
* sync (`previousEdges` from the local sync-state), and the mirror is
|
|
93
|
+
* verified with one causal probe before being trusted.
|
|
94
|
+
*
|
|
95
|
+
* Unlinking an edge that no longer exists on the server is treated as
|
|
96
|
+
* success (case: the container/server was reinstalled but the mirror remains).
|
|
97
|
+
*
|
|
98
|
+
* @param {import('@rahmantech/anchorwire').AnchorWireClient} client
|
|
99
|
+
* @param {import('./model.js').DirectedGraph} graph
|
|
100
|
+
* @param {string[]|null} previousEdges - mirror of the last sync (null = full sync)
|
|
101
|
+
* @returns {Promise<{linked: number, unlinked: number, kept: number, failed: number, edges: string[]}>}
|
|
102
|
+
* `edges` = the full edge set after the sync, to be written to the mirror
|
|
103
|
+
* ONLY when `failed === 0`. On partial failure the mirror is not written so
|
|
104
|
+
* the next start retries the failed part.
|
|
105
|
+
*/
|
|
106
|
+
export async function syncGraphToAnchorDB(client, graph, previousEdges = null) {
|
|
107
|
+
if (!client || !graph) return { linked: 0, unlinked: 0, kept: 0, failed: 0, edges: [] }
|
|
108
|
+
|
|
109
|
+
const edges = edgesOfGraph(graph)
|
|
110
|
+
if (previousEdges && !(await mirrorStillValid(client, previousEdges))) {
|
|
111
|
+
previousEdges = null
|
|
112
|
+
}
|
|
113
|
+
const { toLink, toUnlink } = diffEdges(edges, previousEdges)
|
|
114
|
+
let linked = 0
|
|
115
|
+
let unlinked = 0
|
|
116
|
+
let failed = 0
|
|
117
|
+
|
|
118
|
+
for (const key of toLink) {
|
|
119
|
+
const [from, type, to] = key.split('\u0000')
|
|
120
|
+
try {
|
|
121
|
+
await client.link(from, type, to, 1.0)
|
|
122
|
+
linked += 1
|
|
123
|
+
} catch {
|
|
124
|
+
failed += 1
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
for (const key of toUnlink) {
|
|
129
|
+
const [from, type, to] = key.split('\u0000')
|
|
130
|
+
try {
|
|
131
|
+
await client.unlink(from, type, to)
|
|
132
|
+
unlinked += 1
|
|
133
|
+
} catch (err) {
|
|
134
|
+
// The edge no longer exists on the server: the end state equals what
|
|
135
|
+
// we want, so it counts as success.
|
|
136
|
+
if (isEdgeNotFound(err)) unlinked += 1
|
|
137
|
+
else failed += 1
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
return { linked, unlinked, kept: edges.length - linked, failed, edges }
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* Distributed multi-hop causal chain trace on the AnchorDB server.
|
|
146
|
+
*
|
|
147
|
+
* The AnchorWire client uses a relations array as an ORDERED CHAIN of hops
|
|
148
|
+
* (the second hop goes through the second relation), not "any one of". For
|
|
149
|
+
* the knowledge graphs we want to trace a UNION of relations (DEC
|
|
150
|
+
* --supersedes--> X or --affects_knowledge--> Y), each relation is traced
|
|
151
|
+
* separately per hop and then merged: dedupe per node, highest score wins,
|
|
152
|
+
* and each next hop's score is multiplied by its parent's.
|
|
153
|
+
*
|
|
154
|
+
* @param {import('@rahmantech/anchorwire').AnchorWireClient} client
|
|
155
|
+
* @param {string} seedId - id of the center node of the query
|
|
156
|
+
* @param {string|string[]} relations - relations to follow (e.g. ['supersedes', 'affects_knowledge', 'depends_on'])
|
|
157
|
+
* @param {number} limitK - result limit per server call
|
|
158
|
+
* @param {number} maxHops - trace hop radius
|
|
159
|
+
*/
|
|
160
|
+
export async function traceCausalChain(client, seedId, relations = ['supersedes', 'affects_knowledge', 'depends_on'], limitK = 20, maxHops = 2) {
|
|
161
|
+
if (!client) throw new Error('AnchorWire client unavailable.')
|
|
162
|
+
const rels = (Array.isArray(relations) ? relations : [relations]).filter(Boolean)
|
|
163
|
+
if (!rels.length) return []
|
|
164
|
+
|
|
165
|
+
const best = new Map() // word -> { word, score }
|
|
166
|
+
let frontier = [{ word: seedId, score: 1 }]
|
|
167
|
+
|
|
168
|
+
for (let hop = 1; hop <= Math.max(1, maxHops) && frontier.length; hop++) {
|
|
169
|
+
const nextFrontier = []
|
|
170
|
+
for (const { word, score } of frontier) {
|
|
171
|
+
const perRelation = await Promise.all(
|
|
172
|
+
rels.map((rel) => client.causalChain([word], rel, limitK).catch(() => []))
|
|
173
|
+
)
|
|
174
|
+
for (const results of perRelation) {
|
|
175
|
+
for (const item of results) {
|
|
176
|
+
if (!item?.word || item.word === seedId) continue
|
|
177
|
+
const reached = { word: item.word, score: score * (item.score || 1) }
|
|
178
|
+
const prev = best.get(reached.word)
|
|
179
|
+
if (!prev || reached.score > prev.score) {
|
|
180
|
+
best.set(reached.word, reached)
|
|
181
|
+
nextFrontier.push(reached)
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
frontier = nextFrontier
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
return Array.from(best.values()).sort((a, b) => b.score - a.score)
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/**
|
|
193
|
+
* Reverse traversal over the local sync mirror: the inbound half of
|
|
194
|
+
* two-way lineage. The wire protocol has no "who points at X" query, but
|
|
195
|
+
* the mirror holds every edge this project ever linked, which is exactly
|
|
196
|
+
* the inbound map for this project's model. Entries carry score 1: the
|
|
197
|
+
* server's relevance scoring only exists for forward chains.
|
|
198
|
+
*
|
|
199
|
+
* @param {string[]} mirrorEdges - edge keys "from\x00type\x00to" from the sync mirror
|
|
200
|
+
* @param {string} seedId - id of the node to trace inbound from
|
|
201
|
+
* @param {string[]} relations - relation types to follow
|
|
202
|
+
* @param {number} maxHops - traversal radius
|
|
203
|
+
* @returns {Array<{word: string, score: number}>}
|
|
204
|
+
*/
|
|
205
|
+
export function traceInboundFromMirror(mirrorEdges, seedId, relations, maxHops = 2) {
|
|
206
|
+
if (!Array.isArray(mirrorEdges) || !mirrorEdges.length || !seedId) return []
|
|
207
|
+
const wanted = new Set(relations.filter(Boolean))
|
|
208
|
+
|
|
209
|
+
// Reverse adjacency: target node -> list of { source, type }.
|
|
210
|
+
const inbound = new Map()
|
|
211
|
+
for (const key of mirrorEdges) {
|
|
212
|
+
const [from, type, to] = key.split('\u0000')
|
|
213
|
+
if (!from || !to) continue
|
|
214
|
+
if (wanted.size && !wanted.has(type)) continue
|
|
215
|
+
if (!inbound.has(to)) inbound.set(to, [])
|
|
216
|
+
inbound.get(to).push(from)
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
const best = new Map() // word -> depth reached (BFS, first reach wins)
|
|
220
|
+
let frontier = [seedId]
|
|
221
|
+
best.set(seedId, 0)
|
|
222
|
+
|
|
223
|
+
for (let hop = 1; hop <= Math.max(1, maxHops) && frontier.length; hop++) {
|
|
224
|
+
const nextFrontier = []
|
|
225
|
+
for (const word of frontier) {
|
|
226
|
+
for (const source of inbound.get(word) || []) {
|
|
227
|
+
if (source === seedId || best.has(source)) continue
|
|
228
|
+
best.set(source, hop)
|
|
229
|
+
nextFrontier.push(source)
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
frontier = nextFrontier
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
best.delete(seedId)
|
|
236
|
+
return Array.from(best.keys()).map((word) => ({ word, score: 1 }))
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
/**
|
|
240
|
+
* Merge forward (server-scored) and inbound (mirror) results into one
|
|
241
|
+
* ranked list. A word reached both ways keeps the higher, meaningful score.
|
|
242
|
+
*/
|
|
243
|
+
export function mergeLineage(forward, inbound) {
|
|
244
|
+
const merged = new Map()
|
|
245
|
+
for (const { word, score } of forward) {
|
|
246
|
+
merged.set(word, { word, score })
|
|
247
|
+
}
|
|
248
|
+
for (const { word, score } of inbound) {
|
|
249
|
+
const prev = merged.get(word)
|
|
250
|
+
if (!prev || score > prev.score) merged.set(word, { word, score })
|
|
251
|
+
}
|
|
252
|
+
return Array.from(merged.values()).sort((a, b) => b.score - a.score)
|
|
253
|
+
}
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
// Directed graph (DAG) data model for knowledges-view: knowledge nodes and
|
|
2
|
+
// decision nodes.
|
|
3
|
+
|
|
4
|
+
export class GraphNode {
|
|
5
|
+
constructor({ id, title, type = 'knowledge', path = '', metadata = {} }) {
|
|
6
|
+
this.id = id
|
|
7
|
+
this.title = title || id
|
|
8
|
+
this.type = type // 'knowledge' | 'decision'
|
|
9
|
+
this.path = path
|
|
10
|
+
this.metadata = metadata
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
toJSON() {
|
|
14
|
+
return {
|
|
15
|
+
id: this.id,
|
|
16
|
+
title: this.title,
|
|
17
|
+
type: this.type,
|
|
18
|
+
path: this.path,
|
|
19
|
+
metadata: this.metadata
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export class GraphEdge {
|
|
25
|
+
constructor({ from, to, type, metadata = {} }) {
|
|
26
|
+
this.from = from
|
|
27
|
+
this.to = to
|
|
28
|
+
this.type = type // 'depends_on' | 'references' | 'supersedes' | 'related_to' | 'affects_knowledge'
|
|
29
|
+
this.metadata = metadata
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
toJSON() {
|
|
33
|
+
return {
|
|
34
|
+
from: this.from,
|
|
35
|
+
to: this.to,
|
|
36
|
+
type: this.type,
|
|
37
|
+
metadata: this.metadata
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export class DirectedGraph {
|
|
43
|
+
constructor() {
|
|
44
|
+
this.nodes = new Map()
|
|
45
|
+
this.edges = []
|
|
46
|
+
this.outgoing = new Map() // nodeId -> Array of edges
|
|
47
|
+
this.incoming = new Map() // nodeId -> Array of edges
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
addNode(node) {
|
|
51
|
+
if (!node || !node.id) return
|
|
52
|
+
this.nodes.set(node.id, node)
|
|
53
|
+
if (!this.outgoing.has(node.id)) this.outgoing.set(node.id, [])
|
|
54
|
+
if (!this.incoming.has(node.id)) this.incoming.set(node.id, [])
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
addEdge(edge) {
|
|
58
|
+
if (!edge || !edge.from || !edge.to) return
|
|
59
|
+
// Make sure the nodes exist in the outgoing/incoming maps
|
|
60
|
+
if (!this.outgoing.has(edge.from)) this.outgoing.set(edge.from, [])
|
|
61
|
+
if (!this.incoming.has(edge.to)) this.incoming.set(edge.to, [])
|
|
62
|
+
|
|
63
|
+
this.edges.push(edge)
|
|
64
|
+
this.outgoing.get(edge.from).push(edge)
|
|
65
|
+
this.incoming.get(edge.to).push(edge)
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
getNode(id) {
|
|
69
|
+
return this.nodes.get(id) || null
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
getOutgoingEdges(id) {
|
|
73
|
+
return this.outgoing.get(id) || []
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
getIncomingEdges(id) {
|
|
77
|
+
return this.incoming.get(id) || []
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// Radius-based N-hop sub-graph extraction (cheap enough to feed to an LLM).
|
|
81
|
+
//
|
|
82
|
+
// `direction` selects which edges the BFS may walk, mechanically relative
|
|
83
|
+
// to the edges themselves, never interpreted as cause and effect:
|
|
84
|
+
// 'out': edges leaving the target (what it supersedes / depends_on / affects)
|
|
85
|
+
// 'in': edges entering the target (what references or replaces it)
|
|
86
|
+
// 'both': the default; the full neighborhood as before
|
|
87
|
+
// Each returned node carries `via`: how it connects to the center
|
|
88
|
+
// ('center', 'in', 'out', or 'in+out' when reachable both ways).
|
|
89
|
+
extractSubGraph(startId, maxHops = 2, direction = 'both') {
|
|
90
|
+
const walkOut = direction !== 'in'
|
|
91
|
+
const walkIn = direction !== 'out'
|
|
92
|
+
const reachedVia = new Map() // id -> 'in' | 'out' | 'in+out' | 'center'
|
|
93
|
+
const setVia = (id, via) => {
|
|
94
|
+
const prev = reachedVia.get(id)
|
|
95
|
+
if (prev === undefined) reachedVia.set(id, via)
|
|
96
|
+
else if (prev !== via && prev !== 'center' && via !== 'center') reachedVia.set(id, 'in+out')
|
|
97
|
+
}
|
|
98
|
+
setVia(startId, 'center')
|
|
99
|
+
const collectedEdges = []
|
|
100
|
+
const seenEdges = new Set()
|
|
101
|
+
const queue = [{ id: startId, hop: 0 }]
|
|
102
|
+
|
|
103
|
+
while (queue.length > 0) {
|
|
104
|
+
const { id, hop } = queue.shift()
|
|
105
|
+
if (hop >= maxHops) continue
|
|
106
|
+
|
|
107
|
+
if (walkOut) {
|
|
108
|
+
for (const edge of this.getOutgoingEdges(id)) {
|
|
109
|
+
const edgeKey = `${edge.from}->${edge.to}:${edge.type}`
|
|
110
|
+
if (!seenEdges.has(edgeKey)) {
|
|
111
|
+
seenEdges.add(edgeKey)
|
|
112
|
+
collectedEdges.push(edge)
|
|
113
|
+
}
|
|
114
|
+
if (!reachedVia.has(edge.to)) {
|
|
115
|
+
queue.push({ id: edge.to, hop: hop + 1 })
|
|
116
|
+
}
|
|
117
|
+
setVia(edge.to, 'out')
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
if (walkIn) {
|
|
122
|
+
for (const edge of this.getIncomingEdges(id)) {
|
|
123
|
+
const edgeKey = `${edge.from}->${edge.to}:${edge.type}`
|
|
124
|
+
if (!seenEdges.has(edgeKey)) {
|
|
125
|
+
seenEdges.add(edgeKey)
|
|
126
|
+
collectedEdges.push(edge)
|
|
127
|
+
}
|
|
128
|
+
if (!reachedVia.has(edge.from)) {
|
|
129
|
+
queue.push({ id: edge.from, hop: hop + 1 })
|
|
130
|
+
}
|
|
131
|
+
setVia(edge.from, 'in')
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
const subNodes = []
|
|
137
|
+
for (const [id, via] of reachedVia) {
|
|
138
|
+
const node = this.getNode(id)
|
|
139
|
+
if (node) subNodes.push({ ...node.toJSON(), via })
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
return {
|
|
143
|
+
center: startId,
|
|
144
|
+
maxHops,
|
|
145
|
+
direction,
|
|
146
|
+
nodes: subNodes,
|
|
147
|
+
edges: collectedEdges.map(e => e.toJSON())
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
toJSON() {
|
|
152
|
+
return {
|
|
153
|
+
nodes: Array.from(this.nodes.values()).map(n => n.toJSON()),
|
|
154
|
+
edges: this.edges.map(e => e.toJSON())
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
}
|