@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
@@ -3,6 +3,7 @@ import path from 'node:path'
3
3
  import { GraphNode } from './model.js'
4
4
  import { edgeKey } from './anchordb_client.js'
5
5
  import { SYNTAX_LANGUAGES, extractCode } from './syntax.js'
6
+ import { createPathMatcher } from '../config.js'
6
7
 
7
8
  // Sources graph: the whole project as a dependency graph in AnchorDB, not
8
9
  // just the .knowledges markdown. Nodes are the files (id = repo relative
@@ -47,7 +48,7 @@ const REVERSE_EDGE = {
47
48
  const DEFAULT_LABEL_DIR = '.knowledges/.knl-data'
48
49
  const DEFAULT_IGNORES = ['node_modules', 'dist', 'build', 'coverage']
49
50
 
50
- const JS_EXTS = new Set(['.js', '.mjs', '.cjs'])
51
+ const JS_TS_EXTS = new Set(['.js', '.mjs', '.cjs', '.jsx', '.ts', '.mts', '.cts', '.tsx'])
51
52
 
52
53
  /** One ignore rule after parsing a .gitignore line. */
53
54
  function ignoreRule(line) {
@@ -72,27 +73,42 @@ function fileAttrs(stat) {
72
73
  * graph: { rel -> { size, mtime } }. Skips dot-prefixed folders except
73
74
  * .github, the default ignore list, and basic .gitignore patterns.
74
75
  */
75
- export async function scanSourceFiles(rootAbs) {
76
+ function compileIgnorePattern(pattern) {
77
+ if (!pattern || !pattern.includes('*')) return null
78
+ const escaped = pattern.replace(/[.+?^${}()|[\]\\]/g, '\\$&').replace(/\*/g, '[^/]*')
79
+ try {
80
+ return new RegExp('^' + escaped + '$')
81
+ } catch {
82
+ return null
83
+ }
84
+ }
85
+
86
+ export async function scanSourceFiles(rootAbs, { include = null, exclude = null } = {}) {
87
+ const excludeFilter = createPathMatcher(exclude)
88
+ const includeFilter = createPathMatcher(include)
89
+
76
90
  const rules = []
77
91
  try {
78
92
  const gitignore = await fs.readFile(path.join(rootAbs, '.gitignore'), 'utf8')
79
93
  for (const line of gitignore.split('\n')) {
80
94
  const rule = ignoreRule(line)
81
- if (rule) rules.push(rule)
95
+ if (rule) {
96
+ rule.regex = compileIgnorePattern(rule.pattern)
97
+ rules.push(rule)
98
+ }
82
99
  }
83
100
  } catch {
84
101
  // No .gitignore: the default list still applies.
85
102
  }
86
- rules.push(...DEFAULT_IGNORES.map((pattern) => ({ dirOnly: true, pattern })))
103
+ rules.push(...DEFAULT_IGNORES.map((pattern) => ({ dirOnly: true, pattern, regex: compileIgnorePattern(pattern) })))
87
104
 
88
- const ignored = (rel) => {
105
+ const isDefaultIgnored = (rel, isDir) => {
89
106
  const segments = rel.split('/')
90
107
  if (segments.some((s) => s.startsWith('.') && s !== '.github')) return true
91
108
  return segments.some((segment) =>
92
109
  rules.some((rule) => {
93
- if (rule.pattern.includes('*')) {
94
- const re = new RegExp('^' + rule.pattern.replace(/\*/g, '[^/]*') + '$')
95
- return re.test(segment) || re.test(rel)
110
+ if (rule.regex) {
111
+ return rule.regex.test(segment) || rule.regex.test(rel)
96
112
  }
97
113
  if (rule.dirOnly) return segments.includes(rule.pattern)
98
114
  return segment === rule.pattern || rel === rule.pattern
@@ -109,15 +125,39 @@ export async function scanSourceFiles(rootAbs) {
109
125
  return
110
126
  }
111
127
  for (const entry of entries) {
112
- if (entry.name.startsWith('.') && entry.name !== '.github') continue
113
128
  const childRel = rel ? `${rel}/${entry.name}` : entry.name
114
- if (ignored(childRel)) continue
115
- const childAbs = path.join(dirAbs, entry.name)
129
+
130
+ // 1. Hard exclude: knl-config exclude is truly excluded, always wins
131
+ if (excludeFilter && excludeFilter.matches(childRel, entry.name)) {
132
+ continue
133
+ }
134
+
135
+ // 2. Explicit include overrides default excludes
136
+ const isExplicitlyIncluded = includeFilter && includeFilter.matches(childRel, entry.name)
137
+ const isDirPrefixOfInclude = entry.isDirectory() && includeFilter && includeFilter.isPrefixOfAny(childRel)
138
+
116
139
  if (entry.isDirectory()) {
117
- await walk(childAbs, childRel)
140
+ if (!isExplicitlyIncluded && !isDirPrefixOfInclude) {
141
+ if (includeFilter) {
142
+ continue
143
+ }
144
+ if (isDefaultIgnored(childRel, true)) {
145
+ continue
146
+ }
147
+ }
148
+ await walk(path.join(dirAbs, entry.name), childRel)
118
149
  continue
119
150
  }
151
+
120
152
  if (!entry.isFile()) continue
153
+
154
+ if (includeFilter) {
155
+ if (!isExplicitlyIncluded) continue
156
+ } else {
157
+ if (isDefaultIgnored(childRel, false)) continue
158
+ }
159
+
160
+ const childAbs = path.join(dirAbs, entry.name)
121
161
  try {
122
162
  files[childRel] = fileAttrs(await fs.stat(childAbs))
123
163
  } catch {
@@ -175,17 +215,108 @@ export function resolveGoSpecifiers(spec, modulePath, files) {
175
215
 
176
216
  /**
177
217
  * Resolve one relative specifier from file `fromRel` against the set of
178
- * scanned files. Tries the exact path, the JS extensions, and an index
179
- * file inside the directory.
218
+ * scanned files. Tries the exact path, JS/TS extension fallbacks (including
219
+ * mapping .js specifiers in TS ESM to .ts/.tsx on disk), and directory index files.
180
220
  */
181
221
  export function resolveImportSpecifier(fromRel, spec, files) {
182
222
  const base = path.posix.normalize(path.posix.join(path.posix.dirname(fromRel), spec))
183
223
  const candidates = [base]
184
- for (const ext of JS_EXTS) candidates.push(base + ext)
185
- for (const ext of JS_EXTS) candidates.push(`${base}/index${ext}`)
224
+ if (base.endsWith('.js')) {
225
+ candidates.push(base.slice(0, -3) + '.ts', base.slice(0, -3) + '.tsx')
226
+ } else if (base.endsWith('.mjs')) {
227
+ candidates.push(base.slice(0, -4) + '.mts')
228
+ } else if (base.endsWith('.cjs')) {
229
+ candidates.push(base.slice(0, -4) + '.cts')
230
+ }
231
+ for (const ext of JS_TS_EXTS) candidates.push(base + ext)
232
+ for (const ext of JS_TS_EXTS) candidates.push(`${base}/index${ext}`)
186
233
  return candidates.find((candidate) => files[candidate]) || null
187
234
  }
188
235
 
236
+ /**
237
+ * Resolve Python import specifiers: relative dots (e.g. .local, ..utils) and
238
+ * module paths (e.g. pkg.sub) to .py or /__init__.py files in scanned files.
239
+ */
240
+ export function resolvePySpecifiers(fromRel, specs, files) {
241
+ const dir = path.posix.dirname(fromRel)
242
+ const resolved = []
243
+ for (const spec of specs) {
244
+ const candidates = []
245
+ const m = spec.match(/^(\.+)(.*)$/)
246
+ if (m) {
247
+ const dotCount = m[1].length
248
+ const rest = m[2].replace(/\./g, '/')
249
+ let targetDir = dir
250
+ for (let i = 1; i < dotCount; i++) targetDir = path.posix.dirname(targetDir)
251
+ const base = rest ? path.posix.normalize(path.posix.join(targetDir, rest)) : targetDir
252
+ candidates.push(base + '.py', path.posix.join(base, '__init__.py'))
253
+ } else {
254
+ const asPath = spec.replace(/\./g, '/')
255
+ candidates.push(
256
+ asPath + '.py',
257
+ path.posix.join(asPath, '__init__.py'),
258
+ path.posix.normalize(path.posix.join(dir, asPath)) + '.py',
259
+ path.posix.normalize(path.posix.join(dir, asPath, '__init__.py'))
260
+ )
261
+ }
262
+ const found = candidates.find((c) => files[c])
263
+ if (found && !resolved.includes(found)) resolved.push(found)
264
+ }
265
+ return resolved
266
+ }
267
+
268
+ /**
269
+ * Resolve Rust import specifiers: `crate::`, `super::`, `self::`, and `./`
270
+ * (from `mod foo;`) to .rs or /mod.rs files in scanned files.
271
+ */
272
+ export function resolveRustSpecifiers(fromRel, specs, files) {
273
+ const dir = path.posix.dirname(fromRel)
274
+ let crateRoot = '.'
275
+ const srcIdx = fromRel.indexOf('src/')
276
+ if (srcIdx >= 0) {
277
+ crateRoot = fromRel.slice(0, srcIdx + 3)
278
+ }
279
+ const resolved = []
280
+ for (const spec of specs) {
281
+ const candidates = []
282
+ if (spec.startsWith('crate::')) {
283
+ const rest = spec.slice('crate::'.length).split('::').join('/')
284
+ const parts = rest.split('/')
285
+ for (let i = parts.length; i >= 1; i--) {
286
+ const sub = parts.slice(0, i).join('/')
287
+ candidates.push(
288
+ path.posix.normalize(path.posix.join(crateRoot, sub)) + '.rs',
289
+ path.posix.normalize(path.posix.join(crateRoot, sub, 'mod.rs'))
290
+ )
291
+ }
292
+ } else if (spec.startsWith('super::')) {
293
+ const rest = spec.slice('super::'.length).split('::').join('/')
294
+ const parentDir = path.posix.dirname(dir)
295
+ const parts = rest.split('/')
296
+ for (let i = parts.length; i >= 1; i--) {
297
+ const sub = parts.slice(0, i).join('/')
298
+ candidates.push(
299
+ path.posix.normalize(path.posix.join(parentDir, sub)) + '.rs',
300
+ path.posix.normalize(path.posix.join(parentDir, sub, 'mod.rs'))
301
+ )
302
+ }
303
+ } else if (spec.startsWith('self::') || spec.startsWith('./')) {
304
+ const raw = spec.startsWith('self::') ? spec.slice('self::'.length).split('::').join('/') : spec.slice(2)
305
+ const parts = raw.split('/')
306
+ for (let i = parts.length; i >= 1; i--) {
307
+ const sub = parts.slice(0, i).join('/')
308
+ candidates.push(
309
+ path.posix.normalize(path.posix.join(dir, sub)) + '.rs',
310
+ path.posix.normalize(path.posix.join(dir, sub, 'mod.rs'))
311
+ )
312
+ }
313
+ }
314
+ const found = candidates.find((c) => files[c])
315
+ if (found && !resolved.includes(found)) resolved.push(found)
316
+ }
317
+ return resolved
318
+ }
319
+
189
320
  /**
190
321
  * Build the desired sources graph: nodes for every scanned file plus one
191
322
  * node per named symbol, and edges from tree-sitter extraction (file-level
@@ -193,8 +324,8 @@ export function resolveImportSpecifier(fromRel, spec, files) {
193
324
  * `<path>#<name>`; class methods are `Class.method`. Returns plain arrays
194
325
  * so callers can diff without depending on the DirectedGraph type.
195
326
  */
196
- export async function buildSourcesGraph(rootAbs, files = null) {
197
- const scanned = files || await scanSourceFiles(rootAbs)
327
+ export async function buildSourcesGraph(rootAbs, files = null, options = {}) {
328
+ const scanned = files || await scanSourceFiles(rootAbs, options)
198
329
  const modulePath = await goModulePath(rootAbs)
199
330
  const edges = []
200
331
  const symbolIndex = new Map() // symbol name -> [node id, ...] for call resolution
@@ -241,9 +372,16 @@ export async function buildSourcesGraph(rootAbs, files = null) {
241
372
  }
242
373
 
243
374
  for (const [rel, file] of Object.entries(structure)) {
244
- const targets = file.ext === '.go'
245
- ? file.imports.flatMap((spec) => resolveGoSpecifiers(spec, file.modulePath, scanned))
246
- : resolveJsSpecifiers(rel, file.imports, scanned)
375
+ let targets = []
376
+ if (file.ext === '.go') {
377
+ targets = file.imports.flatMap((spec) => resolveGoSpecifiers(spec, file.modulePath, scanned))
378
+ } else if (file.ext === '.py') {
379
+ targets = resolvePySpecifiers(rel, file.imports, scanned)
380
+ } else if (file.ext === '.rs') {
381
+ targets = resolveRustSpecifiers(rel, file.imports, scanned)
382
+ } else {
383
+ targets = resolveJsSpecifiers(rel, file.imports, scanned)
384
+ }
247
385
  for (const target of targets) {
248
386
  if (target && target !== rel) edges.push({ from: rel, to: target, type: 'imports' })
249
387
  }
@@ -359,7 +497,10 @@ export function labelContent(rel, symbols) {
359
497
  * symbol is gone. Generated files only; never touches anything else.
360
498
  */
361
499
  export async function writeLabelFiles(rootAbs, labelDir = DEFAULT_LABEL_DIR, symbolsByFile) {
362
- const base = path.join(rootAbs, labelDir)
500
+ const base = path.resolve(rootAbs, labelDir)
501
+ if (base !== rootAbs && !base.startsWith(rootAbs + path.sep)) {
502
+ throw new Error(`labelDir "${labelDir}" resolves outside the project root`)
503
+ }
363
504
  const expected = new Map()
364
505
  for (const [rel, symbols] of symbolsByFile) expected.set(rel, labelContent(rel, symbols))
365
506
 
@@ -423,8 +564,8 @@ export async function writeLabelFiles(rootAbs, labelDir = DEFAULT_LABEL_DIR, sym
423
564
  * @returns {Promise<{linked, unlinked, saved, tombstoned, failed, orphans,
424
565
  * files, symbols, edges, labels}>}
425
566
  */
426
- export async function syncSourcesToAnchorDB(client, rootAbs, { labelDir } = {}) {
427
- const { nodes, edges, symbolsByFile } = await buildSourcesGraph(rootAbs)
567
+ export async function syncSourcesToAnchorDB(client, rootAbs, { labelDir, include = null, exclude = null } = {}) {
568
+ const { nodes, edges, symbolsByFile } = await buildSourcesGraph(rootAbs, null, { include, exclude })
428
569
  const desired = desiredSourceEdgeKeys(edges)
429
570
  const diskIds = new Set(nodes.map((node) => node.id))
430
571
 
@@ -15,15 +15,26 @@ const require = createRequire(import.meta.url)
15
15
 
16
16
  const GRAMMAR_WASM = {
17
17
  javascript: 'tree-sitter-javascript/tree-sitter-javascript.wasm',
18
- go: 'tree-sitter-go/tree-sitter-go.wasm'
18
+ typescript: 'tree-sitter-typescript/tree-sitter-typescript.wasm',
19
+ tsx: 'tree-sitter-typescript/tree-sitter-tsx.wasm',
20
+ go: 'tree-sitter-go/tree-sitter-go.wasm',
21
+ python: 'tree-sitter-python/tree-sitter-python.wasm',
22
+ rust: 'tree-sitter-rust/tree-sitter-rust.wasm'
19
23
  }
20
24
 
21
- /** The JS family shares one grammar; each other extension has its own. */
25
+ /** The JS/TS, Go, Python, and Rust families; each extension maps to its grammar. */
22
26
  export const SYNTAX_LANGUAGES = {
23
27
  '.js': 'javascript',
24
28
  '.mjs': 'javascript',
25
29
  '.cjs': 'javascript',
26
- '.go': 'go'
30
+ '.jsx': 'javascript',
31
+ '.ts': 'typescript',
32
+ '.mts': 'typescript',
33
+ '.cts': 'typescript',
34
+ '.tsx': 'tsx',
35
+ '.go': 'go',
36
+ '.py': 'python',
37
+ '.rs': 'rust'
27
38
  }
28
39
 
29
40
  let initPromise = null
@@ -67,12 +78,14 @@ function firstDescendant(node, type) {
67
78
  function specsFromTree(grammar, tree) {
68
79
  const specs = []
69
80
  const names = []
81
+ const bindings = []
70
82
  const visit = (node) => {
71
- if (grammar === 'javascript') {
83
+ if (grammar === 'javascript' || grammar === 'typescript' || grammar === 'tsx') {
72
84
  if (node.type === 'import_statement' || node.type === 'export_statement') {
73
85
  const source = node.childForFieldName('source')
74
86
  const fragment = source && firstDescendant(source, 'string_fragment')
75
- if (fragment) specs.push(fragment.text)
87
+ const specText = fragment ? fragment.text : null
88
+ if (specText) specs.push(specText)
76
89
  // Local binding names a qualified call may be addressed through:
77
90
  // the default import, the namespace alias, and each named import.
78
91
  for (let i = 0; i < node.childCount; i++) {
@@ -80,17 +93,25 @@ function specsFromTree(grammar, tree) {
80
93
  if (clause.type !== 'import_clause') continue
81
94
  for (let j = 0; j < clause.childCount; j++) {
82
95
  const part = clause.child(j)
83
- if (part.type === 'identifier') names.push(part.text)
84
- else if (part.type === 'namespace_import') {
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') {
85
100
  for (let k = 0; k < part.childCount; k++) {
86
- if (part.child(k).type === 'identifier') names.push(part.child(k).text)
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
+ }
87
105
  }
88
106
  } else if (part.type === 'named_imports') {
89
107
  for (let k = 0; k < part.childCount; k++) {
90
108
  const specifier = part.child(k)
91
109
  if (specifier.type !== 'import_specifier') continue
92
110
  const alias = specifier.childForFieldName('alias') || specifier.childForFieldName('name')
93
- if (alias) names.push(alias.text)
111
+ if (alias) {
112
+ names.push(alias.text)
113
+ if (specText) bindings.push({ name: alias.text, spec: specText })
114
+ }
94
115
  }
95
116
  }
96
117
  }
@@ -100,7 +121,8 @@ function specsFromTree(grammar, tree) {
100
121
  if (fn && (fn.text === 'require' || fn.text === 'import')) {
101
122
  const args = node.childForFieldName('arguments')
102
123
  const fragment = args && firstDescendant(args, 'string_fragment')
103
- if (fragment) specs.push(fragment.text)
124
+ const specText = fragment ? fragment.text : null
125
+ if (specText) specs.push(specText)
104
126
  // `const x = require('y')` / `const x = await import('y')`: the
105
127
  // declarator name is the binding: the visit starts above it, so
106
128
  // walk down from any enclosing declarator via field lookup here.
@@ -108,7 +130,10 @@ function specsFromTree(grammar, tree) {
108
130
  while (parent && parent.type === 'await_expression') parent = parent.parent
109
131
  if (parent && parent.type === 'variable_declarator') {
110
132
  const name = parent.childForFieldName('name')
111
- if (name) names.push(name.text)
133
+ if (name) {
134
+ names.push(name.text)
135
+ if (specText) bindings.push({ name: name.text, spec: specText })
136
+ }
112
137
  }
113
138
  }
114
139
  }
@@ -122,15 +147,119 @@ function specsFromTree(grammar, tree) {
122
147
  // The name a qualified call uses: the declared alias, or the last
123
148
  // path segment (Go's convention when no alias is given).
124
149
  const alias = node.childForFieldName('name')
125
- if (alias) names.push(alias.text)
126
- else names.push(path.slice(path.lastIndexOf('/') + 1))
150
+ const name = alias ? alias.text : path.slice(path.lastIndexOf('/') + 1)
151
+ names.push(name)
152
+ bindings.push({ name, spec: path })
153
+ }
154
+ }
155
+ } else if (grammar === 'python') {
156
+ if (node.type === 'import_statement') {
157
+ for (let i = 0; i < node.childCount; i++) {
158
+ const c = node.child(i)
159
+ if (c.type === 'dotted_name') {
160
+ specs.push(c.text)
161
+ const firstId = c.child(0)
162
+ if (firstId) {
163
+ names.push(firstId.text)
164
+ bindings.push({ name: firstId.text, spec: c.text })
165
+ }
166
+ } else if (c.type === 'aliased_import') {
167
+ const name = c.childForFieldName('name')
168
+ const alias = c.childForFieldName('alias')
169
+ if (name) specs.push(name.text)
170
+ if (alias) {
171
+ names.push(alias.text)
172
+ bindings.push({ name: alias.text, spec: name ? name.text : alias.text })
173
+ }
174
+ }
175
+ }
176
+ } else if (node.type === 'import_from_statement') {
177
+ const mod = node.childForFieldName('module_name')
178
+ const modText = mod ? mod.text : ''
179
+ if (modText) specs.push(modText)
180
+ let pastImport = false
181
+ for (let i = 0; i < node.childCount; i++) {
182
+ const c = node.child(i)
183
+ if (c.type === 'import') {
184
+ pastImport = true
185
+ continue
186
+ }
187
+ if (!pastImport) continue
188
+ if (c.type === 'dotted_name') {
189
+ names.push(c.text)
190
+ if (modText) specs.push(modText + '.' + c.text)
191
+ bindings.push({ name: c.text, spec: modText || c.text })
192
+ } else if (c.type === 'aliased_import') {
193
+ const name = c.childForFieldName('name')
194
+ const alias = c.childForFieldName('alias')
195
+ if (alias) {
196
+ names.push(alias.text)
197
+ bindings.push({ name: alias.text, spec: modText || (name ? name.text : alias.text) })
198
+ }
199
+ if (name && modText) specs.push(modText + '.' + name.text)
200
+ }
201
+ }
202
+ }
203
+ } else if (grammar === 'rust') {
204
+ if (node.type === 'mod_item') {
205
+ if (!node.childForFieldName('body')) {
206
+ const name = node.childForFieldName('name')
207
+ if (name) {
208
+ specs.push('./' + name.text)
209
+ names.push(name.text)
210
+ bindings.push({ name: name.text, spec: './' + name.text })
211
+ }
212
+ }
213
+ } else if (node.type === 'use_declaration') {
214
+ const extractRustUse = (n, prefix = '') => {
215
+ if (!n) return
216
+ if (n.type === 'scoped_use_list') {
217
+ const pathNode = n.childForFieldName('path')
218
+ const listNode = n.childForFieldName('list')
219
+ const p = (prefix ? prefix + '::' : '') + (pathNode ? pathNode.text : '')
220
+ extractRustUse(listNode, p)
221
+ } else if (n.type === 'use_list') {
222
+ for (let i = 0; i < n.childCount; i++) {
223
+ const c = n.child(i)
224
+ if (c.type === 'identifier' || c.type === 'scoped_identifier' || c.type === 'use_as_clause' || c.type === 'scoped_use_list') {
225
+ extractRustUse(c, prefix)
226
+ }
227
+ }
228
+ } else if (n.type === 'use_as_clause') {
229
+ const pathNode = n.childForFieldName('path')
230
+ const aliasNode = n.childForFieldName('alias')
231
+ const full = (prefix ? prefix + '::' : '') + (pathNode ? pathNode.text : '')
232
+ specs.push(full)
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
+ }
239
+ } else if (n.type === 'scoped_identifier') {
240
+ const full = (prefix ? prefix + '::' : '') + n.text
241
+ specs.push(full)
242
+ const name = n.childForFieldName('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
+ }
248
+ } else if (n.type === 'identifier') {
249
+ const full = (prefix ? prefix + '::' : '') + n.text
250
+ specs.push(full)
251
+ names.push(n.text)
252
+ const crateName = full.split('::')[0]
253
+ bindings.push({ name: n.text, spec: crateName, full })
254
+ }
127
255
  }
256
+ extractRustUse(node.childForFieldName('argument'))
128
257
  }
129
258
  }
130
259
  for (let i = 0; i < node.childCount; i++) visit(node.child(i))
131
260
  }
132
261
  visit(tree.rootNode)
133
- return { specs, names }
262
+ return { specs, names, bindings }
134
263
  }
135
264
 
136
265
  /**
@@ -170,9 +299,9 @@ export async function extractCode(ext, content) {
170
299
  const parser = parserFor(grammar, lang)
171
300
  const tree = parser.parse(content)
172
301
  try {
173
- const { specs: imports, names: importNames } = specsFromTree(grammar, tree)
302
+ const { specs: imports, names: importNames, bindings: importBindings } = specsFromTree(grammar, tree)
174
303
  const { symbols, calls } = structureFromTree(grammar, tree)
175
- return { imports, importNames, symbols, calls }
304
+ return { imports, importNames, importBindings, symbols, calls }
176
305
  } finally {
177
306
  tree.delete()
178
307
  }
@@ -195,7 +324,7 @@ function structureFromTree(grammar, tree) {
195
324
  })
196
325
  }
197
326
  const visit = (node, cls) => {
198
- if (grammar === 'javascript') {
327
+ if (grammar === 'javascript' || grammar === 'typescript' || grammar === 'tsx') {
199
328
  if (node.type === 'function_declaration' || node.type === 'generator_function_declaration') {
200
329
  const name = node.childForFieldName('name')
201
330
  if (name) pushSymbol(node, name.text, 'function')
@@ -220,6 +349,15 @@ function structureFromTree(grammar, tree) {
220
349
  pushSymbol(declarator, name.text, 'function')
221
350
  }
222
351
  }
352
+ } else if (node.type === 'interface_declaration') {
353
+ const name = node.childForFieldName('name')
354
+ if (name) pushSymbol(node, name.text, 'interface')
355
+ } else if (node.type === 'type_alias_declaration') {
356
+ const name = node.childForFieldName('name')
357
+ if (name) pushSymbol(node, name.text, 'type')
358
+ } else if (node.type === 'enum_declaration') {
359
+ const name = node.childForFieldName('name')
360
+ if (name) pushSymbol(node, name.text, 'enum')
223
361
  }
224
362
  } else if (grammar === 'go') {
225
363
  if (node.type === 'function_declaration') {
@@ -234,18 +372,97 @@ function structureFromTree(grammar, tree) {
234
372
  const name = node.childForFieldName('name')
235
373
  if (name) pushSymbol(node, name.text, 'type')
236
374
  }
375
+ } else if (grammar === 'python') {
376
+ if (node.type === 'class_definition') {
377
+ const name = node.childForFieldName('name')
378
+ if (name) {
379
+ pushSymbol(node, name.text, 'class')
380
+ cls = name.text
381
+ }
382
+ } else if (node.type === 'function_definition') {
383
+ const name = node.childForFieldName('name')
384
+ if (name) {
385
+ if (cls) pushSymbol(node, `${cls}.${name.text}`, 'method')
386
+ else pushSymbol(node, name.text, 'function')
387
+ }
388
+ }
389
+ } else if (grammar === 'rust') {
390
+ if (node.type === 'struct_item') {
391
+ const name = node.childForFieldName('name')
392
+ if (name) pushSymbol(node, name.text, 'struct')
393
+ } else if (node.type === 'enum_item') {
394
+ const name = node.childForFieldName('name')
395
+ if (name) pushSymbol(node, name.text, 'enum')
396
+ } else if (node.type === 'trait_item') {
397
+ const name = node.childForFieldName('name')
398
+ if (name) pushSymbol(node, name.text, 'trait')
399
+ } else if (node.type === 'type_item') {
400
+ const name = node.childForFieldName('name')
401
+ if (name) pushSymbol(node, name.text, 'type')
402
+ } else if (node.type === 'impl_item') {
403
+ const typeNode = node.childForFieldName('type')
404
+ if (typeNode) cls = typeNode.text
405
+ } else if (node.type === 'function_item') {
406
+ const name = node.childForFieldName('name')
407
+ if (name) {
408
+ if (cls) pushSymbol(node, `${cls}.${name.text}`, 'method')
409
+ else pushSymbol(node, name.text, 'function')
410
+ }
411
+ }
237
412
  }
238
- if (node.type === 'call_expression' || node.type === 'new_expression') {
239
- const fn = node.type === 'call_expression'
240
- ? node.childForFieldName('function')
241
- : node.childForFieldName('constructor')
242
- if (fn) {
243
- if (fn.type === 'identifier') {
244
- calls.push({ name: fn.text, recv: null, row: node.startPosition.row })
245
- } else if (fn.type === 'member_expression' || fn.type === 'selector_expression') {
246
- const named = fn.namedChildren
247
- if (named.length >= 2) {
248
- calls.push({ name: named[named.length - 1].text, recv: named[0].text, row: node.startPosition.row })
413
+
414
+ if (grammar === 'python') {
415
+ if (node.type === 'call') {
416
+ const fn = node.childForFieldName('function')
417
+ if (fn) {
418
+ if (fn.type === 'identifier') {
419
+ calls.push({ name: fn.text, recv: null, row: node.startPosition.row })
420
+ } else if (fn.type === 'attribute') {
421
+ const obj = fn.childForFieldName('object')
422
+ const attr = fn.childForFieldName('attribute')
423
+ if (attr) {
424
+ calls.push({ name: attr.text, recv: obj ? obj.text : null, row: node.startPosition.row })
425
+ }
426
+ }
427
+ }
428
+ }
429
+ } else if (grammar === 'rust') {
430
+ if (node.type === 'call_expression') {
431
+ const fn = node.childForFieldName('function')
432
+ if (fn) {
433
+ if (fn.type === 'identifier') {
434
+ calls.push({ name: fn.text, recv: null, row: node.startPosition.row })
435
+ } else if (fn.type === 'field_expression') {
436
+ const val = fn.childForFieldName('value')
437
+ const field = fn.childForFieldName('field')
438
+ if (field) {
439
+ calls.push({ name: field.text, recv: val ? val.text : null, row: node.startPosition.row })
440
+ }
441
+ } else if (fn.type === 'scoped_identifier') {
442
+ const pathNode = fn.childForFieldName('path')
443
+ const nameNode = fn.childForFieldName('name')
444
+ if (nameNode) {
445
+ const recv = pathNode
446
+ ? (pathNode.type === 'scoped_identifier' ? (pathNode.childForFieldName('name') || pathNode).text : pathNode.text)
447
+ : null
448
+ calls.push({ name: nameNode.text, recv, row: node.startPosition.row })
449
+ }
450
+ }
451
+ }
452
+ }
453
+ } else {
454
+ if (node.type === 'call_expression' || node.type === 'new_expression') {
455
+ const fn = node.type === 'call_expression'
456
+ ? node.childForFieldName('function')
457
+ : node.childForFieldName('constructor')
458
+ if (fn) {
459
+ if (fn.type === 'identifier') {
460
+ calls.push({ name: fn.text, recv: null, row: node.startPosition.row })
461
+ } else if (fn.type === 'member_expression' || fn.type === 'selector_expression') {
462
+ const named = fn.namedChildren
463
+ if (named.length >= 2) {
464
+ calls.push({ name: named[named.length - 1].text, recv: named[0].text, row: node.startPosition.row })
465
+ }
249
466
  }
250
467
  }
251
468
  }