@optave/codegraph 3.9.5 → 3.10.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/README.md +30 -16
- package/dist/ast-analysis/engine.d.ts.map +1 -1
- package/dist/ast-analysis/engine.js +4 -3
- package/dist/ast-analysis/engine.js.map +1 -1
- package/dist/ast-analysis/rules/csharp.d.ts.map +1 -1
- package/dist/ast-analysis/rules/csharp.js +8 -1
- package/dist/ast-analysis/rules/csharp.js.map +1 -1
- package/dist/ast-analysis/rules/go.d.ts.map +1 -1
- package/dist/ast-analysis/rules/go.js +4 -1
- package/dist/ast-analysis/rules/go.js.map +1 -1
- package/dist/ast-analysis/rules/index.d.ts +6 -0
- package/dist/ast-analysis/rules/index.d.ts.map +1 -1
- package/dist/ast-analysis/rules/index.js +151 -4
- package/dist/ast-analysis/rules/index.js.map +1 -1
- package/dist/ast-analysis/rules/java.d.ts.map +1 -1
- package/dist/ast-analysis/rules/java.js +5 -1
- package/dist/ast-analysis/rules/java.js.map +1 -1
- package/dist/ast-analysis/rules/php.d.ts.map +1 -1
- package/dist/ast-analysis/rules/php.js +6 -1
- package/dist/ast-analysis/rules/php.js.map +1 -1
- package/dist/ast-analysis/rules/python.d.ts.map +1 -1
- package/dist/ast-analysis/rules/python.js +5 -1
- package/dist/ast-analysis/rules/python.js.map +1 -1
- package/dist/ast-analysis/rules/ruby.d.ts.map +1 -1
- package/dist/ast-analysis/rules/ruby.js +4 -1
- package/dist/ast-analysis/rules/ruby.js.map +1 -1
- package/dist/ast-analysis/rules/rust.d.ts.map +1 -1
- package/dist/ast-analysis/rules/rust.js +5 -1
- package/dist/ast-analysis/rules/rust.js.map +1 -1
- package/dist/ast-analysis/visitors/ast-store-visitor.d.ts +2 -1
- package/dist/ast-analysis/visitors/ast-store-visitor.d.ts.map +1 -1
- package/dist/ast-analysis/visitors/ast-store-visitor.js +171 -37
- package/dist/ast-analysis/visitors/ast-store-visitor.js.map +1 -1
- package/dist/domain/graph/builder/context.d.ts +10 -0
- package/dist/domain/graph/builder/context.d.ts.map +1 -1
- package/dist/domain/graph/builder/context.js +10 -0
- package/dist/domain/graph/builder/context.js.map +1 -1
- package/dist/domain/graph/builder/helpers.d.ts +7 -2
- package/dist/domain/graph/builder/helpers.d.ts.map +1 -1
- package/dist/domain/graph/builder/helpers.js +7 -2
- package/dist/domain/graph/builder/helpers.js.map +1 -1
- package/dist/domain/graph/builder/pipeline.d.ts.map +1 -1
- package/dist/domain/graph/builder/pipeline.js +210 -34
- package/dist/domain/graph/builder/pipeline.js.map +1 -1
- package/dist/domain/graph/builder/stages/collect-files.d.ts.map +1 -1
- package/dist/domain/graph/builder/stages/collect-files.js +8 -0
- package/dist/domain/graph/builder/stages/collect-files.js.map +1 -1
- package/dist/domain/graph/builder/stages/detect-changes.d.ts +24 -0
- package/dist/domain/graph/builder/stages/detect-changes.d.ts.map +1 -1
- package/dist/domain/graph/builder/stages/detect-changes.js +117 -3
- package/dist/domain/graph/builder/stages/detect-changes.js.map +1 -1
- package/dist/domain/graph/builder/stages/finalize.d.ts.map +1 -1
- package/dist/domain/graph/builder/stages/finalize.js +9 -6
- package/dist/domain/graph/builder/stages/finalize.js.map +1 -1
- package/dist/domain/graph/builder/stages/insert-nodes.d.ts +30 -0
- package/dist/domain/graph/builder/stages/insert-nodes.d.ts.map +1 -1
- package/dist/domain/graph/builder/stages/insert-nodes.js +36 -13
- package/dist/domain/graph/builder/stages/insert-nodes.js.map +1 -1
- package/dist/domain/parser.d.ts +54 -1
- package/dist/domain/parser.d.ts.map +1 -1
- package/dist/domain/parser.js +181 -10
- package/dist/domain/parser.js.map +1 -1
- package/dist/domain/search/models.js +2 -2
- package/dist/domain/wasm-worker-entry.js +15 -14
- package/dist/domain/wasm-worker-entry.js.map +1 -1
- package/dist/features/ast.d.ts.map +1 -1
- package/dist/features/ast.js +11 -9
- package/dist/features/ast.js.map +1 -1
- package/dist/infrastructure/config.d.ts +1 -0
- package/dist/infrastructure/config.d.ts.map +1 -1
- package/dist/infrastructure/config.js +1 -0
- package/dist/infrastructure/config.js.map +1 -1
- package/dist/mcp/server.d.ts.map +1 -1
- package/dist/mcp/server.js +14 -8
- package/dist/mcp/server.js.map +1 -1
- package/dist/mcp/tool-registry.d.ts +1 -1
- package/dist/mcp/tool-registry.d.ts.map +1 -1
- package/dist/mcp/tool-registry.js +19 -5
- package/dist/mcp/tool-registry.js.map +1 -1
- package/dist/types.d.ts +1 -0
- package/dist/types.d.ts.map +1 -1
- package/grammars/tree-sitter-erlang.wasm +0 -0
- package/package.json +8 -7
- package/src/ast-analysis/engine.ts +14 -2
- package/src/ast-analysis/rules/csharp.ts +8 -1
- package/src/ast-analysis/rules/go.ts +4 -1
- package/src/ast-analysis/rules/index.ts +181 -4
- package/src/ast-analysis/rules/java.ts +5 -1
- package/src/ast-analysis/rules/php.ts +6 -1
- package/src/ast-analysis/rules/python.ts +5 -1
- package/src/ast-analysis/rules/ruby.ts +4 -1
- package/src/ast-analysis/rules/rust.ts +5 -1
- package/src/ast-analysis/visitors/ast-store-visitor.ts +165 -34
- package/src/domain/graph/builder/context.ts +10 -0
- package/src/domain/graph/builder/helpers.ts +8 -3
- package/src/domain/graph/builder/pipeline.ts +234 -36
- package/src/domain/graph/builder/stages/collect-files.ts +9 -0
- package/src/domain/graph/builder/stages/detect-changes.ts +130 -4
- package/src/domain/graph/builder/stages/finalize.ts +9 -6
- package/src/domain/graph/builder/stages/insert-nodes.ts +38 -14
- package/src/domain/parser.ts +205 -9
- package/src/domain/search/models.ts +2 -2
- package/src/domain/wasm-worker-entry.ts +23 -13
- package/src/features/ast.ts +22 -9
- package/src/infrastructure/config.ts +1 -0
- package/src/mcp/server.ts +16 -9
- package/src/mcp/tool-registry.ts +23 -5
- package/src/types.ts +1 -0
|
@@ -172,4 +172,8 @@ export const dataflow: DataflowRulesConfig = makeDataflowRules({
|
|
|
172
172
|
|
|
173
173
|
// ─── AST Node Types ───────────────────────────────────────────────────────
|
|
174
174
|
|
|
175
|
-
export const astTypes: Record<string, string> | null =
|
|
175
|
+
export const astTypes: Record<string, string> | null = {
|
|
176
|
+
await_expression: 'await',
|
|
177
|
+
string_literal: 'string',
|
|
178
|
+
raw_string_literal: 'string',
|
|
179
|
+
};
|
|
@@ -5,9 +5,42 @@ import type {
|
|
|
5
5
|
Visitor,
|
|
6
6
|
VisitorContext,
|
|
7
7
|
} from '../../types.js';
|
|
8
|
+
import type { AstStringConfig } from '../rules/index.js';
|
|
8
9
|
|
|
9
10
|
const TEXT_MAX = 200;
|
|
10
11
|
|
|
12
|
+
// ── Cross-language node-type constants (mirror Rust `helpers.rs`) ────────
|
|
13
|
+
const IDENT_TYPES = new Set<string>([
|
|
14
|
+
'identifier',
|
|
15
|
+
'type_identifier',
|
|
16
|
+
'name',
|
|
17
|
+
'qualified_name',
|
|
18
|
+
'scoped_identifier',
|
|
19
|
+
'qualified_identifier',
|
|
20
|
+
'member_expression',
|
|
21
|
+
'member_access_expression',
|
|
22
|
+
'field_expression',
|
|
23
|
+
'attribute',
|
|
24
|
+
'scoped_type_identifier',
|
|
25
|
+
]);
|
|
26
|
+
|
|
27
|
+
const CALL_TYPES = new Set<string>([
|
|
28
|
+
'call_expression',
|
|
29
|
+
'call',
|
|
30
|
+
'invocation_expression',
|
|
31
|
+
'method_invocation',
|
|
32
|
+
'function_call_expression',
|
|
33
|
+
'member_call_expression',
|
|
34
|
+
'scoped_call_expression',
|
|
35
|
+
]);
|
|
36
|
+
|
|
37
|
+
const DEFAULT_STRING_CONFIG: AstStringConfig = { quoteChars: '\'"`', stringPrefixes: '' };
|
|
38
|
+
|
|
39
|
+
// Keyword tokens skipped when extracting the inner expression text of a
|
|
40
|
+
// throw/raise/await/new node. Module-level constant avoids reallocating on
|
|
41
|
+
// every call (can be hot in large files).
|
|
42
|
+
const CHILD_EXPR_SKIP_KEYWORDS = new Set<string>(['throw', 'raise', 'await', 'new']);
|
|
43
|
+
|
|
11
44
|
interface AstStoreRow {
|
|
12
45
|
file: string;
|
|
13
46
|
line: number;
|
|
@@ -20,69 +53,150 @@ interface AstStoreRow {
|
|
|
20
53
|
|
|
21
54
|
function truncate(s: string | null | undefined, max: number = TEXT_MAX): string | null {
|
|
22
55
|
if (!s) return null;
|
|
23
|
-
return s.length <= max ? s : `${s.slice(0, max - 1)}
|
|
56
|
+
return s.length <= max ? s : `${s.slice(0, max - 1)}…`;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function trimLeadingChars(s: string, chars: string): string {
|
|
60
|
+
if (!chars) return s;
|
|
61
|
+
let i = 0;
|
|
62
|
+
while (i < s.length && chars.includes(s[i]!)) i++;
|
|
63
|
+
return i === 0 ? s : s.slice(i);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function trimTrailingChars(s: string, chars: string): string {
|
|
67
|
+
if (!chars) return s;
|
|
68
|
+
let i = s.length;
|
|
69
|
+
while (i > 0 && chars.includes(s[i - 1]!)) i--;
|
|
70
|
+
return i === s.length ? s : s.slice(0, i);
|
|
24
71
|
}
|
|
25
72
|
|
|
26
|
-
|
|
73
|
+
/** Extract constructor name from a `new_expression` / `object_creation_expression`. */
|
|
74
|
+
function extractConstructorName(node: TreeSitterNode): string {
|
|
75
|
+
for (const field of ['type', 'class', 'constructor']) {
|
|
76
|
+
const f = node.childForFieldName(field);
|
|
77
|
+
if (f?.text) return f.text;
|
|
78
|
+
}
|
|
27
79
|
for (let i = 0; i < node.childCount; i++) {
|
|
28
80
|
const child = node.child(i);
|
|
29
81
|
if (!child) continue;
|
|
30
|
-
if (child.type
|
|
31
|
-
if (child.type === 'member_expression') return child.text;
|
|
82
|
+
if (IDENT_TYPES.has(child.type)) return child.text;
|
|
32
83
|
}
|
|
33
|
-
|
|
84
|
+
const raw = node.text || '';
|
|
85
|
+
const beforeParen = raw.split('(')[0] || raw;
|
|
86
|
+
return beforeParen.replace(/^new\s+/, '').trim() || '?';
|
|
34
87
|
}
|
|
35
88
|
|
|
36
|
-
function
|
|
89
|
+
/** Extract function name from a call node. */
|
|
90
|
+
function extractCallName(node: TreeSitterNode): string {
|
|
91
|
+
for (const field of ['function', 'method', 'name']) {
|
|
92
|
+
const f = node.childForFieldName(field);
|
|
93
|
+
if (f?.text) return f.text;
|
|
94
|
+
}
|
|
95
|
+
const text = node.text || '';
|
|
96
|
+
return text.split('(')[0] || '?';
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/** Extract name from a throw/raise statement — matches native `extract_throw_target`. */
|
|
100
|
+
function extractThrowName(node: TreeSitterNode, newTypes: Set<string>): string {
|
|
37
101
|
for (let i = 0; i < node.childCount; i++) {
|
|
38
102
|
const child = node.child(i);
|
|
39
103
|
if (!child) continue;
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
104
|
+
const ck = child.type;
|
|
105
|
+
if (newTypes.has(ck)) return extractConstructorName(child);
|
|
106
|
+
if (CALL_TYPES.has(ck)) return extractCallName(child);
|
|
107
|
+
if (IDENT_TYPES.has(ck)) return child.text;
|
|
43
108
|
}
|
|
44
|
-
return truncate(node.text);
|
|
109
|
+
return truncate(node.text) ?? node.text ?? '';
|
|
45
110
|
}
|
|
46
111
|
|
|
47
|
-
/** Extract
|
|
48
|
-
function
|
|
112
|
+
/** Extract name from an await expression — matches native `extract_awaited_name`. */
|
|
113
|
+
function extractAwaitName(node: TreeSitterNode): string {
|
|
49
114
|
for (let i = 0; i < node.childCount; i++) {
|
|
50
115
|
const child = node.child(i);
|
|
51
116
|
if (!child) continue;
|
|
52
|
-
|
|
53
|
-
if (
|
|
54
|
-
|
|
55
|
-
return fn ? fn.text : child.text?.split('(')[0] || '?';
|
|
56
|
-
}
|
|
57
|
-
if (child.type === 'identifier') return child.text;
|
|
117
|
+
const ck = child.type;
|
|
118
|
+
if (CALL_TYPES.has(ck)) return extractCallName(child);
|
|
119
|
+
if (IDENT_TYPES.has(ck)) return child.text;
|
|
58
120
|
}
|
|
59
|
-
return truncate(node.text);
|
|
121
|
+
return truncate(node.text) ?? node.text ?? '';
|
|
60
122
|
}
|
|
61
123
|
|
|
62
|
-
/** Extract the
|
|
63
|
-
function
|
|
124
|
+
/** Extract text of the expression inside a throw/await, skipping the keyword. */
|
|
125
|
+
function extractChildExpressionText(node: TreeSitterNode): string | null {
|
|
64
126
|
for (let i = 0; i < node.childCount; i++) {
|
|
65
127
|
const child = node.child(i);
|
|
66
128
|
if (!child) continue;
|
|
67
|
-
if (child.type
|
|
68
|
-
const fn = child.childForFieldName('function');
|
|
69
|
-
return fn ? fn.text : child.text?.split('(')[0] || '?';
|
|
70
|
-
}
|
|
71
|
-
if (child.type === 'identifier' || child.type === 'member_expression') {
|
|
72
|
-
return child.text;
|
|
73
|
-
}
|
|
129
|
+
if (!CHILD_EXPR_SKIP_KEYWORDS.has(child.type)) return truncate(child.text);
|
|
74
130
|
}
|
|
75
131
|
return truncate(node.text);
|
|
76
132
|
}
|
|
77
133
|
|
|
134
|
+
/**
|
|
135
|
+
* Count code points cheaply: skip the `[...s]` spread when `s.length` already
|
|
136
|
+
* decides the answer. Each code point is 1 or 2 UTF-16 units, so `.length < 2`
|
|
137
|
+
* implies `< 2` code points and `.length >= 3` already guarantees `>= 2` code
|
|
138
|
+
* points (worst case: one surrogate pair + one BMP char = 2 code points).
|
|
139
|
+
* Only `.length === 2` is genuinely ambiguous (could be a single surrogate
|
|
140
|
+
* pair = 1 code point, or two BMP chars = 2 code points) and needs the spread.
|
|
141
|
+
*/
|
|
142
|
+
function codePointCountAtLeast2(s: string): boolean {
|
|
143
|
+
const len = s.length;
|
|
144
|
+
if (len < 2) return false;
|
|
145
|
+
if (len >= 3) return true;
|
|
146
|
+
return [...s].length >= 2;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* Extract string content from a string-literal node, mirroring the native
|
|
151
|
+
* engine's `build_string_node` (`helpers.rs`). Returns `null` when the
|
|
152
|
+
* content is shorter than 2 Unicode code points.
|
|
153
|
+
*/
|
|
154
|
+
function extractStringContent(node: TreeSitterNode, cfg: AstStringConfig): string | null {
|
|
155
|
+
const raw = node.text ?? '';
|
|
156
|
+
const isRawString = node.type.includes('raw_string');
|
|
157
|
+
|
|
158
|
+
let s = raw;
|
|
159
|
+
s = trimLeadingChars(s, '@');
|
|
160
|
+
if (cfg.stringPrefixes) s = trimLeadingChars(s, cfg.stringPrefixes);
|
|
161
|
+
if (isRawString) s = trimLeadingChars(s, 'r#');
|
|
162
|
+
s = trimLeadingChars(s, cfg.quoteChars);
|
|
163
|
+
if (isRawString) s = trimTrailingChars(s, '#');
|
|
164
|
+
s = trimTrailingChars(s, cfg.quoteChars);
|
|
165
|
+
|
|
166
|
+
return codePointCountAtLeast2(s) ? s : null;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
// Per-astTypeMap cache for the set of node-types that map to kind 'new'.
|
|
170
|
+
// Computed once per unique astTypeMap reference (one per language) instead
|
|
171
|
+
// of once per file.
|
|
172
|
+
const _newTypesCache = new WeakMap<Record<string, string>, Set<string>>();
|
|
173
|
+
function newTypesFor(astTypeMap: Record<string, string>): Set<string> {
|
|
174
|
+
let s = _newTypesCache.get(astTypeMap);
|
|
175
|
+
if (s) return s;
|
|
176
|
+
s = new Set<string>();
|
|
177
|
+
for (const type in astTypeMap) {
|
|
178
|
+
if (astTypeMap[type] === 'new') s.add(type);
|
|
179
|
+
}
|
|
180
|
+
_newTypesCache.set(astTypeMap, s);
|
|
181
|
+
return s;
|
|
182
|
+
}
|
|
183
|
+
|
|
78
184
|
export function createAstStoreVisitor(
|
|
79
185
|
astTypeMap: Record<string, string>,
|
|
80
186
|
defs: Definition[],
|
|
81
187
|
relPath: string,
|
|
82
188
|
nodeIdMap: Map<string, number>,
|
|
189
|
+
stringConfig: AstStringConfig = DEFAULT_STRING_CONFIG,
|
|
190
|
+
stopRecurseKinds: ReadonlySet<string> = new Set(),
|
|
83
191
|
): Visitor {
|
|
84
192
|
const rows: AstStoreRow[] = [];
|
|
85
193
|
const matched = new Set<number>();
|
|
194
|
+
const newTypes = newTypesFor(astTypeMap);
|
|
195
|
+
// When nodeIdMap is empty, parentNodeId resolution is wasted work — the
|
|
196
|
+
// worker passes an empty map and the main thread re-resolves against its
|
|
197
|
+
// own DB-populated map in features/ast.ts::collectFileAstRows. Skip the
|
|
198
|
+
// findParentDef linear scan in that case.
|
|
199
|
+
const skipParentLookup = nodeIdMap.size === 0;
|
|
86
200
|
|
|
87
201
|
function findParentDef(line: number): Definition | null {
|
|
88
202
|
let best: Definition | null = null;
|
|
@@ -97,6 +211,7 @@ export function createAstStoreVisitor(
|
|
|
97
211
|
}
|
|
98
212
|
|
|
99
213
|
function resolveParentNodeId(line: number): number | null {
|
|
214
|
+
if (skipParentLookup) return null;
|
|
100
215
|
const parentDef = findParentDef(line);
|
|
101
216
|
if (!parentDef) return null;
|
|
102
217
|
return nodeIdMap.get(`${parentDef.name}|${parentDef.kind}|${parentDef.line}`) || null;
|
|
@@ -106,12 +221,15 @@ export function createAstStoreVisitor(
|
|
|
106
221
|
type KindHandler = (node: TreeSitterNode) => NameTextResult;
|
|
107
222
|
|
|
108
223
|
const kindHandlers: Record<string, KindHandler> = {
|
|
109
|
-
new: (node) => ({ name:
|
|
110
|
-
throw: (node) => ({
|
|
111
|
-
|
|
224
|
+
new: (node) => ({ name: extractConstructorName(node), text: truncate(node.text) }),
|
|
225
|
+
throw: (node) => ({
|
|
226
|
+
name: extractThrowName(node, newTypes),
|
|
227
|
+
text: extractChildExpressionText(node),
|
|
228
|
+
}),
|
|
229
|
+
await: (node) => ({ name: extractAwaitName(node), text: extractChildExpressionText(node) }),
|
|
112
230
|
string: (node) => {
|
|
113
|
-
const content = node
|
|
114
|
-
if (content
|
|
231
|
+
const content = extractStringContent(node, stringConfig);
|
|
232
|
+
if (content == null) return { name: null, text: null, skip: true };
|
|
115
233
|
return { name: truncate(content, 100), text: truncate(node.text) };
|
|
116
234
|
},
|
|
117
235
|
regex: (node) => ({ name: node.text || '?', text: truncate(node.text) }),
|
|
@@ -151,12 +269,25 @@ export function createAstStoreVisitor(
|
|
|
151
269
|
// unrelated subtree. The parent call's skipChildren handles the intended case.
|
|
152
270
|
if (matched.has(node.id)) return;
|
|
153
271
|
|
|
272
|
+
// Gate with `hasOwn` because plain-object lookup walks Object.prototype:
|
|
273
|
+
// tree-sitter node types like `constructor` (Haskell sum-types: Left,
|
|
274
|
+
// Right) would otherwise resolve to `Object.prototype.constructor` (the
|
|
275
|
+
// Object() function), which then crashes the worker boundary with
|
|
276
|
+
// "function Object() { [native code] } could not be cloned" when the
|
|
277
|
+
// resulting astNodes row is structured-cloned back to the main thread.
|
|
278
|
+
if (!Object.hasOwn(astTypeMap, node.type)) return;
|
|
154
279
|
const kind = astTypeMap[node.type];
|
|
155
280
|
if (!kind) return;
|
|
156
281
|
|
|
157
282
|
collectNode(node, kind);
|
|
158
283
|
|
|
159
|
-
|
|
284
|
+
// Mirror the native walker's recursion policy. In JS/TS, the native
|
|
285
|
+
// javascript.rs walker returns after collecting `new` or `throw` to
|
|
286
|
+
// avoid double-counting the wrapped expression (e.g. `throw new
|
|
287
|
+
// Error('x')` emits one `throw` row, not throw+new+string). Other
|
|
288
|
+
// languages go through helpers.rs::walk_ast_nodes_with_config_depth
|
|
289
|
+
// which always recurses — so `stopRecurseKinds` is empty for them.
|
|
290
|
+
if (stopRecurseKinds.has(kind)) {
|
|
160
291
|
return { skipChildren: true };
|
|
161
292
|
}
|
|
162
293
|
},
|
|
@@ -28,6 +28,16 @@ export class PipelineContext {
|
|
|
28
28
|
engineOpts!: EngineOpts;
|
|
29
29
|
engineName!: 'native' | 'wasm';
|
|
30
30
|
engineVersion!: string | null;
|
|
31
|
+
/**
|
|
32
|
+
* The version reported by the native binary itself (CARGO_PKG_VERSION at
|
|
33
|
+
* build time), as opposed to `engineVersion` which prefers the platform
|
|
34
|
+
* package.json. The Rust orchestrator's check_version_mismatch compares
|
|
35
|
+
* `build_meta.engine_version` against CARGO_PKG_VERSION, so build_meta
|
|
36
|
+
* writes must use this value to avoid a perpetual full-rebuild loop when
|
|
37
|
+
* the binary and platform package.json drift apart (e.g., CI hot-swap
|
|
38
|
+
* via ci-install-native.mjs — #1066).
|
|
39
|
+
*/
|
|
40
|
+
nativeBinaryVersion!: string | null;
|
|
31
41
|
aliases!: PathAliases;
|
|
32
42
|
incremental!: boolean;
|
|
33
43
|
forceFullRebuild: boolean = false;
|
|
@@ -222,12 +222,17 @@ export function fileHash(content: string): string {
|
|
|
222
222
|
}
|
|
223
223
|
|
|
224
224
|
/**
|
|
225
|
-
* Stat a file, returning {
|
|
225
|
+
* Stat a file, returning { mtime, size } or null on error.
|
|
226
|
+
*
|
|
227
|
+
* `mtime` is `Math.floor(stat.mtimeMs)` so it matches the integer column
|
|
228
|
+
* stored in the DB. Floor-once-here keeps every consumer honest: storing or
|
|
229
|
+
* comparing a non-floored `mtimeMs` against the integer DB column would cause
|
|
230
|
+
* spurious fast-skip misses on the next build.
|
|
226
231
|
*/
|
|
227
|
-
export function fileStat(filePath: string): {
|
|
232
|
+
export function fileStat(filePath: string): { mtime: number; size: number } | null {
|
|
228
233
|
try {
|
|
229
234
|
const s = fs.statSync(filePath);
|
|
230
|
-
return {
|
|
235
|
+
return { mtime: Math.floor(s.mtimeMs), size: s.size };
|
|
231
236
|
} catch {
|
|
232
237
|
return null;
|
|
233
238
|
}
|