@rahmantech/knl-view 0.1.0 → 0.3.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/README.md +133 -32
- package/bin/knl-view.js +591 -58
- package/dist/assets/{abnfDiagram-WEYOIA3C-B1gaBaSI.js → abnfDiagram-WEYOIA3C-hoSbsMOz.js} +1 -1
- package/dist/assets/{arc-NZ9uRaCl.js → arc-g87Np6E5.js} +1 -1
- package/dist/assets/{architecture-WOLXFQ4H-8MalLwTd.js → architecture-WOLXFQ4H-DOIq-0IA.js} +1 -1
- package/dist/assets/{architectureDiagram-ZHVYKT53-eZ8i5cl4.js → architectureDiagram-ZHVYKT53-Dxwe8n7m.js} +1 -1
- package/dist/assets/{blockDiagram-RJAE7QA3-BavOE8BM.js → blockDiagram-RJAE7QA3-BnGng59Z.js} +1 -1
- package/dist/assets/{c4Diagram-XDPPPE54-DqMaTAzx.js → c4Diagram-XDPPPE54-C5LjqIIo.js} +1 -1
- package/dist/assets/channel-BVWkRFBw.js +1 -0
- package/dist/assets/{chunk-2E7WHVMD-B1Hj2BjJ.js → chunk-2E7WHVMD-BJ1dyGLq.js} +1 -1
- package/dist/assets/{chunk-4EMNKR2T-HDscrCiq.js → chunk-4EMNKR2T-DXF02Sgw.js} +1 -1
- package/dist/assets/{chunk-55XCU76K-QR_ARj8I.js → chunk-55XCU76K-CM1akAbu.js} +1 -1
- package/dist/assets/{chunk-7S6XSTFO-B0p3o547.js → chunk-7S6XSTFO-BOUjqldL.js} +1 -1
- package/dist/assets/{chunk-AQDTSKZB-CCxwNyyA.js → chunk-AQDTSKZB-C4isdNLA.js} +1 -1
- package/dist/assets/{chunk-DSNLSHJZ-BM5PBFmz.js → chunk-DSNLSHJZ-CbOTkSVM.js} +1 -1
- package/dist/assets/{chunk-HAGFOTED-UCDVFuN_.js → chunk-HAGFOTED-CRfnketR.js} +1 -1
- package/dist/assets/{chunk-HNE4Q36P-C_1sakp2.js → chunk-HNE4Q36P-rxlpGUU8.js} +1 -1
- package/dist/assets/{chunk-NQOKRVIF-b2D1Z4oK.js → chunk-NQOKRVIF-BOYCgTmV.js} +1 -1
- package/dist/assets/{chunk-UHW7PFBP-CJs0VwBY.js → chunk-UHW7PFBP-Cnogp1Fb.js} +1 -1
- package/dist/assets/{chunk-VQ5W62QI-DQxjgE4_.js → chunk-VQ5W62QI-Cu1WY5Uo.js} +1 -1
- package/dist/assets/{chunk-WQQL4CDK-DcjPeAfQ.js → chunk-WQQL4CDK-BJc3FPE2.js} +1 -1
- package/dist/assets/{chunk-XXDRQBXY-glA0kB5s.js → chunk-XXDRQBXY-BlPj4H7s.js} +1 -1
- package/dist/assets/{chunk-YZO67HFG-DJqACL5V.js → chunk-YZO67HFG-V85H31x4.js} +1 -1
- package/dist/assets/{chunk-ZAUX2LTX-CWfaYtwb.js → chunk-ZAUX2LTX-C3XKXW09.js} +1 -1
- package/dist/assets/{chunk-ZCHMQSOR-BGX73Hho.js → chunk-ZCHMQSOR-B0mmpEgW.js} +1 -1
- package/dist/assets/{classDiagram-v2-4MYWR3UA-BpYXPz-e.js → classDiagram-v2-4MYWR3UA-oMd7YaHP.js} +1 -1
- package/dist/assets/{cose-bilkent-JH36ORCC-e1quKm0Q.js → cose-bilkent-JH36ORCC-J0kSVsDf.js} +1 -1
- package/dist/assets/{cynefin-EF2NZ3EQ-hb3oUmFO.js → cynefin-EF2NZ3EQ-DK2TNhel.js} +1 -1
- package/dist/assets/{cynefinDiagram-WF24UDRP-CnMQvLqi.js → cynefinDiagram-WF24UDRP-DDvKgieA.js} +1 -1
- package/dist/assets/{dagre-NK4GIZVX-BH_Y8y3F.js → dagre-NK4GIZVX-Ca2Fe-i-.js} +1 -1
- package/dist/assets/{diagram-CBK3MWTC-Dh79VetZ.js → diagram-CBK3MWTC-L6dmeQ2V.js} +1 -1
- package/dist/assets/{diagram-I5XIIV2D-D3u-o1RV.js → diagram-I5XIIV2D-DhG8R-v5.js} +1 -1
- package/dist/assets/{diagram-ODIL2UCW-Bfuo-OKS.js → diagram-ODIL2UCW-BpYYbYBT.js} +1 -1
- package/dist/assets/{diagram-PFBEJUXE-CwHasIbs.js → diagram-PFBEJUXE-DKQpHSsC.js} +1 -1
- package/dist/assets/{diagram-RJA3I4VV-D8ttmuE8.js → diagram-RJA3I4VV-DP7Dsnya.js} +1 -1
- package/dist/assets/{diagram-TYUNFSNR-CzC-8RZ0.js → diagram-TYUNFSNR-CfSFewUS.js} +1 -1
- package/dist/assets/{dist-CUJYGnnI.js → dist-CQNjnrld.js} +1 -1
- package/dist/assets/{ebnfDiagram-V4P3TTPT-9AAhU7In.js → ebnfDiagram-V4P3TTPT-DE3MZoJO.js} +1 -1
- package/dist/assets/{elk-QONTBCTQ-BgvnD1v_.js → elk-QONTBCTQ-p_aAQPHU.js} +1 -1
- package/dist/assets/{erDiagram-KYQIGQAA-RLtbiLlP.js → erDiagram-KYQIGQAA-DZO3vQrk.js} +1 -1
- package/dist/assets/{eventmodeling-K75KTNOO-DVY22vNZ.js → eventmodeling-K75KTNOO-BII9EPl5.js} +1 -1
- package/dist/assets/flowDiagram-IUGLLWBU-D9uPno39.js +1 -0
- package/dist/assets/{ganttDiagram-R7GPNTJY-hHiBGa9W.js → ganttDiagram-R7GPNTJY-BKiH3v2r.js} +1 -1
- package/dist/assets/{gitGraph-VSP46ZUC-Cvxay1wh.js → gitGraph-VSP46ZUC-oHL6cr73.js} +1 -1
- package/dist/assets/{gitGraphDiagram-PDYL7XHY-AOhlpSqg.js → gitGraphDiagram-PDYL7XHY-CqPVIEeo.js} +1 -1
- package/dist/assets/{index-CBLNQvK3.js → index-CSpirV16.js} +3 -3
- package/dist/assets/{info-OHQRW6UA-CJ8EUO1m.js → info-OHQRW6UA-Drrbnhpd.js} +1 -1
- package/dist/assets/{infoDiagram-MVDRKAX5-DOQu7oYI.js → infoDiagram-MVDRKAX5-DnQQFMAd.js} +1 -1
- package/dist/assets/{ishikawaDiagram-HPEHV2GE-DcVMvGmx.js → ishikawaDiagram-HPEHV2GE-DIpQlxRh.js} +1 -1
- package/dist/assets/{journeyDiagram-BOSHW7O7-luMeqEqz.js → journeyDiagram-BOSHW7O7-EBR9I5h1.js} +1 -1
- package/dist/assets/{kanban-definition-FNSA4HC5-KTZbmrW2.js → kanban-definition-FNSA4HC5-CdvNsjik.js} +1 -1
- package/dist/assets/{line-CmPHiOVm.js → line-CDLzgxDc.js} +1 -1
- package/dist/assets/{linear-DHYlKXBA.js → linear-DfXKql9u.js} +1 -1
- package/dist/assets/{mermaid-parser.core-BRDI-WhC.js → mermaid-parser.core-DnXGphT2.js} +3 -3
- package/dist/assets/{mermaid.core-DPTRaYLy.js → mermaid.core-h6fiCaSZ.js} +4 -4
- package/dist/assets/{mindmap-definition-6OY57UG3-70EN5Kw_.js → mindmap-definition-6OY57UG3-BiNfio-j.js} +1 -1
- package/dist/assets/{packet-JDAUHWVQ-CXWz83oS.js → packet-JDAUHWVQ-D76byv1S.js} +1 -1
- package/dist/assets/{pegDiagram-ECIUJNBX-z5UXKmp4.js → pegDiagram-ECIUJNBX-Drs8Pxdt.js} +1 -1
- package/dist/assets/{pie-XZMESJXO-rWI1HIQ7.js → pie-XZMESJXO-D_83z1lM.js} +1 -1
- package/dist/assets/{pieDiagram-GNMPY74T-Bh4Kv3NQ.js → pieDiagram-GNMPY74T-wvzIphSf.js} +1 -1
- package/dist/assets/{quadrantDiagram-D57K4MC3-wGM4Ckig.js → quadrantDiagram-D57K4MC3-B7EfMRWb.js} +1 -1
- package/dist/assets/{radar-ABXABTNO-CIJ92p5N.js → radar-ABXABTNO-Bs6dVdAR.js} +1 -1
- package/dist/assets/{railroad-I3PHUGI6-8bxua-Hf.js → railroad-I3PHUGI6-lKaN4miw.js} +1 -1
- package/dist/assets/{railroad-abnf-LNEFI6M7-B9KVI10m.js → railroad-abnf-LNEFI6M7-Bhh16b7Z.js} +1 -1
- package/dist/assets/{railroad-ebnf-DWYD2UWJ-Bt73rnyJ.js → railroad-ebnf-DWYD2UWJ-m6ybW-Y_.js} +1 -1
- package/dist/assets/{railroad-peg-JR7BVNR7-Cie82UPT.js → railroad-peg-JR7BVNR7-DNn72e7r.js} +1 -1
- package/dist/assets/{railroadDiagram-Y72NRBL7-C1nmi_tl.js → railroadDiagram-Y72NRBL7-DNeop1iZ.js} +1 -1
- package/dist/assets/{requirementDiagram-EQYSW5RA-BSianW40.js → requirementDiagram-EQYSW5RA-BVYZ2XYJ.js} +1 -1
- package/dist/assets/{sankeyDiagram-SY24JHO5-DIa11-h7.js → sankeyDiagram-SY24JHO5-B2MP51on.js} +1 -1
- package/dist/assets/{sequenceDiagram-BLEHNHQU-Df5rZKz2.js → sequenceDiagram-BLEHNHQU-BUvxstXG.js} +1 -1
- package/dist/assets/{src-dClg85lX.js → src-BrR0lqlG.js} +1 -1
- package/dist/assets/{stateDiagram-v2-A6IESKWZ-B15PTSvX.js → stateDiagram-v2-A6IESKWZ-Ba84OGKE.js} +1 -1
- package/dist/assets/{swimlanes-B3ENRKFQ-C9mgRCMi.js → swimlanes-B3ENRKFQ-BMSl2PNA.js} +1 -1
- package/dist/assets/swimlanesDiagram-AYQRV4AK-M1dmvq7m.js +8 -0
- package/dist/assets/{timeline-definition-JXYKGLGB-BDVf_mIv.js → timeline-definition-JXYKGLGB-DOdW-dxq.js} +1 -1
- package/dist/assets/{treeView-D4JQ5SDB-BbBK-25J.js → treeView-D4JQ5SDB-DZ9KQyn5.js} +1 -1
- package/dist/assets/{treemap-SAJKECNS-CEDX8apk.js → treemap-SAJKECNS-WEC5wtDV.js} +1 -1
- package/dist/assets/{usecaseDiagram-WPOLBNNX-D1QkwFF8.js → usecaseDiagram-WPOLBNNX-cldTztFL.js} +1 -1
- package/dist/assets/{vennDiagram-Q3Y3IDCW-5PeGZvEO.js → vennDiagram-Q3Y3IDCW-c41KySoY.js} +1 -1
- package/dist/assets/{wardley-7MLQ67FV-BatN94pO.js → wardley-7MLQ67FV-Bf9_aSSZ.js} +1 -1
- package/dist/assets/{wardleyDiagram-IVAIDKWA-DyCcDS3Y.js → wardleyDiagram-IVAIDKWA-BeKEvEBG.js} +1 -1
- package/dist/assets/{xychartDiagram-4URNHDU4-CbCXcRlg.js → xychartDiagram-4URNHDU4-tNkAxTxG.js} +1 -1
- package/dist/index.html +1 -1
- package/package.json +5 -1
- package/server/config.js +140 -14
- package/server/consensus.js +18 -13
- package/server/graph/modules_builder.js +290 -0
- package/server/graph/modules_catalog.js +288 -0
- package/server/graph/parser.js +3 -2
- package/server/graph/sources.js +165 -24
- package/server/graph/syntax.js +245 -28
- package/server/index.js +124 -61
- package/server/install.js +350 -10
- package/server/models_registry.js +34 -9
- package/server/scaffold.js +109 -9
- package/dist/assets/channel-BZ2_TTTJ.js +0 -1
- package/dist/assets/flowDiagram-IUGLLWBU-l7L5DTb5.js +0 -1
- package/dist/assets/swimlanesDiagram-AYQRV4AK-BGFq4t7O.js +0 -8
|
@@ -0,0 +1,290 @@
|
|
|
1
|
+
import { promises as fs } from 'node:fs'
|
|
2
|
+
import path from 'node:path'
|
|
3
|
+
import { GraphNode } from './model.js'
|
|
4
|
+
import { SYNTAX_LANGUAGES, extractCode } from './syntax.js'
|
|
5
|
+
import { desiredSourceEdgeKeys } from './sources.js'
|
|
6
|
+
import {
|
|
7
|
+
resolveModuleLocation,
|
|
8
|
+
formatExternalPackageNodeId,
|
|
9
|
+
formatExternalSymbolNodeId,
|
|
10
|
+
defaultEcosystemModel,
|
|
11
|
+
updateModulesCatalog
|
|
12
|
+
} from './modules_catalog.js'
|
|
13
|
+
|
|
14
|
+
const MODULE_DEFAULT_IGNORES = [
|
|
15
|
+
'node_modules',
|
|
16
|
+
'.git',
|
|
17
|
+
'.github',
|
|
18
|
+
'test',
|
|
19
|
+
'tests',
|
|
20
|
+
'__tests__',
|
|
21
|
+
'docs',
|
|
22
|
+
'doc',
|
|
23
|
+
'example',
|
|
24
|
+
'examples',
|
|
25
|
+
'fixtures',
|
|
26
|
+
'coverage'
|
|
27
|
+
]
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Scan source files of an external module directory.
|
|
31
|
+
*/
|
|
32
|
+
export async function scanModuleFiles(moduleDir, ecosystem) {
|
|
33
|
+
const files = {}
|
|
34
|
+
const walk = async (dirAbs, rel) => {
|
|
35
|
+
let entries
|
|
36
|
+
try {
|
|
37
|
+
entries = await fs.readdir(dirAbs, { withFileTypes: true })
|
|
38
|
+
} catch {
|
|
39
|
+
return
|
|
40
|
+
}
|
|
41
|
+
for (const entry of entries) {
|
|
42
|
+
const childRel = rel ? `${rel}/${entry.name}` : entry.name
|
|
43
|
+
if (entry.isDirectory()) {
|
|
44
|
+
if (MODULE_DEFAULT_IGNORES.includes(entry.name.toLowerCase())) continue
|
|
45
|
+
await walk(path.join(dirAbs, entry.name), childRel)
|
|
46
|
+
continue
|
|
47
|
+
}
|
|
48
|
+
if (!entry.isFile()) continue
|
|
49
|
+
const ext = path.extname(entry.name).toLowerCase()
|
|
50
|
+
if (!SYNTAX_LANGUAGES[ext]) continue
|
|
51
|
+
if (ecosystem === 'go' && entry.name.endsWith('_test.go')) continue
|
|
52
|
+
if (ecosystem === 'pypi' && entry.name.startsWith('test_')) continue
|
|
53
|
+
files[childRel] = path.join(dirAbs, entry.name)
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
await walk(moduleDir, '')
|
|
57
|
+
return files
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Parse an eco::name string into components: { ecosystem, moduleName }
|
|
62
|
+
*/
|
|
63
|
+
export function parseModuleSpecifier(target) {
|
|
64
|
+
if (!target || typeof target !== 'string') return null
|
|
65
|
+
const idx = target.indexOf('::')
|
|
66
|
+
if (idx <= 0) return null
|
|
67
|
+
const ecosystem = target.slice(0, idx).toLowerCase()
|
|
68
|
+
const moduleName = target.slice(idx + 2)
|
|
69
|
+
if (!moduleName) return null
|
|
70
|
+
return { ecosystem, moduleName }
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Build graph for an external module from disk using Tree-Sitter.
|
|
75
|
+
*/
|
|
76
|
+
export async function buildModuleGraph(ecosystem, moduleName, options = {}) {
|
|
77
|
+
const resolved = options.location
|
|
78
|
+
? { location: options.location, version: options.version || '0.0.0', name: moduleName, ecosystem }
|
|
79
|
+
: await resolveModuleLocation(ecosystem, moduleName, options)
|
|
80
|
+
|
|
81
|
+
if (!resolved || !resolved.location) {
|
|
82
|
+
throw new Error(`Could not resolve module directory for ${ecosystem}::${moduleName} on disk. Ensure it is installed.`)
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
const { location, version } = resolved
|
|
86
|
+
const model = defaultEcosystemModel(ecosystem)
|
|
87
|
+
const moduleNodeId = formatExternalPackageNodeId(ecosystem, moduleName, version)
|
|
88
|
+
|
|
89
|
+
const files = await scanModuleFiles(location, ecosystem)
|
|
90
|
+
const symbolNodes = []
|
|
91
|
+
const symbolNamesSet = new Set()
|
|
92
|
+
const edges = []
|
|
93
|
+
const symbolIndex = new Map() // name -> [id, ...]
|
|
94
|
+
const fileSymbolsMap = new Map()
|
|
95
|
+
|
|
96
|
+
for (const [rel, abs] of Object.entries(files)) {
|
|
97
|
+
const ext = path.extname(rel).toLowerCase()
|
|
98
|
+
let content
|
|
99
|
+
try {
|
|
100
|
+
content = await fs.readFile(abs, 'utf8')
|
|
101
|
+
} catch {
|
|
102
|
+
continue
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
let extracted
|
|
106
|
+
try {
|
|
107
|
+
extracted = await extractCode(ext, content)
|
|
108
|
+
} catch {
|
|
109
|
+
continue
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
const nameCounts = new Map()
|
|
113
|
+
for (const sym of extracted.symbols || []) {
|
|
114
|
+
nameCounts.set(sym.name, (nameCounts.get(sym.name) || 0) + 1)
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
const currentFileSymbols = []
|
|
118
|
+
for (const sym of extracted.symbols || []) {
|
|
119
|
+
const symId = nameCounts.get(sym.name) > 1
|
|
120
|
+
? formatExternalSymbolNodeId(ecosystem, moduleName, version, `${sym.name}@${sym.line}`)
|
|
121
|
+
: formatExternalSymbolNodeId(ecosystem, moduleName, version, sym.name)
|
|
122
|
+
|
|
123
|
+
symbolNamesSet.add(sym.name)
|
|
124
|
+
currentFileSymbols.push({ ...sym, id: symId })
|
|
125
|
+
symbolIndex.set(sym.name, [...(symbolIndex.get(sym.name) || []), symId])
|
|
126
|
+
|
|
127
|
+
edges.push({ from: moduleNodeId, to: symId, type: 'defines' })
|
|
128
|
+
symbolNodes.push(new GraphNode({
|
|
129
|
+
id: symId,
|
|
130
|
+
title: sym.name,
|
|
131
|
+
type: 'symbol',
|
|
132
|
+
metadata: { kind: sym.kind, line: sym.line }
|
|
133
|
+
}))
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
fileSymbolsMap.set(rel, { symbols: currentFileSymbols, calls: extracted.calls || [] })
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// Internal calls between symbols inside the module
|
|
140
|
+
for (const [, { symbols, calls }] of fileSymbolsMap.entries()) {
|
|
141
|
+
for (const call of calls) {
|
|
142
|
+
let owner = null
|
|
143
|
+
for (const sym of symbols) {
|
|
144
|
+
const start = sym.line - 1
|
|
145
|
+
const end = start + sym.lines
|
|
146
|
+
if (call.row >= start && call.row < end) {
|
|
147
|
+
if (!owner || sym.lines < owner.lines) owner = sym
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
if (!owner) continue
|
|
151
|
+
const targetMatches = symbolIndex.get(call.name) || []
|
|
152
|
+
if (targetMatches.length === 1 && targetMatches[0] !== owner.id) {
|
|
153
|
+
edges.push({ from: owner.id, to: targetMatches[0], type: 'calls' })
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
const moduleNode = new GraphNode({
|
|
159
|
+
id: moduleNodeId,
|
|
160
|
+
title: `${moduleName}@${version}`,
|
|
161
|
+
type: 'module',
|
|
162
|
+
metadata: { ecosystem, name: moduleName, version }
|
|
163
|
+
})
|
|
164
|
+
|
|
165
|
+
return {
|
|
166
|
+
moduleNode,
|
|
167
|
+
symbolNodes,
|
|
168
|
+
edges,
|
|
169
|
+
symbols: [...symbolNamesSet].sort(),
|
|
170
|
+
name: moduleName,
|
|
171
|
+
version,
|
|
172
|
+
ecosystem,
|
|
173
|
+
model,
|
|
174
|
+
location
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/**
|
|
179
|
+
* Sync the constructed module graph to AnchorDB in model modules-<ecosystem>.
|
|
180
|
+
*/
|
|
181
|
+
export async function syncModuleToAnchorDB(client, moduleGraph, options = {}) {
|
|
182
|
+
if (!client || !moduleGraph) return { linked: 0, saved: 0, failed: 0 }
|
|
183
|
+
|
|
184
|
+
const desired = desiredSourceEdgeKeys(moduleGraph.edges)
|
|
185
|
+
let linked = 0
|
|
186
|
+
let failed = 0
|
|
187
|
+
|
|
188
|
+
for (const key of desired) {
|
|
189
|
+
const [from, type, to] = key.split('\0')
|
|
190
|
+
try {
|
|
191
|
+
await client.link(from, type, to, 1.0)
|
|
192
|
+
linked += 1
|
|
193
|
+
} catch {
|
|
194
|
+
failed += 1
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
const pending = [
|
|
199
|
+
{ name: moduleGraph.moduleNode.id, props: { ...moduleGraph.moduleNode.metadata, tombstone: 0 } },
|
|
200
|
+
...moduleGraph.symbolNodes.map((s) => ({ name: s.id, props: { ...s.metadata, tombstone: 0 } }))
|
|
201
|
+
]
|
|
202
|
+
|
|
203
|
+
let saved = 0
|
|
204
|
+
if (typeof client.saveBatch === 'function') {
|
|
205
|
+
try {
|
|
206
|
+
await client.saveBatch(pending)
|
|
207
|
+
saved = pending.length
|
|
208
|
+
} catch {
|
|
209
|
+
for (const item of pending) {
|
|
210
|
+
try {
|
|
211
|
+
await client.saveWithID(item.name, item.props)
|
|
212
|
+
saved += 1
|
|
213
|
+
} catch {
|
|
214
|
+
failed += 1
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
} else {
|
|
219
|
+
for (const item of pending) {
|
|
220
|
+
try {
|
|
221
|
+
await client.saveWithID(item.name, item.props)
|
|
222
|
+
saved += 1
|
|
223
|
+
} catch {
|
|
224
|
+
failed += 1
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
await updateModulesCatalog(moduleGraph.ecosystem, {
|
|
230
|
+
[`${moduleGraph.name}@${moduleGraph.version}`]: {
|
|
231
|
+
name: moduleGraph.name,
|
|
232
|
+
version: moduleGraph.version,
|
|
233
|
+
ecosystem: moduleGraph.ecosystem,
|
|
234
|
+
model: moduleGraph.model,
|
|
235
|
+
nodeId: moduleGraph.moduleNode.id,
|
|
236
|
+
symbols: moduleGraph.symbols
|
|
237
|
+
}
|
|
238
|
+
}, options)
|
|
239
|
+
|
|
240
|
+
return {
|
|
241
|
+
linked,
|
|
242
|
+
saved,
|
|
243
|
+
failed,
|
|
244
|
+
symbols: moduleGraph.symbols.length,
|
|
245
|
+
edges: moduleGraph.edges.length,
|
|
246
|
+
model: moduleGraph.model
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
/**
|
|
251
|
+
* Trace lineage for a module or symbol in its ecosystem model.
|
|
252
|
+
*/
|
|
253
|
+
export async function traceModuleSymbol(client, targetId, { direction = 'both', maxHops = 2 } = {}) {
|
|
254
|
+
const forwardRelations = targetId.includes('#') ? ['calls'] : ['defines']
|
|
255
|
+
const backwardRelations = targetId.includes('#') ? ['called_by', 'defined_in'] : []
|
|
256
|
+
|
|
257
|
+
const rels = []
|
|
258
|
+
if (direction === 'out' || direction === 'both') rels.push(...forwardRelations)
|
|
259
|
+
if (direction === 'in' || direction === 'both') rels.push(...backwardRelations)
|
|
260
|
+
|
|
261
|
+
const visited = new Map() // id -> hop
|
|
262
|
+
visited.set(targetId, 0)
|
|
263
|
+
let frontier = [targetId]
|
|
264
|
+
|
|
265
|
+
for (let hop = 1; hop <= maxHops && frontier.length; hop++) {
|
|
266
|
+
const nextFrontier = []
|
|
267
|
+
for (const current of frontier) {
|
|
268
|
+
for (const rel of rels) {
|
|
269
|
+
let rows = []
|
|
270
|
+
try {
|
|
271
|
+
rows = await client.causalChain([current], rel, 100)
|
|
272
|
+
} catch {
|
|
273
|
+
continue
|
|
274
|
+
}
|
|
275
|
+
for (const r of rows || []) {
|
|
276
|
+
if (!r?.word || visited.has(r.word)) continue
|
|
277
|
+
visited.set(r.word, hop)
|
|
278
|
+
nextFrontier.push(r.word)
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
frontier = nextFrontier
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
const results = []
|
|
286
|
+
for (const [id, hop] of visited.entries()) {
|
|
287
|
+
if (id !== targetId) results.push({ id, hop })
|
|
288
|
+
}
|
|
289
|
+
return results.sort((a, b) => a.hop - b.hop || a.id.localeCompare(b.id))
|
|
290
|
+
}
|
|
@@ -0,0 +1,288 @@
|
|
|
1
|
+
import { promises as fs } from 'node:fs'
|
|
2
|
+
import path from 'node:path'
|
|
3
|
+
import { knlGlobalDir } from '../config.js'
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Returns the path to the global modules catalog directory (~/.knlview/modules).
|
|
7
|
+
*/
|
|
8
|
+
export function modulesCatalogDir(home = null, options = {}) {
|
|
9
|
+
const base = knlGlobalDir(home, options)
|
|
10
|
+
return path.join(base, 'modules')
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Default global AnchorDB model name for an ecosystem.
|
|
15
|
+
*/
|
|
16
|
+
export function defaultEcosystemModel(ecosystem) {
|
|
17
|
+
return `modules-${ecosystem}`
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function formatModuleKey(pkgName, version) {
|
|
21
|
+
return `${pkgName}@${version}`
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function formatExternalPackageNodeId(ecosystem, pkgName, version) {
|
|
25
|
+
return `ext:${ecosystem}/${pkgName}@${version}`
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function formatExternalSymbolNodeId(ecosystem, pkgName, version, symbol) {
|
|
29
|
+
return `ext:${ecosystem}/${pkgName}@${version}#${symbol}`
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Read the catalog for a specific ecosystem ('npm', 'go', 'cargo', 'pypi').
|
|
34
|
+
*/
|
|
35
|
+
export async function readModulesCatalog(ecosystem, options = {}) {
|
|
36
|
+
const dir = modulesCatalogDir(options.home, options)
|
|
37
|
+
const file = path.join(dir, `${ecosystem}.json`)
|
|
38
|
+
try {
|
|
39
|
+
const raw = await fs.readFile(file, 'utf8')
|
|
40
|
+
const parsed = JSON.parse(raw)
|
|
41
|
+
return parsed && typeof parsed === 'object' ? parsed : {}
|
|
42
|
+
} catch {
|
|
43
|
+
return {}
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Check if a module version is already indexed in the catalog.
|
|
49
|
+
*/
|
|
50
|
+
export async function isModuleSynced(ecosystem, pkgName, version, options = {}) {
|
|
51
|
+
const catalog = await readModulesCatalog(ecosystem, options)
|
|
52
|
+
const key = formatModuleKey(pkgName, version)
|
|
53
|
+
return Boolean(catalog[key]?.syncedAt)
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Record discovered modules and symbols to ~/.knlview/modules/<ecosystem>.json.
|
|
58
|
+
*/
|
|
59
|
+
export async function updateModulesCatalog(ecosystem, entries, options = {}) {
|
|
60
|
+
if (!entries || !Object.keys(entries).length) return
|
|
61
|
+
const dir = modulesCatalogDir(options.home, options)
|
|
62
|
+
const file = path.join(dir, `${ecosystem}.json`)
|
|
63
|
+
try {
|
|
64
|
+
await fs.mkdir(dir, { recursive: true })
|
|
65
|
+
const existing = await readModulesCatalog(ecosystem, options)
|
|
66
|
+
for (const [key, item] of Object.entries(entries)) {
|
|
67
|
+
const prev = existing[key] || {
|
|
68
|
+
name: item.name,
|
|
69
|
+
version: item.version,
|
|
70
|
+
ecosystem,
|
|
71
|
+
model: item.model || defaultEcosystemModel(ecosystem),
|
|
72
|
+
nodeId: item.nodeId || formatExternalPackageNodeId(ecosystem, item.name, item.version),
|
|
73
|
+
symbols: []
|
|
74
|
+
}
|
|
75
|
+
const symSet = new Set([...(prev.symbols || []), ...(item.symbols || [])])
|
|
76
|
+
existing[key] = {
|
|
77
|
+
name: item.name,
|
|
78
|
+
version: item.version,
|
|
79
|
+
ecosystem,
|
|
80
|
+
model: item.model || prev.model || defaultEcosystemModel(ecosystem),
|
|
81
|
+
nodeId: item.nodeId || prev.nodeId || formatExternalPackageNodeId(ecosystem, item.name, item.version),
|
|
82
|
+
symbols: [...symSet].sort(),
|
|
83
|
+
syncedAt: item.syncedAt || prev.syncedAt || Date.now()
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
await fs.writeFile(file, JSON.stringify(existing, null, 2) + '\n', 'utf8')
|
|
87
|
+
} catch {
|
|
88
|
+
// Fail gracefully: catalog is an informational index, never fatal.
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Detect project dependency versions from on-disk manifests:
|
|
94
|
+
* package.json, go.mod, Cargo.toml, requirements.txt.
|
|
95
|
+
*/
|
|
96
|
+
export async function detectProjectManifests(rootAbs) {
|
|
97
|
+
const manifests = {
|
|
98
|
+
npm: {},
|
|
99
|
+
go: {},
|
|
100
|
+
cargo: {},
|
|
101
|
+
pypi: {}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// 1. npm (package.json)
|
|
105
|
+
try {
|
|
106
|
+
const pkgRaw = await fs.readFile(path.join(rootAbs, 'package.json'), 'utf8')
|
|
107
|
+
const pkg = JSON.parse(pkgRaw)
|
|
108
|
+
const all = { ...pkg.dependencies, ...pkg.devDependencies, ...pkg.peerDependencies }
|
|
109
|
+
for (const [name, ver] of Object.entries(all)) {
|
|
110
|
+
if (typeof ver === 'string') {
|
|
111
|
+
manifests.npm[name] = ver.replace(/^[\^~>=<]/, '').trim() || 'latest'
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
} catch {}
|
|
115
|
+
|
|
116
|
+
// 2. Go (go.mod)
|
|
117
|
+
try {
|
|
118
|
+
const text = await fs.readFile(path.join(rootAbs, 'go.mod'), 'utf8')
|
|
119
|
+
const singleMatch = text.matchAll(/^require\s+([^\s()]+)\s+([^\s()]+)/gm)
|
|
120
|
+
for (const m of singleMatch) manifests.go[m[1]] = m[2]
|
|
121
|
+
const blockMatch = text.match(/require\s*\(([\s\S]*?)\)/m)
|
|
122
|
+
if (blockMatch) {
|
|
123
|
+
for (const line of blockMatch[1].split('\n')) {
|
|
124
|
+
const parts = line.trim().split(/\s+/)
|
|
125
|
+
if (parts.length >= 2 && !parts[0].startsWith('//')) {
|
|
126
|
+
manifests.go[parts[0]] = parts[1]
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
} catch {}
|
|
131
|
+
|
|
132
|
+
// 3. Rust (Cargo.toml)
|
|
133
|
+
try {
|
|
134
|
+
const text = await fs.readFile(path.join(rootAbs, 'Cargo.toml'), 'utf8')
|
|
135
|
+
let inDeps = false
|
|
136
|
+
for (const line of text.split('\n')) {
|
|
137
|
+
const trimmed = line.trim()
|
|
138
|
+
if (
|
|
139
|
+
trimmed.startsWith('[dependencies]') ||
|
|
140
|
+
trimmed.startsWith('[dev-dependencies]') ||
|
|
141
|
+
trimmed.startsWith('[build-dependencies]')
|
|
142
|
+
) {
|
|
143
|
+
inDeps = true
|
|
144
|
+
continue
|
|
145
|
+
}
|
|
146
|
+
if (trimmed.startsWith('[')) {
|
|
147
|
+
inDeps = false
|
|
148
|
+
continue
|
|
149
|
+
}
|
|
150
|
+
if (inDeps && trimmed && !trimmed.startsWith('#')) {
|
|
151
|
+
const eqIdx = trimmed.indexOf('=')
|
|
152
|
+
if (eqIdx > 0) {
|
|
153
|
+
const name = trimmed.slice(0, eqIdx).trim()
|
|
154
|
+
const val = trimmed.slice(eqIdx + 1).trim()
|
|
155
|
+
let ver = 'unknown'
|
|
156
|
+
const strMatch = val.match(/^"([^"]+)"/)
|
|
157
|
+
if (strMatch) ver = strMatch[1]
|
|
158
|
+
const objMatch = val.match(/version\s*=\s*"([^"]+)"/)
|
|
159
|
+
if (objMatch) ver = objMatch[1]
|
|
160
|
+
manifests.cargo[name] = ver
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
} catch {}
|
|
165
|
+
|
|
166
|
+
// 4. Python (requirements.txt)
|
|
167
|
+
try {
|
|
168
|
+
const reqText = await fs.readFile(path.join(rootAbs, 'requirements.txt'), 'utf8')
|
|
169
|
+
for (const line of reqText.split('\n')) {
|
|
170
|
+
const trimmed = line.trim()
|
|
171
|
+
if (!trimmed || trimmed.startsWith('#')) continue
|
|
172
|
+
const m = trimmed.match(/^([a-zA-Z0-9_.-]+)(?:[=<>~!\s]*([a-zA-Z0-9_.-]+))?/)
|
|
173
|
+
if (m) manifests.pypi[m[1].toLowerCase()] = m[2] || 'unknown'
|
|
174
|
+
}
|
|
175
|
+
} catch {}
|
|
176
|
+
|
|
177
|
+
return manifests
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/**
|
|
181
|
+
* Resolve the directory of an installed external module on disk.
|
|
182
|
+
* Supports npm (node_modules), Go (GOPATH/pkg/mod or vendor), Cargo (cargo cache or vendor),
|
|
183
|
+
* and Python (venv site-packages).
|
|
184
|
+
*/
|
|
185
|
+
export async function resolveModuleLocation(ecosystem, moduleName, options = {}) {
|
|
186
|
+
const root = path.resolve(options.projectRoot || process.cwd())
|
|
187
|
+
const manifests = options.manifests || await detectProjectManifests(root)
|
|
188
|
+
|
|
189
|
+
if (ecosystem === 'npm') {
|
|
190
|
+
// 1. Check node_modules in projectRoot and walk up if necessary
|
|
191
|
+
let curr = root
|
|
192
|
+
let foundDir = null
|
|
193
|
+
while (true) {
|
|
194
|
+
const candidate = path.join(curr, 'node_modules', moduleName)
|
|
195
|
+
try {
|
|
196
|
+
const stat = await fs.stat(candidate)
|
|
197
|
+
if (stat.isDirectory()) {
|
|
198
|
+
foundDir = candidate
|
|
199
|
+
break
|
|
200
|
+
}
|
|
201
|
+
} catch {}
|
|
202
|
+
const parent = path.dirname(curr)
|
|
203
|
+
if (parent === curr) break
|
|
204
|
+
curr = parent
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
let version = manifests.npm[moduleName] || 'unknown'
|
|
208
|
+
if (foundDir) {
|
|
209
|
+
try {
|
|
210
|
+
const pkgText = await fs.readFile(path.join(foundDir, 'package.json'), 'utf8')
|
|
211
|
+
const pkg = JSON.parse(pkgText)
|
|
212
|
+
if (pkg.version) version = pkg.version
|
|
213
|
+
} catch {}
|
|
214
|
+
return { location: foundDir, version, name: moduleName, ecosystem }
|
|
215
|
+
}
|
|
216
|
+
} else if (ecosystem === 'go') {
|
|
217
|
+
// 1. Check local vendor
|
|
218
|
+
const vendorDir = path.join(root, 'vendor', moduleName)
|
|
219
|
+
try {
|
|
220
|
+
const stat = await fs.stat(vendorDir)
|
|
221
|
+
if (stat.isDirectory()) {
|
|
222
|
+
return { location: vendorDir, version: manifests.go[moduleName] || 'vendor', name: moduleName, ecosystem }
|
|
223
|
+
}
|
|
224
|
+
} catch {}
|
|
225
|
+
|
|
226
|
+
// 2. Check GOPATH
|
|
227
|
+
const gopath = process.env.GOPATH || path.join(options.home || process.env.HOME || '', 'go')
|
|
228
|
+
const version = manifests.go[moduleName]
|
|
229
|
+
if (version) {
|
|
230
|
+
const modDir = path.join(gopath, 'pkg', 'mod', `${moduleName}@${version}`)
|
|
231
|
+
try {
|
|
232
|
+
const stat = await fs.stat(modDir)
|
|
233
|
+
if (stat.isDirectory()) {
|
|
234
|
+
return { location: modDir, version, name: moduleName, ecosystem }
|
|
235
|
+
}
|
|
236
|
+
} catch {}
|
|
237
|
+
}
|
|
238
|
+
} else if (ecosystem === 'cargo') {
|
|
239
|
+
// 1. Check local vendor
|
|
240
|
+
const vendorDir = path.join(root, 'vendor', moduleName)
|
|
241
|
+
try {
|
|
242
|
+
const stat = await fs.stat(vendorDir)
|
|
243
|
+
if (stat.isDirectory()) {
|
|
244
|
+
return { location: vendorDir, version: manifests.cargo[moduleName] || 'vendor', name: moduleName, ecosystem }
|
|
245
|
+
}
|
|
246
|
+
} catch {}
|
|
247
|
+
|
|
248
|
+
// 2. Check cargo registry cache (~/.cargo/registry/src)
|
|
249
|
+
const cargoHome = process.env.CARGO_HOME || path.join(options.home || process.env.HOME || '', '.cargo')
|
|
250
|
+
const regSrc = path.join(cargoHome, 'registry', 'src')
|
|
251
|
+
const version = manifests.cargo[moduleName]
|
|
252
|
+
try {
|
|
253
|
+
const registries = await fs.readdir(regSrc)
|
|
254
|
+
for (const reg of registries) {
|
|
255
|
+
const targetPrefix = version ? `${moduleName}-${version}` : `${moduleName}-`
|
|
256
|
+
const regDir = path.join(regSrc, reg)
|
|
257
|
+
const entries = await fs.readdir(regDir)
|
|
258
|
+
const match = entries.find((e) => version ? e === targetPrefix : e.startsWith(targetPrefix))
|
|
259
|
+
if (match) {
|
|
260
|
+
const location = path.join(regDir, match)
|
|
261
|
+
const matchedVersion = version || match.slice(moduleName.length + 1)
|
|
262
|
+
return { location, version: matchedVersion, name: moduleName, ecosystem }
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
} catch {}
|
|
266
|
+
} else if (ecosystem === 'pypi') {
|
|
267
|
+
const version = manifests.pypi[moduleName.toLowerCase()] || 'unknown'
|
|
268
|
+
// 1. Check .venv and venv site-packages
|
|
269
|
+
const venvDirs = ['.venv', 'venv']
|
|
270
|
+
for (const venv of venvDirs) {
|
|
271
|
+
const libDir = path.join(root, venv, 'lib')
|
|
272
|
+
try {
|
|
273
|
+
const pyDirs = await fs.readdir(libDir)
|
|
274
|
+
for (const py of pyDirs) {
|
|
275
|
+
const sp = path.join(libDir, py, 'site-packages', moduleName)
|
|
276
|
+
try {
|
|
277
|
+
const stat = await fs.stat(sp)
|
|
278
|
+
if (stat.isDirectory()) {
|
|
279
|
+
return { location: sp, version, name: moduleName, ecosystem }
|
|
280
|
+
}
|
|
281
|
+
} catch {}
|
|
282
|
+
}
|
|
283
|
+
} catch {}
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
return null
|
|
288
|
+
}
|
package/server/graph/parser.js
CHANGED
|
@@ -36,7 +36,7 @@ export function extractFrontmatterAndTitle(content) {
|
|
|
36
36
|
|
|
37
37
|
function parseSimpleYaml(text) {
|
|
38
38
|
const result = {}
|
|
39
|
-
const lines = text.split('\n')
|
|
39
|
+
const lines = text.replace(/\r/g, '').split('\n')
|
|
40
40
|
for (const line of lines) {
|
|
41
41
|
const match = line.match(/^([a-zA-Z0-9_-]+)\s*:\s*(.*)$/)
|
|
42
42
|
if (!match) continue
|
|
@@ -150,7 +150,8 @@ function appendList(existing, val) {
|
|
|
150
150
|
return existing.concat(items.filter((item) => !existing.includes(item)))
|
|
151
151
|
}
|
|
152
152
|
|
|
153
|
-
function parseSingleDecisionBlock(relPath, rawBlock) {
|
|
153
|
+
function parseSingleDecisionBlock(relPath, rawBlock) {
|
|
154
|
+
const lines = rawBlock.replace(/\r/g, '').split('\n')
|
|
154
155
|
let title = 'Untitled Decision'
|
|
155
156
|
let id = null
|
|
156
157
|
let decidedAt = null
|