@massu/core 1.10.8 → 1.12.1

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 (178) hide show
  1. package/CHANGELOG.md +1334 -0
  2. package/dist/adapter.js +6 -4
  3. package/dist/cli.js +581 -448
  4. package/dist/detect/adapters/.bundle-shasums.json +4 -7
  5. package/dist/detect/adapters/rails.js +6 -4
  6. package/dist/detect/adapters/spring.js +6 -4
  7. package/dist/detect/adapters/tree-sitter-loader.js +6 -4
  8. package/dist/hooks/auto-learning-pipeline.js +24 -1
  9. package/dist/hooks/classify-failure.js +32 -3
  10. package/dist/hooks/cost-tracker.js +24 -0
  11. package/dist/hooks/fix-detector.js +97 -1
  12. package/dist/hooks/incident-pipeline.js +30 -1
  13. package/dist/hooks/intent-suggester.js +6 -3
  14. package/dist/hooks/post-edit-context.js +24 -1
  15. package/dist/hooks/post-tool-use.js +67 -19
  16. package/dist/hooks/pre-compact.js +28 -3
  17. package/dist/hooks/pre-delete-check.js +53 -27
  18. package/dist/hooks/pre-tool-use-gate.js +962 -0
  19. package/dist/hooks/quality-event.js +24 -0
  20. package/dist/hooks/rule-enforcement-pipeline.js +24 -1
  21. package/dist/hooks/security-gate.js +29 -16
  22. package/dist/hooks/session-end.js +31 -6
  23. package/dist/hooks/session-start.js +8709 -8459
  24. package/dist/hooks/user-prompt.js +25 -1
  25. package/package.json +15 -18
  26. package/commands/README.md +0 -134
  27. package/src/adapter.ts +0 -121
  28. package/src/adr-generator.ts +0 -292
  29. package/src/analytics.ts +0 -373
  30. package/src/audit-trail.ts +0 -450
  31. package/src/backfill-sessions.ts +0 -182
  32. package/src/changelog-generator.ts +0 -178
  33. package/src/cli.ts +0 -246
  34. package/src/cloud-sync.ts +0 -234
  35. package/src/commands/adapters.ts +0 -824
  36. package/src/commands/changelog.ts +0 -165
  37. package/src/commands/config-check-drift.ts +0 -133
  38. package/src/commands/config-refresh.ts +0 -396
  39. package/src/commands/config-upgrade.ts +0 -127
  40. package/src/commands/doctor.ts +0 -485
  41. package/src/commands/hook-runner.ts +0 -145
  42. package/src/commands/init.ts +0 -1823
  43. package/src/commands/install-commands.ts +0 -692
  44. package/src/commands/install-hooks.ts +0 -27
  45. package/src/commands/permissions.ts +0 -150
  46. package/src/commands/refresh-log.ts +0 -37
  47. package/src/commands/show-template.ts +0 -65
  48. package/src/commands/template-engine.ts +0 -301
  49. package/src/commands/watch.ts +0 -430
  50. package/src/config.ts +0 -765
  51. package/src/cost-tracker.ts +0 -355
  52. package/src/db.ts +0 -370
  53. package/src/dependency-scorer.ts +0 -337
  54. package/src/detect/adapters/aspnet.ts +0 -4
  55. package/src/detect/adapters/discover.ts +0 -469
  56. package/src/detect/adapters/file-sampler.ts +0 -225
  57. package/src/detect/adapters/go-chi.ts +0 -4
  58. package/src/detect/adapters/index.ts +0 -51
  59. package/src/detect/adapters/nextjs-trpc.ts +0 -166
  60. package/src/detect/adapters/parse-guard.ts +0 -133
  61. package/src/detect/adapters/phoenix.ts +0 -4
  62. package/src/detect/adapters/python-django.ts +0 -208
  63. package/src/detect/adapters/python-fastapi.ts +0 -223
  64. package/src/detect/adapters/python-flask.ts +0 -235
  65. package/src/detect/adapters/query-helpers.ts +0 -170
  66. package/src/detect/adapters/rails.ts +0 -4
  67. package/src/detect/adapters/runner.ts +0 -284
  68. package/src/detect/adapters/spring.ts +0 -4
  69. package/src/detect/adapters/swift-swiftui.ts +0 -171
  70. package/src/detect/adapters/tree-sitter-loader.ts +0 -517
  71. package/src/detect/adapters/types.ts +0 -191
  72. package/src/detect/codebase-introspector.ts +0 -210
  73. package/src/detect/domain-inferrer.ts +0 -197
  74. package/src/detect/drift.ts +0 -199
  75. package/src/detect/framework-detector.ts +0 -307
  76. package/src/detect/index.ts +0 -209
  77. package/src/detect/manifest-registry.ts +0 -261
  78. package/src/detect/migrate.ts +0 -362
  79. package/src/detect/monorepo-detector.ts +0 -348
  80. package/src/detect/package-detector.ts +0 -828
  81. package/src/detect/passthrough.ts +0 -108
  82. package/src/detect/regex-fallback.ts +0 -449
  83. package/src/detect/source-dir-detector.ts +0 -297
  84. package/src/detect/vr-command-map.ts +0 -167
  85. package/src/docs-map.json +0 -100
  86. package/src/docs-tools.ts +0 -519
  87. package/src/domains.ts +0 -182
  88. package/src/hooks/auto-learning-pipeline.ts +0 -228
  89. package/src/hooks/classify-failure.ts +0 -259
  90. package/src/hooks/cost-tracker.ts +0 -66
  91. package/src/hooks/fix-detector.ts +0 -186
  92. package/src/hooks/incident-pipeline.ts +0 -190
  93. package/src/hooks/intent-suggester.ts +0 -131
  94. package/src/hooks/post-edit-context.ts +0 -91
  95. package/src/hooks/post-tool-use.ts +0 -413
  96. package/src/hooks/pre-compact.ts +0 -168
  97. package/src/hooks/pre-delete-check.ts +0 -243
  98. package/src/hooks/quality-event.ts +0 -127
  99. package/src/hooks/rule-enforcement-pipeline.ts +0 -159
  100. package/src/hooks/security-gate.ts +0 -153
  101. package/src/hooks/session-end.ts +0 -467
  102. package/src/hooks/session-start.ts +0 -436
  103. package/src/hooks/user-prompt.ts +0 -157
  104. package/src/import-resolver.ts +0 -226
  105. package/src/knowledge-db.ts +0 -176
  106. package/src/knowledge-indexer.ts +0 -708
  107. package/src/knowledge-tools.ts +0 -1414
  108. package/src/lib/fileLock.ts +0 -203
  109. package/src/lib/framework-supports.ts +0 -85
  110. package/src/lib/gitToplevel.ts +0 -22
  111. package/src/lib/hook-registry.ts +0 -43
  112. package/src/lib/installLock.ts +0 -66
  113. package/src/lib/memory-path.ts +0 -49
  114. package/src/lib/pidLiveness.ts +0 -67
  115. package/src/lib/settings-local.ts +0 -110
  116. package/src/lib/sql-table-names.ts +0 -71
  117. package/src/license.ts +0 -547
  118. package/src/lsp/auto-detect.ts +0 -98
  119. package/src/lsp/client.ts +0 -776
  120. package/src/lsp/enrich.ts +0 -127
  121. package/src/lsp/types.ts +0 -221
  122. package/src/memory-db.ts +0 -1614
  123. package/src/memory-file-ingest.ts +0 -128
  124. package/src/memory-tools.ts +0 -424
  125. package/src/middleware-tree.ts +0 -71
  126. package/src/observability-tools.ts +0 -343
  127. package/src/observation-extractor.ts +0 -418
  128. package/src/page-deps.ts +0 -285
  129. package/src/permissions.ts +0 -422
  130. package/src/prompt-analyzer.ts +0 -332
  131. package/src/python/coupling-detector.ts +0 -125
  132. package/src/python/domain-enforcer.ts +0 -84
  133. package/src/python/impact-analyzer.ts +0 -96
  134. package/src/python/import-parser.ts +0 -244
  135. package/src/python/import-resolver.ts +0 -136
  136. package/src/python/migration-indexer.ts +0 -116
  137. package/src/python/migration-parser.ts +0 -332
  138. package/src/python/model-indexer.ts +0 -71
  139. package/src/python/model-parser.ts +0 -279
  140. package/src/python/route-indexer.ts +0 -59
  141. package/src/python/route-parser.ts +0 -317
  142. package/src/python-tools.ts +0 -630
  143. package/src/regression-detector.ts +0 -319
  144. package/src/rules.ts +0 -57
  145. package/src/schema-mapper.ts +0 -232
  146. package/src/security/adapter-origin.ts +0 -130
  147. package/src/security/adapter-verifier.ts +0 -319
  148. package/src/security/atomic-write.ts +0 -164
  149. package/src/security/fetcher.ts +0 -200
  150. package/src/security/install-tracking.ts +0 -319
  151. package/src/security/license-pubkey.generated.ts +0 -30
  152. package/src/security/license-response-verifier.ts +0 -149
  153. package/src/security/local-fingerprint.ts +0 -225
  154. package/src/security/manifest-cache.ts +0 -333
  155. package/src/security/manifest-schema.ts +0 -129
  156. package/src/security/registry-pubkey.generated.ts +0 -35
  157. package/src/security/telemetry.ts +0 -320
  158. package/src/security-scorer.ts +0 -405
  159. package/src/security-utils.ts +0 -133
  160. package/src/sentinel-db.ts +0 -580
  161. package/src/sentinel-scanner.ts +0 -406
  162. package/src/sentinel-tools.ts +0 -513
  163. package/src/sentinel-types.ts +0 -140
  164. package/src/server-dispatch.ts +0 -225
  165. package/src/server.ts +0 -107
  166. package/src/session-archiver.ts +0 -111
  167. package/src/session-state-generator.ts +0 -174
  168. package/src/team-knowledge.ts +0 -407
  169. package/src/tool-db-needs.ts +0 -232
  170. package/src/tools.ts +0 -1206
  171. package/src/transcript-parser.ts +0 -458
  172. package/src/trpc-index.ts +0 -215
  173. package/src/validate-features-runner.ts +0 -107
  174. package/src/validation-engine.ts +0 -358
  175. package/src/watch/daemon.ts +0 -385
  176. package/src/watch/lockfile-detector.ts +0 -65
  177. package/src/watch/paths.ts +0 -279
  178. package/src/watch/state.ts +0 -178
