locksmith-mcp 0.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 (66) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +151 -0
  3. package/dist/analyzer/astUtils.d.ts +19 -0
  4. package/dist/analyzer/astUtils.js +50 -0
  5. package/dist/analyzer/astUtils.js.map +1 -0
  6. package/dist/analyzer/engine.d.ts +11 -0
  7. package/dist/analyzer/engine.js +87 -0
  8. package/dist/analyzer/engine.js.map +1 -0
  9. package/dist/analyzer/parse.d.ts +7 -0
  10. package/dist/analyzer/parse.js +165 -0
  11. package/dist/analyzer/parse.js.map +1 -0
  12. package/dist/analyzer/rules/addCheckConstraintNoNotValid.d.ts +2 -0
  13. package/dist/analyzer/rules/addCheckConstraintNoNotValid.js +24 -0
  14. package/dist/analyzer/rules/addCheckConstraintNoNotValid.js.map +1 -0
  15. package/dist/analyzer/rules/addColumnNotNullNoDefault.d.ts +2 -0
  16. package/dist/analyzer/rules/addColumnNotNullNoDefault.js +42 -0
  17. package/dist/analyzer/rules/addColumnNotNullNoDefault.js.map +1 -0
  18. package/dist/analyzer/rules/addColumnVolatileDefault.d.ts +2 -0
  19. package/dist/analyzer/rules/addColumnVolatileDefault.js +36 -0
  20. package/dist/analyzer/rules/addColumnVolatileDefault.js.map +1 -0
  21. package/dist/analyzer/rules/addForeignKeyValidating.d.ts +2 -0
  22. package/dist/analyzer/rules/addForeignKeyValidating.js +26 -0
  23. package/dist/analyzer/rules/addForeignKeyValidating.js.map +1 -0
  24. package/dist/analyzer/rules/alterColumnType.d.ts +2 -0
  25. package/dist/analyzer/rules/alterColumnType.js +31 -0
  26. package/dist/analyzer/rules/alterColumnType.js.map +1 -0
  27. package/dist/analyzer/rules/createIndexNonConcurrent.d.ts +2 -0
  28. package/dist/analyzer/rules/createIndexNonConcurrent.js +24 -0
  29. package/dist/analyzer/rules/createIndexNonConcurrent.js.map +1 -0
  30. package/dist/analyzer/rules/dropColumnOrTable.d.ts +2 -0
  31. package/dist/analyzer/rules/dropColumnOrTable.js +31 -0
  32. package/dist/analyzer/rules/dropColumnOrTable.js.map +1 -0
  33. package/dist/analyzer/rules/index.d.ts +7 -0
  34. package/dist/analyzer/rules/index.js +28 -0
  35. package/dist/analyzer/rules/index.js.map +1 -0
  36. package/dist/analyzer/rules/indexConcurrentlyInTransaction.d.ts +2 -0
  37. package/dist/analyzer/rules/indexConcurrentlyInTransaction.js +23 -0
  38. package/dist/analyzer/rules/indexConcurrentlyInTransaction.js.map +1 -0
  39. package/dist/analyzer/rules/renameColumnOrTable.d.ts +2 -0
  40. package/dist/analyzer/rules/renameColumnOrTable.js +29 -0
  41. package/dist/analyzer/rules/renameColumnOrTable.js.map +1 -0
  42. package/dist/analyzer/rules/setNotNull.d.ts +2 -0
  43. package/dist/analyzer/rules/setNotNull.js +30 -0
  44. package/dist/analyzer/rules/setNotNull.js.map +1 -0
  45. package/dist/analyzer/suppress.d.ts +23 -0
  46. package/dist/analyzer/suppress.js +36 -0
  47. package/dist/analyzer/suppress.js.map +1 -0
  48. package/dist/analyzer/types.d.ts +93 -0
  49. package/dist/analyzer/types.js +2 -0
  50. package/dist/analyzer/types.js.map +1 -0
  51. package/dist/analyzer/verdict.d.ts +9 -0
  52. package/dist/analyzer/verdict.js +35 -0
  53. package/dist/analyzer/verdict.js.map +1 -0
  54. package/dist/data/lockMatrix.d.ts +17 -0
  55. package/dist/data/lockMatrix.js +65 -0
  56. package/dist/data/lockMatrix.js.map +1 -0
  57. package/dist/format.d.ts +3 -0
  58. package/dist/format.js +52 -0
  59. package/dist/format.js.map +1 -0
  60. package/dist/index.d.ts +2 -0
  61. package/dist/index.js +20 -0
  62. package/dist/index.js.map +1 -0
  63. package/dist/server.d.ts +3 -0
  64. package/dist/server.js +128 -0
  65. package/dist/server.js.map +1 -0
  66. package/package.json +60 -0
