gitnexus 1.6.9 → 1.6.10-rc.10
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 +126 -41
- package/dist/cli/ai-context.d.ts +1 -1
- package/dist/cli/ai-context.js +1 -1
- package/dist/cli/analyze.js +106 -3
- package/dist/cli/cli-message.d.ts +1 -1
- package/dist/cli/doctor.d.ts +5 -0
- package/dist/cli/doctor.js +41 -3
- package/dist/cli/editor-targets.d.ts +17 -2
- package/dist/cli/editor-targets.js +44 -0
- package/dist/cli/embeddings.d.ts +12 -0
- package/dist/cli/embeddings.js +49 -0
- package/dist/cli/help-i18n.js +4 -0
- package/dist/cli/i18n/en.d.ts +5 -1
- package/dist/cli/i18n/en.js +5 -1
- package/dist/cli/i18n/resources.d.ts +9 -1
- package/dist/cli/i18n/zh-CN.d.ts +4 -0
- package/dist/cli/i18n/zh-CN.js +5 -1
- package/dist/cli/index.js +12 -1
- package/dist/cli/setup.js +204 -28
- package/dist/cli/uninstall.js +70 -16
- package/dist/core/embeddings/embedder.js +16 -2
- package/dist/core/embeddings/http-client.d.ts +28 -1
- package/dist/core/embeddings/http-client.js +107 -15
- package/dist/core/embeddings/node-module-compat.d.ts +23 -0
- package/dist/core/embeddings/node-module-compat.js +23 -0
- package/dist/core/embeddings/onnxruntime-common-resolver.js +6 -3
- package/dist/core/embeddings/onnxruntime-node-resolver.js +31 -5
- package/dist/core/embeddings/runtime-install.d.ts +119 -0
- package/dist/core/embeddings/runtime-install.js +372 -0
- package/dist/core/embeddings/runtime-support.d.ts +48 -17
- package/dist/core/embeddings/runtime-support.js +96 -0
- package/dist/core/group/extractors/http-patterns/python.js +246 -108
- package/dist/core/group/extractors/manifest-extractor.js +4 -0
- package/dist/core/ingestion/cobol/jcl-processor.js +9 -8
- package/dist/core/ingestion/cobol-processor.js +27 -26
- package/dist/core/ingestion/community-processor.d.ts +51 -1
- package/dist/core/ingestion/community-processor.js +316 -63
- package/dist/core/ingestion/emit-references.js +3 -2
- package/dist/core/ingestion/markdown-processor.js +3 -2
- package/dist/core/ingestion/parsing-processor.d.ts +3 -1
- package/dist/core/ingestion/parsing-processor.js +4 -0
- package/dist/core/ingestion/pipeline-phases/parse-impl.js +42 -0
- package/dist/core/ingestion/route-extractors/constant-resolver.d.ts +83 -0
- package/dist/core/ingestion/route-extractors/constant-resolver.js +131 -0
- package/dist/core/ingestion/route-extractors/python-const-resolver.d.ts +75 -0
- package/dist/core/ingestion/route-extractors/python-const-resolver.js +299 -0
- package/dist/core/ingestion/tree-sitter-queries.d.ts +1 -1
- package/dist/core/ingestion/tree-sitter-queries.js +12 -1
- package/dist/core/ingestion/utils/line-base.d.ts +20 -0
- package/dist/core/ingestion/utils/line-base.js +20 -0
- package/dist/core/ingestion/utils/symbol-labels.d.ts +21 -0
- package/dist/core/ingestion/utils/symbol-labels.js +45 -0
- package/dist/core/ingestion/workers/parse-worker.d.ts +34 -0
- package/dist/core/ingestion/workers/parse-worker.js +39 -4
- package/dist/core/ingestion/workers/result-merge.js +4 -0
- package/dist/core/lbug/csv-generator.js +8 -2
- package/dist/core/lbug/extension-load-error.d.ts +67 -0
- package/dist/core/lbug/extension-load-error.js +320 -0
- package/dist/core/lbug/extension-loader.d.ts +17 -2
- package/dist/core/lbug/extension-loader.js +38 -13
- package/dist/core/lbug/lbug-adapter.js +7 -16
- package/dist/core/lbug/native-check.d.ts +22 -0
- package/dist/core/lbug/native-check.js +66 -0
- package/dist/core/lbug/pool-adapter.js +10 -1
- package/dist/core/lbug/sidecar-recovery.d.ts +32 -0
- package/dist/core/lbug/sidecar-recovery.js +91 -7
- package/dist/core/platform/capabilities.js +27 -1
- package/dist/core/run-analyze.js +33 -6
- package/dist/core/search/fts-indexes.d.ts +7 -0
- package/dist/core/search/fts-indexes.js +37 -0
- package/dist/mcp/core/embedder.js +16 -2
- package/dist/mcp/local/line-display.d.ts +22 -0
- package/dist/mcp/local/line-display.js +3 -0
- package/dist/mcp/local/local-backend.d.ts +14 -0
- package/dist/mcp/local/local-backend.js +63 -19
- package/dist/mcp/local/pdg-impact.d.ts +5 -3
- package/dist/mcp/local/pdg-impact.js +5 -2
- package/dist/mcp/resources.js +1 -0
- package/dist/mcp/tools.js +1 -1
- package/dist/server/api.js +2 -2
- package/dist/storage/parse-cache.js +1 -1
- package/dist/storage/repo-manager.d.ts +6 -1
- package/dist/storage/repo-manager.js +6 -1
- package/hooks/antigravity/gitnexus-antigravity-hook.cjs +50 -3
- package/hooks/claude/gitnexus-hook.cjs +59 -9
- package/package.json +5 -3
- package/scripts/cross-platform-tests.ts +26 -0
- package/scripts/ensure-fts.ts +32 -0
- package/scripts/install-duckdb-extension.mjs +85 -24
- package/scripts/run-cross-platform.ts +46 -6
- package/scripts/shard-arg.ts +30 -0
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Convert a 1-based source line number to the 0-based convention used by
|
|
3
|
+
* GraphNode `startLine`/`endLine`.
|
|
4
|
+
*
|
|
5
|
+
* The graph layer stores line numbers 0-based (tree-sitter `startPosition.row`),
|
|
6
|
+
* and this is load-bearing: the taint/PDG/CFG join and the MCP consumers all add
|
|
7
|
+
* `+ 1` to recover 1-based (see `summary-harvest-driver.ts` — "Function/Method
|
|
8
|
+
* node startLine is 0-based"). Most emitters get 0-based for free from
|
|
9
|
+
* tree-sitter. The exceptions are the regex-based COBOL/JCL processors (their
|
|
10
|
+
* parsers use `lineNum = i + 1`) and the scope-capture path (`Capture` ranges
|
|
11
|
+
* are 1-based per RFC §2.1). Those must convert to 0-based when they build a
|
|
12
|
+
* graph node, or the exact-content slice in `csv-generator.ts` drops the
|
|
13
|
+
* symbol's declaration line (#2379) and reported line numbers are off (#2377).
|
|
14
|
+
*
|
|
15
|
+
* Apply this ONLY at the graph-node `startLine:`/`endLine:` assignment. The
|
|
16
|
+
* parser-internal 1-based values (`.line`, `prog.startLine`) stay 1-based —
|
|
17
|
+
* they feed `L${line}` node/edge IDs and line-range containment checks that
|
|
18
|
+
* must not shift. The clamp guards degenerate inputs (line 0 / empty files).
|
|
19
|
+
*/
|
|
20
|
+
export const toZeroBasedLine = (oneBasedLine) => Math.max(0, oneBasedLine - 1);
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import type { NodeLabel } from '../../../_shared/index.js';
|
|
2
|
+
/**
|
|
3
|
+
* Graph-node labels that represent a resolvable code symbol — a definition with
|
|
4
|
+
* its own source span (function, type, member, module-like container).
|
|
5
|
+
*
|
|
6
|
+
* These get EXACT source-span content in the FTS index: `csv-generator.ts`
|
|
7
|
+
* slices exactly `[startLine, endLine]` for them (no ±2 padding), while every
|
|
8
|
+
* other label keeps the context window. That exactness depends on the 0-based
|
|
9
|
+
* `startLine`/`endLine` invariant enforced by `line-base.ts` — the slice is only
|
|
10
|
+
* correct because all emitters store 0-based lines. Keep the two together.
|
|
11
|
+
*
|
|
12
|
+
* Single source of truth so the set can't silently drift the way the inline copy
|
|
13
|
+
* did in #2379.
|
|
14
|
+
*
|
|
15
|
+
* NOTE: `group/extractors/manifest-extractor.ts`'s `CUSTOM_CONTRACT_RESOLVE_QUERY`
|
|
16
|
+
* carries a near-identical hand-list that is intentionally a SUBSET — it excludes
|
|
17
|
+
* `Namespace`, `Variable`, `Module`. Unifying the two needs a contract-resolution
|
|
18
|
+
* behavior check (would widen which nodes resolve as contract symbols), so it is
|
|
19
|
+
* deliberately left separate for now.
|
|
20
|
+
*/
|
|
21
|
+
export declare const SYMBOL_NODE_LABELS: ReadonlySet<NodeLabel>;
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Graph-node labels that represent a resolvable code symbol — a definition with
|
|
3
|
+
* its own source span (function, type, member, module-like container).
|
|
4
|
+
*
|
|
5
|
+
* These get EXACT source-span content in the FTS index: `csv-generator.ts`
|
|
6
|
+
* slices exactly `[startLine, endLine]` for them (no ±2 padding), while every
|
|
7
|
+
* other label keeps the context window. That exactness depends on the 0-based
|
|
8
|
+
* `startLine`/`endLine` invariant enforced by `line-base.ts` — the slice is only
|
|
9
|
+
* correct because all emitters store 0-based lines. Keep the two together.
|
|
10
|
+
*
|
|
11
|
+
* Single source of truth so the set can't silently drift the way the inline copy
|
|
12
|
+
* did in #2379.
|
|
13
|
+
*
|
|
14
|
+
* NOTE: `group/extractors/manifest-extractor.ts`'s `CUSTOM_CONTRACT_RESOLVE_QUERY`
|
|
15
|
+
* carries a near-identical hand-list that is intentionally a SUBSET — it excludes
|
|
16
|
+
* `Namespace`, `Variable`, `Module`. Unifying the two needs a contract-resolution
|
|
17
|
+
* behavior check (would widen which nodes resolve as contract symbols), so it is
|
|
18
|
+
* deliberately left separate for now.
|
|
19
|
+
*/
|
|
20
|
+
export const SYMBOL_NODE_LABELS = new Set([
|
|
21
|
+
'Function',
|
|
22
|
+
'Method',
|
|
23
|
+
'Class',
|
|
24
|
+
'Interface',
|
|
25
|
+
'CodeElement',
|
|
26
|
+
'Struct',
|
|
27
|
+
'Enum',
|
|
28
|
+
'Macro',
|
|
29
|
+
'Typedef',
|
|
30
|
+
'Union',
|
|
31
|
+
'Namespace',
|
|
32
|
+
'Trait',
|
|
33
|
+
'Impl',
|
|
34
|
+
'TypeAlias',
|
|
35
|
+
'Const',
|
|
36
|
+
'Static',
|
|
37
|
+
'Variable',
|
|
38
|
+
'Property',
|
|
39
|
+
'Record',
|
|
40
|
+
'Delegate',
|
|
41
|
+
'Annotation',
|
|
42
|
+
'Constructor',
|
|
43
|
+
'Template',
|
|
44
|
+
'Module',
|
|
45
|
+
]);
|
|
@@ -113,6 +113,21 @@ export interface ExtractedDecoratorRoute {
|
|
|
113
113
|
* participate in `include_router(prefix=...)` joining.
|
|
114
114
|
*/
|
|
115
115
|
decoratorReceiver?: string;
|
|
116
|
+
/**
|
|
117
|
+
* Raw text of a non-literal decorator path argument (`#2391`), e.g.
|
|
118
|
+
* `API_V1_WIDGETS_GET` or `API_V1 + "/widgets"`. Present only when the
|
|
119
|
+
* decorator's first argument was NOT a string literal, in which case
|
|
120
|
+
* `routePath` is empty and parse-impl resolves the constant cross-file (or
|
|
121
|
+
* drops the route on failure). Absent for ordinary string-literal routes.
|
|
122
|
+
*/
|
|
123
|
+
routePathExpr?: string;
|
|
124
|
+
/**
|
|
125
|
+
* Parsed operand list for {@link routePathExpr} — an identifier reference or a
|
|
126
|
+
* `+`-concatenation, in the {@link Operand} shape the constant resolver folds.
|
|
127
|
+
* `undefined` when the expression was not a foldable string form (e.g. an
|
|
128
|
+
* attribute access), in which case the route is dropped at resolution.
|
|
129
|
+
*/
|
|
130
|
+
routePathOperands?: Operand[];
|
|
116
131
|
/**
|
|
117
132
|
* FastAPI `app.include_router(prefix='/x')` prefix that applies to
|
|
118
133
|
* this route. Filled by parse-impl after cross-file aggregation; the
|
|
@@ -131,6 +146,17 @@ export interface ExtractedDecoratorRoute {
|
|
|
131
146
|
*/
|
|
132
147
|
handlerName?: string;
|
|
133
148
|
}
|
|
149
|
+
/**
|
|
150
|
+
* One Python file's module-level string constants (#2391), used by parse-impl to
|
|
151
|
+
* resolve non-literal decorator route paths cross-file. `constants` is the
|
|
152
|
+
* `Map`-based {@link ModuleConstants} shape — it survives the worker
|
|
153
|
+
* `postMessage` boundary (structured clone) and the parse cache
|
|
154
|
+
* (`mapReplacer`/`mapReviver`) without conversion.
|
|
155
|
+
*/
|
|
156
|
+
export interface ExtractedModuleConstants {
|
|
157
|
+
filePath: string;
|
|
158
|
+
constants: ModuleConstants;
|
|
159
|
+
}
|
|
134
160
|
export interface ExtractedToolDef {
|
|
135
161
|
filePath: string;
|
|
136
162
|
toolName: string;
|
|
@@ -211,6 +237,13 @@ export interface ParseWorkerResult {
|
|
|
211
237
|
* predate the field; consumers must guard with `if (… ?? [])`).
|
|
212
238
|
*/
|
|
213
239
|
routerModuleAliases?: ExtractedRouterModuleAlias[];
|
|
240
|
+
/**
|
|
241
|
+
* Per-file Python module-level string constants (#2391). parse-impl aggregates
|
|
242
|
+
* these into a repo-wide, file-path-keyed map and resolves each decorator
|
|
243
|
+
* route's non-literal path expression against it. Optional for cache backward
|
|
244
|
+
* compatibility (older entries predate the field; consumers guard with `?? []`).
|
|
245
|
+
*/
|
|
246
|
+
moduleConstants?: ExtractedModuleConstants[];
|
|
214
247
|
toolDefs: ExtractedToolDef[];
|
|
215
248
|
ormQueries: ExtractedORMQuery[];
|
|
216
249
|
constructorBindings: FileConstructorBindings[];
|
|
@@ -256,3 +289,4 @@ export interface ParseWorkerInput {
|
|
|
256
289
|
* Appends results to the provided array (avoids allocation when no matches).
|
|
257
290
|
*/
|
|
258
291
|
export declare function extractORMQueries(filePath: string, content: string, out: ExtractedORMQuery[]): void;
|
|
292
|
+
import { type ModuleConstants, type Operand } from '../route-extractors/python-const-resolver.js';
|
|
@@ -751,6 +751,7 @@ export function extractORMQueries(filePath, content, out) {
|
|
|
751
751
|
// per file; this module does not re-export it. Downstream consumers
|
|
752
752
|
// import the function and its types directly from `route-extractors/`.
|
|
753
753
|
import { extractFastAPIRouterBindings } from '../route-extractors/fastapi-router-bindings.js';
|
|
754
|
+
import { extractPythonModuleConstants, parseConstOperands, } from '../route-extractors/python-const-resolver.js';
|
|
754
755
|
/**
|
|
755
756
|
* Report a non-fatal worker issue to the pool over IPC so a caught error is not
|
|
756
757
|
* invisible to the operator (#2264). The pool logs it on the main thread AND
|
|
@@ -986,6 +987,12 @@ const processFileGroup = (files, language, queryString, result, onFileProcessed)
|
|
|
986
987
|
if (captureMap['decorator'] && captureMap['decorator.name']) {
|
|
987
988
|
const decoratorName = captureMap['decorator.name'].text;
|
|
988
989
|
const decoratorArg = captureMap['decorator.arg']?.text;
|
|
990
|
+
// #2391: the first positional arg captured as either a string node
|
|
991
|
+
// (`arg_str`, present even for the empty-string literal `""` which has no
|
|
992
|
+
// `string_content`) or a non-literal expression (`arg_expr`: an
|
|
993
|
+
// identifier or a `+`-concatenation).
|
|
994
|
+
const decoratorArgStr = captureMap['decorator.arg_str'];
|
|
995
|
+
const decoratorArgExpr = captureMap['decorator.arg_expr'];
|
|
989
996
|
const decoratorReceiver = captureMap['decorator.receiver']?.text;
|
|
990
997
|
const decoratorNode = captureMap['decorator'];
|
|
991
998
|
// Store by the decorator's end line — the definition follows immediately after
|
|
@@ -994,19 +1001,39 @@ const processFileGroup = (files, language, queryString, result, onFileProcessed)
|
|
|
994
1001
|
arg: decoratorArg,
|
|
995
1002
|
});
|
|
996
1003
|
if (ROUTE_DECORATOR_NAMES.has(decoratorName)) {
|
|
997
|
-
const routePath = decoratorArg || '';
|
|
998
1004
|
const method = decoratorName.replace('Mapping', '').toUpperCase();
|
|
999
1005
|
const httpMethod = ['GET', 'POST', 'PUT', 'DELETE', 'PATCH'].includes(method)
|
|
1000
1006
|
? method
|
|
1001
1007
|
: 'GET';
|
|
1002
|
-
|
|
1008
|
+
const base = {
|
|
1003
1009
|
filePath: file.path,
|
|
1004
|
-
routePath,
|
|
1005
1010
|
httpMethod,
|
|
1006
1011
|
decoratorName,
|
|
1007
1012
|
lineNumber: decoratorNode.startPosition.row + lineOffset,
|
|
1008
1013
|
...(decoratorReceiver ? { decoratorReceiver } : {}),
|
|
1009
|
-
}
|
|
1014
|
+
};
|
|
1015
|
+
if (decoratorArgStr) {
|
|
1016
|
+
// String-literal path (the fast path, unchanged). Empty-string
|
|
1017
|
+
// literal `""` has no `string_content` → `decoratorArg` undefined →
|
|
1018
|
+
// routePath '' (a valid path under an APIRouter prefix).
|
|
1019
|
+
result.decoratorRoutes.push({ ...base, routePath: decoratorArg ?? '' });
|
|
1020
|
+
}
|
|
1021
|
+
else if (decoratorArgExpr) {
|
|
1022
|
+
// #2391 non-literal path (imported/composed constant). Emit the raw
|
|
1023
|
+
// expression + its operands for cross-file resolution in parse-impl;
|
|
1024
|
+
// `routePath` stays empty until resolved (or the route is dropped).
|
|
1025
|
+
const operands = decoratorArgExpr.type === 'identifier'
|
|
1026
|
+
? [{ kind: 'ref', name: decoratorArgExpr.text }]
|
|
1027
|
+
: parseConstOperands(decoratorArgExpr);
|
|
1028
|
+
result.decoratorRoutes.push({
|
|
1029
|
+
...base,
|
|
1030
|
+
routePath: '',
|
|
1031
|
+
routePathExpr: decoratorArgExpr.text,
|
|
1032
|
+
...(operands ? { routePathOperands: operands } : {}),
|
|
1033
|
+
});
|
|
1034
|
+
}
|
|
1035
|
+
// Otherwise the first arg is absent or an unsupported shape
|
|
1036
|
+
// (attribute access, call, …) → skip; never a phantom `POST /`.
|
|
1010
1037
|
}
|
|
1011
1038
|
// MCP/RPC tool detection: @mcp.tool(), @app.tool(), @server.tool()
|
|
1012
1039
|
if (decoratorName === 'tool') {
|
|
@@ -1839,6 +1866,14 @@ const processFileGroup = (files, language, queryString, result, onFileProcessed)
|
|
|
1839
1866
|
// came from a `@router.<verb>` decorator. Python-only.
|
|
1840
1867
|
if (language === SupportedLanguages.Python) {
|
|
1841
1868
|
extractFastAPIRouterBindings(file.path, parseContent, result.routerIncludes, result.routerImports, (result.routerModuleAliases ??= []), (result.routerConstructorPrefixes ??= []));
|
|
1869
|
+
// #2391: harvest module-level string constants + from-imports so parse-impl
|
|
1870
|
+
// can resolve non-literal decorator route paths cross-file. Only emit for
|
|
1871
|
+
// files that carry something resolvable (a constant definition or an import
|
|
1872
|
+
// binding) to keep the aggregate bounded on large repos.
|
|
1873
|
+
const constants = extractPythonModuleConstants(tree);
|
|
1874
|
+
if (constants.literals.size > 0 || constants.exprs.size > 0 || constants.imports.size > 0) {
|
|
1875
|
+
(result.moduleConstants ??= []).push({ filePath: file.path, constants });
|
|
1876
|
+
}
|
|
1842
1877
|
}
|
|
1843
1878
|
// Language-specific decorator route extraction via provider hook.
|
|
1844
1879
|
// The provider's extractDecoratorRoutes walks the AST for framework-specific
|
|
@@ -32,6 +32,10 @@ export const mergeResult = (target, src) => {
|
|
|
32
32
|
target.routerModuleAliases ??= [];
|
|
33
33
|
appendAll(target.routerModuleAliases, src.routerModuleAliases);
|
|
34
34
|
}
|
|
35
|
+
if (src.moduleConstants) {
|
|
36
|
+
target.moduleConstants ??= [];
|
|
37
|
+
appendAll(target.moduleConstants, src.moduleConstants);
|
|
38
|
+
}
|
|
35
39
|
if (src.springTypes) {
|
|
36
40
|
target.springTypes ??= [];
|
|
37
41
|
appendAll(target.springTypes, src.springTypes);
|
|
@@ -17,6 +17,7 @@ import path from 'path';
|
|
|
17
17
|
import { NODE_TABLES } from './schema.js';
|
|
18
18
|
import { RelPairRouter } from './rel-pair-routing.js';
|
|
19
19
|
import { parseTruthyEnv } from '../ingestion/utils/env.js';
|
|
20
|
+
import { SYMBOL_NODE_LABELS } from '../ingestion/utils/symbol-labels.js';
|
|
20
21
|
import { applyCjkSegmentationIfEnabled } from '../search/cjk-segmentation.js';
|
|
21
22
|
/**
|
|
22
23
|
* Deterministic output ordering — optional (out-of-core / windowed-resolve
|
|
@@ -187,6 +188,10 @@ class FileContentCache {
|
|
|
187
188
|
export const normalizeFtsText = (text) => text.replace(/[\r\n\t]+/g, ' ');
|
|
188
189
|
/** Composes both FTS-text transforms for the `description` column — one place for the six emission sites below to call, instead of repeating the composition. */
|
|
189
190
|
const formatFtsDescription = (description) => normalizeFtsText(applyCjkSegmentationIfEnabled(description));
|
|
191
|
+
// Labels that get exact source-span content (no ±2 window). Single source of
|
|
192
|
+
// truth in `symbol-labels.ts` — see there for why the exactness depends on the
|
|
193
|
+
// 0-based line invariant. Kept as a named alias to read intent at the use site.
|
|
194
|
+
const EXACT_SYMBOL_CONTENT_LABELS = SYMBOL_NODE_LABELS;
|
|
190
195
|
const extractContent = async (node, contentCache) => {
|
|
191
196
|
const filePath = node.properties.filePath;
|
|
192
197
|
const content = await contentCache.get(filePath);
|
|
@@ -209,8 +214,9 @@ const extractContent = async (node, contentCache) => {
|
|
|
209
214
|
if (startLine === undefined || endLine === undefined)
|
|
210
215
|
return '';
|
|
211
216
|
const lines = content.split('\n');
|
|
212
|
-
const
|
|
213
|
-
const
|
|
217
|
+
const exactSymbolContent = EXACT_SYMBOL_CONTENT_LABELS.has(node.label);
|
|
218
|
+
const start = Math.max(0, exactSymbolContent ? startLine : startLine - 2);
|
|
219
|
+
const end = Math.min(lines.length - 1, exactSymbolContent ? endLine : endLine + 2);
|
|
214
220
|
const snippet = lines.slice(start, end + 1).join('\n');
|
|
215
221
|
const MAX_SNIPPET = 5000;
|
|
216
222
|
const capped = snippet.length > MAX_SNIPPET ? snippet.slice(0, MAX_SNIPPET) + '\n... [truncated]' : snippet;
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
export type ExtensionLoadErrorKind = 'missing_file' | 'corrupt_file' | 'missing_dependency' | 'unknown';
|
|
2
|
+
export interface ExtensionLoadDiagnosis {
|
|
3
|
+
readonly kind: ExtensionLoadErrorKind;
|
|
4
|
+
/** Actionable, literal-English remedy suited to the class. */
|
|
5
|
+
readonly remedy: string;
|
|
6
|
+
}
|
|
7
|
+
/**
|
|
8
|
+
* On-disk file corruption / wrong-platform. FORCE INSTALL re-downloads.
|
|
9
|
+
* Kept byte-identical to `FILE_CORRUPTION_SIGNATURES` in
|
|
10
|
+
* scripts/install-duckdb-extension.mjs (that `.mjs` cannot import this `.ts`;
|
|
11
|
+
* the duplication is deliberate — the two serve different call sites). Note
|
|
12
|
+
* `/not a valid/i` already covers Windows error 193 ("is not a valid Win32
|
|
13
|
+
* application"), so a truncated Windows download is caught here, before the
|
|
14
|
+
* missing-dependency branch.
|
|
15
|
+
*/
|
|
16
|
+
export declare const FILE_CORRUPTION_SIGNATURES: readonly RegExp[];
|
|
17
|
+
/**
|
|
18
|
+
* Classify a collapsed LadybugDB LOAD error. Order is most-specific-first and is
|
|
19
|
+
* load-bearing: corrupt-file is tested before missing-dependency so a truncated
|
|
20
|
+
* Windows download (error 193, matched by `/not a valid/i`) routes to
|
|
21
|
+
* FORCE-reinstall rather than to the runtime-install remedy.
|
|
22
|
+
*/
|
|
23
|
+
export declare function classifyExtensionLoadError(reason: string | undefined | null): ExtensionLoadDiagnosis;
|
|
24
|
+
/** Well-formedness of the extension binary for the host platform + arch. */
|
|
25
|
+
export type ExtensionBinaryState = 'absent' | 'corrupt' | 'valid' | 'indeterminate';
|
|
26
|
+
/**
|
|
27
|
+
* Pull the extension file path out of lbug's load error. lbug's wrapper is
|
|
28
|
+
* English regardless of OS language — `Failed to load library: {path} which is
|
|
29
|
+
* needed by extension: {name}` (real lbug), or the quoted `Failed to load
|
|
30
|
+
* library '{path}': {reason}` variant — so the path is recoverable in any locale.
|
|
31
|
+
* Only paths ending in `.lbug_extension` are accepted, so a regex misfire can
|
|
32
|
+
* never point the inspector at an arbitrary file.
|
|
33
|
+
*/
|
|
34
|
+
export declare function extractExtensionPath(reason: string | undefined | null): string | null;
|
|
35
|
+
/**
|
|
36
|
+
* A structural verdict on a binary header. `indeterminate` means the probe could
|
|
37
|
+
* not prove validity OR corruption from what it read (e.g. the PE header sits past
|
|
38
|
+
* the BINARY_HEADER_BYTES window) — the caller defers to the string classifier
|
|
39
|
+
* rather than assert a false verdict.
|
|
40
|
+
*/
|
|
41
|
+
type HeaderVerdict = 'valid' | 'corrupt' | 'indeterminate';
|
|
42
|
+
/**
|
|
43
|
+
* Decide whether a binary header is a well-formed shared library for the given
|
|
44
|
+
* platform + architecture — using only the file's structure, no localized text.
|
|
45
|
+
* Pure and injectable (platform/arch as params) so every format+arch combination
|
|
46
|
+
* is unit-testable regardless of the host it runs on.
|
|
47
|
+
*/
|
|
48
|
+
export declare function classifyBinaryHeader(buf: Buffer, bytesRead: number, platform: NodeJS.Platform, arch: string): HeaderVerdict;
|
|
49
|
+
/**
|
|
50
|
+
* Best-effort language-independent inspection of the extension file. Reads the
|
|
51
|
+
* header and classifies it; never throws — a missing file is `absent`, an
|
|
52
|
+
* unreadable one is `indeterminate`.
|
|
53
|
+
*/
|
|
54
|
+
export declare function inspectExtensionBinary(extensionPath: string | null | undefined): ExtensionBinaryState;
|
|
55
|
+
/**
|
|
56
|
+
* Diagnose a LadybugDB load failure, preferring a LANGUAGE-INDEPENDENT structural
|
|
57
|
+
* check of the extension binary over the localized error text:
|
|
58
|
+
* - file absent → missing_file
|
|
59
|
+
* - present but malformed → corrupt_file (bad magic / wrong architecture)
|
|
60
|
+
* - present and well-formed → missing_dependency (a valid binary the loader rejected)
|
|
61
|
+
* The path comes from lbug's own English wrapper, so this holds in any OS display
|
|
62
|
+
* language. When the file cannot be located or read, it falls back to the string
|
|
63
|
+
* classifier (which still carries the language-independent hedged fallback). This
|
|
64
|
+
* is the entry point every surface should call.
|
|
65
|
+
*/
|
|
66
|
+
export declare function diagnoseExtensionLoad(reason: string | undefined | null): ExtensionLoadDiagnosis;
|
|
67
|
+
export {};
|
|
@@ -0,0 +1,320 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Classify a LadybugDB `LOAD EXTENSION` failure into one of four actionable
|
|
3
|
+
* classes and produce an accurate, literal-English remedy.
|
|
4
|
+
*
|
|
5
|
+
* Background (#2374): PR #2375 made the real LadybugDB LOAD error visible
|
|
6
|
+
* (instead of a false "not pre-installed" message). The rc.4 reproduction then
|
|
7
|
+
* showed the remaining defect — on Windows the extension file downloads and
|
|
8
|
+
* INSTALLs fine, but `LoadLibrary` fails with error 126 ("the specified module
|
|
9
|
+
* could not be found" / `找不到指定的模块`) because the extension dynamically
|
|
10
|
+
* imports OpenSSL 3 / MSVC 14 DLLs that ship nowhere. For that class, telling
|
|
11
|
+
* the user to reinstall/redownload is wrong — the file is fine; a *runtime
|
|
12
|
+
* dependency* is missing. This module decides which class an error is so each
|
|
13
|
+
* surface (doctor, --repair-fts, the analyze degrade warning, and
|
|
14
|
+
* ftsDegradedWarning) can emit the right remedy instead of a one-size-fits-all
|
|
15
|
+
* "reinstall over the network".
|
|
16
|
+
*
|
|
17
|
+
* `classifyExtensionLoadError` is pure string logic — no `@ladybugdb/core`
|
|
18
|
+
* import, no filesystem — which keeps `native-check.ts` free of a static lbug
|
|
19
|
+
* dependency. `diagnoseExtensionLoad` layers a LANGUAGE-INDEPENDENT structural
|
|
20
|
+
* check on top: it pulls the extension's file path out of lbug's own (English)
|
|
21
|
+
* wrapper and inspects the binary header directly (PE/ELF/Mach-O magic +
|
|
22
|
+
* architecture), so corrupt-vs-valid is decided by the file itself, not by the
|
|
23
|
+
* localized OS error tail. It reads the file (node:fs core module only, still no
|
|
24
|
+
* lbug) and never throws — any read failure degrades to the string classifier.
|
|
25
|
+
*/
|
|
26
|
+
import { closeSync, openSync, readSync } from 'node:fs';
|
|
27
|
+
/** LadybugDB says the extension file was never installed. INSTALL can heal it. */
|
|
28
|
+
const MISSING_FILE_SIGNATURES = [
|
|
29
|
+
/has not been installed/i,
|
|
30
|
+
/not been installed/i,
|
|
31
|
+
];
|
|
32
|
+
/**
|
|
33
|
+
* On-disk file corruption / wrong-platform. FORCE INSTALL re-downloads.
|
|
34
|
+
* Kept byte-identical to `FILE_CORRUPTION_SIGNATURES` in
|
|
35
|
+
* scripts/install-duckdb-extension.mjs (that `.mjs` cannot import this `.ts`;
|
|
36
|
+
* the duplication is deliberate — the two serve different call sites). Note
|
|
37
|
+
* `/not a valid/i` already covers Windows error 193 ("is not a valid Win32
|
|
38
|
+
* application"), so a truncated Windows download is caught here, before the
|
|
39
|
+
* missing-dependency branch.
|
|
40
|
+
*/
|
|
41
|
+
// Exported so a parity test can assert this stays byte-identical to the copy in
|
|
42
|
+
// scripts/install-duckdb-extension.mjs (that `.mjs` cannot import this `.ts`), #2383 F5b.
|
|
43
|
+
export const FILE_CORRUPTION_SIGNATURES = [
|
|
44
|
+
/invalid elf/i,
|
|
45
|
+
/file too short/i,
|
|
46
|
+
/not a valid/i,
|
|
47
|
+
/bad magic/i,
|
|
48
|
+
/wrong architecture/i,
|
|
49
|
+
/mach-o/i,
|
|
50
|
+
/truncat/i,
|
|
51
|
+
];
|
|
52
|
+
/**
|
|
53
|
+
* A *transitive dependency* of the extension is missing — the file loaded far
|
|
54
|
+
* enough to be found, but a library it needs is absent. Reinstalling the
|
|
55
|
+
* extension is a no-op for this class.
|
|
56
|
+
*
|
|
57
|
+
* WINDOWS CATCH-ALL GUARD (adversarial review): LadybugDB wraps *every* Windows
|
|
58
|
+
* load failure in `Failed to load library … which is needed by extension`, so
|
|
59
|
+
* that generic wrapper must NOT be sufficient — otherwise error 127 (wrong
|
|
60
|
+
* OpenSSL minor / unresolved procedure), 5 (AV/permission lock), and 1114
|
|
61
|
+
* (dependency DllMain failure) would all be mislabeled `missing_dependency` and
|
|
62
|
+
* told to install a runtime, the opposite of their real fix. We key strictly on
|
|
63
|
+
* the specific error-126 tail. Linux/macOS loaders name the missing library
|
|
64
|
+
* directly, so their signals are unambiguous.
|
|
65
|
+
*
|
|
66
|
+
* Localized Windows tails we do not enumerate (French, German, Japanese, …) and
|
|
67
|
+
* mojibake renderings of the Chinese text won't match here — but they still
|
|
68
|
+
* carry lbug's language-independent `Failed to load library` wrapper, so they
|
|
69
|
+
* are caught by the hedged fallback (LOAD_FAILURE_WRAPPER) with a non-committal
|
|
70
|
+
* remedy, never a wrong confident "reinstall" instruction.
|
|
71
|
+
*/
|
|
72
|
+
const WINDOWS_MISSING_DEPENDENCY_SIGNATURES = [
|
|
73
|
+
/找不到指定的模块/,
|
|
74
|
+
/specified module could not be found/i,
|
|
75
|
+
];
|
|
76
|
+
const POSIX_MISSING_DEPENDENCY_SIGNATURES = [
|
|
77
|
+
/cannot open shared object file/i, // Linux ld.so
|
|
78
|
+
/image not found/i, // macOS dyld
|
|
79
|
+
/library not loaded/i, // macOS dyld
|
|
80
|
+
];
|
|
81
|
+
/**
|
|
82
|
+
* LadybugDB's own English wrapper for a dlopen/LoadLibrary failure
|
|
83
|
+
* (extension.cpp: `Failed to load library: {path} which is needed by extension:
|
|
84
|
+
* {name}`). It is emitted for EVERY extension load failure regardless of the OS
|
|
85
|
+
* display language — the only localized part is the OS-error tail after it. So
|
|
86
|
+
* it is the language-independent fallback signal once the specific tails miss: a
|
|
87
|
+
* French/German/Japanese Windows 126 has a localized tail we cannot enumerate,
|
|
88
|
+
* but it still carries this wrapper. See HEDGED_LOAD_FAILURE_REMEDY.
|
|
89
|
+
*/
|
|
90
|
+
const LOAD_FAILURE_WRAPPER = /failed to load library/i;
|
|
91
|
+
const MISSING_FILE_REMEDY = 'The FTS extension is not installed. Re-run with network access and ' +
|
|
92
|
+
'GITNEXUS_LBUG_EXTENSION_INSTALL=auto (or `gitnexus analyze --repair-fts`) to download it.';
|
|
93
|
+
const CORRUPT_FILE_REMEDY = 'The FTS extension file is present but unreadable (corrupt, truncated, or built for another ' +
|
|
94
|
+
'platform). Re-download it with network access and GITNEXUS_LBUG_EXTENSION_INSTALL=auto ' +
|
|
95
|
+
'(`gitnexus analyze --repair-fts`).';
|
|
96
|
+
// Single source of truth for the VC++ runtime-install pointer, shared by the
|
|
97
|
+
// Windows-126 and structural missing-dependency remedies so the name/URL cannot
|
|
98
|
+
// drift between them (#2383 F5).
|
|
99
|
+
const VC_REDIST_INSTALL_HINT = 'the Microsoft Visual C++ 2015-2022 Redistributable (x64) from ' +
|
|
100
|
+
'https://aka.ms/vs/17/release/vc_redist.x64.exe';
|
|
101
|
+
// MSVC-first per DuckDB's canonical answer for this exact error; OpenSSL second.
|
|
102
|
+
const WINDOWS_MISSING_DEPENDENCY_REMEDY = 'The FTS extension is present but a required runtime library is missing (Windows error 126). ' +
|
|
103
|
+
'Reinstalling the extension will NOT help. Install ' +
|
|
104
|
+
VC_REDIST_INSTALL_HINT +
|
|
105
|
+
'; if the error persists, the extension also needs OpenSSL 3 ' +
|
|
106
|
+
'(libcrypto-3-x64.dll / libssl-3-x64.dll) on the DLL search path.';
|
|
107
|
+
const POSIX_MISSING_DEPENDENCY_REMEDY = 'The FTS extension is present but a shared library it depends on could not be loaded (named in ' +
|
|
108
|
+
'the error above). Reinstalling the extension will NOT help — install that library or add it to ' +
|
|
109
|
+
'your loader search path.';
|
|
110
|
+
// Language-independent fallback: we know the extension failed to load, but the
|
|
111
|
+
// OS-error tail is in a locale we did not enumerate, so we cannot say which class
|
|
112
|
+
// it is. Hedge honestly — point at the user's own localized error and give both
|
|
113
|
+
// branches — rather than confidently prescribing the wrong single fix. The clean
|
|
114
|
+
// long-term fix is upstream: have LadybugDB include the numeric GetLastError/errno
|
|
115
|
+
// in the message (as it already does elsewhere), so this becomes a code match.
|
|
116
|
+
const HEDGED_LOAD_FAILURE_REMEDY = 'The FTS extension file was found but could not be loaded — see the "Error:" text above (shown ' +
|
|
117
|
+
"in your system's language). Reinstalling usually will not help. If it names a missing module or " +
|
|
118
|
+
'library, install the required runtime (on Windows: the Microsoft Visual C++ 2015-2022 ' +
|
|
119
|
+
'Redistributable x64 and OpenSSL 3); if it names a corrupt or invalid file, run ' +
|
|
120
|
+
'`gitnexus analyze --repair-fts` to re-download.';
|
|
121
|
+
const UNKNOWN_REMEDY = 'The FTS extension failed to load for an unrecognized reason. Run `gitnexus doctor` for live ' +
|
|
122
|
+
'FTS status and verify the extension file and platform.';
|
|
123
|
+
const matchesAny = (reason, signatures) => signatures.some((re) => re.test(reason));
|
|
124
|
+
/**
|
|
125
|
+
* Classify a collapsed LadybugDB LOAD error. Order is most-specific-first and is
|
|
126
|
+
* load-bearing: corrupt-file is tested before missing-dependency so a truncated
|
|
127
|
+
* Windows download (error 193, matched by `/not a valid/i`) routes to
|
|
128
|
+
* FORCE-reinstall rather than to the runtime-install remedy.
|
|
129
|
+
*/
|
|
130
|
+
export function classifyExtensionLoadError(reason) {
|
|
131
|
+
const text = reason ?? '';
|
|
132
|
+
if (matchesAny(text, MISSING_FILE_SIGNATURES)) {
|
|
133
|
+
return { kind: 'missing_file', remedy: MISSING_FILE_REMEDY };
|
|
134
|
+
}
|
|
135
|
+
if (matchesAny(text, FILE_CORRUPTION_SIGNATURES)) {
|
|
136
|
+
return { kind: 'corrupt_file', remedy: CORRUPT_FILE_REMEDY };
|
|
137
|
+
}
|
|
138
|
+
if (matchesAny(text, WINDOWS_MISSING_DEPENDENCY_SIGNATURES)) {
|
|
139
|
+
return { kind: 'missing_dependency', remedy: WINDOWS_MISSING_DEPENDENCY_REMEDY };
|
|
140
|
+
}
|
|
141
|
+
if (matchesAny(text, POSIX_MISSING_DEPENDENCY_SIGNATURES)) {
|
|
142
|
+
return { kind: 'missing_dependency', remedy: POSIX_MISSING_DEPENDENCY_REMEDY };
|
|
143
|
+
}
|
|
144
|
+
// Language-independent fallback: the extension demonstrably failed to load
|
|
145
|
+
// (lbug's English wrapper is present) but the localized OS tail matched no
|
|
146
|
+
// specific class. Treat as a dependency/runtime load failure with a hedged
|
|
147
|
+
// remedy — strictly better than the generic `unknown` for non-English hosts,
|
|
148
|
+
// and it never prescribes the wrong fix.
|
|
149
|
+
if (LOAD_FAILURE_WRAPPER.test(text)) {
|
|
150
|
+
return { kind: 'missing_dependency', remedy: HEDGED_LOAD_FAILURE_REMEDY };
|
|
151
|
+
}
|
|
152
|
+
return { kind: 'unknown', remedy: UNKNOWN_REMEDY };
|
|
153
|
+
}
|
|
154
|
+
const STRUCTURAL_MISSING_DEPENDENCY_REMEDY = 'The FTS extension file is valid, so the failure is a missing or incompatible runtime dependency, ' +
|
|
155
|
+
'not the extension itself — reinstalling will NOT help. On Windows, install ' +
|
|
156
|
+
VC_REDIST_INSTALL_HINT +
|
|
157
|
+
' and ensure OpenSSL 3 is available; on Linux/macOS install the shared library named in the error above.';
|
|
158
|
+
/**
|
|
159
|
+
* Pull the extension file path out of lbug's load error. lbug's wrapper is
|
|
160
|
+
* English regardless of OS language — `Failed to load library: {path} which is
|
|
161
|
+
* needed by extension: {name}` (real lbug), or the quoted `Failed to load
|
|
162
|
+
* library '{path}': {reason}` variant — so the path is recoverable in any locale.
|
|
163
|
+
* Only paths ending in `.lbug_extension` are accepted, so a regex misfire can
|
|
164
|
+
* never point the inspector at an arbitrary file.
|
|
165
|
+
*/
|
|
166
|
+
export function extractExtensionPath(reason) {
|
|
167
|
+
const text = reason ?? '';
|
|
168
|
+
const m = /failed to load library:?\s*['"]?(.+?\.lbug_extension)/i.exec(text);
|
|
169
|
+
const path = m?.[1]?.trim();
|
|
170
|
+
return path && path.length > 0 ? path : null;
|
|
171
|
+
}
|
|
172
|
+
/** Node `process.arch` → PE `Machine`. Undefined for arches we don't map. */
|
|
173
|
+
const PE_MACHINE = { x64: 0x8664, arm64: 0xaa64 };
|
|
174
|
+
/** Node `process.arch` → ELF `e_machine`. */
|
|
175
|
+
const ELF_MACHINE = { x64: 0x3e, arm64: 0xb7 };
|
|
176
|
+
/** Node `process.arch` → Mach-O `cputype`. */
|
|
177
|
+
const MACHO_CPUTYPE = { x64: 0x01000007, arm64: 0x0100000c };
|
|
178
|
+
function classifyPE(buf, bytesRead, arch) {
|
|
179
|
+
if (bytesRead < 0x40 || buf[0] !== 0x4d || buf[1] !== 0x5a)
|
|
180
|
+
return 'corrupt'; // 'MZ'
|
|
181
|
+
const peOffset = buf.readUInt32LE(0x3c);
|
|
182
|
+
// The PE header (e_lfanew) points beyond what we read. A large-DOS-stub VALID PE
|
|
183
|
+
// and a garbage e_lfanew are indistinguishable from here, so don't claim 'corrupt'
|
|
184
|
+
// — defer to the loader's own report (#2383 F1-secondary).
|
|
185
|
+
if (peOffset + 6 > bytesRead)
|
|
186
|
+
return 'indeterminate';
|
|
187
|
+
const isPE = buf[peOffset] === 0x50 &&
|
|
188
|
+
buf[peOffset + 1] === 0x45 &&
|
|
189
|
+
buf[peOffset + 2] === 0 &&
|
|
190
|
+
buf[peOffset + 3] === 0;
|
|
191
|
+
if (!isPE)
|
|
192
|
+
return 'corrupt';
|
|
193
|
+
const expected = PE_MACHINE[arch];
|
|
194
|
+
if (expected === undefined)
|
|
195
|
+
return 'valid'; // arch we don't map: don't claim corrupt
|
|
196
|
+
return buf.readUInt16LE(peOffset + 4) === expected ? 'valid' : 'corrupt';
|
|
197
|
+
}
|
|
198
|
+
function classifyELF(buf, bytesRead, arch) {
|
|
199
|
+
if (bytesRead < 20)
|
|
200
|
+
return 'corrupt';
|
|
201
|
+
if (buf[0] !== 0x7f || buf[1] !== 0x45 || buf[2] !== 0x4c || buf[3] !== 0x46)
|
|
202
|
+
return 'corrupt'; // 0x7F ELF
|
|
203
|
+
const littleEndian = buf[5] === 1; // EI_DATA
|
|
204
|
+
const eMachine = littleEndian ? buf.readUInt16LE(18) : buf.readUInt16BE(18);
|
|
205
|
+
const expected = ELF_MACHINE[arch];
|
|
206
|
+
if (expected === undefined)
|
|
207
|
+
return 'valid';
|
|
208
|
+
return eMachine === expected ? 'valid' : 'corrupt';
|
|
209
|
+
}
|
|
210
|
+
function classifyMachO(buf, bytesRead, arch) {
|
|
211
|
+
if (bytesRead < 8)
|
|
212
|
+
return 'corrupt';
|
|
213
|
+
const magicLE = buf.readUInt32LE(0);
|
|
214
|
+
const magicBE = buf.readUInt32BE(0);
|
|
215
|
+
// Universal ("fat") binary — assume it carries the host slice.
|
|
216
|
+
if (magicBE === 0xcafebabe || magicLE === 0xcafebabe)
|
|
217
|
+
return 'valid';
|
|
218
|
+
const thin = magicLE === 0xfeedfacf || magicLE === 0xfeedface;
|
|
219
|
+
const thinSwapped = magicBE === 0xfeedfacf || magicBE === 0xfeedface;
|
|
220
|
+
if (!thin && !thinSwapped)
|
|
221
|
+
return 'corrupt';
|
|
222
|
+
const cpuType = thin ? buf.readUInt32LE(4) : buf.readUInt32BE(4);
|
|
223
|
+
const expected = MACHO_CPUTYPE[arch];
|
|
224
|
+
if (expected === undefined)
|
|
225
|
+
return 'valid';
|
|
226
|
+
return cpuType === expected ? 'valid' : 'corrupt';
|
|
227
|
+
}
|
|
228
|
+
/**
|
|
229
|
+
* Decide whether a binary header is a well-formed shared library for the given
|
|
230
|
+
* platform + architecture — using only the file's structure, no localized text.
|
|
231
|
+
* Pure and injectable (platform/arch as params) so every format+arch combination
|
|
232
|
+
* is unit-testable regardless of the host it runs on.
|
|
233
|
+
*/
|
|
234
|
+
export function classifyBinaryHeader(buf, bytesRead, platform, arch) {
|
|
235
|
+
if (platform === 'win32')
|
|
236
|
+
return classifyPE(buf, bytesRead, arch);
|
|
237
|
+
if (platform === 'linux')
|
|
238
|
+
return classifyELF(buf, bytesRead, arch);
|
|
239
|
+
if (platform === 'darwin')
|
|
240
|
+
return classifyMachO(buf, bytesRead, arch);
|
|
241
|
+
return 'valid'; // unknown host: never claim corrupt
|
|
242
|
+
}
|
|
243
|
+
const BINARY_HEADER_BYTES = 4096;
|
|
244
|
+
/**
|
|
245
|
+
* Best-effort language-independent inspection of the extension file. Reads the
|
|
246
|
+
* header and classifies it; never throws — a missing file is `absent`, an
|
|
247
|
+
* unreadable one is `indeterminate`.
|
|
248
|
+
*/
|
|
249
|
+
export function inspectExtensionBinary(extensionPath) {
|
|
250
|
+
if (!extensionPath)
|
|
251
|
+
return 'indeterminate';
|
|
252
|
+
let fd;
|
|
253
|
+
try {
|
|
254
|
+
fd = openSync(extensionPath, 'r');
|
|
255
|
+
}
|
|
256
|
+
catch (err) {
|
|
257
|
+
return err?.code === 'ENOENT' ? 'absent' : 'indeterminate';
|
|
258
|
+
}
|
|
259
|
+
try {
|
|
260
|
+
const buf = Buffer.alloc(BINARY_HEADER_BYTES);
|
|
261
|
+
const bytesRead = readSync(fd, buf, 0, BINARY_HEADER_BYTES, 0);
|
|
262
|
+
return classifyBinaryHeader(buf, bytesRead, process.platform, process.arch);
|
|
263
|
+
}
|
|
264
|
+
catch {
|
|
265
|
+
return 'indeterminate';
|
|
266
|
+
}
|
|
267
|
+
finally {
|
|
268
|
+
try {
|
|
269
|
+
closeSync(fd);
|
|
270
|
+
}
|
|
271
|
+
catch {
|
|
272
|
+
/* closing the probe fd must never surface */
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
/**
|
|
277
|
+
* Diagnose a LadybugDB load failure, preferring a LANGUAGE-INDEPENDENT structural
|
|
278
|
+
* check of the extension binary over the localized error text:
|
|
279
|
+
* - file absent → missing_file
|
|
280
|
+
* - present but malformed → corrupt_file (bad magic / wrong architecture)
|
|
281
|
+
* - present and well-formed → missing_dependency (a valid binary the loader rejected)
|
|
282
|
+
* The path comes from lbug's own English wrapper, so this holds in any OS display
|
|
283
|
+
* language. When the file cannot be located or read, it falls back to the string
|
|
284
|
+
* classifier (which still carries the language-independent hedged fallback). This
|
|
285
|
+
* is the entry point every surface should call.
|
|
286
|
+
*/
|
|
287
|
+
export function diagnoseExtensionLoad(reason) {
|
|
288
|
+
const text = reason ?? '';
|
|
289
|
+
const stringResult = classifyExtensionLoadError(text);
|
|
290
|
+
const fileState = inspectExtensionBinary(extractExtensionPath(text));
|
|
291
|
+
if (fileState === 'corrupt') {
|
|
292
|
+
return { kind: 'corrupt_file', remedy: CORRUPT_FILE_REMEDY };
|
|
293
|
+
}
|
|
294
|
+
if (fileState === 'valid') {
|
|
295
|
+
// The structural probe only inspects the first BINARY_HEADER_BYTES, so a file
|
|
296
|
+
// truncated AFTER its header still reads 'valid'. When the loader itself reported
|
|
297
|
+
// corruption (e.g. "file too short" / Windows error 193 "not a valid Win32
|
|
298
|
+
// application"), that whole-file verdict is stronger evidence than an intact-looking
|
|
299
|
+
// header — honor it and route to re-download, not a runtime-dependency install (#2383
|
|
300
|
+
// F1). Localized corrupt tails classify as hedged missing_dependency (not
|
|
301
|
+
// corrupt_file), so they still fall through to the dependency remedy below.
|
|
302
|
+
if (stringResult.kind === 'corrupt_file') {
|
|
303
|
+
return stringResult;
|
|
304
|
+
}
|
|
305
|
+
// A structurally sound binary that still failed to load ⇒ a dependency/runtime
|
|
306
|
+
// problem, decided WITHOUT the localized tail. Keep the string classifier's
|
|
307
|
+
// sharper remedy when it recognized the specific case (e.g. English 126).
|
|
308
|
+
const remedy = stringResult.kind === 'missing_dependency'
|
|
309
|
+
? stringResult.remedy
|
|
310
|
+
: STRUCTURAL_MISSING_DEPENDENCY_REMEDY;
|
|
311
|
+
return { kind: 'missing_dependency', remedy };
|
|
312
|
+
}
|
|
313
|
+
// 'absent' or 'indeterminate' → no positive structural evidence, so defer to the
|
|
314
|
+
// string classifier. Note a real never-installed extension has NO path in its
|
|
315
|
+
// reason (lbug says "has not been installed"), so it lands here via
|
|
316
|
+
// 'indeterminate' and the string classifier reports missing_file correctly; a
|
|
317
|
+
// path that lbug named but that is now gone (stale/racy) is better judged by
|
|
318
|
+
// what lbug actually reported than by re-deriving from disk.
|
|
319
|
+
return stringResult;
|
|
320
|
+
}
|