@monoes/monograph 1.6.0 → 1.6.2
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/dist/src/cache/extraction-cache.d.ts +3 -1
- package/dist/src/cache/extraction-cache.d.ts.map +1 -1
- package/dist/src/cache/extraction-cache.js +10 -2
- package/dist/src/cache/extraction-cache.js.map +1 -1
- package/dist/src/graph/explain.d.ts.map +1 -1
- package/dist/src/graph/explain.js +9 -2
- package/dist/src/graph/explain.js.map +1 -1
- package/dist/src/graph/pagerank.d.ts +9 -3
- package/dist/src/graph/pagerank.d.ts.map +1 -1
- package/dist/src/graph/pagerank.js +69 -34
- package/dist/src/graph/pagerank.js.map +1 -1
- package/dist/src/mcp-tools/neighbors.d.ts +29 -3
- package/dist/src/mcp-tools/neighbors.d.ts.map +1 -1
- package/dist/src/mcp-tools/neighbors.js +85 -10
- package/dist/src/mcp-tools/neighbors.js.map +1 -1
- package/dist/src/mcp-tools/query.d.ts +8 -0
- package/dist/src/mcp-tools/query.d.ts.map +1 -1
- package/dist/src/mcp-tools/query.js +15 -9
- package/dist/src/mcp-tools/query.js.map +1 -1
- package/dist/src/mcp-tools/rename.d.ts.map +1 -1
- package/dist/src/mcp-tools/rename.js +27 -2
- package/dist/src/mcp-tools/rename.js.map +1 -1
- package/dist/src/parsers/cpp.d.ts.map +1 -1
- package/dist/src/parsers/cpp.js +12 -2
- package/dist/src/parsers/cpp.js.map +1 -1
- package/dist/src/parsers/dart.d.ts.map +1 -1
- package/dist/src/parsers/dart.js +18 -0
- package/dist/src/parsers/dart.js.map +1 -1
- package/dist/src/parsers/extractor.d.ts.map +1 -1
- package/dist/src/parsers/extractor.js +22 -8
- package/dist/src/parsers/extractor.js.map +1 -1
- package/dist/src/parsers/kotlin.d.ts +1 -1
- package/dist/src/parsers/kotlin.d.ts.map +1 -1
- package/dist/src/parsers/kotlin.js +4 -0
- package/dist/src/parsers/kotlin.js.map +1 -1
- package/dist/src/parsers/language-config.d.ts +7 -0
- package/dist/src/parsers/language-config.d.ts.map +1 -1
- package/dist/src/parsers/language-config.js +14 -1
- package/dist/src/parsers/language-config.js.map +1 -1
- package/dist/src/parsers/loader.js +17 -3
- package/dist/src/parsers/loader.js.map +1 -1
- package/dist/src/parsers/ruby.d.ts +1 -1
- package/dist/src/parsers/ruby.d.ts.map +1 -1
- package/dist/src/parsers/ruby.js +4 -0
- package/dist/src/parsers/ruby.js.map +1 -1
- package/dist/src/pipeline/index-scope.d.ts +23 -0
- package/dist/src/pipeline/index-scope.d.ts.map +1 -0
- package/dist/src/pipeline/index-scope.js +33 -0
- package/dist/src/pipeline/index-scope.js.map +1 -0
- package/dist/src/pipeline/orchestrator.d.ts +17 -0
- package/dist/src/pipeline/orchestrator.d.ts.map +1 -1
- package/dist/src/pipeline/orchestrator.js +55 -122
- package/dist/src/pipeline/orchestrator.js.map +1 -1
- package/dist/src/pipeline/phases/communities.d.ts +78 -2
- package/dist/src/pipeline/phases/communities.d.ts.map +1 -1
- package/dist/src/pipeline/phases/communities.js +141 -9
- package/dist/src/pipeline/phases/communities.js.map +1 -1
- package/dist/src/pipeline/phases/markdown.js +6 -6
- package/dist/src/pipeline/phases/markdown.js.map +1 -1
- package/dist/src/pipeline/phases/parse.d.ts.map +1 -1
- package/dist/src/pipeline/phases/parse.js +22 -27
- package/dist/src/pipeline/phases/parse.js.map +1 -1
- package/dist/src/pipeline/phases/pdf-parse.js +5 -5
- package/dist/src/pipeline/phases/pdf-parse.js.map +1 -1
- package/dist/src/pipeline/phases/scan.d.ts.map +1 -1
- package/dist/src/pipeline/phases/scan.js +16 -1
- package/dist/src/pipeline/phases/scan.js.map +1 -1
- package/dist/src/pipeline/phases/structure.js +12 -12
- package/dist/src/pipeline/phases/structure.js.map +1 -1
- package/dist/src/pipeline/phases/variables.d.ts.map +1 -1
- package/dist/src/pipeline/phases/variables.js +2 -1
- package/dist/src/pipeline/phases/variables.js.map +1 -1
- package/dist/src/search/hybrid-query.d.ts +77 -14
- package/dist/src/search/hybrid-query.d.ts.map +1 -1
- package/dist/src/search/hybrid-query.js +81 -21
- package/dist/src/search/hybrid-query.js.map +1 -1
- package/dist/src/staleness/git-staleness.d.ts +46 -0
- package/dist/src/staleness/git-staleness.d.ts.map +1 -1
- package/dist/src/staleness/git-staleness.js +188 -24
- package/dist/src/staleness/git-staleness.js.map +1 -1
- package/dist/src/storage/fts-store.d.ts +14 -0
- package/dist/src/storage/fts-store.d.ts.map +1 -1
- package/dist/src/storage/fts-store.js +18 -3
- package/dist/src/storage/fts-store.js.map +1 -1
- package/dist/src/types.d.ts +58 -0
- package/dist/src/types.d.ts.map +1 -1
- package/dist/src/types.js +103 -1
- package/dist/src/types.js.map +1 -1
- package/dist/src/web/api.d.ts +3 -0
- package/dist/src/web/api.d.ts.map +1 -1
- package/dist/src/web/api.js +2 -0
- package/dist/src/web/api.js.map +1 -1
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/package.json +1 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"structure.js","sourceRoot":"","sources":["../../../../src/pipeline/phases/structure.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,OAAO,EAAE,QAAQ,EAAE,MAAM,WAAW,CAAC;AAExD,OAAO,EAAE,gBAAgB,EAAE,MAAM,EAAE,WAAW,EAAE,MAAM,gBAAgB,CAAC;
|
|
1
|
+
{"version":3,"file":"structure.js","sourceRoot":"","sources":["../../../../src/pipeline/phases/structure.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,OAAO,EAAE,QAAQ,EAAE,MAAM,WAAW,CAAC;AAExD,OAAO,EAAE,gBAAgB,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,EAAE,WAAW,EAAE,MAAM,gBAAgB,CAAC;AAUzF,MAAM,CAAC,MAAM,cAAc,GAAmC;IAC5D,IAAI,EAAE,WAAW;IACjB,IAAI,EAAE,CAAC,MAAM,CAAC;IACd,KAAK,CAAC,OAAO,CAAC,GAAG,EAAE,IAAI;QACrB,MAAM,EAAE,SAAS,EAAE,GAAG,IAAI,CAAC,GAAG,CAAC,MAAM,CAAe,CAAC;QACrD,MAAM,SAAS,GAAoB,EAAE,CAAC;QACtC,MAAM,WAAW,GAAoB,EAAE,CAAC;QACxC,MAAM,aAAa,GAAoB,EAAE,CAAC;QAC1C,MAAM,WAAW,GAAG,IAAI,GAAG,EAAU,CAAC;QAEtC,KAAK,MAAM,OAAO,IAAI,SAAS,EAAE,CAAC;YAChC,MAAM,GAAG,GAAG,QAAQ,CAAC,GAAG,CAAC,QAAQ,EAAE,OAAO,CAAC,CAAC;YAC5C,MAAM,SAAS,GAAG,MAAM,CAAC,GAAG,CAAC,CAAC;YAC9B,SAAS,CAAC,IAAI,CAAC;gBACb,EAAE,EAAE,SAAS;gBACb,KAAK,EAAE,MAAM;gBACb,IAAI,EAAE,QAAQ,CAAC,GAAG,CAAC;gBACnB,SAAS,EAAE,WAAW,CAAC,QAAQ,CAAC,GAAG,CAAC,CAAC;gBACrC,QAAQ,EAAE,GAAG;gBACb,UAAU,EAAE,KAAK;aAClB,CAAC,CAAC;YAEH,MAAM,KAAK,GAAG,OAAO,CAAC,GAAG,CAAC,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC;YACtC,IAAI,OAAO,GAAG,EAAE,CAAC;YACjB,IAAI,cAAc,GAAkB,IAAI,CAAC;YACzC,KAAK,MAAM,IAAI,IAAI,KAAK,EAAE,CAAC;gBACzB,IAAI,IAAI,KAAK,GAAG;oBAAE,SAAS;gBAC3B,OAAO,GAAG,OAAO,CAAC,CAAC,CAAC,GAAG,OAAO,IAAI,IAAI,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC;gBAChD,MAAM,eAAe,GAAG,QAAQ,CAAC,OAAO,CAAC,CAAC;gBAC1C,IAAI,CAAC,WAAW,CAAC,GAAG,CAAC,eAAe,CAAC,EAAE,CAAC;oBACtC,WAAW,CAAC,GAAG,CAAC,eAAe,CAAC,CAAC;oBACjC,WAAW,CAAC,IAAI,CAAC;wBACf,EAAE,EAAE,eAAe;wBACnB,KAAK,EAAE,QAAQ;wBACf,IAAI,EAAE,IAAI;wBACV,SAAS,EAAE,WAAW,CAAC,IAAI,CAAC;wBAC5B,QAAQ,EAAE,OAAO;wBACjB,UAAU,EAAE,KAAK;qBAClB,CAAC,CAAC;oBACH,IAAI,cAAc,EAAE,CAAC;wBACnB,aAAa,CAAC,IAAI,CAAC;4BACjB,EAAE,EAAE,MAAM,CAAC,cAAc,EAAE,eAAe,EAAE,UAAU,CAAC;4BACvD,QAAQ,EAAE,cAAc;4BACxB,QAAQ,EAAE,eAAe;4BACzB,QAAQ,EAAE,UAAU;4BACpB,UAAU,EAAE,WAAW;4BACvB,eAAe,EAAE,gBAAgB,CAAC,SAAS;yBAC5C,CAAC,CAAC;oBACL,CAAC;gBACH,CAAC;gBACD,cAAc,GAAG,eAAe,CAAC;YACnC,CAAC;YAED,IAAI,cAAc,EAAE,CAAC;gBACnB,aAAa,CAAC,IAAI,CAAC;oBACjB,EAAE,EAAE,MAAM,CAAC,cAAc,EAAE,SAAS,EAAE,UAAU,CAAC;oBACjD,QAAQ,EAAE,cAAc;oBACxB,QAAQ,EAAE,SAAS;oBACnB,QAAQ,EAAE,UAAU;oBACpB,UAAU,EAAE,WAAW;oBACvB,eAAe,EAAE,gBAAgB,CAAC,SAAS;iBAC5C,CAAC,CAAC;YACL,CAAC;QACH,CAAC;QAED,OAAO,EAAE,SAAS,EAAE,WAAW,EAAE,aAAa,EAAE,CAAC;IACnD,CAAC;CACF,CAAC"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"variables.d.ts","sourceRoot":"","sources":["../../../../src/pipeline/phases/variables.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,gBAAgB,CAAC;
|
|
1
|
+
{"version":3,"file":"variables.d.ts","sourceRoot":"","sources":["../../../../src/pipeline/phases/variables.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,gBAAgB,CAAC;AAGpD,MAAM,WAAW,YAAY;IAC3B,IAAI,EAAE,MAAM,CAAC;IACb,UAAU,EAAE,OAAO,CAAC;IACpB,IAAI,EAAE,MAAM,CAAC;IACb,QAAQ,EAAE,MAAM,CAAC;CAClB;AAqBD,wBAAgB,gBAAgB,CAAC,MAAM,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,YAAY,EAAE,CAyBjF;AAED,wBAAgB,cAAc,CAAC,CAAC,EAAE,YAAY,GAAG,aAAa,CAW7D"}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { symbolId } from '../../types.js';
|
|
1
2
|
// Regex for top-level variable declarations.
|
|
2
3
|
// We detect nesting depth by counting braces before the match.
|
|
3
4
|
const TOP_LEVEL_VAR = /^(export\s+)?(const|let|var)\s+(\w+)\s*[=:]/gm;
|
|
@@ -42,7 +43,7 @@ export function extractVariables(source, filePath) {
|
|
|
42
43
|
}
|
|
43
44
|
export function variableToNode(v) {
|
|
44
45
|
return {
|
|
45
|
-
id:
|
|
46
|
+
id: symbolId({ filePath: v.filePath, scope: [], name: v.name, kind: 'Variable' }),
|
|
46
47
|
label: 'Variable',
|
|
47
48
|
name: v.name,
|
|
48
49
|
normLabel: v.name.toLowerCase(),
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"variables.js","sourceRoot":"","sources":["../../../../src/pipeline/phases/variables.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"variables.js","sourceRoot":"","sources":["../../../../src/pipeline/phases/variables.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,QAAQ,EAAE,MAAM,gBAAgB,CAAC;AAS1C,6CAA6C;AAC7C,+DAA+D;AAC/D,MAAM,aAAa,GAAG,+CAA+C,CAAC;AAEtE;;;GAGG;AACH,SAAS,kBAAkB,CAAC,GAAW;IACrC,8EAA8E;IAC9E,mFAAmF;IACnF,OAAO,GAAG;SACP,OAAO,CAAC,gCAAgC,EAAE,CAAC,CAAC,EAAE,EAAE,CAAC,GAAG,CAAC,MAAM,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC;SACtE,OAAO,CAAC,oBAAoB,EAAE,CAAC,CAAC,EAAE,EAAE,CAAC,GAAG,CAAC,MAAM,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC;SAC1D,OAAO,CAAC,oBAAoB,EAAE,CAAC,CAAC,EAAE,EAAE,CAAC,GAAG,CAAC,MAAM,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC;SAC1D,OAAO,CAAC,mBAAmB,EAAE,CAAC,CAAC,EAAE,EAAE,CAAC,GAAG,CAAC,MAAM,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC;SACzD,OAAO,CAAC,aAAa,EAAE,CAAC,CAAC,EAAE,EAAE,CAAC,GAAG,CAAC,MAAM,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC;AACzD,CAAC;AAED,MAAM,UAAU,gBAAgB,CAAC,MAAc,EAAE,QAAgB;IAC/D,MAAM,OAAO,GAAmB,EAAE,CAAC;IACnC,IAAI,KAA6B,CAAC;IAClC,aAAa,CAAC,SAAS,GAAG,CAAC,CAAC;IAE5B,8EAA8E;IAC9E,MAAM,QAAQ,GAAG,kBAAkB,CAAC,MAAM,CAAC,CAAC;IAE5C,OAAO,CAAC,KAAK,GAAG,aAAa,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC,KAAK,IAAI,EAAE,CAAC;QACrD,MAAM,MAAM,GAAG,QAAQ,CAAC,KAAK,CAAC,CAAC,EAAE,KAAK,CAAC,KAAK,CAAC,CAAC;QAC9C,iDAAiD;QACjD,IAAI,KAAK,GAAG,CAAC,CAAC;QACd,KAAK,MAAM,EAAE,IAAI,MAAM,EAAE,CAAC;YACxB,IAAI,EAAE,KAAK,GAAG;gBAAE,KAAK,EAAE,CAAC;iBACnB,IAAI,EAAE,KAAK,GAAG;gBAAE,KAAK,EAAE,CAAC;QAC/B,CAAC;QACD,IAAI,KAAK,KAAK,CAAC;YAAE,SAAS;QAE1B,MAAM,UAAU,GAAG,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC;QAC9B,MAAM,IAAI,GAAG,KAAK,CAAC,CAAC,CAAC,CAAC;QACtB,MAAM,IAAI,GAAG,MAAM,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC,MAAM,CAAC;QACvC,OAAO,CAAC,IAAI,CAAC,EAAE,IAAI,EAAE,UAAU,EAAE,IAAI,EAAE,QAAQ,EAAE,CAAC,CAAC;IACrD,CAAC;IAED,OAAO,OAAO,CAAC;AACjB,CAAC;AAED,MAAM,UAAU,cAAc,CAAC,CAAe;IAC5C,OAAO;QACL,EAAE,EAAE,QAAQ,CAAC,EAAE,QAAQ,EAAE,CAAC,CAAC,QAAQ,EAAE,KAAK,EAAE,EAAE,EAAE,IAAI,EAAE,CAAC,CAAC,IAAI,EAAE,IAAI,EAAE,UAAU,EAAE,CAAC;QACjF,KAAK,EAAE,UAAU;QACjB,IAAI,EAAE,CAAC,CAAC,IAAI;QACZ,SAAS,EAAE,CAAC,CAAC,IAAI,CAAC,WAAW,EAAE;QAC/B,QAAQ,EAAE,CAAC,CAAC,QAAQ;QACpB,SAAS,EAAE,CAAC,CAAC,IAAI;QACjB,OAAO,EAAE,CAAC,CAAC,IAAI;QACf,UAAU,EAAE,CAAC,CAAC,UAAU;KACzB,CAAC;AACJ,CAAC"}
|
|
@@ -1,21 +1,80 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
2
|
+
* The single lexical retrieval service for the monograph knowledge graph.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
4
|
+
* Every adapter — the package-level `monograph_query` MCP tool, the
|
|
5
|
+
* CLI-hosted `monograph_query` MCP tool, `augmentContext`, the eval server —
|
|
6
|
+
* calls {@link searchGraph} rather than assembling its own BM25/LIKE/fuzzy
|
|
7
|
+
* combination. Two adapters answering the same query differently was a real
|
|
8
|
+
* bug, not a hypothetical one.
|
|
9
|
+
*
|
|
10
|
+
* SCORE CONVENTION — higher is better, always non-negative.
|
|
11
|
+
* SQLite FTS5's `rank` is the opposite (negative, more negative = better).
|
|
12
|
+
* That inversion is resolved in exactly one place,
|
|
13
|
+
* {@link relevanceFromFtsRank} in `../storage/fts-store.js`, at the boundary
|
|
14
|
+
* where a rank enters the system. Nothing downstream ever sees a raw rank, so
|
|
15
|
+
* the sign cannot be forwarded into a ranker again.
|
|
16
|
+
*
|
|
17
|
+
* Monograph search is lexical-only. The former embedding/vector branch
|
|
5
18
|
* (gated on MONOGRAPH_EMBEDDINGS=true) was removed — it was never run in
|
|
6
|
-
* practice (the embeddings table stayed empty) and did a JS cosine full
|
|
7
|
-
* scan. The exported function signatures are unchanged so callers do not
|
|
8
|
-
* need to change.
|
|
19
|
+
* practice (the embeddings table stayed empty) and did a JS cosine full scan.
|
|
9
20
|
*/
|
|
10
21
|
import type Database from 'better-sqlite3';
|
|
11
22
|
import type { RankedResult } from './rrf.js';
|
|
23
|
+
/**
|
|
24
|
+
* Retrieval strategies, named for what they actually do.
|
|
25
|
+
*
|
|
26
|
+
* - `hybrid` — FTS5 BM25 + LIKE fallback + in-memory fuzzy + node-type bonus.
|
|
27
|
+
* The default: it degrades gracefully on short identifiers, where bare BM25
|
|
28
|
+
* over a trigram index returns nothing.
|
|
29
|
+
* - `bm25` — FTS5 BM25 only. Narrower, but predictable; used where a caller
|
|
30
|
+
* wants pure lexical ranking with no heuristic bonuses.
|
|
31
|
+
*/
|
|
32
|
+
export type RetrievalMode = 'bm25' | 'hybrid';
|
|
33
|
+
export interface GraphSearchOptions {
|
|
34
|
+
limit?: number;
|
|
35
|
+
label?: string;
|
|
36
|
+
/** Retrieval strategy (default: `hybrid`). */
|
|
37
|
+
mode?: RetrievalMode;
|
|
38
|
+
}
|
|
39
|
+
export interface GraphSearchResult {
|
|
40
|
+
id: string;
|
|
41
|
+
name: string;
|
|
42
|
+
normLabel: string;
|
|
43
|
+
filePath: string | null;
|
|
44
|
+
label: string;
|
|
45
|
+
/** Higher is better, always ≥ 0. See the score-convention note above. */
|
|
46
|
+
relevance: number;
|
|
47
|
+
/** First line of the symbol in its source file (1-based, null if unknown). */
|
|
48
|
+
startLine: number | null;
|
|
49
|
+
/** Last line of the symbol in its source file (1-based, null if unknown). */
|
|
50
|
+
endLine: number | null;
|
|
51
|
+
/** Which retrieval strategy surfaced this row. */
|
|
52
|
+
matchStrategy: 'fts' | 'like' | 'fuzzy';
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* Run a lexical search over the graph and return results under the
|
|
56
|
+
* higher-is-better score convention.
|
|
57
|
+
*
|
|
58
|
+
* The query is prepared identically for every mode so that two adapters
|
|
59
|
+
* asking the same question in the same mode get the same answer.
|
|
60
|
+
*
|
|
61
|
+
* Preparation is `trim()` only, deliberately. The FTS5 index uses the trigram
|
|
62
|
+
* tokenizer, which folds ASCII case but does NOT fold diacritics, and the
|
|
63
|
+
* index stores `name`/`norm_label` verbatim. Stripping diacritics from the
|
|
64
|
+
* query (as the old `bm25Query` did) therefore could never *gain* a match —
|
|
65
|
+
* it only stopped `naïveResolver` from finding the symbol literally named
|
|
66
|
+
* `naïveResolver`. Lowercasing is likewise skipped: it is a no-op for ASCII
|
|
67
|
+
* under both the trigram tokenizer and SQLite's LIKE, and for non-ASCII it
|
|
68
|
+
* would break the LIKE fallback against a capitalized name.
|
|
69
|
+
*/
|
|
70
|
+
export declare function searchGraph(db: Database.Database, query: string, options?: GraphSearchOptions): GraphSearchResult[];
|
|
12
71
|
export interface BM25QueryOptions {
|
|
13
72
|
limit?: number;
|
|
14
73
|
label?: string;
|
|
15
|
-
/** @deprecated Embeddings are disabled — this option is ignored; search is
|
|
74
|
+
/** @deprecated Embeddings are disabled — this option is ignored; search is lexical-only. Will be removed in the next major version. */
|
|
16
75
|
embedder?: unknown;
|
|
17
76
|
}
|
|
18
|
-
/** @deprecated Use {@link
|
|
77
|
+
/** @deprecated Use {@link GraphSearchOptions} — this alias will be removed in the next major version. */
|
|
19
78
|
export type HybridQueryOptions = BM25QueryOptions;
|
|
20
79
|
export interface BM25Result extends RankedResult {
|
|
21
80
|
id: string;
|
|
@@ -23,24 +82,28 @@ export interface BM25Result extends RankedResult {
|
|
|
23
82
|
normLabel: string;
|
|
24
83
|
filePath: string | null;
|
|
25
84
|
label: string;
|
|
85
|
+
/** Higher is better, always ≥ 0 — see the score-convention note above. */
|
|
26
86
|
score: number;
|
|
27
87
|
/** First line of the symbol in its source file (1-based, null if unknown). */
|
|
28
88
|
startLine?: number | null;
|
|
29
89
|
/** Last line of the symbol in its source file (1-based, null if unknown). */
|
|
30
90
|
endLine?: number | null;
|
|
31
91
|
}
|
|
32
|
-
/** @deprecated Use {@link
|
|
92
|
+
/** @deprecated Use {@link GraphSearchResult} — this alias will be removed in the next major version. */
|
|
33
93
|
export type HybridResult = BM25Result;
|
|
34
94
|
/**
|
|
35
|
-
*
|
|
36
|
-
*
|
|
95
|
+
* @deprecated Use {@link searchGraph} with `mode: 'bm25'`. Retained as a thin
|
|
96
|
+
* wrapper so existing callers keep working; it now reports the same
|
|
97
|
+
* higher-is-better `score` as every other path (it used to forward FTS5's
|
|
98
|
+
* raw negative `rank`, which inverted ranking for anything downstream that
|
|
99
|
+
* compared or propagated scores).
|
|
37
100
|
*/
|
|
38
101
|
export declare function bm25Query(db: Database.Database, query: string, options?: BM25QueryOptions): Promise<BM25Result[]>;
|
|
39
102
|
/**
|
|
40
|
-
* @deprecated Use {@link
|
|
41
|
-
* (search here is
|
|
42
|
-
* `hybridQuery` remains as an alias for backward compatibility
|
|
43
|
-
* removed in the next major version.
|
|
103
|
+
* @deprecated Use {@link searchGraph} instead — this is a misleading name
|
|
104
|
+
* (search here is lexical-only, not a hybrid of multiple retrieval
|
|
105
|
+
* *modalities*). `hybridQuery` remains as an alias for backward compatibility
|
|
106
|
+
* and will be removed in the next major version.
|
|
44
107
|
*/
|
|
45
108
|
export declare const hybridQuery: typeof bm25Query;
|
|
46
109
|
//# sourceMappingURL=hybrid-query.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"hybrid-query.d.ts","sourceRoot":"","sources":["../../../src/search/hybrid-query.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"hybrid-query.d.ts","sourceRoot":"","sources":["../../../src/search/hybrid-query.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;GAmBG;AAEH,OAAO,KAAK,QAAQ,MAAM,gBAAgB,CAAC;AAE3C,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,UAAU,CAAC;AAE7C;;;;;;;;GAQG;AACH,MAAM,MAAM,aAAa,GAAG,MAAM,GAAG,QAAQ,CAAC;AAE9C,MAAM,WAAW,kBAAkB;IACjC,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,8CAA8C;IAC9C,IAAI,CAAC,EAAE,aAAa,CAAC;CACtB;AAED,MAAM,WAAW,iBAAiB;IAChC,EAAE,EAAE,MAAM,CAAC;IACX,IAAI,EAAE,MAAM,CAAC;IACb,SAAS,EAAE,MAAM,CAAC;IAClB,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAC;IACxB,KAAK,EAAE,MAAM,CAAC;IACd,yEAAyE;IACzE,SAAS,EAAE,MAAM,CAAC;IAClB,8EAA8E;IAC9E,SAAS,EAAE,MAAM,GAAG,IAAI,CAAC;IACzB,6EAA6E;IAC7E,OAAO,EAAE,MAAM,GAAG,IAAI,CAAC;IACvB,kDAAkD;IAClD,aAAa,EAAE,KAAK,GAAG,MAAM,GAAG,OAAO,CAAC;CACzC;AAED;;;;;;;;;;;;;;;GAeG;AACH,wBAAgB,WAAW,CACzB,EAAE,EAAE,QAAQ,CAAC,QAAQ,EACrB,KAAK,EAAE,MAAM,EACb,OAAO,GAAE,kBAAuB,GAC/B,iBAAiB,EAAE,CAqCrB;AAED,MAAM,WAAW,gBAAgB;IAC/B,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,uIAAuI;IACvI,QAAQ,CAAC,EAAE,OAAO,CAAC;CACpB;AAED,yGAAyG;AACzG,MAAM,MAAM,kBAAkB,GAAG,gBAAgB,CAAC;AAElD,MAAM,WAAW,UAAW,SAAQ,YAAY;IAC9C,EAAE,EAAE,MAAM,CAAC;IACX,IAAI,EAAE,MAAM,CAAC;IACb,SAAS,EAAE,MAAM,CAAC;IAClB,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAC;IACxB,KAAK,EAAE,MAAM,CAAC;IACd,0EAA0E;IAC1E,KAAK,EAAE,MAAM,CAAC;IACd,8EAA8E;IAC9E,SAAS,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAC1B,6EAA6E;IAC7E,OAAO,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;CACzB;AAED,wGAAwG;AACxG,MAAM,MAAM,YAAY,GAAG,UAAU,CAAC;AAEtC;;;;;;GAMG;AACH,wBAAsB,SAAS,CAC7B,EAAE,EAAE,QAAQ,CAAC,QAAQ,EACrB,KAAK,EAAE,MAAM,EACb,OAAO,GAAE,gBAAqB,GAC7B,OAAO,CAAC,UAAU,EAAE,CAAC,CAYvB;AAED;;;;;GAKG;AACH,eAAO,MAAM,WAAW,kBAAY,CAAC"}
|
|
@@ -1,42 +1,102 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
2
|
+
* The single lexical retrieval service for the monograph knowledge graph.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
4
|
+
* Every adapter — the package-level `monograph_query` MCP tool, the
|
|
5
|
+
* CLI-hosted `monograph_query` MCP tool, `augmentContext`, the eval server —
|
|
6
|
+
* calls {@link searchGraph} rather than assembling its own BM25/LIKE/fuzzy
|
|
7
|
+
* combination. Two adapters answering the same query differently was a real
|
|
8
|
+
* bug, not a hypothetical one.
|
|
9
|
+
*
|
|
10
|
+
* SCORE CONVENTION — higher is better, always non-negative.
|
|
11
|
+
* SQLite FTS5's `rank` is the opposite (negative, more negative = better).
|
|
12
|
+
* That inversion is resolved in exactly one place,
|
|
13
|
+
* {@link relevanceFromFtsRank} in `../storage/fts-store.js`, at the boundary
|
|
14
|
+
* where a rank enters the system. Nothing downstream ever sees a raw rank, so
|
|
15
|
+
* the sign cannot be forwarded into a ranker again.
|
|
16
|
+
*
|
|
17
|
+
* Monograph search is lexical-only. The former embedding/vector branch
|
|
5
18
|
* (gated on MONOGRAPH_EMBEDDINGS=true) was removed — it was never run in
|
|
6
|
-
* practice (the embeddings table stayed empty) and did a JS cosine full
|
|
7
|
-
|
|
8
|
-
|
|
19
|
+
* practice (the embeddings table stayed empty) and did a JS cosine full scan.
|
|
20
|
+
*/
|
|
21
|
+
import { ftsSearch, hybridSearch, relevanceFromFtsRank } from '../storage/fts-store.js';
|
|
22
|
+
/**
|
|
23
|
+
* Run a lexical search over the graph and return results under the
|
|
24
|
+
* higher-is-better score convention.
|
|
25
|
+
*
|
|
26
|
+
* The query is prepared identically for every mode so that two adapters
|
|
27
|
+
* asking the same question in the same mode get the same answer.
|
|
28
|
+
*
|
|
29
|
+
* Preparation is `trim()` only, deliberately. The FTS5 index uses the trigram
|
|
30
|
+
* tokenizer, which folds ASCII case but does NOT fold diacritics, and the
|
|
31
|
+
* index stores `name`/`norm_label` verbatim. Stripping diacritics from the
|
|
32
|
+
* query (as the old `bm25Query` did) therefore could never *gain* a match —
|
|
33
|
+
* it only stopped `naïveResolver` from finding the symbol literally named
|
|
34
|
+
* `naïveResolver`. Lowercasing is likewise skipped: it is a no-op for ASCII
|
|
35
|
+
* under both the trigram tokenizer and SQLite's LIKE, and for non-ASCII it
|
|
36
|
+
* would break the LIKE fallback against a capitalized name.
|
|
9
37
|
*/
|
|
10
|
-
|
|
11
|
-
|
|
38
|
+
export function searchGraph(db, query, options = {}) {
|
|
39
|
+
const { limit = 20, label, mode = 'hybrid' } = options;
|
|
40
|
+
if (limit <= 0)
|
|
41
|
+
return [];
|
|
42
|
+
const normalizedQuery = (query ?? '').trim();
|
|
43
|
+
if (!normalizedQuery)
|
|
44
|
+
return [];
|
|
45
|
+
if (mode === 'bm25') {
|
|
46
|
+
// Over-fetch a little: FTS5's own ordering is what we keep, but a wider
|
|
47
|
+
// window gives the caller's label filter something to bite on.
|
|
48
|
+
const rows = ftsSearch(db, normalizedQuery, Math.max(limit, 50), label);
|
|
49
|
+
return rows.slice(0, limit).map((r) => ({
|
|
50
|
+
id: r.id,
|
|
51
|
+
name: r.name,
|
|
52
|
+
normLabel: r.normLabel,
|
|
53
|
+
filePath: r.filePath,
|
|
54
|
+
label: r.label,
|
|
55
|
+
relevance: relevanceFromFtsRank(r.rank),
|
|
56
|
+
startLine: r.startLine,
|
|
57
|
+
endLine: r.endLine,
|
|
58
|
+
matchStrategy: 'fts',
|
|
59
|
+
}));
|
|
60
|
+
}
|
|
61
|
+
// hybridSearch already reports a positive, higher-is-better combinedScore
|
|
62
|
+
// (it routes its FTS rows through relevanceFromFtsRank).
|
|
63
|
+
return hybridSearch(db, normalizedQuery, limit, label).map((r) => ({
|
|
64
|
+
id: r.id,
|
|
65
|
+
name: r.name,
|
|
66
|
+
normLabel: r.normLabel,
|
|
67
|
+
filePath: r.filePath,
|
|
68
|
+
label: r.label,
|
|
69
|
+
relevance: r.combinedScore,
|
|
70
|
+
startLine: r.startLine,
|
|
71
|
+
endLine: r.endLine,
|
|
72
|
+
matchStrategy: r.matchStrategy,
|
|
73
|
+
}));
|
|
74
|
+
}
|
|
12
75
|
/**
|
|
13
|
-
*
|
|
14
|
-
*
|
|
76
|
+
* @deprecated Use {@link searchGraph} with `mode: 'bm25'`. Retained as a thin
|
|
77
|
+
* wrapper so existing callers keep working; it now reports the same
|
|
78
|
+
* higher-is-better `score` as every other path (it used to forward FTS5's
|
|
79
|
+
* raw negative `rank`, which inverted ranking for anything downstream that
|
|
80
|
+
* compared or propagated scores).
|
|
15
81
|
*/
|
|
16
82
|
export async function bm25Query(db, query, options = {}) {
|
|
17
83
|
const { limit = 20, label } = options;
|
|
18
|
-
|
|
19
|
-
const normalizedQuery = normalizeSearchTerm(query);
|
|
20
|
-
// ── BM25 via FTS5 ──────────────────────────────────────────────────────────
|
|
21
|
-
const bm25Limit = Math.max(limit, 50);
|
|
22
|
-
const bm25Raw = ftsSearch(db, normalizedQuery, bm25Limit, label);
|
|
23
|
-
const bm25Results = bm25Raw.map((r) => ({
|
|
84
|
+
return searchGraph(db, query, { limit, label, mode: 'bm25' }).map((r) => ({
|
|
24
85
|
id: r.id,
|
|
25
86
|
name: r.name,
|
|
26
87
|
normLabel: r.normLabel,
|
|
27
88
|
filePath: r.filePath,
|
|
28
89
|
label: r.label,
|
|
29
|
-
score: r.
|
|
90
|
+
score: r.relevance,
|
|
30
91
|
startLine: r.startLine,
|
|
31
92
|
endLine: r.endLine,
|
|
32
93
|
}));
|
|
33
|
-
return bm25Results.slice(0, limit);
|
|
34
94
|
}
|
|
35
95
|
/**
|
|
36
|
-
* @deprecated Use {@link
|
|
37
|
-
* (search here is
|
|
38
|
-
* `hybridQuery` remains as an alias for backward compatibility
|
|
39
|
-
* removed in the next major version.
|
|
96
|
+
* @deprecated Use {@link searchGraph} instead — this is a misleading name
|
|
97
|
+
* (search here is lexical-only, not a hybrid of multiple retrieval
|
|
98
|
+
* *modalities*). `hybridQuery` remains as an alias for backward compatibility
|
|
99
|
+
* and will be removed in the next major version.
|
|
40
100
|
*/
|
|
41
101
|
export const hybridQuery = bm25Query;
|
|
42
102
|
//# sourceMappingURL=hybrid-query.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"hybrid-query.js","sourceRoot":"","sources":["../../../src/search/hybrid-query.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"hybrid-query.js","sourceRoot":"","sources":["../../../src/search/hybrid-query.ts"],"names":[],"mappings":"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"}
|
|
@@ -1,11 +1,57 @@
|
|
|
1
1
|
import type { MonographDb } from '../storage/db.js';
|
|
2
|
+
/**
|
|
3
|
+
* Explicit index freshness. A boolean cannot distinguish "proven up to date"
|
|
4
|
+
* from "we could not check", and reporting the latter as the former is how a
|
|
5
|
+
* confident-but-baseless "fresh" answer reaches a caller.
|
|
6
|
+
*
|
|
7
|
+
* - `fresh` — index matches HEAD, worktree is clean, no recorded warnings.
|
|
8
|
+
* - `stale` — index is behind HEAD, or the worktree has uncommitted edits.
|
|
9
|
+
* - `building` — a build lock is held by a live process right now.
|
|
10
|
+
* - `partial` — index matches HEAD but is known incomplete (parser warnings
|
|
11
|
+
* or a recorded refresh error).
|
|
12
|
+
* - `unknown` — freshness could not be determined (git unavailable, no
|
|
13
|
+
* recorded revision, or the dirty-worktree probe failed).
|
|
14
|
+
*/
|
|
15
|
+
export type IndexFreshnessState = 'fresh' | 'stale' | 'building' | 'partial' | 'unknown';
|
|
2
16
|
export interface StalenessReport {
|
|
17
|
+
/**
|
|
18
|
+
* Commit-divergence only: true when the indexed commit is known to differ
|
|
19
|
+
* from HEAD (or no commit was recorded). Deliberately unchanged — the
|
|
20
|
+
* pipeline's skip-when-fresh guard reads it together with `currentCommit`.
|
|
21
|
+
* It is NOT a freshness verdict: a clean-commit / dirty-worktree index has
|
|
22
|
+
* `isStale: false` and `state: 'stale'`. New callers should read `state`.
|
|
23
|
+
*/
|
|
3
24
|
isStale: boolean;
|
|
25
|
+
/** Timestamp of the last build, from `index_meta.indexed_at`. */
|
|
4
26
|
indexedAt: string | null;
|
|
27
|
+
/** Short (7-char) SHA of the indexed revision. */
|
|
5
28
|
indexedCommit: string | null;
|
|
29
|
+
/** Short (7-char) SHA of current HEAD. */
|
|
6
30
|
currentCommit: string | null;
|
|
7
31
|
changedSince: string[];
|
|
8
32
|
staleSince: string | null;
|
|
33
|
+
/** Explicit freshness state — prefer this over `isStale`. */
|
|
34
|
+
state: IndexFreshnessState;
|
|
35
|
+
/** One sentence explaining why `state` has this value; safe to show a user. */
|
|
36
|
+
reason: string;
|
|
37
|
+
/** Full SHA of the revision the index was built from, null when unrecorded. */
|
|
38
|
+
indexedRevision: string | null;
|
|
39
|
+
/** Source scope the index was built with (e.g. 'code-only'), null when unrecorded. */
|
|
40
|
+
sourceScope: string | null;
|
|
41
|
+
/** true/false when git could be consulted, null when the probe failed. */
|
|
42
|
+
dirtyWorktree: boolean | null;
|
|
43
|
+
/** Sample of uncommitted paths, capped at {@link DIRTY_PATH_CAP}. */
|
|
44
|
+
dirtyPaths: string[];
|
|
45
|
+
/** True number of uncommitted paths, null when undetermined. */
|
|
46
|
+
dirtyFileCount: number | null;
|
|
47
|
+
/** Parser warnings recorded by the last build, null when none recorded. */
|
|
48
|
+
parserWarnings: string[] | null;
|
|
49
|
+
/** Error recorded by the last refresh attempt, null when none recorded. */
|
|
50
|
+
lastRefreshError: string | null;
|
|
51
|
+
/** A build lock is currently held by a live process. */
|
|
52
|
+
buildInProgress: boolean;
|
|
53
|
+
/** True whenever `state !== 'fresh'`: graph answers may be incomplete. */
|
|
54
|
+
mayBeIncomplete: boolean;
|
|
9
55
|
}
|
|
10
56
|
export declare function checkStaleness(db: MonographDb, repoPath: string): StalenessReport;
|
|
11
57
|
//# sourceMappingURL=git-staleness.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"git-staleness.d.ts","sourceRoot":"","sources":["../../../src/staleness/git-staleness.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"git-staleness.d.ts","sourceRoot":"","sources":["../../../src/staleness/git-staleness.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,kBAAkB,CAAC;AAEpD;;;;;;;;;;;;GAYG;AACH,MAAM,MAAM,mBAAmB,GAAG,OAAO,GAAG,OAAO,GAAG,UAAU,GAAG,SAAS,GAAG,SAAS,CAAC;AAKzF,MAAM,WAAW,eAAe;IAC9B;;;;;;OAMG;IACH,OAAO,EAAE,OAAO,CAAC;IACjB,iEAAiE;IACjE,SAAS,EAAE,MAAM,GAAG,IAAI,CAAC;IACzB,kDAAkD;IAClD,aAAa,EAAE,MAAM,GAAG,IAAI,CAAC;IAC7B,0CAA0C;IAC1C,aAAa,EAAE,MAAM,GAAG,IAAI,CAAC;IAC7B,YAAY,EAAE,MAAM,EAAE,CAAC;IACvB,UAAU,EAAE,MAAM,GAAG,IAAI,CAAC;IAE1B,6DAA6D;IAC7D,KAAK,EAAE,mBAAmB,CAAC;IAC3B,+EAA+E;IAC/E,MAAM,EAAE,MAAM,CAAC;IACf,+EAA+E;IAC/E,eAAe,EAAE,MAAM,GAAG,IAAI,CAAC;IAC/B,sFAAsF;IACtF,WAAW,EAAE,MAAM,GAAG,IAAI,CAAC;IAC3B,0EAA0E;IAC1E,aAAa,EAAE,OAAO,GAAG,IAAI,CAAC;IAC9B,qEAAqE;IACrE,UAAU,EAAE,MAAM,EAAE,CAAC;IACrB,gEAAgE;IAChE,cAAc,EAAE,MAAM,GAAG,IAAI,CAAC;IAC9B,2EAA2E;IAC3E,cAAc,EAAE,MAAM,EAAE,GAAG,IAAI,CAAC;IAChC,2EAA2E;IAC3E,gBAAgB,EAAE,MAAM,GAAG,IAAI,CAAC;IAChC,wDAAwD;IACxD,eAAe,EAAE,OAAO,CAAC;IACzB,0EAA0E;IAC1E,eAAe,EAAE,OAAO,CAAC;CAC1B;AAgKD,wBAAgB,cAAc,CAAC,EAAE,EAAE,WAAW,EAAE,QAAQ,EAAE,MAAM,GAAG,eAAe,CAuHjF"}
|
|
@@ -1,58 +1,216 @@
|
|
|
1
1
|
import { execSync } from 'node:child_process';
|
|
2
|
+
import { readFileSync } from 'node:fs';
|
|
3
|
+
/** Cap on the dirty-path sample carried in the report; `dirtyFileCount` keeps the true total. */
|
|
4
|
+
const DIRTY_PATH_CAP = 50;
|
|
5
|
+
function readMeta(db) {
|
|
6
|
+
try {
|
|
7
|
+
const rows = db.prepare('SELECT key, value FROM index_meta').all();
|
|
8
|
+
return new Map(rows.map((r) => [r.key, r.value]));
|
|
9
|
+
}
|
|
10
|
+
catch {
|
|
11
|
+
return new Map();
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
/** Accepts either a JSON array or a newline-separated list; null when empty. */
|
|
15
|
+
function parseWarnings(raw) {
|
|
16
|
+
if (!raw)
|
|
17
|
+
return null;
|
|
18
|
+
try {
|
|
19
|
+
const parsed = JSON.parse(raw);
|
|
20
|
+
if (Array.isArray(parsed)) {
|
|
21
|
+
const items = parsed.map(String).filter(Boolean);
|
|
22
|
+
return items.length > 0 ? items : null;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
catch {
|
|
26
|
+
/* not JSON — fall through to line parsing */
|
|
27
|
+
}
|
|
28
|
+
const lines = raw
|
|
29
|
+
.split('\n')
|
|
30
|
+
.map((l) => l.trim())
|
|
31
|
+
.filter(Boolean);
|
|
32
|
+
return lines.length > 0 ? lines : null;
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* The pipeline serializes builds with a `<dbPath>.build-lock` file holding the
|
|
36
|
+
* builder's pid. A live holder means a rebuild is under way right now, which is
|
|
37
|
+
* a different answer than "stale" for a caller deciding whether to wait.
|
|
38
|
+
*/
|
|
39
|
+
function isBuildInProgress(db) {
|
|
40
|
+
const dbPath = typeof db.name === 'string' ? db.name : '';
|
|
41
|
+
if (!dbPath || dbPath === ':memory:')
|
|
42
|
+
return false;
|
|
43
|
+
let pid;
|
|
44
|
+
try {
|
|
45
|
+
pid = Number.parseInt(readFileSync(`${dbPath}.build-lock`, 'utf8'), 10);
|
|
46
|
+
}
|
|
47
|
+
catch {
|
|
48
|
+
return false; // no lock file
|
|
49
|
+
}
|
|
50
|
+
if (!Number.isFinite(pid) || pid <= 0)
|
|
51
|
+
return false;
|
|
52
|
+
try {
|
|
53
|
+
process.kill(pid, 0);
|
|
54
|
+
return true;
|
|
55
|
+
}
|
|
56
|
+
catch (err) {
|
|
57
|
+
// EPERM means the process exists but belongs to another user.
|
|
58
|
+
return err?.code === 'EPERM';
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* Commit comparison alone cannot see uncommitted work, which is exactly the
|
|
63
|
+
* code an agent is most likely to ask about. Cheap `git status --porcelain`;
|
|
64
|
+
* a failure reports "undetermined" rather than "clean".
|
|
65
|
+
*/
|
|
66
|
+
function checkDirtyWorktree(repoPath) {
|
|
67
|
+
try {
|
|
68
|
+
const out = execSync('git status --porcelain --no-renames', {
|
|
69
|
+
cwd: repoPath,
|
|
70
|
+
encoding: 'utf8',
|
|
71
|
+
maxBuffer: 8 * 1024 * 1024,
|
|
72
|
+
});
|
|
73
|
+
// Porcelain v1 lines are `XY <path>`; ignored files are excluded by default.
|
|
74
|
+
const paths = out
|
|
75
|
+
.split('\n')
|
|
76
|
+
.filter((line) => line.length > 3)
|
|
77
|
+
.map((line) => line.slice(3).trim())
|
|
78
|
+
.filter(Boolean);
|
|
79
|
+
return { dirty: paths.length > 0, paths: paths.slice(0, DIRTY_PATH_CAP), count: paths.length };
|
|
80
|
+
}
|
|
81
|
+
catch {
|
|
82
|
+
return { dirty: null, paths: [], count: null };
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
function buildReport(meta, parts) {
|
|
86
|
+
return {
|
|
87
|
+
isStale: parts.isStale,
|
|
88
|
+
indexedAt: meta.get('indexed_at') ?? null,
|
|
89
|
+
indexedCommit: parts.indexedCommit,
|
|
90
|
+
currentCommit: parts.currentCommit,
|
|
91
|
+
changedSince: parts.changedSince,
|
|
92
|
+
staleSince: parts.staleSince,
|
|
93
|
+
state: parts.state,
|
|
94
|
+
reason: parts.reason,
|
|
95
|
+
indexedRevision: parts.indexedRevision,
|
|
96
|
+
sourceScope: meta.get('source_scope') ?? meta.get('scope') ?? null,
|
|
97
|
+
dirtyWorktree: parts.dirty.dirty,
|
|
98
|
+
dirtyPaths: parts.dirty.paths,
|
|
99
|
+
dirtyFileCount: parts.dirty.count,
|
|
100
|
+
parserWarnings: parseWarnings(meta.get('parser_warnings') ?? meta.get('parse_errors')),
|
|
101
|
+
lastRefreshError: meta.get('last_refresh_error') ?? null,
|
|
102
|
+
buildInProgress: parts.state === 'building',
|
|
103
|
+
mayBeIncomplete: parts.state !== 'fresh',
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
/**
|
|
107
|
+
* State for an index whose recorded commit equals HEAD. Matching commits are
|
|
108
|
+
* necessary but not sufficient for "fresh": the worktree may be dirty, the
|
|
109
|
+
* probe may have failed, or the last build may have recorded problems.
|
|
110
|
+
*/
|
|
111
|
+
function stateForMatchingCommit(meta, dirty) {
|
|
112
|
+
if (dirty.dirty === null) {
|
|
113
|
+
return {
|
|
114
|
+
state: 'unknown',
|
|
115
|
+
reason: 'Index matches HEAD, but the working tree could not be checked for uncommitted edits.',
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
if (dirty.dirty) {
|
|
119
|
+
return {
|
|
120
|
+
state: 'stale',
|
|
121
|
+
reason: `Index matches HEAD but ${dirty.count} path(s) have uncommitted changes that are not in the graph.`,
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
const warnings = parseWarnings(meta.get('parser_warnings') ?? meta.get('parse_errors'));
|
|
125
|
+
const refreshError = meta.get('last_refresh_error') ?? null;
|
|
126
|
+
if (warnings) {
|
|
127
|
+
return {
|
|
128
|
+
state: 'partial',
|
|
129
|
+
reason: `Index matches HEAD but the last build recorded ${warnings.length} parser warning(s); some symbols may be missing.`,
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
if (refreshError) {
|
|
133
|
+
return {
|
|
134
|
+
state: 'partial',
|
|
135
|
+
reason: `Index matches HEAD but the last refresh reported an error: ${refreshError}`,
|
|
136
|
+
};
|
|
137
|
+
}
|
|
138
|
+
return { state: 'fresh', reason: 'Index matches HEAD and the working tree is clean.' };
|
|
139
|
+
}
|
|
2
140
|
export function checkStaleness(db, repoPath) {
|
|
3
|
-
|
|
4
|
-
const
|
|
5
|
-
const
|
|
6
|
-
|
|
141
|
+
const meta = readMeta(db);
|
|
142
|
+
const indexedCommitFull = meta.get('last_commit_hash') ?? null;
|
|
143
|
+
const indexedCommitShort = indexedCommitFull ? indexedCommitFull.slice(0, 7) : null;
|
|
144
|
+
const building = isBuildInProgress(db);
|
|
145
|
+
// 1. Current HEAD (full SHA — `--short` output length varies by repo size)
|
|
7
146
|
let currentCommit = null;
|
|
8
147
|
try {
|
|
9
148
|
currentCommit = execSync('git rev-parse HEAD', { cwd: repoPath, encoding: 'utf8' }).trim();
|
|
10
149
|
}
|
|
11
150
|
catch {
|
|
12
|
-
// Not a git repo
|
|
13
|
-
|
|
151
|
+
// Not a git repo, no git binary, or a broken checkout. Nothing is known
|
|
152
|
+
// about freshness here — `unknown`, never `fresh`.
|
|
153
|
+
return buildReport(meta, {
|
|
14
154
|
isStale: false,
|
|
15
|
-
indexedAt: null,
|
|
16
155
|
indexedCommit: null,
|
|
17
156
|
currentCommit: null,
|
|
18
157
|
changedSince: [],
|
|
19
158
|
staleSince: null,
|
|
20
|
-
|
|
159
|
+
state: building ? 'building' : 'unknown',
|
|
160
|
+
reason: building
|
|
161
|
+
? 'A build is currently running; git is unavailable so freshness cannot be checked.'
|
|
162
|
+
: 'Git is unavailable in this directory, so index freshness cannot be determined. Treat graph results as possibly incomplete.',
|
|
163
|
+
indexedRevision: indexedCommitFull,
|
|
164
|
+
dirty: { dirty: null, paths: [], count: null },
|
|
165
|
+
});
|
|
21
166
|
}
|
|
22
|
-
|
|
23
|
-
const
|
|
24
|
-
|
|
25
|
-
// 3. If SHAs match → fresh. If no stored commit → staleness unknown (not "fresh").
|
|
167
|
+
const currentCommitShort = currentCommit.slice(0, 7);
|
|
168
|
+
const dirty = checkDirtyWorktree(repoPath);
|
|
169
|
+
// 2. Commits match → freshness still depends on worktree state and warnings.
|
|
26
170
|
if (indexedCommitShort === currentCommitShort) {
|
|
27
|
-
|
|
171
|
+
const { state, reason } = stateForMatchingCommit(meta, dirty);
|
|
172
|
+
return buildReport(meta, {
|
|
28
173
|
isStale: false,
|
|
29
|
-
indexedAt: null,
|
|
30
174
|
indexedCommit: indexedCommitShort,
|
|
31
175
|
currentCommit: currentCommitShort,
|
|
32
176
|
changedSince: [],
|
|
33
177
|
staleSince: null,
|
|
34
|
-
|
|
178
|
+
state: building ? 'building' : state,
|
|
179
|
+
reason: building ? 'A build is currently running.' : reason,
|
|
180
|
+
indexedRevision: indexedCommitFull,
|
|
181
|
+
dirty,
|
|
182
|
+
});
|
|
35
183
|
}
|
|
184
|
+
// 3. No recorded revision → staleness is unknown, not merely "stale".
|
|
185
|
+
// `isStale` stays true so the rebuild guard keeps its conservative behavior.
|
|
36
186
|
if (!indexedCommitFull) {
|
|
37
|
-
return {
|
|
187
|
+
return buildReport(meta, {
|
|
38
188
|
isStale: true,
|
|
39
|
-
indexedAt: null,
|
|
40
189
|
indexedCommit: null,
|
|
41
190
|
currentCommit: currentCommitShort,
|
|
42
191
|
changedSince: [],
|
|
43
192
|
staleSince: null,
|
|
44
|
-
|
|
193
|
+
state: building ? 'building' : 'unknown',
|
|
194
|
+
reason: building
|
|
195
|
+
? 'A build is currently running; no indexed revision has been recorded yet.'
|
|
196
|
+
: 'No indexed revision was recorded, so the index cannot be compared against HEAD.',
|
|
197
|
+
indexedRevision: null,
|
|
198
|
+
dirty,
|
|
199
|
+
});
|
|
45
200
|
}
|
|
46
201
|
// Guard: indexedCommitFull is read from SQLite — validate before shell interpolation
|
|
47
202
|
if (!/^[0-9a-f]{7,40}$/i.test(indexedCommitFull)) {
|
|
48
|
-
return {
|
|
203
|
+
return buildReport(meta, {
|
|
49
204
|
isStale: true,
|
|
50
|
-
indexedAt: null,
|
|
51
205
|
indexedCommit: indexedCommitShort,
|
|
52
206
|
currentCommit: currentCommitShort,
|
|
53
207
|
changedSince: [],
|
|
54
208
|
staleSince: null,
|
|
55
|
-
|
|
209
|
+
state: building ? 'building' : 'unknown',
|
|
210
|
+
reason: 'The recorded index revision is not a valid commit SHA, so freshness cannot be determined. Re-run the build.',
|
|
211
|
+
indexedRevision: indexedCommitFull,
|
|
212
|
+
dirty,
|
|
213
|
+
});
|
|
56
214
|
}
|
|
57
215
|
// 4. Get changed files between indexed commit and HEAD
|
|
58
216
|
let changedSince = [];
|
|
@@ -75,13 +233,19 @@ export function checkStaleness(db, repoPath) {
|
|
|
75
233
|
catch {
|
|
76
234
|
staleSince = null;
|
|
77
235
|
}
|
|
78
|
-
|
|
236
|
+
const dirtyNote = dirty.dirty ? ` ${dirty.count} path(s) also have uncommitted changes.` : '';
|
|
237
|
+
return buildReport(meta, {
|
|
79
238
|
isStale: true,
|
|
80
|
-
indexedAt: null,
|
|
81
239
|
indexedCommit: indexedCommitShort,
|
|
82
240
|
currentCommit: currentCommitShort,
|
|
83
241
|
changedSince,
|
|
84
242
|
staleSince,
|
|
85
|
-
|
|
243
|
+
state: building ? 'building' : 'stale',
|
|
244
|
+
reason: building
|
|
245
|
+
? 'A build is currently running.'
|
|
246
|
+
: `Index was built at ${indexedCommitShort} but HEAD is ${currentCommitShort}; ${changedSince.length} file(s) changed since.${dirtyNote}`,
|
|
247
|
+
indexedRevision: indexedCommitFull,
|
|
248
|
+
dirty,
|
|
249
|
+
});
|
|
86
250
|
}
|
|
87
251
|
//# sourceMappingURL=git-staleness.js.map
|