@massu/core 1.10.6 → 1.10.7

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.
@@ -547,9 +547,14 @@ function globMatch(filePath, pattern) {
547
547
  return regex.test(filePath);
548
548
  }
549
549
 
550
+ // src/lib/sql-table-names.ts
551
+ function t(suffix) {
552
+ return `${getConfig().toolPrefix}_${suffix}`;
553
+ }
554
+
550
555
  // src/middleware-tree.ts
551
556
  function isInMiddlewareTree(dataDb, file) {
552
- const result = dataDb.prepare("SELECT 1 FROM massu_middleware_tree WHERE file = ?").get(file);
557
+ const result = dataDb.prepare(`SELECT 1 FROM ${t("middleware_tree")} WHERE file = ?`).get(file);
553
558
  return result !== void 0;
554
559
  }
555
560
 
@@ -533,6 +533,11 @@ function getResolvedPaths() {
533
533
  // src/memory-db.ts
534
534
  import Database from "better-sqlite3";
535
535
 
536
+ // src/lib/sql-table-names.ts
537
+ function t(suffix) {
538
+ return `${getConfig().toolPrefix}_${suffix}`;
539
+ }
540
+
536
541
  // src/sentinel-db.ts
537
542
  var PROJECT_ROOT = getProjectRoot();
538
543
  function parsePortalScope(raw) {
@@ -561,7 +566,7 @@ function toFeature(row) {
561
566
  };
562
567
  }
563
568
  function getFeatureById(db, id) {
564
- const row = db.prepare("SELECT * FROM massu_sentinel WHERE id = ?").get(id);
569
+ const row = db.prepare(`SELECT * FROM ${t("sentinel")} WHERE id = ?`).get(id);
565
570
  return row ? toFeature(row) : null;
566
571
  }
567
572
  function getFeatureImpact(db, filePaths) {
@@ -569,7 +574,7 @@ function getFeatureImpact(db, filePaths) {
569
574
  const affectedFeatureIds = /* @__PURE__ */ new Set();
570
575
  for (const filePath of filePaths) {
571
576
  const links = db.prepare(
572
- "SELECT feature_id FROM massu_sentinel_components WHERE component_file = ?"
577
+ `SELECT feature_id FROM ${t("sentinel_components")} WHERE component_file = ?`
573
578
  ).all(filePath);
574
579
  for (const link of links) {
575
580
  affectedFeatureIds.add(link.feature_id);
@@ -582,7 +587,7 @@ function getFeatureImpact(db, filePaths) {
582
587
  const feature = getFeatureById(db, featureId);
583
588
  if (!feature || feature.status !== "active") continue;
584
589
  const allComponents = db.prepare(
585
- "SELECT component_file, is_primary FROM massu_sentinel_components WHERE feature_id = ?"
590
+ `SELECT component_file, is_primary FROM ${t("sentinel_components")} WHERE feature_id = ?`
586
591
  ).all(featureId);
587
592
  const affected = allComponents.filter((c) => fileSet.has(c.component_file));
588
593
  const remaining = allComponents.filter((c) => !fileSet.has(c.component_file));
@@ -703,7 +708,7 @@ async function main() {
703
708
  process.exit(0);
704
709
  return;
705
710
  }
706
- const SENTINEL_TABLE = "massu_sentinel";
711
+ const SENTINEL_TABLE = t("sentinel");
707
712
  try {
708
713
  const tableExists = db.prepare(
709
714
  `SELECT name FROM sqlite_master WHERE type='table' AND name=?`
@@ -739,13 +744,13 @@ async function main() {
739
744
  try {
740
745
  for (const pyFile of pyFiles) {
741
746
  const importers = db.prepare(
742
- "SELECT source_file FROM massu_py_imports WHERE target_file = ?"
747
+ `SELECT source_file FROM ${t("py_imports")} WHERE target_file = ?`
743
748
  ).all(pyFile);
744
749
  const routes = db.prepare(
745
- "SELECT method, path FROM massu_py_routes WHERE file = ?"
750
+ `SELECT method, path FROM ${t("py_routes")} WHERE file = ?`
746
751
  ).all(pyFile);
747
752
  const models = db.prepare(
748
- "SELECT class_name FROM massu_py_models WHERE file = ?"
753
+ `SELECT class_name FROM ${t("py_models")} WHERE file = ?`
749
754
  ).all(pyFile);
750
755
  if (importers.length > 0 || routes.length > 0 || models.length > 0) {
751
756
  const parts = [];
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@massu/core",
3
- "version": "1.10.6",
3
+ "version": "1.10.7",
4
4
  "type": "module",
5
5
  "description": "AI Engineering Governance MCP Server - Session memory, knowledge system, feature registry, code intelligence, rule enforcement, tiered tooling (12 free / 73 total), 59 workflow commands, 11 agents, 20+ patterns",
6
6
  "main": "src/server.ts",
package/src/db.ts CHANGED
@@ -5,6 +5,7 @@ import Database from 'better-sqlite3';
5
5
  import { dirname, join } from 'path';
6
6
  import { existsSync, mkdirSync, readdirSync, statSync } from 'fs';
7
7
  import { getResolvedPaths } from './config.ts';
8
+ import { t } from './lib/sql-table-names.ts';
8
9
 
9
10
  /**
10
11
  * Thrown by `getCodeGraphDb()` when `.codegraph/codegraph.db` is missing.
@@ -62,7 +63,7 @@ export function getDataDb(): Database.Database {
62
63
 
63
64
  function initDataSchema(db: Database.Database): void {
64
65
  db.exec(`
65
- CREATE TABLE IF NOT EXISTS massu_imports (
66
+ CREATE TABLE IF NOT EXISTS ${t('imports')} (
66
67
  id INTEGER PRIMARY KEY AUTOINCREMENT,
67
68
  source_file TEXT NOT NULL,
68
69
  target_file TEXT NOT NULL,
@@ -71,10 +72,10 @@ function initDataSchema(db: Database.Database): void {
71
72
  line INTEGER NOT NULL DEFAULT 0
72
73
  );
73
74
 
74
- CREATE INDEX IF NOT EXISTS idx_massu_imports_source ON massu_imports(source_file);
75
- CREATE INDEX IF NOT EXISTS idx_massu_imports_target ON massu_imports(target_file);
75
+ CREATE INDEX IF NOT EXISTS idx_massu_imports_source ON ${t('imports')}(source_file);
76
+ CREATE INDEX IF NOT EXISTS idx_massu_imports_target ON ${t('imports')}(target_file);
76
77
 
77
- CREATE TABLE IF NOT EXISTS massu_trpc_procedures (
78
+ CREATE TABLE IF NOT EXISTS ${t('trpc_procedures')} (
78
79
  id INTEGER PRIMARY KEY AUTOINCREMENT,
79
80
  router_file TEXT NOT NULL,
80
81
  router_name TEXT NOT NULL,
@@ -83,20 +84,20 @@ function initDataSchema(db: Database.Database): void {
83
84
  has_ui_caller INTEGER NOT NULL DEFAULT 0
84
85
  );
85
86
 
86
- CREATE INDEX IF NOT EXISTS idx_massu_trpc_router ON massu_trpc_procedures(router_name);
87
+ CREATE INDEX IF NOT EXISTS idx_massu_trpc_router ON ${t('trpc_procedures')}(router_name);
87
88
 
88
- CREATE TABLE IF NOT EXISTS massu_trpc_call_sites (
89
+ CREATE TABLE IF NOT EXISTS ${t('trpc_call_sites')} (
89
90
  id INTEGER PRIMARY KEY AUTOINCREMENT,
90
91
  procedure_id INTEGER NOT NULL,
91
92
  file TEXT NOT NULL,
92
93
  line INTEGER NOT NULL DEFAULT 0,
93
94
  call_pattern TEXT NOT NULL,
94
- FOREIGN KEY (procedure_id) REFERENCES massu_trpc_procedures(id)
95
+ FOREIGN KEY (procedure_id) REFERENCES ${t('trpc_procedures')}(id)
95
96
  );
96
97
 
97
- CREATE INDEX IF NOT EXISTS idx_massu_call_sites_proc ON massu_trpc_call_sites(procedure_id);
98
+ CREATE INDEX IF NOT EXISTS idx_massu_call_sites_proc ON ${t('trpc_call_sites')}(procedure_id);
98
99
 
99
- CREATE TABLE IF NOT EXISTS massu_page_deps (
100
+ CREATE TABLE IF NOT EXISTS ${t('page_deps')} (
100
101
  id INTEGER PRIMARY KEY AUTOINCREMENT,
101
102
  page_file TEXT NOT NULL,
102
103
  route TEXT NOT NULL,
@@ -107,14 +108,14 @@ function initDataSchema(db: Database.Database): void {
107
108
  tables_touched TEXT NOT NULL DEFAULT '[]'
108
109
  );
109
110
 
110
- CREATE INDEX IF NOT EXISTS idx_massu_page_deps_page ON massu_page_deps(page_file);
111
+ CREATE INDEX IF NOT EXISTS idx_massu_page_deps_page ON ${t('page_deps')}(page_file);
111
112
 
112
- CREATE TABLE IF NOT EXISTS massu_middleware_tree (
113
+ CREATE TABLE IF NOT EXISTS ${t('middleware_tree')} (
113
114
  id INTEGER PRIMARY KEY AUTOINCREMENT,
114
115
  file TEXT NOT NULL UNIQUE
115
116
  );
116
117
 
117
- CREATE TABLE IF NOT EXISTS massu_meta (
118
+ CREATE TABLE IF NOT EXISTS ${t('meta')} (
118
119
  key TEXT PRIMARY KEY,
119
120
  value TEXT NOT NULL
120
121
  );
@@ -123,7 +124,7 @@ function initDataSchema(db: Database.Database): void {
123
124
  -- Python Code Intelligence Tables
124
125
  -- ============================================================
125
126
 
126
- CREATE TABLE IF NOT EXISTS massu_py_imports (
127
+ CREATE TABLE IF NOT EXISTS ${t('py_imports')} (
127
128
  id INTEGER PRIMARY KEY AUTOINCREMENT,
128
129
  source_file TEXT NOT NULL,
129
130
  target_file TEXT NOT NULL,
@@ -131,15 +132,15 @@ function initDataSchema(db: Database.Database): void {
131
132
  imported_names TEXT NOT NULL DEFAULT '[]',
132
133
  line INTEGER NOT NULL DEFAULT 0
133
134
  );
134
- CREATE INDEX IF NOT EXISTS idx_massu_py_imports_source ON massu_py_imports(source_file);
135
- CREATE INDEX IF NOT EXISTS idx_massu_py_imports_target ON massu_py_imports(target_file);
135
+ CREATE INDEX IF NOT EXISTS idx_massu_py_imports_source ON ${t('py_imports')}(source_file);
136
+ CREATE INDEX IF NOT EXISTS idx_massu_py_imports_target ON ${t('py_imports')}(target_file);
136
137
 
137
- CREATE TABLE IF NOT EXISTS massu_py_meta (
138
+ CREATE TABLE IF NOT EXISTS ${t('py_meta')} (
138
139
  key TEXT PRIMARY KEY,
139
140
  value TEXT NOT NULL
140
141
  );
141
142
 
142
- CREATE TABLE IF NOT EXISTS massu_py_routes (
143
+ CREATE TABLE IF NOT EXISTS ${t('py_routes')} (
143
144
  id INTEGER PRIMARY KEY AUTOINCREMENT,
144
145
  file TEXT NOT NULL,
145
146
  method TEXT NOT NULL,
@@ -151,19 +152,19 @@ function initDataSchema(db: Database.Database): void {
151
152
  is_authenticated INTEGER NOT NULL DEFAULT 0,
152
153
  line INTEGER NOT NULL DEFAULT 0
153
154
  );
154
- CREATE INDEX IF NOT EXISTS idx_massu_py_routes_path ON massu_py_routes(path);
155
- CREATE INDEX IF NOT EXISTS idx_massu_py_routes_file ON massu_py_routes(file);
155
+ CREATE INDEX IF NOT EXISTS idx_massu_py_routes_path ON ${t('py_routes')}(path);
156
+ CREATE INDEX IF NOT EXISTS idx_massu_py_routes_file ON ${t('py_routes')}(file);
156
157
 
157
- CREATE TABLE IF NOT EXISTS massu_py_route_callers (
158
+ CREATE TABLE IF NOT EXISTS ${t('py_route_callers')} (
158
159
  id INTEGER PRIMARY KEY AUTOINCREMENT,
159
- route_id INTEGER NOT NULL REFERENCES massu_py_routes(id) ON DELETE CASCADE,
160
+ route_id INTEGER NOT NULL REFERENCES ${t('py_routes')}(id) ON DELETE CASCADE,
160
161
  frontend_file TEXT NOT NULL,
161
162
  line INTEGER NOT NULL DEFAULT 0,
162
163
  call_pattern TEXT NOT NULL
163
164
  );
164
- CREATE INDEX IF NOT EXISTS idx_massu_py_route_callers_route ON massu_py_route_callers(route_id);
165
+ CREATE INDEX IF NOT EXISTS idx_massu_py_route_callers_route ON ${t('py_route_callers')}(route_id);
165
166
 
166
- CREATE TABLE IF NOT EXISTS massu_py_models (
167
+ CREATE TABLE IF NOT EXISTS ${t('py_models')} (
167
168
  id INTEGER PRIMARY KEY AUTOINCREMENT,
168
169
  class_name TEXT NOT NULL,
169
170
  table_name TEXT,
@@ -173,20 +174,20 @@ function initDataSchema(db: Database.Database): void {
173
174
  relationships TEXT NOT NULL DEFAULT '[]',
174
175
  foreign_keys TEXT NOT NULL DEFAULT '[]'
175
176
  );
176
- CREATE INDEX IF NOT EXISTS idx_massu_py_models_file ON massu_py_models(file);
177
- CREATE INDEX IF NOT EXISTS idx_massu_py_models_table ON massu_py_models(table_name);
177
+ CREATE INDEX IF NOT EXISTS idx_massu_py_models_file ON ${t('py_models')}(file);
178
+ CREATE INDEX IF NOT EXISTS idx_massu_py_models_table ON ${t('py_models')}(table_name);
178
179
 
179
- CREATE TABLE IF NOT EXISTS massu_py_fk_edges (
180
+ CREATE TABLE IF NOT EXISTS ${t('py_fk_edges')} (
180
181
  id INTEGER PRIMARY KEY AUTOINCREMENT,
181
182
  source_table TEXT NOT NULL,
182
183
  source_column TEXT NOT NULL,
183
184
  target_table TEXT NOT NULL,
184
185
  target_column TEXT NOT NULL
185
186
  );
186
- CREATE INDEX IF NOT EXISTS idx_massu_py_fk_source ON massu_py_fk_edges(source_table);
187
- CREATE INDEX IF NOT EXISTS idx_massu_py_fk_target ON massu_py_fk_edges(target_table);
187
+ CREATE INDEX IF NOT EXISTS idx_massu_py_fk_source ON ${t('py_fk_edges')}(source_table);
188
+ CREATE INDEX IF NOT EXISTS idx_massu_py_fk_target ON ${t('py_fk_edges')}(target_table);
188
189
 
189
- CREATE TABLE IF NOT EXISTS massu_py_migrations (
190
+ CREATE TABLE IF NOT EXISTS ${t('py_migrations')} (
190
191
  id INTEGER PRIMARY KEY AUTOINCREMENT,
191
192
  revision TEXT NOT NULL UNIQUE,
192
193
  down_revision TEXT,
@@ -195,14 +196,14 @@ function initDataSchema(db: Database.Database): void {
195
196
  operations TEXT NOT NULL DEFAULT '[]',
196
197
  is_head INTEGER NOT NULL DEFAULT 0
197
198
  );
198
- CREATE INDEX IF NOT EXISTS idx_massu_py_migrations_rev ON massu_py_migrations(revision);
199
+ CREATE INDEX IF NOT EXISTS idx_massu_py_migrations_rev ON ${t('py_migrations')}(revision);
199
200
 
200
201
  -- ============================================================
201
202
  -- Sentinel: Feature Registry Tables
202
203
  -- ============================================================
203
204
 
204
205
  -- Core feature definition
205
- CREATE TABLE IF NOT EXISTS massu_sentinel (
206
+ CREATE TABLE IF NOT EXISTS ${t('sentinel')} (
206
207
  id INTEGER PRIMARY KEY AUTOINCREMENT,
207
208
  feature_key TEXT UNIQUE NOT NULL,
208
209
  domain TEXT NOT NULL,
@@ -220,14 +221,14 @@ function initDataSchema(db: Database.Database): void {
220
221
  removed_reason TEXT
221
222
  );
222
223
 
223
- CREATE INDEX IF NOT EXISTS idx_sentinel_domain ON massu_sentinel(domain);
224
- CREATE INDEX IF NOT EXISTS idx_sentinel_status ON massu_sentinel(status);
225
- CREATE INDEX IF NOT EXISTS idx_sentinel_key ON massu_sentinel(feature_key);
224
+ CREATE INDEX IF NOT EXISTS idx_sentinel_domain ON ${t('sentinel')}(domain);
225
+ CREATE INDEX IF NOT EXISTS idx_sentinel_status ON ${t('sentinel')}(status);
226
+ CREATE INDEX IF NOT EXISTS idx_sentinel_key ON ${t('sentinel')}(feature_key);
226
227
 
227
228
  -- Feature-to-component mapping (many-to-many)
228
- CREATE TABLE IF NOT EXISTS massu_sentinel_components (
229
+ CREATE TABLE IF NOT EXISTS ${t('sentinel_components')} (
229
230
  id INTEGER PRIMARY KEY AUTOINCREMENT,
230
- feature_id INTEGER NOT NULL REFERENCES massu_sentinel(id) ON DELETE CASCADE,
231
+ feature_id INTEGER NOT NULL REFERENCES ${t('sentinel')}(id) ON DELETE CASCADE,
231
232
  component_file TEXT NOT NULL,
232
233
  component_name TEXT,
233
234
  role TEXT DEFAULT 'implementation'
@@ -237,9 +238,9 @@ function initDataSchema(db: Database.Database): void {
237
238
  );
238
239
 
239
240
  -- Feature-to-procedure mapping (many-to-many)
240
- CREATE TABLE IF NOT EXISTS massu_sentinel_procedures (
241
+ CREATE TABLE IF NOT EXISTS ${t('sentinel_procedures')} (
241
242
  id INTEGER PRIMARY KEY AUTOINCREMENT,
242
- feature_id INTEGER NOT NULL REFERENCES massu_sentinel(id) ON DELETE CASCADE,
243
+ feature_id INTEGER NOT NULL REFERENCES ${t('sentinel')}(id) ON DELETE CASCADE,
243
244
  router_name TEXT NOT NULL,
244
245
  procedure_name TEXT NOT NULL,
245
246
  procedure_type TEXT,
@@ -247,28 +248,28 @@ function initDataSchema(db: Database.Database): void {
247
248
  );
248
249
 
249
250
  -- Feature-to-page mapping (where feature is accessible)
250
- CREATE TABLE IF NOT EXISTS massu_sentinel_pages (
251
+ CREATE TABLE IF NOT EXISTS ${t('sentinel_pages')} (
251
252
  id INTEGER PRIMARY KEY AUTOINCREMENT,
252
- feature_id INTEGER NOT NULL REFERENCES massu_sentinel(id) ON DELETE CASCADE,
253
+ feature_id INTEGER NOT NULL REFERENCES ${t('sentinel')}(id) ON DELETE CASCADE,
253
254
  page_route TEXT NOT NULL,
254
255
  portal TEXT,
255
256
  UNIQUE(feature_id, page_route, portal)
256
257
  );
257
258
 
258
259
  -- Feature dependency graph
259
- CREATE TABLE IF NOT EXISTS massu_sentinel_deps (
260
+ CREATE TABLE IF NOT EXISTS ${t('sentinel_deps')} (
260
261
  id INTEGER PRIMARY KEY AUTOINCREMENT,
261
- feature_id INTEGER NOT NULL REFERENCES massu_sentinel(id) ON DELETE CASCADE,
262
- depends_on_feature_id INTEGER NOT NULL REFERENCES massu_sentinel(id) ON DELETE CASCADE,
262
+ feature_id INTEGER NOT NULL REFERENCES ${t('sentinel')}(id) ON DELETE CASCADE,
263
+ depends_on_feature_id INTEGER NOT NULL REFERENCES ${t('sentinel')}(id) ON DELETE CASCADE,
263
264
  dependency_type TEXT DEFAULT 'requires'
264
265
  CHECK(dependency_type IN ('requires', 'enhances', 'replaces')),
265
266
  UNIQUE(feature_id, depends_on_feature_id)
266
267
  );
267
268
 
268
269
  -- Feature change log (audit trail)
269
- CREATE TABLE IF NOT EXISTS massu_sentinel_changelog (
270
+ CREATE TABLE IF NOT EXISTS ${t('sentinel_changelog')} (
270
271
  id INTEGER PRIMARY KEY AUTOINCREMENT,
271
- feature_id INTEGER NOT NULL REFERENCES massu_sentinel(id) ON DELETE CASCADE,
272
+ feature_id INTEGER NOT NULL REFERENCES ${t('sentinel')}(id) ON DELETE CASCADE,
272
273
  change_type TEXT NOT NULL,
273
274
  changed_by TEXT,
274
275
  change_detail TEXT,
@@ -276,36 +277,36 @@ function initDataSchema(db: Database.Database): void {
276
277
  created_at TEXT DEFAULT (datetime('now'))
277
278
  );
278
279
 
279
- CREATE INDEX IF NOT EXISTS idx_sentinel_components_file ON massu_sentinel_components(component_file);
280
- CREATE INDEX IF NOT EXISTS idx_sentinel_procedures_router ON massu_sentinel_procedures(router_name);
281
- CREATE INDEX IF NOT EXISTS idx_sentinel_pages_route ON massu_sentinel_pages(page_route);
282
- CREATE INDEX IF NOT EXISTS idx_sentinel_changelog_feature ON massu_sentinel_changelog(feature_id);
280
+ CREATE INDEX IF NOT EXISTS idx_sentinel_components_file ON ${t('sentinel_components')}(component_file);
281
+ CREATE INDEX IF NOT EXISTS idx_sentinel_procedures_router ON ${t('sentinel_procedures')}(router_name);
282
+ CREATE INDEX IF NOT EXISTS idx_sentinel_pages_route ON ${t('sentinel_pages')}(page_route);
283
+ CREATE INDEX IF NOT EXISTS idx_sentinel_changelog_feature ON ${t('sentinel_changelog')}(feature_id);
283
284
  `);
284
285
 
285
286
  // FTS5 for feature search (separate exec since virtual tables can't be in same batch)
286
287
  db.exec(`
287
- CREATE VIRTUAL TABLE IF NOT EXISTS massu_sentinel_fts USING fts5(
288
+ CREATE VIRTUAL TABLE IF NOT EXISTS ${t('sentinel_fts')} USING fts5(
288
289
  feature_key, title, description, domain, subdomain,
289
- content=massu_sentinel, content_rowid=id
290
+ content=${t('sentinel')}, content_rowid=id
290
291
  );
291
292
  `);
292
293
 
293
294
  // FTS5 sync triggers
294
295
  db.exec(`
295
- CREATE TRIGGER IF NOT EXISTS massu_sentinel_ai AFTER INSERT ON massu_sentinel BEGIN
296
- INSERT INTO massu_sentinel_fts(rowid, feature_key, title, description, domain, subdomain)
296
+ CREATE TRIGGER IF NOT EXISTS massu_sentinel_ai AFTER INSERT ON ${t('sentinel')} BEGIN
297
+ INSERT INTO ${t('sentinel_fts')}(rowid, feature_key, title, description, domain, subdomain)
297
298
  VALUES (new.id, new.feature_key, new.title, new.description, new.domain, new.subdomain);
298
299
  END;
299
300
 
300
- CREATE TRIGGER IF NOT EXISTS massu_sentinel_ad AFTER DELETE ON massu_sentinel BEGIN
301
- INSERT INTO massu_sentinel_fts(massu_sentinel_fts, rowid, feature_key, title, description, domain, subdomain)
301
+ CREATE TRIGGER IF NOT EXISTS massu_sentinel_ad AFTER DELETE ON ${t('sentinel')} BEGIN
302
+ INSERT INTO ${t('sentinel_fts')}(${t('sentinel_fts')}, rowid, feature_key, title, description, domain, subdomain)
302
303
  VALUES ('delete', old.id, old.feature_key, old.title, old.description, old.domain, old.subdomain);
303
304
  END;
304
305
 
305
- CREATE TRIGGER IF NOT EXISTS massu_sentinel_au AFTER UPDATE ON massu_sentinel BEGIN
306
- INSERT INTO massu_sentinel_fts(massu_sentinel_fts, rowid, feature_key, title, description, domain, subdomain)
306
+ CREATE TRIGGER IF NOT EXISTS massu_sentinel_au AFTER UPDATE ON ${t('sentinel')} BEGIN
307
+ INSERT INTO ${t('sentinel_fts')}(${t('sentinel_fts')}, rowid, feature_key, title, description, domain, subdomain)
307
308
  VALUES ('delete', old.id, old.feature_key, old.title, old.description, old.domain, old.subdomain);
308
- INSERT INTO massu_sentinel_fts(rowid, feature_key, title, description, domain, subdomain)
309
+ INSERT INTO ${t('sentinel_fts')}(rowid, feature_key, title, description, domain, subdomain)
309
310
  VALUES (new.id, new.feature_key, new.title, new.description, new.domain, new.subdomain);
310
311
  END;
311
312
  `);
@@ -315,7 +316,7 @@ function initDataSchema(db: Database.Database): void {
315
316
  * Check if Massu indexes are stale compared to CodeGraph timestamps.
316
317
  */
317
318
  export function isDataStale(dataDb: Database.Database, codegraphDb: Database.Database): boolean {
318
- const lastBuild = dataDb.prepare("SELECT value FROM massu_meta WHERE key = 'last_build_time'").get() as { value: string } | undefined;
319
+ const lastBuild = dataDb.prepare(`SELECT value FROM ${t('meta')} WHERE key = 'last_build_time'`).get() as { value: string } | undefined;
319
320
  if (!lastBuild) return true;
320
321
 
321
322
  // CodeGraph stores indexed_at as unix timestamp (integer)
@@ -330,14 +331,14 @@ export function isDataStale(dataDb: Database.Database, codegraphDb: Database.Dat
330
331
  * Update the last build timestamp in massu_meta.
331
332
  */
332
333
  export function updateBuildTimestamp(dataDb: Database.Database): void {
333
- dataDb.prepare("INSERT OR REPLACE INTO massu_meta (key, value) VALUES ('last_build_time', ?)").run(new Date().toISOString());
334
+ dataDb.prepare(`INSERT OR REPLACE INTO ${t('meta')} (key, value) VALUES ('last_build_time', ?)`).run(new Date().toISOString());
334
335
  }
335
336
 
336
337
  /**
337
338
  * Check if Python indexes are stale based on massu_py_meta.last_build_time.
338
339
  */
339
340
  export function isPythonDataStale(dataDb: Database.Database, pythonRoot: string): boolean {
340
- const lastBuild = dataDb.prepare("SELECT value FROM massu_py_meta WHERE key = 'last_build_time'").get() as { value: string } | undefined;
341
+ const lastBuild = dataDb.prepare(`SELECT value FROM ${t('py_meta')} WHERE key = 'last_build_time'`).get() as { value: string } | undefined;
341
342
  if (!lastBuild) return true;
342
343
 
343
344
  const lastBuildTime = new Date(lastBuild.value).getTime();
@@ -365,5 +366,5 @@ export function isPythonDataStale(dataDb: Database.Database, pythonRoot: string)
365
366
  * Update the Python build timestamp in massu_py_meta.
366
367
  */
367
368
  export function updatePythonBuildTimestamp(dataDb: Database.Database): void {
368
- dataDb.prepare("INSERT OR REPLACE INTO massu_py_meta (key, value) VALUES ('last_build_time', ?)").run(new Date().toISOString());
369
+ dataDb.prepare(`INSERT OR REPLACE INTO ${t('py_meta')} (key, value) VALUES ('last_build_time', ?)`).run(new Date().toISOString());
369
370
  }
package/src/domains.ts CHANGED
@@ -5,6 +5,7 @@ import type Database from 'better-sqlite3';
5
5
  import { globMatch } from './rules.ts';
6
6
  import { getConfig, getResolvedPaths } from './config.ts';
7
7
  import type { DomainConfig } from './config.ts';
8
+ import { t } from './lib/sql-table-names.ts';
8
9
 
9
10
  // Re-export for backward compatibility
10
11
  export type { DomainConfig };
@@ -107,7 +108,7 @@ export function findCrossDomainImports(dataDb: Database.Database): {
107
108
 
108
109
  const srcPattern = srcPrefix + '/%';
109
110
  const imports = dataDb.prepare(
110
- 'SELECT source_file, target_file FROM massu_imports WHERE source_file LIKE ? AND target_file LIKE ?'
111
+ `SELECT source_file, target_file FROM ${t('imports')} WHERE source_file LIKE ? AND target_file LIKE ?`
111
112
  ).all(srcPattern, srcPattern) as { source_file: string; target_file: string }[];
112
113
 
113
114
  const crossings: {
@@ -15,6 +15,7 @@ import { resolve } from 'path';
15
15
  import { existsSync } from 'fs';
16
16
  import { getFeatureImpact } from '../sentinel-db.ts';
17
17
  import { getProjectRoot, getResolvedPaths } from '../config.ts';
18
+ import { t } from '../lib/sql-table-names.ts';
18
19
 
19
20
  interface HookInput {
20
21
  session_id: string;
@@ -144,8 +145,9 @@ async function main(): Promise<void> {
144
145
  return;
145
146
  }
146
147
 
147
- // The sentinel registry table name (defined by sentinel-db schema)
148
- const SENTINEL_TABLE = 'massu_sentinel';
148
+ // The sentinel registry table name (defined by sentinel-db schema).
149
+ // P-H032: now config-driven via t() helper from lib/sql-table-names.ts.
150
+ const SENTINEL_TABLE = t('sentinel');
149
151
 
150
152
  try {
151
153
  // Check if any sentinel tables exist
@@ -192,15 +194,15 @@ async function main(): Promise<void> {
192
194
  try {
193
195
  for (const pyFile of pyFiles) {
194
196
  const importers = db.prepare(
195
- 'SELECT source_file FROM massu_py_imports WHERE target_file = ?'
197
+ `SELECT source_file FROM ${t('py_imports')} WHERE target_file = ?`
196
198
  ).all(pyFile) as { source_file: string }[];
197
199
 
198
200
  const routes = db.prepare(
199
- 'SELECT method, path FROM massu_py_routes WHERE file = ?'
201
+ `SELECT method, path FROM ${t('py_routes')} WHERE file = ?`
200
202
  ).all(pyFile) as { method: string; path: string }[];
201
203
 
202
204
  const models = db.prepare(
203
- 'SELECT class_name FROM massu_py_models WHERE file = ?'
205
+ `SELECT class_name FROM ${t('py_models')} WHERE file = ?`
204
206
  ).all(pyFile) as { class_name: string }[];
205
207
 
206
208
  if (importers.length > 0 || routes.length > 0 || models.length > 0) {
@@ -6,6 +6,7 @@ import { resolve, dirname, join } from 'path';
6
6
  import type Database from 'better-sqlite3';
7
7
  import { getResolvedPaths, getProjectRoot } from './config.ts';
8
8
  import { ensureWithinRoot } from './security-utils.ts';
9
+ import { t } from './lib/sql-table-names.ts';
9
10
 
10
11
  interface ImportEdge {
11
12
  source_file: string;
@@ -164,10 +165,10 @@ export function buildImportIndex(dataDb: Database.Database, codegraphDb: Databas
164
165
  const files = codegraphDb.prepare("SELECT path FROM files WHERE path LIKE 'src/%'").all() as { path: string }[];
165
166
 
166
167
  // Clear existing import edges
167
- dataDb.exec('DELETE FROM massu_imports');
168
+ dataDb.exec(`DELETE FROM ${t('imports')}`);
168
169
 
169
170
  const insertStmt = dataDb.prepare(
170
- 'INSERT INTO massu_imports (source_file, target_file, import_type, imported_names, line) VALUES (?, ?, ?, ?, ?)'
171
+ `INSERT INTO ${t('imports')} (source_file, target_file, import_type, imported_names, line) VALUES (?, ?, ?, ?, ?)`
171
172
  );
172
173
 
173
174
  let edgeCount = 0;
@@ -9,6 +9,7 @@ import { resolve, basename } from 'path';
9
9
  import { getConfig, getResolvedPaths } from './config.ts';
10
10
  import { getDataDb } from './db.ts';
11
11
  import { getMemoryDb, sanitizeFts5Query } from './memory-db.ts';
12
+ import { t } from './lib/sql-table-names.ts';
12
13
 
13
14
  // ============================================================
14
15
  // Massu Knowledge: MCP Tool Definitions & Handlers
@@ -1186,7 +1187,7 @@ function handleGaps(db: Database.Database, args: Record<string, unknown>): ToolR
1186
1187
  try {
1187
1188
  dataDb = getDataDb();
1188
1189
 
1189
- let sql = "SELECT feature_key, title, domain, status FROM massu_sentinel WHERE status = 'active'";
1190
+ let sql = `SELECT feature_key, title, domain, status FROM ${t('sentinel')} WHERE status = 'active'`;
1190
1191
  const params: string[] = [];
1191
1192
  if (domain) {
1192
1193
  sql += ' AND domain LIKE ?';
@@ -1275,7 +1276,7 @@ function handleGaps(db: Database.Database, args: Record<string, unknown>): ToolR
1275
1276
  dataDb = getDataDb();
1276
1277
 
1277
1278
  const domains = dataDb.prepare(
1278
- "SELECT DISTINCT domain FROM massu_sentinel WHERE status = 'active' ORDER BY domain"
1279
+ `SELECT DISTINCT domain FROM ${t('sentinel')} WHERE status = 'active' ORDER BY domain`
1279
1280
  ).all() as { domain: string }[];
1280
1281
 
1281
1282
  for (const d of domains) {
@@ -0,0 +1,71 @@
1
+ // Copyright (c) 2026 Massu. All rights reserved.
2
+ // Licensed under BSL 1.1 - see LICENSE file for details.
3
+
4
+ /**
5
+ * P-H032 (plan-stage-c-high-batch / 1.10.7): SoT for SQL table names that
6
+ * are prefixed with the customer's configured `toolPrefix`.
7
+ *
8
+ * **Bug class closed**: 20 distinct SQL table names (`massu_imports`,
9
+ * `massu_trpc_procedures`, `massu_sentinel`, etc.) were hardcoded with the
10
+ * `massu_` prefix in ~145 SQL string sites across 21 source files. Tool
11
+ * names were already config-driven via `getConfig().toolPrefix` but
12
+ * persistence wasn't — split-brain where a non-default-prefix customer
13
+ * (e.g. `toolPrefix: 'foo'`) would have tools named `foo_*` but tables
14
+ * still named `massu_*`. Default-prefix customers (100% of current
15
+ * installs) see ZERO behavior change.
16
+ *
17
+ * **Structural fix**: every SQL string that references a prefix-scoped
18
+ * table now goes through `t(suffix)` which returns
19
+ * `\`${getConfig().toolPrefix}_${suffix}\``. The ESLint rule
20
+ * `no-hardcoded-massu-tables` bans bare `'massu_X'` literals so future
21
+ * regressions are impossible. The drift-guard test
22
+ * `sql-table-names-source-grep.test.ts` scans the source tree and asserts
23
+ * zero bare `massu_<table>` literals outside this module + the test files.
24
+ *
25
+ * **20 canonical table suffixes** (registered here as a typed const so
26
+ * typos at callsites are caught at compile time):
27
+ */
28
+
29
+ import { getConfig } from '../config.ts';
30
+
31
+ /**
32
+ * The complete set of suffixes for prefix-scoped SQL tables. Keep this in
33
+ * sync with `packages/core/src/db.ts` (CodeGraph schema) +
34
+ * `sentinel-db.ts` (sentinel schema) + Python indexer DDL in
35
+ * `packages/core/src/python/*.ts`.
36
+ */
37
+ export type MassuTableSuffix =
38
+ | 'imports'
39
+ | 'meta'
40
+ | 'middleware_tree'
41
+ | 'page_deps'
42
+ | 'py_fk_edges'
43
+ | 'py_imports'
44
+ | 'py_meta'
45
+ | 'py_migrations'
46
+ | 'py_models'
47
+ | 'py_route_callers'
48
+ | 'py_routes'
49
+ | 'sentinel'
50
+ | 'sentinel_changelog'
51
+ | 'sentinel_components'
52
+ | 'sentinel_deps'
53
+ | 'sentinel_fts'
54
+ | 'sentinel_pages'
55
+ | 'sentinel_procedures'
56
+ | 'trpc_call_sites'
57
+ | 'trpc_procedures';
58
+
59
+ /**
60
+ * Resolve a prefix-scoped table name to its concrete identifier using the
61
+ * current `getConfig().toolPrefix`. For default-prefix installs returns
62
+ * `massu_<suffix>` (current behavior). For custom-prefix installs returns
63
+ * `<customPrefix>_<suffix>`.
64
+ *
65
+ * Use this in EVERY SQL string that references a prefix-scoped table.
66
+ * The ESLint rule bans bare `'massu_X'` literals; the only legitimate
67
+ * exit is this function.
68
+ */
69
+ export function t(suffix: MassuTableSuffix): string {
70
+ return `${getConfig().toolPrefix}_${suffix}`;
71
+ }
@@ -5,6 +5,7 @@ import type Database from 'better-sqlite3';
5
5
  import { getConfig } from './config.ts';
6
6
  import { existsSync } from 'fs';
7
7
  import { resolve } from 'path';
8
+ import { t } from './lib/sql-table-names.ts';
8
9
 
9
10
  /**
10
11
  * Build the middleware import tree by tracing all transitive imports
@@ -13,7 +14,7 @@ import { resolve } from 'path';
13
14
  */
14
15
  export function buildMiddlewareTree(dataDb: Database.Database): number {
15
16
  // Clear existing data
16
- dataDb.exec('DELETE FROM massu_middleware_tree');
17
+ dataDb.exec(`DELETE FROM ${t('middleware_tree')}`);
17
18
 
18
19
  const config = getConfig();
19
20
  const middlewareFile = config.paths.middleware ?? 'src/middleware.ts';
@@ -30,7 +31,7 @@ export function buildMiddlewareTree(dataDb: Database.Database): number {
30
31
  const current = queue.shift()!;
31
32
 
32
33
  const imports = dataDb.prepare(
33
- 'SELECT target_file FROM massu_imports WHERE source_file = ?'
34
+ `SELECT target_file FROM ${t('imports')} WHERE source_file = ?`
34
35
  ).all(current) as { target_file: string }[];
35
36
 
36
37
  for (const imp of imports) {
@@ -42,7 +43,7 @@ export function buildMiddlewareTree(dataDb: Database.Database): number {
42
43
  }
43
44
 
44
45
  // Store the tree
45
- const insertStmt = dataDb.prepare('INSERT INTO massu_middleware_tree (file) VALUES (?)');
46
+ const insertStmt = dataDb.prepare(`INSERT INTO ${t('middleware_tree')} (file) VALUES (?)`);
46
47
  const insertAll = dataDb.transaction(() => {
47
48
  for (const file of visited) {
48
49
  insertStmt.run(file);
@@ -57,7 +58,7 @@ export function buildMiddlewareTree(dataDb: Database.Database): number {
57
58
  * Check if a file is in the middleware import tree.
58
59
  */
59
60
  export function isInMiddlewareTree(dataDb: Database.Database, file: string): boolean {
60
- const result = dataDb.prepare('SELECT 1 FROM massu_middleware_tree WHERE file = ?').get(file);
61
+ const result = dataDb.prepare(`SELECT 1 FROM ${t('middleware_tree')} WHERE file = ?`).get(file);
61
62
  return result !== undefined;
62
63
  }
63
64
 
@@ -65,6 +66,6 @@ export function isInMiddlewareTree(dataDb: Database.Database, file: string): boo
65
66
  * Get all files in the middleware import tree.
66
67
  */
67
68
  export function getMiddlewareTree(dataDb: Database.Database): string[] {
68
- const rows = dataDb.prepare('SELECT file FROM massu_middleware_tree ORDER BY file').all() as { file: string }[];
69
+ const rows = dataDb.prepare(`SELECT file FROM ${t('middleware_tree')} ORDER BY file`).all() as { file: string }[];
69
70
  return rows.map(r => r.file);
70
71
  }