@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,182 @@
1
+ import { mkdirSync, rmSync, writeFileSync } from 'node:fs';
2
+ import { join } from 'node:path';
3
+ import lockfile from 'proper-lockfile';
4
+ import { createFileLockMetricsSink } from './lockMetrics.js';
5
+ const STALE_MS = 10_000;
6
+ /**
7
+ * Thrown when the lock was taken away while this process still believed it
8
+ * held it — another process judged it stale and stole it, or the lock
9
+ * directory was removed underneath us.
10
+ *
11
+ * It is thrown at RELEASE, not when the compromise is detected, because
12
+ * proper-lockfile detects it inside an `fs.stat` callback on a timer: there is
13
+ * no caller on the stack to receive it there, which is exactly why the
14
+ * library's default handler (`onCompromised: (err) => { throw err; }`,
15
+ * `lib/lockfile.js`) reached the process as an uncaught exception and killed
16
+ * it — measured: `ECOMPROMISED ENOENT`, exit code 7, with no
17
+ * `uncaughtException` handler anywhere in `src`.
18
+ *
19
+ * **What this does and does not fix.** It converts a process kill into a
20
+ * failed operation the caller can retry, and it stops a run that was not
21
+ * exclusive from reporting success. It does NOT stop the work already in
22
+ * flight: by the time the compromise is detected, `fn` is mid-execution and
23
+ * nothing here can interrupt it. Doing that needs an `AbortSignal` threaded
24
+ * through `withLock` and honoured by every write phase, which is a design
25
+ * change, not a patch. The exposure is bounded: SQLite writes go through
26
+ * `populateFile`'s `BEGIN IMMEDIATE` transaction and stay serialised whatever
27
+ * this advisory lock believes, so what is actually at risk is the plain-JSON
28
+ * `file_manifest.json` / `index.json` writes this lock exists to coordinate —
29
+ * and a run that fails here is re-run, which rewrites both.
30
+ */
31
+ export class LockCompromisedError extends Error {
32
+ /** The underlying proper-lockfile error (`ECOMPROMISED`). */
33
+ reason;
34
+ constructor(type, reason) {
35
+ super(`The ${type} lock was compromised while held — another process took it, so this run was ` +
36
+ `not exclusive and its result must not be trusted. Re-run the operation. Cause: ${String(reason)}`);
37
+ this.name = 'LockCompromisedError';
38
+ this.reason = reason;
39
+ }
40
+ }
41
+ /** Returns the path of the marker file that proper-lockfile uses as its lock target. */
42
+ function markerPath(stateDir, type) {
43
+ return join(stateDir, type);
44
+ }
45
+ /**
46
+ * Ensure the marker files required by proper-lockfile exist in `stateDir`.
47
+ * Must be called during `mast init` before any lock can be acquired.
48
+ */
49
+ export function initLockMarkers(stateDir) {
50
+ mkdirSync(stateDir, { recursive: true });
51
+ for (const type of ['structure']) {
52
+ writeFileSync(markerPath(stateDir, type), '', { flag: 'a' });
53
+ }
54
+ }
55
+ /**
56
+ * Acquire an advisory write lock for the given lock type.
57
+ *
58
+ * Returns a release function. Always `await release()` in a finally block.
59
+ * The returned function is wrapped to record hold-duration lock metrics on
60
+ * release — this covers direct callers as well as callers that go through
61
+ * {@link withLock}.
62
+ *
63
+ * Per spec §7.6:
64
+ * - CLI commands: maxRetries=1, retryIntervalMs=2000 (2s total)
65
+ * - mast_reindex / mast serve startup: maxRetries=5, retryIntervalMs=1000
66
+ *
67
+ * **F11 (`IMPLEMENTATION_PLAN.md` "Replace fail-fast advisory locking")
68
+ * narrowed this lock's role**: the JIT re-parse path (`mcp/staleness.ts`'s
69
+ * `checkAndRefreshIfStale`) no longer calls `acquireLock` at all — it relies
70
+ * on `populateFile`'s own `BEGIN IMMEDIATE` transaction
71
+ * (`graph/populate.ts`) plus a dedicated short `busy_timeout` instead. This
72
+ * lock is now acquired only by coarse writers: `mast index` / the startup
73
+ * reindex, `mast_reindex`, and the manifest/`index.json` phase (plain-JSON
74
+ * `writeFileSync`, which SQLite can never coordinate) — see `populateFile`'s
75
+ * doc comment for the full design rationale.
76
+ *
77
+ * @throws if the lock cannot be acquired after all retries.
78
+ */
79
+ export async function acquireLock(stateDir, type, options = {}) {
80
+ const { maxRetries = 5, retryIntervalMs = 1_000, caller = 'unknown', sink = createFileLockMetricsSink(stateDir), staleMs = STALE_MS, } = options;
81
+ const marker = markerPath(stateDir, type);
82
+ const attemptStartMs = Date.now();
83
+ let lastError;
84
+ for (let attempt = 0; attempt <= maxRetries; attempt++) {
85
+ try {
86
+ // `onCompromised` must RECORD, never throw. The library invokes it from
87
+ // an `fs.stat` callback with no caller on the stack, so its default
88
+ // (rethrow) becomes an uncaught exception — see {@link LockCompromisedError}.
89
+ let compromise = null;
90
+ const release = await lockfile.lock(marker, {
91
+ stale: staleMs,
92
+ retries: 0,
93
+ onCompromised: (err) => { compromise = err; },
94
+ });
95
+ sink.record({ kind: 'acquired', type, caller, waitMs: Date.now() - attemptStartMs, timestamp: Date.now() });
96
+ const acquiredAtMs = Date.now();
97
+ return async () => {
98
+ // proper-lockfile marks a compromised lock released and drops it from
99
+ // its registry BEFORE calling the handler (`setLockAsCompromised`), so
100
+ // calling `release()` now only yields `ERELEASED` — there is nothing
101
+ // left of ours to release.
102
+ if (compromise === null)
103
+ await release();
104
+ // Recorded either way: the hold duration is real, and losing it would
105
+ // make the metrics under-report exactly the runs worth looking at.
106
+ sink.record({ kind: 'released', type, caller, holdMs: Date.now() - acquiredAtMs, timestamp: Date.now() });
107
+ if (compromise !== null)
108
+ throw new LockCompromisedError(type, compromise);
109
+ };
110
+ }
111
+ catch (err) {
112
+ lastError = err;
113
+ if (attempt < maxRetries) {
114
+ await new Promise((res) => setTimeout(res, retryIntervalMs));
115
+ }
116
+ }
117
+ }
118
+ sink.record({ kind: 'failed', type, caller, waitMs: Date.now() - attemptStartMs, timestamp: Date.now() });
119
+ throw new Error(`Could not acquire ${type} lock after ${maxRetries + 1} attempt(s): ${String(lastError)}`);
120
+ }
121
+ // Lock directories currently held by this process. A single signal handler
122
+ // removes ALL of them on shutdown — a per-`withLock` handler would race if
123
+ // more than one lock were held at once, because the first handler's
124
+ // `process.exit` pre-empts the others, leaking the second lock until its stale
125
+ // timeout. One registry + one handler also avoids piling up signal listeners.
126
+ const heldLockDirs = new Set();
127
+ let signalHandlerInstalled = false;
128
+ function installSignalHandlerOnce() {
129
+ if (signalHandlerInstalled)
130
+ return;
131
+ signalHandlerInstalled = true;
132
+ const onSignal = () => {
133
+ // release() is async and can't be awaited in a signal handler, so remove
134
+ // every held lock directory synchronously as a best-effort fallback.
135
+ for (const dir of heldLockDirs) {
136
+ try {
137
+ rmSync(dir, { recursive: true, force: true });
138
+ }
139
+ catch { /* already gone */ }
140
+ }
141
+ process.exit(1);
142
+ };
143
+ process.once('SIGTERM', onSignal);
144
+ process.once('SIGINT', onSignal);
145
+ }
146
+ /**
147
+ * Run `fn` while holding the given lock.
148
+ *
149
+ * The lock's directory is tracked in a process-wide registry that a single
150
+ * SIGTERM/SIGINT handler cleans up on shutdown, so an interrupted process does
151
+ * not leave `<type>.lock` behind for every lock it held.
152
+ *
153
+ * @throws {LockCompromisedError} if the lock was taken away while `fn` ran.
154
+ * `fn` still completed — see that error's doc for what is and is not
155
+ * guaranteed. If `fn` itself threw, that error is thrown instead.
156
+ */
157
+ export async function withLock(stateDir, type, options, fn) {
158
+ const release = await acquireLock(stateDir, type, options);
159
+ const lockDir = markerPath(stateDir, type) + '.lock';
160
+ heldLockDirs.add(lockDir);
161
+ installSignalHandlerOnce();
162
+ let outcome;
163
+ try {
164
+ outcome = { ok: true, value: await fn() };
165
+ }
166
+ catch (error) {
167
+ outcome = { ok: false, error };
168
+ }
169
+ heldLockDirs.delete(lockDir);
170
+ try {
171
+ await release();
172
+ }
173
+ catch (releaseError) {
174
+ if (!outcome.ok)
175
+ throw outcome.error;
176
+ throw releaseError;
177
+ }
178
+ if (!outcome.ok)
179
+ throw outcome.error;
180
+ return outcome.value;
181
+ }
182
+ //# sourceMappingURL=lock.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"lock.js","sourceRoot":"","sources":["../../src/store/lock.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,52 @@
1
+ import type { LockType } from './lock.js';
2
+ /**
3
+ * A single lock lifecycle event.
4
+ *
5
+ * `acquired` and `failed` are recorded synchronously with the outcome of an
6
+ * acquisition attempt (including retry backoff) — `waitMs` covers the full
7
+ * attempt, from first try to acquisition or final failure. `released` is
8
+ * recorded when the caller's release function completes and carries the
9
+ * hold duration, i.e. wall-clock time between acquisition and release.
10
+ */
11
+ export type LockEvent = {
12
+ readonly kind: 'acquired';
13
+ readonly type: LockType;
14
+ readonly caller: string;
15
+ readonly waitMs: number;
16
+ readonly timestamp: number;
17
+ } | {
18
+ readonly kind: 'released';
19
+ readonly type: LockType;
20
+ readonly caller: string;
21
+ readonly holdMs: number;
22
+ readonly timestamp: number;
23
+ } | {
24
+ readonly kind: 'failed';
25
+ readonly type: LockType;
26
+ readonly caller: string;
27
+ readonly waitMs: number;
28
+ readonly timestamp: number;
29
+ };
30
+ /**
31
+ * Sink for lock lifecycle events.
32
+ *
33
+ * A role interface (§4.3) so tests can inject an in-memory fake instead of
34
+ * touching the filesystem (§4.4) — the only production implementation is
35
+ * {@link createFileLockMetricsSink}.
36
+ */
37
+ export interface LockMetricsSink {
38
+ /** Must never throw — a metrics failure must never break lock acquisition/release. */
39
+ record(event: LockEvent): void;
40
+ }
41
+ export declare const LOCK_METRICS_FILENAME = "lock-metrics.jsonl";
42
+ /**
43
+ * Default sink: appends one JSON line per event to `<stateDir>/lock-metrics.jsonl`.
44
+ *
45
+ * Synchronous and best-effort: lock acquire/release happens rarely relative
46
+ * to read-tool traffic (near-zero overhead on the hot path), and a write
47
+ * failure (e.g. read-only state dir) is swallowed rather than surfaced,
48
+ * matching the fire-and-forget discipline `telemetry/metrics.ts` already
49
+ * uses for tool-call metrics.
50
+ */
51
+ export declare function createFileLockMetricsSink(stateDir: string): LockMetricsSink;
52
+ //# sourceMappingURL=lockMetrics.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"lockMetrics.d.ts","sourceRoot":"","sources":["../../src/store/lockMetrics.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,WAAW,CAAC;AA0B1C;;;;;;;;GAQG;AACH,MAAM,MAAM,SAAS,GACjB;IACE,QAAQ,CAAC,IAAI,EAAE,UAAU,CAAC;IAC1B,QAAQ,CAAC,IAAI,EAAE,QAAQ,CAAC;IACxB,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;CAC5B,GACD;IACE,QAAQ,CAAC,IAAI,EAAE,UAAU,CAAC;IAC1B,QAAQ,CAAC,IAAI,EAAE,QAAQ,CAAC;IACxB,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;CAC5B,GACD;IACE,QAAQ,CAAC,IAAI,EAAE,QAAQ,CAAC;IACxB,QAAQ,CAAC,IAAI,EAAE,QAAQ,CAAC;IACxB,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;CAC5B,CAAC;AAEN;;;;;;GAMG;AACH,MAAM,WAAW,eAAe;IAC9B,sFAAsF;IACtF,MAAM,CAAC,KAAK,EAAE,SAAS,GAAG,IAAI,CAAC;CAChC;AAKD,eAAO,MAAM,qBAAqB,uBAAuB,CAAC;AAE1D;;;;;;;;GAQG;AACH,wBAAgB,yBAAyB,CAAC,QAAQ,EAAE,MAAM,GAAG,eAAe,CAW3E"}
@@ -0,0 +1,29 @@
1
+ import { appendFileSync } from 'node:fs';
2
+ import { join } from 'node:path';
3
+ // Exported so telemetry/lockMetricsSummary.ts (D6, `mast metrics --locks`)
4
+ // reads the same sink file this module writes, without duplicating the
5
+ // literal filename.
6
+ export const LOCK_METRICS_FILENAME = 'lock-metrics.jsonl';
7
+ /**
8
+ * Default sink: appends one JSON line per event to `<stateDir>/lock-metrics.jsonl`.
9
+ *
10
+ * Synchronous and best-effort: lock acquire/release happens rarely relative
11
+ * to read-tool traffic (near-zero overhead on the hot path), and a write
12
+ * failure (e.g. read-only state dir) is swallowed rather than surfaced,
13
+ * matching the fire-and-forget discipline `telemetry/metrics.ts` already
14
+ * uses for tool-call metrics.
15
+ */
16
+ export function createFileLockMetricsSink(stateDir) {
17
+ const path = join(stateDir, LOCK_METRICS_FILENAME);
18
+ return {
19
+ record(event) {
20
+ try {
21
+ appendFileSync(path, JSON.stringify(event) + '\n');
22
+ }
23
+ catch {
24
+ // Best-effort — see module WHY-comment.
25
+ }
26
+ },
27
+ };
28
+ }
29
+ //# sourceMappingURL=lockMetrics.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"lockMetrics.js","sourceRoot":"","sources":["../../src/store/lockMetrics.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,cAAc,EAAE,MAAM,SAAS,CAAC;AACzC,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AAuEjC,2EAA2E;AAC3E,uEAAuE;AACvE,oBAAoB;AACpB,MAAM,CAAC,MAAM,qBAAqB,GAAG,oBAAoB,CAAC;AAE1D;;;;;;;;GAQG;AACH,MAAM,UAAU,yBAAyB,CAAC,QAAgB;IACxD,MAAM,IAAI,GAAG,IAAI,CAAC,QAAQ,EAAE,qBAAqB,CAAC,CAAC;IACnD,OAAO;QACL,MAAM,CAAC,KAAgB;YACrB,IAAI,CAAC;gBACH,cAAc,CAAC,IAAI,EAAE,IAAI,CAAC,SAAS,CAAC,KAAK,CAAC,GAAG,IAAI,CAAC,CAAC;YACrD,CAAC;YAAC,MAAM,CAAC;gBACP,wCAAwC;YAC1C,CAAC;QACH,CAAC;KACF,CAAC;AACJ,CAAC"}
@@ -0,0 +1,44 @@
1
+ import type { Db } from '../graph/db.js';
2
+ import type { Chunk } from '../ast/types.js';
3
+ export interface ChunkRecord {
4
+ chunk_id: string;
5
+ file_path: string;
6
+ start_line: number;
7
+ end_line: number;
8
+ content: string;
9
+ chunk_type: string;
10
+ symbol_name: string | null;
11
+ parent_symbol: string | null;
12
+ is_exported: boolean;
13
+ language: string;
14
+ file_mtime: number;
15
+ }
16
+ export interface ChunkStore {
17
+ /** Replace all chunks for `filePath` atomically. Returns rows removed. */
18
+ replaceChunksForFile(filePath: string, chunks: readonly Chunk[]): Promise<number>;
19
+ /** Delete all chunks for the given files. Returns total rows removed. */
20
+ deleteChunksForFiles(filePaths: readonly string[]): Promise<number>;
21
+ getChunksByFilePath(filePath: string): Promise<ChunkRecord[]>;
22
+ /** Unrecognised ids are silently skipped. */
23
+ getChunksByIds(ids: readonly string[]): Promise<ChunkRecord[]>;
24
+ getAllChunks(): Promise<ChunkRecord[]>;
25
+ chunkCount(): Promise<number>;
26
+ }
27
+ export declare class SqliteChunkStore implements ChunkStore {
28
+ private readonly db;
29
+ constructor(db: Db);
30
+ /**
31
+ * Used directly by `populateFile`'s default (no-override) path via a plain
32
+ * function reference, not this class — see `graph/populate.ts`'s
33
+ * `replaceChunksInline`. This method exists so `SqliteChunkStore` itself is
34
+ * a complete, independently-usable `ChunkStore` (tests construct it
35
+ * directly, and it is a valid `chunkStoreOverride`/`ChunkWriter` source).
36
+ */
37
+ replaceChunksForFile(filePath: string, chunks: readonly Chunk[]): Promise<number>;
38
+ deleteChunksForFiles(filePaths: readonly string[]): Promise<number>;
39
+ getChunksByFilePath(filePath: string): Promise<ChunkRecord[]>;
40
+ getChunksByIds(ids: readonly string[]): Promise<ChunkRecord[]>;
41
+ getAllChunks(): Promise<ChunkRecord[]>;
42
+ chunkCount(): Promise<number>;
43
+ }
44
+ //# sourceMappingURL=sqliteChunkStore.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"sqliteChunkStore.d.ts","sourceRoot":"","sources":["../../src/store/sqliteChunkStore.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,EAAE,EAAE,MAAM,gBAAgB,CAAC;AACzC,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,iBAAiB,CAAC;AA6B7C,MAAM,WAAW,WAAW;IAC1B,QAAQ,EAAE,MAAM,CAAC;IACjB,SAAS,EAAE,MAAM,CAAC;IAClB,UAAU,EAAE,MAAM,CAAC;IACnB,QAAQ,EAAE,MAAM,CAAC;IACjB,OAAO,EAAE,MAAM,CAAC;IAChB,UAAU,EAAE,MAAM,CAAC;IACnB,WAAW,EAAE,MAAM,GAAG,IAAI,CAAC;IAC3B,aAAa,EAAE,MAAM,GAAG,IAAI,CAAC;IAC7B,WAAW,EAAE,OAAO,CAAC;IACrB,QAAQ,EAAE,MAAM,CAAC;IACjB,UAAU,EAAE,MAAM,CAAC;CACpB;AAOD,MAAM,WAAW,UAAU;IACzB,0EAA0E;IAC1E,oBAAoB,CAAC,QAAQ,EAAE,MAAM,EAAE,MAAM,EAAE,SAAS,KAAK,EAAE,GAAG,OAAO,CAAC,MAAM,CAAC,CAAC;IAClF,yEAAyE;IACzE,oBAAoB,CAAC,SAAS,EAAE,SAAS,MAAM,EAAE,GAAG,OAAO,CAAC,MAAM,CAAC,CAAC;IACpE,mBAAmB,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,WAAW,EAAE,CAAC,CAAC;IAC9D,6CAA6C;IAC7C,cAAc,CAAC,GAAG,EAAE,SAAS,MAAM,EAAE,GAAG,OAAO,CAAC,WAAW,EAAE,CAAC,CAAC;IAC/D,YAAY,IAAI,OAAO,CAAC,WAAW,EAAE,CAAC,CAAC;IACvC,UAAU,IAAI,OAAO,CAAC,MAAM,CAAC,CAAC;CAC/B;AAED,qBAAa,gBAAiB,YAAW,UAAU;IACrC,OAAO,CAAC,QAAQ,CAAC,EAAE;gBAAF,EAAE,EAAE,EAAE;IAEnC;;;;;;OAMG;IACG,oBAAoB,CAAC,QAAQ,EAAE,MAAM,EAAE,MAAM,EAAE,SAAS,KAAK,EAAE,GAAG,OAAO,CAAC,MAAM,CAAC;IA2BjF,oBAAoB,CAAC,SAAS,EAAE,SAAS,MAAM,EAAE,GAAG,OAAO,CAAC,MAAM,CAAC;IAiBnE,mBAAmB,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,WAAW,EAAE,CAAC;IAS7D,cAAc,CAAC,GAAG,EAAE,SAAS,MAAM,EAAE,GAAG,OAAO,CAAC,WAAW,EAAE,CAAC;IAmB9D,YAAY,IAAI,OAAO,CAAC,WAAW,EAAE,CAAC;IAKtC,UAAU,IAAI,OAAO,CAAC,MAAM,CAAC;CAOpC"}
@@ -0,0 +1,135 @@
1
+ import { chunkRowsForSqlite } from '../graph/sqliteBatch.js';
2
+ export class SqliteChunkStore {
3
+ db;
4
+ constructor(db) {
5
+ this.db = db;
6
+ }
7
+ /**
8
+ * Used directly by `populateFile`'s default (no-override) path via a plain
9
+ * function reference, not this class — see `graph/populate.ts`'s
10
+ * `replaceChunksInline`. This method exists so `SqliteChunkStore` itself is
11
+ * a complete, independently-usable `ChunkStore` (tests construct it
12
+ * directly, and it is a valid `chunkStoreOverride`/`ChunkWriter` source).
13
+ */
14
+ async replaceChunksForFile(filePath, chunks) {
15
+ return this.db.transaction().execute(async (trx) => {
16
+ const row = await trx
17
+ .selectFrom('chunks')
18
+ .select((eb) => eb.fn.count('chunk_id').as('count'))
19
+ .where('file_path', '=', filePath)
20
+ .executeTakeFirst();
21
+ const removed = row?.count ?? 0;
22
+ await trx.deleteFrom('chunks').where('file_path', '=', filePath).execute();
23
+ if (chunks.length > 0) {
24
+ // Batched to stay under SQLite's 32,766 bound-parameter ceiling
25
+ // (Stage 4.5 S1, IMPLEMENTATION_PLAN.md) — an 11-column row caps a
26
+ // single unbatched INSERT at ~2,978 rows; a whale file's chunks
27
+ // otherwise throw `SqliteError: too many SQL variables` and roll
28
+ // back this whole transaction. Batching the STATEMENT, not the
29
+ // transaction, keeps every batch inside the same `trx`, so the
30
+ // file's chunks still land atomically (all-or-nothing) with each
31
+ // other — same as before, just as multiple INSERTs instead of one.
32
+ for (const batch of chunkRowsForSqlite(chunks.map(chunkToRow))) {
33
+ await trx.insertInto('chunks').values(batch).execute();
34
+ }
35
+ }
36
+ return removed;
37
+ });
38
+ }
39
+ async deleteChunksForFiles(filePaths) {
40
+ if (filePaths.length === 0)
41
+ return 0;
42
+ return this.db.transaction().execute(async (trx) => {
43
+ let removed = 0;
44
+ for (const fp of filePaths) {
45
+ const row = await trx
46
+ .selectFrom('chunks')
47
+ .select((eb) => eb.fn.count('chunk_id').as('count'))
48
+ .where('file_path', '=', fp)
49
+ .executeTakeFirst();
50
+ removed += row?.count ?? 0;
51
+ await trx.deleteFrom('chunks').where('file_path', '=', fp).execute();
52
+ }
53
+ return removed;
54
+ });
55
+ }
56
+ async getChunksByFilePath(filePath) {
57
+ const rows = await this.db
58
+ .selectFrom('chunks')
59
+ .selectAll()
60
+ .where('file_path', '=', filePath)
61
+ .execute();
62
+ return rows.map(rowToChunkRecord);
63
+ }
64
+ async getChunksByIds(ids) {
65
+ if (ids.length === 0)
66
+ return [];
67
+ // Deliberately unbatched, and the bound is re-derived here rather than
68
+ // recalled: the largest id set any call site passes is 200 — `fused.ts`'s
69
+ // `candidateLimit = limit * 4` with `limit` capped at 50 by
70
+ // `mcp/tools/search.ts`'s schema — and `potential-matches.ts` passes at
71
+ // most its own limit of 50. That is well under SQLite's bound-parameter
72
+ // ceiling (`SQLITE_MAX_VARIABLES` = 32,766, `graph/sqliteBatch.ts`), so no
73
+ // batching is needed. This comment read "50" until 2026-08-20, which was
74
+ // the pre-`candidateLimit` figure; the conclusion was unaffected, but the
75
+ // number is what a future reader would check the ceiling against.
76
+ const rows = await this.db
77
+ .selectFrom('chunks')
78
+ .selectAll()
79
+ .where('chunk_id', 'in', ids)
80
+ .execute();
81
+ return rows.map(rowToChunkRecord);
82
+ }
83
+ async getAllChunks() {
84
+ const rows = await this.db.selectFrom('chunks').selectAll().execute();
85
+ return rows.map(rowToChunkRecord);
86
+ }
87
+ async chunkCount() {
88
+ const row = await this.db
89
+ .selectFrom('chunks')
90
+ .select((eb) => eb.fn.count('chunk_id').as('count'))
91
+ .executeTakeFirst();
92
+ return row?.count ?? 0;
93
+ }
94
+ }
95
+ // ---------------------------------------------------------------------------
96
+ // Row <-> domain conversions
97
+ // ---------------------------------------------------------------------------
98
+ function chunkToRow(c) {
99
+ return {
100
+ chunk_id: c.chunk_id,
101
+ file_path: c.file_path,
102
+ start_line: c.start_line,
103
+ end_line: c.end_line,
104
+ content: c.content,
105
+ chunk_type: c.chunk_type,
106
+ symbol_name: c.symbol_name,
107
+ parent_symbol: c.parent_symbol,
108
+ is_exported: c.is_exported ? 1 : 0,
109
+ language: c.language,
110
+ file_mtime: c.file_mtime,
111
+ };
112
+ }
113
+ /**
114
+ * Explicit `0|1` -> `boolean` conversion at the store boundary. SQLite stores
115
+ * booleans as INTEGER (`BoolCol` convention, graph/db.ts) — every read here
116
+ * converts back to `boolean` explicitly so `SearchResult.is_exported`
117
+ * (search/hybrid.ts) and every MCP tool response never silently ships a
118
+ * number where a client expects `true`/`false`.
119
+ */
120
+ function rowToChunkRecord(r) {
121
+ return {
122
+ chunk_id: r.chunk_id,
123
+ file_path: r.file_path,
124
+ start_line: r.start_line,
125
+ end_line: r.end_line,
126
+ content: r.content,
127
+ chunk_type: r.chunk_type,
128
+ symbol_name: r.symbol_name,
129
+ parent_symbol: r.parent_symbol,
130
+ is_exported: r.is_exported === 1,
131
+ language: r.language,
132
+ file_mtime: r.file_mtime,
133
+ };
134
+ }
135
+ //# sourceMappingURL=sqliteChunkStore.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"sqliteChunkStore.js","sourceRoot":"","sources":["../../src/store/sqliteChunkStore.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,42 @@
1
+ /** Per-caller lock timing summary. */
2
+ export interface LockCallerSummary {
3
+ readonly caller: string;
4
+ /** Number of completed acquire-then-release cycles observed for this caller. */
5
+ readonly count: number;
6
+ readonly hold_p50_ms: number;
7
+ readonly hold_p95_ms: number;
8
+ readonly hold_max_ms: number;
9
+ readonly wait_p50_ms: number;
10
+ readonly wait_p95_ms: number;
11
+ readonly wait_max_ms: number;
12
+ /** Acquisition attempts that exhausted retries without ever acquiring the lock. */
13
+ readonly failed_count: number;
14
+ }
15
+ export interface LockMetricsSummary {
16
+ readonly callers: readonly LockCallerSummary[];
17
+ /** Lines that failed JSON.parse or did not match the LockEvent schema — skipped, not thrown. */
18
+ readonly malformed_line_count: number;
19
+ }
20
+ /**
21
+ * Summarize raw `lock-metrics.jsonl` content into per-caller count/p50/p95/max
22
+ * for both hold duration (`released` events) and wait/acquire duration
23
+ * (`acquired` events), plus a failed-acquisition count (`failed` events).
24
+ *
25
+ * Pure — takes the file's text directly rather than a path, so it is
26
+ * unit-testable against fixture strings without touching the filesystem.
27
+ * Malformed lines (bad JSON, or JSON that doesn't match a known `LockEvent`
28
+ * shape) are skipped and counted rather than thrown, matching
29
+ * store/lockMetrics.ts's own best-effort discipline for this data.
30
+ */
31
+ export declare function summarizeLockMetricsJsonl(jsonlContent: string): LockMetricsSummary;
32
+ /**
33
+ * Read and summarize `<stateDir>/lock-metrics.jsonl`.
34
+ *
35
+ * Returns `null` when the file is missing or empty — the "no lock metrics
36
+ * recorded" case `mast metrics --locks` reports distinctly from a
37
+ * zero-caller summary, so callers can tell "never ran a coarse write" apart
38
+ * from "ran, but every event was malformed" (the latter still returns a
39
+ * summary object, with `malformed_line_count > 0` and `callers: []`).
40
+ */
41
+ export declare function readLockMetricsSummary(stateDir: string): LockMetricsSummary | null;
42
+ //# sourceMappingURL=lockMetricsSummary.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"lockMetricsSummary.d.ts","sourceRoot":"","sources":["../../src/telemetry/lockMetricsSummary.ts"],"names":[],"mappings":"AAgDA,sCAAsC;AACtC,MAAM,WAAW,iBAAiB;IAChC,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,gFAAgF;IAChF,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAC;IAC7B,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAC;IAC7B,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAC;IAC7B,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAC;IAC7B,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAC;IAC7B,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAC;IAC7B,mFAAmF;IACnF,QAAQ,CAAC,YAAY,EAAE,MAAM,CAAC;CAC/B;AAED,MAAM,WAAW,kBAAkB;IACjC,QAAQ,CAAC,OAAO,EAAE,SAAS,iBAAiB,EAAE,CAAC;IAC/C,gGAAgG;IAChG,QAAQ,CAAC,oBAAoB,EAAE,MAAM,CAAC;CACvC;AAED;;;;;;;;;;GAUG;AACH,wBAAgB,yBAAyB,CAAC,YAAY,EAAE,MAAM,GAAG,kBAAkB,CAqElF;AAED;;;;;;;;GAQG;AACH,wBAAgB,sBAAsB,CAAC,QAAQ,EAAE,MAAM,GAAG,kBAAkB,GAAG,IAAI,CAQlF"}