@massu/core 0.1.0 → 0.1.2
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/LICENSE +71 -0
- package/README.md +2 -2
- package/dist/hooks/cost-tracker.js +149 -11527
- package/dist/hooks/post-edit-context.js +127 -11493
- package/dist/hooks/post-tool-use.js +169 -11550
- package/dist/hooks/pre-compact.js +149 -11530
- package/dist/hooks/pre-delete-check.js +144 -11523
- package/dist/hooks/quality-event.js +149 -11527
- package/dist/hooks/session-end.js +188 -11570
- package/dist/hooks/session-start.js +159 -11534
- package/dist/hooks/user-prompt.js +149 -11530
- package/package.json +14 -19
- package/src/adr-generator.ts +292 -0
- package/src/analytics.ts +373 -0
- package/src/audit-trail.ts +450 -0
- package/src/backfill-sessions.ts +180 -0
- package/src/cli.ts +105 -0
- package/src/cloud-sync.ts +190 -0
- package/src/commands/doctor.ts +300 -0
- package/src/commands/init.ts +395 -0
- package/src/commands/install-hooks.ts +26 -0
- package/src/config.ts +357 -0
- package/src/cost-tracker.ts +355 -0
- package/src/db.ts +233 -0
- package/src/dependency-scorer.ts +337 -0
- package/src/docs-map.json +100 -0
- package/src/docs-tools.ts +517 -0
- package/src/domains.ts +181 -0
- package/src/hooks/cost-tracker.ts +66 -0
- package/src/hooks/intent-suggester.ts +131 -0
- package/src/hooks/post-edit-context.ts +91 -0
- package/src/hooks/post-tool-use.ts +175 -0
- package/src/hooks/pre-compact.ts +146 -0
- package/src/hooks/pre-delete-check.ts +153 -0
- package/src/hooks/quality-event.ts +127 -0
- package/src/hooks/security-gate.ts +121 -0
- package/src/hooks/session-end.ts +467 -0
- package/src/hooks/session-start.ts +210 -0
- package/src/hooks/user-prompt.ts +91 -0
- package/src/import-resolver.ts +224 -0
- package/src/memory-db.ts +1376 -0
- package/src/memory-tools.ts +391 -0
- package/src/middleware-tree.ts +70 -0
- package/src/observability-tools.ts +343 -0
- package/src/observation-extractor.ts +411 -0
- package/src/page-deps.ts +283 -0
- package/src/prompt-analyzer.ts +332 -0
- package/src/regression-detector.ts +319 -0
- package/src/rules.ts +57 -0
- package/src/schema-mapper.ts +232 -0
- package/src/security-scorer.ts +405 -0
- package/src/security-utils.ts +133 -0
- package/src/sentinel-db.ts +578 -0
- package/src/sentinel-scanner.ts +405 -0
- package/src/sentinel-tools.ts +512 -0
- package/src/sentinel-types.ts +140 -0
- package/src/server.ts +189 -0
- package/src/session-archiver.ts +112 -0
- package/src/session-state-generator.ts +174 -0
- package/src/team-knowledge.ts +407 -0
- package/src/tools.ts +847 -0
- package/src/transcript-parser.ts +458 -0
- package/src/trpc-index.ts +214 -0
- package/src/validate-features-runner.ts +106 -0
- package/src/validation-engine.ts +358 -0
- package/dist/cli.js +0 -7890
- package/dist/server.js +0 -7008
|
@@ -0,0 +1,512 @@
|
|
|
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 type { ToolDefinition, ToolResult } from './tools.ts';
|
|
6
|
+
import {
|
|
7
|
+
searchFeatures,
|
|
8
|
+
getFeatureDetail,
|
|
9
|
+
getFeatureImpact,
|
|
10
|
+
validateFeatures,
|
|
11
|
+
upsertFeature,
|
|
12
|
+
linkComponent,
|
|
13
|
+
linkProcedure,
|
|
14
|
+
linkPage,
|
|
15
|
+
logChange,
|
|
16
|
+
checkParity,
|
|
17
|
+
} from './sentinel-db.ts';
|
|
18
|
+
import type { ComponentRole, FeatureStatus, FeaturePriority } from './sentinel-types.ts';
|
|
19
|
+
import { getConfig } from './config.ts';
|
|
20
|
+
|
|
21
|
+
/** Prefix a base tool name with the configured tool prefix. */
|
|
22
|
+
function p(baseName: string): string {
|
|
23
|
+
return `${getConfig().toolPrefix}_${baseName}`;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// ============================================================
|
|
27
|
+
// Sentinel: MCP Tool Definitions & Handlers
|
|
28
|
+
// ============================================================
|
|
29
|
+
|
|
30
|
+
function text(content: string): ToolResult {
|
|
31
|
+
return { content: [{ type: 'text', text: content }] };
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export function getSentinelToolDefinitions(): ToolDefinition[] {
|
|
35
|
+
return [
|
|
36
|
+
// P2-001: sentinel_search
|
|
37
|
+
{
|
|
38
|
+
name: p('sentinel_search'),
|
|
39
|
+
description: 'Search and list features in the Sentinel feature registry. Supports FTS5 full-text search and domain/status/portal filters.',
|
|
40
|
+
inputSchema: {
|
|
41
|
+
type: 'object',
|
|
42
|
+
properties: {
|
|
43
|
+
query: { type: 'string', description: 'Full-text search query (FTS5 syntax)' },
|
|
44
|
+
domain: { type: 'string', description: 'Filter by domain (e.g., production, design, crm)' },
|
|
45
|
+
subdomain: { type: 'string', description: 'Filter by subdomain (e.g., factory-review)' },
|
|
46
|
+
status: { type: 'string', description: 'Filter by status: planned, active, deprecated, removed' },
|
|
47
|
+
portal: { type: 'string', description: 'Filter by portal scope: internal, factory, designer, customer' },
|
|
48
|
+
page_route: { type: 'string', description: 'Filter by page route (e.g., /production/factory-reviews/[id])' },
|
|
49
|
+
},
|
|
50
|
+
required: [],
|
|
51
|
+
},
|
|
52
|
+
},
|
|
53
|
+
// P2-002: sentinel_detail
|
|
54
|
+
{
|
|
55
|
+
name: p('sentinel_detail'),
|
|
56
|
+
description: 'Get full feature details including all linked components, procedures, pages, dependencies, and changelog.',
|
|
57
|
+
inputSchema: {
|
|
58
|
+
type: 'object',
|
|
59
|
+
properties: {
|
|
60
|
+
feature_key: { type: 'string', description: 'Feature key (e.g., factory-review.pdf-export)' },
|
|
61
|
+
feature_id: { type: 'number', description: 'Feature ID (alternative to feature_key)' },
|
|
62
|
+
},
|
|
63
|
+
required: [],
|
|
64
|
+
},
|
|
65
|
+
},
|
|
66
|
+
// P2-003: sentinel_impact
|
|
67
|
+
{
|
|
68
|
+
name: p('sentinel_impact'),
|
|
69
|
+
description: 'Pre-deletion impact analysis. Shows which features would be orphaned, degraded, or unaffected if specified files are deleted. BLOCKS if critical features would be orphaned.',
|
|
70
|
+
inputSchema: {
|
|
71
|
+
type: 'object',
|
|
72
|
+
properties: {
|
|
73
|
+
files: {
|
|
74
|
+
type: 'array',
|
|
75
|
+
items: { type: 'string' },
|
|
76
|
+
description: 'Array of file paths to analyze (relative to project root)',
|
|
77
|
+
},
|
|
78
|
+
},
|
|
79
|
+
required: ['files'],
|
|
80
|
+
},
|
|
81
|
+
},
|
|
82
|
+
// P2-004: sentinel_validate
|
|
83
|
+
{
|
|
84
|
+
name: p('sentinel_validate'),
|
|
85
|
+
description: 'Validate all active features have living implementation files. Reports alive, orphaned, and degraded features.',
|
|
86
|
+
inputSchema: {
|
|
87
|
+
type: 'object',
|
|
88
|
+
properties: {
|
|
89
|
+
domain: { type: 'string', description: 'Optional: only validate features in this domain' },
|
|
90
|
+
fix: { type: 'boolean', description: 'If true, auto-mark dead features as deprecated' },
|
|
91
|
+
},
|
|
92
|
+
required: [],
|
|
93
|
+
},
|
|
94
|
+
},
|
|
95
|
+
// P2-005: sentinel_register
|
|
96
|
+
{
|
|
97
|
+
name: p('sentinel_register'),
|
|
98
|
+
description: 'Register or update a feature in the sentinel registry. Links components, procedures, and pages.',
|
|
99
|
+
inputSchema: {
|
|
100
|
+
type: 'object',
|
|
101
|
+
properties: {
|
|
102
|
+
feature_key: { type: 'string', description: 'Unique feature key (e.g., factory-review.pdf-export)' },
|
|
103
|
+
domain: { type: 'string', description: 'Domain (e.g., production, design, crm)' },
|
|
104
|
+
title: { type: 'string', description: 'Human-readable title' },
|
|
105
|
+
description: { type: 'string', description: 'Feature description / user story' },
|
|
106
|
+
status: { type: 'string', description: 'planned | active | deprecated | removed' },
|
|
107
|
+
priority: { type: 'string', description: 'critical | standard | nice-to-have' },
|
|
108
|
+
portal_scope: { type: 'array', items: { type: 'string' }, description: 'Portal scope array' },
|
|
109
|
+
components: {
|
|
110
|
+
type: 'array',
|
|
111
|
+
items: {
|
|
112
|
+
type: 'object',
|
|
113
|
+
properties: {
|
|
114
|
+
file: { type: 'string' },
|
|
115
|
+
name: { type: 'string' },
|
|
116
|
+
role: { type: 'string' },
|
|
117
|
+
is_primary: { type: 'boolean' },
|
|
118
|
+
},
|
|
119
|
+
required: ['file'],
|
|
120
|
+
},
|
|
121
|
+
description: 'Component files to link',
|
|
122
|
+
},
|
|
123
|
+
procedures: {
|
|
124
|
+
type: 'array',
|
|
125
|
+
items: {
|
|
126
|
+
type: 'object',
|
|
127
|
+
properties: {
|
|
128
|
+
router: { type: 'string' },
|
|
129
|
+
procedure: { type: 'string' },
|
|
130
|
+
type: { type: 'string' },
|
|
131
|
+
},
|
|
132
|
+
required: ['router', 'procedure'],
|
|
133
|
+
},
|
|
134
|
+
description: 'tRPC procedures to link',
|
|
135
|
+
},
|
|
136
|
+
pages: {
|
|
137
|
+
type: 'array',
|
|
138
|
+
items: {
|
|
139
|
+
type: 'object',
|
|
140
|
+
properties: {
|
|
141
|
+
route: { type: 'string' },
|
|
142
|
+
portal: { type: 'string' },
|
|
143
|
+
},
|
|
144
|
+
required: ['route'],
|
|
145
|
+
},
|
|
146
|
+
description: 'Page routes to link',
|
|
147
|
+
},
|
|
148
|
+
},
|
|
149
|
+
required: ['feature_key', 'domain', 'title'],
|
|
150
|
+
},
|
|
151
|
+
},
|
|
152
|
+
// P2-006: sentinel_parity
|
|
153
|
+
{
|
|
154
|
+
name: p('sentinel_parity'),
|
|
155
|
+
description: 'Compare two sets of files for feature parity. Shows features in old but not new (GAPs), features in both (DONE), and features only in new (NEW).',
|
|
156
|
+
inputSchema: {
|
|
157
|
+
type: 'object',
|
|
158
|
+
properties: {
|
|
159
|
+
old_files: {
|
|
160
|
+
type: 'array',
|
|
161
|
+
items: { type: 'string' },
|
|
162
|
+
description: 'Old implementation files',
|
|
163
|
+
},
|
|
164
|
+
new_files: {
|
|
165
|
+
type: 'array',
|
|
166
|
+
items: { type: 'string' },
|
|
167
|
+
description: 'New implementation files',
|
|
168
|
+
},
|
|
169
|
+
},
|
|
170
|
+
required: ['old_files', 'new_files'],
|
|
171
|
+
},
|
|
172
|
+
},
|
|
173
|
+
];
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
// ============================================================
|
|
177
|
+
// Tool Handler Router
|
|
178
|
+
// ============================================================
|
|
179
|
+
|
|
180
|
+
export function handleSentinelToolCall(
|
|
181
|
+
name: string,
|
|
182
|
+
args: Record<string, unknown>,
|
|
183
|
+
dataDb: Database.Database
|
|
184
|
+
): ToolResult {
|
|
185
|
+
const prefix = getConfig().toolPrefix + '_';
|
|
186
|
+
const baseName = name.startsWith(prefix) ? name.slice(prefix.length) : name;
|
|
187
|
+
|
|
188
|
+
switch (baseName) {
|
|
189
|
+
case 'sentinel_search':
|
|
190
|
+
return handleSearch(args, dataDb);
|
|
191
|
+
case 'sentinel_detail':
|
|
192
|
+
return handleDetail(args, dataDb);
|
|
193
|
+
case 'sentinel_impact':
|
|
194
|
+
return handleImpact(args, dataDb);
|
|
195
|
+
case 'sentinel_validate':
|
|
196
|
+
return handleValidate(args, dataDb);
|
|
197
|
+
case 'sentinel_register':
|
|
198
|
+
return handleRegister(args, dataDb);
|
|
199
|
+
case 'sentinel_parity':
|
|
200
|
+
return handleParityCheck(args, dataDb);
|
|
201
|
+
default:
|
|
202
|
+
return text(`Unknown sentinel tool: ${name}`);
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
// ============================================================
|
|
207
|
+
// Individual Handlers
|
|
208
|
+
// ============================================================
|
|
209
|
+
|
|
210
|
+
function handleSearch(args: Record<string, unknown>, db: Database.Database): ToolResult {
|
|
211
|
+
const query = (args.query as string) || '';
|
|
212
|
+
const filters = {
|
|
213
|
+
domain: args.domain as string | undefined,
|
|
214
|
+
subdomain: args.subdomain as string | undefined,
|
|
215
|
+
status: args.status as string | undefined,
|
|
216
|
+
portal: args.portal as string | undefined,
|
|
217
|
+
page_route: args.page_route as string | undefined,
|
|
218
|
+
};
|
|
219
|
+
|
|
220
|
+
const results = searchFeatures(db, query, filters);
|
|
221
|
+
const lines: string[] = [];
|
|
222
|
+
|
|
223
|
+
lines.push(`## Sentinel Search Results (${results.length} features)`);
|
|
224
|
+
if (query) lines.push(`Query: "${query}"`);
|
|
225
|
+
const activeFilters = Object.entries(filters).filter(([, v]) => v);
|
|
226
|
+
if (activeFilters.length > 0) {
|
|
227
|
+
lines.push(`Filters: ${activeFilters.map(([k, v]) => `${k}=${v}`).join(', ')}`);
|
|
228
|
+
}
|
|
229
|
+
lines.push('');
|
|
230
|
+
|
|
231
|
+
if (results.length === 0) {
|
|
232
|
+
lines.push('No features found matching criteria.');
|
|
233
|
+
return text(lines.join('\n'));
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
let currentDomain = '';
|
|
237
|
+
for (const f of results) {
|
|
238
|
+
if (f.domain !== currentDomain) {
|
|
239
|
+
currentDomain = f.domain;
|
|
240
|
+
lines.push(`### ${currentDomain}`);
|
|
241
|
+
}
|
|
242
|
+
const sub = f.subdomain ? `[${f.subdomain}]` : '';
|
|
243
|
+
const priority = f.priority === 'critical' ? ' [CRITICAL]' : '';
|
|
244
|
+
lines.push(`- **${f.feature_key}** ${sub}${priority}: ${f.title} (${f.component_count}C/${f.procedure_count}P/${f.page_count}R) [${f.status}]`);
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
return text(lines.join('\n'));
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
function handleDetail(args: Record<string, unknown>, db: Database.Database): ToolResult {
|
|
251
|
+
const key = args.feature_key as string | undefined;
|
|
252
|
+
const id = args.feature_id as number | undefined;
|
|
253
|
+
|
|
254
|
+
if (!key && !id) {
|
|
255
|
+
return text('Error: Provide either feature_key or feature_id');
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
const detail = getFeatureDetail(db, id || key!);
|
|
259
|
+
if (!detail) {
|
|
260
|
+
return text(`Feature not found: ${key || id}`);
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
const lines: string[] = [];
|
|
264
|
+
lines.push(`## Feature: ${detail.feature_key}`);
|
|
265
|
+
lines.push(`- **Title**: ${detail.title}`);
|
|
266
|
+
lines.push(`- **Domain**: ${detail.domain}${detail.subdomain ? '/' + detail.subdomain : ''}`);
|
|
267
|
+
lines.push(`- **Status**: ${detail.status}`);
|
|
268
|
+
lines.push(`- **Priority**: ${detail.priority}`);
|
|
269
|
+
if (detail.description) lines.push(`- **Description**: ${detail.description}`);
|
|
270
|
+
if (detail.portal_scope.length > 0) lines.push(`- **Portals**: ${detail.portal_scope.join(', ')}`);
|
|
271
|
+
lines.push('');
|
|
272
|
+
|
|
273
|
+
if (detail.components.length > 0) {
|
|
274
|
+
lines.push(`### Components (${detail.components.length})`);
|
|
275
|
+
for (const c of detail.components) {
|
|
276
|
+
const primary = c.is_primary ? ' [PRIMARY]' : '';
|
|
277
|
+
lines.push(`- ${c.component_file}${c.component_name ? ':' + c.component_name : ''} (${c.role})${primary}`);
|
|
278
|
+
}
|
|
279
|
+
lines.push('');
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
if (detail.procedures.length > 0) {
|
|
283
|
+
lines.push(`### Procedures (${detail.procedures.length})`);
|
|
284
|
+
for (const p of detail.procedures) {
|
|
285
|
+
lines.push(`- ${p.router_name}.${p.procedure_name}${p.procedure_type ? ' (' + p.procedure_type + ')' : ''}`);
|
|
286
|
+
}
|
|
287
|
+
lines.push('');
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
if (detail.pages.length > 0) {
|
|
291
|
+
lines.push(`### Pages (${detail.pages.length})`);
|
|
292
|
+
for (const p of detail.pages) {
|
|
293
|
+
lines.push(`- ${p.page_route}${p.portal ? ' (' + p.portal + ')' : ''}`);
|
|
294
|
+
}
|
|
295
|
+
lines.push('');
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
if (detail.dependencies.length > 0) {
|
|
299
|
+
lines.push(`### Dependencies (${detail.dependencies.length})`);
|
|
300
|
+
for (const d of detail.dependencies) {
|
|
301
|
+
lines.push(`- ${d.dependency_type}: feature #${d.depends_on_feature_id}`);
|
|
302
|
+
}
|
|
303
|
+
lines.push('');
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
if (detail.changelog.length > 0) {
|
|
307
|
+
lines.push(`### Changelog (last ${detail.changelog.length})`);
|
|
308
|
+
for (const c of detail.changelog.slice(0, 10)) {
|
|
309
|
+
lines.push(`- [${c.created_at}] ${c.change_type}: ${c.change_detail || '(no detail)'}${c.commit_hash ? ' @' + c.commit_hash.substring(0, 8) : ''}`);
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
return text(lines.join('\n'));
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
function handleImpact(args: Record<string, unknown>, db: Database.Database): ToolResult {
|
|
317
|
+
const files = args.files as string[];
|
|
318
|
+
if (!files || files.length === 0) {
|
|
319
|
+
return text('Error: Provide files array');
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
const report = getFeatureImpact(db, files);
|
|
323
|
+
const lines: string[] = [];
|
|
324
|
+
|
|
325
|
+
lines.push(`## Feature Impact Analysis`);
|
|
326
|
+
lines.push(`Files analyzed: ${report.files_analyzed.length}`);
|
|
327
|
+
lines.push('');
|
|
328
|
+
|
|
329
|
+
if (report.blocked) {
|
|
330
|
+
lines.push(`### ${report.block_reason}`);
|
|
331
|
+
lines.push('');
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
lines.push(`### Summary`);
|
|
335
|
+
lines.push(`- Orphaned: ${report.orphaned.length} (no primary components left)`);
|
|
336
|
+
lines.push(`- Degraded: ${report.degraded.length} (some components removed)`);
|
|
337
|
+
lines.push(`- Unaffected: ${report.unaffected.length}`);
|
|
338
|
+
lines.push('');
|
|
339
|
+
|
|
340
|
+
if (report.orphaned.length > 0) {
|
|
341
|
+
lines.push('### Orphaned Features (BLOCKING)');
|
|
342
|
+
for (const item of report.orphaned) {
|
|
343
|
+
lines.push(`- **${item.feature.feature_key}** [${item.feature.priority}]: ${item.feature.title}`);
|
|
344
|
+
lines.push(` Files being deleted: ${item.affected_files.join(', ')}`);
|
|
345
|
+
if (item.remaining_files.length > 0) {
|
|
346
|
+
lines.push(` Remaining (non-primary): ${item.remaining_files.join(', ')}`);
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
lines.push('');
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
if (report.degraded.length > 0) {
|
|
353
|
+
lines.push('### Degraded Features (Warning)');
|
|
354
|
+
for (const item of report.degraded) {
|
|
355
|
+
lines.push(`- **${item.feature.feature_key}**: ${item.feature.title}`);
|
|
356
|
+
lines.push(` Removed: ${item.affected_files.join(', ')}`);
|
|
357
|
+
lines.push(` Remaining: ${item.remaining_files.join(', ')}`);
|
|
358
|
+
}
|
|
359
|
+
lines.push('');
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
return text(lines.join('\n'));
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
function handleValidate(args: Record<string, unknown>, db: Database.Database): ToolResult {
|
|
366
|
+
const domain = args.domain as string | undefined;
|
|
367
|
+
const fix = args.fix as boolean | undefined;
|
|
368
|
+
|
|
369
|
+
const report = validateFeatures(db, domain);
|
|
370
|
+
const lines: string[] = [];
|
|
371
|
+
|
|
372
|
+
lines.push(`## Feature Validation Report`);
|
|
373
|
+
if (domain) lines.push(`Domain filter: ${domain}`);
|
|
374
|
+
lines.push('');
|
|
375
|
+
|
|
376
|
+
lines.push(`### Summary`);
|
|
377
|
+
lines.push(`- Alive: ${report.alive}`);
|
|
378
|
+
lines.push(`- Orphaned: ${report.orphaned}`);
|
|
379
|
+
lines.push(`- Degraded: ${report.degraded}`);
|
|
380
|
+
lines.push(`- Total: ${report.alive + report.orphaned + report.degraded}`);
|
|
381
|
+
lines.push('');
|
|
382
|
+
|
|
383
|
+
const issues = report.details.filter(d => d.status !== 'alive');
|
|
384
|
+
if (issues.length > 0) {
|
|
385
|
+
lines.push('### Issues');
|
|
386
|
+
for (const item of issues) {
|
|
387
|
+
lines.push(`- **${item.feature.feature_key}** [${item.status.toUpperCase()}]`);
|
|
388
|
+
if (item.missing_components.length > 0) {
|
|
389
|
+
lines.push(` Missing components: ${item.missing_components.join(', ')}`);
|
|
390
|
+
}
|
|
391
|
+
if (item.missing_procedures.length > 0) {
|
|
392
|
+
lines.push(` Missing procedures: ${item.missing_procedures.map(p => `${p.router}.${p.procedure}`).join(', ')}`);
|
|
393
|
+
}
|
|
394
|
+
if (item.missing_pages.length > 0) {
|
|
395
|
+
lines.push(` Missing pages: ${item.missing_pages.join(', ')}`);
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
lines.push('');
|
|
399
|
+
|
|
400
|
+
if (fix) {
|
|
401
|
+
lines.push('### Auto-Fix Applied');
|
|
402
|
+
for (const item of issues.filter(i => i.status === 'orphaned')) {
|
|
403
|
+
db.prepare("UPDATE massu_sentinel SET status = 'deprecated', updated_at = datetime('now') WHERE id = ?").run(item.feature.id);
|
|
404
|
+
logChange(db, item.feature.id, 'deprecated', 'Auto-deprecated: all primary components missing');
|
|
405
|
+
lines.push(`- Deprecated: ${item.feature.feature_key}`);
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
} else {
|
|
409
|
+
lines.push('All active features are alive. No issues found.');
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
const result = report.orphaned === 0 ? 'PASS' : 'FAIL';
|
|
413
|
+
lines.push(`### RESULT: ${result}`);
|
|
414
|
+
|
|
415
|
+
return text(lines.join('\n'));
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
function handleRegister(args: Record<string, unknown>, db: Database.Database): ToolResult {
|
|
419
|
+
const featureKey = args.feature_key as string;
|
|
420
|
+
const domain = args.domain as string;
|
|
421
|
+
const title = args.title as string;
|
|
422
|
+
|
|
423
|
+
if (!featureKey || !domain || !title) {
|
|
424
|
+
return text('Error: feature_key, domain, and title are required');
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
const featureId = upsertFeature(db, {
|
|
428
|
+
feature_key: featureKey,
|
|
429
|
+
domain,
|
|
430
|
+
subdomain: args.subdomain as string | undefined,
|
|
431
|
+
title,
|
|
432
|
+
description: args.description as string | undefined,
|
|
433
|
+
status: args.status as FeatureStatus | undefined,
|
|
434
|
+
priority: args.priority as FeaturePriority | undefined,
|
|
435
|
+
portal_scope: args.portal_scope as string[] | undefined,
|
|
436
|
+
});
|
|
437
|
+
|
|
438
|
+
// Link components
|
|
439
|
+
const components = args.components as { file: string; name?: string; role?: string; is_primary?: boolean }[] | undefined;
|
|
440
|
+
if (components) {
|
|
441
|
+
for (const c of components) {
|
|
442
|
+
linkComponent(db, featureId, c.file, c.name || null, (c.role as ComponentRole) || 'implementation', c.is_primary || false);
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
// Link procedures
|
|
447
|
+
const procedures = args.procedures as { router: string; procedure: string; type?: string }[] | undefined;
|
|
448
|
+
if (procedures) {
|
|
449
|
+
for (const p of procedures) {
|
|
450
|
+
linkProcedure(db, featureId, p.router, p.procedure, p.type);
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
// Link pages
|
|
455
|
+
const pages = args.pages as { route: string; portal?: string }[] | undefined;
|
|
456
|
+
if (pages) {
|
|
457
|
+
for (const p of pages) {
|
|
458
|
+
linkPage(db, featureId, p.route, p.portal);
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
logChange(db, featureId, 'created', `Registered via ${p('sentinel_register')}`);
|
|
463
|
+
|
|
464
|
+
return text(`Feature registered: ${featureKey} (ID: ${featureId})\nComponents: ${components?.length || 0}, Procedures: ${procedures?.length || 0}, Pages: ${pages?.length || 0}`);
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
function handleParityCheck(args: Record<string, unknown>, db: Database.Database): ToolResult {
|
|
468
|
+
const oldFiles = args.old_files as string[];
|
|
469
|
+
const newFiles = args.new_files as string[];
|
|
470
|
+
|
|
471
|
+
if (!oldFiles || !newFiles) {
|
|
472
|
+
return text('Error: Provide both old_files and new_files arrays');
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
const report = checkParity(db, oldFiles, newFiles);
|
|
476
|
+
const lines: string[] = [];
|
|
477
|
+
|
|
478
|
+
lines.push(`## Feature Parity Report`);
|
|
479
|
+
lines.push(`Old files: ${oldFiles.length}, New files: ${newFiles.length}`);
|
|
480
|
+
lines.push(`**Parity: ${report.parity_percentage}%**`);
|
|
481
|
+
lines.push('');
|
|
482
|
+
|
|
483
|
+
if (report.done.length > 0) {
|
|
484
|
+
lines.push(`### DONE (${report.done.length} features carried forward)`);
|
|
485
|
+
for (const item of report.done) {
|
|
486
|
+
lines.push(`- ${item.feature_key}: ${item.title}`);
|
|
487
|
+
}
|
|
488
|
+
lines.push('');
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
if (report.gaps.length > 0) {
|
|
492
|
+
lines.push(`### GAPS (${report.gaps.length} features MISSING in new implementation)`);
|
|
493
|
+
for (const item of report.gaps) {
|
|
494
|
+
lines.push(`- **${item.feature_key}**: ${item.title}`);
|
|
495
|
+
lines.push(` Old: ${item.old_files.join(', ')}`);
|
|
496
|
+
}
|
|
497
|
+
lines.push('');
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
if (report.new_features.length > 0) {
|
|
501
|
+
lines.push(`### NEW (${report.new_features.length} features only in new)`);
|
|
502
|
+
for (const item of report.new_features) {
|
|
503
|
+
lines.push(`- ${item.feature_key}: ${item.title}`);
|
|
504
|
+
}
|
|
505
|
+
lines.push('');
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
const result = report.gaps.length === 0 ? 'PASS' : `FAIL (${report.gaps.length} gaps)`;
|
|
509
|
+
lines.push(`### RESULT: ${result}`);
|
|
510
|
+
|
|
511
|
+
return text(lines.join('\n'));
|
|
512
|
+
}
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
// Copyright (c) 2026 Massu. All rights reserved.
|
|
2
|
+
// Licensed under BSL 1.1 - see LICENSE file for details.
|
|
3
|
+
|
|
4
|
+
// ============================================================
|
|
5
|
+
// Sentinel: Feature Registry Type Definitions
|
|
6
|
+
// ============================================================
|
|
7
|
+
|
|
8
|
+
export type FeatureStatus = 'planned' | 'active' | 'deprecated' | 'removed';
|
|
9
|
+
export type FeaturePriority = 'critical' | 'standard' | 'nice-to-have';
|
|
10
|
+
export type ComponentRole = 'implementation' | 'ui' | 'data' | 'utility';
|
|
11
|
+
export type DependencyType = 'requires' | 'enhances' | 'replaces';
|
|
12
|
+
export type ChangeType = 'created' | 'updated' | 'deprecated' | 'removed' | 'restored';
|
|
13
|
+
|
|
14
|
+
export interface Feature {
|
|
15
|
+
id: number;
|
|
16
|
+
feature_key: string;
|
|
17
|
+
domain: string;
|
|
18
|
+
subdomain: string | null;
|
|
19
|
+
title: string;
|
|
20
|
+
description: string | null;
|
|
21
|
+
status: FeatureStatus;
|
|
22
|
+
priority: FeaturePriority;
|
|
23
|
+
portal_scope: string[]; // Parsed from JSON
|
|
24
|
+
created_at: string;
|
|
25
|
+
updated_at: string;
|
|
26
|
+
removed_at: string | null;
|
|
27
|
+
removed_reason: string | null;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export interface FeatureInput {
|
|
31
|
+
feature_key: string;
|
|
32
|
+
domain: string;
|
|
33
|
+
subdomain?: string;
|
|
34
|
+
title: string;
|
|
35
|
+
description?: string;
|
|
36
|
+
status?: FeatureStatus;
|
|
37
|
+
priority?: FeaturePriority;
|
|
38
|
+
portal_scope?: string[];
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export interface FeatureComponent {
|
|
42
|
+
id: number;
|
|
43
|
+
feature_id: number;
|
|
44
|
+
component_file: string;
|
|
45
|
+
component_name: string | null;
|
|
46
|
+
role: ComponentRole;
|
|
47
|
+
is_primary: boolean;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export interface FeatureProcedure {
|
|
51
|
+
id: number;
|
|
52
|
+
feature_id: number;
|
|
53
|
+
router_name: string;
|
|
54
|
+
procedure_name: string;
|
|
55
|
+
procedure_type: string | null;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export interface FeaturePage {
|
|
59
|
+
id: number;
|
|
60
|
+
feature_id: number;
|
|
61
|
+
page_route: string;
|
|
62
|
+
portal: string | null;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export interface FeatureDep {
|
|
66
|
+
id: number;
|
|
67
|
+
feature_id: number;
|
|
68
|
+
depends_on_feature_id: number;
|
|
69
|
+
dependency_type: DependencyType;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export interface FeatureChangeLog {
|
|
73
|
+
id: number;
|
|
74
|
+
feature_id: number;
|
|
75
|
+
change_type: ChangeType;
|
|
76
|
+
changed_by: string | null;
|
|
77
|
+
change_detail: string | null;
|
|
78
|
+
commit_hash: string | null;
|
|
79
|
+
created_at: string;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export interface FeatureWithCounts extends Feature {
|
|
83
|
+
component_count: number;
|
|
84
|
+
procedure_count: number;
|
|
85
|
+
page_count: number;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export interface FeatureDetail extends Feature {
|
|
89
|
+
components: FeatureComponent[];
|
|
90
|
+
procedures: FeatureProcedure[];
|
|
91
|
+
pages: FeaturePage[];
|
|
92
|
+
dependencies: FeatureDep[];
|
|
93
|
+
changelog: FeatureChangeLog[];
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
export interface ImpactItem {
|
|
97
|
+
feature: Feature;
|
|
98
|
+
affected_files: string[];
|
|
99
|
+
remaining_files: string[];
|
|
100
|
+
status: 'orphaned' | 'degraded' | 'unaffected';
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
export interface ImpactReport {
|
|
104
|
+
files_analyzed: string[];
|
|
105
|
+
orphaned: ImpactItem[];
|
|
106
|
+
degraded: ImpactItem[];
|
|
107
|
+
unaffected: ImpactItem[];
|
|
108
|
+
blocked: boolean;
|
|
109
|
+
block_reason: string | null;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
export interface ValidationItem {
|
|
113
|
+
feature: Feature;
|
|
114
|
+
missing_components: string[];
|
|
115
|
+
missing_procedures: { router: string; procedure: string }[];
|
|
116
|
+
missing_pages: string[];
|
|
117
|
+
status: 'alive' | 'orphaned' | 'degraded';
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
export interface ValidationReport {
|
|
121
|
+
alive: number;
|
|
122
|
+
orphaned: number;
|
|
123
|
+
degraded: number;
|
|
124
|
+
details: ValidationItem[];
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
export interface ParityItem {
|
|
128
|
+
feature_key: string;
|
|
129
|
+
title: string;
|
|
130
|
+
status: 'DONE' | 'GAP' | 'NEW';
|
|
131
|
+
old_files: string[];
|
|
132
|
+
new_files: string[];
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
export interface ParityReport {
|
|
136
|
+
done: ParityItem[];
|
|
137
|
+
gaps: ParityItem[];
|
|
138
|
+
new_features: ParityItem[];
|
|
139
|
+
parity_percentage: number;
|
|
140
|
+
}
|