@massu/core 1.10.8 → 1.12.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (178) hide show
  1. package/CHANGELOG.md +1334 -0
  2. package/dist/adapter.js +6 -4
  3. package/dist/cli.js +581 -448
  4. package/dist/detect/adapters/.bundle-shasums.json +4 -7
  5. package/dist/detect/adapters/rails.js +6 -4
  6. package/dist/detect/adapters/spring.js +6 -4
  7. package/dist/detect/adapters/tree-sitter-loader.js +6 -4
  8. package/dist/hooks/auto-learning-pipeline.js +24 -1
  9. package/dist/hooks/classify-failure.js +32 -3
  10. package/dist/hooks/cost-tracker.js +24 -0
  11. package/dist/hooks/fix-detector.js +97 -1
  12. package/dist/hooks/incident-pipeline.js +30 -1
  13. package/dist/hooks/intent-suggester.js +6 -3
  14. package/dist/hooks/post-edit-context.js +24 -1
  15. package/dist/hooks/post-tool-use.js +67 -19
  16. package/dist/hooks/pre-compact.js +28 -3
  17. package/dist/hooks/pre-delete-check.js +53 -27
  18. package/dist/hooks/pre-tool-use-gate.js +962 -0
  19. package/dist/hooks/quality-event.js +24 -0
  20. package/dist/hooks/rule-enforcement-pipeline.js +24 -1
  21. package/dist/hooks/security-gate.js +29 -16
  22. package/dist/hooks/session-end.js +31 -6
  23. package/dist/hooks/session-start.js +8709 -8459
  24. package/dist/hooks/user-prompt.js +25 -1
  25. package/package.json +15 -18
  26. package/commands/README.md +0 -134
  27. package/src/adapter.ts +0 -121
  28. package/src/adr-generator.ts +0 -292
  29. package/src/analytics.ts +0 -373
  30. package/src/audit-trail.ts +0 -450
  31. package/src/backfill-sessions.ts +0 -182
  32. package/src/changelog-generator.ts +0 -178
  33. package/src/cli.ts +0 -246
  34. package/src/cloud-sync.ts +0 -234
  35. package/src/commands/adapters.ts +0 -824
  36. package/src/commands/changelog.ts +0 -165
  37. package/src/commands/config-check-drift.ts +0 -133
  38. package/src/commands/config-refresh.ts +0 -396
  39. package/src/commands/config-upgrade.ts +0 -127
  40. package/src/commands/doctor.ts +0 -485
  41. package/src/commands/hook-runner.ts +0 -145
  42. package/src/commands/init.ts +0 -1823
  43. package/src/commands/install-commands.ts +0 -692
  44. package/src/commands/install-hooks.ts +0 -27
  45. package/src/commands/permissions.ts +0 -150
  46. package/src/commands/refresh-log.ts +0 -37
  47. package/src/commands/show-template.ts +0 -65
  48. package/src/commands/template-engine.ts +0 -301
  49. package/src/commands/watch.ts +0 -430
  50. package/src/config.ts +0 -765
  51. package/src/cost-tracker.ts +0 -355
  52. package/src/db.ts +0 -370
  53. package/src/dependency-scorer.ts +0 -337
  54. package/src/detect/adapters/aspnet.ts +0 -4
  55. package/src/detect/adapters/discover.ts +0 -469
  56. package/src/detect/adapters/file-sampler.ts +0 -225
  57. package/src/detect/adapters/go-chi.ts +0 -4
  58. package/src/detect/adapters/index.ts +0 -51
  59. package/src/detect/adapters/nextjs-trpc.ts +0 -166
  60. package/src/detect/adapters/parse-guard.ts +0 -133
  61. package/src/detect/adapters/phoenix.ts +0 -4
  62. package/src/detect/adapters/python-django.ts +0 -208
  63. package/src/detect/adapters/python-fastapi.ts +0 -223
  64. package/src/detect/adapters/python-flask.ts +0 -235
  65. package/src/detect/adapters/query-helpers.ts +0 -170
  66. package/src/detect/adapters/rails.ts +0 -4
  67. package/src/detect/adapters/runner.ts +0 -284
  68. package/src/detect/adapters/spring.ts +0 -4
  69. package/src/detect/adapters/swift-swiftui.ts +0 -171
  70. package/src/detect/adapters/tree-sitter-loader.ts +0 -517
  71. package/src/detect/adapters/types.ts +0 -191
  72. package/src/detect/codebase-introspector.ts +0 -210
  73. package/src/detect/domain-inferrer.ts +0 -197
  74. package/src/detect/drift.ts +0 -199
  75. package/src/detect/framework-detector.ts +0 -307
  76. package/src/detect/index.ts +0 -209
  77. package/src/detect/manifest-registry.ts +0 -261
  78. package/src/detect/migrate.ts +0 -362
  79. package/src/detect/monorepo-detector.ts +0 -348
  80. package/src/detect/package-detector.ts +0 -828
  81. package/src/detect/passthrough.ts +0 -108
  82. package/src/detect/regex-fallback.ts +0 -449
  83. package/src/detect/source-dir-detector.ts +0 -297
  84. package/src/detect/vr-command-map.ts +0 -167
  85. package/src/docs-map.json +0 -100
  86. package/src/docs-tools.ts +0 -519
  87. package/src/domains.ts +0 -182
  88. package/src/hooks/auto-learning-pipeline.ts +0 -228
  89. package/src/hooks/classify-failure.ts +0 -259
  90. package/src/hooks/cost-tracker.ts +0 -66
  91. package/src/hooks/fix-detector.ts +0 -186
  92. package/src/hooks/incident-pipeline.ts +0 -190
  93. package/src/hooks/intent-suggester.ts +0 -131
  94. package/src/hooks/post-edit-context.ts +0 -91
  95. package/src/hooks/post-tool-use.ts +0 -413
  96. package/src/hooks/pre-compact.ts +0 -168
  97. package/src/hooks/pre-delete-check.ts +0 -243
  98. package/src/hooks/quality-event.ts +0 -127
  99. package/src/hooks/rule-enforcement-pipeline.ts +0 -159
  100. package/src/hooks/security-gate.ts +0 -153
  101. package/src/hooks/session-end.ts +0 -467
  102. package/src/hooks/session-start.ts +0 -436
  103. package/src/hooks/user-prompt.ts +0 -157
  104. package/src/import-resolver.ts +0 -226
  105. package/src/knowledge-db.ts +0 -176
  106. package/src/knowledge-indexer.ts +0 -708
  107. package/src/knowledge-tools.ts +0 -1414
  108. package/src/lib/fileLock.ts +0 -203
  109. package/src/lib/framework-supports.ts +0 -85
  110. package/src/lib/gitToplevel.ts +0 -22
  111. package/src/lib/hook-registry.ts +0 -43
  112. package/src/lib/installLock.ts +0 -66
  113. package/src/lib/memory-path.ts +0 -49
  114. package/src/lib/pidLiveness.ts +0 -67
  115. package/src/lib/settings-local.ts +0 -110
  116. package/src/lib/sql-table-names.ts +0 -71
  117. package/src/license.ts +0 -547
  118. package/src/lsp/auto-detect.ts +0 -98
  119. package/src/lsp/client.ts +0 -776
  120. package/src/lsp/enrich.ts +0 -127
  121. package/src/lsp/types.ts +0 -221
  122. package/src/memory-db.ts +0 -1614
  123. package/src/memory-file-ingest.ts +0 -128
  124. package/src/memory-tools.ts +0 -424
  125. package/src/middleware-tree.ts +0 -71
  126. package/src/observability-tools.ts +0 -343
  127. package/src/observation-extractor.ts +0 -418
  128. package/src/page-deps.ts +0 -285
  129. package/src/permissions.ts +0 -422
  130. package/src/prompt-analyzer.ts +0 -332
  131. package/src/python/coupling-detector.ts +0 -125
  132. package/src/python/domain-enforcer.ts +0 -84
  133. package/src/python/impact-analyzer.ts +0 -96
  134. package/src/python/import-parser.ts +0 -244
  135. package/src/python/import-resolver.ts +0 -136
  136. package/src/python/migration-indexer.ts +0 -116
  137. package/src/python/migration-parser.ts +0 -332
  138. package/src/python/model-indexer.ts +0 -71
  139. package/src/python/model-parser.ts +0 -279
  140. package/src/python/route-indexer.ts +0 -59
  141. package/src/python/route-parser.ts +0 -317
  142. package/src/python-tools.ts +0 -630
  143. package/src/regression-detector.ts +0 -319
  144. package/src/rules.ts +0 -57
  145. package/src/schema-mapper.ts +0 -232
  146. package/src/security/adapter-origin.ts +0 -130
  147. package/src/security/adapter-verifier.ts +0 -319
  148. package/src/security/atomic-write.ts +0 -164
  149. package/src/security/fetcher.ts +0 -200
  150. package/src/security/install-tracking.ts +0 -319
  151. package/src/security/license-pubkey.generated.ts +0 -30
  152. package/src/security/license-response-verifier.ts +0 -149
  153. package/src/security/local-fingerprint.ts +0 -225
  154. package/src/security/manifest-cache.ts +0 -333
  155. package/src/security/manifest-schema.ts +0 -129
  156. package/src/security/registry-pubkey.generated.ts +0 -35
  157. package/src/security/telemetry.ts +0 -320
  158. package/src/security-scorer.ts +0 -405
  159. package/src/security-utils.ts +0 -133
  160. package/src/sentinel-db.ts +0 -580
  161. package/src/sentinel-scanner.ts +0 -406
  162. package/src/sentinel-tools.ts +0 -513
  163. package/src/sentinel-types.ts +0 -140
  164. package/src/server-dispatch.ts +0 -225
  165. package/src/server.ts +0 -107
  166. package/src/session-archiver.ts +0 -111
  167. package/src/session-state-generator.ts +0 -174
  168. package/src/team-knowledge.ts +0 -407
  169. package/src/tool-db-needs.ts +0 -232
  170. package/src/tools.ts +0 -1206
  171. package/src/transcript-parser.ts +0 -458
  172. package/src/trpc-index.ts +0 -215
  173. package/src/validate-features-runner.ts +0 -107
  174. package/src/validation-engine.ts +0 -358
  175. package/src/watch/daemon.ts +0 -385
  176. package/src/watch/lockfile-detector.ts +0 -65
  177. package/src/watch/paths.ts +0 -279
  178. package/src/watch/state.ts +0 -178
