@rahmantech/knl-view 0.2.0 → 0.3.1
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 +194 -261
- package/bin/knl-view.js +162 -0
- package/package.json +1 -1
- package/server/consensus.js +0 -4
- package/server/graph/modules_builder.js +300 -0
- package/server/graph/modules_catalog.js +329 -0
- package/server/graph/sources.js +1 -2
- package/server/graph/syntax.js +54 -17
- package/server/scaffold.js +14 -3
|
@@ -0,0 +1,329 @@
|
|
|
1
|
+
import { promises as fs } from 'node:fs'
|
|
2
|
+
import path from 'node:path'
|
|
3
|
+
import { knlGlobalDir } from '../config.js'
|
|
4
|
+
|
|
5
|
+
export const SUPPORTED_ECOSYSTEMS = new Set(['npm', 'go', 'cargo', 'pypi'])
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Validate that a module name does not contain traversal tokens, null bytes, or invalid paths.
|
|
9
|
+
*/
|
|
10
|
+
export function isValidModuleName(name) {
|
|
11
|
+
if (!name || typeof name !== 'string') return false
|
|
12
|
+
if (name.includes('\0')) return false
|
|
13
|
+
const segments = name.replace(/\\/g, '/').split('/')
|
|
14
|
+
for (const seg of segments) {
|
|
15
|
+
if (seg === '.' || seg === '..' || !seg) return false
|
|
16
|
+
}
|
|
17
|
+
return true
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Returns the path to the global modules catalog directory (~/.knlview/modules).
|
|
22
|
+
*/
|
|
23
|
+
export function modulesCatalogDir(home = null, options = {}) {
|
|
24
|
+
const base = knlGlobalDir(home, options)
|
|
25
|
+
return path.join(base, 'modules')
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Default global AnchorDB model name for an ecosystem.
|
|
30
|
+
*/
|
|
31
|
+
export function defaultEcosystemModel(ecosystem) {
|
|
32
|
+
const eco = String(ecosystem || '').toLowerCase()
|
|
33
|
+
if (!SUPPORTED_ECOSYSTEMS.has(eco)) {
|
|
34
|
+
throw new Error(`Unsupported ecosystem "${ecosystem}". Supported: ${[...SUPPORTED_ECOSYSTEMS].join(', ')}`)
|
|
35
|
+
}
|
|
36
|
+
return `modules-${eco}`
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export function formatModuleKey(pkgName, version) {
|
|
40
|
+
return `${pkgName}@${version}`
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export function formatExternalPackageNodeId(ecosystem, pkgName, version) {
|
|
44
|
+
return `ext:${ecosystem}/${pkgName}@${version}`
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export function formatExternalSymbolNodeId(ecosystem, pkgName, version, symbol) {
|
|
48
|
+
return `ext:${ecosystem}/${pkgName}@${version}#${symbol}`
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Read the catalog for a specific ecosystem ('npm', 'go', 'cargo', 'pypi').
|
|
53
|
+
*/
|
|
54
|
+
export async function readModulesCatalog(ecosystem, options = {}) {
|
|
55
|
+
const eco = String(ecosystem || '').toLowerCase()
|
|
56
|
+
if (!SUPPORTED_ECOSYSTEMS.has(eco)) return {}
|
|
57
|
+
const dir = modulesCatalogDir(options.home, options)
|
|
58
|
+
const file = path.join(dir, `${eco}.json`)
|
|
59
|
+
try {
|
|
60
|
+
const raw = await fs.readFile(file, 'utf8')
|
|
61
|
+
const parsed = JSON.parse(raw)
|
|
62
|
+
return parsed && typeof parsed === 'object' ? parsed : {}
|
|
63
|
+
} catch {
|
|
64
|
+
return {}
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Check if a module version is already indexed in the catalog.
|
|
70
|
+
*/
|
|
71
|
+
export async function isModuleSynced(ecosystem, pkgName, version, options = {}) {
|
|
72
|
+
const catalog = await readModulesCatalog(ecosystem, options)
|
|
73
|
+
const key = formatModuleKey(pkgName, version)
|
|
74
|
+
return Boolean(catalog[key]?.syncedAt)
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Record discovered modules and symbols to ~/.knlview/modules/<ecosystem>.json.
|
|
79
|
+
* Stores lightweight metadata and symbolCount to avoid bloat in the local JSON catalog.
|
|
80
|
+
*/
|
|
81
|
+
export async function updateModulesCatalog(ecosystem, entries, options = {}) {
|
|
82
|
+
const eco = String(ecosystem || '').toLowerCase()
|
|
83
|
+
if (!SUPPORTED_ECOSYSTEMS.has(eco)) return
|
|
84
|
+
if (!entries || !Object.keys(entries).length) return
|
|
85
|
+
const dir = modulesCatalogDir(options.home, options)
|
|
86
|
+
const file = path.join(dir, `${eco}.json`)
|
|
87
|
+
try {
|
|
88
|
+
await fs.mkdir(dir, { recursive: true })
|
|
89
|
+
const existing = await readModulesCatalog(eco, options)
|
|
90
|
+
for (const [key, item] of Object.entries(entries)) {
|
|
91
|
+
const prev = existing[key] || {}
|
|
92
|
+
const symbolCount = item.symbolCount !== undefined
|
|
93
|
+
? item.symbolCount
|
|
94
|
+
: Array.isArray(item.symbols)
|
|
95
|
+
? item.symbols.length
|
|
96
|
+
: (prev.symbolCount || 0)
|
|
97
|
+
|
|
98
|
+
existing[key] = {
|
|
99
|
+
name: item.name,
|
|
100
|
+
version: item.version,
|
|
101
|
+
ecosystem: eco,
|
|
102
|
+
model: item.model || prev.model || defaultEcosystemModel(eco),
|
|
103
|
+
nodeId: item.nodeId || prev.nodeId || formatExternalPackageNodeId(eco, item.name, item.version),
|
|
104
|
+
symbolCount,
|
|
105
|
+
syncedAt: item.syncedAt || prev.syncedAt || Date.now()
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
await fs.writeFile(file, JSON.stringify(existing, null, 2) + '\n', 'utf8')
|
|
109
|
+
} catch {
|
|
110
|
+
// Fail gracefully: catalog is an informational index, never fatal.
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Detect project dependency versions from on-disk manifests:
|
|
116
|
+
* package.json, go.mod, Cargo.toml, requirements.txt.
|
|
117
|
+
*/
|
|
118
|
+
export async function detectProjectManifests(rootAbs) {
|
|
119
|
+
const manifests = {
|
|
120
|
+
npm: {},
|
|
121
|
+
go: {},
|
|
122
|
+
cargo: {},
|
|
123
|
+
pypi: {}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// 1. npm (package.json)
|
|
127
|
+
try {
|
|
128
|
+
const pkgRaw = await fs.readFile(path.join(rootAbs, 'package.json'), 'utf8')
|
|
129
|
+
const pkg = JSON.parse(pkgRaw)
|
|
130
|
+
const all = { ...pkg.dependencies, ...pkg.devDependencies, ...pkg.peerDependencies }
|
|
131
|
+
for (const [name, ver] of Object.entries(all)) {
|
|
132
|
+
if (typeof ver === 'string') {
|
|
133
|
+
manifests.npm[name] = ver.replace(/^[\^~>=<]/, '').trim() || 'latest'
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
} catch {}
|
|
137
|
+
|
|
138
|
+
// 2. Go (go.mod)
|
|
139
|
+
try {
|
|
140
|
+
const text = await fs.readFile(path.join(rootAbs, 'go.mod'), 'utf8')
|
|
141
|
+
const singleMatch = text.matchAll(/^require\s+([^\s()]+)\s+([^\s()]+)/gm)
|
|
142
|
+
for (const m of singleMatch) manifests.go[m[1]] = m[2]
|
|
143
|
+
const blockMatch = text.match(/require\s*\(([\s\S]*?)\)/m)
|
|
144
|
+
if (blockMatch) {
|
|
145
|
+
for (const line of blockMatch[1].split('\n')) {
|
|
146
|
+
const parts = line.trim().split(/\s+/)
|
|
147
|
+
if (parts.length >= 2 && !parts[0].startsWith('//')) {
|
|
148
|
+
manifests.go[parts[0]] = parts[1]
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
} catch {}
|
|
153
|
+
|
|
154
|
+
// 3. Rust (Cargo.toml)
|
|
155
|
+
try {
|
|
156
|
+
const text = await fs.readFile(path.join(rootAbs, 'Cargo.toml'), 'utf8')
|
|
157
|
+
let inDeps = false
|
|
158
|
+
for (const line of text.split('\n')) {
|
|
159
|
+
const trimmed = line.trim()
|
|
160
|
+
if (
|
|
161
|
+
trimmed.startsWith('[dependencies]') ||
|
|
162
|
+
trimmed.startsWith('[dev-dependencies]') ||
|
|
163
|
+
trimmed.startsWith('[build-dependencies]')
|
|
164
|
+
) {
|
|
165
|
+
inDeps = true
|
|
166
|
+
continue
|
|
167
|
+
}
|
|
168
|
+
if (trimmed.startsWith('[')) {
|
|
169
|
+
inDeps = false
|
|
170
|
+
continue
|
|
171
|
+
}
|
|
172
|
+
if (inDeps && trimmed && !trimmed.startsWith('#')) {
|
|
173
|
+
const eqIdx = trimmed.indexOf('=')
|
|
174
|
+
if (eqIdx > 0) {
|
|
175
|
+
const name = trimmed.slice(0, eqIdx).trim()
|
|
176
|
+
const val = trimmed.slice(eqIdx + 1).trim()
|
|
177
|
+
let ver = 'unknown'
|
|
178
|
+
const strMatch = val.match(/^"([^"]+)"/)
|
|
179
|
+
if (strMatch) ver = strMatch[1]
|
|
180
|
+
const objMatch = val.match(/version\s*=\s*"([^"]+)"/)
|
|
181
|
+
if (objMatch) ver = objMatch[1]
|
|
182
|
+
manifests.cargo[name] = ver
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
} catch {}
|
|
187
|
+
|
|
188
|
+
// 4. Python (requirements.txt)
|
|
189
|
+
try {
|
|
190
|
+
const reqText = await fs.readFile(path.join(rootAbs, 'requirements.txt'), 'utf8')
|
|
191
|
+
for (const line of reqText.split('\n')) {
|
|
192
|
+
const trimmed = line.trim()
|
|
193
|
+
if (!trimmed || trimmed.startsWith('#')) continue
|
|
194
|
+
const m = trimmed.match(/^([a-zA-Z0-9_.-]+)(?:[=<>~!\s]*([a-zA-Z0-9_.-]+))?/)
|
|
195
|
+
if (m) manifests.pypi[m[1].toLowerCase()] = m[2] || 'unknown'
|
|
196
|
+
}
|
|
197
|
+
} catch {}
|
|
198
|
+
|
|
199
|
+
return manifests
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
/**
|
|
203
|
+
* Resolve the directory of an installed external module on disk.
|
|
204
|
+
* Supports npm (node_modules), Go (GOPATH/pkg/mod or vendor), Cargo (cargo cache or vendor),
|
|
205
|
+
* and Python (venv site-packages).
|
|
206
|
+
*/
|
|
207
|
+
export async function resolveModuleLocation(ecosystem, moduleName, options = {}) {
|
|
208
|
+
const eco = String(ecosystem || '').toLowerCase()
|
|
209
|
+
if (!SUPPORTED_ECOSYSTEMS.has(eco) || !isValidModuleName(moduleName)) return null
|
|
210
|
+
const root = path.resolve(options.projectRoot || process.cwd())
|
|
211
|
+
const manifests = options.manifests || await detectProjectManifests(root)
|
|
212
|
+
|
|
213
|
+
if (eco === 'npm') {
|
|
214
|
+
// 1. Check node_modules in projectRoot and walk up if necessary
|
|
215
|
+
let curr = root
|
|
216
|
+
let foundDir = null
|
|
217
|
+
while (true) {
|
|
218
|
+
const baseModules = path.join(curr, 'node_modules')
|
|
219
|
+
const candidate = path.resolve(baseModules, moduleName)
|
|
220
|
+
if (candidate.startsWith(baseModules + path.sep)) {
|
|
221
|
+
try {
|
|
222
|
+
const stat = await fs.stat(candidate)
|
|
223
|
+
if (stat.isDirectory()) {
|
|
224
|
+
foundDir = candidate
|
|
225
|
+
break
|
|
226
|
+
}
|
|
227
|
+
} catch {}
|
|
228
|
+
}
|
|
229
|
+
const parent = path.dirname(curr)
|
|
230
|
+
if (parent === curr) break
|
|
231
|
+
curr = parent
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
let version = manifests.npm[moduleName] || 'unknown'
|
|
235
|
+
if (foundDir) {
|
|
236
|
+
try {
|
|
237
|
+
const pkgText = await fs.readFile(path.join(foundDir, 'package.json'), 'utf8')
|
|
238
|
+
const pkg = JSON.parse(pkgText)
|
|
239
|
+
if (pkg.version) version = pkg.version
|
|
240
|
+
} catch {}
|
|
241
|
+
return { location: foundDir, version, name: moduleName, ecosystem: eco }
|
|
242
|
+
}
|
|
243
|
+
} else if (eco === 'go') {
|
|
244
|
+
// 1. Check local vendor
|
|
245
|
+
const vendorBase = path.join(root, 'vendor')
|
|
246
|
+
const vendorDir = path.resolve(vendorBase, moduleName)
|
|
247
|
+
if (vendorDir.startsWith(vendorBase + path.sep)) {
|
|
248
|
+
try {
|
|
249
|
+
const stat = await fs.stat(vendorDir)
|
|
250
|
+
if (stat.isDirectory()) {
|
|
251
|
+
return { location: vendorDir, version: manifests.go[moduleName] || 'vendor', name: moduleName, ecosystem: eco }
|
|
252
|
+
}
|
|
253
|
+
} catch {}
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
// 2. Check GOPATH
|
|
257
|
+
const gopath = process.env.GOPATH || path.join(options.home || process.env.HOME || '', 'go')
|
|
258
|
+
const version = manifests.go[moduleName]
|
|
259
|
+
if (version) {
|
|
260
|
+
const modBase = path.join(gopath, 'pkg', 'mod')
|
|
261
|
+
const modDir = path.resolve(modBase, `${moduleName}@${version}`)
|
|
262
|
+
if (modDir.startsWith(modBase + path.sep)) {
|
|
263
|
+
try {
|
|
264
|
+
const stat = await fs.stat(modDir)
|
|
265
|
+
if (stat.isDirectory()) {
|
|
266
|
+
return { location: modDir, version, name: moduleName, ecosystem: eco }
|
|
267
|
+
}
|
|
268
|
+
} catch {}
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
} else if (eco === 'cargo') {
|
|
272
|
+
// 1. Check local vendor
|
|
273
|
+
const vendorBase = path.join(root, 'vendor')
|
|
274
|
+
const vendorDir = path.resolve(vendorBase, moduleName)
|
|
275
|
+
if (vendorDir.startsWith(vendorBase + path.sep)) {
|
|
276
|
+
try {
|
|
277
|
+
const stat = await fs.stat(vendorDir)
|
|
278
|
+
if (stat.isDirectory()) {
|
|
279
|
+
return { location: vendorDir, version: manifests.cargo[moduleName] || 'vendor', name: moduleName, ecosystem: eco }
|
|
280
|
+
}
|
|
281
|
+
} catch {}
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
// 2. Check cargo registry cache (~/.cargo/registry/src)
|
|
285
|
+
const cargoHome = process.env.CARGO_HOME || path.join(options.home || process.env.HOME || '', '.cargo')
|
|
286
|
+
const regSrc = path.join(cargoHome, 'registry', 'src')
|
|
287
|
+
const version = manifests.cargo[moduleName]
|
|
288
|
+
try {
|
|
289
|
+
const registries = await fs.readdir(regSrc)
|
|
290
|
+
for (const reg of registries) {
|
|
291
|
+
const targetPrefix = version ? `${moduleName}-${version}` : `${moduleName}-`
|
|
292
|
+
const regDir = path.join(regSrc, reg)
|
|
293
|
+
const entries = await fs.readdir(regDir)
|
|
294
|
+
const match = entries.find((e) => version ? e === targetPrefix : e.startsWith(targetPrefix))
|
|
295
|
+
if (match) {
|
|
296
|
+
const location = path.resolve(regDir, match)
|
|
297
|
+
if (location.startsWith(regDir + path.sep)) {
|
|
298
|
+
const matchedVersion = version || match.slice(moduleName.length + 1)
|
|
299
|
+
return { location, version: matchedVersion, name: moduleName, ecosystem: eco }
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
} catch {}
|
|
304
|
+
} else if (eco === 'pypi') {
|
|
305
|
+
const version = manifests.pypi[moduleName.toLowerCase()] || 'unknown'
|
|
306
|
+
// 1. Check .venv and venv site-packages
|
|
307
|
+
const venvDirs = ['.venv', 'venv']
|
|
308
|
+
for (const venv of venvDirs) {
|
|
309
|
+
const libDir = path.join(root, venv, 'lib')
|
|
310
|
+
try {
|
|
311
|
+
const pyDirs = await fs.readdir(libDir)
|
|
312
|
+
for (const py of pyDirs) {
|
|
313
|
+
const spBase = path.join(libDir, py, 'site-packages')
|
|
314
|
+
const sp = path.resolve(spBase, moduleName)
|
|
315
|
+
if (sp.startsWith(spBase + path.sep)) {
|
|
316
|
+
try {
|
|
317
|
+
const stat = await fs.stat(sp)
|
|
318
|
+
if (stat.isDirectory()) {
|
|
319
|
+
return { location: sp, version, name: moduleName, ecosystem: eco }
|
|
320
|
+
}
|
|
321
|
+
} catch {}
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
} catch {}
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
return null
|
|
329
|
+
}
|
package/server/graph/sources.js
CHANGED
|
@@ -27,8 +27,7 @@ import { createPathMatcher } from '../config.js'
|
|
|
27
27
|
|
|
28
28
|
export const SOURCE_RELATIONS = ['imports', 'imported_by']
|
|
29
29
|
export const SOURCE_TOMBSTONE = 2
|
|
30
|
-
// OpStatus on the wire:
|
|
31
|
-
export const OP_ACTIVE = 1
|
|
30
|
+
// OpStatus on the wire: 2 = Archived (TOMBSTONE).
|
|
32
31
|
export const OP_ARCHIVED = 2
|
|
33
32
|
// Symbol layer, same AnchorDB model as the files: a file defines symbols,
|
|
34
33
|
// symbols call symbols. Mirrored both directions like imports, for the same
|
package/server/graph/syntax.js
CHANGED
|
@@ -78,12 +78,14 @@ function firstDescendant(node, type) {
|
|
|
78
78
|
function specsFromTree(grammar, tree) {
|
|
79
79
|
const specs = []
|
|
80
80
|
const names = []
|
|
81
|
+
const bindings = []
|
|
81
82
|
const visit = (node) => {
|
|
82
83
|
if (grammar === 'javascript' || grammar === 'typescript' || grammar === 'tsx') {
|
|
83
84
|
if (node.type === 'import_statement' || node.type === 'export_statement') {
|
|
84
85
|
const source = node.childForFieldName('source')
|
|
85
86
|
const fragment = source && firstDescendant(source, 'string_fragment')
|
|
86
|
-
|
|
87
|
+
const specText = fragment ? fragment.text : null
|
|
88
|
+
if (specText) specs.push(specText)
|
|
87
89
|
// Local binding names a qualified call may be addressed through:
|
|
88
90
|
// the default import, the namespace alias, and each named import.
|
|
89
91
|
for (let i = 0; i < node.childCount; i++) {
|
|
@@ -91,17 +93,25 @@ function specsFromTree(grammar, tree) {
|
|
|
91
93
|
if (clause.type !== 'import_clause') continue
|
|
92
94
|
for (let j = 0; j < clause.childCount; j++) {
|
|
93
95
|
const part = clause.child(j)
|
|
94
|
-
if (part.type === 'identifier')
|
|
95
|
-
|
|
96
|
+
if (part.type === 'identifier') {
|
|
97
|
+
names.push(part.text)
|
|
98
|
+
if (specText) bindings.push({ name: part.text, spec: specText })
|
|
99
|
+
} else if (part.type === 'namespace_import') {
|
|
96
100
|
for (let k = 0; k < part.childCount; k++) {
|
|
97
|
-
if (part.child(k).type === 'identifier')
|
|
101
|
+
if (part.child(k).type === 'identifier') {
|
|
102
|
+
names.push(part.child(k).text)
|
|
103
|
+
if (specText) bindings.push({ name: part.child(k).text, spec: specText })
|
|
104
|
+
}
|
|
98
105
|
}
|
|
99
106
|
} else if (part.type === 'named_imports') {
|
|
100
107
|
for (let k = 0; k < part.childCount; k++) {
|
|
101
108
|
const specifier = part.child(k)
|
|
102
109
|
if (specifier.type !== 'import_specifier') continue
|
|
103
110
|
const alias = specifier.childForFieldName('alias') || specifier.childForFieldName('name')
|
|
104
|
-
if (alias)
|
|
111
|
+
if (alias) {
|
|
112
|
+
names.push(alias.text)
|
|
113
|
+
if (specText) bindings.push({ name: alias.text, spec: specText })
|
|
114
|
+
}
|
|
105
115
|
}
|
|
106
116
|
}
|
|
107
117
|
}
|
|
@@ -111,7 +121,8 @@ function specsFromTree(grammar, tree) {
|
|
|
111
121
|
if (fn && (fn.text === 'require' || fn.text === 'import')) {
|
|
112
122
|
const args = node.childForFieldName('arguments')
|
|
113
123
|
const fragment = args && firstDescendant(args, 'string_fragment')
|
|
114
|
-
|
|
124
|
+
const specText = fragment ? fragment.text : null
|
|
125
|
+
if (specText) specs.push(specText)
|
|
115
126
|
// `const x = require('y')` / `const x = await import('y')`: the
|
|
116
127
|
// declarator name is the binding: the visit starts above it, so
|
|
117
128
|
// walk down from any enclosing declarator via field lookup here.
|
|
@@ -119,7 +130,10 @@ function specsFromTree(grammar, tree) {
|
|
|
119
130
|
while (parent && parent.type === 'await_expression') parent = parent.parent
|
|
120
131
|
if (parent && parent.type === 'variable_declarator') {
|
|
121
132
|
const name = parent.childForFieldName('name')
|
|
122
|
-
if (name)
|
|
133
|
+
if (name) {
|
|
134
|
+
names.push(name.text)
|
|
135
|
+
if (specText) bindings.push({ name: name.text, spec: specText })
|
|
136
|
+
}
|
|
123
137
|
}
|
|
124
138
|
}
|
|
125
139
|
}
|
|
@@ -133,8 +147,9 @@ function specsFromTree(grammar, tree) {
|
|
|
133
147
|
// The name a qualified call uses: the declared alias, or the last
|
|
134
148
|
// path segment (Go's convention when no alias is given).
|
|
135
149
|
const alias = node.childForFieldName('name')
|
|
136
|
-
|
|
137
|
-
|
|
150
|
+
const name = alias ? alias.text : path.slice(path.lastIndexOf('/') + 1)
|
|
151
|
+
names.push(name)
|
|
152
|
+
bindings.push({ name, spec: path })
|
|
138
153
|
}
|
|
139
154
|
}
|
|
140
155
|
} else if (grammar === 'python') {
|
|
@@ -144,12 +159,18 @@ function specsFromTree(grammar, tree) {
|
|
|
144
159
|
if (c.type === 'dotted_name') {
|
|
145
160
|
specs.push(c.text)
|
|
146
161
|
const firstId = c.child(0)
|
|
147
|
-
if (firstId)
|
|
162
|
+
if (firstId) {
|
|
163
|
+
names.push(firstId.text)
|
|
164
|
+
bindings.push({ name: firstId.text, spec: c.text })
|
|
165
|
+
}
|
|
148
166
|
} else if (c.type === 'aliased_import') {
|
|
149
167
|
const name = c.childForFieldName('name')
|
|
150
168
|
const alias = c.childForFieldName('alias')
|
|
151
169
|
if (name) specs.push(name.text)
|
|
152
|
-
if (alias)
|
|
170
|
+
if (alias) {
|
|
171
|
+
names.push(alias.text)
|
|
172
|
+
bindings.push({ name: alias.text, spec: name ? name.text : alias.text })
|
|
173
|
+
}
|
|
153
174
|
}
|
|
154
175
|
}
|
|
155
176
|
} else if (node.type === 'import_from_statement') {
|
|
@@ -167,10 +188,14 @@ function specsFromTree(grammar, tree) {
|
|
|
167
188
|
if (c.type === 'dotted_name') {
|
|
168
189
|
names.push(c.text)
|
|
169
190
|
if (modText) specs.push(modText + '.' + c.text)
|
|
191
|
+
bindings.push({ name: c.text, spec: modText || c.text })
|
|
170
192
|
} else if (c.type === 'aliased_import') {
|
|
171
193
|
const name = c.childForFieldName('name')
|
|
172
194
|
const alias = c.childForFieldName('alias')
|
|
173
|
-
if (alias)
|
|
195
|
+
if (alias) {
|
|
196
|
+
names.push(alias.text)
|
|
197
|
+
bindings.push({ name: alias.text, spec: modText || (name ? name.text : alias.text) })
|
|
198
|
+
}
|
|
174
199
|
if (name && modText) specs.push(modText + '.' + name.text)
|
|
175
200
|
}
|
|
176
201
|
}
|
|
@@ -182,6 +207,7 @@ function specsFromTree(grammar, tree) {
|
|
|
182
207
|
if (name) {
|
|
183
208
|
specs.push('./' + name.text)
|
|
184
209
|
names.push(name.text)
|
|
210
|
+
bindings.push({ name: name.text, spec: './' + name.text })
|
|
185
211
|
}
|
|
186
212
|
}
|
|
187
213
|
} else if (node.type === 'use_declaration') {
|
|
@@ -204,16 +230,27 @@ function specsFromTree(grammar, tree) {
|
|
|
204
230
|
const aliasNode = n.childForFieldName('alias')
|
|
205
231
|
const full = (prefix ? prefix + '::' : '') + (pathNode ? pathNode.text : '')
|
|
206
232
|
specs.push(full)
|
|
207
|
-
|
|
233
|
+
const alias = aliasNode ? aliasNode.text : null
|
|
234
|
+
if (alias) {
|
|
235
|
+
names.push(alias)
|
|
236
|
+
const crateName = full.split('::')[0]
|
|
237
|
+
bindings.push({ name: alias, spec: crateName, full })
|
|
238
|
+
}
|
|
208
239
|
} else if (n.type === 'scoped_identifier') {
|
|
209
240
|
const full = (prefix ? prefix + '::' : '') + n.text
|
|
210
241
|
specs.push(full)
|
|
211
242
|
const name = n.childForFieldName('name')
|
|
212
|
-
if (name)
|
|
243
|
+
if (name) {
|
|
244
|
+
names.push(name.text)
|
|
245
|
+
const crateName = full.split('::')[0]
|
|
246
|
+
bindings.push({ name: name.text, spec: crateName, full })
|
|
247
|
+
}
|
|
213
248
|
} else if (n.type === 'identifier') {
|
|
214
249
|
const full = (prefix ? prefix + '::' : '') + n.text
|
|
215
250
|
specs.push(full)
|
|
216
251
|
names.push(n.text)
|
|
252
|
+
const crateName = full.split('::')[0]
|
|
253
|
+
bindings.push({ name: n.text, spec: crateName, full })
|
|
217
254
|
}
|
|
218
255
|
}
|
|
219
256
|
extractRustUse(node.childForFieldName('argument'))
|
|
@@ -222,7 +259,7 @@ function specsFromTree(grammar, tree) {
|
|
|
222
259
|
for (let i = 0; i < node.childCount; i++) visit(node.child(i))
|
|
223
260
|
}
|
|
224
261
|
visit(tree.rootNode)
|
|
225
|
-
return { specs, names }
|
|
262
|
+
return { specs, names, bindings }
|
|
226
263
|
}
|
|
227
264
|
|
|
228
265
|
/**
|
|
@@ -262,9 +299,9 @@ export async function extractCode(ext, content) {
|
|
|
262
299
|
const parser = parserFor(grammar, lang)
|
|
263
300
|
const tree = parser.parse(content)
|
|
264
301
|
try {
|
|
265
|
-
const { specs: imports, names: importNames } = specsFromTree(grammar, tree)
|
|
302
|
+
const { specs: imports, names: importNames, bindings: importBindings } = specsFromTree(grammar, tree)
|
|
266
303
|
const { symbols, calls } = structureFromTree(grammar, tree)
|
|
267
|
-
return { imports, importNames, symbols, calls }
|
|
304
|
+
return { imports, importNames, importBindings, symbols, calls }
|
|
268
305
|
} finally {
|
|
269
306
|
tree.delete()
|
|
270
307
|
}
|
package/server/scaffold.js
CHANGED
|
@@ -90,9 +90,10 @@ export function buildAgentInstructionBlock(hasGraphify = false) {
|
|
|
90
90
|
|
|
91
91
|
return `<!-- knl-view:start -->
|
|
92
92
|
# Knowledge graph tools
|
|
93
|
-
- KNL-View is the primary graph tool. For \`.knowledges\`, source graphs, and project graph questions, read the project's \`knl-view\` skill and use the relevant KNL-View feature first.
|
|
93
|
+
- KNL-View is the primary graph tool. For \`.knowledges\`, source graphs, external module graphs, and project graph questions, read the project's \`knl-view\` skill and use the relevant KNL-View feature first.
|
|
94
94
|
${fallback}## KNL-View graph access
|
|
95
|
-
- Use the KNL-View feature that fits the task (\`graph trace\`, \`sources trace/sync/collect\`, \`model check/new\`, consensus commands, or the viewer). Do not approximate a supported KNL-View operation with ad-hoc graph queries.
|
|
95
|
+
- Use the KNL-View feature that fits the task (\`graph trace\`, \`sources trace/sync/collect\`, \`modules build-graph/trace/list\`, \`model check/new\`, consensus commands, or the viewer). Do not approximate a supported KNL-View operation with ad-hoc graph queries.
|
|
96
|
+
- For third-party libraries and dependencies (\`node_modules\`, \`go mod\`, \`site-packages\`, \`crates\`): NEVER read raw library files directly. Build and query the module graph on-demand: run \`knl-view modules build-graph <eco>::<module_name>\` (e.g. \`npm::express\`, \`go::github.com/gin-gonic/gin\`), then inspect API definitions and call hierarchies via \`knl-view modules trace <eco>::<module_name> [symbol]\` to save context tokens.
|
|
96
97
|
- Route all graph reads and writes through KNL-View's documented CLI, viewer, or application endpoints. Never connect to AnchorDB directly through AnchorWire, its CLI, direct database endpoints, files, SQL, or custom clients/scripts. If KNL-View cannot perform a requested operation, explain the limitation; do not bypass it.
|
|
97
98
|
- Keep graph work targeted: specify the relevant node/path, direction, and smallest useful hop count; summarize only relevant nodes and edges; avoid dumping whole graphs or large raw JSON.
|
|
98
99
|
- Follow the KNL-View skill's trace-before-edit and \`sources sync\`-after-change rules. Treat edge direction as lineage; do not claim causality without separate evidence.
|
|
@@ -196,7 +197,7 @@ async function statOrNull(abs) {
|
|
|
196
197
|
// structure.
|
|
197
198
|
const SKILL_MD = `---
|
|
198
199
|
name: knl-view
|
|
199
|
-
description: Read and maintain .knowledges knowledge bases. Use whenever a task involves
|
|
200
|
+
description: Read and maintain .knowledges knowledge bases, project source dependency graphs, and on-demand external module boundary graphs (npm, go, cargo, pypi). Use whenever a task involves project architecture, dependencies, third-party libraries, .knowledges, or the knl-view viewer.
|
|
200
201
|
---
|
|
201
202
|
|
|
202
203
|
# Knowledges base
|
|
@@ -366,6 +367,12 @@ file or symbol is tombstoned (attribute \`tombstone = 2\`) after its
|
|
|
366
367
|
edges are unlinked; if it comes back, the tombstone lifts and the edges
|
|
367
368
|
are relinked.
|
|
368
369
|
|
|
370
|
+
External modules graph (third-party dependencies): stored in global AnchorDB
|
|
371
|
+
models per ecosystem (\`modules-npm\`, \`modules-go\`, \`modules-cargo\`, \`modules-pypi\`)
|
|
372
|
+
and cataloged in \`~/.knlview/modules/<eco>.json\`. Built on-demand via
|
|
373
|
+
\`knl-view modules build-graph <eco>::<name>\`, avoiding polluting project models.
|
|
374
|
+
Traceable via \`knl-view modules trace <eco>::<name> [symbol]\`.
|
|
375
|
+
|
|
369
376
|
Decision history skill: recording architectural decisions is delegated
|
|
370
377
|
to the separate decision-history skill (its decision block format is its
|
|
371
378
|
own domain; this skill never writes decision blocks). If it is not
|
|
@@ -454,6 +461,10 @@ this.
|
|
|
454
461
|
- Before renaming a symbol or moving it to another file: remember its
|
|
455
462
|
record id contains the old name/path. Sync tombstones the stale record
|
|
456
463
|
and creates the new one; trace by the new id afterwards.
|
|
464
|
+
- Before reading raw third-party library source files:
|
|
465
|
+
run \`knl-view modules build-graph <eco>::<module>\` (e.g. \`npm::express\`, \`go::github.com/gin-gonic/gin\`)
|
|
466
|
+
to index it on-demand into global AnchorDB model \`modules-<eco>\`, then trace
|
|
467
|
+
its methods with \`knl-view modules trace <eco>::<module> [symbol]\` to save context tokens.
|
|
457
468
|
- A new architectural decision: trigger the decision-history skill
|
|
458
469
|
(install it first if missing; see "Decision history skill" above).
|
|
459
470
|
The decision block format is that skill's domain; this skill never
|