@rahmantech/knl-view 0.1.0 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (96) hide show
  1. package/README.md +117 -32
  2. package/bin/knl-view.js +435 -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/parser.js +3 -2
  88. package/server/graph/sources.js +165 -24
  89. package/server/graph/syntax.js +196 -16
  90. package/server/index.js +124 -61
  91. package/server/install.js +350 -10
  92. package/server/models_registry.js +34 -9
  93. package/server/scaffold.js +99 -9
  94. package/dist/assets/channel-BZ2_TTTJ.js +0 -1
  95. package/dist/assets/flowDiagram-IUGLLWBU-l7L5DTb5.js +0 -1
  96. package/dist/assets/swimlanesDiagram-AYQRV4AK-BGFq4t7O.js +0 -8
@@ -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
@@ -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
@@ -68,7 +79,7 @@ function specsFromTree(grammar, tree) {
68
79
  const specs = []
69
80
  const names = []
70
81
  const visit = (node) => {
71
- if (grammar === 'javascript') {
82
+ if (grammar === 'javascript' || grammar === 'typescript' || grammar === 'tsx') {
72
83
  if (node.type === 'import_statement' || node.type === 'export_statement') {
73
84
  const source = node.childForFieldName('source')
74
85
  const fragment = source && firstDescendant(source, 'string_fragment')
@@ -126,6 +137,87 @@ function specsFromTree(grammar, tree) {
126
137
  else names.push(path.slice(path.lastIndexOf('/') + 1))
127
138
  }
128
139
  }
140
+ } else if (grammar === 'python') {
141
+ if (node.type === 'import_statement') {
142
+ for (let i = 0; i < node.childCount; i++) {
143
+ const c = node.child(i)
144
+ if (c.type === 'dotted_name') {
145
+ specs.push(c.text)
146
+ const firstId = c.child(0)
147
+ if (firstId) names.push(firstId.text)
148
+ } else if (c.type === 'aliased_import') {
149
+ const name = c.childForFieldName('name')
150
+ const alias = c.childForFieldName('alias')
151
+ if (name) specs.push(name.text)
152
+ if (alias) names.push(alias.text)
153
+ }
154
+ }
155
+ } else if (node.type === 'import_from_statement') {
156
+ const mod = node.childForFieldName('module_name')
157
+ const modText = mod ? mod.text : ''
158
+ if (modText) specs.push(modText)
159
+ let pastImport = false
160
+ for (let i = 0; i < node.childCount; i++) {
161
+ const c = node.child(i)
162
+ if (c.type === 'import') {
163
+ pastImport = true
164
+ continue
165
+ }
166
+ if (!pastImport) continue
167
+ if (c.type === 'dotted_name') {
168
+ names.push(c.text)
169
+ if (modText) specs.push(modText + '.' + c.text)
170
+ } else if (c.type === 'aliased_import') {
171
+ const name = c.childForFieldName('name')
172
+ const alias = c.childForFieldName('alias')
173
+ if (alias) names.push(alias.text)
174
+ if (name && modText) specs.push(modText + '.' + name.text)
175
+ }
176
+ }
177
+ }
178
+ } else if (grammar === 'rust') {
179
+ if (node.type === 'mod_item') {
180
+ if (!node.childForFieldName('body')) {
181
+ const name = node.childForFieldName('name')
182
+ if (name) {
183
+ specs.push('./' + name.text)
184
+ names.push(name.text)
185
+ }
186
+ }
187
+ } else if (node.type === 'use_declaration') {
188
+ const extractRustUse = (n, prefix = '') => {
189
+ if (!n) return
190
+ if (n.type === 'scoped_use_list') {
191
+ const pathNode = n.childForFieldName('path')
192
+ const listNode = n.childForFieldName('list')
193
+ const p = (prefix ? prefix + '::' : '') + (pathNode ? pathNode.text : '')
194
+ extractRustUse(listNode, p)
195
+ } else if (n.type === 'use_list') {
196
+ for (let i = 0; i < n.childCount; i++) {
197
+ const c = n.child(i)
198
+ if (c.type === 'identifier' || c.type === 'scoped_identifier' || c.type === 'use_as_clause' || c.type === 'scoped_use_list') {
199
+ extractRustUse(c, prefix)
200
+ }
201
+ }
202
+ } else if (n.type === 'use_as_clause') {
203
+ const pathNode = n.childForFieldName('path')
204
+ const aliasNode = n.childForFieldName('alias')
205
+ const full = (prefix ? prefix + '::' : '') + (pathNode ? pathNode.text : '')
206
+ specs.push(full)
207
+ if (aliasNode) names.push(aliasNode.text)
208
+ } else if (n.type === 'scoped_identifier') {
209
+ const full = (prefix ? prefix + '::' : '') + n.text
210
+ specs.push(full)
211
+ const name = n.childForFieldName('name')
212
+ if (name) names.push(name.text)
213
+ } else if (n.type === 'identifier') {
214
+ const full = (prefix ? prefix + '::' : '') + n.text
215
+ specs.push(full)
216
+ names.push(n.text)
217
+ }
218
+ }
219
+ extractRustUse(node.childForFieldName('argument'))
220
+ }
129
221
  }
130
222
  for (let i = 0; i < node.childCount; i++) visit(node.child(i))
131
223
  }
@@ -195,7 +287,7 @@ function structureFromTree(grammar, tree) {
195
287
  })
196
288
  }
