@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.
Files changed (98) hide show
  1. package/README.md +133 -32
  2. package/bin/knl-view.js +591 -58
  3. package/dist/assets/{abnfDiagram-WEYOIA3C-B1gaBaSI.js → abnfDiagram-WEYOIA3C-hoSbsMOz.js} +1 -1
  4. package/dist/assets/{arc-NZ9uRaCl.js → arc-g87Np6E5.js} +1 -1
  5. package/dist/assets/{architecture-WOLXFQ4H-8MalLwTd.js → architecture-WOLXFQ4H-DOIq-0IA.js} +1 -1
  6. package/dist/assets/{architectureDiagram-ZHVYKT53-eZ8i5cl4.js → architectureDiagram-ZHVYKT53-Dxwe8n7m.js} +1 -1
  7. package/dist/assets/{blockDiagram-RJAE7QA3-BavOE8BM.js → blockDiagram-RJAE7QA3-BnGng59Z.js} +1 -1
  8. package/dist/assets/{c4Diagram-XDPPPE54-DqMaTAzx.js → c4Diagram-XDPPPE54-C5LjqIIo.js} +1 -1
  9. package/dist/assets/channel-BVWkRFBw.js +1 -0
  10. package/dist/assets/{chunk-2E7WHVMD-B1Hj2BjJ.js → chunk-2E7WHVMD-BJ1dyGLq.js} +1 -1
  11. package/dist/assets/{chunk-4EMNKR2T-HDscrCiq.js → chunk-4EMNKR2T-DXF02Sgw.js} +1 -1
  12. package/dist/assets/{chunk-55XCU76K-QR_ARj8I.js → chunk-55XCU76K-CM1akAbu.js} +1 -1
  13. package/dist/assets/{chunk-7S6XSTFO-B0p3o547.js → chunk-7S6XSTFO-BOUjqldL.js} +1 -1
  14. package/dist/assets/{chunk-AQDTSKZB-CCxwNyyA.js → chunk-AQDTSKZB-C4isdNLA.js} +1 -1
  15. package/dist/assets/{chunk-DSNLSHJZ-BM5PBFmz.js → chunk-DSNLSHJZ-CbOTkSVM.js} +1 -1
  16. package/dist/assets/{chunk-HAGFOTED-UCDVFuN_.js → chunk-HAGFOTED-CRfnketR.js} +1 -1
  17. package/dist/assets/{chunk-HNE4Q36P-C_1sakp2.js → chunk-HNE4Q36P-rxlpGUU8.js} +1 -1
  18. package/dist/assets/{chunk-NQOKRVIF-b2D1Z4oK.js → chunk-NQOKRVIF-BOYCgTmV.js} +1 -1
  19. package/dist/assets/{chunk-UHW7PFBP-CJs0VwBY.js → chunk-UHW7PFBP-Cnogp1Fb.js} +1 -1
  20. package/dist/assets/{chunk-VQ5W62QI-DQxjgE4_.js → chunk-VQ5W62QI-Cu1WY5Uo.js} +1 -1
  21. package/dist/assets/{chunk-WQQL4CDK-DcjPeAfQ.js → chunk-WQQL4CDK-BJc3FPE2.js} +1 -1
  22. package/dist/assets/{chunk-XXDRQBXY-glA0kB5s.js → chunk-XXDRQBXY-BlPj4H7s.js} +1 -1
  23. package/dist/assets/{chunk-YZO67HFG-DJqACL5V.js → chunk-YZO67HFG-V85H31x4.js} +1 -1
  24. package/dist/assets/{chunk-ZAUX2LTX-CWfaYtwb.js → chunk-ZAUX2LTX-C3XKXW09.js} +1 -1
  25. package/dist/assets/{chunk-ZCHMQSOR-BGX73Hho.js → chunk-ZCHMQSOR-B0mmpEgW.js} +1 -1
  26. package/dist/assets/{classDiagram-v2-4MYWR3UA-BpYXPz-e.js → classDiagram-v2-4MYWR3UA-oMd7YaHP.js} +1 -1
  27. package/dist/assets/{cose-bilkent-JH36ORCC-e1quKm0Q.js → cose-bilkent-JH36ORCC-J0kSVsDf.js} +1 -1
  28. package/dist/assets/{cynefin-EF2NZ3EQ-hb3oUmFO.js → cynefin-EF2NZ3EQ-DK2TNhel.js} +1 -1
  29. package/dist/assets/{cynefinDiagram-WF24UDRP-CnMQvLqi.js → cynefinDiagram-WF24UDRP-DDvKgieA.js} +1 -1
  30. package/dist/assets/{dagre-NK4GIZVX-BH_Y8y3F.js → dagre-NK4GIZVX-Ca2Fe-i-.js} +1 -1
  31. package/dist/assets/{diagram-CBK3MWTC-Dh79VetZ.js → diagram-CBK3MWTC-L6dmeQ2V.js} +1 -1
  32. package/dist/assets/{diagram-I5XIIV2D-D3u-o1RV.js → diagram-I5XIIV2D-DhG8R-v5.js} +1 -1
  33. package/dist/assets/{diagram-ODIL2UCW-Bfuo-OKS.js → diagram-ODIL2UCW-BpYYbYBT.js} +1 -1
  34. package/dist/assets/{diagram-PFBEJUXE-CwHasIbs.js → diagram-PFBEJUXE-DKQpHSsC.js} +1 -1
  35. package/dist/assets/{diagram-RJA3I4VV-D8ttmuE8.js → diagram-RJA3I4VV-DP7Dsnya.js} +1 -1
  36. package/dist/assets/{diagram-TYUNFSNR-CzC-8RZ0.js → diagram-TYUNFSNR-CfSFewUS.js} +1 -1
  37. package/dist/assets/{dist-CUJYGnnI.js → dist-CQNjnrld.js} +1 -1
  38. package/dist/assets/{ebnfDiagram-V4P3TTPT-9AAhU7In.js → ebnfDiagram-V4P3TTPT-DE3MZoJO.js} +1 -1
  39. package/dist/assets/{elk-QONTBCTQ-BgvnD1v_.js → elk-QONTBCTQ-p_aAQPHU.js} +1 -1
  40. package/dist/assets/{erDiagram-KYQIGQAA-RLtbiLlP.js → erDiagram-KYQIGQAA-DZO3vQrk.js} +1 -1
  41. package/dist/assets/{eventmodeling-K75KTNOO-DVY22vNZ.js → eventmodeling-K75KTNOO-BII9EPl5.js} +1 -1
  42. package/dist/assets/flowDiagram-IUGLLWBU-D9uPno39.js +1 -0
  43. package/dist/assets/{ganttDiagram-R7GPNTJY-hHiBGa9W.js → ganttDiagram-R7GPNTJY-BKiH3v2r.js} +1 -1
  44. package/dist/assets/{gitGraph-VSP46ZUC-Cvxay1wh.js → gitGraph-VSP46ZUC-oHL6cr73.js} +1 -1
  45. package/dist/assets/{gitGraphDiagram-PDYL7XHY-AOhlpSqg.js → gitGraphDiagram-PDYL7XHY-CqPVIEeo.js} +1 -1
  46. package/dist/assets/{index-CBLNQvK3.js → index-CSpirV16.js} +3 -3
  47. package/dist/assets/{info-OHQRW6UA-CJ8EUO1m.js → info-OHQRW6UA-Drrbnhpd.js} +1 -1
  48. package/dist/assets/{infoDiagram-MVDRKAX5-DOQu7oYI.js → infoDiagram-MVDRKAX5-DnQQFMAd.js} +1 -1
  49. package/dist/assets/{ishikawaDiagram-HPEHV2GE-DcVMvGmx.js → ishikawaDiagram-HPEHV2GE-DIpQlxRh.js} +1 -1
  50. package/dist/assets/{journeyDiagram-BOSHW7O7-luMeqEqz.js → journeyDiagram-BOSHW7O7-EBR9I5h1.js} +1 -1
  51. package/dist/assets/{kanban-definition-FNSA4HC5-KTZbmrW2.js → kanban-definition-FNSA4HC5-CdvNsjik.js} +1 -1
  52. package/dist/assets/{line-CmPHiOVm.js → line-CDLzgxDc.js} +1 -1
  53. package/dist/assets/{linear-DHYlKXBA.js → linear-DfXKql9u.js} +1 -1
  54. package/dist/assets/{mermaid-parser.core-BRDI-WhC.js → mermaid-parser.core-DnXGphT2.js} +3 -3
  55. package/dist/assets/{mermaid.core-DPTRaYLy.js → mermaid.core-h6fiCaSZ.js} +4 -4
  56. package/dist/assets/{mindmap-definition-6OY57UG3-70EN5Kw_.js → mindmap-definition-6OY57UG3-BiNfio-j.js} +1 -1
  57. package/dist/assets/{packet-JDAUHWVQ-CXWz83oS.js → packet-JDAUHWVQ-D76byv1S.js} +1 -1
  58. package/dist/assets/{pegDiagram-ECIUJNBX-z5UXKmp4.js → pegDiagram-ECIUJNBX-Drs8Pxdt.js} +1 -1
  59. package/dist/assets/{pie-XZMESJXO-rWI1HIQ7.js → pie-XZMESJXO-D_83z1lM.js} +1 -1
  60. package/dist/assets/{pieDiagram-GNMPY74T-Bh4Kv3NQ.js → pieDiagram-GNMPY74T-wvzIphSf.js} +1 -1
  61. package/dist/assets/{quadrantDiagram-D57K4MC3-wGM4Ckig.js → quadrantDiagram-D57K4MC3-B7EfMRWb.js} +1 -1
  62. package/dist/assets/{radar-ABXABTNO-CIJ92p5N.js → radar-ABXABTNO-Bs6dVdAR.js} +1 -1
  63. package/dist/assets/{railroad-I3PHUGI6-8bxua-Hf.js → railroad-I3PHUGI6-lKaN4miw.js} +1 -1
  64. package/dist/assets/{railroad-abnf-LNEFI6M7-B9KVI10m.js → railroad-abnf-LNEFI6M7-Bhh16b7Z.js} +1 -1
  65. package/dist/assets/{railroad-ebnf-DWYD2UWJ-Bt73rnyJ.js → railroad-ebnf-DWYD2UWJ-m6ybW-Y_.js} +1 -1
  66. package/dist/assets/{railroad-peg-JR7BVNR7-Cie82UPT.js → railroad-peg-JR7BVNR7-DNn72e7r.js} +1 -1
  67. package/dist/assets/{railroadDiagram-Y72NRBL7-C1nmi_tl.js → railroadDiagram-Y72NRBL7-DNeop1iZ.js} +1 -1
  68. package/dist/assets/{requirementDiagram-EQYSW5RA-BSianW40.js → requirementDiagram-EQYSW5RA-BVYZ2XYJ.js} +1 -1
  69. package/dist/assets/{sankeyDiagram-SY24JHO5-DIa11-h7.js → sankeyDiagram-SY24JHO5-B2MP51on.js} +1 -1
  70. package/dist/assets/{sequenceDiagram-BLEHNHQU-Df5rZKz2.js → sequenceDiagram-BLEHNHQU-BUvxstXG.js} +1 -1
  71. package/dist/assets/{src-dClg85lX.js → src-BrR0lqlG.js} +1 -1
  72. package/dist/assets/{stateDiagram-v2-A6IESKWZ-B15PTSvX.js → stateDiagram-v2-A6IESKWZ-Ba84OGKE.js} +1 -1
  73. package/dist/assets/{swimlanes-B3ENRKFQ-C9mgRCMi.js → swimlanes-B3ENRKFQ-BMSl2PNA.js} +1 -1
  74. package/dist/assets/swimlanesDiagram-AYQRV4AK-M1dmvq7m.js +8 -0
  75. package/dist/assets/{timeline-definition-JXYKGLGB-BDVf_mIv.js → timeline-definition-JXYKGLGB-DOdW-dxq.js} +1 -1
  76. package/dist/assets/{treeView-D4JQ5SDB-BbBK-25J.js → treeView-D4JQ5SDB-DZ9KQyn5.js} +1 -1
  77. package/dist/assets/{treemap-SAJKECNS-CEDX8apk.js → treemap-SAJKECNS-WEC5wtDV.js} +1 -1
  78. package/dist/assets/{usecaseDiagram-WPOLBNNX-D1QkwFF8.js → usecaseDiagram-WPOLBNNX-cldTztFL.js} +1 -1
  79. package/dist/assets/{vennDiagram-Q3Y3IDCW-5PeGZvEO.js → vennDiagram-Q3Y3IDCW-c41KySoY.js} +1 -1
  80. package/dist/assets/{wardley-7MLQ67FV-BatN94pO.js → wardley-7MLQ67FV-Bf9_aSSZ.js} +1 -1
  81. package/dist/assets/{wardleyDiagram-IVAIDKWA-DyCcDS3Y.js → wardleyDiagram-IVAIDKWA-BeKEvEBG.js} +1 -1
  82. package/dist/assets/{xychartDiagram-4URNHDU4-CbCXcRlg.js → xychartDiagram-4URNHDU4-tNkAxTxG.js} +1 -1
  83. package/dist/index.html +1 -1
  84. package/package.json +5 -1
  85. package/server/config.js +140 -14
  86. package/server/consensus.js +18 -13
  87. package/server/graph/modules_builder.js +290 -0
  88. package/server/graph/modules_catalog.js +288 -0
  89. package/server/graph/parser.js +3 -2
  90. package/server/graph/sources.js +165 -24
  91. package/server/graph/syntax.js +245 -28
  92. package/server/index.js +124 -61
  93. package/server/install.js +350 -10
  94. package/server/models_registry.js +34 -9
  95. package/server/scaffold.js +109 -9
  96. package/dist/assets/channel-BZ2_TTTJ.js +0 -1
  97. package/dist/assets/flowDiagram-IUGLLWBU-l7L5DTb5.js +0 -1
  98. 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
+ }
@@ -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) { const lines = rawBlock.split('\n')
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