@massu/core 1.10.5 → 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.
- package/dist/cli.js +363 -334
- package/dist/hooks/post-edit-context.js +6 -1
- package/dist/hooks/pre-delete-check.js +12 -7
- package/package.json +1 -1
- package/src/db.ts +63 -62
- package/src/domains.ts +2 -1
- package/src/hooks/pre-delete-check.ts +7 -5
- package/src/import-resolver.ts +3 -2
- package/src/knowledge-tools.ts +3 -2
- package/src/lib/sql-table-names.ts +71 -0
- package/src/middleware-tree.ts +6 -5
- package/src/page-deps.ts +9 -8
- package/src/python/coupling-detector.ts +4 -3
- package/src/python/domain-enforcer.ts +3 -2
- package/src/python/impact-analyzer.ts +7 -6
- package/src/python/import-resolver.ts +3 -2
- package/src/python/migration-indexer.ts +5 -4
- package/src/python/model-indexer.ts +5 -4
- package/src/python/route-indexer.ts +4 -3
- package/src/python-tools.ts +30 -29
- package/src/security/license-pubkey.generated.ts +1 -1
- package/src/security/registry-pubkey.generated.ts +1 -1
- package/src/sentinel-db.ts +47 -46
- package/src/sentinel-scanner.ts +3 -2
- package/src/sentinel-tools.ts +2 -1
- package/src/tools.ts +14 -13
- package/src/trpc-index.ts +5 -4
- package/src/validate-features-runner.ts +5 -4
|
@@ -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(
|
|
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(
|
|
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
|
-
|
|
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
|
-
|
|
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 = "
|
|
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
|
-
|
|
747
|
+
`SELECT source_file FROM ${t("py_imports")} WHERE target_file = ?`
|
|
743
748
|
).all(pyFile);
|
|
744
749
|
const routes = db.prepare(
|
|
745
|
-
|
|
750
|
+
`SELECT method, path FROM ${t("py_routes")} WHERE file = ?`
|
|
746
751
|
).all(pyFile);
|
|
747
752
|
const models = db.prepare(
|
|
748
|
-
|
|
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.
|
|
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
|
|
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
|
|
75
|
-
CREATE INDEX IF NOT EXISTS idx_massu_imports_target ON
|
|
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
|
|
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
|
|
87
|
+
CREATE INDEX IF NOT EXISTS idx_massu_trpc_router ON ${t('trpc_procedures')}(router_name);
|
|
87
88
|
|
|
88
|
-
CREATE TABLE IF NOT EXISTS
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
135
|
-
CREATE INDEX IF NOT EXISTS idx_massu_py_imports_target ON
|
|
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
|
|
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
|
|
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
|
|
155
|
-
CREATE INDEX IF NOT EXISTS idx_massu_py_routes_file ON
|
|
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
|
|
158
|
+
CREATE TABLE IF NOT EXISTS ${t('py_route_callers')} (
|
|
158
159
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
159
|
-
route_id INTEGER NOT NULL REFERENCES
|
|
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
|
|
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
|
|
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
|
|
177
|
-
CREATE INDEX IF NOT EXISTS idx_massu_py_models_table ON
|
|
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
|
|
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
|
|
187
|
-
CREATE INDEX IF NOT EXISTS idx_massu_py_fk_target ON
|
|
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
|
|
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
|
|
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
|
|
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
|
|
224
|
-
CREATE INDEX IF NOT EXISTS idx_sentinel_status ON
|
|
225
|
-
CREATE INDEX IF NOT EXISTS idx_sentinel_key ON
|
|
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
|
|
229
|
+
CREATE TABLE IF NOT EXISTS ${t('sentinel_components')} (
|
|
229
230
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
230
|
-
feature_id INTEGER NOT NULL REFERENCES
|
|
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
|
|
241
|
+
CREATE TABLE IF NOT EXISTS ${t('sentinel_procedures')} (
|
|
241
242
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
242
|
-
feature_id INTEGER NOT NULL REFERENCES
|
|
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
|
|
251
|
+
CREATE TABLE IF NOT EXISTS ${t('sentinel_pages')} (
|
|
251
252
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
252
|
-
feature_id INTEGER NOT NULL REFERENCES
|
|
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
|
|
260
|
+
CREATE TABLE IF NOT EXISTS ${t('sentinel_deps')} (
|
|
260
261
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
261
|
-
feature_id INTEGER NOT NULL REFERENCES
|
|
262
|
-
depends_on_feature_id INTEGER NOT NULL REFERENCES
|
|
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
|
|
270
|
+
CREATE TABLE IF NOT EXISTS ${t('sentinel_changelog')} (
|
|
270
271
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
271
|
-
feature_id INTEGER NOT NULL REFERENCES
|
|
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
|
|
280
|
-
CREATE INDEX IF NOT EXISTS idx_sentinel_procedures_router ON
|
|
281
|
-
CREATE INDEX IF NOT EXISTS idx_sentinel_pages_route ON
|
|
282
|
-
CREATE INDEX IF NOT EXISTS idx_sentinel_changelog_feature ON
|
|
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
|
|
288
|
+
CREATE VIRTUAL TABLE IF NOT EXISTS ${t('sentinel_fts')} USING fts5(
|
|
288
289
|
feature_key, title, description, domain, subdomain,
|
|
289
|
-
content
|
|
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
|
|
296
|
-
INSERT INTO
|
|
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
|
|
301
|
-
INSERT INTO
|
|
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
|
|
306
|
-
INSERT INTO
|
|
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
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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) {
|
package/src/import-resolver.ts
CHANGED
|
@@ -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(
|
|
168
|
+
dataDb.exec(`DELETE FROM ${t('imports')}`);
|
|
168
169
|
|
|
169
170
|
const insertStmt = dataDb.prepare(
|
|
170
|
-
|
|
171
|
+
`INSERT INTO ${t('imports')} (source_file, target_file, import_type, imported_names, line) VALUES (?, ?, ?, ?, ?)`
|
|
171
172
|
);
|
|
172
173
|
|
|
173
174
|
let edgeCount = 0;
|
package/src/knowledge-tools.ts
CHANGED
|
@@ -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 =
|
|
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
|
-
|
|
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
|
+
}
|
package/src/middleware-tree.ts
CHANGED
|
@@ -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(
|
|
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
|
-
|
|
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(
|
|
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(
|
|
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(
|
|
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
|
}
|