@@ -1,244 +0,0 @@
1
- // Copyright (c) 2026 Massu. All rights reserved.
2
- // Licensed under BSL 1.1 - see LICENSE file for details.
3
-
4
- // ============================================================
5
- // Python Import Statement Parser
6
- // ============================================================
7
-
8
- /**
9
- * Represents a single parsed Python import statement.
10
- */
11
- export interface PythonImport {
12
- /** The kind of import: plain absolute, plain relative, from-absolute, or from-relative. */
13
- type: 'absolute' | 'relative' | 'from_absolute' | 'from_relative';
14
- /** The module path, e.g. "os.path" or "..utils". */
15
- module: string;
16
- /** Imported names (empty for plain `import x` statements). */
17
- names: string[];
18
- /** Alias when `import x as alias` is used. */
19
- alias?: string;
20
- /** Relative import level: 0 for absolute, 1 for `.`, 2 for `..`, etc. */
21
- level: number;
22
- /** 1-based line number where the import statement begins. */
23
- line: number;
24
- }
25
-
26
- /**
27
- * Parse all Python import statements from source code.
28
- *
29
- * Handles:
30
- * - `import x`, `import x.y.z`, `import x as alias`, `import x, y, z`
31
- * - `from x import y`, `from x import y, z`, `from x import *`
32
- * - `from . import x`, `from ..x import y`, `from ...x.y import z`
33
- * - Multi-line parenthesized imports: `from x import (\n a,\n b\n)`
34
- * - Skips `if TYPE_CHECKING:` blocks
35
- * - Strips comments
36
- *
37
- * @param source - Python source code
38
- * @returns Array of parsed imports in source order
39
- */
40
- export function parsePythonImports(source: string): PythonImport[] {
41
- const lines = source.split('\n');
42
- const results: PythonImport[] = [];
43
-
44
- /** State machine modes. */
45
- type Mode = 'normal' | 'multiline' | 'type_checking';
46
- let mode: Mode = 'normal';
47
-
48
- // Multiline accumulation state
49
- let multilineBuffer = '';
50
- let multilineStartLine = 0;
51
-
52
- // TYPE_CHECKING block tracking
53
- let typeCheckingIndent = -1;
54
-
55
- for (let i = 0; i < lines.length; i++) {
56
- const rawLine = lines[i];
57
- const lineNum = i + 1; // 1-based
58
-
59
- // ── TYPE_CHECKING block detection ──────────────────────
60
- if (mode === 'normal') {
61
- const tcMatch = rawLine.match(/^(\s*)if\s+TYPE_CHECKING\s*:/);
62
- if (tcMatch) {
63
- mode = 'type_checking';
64
- typeCheckingIndent = tcMatch[1].length;
65
- continue;
66
- }
67
- }
68
-
69
- if (mode === 'type_checking') {
70
- // Stay in type_checking mode until we see a line that is:
71
- // - non-empty, non-comment, and at the same or lesser indentation
72
- const stripped = rawLine.replace(/#.*$/, '').trimEnd();
73
- if (stripped.length === 0) continue; // blank or comment-only line
74
- const currentIndent = rawLine.match(/^(\s*)/)?.[1].length ?? 0;
75
- if (currentIndent <= typeCheckingIndent) {
76
- // Dedented — exit TYPE_CHECKING block, process this line normally
77
- mode = 'normal';
78
- typeCheckingIndent = -1;
79
- // Fall through to normal processing below
80
- } else {
81
- continue; // Still inside TYPE_CHECKING block
82
- }
83
- }
84
-
85
- // ── Multiline continuation ─────────────────────────────
86
- if (mode === 'multiline') {
87
- const cleaned = stripComment(rawLine);
88
- multilineBuffer += ' ' + cleaned.trim();
89
- if (cleaned.includes(')')) {
90
- // Close the multiline import
91
- mode = 'normal';
92
- const parsed = parseFromImportLine(multilineBuffer, multilineStartLine);
93
- if (parsed) results.push(parsed);
94
- multilineBuffer = '';
95
- }
96
- continue;
97
- }
98
-
99
- // ── Normal mode ────────────────────────────────────────
100
- const line = stripComment(rawLine).trim();
101
-
102
- // Skip blank lines
103
- if (line.length === 0) continue;
104
-
105
- // Detect multiline from-import with opening paren but no closing paren
106
- if (line.startsWith('from ') && line.includes('(') && !line.includes(')')) {
107
- mode = 'multiline';
108
- multilineBuffer = line;
109
- multilineStartLine = lineNum;
110
- continue;
111
- }
112
-
113
- // from ... import ...
114
- if (line.startsWith('from ')) {
115
- const parsed = parseFromImportLine(line, lineNum);
116
- if (parsed) results.push(parsed);
117
- continue;
118
- }
119
-
120
- // import ...
121
- if (line.startsWith('import ')) {
122
- const imports = parsePlainImportLine(line, lineNum);
123
- results.push(...imports);
124
- continue;
125
- }
126
- }
127
-
128
- return results;
129
- }
130
-
131
- // ============================================================
132
- // Internal helpers
133
- // ============================================================
134
-
135
- /**
136
- * Strip an inline `# comment` from a line, respecting strings minimally.
137
- * For import lines this is sufficient since import syntax doesn't contain `#`.
138
- */
139
- function stripComment(line: string): string {
140
- // If the line is a pure comment, return empty
141
- const trimmed = line.trimStart();
142
- if (trimmed.startsWith('#')) return '';
143
-
144
- const hashIdx = line.indexOf('#');
145
- if (hashIdx === -1) return line;
146
- return line.slice(0, hashIdx);
147
- }
148
-
149
- /**
150
- * Count leading dots in a module string and return the level + remaining module.
151
- */
152
- function splitRelativePrefix(raw: string): { level: number; rest: string } {
153
- let level = 0;
154
- while (level < raw.length && raw[level] === '.') {
155
- level++;
156
- }
157
- return { level, rest: raw.slice(level) };
158
- }
159
-
160
- /**
161
- * Parse a `from X import Y` line (possibly with parentheses already joined).
162
- */
163
- function parseFromImportLine(line: string, lineNum: number): PythonImport | null {
164
- // Normalize: remove parens, collapse whitespace
165
- const cleaned = line
166
- .replace(/[()]/g, '')
167
- .replace(/\s+/g, ' ')
168
- .trim();
169
-
170
- // Pattern: from <module> import <names>
171
- const match = cleaned.match(/^from\s+(\S+)\s+import\s+(.+)$/);
172
- if (!match) return null;
173
-
174
- const rawModule = match[1];
175
- const namesStr = match[2];
176
-
177
- const { level, rest } = splitRelativePrefix(rawModule);
178
- const isRelative = level > 0;
179
-
180
- const module = rawModule;
181
-
182
- // Parse names: split by comma, trim, handle `name as alias` per name
183
- const names = namesStr
184
- .split(',')
185
- .map((n) => n.trim())
186
- .filter((n) => n.length > 0)
187
- .map((n) => {
188
- // Handle `name as alias` — store just the name in the names array
189
- const asMatch = n.match(/^(\S+)\s+as\s+(\S+)$/);
190
- return asMatch ? asMatch[1] : n;
191
- });
192
-
193
- return {
194
- type: isRelative ? 'from_relative' : 'from_absolute',
195
- module,
196
- names,
197
- level,
198
- line: lineNum,
199
- };
200
- }
201
-
202
- /**
203
- * Parse a plain `import X` line, which may contain multiple comma-separated modules.
204
- *
205
- * Examples:
206
- * - `import os` → one result
207
- * - `import os, sys, re` → three results
208
- * - `import os.path as osp` → one result with alias
209
- */
210
- function parsePlainImportLine(line: string, lineNum: number): PythonImport[] {
211
- const results: PythonImport[] = [];
212
-
213
- // Strip leading "import "
214
- const rest = line.replace(/^import\s+/, '');
215
-
216
- // Split by comma
217
- const parts = rest.split(',').map((p) => p.trim()).filter((p) => p.length > 0);
218
-
219
- for (const part of parts) {
220
- // Check for `module as alias`
221
- const asMatch = part.match(/^(\S+)\s+as\s+(\S+)$/);
222
- const moduleName = asMatch ? asMatch[1] : part;
223
- const alias = asMatch ? asMatch[2] : undefined;
224
-
225
- const { level } = splitRelativePrefix(moduleName);
226
- const isRelative = level > 0;
227
-
228
- const imp: PythonImport = {
229
- type: isRelative ? 'relative' : 'absolute',
230
- module: moduleName,
231
- names: [],
232
- level,
233
- line: lineNum,
234
- };
235
-
236
- if (alias !== undefined) {
237
- imp.alias = alias;
238
- }
239
-
240
- results.push(imp);
241
- }
242
-
243
- return results;
244
- }
@@ -1,136 +0,0 @@
1
- // Copyright (c) 2026 Massu. All rights reserved.
2
- // Licensed under BSL 1.1 - see LICENSE file for details.
3
-
4
- import { readFileSync, existsSync, readdirSync } from 'fs';
5
- import { resolve, join, relative, dirname } from 'path';
6
- import type Database from 'better-sqlite3';
7
- import { parsePythonImports } from './import-parser.ts';
8
- import { getProjectRoot } from '../config.ts';
9
- import { t } from '../lib/sql-table-names.ts';
10
-
11
- /**
12
- * Resolve a Python module path to a file path.
13
- * Checks both module.py and module/__init__.py.
14
- * Returns path relative to project root, or null for external modules.
15
- */
16
- export function resolvePythonModulePath(module: string, fromFile: string, pythonRoot: string, level: number): string | null {
17
- const projectRoot = getProjectRoot();
18
-
19
- if (level > 0) {
20
- // Relative import - resolve from current file's directory
21
- let baseDir = dirname(resolve(projectRoot, fromFile));
22
- for (let i = 1; i < level; i++) {
23
- baseDir = dirname(baseDir);
24
- }
25
- // Strip the dots prefix to get the actual module part
26
- const modulePart = module.replace(/^\.+/, '');
27
- if (modulePart) {
28
- const parts = modulePart.split('.');
29
- return tryResolvePythonPath(join(baseDir, ...parts), projectRoot);
30
- }
31
- // `from . import x` - the module is the current package
32
- return tryResolvePythonPath(baseDir, projectRoot);
33
- }
34
-
35
- // Absolute import
36
- const parts = module.split('.');
37
- const candidate = join(resolve(projectRoot, pythonRoot), ...parts);
38
- return tryResolvePythonPath(candidate, projectRoot);
39
- }
40
-
41
- function tryResolvePythonPath(basePath: string, projectRoot: string): string | null {
42
- // Try as file: module.py
43
- if (existsSync(basePath + '.py')) {
44
- return relative(projectRoot, basePath + '.py');
45
- }
46
- // Try as package: module/__init__.py
47
- if (existsSync(join(basePath, '__init__.py'))) {
48
- return relative(projectRoot, join(basePath, '__init__.py'));
49
- }
50
- // Try exact path (already has .py)
51
- if (basePath.endsWith('.py') && existsSync(basePath)) {
52
- return relative(projectRoot, basePath);
53
- }
54
- return null;
55
- }
56
-
57
- /**
58
- * Walk directory recursively, collecting .py files.
59
- * Skips excluded directories.
60
- */
61
- function walkPythonFiles(dir: string, excludeDirs: string[]): string[] {
62
- const files: string[] = [];
63
- try {
64
- const entries = readdirSync(dir, { withFileTypes: true });
65
- for (const entry of entries) {
66
- if (entry.isDirectory()) {
67
- if (excludeDirs.includes(entry.name)) continue;
68
- files.push(...walkPythonFiles(join(dir, entry.name), excludeDirs));
69
- } else if (entry.name.endsWith('.py')) {
70
- files.push(join(dir, entry.name));
71
- }
72
- }
73
- } catch { /* directory may not exist */ }
74
- return files;
75
- }
76
-
77
- /**
78
- * Build the Python import graph for all .py files under pythonRoot.
79
- * Stores results in massu_py_imports table.
80
- */
81
- export function buildPythonImportIndex(dataDb: Database.Database, pythonRoot: string, excludeDirs: string[] = ['__pycache__', '.venv', 'venv', '.mypy_cache', '.pytest_cache']): number {
82
- const projectRoot = getProjectRoot();
83
- const absRoot = resolve(projectRoot, pythonRoot);
84
-
85
- // Clear existing Python import edges
86
- dataDb.exec(`DELETE FROM ${t('py_imports')}`);
87
-
88
- const insertStmt = dataDb.prepare(
89
- `INSERT INTO ${t('py_imports')} (source_file, target_file, import_type, imported_names, line) VALUES (?, ?, ?, ?, ?)`
90
- );
91
-
92
- const files = walkPythonFiles(absRoot, excludeDirs);
93
- let edgeCount = 0;
94
-
95
- const insertMany = dataDb.transaction((edges: { source: string; target: string; type: string; names: string; line: number }[]) => {
96
- for (const edge of edges) {
97
- insertStmt.run(edge.source, edge.target, edge.type, edge.names, edge.line);
98
- }
99
- });
100
-
101
- const batch: { source: string; target: string; type: string; names: string; line: number }[] = [];
102
-
103
- for (const absFile of files) {
104
- const relFile = relative(projectRoot, absFile);
105
- let source: string;
106
- try {
107
- source = readFileSync(absFile, 'utf-8');
108
- } catch { continue; }
109
-
110
- const imports = parsePythonImports(source);
111
-
112
- for (const imp of imports) {
113
- const targetPath = resolvePythonModulePath(imp.module, relFile, pythonRoot, imp.level);
114
- if (!targetPath) continue; // Skip external/stdlib
115
-
116
- batch.push({
117
- source: relFile,
118
- target: targetPath,
119
- type: imp.type,
120
- names: JSON.stringify(imp.names),
121
- line: imp.line,
122
- });
123
- edgeCount++;
124
-
125
- if (batch.length >= 500) {
126
- insertMany(batch.splice(0));
127
- }
128
- }
129
- }
130
-
131
- if (batch.length > 0) {
132
- insertMany(batch);
133
- }
134
-
135
- return edgeCount;
136
- }
@@ -1,116 +0,0 @@
1
- // Copyright (c) 2026 Massu. All rights reserved.
2
- // Licensed under BSL 1.1 - see LICENSE file for details.
3
-
4
- import { readFileSync, readdirSync } from 'fs';
5
- import { join, relative } from 'path';
6
- import type Database from 'better-sqlite3';
7
- import { parseAlembicMigration } from './migration-parser.ts';
8
- import { getProjectRoot } from '../config.ts';
9
- import { t } from '../lib/sql-table-names.ts';
10
-
11
- export function buildPythonMigrationIndex(dataDb: Database.Database, alembicDir: string): number {
12
- const projectRoot = getProjectRoot();
13
- const absDir = join(projectRoot, alembicDir);
14
- dataDb.exec(`DELETE FROM ${t('py_migrations')}`);
15
-
16
- // Look for version files in versions/ subdirectory
17
- const versionsDir = join(absDir, 'versions');
18
- let files: string[] = [];
19
- try {
20
- files = readdirSync(versionsDir)
21
- .filter(f => f.endsWith('.py'))
22
- .map(f => join(versionsDir, f));
23
- } catch { /* versions/ subdir not found, try parent */
24
- try {
25
- files = readdirSync(absDir)
26
- .filter(f => f.endsWith('.py') && f !== 'env.py')
27
- .map(f => join(absDir, f));
28
- } catch { /* alembic dir not readable, skip */ }
29
- }
30
-
31
- const insertStmt = dataDb.prepare(
32
- `INSERT INTO ${t('py_migrations')} (revision, down_revision, file, description, operations, is_head) VALUES (?, ?, ?, ?, ?, ?)`
33
- );
34
-
35
- let count = 0;
36
- const allRevisions: Set<string> = new Set();
37
- const hasDownRef: Set<string> = new Set();
38
-
39
- // First pass: parse all migrations
40
- interface MigRow { revision: string; downRevision: string | null; file: string; description: string | null; operations: string }
41
- const rows: MigRow[] = [];
42
-
43
- for (const absFile of files) {
44
- let source: string;
45
- try { source = readFileSync(absFile, 'utf-8'); } catch { continue; }
46
-
47
- const parsed = parseAlembicMigration(source);
48
- if (!parsed.revision) continue;
49
-
50
- allRevisions.add(parsed.revision);
51
- if (parsed.downRevision) hasDownRef.add(parsed.downRevision);
52
-
53
- rows.push({
54
- revision: parsed.revision,
55
- downRevision: parsed.downRevision,
56
- file: relative(projectRoot, absFile),
57
- description: parsed.description,
58
- operations: JSON.stringify(parsed.operations),
59
- });
60
- }
61
-
62
- // Determine heads (revisions not referenced as down_revision by anyone)
63
- dataDb.transaction(() => {
64
- for (const row of rows) {
65
- const isHead = !hasDownRef.has(row.revision) ? 1 : 0;
66
- insertStmt.run(row.revision, row.downRevision, row.file, row.description, row.operations, isHead);
67
- count++;
68
- }
69
- })();
70
-
71
- return count;
72
- }
73
-
74
- /**
75
- * Detect drift between SQLAlchemy models and migration state.
76
- */
77
- export interface DriftReport {
78
- unmigratedModels: { className: string; tableName: string }[];
79
- missingColumns: { model: string; column: string }[];
80
- extraMigrations: string[];
81
- }
82
-
83
- export function detectMigrationDrift(dataDb: Database.Database): DriftReport {
84
- const models = dataDb.prepare(`SELECT class_name, table_name, columns FROM ${t('py_models')} WHERE table_name IS NOT NULL`).all() as {
85
- class_name: string; table_name: string; columns: string;
86
- }[];
87
-
88
- const migrations = dataDb.prepare(`SELECT operations FROM ${t('py_migrations')}`).all() as { operations: string }[];
89
-
90
- // Collect all tables and columns mentioned in migrations
91
- const migratedTables = new Set<string>();
92
- const migratedColumns = new Map<string, Set<string>>();
93
-
94
- for (const mig of migrations) {
95
- let ops: { table?: string; column?: string }[];
96
- try { ops = JSON.parse(mig.operations); } catch { ops = []; }
97
- for (const op of ops) {
98
- if (op.table) {
99
- migratedTables.add(op.table);
100
- if (!migratedColumns.has(op.table)) migratedColumns.set(op.table, new Set());
101
- if (op.column) migratedColumns.get(op.table)!.add(op.column);
102
- }
103
- }
104
- }
105
-
106
- const unmigratedModels: DriftReport['unmigratedModels'] = [];
107
- const missingColumns: DriftReport['missingColumns'] = [];
108
-
109
- for (const model of models) {
110
- if (!migratedTables.has(model.table_name)) {
111
- unmigratedModels.push({ className: model.class_name, tableName: model.table_name });
112
- }
113
- }
114
-
115
- return { unmigratedModels, missingColumns, extraMigrations: [] };
116
- }