@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.
Files changed (52) hide show
  1. package/CHANGELOG.md +30 -0
  2. package/README.md +3 -2
  3. package/dist/entry-tools/analyze-mod-service.d.ts +2 -2
  4. package/dist/entry-tools/analyze-symbol-service.d.ts +2 -2
  5. package/dist/entry-tools/batch-class-members-service.d.ts +3 -2
  6. package/dist/entry-tools/batch-class-members-service.js +20 -6
  7. package/dist/entry-tools/batch-class-source-service.d.ts +3 -2
  8. package/dist/entry-tools/batch-class-source-service.js +10 -0
  9. package/dist/entry-tools/compare-minecraft-service.d.ts +27 -4
  10. package/dist/entry-tools/compare-minecraft-service.js +65 -4
  11. package/dist/entry-tools/entry-tool-schema.d.ts +2 -2
  12. package/dist/entry-tools/inspect-minecraft-service.d.ts +2 -2
  13. package/dist/entry-tools/manage-cache-service.d.ts +2 -2
  14. package/dist/entry-tools/validate-project/cases/project-summary.js +71 -12
  15. package/dist/entry-tools/validate-project-service.d.ts +2 -2
  16. package/dist/index.js +37 -15
  17. package/dist/source/artifact-resolver.d.ts +14 -0
  18. package/dist/source/artifact-resolver.js +106 -12
  19. package/dist/source/class-source/members-builder.d.ts +7 -0
  20. package/dist/source/class-source/members-builder.js +4 -1
  21. package/dist/source/class-source.d.ts +9 -2
  22. package/dist/source/class-source.js +173 -25
  23. package/dist/source/indexer.js +69 -1
  24. package/dist/source/lifecycle/mapping-helpers.d.ts +20 -1
  25. package/dist/source/lifecycle/mapping-helpers.js +29 -3
  26. package/dist/source/lifecycle/runtime-check.d.ts +25 -0
  27. package/dist/source/lifecycle/runtime-check.js +68 -39
  28. package/dist/source/symbol-resolver.js +88 -0
  29. package/dist/source-jar-reader.d.ts +33 -0
  30. package/dist/source-jar-reader.js +58 -0
  31. package/dist/source-resolver.d.ts +7 -0
  32. package/dist/source-resolver.js +20 -5
  33. package/dist/source-service.d.ts +5 -0
  34. package/dist/source-service.js +7 -0
  35. package/dist/storage/db.d.ts +62 -2
  36. package/dist/storage/db.js +181 -20
  37. package/dist/storage/sqlite.d.ts +31 -1
  38. package/dist/storage/sqlite.js +125 -16
  39. package/dist/tool-guidance.js +4 -1
  40. package/dist/tool-schemas.d.ts +64 -52
  41. package/dist/tool-schemas.js +9 -7
  42. package/dist/types.d.ts +9 -0
  43. package/dist/v1-parity-schemas.js +36 -2
  44. package/dist/version-diff-service.d.ts +23 -0
  45. package/dist/version-diff-service.js +101 -0
  46. package/dist/version-service.d.ts +14 -0
  47. package/dist/version-service.js +45 -3
  48. package/dist/workspace-mapping-service.d.ts +8 -0
  49. package/dist/workspace-mapping-service.js +35 -7
  50. package/docs/README-ja.md +2 -0
  51. package/docs/tool-reference.md +55 -11
  52. package/package.json +1 -1
@@ -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 Database from "./sqlite.js";
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 runIntegrityCheck(db) {
14
- const result = db.prepare("PRAGMA integrity_check").get();
15
- if (!result || result.integrity_check !== "ok") {
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", integrityCheck: result }
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
- const sqliteError = error;
70
- if (sqliteError?.code === "SQLITE_CORRUPT" || sqliteError?.code === "SQLITE_NOTADB") {
71
- return true;
72
- }
73
- if (typeof sqliteError?.errcode !== "number") {
74
- return false;
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 primaryErrcode = sqliteError.errcode & 0xff;
77
- return primaryErrcode === SQLITE_CORRUPT_ERRCODE || primaryErrcode === SQLITE_NOTADB_ERRCODE;
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) {
@@ -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
- constructor(stmt: StatementSync);
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;
@@ -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
- constructor(stmt) {
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
- return this.invoke("iterate", params);
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
- if (normalized.named !== undefined) {
42
- return target.call(this.stmt, normalized.named);
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
- return new Statement(this.inner.prepare(sql));
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
  }
@@ -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
- (!Object.prototype.hasOwnProperty.call(SUGGESTED_CALL_DEFAULTS, field) ||
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
  }