@spikedpunch/mast 0.1.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 (257) hide show
  1. package/LICENSE +21 -0
  2. package/MAST_SPEC.md +2926 -0
  3. package/README.md +842 -0
  4. package/assets/skill.md +45 -0
  5. package/dist/ast/extract.d.ts +65 -0
  6. package/dist/ast/extract.d.ts.map +1 -0
  7. package/dist/ast/extract.js +147 -0
  8. package/dist/ast/extract.js.map +1 -0
  9. package/dist/ast/extractor.d.ts +85 -0
  10. package/dist/ast/extractor.d.ts.map +1 -0
  11. package/dist/ast/extractor.js +2 -0
  12. package/dist/ast/extractor.js.map +1 -0
  13. package/dist/ast/extractors/markdown.d.ts +40 -0
  14. package/dist/ast/extractors/markdown.d.ts.map +1 -0
  15. package/dist/ast/extractors/markdown.js +175 -0
  16. package/dist/ast/extractors/markdown.js.map +1 -0
  17. package/dist/ast/extractors/typescript.d.ts +179 -0
  18. package/dist/ast/extractors/typescript.d.ts.map +1 -0
  19. package/dist/ast/extractors/typescript.js +1439 -0
  20. package/dist/ast/extractors/typescript.js.map +1 -0
  21. package/dist/ast/parser.d.ts +5 -0
  22. package/dist/ast/parser.d.ts.map +1 -0
  23. package/dist/ast/parser.js +21 -0
  24. package/dist/ast/parser.js.map +1 -0
  25. package/dist/ast/types.d.ts +586 -0
  26. package/dist/ast/types.d.ts.map +1 -0
  27. package/dist/ast/types.js +3 -0
  28. package/dist/ast/types.js.map +1 -0
  29. package/dist/cli/docs-cmd.d.ts +22 -0
  30. package/dist/cli/docs-cmd.d.ts.map +1 -0
  31. package/dist/cli/docs-cmd.js +100 -0
  32. package/dist/cli/docs-cmd.js.map +1 -0
  33. package/dist/cli/index-cmd.d.ts +72 -0
  34. package/dist/cli/index-cmd.d.ts.map +1 -0
  35. package/dist/cli/index-cmd.js +263 -0
  36. package/dist/cli/index-cmd.js.map +1 -0
  37. package/dist/cli/index.d.ts +3 -0
  38. package/dist/cli/index.d.ts.map +1 -0
  39. package/dist/cli/index.js +4 -0
  40. package/dist/cli/index.js.map +1 -0
  41. package/dist/cli/init.d.ts +14 -0
  42. package/dist/cli/init.d.ts.map +1 -0
  43. package/dist/cli/init.js +64 -0
  44. package/dist/cli/init.js.map +1 -0
  45. package/dist/cli/install-hooks.d.ts +3 -0
  46. package/dist/cli/install-hooks.d.ts.map +1 -0
  47. package/dist/cli/install-hooks.js +34 -0
  48. package/dist/cli/install-hooks.js.map +1 -0
  49. package/dist/cli/metrics-cmd.d.ts +3 -0
  50. package/dist/cli/metrics-cmd.d.ts.map +1 -0
  51. package/dist/cli/metrics-cmd.js +164 -0
  52. package/dist/cli/metrics-cmd.js.map +1 -0
  53. package/dist/cli/program.d.ts +14 -0
  54. package/dist/cli/program.d.ts.map +1 -0
  55. package/dist/cli/program.js +44 -0
  56. package/dist/cli/program.js.map +1 -0
  57. package/dist/cli/query.d.ts +46 -0
  58. package/dist/cli/query.d.ts.map +1 -0
  59. package/dist/cli/query.js +153 -0
  60. package/dist/cli/query.js.map +1 -0
  61. package/dist/cli/search-cmd.d.ts +33 -0
  62. package/dist/cli/search-cmd.d.ts.map +1 -0
  63. package/dist/cli/search-cmd.js +114 -0
  64. package/dist/cli/search-cmd.js.map +1 -0
  65. package/dist/cli/serve.d.ts +3 -0
  66. package/dist/cli/serve.d.ts.map +1 -0
  67. package/dist/cli/serve.js +19 -0
  68. package/dist/cli/serve.js.map +1 -0
  69. package/dist/cli/skill-install.d.ts +39 -0
  70. package/dist/cli/skill-install.d.ts.map +1 -0
  71. package/dist/cli/skill-install.js +57 -0
  72. package/dist/cli/skill-install.js.map +1 -0
  73. package/dist/cli/status.d.ts +35 -0
  74. package/dist/cli/status.d.ts.map +1 -0
  75. package/dist/cli/status.js +117 -0
  76. package/dist/cli/status.js.map +1 -0
  77. package/dist/cli/upgrade-cmd.d.ts +48 -0
  78. package/dist/cli/upgrade-cmd.d.ts.map +1 -0
  79. package/dist/cli/upgrade-cmd.js +129 -0
  80. package/dist/cli/upgrade-cmd.js.map +1 -0
  81. package/dist/cli/version.d.ts +3 -0
  82. package/dist/cli/version.d.ts.map +1 -0
  83. package/dist/cli/version.js +19 -0
  84. package/dist/cli/version.js.map +1 -0
  85. package/dist/env.d.ts +17 -0
  86. package/dist/env.d.ts.map +1 -0
  87. package/dist/env.js +17 -0
  88. package/dist/env.js.map +1 -0
  89. package/dist/graph/checker-resolver.d.ts +156 -0
  90. package/dist/graph/checker-resolver.d.ts.map +1 -0
  91. package/dist/graph/checker-resolver.js +458 -0
  92. package/dist/graph/checker-resolver.js.map +1 -0
  93. package/dist/graph/db.d.ts +318 -0
  94. package/dist/graph/db.d.ts.map +1 -0
  95. package/dist/graph/db.js +266 -0
  96. package/dist/graph/db.js.map +1 -0
  97. package/dist/graph/index.d.ts +4 -0
  98. package/dist/graph/index.d.ts.map +1 -0
  99. package/dist/graph/index.js +5 -0
  100. package/dist/graph/index.js.map +1 -0
  101. package/dist/graph/local-type-env.d.ts +45 -0
  102. package/dist/graph/local-type-env.d.ts.map +1 -0
  103. package/dist/graph/local-type-env.js +59 -0
  104. package/dist/graph/local-type-env.js.map +1 -0
  105. package/dist/graph/path-range.d.ts +40 -0
  106. package/dist/graph/path-range.d.ts.map +1 -0
  107. package/dist/graph/path-range.js +42 -0
  108. package/dist/graph/path-range.js.map +1 -0
  109. package/dist/graph/populate.d.ts +263 -0
  110. package/dist/graph/populate.d.ts.map +1 -0
  111. package/dist/graph/populate.js +984 -0
  112. package/dist/graph/populate.js.map +1 -0
  113. package/dist/graph/queries.d.ts +114 -0
  114. package/dist/graph/queries.d.ts.map +1 -0
  115. package/dist/graph/queries.js +415 -0
  116. package/dist/graph/queries.js.map +1 -0
  117. package/dist/graph/sqliteBatch.d.ts +48 -0
  118. package/dist/graph/sqliteBatch.d.ts.map +1 -0
  119. package/dist/graph/sqliteBatch.js +93 -0
  120. package/dist/graph/sqliteBatch.js.map +1 -0
  121. package/dist/indexer/freshness.d.ts +48 -0
  122. package/dist/indexer/freshness.d.ts.map +1 -0
  123. package/dist/indexer/freshness.js +72 -0
  124. package/dist/indexer/freshness.js.map +1 -0
  125. package/dist/indexer/import-resolver.d.ts +50 -0
  126. package/dist/indexer/import-resolver.d.ts.map +1 -0
  127. package/dist/indexer/import-resolver.js +318 -0
  128. package/dist/indexer/import-resolver.js.map +1 -0
  129. package/dist/indexer/index.d.ts +249 -0
  130. package/dist/indexer/index.d.ts.map +1 -0
  131. package/dist/indexer/index.js +560 -0
  132. package/dist/indexer/index.js.map +1 -0
  133. package/dist/indexer/walker.d.ts +60 -0
  134. package/dist/indexer/walker.d.ts.map +1 -0
  135. package/dist/indexer/walker.js +128 -0
  136. package/dist/indexer/walker.js.map +1 -0
  137. package/dist/indexer/watcher.d.ts +80 -0
  138. package/dist/indexer/watcher.d.ts.map +1 -0
  139. package/dist/indexer/watcher.js +151 -0
  140. package/dist/indexer/watcher.js.map +1 -0
  141. package/dist/mcp/context.d.ts +23 -0
  142. package/dist/mcp/context.d.ts.map +1 -0
  143. package/dist/mcp/context.js +2 -0
  144. package/dist/mcp/context.js.map +1 -0
  145. package/dist/mcp/register-tools.d.ts +21 -0
  146. package/dist/mcp/register-tools.d.ts.map +1 -0
  147. package/dist/mcp/register-tools.js +42 -0
  148. package/dist/mcp/register-tools.js.map +1 -0
  149. package/dist/mcp/server.d.ts +58 -0
  150. package/dist/mcp/server.d.ts.map +1 -0
  151. package/dist/mcp/server.js +177 -0
  152. package/dist/mcp/server.js.map +1 -0
  153. package/dist/mcp/staleness.d.ts +72 -0
  154. package/dist/mcp/staleness.d.ts.map +1 -0
  155. package/dist/mcp/staleness.js +213 -0
  156. package/dist/mcp/staleness.js.map +1 -0
  157. package/dist/mcp/startup.d.ts +40 -0
  158. package/dist/mcp/startup.d.ts.map +1 -0
  159. package/dist/mcp/startup.js +114 -0
  160. package/dist/mcp/startup.js.map +1 -0
  161. package/dist/mcp/tools/_helpers.d.ts +26 -0
  162. package/dist/mcp/tools/_helpers.d.ts.map +1 -0
  163. package/dist/mcp/tools/_helpers.js +54 -0
  164. package/dist/mcp/tools/_helpers.js.map +1 -0
  165. package/dist/mcp/tools/callers.d.ts +4 -0
  166. package/dist/mcp/tools/callers.d.ts.map +1 -0
  167. package/dist/mcp/tools/callers.js +112 -0
  168. package/dist/mcp/tools/callers.js.map +1 -0
  169. package/dist/mcp/tools/dependencies.d.ts +4 -0
  170. package/dist/mcp/tools/dependencies.d.ts.map +1 -0
  171. package/dist/mcp/tools/dependencies.js +36 -0
  172. package/dist/mcp/tools/dependencies.js.map +1 -0
  173. package/dist/mcp/tools/efficiency.d.ts +4 -0
  174. package/dist/mcp/tools/efficiency.d.ts.map +1 -0
  175. package/dist/mcp/tools/efficiency.js +39 -0
  176. package/dist/mcp/tools/efficiency.js.map +1 -0
  177. package/dist/mcp/tools/exports.d.ts +4 -0
  178. package/dist/mcp/tools/exports.d.ts.map +1 -0
  179. package/dist/mcp/tools/exports.js +56 -0
  180. package/dist/mcp/tools/exports.js.map +1 -0
  181. package/dist/mcp/tools/implementors.d.ts +4 -0
  182. package/dist/mcp/tools/implementors.d.ts.map +1 -0
  183. package/dist/mcp/tools/implementors.js +47 -0
  184. package/dist/mcp/tools/implementors.js.map +1 -0
  185. package/dist/mcp/tools/project-skeleton.d.ts +4 -0
  186. package/dist/mcp/tools/project-skeleton.d.ts.map +1 -0
  187. package/dist/mcp/tools/project-skeleton.js +61 -0
  188. package/dist/mcp/tools/project-skeleton.js.map +1 -0
  189. package/dist/mcp/tools/reindex.d.ts +14 -0
  190. package/dist/mcp/tools/reindex.d.ts.map +1 -0
  191. package/dist/mcp/tools/reindex.js +32 -0
  192. package/dist/mcp/tools/reindex.js.map +1 -0
  193. package/dist/mcp/tools/rename-impact.d.ts +4 -0
  194. package/dist/mcp/tools/rename-impact.d.ts.map +1 -0
  195. package/dist/mcp/tools/rename-impact.js +124 -0
  196. package/dist/mcp/tools/rename-impact.js.map +1 -0
  197. package/dist/mcp/tools/search.d.ts +4 -0
  198. package/dist/mcp/tools/search.d.ts.map +1 -0
  199. package/dist/mcp/tools/search.js +69 -0
  200. package/dist/mcp/tools/search.js.map +1 -0
  201. package/dist/mcp/tools/signature.d.ts +4 -0
  202. package/dist/mcp/tools/signature.d.ts.map +1 -0
  203. package/dist/mcp/tools/signature.js +127 -0
  204. package/dist/mcp/tools/signature.js.map +1 -0
  205. package/dist/mcp/tools/status.d.ts +4 -0
  206. package/dist/mcp/tools/status.d.ts.map +1 -0
  207. package/dist/mcp/tools/status.js +30 -0
  208. package/dist/mcp/tools/status.js.map +1 -0
  209. package/dist/search/declex.d.ts +109 -0
  210. package/dist/search/declex.d.ts.map +1 -0
  211. package/dist/search/declex.js +242 -0
  212. package/dist/search/declex.js.map +1 -0
  213. package/dist/search/fts.d.ts +77 -0
  214. package/dist/search/fts.d.ts.map +1 -0
  215. package/dist/search/fts.js +223 -0
  216. package/dist/search/fts.js.map +1 -0
  217. package/dist/search/fused.d.ts +110 -0
  218. package/dist/search/fused.d.ts.map +1 -0
  219. package/dist/search/fused.js +323 -0
  220. package/dist/search/fused.js.map +1 -0
  221. package/dist/search/potential-matches.d.ts +88 -0
  222. package/dist/search/potential-matches.d.ts.map +1 -0
  223. package/dist/search/potential-matches.js +83 -0
  224. package/dist/search/potential-matches.js.map +1 -0
  225. package/dist/search/scope.d.ts +66 -0
  226. package/dist/search/scope.d.ts.map +1 -0
  227. package/dist/search/scope.js +71 -0
  228. package/dist/search/scope.js.map +1 -0
  229. package/dist/store/config.d.ts +57 -0
  230. package/dist/store/config.d.ts.map +1 -0
  231. package/dist/store/config.js +208 -0
  232. package/dist/store/config.js.map +1 -0
  233. package/dist/store/lock.d.ts +101 -0
  234. package/dist/store/lock.d.ts.map +1 -0
  235. package/dist/store/lock.js +182 -0
  236. package/dist/store/lock.js.map +1 -0
  237. package/dist/store/lockMetrics.d.ts +52 -0
  238. package/dist/store/lockMetrics.d.ts.map +1 -0
  239. package/dist/store/lockMetrics.js +29 -0
  240. package/dist/store/lockMetrics.js.map +1 -0
  241. package/dist/store/sqliteChunkStore.d.ts +44 -0
  242. package/dist/store/sqliteChunkStore.d.ts.map +1 -0
  243. package/dist/store/sqliteChunkStore.js +135 -0
  244. package/dist/store/sqliteChunkStore.js.map +1 -0
  245. package/dist/telemetry/lockMetricsSummary.d.ts +42 -0
  246. package/dist/telemetry/lockMetricsSummary.d.ts.map +1 -0
  247. package/dist/telemetry/lockMetricsSummary.js +139 -0
  248. package/dist/telemetry/lockMetricsSummary.js.map +1 -0
  249. package/dist/telemetry/metrics.d.ts +140 -0
  250. package/dist/telemetry/metrics.d.ts.map +1 -0
  251. package/dist/telemetry/metrics.js +312 -0
  252. package/dist/telemetry/metrics.js.map +1 -0
  253. package/dist/telemetry/tokenizer.d.ts +125 -0
  254. package/dist/telemetry/tokenizer.d.ts.map +1 -0
  255. package/dist/telemetry/tokenizer.js +174 -0
  256. package/dist/telemetry/tokenizer.js.map +1 -0
  257. package/package.json +81 -0