197
289
  const visit = (node, cls) => {
198
- if (grammar === 'javascript') {
290
+ if (grammar === 'javascript' || grammar === 'typescript' || grammar === 'tsx') {
199
291
  if (node.type === 'function_declaration' || node.type === 'generator_function_declaration') {
200
292
  const name = node.childForFieldName('name')
201
293
  if (name) pushSymbol(node, name.text, 'function')
@@ -220,6 +312,15 @@ function structureFromTree(grammar, tree) {
220
312
  pushSymbol(declarator, name.text, 'function')
221
313
  }
222
314
  }
315
+ } else if (node.type === 'interface_declaration') {
316
+ const name = node.childForFieldName('name')
317
+ if (name) pushSymbol(node, name.text, 'interface')
318
+ } else if (node.type === 'type_alias_declaration') {
319
+ const name = node.childForFieldName('name')
320
+ if (name) pushSymbol(node, name.text, 'type')
321
+ } else if (node.type === 'enum_declaration') {
322
+ const name = node.childForFieldName('name')
323
+ if (name) pushSymbol(node, name.text, 'enum')
223
324
  }
224
325
  } else if (grammar === 'go') {
225
326
  if (node.type === 'function_declaration') {
@@ -234,18 +335,97 @@ function structureFromTree(grammar, tree) {
234
335
  const name = node.childForFieldName('name')
235
336
  if (name) pushSymbol(node, name.text, 'type')
236
337
  }
338
+ } else if (grammar === 'python') {
339
+ if (node.type === 'class_definition') {
340
+ const name = node.childForFieldName('name')
341
+ if (name) {
342
+ pushSymbol(node, name.text, 'class')
343
+ cls = name.text
344
+ }
345
+ } else if (node.type === 'function_definition') {
346
+ const name = node.childForFieldName('name')
347
+ if (name) {
348
+ if (cls) pushSymbol(node, `${cls}.${name.text}`, 'method')
349
+ else pushSymbol(node, name.text, 'function')
350
+ }
351
+ }
352
+ } else if (grammar === 'rust') {
353
+ if (node.type === 'struct_item') {
354
+ const name = node.childForFieldName('name')
355
+ if (name) pushSymbol(node, name.text, 'struct')
356
+ } else if (node.type === 'enum_item') {
357
+ const name = node.childForFieldName('name')
358
+ if (name) pushSymbol(node, name.text, 'enum')
359
+ } else if (node.type === 'trait_item') {
360
+ const name = node.childForFieldName('name')
361
+ if (name) pushSymbol(node, name.text, 'trait')
362
+ } else if (node.type === 'type_item') {
363
+ const name = node.childForFieldName('name')
364
+ if (name) pushSymbol(node, name.text, 'type')
365
+ } else if (node.type === 'impl_item') {
366
+ const typeNode = node.childForFieldName('type')
367
+ if (typeNode) cls = typeNode.text
368
+ } else if (node.type === 'function_item') {
369
+ const name = node.childForFieldName('name')
370
+ if (name) {
371
+ if (cls) pushSymbol(node, `${cls}.${name.text}`, 'method')
372
+ else pushSymbol(node, name.text, 'function')
373
+ }
374
+ }
237
375
  }
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 })
376
+
377
+ if (grammar === 'python') {
378
+ if (node.type === 'call') {
379
+ const fn = node.childForFieldName('function')
380
+ if (fn) {
381
+ if (fn.type === 'identifier') {
382
+ calls.push({ name: fn.text, recv: null, row: node.startPosition.row })
383
+ } else if (fn.type === 'attribute') {
384
+ const obj = fn.childForFieldName('object')
385
+ const attr = fn.childForFieldName('attribute')
386
+ if (attr) {
387
+ calls.push({ name: attr.text, recv: obj ? obj.text : null, row: node.startPosition.row })
388
+ }
389
+ }
390
+ }
391
+ }
392
+ } else if (grammar === 'rust') {
393
+ if (node.type === 'call_expression') {
394
+ const fn = node.childForFieldName('function')
395
+ if (fn) {
396
+ if (fn.type === 'identifier') {
397
+ calls.push({ name: fn.text, recv: null, row: node.startPosition.row })
398
+ } else if (fn.type === 'field_expression') {
399
+ const val = fn.childForFieldName('value')
400
+ const field = fn.childForFieldName('field')
401
+ if (field) {
402
+ calls.push({ name: field.text, recv: val ? val.text : null, row: node.startPosition.row })
403
+ }
404
+ } else if (fn.type === 'scoped_identifier') {
405
+ const pathNode = fn.childForFieldName('path')
406
+ const nameNode = fn.childForFieldName('name')
407
+ if (nameNode) {
408
+ const recv = pathNode
409
+ ? (pathNode.type === 'scoped_identifier' ? (pathNode.childForFieldName('name') || pathNode).text : pathNode.text)
410
+ : null
411
+ calls.push({ name: nameNode.text, recv, row: node.startPosition.row })
412
+ }
413
+ }
414
+ }
415
+ }
416
+ } else {
417
+ if (node.type === 'call_expression' || node.type === 'new_expression') {
418
+ const fn = node.type === 'call_expression'
419
+ ? node.childForFieldName('function')
420
+ : node.childForFieldName('constructor')
421
+ if (fn) {
422
+ if (fn.type === 'identifier') {
423
+ calls.push({ name: fn.text, recv: null, row: node.startPosition.row })
424
+ } else if (fn.type === 'member_expression' || fn.type === 'selector_expression') {
425
+ const named = fn.namedChildren
426
+ if (named.length >= 2) {
427
+ calls.push({ name: named[named.length - 1].text, recv: named[0].text, row: node.startPosition.row })
428
+ }
249
429
  }
250
430
  }
251
431
  }