@@ -1,406 +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 { readFileSync, existsSync, readdirSync, statSync } from 'fs';
6
- import { resolve, join, basename, dirname, relative } from 'path';
7
- import {
8
- upsertFeature,
9
- linkComponent,
10
- linkProcedure,
11
- linkPage,
12
- logChange,
13
- } from './sentinel-db.ts';
14
- import type { FeatureInput, FeaturePriority } from './sentinel-types.ts';
15
- import { getConfig, getProjectRoot } from './config.ts';
16
- import { t } from './lib/sql-table-names.ts';
17
-
18
- // ============================================================
19
- // Sentinel: Feature Auto-Discovery Scanner
20
- // ============================================================
21
-
22
- interface DiscoveredFeature extends FeatureInput {
23
- components: { file: string; name: string | null; role: 'implementation' | 'ui' | 'data' | 'utility'; isPrimary: boolean }[];
24
- procedures: { router: string; procedure: string; type: string }[];
25
- pages: { route: string; portal: string | null }[];
26
- }
27
-
28
- // ============================================================
29
- // Domain inference from file path
30
- // ============================================================
31
-
32
- function inferDomain(filePath: string): string {
33
- const domains = getConfig().domains;
34
- const path = filePath.toLowerCase();
35
-
36
- // Try to match against configured domain page patterns and router patterns
37
- for (const domain of domains) {
38
- const domainLower = domain.name.toLowerCase();
39
- // Check if any router name appears in the path
40
- for (const router of domain.routers) {
41
- const routerLower = router.replace(/\*/g, '').toLowerCase();
42
- if (routerLower && path.includes(routerLower)) {
43
- return domainLower;
44
- }
45
- }
46
- // Check if domain name keyword appears in the path
47
- const nameWords = domainLower.split(/[\/\s]+/);
48
- for (const word of nameWords) {
49
- if (word.length > 2 && path.includes('/' + word + '/')) {
50
- return domainLower;
51
- }
52
- }
53
- }
54
-
55
- return 'system';
56
- }
57
-
58
- function inferSubdomain(routerName: string, procedureName: string): string {
59
- // Convert camelCase router to kebab-case subdomain
60
- return routerName.replace(/([A-Z])/g, '-$1').toLowerCase().replace(/^-/, '');
61
- }
62
-
63
- function kebabToTitle(kebab: string): string {
64
- return kebab
65
- .split(/[-_.]/)
66
- .map(w => w.charAt(0).toUpperCase() + w.slice(1))
67
- .join(' ');
68
- }
69
-
70
- // ============================================================
71
- // @feature annotation parser
72
- // ============================================================
73
-
74
- interface FeatureAnnotation {
75
- featureKey: string;
76
- title?: string;
77
- priority?: string;
78
- description?: string;
79
- }
80
-
81
- function parseFeatureAnnotations(source: string): FeatureAnnotation[] {
82
- const annotations: FeatureAnnotation[] = [];
83
- const regex = /@feature\s+([\w.-]+)/g;
84
- const titleRegex = /@feature-title\s+(.+)/g;
85
- const priorityRegex = /@feature-priority\s+(\w+)/g;
86
-
87
- let match;
88
- while ((match = regex.exec(source)) !== null) {
89
- const annotation: FeatureAnnotation = { featureKey: match[1] };
90
-
91
- // Look for companion annotations in nearby text
92
- const contextStart = Math.max(0, match.index - 200);
93
- const contextEnd = Math.min(source.length, match.index + 300);
94
- const context = source.substring(contextStart, contextEnd);
95
-
96
- const titleMatch = /@feature-title\s+(.+)/.exec(context);
97
- if (titleMatch) annotation.title = titleMatch[1].trim();
98
-
99
- const priorityMatch = /@feature-priority\s+(\w+)/.exec(context);
100
- if (priorityMatch) annotation.priority = priorityMatch[1].trim();
101
-
102
- annotations.push(annotation);
103
- }
104
-
105
- return annotations;
106
- }
107
-
108
- // ============================================================
109
- // Scanner: tRPC Procedures -> Features
110
- // ============================================================
111
-
112
- function scanTrpcProcedures(dataDb: Database.Database): DiscoveredFeature[] {
113
- const features: DiscoveredFeature[] = [];
114
- const featureMap = new Map<string, DiscoveredFeature>();
115
-
116
- const procedures = dataDb.prepare(`
117
- SELECT router_name, procedure_name, procedure_type, router_file
118
- FROM ${t('trpc_procedures')}
119
- ORDER BY router_name, procedure_name
120
- `).all() as { router_name: string; procedure_name: string; procedure_type: string; router_file: string }[];
121
-
122
- for (const proc of procedures) {
123
- const subdomain = inferSubdomain(proc.router_name, proc.procedure_name);
124
- const domain = inferDomain(proc.router_file);
125
-
126
- // Group procedures by router into features
127
- const featureKey = `${subdomain}.${proc.procedure_name}`;
128
- const routerFeatureKey = `${subdomain}.crud`;
129
-
130
- // Individual procedure-level feature
131
- if (!featureMap.has(featureKey)) {
132
- featureMap.set(featureKey, {
133
- feature_key: featureKey,
134
- domain,
135
- subdomain,
136
- title: `${kebabToTitle(subdomain)} - ${kebabToTitle(proc.procedure_name)}`,
137
- status: 'active',
138
- priority: 'standard',
139
- components: [],
140
- procedures: [],
141
- pages: [],
142
- });
143
- }
144
-
145
- const feature = featureMap.get(featureKey)!;
146
- feature.procedures.push({
147
- router: proc.router_name,
148
- procedure: proc.procedure_name,
149
- type: proc.procedure_type,
150
- });
151
-
152
- // Link the router file as a component
153
- if (!feature.components.some(c => c.file === proc.router_file)) {
154
- feature.components.push({
155
- file: proc.router_file,
156
- name: null,
157
- role: 'data',
158
- isPrimary: false,
159
- });
160
- }
161
- }
162
-
163
- return Array.from(featureMap.values());
164
- }
165
-
166
- // ============================================================
167
- // Scanner: Page Routes -> Features
168
- // ============================================================
169
-
170
- function scanPageRoutes(dataDb: Database.Database): DiscoveredFeature[] {
171
- const features: DiscoveredFeature[] = [];
172
-
173
- const pages = dataDb.prepare(`
174
- SELECT page_file, route, portal, components, hooks, routers
175
- FROM ${t('page_deps')}
176
- ORDER BY route
177
- `).all() as { page_file: string; route: string; portal: string; components: string; hooks: string; routers: string }[];
178
-
179
- for (const page of pages) {
180
- const domain = inferDomain(page.route);
181
- const routeParts = page.route.split('/').filter(Boolean);
182
- const subdomain = routeParts.length > 1 ? routeParts.slice(0, 2).join('-') : routeParts[0] || 'root';
183
-
184
- // Skip layout pages, error pages, etc.
185
- if (page.route.includes('error') || page.route.includes('not-found') || page.route === '/') continue;
186
-
187
- const featureKey = `page.${page.route.replace(/\//g, '.').replace(/^\.|\.$/g, '').replace(/\[(\w+)\]/g, '_$1_')}`;
188
-
189
- const components = JSON.parse(page.components || '[]') as string[];
190
- const routers = JSON.parse(page.routers || '[]') as string[];
191
-
192
- const feature: DiscoveredFeature = {
193
- feature_key: featureKey,
194
- domain,
195
- subdomain: subdomain.replace(/\//g, '-'),
196
- title: `Page: ${page.route}`,
197
- status: 'active',
198
- priority: 'standard',
199
- portal_scope: [page.portal],
200
- components: [
201
- { file: page.page_file, name: null, role: 'ui', isPrimary: true },
202
- ...components.map(c => ({ file: c, name: null as string | null, role: 'ui' as const, isPrimary: false })),
203
- ],
204
- procedures: [],
205
- pages: [{ route: page.route, portal: page.portal }],
206
- };
207
-
208
- features.push(feature);
209
- }
210
-
211
- return features;
212
- }
213
-
214
- // ============================================================
215
- // Scanner: Component Exports -> Features
216
- // ============================================================
217
-
218
- function scanComponentExports(dataDb: Database.Database): DiscoveredFeature[] {
219
- const features: DiscoveredFeature[] = [];
220
-
221
- // Scan component directories for interactive features
222
- const config = getConfig();
223
- const projectRoot = getProjectRoot();
224
- const componentsBase = config.paths.components ?? (config.paths.source + '/components');
225
-
226
- // Build component dirs from domain names + a generic scan
227
- const componentDirs: string[] = [];
228
- const basePath = resolve(projectRoot, componentsBase);
229
- if (existsSync(basePath)) {
230
- try {
231
- const entries = readdirSync(basePath, { withFileTypes: true });
232
- for (const entry of entries) {
233
- if (entry.isDirectory()) {
234
- componentDirs.push(componentsBase + '/' + entry.name);
235
- }
236
- }
237
- } catch {
238
- // Skip if unreadable
239
- }
240
- }
241
-
242
- for (const dir of componentDirs) {
243
- const absDir = resolve(projectRoot, dir);
244
- if (!existsSync(absDir)) continue;
245
-
246
- const files = walkDir(absDir).filter(f => f.endsWith('.tsx') || f.endsWith('.ts'));
247
- for (const file of files) {
248
- const relPath = relative(projectRoot, file);
249
- const source = readFileSync(file, 'utf-8');
250
-
251
- // Parse @feature annotations
252
- const annotations = parseFeatureAnnotations(source);
253
- if (annotations.length > 0) {
254
- for (const ann of annotations) {
255
- const domain = inferDomain(relPath);
256
- const parts = ann.featureKey.split('.');
257
- const feature: DiscoveredFeature = {
258
- feature_key: ann.featureKey,
259
- domain,
260
- subdomain: parts[0],
261
- title: ann.title || kebabToTitle(ann.featureKey),
262
- description: ann.description,
263
- status: 'active',
264
- priority: (ann.priority as FeaturePriority) || 'standard',
265
- components: [{ file: relPath, name: null, role: 'implementation', isPrimary: true }],
266
- procedures: [],
267
- pages: [],
268
- };
269
- features.push(feature);
270
- }
271
- }
272
-
273
- // Detect interactive features: exported functions with mutation/handler patterns
274
- const hasHandlers = /onClick|onSubmit|useMutation|api\.\w+\.\w+\.use/.test(source);
275
- const exportMatch = /export\s+(?:default\s+)?(?:function|const)\s+(\w+)/.exec(source);
276
-
277
- if (hasHandlers && exportMatch) {
278
- const componentName = exportMatch[1];
279
- const domain = inferDomain(relPath);
280
- const subdomain = basename(dirname(relPath));
281
- const featureKey = `component.${subdomain}.${componentName.replace(/([A-Z])/g, '-$1').toLowerCase().replace(/^-/, '')}`;
282
-
283
- // Only add if not already covered by annotation
284
- if (!annotations.some(a => a.featureKey === featureKey)) {
285
- features.push({
286
- feature_key: featureKey,
287
- domain,
288
- subdomain,
289
- title: `${componentName}`,
290
- status: 'active',
291
- priority: 'standard',
292
- components: [{ file: relPath, name: componentName, role: 'implementation', isPrimary: true }],
293
- procedures: [],
294
- pages: [],
295
- });
296
- }
297
- }
298
- }
299
- }
300
-
301
- return features;
302
- }
303
-
304
- // ============================================================
305
- // Utility: Walk directory recursively
306
- // ============================================================
307
-
308
- function walkDir(dir: string): string[] {
309
- const results: string[] = [];
310
- try {
311
- const entries = readdirSync(dir);
312
- for (const entry of entries) {
313
- const fullPath = join(dir, entry);
314
- try {
315
- const stat = statSync(fullPath);
316
- if (stat.isDirectory()) {
317
- results.push(...walkDir(fullPath));
318
- } else {
319
- results.push(fullPath);
320
- }
321
- } catch {
322
- // Skip unreadable entries
323
- }
324
- }
325
- } catch {
326
- // Skip unreadable directories
327
- }
328
- return results;
329
- }
330
-
331
- // ============================================================
332
- // Main Scanner Entry Point
333
- // ============================================================
334
-
335
- export interface ScanResult {
336
- totalDiscovered: number;
337
- fromProcedures: number;
338
- fromPages: number;
339
- fromComponents: number;
340
- registered: number;
341
- }
342
-
343
- export function runFeatureScan(dataDb: Database.Database): ScanResult {
344
- const procedureFeatures = scanTrpcProcedures(dataDb);
345
- const pageFeatures = scanPageRoutes(dataDb);
346
- const componentFeatures = scanComponentExports(dataDb);
347
-
348
- // Merge: annotations take priority, then components, then pages, then procedures
349
- const allFeatures = new Map<string, DiscoveredFeature>();
350
-
351
- // Add procedures first (lowest priority)
352
- for (const f of procedureFeatures) {
353
- allFeatures.set(f.feature_key, f);
354
- }
355
-
356
- // Add pages
357
- for (const f of pageFeatures) {
358
- allFeatures.set(f.feature_key, f);
359
- }
360
-
361
- // Add components (highest priority from scanning)
362
- for (const f of componentFeatures) {
363
- allFeatures.set(f.feature_key, f);
364
- }
365
-
366
- // Register all discovered features
367
- let registered = 0;
368
- for (const feature of allFeatures.values()) {
369
- const featureId = upsertFeature(dataDb, {
370
- feature_key: feature.feature_key,
371
- domain: feature.domain,
372
- subdomain: feature.subdomain,
373
- title: feature.title,
374
- description: feature.description,
375
- status: feature.status,
376
- priority: feature.priority,
377
- portal_scope: feature.portal_scope,
378
- });
379
-
380
- // Link components
381
- for (const comp of feature.components) {
382
- linkComponent(dataDb, featureId, comp.file, comp.name, comp.role, comp.isPrimary);
383
- }
384
-
385
- // Link procedures
386
- for (const proc of feature.procedures) {
387
- linkProcedure(dataDb, featureId, proc.router, proc.procedure, proc.type);
388
- }
389
-
390
- // Link pages
391
- for (const page of feature.pages) {
392
- linkPage(dataDb, featureId, page.route, page.portal ?? undefined);
393
- }
394
-
395
- logChange(dataDb, featureId, 'created', 'Auto-discovered by sentinel scanner', undefined, 'scanner');
396
- registered++;
397
- }
398
-
399
- return {
400
- totalDiscovered: allFeatures.size,
401
- fromProcedures: procedureFeatures.length,
402
- fromPages: pageFeatures.length,
403
- fromComponents: componentFeatures.length,
404
- registered,
405
- };
406
- }