@@ -0,0 +1,1439 @@
1
+ import { createHash } from 'node:crypto';
2
+ import { extname } from 'node:path';
3
+ import { parseSource } from '../parser.js';
4
+ import { LocalTypeEnvironment } from '../../graph/local-type-env.js';
5
+ import { getImportResolver } from '../../indexer/import-resolver.js';
6
+ // ---------------------------------------------------------------------------
7
+ // TypeScriptExtractor
8
+ // ---------------------------------------------------------------------------
9
+ /**
10
+ * TypeScript/JavaScript chunk extractor.
11
+ *
12
+ * Implements §10.1 chunking strategy and §10.2 signature extraction.
13
+ * Handles class decomposition (shell + method chunks) and the two-pass
14
+ * walk for `is_exported` detection. tree-sitter parsing is internal —
15
+ * callers only see the `LanguageExtractor` contract.
16
+ */
17
+ export class TypeScriptExtractor {
18
+ language = 'typescript';
19
+ extensions = ['.ts', '.tsx', '.js', '.jsx'];
20
+ extract(src, filePath, fileMtime, options) {
21
+ const extension = extname(filePath);
22
+ const tree = parseSource(src, extension);
23
+ const rawChunks = this.extractChunks(tree, src, filePath, fileMtime, options.contextLines, options.chunkSplitThreshold);
24
+ // extractChunks stamps the extension-derived language on each chunk, but
25
+ // the extractor's own `language` is always 'typescript' — derive the
26
+ // file-level value from the extension so `.js`/`.jsx` filters work.
27
+ const language = languageFromExt(extension);
28
+ const chunks = rawChunks;
29
+ // Resolve each import specifier to a real indexed file (§13.7): relative
30
+ // probing, tsconfig aliases, workspace packages, symlink realpath.
31
+ const resolver = getImportResolver(options.projectRoot);
32
+ const imports = extractImports(tree, filePath).map((imp) => {
33
+ const r = resolver.resolve(imp.module, filePath);
34
+ return { module: imp.module, symbols: imp.symbols, isExternal: r.isExternal, resolvedPath: r.resolvedPath };
35
+ });
36
+ // Re-exports (§10.1): named ones become exported marker symbols (kind
37
+ // 'export') plus RE_EXPORTS edges so rename impact can find barrels; star
38
+ // ones become file-level re_export_files records (no per-symbol identity).
39
+ const reExports = extractReExports(tree);
40
+ const markerSymbols = reExports.named.map((r) => ({
41
+ name: r.exportedName,
42
+ kind: 'export',
43
+ line: r.line,
44
+ isExported: true,
45
+ declarationHash: null,
46
+ bodyHash: null,
47
+ }));
48
+ const reExportEdges = reExports.named.map((r) => ({
49
+ fromName: r.exportedName,
50
+ toName: r.sourceName,
51
+ edgeType: 'RE_EXPORTS',
52
+ // Same resolver call already made for imports/starReExports above — the
53
+ // file evidence `insertEdges` needs to scope this edge to the module the
54
+ // re-export actually names (Task 0 fix, see EdgeRecord.toResolvedPath).
55
+ toResolvedPath: resolver.resolve(r.module, filePath).resolvedPath,
56
+ }));
57
+ const starReExports = reExports.stars.map((s) => ({
58
+ module: s.module,
59
+ resolvedPath: resolver.resolve(s.module, filePath).resolvedPath,
60
+ line: s.line,
61
+ }));
62
+ const symbols = [...symbolsFromChunks(chunks), ...markerSymbols];
63
+ const edges = [...extractEdges(tree, filePath, src), ...reExportEdges];
64
+ // F5 (Stage 3): qualified compounds ("Class.method") for identifier_fts.
65
+ // `searchIdentifiers` phrase-quotes its query term, and identifier_fts'
66
+ // unicode61 tokenizer treats '.' as a separator, so a query for a
67
+ // qualified method name only matches a row whose identifiers column has
68
+ // the class/method tokens ADJACENT — which the bare-identifier bag below
69
+ // essentially never produces (the chunk text rarely repeats "Class"
70
+ // immediately followed by "method"). Deriving the compounds from `edges`
71
+ // (rather than re-walking the AST) rides the SAME resolution already
72
+ // computed by `extractEdges`/`LocalTypeEnvironment` — no parallel
73
+ // mechanism, and a receiver `extractEdges` could not statically link
74
+ // (DI/factory calls, §10.3.1's documented gap) correctly contributes
75
+ // nothing here either, since no POTENTIAL_CALL edge exists for it.
76
+ const qualifiedMentionsByFromName = new Map();
77
+ for (const e of edges) {
78
+ // `toName` is qualified ("Type.method") only for a receiver-based call
79
+ // (LocalTypeEnvironment.resolveCall's `${binding.type}.${method}` path)
80
+ // — a bare call (import/same_file resolution) has no receiver and
81
+ // therefore no dot. PARENT_OF/EXTENDS/etc. use a different fromName
82
+ // scope (the class itself, not a method body) and must not leak in.
83
+ if (e.edgeType !== 'POTENTIAL_CALL' || !e.toName.includes('.'))
84
+ continue;
85
+ const mentions = qualifiedMentionsByFromName.get(e.fromName);
86
+ if (mentions === undefined)
87
+ qualifiedMentionsByFromName.set(e.fromName, [e.toName]);
88
+ else
89
+ mentions.push(e.toName);
90
+ }
91
+ const identifierRows = chunks.flatMap((chunk) => {
92
+ const base = extractIdentifiers(chunk.content);
93
+ // Declaration self-discoverability: a method chunk's own qualified
94
+ // name (constructor/getter/setter forms are already qualified, since
95
+ // `symbol_name` is always `${className}.${methodName}` for chunk_type
96
+ // 'method' — see the `chunks.push` call above).
97
+ const ownQualified = chunk.chunk_type === 'method' && chunk.symbol_name !== null
98
+ ? [chunk.symbol_name]
99
+ : [];
100
+ const mentionQualified = chunk.symbol_name !== null
101
+ ? qualifiedMentionsByFromName.get(chunk.symbol_name) ?? []
102
+ : [];
103
+ const identifiers = appendQualifiedCompounds(base, [...ownQualified, ...mentionQualified]);
104
+ // ONE row per chunk, in chunk order, even when the bag is empty. The empty
105
+ // ones are dropped in `extractFile` AFTER `remapIdentifierRows` has used the
106
+ // positional correspondence to re-key them (D040) — dropping them here is what
107
+ // made the correspondence partial, and a partial correspondence cannot be
108
+ // reconstructed from `chunk_id` alone once two chunks share one.
109
+ return [{ chunk_id: chunk.chunk_id, identifiers }];
110
+ });
111
+ return { language, chunks, symbols, imports, edges, identifierRows, starReExports };
112
+ }
113
+ extractChunks(parsedTree, src, filePath, fileMtime, contextLines, chunkSplitThreshold) {
114
+ const lines = src.split('\n');
115
+ const lang = languageFromExt(extname(filePath));
116
+ const topLevel = nodeChildren(parsedTree.rootNode);
117
+ // -------------------------------------------------------------------------
118
+ // Pass 1: build symbol-name → exported flag map.
119
+ // `export function foo()` / `export class Foo` → name recorded as exported.
120
+ // Bare declarations → name recorded as not-yet-exported (may be updated pass 2).
121
+ // -------------------------------------------------------------------------
122
+ const exportedNames = new Set();
123
+ const knownDeclNames = new Set();
124
+ for (const node of topLevel) {
125
+ if (nodeType(node) === 'export_statement') {
126
+ const decl = getWrappedDeclaration(node);
127
+ if (decl !== null) {
128
+ const name = getDeclName(decl);
129
+ if (name !== null) {
130
+ exportedNames.add(name);
131
+ knownDeclNames.add(name);
132
+ }
133
+ }
134
+ }
135
+ else {
136
+ const name = getDeclName(node);
137
+ if (name !== null)
138
+ knownDeclNames.add(name);
139
+ }
140
+ }
141
+ // -------------------------------------------------------------------------
142
+ // Pass 2: update is_exported via `export { foo }` (no `from` clause).
143
+ // -------------------------------------------------------------------------
144
+ for (const node of topLevel) {
145
+ if (nodeType(node) !== 'export_statement')
146
+ continue;
147
+ if (hasFromClause(node))
148
+ continue;
149
+ if (getWrappedDeclaration(node) !== null)
150
+ continue;
151
+ const exportClause = findChildByType(node, 'export_clause');
152
+ if (exportClause === null)
153
+ continue;
154
+ for (const child of nodeNamedChildren(exportClause)) {
155
+ if (nodeType(child) !== 'export_specifier')
156
+ continue;
157
+ const localName = child.childForFieldName('name')?.text;
158
+ const alias = child.childForFieldName('alias')?.text;
159
+ // `export { foo as bar }` exports `foo` UNDER `bar`, not as `foo` — the
160
+ // alias is emitted as its own exported chunk in pass 3, so the local
161
+ // name is not marked exported here.
162
+ if (alias !== undefined && alias !== localName)
163
+ continue;
164
+ if (localName !== undefined && knownDeclNames.has(localName)) {
165
+ exportedNames.add(localName);
166
+ }
167
+ }
168
+ }
169
+ // -------------------------------------------------------------------------
170
+ // Emit chunks
171
+ // -------------------------------------------------------------------------
172
+ const chunks = [];
173
+ for (const node of topLevel) {
174
+ const t = nodeType(node);
175
+ if (t === 'comment' || t === 'import_statement')
176
+ continue;
177
+ let declNode = node;
178
+ let isExported = false;
179
+ if (t === 'export_statement') {
180
+ const decl = getWrappedDeclaration(node);
181
+ if (decl === null)
182
+ continue; // export { } or export * from — no direct chunk
183
+ declNode = decl;
184
+ isExported = true;
185
+ }
186
+ else {
187
+ const name = getDeclName(node);
188
+ isExported = name !== null && exportedNames.has(name);
189
+ }
190
+ emitChunksForNode(declNode, isExported, chunks, lines, src, filePath, fileMtime, contextLines, chunkSplitThreshold, lang);
191
+ }
192
+ // -------------------------------------------------------------------------
193
+ // Pass 3: local re-export aliases — `export { foo as bar }` exposes foo's
194
+ // declaration under the name `bar`. Emit a chunk for `bar` mirroring foo's
195
+ // so it is discoverable (mast_exports / mast_search / mast_signature).
196
+ // -------------------------------------------------------------------------
197
+ for (const { local, alias, line } of localExportAliases(topLevel)) {
198
+ const target = chunks.find((c) => c.symbol_name === local && c.chunk_type !== 'method');
199
+ if (target === undefined)
200
+ continue;
201
+ chunks.push({
202
+ ...target,
203
+ chunk_id: sha256(`${filePath}:${line}:${alias}`),
204
+ symbol_name: alias,
205
+ is_exported: true,
206
+ });
207
+ }
208
+ return chunks;
209
+ }
210
+ declarationHash(node, src) {
211
+ return declHashOf(node, src);
212
+ }
213
+ bodyHash(node, src) {
214
+ return bodyHashOf(node, src);
215
+ }
216
+ }
217
+ // ---------------------------------------------------------------------------
218
+ // Chunk emission
219
+ // ---------------------------------------------------------------------------
220
+ function emitChunksForNode(node, isExported, chunks, lines, src, filePath, fileMtime, contextLines, chunkSplitThreshold, language) {
221
+ const t = nodeType(node);
222
+ const startLine = nodeStartLine(node);
223
+ const endLine = nodeEndLine(node);
224
+ switch (t) {
225
+ case 'function_declaration':
226
+ case 'generator_function_declaration': {
227
+ const name = node.childForFieldName('name')?.text ?? null;
228
+ pushChunks(chunks, {
229
+ chunkType: 'function',
230
+ symbolName: name,
231
+ parentSymbol: null,
232
+ isExported,
233
+ startLine,
234
+ endLine,
235
+ lines,
236
+ filePath,
237
+ fileMtime,
238
+ contextLines,
239
+ chunkSplitThreshold,
240
+ language,
241
+ declarationHash: declHashOf(node, src),
242
+ bodyHash: bodyHashOf(node, src),
243
+ });
244
+ break;
245
+ }
246
+ case 'lexical_declaration':
247
+ case 'variable_declaration': {
248
+ const declarator = findChildByType(node, 'variable_declarator');
249
+ if (declarator === null)
250
+ break;
251
+ const valNode = declarator.childForFieldName('value');
252
+ const isFunc = valNode !== null &&
253
+ (nodeType(valNode) === 'arrow_function' ||
254
+ nodeType(valNode) === 'function' ||
255
+ nodeType(valNode) === 'generator_function');
256
+ const name = declarator.childForFieldName('name')?.text ?? null;
257
+ pushChunks(chunks, {
258
+ chunkType: isFunc ? 'function' : 'block',
259
+ symbolName: name,
260
+ parentSymbol: null,
261
+ isExported,
262
+ startLine,
263
+ endLine,
264
+ lines,
265
+ filePath,
266
+ fileMtime,
267
+ contextLines,
268
+ chunkSplitThreshold,
269
+ language,
270
+ declarationHash: declHashOf(node, src),
271
+ bodyHash: bodyHashOf(node, src),
272
+ });
273
+ break;
274
+ }
275
+ case 'class_declaration':
276
+ case 'abstract_class_declaration': {
277
+ const className = node.childForFieldName('name')?.text ?? null;
278
+ const bodyNode = node.childForFieldName('body') ?? findChildByType(node, 'class_body');
279
+ // Synthesized class_shell chunk
280
+ const shellContent = synthesiseClassShell(node, bodyNode, src);
281
+ chunks.push({
282
+ chunk_id: chunkId(filePath, startLine),
283
+ file_path: filePath,
284
+ start_line: startLine,
285
+ end_line: endLine,
286
+ content: shellContent,
287
+ chunk_type: 'class_shell',
288
+ symbol_name: className,
289
+ parent_symbol: null,
290
+ is_exported: isExported,
291
+ language,
292
+ file_mtime: fileMtime,
293
+ declaration_hash: declHashOf(node, src),
294
+ body_hash: bodyNode !== null ? classShellBodyHashOf(bodyNode, src) : sha256(''),
295
+ });
296
+ // Method chunks
297
+ if (bodyNode !== null && className !== null) {
298
+ for (const member of nodeNamedChildren(bodyNode)) {
299
+ const mt = nodeType(member);
300
+ if (mt !== 'method_definition' && mt !== 'abstract_method_signature')
301
+ continue;
302
+ const methodName = member.childForFieldName('name')?.text ?? null;
303
+ if (methodName === null)
304
+ continue;
305
+ const isPrivate = hasPrivateModifier(member);
306
+ const methodExported = isExported && !isPrivate;
307
+ const mStart = nodeStartLine(member);
308
+ const mEnd = nodeEndLine(member);
309
+ const content = expandContent(lines, mStart, mEnd, contextLines);
310
+ chunks.push({
311
+ chunk_id: chunkId(filePath, mStart),
312
+ file_path: filePath,
313
+ start_line: mStart,
314
+ end_line: mEnd,
315
+ content,
316
+ chunk_type: 'method',
317
+ symbol_name: `${className}.${methodName}`,
318
+ parent_symbol: className,
319
+ is_exported: methodExported,
320
+ language,
321
+ file_mtime: fileMtime,
322
+ declaration_hash: declHashOf(member, src),
323
+ body_hash: bodyHashOf(member, src),
324
+ });
325
+ }
326
+ }
327
+ break;
328
+ }
329
+ case 'interface_declaration': {
330
+ const name = node.childForFieldName('name')?.text ?? null;
331
+ pushChunks(chunks, {
332
+ chunkType: 'interface',
333
+ symbolName: name,
334
+ parentSymbol: null,
335
+ isExported,
336
+ startLine,
337
+ endLine,
338
+ lines,
339
+ filePath,
340
+ fileMtime,
341
+ contextLines,
342
+ chunkSplitThreshold,
343
+ language,
344
+ declarationHash: declHashOf(node, src),
345
+ bodyHash: bodyHashOf(node, src),
346
+ });
347
+ break;
348
+ }
349
+ case 'type_alias_declaration': {
350
+ const name = node.childForFieldName('name')?.text ?? null;
351
+ pushChunks(chunks, {
352
+ chunkType: 'type',
353
+ symbolName: name,
354
+ parentSymbol: null,
355
+ isExported,
356
+ startLine,
357
+ endLine,
358
+ lines,
359
+ filePath,
360
+ fileMtime,
361
+ contextLines,
362
+ chunkSplitThreshold,
363
+ language,
364
+ declarationHash: declHashOf(node, src),
365
+ bodyHash: bodyHashOf(node, src),
366
+ });
367
+ break;
368
+ }
369
+ default: {
370
+ // Skip zero-content nodes. `declare module 'x';` parses as an
371
+ // `ambient_declaration` (the real content) PLUS a sibling
372
+ // `empty_statement` for the trailing `;` — both fall into this default
373
+ // branch, and `expandContent` then makes the `;` chunk byte-identical
374
+ // to the real declaration's, colliding on chunk_id (GITNEXUS_COMPARISON.md
375
+ // §15.3; measured: directus/app/src/shims.d.ts, 40 chunks/24 unique ids).
376
+ // Filtering here kills the collision at its source — a disambiguator
377
+ // alone would instead legitimize junk `;`-only chunks. The broader
378
+ // whitespace-only check catches the same failure mode for any other
379
+ // node type tree-sitter might emit with no real source text.
380
+ if (t === 'empty_statement' || src.slice(node.startIndex, node.endIndex).trim() === '') {
381
+ break;
382
+ }
383
+ // Everything else at top level → block (skip if we have no meaningful content)
384
+ const name = getDeclName(node);
385
+ pushChunks(chunks, {
386
+ chunkType: 'block',
387
+ symbolName: name,
388
+ parentSymbol: null,
389
+ isExported,
390
+ startLine,
391
+ endLine,
392
+ lines,
393
+ filePath,
394
+ fileMtime,
395
+ contextLines,
396
+ chunkSplitThreshold,
397
+ language,
398
+ });
399
+ break;
400
+ }
401
+ }
402
+ }
403
+ /**
404
+ * Push one chunk — or N overlapping sub-chunks when the declaration exceeds
405
+ * `chunkSplitThreshold` lines (§10.1 split rule). The first sub-chunk always
406
+ * starts at the declaration header so signature extraction works from sub-chunk 0.
407
+ */
408
+ function pushChunks(chunks, opts) {
409
+ const { startLine, endLine, filePath, fileMtime, lines, contextLines, language } = opts;
410
+ const lineCount = endLine - startLine + 1;
411
+ if (lineCount <= opts.chunkSplitThreshold) {
412
+ chunks.push({
413
+ chunk_id: chunkId(filePath, startLine),
414
+ file_path: filePath,
415
+ start_line: startLine,
416
+ end_line: endLine,
417
+ content: expandContent(lines, startLine, endLine, contextLines),
418
+ chunk_type: opts.chunkType,
419
+ symbol_name: opts.symbolName,
420
+ parent_symbol: opts.parentSymbol,
421
+ is_exported: opts.isExported,
422
+ language,
423
+ file_mtime: fileMtime,
424
+ declaration_hash: opts.declarationHash,
425
+ body_hash: opts.bodyHash,
426
+ });
427
+ return;
428
+ }
429
+ // Split into overlapping sub-chunks with OVERLAP_LINES overlap.
430
+ const OVERLAP_LINES = 10;
431
+ const windowSize = opts.chunkSplitThreshold;
432
+ let subStart = startLine;
433
+ let subIndex = 0;
434
+ while (subStart <= endLine) {
435
+ const subEnd = Math.min(subStart + windowSize - 1, endLine);
436
+ chunks.push({
437
+ // Qualify sub-chunk IDs with sub-index to keep them unique.
438
+ chunk_id: sha256(`${filePath}:${startLine}:${subIndex}`),
439
+ file_path: filePath,
440
+ start_line: subStart,
441
+ end_line: subEnd,
442
+ content: expandContent(lines, subStart, subEnd, contextLines),
443
+ chunk_type: opts.chunkType,
444
+ symbol_name: opts.symbolName,
445
+ parent_symbol: opts.parentSymbol,
446
+ is_exported: opts.isExported,
447
+ language,
448
+ file_mtime: fileMtime,
449
+ declaration_hash: opts.declarationHash,
450
+ body_hash: opts.bodyHash,
451
+ });
452
+ if (subEnd >= endLine)
453
+ break;
454
+ subStart = subEnd - OVERLAP_LINES + 1;
455
+ subIndex++;
456
+ }
457
+ }
458
+ // ---------------------------------------------------------------------------
459
+ // Class shell synthesis
460
+ // ---------------------------------------------------------------------------
461
+ /**
462
+ * Synthesise the class_shell content: class declaration header + member
463
+ * signatures (TSDoc + signature line, no bodies) + closing brace.
464
+ */
465
+ export function synthesiseClassShell(classNode, bodyNode, src) {
466
+ // Header: class declaration up to (not including) the opening `{`
467
+ const header = bodyNode !== null
468
+ ? src.slice(classNode.startIndex, bodyNode.startIndex).trimEnd()
469
+ : src.slice(classNode.startIndex, classNode.endIndex);
470
+ if (bodyNode === null)
471
+ return header;
472
+ const memberLines = [header + ' {'];
473
+ for (const member of nodeNamedChildren(bodyNode)) {
474
+ if (!isClassShellMember(nodeType(member)))
475
+ continue;
476
+ // Leading TSDoc/comment
477
+ const doc = getLeadingComment(member, bodyNode, src);
478
+ if (doc !== null)
479
+ memberLines.push(' ' + doc.trim());
480
+ // Signature without body
481
+ memberLines.push(' ' + extractSignatureText(member, src).trim());
482
+ }
483
+ memberLines.push('}');
484
+ return memberLines.join('\n');
485
+ }
486
+ // ---------------------------------------------------------------------------
487
+ // Signature / doc extraction
488
+ // ---------------------------------------------------------------------------
489
+ /**
490
+ * Extract declaration text up to (not including) the body block.
491
+ * For interfaces and type aliases the full node is the signature.
492
+ */
493
+ export function extractSignatureText(node, src) {
494
+ const bodyNode = node.childForFieldName('body') ?? findChildByType(node, 'statement_block');
495
+ if (bodyNode === null) {
496
+ return src.slice(node.startIndex, node.endIndex);
497
+ }
498
+ return src.slice(node.startIndex, bodyNode.startIndex).trimEnd();
499
+ }
500
+ /**
501
+ * Extract a body-free signature (plus structured params and return type) for
502
+ * every top-level symbol and class method in `tree`. Used by `mast_signature`
503
+ * and `mast_exports` so they report the declaration, not the function body.
504
+ */
505
+ export function extractSignatures(tree, src) {
506
+ const out = [];
507
+ const root = tree.rootNode;
508
+ for (const node of nodeChildren(root)) {
509
+ const isExport = nodeType(node) === 'export_statement';
510
+ const decl = isExport ? getWrappedDeclaration(node) : node;
511
+ if (decl === null)
512
+ continue;
513
+ // Doc precedes the export wrapper when there is one.
514
+ const docHost = isExport ? node : decl;
515
+ const t = nodeType(decl);
516
+ switch (t) {
517
+ case 'function_declaration':
518
+ case 'generator_function_declaration': {
519
+ const name = decl.childForFieldName('name')?.text;
520
+ if (name !== undefined)
521
+ out.push(signatureFor(name, decl, decl, root, src));
522
+ break;
523
+ }
524
+ case 'lexical_declaration':
525
+ case 'variable_declaration': {
526
+ const declarator = findChildByType(decl, 'variable_declarator');
527
+ const value = declarator?.childForFieldName('value') ?? null;
528
+ const name = declarator?.childForFieldName('name')?.text ?? null;
529
+ if (name !== null && value !== null && nodeType(value) === 'arrow_function') {
530
+ out.push(arrowSignatureFor(name, decl, value, docHost, root, src));
531
+ }
532
+ break;
533
+ }
534
+ case 'interface_declaration':
535
+ case 'type_alias_declaration': {
536
+ const name = decl.childForFieldName('name')?.text;
537
+ if (name !== undefined) {
538
+ out.push({
539
+ name,
540
+ line: nodeStartLine(decl),
541
+ signature: extractSignatureText(decl, src),
542
+ params: [],
543
+ returnType: null,
544
+ doc: getLeadingComment(docHost, root, src),
545
+ });
546
+ }
547
+ break;
548
+ }
549
+ case 'class_declaration':
550
+ case 'abstract_class_declaration': {
551
+ const className = decl.childForFieldName('name')?.text;
552
+ if (className === undefined)
553
+ break;
554
+ out.push({
555
+ name: className,
556
+ line: nodeStartLine(decl),
557
+ signature: extractSignatureText(decl, src),
558
+ params: [],
559
+ returnType: null,
560
+ doc: getLeadingComment(docHost, root, src),
561
+ });
562
+ const body = decl.childForFieldName('body') ?? findChildByType(decl, 'class_body');
563
+ if (body !== null) {
564
+ for (const member of nodeNamedChildren(body)) {
565
+ const mt = nodeType(member);
566
+ if (mt !== 'method_definition' && mt !== 'abstract_method_signature')
567
+ continue;
568
+ const mName = member.childForFieldName('name')?.text;
569
+ if (mName !== undefined)
570
+ out.push(signatureFor(`${className}.${mName}`, member, member, body, src));
571
+ }
572
+ }
573
+ break;
574
+ }
575
+ default:
576
+ break;
577
+ }
578
+ }
579
+ // Local re-export aliases (`export { foo as bar }`) expose foo's signature
580
+ // under `bar`, so mast_signature/mast_exports can look it up by export name.
581
+ for (const { local, alias } of localExportAliases(nodeChildren(root))) {
582
+ const base = out.find((s) => s.name === local);
583
+ if (base !== undefined)
584
+ out.push({ ...base, name: alias });
585
+ }
586
+ return out;
587
+ }
588
+ /** Signature info for a function/method node. */
589
+ function signatureFor(name, node, docHost, docParent, src) {
590
+ return {
591
+ name,
592
+ line: nodeStartLine(node),
593
+ signature: extractSignatureText(node, src),
594
+ params: paramsOf(node, src),
595
+ returnType: returnTypeOf(node, src),
596
+ doc: getLeadingComment(docHost, docParent, src),
597
+ };
598
+ }
599
+ /** Signature info for an arrow-function const (`export const f = (...) => ...`). */
600
+ function arrowSignatureFor(name, declNode, arrow, docHost, docParent, src) {
601
+ const body = arrow.childForFieldName('body');
602
+ // Declaration text up to the arrow body, with a trailing `=>` trimmed.
603
+ const raw = body !== null
604
+ ? src.slice(declNode.startIndex, body.startIndex).trimEnd()
605
+ : src.slice(declNode.startIndex, declNode.endIndex);
606
+ return {
607
+ name,
608
+ line: nodeStartLine(declNode),
609
+ signature: raw.replace(/=>\s*$/, '').trimEnd(),
610
+ params: paramsOf(arrow, src),
611
+ returnType: returnTypeOf(arrow, src),
612
+ doc: getLeadingComment(docHost, docParent, src),
613
+ };
614
+ }
615
+ /** Structured parameters from a node's `formal_parameters`. */
616
+ function paramsOf(node, src) {
617
+ const fp = node.childForFieldName('parameters') ?? findChildByType(node, 'formal_parameters');
618
+ if (fp === null)
619
+ return [];
620
+ const params = [];
621
+ for (const p of nodeNamedChildren(fp)) {
622
+ const pt = nodeType(p);
623
+ if (pt !== 'required_parameter' && pt !== 'optional_parameter')
624
+ continue;
625
+ const nameNode = p.childForFieldName('pattern') ?? findChildByType(p, 'identifier');
626
+ const name = nameNode !== null ? src.slice(nameNode.startIndex, nameNode.endIndex) : '';
627
+ params.push({ name, type: typeAnnotationText(p, src) ?? '' });
628
+ }
629
+ return params;
630
+ }
631
+ /** Return-type text from a node's `return_type` annotation, or null. */
632
+ function returnTypeOf(node, src) {
633
+ const rt = node.childForFieldName('return_type');
634
+ if (rt === null)
635
+ return null;
636
+ const typeNode = nodeNamedChildren(rt)[0];
637
+ return typeNode !== undefined ? src.slice(typeNode.startIndex, typeNode.endIndex) : null;
638
+ }
639
+ /** Full type text from a node's `type_annotation` child (e.g. `Promise<void>`). */
640
+ function typeAnnotationText(node, src) {
641
+ const ann = findChildByType(node, 'type_annotation');
642
+ if (ann === null)
643
+ return null;
644
+ const typeNode = nodeNamedChildren(ann)[0];
645
+ return typeNode !== undefined ? src.slice(typeNode.startIndex, typeNode.endIndex) : null;
646
+ }
647
+ // ---------------------------------------------------------------------------
648
+ // Stability hashes (§7.1) — computed from the AST, never from raw chunk text
649
+ // ---------------------------------------------------------------------------
650
+ /** sha256 of a declaration's signature (everything up to, not including, its body). */
651
+ function declHashOf(node, src) {
652
+ return sha256(extractSignatureText(node, src));
653
+ }
654
+ /** sha256 of a declaration's body block; empty-body hash when there is none. */
655
+ function bodyHashOf(node, src) {
656
+ const bodyNode = node.childForFieldName('body') ?? findChildByType(node, 'statement_block');
657
+ if (bodyNode === null)
658
+ return sha256('');
659
+ return sha256(src.slice(bodyNode.startIndex, bodyNode.endIndex));
660
+ }
661
+ /**
662
+ * Body hash for a `class_shell`: sha256 of the sorted member signatures (each
663
+ * with its leading doc), with NO method bodies (§10.1). This makes the shell
664
+ * re-embed when a method is renamed/added/removed but stay stable when only a
665
+ * method body changes — that change is captured by the method chunk's own hash.
666
+ */
667
+ /**
668
+ * Class-body members whose signatures make up the `class_shell` outline (§10.1).
669
+ * Shared by `synthesiseClassShell` (the outline text) and `classShellBodyHashOf`
670
+ * (its body hash) so the two cannot disagree on what counts as a member.
671
+ * `property_signature` covers ambient/`declare` class fields; `readonly_type`
672
+ * (a type-level node, not a member) is intentionally excluded.
673
+ */
674
+ function isClassShellMember(memberType) {
675
+ return (memberType === 'method_definition' ||
676
+ memberType === 'abstract_method_signature' ||
677
+ memberType === 'public_field_definition' ||
678
+ memberType === 'property_signature');
679
+ }
680
+ function classShellBodyHashOf(bodyNode, src) {
681
+ const sigs = [];
682
+ for (const member of nodeNamedChildren(bodyNode)) {
683
+ if (!isClassShellMember(nodeType(member)))
684
+ continue;
685
+ const doc = getLeadingComment(member, bodyNode, src) ?? '';
686
+ sigs.push((doc + '\n' + extractSignatureText(member, src)).trim());
687
+ }
688
+ sigs.sort();
689
+ return sha256(sigs.join('\n'));
690
+ }
691
+ /**
692
+ * Find the immediately preceding TSDoc or line comment for a node.
693
+ * Returns the comment text, or null if none found.
694
+ */
695
+ function getLeadingComment(node, parentNode, src) {
696
+ const siblings = nodeChildren(parentNode);
697
+ const nodeStart = node.startIndex;
698
+ // Walk backwards from the node's position to find an adjacent comment.
699
+ let precedingComment = null;
700
+ for (const sibling of siblings) {
701
+ if (sibling.endIndex > nodeStart)
702
+ break;
703
+ if (nodeType(sibling) === 'comment') {
704
+ precedingComment = sibling;
705
+ }
706
+ else {
707
+ // Reset if a non-comment node appears between the comment and our node.
708
+ precedingComment = null;
709
+ }
710
+ }
711
+ if (precedingComment === null)
712
+ return null;
713
+ return src.slice(precedingComment.startIndex, precedingComment.endIndex);
714
+ }
715
+ // ---------------------------------------------------------------------------
716
+ // Node helpers
717
+ // ---------------------------------------------------------------------------
718
+ function nodeType(node) {
719
+ return node.type;
720
+ }
721
+ function nodeStartLine(node) {
722
+ return node.startPosition.row + 1;
723
+ }
724
+ function nodeEndLine(node) {
725
+ return node.endPosition.row + 1;
726
+ }
727
+ function nodeChildren(node) {
728
+ return node.children;
729
+ }
730
+ function nodeNamedChildren(node) {
731
+ return node.namedChildren;
732
+ }
733
+ function findChildByType(node, type) {
734
+ for (const child of nodeChildren(node)) {
735
+ if (nodeType(child) === type)
736
+ return child;
737
+ }
738
+ return null;
739
+ }
740
+ /**
741
+ * Return the wrapped declaration node for `export function foo(){}` style.
742
+ * Returns null for `export { foo }` and `export * from '...'` forms.
743
+ */
744
+ function getWrappedDeclaration(exportStmtNode) {
745
+ const fieldDecl = exportStmtNode.childForFieldName('declaration');
746
+ if (fieldDecl !== null)
747
+ return fieldDecl;
748
+ // Fallback: look for a named child that is a declaration-type node
749
+ for (const child of nodeNamedChildren(exportStmtNode)) {
750
+ const t = nodeType(child);
751
+ if (t === 'function_declaration' ||
752
+ t === 'generator_function_declaration' ||
753
+ t === 'class_declaration' ||
754
+ t === 'abstract_class_declaration' ||
755
+ t === 'interface_declaration' ||
756
+ t === 'type_alias_declaration' ||
757
+ t === 'lexical_declaration' ||
758
+ t === 'variable_declaration' ||
759
+ t === 'enum_declaration') {
760
+ return child;
761
+ }
762
+ }
763
+ return null;
764
+ }
765
+ /**
766
+ * Local re-export aliases: `export { foo as bar }` (no `from` clause) pairs
767
+ * each local name with the exported alias. Used to expose the aliased
768
+ * declaration under its export name in both chunks and signatures.
769
+ */
770
+ function localExportAliases(topLevel) {
771
+ const aliases = [];
772
+ for (const node of topLevel) {
773
+ if (nodeType(node) !== 'export_statement')
774
+ continue;
775
+ if (hasFromClause(node))
776
+ continue;
777
+ if (getWrappedDeclaration(node) !== null)
778
+ continue;
779
+ const clause = findChildByType(node, 'export_clause');
780
+ if (clause === null)
781
+ continue;
782
+ for (const spec of nodeNamedChildren(clause)) {
783
+ if (nodeType(spec) !== 'export_specifier')
784
+ continue;
785
+ const local = spec.childForFieldName('name')?.text;
786
+ const alias = spec.childForFieldName('alias')?.text;
787
+ if (local !== undefined && alias !== undefined && alias !== local) {
788
+ aliases.push({ local, alias, line: nodeStartLine(spec) });
789
+ }
790
+ }
791
+ }
792
+ return aliases;
793
+ }
794
+ /**
795
+ * Return true if the export_statement has a `from` clause (re-exporting from another module).
796
+ */
797
+ function hasFromClause(node) {
798
+ for (const child of nodeChildren(node)) {
799
+ if (nodeType(child) === 'string')
800
+ return true; // `from './module'` has a string child
801
+ }
802
+ return false;
803
+ }
804
+ /**
805
+ * Extract `from`-clause re-exports from a file's top level (§10.1).
806
+ *
807
+ * - `export { Foo, Bar as Baz } from './x'` → named records (one per specifier).
808
+ * - `export * from './x'` (incl. `export * as ns from`) → star record; stars
809
+ * carry no per-symbol names, so they map to `re_export_files` rows rather
810
+ * than symbols/edges.
811
+ *
812
+ * Local aliases without `from` are NOT re-exports — they are handled by
813
+ * `localExportAliases`.
814
+ */
815
+ export function extractReExports(parsedTree) {
816
+ const named = [];
817
+ const stars = [];
818
+ for (const node of nodeChildren(parsedTree.rootNode)) {
819
+ if (nodeType(node) !== 'export_statement')
820
+ continue;
821
+ if (!hasFromClause(node))
822
+ continue;
823
+ const moduleNode = findChildByType(node, 'string');
824
+ if (moduleNode === null)
825
+ continue;
826
+ const module = moduleNode.text.slice(1, -1);
827
+ const clause = findChildByType(node, 'export_clause');
828
+ if (clause === null) {
829
+ // No specifier list + a from clause = `export * from` / `export * as ns from`.
830
+ stars.push({ module, line: nodeStartLine(node) });
831
+ continue;
832
+ }
833
+ for (const spec of nodeNamedChildren(clause)) {
834
+ if (nodeType(spec) !== 'export_specifier')
835
+ continue;
836
+ const sourceName = spec.childForFieldName('name')?.text;
837
+ if (sourceName === undefined)
838
+ continue;
839
+ const alias = spec.childForFieldName('alias')?.text;
840
+ named.push({
841
+ exportedName: alias ?? sourceName,
842
+ sourceName,
843
+ line: nodeStartLine(spec),
844
+ module,
845
+ });
846
+ }
847
+ }
848
+ return { named, stars };
849
+ }
850
+ /**
851
+ * Get the primary declared name from a declaration node, or null.
852
+ */
853
+ function getDeclName(node) {
854
+ const t = nodeType(node);
855
+ if (t === 'function_declaration' ||
856
+ t === 'generator_function_declaration' ||
857
+ t === 'class_declaration' ||
858
+ t === 'abstract_class_declaration' ||
859
+ t === 'interface_declaration' ||
860
+ t === 'type_alias_declaration' ||
861
+ t === 'enum_declaration') {
862
+ return node.childForFieldName('name')?.text ?? null;
863
+ }
864
+ if (t === 'lexical_declaration' || t === 'variable_declaration') {
865
+ const declarator = findChildByType(node, 'variable_declarator');
866
+ if (declarator === null)
867
+ return null;
868
+ return declarator.childForFieldName('name')?.text ?? null;
869
+ }
870
+ return null;
871
+ }
872
+ /**
873
+ * Return true if the class member has a `private` accessibility modifier.
874
+ */
875
+ function hasPrivateModifier(memberNode) {
876
+ for (const child of nodeChildren(memberNode)) {
877
+ const t = nodeType(child);
878
+ if (t === 'accessibility_modifier') {
879
+ return child.text === 'private';
880
+ }
881
+ // TypeScript grammar uses '#' for private fields (ESNext private syntax)
882
+ if (t === 'private_field_definition')
883
+ return true;
884
+ }
885
+ return false;
886
+ }
887
+ // ---------------------------------------------------------------------------
888
+ // Public helpers (re-exported from Stage 1 scaffold, now implemented)
889
+ // ---------------------------------------------------------------------------
890
+ export function sha256(text) {
891
+ return createHash('sha256').update(text).digest('hex');
892
+ }
893
+ export function chunkId(filePath, startLine) {
894
+ return sha256(`${filePath}:${startLine}`);
895
+ }
896
+ /** Determine language from extension. */
897
+ export function languageFromExt(ext) {
898
+ return ext === '.js' || ext === '.jsx' ? 'javascript' : 'typescript';
899
+ }
900
+ /**
901
+ * Expand chunk content by `contextLines` around `[startLine, endLine]`.
902
+ * Returns the expanded content slice; `startLine`/`endLine` in the chunk
903
+ * record always reflect AST boundaries, not the expanded region.
904
+ */
905
+ export function expandContent(lines, startLine, // 1-indexed
906
+ endLine, // 1-indexed, inclusive
907
+ contextLines) {
908
+ const expandedStart = Math.max(1, startLine - contextLines);
909
+ const expandedEnd = Math.min(lines.length, endLine + contextLines);
910
+ return lines.slice(expandedStart - 1, expandedEnd).join('\n');
911
+ }
912
+ // ---------------------------------------------------------------------------
913
+ // Graph data extraction (symbols, imports, identifiers)
914
+ // ---------------------------------------------------------------------------
915
+ /**
916
+ * Derive `SymbolRecord` entries from an already-extracted chunk list.
917
+ * Skips anonymous chunks (`symbol_name === null`) and raw `block` chunks.
918
+ */
919
+ export function symbolsFromChunks(chunks) {
920
+ const records = [];
921
+ for (const c of chunks) {
922
+ if (c.symbol_name === null || c.chunk_type === 'block')
923
+ continue;
924
+ records.push({
925
+ name: c.symbol_name,
926
+ kind: chunkTypeToKind(c.chunk_type),
927
+ line: c.start_line,
928
+ isExported: c.is_exported,
929
+ // AST-derived hashes attached during extraction (§7.1). Null only if a
930
+ // symbol chunk somehow lacked them (defensive; should not happen).
931
+ declarationHash: c.declaration_hash ?? null,
932
+ bodyHash: c.body_hash ?? null,
933
+ });
934
+ }
935
+ return records;
936
+ }
937
+ /**
938
+ * Extract import records from the tree-sitter AST.
939
+ *
940
+ * Handles named imports (`import { foo } from './bar'`). Side-effect imports
941
+ * (`import './side-effect'`), default imports, and namespace imports are
942
+ * recorded with an empty `symbols` array.
943
+ *
944
+ * Specifier-to-file resolution (relative probing, tsconfig aliases, workspace
945
+ * packages) is NOT done here — it needs project context and is applied by
946
+ * `extract.ts` via the import resolver (§13.7).
947
+ */
948
+ export function extractImports(parsedTree, _filePath) {
949
+ const topLevel = nodeChildren(parsedTree.rootNode);
950
+ const imports = [];
951
+ for (const node of topLevel) {
952
+ if (nodeType(node) !== 'import_statement')
953
+ continue;
954
+ // Module specifier: find the string child (quoted module path).
955
+ const moduleNode = findChildByType(node, 'string');
956
+ if (moduleNode === null)
957
+ continue;
958
+ // Strip surrounding quotes (' or ").
959
+ const module = moduleNode.text.slice(1, -1);
960
+ // Placeholder classification; extract.ts re-resolves authoritatively via
961
+ // the import resolver (§13.7 — tsconfig aliases, workspace packages,
962
+ // extension probing, symlink realpath). `resolvedPath` is filled there.
963
+ const isExternal = !module.startsWith('.') && !module.startsWith('/');
964
+ const resolvedPath = null;
965
+ // Extract named imports from the import_clause.
966
+ const symbols = [];
967
+ const importClause = findChildByType(node, 'import_clause');
968
+ if (importClause !== null) {
969
+ const namedImports = findChildByType(importClause, 'named_imports');
970
+ if (namedImports !== null) {
971
+ for (const specifier of nodeNamedChildren(namedImports)) {
972
+ if (nodeType(specifier) !== 'import_specifier')
973
+ continue;
974
+ const name = specifier.childForFieldName('name')?.text;
975
+ if (name !== undefined)
976
+ symbols.push(name);
977
+ }
978
+ }
979
+ }
980
+ imports.push({ module, symbols, isExternal, resolvedPath });
981
+ }
982
+ return imports;
983
+ }
984
+ /**
985
+ * Extract graph edge records from a parsed tree:
986
+ * - IMPLEMENTS — class → interface (`implements` clause)
987
+ * - EXTENDS — class/interface → its base (`extends` clause)
988
+ * - PARENT_OF — class → its method symbols
989
+ * - POTENTIAL_CALL — caller symbol → statically-resolved callee (§10.3.1)
990
+ *
991
+ * Edge records use symbol *names*; `insertEdges` resolves them to ids after all
992
+ * files' symbols are inserted (two-pass requirement). A POTENTIAL_CALL whose
993
+ * callee name is not a known indexed symbol is dropped there — that is how the
994
+ * "only a known symbol becomes a verified edge" rule from §10.3 is enforced.
995
+ *
996
+ * `src` is the raw source, used to attach the call-site line + context.
997
+ *
998
+ * `onCallSite` (D7, Stage 4) is an optional diagnostics seam: when provided,
999
+ * it is invoked exactly once per `call_expression` node returned by
1000
+ * `collectCalls`, classifying the outcome of resolving that call. It exists
1001
+ * to make the invariant "every visited call yields an edge or a recorded
1002
+ * drop-reason" assertable in tests (`call-oracle.test.ts`); it is not
1003
+ * persisted, configured, or surfaced in any tool response — a future
1004
+ * instrumentation hook (E2's registered corpus measurement), not a shipped
1005
+ * feature. Leaving it `undefined` (the default) costs one `undefined`-check
1006
+ * per call site and zero allocation on the production path.
1007
+ */
1008
+ export function extractEdges(parsedTree, _filePath, src, onCallSite) {
1009
+ const lines = src.split('\n');
1010
+ const topLevel = nodeChildren(parsedTree.rootNode);
1011
+ const edges = [];
1012
+ // File-scoped callables: named imports + same-file top-level symbol names.
1013
+ const importedNames = [];
1014
+ const sameFileNames = [];
1015
+ for (const node of topLevel) {
1016
+ if (nodeType(node) === 'import_statement') {
1017
+ const importClause = findChildByType(node, 'import_clause');
1018
+ const namedImports = importClause !== null ? findChildByType(importClause, 'named_imports') : null;
1019
+ if (namedImports !== null) {
1020
+ for (const spec of nodeNamedChildren(namedImports)) {
1021
+ if (nodeType(spec) !== 'import_specifier')
1022
+ continue;
1023
+ const name = spec.childForFieldName('name')?.text;
1024
+ if (name !== undefined)
1025
+ importedNames.push(name);
1026
+ }
1027
+ }
1028
+ continue;
1029
+ }
1030
+ const decl = nodeType(node) === 'export_statement' ? getWrappedDeclaration(node) : node;
1031
+ if (decl === null)
1032
+ continue;
1033
+ const name = getDeclName(decl);
1034
+ if (name !== null)
1035
+ sameFileNames.push(name);
1036
+ }
1037
+ const seedFileScope = (env) => {
1038
+ for (const n of importedNames)
1039
+ env.recordImport(n);
1040
+ for (const n of sameFileNames)
1041
+ env.recordSameFileSymbol(n);
1042
+ };
1043
+ for (const node of topLevel) {
1044
+ const declNode = nodeType(node) === 'export_statement' ? getWrappedDeclaration(node) : node;
1045
+ if (declNode === null)
1046
+ continue;
1047
+ const t = nodeType(declNode);
1048
+ if (t === 'class_declaration' || t === 'abstract_class_declaration') {
1049
+ emitClassEdges(declNode, edges, seedFileScope, lines, onCallSite);
1050
+ }
1051
+ else if (t === 'interface_declaration') {
1052
+ emitInterfaceExtends(declNode, edges);
1053
+ }
1054
+ else if (t === 'function_declaration' || t === 'generator_function_declaration') {
1055
+ const name = declNode.childForFieldName('name')?.text ?? null;
1056
+ const body = declNode.childForFieldName('body');
1057
+ if (name !== null && body !== null) {
1058
+ emitCallEdges(name, declNode.childForFieldName('parameters'), body, edges, seedFileScope, lines, [], onCallSite);
1059
+ }
1060
+ }
1061
+ else if (t === 'lexical_declaration' || t === 'variable_declaration') {
1062
+ const declarator = findChildByType(declNode, 'variable_declarator');
1063
+ const value = declarator?.childForFieldName('value') ?? null;
1064
+ const name = declarator?.childForFieldName('name')?.text ?? null;
1065
+ if (name !== null && value !== null && nodeType(value) === 'arrow_function') {
1066
+ const body = value.childForFieldName('body');
1067
+ if (body !== null) {
1068
+ emitCallEdges(name, value.childForFieldName('parameters'), body, edges, seedFileScope, lines, [], onCallSite);
1069
+ }
1070
+ }
1071
+ }
1072
+ }
1073
+ return edges;
1074
+ }
1075
+ /** IMPLEMENTS + EXTENDS + PARENT_OF + per-method POTENTIAL_CALL for one class. */
1076
+ function emitClassEdges(classNode, edges, seedFileScope, lines, onCallSite) {
1077
+ const className = classNode.childForFieldName('name')?.text ?? null;
1078
+ if (className === null)
1079
+ return;
1080
+ // tree-sitter-typescript wraps extends/implements in a `class_heritage` node.
1081
+ const heritage = findChildByType(classNode, 'class_heritage');
1082
+ const implClause = heritage !== null
1083
+ ? findChildByType(heritage, 'implements_clause')
1084
+ : findChildByType(classNode, 'implements_clause');
1085
+ if (implClause !== null) {
1086
+ for (const typeRef of nodeNamedChildren(implClause)) {
1087
+ const ifaceName = typeRefName(typeRef);
1088
+ if (ifaceName !== null)
1089
+ edges.push({ fromName: className, toName: ifaceName, edgeType: 'IMPLEMENTS' });
1090
+ }
1091
+ }
1092
+ // EXTENDS: the `extends` clause names the base class. `baseClassName` is
1093
+ // also the F4 `super_method` receiver binding's type below — captured here
1094
+ // rather than re-derived so the binding and the edge agree by construction.
1095
+ const extendsClause = heritage !== null
1096
+ ? findChildByType(heritage, 'extends_clause')
1097
+ : findChildByType(classNode, 'extends_clause');
1098
+ let baseClassName = null;
1099
+ if (extendsClause !== null) {
1100
+ for (const typeRef of nodeNamedChildren(extendsClause)) {
1101
+ const name = typeRefName(typeRef);
1102
+ if (name !== null) {
1103
+ baseClassName = name;
1104
+ edges.push({ fromName: className, toName: name, edgeType: 'EXTENDS' });
1105
+ break; // a class extends at most one base
1106
+ }
1107
+ }
1108
+ }
1109
+ const bodyNode = classNode.childForFieldName('body') ?? findChildByType(classNode, 'class_body');
1110
+ if (bodyNode === null)
1111
+ return;
1112
+ // Class-wide receiver bindings: constructor parameter properties + fields,
1113
+ // plus F4's `this`/`super` bindings — riding the same receiver-binding
1114
+ // machinery LocalTypeEnvironment already provides for `field_type` etc.,
1115
+ // per §10.3.1 "Method calls on super and this". No `super` binding when
1116
+ // there is no parent class: an unresolvable super-call must fall through
1117
+ // to the identifier_fts potential set, never guess a target.
1118
+ const classScopeBindings = [
1119
+ ...collectClassFieldBindings(bodyNode),
1120
+ { receiver: 'this', type: className, resolution: 'this_method' },
1121
+ ];
1122
+ if (baseClassName !== null) {
1123
+ classScopeBindings.push({ receiver: 'super', type: baseClassName, resolution: 'super_method' });
1124
+ }
1125
+ for (const member of nodeNamedChildren(bodyNode)) {
1126
+ const mt = nodeType(member);
1127
+ if (mt !== 'method_definition' && mt !== 'abstract_method_signature')
1128
+ continue;
1129
+ const methodName = member.childForFieldName('name')?.text ?? null;
1130
+ if (methodName === null)
1131
+ continue;
1132
+ edges.push({ fromName: className, toName: `${className}.${methodName}`, edgeType: 'PARENT_OF' });
1133
+ const body = member.childForFieldName('body');
1134
+ if (body === null)
1135
+ continue; // abstract / no body
1136
+ emitCallEdges(`${className}.${methodName}`, member.childForFieldName('parameters'), body, edges, seedFileScope, lines, classScopeBindings, onCallSite);
1137
+ }
1138
+ }
1139
+ /** interface extends interface(s). */
1140
+ function emitInterfaceExtends(ifaceNode, edges) {
1141
+ const name = ifaceNode.childForFieldName('name')?.text ?? null;
1142
+ if (name === null)
1143
+ return;
1144
+ const extendsClause = findChildByType(ifaceNode, 'extends_type_clause')
1145
+ ?? findChildByType(ifaceNode, 'extends_clause');
1146
+ if (extendsClause === null)
1147
+ return;
1148
+ for (const typeRef of nodeNamedChildren(extendsClause)) {
1149
+ const baseName = typeRefName(typeRef);
1150
+ if (baseName !== null)
1151
+ edges.push({ fromName: name, toName: baseName, edgeType: 'EXTENDS' });
1152
+ }
1153
+ }
1154
+ /**
1155
+ * Line of the callee TOKEN itself — the method name for `obj.method()`, or
1156
+ * the identifier for a bare `foo()`. Deliberately NOT `call.startPosition`:
1157
+ * for a multi-line fluent/chained call (e.g. `program\n .command(...)`),
1158
+ * the call_expression node's own start position is the RECEIVER's line, not
1159
+ * the line the call syntax actually appears on — which previously produced
1160
+ * a `context` string with no parentheses at all (e.g. the trimmed text
1161
+ * `program`), violating `EdgeRecord.context`'s "source text of the
1162
+ * call-site line" contract. Found by the D7 self-oracle test running over
1163
+ * mast's own `src/` corpus (`cli/index-cmd.ts:9`, `program.command(...)`);
1164
+ * see IMPLEMENTATION_PLAN.md's D7 result for the finding.
1165
+ */
1166
+ function calleeLine(call) {
1167
+ const fn = call.childForFieldName('function') ?? call.namedChildren[0] ?? null;
1168
+ if (fn !== null && nodeType(fn) === 'member_expression') {
1169
+ const property = fn.childForFieldName('property');
1170
+ if (property !== null)
1171
+ return property.startPosition.row + 1;
1172
+ }
1173
+ return call.startPosition.row + 1;
1174
+ }
1175
+ /**
1176
+ * Build the local type environment for a function/method scope and emit one
1177
+ * POTENTIAL_CALL edge per statically-resolvable call site in its body.
1178
+ */
1179
+ function emitCallEdges(fromName, paramsNode, bodyNode, edges, seedFileScope, lines,
1180
+ // Class-wide bindings for a method scope: field/constructor-parameter
1181
+ // types (`this.x`) plus F4's `this`/`super` bindings. Empty for top-level
1182
+ // function/arrow scopes, which have no enclosing class.
1183
+ classScopeBindings = [], onCallSite) {
1184
+ const env = new LocalTypeEnvironment();
1185
+ seedFileScope(env);
1186
+ for (const b of classScopeBindings)
1187
+ env.recordReceiverType(b.receiver, b.type, b.resolution);
1188
+ if (paramsNode !== null) {
1189
+ for (const b of collectParamBindings(paramsNode))
1190
+ env.recordReceiverType(b.receiver, b.type, b.resolution);
1191
+ }
1192
+ for (const b of collectNewBindings(bodyNode))
1193
+ env.recordReceiverType(b.receiver, b.type, b.resolution);
1194
+ for (const call of collectCalls(bodyNode)) {
1195
+ const parsed = parseCallee(call);
1196
+ if (parsed === null) {
1197
+ onCallSite?.('unparseable_callee');
1198
+ continue;
1199
+ }
1200
+ const resolved = env.resolveCall(parsed.receiver, parsed.method);
1201
+ if (resolved === null) {
1202
+ onCallSite?.(parsed.receiver === null ? 'bare_call_unresolved' : 'unresolved_receiver');
1203
+ continue;
1204
+ }
1205
+ onCallSite?.('edge_emitted');
1206
+ const line = calleeLine(call);
1207
+ edges.push({
1208
+ fromName,
1209
+ toName: resolved.callee,
1210
+ edgeType: 'POTENTIAL_CALL',
1211
+ resolution: resolved.resolution,
1212
+ callLine: line,
1213
+ context: (lines[line - 1] ?? '').trim(),
1214
+ });
1215
+ }
1216
+ }
1217
+ /** Constructor parameter properties + plain field declarations → `this.x` bindings. */
1218
+ function collectClassFieldBindings(classBody) {
1219
+ const bindings = [];
1220
+ for (const member of nodeNamedChildren(classBody)) {
1221
+ const mt = nodeType(member);
1222
+ if (mt === 'public_field_definition' || mt === 'property_signature') {
1223
+ const name = member.childForFieldName('name')?.text ?? null;
1224
+ const type = annotationTypeName(member);
1225
+ if (name !== null && type !== null)
1226
+ bindings.push({ receiver: `this.${name}`, type, resolution: 'field_type' });
1227
+ }
1228
+ else if (mt === 'method_definition' && member.childForFieldName('name')?.text === 'constructor') {
1229
+ const params = member.childForFieldName('parameters');
1230
+ if (params !== null) {
1231
+ for (const param of nodeNamedChildren(params)) {
1232
+ // A constructor parameter property carries an accessibility modifier.
1233
+ if (findChildByType(param, 'accessibility_modifier') === null)
1234
+ continue;
1235
+ const name = findChildByType(param, 'identifier')?.text ?? null;
1236
+ const type = annotationTypeName(param);
1237
+ if (name !== null && type !== null)
1238
+ bindings.push({ receiver: `this.${name}`, type, resolution: 'field_type' });
1239
+ }
1240
+ }
1241
+ }
1242
+ }
1243
+ return bindings;
1244
+ }
1245
+ /** Annotated (non-property) parameters → bare-name receiver bindings. */
1246
+ function collectParamBindings(paramsNode) {
1247
+ const bindings = [];
1248
+ for (const param of nodeNamedChildren(paramsNode)) {
1249
+ const pt = nodeType(param);
1250
+ if (pt !== 'required_parameter' && pt !== 'optional_parameter')
1251
+ continue;
1252
+ if (findChildByType(param, 'accessibility_modifier') !== null)
1253
+ continue; // handled as a field
1254
+ const name = findChildByType(param, 'identifier')?.text ?? null;
1255
+ const type = annotationTypeName(param);
1256
+ if (name !== null && type !== null)
1257
+ bindings.push({ receiver: name, type, resolution: 'parameter_type' });
1258
+ }
1259
+ return bindings;
1260
+ }
1261
+ /** `const x = new Foo()` → `x` resolves to type `Foo`. */
1262
+ function collectNewBindings(bodyNode) {
1263
+ const bindings = [];
1264
+ const visit = (node) => {
1265
+ if (nodeType(node) === 'variable_declarator') {
1266
+ const value = node.childForFieldName('value');
1267
+ const name = node.childForFieldName('name')?.text ?? null;
1268
+ if (name !== null && value !== null && nodeType(value) === 'new_expression') {
1269
+ const ctor = value.childForFieldName('constructor') ?? value.namedChildren[0] ?? null;
1270
+ const type = ctor !== null && nodeType(ctor) === 'identifier' ? ctor.text : null;
1271
+ if (type !== null)
1272
+ bindings.push({ receiver: name, type, resolution: 'new_expression' });
1273
+ }
1274
+ }
1275
+ for (const child of nodeNamedChildren(node))
1276
+ visit(child);
1277
+ };
1278
+ visit(bodyNode);
1279
+ return bindings;
1280
+ }
1281
+ /** Collect call_expression nodes within a body, not descending into nested
1282
+ * named/anonymous functions, generators, methods, or classes (their bodies
1283
+ * are their own scope — non-arrow functions also get their own dynamic
1284
+ * `this`, so a `this.foo()` inside one is never the enclosing class
1285
+ * instance; F4's `this_method`/`super_method` bindings must not leak in.
1286
+ * Arrow functions are deliberately NOT in this list — they inherit the
1287
+ * enclosing scope's `this` and calls inside them must still resolve).
1288
+ *
1289
+ * Exported (D7, Stage 4) so `call-oracle.test.ts` can independently
1290
+ * enumerate the same call sites `emitCallEdges` visits, as the ground
1291
+ * truth for the `onCallSite` accounting invariant — not part of the
1292
+ * extractor's public tool-facing surface. */
1293
+ export function collectCalls(bodyNode) {
1294
+ const calls = [];
1295
+ const visit = (node) => {
1296
+ for (const child of nodeNamedChildren(node)) {
1297
+ const t = nodeType(child);
1298
+ if (t === 'function_declaration' || t === 'method_definition' ||
1299
+ t === 'class_declaration' || t === 'abstract_class_declaration' ||
1300
+ t === 'function_expression' || t === 'generator_function' ||
1301
+ t === 'generator_function_declaration') {
1302
+ continue;
1303
+ }
1304
+ if (t === 'call_expression')
1305
+ calls.push(child);
1306
+ visit(child);
1307
+ }
1308
+ };
1309
+ visit(bodyNode);
1310
+ return calls;
1311
+ }
1312
+ /** Extract `{ receiver, method }` from a call expression, or null if unhandled. */
1313
+ function parseCallee(call) {
1314
+ const fn = call.childForFieldName('function') ?? call.namedChildren[0] ?? null;
1315
+ if (fn === null)
1316
+ return null;
1317
+ if (nodeType(fn) === 'identifier') {
1318
+ return { receiver: null, method: fn.text };
1319
+ }
1320
+ if (nodeType(fn) === 'member_expression') {
1321
+ const method = fn.childForFieldName('property')?.text ?? null;
1322
+ const objectNode = fn.childForFieldName('object');
1323
+ if (method === null || objectNode === null)
1324
+ return null;
1325
+ const receiver = receiverString(objectNode);
1326
+ if (receiver === null)
1327
+ return null;
1328
+ return { receiver, method };
1329
+ }
1330
+ return null;
1331
+ }
1332
+ /**
1333
+ * Unwrap `(await x)` down to `x` for the receiver position (F3). Tree-sitter
1334
+ * parses `(await x).m()`'s object as `parenthesized_expression(
1335
+ * await_expression(x))` — verified via a tree-sitter S-expression dump
1336
+ * against `tree-sitter-typescript`'s current grammar. Only unwraps an
1337
+ * await specifically (not parens in general) — this is syntax unwrapping,
1338
+ * not type inference: `const y = await makeFoo(); y.bar()` still does not
1339
+ * resolve, per §10.3.1's "does NOT catch" list.
1340
+ */
1341
+ function unwrapAwaitedReceiver(node) {
1342
+ if (nodeType(node) !== 'parenthesized_expression')
1343
+ return node;
1344
+ const inner = node.namedChildren[0];
1345
+ if (inner === undefined || nodeType(inner) !== 'await_expression')
1346
+ return node;
1347
+ const awaited = inner.namedChildren[0];
1348
+ return awaited ?? node;
1349
+ }
1350
+ /** Stringify a member-expression receiver for the conservative resolver. */
1351
+ function receiverString(objectNode) {
1352
+ const node = unwrapAwaitedReceiver(objectNode);
1353
+ const t = nodeType(node);
1354
+ if (t === 'identifier')
1355
+ return node.text;
1356
+ // F4: bare `this`/`super` as the receiver — `this.foo()`/`super.foo()`.
1357
+ // The env bindings for these two literal strings are seeded per-method by
1358
+ // `emitClassEdges`; outside a class scope (or inside a nested function
1359
+ // that shadows `this`, per collectCalls above) no binding exists and
1360
+ // `resolveCall` returns null, same as any other unbound receiver.
1361
+ if (t === 'this')
1362
+ return 'this';
1363
+ if (t === 'super')
1364
+ return 'super';
1365
+ if (t === 'member_expression') {
1366
+ const inner = node.childForFieldName('object');
1367
+ const prop = node.childForFieldName('property')?.text ?? null;
1368
+ if (inner !== null && prop !== null && nodeType(inner) === 'this')
1369
+ return `this.${prop}`;
1370
+ }
1371
+ return null;
1372
+ }
1373
+ /** Name of a type reference node in a heritage clause. */
1374
+ function typeRefName(typeRef) {
1375
+ const t = nodeType(typeRef);
1376
+ if (t === 'type_identifier' || t === 'identifier')
1377
+ return typeRef.text;
1378
+ if (t === 'generic_type')
1379
+ return typeRef.childForFieldName('name')?.text ?? null;
1380
+ return null;
1381
+ }
1382
+ /** Resolve the named type from a node's `type_annotation` child, if simple. */
1383
+ function annotationTypeName(node) {
1384
+ const annotation = findChildByType(node, 'type_annotation');
1385
+ if (annotation === null)
1386
+ return null;
1387
+ for (const child of nodeNamedChildren(annotation)) {
1388
+ const t = nodeType(child);
1389
+ if (t === 'type_identifier')
1390
+ return child.text;
1391
+ if (t === 'generic_type')
1392
+ return child.childForFieldName('name')?.text ?? null;
1393
+ }
1394
+ return null;
1395
+ }
1396
+ /**
1397
+ * Extract all identifier tokens from chunk content for `identifier_fts`.
1398
+ * Returns a whitespace-separated, deduplicated list of identifier strings.
1399
+ * Uses a word-boundary regex — sufficient for the unicode61 tokeniser's
1400
+ * identifier-boundary separators.
1401
+ */
1402
+ export function extractIdentifiers(content) {
1403
+ const re = /\b[A-Za-z_$][A-Za-z0-9_$]*\b/g;
1404
+ const seen = new Set();
1405
+ let m;
1406
+ while ((m = re.exec(content)) !== null) {
1407
+ seen.add(m[0]);
1408
+ }
1409
+ return [...seen].join(' ');
1410
+ }
1411
+ /**
1412
+ * Append qualified compound strings (e.g. "Class.method", F5) to an
1413
+ * already-built identifier bag, deduplicated and appended AFTER the bare
1414
+ * bag — never interleaved — so the qualified compound's own two sub-tokens
1415
+ * (post unicode61 tokenisation, which splits on '.') land adjacent to each
1416
+ * other and are not accidentally split apart by an unrelated bare token.
1417
+ */
1418
+ function appendQualifiedCompounds(base, extra) {
1419
+ if (extra.length === 0)
1420
+ return base;
1421
+ const seen = new Set(base.length > 0 ? base.split(' ') : []);
1422
+ for (const q of extra)
1423
+ seen.add(q);
1424
+ return [...seen].join(' ');
1425
+ }
1426
+ // ---------------------------------------------------------------------------
1427
+ // Private hash helpers for symbol records
1428
+ // ---------------------------------------------------------------------------
1429
+ function chunkTypeToKind(t) {
1430
+ switch (t) {
1431
+ case 'function': return 'function';
1432
+ case 'class_shell': return 'class';
1433
+ case 'method': return 'method';
1434
+ case 'interface': return 'interface';
1435
+ case 'type': return 'type';
1436
+ default: return 'const';
1437
+ }
1438
+ }
1439
+ //# sourceMappingURL=typescript.js.map