@adhisang/minecraft-modding-mcp 7.0.0 → 7.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +30 -0
- package/README.md +3 -2
- package/dist/entry-tools/analyze-mod-service.d.ts +2 -2
- package/dist/entry-tools/analyze-symbol-service.d.ts +2 -2
- package/dist/entry-tools/batch-class-members-service.d.ts +3 -2
- package/dist/entry-tools/batch-class-members-service.js +20 -6
- package/dist/entry-tools/batch-class-source-service.d.ts +3 -2
- package/dist/entry-tools/batch-class-source-service.js +10 -0
- package/dist/entry-tools/compare-minecraft-service.d.ts +27 -4
- package/dist/entry-tools/compare-minecraft-service.js +65 -4
- package/dist/entry-tools/entry-tool-schema.d.ts +2 -2
- package/dist/entry-tools/inspect-minecraft-service.d.ts +2 -2
- package/dist/entry-tools/manage-cache-service.d.ts +2 -2
- package/dist/entry-tools/validate-project/cases/project-summary.js +71 -12
- package/dist/entry-tools/validate-project-service.d.ts +2 -2
- package/dist/index.js +37 -15
- package/dist/source/artifact-resolver.d.ts +14 -0
- package/dist/source/artifact-resolver.js +106 -12
- package/dist/source/class-source/members-builder.d.ts +7 -0
- package/dist/source/class-source/members-builder.js +4 -1
- package/dist/source/class-source.d.ts +9 -2
- package/dist/source/class-source.js +173 -25
- package/dist/source/indexer.js +69 -1
- package/dist/source/lifecycle/mapping-helpers.d.ts +20 -1
- package/dist/source/lifecycle/mapping-helpers.js +29 -3
- package/dist/source/lifecycle/runtime-check.d.ts +25 -0
- package/dist/source/lifecycle/runtime-check.js +68 -39
- package/dist/source/symbol-resolver.js +88 -0
- package/dist/source-jar-reader.d.ts +33 -0
- package/dist/source-jar-reader.js +58 -0
- package/dist/source-resolver.d.ts +7 -0
- package/dist/source-resolver.js +20 -5
- package/dist/source-service.d.ts +5 -0
- package/dist/source-service.js +7 -0
- package/dist/storage/db.d.ts +62 -2
- package/dist/storage/db.js +181 -20
- package/dist/storage/sqlite.d.ts +31 -1
- package/dist/storage/sqlite.js +125 -16
- package/dist/tool-guidance.js +4 -1
- package/dist/tool-schemas.d.ts +64 -52
- package/dist/tool-schemas.js +9 -7
- package/dist/types.d.ts +9 -0
- package/dist/v1-parity-schemas.js +36 -2
- package/dist/version-diff-service.d.ts +23 -0
- package/dist/version-diff-service.js +101 -0
- package/dist/version-service.d.ts +14 -0
- package/dist/version-service.js +45 -3
- package/dist/workspace-mapping-service.d.ts +8 -0
- package/dist/workspace-mapping-service.js +35 -7
- package/docs/README-ja.md +2 -0
- package/docs/tool-reference.md +55 -11
- package/package.json +1 -1
package/dist/storage/db.js
CHANGED
|
@@ -1,22 +1,131 @@
|
|
|
1
|
-
import { existsSync, renameSync } from "node:fs";
|
|
2
|
-
import { dirname } from "node:path";
|
|
1
|
+
import { existsSync, readdirSync, renameSync, unlinkSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { basename, dirname, join } from "node:path";
|
|
3
3
|
import { mkdirSync } from "node:fs";
|
|
4
|
-
import
|
|
4
|
+
import { randomUUID } from "node:crypto";
|
|
5
|
+
import Database, { isRawSqliteCorruptionError } from "./sqlite.js";
|
|
5
6
|
import { runMigrations } from "./migrations.js";
|
|
6
7
|
import { createError, ERROR_CODES, isAppError } from "../errors.js";
|
|
7
8
|
import { log } from "../logger.js";
|
|
9
|
+
// Re-exported for callers that already import the predicate from this module
|
|
10
|
+
// (it now lives in ./sqlite.js so the Database wrapper can call it directly
|
|
11
|
+
// without a module cycle back to this file).
|
|
12
|
+
export { isRawSqliteCorruptionError };
|
|
8
13
|
const DEFAULT_SQLITE_CACHE_KB = 8_000;
|
|
9
14
|
const DEFAULT_SQLITE_MMAP_SIZE = 268_435_456;
|
|
15
|
+
// Per-request escalation files created by the runtime-corruption recovery
|
|
16
|
+
// path (see attachRuntimeCorruptionObserver / convertRuntimeSqliteCorruption
|
|
17
|
+
// below): a SQLite error surfaced while serving a tool call - not at open
|
|
18
|
+
// time - means quick_check already passed for this file, so the NEXT open
|
|
19
|
+
// must pay for the full integrity_check instead of trusting quick_check
|
|
20
|
+
// again.
|
|
21
|
+
//
|
|
22
|
+
// Each request is its OWN immutable, uniquely-named file (exclusive-create,
|
|
23
|
+
// never overwritten), rather than one shared marker whose content gets
|
|
24
|
+
// replaced. That is what makes two requests race-free even across processes
|
|
25
|
+
// sharing one cache directory: an open lists whatever request files exist at
|
|
26
|
+
// its OWN decision point and remembers those exact names; after honoring
|
|
27
|
+
// them, it deletes only those names. A request file created by someone else
|
|
28
|
+
// AFTER that decision (a concurrent process, or a second corruption event)
|
|
29
|
+
// is simply a name that open never saw, so it can never be swept up by a
|
|
30
|
+
// clear meant for a different, earlier decision - it waits for the NEXT
|
|
31
|
+
// open. (An earlier single shared marker file, `<sqlitePath>.integrity-check-
|
|
32
|
+
// requested`, was never released, so there is no old name to keep reading
|
|
33
|
+
// for compatibility; it is dropped outright rather than special-cased.)
|
|
34
|
+
const INTEGRITY_CHECK_REQUEST_INFIX = ".integrity-check-requested.";
|
|
10
35
|
function ensureParentDirectory(path) {
|
|
11
36
|
mkdirSync(dirname(path), { recursive: true });
|
|
12
37
|
}
|
|
13
|
-
function
|
|
14
|
-
|
|
15
|
-
|
|
38
|
+
function integrityCheckRequestFileName(sqlitePath, token) {
|
|
39
|
+
return `${basename(sqlitePath)}${INTEGRITY_CHECK_REQUEST_INFIX}${token}`;
|
|
40
|
+
}
|
|
41
|
+
function integrityCheckRequestFileNamePrefix(sqlitePath) {
|
|
42
|
+
return `${basename(sqlitePath)}${INTEGRITY_CHECK_REQUEST_INFIX}`;
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Best-effort request that the NEXT `openDatabase` for `sqlitePath` run the
|
|
46
|
+
* full `PRAGMA integrity_check` instead of the default `quick_check`. Creates
|
|
47
|
+
* a new, uniquely-named request file (see the block comment above) with
|
|
48
|
+
* exclusive-create so it can never collide with or overwrite another
|
|
49
|
+
* pending request. Never throws: a failure to write the file only means the
|
|
50
|
+
* escalation is missed, which is logged rather than allowed to mask the
|
|
51
|
+
* caller's real error. Returns whether the file was actually written, so a
|
|
52
|
+
* caller that cannot schedule the follow-up check can say so instead of
|
|
53
|
+
* promising one.
|
|
54
|
+
*/
|
|
55
|
+
export function requestFullIntegrityCheck(sqlitePath, logger = buildDefaultLogger()) {
|
|
56
|
+
const token = `${Date.now()}-${process.pid}-${randomUUID()}`;
|
|
57
|
+
const requestPath = join(dirname(sqlitePath), integrityCheckRequestFileName(sqlitePath, token));
|
|
58
|
+
try {
|
|
59
|
+
ensureParentDirectory(sqlitePath);
|
|
60
|
+
writeFileSync(requestPath, "", { flag: "wx" });
|
|
61
|
+
return true;
|
|
62
|
+
}
|
|
63
|
+
catch (writeError) {
|
|
64
|
+
logger.warn("Failed to request a full SQLite integrity check", {
|
|
65
|
+
sqlitePath,
|
|
66
|
+
reason: writeError instanceof Error ? writeError.message : String(writeError)
|
|
67
|
+
});
|
|
68
|
+
return false;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* Lists the full paths of every currently pending full-integrity-check
|
|
73
|
+
* request file for `sqlitePath`. Best-effort: a missing or unreadable
|
|
74
|
+
* directory is treated as "no requests pending" rather than as an error,
|
|
75
|
+
* since the fallback (a quick_check) is always safe to run.
|
|
76
|
+
*/
|
|
77
|
+
export function listIntegrityCheckRequestFiles(sqlitePath) {
|
|
78
|
+
const dir = dirname(sqlitePath);
|
|
79
|
+
const prefix = integrityCheckRequestFileNamePrefix(sqlitePath);
|
|
80
|
+
try {
|
|
81
|
+
return readdirSync(dir)
|
|
82
|
+
.filter((name) => name.startsWith(prefix))
|
|
83
|
+
.map((name) => join(dir, name));
|
|
84
|
+
}
|
|
85
|
+
catch {
|
|
86
|
+
return [];
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
/**
|
|
90
|
+
* Deletes EXACTLY the given request file paths - the ones an earlier
|
|
91
|
+
* `listIntegrityCheckRequestFiles` call returned, never a fresh listing - so
|
|
92
|
+
* a request file created after that decision point is never deleted by a
|
|
93
|
+
* clear that only ever meant to honor the requests it actually acted on.
|
|
94
|
+
* Best-effort per file: ENOENT (already gone) is ignored, anything else is
|
|
95
|
+
* logged, not thrown.
|
|
96
|
+
*
|
|
97
|
+
* Exported so a unit test can exercise the race guard directly:
|
|
98
|
+
* `openDatabase` lists-then-clears synchronously with no `await` in between,
|
|
99
|
+
* so there is no way to interleave a concurrent request file appearing
|
|
100
|
+
* between "decide" and "clear" from outside that call.
|
|
101
|
+
*/
|
|
102
|
+
export function clearIntegrityCheckRequestFiles(requestFiles, logger = buildDefaultLogger()) {
|
|
103
|
+
for (const requestFile of requestFiles) {
|
|
104
|
+
try {
|
|
105
|
+
unlinkSync(requestFile);
|
|
106
|
+
}
|
|
107
|
+
catch (error) {
|
|
108
|
+
if (error?.code === "ENOENT") {
|
|
109
|
+
continue;
|
|
110
|
+
}
|
|
111
|
+
logger.warn("Failed to clear a SQLite full-integrity-check request file", {
|
|
112
|
+
requestFile,
|
|
113
|
+
reason: error instanceof Error ? error.message : String(error)
|
|
114
|
+
});
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
function runIntegrityCheck(db, logger, mode = "quick") {
|
|
119
|
+
const resultKey = mode === "full" ? "integrity_check" : "quick_check";
|
|
120
|
+
const startedAt = Date.now();
|
|
121
|
+
const result = db.prepare(`PRAGMA ${resultKey}`).get();
|
|
122
|
+
const durationMs = Date.now() - startedAt;
|
|
123
|
+
logger.info("SQLite consistency check completed", { durationMs, check: resultKey });
|
|
124
|
+
if (!result || result[resultKey] !== "ok") {
|
|
16
125
|
throw createError({
|
|
17
126
|
code: ERROR_CODES.DB_FAILURE,
|
|
18
127
|
message: "SQLite integrity check failed.",
|
|
19
|
-
details: { reason: "integrity_check_failed",
|
|
128
|
+
details: { reason: "integrity_check_failed", check: resultKey, checkResult: result }
|
|
20
129
|
});
|
|
21
130
|
}
|
|
22
131
|
}
|
|
@@ -60,21 +169,59 @@ function isSchemaVersionMismatchError(error) {
|
|
|
60
169
|
return (error.details?.reason === "schema_version_unsupported" ||
|
|
61
170
|
error.details?.reason === "schema_version_invalid");
|
|
62
171
|
}
|
|
63
|
-
const SQLITE_CORRUPT_ERRCODE = 11;
|
|
64
|
-
const SQLITE_NOTADB_ERRCODE = 26;
|
|
65
172
|
function isCorruptionError(error) {
|
|
66
173
|
if (isAppError(error)) {
|
|
67
174
|
return (error.code === ERROR_CODES.DB_FAILURE && error.details?.reason === "integrity_check_failed");
|
|
68
175
|
}
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
176
|
+
return isRawSqliteCorruptionError(error);
|
|
177
|
+
}
|
|
178
|
+
/**
|
|
179
|
+
* Wires a Database instance's corruption observer (see sqlite.ts) so that a
|
|
180
|
+
* raw corruption error thrown by ANY later statement on this handle - no
|
|
181
|
+
* matter what catches and wraps it further up the call stack, including a
|
|
182
|
+
* tool-specific error wrapper that never reaches runTool's own catch block -
|
|
183
|
+
* still schedules the next open's full integrity_check. Exported so a unit
|
|
184
|
+
* test can attach the same wiring to a Database instance opened directly
|
|
185
|
+
* (bypassing openDatabase's own open-time check, which would otherwise catch
|
|
186
|
+
* a test's injected corruption before a "successfully opened" handle ever
|
|
187
|
+
* existed to query against).
|
|
188
|
+
*/
|
|
189
|
+
export function attachRuntimeCorruptionObserver(db, sqlitePath, logger = buildDefaultLogger()) {
|
|
190
|
+
db.setCorruptionObserver(() => {
|
|
191
|
+
requestFullIntegrityCheck(sqlitePath, logger);
|
|
192
|
+
});
|
|
193
|
+
}
|
|
194
|
+
/**
|
|
195
|
+
* runTool catch-path helper: when `caughtError` is a raw SQLite corruption
|
|
196
|
+
* error observed while serving a tool call (not at open time - open-time
|
|
197
|
+
* corruption is already an AppError by the time it reaches here), this
|
|
198
|
+
* requests a full integrity check on the next open and returns a typed
|
|
199
|
+
* ERR_DB_FAILURE the caller should report instead of the raw error. Any other
|
|
200
|
+
* error is returned unchanged so non-corruption failures are unaffected.
|
|
201
|
+
*
|
|
202
|
+
* The marker request here is redundant with the Database corruption observer
|
|
203
|
+
* (attachRuntimeCorruptionObserver) for an UNWRAPPED raw error, since that
|
|
204
|
+
* observer already fired deeper in the call stack; it is kept because
|
|
205
|
+
* `convertRuntimeSqliteCorruption` is the only place with reliable access to
|
|
206
|
+
* whether scheduling succeeded, which decides which `nextAction` to publish.
|
|
207
|
+
*/
|
|
208
|
+
export function convertRuntimeSqliteCorruption(error, sqlitePath, logger = buildDefaultLogger()) {
|
|
209
|
+
if (!isRawSqliteCorruptionError(error)) {
|
|
210
|
+
return error;
|
|
75
211
|
}
|
|
76
|
-
const
|
|
77
|
-
|
|
212
|
+
const scheduled = requestFullIntegrityCheck(sqlitePath, logger);
|
|
213
|
+
const nextAction = scheduled
|
|
214
|
+
? "Restart the MCP server so the cache database is fully checked and rebuilt if necessary, then retry the request."
|
|
215
|
+
: `The automatic full check could not be scheduled. Stop every MCP server process that uses this cache, delete the cache database at ${sqlitePath} together with its -wal and -shm files, then restart; the cache is rebuilt on the next start.`;
|
|
216
|
+
return createError({
|
|
217
|
+
code: ERROR_CODES.DB_FAILURE,
|
|
218
|
+
message: "A SQLite consistency error occurred while serving this request.",
|
|
219
|
+
details: {
|
|
220
|
+
sqlitePath,
|
|
221
|
+
reason: "runtime_corruption",
|
|
222
|
+
nextAction
|
|
223
|
+
}
|
|
224
|
+
});
|
|
78
225
|
}
|
|
79
226
|
function buildDefaultLogger() {
|
|
80
227
|
return {
|
|
@@ -100,12 +247,21 @@ function buildDefaultLogger() {
|
|
|
100
247
|
}
|
|
101
248
|
export function openDatabase(config, logger = buildDefaultLogger()) {
|
|
102
249
|
let db;
|
|
250
|
+
const pendingRequestFiles = listIntegrityCheckRequestFiles(config.sqlitePath);
|
|
103
251
|
try {
|
|
104
252
|
ensureParentDirectory(config.sqlitePath);
|
|
105
253
|
db = new Database(config.sqlitePath);
|
|
106
254
|
applyPragmas(db, config);
|
|
107
255
|
const schemaVersion = runMigrations(db);
|
|
108
|
-
runIntegrityCheck(db);
|
|
256
|
+
runIntegrityCheck(db, logger, pendingRequestFiles.length > 0 ? "full" : "quick");
|
|
257
|
+
if (pendingRequestFiles.length > 0) {
|
|
258
|
+
clearIntegrityCheckRequestFiles(pendingRequestFiles, logger);
|
|
259
|
+
}
|
|
260
|
+
// Only attach the runtime-corruption observer once the open itself
|
|
261
|
+
// (migrations + consistency check) has fully succeeded: open-time
|
|
262
|
+
// failures must keep going through the backup/rebuild path below, not
|
|
263
|
+
// silently re-request a check that is about to happen anyway.
|
|
264
|
+
attachRuntimeCorruptionObserver(db, config.sqlitePath, logger);
|
|
109
265
|
return { db, schemaVersion };
|
|
110
266
|
}
|
|
111
267
|
catch (caughtError) {
|
|
@@ -152,12 +308,17 @@ export function openDatabase(config, logger = buildDefaultLogger()) {
|
|
|
152
308
|
const backupPath = backupCorruptedDb(config.sqlitePath);
|
|
153
309
|
logger.warn("SQLite database integrity check failed. Recreated database after backup", {
|
|
154
310
|
sqlitePath: config.sqlitePath,
|
|
155
|
-
backupPath
|
|
311
|
+
backupPath,
|
|
312
|
+
reason: errorMessage
|
|
156
313
|
});
|
|
157
314
|
rebuilt = new Database(config.sqlitePath);
|
|
158
315
|
applyPragmas(rebuilt, config);
|
|
159
316
|
const schemaVersion = runMigrations(rebuilt);
|
|
160
|
-
runIntegrityCheck(rebuilt);
|
|
317
|
+
runIntegrityCheck(rebuilt, logger);
|
|
318
|
+
if (pendingRequestFiles.length > 0) {
|
|
319
|
+
clearIntegrityCheckRequestFiles(pendingRequestFiles, logger);
|
|
320
|
+
}
|
|
321
|
+
attachRuntimeCorruptionObserver(rebuilt, config.sqlitePath, logger);
|
|
161
322
|
return { db: rebuilt, schemaVersion };
|
|
162
323
|
}
|
|
163
324
|
catch (rebuildError) {
|
package/dist/storage/sqlite.d.ts
CHANGED
|
@@ -1,17 +1,47 @@
|
|
|
1
1
|
import { type StatementSync } from "node:sqlite";
|
|
2
|
+
/**
|
|
3
|
+
* Recognizes a raw (non-AppError) SQLite corruption error - the shape
|
|
4
|
+
* node:sqlite throws mid-query, e.g. `{ code: "ERR_SQLITE_ERROR", errcode: 11 }`.
|
|
5
|
+
* Lives here (rather than storage/db.ts, which imports this module) so the
|
|
6
|
+
* Database wrapper below can call it directly without a module cycle; db.ts
|
|
7
|
+
* re-exports it for callers that used to import it from there.
|
|
8
|
+
*/
|
|
9
|
+
export declare function isRawSqliteCorruptionError(error: unknown): boolean;
|
|
10
|
+
/**
|
|
11
|
+
* Notified, at most once per `Database` instance, the first time a raw SQLite
|
|
12
|
+
* corruption error is thrown by any statement/pragma/transaction path on that
|
|
13
|
+
* instance. Called BEFORE the triggering error is rethrown unchanged, so it
|
|
14
|
+
* runs regardless of how (or whether) a caller further up the stack wraps or
|
|
15
|
+
* swallows that error - a wrapping catch block elsewhere in the codebase can
|
|
16
|
+
* no longer hide a runtime corruption from this observer.
|
|
17
|
+
*/
|
|
18
|
+
export type SqliteCorruptionObserver = (error: unknown) => void;
|
|
2
19
|
export declare class Statement<T = unknown> {
|
|
3
20
|
private readonly stmt;
|
|
4
|
-
|
|
21
|
+
private readonly notifyCorruption;
|
|
22
|
+
constructor(stmt: StatementSync, notifyCorruption: (error: unknown) => void);
|
|
5
23
|
run(...params: unknown[]): unknown;
|
|
6
24
|
get(...params: unknown[]): T | undefined;
|
|
7
25
|
all(...params: unknown[]): T[];
|
|
8
26
|
iterate(...params: unknown[]): Iterable<T>;
|
|
27
|
+
private wrapIterable;
|
|
9
28
|
private invoke;
|
|
10
29
|
}
|
|
11
30
|
export default class Database {
|
|
12
31
|
private readonly inner;
|
|
13
32
|
private transactionDepth;
|
|
33
|
+
private corruptionObserver;
|
|
34
|
+
private corruptionNotified;
|
|
14
35
|
constructor(path: string);
|
|
36
|
+
/**
|
|
37
|
+
* Registers (or clears, with `undefined`) the observer notified on this
|
|
38
|
+
* instance's first raw corruption error. Best-effort: an observer that
|
|
39
|
+
* throws is swallowed so it can never mask the real SQLite error being
|
|
40
|
+
* rethrown, and it fires at most once per instance.
|
|
41
|
+
*/
|
|
42
|
+
setCorruptionObserver(observer: SqliteCorruptionObserver | undefined): void;
|
|
43
|
+
private notifyCorruption;
|
|
44
|
+
private runRaw;
|
|
15
45
|
pragma(pragma: string): unknown;
|
|
16
46
|
prepare<T = Record<string, unknown>>(sql: string): Statement<T>;
|
|
17
47
|
transaction<T>(fn: () => T): () => T;
|
package/dist/storage/sqlite.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { DatabaseSync } from "node:sqlite";
|
|
2
|
+
import { isAppError } from "../errors.js";
|
|
2
3
|
function isPlainObject(value) {
|
|
3
4
|
if (value === null || typeof value !== "object" || Array.isArray(value) || ArrayBuffer.isView(value)) {
|
|
4
5
|
return false;
|
|
@@ -18,10 +19,35 @@ function normalizeParameters(args) {
|
|
|
18
19
|
}
|
|
19
20
|
return { positional: args };
|
|
20
21
|
}
|
|
22
|
+
const SQLITE_CORRUPT_ERRCODE = 11;
|
|
23
|
+
const SQLITE_NOTADB_ERRCODE = 26;
|
|
24
|
+
/**
|
|
25
|
+
* Recognizes a raw (non-AppError) SQLite corruption error - the shape
|
|
26
|
+
* node:sqlite throws mid-query, e.g. `{ code: "ERR_SQLITE_ERROR", errcode: 11 }`.
|
|
27
|
+
* Lives here (rather than storage/db.ts, which imports this module) so the
|
|
28
|
+
* Database wrapper below can call it directly without a module cycle; db.ts
|
|
29
|
+
* re-exports it for callers that used to import it from there.
|
|
30
|
+
*/
|
|
31
|
+
export function isRawSqliteCorruptionError(error) {
|
|
32
|
+
if (isAppError(error)) {
|
|
33
|
+
return false;
|
|
34
|
+
}
|
|
35
|
+
const sqliteError = error;
|
|
36
|
+
if (sqliteError?.code === "SQLITE_CORRUPT" || sqliteError?.code === "SQLITE_NOTADB") {
|
|
37
|
+
return true;
|
|
38
|
+
}
|
|
39
|
+
if (typeof sqliteError?.errcode !== "number") {
|
|
40
|
+
return false;
|
|
41
|
+
}
|
|
42
|
+
const primaryErrcode = sqliteError.errcode & 0xff;
|
|
43
|
+
return primaryErrcode === SQLITE_CORRUPT_ERRCODE || primaryErrcode === SQLITE_NOTADB_ERRCODE;
|
|
44
|
+
}
|
|
21
45
|
export class Statement {
|
|
22
46
|
stmt;
|
|
23
|
-
|
|
47
|
+
notifyCorruption;
|
|
48
|
+
constructor(stmt, notifyCorruption) {
|
|
24
49
|
this.stmt = stmt;
|
|
50
|
+
this.notifyCorruption = notifyCorruption;
|
|
25
51
|
}
|
|
26
52
|
run(...params) {
|
|
27
53
|
return this.invoke("run", params);
|
|
@@ -33,34 +59,116 @@ export class Statement {
|
|
|
33
59
|
return this.invoke("all", params);
|
|
34
60
|
}
|
|
35
61
|
iterate(...params) {
|
|
36
|
-
|
|
62
|
+
const rawIterable = this.invoke("iterate", params);
|
|
63
|
+
return this.wrapIterable(rawIterable);
|
|
64
|
+
}
|
|
65
|
+
// node:sqlite's iterate() returns lazily: the corrupted page is only ever
|
|
66
|
+
// touched once the consumer actually pulls a row, i.e. inside next(), not
|
|
67
|
+
// at the call above. Wrap the iterator itself so a mid-iteration corruption
|
|
68
|
+
// error still reaches the observer before propagating to the consumer.
|
|
69
|
+
wrapIterable(iterable) {
|
|
70
|
+
const notifyCorruption = this.notifyCorruption;
|
|
71
|
+
return {
|
|
72
|
+
[Symbol.iterator]() {
|
|
73
|
+
const inner = iterable[Symbol.iterator]();
|
|
74
|
+
const wrapped = {
|
|
75
|
+
next() {
|
|
76
|
+
try {
|
|
77
|
+
return inner.next();
|
|
78
|
+
}
|
|
79
|
+
catch (error) {
|
|
80
|
+
notifyCorruption(error);
|
|
81
|
+
throw error;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
};
|
|
85
|
+
// Forward early termination (a `break` out of for...of, or a consuming
|
|
86
|
+
// generator being closed) so the underlying statement is reset instead
|
|
87
|
+
// of being left mid-iteration with its read snapshot open.
|
|
88
|
+
if (typeof inner.return === "function") {
|
|
89
|
+
wrapped.return = (value) => inner.return(value);
|
|
90
|
+
}
|
|
91
|
+
return wrapped;
|
|
92
|
+
}
|
|
93
|
+
};
|
|
37
94
|
}
|
|
38
95
|
invoke(method, params) {
|
|
39
96
|
const normalized = normalizeParameters(params);
|
|
40
97
|
const target = this.stmt[method];
|
|
41
|
-
|
|
42
|
-
|
|
98
|
+
try {
|
|
99
|
+
if (normalized.named !== undefined) {
|
|
100
|
+
return target.call(this.stmt, normalized.named);
|
|
101
|
+
}
|
|
102
|
+
return target.call(this.stmt, ...(normalized.positional ?? []));
|
|
103
|
+
}
|
|
104
|
+
catch (error) {
|
|
105
|
+
this.notifyCorruption(error);
|
|
106
|
+
throw error;
|
|
43
107
|
}
|
|
44
|
-
return target.call(this.stmt, ...(normalized.positional ?? []));
|
|
45
108
|
}
|
|
46
109
|
}
|
|
47
110
|
let transactionSerial = 0;
|
|
48
111
|
export default class Database {
|
|
49
112
|
inner;
|
|
50
113
|
transactionDepth = 0;
|
|
114
|
+
corruptionObserver;
|
|
115
|
+
corruptionNotified = false;
|
|
51
116
|
constructor(path) {
|
|
52
117
|
this.inner = new DatabaseSync(path);
|
|
53
118
|
}
|
|
119
|
+
/**
|
|
120
|
+
* Registers (or clears, with `undefined`) the observer notified on this
|
|
121
|
+
* instance's first raw corruption error. Best-effort: an observer that
|
|
122
|
+
* throws is swallowed so it can never mask the real SQLite error being
|
|
123
|
+
* rethrown, and it fires at most once per instance.
|
|
124
|
+
*/
|
|
125
|
+
setCorruptionObserver(observer) {
|
|
126
|
+
this.corruptionObserver = observer;
|
|
127
|
+
}
|
|
128
|
+
notifyCorruption(error) {
|
|
129
|
+
if (this.corruptionNotified) {
|
|
130
|
+
return;
|
|
131
|
+
}
|
|
132
|
+
const observer = this.corruptionObserver;
|
|
133
|
+
if (!observer) {
|
|
134
|
+
return;
|
|
135
|
+
}
|
|
136
|
+
if (!isRawSqliteCorruptionError(error)) {
|
|
137
|
+
return;
|
|
138
|
+
}
|
|
139
|
+
this.corruptionNotified = true;
|
|
140
|
+
try {
|
|
141
|
+
observer(error);
|
|
142
|
+
}
|
|
143
|
+
catch {
|
|
144
|
+
// best-effort: never let the observer mask the real error being rethrown
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
runRaw(fn) {
|
|
148
|
+
try {
|
|
149
|
+
return fn();
|
|
150
|
+
}
|
|
151
|
+
catch (error) {
|
|
152
|
+
this.notifyCorruption(error);
|
|
153
|
+
throw error;
|
|
154
|
+
}
|
|
155
|
+
}
|
|
54
156
|
pragma(pragma) {
|
|
55
157
|
const sql = `PRAGMA ${pragma}`;
|
|
56
158
|
if (pragma.includes("=")) {
|
|
57
|
-
this.inner.exec(sql);
|
|
159
|
+
this.runRaw(() => this.inner.exec(sql));
|
|
58
160
|
return undefined;
|
|
59
161
|
}
|
|
60
|
-
return this.inner.prepare(sql).all();
|
|
162
|
+
return this.runRaw(() => this.inner.prepare(sql).all());
|
|
61
163
|
}
|
|
62
164
|
prepare(sql) {
|
|
63
|
-
|
|
165
|
+
// Wrapped in runRaw: PREPARING a statement can itself throw a raw
|
|
166
|
+
// corruption error - e.g. damage to the schema (sqlite_master) that a
|
|
167
|
+
// fresh connection only discovers while compiling its first statement -
|
|
168
|
+
// not just running one. Without this, that error skipped the observer
|
|
169
|
+
// entirely (reproduced: errcode 11, observer never notified).
|
|
170
|
+
const stmt = this.runRaw(() => this.inner.prepare(sql));
|
|
171
|
+
return new Statement(stmt, (error) => this.notifyCorruption(error));
|
|
64
172
|
}
|
|
65
173
|
transaction(fn) {
|
|
66
174
|
return () => this.runInTransaction(fn);
|
|
@@ -74,19 +182,19 @@ export default class Database {
|
|
|
74
182
|
const savepoint = `sp_${++transactionSerial}`;
|
|
75
183
|
try {
|
|
76
184
|
if (isOutermost) {
|
|
77
|
-
this.inner.exec("BEGIN");
|
|
185
|
+
this.runRaw(() => this.inner.exec("BEGIN"));
|
|
78
186
|
}
|
|
79
187
|
else {
|
|
80
|
-
this.inner.exec(`SAVEPOINT ${savepoint}`);
|
|
188
|
+
this.runRaw(() => this.inner.exec(`SAVEPOINT ${savepoint}`));
|
|
81
189
|
}
|
|
82
190
|
this.transactionDepth = initialDepth + 1;
|
|
83
191
|
const result = fn();
|
|
84
192
|
this.transactionDepth = initialDepth;
|
|
85
193
|
if (isOutermost) {
|
|
86
|
-
this.inner.exec("COMMIT");
|
|
194
|
+
this.runRaw(() => this.inner.exec("COMMIT"));
|
|
87
195
|
}
|
|
88
196
|
else {
|
|
89
|
-
this.inner.exec(`RELEASE SAVEPOINT ${savepoint}`);
|
|
197
|
+
this.runRaw(() => this.inner.exec(`RELEASE SAVEPOINT ${savepoint}`));
|
|
90
198
|
}
|
|
91
199
|
return result;
|
|
92
200
|
}
|
|
@@ -94,15 +202,16 @@ export default class Database {
|
|
|
94
202
|
this.transactionDepth = initialDepth;
|
|
95
203
|
try {
|
|
96
204
|
if (isOutermost) {
|
|
97
|
-
this.inner.exec("ROLLBACK");
|
|
205
|
+
this.runRaw(() => this.inner.exec("ROLLBACK"));
|
|
98
206
|
}
|
|
99
207
|
else {
|
|
100
|
-
this.inner.exec(`ROLLBACK TO SAVEPOINT ${savepoint}`);
|
|
101
|
-
this.inner.exec(`RELEASE SAVEPOINT ${savepoint}`);
|
|
208
|
+
this.runRaw(() => this.inner.exec(`ROLLBACK TO SAVEPOINT ${savepoint}`));
|
|
209
|
+
this.runRaw(() => this.inner.exec(`RELEASE SAVEPOINT ${savepoint}`));
|
|
102
210
|
}
|
|
103
211
|
}
|
|
104
212
|
catch {
|
|
105
|
-
// best-effort rollback cleanup
|
|
213
|
+
// best-effort rollback cleanup - runRaw already notified the
|
|
214
|
+
// observer (if any) before this catch swallows the rollback failure
|
|
106
215
|
}
|
|
107
216
|
throw error;
|
|
108
217
|
}
|
package/dist/tool-guidance.js
CHANGED
|
@@ -800,7 +800,10 @@ export function buildValidateProjectSuggestedParams(normalizedInput) {
|
|
|
800
800
|
for (const field of booleanFields) {
|
|
801
801
|
const value = record[field];
|
|
802
802
|
if (typeof value === "boolean" &&
|
|
803
|
-
|
|
803
|
+
// project-summary infers an omitted version, so an explicit
|
|
804
|
+
// preferProjectVersion=false is an opt-out, not a droppable default.
|
|
805
|
+
(field === "preferProjectVersion" ||
|
|
806
|
+
!Object.prototype.hasOwnProperty.call(SUGGESTED_CALL_DEFAULTS, field) ||
|
|
804
807
|
!isSuggestedCallDefault(field, value))) {
|
|
805
808
|
result[field] = value;
|
|
806
809
|
}
|