@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,48 @@
1
+ /**
2
+ * better-sqlite3 12.11.1 / SQLite 3.53.2's default `MAX_VARIABLE_NUMBER` — the
3
+ * maximum number of `?` bound parameters a single statement may carry. A
4
+ * multi-row `INSERT ... VALUES (?, ?, ...), (?, ?, ...), ...` binds
5
+ * `columns * rows` parameters; exceeding this throws `SqliteError: too many
6
+ * SQL variables` and (inside a transaction) rolls back everything the
7
+ * transaction had written so far. Already documented at
8
+ * `store/sqliteChunkStore.ts`'s `getChunksByIds` (bounded by the caller, no
9
+ * batching needed there); this constant is the shared source of truth for
10
+ * every site that DOES need batching.
11
+ */
12
+ export declare const SQLITE_MAX_VARIABLES = 32766;
13
+ /**
14
+ * Splits `rows` into batches sized so that `batch.length * columnsPerRow`
15
+ * never exceeds {@link SQLITE_MAX_VARIABLES}, where `columnsPerRow` is the
16
+ * number of keys on `rows[0]`.
17
+ *
18
+ * ASSUMPTION: every row in `rows` has the same key count as `rows[0]`. This
19
+ * holds for every call site in mast — each builds its rows through one
20
+ * fixed-shape mapper (`chunkToRow`, the `symbols.map(...)` object literal in
21
+ * `populate.ts`, etc.), never a mapper that conditionally includes/omits
22
+ * keys — so row 0 is authoritative and this function does not re-check every
23
+ * row (that would defeat the point of a cheap batching helper).
24
+ *
25
+ * Empty input returns `[]`, not `[[]]` — callers that already guard
26
+ * `if (rows.length > 0)` before inserting keep that guard; this only removes
27
+ * the *second* guard (staying under the parameter ceiling).
28
+ */
29
+ export declare function chunkRowsForSqlite<T extends object>(rows: readonly T[]): readonly (readonly T[])[];
30
+ /**
31
+ * Splits `values` into batches sized so that `batch.length * paramsPerValue`
32
+ * never exceeds {@link SQLITE_MAX_VARIABLES}. Sibling to
33
+ * {@link chunkRowsForSqlite} for call sites binding bare scalars (e.g. an
34
+ * `IN (?, ?, ...)` name list) rather than object rows — `paramsPerValue`
35
+ * defaults to 1, the parameter each value contributes to an `IN` list.
36
+ *
37
+ * `reservedParams` is the number of bound parameters the SAME statement binds
38
+ * outside this list — a `MATCH` expression, a `LIMIT`, any other predicate.
39
+ * It defaults to 0 because most call sites bind nothing else, but a batch
40
+ * sized to exactly the ceiling overflows the moment the statement binds one
41
+ * more: measured, `searchFts` (whose statement also binds its MATCH
42
+ * expression and its LIMIT) threw `too many SQL variables` at a batch of
43
+ * exactly {@link SQLITE_MAX_VARIABLES} paths until it declared its two.
44
+ *
45
+ * Empty input returns `[]`, matching {@link chunkRowsForSqlite}.
46
+ */
47
+ export declare function chunkValuesForSqlite<T>(values: readonly T[], paramsPerValue?: number, reservedParams?: number): readonly (readonly T[])[];
48
+ //# sourceMappingURL=sqliteBatch.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"sqliteBatch.d.ts","sourceRoot":"","sources":["../../src/graph/sqliteBatch.ts"],"names":[],"mappings":"AAaA;;;;;;;;;;GAUG;AACH,eAAO,MAAM,oBAAoB,QAAS,CAAC;AAE3C;;;;;;;;;;;;;;;GAeG;AACH,wBAAgB,kBAAkB,CAAC,CAAC,SAAS,MAAM,EACjD,IAAI,EAAE,SAAS,CAAC,EAAE,GACjB,SAAS,CAAC,SAAS,CAAC,EAAE,CAAC,EAAE,CAe3B;AAED;;;;;;;;;;;;;;;;GAgBG;AACH,wBAAgB,oBAAoB,CAAC,CAAC,EACpC,MAAM,EAAE,SAAS,CAAC,EAAE,EACpB,cAAc,SAAI,EAClB,cAAc,SAAI,GACjB,SAAS,CAAC,SAAS,CAAC,EAAE,CAAC,EAAE,CAO3B"}
@@ -0,0 +1,93 @@
1
+ // ---------------------------------------------------------------------------
2
+ // SQLite bound-parameter batching (Stage 4.5 S1, IMPLEMENTATION_PLAN.md
3
+ // "batch `replaceChunksForFile`'s insert", added 2026-08-07).
4
+ //
5
+ // Placed in graph/ (alongside db.ts) rather than store/: `store/sqliteChunkStore.ts`
6
+ // already imports `Db` from `../graph/db.js`, so store -> graph is the existing
7
+ // dependency direction. Putting the shared batching helper here lets both
8
+ // `store/sqliteChunkStore.ts` and `graph/populate.ts` import it without
9
+ // introducing a new edge (graph -> store would be the alternative, and mast has
10
+ // no such edge today). mast is one flat align component (CLAUDE.md), so this is
11
+ // a placement choice, not a conformance requirement — stated here for the record.
12
+ // ---------------------------------------------------------------------------
13
+ /**
14
+ * better-sqlite3 12.11.1 / SQLite 3.53.2's default `MAX_VARIABLE_NUMBER` — the
15
+ * maximum number of `?` bound parameters a single statement may carry. A
16
+ * multi-row `INSERT ... VALUES (?, ?, ...), (?, ?, ...), ...` binds
17
+ * `columns * rows` parameters; exceeding this throws `SqliteError: too many
18
+ * SQL variables` and (inside a transaction) rolls back everything the
19
+ * transaction had written so far. Already documented at
20
+ * `store/sqliteChunkStore.ts`'s `getChunksByIds` (bounded by the caller, no
21
+ * batching needed there); this constant is the shared source of truth for
22
+ * every site that DOES need batching.
23
+ */
24
+ export const SQLITE_MAX_VARIABLES = 32_766;
25
+ /**
26
+ * Splits `rows` into batches sized so that `batch.length * columnsPerRow`
27
+ * never exceeds {@link SQLITE_MAX_VARIABLES}, where `columnsPerRow` is the
28
+ * number of keys on `rows[0]`.
29
+ *
30
+ * ASSUMPTION: every row in `rows` has the same key count as `rows[0]`. This
31
+ * holds for every call site in mast — each builds its rows through one
32
+ * fixed-shape mapper (`chunkToRow`, the `symbols.map(...)` object literal in
33
+ * `populate.ts`, etc.), never a mapper that conditionally includes/omits
34
+ * keys — so row 0 is authoritative and this function does not re-check every
35
+ * row (that would defeat the point of a cheap batching helper).
36
+ *
37
+ * Empty input returns `[]`, not `[[]]` — callers that already guard
38
+ * `if (rows.length > 0)` before inserting keep that guard; this only removes
39
+ * the *second* guard (staying under the parameter ceiling).
40
+ */
41
+ export function chunkRowsForSqlite(rows) {
42
+ if (rows.length === 0)
43
+ return [];
44
+ const first = rows[0];
45
+ // Unreachable given the length check above; `noUncheckedIndexedAccess`
46
+ // still types `rows[0]` as `T | undefined`, so this is a real (if
47
+ // defensive) branch rather than a `!` assertion (CLAUDE.md §3.1).
48
+ if (first === undefined)
49
+ return [];
50
+ const columnsPerRow = Object.keys(first).length;
51
+ // A 0-column row shape binds no parameters at all, so the whole array fits
52
+ // in one batch; guard division-by-zero rather than let it fall out of the
53
+ // Math.floor below.
54
+ const rowsPerBatch = columnsPerRow > 0
55
+ ? Math.max(1, Math.floor(SQLITE_MAX_VARIABLES / columnsPerRow))
56
+ : rows.length;
57
+ return chunk(rows, rowsPerBatch);
58
+ }
59
+ /**
60
+ * Splits `values` into batches sized so that `batch.length * paramsPerValue`
61
+ * never exceeds {@link SQLITE_MAX_VARIABLES}. Sibling to
62
+ * {@link chunkRowsForSqlite} for call sites binding bare scalars (e.g. an
63
+ * `IN (?, ?, ...)` name list) rather than object rows — `paramsPerValue`
64
+ * defaults to 1, the parameter each value contributes to an `IN` list.
65
+ *
66
+ * `reservedParams` is the number of bound parameters the SAME statement binds
67
+ * outside this list — a `MATCH` expression, a `LIMIT`, any other predicate.
68
+ * It defaults to 0 because most call sites bind nothing else, but a batch
69
+ * sized to exactly the ceiling overflows the moment the statement binds one
70
+ * more: measured, `searchFts` (whose statement also binds its MATCH
71
+ * expression and its LIMIT) threw `too many SQL variables` at a batch of
72
+ * exactly {@link SQLITE_MAX_VARIABLES} paths until it declared its two.
73
+ *
74
+ * Empty input returns `[]`, matching {@link chunkRowsForSqlite}.
75
+ */
76
+ export function chunkValuesForSqlite(values, paramsPerValue = 1, reservedParams = 0) {
77
+ if (values.length === 0)
78
+ return [];
79
+ const budget = SQLITE_MAX_VARIABLES - reservedParams;
80
+ const valuesPerBatch = paramsPerValue > 0
81
+ ? Math.max(1, Math.floor(budget / paramsPerValue))
82
+ : values.length;
83
+ return chunk(values, valuesPerBatch);
84
+ }
85
+ /** Plain array-slicing chunker shared by both batching functions above. */
86
+ function chunk(items, size) {
87
+ const batches = [];
88
+ for (let i = 0; i < items.length; i += size) {
89
+ batches.push(items.slice(i, i + size));
90
+ }
91
+ return batches;
92
+ }
93
+ //# sourceMappingURL=sqliteBatch.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"sqliteBatch.js","sourceRoot":"","sources":["../../src/graph/sqliteBatch.ts"],"names":[],"mappings":"AAAA,8EAA8E;AAC9E,wEAAwE;AACxE,8DAA8D;AAC9D,EAAE;AACF,qFAAqF;AACrF,gFAAgF;AAChF,0EAA0E;AAC1E,wEAAwE;AACxE,gFAAgF;AAChF,gFAAgF;AAChF,kFAAkF;AAClF,8EAA8E;AAE9E;;;;;;;;;;GAUG;AACH,MAAM,CAAC,MAAM,oBAAoB,GAAG,MAAM,CAAC;AAE3C;;;;;;;;;;;;;;;GAeG;AACH,MAAM,UAAU,kBAAkB,CAChC,IAAkB;IAElB,IAAI,IAAI,CAAC,MAAM,KAAK,CAAC;QAAE,OAAO,EAAE,CAAC;IACjC,MAAM,KAAK,GAAG,IAAI,CAAC,CAAC,CAAC,CAAC;IACtB,uEAAuE;IACvE,kEAAkE;IAClE,kEAAkE;IAClE,IAAI,KAAK,KAAK,SAAS;QAAE,OAAO,EAAE,CAAC;IACnC,MAAM,aAAa,GAAG,MAAM,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,MAAM,CAAC;IAChD,2EAA2E;IAC3E,0EAA0E;IAC1E,oBAAoB;IACpB,MAAM,YAAY,GAAG,aAAa,GAAG,CAAC;QACpC,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,IAAI,CAAC,KAAK,CAAC,oBAAoB,GAAG,aAAa,CAAC,CAAC;QAC/D,CAAC,CAAC,IAAI,CAAC,MAAM,CAAC;IAChB,OAAO,KAAK,CAAC,IAAI,EAAE,YAAY,CAAC,CAAC;AACnC,CAAC;AAED;;;;;;;;;;;;;;;;GAgBG;AACH,MAAM,UAAU,oBAAoB,CAClC,MAAoB,EACpB,cAAc,GAAG,CAAC,EAClB,cAAc,GAAG,CAAC;IAElB,IAAI,MAAM,CAAC,MAAM,KAAK,CAAC;QAAE,OAAO,EAAE,CAAC;IACnC,MAAM,MAAM,GAAG,oBAAoB,GAAG,cAAc,CAAC;IACrD,MAAM,cAAc,GAAG,cAAc,GAAG,CAAC;QACvC,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,IAAI,CAAC,KAAK,CAAC,MAAM,GAAG,cAAc,CAAC,CAAC;QAClD,CAAC,CAAC,MAAM,CAAC,MAAM,CAAC;IAClB,OAAO,KAAK,CAAC,MAAM,EAAE,cAAc,CAAC,CAAC;AACvC,CAAC;AAED,2EAA2E;AAC3E,SAAS,KAAK,CAAI,KAAmB,EAAE,IAAY;IACjD,MAAM,OAAO,GAAU,EAAE,CAAC;IAC1B,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,KAAK,CAAC,MAAM,EAAE,CAAC,IAAI,IAAI,EAAE,CAAC;QAC5C,OAAO,CAAC,IAAI,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC,GAAG,IAAI,CAAC,CAAC,CAAC;IACzC,CAAC;IACD,OAAO,OAAO,CAAC;AACjB,CAAC"}
@@ -0,0 +1,48 @@
1
+ /**
2
+ * How far the index has drifted from the working tree — one producer, read by
3
+ * both `mast status` (CLI) and `mast_status` (MCP).
4
+ *
5
+ * The two surfaces used to compute this separately and disagree. The CLI
6
+ * diffed `file_manifest.json` against a fresh walk; the MCP tool enumerated the
7
+ * `files` table and stat'd each row. Neither is sufficient alone, which is why
8
+ * this reads **both** signals and takes the union:
9
+ *
10
+ * - The **manifest** knows which files the last run considered. It is the only
11
+ * signal that can see a file which exists on disk and was never indexed —
12
+ * such a file is in no `files` row, so a table-driven check is structurally
13
+ * blind to it. That blindness was the MCP tool's defect
14
+ * (`docs/defects/LEDGER.md` D035): add one file to a project and it reported
15
+ * `index_fresh: true`.
16
+ * - The **`files.mtime` stamp** knows what was actually written, and records
17
+ * the mtime of the content that was parsed (invariant 1 in `runIndex`'s
18
+ * WHY-comment — stamped BEFORE the extract, never re-stat'd at write time).
19
+ * The manifest, by contrast, is stamped from a re-stat during the finalise
20
+ * phase, so an edit landing mid-run leaves the manifest looking current
21
+ * while the row correctly reads stale. It is also the only signal that sees
22
+ * a file which is in the manifest but absent from the index — the residue
23
+ * D034 left on indexes written before that fix, which would otherwise need a
24
+ * full reindex to detect.
25
+ *
26
+ * A file counts once, under the first heading that applies.
27
+ */
28
+ import type { Db } from '../graph/db.js';
29
+ import type { ResolvedConfig } from '../store/config.js';
30
+ export interface IndexFreshness {
31
+ /** On disk and known to the index, but its content has changed since. */
32
+ readonly stale: number;
33
+ /** On disk and not indexed — never seen, or lost to a failed run. */
34
+ readonly unindexed: number;
35
+ /** Known to the index, no longer on disk. */
36
+ readonly deleted: number;
37
+ /** `stale + unindexed + deleted` — what both surfaces report as `stale_files`. */
38
+ readonly total: number;
39
+ }
40
+ /**
41
+ * Measure the index against the working tree.
42
+ *
43
+ * Costs one project walk plus one `files` scan per call, which is why it is
44
+ * called from the status surfaces and not from the read tools. The walk stats
45
+ * every file anyway, so the mtime comparisons below are free.
46
+ */
47
+ export declare function measureFreshness(config: ResolvedConfig, db: Db): Promise<IndexFreshness>;
48
+ //# sourceMappingURL=freshness.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"freshness.d.ts","sourceRoot":"","sources":["../../src/indexer/freshness.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AAIH,OAAO,KAAK,EAAE,EAAE,EAAE,MAAM,gBAAgB,CAAC;AACzC,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,oBAAoB,CAAC;AAGzD,MAAM,WAAW,cAAc;IAC7B,yEAAyE;IACzE,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,qEAAqE;IACrE,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B,6CAA6C;IAC7C,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,kFAAkF;IAClF,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;CACxB;AAED;;;;;;GAMG;AACH,wBAAsB,gBAAgB,CAAC,MAAM,EAAE,cAAc,EAAE,EAAE,EAAE,EAAE,GAAG,OAAO,CAAC,cAAc,CAAC,CAiC9F"}
@@ -0,0 +1,72 @@
1
+ /**
2
+ * How far the index has drifted from the working tree — one producer, read by
3
+ * both `mast status` (CLI) and `mast_status` (MCP).
4
+ *
5
+ * The two surfaces used to compute this separately and disagree. The CLI
6
+ * diffed `file_manifest.json` against a fresh walk; the MCP tool enumerated the
7
+ * `files` table and stat'd each row. Neither is sufficient alone, which is why
8
+ * this reads **both** signals and takes the union:
9
+ *
10
+ * - The **manifest** knows which files the last run considered. It is the only
11
+ * signal that can see a file which exists on disk and was never indexed —
12
+ * such a file is in no `files` row, so a table-driven check is structurally
13
+ * blind to it. That blindness was the MCP tool's defect
14
+ * (`docs/defects/LEDGER.md` D035): add one file to a project and it reported
15
+ * `index_fresh: true`.
16
+ * - The **`files.mtime` stamp** knows what was actually written, and records
17
+ * the mtime of the content that was parsed (invariant 1 in `runIndex`'s
18
+ * WHY-comment — stamped BEFORE the extract, never re-stat'd at write time).
19
+ * The manifest, by contrast, is stamped from a re-stat during the finalise
20
+ * phase, so an edit landing mid-run leaves the manifest looking current
21
+ * while the row correctly reads stale. It is also the only signal that sees
22
+ * a file which is in the manifest but absent from the index — the residue
23
+ * D034 left on indexes written before that fix, which would otherwise need a
24
+ * full reindex to detect.
25
+ *
26
+ * A file counts once, under the first heading that applies.
27
+ */
28
+ import { existsSync, readFileSync } from 'node:fs';
29
+ import { join } from 'node:path';
30
+ import { walkProject } from './walker.js';
31
+ /**
32
+ * Measure the index against the working tree.
33
+ *
34
+ * Costs one project walk plus one `files` scan per call, which is why it is
35
+ * called from the status surfaces and not from the read tools. The walk stats
36
+ * every file anyway, so the mtime comparisons below are free.
37
+ */
38
+ export async function measureFreshness(config, db) {
39
+ const manifestPath = join(config.resolved_state_dir, 'file_manifest.json');
40
+ const manifest = existsSync(manifestPath)
41
+ ? JSON.parse(readFileSync(manifestPath, 'utf-8'))
42
+ : {};
43
+ const currentFiles = await walkProject(config);
44
+ const onDisk = new Map(currentFiles.map((e) => [e.relativePath, e.mtime]));
45
+ const rows = await db.selectFrom('files').select(['path', 'mtime']).execute();
46
+ const indexed = new Map(rows.map((r) => [r.path, r.mtime]));
47
+ let stale = 0;
48
+ let unindexed = 0;
49
+ for (const [path, diskMtime] of onDisk) {
50
+ const manifestMtime = manifest[path];
51
+ const storedMtime = indexed.get(path);
52
+ // Absent from either record means it is not in the index: never walked
53
+ // before, or walked and then lost when its parse or write failed.
54
+ if (manifestMtime === undefined || storedMtime === undefined) {
55
+ unindexed++;
56
+ continue;
57
+ }
58
+ if (diskMtime > manifestMtime || diskMtime > storedMtime)
59
+ stale++;
60
+ }
61
+ // A path recorded by either side but no longer on disk. Deduplicated: the
62
+ // usual case is that both sides still carry it.
63
+ const gone = new Set();
64
+ for (const path of Object.keys(manifest))
65
+ if (!onDisk.has(path))
66
+ gone.add(path);
67
+ for (const path of indexed.keys())
68
+ if (!onDisk.has(path))
69
+ gone.add(path);
70
+ return { stale, unindexed, deleted: gone.size, total: stale + unindexed + gone.size };
71
+ }
72
+ //# sourceMappingURL=freshness.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"freshness.js","sourceRoot":"","sources":["../../src/indexer/freshness.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AAEH,OAAO,EAAE,UAAU,EAAE,YAAY,EAAE,MAAM,SAAS,CAAC;AACnD,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AAGjC,OAAO,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC;AAa1C;;;;;;GAMG;AACH,MAAM,CAAC,KAAK,UAAU,gBAAgB,CAAC,MAAsB,EAAE,EAAM;IACnE,MAAM,YAAY,GAAG,IAAI,CAAC,MAAM,CAAC,kBAAkB,EAAE,oBAAoB,CAAC,CAAC;IAC3E,MAAM,QAAQ,GAA2B,UAAU,CAAC,YAAY,CAAC;QAC/D,CAAC,CAAE,IAAI,CAAC,KAAK,CAAC,YAAY,CAAC,YAAY,EAAE,OAAO,CAAC,CAA4B;QAC7E,CAAC,CAAC,EAAE,CAAC;IAEP,MAAM,YAAY,GAAG,MAAM,WAAW,CAAC,MAAM,CAAC,CAAC;IAC/C,MAAM,MAAM,GAAG,IAAI,GAAG,CAAC,YAAY,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,CAAC,YAAY,EAAE,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC;IAE3E,MAAM,IAAI,GAAG,MAAM,EAAE,CAAC,UAAU,CAAC,OAAO,CAAC,CAAC,MAAM,CAAC,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,CAAC,OAAO,EAAE,CAAC;IAC9E,MAAM,OAAO,GAAG,IAAI,GAAG,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,CAAC,IAAI,EAAE,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC;IAE5D,IAAI,KAAK,GAAG,CAAC,CAAC;IACd,IAAI,SAAS,GAAG,CAAC,CAAC;IAClB,KAAK,MAAM,CAAC,IAAI,EAAE,SAAS,CAAC,IAAI,MAAM,EAAE,CAAC;QACvC,MAAM,aAAa,GAAG,QAAQ,CAAC,IAAI,CAAC,CAAC;QACrC,MAAM,WAAW,GAAG,OAAO,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC;QACtC,uEAAuE;QACvE,kEAAkE;QAClE,IAAI,aAAa,KAAK,SAAS,IAAI,WAAW,KAAK,SAAS,EAAE,CAAC;YAC7D,SAAS,EAAE,CAAC;YACZ,SAAS;QACX,CAAC;QACD,IAAI,SAAS,GAAG,aAAa,IAAI,SAAS,GAAG,WAAW;YAAE,KAAK,EAAE,CAAC;IACpE,CAAC;IAED,0EAA0E;IAC1E,gDAAgD;IAChD,MAAM,IAAI,GAAG,IAAI,GAAG,EAAU,CAAC;IAC/B,KAAK,MAAM,IAAI,IAAI,MAAM,CAAC,IAAI,CAAC,QAAQ,CAAC;QAAE,IAAI,CAAC,MAAM,CAAC,GAAG,CAAC,IAAI,CAAC;YAAE,IAAI,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC;IAChF,KAAK,MAAM,IAAI,IAAI,OAAO,CAAC,IAAI,EAAE;QAAE,IAAI,CAAC,MAAM,CAAC,GAAG,CAAC,IAAI,CAAC;YAAE,IAAI,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC;IAEzE,OAAO,EAAE,KAAK,EAAE,SAAS,EAAE,OAAO,EAAE,IAAI,CAAC,IAAI,EAAE,KAAK,EAAE,KAAK,GAAG,SAAS,GAAG,IAAI,CAAC,IAAI,EAAE,CAAC;AACxF,CAAC"}
@@ -0,0 +1,50 @@
1
+ /**
2
+ * A specifier that resolved only because the filesystem ignored its casing.
3
+ *
4
+ * The import is broken on a case-sensitive filesystem — a Linux CI box will
5
+ * fail to compile it — so this is a defect in the indexed repository, not in
6
+ * MAST. MAST resolves it to the on-disk path anyway (see `safeRealpath`) and
7
+ * reports it rather than dropping the edge.
8
+ */
9
+ export interface MiscasedImport {
10
+ /** Project-relative path of the importing file. */
11
+ readonly fromFile: string;
12
+ /** The module specifier exactly as written. */
13
+ readonly specifier: string;
14
+ /** The target as it is actually spelled on disk. */
15
+ readonly onDiskPath: string;
16
+ }
17
+ export interface MiscasedImportReport {
18
+ /** Total observations. Not capped. */
19
+ readonly count: number;
20
+ /** The first `MISCASED_SAMPLE_LIMIT` observations, in first-seen order. */
21
+ readonly samples: readonly MiscasedImport[];
22
+ }
23
+ /**
24
+ * Samples are capped because a resolver outlives a single index run in the MCP
25
+ * server (it is cached per project root for the process lifetime), and a repo
26
+ * that mis-cases one import usually mis-cases many. The count stays exact; only
27
+ * the retained detail is bounded.
28
+ */
29
+ export declare const MISCASED_SAMPLE_LIMIT = 20;
30
+ export interface ResolvedImport {
31
+ /** Project-relative path (with extension) of the resolved file, or null. */
32
+ readonly resolvedPath: string | null;
33
+ /** True for node_modules / built-in modules (no intra-repo target). */
34
+ readonly isExternal: boolean;
35
+ }
36
+ export interface ImportResolver {
37
+ resolve(moduleSpecifier: string, fromFileRel: string): ResolvedImport;
38
+ /**
39
+ * Mis-cased specifiers observed since the last drain, clearing them.
40
+ *
41
+ * Draining rather than reading keeps the accumulator bounded across the many
42
+ * index runs one cached resolver serves, and makes each run's report describe
43
+ * that run alone.
44
+ */
45
+ drainMiscased(): MiscasedImportReport;
46
+ }
47
+ export declare function getImportResolver(projectRoot: string): ImportResolver;
48
+ /** Test seam: forget cached resolvers (inputs may change between tests). */
49
+ export declare function clearImportResolverCache(): void;
50
+ //# sourceMappingURL=import-resolver.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"import-resolver.d.ts","sourceRoot":"","sources":["../../src/indexer/import-resolver.ts"],"names":[],"mappings":"AAiCA;;;;;;;GAOG;AACH,MAAM,WAAW,cAAc;IAC7B,mDAAmD;IACnD,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,+CAA+C;IAC/C,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B,oDAAoD;IACpD,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAC;CAC7B;AAED,MAAM,WAAW,oBAAoB;IACnC,sCAAsC;IACtC,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,2EAA2E;IAC3E,QAAQ,CAAC,OAAO,EAAE,SAAS,cAAc,EAAE,CAAC;CAC7C;AAED;;;;;GAKG;AACH,eAAO,MAAM,qBAAqB,KAAK,CAAC;AAExC,MAAM,WAAW,cAAc;IAC7B,4EAA4E;IAC5E,QAAQ,CAAC,YAAY,EAAE,MAAM,GAAG,IAAI,CAAC;IACrC,uEAAuE;IACvE,QAAQ,CAAC,UAAU,EAAE,OAAO,CAAC;CAC9B;AAED,MAAM,WAAW,cAAc;IAC7B,OAAO,CAAC,eAAe,EAAE,MAAM,EAAE,WAAW,EAAE,MAAM,GAAG,cAAc,CAAC;IACtE;;;;;;OAMG;IACH,aAAa,IAAI,oBAAoB,CAAC;CACvC;AAMD,wBAAgB,iBAAiB,CAAC,WAAW,EAAE,MAAM,GAAG,cAAc,CAQrE;AAED,4EAA4E;AAC5E,wBAAgB,wBAAwB,IAAI,IAAI,CAE/C"}
@@ -0,0 +1,318 @@
1
+ import { existsSync, statSync, realpathSync, readFileSync } from 'node:fs';
2
+ import { dirname, join, resolve, relative } from 'node:path';
3
+ import fg from 'fast-glob';
4
+ import { loadConfig, createMatchPath } from 'tsconfig-paths';
5
+ // Module/import path resolution (§13.7).
6
+ //
7
+ // Turns an import specifier into a project-relative path that matches an
8
+ // indexed `files.path` (extension included), or marks it external. Handles:
9
+ // 1. relative imports (`./x`, `../y`) — probed for the real file on disk;
10
+ // 2. tsconfig `paths` aliases (`@api/types`) — via tsconfig-paths;
11
+ // 3. pnpm workspace package names (`@kluster/shared`) — via the workspace map;
12
+ // 4. everything else — external (node_modules / built-ins).
13
+ // `realpathSync.native` collapses pnpm symlinks so resolved paths point at real
14
+ // files, and reports the on-disk casing (see safeRealpath).
15
+ const CANDIDATE_EXTS = ['.ts', '.tsx', '.js', '.jsx'];
16
+ // NodeNext/ESM TypeScript writes the *output* extension in relative specifiers
17
+ // (`import { x } from './x.js'`) while the on-disk source is `./x.ts`. tsc resolves
18
+ // such a specifier against the TypeScript source first, and only falls back to the
19
+ // literal file — e.g. `./mod.js` looks up `mod.ts`, then `mod.tsx`, then `mod.js`.
20
+ // We mirror that source-first precedence here (declaration files are out of scope
21
+ // since MAST indexes implementation files, not `.d.ts`). See the TypeScript Modules
22
+ // Reference, "File extension substitution":
23
+ // https://www.typescriptlang.org/docs/handbook/modules/reference.html
24
+ const JS_TO_TS_EXTS = [
25
+ ['.js', ['.ts', '.tsx']],
26
+ ['.jsx', ['.tsx']],
27
+ ['.mjs', ['.mts']],
28
+ ['.cjs', ['.cts']],
29
+ ];
30
+ /**
31
+ * Samples are capped because a resolver outlives a single index run in the MCP
32
+ * server (it is cached per project root for the process lifetime), and a repo
33
+ * that mis-cases one import usually mis-cases many. The count stays exact; only
34
+ * the retained detail is bounded.
35
+ */
36
+ export const MISCASED_SAMPLE_LIMIT = 20;
37
+ // Built once per project root — reading tsconfig and globbing the workspace is
38
+ // not free, and the inputs are stable for the life of an index run / session.
39
+ const cache = new Map();
40
+ export function getImportResolver(projectRoot) {
41
+ const key = resolve(projectRoot);
42
+ let resolver = cache.get(key);
43
+ if (resolver === undefined) {
44
+ resolver = buildResolver(key);
45
+ cache.set(key, resolver);
46
+ }
47
+ return resolver;
48
+ }
49
+ /** Test seam: forget cached resolvers (inputs may change between tests). */
50
+ export function clearImportResolverCache() {
51
+ cache.clear();
52
+ }
53
+ function buildResolver(projectRoot) {
54
+ const matchPath = buildTsconfigMatcher(projectRoot);
55
+ const workspace = buildWorkspaceMap(projectRoot);
56
+ // Relativise against the realpath of the root as well, so a symlinked root
57
+ // (e.g. macOS /tmp → /private/tmp) cancels out and the result matches the
58
+ // walker's `files.path`. realpathSync also collapses pnpm package symlinks.
59
+ const realRoot = safeRealpath(projectRoot);
60
+ let miscasedCount = 0;
61
+ const miscasedSamples = [];
62
+ const norm = (p) => p.split('\\').join('/');
63
+ /**
64
+ * Project-relative path of `abs` as it is spelled on disk, noting the case
65
+ * discrepancy when the specifier's own spelling differed by case alone.
66
+ *
67
+ * The comparison uses the raw `abs` rather than a second (case-preserving)
68
+ * realpath call, so detection costs no extra syscall on the resolution path.
69
+ * The price is that a mis-casing reached THROUGH a symlinked directory
70
+ * differs by more than case and so goes unreported — it still resolves to the
71
+ * right file, it just is not named in the report.
72
+ */
73
+ const toRel = (abs, ctx) => {
74
+ const onDisk = norm(relative(realRoot, safeRealpath(abs)));
75
+ // Relative to `projectRoot`, not `realRoot`: `abs` was built from
76
+ // `projectRoot`, and on a symlinked root (macOS /tmp -> /private/tmp) the
77
+ // two roots differ, which would make every path look like a mismatch.
78
+ const asWritten = norm(relative(projectRoot, abs));
79
+ if (asWritten !== onDisk && asWritten.toLowerCase() === onDisk.toLowerCase()) {
80
+ miscasedCount++;
81
+ if (miscasedSamples.length < MISCASED_SAMPLE_LIMIT) {
82
+ miscasedSamples.push({
83
+ fromFile: ctx.fromFileRel,
84
+ specifier: ctx.specifier,
85
+ onDiskPath: onDisk,
86
+ });
87
+ }
88
+ }
89
+ return onDisk;
90
+ };
91
+ /** Resolve a base path (possibly without extension) to a real indexed file. */
92
+ const probe = (base, ctx) => {
93
+ // NodeNext source-first precedence: when the specifier carries a compiled
94
+ // JS extension (`./x.js`), prefer the TypeScript source (`x.ts`) that would
95
+ // emit it, ahead of any literal `x.js` on disk (see JS_TO_TS_EXTS).
96
+ for (const [jsExt, tsExts] of JS_TO_TS_EXTS) {
97
+ if (base.endsWith(jsExt)) {
98
+ const stem = base.slice(0, -jsExt.length);
99
+ for (const tsExt of tsExts) {
100
+ if (isFile(stem + tsExt))
101
+ return toRel(stem + tsExt, ctx);
102
+ }
103
+ break; // a base ends in at most one of these extensions
104
+ }
105
+ }
106
+ if (isFile(base))
107
+ return toRel(base, ctx);
108
+ for (const ext of CANDIDATE_EXTS) {
109
+ if (isFile(base + ext))
110
+ return toRel(base + ext, ctx);
111
+ }
112
+ for (const ext of CANDIDATE_EXTS) {
113
+ const idx = join(base, `index${ext}`);
114
+ if (isFile(idx))
115
+ return toRel(idx, ctx);
116
+ }
117
+ return null;
118
+ };
119
+ return {
120
+ resolve(spec, fromFileRel) {
121
+ const ctx = { specifier: spec, fromFileRel };
122
+ // 1. Relative / absolute imports.
123
+ if (spec.startsWith('.')) {
124
+ const fromDir = dirname(join(projectRoot, fromFileRel));
125
+ return { resolvedPath: probe(resolve(fromDir, spec), ctx), isExternal: false };
126
+ }
127
+ if (spec.startsWith('/')) {
128
+ return { resolvedPath: probe(spec, ctx), isExternal: false };
129
+ }
130
+ // 2. tsconfig path alias.
131
+ if (matchPath !== null) {
132
+ const aliasBase = matchPath(spec, undefined, undefined, [...CANDIDATE_EXTS]);
133
+ if (aliasBase !== undefined) {
134
+ const rel = probe(aliasBase, ctx);
135
+ if (rel !== null)
136
+ return { resolvedPath: rel, isExternal: false };
137
+ }
138
+ }
139
+ // 3. pnpm workspace package.
140
+ const ws = resolveWorkspace(spec, workspace, (base) => probe(base, ctx));
141
+ if (ws !== null)
142
+ return { resolvedPath: ws, isExternal: false };
143
+ // 4. External.
144
+ return { resolvedPath: null, isExternal: true };
145
+ },
146
+ drainMiscased() {
147
+ const report = {
148
+ count: miscasedCount,
149
+ samples: [...miscasedSamples],
150
+ };
151
+ miscasedCount = 0;
152
+ miscasedSamples.length = 0;
153
+ return report;
154
+ },
155
+ };
156
+ }
157
+ /** Resolve `@scope/pkg` or `@scope/pkg/sub` against the workspace package map. */
158
+ function resolveWorkspace(spec, workspace, probe) {
159
+ for (const [name, dir] of workspace) {
160
+ if (spec === name) {
161
+ // Bare package import → its entry point.
162
+ const main = packageMain(dir);
163
+ const candidates = [
164
+ main !== null ? join(dir, main) : null,
165
+ join(dir, 'src', 'index'),
166
+ join(dir, 'index'),
167
+ ].filter((c) => c !== null);
168
+ for (const c of candidates) {
169
+ const rel = probe(c);
170
+ if (rel !== null)
171
+ return rel;
172
+ }
173
+ return null;
174
+ }
175
+ if (spec.startsWith(`${name}/`)) {
176
+ const sub = spec.slice(name.length + 1);
177
+ return probe(join(dir, sub)) ?? probe(join(dir, 'src', sub));
178
+ }
179
+ }
180
+ return null;
181
+ }
182
+ // ---------------------------------------------------------------------------
183
+ // tsconfig paths
184
+ // ---------------------------------------------------------------------------
185
+ function buildTsconfigMatcher(projectRoot) {
186
+ const config = loadConfig(projectRoot);
187
+ if (config.resultType !== 'success')
188
+ return null;
189
+ if (Object.keys(config.paths).length === 0)
190
+ return null;
191
+ return createMatchPath(config.absoluteBaseUrl, config.paths);
192
+ }
193
+ // ---------------------------------------------------------------------------
194
+ // pnpm workspace map (package name → absolute package dir)
195
+ // ---------------------------------------------------------------------------
196
+ function buildWorkspaceMap(projectRoot) {
197
+ const map = new Map();
198
+ const workspaceRoot = findWorkspaceRoot(projectRoot);
199
+ if (workspaceRoot === null)
200
+ return map;
201
+ const globs = readWorkspaceGlobs(workspaceRoot);
202
+ if (globs.length === 0)
203
+ return map;
204
+ const dirs = fg.sync(globs, {
205
+ cwd: workspaceRoot,
206
+ onlyDirectories: true,
207
+ absolute: true,
208
+ followSymbolicLinks: false,
209
+ });
210
+ for (const dir of dirs) {
211
+ const name = packageName(dir);
212
+ if (name !== null && !map.has(name))
213
+ map.set(name, dir);
214
+ }
215
+ return map;
216
+ }
217
+ /** Walk up from `start` to the nearest dir containing pnpm-workspace.yaml. */
218
+ function findWorkspaceRoot(start) {
219
+ let dir = resolve(start);
220
+ for (;;) {
221
+ if (existsSync(join(dir, 'pnpm-workspace.yaml')))
222
+ return dir;
223
+ const parent = dirname(dir);
224
+ if (parent === dir)
225
+ return null;
226
+ dir = parent;
227
+ }
228
+ }
229
+ /**
230
+ * Parse the `packages:` glob list from pnpm-workspace.yaml. The file is a flat
231
+ * list of quoted/unquoted globs under a single `packages:` key, so a line scan
232
+ * is sufficient and avoids a YAML dependency.
233
+ */
234
+ function readWorkspaceGlobs(workspaceRoot) {
235
+ let text;
236
+ try {
237
+ text = readFileSync(join(workspaceRoot, 'pnpm-workspace.yaml'), 'utf-8');
238
+ }
239
+ catch {
240
+ return [];
241
+ }
242
+ const globs = [];
243
+ let inPackages = false;
244
+ for (const raw of text.split('\n')) {
245
+ if (/^packages:\s*$/.test(raw)) {
246
+ inPackages = true;
247
+ continue;
248
+ }
249
+ if (!inPackages)
250
+ continue;
251
+ const item = raw.match(/^\s*-\s*['"]?([^'"#]+?)['"]?\s*(?:#.*)?$/);
252
+ if (item?.[1] !== undefined) {
253
+ globs.push(item[1].trim());
254
+ continue;
255
+ }
256
+ // A non-list, non-blank line at column 0 ends the packages block.
257
+ if (raw.trim() !== '' && !/^\s/.test(raw))
258
+ break;
259
+ }
260
+ return globs;
261
+ }
262
+ // ---------------------------------------------------------------------------
263
+ // package.json helpers
264
+ // ---------------------------------------------------------------------------
265
+ function packageName(dir) {
266
+ const pkg = readPackageJson(dir);
267
+ return typeof pkg?.name === 'string' ? pkg.name : null;
268
+ }
269
+ function packageMain(dir) {
270
+ const pkg = readPackageJson(dir);
271
+ return typeof pkg?.main === 'string' ? pkg.main : null;
272
+ }
273
+ function readPackageJson(dir) {
274
+ const path = join(dir, 'package.json');
275
+ if (!existsSync(path))
276
+ return null;
277
+ try {
278
+ return JSON.parse(readFileSync(path, 'utf-8'));
279
+ }
280
+ catch {
281
+ return null;
282
+ }
283
+ }
284
+ function isFile(path) {
285
+ try {
286
+ return statSync(path).isFile();
287
+ }
288
+ catch {
289
+ return false;
290
+ }
291
+ }
292
+ /**
293
+ * realpath that falls back to the input when the path does not exist.
294
+ *
295
+ * `.native` is load-bearing, not an optimisation. The JS `realpathSync`
296
+ * resolves symlinks but echoes back whatever casing it was handed, whereas the
297
+ * platform `realpath(3)` reports the name as it is spelled on disk. That
298
+ * matters because `statSync` succeeds for a mis-cased path on a
299
+ * case-insensitive filesystem (APFS, NTFS): `./utils/foo` finds `Utils/Foo.ts`
300
+ * and nothing in the return value says the match was inexact. Echoing the
301
+ * specifier's casing then puts a `resolved_path` in the database that disagrees
302
+ * with the walker's `files.path` — fast-glob reports the on-disk name — and
303
+ * every path-range join against `files.path` (`resolveInFileOrReExportChain`,
304
+ * `insertReExportFiles`, `resolveTypeContext`) matches nothing and drops the
305
+ * edge silently. Canonicalising here fixes all three at the source, and never
306
+ * has to guess between two candidates the way a case-folded lookup would: on a
307
+ * case-insensitive filesystem `Foo.ts` and `foo.ts` cannot coexist, and on a
308
+ * case-sensitive one `statSync` already matched the literal name.
309
+ */
310
+ function safeRealpath(path) {
311
+ try {
312
+ return realpathSync.native(path);
313
+ }
314
+ catch {
315
+ return path;
316
+ }
317
+ }
318
+ //# sourceMappingURL=import-resolver.js.map