@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,242 @@
1
+ /**
2
+ * Ranker D — the declaration-exact ranker: a match against `chunks.symbol_name`
3
+ * (full-name or final-dot-segment), case-insensitive, deterministically
4
+ * ordered.
5
+ *
6
+ * Ported from `eval/declex-ranker.mjs`, the Q1/DECLEX-measured construction
7
+ * (IMPLEMENTATION_PLAN.md § "Q1/DECLEX — the declaration-exact ranker" +
8
+ * AMENDMENT 1, commit dd10796; Stage 6 of the productization plan). PRIMARY
9
+ * ARM ONLY — this file intentionally does NOT port the escape variant
10
+ * (`escape`/`escapeCap` options, `escapeEligibleTerms`/
11
+ * `lowercaseTokenMatchCounts` diagnostics) or `classifyTargetReach`
12
+ * (instrument-analysis machinery, not product). The escape variant was
13
+ * measured harmful off-stratum (M2 decision memo) and is barred from
14
+ * shipping without a fresh pre-registration.
15
+ *
16
+ * Ranker D queries the `chunks` table DIRECTLY (not `chunk_fts`/
17
+ * `identifier_fts`): `chunk_fts.symbol_name` is stored UNINDEXED
18
+ * (`src/graph/db.ts`) — not reachable via FTS5 MATCH — and the registered
19
+ * match rule ("equals the chunk's own symbol_name OR its final dot-segment")
20
+ * is a structural string comparison, not a full-text match, so a direct SQL
21
+ * predicate against `chunks.symbol_name` is the correct primitive.
22
+ */
23
+ import { sql } from 'kysely';
24
+ import { compileScopeMatcher } from './scope.js';
25
+ /**
26
+ * Ranker D's candidate pool multiplier — 4x the caller's own `limit`, the
27
+ * SAME convention as ranker I (`eval/idfuse-ranker.mjs`, AMENDMENT 1 F8) and
28
+ * the pre-Stage-7 vector search helper: the caller multiplies by 4 and passes
29
+ * the already-multiplied limit directly; `searchRankerD` applies no further
30
+ * internal multiplication.
31
+ */
32
+ export const RANKER_D_POOL_MULTIPLIER = 4;
33
+ // ---------------------------------------------------------------------------
34
+ // Term derivation + eligibility gate (pure, no DB)
35
+ // ---------------------------------------------------------------------------
36
+ /** Raw token split — identical character class to ranker I's own derivation
37
+ * (`eval/idfuse-ranker.mjs`); no camelCase split, no lowercasing. `Class.method`
38
+ * splits into two independent terms (the dot is not in the character class). */
39
+ export function deriveRankerDTerms(query) {
40
+ return query.match(/[A-Za-z0-9_$]+/g) ?? [];
41
+ }
42
+ const DIGIT_ADJACENT_LETTER = /[0-9][A-Za-z]|[A-Za-z][0-9]/;
43
+ /**
44
+ * Symbol-shaped eligibility predicate for ranker D's PRIMARY arm — reproduces
45
+ * `isSymbolShapedTerm` (`eval/idfuse-ranker.mjs`) MINUS the dead
46
+ * `term.includes('.')` clause (registration: "the implementation also tests
47
+ * term.includes('.'), but the upstream split character class
48
+ * `/[A-Za-z0-9_$]+/` never includes `.` in any surviving token ... Ranker D's
49
+ * eligibility gate reproduces this predicate but drops the dead clause rather
50
+ * than reimplementing dead code"). A term qualifies if it contains an
51
+ * uppercase letter, an underscore, a dollar sign, or a digit adjacent to a
52
+ * letter.
53
+ */
54
+ export function isEligiblePrimaryTerm(term) {
55
+ return (/[A-Z]/.test(term) ||
56
+ term.includes('_') ||
57
+ term.includes('$') ||
58
+ DIGIT_ADJACENT_LETTER.test(term));
59
+ }
60
+ /** Ranker D's PRIMARY-arm eligible terms: raw split, filtered to symbol-shaped. */
61
+ export function deriveEligiblePrimaryTerms(query) {
62
+ return deriveRankerDTerms(query).filter(isEligiblePrimaryTerm);
63
+ }
64
+ // ---------------------------------------------------------------------------
65
+ // SQL match primitive — direct predicate against `chunks.symbol_name`
66
+ // ---------------------------------------------------------------------------
67
+ /** Escape `\`, `%`, `_` for a SQLite LIKE pattern (registration's segment
68
+ * match is a STRUCTURAL suffix-after-dot comparison; a raw token containing
69
+ * `_` — a legal, even common, eligible character per the split regex — would
70
+ * otherwise be silently reinterpreted as a single-character LIKE wildcard,
71
+ * producing false-positive segment matches. `ESCAPE '\'` in the query below
72
+ * neutralises that.) */
73
+ function escapeLikeToken(token) {
74
+ return token.replace(/[\\%_]/g, (c) => `\\${c}`);
75
+ }
76
+ /**
77
+ * Query `chunks` for every row whose `symbol_name` equals `token`
78
+ * case-insensitively (full-name match) OR whose `symbol_name` ends with
79
+ * `.` + `token` case-insensitively (segment match — the only chunk type with
80
+ * a dot in `symbol_name` is `method`, `symbol_name = `${className}.${methodName}``,
81
+ * `typescript.ts:324`; a chunk with no dot can only ever full-name-match, per
82
+ * the registration's "no segment logic needed for class_shell").
83
+ *
84
+ * Full-name equality is a plain parameterized `=` (no wildcard risk). Segment
85
+ * matching uses `LIKE` with the token pre-escaped (see `escapeLikeToken`) so
86
+ * an underscore inside the token cannot masquerade as a wildcard.
87
+ */
88
+ async function matchToken(db, token) {
89
+ const lower = token.toLowerCase();
90
+ const likePattern = `%.${escapeLikeToken(lower)}`;
91
+ const rows = await db
92
+ .selectFrom('chunks')
93
+ .select(['chunk_id', 'symbol_name', 'chunk_type', 'parent_symbol', 'file_path', 'language'])
94
+ .where('symbol_name', 'is not', null)
95
+ // NOTE: the doubled backslash below is JS template-literal escaping for a
96
+ // SINGLE literal backslash in the resulting SQL text (`ESCAPE '\'`) — a
97
+ // lone `\'` here would be parsed by JS as an escaped quote, silently
98
+ // dropping the backslash and producing invalid SQL (`ESCAPE ''`).
99
+ .where(sql `(LOWER(symbol_name) = ${lower} OR LOWER(symbol_name) LIKE ${likePattern} ESCAPE '\\')`)
100
+ .execute();
101
+ // `WHERE symbol_name IS NOT NULL` guarantees no row here has a null
102
+ // symbol_name, but Kysely's column type (`string | null`, per the schema)
103
+ // does not narrow from a runtime predicate — filter with a type guard
104
+ // rather than a non-null assertion (banned in product code, §3.1).
105
+ return rows
106
+ .filter((r) => r.symbol_name !== null)
107
+ .map((r) => {
108
+ const matchType = r.symbol_name.toLowerCase() === lower ? 'full' : 'segment';
109
+ return {
110
+ chunk_id: r.chunk_id,
111
+ symbol_name: r.symbol_name,
112
+ chunk_type: r.chunk_type,
113
+ parent_symbol: r.parent_symbol,
114
+ file_path: r.file_path,
115
+ language: r.language,
116
+ match_type: matchType,
117
+ };
118
+ });
119
+ }
120
+ /**
121
+ * Drop candidates outside the caller's scope.
122
+ *
123
+ * Applied to the merged pool BEFORE ordering and capping, not after, for two
124
+ * reasons. The cap would otherwise be spent on rows that are about to be
125
+ * discarded, so a narrow `file_pattern` over a crowded symbol name would
126
+ * silently return fewer in-scope declarations than the index holds — the same
127
+ * silently-incomplete answer the scope filter exists to prevent. And the
128
+ * ordering itself is scope-dependent: rank position 2 is "how many candidates
129
+ * share this matched name", which is a property of the pool being ranked, so a
130
+ * scoped search must count within its own scope to order consistently with what
131
+ * it returns.
132
+ */
133
+ function applyScope(candidates, matcher) {
134
+ if (matcher === null)
135
+ return [...candidates];
136
+ return candidates.filter((c) => matcher(c.file_path, c.language));
137
+ }
138
+ /** The final dot-segment of a symbol name (itself, if no dot present). */
139
+ function finalDotSegment(symbolName) {
140
+ const idx = symbolName.lastIndexOf('.');
141
+ return idx === -1 ? symbolName : symbolName.slice(idx + 1);
142
+ }
143
+ /**
144
+ * Ranker D: declaration-exact match against `chunks.symbol_name` (full-name
145
+ * or final-dot-segment), case-insensitive, deterministically ordered.
146
+ *
147
+ * Ordering (registration): full-name matches before segment-only matches;
148
+ * then fewer total same-matched-name candidates first; then ascending
149
+ * chunk_id. JUDGMENT CALL (flagged per this codebase's convention, e.g.
150
+ * `eval/idfuse-score.mjs`'s a-fortiori routing note): "same-name" is read as
151
+ * "same MATCHED name" — full-name candidates group by their own
152
+ * `symbol_name`; segment candidates group by the shared final dot-segment
153
+ * (the quantity the design review's F-4 finding is actually about — a
154
+ * `toJSON`-class query facing ~140 candidates that all share the SEGMENT
155
+ * "toJSON", not 140 candidates sharing one `symbol_name`, since each belongs
156
+ * to a different class). This groups the exact multiplicity class the
157
+ * registration's own fixture (Gate B, high-multiplicity segment) is built to
158
+ * exercise.
159
+ *
160
+ * `options.scope` restricts the candidate pool to the caller's `file_pattern` /
161
+ * `language` before any of the above is computed, so `diagnostics.candidate_count`
162
+ * and the ordering both describe the scoped pool — a scoped search behaves as
163
+ * though the index contained only the files in scope.
164
+ *
165
+ * @param query - the RAW query string (term derivation happens here).
166
+ */
167
+ export async function searchRankerD(db, query, options) {
168
+ const rawTerms = deriveRankerDTerms(query);
169
+ const primaryTerms = [...new Set(rawTerms.filter(isEligiblePrimaryTerm))];
170
+ // matchesByTerm caches every matchToken() call so the final candidate
171
+ // assembly never re-queries the same term twice.
172
+ const matchesByTerm = new Map();
173
+ for (const t of primaryTerms)
174
+ matchesByTerm.set(t, await matchToken(db, t));
175
+ if (primaryTerms.length === 0) {
176
+ return {
177
+ rows: [],
178
+ diagnostics: {
179
+ fired: false,
180
+ top_match_channel: null,
181
+ candidate_count: 0,
182
+ primary_eligible_terms: primaryTerms,
183
+ },
184
+ };
185
+ }
186
+ // Merge per-term matches into one candidate map keyed by chunk_id — a
187
+ // chunk reachable via more than one eligible term is counted once. If any
188
+ // matching term produced a 'full' hit for that chunk, 'full' wins (a chunk
189
+ // can only structurally be a 'full' match for its OWN symbol_name and a
190
+ // 'segment' match for its OWN symbol_name's final segment — these are the
191
+ // same string only when the symbol_name has no dot, so this precedence
192
+ // never silently discards information; it only resolves the case where two
193
+ // DIFFERENT terms both matched the same chunk).
194
+ const byChunkId = new Map();
195
+ for (const t of primaryTerms) {
196
+ const matches = matchesByTerm.get(t) ?? [];
197
+ for (const m of matches) {
198
+ const existing = byChunkId.get(m.chunk_id);
199
+ if (existing === undefined || (existing.match_type === 'segment' && m.match_type === 'full')) {
200
+ byChunkId.set(m.chunk_id, m);
201
+ }
202
+ }
203
+ }
204
+ const candidates = applyScope([...byChunkId.values()], compileScopeMatcher(options.scope ?? {}));
205
+ // Ordering: matchedName grouping (see JSDoc above) — count how many
206
+ // candidates in THIS query's pool share the same (case-insensitive)
207
+ // matched name, then sort full-before-segment, ascending count, ascending
208
+ // chunk_id.
209
+ const matchedNameOf = (c) => (c.match_type === 'full' ? c.symbol_name : finalDotSegment(c.symbol_name)).toLowerCase();
210
+ const nameCounts = new Map();
211
+ for (const c of candidates) {
212
+ const key = matchedNameOf(c);
213
+ nameCounts.set(key, (nameCounts.get(key) ?? 0) + 1);
214
+ }
215
+ candidates.sort((a, b) => {
216
+ const typeRank = (c) => (c.match_type === 'full' ? 0 : 1);
217
+ if (typeRank(a) !== typeRank(b))
218
+ return typeRank(a) - typeRank(b);
219
+ const countA = nameCounts.get(matchedNameOf(a)) ?? 0;
220
+ const countB = nameCounts.get(matchedNameOf(b)) ?? 0;
221
+ if (countA !== countB)
222
+ return countA - countB;
223
+ return a.chunk_id < b.chunk_id ? -1 : a.chunk_id > b.chunk_id ? 1 : 0;
224
+ });
225
+ const capped = candidates.slice(0, options.limit);
226
+ const first = candidates[0];
227
+ return {
228
+ rows: capped.map((c) => ({
229
+ chunk_id: c.chunk_id,
230
+ symbol_name: c.symbol_name,
231
+ chunk_type: c.chunk_type,
232
+ match_type: c.match_type,
233
+ })),
234
+ diagnostics: {
235
+ fired: candidates.length > 0,
236
+ top_match_channel: first !== undefined ? first.match_type : null,
237
+ candidate_count: candidates.length,
238
+ primary_eligible_terms: primaryTerms,
239
+ },
240
+ };
241
+ }
242
+ //# sourceMappingURL=declex.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"declex.js","sourceRoot":"","sources":["../../src/search/declex.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,77 @@
1
+ import type { Db } from '../graph/db.js';
2
+ export interface FtsSearchOptions {
3
+ readonly limit: number;
4
+ readonly filePattern?: string | null;
5
+ readonly language?: string | null;
6
+ }
7
+ export interface FtsSearchRow {
8
+ readonly chunk_id: string;
9
+ readonly symbol_name: string | null;
10
+ readonly bm25_score: number;
11
+ readonly match_snippet: string;
12
+ }
13
+ /**
14
+ * BM25 search over `chunk_fts` using SQLite FTS5.
15
+ *
16
+ * The trigram tokeniser surfaces camelCase identifiers and partial matches.
17
+ * `bm25(chunk_fts)` returns negative scores — lower (more negative) is a
18
+ * better match. Results are sorted ascending (best first) before RRF ranking.
19
+ *
20
+ * `filePattern` and `language` are applied as an `IN` list on `file_path`,
21
+ * built before the ORDER BY and LIMIT clauses so the restriction is part of
22
+ * the ranked query rather than a post-filter on its window. Both are resolved
23
+ * through `./scope.ts`, which owns the glob and language semantics for every
24
+ * ranker `fusedSearch` fuses — this function must not translate the pattern
25
+ * itself, or the two rankers can disagree about what the caller asked for.
26
+ * `chunk_type` and `only_exported` are post-filters applied by the caller
27
+ * after fetching full chunk records from the chunk store.
28
+ */
29
+ export declare function searchFts(db: Db, query: string, options: FtsSearchOptions): Promise<FtsSearchRow[]>;
30
+ export interface IdentifierFtsRow {
31
+ readonly chunk_id: string;
32
+ }
33
+ /**
34
+ * Exact-identifier search over `identifier_fts`.
35
+ *
36
+ * Used by `mast_callers` to produce the `potential_matches` set — chunks that
37
+ * contain the symbol name as an identifier token but whose call site the
38
+ * heuristic resolver could not statically link.
39
+ */
40
+ export declare function searchIdentifiers(db: Db, symbolName: string, limit?: number): Promise<IdentifierFtsRow[]>;
41
+ /**
42
+ * Uncapped match count for `symbolName` over `identifier_fts` — same
43
+ * phrase-quoted MATCH expression as {@link searchIdentifiers} (via the shared
44
+ * {@link buildIdentifierMatchExpr} helper, never duplicated), no `LIMIT`.
45
+ *
46
+ * F10 (Stage 3, IMPLEMENTATION_PLAN.md): `searchIdentifiers`' cap silently
47
+ * dropped real matches with no signal the cap was hit
48
+ * (`eval/GITNEXUS_COMPARISON.md` M4 — `isUndefined` reported 50 candidates
49
+ * when the real count was 71). Callers should run this ONLY when the capped
50
+ * fetch came back full (`identRows.length === limit`) — below the cap, the
51
+ * fetch count already IS the real count, and this query would be pure waste.
52
+ */
53
+ export declare function countIdentifierMatches(db: Db, symbolName: string): Promise<number>;
54
+ /**
55
+ * Near-miss identifier search over `identifier_fts`.
56
+ *
57
+ * Unlike {@link searchIdentifiers} (which requires an exact phrase match on the
58
+ * full symbol), this ORs each term so a chunk matching *any* sub-term is
59
+ * returned. Used by the zero-result assist path to gather candidate chunks
60
+ * whose identifiers partially overlap a query that otherwise found nothing.
61
+ *
62
+ * Terms are quoted as phrases so separator chars never leak into FTS5 query
63
+ * syntax. Returns an empty array when no usable term remains.
64
+ */
65
+ export declare function searchIdentifierNearMiss(db: Db, terms: readonly string[], limit?: number): Promise<IdentifierFtsRow[]>;
66
+ /**
67
+ * Split an identifier-shaped query into its constituent sub-terms.
68
+ *
69
+ * Handles camelCase (`getUser` → `get`, `user`), acronym boundaries
70
+ * (`HTTPServer` → `http`, `server`), and snake/kebab separators. Sub-terms are
71
+ * lowercased (FTS5 trigram matching is case-insensitive), de-duplicated, and
72
+ * filtered below the 3-char trigram floor — a sub-term shorter than a trigram
73
+ * cannot match `chunk_fts` anyway. Returns an empty array when nothing usable
74
+ * remains, so callers can short-circuit the assist.
75
+ */
76
+ export declare function splitIdentifierTerms(query: string): string[];
77
+ //# sourceMappingURL=fts.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"fts.d.ts","sourceRoot":"","sources":["../../src/search/fts.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,EAAE,EAAE,MAAM,gBAAgB,CAAC;AAQzC,MAAM,WAAW,gBAAgB;IAC/B,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,WAAW,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACrC,QAAQ,CAAC,QAAQ,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;CACnC;AAED,MAAM,WAAW,YAAY;IAC3B,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,QAAQ,CAAC,WAAW,EAAE,MAAM,GAAG,IAAI,CAAC;IACpC,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAC;IAC5B,QAAQ,CAAC,aAAa,EAAE,MAAM,CAAC;CAChC;AAED;;;;;;;;;;;;;;;GAeG;AACH,wBAAsB,SAAS,CAC7B,EAAE,EAAE,EAAE,EACN,KAAK,EAAE,MAAM,EACb,OAAO,EAAE,gBAAgB,GACxB,OAAO,CAAC,YAAY,EAAE,CAAC,CA+DzB;AAMD,MAAM,WAAW,gBAAgB;IAC/B,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;CAC3B;AAED;;;;;;GAMG;AACH,wBAAsB,iBAAiB,CACrC,EAAE,EAAE,EAAE,EACN,UAAU,EAAE,MAAM,EAClB,KAAK,SAAK,GACT,OAAO,CAAC,gBAAgB,EAAE,CAAC,CAS7B;AAED;;;;;;;;;;;GAWG;AACH,wBAAsB,sBAAsB,CAAC,EAAE,EAAE,EAAE,EAAE,UAAU,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CASxF;AAED;;;;;;;;;;GAUG;AACH,wBAAsB,wBAAwB,CAC5C,EAAE,EAAE,EAAE,EACN,KAAK,EAAE,SAAS,MAAM,EAAE,EACxB,KAAK,SAAK,GACT,OAAO,CAAC,gBAAgB,EAAE,CAAC,CAU7B;AAMD;;;;;;;;;GASG;AACH,wBAAgB,oBAAoB,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,EAAE,CAc5D"}
@@ -0,0 +1,223 @@
1
+ import { sql } from 'kysely';
2
+ import { resolveScopedPaths } from './scope.js';
3
+ import { chunkValuesForSqlite } from '../graph/sqliteBatch.js';
4
+ /**
5
+ * BM25 search over `chunk_fts` using SQLite FTS5.
6
+ *
7
+ * The trigram tokeniser surfaces camelCase identifiers and partial matches.
8
+ * `bm25(chunk_fts)` returns negative scores — lower (more negative) is a
9
+ * better match. Results are sorted ascending (best first) before RRF ranking.
10
+ *
11
+ * `filePattern` and `language` are applied as an `IN` list on `file_path`,
12
+ * built before the ORDER BY and LIMIT clauses so the restriction is part of
13
+ * the ranked query rather than a post-filter on its window. Both are resolved
14
+ * through `./scope.ts`, which owns the glob and language semantics for every
15
+ * ranker `fusedSearch` fuses — this function must not translate the pattern
16
+ * itself, or the two rankers can disagree about what the caller asked for.
17
+ * `chunk_type` and `only_exported` are post-filters applied by the caller
18
+ * after fetching full chunk records from the chunk store.
19
+ */
20
+ export async function searchFts(db, query, options) {
21
+ // Sanitise the user query into a safe FTS5 expression. Passing the raw string
22
+ // to MATCH lets FTS5 interpret `(`, `:`, `"`, `OR`, etc. as query syntax — a
23
+ // query like `handleLogin(req` would throw "fts5: syntax error" and (because
24
+ // this runs outside fusedSearch's try/catch) fail the whole search.
25
+ const matchExpr = toFtsMatch(query);
26
+ if (matchExpr === null)
27
+ return [];
28
+ // SQLite FTS5 UNINDEXED columns support IN with a literal list reliably,
29
+ // but LIKE and subquery IN may not be applied by the FTS5 query planner.
30
+ // The scope is therefore materialised against the regular `files` table
31
+ // first, then passed as an IN list to the FTS query.
32
+ const allowedPaths = await resolveScopedPaths(db, {
33
+ filePattern: options.filePattern,
34
+ language: options.language,
35
+ });
36
+ // A scope that matched no indexed file is an empty result, not an
37
+ // unrestricted one — `null` means "no scope given" and must not be conflated.
38
+ if (allowedPaths !== null && allowedPaths.length === 0)
39
+ return [];
40
+ const rowLimit = options.limit * 2;
41
+ // Build all WHERE conditions before ORDER BY / LIMIT.
42
+ const ranked = (paths) => {
43
+ let q = db
44
+ .selectFrom('chunk_fts')
45
+ .select([
46
+ 'chunk_id',
47
+ 'symbol_name',
48
+ sql `bm25(chunk_fts)`.as('bm25_score'),
49
+ sql `snippet(chunk_fts, 0, '**', '**', '...', 12)`.as('match_snippet'),
50
+ ])
51
+ .where(sql `chunk_fts MATCH ${matchExpr}`);
52
+ if (paths !== null)
53
+ q = q.where('file_path', 'in', [...paths]);
54
+ return q.orderBy(sql `bm25(chunk_fts)`, 'asc').limit(rowLimit).execute();
55
+ };
56
+ if (allowedPaths === null)
57
+ return ranked(null);
58
+ // The scope can name every indexed file — a `language` filter on a large
59
+ // monorepo does exactly that — and an IN list is one bound parameter per
60
+ // path, so past SQLITE_MAX_VARIABLES the statement throws `too many SQL
61
+ // variables` and the search fails outright.
62
+ //
63
+ // Splitting is safe for a ranked, limited query: each batch is a subset of
64
+ // the scope, so the global best `rowLimit` rows are necessarily contained in
65
+ // the union of the per-batch best `rowLimit` rows. Re-sorting that union and
66
+ // re-applying the limit therefore reproduces the single-statement result
67
+ // exactly. bm25 is negative, best-first is ascending — the same convention
68
+ // the SQL above uses.
69
+ // Two parameters of the statement's budget are not path values: the MATCH
70
+ // expression and the LIMIT. Declaring them is not defensive padding — a
71
+ // batch of exactly SQLITE_MAX_VARIABLES paths threw here, and the test at
72
+ // `graph/__tests__/in-list-batching.test.ts` fails again if a future
73
+ // predicate binds a third without being counted.
74
+ const NON_PATH_BOUND_PARAMS = 2;
75
+ const batches = chunkValuesForSqlite(allowedPaths, 1, NON_PATH_BOUND_PARAMS);
76
+ if (batches.length === 1)
77
+ return ranked(allowedPaths);
78
+ const merged = [];
79
+ for (const batch of batches)
80
+ merged.push(...await ranked(batch));
81
+ merged.sort((a, b) => a.bm25_score - b.bm25_score);
82
+ return merged.slice(0, rowLimit);
83
+ }
84
+ /**
85
+ * Exact-identifier search over `identifier_fts`.
86
+ *
87
+ * Used by `mast_callers` to produce the `potential_matches` set — chunks that
88
+ * contain the symbol name as an identifier token but whose call site the
89
+ * heuristic resolver could not statically link.
90
+ */
91
+ export async function searchIdentifiers(db, symbolName, limit = 50) {
92
+ const matchExpr = buildIdentifierMatchExpr(symbolName);
93
+ if (matchExpr === null)
94
+ return [];
95
+ return db
96
+ .selectFrom('identifier_fts')
97
+ .select('chunk_id')
98
+ .where(sql `identifier_fts MATCH ${matchExpr}`)
99
+ .limit(limit)
100
+ .execute();
101
+ }
102
+ /**
103
+ * Uncapped match count for `symbolName` over `identifier_fts` — same
104
+ * phrase-quoted MATCH expression as {@link searchIdentifiers} (via the shared
105
+ * {@link buildIdentifierMatchExpr} helper, never duplicated), no `LIMIT`.
106
+ *
107
+ * F10 (Stage 3, IMPLEMENTATION_PLAN.md): `searchIdentifiers`' cap silently
108
+ * dropped real matches with no signal the cap was hit
109
+ * (`eval/GITNEXUS_COMPARISON.md` M4 — `isUndefined` reported 50 candidates
110
+ * when the real count was 71). Callers should run this ONLY when the capped
111
+ * fetch came back full (`identRows.length === limit`) — below the cap, the
112
+ * fetch count already IS the real count, and this query would be pure waste.
113
+ */
114
+ export async function countIdentifierMatches(db, symbolName) {
115
+ const matchExpr = buildIdentifierMatchExpr(symbolName);
116
+ if (matchExpr === null)
117
+ return 0;
118
+ const row = await db
119
+ .selectFrom('identifier_fts')
120
+ .select((eb) => eb.fn.count('chunk_id').as('count'))
121
+ .where(sql `identifier_fts MATCH ${matchExpr}`)
122
+ .executeTakeFirst();
123
+ return row?.count ?? 0;
124
+ }
125
+ /**
126
+ * Near-miss identifier search over `identifier_fts`.
127
+ *
128
+ * Unlike {@link searchIdentifiers} (which requires an exact phrase match on the
129
+ * full symbol), this ORs each term so a chunk matching *any* sub-term is
130
+ * returned. Used by the zero-result assist path to gather candidate chunks
131
+ * whose identifiers partially overlap a query that otherwise found nothing.
132
+ *
133
+ * Terms are quoted as phrases so separator chars never leak into FTS5 query
134
+ * syntax. Returns an empty array when no usable term remains.
135
+ */
136
+ export async function searchIdentifierNearMiss(db, terms, limit = 20) {
137
+ const cleaned = terms.map((t) => t.trim()).filter((t) => t.length > 0);
138
+ if (cleaned.length === 0)
139
+ return [];
140
+ const matchExpr = cleaned.map((t) => `"${t.replace(/"/g, '""')}"`).join(' OR ');
141
+ return db
142
+ .selectFrom('identifier_fts')
143
+ .select('chunk_id')
144
+ .where(sql `identifier_fts MATCH ${matchExpr}`)
145
+ .limit(limit)
146
+ .execute();
147
+ }
148
+ // ---------------------------------------------------------------------------
149
+ // Helpers
150
+ // ---------------------------------------------------------------------------
151
+ /**
152
+ * Split an identifier-shaped query into its constituent sub-terms.
153
+ *
154
+ * Handles camelCase (`getUser` → `get`, `user`), acronym boundaries
155
+ * (`HTTPServer` → `http`, `server`), and snake/kebab separators. Sub-terms are
156
+ * lowercased (FTS5 trigram matching is case-insensitive), de-duplicated, and
157
+ * filtered below the 3-char trigram floor — a sub-term shorter than a trigram
158
+ * cannot match `chunk_fts` anyway. Returns an empty array when nothing usable
159
+ * remains, so callers can short-circuit the assist.
160
+ */
161
+ export function splitIdentifierTerms(query) {
162
+ const spaced = query
163
+ .replace(/([a-z0-9])([A-Z])/g, '$1 $2') // camelCase: fooBar -> foo Bar
164
+ .replace(/([A-Z]+)([A-Z][a-z])/g, '$1 $2'); // acronym: HTTPServer -> HTTP Server
165
+ const seen = new Set();
166
+ const out = [];
167
+ for (const part of spaced.split(/[^A-Za-z0-9]+/)) {
168
+ const term = part.toLowerCase();
169
+ if (term.length < 3)
170
+ continue;
171
+ if (seen.has(term))
172
+ continue;
173
+ seen.add(term);
174
+ out.push(term);
175
+ }
176
+ return out;
177
+ }
178
+ /**
179
+ * Build the phrase-quoted FTS5 MATCH expression shared by
180
+ * {@link searchIdentifiers} and {@link countIdentifierMatches} — the two must
181
+ * agree on exactly which rows count as a match, or the "real count" F10
182
+ * reports could disagree with what the capped fetch actually returned.
183
+ * Quoting as a phrase means any separator char (e.g. `Class.method`) is
184
+ * matched literally rather than parsed as FTS5 query syntax. Returns null for
185
+ * an empty/whitespace-only name, so callers can short-circuit instead of
186
+ * running an invalid MATCH query.
187
+ */
188
+ function buildIdentifierMatchExpr(symbolName) {
189
+ const term = symbolName.trim();
190
+ if (term === '')
191
+ return null;
192
+ return `"${term.replace(/"/g, '""')}"`;
193
+ }
194
+ /**
195
+ * Turn a free-form query into a safe FTS5 MATCH expression for the trigram
196
+ * `chunk_fts`: identifier-ish tokens (length ≥ 3, the trigram minimum), each
197
+ * quoted as a phrase so no character is treated as query syntax, joined by
198
+ * `OR`. Returns null when no usable token remains, so the caller can
199
+ * short-circuit to an empty result instead of running an invalid query.
200
+ *
201
+ * **F15 — why OR and not AND.** FTS5 ANDs bare space-separated phrases, so the
202
+ * previous space-join required a chunk to contain *every* token. A conceptual
203
+ * multi-word query therefore matched nothing at all: measured against the nest
204
+ * corpus, 6 of 20 TSDoc-derived queries returned zero rows despite the corpus
205
+ * plainly containing the target symbol (e.g. "precondition failed exception
206
+ * defines an http for type errors" → 0 rows ANDed, 5 ORed). That silently
207
+ * crippled the lexical half of hybrid search, and made the vector store look
208
+ * indispensable when it was partly compensating for this bug — see
209
+ * IMPLEMENTATION_PLAN.md § "nest replication".
210
+ *
211
+ * Recall is not traded for precision here: `bm25()` already ranks by term
212
+ * coverage and inverse document frequency, so a chunk matching every token
213
+ * still outranks one matching a single common token. OR widens the candidate
214
+ * pool and lets the ranker do the discriminating — which is what
215
+ * `identifier_fts` (searchIdentifiers, below) has always done.
216
+ */
217
+ function toFtsMatch(query) {
218
+ const tokens = (query.match(/[A-Za-z0-9_]+/g) ?? []).filter((t) => t.length >= 3);
219
+ if (tokens.length === 0)
220
+ return null;
221
+ return tokens.map((t) => `"${t.replace(/"/g, '""')}"`).join(' OR ');
222
+ }
223
+ //# sourceMappingURL=fts.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"fts.js","sourceRoot":"","sources":["../../src/search/fts.ts"],"names":[],"mappings":"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"}