@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.
- package/LICENSE +21 -0
- package/MAST_SPEC.md +2926 -0
- package/README.md +842 -0
- package/assets/skill.md +45 -0
- package/dist/ast/extract.d.ts +65 -0
- package/dist/ast/extract.d.ts.map +1 -0
- package/dist/ast/extract.js +147 -0
- package/dist/ast/extract.js.map +1 -0
- package/dist/ast/extractor.d.ts +85 -0
- package/dist/ast/extractor.d.ts.map +1 -0
- package/dist/ast/extractor.js +2 -0
- package/dist/ast/extractor.js.map +1 -0
- package/dist/ast/extractors/markdown.d.ts +40 -0
- package/dist/ast/extractors/markdown.d.ts.map +1 -0
- package/dist/ast/extractors/markdown.js +175 -0
- package/dist/ast/extractors/markdown.js.map +1 -0
- package/dist/ast/extractors/typescript.d.ts +179 -0
- package/dist/ast/extractors/typescript.d.ts.map +1 -0
- package/dist/ast/extractors/typescript.js +1439 -0
- package/dist/ast/extractors/typescript.js.map +1 -0
- package/dist/ast/parser.d.ts +5 -0
- package/dist/ast/parser.d.ts.map +1 -0
- package/dist/ast/parser.js +21 -0
- package/dist/ast/parser.js.map +1 -0
- package/dist/ast/types.d.ts +586 -0
- package/dist/ast/types.d.ts.map +1 -0
- package/dist/ast/types.js +3 -0
- package/dist/ast/types.js.map +1 -0
- package/dist/cli/docs-cmd.d.ts +22 -0
- package/dist/cli/docs-cmd.d.ts.map +1 -0
- package/dist/cli/docs-cmd.js +100 -0
- package/dist/cli/docs-cmd.js.map +1 -0
- package/dist/cli/index-cmd.d.ts +72 -0
- package/dist/cli/index-cmd.d.ts.map +1 -0
- package/dist/cli/index-cmd.js +263 -0
- package/dist/cli/index-cmd.js.map +1 -0
- package/dist/cli/index.d.ts +3 -0
- package/dist/cli/index.d.ts.map +1 -0
- package/dist/cli/index.js +4 -0
- package/dist/cli/index.js.map +1 -0
- package/dist/cli/init.d.ts +14 -0
- package/dist/cli/init.d.ts.map +1 -0
- package/dist/cli/init.js +64 -0
- package/dist/cli/init.js.map +1 -0
- package/dist/cli/install-hooks.d.ts +3 -0
- package/dist/cli/install-hooks.d.ts.map +1 -0
- package/dist/cli/install-hooks.js +34 -0
- package/dist/cli/install-hooks.js.map +1 -0
- package/dist/cli/metrics-cmd.d.ts +3 -0
- package/dist/cli/metrics-cmd.d.ts.map +1 -0
- package/dist/cli/metrics-cmd.js +164 -0
- package/dist/cli/metrics-cmd.js.map +1 -0
- package/dist/cli/program.d.ts +14 -0
- package/dist/cli/program.d.ts.map +1 -0
- package/dist/cli/program.js +44 -0
- package/dist/cli/program.js.map +1 -0
- package/dist/cli/query.d.ts +46 -0
- package/dist/cli/query.d.ts.map +1 -0
- package/dist/cli/query.js +153 -0
- package/dist/cli/query.js.map +1 -0
- package/dist/cli/search-cmd.d.ts +33 -0
- package/dist/cli/search-cmd.d.ts.map +1 -0
- package/dist/cli/search-cmd.js +114 -0
- package/dist/cli/search-cmd.js.map +1 -0
- package/dist/cli/serve.d.ts +3 -0
- package/dist/cli/serve.d.ts.map +1 -0
- package/dist/cli/serve.js +19 -0
- package/dist/cli/serve.js.map +1 -0
- package/dist/cli/skill-install.d.ts +39 -0
- package/dist/cli/skill-install.d.ts.map +1 -0
- package/dist/cli/skill-install.js +57 -0
- package/dist/cli/skill-install.js.map +1 -0
- package/dist/cli/status.d.ts +35 -0
- package/dist/cli/status.d.ts.map +1 -0
- package/dist/cli/status.js +117 -0
- package/dist/cli/status.js.map +1 -0
- package/dist/cli/upgrade-cmd.d.ts +48 -0
- package/dist/cli/upgrade-cmd.d.ts.map +1 -0
- package/dist/cli/upgrade-cmd.js +129 -0
- package/dist/cli/upgrade-cmd.js.map +1 -0
- package/dist/cli/version.d.ts +3 -0
- package/dist/cli/version.d.ts.map +1 -0
- package/dist/cli/version.js +19 -0
- package/dist/cli/version.js.map +1 -0
- package/dist/env.d.ts +17 -0
- package/dist/env.d.ts.map +1 -0
- package/dist/env.js +17 -0
- package/dist/env.js.map +1 -0
- package/dist/graph/checker-resolver.d.ts +156 -0
- package/dist/graph/checker-resolver.d.ts.map +1 -0
- package/dist/graph/checker-resolver.js +458 -0
- package/dist/graph/checker-resolver.js.map +1 -0
- package/dist/graph/db.d.ts +318 -0
- package/dist/graph/db.d.ts.map +1 -0
- package/dist/graph/db.js +266 -0
- package/dist/graph/db.js.map +1 -0
- package/dist/graph/index.d.ts +4 -0
- package/dist/graph/index.d.ts.map +1 -0
- package/dist/graph/index.js +5 -0
- package/dist/graph/index.js.map +1 -0
- package/dist/graph/local-type-env.d.ts +45 -0
- package/dist/graph/local-type-env.d.ts.map +1 -0
- package/dist/graph/local-type-env.js +59 -0
- package/dist/graph/local-type-env.js.map +1 -0
- package/dist/graph/path-range.d.ts +40 -0
- package/dist/graph/path-range.d.ts.map +1 -0
- package/dist/graph/path-range.js +42 -0
- package/dist/graph/path-range.js.map +1 -0
- package/dist/graph/populate.d.ts +263 -0
- package/dist/graph/populate.d.ts.map +1 -0
- package/dist/graph/populate.js +984 -0
- package/dist/graph/populate.js.map +1 -0
- package/dist/graph/queries.d.ts +114 -0
- package/dist/graph/queries.d.ts.map +1 -0
- package/dist/graph/queries.js +415 -0
- package/dist/graph/queries.js.map +1 -0
- package/dist/graph/sqliteBatch.d.ts +48 -0
- package/dist/graph/sqliteBatch.d.ts.map +1 -0
- package/dist/graph/sqliteBatch.js +93 -0
- package/dist/graph/sqliteBatch.js.map +1 -0
- package/dist/indexer/freshness.d.ts +48 -0
- package/dist/indexer/freshness.d.ts.map +1 -0
- package/dist/indexer/freshness.js +72 -0
- package/dist/indexer/freshness.js.map +1 -0
- package/dist/indexer/import-resolver.d.ts +50 -0
- package/dist/indexer/import-resolver.d.ts.map +1 -0
- package/dist/indexer/import-resolver.js +318 -0
- package/dist/indexer/import-resolver.js.map +1 -0
- package/dist/indexer/index.d.ts +249 -0
- package/dist/indexer/index.d.ts.map +1 -0
- package/dist/indexer/index.js +560 -0
- package/dist/indexer/index.js.map +1 -0
- package/dist/indexer/walker.d.ts +60 -0
- package/dist/indexer/walker.d.ts.map +1 -0
- package/dist/indexer/walker.js +128 -0
- package/dist/indexer/walker.js.map +1 -0
- package/dist/indexer/watcher.d.ts +80 -0
- package/dist/indexer/watcher.d.ts.map +1 -0
- package/dist/indexer/watcher.js +151 -0
- package/dist/indexer/watcher.js.map +1 -0
- package/dist/mcp/context.d.ts +23 -0
- package/dist/mcp/context.d.ts.map +1 -0
- package/dist/mcp/context.js +2 -0
- package/dist/mcp/context.js.map +1 -0
- package/dist/mcp/register-tools.d.ts +21 -0
- package/dist/mcp/register-tools.d.ts.map +1 -0
- package/dist/mcp/register-tools.js +42 -0
- package/dist/mcp/register-tools.js.map +1 -0
- package/dist/mcp/server.d.ts +58 -0
- package/dist/mcp/server.d.ts.map +1 -0
- package/dist/mcp/server.js +177 -0
- package/dist/mcp/server.js.map +1 -0
- package/dist/mcp/staleness.d.ts +72 -0
- package/dist/mcp/staleness.d.ts.map +1 -0
- package/dist/mcp/staleness.js +213 -0
- package/dist/mcp/staleness.js.map +1 -0
- package/dist/mcp/startup.d.ts +40 -0
- package/dist/mcp/startup.d.ts.map +1 -0
- package/dist/mcp/startup.js +114 -0
- package/dist/mcp/startup.js.map +1 -0
- package/dist/mcp/tools/_helpers.d.ts +26 -0
- package/dist/mcp/tools/_helpers.d.ts.map +1 -0
- package/dist/mcp/tools/_helpers.js +54 -0
- package/dist/mcp/tools/_helpers.js.map +1 -0
- package/dist/mcp/tools/callers.d.ts +4 -0
- package/dist/mcp/tools/callers.d.ts.map +1 -0
- package/dist/mcp/tools/callers.js +112 -0
- package/dist/mcp/tools/callers.js.map +1 -0
- package/dist/mcp/tools/dependencies.d.ts +4 -0
- package/dist/mcp/tools/dependencies.d.ts.map +1 -0
- package/dist/mcp/tools/dependencies.js +36 -0
- package/dist/mcp/tools/dependencies.js.map +1 -0
- package/dist/mcp/tools/efficiency.d.ts +4 -0
- package/dist/mcp/tools/efficiency.d.ts.map +1 -0
- package/dist/mcp/tools/efficiency.js +39 -0
- package/dist/mcp/tools/efficiency.js.map +1 -0
- package/dist/mcp/tools/exports.d.ts +4 -0
- package/dist/mcp/tools/exports.d.ts.map +1 -0
- package/dist/mcp/tools/exports.js +56 -0
- package/dist/mcp/tools/exports.js.map +1 -0
- package/dist/mcp/tools/implementors.d.ts +4 -0
- package/dist/mcp/tools/implementors.d.ts.map +1 -0
- package/dist/mcp/tools/implementors.js +47 -0
- package/dist/mcp/tools/implementors.js.map +1 -0
- package/dist/mcp/tools/project-skeleton.d.ts +4 -0
- package/dist/mcp/tools/project-skeleton.d.ts.map +1 -0
- package/dist/mcp/tools/project-skeleton.js +61 -0
- package/dist/mcp/tools/project-skeleton.js.map +1 -0
- package/dist/mcp/tools/reindex.d.ts +14 -0
- package/dist/mcp/tools/reindex.d.ts.map +1 -0
- package/dist/mcp/tools/reindex.js +32 -0
- package/dist/mcp/tools/reindex.js.map +1 -0
- package/dist/mcp/tools/rename-impact.d.ts +4 -0
- package/dist/mcp/tools/rename-impact.d.ts.map +1 -0
- package/dist/mcp/tools/rename-impact.js +124 -0
- package/dist/mcp/tools/rename-impact.js.map +1 -0
- package/dist/mcp/tools/search.d.ts +4 -0
- package/dist/mcp/tools/search.d.ts.map +1 -0
- package/dist/mcp/tools/search.js +69 -0
- package/dist/mcp/tools/search.js.map +1 -0
- package/dist/mcp/tools/signature.d.ts +4 -0
- package/dist/mcp/tools/signature.d.ts.map +1 -0
- package/dist/mcp/tools/signature.js +127 -0
- package/dist/mcp/tools/signature.js.map +1 -0
- package/dist/mcp/tools/status.d.ts +4 -0
- package/dist/mcp/tools/status.d.ts.map +1 -0
- package/dist/mcp/tools/status.js +30 -0
- package/dist/mcp/tools/status.js.map +1 -0
- package/dist/search/declex.d.ts +109 -0
- package/dist/search/declex.d.ts.map +1 -0
- package/dist/search/declex.js +242 -0
- package/dist/search/declex.js.map +1 -0
- package/dist/search/fts.d.ts +77 -0
- package/dist/search/fts.d.ts.map +1 -0
- package/dist/search/fts.js +223 -0
- package/dist/search/fts.js.map +1 -0
- package/dist/search/fused.d.ts +110 -0
- package/dist/search/fused.d.ts.map +1 -0
- package/dist/search/fused.js +323 -0
- package/dist/search/fused.js.map +1 -0
- package/dist/search/potential-matches.d.ts +88 -0
- package/dist/search/potential-matches.d.ts.map +1 -0
- package/dist/search/potential-matches.js +83 -0
- package/dist/search/potential-matches.js.map +1 -0
- package/dist/search/scope.d.ts +66 -0
- package/dist/search/scope.d.ts.map +1 -0
- package/dist/search/scope.js +71 -0
- package/dist/search/scope.js.map +1 -0
- package/dist/store/config.d.ts +57 -0
- package/dist/store/config.d.ts.map +1 -0
- package/dist/store/config.js +208 -0
- package/dist/store/config.js.map +1 -0
- package/dist/store/lock.d.ts +101 -0
- package/dist/store/lock.d.ts.map +1 -0
- package/dist/store/lock.js +182 -0
- package/dist/store/lock.js.map +1 -0
- package/dist/store/lockMetrics.d.ts +52 -0
- package/dist/store/lockMetrics.d.ts.map +1 -0
- package/dist/store/lockMetrics.js +29 -0
- package/dist/store/lockMetrics.js.map +1 -0
- package/dist/store/sqliteChunkStore.d.ts +44 -0
- package/dist/store/sqliteChunkStore.d.ts.map +1 -0
- package/dist/store/sqliteChunkStore.js +135 -0
- package/dist/store/sqliteChunkStore.js.map +1 -0
- package/dist/telemetry/lockMetricsSummary.d.ts +42 -0
- package/dist/telemetry/lockMetricsSummary.d.ts.map +1 -0
- package/dist/telemetry/lockMetricsSummary.js +139 -0
- package/dist/telemetry/lockMetricsSummary.js.map +1 -0
- package/dist/telemetry/metrics.d.ts +140 -0
- package/dist/telemetry/metrics.d.ts.map +1 -0
- package/dist/telemetry/metrics.js +312 -0
- package/dist/telemetry/metrics.js.map +1 -0
- package/dist/telemetry/tokenizer.d.ts +125 -0
- package/dist/telemetry/tokenizer.d.ts.map +1 -0
- package/dist/telemetry/tokenizer.js +174 -0
- package/dist/telemetry/tokenizer.js.map +1 -0
- 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":"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"}
|
|
@@ -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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"}
|