@@ -0,0 +1,36 @@
1
+ import { alterChanges, tableName, nameOf, isConstantDefault } from "../astUtils.js";
2
+ export const addColumnVolatileDefault = {
3
+ id: "add-column-volatile-default",
4
+ title: "ADD COLUMN with a non-constant default",
5
+ severity: "warning",
6
+ rationale: "Since PG11 a constant default is free (metadata only), but a volatile/non-constant default (e.g. now(), a function call, a sequence) still forces a full table rewrite under ACCESS EXCLUSIVE.",
7
+ remediation: "Add the column with no default, backfill the computed value in batches, then set the default for future rows.",
8
+ docsUrl: "https://www.postgresql.org/docs/current/sql-altertable.html",
9
+ check(stmt) {
10
+ for (const change of alterChanges(stmt.ast)) {
11
+ if (change?.type !== "add column")
12
+ continue;
13
+ const cons = change?.column?.constraints;
14
+ if (!Array.isArray(cons))
15
+ continue;
16
+ const def = cons.find((c) => c?.type === "default");
17
+ if (!def)
18
+ continue;
19
+ if (isConstantDefault(def.default))
20
+ continue;
21
+ const table = tableName(stmt.ast) ?? "your_table";
22
+ const col = nameOf(change.column) ?? nameOf(change.column?.name) ?? "new_col";
23
+ return {
24
+ message: `Column "${col}" uses a non-constant default; this rewrites the whole table.`,
25
+ lockTaken: "ACCESS EXCLUSIVE",
26
+ suggestedRewrite: [
27
+ `ALTER TABLE ${table} ADD COLUMN ${col} <type>;`,
28
+ `UPDATE ${table} SET ${col} = <expr> WHERE ${col} IS NULL; -- batch this`,
29
+ `ALTER TABLE ${table} ALTER COLUMN ${col} SET DEFAULT <expr>; -- future rows`,
30
+ ].join("\n"),
31
+ };
32
+ }
33
+ return null;
34
+ },
35
+ };
36
+ //# sourceMappingURL=addColumnVolatileDefault.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"addColumnVolatileDefault.js","sourceRoot":"","sources":["../../../src/analyzer/rules/addColumnVolatileDefault.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,YAAY,EAAE,SAAS,EAAE,MAAM,EAAE,iBAAiB,EAAU,MAAM,gBAAgB,CAAC;AAE5F,MAAM,CAAC,MAAM,wBAAwB,GAAS;IAC5C,EAAE,EAAE,6BAA6B;IACjC,KAAK,EAAE,wCAAwC;IAC/C,QAAQ,EAAE,SAAS;IACnB,SAAS,EACP,gMAAgM;IAClM,WAAW,EACT,+GAA+G;IACjH,OAAO,EAAE,6DAA6D;IACtE,KAAK,CAAC,IAAI;QACR,KAAK,MAAM,MAAM,IAAI,YAAY,CAAC,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC;YAC5C,IAAI,MAAM,EAAE,IAAI,KAAK,YAAY;gBAAE,SAAS;YAC5C,MAAM,IAAI,GAAG,MAAM,EAAE,MAAM,EAAE,WAAW,CAAC;YACzC,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,IAAI,CAAC;gBAAE,SAAS;YACnC,MAAM,GAAG,GAAG,IAAI,CAAC,IAAI,CAAC,CAAC,CAAS,EAAE,EAAE,CAAC,CAAC,EAAE,IAAI,KAAK,SAAS,CAAC,CAAC;YAC5D,IAAI,CAAC,GAAG;gBAAE,SAAS;YACnB,IAAI,iBAAiB,CAAC,GAAG,CAAC,OAAO,CAAC;gBAAE,SAAS;YAE7C,MAAM,KAAK,GAAG,SAAS,CAAC,IAAI,CAAC,GAAG,CAAC,IAAI,YAAY,CAAC;YAClD,MAAM,GAAG,GAAG,MAAM,CAAC,MAAM,CAAC,MAAM,CAAC,IAAI,MAAM,CAAC,MAAM,CAAC,MAAM,EAAE,IAAI,CAAC,IAAI,SAAS,CAAC;YAC9E,OAAO;gBACL,OAAO,EAAE,WAAW,GAAG,+DAA+D;gBACtF,SAAS,EAAE,kBAAkB;gBAC7B,gBAAgB,EAAE;oBAChB,eAAe,KAAK,eAAe,GAAG,UAAU;oBAChD,UAAU,KAAK,QAAQ,GAAG,mBAAmB,GAAG,yBAAyB;oBACzE,eAAe,KAAK,iBAAiB,GAAG,qCAAqC;iBAC9E,CAAC,IAAI,CAAC,IAAI,CAAC;aACb,CAAC;QACJ,CAAC;QACD,OAAO,IAAI,CAAC;IACd,CAAC;CACF,CAAC"}
@@ -0,0 +1,2 @@
1
+ import type { Rule } from "../types.js";
2
+ export declare const addForeignKeyValidating: Rule;
@@ -0,0 +1,26 @@
1
+ const FK_RE = /\badd\s+(?:constraint\s+\S+\s+)?foreign\s+key\b/i;
2
+ export const addForeignKeyValidating = {
3
+ id: "add-foreign-key-validating",
4
+ title: "ADD FOREIGN KEY validates immediately",
5
+ severity: "warning",
6
+ rationale: "Adding a foreign key validates every existing row against the referenced table while holding a SHARE ROW EXCLUSIVE lock on BOTH tables, blocking writes during the scan.",
7
+ remediation: "Add the constraint NOT VALID (instant), then VALIDATE CONSTRAINT in a second statement — validation takes only a weak lock and does not block writes.",
8
+ docsUrl: "https://www.postgresql.org/docs/current/sql-altertable.html#SQL-ALTERTABLE-DESC-ADD-TABLE-CONSTRAINT",
9
+ check(stmt) {
10
+ // NOT VALID is the safe form — and the parser rejects it, so this rule is
11
+ // deliberately text-based and only fires on the validating (unsafe) form.
12
+ if (!stmt.lower.startsWith("alter table"))
13
+ return null;
14
+ if (!FK_RE.test(stmt.lower))
15
+ return null;
16
+ if (/\bnot\s+valid\b/i.test(stmt.lower))
17
+ return null;
18
+ const withNotValid = stmt.normalized.replace(/;?\s*$/, " NOT VALID;");
19
+ return {
20
+ message: "This foreign key is validated immediately, scanning the table under a lock.",
21
+ lockTaken: "SHARE ROW EXCLUSIVE (both tables)",
22
+ suggestedRewrite: `${withNotValid}\n-- then, separately:\n-- ALTER TABLE <table> VALIDATE CONSTRAINT <name>;`,
23
+ };
24
+ },
25
+ };
26
+ //# sourceMappingURL=addForeignKeyValidating.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"addForeignKeyValidating.js","sourceRoot":"","sources":["../../../src/analyzer/rules/addForeignKeyValidating.ts"],"names":[],"mappings":"AAEA,MAAM,KAAK,GAAG,kDAAkD,CAAC;AAEjE,MAAM,CAAC,MAAM,uBAAuB,GAAS;IAC3C,EAAE,EAAE,4BAA4B;IAChC,KAAK,EAAE,uCAAuC;IAC9C,QAAQ,EAAE,SAAS;IACnB,SAAS,EACP,0KAA0K;IAC5K,WAAW,EACT,uJAAuJ;IACzJ,OAAO,EAAE,sGAAsG;IAC/G,KAAK,CAAC,IAAI;QACR,0EAA0E;QAC1E,0EAA0E;QAC1E,IAAI,CAAC,IAAI,CAAC,KAAK,CAAC,UAAU,CAAC,aAAa,CAAC;YAAE,OAAO,IAAI,CAAC;QACvD,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,KAAK,CAAC;YAAE,OAAO,IAAI,CAAC;QACzC,IAAI,kBAAkB,CAAC,IAAI,CAAC,IAAI,CAAC,KAAK,CAAC;YAAE,OAAO,IAAI,CAAC;QAErD,MAAM,YAAY,GAAG,IAAI,CAAC,UAAU,CAAC,OAAO,CAAC,QAAQ,EAAE,aAAa,CAAC,CAAC;QACtE,OAAO;YACL,OAAO,EAAE,6EAA6E;YACtF,SAAS,EAAE,mCAAmC;YAC9C,gBAAgB,EAAE,GAAG,YAAY,4EAA4E;SAC9G,CAAC;IACJ,CAAC;CACF,CAAC"}
@@ -0,0 +1,2 @@
1
+ import type { Rule } from "../types.js";
2
+ export declare const alterColumnType: Rule;
@@ -0,0 +1,31 @@
1
+ import { alterChanges, tableName, nameOf } from "../astUtils.js";
2
+ export const alterColumnType = {
3
+ id: "alter-column-type",
4
+ title: "ALTER COLUMN ... TYPE rewrites the table",
5
+ severity: "critical",
6
+ rationale: "Changing a column's type generally rewrites every row and holds ACCESS EXCLUSIVE for the whole operation, blocking reads and writes. (A few binary-compatible changes are exceptions, but assume a rewrite.)",
7
+ remediation: "Add a new column of the target type, backfill it in batches, swap reads/writes over, then drop the old column — the expand/contract pattern.",
8
+ docsUrl: "https://www.postgresql.org/docs/current/sql-altertable.html#SQL-ALTERTABLE-NOTES",
9
+ check(stmt) {
10
+ for (const change of alterChanges(stmt.ast)) {
11
+ if (change?.type !== "alter column")
12
+ continue;
13
+ if (change?.alter?.type !== "set type")
14
+ continue;
15
+ const table = tableName(stmt.ast) ?? "your_table";
16
+ const col = nameOf(change.column) ?? "col";
17
+ const newType = nameOf(change.alter?.dataType) ?? "<new_type>";
18
+ return {
19
+ message: `Changing the type of "${col}" rewrites the entire table and blocks reads and writes.`,
20
+ lockTaken: "ACCESS EXCLUSIVE",
21
+ suggestedRewrite: [
22
+ `ALTER TABLE ${table} ADD COLUMN ${col}_new ${newType};`,
23
+ `UPDATE ${table} SET ${col}_new = ${col}::${newType}; -- batch this`,
24
+ `-- then swap in application + rename in a later migration; drop the old column last`,
25
+ ].join("\n"),
26
+ };
27
+ }
28
+ return null;
29
+ },
30
+ };
31
+ //# sourceMappingURL=alterColumnType.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"alterColumnType.js","sourceRoot":"","sources":["../../../src/analyzer/rules/alterColumnType.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,YAAY,EAAE,SAAS,EAAE,MAAM,EAAE,MAAM,gBAAgB,CAAC;AAEjE,MAAM,CAAC,MAAM,eAAe,GAAS;IACnC,EAAE,EAAE,mBAAmB;IACvB,KAAK,EAAE,0CAA0C;IACjD,QAAQ,EAAE,UAAU;IACpB,SAAS,EACP,8MAA8M;IAChN,WAAW,EACT,8IAA8I;IAChJ,OAAO,EAAE,kFAAkF;IAC3F,KAAK,CAAC,IAAI;QACR,KAAK,MAAM,MAAM,IAAI,YAAY,CAAC,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC;YAC5C,IAAI,MAAM,EAAE,IAAI,KAAK,cAAc;gBAAE,SAAS;YAC9C,IAAI,MAAM,EAAE,KAAK,EAAE,IAAI,KAAK,UAAU;gBAAE,SAAS;YACjD,MAAM,KAAK,GAAG,SAAS,CAAC,IAAI,CAAC,GAAG,CAAC,IAAI,YAAY,CAAC;YAClD,MAAM,GAAG,GAAG,MAAM,CAAC,MAAM,CAAC,MAAM,CAAC,IAAI,KAAK,CAAC;YAC3C,MAAM,OAAO,GAAG,MAAM,CAAC,MAAM,CAAC,KAAK,EAAE,QAAQ,CAAC,IAAI,YAAY,CAAC;YAC/D,OAAO;gBACL,OAAO,EAAE,yBAAyB,GAAG,0DAA0D;gBAC/F,SAAS,EAAE,kBAAkB;gBAC7B,gBAAgB,EAAE;oBAChB,eAAe,KAAK,eAAe,GAAG,QAAQ,OAAO,GAAG;oBACxD,UAAU,KAAK,QAAQ,GAAG,UAAU,GAAG,KAAK,OAAO,iBAAiB;oBACpE,qFAAqF;iBACtF,CAAC,IAAI,CAAC,IAAI,CAAC;aACb,CAAC;QACJ,CAAC;QACD,OAAO,IAAI,CAAC;IACd,CAAC;CACF,CAAC"}
@@ -0,0 +1,2 @@
1
+ import type { Rule } from "../types.js";
2
+ export declare const createIndexNonConcurrent: Rule;
@@ -0,0 +1,24 @@
1
+ import { astType } from "../astUtils.js";
2
+ export const createIndexNonConcurrent = {
3
+ id: "create-index-non-concurrent",
4
+ title: "CREATE INDEX without CONCURRENTLY",
5
+ severity: "critical",
6
+ rationale: "A plain CREATE INDEX takes a SHARE lock on the table, blocking ALL writes (INSERT/UPDATE/DELETE) for the entire duration of the build. On a large table that can be minutes of downtime.",
7
+ remediation: "Build the index with CREATE INDEX CONCURRENTLY, which does not block writes.",
8
+ docsUrl: "https://www.postgresql.org/docs/current/sql-createindex.html#SQL-CREATEINDEX-CONCURRENTLY",
9
+ check(stmt) {
10
+ const isCreateIndex = astType(stmt) === "create index" || /^create\s+(unique\s+)?index\b/i.test(stmt.lower);
11
+ if (!isCreateIndex)
12
+ return null;
13
+ const concurrently = stmt.ast?.concurrently === true || /\bconcurrently\b/i.test(stmt.lower);
14
+ if (concurrently)
15
+ return null;
16
+ const rewrite = stmt.normalized.replace(/^create\s+(unique\s+)?index\b/i, (_m, unique) => `CREATE ${unique ? "UNIQUE " : ""}INDEX CONCURRENTLY`);
17
+ return {
18
+ message: "This index build will block all writes to the table until it completes.",
19
+ lockTaken: "SHARE (blocks writes)",
20
+ suggestedRewrite: rewrite,
21
+ };
22
+ },
23
+ };
24
+ //# sourceMappingURL=createIndexNonConcurrent.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"createIndexNonConcurrent.js","sourceRoot":"","sources":["../../../src/analyzer/rules/createIndexNonConcurrent.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,OAAO,EAAU,MAAM,gBAAgB,CAAC;AAEjD,MAAM,CAAC,MAAM,wBAAwB,GAAS;IAC5C,EAAE,EAAE,6BAA6B;IACjC,KAAK,EAAE,mCAAmC;IAC1C,QAAQ,EAAE,UAAU;IACpB,SAAS,EACP,0LAA0L;IAC5L,WAAW,EAAE,8EAA8E;IAC3F,OAAO,EAAE,2FAA2F;IACpG,KAAK,CAAC,IAAI;QACR,MAAM,aAAa,GACjB,OAAO,CAAC,IAAI,CAAC,KAAK,cAAc,IAAI,gCAAgC,CAAC,IAAI,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;QACxF,IAAI,CAAC,aAAa;YAAE,OAAO,IAAI,CAAC;QAEhC,MAAM,YAAY,GACf,IAAI,CAAC,GAAc,EAAE,YAAY,KAAK,IAAI,IAAI,mBAAmB,CAAC,IAAI,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;QACtF,IAAI,YAAY;YAAE,OAAO,IAAI,CAAC;QAE9B,MAAM,OAAO,GAAG,IAAI,CAAC,UAAU,CAAC,OAAO,CACrC,gCAAgC,EAChC,CAAC,EAAE,EAAE,MAAM,EAAE,EAAE,CAAC,UAAU,MAAM,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,EAAE,oBAAoB,CACtE,CAAC;QAEF,OAAO;YACL,OAAO,EAAE,yEAAyE;YAClF,SAAS,EAAE,uBAAuB;YAClC,gBAAgB,EAAE,OAAO;SAC1B,CAAC;IACJ,CAAC;CACF,CAAC"}
@@ -0,0 +1,2 @@
1
+ import type { Rule } from "../types.js";
2
+ export declare const dropColumnOrTable: Rule;
@@ -0,0 +1,31 @@
1
+ import { astType, alterChanges, tableName, nameOf } from "../astUtils.js";
2
+ export const dropColumnOrTable = {
3
+ id: "drop-column-or-table",
4
+ title: "Destructive DROP (column or table)",
5
+ severity: "warning",
6
+ rationale: "Dropping a column or table is irreversible data loss and instantly breaks any deployed application code that still references it. The lock is brief, but a still-running old release will start erroring the moment it lands.",
7
+ remediation: "Use expand/contract: first ship a release that no longer reads the column/table, confirm nothing references it, then drop it in a later migration.",
8
+ docsUrl: "https://www.postgresql.org/docs/current/sql-altertable.html",
9
+ check(stmt) {
10
+ // DROP TABLE
11
+ if (astType(stmt) === "drop table" || /^drop\s+table\b/i.test(stmt.lower)) {
12
+ return {
13
+ message: "Dropping a table destroys its data and breaks any code still using it.",
14
+ lockTaken: "ACCESS EXCLUSIVE (brief)",
15
+ };
16
+ }
17
+ // ALTER TABLE ... DROP COLUMN
18
+ for (const change of alterChanges(stmt.ast)) {
19
+ if (change?.type !== "drop column")
20
+ continue;
21
+ const table = tableName(stmt.ast) ?? "your_table";
22
+ const col = nameOf(change.column) ?? "col";
23
+ return {
24
+ message: `Dropping column "${col}" from ${table} destroys its data and breaks code still reading it.`,
25
+ lockTaken: "ACCESS EXCLUSIVE (brief)",
26
+ };
27
+ }
28
+ return null;
29
+ },
30
+ };
31
+ //# sourceMappingURL=dropColumnOrTable.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"dropColumnOrTable.js","sourceRoot":"","sources":["../../../src/analyzer/rules/dropColumnOrTable.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,OAAO,EAAE,YAAY,EAAE,SAAS,EAAE,MAAM,EAAE,MAAM,gBAAgB,CAAC;AAE1E,MAAM,CAAC,MAAM,iBAAiB,GAAS;IACrC,EAAE,EAAE,sBAAsB;IAC1B,KAAK,EAAE,oCAAoC;IAC3C,QAAQ,EAAE,SAAS;IACnB,SAAS,EACP,+NAA+N;IACjO,WAAW,EACT,oJAAoJ;IACtJ,OAAO,EAAE,6DAA6D;IACtE,KAAK,CAAC,IAAI;QACR,aAAa;QACb,IAAI,OAAO,CAAC,IAAI,CAAC,KAAK,YAAY,IAAI,kBAAkB,CAAC,IAAI,CAAC,IAAI,CAAC,KAAK,CAAC,EAAE,CAAC;YAC1E,OAAO;gBACL,OAAO,EAAE,wEAAwE;gBACjF,SAAS,EAAE,0BAA0B;aACtC,CAAC;QACJ,CAAC;QACD,8BAA8B;QAC9B,KAAK,MAAM,MAAM,IAAI,YAAY,CAAC,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC;YAC5C,IAAI,MAAM,EAAE,IAAI,KAAK,aAAa;gBAAE,SAAS;YAC7C,MAAM,KAAK,GAAG,SAAS,CAAC,IAAI,CAAC,GAAG,CAAC,IAAI,YAAY,CAAC;YAClD,MAAM,GAAG,GAAG,MAAM,CAAC,MAAM,CAAC,MAAM,CAAC,IAAI,KAAK,CAAC;YAC3C,OAAO;gBACL,OAAO,EAAE,oBAAoB,GAAG,UAAU,KAAK,sDAAsD;gBACrG,SAAS,EAAE,0BAA0B;aACtC,CAAC;QACJ,CAAC;QACD,OAAO,IAAI,CAAC;IACd,CAAC;CACF,CAAC"}
@@ -0,0 +1,7 @@
1
+ import type { Rule } from "../types.js";
2
+ /**
3
+ * The rule catalog. Adding a new safety check is a one-file change plus one
4
+ * line here — rules are pure, independent, and carry their own metadata.
5
+ */
6
+ export declare const rules: Rule[];
7
+ export declare const rulesById: Map<string, Rule>;
@@ -0,0 +1,28 @@
1
+ import { createIndexNonConcurrent } from "./createIndexNonConcurrent.js";
2
+ import { indexConcurrentlyInTransaction } from "./indexConcurrentlyInTransaction.js";
3
+ import { addColumnNotNullNoDefault } from "./addColumnNotNullNoDefault.js";
4
+ import { addColumnVolatileDefault } from "./addColumnVolatileDefault.js";
5
+ import { alterColumnType } from "./alterColumnType.js";
6
+ import { setNotNull } from "./setNotNull.js";
7
+ import { addForeignKeyValidating } from "./addForeignKeyValidating.js";
8
+ import { addCheckConstraintNoNotValid } from "./addCheckConstraintNoNotValid.js";
9
+ import { dropColumnOrTable } from "./dropColumnOrTable.js";
10
+ import { renameColumnOrTable } from "./renameColumnOrTable.js";
11
+ /**
12
+ * The rule catalog. Adding a new safety check is a one-file change plus one
13
+ * line here — rules are pure, independent, and carry their own metadata.
14
+ */
15
+ export const rules = [
16
+ createIndexNonConcurrent,
17
+ indexConcurrentlyInTransaction,
18
+ addColumnNotNullNoDefault,
19
+ addColumnVolatileDefault,
20
+ alterColumnType,
21
+ setNotNull,
22
+ addForeignKeyValidating,
23
+ addCheckConstraintNoNotValid,
24
+ dropColumnOrTable,
25
+ renameColumnOrTable,
26
+ ];
27
+ export const rulesById = new Map(rules.map((r) => [r.id, r]));
28
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../../src/analyzer/rules/index.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,wBAAwB,EAAE,MAAM,+BAA+B,CAAC;AACzE,OAAO,EAAE,8BAA8B,EAAE,MAAM,qCAAqC,CAAC;AACrF,OAAO,EAAE,yBAAyB,EAAE,MAAM,gCAAgC,CAAC;AAC3E,OAAO,EAAE,wBAAwB,EAAE,MAAM,+BAA+B,CAAC;AACzE,OAAO,EAAE,eAAe,EAAE,MAAM,sBAAsB,CAAC;AACvD,OAAO,EAAE,UAAU,EAAE,MAAM,iBAAiB,CAAC;AAC7C,OAAO,EAAE,uBAAuB,EAAE,MAAM,8BAA8B,CAAC;AACvE,OAAO,EAAE,4BAA4B,EAAE,MAAM,mCAAmC,CAAC;AACjF,OAAO,EAAE,iBAAiB,EAAE,MAAM,wBAAwB,CAAC;AAC3D,OAAO,EAAE,mBAAmB,EAAE,MAAM,0BAA0B,CAAC;AAE/D;;;GAGG;AACH,MAAM,CAAC,MAAM,KAAK,GAAW;IAC3B,wBAAwB;IACxB,8BAA8B;IAC9B,yBAAyB;IACzB,wBAAwB;IACxB,eAAe;IACf,UAAU;IACV,uBAAuB;IACvB,4BAA4B;IAC5B,iBAAiB;IACjB,mBAAmB;CACpB,CAAC;AAEF,MAAM,CAAC,MAAM,SAAS,GAAsB,IAAI,GAAG,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,CAAC,CAAC"}
@@ -0,0 +1,2 @@
1
+ import type { Rule } from "../types.js";
2
+ export declare const indexConcurrentlyInTransaction: Rule;
@@ -0,0 +1,23 @@
1
+ import { astType } from "../astUtils.js";
2
+ export const indexConcurrentlyInTransaction = {
3
+ id: "index-concurrently-in-transaction",
4
+ title: "CREATE INDEX CONCURRENTLY inside a transaction",
5
+ severity: "critical",
6
+ rationale: "CREATE INDEX CONCURRENTLY cannot run inside a transaction block — Postgres raises an error. Many migration frameworks wrap every migration in BEGIN/COMMIT by default, so this fails at deploy time.",
7
+ remediation: "Run the CONCURRENTLY index outside the surrounding transaction (e.g. disable the framework's automatic transaction for this migration).",
8
+ docsUrl: "https://www.postgresql.org/docs/current/sql-createindex.html#SQL-CREATEINDEX-CONCURRENTLY",
9
+ check(stmt, ctx) {
10
+ if (!ctx.inExplicitTransaction)
11
+ return null;
12
+ const isCreateIndex = astType(stmt) === "create index" || /^create\s+(unique\s+)?index\b/i.test(stmt.lower);
13
+ if (!isCreateIndex)
14
+ return null;
15
+ const concurrently = stmt.ast?.concurrently === true || /\bconcurrently\b/i.test(stmt.lower);
16
+ if (!concurrently)
17
+ return null;
18
+ return {
19
+ message: "CONCURRENTLY is used inside an explicit BEGIN/COMMIT block; Postgres will reject this with 'CREATE INDEX CONCURRENTLY cannot run inside a transaction block'.",
20
+ };
21
+ },
22
+ };
23
+ //# sourceMappingURL=indexConcurrentlyInTransaction.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"indexConcurrentlyInTransaction.js","sourceRoot":"","sources":["../../../src/analyzer/rules/indexConcurrentlyInTransaction.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,OAAO,EAAU,MAAM,gBAAgB,CAAC;AAEjD,MAAM,CAAC,MAAM,8BAA8B,GAAS;IAClD,EAAE,EAAE,mCAAmC;IACvC,KAAK,EAAE,gDAAgD;IACvD,QAAQ,EAAE,UAAU;IACpB,SAAS,EACP,sMAAsM;IACxM,WAAW,EACT,yIAAyI;IAC3I,OAAO,EAAE,2FAA2F;IACpG,KAAK,CAAC,IAAI,EAAE,GAAG;QACb,IAAI,CAAC,GAAG,CAAC,qBAAqB;YAAE,OAAO,IAAI,CAAC;QAC5C,MAAM,aAAa,GACjB,OAAO,CAAC,IAAI,CAAC,KAAK,cAAc,IAAI,gCAAgC,CAAC,IAAI,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;QACxF,IAAI,CAAC,aAAa;YAAE,OAAO,IAAI,CAAC;QAChC,MAAM,YAAY,GACf,IAAI,CAAC,GAAc,EAAE,YAAY,KAAK,IAAI,IAAI,mBAAmB,CAAC,IAAI,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;QACtF,IAAI,CAAC,YAAY;YAAE,OAAO,IAAI,CAAC;QAE/B,OAAO;YACL,OAAO,EACL,+JAA+J;SAClK,CAAC;IACJ,CAAC;CACF,CAAC"}
@@ -0,0 +1,2 @@
1
+ import type { Rule } from "../types.js";
2
+ export declare const renameColumnOrTable: Rule;
@@ -0,0 +1,29 @@
1
+ import { alterChanges, nameOf } from "../astUtils.js";
2
+ const RENAME_TABLE_RE = /^alter\s+table\s+.+\brename\s+to\b/i;
3
+ export const renameColumnOrTable = {
4
+ id: "rename-column-or-table",
5
+ title: "RENAME breaks running application code",
6
+ severity: "warning",
7
+ rationale: "A rename is metadata-only and fast at the database level, but it is a breaking change: any currently-deployed code referencing the old name starts failing the instant the migration lands, before the new code rolls out.",
8
+ remediation: "Avoid renames in a single step. Add the new name (column/view) alongside the old, migrate readers/writers, then remove the old name in a later release.",
9
+ docsUrl: "https://www.postgresql.org/docs/current/sql-altertable.html",
10
+ check(stmt) {
11
+ for (const change of alterChanges(stmt.ast)) {
12
+ if (change?.type === "rename column") {
13
+ const from = nameOf(change.column) ?? "old";
14
+ const to = nameOf(change.to) ?? "new";
15
+ return {
16
+ message: `Renaming column "${from}" to "${to}" breaks code that still selects "${from}".`,
17
+ };
18
+ }
19
+ if (change?.type === "rename") {
20
+ return { message: "Renaming the table breaks code that still references the old name." };
21
+ }
22
+ }
23
+ if (RENAME_TABLE_RE.test(stmt.lower)) {
24
+ return { message: "Renaming the table breaks code that still references the old name." };
25
+ }
26
+ return null;
27
+ },
28
+ };
29
+ //# sourceMappingURL=renameColumnOrTable.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"renameColumnOrTable.js","sourceRoot":"","sources":["../../../src/analyzer/rules/renameColumnOrTable.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,YAAY,EAAE,MAAM,EAAE,MAAM,gBAAgB,CAAC;AAEtD,MAAM,eAAe,GAAG,qCAAqC,CAAC;AAE9D,MAAM,CAAC,MAAM,mBAAmB,GAAS;IACvC,EAAE,EAAE,wBAAwB;IAC5B,KAAK,EAAE,wCAAwC;IAC/C,QAAQ,EAAE,SAAS;IACnB,SAAS,EACP,4NAA4N;IAC9N,WAAW,EACT,yJAAyJ;IAC3J,OAAO,EAAE,6DAA6D;IACtE,KAAK,CAAC,IAAI;QACR,KAAK,MAAM,MAAM,IAAI,YAAY,CAAC,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC;YAC5C,IAAI,MAAM,EAAE,IAAI,KAAK,eAAe,EAAE,CAAC;gBACrC,MAAM,IAAI,GAAG,MAAM,CAAC,MAAM,CAAC,MAAM,CAAC,IAAI,KAAK,CAAC;gBAC5C,MAAM,EAAE,GAAG,MAAM,CAAC,MAAM,CAAC,EAAE,CAAC,IAAI,KAAK,CAAC;gBACtC,OAAO;oBACL,OAAO,EAAE,oBAAoB,IAAI,SAAS,EAAE,qCAAqC,IAAI,IAAI;iBAC1F,CAAC;YACJ,CAAC;YACD,IAAI,MAAM,EAAE,IAAI,KAAK,QAAQ,EAAE,CAAC;gBAC9B,OAAO,EAAE,OAAO,EAAE,oEAAoE,EAAE,CAAC;YAC3F,CAAC;QACH,CAAC;QACD,IAAI,eAAe,CAAC,IAAI,CAAC,IAAI,CAAC,KAAK,CAAC,EAAE,CAAC;YACrC,OAAO,EAAE,OAAO,EAAE,oEAAoE,EAAE,CAAC;QAC3F,CAAC;QACD,OAAO,IAAI,CAAC;IACd,CAAC;CACF,CAAC"}
@@ -0,0 +1,2 @@
1
+ import type { Rule } from "../types.js";
2
+ export declare const setNotNull: Rule;
@@ -0,0 +1,30 @@
1
+ import { alterChanges, tableName, nameOf } from "../astUtils.js";
2
+ export const setNotNull = {
3
+ id: "set-not-null",
4
+ title: "SET NOT NULL scans the whole table",
5
+ severity: "warning",
6
+ rationale: "ALTER COLUMN ... SET NOT NULL must scan every row to verify no NULLs while holding ACCESS EXCLUSIVE, blocking reads and writes for the duration on a large table.",
7
+ remediation: "Add a CHECK (col IS NOT NULL) NOT VALID, VALIDATE it (a non-blocking scan), then SET NOT NULL — which on PG12+ reuses the validated constraint and is instant.",
8
+ docsUrl: "https://www.postgresql.org/docs/current/sql-altertable.html",
9
+ check(stmt) {
10
+ for (const change of alterChanges(stmt.ast)) {
11
+ if (change?.type !== "alter column")
12
+ continue;
13
+ if (change?.alter?.type !== "set not null")
14
+ continue;
15
+ const table = tableName(stmt.ast) ?? "your_table";
16
+ const col = nameOf(change.column) ?? "col";
17
+ return {
18
+ message: `SET NOT NULL on "${col}" scans the whole table under an exclusive lock.`,
19
+ lockTaken: "ACCESS EXCLUSIVE",
20
+ suggestedRewrite: [
21
+ `ALTER TABLE ${table} ADD CONSTRAINT ${col}_not_null CHECK (${col} IS NOT NULL) NOT VALID;`,
22
+ `ALTER TABLE ${table} VALIDATE CONSTRAINT ${col}_not_null; -- non-blocking scan`,
23
+ `ALTER TABLE ${table} ALTER COLUMN ${col} SET NOT NULL; -- instant on PG12+`,
24
+ ].join("\n"),
25
+ };
26
+ }
27
+ return null;
28
+ },
29
+ };
30
+ //# sourceMappingURL=setNotNull.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"setNotNull.js","sourceRoot":"","sources":["../../../src/analyzer/rules/setNotNull.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,YAAY,EAAE,SAAS,EAAE,MAAM,EAAE,MAAM,gBAAgB,CAAC;AAEjE,MAAM,CAAC,MAAM,UAAU,GAAS;IAC9B,EAAE,EAAE,cAAc;IAClB,KAAK,EAAE,oCAAoC;IAC3C,QAAQ,EAAE,SAAS;IACnB,SAAS,EACP,mKAAmK;IACrK,WAAW,EACT,gKAAgK;IAClK,OAAO,EAAE,6DAA6D;IACtE,KAAK,CAAC,IAAI;QACR,KAAK,MAAM,MAAM,IAAI,YAAY,CAAC,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC;YAC5C,IAAI,MAAM,EAAE,IAAI,KAAK,cAAc;gBAAE,SAAS;YAC9C,IAAI,MAAM,EAAE,KAAK,EAAE,IAAI,KAAK,cAAc;gBAAE,SAAS;YACrD,MAAM,KAAK,GAAG,SAAS,CAAC,IAAI,CAAC,GAAG,CAAC,IAAI,YAAY,CAAC;YAClD,MAAM,GAAG,GAAG,MAAM,CAAC,MAAM,CAAC,MAAM,CAAC,IAAI,KAAK,CAAC;YAC3C,OAAO;gBACL,OAAO,EAAE,oBAAoB,GAAG,kDAAkD;gBAClF,SAAS,EAAE,kBAAkB;gBAC7B,gBAAgB,EAAE;oBAChB,eAAe,KAAK,mBAAmB,GAAG,oBAAoB,GAAG,0BAA0B;oBAC3F,eAAe,KAAK,wBAAwB,GAAG,iCAAiC;oBAChF,eAAe,KAAK,iBAAiB,GAAG,oCAAoC;iBAC7E,CAAC,IAAI,CAAC,IAAI,CAAC;aACb,CAAC;QACJ,CAAC;QACD,OAAO,IAAI,CAAC;IACd,CAAC;CACF,CAAC"}
@@ -0,0 +1,23 @@
1
+ /**
2
+ * eslint-style inline suppression. A comment of the form
3
+ *
4
+ * -- locksmith:disable create-index-non-concurrent, set-not-null
5
+ * -- locksmith:disable (no ids => suppress everything)
6
+ *
7
+ * suppresses the listed rules (or all rules) for the statement it precedes
8
+ * (its leading comment lines) or sits on. This keeps the tool honest: a team
9
+ * can acknowledge a deliberate risk without disabling the rule globally.
10
+ */
11
+ export interface Directive {
12
+ line: number;
13
+ /** Specific rule ids, or "all" when no ids were given. */
14
+ rules: Set<string> | "all";
15
+ }
16
+ export declare function parseDirectives(sql: string): Directive[];
17
+ /**
18
+ * Is `ruleId` suppressed for a statement occupying lines
19
+ * [leadingFrom .. statementEndLine]? A directive applies if it lands anywhere
20
+ * in that range — i.e. on the statement itself or in the comment block
21
+ * immediately above it.
22
+ */
23
+ export declare function isSuppressed(ruleId: string, directives: Directive[], leadingFrom: number, statementEndLine: number): boolean;
@@ -0,0 +1,36 @@
1
+ const DIRECTIVE_RE = /--\s*locksmith:disable\b(.*)$/i;
2
+ export function parseDirectives(sql) {
3
+ const out = [];
4
+ const lines = sql.split("\n");
5
+ lines.forEach((text, idx) => {
6
+ const m = DIRECTIVE_RE.exec(text);
7
+ if (!m)
8
+ return;
9
+ const rest = m[1].trim();
10
+ if (rest.length === 0) {
11
+ out.push({ line: idx + 1, rules: "all" });
12
+ }
13
+ else {
14
+ const ids = rest
15
+ .split(/[\s,]+/)
16
+ .map((s) => s.trim())
17
+ .filter(Boolean);
18
+ out.push({ line: idx + 1, rules: new Set(ids) });
19
+ }
20
+ });
21
+ return out;
22
+ }
23
+ /**
24
+ * Is `ruleId` suppressed for a statement occupying lines
25
+ * [leadingFrom .. statementEndLine]? A directive applies if it lands anywhere
26
+ * in that range — i.e. on the statement itself or in the comment block
27
+ * immediately above it.
28
+ */
29
+ export function isSuppressed(ruleId, directives, leadingFrom, statementEndLine) {
30
+ return directives.some((d) => {
31
+ if (d.line < leadingFrom || d.line > statementEndLine)
32
+ return false;
33
+ return d.rules === "all" || d.rules.has(ruleId);
34
+ });
35
+ }
36
+ //# sourceMappingURL=suppress.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"suppress.js","sourceRoot":"","sources":["../../src/analyzer/suppress.ts"],"names":[],"mappings":"AAgBA,MAAM,YAAY,GAAG,gCAAgC,CAAC;AAEtD,MAAM,UAAU,eAAe,CAAC,GAAW;IACzC,MAAM,GAAG,GAAgB,EAAE,CAAC;IAC5B,MAAM,KAAK,GAAG,GAAG,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC;IAC9B,KAAK,CAAC,OAAO,CAAC,CAAC,IAAI,EAAE,GAAG,EAAE,EAAE;QAC1B,MAAM,CAAC,GAAG,YAAY,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;QAClC,IAAI,CAAC,CAAC;YAAE,OAAO;QACf,MAAM,IAAI,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,IAAI,EAAE,CAAC;QACzB,IAAI,IAAI,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;YACtB,GAAG,CAAC,IAAI,CAAC,EAAE,IAAI,EAAE,GAAG,GAAG,CAAC,EAAE,KAAK,EAAE,KAAK,EAAE,CAAC,CAAC;QAC5C,CAAC;aAAM,CAAC;YACN,MAAM,GAAG,GAAG,IAAI;iBACb,KAAK,CAAC,QAAQ,CAAC;iBACf,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,IAAI,EAAE,CAAC;iBACpB,MAAM,CAAC,OAAO,CAAC,CAAC;YACnB,GAAG,CAAC,IAAI,CAAC,EAAE,IAAI,EAAE,GAAG,GAAG,CAAC,EAAE,KAAK,EAAE,IAAI,GAAG,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC;QACnD,CAAC;IACH,CAAC,CAAC,CAAC;IACH,OAAO,GAAG,CAAC;AACb,CAAC;AAED;;;;;GAKG;AACH,MAAM,UAAU,YAAY,CAC1B,MAAc,EACd,UAAuB,EACvB,WAAmB,EACnB,gBAAwB;IAExB,OAAO,UAAU,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE;QAC3B,IAAI,CAAC,CAAC,IAAI,GAAG,WAAW,IAAI,CAAC,CAAC,IAAI,GAAG,gBAAgB;YAAE,OAAO,KAAK,CAAC;QACpE,OAAO,CAAC,CAAC,KAAK,KAAK,KAAK,IAAI,CAAC,CAAC,KAAK,CAAC,GAAG,CAAC,MAAM,CAAC,CAAC;IAClD,CAAC,CAAC,CAAC;AACL,CAAC"}
@@ -0,0 +1,93 @@
1
+ import type { Statement } from "pgsql-ast-parser";
2
+ /** How serious a finding is. Drives the overall verdict. */
3
+ export type Severity = "info" | "warning" | "critical";
4
+ /** Final gate decision derived from the worst finding present. */
5
+ export type Verdict = "PASS" | "REVIEW" | "BLOCK";
6
+ /**
7
+ * One parsed statement from a migration. Rules get BOTH the AST (when the
8
+ * parser understood the statement) and normalized text — because some
9
+ * Postgres clauses the rules care about (NOT VALID, CONCURRENTLY) are either
10
+ * unmodeled or outright rejected by the parser, so text is the reliable signal.
11
+ */
12
+ export interface Stmt {
13
+ /** Original statement text, trailing semicolon removed. */
14
+ raw: string;
15
+ /** Comments stripped, whitespace collapsed; original case preserved. */
16
+ normalized: string;
17
+ /** `normalized` lowercased — convenient for keyword matching. */
18
+ lower: string;
19
+ /** 1-based line where this statement begins in the original input. */
20
+ startLine: number;
21
+ /** 1-based line where this statement ends. */
22
+ endLine: number;
23
+ /** Parsed AST, or null if the parser rejected this statement. */
24
+ ast: Statement | null;
25
+ /** Parser error message, present only when `ast` is null. */
26
+ parseError?: string;
27
+ }
28
+ /** Shared context a rule may consult while evaluating a statement. */
29
+ export interface RuleContext {
30
+ /**
31
+ * When true (the safe default), rules assume the target tables are large /
32
+ * hot, so a full scan or rewrite is treated as dangerous. Set false to
33
+ * silence size-dependent rules for known-small tables.
34
+ */
35
+ assumeLargeTables: boolean;
36
+ /** True if this statement runs inside an explicit BEGIN/START TRANSACTION. */
37
+ inExplicitTransaction: boolean;
38
+ }
39
+ /** What a rule returns when it matches a statement. */
40
+ export interface RuleHit {
41
+ /** Specific, human-readable description of the problem in this statement. */
42
+ message: string;
43
+ /** The Postgres lock this operation acquires, if relevant. */
44
+ lockTaken?: string;
45
+ /** A concrete, safer rewrite the author can paste in. */
46
+ suggestedRewrite?: string;
47
+ }
48
+ /** A safety rule: pure metadata + a pure check function. */
49
+ export interface Rule {
50
+ /** Stable kebab-case id, used in output and suppression directives. */
51
+ id: string;
52
+ title: string;
53
+ severity: Severity;
54
+ /** Why this operation is dangerous (general explanation). */
55
+ rationale: string;
56
+ /** General remediation guidance. */
57
+ remediation: string;
58
+ /** Link to authoritative docs. */
59
+ docsUrl?: string;
60
+ /** Returns a hit if the statement violates this rule, else null. */
61
+ check(stmt: Stmt, ctx: RuleContext): RuleHit | null;
62
+ }
63
+ /** A rule hit promoted to a reportable finding (rule metadata folded in). */
64
+ export interface Finding {
65
+ ruleId: string;
66
+ title: string;
67
+ severity: Severity;
68
+ line: number;
69
+ statement: string;
70
+ lockTaken?: string;
71
+ message: string;
72
+ rationale: string;
73
+ remediation: string;
74
+ suggestedRewrite?: string;
75
+ docsUrl?: string;
76
+ }
77
+ export interface AnalysisStats {
78
+ statements: number;
79
+ findings: number;
80
+ critical: number;
81
+ warning: number;
82
+ info: number;
83
+ suppressed: number;
84
+ /** Statements the parser could not understand (still text-scanned by rules). */
85
+ unparsed: number;
86
+ }
87
+ /** The complete result of analyzing a migration. */
88
+ export interface AnalysisResult {
89
+ verdict: Verdict;
90
+ summary: string;
91
+ stats: AnalysisStats;
92
+ findings: Finding[];
93
+ }
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=types.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.js","sourceRoot":"","sources":["../../src/analyzer/types.ts"],"names":[],"mappings":""}
@@ -0,0 +1,9 @@
1
+ import type { Finding, Verdict } from "./types.js";
2
+ /**
3
+ * Map the set of findings to a single gate decision:
4
+ * - any critical => BLOCK (will likely cause an outage; do not ship as-is)
5
+ * - any warning => REVIEW (risky; a human should confirm it's safe here)
6
+ * - otherwise => PASS
7
+ */
8
+ export declare function computeVerdict(findings: Finding[]): Verdict;
9
+ export declare function summarize(verdict: Verdict, findings: Finding[]): string;
@@ -0,0 +1,35 @@
1
+ /**
2
+ * Map the set of findings to a single gate decision:
3
+ * - any critical => BLOCK (will likely cause an outage; do not ship as-is)
4
+ * - any warning => REVIEW (risky; a human should confirm it's safe here)
5
+ * - otherwise => PASS
6
+ */
7
+ export function computeVerdict(findings) {
8
+ if (findings.some((f) => f.severity === "critical"))
9
+ return "BLOCK";
10
+ if (findings.some((f) => f.severity === "warning"))
11
+ return "REVIEW";
12
+ return "PASS";
13
+ }
14
+ export function summarize(verdict, findings) {
15
+ if (findings.length === 0) {
16
+ return "No locking hazards detected. Migration looks safe to apply.";
17
+ }
18
+ const crit = findings.filter((f) => f.severity === "critical").length;
19
+ const warn = findings.filter((f) => f.severity === "warning").length;
20
+ const parts = [];
21
+ if (crit)
22
+ parts.push(`${crit} critical`);
23
+ if (warn)
24
+ parts.push(`${warn} warning${warn === 1 ? "" : "s"}`);
25
+ const counts = parts.join(", ");
26
+ switch (verdict) {
27
+ case "BLOCK":
28
+ return `Blocked: ${counts}. These operations take heavy locks and can cause an outage on a busy table. See suggested rewrites.`;
29
+ case "REVIEW":
30
+ return `Needs review: ${counts}. Likely safe on small tables, risky on large/hot ones.`;
31
+ default:
32
+ return `${counts} informational note(s).`;
33
+ }
34
+ }
35
+ //# sourceMappingURL=verdict.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"verdict.js","sourceRoot":"","sources":["../../src/analyzer/verdict.ts"],"names":[],"mappings":"AAEA;;;;;GAKG;AACH,MAAM,UAAU,cAAc,CAAC,QAAmB;IAChD,IAAI,QAAQ,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,QAAQ,KAAK,UAAU,CAAC;QAAE,OAAO,OAAO,CAAC;IACpE,IAAI,QAAQ,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,QAAQ,KAAK,SAAS,CAAC;QAAE,OAAO,QAAQ,CAAC;IACpE,OAAO,MAAM,CAAC;AAChB,CAAC;AAED,MAAM,UAAU,SAAS,CAAC,OAAgB,EAAE,QAAmB;IAC7D,IAAI,QAAQ,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;QAC1B,OAAO,6DAA6D,CAAC;IACvE,CAAC;IACD,MAAM,IAAI,GAAG,QAAQ,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,QAAQ,KAAK,UAAU,CAAC,CAAC,MAAM,CAAC;IACtE,MAAM,IAAI,GAAG,QAAQ,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,QAAQ,KAAK,SAAS,CAAC,CAAC,MAAM,CAAC;IACrE,MAAM,KAAK,GAAa,EAAE,CAAC;IAC3B,IAAI,IAAI;QAAE,KAAK,CAAC,IAAI,CAAC,GAAG,IAAI,WAAW,CAAC,CAAC;IACzC,IAAI,IAAI;QAAE,KAAK,CAAC,IAAI,CAAC,GAAG,IAAI,WAAW,IAAI,KAAK,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,GAAG,EAAE,CAAC,CAAC;IAChE,MAAM,MAAM,GAAG,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;IAChC,QAAQ,OAAO,EAAE,CAAC;QAChB,KAAK,OAAO;YACV,OAAO,YAAY,MAAM,sGAAsG,CAAC;QAClI,KAAK,QAAQ;YACX,OAAO,iBAAiB,MAAM,yDAAyD,CAAC;QAC1F;YACE,OAAO,GAAG,MAAM,yBAAyB,CAAC;IAC9C,CAAC;AACH,CAAC"}
@@ -0,0 +1,17 @@
1
+ /**
2
+ * PostgreSQL table-level lock modes, what each one blocks, and the common DDL
3
+ * that acquires it. Exposed as an MCP resource so a model (or a human) can
4
+ * reason about *why* an operation is dangerous, not just that it is.
5
+ *
6
+ * Source: PostgreSQL docs, "Explicit Locking" → Table-level Locks.
7
+ * https://www.postgresql.org/docs/current/explicit-locking.html
8
+ */
9
+ export interface LockMode {
10
+ mode: string;
11
+ /** Plain-language summary of what this lock prevents concurrently. */
12
+ blocks: string;
13
+ /** Representative operations that take this lock. */
14
+ acquiredBy: string[];
15
+ }
16
+ export declare const LOCK_MODES: LockMode[];
17
+ export declare function renderLockMatrix(): string;