@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,177 @@
1
+ import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
2
+ import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
3
+ import { randomUUID } from 'node:crypto';
4
+ import { existsSync } from 'node:fs';
5
+ import { join } from 'node:path';
6
+ import { openDatabase } from '../graph/db.js';
7
+ import { SqliteChunkStore } from '../store/sqliteChunkStore.js';
8
+ import { runIndex, loadIndexMeta } from '../indexer/index.js';
9
+ import { startWatchMode } from '../indexer/watcher.js';
10
+ import { bootstrapState } from './startup.js';
11
+ import { registerAllTools } from './register-tools.js';
12
+ // ---------------------------------------------------------------------------
13
+ // M6 Part A (eval/GITNEXUS_COMPARISON.md §13.8 item 4): refuse to serve only
14
+ // where nothing can EVER fill the index — see assertServableIndex below.
15
+ // ---------------------------------------------------------------------------
16
+ /**
17
+ * Thrown by {@link assertServableIndex} when `mast serve --no-startup-reindex`
18
+ * targets a state directory that has never completed an index run. Extends
19
+ * `Error` per project convention (never throw a plain string/object) —
20
+ * mirrors `cli/query.ts`'s `QueryError` precedent for a typed, CLI-surfaced
21
+ * failure mode.
22
+ */
23
+ export class NeverIndexedError extends Error {
24
+ }
25
+ /**
26
+ * True when `config.resolved_state_dir` has never completed an index run:
27
+ * either `graph.db` itself is absent (nothing has ever opened the database),
28
+ * or `index.json` reports zero chunks AND a null/absent `last_indexed` (the
29
+ * database was opened — e.g. by a prior aborted run — but no index write
30
+ * ever landed). Read via `loadIndexMeta` (`indexer/index.ts`), the same
31
+ * unvalidated `JSON.parse` reader every other `index.json` consumer uses.
32
+ *
33
+ * Deliberately NOT "chunk_count === 0" alone — a state dir indexed over a
34
+ * genuinely empty file set (no matching files) still writes a live
35
+ * `graph.db` and `index.json` with `last_indexed` set. That is Part B's
36
+ * territory
37
+ * (`mcp/tools/_helpers.ts`'s `isIndexEmpty`), not Part A's: the index CAN be
38
+ * queried honestly (with `index_empty: true`), it just has nothing in it.
39
+ * This function only fires for the state that will NEVER produce anything
40
+ * different, because nothing (that this server process controls) is ever
41
+ * going to index it.
42
+ *
43
+ * MUST be called before `openDatabase` runs (`serve()`'s Step 2, below):
44
+ * `openDatabase` creates `graph.db` with an empty schema as a side effect of
45
+ * opening a missing file (`graph/db.ts`), which would flip this check to
46
+ * "not absent" a moment later — the same ordering hazard `cli/query.ts`'s
47
+ * `runQuery` already documents for its own graph.db existence check.
48
+ */
49
+ function isNeverIndexed(config) {
50
+ const graphDbPath = join(config.resolved_state_dir, 'graph.db');
51
+ if (!existsSync(graphDbPath))
52
+ return true;
53
+ const meta = loadIndexMeta(config.resolved_state_dir);
54
+ return (meta?.chunk_count ?? 0) === 0 && (meta?.last_indexed ?? null) === null;
55
+ }
56
+ /**
57
+ * Refuse to start serving when nothing can ever fill the index (M6 Part A).
58
+ *
59
+ * A blanket "refuse whenever the state dir is empty" is wrong: §7.4's
60
+ * startup ladder LEGITIMATELY serves from an empty state dir while Step 4's
61
+ * background reindex fills it in — that window is the designed SDD
62
+ * container flow and must keep working untouched. This function is a no-op
63
+ * whenever the startup reindex is enabled (the default) — see the early
64
+ * return below — so it can never gate or delay that path.
65
+ *
66
+ * The refusal fires only for the one combination where an empty index can
67
+ * NEVER be filled by this server process: `--no-startup-reindex` disables
68
+ * the one mechanism that would have filled it, permanently pinning every
69
+ * query at `{"results":[]}` — indistinguishable from "symbol doesn't exist",
70
+ * the exact ambiguity M6 (`eval/GITNEXUS_COMPARISON.md` §13.8 item 4) names.
71
+ * Part B (`mcp/tools/_helpers.ts`'s `isIndexEmpty`, consumed by every read
72
+ * tool) covers the legitimate empty window this function deliberately
73
+ * leaves alone.
74
+ *
75
+ * @throws NeverIndexedError naming the state dir and suggesting `mast init`
76
+ * / `mast index`, or dropping `--no-startup-reindex`.
77
+ */
78
+ export function assertServableIndex(config, options) {
79
+ if (options.noStartupReindex !== true)
80
+ return;
81
+ if (!isNeverIndexed(config))
82
+ return;
83
+ throw new NeverIndexedError(`mast serve: state directory "${config.resolved_state_dir}" has never been indexed, and ` +
84
+ '--no-startup-reindex disables the startup reindex that would normally fill it — every ' +
85
+ 'query would return {"results":[]} forever, indistinguishable from "symbol doesn\'t exist". ' +
86
+ 'Run `mast init` or `mast index` against this state directory first, or drop --no-startup-reindex.');
87
+ }
88
+ /**
89
+ * Run the MAST MCP server.
90
+ *
91
+ * Implements the startup ladder from §7.4 (Step 4's embed half was excised in
92
+ * Stage 7.1, IMPLEMENTATION_PLAN.md "Stage 7: Vector-store deletion"):
93
+ * Step 1 — bootstrap state directory (< 1s)
94
+ * Step 2 — schema version check + open DB (< 2s)
95
+ * Step 3 — open MCP transport, register tools, accept connections (< 1s)
96
+ * Step 4 — background incremental reindex (async)
97
+ */
98
+ export async function serve(options) {
99
+ const { config } = options;
100
+ // ── Steps 1–2: bootstrap state dir + schema-version guard ─────────────────
101
+ // On a schema-version change this wipes all derived state (graph.db,
102
+ // file_manifest.json, plus any orphaned lance/embed_cache/ left behind by a
103
+ // pre-Stage-7 state dir) before any of it is opened. See §7.4.
104
+ const { needsFullReindex } = await bootstrapState(config);
105
+ // M6 Part A: refuse before openDatabase (which would create graph.db as a
106
+ // side effect and mask the never-indexed state this check exists to catch)
107
+ // — see assertServableIndex's docstring above for the full ordering hazard.
108
+ assertServableIndex(config, { noStartupReindex: options.noStartupReindex });
109
+ const db = openDatabase(config.resolved_state_dir);
110
+ // M1 (eval/GITNEXUS_COMPARISON.md §15.1): chunks live in graph.db's
111
+ // `chunks` table — `SqliteChunkStore` wraps the same `db` connection every
112
+ // tool below already uses.
113
+ const chunkStore = new SqliteChunkStore(db);
114
+ // ── Step 3: open MCP transport and register tools ─────────────────────────
115
+ const ctx = {
116
+ db,
117
+ chunkStore,
118
+ config,
119
+ sessionId: randomUUID(),
120
+ };
121
+ const server = new McpServer({ name: 'mast', version: '0.1.0' });
122
+ registerAllTools(server, ctx);
123
+ const transport = new StdioServerTransport();
124
+ await server.connect(transport);
125
+ // ── Optional --watch mode (§11.4) ──────────────────────────────────────────
126
+ // Started after the transport so a watcher failure can never block serving.
127
+ // Each batch is one incremental Phase 1 (same structure.lock path as
128
+ // mast_reindex). JIT staleness handling (§9.0) already guarantees read
129
+ // correctness; watch mode only keeps the FTS/graph index fresh between reads.
130
+ const startWatchIfRequested = () => {
131
+ if (options.watch !== true)
132
+ return;
133
+ let watchHandle = null;
134
+ try {
135
+ watchHandle = startWatchMode({
136
+ config,
137
+ runBatch: async (paths) => {
138
+ process.stderr.write(`[mast] watch: reindexing after ${paths.length} change(s)\n`);
139
+ await runIndex(config, { incremental: true });
140
+ },
141
+ onWarn: (message) => process.stderr.write(`${message}\n`),
142
+ });
143
+ }
144
+ catch (err) {
145
+ // Degrade gracefully (EMFILE, permissions, …) — serve without watch.
146
+ process.stderr.write(`[mast] watch: failed to start watcher, continuing without --watch: ${String(err)}\n`);
147
+ return;
148
+ }
149
+ // Clean shutdown: chokidar's persistent watcher would otherwise keep the
150
+ // process alive after the MCP client disconnects (stdin close) or on
151
+ // SIGTERM/SIGINT.
152
+ const closeWatcher = () => {
153
+ const handle = watchHandle;
154
+ watchHandle = null;
155
+ if (handle !== null)
156
+ void handle.close().catch(() => { });
157
+ };
158
+ process.stdin.on('close', closeWatcher);
159
+ process.once('SIGTERM', closeWatcher);
160
+ process.once('SIGINT', closeWatcher);
161
+ };
162
+ // ── Step 4: background reindex ────────────────────────────────────────────
163
+ if (options.noStartupReindex) {
164
+ startWatchIfRequested();
165
+ return;
166
+ }
167
+ void (async () => {
168
+ try {
169
+ await runIndex(config, { incremental: !needsFullReindex });
170
+ }
171
+ catch (err) {
172
+ process.stderr.write(`[mast] startup indexing failed: ${String(err)}\n`);
173
+ }
174
+ })();
175
+ startWatchIfRequested();
176
+ }
177
+ //# sourceMappingURL=server.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"server.js","sourceRoot":"","sources":["../../src/mcp/server.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,72 @@
1
+ import type { Db } from '../graph/db.js';
2
+ import type { ResolvedConfig } from '../store/config.js';
3
+ import { populateFile } from '../graph/populate.js';
4
+ /**
5
+ * Stat each of `filePaths` (deduplicated by the caller — see below) and
6
+ * compare disk mtime against the `files` table's stored mtime, WITHOUT
7
+ * acquiring `structure.lock` or re-parsing anything.
8
+ *
9
+ * `mast_search` and `mast_implementors` deliberately do NOT get the
10
+ * JIT-refresh treatment `checkAndRefreshIfStale` gives single-file tools
11
+ * (`mast_signature`, `mast_exports`, ...): both can return results spanning
12
+ * dozens of files, and a naive per-result JIT refresh would mean up to ~50
13
+ * `structure.lock` acquisitions on one call PLUS re-parsing files whose
14
+ * content change could invalidate the ranking that already selected these
15
+ * results (a result re-parsed mid-response could shift rank, gain/lose a
16
+ * match, or change chunk boundaries entirely — `eval/GITNEXUS_COMPARISON.md`
17
+ * §13.7). Stat-and-flag reports the discrepancy honestly without paying
18
+ * that cost or contaminating the ranking: the agent is told the coordinates
19
+ * may be off and can decide whether to re-verify.
20
+ *
21
+ * Returns the subset of `filePaths` to flag with `stale: true` (C1 split
22
+ * this signal off the JIT-refresh tools' `file_busy_returning_stale_cache`
23
+ * — no lock is ever taken here, so nothing is ever "busy" in that sense):
24
+ * disk mtime newer than the indexed mtime, OR the stat failed (file
25
+ * deleted/renamed — its coordinates are definitely untrustworthy). A path
26
+ * absent from the `files` table is left out of the returned set entirely —
27
+ * there is nothing indexed to be stale against, so no signal is invented.
28
+ */
29
+ export declare function findStaleFiles(db: Db, config: ResolvedConfig, filePaths: readonly string[]): Promise<ReadonlySet<string>>;
30
+ export interface StalenessCheckResult {
31
+ /** True if a JIT re-parse was successfully performed. */
32
+ readonly refreshed: boolean;
33
+ /**
34
+ * True when the file was stale but the lock could not be acquired (file is
35
+ * mid-write). Callers should set `file_busy_returning_stale_cache: true` on
36
+ * affected result objects rather than throwing.
37
+ */
38
+ readonly busy: boolean;
39
+ }
40
+ /**
41
+ * Check `filePath` for staleness by comparing disk mtime against
42
+ * `storedMtime`. If stale, re-parse and persist it via {@link populateFile}
43
+ * (10–50ms per spec §9.0).
44
+ *
45
+ * **F11 (`IMPLEMENTATION_PLAN.md` "Replace fail-fast advisory locking")
46
+ * removed `structure.lock` from this path entirely.** Before F11, this
47
+ * function acquired the lock (`maxRetries: 3, retryIntervalMs: 100`) before
48
+ * re-parsing — but `structure.lock` was a single lock per state dir with no
49
+ * file component (`store/lock.ts`'s `markerPath`), so a JIT re-parse of file
50
+ * A blocked a JIT re-parse of file B despite touching disjoint rows. E7
51
+ * measured this as 35–88.5% JIT failure rates under pure reader-vs-reader
52
+ * concurrency (zero reindex running), which only gets worse as more agents
53
+ * work concurrently. That contention class disappears by construction once
54
+ * this path stops taking a lock at all: two JIT refreshes of different files
55
+ * now run their (dominant-cost, per E7-r2) parse step fully in parallel, and
56
+ * only briefly serialize on `populateFile`'s own short `BEGIN IMMEDIATE`
57
+ * write (see that function's doc comment) — which also correctly governs the
58
+ * ONE thing `structure.lock` was still protecting on this path: real
59
+ * cross-connection SQLite contention (a concurrent reindex batch, or another
60
+ * `mast serve` process). `structure.lock` itself is not deleted — coarse
61
+ * writers (`mast index`, `mast_reindex`, the manifest/`index.json` phase, the
62
+ * checker-resolver flush) still acquire it unchanged, because SQLite cannot
63
+ * coordinate the plain-JSON manifest writes those callers also make.
64
+ *
65
+ * On a genuinely contended write (`populateFile`'s `BEGIN IMMEDIATE` losing
66
+ * its own short `busy_timeout` wait, or a mid-write parse failure) returns
67
+ * `{ busy: true }` so the caller can fall back to the TOCTOU policy — return
68
+ * the stale chunk with `file_busy_returning_stale_cache: true`. Never throws
69
+ * for these expected contention outcomes (§9.0).
70
+ */
71
+ export declare function checkAndRefreshIfStale(db: Db, config: ResolvedConfig, filePath: string, storedMtime: number, populateFileImpl?: typeof populateFile): Promise<StalenessCheckResult>;
72
+ //# sourceMappingURL=staleness.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"staleness.d.ts","sourceRoot":"","sources":["../../src/mcp/staleness.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,EAAE,EAAE,MAAM,gBAAgB,CAAC;AACzC,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,oBAAoB,CAAC;AAEzD,OAAO,EAAE,YAAY,EAAE,MAAM,sBAAsB,CAAC;AA+DpD;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AACH,wBAAsB,cAAc,CAClC,EAAE,EAAE,EAAE,EACN,MAAM,EAAE,cAAc,EACtB,SAAS,EAAE,SAAS,MAAM,EAAE,GAC3B,OAAO,CAAC,WAAW,CAAC,MAAM,CAAC,CAAC,CAqB9B;AAMD,MAAM,WAAW,oBAAoB;IACnC,yDAAyD;IACzD,QAAQ,CAAC,SAAS,EAAE,OAAO,CAAC;IAC5B;;;;OAIG;IACH,QAAQ,CAAC,IAAI,EAAE,OAAO,CAAC;CACxB;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA8BG;AACH,wBAAsB,sBAAsB,CAC1C,EAAE,EAAE,EAAE,EACN,MAAM,EAAE,cAAc,EACtB,QAAQ,EAAE,MAAM,EAChB,WAAW,EAAE,MAAM,EAKnB,gBAAgB,GAAE,OAAO,YAA2B,GACnD,OAAO,CAAC,oBAAoB,CAAC,CAwE/B"}
@@ -0,0 +1,213 @@
1
+ import { statSync } from 'node:fs';
2
+ import { join } from 'node:path';
3
+ import { extractFile } from '../ast/extract.js';
4
+ import { populateFile } from '../graph/populate.js';
5
+ function hasSqliteCode(err) {
6
+ return err instanceof Error && 'code' in err && typeof err.code === 'string';
7
+ }
8
+ /**
9
+ * `SQLITE_BUSY_SNAPSHOT` (and plain `SQLITE_BUSY`) are the only codes this
10
+ * function treats as recoverable.
11
+ *
12
+ * Originally (F13): F12's monotonic-guard `SELECT` turned `populateFile`'s
13
+ * transaction into a read-then-write shape, which — under Kysely's deferred
14
+ * `BEGIN` (`sqlite-driver.js:32-34`) — could make the transaction's snapshot
15
+ * go stale if another writer committed in between; a fresh transaction takes
16
+ * a fresh snapshot and usually succeeds (E7-r2, `eval/e7-round2.json`, 52
17
+ * occurrences across 23/32 real runs).
18
+ *
19
+ * F11 moved `populateFile` to `BEGIN IMMEDIATE` (`graph/populate.ts`), which
20
+ * eliminates that specific `SQLITE_BUSY_SNAPSHOT` failure mode at the source
21
+ * — it takes the write reservation up front instead of discovering
22
+ * contention on commit. This retry-then-map loop stays as the backstop §9.0
23
+ * requires regardless: `populateFile`'s own short `busy_timeout`
24
+ * (`IMMEDIATE_WRITE_BUSY_TIMEOUT_MS`) can still be legitimately exhausted
25
+ * under real contention (surfacing as plain `SQLITE_BUSY`), and a retry with
26
+ * a fresh transaction is still the correct response to either code. Any
27
+ * other `SQLITE_*` code (constraint violation, corruption, disk full, ...)
28
+ * is a genuinely unexpected failure, not TOCTOU noise — conflating it with
29
+ * "busy" would repeat the parse_errors/write_errors misclassification this
30
+ * codebase already fixed once (`indexer/__tests__/write-failures.test.ts`),
31
+ * so it is deliberately excluded here and left to propagate.
32
+ */
33
+ function isRetryableBusySnapshot(err) {
34
+ return hasSqliteCode(err) && (err.code === 'SQLITE_BUSY_SNAPSHOT' || err.code === 'SQLITE_BUSY');
35
+ }
36
+ /**
37
+ * Bounded retry count for `populateFile` under a busy snapshot. Chosen small
38
+ * (1 retry = 2 attempts total) because each retry opens a brand-new
39
+ * transaction — either the contention has cleared and the retry succeeds
40
+ * immediately, or the file is under sustained concurrent write pressure and
41
+ * further retries just delay the `busy: true` fallback the TOCTOU policy
42
+ * (§9.0) already exists to handle. No sleep between attempts: a stale
43
+ * `SQLITE_BUSY_SNAPSHOT` needs a fresh transaction, not time, to resolve;
44
+ * `SQLITE_BUSY` already got its own bounded wait inside `populateFile`'s own
45
+ * `busy_timeout` (`IMMEDIATE_WRITE_BUSY_TIMEOUT_MS`, F11) before it ever
46
+ * reaches this retry loop, so adding a second wait here would double-count
47
+ * the same contention.
48
+ */
49
+ const MAX_POPULATE_RETRIES = 1;
50
+ // ---------------------------------------------------------------------------
51
+ // F7 — stat-and-flag staleness for mast_search / mast_implementors (§9.0)
52
+ // ---------------------------------------------------------------------------
53
+ /**
54
+ * Stat each of `filePaths` (deduplicated by the caller — see below) and
55
+ * compare disk mtime against the `files` table's stored mtime, WITHOUT
56
+ * acquiring `structure.lock` or re-parsing anything.
57
+ *
58
+ * `mast_search` and `mast_implementors` deliberately do NOT get the
59
+ * JIT-refresh treatment `checkAndRefreshIfStale` gives single-file tools
60
+ * (`mast_signature`, `mast_exports`, ...): both can return results spanning
61
+ * dozens of files, and a naive per-result JIT refresh would mean up to ~50
62
+ * `structure.lock` acquisitions on one call PLUS re-parsing files whose
63
+ * content change could invalidate the ranking that already selected these
64
+ * results (a result re-parsed mid-response could shift rank, gain/lose a
65
+ * match, or change chunk boundaries entirely — `eval/GITNEXUS_COMPARISON.md`
66
+ * §13.7). Stat-and-flag reports the discrepancy honestly without paying
67
+ * that cost or contaminating the ranking: the agent is told the coordinates
68
+ * may be off and can decide whether to re-verify.
69
+ *
70
+ * Returns the subset of `filePaths` to flag with `stale: true` (C1 split
71
+ * this signal off the JIT-refresh tools' `file_busy_returning_stale_cache`
72
+ * — no lock is ever taken here, so nothing is ever "busy" in that sense):
73
+ * disk mtime newer than the indexed mtime, OR the stat failed (file
74
+ * deleted/renamed — its coordinates are definitely untrustworthy). A path
75
+ * absent from the `files` table is left out of the returned set entirely —
76
+ * there is nothing indexed to be stale against, so no signal is invented.
77
+ */
78
+ export async function findStaleFiles(db, config, filePaths) {
79
+ if (filePaths.length === 0)
80
+ return new Set();
81
+ const rows = await db
82
+ .selectFrom('files')
83
+ .select(['path', 'mtime'])
84
+ .where('path', 'in', filePaths)
85
+ .execute();
86
+ const stale = new Set();
87
+ for (const row of rows) {
88
+ try {
89
+ const stat = statSync(join(config.resolved_project_root, row.path));
90
+ if (stat.mtimeMs / 1_000 > row.mtime)
91
+ stale.add(row.path);
92
+ }
93
+ catch {
94
+ // File deleted/renamed since indexing — coordinates are definitely
95
+ // untrustworthy, flag rather than silently omit.
96
+ stale.add(row.path);
97
+ }
98
+ }
99
+ return stale;
100
+ }
101
+ /**
102
+ * Check `filePath` for staleness by comparing disk mtime against
103
+ * `storedMtime`. If stale, re-parse and persist it via {@link populateFile}
104
+ * (10–50ms per spec §9.0).
105
+ *
106
+ * **F11 (`IMPLEMENTATION_PLAN.md` "Replace fail-fast advisory locking")
107
+ * removed `structure.lock` from this path entirely.** Before F11, this
108
+ * function acquired the lock (`maxRetries: 3, retryIntervalMs: 100`) before
109
+ * re-parsing — but `structure.lock` was a single lock per state dir with no
110
+ * file component (`store/lock.ts`'s `markerPath`), so a JIT re-parse of file
111
+ * A blocked a JIT re-parse of file B despite touching disjoint rows. E7
112
+ * measured this as 35–88.5% JIT failure rates under pure reader-vs-reader
113
+ * concurrency (zero reindex running), which only gets worse as more agents
114
+ * work concurrently. That contention class disappears by construction once
115
+ * this path stops taking a lock at all: two JIT refreshes of different files
116
+ * now run their (dominant-cost, per E7-r2) parse step fully in parallel, and
117
+ * only briefly serialize on `populateFile`'s own short `BEGIN IMMEDIATE`
118
+ * write (see that function's doc comment) — which also correctly governs the
119
+ * ONE thing `structure.lock` was still protecting on this path: real
120
+ * cross-connection SQLite contention (a concurrent reindex batch, or another
121
+ * `mast serve` process). `structure.lock` itself is not deleted — coarse
122
+ * writers (`mast index`, `mast_reindex`, the manifest/`index.json` phase, the
123
+ * checker-resolver flush) still acquire it unchanged, because SQLite cannot
124
+ * coordinate the plain-JSON manifest writes those callers also make.
125
+ *
126
+ * On a genuinely contended write (`populateFile`'s `BEGIN IMMEDIATE` losing
127
+ * its own short `busy_timeout` wait, or a mid-write parse failure) returns
128
+ * `{ busy: true }` so the caller can fall back to the TOCTOU policy — return
129
+ * the stale chunk with `file_busy_returning_stale_cache: true`. Never throws
130
+ * for these expected contention outcomes (§9.0).
131
+ */
132
+ export async function checkAndRefreshIfStale(db, config, filePath, storedMtime,
133
+ // Test-only injection point (§4.4 DI), mirroring `populateFile`'s own
134
+ // `ChunkWriter` seam (`graph/populate.ts`) — lets F13's regression tests
135
+ // (`mcp/__tests__/staleness.test.ts`) induce a genuine SQLITE_BUSY_SNAPSHOT
136
+ // via a second real connection instead of mocking `populateFile` wholesale.
137
+ populateFileImpl = populateFile) {
138
+ const absPath = join(config.resolved_project_root, filePath);
139
+ let diskMtime;
140
+ try {
141
+ const stat = statSync(absPath);
142
+ diskMtime = stat.mtimeMs / 1_000;
143
+ }
144
+ catch {
145
+ // File deleted — nothing to refresh; callers handle missing results.
146
+ return { refreshed: false, busy: false };
147
+ }
148
+ if (diskMtime <= storedMtime)
149
+ return { refreshed: false, busy: false };
150
+ // File is stale — re-parse and persist it. No lock acquisition here (F11,
151
+ // see this function's doc comment) — `populateFile`'s own `BEGIN IMMEDIATE`
152
+ // transaction is what now bounds and serializes the write.
153
+ let result;
154
+ try {
155
+ result = extractFile(absPath, config.resolved_project_root, config.context_lines, config.chunk_split_threshold, config.markdown_heading_depth);
156
+ }
157
+ catch {
158
+ // TOCTOU: file mid-write, first parse attempt failed.
159
+ await new Promise((res) => setTimeout(res, 50));
160
+ try {
161
+ result = extractFile(absPath, config.resolved_project_root, config.context_lines, config.chunk_split_threshold, config.markdown_heading_depth);
162
+ }
163
+ catch {
164
+ // Second attempt also failed — signal busy to caller.
165
+ return { refreshed: false, busy: true };
166
+ }
167
+ }
168
+ // populateFile writes chunks + FTS5 + graph rows in ONE SQLite transaction
169
+ // (M1, §15.1) — no separate chunk-store call needed here any more (the
170
+ // pre-M1 code wrote chunks to Lance in a step of its own, which — once
171
+ // SQLite became the default chunk store — would have left a JIT refresh's
172
+ // new chunk content invisible to every reader).
173
+ //
174
+ // F13: this call used to be unwrapped, so a SQLITE_BUSY_SNAPSHOT thrown
175
+ // here escaped uncaught — bypassing the busy flag below and violating
176
+ // §9.0's "do not throw" TOCTOU policy (52 real occurrences, E7-r2). F11's
177
+ // `BEGIN IMMEDIATE` (`graph/populate.ts`) eliminates that specific failure
178
+ // class at the source, but this retry-then-map loop stays as the backstop
179
+ // §9.0 requires: `populateFile`'s own short `busy_timeout` can still be
180
+ // legitimately exhausted under real contention, and that must map to
181
+ // `{ busy: true }`, never propagate as a raw throw. A non-busy error is NOT
182
+ // retried and NOT reclassified — it propagates immediately.
183
+ const totalAttempts = MAX_POPULATE_RETRIES + 1;
184
+ for (let attempt = 1; attempt <= totalAttempts; attempt++) {
185
+ try {
186
+ await populateFileImpl(db, {
187
+ filePath,
188
+ language: result.language,
189
+ mtime: diskMtime,
190
+ chunks: result.chunks,
191
+ imports: result.imports,
192
+ symbols: result.symbols,
193
+ identifierRows: result.identifierRows,
194
+ });
195
+ return { refreshed: true, busy: false };
196
+ }
197
+ catch (err) {
198
+ if (!isRetryableBusySnapshot(err))
199
+ throw err; // not busy — surface distinctly, do not retry
200
+ if (attempt === totalAttempts)
201
+ return { refreshed: false, busy: true }; // retries exhausted
202
+ // Retries remain — loop immediately, no sleep (see
203
+ // MAX_POPULATE_RETRIES's WHY-comment: a stale snapshot needs a fresh
204
+ // transaction, not time).
205
+ }
206
+ }
207
+ // Unreachable: the loop above always returns or throws on its final
208
+ // iteration. TypeScript can't prove that from a `for` bound by a runtime
209
+ // constant, so an explicit busy fallback keeps `noImplicitReturns`
210
+ // satisfied without an `as never`.
211
+ return { refreshed: false, busy: true };
212
+ }
213
+ //# sourceMappingURL=staleness.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"staleness.js","sourceRoot":"","sources":["../../src/mcp/staleness.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,MAAM,SAAS,CAAC;AACnC,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AAGjC,OAAO,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAC;AAChD,OAAO,EAAE,YAAY,EAAE,MAAM,sBAAsB,CAAC;AAWpD,SAAS,aAAa,CAAC,GAAY;IACjC,OAAO,GAAG,YAAY,KAAK,IAAI,MAAM,IAAI,GAAG,IAAI,OAAQ,GAAyB,CAAC,IAAI,KAAK,QAAQ,CAAC;AACtG,CAAC;AAED;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AACH,SAAS,uBAAuB,CAAC,GAAY;IAC3C,OAAO,aAAa,CAAC,GAAG,CAAC,IAAI,CAAC,GAAG,CAAC,IAAI,KAAK,sBAAsB,IAAI,GAAG,CAAC,IAAI,KAAK,aAAa,CAAC,CAAC;AACnG,CAAC;AAED;;;;;;;;;;;;GAYG;AACH,MAAM,oBAAoB,GAAG,CAAC,CAAC;AAE/B,8EAA8E;AAC9E,0EAA0E;AAC1E,8EAA8E;AAE9E;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AACH,MAAM,CAAC,KAAK,UAAU,cAAc,CAClC,EAAM,EACN,MAAsB,EACtB,SAA4B;IAE5B,IAAI,SAAS,CAAC,MAAM,KAAK,CAAC;QAAE,OAAO,IAAI,GAAG,EAAE,CAAC;IAE7C,MAAM,IAAI,GAAG,MAAM,EAAE;SAClB,UAAU,CAAC,OAAO,CAAC;SACnB,MAAM,CAAC,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;SACzB,KAAK,CAAC,MAAM,EAAE,IAAI,EAAE,SAAS,CAAC;SAC9B,OAAO,EAAE,CAAC;IAEb,MAAM,KAAK,GAAG,IAAI,GAAG,EAAU,CAAC;IAChC,KAAK,MAAM,GAAG,IAAI,IAAI,EAAE,CAAC;QACvB,IAAI,CAAC;YACH,MAAM,IAAI,GAAG,QAAQ,CAAC,IAAI,CAAC,MAAM,CAAC,qBAAqB,EAAE,GAAG,CAAC,IAAI,CAAC,CAAC,CAAC;YACpE,IAAI,IAAI,CAAC,OAAO,GAAG,KAAK,GAAG,GAAG,CAAC,KAAK;gBAAE,KAAK,CAAC,GAAG,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC;QAC5D,CAAC;QAAC,MAAM,CAAC;YACP,mEAAmE;YACnE,iDAAiD;YACjD,KAAK,CAAC,GAAG,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC;QACtB,CAAC;IACH,CAAC;IACD,OAAO,KAAK,CAAC;AACf,CAAC;AAiBD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA8BG;AACH,MAAM,CAAC,KAAK,UAAU,sBAAsB,CAC1C,EAAM,EACN,MAAsB,EACtB,QAAgB,EAChB,WAAmB;AACnB,sEAAsE;AACtE,yEAAyE;AACzE,4EAA4E;AAC5E,4EAA4E;AAC5E,mBAAwC,YAAY;IAEpD,MAAM,OAAO,GAAG,IAAI,CAAC,MAAM,CAAC,qBAAqB,EAAE,QAAQ,CAAC,CAAC;IAC7D,IAAI,SAAiB,CAAC;IACtB,IAAI,CAAC;QACH,MAAM,IAAI,GAAG,QAAQ,CAAC,OAAO,CAAC,CAAC;QAC/B,SAAS,GAAG,IAAI,CAAC,OAAO,GAAG,KAAK,CAAC;IACnC,CAAC;IAAC,MAAM,CAAC;QACP,qEAAqE;QACrE,OAAO,EAAE,SAAS,EAAE,KAAK,EAAE,IAAI,EAAE,KAAK,EAAE,CAAC;IAC3C,CAAC;IAED,IAAI,SAAS,IAAI,WAAW;QAAE,OAAO,EAAE,SAAS,EAAE,KAAK,EAAE,IAAI,EAAE,KAAK,EAAE,CAAC;IAEvE,0EAA0E;IAC1E,4EAA4E;IAC5E,2DAA2D;IAC3D,IAAI,MAAM,CAAC;IACX,IAAI,CAAC;QACH,MAAM,GAAG,WAAW,CAAC,OAAO,EAAE,MAAM,CAAC,qBAAqB,EAAE,MAAM,CAAC,aAAa,EAAE,MAAM,CAAC,qBAAqB,EAAE,MAAM,CAAC,sBAAsB,CAAC,CAAC;IACjJ,CAAC;IAAC,MAAM,CAAC;QACP,sDAAsD;QACtD,MAAM,IAAI,OAAO,CAAO,CAAC,GAAG,EAAE,EAAE,CAAC,UAAU,CAAC,GAAG,EAAE,EAAE,CAAC,CAAC,CAAC;QACtD,IAAI,CAAC;YACH,MAAM,GAAG,WAAW,CAAC,OAAO,EAAE,MAAM,CAAC,qBAAqB,EAAE,MAAM,CAAC,aAAa,EAAE,MAAM,CAAC,qBAAqB,EAAE,MAAM,CAAC,sBAAsB,CAAC,CAAC;QACjJ,CAAC;QAAC,MAAM,CAAC;YACP,sDAAsD;YACtD,OAAO,EAAE,SAAS,EAAE,KAAK,EAAE,IAAI,EAAE,IAAI,EAAE,CAAC;QAC1C,CAAC;IACH,CAAC;IAED,2EAA2E;IAC3E,uEAAuE;IACvE,uEAAuE;IACvE,0EAA0E;IAC1E,gDAAgD;IAChD,EAAE;IACF,wEAAwE;IACxE,sEAAsE;IACtE,0EAA0E;IAC1E,2EAA2E;IAC3E,0EAA0E;IAC1E,wEAAwE;IACxE,qEAAqE;IACrE,4EAA4E;IAC5E,4DAA4D;IAC5D,MAAM,aAAa,GAAG,oBAAoB,GAAG,CAAC,CAAC;IAC/C,KAAK,IAAI,OAAO,GAAG,CAAC,EAAE,OAAO,IAAI,aAAa,EAAE,OAAO,EAAE,EAAE,CAAC;QAC1D,IAAI,CAAC;YACH,MAAM,gBAAgB,CAAC,EAAE,EAAE;gBACzB,QAAQ;gBACR,QAAQ,EAAE,MAAM,CAAC,QAAQ;gBACzB,KAAK,EAAE,SAAS;gBAChB,MAAM,EAAE,MAAM,CAAC,MAAM;gBACrB,OAAO,EAAE,MAAM,CAAC,OAAO;gBACvB,OAAO,EAAE,MAAM,CAAC,OAAO;gBACvB,cAAc,EAAE,MAAM,CAAC,cAAc;aACtC,CAAC,CAAC;YACH,OAAO,EAAE,SAAS,EAAE,IAAI,EAAE,IAAI,EAAE,KAAK,EAAE,CAAC;QAC1C,CAAC;QAAC,OAAO,GAAG,EAAE,CAAC;YACb,IAAI,CAAC,uBAAuB,CAAC,GAAG,CAAC;gBAAE,MAAM,GAAG,CAAC,CAAC,8CAA8C;YAC5F,IAAI,OAAO,KAAK,aAAa;gBAAE,OAAO,EAAE,SAAS,EAAE,KAAK,EAAE,IAAI,EAAE,IAAI,EAAE,CAAC,CAAC,oBAAoB;YAC5F,mDAAmD;YACnD,qEAAqE;YACrE,0BAA0B;QAC5B,CAAC;IACH,CAAC;IAED,oEAAoE;IACpE,yEAAyE;IACzE,mEAAmE;IACnE,mCAAmC;IACnC,OAAO,EAAE,SAAS,EAAE,KAAK,EAAE,IAAI,EAAE,IAAI,EAAE,CAAC;AAC1C,CAAC"}
@@ -0,0 +1,40 @@
1
+ import type { ResolvedConfig } from '../store/config.js';
2
+ /**
3
+ * Remove all index-derived state from `stateDir`, keeping `config.json` and the
4
+ * advisory lock markers. Used on a schema-version change (§7.4 Step 2) so a
5
+ * stale on-disk shape can never be read by code expecting the new schema.
6
+ */
7
+ export declare function wipeDerivedState(stateDir: string): void;
8
+ /**
9
+ * Best-effort delete orphaned vector-store state from `stateDir`.
10
+ *
11
+ * Stage 7 (decision 3) deliberately did NOT bump `CURRENT_SCHEMA_VERSION` —
12
+ * nothing the new code READS changed shape, so `bootstrapState`'s schema
13
+ * guard never fires for a pre-Stage-7 state dir and never runs
14
+ * {@link wipeDerivedState} on its behalf. Without this, `lance/`/`embed_cache/`/
15
+ * `vectors.lock` would sit on disk forever after an upgrade. Runs
16
+ * unconditionally on every startup (not gated on the schema check), and never
17
+ * throws: a permission error or a race with another process must not block
18
+ * serving over a directory nothing reads any more.
19
+ */
20
+ export declare function cleanupOrphanedVectorState(stateDir: string): void;
21
+ export interface BootstrapResult {
22
+ /** True when a full (non-incremental) reindex must run, e.g. after a wipe. */
23
+ readonly needsFullReindex: boolean;
24
+ }
25
+ /**
26
+ * Startup steps 1–2 of §7.4.
27
+ *
28
+ * Step 1 — bootstrap the state directory: copy the Docker-baked seed when the
29
+ * state dir is empty, ensure lock markers exist, persist the resolved
30
+ * config, and best-effort remove orphaned pre-Stage-7 vector-store state
31
+ * (IMPLEMENTATION_PLAN.md "Stage 7: Vector-store deletion", decision 3).
32
+ * Step 2 — enforce the schema-version guard: if the on-disk `index.json` was
33
+ * written by a different `schema_version` (including a seed baked against an
34
+ * old schema, §13.8.2), wipe ALL derived state and request a full rebuild.
35
+ *
36
+ * `seedPath` is injectable so tests can point at a fixture or a path that does
37
+ * not exist (the common test case).
38
+ */
39
+ export declare function bootstrapState(config: ResolvedConfig, seedPath?: string): Promise<BootstrapResult>;
40
+ //# sourceMappingURL=startup.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"startup.d.ts","sourceRoot":"","sources":["../../src/mcp/startup.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,oBAAoB,CAAC;AAsBzD;;;;GAIG;AACH,wBAAgB,gBAAgB,CAAC,QAAQ,EAAE,MAAM,GAAG,IAAI,CAIvD;AAgBD;;;;;;;;;;;GAWG;AACH,wBAAgB,0BAA0B,CAAC,QAAQ,EAAE,MAAM,GAAG,IAAI,CAejE;AAED,MAAM,WAAW,eAAe;IAC9B,8EAA8E;IAC9E,QAAQ,CAAC,gBAAgB,EAAE,OAAO,CAAC;CACpC;AAED;;;;;;;;;;;;;GAaG;AACH,wBAAsB,cAAc,CAClC,MAAM,EAAE,cAAc,EACtB,QAAQ,GAAE,MAA0B,GACnC,OAAO,CAAC,eAAe,CAAC,CA2B1B"}
@@ -0,0 +1,114 @@
1
+ import { existsSync, rmSync } from 'node:fs';
2
+ import { cp } from 'node:fs/promises';
3
+ import { join } from 'node:path';
4
+ import { CURRENT_SCHEMA_VERSION, writeStateConfig } from '../store/config.js';
5
+ import { initLockMarkers } from '../store/lock.js';
6
+ import { loadIndexMeta, writeIndexMeta } from '../indexer/index.js';
7
+ /** Default location of the Docker-baked seed index (§13.8). */
8
+ const DEFAULT_SEED_PATH = '/opt/mast-seed';
9
+ /**
10
+ * All index-derived state under the state directory. Everything here is rebuilt
11
+ * from source by a reindex; `config.json` and the lock marker files are NOT in
12
+ * this set and are preserved.
13
+ */
14
+ const DERIVED_STATE_ENTRIES = [
15
+ 'lance', // chunks + vectors tables
16
+ 'graph.db', // knowledge graph + FTS
17
+ 'graph.db-wal', // SQLite WAL sidecar
18
+ 'graph.db-shm', // SQLite shared-memory sidecar
19
+ 'file_manifest.json', // mtime snapshot
20
+ 'embed_cache', // per-content-hash embedding cache
21
+ ];
22
+ /**
23
+ * Remove all index-derived state from `stateDir`, keeping `config.json` and the
24
+ * advisory lock markers. Used on a schema-version change (§7.4 Step 2) so a
25
+ * stale on-disk shape can never be read by code expecting the new schema.
26
+ */
27
+ export function wipeDerivedState(stateDir) {
28
+ for (const entry of DERIVED_STATE_ENTRIES) {
29
+ rmSync(join(stateDir, entry), { recursive: true, force: true });
30
+ }
31
+ }
32
+ // ---------------------------------------------------------------------------
33
+ // Orphaned vector-store state (IMPLEMENTATION_PLAN.md "Stage 7: Vector-store
34
+ // deletion", decision 3)
35
+ // ---------------------------------------------------------------------------
36
+ /**
37
+ * State a pre-Stage-7 install can leave behind: `lance/` (the retired chunk +
38
+ * vector tables), `embed_cache/` (per-content-hash embedding cache), and
39
+ * `vectors.lock` (proper-lockfile's own `<marker>.lock` artifact for the
40
+ * 'vectors' lock type, which Stage 7.1 removed — `store/lock.ts`'s
41
+ * `LockType` union of one).
42
+ */
43
+ const ORPHANED_VECTOR_STATE_ENTRIES = ['lance', 'embed_cache', 'vectors.lock'];
44
+ /**
45
+ * Best-effort delete orphaned vector-store state from `stateDir`.
46
+ *
47
+ * Stage 7 (decision 3) deliberately did NOT bump `CURRENT_SCHEMA_VERSION` —
48
+ * nothing the new code READS changed shape, so `bootstrapState`'s schema
49
+ * guard never fires for a pre-Stage-7 state dir and never runs
50
+ * {@link wipeDerivedState} on its behalf. Without this, `lance/`/`embed_cache/`/
51
+ * `vectors.lock` would sit on disk forever after an upgrade. Runs
52
+ * unconditionally on every startup (not gated on the schema check), and never
53
+ * throws: a permission error or a race with another process must not block
54
+ * serving over a directory nothing reads any more.
55
+ */
56
+ export function cleanupOrphanedVectorState(stateDir) {
57
+ const removed = [];
58
+ for (const entry of ORPHANED_VECTOR_STATE_ENTRIES) {
59
+ const entryPath = join(stateDir, entry);
60
+ try {
61
+ if (!existsSync(entryPath))
62
+ continue;
63
+ rmSync(entryPath, { recursive: true, force: true });
64
+ removed.push(entry);
65
+ }
66
+ catch (err) {
67
+ process.stderr.write(`[mast] startup: failed to remove orphaned vector state "${entry}": ${String(err)}\n`);
68
+ }
69
+ }
70
+ if (removed.length > 0) {
71
+ process.stderr.write(`[mast] startup: removed orphaned vector state: ${removed.join(', ')}\n`);
72
+ }
73
+ }
74
+ /**
75
+ * Startup steps 1–2 of §7.4.
76
+ *
77
+ * Step 1 — bootstrap the state directory: copy the Docker-baked seed when the
78
+ * state dir is empty, ensure lock markers exist, persist the resolved
79
+ * config, and best-effort remove orphaned pre-Stage-7 vector-store state
80
+ * (IMPLEMENTATION_PLAN.md "Stage 7: Vector-store deletion", decision 3).
81
+ * Step 2 — enforce the schema-version guard: if the on-disk `index.json` was
82
+ * written by a different `schema_version` (including a seed baked against an
83
+ * old schema, §13.8.2), wipe ALL derived state and request a full rebuild.
84
+ *
85
+ * `seedPath` is injectable so tests can point at a fixture or a path that does
86
+ * not exist (the common test case).
87
+ */
88
+ export async function bootstrapState(config, seedPath = DEFAULT_SEED_PATH) {
89
+ // Step 1.
90
+ if (!existsSync(config.resolved_state_dir) && existsSync(seedPath)) {
91
+ await cp(seedPath, config.resolved_state_dir, { recursive: true });
92
+ }
93
+ initLockMarkers(config.resolved_state_dir);
94
+ writeStateConfig(config.resolved_state_dir, config);
95
+ // Runs on every startup, independent of the schema-version guard below —
96
+ // Stage 7 deliberately did not bump CURRENT_SCHEMA_VERSION (decision 3), so
97
+ // the guard's wipe path never fires for a pre-Stage-7 state dir and this is
98
+ // the only thing that ever clears these orphans.
99
+ cleanupOrphanedVectorState(config.resolved_state_dir);
100
+ // Step 2.
101
+ const meta = loadIndexMeta(config.resolved_state_dir);
102
+ if (meta !== null && meta.schema_version !== CURRENT_SCHEMA_VERSION) {
103
+ wipeDerivedState(config.resolved_state_dir);
104
+ writeIndexMeta(config.resolved_state_dir, {
105
+ schema_version: CURRENT_SCHEMA_VERSION,
106
+ last_indexed: null,
107
+ file_count: 0,
108
+ chunk_count: 0,
109
+ });
110
+ return { needsFullReindex: true };
111
+ }
112
+ return { needsFullReindex: false };
113
+ }
114
+ //# sourceMappingURL=startup.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"startup.js","sourceRoot":"","sources":["../../src/mcp/startup.ts"],"names":[],"mappings":"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"}