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