@massu/core 1.10.7 → 1.12.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +1303 -0
- package/dist/adapter.js +6 -4
- package/dist/cli.js +559 -400
- package/dist/detect/adapters/.bundle-shasums.json +4 -7
- package/dist/detect/adapters/rails.js +6 -4
- package/dist/detect/adapters/spring.js +6 -4
- package/dist/detect/adapters/tree-sitter-loader.js +6 -4
- package/dist/hooks/auto-learning-pipeline.js +24 -1
- package/dist/hooks/classify-failure.js +32 -3
- package/dist/hooks/cost-tracker.js +24 -0
- package/dist/hooks/fix-detector.js +97 -1
- package/dist/hooks/incident-pipeline.js +30 -1
- package/dist/hooks/intent-suggester.js +6 -3
- package/dist/hooks/post-edit-context.js +24 -1
- package/dist/hooks/post-tool-use.js +66 -18
- package/dist/hooks/pre-compact.js +27 -2
- package/dist/hooks/pre-delete-check.js +48 -22
- package/dist/hooks/pre-tool-use-gate.js +962 -0
- package/dist/hooks/quality-event.js +24 -0
- package/dist/hooks/rule-enforcement-pipeline.js +24 -1
- package/dist/hooks/security-gate.js +29 -16
- package/dist/hooks/session-end.js +24 -0
- package/dist/hooks/session-start.js +8710 -8460
- package/dist/hooks/user-prompt.js +24 -0
- package/package.json +15 -18
- package/commands/README.md +0 -134
- package/src/adapter.ts +0 -121
- package/src/adr-generator.ts +0 -292
- package/src/analytics.ts +0 -373
- package/src/audit-trail.ts +0 -450
- package/src/backfill-sessions.ts +0 -182
- package/src/changelog-generator.ts +0 -178
- package/src/cli.ts +0 -246
- package/src/cloud-sync.ts +0 -234
- package/src/commands/adapters.ts +0 -824
- package/src/commands/changelog.ts +0 -165
- package/src/commands/config-check-drift.ts +0 -133
- package/src/commands/config-refresh.ts +0 -396
- package/src/commands/config-upgrade.ts +0 -127
- package/src/commands/doctor.ts +0 -485
- package/src/commands/hook-runner.ts +0 -145
- package/src/commands/init.ts +0 -1823
- package/src/commands/install-commands.ts +0 -692
- package/src/commands/install-hooks.ts +0 -27
- package/src/commands/permissions.ts +0 -150
- package/src/commands/refresh-log.ts +0 -37
- package/src/commands/show-template.ts +0 -65
- package/src/commands/template-engine.ts +0 -301
- package/src/commands/watch.ts +0 -430
- package/src/config.ts +0 -765
- package/src/cost-tracker.ts +0 -355
- package/src/db.ts +0 -370
- package/src/dependency-scorer.ts +0 -337
- package/src/detect/adapters/aspnet.ts +0 -4
- package/src/detect/adapters/discover.ts +0 -469
- package/src/detect/adapters/file-sampler.ts +0 -225
- package/src/detect/adapters/go-chi.ts +0 -4
- package/src/detect/adapters/index.ts +0 -51
- package/src/detect/adapters/nextjs-trpc.ts +0 -166
- package/src/detect/adapters/parse-guard.ts +0 -133
- package/src/detect/adapters/phoenix.ts +0 -4
- package/src/detect/adapters/python-django.ts +0 -208
- package/src/detect/adapters/python-fastapi.ts +0 -223
- package/src/detect/adapters/python-flask.ts +0 -235
- package/src/detect/adapters/query-helpers.ts +0 -170
- package/src/detect/adapters/rails.ts +0 -4
- package/src/detect/adapters/runner.ts +0 -284
- package/src/detect/adapters/spring.ts +0 -4
- package/src/detect/adapters/swift-swiftui.ts +0 -171
- package/src/detect/adapters/tree-sitter-loader.ts +0 -517
- package/src/detect/adapters/types.ts +0 -191
- package/src/detect/codebase-introspector.ts +0 -210
- package/src/detect/domain-inferrer.ts +0 -197
- package/src/detect/drift.ts +0 -199
- package/src/detect/framework-detector.ts +0 -307
- package/src/detect/index.ts +0 -209
- package/src/detect/manifest-registry.ts +0 -261
- package/src/detect/migrate.ts +0 -362
- package/src/detect/monorepo-detector.ts +0 -348
- package/src/detect/package-detector.ts +0 -828
- package/src/detect/passthrough.ts +0 -108
- package/src/detect/regex-fallback.ts +0 -449
- package/src/detect/source-dir-detector.ts +0 -297
- package/src/detect/vr-command-map.ts +0 -167
- package/src/docs-map.json +0 -100
- package/src/docs-tools.ts +0 -519
- package/src/domains.ts +0 -182
- package/src/hooks/auto-learning-pipeline.ts +0 -228
- package/src/hooks/classify-failure.ts +0 -259
- package/src/hooks/cost-tracker.ts +0 -66
- package/src/hooks/fix-detector.ts +0 -186
- package/src/hooks/incident-pipeline.ts +0 -190
- package/src/hooks/intent-suggester.ts +0 -131
- package/src/hooks/post-edit-context.ts +0 -91
- package/src/hooks/post-tool-use.ts +0 -413
- package/src/hooks/pre-compact.ts +0 -168
- package/src/hooks/pre-delete-check.ts +0 -243
- package/src/hooks/quality-event.ts +0 -127
- package/src/hooks/rule-enforcement-pipeline.ts +0 -159
- package/src/hooks/security-gate.ts +0 -153
- package/src/hooks/session-end.ts +0 -467
- package/src/hooks/session-start.ts +0 -436
- package/src/hooks/user-prompt.ts +0 -157
- package/src/import-resolver.ts +0 -226
- package/src/knowledge-db.ts +0 -176
- package/src/knowledge-indexer.ts +0 -708
- package/src/knowledge-tools.ts +0 -1414
- package/src/lib/fileLock.ts +0 -203
- package/src/lib/gitToplevel.ts +0 -22
- package/src/lib/hook-registry.ts +0 -43
- package/src/lib/installLock.ts +0 -66
- package/src/lib/memory-path.ts +0 -49
- package/src/lib/pidLiveness.ts +0 -67
- package/src/lib/settings-local.ts +0 -110
- package/src/lib/sql-table-names.ts +0 -71
- package/src/license.ts +0 -547
- package/src/lsp/auto-detect.ts +0 -98
- package/src/lsp/client.ts +0 -776
- package/src/lsp/enrich.ts +0 -127
- package/src/lsp/types.ts +0 -221
- package/src/memory-db.ts +0 -1614
- package/src/memory-file-ingest.ts +0 -128
- package/src/memory-tools.ts +0 -424
- package/src/middleware-tree.ts +0 -71
- package/src/observability-tools.ts +0 -343
- package/src/observation-extractor.ts +0 -418
- package/src/page-deps.ts +0 -285
- package/src/permissions.ts +0 -422
- package/src/prompt-analyzer.ts +0 -332
- package/src/python/coupling-detector.ts +0 -125
- package/src/python/domain-enforcer.ts +0 -84
- package/src/python/impact-analyzer.ts +0 -96
- package/src/python/import-parser.ts +0 -244
- package/src/python/import-resolver.ts +0 -136
- package/src/python/migration-indexer.ts +0 -116
- package/src/python/migration-parser.ts +0 -332
- package/src/python/model-indexer.ts +0 -71
- package/src/python/model-parser.ts +0 -279
- package/src/python/route-indexer.ts +0 -59
- package/src/python/route-parser.ts +0 -317
- package/src/python-tools.ts +0 -630
- package/src/regression-detector.ts +0 -319
- package/src/rules.ts +0 -57
- package/src/schema-mapper.ts +0 -232
- package/src/security/adapter-origin.ts +0 -130
- package/src/security/adapter-verifier.ts +0 -319
- package/src/security/atomic-write.ts +0 -164
- package/src/security/fetcher.ts +0 -200
- package/src/security/install-tracking.ts +0 -319
- package/src/security/license-pubkey.generated.ts +0 -30
- package/src/security/license-response-verifier.ts +0 -149
- package/src/security/local-fingerprint.ts +0 -225
- package/src/security/manifest-cache.ts +0 -333
- package/src/security/manifest-schema.ts +0 -129
- package/src/security/registry-pubkey.generated.ts +0 -35
- package/src/security/telemetry.ts +0 -320
- package/src/security-scorer.ts +0 -405
- package/src/security-utils.ts +0 -133
- package/src/sentinel-db.ts +0 -580
- package/src/sentinel-scanner.ts +0 -406
- package/src/sentinel-tools.ts +0 -513
- package/src/sentinel-types.ts +0 -140
- package/src/server-dispatch.ts +0 -225
- package/src/server.ts +0 -107
- package/src/session-archiver.ts +0 -111
- package/src/session-state-generator.ts +0 -174
- package/src/team-knowledge.ts +0 -407
- package/src/tool-db-needs.ts +0 -232
- package/src/tools.ts +0 -1205
- package/src/transcript-parser.ts +0 -458
- package/src/trpc-index.ts +0 -215
- package/src/validate-features-runner.ts +0 -107
- package/src/validation-engine.ts +0 -358
- package/src/watch/daemon.ts +0 -385
- package/src/watch/lockfile-detector.ts +0 -65
- package/src/watch/paths.ts +0 -279
- package/src/watch/state.ts +0 -178
package/src/sentinel-db.ts
DELETED
|
@@ -1,580 +0,0 @@
|
|
|
1
|
-
// Copyright (c) 2026 Massu. All rights reserved.
|
|
2
|
-
// Licensed under BSL 1.1 - see LICENSE file for details.
|
|
3
|
-
|
|
4
|
-
import type Database from 'better-sqlite3';
|
|
5
|
-
import { existsSync } from 'fs';
|
|
6
|
-
import { resolve } from 'path';
|
|
7
|
-
import type {
|
|
8
|
-
Feature,
|
|
9
|
-
FeatureInput,
|
|
10
|
-
FeatureComponent,
|
|
11
|
-
FeatureProcedure,
|
|
12
|
-
FeaturePage,
|
|
13
|
-
FeatureDep,
|
|
14
|
-
FeatureChangeLog,
|
|
15
|
-
FeatureWithCounts,
|
|
16
|
-
FeatureDetail,
|
|
17
|
-
ImpactReport,
|
|
18
|
-
ImpactItem,
|
|
19
|
-
ValidationReport,
|
|
20
|
-
ValidationItem,
|
|
21
|
-
ParityReport,
|
|
22
|
-
ParityItem,
|
|
23
|
-
ComponentRole,
|
|
24
|
-
} from './sentinel-types.ts';
|
|
25
|
-
import { getProjectRoot } from './config.ts';
|
|
26
|
-
import { sanitizeFts5Query } from './memory-db.ts';
|
|
27
|
-
import { t } from './lib/sql-table-names.ts';
|
|
28
|
-
|
|
29
|
-
// ============================================================
|
|
30
|
-
// Sentinel: Feature Registry Data Access Layer
|
|
31
|
-
// ============================================================
|
|
32
|
-
|
|
33
|
-
const PROJECT_ROOT = getProjectRoot();
|
|
34
|
-
|
|
35
|
-
function parsePortalScope(raw: string | null): string[] {
|
|
36
|
-
if (!raw) return [];
|
|
37
|
-
try {
|
|
38
|
-
return JSON.parse(raw);
|
|
39
|
-
} catch {
|
|
40
|
-
return [];
|
|
41
|
-
}
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
function toFeature(row: Record<string, unknown>): Feature {
|
|
45
|
-
return {
|
|
46
|
-
id: row.id as number,
|
|
47
|
-
feature_key: row.feature_key as string,
|
|
48
|
-
domain: row.domain as string,
|
|
49
|
-
subdomain: (row.subdomain as string) || null,
|
|
50
|
-
title: row.title as string,
|
|
51
|
-
description: (row.description as string) || null,
|
|
52
|
-
status: row.status as Feature['status'],
|
|
53
|
-
priority: row.priority as Feature['priority'],
|
|
54
|
-
portal_scope: parsePortalScope(row.portal_scope as string),
|
|
55
|
-
created_at: row.created_at as string,
|
|
56
|
-
updated_at: row.updated_at as string,
|
|
57
|
-
removed_at: (row.removed_at as string) || null,
|
|
58
|
-
removed_reason: (row.removed_reason as string) || null,
|
|
59
|
-
};
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
// ============================================================
|
|
63
|
-
// Core CRUD
|
|
64
|
-
// ============================================================
|
|
65
|
-
|
|
66
|
-
export function upsertFeature(db: Database.Database, input: FeatureInput): number {
|
|
67
|
-
const existing = db.prepare(`SELECT id FROM ${t('sentinel')} WHERE feature_key = ?`).get(input.feature_key) as { id: number } | undefined;
|
|
68
|
-
|
|
69
|
-
if (existing) {
|
|
70
|
-
db.prepare(`
|
|
71
|
-
UPDATE ${t('sentinel')} SET
|
|
72
|
-
domain = ?, subdomain = ?, title = ?, description = ?,
|
|
73
|
-
status = COALESCE(?, status), priority = COALESCE(?, priority),
|
|
74
|
-
portal_scope = COALESCE(?, portal_scope),
|
|
75
|
-
updated_at = datetime('now')
|
|
76
|
-
WHERE id = ?
|
|
77
|
-
`).run(
|
|
78
|
-
input.domain,
|
|
79
|
-
input.subdomain || null,
|
|
80
|
-
input.title,
|
|
81
|
-
input.description || null,
|
|
82
|
-
input.status || null,
|
|
83
|
-
input.priority || null,
|
|
84
|
-
input.portal_scope ? JSON.stringify(input.portal_scope) : null,
|
|
85
|
-
existing.id
|
|
86
|
-
);
|
|
87
|
-
return existing.id;
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
const result = db.prepare(`
|
|
91
|
-
INSERT INTO ${t('sentinel')} (feature_key, domain, subdomain, title, description, status, priority, portal_scope)
|
|
92
|
-
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
|
|
93
|
-
`).run(
|
|
94
|
-
input.feature_key,
|
|
95
|
-
input.domain,
|
|
96
|
-
input.subdomain || null,
|
|
97
|
-
input.title,
|
|
98
|
-
input.description || null,
|
|
99
|
-
input.status || 'active',
|
|
100
|
-
input.priority || 'standard',
|
|
101
|
-
JSON.stringify(input.portal_scope || [])
|
|
102
|
-
);
|
|
103
|
-
|
|
104
|
-
return Number(result.lastInsertRowid);
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
export function getFeature(db: Database.Database, featureKey: string): Feature | null {
|
|
108
|
-
const row = db.prepare(`SELECT * FROM ${t('sentinel')} WHERE feature_key = ?`).get(featureKey) as Record<string, unknown> | undefined;
|
|
109
|
-
return row ? toFeature(row) : null;
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
export function getFeatureById(db: Database.Database, id: number): Feature | null {
|
|
113
|
-
const row = db.prepare(`SELECT * FROM ${t('sentinel')} WHERE id = ?`).get(id) as Record<string, unknown> | undefined;
|
|
114
|
-
return row ? toFeature(row) : null;
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
// ============================================================
|
|
118
|
-
// Search & Query
|
|
119
|
-
// ============================================================
|
|
120
|
-
|
|
121
|
-
export function searchFeatures(
|
|
122
|
-
db: Database.Database,
|
|
123
|
-
query: string,
|
|
124
|
-
filters?: { domain?: string; subdomain?: string; status?: string; portal?: string; page_route?: string }
|
|
125
|
-
): FeatureWithCounts[] {
|
|
126
|
-
let sql: string;
|
|
127
|
-
const params: unknown[] = [];
|
|
128
|
-
|
|
129
|
-
if (query && query.trim()) {
|
|
130
|
-
// FTS5 search
|
|
131
|
-
sql = `
|
|
132
|
-
SELECT s.*, fts.rank,
|
|
133
|
-
(SELECT COUNT(*) FROM ${t('sentinel_components')} WHERE feature_id = s.id) as component_count,
|
|
134
|
-
(SELECT COUNT(*) FROM ${t('sentinel_procedures')} WHERE feature_id = s.id) as procedure_count,
|
|
135
|
-
(SELECT COUNT(*) FROM ${t('sentinel_pages')} WHERE feature_id = s.id) as page_count
|
|
136
|
-
FROM ${t('sentinel')} s
|
|
137
|
-
JOIN ${t('sentinel_fts')} fts ON s.id = fts.rowid
|
|
138
|
-
WHERE ${t('sentinel_fts')} MATCH ?
|
|
139
|
-
`;
|
|
140
|
-
params.push(sanitizeFts5Query(query));
|
|
141
|
-
} else {
|
|
142
|
-
sql = `
|
|
143
|
-
SELECT s.*,
|
|
144
|
-
(SELECT COUNT(*) FROM ${t('sentinel_components')} WHERE feature_id = s.id) as component_count,
|
|
145
|
-
(SELECT COUNT(*) FROM ${t('sentinel_procedures')} WHERE feature_id = s.id) as procedure_count,
|
|
146
|
-
(SELECT COUNT(*) FROM ${t('sentinel_pages')} WHERE feature_id = s.id) as page_count
|
|
147
|
-
FROM ${t('sentinel')} s
|
|
148
|
-
WHERE 1=1
|
|
149
|
-
`;
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
if (filters?.domain) {
|
|
153
|
-
sql += ' AND s.domain = ?';
|
|
154
|
-
params.push(filters.domain);
|
|
155
|
-
}
|
|
156
|
-
if (filters?.subdomain) {
|
|
157
|
-
sql += ' AND s.subdomain = ?';
|
|
158
|
-
params.push(filters.subdomain);
|
|
159
|
-
}
|
|
160
|
-
if (filters?.status) {
|
|
161
|
-
sql += ' AND s.status = ?';
|
|
162
|
-
params.push(filters.status);
|
|
163
|
-
}
|
|
164
|
-
if (filters?.portal) {
|
|
165
|
-
sql += ` AND s.portal_scope LIKE ? ESCAPE '\\'`;
|
|
166
|
-
const escapedPortal = filters.portal.replace(/[%_]/g, '\\$&');
|
|
167
|
-
params.push(`%"${escapedPortal}"%`);
|
|
168
|
-
}
|
|
169
|
-
if (filters?.page_route) {
|
|
170
|
-
sql += ` AND s.id IN (SELECT feature_id FROM ${t('sentinel_pages')} WHERE page_route = ?)`;
|
|
171
|
-
params.push(filters.page_route);
|
|
172
|
-
}
|
|
173
|
-
|
|
174
|
-
sql += query && query.trim() ? ' ORDER BY fts.rank LIMIT 100' : ' ORDER BY s.domain, s.subdomain, s.feature_key LIMIT 100';
|
|
175
|
-
|
|
176
|
-
const rows = db.prepare(sql).all(...params) as (Record<string, unknown>)[];
|
|
177
|
-
return rows.map(row => ({
|
|
178
|
-
...toFeature(row),
|
|
179
|
-
component_count: row.component_count as number,
|
|
180
|
-
procedure_count: row.procedure_count as number,
|
|
181
|
-
page_count: row.page_count as number,
|
|
182
|
-
}));
|
|
183
|
-
}
|
|
184
|
-
|
|
185
|
-
export function getFeaturesByDomain(db: Database.Database, domain: string): Feature[] {
|
|
186
|
-
const rows = db.prepare(`SELECT * FROM ${t('sentinel')} WHERE domain = ? ORDER BY subdomain, feature_key`).all(domain) as Record<string, unknown>[];
|
|
187
|
-
return rows.map(toFeature);
|
|
188
|
-
}
|
|
189
|
-
|
|
190
|
-
export function getFeaturesByFile(db: Database.Database, filePath: string): Feature[] {
|
|
191
|
-
const rows = db.prepare(`
|
|
192
|
-
SELECT DISTINCT s.* FROM ${t('sentinel')} s
|
|
193
|
-
JOIN ${t('sentinel_components')} c ON c.feature_id = s.id
|
|
194
|
-
WHERE c.component_file = ?
|
|
195
|
-
ORDER BY s.feature_key
|
|
196
|
-
`).all(filePath) as Record<string, unknown>[];
|
|
197
|
-
return rows.map(toFeature);
|
|
198
|
-
}
|
|
199
|
-
|
|
200
|
-
export function getFeaturesByRoute(db: Database.Database, route: string): Feature[] {
|
|
201
|
-
const rows = db.prepare(`
|
|
202
|
-
SELECT DISTINCT s.* FROM ${t('sentinel')} s
|
|
203
|
-
JOIN ${t('sentinel_pages')} p ON p.feature_id = s.id
|
|
204
|
-
WHERE p.page_route = ?
|
|
205
|
-
ORDER BY s.feature_key
|
|
206
|
-
`).all(route) as Record<string, unknown>[];
|
|
207
|
-
return rows.map(toFeature);
|
|
208
|
-
}
|
|
209
|
-
|
|
210
|
-
// ============================================================
|
|
211
|
-
// Feature Detail (full join)
|
|
212
|
-
// ============================================================
|
|
213
|
-
|
|
214
|
-
export function getFeatureDetail(db: Database.Database, featureKeyOrId: string | number): FeatureDetail | null {
|
|
215
|
-
let feature: Feature | null;
|
|
216
|
-
if (typeof featureKeyOrId === 'number') {
|
|
217
|
-
feature = getFeatureById(db, featureKeyOrId);
|
|
218
|
-
} else {
|
|
219
|
-
feature = getFeature(db, featureKeyOrId);
|
|
220
|
-
}
|
|
221
|
-
if (!feature) return null;
|
|
222
|
-
|
|
223
|
-
const components = db.prepare(`SELECT * FROM ${t('sentinel_components')} WHERE feature_id = ?`).all(feature.id) as FeatureComponent[];
|
|
224
|
-
const procedures = db.prepare(`SELECT * FROM ${t('sentinel_procedures')} WHERE feature_id = ?`).all(feature.id) as FeatureProcedure[];
|
|
225
|
-
const pages = db.prepare(`SELECT * FROM ${t('sentinel_pages')} WHERE feature_id = ?`).all(feature.id) as FeaturePage[];
|
|
226
|
-
const dependencies = db.prepare(`SELECT * FROM ${t('sentinel_deps')} WHERE feature_id = ?`).all(feature.id) as FeatureDep[];
|
|
227
|
-
const changelog = db.prepare(`SELECT * FROM ${t('sentinel_changelog')} WHERE feature_id = ? ORDER BY created_at DESC LIMIT 50`).all(feature.id) as FeatureChangeLog[];
|
|
228
|
-
|
|
229
|
-
return { ...feature, components, procedures, pages, dependencies, changelog };
|
|
230
|
-
}
|
|
231
|
-
|
|
232
|
-
// ============================================================
|
|
233
|
-
// Orphan & Impact Detection
|
|
234
|
-
// ============================================================
|
|
235
|
-
|
|
236
|
-
export function getOrphanedFeatures(db: Database.Database): Feature[] {
|
|
237
|
-
// Active features with no living primary component files
|
|
238
|
-
const features = db.prepare(`
|
|
239
|
-
SELECT s.* FROM ${t('sentinel')} s
|
|
240
|
-
WHERE s.status = 'active'
|
|
241
|
-
AND NOT EXISTS (
|
|
242
|
-
SELECT 1 FROM ${t('sentinel_components')} c
|
|
243
|
-
WHERE c.feature_id = s.id AND c.is_primary = 1
|
|
244
|
-
)
|
|
245
|
-
ORDER BY s.domain, s.feature_key
|
|
246
|
-
`).all() as Record<string, unknown>[];
|
|
247
|
-
return features.map(toFeature);
|
|
248
|
-
}
|
|
249
|
-
|
|
250
|
-
export function getFeatureImpact(db: Database.Database, filePaths: string[]): ImpactReport {
|
|
251
|
-
const fileSet = new Set(filePaths);
|
|
252
|
-
const affectedFeatureIds = new Set<number>();
|
|
253
|
-
|
|
254
|
-
// Find all features linked to these files
|
|
255
|
-
for (const filePath of filePaths) {
|
|
256
|
-
const links = db.prepare(
|
|
257
|
-
`SELECT feature_id FROM ${t('sentinel_components')} WHERE component_file = ?`
|
|
258
|
-
).all(filePath) as { feature_id: number }[];
|
|
259
|
-
for (const link of links) {
|
|
260
|
-
affectedFeatureIds.add(link.feature_id);
|
|
261
|
-
}
|
|
262
|
-
}
|
|
263
|
-
|
|
264
|
-
const orphaned: ImpactItem[] = [];
|
|
265
|
-
const degraded: ImpactItem[] = [];
|
|
266
|
-
const unaffected: ImpactItem[] = [];
|
|
267
|
-
|
|
268
|
-
for (const featureId of affectedFeatureIds) {
|
|
269
|
-
const feature = getFeatureById(db, featureId);
|
|
270
|
-
if (!feature || feature.status !== 'active') continue;
|
|
271
|
-
|
|
272
|
-
const allComponents = db.prepare(
|
|
273
|
-
`SELECT component_file, is_primary FROM ${t('sentinel_components')} WHERE feature_id = ?`
|
|
274
|
-
).all(featureId) as { component_file: string; is_primary: number }[];
|
|
275
|
-
|
|
276
|
-
const affected = allComponents.filter(c => fileSet.has(c.component_file));
|
|
277
|
-
const remaining = allComponents.filter(c => !fileSet.has(c.component_file));
|
|
278
|
-
const primaryAffected = affected.some(c => c.is_primary);
|
|
279
|
-
|
|
280
|
-
const item: ImpactItem = {
|
|
281
|
-
feature,
|
|
282
|
-
affected_files: affected.map(c => c.component_file),
|
|
283
|
-
remaining_files: remaining.map(c => c.component_file),
|
|
284
|
-
status: 'unaffected',
|
|
285
|
-
};
|
|
286
|
-
|
|
287
|
-
if (primaryAffected && remaining.filter(c => c.is_primary).length === 0) {
|
|
288
|
-
item.status = 'orphaned';
|
|
289
|
-
orphaned.push(item);
|
|
290
|
-
} else if (affected.length > 0) {
|
|
291
|
-
item.status = 'degraded';
|
|
292
|
-
degraded.push(item);
|
|
293
|
-
} else {
|
|
294
|
-
unaffected.push(item);
|
|
295
|
-
}
|
|
296
|
-
}
|
|
297
|
-
|
|
298
|
-
const hasCriticalOrphans = orphaned.some(o => o.feature.priority === 'critical');
|
|
299
|
-
const hasStandardOrphans = orphaned.some(o => o.feature.priority === 'standard');
|
|
300
|
-
|
|
301
|
-
return {
|
|
302
|
-
files_analyzed: filePaths,
|
|
303
|
-
orphaned,
|
|
304
|
-
degraded,
|
|
305
|
-
unaffected,
|
|
306
|
-
blocked: hasCriticalOrphans || hasStandardOrphans,
|
|
307
|
-
block_reason: hasCriticalOrphans
|
|
308
|
-
? `BLOCKED: ${orphaned.length} features would be orphaned (includes critical features). Create migration plan first.`
|
|
309
|
-
: hasStandardOrphans
|
|
310
|
-
? `BLOCKED: ${orphaned.length} standard features would be orphaned. Create migration plan first.`
|
|
311
|
-
: null,
|
|
312
|
-
};
|
|
313
|
-
}
|
|
314
|
-
|
|
315
|
-
// ============================================================
|
|
316
|
-
// Links: Components, Procedures, Pages
|
|
317
|
-
// ============================================================
|
|
318
|
-
|
|
319
|
-
export function linkComponent(
|
|
320
|
-
db: Database.Database,
|
|
321
|
-
featureId: number,
|
|
322
|
-
filePath: string,
|
|
323
|
-
componentName: string | null,
|
|
324
|
-
role: ComponentRole = 'implementation',
|
|
325
|
-
isPrimary: boolean = false
|
|
326
|
-
): void {
|
|
327
|
-
db.prepare(`
|
|
328
|
-
INSERT OR REPLACE INTO ${t('sentinel_components')} (feature_id, component_file, component_name, role, is_primary)
|
|
329
|
-
VALUES (?, ?, ?, ?, ?)
|
|
330
|
-
`).run(featureId, filePath, componentName, role, isPrimary ? 1 : 0);
|
|
331
|
-
}
|
|
332
|
-
|
|
333
|
-
export function linkProcedure(
|
|
334
|
-
db: Database.Database,
|
|
335
|
-
featureId: number,
|
|
336
|
-
routerName: string,
|
|
337
|
-
procedureName: string,
|
|
338
|
-
procedureType?: string
|
|
339
|
-
): void {
|
|
340
|
-
db.prepare(`
|
|
341
|
-
INSERT OR REPLACE INTO ${t('sentinel_procedures')} (feature_id, router_name, procedure_name, procedure_type)
|
|
342
|
-
VALUES (?, ?, ?, ?)
|
|
343
|
-
`).run(featureId, routerName, procedureName, procedureType || null);
|
|
344
|
-
}
|
|
345
|
-
|
|
346
|
-
export function linkPage(
|
|
347
|
-
db: Database.Database,
|
|
348
|
-
featureId: number,
|
|
349
|
-
route: string,
|
|
350
|
-
portal?: string
|
|
351
|
-
): void {
|
|
352
|
-
db.prepare(`
|
|
353
|
-
INSERT OR REPLACE INTO ${t('sentinel_pages')} (feature_id, page_route, portal)
|
|
354
|
-
VALUES (?, ?, ?)
|
|
355
|
-
`).run(featureId, route, portal || null);
|
|
356
|
-
}
|
|
357
|
-
|
|
358
|
-
// ============================================================
|
|
359
|
-
// Changelog
|
|
360
|
-
// ============================================================
|
|
361
|
-
|
|
362
|
-
export function logChange(
|
|
363
|
-
db: Database.Database,
|
|
364
|
-
featureId: number,
|
|
365
|
-
changeType: string,
|
|
366
|
-
detail: string | null,
|
|
367
|
-
commitHash?: string,
|
|
368
|
-
changedBy: string = 'claude-code'
|
|
369
|
-
): void {
|
|
370
|
-
db.prepare(`
|
|
371
|
-
INSERT INTO ${t('sentinel_changelog')} (feature_id, change_type, changed_by, change_detail, commit_hash)
|
|
372
|
-
VALUES (?, ?, ?, ?, ?)
|
|
373
|
-
`).run(featureId, changeType, changedBy, detail, commitHash || null);
|
|
374
|
-
}
|
|
375
|
-
|
|
376
|
-
// ============================================================
|
|
377
|
-
// Validation
|
|
378
|
-
// ============================================================
|
|
379
|
-
|
|
380
|
-
export function validateFeatures(db: Database.Database, domainFilter?: string): ValidationReport {
|
|
381
|
-
let sql = `SELECT * FROM ${t('sentinel')} WHERE status = 'active'`;
|
|
382
|
-
const params: unknown[] = [];
|
|
383
|
-
if (domainFilter) {
|
|
384
|
-
sql += ' AND domain = ?';
|
|
385
|
-
params.push(domainFilter);
|
|
386
|
-
}
|
|
387
|
-
sql += ' ORDER BY domain, feature_key';
|
|
388
|
-
|
|
389
|
-
const features = db.prepare(sql).all(...params) as Record<string, unknown>[];
|
|
390
|
-
const details: ValidationItem[] = [];
|
|
391
|
-
let alive = 0;
|
|
392
|
-
let orphaned = 0;
|
|
393
|
-
let degradedCount = 0;
|
|
394
|
-
|
|
395
|
-
for (const row of features) {
|
|
396
|
-
const feature = toFeature(row);
|
|
397
|
-
const components = db.prepare(`SELECT * FROM ${t('sentinel_components')} WHERE feature_id = ?`).all(feature.id) as FeatureComponent[];
|
|
398
|
-
const procedures = db.prepare(`SELECT * FROM ${t('sentinel_procedures')} WHERE feature_id = ?`).all(feature.id) as FeatureProcedure[];
|
|
399
|
-
const pages = db.prepare(`SELECT * FROM ${t('sentinel_pages')} WHERE feature_id = ?`).all(feature.id) as FeaturePage[];
|
|
400
|
-
|
|
401
|
-
const missingComponents: string[] = [];
|
|
402
|
-
const missingProcedures: { router: string; procedure: string }[] = [];
|
|
403
|
-
const missingPages: string[] = [];
|
|
404
|
-
|
|
405
|
-
// Check component files exist on disk
|
|
406
|
-
for (const comp of components) {
|
|
407
|
-
const absPath = resolve(PROJECT_ROOT, comp.component_file);
|
|
408
|
-
if (!existsSync(absPath)) {
|
|
409
|
-
missingComponents.push(comp.component_file);
|
|
410
|
-
}
|
|
411
|
-
}
|
|
412
|
-
|
|
413
|
-
// Check procedure files exist and contain the procedure
|
|
414
|
-
for (const proc of procedures) {
|
|
415
|
-
// Look for the router file based on convention
|
|
416
|
-
const routerPath = resolve(PROJECT_ROOT, `src/server/api/routers/${proc.router_name}.ts`);
|
|
417
|
-
if (!existsSync(routerPath)) {
|
|
418
|
-
missingProcedures.push({ router: proc.router_name, procedure: proc.procedure_name });
|
|
419
|
-
}
|
|
420
|
-
}
|
|
421
|
-
|
|
422
|
-
// Check page routes exist
|
|
423
|
-
for (const page of pages) {
|
|
424
|
-
const routeToPath = page.page_route
|
|
425
|
-
.replace(/^\/(portal-[^/]+\/)?/, 'src/app/')
|
|
426
|
-
.replace(/\/$/, '')
|
|
427
|
-
+ '/page.tsx';
|
|
428
|
-
const absPath = resolve(PROJECT_ROOT, routeToPath);
|
|
429
|
-
// Page route checking is approximate - don't flag as missing if path conversion is ambiguous
|
|
430
|
-
if (page.page_route.startsWith('/') && !existsSync(absPath)) {
|
|
431
|
-
// Try alternative path patterns
|
|
432
|
-
const altPath = resolve(PROJECT_ROOT, `src/app${page.page_route}/page.tsx`);
|
|
433
|
-
if (!existsSync(altPath)) {
|
|
434
|
-
missingPages.push(page.page_route);
|
|
435
|
-
}
|
|
436
|
-
}
|
|
437
|
-
}
|
|
438
|
-
|
|
439
|
-
const hasMissing = missingComponents.length > 0 || missingProcedures.length > 0;
|
|
440
|
-
const primaryMissing = components.filter(c => c.is_primary && missingComponents.includes(c.component_file)).length > 0;
|
|
441
|
-
const allPrimaryMissing = components.filter(c => c.is_primary).length > 0 &&
|
|
442
|
-
components.filter(c => c.is_primary).every(c => missingComponents.includes(c.component_file));
|
|
443
|
-
|
|
444
|
-
let status: 'alive' | 'orphaned' | 'degraded';
|
|
445
|
-
if (allPrimaryMissing || (components.length > 0 && missingComponents.length === components.length)) {
|
|
446
|
-
status = 'orphaned';
|
|
447
|
-
orphaned++;
|
|
448
|
-
} else if (hasMissing) {
|
|
449
|
-
status = 'degraded';
|
|
450
|
-
degradedCount++;
|
|
451
|
-
} else {
|
|
452
|
-
status = 'alive';
|
|
453
|
-
alive++;
|
|
454
|
-
}
|
|
455
|
-
|
|
456
|
-
details.push({
|
|
457
|
-
feature,
|
|
458
|
-
missing_components: missingComponents,
|
|
459
|
-
missing_procedures: missingProcedures,
|
|
460
|
-
missing_pages: missingPages,
|
|
461
|
-
status,
|
|
462
|
-
});
|
|
463
|
-
}
|
|
464
|
-
|
|
465
|
-
return { alive, orphaned, degraded: degradedCount, details };
|
|
466
|
-
}
|
|
467
|
-
|
|
468
|
-
// ============================================================
|
|
469
|
-
// Parity Check (for rebuild scenarios)
|
|
470
|
-
// ============================================================
|
|
471
|
-
|
|
472
|
-
export function checkParity(db: Database.Database, oldFiles: string[], newFiles: string[]): ParityReport {
|
|
473
|
-
const oldFileSet = new Set(oldFiles);
|
|
474
|
-
const newFileSet = new Set(newFiles);
|
|
475
|
-
|
|
476
|
-
// Find features linked to old files
|
|
477
|
-
const oldFeatureIds = new Set<number>();
|
|
478
|
-
for (const file of oldFiles) {
|
|
479
|
-
const links = db.prepare(`SELECT feature_id FROM ${t('sentinel_components')} WHERE component_file = ?`).all(file) as { feature_id: number }[];
|
|
480
|
-
for (const link of links) {
|
|
481
|
-
oldFeatureIds.add(link.feature_id);
|
|
482
|
-
}
|
|
483
|
-
}
|
|
484
|
-
|
|
485
|
-
// Find features linked to new files
|
|
486
|
-
const newFeatureIds = new Set<number>();
|
|
487
|
-
for (const file of newFiles) {
|
|
488
|
-
const links = db.prepare(`SELECT feature_id FROM ${t('sentinel_components')} WHERE component_file = ?`).all(file) as { feature_id: number }[];
|
|
489
|
-
for (const link of links) {
|
|
490
|
-
newFeatureIds.add(link.feature_id);
|
|
491
|
-
}
|
|
492
|
-
}
|
|
493
|
-
|
|
494
|
-
const done: ParityItem[] = [];
|
|
495
|
-
const gaps: ParityItem[] = [];
|
|
496
|
-
const newFeatures: ParityItem[] = [];
|
|
497
|
-
|
|
498
|
-
// Features in old that are also in new = DONE
|
|
499
|
-
// Features in old but NOT in new = GAP
|
|
500
|
-
for (const fId of oldFeatureIds) {
|
|
501
|
-
const feature = getFeatureById(db, fId);
|
|
502
|
-
if (!feature) continue;
|
|
503
|
-
|
|
504
|
-
const oldComps = db.prepare(`SELECT component_file FROM ${t('sentinel_components')} WHERE feature_id = ? AND component_file IN (` + oldFiles.map(() => '?').join(',') + ')').all(fId, ...oldFiles) as { component_file: string }[];
|
|
505
|
-
const newComps = db.prepare(`SELECT component_file FROM ${t('sentinel_components')} WHERE feature_id = ? AND component_file IN (` + newFiles.map(() => '?').join(',') + ')').all(fId, ...newFiles) as { component_file: string }[];
|
|
506
|
-
|
|
507
|
-
const item: ParityItem = {
|
|
508
|
-
feature_key: feature.feature_key,
|
|
509
|
-
title: feature.title,
|
|
510
|
-
status: newFeatureIds.has(fId) ? 'DONE' : 'GAP',
|
|
511
|
-
old_files: oldComps.map(c => c.component_file),
|
|
512
|
-
new_files: newComps.map(c => c.component_file),
|
|
513
|
-
};
|
|
514
|
-
|
|
515
|
-
if (item.status === 'DONE') {
|
|
516
|
-
done.push(item);
|
|
517
|
-
} else {
|
|
518
|
-
gaps.push(item);
|
|
519
|
-
}
|
|
520
|
-
}
|
|
521
|
-
|
|
522
|
-
// Features only in new = NEW
|
|
523
|
-
for (const fId of newFeatureIds) {
|
|
524
|
-
if (oldFeatureIds.has(fId)) continue;
|
|
525
|
-
const feature = getFeatureById(db, fId);
|
|
526
|
-
if (!feature) continue;
|
|
527
|
-
|
|
528
|
-
const newComps = db.prepare(`SELECT component_file FROM ${t('sentinel_components')} WHERE feature_id = ? AND component_file IN (` + newFiles.map(() => '?').join(',') + ')').all(fId, ...newFiles) as { component_file: string }[];
|
|
529
|
-
|
|
530
|
-
newFeatures.push({
|
|
531
|
-
feature_key: feature.feature_key,
|
|
532
|
-
title: feature.title,
|
|
533
|
-
status: 'NEW',
|
|
534
|
-
old_files: [],
|
|
535
|
-
new_files: newComps.map(c => c.component_file),
|
|
536
|
-
});
|
|
537
|
-
}
|
|
538
|
-
|
|
539
|
-
const total = done.length + gaps.length;
|
|
540
|
-
const parityPercentage = total > 0 ? Math.round((done.length / total) * 100) : 100;
|
|
541
|
-
|
|
542
|
-
return { done, gaps, new_features: newFeatures, parity_percentage: parityPercentage };
|
|
543
|
-
}
|
|
544
|
-
|
|
545
|
-
// ============================================================
|
|
546
|
-
// Bulk operations for scanner
|
|
547
|
-
// ============================================================
|
|
548
|
-
|
|
549
|
-
export function clearAutoDiscoveredFeatures(db: Database.Database): void {
|
|
550
|
-
// Only clear features that were auto-discovered (not manually registered)
|
|
551
|
-
// We use the changelog to distinguish: auto-discovered ones have changed_by = 'scanner'
|
|
552
|
-
const autoIds = db.prepare(`
|
|
553
|
-
SELECT DISTINCT feature_id FROM ${t('sentinel_changelog')}
|
|
554
|
-
WHERE changed_by = 'scanner' AND change_type = 'created'
|
|
555
|
-
`).all() as { feature_id: number }[];
|
|
556
|
-
|
|
557
|
-
// Don't delete features that also have manual changes
|
|
558
|
-
for (const { feature_id } of autoIds) {
|
|
559
|
-
const hasManualChanges = db.prepare(`
|
|
560
|
-
SELECT 1 FROM ${t('sentinel_changelog')}
|
|
561
|
-
WHERE feature_id = ? AND changed_by != 'scanner'
|
|
562
|
-
`).get(feature_id);
|
|
563
|
-
|
|
564
|
-
if (!hasManualChanges) {
|
|
565
|
-
db.prepare(`DELETE FROM ${t('sentinel')} WHERE id = ?`).run(feature_id);
|
|
566
|
-
}
|
|
567
|
-
}
|
|
568
|
-
}
|
|
569
|
-
|
|
570
|
-
export function bulkUpsertFeatures(db: Database.Database, features: FeatureInput[]): number {
|
|
571
|
-
let count = 0;
|
|
572
|
-
const upsert = db.transaction((items: FeatureInput[]) => {
|
|
573
|
-
for (const item of items) {
|
|
574
|
-
upsertFeature(db, item);
|
|
575
|
-
count++;
|
|
576
|
-
}
|
|
577
|
-
});
|
|
578
|
-
upsert(features);
|
|
579
|
-
return count;
|
|
580
|
-
}
|